概述
TCP(Transmission Control Protocol,傳輸控制協議)是一種面向連接的、可靠的傳輸層協議。在網絡編程中,TCP常用于實現客戶端和服務器之間的可靠數據傳輸。本文將基于C語言實現TCP服務端和客戶端建立通信的過程。
三次握手
在 TCP 連接建立之前,客戶端和服務器之間需要進行三次握手來同步雙方的序列號,并確認雙方都準備好進行數據傳輸
- 第一次握手:客戶端向服務器發送一個 SYN(同步序列編號)報文段,表示請求建立連接。客戶端進入 SYN_SENT 狀態
- 第二次握手:服務器收到 SYN 報文段后,回復一個 SYN-ACK(同步序列編號 + 確認)報文段,表示同意建立連接。服務器進入 SYN_RCVD 狀態
- 第三次握手:客戶端收到 SYN-ACK 報文段后,回復一個 ACK(確認)報文段,表示確認收到服務器的響應。客戶端和服務器都進入 ESTABLISHED 狀態,連接正式建立
圖片來源:https://img-blog.csdnimg.cn/39bb4f4da21a4513b9506ecdf6a40cf3.png
四次揮手
通信結束時,客戶端或服務器可以發起斷開連接的請求。斷開連接的過程稱為四次揮手,以確保雙方都能正確關閉連接并釋放資源
- 第一次揮手:主動關閉方(通常是客戶端)發送一個 FIN(終止)報文段,表示不再發送數據。主動關閉方進入 FIN_WAIT_1 狀態
- 第二次揮手:被動關閉方(通常是服務器)收到 FIN 報文段后,回復一個 ACK 報文段,表示確認收到 FIN。被動關閉方進入 CLOSE_WAIT 狀態,而主動關閉方進入 FIN_WAIT_2 狀態
- 第三次揮手:被動關閉方在處理完所有未完成的數據后,發送一個 FIN 報文段,表示自己也不再發送數據。被動關閉方進入 LAST_ACK 狀態
- 第四次揮手:主動關閉方收到 FIN 報文段后,回復一個 ACK 報文段,表示確認收到 FIN。主動關閉方進入 TIME_WAIT 狀態,等待一段時間(通常為2倍的最大報文段生命周期,即2MSL),以確保被動關閉方收到了最后的 ACK。之后,主動關閉方進入 CLOSED 狀態,連接完全關閉
2MSL:MSL 的默認值是 30 秒,基于經驗選擇的一個保守估計,用來確保大多數網絡環境下的數據包都能被接收或者超時。
大部分操作系統都允許用戶調整 MSL 的值,從而改變
TIME_WAIT
狀態的持續時間
圖片來源:https://i-blog.csdnimg.cn/blog_migrate/843f121dd50cd8458daf1fa834bc1f36.png
TCP保證可靠傳輸方式
- 序列號:每個TCP報文段都有一個序列號,表示該報文段中的第一個字節在整個數據流中的位置。接收方可以根據序列號重新排序接收到的報文段,確保數據按順序傳遞
- 確認應答:接收方在收到報文段后,會發送一個確認應答,告訴發送方哪些數據已經成功接收。發送方根據確認應答判斷是否需要重傳丟失或損壞的報文段
- 超時重傳:如果發送方在一定時間內沒有收到確認應答,它會認為報文段可能丟失或延遲,并重新發送該報文段。TCP使用動態調整的超時機制來優化重傳策略
- 流量控制:TCP使用滑動窗口機制來控制發送方的發送速率,確保接收方不會被過多的數據淹沒。接收方會在確認應答中告知發送方當前可用的接收窗口大小,發送方根據這個信息調整自己的發送速率
- 擁塞控制:TCP通過多種算法(如慢啟動、擁塞避免、快速重傳和快速恢復)來動態調整發送方的發送速率,避免網絡擁塞。這些算法旨在在網絡負載較高時減小發送速率,在網絡條件改善時逐漸增加發送速率
TCP通信實現流程
涉及到的庫方法
-
創建套接字(Socket)
#include <sys/socket.h> int socket(int domain, int type, int protocol);
-
綁定(Bind)套接字到指定的IP地址和端口
#include <sys/socket.h> int bind(int socket, const struct sockaddr *address, socklen_t address_len);
-
監聽(Listen)客戶端的連接請求
#include <sys/socket.h> int listen(int socket, int backlog)
-
接受(Accept)客戶端的連接
#include <sys/socket.h> int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
-
連接、發送和接收數據(Send/Recv)
#include <sys/socket.h> int connect(int socket, const struct sockaddr *address, socklen_t address_len); ssize_t send(int socket, const void *buffer, size_t length, int flags); ssize_t recv(int socket, void *buffer, size_t length, int flags);
-
關閉連接(Close)
#include <unistd.h> int close(int fildes);
代碼實現
服務器代碼(Linux)
tcp_server.h
#ifndef TCP_SERVER_H
#define TCP_SERVER_H#include <pthread.h>
#include <netinet/in.h>
#include "cJson.h"
#include "common_base.h"// 定義常量
#define PORT 18888
#define BUFFER_SIZE 1024
#define TCP_IP "127.0.0.1"// 外部函數聲明
extern void getSerialNoStr(char *buf);/*** 啟動服務器主循環,等待客戶端連接*/
void start_serve_tcp(void *arg);/*** 解析接收到的消息
*/
void parse_message(char *data, size_t data_size);#endif // TCP_SERVER_H
tcp_server.c
#include "tcp_server.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <pthread.h>/*** 啟動TCP服務器,等待客戶端連接并處理接收到的數據。** @param arg 傳遞給線程的參數,通常為NULL。*/
void start_serve_tcp(void *arg)
{int server_fd, client_fd;struct sockaddr_in server_addr, client_addr;socklen_t client_addr_len = sizeof(client_addr);char buffer[BUFFER_SIZE] = {0};// 創建 Socketif ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1){printf("Socket 創建失敗");return;}// 配置服務器地址memset_s(&server_addr, sizeof(server_addr), 0, sizeof(server_addr));server_addr.sin_family = AF_INET;// 配置端口號和IPserver_addr.sin_port = htons(PORT);server_addr.sin_addr.s_addr = inet_addr(TCP_IP);// 綁定 Socketif (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {printf("綁定失敗\n");close(server_fd);return;}// 開始監聽if (listen(server_fd, 5) == -1){printf("監聽失敗\n");close(server_fd);return;}printf("服務器已啟動,監聽地址:%s:%d\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port));// 等待客戶端連接while (1){printf("等待客戶端連接...\n");if ((client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len)) == -1) {printf("接受客戶端連接失敗");continue;}printf("客戶端已連接:%s:%d\n",inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));// 接收數據int received_size;printf("receive before client_fd: %d\n", client_fd);while ((received_size = recv(client_fd, buffer, BUFFER_SIZE, 0)) > 0) {printf("收到數據,大小: %d 字節\n", received_size);parse_message(buffer, received_size);printf("jsonString: %s\n", jsonString);sleep(1);if(jsonString){ printf("receive after client_fd: %d\n", client_fd);// 發送 JSON 數據到客戶端ssize_t sent_size = send(client_fd, jsonString, strlen(jsonString), 0);if (sent_size == -1){printf("發送數據失敗\n");} else {printf("成功發送 %zd 字節到客戶端\n", sent_size);}free(jsonString);jsonString = NULL;}else {printf("生成 JSON 數據失敗\n");}}if (received_size == 0){printf("客戶端已斷開連接\n");} else if (received_size == -1){printf("接收數據失敗");}close(client_fd);}close(server_fd);
}void parse_message(char *data, size_t data_size)
{cJSON *rootMsg = NULL; cJSON *serialNo = NULL; // 序列號cJSON *netCmd = NULL; // 操作行為MANUAL_TRIG_PARAM manualTrParam; // 手動觸發抓拍接口傳參結構體// 確保消息頭部完整性(消息類型:4字節,數據長度:4字節)if (data_size < 8){printf("數據長度不足,無法解析\n");return;}// 解析消息類型和數據長度int message_type = ntohl(*(int *)data);int data_length = ntohl(*(int *)(data + 4));printf("消息類型: %d\n", message_type);printf("數據長度: %d\n", data_length);// 檢查數據長度是否匹配if (data_size < 8 + data_length) {printf("數據長度與實際內容不匹配\n");return;}// 解析消息內容char *message_content = (char *)malloc(data_length + 1);if (!message_content) {printf("內存分配失敗\n");return;}memcpy(message_content, data + 8, data_length);message_content[data_length] = '\0'; // 確保字符串以 \0 結尾printf("消息內容: %s\n", message_content);rootMsg = cJSON_Parse(message_content);if (NULL == rootMsg){printf("rootMsg is not json tpye\n");free(message_content);return;}printf("解析成功\n");serialNo = cJSON_GetObjectItem(rootMsg, "serialNo");netCmd = cJSON_GetObjectItem(rootMsg, "netCmd");if (serialNo && netCmd){printf("serialNo: %s\n", serialNo->valuestring);printf("netCmd: %s\n", netCmd->valuestring);} else{printf("缺少必要的 JSON 字段\n");cJSON_Delete(rootMsg);free(message_content);return;}char serial[64] = {0};getSerialNoStr(serial);printf("getSerialNoStr: %s\n", serial);printf("serialNo: %s\n", serialNo->valuestring);if(strncmp(serial, serialNo->valuestring, 9) == 0){// TODO:業務處理}else{printf("serialNo is not equal\n");}cJSON_Delete(rootMsg);free(message_content);
}
客戶端代碼(Windows)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <ws2tcpip.h>#pragma comment(lib, "ws2_32.lib")void send_message(const char *ip, int port, int message_type, const char *content) {WSADATA wsa;SOCKET sock;struct sockaddr_in server_addr;char buffer[1024];int recv_size;char recv_buffer[1024];// 初始化 Winsockif (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {printf("Winsock 初始化失敗,錯誤代碼: %d\n", WSAGetLastError());exit(EXIT_FAILURE);}// 創建 Socketif ((sock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {printf("Socket 創建失敗,錯誤代碼: %d\n", WSAGetLastError());WSACleanup();exit(EXIT_FAILURE);}// 配置服務器地址server_addr.sin_family = AF_INET;server_addr.sin_port = htons(port);server_addr.sin_addr.s_addr = inet_addr(ip);// 連接服務器if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) {printf("連接服務器失敗,錯誤代碼: %d\n", WSAGetLastError());closesocket(sock);WSACleanup();exit(EXIT_FAILURE);}// 準備消息int data_length = strlen(content);int net_message_type = htonl(message_type);int net_data_length = htonl(data_length);memcpy(buffer, &net_message_type, 4);memcpy(buffer + 4, &net_data_length, 4);memcpy(buffer + 8, content, data_length);// 發送消息send(sock, buffer, 8 + data_length, 0);// 接收服務器返回的數據if ((recv_size = recv(sock, recv_buffer, sizeof(recv_buffer) - 1, 0)) == SOCKET_ERROR) {printf("接收數據失敗,錯誤代碼: %d\n", WSAGetLastError());} else {recv_buffer[recv_size] = '\0'; // 確保以 null 結尾printf("服務器返回: %s\n", recv_buffer);}closesocket(sock);WSACleanup();
}int main() {send_message("127.0.0.1", 18888, 1, "{id: 'FS123456', command: 'NET_CAP'}");return 0;
}