Post

UNIX 网络编程笔记

OSI模型

理解两个词 ISO & OSI

  1. ISO International Organizational for Standardization 国际标准化组织
  2. OSI Open System Interconnection 计算机通信开放系统互连

图解:OSI模型和网际协议族中的各层

描述一个网络中各个协议层的常用方法是使用国际标准化组织(Intermational Orgamization for Standardization,ISO)的计算机通信开放系统互连(open systems interconnection, OSI)模型。这是一个七层模型,如下图所示。图中同时给出了它与网际协议族的近似映射,

OSI模型就是描述一个网络中各个协议层的,中文是计算机通信开放系统互连(OSI)模型,是一个七层模型

OSI模型和网际协议族中的各层 OSI模型和网际协议族中的各层

网络层由 IPv4 和 IPv6 这两个协议处理。传输层有 TCP 和 UDP 协议。注意 TCP 和 UDP 中间用间隙,表明应用层的网络应用可以绕过传输层直接使用 IPv4 和 IPv6,即使用原始套接字(row socket)

应用层是由 OSI 模型的顶上三层被合并成一层,称之为应用层。Web 客户(浏览器)、Telnet 客户、Web 服务器、FTP 服务器和其他我们在使用的网络应用所在的层。

所谓的套接字编程接口:从顶上三层(网际协议的应用层)进入传输层的接口。本书聚焦于怎么使用套接字编写使用TCP或UDP的网络应用程序。

Q: 上图中,XTI 是啥?

Ans: XTI(X/Open Transport Interface,X/Open 传输接口)是一个面向连接的网络编程接口规范,由 X/Open 组织定义。它提供了一种独立于网络协议的编程接口,使应用程序能够在不同的操作系统和网络环境中进行网络通信。Socket 是一种通用的网络编程接口,而 XTI 是一种在 UNIX 系统中特定的标准接口,提供了更高层次的抽象和功能。XTI 在一些旧的 UNIX 系统中使用较多,而 Socket 则是更通用且在各种操作系统中都可用的网络编程接口。fron Wiki X/Open Transport Interface https://en.wikipedia.org/wiki/X/Open_Transport_Interface

Q: 为什么套接字提供的是从 OSI 模型的顶上三层进入传输层的接口?

Ans: 这样设计有两个理由,如上图右侧所注

  1. 理由之一是顶上三层处理具体网络应用(如 FTP、Telnet 或 HTTP)的所有细节,却对通信细节了解很少;底下四层对具体网络应用了解不多,却处理所有的通信细节:发送数据,等待确认,给无序到达的数据排序,计算并验证校验和,等等。
  2. 理由之二是顶上三层通常构成所谓的用户进程(user process),底下四层却通常作为操作系统内核的一部分提供。Unix 与其他现代操作系统都提供分隔用户进程与内核的机制。由此可见,第 4 层和第 5 层之间的接口是构建 API 的自然位置。

Q:为什么套接字提供的是从OSI模型的顶上三层进入传输层的接口?

Ans:

  1. 顶上三层处理具体网络应用(FTP、Telnet、HTTP)的所有细节,对通信细节了解很少;相反,对应底下四层对具体网络应用了解不多,但可以处理所有通信细节。
  2. 通信细节:发送数据,等待确认,给无需到达的数据排序,计算并验证校验和等等
  3. 顶上三层通常构成所谓的用户进程(user process),底下四层却通常作为操作系统内核的一部分提供。
    1. Unix与其他Unix-like系统都提供了分隔用户进程与内核的机制,就是所谓用户态和内核态。
    2. 用户态和内核态有安全性和隔离性,同时提供了高度可编程的接口。通过系统调用,应用程序可以请求内核执行特权操作,例如创建新的进程、读写文件、分配内存等。内核在收到系统调用请求后,会切换到内核态,并根据请求执行相应的操作。完成后,内核将结果返回给应用程序,并将控制权重新交还给应用程序,使其继续在用户态执行。【扩展】

传输控制协议 TCP

传输层概述

传输层包括 TCP、UDP 和 SCTP 这些传输层协议都转而使用网络层协议 IP IPv4 或者 IPv6

  • UDP:简单的,不可靠的用户数据报协议
  • TCP:复杂的、可靠的,基于字节流的传输控制协议

Q: 需要理解什么?

Ans:1 这些传输层协议提供给应用进程的服务是啥?弄清这些协议处理什么?应用进程中又需要处理什么?重点理解TCP

  • 调试工具:调试客户和服务器程序用netstat

总图:传输层协议一览表

协议族被称为”TCP/IP”,但是除了TCP、IP这俩主要协议外还要其他的,如图,协议族(Protocol family)是指一组相关的网络协议的集合

TCP/IP 协议概况 TCP/IP 协议概况

从最左边说起tcpdump

  1. 命令 tcpdump
    1. 使用 BSD 分组过滤器(BSD packet filter, BPF),或者使用数据链路提供者接口(datalink provider interface, DLPI)直接与数据链路进行通信
    2. 图示右边9个应用下面的虚线标记为API,它通常是套接字或 XTI(X/Open Transport Interface,X/Open传输接口),tcpdump 应用则不使用 socket 或 XTI
  2. 命令mrouted
  3. 命令ping
  4. 命令traceroute
    1. 使用两种套接字:1 IP套接字 2 ICMP套接字
    2. IP 套接字用于访问 IP,ICMP 套接字用于访问 ICMP

简单解释图中每个协议框

  1. IPv4 网际版本协议(Internet Protocol version 4),IPv4给TCP、UDP、SCTP、ICMP和IGMP提供分组递送服务,使用32位地址。

    分组递送服务(Packet Delivery Service)是一种网络服务,用于在计算机网络中将数据分组从源节点传送到目标节点。它是指网络层提供的基本传输服务,负责将数据包按照网络协议规定的路由路径进行传输和交付。

  2. IPv6网际版本协议(Internet Protocol version 6),IPv6给TCP、UDP、SCTP、ICMP和IGMPv6提供分组递送服务,使用128位地址。
  3. TCP传输控制协议(Transmission Control Protocol)
    1. 面向连接、为用户进程提供可靠的全双工字节流。
    2. TCP套接字是一种流套接字(stream socket)。
    3. TCP关心确认、超时和重传之类的细节
  4. UDP 用户数据包协议(User Datagram Protocol)
    1. 无连接协议
    2. UDP套接字是一种数据包套接字(datagram socket)
    3. UDP数据包不能保证最终到达它们的目的地
  5. SCTP 流控制传输协议(Stream Control Transmission Protocol)
  6. ICMP 网际控制消息协议(Internet Control Message Protocol)
  7. IGMP 网际组管理协议(Internet Group Management Protocol)
  8. ARP 地址解析协议(Address Resolution Protocol),作用是将IP地址解析成MAC地址,网络层的IP地址映射到数据链路层的MAC地址
  9. RARP 反向地址解析协议(Reverse Address Resolution Protocol)
  10. ICMPv6 网际控制消息协议版本6(Internet Control Message Protocol version 6)
  11. 6 综合了ICMPv4、IGMP和ARP的功能
  12. BPF BSD分组过滤器(BSD packet filter),这个接口提供了对数据链路层的访问能力。
  13. DLPI数据链路提供者接口(datalink provider interface),同样提供了对数据链路层的访问能力。

