在网络编程领域,UDP(用户数据报协议)以其无连接、低延迟的特性,在实时通信场景中有着广泛应用。本文将以实战为导向,从基础的 UDP 回显服务器入手,逐步升级到英译汉字典服务,最终实现多线程聊天室,并深入解析 UDP 编程的核心原理与关键技术,帮助开发者快速掌握 UDP Socket 编程精髓。
一、UDP 编程基础:核心概念与地址转换
在开始编码前,我们需要先理解 UDP 编程的底层逻辑和必备工具,这是后续开发的基石。
1.1 UDP 协议核心特性
UDP 作为传输层协议,与 TCP 的 “可靠连接” 不同,它具有以下特点:
无连接:通信前无需建立三次握手,直接发送数据报
不可靠:不保证数据有序到达,也不重传丢失的数据包
面向数据报:数据以固定大小的 “数据报” 为单位传输
低延迟:省去连接建立和确认机制,适合实时场景(如聊天、直播)
1.2 地址转换函数:解决 “人类可读” 与 “机器可识别” 的矛盾
IP 地址在代码中以 32 位整数(网络字节序)存储,但人类习惯用 “点分十进制”(如 192.168.1.1)表示。以下函数是连接两者的关键工具:
| 功能 | 函数 | 说明 |
|---|---|---|
| 字符串转 IP(IPv4) | inet_addr(const char *strptr) | 将点分十进制字符串转为in_addr_t(网络字节序) |
| 字符串转 IP(通用) | inet_pton(int family, const char *strptr, void *addrptr) | 支持 IPv4(AF_INET)和 IPv6(AF_INET6),更通用 |
| IP 转字符串(IPv4) | inet_ntoa(struct in_addr inaddr) | 将in_addr转为点分十进制字符串,返回静态缓冲区地址 |
| IP 转字符串(通用) | inet_ntop(int family, const void *addrptr, char *strptr, size_t len) | 支持 IPv4/IPv6,需手动提供缓冲区,线程安全 |
注意点:inet_ntoa返回的是静态缓冲区地址,多线程调用会导致数据覆盖,推荐在多线程场景使用inet_ntop。
二、V1 版本:极简 UDP 回显服务器(Echo Server)
回显服务器是网络编程的 “Hello World”,功能很简单:接收客户端发送的消息,原样返回给客户端。通过这个案例,我们可以掌握 UDP 编程的基本流程。
2.1 核心设计:禁止拷贝与基础封装
为了避免对象拷贝导致的资源泄漏(如重复关闭 socket),首先定义一个nocopy基类,通过删除拷贝构造和赋值运算符实现 “禁止拷贝”:
// nocopy.hpp
#pragma once
class nocopy {
public:
nocopy() = default;
~nocopy() = default;
// 禁止拷贝构造和赋值
nocopy(const nocopy&) = delete;
const nocopy& operator=(const nocopy&) = delete;
};
2.2 服务器实现:三步完成 UDP 服务搭建
UDP 服务器的核心流程可总结为 “创建 socket → 绑定地址 → 循环收发数据”,具体代码如下:
// UdpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "nocopy.hpp"
// 常量定义
const static uint16_t default_port = 8888;
const static int default_fd = -1;
const static int buffer_size = 1024;
class UdpServer : public nocopy {
public:
UdpServer(uint16_t port = default_port)
: _port(port), _sock_fd(default_fd) {}
// 1. 初始化:创建socket + 绑定地址
void Init() {
// 创建UDP socket(SOCK_DGRAM表示UDP)
_sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sock_fd < 0) {
perror("socket create failed");
exit(1);
}
// 绑定地址(IP + 端口)
struct sockaddr_in local_addr;
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sin_family = AF_INET; // IPv4
local_addr.sin_port = htons(_port); // 端口转为网络字节序
local_addr.sin_addr.s_addr = INADDR_ANY; // 绑定所有网卡IP
if (bind(_sock_fd, (struct sockaddr*)&local_addr, sizeof(local_addr)) < 0) {
perror("bind failed");
close(_sock_fd);
exit(1);
}
std::cout << "Server init success, sock_fd: " << _sock_fd << std::endl;
}
// 2. 启动服务:循环收发数据
void Start() {
char buffer[buffer_size];
while (true) { // 服务器常驻运行
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
// 接收客户端消息
ssize_t recv_len = recvfrom(
_sock_fd, buffer, buffer_size - 1, 0,
(struct sockaddr*)&client_addr, &client_addr_len
);
if (recv_len < 0) {
perror("recvfrom failed");
continue;
}
buffer[recv_len] = '\0'; // 确保字符串结束
// 打印客户端信息与消息
std::string client_ip = inet_ntoa(client_addr.sin_addr);
uint16_t client_port = ntohs(client_addr.sin_port);
std::cout << "[" << client_ip << ":" << client_port << "]# " << buffer << std::endl;
// 回显消息给客户端
sendto(
_sock_fd, buffer, strlen(buffer), 0,
(struct sockaddr*)&client_addr, client_addr_len
);
}
}
// 3. 析构:释放资源
~UdpServer() {
if (_sock_fd != default_fd) {
close(_sock_fd);
}
}
private:
uint16_t _port; // 服务端口
int _sock_fd; // UDP socket文件描述符
};
2.3 客户端实现:无需显式绑定的秘密
UDP 客户端无需像服务器那样 “绑定固定端口”,因为客户端只需随机端口即可(操作系统会自动分配)。客户端流程为 “创建 socket → 发送消息 → 接收回显”:
// UdpClient.cpp
#include <iostream>
#include <string>
#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 server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// 1. 创建UDP socket
int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_fd < 0) {
perror("socket create failed");
return 1;
}
// 2. 填充服务器地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(server_port);
server_addr.sin_addr.s_addr = inet_addr(server_ip.c_str());
// 3. 循环发送与接收
std::string message;
char buffer[1024];
while (true) {
std::cout << "Please Enter# ";
std::getline(std::cin, message);
// 发送消息给服务器
sendto(
sock_fd, message.c_str(), message.size(), 0,
(struct sockaddr*)&server_addr, sizeof(server_addr)
);
// 接收服务器回显
socklen_t server_addr_len = sizeof(server_addr);
ssize_t recv_len = recvfrom(
sock_fd, buffer, 1023, 0,
(struct sockaddr*)&server_addr, &server_addr_len
);
if (recv_len < 0) {
perror("recvfrom failed");
continue;
}
buffer[recv_len] = '\0';
std::cout << "Server echo# " << buffer << std::endl;
}
close(sock_fd);
return 0;
}
2.4 关键疑问:客户端为什么不需要显式 bind?
服务器需要bind固定端口:因为客户端需要 “知道去哪里找服务器”(比如 HTTP 默认 80 端口)。
客户端无需显式bind:操作系统会在客户端首次调用sendto时,自动分配一个随机端口并绑定。若手动bind固定端口,多客户端运行时会出现 “端口占用” 错误。
三、V2 版本:UDP 英译汉字典服务
在回显服务器基础上,我们将 “业务逻辑” 与 “网络通信” 解耦,通过回调函数实现字典查询功能,让代码更具扩展性。
3.1 字典核心:加载词库与查询
首先实现一个Dict类,负责从文件加载词库(dict.txt)并提供查询接口:
// Dict.hpp
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
const std::string sep = ": "; // 词库文件分隔符(如"apple: 苹果")
class Dict {
public:
Dict(const std::string& dict_path) : _dict_path(dict_path) {
LoadDict(); // 构造时加载词库
}
// 翻译接口:查询不到返回"Unknown"
std::string Translate(const std::string& word) {
auto iter = _dict.find(word);
return iter != _dict.end() ? iter->second : "Unknown";
}
private:
// 加载词库文件到哈希表
void LoadDict() {
std::ifstream dict_file(_dict_path);
if (!dict_file.is_open()) {
perror("dict file open failed");
return;
}
std::string line;
while (std::getline(dict_file, line)) {
if (line.empty()) continue;
// 分割"单词: 释义"
size_t sep_pos = line.find(sep);
if (sep_pos == std::string::npos) continue;
std::string word = line.substr(0, sep_pos);
std::string meaning = line.substr(sep_pos + sep.size());
_dict.emplace(word, meaning);
}
dict_file.close();
std::cout << "Dict loaded, total words: " << _dict.size() << std::endl;
}
private:
std::string _dict_path; // 词库文件路径
std::unordered_map<std::string, std::string> _dict; // 哈希表存储词库
};
词库文件dict.txt格式如下:
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
happy: 快乐的
hello: 你好
3.2 服务器改造:用回调函数解耦业务
修改UdpServer类,通过std::function定义回调函数类型,将 “网络通信” 与 “字典查询” 解耦:
// UdpServer.hpp(V2版本)
#pragma once
#include <iostream>
#include <string>
#include <functional> // 用于std::function
#include <sys/socket.h>
#include <netinet/in.h>
#include "nocopy.hpp"
// 定义回调函数类型:输入请求(单词),输出响应(释义)
using Callback = std::function<void(const std::string& req, std::string* resp)>;
class UdpServer : public nocopy {
public:
// 构造时传入回调函数(业务逻辑)
UdpServer(Callback callback, uint16_t port = 8888)
: _callback(callback), _port(port), _sock_fd(-1) {}
// 初始化(与V1一致,省略重复代码)
void Init() { /* 同V1,创建socket + 绑定地址 */ }
// 启动服务:接收请求 → 回调业务 → 发送响应
void Start() {
char buffer[1024];
while (true) {
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
// 1. 接收客户端请求(单词)
ssize_t recv_len = recvfrom(
_sock_fd, buffer, 1023, 0,
(struct sockaddr*)&client_addr, &client_addr_len
);
if (recv_len < 0) continue;
buffer[recv_len] = '\0';
std::string req = buffer;
// 2. 调用回调函数查询字典
std::string resp;
_callback(req, &resp);
// 3. 发送释义给客户端
sendto(
_sock_fd, resp.c_str(), resp.size(), 0,
(struct sockaddr*)&client_addr, client_addr_len
);
// 打印日志
std::string client_ip = inet_ntoa(client_addr.sin_addr);
uint16_t client_port = ntohs(client_addr.sin_port);
std::cout << "[" << client_ip << ":" << client_port << "] query: " << req << " → " << resp << std::endl;
}
}
private:
Callback _callback; // 业务回调函数(字典查询)
uint16_t _port; // 服务端口
int _sock_fd; // UDP socket
};
3.3 主函数:组装服务与启动
在主函数中创建字典实例、定义回调函数,并启动服务器:
// Main.cpp
#include "UdpServer.hpp"
#include "Dict.hpp"
#include <memory>
// 全局字典实例(加载词库)
Dict g_dict("./dict.txt");
// 回调函数:调用字典查询
void DictCallback(const std::string& req, std::string* resp) {
*resp = g_dict.Translate(req);
}
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cout << "Usage: " << argv[0] << " port" << std::endl;
return 1;
}
uint16_t port = std::stoi(argv[1]);
// 创建服务器实例(传入回调函数)
std::unique_ptr<UdpServer> server = std::make_unique<UdpServer>(DictCallback, port);
server->Init();
server->Start();
return 0;
}
3.4 测试效果
- 启动服务器:
./dict_server 8888 - 启动客户端:
./udp_client 127.0.0.1 8888 - 客户端输入 “apple”,服务器返回 “苹果”;输入 “test”,返回 “Unknown”。
四、V3 版本:多线程 UDP 聊天室
聊天室是 UDP 的典型应用场景,需要实现 “消息广播”(将一个客户端的消息发送给所有在线用户)和 “多线程并发”(处理多个客户端的消息)。
4.1 核心挑战与解决方案
挑战 1:管理在线用户:需要记录所有在线客户端的地址(IP + 端口),用于消息广播。
挑战 2:多线程安全:多个线程会同时读写 “在线用户列表”,需用互斥锁保护。
挑战 3:并发处理消息:用线程池处理消息广播,避免主线程阻塞。
4.2 在线用户管理:线程安全的用户列表
首先扩展InetAddr类,增加地址比较和获取原生地址的接口,方便用户管理:
// InetAddr.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class InetAddr {
public:
InetAddr(struct sockaddr_in& addr) : _addr(addr) {
_ip = inet_ntoa(addr.sin_addr);
_port = ntohs(addr.sin_port);
}
// 获取IP和端口
std::string Ip() const { return _ip; }
uint16_t Port() const { return _port; }
// 获取原生sockaddr_in地址(用于sendto)
const struct sockaddr_in& GetAddr() const { return _addr; }
// 重载==:判断是否为同一用户(IP + 端口相同)
bool operator==(const InetAddr& other) const {
return _ip == other._ip && _port == other._port;
}
private:
std::string _ip; // 客户端IP
uint16_t _port; // 客户端端口
struct sockaddr_in _addr; // 原生地址结构
};
4.3 服务器改造:线程池 + 消息广播
引入线程池(ThreadPool,实现略)处理消息广播任务,用互斥锁保护在线用户列表:
// UdpServer.hpp(V3版本)
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <pthread.h>
#include <functional>
#include <sys/socket.h>
#include <netinet/in.h>
#include "nocopy.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
// 任务类型:线程池执行的任务
using Task = std::function<void()>;
class UdpServer : public nocopy {
public:
UdpServer(uint16_t port = 8888) : _port(port), _sock_fd(-1) {
// 初始化互斥锁
pthread_mutex_init(&_user_mutex, nullptr);
// 初始化线程池(4个线程)
ThreadPool<Task>::GetInstance()->Init(4);
}
// 初始化(创建socket + 绑定地址 + 启动线程池)
void Init() {
// 1. 创建socket(同V1)
_sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sock_fd < 0) { perror("socket failed"); exit(1); }
// 2. 绑定地址(同V1)
struct sockaddr_in local_addr;
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sin_family = AF_INET;
local_addr.sin_port = htons(_port);
local_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(_sock_fd, (struct sockaddr*)&local_addr, sizeof(local_addr)) < 0) {
perror("bind failed"); close(_sock_fd); exit(1);
}
// 3. 启动线程池
ThreadPool<Task>::GetInstance()->Start();
std::cout << "Chat room server init success, port: " << _port << std::endl;
}
// 添加在线用户(线程安全)
void AddOnlineUser(const InetAddr& user) {
pthread_mutex_lock(&_user_mutex);
// 避免重复添加同一用户
for (const auto& u : _online_users) {
if (u == user) {
pthread_mutex_unlock(&_user_mutex);
return;
}
}
_online_users.push_back(user);
std::cout << "User [" << user.Ip() << ":" << user.Port() << "] online, total: " << _online_users.size() << std::endl;
pthread_mutex_unlock(&_user_mutex);
}
// 消息广播:将消息发送给所有在线用户
void Broadcast(const std::string& message) {
pthread_mutex_lock(&_user_mutex);
for (const auto& user : _online_users) {
sendto(
_sock_fd, message.c_str(), message.size(), 0,
(struct sockaddr*)&user.GetAddr(), sizeof(user.GetAddr())
);
}
pthread_mutex_unlock(&_user_mutex);
}
// 启动服务:接收消息 → 加入线程池广播
void Start() {
char buffer[1024];
while (true) {
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
// 1. 接收客户端消息
ssize_t recv_len = recvfrom(
_sock_fd, buffer, 1023, 0,
(struct sockaddr*)&client_addr, &client_addr_len
);
if (recv_len < 0) continue;
buffer[recv_len] = '\0';
InetAddr user(client_addr);
// 2. 添加用户到在线列表
AddOnlineUser(user);
// 3. 构造广播消息(带用户标识)
std::string message = "[" + user.Ip() + ":" + std::to_string(user.Port()) + "]# " + buffer;
// 4. 将广播任务加入线程池
Task broadcast_task = std::bind(&UdpServer::Broadcast, this, message);
ThreadPool<Task>::GetInstance()->PushTask(broadcast_task);
}
}
~UdpServer() {
pthread_mutex_destroy(&_user_mutex);
if (_sock_fd != -1) close(_sock_fd);
}
private:
uint16_t _port; // 服务端口
int _sock_fd; // UDP socket
std::vector<InetAddr> _online_users; // 在线用户列表
pthread_mutex_t _user_mutex; // 保护用户列表的互斥锁
};
4.4 客户端改造:多线程收发
客户端需要同时 “发送消息” 和 “接收广播”,因此用两个线程分别处理:
// UdpClient.cpp(V3版本)
#include <iostream>
#include <string>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 线程数据:传递socket和服务器地址
struct ThreadData {
int sock_fd;
struct sockaddr_in server_addr;
};
// 接收线程:持续接收服务器广播
void* RecvThread(void* arg) {
ThreadData* data = (ThreadData*)arg;
char buffer[1024];
while (true) {
socklen_t server_addr_len = sizeof(data->server_addr);
ssize_t recv_len = recvfrom(
data->sock_fd, buffer, 1023, 0,
(struct sockaddr*)&data->server_addr, &server_addr_len
);
if (recv_len < 0) continue;
buffer[recv_len] = '\0';
std::cout << "\n" << buffer << std::endl; // 打印广播消息
std::cout << "Please Enter# "; // 提示用户输入
fflush(stdout); // 刷新缓冲区,避免输出卡顿
}
return nullptr;
}
// 发送线程:从标准输入读取消息并发送
void* SendThread(void* arg) {
ThreadData* data = (ThreadData*)arg;
std::string message;
while (true) {
std::cout << "Please Enter# ";
std::getline(std::cin, message);
// 发送消息给服务器
sendto(
data->sock_fd, message.c_str(), message.size(), 0,
(struct sockaddr*)&data->server_addr, sizeof(data->server_addr)
);
}
return nullptr;
}
int main(int argc, char* argv[]) {
if (argc != 3) {
std::cout << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}
// 解析服务器地址
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// 创建UDP socket
int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_fd < 0) { perror("socket failed"); return 1; }
// 填充服务器地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(server_port);
server_addr.sin_addr.s_addr = inet_addr(server_ip.c_str());
// 准备线程数据
ThreadData data = {sock_fd, server_addr};
// 创建收发线程
pthread_t recv_tid, send_tid;
pthread_create(&recv_tid, nullptr, RecvThread, &data);
pthread_create(&send_tid, nullptr, SendThread, &data);
// 等待线程结束(实际不会结束,常驻运行)
pthread_join(recv_tid, nullptr);
pthread_join(send_tid, nullptr);
close(sock_fd);
return 0;
}
4.5 测试效果
- 启动服务器:
./chat_server 8888 - 启动多个客户端:
./chat_client 127.0.0.1 8888 - 任一客户端发送消息,所有客户端都会收到该消息(带发送者 IP 和端口)。
五、总结与扩展
本文通过三个递进的案例,覆盖了 UDP Socket 编程的核心知识点:
- 基础流程:创建 socket → 绑定地址(服务器)→ 收发数据(
recvfrom/sendto)。 - 关键技术:地址转换函数、禁止拷贝、回调函数解耦、线程池、互斥锁。
- 最佳实践:服务器绑定
INADDR_ANY、客户端不手动bind、多线程场景用inet_ntop。
扩展方向
可靠性优化:UDP 本身不可靠,可通过 “序列号”“重传机制” 实现可靠 UDP(如 QUIC 协议)。
性能优化:增大接收缓冲区、使用内存池减少内存分配、优化线程池大小。
功能扩展:聊天室添加用户昵称、消息撤回、文件传输等功能。
8397

被折叠的 条评论
为什么被折叠?



