欢迎光临
我们一直在努力

【Linux网络】Linux 网络编程入门:UDP Socket 编程(下)

头像

🎬 个人主页:艾莉丝努力练剑

❄专栏传送门:《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》 《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》

⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平


🎬 艾莉丝的简介:

在这里插入图片描述


文章目录

  • 1 ~> UDP编程:三个代码案例搞定
  • 2 ~> 先搞透基础:UDP Socket核心原理全景
    • 2.1 Socket的本质:操作系统提供的网络抽象
    • 2.2 核心数据结构:sockaddr\\_in与地址转换
      • 2.2.1 地址转换函数
    • 2.3 字节序:为什么必须处理大小端?
    • 2.4 bind函数深度解析
    • 2.5 客户端为什么不需要显式bind?
  • 3 ~> V1版本:Echo回显服务器
    • 3.1 完整代码实现
      • 3.1.1 Mutex\\.hpp(互斥锁封装)
      • 3.1.2 Logger\\.hpp(日志模块)
      • 3.1.3 UdpEchoServer\\.hpp(核心逻辑)
      • 3.1.4 main\\.cpp(程序入口)
    • 3.2 编译与运行
      • 3.2.1 Makefile
      • 3.2.2 运行命令
      • 3.2.3 客户端测试
    • 3.3 V1版本的局限性
  • 4 ~> V2版本:增强版英译汉字典服务器(生产级解耦)
    • 4.1 核心改进:生产级工程化能力
    • 4.2 完整代码实现
      • 4.2.1 Mutex\\.hpp(复用V1版本)
      • 4.2.2 Logger\\.hpp(复用V1版本)
      • 4.2.3 字典业务模块:Dictionary\\.hpp
      • 4.2.4 字典数据文件:Dict\\.txt
      • 4.2.5 通用UDP服务器:UdpServer\\.hpp
      • 4.2.6 服务器主函数:Main\\.cc
    • 4.3 客户端代码(通用版)
    • 4.4 编译与运行
      • 4.4.1 Makefile
      • 4.4.2 运行步骤
      • 4.5 V2版本优势总结
  • 5 ~> V3版本:多人聊天室(生产级并发)
    • 5.1 整体架构:生产者 \\- 消费者模型
    • 5.2 核心模块补充
      • 5.2.1 Cond\\.hpp(条件变量封装)
      • 5.2.2 Thread\\.hpp(线程封装)
      • 5.2.3 InetAddr\\.hpp(地址封装)
      • 5.2.4 Route\\.hpp(消息路由)
      • 5.2.5 UdpServer\\.hpp(聊天室专用版)
      • 5.2.6 ThreadPool\\.hpp(懒汉单例线程池)
    • 5.3 主函数整合
    • 5.4 编译与运行
      • 5.4.1 Makefile
      • 5.4.2 运行步骤
    • 5.5 客户端实现:多线程收发分离
      • 5.5.1 Linux客户端
  • 6 ~> 工程化实践与踩坑总结
    • 6.1 日志系统使用指南
    • 6.2 UDP编程踩坑总结
      • 6.2.1 基础API踩坑
      • 6.2.2 多线程踩坑
      • 6.2.3 业务逻辑踩坑
    • 6.3 客户端访问服务端,云服务器需要开放一些端口
  • 7 ~> 总结与后续优化方向
    • 7.1 本文总结
    • 7.2 后续优化方向
  • 附录:完整项目文件结构
  • 结尾

在这里插入图片描述


1 ~> UDP编程:三个代码案例搞定

很多人学UDP编程只停留在"能发能收"的Echo服务器阶段,但真实的网络服务需要解决多用户并发、业务解耦、性能瓶颈、跨平台兼容四大核心问题。本文将带你走完UDP编程的完整学习路线:

版本功能解决的核心问题核心技术点
V1 Echo回显服务器 基础Socket收发、地址转换、端口绑定 socket()/bind()/recvfrom()/sendto()
V2 增强版英译汉字典服务器 网络层与业务层解耦、生产级日志、配置文件加载 回调函数、策略模式日志、文件IO
V3 多人聊天室 多用户管理、并发优化、跨平台通信 生产者-消费者模型、线程池、多线程收发分离

本文所有代码均基于C++17标准,兼容Linux与Windows平台,所有代码均来自真实生产实践,可直接编译运行,如果有问题,简单调试,通过修改参数、调整文件名、检查构造函数初始化顺序等方式可以矫正。


2 ~> 先搞透基础:UDP Socket核心原理全景

2.1 Socket的本质:操作系统提供的网络抽象

  • 定义:Socket是应用层与TCP/IP协议栈之间的编程接口,本质是一个特殊的文件描述符

  • 类比:Socket = 网络电话,IP = 手机号,端口 = 分机号

  • 核心特性:UDP是无连接、不可靠、面向数据报的协议,支持全双工通信

2.2 核心数据结构:sockaddr_in与地址转换

// IPv4地址结构体
struct sockaddr_in {
short int sin_family; // 地址族,固定为AF_INET
unsigned short int sin_port; // 16位端口号(网络字节序)
struct in_addr sin_addr; // 32位IP地址(网络字节序)
unsigned char sin_zero[8]; // 填充字节
};

2.2.1 地址转换函数