这些网际协议由一个或多个称为请求批注(Request for Comments, RFC)的文档定义,这些RFC就是它们的正式规范。

传输控制协议(TCP)特点介绍

TCP 提供客户与服务器之间的连接(connection)。TCP 客户先与某个给定服务器建立一个连接,再跨该连接与那个服务器交换数据,然后终止这个连接。

TCP 是可靠性(reliability):TCP 向一端发送数据时,要求对端返回确认。如果没有收到确认,TCP 就自动重传数据并等待更长时间。在数次重传失败后,TCP 才放弃,如此在尝试发送数据上所花的总时间一般为4~10分钟(依赖于具体实现)。

RTT:TCP 有动态估算客户和服务器之间的往返时间(round-trip time, RTT)的算法,以便于它知道等待一个确认需要多少时间。RTT 往返时间,时延。

TCP 会对所发送的数据进行排序(sequencing):TCP 通过给其中每个字节关联一个序列号,根据序列号判定数据重复;

  1. 如果这些数据非顺序到达,接收端 TCP 会根据序列号来排序,排序后再把结果传递给接收应用。
  2. 如果接收端 TCP 接收到来自对端的重复数据(比如说对端认为一个分节已丢失并因此重传,而这个分节并没有真正丢失,只是网络通信过于拥挤),它可以(根据序列号)判定数据是重复的,从而丢弃重复数据。

TCP 提供流量控制(flow control):TCP总是告知对端在任何时刻它一次能够从对端接收多少字节的数据,这称为通告窗口(advertised window)。在任何时刻,该窗口指出接收缓冲区中当前可用的空间量,从而确保发送端发送的数据不会让接收缓冲区溢出。

  1. 该窗口时刻动态变化:当接收到来自发送端的数据时,窗口大小就减小,但是当接收端应用从缓冲区中读取数据时,窗口大小就增大。
  2. 通告窗口大小减小到0是有可能的:当TCP对应某个套接字的接收缓冲区已满,导致它必须等待应用从该缓冲区读取数据时,方能从对端再接收数据。

TCP 连接是全双工的(full-duplex):这意味着在一个给定的连接上应用可以在任何时刻在进出两个方向上既发送数据又接收数据。因此,TCP 必须为每个数据流方向跟踪诸如序列号和通告窗口大小等状态信息。

TCP 连接的建立和终止

完整的 TCP 连接:1 连接TCP建立 2 数据传送 3 释放TCP连接

  1. 物理层:二进制比特流传输;bit(比特流);
  2. 数据链路层::介质访问控制;frame(帧);
  3. 网络层:确定地址和路由选择;packet(包),又叫做分组
  4. 传输层:端到端连接;也叫作数据包。但是谈论TCP等具体协议时有特殊叫法,TCP的数据单元叫数据段,segment(段),而UDP协议的数据单元称为数据报(datagram)
  5. 会话层、表示层、应用层:一般就称呼为消息(message)

TCP 连接建立:三次握手

建立一个TCP连接时会发生:

  1. Server 必须 ready,服务器已经准备好接受外来的连接。
    1. 通常调用socket()bind()listen()三个函数来完成
    2. 称之为被动打开(passive open)
  2. Client 通过调用connect()发起主动打开(active open)
    1. 客户 TCP 发送一个SYN(同步)分节,它告诉服务器将在(待建立的)连接中发送的数据的初始序列号。
    2. 通常 SYN 分节不携带数据
  3. Server 必须确认客户的 SYN,同时自己也得发送一个 SYN 分节
    1. 这里发送的 SYN 分节有发送数据的初始序列号,这个数据就是服务器在这个连接中准备发送的数据。
    2. 服务器在单个分节中发送 SYN 和对客户 SYN 的 ACK(确认)
  4. Client 必须确认服务器的 SYN

这些至少需要3个分组,所以叫TCP的三路握手(three-way handshake),如下图所示

TCP的三路握手 TCP 的三路握手

根据图,对于 SYN 序列号和 ACK 中确认号的说明:

  1. 客户的初始序列号为 J,服务器的初始序列号为 K。
  2. ACK 中的确认号:指的是发送这个 ACK 的一端所期待的下一个序列号
    1. SYN 占据 1 字节
    2. 所以每个 SYN 的 ACK 中的确认号是 SYN 的初始序列号加 1
  3. 同样的,每一个 FIN(表示结束)的 ACK 中的确认号就是 FIN 的序列号加 1

TCP 使用“三报文握手”建立连接

要在 TCP 客户和服务器之间交换三个 TCP 报文段。最初两端的 TCP 进程都处于关闭状态(CLOSED)。

一开始 TCP 服务器进程首先创建传输控制块,用来存储 TCP 连接中的一些重要信息,例如 TCP 连接表指向发送和接收缓存的指针、指向重传队列的指针当前发送和接收序号等,之后就准备接受 TCP 客户进程的连接请求,此时 TCP 服务器进程就要进入监听状态(LISTEN),等待 TCP 客户进程的连接请求。

TCP 服务器进程是被动等待来自 TCP 客户进程的连接请求,而不是主动发起,因此称为被动打开连接。 TCP 客户进程也是首先创建传输控制块,然后在打算建立 TCP 连接时向 TCP 服务器进程1️⃣发送 TCP 连接请求报文段,并进入同步已发送状态(SYN_SENT)。TCP 连接请求报文段首部中的同步位 SYN 被设置为1,表明这是一个 TCP 连接请求报文段序号字段 SEQ 被设置了一个初始值 x 作为 TCP 客户进程所选择的初始序号。请注意 TCP 规定 SYN 被设置为一的报文段不能携带数据,但要消耗掉一个序号。由于 TCP 连接建立是由 TCP 客户进程主动发起的,因此称为主动打开连接。

