Linux网络编程:UDP进阶,从字典服务器到简单聊天室

前言

上一篇简单实现了 UDP Echo 服务器和客户端,主要熟悉了 socket、bind、sendto 和 recvfrom 等接口。

不过,单纯实现数据的发送和接收还不够。实际开发中,服务器通常还需要根据客户端发送的内容执行不同的操作。

这篇继续在 UDP 的基础上做一些扩展,主要记录三个内容:

  1. 实现一个简单的英译汉字典服务器。
  2. 对 UDP 通信接口进行封装。
  3. 利用 UDP 实现一个简单聊天室。

一、实现一个简单的网络字典

先来做一个比较简单的功能:客户端输入一个英文单词,服务器查询对应的中文意思,然后返回给客户端。

例如:

text 复制代码
hello: 你好
apple: 苹果
book: 书
teacher: 老师
student: 学生

这里可以把单词和对应的中文意思保存到一个文件中,服务器启动时读取文件,再将数据存入哈希表。

1. 使用unordered_map存储单词

这里使用 C++ 中的 unordered_map:

cpp 复制代码
unordered_map<string, string> _dict;

其中:

  • key 保存英文单词。
  • value 保存对应的中文意思。

读取文件时,可以使用 getline 按行读取。

每行通过 ": " 分隔,分别取出单词和中文意思。

核心代码:

cpp 复制代码
const std::string sep = ": ";
void LoadDict()
{
    std::ifstream in(_confpath);

    if(!in.is_open())
    {
        std::cerr << "open file error" << std::endl;
        return;
    }

    std::string line;

    while(std::getline(in, line))
    {
        if(line.empty())
            continue;

        auto pos = line.find(sep);

        if(pos == std::string::npos)
            continue;

        std::string key = line.substr(0, pos);
        std::string value = line.substr(pos + sep.size());

        _dict.insert(std::make_pair(key, value));
    }

    in.close();
}

这里主要用到了几个字符串操作:

  • find():查找分隔符的位置。
  • substr():截取字符串。
  • insert():将键值对插入哈希表。

例如:

text 复制代码
apple: 苹果

通过 find 找到分隔符的位置,就可以把前面的 apple 和后面的 苹果 分别取出来。

2. 实现查询功能

字典加载完成之后,查询就比较简单了。

cpp 复制代码
std::string Translate(const std::string& key)
{
    auto iter = _dict.find(key);

    if(iter == _dict.end())
        return "Unknown";

    return iter->second;
}

如果找到了对应单词,就返回它的中文意思;如果没有找到,就返回 Unknown。

这样字典的基本功能就实现了。

二、通过回调函数分离通信和业务逻辑

现在虽然已经有了字典查询功能,但是还有一个问题:

如果以后服务器不只是查字典,还需要实现其他功能,是不是每增加一种功能,都要修改 UDP 服务器内部的代码?

显然这样不太方便。

所以这里引入了回调函数。

1. 使用std::function定义回调类型

先定义一个函数类型:

cpp 复制代码
using func_t = std::function<void(const std::string& req,
                                  std::string* resp)>;

这里的回调函数接收两个参数:

  • req:客户端发送过来的请求。
  • resp:服务器处理后返回的响应。

然后在 UDP 服务器中保存这个回调函数:

cpp 复制代码
class UdpServer
{
public:
    UdpServer(func_t func, uint16_t port)
        : _func(func), _port(port)
    {}

private:
    func_t _func;
};

服务器接收到客户端的数据之后,不再直接处理具体业务,而是调用:

cpp 复制代码
std::string value;

_func(buffer, &value);

最后再通过 sendto 将处理结果返回给客户端:

cpp 复制代码
sendto(_sockfd,
       value.c_str(),
       value.size(),
       0,
       (struct sockaddr*)&peer,
       len);

这样,UDP 服务器只负责接收数据和发送数据,具体要做什么交给外部传入的回调函数处理。

2. 为什么使用std::function?

这里使用 std::function 而不是直接使用普通函数指针,是因为它可以兼容普通函数、仿函数和 Lambda 表达式。

例如,我们可以直接传入一个普通函数:

cpp 复制代码
void Execute(const std::string& req, std::string* resp)
{
    *resp = gdict.Translate(req);
}

创建服务器时:

cpp 复制代码
UdpServer server(Execute, 8888);

也可以传入 Lambda 表达式。

这样就不需要把具体的业务逻辑写死在 UDP 服务器里面,后续想增加其他业务,只需要提供不同的回调函数即可。

三、DictServer代码

cpp 复制代码
// Dict.hpp
#pragma once

