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 &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:%sn", __func__, token);
token = strtok(NULL, split.c_str());
}
}
//---------使用示例: 解析服務器的ip----------
char recvbuf[100]={0};
//...接收服務端返回的信息
vector 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 errorn", __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] %sn", __func__, ETH_NAME, localIP.c_str());
}
int udpClientSocket = -1;
if ((udpClientSocket = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
printf("[%s] socket errorn", __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: %dn", __func__, ret);
}
else
{
printf("[%s] broadcast ok, msg: %sn", __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 failedn", __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:%sn", __func__, recvbuf);
//解析服務器的ip
vector 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 timeoutn", __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:%sn", __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: %sn", __func__, msg.c_str());
//接收服務器返回的數據
int n= recv(tcpClientSocket, buf, MAXLINE, MSG_DONTWAIT); //非阻塞讀取
if(n>0)
{
printf("[%s] Response from server: %sn", __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] %sn", __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 errorn", __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 errorn", __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:%dn", __func__, ret);
}
else
{
printf("[%s]receive: %sn", __func__, smsg);
vector 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: %dn", __func__, ret);
}
else
{
printf("[%s] reply ok, msg: %sn", __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:%dn", __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:%dn",
__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 ---> %sn", __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:%dn", __func__, op);
return false;
}
}
4 測試結果
這里測試了4種不同的情況,來驗證客戶端可以自動獲取到服務端的IP,并進行TCP連接,另外,服務端也可以處理多個客戶端的請求:
1)單個客戶端連接服務端
2)單個客戶端連接并中止后,另一個客戶端再次連接服務端
3)客戶端先啟動后,服務端再啟動,客戶端依然能在服務端啟動后連接到服務端
4)兩個客戶端現后進行連接服務端
5 總結
本篇介紹了在TCP通信中,客戶端通過UDP廣播,實現自動獲取服務端的IP地址,并進行TCP連接的具體方法,并通過代碼實現,來測試此方案是實際效果,為了使服務端能夠處理多個客戶端的請求,這里使用了多線程編程,以及epoll機制來實現多客戶端的處理。
審核編輯:湯梓紅
-
Linux
+關注
關注
87文章
11304瀏覽量
209503 -
TCP
+關注
關注
8文章
1353瀏覽量
79074 -
網絡編程
+關注
關注
0文章
71瀏覽量
10075
發布評論請先 登錄
相關推薦
評論