TCP 通信基本流程

不管多麼複雜的伺服器或客戶端程序,其網路通信的基本原理一定如下所述:

對於伺服器,其通信流程一般有如下步驟:

1. 調用 socket 函數創建 socket(偵聽socket)
2. 調用 bind 函數 將 socket綁定到某個ip和埠的二元組上
3. 調用 listen 函數 開啟偵聽
4. 當有客戶端請求連接上來後,調用 accept 函數接受連接,產生一個新的 socket(客戶端 socket)
5. 基於新產生的 socket 調用 send 或 recv 函數開始與客戶端進行數據交流
6. 通信結束後,調用 close 函數關閉偵聽 socket

對於客戶端,其通信流程一般有如下步驟:

1. 調用 socket函數創建客戶端 socket
2. 調用 connect 函數嘗試連接伺服器
3. 連接成功以後調用 send 或 recv 函數開始與伺服器進行數據交流
4. 通信結束後,調用 close 函數關閉偵聽socket

上述流程可以繪製成如下圖示:

對於上面的圖,讀者可能有疑問,為什麼客戶端調用 close() ,會和伺服器端 recv() 函數有關。這個涉及到 recv() 函數的返回值意義,我們在下文中詳細講解。

伺服器端實現代碼:

/**
* TCP伺服器通信基本流程
* zhangyl 2018.12.13
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>

int main(int argc, char* argv[])
{
//1.創建一個偵聽socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1)
{
std::cout << "create listen socket error." << std::endl;
return -1;
}

//2.初始化伺服器地址
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(3000);
if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1)
{
std::cout << "bind listen socket error." << std::endl;
return -1;
}

//3.啟動偵聽
if (listen(listenfd, SOMAXCONN) == -1)
{
std::cout << "listen error." << std::endl;
return -1;
}

while (true)
{
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
//4. 接受客戶端連接
int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen);
if (clientfd != -1)
{
char recvBuf[32] = {0};
//5. 從客戶端接受數據
int ret = recv(clientfd, recvBuf, 32, 0);
if (ret > 0)
{
std::cout << "recv data from client, data: " << recvBuf << std::endl;
//6. 將收到的數據原封不動地發給客戶端
ret = send(clientfd, recvBuf, strlen(recvBuf), 0);
if (ret != strlen(recvBuf))
std::cout << "send data error." << std::endl;

std::cout << "send data to client successfully, data: " << recvBuf << std::endl;
}
else
{
std::cout << "recv data error." << std::endl;
}

close(clientfd);
}
}

//7.關閉偵聽socket
close(listenfd);

return 0;
}

客戶端實現代碼:

/**
* TCP客戶端通信基本流程
* zhangyl 2018.12.13
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>

#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT 3000
#define SEND_DATA "helloworld"

int main(int argc, char* argv[])
{
//1.創建一個socket
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
if (clientfd == -1)
{
std::cout << "create client socket error." << std::endl;
return -1;
}

//2.連接伺服器
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
serveraddr.sin_port = htons(SERVER_PORT);
if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1)
{
std::cout << "connect socket error." << std::endl;
return -1;
}

//3. 向伺服器發送數據
int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0);
if (ret != strlen(SEND_DATA))
{
std::cout << "send data error." << std::endl;
return -1;
}

std::cout << "send data successfully, data: " << SEND_DATA << std::endl;

//4. 從客戶端收取數據
char recvBuf[32] = {0};
ret = recv(clientfd, recvBuf, 32, 0);
if (ret > 0)
{
std::cout << "recv data successfully, data: " << recvBuf << std::endl;
}
else
{
std::cout << "recv data error, data: " << recvBuf << std::endl;
}

//5. 關閉socket
close(clientfd);

return 0;
}

以上代碼,伺服器端在地址 0.0.0.0:3000 啟動一個偵聽,客戶端連接伺服器成功後,給伺服器發送字元串"helloworld";伺服器收到後,將收到的字元串原封不動地發給客戶端。

在 Linux Shell 界面輸入以下命令編譯伺服器端和客戶端:

# 編譯 server.cpp 生成可執行文件 server
[root@localhost testsocket]# g++ -g -o server server.cpp
# 編譯 client.cpp 生成可執行文件 client
[root@localhost testsocket]# g++ -g -o client client.cpp

接著,我們看下執行效果,先啟動伺服器程序:

[root@localhost testsocket]# ./server

再啟動客戶端程序:

[root@localhost testsocket]# ./client

這個時候客戶端輸出:

send data successfully, data: helloworld
recv data successfully, data: helloworld

伺服器端輸出:

recv data from client, data: helloworld
send data to client successfully, data: helloworld

以上就是 TCP socket 網路通信的基本原理,對於很多讀者來說,上述代碼可能很簡單,更有點「玩具」的意味。但是深刻理解這兩個代碼片段是進一步學習開發複雜的網路通信程序的基礎。而且看似很簡單的代碼,卻隱藏了很多的玄機和原理,接下來的章節我們將以這兩段代碼為藍本,逐漸深入。


本文首發於『easyserverdev』公眾號,歡迎關注,轉載請保留版權信息。

歡迎加入高性能伺服器開發 QQ 群一起交流: 578019391 。


推薦閱讀:
相关文章