#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "Log.hpp"
#include "InetAddr.hpp"

const std::string defaultdict = "./dictionary.txt";
const std::string sep = ": ";

using namespace LogModule;

class Dict
{
public:
    Dict(const std::string &path = defaultdict) : _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))
        {
            // "apple: 苹果"
            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;
    }
    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;
};
cpp 复制代码
// InetAddr.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

// 网络地址和主机地址之间进行转换的类
class InetAddr
{
public:
    InetAddr(struct sockaddr_in &addr) : _addr(addr)
    {
        _port = ntohs(_addr.sin_port);   // 从网络中拿到的, 网络序列->本地格式
        _ip = inet_ntoa(_addr.sin_addr); // 4字节风格网络的IP->点分十进制的字符串风格的IP
    }
    uint16_t Port() { return _port; }
    std::string Ip() { return _ip; }
    ~InetAddr()
    {
    }

private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};
cpp 复制代码
// UdpClient.cc
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.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. 创建socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        return 2;
    }

    // 2. client端不用显式地bind, 首次发送消息, OS会自动给client进行bind, OS知道IP, 端口号采用随机端口号的方式
    //    client端的端口号是几不重要, 只要唯一就行

    // 填写服务器信息
    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());

    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 *)&peer, &len);
        if (m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }
    return 0;
}
cpp 复制代码
// UdpServer.cc
#include <iostream>
#include <memory>
#include "Dict.hpp"
#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;
    }
    // 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;
}
cpp 复制代码
// UdpServer.hpp 
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogModule;

using func_t = std::function<std::string(const std::string &, InetAddr &)>;

const int defaultfd = -1;

class UdpServer
{
public:
    UdpServer(uint16_t port, func_t func)
        : _sockfd(defaultfd),
          _port(port),
          _isrunning(false),
          _func(func)
    {
    }
    void Init()
    {
        // 1. 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket sucess, sockfd: " << _sockfd;

        // 2. 绑定socket信息, ip和端口
        // 2.1 绑定sockaddr_in结构体
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        // IP信息和端口号信息要发送到网络
        // 本地格式->网络序列
        local.sin_port = htons(_port);
        // IP, 1. IP转成4字节  2. 4字节转成网络序列 -> 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)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 1. 收消息
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                InetAddr client(peer);
                buffer[s] = 0;
                std::string result = _func(buffer, client);
                // LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port << "]# " << buffer;

                // 2. 发消息
                // std::string echo_string = "server echo@ ";
                // 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; // 用的是字符串风格, 点分十进制
    bool _isrunning;
    func_t _func; // 服务器的回调函数, 用来对数据进行处理
};

四、利用UDP实现一个简单聊天室

前面实现的字典服务器,基本上都是一个客户端发送请求,服务器处理之后再返回结果。

现在换一个需求:

多个客户端连接到同一个服务器,每个人发送的消息都可以转发给其他在线用户。

这就是一个简单的聊天室。

1. 如何记录在线用户?

UDP 通信中,我们可以通过客户端的 IP 地址和端口号来区分不同的用户。

所以可以定义一个 InetAddr 类,用来封装客户端的网络地址信息。

其中主要保存:

cpp 复制代码
std::string _ip;
uint16_t _port;
struct sockaddr_in _addr;

还可以提供一些接口:

cpp 复制代码
Ip();
Port();
PrintDebug();
GetAddr();

另外,可以重载 == 运算符:

cpp 复制代码
bool operator == (const InetAddr& addr)
{
    return this->_ip == addr._ip &&
           this->_port == addr._port;
}

这样就可以通过 IP 和端口判断两个地址是否属于同一个客户端。

接下来使用 vector 保存在线用户:

cpp 复制代码
std::vector<InetAddr> _online_user;

2. 实现用户管理

在聊天室中,需要处理三个基本操作:

  • 判断用户是否已经在线。
  • 添加新用户。
  • 删除退出的用户。

判断用户是否存在:

cpp 复制代码
bool IsExist(InetAddr& peer)
{
    for(auto& user : _online_user)
    {
        if(user == peer)
            return true;
    }

    return false;
}

添加用户:

cpp 复制代码
void AddUser(InetAddr& peer)
{
    _online_user.push_back(peer);
}

删除用户:

cpp 复制代码
void DeleteUser(InetAddr& peer)
{
    for(auto iter = _online_user.begin();
        iter != _online_user.end();
        iter++)
    {
        if(*iter == peer)
        {
            _online_user.erase(iter);
            break;
        }
    }
}

这里采用的方式是:客户端第一次发送消息,就认为这个用户已经上线。

也就是说,不需要额外设计一个复杂的登录流程。

3. 消息路由和广播

接下来是聊天室最重要的部分:消息转发。

当服务器收到某个客户端的消息时,首先判断这个客户端是否已经存在于在线用户列表中。

如果不存在,就把它添加进去。

然后给消息加上发送者的地址信息:

cpp 复制代码
std::string send_message =
    peer.StringAddr() + "# " + message;

例如:

text 复制代码
127.0.0.1:8080# 你好

这样其他用户就可以知道是谁发送的消息。

接下来遍历在线用户列表,把消息发送给每一个用户:

cpp 复制代码
for(auto& user : _online_user)
{
    sendto(sockfd,
           send_message.c_str(),
           send_message.size(),
           0,
           (const struct sockaddr*)&user.NetAddr(),
           sizeof(user.NetAddr()));
}

这样就实现了简单的消息广播。

4. 用户退出

如果客户端发送的消息是:

text 复制代码
QUIT

服务器就将这个用户从在线列表中删除:

cpp 复制代码
if(message == "QUIT")
{
    DeleteUser(peer);
}

这样聊天室就具备了最基本的用户上线、消息广播和退出功能。

五、客户端使用两个线程实现收发分离

聊天室还有一个问题。

如果客户端仍然采用之前的方式,先发送消息,再等待服务器返回,那么客户端在等待接收消息的时候,就无法方便地继续输入其他内容。

所以这里可以使用两个线程:

  • 一个线程专门负责发送消息。
  • 一个线程专门负责接收消息。

发送线程:

cpp 复制代码
void SenderRoutine(ThreadData& td)
{
    while(true)
    {
        std::string inbuffer;

        std::cout << "Please Enter# ";
        std::getline(std::cin, inbuffer);

        auto server = td._serveraddr.GetAddr();

        sendto(td._sockfd,
               inbuffer.c_str(),
               inbuffer.size(),
               0,
               (struct sockaddr*)&server,
               sizeof(server));
    }
}

接收线程:

cpp 复制代码
void RecverRoutine(ThreadData& td)
{
    char buffer[4096];

    while(true)
    {
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);

        ssize_t n = recvfrom(td._sockfd,
                             buffer,
                             sizeof(buffer) - 1,
                             0,
                             (struct sockaddr*)&temp,
                             &len);

        if(n > 0)
        {
            buffer[n] = '\0';
            std::cerr << buffer << std::endl;
        }
    }
}

在 main 函数中创建两个线程:

cpp 复制代码
Thread<ThreadData> recver("recver", RecverRoutine, td);
Thread<ThreadData> sender("sender", SenderRoutine, td);

recver.Start();
sender.Start();

recver.Join();
sender.Join();

这里两个线程使用的是同一个 socket 文件描述符。

UDP 套接字既可以发送数据,也可以接收数据,因此可以让一个线程负责发送,另一个线程负责接收。

这样客户端就可以一边接收其他用户的消息,一边继续发送自己的消息。

六、服务器引入线程池

前面聊天室的消息处理主要由服务器直接完成。

如果后续客户端数量增加,可以进一步引入线程池处理业务任务。

整体思路是:

  1. UDP 服务器负责接收客户端消息。
  2. 收到消息之后,构造一个任务。
  3. 将任务放入线程池。
  4. 线程池中的工作线程取出任务并执行。
  5. 通过路由对象处理消息。

例如:

cpp 复制代码
auto tp = ThreadPool<task_t>::GetInstance();

std::unique_ptr<Route> r =
    std::make_unique<Route>();

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.get(),
                sockfd,
                message,
                peer
            );

            tp->Enqueue(t);
        }
    );

这里使用 Lambda 表达式捕获路由对象和线程池对象,再通过 std::bind 将消息路由函数绑定成一个任务,最后放入线程池。

这样就把网络通信和具体的消息处理进一步分开了。

七、简单聊天室代码

cpp 复制代码
// UdpClient.cc
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Thread.hpp"

int sockfd = 0;
std::string server_ip;
uint16_t server_port = 0;
pthread_t id;

using namespace ThreadModule;

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;
        }
    }
}
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 = "online";
    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;
        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;
}
cpp 复制代码
// UdpServer.cc
#include <iostream>
#include <memory>
#include "Route.hpp"
#include "UdpServer.hpp"
#include "ThreadPool.hpp"

using namespace ThreadPoolModule;

