【Linux】UDP
一、UDP核心特性与编程基础
1.1 UDP协议关键特性
- 无连接:通信前无需建立连接,直接发送数据,减少连接开销。
- 不可靠传输:不保证数据的到达顺序、完整性,可能丢失或重复。
- 面向数据报:数据以"数据包"为单位传输,每个数据包有明确边界,不会粘包。
- 全双工:单个Socket可同时读写,支持双向通信。
1.2 UDP核心编程API
UDP编程依赖4个核心系统调用,与TCP不同,无需listen、accept等连接相关接口:
| 接口 | 功能描述 |
|---|---|
socket() |
创建UDP Socket,类型指定为SOCK_DGRAM。 |
bind() |
绑定IP和端口(服务器必须显式绑定,客户端通常由系统自动绑定随机端口)。 |
recvfrom() |
接收数据,同时获取发送方的IP和端口。 |
sendto() |
发送数据,需指定接收方的IP和端口。 |
1.3 关键问题:客户端需要显式bind吗?
- 结论 :不需要。客户端首次调用
sendto()时,操作系统会自动分配一个随机端口并绑定到Socket,避免端口冲突(多客户端同时连接时,随机端口保证唯一性)。 - 服务器必须显式
bind:服务器端口是"众所周知"的(如8080),客户端需通过固定端口找到服务器,因此必须手动绑定。
二、V1版本:基础Echo服务器(数据回显)
Echo服务器是最基础的UDP应用,核心功能是"接收客户端数据并原样返回",适合入门UDP编程流程。
2.1 核心代码结构
2.1.1 工具类:禁止拷贝(nocopy.hpp)
避免Socket对象被拷贝导致的资源泄漏:
cpp
#pragma once
class nocopy {
public:
nocopy() = default;
nocopy(const nocopy&) = delete; // 禁用拷贝构造
nocopy& operator=(const nocopy&) = delete; // 禁用赋值运算符
~nocopy() = default;
};
2.1.2 地址封装类(InetAddr.hpp)
封装struct sockaddr_in,简化IP和端口的转换与打印:
cpp
#pragma once
#include <string>
#include <netinet/in.h>
#include <arpa/inet.h>
class InetAddr {
public:
InetAddr(struct sockaddr_in& addr) : _addr(addr) {
_port = ntohs(addr.sin_port); // 网络字节序→主机字节序
_ip = inet_ntoa(addr.sin_addr); // in_addr→点分十进制字符串
}
std::string Ip() const { return _ip; }
uint16_t Port() const { return _port; }
// 格式化输出:IP:端口(如127.0.0.1:8888)
std::string PrintDebug() const {
return _ip + ":" + std::to_string(_port);
}
private:
std::string _ip;
uint16_t _port;
struct sockaddr_in _addr;
};
2.1.3 UDP服务器类(UdpServer.hpp)
cpp
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "nocopy.hpp"
#include "InetAddr.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), _sockfd(default_fd) {}
// 初始化:创建Socket + 绑定端口
void Init() {
// 1. 创建UDP Socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) {
std::cerr << "socket error: " << strerror(errno) << std::endl;
exit(1);
}
std::cout << "socket created, fd: " << _sockfd << std::endl;
// 2. 绑定IP和端口
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port); // 主机字节序→网络字节序
local.sin_addr.s_addr = INADDR_ANY; // 绑定所有网卡IP
int ret = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if (ret != 0) {
std::cerr << "bind error: " << strerror(errno) << std::endl;
exit(2);
}
std::cout << "bind success, port: " << _port << std::endl;
}
// 启动服务器:循环接收并回显数据
void Start() {
char buffer[buffer_size];
while (true) {
struct sockaddr_in peer; // 存储发送方地址
socklen_t peer_len = sizeof(peer);
// 3. 接收数据(阻塞等待)
ssize_t n = recvfrom(_sockfd, buffer, buffer_size - 1, 0,
(struct sockaddr*)&peer, &peer_len);
if (n > 0) {
buffer[n] = '\0'; // 手动添加字符串结束符
InetAddr client_addr(peer);
std::cout << "[" << client_addr.PrintDebug() << "]# " << buffer << std::endl;
// 4. 回显数据(原样返回)
sendto(_sockfd, buffer, strlen(buffer), 0,
(struct sockaddr*)&peer, peer_len);
}
}
}
private:
uint16_t _port; // 服务器端口
int _sockfd; // Socket文件描述符
};
2.1.4 UDP客户端(UdpClient.cpp)
cpp
#include <iostream>
#include <string>
#include <cerrno>
#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 server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// 1. 创建UDP Socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
std::cerr << "socket error: " << strerror(errno) << std::endl;
return 2;
}
// 2. 填充服务器地址信息
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()); // 字符串IP→网络字节序
// 3. 循环发送并接收数据
std::string input;
while (true) {
std::cout << "Please Enter# ";
std::getline(std::cin, input);
// 发送数据到服务器
sendto(sockfd, input.c_str(), input.size(), 0,
(struct sockaddr*)&server, sizeof(server));
// 接收服务器回显数据
char buffer[1024];
struct sockaddr_in temp;
socklen_t temp_len = sizeof(temp);
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr*)&temp, &temp_len);
if (n > 0) {
buffer[n] = '\0';
std::cout << "server echo# " << buffer << std::endl;
}
}
close(sockfd);
return 0;
}
2.2 编译运行与核心要点
编译命令
bash
# 编译服务器
g++ -o udp_server UdpServer.cpp -std=c++11
# 编译客户端
g++ -o udp_client UdpClient.cpp -std=c++11
运行步骤
- 启动服务器:
./udp_server 8888 - 启动客户端:
./udp_client 127.0.0.1 8888 - 客户端输入数据,服务器会原样返回。
关键要点
INADDR_ANY:绑定所有网卡IP,服务器可接收来自任意网卡的请求(如本地回环127.0.0.1、局域网IP 192.168.0.100)。- 字节序转换:端口和IP需通过
htons()、inet_addr()等函数转换为网络字节序(大端)。 recvfrom()的peer_len参数:必须传入有效长度,否则可能导致内存错误。
三、V2版本:字典服务器(业务解耦)
在Echo服务器基础上,扩展为"英译汉字典"功能,核心是实现"网络层"与"业务层"的解耦,提高代码可维护性。
3.1 核心设计思路
- 网络层:负责数据收发、地址解析,不关心业务逻辑。
- 业务层:负责字典查询,通过回调函数与网络层交互。
- 配置文件:字典数据存储在
dict.txt,启动时加载到内存。
3.2 核心代码实现
3.2.1 字典类(Dict.hpp)
加载配置文件并提供翻译接口:
cpp
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
const std::string sep = ": "; // 字典分隔符(如"apple: 苹果")
class Dict {
public:
Dict(const std::string& conf_path) : _conf_path(conf_path) {
LoadDict(); // 初始化时加载字典
}
// 翻译接口:输入英文单词,返回中文释义
std::string Translate(const std::string& word) {
auto iter = _dict.find(word);
return iter != _dict.end() ? iter->second : "Unknown word";
}
private:
// 加载字典文件到哈希表
void LoadDict() {
std::ifstream in(_conf_path);
if (!in.is_open()) {
std::cerr << "open dict file failed: " << _conf_path << std::endl;
return;
}
std::string line;
while (std::getline(in, line)) {
if (line.empty()) continue;
size_t pos = line.find(sep);
if (pos == std::string::npos) continue; // 跳过格式错误行
std::string key = line.substr(0, pos);
std::string value = line.substr(pos + sep.size());
_dict.emplace(key, value);
}
in.close();
std::cout << "dict loaded, size: " << _dict.size() << std::endl;
}
private:
std::string _conf_path; // 字典文件路径
std::unordered_map<std::string, std::string> _dict; // 存储单词-释义映射
};
3.2.2 解耦后的UDP服务器(UdpServer.hpp)
通过函数对象(std::function)实现业务回调:
cpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "nocopy.hpp"
#include "InetAddr.hpp"
// 定义回调函数类型:输入请求,输出响应
using FuncType = std::function<void(const std::string& req, std::string* resp)>;
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(FuncType func, uint16_t port = default_port)
: _func(func), _port(port), _sockfd(default_fd) {}
void Init() {
// 与V1版本一致:创建Socket + 绑定端口
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) {
std::cerr << "socket error: " << strerror(errno) << std::endl;
exit(1);
}
struct sockaddr_in local;
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) {
std::cerr << "bind error: " << strerror(errno) << std::endl;
exit(2);
}
}
void Start() {
char buffer[buffer_size];
while (true) {
struct sockaddr_in peer;
socklen_t peer_len = sizeof(peer);
// 接收客户端请求
ssize_t n = recvfrom(_sockfd, buffer, buffer_size - 1, 0,
(struct sockaddr*)&peer, &peer_len);
if (n > 0) {
buffer[n] = '\0';
InetAddr client_addr(peer);
std::cout << "[" << client_addr.PrintDebug() << "]# " << buffer << std::endl;
// 调用业务回调函数处理请求
std::string resp;
_func(buffer, &resp);
// 发送响应给客户端
sendto(_sockfd, resp.c_str(), resp.size(), 0,
(struct sockaddr*)&peer, peer_len);
}
}
}
private:
FuncType _func; // 业务回调函数
uint16_t _port; // 服务器端口
int _sockfd; // Socket文件描述符
};
3.2.3 服务器主函数(Main.cpp)
初始化字典和服务器,绑定业务逻辑:
cpp
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
#include "Dict.hpp"
// 全局字典对象(启动时加载)
Dict g_dict("./dict.txt");
// 业务处理函数:调用字典翻译
void TranslateFunc(const std::string& req, std::string* resp) {
*resp = g_dict.Translate(req);
}
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return 1;
}
uint16_t port = std::stoi(argv[1]);
// 创建服务器,绑定业务回调函数
std::unique_ptr<UdpServer> server = std::make_unique<UdpServer>(TranslateFunc, port);
server->Init();
server->Start();
return 0;
}
3.2.4 字典配置文件(dict.txt)
txt
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
happy: 快乐的
hello: 你好
goodbye: 再见
3.3 核心要点与扩展
运行效果
客户端输入英文单词(如apple),服务器返回中文释义(苹果);输入未知单词,返回Unknown word。
关键亮点
- 业务解耦:网络层与业务层通过回调函数分离,若需替换业务(如改为天气查询),只需修改回调函数,无需改动服务器核心代码。
- 配置化设计:字典数据存储在文件中,无需修改代码即可更新词汇。
扩展方向
- 支持中文到英文的反向翻译。
- 增加单词联想功能(前缀匹配)。
- 优化字典加载(支持大文件分片加载)。
四、V3版本:多人聊天室(消息转发+多线程)
聊天室是UDP的典型应用,核心功能是"消息转发"------服务器接收客户端消息后,广播给所有在线用户。需解决用户管理、多线程通信、消息路由等问题。
4.1 核心设计架构
- 用户管理:维护在线用户列表(存储IP和端口)。
- 消息路由:接收客户端消息,转发给所有在线用户。
- 多线程客户端:客户端分"读线程"(接收广播消息)和"写线程"(发送消息),支持同时读写。
- 线程池优化:服务器用线程池处理消息转发,提高并发能力。
4.2 核心代码实现
4.2.1 路由类(Route.hpp)
管理在线用户和消息转发逻辑:
cpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"
class Route {
public:
// 检查用户是否已在线
bool IsOnline(const InetAddr& user) {
for (const auto& u : _online_users) {
if (u.Ip() == user.Ip() && u.Port() == user.Port()) {
return true;
}
}
return false;
}
// 添加在线用户(首次发消息即登录)
void AddUser(const InetAddr& user) {
if (!IsOnline(user)) {
_online_users.push_back(user);
std::cout << "user login: " << user.PrintDebug() << std::endl;
}
}
// 移除在线用户(发送"QUIT"退出)
void RemoveUser(const InetAddr& user) {
for (auto iter = _online_users.begin(); iter != _online_users.end(); ++iter) {
if (iter->Ip() == user.Ip() && iter->Port() == user.Port()) {
_online_users.erase(iter);
std::cout << "user logout: " << user.PrintDebug() << std::endl;
break;
}
}
}
// 消息转发:向所有在线用户发送消息
void ForwardMessage(int sockfd, const std::string& msg, const InetAddr& sender) {
AddUser(sender); // 未在线则登录
// 构建消息:发送方IP:端口# 消息内容
std::string broadcast_msg = sender.PrintDebug() + "# " + msg;
// 转发给所有在线用户
for (const auto& user : _online_users) {
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(user.Port());
addr.sin_addr.s_addr = inet_addr(user.Ip().c_str());
sendto(sockfd, broadcast_msg.c_str(), broadcast_msg.size(), 0,
(struct sockaddr*)&addr, sizeof(addr));
}
// 发送"QUIT"则退出
if (msg == "QUIT") {
RemoveUser(sender);
}
}
private:
std::vector<InetAddr> _online_users; // 在线用户列表
};
4.2.2 多线程客户端(UdpClient.cpp)
分读、写线程,支持同时接收和发送消息:
cpp
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "InetAddr.hpp"
// 线程参数:Socket fd + 服务器地址
struct ThreadData {
int sockfd;
InetAddr server_addr;
};
// 读线程:接收服务器广播消息
void* RecvThread(void* arg) {
ThreadData* data = (ThreadData*)arg;
char buffer[4096];
while (true) {
struct sockaddr_in temp;
socklen_t temp_len = sizeof(temp);
ssize_t n = recvfrom(data->sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr*)&temp, &temp_len);
if (n > 0) {
buffer[n] = '\0';
std::cout << "\n" << buffer << std::endl; // 打印广播消息
std::cout << "Please Enter# ";
std::flush(std::cout); // 刷新输入提示
}
}
return nullptr;
}
// 写线程:从标准输入发送消息
void* SendThread(void* arg) {
ThreadData* data = (ThreadData*)arg;
std::string input;
while (true) {
std::cout << "Please Enter# ";
std::getline(std::cin, input);
// 发送消息到服务器
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(data->server_addr.Port());
server_addr.sin_addr.s_addr = inet_addr(data->server_addr.Ip().c_str());
sendto(data->sockfd, input.c_str(), input.size(), 0,
(struct sockaddr*)&server_addr, sizeof(server_addr));
// 输入"QUIT"退出
if (input == "QUIT") {
break;
}
}
close(data->sockfd);
return nullptr;
}
int main(int argc, char* argv[]) {
if (argc != 3) {
std::cerr << "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 sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
std::cerr << "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(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
InetAddr server_addr(server);
// 初始化线程参数
ThreadData data;
data.sockfd = sockfd;
data.server_addr = server_addr;
// 创建读、写线程
pthread_t recv_tid, send_tid;
pthread_create(&recv_tid, nullptr, RecvThread, &data);
pthread_create(&send_tid, nullptr, SendThread, &data);
// 等待线程结束
pthread_join(send_tid, nullptr);
pthread_cancel(recv_tid); // 写线程退出后,终止读线程
return 0;
}
4.2.3 线程池优化的服务器(Main.cpp)
结合之前实现的线程池,处理消息转发任务:
cpp
#include <iostream>
#include <memory>
#include <functional>
#include "UdpServer.hpp"
#include "Route.hpp"
#include "ThreadPool.hpp" // 复用之前实现的线程池
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return 1;
}
uint16_t port = std::stoi(argv[1]);
auto route = std::make_unique<Route>();
auto thread_pool = ThreadPool<std::function<void()>>::GetInstance();
// 业务回调:将消息转发任务提交到线程池
auto forward_func = [&](const std::string& msg, std::string* resp) {
(void)resp; // 忽略响应(聊天室无需单独回复)
// 获取发送方地址(需修改UdpServer,传递InetAddr参数)
// 此处简化:实际需在UdpServer中扩展,传递sender地址
InetAddr sender; // 实际应从recvfrom获取
thread_pool->Enqueue(std::bind(&Route::ForwardMessage, route.get(), _sockfd, msg, sender));
};
// 启动服务器
std::unique_ptr<UdpServer> server = std::make_unique<UdpServer>(forward_func, port);
server->Init();
server->Start();
return 0;
}
4.3 核心要点与运行效果
运行步骤
- 启动聊天室服务器:
./chat_server 8888 - 启动多个客户端:
./chat_client 127.0.0.1 8888 - 任意客户端输入消息,所有客户端都会收到广播;输入
QUIT退出。
关键技术点
- UDP全双工:单个Socket同时支持读和写,多线程操作无冲突。
- 用户管理 :通过IP+端口唯一标识用户,首次发消息自动登录,发送
QUIT自动退出。 - 线程池优化:消息转发任务提交到线程池,避免单线程阻塞,提高并发处理能力。
五、UDP编程关键知识点补充
5.1 地址转换函数(避免线程安全问题)
UDP编程中常用IP地址转换函数,需注意线程安全:
| 函数 | 功能描述 | 线程安全 |
|---|---|---|
inet_addr() |
点分十进制字符串→in_addr_t(网络字节序) |
是 |
inet_ntoa() |
in_addr→点分十进制字符串 |
否(静态缓冲区覆盖) |
inet_pton() |
支持IPv4/IPv6,字符串→二进制地址 | 是 |
inet_ntop() |
支持IPv4/IPv6,二进制地址→字符串 | 是(用户提供缓冲区) |
推荐使用线程安全函数:
cpp
// inet_pton + inet_ntop 示例
#include <arpa/inet.h>
int main() {
const char* ip_str = "192.168.0.1";
struct in_addr addr;
// 字符串→二进制地址
inet_pton(AF_INET, ip_str, &addr);
// 二进制地址→字符串(线程安全)
char buf[INET_ADDRSTRLEN];
const char* res = inet_ntop(AF_INET, &addr, buf, sizeof(buf));
std::cout << "ip: " << res << std::endl;
return 0;
}
5.2 UDP常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 数据丢失 | UDP不可靠传输,网络拥堵或接收缓冲区满 | 应用层添加确认机制(如ACK)、重传策略 |
| 端口冲突 | 服务器绑定已被占用的端口 | 选择未使用的端口,或检测端口占用后自动重试 |
| 消息乱序 | 数据包路由路径不同,到达顺序不一致 | 应用层添加序列号,接收端按序列号重组 |
| 缓冲区溢出 | 接收方处理速度慢,缓冲区被填满 | 增大接收缓冲区(setsockopt),优化处理逻辑 |
六、总结与进阶方向
UDP编程的核心是"简单高效",适合对实时性要求高、可容忍少量数据丢失的场景。
进阶学习方向
- 应用层可靠性优化:实现基于UDP的可靠传输(如添加序列号、确认、重传)。
- 广播与组播 :学习UDP广播(
SO_BROADCAST)和组播(IP_ADD_MEMBERSHIP)。 - 性能优化 :调整Socket缓冲区大小、使用IO多路复用(
select/poll/epoll)处理高并发。 - 跨平台兼容 :适配Windows系统的UDP编程接口(
WSASocket等)。