一、Socket 编程是什么
Socket(套接字)是网络通信的编程接口,是应用层与 TCP/IP 协议族通信的中间软件抽象层,简单来说,它是两个网络程序之间实现数据传输的 "桥梁"。无论是 TCP 还是 UDP 协议,都可以通过 Socket 接口实现跨主机、跨网络的进程间通信,也是实现网络编程的基础核心
Socket 编程主要分为TCP Socket 和UDP Socket两类:
- TCP Socket:基于面向连接的 TCP 协议,提供可靠、有序、字节流的传输,适用于文件传输、登录认证等对数据可靠性要求高的场景
- UDP Socket:基于无连接的 UDP 协议,提供无可靠保证、面向数据报的传输,传输速度快、开销小,适用于聊天、音视频传输、广播等对实时性要求高的场景
本文重点讲解UDP Socket的核心基础与常用接口(也是后续将写的三个实战项目的基础),TCP Socket 会在后续文章补充
二、UDP Socket 编程核心特点
UDP 是无连接的传输层协议,决定了 UDP Socket 编程的核心特性,也是与 TCP Socket 的核心区别:
- 无连接:通信双方无需提前建立连接,客户端直接向服务端发送数据报,服务端直接接收即可
- 面向数据报:数据以 "数据报" 为单位传输,每次发送 / 接收都是一个完整的数据报,数据报大小有限制(通常小于 64K)
- 无需维护连接状态:服务端可同时接收多个客户端的数据,无需为每个客户端维护连接,资源开销小
- 无可靠保证:数据传输可丢失、乱序,UDP 协议不提供重传、确认机制,可靠性由应用层自行实现
- 全双工通信:一个 Socket 描述符(fd)既可以用于读取数据,也可以用于写入数据,支持同时收发
三、UDP Socket 编程核心接口(Linux 下 C/C++)
UDP Socket 编程的接口均来自 Linux 系统的网络编程头文件,核心头文件包含:
#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 为 IPPROTO_UDP,TCP 为 IPPROTO_TCP)。示例:创建 UDP Socketint 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:套接字地址结构的大小,sizeof(struct sockaddr_in)。套接字地址结构(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地址结构
};
struct in_addr {
in_addr_t s_addr; // IP地址,需转换为网络字节序(inet_addr()/inet_pton())
};
示例:服务端绑定端口 8888,IP 为 INADDR_ANY
struct sockaddr_in local;
memset(&local, 0, sizeof(local)); // 初始化结构体,置0
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:目标地址结构的大小,sizeof(struct sockaddr_in)。返回值 :成功返回发送的字节数,失败返回 - 1。示例:向服务端(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的大小,出参为实际的地址结构大小。返回值 :成功返回接收的字节数,失败返回 - 1,返回 0 表示连接关闭(UDP 几乎不会出现)。示例:接收数据并获取发送方信息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()一致。函数原型:
int close(int sockfd);
示例:
close(sockfd); // 关闭后,sockfd不可再使用
四、UDP Socket 编程基本流程
服务端流程(固定步骤)
- 调用
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/IPv6inet_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()(关闭),以及网络字节序与主机字节序的转换
后续三篇博客将基于 UDP Socket 编程基础,实现三个实战项目:英译汉翻译服务器 、简单 UDP 通信程序 、UDP 群聊聊天室,从简单到复杂,逐步掌握 Socket 编程的实际应用
看到这里,不打算给我一个可爱的赞嘛~~