网络协议扫盲,一文从0搞定网络协议
一、网络协议基础概念
- TCP(Transmission Control Protocol)
定义:面向连接、可靠传输的应用层协议,确保数据包按序到达且无丢失。
工作原理:
三次握手:客户端发送SYN包(序列号x)→ 服务端回复SYN+ACK(确认号x+1,序列号y)→ 客户端发送ACK(确认号y+1),连接建立。
四次挥手:客户端发FIN(请求关闭)→ 服务端回ACK(确认接收到关闭,继续提供未完成的服务) → 服务端发FIN(提供服务完毕) → 客户端回ACK,连接终止。
优点:可靠性高(重传机制、流量控制)、支持全双工通信。
缺点:握手和挥手开销大,头部占用20-60字节。
Java应用:Socket/ServerSocket实现TCP通信,Netty框架优化高并发场景。
- UDP(User Datagram Protocol)
定义:无连接、不可靠传输,追求低延迟和高吞吐量。
工作原理:直接发送数据包,无确认机制,不保证顺序和完整性。
优点:延迟低(无握手)、头部仅8字节、支持广播/多播。
缺点:丢包率高、无流量控制。
Java应用:DatagramSocket实现实时音视频传输(如直播推流)。
- HTTP/HTTPS
HTTP:
定义:超文本传输协议,基于TCP,无状态。
请求方法:GET(获取数据)、POST(提交数据)、PUT/DELETE(RESTful操作)。
缺点:明文传输(易被窃听)、无身份验证。
HTTPS:
定义:HTTP+SSL/TLS,通过加密和证书验证保障安全。
握手过程:客户端发起HTTPS请求 → 服务端返回证书 → 客户端验证证书并生成会话密钥 → 双方加密通信。
优点:防中间人攻击、数据加密。
缺点:握手耗时(约增加50%延迟)、证书成本。
Java应用:HttpURLConnection/HttpClient发送HTTP请求,SSLContext配置HTTPS连接。
二、协议对比与选型
- TCP vs UDP
场景 TCP适用 UDP适用
文件传输 ✅ 可靠传输(如FTP) ❌ 丢包不可接受
实时音视频 ❌ 延迟高 ✅ 低延迟优先(如Zoom)
游戏指令 ❌ 队头阻塞问题 ✅ 快速响应(如《王者荣耀》)
- HTTP/1.1 vs HTTP/2 vs HTTP/3
HTTP/1.1:
优点:兼容性强,广泛支持。
缺点:队头阻塞(同一连接只能串行处理请求)。
HTTP/2:
优点:多路复用(单连接并行处理请求)、Header压缩(HPACK算法)。
缺点:服务端推送可能浪费带宽。
HTTP/3:
优点:基于QUIC协议(UDP),解决TCP队头阻塞。
缺点:兼容性差,需客户端和服务端同时支持。
- RESTful API vs GraphQL
RESTful:
优点:资源导向、标准化(GET/POST/PUT/DELETE)。
缺点:过度获取数据(需多次请求)。
GraphQL:
优点:客户端自定义查询字段,减少冗余数据。
缺点:服务端复杂度高,调试困难。
三、TCP沾包拆包问题
一、什么是沾包和拆包?
- 沾包(粘包)
现象:发送方发送的多个独立数据包(消息),在接收方被合并成一个大的数据包,导致接收方无法区分每个消息的边界。
例如:发送方依次发送 Msg1(10 字节)和 Msg2(20 字节),接收方可能一次性读取到 30 字节的数据,无法判断哪里是 Msg1 的结束和 Msg2 的开始。
- 拆包
现象:发送方发送的一个完整数据包,在接收方被拆分成多个不完整的小数据包。
例如:发送方发送一个 30 字节的 Msg,接收方可能先读取到 10 字节,再读取到 20 字节,需要将两次数据合并才能得到完整消息。
二、为什么会出现沾包和拆包?
TCP 是面向字节流的协议,没有 “消息” 的概念,仅保证字节流的可靠传输。问题的核心原因是 发送方和接收方的缓冲区处理机制 以及 数据传输的不确定性,具体包括:- 发送方缓冲区的合并(沾包主因)
发送方为了效率,会将多次发送的数据合并到缓冲区中,通过 Nagle 算法(减少小包数量)或直接批量发送。
例如:调用 3 次 send() 发送 3 条消息,可能被合并成一个大的 TCP 报文段发送。
- 接收方缓冲区的读取不完整(拆包主因)
接收方读取数据时,可能因缓冲区大小限制或读取时机问题,无法一次性读取完整的消息,导致一个消息被拆分成多次读取。
例如:消息长度为 30 字节,接收方每次最多读取 10 字节,需分 3 次读取。
- 网络层的分片(底层原因)
IP 层传输数据时,若数据包超过 MTU(最大传输单元),会将数据分片,导致一个 TCP 报文段被拆分成多个 IP 分片,接收方需重组分片。
- 发送 / 接收速度不匹配
发送方发送速度快,接收方处理速度慢,导致多个消息被缓存到接收方缓冲区中,形成沾包。
三、如何解决沾包和拆包问题?
常见的解决办法是: 消息长度前缀(最通用方案)即先统一规定长度再规定内容 做法:在每个消息前添加一个固定长度的字段(如 4 字节整数),表示消息的总长度。接收方先读取长度字段,再按长度读取完整消息。 步骤: 发送方:[4字节长度][消息内容],例如消息内容为 Hello(5 字节),则发送 00000005Hello。 接收方:先读取 4 字节得到长度 len,再读取 len 字节作为完整消息。 优点:支持任意变长消息,适用二进制和文本协议,是 RPC、即时通讯等场景的主流方案。示例(Java 实现):
// 发送方
byte[] content = "Hello".getBytes();
byte[] lengthBytes = ByteBuffer.allocate(4).putInt(content.length).array();
outputStream.write(lengthBytes); // 先写长度
outputStream.write(content); // 再写内容
// 接收方(需处理缓冲区不完整的情况)
private byte[] readMessage(InputStream is) throws IOException {
// 先读 4 字节长度
byte[] lenBuffer = new byte[4];
if (readFully(is, lenBuffer) != 4) return null; // 未读满,继续等待
int length = ByteBuffer.wrap(lenBuffer).getInt();
// 再读 length 字节内容
byte[] content = new byte[length];
if (readFully(is, content) != length) return null;
return content;
}
private int readFully(InputStream is, byte[] buffer) throws IOException {
int offset = 0;
while (offset < buffer.length) {
int read = is.read(buffer, offset, buffer.length - offset);
if (read == -1) return -1; // 连接关闭
offset += read;
}
return buffer.length;
}
四、实际开发中的注意事项
缓冲区管理: 接收方需维护一个 接收缓冲区(如 Java 中的 ByteArrayOutputStream),暂存未处理的不完整数据,直到凑齐一个完整消息。 避免直接使用 read() 方法单次读取,可能导致频繁的不完整读取。 粘包拆包的本质: 沾包是多个消息被合并,拆包是单个消息被拆分,两者的根本原因都是 TCP 流式传输无边界,需在应用层显式定义边界。 框架的支持: 主流网络框架(如 Netty、MINA)内置了拆包器(LengthFieldBasedFrameDecoder、DelimiterBasedFrameDecoder),无需手动处理缓冲区。 例如 Netty 中使用 LengthFieldBasedFrameDecoder 处理长度前缀协议:// 长度字段位于消息前 4 字节,偏移量 0,长度字段长度 4,长度调整 0,结束偏移量 4
pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4));
面试高频问题:
问:为什么 UDP 没有沾包拆包问题?
答:UDP 是面向数据报的,每个 sendto() 对应一个独立的数据报,接收方 recvfrom() 能完整读取单个数据报,天然有消息边界。
问:如何设计一个可靠的变长消息协议?
答:使用 “长度前缀 + 消息内容” 的格式,配合接收缓冲区暂存不完整数据,确保每次处理完整消息。

浙公网安备 33010602011771号