【Linux网络编程】UDP Socket

**前言:**最近在复习 Linux 网络编程,重点梳理了 UDP 协议的实现细节。虽然 UDP 是无连接、不可靠的协议,但其简单高效的特性在很多场景下(如实时音视频、DNS)依然是首选。从最简单的 Echo Server 出发,逐步重构为支持业务解耦的字典服务器,最后实现一个支持多线程的全双工聊天室,并探讨其中涉及的地址转换陷阱。

一、UDP 编程的核心套路

与 TCP 不同,UDP 不需要维护连接状态(没有三次握手),因此其系统调用更加精简。

核心 API 概览

  • socket() : 创建套接字,参数需指定 SOCK_DGRAM
  • bind() : 服务器必须显式绑定 IP 和 Port;客户端通常不需要显式绑定,由 OS 在首次发送数据时自动分配随机端口 。
  • recvfrom() : 核心读取函数。因为没有连接,必须通过参数(src_addr)获取发送方的地址信息(IP+Port),否则无法回信 。
  • sendto() : 核心发送函数。必须指定目标地址(dest_addr)。

为什么服务器推荐绑定 INADDR_ANY

在云服务器或多网卡机器上,直接绑定公网 IP 可能会失败(因为公网 IP 通常是在云厂商的 NAT 网关上)。 最佳实践是绑定 INADDR_ANY (0.0.0.0)。

含义:服务器愿意接收本机所有网卡(任意 IP)转发到该端口的数据 。

优势:通用性强,无需关心具体 IP 变动。

二、不同版本的UDP服务器

V1 版本:Echo Server(最简模型)

这是 UDP 的"Hello World"。逻辑非常简单:收到什么,就发回什么。

关键点 在于使用 recvfrom 获取客户端的 sockaddr,利用这个地址直接 sendto 返回数据。

不足在于网络逻辑和业务逻辑耦合在一起,无法扩展。

服务端由以下几个文件组成:

UdpServer.hpp:

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

static const int gdefaultsockfd = -1;
class UdpServer
{
public:
    UdpServer(uint16_t port) : _sockfd(gdefaultsockfd), _port(port), _isrunning(false)
    {
    }
    void Init()
    {
        // 1.创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); //  把网络在文件系统中展开
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(1);
        }
        LOG(LogLevel::INFO) << "create socket success: " << _sockfd;

        // 2.bind套接字
        // 2.1 填充IP和Port
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port); // 统一大端

        // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // inet_addr 1. 字符串转整数ip 2. 整数ip是网络序列的

        local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意IPbind  不明确具体IP,只要发给对应的主机,对应的port,全都能收到
        // 2.2 和socketfd进行bind
        int n = bind(_sockfd, (const sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind socket success : " << _sockfd;
    }
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 读取数据
            char buffer[1024];
            buffer[0] = 0; //  O(1) 清空数据
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len); // flag == 0 , 阻塞模式
            if (n > 0)
            {
                uint16_t clientport = ntohs(peer.sin_port);
                std::string clientip = inet_ntoa(peer.sin_addr);

                buffer[n] = 0;
                LOG(LogLevel::DEBUG) << "[" << clientip << " : " << clientport << "]#" << buffer;

                std::string echo_string = "server echo# ";
                echo_string += buffer;
                sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
            }
        }
        _isrunning = false;
    }
    void Stop()
    {
        _isrunning = false;
    }
    ~UdpServer()
    {
    }

private:
    int _sockfd;
    uint16_t _port;
    // std::string _ip; // 不建议绑定固定ip

    bool _isrunning;
};

UdpServer.cc:

cpp 复制代码
#include "UdpServer.hpp"
#include <memory>

void Usage(std::string proc)
{
    std::cerr << "Usage :" << proc << "loaclport" << std::endl;
}

