當前位置:首頁 > 公眾號精選 > 碼農(nóng)愛學習
[導讀]在進行socket通信開發(fā)時,一般會用到TCP或UDP這兩種傳輸層協(xié)議,UDP(User Datagram Protocol)是一種面向無連接的協(xié)議,在數(shù)據(jù)發(fā)送前,不需要提前建立連接,它可以更高效地傳輸數(shù)據(jù),但可靠性無法保證。TCP(Transmission Control Protocol)是一種面向連接的協(xié)議,一個應用程序開始向另一個應用程序發(fā)送數(shù)據(jù)之前,必須先進行握手連接,以保證數(shù)據(jù)的可靠傳輸。所以,對于數(shù)據(jù)可靠性要求較高的場合,一般使用TCP協(xié)議通信。



1 問題引出

在進行socket通信開發(fā)時,一般會用到TCP或UDP這兩種傳輸層協(xié)議,UDP(User Datagram Protocol)是一種面向無連接的協(xié)議,在數(shù)據(jù)發(fā)送前,不需要提前建立連接,它可以更高效地傳輸數(shù)據(jù),但可靠性無法保證。TCP(Transmission Control Protocol)是一種面向連接的協(xié)議,一個應用程序開始向另一個應用程序發(fā)送數(shù)據(jù)之前,必須先進行握手連接,以保證數(shù)據(jù)的可靠傳輸。所以,對于數(shù)據(jù)可靠性要求較高的場合,一般使用TCP協(xié)議通信。

使用TCP方式的socket編程,客戶端需要知道服務端的IP和端口號,然后向服務端申請連接,對于端口號,可以事先固定一個特定的端口號,但對于IP地址,在實際的開發(fā)使用中,比如嵌入式開發(fā)中,兩個連網(wǎng)的硬件需要進行TCP通信,在建立通信,客戶端硬件是不知道服務端硬件IP的(除了程序開發(fā)階段,事先知道IP,將IP寫死到程序中),因為通常情況下IP是由路由器分配的,不是一個固定值,這種情況,客戶端如何自動獲取服務端的IP來建立TCP通信呢?

2 解決方案

本篇就來實現(xiàn)一種解決方法:在建立TCP通信前,可以先通過UDP通信來獲取服務端的IP。

UDP具有廣播功能,客戶端可以通過UDP廣播,向局域網(wǎng)內(nèi)的所有設置發(fā)送廣播包,可以事先定義一種廣播協(xié)議,服務端在收到特定的廣播包后,判斷為有客戶端需要請求連接,則將自己的IP地址發(fā)送出去,當客戶端收到服務端發(fā)出的IP信息后,即可通過解析到的服務端IP地址,實現(xiàn)與服務端進行TCP連接。

3 編程實現(xiàn)

在進行客戶端與服務端的socket編程之前,先實現(xiàn)一些兩個程序都會用到的功能代碼。

3.1 公共代碼塊

服務端要將自己的IP發(fā)給客戶端,首先要能自動獲取到自己的IP,客戶端在進行UDP廣播時,也可以將自己的IP也一起發(fā)出去作為附加信息,所以,需要先實現(xiàn)一個獲取自己IP地址的函數(shù):

#define ETH_NAME "wlan0" //獲取本機ip(根據(jù)實際情況修改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廣播時,客戶端與服務端需要事先規(guī)定一種信息格式,當格式符合時,說明是客戶端要請求IP信息,以及服務端返回的IP信息,本篇的測試程序,規(guī)定一種比較簡單的方式:

  • 客戶端請求服務端IP的信息格式為:字符串"new_client_ip"+分隔符“:”+客戶端自己的IP
  • 服務端回復自己的IP的信息格式為:字符串"server_ip"+分隔符“:”+服務端自己的IP

因為這里的信息是字符串,并以冒號分割符來分隔信息段,因此,需要先編寫一個能拆分字符串的函數(shù):

#define REQUEST_INFO "new_client_ip" //客戶端發(fā)送的廣播信息頭 #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]; //...后續(xù)處理 

在進行UDP廣播前,需要先設置該套接字為廣播類型,這里將此部分代碼封裝為一個函數(shù)

//設置該套接字為廣播類型  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廣播的附加信息)
  • 創(chuàng)建一個socket,類型為UDP數(shù)據(jù)報(SOCK_DGRAM)
  • sockaddrd的IP設置為廣播IP(INADDR_BROADCAST, 255.255.255.255)
  • 為socket添加廣播屬性(setsockopt,SO_BROADCAST)
  • 發(fā)送UDP廣播報(sendto)
  • 接收UDP回復信息(recvfrom),接收設置超時時間(setsockopt,SO_RCVTIMEO),沒收到服務端回復則繼續(xù)廣播
  • 收到服務端回復后,解析出服務端的IP地址,然后即可中止廣播

具體代碼實現(xiàn)如下:

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后, 就不需要再廣播了 } //從廣播地址發(fā)送消息  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;
            } //再接收數(shù)據(jù) 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連接,并進行數(shù)據(jù)通信,該線程的實現(xiàn)邏輯如下:

  • 創(chuàng)建一個socket,類型為TCP數(shù)據(jù)流(SOCK_STREAM)
  • sockaddrd的IP設置為剛才獲取的服務端的IP(serverIP,例如192.168.1.101)
  • 向服務端請求連接(connect)
  • 連接成功之后,可以發(fā)送自定義的數(shù)據(jù)(send),這里發(fā)送的一串字母"abcdefg"加上自己的IP地址
  • 如果服務端會還會回復信息,可以進行接收(recv),這里的接收設置為非阻塞模式(MSG_DONTWAIT),這樣在服務端沒有回復數(shù)據(jù)的情況下,客戶端也不會一直等待,能夠再次發(fā)送自己的數(shù)據(jù)

