核心要点速览

  • 协议栈:TCP/IP 四层模型(应用层→传输层→网络层→数据链路层)
  • TCP vs UDP:TCP 面向连接、可靠流式;UDP 无连接、高效数据报
  • 三次握手:建立 TCP 连接,确保双方收发能力正常;四次挥手:断开连接,释放全双工通道
  • TIME_WAIT:客户端第四次挥手后停留 2MSL,确保 ACK 送达、旧报文失效
  • Socket:网络编程接口,由 “IP + 端口” 唯一标识,TCP 需按固定流程(绑定 - 监听 - 连接 - 收发)编程

一、TCP/IP 四层模型

  • 应用层:提供具体业务协议(HTTP、FTP、DNS),定义数据格式和交互逻辑
  • 传输层:TCP/UDP,负责端到端(进程间)数据传输(可靠 / 高效)
  • 网络层:IP 协议,负责跨网络路由转发(寻址)
  • 数据链路层:处理物理介质上的帧传输(如以太网帧)

二、TCP 与 UDP 核心对比

对比维度 TCP(传输控制协议) UDP(用户数据报协议)
连接性 面向连接(三次握手建连,四次挥手断连) 无连接(直接发送,无需建连)
可靠性 可靠(重传、序列号、确认、滑动窗口、拥塞控制) 不可靠(无重传,可能丢包 / 乱序)
传输速率 低(含确认、重传等额外开销) 高(无额外开销,仅传输数据)
数据形式 字节流(无边界,需应用层定义分割) 数据报(有边界,一次收发一个完整报文)
拥塞控制 有(避免网络过载) 无(可能导致网络拥塞)
适用场景 文件传输、HTTP、邮件(需可靠性) 实时通信(视频 / 语音)、DNS(需实时性)

1. TCP:面向连接的可靠传输

  • 核心特性:
    • 面向连接:通信前必须通过三次握手建立连接,结束后通过四次挥手断开。
    • 可靠保障:通过序列号(保证有序)、确认应答(ACK,确保接收)、重传机制(丢失重发)、滑动窗口(流量控制)、拥塞控制(避免网络过载)实现数据不丢、不重、有序。
    • 字节流:数据无天然边界,需应用层自行处理粘包 / 半包问题。
  • 典型协议:HTTP、FTP、SMTP、SSH。

2. UDP:无连接的高效传输

  • 核心特性:
    • 无连接:无需建连 / 断连,直接发送数据,开销极低。
    • 不可靠:不保证数据到达、有序,无重传机制(丢包需应用层处理)。
    • 数据报:每个报文是独立单元(有边界),接收方一次接收一个完整报文(无粘包)。
  • 典型协议:RTP(实时音视频)、DNS、DHCP、游戏数据传输。

三、三次握手与四次挥手

1. 三次握手(建立 TCP 连接)

  • 目的:确认双方 “发送” 和 “接收” 能力正常,协商初始序列号(避免历史报文干扰)。
  • 流程(客户端→服务器):
    1. 第一次握手:客户端发SYN报文(同步请求),携带初始序列号seq = x
    2. 第二次握手:服务器回SYN+ACK报文(同步 + 确认),携带自身初始序列号seq = y、确认号ack = x + 1(表示已接收客户端x)。
    3. 第三次握手:客户端回ACK报文,确认号ack = y + 1(表示已接收服务器y)。
  • 关键问答:为什么需要三次握手?
    • 避免 “过期连接请求” 浪费服务器资源。若客户端旧SYN报文延迟到达,服务器二次握手后,客户端会因识别为无效请求而不发第三次ACK,服务器超时后释放资源(二次握手会导致服务器误建连)。

2. 四次挥手(断开 TCP 连接)

  • 目的:TCP 是全双工通信(双方可同时发数据),需分别关闭各自的发送通道。
  • 流程(客户端先发起关闭):
    1. 第一次挥手:客户端发FIN报文(终止请求),seq = u,表示不再发送数据。
    2. 第二次挥手:服务器回ACK报文,ack = u + 1,表示确认关闭请求(此时服务器→客户端通道仍可发数据)。
    3. 第三次挥手:服务器数据发送完毕,发FIN报文,seq = v,表示不再发送数据。
    4. 第四次挥手:客户端回ACK报文,ack = v + 1,表示确认关闭(此时客户端→服务器通道关闭)。
  • 关键问答:为什么需要四次挥手?
    • 全双工特性导致:第二次挥手仅确认客户端的关闭请求,服务器可能仍有未发送完的数据,需等数据发完后,再通过第三次挥手关闭自身发送通道,因此需四次交互。