// ./udp_server serverport
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t localport = std::stoi(argv[1]);
    EnableConsoleLogStrategy();
    std::unique_ptr<UdpServer> ptr = std::make_unique<UdpServer>(localport);
    ptr->Init();
    ptr->Start();

    return 0;
}

客户端文件为:

UdpClient.cc

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

void Usage(std::string proc)
{
    std::cerr << "Usage :" << proc << "serverip serverport" << std::endl;
}

// ./udp_client serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cout << "create socket error" << std::endl;
        return 0;
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    socklen_t len = sizeof(server);

    while (true)
    {
        std::cout << "Please Enter@ ";
        std::string line;
        std::getline(std::cin, line);
        sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, len);

        char buffer[1024];
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
        if(m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return 0;
}

V2 版本:Dict Server(业务解耦)

为了让服务器能处理不同业务(如英译汉、查数据),我们需要将"网络收发"与"业务处理"分离。

设计模式:策略模式 / 回调函数。

实现 :定义一个 callback_t 类型(利用 C++11 std::function):

cpp 复制代码
using callback_t = std::function<std::string(const std::string &word, const std::string &whoip, uint16_t whoport)>;

服务器类 UdpServer 持有一个 func_t 成员。在 Recv 之后,调用回调函数处理业务,获取结果后再 Send 。

服务端文件为:

DictServer.hpp:

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

using callback_t = std::function<std::string(const std::string &word, const std::string &whoip, uint16_t whoport)>;
static const int gdefaultsockfd = -1;
class DictServer
{
public:
    DictServer(uint16_t port, callback_t func)
        : _sockfd(gdefaultsockfd), _port(port), _cb(func), _isrunning(false)
    {
    }
    void Init()
    {
        // 1.创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); //  把网络在文件系统中展开
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(1);
        }
        LOG(LogLevel::INFO) << "create socket success: " << _sockfd;

        // 2.bind套接字
        // 2.1 填充IP和Port
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port); // 统一大端

        // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // inet_addr 1. 字符串转整数ip 2. 整数ip是网络序列的

        local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意IPbind  不明确具体IP,只要发给对应的主机,对应的port,全都能收到
        // 2.2 和socketfd进行bind
        int n = bind(_sockfd, (const sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind socket success : " << _sockfd;
    }
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 读取数据
            char buffer[1024];
            buffer[0] = 0; //  O(1) 清空数据
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len); // flag == 0 , 阻塞模式
            if (n > 0)
            {
                uint16_t clientport = ntohs(peer.sin_port);
                std::string clientip = inet_ntoa(peer.sin_addr);
                buffer[n] = 0;

                // buffer[n] = 0;
                // LOG(LogLevel::DEBUG) << "[" << clientip << " : " << clientport << "]#" << buffer;

                // std::string echo_string = "server echo# ";
                // echo_string += buffer;
                std::string word = buffer;
                LOG(LogLevel::DEBUG) << "用户查找: " << word;
                std::string result = _cb(word, clientip, clientport);
                sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);
            }
        }
        _isrunning = false;
    }
    void Stop()
    {
        _isrunning = false;
    }
    ~DictServer()
    {
    }

private:
    int _sockfd;
    uint16_t _port;
    // std::string _ip; // 不建议绑定固定ip
    callback_t _cb;
    bool _isrunning;
};

Dictionary.hpp:

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

