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地址的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #define ETH_NAME "wlan0" 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
因为这里的信息是字符串,并以冒号分割符来分隔信息段,因此,需要先编写一个能拆分字符串的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #define REQUEST_INFO "new_client_ip" #define REPLAY_INFO "server_ip" #define INFO_SPLIT std::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()); } } 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广播前,需要先设置该套接字为广播类型,这里将此部分代码封装为一个函数
1 2 3 4 5 6 7 8 9 10 11 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地址,然后即可中止广播
具体代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 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 ; } 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 ; 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); 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),这样在服务端没有回复数据的情况下,客户端也不会一直等待,能够再次发送自己的数据
具体的代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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请求进行处理。
1 2 3 4 5 6 7 8 9 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信息按照规定的格式发送出去
具体的代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 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); 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)
具体的代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 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)) ; listen(listenfd, 20 ); printf ("[%s] Accepting connections... \n" , __func__); 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; if ((fd == listenfd) &&(events[i].events & EPOLLIN)) { 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); epoll_set_fd_a_event(epollfd, EPOLL_CTL_ADD, clifd, EPOLLIN); } } 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 { printf ("[%s] read message from fd:%d ---> %s\n" , __func__, fd, buf); } } } } close(epollfd); }
为epoll中的某个fd添加、修改或删除某个事件,这里封装成了一个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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连接,另外,服务端也可以处理多个客户端的请求:
2)单个客户端连接并中止后,另一个客户端再次连接服务端
3)客户端先启动后,服务端再启动,客户端依然能在服务端启动后连接到服务端
5 总结 本篇介绍了在TCP通信中,客户端通过UDP广播,实现自动获取服务端的IP地址,并进行TCP连接的具体方法,并通过代码实现,来测试此方案是实际效果,为了使服务端能够处理多个客户端的请求,这里使用了多线程编程,以及epoll机制来实现多客户端的处理。