函数功能线程安全推荐场景
inet\\_addr() 点分十进制 → 网络序IP 简单场景
inet\\_ntoa() 网络序IP → 点分十进制 单线程环境
inet\\_pton() 点分十进制 → 网络序IP(支持IPv6) 多线程/生产环境
inet\\_ntop() 网络序IP → 点分十进制(支持IPv6) 多线程/生产环境

坑人点:inet_ntoa()使用静态缓冲区存储结果,多线程调用会互相覆盖,生产环境必须使用inet_ntop()。

2.3 字节序:为什么必须处理大小端?

  • 大端序:高位字节存低地址,网络字节序统一使用大端

  • 小端序:低位字节存低地址,x86/x86_64架构CPU默认使用小端

  • 核心转换函数:

    • htons():主机序 → 网络序(短整型,用于端口)

    • ntohs():网络序 → 主机序(短整型)

    • htonl()/ntohl():长整型转换(用于IP地址)

2.4 bind函数深度解析

  • 作用:将Socket与指定的IP和端口绑定,告诉操作系统"这个Socket负责接收发往该地址的数据包"

  • 为什么服务器必须显式bind?:服务器端口必须是固定且众所周知的,客户端才能连接

  • 为什么推荐绑定 0.0.0.0 ?:

    • 0.0.0.0表示监听本机所有网卡的所有IP地址

    • 云服务器网卡上没有配置公网IP(公网IP是运营商NAT映射的),直接绑定公网IP会失败

  • 常见bind失败原因:端口被占用、权限不足(1024以下端口需要root)、绑定不存在的IP

2.5 客户端为什么不需要显式bind?

  • 客户端调用sendto()时,操作系统会自动为其分配一个随机的未被占用的端口

  • 客户端数量众多,不需要固定端口,自动分配可以避免端口冲突


3 ~> V1版本:Echo回显服务器

3.1 完整代码实现

3.1.1 Mutex.hpp(互斥锁封装)

#ifndef __MUTEX_HPP
#define __MUTEX_HPP

#include <pthread.h>

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

// 自动加解锁的Guard(RAII风格)
class LockGuard
{
public:
LockGuard(Mutex *lockp): _lockp(lockp)
{
_lockp->Lock();
}
~LockGuard()
{
_lockp->Unlock();
}
private:
Mutex *_lockp;
};

#endif

3.1.2 Logger.hpp(日志模块)

#ifndef __LOGGER_HPP
#define __LOGGER_HPP

#include <iostream>
#include <cstdio>
#include <string>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <memory>
#include <unistd.h>
#include <cstdlib>
#include "Mutex.hpp"

namespace LogModule
{
std::string GetTimeStamp()
{
time_t timestamp = time(nullptr);
struct tm data_time;
localtime_r(&timestamp, &data_time);

char data_time_str[128];
snprintf(data_time_str, sizeof(data_time_str), "%4d-%02d-%02d %02d:%02d:%02d",
data_time.tm_year + 1900,
data_time.tm_mon + 1,
data_time.tm_mday,
data_time.tm_hour,
data_time.tm_min,
data_time.tm_sec);

return data_time_str;
}

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

std::string LogLevel2String(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 LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &logmessage) = 0;
};

class ConsoleLogStrategy : public LogStrategy
{
public:
void SyncLog(const std::string &logmessage) override
{
LockGuard lockguard(&_mutex);
std::cout << logmessage << std::endl;
}

private:
Mutex _mutex;
};

static const std::string glogdir = "./log/";
static const std::string glogfilename = "log.txt";
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &dir = glogdir, const std::string &filename = glogfilename)
: _logdir(dir), _logfilename(filename)
{
LockGuard lockguard(&_mutex);
if (!std::filesystem::exists(_logdir))
{
try
{
std::filesystem::create_directories(_logdir);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << "创建日志目录失败: " << e.what() << '\\n';
}
}
}

void SyncLog(const std::string &logmessage) override
{
LockGuard lockguard(&_mutex);
std::string target = _logdir + _logfilename;
std::ofstream out(target, std::ios::app);
if (!out.is_open())
{
std::cerr << "打开日志文件失败: " << target << std::endl;
return;
}
out << logmessage << "\\n";
out.close();
}

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

class Logger
{
public:
Logger()
{
UseConsoleLogStrategy();
}

void UseConsoleLogStrategy()
{
_strategy = std::make_unique<ConsoleLogStrategy>();
}

void UseFileLogStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}

class LogMessage
{
public:
LogMessage(LogLevel level, std::string filename, int line, Logger &self)
: _level(level),
_curr_time(GetTimeStamp()),
_pid(getpid()),
_filename(std::move(filename)),
_line(line),
_logger(self)
{
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << LogLevel2String(_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()
{
if (_logger._strategy)
{
_logger._strategy->SyncLog(_loginfo);
}
if (_level == LogLevel::FATAL)
{
exit(EXIT_FAILURE);
}
}

private:
LogLevel _level;
std::string _curr_time;
pid_t _pid;
std::string _filename;
int _line;
std::string _loginfo;
Logger &_logger;
};

LogMessage operator()(LogLevel level, std::string filename, int line)
{
return LogMessage(level, std::move(filename), line, *this);
}

private:
std::unique_ptr<LogStrategy> _strategy;
};

Logger logger;

#define LOG(level) logger(level, __FILE__, __LINE__)
#define ENABLE_CONSOLE_LOG() logger.UseConsoleLogStrategy()
#define ENABLE_FILE_LOG() logger.UseFileLogStrategy()
}

