套接字接口
我們把服務器封裝成一個類,當我們定義出一個服務器對象后需要馬上初始化服務器,而初始化服務器需要做的第一件事就是創建套接字。
🌎socket函數
這是Linux中創建套接字的系統調用,函數原型如下:
int socket(int domain, int type, int protocol);
參數說明:
- domain:創建套接字的域或者叫做協議家族,也就是創建套接字的類型。該參數就相當于struct
sockaddr結構的前16個位。如果是本地通信就設置為AF_UNIX,如果是網絡通信就設置為AF_INET(IPv4)或AF_INET6(IPv6)。 - type:創建套接字時所需的服務類型。其中最常見的服務類型是SOCK_STREAM和SOCK_DGRAM,如果是基于UDP的網絡通信,我們采用的就是SOCK_DGRAM,叫做用戶數據報服務,如果是基于TCP的網絡通信,我們采用的就是SOCK_STREAM,叫做流式套接字,提供的是流式服務。
- protocol:創建套接字的協議類別。你可以指明為TCP或UDP,但該字段一般直接設置為0就可以了,設置為0表示的就是默認,此時會根據傳入的前兩個參數自動推導出你最終需要使用的是哪種協議。
返回值說明:
- 套接字創建成功返回一個文件描述符,創建失敗返回-1,同時錯誤碼會被設置。
🌎socket函數屬于什么類型的接口?
網絡協議棧是分層的,按照TCP/IP四層模型來說,自頂向下依次是應用層、傳輸層、網絡層和數據鏈路層。而我們現在所寫的代碼都叫做用戶級代碼,也就是說我們是在應用層編寫代碼,因此我們調用的實際是下三層的接口,而傳輸層和網絡層都是在操作系統內完成的,也就意味著我們在應用層調用的接口都叫做系統調用接口。
🌎socket函數是被誰調用的?
socket這個函數是被程序調用的,但并不是被程序在編碼上直接調用的,而是程序編碼形成的可執行程序運行起來變成進程,當這個進程被CPU調度執行到socket函數時,然后才會執行創建套接字的代碼,也就是說socket函數是被進程所調用的。
🌎socket函數底層做了什么?
socket函數上面說到是被進程調用的,而每一個進程在系統層面有一個進程地址空間PCB(task_struct),文件描述符表(files_struct)以及對應打開的各種文件。而文件描述符表里包含一個數組fd_array,其中數組的0,1,2下標分別對應,標準輸入,標準輸出以及標準錯誤。
當我們調用socket函數創建套接字時,實際相當于我們打開了一個“網絡文件”,打開后在內核層面上就形成了一個對應的struct file結構體,同時該結構體被連入到了該進程對應的文件雙鏈表,并將該結構體的首地址填入到了fd_array數組當中下標為3的位置,此時fd_array數組中下標為3的指針就指向了這個打開的“網絡文件”,最后3號文件描述符作為socket函數的返回值返回給了用戶。
每一個struct file結構體中包含的就是對應打開文件各種信息,比如文件的屬性信息、操作方法以及文件緩沖區等。其中文件對應的屬性在內核當中是由struct inode結構體來維護的,而文件對應的操作方法實際就是一堆的函數指針(比如read和write)在內核當中就是由struct file_operations結構體來維護的。而文件緩沖區對于打開的普通文件來說對應的一般是磁盤,但對于現在打開的“網絡文件”來說,這里的文件緩沖區對應的就是網卡。
對于一般的普通文件來說,當用戶通過文件描述符將數據寫到文件緩沖區,然后再把數據刷到磁盤上就完成了數據的寫入操作。而對于現在socket函數打開的“網絡文件”來說,當用戶將數據寫到文件緩沖區后,操作系統會定期將數據刷到網卡里面,而網卡則是負責數據發送的,因此數據最終就發送到了網絡當中。
🌎服務端創建套接字
我們初始化服務器創建套接字時,第一個參數需要填入的就是AF_INET,表明我們要進行網絡通信,因為我們是UDP服務器,面向數據報的,所以填入SOCK_DGRAM,第三個參數協議選擇,可以自行選擇UDP協議,也可以傳入0,讓socket根據第二個參數自動識別。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>class UdpServer
{
public://服務器初始化bool InitServer(){//AF_INET,表明需要網絡通信//SOCK_DGRAM,面向數據報//0,默認,根據第二個參數為面向數據報,自動識別為UDP協議_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "socket create error!!" << std::endl;return;}std::cout << "socket create success,sockfd : " << _sockfd << std::endl;}~UdpServer(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd;//文件描述符
};
注意: 當析構服務器時,我們可以將sockfd對應的文件進行關閉,但實際上不進行該操作也行,因為一般服務器運行后是就不會停下來的。
這里我們可以做一個簡單的測試,看看套接字是否創建成功。
int main()
{UdpServer* srv = new UdpServer();srv->InitServer();return 0;
}
運行程序后可以看到套接字是創建成功的,對應獲取到的文件描述符就是3,這也很好理解,因為0、1、2默認被標準輸入流、標準輸出流和標準錯誤流占用了,此時最小的、未被利用的文件描述符就是3。
UDP服務器端和客戶端均只需1個套接字:TCP 中,套接字是一對一的關系。如要向 10 個客戶端提供服務,那么除了負責監聽的套接字外,還需要創建 10 套接字。但在 UDP 中,不管是服務器端還是客戶端都只需要 1 個套接字。舉個例子,負責郵寄包裹的快遞公司可以比喻為 UDP 套接字,只要有 1 個快遞公司,就可以通過它向任意地址郵寄包裹。同樣,只需 1 個 UDP 套接字就可以向任意主機傳送數據。
服務端綁定
現在套接字已經創建成功了,但作為一款服務器來講,如果只是把套接字創建好了,那我們也只是在系統層面上打開了一個文件,操作系統將來并不知道是要將數據寫入到磁盤還是刷到網卡,此時該文件還沒有與網絡關聯起來。
UDP中的服務器端和客戶端沒有連接,UDP 不像 TCP,無需在連接狀態下交換數據,因此基于 UDP 的服務器端和客戶端也無需經過連接過程。也就是說,不必調用 listen() 和 accept() 函數。UDP 中只有創建套接字的過程和數據交換的過程。
由于現在編寫的是不面向連接的UDP服務器,所以初始化服務器要做的第二件事就是綁定。
🌎bind函數
綁定的函數叫做bind,注意它跟c++的bind函數只是同名,功能上沒有任何關聯,該函數的函數原型如下:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
參數說明:
- sockfd:綁定的文件的文件描述符。也就是我們創建套接字時獲取到的文件描述符。
- addr:網絡相關的屬性信息,包括協議家族、IP地址、端口號等。
- addrlen:傳入的addr結構體的長度。
返回值說明:
- 綁定成功返回0,綁定失敗返回-1,同時錯誤碼會被設置。
🌎struct sockaddr_in結構體
在綁定時需要將網絡相關的屬性信息填充到一個結構體當中,這里應該填入一個struct sockaddr結構體指針,但我們應該先創建struct sockaddr_in這個結構體,再將其強轉到struct sockaddr*填入,具體原因我的上篇博客對這兩個結構體解釋得很清楚。
附上連接:點擊跳轉到我的上篇博客
我們可以用grep命令在/usr/include目錄下查找該結構,此時就可以找到定義該結構的文件。
可以發現在結構體在/usr/include/linux/in.h文件中,需要注意的是,struct sockaddr_in屬于系統級的概念,不同的平臺接口設計可能會有點差別。
可以看到,struct sockaddr_in當中的成員如下:
- sin_family:表示協議家族。
- sin_port:表示端口號,是一個16位的整數。
- sin_addr:表示IP地址,是一個32位的整數。
剩下的字段一般不做處理,當然你也可以進行初始化。
其中sin_addr的類型是struct in_addr,實際該結構體當中就只有一個成員,該成員就是一個32位的整數,IP地址實際就是存儲在這個整數當中的。
🌎如何理解綁定?
在進行綁定的時候需要將IP地址和端口號告訴對應的網絡文件,此時就可以改變網絡文件當中文件操作函數的指向,將對應的操作函數改為對應網卡的操作方法,此時讀數據和寫數據對應的操作對象就是網卡了,所以綁定實際上就是將文件和網絡關聯起來。
🌎增加IP地址和端口號
由于綁定時需要用到IP地址和端口號,因此我們需要在服務器類當中引入IP地址和端口號,在創建服務器對象時需要傳入對應的IP地址和端口號,此時我們就可以根據傳入的IP地址和端口號對對應的成員進行初始化。
class UdpServer
{
public:UdpServer(std::string ip,uint16_t port):_sockfd(-1),_port(port),_ip(ip){}//服務器初始化bool InitServer(){//AF_INET,表明需要網絡通信//SOCK_DGRAM,面向數據報//0,默認,根據第二個參數為面向數據報,自動識別為UDP協議_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "socket create error!!" << std::endl;return false;}std::cout << "socket create success,sockfd : " << _sockfd << std::endl;return true;}~UdpServer(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd; //文件描述符uint16_t _port; //端口號std::string _ip; //IP地址
};
🌎服務端綁定
綁定之前,需要注意的是,在發送到網絡之前需要將端口號設置為網絡序列,由于端口號是16位的,因此我們需要使用htons函數將端口號轉為網絡序列。此外,由于網絡當中傳輸的是整數IP,我們需要調用inet_addr函數將字符串IP轉換成整數IP,然后再將轉換后的整數IP進行設置。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <string>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>class UdpServer
{
public:UdpServer(std::string ip,uint16_t port):_sockfd(-1),_port(port),_ip(ip){}//服務器初始化bool InitServer(){//AF_INET,表明需要網絡通信//SOCK_DGRAM,面向數據報//0,默認,根據第二個參數為面向數據報,自動識別為UDP協議_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "socket create error!!" << std::endl;return false;}std::cout << "socket create success,sockfd : " << _sockfd << std::endl;//填充網絡通信相關信息struct sockaddr_in local;memset(&local,'\0',sizeof(struct sockaddr_in));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = inet_addr(_ip.c_str());//綁定int ret = bind(_sockfd,(struct sockaddr*)&local,sizeof(struct sockaddr));if(ret < 0){std::cout << "bind error!!" << std::endl;return false;}std::cout << "bind success " << std::endl;return true;}~UdpServer(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd; //文件描述符uint16_t _port; //端口號std::string _ip; //IP地址
};
字符串IP VS 整數IP
IP地址的表現形式有兩種:
- 字符串IP:類似于192.168.233.123這種字符串形式的IP地址,叫做基于字符串的點分十進制IP地址。
- 整數IP:IP地址在進行網絡傳輸時所用的形式,用一個32位的整數來表示IP地址。
🌎整數IP存在的意義
網絡傳輸數據時是寸土寸金的,如果我們在網絡傳輸時直接以基于字符串的點分十進制IP的形式進行IP地址的傳送,那么此時一個IP地址至少就需要15個字節,但實際并不需要耗費這么多字節。
IP地址實際可以劃分為四個區域,其中每一個區域的取值都是0~255,而這個范圍的數字只需要用8個比特位就能表示,因此我們實際只需要32個比特位就能夠表示一個IP地址。其中這個32位的整數的每一個字節對應的就是IP地址中的某個區域,我們將IP地址的這種表示方法稱之為整數IP,此時表示一個IP地址只需要4個字節。
因為采用整數IP的方案表示一個IP地址只需要4個字節,并且在網絡通信也能表示同樣的含義,因此在網絡通信時就沒有用字符串IP而用的是整數IP,因為這樣能夠減少網絡通信時數據的傳送。
🌎字符串IP和整數IP相互轉換的方式
轉換的方式有很多,比如我們可以定義一個位段A,位段A當中有四個成員,每個成員的大小都是8個比特位,這四個成員就依次表示IP地址的四個區域,一共32個比特位。
然后我們再定義一個聯合體IP,該聯合體當中有兩個成員,其中一個是32位的整數,其代表的就是整數IP,還有一個就是位段A類型的成員,其代表的就是字符串IP。
由于聯合體的空間是成員共享的,因此我們設置IP和讀取IP的方式如下:
- 當我們想以整數IP的形式設置IP時,直接將其賦值給聯合體的第一個成員就行了。
- 當我們想以字符串IP的形式設置IP時,先將字符串分成對應的四部分,然后將每部分轉換成對應的二進制序列依次設置到聯合體中第二個成員當中的p1、p2、p3和p4就行了。
- 當我們想取出整數IP時,直接讀取聯合體的第一個成員就行了。
- 當我們想取出字符串IP時,依次獲取聯合體中第二個成員當中的p1、p2、p3和p4,然后將每一部分轉換成字符串后拼接到一起就行了。
注意: 在操作系統內部實際用的就是位段和枚舉,來完成字符串IP和整數IP之間的相互轉換的
🌎inet_addr函數
實際在進行字符串IP和整數IP的轉換時,我們不需要自己編寫轉換邏輯,系統已經為我們提供了相應的轉換函數,我們直接調用即可。
將字符串IP轉換成整數IP的函數叫做inet_addr,該函數的函數原型如下:
in_addr_t inet_addr(const char *cp);
該函數使用起來非常簡單,我們只需傳入待轉換的字符串IP,該函數返回的就是轉換后的整數IP。除此之外,inet_aton函數也可以將字符串IP轉換成整數IP,不過該函數使用起來沒有inet_addr簡單。
🌎inet_ntoa函數
將整數IP轉換成字符串IP的函數叫做inet_ntoa,該函數的函數原型如下:
char *inet_ntoa(struct in_addr in);
需要注意的是,傳入inet_ntoa函數的參數類型是in_addr,因此我們在傳參時不需要選中in_addr結構當中的32位的成員傳入,直接傳入in_addr結構體即可。
運行服務器
UDP服務器的初始化就只需要創建套接字和綁定就行了,當服務器初始化完畢后我們就可以啟動服務器了。
服務器實際上就是在周而復始的為我們提供某種服務,服務器之所以稱為服務器,是因為服務器運行起來后就永遠不會退出,因此服務器實際執行的是一個死循環代碼。由于UDP服務器是不面向連接的,因此只要UDP服務器啟動后,就可以直接讀取客戶端發來的數據。
🌎recvfrom函數
UDP服務器讀取數據的函數叫做recvfrom,該函數的函數原型如下:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
參數說明:
- sockfd:對應操作的文件描述符。表示從該文件描述符索引的文件當中讀取數據。
- buf:讀取數據的存放位置。
- len:期望讀取數據的字節數。
- flags:讀取的方式。一般設置為0,表示阻塞讀取。
- src_addr:對端網絡相關的屬性信息,包括協議家族、IP地址、端口號等。
- addrlen:調用時傳入期望讀取的src_addr結構體的長度,返回時代表實際讀取到的src_addr結構體的長度,這是一個輸入輸出型參數。
返回值說明:
- 讀取成功返回實際讀取到的字節數,讀取失敗返回-1,同時錯誤碼會被設置。
注意:
- 由于UDP是不面向連接的,因此我們除了獲取到數據以外還需要獲取到對端網絡相關的屬性信息,包括IP地址和端口號等。
- 在調用recvfrom讀取數據時,必須將addrlen設置為你要讀取的結構體對應的大小。
- 由于recvfrom函數提供的參數也是struct sockaddr類型的,因此我們在傳入結構體地址時需要將struct
sockaddr_in類型進行強轉。
🌎啟動服務器函數
現在服務端通過recvfrom函數讀取客戶端數據,我們可以先將讀取到的數據當作字符串看待,將讀取到的數據的最后一個位置設置為’\0’,此時我們就可以將讀取到的數據進行輸出,同時我們也可以將獲取到的客戶端的IP地址和端口號也一并進行輸出。
需要注意的是,我們獲取到的客戶端的端口號此時是網絡序列,我們需要調用ntohs函數將其轉為主機序列再進行打印輸出。同時,我們獲取到的客戶端的IP地址是整數IP,我們需要通過調用inet_ntoa函數將其轉為字符串IP再進行打印輸出。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <string>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>const int BUFFERSIZE = 128;
class UdpServer
{
public:UdpServer(std::string ip,uint16_t port):_sockfd(-1),_port(port),_ip(ip){}//服務器啟動void Start(){char buffer[BUFFERSIZE] = {0};while(true){struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t size = recvfrom(_sockfd,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&peer,&len);if(size < 0){std::cout << "recvfrom error!!" << std::endl;}buffer[size] = '\0';int port = ntohs(peer.sin_port);std::string ip = inet_ntoa(peer.sin_addr);//消息打印std::cout << ip << ":" << port << "# " << buffer << std::endl;}}//服務器初始化bool InitServer(){//AF_INET,表明需要網絡通信//SOCK_DGRAM,面向數據報//0,默認,根據第二個參數為面向數據報,自動識別為UDP協議_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "socket create error!!" << std::endl;return false;}std::cout << "socket create success,sockfd : " << _sockfd << std::endl;//填充網絡通信相關信息struct sockaddr_in local;memset(&local,'\0',sizeof(struct sockaddr_in));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = inet_addr(_ip.c_str());//綁定int ret = bind(_sockfd,(struct sockaddr*)&local,sizeof(struct sockaddr));if(ret < 0){std::cout << "bind error!!" << std::endl;return false;}std::cout << "bind success " << std::endl;return true;}~UdpServer(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd; //文件描述符uint16_t _port; //端口號std::string _ip; //IP地址
};
注意: 如果調用recvfrom函數讀取數據失敗,我們可以打印一條提示信息,但是不要讓服務器退出,服務器不能因為讀取某一個客戶端的數據失敗就退出。
🌎引入命令行參數
鑒于構造服務器時需要傳入IP地址和端口號,我們這里可以引入命令行參數。此時當我們運行服務器時在后面跟上對應的IP地址和端口號即可。
由于云服務器的原因,后面實際不需要傳入IP地址,因此在運行服務器的時候我們只需要傳入端口號即可,目前我們就手動將IP地址設置為127.0.0.1。IP地址為127.0.0.1實際上等價于localhost表示本地主機,我們將它稱之為本地環回,相當于我們一會先在本地測試一下能否正常通信,然后再進行網絡通信的測試。
int main(int argc,char* argv[])
{if(argc != 2){std::cout << "Usage: " << argv[0] << " port" << std::endl;return 1;}std::string ip = "127.0.0.1";//本地環回uint16_t port = atoi(argv[1]);UdpServer* srv = new UdpServer(ip,port);srv->InitServer();srv->Start();return 0;
}
此時帶上端口號運行程序就可以看到套接字創建成功、綁定成功,現在服務器就在等待客戶端向它發送數據。
雖然現在客戶端代碼還沒有編寫,但是我們可以通過netstat命令來查看當前網絡的狀態,這里我們可以選擇攜帶nlup選項。
netstat常用選項說明:
- -n:直接使用IP地址,而不通過域名服務器。
- -l:顯示監控中的服務器的Socket。
- -t:顯示TCP傳輸協議的連線狀況。
- -u:顯示UDP傳輸協議的連線狀況。
- -p:顯示正在使用Socket的程序識別碼和程序名稱。
此時你就能查看到對應網絡相關的信息,在這些信息中程序名稱為./server的那一行顯示的就是我們運行的UDP服務器的網絡信息。
客戶端創建套接字
同樣的,我們把客戶端也封裝成一個類,當我們定義出一個客戶端對象后也是需要對其進行初始化,而客戶端在初始化時也需要創建套接字,之后客戶端發送數據或接收數據也就是對這個套接字進行操作。
客戶端創建套接字時選擇的協議家族也是AF_INET,需要的服務類型也是SOCK_DGRAM,當客戶端被析構時也可以選擇關閉對應的套接字。與服務端不同的是,客戶端在初始化時只需要創建套接字就行了,而不需要進行綁定操作。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <string>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>class UdpClient
{
public:bool InitClient(){//創建套接字_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "create sockfd error!!" std::endl;return false;}return true;}~UdpClient(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd;
};
客戶端的綁定問題
首先,由于是網絡通信,通信雙方都需要找到對方,因此服務端和客戶端都需要有各自的IP地址和端口號,只不過服務端需要進行端口號的綁定,而客戶端不需要。
因為服務器就是為了給別人提供服務的,因此服務器必須要讓別人知道自己的IP地址和端口號,IP地址一般對應的就是域名,而端口號一般沒有顯示指明過,因此服務端的端口號一定要是一個眾所周知的端口號,并且選定后不能輕易改變,否則客戶端是無法知道服務端的端口號的,這就是服務端要進行綁定的原因,只有綁定之后這個端口號才真正屬于自己,因為一個端口只能被一個進程所綁定,服務器綁定一個端口就是為了獨占這個端口。
而客戶端在通信時雖然也需要端口號,但客戶端一般是不進行綁定的,客戶端訪問服務端的時候,端口號只要是唯一的就行了,不需要和特定客戶端進程強相關。
如果客戶端綁定了某個端口號,那么以后這個端口號就只能給這一個客戶端使用,就是這個客戶端沒有啟動,這個端口號也無法分配給別人,并且如果這個端口號被別人使用了,那么這個客戶端就無法啟動了。所以客戶端的端口只要保證唯一性就行了,因此客戶端端口可以動態的進行設置,并且客戶端的端口號不需要我們來設置,當我們調用類似于sendto這樣的接口時,操作系統會自動給當前客戶端獲取一個唯一的端口號。
也就是說,客戶端每次啟動時使用的端口號可能是變化的,此時只要我們的端口號沒有被耗盡,客戶端就永遠可以啟動。
啟動客戶端
🌎增加服務端IP地址和端口號
作為一個客戶端,它必須知道它要訪問的服務端的IP地址和端口號,因此在客戶端類當中需要引入服務端的IP地址和端口號,此時我們就可以根據傳入的服務端的IP地址和端口號對對應的成員進行初始化。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <string>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>class UdpClient
{
public:UdpClient(std::string ip,uint16_t port):_sockfd(-1),_server_port(port),_server_ip(ip){}bool InitClient(){//創建套接字_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "create sockfd error!!" << std::endl;return false;}return true;}~UdpClient(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd;uint16_t _server_port; //服務器端口號std::string _server_ip; //服務器ip
};
當客戶端初始化完畢后我們就可以將客戶端運行起來,由于客戶端和服務端在功能上是相互補充的,既然服務器是在讀取客戶端發來的數據,那么客戶端就應該想服務端發送數據。
🌎sendto函數
UDP客戶端發送數據的函數叫做sendto,該函數的函數原型如下:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
參數說明:
- sockfd:對應操作的文件描述符。表示將數據寫入該文件描述符索引的文件當中。
- buf:待寫入數據的存放位置。
- len:期望寫入數據的字節數。
- flags:寫入的方式。一般設置為0,表示阻塞寫入。
- dest_addr:對端網絡相關的屬性信息,包括協議家族、IP地址、端口號等。 addrlen:傳入dest_addr結構體的長度。
返回值說明:
- 寫入成功返回實際寫入的字節數,寫入失敗返回-1,同時錯誤碼會被設置。
注意:
- 由于UDP不是面向連接的,因此除了傳入待發送的數據以外還需要指明對端網絡相關的信息,包括IP地址和端口號等。
- 由于sendto函數提供的參數也是struct sockaddr類型的,因此我們在傳入結構體地址時需要將struct
sockaddr_in類型進行強轉。
🌎啟動客戶端函數
現在客戶端要發送數據給服務端,我們可以讓客戶端獲取用戶輸入,不斷將用戶輸入的數據發送給服務端。
需要注意的是,客戶端中存儲的服務端的端口號此時是主機序列,我們需要調用htons函數將其轉為網絡序列后再設置進struct sockaddr_in結構體。同時,客戶端中存儲的服務端的IP地址是字符串IP,我們需要通過調用inet_addr函數將其轉為整數IP后再設置進struct sockaddr_in結構體。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <string>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>class UdpClient
{
public:UdpClient(std::string ip,uint16_t port):_sockfd(-1),_server_port(port),_server_ip(ip){}void Start(){std::string msg;//目標服務器信息struct sockaddr_in peer;memset(&peer,'\0',sizeof(peer));peer.sin_family = AF_INET;peer.sin_port = htons(_server_port);peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());while(true){std::cout << "Please Enter# ";getline(std::cin,msg);sendto(_sockfd,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));}}bool InitClient(){//創建套接字_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "create sockfd error!!" << std::endl;return false;}return true;}~UdpClient(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd;uint16_t _server_port; //服務器端口號std::string _server_ip; //服務器ip
};
🌎引入命令行參數
鑒于構造客戶端時需要傳入對應服務端的IP地址和端口號,我們這里也可以引入命令行參數。當我們運行客戶端時直接在后面跟上對應服務端的IP地址和端口號即可。
int main(int argc,char* argv[])
{if(argc != 3){std::cout << "Usage: " << argv[0] << " server_ip server_port" << std::endl;return 1;}std::string serverip = argv[1];uint16_t serverport = atoi(argv[2]);UdpClient* clt = new UdpClient(serverip,serverport);clt->InitClient();clt->Start();return 0;
}
客戶端和服務器完整代碼
🌎server
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <string>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>const int BUFFERSIZE = 128;
class UdpServer
{
public:UdpServer(std::string ip,uint16_t port):_sockfd(-1),_port(port),_ip(ip){}//服務器啟動void Start(){char buffer[BUFFERSIZE] = {0};while(true){struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t size = recvfrom(_sockfd,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&peer,&len);if(size < 0){std::cout << "recvfrom error!!" << std::endl;}buffer[size] = '\0';int port = ntohs(peer.sin_port);std::string ip = inet_ntoa(peer.sin_addr);//消息打印std::cout << ip << ":" << port << "# " << buffer << std::endl;}}//服務器初始化bool InitServer(){//AF_INET,表明需要網絡通信//SOCK_DGRAM,面向數據報//0,默認,根據第二個參數為面向數據報,自動識別為UDP協議_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "socket create error!!" << std::endl;return false;}std::cout << "socket create success,sockfd : " << _sockfd << std::endl;//填充網絡通信相關信息struct sockaddr_in local;memset(&local,'\0',sizeof(struct sockaddr_in));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = inet_addr(_ip.c_str());//綁定int ret = bind(_sockfd,(struct sockaddr*)&local,sizeof(struct sockaddr));if(ret < 0){std::cout << "bind error!!" << std::endl;return false;}std::cout << "bind success " << std::endl;return true;}~UdpServer(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd; //文件描述符uint16_t _port; //端口號std::string _ip; //IP地址
};int main(int argc,char* argv[])
{if(argc != 2){std::cout << "Usage: " << argv[0] << " port" << std::endl;return 1;}std::string ip = "127.0.0.1";//本地環回uint16_t port = atoi(argv[1]);UdpServer* srv = new UdpServer(ip,port);srv->InitServer();srv->Start();return 0;
}
🌎client
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <string>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>class UdpClient
{
public:UdpClient(std::string ip,uint16_t port):_sockfd(-1),_server_port(port),_server_ip(ip){}void Start(){std::string msg;//目標服務器信息struct sockaddr_in peer;memset(&peer,'\0',sizeof(peer));peer.sin_family = AF_INET;peer.sin_port = htons(_server_port);peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());while(true){std::cout << "Please Enter# ";getline(std::cin,msg);sendto(_sockfd,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));}}bool InitClient(){//創建套接字_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "create sockfd error!!" << std::endl;return false;}return true;}~UdpClient(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd;uint16_t _server_port; //服務器端口號std::string _server_ip; //服務器ip
};int main(int argc,char* argv[])
{if(argc != 3){std::cout << "Usage: " << argv[0] << " server_ip server_port" << std::endl;return 1;}std::string serverip = argv[1];uint16_t serverport = atoi(argv[2]);UdpClient* clt = new UdpClient(serverip,serverport);clt->InitClient();clt->Start();return 0;
}
本地測試
現在服務端和客戶端的代碼都已經編寫完畢,我們可以先進行本地測試,此時服務器沒有綁定外網,綁定的是本地環回。現在我們運行服務器時指明端口號為5678,再運行客戶端,此時客戶端要訪問的服務器的IP地址就是本地環回127.0.0.1,服務端的端口號就是5678。
客戶端運行之后提示我們進行輸入,當我們在客戶端輸入數據后,客戶端將數據發送給服務端,此時服務端再將收到的數據打印輸出,這時我們在服務端的窗口也看到我們輸入的內容。
此時我們再用netstat命令查看網絡信息,可以看到服務端的端口是8081,客戶端的端口是55210。這里客戶端能被netstat命令查看到,說明客戶端也已經動態綁定成功了,這就是我們所謂的網絡通信。
INADDR_ANY
現在我們已經通過了本地測試,接下來就需要進行網絡測試了,那是不是直接讓服務端綁定我的公網IP,此時這個服務端就能夠被外網訪問了呢?
理論上確實是這樣的,就比如我的服務器的公網IP是47.94.84.249,這里用ping命令也是能夠ping通的。
現在我將服務端設置的本地環回改為我的公網IP,此時當我們重新編譯程序再次運行服務端的時候會發現服務端綁定失敗。
由于云服務器的IP地址是由對應的云廠商提供的,這個IP地址并不一定是真正的公網IP,這個IP地址是不能直接被綁定的,如果需要讓外網訪問,此時我們需要bind 0。系統當中提供的一個INADDR_ANY,這是一個宏值,它對應的值就是0。
因此如果我們需要讓外網訪問,那么在云服務器上進行綁定時就應該綁定INADDR_ANY,此時我們的服務器才能夠被外網訪問。
🌎綁定INADDR_ANY的好處
當一個服務器的帶寬足夠大時,一臺機器接收數據的能力就約束了這臺機器的IO效率,因此一臺服務器底層可能裝有多張網卡,此時這臺服務器就可能會有多個IP地址,但一臺服務器上端口號為5678的服務只有一個。這臺服務器在接收數據時,這里的多張網卡在底層實際都收到了數據,如果這些數據也都想訪問端口號為5678的服務。此時如果服務端在綁定的時候是指明綁定的某一個IP地址,那么此時服務端在接收數據的時候就只能從綁定IP對應的網卡接收數據。而如果服務端綁定的是INADDR_ANY,那么只要是發送給端口號為5678的服務的數據,系統都會可以將數據自底向上交給該服務端。
因此服務端綁定INADDR_ANY這種方案也是強烈推薦的方案,所有的服務器具體在操作的時候用的也就是這種方案。
當然,如果你既想讓外網訪問你的服務器,但你又指向綁定某一個IP地址,那么就不能用云服務器,此時可以選擇使用虛擬機或者你自定義安裝的Linux操作系統,那個IP地址就是支持你綁定的,而云服務器是不支持的。
🌎更改代碼
因此,如果想要讓外網訪問我們的服務,我們這里就需要將服務器類當中IP地址相關的代碼去掉,而在填充網絡相關信息設置struct sockaddr_in結構體時,將設置的IP地址改為INADDR_ANY就行了。由于INADDR_ANY的值本質就是0,不存在大小端的問題,因此在設置時可以不進行網絡字節序的轉換。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <string>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>const int BUFFERSIZE = 128;
class UdpServer
{
public:UdpServer(std::string ip,uint16_t port):_sockfd(-1),_port(port),_ip(ip){}//服務器啟動void Start(){char buffer[BUFFERSIZE] = {0};while(true){struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t size = recvfrom(_sockfd,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&peer,&len);if(size < 0){std::cout << "recvfrom error!!" << std::endl;}buffer[size] = '\0';int port = ntohs(peer.sin_port);std::string ip = inet_ntoa(peer.sin_addr);//消息打印std::cout << ip << ":" << port << "# " << buffer << std::endl;}}//服務器初始化bool InitServer(){//AF_INET,表明需要網絡通信//SOCK_DGRAM,面向數據報//0,默認,根據第二個參數為面向數據報,自動識別為UDP協議_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "socket create error!!" << std::endl;return false;}std::cout << "socket create success,sockfd : " << _sockfd << std::endl;//填充網絡通信相關信息struct sockaddr_in local;memset(&local,'\0',sizeof(struct sockaddr_in));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;//綁定INADDR_ANY//綁定int ret = bind(_sockfd,(struct sockaddr*)&local,sizeof(struct sockaddr));if(ret < 0){std::cout << "bind error!!" << std::endl;return false;}std::cout << "bind success " << std::endl;return true;}~UdpServer(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd; //文件描述符uint16_t _port; //端口號std::string _ip; //IP地址
};int main(int argc,char* argv[])
{if(argc != 2){std::cout << "Usage: " << argv[0] << " port" << std::endl;return 1;}std::string ip = "47.94.84.249";//本地環回uint16_t port = atoi(argv[1]);UdpServer* srv = new UdpServer(ip,port);srv->InitServer();srv->Start();return 0;
}
此時當我們再重新編譯運行服務器時就不會綁定失敗了,并且此時當我們再用netstat命令查看時會發現,該服務器的本地IP地址變成了0.0.0.0,這就意味著該UDP服務器可以在本地讀取任何一張網卡里面的數據。
簡單的UDP回聲服務器
由于在進行網絡測試的時候,當客戶端發送數據給服務端時,服務端會將從客戶端收到的數據進行打印,因此服務端是能夠看到現象的。但客戶端一直在向服務端發送數據,在客戶端這邊看不出服務端是否收到了自己發送的數據。
🌎服務端代碼改寫
鑒于此,我們可以將該服務器改成一個簡單的回聲服務器。當服務端收到客戶端發來的數據后,除了在服務端進行打印以外,服務端可以調用sento函數將收到的數據重新發送給對應的客戶端。
需要注意的是,服務端在調用sendto函數時需要傳入客戶端的網絡屬性信息,但服務端現在是知道客戶端的網絡屬性信息的,因為服務端在此之前就已經通過recvfrom函數獲取到了客戶端的網絡屬性信息。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <string>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>const int BUFFERSIZE = 128;
class UdpServer
{
public:UdpServer(std::string ip,uint16_t port):_sockfd(-1),_port(port),_ip(ip){}//服務器啟動void Start(){char buffer[BUFFERSIZE] = {0};while(true){struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t size = recvfrom(_sockfd,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&peer,&len);if(size < 0){std::cout << "recvfrom error!!" << std::endl;}buffer[size] = '\0';int port = ntohs(peer.sin_port);std::string ip = inet_ntoa(peer.sin_addr);//消息打印std::cout << ip << ":" << port << "# " << buffer << std::endl;std::string echo_msg = "server get!->";echo_msg += buffer;sendto(_sockfd,echo_msg.c_str(),echo_msg.size(),0,(struct sockaddr*)&peer,len);}}//服務器初始化bool InitServer(){//AF_INET,表明需要網絡通信//SOCK_DGRAM,面向數據報//0,默認,根據第二個參數為面向數據報,自動識別為UDP協議_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "socket create error!!" << std::endl;return false;}std::cout << "socket create success,sockfd : " << _sockfd << std::endl;//填充網絡通信相關信息struct sockaddr_in local;memset(&local,'\0',sizeof(struct sockaddr_in));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;//綁定INADDR_ANY//綁定int ret = bind(_sockfd,(struct sockaddr*)&local,sizeof(struct sockaddr));if(ret < 0){std::cout << "bind error!!" << std::endl;return false;}std::cout << "bind success " << std::endl;return true;}~UdpServer(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd; //文件描述符uint16_t _port; //端口號std::string _ip; //IP地址
};int main(int argc,char* argv[])
{if(argc != 2){std::cout << "Usage: " << argv[0] << " port" << std::endl;return 1;}std::string ip = "47.94.84.249";//本地環回uint16_t port = atoi(argv[1]);UdpServer* srv = new UdpServer(ip,port);srv->InitServer();srv->Start();return 0;
}
🌎客戶端代碼改寫
服務端的代碼改了之后,對應客戶端的代碼也得改改。當客戶端發完數據給服務端后,由于服務端還會將該數據重新發給客戶端,因此客戶端發完數據后還需要調recvfrom來讀取服務端發來的響應數據。
在客戶端調用recvfrom函數接收服務端發來的響應數據時,客戶端同時也需要讀取服務端與網絡相關的各種信息。雖然客戶端早已知道服務端的網絡信息了,此時服務端的網絡信息已經不重要了,但還是建議不要把參數設置為空,這樣可能會出問題,所以我們還是用一個臨時變量將服務端的網絡信息讀取一下。
而客戶端接收到服務端的響應數據后,將數據原封不動的打印出來就行了。此時客戶端發送給服務端的數據,除了在服務端會打印顯示以外,服務端還會將數據再重新發回給客戶端,此時客戶端也會接收到響應數據然后將該數據進行打印。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <string>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>const int BUFFERSIZE = 128;
class UdpClient
{
public:UdpClient(std::string ip,uint16_t port):_sockfd(-1),_server_port(port),_server_ip(ip){}void Start(){std::string msg;//目標服務器信息struct sockaddr_in peer;memset(&peer,'\0',sizeof(peer));peer.sin_family = AF_INET;peer.sin_port = htons(_server_port);peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());while(true){std::cout << "Please Enter# ";getline(std::cin,msg);sendto(_sockfd,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));char buffer[BUFFERSIZE] = {0};struct sockaddr_in tmp;socklen_t len = sizeof(tmp);ssize_t size = recvfrom(_sockfd,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&tmp,&len);if(size < 0){std::cout << "recvfrom error!!" << std::endl;}buffer[size] = '\0';std::cout << buffer << std::endl;}}bool InitClient(){//創建套接字_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "create sockfd error!!" << std::endl;return false;}return true;}~UdpClient(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd;uint16_t _server_port; //服務器端口號std::string _server_ip; //服務器ip
};int main(int argc,char* argv[])
{if(argc != 3){std::cout << "Usage: " << argv[0] << " server_ip server_port" << std::endl;return 1;}std::string serverip = argv[1];uint16_t serverport = atoi(argv[2]);UdpClient* clt = new UdpClient(serverip,serverport);clt->InitClient();clt->Start();return 0;
}
此時當我們測試回聲服務器時,在服務端和客戶端就都能夠看到對應的現象,這樣就能夠判斷通信是否正常了。