日常学习

linux-interface-socket

January 14, 2019

The linux programming interface

Socket

socket是一种IPC方法, 它允许位于同意主机或者网络连接的不同主机 上的应用程序之间交换数据(第一个被广泛接收的socket API 实现于 1983年,现在这组API 已经被移植到了大部分的计算机系统上)

  1. socket(domain, type, protocol): 系统调用
    • domain: 1)识别 socket 地址的格式 2) 确定范围: 在同一个主机上的不同应用程序还是 在一个网络上的不同主机

      DOMAIN 执行的通讯 应用程序间的通讯 地址格式 地址结构
      AF_UNIX 内核中 同一主机 路径名 sockaddr_un
      AF_INET 通过IPv4 IPv4 连接起来的网络 32为IPoe 地址+ 16位端口号 sockaddr_in
      AF_INET6 通过IPv6 IPv6 连接起来的网络 128为IP地址+ 16位端口号 sockaddr_in6
    • type: sock_stream, sock_dgram

      属性 流(SOCK_STREAM) 数据包 (SOCK_DGRAM)
      可靠的传输?
      边界消息保留?
      面向连接?
    • 流: 提供了一个可靠的双向的字节流的通讯通道。 (因为需要一对 相互连接的socket,因为被称为面向连接的socket)其中:
      1. 可靠的: 表示可以保证发送者传输的数据会完整的传递到接收者应用程序 (假设接收者发送者应用程序不会崩溃)
      2. 双向的: 数据可以在socket 之间的任意方向上传输
      3. 字节流: 表示与管道一样不存在 消息边界的概念
    • 数据报 socket: 允许数据以 数据报的消息形式进行交换, 在数据报socket中, 消息边界得到了保留,但是数据传输是不可靠的,消息的到达顺序 可能是无序的、重复的、或者根本无法到达的。数据包socket是一个更一般的无连接socket概念的一个示例, 与流socket连接,一个数据报 socket 在使用时候,无需与另一个socket 连接,现在internet domain 中, 数据包socket使用了UDP(用户数据报协议), 而流socket 则使用了TCP(传输控制协议)(是否意味着更多的协议的存在?)
  2. socket 相关的系统调用:
    • socket(int domain, int type, int protocol): 其中domain, type 在上面有所描述。protocol 总是为0, 在一些socket类型中会使用非0数值,socket成功后会返回一个socket的文件描述符
    • bind(int sockfd, struct sockaddr * addr, socklen_t addrlen): sockfd 为 socket调用返回的文件描述符,addr 为socket绑定到的地址结构指针,结构详细取决于 socket domain, addrlen 为结构地址大小。一般来讲服务器会将socket 绑定到一个 约定的地址上。
    • listen(int sockfd, int backlog): 系统调用将会sockfd设定为 『被动』, 接收主动连接的请求。 backlog 用于 设定 服务器端 保持等待连接的数量。(在backlog之内的连接会立即成功,等待accept, 更多的连接会阻塞一直到有等待中的连接被accept并冲等待连接中删除掉)(backlog的限制在sys/socket.h 中的 somaxconn 常量设定, linux中 这个常量设定为128,从内核 2.4.25起 linux允许在运行时通过 特有的/proc/sys/net/core/somaxconn 文件来调整这个限制)
    • accept(int sockfd, struct sockaddr * addr, socklen_t * addrlen): 系统调用在 sockfd 的文件描述符 引用的监听流socket上接收一个接入连接。如果在调用accpet时不存在 未决 的连接,那么调用就会阻塞直到 有连接请求为止。参数 addr, addrlen, 会返回连接socket 的地址信息。理解accept 的关键点在于:
      • accept 会创建一个新的scoket, 这个socket与执行connect的客户端scoket进行连接。
      • accpet调用返回的结果是 已连接的 socket文件描述符,监听 socketfd 会保持打开状态。并可以接收后续连接。
      • 典型的 服务器应用 会创建一个 监听socketfd, 将其绑定到一个约定的地址上。然后 accept 该socketfd 上 的连接 来处理所有的客户端请求。
    • connect(int sockfd, struct sockaddr * addr, scoklen_t addrlen): 系统调用将sockfd 主动连接到 地址addr 指定的监听socket上。如果连接失败,标准的可以移植的方法为: 关闭socket,创建一个新的socket,并重新连接
      udp
  3. 流 socket 提供了一个在两个端点之间 一个双向通信的通道,流socket IO 上的操作与 管道 IO的操作类似
    • 可以使用 read, write,因为socket是双向的,所以两端都可以使用
    • socket可以使用close来关闭,之后对应的另一端的socket 在读取数据时候会收到文件结束的标志,如果继续进行写入 会收到一个SIGPIPE的信号,并且系统调用会返回一个EPIPE的错误。
  4. 数据报 socket(SOCK_DGRAM):
    1. socket 系统调用创建一个邮箱,
    2. bind 到一个约定的地址上, 来允许 一个应用程序发送数据报 到这里,一般来讲, 一个服务器会将其socket 绑定到一个地址上,客户端会向该地址发送一个数据报 来发起通讯 (在一个domain 特别是UNIX domain 中,客服端想接收到服务器发送来的数据报的话,也需要bind到一个地址上)
    3. sendto(int sockfd, void * buffer, size_t length, int flags, sockaddr * dest_addr, socklen_t addrlen): 用来发送一个数据报, flags 用来控制一些socket的特性,dest_addr置顶了目标接收者的socket地址,
    4. recvfrom(int sockfd, void * buffer, size_t length, int flags, sockaddr * src_addr, socklen_t addrlen): 用来接收数据报,在没有数据报时候会阻塞应用。由于recvfrom允许获取发送者的地址,因为可以发送一个响应(这在 发送者的socket没有绑定到一个地址上是有用的,正如bind中的描述所说,unix domain中也需要 客服端 来bind一个地址,才能接收到服务器的响应)其中 src_addr 用来获取发送数据报的远程socket地址,如果并不关心发送者的地址,可以传递NULL,length 数值 用来限制recvfrom获取的数据大小,如果超过length,则会进行截断。(使用recvmsg 则可以找出被截断的数据报)
    5. 数据报通讯无法保证 数据报 接收的顺序,甚至无法保证数据是到达的 或者是 多次到达
    6. connect: 尽管数据报socket是无连接的,但是依然可以使用connect调用
      • 发送者 socket connect之后,数据报的发送可以使用write来完成,而无需使用sendto,并每次传递addr地址。
      • 接收者 socket connect之后,只能接收由对等的socket 发送的数据报
      • 数据报socket connect的明显优势在于 可以使用更简单的IO 系统调用,在一些TCP/IP实践中,将一个数据报的socket连接到一个对等socket(connect)能够带来性能上的提升
        udp