#endif

3.1.3 UdpEchoServer.hpp(核心逻辑)

#ifndef __UDP_ECHOSERVER_HPP
#define __UDP_ECHOSERVER_HPP

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

using namespace LogModule;

class UdpEchoServer
{
public:
UdpEchoServer(uint16_t port)
: _sockfd(1), _port(port)
{
}

void Init()
{
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "创建Socket失败!";
exit(EXIT_FAILURE);
}
LOG(LogLevel::INFO) << "创建Socket成功,fd: " << _sockfd;

struct sockaddr_in local;
std::memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;

if (bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
{
LOG(LogLevel::FATAL) << "绑定端口[" << _port << "]失败!";
exit(EXIT_FAILURE);
}
LOG(LogLevel::INFO) << "绑定端口[" << _port << "]成功!";
}

void Start()
{
LOG(LogLevel::INFO) << "UDP Echo Server 启动成功,监听端口: " << _port;

char buffer[1024] = {0};
struct sockaddr_in peer;
socklen_t peer_len = sizeof(peer);

while (true)
{
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer)1, 0,
(struct sockaddr *)&peer, &peer_len);
if (n < 0)
{
LOG(LogLevel::WARNING) << "接收数据失败!";
continue;
}

buffer[n] = '\\0';
std::string client_ip = inet_ntoa(peer.sin_addr);
uint16_t client_port = ntohs(peer.sin_port);

LOG(LogLevel::INFO) << "收到客户端[" << client_ip << ":" << client_port
<< "]消息: " << buffer;

std::string echo_msg = "[Echo] " + std::string(buffer);

sendto(_sockfd, echo_msg.c_str(), echo_msg.size(), 0,
(struct sockaddr *)&peer, peer_len);
}
}

~UdpEchoServer()
{
if (_sockfd >= 0)
{
close(_sockfd);
LOG(LogLevel::INFO) << "关闭Socket,fd: " << _sockfd;
}
}

private:
int _sockfd;
uint16_t _port;
};

#endif

3.1.4 main.cpp(程序入口)

#include "UdpEchoServer.hpp"
#include <iostream>

void Usage(const char *progname)
{
std::cout << "Usage: " << progname << " <port>" << std::endl;
std::cout << "Example: " << progname << " 8080" << std::endl;
}

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

uint16_t port = atoi(argv[1]);
if (port <= 1024 || port > 65535)
{
std::cerr << "端口必须在 1025~65535 之间!" << std::endl;
return EXIT_FAILURE;
}

// ENABLE_FILE_LOG(); // 可选:切换到文件日志

UdpEchoServer server(port);
server.Init();
server.Start();

return EXIT_SUCCESS;
}

3.2 编译与运行

3.2.1 Makefile

CC = g++
STD = -std=c++17
CFLAGS = -Wall -g -lpthread

all: udp_echo_server

udp_echo_server: main.cpp
$(CC) -o $@ $^ $(STD) $(CFLAGS)

.PHONY: clean
clean:
rm -f udp_echo_server
rm -rf log/

3.2.2 运行命令

make
./udp_echo_server 8080

3.2.3 客户端测试

nc -u 127.0.0.1 8080
# 输入任意字符,会收到服务器回显

3.3 V1版本的局限性

  • 网络层与业务逻辑强耦合,无法复用

  • 只能处理单客户端,不支持多用户

  • 基础日志,无生产级错误处理

  • 单线程模型,性能瓶颈明显


4 ~> V2版本:增强版英译汉字典服务器(生产级解耦)

4.1 核心改进:生产级工程化能力

相比基础版字典服务器,本版本增加了:

  • 策略模式日志系统:支持控制台/文件双日志输出,线程安全

  • 配置文件加载:从外部txt文件加载字典数据,支持动态扩展

  • 完善的错误处理:文件打开失败、格式错误等异常处理

  • 增强字典内容:每个单词包含中文翻译+英文例句+中文例句

4.2 完整代码实现

4.2.1 Mutex.hpp(复用V1版本)

代码同V1版本,此处省略。

4.2.2 Logger.hpp(复用V1版本)

代码同V1版本,此处省略。

4.2.3 字典业务模块:Dictionary.hpp

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

std::string defaultdict = "./Dict.txt";
std::string gsep = ": ";

using namespace LogModule;

class Dictionary
{
private:
void LoadConf()
{
std::ifstream in(_dictfilename);
if (!in.is_open())
{
LOG(LogLevel::FATAL) << "Open dictionary file error: " << _dictfilename;
exit(1);
}

std::string line;
int line_num = 0;
while (std::getline(in, line))
{
line_num++;
if (line.empty()) continue;

auto pos = line.find(gsep);
if (pos == std::string::npos)
{
LOG(LogLevel::WARNING) << "Line " << line_num << " format error, skip: " << line;
continue;
}

std::string key = line.substr(0, pos);
std::string value = line.substr(pos + gsep.size());
_dictmap.insert({key, value});
}

in.close();
LOG(LogLevel::INFO) << "Load dictionary success, total " << _dictmap.size() << " words";
}

public:
Dictionary(const std::string &dictfilename = defaultdict)
: _dictfilename(dictfilename)
{
LoadConf();
}

std::string Translate(const std::string &word)
{
auto iter = _dictmap.find(word);
if(iter == _dictmap.end())
{
return "未知单词";
}
return iter->second;
}

~Dictionary() {}

private:
std::string _dictfilename;
std::unordered_map<std::string, std::string> _dictmap;
};

