跳到主要内容Linux 网络编程基础:套接字 | 极客日志C
Linux 网络编程基础:套接字
综述由AI生成介绍 Linux 下网络编程中的套接字(Socket)概念。对比了 UDP 和 TCP 协议的特性,包括连接性、可靠性及适用场景。详细讲解了 sockaddr 结构体及其派生类,字节序转换接口(htons/htonl 等)。重点阐述了 socket、bind、sendto、recvfrom 等 UDP 接口,以及 listen、accept、connect 等 TCP 接口。最后提供了基于 C 语言的 UDP 和 TCP 客户端服务端通信代码示例,涵盖创建套接字、绑定端口、收发数据等完整流程。
星辰大海32 浏览 一、套接字基本概念
套接字(Socket)是计算机网络中用于进程间通信的一种机制,通常用于不同主机之间的数据传输。它抽象了底层网络协议的细节,为应用程序提供统一的接口。套接字是 IP 地址与端口号的组合,用于唯一标识网络中的一个通信端点。
二、套接字工作原理
套接字通过绑定特定的 IP 地址和端口号,实现数据的发送和接收。通信双方各自创建一个套接字,并通过网络协议(如 TCP 或 UDP)建立连接。数据通过套接字进行传输,发送方将数据写入套接字,接收方从套接字读取数据。
三、协议认识-UDP/TCP
UDP 和 TCP 特点
| 特性 | TCP(传输控制协议) | UDP(用户数据报协议) |
|---|
| 连接性 | 面向连接(三次握手建立连接,四次挥手断开) | 无连接(无需握手,直接发数据) |
| 可靠性 | 可靠传输(保证数据不丢、不重、有序到达) | 不可靠传输(不保证送达,可能丢包 / 乱序) |
| 有序性 | 保证数据按发送顺序到达(有序列号机制) | 不保证有序(先发的可能后到) |
| 重传机制 | 丢包会重传(超时重传、快速重传) | 无重传(丢包就丢了,不处理) |
| 流量控制 | 有(滑动窗口机制,避免发送方发太快压垮接收方) | 无(发送方只管发,不管接收方能不能处理) |
| 拥塞控制 | 有(慢启动、拥塞避免等,适配网络拥堵) | 无(网络再堵也照常发) |
| 数据边界 | 无(流式数据,比如发 2 次 10 字节,接收可能一次收 20 字节) | 有(数据报边界,发 1 次 10 字节,接收只能收 10 字节) |
| 开销 | 高(头部 20~60 字节,加握手 / 重传 / 控流等逻辑) | 低(头部仅 8 字节,无额外控制逻辑) |
| 速度 | 慢(可靠性和控流导致延迟高) | 快(无额外开销,延迟低) |
| 适用场景 | 对可靠性要求高的场景 | 对速度 / 实时性要求高的场景 |
前置:sockaddr 结构体
struct sockaddr {
unsigned short sa_family;
char sa_data[14];
};
sockaddr 是网络编程中用于表示套接字地址的通用结构体,定义在 <sys/socket.h> 头文件中。它通常用于存储 IP 地址和端口信息,支持多种协议族(如 IPv4、IPv6 等)。
结构体常见派生类
sockaddr_in(IPv4)
struct sockaddr_in {
short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
sockaddr_in6(IPv6)
struct sockaddr_in6 {
unsigned short sin6_family;
unsigned short sin6_port;
unsigned long sin6_flowinfo;
struct in6_addr sin6_addr;
unsigned int sin6_scope_id;
};
注意事项
- 字节序:端口和 IP 地址需转换为网络字节序(大端)。
- 地址族匹配:确保
sa_family 与使用的套接字类型一致。
- 通用性:
sockaddr 设计为通用结构体,实际使用时应优先选择具体的派生结构体。
UDP
相关接口认识
UDP 作为无连接、轻量级的传输层协议,其编程接口相比 TCP 更简洁,核心就是 socket()、bind()、sendto()、recvfrom() 四个接口。
1. 字节序转换核心接口(htons/htonl/ntohs/ntohl)
功能定位
网络协议规定所有传输的多字节数据(如端口、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);
2. int socket(int domain, int type, int protocol);
功能定位
这是网络编程的第一步,作用是创建一个套接字描述符(简称 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") 打印具体的错误原因,比如权限不足、参数填错等。
3. int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能定位
把创建好的套接字 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 场景的需求。
4. ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
功能定位
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 无效等。
5. ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
功能定位
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 套接字创建流程-client-server 通信实现
创建 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() {
int sockfd = socket(AF_INET, SOCK_DGRAM, 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(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
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() {
int sockfd = socket(AF_INET, SOCK_DGRAM, 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);
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
相关接口认识
TCP 作为面向连接、可靠的传输层协议,其编程接口相比 UDP 更复杂,核心新增接口包含 listen ()、accept ()、connect ()、send ()、recv (),基础的 socket ()、bind () 及字节序转换接口与 UDP 通用(不再重复)。
1. int listen(int sockfd, int backlog);
功能定位
服务端调用,将已绑定的套接字从'主动态'转为'被动态',使其监听客户端的连接请求。TCP 是面向连接的协议,服务端必须先监听端口,才能接收客户端的连接。
参数解读
sockfd:已绑定地址的套接字 fd(由 socket() 创建、bind() 绑定);
backlog:指定内核为该套接字维护的'未完成连接队列 + 已完成连接队列'的最大长度(如 5、10),超出的连接请求会被拒绝,新手填 5 即可满足基础场景。
返回值
成功返回 0;失败返回 -1,可通过 perror("listen") 排查错误(如 fd 未绑定、参数非法)。
2. int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能定位
服务端阻塞等待客户端的连接请求,当有客户端发起连接(三次握手完成),会创建一个新的套接字 fd 用于与该客户端通信,原监听 fd 继续监听其他客户端。
参数解读
sockfd:监听状态的套接字 fd(listen() 调用后的 fd);
addr:指向存储客户端地址信息的结构体,调用后会自动填充客户端的 IP 和端口,可传 NULL 表示不关心客户端地址;
addrlen:传入传出参数,调用前初始化为地址结构体长度(sizeof(struct sockaddr_in)),调用后会更新为实际地址长度,必须传指针。
返回值
成功返回新的通信套接字 fd(用于和该客户端收发数据);失败返回 -1(如 fd 未监听、被信号中断)。
3. int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能定位
客户端调用,主动向服务端发起 TCP 连接(触发三次握手)。只有连接建立成功后,才能通过该套接字收发数据。
参数解读
sockfd:客户端创建的套接字 fd(socket() 返回);
addr:指向服务端地址结构体,需初始化服务端的 IP 和端口(端口转网络序);
addrlen:服务端地址结构体的长度(sizeof(struct sockaddr_in))。
返回值
成功返回 0(三次握手完成,连接建立);失败返回 -1(如服务端未监听、网络不可达、端口错误)。
4. ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能定位
TCP 核心发送接口,向已建立连接的对端发送数据。因 TCP 是流式协议,send() 可能返回小于 len 的值(如内核缓冲区满),需循环发送确保数据全部发出。
sockfd:已建立连接的通信套接字 fd(客户端 connect() 成功后的 fd、服务端 accept() 返回的新 fd);
buf:要发送的数据缓冲区(只读);
len:要发送的数据长度(字节);
flags:默认填 0(阻塞发送),与 UDP sendto() 的 flags 用法一致(如 MSG_DONTWAIT 为非阻塞)。
返回值
成功返回实际发送的字节数(可能小于 len);失败返回 -1(如连接断开、fd 无效)。
5. ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能定位
TCP 核心接收接口,从已建立连接的套接字中读取数据。因 TCP 是流式无边界协议,recv() 读取的字节数可能小于缓冲区长度,需循环读取直到获取完整数据。
参数解读
sockfd:已建立连接的通信套接字 fd;
buf:存储接收数据的缓冲区;
len:缓冲区最大长度(避免溢出);
flags:默认填 0(阻塞接收),无数据时会阻塞等待。
返回值
成功返回实际接收的字节数;返回 0 表示对端关闭连接(四次挥手完成);失败返回 -1(如连接中断、fd 无效)。
TCP 套接字创建流程-client-server 通信实现
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() {
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() {
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;
}
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online