11.1 Sockets
有一个哲学问题贯穿了 Unix 的历史:"一切皆文件"。
按照这个哲学,网络通信应该和读写本地文件一样自然:open(),read(),write(),close()。但当你真正深入到网络子系统的实现时,会发现内核为了维持这种"文件"的假象,在背后做了一系列极其复杂的伪装。
更让人困惑的是,为了实现这套接口,内核维护了两个名字极度相似、但职责完全不同的结构体。为什么要把一个简单的通信端点拆成两半?这不仅是历史包袱,更是理解整个 Linux 网络栈的第一道门槛。
这一节,我们先从用户视角看这套接口是怎么设计的,然后撕开这层接口,看看内核里藏着的那个"双胞胎"谜题。
套接字的类型
Socket API 支持多种通信类型。我们这里先扫一眼,重点放在本章会深入讨论的几种上。
Stream Sockets (SOCK_STREAM) 把它想象成打电话。 首先,你得先拨号建立连接,这就像 TCP 的三次握手——如果对方没接通(占线、关机),你这一通电话是打不出去的。一旦接通,信道就是独占的、双向的。你说什么,对方就听到什么,话音(字节流)是有序到达的,不会丢字。 TCP 就是这种机制的代名词。如果你受不了数据出错或乱序,选它准没错。
Datagram Sockets (SOCK_DGRAM) 这就像寄明信片。 你把明信片扔进邮筒,转身就走。既不邮局也不保证它什么时候送到,甚至不保证它一定能送到——可能半路被风吹走了,可能晚了三天才到,可能对方收到了两份一样的。好处是你不需要先打电话问对方"我可以寄信了吗",直接寄就行。 UDP 就是这个模式。如果你在写实时音视频或者游戏,对丢包容忍度较高,但对延迟极度敏感,明信片就是你的首选。
但请记住"明信片"这个比喻有一个地方是不严谨的:在局域网环境极其通畅的情况下,UDP 几乎不丢包,这时候它看起来像是一个可靠的快递。但千万别被这种表象迷惑,一旦跨公网,它立马变回那张可能丢失的明信片。
Raw Sockets (SOCK_RAW)
这是一个「上帝模式」接口。
它允许你绕过传输层,直接跟 IP 层对话。如果你想自己构造 IP 头、实现自己的协议,或者写一个类似 ping 的工具(需要 ICMP 访问权限),你就得用这个。
当然,这种特权通常需要 root 权限——毕竟内核可不想让你随便伪造一个源 IP 去搞事情。
其他类型 Linux 还支持其他几种较为特殊的类型:
- SOCK_RDM:提供可靠传递消息,主要用于 TIPC(透明进程间通信协议)。
- SOCK_SEQPACKET:类似 SOCK_STREAM,也是面向连接的,但它保留了记录边界。
- SOCK_DCCP:数据报拥塞控制协议,一种结合了 TCP 和 UDP 特性的传输协议。
- SOCK_PACKET:在
AF_INET协议族中已被认为过时。
Socket API 的核心方法
用户空间的程序员每天都在和下面这些函数打交道。但在内核视角下,它们不仅仅是函数调用,而是通向复杂内核子系统的入口:
socket():创建一个新的 Socket。这不仅仅是malloc一块内存那么简单,背后涉及协议族的查找和初始化。bind():给 Socket 赋予一个「身份」(本地端口和 IP 地址)。send()/recv():收发数据的基石。listen():让 Socket 进入「监听」模式,准备接受连接请求。注意,只有"打电话"(TCP)才需要这个,"寄明信片"(UDP)用不着。accept():从等待队列里拿出一个连接,返回一个新的 Socket 描述符。这是 TCP 服务器端最关键的一步。connect():发起连接。对 TCP 来说是三次握手的开始;对 UDP 来说,它只是设置了一个默认的目标地址(给明信片写死了收件人),方便后续直接用send()。
在内核代码里(net/socket.c),所有的这些系统调用最终都会汇聚到 socketcall() 方法中进行统一分发。
本书的重点是内核网络实现,而不是用户空间的 API 用法指南。所以,关于如何写一个 connect() 循环或者如何处理 EINTR 错误码,我们不会展开。
内核里的 Socket 是什么?
仅仅知道这些 API 怎么调用,离真正搞懂网络子系统还有十万八千里。
真正的麻烦藏在内核里。当你在用户空间调用 socket() 时,内核并没有像打开普通文件那样只创建一个 inode,而是精心策划了一场"分裂":它同时创建了两样东西——struct socket 和 struct sock。
这两个名字长得像双胞胎,但完全是两个物种。为什么要这么设计?把它们合并了不行吗?
如果你觉得这个设计有点奇怪,那你的直觉是对的。这背后隐藏着 Linux 网络栈最核心的分层哲学。下一节,我们将深入源码,把这对"双胞胎"放在手术台上解剖,顺便看一下 struct msghdr——它是用户空间和内核之间交换数据时的"旅行箱"。