static const std::string sep = ": ";
class Dictionary
{
private:
    void LoadConf()
    {
        std::ifstream in(_path);
        if (!in.is_open())
        {
            LOG(LogLevel::ERROR) << "open file error" << _path;
            return;
        }
        std::string line;
        while (std::getline(in, line))
        {
            LOG(LogLevel::DEBUG) << "load dict message: " << line;
            auto pos = line.find(sep);
            if (pos == std::string::npos)
            {
                LOG(LogLevel::WARNING) << "format error: " << line;
                continue;
            }
            std::string word = line.substr(0, pos);
            std::string value = line.substr(pos + sep.size());
            if (word.empty() || value.empty())
            {
                LOG(LogLevel::WARNING) << "format error, word or value is empty";
            }
            _dict.insert({word, value});
        }

        in.close();
    }

public:
    Dictionary(const std::string &path) : _path(path)
    {
        LOG(LogLevel::INFO) << "construct Dictionary obj";
        LoadConf();
    }
    std::string Translate(const std::string &word, const std::string &whoip, uint16_t whoport)
    {
        (void)whoip,(void)whoport;
        auto iter = _dict.find(word);
        if(iter == _dict.end())
        {
            return "unknown";
        }
        return iter->first + " -> " + iter->second;
    }
    ~Dictionary()
    {
    }

private:
    std::string _path;
    std::unordered_map<std::string, std::string> _dict;
};

DictServer.cc

cpp 复制代码
#include "DictServer.hpp"
#include "Dictionary.hpp"
#include <memory>

void Usage(std::string proc)
{
    std::cerr << "Usage :" << proc << " loaclport" << std::endl;
}

// ./udp_server serverport
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    EnableConsoleLogStrategy();

    Dictionary dict("./dict.txt");

    uint16_t localport = std::stoi(argv[1]);
    std::unique_ptr<DictServer> ptr = std::make_unique<DictServer>(localport,
            [&dict](const std::string &word, const std::string &whoip, uint16_t whoport) -> std::string
        {
            return dict.Translate(word, whoip, whoport);
        });
    ptr->Init();
    ptr->Start();

    return 0;
}

客户端文件为:

DictClient.cc:

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

void Usage(std::string proc)
{
    std::cerr << "Usage :" << proc << "serverip serverport" << std::endl;
}

// ./udp_client serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cout << "create socket error" << std::endl;
        return 0;
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    socklen_t len = sizeof(server);
    //  client 不需要显示bind自己的ip和端口
    //  client会在os的帮助下,随机bind端口号,防止端口冲突
    while (true)
    {
        std::cout << "Please Enter@ ";
        std::string line;
        std::getline(std::cin, line);
        sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, len);

        char buffer[1024];
        buffer[0] = 0;
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);

        if(m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }

    }

    return 0;
}

效果 :UdpServer 变成了通用的网络底层,具体的查字典逻辑(加载 dict.txtunordered_map)封装在外部模块中 。

V3 版本:多人聊天室(状态管理与多线程)

这是最复杂的场景,实现了类似群聊的功能。

服务端设计: 消息路由,服务端不再是简单的"请求-响应"模型,而是变成了消息分发中心

用户管理 :维护一个在线用户列表 std::vector<InetAddr>。

核心逻辑

  • 接收消息。
  • 检查发送者是否在用户列表中。如果不在,视为新用户上线,加入列表 。
  • 广播 :遍历用户列表,将消息 sendto 给除了发送者之外的所有人(或所有人)。
  • 处理指令(如 "QUIT"):从列表中移除用户 。

服务端由以下几个文件组成:

ChatServer.hpp:

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <functional>
#include "Logger.hpp"
#include "InetAddr.hpp"

using callback_t = std::function<void(int socket, const std::string message, InetAddr inetaddr)>; // 不要设为引用
static const int gdefaultsockfd = -1;
class ChatServer
{
public:
    ChatServer(uint16_t port, callback_t func)
        : _sockfd(gdefaultsockfd), _port(port), _cb(func), _isrunning(false) 
    {
    }
    void Init()
    {
        // 1.创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); //  把网络在文件系统中展开
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(1);
        }
        LOG(LogLevel::INFO) << "create socket success: " << _sockfd;

        InetAddr local(_port);