TCP 服务器进程收到 TCP 连接请求报文段后,如果同意建立连接,则2️⃣向 TCP 客户进程发送 TCP 连接请求确认报文段并进入同步已接收状态(SYN_RCVD)。该报文段首部中的同步位 SYN 和确认位 ACK 都设置为 1,表明这是一个 TCP 连接请求确认报文段序号字段 seq 被设置了一个初始之外,作为 TCP 服务器进程所选择的初始序号。确认号字段 ack 的值被设置成了 x+1,这是对 TCP 客户进程所选择的初始序号的确认。请注意,这个报文段也不能携带数据,因为它是 SYN 被设置为1的报文段,但同样要消耗掉一个序号。

TCP 客户进程收到 TCP 连接请求确认报文段后,还要3️⃣向 TCP 服务器进程发送一个普通的 TCP 确认报文段,并进入连接已建立状态(ESTABLISHED)。该报文段首部中的确认位 ACK 被设置为 1,表明这是一个普通的 TCP 确认报文段。序号字段 seq 被设置为 x+1,这是因为 TCP 客户进程发送的第一个 TCP 报文段的序号为 x,并且不携带数据,因此第二个报文段的序号为 x+1。请注意 TCP 规定普通的 TCP 确认报文段可以携带数据,但如果不携带数据,则不消耗序号在这种情况下所发送的下一个数据报文段的序号仍是 x+1。确认号字段 ack 被设置为 y+1,这是对 TCP 服务器进程所选择的初始序号的确认。TCP 服务器进程收到该确认报文段后也进入连接已建立状态(ESTABLISHED)。

现在 TCP 双方都进入了连接已建立状态,他们可以基于已建立好的 TCP 连接进行可靠的数据传输了。

TCP 选项

每一个 SYN 可以含有多个 TCP 选项。下面是常用的 TCP 选项

TCP segment options TCP segment options

  • MSS 选项:发送 SYN 的 TCP 一端使用 MSS 告诉对端的最大分节大小(maximum segment size, MSS),就是指明本连接中每个 TCP 分节传输可接受的最大数据量。发送端 TCP 使用接收端的 MSS 值作为所发送分节的最大大小。
  • 窗口规模选项:TCP 连接任何一端能够通告对端的最大窗口大小是 65535。
  • 时间戳选项:这个选项对于高速网络连接是必要的,它可以防止由失而复现的分组可能造成的数据损坏。

带宽,类似道路的车道数,你应该见过节假日里高速公路上停满私家车的图片。可见,车道越多,高速路越长,能容纳的车就越多。在网络世界里,带宽很大、RTT 很长的网络,被冠以一个特定的名词,叫做长肥网络,英文是 Long Fat Network。在长肥网络中的 TCP 连接,叫做长肥管道,既然高带宽或长延迟的网络被称为“长胖管道”(long fat pipe),这两个选项也称为“长胖管道选项”

TCP 连接终止:四次挥手

TCP建立一个连接需要3个分节,终止一个连接则需要4个分节

  1. 某个应用进程首先调用close,该端的 TCP 于是乎发送一个 FIN 分节
    1. 进程调用了close,我们称之为该端执行主动关闭(active close)
    2. FIN 分节(finish),表示数据发送完毕,FIN 标志告知对端,我已经没有更多的数据要发送了,并且请求关闭连接。
  2. 接收到这个FIN的对端执行被动关闭(passive close)
    1. 这个 FIN 由 TCP 确认????
    2. 这个接收了也作为一个文件结束符(end-of-file)传递给接收端应用进程
  3. 一端时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字
    1. 它的 TCP 同样要发送一个 FIN
  4. 接收这个最终 FIN 的原发送端 TCP 确认这个 FIN
    1. 原发送端 TCP,就是执行主动关闭的那一端

既然每个方向都需要 1 个 FIN 和 1 个 ACK,因此通常需要 4 个分节。
我们使用限定词“通常”是因为:某些情形下步骤 1 的 FIN 随数据一起发送:另外,步骤 2 和步骤 3 发送的分节都出自执行被动关闭那一端,有可能被合并成一个分节。

TCP 连接关闭时的分组交换 TCP 连接关闭时的分组交换

在步骤 2 与步骤 3 之间,从执行被动关闭一端到执行主动关闭一端流动数据是可能的。这称为半关闭(half-close)。

图片中是客户执行主动关闭的,其实无论客户还是服务器都可以执行主动关闭。

TCP 状态转换图

TCP 为一个连接定义了 11 中状态。也叫 TCP 有限状态机【研究生课程的学习的内容??】

有限状态机(英语:finite-state machine,缩写:FSM)又称有限状态自动机,简称状态机, 是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。 有限状态机是一种用来进行对象行为建模的工具,其作用主要是描述对象在它的生命周期内所经历的状态序列, 以及如何响应来自外界的各种事件。 在计算机科学中,有限状态机被广泛用于建模应用行为、硬件电路系统设计、软件工程, 编译器、网络协议、和计算与语言的研究。比如非常有名的 TCP 协议状态机。wiki https://en.wikipedia.org/wiki/Finite-state_machine

TCP 状态转换 TCP/IP State Transition Diagram

基于 TCP 状态转换图的说明:

  1. ESTABLISHED状态引出的两个箭头处理连接的终止。两种情况
    1. active close 应用进程接收到1个FIN之前调用close(主动关闭),那就转换到FIN_WAIT_1状态
    2. passive close 应用进程在ESTABLISHED状态期间收到1个FIN(被动关闭),那就转换到CLOSE_WAIT状态
  2. 图中粗实线表示通常的客户状态转换,用粗虚线表示通常的服务器状态转换。
    1. 同时打开(simultaneous open),发生在两端几乎同时发送SYN并且这两个SYN在网络中交错的情况下
    2. 同时关闭(simultaneous close),发生在两端几乎同时发送FIN的情况
    3. 同时打开,同时关闭有可能发生,但是及其罕见
  3. 状态说明
    1. CLOSED:无连接是活动的或正在进行
    2. LISTEN:服务器在等待进入呼叫
    3. SYN_RECV:一个连接请求已经到达,等待确认
    4. SYN_SENT:应用已经开始,打开一个连接
    5. ESTABLISHED:正常数据传输状态
    6. FIN_WAIT1:应用说它已经完成
    7. FIN_WAIT2:另一边已同意释放
    8. TIMED_WAIT:等待所有分组死掉
    9. CLOSING:两边同时尝试关闭
    10. TIME_WAIT:另一边已初始化一个释放
    11. LAST_ACK:等待所有分组死掉

TCP 连接实际分组交换情况

TCP连接的分组交换 Packet exchange for TCP connection

一个完整的 TCP 连接所发生的实际分组交换情况,包括连接建立、数据传送和连接终止 3 个阶段。图中还展示了每个端点所历经的 TCP 状态。

