当前位置:首页 > 公众号精选 > 码农爱学习
[导读]在进行socket通信开发时,一般会用到TCP或UDP这两种传输层协议,UDP(User Datagram Protocol)是一种面向无连接的协议,在数据发送前,不需要提前建立连接,它可以更高效地传输数据,但可靠性无法保证。TCP(Transmission Control Protocol)是一种面向连接的协议,一个应用程序开始向另一个应用程序发送数据之前,必须先进行握手连接,以保证数据的可靠传输。所以,对于数据可靠性要求较高的场合,一般使用TCP协议通信。



1 问题引出

在进行socket通信开发时,一般会用到TCP或UDP这两种传输层协议,UDP(User Datagram Protocol)是一种面向无连接的协议,在数据发送前,不需要提前建立连接,它可以更高效地传输数据,但可靠性无法保证。TCP(Transmission Control Protocol)是一种面向连接的协议,一个应用程序开始向另一个应用程序发送数据之前,必须先进行握手连接,以保证数据的可靠传输。所以,对于数据可靠性要求较高的场合,一般使用TCP协议通信。

使用TCP方式的socket编程,客户端需要知道服务端的IP和端口号,然后向服务端申请连接,对于端口号,可以事先固定一个特定的端口号,但对于IP地址,在实际的开发使用中,比如嵌入式开发中,两个连网的硬件需要进行TCP通信,在建立通信,客户端硬件是不知道服务端硬件IP的(除了程序开发阶段,事先知道IP,将IP写死到程序中),因为通常情况下IP是由路由器分配的,不是一个固定值,这种情况,客户端如何自动获取服务端的IP来建立TCP通信呢?

2 解决方案

本篇就来实现一种解决方法:在建立TCP通信前,可以先通过UDP通信来获取服务端的IP

UDP具有广播功能,客户端可以通过UDP广播,向局域网内的所有设置发送广播包,可以事先定义一种广播协议,服务端在收到特定的广播包后,判断为有客户端需要请求连接,则将自己的IP地址发送出去,当客户端收到服务端发出的IP信息后,即可通过解析到的服务端IP地址,实现与服务端进行TCP连接。

3 编程实现

在进行客户端与服务端的socket编程之前,先实现一些两个程序都会用到的功能代码。

3.1 公共代码块

服务端要将自己的IP发给客户端,首先要能自动获取到自己的IP,客户端在进行UDP广播时,也可以将自己的IP也一起发出去作为附加信息,所以,需要先实现一个获取自己IP地址的函数:

#define ETH_NAME "wlan0" //获取本机ip(根据实际情况修改ETH_NAME) bool get_local_ip(std::string &ip) { int sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock == -1) 
    { printf("[%s] socket err!\n", __func__); return false;
    } struct ifreq ifr; memcpy(&ifr.ifr_name, ETH_NAME, IFNAMSIZ);
    ifr.ifr_name[IFNAMSIZ - 1] = 0; if (ioctl(sock, SIOCGIFADDR, &ifr) < 0) 
    { printf("[%s] ioctl err!\n", __func__); return false;
    } struct sockaddr_in sin; memcpy(&sin, &ifr.ifr_addr, sizeof(sin));
    ip = std::string(inet_ntoa(sin.sin_addr)); return true;
}

在进行UDP广播时,客户端与服务端需要事先规定一种信息格式,当格式符合时,说明是客户端要请求IP信息,以及服务端返回的IP信息,本篇的测试程序,规定一种比较简单的方式:

  • 客户端请求服务端IP的信息格式为:字符串"new_client_ip"+分隔符“:”+客户端自己的IP
  • 服务端回复自己的IP的信息格式为:字符串"server_ip"+分隔符“:”+服务端自己的IP

因为这里的信息是字符串,并以冒号分割符来分隔信息段,因此,需要先编写一个能拆分字符串的函数:

#define REQUEST_INFO "new_client_ip" //客户端发送的广播信息头 #define REPLAY_INFO "server_ip" //服务端回复的信息头 #define INFO_SPLIT std::string(":") //信息分割符 //对c字符串按照指定分割符拆分为多个string字符串 void cstr_split(char *cstr, vector<std::string> &res, std::string split = INFO_SPLIT) {        
    res.clear(); char *token = strtok(cstr, split.c_str()); while(token)
    {
        res.push_back(std::string(token)); printf("[%s] token:%s\n", __func__, token);
        token = strtok(NULL, split.c_str());
    }
} //---------使用示例: 解析服务器的ip---------- char recvbuf[100]={0}; //...接收服务端返回的信息 vector<std::string> recvInfo;
cstr_split(recvbuf, recvInfo); if(recvInfo.size() == 2 && recvInfo[0] == REPLAY_INFO)
{ std::string serverIP = recvInfo[1]; //...后续处理 

在进行UDP广播前,需要先设置该套接字为广播类型,这里将此部分代码封装为一个函数

//设置该套接字为广播类型  void set_sockopt_broadcast(int socket, bool bEnable = true) { const int opt = (int)bEnable; int nb = setsockopt(socket, SOL_SOCKET, SO_BROADCAST, (char *)&opt, sizeof(opt)); if(nb == -1)  
    { printf("[%s] set socket error\n", __func__); return;  
    }  
}

3.2 客户端程序

3.2.1 客户端进行UDP广播

客户端进行UDP广播的主要逻辑是:

  • 获取自己的IP(作为UDP广播的附加信息)
  • 创建一个socket,类型为UDP数据报(SOCK_DGRAM)
  • sockaddrd的IP设置为广播IP(INADDR_BROADCAST, 255.255.255.255)
  • 为socket添加广播属性(setsockopt,SO_BROADCAST)
  • 发送UDP广播报(sendto)
  • 接收UDP回复信息(recvfrom),接收设置超时时间(setsockopt,SO_RCVTIMEO),没收到服务端回复则继续广播
  • 收到服务端回复后,解析出服务端的IP地址,然后即可中止广播

具体代码实现如下:

int main() { bool bHasGetServerIP = false;
    thread th_tcp_client; std::string localIP = "xxx"; if (true == get_local_ip(localIP))
    { printf("[%s] localIP: [%s] %s\n", __func__, ETH_NAME, localIP.c_str());
    } int udpClientSocket = -1; if ((udpClientSocket = socket(AF_INET, SOCK_DGRAM, 0)) == -1)   
    { printf("[%s] socket error\n", __func__); return false;  
    } struct sockaddr_in udpClientAddr; memset(&udpClientAddr, 0, sizeof(struct sockaddr_in));  
    udpClientAddr.sin_family=AF_INET;  
    udpClientAddr.sin_addr.s_addr=htonl(INADDR_BROADCAST);  
    udpClientAddr.sin_port=htons(6000); int nlen=sizeof(udpClientAddr);  
    
    set_sockopt_broadcast(udpClientSocket); while(1)  
    {  
        sleep(1); if(bHasGetServerIP)
        { continue; //获取到服务器的IP后, 就不需要再广播了 } //从广播地址发送消息  std::string smsg = REQUEST_INFO + INFO_SPLIT + localIP; int ret=sendto(udpClientSocket, smsg.c_str(), smsg.length(), 0, (sockaddr*)&udpClientAddr, nlen); if(ret<0)  
        { printf("[%s] sendto error, ret: %d\n", __func__, ret);  
        } else { printf("[%s] broadcast ok, msg: %s\n", __func__, smsg.c_str()); /* 设置阻塞超时 */ struct timeval timeOut; timeOut.tv_sec = 2; //设置2s超时 timeOut.tv_usec = 0; if (setsockopt(udpClientSocket, SOL_SOCKET, SO_RCVTIMEO, &timeOut, sizeof(timeOut)) < 0)
            { printf("[%s] time out setting failed\n", __func__); return 0;
            } //再接收数据 char recvbuf[100]={0}; int num = recvfrom(udpClientSocket, recvbuf, 100, 0, (struct sockaddr*)&udpClientAddr,(socklen_t*)&nlen); if (num > 0)
            { printf("[%s] receive server reply:%s\n", __func__, recvbuf); //解析服务器的ip vector<std::string> recvInfo;
                cstr_split(recvbuf, recvInfo); if(recvInfo.size() == 2 && recvInfo[0] == REPLAY_INFO)
                { std::string serverIP = recvInfo[1];
                    bHasGetServerIP = true;
                    th_tcp_client = thread(tcp_client_thread, serverIP, localIP);
                    th_tcp_client.join();
                }
            } else if (num == -1 && errno == EAGAIN)
            { printf("[%s] receive timeout\n", __func__);
            }
        }  
    } return 0;  
}

3.2.2 客户端进行TCP连接

在获取到服务端的IP后,再开启一个线程,与服务端建立TCP连接,并进行数据通信,该线程的实现逻辑如下:

  • 创建一个socket,类型为TCP数据流(SOCK_STREAM)
  • sockaddrd的IP设置为刚才获取的服务端的IP(serverIP,例如192.168.1.101)
  • 向服务端请求连接(connect)
  • 连接成功之后,可以发送自定义的数据(send),这里发送的一串字母"abcdefg"加上自己的IP地址
  • 如果服务端会还会回复信息,可以进行接收(recv),这里的接收设置为非阻塞模式(MSG_DONTWAIT),这样在服务端没有回复数据的情况下,客户端也不会一直等待,能够再次发送自己的数据

具体的代码实现如下:

void tcp_client_thread(std::string serverIP, std::string localIP) { printf("[%s] in, prepare connect serverIP:%s\n", __func__, serverIP.c_str()); //创建客户端套接字文件 int tcpClientSocket= socket(AF_INET, SOCK_STREAM, 0); //初始化服务器端口地址 struct sockaddr_in servaddr; bzero(&servaddr, sizeof(servaddr)) ;
	servaddr.sin_family= AF_INET;
	inet_pton(AF_INET, serverIP.c_str(), &servaddr.sin_addr);
	servaddr.sin_port= htons(SERV_PORT); //请求连接 connect(tcpClientSocket, (struct sockaddr*)&servaddr, sizeof (servaddr)); //要向服务器发送的信息 char buf [MAXLINE]; std::string msg = "abcdefg" + std::string("(") + localIP + std::string(")"); while(1)
    { //发送数据 send(tcpClientSocket, msg.c_str(), msg.length(),0); printf("[%s] send to server: %s\n", __func__, msg.c_str()); //接收服务器返回的数据 int n= recv(tcpClientSocket, buf, MAXLINE, MSG_DONTWAIT); //非阻塞读取 if(n>0)
        { printf("[%s] Response from server: %s\n", __func__, buf);
        }
        
        sleep(2);
    } //关闭连接 close(tcpClientSocket) ;
}

3.3 服务端程序

服务端程序,主要设计了2个线程来分别实现对客户端UDP广播的处理和对客户端TCP连接的处理,两个功能独立开来,可以实现对多个客户端的UDP请求和TCP请求进行处理。

int main() { thread th1(recv_broadcast_thread); thread th2(tcp_server_thread);
    th1.join();
    th2.join(); return 0;  
}

3.3.1 服务端处理UDP广播

接收客户端广播信息的处理线程的主要逻辑为:

  • 获取自己的IP(用于回复给客户端,客户端获取到IP后进行TCP连接)
  • 创建一个socket,类型为UDP数据报(SOCK_DGRAM)
  • sockaddrd的IP设置为接收所有IP(INADDR_ANY,0.0.0.0),并进行绑定(bind)
  • 为socket添加广播属性(setsockopt,SO_BROADCAST)
  • 接收UDP广播信息(recvfrom),这里是默认的阻塞接收,没有广播信息则一直等待
  • 收到客户端的UDP广播信息后,解析信息,判断确实是要获取IP后,将自己的IP信息按照规定的格式发送出去

具体的代码实现如下:

//接收客户端广播信息的处理线程, 收到客户端的UDP广播后, 将自己(服务端)的IP发送回去 void recv_broadcast_thread() { std::string localIP = ""; if (true == get_local_ip(localIP))
    { printf("[%s] localIP: [%s] %s\n", __func__, ETH_NAME, localIP.c_str());
    } else { printf("[%s] get local ip err!\n", __func__); return;
    } int sock = -1; if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) == -1)   
    { printf("[%s] socket error\n", __func__); return;  
    } struct sockaddr_in udpServerAddr; bzero(&udpServerAddr, sizeof(struct sockaddr_in));  
    udpServerAddr.sin_family = AF_INET;  
    udpServerAddr.sin_addr.s_addr = htonl(INADDR_ANY);  
    udpServerAddr.sin_port = htons(6000); int len = sizeof(sockaddr_in); if(bind(sock,(struct sockaddr *)&(udpServerAddr), sizeof(struct sockaddr_in)) == -1)   
    { printf("[%s] bind error\n", __func__); return;  
    }  
    
    set_sockopt_broadcast(sock); char smsg[100] = {0}; while(1)  
    { //从广播地址接收消息  int ret=recvfrom(sock, smsg, 100, 0, (struct sockaddr*)&udpServerAddr, (socklen_t*)&len); if(ret<=0)  
        { printf("[%s] read error, ret:%d\n", __func__, ret);  
        } else { printf("[%s]receive: %s\n", __func__, smsg); vector<std::string> recvInfo;
            cstr_split(smsg, recvInfo); //将自己的IP回应给请求的客户端 if(recvInfo.size() == 2 && recvInfo[0] == REQUEST_INFO)
            { std::string clientIP = recvInfo[1]; std::string replyInfo = REPLAY_INFO + INFO_SPLIT + localIP;
                
                ret = sendto(sock, replyInfo.c_str(), replyInfo.length(), 0, (struct sockaddr *)&udpServerAddr, len); if(ret<0)  
                { printf("[%s] sendto error, ret: %d\n", __func__, ret);  
                } else { printf("[%s] reply ok, msg: %s\n", __func__, replyInfo.c_str());   
                }  
            }
        }  
 
        sleep(1);  
    } 
}