        int n = bind(_sockfd, local.Addr(), local.Length());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind socket success : " << _sockfd;
    }
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 读取数据
            char buffer[1024];
            buffer[0] = 0; //  O(1) 清空数据
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len); // flag == 0 , 阻塞模式
            if (n > 0)
            {
                buffer[n] = 0;
                InetAddr clinetaddr(peer);
                LOG(LogLevel::DEBUG) << "get a client info# " << clinetaddr.Ip() << " - " << clinetaddr.Port() << ": " << buffer;

                std::string message = buffer;
                // 回调
                _cb(_sockfd, message, clinetaddr);
            }
        }
        _isrunning = false;
    }
    void Stop()
    {
        _isrunning = false;
    }
    ~ChatServer()
    {
    }

private:
    int _sockfd;
    uint16_t _port;
    // std::string _ip; // 不建议绑定固定ip
    callback_t _cb;
    bool _isrunning;
};

Route.hpp:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"
class Route
{
private:
    bool IsExists(const InetAddr &addr)
    {
        for (auto &e : _online_user)
        {
            if (e == addr)
                return true;
        }
        return false;
    }
    void AddUser(const InetAddr &addr)
    {
        if (!IsExists(addr))
            _online_user.push_back(addr);
    }
    void SendMessageToAll(int sockfd, std::string &message, InetAddr &addr)
    {
        for (auto &e : _online_user)
        {
            LOG(LogLevel::DEBUG) << "route [" << message << "] to: " << e.ToString();
            std::string info = addr.ToString();
            info += "# ";
            info += message;
            sendto(sockfd, message.c_str(), message.size(), 0, e.Addr(), e.Length());
        }
    }
    void DeleteUser(const std::string &message, const InetAddr &addr)
    {
        if (message == "QUIT")
        {
            auto iter = _online_user.begin();
            for (; iter < _online_user.end(); iter++)
            {
                if (*iter == addr)
                {
                    _online_user.erase(iter);
                }
                break;
            }
        }
    }

public:
    Route()
    {
    }

    void RouteMessageToAll(int sockfd, std::string &message, InetAddr &addr)
    {
        AddUser(addr);
        SendMessageToAll(sockfd, message,addr);
        DeleteUser(message, addr);
    }
    ~Route()
    {
    }

private:
    // 临界资源
    // 1. 锁
    std::vector<InetAddr> _online_user;

    // 2. 锁 + 拷贝
    // std::vector<InetAddr> _send_lint;

};

ChatServer.cc:

cpp 复制代码
#include "ChatServer.hpp"
#include "ThreadPool.hpp"
#include "Route.hpp"
#include <memory>

void Usage(std::string proc)
{
    std::cerr << "Usage :" << proc << " loaclport" << std::endl;
}

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

// ./udp_server serverport
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    EnableConsoleLogStrategy();

    uint16_t localport = std::stoi(argv[1]);

    // 1.消息转发功能
    std::unique_ptr<Route> r = std::make_unique<Route>();

    // 2.线程池对象
    auto tp = ThreadPool<task_t>::GetInstance();

    // 3.服务器对象
    std::unique_ptr<ChatServer> ptr = std::make_unique<ChatServer>(localport,
                                                                   [&r, &tp](int sockfd, std::string message, InetAddr addr)
                                                                   {
                                                                       task_t task = std::bind(&Route::RouteMessageToAll, r.get(), sockfd, message, addr);
                                                                       tp->Enqueue(task);
                                                                   });
    ptr->Init();
    ptr->Start();

    return 0;
}

V3代码中为了使IP、端口号看起来更具体,定义了InetAddr类:

cpp 复制代码
#pragma once
// 描述client socket信息的类
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <functional>
#include <string>
#include "Logger.hpp"

