【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
相关推荐
一条懒鱼6666 小时前
k8s-网络
网络·容器·kubernetes
是毛毛吧6 小时前
开发环境配置指南:解决 GitHub 连接超时与依赖下载失败的问题
网络·git·网络安全·docker·信息与通信
Ha_To6 小时前
25.12.15 eNSP rip命令,MAC地址绑定,vlan配置以及STP协议解释
网络·智能路由器
QT 小鲜肉6 小时前
【Linux命令大全】001.文件管理(理论篇)
linux·数据库·chrome·笔记
云飞云共享云桌面6 小时前
云飞云智能共享云桌面:企业PLM/ERP/MES等系统管理的革新方案
运维·服务器·网络·算法·性能优化
小疆智控7 小时前
Modbus转EtherCAT网关:真空浓缩设备的 “通讯加速器”
网络
JXNL@7 小时前
网通领域核心设备解析:CPE、IP Phone 与 AP 技术全指南
网络·网络协议·tcp/ip
VekiSon7 小时前
Linux系统编程——进程进阶:exec 族、system 与工作路径操作
linux·运维·服务器
博语小屋7 小时前
Socket UDP 网络编程V2 版本- 简单聊天室
linux·网络·c++·网络协议·udp