Socket 编程概述
Socket(套接字)是应用层与 TCP/IP 协议族通信的中间软件抽象层,简单来说,它是两个网络程序之间实现数据传输的'桥梁'。无论是 TCP 还是 UDP 协议,都可以通过 Socket 接口实现跨主机、跨网络的进程间通信。
Socket 编程主要分为 TCP Socket 和 UDP Socket 两类:
- TCP Socket:基于面向连接的 TCP 协议,提供可靠、有序、字节流的传输,适用于文件传输、登录认证等对数据可靠性要求高的场景。
- UDP Socket:基于无连接的 UDP 协议,提供无可靠保证、面向数据报的传输,传输速度快、开销小,适用于聊天、音视频传输、广播等对实时性要求高的场景。
本文重点讲解 UDP Socket 的核心基础与常用接口。
UDP 核心特性
UDP 是无连接的传输层协议,决定了 UDP Socket 编程的核心特性,也是与 TCP Socket 的主要区别:
- 无连接:通信双方无需提前建立连接,客户端直接向服务端发送数据报,服务端直接接收即可。
- 面向数据报:数据以'数据报'为单位传输,每次发送/接收都是一个完整的数据报,大小通常有限制(小于 64K)。
- 无需维护连接状态:服务端可同时接收多个客户端的数据,无需为每个客户端维护连接,资源开销小。
- 无可靠保证:数据传输可丢失、乱序,UDP 协议不提供重传、确认机制,可靠性需由应用层自行实现。
- 全双工通信:一个 Socket 描述符(fd)既可以用于读取数据,也可以用于写入数据,支持同时收发。
核心 API 接口
UDP Socket 编程的接口均来自 Linux 系统的网络编程头文件。在 C/C++ 中,通常需要包含以下头文件:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
所有接口的返回值均为 int 或 ssize_t,返回 -1 表示调用失败,可通过 errno 和 strerror(errno) 查看错误原因。
1. socket() —— 创建套接字描述符
功能:创建一个 Socket 描述符,作为后续网络通信的句柄,相当于打开一个'网络文件'。
函数原型:
int socket(int domain, int type, int protocol);
参数说明:
domain:协议域,指定网络层协议,UDP/TCP 均使用AF_INET(IPv4 协议);type:套接字类型,UDP 使用SOCK_DGRAM(数据报套接字),TCP 使用SOCK_STREAM(字节流套接字);protocol:指定具体协议,填 0 表示根据domain和type自动选择。
示例:创建 UDP Socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket error");
exit(1);
}
2. bind() —— 绑定 IP 与端口
功能:将 Socket 描述符与本地 IP 地址、端口号绑定,让系统知道该 Socket 监听哪个端口的网络数据。
核心说明:
- 服务端必须显式绑定:服务端的端口需要是固定值,让客户端能准确发送数据,IP 推荐使用
INADDR_ANY(表示绑定本机所有网卡的 IP)。 - 客户端无需显式绑定:客户端会在首次调用
sendto()时,由系统自动绑定一个随机端口和本机 IP。
函数原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
sockfd:由socket()创建的 Socket 描述符;addr:指向套接字地址结构的指针,UDP/TCP 使用struct sockaddr_in(IPv4 专用),需强制转换为struct sockaddr*;addrlen:套接字地址结构的大小。
套接字地址结构(struct sockaddr_in):
struct sockaddr_in {
sa_family_t sin_family; // 协议域,与 socket() 的 domain 一致,AF_INET
in_port_t sin_port; // 端口号,需转换为网络字节序(htons())
struct in_addr sin_addr; // IP 地址结构
};
示例:服务端绑定端口 8888,IP 为 INADDR_ANY
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(8888); // 主机字节序转网络字节序
local.sin_addr.s_addr = INADDR_ANY; // 绑定本机所有 IP
int ret = bind(sockfd, (struct sockaddr*)&local, sizeof(local));
if (ret < 0) {
perror("bind error");
exit(2);
}
3. sendto() —— 发送数据报
功能:向指定的目标 IP 和端口发送 UDP 数据报,是 UDP 编程的核心发送接口。
函数原型:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数说明:
sockfd:Socket 描述符;buf:指向要发送数据的缓冲区指针;len:要发送数据的字节数;flags:发送标志,填 0 表示默认(阻塞发送);dest_addr:目标端的套接字地址结构(包含目标 IP 和端口);addrlen:目标地址结构的大小。
示例:向服务端(192.168.1.100:8888)发送数据
std::string data = "hello udp";
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(8888);
server.sin_addr.s_addr = inet_addr("192.168.1.100"); // 点分十进制转网络字节序
ssize_t n = sendto(sockfd, data.c_str(), data.size(), 0,
(struct sockaddr*)&server, sizeof(server));
if (n < 0) {
perror("sendto error");
}
4. recvfrom() —— 接收数据报
功能:接收来自任意客户端的 UDP 数据报,并获取发送方的 IP 和端口号,是 UDP 编程的核心接收接口。
函数原型:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数说明:
sockfd:Socket 描述符;buf:指向接收数据的缓冲区指针;len:接收缓冲区的大小;flags:接收标志,填 0 表示默认(阻塞接收);src_addr:输出参数,用于存储发送方的套接字地址结构(获取发送方 IP 和端口);addrlen:输入输出参数,入参为src_addr的大小,出参为实际的地址结构大小。
示例:接收数据并获取发送方信息
char buffer[1024] = {0};
struct sockaddr_in peer; // 存储发送方信息
socklen_t len = sizeof(peer);
ssize_t m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0,
(struct sockaddr*)&peer, &len);
if (m > 0) {
buffer[m] = 0; // 手动添加字符串结束符
// 获取发送方 IP 和端口(网络字节序转主机字节序)
std::string peer_ip = inet_ntoa(peer.sin_addr);
uint16_t peer_port = ntohs(peer.sin_port);
std::cout << "[" << peer_ip << ":" << peer_port << "]# " << buffer << std::endl;
}
5. close() —— 关闭套接字
功能:关闭 Socket 描述符,释放系统分配的网络资源,与文件操作的 close() 一致。
示例:
close(sockfd); // 关闭后,sockfd 不可再使用
典型通信流程
服务端流程
- 调用
socket()创建 UDP Socket 描述符。 - 调用
bind()绑定固定端口和INADDR_ANY。 - 循环调用
recvfrom()接收客户端数据。 - (可选)调用
sendto()向客户端返回响应数据。 - 通信结束后,调用
close()关闭 Socket。
客户端流程
- 调用
socket()创建 UDP Socket 描述符。 - 填充服务端的
struct sockaddr_in结构(IP + 端口)。 - 调用
sendto()向服务端发送数据(系统自动绑定随机端口)。 - (可选)调用
recvfrom()接收服务端响应。 - 通信结束后,调用
close()关闭 Socket。
字节序转换
网络传输的数据采用大端序(网络字节序),而主机的字节序可能是大端或小端(x86 架构为小端),因此需要通过接口完成主机字节序与网络字节序的转换:
htons(uint16_t n):主机字节序(16 位)转网络字节序,用于端口号转换。ntohs(uint16_t n):网络字节序(16 位)转主机字节序,用于获取端口号。inet_addr(const char *cp):点分十进制 IP 字符串转网络字节序 32 位整数,简单但有兼容性问题。inet_pton(int family, const char *cp, void *addr):推荐使用,点分十进制 IP 字符串转网络字节序,支持 IPv4/IPv6。inet_ntoa(struct in_addr in):网络字节序 IP 转点分十进制字符串,非线程安全。inet_ntop(int family, const void *addr, char *cp, size_t len):推荐使用,网络字节序 IP 转点分十进制字符串,线程安全,支持 IPv4/IPv6。
Socket 编程是网络编程的基础,而 UDP Socket 因无连接、开销小、实时性高的特点,成为轻量级网络通信的首选。核心需掌握 5 个接口:socket()(创建)、bind()(绑定)、sendto()(发送)、recvfrom()(接收)、close()(关闭),以及网络字节序与主机字节序的转换。