TIME_WAIT 状态

TIME_WAIT状态是TCP中网络编程最不容易理解的部分】

等待

执行主动关闭的一端经历了这个状态。该端点停留在这个状态的持续时间是 MSL 的两倍,有时候也称之为 2MSL。

MSL 是任何 IP 数据报能够在因特网中存活的最长时间。这个时间是有限的,因为每个数据报含有一个称为跳限的 8 位字段(最大值255)。跳数有限制。

解释两个概念 MSL&TTL

  • MSL 最长分节生命周期(maximum segment lifetime, MSL)
  • TTL Time to Live 跳限 hop limit 生存时间(TTL) _是_指数据包被设置为在被路由器丢弃之前存在于网络中的时间或“_跳_数”

TIME_WAIT状态存在的理由

  1. 可靠地实现TCP全双工连接的终止:防止连接关闭时四次挥手中的最后一次ACK丢失

TCP需要保证每一包数据都可靠的到达对端,包括正常连接状态下的业务数据报文,以及用于连接管理的握手、挥手报文,这其中在四次挥手中的最后一次ACK报文比较特殊,TIME_WAIT状态就是为了应对最后一条ACK丢失的情况。 TCP保证可靠传输的前提是收发两端分别维护关于这条连接的状态信息(TCB控制块),当发生丢包时进行ARQ重传。如果连接释放了,就无法进行重传,也就无法保证发生丢包时的可靠传输。 对于最后一条ACK,如果没有TIME_WAIT状态,主动关闭一方(客户端)就会在收到对端(服务器)的FIN并回复ACK后 直接从FIN_WAIT_2 进入 CLOSED状态,并释放连接,销毁TCB实例。此时如果最后一条ACK丢失,那么服务器重传的FIN将无人处理,最后导致服务器长时间的处于 LAST_ACK状态而无法正常关闭(服务器只能等到达到FIN的最大重传次数后关闭)。 至于将TIME_WAIT的时长设置为 2_MSL,是因为报文在链路中的最大生存时间为MSL(Maximum Segment Lifetime),超过这个时长后报文就会被丢弃。TIME_WAIT的时长则是:最后一次ACK传输到服务器的时间 + 服务器重传FIN 的时间,即为 2_MSL。

  1. 允许老的重复分节在网络中消逝:防止新连接收到旧链接的TCP报文
TCP使用四元组区分一个连接(源端口、目的端口、源IP、目的IP),如果新、旧连接的IP与端口号完全一致,则内核协议栈无法区分这两条连接。 2*MSL 的时间足以保证两个方向上的数据都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定是新连接上产生的。

端口号与并发服务器

端口号 port number

客户与服务器通信用端口号(port number)来标识进程。端口号16位整数。

1
2
3
4
+-------------------------+-----------------------+------------------------+
|   Well-Known Ports      |   Registered Ports    |   Dynamic/Private      |
|       (0 - 1023)        |    (1024 - 49151)     |     (49152 - 65535)    |
+-------------------------+-----------------------+------------------------+
  1. 众所周知的端口(well-known port)为 0~1023。
    1. 比如 80 给 Web 服务器用,HTTP 用 80,HTTPS 用 443
    2. Unix 系统的保留端口(reserved port),小于 1024 的都是,这些端口只能赋予特权用户进程的套接字。
  2. 已登记的端口(registered port)为 1024~49151
    1. 这些端口由 IANA 登记过
  3. 动态的(dynamic)或者私用的(private)端口:49152~65535
    1. IANA 不管这些,这些都是临时端口

LANA(the Internet Assigned Numbers Authority,因特网已分配数值权威机构)

套接字对 socket pair

定义:一个TCP连接的套接字对是一个定义该连接的两个端点的四元组:本地IP地址、本地TCP端口号、外地IP地址、外地TCP端口号。

套接字对唯一标识一个网络上的每个TCP连接。标识每个端点的两个值(IP地址和端口号)通常称为一个套接字。

TCP端口号与并发服务器

并发服务器中主服务器循环会派生一个子进程来处理每个新的连接。记号{:21, *:}指出服务器的套接字对。

  • 服务器在任意本地接口(第一个星号)的端口21上等待连接请求。
  • 外地IP地址和外地端口都没有指定,用点号标识
  • 称这些为监听套接字(listening socket)
  • 上面的星号叫通配(wildcard)符

处理两个客户连接的过程如下

主机A

  • 主机A的IP地址:206.168.112.219
  • 客户1的连接套接字对:{206.168.112.219:1500, 12.106.32.254:21}
  • 客户2的连接套接字对:{206.168.112.219:1501, 12.106.32.254:21}

主机B

  • 这个主机是多宿主机,有两个IP地址,12.106.32.254 和 192.168.42.1
  • 监听套接字是{:21, *:}

过程说明:

  1. 主机A启动客户1,对主机B的IP地址12.106.32.254执行主动打开。
  2. 主机B接收并接受客户1的连接时,它fork一个自身的副本,让子进程来处理该客户的情况。
    1. 已连接套接字(connected socket)使用和监听套接字相同的本地端口(21)
    2. 主机A的客户1与主机B连接一旦建立,已连接套接字的本地地址(12.106.32.254)随即填入
    3. 主机B fork 出来的子进程1的已连接套接字变为{12.106.32.254:21, 206.168.112.219:1500}
  3. 然后假设主机A启动了另一个客户2,请求连接到同一个主机B上面
    1. 主机A的TCP则会为客户2的套接字分配一个未使用的临时端口,假设为1501
    2. 主机A的客户2的连接套接字对则会变为{206.168.112.219:1501, 12.106.32.254:21}
    3. 客户1与2的连接套接字对不同,端口号不一样
  4. 主机B接收并接受客户2的连接,并fork出一个自身的副本子进程2来处理这个TCP连接
    1. 主机B fork 出来的子进程2的已连接套接字变为{12.106.32.254:21, 206.168.112.219:1501}
    2. 所有目的端口为21的其他TCP分节都被递送给拥有监听套接字的最初的那个父进程来处理

缓冲区大小及限制

影响IP数据报大小的一些限制

