Linux网络学习【UDP Socket编程实战:网络命令与客户端访问Linux验证】


🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统与网络知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》 《MySQL数据库学习》 《Python知识内容》 《cpolar知识学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

在完成 Linux 网络基础知识的学习之后,接下来就需要真正进入网络编程实战 .相比只理解 IP、端口、协议和网络分层,自己动手编写一个能够收发数据的网络程序,才能更直观地理解数据究竟是如何从客户端发送到Linux主机,并由服务端接收和处理的.UDP 是网络编程中非常重要的一种传输方式.它采用无连接、面向数据报 的通信模型,不需要像TCP一样提前建立连接,通信过程更加简单,传输开销也更小,因此常被应用在实时通信、状态上报、局域网通信、音视频传输等场景中.对于刚开始学习 Socket 编程的读者来说,UDP 也是理解"IP + 端口 + 套接字 + 数据收发"这一整套网络通信流程的很好切入点.不过,仅仅写出 UDP 服务端和客户端代码还不够.在实际学习和调试过程中,我们还需要掌握一些常用的 Linux 网络命令,用来查看网卡信息、确认 IP 地址、检查端口监听状态、测试网络连通性以及分析数据包.例如通过相关命令确认服务是否启动成功,再使用客户端从其他主机访问 Linux 服务端,才能真正完成从程序编写、网络配置、通信测试到结果验证 的完整闭环.本文将围绕 UDP Socket 编程实战 展开,从常用 Linux 网络命令入手,逐步实现 UDP 服务端与客户端程序,并结合 IP 地址、端口号、socket、bind、sendto、recvfrom 等核心接口,详细梳理 UDP 数据收发流程.随后通过客户端访问 Linux 主机进行实际通信验证,进一步分析网络配置、端口监听、连通性检测以及常见问题排查方法.通过本文的学习,你不仅能够完成一个基本的UDP网络通信程序,还能建立起较完整的Linux网络实战思路,为后续深入学习 TCP Socket 编程、客户端/服务器模型、并发服务器、网络协议分析以及高性能网络编程 打下扎实基础.

目录

一、UDP⽹络编程

UDP 网络编程的核心可以概括成一句话:通过 Socket 直接发送和接收一个个独立的数据报,不需要先建立连接.

1.UDP的特点

UDP(User Datagram Protocol,用户数据报协议)属于传输层协议,和TCP相比:

UDP TCP
无连接 面向连接
不保证送达 可靠传输
不保证顺序 保证有序
可能重复、丢包 自动重传
开销小、速度快 开销相对较大
保留消息边界 字节流

典型应用包括:DNS、音视频直播、实时游戏、语音通话、局域网广播等.

2.UDP编程流程

UDP 服务端通常是:

text 复制代码
socket()
   ↓
bind()
   ↓
recvfrom()
   ↓
sendto()
   ↓
close()

客户端通常是:

text 复制代码
socket()
   ↓
sendto()
   ↓
recvfrom()
   ↓
close()

注意:UDP 一般不需要 listen() 和 accept().

3.Linux C语言UDP服务端

c 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);

    char buffer[1024];

    // 1. 创建 UDP socket
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    // 2. 设置服务端地址
    memset(&server_addr, 0, sizeof(server_addr));

    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8888);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    // 3. 绑定端口
    bind(sockfd,
         (struct sockaddr *)&server_addr,
         sizeof(server_addr));

    printf("UDP server listening on port 8888...\n");

    while (1) {
        // 4. 接收数据
        int n = recvfrom(
            sockfd,
            buffer,
            sizeof(buffer) - 1,
            0,
            (struct sockaddr *)&client_addr,
            &client_len
        );

        buffer[n] = '\0';

        printf("收到:%s\n", buffer);

        // 5. 回复客户端
        sendto(
            sockfd,
            buffer,
            n,
            0,
            (struct sockaddr *)&client_addr,
            client_len
        );
    }

    close(sockfd);

    return 0;
}

4.UDP客户端

c 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    int sockfd;
    struct sockaddr_in server_addr;

    char buffer[1024];

    // 1. 创建 UDP socket
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    // 2. 配置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));

    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8888);

    inet_pton(
        AF_INET,
        "127.0.0.1",
        &server_addr.sin_addr
    );

    char *msg = "Hello UDP";

    // 3. 发送数据
    sendto(
        sockfd,
        msg,
        strlen(msg),
        0,
        (struct sockaddr *)&server_addr,
        sizeof(server_addr)
    );

    // 4. 接收回复
    socklen_t len = sizeof(server_addr);

    int n = recvfrom(
        sockfd,
        buffer,
        sizeof(buffer) - 1,
        0,
        (struct sockaddr *)&server_addr,
        &len
    );

    buffer[n] = '\0';

    printf("服务器回复:%s\n", buffer);

    close(sockfd);

    return 0;
}

编译:

bash 复制代码
gcc server.c -o server
gcc client.c -o client

先启动:

bash 复制代码
./server

再运行:

bash 复制代码
./client

客户端发送:

text 复制代码
Hello UDP

服务端接收后再原样返回.

5.最需要掌握的函数

c 复制代码
socket(AF_INET, SOCK_DGRAM, 0);

创建 UDP Socket.

c 复制代码
bind()

把 Socket 和本机 IP + 端口号绑定,一般服务端需要.

c 复制代码
sendto()

向指定 IP 和端口发送一个 UDP 数据报.

c 复制代码
recvfrom()

接收UDP数据报,同时可以获得发送方的 IP 和端口.

其中最关键的是理解:

text 复制代码
TCP:一个连接 → 连续字节流

UDP:
客户端A ──数据报1──→ 服务端
客户端B ──数据报2──→ 服务端
客户端A ──数据报3──→ 服务端

每个 UDP 数据报彼此独立。


1.1 V1版本--UDP Echo Server:完成客户端与服务端的基本通信闭环

cpp 复制代码
//Makefile
.PHONY:all
all:client_udp server_udp

server_udp:EchoServerMain.cc
	g++ -o $@ $^ -std=c++17
client_udp:EchoClient.cc
	g++ -o $@ $^ -std=c++17 -static

.PHONY:clean
clean:
	rm -f client_udp server_udp
cpp 复制代码
//EchoServer.hpp
#ifndef __ECHOSERVER_HPP
#define __ECHOSERVER_HPP

#include <iostream>
#include <string>
#include <cstdlib>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "Logger.hpp"

using namespace NS_LOG_MODULE;

const static int default_fd = -1;
const static int default_port = 8888;

enum
{
    SUCCESS = 0,
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR,
};

class UdpServer
{
public:
    // UdpServer(const std::string &ip, uint16_t port = default_port)
    UdpServer(uint16_t port = default_port)
        : _port(port),
          _sockfd(default_fd)
    {
    }
    ~UdpServer()
    {
        close(_sockfd);
    }
    void Init()
    {
        // 第一步: 创建socket, 本质: 打开网卡 --- 系统特性
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "create socket success, sockfd: " << _sockfd;

        // 第二步: 填充网络信息, 有没有IP和端口信息设置到内核中??设置到你刚刚打开的网络socket对应的文件内部?
        struct sockaddr_in local; // struct sockaddr_in 数据类型,local 用户栈上的!!!,并没有设置到内核
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);                  // h->n
        // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 字符串ip->4字节IP 2. hton
        local.sin_addr.s_addr = INADDR_ANY; // 最佳实践:任意IP地址bind

        // 第三步:bind socket 信息
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind socket success"<< ", port: " << _port;
    }
    void Start()
    {
        // 传递的是字符串,echo server
        char inbuffer[1024];
        while (true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 1. 用户发来的数据
            // 2. 用户的socket信息
            ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (n > 0)
            {
                // ?
                // 用户的IP + Port信息,是recvfrom从网络中获取到的数据,网络序列(大端)-> 转换成为主机序列
                // peer.sin_port; // peer client port
                // peer.sin_addr; // peer client ip
                uint16_t client_port = ntohs(peer.sin_port);
                std::string client_ip = inet_ntoa(peer.sin_addr); // 4字节IP-> ntoh -> 字符串
                std::string client_address = "[" + client_ip + ":" + std::to_string(client_port) + "]# ";

                // 用户发来的数据
                inbuffer[n] = 0;
                LOG(LogLevel::DEBUG) << client_address << inbuffer;
                std::string echo_string = "server echo# ";
                echo_string += inbuffer;
                // h to n
                sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
            }
            else
            {
                LOG(LogLevel::ERROR) << "recvfrom error";
            }
        }
    }

private:
    int _sockfd;
    // std::string _ip; // "192.168.2.2"(字符串风格的点分十进制IP地址, 让人看的) && 4字节IP ???
    uint16_t _port;  // 用户设置好的,server port必须是固定的!
};

#endif
cpp 复制代码
//EchoServerMain.cc
#include "EchoServer.hpp"
#include <memory>

static void Usage(const std::string &process)
{
    std::cerr << "Usage:\n\t";
    std::cerr << process << " local_port" << std::endl;
}

// ./server_udp port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    ENABLE_CONSOLE_LOG_STRATEGY();

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

    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(server_port);
    usvr->Init();
    usvr->Start();

    return 0;
}
cpp 复制代码
//Logger.hpp
#ifndef __LOGGER_HPP
#define __LOGGER_HPP

#include <iostream>
#include <cstdio>
#include <string>
#include <memory>
#include <sstream>
#include <ctime>
#include <sys/time.h>
#include <unistd.h>
#include <filesystem> // C++17
#include <fstream>
#include "Mutex.hpp"

namespace NS_LOG_MODULE
{
    enum class LogLevel
    {
        INFO,
        WARNING,
        ERROR,
        FATAL,
        DEBUG
    };
    std::string LogLevel2Message(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        case LogLevel::DEBUG:
            return "DEBUG";
        default:
            return "UNKNOWN";
        }
    }

    // 1. 时间戳 2. 日期+时间
    std::string GetCurrentTime()
    {
        struct timeval current_time;
        int n = gettimeofday(&current_time, nullptr);
        (void)n;

        // current_time.tv_sec; current_time.tv_usec;
        struct tm struct_time;
        localtime_r(&(current_time.tv_sec), &struct_time); // r: 可重入函数
        char timestr[128];
        snprintf(timestr, sizeof(timestr), "%04d-%02d-%02d %02d:%02d:%02d.%ld",
                 struct_time.tm_year + 1900,
                 struct_time.tm_mon + 1,
                 struct_time.tm_mday,
                 struct_time.tm_hour,
                 struct_time.tm_min,
                 struct_time.tm_sec,
                 current_time.tv_usec);
        return timestr;
    }

    // 输出角度 -- 刷新策略
    // 1. 显示器打印
    // 2. 文件写入
    // 策略模式,策略接口
    class LogStrategy
    {
    public:
        virtual ~LogStrategy() = default;
        virtual void SyncLog(const std::string &message) = 0;
    };
    // 控制台日志刷新策略, 日志将来要向显示器打印
    class ConsoleStrategy : public LogStrategy
    {
    public:
        void SyncLog(const std::string &message) override
        {
            LockGuard lockguard(_mutex);
            std::cerr << message << std::endl; // ??
        }
        ~ConsoleStrategy()
        {
        }

    private:
        Mutex _mutex;
    };

    const std::string defaultpath = "./log";
    const std::string defaultfilename = "log.txt";


    // 文件策略
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &path = defaultpath, const std::string &name = defaultfilename)
            : _logpath(path),
              _logfilename(name)
        {
            LockGuard lockguard(_mutex);
            if (std::filesystem::exists(_logpath))
                return;
            try
            {
                std::filesystem::create_directories(_logpath);
            }
            catch (const std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << '\n';
            }
        }

        void SyncLog(const std::string &message) override
        {
            {
                LockGuard lockguard(_mutex);
                if (!_logpath.empty() && _logpath.back() != '/')
                {
                    _logpath += "/";
                }
                std::string targetlog = _logpath + _logfilename; // "./log/log.txt"
                std::ofstream out(targetlog, std::ios::app);     // 追加方式写入
                if (!out.is_open())
                {
                    std::cerr << "open " << targetlog << "failed" << std::endl;
                    return;
                }
                out << message << "\n";
                out.close();
            }
        }

        ~FileLogStrategy()
        {
        }

    private:
        std::string _logpath;
        std::string _logfilename;
        Mutex _mutex;
    };

    // 交给大家
    // const std::string defaultfilename = "log.info";
    // const std::string defaultfilename = "log.warning";
    // const std::string defaultfilename = "log.fatal";
    // const std::string defaultfilename = "log.error";
    // const std::string defaultfilename = "log.debug";
     // 文件策略&&分日志等级来进行保存
    // class FileLogLevelStrategy : public LogStrategy
    // {
    // public:
    // private:
    // };


    // 日志类:
    // 1. 日志的生成
    // 2. 根据不同的策略,进行刷新
    class Logger
    {
        // 日志的生成:
        // 构建日志字符串
    public:
        Logger()
        {
            UseConsoleStrategy();
        }
        void UseConsoleStrategy()
        {
            _strategy = std::make_unique<ConsoleStrategy>();
        }
        void UseFileStrategy()
        {
            _strategy = std::make_unique<FileLogStrategy>();
        }
        // 内部类, 标识一条完整的日志信息
        //  一条完整的日志信息 = 做半部分固定部分 + 右半部分不固定部分
        //  LogMessage RAII风格的方式,进行刷新
        class LogMessage
        {
        public:
            LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
                : _level(level),
                  _curr_time(GetCurrentTime()),
                  _pid(getpid()),
                  _filename(filename),
                  _line(line),
                  _logger(logger)
            {
                // 先构建出来左半部分
                std::stringstream ss;
                ss << "[" << _curr_time << "] "
                   << "[" << LogLevel2Message(_level) << "] "
                   << "[" << _pid << "] "
                   << "[" << _filename << "] "
                   << "[" << _line << "] "
                   << " - ";

                _loginfo = ss.str();
            }
            template <typename T>
            LogMessage &operator<<(const T &info)
            {
                std::stringstream ss;
                ss << info;
                _loginfo += ss.str();
                return *this; // 返回当前LogMessage对象,方便下次继续进行<<
            }

            ~LogMessage()
            {
                if (_logger._strategy)
                {
                    _logger._strategy->SyncLog(_loginfo);
                }
            }

        private:
            LogLevel _level;
            std::string _curr_time;
            pid_t _pid;
            std::string _filename;
            int _line;
            std::string _loginfo; // 一条完整的日志信息

            // 一个引用,引用外部的Logger类对象
            Logger &_logger; // 方便我们后续进行策略式刷新
        };

        // 这里已经不是内部类了
        // 故意采用拷贝LogMessage
        LogMessage operator()(LogLevel level, std::string filename, int line)
        {
            return LogMessage(level, filename, line, *this);
        }

        ~Logger()
        {
        }

    private:
        std::unique_ptr<LogStrategy> _strategy; // 刷新策略
    };

    // 日志对象,全局使用
    Logger logger;

