《UDP网络编程完全指南:从套接字到高并发聊天室实战》
目录
一、套接字描述符
1. socket 创建套接字
2. shutdown 禁止套接字 I/O
二、寻址
1. 字节序
2. 地址格式
(1)sockaddr 结构
(2)sockaddr_in 结构
(3) sockaddr_in6
(4)sockaddr_un 结构
3. 地址转换函数
(1)传统转换函数
(2)现代转换函数
4. 将套接字与地址关联
三、数据传输
1. 发送数据
2. 接收数据
四、UDP 客户端-服务器程序
1. UdpServer.hpp
2. UdpServer.cc
3. UdpClient.cc
4. 运行
5. 查看网络状态:netstat
五、实现DictServer网络字典
1. Dict.hpp
2. InetAddr.hpp
3. UdpServer.hpp
4. UpdServer.cc
5. UdpClient.cc
6. 运行
六、进程聊天室
1. 单进程聊天室
(1)Route.hpp
(2)UdpServer.hpp
(3)UdpServer.cc
(4)UdpClient.cc
(5)实现通信
2. 多进程聊天室---线程池
(1)UdpServer.hpp
(2)UdpServer.cc
(3)运行聊天室
本篇,我们将描述UDP套接字网络进程间通信接口,进程用该接口能够和其他进程通信,无论他们是处于同一台计算机还是不同计算机上。实际上,这正是套接字接口设计的目标之一:同样的接口既可以用于计算机间通信,也可以用于计算机内通信。
一、套接字描述符
套接字是通信端点的抽象。正如使用文件描述符访问文件,应用程序用套接字描述符访问套接字。套接字描述符在 UNIX系统中被当作是一种文件描述符。事实上,许多处理文件描述符的函数(如read 和write)可以用于处理套接字描述符。
1. socket 创建套接字
#include <sys/types.h>
#include <sys/socket.h>int socket(int domain, int type, int protocol);
参数:
(1)domain(域)确定通信的特性,包括地址格式。下图总结了POSIX.1指定的各个域。各个域都有自己表示地址的格式,而表示各个域的常数都以AF开头,意指地址族(address family)。
(2)参数 type 确定套接字的类型
在AFINET通信域中,套接字类型【SOCK_STREAM】的默认协议是传输控制协议(TCP)。在AF_INET 通信域中,套接字类型【SOCKDGRAM】的默认协议是 UDP。
(3)参数 protocol 通常是0,表示为给定的域和套接字类型选择默认协议。当对同一域和套接字类型支持多个协议时,可以使用 protocol 选择一个特定协议。
调用 socket 与调用 open 相类似。在两种情况下,均可获得用于I/O的文件描述符。当不再需要该文件描述符时,调用 close 来关闭对文件或套接字的访问,并且释放该描述符以便重新使用。虽然套接字描述符本质上是一个文件描述符,但不是所有参数为文件描述符的函数都可以接受套接字描述符。
2. shutdown 禁止套接字 I/O
套接字通信是双向的。可以采用 shutdown 函数来禁止一个套接字的 I/O。
#include <sys/socket.h>int shutdown(int sockfd, int how);
如果 how 是 SHUT_RD(关闭读端),那么无法从套接字读取数据。如果 how是 SHUT_WR(关闭写端),那么无法使用套接字发送数据。如果 how 是 SHUT_RDWR,则既无法读取数据,又无法发送数据。
close 能够关闭一个套接字,为何还使用 shutdown 呢?
这里有若干理由。首先,只有最后一个活动引用关闭时,close 才释放网络端点。这意味着如果复制一个套接字(如采用 dup),要直到关闭了最后一个引用它的文件描述符才会释放这个套接字。而 shutdown 允许使一个套接字处于不活动状态,和引用它的文件描述符数目无关。其次,有时可以很方便地关闭套接字双向传输中的一个方向。例如,如果想让所通信的进程能够确定数据传输何时结束,可以关闭该套接字的写端,然而通过该套接字读端仍可以继续接收数据。
二、寻址
进程标识由两个部分组成。一部分是计算机的网络地址,他可以帮助标识网络上我们想与之通信的计算机;另一部分是该计算机上用端口号表示的服务,它可以帮助标识特定的进程。
1. 字节序
与同一台计算机上的进程进行通信时,一般不用考虑字节序。上一章我们已经讲过网络字节序的知识:
https://blog.csdn.net/2401_83431652/article/details/152168911?spm=1001.2014.3001.5502#:~:text=%E9%9D%A2%E5%90%91%E6%95%B0%E6%8D%AE%E6%8A%A5-,4.%20%E7%BD%91%E7%BB%9C%E5%AD%97%E8%8A%82%E5%BA%8F,-%E6%88%91%E4%BB%AC%E5%B7%B2%E7%BB%8F%E7%9F%A5%E9%81%93https://blog.csdn.net/2401_83431652/article/details/152168911?spm=1001.2014.3001.5502#:~:text=%E9%9D%A2%E5%90%91%E6%95%B0%E6%8D%AE%E6%8A%A5-,4.%20%E7%BD%91%E7%BB%9C%E5%AD%97%E8%8A%82%E5%BA%8F,-%E6%88%91%E4%BB%AC%E5%B7%B2%E7%BB%8F%E7%9F%A5%E9%81%93具体细节就不多赘述,这里再看一下4个在处理器字节序与网络字节序之间实施转换的函数。
#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
h:代表 host(主机字节序)
n:代表 network(网络字节序)
l:代表 long(32位长整数,如IPv4地址)
s:代表 short(16位短整数,如端口号)
2. 地址格式
(1)sockaddr 结构
一个地址标识一个特定通信域的套接字端点,地址格式与这个特定的通信域相关。为使不同格式地址能够传入到套接字函数,地址会被强制转换成一个通用的地址结构sockaddr:
#include <sys/socket.h>struct sockaddr {sa_family_t sa_family; // 地址族char sa_data[14]; // 地址数据
};
(2)sockaddr_in 结构
因特网地址定义在<netinet/inh>头文件中。在IPv4因特网域(AF_INET)中,套接字地址用结构 sockaddr in 表示:
#include <netinet/in.h>struct sockaddr_in {sa_family_t sin_family; // 地址族 (AF_INET)in_port_t sin_port; // 端口号 (网络字节序)struct in_addr sin_addr; // IPv4地址unsigned char sin_zero[8]; // 填充字节
};struct in_addr {in_addr_t s_addr; // IPv4地址 (网络字节序)
};
数据类型 in port_t 定义成 uint16_t。数据类型 in_addr_t定义成uint32_t。这些整数类型在<stdint.h>中定义并指定了相应的位数。
(3) sockaddr_in6
与AF_INET域相比较,IPv6因特网域(AF_INET6)套接字地址用结构 sockaddr_in6表示:
#include <netinet/in.h>struct sockaddr_in6 {sa_family_t sin6_family; // 地址族 (AF_INET6)in_port_t sin6_port; // 端口号 (网络字节序)uint32_t sin6_flowinfo; // IPv6流信息struct in6_addr sin6_addr; // IPv6地址uint32_t sin6_scope_id; // 范围ID
};struct in6_addr {unsigned char s6_addr[16]; // 128位IPv6地址
};
尽管 sockaddr_in与 sockaddr_in6 结构相差比较大,但它们均被强制转换成sockaddr _结构输入到套接字例程中。
(4)sockaddr_un 结构
sockaddr un结构的 sun_path成员包含一个路径名。当我们将一个地址绑定到一个UNIX域套接字时,系统会用该路径名创建一个S_IFSOCK 类型的文件。该文件仅用于向客户进程告示套接字名字。该文件无法打开,也不能由应用程序用于通信。
如果我们试图绑定同一地址时,该文件已经存在,那么 bind 请求会失败。当关闭套接字时,并不自动删除该文件,所以必须确保在应用程序退出前,对该文件执行解除链接操作。
#include <sys/un.h>struct sockaddr_un {sa_family_t sun_family; // 地址族 (AF_UNIX)char sun_path[108]; // 路径名
};
3. 地址转换函数
(1)传统转换函数
有时,需要打印出能被人理解而不是计算机所理解的地址格式。BSD网络软件包含函数 inet_addr 和 inet_ntoa(传统转换函数),用于二进制地址格式与点分十进制字符表示(a.b.c.d)之间的相互转换。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 将点分十进制IPv4地址字符串转换为网络字节序的32位整数。
// 成功返回地址,失败返回-1
in_addr_t inet_addr(const char *cp);
inet_ntoa会返回指向自己内部静态缓冲区的指针,多次调用会覆盖之前的结果,非线程安全!
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 将网络字节序的IPv4地址转换为点分十进制字符串。
char *inet_ntoa(struct in_addr in);
但是上面这些函数仅适用于IPV4地址,已经过时了。
(2)现代转换函数
有两个新函数 inet_ntop 和 inet_pton (现代转换函数)具有相似的功能,而且同时支持 IPV4 地址和 IPV6 地址。
#include <arpa/inet.h>
// 将网络字节序的二进制地址转换成文本字符串格式
int inet_pton(int af, const char *src, void *dst);// 将文本字符串格式转换成网络字节序的二进制地址
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数 af(地址族),仅支持两个值 AF_INET(IPv4) 和 AF_INET6(IPv6)。对于inet_ntop,参数size指定了保存文件字符串的缓冲区(str)的大小。两个常数用于简化工作:
INET ADDRSTRLEN 定义了足够大的空间来存放一个表示IPv4地址的文本字符串;INET6_ADDRSTRLEN定义了足够大的空间来存放一个表示Pv6地址的文本字符串。
4. 将套接字与地址关联
对于服务器,需要给一个接受客户端请求的服务器套接字关联上一个众所周知的地址(一个服务器可能连多个客户端,不能轻易更改地址)。客户端应有一种方法来发现连接服务器所需要的地址,最简单的方法就是服务器保留一个地址并且注册在/etc/services 或者某个名字服务中。
使用 bind 函数来关联地址和套接字。
#include <sys/types.h>
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
• sockfd: 套接字描述符,由 socket() 函数创建
• addr: 指向 sockaddr 结构体的指针,包含要绑定的地址信息
• addrlen: addr 结构体的长度
返回值:
成功返回 0;失败返回 -1。
对于使用的地址有以下限制:
• 在进程正在运行的计算机上,指定的地址必须有效;不能指定一个其他机器的地址。
• 地址必须和创建套接字是的地址族所支持的格式相匹配。
• 地址中的端口号必须不小于,除非该进程具有相应的特权(超级用户)。
• 一般只能将一个套接字端点绑定到一个给定地址上,避免 client 端冲突。
对于因特网域,如果指定IP地址为 INADDR_ANY(<netinet/in.h>中定义的),套接字端点可以被绑定到所有的系统网络接口。这意味着可以接受这个系统所安装的任何一个网卡的数据包。
客户端不用显示 bind,首次发送消息时,OS会自动给客户端进行绑定。OS采用随机端口号的方式。client 端口号只要满足唯一性就行,是多少不重要。
三、数据传输
1. 发送数据
sendto() 函数用于在无连接的套接字(如 UDP)上发送数据,它允许在每次发送时指定目标地址。
#include <sys/types.h>
#include <sys/socket.h>ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
参数:
• sockfd: 套接字描述符
• buf: 指针指向要发送数据的缓冲区
• len: 要发送数据的长度,sizeof( buf)
• flags: 发送标志(通常为 0)
• dest_addr: 目标地址结构体指针(服务端)
• addrlen: 目标地址结构体的长度
返回值:
成功返回发送的字节数,失败返回 -1
2. 接收数据
recvfrom() 函数用于从无连接的套接字(如 UDP)接收数据,并获取发送方的地址信息。
#include <sys/socket.h>ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
参数:
• sockfd: 套接字描述符
• buf: 接收数据的缓冲区(自行设置)
• len: 缓冲区的最大长度
• flags: 接收标志,默认0(阻塞式IO)
• src_addr: 用于存储发送方地址的结构体指针
• addrlen: 输入时为地址结构体大小,输出时为实际地址长度
返回值:
• 成功:返回接收到的字节数
• 失败:返回 -1,并设置 errno
• 连接关闭:返回 0(对于面向连接的协议)
四、UDP 客户端-服务器程序
1. UdpServer.hpp
使用 std::function 实现回调机制,实现业务逻辑与网络通信分离!
#pragma once
#include <iostream>
#include <string>
#include <string.h>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"using namespace LogMoudle;// 定义函数类型func_t
// 分层处理,UDP只进行网络通信, 上层操作自定义
using func_t = std::function<std::string(const std::string&)>;const int defaultsockfd = -1;class UdpServer
{
public:UdpServer(uint16_t port, func_t func): _sockfd(defaultsockfd),// _ip(ip),_port(port),_isrunning(false),_func(func) {}void Init(){// 1. 创建套接字// AF_INET + SOCK_DGRAM = UDP_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 成功返回文件描述符if (_sockfd < 0){ // 失败LOG(LogLevel::FATAL) << "socket error!";exit(1);}LOG(LogLevel::INFO) << "socket success, sockfd: " << _sockfd;// 2. 绑定socket信息: ip和端口号// (1) 填充sockaddr_in结构体struct sockaddr_in local; // 栈上开辟的bzero(&local, sizeof(local)); // 清空lock, 用bzero初始化local.sin_family = AF_INET; // 协议家族 —> AF_INET 网络通信// IP信息和端口信息, 都要被发送到网络// 主机序列 -> 网络序列!!!local.sin_port = htons(_port);// ip -> 四字节 -> 网络序列 : in_addr_t inet_addr(const char *cp);// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 不建议local.sin_addr.s_addr = INADDR_ANY; // 可以被绑定到所有系统网络接口上// 为什么服务器端需要显示的bind端口号?因为一个服务端可能要链接多个客户端,IP和端口号必须是众所周知且不能被轻易改变的!// 绑定int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind error";exit(2);}LOG(LogLevel::INFO) << "bind success, sockfd: " << _sockfd;}void Start(){_isrunning = true;while (_isrunning){// 1. 收消息char buffer[1024]; // 缓冲区struct sockaddr_in peer; // 远端socklen_t len = sizeof(peer); // 无符号整数, 远端的大小ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); // flag设置为0, 阻塞式IOif (s > 0){ // 成功// 发送者的端口号和ipint peer_port = ntohs(peer.sin_port); // 从网络拿的端口号是网络序列, 要转成主机序列std::string peer_ip = inet_ntoa(peer.sin_addr); // 4字节网络序列风格的IP->点分十进制的字符串风格的IPbuffer[s] = 0;std::string result = _func(buffer); // 回调函数,处理数据LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port << "]#" << buffer; // 消息内容 + 发送者// 2. 发消息(结果)// std::string echo_string = "server say: ";// echo_string += buffer;sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len); // 发送处理后的结果}}}~UdpServer(){}private:int _sockfd;uint16_t _port; // 端口号// std::string _ip; // 字符串风格,点分十进制 eg:192.168.1.1bool _isrunning;func_t _func; // 服务器的回调函数, 用来对数据进行处理
};
2. UdpServer.cc
服务器端工作流程:
(1)创建套接字:socket(AF_INET, SOCK_DGRAM, 0)
(2)绑定地址:bind() 指定监听端口
(3)循环处理:recvfrom() 接收客户端数据,同时获取客户端地址;调用回调函数处理业务逻辑;sendto() 将处理结果发送回客户端
// UDP服务端
#include <iostream>
#include <memory>
#include"UdpServer.hpp"std::string defaulthandler(const std::string &message)
{std::string hello = "hello, ";hello += message; return hello;
}
// ./udpsrver port
int main(int argc, char *argv[])
{if(argc != 2){std::cerr << "Usage: " << argv[0] << " port" << std::endl;return 1;}std::string ip = argv[1];uint16_t port = std::stoi(argv[1]); // 字符串转整数Enable_Console_Log_Strategy(); // 显示器打印std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, defaulthandler); // 智能指针构造服务器usvr->Init();usvr->Start();return 0;
}
3. UdpClient.cc
客户端工作流程:
(1)创建套接字:socket(AF_INET, SOCK_DGRAM, 0)
(2)设置服务器地址
(3)交互循环:sendto() 发送数据到服务器;recvfrom() 接收服务器回复
// UDP客户端
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>// ./udpclient server_ip server_port
int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;return 1;}std::string server_ip = argv[1];uint16_t server_port = std::stoi(argv[2]); // 字符串转整数// 1. 创建套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd < 0){std::cerr << "socket error" << std::endl;return 2;}// 2.本地ip和端口号也要与上面的“文件”套接字关联?client 端也要bind!// 但是 client 不用显示的bind, 首次发送消息,OS会自动给 client 进行bind,OS知道ip,端口号采用随机端口号的方式。// 为什么一个端口号只能被一个进程 bind?为了避免 client 端口冲突// client 端的端口号是几不重要,只要是唯一个就行!// 填写服务端信息struct sockaddr_in server;memset(&server, 0, sizeof(server)); // 清空server.sin_family = AF_INET; // AF_INET代表: 使用的是ip协议家族server.sin_port = htons(server_port); // 端口号主机转网络server.sin_addr.s_addr = inet_addr(server_ip.c_str()); // IP地址: ip -> 四字节 -> 网络序列while(true){std::string input;std::cout << "Please Enter# ";std::getline(std::cin, input);// 发消息int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));(void)n;// 读取消息char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);int m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&server, &len);if(m > 0){buffer[m] = 0;std::cout << buffer << std::endl;} }return 0;
}
4. 运行
# Makefile
.PHONY:all
all:udpclient udpserverudpclient:UdpClient.ccg++ -o $@ $^ -std=c++17 -Wno-deprecated
udpserver:UdpServer.ccg++ -o $@ $^ -std=c++17 -Wno-deprecated.PHONY:clean
clean:rm -r udpclient udpserver
本环回地(127.0.0.1)是一个虚拟的网络接口,它允许在同一台计算机上运行的网络程序相互通信,而数据包不会真正离开计算机进入物理网络。---> 经常用来进行网络代码的测试。
# server端
$ ./udpserver 8080
[2025-10-16 14:42:38][INFO][3317203][UdpServer.hpp][42]- socket success, sockfd: 3
[2025-10-16 14:42:38][INFO][3317203][UdpServer.hpp][65]- bind success, sockfd: 3
[2025-10-16 14:42:51][DEBUG][3317203][UdpServer.hpp][88]- [127.0.0.1:57842]#1234
[2025-10-16 14:42:59][DEBUG][3317203][UdpServer.hpp][88]- [127.0.0.1:57842]#aaa
# client端,绑定本地环回地址
$ ./udpclient 127.0.0.1 8080
Please Enter# 1234
hello, 1234
Please Enter# aaa
hello, aaa
这样就完成了一个UDP服务器的实现!
5. 查看网络状态:netstat
server端绑定地址为 INADDR_ANY 后:Local Address 的地址全部变成0,只显示了端口号为我们设置的8080。
$ netstat -anup
(Not all processes could be identified, non-owned process infowill not be shown, you would have to be root to see it all.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
udp 0 0 10.2.24.14:68 0.0.0.0:* -
udp 0 0 10.2.24.14:123 0.0.0.0:* -
udp 0 0 127.0.0.1:123 0.0.0.0:* -
udp 0 0 0.0.0.0:123 0.0.0.0:* -
udp 0 0 0.0.0.0:8080 0.0.0.0:* 114365/./udpserver
udp 0 0 127.0.0.53:53 0.0.0.0:* -
udp6 0 0 fe80::5054:ff:fe5a::123 :::* -
udp6 0 0 ::1:123 :::* -
udp6 0 0 :::123 :::* -
五、实现DictServer网络字典
业务逻辑:
UdpServer (网络通信层)
↓
Dict (业务逻辑层 - 翻译功能)
↓
InetAddr (工具层 - 地址处理)
↓
Log (工具层 - 日志系统)
1. Dict.hpp
(1) 从文件加载词典到内存哈希表
(2) 提供单词翻译查询
(3) 集成客户端信息日志
#pragma once#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "Log.hpp"
#include "InetAddr.hpp"using namespace LogMoudle;const std::string default_path = "./dictionary.txt";
const std::string sep = ": ";class Dict
{
public:Dict(const std::string &path = default_path) : _dict_path(path){}bool LoadDict(){std::ifstream in(_dict_path); // 打开了一个输入流if (!in.is_open()){LOG(LogLevel::DEBUG) << "打开字典:" << _dict_path << "失败";return false;}// 打开成功了std::string line;while (std::getline(in, line)){ // 每次得到一行auto pos = line.find(sep); // 查找分隔符下标if (pos == std::string::npos) // 没找到{LOG(LogLevel::WARNING) << "解析:" << line << "失败";continue;}std::string english = line.substr(0, pos);std::string chinese = line.substr(pos + sep.size());if (english.empty() || chinese.empty()){LOG(LogLevel::WARNING) << "没有有效内容:" << line;continue;}_dict.insert(std::make_pair(english, chinese)); // 插入数据对LOG(LogLevel::WARNING) << "加载:" << line;}in.close();return true;}std::string Translate(const std::string &word, InetAddr &client){auto iter = _dict.find(word);if (iter == _dict.end()) // 没找到{LOG(LogLevel::DEBUG) << "进入到翻译模块, [" << client.Ip() << "; " << client.Port() << "]#" << word << "->None";return "None";}LOG(LogLevel::DEBUG) << "进入到翻译模块, [" << client.Ip() << "; " << client.Port() << "]#" << word << "->" << iter->second;return iter->second;}~Dict(){}private:std::string _dict_path; // 路径+文件名std::unordered_map<std::string, std::string> _dict; // 字典:哈希映射
};
// dictionary.txt
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
word:
:单词
2. InetAddr.hpp
封装网络地址转换,提供接口访问客户端信息。
// 网络地址与主机地址进行转换的类
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>class InetAddr
{
public:InetAddr(const sockaddr_in &addr) : _addr(addr){// 网络地址转化成ip和端口_port = ntohs(_addr.sin_port);_ip = inet_ntoa(_addr.sin_addr);}uint16_t Port() { return _port; }std::string Ip() { return _ip; }~InetAddr() {}private:struct sockaddr_in _addr;std::string _ip;uint16_t _port;
};
3. UdpServer.hpp
关键修改:
// 回调函数现在接收客户端地址信息
using func_t = std::function<std::string(const std::string&, InetAddr&)>;std::string result = _func(buffer, client); // 传递客户端信息
#pragma once
#include <iostream>
#include <string>
#include <string.h>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "InetAddr.hpp"using namespace LogMoudle;// 定义函数类型func_t
// 分层处理,UDP只进行网络通信, 上层操作自定义
using func_t = std::function<std::string(const std::string&, InetAddr&)>;const int defaultsockfd = -1;class UdpServer
{
public:UdpServer(uint16_t port, func_t func): _sockfd(defaultsockfd),// _ip(ip),_port(port),_isrunning(false),_func(func) {}void Init(){// 1. 创建套接字// AF_INET + SOCK_DGRAM = UDP_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 成功返回文件描述符if (_sockfd < 0){ // 失败LOG(LogLevel::FATAL) << "socket error!";exit(1);}LOG(LogLevel::INFO) << "socket success, sockfd: " << _sockfd;// 2. 绑定socket信息: ip和端口号// (1) 填充sockaddr_in结构体struct sockaddr_in local; // 栈上开辟的bzero(&local, sizeof(local)); // 清空lock, 用bzero初始化local.sin_family = AF_INET; // 协议家族 —> AF_INET 网络通信// IP信息和端口信息, 都要被发送到网络// 主机序列 -> 网络序列!!!local.sin_port = htons(_port);// ip -> 四字节 -> 网络序列 : in_addr_t inet_addr(const char *cp);local.sin_addr.s_addr = INADDR_ANY; // 可以被绑定到所有系统网络接口上// 为什么服务器端需要显示的bind端口号?因为一个服务端可能要链接多个客户端,IP和端口号必须是众所周知且不能被轻易改变的!// 绑定int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind error";exit(2);}LOG(LogLevel::INFO) << "bind success, sockfd: " << _sockfd;}void Start(){_isrunning = true;while (_isrunning){// 1. 收消息char buffer[1024]; // 缓冲区struct sockaddr_in peer; // 远端socklen_t len = sizeof(peer); // 无符号整数, 远端的大小ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); // flag设置为0, 阻塞式IOif (s > 0){ // 成功// 发送者的端口号和ipInetAddr client(peer);buffer[s] = 0;// 收到的内容当做英文单词std::string result = _func(buffer, client); // 回调函数,处理数据// 2. 发消息(结果)sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len); // 发送处理后的结果}}}~UdpServer(){}private:int _sockfd;uint16_t _port; // 端口号bool _isrunning;func_t _func; // 服务器的回调函数, 用来对数据进行处理
};
4. UpdServer.cc
(1)加载词典文件到内存
(2)创建 UDP 服务器并绑定端口
(3)进入事件循环等待客户端请求
// UDP服务端
#include <iostream>
#include <memory>
#include "UdpServer.hpp" // 网络通信功能
#include "Dict.hpp" // 翻译功能// 翻译
// 1. 翻译系统,把字符串当成英文单词,把英文单词翻译成汉语
// 2. 基于文件来做std::string defaulthandler(const std::string &message)
{std::string hello = "hello, ";hello += message;return hello;
}// ./udpsrver port
int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << " port" << std::endl;return 1;}// std::string ip = argv[1];uint16_t port = std::stoi(argv[1]); // 字符串转整数Enable_Console_Log_Strategy(); // 显示器打印// 1. 字典对象提供翻译功能Dict dict;dict.LoadDict(); // 加载字典// 2. 网络服务器对象,提供通信功能std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&dict](const std::string &word, InetAddr &client) -> std::string{ return dict.Translate(word, client); }); // 智能指针构造服务器usvr->Init();usvr->Start();return 0;
}
5. UdpClient.cc
客户端发送单词 → UDP接收 → Dict翻译 → 返回结果 → UDP发送回复
// UDP客户端
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>// ./udpclient server_ip server_port
int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;return 1;}std::string server_ip = argv[1];uint16_t server_port = std::stoi(argv[2]); // 字符串转整数// 1. 创建套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd < 0){std::cerr << "socket error" << std::endl;return 2;}// 2.本地ip和端口号也要与上面的“文件”套接字关联?client 端也要bind!// 但是 client 不用显示的bind, 首次发送消息,OS会自动给 client 进行bind,OS知道ip,端口号采用随机端口号的方式。// 为什么一个端口号只能被一个进程 bind?为了避免 client 端口冲突// client 端的端口号是几不重要,只要是唯一个就行!// 填写服务端信息struct sockaddr_in server;memset(&server, 0, sizeof(server)); // 清空server.sin_family = AF_INET; // AF_INET代表: 使用的是ip协议家族server.sin_port = htons(server_port); // 端口号主机转网络server.sin_addr.s_addr = inet_addr(server_ip.c_str()); // IP地址: ip -> 四字节 -> 网络序列while(true){std::string input;std::cout << "Please Enter# ";std::getline(std::cin, input);// 发消息int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));(void)n;// 读取消息char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);int m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&server, &len);if(m > 0){buffer[m] = 0;std::cout << buffer << std::endl;} }return 0;
}
6. 运行
服务端:
$ ./udpserver 8080
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][48]- 加载:apple: 苹果
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][48]- 加载:banana: 香蕉
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][48]- 加载:cat: 猫
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][48]- 加载:dog: 狗
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][48]- 加载:book: 书
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][48]- 加载:pen: 笔
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][48]- 加载:happy: 快乐的
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][48]- 加载:sad: 悲伤的
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][48]- 加载:run: 跑
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][48]- 加载:jump: 跳
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][48]- 加载:teacher: 老师
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][48]- 加载:student: 学生
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][48]- 加载:car: 汽车
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][48]- 加载:bus: 公交车
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][48]- 加载:love: 爱
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][48]- 加载:hate: 恨
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][48]- 加载:hello: 你好
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][48]- 加载:goodbye: 再见
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][48]- 加载:summer: 夏天
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][48]- 加载:winter: 冬天
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][36]- 解析:word:失败
[2025-10-16 15:52:38][WARNING][3339796][Dict.hpp][36]- 解析::单词失败
[2025-10-16 15:52:38][INFO][3339796][UdpServer.hpp][43]- socket success, sockfd: 3
[2025-10-16 15:52:38][INFO][3339796][UdpServer.hpp][65]- bind success, sockfd: 3
[2025-10-16 15:52:44][DEBUG][3339796][Dict.hpp][62]- 进入到翻译模块, [127.0.0.1; 38383]#hello->你好
[2025-10-16 15:52:49][DEBUG][3339796][Dict.hpp][62]- 进入到翻译模块, [127.0.0.1; 38383]#apple->苹果
[2025-10-16 15:52:51][DEBUG][3339796][Dict.hpp][62]- 进入到翻译模块, [127.0.0.1; 38383]#pen->笔
[2025-10-16 15:52:54][DEBUG][3339796][Dict.hpp][59]- 进入到翻译模块, [127.0.0.1; 38383]#word->None
客户端:
$ ./udpclient 127.0.0.1 8080
Please Enter# hello
你好
Please Enter# apple
苹果
Please Enter# pen
笔
Please Enter# word
None
这样就实现了一个简单的网络字典!
六、进程聊天室
1. 单进程聊天室
服务器端:负责消息路由和用户管理
客户端:多线程(两个线程)分别处理消息发送和接收
(1)Route.hpp
• 管理在线用户列表
• 实现消息广播(群聊功能)
• 处理用户上线/下线
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"
#include "Log.hpp"using namespace LogMoudle;class Route
{
private:bool IsExit(InetAddr &peer){// 遍历查找for (auto &user : _online_user){if (user == peer) // [==]是重载过的return true;}return false;}void AddUser(InetAddr &peer){LOG(LogLevel::INFO) << "新增一个在线用户:" << peer.StringAddr();_online_user.push_back(peer);}void DeleteUser(InetAddr &peer){for (auto iter = _online_user.begin(); iter != _online_user.end(); iter++){if (*iter == peer){LOG(LogLevel::INFO) << "删除一个在线用户:" << peer.StringAddr() << "成功";_online_user.erase(iter);break;}}}public:Route(){}void MessageRpute(int sockfd, const std::string &message, InetAddr &peer){// 判断远端是否存在if (!IsExit(peer)){AddUser(peer);} // 此时用户已经在线了std::string send_message = peer.StringAddr() + "# " + message; // 127.0.0.1# 你好// 把一条消息发送给所有在线用户for (auto &user : _online_user){sendto(sockfd, send_message.c_str(), send_message.size(), 0, (const struct sockaddr *)&(user.NetAddr()), sizeof(user.NetAddr()));}// 用户想退出,输入“QUIT”if (message == "QUIT"){LOG(LogLevel::INFO) << "删除一个在线用户:" << peer.StringAddr();DeleteUser(peer);}}~Route(){}private:// 首次发消息等同于登录std::vector<InetAddr> _online_user; // 记录在线用户
};
(2)UdpServer.hpp
关键:
// 回调函数现在返回void,由回调函数自己负责发送回复
using func_t = std::function<void(int sockfd, const std::string&, InetAddr&)>;// 在Start()中不再自动发送回复,而是交给回调函数处理
_func(_sockfd, buffer, client); // 回调函数自己决定如何发送
#pragma once
#include <iostream>
#include <string>
#include <string.h>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "InetAddr.hpp"using namespace LogMoudle;// 定义函数类型func_t
// 分层处理,UDP只进行网络通信, 上层操作自定义
using func_t = std::function<void(int sockfd, const std::string&, InetAddr&)>;const int defaultsockfd = -1;class UdpServer
{
public:UdpServer(uint16_t port, func_t func): _sockfd(defaultsockfd),// _ip(ip),_port(port),_isrunning(false),_func(func) {}void Init(){// 1. 创建套接字// AF_INET + SOCK_DGRAM = UDP_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 成功返回文件描述符if (_sockfd < 0){ // 失败LOG(LogLevel::FATAL) << "socket error!";exit(1);}LOG(LogLevel::INFO) << "socket success, sockfd: " << _sockfd;// 2. 绑定socket信息: ip和端口号// (1) 填充sockaddr_in结构体struct sockaddr_in local; // 栈上开辟的bzero(&local, sizeof(local)); // 清空lock, 用bzero初始化local.sin_family = AF_INET; // 协议家族 —> AF_INET 网络通信// IP信息和端口信息, 都要被发送到网络// 主机序列 -> 网络序列!!!local.sin_port = htons(_port);// ip -> 四字节 -> 网络序列 : in_addr_t inet_addr(const char *cp);local.sin_addr.s_addr = INADDR_ANY; // 可以被绑定到所有系统网络接口上// 为什么服务器端需要显示的bind端口号?因为一个服务端可能要链接多个客户端,IP和端口号必须是众所周知且不能被轻易改变的!// 绑定int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind error";exit(2);}LOG(LogLevel::INFO) << "bind success, sockfd: " << _sockfd;}void Start(){_isrunning = true;while (_isrunning){// 1. 收消息char buffer[1024]; // 缓冲区struct sockaddr_in peer; // 远端socklen_t len = sizeof(peer); // 无符号整数, 远端的大小ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); // flag设置为0, 阻塞式IOif (s > 0){ // 成功// 发送者的端口号和ipInetAddr client(peer);buffer[s] = 0;// 回调函数,处理数据_func(_sockfd, buffer, client); // 2. 发消息(结果)// sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len); // 发送处理后的结果}}}~UdpServer(){}private:int _sockfd;uint16_t _port; // 端口号bool _isrunning;func_t _func; // 服务器的回调函数, 用来对数据进行处理
};
(3)UdpServer.cc
1. 接收客户端消息
2. 检查用户是否在线,如不在线则添加到在线列表
3. 将消息广播给所有在线用户
4. 如果消息是"QUIT",则从在线列表中移除用户
// UDP服务端
#include <iostream>
#include <memory>
#include "UdpServer.hpp" // 网络通信功能
#include "Route.hpp" // 路由功能// 翻译
// 1. 翻译系统,把字符串当成英文单词,把英文单词翻译成汉语
// 2. 基于文件来做std::string defaulthandler(const std::string &message)
{std::string hello = "hello, ";hello += message;return hello;
}// ./udpsrver port
int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << " port" << std::endl;return 1;}// std::string ip = argv[1];uint16_t port = std::stoi(argv[1]); // 字符串转整数Enable_Console_Log_Strategy(); // 显示器打印// 1. 路由服务Route r;// 2. 网络服务器对象,提供通信功能std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&r](int sockfd, const std::string &message, InetAddr &peer){r.MessageRpute(sockfd, message, peer);}); // 智能指针构造服务器usvr->Init();usvr->Start();return 0;
}
(4)UdpClient.cc
创建两个线程分别处理发送和接收:
发送线程:用户输入 → 发送到服务器
接收线程:监听服务器 → 显示广播消息
// UDP客户端
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include "Thread.hpp"using namespace ThreadModlue;int sockfd = 0; // 定义成全局的
std::string server_ip;
uint16_t server_port = 0; // 字符串转整数
pthread_t id;void Recv()
{while (true){// 读取消息char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (m > 0){buffer[m] = 0;std::cerr << buffer << std::endl; // 重定向//std::cout << buffer << std::endl; std::cerr.flush(); // std::cerr 默认是无缓冲, 要强制刷新缓冲区(***)}}
}void Send()
{// 填写服务端信息struct sockaddr_in server;memset(&server, 0, sizeof(server)); // 清空server.sin_family = AF_INET; // AF_INET代表: 使用的是ip协议家族server.sin_port = htons(server_port); // 端口号主机转网络server.sin_addr.s_addr = inet_addr(server_ip.c_str()); // IP地址: ip -> 四字节 -> 网络序列// 发送上线提示消息const std::string online = "inline";sendto(sockfd, online.c_str(), online.size(), 0, (struct sockaddr *)&server, sizeof(server));while (true){// 不断从标准输入获取std::string input;std::cout << "Please Enter# ";std::getline(std::cin, input);// 发消息int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));(void)n;// 退出聊天,直接cancel进程if(input == "QUIT"){pthread_cancel(id);break;}}
}// client也要做多线程改造
// ./udpclient server_ip server_port
int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;return 1;}server_ip = argv[1];server_port = std::stoi(argv[2]);// 1. 创建套接字sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;return 2;}// 2. 创建线程Thread recver(Recv);Thread sender(Send);recver.Start();sender.Start();id = recver.ID();recver.Join();sender.Join();return 0;
}
(5)实现通信
$ ./udpserver 8080
[2025-10-16 17:44:48][INFO][3375620][UdpServer.hpp][43]- socket success, sockfd: 3
[2025-10-16 17:44:48][INFO][3375620][UdpServer.hpp][65]- bind success, sockfd: 3
[2025-10-16 17:44:54][INFO][3375620][Route.hpp][26]- 新增一个在线用户:127.0.0.1:33630
2. 多进程聊天室---线程池
接收消息 → 封装任务 → 入队 → 线程池处理 → 广播消息
(1)UdpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <string.h>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "InetAddr.hpp"using namespace LogMoudle;// 定义函数类型func_t
// 分层处理,UDP只进行网络通信, 上层操作自定义
using func_t = std::function<void(int sockfd, const std::string &, InetAddr &)>;const int defaultsockfd = -1;class UdpServer
{
public:UdpServer(uint16_t port, func_t func): _sockfd(defaultsockfd),// _ip(ip),_port(port),_isrunning(false),_func(func){}void Init(){// 1. 创建套接字// AF_INET + SOCK_DGRAM = UDP_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 成功返回文件描述符if (_sockfd < 0){ // 失败LOG(LogLevel::FATAL) << "socket error!";exit(1);}LOG(LogLevel::INFO) << "socket success, sockfd: " << _sockfd;// 2. 绑定socket信息: ip和端口号// (1) 填充sockaddr_in结构体struct sockaddr_in local; // 栈上开辟的bzero(&local, sizeof(local)); // 清空lock, 用bzero初始化local.sin_family = AF_INET; // 协议家族 —> AF_INET 网络通信// IP信息和端口信息, 都要被发送到网络// 主机序列 -> 网络序列!!!local.sin_port = htons(_port);// ip -> 四字节 -> 网络序列 : in_addr_t inet_addr(const char *cp);local.sin_addr.s_addr = INADDR_ANY; // 可以被绑定到所有系统网络接口上/* //用该实现方法可以替换上面绑定socket信息内容, 创建类对象时就绑定了InetAddr addr("0", _port);addr.NetAddr();*/// 为什么服务器端需要显示的bind端口号?因为一个服务端可能要链接多个客户端,IP和端口号必须是众所周知且不能被轻易改变的!// 绑定int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind error";exit(2);}LOG(LogLevel::INFO) << "bind success, sockfd: " << _sockfd;}void Start(){_isrunning = true;while (_isrunning){// 1. 收消息char buffer[1024]; // 缓冲区struct sockaddr_in peer; // 远端socklen_t len = sizeof(peer); // 无符号整数, 远端的大小ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); // flag设置为0, 阻塞式IOif (s > 0){ // 成功// 发送者的端口号和ipInetAddr client(peer);buffer[s] = 0;// 回调函数,处理数据_func(_sockfd, buffer, client);// 2. 发消息(结果)// sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len); // 发送处理后的结果}}}~UdpServer(){}private:int _sockfd;uint16_t _port; // 端口号bool _isrunning;func_t _func; // 服务器的回调函数, 用来对数据进行处理
};
(2)UdpServer.cc
客户端流程不变,服务端流程如下:
1. 主线程:接收UDP消息
2. 主线程:封装为路由任务,提交到线程池
3. 线程池:工作线程从队列获取任务
4. 工作线程:执行Route::MessageRoute处理消息
5. 工作线程:广播消息给所有在线用户
// UDP服务端
#include <iostream>
#include <memory>
#include "UdpServer.hpp" // 网络通信功能
#include "Route.hpp" // 路由功能
#include "ThreadPool.hpp"using namespace ThraedPoolMoudule;
using task_t = std::function<void()>;// 翻译
// 1. 翻译系统,把字符串当成英文单词,把英文单词翻译成汉语
// 2. 基于文件来做std::string defaulthandler(const std::string &message)
{std::string hello = "hello, ";hello += message;return hello;
}// ./udpsrver port
int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << " port" << std::endl;return 1;}// std::string ip = argv[1];uint16_t port = std::stoi(argv[1]); // 字符串转整数Enable_Console_Log_Strategy(); // 显示器打印// 1. 路由服务Route r;// 2. 线程池, 将任务推送到线程池,再由线程池中的线程进行消息转发auto tp = ThreadPool<task_t>::GetInstance(); // 定义线程池对象,内部自动初始化// 3. 网络服务器对象,提供通信功能// 将路由功能push到线程池中std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port,[&r, &tp](int sockfd, const std::string &message, InetAddr &peer){// 后面三个参数是传给成员函数指针 Route::MessageRoute 的// 等同于:r.Route::MessageRoute(sockfd, message, peer); task_t t = std::bind(&Route::MessageRoute, &r, sockfd, message, peer); // 绑定路由tp->Enqueue(t);});usvr->Init();usvr->Start();return 0;
}
(3)运行聊天室
服务端显示日志:
$ ./udpserver 8080
[2025-10-16 18:06:30][DEBUG][3382960][ThreadPool.hpp][82]- 获取单例...
[2025-10-16 18:06:30][DEBUG][3382960][ThreadPool.hpp][85]- 首次使用单例, 创建之
[2025-10-16 18:06:30][INFO][3382960][ThreadPool.hpp][52]- create new thread success
[2025-10-16 18:06:30][INFO][3382960][ThreadPool.hpp][52]- create new thread success
[2025-10-16 18:06:30][INFO][3382960][ThreadPool.hpp][52]- create new thread success
[2025-10-16 18:06:30][INFO][3382960][ThreadPool.hpp][52]- create new thread success
[2025-10-16 18:06:30][INFO][3382960][ThreadPool.hpp][52]- create new thread success
[2025-10-16 18:06:30][INFO][3382960][UdpServer.hpp][43]- socket success, sockfd: 3
[2025-10-16 18:06:30][INFO][3382960][UdpServer.hpp][71]- bind success, sockfd: 3
[2025-10-16 18:06:43][INFO][3382960][ThreadPool.hpp][37]- 唤醒一个休眠线程
[2025-10-16 18:06:43][INFO][3382960][Route.hpp][28]- 新增一个在线用户:127.0.0.1:53620
[2025-10-16 18:06:51][INFO][3382960][ThreadPool.hpp][37]- 唤醒一个休眠线程
[2025-10-16 18:06:51][INFO][3382960][Route.hpp][28]- 新增一个在线用户:43.138.121.254:41444
[2025-10-16 18:06:56][INFO][3382960][ThreadPool.hpp][37]- 唤醒一个休眠线程
[2025-10-16 18:06:59][INFO][3382960][ThreadPool.hpp][37]- 唤醒一个休眠线程
[2025-10-16 18:07:05][INFO][3382960][ThreadPool.hpp][37]- 唤醒一个休眠线程
[2025-10-16 18:07:11][INFO][3382960][ThreadPool.hpp][37]- 唤醒一个休眠线程
[2025-10-16 18:10:20][INFO][3382960][Route.hpp][71]- 删除一个在线用户:127.0.0.1:53620
[2025-10-16 18:10:20][INFO][3382960][Route.hpp][38]- 删除一个在线用户:127.0.0.1:53620成功
[2025-10-16 18:10:27][INFO][3382960][ThreadPool.hpp][37]- 唤醒一个休眠线程
[2025-10-16 18:10:27][INFO][3382960][Route.hpp][71]- 删除一个在线用户:43.138.121.254:41444
[2025-10-16 18:10:27][INFO][3382960][Route.hpp][38]- 删除一个在线用户:43.138.121.254:41444成功
完整的聊天室代码,已上传资源!