
📚 本文收录于「流浪」的系列专栏
| 🐧 Linux系统 | ⚙️ C++ |
| 📊 数据结构与算法 | 🐍 Python |
| 🔗 LangChain & LangGraph | 🗄️ MySQL 数据库 |
| 🌿 Git 工具 | 🌐 计算机网络 |
| 🤖 LLM | 💯 大厂面试、八股 |
| 📚 学习筑基专栏 |
🏠 博客主页:流浪 | 📝 原创首发于 CSDN
前言:计算机网络篇3把「数据找到进程」的最后一段路修通了,端口号加 IP 拼成 socket,全网唯一定位一个进程。但 socket 不只是个定位坐标,它还是一组编程接口的名字——socket、bind、listen、accept、connect 五个接口共用一套参数规范,统一的入口是 sockaddr。本篇讲清四件事:五个接口各自干什么、为什么一种接口要同时管网络、本地、原始三种通信、sockaddr 靠前两字节怎么把不同结构区分开、填地址前还要过的字节序这一关。
一、传输层的两个典型协议 TCP 与 UDP
1.1 TCP 与 UDP 在连接、可靠性、数据形态上正好相反
1. TCP 是传输层有连接、可靠传输、面向字节流的协议
- TCP 是传输层协议,通信前要先建立连接,建好连接再传数据
- 它保证可靠传输,发出去的数据对方收不到会重传,顺序也不会乱
- 数据形态是面向字节流,发方写进去的是一串字节,收方按流的方式读
2. UDP 是传输层无连接、不可靠传输、面向数据报的协议
- UDP 也是传输层协议,但不建立连接,发方直接把数据报扔出去就完事
- 它不保证可靠传输,丢了不重传、乱了不重排,发出去就不管了
- 数据形态是面向数据报,发方发一个完整的报文,收方也收一个完整的报文
3. TCP 可靠要做更多事,复杂、占有资源多;UDP 不可靠简单、用资源少
- TCP 要维持连接、要确认重传、要管顺序,做的事情多,协议就复杂,占有的系统资源也多
- UDP 什么都不保证,协议头短、逻辑简单,用的资源自然少
- 可靠和简单是一组取舍,要可靠就得付出复杂度和资源的代价
4. 业务场景按可靠性需求选用 TCP 或 UDP
- 对可靠性要求高的场景,比如网页、文件传输、支付,选 TCP
- 对实时性要求高、能容忍少量丢包的场景,比如直播、语音、游戏,选 UDP
- 选型的判据就一条:业务能不能容忍丢包,不能容忍选 TCP,能容忍选 UDP
TCP 可靠但复杂、占资源多,UDP 简单但不可靠、用资源少,选谁由业务对可靠性的需求决定。
二、socket 编程接口的五个常见 API
2.1 socket、bind、listen、accept、connect 是网络编程的五件套
#include <sys/socket.h>
1. socket 创建一个套接字,返回一个文件描述符
// 创建套接字:domain 填地址族(AF_INET 即 IPv4),type 填类型(SOCK_STREAM 即 TCP / SOCK_DGRAM 即 UDP),
// protocol 填协议(0 表示按前两个参数选默认协议)
// 返回值:成功返回套接字的文件描述符,失败返回 -1
int socket(int domain, int type, int protocol);
- socket 这个接口干的事就是创建一个套接字,返回值是一个文件描述符
- 套接字在 Linux 里也是一种文件,往后对它的读写就和对普通文件读写一样
- 创建时得告诉它要哪种地址族、哪种类型、用哪个协议,比如 AF_INET 加 SOCK_STREAM 加 0 就是 IPv4 的 TCP 套接字
2. bind 把套接字和本地的 IP 加端口号绑在一起
// 绑定地址:sockfd 是 socket 返回的描述符,addr 是填好的地址结构(要强转成 sockaddr*),addrlen 是地址结构的大小
// 返回值:成功返回 0,失败返回 -1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- socket 刚创建出来还没绑定地址,bind 干的就是把一个 IP 和端口号绑到这个套接字上
- 服务器必须 bind,因为它要在某个固定端口上等客户端来连;客户端一般不显式 bind,让系统自动分配端口
- 绑定之后,发到这个 IP 加端口的报文就会被内核交给这个套接字
3. listen 把套接字置为监听状态,准备接收连接
// 开启监听:sockfd 是已绑定的套接字描述符,backlog 是等待连接队列的最大长度
// 返回值:成功返回 0,失败返回 -1
int listen(int sockfd, int backlog);
- bind 只是占了地址,还没进入「能接连接」的状态,listen 干的就是把这步走完
- 调用 listen 后,套接字从普通的未连接状态变成被动套接字,开始监听客户端的连接请求
- 它还有一个参数指定等待连接队列的长度,超过这个数的连接请求会被拒绝
4. accept 从已完成连接队列里取一个连接,返回新的文件描述符
// 接受连接:sockfd 是监听描述符,addr 用来存客户端的地址(不关心可传 NULL),addrlen 是地址结构大小的指针
// 返回值:成功返回新连接的文件描述符(用它和客户端通信,原监听描述符继续接新连接),失败返回 -1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- accept 是服务器专用的接口,它从已完成连接的队列里取出一个连接
- 取出来后返回的是一个新的文件描述符,这个新 fd 才是用来和客户端真正通信的,原来的监听 fd 继续接新连接
- 如果没有已完成连接,accept 会阻塞,直到有客户端连进来
5. connect 是客户端用来向服务器发起连接的接口
// 发起连接:sockfd 是客户端套接字描述符,addr 是服务器的地址结构(要强转成 sockaddr*),addrlen 是地址结构的大小
// 返回值:成功返回 0(TCP 下内部走完三次握手),失败返回 -1
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- connect 是客户端用的,它拿着服务器的 IP 和端口号,主动向服务器发起连接
- 对 TCP 来说,connect 内部会触发三次握手,握手成功连接才算建立
- 连接建立后,客户端就可以用这个套接字发数据了
五个接口各管一段:socket 建描述符、bind 占地址、listen 开监听、accept 取连接、connect 发起连接,串起来就是一次通信的完整流程。
2.2 五个接口的参数里都出现了 const struct sockaddr* addr,它就是接口规范的统一入口
1. 五个接口的入参里都有一段 const struct sockaddr* addr
- 看上面的函数签名,bind、connect、accept 的参数里都有一段 const struct sockaddr* addr
- 不管是绑定本地地址、连接远端地址、还是取出客户端地址,传进去的地址指针都是 sockaddr*
- 但 sockaddr 这个结构本身又很简单,只有一个地址族加一段 14 字节的原始数据,根本看不出 IP 和端口在哪