#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();

#define LOG(level) logger(level, __FILE__, __LINE__)

}

#endif
cpp 复制代码
//Mutex.hpp
#pragma once

#include <iostream>
#include <pthread.h>

class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&_lock, nullptr);
    }
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }
    pthread_mutex_t *Ptr()
    {
        return &_lock;
    }
    void Unlock()
    {
        pthread_mutex_unlock(&_lock);
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&_lock);
    }
private:
    pthread_mutex_t _lock;
};

class LockGuard // RAII风格代码
{
public:
    LockGuard(Mutex &lock):_lockref(lock)
    {
        _lockref.Lock();
    }
    ~LockGuard()
    {
        _lockref.Unlock();
    }
private:
    Mutex &_lockref;
};
cpp 复制代码
//EchoClient.cc
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
        
static void Usage(const std::string &proc)
{
    std::cout << "Usage:\n\t";
    std::cout << proc << " server_ip server_port" << std::endl;
}

// 我怎么知道server对方的IP和端口啊, 类似IP+Port 是被内置到client的!!!
// ./client_udp server_ip server_port
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(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;
        exit(2);
    }

    // 2. 构建server端socket信息 
    // client 需要有自己的IP和Port信息吗? 需要的!
    // 需要显示的bind自己的ip和端口吗?不要显示bind!!!
    // 1. 为什么不让client显示bind?client bind port 出现冲突!client port只需要具有唯一性即可,具体是几,不重要。
    // 2. 如何设置自己的IP和端口呢?client 一般会采用随机端口的方式!由OS自主选择!
    // udp client 首次发送数据的时候,OS底层会隐式自动帮你进行获取随机端口,然后bind + Port + IP
    struct sockaddr_in server;
    memset(&server, 0, sizeof(0));
    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 message;
        // 1. 获取用户输入
        std::cout << "Please Enter# ";
        std::getline(std::cin, message);

        // 2. clinet 发送数据给 server,首次发送即自动bind
        ssize_t n = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
        if(n > 0)
        {
            // recvfrom
            char inbuffer[1024] = {0};
            struct sockaddr_in temp;
            socklen_t len = sizeof(temp);
            ssize_t m = recvfrom(sockfd, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr*)&temp, &len);
            if(m > 0)
            {
                inbuffer[m] = 0;
                std::cout << inbuffer << std::endl;
            }
        }
    }


    return 0;
}

这部分代码本质上是在完成 UDP 网络编程的第一个完整版本:Echo Server(回显服务器) .它已经不只是演示 socket(),而是把 服务端封装、客户端通信、日志系统、互斥锁 RAII、程序入口 串成了一个完整的小项目.

1.Echo Server 要解决什么问题

Echo Server 就是回显服务器.

整个通信过程非常简单:

text 复制代码
Client                             Server

输入 hello
   │
   │  sendto("hello")
   ├──────────────────────────────►│
   │                               │ recvfrom()
   │                               │
   │                               │ "server echo# " + "hello"
   │                               │
   │       sendto(response)        │
   │◄──────────────────────────────┤
   │
 recvfrom()
   │
输出:
server echo# hello

也就是说:客户端发送什么,服务器收到以后加工一下,再发送回来.

虽然业务很简单,但它把 UDP 编程最重要的流程全部覆盖了:

text 复制代码
socket
  ↓
bind
  ↓
recvfrom
  ↓
sendto

这也是后面 DictServer、聊天室等程序的基础.

2.整个项目的文件结构

当前工程主要包含:

text 复制代码
EchoClient.cc
EchoServer.hpp
EchoServerMain.cc
Logger.hpp
Mutex.hpp
Makefile

可以把它们理解成:

文件 作用
EchoServer.hpp UDP 服务端核心逻辑
EchoServerMain.cc 服务端程序入口
EchoClient.cc UDP 客户端
Logger.hpp 日志系统
Mutex.hpp 互斥锁及 RAII 封装
Makefile 编译整个工程

其中真正完成网络通信的是:

text 复制代码
EchoClient.cc
        │
        │ UDP
        ▼
EchoServer.hpp

而:

text 复制代码
Logger.hpp
Mutex.hpp

属于项目的基础设施代码.

3.服务端封装 ------ UdpServer

EchoServer.hpp 中最核心的是:

cpp 复制代码
class UdpServer
{
public:
    UdpServer(uint16_t port = default_port)
        : _port(port),
          _sockfd(default_fd)
    {}

private:
    int _sockfd;
    uint16_t _port;
};

这里把 UDP 服务端抽象成了一个 UdpServer 类.

对象里面保存两个最重要的数据:

text 复制代码
_sockfd

表示服务器创建出来的 socket 文件描述符.

text 复制代码
_port

表示服务器绑定的端口号.

于是:

cpp 复制代码
UdpServer server(8888);

就可以理解成:创建一个准备在 8888 端口工作的 UDP 服务器对象.

4.Init ------ 创建 Socket 并绑定端口

服务端初始化的核心逻辑是:

cpp 复制代码
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);

这里三个参数分别表示:

text 复制代码
AF_INET
    ↓
IPv4

SOCK_DGRAM
    ↓
UDP

0
    ↓
使用默认协议

所以:

cpp 复制代码
socket(AF_INET, SOCK_DGRAM, 0);

实际上就是:创建一个 IPv4 UDP socket.

接下来构造服务器自己的地址:

cpp 复制代码
struct sockaddr_in local;

bzero(&local, sizeof(local));

local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;

这里的 sockaddr_in 可以理解为:

text 复制代码
一个描述 IPv4 网络地址的数据结构

┌─────────────────────────┐
│ IP协议类型 AF_INET       │
├─────────────────────────┤
│ Port                    │
├─────────────────────────┤
│ IP Address              │
└─────────────────────────┘

其中:

cpp 复制代码
local.sin_port = htons(_port);

是因为端口号需要使用网络字节序.

所以:

text 复制代码
host to network short

htons

而:

cpp 复制代码
local.sin_addr.s_addr = INADDR_ANY;

非常重要.

表示:不限定某一个本机 IP,只要是发送到本机该端口的数据,都可以接收.

类似于:

text 复制代码
0.0.0.0:8888

最后:

cpp 复制代码
bind(_sockfd,
     (struct sockaddr *)&local,
     sizeof(local));

完成:

text 复制代码
socket 文件描述符
       +
本机 IP
       +
端口号

之间的绑定.

因此服务端初始化阶段可以总结成:

text 复制代码
socket()
   │
   ▼
创建 UDP socket
   │
   ▼
构造 sockaddr_in
   │
   ▼
bind()
   │
   ▼
服务器正式绑定端口

5.Start ------ 服务端循环接收客户端请求

服务器进入:

cpp 复制代码
while(true)

以后,就不断等待 UDP 数据.

核心代码:

cpp 复制代码
struct sockaddr_in peer;
socklen_t len = sizeof(peer);

ssize_t n = recvfrom(
    _sockfd,
    inbuffer,
    sizeof(inbuffer) - 1,
    0,
    (struct sockaddr *)&peer,
    &len
);

这里最值得理解的是:

recvfrom() 不仅收到数据,还能知道是谁发来的

它得到两部分信息:

text 复制代码
recvfrom()

① 数据
   ↓
inbuffer

② 对方地址
   ↓
peer

也就是说服务器收到:

text 复制代码
hello

的同时,还知道:

text 复制代码
客户端 IP
客户端 Port

所以代码接下来:

cpp 复制代码
uint16_t client_port = ntohs(peer.sin_port);

取得客户端端口.

这里:

text 复制代码
ntohs
network to host short

负责:

text 复制代码
网络字节序
    ↓
主机字节序

再通过:

cpp 复制代码
std::string client_ip = inet_ntoa(peer.sin_addr);

得到客户端 IP.

最终拼接:

cpp 复制代码
std::string client_address =
    "[" + client_ip + ":" +
    std::to_string(client_port) + "]# ";

日志就可以打印类似:

text 复制代码
[192.168.2.3:54321]# hello

6.Echo ------ 把客户端消息返回

服务器收到:

text 复制代码
hello

后:

cpp 复制代码
inbuffer[n] = 0;

把收到的数据变成合法的 C 风格字符串.

然后:

cpp 复制代码
std::string echo_string = "server echo# ";
echo_string += inbuffer;

得到:

text 复制代码
server echo# hello

随后:

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

把数据发送给刚才那个客户端.

注意这里特别体现了 UDP 的特点.

服务端没有:

cpp 复制代码
accept()

也没有保存:

text 复制代码
client socket

而是:

text 复制代码
recvfrom()
   ↓
得到客户端 IP + Port
   ↓
sendto()
   ↓
直接根据 IP + Port 发回去

这就是 UDP 的典型通信方式.

7.服务端入口 ------ EchoServerMain.cc

EchoServerMain.cc 的任务不是实现网络通信,而是:负责启动服务器。

首先要求运行程序时传入一个端口:

cpp 复制代码
if(argc != 2)
{
    Usage(argv[0]);
    exit(USAGE_ERR);
}

所以启动方式类似:

bash 复制代码
./server_udp 8888

然后:

cpp 复制代码
uint16_t server_port = std::stoi(argv[1]);

得到:

text 复制代码
8888

接下来:

cpp 复制代码
std::unique_ptr<UdpServer> usvr =
    std::make_unique<UdpServer>(server_port);

创建服务器对象.

然后:

cpp 复制代码
usvr->Init();
usvr->Start();

整个服务端程序逻辑实际上非常清楚:

text 复制代码
main()

  ↓

读取 port

  ↓

创建 UdpServer

  ↓

Init()
│
├── socket()
│
└── bind()

  ↓

Start()
│
├── recvfrom()
├── 处理数据
└── sendto()

8.UDP Client ------ 客户端主动发送请求

EchoClient.cc 就是客户端.

运行形式:

bash 复制代码
./client_udp server_ip server_port

比如:

bash 复制代码
./client_udp 127.0.0.1 8888

首先取得:

cpp 复制代码
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);

然后客户端同样要创建 socket:

cpp 复制代码
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

因此要注意:

text 复制代码
Server 需要 socket
Client 也需要 socket

socket 本质上就是:操作系统提供给用户进行网络通信的接口.

9.客户端构造服务器地址

客户端需要知道:我要把数据发给谁?

所以构造:

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

这里实际上描述的是:

text 复制代码
目标服务器

IP:server_ip
Port:server_port

例如:

text 复制代码
127.0.0.1:8888

10.为什么客户端没有手动 bind?

代码里的注释提到了一个非常重要的问题:

text 复制代码
client 需要 bind 吗?

实际上:UDP 客户端通常不需要主动调用 bind。

因为第一次执行:

cpp 复制代码
sendto()

时,如果客户端还没有绑定地址,操作系统会自动给它:

text 复制代码
选择本机 IP
+
分配临时端口

例如:

text 复制代码
客户端

192.168.1.10:52731

其中:

text 复制代码
52731

可能就是操作系统自动选择的临时端口.

所以执行:

cpp 复制代码
sendto(...)

之后,可以近似理解为操作系统已经帮你完成了:

text 复制代码
socket
   ↓
自动选择 IP
   ↓
自动选择临时 Port
   ↓
隐式 bind
   ↓
sendto

11.客户端发送与接收

客户端首先:

cpp 复制代码
std::getline(std::cin, message);

让用户输入:

text 复制代码
hello

然后:

cpp 复制代码
sendto(
    sockfd,
    message.c_str(),
    message.size(),
    0,
    (struct sockaddr *)&server,
    sizeof(server)
);

发送到:

text 复制代码
server_ip:server_port

随后:

cpp 复制代码
recvfrom(
    sockfd,
    inbuffer,
    sizeof(inbuffer) - 1,
    0,
    (struct sockaddr *)&temp,
    &len
);

等待服务器回复.

服务器发送:

text 复制代码
server echo# hello

客户端最终:

cpp 复制代码
std::cout << inbuffer << std::endl;

打印:

text 复制代码
server echo# hello

这样一个完整的 UDP 请求---响应过程就完成了.

12.Logger.hpp ------ 日志系统

这里并没有直接大量使用:

cpp 复制代码
std::cout
std::cerr

而是封装了一套自己的日志系统:

cpp 复制代码
LOG(LogLevel::INFO)
LOG(LogLevel::DEBUG)
LOG(LogLevel::ERROR)
LOG(LogLevel::FATAL)

例如:

cpp 复制代码
LOG(LogLevel::INFO)
    << "create socket success, sockfd: "
    << _sockfd;

这样做的优势是可以统一输出:

text 复制代码
日志等级
时间
文件名
行号
具体消息

例如以后可以形成:

text 复制代码
[INFO][2026-10-05 15:00:00][EchoServer.hpp:49]
create socket success, sockfd: 3

而且你的 Logger.hpp 还使用了策略模式.

大致关系是:

text 复制代码
               Log
                │
                ▼
          LogStrategy
           /       \
          /         \
         ▼           ▼
Console          File
Strategy         Strategy

因此日志以后可以选择:

text 复制代码
输出到控制台

或者:

text 复制代码
输出到日志文件

这部分和网络本身没有直接关系,但属于比较好的工程化封装.

13.Mutex.hpp ------ 对 pthread_mutex 的 RAII 封装

我的:

cpp 复制代码
class Mutex

实际上是对:

cpp 复制代码
pthread_mutex_t

的一层 C++ 封装.

提供:

cpp 复制代码
Lock()
Unlock()

对应:

cpp 复制代码
pthread_mutex_lock()
pthread_mutex_unlock()

更加重要的是:

cpp 复制代码
class LockGuard

它采用了 RAII 思想.

构造:

cpp 复制代码
LockGuard(Mutex &lock)
{
    _lockref.Lock();
}

自动加锁.

析构:

cpp 复制代码
~LockGuard()
{
    _lockref.Unlock();
}

自动解锁.

因此可以写:

cpp 复制代码
{
    LockGuard guard(mutex);

    // 临界区
}

离开作用域之后:

text 复制代码
guard 析构
   ↓
自动 Unlock

这样可以防止:

text 复制代码
return
异常
忘记调用 Unlock

导致锁无法释放.

当前 Echo Server 本身还是单线程程序,因此这里的互斥锁主要是为日志系统以及后续多线程扩展做准备.

运行测试结果如下:

完整过程就是:

bash 复制代码
终端1:Server                         终端2:Client

./server_udp 8888                    ./client_udp 127.0.0.1 8888
       │                                      │
       │                                      │
       │                            Please Enter# hello
       │                                      │
       │◄──────── sendto("hello") ────────────┤
       │                                      │
   recvfrom()                                 │
       │                                      │
打印:                                        │
[127.0.0.1:xxxxx]# hello                      │
       │                                      │
       ├──── sendto("server echo# hello") ───►│
       │                                      │
       │                            recvfrom()
       │                                      │
       │                            输出:
       │                            server echo# hello

1.2 V2版本--DictServer:引入词典业务模块,实现单词查询

实现⼀个简单的英译汉的⽹络字典

bash 复制代码
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天

前面的代码不变,增加了Dict.hpp部分的代码,以及在EchoServer.hpp文件增加"Dict.hpp"头文件即可

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

#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Logger.hpp"

static const std::string default_dict = "./dict.txt";
static const std::string sep = ": ";
using namespace NS_LOG_MODULE;

class Dict
{
public:
    Dict(const std::string &dict_path = default_dict) : _dict_path(dict_path)
    {
        LoadDict();
    }
    ~Dict()
    {
    }
    void LoadDict()
    {
        std::ifstream in(_dict_path);
        if (!in.is_open())
        {
            LOG(LogLevel::FATAL) << " open " << _dict_path << " error";
            exit(1);
        }
        std::string line;
        while (std::getline(in, line))
        {
            LOG(LogLevel::FATAL) << "load: " << line << " success";
            // apple: 苹果
            auto pos = line.find(sep);
            if (pos == std::string::npos)
            {
                LOG(LogLevel::WARNING) << "Format: " << line << " error";
                continue;
            }

            std::string k = line.substr(0, pos); // [)
            std::string v = line.substr(pos + sep.size());
            _dict.insert(std::make_pair(k, v));
        }

        in.close();
        LOG(LogLevel::INFO) << "load done....";
    }
    std::string Translate(std::string word)
    {
        auto iter = _dict.find(word);
        if(iter != _dict.end())
        {
            return iter->second;
        }
        else
        {
            return "None";
        }
    }

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

1.3 V2版本--DictServer封装版:网络层与业务层解耦

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

#include <string>

#include "UdpServer.hpp"
#include "Dict.hpp"

class DictServer
{
public:
    DictServer(
        uint16_t port,
        const std::string &dict_path = default_dict)
        : _dict(dict_path),
          _server(
              port,
              [this](const std::string &word)
              {
                  return Translate(word);
              })
    {
    }

    ~DictServer()
    {
    }

public:
    void Init()
    {
        _server.Init();
    }

    void Start()
    {
        _server.Start();
    }

private:
    // DictServer 真正的业务处理函数
    std::string Translate(const std::string &word)
    {
        LOG(LogLevel::DEBUG)
            << "translate request: "
            << word;

        std::string result =
            _dict.Translate(word);

        LOG(LogLevel::DEBUG)
            << "translate result: "
            << word
            << " -> "
            << result;

        return result;
    }

private:
    // 业务模块
    Dict _dict;

    // 网络模块
    UdpServer _server;
};
cpp 复制代码
//DictServerMain.cc
#include <iostream>
#include <string>
#include <memory>
#include <cstdlib>

#include "DictServer.hpp"

static void Usage(const std::string &process)
{
    std::cerr
        << "Usage:\n\t"
        << process
        << " port [dict_path]"
        << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 2 && argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }

    ENABLE_CONSOLE_LOG_STRATEGY();

    uint16_t port =
        static_cast<uint16_t>(
            std::stoi(argv[1]));

    std::string dict_path = default_dict;

    if (argc == 3)
    {
        dict_path = argv[2];
    }

    std::unique_ptr<DictServer> server =
        std::make_unique<DictServer>(
            port,
            dict_path);

    server->Init();

    server->Start();

    return 0;
}

1.4 V3版本--简单聊天室:基于UDP+线程池实现多用户广播通信

cpp 复制代码
//Makefile
.PHONY:all
all:client_chat server_chat

server_chat:ChatServerMain.cc
	g++ -o $@ $^ -std=c++17
client_chat:ChatClient.cc
	g++ -o $@ $^ -std=c++17 -static

.PHONY:clean
clean:
	rm -f client_chat server_chat
cpp 复制代码
//ChatClient.cc
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Thread.hpp"
#include "InetAddr.hpp"

using namespace NS_THREAD_MODULE;

int sockfd = 0;
std::string server_ip;
uint16_t server_port = 0;
std::string nickname;

static void Usage(const std::string &proc)
{
    std::cout << "Usage:\n\t";
    std::cout << proc << " server_ip server_port" << std::endl;
}
static void Online(InetAddr &serveraddr)
{
    std::cout << "Please Set Your Nick Name# ";
    std::getline(std::cin, nickname);
    std::string online_message = nickname + " online!";
    ssize_t n = sendto(sockfd, online_message.c_str(), online_message.size(), 0,
                       (struct sockaddr *)serveraddr.GetNetAddress(), serveraddr.Len());
    (void)n;
}

void RecvMessage()
{
    while (true)
    {
        // recvfrom
        char inbuffer[1024] = {0};
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t m = recvfrom(sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&temp, &len);
        if (m > 0)
        {
            inbuffer[m] = 0;
            std::cerr << inbuffer << std::endl; // 2
        }
    }
}

void SendMessage()
{
    InetAddr serveraddr(server_port, server_ip);
    Online(serveraddr);

    while (true)
    {
        std::string message;
        // 1. 获取用户输入
        std::cout << "Please Enter# "; // 1
        std::getline(std::cin, message);

        message = nickname + "# " + message;

        // 2. clinet 发送数据给 server,首次发送即自动bind
        ssize_t n = sendto(sockfd, message.c_str(), message.size(), 0,
                           (struct sockaddr *)serveraddr.GetNetAddress(), serveraddr.Len());
        (void)n;
    }
}

// 我怎么知道server对方的IP和端口啊, 类似IP+Port 是被内置到client的!!!
// ./client_udp server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(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;
        exit(2);
    }

    Thread recver(RecvMessage);
    Thread sender(SendMessage);

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

    recver.Join();
    sender.Join();
    return 0;
}
cpp 复制代码
//ChatServerMain.cc
#include "ThreadPool.hpp" // 执行者,执行处理动作的人
#include "Route.hpp"   // 任务
#include "UdpServer.hpp" // 获取事件
#include <memory>

static void Usage(const std::string &process)
{
    std::cerr << "Usage:\n\t";
    std::cerr << process << " local_port" << std::endl;
}

using namespace NS_THREAD_POOL_MODULE;

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

// ./server_udp port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    ENABLE_CONSOLE_LOG_STRATEGY();
    uint16_t server_port = std::stoi(argv[1]);
    
    // 线程池模块
    auto thread_pool = ThreadPool<task_t>::Instance();

    // 路由模块
    Route r;

    // 网络模块
    UdpServer usvr(server_port);
    usvr.Init();

    usvr.RegisterService(
        [&r](const InetAddr &addr){
            r.CheckUser(addr);
        },
        [&r, thread_pool](int sockfd, std::string msg){
            auto t = std::bind(&Route::Broadcast, &r, sockfd, msg);
            thread_pool->Enqueue(t);
            // thread_pool->Enqueue([&r, &sockfd, &msg](){
            //     r.Broadcast(sockfd, msg);
            // });
        }
    );

    usvr.Start();
    return 0;
}
cpp 复制代码
//Cond.hpp
#ifndef __COND_HPP
#define __COND_HPP

#include <pthread.h>
#include "Mutex.hpp"

class Cond
{
public:
    Cond()
    {
        pthread_cond_init(&_cond, nullptr);
    }
    void Wait(Mutex &mutex)
    {
        int n = pthread_cond_wait(&_cond, mutex.Ptr());
        (void)n;
    }
    void Signal()
    {
        int n = pthread_cond_signal(&_cond);
        (void)n;
    }
    void Broadcast()
    {
        int n = pthread_cond_broadcast(&_cond);
        (void)n;
    }
    ~Cond()
    {
        pthread_cond_destroy(&_cond);
    }
private:
    pthread_cond_t _cond;
};

#endif
cpp 复制代码
//InetAddr.hpp
#pragma once

#include <iostream>
#include <string>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>


// 对客户端进行先描述
class InetAddr
{
public:
    InetAddr(const struct sockaddr_in &address):_address(address), _len(sizeof(address))
    {
        _ip = inet_ntoa(_address.sin_addr);
        _port = ntohs(_address.sin_port);
    }
    InetAddr(uint16_t port, const std::string &ip = "0.0.0.0"):_ip(ip), _port(port)
    {
        bzero(&_address, sizeof(_address));
        _address.sin_family = AF_INET;
        _address.sin_port = htons(_port);                  // h->n
        _address.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 字符串ip->4字节IP 2. hton
        _len = sizeof(_address);
    }
    bool operator == (const InetAddr &addr)
    {
        return (this->_ip == addr._ip) && (this->_port == addr._port);
    }
    std::string ToString()
    {
        return "[" + _ip + ":" + std::to_string(_port) + "]";
    }
    InetAddr()
    {}
    struct sockaddr_in *GetNetAddress()
    {
        return &_address;
    }
    socklen_t Len()
    {
        return _len;
    }
    ~InetAddr()
    {}
private:
    // net address
    struct sockaddr_in _address;
    socklen_t _len;
    // host address
    std::string _ip;
    uint16_t _port;
};
cpp 复制代码
//Logger.hpp
#ifndef __LOGGER_HPP
#define __LOGGER_HPP

#include <iostream>
#include <cstdio>
#include <string>
#include <memory>
#include <sstream>
#include <ctime>
#include <sys/time.h>
#include <unistd.h>
#include <filesystem> // C++17
#include <fstream>
#include "Mutex.hpp"

