第 61 章 Sockets: 高级主题 (Sockets: Advanced Topics)

      +

      核心结论

      • 流式 socket 上的 partial read/writeread/write 都可能传输少于请求字节数(信号中断、非阻塞模式、异步错误);readn/writen 包装循环自动恢复直到读完/写完。

      • shutdown(sockfd, how) vs close()close 关闭双向并依赖 fd 引用计数;shutdown 直接关闭单向(SHUT_RD/SHUT_WR/SHUT_RDWR),无视其他 dup/fork 后的 fd 副本——这是「半关闭」(half-close)。

      • recv/send 是 read/write 的 socket 增强版:额外 flags 参数支持 MSG_DONTWAITMSG_PEEKMSG_WAITALLMSG_OOBMSG_NOSIGNALMSG_MORE

      • sendfile 实现 zero-copy:内核直接把 regular file 拷到 socket,避免用户态往返;HTTP 服务器常配合 TCP_CORK 合并 header + body 到同一 TCP segment。

      • TCP 状态机与 TIME_WAIT:三次握手建连(SYN → SYN/ACK → ACK),四次挥手拆连(FIN/ACK/FIN/ACK);主动关闭方经历 TIME_WAIT(2×MSL = 60s on Linux)以 (1) 处理最后 ACK 丢失 (2) 让旧 segment 在网络中过期。

      • socket 选项 SO_REUSEADDR 解决重启时报 EADDRINUSE:默认内核不让绑定还在 TIME_WAIT 的端口;SO_REUSEADDR 允许重用,避免绕开 TIME_WAIT 可靠性保证。

      本章主旨

      本章是 socket 编程「深水区」——前面第 56-60 章是基本功,本章解释常见误解和调试技能。读者需建立:(1) closeshutdown,dup/fork 后仍维持的双向通信可被 shutdown 关闭;(2) TCP 状态机解释为什么「主动关闭方经历 TIME_WAIT」、「重启 server 报 EADDRINUSE 用什么解决」;(3) sendfile + TCP_CORK 是高性能 web 服务器常用武器;(4) SO_REUSEADDR 不破坏 TIME_WAIT 的可靠性;(5) netstat / tcpdump 是必备调试工具。

      一、核心概念

      本章围绕 6 个核心概念:partial I/O、shutdown、半关闭、socket 专用 I/O、TCP 状态机与 TIME_WAIT、SO_REUSEADDR。

      概念 定义 + 重要性 实现提示

      Partial reads / writes

      流式 socket 的 read/write 可能传输少于请求的字节数(管道样,部分读 / 写);readn/writen 用循环保证读够 N 字节,途中处理 EINTR

      §61.1;TLPI Listing 61-1;readn 返回实际读取的字节数(与 read 一致),若读到 EOF 返回当前累计;注意 MSG_WAITALL 等价但不重启。

      shutdown 与 half-close

      shutdown 关闭 socket 双向通道中「单独方向」或同时;不受 fd 引用计数影响;SHUT_WR 让对端读 EOF,本地继续可读对端后续数据——ssh/rsh 用它。

      §61.2;SHUT_RDWR = SHUT_RD + SHUT_WR;TCP 上 SHUT_RD 行为跨实现差异大,应避免。

      recv/send flags

      read/writeflags 参数:MSG_DONTWAIT(per-call 非阻塞)、MSG_PEEK(查看不移走)、MSG_WAITALL(阻塞够 length)、MSG_OOB(带外数据)、MSG_NOSIGNAL(不发 SIGPIPE)、MSG_MORE(cork 数据到 segment)。

      §61.3;SUSv3 仅 MSG_OOB;其他为扩展(Linux 都有)。

      sendfile zero-copy

      sendfile(out_fd, in_fd, offset, count) 把 mmap-able regular file 直接由内核送 socket;省去用户缓冲区往返;与 TCP_CORK 配合合并 header + body。

      §61.4;Linux 2.4 起,out_fd 只能是 socket;2.6 后文件到文件不再支持。splice/tee/vmsplice 在 2.6.17 后提供更灵活的 zero-copy 选项。

      TCP 状态机 / TIME_WAIT

      三次握手(SYN → SYN/ACK → ACK)→ ESTABLISHED;四次挥手(FIN/ACK/FIN/ACK);主动关闭方经历 TIME_WAIT(2×MSL),目的 (1) 重发最后 ACK if lost;(2) 让旧 segment 过期。Linux MSL=30s,TIME_WAIT=60s。

      §61.6;其余状态:LISTEN、SYN_SENT、SYN_RECV、ESTABLISHED、FIN_WAIT1/2、CLOSING、CLOSE_WAIT、LAST_ACK、CLOSED;netstat -t 输出 state。

      SO_REUSEADDR / socket 选项

      setsockopt(sockfd, level, optname, &val, optlen) 控制 socket 行为;SO_REUSEADDR 让绑定允许重用 TIME_WAIT 状态的本地端口;其他常用:SO_KEEPALIVESO_RCVBUF/SO_SNDBUFSO_LINGERSO_BROADCASTSO_OOBINLINE 等。

      §61.9–61.10;SO_REUSEADDR 应在 listen 之前调用;TLPI Listing 61-4 给出典型用法。

      二、详细笔记

      61.1 流式 socket 的 partial read/write 与 readn/writen

      What:流式 socket 是无 record-boundary 的字节流。read 可能返回少于请求的字节数(仅返回内核缓冲区已有的数据);write 可能写入少于请求的字节数(信号中断、非阻塞模式、异步错误:TCP peer 突然 RST 等)。

      Why:应用协议如 HTTP、is_seqnum 都基于「按行读取」或「读够 N 字节」语义;若不显式循环,要么丢数据要么死锁。

      How:TLPI Listing 61-1 的 readn/writen 循环:

      // 摘自《The Linux Programming Interface》 第 61 章 Listing 61-1
      #include <unistd.h>
      #include <errno.h>
      #include "rdwrn.h"
      ssize_t readn(int fd, void *buffer, size_t n) {
          ssize_t numRead; size_t totRead; char *buf;
          buf = buffer;
          for (totRead = 0; totRead < n; ) {
              numRead = read(fd, buf, n - totRead);
              if (numRead == 0) return totRead;       /* EOF */
              if (numRead == -1) {
                  if (errno == EINTR) continue;        /* restart */
                  else return -1;
              }
              totRead += numRead;
              buf += numRead;
          }
          return totRead;
      }
      
      ssize_t writen(int fd, const void *buffer, size_t n) {
          ssize_t numWritten; size_t totWritten; const char *buf;
          buf = buffer;
          for (totWritten = 0; totWritten < n; ) {
              numWritten = write(fd, buf, n - totWritten);
              if (numWritten <= 0) {
                  if (numWritten == -1 && errno == EINTR) continue;
                  else return -1;
              }
              totWritten += numWritten;
              buf += numWritten;
          }
          return totWritten;
      }

      When:(1) 任何「发送长度明确的二进制 blob」、「按固定长度解析」协议——使用 writen/readn;(2) 类似 MSG_WAITALL 能在单次系统调用阻塞直到收到 N 字节,但被信号打断它不自动重启。

      Examplewriten(cfd, &len, sizeof(len)); writen(cfd, payload, len); readn(cfd, &len, sizeof(len)); readn(cfd, payload, len); —— 长度前缀 + payload 是最常见的「定长协议」模式。

      61.2 shutdown 系统调用与 half-close

      Whatshutdown(sockfd, how) 关闭 socket 双向通道中一个方向或两个方向;不受 dup 或 fork 出来的其它 fd 副本的影响——它作用在 open file description 上。

      Why:很多协议需要「我已写完,但现在还想读你的回应」——典型如 HTTP 请求后等响应、rsh 远程命令、SSH 双向独立通信。close 必须等所有 fd 副本关闭才真正关连接,无法实现半关闭。

      How

      how 语义

      SHUT_RD

      关闭读半;之后 read 立即返回 0(EOF);TCP 上对端若继续写,本地仍能读到部分实现是有的(Linux 一致;但 BSD 不同),应用不建议跨平台用 SHUT_RD

      SHUT_WR

      关闭写半;对端读到 EOF;本地仍可读对端后续数据;触发 active close 序列(FIN)

      SHUT_RDWR

      关闭两个半(sequenced)

      #include <sys/socket.h>
      int shutdown(int sockfd, int how);

      shutdown 不关闭 fd 本身;fd 仍需 close()

      When:(1) client 发送完请求后等响应 —— shutdown(cfd, SHUT_WR) 让 server 看到 EOF 关连接,client 继续读响应;(2) daemon 重启 / 子进程退出;想发 EOF 但保留 fd 复用 —— shutdown(sfd, SHUT_WR)

      Example:TLPI Listing 61-2 is_echo_cl 中的 echo client:

      // 摘自《The Linux Programming Interface》 第 61 章 Listing 61-2 (片段)
      switch (fork()) {
      case -1: errExit("fork");
      case 0:  /* Child: read echo server's response */
          for (;;) {
              numRead = read(sfd, buf, BUF_SIZE);
              if (numRead <= 0) break;
              printf("%.*s", (int) numRead, buf);
          }
          exit(EXIT_SUCCESS);
      default: /* Parent: copy stdin to socket */
          for (;;) {
              numRead = read(STDIN_FILENO, buf, BUF_SIZE);
              if (numRead <= 0) break;
              if (write(sfd, buf, numRead) != numRead)
                  fatal("write() failed");
          }
          /* Close writing channel, so server sees EOF */
          if (shutdown(sfd, SHUT_WR) == -1) errExit("shutdown");
          exit(EXIT_SUCCESS);
      }

      61.3 socket 专用 I/O:recv/send 与 flags

      Whatrecv(sockfd, buf, length, flags)send(sockfd, buf, length, flags) 在 read/write 基础上加 flags 标志位实现 socket 特殊行为。

      Why:很多 socket 行为不能仅靠 read/write 表达(如不要 SIGPIPE、non-stream peek、cork data 等)。

      How

      Flag 方向 语义

      MSG_DONTWAIT

      recv/send

      临时非阻塞

      MSG_PEEK

      recv

      从 socket buffer peek 但不移走

      MSG_WAITALL

      recv

      阻塞直到 length 字节就绪(被信号/EOF/OOB/error 打断)

      MSG_OOB

      recv/send

      带外数据(SUSv3)

      MSG_NOSIGNAL

      send

      对端关时不发 SIGPIPE(仅 Linux)

      MSG_MORE

      send

      cork 数据同 TCP segment 等;UDP 时聚合成一个 datagram

      When:(1) 想在一段代码中混合阻塞与非阻塞——MSG_DONTWAIT;(2) HTTP server 想确定请求是否完整又不想消耗数据——MSG_PEEK;(3) 写 handler 想防 SIGPIPE——signal(SIGPIPE, SIG_IGN)MSG_NOSIGNAL;(4) 想优化 small HTTP 响应——MSG_MORE cork headers 后接 body。

      Examplerecv(sfd, buf, n, MSG_PEEK) 看但不取,再调 recv 真正取(用于协议分帧);MSG_WAITALL 等价 readn,但系统调用次数少且行为更严格。

      61.4 sendfileTCP_CORK 零拷贝传输

      Whatsendfile(out_fd, in_fd, offset, count) 把 regular file 直接由内核送 socket,不经用户空间。TCP_CORK socket 选项在 TCP 层 corks 后续 write 直到 unsets,形成「HTTP header + body 一个 segment」。

      Why:(1) 普通 read+write 至少 2 次用户态 ↔ 内核态拷贝 + 2 次系统调用;(2) sendfile 一次系统调用、内核直接 page flip;HTTP server、文件下载、CDN 大幅提效。

      Howsendfile 用法:

      #include <sys/sendfile.h>
      ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
      /* out_fd: socket;in_fd: regular file(可 mmap);offset != NULL 时为 value-result。 */
      /* TCP_CORK 配 HTTP: 头 + body 一个 segment */
      int optval = 1;
      setsockopt(sockfd, IPPROTO_TCP, TCP_CORK, &optval, sizeof(optval));
      write(sockfd, headers, header_len);
      sendfile(sockfd, body_fd, NULL, body_size);
      optval = 0;
      setsockopt(sockfd, IPPROTO_TCP, TCP_CORK, &optval, sizeof(optval));

      Linux 2.4:out_fd 可以是 regular file(互拷两份文件);Linux 2.6 起不再支持;2.6.17 后增加 splice/tee/vmsplice 提供更通用 zero-copy;FreeBSD 上有 TCP_NOPUSH 等价物。

      When:(1) 文件下载 / 静态资源服务器;(2) HTTP server 想要 header + body 一个 segment;(3) 不需要预读 / 处理 文件内容场景。

      Example:图 61-1 左侧 read+write 浪费用户态 buffer;右侧 sendfile 直接内核 page cache → socket send buffer。

      61.5 getsockname / getpeername 与连接后的地址

      Whatgetsockname(sockfd, addr, *addrlen) 返回 socket 绑定的本地地址;getpeername 返回对端地址(限连接的 socket)。

      Why:(1) 被 inetd 启动的程序无法用 accept 取对端地址,只能用 getpeername;(2) 想取得内核隐式 bind 给的 ephemeral port;(3) 调试 / 日志打印对端信息。

      Howaddrlen 是 value-result,传入 buf 大小、返回实际写入。

      When:(1) 程序 accept 后想记录 client 信息或实现黑名单;(2) client 想确认自己连的是哪个 ephemeral port 或哪个 local IP;(3) port=0 隐式 bind 后想确认分到的实际 port。

      Example:TLPI Listing 61-3 socknames.c 同时跑 listener+client+accept,从 netstat 也能看到两端端口号一致。

      61.6 TCP 深度:状态机、TIME_WAIT

      What:TCP 连接在内核里有离散状态——LISTEN / SYN_SENT / SYN_RECV / ESTABLISHED / FIN_WAIT1/2 / CLOSING / TIME_WAIT / CLOSE_WAIT / LAST_ACK / CLOSED。

      Why:调试 TCP 应用必须能解释为什么「服务器重启报 EADDRINUSE」、「主动关闭方多耗时 2 MSL 才释放」、「为什么对端先 FIN 一段时间后才进入 CLOSED」——这些都来自状态机。

      How

      阶段 主动关闭方(client 视角)

      被动关闭方(server 视角)

      数据传输

      ESTABLISHED

      ESTABLISHED

      本端 close / 发 FIN

      FIN_WAIT1 → (收 ACK) → FIN_WAIT2 → (收 FIN) → TIME_WAIT → (2MSL) → CLOSED

      CLOSE_WAIT → close → LAST_ACK → (收 ACK) → CLOSED

      异常路径

      FIN_WAIT1 同时收 FIN → CLOSING → (收 ACK) → TIME_WAIT

      (无)

      TIME_WAIT 两大作用: . 确保可靠终止:主动方发的最后 ACK 可能丢失;保留 socket 让它在 2MSL 期间重发最后 ACK,对端不会收到 RST 而把连接当错。 . 清掉旧 segment:避免同一 4-tuple(local-IP, local-port, foreign-IP, foreign-port)的新连接接收旧 connection 的延迟 segment,导致数据错乱。

      Linux MSL = 30s(BSD 同理),TIME_WAIT = 60s;RFC 1122 推荐 MSL=120s,TIME_WAIT 可达 240s。

      When:(1) 重启服务器报 EADDRINUSE —— 不是真的 TIME_WAIT 防错,而是默认内核禁止在 active state 存在时 bind;(2) 想要短重启间隔不撞端口 —— 用 SO_REUSEADDR;(3) 不要尝试 SO_LINGER l_linger=0 绕过 TIME_WAIT——会让 socket 不发 FIN 直接 RST,绕开可靠性。

      Examplenetstat -atn | grep TIME_WAIT | wc -l 看 server 关闭后的 TIME_WAIT 数;client 主动 close 后会用 ephemeral port 出现一次 TIME_WAIT。

      61.7–61.8 netstat / tcpdump 调试

      Whatnetstat 显示本机 socket 状态;tcpdump 抓包显示 TCP segment 序列号、SYN/FIN/RST 标志、ack、window 等。

      Why:调试 TCP 应用必备工具——「为什么对端没收到数据」、「为什么 server close 后立刻 bind 失败」、「三次握手在哪一步中断」——都靠它们。

      Hownetstat -a --inet 列出全部 Internet socket,State 列指明 LISTEN / ESTABLISHED / TIME_WAIT 等;-p 看 pid;/proc/net/{tcp,udp,tcp6,udp6,unix} 也可读取。

      tcpdump -tn 'port 55555' 抓某端口的包;每行 src > dst: flags data-seqno ack window urg <options>

      When:(1) 怀疑端口被占 → netstat -tlnp;(2) 怀疑三次握手失败 → tcpdump;(3) 想看 server TIME_WAIT 数 → netstat -t | grep TIME_WAIT | wc -l

      Example:TLPI 给的 TCP 连接建立 3 个 segment —— SYNSYN/ACKACK,flags 列分别 SS. (SYN+ACK). (ACK)

      61.9–61.10 socket 选项与 SO_REUSEADDR

      Whatgetsockopt/setsockopt(sockfd, level, optname, &val, optlen) 读取 / 修改 socket 选项。level=SOL_SOCKET 是套接字级;IPPROTO_TCP 等对应协议级;SO_REUSEADDR 是套接字级选项。

      Why:(1) SO_REUSEADDR —— 解决 server 重启 EADDRINUSE;(2) SO_KEEPALIVE —— 启用 TCP keepalive;(3) SO_RCVBUF/SO_SNDBUF —— 调整 buffer 大小;(4) SO_LINGER —— 控制 close 行为(一般不建议);(5) SO_BROADCAST —— 允许 UDP 广播;(6) SO_OOBINLINE —— 把 OOB 数据混入正常流。

      How

      #include <sys/socket.h>
      int getsockopt(int sockfd, int level, int optname, void *optval,
                     socklen_t *optlen);          /* optlen 是 value-result */
      int setsockopt(int sockfd, int level, int optname, const void *optval,
                     socklen_t optlen);           /* setsockopt optlen 是 value */

      SO_TYPE 是只读;不能 set 类型。

      • 4-元组 {local-IP, local-port, foreign-IP, foreign-port} 必须唯一——但多数内核做了更严的「本地端口被任何 connection 占用时禁止 bind」,SO_REUSEADDR 把规则放松到 TCP 规范。

      When:(1) TCP server 启用 SO_REUSEADDR(accept 前)+ bind + listen;(2) 想调整 buffer 用 SO_RCVBUF/SO_SNDBUF;(3) 想探测已存在 socket 类型用 SO_TYPE

      Example:TLPI Listing 61-4 标准用法:

      int sockfd = socket(AF_INET, SOCK_STREAM, 0);
      int optval = 1;
      setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
      bind(sockfd, &addr, addrlen);
      listen(sockfd, backlog);

      61.11 accept() 后的继承与 socket 高级特性

      What:accept 返回的新 fd 在 Linux 上 不继承 listen fd 的 open-file status flags (O_NONBLOCK)、fd flags (FD_CLOEXEC)、F_SETOWN/F_SETSIG 信号驱动 I/O 关联;但 继承 大部分 socket 选项(SO_REUSEADDR 等)。SUSv3 对此没规定,跨实现行为有差异。

      Why:可移植代码需要在 accept 后显式重设这些属性;TLPI 给出 fcntl 调用模式。

      Howfcntl(cfd, F_SETFL, O_NONBLOCK); 在子进程里显式设非阻塞。

      When:(1) 写可移植代码必须显式重设 O_NONBLOCK;(2) UDP 通知 / SIGURG 行为要在 accept 后重新 fcntl(F_SETOWN, getpid())

      Example:accept 后立刻 fcntl(cfd, F_SETFD, FD_CLOEXEC) 防 EXEC leak。

      61.12 TCP vs UDP / 高级 topics

      What:UDP 在某些场景比 TCP 更优——单次 RTT 节省、广播 / 多播、流式媒体偶尔丢包可容忍;但可靠性 / 流控 / 拥塞控制需应用自实现;不少场景还是 TCP 更稳。

      Why:TCP best-case 时间 = 2×RTT + SPT(建连 → 请求 → 响应 → 拆连),UDP best-case = RTT + SPT;远距离 WAN 上 RTT 数百 ms,UDP 节省可观,但需要应用层重传。

      How

      场景 TCP 选择 UDP 选择

      一次性请求/响应

      DNS、SNMP 查询

      长会话 / 大块传输

      HTTP 文件传输、FTP、SSH

      实时视频/音频

      RTP、游戏状态、直播

      广播 / 多播

      DHCP、mDNS、TV 流

      简单 RPC 重试机制

      一般选 TCP 省事

      QUIC 类重传机制可让 UDP 容忍丢包

      高级话题: . Out-of-band dataMSG_OOB 发送接收;TCP 实际只支持 1 byte 紧急(urgent pointer 指向 1 byte);现代建议用双 socket 替代。 . sendmsg/recvmsg:最通用 I/O,支持 scatter-gather + ancillary data (cmsg);UNIX 域上可传 fd、sender credentials。 . Passing file descriptors:UNIX 域 + SCM_RIGHTS 跨进程传 fd(master server → worker pool)。 . Sequenced-packet socketsSOCK_SEQPACKET 结合 stream 可靠性 + dgram 边界;Linux 2.6.4 起 UNIX 域支持;Internet 上需 SCTP。 . SCTP / DCCP:SCTP 多 stream 通信;DCCP UDP-like + 拥塞控制。

      When:(1) 写跨进程文件描述符传递——UNIX 域 sendmsg + SCM_RIGHTS;(2) 想 1 字节紧急信号——OOB;(3) 多流可靠传输——SCTP。

      Example:TLPI 练习 61-5 建议 server 用 socket + dup2(STDOUT/STDERR) + execl 让 shell 命令输出回流 client —— 演示 child handle single connection 模式。

      三、关键图表

      关键系统调用与选项
      系统调用 头文件 用途

      readn/writen

      "rdwrn.h"

      完整 N 字节读写循环

      shutdown

      <sys/socket.h>

      半关闭(SHUT_RD/WR/RDWR)

      recv/send

      <sys/socket.h>

      socket I/O + flags

      sendfile

      <sys/sendfile.h>

      zero-copy file → socket

      getsockname/getpeername

      <sys/socket.h>

      取连接地址

      getsockopt/setsockopt

      <sys/socket.h>

      读写 socket 选项

      recvmsg/sendmsg

      <sys/socket.h>

      scatter-gather + ancillary

      关键选项

      头文件

      用途

      SO_TYPE

      SOL_SOCKET

      只读,socket 类型

      SO_REUSEADDR

      SOL_SOCKET

      bind 时跳过连接占用

      SO_KEEPALIVE

      SOL_SOCKET

      TCP keepalive

      SO_RCVBUF/SO_SNDBUF

      SOL_SOCKET

      收发缓冲区大小

      SO_BROADCAST

      SOL_SOCKET

      允许 UDP 广播

      SO_LINGER

      SOL_SOCKET

      close 行为

      SO_OOBINLINE

      SOL_SOCKET

      OOB 放回正常流

      TCP_CORK

      IPPROTO_TCP

      cork 数据到同 segment

      TCP_NODELAY

      IPPROTO_TCP

      禁用 Nagle

      TCP_KEEPALIVE

      IPPROTO_TCP

      TCP 协议级参数

      TCP 状态
      状态 触发 描述

      CLOSED

      初始 / 最终

      无连接

      LISTEN

      listen

      server 接受 SYN

      SYN_SENT

      connect

      client 发 SYN 后等待

      SYN_RECV

      accept 前

      server 收到 SYN 已回 SYN/ACK

      ESTABLISHED

      三次握手后

      数据传输

      FIN_WAIT1

      close 发 FIN 后

      等 ACK

      FIN_WAIT2

      收到 FIN_WAIT1 ACK

      等对端 FIN

      CLOSING

      FIN_WAIT1 中收对端 FIN

      双方同时 close 罕见

      TIME_WAIT

      主动关最后 ACK 后

      2MSL 等待

      CLOSE_WAIT

      收对端 FIN

      本端 close 后 → LAST_ACK

      LAST_ACK

      本端 close 后发 FIN

      等最后 ACK

      四、思维导图

      mindmap
        root((第 61 章 高级主题))
          部分读写
            readn writen
            EINTR 重启
            MSG_WAITALL
          shutdown 半关闭
            SHUT_RD
            SHUT_WR
            SHUT_RDWR
            跨 fd 引用关闭
          socket I/O flags
            MSG_PEEK
            MSG_DONTWAIT
            MSG_NOSIGNAL
            MSG_MORE
          sendfile zero copy
            文件 socket
            TCP_CORK
            splice tee
          TCP 状态
            三次握手
            四次挥手
            TIME_WAIT 2MSL
            netstat tcpdump
          socket 选项
            SO_REUSEADDR
            SO_TYPE 只读
            accept 不继承 flags
            inherit options
          高级特性
            OOB data
            sendmsg recvmsg
            传 fd credentials
            SOCK_SEQPACKET
            SCTP DCCP

      五、重点与易错点

      1. read()/write() 可能 partial——必须循环——除非明确只想要「任何字节」,否则都得用 readn/writen 或 MSG_WAITALL。

      2. close() ≠ shutdown()——close 关闭整个 socket,受 fd 引用计数;shutdown 关闭通道,作用在 open file description。要半关闭必须 shutdown(SHUT_WR)。

      3. SHUT_RD 在 TCP 上跨实现行为不一致——Linux / BSD 表现不同;除非必要不要在跨平台代码用 SHUT_RD。

      4. MSG_PEEK 配合两次 recv 是常见协议分帧模式——看但不移走数据;不要忘记 MSG_PEEK 后第二次 recv 必须不传 MSG_PEEK 才真实消费。

      5. MSG_NOSIGNAL 比 signal(SIGPIPE, SIG_IGN) 粒度更细——前者 per-call 控制;后者全局;某些库内部用前者避免影响调用方。

      6. MSG_MORE 是 TCP_CORK 的 per-call 版本——都 cork 数据合 segment;要确保 finally 关 cork 否则数据不发出。

      7. sendfile 只支持 file → socket——不能 socket ↔ socket、不能 socket → file(2.6 起);splice/tee 是更通用的 zero-copy 工具。

      8. TCP TIME_WAIT 是设计而非缺陷——2MSL 让最后 ACK 可重发 + 让旧 segment 过期;不要试图关掉,SO_REUSEADDR 已可解决常见 bind 冲突。

      9. EADDRINUSE 不一定来自 TIME_WAIT——也可能是其他活跃 TCP 占本地端口;用 netstat -tlnp 查实际占用者。

      10. SO_REUSEADDR 必须在 bind 前设置——在 listen 前调用 setsockopt;否则无效。

      11. SO_LINGER 是陷阱多于价值——l_linger=0 时 close 直接 RST 不发 FIN,丢弃未发数据、破坏可靠性;生产代码几乎不用。

      12. accept 后新 fd 不继承 O_NONBLOCK / FD_CLOEXEC——跨进程 / fork-per-client 设计必须在子进程显式重设。

      13. OOB (MSG_OOB) 在 TCP 上只 1 byte——TCP 用 urgent pointer,紧急数据实际是「后面第 1 个 byte」语义;现代建议用双 socket。

      14. splice + tee + vmsplice 是 sendfile 的现代替代——更灵活(fd ↔ pipe、pipe ↔ socket);研究 Linux 2.6.17+ 的这组系统调用。

      15. netstat / tcpdump 必备工具——ESTABLISHED、TIME_WAIT、LISTEN 三种状态是日常调试核心;tcpdump 看 SYN/FIN 标志验证三次握手、四次挥手。

      16. 跨章衔接:第 59 章 IPv4/IPv6 地址结构与 getaddrinfo;第 60 章 fork-per-client 主动关闭后产生 TIME_WAIT;第 63 章备选 I/O 模型(select/poll/epoll)是对高负载服务器 model 的延伸;第 44 章管道与本节「partial read/write」互为对照(管道也是字节流)。