一、UDP 协议基础
UDP:用户数据报协议,无连接、不可靠、保留报文边界。
- 无连接:通信双方不需要握手,发数据直接扔出去;类似发短信,不需要拨号建立会话。
- 不可靠 :内核不做确认、重传,报文可能丢包、乱序、重复;可靠性需要业务应用层自己实现。
- 保留数据报边界 :一次
recvfrom读到的一定是对方一次sendto发送完整报文,不会拆包粘包。 - UDP 头部固定只有 8 字节,开销很小,适合直播、游戏、DNS 等低延迟场景。
网络通信本质 :进程间通信;IP 定位主机,端口号定位主机上面具体进程。
报文五元组 :
源 IP、源端口、目的 IP、目的端口、协议(UDP/TCP),每个 UDP 报文一定携带这 5 个信息。
二、UDP 服务端完整工作流程
1. 核心系统调用
-
socket() 创建套接字
cpp// AF_INET:IPv4 协议族;SOCK_DGRAM:UDP 数据报;protocol 填 0 自动选择 UDP int sockfd = socket(AF_INET, SOCK_DGRAM, 0);- 返回值 :成功返回文件描述符,失败返回
-1。 - 本质 :在内核开辟套接字资源,相当于打开网卡的"文件",后续所有收发都依靠这个 fd。此时套接字还没有 IP、端口信息。
- 返回值 :成功返回文件描述符,失败返回
-
bind() 绑定:给套接字设置固定 IP + 端口
服务器必须显式 bind,客户端不建议手动 bind。服务器端口必须固定公开,客户端端口由操作系统自动分配临时端口。
cppstruct sockaddr_in local; bzero(&local, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(port); // 主机字节序 → 网络字节序 local.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0,任意地址绑定 int ret = bind(sockfd, (struct sockaddr *)&local, sizeof(local));- bind 的本质 :把用户栈上填充好的
sockaddr_in地址信息拷贝到内核套接字对象中。 struct sockaddr是通用套接字结构体 ,是接口的参数;实际 IPv4 填充sockaddr_in,传参强制类型转换。
INADDR_ANY(0.0.0.0)任意地址绑定云服务器不要绑定公网 IP ,会报错
错误码 99 Cannot assign requested address;云服务器公网 IP 是虚拟化的,不是本机网卡真实 IP。0.0.0.0含义:本机所有网卡收到的、目标端口匹配的 UDP 报文,全部交付这个进程;机器有多块网卡多个 IP,全部都可以接收报文。虚拟机环境可以绑定具体 IP;
127.0.0.1本地回环,只能本机内部通信,数据包不会走物理网卡,用于本地测试。端口范围
0‑1023:系统特权端口,普通用户不能绑定,需要 root;被知名协议占用(80、443、22、3306)1024‑65535:用户可用端口,写服务器优先选这个区间。- 一个端口同一时刻只能被一个进程绑定;一个进程可以 bind 多个不同端口。
- bind 的本质 :把用户栈上填充好的
-
recvfrom() 接收 UDP 报文
UDP 没有
read,因为read面向字节流(TCP),UDP 必须使用recvfrom,不但拿到数据,还拿到对端客户端地址信息(IP + 端口),后续回复客户端就靠这个地址。cppchar buf[1024]; struct sockaddr_in peer; socklen_t peer_len = sizeof(peer); ssize_t n = recvfrom(sockfd, buf, sizeof(buf)-1, 0, (struct sockaddr *)&peer, &peer_len);参数说明:
- 最后两个参数是输出参数:内核把发送方的 IP、端口填进
peer结构体。 - 返回 n > 0:读到 n 字节;n = -1 出错。
读到数据之后手动
buf[n] = '\0',当做字符串处理,UDP 本身不携带字符串结束符。 - 最后两个参数是输出参数:内核把发送方的 IP、端口填进
-
sendto() 发送 UDP 报文
发送的时候必须指定目标对端地址;回显服务器就把
recvfrom拿到的peer直接传给 sendto。cppssize_t m = sendto(sockfd, data, len, 0, (struct sockaddr *)&peer, peer_len);UDP 服务器整体循环逻辑:
plaintextsocket → bind → while(true) { recvfrom → 业务处理 → sendto }UDP 不需要 listen、accept!!这是 TCP 专属接口。
三、UDP 客户端流程
cpp
socket(AF_INET, SOCK_DGRAM, 0); // 不需要 bind!!操作系统第一次 sendto 的时候,自动分配临时源端口
while(true) {
用户输入数据;
sendto(发给服务器地址);
recvfrom(接收服务器应答);
}
close(sockfd);
为什么客户端不手动 bind?
客户端如果写死 bind 固定端口:同一台机器开多个客户端就会端口冲突,程序启动失败。
操作系统在客户端第一次调用 sendto 的时候,自动分配 1024‑65535 空闲临时端口,完成隐式 bind。
客户端依然需要源 IP + 源端口,只是交给 OS 自动管理,不需要程序员写代码。
客户端必须知道服务器 IP 和端口,作为 sendto 的目标地址;服务器从 recvfrom 获取客户端地址。
四、字节序与 IP 地址转换函数
网络统一使用大端字节序;主机可能是小端(x86),必须做转换。
-
端口(16bit)
htons():host‑to‑network‑short,主机转网络ntohs():network‑to‑host‑short,网络转主机
-
IP 地址转换
函数 作用 注意事项 inet_addr()点分字符串 → 网络序 32 位整数 过时,错误返回和 255.255.255.255 冲突 inet_ntoa()网络序 in_addr → 点分字符串 非线程安全!内部静态缓冲区,多线程会覆盖结果,APUE 明确警告不推荐 inet_pton()字符串 IP → 网络二进制 线程安全,调用者提供缓冲区,支持 IPv4/IPv6,生产环境首选 inet_ntop()网络二进制 → 字符串 IP 线程安全,调用方提供输出缓冲区,替代 inet_ntoa 说明:
inet_ntoa内部是静态全局缓冲区,多线程并发调用会数据覆盖;开发优先用inet_pton / inet_ntop。
五、代码

Udpclient.cc
#include"Common.hpp"
const int size = 1024;
void Usage()
{
std::cout << "UdpClientSrv serverip serverport\n";
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage();
exit(0);
}
//创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cerr << "create sockfd error\n";
exit(1);
}
std::cout << "sockfd : " << sockfd << std::endl;
//客户端的套接字绑定端口号在第一次向服务器发起请求时,由操作系统自动绑定
//客户端发送数据&&接收数据(可能多个服务器回复同一个客户端)
std::string msg;
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_addr.s_addr = inet_addr(argv[1]);
server.sin_family = AF_INET;
server.sin_port = htons(std::stoi(argv[2]));
socklen_t len = sizeof(server);
while(true)
{
std::cout << "Please Enter@: ";
//发送数据
std::cin >> msg;
std::cout << msg <<std::endl;
int m = sendto(sockfd, msg.c_str(), msg.size(), 0, (const sockaddr*)&server, len);
std::cout << "send " << m << "data\n";
//接收数据
char buffer[size];
struct sockaddr_in tmp;
bzero(&tmp, sizeof(tmp));
socklen_t len_;
int n = recvfrom(sockfd, buffer, size-1, 0, (sockaddr*)&tmp, &len_);
if(n > 0) buffer[n] = 0;
std::cout << buffer << std::endl;
}
}
Udpserver.hpp
#include "Common.hpp"
const uint16_t defaultport = 8080;
const int size = 1024;
using func_t = std::function<std::string(const std::string&)>;
class UdpEchoSrv
{
private:
uint16_t _port;
int _sockfd;
public:
UdpEchoSrv(uint16_t port = defaultport) : _port(port), _sockfd(-1) {}
void Init()
{
// 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
lg(Fatal, "create sockfd error");
// 绑定端口号
struct sockaddr_in server;
int len = sizeof(server);
bzero(&server, len);
server.sin_family = AF_INET;
server.sin_port = htons(_port);
server.sin_addr.s_addr = INADDR_ANY;
int res = bind(_sockfd, (const sockaddr *)&server, len);
if (res < 0)
{
lg(Warning, "bind error");
exit(2);
}
lg(Info, "bind success");
}
void Run(func_t func)
{
char buffer[size];
while (true)
{
sockaddr_in client;
socklen_t len = sizeof(client);
// 接收数据
// std::cout << "start recv data\n";
ssize_t ret = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&client, &len);
if (ret < 0)
{
lg(Warning, "recvfrom error: %s", strerror(errno));
break;
}
buffer[ret] = 0;
// std::cout << buffer << std::endl;
// 处理数据---echo,服务器收到数据打印出来,并且给客户端发送数据
uint16_t clientport = ntohs(client.sin_port);
std::string clientip = inet_ntoa(client.sin_addr);
// lg(Info, "server get a request from %s:%d## %s", clientip.c_str(), clientport, buffer);
printf("%s:%d say : %s\n", clientip.c_str(), clientport, buffer);
// std::string str = "server handled your send message: "; ####什么都不处理就返回的版本
std::string str = func(buffer); // ##执行shell命令的版本
str += buffer;
std::cout << str << std::endl;
// func(buffer);
// ssize_t re = sendto(_sockfd, str.c_str(), str.size(), 0, (const sockaddr *)&client, len);
// (void)re;
}
}
~UdpEchoSrv()
{
close(_sockfd);
}
};
Udpserver.cc
#include"UdpEchoSrv.hpp"
#include<memory>
void Useage()
{
std::cout << "Udpserver port(1024+)\n";
}
std::string Handle(const std::string& str)
{
/// 执行shell命令
char buffer[4096];
FILE* fd = popen(str.c_str(), "r"); //以读的方式,读取结果,之后的逻辑再发送给客户端
std::string res;
while(true)
{
char* ch = fgets(buffer, sizeof(buffer), fd);
if(ch == nullptr) break; //当读到nullptr时,结束读取
res += ch;
}
pclose(fd);
return res;
/// 程序替换
// signal(SIGCHLD, 0);
// pid_t id = fork();
// if(id == 0)
// {
// execlp("sky", "sky", str.c_str(), NULL);
// }
// return "";
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Useage();
exit(0);
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<UdpEchoSrv> uptr(new UdpEchoSrv(port));
uptr->Init();
uptr->Run(Handle);
return 0;
}
Log.hpp
#include<string>
#include<unistd.h>
enum level
{
Info,
Warning,
Error,
Fatal
};
#define lg(level, format, ...) \
do \
{ \
std::string str; \
switch (level) \
{ \
case 0: \
str = "Info"; \
break; \
case 1: \
str = "Warning"; \
break; \
case 2: \
str = "Error"; \
break; \
case 3: \
str = "Fatal"; \
break; \
} \
time_t t = time(nullptr); \
struct tm *ltm = localtime(&t); \
char tmp[32] = {0}; \
strftime(tmp, 31, "%H:%M:%S", ltm); \
fprintf(stdout, "%s->" \
"[%s %s:%d]" format "\n", \
str.c_str(), tmp, __FILE__, __LINE__, ##__VA_ARGS__); \
}while (0);
