本阶段目标
理解 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字节长度 │ 消息内容 │ │ (告诉有多长) │ (按长度读取,不会多读) │ └──────────────┴────────────────────────────┘
|
读取流程:
- 先读 4 字节 → 知道消息体长度是 N
- 再读 N 字节 → 正好读完一条完整消息
- 重复步骤 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_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
| #include <iostream> #include <cstring> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h>
int main() { int server_fd = socket(AF_INET, SOCK_STREAM, 0); 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)); listen(server_fd, 3); std::cout << "服务器启动,端口 8080" << std::endl; int client_fd = accept(server_fd, nullptr, nullptr); std::cout << "客户端已连接!" << std::endl; 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 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
| #include <iostream> #include <cstring> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h>
int main() { int sock = socket(AF_INET, SOCK_STREAM, 0); 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; const char* msg = "Hello from client!"; write(sock, msg, strlen(msg)); 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-自定义通信协议