影响IP数据包大小的限制,进而会影响应用进程能够传送的数据

  1. IPv4数据报的最大大小时65535字节,包括IPv4首部。
  2. IPv6数据报的最大大小时65575字节,包括40字节的IPv6首部。
    1. IPv6有一个特大净荷(jumbo payload)选项,它把净荷长度字段扩展到32位
    2. a. 的操作需要MTU(maximum transmission unit, 最大传输单元)超过65535的数据链路提供支持
  3. 许多网络有一个可由硬件规定的MTU。
    1. 以太网的MTU是1500字节
  4. 在两个主机之间的路径中最小的MTU称为路径MTU(path MTU)
    1. 1500字节的以太网MTU是当今常见的路径MTU。
    2. 两个主机之间相反的两个方向上路径MTU可用不一致
    3. 因为在因特网上路由选择往往是不对称的。A到B的路径和B到A的路径不一样
  5. 当一个IP数据报将从某个接口送出时,如果它的大小超过相应链路的MTU,IPv4和IPv6都将执行分片(fragmentation)
    1. 这些片段在到达最终目的地之前通常不会被重组(reassembling)
    2. IPv4主机对其产生的数据报执行分片,IPv4路由器则对其转发的数据报执行分片
    3. IPv6只有主机对其产生的数据报执行分片,IPv6路由器不会对其转发的数据报执行分片
  6. IPv4首部的“不分片(don’t fragment)”位(即DF位)若被设置,那么不管是发送数据报的主机还是转发数据报的路由器,都不允许对它们分片。
  7. IPv4和IPv6都定义了最小重组缓冲区大小(minimum reassembly buffer size),它是IPv4或IPv6的任何实现都必须保证支持的最小数据报大小。
  8. TCP 有一个 MSS(maximum segment size,最大分节大小),用于向对端 TCP 通告对端在每个分节中能发送的最大TCP数据量。

TCP 输出

某个应用进程写数据到一个TCP套接字中发生的步骤如下图

应用进程写 TCP 套接字时涉及的步骤和缓冲区 应用进程写 TCP 套接字时涉及的步骤和缓冲区

对步骤进行说明:

  1. 当某个应用进程调用write时,内核从该应用进程的缓冲区中复制所有数据到所写套接字的发送缓冲区。
    1. 每一个TCP套接字有一个发送缓冲区
    2. SO_SNDBUF套接字选项来更改缓冲区大小
  2. 内核将不从write系统调用返回,直到应用进程缓冲区中的所有数据都复制到套接字发送缓冲区。
    1. 如果套接字的发送缓冲区容不下该应用进程的所有数据,该应用进程将被投入睡眠。
      1. 可能是应用进程的缓冲区大于套接字的发送缓冲区
      2. 也可能是套接字的发送缓冲区已有其他数据
    2. 这里假设这个套接字是阻塞的(也可以设置非阻塞)
    3. 这里write调用成功返回只能说明能重新使用原来的应用进程缓冲区,不表示对端的TCP或应用进程已接收到数据。
  3. 这一端的TCP提取套接字发送缓冲区中的数据并把它发送给对端TCP
  4. 对端TCP必须确认收到的数据,伴随来自对端的ACK的不断送达,本端TCP至此才能从套接字发送缓冲区中丢弃已确认的数据。
    1. TCP必须为已发送的数据保留一个副本,直到它被对端确认为止。
  5. 本端TCP以MSS大小的或更小的块把数据传递给IP,同时给每个数据块安上一个TCP首部以构成TCP分节
    1. 其中MSS可能是对端通告的值,或是536
  6. IP给每个TCP分节安上一个IP首部以构成IP数据报,并按照其目的IP地址查找路由表项以确定外出接口,然后把数据报传递给相应的数据链路。
    1. 在IP数据报传递给数据链路之前,可能会对其分片
    2. MSS选项的作用一个就是会避免分片
    3. 较新的实现还用了路径MTU发现功能
  7. 每个数据链路都有一个输出队列,如果该队列已满,那么新到的分组将被丢弃,并沿协议栈上返回一个错误
    1. 从数据链路到IP,再从IP到TCP。
    2. TCP看到这个错误后,就在后面某时刻重传对应的分节。
    3. 这时候应用进程并不知道这个暂时的情况。

基本套接字编程

这里整理套接字 API,从套接字地址结构开始。

  • 这些结构可以在两个方向上传递:
    1. 从进程到内核
    2. 从内核到进程
  • 地址转换函数:在地址的文本表达和它们存放在套接字地址结构中的二进制值之间进行转换。
    • 地址转换函数有个问题:它们与所转换的地址类型协议相关,要考虑是 IPv4 还是 IPv6 地址。
    • 解决:有一组 sock_ 开头的函数,会以协议无关的方式使用套接字地址结构。
  • 编写一个完整的 TCP 客户/服务器程序所需的基本套接字函数socket()connect()bind()listen()accept()fork() & exec()close()
  • 图示在一对 TCP 客户和服务器进程之间发生的一些典型事件的时间表。

基本 TCP 客户端/服务器的套接字函数 基本 TCP 客户端/服务器的套接字函数

  1. 服务器首先启动,稍后某时刻客户启动,它试图连接到服务器。
  2. 假设客户给服务器发送一个请求,服务器处理该请求,并且给客户发回一个响应。
  3. 这个过程一致持续下去,直到客户关闭连接的客户端,从而给服务器发送一个 EOF(文件结束)通知为止。
  4. 服务器接着也关闭连接的服务器端,然后结束运行或者等待新的客户连接。

套接字地址结构

大多数套接字函数都需要一个指向套接字地址结构的指针来做参数。

IPv4 套接字地址结构

也叫“网际套接字地址结构”,命名为sockaddr_in

网际(IPv4)套接字地址结构:sockaddr_in POSIX 定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct in_addr {
  int_addr_t s_addr; /* 32-bit IPv4 address */
                     /* network byte ordered */
};

struct sockaddr_in {
  uint8_t sin_len;           /* length of structure (16) */
  sa_family_t sin_family;    /* AF_INET */
  in_port_t sin_port;        /* 16-bit TCP or UDP port number */
                             /* network byte ordered */
  struct in_addr sin_addr;   /* 32-bit IPv4 address */
                             /* network byte ordered */
  char sin_zero[8]; 				 /* unused */
}

几点说明:

  1. IPv4 地址和 TCP 或 UDP 端口号在套接字地址结构中总是以网络字节序来存储。
  2. 套接字地址结构仅在给定主机上使用
    1. 某些字段用在不同主机之间通信
    2. 结构本身并不在主机之间传递

通用套接字地址结构

一个参数传递进任何套接字函数时,套接字地址结构总是以引用形式(指向该结构的指针)来传递。

  • 问题:套接字函数需要处理不同的支持任何协议族的套接字地址结构。
  • 解决:将传入的参数指向一个通用的套接字地址结构

所有这个通用套接字地址结构的唯一用途就是对指向特定协议的套接字地址结构的指针执行类型强制转换。

IPv6 套接字地址结构

