【Linux网络】UDP Socket 编程总结:从回显服务到字典服务,吃透无连接通信
前言
在 Linux 网络编程中,TCP 和 UDP 是传输层最重要的两类协议。TCP 强调连接管理与可靠传输,适合文件传输、HTTP 通信、金融交易等可靠性优先的场景;UDP 则强调无连接、低延迟、轻量高效,适合直播推流、实时游戏、DNS 查询、物联网数据上报等实时性优先的场景。
很多初学者一提到 UDP,只记住了“不可靠”三个字。但真正写代码时,更重要的是理解:UDP 为什么不可靠、它把哪些能力交给了应用层、它的 Socket 编程模型和 TCP 有什么不同、服务端和客户端分别应该怎么写。
本文保留原文的核心学习路线,把重点内容整理成一篇发布友好的精华版:先讲协议特性,再讲 API,再通过 Echo 服务和在线字典服务把代码落地,最后总结通用封装思想与高频踩坑点。
一、UDP 协议核心特性
1. 无连接
UDP 通信前不需要建立连接,只要知道对方的 IP 和端口,就可以直接发送数据。它没有 TCP 的三次握手和四次挥手,因此通信成本更低,启动速度更快。
这也是 UDP 在实时场景中很受欢迎的原因:能直接发,就不先绕一圈建立连接。
2. 不保证可靠
UDP 不提供确认应答、超时重传、序列号、乱序重排等机制。它只负责尽力发送,不保证数据一定到达,也不保证顺序和唯一性。
但这不是缺点,而是取舍。UDP 把可靠性设计交给应用层,换来更小的协议开销和更低的传输延迟。如果业务需要可靠性,可以自己在应用层实现 ACK、重传、序列号、去重、心跳等机制。
3. 面向数据报
UDP 是面向数据报的。发送端一次 sendto 发送一个完整报文,接收端一次 recvfrom 接收一个完整报文,报文之间天然有边界。
这点和 TCP 非常不同。TCP 是面向字节流的,没有消息边界;UDP 保留消息边界,但接收缓冲区必须足够大,否则报文会被截断,剩余部分直接丢弃。
二、UDP Socket 编程模型
服务端核心流程
服务端需要固定端口供客户端访问,所以必须显式 bind。
socket() -> bind() -> recvfrom() -> 业务处理 -> sendto() -> close()
客户端核心流程
客户端一般不需要显式 bind。第一次调用 sendto 时,操作系统会自动给客户端分配一个随机可用端口。
socket() -> 填充服务端地址 -> sendto() -> recvfrom() -> close()
这里要记住一句话:服务端端口必须稳定,客户端端口只要唯一。
三、UDP 编程核心 API
1. socket:创建套接字
#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
exit(1);
}
说明:
AF_INET:使用 IPv4SOCK_DGRAM:使用数据报套接字,也就是 UDP- 第三个参数填
0,系统会自动匹配 UDP 协议
2. bind:绑定 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;
if (bind(sockfd, (struct sockaddr*)&local, sizeof(local)) < 0) {
perror("bind");
exit(2);
}
说明:
htons(port):端口号从主机序转网络序INADDR_ANY:绑定本机所有网卡地址- 云服务器上一般不要直接绑定公网 IP,推荐绑定
INADDR_ANY
3. recvfrom:接收数据并获取发送端地址
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr*)&peer, &len);
if (n > 0) {
buffer[n] = 0;
std::cout << "client: "
<< inet_ntoa(peer.sin_addr)
<< ":"
<< ntohs(peer.sin_port)
<< " msg: "
<< buffer
<< std::endl;
}
注意:网络发来的数据是原始字节,如果要按 C 字符串打印,必须手动补 \0。
4. sendto:发送数据到指定目标
sendto(sockfd, buffer, strlen(buffer), 0,
(struct sockaddr*)&peer, len);
UDP 没有连接,因此每次发送都要指定目标地址。
四、V1 版本:UDP Echo 回显服务
Echo 服务是 UDP 入门最经典的案例:客户端发什么,服务端就回什么。它的重点不在业务,而在于打通 UDP 的完整通信链路。
1. Echo 服务端核心代码
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
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]);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
return 2;
}
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) {
perror("bind");
return 3;
}
std::cout << "UDP Echo Server Start, port: " << port << std::endl;
while (true) {
char buffer[1024];
struct sockaddr_in client;
socklen_t len = sizeof(client);
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr*)&client, &len);
if (n <= 0) continue;
buffer[n] = 0;
std::cout << "[" << inet_ntoa(client.sin_addr)
<< ":" << ntohs(client.sin_port)
<< "]# " << buffer << std::endl;
sendto(sockfd, buffer, strlen(buffer), 0,
(struct sockaddr*)&client, len);
}
close(sockfd);
return 0;
}
核心细节:
- 服务端必须
bind,否则客户端不知道发到哪里 recvfrom会拿到客户端 IP 和端口sendto必须使用客户端地址把数据发回去buffer[n] = 0是防止打印乱码的关键
2. Echo 客户端核心代码
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
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]);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
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());
while (true) {
std::string msg;
std::cout << "Please Enter# ";
std::cin >> msg;
sendto(sockfd, msg.c_str(), msg.size(), 0,
(struct sockaddr*)&server, sizeof(server));
char buffer[1024];
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr*)&temp, &len);
if (n > 0) {
buffer[n] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
close(sockfd);
return 0;
}
客户端不显式 bind,它在第一次 sendto 时由操作系统自动分配端口。这一点非常重要。
五、V2 版本:UDP 在线英译汉字典服务
Echo 服务验证的是通信链路,字典服务验证的是业务拆分。我们让客户端输入英文单词,服务端查询字典并返回中文释义。
1. 字典类设计
#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
class Dictionary {
public:
Dictionary(const std::string& filename = "Dict.txt") {
Load(filename);
}
std::string Translate(const std::string& word) {
auto it = dict_.find(word);
if (it == dict_.end()) {
return "Unknown word!";
}
return it->second;
}
private:
void Load(const std::string& filename) {
std::ifstream in(filename);
if (!in.is_open()) {
std::cerr << "open dict file failed" << std::endl;
return;
}
std::string key, value;
while (in >> key >> value) {
dict_[key] = value;
}
}
private:
std::unordered_map<std::string, std::string> dict_;
};
示例词典文件:
apple 苹果
banana 香蕉
hello 你好
world 世界
computer 计算机
2. 通用 UDP 服务端雏形
这里的重点是通过回调函数把“网络通信”和“业务处理”解耦。
#include <functional>
#include <string>
using Handler = std::function<std::string(const std::string&)>;
class UdpServer {
public:
UdpServer(uint16_t port, Handler handler)
: port_(port), handler_(handler), sockfd_(-1) {}
void Init() {
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd_ < 0) {
perror("socket");
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) {
perror("bind");
exit(2);
}
}
void Start() {
while (true) {
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(sockfd_, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr*)&peer, &len);
if (n <= 0) continue;
buffer[n] = 0;
std::string request = buffer;
std::string response = handler_(request);
sendto(sockfd_, response.c_str(), response.size(), 0,
(struct sockaddr*)&peer, len);
}
}
private:
uint16_t port_;
Handler handler_;
int sockfd_;
};
这个设计的精华在这里:
using Handler = std::function<std::string(const std::string&)>;
UDP 服务端不关心业务是什么。它只负责接收请求、调用处理函数、发送响应。
3. 字典服务端主函数
#include <memory>
#include "Dictionary.hpp"
#include "UdpServer.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 dict = std::make_unique<Dictionary>();
UdpServer server(port, [&dict](const std::string& word) {
return dict->Translate(word);
});
server.Init();
server.Start();
return 0;
}
这段代码就是原文最值得吸收的设计思想:服务端框架不变,业务通过 lambda 注入。以后想实现计算器服务、天气服务、命令服务,只需要换回调函数。
六、进阶封装:UdpSocket / UdpServer / UdpClient
当代码写到第二个版本时,就会发现 socket、bind、sendto、recvfrom 这些逻辑会反复出现。为了提高复用性,可以进一步封装。
1. UdpSocket 封装
class UdpSocket {
public:
UdpSocket() : fd_(-1) {}
bool Socket() {
fd_ = socket(AF_INET, SOCK_DGRAM, 0);
if (fd_ < 0) {
perror("socket");
return false;
}
return true;
}
bool Bind(const std::string& ip, uint16_t port) {
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = ip.empty() ? INADDR_ANY : inet_addr(ip.c_str());
if (bind(fd_, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind");
return false;
}
return true;
}
bool RecvFrom(std::string* out, std::string* ip, uint16_t* port) {
char buffer[1024] = {0};
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(fd_, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr*)&peer, &len);
if (n < 0) {
perror("recvfrom");
return false;
}
out->assign(buffer, n);
if (ip) *ip = inet_ntoa(peer.sin_addr);
if (port) *port = ntohs(peer.sin_port);
return true;
}
bool SendTo(const std::string& data, const std::string& ip, uint16_t port) {
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
peer.sin_addr.s_addr = inet_addr(ip.c_str());
ssize_t n = sendto(fd_, data.c_str(), data.size(), 0,
(struct sockaddr*)&peer, sizeof(peer));
return n >= 0;
}
void Close() {
if (fd_ >= 0) {
close(fd_);
fd_ = -1;
}
}
private:
int fd_;
};
2. 通用服务端封装
using Service = std::function<void(const std::string&, std::string*)>;
class UdpServer {
public:
UdpServer(const std::string& ip, uint16_t port, Service service)
: ip_(ip), port_(port), service_(service) {
sock_.Socket();
}
void Start() {
sock_.Bind(ip_, port_);
while (true) {
std::string request;
std::string client_ip;
uint16_t client_port = 0;
if (!sock_.RecvFrom(&request, &client_ip, &client_port)) {
continue;
}
std::string response;
service_(request, &response);
sock_.SendTo(response, client_ip, client_port);
}
}
private:
std::string ip_;
uint16_t port_;
Service service_;
UdpSocket sock_;
};
这个版本已经具备了“通用 UDP 服务框架”的味道:网络层负责收发,业务层负责处理,二者通过回调函数连接。
七、Makefile 示例
.PHONY: all clean
CXX = g++
CXXFLAGS = -std=c++17 -Wall
all: UdpEchoServer UdpEchoClient DictServer DictClient
UdpEchoServer: UdpEchoServer.cpp
$(CXX) $(CXXFLAGS) -o UdpEchoServer UdpEchoServer.cpp
UdpEchoClient: UdpEchoClient.cpp
$(CXX) $(CXXFLAGS) -o UdpEchoClient UdpEchoClient.cpp
DictServer: DictServer.cpp Dictionary.hpp UdpServer.hpp
$(CXX) $(CXXFLAGS) -o DictServer DictServer.cpp
DictClient: DictClient.cpp
$(CXX) $(CXXFLAGS) -o DictClient DictClient.cpp
clean:
rm -f UdpEchoServer UdpEchoClient DictServer DictClient
运行方式示例:
./UdpEchoServer 8080
./UdpEchoClient 127.0.0.1 8080
./DictServer 8081
./DictClient 127.0.0.1 8081
八、核心考点与踩坑指南
1. UDP 和 TCP 的核心区别
TCP 面向连接、可靠传输、面向字节流,有流量控制和拥塞控制;UDP 无连接、不保证可靠、面向数据报,头部开销小,延迟低。
2. 服务端为什么必须 bind?
服务端要提供固定入口,客户端必须知道请求发到哪个端口,所以服务端必须显式 bind。
3. 客户端为什么不推荐显式 bind?
客户端端口没有业务含义,只要唯一即可。如果多个客户端都绑定同一个固定端口,会直接冲突。交给操作系统自动分配是最合适的。
4. INADDR_ANY 的作用
INADDR_ANY 表示绑定本机所有网卡。服务器有多个 IP 时,客户端访问任意网卡地址都能被服务端接收。
5. UDP 会不会粘包?
UDP 不存在 TCP 意义上的粘包,因为 UDP 保留报文边界。但如果接收缓冲区太小,报文会被截断,剩余部分会丢失。
6. inet_ntoa 是否线程安全?
不是。inet_ntoa 返回的是静态缓冲区,多次调用会覆盖旧结果。多线程场景建议使用 inet_ntop。
7. recvfrom 后打印乱码
原因通常是没有手动补 \0。
ssize_t n = recvfrom(...);
if (n > 0) buffer[n] = 0;
8. 客户端收不到服务端响应
常见原因:
- 服务端只绑定了
127.0.0.1 - 防火墙没有开放 UDP 端口
- 云服务器安全组没有放行端口
- 服务端返回时没有使用客户端的真实 IP 和端口
9. UDP 丢包怎么办?
UDP 本身不保证可靠。可以从两个方向优化:
- 系统层:调大 socket 接收缓冲区
- 应用层:实现 ACK、超时重传、序列号、去重、心跳机制
结语
本文从 UDP 的协议特性出发,梳理了 Socket 编程的核心 API,并通过 Echo 回显服务和在线字典服务,把 UDP 的收发流程、地址绑定、字节序转换、业务解耦完整串了起来。
UDP 编程看起来 API 不多,但真正写好并不只是会调用 socket、bind、sendto、recvfrom。更重要的是理解它的设计边界:它不维护连接,不保证可靠,不处理业务语义。正因为它足够轻,应用层才有更大的自由度去设计自己的协议。
建议学习路线很简单:先敲 Echo 服务,打通通信链路;再写字典服务,理解业务解耦;最后封装 UdpSocket、UdpServer、UdpClient,形成自己的 UDP 编程模板。走完这三步,Linux UDP Socket 编程的主干基本就掌握了。
更多推荐


所有评论(0)