Linux UdpSocket的应用
一.基于UdpSocket的翻译系统
根据上一章的内容我们知道,服务端在启动时会默认选择消息处理方式为回显,效果如下:
#include <iostream>
#include <memory>
#include "UdpServer.hpp"// 默认的处理函数
std::string defaulthandler(const std::string &message)
{std::string hello = "hello, ";hello += message;return hello;
}// ./udpserver port方式传参
int main(int argc, char *argv[])
{if(argc != 2){std::cerr << "Usage: " << argv[0] << " port" << std::endl;return 1;}uint16_t port = std::stoi(argv[1]);Enable_Console_Log_Strategy();// 创建一个UdpServer对象,并启动服务std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, defaulthandler);usvr->Init();usvr->Start();return 0;
}
本章我们将编写一个Dict.hpp,目的为当客户端发送消息之后,由服务端接收消息,并执行方法为将英文单词转换为中文。为此,我们需要添加以下模块:
1.字典集dictionary.txt
创建一个txt文件,内容格式为中文: 英文,便于我们后续的查找和翻译。
apple: 苹果
ability: 能力
abnormal: 反常的
abolish: 废除
abroad: 在国外
//内容可自定义
2.字典类Dict.hpp
1.加载字典方法LoadDict()
这个类主要包含了我们对上面字典集的处理以及英汉互译的功能。首先就是对字典集的处理。
该方法用于按行读取字典集dictionary.txt,并利用冒号将中英文分割,从而将一个英汉单词组合存储在一个unordered_map中。除此之外,还对字典集中的空白单词做了差错控制——当读取到空白行或者英汉其中一个为空时,就返回没有有效内容。
bool LoadDict(){// 将txt中的文件按key-value存储到map中std::ifstream in(_path);if (!in.is_open()){LOG(LogLevel::DEBUG) << "打开字典:" << _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::DEBUG) << "加载: " << line;}in.close();return true;}
2.单词翻译功能Translate()
接着就是单词翻译功能,参数就是从客户端读取的消息message以及客户端信息InetAddr,然后根据message查询unordered_map,如果查询成功则返回对应message的中文翻译。
std::string Translate(const std::string &word, InetAddr &client){auto it = _dict.find(word);if (it == _dict.end()){LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip() << " : " << client.Port() << "]# " << word << "->None";return "None";}LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip() << " : " << client.Port() << "]# " << word << "->" << it->second;return it->second;}
3.完整的字典功能
1.完整Dict.hpp
首先要为Dict类传入我们的字典集dictionary.txt路径,路径处理方法使用C++ 17对文件路径的处理方法ifstream,找到后就打开文件流。之后的操作同上所述。
#pragma once#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "Log.hpp"
#include "InetAddr.hpp"// 这个类的用处很简单
// 先新建一个dictionary.txt用于存放单词,然后将英文:汉语的形式存储在map中
// 读取用户输入,并进行分割
// 根据key-value进行查找const std::string defaultdict = "./dictionary.txt";
const std::string sep = ": ";using namespace LogModule;class Dict
{
public:Dict(const std::string &path = defaultdict) : _path(path){}bool LoadDict(){// 将txt中的文件按key-value存储到map中std::ifstream in(_path);if (!in.is_open()){LOG(LogLevel::DEBUG) << "打开字典:" << _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::DEBUG) << "加载: " << line;}in.close();return true;}// 翻译功能,从输入读取英文单词根据map检索中文std::string Translate(const std::string &word, InetAddr &client){auto it = _dict.find(word);if (it == _dict.end()){LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip() << " : " << client.Port() << "]# " << word << "->None";return "None";}LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip() << " : " << client.Port() << "]# " << word << "->" << it->second;return it->second;}~Dict(){}private:std::string _path;std::unordered_map<std::string, std::string> _dict;
};
2.服务端对应修改
服务端的底层封装方法没有改变,改变的只有UdpServer.cc创建server对象时传入的func_t对消息的处理方法。因此我们仅需要做如下改动即可:
#include <iostream>
#include <memory>
#include "Dict.hpp" // 翻译的功能
#include "UdpServer.hpp" // 网络通信的功能// 需求
// 1. 翻译系统,字符串当成英文单词,把英文单词翻译成为汉语
// 2. 基于文件来做// ./udpserver 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&cli)->std::string{return dict.Translate(word, cli);});usvr->Init();usvr->Start();return 0;
}
4.演示效果
运行服务端UdpServer,并指定端口号为8080。然后运行客户端UdpClient。

二.基于UdpSocket的消息群发系统
在编写这个系统前,我们先复习以下UdpSocket的工作方式:
服务器端:
创建Socket → 2. 绑定地址 → 3. 接收/发送数据 → 4. 关闭Socket
客户端:
创建Socket → 2. 发送/接收数据 → 3. 关闭Socket
客户端不需要显式绑定。
1.网络通信模块UdpServer/UdpClient
1.UdpServer
Init函数主要完成上面的socket创建与初始化一系列工作,包含创建Socket,填写服务端的信息sockaddr_in local(ip和port,其中ip设置为任意ip),绑定地址。
Start函数即启动服务端,为了实现服务器持久运行的效果,将整个业务逻辑设置在while死循环中。在这个函数中用于接收来自服务端的消息,并通过_func进行回调处理。因为我们这里是一个聊天室系统,所以_func在使用时应该传入一个群转发消息效果的函数。
// UdpServer 核心功能
void UdpServer::Init() {_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 创建UDP socketbind(_sockfd, (struct sockaddr*)&local, sizeof(local)); // 绑定端口
}void UdpServer::Start() {while (_isrunning) {// 阻塞接收消息recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);_func(_sockfd, buffer, client); // 回调处理函数}
}
我们对服务端做多线程改造:接收到客户端的消息不再由当前服务端主线程处理,而是将当前消息推送给一个线程池,让线程池对消息执行对应的方法。
#include <iostream>
#include <memory>
#include "Route.hpp"
#include "UdpServer.hpp" // 网络通信的功能
#include "ThreadPool.hpp"using namespace ThreadPoolModule;using task_t = std::function<void()>;// ./udpserver 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. 网络服务器对象,提供通信功能std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&r, &tp](int sockfd, const std::string &message, InetAddr&peer){task_t t = std::bind(&Route::MessageRoute,&r, sockfd, message, peer);tp->Enqueue(t);});usvr->Init();usvr->Start();return 0;
}
2.UdpClient
对客户端进行多线程改造:在改造前,会出现因为客户端思考而看不到服务端群发消息的情况,那是因为getline函数和recvfrom函数的特性:阻塞式读取。我们需要对客户端内多种方法进行解耦。
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "Thread.hpp"int sockfd = 0;
std::string server_ip;
uint16_t server_port = 0;
pthread_t id;using namespace ThreadModlue;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; // 2}}
}
void Send()
{struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());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# "; // 1std::getline(std::cin, input); // 0int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));(void)n;if (input == "QUIT"){pthread_cancel(id);break;}}
}// ./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. 创建socketsockfd = 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;
}
2.一个例子串联各模块协作
场景:客户端发送Hello消息
阶段1: 客户端发送准备
1.1 用户输入处理
// UdpClient.cc - Send线程
void Send() {std::string input;std::cout << "Please Enter# "; // 用户提示std::getline(std::cin, input); // 等待用户输入"Hello"// 构建发送数据包sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));
}
数据包内容:
{源地址: 客户端A的IP:随机端口 (192.168.1.100:50001),目标地址: 服务器IP:端口 (192.168.1.10:8080),数据: "Hello"
}
1.2 多线程优势体现
-
发送线程:专注于用户交互,不受网络延迟影响
-
接收线程:在后台实时监听服务器消息,随时准备显示
阶段2: 服务器接收与任务分发
2.1 网络层接收
// UdpServer.hpp - Start()
while (_isrunning) {char buffer[1024];struct sockaddr_in peer; // 保存客户端地址socklen_t len = sizeof(peer);// 阻塞接收,所有客户端消息都进入同一个Socketssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);if (s > 0) {buffer[s] = 0; // 添加字符串终止符InetAddr client(peer); // 封装客户端地址信息_func(_sockfd, buffer, client); // 触发回调}
}
此时的关键信息:
-
buffer= "Hello" -
client= 封装了192.168.1.100:50001的InetAddr对象
2.2 回调触发与任务提交
这里做的多线程改造,即将从网络层接收到的数据Hello推送给线程池,让随机一个线程对该消息执行对应方法
// UdpServer.cc - Lambda回调
[&r, &tp](int sockfd, const std::string &message, InetAddr& peer){// 创建路由任务task_t t = std::bind(&Route::MessageRoute, &r, sockfd, message, peer);// 提交到线程池队列tp->Enqueue(t);
}
线程池的工作机制:
// ThreadPool.hpp - Enqueue()
bool Enqueue(const T &in) {if (_isrunning) {LockGuard lockguard(_mutex);_taskq.push(in); // 任务入队// 如果有休眠线程,唤醒一个if (_threads.size() == _sleepernum)WakeUpOne();return true;}return false;
}
阶段3: 线程池并发处理
3.1 工作者线程获取任务
// ThreadPool.hpp - HandlerTask()
void HandlerTask() {while (true) {T t; // task_t类型{LockGuard lockguard(_mutex);// 等待条件:队列有任务或线程池关闭while (_taskq.empty() && _isrunning) {_sleepernum++;_cond.Wait(_mutex); // 线程休眠,释放CPU_sleepernum--;}// 退出条件检查if (!_isrunning && _taskq.empty()) break;// 获取任务t = _taskq.front();_taskq.pop();}t(); // 执行Route::MessageRoute}
}
3.2 并发处理优势
-
多个消息并行处理:客户端B、C的消息可同时被不同线程处理
-
I/O不阻塞业务逻辑:网络接收线程迅速返回,继续接收新消息
阶段4: 路由逻辑与消息广播
4.1 用户管理与消息格式化
// Route.hpp - MessageRoute()
void MessageRoute(int sockfd, const std::string &message, InetAddr &peer) {LockGuard lockguard(_mutex); // 保护在线用户列表// 1. 用户上线检测if (!IsExist(peer)) {AddUser(peer); // 添加到在线列表LOG(LogLevel::INFO) << "新增在线用户: " << peer.StringAddr();}// 2. 消息格式化std::string send_message = peer.StringAddr() + "# " + message;// 结果: "192.168.1.100:50001# Hello"// 3. 广播给所有在线用户for (auto &user : _online_user) {sendto(sockfd, send_message.c_str(), send_message.size(), 0,(const struct sockaddr *)&(user.NetAddr()), sizeof(user.NetAddr()));}// 4. 用户退出处理if (message == "QUIT") {DeleteUser(peer);}
}
4.2 广播机制细节
-
包含发送者:客户端A也会收到自己发送的消息(聊天室特性)
-
原子性操作:加锁确保用户列表在广播过程中不被修改
-
网络效率:使用同一个socket向多个目标发送
阶段5: 客户端接收与显示
5.1 实时消息接收
// UdpClient.cc - Recv线程
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; // 立即显示}}
}
5.2 多线程协同效果
终端显示效果:
Please Enter# Hello ← 发送线程的cout输出
192.168.1.100:50001# Hello ← 接收线程的cerr输出(立即显示)
Please Enter# ← 发送线程继续等待输入
3.关键协同机制总结
1. Socket身份标识与消息路由
-
服务器Socket:既是身份名片(
192.168.1.10:8080),又是统一入口 -
客户端Socket:自动分配身份(
随机IP:Port),作为通信端点 -
路由系统:通过保存的客户端身份实现精确消息投递
2. 并发处理的层次化设计
网络I/O层 (UdpServer) → 任务队列层 (ThreadPool) → 业务逻辑层 (Route)↓ ↓ ↓高频率操作 负载均衡 复杂业务处理简单快速 任务调度 状态管理
3. 线程安全的协同
-
互斥锁:保护共享资源(在线用户列表、任务队列)
-
条件变量:高效线程调度,避免忙等待
-
RAII模式:自动资源管理,防止死锁
4. 实时性保障
-
客户端双线程:输入不阻塞接收,实现真正实时聊天
-
服务器异步处理:网络接收与业务逻辑分离,提高吞吐量
-
无阻塞设计:关键路径上无长时间阻塞操作
5.Route方法上锁的作用
保护数据竞争
// 线程A正在广播消息
for (auto &user : _online_user) { // 迭代器遍历中...sendto(sockfd, message, ...); // 向用户发送消息
}// 线程B同时删除用户
_online_user.erase(iter); // 修改vector结构!
不加锁的后果:
-
迭代器失效:线程B删除元素可能导致线程A的迭代器失效,程序崩溃
-
数据不一致:可能向已离线的用户发送消息,或漏发消息
-
内存访问违规:访问已删除的用户地址信息
实际保护的操作:
void MessageRoute(...) {LockGuard lockguard(_mutex);// 1. 检查用户是否存在if (!IsExist(peer)) {AddUser(peer); // 修改vector:push_back}// 2. 遍历广播消息 for (auto &user : _online_user) { // 读取vectorsendto(...); // 使用用户信息}// 3. 处理用户退出if (message == "QUIT") {DeleteUser(peer); // 修改vector:erase}
}
完整数据流图

完整代码
https://gitee.com/wjhwujiahao/linux-fundamentals-learning.git
