TCP交互流程:
服務器:1. 創建socket;2. 綁定socket和端口號;3. 監聽端口號;4. 接收來自客戶端的連接請求;5. 從socket中讀取字符;6. 關閉socket。
客戶端:1. 創建socket;2. 連接指定計算機的端口;3. 向socket中寫入信息;4. 關閉socket。
創建socket:
socket函數
int socket (int __family, int __type, int __protocol);
__family是協議域,也稱協議族。常見的有AF_INET(ipv4)。
__type指定socket類型。SOCK_STREAM即TCP協議,SOCK_DGRAM即UDP協議。
__protocol指定協議。
該函數返回的socket描述字存在於協議族空間中,但是並沒有一個具體的地址。如果想要給它賦予一個地址,就必須調用bind()函數,否則系統就在調用connect()和listen()時自動隨機分配一個端口。
這里注意:type和protocol並不能隨意組合。當protocol為0時,會自動選擇type類型對應的默認協議。
創建socket的樣例代碼如下:
//創建TCP套接字 //AF_INET:網絡連接,ipv4 //SOCK_STREAM:TCP連接 int fd = socket(AF_INET, SOCK_STREAM, 0); if (fd<0) { std::cout<<"create socket error!"<<std::endl; return 0; } std::cout<<"create socket: "<<fd<<std::endl;
綁定socket和端口號:
bind函數
int bind (int, const struct sockaddr *__my_addr, socklen_t __addrlen);
第一個參數是socket描述字。(我不理解為啥這兒沒有參數名)
__my_addr是指向要綁定給該socket的協議地址。這個地址結構根據socket創建時的地址協議族(family)的不同而不同。
__addrlen對應的是地址的長度。
如果該函數執行成功,就返回0,否則為SOCKET_ERROR。
//命名套接字 struct sockaddr_in myaddr; memset((void *)&myaddr, 0, sizeof(myaddr)); //關於htonl和htons,參考以下網頁:ntohs, ntohl, htons,htonl的比較和詳解 //https://blog.csdn.net/haoxiaodao/article/details/73162663 myaddr.sin_family = AF_INET; myaddr.sin_addr.s_addr = htonl(INADDR_ANY); myaddr.sin_port = htons(6666); if (bind(fd, (struct sockaddr*)&myaddr, sizeof(myaddr)) < 0) { std::cout<<"name socket error!"<<std::endl; return 0; } std::cout<<"name socket"<<std::endl;
監聽端口號:
作為一個服務器,在調用socket()和bind()之后就會調用listen()來監聽這個socket。為了能夠在套接字上接受進入的連接,服務器程序必須創建一個隊列來保存未處理的請求。
listen函數
int listen (int, int __n);
第一個參數即socket描述子
__n為隊列的大小。
//創建監聽隊列 if (listen(fd, 5) < 0) { std::cout<<"listen failed"<<std::endl; return 0; }
接收來自客戶端的連接請求:
當TCP服務器監聽到了連接請求之后,就會調用accept()函數接收請求,這樣連接就建立好了。
accept函數
int accept (int, struct sockaddr *__peer, socklen_t *);
第一個是socket描述子,第二個是用來接收的客戶端地址,第三個是地址的大小。注意第三個是指針類型,所以要事先構造好大小的變量,然后傳地址進去。
另外,《后台開發核心技術與應用實踐》中的例子,第二個和第三個都傳的NULL。我的理解是,如果不需要接收這兩個量,就可以傳一個空值進去。
accept函數會返回一個新的socket描述子,這個新的描述子代表了服務端和客戶端的連接。后面可以用於讀取數據以及關閉連接。
//等待並接受連接 const int MAXBUF = 4096; char buff[MAXBUF]; struct sockaddr_in client_addr; int client_addr_len = sizeof(client_addr); int client_fd; while (1) { client_fd = accept(fd, (struct sockaddr*)&client_addr, &client_addr_len); if (client_fd < 0) { std::cout<<"connect error"<<std::endl; continue; } //接收數據 //關閉套接字 }
從socket中讀取字符:
服務器與客戶端建立好連接之后,就可以調用網絡I/O進行讀寫操作了。網絡I/O操作有下面幾組:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
具體的區別待補充,這里只舉一個例子。
_ssize_t read (int __fd, void *__buf, size_t __nbyte);
__fd是剛才獲得的服務器與客戶端建立連接的socket描述子
__buf是緩沖區指針
__nbyte是緩沖區大小
//接收數據 int nbytes = read(client_fd, buff, MAXBUF); std::cout<<"get infomation: "<<buff<<std::endl;
關閉socket:
完成讀寫操作就要關閉相應的socket描述子,可以類比與文件完成讀寫操作之后也要關閉一樣。
close函數
int close (int __fildes);
只有一個參數,就是socket描述子。
close會把該socket標記為關閉,然后立即返回到調用進程。該描述子不能再由調用進程使用,即,不能再作為read或write的第一個參數。
但是,這里需要注意的是,close操作只是使相應socket描述子的引用-1,只有當引用計數為0時,才會出發TCP發送終止連接請求。
以上是服務器端的基本代碼,總的代碼如下:
#include <sys/socket.h> #include <iostream> #include <cygwin/in.h> #include <cstring> #include <unistd.h> int main() { std::cout<<"running server"<<std::endl; //創建TCP套接字 //AF_INET:網絡連接,ipv4 //SOCK_STREAM:TCP連接 int fd = socket(AF_INET, SOCK_STREAM, 0); if (fd<0) { std::cout<<"create socket error!"<<std::endl; return 0; } std::cout<<"create socket: "<<fd<<std::endl; //命名套接字 struct sockaddr_in myaddr; memset((void *)&myaddr, 0, sizeof(myaddr)); //關於htonl和htons,參考以下網頁:ntohs, ntohl, htons,htonl的比較和詳解 //https://blog.csdn.net/haoxiaodao/article/details/73162663 myaddr.sin_family = AF_INET; myaddr.sin_addr.s_addr = htonl(INADDR_ANY); myaddr.sin_port = htons(6666); if (bind(fd, (struct sockaddr*)&myaddr, sizeof(myaddr)) < 0) { std::cout<<"name socket error!"<<std::endl; return 0; } std::cout<<"name socket"<<std::endl; //創建監聽隊列 if (listen(fd, 5) < 0) { std::cout<<"listen failed"<<std::endl; return 0; } std::cout<<"=============listening, port = 6666"<<std::endl; //等待並接受連接 const int MAXBUF = 4096; char buff[MAXBUF]; struct sockaddr_in client_addr; int client_addr_len = sizeof(client_addr); int client_fd; while (1) { client_fd = accept(fd, (struct sockaddr*)&client_addr, &client_addr_len); if (client_fd < 0) { std::cout<<"connect error"<<std::endl; continue; } //接收數據 int nbytes = read(client_fd, buff, MAXBUF); std::cout<<"get infomation: "<<buff<<std::endl; //關閉套接字 close(client_fd); } close(fd); return 0; }
相比與服務器端,客戶端的區別主要在於連接指定計算機的端口和向socket中寫入信息。
連接制定計算機的端口:
connect函數
int connect (int, const struct sockaddr *, socklen_t);
第一個參數是socket描述子,第二個是目標服務器的地址,第三個是地址struct的大小。
關於目標服務器地址的構造,需要利用函數inet_pton(),代碼如下:
const char* server = "127.0.0.1"; struct sockaddr_in server_addr; memset((char*)&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(6666); inet_pton(AF_INET, server, &server_addr.sin_addr);
構造完目標服務器的地址,就可以調用connect()函數了。
if (connect(fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { std::cout<<"connect failed"<<std::endl; return 0; }
向socket中寫入信息:
跟上面接收信息對應,使用了write()函數
const int MAXBUF = 4096; char buffer[MAXBUF] = "hello TCP"; int nbytes = write(fd, buffer, 10);
以上就是客戶端與服務器端相比不同的部分,客戶端的基本代碼如下:
#include <sys/socket.h> #include <iostream> #include <cstring> #include <cygwin/in.h> #include <arpa/inet.h> #include <unistd.h> int main() { int fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) { std::cout<<"socket error"<<std::endl; return 0; } const char* server = "127.0.0.1"; struct sockaddr_in server_addr; memset((char*)&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(6666); inet_pton(AF_INET, server, &server_addr.sin_addr); if (connect(fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { std::cout<<"connect failed"<<std::endl; return 0; } const int MAXBUF = 4096; char buffer[MAXBUF] = "hello TCP"; int nbytes = write(fd, buffer, 10); close(fd); }
參考資料:
1. TCP套接字編程入門 https://blog.csdn.net/lihao21/article/details/64624796?locationNum=7&fps=1
2. 《后台開發核心技術與應用實踐》