书上说了几点注意

  1. IPv6的地址族是AF_INET6,IPv4的地址族是AF_INET

新的通用套接字地址结构

新的同样套接字地址结构:struct sockaddr_storage足以容纳系统所支持的任何套接字地址结构。 对比sockaddr有2点区别

  1. 如果系统支持的任何套接字地址结构有对齐需要,那么sockaddr_storage能满足最苛刻的对齐要求
  2. sockaddr_storage足够大,能够容纳系统支持的任何套接字地址结构。

套接字地址结构的比较,一张图来解释

IPv4、IPv6、Unix域、数据链路和存储对比

image.png

socket() 函数

要执行网络 I/O,一个进程必须做的第一件事情就是调用socket()函数,指定期望的通信协议类型(使用 IPv4 的 TCP、使用 IPv6 的 UDP、Unix 域字节流协议等)

1
2
# include <sys/socket.h>
int socket(int family, int type, int protocol);
  • 返回值:若成功则为非负描述符, 出错则为-1
  • 参数 family 指明协议族,这个参数也叫协议域。

协议族 family 常数值:AF_INET(IPv4 协议)、AF_INET6(IPv6 协议)、AF_LOCA(Unix 域套接字)L、AF_ROUTE(路由套接字)、AF_KEY(密钥套接字)

  • 参数 type 指明套接字类型

套接字类型 type 常数值:SOCK_STREAM(字节流套接字)、SOCK_DGRAM(数据报套接字)、SOCK_SEQPACKET(有序分组套接字)、SOCK_RAW(原始套接字)

  • 参数 protocol 指明某个协议类型常值或者设置为 0(置为 0 会选择给定 family 和 type 组合的系统默认值)

注意:也不是所有套接字 family 与 type 的组合都是有效的。 协议类型常值 protocol:IPPROTO_CP(TCP 传输协议)、IPPROTO_UDP(UDP 传输协议)、IPPROTO_SCTP(SCTP 传输协议) socket()函数在成功时返回一个小的非负整数,它与文件描述符类似,叫它套接字描述符(socket descriptor),简称sockfd。 看函数原型可知socket()函数只制订了协议族和套接字类型,并没指定本地协议地址或远程协议地址。

对比AF_XXXPF_XXX:AF_前缀表示地址族(Address Family)、PF_前缀表示协议族(Protocol Family)

connect() 函数

TCP 客户用connect()函数来建立与 TCP 服务器端连接。

1
2
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
  • 返回值:成功 0,出错 -1
  • sockfd:是socket()函数返回的套接字描述符
  • servaddr:指向套接字地址结构的指针
  • addrlen:该结构的大小

客户在调用connect()之前不用非得调用bind()函数,如果需要的话内核会去确定源 IP 地址,并选择一个临时端口作为源端口。

如果是 TCP 套接字,调用connect()函数将触发 TCP 的三路握手过程,而且尽在连接建立成功或出错的时候才返回,出错返回可能的几种情况:

  1. 若 TCP 客户没有收到 SYN 分节的响应,则返回 ETIMEDOUT 错误。就是报文超时。
  2. 若对客户的 SYN 的响应是 RST(表示复位),则表明该服务器主机在我们指定的端口上没有进程在等待与之连接
    1. 服务器进程也许就没在运行
    2. 这是一种硬错误(hard error),客户一接收 RST 就马上返回 ECONNREFUSED 错误
    3. RST 是 TCP 在发生错误时发送的一种 TCP 分节。产生的三个条件
      1. 目的地为某端口的 SYN 到达,然而这个端口上没有正在监听的服务器
      2. TCP 想取消一个已有连接
      3. TCP 接收到一个根本不存在的连接的分节
  3. 若客户发出的SYN在中间的某个路由器上引发了一个“destination unreadable”(目的地不可达)ICMP 错误
    1. 这是一种软错误(soft error),就是不会立刻返回错误,会按照一定时间间隔继续发送 SYN。
    2. 如果在规定的时间还没收到响应就将 ICMP 错误,作为 EHOSTUNREACH 或 ENETUNREACH 错误返回给进程。
    3. 可能原因:
      1. 按照本地系统的转发表,根本没有到达远程系统的路径
      2. connect()调用根本不等待就返回

根据 TCP 状态转换图,connect()函数导致当前套接字从CLOSED状态转移到SYN_SENT状态

  1. 成功,再转移到ESTABLISHED状态
  2. 失败,则该套接字不再可用,必须关闭,这个套接字就不能再次调用connect()函数来。
  3. 所以在每次connect()失败后,都必须close()当前的套接字描述符并重新调用socket()

bind() 函数

bind()函数把本地协议地址赋予一个套接字

协议地址:32 位的 IPv4 地址(或 128 位的 IPv6 地址)与 16 位的 TCP(或 UDP 端口号)组合。

1
2
#include <sys/socket.h>
int bind(int sockdf, const struct sockaddr *myaddr, socklen_t addrlen);
  • 返回值:成功 0,出错 -1
  • myaddr:指向特定于协议的地址结构的指针
  • addrlen:该地址结构的长度

对于 TCP,调用bind()函数可以指定一个端口号,或指定一个 IP 地址,也能都指定,或者都不指定。

常见返回的错误EADDRINUSE(Address already in use,地址已使用)

listen() 函数

listen()函数仅由 TCP 服务器调用,做 2 件事情: 当socket()函数创建一个套接字时,假定为一个主动套接字(就是将会调用connect()发起连接的客户套接字)

  1. listen()函数把一个未连接的套接字转换成一个被动套接字,指示内核要接受指向这个套接字的连接请求。
  2. 调用listen()函数会导致套接字从CLOSED状态转换到LISTEN状态。

listen()函数的第二个参数是内核应该为相应套接字排队的最大连接个数。

1
2
#include <sys/socket.h>
int listen(int sockfd, int backlog);
  • 返回值:成功 0,出错 -1

一般listen()函数在调用socket()bind()两个函数之后,并在调用accept()之前调用。

对于backlog参数,内核为任何一个给定的监听套接字维护两个队列:

  1. 未完成连接队列,某些 SYN 分节对应其中一项,这些套接字处于SYN_RCVD状态
    1. 某个客户以及发出并到达服务器,而服务器在等待完成相应的 TCP 三路握手过程。
  2. 已完成连接队列,每个已完成 TCP 三路握手过程的客户对应其中的一项。这些套接字处于ESTABLISHED状态

image.png TCP 为监听套接字维护的两个队列

每当在未完成连接队列中创建一项时,来自监听套接字的参数就复制到即将建立的连接中。连接的创建过程完全自动,无需服务器进程插手。