3.3.2 服务端处理客户端的TCP连接

TCP服务器线程, 用于接受客户端的连接, 主要逻辑如下:

  • 创建一个socket,命名为listenfd,类型为TCP数据流(SOCK_STREAM)
  • sockaddrd的IP设置为接收所有IP(INADDR_ANY,0.0.0.0),并进行绑定(bind)
  • 监听,并设置最大连接数(listen)
  • 创建一个epoll,来处理多客户端请求时(epoll_create)
  • 将TCP socket添加到epoll进行监听(epoll_ctl,EPOLLIN)
  • epoll等待事件到来(epoll_wait)
  • epoll处理到来的事件
  • 如果到来的是listenfd,说明有新的客户端请求连接,TCP服务端则接受请求(accept),然后将对应的客户端fd添加到epoll进行监听(epoll_ctl,EPOLLIN)
  • 如果到来的不是listenfd,说明有已连接的客户端发来的数据信息,则读取信息(read)

具体的代码实现如下:

//TCP服务器线程, 用于接受客户端的连接, 并接收客户端的信息 void tcp_server_thread() { //创建服务器端套接字文件 int listenfd=socket(AF_INET, SOCK_STREAM, 0); //初始化服务器端口地址 struct sockaddr_in tcpServerAddr; bzero(&tcpServerAddr, sizeof(tcpServerAddr));
	tcpServerAddr.sin_family=AF_INET;
	tcpServerAddr.sin_addr.s_addr= htonl(INADDR_ANY);
	tcpServerAddr.sin_port=htons(SERV_PORT); //将套接字文件与服务器端口地址绑定 bind(listenfd, (struct sockaddr *)&tcpServerAddr, sizeof (tcpServerAddr)) ; //监听,并设置最大连接数为20 listen(listenfd, 20); printf("[%s] Accepting connections... \n", __func__); //通过epoll来监控多个客户端的请求 int epollfd; struct epoll_event events[EPOLLEVENTS]; int num; char buf[MAXSIZE]; memset(buf,0,MAXSIZE);
    epollfd = epoll_create(FDSIZE); printf("[%s] create epollfd:%d\n", __func__, epollfd); //添加监听描述符事件 epoll_set_fd_a_event(epollfd, EPOLL_CTL_ADD, listenfd, EPOLLIN); while(1)
    { //获取已经准备好的描述符事件 printf("[%s] epollfd:%d epoll_wait...\n", __func__, epollfd);
        num = epoll_wait(epollfd,events,EPOLLEVENTS,-1); for (int i = 0;i < num;i++) { int fd = events[i].data.fd; //listenfd说明有新的客户端请求连接 if ((fd == listenfd) &&(events[i].events & EPOLLIN))
            { //accept客户端的请求 struct sockaddr_in cliaddr; socklen_t cliaddrlen = sizeof(cliaddr); int clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen); if (clifd == -1)
                {
                    perror("accpet error:");
                } else { printf("[%s] accept a new client(fd:%d): %s:%d\n",
                           __func__, clifd, inet_ntoa(cliaddr.sin_addr), cliaddr.sin_port); //将客户端fd添加到epoll进行监听 epoll_set_fd_a_event(epollfd, EPOLL_CTL_ADD, clifd, EPOLLIN);
                }
            } //收到已连接的客户端fd的消息 else if (events[i].events & EPOLLIN)
            { memset(buf,0,MAXSIZE); //读取客户端的消息 int nread = read(fd,buf,MAXSIZE); if (nread == -1)
                {
                    perror("read error:");
                    close(fd);
                    epoll_set_fd_a_event(epollfd, EPOLL_CTL_DEL, fd, EPOLLIN);
                } else if (nread == 0)
                { printf("[%s] client(fd:%d) close.\n", __func__, fd);
                    close(fd);
                    epoll_set_fd_a_event(epollfd, EPOLL_CTL_DEL, fd, EPOLLIN);
                } else { //将客户端的消息打印处理, 并表明是哪里客户端fd发来的消息 printf("[%s] read message from fd:%d ---> %s\n", __func__, fd, buf);
                }
            }
        }
    }

    close(epollfd);
}