namespace NS_LOG_MODULE
{
    enum class LogLevel
    {
        INFO,
        WARNING,
        ERROR,
        FATAL,
        DEBUG
    };
    std::string LogLevel2Message(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        case LogLevel::DEBUG:
            return "DEBUG";
        default:
            return "UNKNOWN";
        }
    }

    // 1. 时间戳 2. 日期+时间
    std::string GetCurrentTime()
    {
        struct timeval current_time;
        int n = gettimeofday(&current_time, nullptr);
        (void)n;

        // current_time.tv_sec; current_time.tv_usec;
        struct tm struct_time;
        localtime_r(&(current_time.tv_sec), &struct_time); // r: 可重入函数
        char timestr[128];
        snprintf(timestr, sizeof(timestr), "%04d-%02d-%02d %02d:%02d:%02d.%ld",
                 struct_time.tm_year + 1900,
                 struct_time.tm_mon + 1,
                 struct_time.tm_mday,
                 struct_time.tm_hour,
                 struct_time.tm_min,
                 struct_time.tm_sec,
                 current_time.tv_usec);
        return timestr;
    }

    // 输出角度 -- 刷新策略
    // 1. 显示器打印
    // 2. 文件写入
    // 策略模式,策略接口
    class LogStrategy
    {
    public:
        virtual ~LogStrategy() = default;
        virtual void SyncLog(const std::string &message) = 0;
    };
    // 控制台日志刷新策略, 日志将来要向显示器打印
    class ConsoleStrategy : public LogStrategy
    {
    public:
        void SyncLog(const std::string &message) override
        {
            LockGuard lockguard(_mutex);
            std::cerr << message << std::endl; // ??
        }
        ~ConsoleStrategy()
        {
        }

    private:
        Mutex _mutex;
    };

    const std::string defaultpath = "./log";
    const std::string defaultfilename = "log.txt";


    // 文件策略
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &path = defaultpath, const std::string &name = defaultfilename)
            : _logpath(path),
              _logfilename(name)
        {
            LockGuard lockguard(_mutex);
            if (std::filesystem::exists(_logpath))
                return;
            try
            {
                std::filesystem::create_directories(_logpath);
            }
            catch (const std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << '\n';
            }
        }

        void SyncLog(const std::string &message) override
        {
            {
                LockGuard lockguard(_mutex);
                if (!_logpath.empty() && _logpath.back() != '/')
                {
                    _logpath += "/";
                }
                std::string targetlog = _logpath + _logfilename; // "./log/log.txt"
                std::ofstream out(targetlog, std::ios::app);     // 追加方式写入
                if (!out.is_open())
                {
                    std::cerr << "open " << targetlog << "failed" << std::endl;
                    return;
                }
                out << message << "\n";
                out.close();
            }
        }

        ~FileLogStrategy()
        {
        }

    private:
        std::string _logpath;
        std::string _logfilename;
        Mutex _mutex;
    };

    // 交给大家
    // const std::string defaultfilename = "log.info";
    // const std::string defaultfilename = "log.warning";
    // const std::string defaultfilename = "log.fatal";
    // const std::string defaultfilename = "log.error";
    // const std::string defaultfilename = "log.debug";
     // 文件策略&&分日志等级来进行保存
    // class FileLogLevelStrategy : public LogStrategy
    // {
    // public:
    // private:
    // };


    // 日志类:
    // 1. 日志的生成
    // 2. 根据不同的策略,进行刷新
    class Logger
    {
        // 日志的生成:
        // 构建日志字符串
    public:
        Logger()
        {
            UseConsoleStrategy();
        }
        void UseConsoleStrategy()
        {
            _strategy = std::make_unique<ConsoleStrategy>();
        }
        void UseFileStrategy()
        {
            _strategy = std::make_unique<FileLogStrategy>();
        }
        // 内部类, 标识一条完整的日志信息
        //  一条完整的日志信息 = 做半部分固定部分 + 右半部分不固定部分
        //  LogMessage RAII风格的方式,进行刷新
        class LogMessage
        {
        public:
            LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
                : _level(level),
                  _curr_time(GetCurrentTime()),
                  _pid(getpid()),
                  _filename(filename),
                  _line(line),
                  _logger(logger)
            {
                // 先构建出来左半部分
                std::stringstream ss;
                ss << "[" << _curr_time << "] "
                   << "[" << LogLevel2Message(_level) << "] "
                   << "[" << _pid << "] "
                   << "[" << _filename << "] "
                   << "[" << _line << "] "
                   << " - ";

                _loginfo = ss.str();
            }
            template <typename T>
            LogMessage &operator<<(const T &info)
            {
                std::stringstream ss;
                ss << info;
                _loginfo += ss.str();
                return *this; // 返回当前LogMessage对象,方便下次继续进行<<
            }

            ~LogMessage()
            {
                if (_logger._strategy)
                {
                    _logger._strategy->SyncLog(_loginfo);
                }
            }

        private:
            LogLevel _level;
            std::string _curr_time;
            pid_t _pid;
            std::string _filename;
            int _line;
            std::string _loginfo; // 一条完整的日志信息

            // 一个引用,引用外部的Logger类对象
            Logger &_logger; // 方便我们后续进行策略式刷新
        };

        // 这里已经不是内部类了
        // 故意采用拷贝LogMessage
        LogMessage operator()(LogLevel level, std::string filename, int line)
        {
            return LogMessage(level, filename, line, *this);
        }

        ~Logger()
        {
        }

    private:
        std::unique_ptr<LogStrategy> _strategy; // 刷新策略
    };

    // 日志对象,全局使用
    Logger logger;

#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();

#define LOG(level) logger(level, __FILE__, __LINE__)

}

#endif
cpp 复制代码
//Mutex.hpp
#pragma once

#include <iostream>
#include <pthread.h>

class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&_lock, nullptr);
    }
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }
    pthread_mutex_t *Ptr()
    {
        return &_lock;
    }
    void Unlock()
    {
        pthread_mutex_unlock(&_lock);
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&_lock);
    }
private:
    pthread_mutex_t _lock;
};

class LockGuard // RAII风格代码
{
public:
    LockGuard(Mutex &lock):_lockref(lock)
    {
        _lockref.Lock();
    }
    ~LockGuard()
    {
        _lockref.Unlock();
    }
private:
    Mutex &_lockref;
};
cpp 复制代码
//Route.hpp
#pragma once

#include <iostream>
#include <memory>
#include <string>
#include <sys/socket.h>
#include "Mutex.hpp"
#include "UserManager.hpp"

class Route
{
public:
    Route():_uma(std::make_unique<UserManager>())
    {}
    void CheckUser(const InetAddr &addr)
    {
        LockGuard lockguard(_lock);
        _uma->AddUser(addr);
    }
    void OfflineUser(const InetAddr &addr)
    {
        LockGuard lockguard(_lock);
        _uma->DelUser(addr);
    }
    void Broadcast(int sockfd, std::string message)
    {
        LockGuard lockguard(_lock);
        auto &users = _uma->Users();
        for(auto &user : users)
        {
            sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)user.GetNetAddress(), user.Len());
        }
    }
    // void Sendto(int sockfd, std::string message, InetAddr & who)
    // {
        
    // }
    ~Route()
    {}
private:
    std::unique_ptr<UserManager> _uma;
    // std::queue<std::string> _q;
    Mutex _lock;
};
cpp 复制代码
//Thread.hpp
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>

namespace NS_THREAD_MODULE
{
    static int gnumber = 1;
    using callback_t = std::function<void()>;

    enum class TSTATUS
    {
        THREAD_NEW,
        THREAD_RUNNING,
        THREAD_STOP
    };

    std::string Status2String(TSTATUS s)
    {
        switch (s)
        {
        case TSTATUS::THREAD_NEW:
            return "THREAD_NEW";
        case TSTATUS::THREAD_RUNNING:
            return "THREAD_RUNNING";
        case TSTATUS::THREAD_STOP:
            return "THREAD_STOP";
        default:
            return "UNKNOWN";
        }
    }

    std::string IsJoined(bool joinable)
    {
        return joinable ? "true" : "false";
    }

    class Thread
    {
    private:
        void ToRunning()
        {
            _status = TSTATUS::THREAD_RUNNING;
        }
        void ToStop()
        {
            _status = TSTATUS::THREAD_STOP;
        }
        static void *ThreadRoutine(void *args)
        {
            Thread *self = static_cast<Thread *>(args);
            pthread_setname_np(self->_tid, self->_name.c_str());
            self->_cb();
            self->ToStop();
            return nullptr;
        }

    public:
        Thread(callback_t cb)
            : _tid(-1), _status(TSTATUS::THREAD_NEW), _joinable(true), _cb(cb), _result(nullptr)
        {
            _name = "Slaver-" + std::to_string(gnumber++);
        }
        bool Start()
        {
            int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);
            if (n != 0)
                return false;

            ToRunning();
            return true;
        }
        void Join()
        {
            if (_joinable)
            {
                int n = pthread_join(_tid, &_result);
                if (n != 0)
                {
                    std::cerr << "join error: " << n << std::endl;
                    return;
                }
                (void)_result;
                _status = TSTATUS::THREAD_STOP;
            }
            else
            {
                std::cerr << "error, thread join status: " << IsJoined(_joinable) << std::endl;
            }
        }
        // 暂停
        // void Stop() // restart()
        // {
        //     // 让线程暂停
        // }
        void Die()
        {
            if (_status == TSTATUS::THREAD_RUNNING)
            {
                pthread_cancel(_tid);
                _status = TSTATUS::THREAD_STOP;
            }
        }
        void Detach()
        {
            if (_status == TSTATUS::THREAD_RUNNING && _joinable)
            {
                pthread_detach(_tid);
                _joinable = false;
            }
            else
            {
                std::cerr << "detach " << _name << " failed" << std::endl;
            }
        }
        void PrintInfo()
        {
            std::cout << "thread name : " << _name << std::endl;
            std::cout << "thread _tid : " << _tid << std::endl;
            std::cout << "thread _status : " << Status2String(_status) << std::endl;
            std::cout << "thread _joinable : " << IsJoined(_joinable) << std::endl;
        }

        ~Thread()
        {
        }

    private:
        std::string _name;
        pthread_t _tid;
        TSTATUS _status;
        bool _joinable;
        // 线程要有自己的任务处理,即回调函数
        callback_t _cb;

        // 线程退出信息
        void *_result;
    };
}
cpp 复制代码
//ThreadPool.hpp
#pragma once

#include <iostream>
#include <vector>
#include <queue>
#include "Logger.hpp"
#include "Thread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"

namespace NS_THREAD_POOL_MODULE
{
    using namespace NS_LOG_MODULE;
    using namespace NS_THREAD_MODULE;

    const int defaultnum = 5;

    // void Test()
    // {
    //     char name[128];
    //     pthread_getname_np(pthread_self(), name, sizeof(name));
    //     while (true)
    //     {
    //         LOG(LogLevel::DEBUG) << "我是一个线程,我要进行运行:" << name;
    //         sleep(1);
    //     }
    // }

    // 线程池要不要对多个线程进行管理呢??
    // 先描述,在组织!
    template <typename T>
    class ThreadPool
    {
    private:
        void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while (true)
            {
                T task;
                {
                    // 保护临界区
                    LockGuard lockguard(_mutex);
                    // 检测任务。不休眠:1. 队列不为空 2. 线程池退出 -> 队列为空 && 线程池不退出
                    while (_tasks.empty() && _isrunning)
                    {
                        // 没有任务, 休眠
                        _slaver_sleep_count++;
                        _cond.Wait(_mutex);
                        _slaver_sleep_count--;
                    }
                    // 线程池退出了-> while 就要break -> 不能
                    // 1. 线程池退出 && _tasks empty
                    if (!_isrunning && _tasks.empty())
                    {
                        _mutex.Unlock();
                        break;
                    }

                    // 有任务, 取任务,本质:把任务由公共变成私有
                    // T -> task*
                    task = _tasks.front();
                    _tasks.pop();
                }

                // 处理任务, 约定
                // 处理任务需要再临界区内部处理吗?不需要
                LOG(LogLevel::INFO) << name << "处理任务:";
                task();
            }

            // 线程退出
            LOG(LogLevel::INFO) << name << " quit...";
        }

        ThreadPool(int slaver_num = defaultnum) : _isrunning(false), _slaver_sleep_count(0), _slaver_num(slaver_num)
        {
            // ThreadPool对象已经存在了
            for (int idx = 0; idx < _slaver_num; idx++)
            {
                // auto f = std::bind(&ThreadPool::HandlerTask, this);
                // // auto f = [this](){
                // //      this->HandlerTask();
                // // };
                // _slavers.emplace_back(f);
                _slavers.emplace_back([this]()
                                      { this->HandlerTask(); });
            }
        }
        // 赋值 拷贝构造禁止
        ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;
        ThreadPool(const ThreadPool<T> &) = delete;

