一、套接字基本概念
套接字(Socket)是计算机网络中用于进程间通信的一种机制,通常用于不同主机之间的数据传输。它抽象了底层网络协议的细节,为应用程序提供统一的接口。套接字是 IP 地址与端口号的组合,用于唯一标识网络中的一个通信端点。
本文介绍 Linux 下网络编程中的套接字(Socket)概念。对比了 UDP 和 TCP 协议的特性,包括连接性、可靠性及适用场景。详细讲解了 sockaddr 结构体及其派生类,字节序转换接口(htons/htonl 等)。重点阐述了 socket、bind、sendto、recvfrom 等 UDP 接口,以及 listen、accept、connect 等 TCP 接口。最后提供了基于 C 语言的 UDP 和 TCP 客户端服务端通信代码示例,涵盖创建套接字、绑定端口、收发数据等完整流程。

套接字(Socket)是计算机网络中用于进程间通信的一种机制,通常用于不同主机之间的数据传输。它抽象了底层网络协议的细节,为应用程序提供统一的接口。套接字是 IP 地址与端口号的组合,用于唯一标识网络中的一个通信端点。
套接字通过绑定特定的 IP 地址和端口号,实现数据的发送和接收。通信双方各自创建一个套接字,并通过网络协议(如 TCP 或 UDP)建立连接。数据通过套接字进行传输,发送方将数据写入套接字,接收方从套接字读取数据。
| 特性 | TCP(传输控制协议) | UDP(用户数据报协议) |
|---|---|---|
| 连接性 | 面向连接(三次握手建立连接,四次挥手断开) | 无连接(无需握手,直接发数据) |
| 可靠性 | 可靠传输(保证数据不丢、不重、有序到达) | 不可靠传输(不保证送达,可能丢包 / 乱序) |
| 有序性 | 保证数据按发送顺序到达(有序列号机制) | 不保证有序(先发的可能后到) |
| 重传机制 | 丢包会重传(超时重传、快速重传) | 无重传(丢包就丢了,不处理) |
| 流量控制 | 有(滑动窗口机制,避免发送方发太快压垮接收方) | 无(发送方只管发,不管接收方能不能处理) |
| 拥塞控制 | 有(慢启动、拥塞避免等,适配网络拥堵) | 无(网络再堵也照常发) |
| 数据边界 | 无(流式数据,比如发 2 次 10 字节,接收可能一次收 20 字节) | 有(数据报边界,发 1 次 10 字节,接收只能收 10 字节) |
| 开销 | 高(头部 20~60 字节,加握手 / 重传 / 控流等逻辑) | 低(头部仅 8 字节,无额外控制逻辑) |
| 速度 | 慢(可靠性和控流导致延迟高) | 快(无额外开销,延迟低) |
| 适用场景 | 对可靠性要求高的场景 | 对速度 / 实时性要求高的场景 |
struct sockaddr {
unsigned short sa_family; // 地址族(如 AF_INET、AF_INET6)
char sa_data[14]; // 协议特定的地址信息
};
/* sa_family:指定地址族,常见值包括:
* AF_INET:IPv4 地址。
* AF_INET6:IPv6 地址。
* AF_UNIX:Unix 域套接字。
* sa_data:存储具体的地址和端口信息,格式由协议族决定。
*/
sockaddr 是网络编程中用于表示套接字地址的通用结构体,定义在 <sys/socket.h> 头文件中。它通常用于存储 IP 地址和端口信息,支持多种协议族(如 IPv4、IPv6 等)。
struct sockaddr_in {
short sin_family; // 地址族(AF_INET)
unsigned short sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IPv4 地址
char sin_zero[8]; // 填充字段(未使用)
};
struct sockaddr_in6 {
unsigned short sin6_family; // 地址族(AF_INET6)
unsigned short sin6_port; // 端口号(网络字节序)
unsigned long sin6_flowinfo; // 流信息
struct in6_addr sin6_addr; // IPv6 地址
unsigned int sin6_scope_id; // 作用域 ID
};
sa_family 与使用的套接字类型一致。sockaddr 设计为通用结构体,实际使用时应优先选择具体的派生结构体。UDP 作为无连接、轻量级的传输层协议,其编程接口相比 TCP 更简洁,核心就是 socket()、bind()、sendto()、recvfrom() 四个接口。
网络协议规定所有传输的多字节数据(如端口、32 位 IP 整数)必须使用网络字节序(大端序),而不同机器的本地字节序可能是小端(主流)或大端,因此在绑定 / 指定地址时,必须通过这些接口完成本地序 ↔ 网络序的转换,否则会出现端口 / IP 解析错误。
uint16_t htons(uint16_t hostshort):Host to Network Short,将 16 位本地序端口号转为网络序(UDP/TCP 端口都是 16 位,必用);uint32_t htonl(uint32_t hostlong):Host to Network Long,将 32 位本地序 IP 整数转为网络序(如 INADDR_ANY 转换);uint16_t ntohs(uint16_t netshort):Network to Host Short,将 16 位网络序端口号转回本地序(接收数据后解析端口时用);uint32_t ntohl(uint32_t netlong):Network to Host Long,将 32 位网络序 IP 整数转回本地序(极少用,通常用 inet_ntop 直接转字符串)。local_addr.sin_port = htons(8080);server_addr.sin_port = htons(8080);local_addr.sin_addr.s_addr = htonl(INADDR_ANY);ntohs(client_addr.sin_port);这是网络编程的第一步,作用是创建一个套接字描述符(简称 sockfd),相当于给程序分配一个专属的'通信管道',后续所有的发送、接收操作都要基于这个 fd 完成。
domain:指定通信使用的地址族,决定网络协议版本。日常开发中最常用的是 AF_INET(对应 IPv4 协议),如果需要支持 IPv6 则用 AF_INET6。type:指定套接字的通信类型,UDP 必须用 SOCK_DGRAM(数据报类型),这个参数直接决定了通信是无连接、有数据边界的 UDP 模式;如果填 SOCK_STREAM 则是 TCP 模式。protocol:指定具体的传输协议,通常填 0 即可 —— 系统会根据前面的 type 参数自动匹配对应的协议(比如 SOCK_DGRAM 对应 UDP,SOCK_STREAM 对应 TCP),无需手动指定 IPPROTO_UDP 或 IPPROTO_TCP。成功时返回一个非负整数(就是套接字 fd,可理解为'管道编号');失败时返回 -1,此时可以用 perror("socket") 打印具体的错误原因,比如权限不足、参数填错等。
把创建好的套接字 fd 绑定到本地的 IP 地址和端口号上。对 UDP 来说,服务端必须调用 bind 绑定固定端口(比如 8080),否则客户端无法找到对应的服务端;客户端则可选调用 —— 如果不绑定,系统会在发送数据时自动分配一个随机端口。
sockfd:要绑定的套接字 fd,必须是 socket() 调用成功返回的有效 fd,且未被关闭。addr:指向存储本地地址信息的结构体,虽然参数类型是通用的 struct sockaddr,但实际开发中我们会用更贴合 IPv4 的 struct sockaddr_in 结构体,传参时强转为 struct sockaddr* 即可。这个结构体需要提前初始化,包括地址族(和 socket() 的 domain 一致)、端口号(必须转网络序)、IP 地址(通常填 INADDR_ANY 表示绑定本机所有网卡,一般不进行手动绑定特定 IP)。addrlen:地址结构体的长度,直接填 sizeof(struct sockaddr_in) 就能满足 IPv4 场景的需求。UDP 最核心的发送接口,作用是把缓冲区里的数据发送到指定的目标地址(对方的 IP + 端口)。因为 UDP 是无连接的,所以每次发送都需要明确指定目标地址,这也是它和 TCP 的 send() 最核心的区别。
sockfd:发送数据用的套接字 fd,需是有效且未关闭的。buf:指向要发送的数据缓冲区,比如存储字符串、字节数组的内存地址,这个参数是只读的,函数不会修改缓冲区内容。len:要发送的数据长度,单位是字节,比如发送字符串时可用 strlen(buf),发送字节数组则填数组的实际长度。flags:发送标志位,日常开发中填 0 即可(表示阻塞发送,直到数据发出或出错);如果需要非阻塞发送,可填 MSG_DONTWAIT,但新手建议先掌握默认的阻塞模式。dest_addr:指向目标地址的结构体,和 bind() 的 addr 类似,需提前初始化对方的 IP 地址和端口号(端口同样要转网络序)。addrlen:目标地址结构体的长度,填 sizeof(struct sockaddr_in) 即可。成功时返回实际发送的字节数(UDP 特性:要么发送全部数据,要么失败,不会出现发送部分数据的情况);失败时返回 -1,常见错误有网络不可达、目标地址错误、fd 无效等。
UDP 核心的接收接口,作用是从套接字中读取接收到的数据,同时获取发送方的地址(IP + 端口)—— 这也是 UDP 无连接特性的体现:接收数据时才知道数据是谁发的。
sockfd:接收数据用的套接字 fd,需是有效且绑定了端口的(服务端必须 bind,客户端若未 bind 则系统自动分配端口)。buf:指向存储接收数据的缓冲区,函数会把接收到的数据写入这个缓冲区,需提前分配足够的内存(比如定义 char buf[1024])。len:缓冲区的最大长度,避免数据溢出,比如 sizeof(buf)。flags:接收标志位,默认填 0(阻塞接收,直到有数据到来);MSG_DONTWAIT 同样用于非阻塞接收。src_addr:指向存储发送方地址的结构体,调用后会自动填充对方的 IP 和端口,方便后续回复数据。addrlen:传入传出参数,调用前要初始化为地址结构体的长度(比如 sizeof(struct sockaddr_in)),函数会根据实际地址长度修改这个值,因此必须传指针。成功时返回实际接收的字节数;失败时返回 -1,常见错误有 fd 被关闭、缓冲区过小(但不会溢出,只会截断数据)等;如果是非阻塞模式,无数据时也会返回 -1,需结合 errno 判断。
创建 UDP 套接字实现网络通信的步骤简单并且公式化
server 端:创建套接字->绑定端口->循环收取数据
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8888
#define BUF_LEN 1024
int main() {
// 1. 创建 UDP 套接字:socket(AF_INET, SOCK_DGRAM, 0)
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 2. 绑定地址:bind(sockfd, &addr, sizeof(addr))
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 网络序转换:htonl()
serv_addr.sin_port = htons(PORT); // 网络序转换:htons()
bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
// 3. 接收数据:recvfrom(sockfd, buf, len, 0, &cli_addr, &len)
char buf[BUF_LEN];
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
while (1) {
int n = recvfrom(sockfd, buf, BUF_LEN, 0, (struct sockaddr*)&cli_addr, &cli_len);
buf[n] = '\0';
printf("收到客户端 [%s:%d]:%s\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), buf);
}
close(sockfd);
return 0;
}
client 端:创建套接字->指定服务端 IP->发送数据
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888
#define BUF_LEN 1024
int main() {
// 1. 创建 UDP 套接字:socket(AF_INET, SOCK_DGRAM, 0)
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 2. 配置服务端地址
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(SERV_IP);
serv_addr.sin_port = htons(SERV_PORT); // 网络序转换:htons()
// 3. 发送数据:sendto(sockfd, buf, len, 0, &serv_addr, sizeof(addr))
char buf[BUF_LEN] = "Hello UDP Server!";
sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
printf("已发送:%s\n", buf);
close(sockfd);
return 0;
}
TCP 作为面向连接、可靠的传输层协议,其编程接口相比 UDP 更复杂,核心新增接口包含 listen ()、accept ()、connect ()、send ()、recv (),基础的 socket ()、bind () 及字节序转换接口与 UDP 通用(不再重复)。
服务端调用,将已绑定的套接字从'主动态'转为'被动态',使其监听客户端的连接请求。TCP 是面向连接的协议,服务端必须先监听端口,才能接收客户端的连接。
sockfd:已绑定地址的套接字 fd(由 socket() 创建、bind() 绑定);backlog:指定内核为该套接字维护的'未完成连接队列 + 已完成连接队列'的最大长度(如 5、10),超出的连接请求会被拒绝,新手填 5 即可满足基础场景。成功返回 0;失败返回 -1,可通过 perror("listen") 排查错误(如 fd 未绑定、参数非法)。
服务端阻塞等待客户端的连接请求,当有客户端发起连接(三次握手完成),会创建一个新的套接字 fd 用于与该客户端通信,原监听 fd 继续监听其他客户端。
sockfd:监听状态的套接字 fd(listen() 调用后的 fd);addr:指向存储客户端地址信息的结构体,调用后会自动填充客户端的 IP 和端口,可传 NULL 表示不关心客户端地址;addrlen:传入传出参数,调用前初始化为地址结构体长度(sizeof(struct sockaddr_in)),调用后会更新为实际地址长度,必须传指针。成功返回新的通信套接字 fd(用于和该客户端收发数据);失败返回 -1(如 fd 未监听、被信号中断)。
客户端调用,主动向服务端发起 TCP 连接(触发三次握手)。只有连接建立成功后,才能通过该套接字收发数据。
sockfd:客户端创建的套接字 fd(socket() 返回);addr:指向服务端地址结构体,需初始化服务端的 IP 和端口(端口转网络序);addrlen:服务端地址结构体的长度(sizeof(struct sockaddr_in))。成功返回 0(三次握手完成,连接建立);失败返回 -1(如服务端未监听、网络不可达、端口错误)。
TCP 核心发送接口,向已建立连接的对端发送数据。因 TCP 是流式协议,send() 可能返回小于 len 的值(如内核缓冲区满),需循环发送确保数据全部发出。
参数解读
sockfd:已建立连接的通信套接字 fd(客户端 connect() 成功后的 fd、服务端 accept() 返回的新 fd);buf:要发送的数据缓冲区(只读);len:要发送的数据长度(字节);flags:默认填 0(阻塞发送),与 UDP sendto() 的 flags 用法一致(如 MSG_DONTWAIT 为非阻塞)。成功返回实际发送的字节数(可能小于 len);失败返回 -1(如连接断开、fd 无效)。
TCP 核心接收接口,从已建立连接的套接字中读取数据。因 TCP 是流式无边界协议,recv() 读取的字节数可能小于缓冲区长度,需循环读取直到获取完整数据。
sockfd:已建立连接的通信套接字 fd;buf:存储接收数据的缓冲区;len:缓冲区最大长度(避免溢出);flags:默认填 0(阻塞接收),无数据时会阻塞等待。成功返回实际接收的字节数;返回 0 表示对端关闭连接(四次挥手完成);失败返回 -1(如连接中断、fd 无效)。
TCP 通信需遵循'服务端监听→客户端连接→双向收发'的固定流程,核心是三次握手建立连接、基于新 fd 通信。
server 端:创建套接字→绑定端口→监听端口→接受连接→循环接收数据
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8888
#define BUF_LEN 1024
int main() {
// 创建 TCP 套接字
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
// 绑定地址
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(PORT);
bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
// 监听端口
listen(listen_fd, 5);
// 接受连接
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
int conn_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, &cli_len);
printf("客户端 [%s:%d] 已连接\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
// 接收数据
char buf[BUF_LEN];
while (1) {
int n = recv(conn_fd, buf, BUF_LEN-1, 0);
if (n <= 0) {
printf("客户端断开连接\n");
break;
}
buf[n] = '\0';
printf("收到客户端:%s\n", buf);
}
close(conn_fd);
close(listen_fd);
return 0;
}
client 端:创建套接字→连接服务端→发送数据
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888
#define BUF_LEN 1024
int main() {
// 创建 TCP 套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 连接服务端
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(SERV_IP);
serv_addr.sin_port = htons(SERV_PORT);
connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
// 发送数据
char buf[BUF_LEN] = "Hello TCP Server!";
send(sockfd, buf, strlen(buf), 0);
printf("已发送:%s\n", buf);
close(sockfd);
return 0;
}

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online