为epoll中的某个fd添加、修改或删除某个事件,这里封装成了一个函数:

//为epoll中的某个fd添加/修改/删除某个事件 bool epoll_set_fd_a_event(int epollfd, int op, int fd, int event) { if (EPOLL_CTL_ADD == op || EPOLL_CTL_MOD == op || EPOLL_CTL_DEL == op)
    { struct epoll_event ev; ev.events = event;
        ev.data.fd = fd;
        epoll_ctl(epollfd, op, fd, &ev); return true;
    } else { printf("[%s] err op:%d\n", __func__, op); return false;
    }
}

4 测试结果

这里测试了4种不同的情况,来验证客户端可以自动获取到服务端的IP,并进行TCP连接,另外,服务端也可以处理多个客户端的请求:

  • 1)单个客户端连接服务端
  • 2)单个客户端连接并中止后,另一个客户端再次连接服务端
  • 3)客户端先启动后,服务端再启动,客户端依然能在服务端启动后连接到服务端
  • 4)两个客户端现后进行连接服务端

5 总结

本篇介绍了在TCP通信中,客户端通过UDP广播,实现自动获取服务端的IP地址,并进行TCP连接的具体方法,并通过代码实现,来测试此方案是实际效果,为了使服务端能够处理多个客户端的请求,这里使用了多线程编程,以及epoll机制来实现多客户端的处理。


本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

9月2日消息,不造车的华为或将催生出更大的独角兽公司,随着阿维塔和赛力斯的入局,华为引望愈发显得引人瞩目。

关键字: 阿维塔 塞力斯 华为

加利福尼亚州圣克拉拉县2024年8月30日 /美通社/ -- 数字化转型技术解决方案公司Trianz今天宣布,该公司与Amazon Web Services (AWS)签订了...

关键字: AWS AN BSP 数字化

伦敦2024年8月29日 /美通社/ -- 英国汽车技术公司SODA.Auto推出其旗舰产品SODA V,这是全球首款涵盖汽车工程师从创意到认证的所有需求的工具,可用于创建软件定义汽车。 SODA V工具的开发耗时1.5...

关键字: 汽车 人工智能 智能驱动 BSP

北京2024年8月28日 /美通社/ -- 越来越多用户希望企业业务能7×24不间断运行,同时企业却面临越来越多业务中断的风险,如企业系统复杂性的增加,频繁的功能更新和发布等。如何确保业务连续性,提升韧性,成...

关键字: 亚马逊 解密 控制平面 BSP

8月30日消息,据媒体报道,腾讯和网易近期正在缩减他们对日本游戏市场的投资。

关键字: 腾讯 编码器 CPU

8月28日消息,今天上午,2024中国国际大数据产业博览会开幕式在贵阳举行,华为董事、质量流程IT总裁陶景文发表了演讲。

关键字: 华为 12nm EDA 半导体

8月28日消息,在2024中国国际大数据产业博览会上,华为常务董事、华为云CEO张平安发表演讲称,数字世界的话语权最终是由生态的繁荣决定的。

关键字: 华为 12nm 手机 卫星通信

要点: 有效应对环境变化,经营业绩稳中有升 落实提质增效举措,毛利润率延续升势 战略布局成效显著,战新业务引领增长 以科技创新为引领,提升企业核心竞争力 坚持高质量发展策略,塑强核心竞争优势...

关键字: 通信 BSP 电信运营商 数字经济

北京2024年8月27日 /美通社/ -- 8月21日,由中央广播电视总台与中国电影电视技术学会联合牵头组建的NVI技术创新联盟在BIRTV2024超高清全产业链发展研讨会上宣布正式成立。 活动现场 NVI技术创新联...

关键字: VI 传输协议 音频 BSP

北京2024年8月27日 /美通社/ -- 在8月23日举办的2024年长三角生态绿色一体化发展示范区联合招商会上,软通动力信息技术(集团)股份有限公司(以下简称"软通动力")与长三角投资(上海)有限...

关键字: BSP 信息技术
关闭