具體的代碼實現(xiàn)如下:

void tcp_client_thread(std::string serverIP, std::string localIP) { printf("[%s] in, prepare connect serverIP:%s\n", __func__, serverIP.c_str()); //創(chuàng)建客戶端套接字文件 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)); //要向服務器發(fā)送的信息 char buf [MAXLINE]; std::string msg = "abcdefg" + std::string("(") + localIP + std::string(")"); while(1)
    { //發(fā)送數(shù)據(jù) send(tcpClientSocket, msg.c_str(), msg.length(),0); printf("[%s] send to server: %s\n", __func__, msg.c_str()); //接收服務器返回的數(shù)據(jù) 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個線程來分別實現(xiàn)對客戶端UDP廣播的處理和對客戶端TCP連接的處理,兩個功能獨立開來,可以實現(xiàn)對多個客戶端的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連接)
  • 創(chuàng)建一個socket,類型為UDP數(shù)據(jù)報(SOCK_DGRAM)
  • sockaddrd的IP設置為接收所有IP(INADDR_ANY,0.0.0.0),并進行綁定(bind)
  • 為socket添加廣播屬性(setsockopt,SO_BROADCAST)
  • 接收UDP廣播信息(recvfrom),這里是默認的阻塞接收,沒有廣播信息則一直等待
  • 收到客戶端的UDP廣播信息后,解析信息,判斷確實是要獲取IP后,將自己的IP信息按照規(guī)定的格式發(fā)送出去

具體的代碼實現(xiàn)如下:

//接收客戶端廣播信息的處理線程, 收到客戶端的UDP廣播后, 將自己(服務端)的IP發(fā)送回去 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服務器線程, 用于接受客戶端的連接, 主要邏輯如下:

  • 創(chuàng)建一個socket,命名為listenfd,類型為TCP數(shù)據(jù)流(SOCK_STREAM)
  • sockaddrd的IP設置為接收所有IP(INADDR_ANY,0.0.0.0),并進行綁定(bind)
  • 監(jiān)聽,并設置最大連接數(shù)(listen)
  • 創(chuàng)建一個epoll,來處理多客戶端請求時(epoll_create)
  • 將TCP socket添加到epoll進行監(jiān)聽(epoll_ctl,EPOLLIN)
  • epoll等待事件到來(epoll_wait)
  • epoll處理到來的事件
  • 如果到來的是listenfd,說明有新的客戶端請求連接,TCP服務端則接受請求(accept),然后將對應的客戶端fd添加到epoll進行監(jiān)聽(epoll_ctl,EPOLLIN)
  • 如果到來的不是listenfd,說明有已連接的客戶端發(fā)來的數(shù)據(jù)信息,則讀取信息(read)

具體的代碼實現(xiàn)如下:

//TCP服務器線程, 用于接受客戶端的連接, 并接收客戶端的信息 void tcp_server_thread() { //創(chuàng)建服務器端套接字文件 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)) ; //監(jiān)聽,并設置最大連接數(shù)為20 listen(listenfd, 20); printf("[%s] Accepting connections... \n", __func__); //通過epoll來監(jiān)控多個客戶端的請求 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); //添加監(jiān)聽描述符事件 epoll_set_fd_a_event(epollfd, EPOLL_CTL_ADD, listenfd, EPOLLIN); while(1)
    { //獲取已經(jīng)準備好的描述符事件 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進行監(jiān)聽 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發(fā)來的消息 printf("[%s] read message from fd:%d ---> %s\n", __func__, fd, buf);
                }
            }
        }
    }

    close(epollfd);
}

為epoll中的某個fd添加、修改或刪除某個事件,這里封裝成了一個函數(shù):

//為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 測試結(jié)果

這里測試了4種不同的情況,來驗證客戶端可以自動獲取到服務端的IP,并進行TCP連接,另外,服務端也可以處理多個客戶端的請求:

  • 1)單個客戶端連接服務端
  • 2)單個客戶端連接并中止后,另一個客戶端再次連接服務端
  • 3)客戶端先啟動后,服務端再啟動,客戶端依然能在服務端啟動后連接到服務端
  • 4)兩個客戶端現(xiàn)后進行連接服務端

5 總結(jié)

本篇介紹了在TCP通信中,客戶端通過UDP廣播,實現(xiàn)自動獲取服務端的IP地址,并進行TCP連接的具體方法,并通過代碼實現(xiàn),來測試此方案是實際效果,為了使服務端能夠處理多個客戶端的請求,這里使用了多線程編程,以及epoll機制來實現(xiàn)多客戶端的處理。


本站聲明: 本文章由作者或相關機構(gòu)授權發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫毥谦F公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉(zhuǎn)型技術解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關鍵字: 汽車 人工智能 智能驅(qū)動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務能7×24不間斷運行,同時企業(yè)卻面臨越來越多業(yè)務中斷的風險,如企業(yè)系統(tǒng)復雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務連續(xù)性,提升韌性,成...

關鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報道,騰訊和網(wǎng)易近期正在縮減他們對日本游戲市場的投資。

關鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關鍵字: 華為 12nm EDA 半導體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會上,華為常務董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權最終是由生態(tài)的繁榮決定的。

關鍵字: 華為 12nm 手機 衛(wèi)星通信

要點: 有效應對環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實提質(zhì)增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務引領增長 以科技創(chuàng)新為引領,提升企業(yè)核心競爭力 堅持高質(zhì)量發(fā)展策略,塑強核心競爭優(yōu)勢...

關鍵字: 通信 BSP 電信運營商 數(shù)字經(jīng)濟

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術學會聯(lián)合牽頭組建的NVI技術創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術創(chuàng)新聯(lián)...

關鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(集團)股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關鍵字: BSP 信息技術
關閉