文章目錄
- 前言
- 一,服務器端流程
- 1-1 綁定協議
- 1-2 綁定IP和端口
- 1-3 監聽客戶端
- 1-4 接收連接
- 1-5 收發數據
- 1-6 關閉連接
- 1-7 服務端整體代碼
- 二,客戶端流程
- 2-1 指定地址和端口
- 2-2 連接服務器
- 2-3 發送消息
- 2-4 客戶端整體代碼
前言
TCP
的通信過程就像兩個人打電話:客戶端先和服務端三次握手建立一條可靠的連接通道,之后數據會以字節流的形式在這條通道里雙向傳輸,系統會負責把數據切片、編號、確認和重傳,保證信息不丟失、不重復、按順序送達,最后通過四次揮手優雅地斷開連接。
連接流程大致如下圖
一,服務器端流程
1-1 綁定協議
TCP
通信也和UDP
一樣需要先創建套接字
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
這里的SOCK_STREAM
,說明套接字是用字節流的形式傳遞數據,也就是TCP
通信
1-2 綁定IP和端口
sockaddr_in addr {};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY; // 本機任意 IP
addr.sin_port = htons(12345); // 端口 12345
if (bind(server_fd, (sockaddr * ) & addr, sizeof(addr)) < 0) {perror("bind");return 1;
}
addr.sin_family = AF_INET;
的意思就是,設置協議為IPv4
,其它的不過多講解就是綁定端口和將sockaddr_in
轉化為sockaddr
addr.sin_addr.s_addr = INADDR_ANY
表示本機的客戶端的任意IP
都可以連接服務端
1-3 監聽客戶端
listen(server_fd, 5);
std::cout << "服務端啟動,等待客戶端連接..." << std::endl;
server_fd
是我們socket
創建的套接字,用于服務端得知是哪一個套接字要監聽,所以必須傳入 server_fd
。沒有 server_fd
,內核就不知道“我要監聽哪條網絡通道”,就沒法接受連接
這里的這個參數5
表明:連接隊列的最大長度,內核會維護一個隊列,存放已到達但還沒被 accept()
處理的客戶端連接,這里寫 5
表示最多允許排隊 5
個連接請求。超過的請求可能會被拒絕。
1-4 接收連接
sockaddr_in client_addr{};
socklen_t len = sizeof(client_addr);
int client_fd = accept(server_fd, (sockaddr*)&client_addr, &len);
我們需要接收客戶端的連接,這時會用到 sockaddr_in
來存儲客戶端的地址信息。accept()
會返回一個新的套接字 client_fd
。因此服務器中會有 兩種套接字:
-
server_fd
:用于監聽端口和接受客戶端的連接請求。它可以同時管理多個客戶端連接,但 不能直接用于數據通信。 -
client_fd
:由accept()
返回,用于和 特定客戶端 進行通信。每個客戶端連接都會對應一個獨立的client_fd
,可以通過read()
和write()
發送或接收數據。
簡而言之:server_fd
用于建立連接,client_fd
用于通信,server_fd
可以服務多個客戶端,而每個 client_fd
只對應一個客戶端。
1-5 收發數據
char buffer[1024];
int n = read(client_fd, buffer, sizeof(buffer) - 1);
if (n > 0) {buffer[n] = '\0';std::cout << "收到客戶端消息: " << buffer << std::endl;std::string reply = "Hello from server!";write(client_fd, reply.c_str(), reply.size());
}
先是讀取數據,我們先定義一個buffer
,將對應客戶端的套接字對應的文件信息讀取到buffer
中,再使用write
將數據發送到client_fd
對應的套接字當中,這里其實沒什么特別的,就是收數據/發數據
1-6 關閉連接
close(client_fd);
close(server_fd);
雖然只有一行,但是OS
這里會讓客戶端和服務端發生四次揮手:
-
FIN(發送方): 服務端調用
close(client_fd)
內核向客戶端發送FIN
報文,表示“我已經沒有數據要發了”。 -
ACK(接收方): 客戶端收到
FIN
后,回復ACK
報文,確認收到,客戶端仍然可以發送剩余數據。 -
FIN(接收方): 客戶端發送完數據后,也調用
close()
,向服務端發送FIN
報文,表示“我也發送完了”。 -
ACK(發送方): 服務端收到
FIN
后,發送ACK
報文 確認。連接真正關閉。
1-7 服務端整體代碼
int main() {// 1. 創建套接字int server_fd = socket(AF_INET, SOCK_STREAM, 0);if (server_fd < 0) {perror("socket");return 1;}// 2. 綁定 IP 和端口sockaddr_in addr {};addr.sin_family = AF_INET;addr.sin_addr.s_addr = INADDR_ANY; // 本機任意 IPaddr.sin_port = htons(12345); // 端口 12345if (bind(server_fd, (sockaddr * ) & addr, sizeof(addr)) < 0) {perror("bind");return 1;}// 3. 監聽listen(server_fd, 5);std::cout << "服務端啟動,等待客戶端連接..." << std::endl;// 4. 接受連接sockaddr_in client_addr {};socklen_t len = sizeof(client_addr);int client_fd = accept(server_fd, (sockaddr * ) & client_addr, & len);if (client_fd < 0) {perror("accept");return 1;}std::cout << "客戶端已連接!" << std::endl;// 5. 收發數據char buffer[1024];int n = read(client_fd, buffer, sizeof(buffer) - 1);if (n > 0) {buffer[n] = '\0';std::cout << "收到客戶端消息: " << buffer << std::endl;std::string reply = "Hello from server!";write(client_fd, reply.c_str(), reply.size());}// 6. 關閉連接close(client_fd);close(server_fd);return 0;
}
二,客戶端流程
在客戶端流程當中存在和服務端一樣的流程,就是創建套接字,這里我們直接省略了。
2-1 指定地址和端口
sockaddr_in server_addr {};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(12345);
inet_pton(AF_INET, "127.0.0.1", & server_addr.sin_addr); // 本地回環地址
這里我們創建的inet_pton
的作用是把點分十進制的 IP
地址字符串轉換成網絡字節序的二進制形式
因為 socket
系統調用只能處理二進制的網絡地址,而不能直接識別字符串形式的 IP
,人類習慣用 "127.0.0.1"
這樣的點分十進制 IP
。
內核底層在發送數據時,需要 32
位的二進制形式(網絡字節序)來表示 IP
地址,inet_pton
就完成了 “人類可讀 IP
→ 網絡可用二進制 IP
” 的轉換,如果不做轉換,connect()
或 bind()
會因為地址無效而失敗
2-2 連接服務器
我們通過connet
來與服務端建立連接
if (connect(sock, (sockaddr * ) & server_addr, sizeof(server_addr)) < 0) {perror("connect");return 1;
}
這里沒有什么很特別的,就是傳遞我們的地址端口接口體給服務端,然后服務端拿到結構體和客戶端進行連接,并且客戶端的sock
套接字也和服務端的套接字對應的文件進行連接
2-3 發送消息
std::string msg = "Hello Server!";
write(sock, msg.c_str(), msg.size());
char buffer[1024];
int n = read(sock, buffer, sizeof(buffer) - 1);
if (n > 0) {buffer[n] = '\0';std::cout << "收到服務端回復: " << buffer << std::endl;
}
通過write
和read
發送和接收消息,也是很簡單的代碼
2-4 客戶端整體代碼
int main() {// 1. 創建套接字int sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0) {perror("socket");return 1;}// 2. 指定服務端地址sockaddr_in server_addr {};server_addr.sin_family = AF_INET;server_addr.sin_port = htons(12345);inet_pton(AF_INET, "127.0.0.1", & server_addr.sin_addr); // 本地回環地址// 3. 連接服務器if (connect(sock, (sockaddr * ) & server_addr, sizeof(server_addr)) < 0) {perror("connect");return 1;}std::cout << "已連接服務器!" << std::endl;// 4. 發送消息std::string msg = "Hello Server!";write(sock, msg.c_str(), msg.size());// 5. 接收回復char buffer[1024];int n = read(sock, buffer, sizeof(buffer) - 1);if (n > 0) {buffer[n] = '\0';std::cout << "收到服務端回復: " << buffer << std::endl;}// 6. 關閉close(sock);return 0;
}
演示結果:
服務端
root@hcss-ecs-f59a:/gch/code/HaoHao/learn3/day6# ./server
服務端啟動,等待客戶端連接...
客戶端已連接!
收到客戶端消息: Hello Server!
客戶端
root@hcss-ecs-f59a:/gch/code/HaoHao/learn3/day6# ./client
已連接服務器!
收到服務端回復: Hello from server!
本博客中只是做了一個簡單的服務端和客戶端的通信,在實際項目當中,這種通信代碼還是前篇一律的,所以我就采用了分布式講解