    public:
        // 如果多线程获取这个单例呢.加锁
        // 多线程安全了,但是效率比较低,双if判断
        static ThreadPool<T> *Instance()
        {
            if(nullptr == _instance) // 双if判断
            {
                // 多线程
                LockGuard lockguard(_lock);
                if (nullptr == _instance)
                {
                    // 第一次调用
                    _instance = new ThreadPool<T>();
                    _instance->Start();
                    LOG(LogLevel::INFO) << "第一次使用线程池,创建线程池对象";
                }
            }
            return _instance;
        }
        void Start()
        {
            if (_isrunning)
            {
                LOG(LogLevel::WARNING) << "Thread Pool Is Already Running";
                return;
            }
            _isrunning = true;
            for (auto &slave : _slavers)
            {
                slave.Start();
            }
        }
        void Stop()
        {
            // version1 -- 后续调整
            // if (!_isrunning)
            // {
            //     LOG(LogLevel::WARNING) << "Thread Pool Is Not Running";
            //     return;
            // }
            // for (auto &slave : _slavers)
            // {
            //     slave.Die(); // 太简单粗暴了
            // }
            // _isrunning = false;
            // version 2
            // 1. _isrunning = false
            // 2. 处理完成tasks所有的任务
            // 线程状态: 休眠,正在处理任务 -> 让所有线程全部唤醒
            // HandlerTask自动break
            _mutex.Lock();
            _isrunning = false;
            if (_slaver_sleep_count > 0)
                _cond.Broadcast();
            _mutex.Unlock();
        }
        void Wait()
        {
            for (auto &slave : _slavers)
            {
                slave.Join();
            }
        }
        void Enqueue(T in)
        {
            _mutex.Lock();
            _tasks.push(in);
            if (_slaver_sleep_count > 0)
                _cond.Signal();
            _mutex.Unlock();
        }
        ~ThreadPool()
        {
        }

    private:
        bool _isrunning;
        int _slaver_num;
        std::vector<Thread> _slavers;
        std::queue<T> _tasks; // 任务队列,临界资源
        Mutex _mutex;
        Cond _cond;
        int _slaver_sleep_count;

        // 添加单例模式
        static ThreadPool<T> *_instance;
        static Mutex _lock; // 保证单例的安全
    };

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

    template <typename T>
    Mutex ThreadPool<T>::_lock;

}
cpp 复制代码
//UdpServer.hpp
#ifndef __ECHOSERVER_HPP
#define __ECHOSERVER_HPP

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

using namespace NS_LOG_MODULE;

const static int default_fd = -1;
const static int default_port = 8888;

using handler_addr_t = std::function<void (const InetAddr &)>;
using handler_msg_t = std::function<void (int sokcfd, std::string msg)>;

enum
{
    SUCCESS = 0,
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR,
};

class UdpServer
{
public:
    // UdpServer(const std::string &ip, uint16_t port = default_port)
    UdpServer(uint16_t port = default_port)
        : _port(port),
          _sockfd(default_fd)
    {
    }
    ~UdpServer()
    {
        close(_sockfd);
    }
    void Init()
    {
        // 第一步: 创建socket, 本质: 打开网卡 --- 系统特性
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "create socket success, sockfd: " << _sockfd;

        InetAddr local(_port);

        // 第三步:bind socket 信息
        int n = bind(_sockfd, (struct sockaddr *)(local.GetNetAddress()), local.Len());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind socket success"<< ", port: " << _port;
    }
    void RegisterService(handler_addr_t handler_addr, handler_msg_t handler_msg)
    {
        _handler_addr = handler_addr;
        _handler_msg = handler_msg;
    }
    void Start()
    {
        // 传递的是字符串,echo server
        char inbuffer[1024];
        while (true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 1. 用户发来的数据
            // 2. 用户的socket信息
            ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (n > 0)
            {
                inbuffer[n] = 0;
                // 1. 检测新用户
                InetAddr clientaddress(peer);
                std::string tips = clientaddress.ToString();
                std::string message = tips + inbuffer;
                LOG(LogLevel::DEBUG) << message;
                _handler_addr(clientaddress);
                // 2. 转发消息

                // nickname# message -> [ip:port]-nickname# message

                _handler_msg(_sockfd, message);
            }
            else
            {
                LOG(LogLevel::ERROR) << "recvfrom error";
            }
            // sleep(3);
        }
    }

private:
    int _sockfd;
    // std::string _ip; // "192.168.2.2"(字符串风格的点分十进制IP地址, 让人看的) && 4字节IP ???
    uint16_t _port;  // 用户设置好的,server port必须是固定的!
    handler_addr_t _handler_addr;
    handler_msg_t _handler_msg;
};

#endif
cpp 复制代码
//UserManager.hpp
#pragma once

#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"
#include "Logger.hpp"

using namespace NS_LOG_MODULE;

// class User
// {
//     std::string username;
//     std::string userstatus;
//     InetAddr address;
//     ...
// }

// 增删查改
class UserManager
{
public:
    UserManager(){}
    void AddUser(const InetAddr &addr)
    {
        if(SearchUser(addr))
            return;
        _users.push_back(addr);
    }
    void DelUser(const InetAddr &addr)
    {
        // if(!SearchUser(addr))
        //     return;

        for(auto iter = _users.begin(); iter != _users.end(); iter++)
        {
            if(*iter == addr)
            {
                _users.erase(iter);
                break;
            }
        }
    }
    bool SearchUser(const InetAddr &addr)
    {
        for(auto &user : _users)
        {
            if(user == addr)
            {
                return true;
            }
        }
        return false;
    }
    bool ModUser(const InetAddr &addr)
    {
        // 简单一点
        DelUser(addr);
        AddUser(addr);
        return true;
    }
    std::vector<InetAddr> &Users()
    {
        return _users;
    }
    ~UserManager(){}
private:
    // "ip:port" -> InetAddr
    std::vector<InetAddr> _users;
};

这套 V3 简单聊天室 ,已经不是单纯的 UDP 收发示例,而是一个比较完整的"小型服务器工程"了.它把前面 V1/V2 学到的 UDP、封装、回调 继续往前推进,又加入了 用户管理、消息广播、线程池、互斥锁、条件变量和客户端双线程.

先抓住它的整体运行链:

text 复制代码
ChatClient
    │
    │ sendto()
    ▼
UdpServer
    │
    │ recvfrom()
    ▼
InetAddr 封装客户端地址
    │
    ├──────────────► Route::CheckUser()
    │                    │
    │                    ▼
    │               UserManager
    │               维护在线用户
    │
    └──────────────► ThreadPool::Enqueue()
                         │
                         ▼
                     工作线程
                         │
                         ▼
                  Route::Broadcast()
                         │
                         ▼
                 遍历所有在线用户
                         │
                    sendto()
                 ↙       ↓       ↘
             Client A Client B Client C

这里最重要的一点是:UDP 本身没有"连接"概念,所以服务器实际上是通过客户端的 IP + Port 来识别和管理用户的.

一、项目中每个文件分别干什么

文件 核心职责
ChatClient.cc 聊天客户端,负责上线、发送、接收消息
ChatServerMain.cc 服务端入口,负责组装所有模块
UdpServer.hpp UDP 网络层,负责 socket、bind、recvfrom
InetAddr.hpp 封装 IP + Port
Route.hpp 聊天业务层,负责用户检查和消息广播
UserManager.hpp 维护在线用户列表
ThreadPool.hpp 将广播任务交给工作线程异步执行
Thread.hpp 对 pthread 线程进行 C++ 封装
Mutex.hpp 互斥锁及 RAII 加锁封装
Cond.hpp 条件变量,用于线程等待和唤醒
Logger.hpp 日志输出
Makefile 编译 server_chat 和 client_chat

二、ChatClient.cc ------ 客户端核心

客户端承担两件事情:

text 复制代码
发送消息
+
接收消息

而且这两件事情必须同时进行.

假如客户端只有一个线程:

text 复制代码
getline()
  ↓
等待用户输入
  ↓
sendto()
  ↓
recvfrom()

那么程序等待你输入的时候,就没办法及时显示其他人的消息.

所以我的设计是:

text 复制代码
                ChatClient

              ┌───────────┐
              │ UDP socket│
              └─────┬─────┘
                    │
          ┌─────────┴─────────┐
          ▼                   ▼
    RecvMessage()        SendMessage()
      接收线程              发送线程
          │                   │
      recvfrom()          getline()
          │                   │
      cout显示             sendto()

这是客户端代码最核心的思想.

三、Online() ------ 告诉服务器"我上线了"

函数:

cpp 复制代码
static void Online(InetAddr &serveraddr)

首先:

cpp 复制代码
std::cout << "Please Set Your Nick Name# ";
std::getline(std::cin, nickname);

取得昵称.

例如:

text 复制代码
Alice

然后:

cpp 复制代码
std::string online_message =
    nickname + " online!";

形成:

text 复制代码
Alice online!

再:

cpp 复制代码
sendto(sockfd,
       online_message.c_str(),
       online_message.size(),
       0,
       (struct sockaddr *)serveraddr.GetNetAddress(),
       serveraddr.Len());

发送到服务器.

所以:

text 复制代码
Alice 客户端

Alice online!
      │
      │ UDP
      ▼
    Server

不过这里有一个非常重要的细节:

服务器并没有真正解析 "online!" 关键字.

你的 UdpServer 每收到一个客户端的数据,都会:

cpp 复制代码
_handler_addr(clientaddress);

所以本质上是:客户端第一次发消息时,服务器根据这个客户端的 IP + Port 将它加入在线用户列表。

"Alice online!" 更像是一条约定好的"第一条消息".

四、SendMessage() ------ 客户端发送线程

首先:

cpp 复制代码
InetAddr serveraddr(server_port, server_ip);

创建服务器地址对象.

然后:

cpp 复制代码
Online(serveraddr);

发送上线消息.

之后进入:

cpp 复制代码
while (true)

不断读取:

cpp 复制代码
std::getline(std::cin, message);

如果用户输入:

text 复制代码
hello

代码:

cpp 复制代码
message = nickname + "#" + message;

会形成:

text 复制代码
Alice#hello

然后:

cpp 复制代码
sendto(...)

发送到服务器.

所以发送线程的完整逻辑是:

text 复制代码
启动
 │
 ▼
输入 nickname
 │
 ▼
发送上线消息
 │
 ▼
while(true)
 │
 ├── getline()
 │
 ├── nickname + "#" + message
 │
 └── sendto()

注意:昵称并没有保存在服务器的 UserManager 中。

服务器只记录:

text 复制代码
IP + Port

昵称只是客户端自己拼到消息正文里面.

五、RecvMessage() ------ 客户端接收线程

另外一个线程一直执行:

cpp 复制代码
recvfrom(...)

例如:

cpp 复制代码
ssize_t m = recvfrom(
    sockfd,
    inbuffer,
    sizeof(inbuffer) - 1,
    0,
    (struct sockaddr *)&temp,
    &len
);

如果收到数据:

cpp 复制代码
inbuffer[m] = 0;
std::cerr << inbuffer << std::endl;

就直接打印.

所以这个线程完全不负责发送:

text 复制代码
while(true)
    │
    ▼
 recvfrom()
    │
    ▼
收到服务器广播
    │
    ▼
打印到屏幕

例如 Bob 的客户端收到:

text 复制代码
[127.0.0.1:52318]Alice#hello

就直接显示.

六、客户端为什么创建两个 Thread

main 中:

cpp 复制代码
Thread recver(RecvMessage);
Thread sender(SendMessage);

创建:

text 复制代码
recver
    ↓
RecvMessage()

sender
    ↓
SendMessage()

然后:

cpp 复制代码
recver.Start();
sender.Start();

形成真正的并发:

text 复制代码
线程1                        线程2

RecvMessage()               SendMessage()
     │                           │
 recvfrom()                  getline()
     │                           │
 打印消息                    sendto()
     │                           │
 recvfrom()                  getline()
     │                           │
    ...                         ...

最后:

cpp 复制代码
recver.Join();
sender.Join();

等待两个线程.

由于两个函数基本都是无限循环,所以正常情况下 Join() 不会返回.

七、ChatServerMain.cc ------ 整个服务器的组装者

这个文件本身几乎不处理具体网络数据.

它承担的是:把各个模块组装起来.

这一点和前面的 DictServer 封装版思想是一样的.

首先:

cpp 复制代码
uint16_t server_port = std::stoi(argv[1]);

取得:

text 复制代码
./server_chat 8888
              ↓
             8888

然后:

cpp 复制代码
ENABLE_CONSOLE_LOG_STRATEGY();

开启日志.

接下来:

cpp 复制代码
auto thread_pool =
    ThreadPool<task_t>::Instance();

取得线程池单例.

注意你的 Instance() 内部在第一次创建线程池后调用:

cpp 复制代码
instance->Start();

所以第一次取得线程池对象时,工作线程已经被启动了.

然后:

cpp 复制代码
Route r;

创建聊天业务模块.

再:

cpp 复制代码
UdpServer usvr(server_port);
usvr.Init();

创建并初始化 UDP Server.

八、最关键的 RegisterService()

这里是整个 V3 架构的核心:

cpp 复制代码
usvr.RegisterService(
    [&r](const InetAddr &addr)
    {
        r.CheckUser(addr);
    },

    [&r, thread_pool](int sockfd, std::string msg)
    {
        auto t =
            std::bind(&Route::Broadcast,
                      &r,
                      sockfd,
                      msg);

        thread_pool->Enqueue(t);
    }
);

实际上给 UdpServer 注册了两个回调.

第一个回调:检查用户

收到 UDP 数据以后:

cpp 复制代码
r.CheckUser(addr);

也就是:

text 复制代码
UdpServer
    │
    │ 客户端地址
    ▼
Route::CheckUser()
    │
    ▼
UserManager

确保这个客户端已经进入在线用户列表.