2. sockaddr 是接口规范的统一入口,五个接口都靠它收地址
- 既然真正要填的是 IP 和端口号,为什么接口不直接收 sockaddr_in,偏偏要收 sockaddr
- 这个问题的答案就是 socket 接口的设计规范:设计者只想做一种通信接口,让它能同时管网络、本地、原始三种通信
- sockaddr 就是这套规范的统一入口,它的设计动机和区分机制是本篇接下来的主线
五个接口共用一套参数规范,sockaddr 是这套规范的统一入口。
三、网络通信的本质是进程间通信,socket 要管三种通信
3.1 网络通信沿用进程间通信的老前提,POSIX 统一了网络与本地
1. 网络的本质是进程间通信,socket 就是这层通信的入口
- 篇3已经讲过,网络通信的本质就是跨主机的进程间通信,两个进程借着网络读写数据
- socket 就是这层进程间通信的编程入口,进程通过它接进网络
2. System V 是本地进程间通信的标准,管不了跨主机
- 在 socket 出现之前,进程间通信走的是 System V 那一套,比如消息队列、共享内存、信号量
- System V 是本地进程间通信标准,它只能管同一台主机上的进程,管不了跨主机
- 这一套在 Linux 系统系列里已经讲过,本篇只做技术层面的对照
3. POSIX 标准是网络通信标准,但它也能进行本地通信
- socket 接口走的是 POSIX 标准,它一开始就是为网络通信设计的
- 但 POSIX 的 socket 不只能做网络通信,它也能做本地通信,也就是 Unix 域间通信
- POSIX 既管网络又管本地,一套接口把两件事都覆盖了
4. POSIX 既管网络又管本地,统一了标准,System V 因此被淘汰
- System V 只能做本地通信,要做网络通信得另学一套,标准割裂
- POSIX 的 socket 一套接口既能做网络又能做本地,标准统一,学一套就够
- 标准统一带来的便利让 POSIX 成了主流,System V 在网络时代就逐渐被淘汰了
POSIX 一套接口把网络和本地都收编了,标准统一是 System V 退场的根本原因。
3.2 三种 socket:网络 socket、本地 socket、原始 socket
1. 网络 socket 用来做跨主机的进程间通信
- 网络 socket 是最常见的,两台主机上的进程隔着网络通信就靠它
- 它的地址结构里填的是 IP 加端口号,前面篇3讲的就是这一种

