1、TCP基础铺垫
TCP/IP协议簇中包含了如TCP、UDP、IP、ICMP、ARP、HTTP等通信协议。TCP协议是TCP/IP协议簇中最为常见且重要的通信方式之一,它为互联网上的数据传输提供了可靠性和连接管理。
TCP(Transmission Control Protocol,传输控制协议)是面向连接的、可靠的、基于字节流的传输层通信协议。它主要用于在不可靠的互联网上提供可靠的数据传输。TCP被广泛应用于各种网络应用中,如Web浏览(HTTP/HTTPS)、电子邮件(SMTP、POP3、IMAP)、文件传输(FTP)等。
TCP通信时,是一发一收的,即TCP的数据发出时,会进行一个等待确认操作,确认数据是否正常发出并被接收方接收到数据信息。
TCP的关键特性:
(1)、面向连接:在数据传输之前,TCP需要通信双方建立一个连接。
(2)、可靠性:TCP采用了多种机制来确保数据的可靠传输,包括: 校验和 、序列号 、确认应答 、超时重传 、流量控制 、拥塞控制等技术。
(3)、全双工通信:TCP允许通信双方同时发送和接收数据,即双向通信可以在同一时间进行。
(4)、有序性:即使数据分段到达的顺序不同,TCP也能按照正确的顺序组装数据。
(5)、错误恢复:当发生错误或丢失数据时,TCP能够自动检测并尝试恢复丢失的数据。
(6)、连接终止:在数据传输完成后,TCP需要关闭连接。
(7)、多路复用:TCP支持在一个IP地址上通过不同的端口号区分多个应用程序或服务。
2、TCP关键技术梳理
(1)、TCP头部结构
(2)、三次握手
在TCP连接建立之前,客户端和服务器之间需要进行三次握手来同步双方的序列号,并确认双方都准备好进行数据传输。
第一次握手:客户端向服务器发送一个 SYN(同步序列编号)报文段,表示请求建立连接。客户端进入 SYN_SENT 状态。
第二次握手:服务器收到 SYN 报文段后,回复一个 SYN-ACK(同步序列编号 + 确认)报文段,表示同意建立连接。服务器进入 SYN_RCVD 状态。
第三次握手:客户端收到 SYN-ACK 报文段后,回复一个 ACK(确认)报文段,表示确认收到服务器的响应。客户端和服务器都进入 ESTABLISHED 状态,连接正式建立。
Client Server| || SYN (seq=0) ||-------------------------------------->|| | SYN_RCVD| <------------------------------------|| SYN-ACK (ack=1, seq=0) || ACK (seq=1, ack=1) ||-------------------------------------->|| | ESTABLISHED| ESTABLISHED || |
(3)、四次握手
当通信结束时,客户端或服务器可以发起断开连接的请求。断开连接的过程称为四次挥手,以确保双方都能正确关闭连接并释放资源。
第一次挥手:主动关闭方(通常是客户端)发送一个 FIN(终止)报文段,表示不再发送数据。主动关闭方进入 FIN_WAIT_1 状态。
第二次挥手:被动关闭方(通常是服务器)收到 FIN 报文段后,回复一个 ACK 报文段,表示确认收到 FIN。被动关闭方进入 CLOSE_WAIT 状态,而主动关闭方进入 FIN_WAIT_2 状态。
第三次挥手:被动关闭方在处理完所有未完成的数据后,发送一个 FIN 报文段,表示自己也不再发送数据。被动关闭方进入 LAST_ACK 状态。
第四次挥手:主动关闭方收到 FIN 报文段后,回复一个 ACK 报文段,表示确认收到 FIN。主动关闭方进入 TIME_WAIT 状态,等待一段时间(通常为2倍的最大报文段生命周期,即2MSL),以确保被动关闭方收到了最后的 ACK。之后,主动关闭方进入 CLOSED 状态,连接完全关闭。
Client Server| || FIN (seq=1) ||-------------------------------------->|| <------------------------------------|| ACK (ack=2) || FIN (seq=1) || <------------------------------------|| ACK (seq=2, ack=2) ||-------------------------------------->|| |
(4)、可靠传输
TCP通过以下机制确保数据的可靠传输:
序列号(Sequence Number):每个TCP报文段都有一个序列号,表示该报文段中的第一个字节在整个数据流中的位置。接收方可以根据序列号重新排序接收到的报文段,确保数据按顺序传递。
确认应答(Acknowledgment, ACK):接收方在收到报文段后,会发送一个确认应答,告诉发送方哪些数据已经成功接收。发送方根据确认应答判断是否需要重传丢失或损坏的报文段。
超时重传(Timeout and Retransmission):如果发送方在一定时间内没有收到确认应答,它会认为报文段可能丢失或延迟,并重新发送该报文段。TCP使用动态调整的超时机制来优化重传策略。
流量控制(Flow Control):TCP使用滑动窗口机制来控制发送方的发送速率,确保接收方不会被过多的数据淹没。接收方会在确认应答中告知发送方当前可用的接收窗口大小,发送方根据这个信息调整自己的发送速率。
拥塞控制(Congestion Control):TCP通过多种算法(如慢启动、拥塞避免、快速重传和快速恢复)来动态调整发送方的发送速率,避免网络拥塞。这些算法旨在在网络负载较高时减小发送速率,在网络条件改善时逐渐增加发送速率。
(5)、数据分段与重组
TCP将应用层的数据分成多个较小的报文段(Segment),并通过IP层进行传输。每个报文段包含一个TCP头部和一部分应用层数据。接收方会根据报文段的序列号将它们重新组合成完整的数据流。
最大报文段长度(MSS, Maximum Segment Size):为了提高传输效率并避免IP层的分片,TCP在连接建立时会协商一个合适的最大报文段长度。MSS通常由路径MTU(Maximum Transmission Unit)决定。
分段与重组:如果应用层数据较大,TCP会将其分成多个报文段进行传输。接收方会根据序列号将这些报文段重新组合成原始的应用层数据。
(6)、连接管理
TCP是一个面向连接的协议,这意味着在数据传输之前,必须先建立连接,传输结束后再关闭连接。TCP通过以下状态机来管理连接的生命周期:
- LISTEN:服务器处于监听状态,等待客户端的连接请求。
- SYN_SENT:客户端已发送 SYN 报文段,等待服务器的 SYN-ACK 响应。
- SYN_RCVD:服务器已收到 SYN 报文段,等待客户端的 ACK 确认。
- ESTABLISHED:连接已建立,双方可以开始传输数据。
- FIN_WAIT_1:主动关闭方已发送 FIN 报文段,等待对方的 ACK 确认。
- FIN_WAIT_2:主动关闭方已收到对方的 ACK,等待对方的 FIN。
- CLOSE_WAIT:被动关闭方已收到对方的 FIN,等待应用程序关闭连接。
- CLOSING:双方同时发送 FIN,等待对方的 ACK。
- LAST_ACK:被动关闭方已发送 FIN,等待对方的 ACK。
- TIME_WAIT:主动关闭方已收到对方的 FIN 和 ACK,等待2MSL后进入 CLOSED 状态。
- CLOSED:连接已完全关闭,资源已释放。
(7)、带外数据(OOB)
TCP支持带外数据传输,允许发送方发送紧急数据,而不必等待正常的TCP流排队。带外数据通常用于通知接收方有紧急事件发生,例如终止连接或重启服务。接收方可以通过SO_OOBINLINE选项将带外数据作为普通数据处理,或者通过 recv() 函数的 MSG_OOB 标志单独接收带外数据。
(8)、半关闭(Half-Close)
TCP允许一方关闭连接的发送方向,而保持接收方向仍然打开。这种操作称为半关闭(Half-Close)。例如,客户端可以发送 FIN 报文段,表示不再发送数据,但仍然可以接收来自服务器的数据。服务器在收到 FIN 后,可以继续发送数据,直到它也发送 FIN 报文段,最终关闭整个连接。
3、TCP通信编程实现
3.1、TCP通信常用接口
TCP实现网络通信的接口操作与文件操作惊人的相似,在对文件操作中,一般是先打开操作的文件描述符,然后对文件描述符配置相应的属性(如错误处理和多路复用等),设置完文件描述符的配置后,再进行对文件的读写操作,最后关闭文件描述符。而TCP网络通信时,也是先创建一个通信套接字,然后配置套接字属性及绑定、连接等操作,再进行数据的发送接收,最后关闭套接字。接下来这部分将重点介绍TCP网络通信的常用API。
提醒:TCP通信接口API中,Windows和Linux的在使用上面存在一些区别,在下面主要介绍的是Linux平台下的API及使用。
(1)、socket
函数原型:#include <sys/socket.h>int socket(int domain, int type, int protocol);
函数功能:用于创建一个套接字的系统调用,它返回一个新的套接字描述符(一个非负整数),这个描述符可以用来进行后续的网络通信操作。
参数:domain:指定协议族(也称为地址族)AF_INET 或 PF_INET:IPv4 互联网协议AF_INET6 或 PF_INET6:IPv6 互联网协议AF_UNIX 或 PF_UNIX:本地通信(Unix域套接字)AF_ROUTE:路由套接字,用于与内核路由表交互...type:指定套接字类型SOCK_STREAM:提供面向连接、可靠的数据传输服务,使用TCP协议SOCK_DGRAM:提供无连接、不可靠的数据报服务,使用UDP协议SOCK_RAW:原始套接字,允许直接访问低层协议,如IP或ICMPSOCK_SEQPACKET:有序的、可靠的、双向传输的数据包服务,类似于 SOCK_STREAM,但以消息为单位SOCK_RDM:可靠的无连接数据报服务,保证消息按顺序到达,但不保证无重复...protocol:指定具体的协议设置为0,表示使用默认协议(根据domain和type自动选择)AF_INET 和 SOCK_STREAM,可以指定 IPPROTO_TCPAF_INET 和SOCK_DGRAM,可以指定IPPROTO_UDPAF_PACKET 和SOCK_RAW,可以指定具体的以太网协议,如ETH_P_IP ...
返回值:成功返回套接字的文件描述符(非0),失败返回-1,并设置 errno 变量来指示错误原因。
(2)、bind
函数原型:#include <sys/socket.h>int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
函数功能:用于将一个本地地址(IP地址和端口号)绑定到一个已创建的套接字上。
参数:socket:要绑定地址的套接字描述符address:指向包含地址信息的sockaddr 结构体的指针对于 IPv4 (AF_INET),使用 struct sockaddr_in对于 IPv6 (AF_INET6),使用 struct sockaddr_in6对于 Unix 域套接字 (AF_UNIX),使用 struct sockaddr_unaddress_len:指定address指向的结构体的大小,以字节为单位对于 sockaddr_in 通常是sizeof(struct sockaddr_in)对于 sockaddr_in6 通常是sizeof(struct sockaddr_in6)
返回值:成功返回0,失败返回 -1,并设置 errno 变量来指示错误原因。
(3)、listen
函数原型:#include <sys/socket.h>int listen(int socket, int backlog);
函数功能:用于将一个未连接的套接字转换为监听套接字,使其能够接收传入的连接请求。
参数:socket:已经通过 bind() 绑定到一个本地地址的套接字backlog:指定监听队列的最大长度,即操作系统可以为该套接字排队的最大未接受连接数
返回值:成功返回0,失败返回 -1,并设置 errno 变量来指示错误原因。
(4)、accept
函数原型: #include <sys/socket.h>int accept(int socket, struct sockaddr *restrict address,
socklen_t *restrict address_len);
函数功能:用于服务器端接受一个传入的连接请求。当有客户端尝试连接到一个监听中的套接字时,accept() 会创建一个新的套接字来处理这个连接,而原来的监听套接字继续等待其他连接请求。
参数:socket:设置为监听状态的套接字描述符。该套接字应该已经绑定到一个本地地址,并且正在监听传入的连接请求address:指向 struct sockaddr 结构体的指针,用于接收客户端的地址信息。如果不需要获取客户端地址,可以传递 NULLaddress_len:指向一个 socklen_t 类型变量的指针,该变量在调用时应包含 address 指向的结构体的大小。函数返回时,address_len 将被更新为实际存储在 address 中的地址长度。如果 address 是 NULL,则 address_len 也应该是 NULL。
返回值:成功时,返回一个新的文件描述符,表示与客户端的连接。这个新的套接字专门用于与特定客户端通信。失败时,返回 -1,并且会设置 errno 变量来指示具体的错误原因。
(5)、connect
函数原型: #include <sys/socket.h>int connect(int socket, const struct sockaddr *address,
socklen_t address_len);
函数功能:用于主动发起一个到指定服务器的连接请求。将客户端套接字与远程服务器的地址和端口关联起来,从而建立一个通信通道。
参数:socket:已经配置好的套接字(例如,设置了协议族、类型和协议),但尚未绑定到本地地址或连接到远程地址address:指向 struct sockaddr 结构体的指针,该结构体包含要连接的远程服务器的地址信息对于 IPv4 (AF_INET),通常使用 struct sockaddr_in对于 IPv6 (AF_INET6),通常使用 struct sockaddr_in6对于 Unix 域套接字 (AF_UNIX),通常使用 struct sockaddr_unaddress_len:指定address指向的结构体的大小,以字节为单位对于 sockaddr_in 通常是sizeof(struct sockaddr_in)对于 sockaddr_in6 通常是sizeof(struct sockaddr_in6)
返回值:成功返回0,失败返回 -1,并设置 errno 变量来指示错误原因。
(6)、send
函数原型: #include <sys/socket.h>ssize_t send(int socket, const void *buffer, size_t length, int flags);
函数功能:用于通过已连接的套接字发送数据。它通常用于面向连接的协议(如TCP),但也可以用于无连接的协议(如UDP)。
参数:socket:已经连接的套接字描述符buffer:指向要发送的数据缓冲区的指针。该缓冲区包含要传输的字节数据length:要发送的数据长度,以字节为单位。如果缓冲区中的数据长度小于 len,则只发送缓冲区中实际存在的数据 flags:控制 send() 行为的标志0:默认行为,没有特殊选项MSG_OOB:发送带外数据(out-of-band data),适用于支持带外数据的协议(如TCP)MSG_DONTROUTE:跳过路由表查找,直接将数据发送到目标地址。通常用于诊断工具或特定网络配置MSG_DONTWAIT:使 send() 调用非阻塞。如果套接字是阻塞模式,此标志会使 send() 在无法立即发送所有数据时返回,而不是等待。如果套接字已经是非阻塞模式,此标志通常不会产生额外效果MSG_NOSIGNAL:防止 SIGPIPE 信号在尝试向已关闭的连接写入时生成。这对于避免程序意外终止非常有用
返回值:成功时,返回实际发送的字节数,可能小于length。失败时,返回 -1,并且会设置 errno 变量来指示具体的错误原因
(7)、recv
函数原型: #include <sys/socket.h>ssize_t recv(int socket, void *buffer, size_t length, int flags);
函数功能:用于从已连接的套接字接收数据。它通常用于面向连接的协议(如TCP),但也可以用于无连接的协议(如UDP)。
参数:socket:已经连接的套接字描述符。buffer:指向接收数据缓冲区的指针。该缓冲区将存储从套接字接收到的数据。length:指定缓冲区的最大长度,以字节为单位。flags:控制 recv() 行为的标志0:默认行为,没有特殊选项MSG_OOB:接收带外数据(out-of-band data),适用于支持带外数据的协议(如TCP)MSG_PEEK:窥视模式,数据被读取到缓冲区但不从输入队列中移除。下次调用 recv() 时仍然可以读取这些数据MSG_WAITALL:等待直到接收到请求的所有数据(len 个字节)。如果设置了这个标志,recv() 可能在接收到部分数据后仍然阻塞,直到接收到所有数据或发生错误MSG_DONTWAIT:使 recv() 调用非阻塞。如果套接字是阻塞模式,此标志会使 recv() 在无法立即读取数据时返回,而不是等待。如果套接字已经是非阻塞模式,此标志通常不会产生额外效果
返回值:成功时,返回实际接收到的字节数。如果返回值为 0,表示对端已经关闭了连接。失败时,返回 -1,并且会设置 errno 变量来指示具体的错误原因。
(8)、close
函数原型: #include <unistd.h>int close(int fildes);
函数功能:用于关闭一个打开的文件描述符,包括普通文件、设备、管道以及套接字等。关闭文件描述符后,操作系统将释放与该描述符相关的资源,并使其可以被重新分配给其他文件或套接字。
参数:fildes:要关闭的文件描述符。这个描述符可以是通过 open()、socket()、pipe() 等函数创建的。
返回值:成功返回0,失败返回 -1,并设置 errno 变量来指示错误原因。
3.2、TCP通信代码
(1)、客户端代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>#define SERVER_IP "127.0.0.1" // 服务器IP地址
#define SERVER_PORT 8080 // 服务器端口
#define LOCAL_IP "0.0.0.0" // 本地IP地址 (INADDR_ANY 表示任意可用接口)
#define LOCAL_PORT 6666 // 本地端口 (0 表示由操作系统选择)
#define BUFFER_SIZE 1024 // 缓冲区大小void error_exit(const char *msg)
{perror(msg);exit(EXIT_FAILURE);
}void handle_receive(int client_sockfd)
{char buffer[BUFFER_SIZE];ssize_t bytes_received;while (1) {bytes_received = recv(client_sockfd, buffer, BUFFER_SIZE - 1, 0);if (bytes_received < 0) {perror("Receive failed");break;} else if (bytes_received == 0) {printf("Server closed the connection\n");break;} else {buffer[bytes_received] = '\0'; // 确保字符串以null结尾printf("Received %zd bytes: %s\n", bytes_received, buffer);}}close(client_sockfd); // 关闭客户端套接字printf("Client socket closed in receive process\n");exit(EXIT_SUCCESS); // 子进程退出
}int main(int argc, const char **argv)
{int client_sockfd;struct sockaddr_in server_addr, local_addr;pid_t child_pid;// 1、创建一个TCP套接字client_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (client_sockfd < 0) {error_exit("Socket creation failed");}printf("Socket created successfully\n");// 配置本地地址结构memset(&local_addr, 0, sizeof(local_addr));local_addr.sin_family = AF_INET;local_addr.sin_port = htons(LOCAL_PORT); // 设置本地端口if (inet_pton(AF_INET, LOCAL_IP, &local_addr.sin_addr) <= 0) {error_exit("Invalid local address/ Address not supported");}// 2、绑定本地地址if (bind(client_sockfd, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0) {error_exit("Bind failed");}printf("Bound to local address %s:%d\n", LOCAL_IP, LOCAL_PORT);// 配置服务器地址结构memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(SERVER_PORT); // 设置服务器端口if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {error_exit("Invalid server address/ Address not supported");}// 3、连接到服务器if (connect(client_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {error_exit("Connection failed");}printf("Connected to server at %s:%d\n", SERVER_IP, SERVER_PORT);// 4、创建子进程处理接收数据child_pid = fork();if (child_pid < 0) {error_exit("Fork failed");} else if (child_pid == 0) {// 子进程:关闭监听套接字,处理接收数据handle_receive(client_sockfd);} else {// 父进程:继续发送数据// 5、发送数据const char *message = "Hello, Server!";ssize_t bytes_sent = send(client_sockfd, message, strlen(message), 0);if (bytes_sent < 0) {error_exit("Send failed");}printf("Sent %zd bytes: %s\n", bytes_sent, message);printf("Enter message to send (or type 'exit' to quit): \n");// 父进程可以继续发送更多数据while (1) {char buffer[BUFFER_SIZE] = {0};fgets(buffer, BUFFER_SIZE, stdin);buffer[strcspn(buffer, "\n")] = '\0'; // 去掉换行符if (strcmp(buffer, "exit") == 0) {break;}bytes_sent = send(client_sockfd, buffer, strlen(buffer), 0);if (bytes_sent < 0) {error_exit("Send failed");}printf("Sent %zd bytes: %s\n", bytes_sent, buffer);}// 6、关闭套接字close(client_sockfd);printf("Socket closed\n");// 等待子进程结束,避免僵尸进程waitpid(child_pid, NULL, 0);}return 0;
}
(2)、服务器代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/wait.h>#define SERVER_IP "127.0.0.1" // 服务器IP地址,如果为 0.0.0.0则是监听所有的IP
#define SERVER_PORT 8080 // 服务器端口
#define BUFFER_SIZE 1024 // 缓冲区大小
#define BACKLOG 5 // 监听队列的最大长度void error_exit(const char *msg)
{perror(msg);exit(EXIT_FAILURE);
}void handle_client(int client_sockfd)
{char buffer[BUFFER_SIZE];ssize_t bytes_received;while ((bytes_received = recv(client_sockfd, buffer, BUFFER_SIZE - 1, 0)) > 0) {buffer[bytes_received] = '\0'; // 确保字符串以null结尾printf("Received %zd bytes from client: %s\n", bytes_received, buffer);// 回显接收到的数据if (send(client_sockfd, buffer, bytes_received, 0) < 0) {perror("Send failed");break;}}if (bytes_received == 0) {printf("Client closed the connection\n");} else if (bytes_received < 0) {perror("Receive failed");}close(client_sockfd); // 关闭客户端套接字printf("Client socket closed\n");
}int main(int argc, const char **argv)
{int server_sockfd, client_sockfd;struct sockaddr_in server_addr, client_addr;socklen_t client_addr_len = sizeof(client_addr);pid_t child_pid;//1、创建一个TCP套接字server_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (server_sockfd < 0) {error_exit("Socket creation failed");}printf("Socket created successfully\n");// 配置服务器地址结构memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(SERVER_PORT); // 设置服务器端口if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {error_exit("Invalid server address/ Address not supported");}//2、绑定套接字到指定的地址和端口if (bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {error_exit("Bind failed");}printf("Bind completed\n");//3、开始监听连接请求if (listen(server_sockfd, BACKLOG) < 0) // backlog 设置为 5{error_exit("Listen failed");}printf("Server listening on port %d\n", SERVER_PORT);//4、接受并处理客户端连接while (1) {client_addr_len = sizeof(client_addr);client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_addr, &client_addr_len);if (client_sockfd < 0) {perror("Accept failed");continue;}printf("Accepted connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));// 创建子进程处理客户端连接child_pid = fork();if (child_pid < 0) {perror("Fork failed");close(client_sockfd);continue;} else if (child_pid == 0) {// 子进程:关闭监听套接字,处理客户端连接close(server_sockfd);handle_client(client_sockfd);exit(EXIT_SUCCESS);} else {// 父进程:关闭客户端套接字,继续监听新的连接close(client_sockfd);// 等待子进程结束,避免僵尸进程waitpid(-1, NULL, WNOHANG);}}//5、关闭监听套接字close(server_sockfd);printf("Server socket closed\n");return 0;
}
如下图所示的是TCP的客户端与服务器的API调用过程的核心梳理总结。
4、TCP通信展示
上述TCP客户端与服务器代码编译运行后,实现的效果如下所示。