image.png

  1. 当来自客户的 SYN 到达时,TCP 在未完成连接队列中创建一个新项,然后响应以三路握手的第二个分节:服务器的 SYN 响应,其中捎带对客户 SYN 的 ACK。
  2. 这一项一直保留在未完成连接队列中,直到三路握手的第三个分节(客户对服务器 SYN 的 ACK)到达或者该项超时为止。
  3. 如果三路握手正常完成,该项从未完成队列移到已完成连接队列的队尾。
  4. 当进程调用accept()时,已完成连接队列中的队头项将返回给进程,或者如果该队列为空,那么进程将被投入睡眠,直到 TCP 在该队列放入一项才唤醒它。

accept() 函数

accept()函数由 TCP 服务器调用,用于从已完成队列队头返回下一个已完成连接。

如果已完成队列为空,那么进程将被投入睡眠(假定套接字为默认的阻塞方式)。

1
2
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
  • 返回值:成功为非负描述符,出错-1
  • cliaddr:已连接的对端进程(客户)的协议地址
  • addrlen:值-结果参数,调用前,将*addrlen所引用的整数值置为由cliaddr所指的套接字地址结构的长度,返回时,该整数值即为内核存放在该套接字地址结构内的确切字节数。

如果accept()成功,其返回值时由内核自动生成的一个全新描述符,代表与返回客户的 TCP 连接。

本函数最多返回三个值

  1. 可能新套接字描述符
  2. 可能出错指示的整数
  3. 客户进程的协议地址(cliaddr)以及该地址的大小(addrlen)

说明:

  1. 一个服务器通常仅仅创建1个监听套接字,在服务器的生命期内一直存在。
  2. 内核为每个服务器进程接受的客户连接创建一个已连接套接字(对于它的TCP三次握手已完成)。
  3. 当服务器完成对某个给定客户的服务时,相应的已连接套接字就被关闭(关闭指调用close())。

fork() 和 exec() 函数

fork()是 Unix 中派生新进程的唯一办法。

1
2
#include <unistd.h>
pid_t fork(void)
  • 返回值:在子进程中为 0,在父进程中为子进程 ID,出错 -1
    • 在调用进程(父进程)返回一次,返回值是新派生进程(子进程)的 PID;
    • 在子进程又返回一次,返回值是 0.

Q:fork()在子进程返回 0 而不是父进程的 PID?

Ans:任何子进程只有一个父进程,子进程总是可以 getppid 获得父进程 PID。相反,父进程可以只有许多子进程,而且无法获取各个子进程的 PID。如果父进程想要跟踪所有子进程的 PID,则必须记录每次调用fork()的返回值。父进程中调用fork()之前打开的所有描述符在fork()返回之后由子进程分享。

  1. 父进程调用accept()之后调用fork()
  2. 所接受的已连接套接字随后就在父进程与子进程之间共享。
  3. 一般,子进程接着读写这个已连接套接字,父进程则关闭这个已连接套接字(关闭是指调用close())。

fork() 两个典型用法

  1. 一个进程创建一个自身的副本,每个副本都可以在另一个副本执行其他任务的同时处理各自的某个操作。
  2. 一个进程想要执行另一个程序,就创建新进程的唯一办法是调用fork(),该进程于是首先调用fork()创建一个自身副本,然后其中一个副本(通常为子进程)调用exec()把自身替换成新的程序。(像 shell 之类程序的典型用法)

介绍 exec() 函数

存放在硬盘上的可执行程序文件能够被 Unix 执行的唯一办法:由现有进程调用 6 个 exec 函数中的一个。

exec 把当前进程映像替换成新的程序文件,而且该新程序通常从 main 函数开始执行。进程 ID 不改变。称调用 exec 的进程为调用进程(calling process),称新执行的程序为新程序(new program)

image.png

一般只有 execve 是内核中的系统调用,其他是调用 execve 的库函数。

close() 函数

通常的 Unix close()函数也用来关闭套接字,并终止 TCP 连接。

1
2
#include <unistd.h>
int close(int sockfd);
  • 返回值:成功 0,出错 -1

close一个TCP套接字的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程。可以用 SO_LINGER 套接字选项来改变这个默认行为。

描述符引用计数

并发服务器中父进程关闭已连接套接字只会让对应描述符的引用计数减 1。

引用计数大于 0,close()调用并不引发 TCP 的四分组连接终止序列。

如果我们确实想关闭某个 TCP 连接,发送一个 FIN,用shutdown()函数,不用close()

如果父进程对每个由accept()返回的已连接套接字都不调用close(),那么 TCP 的四次挥手永远也不会发生,引用计数永远大于 0,没有客户连接会被终止。

并发服务器

大多数 TCP 服务器是并发的,来一个客户连接就调用fork()派生一个子进程。

并发服务器程序大致过程

  1. 当一个连接建立时,accept()返回,服务器接着调用fork()
    1. 连接被内核接受,新的套接字 connfd(已连接套接字)被创建,由此跨连接读写数据。
    2. 并发服务器下一步调用fork(),搞出一个子进程
    3. 此时 listenfd 和 connfd 这两个描述符都在父进程和子进程之间共享(被复制)
  2. 然后由子进程服务客户(通过已连接套接字 connfd),父进程则等待另一个连接(通过监听套接字 listenfd)
    1. 这里父进程关闭已连接套接字,子进程关闭监听套接字,分别close()
    2. 引用计数减 1
  3. 此时新的客户由子进程服务,父进程就关闭已连接套接字(close)。

I/O 复用:select 和 poll 函数

TCP 客户会同时处理两个输入:1 标准输入(fgets) 2 TCP 套接字

出现的问题:客户将阻塞在 fgets 期间,另一个客户数据到达,服务器忙不过来无法及时处理。

解决:I/O 多路复用,即同时监听 N 个客户,解决对多个 I/O 监听时,一个 I/O 阻塞影响其他 I/O 的问题。

I/O 复用(I/O multiplexing):内核一旦发现进程指定的一个或多个 I/O 条件就绪,他就通知进程。

I/O 复用的典型应用:

  1. 客户处理多个描述符(交互式输入和网络套接字)
  2. 一个客户同时处理多个套接字
  3. 一个 TCP 服务器既要处理监听套接字,又要处理已连接套接字
  4. 一个服务器既要处理 TCP,又要处理 UDP
  5. 如果服务器要处理多个服务或者多个协议

一个输入操作通常包括两个不同的阶段:

  1. 等待数据准备好
    1. 等待数据从网络中到达
    2. 所有等待的分组到达时候,它被复制到内核中某个缓冲区
  2. 从内核向进程复制数据
    1. 把数据从内核缓冲区复制到应用缓冲区