4.2.4 字典数据文件:Dict.txt

apple: 苹果 – I eat an apple every day. / 我每天吃一个苹果。
banana: 香蕉 – The monkey is eating a banana. / 猴子正在吃香蕉。
cat: 猫 – My cat likes to sleep on the sofa. / 我的猫喜欢在沙发上睡觉。
dog: 狗 – She takes her dog for a walk every morning. / 她每天早上带她的狗去散步。
book: 书 – This book is very interesting. / 这本书非常有趣。
pen: 笔 – May I use your pen? / 我可以用一下你的笔吗?
happy: 快乐的 – She looks very happy today. / 她今天看起来很快乐。
sad: 悲伤的 – He felt sad when he lost his watch. / 他丢了手表时感到很悲伤。
run: 跑 – I run fast in the park. / 我在公园里跑得很快。
jump: 跳 – The child can jump high. / 这个孩子能跳得很高。
teacher: 老师 – Our teacher is very kind. / 我们的老师非常和蔼。
student: 学生 – He is a hardworking student. / 他是一个勤奋的学生。
car: 汽车 – His car is very fast. / 他的汽车非常快。
bus: 公交车 – I go to school by bus. / 我乘公交车上学。
love: 爱 – I love my family. / 我爱我的家人。
hate: 恨 – I hate getting up early. / 我讨厌早起。
hello: 你好 – Hello, nice to meet you! / 你好,很高兴认识你!
goodbye: 再见 – Goodbye, see you tomorrow! / 再见,明天见!
summer: 夏天 – Summer is my favorite season. / 夏天是我最喜欢的季节。
winter: 冬天 – It is very cold in winter. / 冬天非常冷。

4.2.5 通用UDP服务器:UdpServer.hpp

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

using namespace LogModule;

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

class UdpServer
{
public:
UdpServer(callback_t cb, uint16_t port)
: _sockfd(1),
_port(port),
_cb(cb)
{
}

void Init()
{
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "Create socket error";
exit(1);
}
LOG(LogLevel::INFO) << "Create socket success, fd: " << _sockfd;

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;

int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "Bind socket error";
exit(2);
}
LOG(LogLevel::INFO) << "Bind socket success, port: " << _port;
}

void Start()
{
LOG(LogLevel::INFO) << "Udp Dictionary Server start running…";
char inbuffer[1024];
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);

ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) 1, 0,
(struct sockaddr *)&peer, &len);
if (n < 0)
{
LOG(LogLevel::WARNING) << "Recvfrom error, errno: " << errno;
continue;
}

inbuffer[n] = '\\0';
std::string clientip = inet_ntoa(peer.sin_addr);
uint16_t clientport = ntohs(peer.sin_port);
LOG(LogLevel::INFO) << "Received from [" << clientip << ":" << clientport
<< "]: " << inbuffer;

std::string result;
if(_cb)
{
result = _cb(inbuffer);
}

sendto(_sockfd, result.c_str(), result.size(), 0,
(struct sockaddr *)&peer, len);
LOG(LogLevel::DEBUG) << "Sent response to [" << clientip << ":" << clientport
<< "]: " << result;
}
}

~UdpServer()
{
if (_sockfd >= 0)
{
close(_sockfd);
}
}

private:
int _sockfd;
uint16_t _port;
callback_t _cb;
};

#endif

4.2.6 服务器主函数:Main.cc

#include "UdpServer.hpp"
#include "Dictionary.hpp"
#include <memory>

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

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

ENABLE_CONSOLE_LOG_STRATEGY();
// ENABLE_FILE_LOG_STRATEGY();

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

std::unique_ptr<Dictionary> dict = std::make_unique<Dictionary>();

std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(
[&dict](const std::string &word) -> std::string {
return dict->Translate(word);
},
port
);

usvr->Init();
usvr->Start();

return 0;
}

4.3 客户端代码(通用版)

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

void Usage(const std::string &process)
{
std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}

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

std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);

int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "Create socket error: " << strerror(errno) << std::endl;
return 2;
}

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());

std::cout << "=== UDP Dictionary Client ===" << std::endl;
std::cout << "Enter 'quit' to exit" << std::endl;

std::string word;
while (true)
{
std::cout << "\\nPlease enter a word: ";
std::getline(std::cin, word);

if (word == "quit" || word == "exit")
{
std::cout << "Goodbye!" << std::endl;
break;
}

if (word.empty()) continue;

sendto(sock, word.c_str(), word.size(), 0,
(struct sockaddr*)&server, sizeof(server));

char buffer[1024];
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t n = recvfrom(sock, buffer, sizeof(buffer)1, 0,
(struct sockaddr*)&temp, &len);
if (n > 0)
{
buffer[n] = '\\0';
std::cout << "Result: " << buffer << std::endl;
}
else
{
std::cerr << "Recvfrom error" << std::endl;
}
}

close(sock);
return 0;
}

4.4 编译与运行

4.4.1 Makefile

CC = g++
STD = -std=c++17
CFLAGS = -Wall -g -lpthread -lstdc++fs

