计算机网络篇4:socket 编程接口与 sockaddr 通用结构的设计

发布时间:2026/10/13 10:13:00
计算机网络篇4:socket 编程接口与 sockaddr 通用结构的设计
本文收录于「流浪」的系列专栏Linux系统⚙️C数据结构与算法PythonLangChain LangGraph️MySQL 数据库Git 工具计算机网络LLM大厂面试、八股学习筑基专栏 博客主页流浪 原创首发于 CSDN前言计算机网络篇3把「数据找到进程」的最后一段路修通了端口号加 IP 拼成 socket全网唯一定位一个进程。但 socket 不只是个定位坐标它还是一组编程接口的名字——socket、bind、listen、accept、connect 五个接口共用一套参数规范统一的入口是 sockaddr。本篇讲清四件事五个接口各自干什么、为什么一种接口要同时管网络、本地、原始三种通信、sockaddr 靠前两字节怎么把不同结构区分开、填地址前还要过的字节序这一关。一、传输层的两个典型协议 TCP 与 UDP1.1 TCP 与 UDP 在连接、可靠性、数据形态上正好相反1. TCP 是传输层有连接、可靠传输、面向字节流的协议TCP 是传输层协议通信前要先建立连接建好连接再传数据它保证可靠传输发出去的数据对方收不到会重传顺序也不会乱数据形态是面向字节流发方写进去的是一串字节收方按流的方式读2. UDP 是传输层无连接、不可靠传输、面向数据报的协议UDP 也是传输层协议但不建立连接发方直接把数据报扔出去就完事它不保证可靠传输丢了不重传、乱了不重排发出去就不管了数据形态是面向数据报发方发一个完整的报文收方也收一个完整的报文3. TCP 可靠要做更多事复杂、占有资源多UDP 不可靠简单、用资源少TCP 要维持连接、要确认重传、要管顺序做的事情多协议就复杂占有的系统资源也多UDP 什么都不保证协议头短、逻辑简单用的资源自然少可靠和简单是一组取舍要可靠就得付出复杂度和资源的代价4. 业务场景按可靠性需求选用 TCP 或 UDP对可靠性要求高的场景比如网页、文件传输、支付选 TCP对实时性要求高、能容忍少量丢包的场景比如直播、语音、游戏选 UDP选型的判据就一条业务能不能容忍丢包不能容忍选 TCP能容忍选 UDPTCP 可靠但复杂、占资源多UDP 简单但不可靠、用资源少选谁由业务对可靠性的需求决定。二、socket 编程接口的五个常见 API2.1 socket、bind、listen、accept、connect 是网络编程的五件套#includesys/socket.h1. socket 创建一个套接字返回一个文件描述符// 创建套接字domain 填地址族(AF_INET 即 IPv4)type 填类型(SOCK_STREAM 即 TCP / SOCK_DGRAM 即 UDP)// protocol 填协议(0 表示按前两个参数选默认协议)// 返回值成功返回套接字的文件描述符失败返回 -1intsocket(intdomain,inttype,intprotocol);socket 这个接口干的事就是创建一个套接字返回值是一个文件描述符套接字在 Linux 里也是一种文件往后对它的读写就和对普通文件读写一样创建时得告诉它要哪种地址族、哪种类型、用哪个协议比如 AF_INET 加 SOCK_STREAM 加 0 就是 IPv4 的 TCP 套接字2. bind 把套接字和本地的 IP 加端口号绑在一起// 绑定地址sockfd 是 socket 返回的描述符addr 是填好的地址结构(要强转成 sockaddr*)addrlen 是地址结构的大小// 返回值成功返回 0失败返回 -1intbind(intsockfd,conststructsockaddr*addr,socklen_t addrlen);socket 刚创建出来还没绑定地址bind 干的就是把一个 IP 和端口号绑到这个套接字上服务器必须 bind因为它要在某个固定端口上等客户端来连客户端一般不显式 bind让系统自动分配端口绑定之后发到这个 IP 加端口的报文就会被内核交给这个套接字3. listen 把套接字置为监听状态准备接收连接// 开启监听sockfd 是已绑定的套接字描述符backlog 是等待连接队列的最大长度// 返回值成功返回 0失败返回 -1intlisten(intsockfd,intbacklog);bind 只是占了地址还没进入「能接连接」的状态listen 干的就是把这步走完调用 listen 后套接字从普通的未连接状态变成被动套接字开始监听客户端的连接请求它还有一个参数指定等待连接队列的长度超过这个数的连接请求会被拒绝4. accept 从已完成连接队列里取一个连接返回新的文件描述符// 接受连接sockfd 是监听描述符addr 用来存客户端的地址(不关心可传 NULL)addrlen 是地址结构大小的指针// 返回值成功返回新连接的文件描述符(用它和客户端通信原监听描述符继续接新连接)失败返回 -1intaccept(intsockfd,structsockaddr*addr,socklen_t*addrlen);accept 是服务器专用的接口它从已完成连接的队列里取出一个连接取出来后返回的是一个新的文件描述符这个新 fd 才是用来和客户端真正通信的原来的监听 fd 继续接新连接如果没有已完成连接accept 会阻塞直到有客户端连进来5. connect 是客户端用来向服务器发起连接的接口// 发起连接sockfd 是客户端套接字描述符addr 是服务器的地址结构(要强转成 sockaddr*)addrlen 是地址结构的大小// 返回值成功返回 0(TCP 下内部走完三次握手)失败返回 -1intconnect(intsockfd,conststructsockaddr*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、原始 socket1. 网络 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 是网络通信结构要端口号和 IP1. 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 和 readsockaddr_un 里有一个 sun_path 字段填的是文件系统里的一个路径这个路径文件只是个占位的地址标记不像普通文件那样要 open、read、write 去读写内容内核靠这个路径把两个进程的套接字关联起来通信走的是内核缓冲区不落盘sockaddr_un 用文件路径当地址本地通信不跨网络地址结构和网络的 sockaddr_in 完全不同。4.4 传参时按场景填 sockaddr_in 或 sockaddr_un接口统一收 sockaddr1. 网络通信就填 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_instructsockaddr_inserver_addr;server_addr.sin_familyAF_INET;// IPv4地址族server_addr.sin_porthtons(8888);// 端口转网络字节序server_addr.sin_addr.s_addrinet_addr(127.0.0.1);//IP地址memset(server_addr.sin_zero,0,8);// 置0对齐intfdsocket(AF_INET,SOCK_STREAM,0);// 2. 强制转成 struct sockaddr* 传给bindbind(fd,(structsockaddr*)server_addr,sizeof(server_addr));//本地通信//1. 定义填充 sockaddr_unstructsockaddr_unun_addr;un_addr.sun_familyAF_UNIX;// UNIX域地址族strcpy(un_addr.sun_path,/tmp/mysock);// 本地套接字文件路径intfdsocket(AF_UNIX,SOCK_STREAM,0);//2. 强制转成 struct sockaddr* 传给bindbind(fd,(structsockaddr*)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_datasockaddr_in 是 IPv4 专用结构把原始数据拆成了具体的 sin_port端口、sin_addrIP、sin_zero填充位。区别在于 sockaddr 是通用容器、sockaddr_in 是具体填充。bind 等接口的形参统一是 sockaddr*所以传 sockaddr_in 时要强转。强转能成立是因为两者的首字段都是地址族C 语言保证结构体首成员地址等于结构体地址强转后首字段位置重叠内核读前两个字节的地址族就能判断真实结构再向下转型。这是 C 语言用结构体内存布局模拟面向对象多态的经典设计。【真题·转述自 CSDN 博客sockaddr 和 sockaddr_in 的区别与联系】这一篇把 socket 的接口规范从头到尾串了一遍五个接口共用一套参数一种形参收网络、本地、原始三种实参靠前两字节的地址类型做多态分发最后用四个字节序函数把字段转成网络大端。评论区聊聊你第一次看到 (struct sockaddr*)serv 这种强转时有没有想过它凭什么不会转出错