一、UDP 套接字编程的核心步骤
|-----|--------------------------------------------------------|
| 角色 | 步骤 |
| 服务端 | 1. 创建 socket → 2. bind 绑定地址和端口 → 3. 循环 recvfrom/sendto |
| 客户端 | 1. 创建 socket → 2. (可选 bind)→ 3. sendto/recvfrom |
为什么客户端通常不需要显式 bind?
-
UDP 客户端发送数据时,操作系统会自动分配一个临时端口。
-
显式 bind 反而可能造成端口冲突。
-
当然,如果你需要固定客户端的端口(例如某些 P2P 场景),也可以手动 bind。
二、核心 API 速查
socket() --- 创建套接字
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);


|------------|--------------------------------------------------------------------------|
| 参数 | 说明 |
| domain | 地址家族(Address Family),常用 AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX(本地通信) |
| type | 套接字类型,SOCK_DGRAM(UDP,数据报)、SOCK_STREAM(TCP,字节流) |
| protocol | 协议类型,通常填 0 让系统自动选择 |
返回值:成功返回文件描述符(非负整数),失败返回 -1。
💡 关键理解:
socket()仅仅是打开了内核中的一个"文件",但此时这个文件还没有和任何 IP、端口关联,所以需要进行下一步 ------ bind。
bind() --- 绑定地址到套接字

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
|-----------|------------------------------------|
| 参数 | 说明 |
| sockfd | socket() 返回的文件描述符 |
| addr | 指向地址结构体的指针(实际传入 sockaddr_in 并强转) |
| addrlen | 地址结构体的长度 |
返回值:成功返回 0,失败返回 -1。
💡 为什么需要 bind? 创建套接字只是打开了文件,没有设置到内核的网络协议栈中。bind 的作用就是将 IP + 端口 与这个套接字文件关联,让内核知道:收到这个 IP 和端口的数据包,应该交给这个套接字处理。
地址转换与字节序处理
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
|--------------------------------------------------|-----------------------------------------------|
| 函数 | 作用 |
| inet_addr(const char *cp) | 将点分十进制 IP 字符串 → 网络字节序的 4 字节整数 |
| inet_aton(const char *cp, struct in_addr *inp) | 同上,但结果存入结构体 |
| inet_ntoa(struct in_addr in) | 将网络字节序的 4 字节整数 → 点分十进制字符串 |
| htons(uint16_t hostshort) | Host TO Network Short,主机字节序 → 网络字节序(16位,用于端口) |
| htonl(uint32_t hostlong) | Host TO Network Long,主机字节序 → 网络字节序(32位,用于IP) |

⚠️ 为什么需要字节序转换?
网络通信是双方进程之间的通信。网络协议规定使用大端字节序,而主机可能是大端或小端(x86 是小端)。所以:凡是发送到网络的数据,必须转成网络字节序。
sockaddr_in 结构体解析
/* 描述 Internet 套接字地址的结构体 */
struct sockaddr_in {
sa_family_t sin_family; /* 地址家族: AF_INET */
in_port_t sin_port; /* 端口号(网络字节序) */
struct in_addr sin_addr; /* IP 地址(网络字节序) */
unsigned char sin_zero[8]; /* 填充字段,使大小与 struct sockaddr 相同 */
};
struct in_addr {
uint32_t s_addr; /* 32 位 IPv4 地址,网络字节序 */
};

底层宏展开:
#define SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
// 展开后: sa_family_t sin_family