第二个回调:处理聊天消息

没有直接:

cpp 复制代码
r.Broadcast(...)

而是:

cpp 复制代码
std::bind(...)

先把广播操作包装成一个任务:

text 复制代码
task
 =
Route::Broadcast(sockfd, msg)

然后:

cpp 复制代码
thread_pool->Enqueue(t);

放进线程池.

因此主线程只需要:

text 复制代码
收到消息
   ↓
生成广播任务
   ↓
扔进线程池
   ↓
继续 recvfrom()

不需要自己完成整个广播过程.

九、UdpServer.hpp ------ 网络层核心

这个文件和前面 V1/V2 的 UDP Server 有明显演进.

它现在只负责:

text 复制代码
创建 socket
绑定端口
接收数据
封装客户端地址
调用业务回调

而不负责具体:

text 复制代码
聊天室怎么广播
用户怎么保存
线程池怎么运行

这就是网络层和业务层解耦.

handler_addr_t

定义:

cpp 复制代码
using handler_addr_t =
    std::function<void(const InetAddr &)>;

表示一个:

text 复制代码
接收 InetAddr
返回 void

的业务函数.

这里实际绑定的是:

cpp 复制代码
Route::CheckUser()

handler_msg_t

定义:

cpp 复制代码
using handler_msg_t =
    std::function<void(int sockfd,
                       std::string msg)>;

表示:

text 复制代码
socket + message
        ↓
业务处理

实际绑定的是:

text 复制代码
创建 Broadcast 任务
        ↓
ThreadPool::Enqueue()

十、UdpServer::Init() ------ 创建服务器

首先:

cpp 复制代码
_sockfd =
    socket(AF_INET, SOCK_DGRAM, 0);

创建 UDP Socket.

然后:

cpp 复制代码
InetAddr local(_port);

这里相当于:

text 复制代码
0.0.0.0:8888

再:

cpp 复制代码
bind(
    _sockfd,
    (struct sockaddr *)local.GetNetAddress(),
    local.Len()
);

完成绑定.

所以:

text 复制代码
UdpServer::Init()

socket()
   ↓
InetAddr local
   ↓
0.0.0.0:8888
   ↓
bind()

十一、UdpServer::Start() ------ 网络事件循环

进入:

cpp 复制代码
while (true)

以后不断:

cpp 复制代码
recvfrom(...)

取得两个东西:

text 复制代码
数据
+
客户端 sockaddr_in

例如:

text 复制代码
数据:
Alice#hello

客户端地址:
127.0.0.1:45879

然后:

cpp 复制代码
InetAddr clientaddress(peer);

把原始:

cpp 复制代码
sockaddr_in

封装为:

cpp 复制代码
InetAddr

接下来:

cpp 复制代码
std::string tips =
    clientaddress.ToString();

得到:

text 复制代码
[127.0.0.1:45879]

再:

cpp 复制代码
std::string message =
    tips + inbuffer;

最终可能形成:

text 复制代码
[127.0.0.1:45879]Alice#hello

然后:

cpp 复制代码
_handler_addr(clientaddress);

确保用户存在.

最后:

cpp 复制代码
_handler_msg(_sockfd, message);

把真正消息交给业务层.

整个网络层:

text 复制代码
recvfrom
   │
   ▼
sockaddr_in
   │
   ▼
InetAddr
   │
   ├──► handler_addr
   │
   └──► handler_msg

十二、InetAddr.hpp ------ 网络地址封装

这是一个非常值得学习的小封装.

以前你需要到处操作:

cpp 复制代码
struct sockaddr_in

然后:

cpp 复制代码
htons()
ntohs()
inet_addr()
inet_ntoa()

现在统一放到:

cpp 复制代码
class InetAddr

里面.

从客户端地址构造

cpp 复制代码
InetAddr(const struct sockaddr_in &address)

把内核返回的:

text 复制代码
sockaddr_in

转换为:

text 复制代码
_ip
_port
_address
_len

其中:

cpp 复制代码
_ip = inet_ntoa(_address.sin_addr);
_port = ntohs(_address.sin_port);

所以业务层以后不需要直接处理字节序.

根据 IP + Port 构造

cpp 复制代码
InetAddr(uint16_t port,
         const std::string &ip = "0.0.0.0")

内部完成:

cpp 复制代码
_address.sin_family = AF_INET;
_address.sin_port = htons(_port);
_address.sin_addr.s_addr =
    inet_addr(_ip.c_str());

例如:

cpp 复制代码
InetAddr addr(8888, "127.0.0.1");

代表:

text 复制代码
127.0.0.1:8888

operator==

cpp 复制代码
return
    _ip == addr._ip &&
    _port == addr._port;

这对聊天室非常重要.

因为 UDP 没有连接对象,所以:

text 复制代码
IP + Port

就是一个客户端的"身份".

比如:

text 复制代码
127.0.0.1:50001

和:

text 复制代码
127.0.0.1:50002

虽然 IP 一样,也是两个不同客户端.

ToString()

将:

text 复制代码
IP + Port

转换为:

text 复制代码
[127.0.0.1:50001]

方便日志和聊天消息展示.

GetNetAddress() 和 Len()

这两个主要是给:

cpp 复制代码
bind()
sendto()
recvfrom()

这些原生 socket API 使用.

所以 InetAddr 本质就是:C++ 业务代码和底层 sockaddr_in 之间的一层适配器。

十三、UserManager.hpp ------ 在线用户管理

服务器怎么知道应该广播给谁?

就是靠:

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

保存所有客户端地址.

所以所谓"在线用户"实际上是:

text 复制代码
_users

[127.0.0.1:50001]
[127.0.0.1:50002]
[192.168.1.5:52001]
...

这里没有存昵称.

SearchUser()

遍历:

cpp 复制代码
for(auto &user : _users)

通过:

cpp 复制代码
user == addr

判断:

text 复制代码
IP 相同
+
Port 相同

就认为是同一个客户端.

AddUser()

首先:

cpp 复制代码
if(SearchUser(addr))
    return;

防止重复添加.

否则:

cpp 复制代码
_users.push_back(addr);

所以即使 UdpServer 每次收到消息都执行:

cpp 复制代码
Route::CheckUser()

也不会不断产生重复用户。

DelUser()

遍历在线用户:

cpp 复制代码
for(...)

找到之后:

cpp 复制代码
_users.erase(iter);

用于用户下线.

Users()

cpp 复制代码
std::vector<InetAddr> &Users()

直接把在线用户表交给 Route.

于是 Route 可以:

text 复制代码
获取在线用户
    ↓
逐个 sendto

十四、Route.hpp ------ 聊天室真正的业务层

如果说:

text 复制代码
UdpServer = 网络层

那么:

text 复制代码
Route = 业务层

聊天室"应该怎么处理消息"全部在这里.

Route::CheckUser()

代码实际上是:

cpp 复制代码
LockGuard lockguard(_lock);
_uma->AddUser(addr);

所以这个函数名字虽然叫:

text 复制代码
CheckUser

实际更准确的意思是:EnsureUser:确保用户已经存在。

如果没有:

text 复制代码
加入

如果已有:

text 复制代码
什么都不做

Route::OfflineUser()

执行:

cpp 复制代码
_uma->DelUser(addr);

负责删除用户.

不过你目前的整个客户端/服务器协议里,还没有真正调用这个接口.

这点非常重要.

UDP 没有 TCP 那种:

text 复制代码
连接断开
FIN
EOF

所以服务器不知道一个 UDP 客户端什么时候真正退出.

因此当前代码虽然写了:

cpp 复制代码
OfflineUser()

但是实际上还需要设计类似:

text 复制代码
Alice offline!

这样的离线协议,然后服务器主动调用它.

否则客户端退出之后,地址仍可能留在:

cpp 复制代码
_users

中.

十五、Route::Broadcast() ------ 聊天室的核心

这就是聊天广播功能:

cpp 复制代码
auto &users = _uma->Users();

for(auto &user : users)
{
    sendto(...);
}

假设当前在线:

text 复制代码
Alice
Bob
Carol

Alice 发:

text 复制代码
hello

服务器收到后最终:

text 复制代码
             Broadcast()

             ┌────────► Alice
             │
hello ───────┼────────► Bob
             │
             └────────► Carol

这就是聊天室最核心的业务.

注意:

cpp 复制代码
LockGuard lockguard(_lock);

会在广播期间锁住用户列表。

这样可以防止:

text 复制代码
线程 A 正在遍历 users

同时

线程 B AddUser / DelUser

造成 vector 迭代器失效或数据竞争.

十六、为什么需要 Mutex.hpp

聊天室已经进入多线程环境.

例如:

text 复制代码
工作线程1
正在 Broadcast

工作线程2
正在 CheckUser

工作线程3
可能正在 OfflineUser

它们都可能操作:

cpp 复制代码
_users

如果没有锁:

text 复制代码
线程 A                线程 B

遍历 vector           push_back
     │                     │
     └──── 同时发生 ───────┘

可能导致数据竞争
甚至 vector 扩容导致迭代器失效

所以你封装:

cpp 复制代码
class Mutex

提供:

cpp 复制代码
Lock();
Unlock();

底层实际上就是:

cpp 复制代码
pthread_mutex_lock()
pthread_mutex_unlock()

十七、LockGuard ------ RAII 自动锁

比:

cpp 复制代码
mutex.Lock();

...

mutex.Unlock();

更推荐的是:

cpp 复制代码
{
    LockGuard guard(mutex);

    // 临界区
}

因为构造:

cpp 复制代码
_lockref.Lock();

离开作用域:

cpp 复制代码
~LockGuard()
{
    _lockref.Unlock();
}

实现:

text 复制代码
进入作用域
   ↓
自动 Lock

执行代码

离开作用域
   ↓
自动 Unlock

这就是 RAII.

最大的优势是:

text 复制代码
return
异常
break
continue

都不容易导致忘记解锁.

十八、Cond.hpp ------ 条件变量

条件变量主要不是给 Route 用的,而是给:

text 复制代码
ThreadPool

使用.

线程池有一个任务队列:

cpp 复制代码
std::queue<T> _tasks;

如果没有任务,工作线程没有必要:

text 复制代码
while(true)
不停检查
不停检查
不停检查

这样会浪费 CPU.

所以:

cpp 复制代码
_cond.Wait(_mutex);

让工作线程睡眠.

流程:

text 复制代码
工作线程
   │
检查 tasks
   │
   ├── 有任务 ──► 执行
   │
   └── 没任务
          │
          ▼
       Cond::Wait()
          │
          ▼
         睡眠

生产者提交任务:

cpp 复制代码
Enqueue()

以后:

cpp 复制代码
_cond.Signal();

唤醒一个工作线程.

因此:

text 复制代码
             ThreadPool

网络线程                    Worker
   │                          │
Enqueue(task)                 │
   │                          │
push task                     │
   │                          │
Signal() ────────────────────►│
                              │
                           Wake Up
                              │
                              ▼
                         执行 task

十九、pthread_cond_wait() 为什么需要 Mutex

封装:

cpp 复制代码
void Wait(Mutex &mutex)
{
    pthread_cond_wait(
        &_cond,
        mutex.Ptr()
    );
}

这个函数非常特殊.

它会:

text 复制代码
1. 自动释放 mutex
2. 线程进入等待
3. 收到 Signal
4. 重新获得 mutex
5. pthread_cond_wait 返回

也就是:

text 复制代码
持有锁
  │
  ▼
cond_wait
  │
  ├── 释放锁
  ├── 睡眠
  ├── 被唤醒
  └── 重新加锁
       │
       ▼
继续执行

这正是生产者---消费者模型的标准写法.

二十、Thread.hpp ------ pthread 的 C++ 封装

这里进一步对:

cpp 复制代码
pthread_t

进行了封装。

主要成员包括:

text 复制代码
_name
_tid
_status
_joinable
_cb
_result

其中最重要的是:

cpp 复制代码
callback_t _cb;

即:

cpp 复制代码
std::function<void()>

所以创建线程时不用再直接写复杂的:

cpp 复制代码
void *ThreadRoutine(void *)

而可以直接:

cpp 复制代码
Thread t([]()
{
    // 线程任务
});

Start()

内部:

cpp 复制代码
pthread_create(
    &_tid,
    nullptr,
    ThreadRoutine,
    this
);

真正创建线程.

ThreadRoutine()

pthread 要求入口是:

cpp 复制代码
void *(*)(void *)

所以这里设置一个静态函数:

cpp 复制代码
static void *ThreadRoutine(void *args)

然后:

cpp 复制代码
Thread *self =
    static_cast<Thread *>(args);

找回当前对象.

最终:

cpp 复制代码
self->_cb();

执行真正业务.

因此这个封装完成:

text 复制代码
pthread 要求的 C 风格接口
             │
             ▼
       ThreadRoutine
             │
             ▼
      std::function<void()>
             │
             ▼
      普通 C++ 回调函数

Join()

底层:

cpp 复制代码
pthread_join()

等待线程结束.

Detach()

底层:

cpp 复制代码
pthread_detach()

将线程设置为分离状态.

Die()

使用:

cpp 复制代码
pthread_cancel()

尝试取消线程.

这部分属于线程生命周期管理.

