1.前言
UDP(User Datagram Protocol)是面向无连接的不可靠数据报传输协议,其特点如下:
- 低开销 :无需建立连接(三次握手),直接发送数据
- 实时性强 :适合音视频传输等对实时性要求高的场景
- 数据独立 :每个数据报携带完整地址信息
- 无流量控制 :可能导致数据丢失,但效率更高
典型应用场景:
- DNS查询
- DHCP协议
- 多媒体流传输
- 在线游戏数据同步
2.代码框架设计
项目结构
udp_project/
├── include/
│ ├── UdpServer.hpp // 服务器核心类
│ ├── Common.hpp // 公共定义
│ └── Log.hpp // 日志系统
├── src/
│ ├── UdpServerMain.cc // 服务器入口
│ └── UdpClientMain.cc // 客户端入口
└── Makefile // 编译脚本
3.基本实现 - EchoServer
UdpServer.hpp 核心实现
class UdpServer
{
public:
UdpServer(const std::string &ip = gdefaultip, uint16_t port = gdefaultport)
: _sockfd(gsockfd), _ip(ip), _port(port), _isrunning(false) {}
void InitServer()
{
// 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) {
LOG(FATAL) << "Socket creation failed: " << strerror(errno);
Die(SOCKET_ERR);
}
LOG(INFO) << "Socket created successfully, FD: " << _sockfd;
// 绑定地址
sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip.c_str());
if (bind(_sockfd, CONV(&local), sizeof(local)) < 0) {
LOG(FATAL) << "Binding failed: " << strerror(errno);
Die(BIND_ERR);
}
LOG(INFO) << "Binding successful";
}
void Start()
{
_isrunning = true;
while (true) {
char inbuffer[1024];
sockaddr_in peer;
socklen_t len = sizeof(peer);
// 接收数据
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0,
CONV(&peer), &len);
if (n > 0) {
inbuffer[n] = '\0';
LOG(DEBUG) << "Client says: " << inbuffer;
// 构建响应
std::string response = "Echo from server: ";
response += inbuffer;
// 发送响应
sendto(_sockfd, response.c_str(), response.size(), 0,
CONV(&peer), sizeof(peer));
}
}
}
private:
int _sockfd;
uint16_t _port;
std::string _ip;
bool _isrunning;
};
代码片段分析
InitServer()
方法详解
void InitServer()
{
// 1. 创建套接字
_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd < 0) {
// 错误处理:使用日志系统记录错误信息
LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
Die(SOCKET_ERR);
}
// 2. 填充地址结构
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清空结构体
local.sin_family = AF_INET; // IPv4协议
local.sin_port = htons(_port); // 主机字节序转网络字节序
local.sin_addr.s_addr = inet_addr(_ip.c_str()); // IP地址转换
// 3. 绑定套接字
int n = ::bind(_sockfd, CONV(&local), sizeof(local));
if(n < 0) {
LOG(LogLevel::FATAL) << "bind: " << strerror(errno);
Die(BIND_ERR);
}
}
关键点解析:
-
socket()函数 :
AF_INET
:IPv4协议族SOCK_DGRAM
:数据报套接字protocol=0
:自动选择UDP协议
sockaddr_in结构体 :
struct sockaddr_in {
sa_family_t sin_family; // 地址族
in_port_t sin_port; // 端口号
struct in_addr sin_addr; // IPv4地址
char sin_zero[8]; // 填充字段
};
-
地址转换函数 :
htons()
:主机字节序转网络字节序(16位)htonl()
:32位版本inet_addr()
:将点分IP转为32位网络序整数
Start()
方法详解
void Start()
{
_isrunning = true;
while(true) {
char inbuffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 接收数据报
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0,
CONV(&peer), &len);
if(n > 0) {
inbuffer[n] = '\0'; // 添加字符串结束符
// 构建响应数据
std::string echo_string = "echo# ";
echo_string += inbuffer;
// 发送响应
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0,
CONV(&peer), sizeof(peer));
}
}
}
核心API解析:
1.recvfrom() :
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
sockfd
:套接字描述符buf
:接收缓冲区len
:缓冲区大小flags
:0表示阻塞接收src_addr
:对端地址信息addrlen
:地址结构长度
2.sendto() :
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
dest_addr
:目标地址addrlen
:地址长度
3.UdpClientMain.cc 客户端实现
#include "UdpClient.hpp"
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"
int main(int argc, char *argv[])
{
if(argc != 3) {
std::cerr << "Usage: " << argv[0] << " serverIp serverPort" << std::endl;
Die(USAGE_ERR);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0) {
std::cerr << "Socket creation failed: " << strerror(errno) << std::endl;
Die(SOCKET_ERR);
}
// 填充服务器地址
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());
// 通信循环
while(true) {
std::cout << "Client> ";
std::string message;
std::getline(std::cin, message);
// 发送请求
sendto(sockfd, message.c_str(), message.size(), 0,
CONV(&server), sizeof(server));
// 接收响应
char buffer[64];
sockaddr_in tmp;
socklen_t len = sizeof(tmp);
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0,
CONV(&tmp), &len);
if(n > 0) {
buffer[n] = '\0';
std::cout << "Server> " << buffer << std::endl;
}
}
return 0;
}
注意事项
-
客户端绑定机制 :
- 不需要显式调用
bind()
,操作系统会自动分配端口 - 首次调用
sendto()
时,OS会自动完成绑定
- 不需要显式调用
-
端口选择原则 :
- 0-1023:系统端口(需root权限)
- 1024-49151:注册端口
- 49152-65535:动态/私有端口
- 建议使用8080/8081等未被占用的端口
地址转换技巧 :
// 字符串IP转网络序整数
uint32_t ip = inet_addr("192.168.1.1");
// 网络序整数转字符串
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &ip, ip_str, INET_ADDRSTRLEN);
4.UdpServer.hpp 修改 - 打印客户端信息
void Start()
{
_isrunning = true;
while (true) {
char inbuffer[1024];
sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0,
CONV(&peer), &len);
if (n > 0) {
inbuffer[n] = '\0';
// 获取客户端信息
std::string client_ip = inet_ntoa(peer.sin_addr);
uint16_t client_port = ntohs(peer.sin_port);
LOG(DEBUG) << "Client[" << client_ip << ":" << client_port
<< "] says: " << inbuffer;
// 构建响应
std::string response = "Echo from server: ";
response += inbuffer;
sendto(_sockfd, response.c_str(), response.size(), 0,
CONV(&peer), sizeof(peer));
}
}
}
5.通信验证(Linux-Linux)
启动服务器
# 编译服务器
g++ -o server UdpServerMain.cc -Iinclude
# 运行服务器
./server 127.0.0.1 8080
启动客户端
# 编译客户端
g++ -o client UdpClientMain.cc -Iinclude
# 运行客户端
./client 127.0.0.1 8080
交互示例
服务器端日志
[INFO] Socket created successfully, FD: 3
[INFO] Binding successful
[DEBUG] Client[127.0.0.1:34567] says: Hello Server
[DEBUG] Client[127.0.0.1:34567] says: UDP Test
客户端交互:
Client> Hello Server
Server> Echo from server: Hello Server
Client> UDP Test
Server> Echo from server: UDP Test
6.核心API总结
7.错误处理机制
错误码定义(Common.hpp)
enum ErrorType {
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
// ...其他错误码
};
日志系统(Log.hpp)
namespace LogModule {
enum LogLevel {
DEBUG = 0,
INFO,
WARNING,
ERROR,
FATAL
};
}