std::string defaulthandler(const std::string &message)
{
    std::string hello = "hello, ";
    hello += message;
    return hello;
}

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](int sockfd, const std::string &message, InetAddr &peer)
    //                                                               { r.MessageRoute(sockfd, message, peer); });
    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;
}
cpp 复制代码
// UdpServer.hpp
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogModule;

using func_t = std::function<void(int sockfd, const std::string &, InetAddr &)>;

const int defaultfd = -1;

class UdpServer
{
public:
    UdpServer(uint16_t port, func_t func)
        : _sockfd(defaultfd),
          _port(port),
          _isrunning(false),
          _func(func)
    {
    }
    void Init()
    {
        // 1. 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket sucess, sockfd: " << _sockfd;

        // 2. 绑定socket信息, ip和端口
        // 2.1 绑定sockaddr_in结构体
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        // IP信息和端口号信息要发送到网络
        // 本地格式->网络序列
        local.sin_port = htons(_port);
        // IP, 1. IP转成4字节  2. 4字节转成网络序列 -> 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)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 1. 收消息
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                InetAddr client(peer);
                buffer[s] = 0;
                _func(_sockfd, buffer, client);
                // LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port << "]# " << buffer;
            }
        }
    }
    ~UdpServer()
    {
    }

private:
    int _sockfd;
    uint16_t _port;
    // std::string _ip; // 用的是字符串风格, 点分十进制
    bool _isrunning;
    func_t _func; // 服务器的回调函数, 用来对数据进行处理
};
cpp 复制代码
// Route.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "Mutex.hpp"

using namespace LogModule;
using namespace MutexModule;

class Route
{
private:
    bool IsExist(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 MessageRoute(int sockfd, const std::string &message, InetAddr &peer)
    {
        LockGuard lockguard(_mutex);
        if (!IsExist(peer))
        {
            AddUser(peer);
        }
        std::string send_message = peer.StringAddr() + "# " + message;
        for (auto &user : _online_user)
        {
            sendto(sockfd, send_message.c_str(), send_message.size(), 0, (const struct sockaddr *)&user.NetAddr(), sizeof(user.NetAddr()));
        }
        if (message == "QUIT")
        {
            LOG(LogLevel::INFO) << "删除一个在线用户: " << peer.StringAddr();
            DeleteUser(peer);
        }
    }
    ~Route()
    {
    }

private:
    // 首次给我发消息, 等同于登录
    std::vector<InetAddr> _online_user; // 在线用户
    Mutex _mutex;
};

八、总结

这一部分是在之前 UDP 基础通信的基础上继续扩展。

从最开始的 Echo 服务器,到后面的网络字典,再到聊天室,可以看到 UDP 网络编程逐渐从简单的数据收发,发展到了具体的业务处理。

这里有几个比较重要的知识点:

  1. 使用 unordered_map 保存字典数据,通过文件读取完成初始化。
  2. 使用 std::function 封装回调函数,让通信逻辑和业务逻辑分离。
  3. 使用 vector 保存在线用户,通过 IP 和端口识别客户端。
  4. 使用消息路由实现聊天室的广播功能。
  5. 使用两个线程分别处理客户端的发送和接收。
  6. 使用线程池处理服务器端的业务任务。

我觉得这一部分最值得学习的地方是代码结构上的变化。

一开始所有操作都写在服务器里面,后面逐渐将通信、业务处理和消息路由拆分开来。这样不仅代码更容易维护,后续想增加新的功能也会方便很多。


相关推荐
Rabitebla33 分钟前
【Linux学习笔记】进程篇(九):进程的创建与退出——fork、写时拷贝与退出码
linux·c++·笔记·学习
楚山之石1 小时前
【MRM】CAPLEX 对比剂灌注 MRI 开源术语库:ISMRM OSIPI 共识报告
图像处理·人工智能·学习·mri
李松桃1 小时前
基于IP的服务配置
linux·nginx-核心模块配置
FungLeo1 小时前
成为全栈·基础补充·Linux 服务器入门
linux·运维·服务器·ssh·部署·成为全栈
时间的拾荒人1 小时前
CMake 实战指南:从基础语法到静态库/动态库制作与链接
开发语言·c++·面试
智者知已应修善业1 小时前
【51单片机实现从下到上,从上到下,从中间到两边,两边到中间】2024-10-23
c语言·经验分享·笔记·单片机·嵌入式硬件·51单片机
道尔柯南1 小时前
C++ STL 入门详解:从认识标准模板库到掌握学习方法
java·c++·学习方法
爱和冰阔落1 小时前
【Linux网络】TCP 服务器怎么写?从 listen、accept 到 read/write,手写完整 Echo Server
linux·运维·服务器·网络