Socket: Unix domain

  1. Unix domain socket address:
    unix domain socket的地址以路径名来表示,其中sun_path 的大小,早期为 108, 104,现在的一般为 92,可移植的需要小一些,应该使用strncpy 以避免缓冲区溢出问题,使用 路径名 初始化 sun_path 来初始化 socket address

       struct sockaddr_un {
         sa_family_t sun_family
         char sun_path[108];
       }
    
    
    • 当绑定 UNIX domain socket 时, bind会在 文件系统中创建一个条目, 文件的所有权会根据文件的创建规则来确定,并标记为一个socket, ls -l 第一列 为s, stat()返回的结构中st_mode字段中的文件类型部分为 S_IFSOCK,
    • 无法将一个socket绑定到 现有的路径名上。
    • 通常将一个socket绑定到绝对路径上
    • 一个socket只能绑定到一个路径名上,相应的一个路径名只能被一个socket绑定
    • 无法使用open打开一个socket
    • 不在需要socket时,使用unlink 来删除其路径
    • 示例中通常将socket绑定到/tmp目录下,这并不是一个好的设计,在现实中不要这么做,因为/tmp 此类公共可写的目录中创建文件会导致安全问题,所以应该将socket绑定到一个有安全措施的绝对路径上
  2. socketpair(int domain, int type, int protocol, int sockfd[2]): 该系统调用 用于创建一对 互相连接的socket,
    • 只能用在UNIX domain中(也就是说 domain 必须指定为 AF_UNIX) type 可以为sock_dgram, sock_stream, protocol必须为0,
    • sockfd 数组返回了 引用这两个相互连接的socket文件描述符。type 为sock_stream 相当于创建了一个双向管道,一般来讲 socket对的使用方式与管道的使用方式类似,在调用完socketpair()之后,可以fork出一个子进程,然后子父进程可以通过这一对socket来进行IPC了。
    • 与 手动创建一对相互连接的socket的做法的优势: socketpair 创建的socket不会绑定到任意的地址上(即其他方式的socket创建都需要bind 到一个地址上)这样就能避免安全问题,因为这一对socket对其他进程是不可见的
  3. linux 抽象socket 命名空间
    所谓的抽象命名空间 是linux特有的特性。他允许将一个UNIX domain socket绑定到一个名字上但不会在文件系统上创建该名字 优势有:
    • 无需担心与文件系统中的既有名字冲突
    • 没有必要在使用完一个socket之后,删除socket路径名,当socket被关闭之后会自动删除这个抽象名
    • 无需为socket创建一个文件系统路径名了
    • 创建一个抽象的绑定,只需要将sun_path字段的第一个字节指定为null,用于区分抽象socket 与传统的UNIX domain socket

TCP/IP 网络基础