2. 本地 socket 用来做同主机的进程间通信
- 本地 socket 也叫 Unix 域间 socket,它只管同一台主机上两个进程的通信
- 它不走网络,地址结构里填的是文件系统里的一个路径,靠这个路径文件把两个进程关联起来

3. 原始 socket 用来直接操作底层协议
- 原始 socket 绕过传输层,直接操作底层的协议报文
- 它一般用来抓包、自定义构造报文、做协议分析,普通业务开发几乎用不到
4. 不同应用场景导致通信特性不同,所以分三种
- 跨主机要寻址就要 IP 加端口,同主机不跨网络用文件路径就够,抓包要绕过传输层就得直接操作底层报文
- 三种场景对地址结构的要求完全不同,所以 socket 分成网络、本地、原始三种
- 但接口只有一套,这一套接口要同时管这三种通信,这就引出了 sockaddr 的设计
一套 socket 接口要同时管网络、本地、原始三种通信,这是 sockaddr 出现的根本原因。
四、sockaddr 通用结构:一种接口支持三种通信
4.1 设计者只想做一种通信接口,于是设计出 sockaddr 通用结构
1. socket 设计者不想为每种通信各做一套接口
- 如果为每种通信各做一套接口,网络通信一套 bind_in、本地通信一套 bind_un、原始通信一套 bind_raw,接口数量就爆炸了
- 而且这三套接口的流程其实都一样:创建套接字、绑定地址、监听、接连接,只是地址结构不同
- 设计者不想重复造接口,只想做一种通信接口,让它能兼容所有场景
2. 为了让一种接口支持不同场景的通信,设计出 sockaddr 通用结构
- 解决思路是抽出一个通用结构 sockaddr,作为所有接口的统一参数类型
- sockaddr 只定义所有地址结构都有的公共部分,也就是地址族加一段原始字节
- 具体的地址信息留给各自的专用结构去填,接口只认 sockaddr 这一层
sockaddr 用「只定义公共部分、具体留给派生结构」的思路,让一种接口兼容了所有通信场景。
4.2 sockaddr_in 是网络通信结构,要端口号和 IP
1. struct sockaddr_in 是网络通信专用的地址结构
- 做网络通信时,真正填地址用的是 struct sockaddr_in 这个结构
- 它是 sockaddr 在网络场景下的具体化,把 sockaddr 里那段原始字节拆成了有名字的字段
2. sockaddr_in 里要填端口号和 IP 地址
- sockaddr_in 里有一个 16 位的 sin_port 字段填端口号
- 还有一个 32 位的 sin_addr 字段填 IPv4 地址
- 这两个字段正好对应篇3讲的「IP 定位主机、端口定位进程」,拼起来就是网络进程的身份

sockaddr_in 把 sockaddr 里的原始字节拆成了有名字的端口和 IP 字段,这是网络场景下对通用结构的具体化。
4.3 sockaddr_un 是本地通信结构,要文件路径
1. struct sockaddr_un 是本地通信专用的地址结构
- 做本地通信时,真正填地址用的是 struct sockaddr_un 这个结构
- 它是 sockaddr 在本地场景下的具体化,字段和 sockaddr_in 完全不同
2. 本地通信用文件路径当地址,不需要像普通文件那样 open 和 read
- sockaddr_un 里有一个 sun_path 字段,填的是文件系统里的一个路径
- 这个路径文件只是个占位的地址标记,不像普通文件那样要 open、read、write 去读写内容
- 内核靠这个路径把两个进程的套接字关联起来,通信走的是内核缓冲区,不落盘

