3、TCP客户端与服务端:TCP三次握手、四次挥手、TCP客户端实现、TCP服务端实现
各位同学,咱们今天聊点实在的。TCP 这个东西,说白了就是网络通讯的「定海神针」。你上网、刷视频、发消息,底层全是它在撑着。我做了十几年嵌入式通讯,可以负责任地告诉你:搞懂 TCP,你就掌握了网络编程的半壁江山。
这一节,咱们不扯虚的。直接上硬菜:三次握手、四次挥手,然后手撸一个客户端和服务端。嗯,跟着我的节奏走,保证你听完就能用。
3.1 TCP 三次握手:连接是怎么「谈」出来的?
先问大家一个问题:为什么 TCP 要握三次手?两次不行吗?四次呢?
我当年刚入行时也觉得这问题很玄乎。后来在项目里调试一个工业控制器的远程升级功能,发现设备老是连不上服务器。抓包一看,好家伙,握手阶段就卡住了。从那以后,我对三次握手有了刻骨铭心的理解。
三次握手的本质,说白了就是「确认双方都能收发数据」。你想想看,如果只握两次,服务端怎么知道客户端已经准备好接收数据了?
流程是这样的:
- 第一次握手(SYN):客户端发送一个 SYN 包,告诉服务端「我想跟你连」。此时客户端进入 SYN_SENT 状态。
- 第二次握手(SYN+ACK):服务端收到后,回复 SYN+ACK,意思是「收到你的请求,我也准备好了」。服务端进入 SYN_RCVD 状态。
- 第三次握手(ACK):客户端再发一个 ACK,表示「好的,我知道了,咱们开始传数据吧」。双方进入 ESTABLISHED 状态。
核心要点:三次握手不仅仅是建立连接,它还在协商一些关键参数,比如最大报文段长度(MSS)、窗口缩放因子等。这些细节,很多教材不讲,但实际项目中经常踩坑。
我曾经遇到过一个案例:某嵌入式设备因为 TCP 窗口太小,导致传输速度只有理论值的十分之一。查了两天才发现,是握手阶段协商的窗口参数有问题。所以,别小看这三次握手。
3.2 TCP 四次挥手:分手也要体面
连接建立要握手,断开连接要挥手。四次挥手,比握手多一次,为什么?
原因很简单:TCP 是全双工的。也就是说,数据可以双向同时传输。断开时,每一方向都要单独说再见。
流程是这样的:
- 第一次挥手(FIN):主动关闭方(比如客户端)发送 FIN,表示「我这边没数据要发了」。进入 FIN_WAIT_1 状态。
- 第二次挥手(ACK):被动关闭方(服务端)回复 ACK,表示「收到,我知道你发完了」。但此时服务端可能还有数据要发,所以连接还没完全断。主动方进入 FIN_WAIT_2 状态。
- 第三次挥手(FIN):服务端数据发完了,发送 FIN,表示「我也没数据了」。
- 第四次挥手(ACK):客户端回复 ACK,然后进入 TIME_WAIT 状态,等待 2MSL(最大报文生存时间)后彻底关闭。
避坑指南:我曾经在做一个远程数据采集系统时,发现服务端频繁出现端口被占用的错误。查了半天,原来是客户端没有正确处理 TIME_WAIT 状态,导致端口无法立即复用。解决方案是设置 SO_REUSEADDR 选项。嗯,这个坑我替你们踩过了。
3.3 TCP 客户端实现:从零开始写一个
理论讲完了,咱们来点实际的。我习惯用 C 语言写嵌入式网络程序,因为可控性高。下面是一个最简的 TCP 客户端实现:
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
int main() {
int sockfd;
struct sockaddr_in server_addr;
char buffer[1024] = {0};
// 1. 创建 socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
return -1;
}
// 2. 设置服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = inet_addr("192.168.1.100");
// 3. 连接服务器(触发三次握手)
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connect failed");
return -1;
}
printf("Connected to server\n");
// 4. 发送数据
char *msg = "Hello from client";
send(sockfd, msg, strlen(msg), 0);
// 5. 接收响应
recv(sockfd, buffer, sizeof(buffer), 0);
printf("Server response: %s\n", buffer);
// 6. 关闭连接(触发四次挥手)
close(sockfd);
return 0;
}
个人经验:实际项目中,我一般会在 connect 之后加一个超时处理。因为嵌入式设备网络环境复杂,如果服务器没响应,程序会一直卡在 connect 上。用非阻塞 socket + select 可以优雅解决这个问题。
3.4 TCP 服务端实现:做个靠谱的「接客」程序
服务端比客户端复杂一些。它要监听、接受连接、处理多个客户端。咱们先写一个单线程的版本:
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
int main() {
int server_fd, client_fd;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[1024] = {0};
// 1. 创建 socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket failed");
return -1;
}
// 2. 绑定地址和端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) {
perror("bind failed");
return -1;
}
// 3. 开始监听
if (listen(server_fd, 3) < 0) {
perror("listen failed");
return -1;
}
printf("Server listening on port 8080\n");
// 4. 接受客户端连接(完成三次握手)
client_fd = accept(server_fd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
if (client_fd < 0) {
perror("accept failed");
return -1;
}
// 5. 接收数据
recv(client_fd, buffer, sizeof(buffer), 0);
printf("Received: %s\n", buffer);
// 6. 发送响应
char *response = "Hello from server";
send(client_fd, response, strlen(response), 0);
// 7. 关闭连接
close(client_fd);
close(server_fd);
return 0;
}
注意:上面的代码只能处理一个客户端。实际项目中,我会用多线程或者 select/epoll 来处理并发。特别是嵌入式场景,资源有限,我建议用 select 模型,既简单又高效。
3.5 核心流程图:TCP 通讯全貌
下面这张图,是我自己画的。它把三次握手、数据传输、四次挥手串在了一起。你盯着看三分钟,整个流程就刻在脑子里了。
3.6 常见问题与避坑指南
| 问题 | 现象 | 解决方案 |
|---|---|---|
| 连接超时 | connect 卡住不动 | 设置非阻塞 socket + 超时处理 |
| 端口被占用 | bind 返回 EADDRINUSE | 使用 SO_REUSEADDR 选项 |
| 粘包问题 | 接收端一次收到多条消息 | 自定义应用层协议,添加消息头和长度字段 |
| 半关闭状态 | 一方关闭,另一方还在发数据 | 使用 shutdown() 而不是 close() 实现优雅关闭 |
重要提醒:嵌入式系统中,TCP 的缓冲区大小往往有限。我建议你在实际项目中,根据网络带宽和 CPU 性能,合理调整 SO_RCVBUF 和 SO_SNDBUF。太小了丢包,太大了浪费内存。
好了,这一节的内容就到这里。TCP 的三次握手和四次挥手,是网络通讯的基石。客户端和服务端的实现,是动手能力的体现。我建议你照着代码敲一遍,然后抓个包看看握手过程。嗯,实践出真知。
公众号:蓝海资料掘金营,微信deep3321