
🎬 个人主页:HABuo
📖 个人专栏:《C++系列》 《Linux系列》《数据结构》《C语言系列》《Python系列》《YOLO系列》
⛰️ 如果再也不能见到你,祝你早安,午安,晚安

目录
[📖4.1 socket](#📖4.1 socket)
[📖4.2 bind](#📖4.2 bind)
[📖4.3 listen](#📖4.3 listen)
[📖4.4 accept](#📖4.4 accept)
[📖4.5 connect](#📖4.5 connect)
[📖4.6 sendto、write](#📖4.6 sendto、write)
[📖4.7 recvfrom、read](#📖4.7 recvfrom、read)
前言:
本篇博客我们正式进入网络通信编程的章节,今天我们主要来了解套接字及其接口,这些接口可能比较复杂,但是没有关系,大家先看懂,我将在后面的博客中,逐步地实现各个协议版本的服务端客户端代码,经过几轮的使用给,你也定会熟悉的!
📚一、Socket套接字是什么
网络套接字(Socket)是应用程序与内核网络协议栈之间的"通信门户"。它是一个抽象概念,允许程序像操作普通文件一样(通过文件描述符)发送和接收网络数据。可以说,Socket就是实现网络通信的编程接口。
Socket的本质:一种特殊的文件描述符
在Linux中,"一切皆文件"。当你创建一个Socket时,内核会返回一个非负整数 ,称为套接字描述符(Socket Descriptor) ,本质上与文件描述符(File Descriptor)是同一概念。你可以用read()、write()、close()等系统调用来操作它,就像操作文件一样。
-
对于普通文件:
read()从磁盘读数据,write()写入磁盘。 -
对于Socket:
read()从网络接收缓冲区读数据,write()将数据写入发送缓冲区,由内核协议栈负责发送出去。
验证 :在Linux终端输入ls -l /proc/$$/fd,可以看到当前进程打开的文件描述符,其中也包括socket类型的fd。
📚二、网络套接字分类
套接字是包含多种数据类型
- 域间套接字,用于同一个主机内编程的使用,原理是使用文件路径进行标识,看到同一个文件
- 原始套接字,通常用于在网络协议栈中,跨过传输层,直接封装或调用网络层,数据链路层的系统调用接口,通常是用于编写一些网络工具,例如抓包工具等
- 网络套接字,用户间的网络通信,即跨主机的在网络中进行通信的要使用到的数据类型,基于网络套接字进行网络编程
三种套接字对应的套接字编程的函数接口就要有三套,三套使用也太不方便了,利用多态的思想将三种套接字的使用合并为同一套函数,因此下面我们就来解读一套函数应对不同场景的原理:
Socket的分类(根据协议族和类型)
| 分类角度 | 类型 | 说明 |
|---|---|---|
| 协议族 | AF_INET(IPv4) AF_INET6(IPv6) AF_UNIX(本地进程间通信) |
决定使用哪种网络协议 |
| 套接字类型 | SOCK_STREAM(流式) SOCK_DGRAM(数据报) SOCK_RAW(原始套接字) |
决定传输层行为 |
| 传输协议 | TCP(IPPROTO_TCP) UDP(IPPROTO_UDP) | 实际使用的协议 |
常用组合:
-
AF_INET + SOCK_STREAM→ TCP -
AF_INET + SOCK_DGRAM→ UDP -
AF_UNIX + SOCK_STREAM→ 本地进程间通信(不经过网络协议栈,高性能)
使用示例:
cpp
int socket(int domain, int type, int protocol);
// 例如:创建TCP socket
int tcp_sock = socket(AF_INET, SOCK_STREAM, 0);
// 创建UDP socket
int udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
📚三、sockaddr结构体
struct sockaddr、struct sockaddr_in 和 struct sockaddr_un 是三个紧密相关的数据结构,用于传递套接字地址信息 。它们的设计是为了让套接字API(如bind、connect、accept)能够统一处理不同协议族的地址(IPv4、IPv6、Unix域等)。

struct sockaddr ------ 通用套接字地址结构
实际作用 :几乎从不直接操作 sockaddr 变量。它仅仅作为函数参数的类型,要求具体地址结构强制转换后传入。例如:
cpp
bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
struct sockaddr_in ------ IPv4地址结构
cpp
struct sockaddr_in {
sa_family_t sin_family; // 总是 AF_INET
in_port_t sin_port; // 端口号(16位,网络字节序)
struct in_addr sin_addr; // IPv4地址(32位,网络字节序)
unsigned char sin_zero[8]; // 填充,使结构体大小与sockaddr一致(16字节)
};
struct in_addr {
in_addr_t s_addr; // 32位IPv4地址
};
-
sin_family:必须设为AF_INET。 -
sin_port:端口号,必须用htons()转换为网络字节序。 -
sin_addr.s_addr:IPv4地址,必须用htonl()或inet_pton()转换为网络字节序。 -
sin_zero[8]:填充字段,通常用memset全部置0,目的是让sockaddr_in的大小正好等于sockaddr(16字节),这样两种结构可以安全地进行强制转换。
示例(服务器绑定本地地址):
cpp
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
// 或指定具体IP
// inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr);
bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
示例(客户端连接):
cpp
struct sockaddr_in server_addr;
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
struct sockaddr_un ------ Unix域套接字地址结构
cpp
struct sockaddr_un {
sa_family_t sun_family; // 总是 AF_UNIX 或 AF_LOCAL
char sun_path[108]; // 文件系统路径(以'\0'结尾)
};
-
sun_family:设为AF_UNIX或AF_LOCAL。 -
sun_path:一个文件路径字符串(长度不超过108字节,包括结尾NUL)。这个路径会在文件系统中创建一个特殊文件,用于进程间通信。
特点:
-
不涉及网络协议栈,纯内存/文件系统通信,效率高。
-
支持流式 (
SOCK_STREAM)和数据报 (SOCK_DGRAM)。 -
路径可以是抽象地址 (以
\0开头,不占用文件系统名),比如"\0hidden_socket"。
示例(Unix域服务器监听):
cpp
struct sockaddr_un addr;
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
unlink("/tmp/mysocket"); // 删除旧文件
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/mysocket", sizeof(addr.sun_path) - 1);
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sockfd, 5);
示例(客户端连接):
cpp
struct sockaddr_un addr;
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/mysocket");
connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
三者的关系与使用规则
| 结构 | 对应的协议族 | 大小 | 主要字段 | 适用函数参数类型 |
|---|---|---|---|---|
struct sockaddr |
通用(不直接使用) | 16字节 | sa_family, sa_data |
所有套接字函数的形参类型 |
struct sockaddr_in |
IPv4(AF_INET) | 16字节 | sin_family, sin_port, sin_addr |
需强制转换为 (struct sockaddr*) |
struct sockaddr_un |
Unix域(AF_UNIX) | 110字节+ | sun_family, sun_path |
需强制转换为 (struct sockaddr*) |
上面那么多只需记住下面几句
-
struct sockaddr:通用类型,仅用于函数形参和类型转换,不直接实例化。 -
struct sockaddr_in:IPv4专用,用得最多,必须设置端口和IP的网络字节序。 -
struct sockaddr_un:Unix域专用,用于本地高效IPC,使用文件系统路径。
sockaddr 是"抽象基类",sockaddr_in 和 sockaddr_un 是"具体派生类";在C语言中通过强制转换实现多态,所有套接字函数都用 struct sockaddr* 作为地址参数。
📚四、网络套接字接口

socket函数的作用是创建套接字, 套接字的本质是一个文件描述符, 这个套接字会在后续中起重要作用. 第二个函数bind, 它的作用是: 将本服务的IP和端口号绑定到操作系统内部,供外部来访问. 而最后三个函数: listen和accept是TCP通信中需要用到的, 它们表示: listen用于开始监听是否有请求到来. accept用于将到来的请求拿到内存当中做解析. 而connect函数用于发送TCP请求的一方调用,与服务器建立连接
📖4.1 socket
socket() ------ 创建一个通信端点
cpp
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
-
作用 :创建一个套接字,内核返回一个文件描述符(非负整数),后续操作均使用这个fd。
-
参数:
-
domain:协议族(地址族)。-
AF_INET:IPv4 -
AF_INET6:IPv6 -
AF_UNIX:本地进程间通信(Unix域)
-
-
type:套接字类型。-
SOCK_STREAM:流式套接字,提供可靠、面向连接的字节流(通常对应TCP)。 -
SOCK_DGRAM:数据报套接字,提供不可靠、无连接的消息(通常对应UDP)。 -
SOCK_RAW:原始套接字,可直接操作IP层。
-
-
protocol:指定具体协议,通常设为0,表示使用给定domain和type的默认协议(TCP或UDP)。也可以显式指定IPPROTO_TCP或IPPROTO_UDP。
-
-
返回值 :成功返回文件描述符(>=0),失败返回-1并设置
errno。
示例:
cpp
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
📖4.2 bind
bind() ------ 将套接字绑定到本地地址
cpp
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
作用 :将套接字与一个本地IP地址和端口关联起来。服务器必须
bind一个众所周知的端口,客户端一般不需要显式bind(内核会自动分配临时端口)。 -
参数:
-
sockfd:由socket()返回的套接字描述符。 -
addr:指向struct sockaddr的指针,实际上根据不同协议使用不同的结构体(如struct sockaddr_in),需要强制转换。 -
addrlen:addr结构体的长度,使用sizeof即可。
-
-
返回值:成功返回0,失败返回-1。
重点 :需要构造sockaddr_in结构体,并注意端口号和IP地址必须使用网络字节序 (通过htons、htonl转换)。
cpp
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888); // 端口8888
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0,监听所有网卡
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
close(listen_fd);
exit(EXIT_FAILURE);
}
📖4.3 listen
listen() ------ 监听连接
cpp
#include <sys/socket.h>
int listen(int sockfd, int backlog);
-
作用 :将套接字从主动连接状态转变为被动监听状态,告诉内核该套接字用于接受传入连接。同时指定连接队列的最大长度。
-
参数:
-
sockfd:已经bind但尚未监听的套接字。 -
backlog:已完成连接队列(全连接队列)的最大长度。内核通常会为这个值加上一些余量。典型值设置为128或更高(视系统限制SOMAXCONN而定)。
-
-
返回值:成功返回0,失败返回-1。
内核队列机制(后面会详细介绍,这里只见识见识这个接口知道怎么用即可):
-
半连接队列(SYN队列):收到SYN但尚未完成三次握手的连接。
-
全连接队列(Accept队列):已完成三次握手,等待
accept()取走的连接。
当全连接队列满时,新的连接行为取决于系统设置(默认会丢弃或发送RST)。
示例:
cpp
if (listen(listen_fd, 128) == -1) {
perror("listen");
close(listen_fd);
exit(EXIT_FAILURE);
}
📖4.4 accept
accept() ------ 接受一个连接
cpp
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
作用 :从监听套接字的全连接队列 中取出第一个连接,并返回一个新的套接字描述符,专门用于与该客户端通信。原监听套接字继续监听新连接。
-
参数:
-
sockfd:监听套接字。 -
addr:输出参数,返回客户端的地址结构(如IP和端口)。如果不需要,可以设为NULL。 -
addrlen:输入输出参数,传入时指向addr结构体的大小,返回时填充实际地址长度。
-
-
返回值 :成功返回新的连接套接字fd,失败返回-1。如果监听套接字是非阻塞模式且没有等待的连接,会返回-1并设置
errno为EAGAIN或EWOULDBLOCK。
注意 :默认accept()会阻塞直到有连接到来。若要非阻塞,需先设置O_NONBLOCK标志。(通常设置就是默认)
示例:
cpp
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (conn_fd == -1) {
perror("accept");
continue; // 或退出
}
printf("New connection from %s:%d\n", inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
📖4.5 connect
connect() ------ 建立连接(客户端)
cpp
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
作用:客户端主动发起TCP三次握手,与服务器建立连接。
-
参数:
-
sockfd:客户端的套接字描述符。 -
addr:服务器地址(IP+端口),使用网络字节序。 -
addrlen:地址结构体长度。
-
-
返回值:成功返回0,失败返回-1。
阻塞行为 :默认阻塞直到连接成功或失败(超时通常75秒)。可设置为非阻塞模式,并用select/epoll等待。
示例:
cpp
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("connect");
close(sockfd);
exit(EXIT_FAILURE);
}
注意 :客户端通常不需要bind,内核会自动选择一个临时端口和本机IP。如果需要指定源IP(多网卡),可以显式bind后再connect。
📖4.6 sendto、write
sendto() 和 write() ------ 发送数据
cpp
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t write(int sockfd, const void *buf, size_t len);
-
作用 :
sendto()是 UDP 数据发送的核心系统调用 。它最大的特点是:每次发送时都直接指定目标地址(IP + 端口),无需预先建立连接。 -
参数:
-
sockfd:套接字描述符(通常是 UDP 类型SOCK_DGRAM)。 -
buf:要发送的数据缓冲区(用户态)。 -
len:要发送的字节数(不能超过底层限制,UDP 一般建议不超过 64KB-头部)。 -
flags:控制发送行为(常用0、MSG_DONTWAIT、MSG_NOSIGNAL)。 -
dest_addr:目标地址结构(IP 和端口),采用网络字节序。 -
addrlen:dest_addr指向结构体的长度(例如sizeof(struct sockaddr_in))。
-
-
返回值 :
>0:实际发送的字节数(UDP 下通常等于len,除非发生错误)。-1:发生错误,错误码存入errno。
📖4.7 recvfrom、read
recvfrom() 和 read() ------ 接收数据
cpp
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t read(int sockfd, void *buf, size_t len);
-
作用 :
recvfrom()是 Linux 网络编程中用于从套接字接收数据的系统调用,最重要的特点是它能够同时接收数据和获取发送方的地址。它与read()、recv()的最大区别在于:recvfrom()不仅返回数据,还会填充数据来源的地址结构(例如 IP 和端口),使其成为 UDP 服务器 以及任何需要知道数据来源场景的标准选择。 -
参数:
-
sockfd:套接字描述符(UDP 或 TCP 都可以使用,但 UDP 更常用)。 -
buf:用户缓冲区指针,用于存放接收到的数据。 -
len:缓冲区大小(最多可接收的字节数)。 -
flags:控制接收行为的标志(常用0、MSG_DONTWAIT、MSG_PEEK等)。 -
src_addr:输出参数,返回数据发送方的协议地址(如 IP + 端口) 。如果不需要,可以填NULL。 -
addrlen:值-结果参数 。传入时指向src_addr的缓冲区长度的地址,返回时填充实际地址长度。
-
-
返回值:
-
>0:成功接收的字节数。 -
0:对端已关闭连接(仅对 TCP 有意义;UDP 收到长度为 0 的数据报可能返回 0,但极少见)。 -
-1:发生错误,错误码存于errno。
-
TCP流式读取建议:由于TCP没有消息边界,应用层协议通常需要自行定义分隔(如固定长度头、特殊分隔符)。循环读取直到满足要求。
示例(简单接收):
cpp
char buffer[1024];
// 读取数据
struct sockaddr_in peer;
socklen_t len = sizeof(peer); //必填
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
上述讲了那么多的接口,它们是不是系统调用呢?
实际上它们大多数是标准C库函数中封装的,但是这些套接字API在底层依然是通过系统调用实现的,只不过这个映射过程并不是简单的一对一 ,本质的调用逻辑:将调用包裹成一个请求,再通过软中断指令等底层机制触发一个"陷入内核"的动作来执行真正的系统调用
cpp你的代码: socket() │ ▼ 用户态 (User Space) 内核态 (Kernel Space) ┌─────────────────┐ ┌────────────────────────────────────┐ │ glibc 封装函数 │ │ Linux Kernel │ │ (socket()) │ │ │ │ │ │ │ │ │ 触发系统调用中断 │──────│──► 系统调用表查找函数入口 │ └─────────────────┘ │ │ │ │ ▼ │ │ 1. (旧) socketcall() 统一入口 │ │ 2. (新)独立的 sys_socket() 接口 │ │ │ │ 随后执行协议栈代码完成工作 │ └────────────────────────────────────┘因此,虽然你调用的是C库函数,但从本质上讲,这些套接字接口的全部核心工作,确实是由内核的系统调用完成的。
📚五、总结
本篇博客我们主要了解了套接字的定义及其接口使用,小结一下:
socket套接字的本质:一种网络文件描述符,通过这种方式,可以类似文件操作,从而完成网络数据的发送与接收。是应用层到传输层的中间件!
**socketaddr结构体:**通过一套方法,应对三种套接字种类的编程,体现多态的思想!
套接字接口使用总结:
cpp
socket(AF_INET, SOCK_DGRAM, 0)
1. AF_INET针对IPv4(常用),AF_INET6针对IPv6
2. SOCK_DGRAM针对数据报,SOCK_STREAM针对字节流
bind(_sockfd, (struct sockaddr *)&local, (socklen_t)sizeof(local))
1. _sockfd:socket的返回值
2. local:绑定到对应的IP地址和端口(而这些信息我们已经初始化这个结构体中)
recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len)
1. _sockfd:socket的返回值(这是服务端(接收信息端)创建的套接字的返回值不要理解错误)
2. buffer:用来接收数据存储的缓冲区
3. sizeof(buffer) - 1:文件字符串的读取不默认带'\0',-1为了下面的代码在结尾处+0
4. 0:阻塞式接收
5. (struct sockaddr *)&peer:输出型参数,给一个空的结构体,去拿接收客户端的IP和端口
6. &len:输入输出型参数:拿对应填充结构体的大小
sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server))
1. _sockfd:socket的返回值(这是客户端(发送信息端)创建的套接字的返回值不要理解错误)
2. message.c_str():发送的信息,这里.c_str()是string里的成员函数,只是转成c式的字符串,后面带'\0'而已
3. message.size():发送信息的大小
4. 0:常用参数为0
5. (struct sockaddr*)&server:这里不是输出型参数,是为了告诉服务端发送信息的这一端的IP和端口
6. sizeof(server):这个结构体的字段长度,用sizeof不计算后面'\0'
