引言
在上一篇文章中,我们详细探讨了基于TCP协议的Socket编程,可以感受到TCP的编程流程还是有一些繁琐的,然而,在网络编程的世界里,并非所有场景都需要如此繁琐的连接建立。今天,我们将把目光投向另一个重要的主角------UDP协议(User Datagram Protocol)。
UDP是一种无连接、不可靠但在特定场景下(如视频流、DNS查询、实时游戏)效率极高的传输协议。本文将带你一步步掌握Linux环境下C++ UDP Socket的编程规范,并深入剖析其与TCP开发的异同。
一、UDP 协议的特点(与 TCP 对比)
| 特性 | UDP | TCP |
|---|---|---|
| 连接方式 | 无连接,无需三次握手 | 面向连接,三次握手建立连接 |
| 数据传输 | 不可靠、不保证顺序、不重传,但轻量、延迟低 | 可靠、有序、重传 |
| 数据形式 | 数据报,一次收一次发 | 字节流,无消息边界 |
| 适用场景 | 实时性要求高:视频、语音、游戏、直播、DNS | 可靠性要求高:HTTP、文件传输、数据库通讯 |
| 服务端并发模型 | 单进程即可处理多个客户端,无需 accept | 必须 accept 为每个客户端创建连接 |
二、UDP Socket 编程中常用的 API
UDP 使用的 API 与 TCP 90% 一样,但流程不同,由于UDP不存在握手这一步骤,所以在绑定地址之后,服务端不需要 listen,客户端也不需要 connetc,服务端同样不需要 accept。
同时在UDP中,我们主要使用以下两个函数来替代TCP中的 send 和 recv:
2.1 recvfrom
cpp
#include <sys/types.h>
#include <sys/socket.h>
/**
* @brief 将接收到的消息放入缓冲区 buf 中。
*
* @param sockfd 套接字文件描述符
* @param buf 缓冲区指针
* @param len 缓冲区大小
* @param flags 通信标签,详见recv方法说明
* @param src_addr 可以填NULL,如果 src_addr 不是 NULL,并且底层协议提供了消息的源地址,则该源地址将被放置在 src_addr 指向的缓冲区中。
* @param addrlen 如果src_addr不为NULL,它应初始化为与 src_addr 关联的缓冲区的大小。返回时,addrlen 被更新为包含实际源地址的大小。如果提供的缓冲区太小,则返回的地址将被截断;在这种情况下,addrlen 将返回一个大于调用时提供的值。
* @return ssize_t 实际收到消息的大小。如果接收失败,返回-1
*/
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
2.2 sendto
cpp
/**
* @brief 向指定地址发送缓冲区中的数据(一般用于UDP模式)
*
* @param sockfd 套接字文件描述符
* @param buf 缓冲区指针
* @param len 缓冲区大小
* @param flags 通信标签,详细减send方法说明
* @param dest_addr 目标地址。如果用于连接模式,该参数会被忽略
* @param addrlen 目标地址长度
* @return ssize_t 发送的消息大小。发送失败返回-1
*/
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
三、实战范例
3.1 服务端代码
服务端的核心在于必须绑定端口,否则客户端无法找到它。
cpp
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#define handle_error(cmd,result) \
if (result < 0) \
{ \
perror(cmd); \
return -1; \
} \
int main(int argc, char const *argv[])
{
// 定义socket的文件描述符
int sockfd,temp_result;
// 定义客户端与服务端的套结字数据结构,供之后填写
struct sockaddr_in server_addr,client_addr;
// 清空
memset(&server_addr,0,sizeof(server_addr));
memset(&client_addr,0,sizeof(client_addr));
// 填写服务段地址,指定协议
server_addr.sin_family = AF_INET;
// 填写ip地址 INADDR_ANY就是0.0.0.0
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 填写端口号
server_addr.sin_port = htons(6666);
//udp协议socket编程流程
// 1. 创建socket
sockfd = socket(AF_INET,SOCK_DGRAM,0);
handle_error("socket",sockfd);
// 2. bind函数绑定socket地址
temp_result = bind(sockfd,(struct sockaddr*)&server_addr,
sizeof(server_addr));
handle_error("bind",temp_result);
char* buf = malloc(sizeof(char) * 1024);
// 不需要监听、接受,直接进行连接
do
{
// 清0缓冲区
memset(buf,0,1024);
// 接收数据到缓冲区
socklen_t client_len = sizeof(client_addr);
temp_result = recvfrom(sockfd,buf,1024,0,
(struct sockaddr*)&client_addr,&client_len);
handle_error("recfrom",temp_result);
if (strncmp(buf,"EOF",3) != 0)
{
printf("接收到客户端%s %d的信息:%s\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port),buf);
strcpy(buf,"OK\n");
}else
{
printf("收到结束消息 准备关闭\n");
}
// 回复数据
temp_result = sendto(sockfd,buf,strlen(buf),0,
(struct sockaddr*)&client_addr
,sizeof(client_addr));
handle_error("sendto",temp_result);
} while (strncmp(buf,"EOF",3) != 0);
free(buf);
close(sockfd);
return 0;
}
3.2 客户端代码
cpp
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <unistd.h>
#define handle_error(cmd,result) \
if (result < 0) \
{ \
perror(cmd); \
return -1; \
} \
int main(int argc, char const *argv[])
{
// 定义socket的文件描述符
int sockfd,temp_result;
// 定义客户端与服务端的套结字数据结构,供之后填写
struct sockaddr_in server_addr,client_addr;
// 清空
memset(&server_addr,0,sizeof(server_addr));
memset(&client_addr,0,sizeof(client_addr));
// 填写服务段地址,指定协议
server_addr.sin_family = AF_INET;
// 填写ip地址 INADDR_ANY就是0.0.0.0
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 填写端口号
server_addr.sin_port = htons(6666);
//udp协议socket编程流程
// 1. 创建socket
sockfd = socket(AF_INET,SOCK_DGRAM,0);
handle_error("socket",sockfd);
// 2. bind函数绑定socket地址
temp_result = bind(sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
handle_error("bind",temp_result);
char* buf = malloc(sizeof(char) * 1024);
// 不需要监听、接受,直接进行连接
do
{
// 清空缓冲区 用来接收数据
memset(buf,0,1024);
temp_result = recvfrom(sockfd,buf,1024,0,NULL,NULL);
handle_error("recefrom",temp_result);
if (strncmp(buf,"EOF",3) != 0)
{
printf("收到服务端%s %d返回的数据%s\n",inet_ntoa(server_addr.sin_addr),
ntohs(server_addr.sin_port),buf);
break;
}
} while (strncmp(buf,"EOF",3) != 0);
free(buf);
close(sockfd);
return 0;
}
可以看到其实UDP的编程流程比TCP的要简单很多。
测试:
开启服务端
客户端建立UDP通讯

发送消息:

服务端接收到来自客户端的消息
四、UDP vs TCP ------ 编程流程清晰对比总结
1. 编程流程对比
TCP 服务端
cpp
socket() bind() listen() accept() ← 创建连接 recv()/send() close()
UDP 服务端
cpp
socket() bind() recvfrom()/sendto() close()
2. 客户端对比
TCP 客户端
cpp
socket() connect() send()/recv()
UDP 客户端
cpp
socket() sendto()/recvfrom()
3. API 差异
| API | TCP | UDP |
|---|---|---|
| connect | 必须,建立连接 | 可选,仅绑定目标地址 |
| listen/accept | 必须 | 不需要 |
| send/recv | 用于已连接的套接字 | 多用于 connect 之后 |
| sendto/recvfrom | 一般不用 | UDP 必须 |
4. 并发模型差异
| TCP 服务端 | UDP 服务端 |
|---|---|
| 每个客户端一条连接,需要多线程/epoll | 所有客户端共享同一个 socket |
| 连接维护成本高 | 无连接,性能高 |
总结
UDP编程相比TCP显得更加"自由"和"原始"。它剥离了繁重的连接管理,还原了网络通信最本质的数据投递功能。作为C++开发者,掌握UDP不仅仅是学会几个API,更是理解可靠性与效率之间权衡的艺术。
希望这篇博客能成为你学习路上的"知识锚点"。欢迎点赞、收藏、评论交流!