all: dict_server dict_client

dict_server: Main.cc
$(CC) -o $@ $^ $(STD) $(CFLAGS)

dict_client: DictClient.cc
$(CC) -o $@ $^ $(STD) $(CFLAGS)

.PHONY: clean
clean:
rm -f dict_server dict_client
rm -rf log/

4.4.2 运行步骤

  • 确保所有文件在同一目录下

  • 执行make编译

  • 运行服务器:\\./dict\\_server 8888

  • 运行客户端:\\./dict\\_client 127\\.0\\.0\\.1 8888

  • 输入单词查询翻译,输入quit退出

  • 4.5 V2版本优势总结

    • 完全解耦:网络层与业务层完全分离,UdpServer可复用

    • 生产级日志:支持控制台/文件双输出,线程安全

    • 可扩展性强:字典数据从外部文件加载,无需修改代码即可扩展

    • 完善的错误处理:文件错误、网络错误、格式错误均有处理

    • 线程安全:所有共享资源访问均加锁保护


    5 ~> V3版本:多人聊天室(生产级并发)

    5.1 整体架构:生产者 – 消费者模型

    我们采用经典的生产者-消费者分层架构,解决单线程性能瓶颈:

    • 生产者层:UdpServer主线程,专门负责接收客户端UDP数据包(IO密集型,阻塞在recvfrom)

    • 缓冲层:线程池内部的线程安全任务队列,解耦生产和消费速度,削峰填谷

    • 消费者层:线程池中的多个工作线程,负责用户管理、消息路由和广播(CPU密集型)

    5.2 核心模块补充

    5.2.1 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)
    {
    pthread_cond_wait(&_cond, mutex.Orgin());
    }

    void NotifyOne()
    {
    pthread_cond_signal(&_cond);
    }

    void NotifyAll()
    {
    pthread_cond_broadcast(&_cond);
    }

    ~Cond()
    {
    pthread_cond_destroy(&_cond);
    }

    private:
    pthread_cond_t _cond;
    };

    #endif

    5.2.2 Thread.hpp(线程封装)

    #ifndef __THREAD_HPP
    #define __THREAD_HPP
    #include <iostream>
    #include <string>
    #include <functional>
    #include <pthread.h>
    #include <unistd.h>
    #include <sys/syscall.h>
    #include "Logger.hpp"

    using namespace LogModule;

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

    enum class TSTATUS
    {
    THREAD_NEW,
    THREAD_RUNNING,
    THREAD_STOP
    };

    static int gcnt = 1;

    class Thread
    {
    private:
    void getprocessid()
    {
    _pid = getpid();
    }

    void getlwp()
    {
    _lwpid = syscall(SYS_gettid);
    }

    static void *routine(void *args)
    {
    Thread *ts = static_cast<Thread *>(args);
    ts->getprocessid();
    ts->getlwp();
    pthread_setname_np(pthread_self(), ts->Name().c_str());
    ts->_func();
    return nullptr;
    }

    public:
    Thread(func_t f) : _joinable(true), _status(TSTATUS::THREAD_NEW), _func(f)
    {
    _name = "Worker-" + std::to_string(gcnt++);
    }

    void start()
    {
    if (_status == TSTATUS::THREAD_RUNNING)
    {
    LOG(LogLevel::INFO) << "thread is already running";
    return;
    }
    int n = pthread_create(&_tid, nullptr, routine, this);
    (void)n;
    _status = TSTATUS::THREAD_RUNNING;
    }

    void stop()
    {
    if (_status == TSTATUS::THREAD_RUNNING)
    {
    int n = pthread_cancel(_tid);
    (void)n;
    _status = TSTATUS::THREAD_STOP;
    }
    else
    {
    LOG(LogLevel::WARNING) << "thread status is : THREAD_NEW or THREAD_STOP! stop error";
    }
    }

    void join()
    {
    if (_joinable)
    {
    int n = pthread_join(_tid, nullptr);
    (void)n;
    LOG(LogLevel::INFO) << "lwp : " << _lwpid << ", name: " << _name << ", join success";
    }
    else
    {
    LOG(LogLevel::WARNING) << "lwp : " << _lwpid << ", name: " << _name << ", join failed, because thread is detach";
    }
    }

    void detach()
    {
    if (_joinable && _status == TSTATUS::THREAD_RUNNING)
    {
    _joinable = false;
    int n = pthread_detach(_tid);
    (void)n;
    }
    }

    std::string Name()
    {
    return _name;
    }

    ~Thread() {}

    private:
    pthread_t _tid;
    pid_t _pid;
    pid_t _lwpid;
    std::string _name;
    bool _joinable;
    TSTATUS _status;
    func_t _func;
    };

    #endif

    5.2.3 InetAddr.hpp(地址封装)

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

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

    class InetAddr
    {
    public:
    InetAddr(struct sockaddr_in &addr) : _net_addr(addr)
    {
    _port = ntohs(_net_addr.sin_port);
    _ip = inet_ntoa(_net_addr.sin_addr);
    }

    InetAddr(uint16_t port, std::string ip = "0.0.0.0")
    : _port(port), _ip(ip)
    {
    _net_addr.sin_family = AF_INET;
    _net_addr.sin_port = htons(_port);
    _net_addr.sin_addr.s_addr = inet_addr(_ip.c_str());
    }

    uint16_t Port() { return _port; }
    std::string Ip() { return _ip; }

    struct sockaddr *Addr()
    {
    return CONV(&_net_addr);
    }

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

    socklen_t AddrLen()
    {
    return sizeof(_net_addr);
    }

    std::string StringAddress()
    {
    return "[" + _ip + ":" + std::to_string(_port) + "]";
    }

    ~InetAddr()
    {
    }

    private:
    uint16_t _port;
    std::string _ip;
    struct sockaddr_in _net_addr;
    };

    5.2.4 Route.hpp(消息路由)

    #pragma once
    #include <iostream>
    #include <vector>
    #include <string>
    #include <algorithm>
    #include "InetAddr.hpp"
    #include "Mutex.hpp"
    #include "Logger.hpp"

    using namespace LogModule;

    class Route
    {
    private:
    bool IsOnline(const InetAddr &who)
    {
    for (auto &user : _users)
    {
    if (user == who)
    return true;
    }
    return false;
    }

    void AddUser(const InetAddr &who)
    {
    _users.push_back(who);
    LOG(LogLevel::INFO) << "New user online: " << who.StringAddress();
    }

    void DeleteUser(const InetAddr &who)
    {
    auto iter = std::remove_if(_users.begin(), _users.end(),
    [&who](const InetAddr &user)
    { return user == who; });
    if (iter != _users.end())
    {
    _users.erase(iter, _users.end());
    LOG(LogLevel::INFO) << "User offline: " << who.StringAddress();
    }
    }

    public:
    Route() {}

    void RouteMessage(std::string message, InetAddr who, int sockfd)
    {
    LOG(LogLevel::DEBUG) << "3. RouteMessage";
    std::vector<InetAddr> temp_users;

    {
    LockGuard lockguard(&_lock);
    if (!IsOnline(who))
    {
    AddUser(who);
    }
    temp_users = _users;
    };

    std::string send_message = who.StringAddress() + "# " + message;
    for (auto &user : temp_users)
    {
    ssize_t ret = sendto(sockfd, send_message.c_str(), send_message.size(), 0, user.Addr(), user.AddrLen());
    if (ret < 0)
    {
    LOG(LogLevel::WARNING) << "sendto error: " << user.StringAddress();
    }
    }

    if (message == "QUIT")
    {
    LockGuard lockguard(&_lock);
    DeleteUser(who);
    }
    }

    ~Route() {}

    private:
    std::vector<InetAddr> _users;
    Mutex _lock;
    };

    5.2.5 UdpServer.hpp(聊天室专用版)

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

    using namespace LogModule;

    using callback_t = std::function<void(std::string message, InetAddr who, int sockfd)>;

    class UdpServer
    {
    public:
    UdpServer(callback_t cb, uint16_t port)
    : _sockfd(1),
    _port(port),
    _cb(cb)
    {
    }

    void Init()
    {
    _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (_sockfd < 0)
    {
    LOG(LogLevel::FATAL) << "create socket error";
    exit(1);
    }
    LOG(LogLevel::INFO) << "create socket fd success: " << _sockfd;

    InetAddr local(_port);
    int n = bind(_sockfd, local.Addr(), local.AddrLen());
    if (n < 0)
    {
    LOG(LogLevel::FATAL) << "bind socket error";
    exit(2);
    }
    LOG(LogLevel::INFO) << "bind socket fd success: " << _sockfd;
    }

    void Start()
    {
    char inbuffer[1024];
    while (true)
    {
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) 1, 0, (struct sockaddr *)&peer, &len);
    if (n < 0)
    {
    LOG(LogLevel::WARNING) << "recvfrom error";
    break;
    }
    inbuffer[n] = 0;
    InetAddr clientaddr(peer);
    if (_cb)
    {
    _cb(inbuffer, clientaddr, _sockfd);
    }
    }
    }

    ~UdpServer()
    {
    if (_sockfd >= 0)
    {
    close(_sockfd);
    _sockfd = 1;
    }
    }

    private:
    int _sockfd;
    uint16_t _port;
    callback_t _cb;
    };

    #endif

    5.2.6 ThreadPool.hpp(懒汉单例线程池)

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

    static const int gnum = 5;

    using namespace LogModule;

    template <typename T>
    class ThreadPool
    {
    private:
    bool IsTaskQueueEmpty()
    {
    return _queue.empty();
    }

    T PopHelper()
    {
    T t = _queue.front();
    _queue.pop();
    return t;
    }

    void ThreadRoutine()
    {
    char name[64];
    pthread_getname_np(pthread_self(), name, sizeof(name));
    while (true)
    {
    T task;
    {
    LockGuard lockguard(&_lock);
    while (IsTaskQueueEmpty() && _isrunning)
    {
    _sleeper_cnt++;
    LOG(LogLevel::DEBUG) << "没有任务, 线程休眠: |" << name << "|";
    _cond.Wait(_lock);
    LOG(LogLevel::DEBUG) << "有任务, 线程唤醒: |" << name << "|";
    _sleeper_cnt;
    }
    if (IsTaskQueueEmpty() && !_isrunning)
    {
    LOG(LogLevel::INFO) << "Thread: " << name << " quit";
    break;
    }
    task = PopHelper();
    }
    task();
    }
    }

    ThreadPool(int num = gnum) : _num(num), _isrunning(false), _sleeper_cnt(0)
    {
    for (int i = 0; i < _num; i++)
    {
    _threads.emplace_back([this]()
    {
    this->ThreadRoutine();
    });
    }
    }

    ThreadPool(const ThreadPool<T> &) = delete;
    ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;

    public:
    static ThreadPool<T> *GetInstance()
    {
    if (_instance == nullptr)
    {
    LockGuard lockguard(&_singleton_lock);
    if (_instance == nullptr)
    {
    LOG(LogLevel::DEBUG) << "首次使用,创建线程池对象";
    _instance = new ThreadPool<T>();
    _instance->Start();
    }
    }
    return _instance;
    }

    void Start()
    {
    LockGuard lockguard(&_lock);
    if (_isrunning)
    return;
    _isrunning = true;
    for (auto &thread : _threads)
    thread.start();
    }

    void Enqueue(T task)
    {
    LockGuard lockguard(&_lock);
    if (!_isrunning)
    return;
    _queue.push(task);
    if (_sleeper_cnt > 0)
    _cond.NotifyOne();
    LOG(LogLevel::DEBUG) << "2. Enqueue task";
    }

    void Stop()
    {
    LockGuard lockguard(&_lock);
    if (_isrunning)
    {
    LOG(LogLevel::DEBUG) << "关闭线程池";
    _isrunning = false;
    if (_sleeper_cnt > 0)
    _cond.NotifyAll();
    }
    }

    void Wait()
    {
    for (auto &thread : _threads)
    thread.join();
    }

    ~ThreadPool() {}

    private:
    std::vector<Thread> _threads;
    int _num;
    bool _isrunning;
    int _sleeper_cnt;
    std::queue<T> _queue;
    Mutex _lock;
    Cond _cond;
    static ThreadPool<T> *_instance;
    static Mutex _singleton_lock;
    };

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

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

    5.3 主函数整合

    #include <iostream>
    #include <signal.h>
    #include <unistd.h>
    #include "UdpServer.hpp"
    #include "Route.hpp"
    #include "ThreadPool.hpp"
    #include "Logger.hpp"

    using namespace LogModule;

    static Route g_route;

    void StopHandler(int signo)
    {
    LOG(LogLevel::INFO) << "recv signal: " << signo << ", stop chatroom…";

    ThreadPool<std::function<void()>>::GetInstance()->Stop();
    ThreadPool<std::function<void()>>::GetInstance()->Wait();

    LOG(LogLevel::INFO) << "chatroom stop success!";
    exit(0);
    }

    int main(int argc, char *argv[])
    {
    if (argc != 2)
    {
    std::cerr << "Usage: " << argv[0] << " <port>" << std::endl;
    return 1;
    }

    ENABLE_CONSOLE_LOG_STRATEGY();
    // ENABLE_FILE_LOG_STRATEGY();

    signal(SIGINT, StopHandler);
    signal(SIGTERM, StopHandler);

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

    auto *tp = ThreadPool<std::function<void()>>::GetInstance();

    UdpServer server
    (
    [&g_route, tp](std::string message, InetAddr who, int sockfd)
    {
    LOG(LogLevel::INFO) << "1. get a message: " << message
    << ", client addr: " << who.Ip() << ":" << who.Port();
    auto task = [message, who, sockfd, &g_route]()
    {
    g_route.RouteMessage(message, who, sockfd);
    };
    tp->Enqueue(task);
    },
    port
    );

    server.Init();

    LOG(LogLevel::INFO) << "chatroom start success! port: " << port;

    server.Start();

    return 0;
    }

    5.4 编译与运行

    5.4.1 Makefile

    CC = g++
    STD = -std=c++17
    CFLAGS = -Wall -g -lpthread -lstdc++fs

    all: chat_server chat_client

    chat_server: ChatMain.cc
    $(CC) -o $@ $^ $(STD) $(CFLAGS)

    chat_client: ChatClient.cc
    $(CC) -o $@ $^ $(STD) $(CFLAGS)

    .PHONY: clean
    clean:
    rm -f chat_server chat_client
    rm -rf log/

    5.4.2 运行步骤

    make
    ./chat_server 8888
    nc -u 127.0.0.1 8888

    5.5 客户端实现:多线程收发分离

    5.5.1 Linux客户端

    #include <iostream>
    #include <string>
    #include <thread>
    #include <cstring>
    #include "InetAddr.hpp"
    #include <sys/socket.h>
    #include <unistd.h>

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

    void Usage(const std::string& name)
    {
    std::cerr << "Usage: " << name << " server_ip server_port" << std::endl;
    }

    void RecvMessage()
    {
    char buffer[4096];
    while (true)
    {
    struct sockaddr_in temp;
    socklen_t len = sizeof(temp);
    ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer)1, 0,
    (struct sockaddr*)&temp, &len);
    if (n > 0)
    {
    buffer[n] = '\\0';
    std::cerr << buffer << std::endl;
    }
    }
    }

    void SendMessage()
    {
    InetAddr server_addr(server_port, server_ip);
    std::string line;
    while (true)
    {
    std::cout << "Please Enter# ";
    std::getline(std::cin, line);
    if (line.empty()) continue;
    sendto(sockfd, line.c_str(), line.size(), 0,
    server_addr.Addr(), server_addr.AddrLen());

    if (line == "QUIT")
    {
    std::cout << "Goodbye!" << std::endl;
    close(sockfd);
    exit(0);
    }
    }
    }

    int main(int argc, char* argv[])
    {
    if (argc != 3)
    {
    Usage(argv[0]);
    exit(0);
    }
    server_ip = argv[1];
    server_port = std::stoi(argv[2]);

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
    std::cerr << "Create socket error" << std::endl;
    exit(1);
    }

    std::thread recv_thread(RecvMessage);
    std::thread send_thread(SendMessage);

    recv_thread.join();
    send_thread.join();

    close(sockfd);
    return 0;
    }


    6 ~> 工程化实践与踩坑总结

    6.1 日志系统使用指南

    • 默认控制台日志:程序启动时自动启用

    • 切换到文件日志:在main函数开头调用ENABLE\\_FILE\\_LOG\\_STRATEGY\\(\\)

    • 日志等级:从低到高为DEBUG &lt; INFO &lt; WARNING &lt; ERROR &lt; FATAL

    • 日志格式:\\[时间\\] \\[等级\\] \\[进程ID\\] \\[文件名\\] \\[行号\\] \\- 日志内容

    6.2 UDP编程踩坑总结

    6.2.1 基础API踩坑

  • bind公网IP失败:云服务器网卡上没有配置公网IP,只能绑定0.0.0.0

  • 端口被占用:使用netstat -tulpn | grep 端口号查看占用进程

  • recvfrom返回-1:检查sockfd是否有效、len参数是否正确初始化

  • sendto返回-1:检查目标地址是否正确、防火墙是否放行

  • 6.2.2 多线程踩坑

  • 线程池未启动:懒汉单例线程池必须在GetInstance()中启动线程

  • vector迭代器失效:多线程遍历vector时必须加锁,避免同时扩容

  • inet_ntoa线程不安全:多线程环境下必须使用inet\\_ntop

  • 6.2.3 业务逻辑踩坑

  • 用户身份标识错误:必须用 IP\\+端口 唯一标识用户,不能只用IP

  • 锁粒度过大:加锁期间只操作共享资源,转发逻辑放在锁外

  • 中文乱码:Linux默认UTF-8,Windows默认GBK,需要统一编码

  • 在这里插入图片描述

    6.3 客户端访问服务端,云服务器需要开放一些端口

    为保障云服务器安全,厂商已通过防火墙对所有端口进行了限制处理,导致客户端无法正常访问服务器端口。若需解决该问题,可在防火墙或安全组中开放云服务器的端口(建议开放8000~9000端口范围),相关操作有对应视频教程,完成端口开放后,客户端即可正常访问服务器。


    7 ~> 总结与后续优化方向

    7.1 本文总结

    我们完成了UDP编程从入门到生产级的完整演进:

    • 从V1的Echo服务器掌握了基础Socket收发

    • 从V2的增强版字典服务器学会了生产级工程化实践(日志、配置、解耦)

    • 从V3的聊天室掌握了多用户管理、并发优化和跨平台通信

    7.2 后续优化方向

  • 用户下线检测:实现心跳机制,定期清理超时用户

  • 消息可靠性:实现ACK确认和重传机制

  • 大消息传输:实现消息分片与重组

  • 用户体系:实现登录注册,支持用户名密码

  • 私聊功能:支持一对一消息发送

  • 性能优化:使用无锁队列、epoll非阻塞IO


  • 附录:完整项目文件结构

    udp_project/
    ├── V1-Echo/
    │ ├── Mutex.hpp
    │ ├── Logger.hpp
    │ ├── UdpEchoServer.hpp
    │ ├── main.cpp
    │ └── Makefile
    ├── V2-Dictionary/
    │ ├── Mutex.hpp
    │ ├── Logger.hpp
    │ ├── Dictionary.hpp
    │ ├── UdpServer.hpp
    │ ├── Main.cc
    │ ├── DictClient.cc
    │ ├── Dict.txt
    │ └── Makefile
    ├── V3-ChatRoom/
    │ ├── Mutex.hpp
    │ ├── Logger.hpp
    │ ├── Cond.hpp
    │ ├── Thread.hpp
    │ ├── InetAddr.hpp
    │ ├── Route.hpp
    │ ├── UdpServer.hpp
    │ ├── ThreadPool.hpp
    │ ├── ChatMain.cc
    │ ├── ChatClient.cc
    │ └── Makefile
    └── README.md


    结尾

    uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!

    艾莉丝努力练剑

    C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主


    👀
    【关注】 跟随我一起深耕技术领域,见证每一次成长。

    ❤️
    【点赞】 让优质内容被更多人看见,让知识传递更有力量。


    【收藏】 把核心知识点存好,在需要时随时查、随时用。

    💬
    【评论】 分享你的经验或疑问,评论区一起交流避坑!

    不要忘记给博主“一键四连”哦!

    “今日练剑达成!”

    “技术之路难免有困惑,但同行的人会让前进更有方向。”

    结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主“一键四连”哦!

    往期回顾:

    【Linux网络】Linux 网络编程入门:UDP Socket 编程(上)

    🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡

    ૮₍ ˶ ˊ ᴥ ˋ˶₎ა

    在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » 【Linux网络】Linux 网络编程入门:UDP Socket 编程(下)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址