class InetAddr
{
private:
    void Net2Host()
    {
        _port = ntohs(_addr.sin_port);
        _ip = inet_ntoa(_addr.sin_addr);
    }
    void Host2Net()
    {
        bzero(&_addr, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        _addr.sin_addr.s_addr = inet_addr(_ip.c_str());
    }

public:
    InetAddr(const struct sockaddr_in &client) : _addr(client)
    {
        Net2Host();
    }
    InetAddr(uint16_t port,const std::string &ip = "0.0.0.0") : _port(port), _ip(ip) 
    {
        Host2Net();
    }
    uint16_t Port()
    {
        return _port;
    }
    std::string Ip()
    {
        return _ip;
    }
    struct sockaddr *Addr()
    {
        return (sockaddr *)&_addr;
    }
    socklen_t Length()
    {
        socklen_t len = sizeof(_addr);
        return len;
    }

    std::string ToString()
    {
        return _ip + std::to_string(_port);
    }
    bool operator==(const InetAddr &addr)
    {
        return _ip == addr._ip && _port == addr._port;
    }
    ~InetAddr()
    {
    }

private:
    struct sockaddr_in _addr; // 网络风格地址

    std::string _ip; // 主机风格地址
    uint16_t _port;
};

以及单例线程池代码:

Thread.hpp:

cpp 复制代码
#ifndef __THREAD_HPP__
#define __THREAD_HPP__
#include <iostream>
#include <vector>
#include <pthread.h>
#include <unistd.h>
#include <functional>
#include <sys/syscall.h>
#include "Logger.hpp"

#define get_lwp_id() syscall(SYS_gettid);
using func_t = std::function<void(const std::string& name)>;
const std::string threadnamedefault = "None - name";
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
class Thread
{
private:
    /* data */
    pthread_t _tid;
    pid_t lwpid;
    std::string _name;
    func_t _func;
    bool _isrunning;

public:
    Thread(func_t func, const std::string &name = threadnamedefault) : _name(name), _func(func), _isrunning(false)
    {
        LOG(LogLevel::INFO) << _name << " create thread success";
    }
    ~Thread()
    {
        // LOG(LogLevel::INFO) << _name << "thread destory";
    }
    static void *start_routine(void *args) // 取消this指针
    {
        Thread *self = static_cast<Thread *>(args);
        self->_isrunning = true;
        self->lwpid = get_lwp_id();
        self->_func(self->_name);
        pthread_exit((void *)0);
    }
    void Start()
    {
        int n = pthread_create(&_tid, nullptr, start_routine, this);
        if (n == 0)
        {
            LOG(LogLevel::INFO) << _name <<  " run thread success";
        }
    }
    void Join() 
    {
        if (!_isrunning)
            return;
        int n = pthread_join(_tid, nullptr);
        if (n == 0)
        {
            LOG(LogLevel::INFO) << _name << " pthread join success";
        }
    }
    void Stop()
    {
        if(!_isrunning)
            return;
        _isrunning = false;
        int n = pthread_cancel(_tid);
    }
};

#endif

ThreadPool.hpp:

cpp 复制代码
#pragma once

#include <iostream>
#include <memory>
#include <queue>
#include <vector>

#include "Thread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"

const static int threadnum_default = 3; // for debug

// 单例线程池
template <typename T>
class ThreadPool
{
private:
    bool QueueIsEmpty()
    {
        return _q.empty();
    }
    void Routine(const std::string &name)
    {
        // LOG(LogLevel::INFO) << name << " hello world";
        while (true) // 死循环
        {
            // 把任务从线程获取到线程私有  临界区 -> 私有栈
            T t;
            {
                LockGuard lockguard(&_lock);
                while (QueueIsEmpty() && _isrunning)
                {
                    _thread_wait_num++;
                    _cond.Wait(_lock);
                    _thread_wait_num--;
                }
                // T t = _q.front();   // 处理任务不需要再临界区,只需把取任务保护好就行
                if (!_isrunning && QueueIsEmpty()) // 退出情况设计
                {
                    LOG(LogLevel::INFO) << "线程池退出 && 任务队列为空, " << name << "退出";
                    break;
                }

                t = _q.front();
                _q.pop();
                // t();
            }
            t();  // 处理任务
        }
    }
    ThreadPool(int threadnum = threadnum_default) : _threadnum(threadnum), _thread_wait_num(0), _isrunning(false) // 对象先创建(存在),再初始化
    {
        for (int i = 0; i < threadnum; i++)
        {
            // 方法1
            // auto f = std::bind(hello, this);

            // 方法2
            std::string name = "thread-" + std::to_string(i + 1);
            _threads.emplace_back(Thread([this](const std::string &name)
                                         { this->Routine(name); }, name));

            // Thread t([this]() {

            // }, name);
            // _threads.push_back(std::move(t));
        }
        LOG(LogLevel::INFO) << " threadpool cerate success";
    }