五种 I/O 模型

五种I/O模型有

  1. blocking I/O
  2. nonblocking I/O
  3. I/O multiplexing
  4. signal-driven I/O
  5. asynchronous I/O

1 阻塞式 I/O 模型

默认情况,所有的套接字都是阻塞的。图示如下

image.png

2 非阻塞式 I/O 模型

把套接字设置为非阻塞就是通知内核,所请求的I/O操作需要将进程投入睡眠时候才能完成,不要投入睡眠,而是返回一个错误。

如图,当一个进程像这样对一个非阻塞描述符循环调用 recvfrom 时,就叫轮询(polling)

image.png

3 I/O 复用模型

调用 select 或 poll,阻塞在这两个系统调用某一个之上,而不是阻塞在真正的 I/O 系统调用上。

image.png

阻塞于select调用,等待数据报套接字可读。当返回可读时,就调用recvfrom把所读数据报复制到应用进程缓冲区。

select的优势在于可以等待多个描述符就绪。

4 信号驱动式 I/O 模型

用信号,让内核在描述符就绪时发送SIGIO信号通知。

image.png

5 异步 I/O 模型

异步I/O模型是指内核会在I/O操作完成时通知你,等待I/O完成期间你不会被阻塞。

image.png

各种 I/O 模型对比

  • 同步I/O操作(synchronous I/O opetaion)导致请求进程阻塞,直到I/O操作完成;
  • 异步I/O操作(asynchronous I/O opetaion)不导致请求进程阻塞。

image.png

select 函数

select函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的事件后才唤醒它。

讲个例子:

我们调用select,告知内核出现下面的几种情况的时候返回:

  • 集合 {1, 4, 5} 中的任何描述符准备好读;
  • 集合 {2, 7} 中的任何描述符准备好写;
  • 集合 {1, 4} 中的任何描述符有异常条件等待处理;
  • 已经历了 10.2 秒。

调用select告知内核对哪些描述符(读、写或异常条件)感兴趣以及等待多长时间。但不局限于套接字,任何描述符都可以用select来测试。

1
2
3
4
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdpl, fd_set *readset, fd_set *writeset, fd_set *exceptset,
					 const struct timeval *timeout);
  • timeout:告知内核它等待的指定描述符中的任意一个就绪能花多长时间。有三种可能
    • 永远等待下去:只在有一个描述符准备好I/O的时候才返回(设置为空指针)
    • 等待一段固定时间:在有一个描述符准备好I/O时返回,但不超过设定的时间。
    • 根本不等等:检查描述符后立即返回,这叫轮询(polling)(必须指向一个timeval结构,设定定时器值为0)
  • readset、writeset、exceptset:让内核测试读、写和异常条件的描述符。支持的异常条件只有俩
    • 某个套接字的带外数据的到达;
    • 某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息。

描述符就绪条件

满足下列4个条件中的任何一个时,一个套接字准备好

  1. 该套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的当前大小。
  2. 连接的读半部关闭(就是接收了FIN的TCP连接)。对这样子的套接字的读操作将不阻塞并返回0(就是返回EOF)
  3. 套接字时一个监听套接字且已完成的连接数不是0.
  4. 其上有一个套接字错误待处理。
    1. 这样的套接字的读操作将不阻塞并返回-1(就是返回一个错误)
    2. 同时把errno设置成确切的错误条件。
    3. 待处理错误(pending error)

满足下面4个条件中的任何一个时,一个套接字准备好写。

  1. 该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前大小,并且有下面俩条件的其中一个
    1. 该套接字已连接
    2. 该套接字不需要连接(UDP套接字)
  2. 该连接的写半部关闭。
    1. 对这种套接字的写操作将产生SIGPIPE信号
  3. 使用非阻塞式connect的套接字已建立连接,或者connect已经以失败告终。
  4. 其上有一个套接字错误待处理。
    1. 这样的套接字的写操作将不阻塞并返回-1(就是返回一个错误)
    2. 同时把errno设置成确切的错误条件。

如果一个套接字存在带外数据或者还处于带外标记,那么它有异常条件待处理。

下图汇总了上述导致select返回某个套接字就绪的条件:

select 的最大描述符数

大多数应用不会用到很多描述符,如果有那他也往往用select来复选描述符。 现在Unix版本允许每个进程使用事实上无限制的描述符(会受限于内存总量和管理性限制)

poll 函数

poll函数可用工作在任何描述符上。 poll函数提供的功能与select类似,不过在处理流设备时,它能提供额外的信息。

一个进程或线程同时监视多个文件描述符的状态,以确定是否有可读、可写或异常等事件发生,而无需阻塞在单个I/O操作上。

在解释一下

当调用poll()函数时,它会阻塞当前进程或线程,等待指定的文件描述符上发生感兴趣的事件。poll()函数使用 struct pollfd 数组来传递要监视的文件描述符和感兴趣的事件,并将实际发生的事件填充到同样的数组中的 revents 字段中。

poll 的机制与 select 类似,与 select 在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 poll 没有最大文件描述符数量的限制。

1
2
#include <poll.h>
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout)
  • 返回值:若有就绪描述符则为其数目,若超时为 0,出错 -1
  • nfds:设置结构数组fdarray中的元素的个数
  • timeout:指定poll函数返回前等待多长时间。
    • INFTIM:永远等待
    • 0:立即返回,不阻塞进程
    • 0:等待指定数目的毫秒数

  • fdarray:指向一个结构数组第一个元素的指针,每个数组元素都是一个pollfd结构,用于指定测试某个给定描述符fd的条件。
1
2
3
4
5
struct pollfd {
  int fd;         /* descriptor to check */
  short events;   /* events of interest on fd */
  short revents;  /* events that occurred on fd */
}

测试条件由events成员指定,函数在相应的revents成员中返回该描述符的状态。

这个图的意思是三个部分

  1. 第一部分处理输入的四个常值
  2. 第二部分处理输出的三个常值
  3. 第三个部分处理错误的三个常值

其中第三部分的三个不能在events中设置,但是相应条件存在就需要在revents中返回。

poll识别三类数据:普通(normal)、优先级带(priority band)和高优先级(high priority),这些听不懂的术语都是基于流的实现。

Q:什么情况下设置这些标志呢?

  • 所有正规TCP数据和UDP数据都被认为是普通数据
  • TCP读半部关闭时,也被认为是普通数据,随后读操作将返回0
  • TCP连接存在错误也可以认为是普通数据,随后的读操作返回-1,错误码从errno中获得
  • 监听套接字上有新的连接也可以认为是普通数据
This post is licensed under CC BY 4.0 by the author.