一个联网协议是定了如何在一个网络上传输信息的一组规则,网络协议通常会被组织成一系列的层,其中每一层都构建于下一层之上并提供特性以供上层使用。TCP/IP 协议套件 是一个封层联网协议。包括因特网协议(IP)和魏雨琦上层的各个协议层。 TCP 为传输层控制协议

  1. 封装协议的特点有:
    1. 透明: 每一个协议层都对上层 隐藏下层的操作和复杂性。 入一个使用TCP的应用程序只需要使用标准的socket API并清楚自己在使用 一项可靠的字节流传输服务,而无需理解TCP操作的细节。(严格来说 这个标准并不总是正确,应用程序偶尔也需要弄清楚 底层传输协议的操作细节)
    2. 封装: 是分层连网协议中的一个重要的原则。关键概念: 底层会将从高层向底层传递的信息 当成不透明的数据进行处理。并不会尝试对高哦曾发送过来的信息进行解释。只会 将这些信息 添加自身这一层所使用的头信息,并传递到下一层。当数据从底层传递到高层时,会进行一个逆向的解包过程。
      tcp_ip_protocol
  2. 数据链路层: 传输数据, 数据链路层需要将上层传递过来的数据报封装进 被称为帧的一个一个单元,其中每帧都会包含一个头,包含了目标地址和帧的大小。数据链路层在物理连接上 传输帧并处理来自接收者的确认。这一层可能进行 错误检测、重传、以及流量控制。一些数据链层还会将大的网络报分割成多个帧并在接收者端对这些帧进行重传。应用程序编程的角度通常可以忽略 数据链路层,因为所有的通讯细节都是由 驱动和硬件来处理的。有关IP的讨论中, 数据链路层中比较重要的一个特点是最大传输单元(MTU)MTU是该层 所能传输的帧大小的上限, 不同的数据链路层MTU是不同的(netstat -i )
  3. 网络层IP
    网络层的、关注的为 如何将包 从愿主几发送到目标主机, 这一层执行的任务如下:
    • 将数据分解成足够小的片段一变数据链路层进行传输
    • 在因特网上路由数据
    • 为传输层提供服务
    • 在TCP/IP 协议套件中, 网路层主要的协议是IP, 存在IPv4, IPv6版本。一个IP数据报包含了一个头,起大小范围为20字节到60字节,这个包中包含了的目标主机的地址,以及包的源地址。这样就可以在网络上讲这个数据报路由到目标地址了、以及接收方直到数据报的源头了。
    • IP 是无连接协议、不可靠的协。 尽可能的将数据报从发送者传给接收者。当并不保证数据报到达的顺序以是否重传,甚至是否达到等。IP 也没有提供数据恢复。TCP/IP 的可靠性是通过使用一个可靠的TCP (传输层他协议)来保证的。
    • IP 路径MTU: 原主机到目标主机之间的路径上的所有数据链路层的最小MTU (通常以太网的MTU 最小)当一个数据报的大小大鱼MTU时, IP会将数据报分段(分解成一个大小适合在网络上传输的单元, 这些分段在达到最终目标地址之后会被重组成原始的数据报), IP分段的发生对高层协议层是透明的,一般来讲并不希望发生这种事情。这里的问题在于IP并不进行重传,所以只有在所有分段到达目标之后才能对数据报进行重组,导致如果其中一些分段丢失可能导致整个数据报的失败,一些情况下会导致极高的丢包率(比如UDP并不会进行重传)或者降低传输速率(进行重传的TCP) 现在TCP实现采用了一些算法(路径MTU发现,这个是不是很简单的获取 IP的路径MTU?)并根据该值对传递给IP 的数据进行分解,来防止IP层对数据报进程分解。
  4. 传输层
    1. 端口号: 传输层协议的任务是向位于不同主机的上的应用程序提供端到端 通讯服务,所以传输层需要才用一种方法来区分一个主机上的应用程序,端口号的工作就是如此。(16位)
    2. UDP(用户数据报协议): 仅仅在IP上增加了: 1) 端口号, 2)进行检测传数据错误的 数据校验和, 因为IP是无连接的,而UCP并没有进行 可靠性的保证,所以UCP 具有同IP一样的特性
    3. TCP(传输控制协议): TCP 在两个端点之间提供了可靠的、面向连接的、双响字节流通信通道。通过如下几个方面来保证:
      • 建立连接: 在连接期间,即 对交换数据 的通讯参数进行协商
      • 将数据打包成分段: 将数据分解成段(使单个IP数据报封装成为可能,从而避免 IP层面 进行拆解)每个段都包含一个校验和,并使用单个IP进行传输,从而能够检出数据报的错误。
      • 确认、重传、超时: 一个TCP段无错的到达接收方,接收方会向发送者发送确认请求,如果报发生错误,接收方丢弃即可。发送方 在发送每一个分段时会启用一个定时器,在定时器超时时没有收到确认。那么就重传这个分段 (由于所使用的网络以及当前的流量负载会影响 传输一个分段和接收其确认 所需要的时间,所以 TCP采用了 一个算法来动态的调整 重传超时时间(RTO) 的大小。接收者可能不会立即发送确认,而是等待几毫秒 来观察是否可以将 确认塞进接收者返回给发送者的响应中(因为相应是接收者发送给接收者的,并不需要传递确认信息)来减少一个TCP段的发送,从而降低网络中的包的数量。这个称为 延迟ACK 技术)
      • 排序: TCP连接中的每个分段都会分配到一个逻辑号。这个数字指出了该分段在 该连接 的数据流中的位置(连接中的两个流都有各自的序号计数系统)序号的作用有: 1)这个序号可以保证TCP分段能够以正确的顺序在接收者进行组装, 然后以字节流的形式传送给应用层 2)接收者 发送给发送者 使用序号来标识出收到了那个TCP分段 3)接收者可以去除重复的分段信息。(一个流的初始序列ISN, 不是从0开始的,而是通过一个算法来生成的,该算法会递增分配给后续的TCP链接的ISN(防止前后的多个连接中 序号重复混淆的情况发生))
      • 流量控制: 防止一个快速的发送者压垮一个慢速的接收者: 如何实现: 接收TCP 需要为进入的数据维护一个缓冲区(每个TCP在建立连接时候,都会告知其缓冲区大小)当从发送TCP端收到数据时会将数据放入到缓冲区中。当应用层读取数据时会从缓冲区中删除数据, 在每个确认中,接收者会通知发送者 其缓冲区的可用空间,TCP流量控制算法 采用了 滑动窗口算法,来允许包含N个字节的窗口大小的 未确认段 同时在 发送者与接收者之间传递,接收端的缓冲区被充满,那么窗口就会关闭,发送端就会停止传输数据。
        tcp_protocol
  5. RFC(请求注解): 是由 国际互联网学会赞助的RFC编辑组织发布的,描述互联网标准的RFC是由互联网工程任务组资助开发的, 互联网工程任务组 是一个由 网络设计师、操作员、厂商以及研究人员组成的社区,主要关注互联网的发展和平稳运行。