    // 复制拷⻉禁⽤
    ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;
    ThreadPool(const ThreadPool<T> &) = delete;

public:
    void Start()
    {
        if (_isrunning)
            return;
        _isrunning = true;
        for (auto &t : _threads)
        {
            t.Start();
        }
        LOG(LogLevel::INFO) << " threadpool start success";
    }

    // 退出逻辑设计
    // 1.如果被唤醒 && 队列没有任务 = 让线程退出
    // 2.如果被唤醒 && 队列有任务 = 线程不能立即退出 而应该让线程把任务处理完,再退出
    // 3.线程本身没有休眠 我们应该让他把能处理的任务全处理完成,再退出
    void Stop()
    {
        // 这种做法太简单粗暴
        // if (!_isrunning)
        //     return;
        // _isrunning = false;
        // for (auto &t : _threads)
        // {
        //     t.Stop();
        // }
        // LOG(LogLevel::INFO) << " threadpool stop success";
        if (!_isrunning)
            return;
        _isrunning = false;
        if (_thread_wait_num > 0)
            _cond.NotifyAll();
    }
    void Wait()
    {
        for (auto &t : _threads)
        {
            t.Join();
        }
        LOG(LogLevel::INFO) << " threadpool wait success";
    }
    void Enqueue(const T &in)
    {
        if (!_isrunning)
            return;
        {
            LockGuard lockguard(&_lock);
            _q.push(in);
            if (_thread_wait_num > 0)
                _cond.NotifyOne();
        }
    }
    //  获取单例   //  让用户以类的方式访问构造单例,不需要自己构造
    static ThreadPool<T> *GetInstance()
    {
        if(!_instance)
        {
            LockGuard LockGuard(&_singleton_lock);
            if (!_instance)
            {
                LOG(LogLevel::DEBUG) << "线程池首次被使用,创建并初始化";

                _instance = new ThreadPool<T>();
                _instance->Start();
            }
            // else
            // {
            //     LOG(LogLevel::DEBUG) << "线程池单例已经存在,直接获取";
            // }
        }
        return _instance;
    }
    ~ThreadPool()
    {
        LOG(LogLevel::INFO) << " threadpool destory success";
    }

private:
    // 任务队列
    std::queue<T> _q; //  整体使用的临界资源

    // 多个线程
    std::vector<Thread> _threads;
    int _threadnum;
    int _thread_wait_num;
    // 锁
    Mutex _lock;
    // 条件变量
    Cond _cond;

    bool _isrunning; // 防止线程池重复启动

    // 单例中静态指针   // 懒汉模式设计
    static ThreadPool<T> *_instance;
    static Mutex _singleton_lock;
};

template <class T>
ThreadPool<T> *ThreadPool<T>::_instance = nullptr;

template <class T>
Mutex ThreadPool<T>::_singleton_lock;

线程池中的Cond、Mutex都是对库函数的封装,不难理解。

而UDP 协议本身是全双工的(可以同时读写)。在聊天室场景下,客户端不能阻塞在 cin 等待输入,否则无法及时接收别人发来的消息。

于是采用多线程来解决该问题。

主线程 :负责从标准输入读取数据,调用 sendto。

接收线程 :负责死循环调用 recvfrom,将收到的消息打印到屏幕 。

于是客户端文件为:

ChatClient.cc:

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

void Usage(std::string proc)
{
    std::cerr << "Usage :" << proc << "serverip serverport" << std::endl;
}

int sockfd = -1;
uint16_t serverport;
std::string serverip;

void InitClient(const std::string &serverip, uint16_t serverport)
{
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cout << "create socket error" << std::endl;
        exit(2);
    }
}