sockaddr_un 用文件路径当地址,本地通信不跨网络,地址结构和网络的 sockaddr_in 完全不同。
4.4 传参时按场景填 sockaddr_in 或 sockaddr_un,接口统一收 sockaddr
1. 网络通信就填 sockaddr_in
- 做网络通信时,先填好一个 sockaddr_in 结构,把地址族设成 AF_INET,端口和 IP 填进去
- 传给 bind 或 connect 时,把这个 sockaddr_in 的指针传进去
2. 本地通信就填 sockaddr_un
- 做本地通信时,先填好一个 sockaddr_un 结构,把地址族设成 AF_UNIX,路径填进去
- 传给 bind 或 connect 时,把这个 sockaddr_un 的指针传进去
3. 接口形参统一是 sockaddr*,调用时强转传入
- 不管传的是 sockaddr_in 还是 sockaddr_un,接口的形参统一都是 const struct sockaddr*
- 调用时要把具体结构的指针强制转换成 sockaddr* 再传入,比如 (struct sockaddr*)&serv
- 接口拿到这个指针后,再靠地址族字段判断真实结构,这就是 sockaddr 这套规范的核心
//网络通信
// 1. 定义并填充 sockaddr_in
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET; // IPv4地址族
server_addr.sin_port = htons(8888); // 端口,转网络字节序
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //IP地址
memset(&server_addr.sin_zero, 0, 8); // 置0,对齐
int fd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 强制转成 struct sockaddr* 传给bind
bind(fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
//本地通信
//1. 定义填充 sockaddr_un
struct sockaddr_un un_addr;
un_addr.sun_family = AF_UNIX; // UNIX域地址族
strcpy(un_addr.sun_path, "/tmp/mysock"); // 本地套接字文件路径
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
//2. 强制转成 struct sockaddr* 传给bind
bind(fd, (struct sockaddr *)&un_addr, sizeof(un_addr));
sockaddr 是接口规范的统一入口,一种形参收三种实参结构,靠强转和地址族字段把两边对接起来。
五、前两字节的地址类型:sockaddr 怎么做多态分发
5.1 sockaddr_in 和 sockaddr_un 的前两字段都是地址类型
1. sockaddr_in 的前两字段是 sin_family,存的是地址类型
- sockaddr_in 的第一个字段是 sin_family,类型是 sa_family_t,占 16 位
- 这个字段存的就是地址类型,告诉内核这个结构是哪种地址族
2. sockaddr_un 的前两字段是 sun_family,存的也是地址类型
- sockaddr_un 的第一个字段是 sun_family,类型同样是 sa_family_t,占 16 位
- 不管是网络结构还是本地结构,第一个字段都是地址类型,只是名字不同
3. 网络通信填 AF_INET,本地通信填 AF_UNIX,用来区分类型
- 做 IPv4 网络通信时,sin_family 填 AF_INET
- 做本地通信时,sun_family 填 AF_UNIX
- 内核就是靠这个字段的值,区分传进来的到底按哪种结构解析
三个具体结构的首字段都是 16 位地址类型,填的值不同就代表不同的通信场景。
5.2 sockaddr 头部也有 16 位地址类型字段,靠它区分按哪种结构解析
1. sockaddr 头部也有同样的 16 位地址类型字段
- sockaddr 这个通用结构的第一个字段是 sa_family,也是 sa_family_t,占 16 位
- 它和 sockaddr_in 的 sin_family、sockaddr_un 的 sun_family 在内存里是同一个位置
- 因为强转只是换了指针类型,内存布局没变,所以三个结构的第一个字段永远重叠
2. 传参统一传 sockaddr,内核靠前两字节判断真实结构
- 应用层传进来的是 sockaddr*,内核拿到后先读前两个字节的 sa_family
- 读出来是 AF_INET,就知道真实结构是 sockaddr_in,按网络结构的字段去解析
- 读出来是 AF_UNIX,就知道真实结构是 sockaddr_un,按本地结构的字段去解析
3. 一种形参、多种实参,这就是 socket 接口的多态分发
- C 语言没有面向对象的多态,但 socket 接口靠「公共首字段加强转」模拟出了多态
- sockaddr 相当于基类,sockaddr_in、sockaddr_un 相当于派生类,sa_family 相当于类型标签
- 这是一种形参收多种实参的经典设计,用 C 的内存布局规则做到了运行时按类型分发

前两字节的地址类型是 sockaddr 多态的钥匙,内核靠它判断该按哪种结构解析。
六、文末面试题
6.1 推导题
1. 为什么 socket 接口要设计成一种通用接口管三种通信,而不是每种通信各做一套?
答(推导):如果每种通信各做一套接口,网络通信一套 bind_in、本地通信一套 bind_un、原始通信一套 bind_raw,接口数量会随地址族数量线性膨胀。但这三套接口的调用流程其实完全一样——创建套接字、绑定地址、监听、接连接,只是地址结构不同。设计者抽出一个通用结构 sockaddr 作为统一参数类型,让具体地址结构强转成 sockaddr* 传入,接口只认这一层,新增地址族时接口签名不用改。这是一种接口数量收敛、扩展性强的设计,比起各做一套更合理。
2. socket 为什么要分成网络、本地、原始三种?分流的依据是什么?
答(推导):三种通信场景对地址结构的要求完全不同——跨主机要 IP 加端口寻址、同主机不跨网络用文件路径就够、抓包要绕过传输层直接操作底层报文,地址结构填的字段不一样,所以得分三种。但分流依据不是协议层而是应用场景:跨主机场景用网络 socket、同主机场景用本地 socket、底层操作场景用原始 socket,每种场景对应一种地址结构和一种通信特性。按场景分流让每种通信各得其所,又能共用同一套接口流程,是「接口统一、结构分流」的设计。
3. sockaddr 和 sockaddr_in 是什么关系?传参时为什么要强转?
答(推导):sockaddr 是通用地址结构,相当于基类,只定义公共的地址族字段加一段原始字节;sockaddr_in 是 IPv4 网络通信的专用结构,相当于派生类,把原始字节拆成了有名字的端口和 IP 字段。两者的关系是「通用容器」和「具体填充」的关系。接口形参统一收 sockaddr,但应用层真正填的是 sockaddr_in,所以传参时要强转成 sockaddr。强转能成立的前提是 sockaddr_in 的第一个字段 sin_family 和 sockaddr 的第一个字段 sa_family 在内存里同一位置,C 语言保证结构体首成员地址等于结构体地址,所以强转后内核读前两字节拿到的就是地址族,能安全判断真实结构。
4. 为什么靠前两字节的地址类型做多态分发,而不是靠长度或额外标志位?
答(推导):靠前两字节的地址类型,是因为结构体的首成员地址等于结构体地址,C 语言保证这一点,强转后首字段位置不变,内核读前两字节一定能拿到正确的地址族。靠长度不行,因为长度要靠额外的 addrlen 参数传,内核没法只看指针就判断类型;靠额外标志位也不行,等于在结构体里再加一个字段,还是回到首字段分发的路子。地址族字段本来就是地址结构必须有的(要标识自己属于哪种地址),把它放在首字段,既满足多态分发的需要,又不增加额外开销,是复用已有字段的最优解。
6.2 真题
1. socket 编程有哪些原语?说一下各自的作用。
答(推导 · 已对照解析,转述):socket 创建一个套接字,返回文件描述符作为后续操作的句柄;bind 把套接字和本地地址(IP 加端口)绑定,服务器必须调用以在固定端口上等待连接;listen 把套接字置为监听状态,并指定等待连接队列的长度;accept 由服务器调用,从已完成连接队列取出一个连接,返回新的文件描述符用于和客户端通信,原监听 fd 继续接新连接;connect 由客户端调用,向服务器发起连接,对 TCP 来说内部触发三次握手。这五个原语串起来就是一次完整的 socket 通信流程:服务器端 socket → bind → listen → accept,客户端 socket → connect。
【真题·转述自 腾讯面经整理:Socket 原语和通信原理】
2. sockaddr 和 sockaddr_in 的区别是什么?为什么调用 bind 时要强制类型转换?
答(推导 · 已对照解析,转述):sockaddr 是通用地址结构,只含地址族 sa_family 和一段 14 字节的原始数据 sa_data;sockaddr_in 是 IPv4 专用结构,把原始数据拆成了具体的 sin_port(端口)、sin_addr(IP)、sin_zero(填充位)。区别在于 sockaddr 是通用容器、sockaddr_in 是具体填充。bind 等接口的形参统一是 sockaddr*,所以传 sockaddr_in 时要强转。强转能成立是因为两者的首字段都是地址族,C 语言保证结构体首成员地址等于结构体地址,强转后首字段位置重叠,内核读前两个字节的地址族就能判断真实结构再向下转型。这是 C 语言用结构体内存布局模拟面向对象多态的经典设计。
【真题·转述自 CSDN 博客:sockaddr 和 sockaddr_in 的区别与联系】
这一篇把 socket 的接口规范从头到尾串了一遍:五个接口共用一套参数,一种形参收网络、本地、原始三种实参,靠前两字节的地址类型做多态分发,最后用四个字节序函数把字段转成网络大端。评论区聊聊:你第一次看到 (struct sockaddr*)&serv 这种强转时,有没有想过它凭什么不会转出错?
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2502_94387000/article/details/167129848




