Linux网络编程(六):UDP聊天室与线程池

发布时间:2026/5/22 8:20:15

Linux网络编程(六):UDP聊天室与线程池 目录一、聊天室背景1. DictServer 的局限性2. 什么是聊天室二、整体架构1. 工作流程2. 为什么需要并发三、线程池1. 为什么使用线程池2. 线程池模型3. 聊天室中的任务四、服务端实现1. InetAddr 类的设计与封装2. Route 类的广播与用户管理3. ChatServer 类的整合与任务派发4. 服务端组装主程序五、线程安全1. 用户表加锁2. 广播中的竞态问题六、客户端实现1. 发送流与接收流2. 代码实现七、程序测试1. 多客户端测试2. 消息广播总结一、聊天室背景在上一篇博客中我们成功实现了一个具备业务处理能力的网络词典服务器DictServer。通过引入回调函数机制我们成功地将网络传输引擎与上层查询业务解耦然而DictServer 依然存在着演进上的底层缺陷。为了打破这种局限性本篇博客我们将进入多线程并发网络编程的世界手写一个基于 UDP 协议的高并发多线程聊天室1. DictServer 的局限性回看我们之前实现的 Echo Server 和 DictServer它们的交互模型本质上都是典型的 一问一答 式Request-Response / Ping-Pong 模型这种模型在处理网页浏览HTTP、API 请求时非常高效但在应对更加复杂的实时交互场景时就会暴露出以下两个致命痛点极其严格的单向点对点耦合服务端只会单纯的被动响应。它永远是在收到客户端 A 的请求后将结果原路返回给客户端 A。服务端既无法主动向客户端推送消息也无法将客户端 A 的消息转发给客户端 B无状态执行流在 DictServer 中服务端调用完 sendto 释放完数据后就会瞬间 忘记 这个客户端。它在内核和内存中不维护任何用户状态这意味着它根本不知道当前有多少人正在连接或使用这个服务如果要把这个架构直接拿去做聊天室你会发现当张三在客户端输入了一句 Hello服务器确实收到了但它只能把 Hello 重新回弹给张三自己。李四和王五坐在电脑前什么也收不到2. 什么是聊天室为了打破 一问一答 的局限我们需要构建一个真正的群聊多点广播模型一个标准的网络聊天室服务其核心逻辑会发生以下两个改变从 无状态 到 有状态 服务端在内存中必须引入一个在线用户管理表。任意一个客户端首次向服务器发送消息时服务端需要捕捉其 sockaddr_in 地址判定其为新用户上线并将其身份记录在表中全员广播 当任何一个在线用户向服务器发送一条聊天消息时服务端在接收到数据后不再只是单线回复。而是会遍历整个在线用户管理表提取出所有在用用户的网络地址循环调用 sendto 将消息分发、推送给当前所有在线的客户端这种数据流向的改变意味着我们的网络程序正式从单机独白走向了多端协作的互联网分布式形态。然而这种 遍历全员广播 式的密集I/O操作对服务器性能提出了极高要求因此必须引入线程池机制二、整体架构要实现一个真正的多端实时聊天室服务端需要承担调度中心的角色。在深入代码之前我们需要从宏观上理清整个系统的数据流向与架构1. 工作流程UDP 聊天室的业务核心围绕着 状态维护 与 消息分发 展开。一个典型的消息转发闭环包含以下物理步骤用户注册与识别 由于 UDP 无连接当任意客户端如客户端 A首次向服务端发送任意数据报时服务端通过 recvfrom 捕获其地址。若该地址不在服务端的在线用户表中则将其标记为新用户记录其 IP 和端口并触发全员广播用户 X 已上线消息投递 在线的客户端 A 在控制台输入聊天内容并发送。该消息作为普通的 UDP 数据报投递给服务端服务端接收与解析 服务端主循环读取到数据后提取出聊天文本和发送方身份客户端 A 的地址全员广播 服务端遍历在线用户表。对于表中的每一个用户地址通常排除发送方 A 自己或者包含 A 作为送达确认服务端都会发起一次 sendto 调用将消息推送到这些客户端的监听端口客户端并发接收 所有存活的客户端在后台持续监听收到服务端的广播报文后将其刷新并呈现在用户的屏幕上2. 为什么需要并发如果延续上一篇 DictServer 的单线程串行模型来跑这个工作流程当聊天室人数较少时比如 3-5 人看似平安无事。然而一旦用户量上升比如达到 500 人这个单线程系统会瞬间崩溃我们来拆解一下单线程模型的致命死结广播带来的 I/O 阻塞放大 假设当前聊天室有 1000 个在线用户。当用户 A 发送了一条消息单线程的服务端必须执行一个循环for (auto user : user_list) { sendto(sockfd, msg, ..., user, ...); // 循环执行 1000 次每一次 sendto 都是一次跨越用户态与内核态的系统调用受限于网卡驱动和网络栈性能。这意味着完成这一次全员广播服务端线程可能需要耗时数毫秒甚至数十毫秒服务空窗期 在单线程模型下当这个唯一的线程正在给这 1000 个人循环发消息时它是无法调用 recvfrom 的此时如果有新的用户尝试发送消息或者有新用户尝试上线这些数据包只能被堆积在操作系统的内核接收缓冲区中。如果缓冲区满了后续的数据包就只能被丢弃。在用户端的表现就是聊天室卡顿、消息延迟极高、甚至大面积丢包网络抖动 如果在循环广播的过程中某个用户的网络链路发生严重抖动或者某个套接字操作出现了短暂的系统级阻塞整个唯一的线程就会被卡在循环的某一个步长上。这会导致后面几百个用户的消息接收被无辜延时单线程模型的根本矛盾在于网络消息的接收 与 消息的多路广播 在同一个执行流里。广播动作太慢严重拖累了接收的速度为提升吞吐量我们必须将这两个核心动作进行解耦 服务端的主线程唯一的工作就是循环调用 recvfrom。消息接收后立即交由线程池处理三、线程池在明确了聊天室必须依赖并发架构后我们不能采取 每来一条消息就临时创建一个线程 的粗暴做法。在高性能网络服务器中线程池是解决高并发密集 I/O 的最佳解决方案由于我们在之前的多线程专题中已经详细手写并剖析过线程池的底层源码本篇博客将不再重复贴出线程池的实现代码而是将核心聚焦于如何将现有的线程池无缝嵌入到我们的 UDP 聊天室架构中1. 为什么使用线程池面对成百上千用户同时在线的聊天室引入线程池主要基于以下三个工程考量降低创建与销毁开销Linux 中创建线程虽然比进程轻量但依然涉及栈空间的开辟、上下文切换以及内核数据结构的初始化。如果每收到一条聊天消息就创建线程CPU 的大量算力将被白白浪费在线程的生命周期管理上资源边界控制线程池的核心特征是总量受控。如果遭遇恶意刷屏或突发流量单线程接收端只会将消息化为任务堆积在内存队列中而不会无限拉起新线程。这就死死锁定了服务器的 CPU/内存资源上限避免了系统因资源耗尽而宕机解耦与异步化主线程网络 I/O 线程只负责调用 recvfrom网卡缓冲区读取数据并将数据打包成任务推进队列工作线程线程池内部后台从队列中获取任务后执行耗时操作遍历在线用户表并逐个发送广播。主线程和工作线程分工明确网络通信吞吐量因此得到显著提升2. 线程池模型结合我们贴出的单例线程池设计整个聊天室的并发运转完美契合了经典的生产者-消费者模型单例模式我们通过懒汉模式并配合双重检查锁确保线程池在整个服务端进程中有且仅有一个实例。这意味着全校/全公司只有一个任务调度中心主线程为生产者网卡一旦收到 UDP 报文主线程提取后调用 Enqueue(task)。这个动作会将任务压入临界资源——任务队列中并通过条件变量唤醒后台正在休眠的工作线程工作线程为消费者线程池启动时预先初始化好的若干个线程在内部的循环中阻塞等待。一旦被唤醒它们会在互斥锁的保护下将任务从队列中竞争出来拷贝到自己的私有栈空间中接着立刻释放锁在临界区外部异步执行任务3. 聊天室中的任务线程池是一个泛型模板类它要求传入的任务类型 T 必须重载 operator()。那么在群聊的业务场景下这个任务 T 应该如何设计聊天室任务的核心目标是将收到的消息安全、完整地发送给所有在线的人。因此一个合格的聊天室 Task 应当具备以下条件数据载荷包含发送方客户端的聊天文本源身份标签包含发送方的 IP 和 Port。这是为了让服务端在广播时能够识别出是谁说了这句话从而在消息前拼接出[张三(127.0.0.1:8080)]# Hello World的格式执行逻辑任务内部必须持有一个回调函数或者直接在 operator() 中实现具体的广播逻辑——即访问服务端的在线用户列表并调用 sendto 发送给每一个人架构细节处理任务需要放在临界区内部进行吗绝对不行从队列 pop 完任务后必须先用花括号 {} 触发 LockGuard 的析构函数释放锁然后再去执行 t()。因为广播多路是耗时的 I/O 操作如果把 t() 放在锁内部执行线程池的所有工作线程将会发生严重的串行死锁多线程的并发优势将荡然无存四、服务端实现为了贯彻面向对象的设计范式并让聊天室的系统架构具备工业级的可扩展性我们不能再把所有的网络接口和业务逻辑塞进一个杂乱的源文件中。我们将服务端拆解为三个高内聚、低耦合的模块InetAddr 类负责底层网络序与主机序地址的包装与一键转换Route 类核心路由调度中心负责在线用户的管理与全员消息广播ChatServer 类纯粹的网络引擎负责 I/O 接收与任务派发1. InetAddr 类的设计与封装在 Socket 编程中每次面对 struct sockaddr_in我们都要手动调用 inet_ntop、ntohs 或者是 inet_pton代码极其冗长且易错InetAddr 类的目的就是将复杂的 C 风格结构体彻底包装。它不仅在构造时自动完成网络序到主机序的解析还重载了 运算符为后续在用户表中快速比对、查找用户打下基础#pragma once #include iostream #include string #include sys/socket.h #include netinet/in.h #include arpa/inet.h class InetAddr { public: InetAddr() default; // 从底层的 sockaddr_in 结构体直接构造 InetAddr(const struct sockaddr_in addr) : _addr(addr) { char ip_buf[64]; inet_ntop(AF_INET, _addr.sin_addr, ip_buf, sizeof(ip_buf)); _ip ip_buf; _port ntohs(_addr.sin_port); } // 从明文 IP 和端口构造 InetAddr(const std::string ip, uint16_t port) : _ip(ip), _port(port) { std::memset(_addr, 0, sizeof(_addr)); _addr.sin_family AF_INET; _addr.sin_port htons(_port); inet_pton(AF_INET, _ip.c_str(), _addr.sin_addr); } std::string Ip() const { return _ip; } uint16_t Port() const { return _port; } struct sockaddr_in GetAddr() const { return _addr; } // 转换为可读字符串例如 127.0.0.1:8080 std::string PrintStr() const { return _ip : std::to_string(_port); } // 重载 运算符方便在 vector 中查找用户 bool operator(const InetAddr other) const { return (this-_ip other._ip this-_port other._port); } private: struct sockaddr_in _addr; std::string _ip; uint16_t _port; };2. Route 类的广播与用户管理Route 类是聊天室的核心。它内部维护了一个在线用户表。当新用户发送第一条消息时将其自动录入并提供全员广播接口。为了应对线程池中多个工作线程同时读写用户表的并发冲突这里我们引入互斥锁#pragma once #include InetAddr.hpp #include vector #include mutex #include algorithm class Route { public: Route() default; // 添加新用户上线 void AddUser(const InetAddr user) { std::lock_guardstd::mutex lock(_mutex); auto it std::find(_online_users.begin(), _online_users.end(), user); if (it _online_users.end()) { _online_users.push_back(user); std::cout 新用户上线: user.PrintStr() 当前在线人数: _online_users.size() std::endl; } } // 移除下线用户 void RemoveUser(const InetAddr user) { std::lock_guardstd::mutex lock(_mutex); auto it std::find(_online_users.begin(), _online_users.end(), user); if (it ! _online_users.end()) { _online_users.erase(it); std::cout 用户下线: user.PrintStr() std::endl; } } // 消息分发遍历用户表调用 sendto 广播消息 void ForwardMessage(int sockfd, const std::string msg, const InetAddr sender) { // 首先确保发送者本身存在于在线列表中 AddUser(sender); // 拼接标准群聊格式[127.0.0.1:5050]# hello std::string formatted_msg [ sender.PrintStr() ]# msg; std::lock_guardstd::mutex lock(_mutex); for (const auto user : _online_users) { struct sockaddr_in target_addr user.GetAddr(); // 执行网络 I/O 密集分发 sendto(sockfd, formatted_msg.c_str(), formatted_msg.size(), 0, (struct sockaddr*)target_addr, sizeof(target_addr)); } } private: std::vectorInetAddr _online_users; // 在线用户管理表 std::mutex _mutex; // 保护用户表的互斥锁 };3. ChatServer 类的整合与任务派发ChatServer 是一个纯粹的网络 I/O 驱动器。它只管两件事通过 recvfrom 接受数据然后把数据和外部传入的业务回调函数包装成任务丢进线程池#pragma once #include iostream #include string #include cstring #include functional #include memory #include InetAddr.hpp #include ThreadPool.hpp class ChatServer { public: // 定义业务回调拓扑参数为 (网络句柄, 消息内容, 发送方地址) using bus_cb_t std::functionvoid(int, const std::string, const InetAddr); // 线程池任务接口 using Task std::functionvoid(); // 构造时注入端口、业务回调函数和绑定 IP ChatServer(uint16_t port, bus_cb_t cb, const std::string ip 0.0.0.0) : _sockfd(-1), _port(port), _cb(cb), _ip(ip), _is_running(false) {} ~ChatServer() { if (_sockfd 0) close(_sockfd); } bool Init() { _sockfd socket(AF_INET, SOCK_DGRAM, 0); if (_sockfd 0) return false; struct sockaddr_in local; std::memset(local, 0, sizeof(local)); local.sin_family AF_INET; local.sin_port htons(_port); inet_pton(AF_INET, _ip.c_str(), local.sin_addr); if (bind(_sockfd, (struct sockaddr*)local, sizeof(local)) 0) { return false; } return true; } void Start() { _is_running true; char buffer[2048]; auto* pool ThreadPoolTask::GetInstance(); std::cout [异步网络引擎] 启动成功并发线程池已就绪... std::endl; while (_is_running) { struct sockaddr_in peer; socklen_t len sizeof(peer); // 主线程只负责高频、单纯的 I/O 接收 ssize_t n recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)peer, len); if (n 0) continue; buffer[n] \0; std::string client_msg buffer; InetAddr sender(peer); // 将外部注入的业务回调 _cb 与数据组装成 Task 投递给线程池 Task task [this, client_msg, sender]() { this-_cb(this-_sockfd, client_msg, sender); }; pool-Enqueue(task); } } private: int _sockfd; uint16_t _port; bus_cb_t _cb; // 外部注入的业务核心逻辑 std::string _ip; bool _is_running; };4. 服务端组装主程序这里把完全解耦、由外部注入 Lambda 业务回调的服务端独立启动入口 main.cc 完整地贴出来。它将网络引擎ChatServer与业务分发中枢Route进行了完美的缝合#include ChatServer.hpp #include Route.hpp #include memory #include cstdlib void ServerUsage(const std::string proc) { std::cout Usage:\n\t proc port\n std::endl; } int main(int argc, char* argv[]) { if (argc ! 2) { ServerUsage(argv[0]); return 1; } // 1. 提取外部指定的服务器开放端口 uint16_t port std::atoi(argv[1]); // 2. 实例化业务核心路由分发中枢 Route route; // 3. 业务 Lambda 回调 // 捕获 route 对象的引用当网络引擎捞到数据时由该回调接手进行分发 auto chat_business [route](int sockfd, const std::string msg, const InetAddr sender) { // 解耦后的核心操作调用路由中心进行轻量锁保护下的全员网络广播 route.ForwardMessage(sockfd, msg, sender); }; // 4. 初始化并拉起高并发异步网络引擎 // 绑定 0.0.0.0 IP 使得服务器能接收来自任意网卡的 UDP 数据报 std::unique_ptrChatServer server(new ChatServer(port, chat_business, 0.0.0.0)); if (!server-Init()) { std::cerr Server Init failed! std::endl; return 2; } // 5. 启动引擎进入高性能多线程通信循环 server-Start(); return 0; }五、线程安全引入线程池后多条执行流在后台并发运转。这时原本在单线程下看似安全的系统会暴露出巨大的潜在风险。我们必须站在底层数据结构的角度对系统进行线程安全加固1. 用户表加锁在我们的 Route 类中在线用户表是由 std::vectorInetAddr 承载的。请注意C 标准库中的 vector是完全线程不安全的如果不加任何保护当聊天室处于高并发状态时会发生以下灾难性的场景工作线程 A正在执行 ForwardMessage 中的 for (const auto user : _online_users) 循环在底层表现为持有迭代器从头向后遍历就在此时一个从未上线的新用户突然发送了一条消息。工作线程 B被唤醒执行 AddUser触发了 _online_users.push_back()一旦 vector 的容量达到上限push_back 将在堆区开辟一块更大的新空间将老数据搬迁过去并销毁老空间此时工作线程 A 手中的迭代器瞬间变成了一个野指针。程序会立即报出 Segmentation Fault段错误并直接崩溃因此任何对 _online_users 的读遍历广播和写新用户上线、用户下线操作都必须接入互斥锁进行强制的原子化保护2. 广播中的竞态问题虽然我们在 Route 类的方法中使用了 std::lock_guard但直接在全员广播的循环体外加锁在工业级开发中存在明显的锁粒度过大缺陷// 糟糕的设计把耗时的 I/O 操作锁在临界区内 void ForwardMessage(...) { std::lock_guardstd::mutex lock(_mutex); // 锁住全局 for (const auto user : _online_users) { sendto(...); // 如果有1000个用户锁会一直被占满 } }如果把密集调用的 sendto 锁在临界区内当一个工作线程在拼命发广播时其他所有工作线程甚至都无法进行新用户上线登记线程池实质上退化成了单线程串行Read-Copy-Update写时复制/读写分离思想的轻量实现为了把锁的粒度降到最低我们可以利用局部拷贝的技巧将读写冲突降到最低void Route::ForwardMessage(int sockfd, const std::string msg, const InetAddr sender) { CheckAndAddUser(sender); std::string formatted_msg [ sender.PrintStr() ]# msg; // 1. 定义一个空的高速局部缓冲区 std::vectorInetAddr users_copy; // 2. 仅在拷贝用户表指针/轻量数据时加锁锁的生命周期随着花括号结束立刻终止 { std::lock_guardstd::mutex lock(_mutex); users_copy _online_users; // 内存级内存快照拷贝极快 } // 锁在这里已经释放 // 3. 在完全没有锁负担的环境下安全的去干耗时的 sendto 网络 I/O 广播 // 哪怕此时有新用户上线修改了 _online_users也绝不会引发迭代器失效 for (const auto user : users_copy) { struct sockaddr_in target_addr user.GetAddr(); sendto(sockfd, formatted_msg.c_str(), formatted_msg.size(), 0, (struct sockaddr*)target_addr, sizeof(target_addr)); } }通过这种优化互斥锁的占有时间从 整个网络广播周期 缩短到了 微秒级的内存拷贝周期服务器的并发性能由此得到了真正的释放六、客户端实现1. 发送流与接收流在传统的单机或者 一问一答 式客户端中单线程的线性执行流绰绰有余。但在实时聊天室场景下客户端遭遇了与服务端类似的阻塞问题如果程序阻塞在标准输入等待用户打字网卡收到其他人的群聊广播消息就无法被及时读取并刷新如果程序阻塞在网络接收等待别人的消息用户就无法在键盘上敲字发送为了打破这个死锁客户端必须告别单线程演进为双线程分工架构利用 C11 原生支持的 std::thread我们可以将客户端拆分为两条完全独立的执行流。值得注意的是在 Linux 内核的设计中UDP 的 Socket 文件描述符是支持全双工并发读写的。这意味着一条线程在对同一个 sockfd 疯狂调用 sendto 的同时另一条线程对该 sockfd 调用 recvfrom 是绝对安全的底层不会发生死锁冲突2. 代码实现#include iostream #include string #include cstring #include thread #include memory #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include unistd.h void Usage(const std::string proc) { std::cout Usage:\n\t proc server_ip server_port std::endl; } // 接收线程函数全速从网络捞数据并打印到屏幕 void ReceiveRoutine(int sockfd) { char buffer[2048]; while (true) { struct sockaddr_in peer; socklen_t len sizeof(peer); // 阻塞等待服务端推送的群聊广播 ssize_t n recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)peer, len); if (n 0) { buffer[n] \0; // 实时刷新到终端界面 std::cout \r buffer \nChatClient# std::flush; } } } int main(int argc, char* argv[]) { if (argc ! 3) { Usage(argv[0]); return 1; } std::string server_ip argv[1]; uint16_t server_port std::atoi(argv[2]); // 1. 创建本地 UDP 套接字双线程共享此套接字句柄 int sockfd socket(AF_INET, SOCK_DGRAM, 0); if (sockfd 0) { std::cerr Create socket failed std::endl; return 2; } // 2. 填充远端服务端的网络地址 struct sockaddr_in server_addr; std::memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_port htons(server_port); inet_pton(AF_INET, server_ip.c_str(), server_addr.sin_addr); std::cout std::endl; std::cout 欢迎来到 UDP 高并发线程池聊天室 std::endl; std::cout 输入你想说的话并回车发送输入 q 退出聊天 std::endl; std::cout std::endl; // 3. 创建接收线程让其后台独立运行 // 将套接字描述符作为参数传递给接收线程 std::thread recv_thread(ReceiveRoutine, sockfd); recv_thread.detach(); // 分离线程让其自主回收资源不阻塞主线程 // 输入线程捕获用户键盘输入并发送 std::string line; while (true) { std::cout ChatClient# std::flush; std::getline(std::cin, line); if (line.empty()) continue; if (line q || line quit) { std::cout 正在退出聊天室... std::endl; break; } // 通过 sendto 向服务端中转投递消息 sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr*)server_addr, sizeof(server_addr)); } close(sockfd); return 0; }七、程序测试在双端的多线程并发架构全部合拢后接下来便是进行联调测试。通过测试将直观地向我们展示在后台线程池和客户端双线程下一个真正的分布式多用户群聊服务是如何运转的1. 多客户端测试为了模拟真实的群聊场景我们需要在 Linux 环境中同时拉起一个服务端和至少三个不同的客户端终端(1) 快速编译在终端中利用 g 编译我们的分布式组件确保 C11 标准开启# 编译高并发服务端 g -stdc11 main.cc -o chat_server -lpthread # 编译多线程客户端 g -stdc11 chat_client.cc -o chat_client -lpthread由于代码中引入了 std::thread 以及底层的 pthread 线程库编译时必须带上 -lpthread 链接参数(2) 部署启动服务器我们在终端 1 中让服务器在 8080 端口跑起来./chat_server 8080此时网络引擎主线程已经阻塞在 recvfrom 上准备获取客户端的 UDP 数据报2. 消息广播我们开启三个新的Linux终端终端2、终端3、终端4分别代表三位用户张三Alice、李四Bob和王五Charlie。他们将通过本地回环地址 127.0.0.1 接入聊天室(1) 张三上线在终端 2 中启动客户端./chat_client 127.0.0.1 8080服务端终端 1 瞬间刷新日志新用户上线: 127.0.0.1:49215 当前在线人数: 1由于 UDP 无连接张三没有经过任何 connect 握手。路由中心通过张三发送的第一条消息隐式地将其网络标识 127.0.0.1:49215 录入了在线用户表(2) 李四、王五上线李四在终端 3 也启动客户端新用户上线: 127.0.0.1:53182 当前在线人数: 2王五在终端 4 启动客户端新用户上线: 127.0.0.1:38901 当前在线人数: 3此时服务端 Route 路由中心的 _online_users 容器中在互斥锁的保护下已安全存储了三个独立的套接字地址结构总结综上所述从 UDP Socket 通信到 DictServer再到基于线程池实现的多人聊天室我们已经逐步完成了从 网络通信 到 网络服务 的过渡其中聊天室模型让我们第一次真正接触到了现代服务器程序中的几个核心问题如何管理多个客户端如何进行消息广播如何维护在线状态如何通过线程池提升并发处理能力如何保证共享资源访问的线程安全与此同时我们也进一步认识到真正的服务器程序本质上是网络IO 并发处理 状态管理而 UDP 虽然简单高效但它本身并不保证数据可靠性也不存在连接管理机制因此更加适合聊天室、直播、语音等对实时性要求较高的场景在下一篇中我们将正式进入 TCP 编程深入理解面向连接、可靠传输以及 TCP Socket 服务端的工作模型真正开始接触现代互联网中最核心的网络通信协议

相关新闻