Linux 上手 UDP Socket 程序编写(含完整具体demo)

文章目录

  • [Linux 上手 UDP Socket 程序编写(含完整具体demo)](#Linux 上手 UDP Socket 程序编写(含完整具体demo))
    • [1. 核心技术点](#1. 核心技术点)
    • [2. UDP 通信模型速览](#2. UDP 通信模型速览)
    • [3. 构建服务端:步骤与 API](#3. 构建服务端:步骤与 API)
      • [3.1 创建套接字 socket](#3.1 创建套接字 socket)
      • [3.2 填充地址并绑定 bind](#3.2 填充地址并绑定 bind)
      • [3.3 循环收包 recvfrom → 处理 → 回包 sendto](#3.3 循环收包 recvfrom → 处理 → 回包 sendto)
      • [3.4 注入业务回调(示例)](#3.4 注入业务回调(示例))
    • [4. 构建客户端:步骤与 API](#4. 构建客户端:步骤与 API)
      • [4.1 创建套接字 socket](#4.1 创建套接字 socket)
      • [4.2 组织对端地址、发送 sendto](#4.2 组织对端地址、发送 sendto)
      • [4.3 接收回复 recvfrom](#4.3 接收回复 recvfrom)
    • [5. 字典 Demo 的实现(作为业务回调接入)](#5. 字典 Demo 的实现(作为业务回调接入))
      • [5.1 字典文件与数据结构](#5.1 字典文件与数据结构)
      • [5.2 加载流程:读取 → 切分 → 裁剪 → 写入 map](#5.2 加载流程:读取 → 切分 → 裁剪 → 写入 map)
      • [5.3 查询接口与不命中返回](#5.3 查询接口与不命中返回)
      • [5.4 将业务回调注入服务端](#5.4 将业务回调注入服务端)
    • [6. 模块设计与职责划分](#6. 模块设计与职责划分)
    • [7. 关键函数与细节清单](#7. 关键函数与细节清单)
    • [8. 测试与运行](#8. 测试与运行)
    • [9. 常见坑位速查](#9. 常见坑位速查)
    • [10. 结语](#10. 结语)
    • 附录:完整代码
        • [文件: `test/Dict.hpp`](#文件: test/Dict.hpp)
        • [文件: `test/MyUdpServer.hpp`](#文件: test/MyUdpServer.hpp)
        • [文件: `test/MyUdpServer.cc`](#文件: test/MyUdpServer.cc)
        • [文件: `test/MyUdpClient.cc`](#文件: test/MyUdpClient.cc)
        • [文件: `test/Makefile`](#文件: test/Makefile)
        • [文件: `test/Log.hpp`](#文件: test/Log.hpp)
        • [文件: `test/Mutex.hpp`](#文件: test/Mutex.hpp)
        • [文件: `test/dictionary.txt`](#文件: test/dictionary.txt)

Linux 上手 UDP Socket 程序编写(含完整具体demo)

本文核心讲解"如何用 UDP socket 进行通信",包括每一步该调用哪些系统调用、参数怎么填、如何设计服务器和客户端的模块与边界、常见坑点与增强方式。字典查询只是业务载体,重点在 UDP 的通信。

1. 核心技术点

  • UDP 的通信模型与适用场景
  • 服务端/客户端从 0 到 1 的完整步骤
  • 关键系统调用的使用细节:socketbindsendtorecvfrom
  • 地址结构 sockaddr_in、字节序转换、IP 转换函数
  • 缓冲区处理、错误处理、阻塞与非阻塞、超时与重试
  • 可扩展的架构设计与实战建议

2. UDP 通信模型速览

  • 无连接(connectionless):没有三次握手,直接收发。开销低、时延小。
  • 不可靠(unreliable):可能丢包、乱序、重复。应用层需要容错(超时、重试、去重)。
  • 保留消息边界(message-oriented):一次 sendto 对应一次 recvfrom,天然"报文"语义,不用像 TCP 处理粘包/拆包。
  • 适合"体量小、请求-应答、可容忍丢包"的场景。本示例查询一次、返回一次,正合适。

3. 构建服务端:步骤与 API

3.1 创建套接字 socket

文件: test/MyUdpServer.hpp

cpp 复制代码
void Init(){
    //1.创建套接字
    _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(_sockfd < 0){
        LOG(LogLevel::FATAL) << "socket error" ;
        exit(1);
    }
    LOG(LogLevel::INFO) << "socket create success, sockfd: " << _sockfd ;
  • 重要说明:

    • domain : AF_INET(IPv4);如需 IPv6 用 AF_INET6
    • type : SOCK_DGRAM(UDP 数据报);TCP 则是 SOCK_STREAM
    • protocol: 一般填 0,由内核根据 type 推断。
    • 返回值 : 成功为非负 fd;失败 -1 并设置 errno(如 EMFILE/ENFILE 资源耗尽)。
    • UDP 默认阻塞模式;如需非阻塞,后续用 fcntl 设置 O_NONBLOCK
  • 可选优化(快速重启端口):

cpp 复制代码
int yes = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
  • SO_REUSEADDR :允许在 TIME_WAIT 等场景快速复用端口,对 UDP 也常用;须在 bind 前设置。

3.2 填充地址并绑定 bind

文件: test/MyUdpServer.hpp

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;

文件: test/MyUdpServer.hpp

cpp 复制代码
int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if( n < 0 ){
    LOG(LogLevel::FATAL) << "bind error";
    exit(2);
}
LOG(LogLevel::INFO) << "bind success, sockfd:" << _sockfd;
  • 重要说明:
    • sockaddr_in 字段:sin_family=AF_INETsin_port 必须 htonssin_addr.s_addr 可设 INADDR_ANY(所有本地网卡)。
    • htons/ntohs 端口字节序转换;IP 推荐用 inet_pton(现代、安全),此处演示 INADDR_ANY
    • bind 将本地地址:端口与套接字关联,服务端必须显式 bind 固定端口。
    • 常见错误:EADDRINUSE 端口占用、EACCES 低号端口权限不足。

3.3 循环收包 recvfrom → 处理 → 回包 sendto

文件: test/MyUdpServer.hpp

cpp 复制代码
while(_isrunning){
    char buffer[1024];
    //1.不断收(读)消息
    ssize_t rec_msg = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr* )&peer, &len);
    if(rec_msg > 0){
        int peer_port = ntohs(peer.sin_port);
        std::string peer_ip = inet_ntoa(peer.sin_addr);

        buffer[rec_msg] = 0;

        std::string result = _func(buffer);
        LOG(LogLevel::DEBUG) << "buffer:" << buffer << " from " << peer_ip << ":" << peer_port;

        // 2.发消息
        ssize_t snd_sz = sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);
    }
}
  • 重要说明(recvfrom):

    • 原型:ssize_t recvfrom(int fd, void* buf, size_t len, int flags, struct sockaddr* src, socklen_t* addrlen)
    • 阻塞读:无数据会阻塞(可设置 SO_RCVTIMEO 或改非阻塞)。
    • 返回值:本次数据报长度;若 len 小于数据报,超出部分被丢弃(UDP 保持报文边界,不会"分多次收")。
    • sizeof(buffer)-1:为字符串结尾 '\0' 预留一字节。
    • peer:回填对端地址,后续回包直接使用。
    • inet_ntoa 返回静态缓冲区的字符串,非线程安全;多线程推荐 inet_ntop
  • 重要说明(sendto):

    • 原型:ssize_t sendto(int fd, const void* buf, size_t len, int flags, const struct sockaddr* dst, socklen_t addrlen)
    • UDP 要么整报成功,要么失败(不会部分发送)。
    • 典型错误:EAGAIN(非阻塞且缓冲区满)、EMSGSIZE(报文过大)、ENETUNREACH/EHOSTUNREACH(网络不可达)。
    • flags 常为 0;除非需要 MSG_DONTWAIT 等特殊行为。
  • 业务解耦:网络层只负责"收/发",实际处理交给回调 _func

3.4 注入业务回调(示例)

文件: test/MyUdpServer.cc

cpp 复制代码
std::unique_ptr<MyUdpServer> usvr = std::make_unique<MyUdpServer>(port, [&dict](const std::string &message){
    return dict.Translate(message);
});
usvr->Init();
usvr->Start();
  • 重要说明:
    • 以函数对象注入业务逻辑(字典翻译/回显/计算器均可),不侵入网络层。
    • 更换业务=替换回调,网络收发与生命周期管理不变。

4. 构建客户端:步骤与 API

4.1 创建套接字 socket

文件: test/MyUdpClient.cc

cpp 复制代码
int sockfd = socket (AF_INET,SOCK_DGRAM,0);
if(sockfd < 0){
    std::cerr << "socket error" << std::endl;
    return 2;
}
  • 重要说明:
    • 客户端通常无需显式 bind;首次 sendto 时内核自动分配临时端口(ephemeral port)。
    • 如需固定本地端口/网卡(多播/防火墙策略等),可主动 bind 到指定本地地址:端口。

4.2 组织对端地址、发送 sendto

文件: test/MyUdpClient.cc

cpp 复制代码
std::string input;
std::cout << "Please Enter# ";
std::getline(std::cin,input);

sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(atoi(argv[2]));
dest_addr.sin_addr.s_addr = inet_addr(argv[1]);

int send_size = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&dest_addr, sizeof(dest_addr));
  • 重要说明:
    • dest_addr 必须是"服务端"的地址;客户端不需要 connect 也能直接 sendto
    • 发送长度必须用实际数据长度:input.size() ,绝不要用 sizeof(std::string)(那是类型大小)。
    • inet_addr 将点分十进制 IP 转为网络序整数;更推荐 inet_pton(AF_INET, argv[1], &dest_addr.sin_addr)(更健壮)。

4.3 接收回复 recvfrom

文件: test/MyUdpClient.cc

cpp 复制代码
char recv_buf[1024];
sockaddr_in peer_addr;
socklen_t peer_addr_len = sizeof(peer_addr);
int recv_size = recvfrom(sockfd,recv_buf,sizeof(recv_buf)-1,0,(struct sockaddr*)&peer_addr,&peer_addr_len);
if(recv_size > 0){
    recv_buf[recv_size] = '\0';
    std::cout << "server# " << recv_buf << std::endl;
}
  • 重要说明:
    • 仍然要用返回值补 '\0',避免打印脏数据。
    • peer_addr 给出实际回应者(在多播/任播/多服务端测试时很有用)。
    • 想要接收超时,可 setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, ...);想非阻塞,用 fcntl + select/epoll

5. 字典 Demo 的实现(作为业务回调接入)

本节只聚焦"字典查询"如何作为业务层接入到 UDP 通信中,帮助你理解网络层与应用层的协作关系。

5.1 字典文件与数据结构

  • 文件:test/dictionary.txt,每行 key:value
  • 结构:unordered_map<string,string>,平均 O(1) 查询。
  • 默认路径:
    文件: test/Dict.hpp
cpp 复制代码
const std::string default_dict_path = "./dictionary.txt";

5.2 加载流程:读取 → 切分 → 裁剪 → 写入 map

文件: test/Dict.hpp

cpp 复制代码
bool Load(){
    std::ifstream ifs(_path);
    if(!ifs.is_open()){
        return false;
    }
    std::string line;
    while(std::getline(ifs, line)){
        // 忽略空行
        if(line.empty()) continue;
        // 查找分隔符
        std::size_t pos = line.find(':');
        if(pos == std::string::npos) continue;

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

        // 去除首尾空白
        auto trim = [](std::string &s){
            auto not_space = [](int ch){ return !std::isspace(ch); };
            s.erase(s.begin(), std::find_if(s.begin(), s.end(), not_space));
            s.erase(std::find_if(s.rbegin(), s.rend(), not_space).base(), s.end());
        };
        trim(key);
        trim(value);

        if(!key.empty() && !value.empty()){
            _dict[key] = value;
        }
    }
    return true;
}
  • 重要说明:
    • std::getline 每次读取一行(不含换行符),适合按行解析。
    • find(':') 只取第一个冒号分隔:支持 定义:内容:附注 这类行时会把后缀全部并入 value
    • substr(pos + 1) 从冒号右侧取值(见当前文件第 31 行),避免把冒号包含进 value
    • trim 使用 std::isspace 去除首尾空白,容忍文件中多余空格/Tab;如编译器报未声明,请 #include <cctype>
    • unordered_map 提供均摊 O(1) 插入/查询;若需有序遍历可换 std::map

5.3 查询接口与不命中返回

文件: test/Dict.hpp

cpp 复制代码
std::string Translate(const std::string &word){
    auto it = _dict.find(word);
    if(it != _dict.end()){
        return it->second;
    }
    return "not found";
}
  • 重要说明:
    • 完全匹配、大小写敏感;可在装载或查询时统一 tolower 实现不敏感匹配。
    • 未命中返回 "not found",服务端原样返回给客户端。

5.4 将业务回调注入服务端

文件: test/MyUdpServer.cc

cpp 复制代码
std::unique_ptr<MyUdpServer> usvr = std::make_unique<MyUdpServer>(port, [&dict](const std::string &message){
    return dict.Translate(message);
});
usvr->Init();
usvr->Start();
  • 重要说明:
    • 这体现"网络 I/O"与"业务处理"分层:更换业务无需触碰 UDP 细节。
    • 回调签名 std::function<std::string(const std::string&)>,输入/输出均为文本,便于替换其他业务。

6. 模块设计与职责划分

  • 网络层(UDP 收发):
    • 负责 socket/bind/recvfrom/sendto,不涉及业务。
    • 把"收到的消息"和"对端地址"传给业务层处理。
  • 业务层(翻译/计算/路由):
    • 输入消息字符串,输出要回的字符串。
    • 在本示例中以回调形式注入网络层,解耦清晰。

文件: test/MyUdpServer.cc

cpp 复制代码
std::unique_ptr<MyUdpServer> usvr = std::make_unique<MyUdpServer>(port, [&dict](const std::string &message){
    return dict.Translate(message);
});
usvr->Init();
usvr->Start();

7. 关键函数与细节清单

  • socket : socket(AF_INET, SOCK_DGRAM, 0);失败返回 -1,查 errno
  • bind: 绑定本地地址:端口;失败多为端口占用或权限问题。
  • sendto/recvfrom: 保留报文边界;UDP 不会"部分发送",要么整报成功要么失败。
  • htons/ntohs、htonl/ntohl: 端口/地址的主机序 ↔ 网络序。
  • inet_addr/inet_ntoa (或 inet_pton/inet_ntop): 文本 ↔ 网络序地址;inet_ntoa 非线程安全。
  • bzero/memset: 清零结构体,避免未初始化字段带来未定义行为。
  • 超时 : setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, ...) 设置接收超时(阻塞模式下生效)。
  • 非阻塞 : fcntl(fd, F_SETFL, O_NONBLOCK) 配合 select/poll/epoll

示例:接收超时 2s(阻塞模式)

cpp 复制代码
timeval tv{.tv_sec = 2, .tv_usec = 0};
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

8. 测试与运行

  • 编译:
bash 复制代码
cd /root/linux/test
make
  • 启动服务端(如 8080):
bash 复制代码
./myudpserver 8080
  • 启动客户端并交互:
bash 复制代码
./myudpclient 127.0.0.1 8080
  • 输入 apple,观察请求-应答链路(验证 UDP 收发、长度、字节序、地址填充)。

9. 常见坑位速查

  • 发送长度错用 sizeof(std::string) → 正确是 input.size()
  • 忘记 htons/ntohs,导致端口错乱
  • recvfrom 后未补 \0,输出脏字符
  • 客户端不 bind 正常;首发 sendto 自动绑定临时端口
  • 字典只是业务层;用回显也能完整验证 UDP 收发

10. 结语

本文把 UDP 的核心系统调用与工程实践一网打尽,示例中的"字典查询"只是把回调接上,让你更直观看见"收到一条请求 → 处理 → 回一条响应"的完整链路。掌握这些 API 的使用细节与常见坑,你就可以自信地实现自己的 UDP 小服务,并按需扩展到更复杂的场景。

  • 核心流程:socket →(服务端 bind)→ sendto/recvfrom
  • 关键细节:正确填 sockaddr_in、字节序转换、长度与缓冲区处理
  • 工程建议:业务与网络解耦、非阻塞与超时机制、应用层容错与可观测性

附录:完整代码

文件: test/Dict.hpp
cpp 复制代码
#pragma once
#include<iostream>
#include<string>
#include<vector>
#include<map>
#include<unordered_map>
#include<algorithm>
#include<fstream>

const std::string default_dict_path = "./dictionary.txt";

class Dict
{
public:
    Dict(const std::string &path = default_dict_path):_path(path)
    {}
    bool Load(){
        std::ifstream ifs(_path);
        if(!ifs.is_open()){
            return false;
        }
        std::string line;
        while(std::getline(ifs, line)){
            // 忽略空行
            if(line.empty()) continue;
            // 查找分隔符
            std::size_t pos = line.find(':');
            if(pos == std::string::npos) continue;

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

            // 去除首尾空白
            auto trim = [](std::string &s){
                auto not_space = [](int ch){ return !std::isspace(ch); };
                s.erase(s.begin(), std::find_if(s.begin(), s.end(), not_space));
                s.erase(std::find_if(s.rbegin(), s.rend(), not_space).base(), s.end());
            };
            trim(key);
            trim(value);

            if(!key.empty() && !value.empty()){
                _dict[key] = value;
            }
        }
        return true;
    }

    std::string Translate(const std::string &word){
        auto it = _dict.find(word);
        if(it != _dict.end()){
            return it->second;
        }
        return "not found";
    }

private:
    std::string _path;
    std::unordered_map<std::string, std::string> _dict;
};
文件: test/MyUdpServer.hpp
cpp 复制代码
#pragma once

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

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

const int defaultfd = -1;

class MyUdpServer{
public:
    MyUdpServer(uint16_t port, func_t func)
     :_sockfd(defaultfd),
    //   _ip(ip),
      _port(port),
      _func(func)
    {}

    void Init(){
        //1.创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(_sockfd < 0){
            LOG(LogLevel::FATAL) << "socket error" ;
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket create success, sockfd: " << _sockfd ;
        
        //绑定socket信息,ip和端口
        //填充sockaddr_in结构体
        struct sockaddr_in local;
        bzero(&local,sizeof(local)); //一个用于内存初始化的函数,主要功能是将指定内存区域的所有字节设置为 0。
        local.sin_family = AF_INET;

        //将端口从本地格式转换成网络序列
        local.sin_port = htons(_port);
        //将IP从string类型转化为4个字节存储,再转化为网络序列:
        //转成in_addr_t类型 inet_addr (const char *cp)
        // local.sin_addr.s_addr = inet_addr(_ip.c_str());
        // 不用上面的绑定具体ip,使用INADDR_ANY,表示绑定所有网卡
        local.sin_addr.s_addr = INADDR_ANY;
        
    
        //绑定套接字
        int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if( n < 0 ){
            LOG(LogLevel::FATAL) << "bind error";
            exit(2);
        }
        //绑定成功,输出日志
        LOG(LogLevel::INFO) << "bind success, sockfd:" << _sockfd;

        
    }
    void Start(){
        //因为udp不用管链接,一直管收发就好了
        _isrunning = true;
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        while(_isrunning){
            char buffer[1024];
            //1.不断收(读)消息
            ssize_t rec_msg = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr* )&peer, &len);
            //sizeof(buffer)-1 是为了留一个位置将来添加 \n
            if(rec_msg > 0){
                //说明收到了消息
                int peer_port = ntohs(peer.sin_port);
                std::string peer_ip = inet_ntoa(peer.sin_addr);

                buffer[rec_msg] = 0;

                std::string result = _func(buffer);
                LOG(LogLevel::DEBUG) << "buffer:" << buffer << " from " << peer_ip << ":" << peer_port;

                // 2.发消息
                // std::string snd_msg = "receive message from server:";
                // snd_msg += buffer;
                // ssize_t snd_sz = sendto(_sockfd, snd_msg.c_str(), snd_msg.size(), 0, (struct sockaddr *)&peer, len);
                ssize_t snd_sz = sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);

            }
        }
    }

private:
    int _sockfd;
    uint16_t _port;  // 就是unsigned short int类型重定义了
    // std::string _ip; // 这里使用字符串风格存储点分十进制的ip(比如1234.1.2.3)后需要转换成网络字节序
    bool _isrunning;
    func_t _func;   //  服务器回调函数,用来对数据进行处理
};
文件: test/MyUdpServer.cc
cpp 复制代码
#include "MyUdpServer.hpp"
#include "Dict.hpp"
#include <memory>
#include <iostream>


// ./myudpserver ip port
int main(int argc, char* argv[]){
    if(argc != 2){
        std::cerr << "Usage:" << argv[0] << " + port " << std::endl;
        return 1;
    }
    // std::string ip = argv[1];
    uint16_t port = std::stoi(argv[1]);
    Enable_Console_Log_Strategy();

    //字典对象提供翻译功能
    Dict dict;
    dict.Load();

    std::unique_ptr<MyUdpServer> usvr = std::make_unique<MyUdpServer>(port, [&dict](const std::string &message){
        return dict.Translate(message);
    });
    usvr->Init();
    usvr->Start();
    return 0;
}
文件: test/MyUdpClient.cc
cpp 复制代码
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

// ./myudpclient server_ip server_port
int main(int argc, char* argv[]){
    if(argc != 3){
        std::cerr << "Usage:" << argv[0] << " + server_ip + server_port + " << std::endl;
        return 1;
    }
    
    //1.创建套接字
    int sockfd = socket (AF_INET,SOCK_DGRAM,0);
    if(sockfd < 0){
        std::cerr << "socket error" << std::endl;
        return 2;
    }
    //2.绑定。
    //客户端需要进行绑定,但是不需要我们手动显式地进行bind绑定
    //当首次发送消息的时候,os会自动给客户端进行绑定。
    //端口号采用随机端口号
    while(1){
        std::string input;
        std::cout << "Please Enter# ";
        std::getline(std::cin,input);
        sockaddr_in dest_addr;
        dest_addr.sin_family = AF_INET;
        dest_addr.sin_port = htons(atoi(argv[2]));
        dest_addr.sin_addr.s_addr = inet_addr(argv[1]);
        socklen_t addrlen = sizeof(dest_addr);

        int send_size = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&dest_addr, addrlen);
    
        char recv_buf[1024];
        sockaddr_in peer_addr;
        socklen_t peer_addr_len = sizeof(peer_addr);
        int recv_size = recvfrom(sockfd,recv_buf,sizeof(recv_buf)-1,0,(struct sockaddr*)&peer_addr,&peer_addr_len);
        if(recv_size > 0){
            recv_buf[recv_size] = '\0';
            std::cout << "server# " << recv_buf << std::endl;
        }
    }


    return 0;
}
文件: test/Makefile
cpp 复制代码
.PHONY: all clean

# 定义生成的可执行文件目标
all: myudpserver myudpclient

# 编译服务端
myudpserver: MyUdpServer.cc
	g++ -o $@ $^ -std=c++17

# 编译客户端
myudpclient: MyUdpClient.cc
	g++ -o $@ $^ -std=c++17

# 清理生成的可执行文件
clean:
	rm -f myudpserver myudpclient
文件: test/Log.hpp
cpp 复制代码
#pragma once
#include<iostream>
#include<sstream>
#include<string>
#include<ctime>
#include<unistd.h>

#include<pthread.h>

#include"Mutex.hpp"

namespace LogModule
{

enum class LogLevel
{
        DEBUG = 0,
        INFO,
        WARNING,
        ERROR,
        FATAL
};

const char* LogLevelToString(LogLevel level)
{
        switch(level)
        {
        case LogLevel::DEBUG:
                return "DEBUG";
        case LogLevel::INFO:
                return "INFO";
        case LogLevel::WARNING:
                return "WARNING";
        case LogLevel::ERROR:
                return "ERROR";
        case LogLevel::FATAL:
                return "FATAL";
        default:
                return "UNKNOWN";
        }
}

class Log
{
public:
        Log(LogLevel level, const char* file_name, int line)
                : _level(level), _file_name(file_name), _line(line)
        {}

        ~Log()
        {
                std::string log_str = __Stream.str();

                time_t timestamp;
                time(&timestamp);
                struct tm* tm_time = localtime(&timestamp);
                char time_buffer[128] = {0};
                snprintf(time_buffer, sizeof(time_buffer), "%04d-%02d-%02d %02d:%02d:%02d", 
                        tm_time->tm_year + 1900, tm_time->tm_mon + 1, tm_time->tm_mday, 
                        tm_time->tm_hour, tm_time->tm_min, tm_time->tm_sec);

                //拼接
                std::stringstream _log_stream;
                _log_stream << "[" << time_buffer << "] ";
                _log_stream << "[" << LogLevelToString(_level) << "] ";
                _log_stream << "[" << getpid() << "] ";
                _log_stream << "[" << _file_name << "] ";
                _log_stream << "[" << _line  << "] - ";
                _log_stream << log_str;

                std::string log_string = _log_stream.str();

                //输出
                if(_consoleswitch)
                        std::cout << log_string << std::endl;
                else if(_fileswitch)
                {
                        _mutex.Lock();
                        FileWriteLog(log_string);
                        _mutex.Unlock();
                }
        }
public:
        std::ostream& Stream(){ return __Stream; }
private:
        void FileWriteLog(const std::string& log_string, bool create_new_file = false)
        {
                const std::string default_log_file = "./log.txt";

                //打开文件
                FILE* fp = nullptr;
                if(create_new_file){
                        fp = fopen(default_log_file.c_str(), "w");
                }else{
                        fp = fopen(default_log_file.c_str(), "a");
                }
                
                if(fp == nullptr){
                        std::cerr << "FileWriteLog::open " << default_log_file << " error" << std::endl;
                        return;
                }

                //写入,换行
                log_string;
                fprintf(fp, "%s\n", log_string.c_str());

                //关闭文件
                fclose(fp);
        }

private:
        LogLevel _level;
        const char* _file_name;
        int _line;
        std::stringstream __Stream;
private:
        //控制输出方式
        static bool _consoleswitch;
        static bool _fileswitch;
        //文件锁
        static Mutex _mutex;
};

bool Log::_consoleswitch = true;
bool Log::_fileswitch = false;
Mutex Log::_mutex;


Log LogMessage(LogLevel level, const char* file_name, int line)
{
        return Log(level, file_name, line);
}

#define LOG(level) LogMessage(level, __FILE__, __LINE__).Stream()

//5种日志输出策略
void Enable_Console_Log_Strategy()
{
        Log::_consoleswitch = true;
        Log::_fileswitch = false;
}
void Enable_File_Log_Strategy()
{
        Log::_consoleswitch = false;
        Log::_fileswitch = true;
}
void Enable_Dual_Log_Strategy()
{
        Log::_consoleswitch = true;
        Log::_fileswitch = true;
}
void Disable_Log_Strategy()
{
        Log::_consoleswitch = false;
        Log::_fileswitch = false;
}


// #ifdef DEBUG
// #define LOG(level) DebugLog(level, __FILE__, __LINE__)
// #else
// #define LOG(level) 1 ? (void) 0 : LogMessage(level, __FILE__, __LINE__).Stream()
// #endif

}
文件: test/Mutex.hpp
cpp 复制代码
#pragma once

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

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

class LockGuard
{
public:
    LockGuard(Mutex& mutex)
        :_mutex(mutex)
    {
        _mutex.Lock();
    }
    ~LockGuard()
    {
        _mutex.Unlock();
    }
private:
    Mutex& _mutex;
};
文件: test/dictionary.txt
复制代码
apple:苹果
banana:香蕉
orange:橙子
grape:葡萄
watermelon:西瓜
strawberry:草莓
peach:桃子
pear:梨
pineapple:菠萝
mango:芒果
lemon:柠檬
cherry:樱桃
blueberry:蓝莓
kiwi:奇异果
tomato:西红柿
potato:土豆
carrot:胡萝卜
cucumber:黄瓜
onion:洋葱
garlic:大蒜
ginger:生姜
rice:大米
noodles:面条
bread:面包
milk:牛奶
egg:鸡蛋
meat:肉
beef:牛肉
pork:猪肉
chicken:鸡肉
fish:鱼
shrimp:虾
tofu:豆腐
vegetable:蔬菜
fruit:水果
water:水
tea:茶
coffee:咖啡
juice:果汁
sugar:糖
salt:盐
oil:油
pepper:胡椒
computer:电脑
phone:手机
keyboard:键盘
mouse:鼠标
screen:屏幕
internet:互联网
book:书
pen:笔
paper:纸
table:桌子
chair:椅子
window:窗户
door:门
house:房子
car:汽车
bus:公交车
bicycle:自行车

thread_mutex_t _mutex;

};

class LockGuard

{

public:

LockGuard(Mutex& mutex)

:_mutex(mutex)

{

_mutex.Lock();

}

~LockGuard()

{

_mutex.Unlock();

}

private:

Mutex& _mutex;

};

复制代码
#### 文件: `test/dictionary.txt`

apple:苹果

banana:香蕉

orange:橙子

grape:葡萄

watermelon:西瓜

strawberry:草莓

peach:桃子

pear:梨

pineapple:菠萝

mango:芒果

lemon:柠檬

cherry:樱桃

blueberry:蓝莓

kiwi:奇异果

tomato:西红柿

potato:土豆

carrot:胡萝卜

cucumber:黄瓜

onion:洋葱

garlic:大蒜

ginger:生姜

rice:大米

noodles:面条

bread:面包

milk:牛奶

egg:鸡蛋

meat:肉

beef:牛肉

pork:猪肉

chicken:鸡肉

fish:鱼

shrimp:虾

tofu:豆腐

vegetable:蔬菜

fruit:水果

water:水

tea:茶

coffee:咖啡

juice:果汁

sugar:糖

salt:盐

oil:油

pepper:胡椒

computer:电脑

phone:手机

keyboard:键盘

mouse:鼠标

screen:屏幕

internet:互联网

book:书

pen:笔

paper:纸

table:桌子

chair:椅子

window:窗户

door:门

house:房子

car:汽车

bus:公交车

bicycle:自行车

复制代码
> 本附录覆盖项目目录下与本示例相关的全部源码与资源,便于直接编译运行与对照阅读。
相关推荐
木古古181 小时前
搞一个高效的c/c++开发环境,工具VIm+自研vim插件+Shell脚本
linux·编辑器·vim
茫忙然2 小时前
U 盘搭建免驱 Linux 便携系统教程
linux·服务器
一起逃去看海吧3 小时前
dify-03
java·linux·开发语言
fengyehongWorld3 小时前
Linux 根据端口进行的相关查询
linux
lihao lihao3 小时前
linux匿名管道
linux·运维·服务器
うちは止水3 小时前
weston出图调试
linux·wayland·weston
STDD3 小时前
Farming Simulator 25(模拟农场 25) Linux 专服搭建完全指南
linux·运维·javascript
好好风格4 小时前
宝塔面板 HTTPS 端口证书不生效排查记录
linux·运维·nginx
Forget_85504 小时前
HCIA——计算机网络诞生与发展
服务器·网络·计算机网络