Linux UDP Socket 编程入门:Echo Server/Client实现
一、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);


|
参数 |
说明 |
|
|
地址家族(Address Family),常用 |
|
|
套接字类型, |
|
|
协议类型,通常填 |
返回值:成功返回文件描述符(非负整数),失败返回 -1。
💡 关键理解:
socket()仅仅是打开了内核中的一个"文件",但此时这个文件还没有和任何 IP、端口关联,所以需要进行下一步 —— bind。
bind() — 绑定地址到套接字

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
|
参数 |
说明 |
|
|
|
|
|
指向地址结构体的指针(实际传入 |
|
|
地址结构体的长度 |
返回值:成功返回 0,失败返回 -1。
💡 为什么需要 bind? 创建套接字只是打开了文件,没有设置到内核的网络协议栈中。bind 的作用就是将 IP + 端口 与这个套接字文件关联,让内核知道:收到这个 IP 和端口的数据包,应该交给这个套接字处理。
地址转换与字节序处理
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
|
函数 |
作用 |
|
|
将点分十进制 IP 字符串 → 网络字节序的 4 字节整数 |
|
|
同上,但结果存入结构体 |
|
|
将网络字节序的 4 字节整数 → 点分十进制字符串 |
|
|
Host TO Network Short,主机字节序 → 网络字节序(16位,用于端口) |
|
|
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);
|
参数 |
说明 |
|
|
对端地址信息(输入/输出型参数) |
|
|
地址长度(既是输入又是输出) |
|
|
默认为 |
💡 阻塞式 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 以一定形式内置了服务器地址)
六、数据流图示

|
要点 |
说明 |
|
|
避免结构体填充字段的干扰数据 |
|
|
为字符串结束符 |
|
|
暂时忽略返回值,避免编译器警告,生产环境应检查错误 |
|
|
|
|
|
读取整行输入(含空格),比 |
更多推荐




所有评论(0)