二十一、ThreadPool.hpp ------ 服务端并发处理核心

这是整个 V3 中复杂度最高的模块之一.

线程池解决的问题是:不要每收到一条聊天消息,就创建一个新线程.

如果:

text 复制代码
消息1 → pthread_create
消息2 → pthread_create
消息3 → pthread_create
消息4 → pthread_create
...

频繁创建/销毁线程代价很高.

于是提前创建:

text 复制代码
Worker1
Worker2
Worker3
Worker4
Worker5

然后:

text 复制代码
聊天任务
聊天任务
聊天任务
    │
    ▼
任务队列
    │
    ├──► Worker1
    ├──► Worker2
    └──► Worker3

这就是线程池.

二十二、线程池里的任务 T

这是模板:

cpp 复制代码
template <typename T>
class ThreadPool

你服务器定义:

cpp 复制代码
using task_t =
    std::function<void()>;

所以:

text 复制代码
T
=
std::function<void()>

一个任务实际上就是:

cpp 复制代码
Route::Broadcast(...)

被 std::bind 包装后的可调用对象.

二十三、ThreadPool 构造函数

构造时根据:

cpp 复制代码
_slaver_num

创建多个:

cpp 复制代码
Thread

每个线程执行:

cpp 复制代码
this->HandlerTask();

所以:

text 复制代码
ThreadPool
   │
   ├── Thread 1 → HandlerTask()
   ├── Thread 2 → HandlerTask()
   ├── Thread 3 → HandlerTask()
   ├── Thread 4 → HandlerTask()
   └── Thread 5 → HandlerTask()

这里代码把 Worker 命名为:

text 复制代码
slaver

概念上就是工作线程 Worker.

二十四、ThreadPool 是单例

代码:

cpp 复制代码
static ThreadPool<T> *Instance()

保证整个进程中只有一个线程池.

调用:

cpp 复制代码
ThreadPool<task_t>::Instance();

第一次:

text 复制代码
instance == nullptr
     │
     ▼
new ThreadPool
     │
     ▼
Start()

之后:

text 复制代码
直接返回已有 instance

所以服务器所有消息共享同一个线程池.

代码这里还用了:

cpp 复制代码
static Mutex _lock;

保护单例初始化,采用了类似 double-check 的写法.

二十五、HandlerTask() ------ Worker 线程真正工作的地方

每个 Worker 一直:

cpp 复制代码
while(true)

首先加锁:

cpp 复制代码
LockGuard lockguard(_mutex);

然后检查:

cpp 复制代码
_tasks.empty()

如果:

text 复制代码
任务队列为空
+
线程池仍运行

就:

cpp 复制代码
_cond.Wait(_mutex);

休眠.

有任务之后:

cpp 复制代码
task = _tasks.front();
_tasks.pop();

把任务从队列取出来.

然后非常重要:执行任务是在锁外.

也就是:

text 复制代码
加锁
 │
取任务
 │
解锁
 │
 ▼
task()

而不是:

text 复制代码
加锁
 │
task()
 │
解锁

这样一个 Worker 广播的时候,不会把整个任务队列锁死.

这是正确的线程池设计思想.

二十六、Enqueue() ------ 提交任务

ChatServerMain 中:

cpp 复制代码
thread_pool->Enqueue(t);

最终执行:

cpp 复制代码
_mutex.Lock();

_tasks.push(in);

if(_slaver_sleep_count > 0)
{
    _cond.Signal();
}

_mutex.Unlock();

含义:

text 复制代码
广播任务
   │
   ▼
锁住队列
   │
   ▼
push
   │
   ▼
如果有 Worker 在睡觉
   │
   ▼
Signal()
   │
   ▼
解锁

所以线程池实现了一个标准的:

text 复制代码
生产者                       消费者

UdpServer                    Worker Threads

Enqueue(task)
     │
     ▼
 ┌───────────┐
 │Task Queue │
 └───────────┘
       │
       ▼
 HandlerTask()
       │
       ▼
 Broadcast()

二十七、Start / Stop / Wait

Start():

cpp 复制代码
_isrunning = true;

然后:

cpp 复制代码
slave.Start();

启动所有 Worker.

Stop():

cpp 复制代码
_isrunning = false;

如果有 Worker 正在等待:

cpp 复制代码
_cond.Broadcast();

把所有线程叫醒,让它们检查:

text 复制代码
线程池已经停止
+
没有剩余任务

然后退出.

Wait():

cpp 复制代码
slave.Join();

等待所有 Worker 结束.

这是一个比较完整的线程池生命周期模型.

二十八、Logger.hpp ------ 日志模块

Logger 和聊天室业务没有直接关系,但服务器开发非常需要.

支持:

text 复制代码
DEBUG
INFO
WARNING
ERROR
FATAL

可以打印:

text 复制代码
时间
日志等级
线程/进程信息
源文件
代码行
具体内容

例如:

text 复制代码
[INFO] [UdpServer.hpp] - bind socket success

Logger 还抽象了输出策略,大致是:

text 复制代码
               Logger
                  │
              LogStrategy
               /       \
              /         \
       Console        File
       Strategy      Strategy

所以可以:

cpp 复制代码
ENABLE_CONSOLE_LOG_STRATEGY();

让日志输出到控制台.

这样的意义在多线程服务器里尤其明显,因为:

text 复制代码
谁收到了消息
哪个线程正在处理
什么时候发生错误

都可以通过日志定位.

二十九、整个聊天室一次消息到底怎么跑

假设有:

text 复制代码
Alice
Bob
Carol

三个人在线.

Alice 输入:

text 复制代码
hello

客户端首先形成:

text 复制代码
Alice#hello

之后真正调用链是:

text 复制代码
Alice
 │
 │ SendMessage()
 │
 │ sendto("Alice#hello")
 ▼
──────────────────────── UDP ────────────────────────
 │
 ▼
UdpServer::Start()
 │
 │ recvfrom()
 ▼
得到 peer
 │
 ▼
InetAddr(peer)
 │
 ▼
[127.0.0.1:50001]
 │
 ▼
message =
"[127.0.0.1:50001]Alice#hello"
 │
 ├──────────────► Route::CheckUser()
 │                    │
 │                    ▼
 │               UserManager
 │                    │
 │             已存在 → 不重复添加
 │
 ▼
handler_msg()
 │
 ▼
std::bind(Route::Broadcast)
 │
 ▼
生成 task
 │
 ▼
ThreadPool::Enqueue(task)
 │
 ▼
       Task Queue
 │
 ▼
某一个 Worker 被唤醒
 │
 ▼
HandlerTask()
 │
 ▼
task()
 │
 ▼
Route::Broadcast()
 │
 ▼
UserManager::Users()
 │
 ├──────── sendto ───────► Alice
 │
 ├──────── sendto ───────► Bob
 │
 └──────── sendto ───────► Carol
                            │
                            ▼
                      RecvMessage()
                            │
                            ▼
                          cout

这就是 V3 的核心.

只要Alice发一条消息,Bob和Carol都能在自己的终端看到,就说明V3简单聊天室的核心功能已经运行测试成功.


二、补充相关参考内容

2.1地址转换函数

可以把这部分理解成:地址转换函数负责在"人能看懂的 IP 字符串"和"内核/socket 使用的二进制 IP 地址"之间做转换.

例如我们平时写:

text 复制代码
127.0.0.1
192.168.1.10

这是点分十进制字符串 ;而 sockaddr_in 中:

cpp 复制代码
struct in_addr sin_addr;

保存的是 IPv4 的 32 位二进制地址.

1.字符串 → 网络地址

cpp 复制代码
inet_aton("127.0.0.1", &addr.sin_addr);
cpp 复制代码
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
cpp 复制代码
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);

作用都是把:

text 复制代码
"127.0.0.1"

转换成 struct in_addr 使用的二进制 IP.

其中更推荐:

cpp 复制代码
inet_pton()

因为它同时支持 IPv4 和 IPv6.

2.网络地址 → 字符串

cpp 复制代码
inet_ntoa(addr.sin_addr);

或者:

cpp 复制代码
char ip[INET_ADDRSTRLEN];

inet_ntop(
    AF_INET,
    &addr.sin_addr,
    ip,
    sizeof(ip)
);

作用是把二进制 IP 转回:

text 复制代码
"127.0.0.1"

更推荐:

cpp 复制代码
inet_ntop()

最核心记忆

text 复制代码
字符串 IP
   │
   │ inet_pton
   ▼
struct in_addr
   │
   │ inet_ntop
   ▼
字符串 IP

即:

text 复制代码
inet_pton:字符串 → 网络地址
inet_ntop:网络地址 → 字符串

2.2关于inet_ntoa

inet_ntoa这个函数返回了⼀个char*,很显然是这个函数⾃⼰在内部为我们申请了⼀块内存来保存ip的结果.那么是否需要调⽤者⼿动释放呢?

man⼿册上说,inet_ntoa函数,是把这个返回结果放到了静态存储区.这个时候不需要我们⼿动进⾏释放.那么问题来了,如果我们调⽤多次这个函数,会有什么样的效果呢?参⻅如下代码:

cpp 复制代码
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main()
{
    struct sockaddr_in addr1;
    struct sockaddr_in addr2;

    addr1.sin_addr.s_addr = 0;
    addr2.sin_addr.s_addr = 0xffffffff;

    char *ptr1 = inet_ntoa(addr1.sin_addr);
    char *ptr2 = inet_ntoa(addr2.sin_addr);

    printf("ptr1: %s, ptr2: %s\n", ptr1, ptr2);

    return 0;
}
bash 复制代码
ptr1: 255.255.255.255, ptr2: 255.255.255.255

因为inet_ntoa把结果放到⾃⼰内部的⼀个静态存储区,这样第⼆次调⽤时的结果会覆盖掉上⼀次的结果.

多线程调⽤inet_ntoa代码⽰例如下:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

void *Func1(void *p)
{
    struct sockaddr_in *addr = (struct sockaddr_in *)p;

    while (1)
    {
        char *ptr = inet_ntoa(addr->sin_addr);
        printf("addr1: %s\n", ptr);
    }

    return NULL;
}

void *Func2(void *p)
{
    struct sockaddr_in *addr = (struct sockaddr_in *)p;

    while (1)
    {
        char *ptr = inet_ntoa(addr->sin_addr);
        printf("addr2: %s\n", ptr);
    }

    return NULL;
}

int main()
{
    pthread_t tid1 = 0;

    struct sockaddr_in addr1;
    struct sockaddr_in addr2;

    addr1.sin_addr.s_addr = 0;
    addr2.sin_addr.s_addr = 0xffffffff;

    pthread_create(&tid1, NULL, Func1, &addr1);

    pthread_t tid2 = 0;
    pthread_create(&tid2, NULL, Func2, &addr2);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    return 0;
}

2.3remove_if 样例

cpp 复制代码
#include <iostream>
#include <list>
#include <memory>
#include <algorithm>

int main()
{
    std::list<std::shared_ptr<int>> ls;

    ls.push_back(std::make_shared<int>(1));
    ls.push_back(std::make_shared<int>(2));
    ls.push_back(std::make_shared<int>(3));
    ls.push_back(std::make_shared<int>(4));
    ls.push_back(std::make_shared<int>(4));
    ls.push_back(std::make_shared<int>(4));
    ls.push_back(std::make_shared<int>(5));
    ls.push_back(std::make_shared<int>(6));

    for (auto &v : ls)
    {
        std::cout << *v << std::endl;
    }

    std::cout << "aa: " << ls.size() << std::endl;
    std::cout << "\n";

    // int a = 3;
    int a = 4;

    auto pos = remove_if(
        ls.begin(),
        ls.end(),
        [&a](const std::shared_ptr<int> &elem) -> bool
        {
            return a == *elem;
        });

    ls.erase(pos, ls.end());

    std::cout << "aa: " << ls.size() << std::endl;

    for (auto &v : ls)
    {
        std::cout << *v << std::endl;
    }

    return 0;
}

remove_if() 并不会实际移除序列 [start, end) 中的元素;如果在一个容器上应用 remove_if(),容器的长度并不会改变,所有的元素都还在容器里面(但是逻辑上已经无法访问).

remove_if() 将所有应该移除的元素都移动到容器尾部并返回一个分界的迭代器.为了实际移除元素,你必须对容器自行调用 erase() 以擦除需要移除的元素.

这段代码的核心就是经典的:

cpp 复制代码
auto pos = std::remove_if(...);
ls.erase(pos, ls.end());

也就是 erase-remove 惯用法.


三、⽹络命令

3.1Ping命令

ping 命令主要用来测试 目标主机是否可达、网络延迟大概多少、是否存在丢包.

基本用法

bash 复制代码
ping 目标IP

例如:

bash 复制代码
ping 127.0.0.1

或者:

bash 复制代码
ping 192.168.1.10

也可以测试域名:

bash 复制代码
ping www.baidu.com
bash 复制代码
//+-c+数字等于ping多少次数
ping -c5 www.baidu.com

工作原理

ping 使用的是 ICMP 协议,不是TCP,也不是UDP.

大致过程:

text 复制代码
本机
  │
  │ ICMP Echo Request
  ▼
目标主机
  │
  │ ICMP Echo Reply
  ▼
