Linux UdpSocket的应用

一.基于UdpSocket的翻译系统

根据上一章的内容我们知道,服务端在启动时会默认选择消息处理方式为回显,效果如下:

cpp 复制代码
#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文件,内容格式为中文: 英文,便于我们后续的查找和翻译。

cpp 复制代码
apple: 苹果
ability: 能力
abnormal: 反常的
abolish: 废除
abroad: 在国外
//内容可自定义

2.字典类Dict.hpp

1.加载字典方法LoadDict()

这个类主要包含了我们对上面字典集的处理以及英汉互译的功能。首先就是对字典集的处理。

该方法用于按行读取字典集dictionary.txt,并利用冒号将中英文分割,从而将一个英汉单词组合存储在一个unordered_map中。除此之外,还对字典集中的空白单词做了差错控制------当读取到空白行或者英汉其中一个为空时,就返回没有有效内容。

cpp 复制代码
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的中文翻译。

cpp 复制代码
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,找到后就打开文件流。之后的操作同上所述。

cpp 复制代码
#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对消息的处理方法。因此我们仅需要做如下改动即可:

cpp 复制代码
#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的工作方式:

服务器端:

  1. 创建Socket → 2. 绑定地址 → 3. 接收/发送数据 → 4. 关闭Socket

客户端:

  1. 创建Socket → 2. 发送/接收数据 → 3. 关闭Socket

  2. 客户端不需要显式绑定。

1.网络通信模块UdpServer/UdpClient

1.UdpServer

Init函数主要完成上面的socket创建与初始化一系列工作,包含创建Socket,填写服务端的信息sockaddr_in local(ip和port,其中ip设置为任意ip),绑定地址。

Start函数即启动服务端,为了实现服务器持久运行的效果,将整个业务逻辑设置在while死循环中。在这个函数中用于接收来自服务端的消息,并通过_func进行回调处理。因为我们这里是一个聊天室系统,所以_func在使用时应该传入一个群转发消息效果的函数。

cpp 复制代码
// UdpServer 核心功能
void UdpServer::Init() {
    _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 创建UDP socket
    bind(_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); // 回调处理函数
    }
}

我们对服务端做多线程改造:接收到客户端的消息不再由当前服务端主线程处理,而是将当前消息推送给一个线程池,让线程池对消息执行对应的方法。

cpp 复制代码
#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函数的特性:阻塞式读取。我们需要对客户端内多种方法进行解耦。

cpp 复制代码
#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# "; // 1
        std::getline(std::cin, input); // 0

        int 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. 创建socket
    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;
}

2.一个例子串联各模块协作

场景:客户端发送Hello消息

阶段1: 客户端发送准备

1.1 用户输入处理
cpp 复制代码
// 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));
}

数据包内容:

cpp 复制代码
{
    源地址: 客户端A的IP:随机端口 (192.168.1.100:50001),
    目标地址: 服务器IP:端口 (192.168.1.10:8080),
    数据: "Hello"
}
1.2 多线程优势体现
  • 发送线程:专注于用户交互,不受网络延迟影响

  • 接收线程:在后台实时监听服务器消息,随时准备显示

阶段2: 服务器接收与任务分发

2.1 网络层接收
cpp 复制代码
// UdpServer.hpp - Start()
while (_isrunning) {
    char buffer[1024];
    struct sockaddr_in peer;  // 保存客户端地址
    socklen_t len = sizeof(peer);
    
    // 阻塞接收,所有客户端消息都进入同一个Socket
    ssize_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推送给线程池,让随机一个线程对该消息执行对应方法

cpp 复制代码
// 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);
}

线程池的工作机制:

cpp 复制代码
// 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 工作者线程获取任务
cpp 复制代码
// 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 用户管理与消息格式化
cpp 复制代码
// 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 实时消息接收
cpp 复制代码
// 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 多线程协同效果
cpp 复制代码
终端显示效果:
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. 并发处理的层次化设计

cpp 复制代码
网络I/O层 (UdpServer) → 任务队列层 (ThreadPool) → 业务逻辑层 (Route)
     ↓                      ↓                       ↓
 高频率操作             负载均衡                复杂业务处理
 简单快速               任务调度                状态管理

3. 线程安全的协同

  • 互斥锁:保护共享资源(在线用户列表、任务队列)

  • 条件变量:高效线程调度,避免忙等待

  • RAII模式:自动资源管理,防止死锁

4. 实时性保障

  • 客户端双线程:输入不阻塞接收,实现真正实时聊天

  • 服务器异步处理:网络接收与业务逻辑分离,提高吞吐量

  • 无阻塞设计:关键路径上无长时间阻塞操作

5.Route方法上锁的作用

保护数据竞争

cpp 复制代码
// 线程A正在广播消息
for (auto &user : _online_user) {     // 迭代器遍历中...
    sendto(sockfd, message, ...);     // 向用户发送消息
}

// 线程B同时删除用户
_online_user.erase(iter);             // 修改vector结构!

不加锁的后果:

  • 迭代器失效:线程B删除元素可能导致线程A的迭代器失效,程序崩溃

  • 数据不一致:可能向已离线的用户发送消息,或漏发消息

  • 内存访问违规:访问已删除的用户地址信息

实际保护的操作:

cpp 复制代码
void MessageRoute(...) {
    LockGuard lockguard(_mutex);
    
    // 1. 检查用户是否存在
    if (!IsExist(peer)) {
        AddUser(peer);  // 修改vector:push_back
    }
    
    // 2. 遍历广播消息  
    for (auto &user : _online_user) {  // 读取vector
        sendto(...);                   // 使用用户信息
    }
    
    // 3. 处理用户退出
    if (message == "QUIT") {
        DeleteUser(peer);  // 修改vector:erase
    }
}

完整数据流图

完整代码

https://gitee.com/wjhwujiahao/linux-fundamentals-learning.git

相关推荐
稚辉君.MCA_P8_Java2 小时前
深入理解 TCP;场景复现,掌握鲜为人知的细节
java·linux·网络·tcp/ip·kubernetes
小无名呀2 小时前
socket_udp
linux·网络·c++·网络协议·计算机网络·udp
wusam2 小时前
计算机网络实验04:IP与ICMP数据报分析实验
网络·计算机网络·icmp分片报文
小马哥编程2 小时前
【软考架构】案例分析-瘦客户端C/S架构
运维·服务器·架构
大大da怪i2 小时前
WSL-Ubuntu忘记root密码,修改root密码
linux·ubuntu
老黄编程2 小时前
09-ubuntu20.04 执行 apt update时报错,是因为官网已停止维护不再更新的缘故吗?
linux·运维·服务器·ubuntu·数字证书
Supernova_Jun2 小时前
ffmpeg图片转视频
linux·运维·服务器
水月wwww3 小时前
ubuntu网络连接出错解决办法
linux·运维·计算机网络·ubuntu·操作系统·ubuntu网络连接
0wioiw03 小时前
Ubuntu(①shell脚本)
linux·运维·ubuntu