本阶段目标

理解 TCP 通信的底层原理,为聊天应用的网络层打好基础。


一、TCP 通信的水管模型

把 TCP 连接想象成一根水管:

1
2
3
4
5
6
客户端A                    服务端                   客户端B
│ │ │
│────"hello"──→TCP──────→ │ │
│ │──"hello"──→TCP────────→│
│ │ │
│←──TCP──"hello"──────────│←────────TCP──"hello"───│

四个核心概念

概念 大白话解释
TCP 连接 一根”水管”,数据从一端流向另一端,保证不丢不乱序
端口 水管的”接头编号”,服务端固定监听一个端口(如 8080)
socket 水管的两端,一端是客户端,一端是服务端
三次握手 打电话:A 拨号 → B 接听说”喂” → A 说”听到了”,连接建立

二、TCP 与 UDP 的区别

TCP UDP
连接方式 面向连接(先握手) 无连接(直接发)
可靠性 保证送达、不丢包、不乱序 不保证,可能丢
速度 较慢(要确认)
适用场景 聊天、文件传输、网页 直播、视频通话、游戏
类比 打电话(确认对方在听) 发广播(不管谁收到)

聊天应用选 TCP:因为消息不能丢,必须保证 A 发的内容 B 一定能收到。


三、TCP 粘包与拆包问题

什么是粘包?

TCP 是字节流,没有消息边界。你发送两次 "hello",接收方可能收到:

1
2
3
情况1(正常):  "hello" + "hello"     (两条分开)
情况2(粘包): "hellohello" (合并成一条)
情况3(拆包): "hel" + "lohe" + "llo" (被切成三段)

为什么聊天必须解决粘包?

1
2
3
4
5
6
7
// 客户端发送
send("登录请求: {user: 'tom'}")
send("聊天消息: {text: '你好'}")

// 服务端如果收到粘包数据:
"登录请求: {user: 'tom'}聊天消息: {text: '你好'}"
// 根本分不清哪是登录、哪是消息!

解决方案:自定义协议

在每条消息前加上长度头,告诉对方这条消息有多长:

1
2
3
4
┌──────────────┬────────────────────────────┐
│ 4字节长度 │ 消息内容 │
│ (告诉有多长) │ (按长度读取,不会多读) │
└──────────────┴────────────────────────────┘

读取流程:

  1. 先读 4 字节 → 知道消息体长度是 N
  2. 再读 N 字节 → 正好读完一条完整消息
  3. 重复步骤 1 → 处理下一条

这样永远不会把两条消息混在一起。


四、阻塞与非阻塞 I/O

阻塞模式(简单但低效)

1
2
3
4
5
6
7
// 服务端阻塞模式伪代码
while (true) {
int client = accept(); // 阻塞:没人连接就卡住
char buf[1024];
read(client, buf); // 阻塞:客户端不发数据就卡住
// 处理...
}

问题: 服务端同一时刻只能处理一个客户端,其他人全在排队等。

非阻塞 + epoll(高性能)

1
2
3
4
5
6
7
// epoll 同时监听所有连接
epoll_wait(epfd, events, MAX, -1); // 阻塞等待"任意一个"有数据

for (int i = 0; i < n; i++) {
if (events[i] 是新的连接) accept();
if (events[i] 是已有客户端发来数据) 处理消息();
}

优点: 一个线程管理成百上千个连接,谁有数据就处理谁。


五、C++ 最简 TCP 通信实现

5.1 服务端(Echo Server)

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
36
37
38
39
40
41
// echo_server.cpp —— 聊天系统的原子形态
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

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

// 2. 绑定地址和端口
sockaddr_in address{};
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
bind(server_fd, (sockaddr*)&address, sizeof(address));

// 3. 开始监听
listen(server_fd, 3);
std::cout << "服务器启动,端口 8080" << std::endl;

// 4. 接受客户端连接
int client_fd = accept(server_fd, nullptr, nullptr);
std::cout << "客户端已连接!" << std::endl;

// 5. 收发循环
char buffer[1024];
while (true) {
memset(buffer, 0, sizeof(buffer));
int bytes = read(client_fd, buffer, sizeof(buffer));
if (bytes <= 0) break; // 客户端断开

std::cout << "收到: " << buffer << std::endl;
write(client_fd, buffer, bytes); // 原样返回
}

close(client_fd);
close(server_fd);
return 0;
}

编译与测试:

1
2
3
4
5
6
7
8
9
# 编译
g++ echo_server.cpp -o echo_server

# 启动服务端
./echo_server

# 另开终端,用 telnet 测试
telnet localhost 8080
# 输入任意文字,回车,服务端会原样返回

5.2 客户端(最简版本)

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
// echo_client.cpp
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

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

// 2. 连接服务端
sockaddr_in server{};
server.sin_family = AF_INET;
server.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server.sin_addr);
connect(sock, (sockaddr*)&server, sizeof(server));

std::cout << "已连接到服务器!" << std::endl;

// 3. 发送消息
const char* msg = "Hello from client!";
write(sock, msg, strlen(msg));

// 4. 接收回复
char buffer[1024] = {0};
read(sock, buffer, sizeof(buffer));
std::cout << "服务器回复: " << buffer << std::endl;

close(sock);
return 0;
}

六、聊天系统的通信层次

1
2
3
4
5
6
7
8
9
10
11
12
┌─────────────────────────────────┐
│ 前端界面(Vue3) │ ← 用户看到的
├─────────────────────────────────┤
│ WebSocket 协议层 │ ← 浏览器与服务器通信
├─────────────────────────────────┤
│ 自定义消息协议 │ ← 登录/消息/心跳 消息格式
│ [长度头][类型][JSON消息体] │
├─────────────────────────────────┤
│ TCP 传输层 │ ← 可靠字节流(本章重点)
├─────────────────────────────────┤
│ 网络层(IP) │ ← 数据包路由
└─────────────────────────────────┘

我们正在从最底层的 TCP 传输层 开始,逐步向上构建。


七、本节小结

学到了什么

知识点 关键理解
TCP 水管模型 可靠、有序、面向连接的数据流
粘包/拆包 TCP 没有消息边界,必须自定义协议
阻塞 vs 非阻塞 epoll 让一个线程管理多个连接
socket 编程 socket() → bind() → listen() → accept() → read/write()

下一步

基于 Echo Server,加入自定义协议(长度头 + 消息类型),让服务端能区分不同类型的消息(登录、聊天、心跳)。


系列文章:聊天项目-02-自定义通信协议