本机

所以它本质是在问:"你在线吗?"

对方回复:"我在线。"

输出怎么看

例如:

text 复制代码
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.035 ms

几个重点字段:

  • icmp_seq=1:第几个 ICMP 包
  • ttl=64:数据包还能经过多少跳
  • time=0.035 ms:往返延迟

停止 ping

Linux 默认会一直 ping:

bash 复制代码
ping 127.0.0.1

按:

text 复制代码
Ctrl + C

停止.

指定次数

例如只发 4 个包:

bash 复制代码
ping -c 4 127.0.0.1

常见测试顺序

1.测试本机协议栈

bash 复制代码
ping 127.0.0.1

成功说明本机 TCP/IP 协议栈基本正常.

2.测试局域网主机

bash 复制代码
ping 192.168.1.100

3.测试公网 IP

bash 复制代码
ping 8.8.8.8

4.测试域名

bash 复制代码
ping www.baidu.com

如果:

bash 复制代码
ping 8.8.8.8

能通,但:

bash 复制代码
ping www.baidu.com

不通,通常优先怀疑 DNS 解析问题.

注意

ping 不通不一定说明目标主机真的不可达,因为有些服务器或防火墙会禁止 ICMP.

所以:

text 复制代码
ping 不通
≠
TCP/UDP 服务一定不可用

一句话总结

text 复制代码
ping = 用 ICMP 测试网络连通性、延迟和丢包情况

3.2netstat命令

netstat 用来查看 网络连接、监听端口、路由表、网络接口统计信息.

常用命令

查看所有网络连接:

bash 复制代码
netstat -an

查看 TCP:

bash 复制代码
netstat -ant

查看 UDP:

bash 复制代码
netstat -anu

查看监听端口:

bash 复制代码
netstat -lnt

查看监听的 UDP 端口:

bash 复制代码
netstat -lnu

查看对应进程:

bash 复制代码
netstat -antp

例如查看 8888 端口:

bash 复制代码
netstat -anp | grep 8888

常见参数

text 复制代码
-a   显示所有连接和监听端口
-n   直接显示 IP 和端口号,不做域名解析
-t   TCP
-u   UDP
-l   只显示监听状态
-p   显示进程 PID / 程序名
-r   查看路由表
-i   查看网络接口

常见状态

TCP 常见:

text 复制代码
LISTEN       监听
ESTABLISHED  已建立连接
TIME_WAIT    等待连接彻底关闭
CLOSE_WAIT   等待本地程序关闭连接

UDP 常见:

text 复制代码
UNCONN

因为 UDP 是无连接协议,属于正常现象.

例如你的 UDP 服务端运行在 8888:

bash 复制代码
netstat -anup | grep 8888

可能看到:

text 复制代码
udp  0  0  0.0.0.0:8888  0.0.0.0:*  ...

表示服务端已经绑定 8888 端口.

现在 Linux 中更推荐使用:

bash 复制代码
ss

例如:

bash 复制代码
ss -lntp
ss -unlp

一句话记忆:netstat = 查看当前主机的网络连接和端口状态.


3.3pidof命令

pidof 用来根据进程名查找进程 PID.

基本用法

bash 复制代码
pidof 进程名

例如:

bash 复制代码
pidof bash

可能输出:

text 复制代码
1234 5678

表示当前有两个 bash 进程.

查找自己写的程序

例如启动:

bash 复制代码
./server_chat 8888

查询:

bash 复制代码
pidof server_chat

可能得到:

text 复制代码
24680

这个 24680 就是进程 PID.

常用搭配

查看进程:

bash 复制代码
pidof server_chat

结束进程:

bash 复制代码
kill $(pidof server_chat)

强制结束:

bash 复制代码
kill -9 $(pidof server_chat)

常用选项

只返回一个 PID:

bash 复制代码
pidof -s bash

忽略指定 PID:

bash 复制代码
pidof -o 1234 bash

和 ps 的区别

bash 复制代码
pidof server_chat

直接得到 PID.

而:

bash 复制代码
ps aux | grep server_chat

会显示更详细的信息.

一句话记:pidof = 根据程序名快速找到进程PID.


四、验证UDP---Windows作为客户端访问Linux

cpp 复制代码
//Makefile
.PHONY:all
all:tcp_server udp_server

tcp_server:TcpServer.cc
	g++ -o $@ $^ -std=c++14
udp_server:UdpServer.cc
	g++ -o $@ $^ -std=c++14

.PHONY:clean
clean:
	rm -rf tcp_server udp_server
cpp 复制代码
//TcpClient.cc
#include <winsock2.h>
#include <iostream>
#include <string>

#pragma warning(disable : 4996)

#pragma comment(lib, "ws2_32.lib")

std::string serverip = "";  // 填写你的云服务器ip
uint16_t serverport = 8888; // 填写你的云服务开放的端口号

int main()
{
    WSADATA wsaData;
    int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (result != 0)
    {
        std::cerr << "WSAStartup failed: " << result << std::endl;
        return 1;
    }

    SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (clientSocket == INVALID_SOCKET)
    {
        std::cerr << "socket failed" << std::endl;
        WSACleanup();
        return 1;
    }

    sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(serverport);                  // 替换为服务器端口
    serverAddr.sin_addr.s_addr = inet_addr(serverip.c_str()); // 替换为服务器IP地址

    result = connect(clientSocket, (SOCKADDR *)&serverAddr, sizeof(serverAddr));
    if (result == SOCKET_ERROR)
    {
        std::cerr << "connect failed" << std::endl;
        closesocket(clientSocket);
        WSACleanup();
        return 1;
    }
    while (true)
    {
        std::string message;
        std::cout << "Please Enter@ ";
        std::getline(std::cin, message);
        if(message.empty()) continue;
        send(clientSocket, message.c_str(), message.size(), 0);

        char buffer[1024] = {0};
        int bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
        if (bytesReceived > 0)
        {
            buffer[bytesReceived] = '\0'; // 确保字符串以 null 结尾
            std::cout << "Received from server: " << buffer << std::endl;
        }
        else
        {
            std::cerr << "recv failed" << std::endl;
        }
    }

    closesocket(clientSocket);
    WSACleanup();

    return 0;
}
cpp 复制代码
//TcpServer.cc
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>

const static int default_backlog = 6;

enum
{
    Usage_Err = 1,
    Socket_Err,
    Bind_Err,
    Listen_Err
};

#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr)

class TcpServer
{
public:
    TcpServer(uint16_t port) : _port(port), _isrunning(false)
    {
    }
    // 都是固定套路
    void Init()
    {
        // 1. 创建socket, file fd, 本质是文件
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            exit(0);
        }
        int opt = 1;
        setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

        // 2. 填充本地网络信息并bind
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = htonl(INADDR_ANY);

        // 2.1 bind
        if (bind(_listensock, CONV(&local), sizeof(local)) != 0)
        {
            exit(Bind_Err);
        }

        // 3. 设置socket为监听状态,tcp特有的
        if (listen(_listensock, default_backlog) != 0)
        {
            exit(Listen_Err);
        }
    }
    void ProcessConnection(int sockfd, struct sockaddr_in &peer)
    {
        uint16_t clientport = ntohs(peer.sin_port);
        std::string clientip = inet_ntoa(peer.sin_addr);
        std::string prefix = clientip + ":" + std::to_string(clientport);
        std::cout << "get a new connection, info is : " << prefix << std::endl;
        while (true)
        {
            char inbuffer[1024];
            ssize_t s = ::read(sockfd, inbuffer, sizeof(inbuffer)-1);
            if(s > 0)
            {
                inbuffer[s] = 0;
                std::cout << prefix << "# " << inbuffer << std::endl;
                std::string echo = inbuffer;
                echo += "[tcp server echo message]";
                write(sockfd, echo.c_str(), echo.size());
            }
            else
            {
                std::cout << prefix << " client quit" << std::endl;
                break;
            }
        }
    }
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 4. 获取连接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = accept(_listensock, CONV(&peer), &len);
            if (sockfd < 0)
            {
                continue;
            }
            ProcessConnection(sockfd, peer);
        }
    }
    ~TcpServer()
    {
    }

private:
    uint16_t _port;
    int _listensock; // TODO
    bool _isrunning;
};

using namespace std;

void Usage(std::string proc)
{
    std::cout << "Usage : \n\t" << proc << " local_port\n"
              << std::endl;
}
// ./tcp_server 8888
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return Usage_Err;
    }
    uint16_t port = stoi(argv[1]);
    std::unique_ptr<TcpServer> tsvr = make_unique<TcpServer>(port);
    tsvr->Init();
    tsvr->Start();

    return 0;
}
cpp 复制代码
//UdpClient.cc
#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
#include <WinSock2.h>
#include <Windows.h>

#pragma warning(disable : 4996)

#pragma comment(lib, "ws2_32.lib")

std::string serverip = "";  // 填写你的云服务器ip
uint16_t serverport = 8888; // 填写你的云服务开放的端口号

int main()
{
    WSADATA wsd;
    WSAStartup(MAKEWORD(2, 2), &wsd);

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport); //?
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == SOCKET_ERROR)
    {
        std::cout << "socker error" << std::endl;
        return 1;
    }
    std::string message;
    char buffer[1024];
    while (true)
    {
        std::cout << "Please Enter@ ";
        std::getline(std::cin, message);
        if(message.empty()) continue;
        sendto(sockfd, message.c_str(), (int)message.size(), 0, (struct sockaddr *)&server, sizeof(server));
        struct sockaddr_in temp;
        int len = sizeof(temp);
        int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << buffer << std::endl;
        }
    }

    closesocket(sockfd);
    WSACleanup();
    return 0;
}
cpp 复制代码
//UdpServer.cc
#include <iostream>
#include <string>
#include <memory>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

const static uint16_t defaultport = 8888;
const static int defaultfd = -1;
const static int defaultsize = 1024;

enum
{
    Usage_Err = 1,
    Socket_Err,
    Bind_Err
};

class UdpServer
{
public:
    UdpServer(uint16_t port = defaultport)
        : _port(port), _sockfd(defaultfd)
    {
    }
    void Init()
    {
        // 1. 创建socket,就是创建了文件细节
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            exit(Socket_Err);
        }

        // 2. 绑定,指定网络信息
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // memset
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY; // 1. 4字节IP 2. 变成网络序列

        // 结构体填完,设置到内核中了吗??没有
        int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n != 0)
        {
            exit(Bind_Err);
        }
    }
    void Start()
    {
        // 服务器永远不退出
        char buffer[defaultsize];
        for (;;)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer); // 不能乱写
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (n > 0)
            {
                uint16_t clientport = ntohs(peer.sin_port);
                std::string clientip = inet_ntoa(peer.sin_addr);
                std::string prefix = clientip + ":" + std::to_string(clientport);
                buffer[n] = 0;
                std::cout << prefix << "# " << buffer << std::endl;


                std::string echo = buffer;
                echo += "[udp server echo message]";
                sendto(_sockfd, echo.c_str(), echo.size(), 0, (struct sockaddr *)&peer, len);
            }
        }
    }
    ~UdpServer()
    {
    }

private:
    uint16_t _port;
    int _sockfd;
};

void Usage(std::string proc)
{
    std::cout << "Usage : \n\t" << proc << " local_port\n"
              << std::endl;
}

// ./udp_server 8888
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return Usage_Err;
    }

    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);
    usvr->Init();
    usvr->Start();

    return 0;
}

🚀真正的勇者不是流泪的人,而是含泪奔跑的人!


敬请期待下一篇文章内容


每日心灵鸡汤: 做真实的自己,奔赴属于你的远方!

活出真实,接纳不完美的自己!不必伪装优秀,硬撑不属于你的模样;课堂上不懂就问,不必害怕别人笑话.允许考试有失误,接纳学习里的短板,不用刻意迎合,勉强融入不适合的圈子;正视自身优缺点,不盲目贬低或高估;累了可以停下休整,不必一直勉强硬扛;踏实做好分内之事,遵从内心稳步前行;不必事事追求亮眼,平凡努力同样可贵.坦然做真实的你,自在奔赴属于你的远方.

相关推荐
小O的算法实验室1 小时前
IEEE TEVC,知识与学习驱动人工蜂群算法+家庭医疗护理路径与调度
学习
雷✘1 小时前
栈与队列的进出顺序、存储结构选择及循环队列判满
网络
JWASX1 小时前
go 学习 - prometheus 指标协程池监控
学习·golang·prometheus
@#¥&~是乱码鱼啦1 小时前
ArkWeb开发手记02|权限、网络白名单与页面缓存控制
网络·缓存
peter67681 小时前
vue学习小结
前端·vue.js·学习
程序员-Benothing1 小时前
正则表达式详解:BRE、ERE、PCRE语法与实战案例
linux·运维·服务器
软件架构师-叶秋1 小时前
V853方案之ubuntu开发环境搭建
linux·运维·服务器·全志·v853
我命由我123451 小时前
单利计息与复利计息
经验分享·学习·职场和发展·产品运营·求职招聘·职场发展·产品经理
火眼金睛记单词1 小时前
零基础成人重拾英语:第一个月的30天行动地图
前端·经验分享·学习·小程序