3. TIME_WAIT 状态

  • 触发场景:客户端发送第四次挥手的ACK后进入该状态。
  • 停留时间:默认 2MSL(MSL 是报文最大生存时间,通常 1 分钟)。
  • 核心目的:
    1. 确保服务器能收到第四次挥手的ACK(若服务器未收到,会重发FIN,客户端可在TIME_WAIT内重传)。
    2. 避免客户端新连接收到旧连接的残留报文(2MSL 足够让网络中旧报文失效)。

四、Socket:网络编程接口

1. Socket 本质

  • 操作系统提供的网络通信接口,封装了 TCP/UDP 底层协议细节。
  • 唯一标识:IP地址 + 端口号(如(192.168.1.1, 8080)),对应进程间通信的端点。

2. TCP Socket 编程流程(极简示例)

服务器端(被动连接)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <cstring>

int main() {
// 1. 创建TCP Socket
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);

// 2. 绑定IP和端口
sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET; // IPv4
serv_addr.sin_addr.s_addr = INADDR_ANY; // 绑定所有网卡IP
serv_addr.sin_port = htons(8080); // 端口转换为网络序
bind(listen_fd, (sockaddr*)&serv_addr, sizeof(serv_addr));

// 3. 监听(最大等待连接数10)
listen(listen_fd, 10);

// 4. 阻塞等待客户端连接(返回与该客户端通信的新Socket)
sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int conn_fd = accept(listen_fd, (sockaddr*)&client_addr, &client_len);

// 5. 收发数据
char buf[1024] = {0};
recv(conn_fd, buf, sizeof(buf), 0); // 接收客户端数据
send(conn_fd, "Hello Client", 12, 0); // 发送响应

// 6. 关闭Socket
close(conn_fd);
close(listen_fd);
return 0;
}

客户端(主动连接)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>

int main() {
// 1. 创建TCP Socket
int client_fd = socket(AF_INET, SOCK_STREAM, 0);

// 2. 连接服务器
sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器IP
serv_addr.sin_port = htons(8080); // 服务器端口
connect(client_fd, (sockaddr*)&serv_addr, sizeof(serv_addr));

// 3. 收发数据
send(client_fd, "Hello Server", 12, 0); // 发送数据
char buf[1024] = {0};
recv(client_fd, buf, sizeof(buf), 0); // 接收响应

// 4. 关闭Socket
close(client_fd);
return 0;
}

3. UDP Socket 收发示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// UDP服务器端(接收数据)
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <cstring>

int main() {
int sock_fd = socket(AF_INET, SOCK_DGRAM, 0); // SOCK_DGRAM指定UDP

sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(8080);
bind(sock_fd, (sockaddr*)&serv_addr, sizeof(serv_addr));

char buf[1024] = {0};
sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
// 接收数据并获取客户端地址
recvfrom(sock_fd, buf, sizeof(buf), 0, (sockaddr*)&client_addr, &client_len);
// 回复客户端
sendto(sock_fd, "Hello UDP Client", 15, 0, (sockaddr*)&client_addr, client_len);

close(sock_fd);
return 0;
}

4. TCP 粘包解决方案示例(消息头 + 消息体,最常用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 发送端:打包消息(4字节长度+消息体)
void send_msg(int sock_fd, const std::string& data) {
int len = data.size();
len = htonl(len); // 长度转换为网络序
// 先发送消息长度(4字节)
send(sock_fd, &len, sizeof(len), 0);
// 再发送消息体
send(sock_fd, data.c_str(), data.size(), 0);
}

// 接收端:解析消息(先读长度,再读对应长度的消息体)
std::string recv_msg(int sock_fd) {
int len = 0;
// 先接收消息长度(4字节)
recv(sock_fd, &len, sizeof(len), 0);
len = ntohl(len); // 转换为主机序

// 再接收消息体
char* buf = new char[len + 1];
recv(sock_fd, buf, len, 0);
buf[len] = '\0';
std::string data(buf);
delete[] buf;
return data;
}