服务器设计

  1. 迭代型: 服务器每次只处理一个客户端,只有当完全处理完一个客户端的请求后,才会去处理下一个客户端
  2. 并发型: 能够同时处理多个客户端的请求。 本章主要放在并发型服务器的传统设计方法:针对每个新的客户端连接, 创建一个新的子进程来处理,每个服务器子进程执行完所有服务于单个客户端的任务后就终止。因此可以同时处理多个客户端。
    • 由于 服务器为每个客户端连接创建一个子进程,需要保证不会出现僵尸进程,所以需要为信号SIGCHLD 安装信号处理器。
    • 主进程 主要由for 循环组成。在循环中accept 链接,然后fork 创建子进程, 在子进程中 调用hanldeRequest来处理客户端。(现实世界中,需要限制 服务器创建子进程的数量,大量的子进程会使系统变的不可用。)
    • 每次fork后,监听套接字和连接套接字都在子进程中得到了复制。父进程 关闭 连接套接字, 子进程关闭 监听套接字。(如果父进程没有关闭 连接套接字的话 那么连接套接字永远不会被关闭,从而导致文件描述符被用完。监听套接字并不会这样,因为子进程 结束之后会释放所有的文件描述符)
    • 每个子进程处理完客户端之后,终止
  3. 并发型服务器的其他设计:
    • 预先创建进程或线程:
      1. 服务器在启动阶段就立刻 预先创建好一定数量的 子进程, 而不是针对一个新的客户端来创建 一个子进程。这些子进程构成了一个 服务池
      2. 每个子进程一次只处理一个客户端,在处理完之后,子进程并不终止,而是获取下一个待处理的客户端继续处理。
      3. 主进程需要仔细的管理子进程,并可以相应的根据负载来调节子进程的数量大小,此外, 子进程需要遵循某些协议,是的他们是以 独占 的方式来处理一个客户端的 连接套接字的。在大多数的UNIX实现中,让子进程 在监听套接字上调用accept即可,(即是: 主进程先创建 监听套接字,子进程在每个fork之后 继续使用套接字 并 accept,因为accept调用是一个原子化的操作,所以当客户端连接 到来时,之后一个子进程能够完成 accept调用。负载则由系统进行调度)
    • 单个进程中处理多个客户端: 必须能够允许单个进程同时监听多个文件描述符上 IO事件 (IO多路复用, 信号驱动IO, epoll),单进程服务器需要做一些通常由内核来处理的调度任务。在 进程服务池 的设计中,我们可以依靠内核来确保每个服务器进程 能够公平的访问服务器主机的资源。但是当使用 单个进程处理多个客户端的方案时,服务器进程必须自行确保一个或多个 客户端不会霸占服务器,而使 其他的客户端处于饥饿状态
  4. 服务器集群:
    • DNS 轮询负载共享:一个地区的域名权威服务器将同一个域名映射到多个IP地址上(即 多个服务器共享统一域名)后续对DNS服务器的域名解析请求将以 循环轮转的方式返回这些IP地址,优势是成本低,存在的问题有:客户端 DNS缓存、 没有任何机制来达到良好的负载均衡、高可用 的机制 以及 无法确保同一个客户端的请求都到达同一台服务器(所以存在状态的服务器 需要在多个机器之间共享状态 这个特性成为 服务器亲和力)
    • 负载均衡(server load balancing): 由一台负载均衡服务器将客户端的请求路由到服务器集群中的一个,这消除了 远端DNS 缓存所引起的问题。因为服务器集群只对外传递一个IP地址。负载均衡服务器会 结合一些算法来衡量或计算服务器负载 并智能化的 将负载分发到 集群中的各个成员上,可能还会提供对服务器亲和力的支持
  5. inted 守护进程:
    1. 守护进程 inetd 被设计为用来消除运行大量非 常用服务器进程的需要,inetd 优势有:
      1. 预期为每个服务运行一个单独的守护进程,现在只用一个进程 inetd 守护进程,就可以监视一组指定的套接字端口,并按照需要启动其服务,因为可以降低系统进程的运行数量
      2. inetd 简化了 启动服务的编程工作。
    2. inetd 进程:
      1. 读取 /etc/inetd.conf 文件,对于其中的每项服务,创建一个 恰当类型的套接字,然后绑定到指定的端口上,其中每个TCP套接字 都会通过listen 允许客户端请求连接
      2. 通过select调用, inetd 对前一步中创建的所有的套接字进行监听,看是否有数据报或者连接请求 进来
      3. select 进入阻塞状态: 直到一个UDP 数据报到来或 TCP 监听套接字 收到连接请求。TCP 会进行accept
      4. 启动套接字对应的服务,inetd 调用fork创建 一个新进程,然后调用exec 启动服务器程序,执行exec前,子进程执行如下步骤:这里面 子进程 代表的是 inetd 守护进程的标准执行过程, 所以需要执行步骤2,方便exec启动的服务器程序 使用标准文件描述符来 对客户端进行通讯
        1. 关闭从父进程继承的所有文件描述符,除了 用于监听的套接字
        2. 在文件描述符0,1,2上复制套接字文件描述符,并关闭套接字文件描述符本身,完成这一步之后,启动服务器进程就能通过 这三个标准 文件描述符 同套接字 通讯了
        3. 为启动的服务器进程设定用户、组ID (可选,通过/etc/inetd.conf 配置)
      5. 如果是TCP套接字,则关闭连接套接字
      6. 返回到 第 2步骤,继续执行
    3. /etc/inetd.conf 配置文件:

      service name socket type protocol flags login name server program server program argument
      ftp stream tcp nowait root /usr/sbin/tcpd in.ftpd
      telnet stream tcp nowait root /usr/sbin/tcpd in.telnetd
      login stream tcp nowait root /usr/sbin/tcpd in.rlogind
    • service name (服务名称): 该字段为服务名称,结合 protocol 就可以通过查找 /etc/services 文件以确定 inted 问该服务监听的 端口号
    • socket type (套接字类型): stream, or dgram
    • protocol(协议): 该字段指定了 这个套接字所使用的协议,这个字段 可以包含文件 /etc/protocols 中所列出的任何的 internet 协议
    • flags:
      • wait or nowait 字段表明 由 inetd 启动的服务器 是否会接管 用于该服务的 监听套接字。 wait 表明 启动的服务器 需要管理 该监听套接字,inetd 将该套接字从它所监视的文件描述符列表中移除, 直到这个服务器程序退出为止。
      • inetd 调用的 TCP 服务器通常被设计为 只处理一个单独的客户端连接,处理完后就终止, 把监听其他链接的任务留给inetd, 对于这样的服务器, flags字段应为 nowait, 相反 如果是被执行的服务器进程 来接受连接(accept)的话,那么该字段为wait, 此时inetd 不会去接受连接,而是将监听套接字 作为 文件描述符0 传递给 服务器进程。对于大部分的UDP 服务器, flags字段需要设定为wait, 由 inetd 调用的UDP 服务器设定为 读取并处理所有套接字上未完成的数据报,然后终止(通常需要一些超时机制)wait 可以组织 inetd在套接字上做select,会导致inetd 同 UDP 服务器程序之间,产生竞争条件,如果inetd 赢了,会启动一个新的 UDP 服务器实例
    • login name(登录名) :该字段为 /etc/passwd 中的用户名组成部分,还可以在其后 添加 “:” + /etc/group 中的组名称, 这些确定了运行的服务器程序的用户ID和组ID, 因为inetd为 root 方式运行,所以 子进程同样可以是 特权进程。 因此可以在需要时 通过调用setuid, setgid 来修改进程凭证
    • serever program (服务器程序): 指定了 被执行程序的服务器程序路径
    • server program argument(服务器 程序参数): 该字段指定了 一个或多个参数, 参数之间由 空格符分隔,当执行服务器程序时,这些参数就作为程序的参数列表
    • inetd 作为一个提高效率的机制,本身就实现了一些简单的服务,而无需单独的 服务器程序编码来完成任务, UDP, TCP 的echo 服务就是由inetd 来实现的一个例子。编辑修改 /etc/inetd.conf 之后,需要 killall -HUP inetd 发送SIGHUP信号来重新读取配置文件