void recver()
{
    while (true)
    {
        char buffer[1024];
        buffer[0] = 0;
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);

        if (m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }
}
                                            
void sender()
{
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    socklen_t len = sizeof(server);

    while (true)
    {
        std::cout << "Please Enter@ ";
        std::string line;
        std::getline(std::cin, line);

        sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, len);
    }
}

// ./udp_client serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }

    serverip = argv[1];
    serverport = std::stoi(argv[2]);
    InitClient(serverip, serverport);
    
    std::thread tsend(sender);
    std::thread trecv(recver);

    trecv.join();
    tsend.join();

    return 0;
}

上述的聊天逻辑使用图片来进行如下总结:

三、地址转换与线程安全

在网络编程中,我们经常需要将 sockaddr_in 中的 IP 地址(32位整数)转换为点分十进制字符串(如 "192.168.1.1")。

而使用in_addr转字符串函数 inet_ntoa 需要注意一些情况。

接口原型:

cpp 复制代码
char *inet_ntoa(struct in_addr in);

该函数会将转换结果存放在内部的一个静态缓冲区 (static buffer) 中,并返回该缓冲区的指针 。

会出现如下问题:

覆盖问题 :如果在一行代码中连续调用两次 inet_ntoa,第二次的结果会覆盖第一次。

cpp 复制代码
// 错误示例
printf("ptr1: %s, ptr2: %s\n", inet_ntoa(addr1), inet_ntoa(addr2));
// 输出结果可能是一样的,都是 addr2 的值

线程不安全:在多线程环境下,如果两个线程同时调用该函数,缓冲区内容会被竞争覆盖,导致乱码或逻辑错误 。

解决方案

推荐使用**inet_ntop** : 这是一个现代的、线程安全的函数。它要求调用者提供缓冲区,从而避免了内部静态缓冲区。

函数原型:

cpp 复制代码
const char *inet_ntop(int af, const void *restrict src,
                        char dst[restrict .size], socklen_t size);

使用示例:

cpp 复制代码
char ip_buf[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr.sin_addr, ip_buf, sizeof(ip_buf));

总结

关键知识点在于:

  • 客户端不需要显式 Bind,服务端建议 Bind INADDR_ANY
  • 网络层与业务层必须解耦(Function/Callback)。
  • 实现即时通讯类应用时,客户端必须通过多线程实现收发分离。
  • 严禁在多线程环境使用 inet_ntoa,请认准 inet_ntop
相关推荐
C_心欲无痕6 小时前
ts - tsconfig.json配置讲解
linux·前端·ubuntu·typescript·json
冰西瓜6006 小时前
国科大2025操作系统高级教程期末回忆版
linux
sunfove7 小时前
光网络的立交桥:光开关 (Optical Switch) 原理与主流技术解析
网络
HIT_Weston7 小时前
93、【Ubuntu】【Hugo】搭建私人博客:面包屑(一)
linux·运维·ubuntu
cuijiecheng20188 小时前
Linux下Beyond Compare过期
linux·运维·服务器
HIT_Weston8 小时前
92、【Ubuntu】【Hugo】搭建私人博客:侧边导航栏(六)
linux·运维·ubuntu
CodeAllen嵌入式8 小时前
Windows 11 本地安装 WSL 支持 Ubuntu 24.04 完整指南
linux·运维·ubuntu
码农小韩9 小时前
基于Linux的C++学习——指针
linux·开发语言·c++·学习·算法
Kevin Wang7279 小时前
欧拉系统服务部署注意事项
网络·windows
min1811234569 小时前
深度伪造内容的检测与溯源技术
大数据·网络·人工智能