数据收发函数
// UDP 接收(可获取对端地址)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
// UDP 发送(需要指定对端地址)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
|--------------------------|------------------|
| 参数 | 说明 |
| src_addr / dest_addr | 对端地址信息(输入/输出型参数) |
| addrlen | 地址长度(既是输入又是输出) |
| flags | 默认为 0,阻塞式 I/O |
💡 阻塞式 I/O:如果对方不发数据,
recvfrom会一直阻塞等待,等同于scanf的行为。
三、UDP Server 实现
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include "Log.hpp"
using namespace LogModule;
const int dafultfd = -1;
class UdpServer
{
public:
UdpServer(uint16_t port)
: _sockfd(dafultfd),
_port(port),
_isrunning(false)
{};
// 初始化
void Init()
{
// 1.创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error!";
exit(1); // 进程退出
}
LOG(LogLevel::INFO) << "socket success sockfd: " << _sockfd;
// 2.绑定socket信息 , ip和端口,(ip比较特殊,后续解释)
// 2.1 填充sockaddr_in结构体
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
// 我会不会把我的IP地址和端口号发给对方?
// IP信息和端口信息,一定要发送到网络
// 本地格式->网络序列
local.sin_port = htons(_port);
// IP也是如此,1.IP转成4字节 2.4字节转成网络序列
//->in_addr_t inet_addr(const char *cp);
//local.sin_addr.s_addr = inet_addr(_ip.c_str());
local.sin_addr.s_addr = INADDR_ANY ;
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(2);
}
LOG(LogLevel::INFO) << "bind success, sockfd : " << _sockfd;
}
void Start()
{
// udp不面向连接,启动的时候,一直读写/收发
_isrunning = true;
while (_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1. 收消息
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (s > 0)
{
buffer[s] = 0;
LOG(LogLevel::DEBUG) << "buffer: " << buffer; // 1.消息内容 2.谁发的???
// 2. 发消息
std::string echo_string = "server say@ ";
echo_string += buffer;
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
}
}
}
~UdpServer() {};
private:
int _sockfd;
uint16_t _port;
//std::string _ip; // 用的是字符串风格,点分十进制
bool _isrunning;
};
主函数入口
#include <iostream>
#include "UdpServer.hpp"
#include <memory>
// ./udpserver port
int main(int argc,char* argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
}
uint16_t port = std::stoi(argv[1]);
Enable_Console_Log_Stratege();
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);
usvr->Init();
usvr->Start();
return 0;
}
四、UDP Client 完整实现
#include <iostream>
#include "Log.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <cstring>
using namespace LogModule;
// ./udpclient server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
LOG(LogLevel::DEBUG) << "Usage: " << argv[0] << " ip port";
return 1;
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// 1.创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return 2;
}
// 2.本地的ip和端口号是什么,需要和上面的'文件'关联吗?
// 问题:客户端是否需要bind?client是或否需要显式的bind?为什么?
// 首次发送消息,OS会自动给client进行bind,OS知道Ip,端口号采用随机端口号的方式~
// 填写服务器信息
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());
while (true)
{
std::string input;
std::cout << "Please Enter# ";
std::getline(std::cin, input);
int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
(void)n;
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}


五、关键问题解析
Q1: 客户端是否需要显式 bind?
答案:不需要。
|------------|--------------|--------------------------------------------------------|
| 角色 | 是否需要 bind | 原因 |
| Server | ✅ 必须显式 bind | 需要固定 IP + 端口,让客户端知道"去哪里找" |
| Client | ❌ 不需要显式 bind | 首次发送消息时, OS 自动进行隐式 bind。系统会自动分配临时端口,避免端口冲突 |
💡 核心原理:客户端是主动发起通信的一方。当第一次调用
sendto()时,如果内核发现该套接字尚未绑定,会自动为其分配一个临时端口(Ephemeral Port,通常范围 1024-65535),并绑定客户端的 IP 地址。
Q2: 为什么 IP 地址要转换两次?



实际流程:
inet_addr()内部完成:字符串 → 4 字节整数 → 网络字节序
htons():端口号 主机字节序 → 网络字节序
Q3: UDP 是全双工的吗?
是的! UDP 套接字既可以读也可以写,收发互不干扰。
// 同一个 sockfd,既可以 recvfrom 也可以 sendto sendto(sockfd, ...); // 发 recvfrom(sockfd, ...); // 收



Q4: 服务器是"死循环"
普通 C/C++ 程序:启动 → 运行 → 结束 → 退出
服务器程序: 启动 → 7×24 小时一直运行(死循环收发数据)
Q5: 为什么客户端需要填写 server 地址?
**"你要发消息,**你需要知道消息发送给谁!!!"
客户端必须知道服务器的 IP 和端口 才能发送数据。通常有两种方式:
-
命令行参数传入(如本例:
./udpclient 127.0.0.1 8080) -
配置文件 / 内置在 App 中**(客户端和服务器是同一家公司写的,App 以一定形式内置了服务器地址)**
六、数据流图示

|----------------------|-------------------------------------|
| 要点 | 说明 |
| memset/bzero 清零 | 避免结构体填充字段的干扰数据 |
| sizeof(buffer) - 1 | 为字符串结束符 \0 预留空间 |
| (void)n | 暂时忽略返回值,避免编译器警告,生产环境应检查错误 |
| peer 作为输出参数 | recvfrom 会填充实际发送方的地址信息 |
| std::getline | 读取整行输入(含空格),比 std::cin >> 更实用 |