【Linux网络】构建UDP网络服务:从Echo到聊天室的线程池架构演进

文章目录

  • 前言
  • [一、V1版本 - Echo_server](#一、V1版本 - Echo_server)
    • [1. 写代码前的预备知识](#1. 写代码前的预备知识)
    • [2. V1版本 - Echo server 的最初版本(服务端 绑定指定的 ip)](#2. V1版本 - Echo server 的最初版本(服务端 绑定指定的 ip))
    • [3. 查看系统中的udp](#3. 查看系统中的udp)
    • [4. 特殊的ip地址:127.0.0.1 和 0.0.0.0](#4. 特殊的ip地址:127.0.0.1 和 0.0.0.0)
    • [5. V1版本 - Echo server 的最终版本(服务端 绑定ip:0.0.0.0)](#5. V1版本 - Echo server 的最终版本(服务端 绑定ip:0.0.0.0))
    • [6. UDP通信的核心流程 总结](#6. UDP通信的核心流程 总结)
  • [二、V2版本 - Dict_server(加入业务处理模块)](#二、V2版本 - Dict_server(加入业务处理模块))
    • [1. 业务处理模块(业务:简单的英译汉的网络字典)](#1. 业务处理模块(业务:简单的英译汉的网络字典))
    • [2. 在服务端接入 业务处理模块](#2. 在服务端接入 业务处理模块)
  • [三、V3版本 - Chat_server(加入线程池模块)](#三、V3版本 - Chat_server(加入线程池模块))
    • [1. 全流程图解(含gitee代码链接)](#1. 全流程图解(含gitee代码链接))
    • [2. 业务处理模块(业务:简单的聊天室)](#2. 业务处理模块(业务:简单的聊天室))
    • [3. 线程池模块](#3. 线程池模块)
    • [4. 在服务端 加入线程池模块(将业务处理模块 作为任务放入线程池的任务队列中)](#4. 在服务端 加入线程池模块(将业务处理模块 作为任务放入线程池的任务队列中))
    • [5. 对客户端进行修改(修改后进行代码测试)](#5. 对客户端进行修改(修改后进行代码测试))

前言

使用Socket编程UDP通信的相关函数,要包含以下头文件:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>


一、V1版本 - Echo_server

1. 写代码前的预备知识

  • socket函数
c 复制代码
#include <sys/socket.h>

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);

返回值:
调用成功,返回一个为socket创建的文件描述符;调用失败,返回 -1,并设置错误码。


  • bind函数
c 复制代码
#include <sys/socket.h>

// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// const struct sockaddr *address: 指向的内容 保存了主机的ip和端口号,将socket文件 与 ip+port进行绑定
// socklen_t address_len: 传入的结构体 的实际大小

返回值:
调用成功,返回0;调用失败,返回 -1,并设置错误码

IPv4 和 IPv6的地址格式定义在 netinet/in.h 中,IPv4地址用sockaddr_in结构体表示
IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6

注: 用户(应用层)使用的 ip地址是以"点分十进制"的字符串格式保存的(例如:192.168.1.1 或 127.0.0.1)

  1. C语言提供了专门的函数 inet_addr,该函数能将 "点分十进制"的字符串格式的ip地址 转换为 32位网络序列ip
    函数原型:uint32_t inet_addr( const char * cp );
  2. 当然 也提供了函数 inet_ntoa,该函数能将 网络序列ip 转换为 "点分十进制"的字符串格式的ip地址
    函数原型:char * inet_ntoa( struct in_addr sin );

  • recvfrom函数 和 sendto函数

// 注: udp通信是面向数据报的,数据报数据的发送 和 接收需要专门的收发函数,比如:接收数据报使用 recvfrom函数,发送数据报使用 sendto函数

返回值:
成功时,返回 实际接收的字节数。失败时,返回 -1,并设置错误码

返回值:
成功时,返回 实际发送的字节数。失败时,返回 -1,并设置错误码

2. V1版本 - Echo server 的最初版本(服务端 绑定指定的 ip)

  1. InetAddr.hpp

每个InetAddr类对象 管理 一对ip+port (将网络序列 转 主机序列)

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// 每个InetAddr类对象 管理 一对ip+port (将网络序列 转 主机序列)
class InetAddr
{
public:
    InetAddr(struct sockaddr_in &addr) : _addr(addr)
    {
        _port = ntohs(_addr.sin_port);
        _ip = inet_ntoa(_addr.sin_addr);
        // char* inet_ntoa( struct in_addr sin );
        // 该函数能将 网络序列ip 转换为 "点分十进制"的字符串格式的ip地址
    }

    std::string Ip() { return _ip; }

    uint16_t Port() { return _port; }

    std::string PrintDebug()
    {
        std::string info = _ip;
        info += ":";
        info += std::to_string(_port); // 例如:"127.0.0.1:4444"
        return info;
    }

    ~InetAddr() {}

private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};
  1. Server_udp.hpp
cpp 复制代码
#pragma once
#include "Log.hpp"
#include "InetAddr.hpp"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <iostream>

class Server_udp
{
public:
    Server_udp(u_short port, std::string ip)
        : _port(port),
          _ip(ip)
    {
    }

    void Init()
    {
        // 1. 创建Udp_Socket文件
        _socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socket_fd < 0)
        {
            LOG(FATAL) << "socket err: " << strerror(errno);
            exit(1);
        }
        LOG(INFO) << "socket success, _socket_fd: " << _socket_fd;

        // 2. 将socket文件绑定 ip+port
        // 服务器端必须绑定一个固定的IP和端口,因为它是被动等待连接的一方
        struct sockaddr_in sock_in;
        sock_in.sin_family = AF_INET;
        sock_in.sin_port = htons(_port);
        sock_in.sin_addr.s_addr = inet_addr(_ip.c_str());

        int n = bind(_socket_fd, (sockaddr *)&sock_in, sizeof(sock_in));
        if (n != 0)
        {
            LOG(FATAL) << "bind err: " << strerror(errno);
            exit(1);
        }
        LOG(INFO) << "bind success";
    }

    void Start()
    {
        char buffer[1024];
        // 服务器一旦开始运行,永不退出(除非特殊情况)
        while (1)
        {
            buffer[0] = 0;
            // 3. 接收数据
            struct sockaddr_in peer; 
            socklen_t len = sizeof(peer);
            int n = recvfrom(_socket_fd, buffer, sizeof(buffer) - 1,
                             0, (sockaddr *)&peer, &len);
            if (n > 0)
            {   
                // 成功接收到数据,打印该数据
                buffer[n] = 0;
                InetAddr addr(peer); // 用InetAddr类对象 管理 客户端的ip+port
                std::cout << addr.PrintDebug() << "# " << buffer << std::endl;

                // 收到消息后,向消息发送方 回复消息
                // 4. 发送数据
                std::string str("client# ");
                str += buffer;
                sendto(_socket_fd, str.c_str(), str.size(), 0, (sockaddr *)&peer, len);
            }
        }
    }

    ~Server_udp()
    {
    }

private:
    int _socket_fd;
    u_short _port;
    std::string _ip;
};
  1. Server_udp.cpp
bash 复制代码
 g++  Server_udp.cpp  -o  Server_udp
cpp 复制代码
#include "Server_udp.hpp"
#include <memory>

int main(int argc, char *argv[])
{
    ENABLE_CONSOLE_LOG_STRATEGY(); // 开启日志类
    if (argc != 3)
    {
        std::cout << "请输入:" << argv[0] << " ip port" << std::endl;
        exit(1);
    }
    std::string ip(argv[1]);
    u_short port = atoi(argv[2]);

    std::unique_ptr<Server_udp> baz1 = std::make_unique<Server_udp>(port, ip);
    baz1->Init();
    baz1->Start();

    return 0;
}
  1. Client_udp.cpp
bash 复制代码
g++ Client_udp.cpp -o Client_udp
cpp 复制代码
#include <stdlib.h>
#include <iostream>
#include <string>
#include <memory>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "请输入:" << argv[0] << " ip port" << std::endl;
        exit(1);
    }
    std::string ip(argv[1]);
    u_short port = atoi(argv[2]);

    // 1. 创建Udp_Socket文件
    int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
    // 客户端不需要显示绑定自己的 ip+port

    // 向服务器发送信息前,设置对应服务器 的ip+port
    // 服务器就是用来给客户端访问的(服务器的ip+port是公开的)
    struct sockaddr_in tar_server;
    tar_server.sin_family = AF_INET;
    tar_server.sin_port = htons(port);
    tar_server.sin_addr.s_addr = inet_addr(ip.c_str());

    socklen_t len = sizeof(tar_server);

    while(1)
    {
        // 2. 发送数据(第一次发数据时,系统会自动为客户端socket 绑定主机ip 和 合适的port)
        std::string str;
        std::cin >> str;
        sendto(socket_fd, str.c_str(), str.size(), 0, (sockaddr *)&tar_server, len);

        char buffer[1024] = {0};
        struct sockaddr_in peer;
        socklen_t lenth = sizeof(peer);
        // 3. 接收数据
        recvfrom(socket_fd, buffer, sizeof(buffer), 0, (sockaddr *)&peer, &lenth);
        std::cout << buffer << std::endl;
    }

    return 0;
}

在UDP/TCP通信中:服务器端必须绑定一个固定的IP和端口,因为它是被动等待连接的一方。 客户端是主动发起连接的一方,不需要显式绑定IP和端口,因为操作系统会自动分配一个临时端口。如果客户端也绑定固定端口,可能会导致端口冲突(特别是当多个客户端同时运行时)。

  1. 在UDP通信中,当客户端第一次调用sendto() 发送数据包时,系统自动为socket绑定ip+空闲端口
  2. 在TCP通信中,当客户端第一次调用connect() 时,系统自动为socket绑定ip+空闲端口

3. 查看系统中的udp

netstat -uanp查看系统中的udp信息

  1. -u:查看udp信息
  2. -a:展示所有
  3. -n:把能显示成数字的信息 转换成 数字
  4. -p:增加进程信息
  • 服务端的socket绑定 ip+port
  • 客户端在第一次调用sendto()向服务端发送消息时,系统才自动为客户端socket 绑定 主机ip + 空闲的port

4. 特殊的ip地址:127.0.0.1 和 0.0.0.0



  • 主机A上的 进程服务端绑定 127.0.0.1的 ip,这个ip被叫做 本地环回ip(只能用于 本地调试,禁止外部访问)

假设客户端要 访问127.0.0.1的ip,客户端发出的数据包 送到网络层时,网络层检测到要访问的 ip是本地环回ip,该数据包就不会被向下发送到数据链路层,而是直接往回发了


  • 主机A上的 进程服务端绑定 0.0.0.0的 ip,代表:该服务端的 所有网络接口(公网+内网),对外开放服务

主机A上的 客户端可以通过 127.0.0.1(本地环回ip)、172.31.13.207(私有ip)和 113.44.10.137(弹性公网ip)访问到该服务端。

主机B上的客户端可以通过 113.44.10.137(弹性公网ip)访问到该服务端。

  • 如果服务端绑定 172.31.13.207(私有ip),那么 仅在云服务器所属的内部网络 的客户端才能通过 172.31.13.207(私有ip)访问到该服务端(非本地客户端无法直接通过私有IP访问云服务器上的服务端);
  • 如果服务端绑定 113.44.10.137(弹性公网ip),那么 客户端只能通过 113.44.10.137(弹性公网ip)才能访问到该服务端。

5. V1版本 - Echo server 的最终版本(服务端 绑定ip:0.0.0.0)

只需要对服务端代码进行修改,服务端不需要用户输入 ip进行绑定,而是绑定固定的ip:0.0.0.0

  1. Server_udp.hpp
cpp 复制代码
#pragma once
#include "Log.hpp"
#include "InetAddr.hpp"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <iostream>

class Server_udp
{
public:
    /* 原代码:
    Server_udp(u_short port, std::string ip)
        : _port(port),
          _ip(ip)
    {
    }
    */
    Server_udp(u_short port)
        : _port(port)
    {
    }

    void Init()
    {
        // 1. 创建Udp_Socket文件
        _socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socket_fd < 0)
        {
            LOG(FATAL) << "socket err: " << strerror(errno);
            exit(1);
        }
        LOG(INFO) << "socket success, _socket_fd: " << _socket_fd;

        // 2. 将socket文件绑定 ip+port
        // 服务器端必须绑定一个固定的IP和端口,因为它是被动等待连接的一方
        struct sockaddr_in sock_in;
        sock_in.sin_family = AF_INET;
        sock_in.sin_port = htons(_port);
        /* 原代码:
        sock_in.sin_addr.s_addr = inet_addr(_ip.c_str());
        */
        sock_in.sin_addr.s_addr = INADDR_ANY;
        // INADDR_ANY: ((uint32_t) 0x00000000)
        // 不需要转换字节序,32位全0的数据 不受大小端存储的影响

        int n = bind(_socket_fd, (sockaddr *)&sock_in, sizeof(sock_in));
        if (n != 0)
        {
            LOG(FATAL) << "bind err: " << strerror(errno);
            exit(1);
        }
        LOG(INFO) << "bind success";
    }

    void Start()
    {
        char buffer[1024];
        // 服务器一旦开始运行,永不退出(除非特殊情况)
        while (1)
        {
            buffer[0] = 0;
            // 3. 接收数据
            struct sockaddr_in peer; 
            socklen_t len = sizeof(peer);
            int n = recvfrom(_socket_fd, buffer, sizeof(buffer) - 1,
                             0, (sockaddr *)&peer, &len);
            if (n > 0)
            {   
                // 成功接收到数据,打印该数据
                buffer[n] = 0;
                InetAddr addr(peer); // 用InetAddr类对象 管理 客户端的ip+port
                std::cout << addr.PrintDebug() << "# " << buffer << std::endl;

                // 收到消息后,向消息发送方 回复消息
                // 4. 发送数据
                std::string str("client# ");
                str += buffer;
                sendto(_socket_fd, str.c_str(), str.size(), 0, (sockaddr *)&peer, len);
            }
        }
    }

    ~Server_udp()
    {
    }

private:
    int _socket_fd;
    u_short _port;
    /* 删除:
    std::string _ip;
    */
};
  1. Server_udp.cpp
bash 复制代码
 g++  Server_udp.cpp  -o  Server_udp
cpp 复制代码
#include "Server_udp.hpp"
#include <memory>

int main(int argc, char *argv[])
{
    ENABLE_CONSOLE_LOG_STRATEGY(); // 开启日志类
    /* 原代码: 
    if (argc != 3)
    {
        std::cout << "请输入:" << argv[0] << " ip port" << std::endl;
        exit(1);
    }
    */
    if (argc != 2)
    {
        std::cout << "请输入:" << argv[0] << " port" << std::endl;
        exit(1);
    }
    // std::string ip(argv[1]);
    // u_short port = atoi(argv[2]);
    u_short port = atoi(argv[1]);

    // std::unique_ptr<Server_udp> baz1 = std::make_unique<Server_udp>(port, ip);
    std::unique_ptr<Server_udp> baz1 = std::make_unique<Server_udp>(port);
    baz1->Init();
    baz1->Start();

    return 0;
}

6. UDP通信的核心流程 总结

(1) UDP通信的核心流程

  • 服务端准备

服务端创建UDP Socket(socket()) → 绑定(bind())到 IP+端口(例如弹性公网IP 113.44.10.137:8080)。

关键点:绑定IP必须是服务端所在主机的有效IP(公网IP或私有IP均可,但影响访问范围)。

  • 客户端发送

客户端创建UDP Socket → 调用 sendto() 向目标地址(113.44.10.137:8080)发送数据。
无需显式绑定: 客户端通常由操作系统自动分配临时端口(IP:随机端口)。

网络传输路径

  • 步骤1:主机定位
    数据包通过公网路由,根据目标IP(113.44.10.137)找到云服务器所在的物理主机。
  • 步骤2:端口分发
    数据包到达主机后,操作系统根据目标端口(8080)将数据交给绑定该端口的服务端进程。

(2) 关于"绑定私有IP"的误区修正

若服务端绑定私有IP(如172.31.13.207:8080):

外部客户端向公网IP 113.44.10.137:8080 发送的数据包能到达云服务器主机(因为公网IP是主机的入口)。

但是! 操作系统发现该数据包的目标端口8080绑定的是私有IP而非公网IP,会丢弃此包(因IP不匹配)。
结果:外部客户端收不到响应,表现为"超时"或"连接被拒绝"。

(3) 解决方案:服务端需正确绑定

  • 最佳实践:绑定0.0.0.0
c 复制代码
// C语言示例(UDP服务端)
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
server_addr.sin_port = htons(8080);
bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));

作用: 服务端监听所有网络接口(包括公网IP、私有IP、本地环回IP)。
效果: 无论客户端通过公网IP还是私有IP访问,数据包均能被接收。

(4) 为什么客户端无需绑定固定IP+端口?

UDP是无连接的,客户端只需指定目标地址(服务端的IP+端口)。
操作系统自动为客户端Socket分配源IP+临时端口,并在收到服务端响应时匹配到该Socket。

二、V2版本 - Dict_server(加入业务处理模块)

1. 业务处理模块(业务:简单的英译汉的网络字典)

  • dictionary.txt
c 复制代码
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
  • Dictionary.hpp
cpp 复制代码
#pragma once
#include <fstream>
#include <map>
#include <string>
#include <iostream>
#include "Log.hpp"

class Dictionary
{
public:
    // 构造函数: 读取字典文件
    Dictionary(std::string dict_path = "./dictionary.txt")
        : _dict_path(dict_path)
    {
        // 1. 读取字典文件
        std::ifstream ifs(_dict_path);
        if (!ifs)
        {
            LOG(FATAL) << "ifstream err, 打开字典文件失败, _dict_path: " << _dict_path;
            exit(1);
        }
        LOG(INFO) << "ifstream success, _dict_path: " << _dict_path;

        // 按行读取
        std::string line;
        while (std::getline(ifs, line)) {
            // 处理每一行
            size_t pos = line.find(": ");
            if (pos == std::string::npos)
            {
                continue;
            }
            std::string word = line.substr(0, pos);
            std::string meaning = line.substr(pos + 2);
            _dict[word] = meaning;
        }

        // 2. 关闭文件流
        ifs.close();
    }

    // 翻译函数: 根据单词返回其翻译结果
    std::string Translate(const std::string &key)
    {
        auto iter = _dict.find(key);
        if(iter == _dict.end()) 
            return std::string("Unknown");
        else 
            return iter->second;
    }

private:
    std::string _dict_path;
    std::map<std::string, std::string> _dict;
};

2. 在服务端接入 业务处理模块

  • Server_udp.hpp (在Server_udp类的Start方法中 调用业务处理模块的函数)
cpp 复制代码
#pragma once
#include "Log.hpp"
#include "InetAddr.hpp"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <iostream>
#include <functional>

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

class Server_udp
{
public:
    Server_udp(u_short port, func_t& func)
        : _port(port), _func(func)
    {
    }

    void Init()
    {
        // 1. 创建Udp_Socket文件
        _socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socket_fd < 0)
        {
            LOG(FATAL) << "socket err: " << strerror(errno);
            exit(1);
        }
        LOG(INFO) << "socket success, _socket_fd: " << _socket_fd;

        // 2. 将socket文件绑定 ip+port
        struct sockaddr_in sock_in;
        sock_in.sin_family = AF_INET;
        sock_in.sin_port = htons(_port);
        sock_in.sin_addr.s_addr = INADDR_ANY;  // 任意IP地址: 0.0.0.0
        // sock_in.sin_addr.s_addr = inet_addr(_ip.c_str());

        int n = bind(_socket_fd, (sockaddr *)&sock_in, sizeof(sock_in));
        if (n != 0)
        {
            LOG(FATAL) << "bind err: " << strerror(errno);
            exit(1);
        }
        LOG(INFO) << "bind success";
    }

    void Start()
    {
        char buffer[1024];
        // 服务器一旦开始运行,永不退出(除非特殊情况)
        while (1)
        {
            buffer[0] = 0;
            // 接收数据
            struct sockaddr_in peer; 
            socklen_t len = sizeof(peer);
            int n = recvfrom(_socket_fd, buffer, sizeof(buffer) - 1,
                             0, (sockaddr *)&peer, &len);
            if (n > 0)
            {   
                // 成功接收到数据,打印该数据
                buffer[n] = 0;
                InetAddr addr(peer);
                std::cout << addr.PrintDebug() << "# " << buffer << std::endl;

                // 调用翻译函数
                // 翻译函数返回翻译结果
                std::string resp = _func(buffer); // 调用业务处理模块的函数

                std::string str("client# ");
                str += resp;
                // 向客户端回复翻译结果
                sendto(_socket_fd, str.c_str(), str.size(), 0, (sockaddr *)&peer, len);
            }
        }
    }

    ~Server_udp()
    {
    }

private:
    int _socket_fd;
    u_short _port;
    func_t _func;  // 用包装器封装 业务处理模块的函数
    // std::string _ip;
};
  • Server_udp.cpp (构造字典类对象,将字典类中的 业务处理方法 传入服务器类对象)
cpp 复制代码
#include "Server_udp.hpp"
#include <memory>
#include "Dictionary.hpp"

int main(int argc, char *argv[])
{
    ENABLE_CONSOLE_LOG_STRATEGY(); // 开启日志类

    if (argc != 2)
    {
        std::cout << "请输入:" << argv[0] << " port" << std::endl;
        exit(1);
    }

    u_short port = atoi(argv[1]);

    // 构造字典类对象
    Dictionary dict;
    func_t func = std::bind(&Dictionary::Translate, &dict, std::placeholders::_1);

    // 构造服务器类对象(绑定端口 和 翻译函数包装器)
    std::unique_ptr<Server_udp> baz1 = std::make_unique<Server_udp>(port, func);
    baz1->Init();
    baz1->Start();

    return 0;
}
  • 效果展示

三、V3版本 - Chat_server(加入线程池模块)

1. 全流程图解(含gitee代码链接)

gitee代码链接: V3版本 - Chat_server(加入线程池模块)

  • 全流程图解

2. 业务处理模块(业务:简单的聊天室)

  • InetAddr.hpp (每个InetAddr类对象 管理 一对ip+port,用一对ip+port 指代一名用户)
cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// 每个InetAddr类对象 管理 一对ip+port (将网络序列 转 主机序列)
class InetAddr
{
public:
    InetAddr(struct sockaddr_in &addr) : _addr(addr)
    {
        _port = ntohs(_addr.sin_port);
        _ip = inet_ntoa(_addr.sin_addr);
    }

    std::string Ip() { return _ip; }

    uint16_t Port() { return _port; }

    std::string PrintDebug()
    {
        std::string info = _ip;
        info += ":";
        info += std::to_string(_port); // "127.0.0.1:4444"
        return info;
    }

    struct sockaddr_in &GetAddr() 
    { 
        return _addr; 
    }

    bool operator==(const InetAddr &peer)
    {
        return (_ip == peer._ip) && (_port == peer._port);
    }

    ~InetAddr() {}

private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};
  • Talk_manager.hpp (维护一个在线用户列表。功能:当一名用户发消息时,将消息转发各其它在线用户)
cpp 复制代码
#pragma once
#include <vector>
#include "Mutex.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

class Talk_manager
{
private:
    bool is_exist(InetAddr &peer)
    {
        for (auto &user : _online_user)
        {
            if (user == peer)
                return true;
        }
        return false;
    }

    void AddUser(InetAddr &peer)
    {
        _online_user.push_back(peer);
        LOG(LogLevel::INFO) << "添加⼀个在线用户: " << peer.PrintDebug() << " 成功";
    }

    void DeleteUser(InetAddr &peer)
    {
        for (auto iter = _online_user.begin(); iter != _online_user.end(); iter++)
        {
            if (*iter == peer)
            {
                LOG(LogLevel::INFO) << "删除⼀个在线用户:" << peer.PrintDebug() << " 成功";
                _online_user.erase(iter);
                break;
            }
        }
    }   
public:
    Talk_manager()
    {
    }

    // 消息路由
    void MessageRoute(int sockfd, const std::string &message, InetAddr &peer)
    {
        if (!is_exist(peer))  // 如果用户不存在,添加到在线用户列表
        {
            LockGuard lockguarg(_mutex);
            AddUser(peer);
        }

        std::string send_message = peer.PrintDebug() + "# " + message; // 127.0.0.1:8080# 你好

        // 发送消息给所有在线用户
        for (auto &user : _online_user)
        {
            if (user == peer)
                continue;
            sendto(sockfd, send_message.c_str(), send_message.size(), 0, (sockaddr *)&(user.GetAddr()), sizeof(user.GetAddr()));
        }

        // 这个用户⼀定已经在线了
        if (message == "QUIT")
        {
            LockGuard lockguarg(_mutex);
            DeleteUser(peer);
        }
    }

private:
    // 首次给我发消息,等同于登录
    std::vector<InetAddr> _online_user; // 在线用户列表 ,每个元素是一个InetAddr对象
    My_Mutex _mutex;  // 互斥锁,保护在线用户列表的访问,防止多线程并发修改
}; 

3. 线程池模块

  • Thread.hpp (线程模块)
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <functional>
#include "../Log.hpp"

using func = std::function<void(const std::string &)>;

class My_Thread
{
private:
    static void *thread_routine(void *th)
    {
        My_Thread *self = (My_Thread *)th;
        self->_start_routine(self->_thread_name);
        LOG(DEBUG) << self->_thread_name << "退出成功";
        return (void *)0;
    }

public:
    My_Thread(func routine, const std::string &thread_name)
        : _start_routine(routine),
          _thread_name(thread_name)
    {
        LOG(DEBUG) << "创建" << _thread_name << "成功";
    }

    void Start()
    {
        int n = pthread_create(&_id, nullptr, thread_routine, this);
        _run = true;
        LOG(DEBUG) << "启动" << _thread_name << "成功";
    }

    void Stop()
    {
        if (_run)
        {
            pthread_cancel(_id);
            LOG(DEBUG) << "取消" << _thread_name << "成功";
        }
    }

    bool Join()
    {
        if (_run)
        {
            int n = pthread_join(_id, nullptr); // 阻塞等待目标线程退出
            if (n == 0)
                LOG(DEBUG) << "回收" << _thread_name << "成功";

            _run = false;
            return true;
        }
        return false;
    }

private:
    func _start_routine;
    std::string _thread_name;
    pthread_t _id;
    bool _run = false;
};
  • Threadpool_Singleton.hpp (单例模式 的线程池模块)
cpp 复制代码
#pragma once

#include <vector>
#include <queue>
#include "../Mutex.hpp"
#include "../Cond.hpp"
#include "../Log.hpp"
#include "Thread.hpp"

using namespace std;
using placeholders::_1;

using Task = std::function<void(void)>;

class Thread_Pool
{
private:
    void Handler_Task(const string &thread_name)
    {
        while (true)
        {
            Task task;
            {
                // 多线程要串行从任务队列拿取任务
                LockGuard lockguarg(_mutex);
                while (_task_queue.empty() && _isrunning)
                {
                    _waitnum++;
                    _cond.Wait(_mutex.Get());
                    _waitnum--;
                }
                if (_task_queue.empty() && (!_isrunning))
                    break;
                task = _task_queue.front();
                _task_queue.pop();
                LOG(DEBUG) << thread_name << "获取任务成功";
            }
            // 多线程执行任务可以并发
            task();
        }
    }

    // 将构造函数设置为私有
    Thread_Pool(int thread_num)
        : _thread_num(thread_num)
    {
        for (int i = 0; i < _thread_num; i++)
        {
            string str = "thread_";
            str += to_string(i + 1);
            _threads.emplace_back(bind(&Thread_Pool::Handler_Task, this, _1), str);
        }
        LOG(DEBUG) << "创建 Thread_Pool 成功";
    }

    // 禁用拷贝构造 和 赋值重载
    Thread_Pool(const Thread_Pool &) = delete;
    Thread_Pool &operator=(const Thread_Pool &) = delete;

public:
    void threads_start()
    {
        _isrunning = true;
        for (int i = 0; i < _thread_num; i++)
        {
            _threads[i].Start();
        }
        LOG(DEBUG) << "启动 Thread_Pool 成功";
    }

    void threads_stop()
    {
        LockGuard lockguarg(_mutex);
        _isrunning = false;
        _cond.NotifyAll();
    }

    void threads_wait()
    {
        for (int i = 0; i < _thread_num; i++)
        {
            _threads[i].Join();
        }
        LOG(DEBUG) << "回收 Thread_Pool 成功";
    }

    void Enqueue(const Task &task)
    {
        if (!_isrunning)
            return;

        LockGuard lockguarg(_mutex);
        _task_queue.push(task);
        if (_waitnum > 0)
            _cond.Notify();
    }

    // 添加单例模式
    static Thread_Pool *GetInstance()
    {
        if (_instance == nullptr)
        {
            LockGuard lockguarg(_Singleton_mutex);
            if (_instance == nullptr)
            {
                _instance = new Thread_Pool(5);  // 线程池默认创建5个线程
            }
        }
        return _instance;
    }

    ~Thread_Pool()
    {
    }

private:
    vector<My_Thread> _threads;
    int _thread_num; // 线程个数

    queue<Task> _task_queue;
    My_Mutex _mutex;
    My_Cond _cond;
    int _waitnum; // 在条件变量_cond等待队列的线程个数

    bool _isrunning = false; // 线程池是否开启

    // 添加单例模式
    static Thread_Pool *_instance;
    static My_Mutex _Singleton_mutex;
};

Thread_Pool *Thread_Pool::_instance = nullptr;
My_Mutex Thread_Pool::_Singleton_mutex;

4. 在服务端 加入线程池模块(将业务处理模块 作为任务放入线程池的任务队列中)

  • Server_udp.hpp
cpp 复制代码
#pragma once
#include "Log.hpp"
#include "InetAddr.hpp"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <iostream>
#include <functional>

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

class Server_udp
{
public:
    Server_udp(u_short port, func_t func)
        : _port(port), _func(func)
    {
    }

    void Init()
    {
        // 1. 创建Udp_Socket文件
        _socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socket_fd < 0)
        {
            LOG(FATAL) << "socket err: " << strerror(errno);
            exit(1);
        }
        LOG(INFO) << "socket success, _socket_fd: " << _socket_fd;

        // 2. 将socket文件绑定 ip+port
        struct sockaddr_in sock_in;
        sock_in.sin_family = AF_INET;
        sock_in.sin_port = htons(_port);
        sock_in.sin_addr.s_addr = INADDR_ANY;  // 任意IP地址: 0.0.0.0
        // sock_in.sin_addr.s_addr = inet_addr(_ip.c_str());

        int n = bind(_socket_fd, (sockaddr *)&sock_in, sizeof(sock_in));
        if (n != 0)
        {
            LOG(FATAL) << "bind err: " << strerror(errno);
            exit(1);
        }
        LOG(INFO) << "bind success";
    }

    void Start()
    {
        char buffer[1024];
        // 服务器一旦开始运行,永不退出(除非特殊情况)
        while (1)
        {
            buffer[0] = 0;
            // 接收数据
            struct sockaddr_in peer; 
            socklen_t len = sizeof(peer);
            int n = recvfrom(_socket_fd, buffer, sizeof(buffer) - 1,
                             0, (sockaddr *)&peer, &len);
            if (n > 0)
            {   
                // 成功接收到数据,打印该数据
                buffer[n] = 0;
                InetAddr addr(peer);
                std::cout << addr.PrintDebug() << "# " << buffer << std::endl;

                // 调用聊天业务函数
                _func(_socket_fd, buffer, addr);
            }
        }
    }

    ~Server_udp()
    {
    }

private:
    int _socket_fd;  // 服务器socket文件描述符
    u_short _port;   // 服务器端口
    func_t _func;
};
  • Server_udp.cpp (将业务处理模块的Talk_manager::MessageRoute方法 作为任务放入线程池的任务队列中)
cpp 复制代码
#include "Server_udp.hpp"
#include "Talk_manager.hpp"
#include "./Thread_pool/Threadpool_Singleton.hpp"
#include <memory>

using placeholders::_1;
using placeholders::_2;
using placeholders::_3;

int main(int argc, char *argv[])
{
    ENABLE_CONSOLE_LOG_STRATEGY(); // 开启日志类

    if (argc != 2)
    {
        std::cout << "请输入:" << argv[0] << " port" << std::endl;
        exit(1);
    }

    u_short port = atoi(argv[1]);

    // 构造聊天管理类对象
    Talk_manager tm_manager;

    // 构造线程池对象
    std::shared_ptr<Thread_Pool> thread_pool(Thread_Pool::GetInstance());
    thread_pool->threads_start();

    // 构造服务器类对象(绑定端口 和 翻译函数包装器)
    std::unique_ptr<Server_udp> baz1 = std::make_unique<Server_udp>(port, [&tm_manager, &thread_pool](int sockfd, const std::string &message, InetAddr &peer)
    {
        // 将业务处理模块的Talk_manager::MessageRoute方法 作为任务放入线程池的任务队列中
        Task task = std::bind(&Talk_manager::MessageRoute, &tm_manager, sockfd, message, peer);
        thread_pool->Enqueue(task);
    });

    baz1->Init();
    baz1->Start();

    return 0;
}

5. 对客户端进行修改(修改后进行代码测试)

原来的客户端的发送 和 接收消息串行执行(要先发送一次数据,才能接收一次数据),不符合聊天业务的需求(比如:如果一个用户一直不发送信息,客户端就会一直阻塞等待发送数据的准备,客户端就无法接收其它客户端发送过来的数据)

  • Client_udp.cpp(原来的客户端代码)
cpp 复制代码
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "请输入:" << argv[0] << " ip port" << std::endl;
        exit(1);
    }
    std::string ip(argv[1]);
    u_short port = atoi(argv[2]);

    int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);

    struct sockaddr_in tar_server;
    tar_server.sin_family = AF_INET;
    tar_server.sin_port = htons(port);
    tar_server.sin_addr.s_addr = inet_addr(ip.c_str());

    socklen_t len = sizeof(tar_server);

    while(1)
    {
        std::cout << "请输入要发送的内容:";
        fflush(stdout);
        // 发送数据(第一次发数据时,系统会自动为socket 绑定主机ip 和 合适的port)
        std::string str;
        std::cin >> str;
        sendto(socket_fd, str.c_str(), str.size(), 0, (sockaddr *)&tar_server, len);

        char buffer[1024] = {0};
        struct sockaddr_in peer;
        socklen_t lenth = sizeof(peer);
        // 接收数据
        recvfrom(socket_fd, buffer, sizeof(buffer), 0, (sockaddr *)&peer, &lenth);
        std::cout << buffer << std::endl;
    }

    return 0;
}

使用多线程,将接收数据的操作放在一个新线程中,使接收数据的操作不会被 发送数据的操作阻塞!

  • Client_udp.cpp(修改后的客户端代码,将接收数据操作放在一个新线程中)
cpp 复制代码
#include <stdlib.h>
#include <iostream>
#include <string>
#include <memory>
#include <thread>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "请输入:" << argv[0] << " ip port" << std::endl;
        exit(1);
    }
    std::string ip(argv[1]);
    u_short port = atoi(argv[2]);

    // 1. 创建Udp_Socket文件
    int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
    // 客户端不需要显示绑定自己的 ip+port

    // 向服务器发送信息前,设置对应服务器 的ip+port
    // 服务器就是用来给客户端访问的(服务器的ip+port是公开的)
    struct sockaddr_in tar_server;
    tar_server.sin_family = AF_INET;
    tar_server.sin_port = htons(port);
    tar_server.sin_addr.s_addr = inet_addr(ip.c_str());

    socklen_t len = sizeof(tar_server);

    int flag = -1;
    while(1)
    {
        std::cout << "请输入要发送的内容:";
        fflush(stdout);
        // 发送数据(第一次发数据时,系统会自动为socket 绑定主机ip 和 合适的port)
        std::string str;
        std::cin >> str;
        sendto(socket_fd, str.c_str(), str.size(), 0, (sockaddr *)&tar_server, len);

        if(flag == -1) // 第一次发送数据后,创建新线程接收数据(后续发送数据时,不会创建线程接收数据)
        {
            flag = 0;
            // 将接收数据的操作放在一个新线程中,使接收数据的操作不会被 发送数据的操作阻塞
            std::thread t([socket_fd](){
                while(1)
                {
                    char buffer[1024] = {0};
                    struct sockaddr_in peer;
                    socklen_t lenth = sizeof(peer);
                    // 接收数据
                    recvfrom(socket_fd, buffer, sizeof(buffer), 0, (sockaddr *)&peer, &lenth);
                    std::cout << buffer << std::endl;
                }
            });
            t.detach();
        }
    }
    return 0;
}

每个用户先输入一次消息,这样所有用户都被添加为在线用户:

用户3每次输入消息,全部被转发给用户1 和 用户2,效果符合预期:


相关推荐
byte轻骑兵1 小时前
【LE Audio】CAP精讲[8]:CCID绑定术,打通音频流与控制的任督二脉
网络·人工智能·音视频·le audio·音视频控制
初願致夕霞1 小时前
Linux网络编程_数据链路层MAC帧协议与ARP协议
linux·网络·网络协议·macos
学会去珍惜1 小时前
C++如何与C语言混合编程_在C++项目中调用C库函数的extern “C“方法
c语言·c++·混合编程·extern
十五年专注C++开发1 小时前
QHttp: 一个开源的轻量级、异步、高性能 HTTP 库
c++·qt·网络协议·http·qhttp
IT瑞先生1 小时前
运维专题2——分区、挂载、扩容及问题排查
linux·运维
小小de风呀1 小时前
de风——【从零开始学C++】(八):string的模拟实现
开发语言·c++
程序猿编码1 小时前
藏在TCP握手里的暗号:一种基于序列号触发的加密回连后门
linux·网络·网络协议·tcp/ip
运维行者_1 小时前
ITOps自动化:全面解析
java·服务器·开发语言·网络·云计算
minji...1 小时前
Linux 网络基础之传输层协议TCP(八)拥塞控制,延迟应答,捎带应答,TCP粘包问题,异常退出问题
linux·服务器·网络·网络协议·tcp/ip·http·智能路由器