socket 高级主题

  1. 流式套接字 上的部分读和部分写
    • read, write 系统调用 会产生部分读 和 部分写,在流式套接字上 更容易出现这样的问题。套接字傻姑娘可用的数据比read 读取的数量要少,那么read就会出现部分读 的现象,但是只是简单的返回读取的内容大小。write 调用在没有足够的缓冲区 来传输所有的字节时,并且 被信号处理函数中断 或 在 非阻塞模式下工作 或 TCP连接出现问题 write 会产生部分写的现象。 readn, writen 这两个函数 使用循环来 启用这些系统调用,总能确保 所有的数据都会被写入 或者读取。
  2. shutdown(int sockfd, int how) 系统调用
    套接字 上调用close 将双向通讯通道 的两端都关闭,shutdown 提供了 更精细的控制 其中how的选项有;
    • SHUT_RD: 关闭连接的 读取端, 之后的读取操作将返回文件结尾,写入套接字操作依然可以进行。在UNIX domain 上进行 SHUT_RD, 对端的应用程序 将接受一个 SIGPIPE 信号,对端程序依然写入的话 将产生 EPIPE 错误, 对于TCP 套接字来说没有什么意义(需要在 61.6.6 中讨论)
    • SHUT_WR: 关闭连接的 写端, 对端 会检测到 文件结尾。后续对套接字的读取操作会产生 SIGPIPE 信号以及 EPIPE 错误,而由对端的写入数据依然可以在套接字上正常读取。这个操作允许我们依然可以读取数据,并且告知对方写入已经完成。该操作在ssh 中rsh中 有用到,并称为 半关闭 套接字
    • SHUT_RDWR: 将连接的读写端都关闭。等同于调用了 shutdown SHUT_RD, SHUT_WR
    • 区分于close: shutdown 关闭的是系统级 文件表, 而非 进程文件描述符。 这意味着, 父进程 shutdown之后, fork 的子进程中的文件描述符 同样受到影响。需要注意的是, shutdown 并不会关闭进程文件描述符, 依然需要进程close来关闭文件描述符

      fd2 = dup(sockfd);
      close(sockfd); // 之后依然可以在 fd2上进行IO操作
      //--------
      fd2 = dup(sockfd);
      shutdown(sockfd, SHUT_RDWR); // 之后无法在fd2 上sockfd 上 进行IO操作
      
  3. recv, send, 专属于 套接字的IO系统调用:
    • recv(int sockfd, void * buffer, size_t length, inf flags) flags 的选项有:
      • MSG_DONTWAIT: 使recv 以非阻塞方式执行, 没有数据可用 立即返回, 错误码为EAGAIN, 同样可以通过 fcntl 来把套接字设定为非阻塞方式运行, 区别在于,这个可以设定每次的调用的阻塞行为
      • MSG_OOB: 在 套接字上接受带外数据
      • MSG_PEEK: 从套接字数据缓冲区 获取一份请求字节的副本,但不会将数据从缓冲区中移除,这份数据可以在之后的read中重新读取
      • MSG_WAITALL: 指定标记后, 将导致系统调用阻塞到 接收到length字节,但是总会出现返回的字节数少于 length的情况: 1)捕获到一个信号, 2)对端终止了连接 3) 遇到了带外数据字节 d)接收到的数据总长度小于 length, 4)套接字错误
    • send(int sockfd, const void * buffer, size_t length, int flags)
      • MSG_DONTWAIT: send 以非阻塞方式运行,如果 数据不能立即传送(发送缓冲区满时)该调用失败,错误码 EAGAIN
      • MSG_MORE: 在TCP 套接字上,这个标记实现的效果同 套接字选项 TCP_CORK 完成的功能相同,区别在于该标记可以在每次调用中 对数据进行 栓塞 处理。
      • MSG_NOSIGNAL: 指定该标记时,在已连接的套接字上发送数据时,如果连接的另一端已经关闭时,send 不会产生SIGPIPE信号,而是返回错误 EPIPE
      • MSG_OOB: 在流式套接字 上 发送带外数据
  4. size_t sendfile(int outfd, int in_fd, off_t * offset, size_t count):
    传输文件的简单写法:
       while ((n = read(diskfilefd, buf, BUZ_SIZE)) > 0)
         write(sockfd, buf, n);
    

    示例代码中 read 简单的将文件内容 从内核缓冲区cache中拷贝到用户空间,write将用户空间缓冲区拷贝到内核空间中的socket缓冲区。 sendfile 被用来减少这种操作的低效性。文件内容会直接传送到套接字上,而不会经过用户空间, 这种技术成为 zero-copy transfer 零拷贝传输
    sendfile 函数调用的限制: out_fd 必须为套接字, in_fd 必须指向文件,能够进行mmap,这通常只能是一个普通文件。
    zero_copy

  5. TCP_CORK 套接字选项: 为了提高TCP使用效率(linux专有的选项),在web服务器传送页面时候,作为请求的响应,通常由两部分组成, HTTP 首部, 页面数据,单独的使用write操作 会传输2个TCP报文段,一个非常小的HTTP报文放在第一个分段中,这对网络是非常浪费的。这时候使用TCP_CORK 来避免其低效性。当在TCP 套接字上启用 TCP_CORK 选项时,之后所有的数据都会穿充到一个单独的TCP 报文段中,直到满足以下条件为止: 以达到报文段的大小上限、取消了 TCP_CORK 选项、套接字被关闭、或者启用 TCP_CORK后,从写入的第一个字节开始已经超过200ms(防止忘记取消 TCP_CORK 选项,超时时间可以保证传输)下面例子 介绍如何使用 TCP_CORK:
     optval = 1
     setsockopt(sockfd, IPPROTO_TCP, TCP_CORK, sizeof(optval)); // enable TCP_CORK
     write(sockfd, ...);
     sendfile(sockfd, ...);
     optval = 0
     setsockopt(sockfd, IPPROTO_TCP, TCP_CORK, sizeof(optval)); // disable TCP_CORK, TCP 开始传输
    
  6. 获取套接字地址:
    • getsockname(int sockfd, struct sockaddr * addr, socklen_t * addrlen): 获取本地套接字地址, sockfd表示套接字的描述符, addr 为返回的套接字地址存储结构, addrlen 为 addr 结构的大小。在套接字并不是由自己初始化时,如 inet调用的应用程序只能获取 已经存在套接字,则可以通过该函数 获取 对应的绑定地址。
    • getpeername(int sockfd, struct sockaddr * addr, socklen_t * addrlen): 获取对端的套接字地址, sockfd, 为对端的套接字描述符, 其他的同 getsockname 一致。在TCP 连接中,可以在accept时获取对端地址,但是如果服务器进程是由另一个进城调用的,比如inetd,那么这个函数就非常有用
    • 内核隐式绑定的情况:
      1. 已经在TCP 套接字上执行了connect, listen调用,但之前并没有bind 调用到一个地址上
      2. 在UDP套接字上 首次调用sendto,盖套接字之前并没有bind到地址上
      3. 调用bind 时,将端口号 指定为0, 这种情况下 bind 会为套接字制定一个IP地址,并选择一个临时端口号
  7. 深入探讨TCP 协议
    1. TCP 报文格式
      zero_copy
      • Source port number(源端口号): TCP 发送端的端口号
      • Destination port number(目的端口号): TCP 接收端的端口号
      • Sequence number(序列号): 报文的 序列号
      • Acknowledgement number(确认序列号): 如果设定了ACK 位,那么这个字段包含了接收方期望从发送方接收到的下一个 报文的序列号
      • Header length: 表示TCP 报文首部的长度,首部长度单位是32位,因为这个字段只有4个byte位,所以首部总长度最大可以达到 60字节, 该字段 是的TCP接收端可以确定变长的选项字段的长度,以及数据域的起始点
      • Reserved(保留位): 该字段包含4个为啥喜欢i用的byte (必须设置为 0)
      • Control bit(控制位): 8个byte组成:
        - CER: 拥塞窗口减小标记
        - ECE: 现实的拥塞通知回显标记 cwr & ece 标记用在TCP 的显示拥塞通知(ECN)算法中。 linux中 可以通过编辑文件 /proc/sys/net/ipv4/tcp_enc 设定一个非零值 来开启这个功能
        - URG: 设定了该位, 紧急指针字段包含的信息是有效的
        - ACK: 如果设定了该位, 那么确认序号字段包含的信息就是有效的 (可以同时包含对对端数据报的确认)
        - PSH: 将所有收到的数据发给接受的进程 (RFC993)
        - RST: 重置连接
        - SYN: 同步序列号,在建立连接时,双方需要交换设定了该位的报文,使得tcp 连接的两段可以指定初始序列好
        - FIN: 发送端提示已经完成了传送任务,TCP 连接关闭
      • Window size (窗口大小): 滑动窗口机智有关, 用于 在ACK确认时 提示自己可以接受数据的空间大小
      • Checksum(校验和)
      • Urgent Pointer(紧急指针): 设定了该位置, 表示传送的数据位紧急数据
      • Options(选项): 这是一个变长的字段, 包含了控制TCP连接操作的选项
      • Data (数据): 包含了该报文中 传输的用户数据
    2. TCP 序列号 和 确认机制
      每个通过TCP 连接传送的字节都由TCP 协议分配了一个逻辑序列号,双向数据流都有各自的序列号,当传送一个报文时,该报文的序列号被设为该传送方向上的 报文段数据域的第一个字姐的逻辑偏移。这样接收端 就可以按照正确的顺序对接收到的报文进行重组了。TCP 采用了主动确认,当一个报文段被成功接收后, 接收端会发送一个确认信息 即发送ACK 确认报文 给发送端,该报文的 确认序号字段被设置为 期望接受的下一个数据字节的逻辑序列号 (上一个成功收到的序列号 + 1 )TCP 发送端发送报文时会启动一个定时器,如果在定时器超时时,仍未收到确认报文,那么就重传该报文 (注意 序列号并非常简单的递增 1,而是 按照 传送报文数据 大小来递增的 如下图)
      tcp_ack
    3. TCP 连接的建立: API 层面: 服务器)调用listen 打开套接字,然后accept, 阻塞服务器进程 直到连接建立完成。客户端)调用connect 同服务器打开的套接字 建立连接
      1. 客户端 TCP节点 发送一个SYN 报文到服务器 TCP端,这个报文将告知 服务器有关客户端的TCP节点的初始序列号 (因为序列号不是从0 开始)
      2. 服务器 TCP端 发送确认 客户端 SYN报文的 ACK报文,并同时携带 SYN 的序列号。 即发送ACK,SYN 报文
      3. 客户端 TCP 节点发送一个ACK报文 来确认服务器端的TCP SYN 报文
        tcp_three_hand_shake
    4. TCP 连接的终止: 一端的应用程序执行close 调用 (主动关闭), 之后 连接另一端的应用程序 也执行close调用(被动关闭)下面的报文顺序为假设 客户端 发起主动关闭
      1. 客户端执行主动关闭, 导致客户端TCP节点 发送一个FIN报文到服务器端
      2. 服务器端收到FIN 报文后,发送 ACK 报文进行响应。(之后服务器端任何对 套接字 read 操作 的尝试都会读取到 文件结尾)
      3. 稍后, 当服务器关闭 自己端的 连接时,服务器端 TCP 节点发送 FIN报文到客户端
      4. 客户端TCP 节点发送ACK报文作为响应
        tcp_close
        上面讨论的是 close 全双工的关闭(连接虽然是双向的,但是TCP节点的状态是唯一共享的), 然而系统调用允许 shutdown 调用来关闭其中的一个通道,使TCP 成为 一个半双工。我们使用 shut_rdwr, SHUT_WR 来调用 shutdown 时候,TCP 连接将开始上面的关闭步骤。本地的 TCP节点 迁移到FIN_WAIT1 状态, 然后进入 FIN_WAIT2 状态,对端的进入到 CLOSE_WAIT 状态, 如果参数为 SHUT_WR 那么 套接字依然合法(合法的定义是? 某种符合条件的状态? FIN_WAIT1, FIN_WAIT2 正好处于 接受对端的ACK 报文, 而没有收到对方FIN 的状态,即 自己主动关闭成功,对方并未关闭),读端依然是打开的,因为对端的写入操作依然可以进行。这里 SHUT_RD 在TCP套接字上没有实际意义的原因是因为, 大多数TCP协议的实现都没有为 SHUT_RD 提供所期望的行为。导致该参数调用的shutdown 并不具有可移植性
    5. TIME_WAIT 状态: 执行主动关闭的TCP 端在该状态下, 等待 2MSL时间,然后迁移到closed 状态。 这样的设计目的有两点:(该状态应该只存在于 主动关闭的 TCP 节点上,主要原因在于, 主动关闭的节点 需要对上一条 别动关闭的节点 的FIN报文响应 ACK报文,并组织新的连接的创建 来 保证TCP 可靠性连接的建立)
      • 实现可靠的链接终止:等待2被的MSL, 这里的MSL 是TCP报文最大生存时间
      • 让老的重复的数据报文段在网络中过期失效:TCP协议采用的重传算法,可能会产生重复的报文,根据路由器选择,这些重复的报文可能会在连接已经终止后到达, 而之后使用同样的IP端口重新建立的连接, 然后接收到的数据报 在这种情况下, 为了保证上一次连接中老的重复报文不会重复的出现在新的连接中被当成合法报文接收。当有TCP节点处于TIME_WAIT 状态时 是无法通过 该节点 创建新的连接的。这样就组织了新链接的建立。(在论坛上会看到相关的 如何关闭 TIME_WAIT 状态,因为重新启动的服务器进程会尝试将套接字绑定到处于 TIME_WAIT 状态的地址上时候,会出现 EADDRINUSE 的错误,尽管有办法关闭 TIME_WAIT状态 但是还是应该避免这样做, 因为会阻碍 TCP 提供可靠性保证)
    6. netstat(监视套接字)
      netstat 程序可以显示系统中Internet 和 UNIX域套接字的状态, 是一个非常好的调试工具, 大多数的Unix 都会提供一个版本的 netstat, 但是各个实现中的命令行参数语法有很大的区别
      其中展示的信息中的字段含义列表有:更多的细节需要查阅netstat用户手册, /proc/net中有多个专属Linux 的文件,例如tcp, udp, 的呢个,获取更多细节,参考 proc

      字段名称 含义
      Proto 套接字所使用的协议
      Recv-Q 套接字接收缓冲区中还未被本地应用读取的字节数(UDP 中该字段不仅仅包含数据还包含头部信息等)
      Send-Q 套接字发送缓冲区中排队等待发送的字节数(同Recv-Q 一样 UDP)
      Local Address 套接字绑定到的地址, 主机IP:端口号 展现形式
      Foreign Address 对端套接字锁绑定到的地址, *:* 表示没有对端地址
      State 当前套接字所处的状态
    7. tcpdump: 有用的调试工具,可以让超级用户监视网络中的 实时流量,实时生成文本信息, 可以显示所有类型的TCP/IP 数据报流量。显示方式如下(具体的使用细节需要google):
      src > dst: flags data-seqno ack window urg
      • src, dst: 源 IP 地址和端口, 目的 IP 地址 和端口号
      • flags: TCP 报文控制位 信息, 他们是 S(SYN) , F(FIN), P(PSH), R(RST), E(ECE), C(CWR) 中的标记位组合
      • data-seqno: 数据报中的序列号范围
      • ack: ack num,对端 期望的下一个方法字节的序列号
      • window: 对端缓冲区大小(存在接收、写入缓冲区两种)
      • urg: urg num 该报文 在指定的偏移量上包含紧急数据
      • options: 任意的TCP选项
    8. 套接字选项:
      • int getsockopt(int sockfd, int level, int optname, void * optval, socklen_t optlen); int setsockopt(int sockfd, int level, int optname, const void * optval, socklen_t optlen) 两个系统调用获取 设定 套接字选项
      • SO_REUSEADDR 套接字选项: 该选项主要应用在 服务器bind 地址时候,出现EADDRINUSE 错误(即存在TCP节点处于 TIME_WAIT 状态)出现该现象的情况有:
        1. 之前连接到 客户端的服务器 被close, 或者是 崩溃 而执行了一个主动关闭,这就使得 TCP节点处于 TIME_WAIT状态, 直到 2倍的MSL超时 过期为止
        2. 服务器创建一个子进程来处理客户端的请求, 稍后,服务器终止, 而子进程继续服务客户端, 因为使得 TCP 节点占用了服务器的 端口号
          针对以上情况,默认的TCP实现会阻止 新的监听套接字的绑定, (客户端不常出现这样的错误, 因为他们一般使用的是不会处在 TIME_WAIT 的临时端口号(新建的连接会选择新的端口号),但是如果客户端指定的保定到一个端口上那么还是会遇到这个问题)
        3. (类比并不严谨)accept 需要识别出 一个新来的套接字,系统内 记录的大概内容是 [ {local-ip-Address, local-port, Foreign-ip-address, Foreign-port }] 每个 已连接的套接字对应的对端的一个套接字,当有链接对应的接入时,通过对端的 TCP 报文 中对应的 ip port 可以识别出对应的 本地的 已经接套接字,如果没有对应的连接套接字存在 则 accpet 创建新的 连接套接字。 TCP 规范要求这个4 元祖是唯一的,问题在于大多数的 实现 都强制限制了 一个更为严格的约束,如果本地存在 可以匹配到本地端口 TCP连接, 则本地端口不能被重用 (即 不能bind 该 端口号)也不能在接受新的链接(即不能accept) 启用该socket选项可以放开这个限制,即便是 情况 2 我们依然可以绑定到该地址,大多数的TCP 服务器应该启用这个选项。
    9. UDP 相对TCP 的优势有: 1)UDP 服务器可以从多个客户端接受数据报,而不必为每个 客户端 创建和终止连接 2) 对于简单的请求: UDP的速度更快, 因为UDP不需要建立 和终止连接. DNS 是一个 应用UDP的绝好例子。
    10. 传递 文件描述符: 通过sendmsg, recvmsg 系统调用,我们可以在同一台主机 上通过UNIX 域套接字 将文件描述符 辅助数据 从一个进程传递到 另一个进程中。 这种方式可以传递任意类型的文件描述符(open 得到的文件描述符, 套接字)这个可以应用在服务器的并发模型中, 主进程可以在TCP监听套接字上接收客户端连接,然后将返回的文件描述符传递给进程池中的一个,之后子进程就可以响应客户端的请求了(虽然这种技术通常称为 传递文件描述符, 但实际上 进程间 传递的是对同一个打开文件描述符的引用。在接收端进程中使用的文件描述符一般和发送进程中文件描述符不同)

其他 的IO模型: IO多路复用, 信号驱动IO, Linux专有 epoll

大部分的程序使用的IO 模型都是单个进程 在多个文件上执行阻塞IO,例如 在管道上调用read,如果管道 中没有数据,那么read会阻塞到 直到管道中有数据,才会继续执行后续的工作。(磁盘文件是一个特例, 对磁盘的write 会立即返回,而不是等到将数据写入到磁盘文件上之后才返回。对应的read 如果数据不在缓冲区内,内核会休眠该进程 (内核 缓存页 调度)然后再 继续read)

问题列表

  1. DNS 负载 如何实现?
  2. 信号 相关的设定 是如何的? 为什么会出现 如此复杂的设计,race conditoin,
  3. epoll 中 一小段不能交换的内存是?新概念吗?
  4. epoll 的内部实现
  5. 消息边界的概念,以及实际意义