核心要点速览
- 协议栈: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 连接)
- 目的:确认双方 "发送" 和 "接收" 能力正常,协商初始序列号(避免历史报文干扰)。
- 流程 (客户端→服务器):
- 第一次握手:客户端发
SYN报文(同步请求),携带初始序列号seq = x。
- 第二次握手:服务器回
SYN+ACK报文(同步 + 确认),携带自身初始序列号seq = y、确认号ack = x + 1(表示已接收客户端x)。
- 第三次握手:客户端回
ACK报文,确认号ack = y + 1(表示已接收服务器y)。
- 问答 :为什么需要三次握手?
- 避免 "过期连接请求" 浪费服务器资源。若客户端旧
SYN报文延迟到达,服务器二次握手后,客户端会因识别为无效请求而不发第三次ACK,服务器超时后释放资源(二次握手会导致服务器误建连)。
2. 四次挥手(断开 TCP 连接)
- 目的:TCP 是全双工通信(双方可同时发数据),需分别关闭各自的发送通道。
- 流程 (客户端先发起关闭):
- 第一次挥手:客户端发
FIN报文(终止请求),seq = u,表示不再发送数据。
- 第二次挥手:服务器回
ACK报文,ack = u + 1,表示确认关闭请求(此时服务器→客户端通道仍可发数据)。
- 第三次挥手:服务器数据发送完毕,发
FIN报文,seq = v,表示不再发送数据。
- 第四次挥手:客户端回
ACK报文,ack = v + 1,表示确认关闭(此时客户端→服务器通道关闭)。
- 问答 :为什么需要四次挥手?
- 全双工特性导致:第二次挥手仅确认客户端的关闭请求,服务器可能仍有未发送完的数据,需等数据发完后,再通过第三次挥手关闭自身发送通道,因此需四次交互。
3. TIME_WAIT 状态
- 触发场景 :客户端发送第四次挥手的
ACK后进入该状态。
- 停留时间:默认 2MSL(MSL 是报文最大生存时间,通常 1 分钟)。
- 目的 :
- 确保服务器能收到第四次挥手的
ACK(若服务器未收到,会重发FIN,客户端可在TIME_WAIT内重传)。
- 避免客户端新连接收到旧连接的残留报文(2MSL 足够让网络中旧报文失效)。
四、Socket:网络编程接口
1. Socket 本质
- 操作系统提供的网络通信接口,封装了 TCP/UDP 底层协议细节。
- 唯一标识:
IP地址 + 端口号(如(192.168.1.1, 8080)),对应进程间通信的端点。
2. 核心Socket函数
| 函数名 |
作用 |
说明 |
socket() |
创建 Socket 文件描述符 |
1. 参数 type:SOCK_STREAM=TCP,SOCK_DGRAM=UDP(必考);2. 返回值:成功非负 fd,失败 - 1 |
bind() |
绑定本地 IP + 端口 |
1. 端口范围 165535,11024 为知名端口;2. 必须用 htons() 转换端口为网络序(高频易错点) |
listen() |
TCP 服务器开启监听 |
1. 仅 TCP 使用,UDP 无需;2. 参数 backlog:监听队列最大长度,不代表最大连接数(易混淆点) |
accept() |
TCP 服务器接收客户端连接 |
1. 阻塞函数,返回新的 conn_fd用于通信,原 listen_fd 继续监听;2. 输出参数获取客户端地址 |
connect() |
TCP 客户端发起连接 |
1. 触发三次握手,阻塞直到连接建立;2. UDP 无此函数(必考差异点) |
send()/recv() |
TCP 收发数据 |
1. 面向字节流,recv()返回 0 表示对方关闭连接;2. send()返回值可能 < len,需循环发送 |
sendto()/recvfrom() |
UDP 收发数据 |
1. 无连接,需指定对方地址;2. 自带消息边界,无粘包问题(与 TCP 对比考点) |
close() |
关闭 Socket 释放资源 |
1. TCP 关闭触发四次挥手;2. 服务器需先关 conn_fd,再关 listen_fd |
3. TCP Socket 编程流程(示例)
服务器端(被动连接):
cpp
复制代码
#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;
}
客户端(主动连接):
cpp
复制代码
#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;
}
4. UDP Socket 收发示例
cpp
复制代码
// 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;
}
5. TCP 粘包解决方案示例(消息头 + 消息体,最常用)
cpp
复制代码
// 发送端:打包消息(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;
}