- 第一章 简介
- 第二章 传输层:TCP、UDP和SCTP
- 第三章 基本套接字编程
- 第四章 基本TCP套接字编程
- 第五章 TCP客户/服务器程序示例
- 第六章 I/O复用:select和poll函数
- 第七章 套接字选项
- 第八章 基本UDP套接字编程
- 第九章 基本SCTP套接字编程
- 第十章 SCTP客户/服务器程序例子
- 第十一章 名字与地址转换
- 第十二章 IPv4和IPv6的互操作性
- 第十三章 守护进程和inetd超级服务器
- 第十四章 高级I/O函数
- 第十五章 Unix域协议
《UNIX网络编程卷一》读书笔记
第一章 简介
- 概述
- 确定总是由客户端发起请求能够简化协议和程序,但是较复杂的网络引用还需要异步回调,也就是由服务器向客户端发起请求
- 一些名词
- TCP/IP协议族又称为网际协议族
- TCP(Transmission Control Protocol):传输控制协议
- IP(Internet Protocol):网际协议
- UDP(User Datagram Protocol):用户数据报协议
- LAN(Local Area Network):局域网
- WAN(Wide Area Network):广域网
- 当今最大的广域网是因特网(Internet),许多公司也构建自己的广域网,这些私用的广域网可以不连接到因特网
- 一个简单的时间获取客户程序
- 使用字节流协议在接收数据时需要保证数据的完整性,TCP协议是字节流协议
- PDU(Protocol Data Unit)是计算机网络对等实体间交换的单位信息,称为协议数据单元
- 应用层的PDU称为应用数据(application data)
- 传输层的PDU称为消息(message),对于TCP特称为分节(segment),消息或分节的长度是有限制的
- 对于TCP,发送端把来自应用层的字节流数据按顺序经分割后封装在各个分节中,每个分节的数据可能来自发送端一次或多次的输出操作,因此没有边界
- 对于UDP,发送端把来自应用层的单个记录封装在UDP消息中,如果单个记录过长可以选择拒绝发送
- 对于SCTP,引入了块的数据单元,把来自应用层的记录流数据按照流内的顺序和边界记录在各DATA块中,一个记录通常对应一个DATA块
- 网络层的PDU称为IP数据报(IP datagram),其长度有限
- 发送端直接把来自传输层的消息整个封装在PDU中传送
- IP数据报由IP首部和传输层数据组成(SDU)
- 对于无法放在一帧中的数据报,对其SDU进行分片(fragmentation),再在每个片段加上新的IP首部
- 分片操作可能发生在源端和传送途中,而其逆操作重组(reassembly)一般只发生在目的端
- TCP/IP协议族为了提高效率会尽量避免分片和重组,IPv6禁止在中途分片
- 链路层的PDU称为帧(frame)
- 最后由链路层处理完的数据称为包(packet)
- 本层的PDU是通过下层的SDU承载的,当本层的PDU长度超过下层的SDU的长度时,本层就要事先将数据划分成适当的片段让下层发送
- TCP本身并不提供记录结束的标志,需要自己实现
- 使用特殊的字节序列作为结束标志
- 在发送的记录前放置一个二进制的计数值,作为记录的长度
- 使用字节流协议在接收数据时需要保证数据的完整性,TCP协议是字节流协议
- 协议无关性
- 可以使用getaddrinfo实现协议无关
- 错误处理:包裹函数
- 例如为socket函数定义Socket函数,在其中判断是否出错,打印错误消息等等
- 只要一个Unix函数出错,errno就会被置为一个正数的错误值(0表示无错误),函数通常返回-1
- 一个简单的时间获取服务器程序
- 将服务器的地址bind为
INADDR_ANY
就可以在任意网络接口上监听客户端的连接 - accept返回的文件描述符是已连接描述符,用来与连接的客户端进行通信
- 应该使用snprintf代替sprintf避免缓冲区溢出攻击,类似的还有fgets、strncat、strncpy,strlcat和strlcpy的引入是为了确保字符串正确终止
- 实现的时间服务器称为迭代服务器(iterative server), 如果有多个请求到达,那么内核会按照listen函数指定的值依次进行服务;能够同时处理多个请求的服务器类型为并发服务器(concurrent server)
- 将服务器的地址bind为
- OSI模型(open systems interconnection, 开放系统互联)
OSI模型 TCP/IP模型 +----------+ +-------------+ |应用层 | | | +----------+ | | |表示层 | | 应用层 | +----------+ | | |会话层 | | | +----------+----socket is here-----+----+---+----+ |传输层 | |TCP | | UDP| +----------+ +----+---+----+ |网络层 | | IPv4,IPv6 | +----------+ +-------------+ |数据链路层| |设备驱动程序 | +----------+ |和硬件 | |物理层 | | | +----------+ +-------------+
- BSD网络支持历史
- 套接字起源于BSD操作系统
- 许多Unix系统的网络支持是从BSD的代码中发展而来的,这些实现称为Berkeley-derived implementation
- Linux不属于Berkeley系列,因此其网络支持代码和套接字API都是从头开始开发的
- 测试用网络及主机
- 使用nestat和ifconfig查看网络配置
- 通过ping广播地址找出本地网络的所有主机IP
- Unix标准
- 同一个Unix标准有多种名称,例如SUSv3和ISO/IEC 9945:2002都是同一个Unix标准
- POSIX(Portable Operating System Interface, 可移植操作系统接口)是由IEEE开发的一系列标准,被ISO(国际标准化组织)和IEC(国际电工委员会)采纳为国际标准
- 因此现在每一个IEEE标准都有一个ISO/IEC标准与其对应
- POSIX.1指的是POSIX标准中的第一部分系统API,POSIX.2类似
- SUS(Single Unix Specification, 单一Unix规范)是由The Open Group(开放组织)制定的
- 在POSIX和The Open Group的努力下,如今大多数Unix系统都符合POSIX.1和POSIX.2的某个版本,不少系统符合SUSv3
- 多数Unix系统源自Berkeley或者System V,虽然标准使它们的差别逐渐缩小,但是在系统管理方面仍然存在较大差别
- 64位体系结构
- 32位Unix共同的编程模型为ILP32(int,long,pointer都为32位);64位Unix流行的编程模型为LP64(long,pointer为64位)
第二章 传输层:TCP、UDP和SCTP
- 概述
- UDP是一个简单的、不可靠的数据报协议
- TCP是一个复杂的、可靠的字节流协议
- SCTP是一个可靠的传输协议,还提供消息边界、传输级别多宿支持以及将头端阻塞减少到最小的一种方法
- 总图
- TCP/IP协议概况
- IPv4(Internet Protocol version 4):网际协议版本4;使用32位地址,为TCP、UDP、SCTP、ICMP和IGMP提供分组递送服务
- IPv6(Internet Protocol version 6):网际协议版本6;使用128位地址,为TCP、UDP、SCTP、ICMPv6提供分组递送服务
- TCP(Transmission Control Protocol):传输控制协议;TCP是面向连接的协议,为用户提供可靠的全双工字节流;TCP套接字是一种流套接字;既可以使用IPv4也可以使用IPv6
- UDP(User Datagram Protocol):用户数据报协议;UDP是无连接的协议,为用户提供不可靠的数据报服务,不能保证到达目的地;既可以使用IPv4也可以使用IPv6
- SCTP(Stream Control Transmission Protocol):流控制传输协议;SCTP是一个提供可靠全双工关联的面向连接的协议,同时维护来自应用层的记录边界;既可以使用IPv4也可以使用IPv6
- ICMP(Internet Control Message Protocol):网际控制消息协议;ICMP处理在路由器和主机之间流通的错误和控制消息
- IGMP(Internet Group Management Protocol):网际组管理协议;IGMP用于多播,在IPv4中是可选的
- ARP(Address Resolution Protocol):地址解析协议;ARP将一个IPv4地址解析为硬件地址,在点到点的网络不需要
- RARP(Reverse Address Resolution Protocol):反向地址解析协议;RARP将硬件地址反向解析为IPv4地址
- ICMPv6(Internet Control Message version 6):网际控制消息协议版本6;ICMPv6综合了ICMPv4、IGMP和ARP的功能
- BPF(BSD packet filter):BSD分组过滤器;提供对于数据链路层的访问能力,通常在Berkeley内核中找到
- DLPI(Datalink provider interface):数据链路提供者接口;提供对于数据链路层的访问能力,通常在SVR4内核中找到
- TCP/IP协议概况
- 用户数据报协议(UDP)
- 应用程序往UDP套接字中写入消息,该消息被封装在UDP数据报中,UDP数据报又进而被封装进IP数据报,然后发往目的地
- UDP缺乏可靠性
- 不保证数据报到达目的地
- 不保证数据报到达目的地的顺序
- 不保证数据报只到达一次
- 每个UDP数据报都有一个长度,如果一个UDP数据报正确地到达目的地,该数据报的长度随数据一同传递给接收端应用进程
- UDP提供无连接(connectionless)服务,客户端可以使用同一个套接字给多个服务器发送数据报,服务器也可以使用同一个套接字接收多个客户端的数据报
- 传输控制协议(TCP)
- TCP提供客户端与服务器之间的连接(connection);客户端先与服务器建立连接,通过该连接与服务器交换数据,然后终止该连接
- TCP提供可靠性(reliability);TCP要求对端发送确认,如果没有接收到确认,TCP就自动重传并等待更长的时间接受确认,在数次失败之后,TCP才会放弃,因此TCP不保证数据一定会被对方接收
- TCP含有动态估算客户和服务器之间往返时间(round-trip time, RTT)的算法,以便知道等待一个确认需要的时间,RTT受网络流通的影响,因此TCP持续估算这一值
- TCP通过给发送的每个字节关联一个序列号对所发送的数据进行排序(sequencing),也可用于重复数据的判断
- TCP提供流量控制(flow control);TCP总是告知对端在任何时刻它一次能够从对端接收多少字节的数据,这称为通告窗口(advertised window);UDP不提供流量控制
- TCP是全双工的(full-duplex),建立一个全双工连接后可以将其转换为一个单工连接;UDP也可以是全双工的
- 流控制传输协议(SCTP)
- SCTP在客户端和服务器之间提供关联,因为一个连接只设计两个IP地址之间的通信,SCTP支持多宿而涉及不止两个地址
- SCTP提供TCP提供的可靠性、排序、流量控制以及全双工数据传输
- SCTP是面向消息的(message-oriented),提供各个消息按序递送服务,与UDP类似,将消息的长度随数据发送给接收端
- SCTP在所连接的端点之间提供多个流,一个流上消息的丢失不会阻塞其他流消息的投递
- SCTP提供多宿特性,使得单个端点能够支持多个IP地址
- TCP连接的建立和终止
- 三次握手(three-way handshake)
- 过程
- 服务器必须准备好接受外来的连接,通常通过调用socket、bind和listen三个函数来完成,称之为被动打开(passive open)
- 客户端通过调用connect发起主动打开(active open);这导致客户端发送SYN分节,告诉服务器客户端的初始序列号,通常SYN分节不携带数据
- 服务器ACK客户端的SYN分节,同时自己也发送一个SYN分节,包含服务器发送数据的初始序列号;服务器在单个分节中发送ACK和SYN
- 客户端确认服务器的SYN
- 因为SYN占用一个序列号,因此ACK就是SYN对应的序列号加一
- 过程
- TCP选项
- MSS选项:发送SYN的TCP端通过这一选项告诉对端它的最大分节大小(max segment size),发送端TCP使用接收端的MSS值作为所发送分节的最大大小
- 窗口规模选项:指定TCP首部中的通告窗口必须扩大的位数(0~14),能够提供的最大窗口为65535x2^14;此选项需要两个端系统的支持
- 时间戳选项:在高速网络中防止失而复现的分组对数据造成损坏,因为在高速网络中序列号很快会重新循环
- 高带宽和长延迟的网络被称为“长胖管道”,后两个选项也被称为“长胖管道选项”
- TCP连接终止
- 过程
- 某个应用进程调用close,我们称该端为主动关闭(active close);该端TCP发送一个FIN分节表示数据发送完毕
- 接收到这个FIN的对端执行被动关闭(passive close);由TCP确认FIN,它的接收也作为一个文件结束符传递给接收端应用进程(存放在接受数据的最后)
- 一段时间后,接收到文件结束符的应用进程调用close关闭套接字,这导致TCP发送一个FIN
- 接收到这个最终FIN的原发送端TCP确认这个FIN
- 终止一个TCP连接通常需要4个分节,有时候可以把步骤一的FIN随数据一起发送,有时执行被动关闭的一端把ACK和FIN一起发送
- 类似与SYN,FIN也占用一个序列号
- 半关闭(half-close):从被动关闭一端到主动关闭一端的数据流是有可能的
- 当进程终止时,Unix内核会关闭其所打开的所有文件描述符,因此会在仍然打开的TCP连接上发送FIN
- TCP状态转移图
- 见图2-4
- TCP的11种状态在用netstat调试客户端/服务器应用时是非常有用的
- 观察分组
- 捎带确认(piggybacking)通常在服务器处理请求并产生应答的时间少于200ms时发生,如果服务器耗用更长时间,将先确认后应答
- 执行主动关闭的一端进入
TIME_WAIT
状态 - 如果仅仅是发送一个分节,使用TCP会有八个分节的开销,使用UDP则只需要交换两个分组,但是使用UDP会丢失TCP的可靠性,尽管如此,如果需要交换的数据量较少,许多网络应用使用UDP来避免TCP建立连接和终止时带来的开销
- 三次握手(three-way handshake)
TIME_WAIT状态
TIME_WAIT
状态的持续时间是最长分节生命期(maximum segment lifetime, MSL)的两倍,有时称之为2MSL- MSL是任何IP数据报能够在因特网中存活的最长时间,因为每个数据报都含有一个称为跳限(hop limit)的8位字段(我们假设最大跳限255的分组在网络中的存在时间不会超过MSL秒)
- 存在的理由
- 可靠地实现TCP全双工连接的终止
- 执行主动关闭的一端必须要确保ACK的发送
- 允许老的重复分节在网络中消逝
- 关闭连接后,在相同的IP和端口建立另一个连接,后一个连接称为前一个连接的化身(incarnation)
- TCP必须防止来自某个连接的老的重复分组在该连接已经终止后再现从而被误解成分组是属于化身的,因此TCP将不给处于
TIME_WAIT
状态的连接发起新的化身 - 源自Berkeley的实现有例外情况,允许给处于
TIME_WAIT
状态的连接启动化身
- SCTP关联的建立和终止
- 此节先跳过
- 端口号
- TCP、UDP和SCTP都使用16位整数的端口号(port number)来区分不同的进程
- IANA(the Internet Assigned Numbers Authority, 因特网已分配数值权威机构)维护着一个端口号分配状况清单
- 众所周知的端口(well-known port)为0~1023,由IANA分配和控制,如果可能将相同的端口号分配给TCP、UDP和SCTP的同一给定服务
- 已登记的端口(registered port)为1024~49151,不受IANA的控制,但是由其登记并提供它们的使用情况
- 动态的(dynamic)或私有的(private)端口为49152~65535,IANA不管这些端口
- Unix系统的保留端口为0~1023,这些端口只能由特权用户的套接字使用;因此所有使用IANA well-known port的服务器都必须以超级用户特权启动
- 有少数客户端需要绑定保留端口用于客户端/服务器认证,调用库函数rresvport创建一个TCP套接字,该函数按照1023~513的顺序赋予一个未使用的端口
- 标识每个端点的IP地址和端口号称为一个套接字
- 表示一个连接的四元组(本地IP地址,本地TCP端口号,目的IP地址,目的TCP端口号)称为一个套接字对
- TCP端口号与并发服务器
- 监听套接字(listening socket)是服务器用来监听客户端连接的,形式为
*:*
,表示接受来自任意外地地址、任意外地端口的连接 - 已连接套接字(connected socket)是并发服务器的子进程用来与客户端通信的,标识了客户端的IP地址和端口
- TCP必须通过查看套接字对的所有4个元素才能确定由哪个进程接收某个到达的分节
- 监听套接字(listening socket)是服务器用来监听客户端连接的,形式为
- 缓冲区大小及限制
- IPv4数据报的最大大小为65535bytes,包括首部,这是因为表示其长度的字段占据16字节
- IPv6数据报的最大大小为(65535+40)bytes,比IPv4多了40字节的首部
- 许多网络有一个硬件规定的MTU(maximum transmmission unit, 最大传输单元)
- 两个主机之间路径的最小MTU称为路径MTU(path MTU),路径MTU可以是不对称的
- 如果一个IP数据报的大小超过相应链路的MTU,IPv4和IPv6都将进行分片操作(fragmentation),在到达目的地时才重组(reassembling)
- IPv4主机对其产生的数据报执行分片,IPv4路由器对其转发的数据报执行分片
- IPv6主机对其产生的数据报执行分片,IPv6路由器对其转发的数据报不执行分片,但是IPv6路由器对其产生的数据报还是会执行分片操作
- 某些通常用作路由器的防火墙可能会重组分组
- 如果IPv4的DF位(don’t fragment)被设置,那么无论是发送数据的主机还是转发数据的路由器都不会进行分片
- IPv6隐含了DF位
- 如果分组长度超过MTU,那么产生一个ICMPv4报文,对于IPv6将产生ICMPv6报文
- 可以使用DF位来实现路径MTU发现,但是如今很多防火墙都丢弃了ICMP信息
- IPv4和IPv6都定义了最小重组缓冲区(minimum reassembly buffer size),它是IPv4或IPv6实现必须保证支持的最小数据报大小
- TCP的MSS用于向对端通告在每个分节中能发送的最大TCP数据量;MSS经常被设置成MTU减去IP和TCP首部的固定长度,以避免分片
- SCTP基于到对端所有地址发现的最小路径MTU保持一个分片点
- TCP输出
- 当某个应用进程调用write时,内核从应用缓冲区中复制所有数据到所写套接字的发送缓冲区,如果发送缓冲区容不下应用缓冲区所有的数据,对于阻塞的套接字应用进程将睡眠
- write调用成功返回时仅仅表示可以重用应用缓冲区,并不表明对端的TCP已接收到数据
- TCP必须为已发送的数据保留一个副本,直到它被对端确认才丢弃
- 本端TCP给每个数据块安装TCP首部以构成TCP分节,每个数据块不超过MSS(若对端没有发送MSS选项,那么MSS为最小重组缓冲区减去IPv4和TCP首部)
- IP给每个TCP分节安装IP首部以构成IP数据报,按照目的IP地址查找路由器表项以确定外出接口,IP可能在把数据报传递给数据链路层之前将其分片
- 每个数据链路都有一个输出队列,如果该队列已满,那么新到的分组将被丢弃,并沿着协议栈向上返回错误:从数据链路到IP,再从IP到TCP,TCP注意到这个错误,并在以后某个时刻重传相应的分节,应用进程并不知道这种做暂时的情况
- UDP输出
- UDP没有发送缓冲区,当数据被发送之后就被丢弃了,无需保存副本;但有发送缓冲区大小(通过
SO_SNDBUF
指定),发送缓冲区大小仅仅指定写到该套接字UDP数据报大小上限 - 本端UDP简单地给来自用户的数据加上8字节的UDP首部构成UDP数据报,然后传递给IP
- IP给UDP数据报加上IP首部构成IP数据报,执行路由操作确定外出接口,如果数据报太大就进行分片,然后加入到数据链路层输出队列(相比TCP会有分片操作,UDP更有可能被IP分片)
- 从write调用返回表示所写的数据被加入到了链路层的输出队列,如果该队列没有空间存放该数据报或者它的某个片段,内核通常会返回ENOBUFS给它的应用进程(不幸的是有些UDP实现不返回错误,这样数据报未经发送就被丢弃的情况应用进程也不知道)
- UDP没有发送缓冲区,当数据被发送之后就被丢弃了,无需保存副本;但有发送缓冲区大小(通过
- SCTP输出
- 与TCP类似,SCTP也有发送缓冲区,内核从应用进程缓冲区复制数据到发送缓冲区,如果发送缓冲区容不下应用进程缓冲区的数据那么对于阻塞套接字应用进程将睡眠
- 从write调用返回仅仅表示可以重用应用缓冲区,并不表明对端已接收到数据
- 本端SCTP必须等待SACK,在累计确认点超过已发送数据后,才可以从发送缓冲区中删除该数据
- 标准因特网服务
- echo:回射,服务器返回客户端发送的数据
- discard:服务器丢弃客户端发送的数据
- daytime:服务器返回直观可读的日期和时间
- chargen:TCP服务器发送连续的字符流直到客户端终止连接;UDP服务器每个客户发送一个数据报就返回一个包含随机数量字符的数据报
- time:服务器返回一个32位二进制表示的时间,表示UTC时间
- 为了防止针对这些服务的拒绝服务攻击,如今系统禁止这些服务
- 常见因特网应用的协议使用
- DHCP:UDP
- SMTP, Telnet, SSH, FTP, HTTP:TCP
- DNS, NFS:UDP, TCP
- 小结
- UDP是一个简单、不可靠、无连接的协议;TCP是一个复杂、可靠、面向连接的协议;SCTP组合了这两个协议的一些特性
- 理解TCP的状态转换图是使用netstat命令诊断网络问题的基础,也是理解connect、accept和close函数过程的关键
TIME_WAIT
状态是为了防止四次挥手最终ACK丢失的情况,并允许老的重复分节从网络中消逝- SCTP使用四次握手建立关联,三次分组交换序列终止关联,使用验证标记而不不需要
TIME_WAIT
第三章 基本套接字编程
- 套接字地址结构
- 所有套接字地址结构的形式为
1sockaddr_xx
的形式,其中xx表明该地址结构对应的协议 - IPv4套接字地址结构
- 几点说明
- 不是所有的实现都支持
sin_len
字段,除非涉及路由套接字,否则无需设置和检查它 - POSIX规范只需要3个字段
sin_family
,sin_addr
和sin_port
,所有套接字地址结构至少为16字节 - 图3-2给出了POSIX规范要求的数据类型大小
- IPv4地址和TCP或UDP端口号在套接字地质结构中总是以网络字节序来存储
- 当绑定一个非通配的IPv4地址时,要求
sin_zero
字段置为0,按照惯例,总是在填写sockaddr前将整个结构置为0 - sockaddr结构仅在主机上使用,结构本身不会在主机之间传递
- 定义在头文件
<netinet/in.h>
中struct in_addr{ in_addr_t s_addr; //32位IPv4地址,网络字节序 }; struct sockaddr_in{ uint8_t sin_len; sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; char sin_zero[8]; }
- 通用套接字地址结构
- 因为套接字处理函数需要处理不同协议的地址结构,在ANSI C后有
void *
空指针解决这个问题,但是这些处理函数是在ANSI C之前定义的,因此需要将不同协议的地址结构强制转换为通用的地址结构struct sockaddr{ uint8_t sa_len; sa_family_t sa_family; char sa_data[14]; };
- 因为套接字处理函数需要处理不同协议的地址结构,在ANSI C后有
- IPv6套接字地址结构
- 几点说明
- 定义宏
SIN6_LEN
表明系统支持长度字段 - IPv6的地址簇为
AF_INET6
,而IPv4是AF_INET
- 结构体中的字段是做过编排的,使得
in6_addr
字段是64位对齐的 sin6_flowinfo
低20位为流标(flow label),高12位保留- 对于具备范围的地址(scoped address),
sin6_scope_id
标识其范围,最常见的是链路局部地址 - 定义在头文件
<netinet/in.h>
struct in6_addr{ uint8_t s6_addr[16]; }; #define SIN6_LEN struct sockaddr_in6{ uint8_t sin6_len; sa_family_t sin6_family; in_prot_t sin6_port; uin32_t sin6_flowinfo; struct in6_addr sin6_addr; uint32_t sin6_scope_id; };
- 新的通用套接字地址结构
- 相比于传统的sockaddr,新结构的优点
- 能够满足最苛刻的对齐要求
- 能够容纳系统支持的任何套接字地址结构,因为该结构足够大
- 定义在头文件
<netinet/in.h>
struct sockaddr_storage{ uint8_t ss_len; sa_family_t ss_family; /*根据具体实现添加成员*/ };
- 套接字地址结构的比较
struct sockaddr_in
和struct sockaddr_in6
固定长度
struct sockaddr_un
本身长度是一定的,但是其路径名的实际长度是可变的,BSD下最多104字节- 数据链路地址
struct sockaddr_dl
的长度是可变的
- 所有套接字地址结构的形式为
- 值-结果参数
- 在传递套接字地址结构时,地址结构长度的传递取决于该结构的传递方向
- 从进程到内核传递套接字地址结构的函数有bind、connect和sendto,地址结构长度传递给内核,内核才知道需要从进程复制多少数据
- 从内核到进程传递套接字地址结构的函数由accept、recvfrom、getsockname和getpeername,地址结构长度是一个值(value),告诉内核该结构的大小,这样内核在写该结构时不至于越界;当函数返回时,结构大小有一个结果(result),它告诉进程内核在该结构中究竟存储了多少信息,这种类型的参数称为值-结果(value-result)参数
- 使用值-结果参数的函数还有
- select函数中间的3个参数
- getsockopt函数的长度参数
- recvmsg函数
- ifconf结构
- sysctl函数
- 在传递套接字地址结构时,地址结构长度的传递取决于该结构的传递方向
- 字节排序函数
- 术语“大端”和“小端”指的是多字节数据的哪一端存储在该值的起始地址
- 某个给定系统的字节序称为主机字节序(host byte order);网络协议必须指定一个网络字节序(network byte order),网际协议使用大端字节序来传送多字节整数
- 本来具体实现可以隐藏字节序细节,但是由于历史原因和POSIX规范的规定,必须使用字节排序函数将主机字节序转换为网络字节序
uint16_t htons(uint16_t host16bitvalue)
uint32_t htonl(uint32_t host32bitvalue)
uint16_t ntohs(uint16_t net16bitvalue)
uint32_t ntohl(uint32_t net32bitvalue)
- 注意这些函数的s指的是short为16位;l指的是long为32位即使是在64位平台上
- 在与网际协议使用相同字节序的系统中这些函数被定义为空宏
- 大多数因特网标准使用octet(八位组)来定义8位量而不是byte(字节)
- 因特网对位序的规定:最左边的位是在电缆上最早出现的最高有效位
- 字节操纵函数
- 由两组字节操纵函数,它们不解释数据,也不假设数据是以空字符结尾的C字符串
- 以b开头的是第一组函数起源于BSD,如今几乎所有提供套接字的系统仍然提供它们
void bzero(void *dest, size_t nbytes)
void bcopy(const void *src, void *dest, size_t nbytes)
void bcmp(const void *ptr1, const void *ptr2, size_t nbytes)
- 若相等返回0,否则返回非0
- 以mem开头的第二组函数起源于ANSI C标准,支持ANSI C函数库的所有系统都提供它们
void *memset(void *dest, int c, size_t len)
void *memcpy(void *dest, const void *src, size_t nbytes)
- 如果dest和src的内存区域有重叠,应该使用memmove函数,bcopy可以正常处理这种情况
int memcpy(const void *ptr1, const void *ptr2, size_t nbytes)
- 返回值类似于strncmp
inet_aton
,inet_addr
和inet_ntoa
函数- 这三个函数只能作用于IPv4地址
inet_addr
不能够处理255.255.255.255
,因为该值作为函数的出错返回值,该函数已被废弃,代码应该使用inet_aton
inet_ntoa
使用静态存储来保存结果,因此不可重入,还要注意其参数是按值传递,而不是指针
inet_pton
和inet_ntop
函数- 这两个函数适用于IPv4和IPv6,p表示presentation,n表示numeric
int inet_pton(int family, const char *strptr, void *addrptr)
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len)
- family的取值只能为
AF_INET
,AF_INET6
- family的取值只能为
- len的取值:
INET_ADDRSTRLEN
为16,INET6_ADDRSTRLEN
为46
sock_ntop
和相关函数- 自己定义的,与具体协议无关,因为
inet_ntop
与协议相关
- 自己定义的,与具体协议无关,因为
- readn,writen和readline函数
- 自己实现的函数
- 对于文本行交互的应用来说,程序应该按照操作缓冲区而非按照操作文本行来编写
第四章 基本TCP套接字编程
- socket函数
int socket(int family, int type, int protocol)
- family称为协议族或协议域
- protocol为0表示使用family和type组合的系统默认值
- 对比
AF_XX
和PF_XX
- AF前缀表示地址族,PF前缀表示协议族,历史上曾想让单个协议族支持多个地址族,但从未实现过
- 理论上创建套接字时使用PF前缀的协议族,在套接字地址结构中使用AF前缀的地址族,在头文件中两者的值一般是相同的,因此对于family参数使用AF前缀的地址族即可
- connect函数
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
- 客户端调用connect前不必非得调用bind函数,内核会确定源IP地址,并选择一个临时端口作为源端口
- 以下几种情况会导致TCP套接字出错返回
- 客户端在超时时间内没有收到SYN分节响应
- 客户端收到RST响应,这是一种硬错误(hard error),产生RST的三个条件是
- 在目的端口上没有监听的服务器
- TCP想取消一个已有连接
- TCP接收到一个根本不存在连接上的分节
- 客户端发送的SYN分节引发了”destination unreachable”ICMP错误,这是一种软错误(soft error),一般内核会在超时时间内继续发送SYN分节
- 如果connect函数成功,将导致套接字从CLOSED状态转移到
SYN_SENT
状态,若成功则再转移到ESTABLISHED状态;否则该套接字不再可用,不能再次调用connect,必须关闭
- bind函数
- 把一个协议地址赋予一个套接字,对于网际网协议,协议地址是32位的IPv4或64位IPv6加上16位的TCP或UDP端口号的组合
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen)
- 调用bind函数时,可以指定一个端口号,或者一个IP地址,或者两者都指定,或者两者都不指定
- 如果一个TCP客户端或服务器没有绑定端口号,那么调用bind时内核会为它们选择一个临时端口;服务器是通过众所周知的端口的被认识的,因此不指定端口的情况比较少见
- 如果为TCP客户端套接字绑定了地址,该地址作为IP数据报的源地址;如果为服务器套接字绑定了地址,就限定该套接字只接受目的地为这个地址的客户端连接
- TCP客户端通常不绑定地址,当connect时,内核根据所用的外出网络接口来选择源地址;如果TCP服务器没有绑定地址,内核就把客户端发送的SYN目的地址作为服务器的源地址
- 如果指定端口号为0,那么内核在bind调用时选择一个临时端口;如果指定IP地址为通配地址,那么内核在等到套接字已连接(TCP)或在套接字上发出数据报(UDP)时才选择一个本地IP地址
- 通配地址
- IPv4:htonl(INADDR_ANY);头文件定义的
INADDR_
常值都是按照主机序定义的,所以应该使用htonl
- IPv4:htonl(INADDR_ANY);头文件定义的
- IPv6:
in6addr_any
- 如果让内核选择临时端口值,必须调用函数getsockname来返回协议地址
- 如果该地址处于
TIME_WAIT
状态,bind函数返回EADDRINUSE
- listen函数
int listen(int sockfd, int backlog)
- listen函数仅由TCP服务器调用,它做两件事情
- 当socket创建套接字时,它被假设为一个主动套接字,将来会用于调用connect函数;listen函数将其转换成一个被动套接字,指示内核应接受指向该套接字的连接请求;调用listen导致套接字状态从CLOSED转换到LISTEN状态
- backlog规定了内核应该为套接字排队的最大连接个数
- listen函数应该在调用socket、bind之后,accept之前调用
- 为了理解backlog参数,必须认识到内核为任何一个给定的监听套接字维护两个队列
- 未完成连接队列(incomplete connection queue):服务器收到客户端的SYN,正在等待完成三次握手,套接字处于
SYN_RCVD
状态
- 未完成连接队列(incomplete connection queue):服务器收到客户端的SYN,正在等待完成三次握手,套接字处于
- 已完成连接队列(completed connection queue):完成三次握手,套接字处于ESTABLISHED状态
- 当客户端的SYN分节到达,TCP在未完成连接队列中创建一个新项,当收到客户端的ACK分节时,该项从未完成连接队列中移到已完成连接队列队尾;当进程调用accept时,返回已完成队列的队头,如果此时队列为空,那么挂起进程,直到队列不为空
- 对于backlog,有几点需要考虑
- backlog曾被定义为两个队列总和的最大值,在本机Linux上定义为已完成连接队列的最大值
- 源自Berkeley实现给backlog增加了模糊因子
- 不要把backlog定义为0,不同实现有不同的解释,见图4-10
- 当一个客户SYN到达时,若这些队列是满的,TCP会忽略该分节不发送RST,这样客户TCP将重发SYN而不会导致connect立即出错
- 对于已完成连接队列中的套接字,在调用accept之前到达的数据由服务器TCP排队,最大数据量为相应已连接套接字的接受缓冲区大小
- accept函数
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen)
- 如果对客户身份不感兴趣,可以将后两个参数置为空
- addrlen为值-结果参数
- accept函数由服务器调用,用于从已完成连接队列头返回下一个已完成连接,如果已完成连接队列为空,进程被投入睡眠(假设套接字为阻塞的)
- 监听套接字(listening socket)和已连接套接字(connected socket)
- 一个服务器通常只拥有一个监听套接字,贯穿服务器的整个生命周期;内核为接受的客户连接创建已连接套接字,当服务器完成服务时,会关闭该套接字
- fork和exec函数
pid_t fork(void)
- 存放在硬盘上的可执行程序文件被Unix执行的唯一方法是:由一个现有的进程调用exec类函数,把当前进程映像替换成新的程序文件,新程序通常从main开始执行,进程ID不改变;我们称调用exec的进程为调用进程(calling process),新执行的程序为新程序(new program)
- exec函数
- 对于下图的exec类函数,上面三个函数都以0作为arg的结束,下面三个函数的argv中需要一个空指针作为结束
- 左列两个函数指定了filename,exec函数将使用环境变量将其转换为路径名,一旦filename中出现了/分隔符,就不再使用环境变量
- 左两列四个函数使用的是外部变量environ作为传递给新程序的环境列表;右列两个函数显示使用envp指定环境列表,其必须以一个空指针结束
- 调用exec函数后,除非指定文件描述符的
FD_CLOEXEC
标志,否则文件描述符保持打开状态
+-----------------------+ +------------------------+ +-------------------------------+
|execlp(file, arg,...,0)| |execl(path, arg, ..., 0)| |execle(path, arg, ..., 0, envp)|
+----------+------------+ +------------+-----------+ +----------------+--------------+
| 创建argv | 创建argv | 创建argv
| | |
v v v
+------------------+ 转换file +------------------+ 增加envp +------------------------+ 系统调用
|execvp(file, argv)+--------->|execvl(path, argv)+--------->|execve(path, argv, envp)+--------->
+------------------+ 为path +------------------+ +------------------------+
- 并发服务器
- 典型并发服务器程序轮廓
pid_t pid; int listenfd, connfd; listenfd=socket(...); bind(listenfd, ...); listen(lisenfd, ...); for(;;){ connfd=accept(listenfd, ...); if( (pid=fork())==0){ close(listenfd); doit(connfd); close(connfd); exit(0); } close(connfd); //一定要在父进程中关闭,否则会导致连接无法释放 }
- close一个套接字会减少该套接字的引用计数,当引用计数减到为0时会发送一个FIN分节终止连接
- 典型并发服务器程序轮廓
- close函数
int close(int sockfd)
- close函数把sockfd标记为已关闭然后马上返回,之后sockfd不能用于read和write
- 当关闭一个TCP套接字时,TCP将尝试发送已排队等候发送到对端的数据,发送完毕后发生的是正常的TCP连接终止序列;可以通过套接字选项来改变这种默认行为
- close受描述符引用计数的影响,但是调用shutdown会立即发送FIN分节,不受引用计数的影响
- getsockname和getpeername函数
int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen)
- 返回与套接字关联的本地协议地址,对于IP协议就是IP地址和端口号
- POSIX允许对未绑定的套接字调用该函数
int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen)
- 返回与套接字关联的外地协议地址,对于IP协议就是IP地址和端口号
- 这两个函数的addrlen参数都是值-结果类型的参数
- 小结
- 大多数TCP服务器是并发的,它们为每个连接fork一个子进程处理
- 大多数UDP服务器是迭代的
第五章 TCP客户/服务器程序示例
- TCP回射服务器和客户端
- 正常启动
- 正常终止
- POSIX信号处理
- 信号就是告知某个进程发送了某个事件的通知,有时也称为软件中断(software interrupt)
- 每个信号都有与之关联的处置(disposition),也称为行为(action),通过调用sigaction来设定一个信号的处置,有三种选择
- 可以提供信号处理函数(signal handler),这种行为叫做捕获(catching)信号;SIGKILL和SIGSTOP不能捕获
- 可以把某个信号处置设定为
SIG_IGN
来忽略它;SIGKILL和SIGSTOP不能忽略 - 可以把某个信号处置设定为
SIG_DFL
来启动默认处置 - POSIX信号的语义
- 一旦安装了信号处理函数,它就一直安装着(较早期的系统是每执行一次就将其拆除)
- 在一个信号处理函数运行期间,该信号处理函数对应的信号是阻塞的;sigaction中的
sa_mask
指定的信号也被阻塞 - 如果一个信号在被阻塞期间产生了多次,那么在解除阻塞后通常只提交一次,也就是Unix默认对信号不排队,但POSIX实时标准定义了一些排队的可靠信号
- 可以使用sigprocmask函数可以选择性地阻塞或解阻塞信号
- 处理SIGCHLD信号
- 设置僵死(zombie)进程的目的是维护子进程的信息,以便父进程在某个时候获取,这些信息一般包括进程ID、终止状态以及资源信息
- 当父进程终止时,处于僵死状态的子进程的父进程会被重置为init进程,init进程将调用wait函数清理它们
- 当阻塞于慢速系统调用(slow system call)的一个进程捕获某个信号而且当信号处理处理函数返回时,该系统调用可能返回EINTR错误,必须设置好
SA_RESTART
标志;但是对于某些源自Berkeley的实现从不自动重启select,其中有些从不重启accept和recvfrom - 重启大多数系统调用是合适的,但是当connect返回EINTR时,不能再调用它,否则立即返回一个错误,必须调用select来等待连接完成
- wait和waitpid函数
pid_t wait(int *wstatus)
pid_t waitpid(pid_t pid, int *wstatus, int options)
- 返回值为已终止子进程的ID号,通过宏来检查wstatus
- wait函数会阻塞到一个子进程终止,waitpid提供了更多的控制
- 在网络编程中可能会遇到三种情况
- 当fork子进程时,必须捕获SIGCHLD信号
- 当捕获信号时,必须处理被中断的系统调用
- 在SIGCHLD的信号处理函数中应该使用waitpid避免留下僵死进程
- accept返回前连接中止
- 描述:在三次握手完成之后,父进程调用accept之前,客户端发送RST终止连接
- 服务器只需要再次调用accept即可
- 源自Berkeley的实现完全在内核中处理,accept不会返回任何错误
- 大多数SVR4实现accept返回EPROTO,但是POSIX规定应该返回ECONNABORTED,因为前者通常作为致命错误
- 服务器进程终止
- 客户端不应该阻塞在某个特定输入源上,而应该使用select等函数阻塞在所有的源上,这样才能够及时响应所有的输入源
- 客户端向服务器写数据时会导致服务器TCP响应RST,这个RST导致连接中断,使得客户端不发送最后两个分节,服务器也不经历
TIME_WAIT
状态
- SIGPIPE信号
- 写一个接收了RST的套接字会引发SIGPIPE信号,同时write会返回EPIPE错误;写一个接收了FIN的套接字是可以接受的
- 服务器主机崩溃
- 服务器主机崩溃指的是服务器已有的网络连接上不发出任何东西
- 客户端TCP持续重传数据分节,试图从服务器上接收ACK;当客户端TCP终于放弃的时候,给客户端进程返回一个错误
- 如果服务器已崩溃,从而对客户端的数据根本没有响应,那么返回ETIMEDOUT
- 如果某个中间路由器判断服务器不可达,从而响应一个”destination unreachable”的ICMP消息,那么返回EHOSTUNREACH
- 使用
SO_KEEPALIVE
的套接字选项,使得不需要向服务器发送数据也能检测出它是否崩溃
- 服务器主机崩溃后重启
- 服务器因为处于崩溃状态没能发出FIN,导致客户端的套接字仍然处于
ESTABLISHED
状态,当服务器重启后连接信息丢失,对客户端的请求响应RST
- 服务器因为处于崩溃状态没能发出FIN,导致客户端的套接字仍然处于
- 服务器主机关机
- Unix系统关机时,init进程给所有进程发送SIGTERM信号,等待一段固定的时间给需要的进程执行清理工作,然后发送SIGKILL杀死所有进程,进程的终止将会关闭所有的打开的描述符,对于套接字描述符将发送FIN分节断开连接
- TCP程序例子小结
- 数据格式
- 在客户端和服务器之间传递二进制结构
- 存在的问题
- 不同实现以不同形式存储二进制数,比如大端和小端
- 不同实现存储相同的C数据类型可能存在差异,比如32位和64位上的long类型
- 不同实现对结构打包的方式存在差异
- 存在的问题
- 解决方法
- 把所有数据作为文本传递,需要客户端和服务器具有相同的字符集
- 显式定义所支持数据类型的二进制格式(字节序,位数),并以这样的格式在客户端与服务器之间传递所有的数据
- 在客户端和服务器之间传递二进制结构
第六章 I/O复用:select和poll函数
- 概述
- I/O复用(I/O multiplexing):内核一旦发现进程指定的一个或多个I/O条件就绪,就通知进程
- I/O复用典型地使用在下列场合
- 当客户端处理多个描述符时(通常是交互式输入和网络套接字),必须使用I/O复用
- 客户端同时处理多个套接字,不过比较少见
- TCP服务器既要监听套接字,又要处理已连接套接字
- 服务器既要处理TCP,又要处理UDP
- 服务器既要处理多个服务或者多个协议
- I/O复用不仅仅局限于网络编程,许多重要的应用程序也需要使用这项技术
- I/O模型
- 一个输入操作通常包括两个阶段
- 等待数据准备好
- 从内核向进程复制数据
- 阻塞式I/O(blocking I/O)模型
- 所有套接字默认都是阻塞的
- blocking I/O系统调用直到数据报到达且被复制到应用进程缓冲区或者发生错误才返回,最常见的错误是被信号中断
- 非阻塞式I/O(nonblocking I/O)模型
- 当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误
- 应用进程往往通过轮询内核查看非阻塞I/O是否完成,这样做往往会浪费大量CPU时间
- I/O复用(I/O multiplexing)模型
- 阻塞于select/poll调用,等待多个描述符可用
- 与此模型相关的是在多线程中使用阻塞式I/O
- 信号驱动式I/O(signal-driven I/O)模型
- 开启套接字的信号驱动式I/O功能,安装SIGIO的信号处理函数;当数据报准备好时,内核就为该进程产生一个SIGIO信号;可以在信号处理函数中读取数据也可以通知主进程读取数据
- 其优点在于等待数据报到达时可以不阻塞
- 异步I/O(asynchronous I/O)模型
- 告知内核启动某个操作,并在该操作完成时通知我们(包括将数据从内核复制到用户空间缓冲区)
- 信号驱动式I/O模型告知我们何时启动I/O操作,而异步I/O模型告知我们I/O操作何时完成
- 各种I/O模型的比较
- 见图6-6
- 同步I/O和异步I/O对比
- POSIX定义
- 同步I/O(synchronous I/O operation):导致请求进程阻塞,直到I/O操作完成
- 包括阻塞式I/O模型、非阻塞式I/O模型、I/O复用和信号驱动式I/O都属于同步I/O
- 异步I/O(asynchronous I/O operation):不导致请求进程阻塞
- 异步I/O模型
- 一个输入操作通常包括两个阶段
- select函数
int select(int nfds, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout)
- 源自Berkeley的内核绝不重启被信号中断select调用,出于可移植性考虑,需要做好返回EINTR错误的准备
- timeout
- 尽管timeval结构支持微妙级的分辨率,但是内核支持的真实分辨率粗糙的多,因此定时时间不是精确的
- POSIX规定返回时timeout不会被修改,但是Linux会修改该值,可移植性的做法是在调用select前后获取系统时间,但是要注意系统时间可能被管理员修改
- 目前exceptset支持的异常条件只有两个
- 某个套接字的带外数据到达
- 某个置为分组模式的伪终端主设备发现从设备状态改变
- 头文件
<sys/select.h>
定义的FD_SETSIZE
是nfds的最大值 - readset,writeset和exceptset都是值结果类型,因此每次调用都得重新设置这三个条件
- 一些相关的宏
void FD_ZERO(fd_set *fdset)
void FD_SET(int fd, fd_set *fdset)
void FD_CLR(int fd, fd_set *fdset)
int FD_ISSET(int fd, fd_set *fdset)
- 可以使用赋值语句复制
fd_set
类型 - 如果三个测试条件都为空,那么select函数可以作为一个精度比sleep还高的睡眠函数,poll也提供了此功能
- 描述符就绪条件
- 满足下列四个条件之一时,套接字描述符可读
- 该套接字的接收缓冲区中的数据字节数大于等于接收缓冲区低水平标记;可以使用
SO_RCVLOWAT
套接字选项设置接收缓冲区低水平标记;对于TCP和UDP默认值为1 - 该连接是读半部关闭的,也就是接收了FIN的套接字;对这样的套接字读操作返回0
- 该套接字为监听套接字且已完成连接数不为0,对这样的套接字的accept操作不会阻塞
- 其上有一个套接字错误待处理,对这样的套接字的读操作将不阻塞并返回-1;这些待处理的错误(pending error)可以通过指定
SO_ERROR
套接字选项调用getsockopt获取并清除
- 满足下列四个条件之一时,套接字描述符可写
- 套接字发送缓冲区的可用空间大于等于发送缓冲区低水平标记;可以使用
SO_SNDLOWAT
套接字选项设置发送缓冲区低水平标记;对于TCP和UDP默认值为2048- 当指定写入的字节数大于发送缓冲区的可用空间时,只有使用非阻塞式套接字才可能不阻塞,对于阻塞式套接字会阻塞到指定的字节数完全写入为止
- 该连接是写半部关闭的,对这样的套接字写操作将产生SIGPIPE信号
- 使用非阻塞式connect的套接字已建立连接,或者connect已经以失败告终
- 其上有一个套接字错误待处理,对这样的套接字的写操作将不阻塞并返回-1;可以通过getsockopt指定
SO_ERROR
选项获取并清除错误
- 套接字发送缓冲区的可用空间大于等于发送缓冲区低水平标记;可以使用
- 如果一个套接字存在带外数据或者仍处于带外标记,那么它就有异常条件待处理
- 注意当套接字上发送错误时即可读又可写
- 接收低水平标记和发送低水平标记的目的在于允许应用程序控制select返回之前有多少数据可读或者有多大的空间可写
- 任何UDP套接字只要其发送低水平标记小于等于发送缓冲区大小(注意不是可用空间),那么UDP套接字总是可写的
- select的最大描述符数
- 单纯修改头文件
FD_SETSIZE
的值来提高最大描述符数通常行不通,因为涉及到操作fd_set
的宏
- 单纯修改头文件
- 为了使用超过
FD_SETSIZE
(通常为1024)的描述符数,使用poll
- 批量输入
- 客户端从标准输入批量读取数据发往服务器,当客户端的标准输入遇到EOF时,表示客户端没有数据发送给服务器,但是这时不能退出客户端,因为客户端的请求可能还在去服务器的路上,或者服务器的应答可能在返回客户端的路上;这时候正确的操作使用shutdown函数关闭写半部,给服务器发送FIN分节告诉它客户端已经完成了数据发送但是仍保持套接字打开以便读取服务器的响应
- 一般来说,为提升性能而引入的缓冲机制增加了网络编程的复杂性
- select只是从read系统调用的角度指出是否还有数据可读,而不是检查stdio的缓冲区是否还有数据;混合使用select和stdio极容易犯错误,必须极其小心
- 同样的问题不止存在在stdio中,如果我们自己使用了缓冲区也会有这样的问题
- shutdown函数
int shutdown(int sockfd, int howto)
SHUT_RD
:关闭连接的读这一半,套接字不再有数据可接收,而且套接字接收缓冲区中现有的数据都被丢弃,之后收到的数据由TCP确认然后悄然丢弃
SHUT_WR
:关闭连接的写这一半,对于TCP套接字称为半关闭(half-close);当前留在套接字发送缓冲区的数据将被发送掉,后跟TCP的正常连接终止序列SHUT_RDWR
:关闭连接的读半部和写半部,与用SHUT_RD
、SHUT_WR
调用两次shutdown等效- shutdown函数可以避免close函数的两个限制
- shutdown不管引用计数就激发TCP的四次挥手断开连接
- shutdown可以关闭一个方向上的TCP连接
- 拒绝服务型攻击
- 当一个服务器在处理多个客户时,它绝对不能阻塞于只与单个客户相关的某个函数调用,否则可能导致服务器被挂起,拒绝为其他客户提供服务
- 解决方法
- 使用非阻塞式I/O
- 每个客户由单独的线程提供服务
- 对I/O操作设置一个超时
- pselect函数
int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask)
- timeout的类型为struct timespec,相比于select中的timeout(struct timeval),其精度可以到纳秒
- sigmask设置当前线程信号屏蔽字,函数返回后恢复原来的信号屏蔽字,可以防止select错过信号的通知 ``` sigset_t newmask, oldmask, zeromask; sig_emptyset(&newmask); sig_emptyset(&zeromask); sig_addset(&newmask, SIGINT);
sigprocmask(SIG_BLOCK, &newmask, &oldmask); if(intr_flag) handle_intr(); if(pselect(…, &zeromask)<0){ if(errno=EINTR){ if(intr_flag) handle_intr(); } } ```
- poll函数
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout)
- fdarray是指向struct poll的数组,nfds指明了数组的长度
struct pollfd{ int fd; //如果不关心某个fd,可以将fd设置为0,poll函数将忽略它,返回时将revents设置为0 short events; //测试的条件 short revents; //返回描述符的状态,events和revents的取值见图6\-23 };
- fdarray是指向struct poll的数组,nfds指明了数组的长度
- timeout
- <0:永远等待
- 0:立即返回
-
0:等待指定的毫秒数,同select一样,超时值受限于系统的分辨率
- poll识别三类数据:普通(normal),优先级带(priority band)和高优先级(high priority),这些术语均出自基于流的实现
- 以下条件引起poll返回特定的revents
- 所有正规的TCP数据和UDP数据都被认为是普通数据
- TCP的带外数据被认为是优先级带数据
- 当TCP连接的读半部关闭时(比如收到对端的FIN分节)被认为是普通数据,随后读操作返回0
- TCP连接存在错误既可认为是普通数据,也可认为是错误(POLLERR),无论哪种情况读操作返回-1;可以用于处理接收到RST或者发生超时等情况
- 在监听套接字上有新的连接既可认为是普通数据也可认为是优先级数据,但大多数实现视之为普通数据
- 非阻塞式connect的完成被认为是相应套接字可写
- poll没有类似
FD_SETSIZE
的限制,因为poll不需要类似fd_set
固定大小的数据类型 - 从可移植性的角度考虑,支持select的系统比poll的多
第七章 套接字选项
- getsockopt和setsockopt函数
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)
- sockfd必须指向打开的套接字描述符
- optlen对于getsockopt是值结果参数,对于setsockopt是值参数
- 套接字选项汇总见图7-1,图7-2
- 检查选项是否受支持并获取默认值
- 套接字状态
- 对于某些套接字选项,什么时候设置或获取有时序上的考虑
- TCP连接套接字的某些选项是从监听套接字继承来的,因为accept一直要到TCP完成三次握手后才会给服务器返回连接套接字进行设置,如果要在三次握手完成时确保这些套接字选项中的某一个是给已连接套接字设置的,那么我们必须先给监听套接字设置该选项
- 具体受影响的套接字选项见书P156
- 通用套接字选项(与具体协议无关,但是有些选项只能适用于特定的套接字类型)
SOCK_BROADCAST
:开启或禁止进程发送广播消息的能力- 只有UDP套接字支持该选项,并且还必须在支持广播消息的网络上才能发送成功
- 应用进程在发送广播数据报之前必须设置本选项
SOCK_DEBUG
- 只有TCP套接字支持该选项
- 开启该选项后,内核保留了详细的跟踪消息
SO_DONTROUTE
- 开启该选项将绕开底层协议的正常路由机制,强制从特定接口送出
SO_ERROR
- 通过getsockopt获取该套接字上的待处理错误(pending error)
- select将设置可读或可写条件
- 对于信号驱动式I/O将触发SIGIO信号
- 当进程调用read时,首先读取套接字上在排队等待读取的数据,在没有数据返回并且套接字出错时read返回-1并且设置errno
- 当进程调用write时,如果套接字出错,write返回-1并且设置errno
- 对于上述情况,一个套接字上的待处理错误一旦返回给用户进程,它的
so_error
就被复位为0 SO_KEEPALIVE
- 设置该选项后,如果2小时内该套接字任一方向上都没有数据交换,TCP就自动给对端发送保持存活探测分节(keep-alive probe),这是一个对端必须响应的TCP分节,可能导致下列三种情况
- 对端以期望的ACK响应,一切正常
- 对端以RST响应,该套接字的待处理错误设置为ECONNRESET,关闭套接字
- 对端没有任何响应,本端重发,如果超时,该套接字的待处理错误被设置为ETIMEOUT,关闭套接字;如果接收到一个ICMP错误,则待处理错误设置为相应的错误
- 本选项的功用是检测对端主机是否崩溃或不可达,修改发送时间参数将影响所有的套接字,因此有些服务器在应用层提供了分钟量级的超时检测
- 服务器可以利用此选项来检测客户的半开连接(half-open connection)
- 检测对端TCP各种条件的方法见图7-6
- 本选项对于监听套接字不起作用
SO_LINGER
- close的默认行为是立即返回,如果有数据残留在套接字发送缓冲区中,系统将尝试把这些数据发送给对端
- 本选项指定close函数对面向连接协议的行为,需要在用户进程和内核间传递如下结构
struct linger{ int l_onoff; //开关 int l_linger; //控制延时时间,POSIX规定单位为秒 };
- 如果
l_onoff
为0则关闭本选项,否则开启本选项 l_linge
r的取值- 0:close将丢弃发送缓冲区所有的数据,给对端发送RST分节终止连接,而不是通常的四次挥手,避免了
TIME_WAIT
状态,但是也带来旧分节的问题 - 正值:close将延时一段时间,直到数据都发送完且均被对方确认(此时close成功返回),或者延时时间到(此时close返回EWOULDBLOCK)
- 0:close将丢弃发送缓冲区所有的数据,给对端发送RST分节终止连接,而不是通常的四次挥手,避免了
- 设置
SO_LINGER
后,close的成功返回只是表示先前发送的数据和FIN已被对端TCP确认,并不意味着对端应用进程已读取数据,但是如果不设置SO_LINGER
,就连对端TCP是否确认了数据都不知道,见图7-7,图7-8 - 为了知道对端已经读取数据
- 可以使用shutdown来关闭连接,之后使用read来获取对端的FIN,见图7-10
- 使用应用ACK,对端进程收到数据后发送约定的字节通知本端其已接收到数据
- 总结在关闭本端的连接时,有3个不同的返回时机
- close立即返回
- close延时到接收了对本端FIN的ACK才返回
- 后跟一个read调用的shutdown等到接收了对端的FIN才返回
- shutdown和
SO_LINGER
各种情况总结见图7-12 SO_OOBINLINE
- 开启此选项带外数据将被留在正常的输入队列中,接收函数的
MSG_OOB
标志不能用来读带外数据
- 开启此选项带外数据将被留在正常的输入队列中,接收函数的
SO_RCVBUF
,SO_SNDBUF
- TCP套接字接收缓冲区不可能溢出,因为TCP具有流量控制;然而较快的UDP发送端可以淹没较慢的接收端
- 这两个选项允许我们改变缓冲区的默认大小
- TCP窗口规模受套接字接收缓冲区可用空间的影响,是在建立连接时通过SYN分节交换得到的,因此
- 客户端应该在调用connect之前设置接收缓冲区的大小
- 服务器应该在accept之前给监听套接字设置接收缓冲区的大小,因为新创建的连接套接字的缓冲区大小总是继承自监听套接字
- TCP套接字的缓冲区大小至少应该是MSS的四倍,还应该是MSS值的偶数倍
- 套接字缓冲区应该大于带宽延迟积(bandwidth-delay product),这样才有可能将管道填满
- 带宽延迟积=带宽(bit/s)xRTT(s),带宽为两个端点之间最慢链路的值,RTT可以通过ping测得
SO_RCVLOWAT
,SO_SNDLOWAT
- 接收低水平标记是让select返回可读时套接字接收缓冲区中所需的数据量,对于TCP、UDP和SCTP其默认值为1
- 发送低水平标记是让select返回可写时套接字发送缓冲区中所需的可用空间;UDP没有发送缓冲区,只有发送缓冲区大小,因此只要发送缓冲区大小大于等于发送低水平标记UDP就总是可写的
SO_RCVTIMEO
,SO_SNDTIMEO
- 这两个选项设置套接字接收和发送的超时值(使用struct timeval表示,可以通过将timeval的两个成员都设置为0来禁止超时),默认情况下这两个超时都是禁止的
- 接收超时影响的函数
- read, readv, recv, recvfrom, recvmsg
- 发送超时影响的函数
- write, writev, send, sendto, sendmsg
SO_REUSEADDR
,SO_REUSEPORT
(重用地址成功的前提是能够构成唯一的四元组)SO_REUSEADDR
起到四个不同的作用- 允许启动一个监听服务器并绑定其众所周知的端口,即使以前建立的将该端口作为它们本地端口的连接仍存在
- 所有TCP服务器都应该指定
SO_REUSEADDR
,以允许服务器重新启动
- 所有TCP服务器都应该指定
- 允许在同一端口上启动同一服务器的多个实例,只要每个实例绑定一个不同的本地IP地址即可
- 通配地址的意思是没有更好匹配的任何地址
- 对于TCP不可能绑定IP地址和端口号都相同的服务器,但是客户端可以绑定相同的IP地址和端口号,见习题7.4
- 有些系统出于安全考虑不允许对已经绑定了通配地址的端口再绑定任何“更为明确的”地址
- 允许单个进程绑定同一个端口到多个套接字上,只要每次绑定不同的本地IP地址即可
- 如果传输协议支持,允许IP地址和端口号都相同的完全重复绑定,一般来说只有UDP支持
- 如果UDP数据报的目的地址为广播地址或多播地址,那就给每个匹配的数据报递送一个副本;如果是单播地址,那么选择哪个套接字来接收取决于实现
SO_REUSEPORT
- 本选项允许完全重复的绑定,不过只有在绑定同一个IP地址和端口的每个套接字都指定了本套接字选项才行
- 如果被绑定的是多播地址,那么与
SO_REUSEADDR
等效 - 并非所有系统都支持此选项,在不支持此选项的系统上使用
SO_REUSEADDR
- 总结
- 在所有TCP服务器程序中,在调用bind之前设置
SO_REUSEADDR
套接字选项 - 当编写一个可在同一时刻在同一主机上运行多次的多播应用程序时,设置
SO_REUSEADDR
套接字选项,并将多播组地址作为本地IP地址绑定 SO_REUSEADDR
潜在安全问题是当绑定的端口不是保留端口时,容易被盗用
- 在所有TCP服务器程序中,在调用bind之前设置
SO_TYPE
- 返回套接字的类型,返回的整数值类似于
SOCK_STREAM
等,本选项主要继承了套接字的进程使用
- 返回套接字的类型,返回的整数值类似于
SO_USELOOPBACK
- 本选项开启时,套接字接收在其上发送的任何数据报的一个副本,路由域(AF_ROUTE)套接字默认开启此选项
- 禁止环回副本的另一个方法是以参数
SHUT_RD
调用shutdown函数
- IPv4套接字选项
IP_HDRINCL
:如果本选项是为原始IP套接字设置的,那么必须为该原始套接字上发送的数据报构造自己的IP首部;默认情况下内核会为原始IP套接字构造首部IP_OPTIONS
:设置该选项允许在IPv4首部中设置IP选项IP_RECVDSTADDR
:设置本选项导致所收到的UDP数据报的目的IP地址由recvmsg函数作为辅助数据返回IP_RECVIF
:设置本选项导致所收到的UDP数据报的接收接口索引由recvmsg函数作为辅助数据返回IP_TOS
:设置该选项允许在IP首部中的设置服务类型字段IP_TTL
:设置或获取从某个给定套接字发送的单播分组上的默认TTL
- ICMPv6套接字选项
ICMP6_FILTER
:获取或设置icmp6_filter
结构,该结构指出哪些ICMPv6消息可以通过套接字传递给所在进程
- IPv6套接字选项
IPV6_CHECKSUM
IPV6_DONTFRAG
IPV6_NEXTHOP
IPV6_PATHMTU
IPV6_RECVDSTOPTS
IPV6_RECVHOPLIMIT
IPV6_RECVHOPOPTS
IPV6_RECVPATHMTU
IPV6_RECVPKTINFO
IPV6_RECVRTHDR
IPV6_RECVTCLASS
IPV6_UNICAST_HOPS
IPV6_USE_MIN_MTU
IPV6_V6ONLY
IPV6_XXX
- TCP套接字选项
TCP_MAXSEG
:本选项用于获取和设置TCP连接的最大分节大小(MSS),一旦建立连接,本选项的值就是对端通告的MSS选项值- 如果在TCP连接中使用时间戳,那么实际用于连接的最大分节大小可能小于本套接字选项的返回值
- 如果TCP支持路径MTU发现功能,那么当到对端的路径发生改变时,每个分节的最大数据量可能会变化
- 并非所有的系统都支持修改该值,某些系统也只能降低该值
TCP_NODELAY
:开启本选项将禁止Nagle算法,默认情况下该算法是启动的- Nagle算法的目的在于减少广域网上小分组的数目:如果某个给定连接上有待确认的数据,那么该连接上立即发送小分组的行为就不会发生,直到现有数据被确认为止
- Nagle算法常常与ACK延滞算法(delayed ACK algorithm)联合使用,该算法使得TCP在收到数据后不立即发送ACK,而是等待一小段时间,期待这段时间内产生数据并捎带确认
- Nagle算法和ACK延滞算法会给某些应用带来延时,例如以若干小片数据向服务器发送单个逻辑请求的客户端
- SCTP套接字选项
- 暂时跳过
- fcntl函数
- fcntl,ioctl和路由器套接字操作小结见图7-20
- 为网络编程提供的特性
- 非阻塞套接字:使用
F_SETFL
设置O_NONBLOCK
- 非阻塞套接字:使用
- 信号驱动式I/O:使用
F_SETFL
设置O_ASYNC
,一旦套接字状态发送变化就产生SIGIO信号 F_SETOWN
允许我们指定用于接收SIGIO和SIGURG信号的套接字属主- SIGIO是由信号驱动式I/O产生的
- SIGURG是由接收到带外数据产生的
- 如果传递的参数值为正值表示的是进程ID,如果为负值表示进程组ID的绝对值
- 属主为进程组将导致整个进程组的所有进程接收到信号
F_GETOWN
返回当前套接字的属主- 通过sockect创建的套接字没有属主
- 通过accept创建的套接字的属主继承于监听套接字
- 使用fcntl设置某个文件状态的正确方法是首先获得该文件的文件状态,或上想要设置的文件状态然后再设置,一种常见的错误是为了设置一个文件状态而清空了其他的状态
第八章 基本UDP套接字编程
- recvfrom和sendto函数
ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags, struct sockaddr *from, socklen_t *addrlen)
ssize_t sendto(int sockfd, const void *buff, size_t nbytes, int flags, const struct sockaddr *to, socklen_t addrlen)
- 可以发送长度为0的数据,这意味着recvfrom函数的返回值为0是可以接受的
- recvfrom和sendto可以用于TCP,但通常没有这个必要
- UDP回射服务器程序
- 大多数TCP服务器是并发的,大多数的UDP服务器是迭代的(iterative server)
- 接收的数据报存放在UDP套接字的接收缓冲区中,按照FIFO的方式返回给进程,隐含了排队的发生
- UDP回射客户端程序
- 如果客户端没有调用bind绑定地址,类似与TCP的connect,内核会在调用sendto时给客户端选择临时端口
- 数据报的丢失
- 对于客户端而言,如果发送给服务器的数据报丢失或者服务器发回的ACK丢失,都会导致客户端一直阻塞在recvfrom
- 即使给客户端的recvfrom设置超时也不是一个完整的解决方法,因为无法判断是哪一种情况引起的
- 验证接收到的响应
- 内核在返回套接字地址结构时(例如recvfrom),长度字段(不是所有的套接字实现都有长度字段)是被设置的,因此在比较时要注意设置长度字段(因为一般不使用套接字的长度字段)
- 对于强端系统模型(strong end system model),只接受目的地址与到达接口地址一致的数据报
- 对于弱端系统模型(weak end system model),能够接受目的地址为本机任一IP地址的数据报,而不管到达的接口
- 如果服务器没有绑定实际的IP地址,当服务器发送应答时,内核路由会选择外出接口的主IP地址作为应答的源地址,因此客户通过recvfrom验证接收到的响应来自服务器时,获得的服务器IP地址可能与发送时的服务器IP地址不同,有以下的解决方法:
- 通过DNS验证服务器的主机名是否一致
- UDP服务器为服务器主机上的每个IP地址都创建套接字,然后使用select查看这些套接字是否有请求到达,只有当请求的目的地址为套接字绑定的地址时,请求才会投递给该套接字
- 服务器进程未运行
- 对于一个UDP套接字,由它引发的异步错误总不返回给它,除非它已连接(调用connect)
- 异步错误:引发错误的调用成功返回,错误直到后来才返回
- UDP程序例子小结
- 如果客户端没有显式bind地址,那么当调用sendto时由内核分配IP地址和端口,端口在后续不会发生改变,IP地址会随着路由选择的外出接口IP变化
- 如果客户端显式bind地址,那么UDP数据报中源IP和端口号都不会发生变化,无视路由选择的外出接口IP
- 通过设置套接字选项
IP_RECVDSTADDR
然后调用recvmsg可以获得UDP数据报的目的地址 - UDP服务器也可接收目的地址为主机广播地址的或多播地址的数据报
- 服务器从到达的IP数据报中获取信息的方法见图8-13
- UDP的connect函数
- 对于UDP套接字调用connect不会导致三次握手序列,内核只是简单地检查一下地址是否可达,然后记录对端的IP地址和端口号
- 已连接UDP套接字(connected UDP socket)和未连接UDP套接字(unconnected UDP socket)的对比,发生了三个变化
- 不能给输出操作指定目的IP地址和端口号;写到已连接套接字的任何内容都自动发送到connect指定的协议地址
- 不必使用recvfrom来获取数据报的发送者;内核为输入操作返回的数据报只有那些来自connect所指定地址的数据报
- 由已连接套接字引发的异步错误会返回给它们所在的进程,而未连接套接字不会接收到任何异步错误
- UDP客户进程和服务器进程只在使用自己的UDP套接字与确定的唯一对端进行通信时,才可以调用connect
- 给一个UDP套接字多次调用connect
- 指定新的IP地址和端口号
- 不像TCP套接字,connect函数只能够调用一次
- 断开已连接套接字上的连接
- 清零一个地址结构,然后把
sin_family
成员设置为AF_UNSPEC
,传递给connect
- 清零一个地址结构,然后把
- 性能
- 当对一个未连接的UDP套接字调用sendto时,源自Berkeley的内核会暂时连接该套接字,发送数据报,然后断开连接
- 当需要向同一个目的地址发送多个数据报时,显式连接套接字效率更高,因为内核只会复制一次套接字地址结构
- 如果UDP连接是非法的,connect函数实际并不做什么,只有当发送数据时才会收到对端主机发送的ICMP错误信息;对于TCP而言,调用connect函数会导致发送三次握手的第一个分节,导致对端主机响应RST分节
- UDP缺乏流量控制
- 一个高速的发送者很容易淹没低速的接受者,但是在内网中,UDP还是不会轻易丢包的
- UDP套接字接收缓冲区的大小决定了可以排队的UDP数据报数目,超过缓冲区的数据报被丢弃
- UDP外出接口的确定
- 对一个UDP套接字使用connect时,内核会为其设置本地地址(通过目的IP地址查找路由表得到外出接口,将外出接口的主IP地址作为本地地址),并绑定一个临时端口
- 可以通过对套接字使用getsockname获取外出接口的主IP地址
- 这项技术不是对所有实现都有效
- 使用select函数的TCP和UDP回射服务器程序
- TCP端口独立于UDP端口
第九章 基本SCTP套接字编程
- 跳过
第十章 SCTP客户/服务器程序例子
- 跳过
第十一章 名字与地址转换
- 域名系统(Domain Name System, DNS):主要用于主机名字和IP地址之间的映射
- DNS中的条目称为资源记录(resource record)
- A记录把一个主机名映射为一个32位IPv4地址
- AAAA记录把一个主机名映射为一个128位IPv6地址
- PTR记录把IP地址映射为主机名
- MX记录把一个主机指定作为给定主机的“邮件交换器”
- CNAME代表”canonical name”(规范名字),常见的用法是为常用的服务指派CNAME记录
- 解析器(resolver)和名字服务器(name server)
- 应用程序通过调用解析器函数库中的函数(gethostbyname,gethostbyaddr)接触DNS服务器
- 解析器函数库中的代码通过读取系统相关配置文件(/etc/resolv.conf)确定本地name server的地址
- 解析器使用UDP向本地name server发出查询,如果本地name server不知道答案,解析器就会使用UDP向整个因特网查询其他的名字服务器;如果答案太长超过了UDP的承载能力,自动切换到TCP
- resolver可以看成DNS的客户端;nameserver可以看成DNS的服务器
- DNS替代方法
- 静态主机文件(/etc/hosts)
- 网络信息系统(NIS)
- 轻量级目录访问协议(LDAP)
- DNS中的条目称为资源记录(resource record)
- gethostbyname
- 在新的程序中应该使用getaddrinfo
- gethostbyaddr
- 在新的程序中应该使用getnameinfo
- getservbyname和getservbyport
- 服务名到端口号的映射关系保存在文件/etc/services
struct servent *getservbyname(const char *servname, const char *protoname)
- 如果servname对应的服务支持多种协议,但是protoname没有指定,那么具体返回哪种类型的服务由实现决定;一般来说支持多个协议的服务往往使用相同的端口号
- 在servent结构中的端口号是以网络字节序表示的,把它存放到套接字地址结构时不能调用htons
struct servent *getservbyport(int port, const char *protoname)
- port要求使用网络字节序
- getaddrinfo
int getaddrinfo(const char *hostname, const char *service, const struct addrinfo *hints, struct addrinfo **result)
- hostname可以为主机名或者地址串
- service可以为服务名或者端口号
- 在hints中填入关于期望返回的信息类型的暗示,hints中可以设置的成员
ai_flags
:零个或多个值ai_family
:某个值ai_socktype
:某个值ai_protocol
:某个值
- 相比于gethostbyname,gethostbyaddr函数,该函数支持IPv4和IPv6
- 当有多个地址结构返回时,不能够假定地址结构的返回顺序,比如TCP服务比UDP服务先返回
- 如果在hints中设置了
AI_CANONNAME
标志,那么返回的第一个addrinfo结构的ai_canonname
成员指向所查找主机的规范名字,规范名字通常就是QFDN - 对于返回的addrinfo结构,如果
ai_family
和ai_socktype
组合能够确定唯一的协议,那么ai_protocol
的值可以为0 - getaddrinfo常见的输入
- 指定hostname和service
- 典型的服务器进程只指定service而不指定hostname,同时在hints结构中指定
AI_PASSIVE
标志,返回的地址结构中含有INADDR_ANY
和IN6ADDR_ANY_INIT
的地址 - 服务器的另一种设计是使用select和poll处理多个套接字,服务器将遍历getaddrinfo返回的整个addrinfo链表,并为每个结构创建一个套接字
gai_strerror
const char *gai_strerror(int error)
- 返回error对应的出错消息指针
freeaddrinfo
void freeaddrinfo(struct addrinfo *ai)
- ai应该指向getaddrinfo函数返回的第一个结构
- 由getaddrinfo函数返回的addrinfo结构都是动态分配的
- 当需要保存地址结构信息时,需要注意浅复制(shallow copy)和深复制(deep copy)的问题
getaddrinfo
函数:IPv6- 如果调用者指定
AF_UNSPEC
,那么getaddrinfo返回的是既适合用于指定主机和服务名且适合任一协议族的地址;如果某个主机既有AAAA记录又有A记录,那么不会将IPv4地址映射为IPv6地址返回 - 如果设置了
AI_PASSIVE
标志但是没有指定主机名,那么将会返回IPv6和IPv4的统配地址,并且首先返回IPv6地址 - hints结构中的
ai_family
和ai_flags
指定的AI_V4MAPPED
,AI_ALL
标志决定了在DNS中查找的资源记录类型 - 主机名参数可以是IPv6的冒分十六进制数串或IPv4的点分十进制数串;如果指定了
AF_INET
就不能够使用十六进制数串,AF_INET6
就不能够使用十进制数串,它们将作为主机名查找;直接使用inet_pton
返回地址结构,不会查找DNS - 具体getaddrinfo行为和结果的汇总见图11-8
- 如果调用者指定
host_serv
tcp_connect
tcp_listen
udp_client
udp_connect
udp_server
- getnameinfo
int getnameinfo(const struct sockaddr *sockaddr, socklen_t addrlen, char *host, socklen_t hostlen, char *serv, socklen_t servlne, int flags)
- 如果不想获得host,将hostlen置为0;如果不想获得serv,将servlen置为0
- flags
- 当知道要处理的是数据报套接字时,应该设置
NI_DGRAM
标志,因为在套接字地址结构中给出的仅仅是IP地址和端口号 - 如果无法使用DNS反向解析出主机名,标志
NI_NAMEREQD
将导致返回一个错误 NI_NOQFDN
标志将导致返回的主机名第一个点号的内容被截去NI_NUMERICHOST
,NI_NUMERICSERV
告知getnameinfo不要去调用DNS,而是直接将数据表达格式以字符串返回IP地址和端口号(可能调用inet_ntop
实现);服务器通常应该设置NI_NUMERICSERV
- 当知道要处理的是数据报套接字时,应该设置
- 可重入(re-entrant)函数
- gethostbyname,gethostbyaddr,getservbyname,getservbyport都是不可重入的函数,因为它们使用静态结构,支持线程的一些实现提供了这些函数的可重入版本,以
_r
结尾 inet_pton
,inet_ntop
总是可重入的inet_ntoa
是不可重入的,不过支持线程的实现提供了可重入版本getaddrinfo
可重入的前提是由它调用的函数都是可重入的,它应该调用可重入版本的gethostbyname和getservbynamegetnameinfo
可重入的前提是由它调用的函数都是可重入的,它应该调用可重入版本的gethostbyaddr和getservbyport- errno和标准I/O函数同样存在可重入问题,对于可重入函数的进一步讨论见第26章
- gethostbyname,gethostbyaddr,getservbyname,getservbyport都是不可重入的函数,因为它们使用静态结构,支持线程的一些实现提供了这些函数的可重入版本,以
gethostbyname_r
,gethostbyaddr_r
- 将不可重入函数改为可重入函数的方法
- 将原先的静态结构改为由调用者分配内存空间
- 由可重入函数调用malloc动态分配内存空间
- 这两个函数都不是POSIX标准的,取决于不同平台的实现
- 将不可重入函数改为可重入函数的方法
- 作废的IPv6地址解析函数
RES_USE_INET6
常值gethostbyname2
函数getipnodebyname
函数
- 其他网络相关信息
- 应用进程可能想要查找四类与网络相关的信息:主机,网络,协议和服务
- 每类信息存放在一个文件中,各自定义有三个访问函数
- getXXXent读取文件中的下一个表项,必要的话首先打开文件
- setXXXent打开并回绕文件
- endXXXent关闭文件
- 每类信息都定义了各自的结构,包括hostent,netent,protoent和servent,通过头文件
<netdb.h>
提供 - 每类信息还提供了键值查找函数getXXXbyXXX,比如gethostbyname
- 只有主机和网络可以通过DNS获取,协议和服务信息总是从相应的文件中读取,系统管理员可以指定是使用DNS还是使用文件来查找主机和网络信息
- 如果使用DNS查找主机和网络信息,那么只有键值查找函数才有意义;比如如果调用gethostent函数,那么它仅仅读取/etc/hosts文件并避免访问DNS
- 虽然可以通过DNS访问到网络信息,但是很少有人那么做;典型的做法是系统管理员创建并维护一个/etc/networks文件,网络信息通过它而不是通过DNS获取,无类寻址使得这些函数几乎无用,而且这些函数不支持IPv6,因此新的网络应用程序应该避免使用网络名字
信息 | 数据文件 | 结构 | 键值查找函数 |
---|---|---|---|
主机 | /etc/hosts | hostent | gethostbyaddr, gethostbyname |
网络 | /etc/networks | netent | getnetbyaddr, getnetbyname |
协议 | /etc/protocols | protoent | getprotobyname, getprotobynumber |
服务 | /etc/services | ervent | getservbyname, getservbyport |
第十二章 IPv4和IPv6的互操作性
- IPv4客户和IPv6服务器(IPv4地址映射为IPv6地址由内核完成)
- 每个帧的以太网首部的类型字段标识了IPv4帧和IPv6帧,接受数据链路通过查看该类型字段将每个帧传递给相应IP模块,IPv4模块结合其上的TCP模块检测到IPv4数据报的目的端口对应的是一个IPv6套接字,于是把数据报中的源地址的IPv4地址映射为IPv6地址,服务器accept拿到的是IPv6地址
- IPv4客户和IPv6服务器进行通信的步骤
- IPv6服务器创建一个IPv6的监听套接字,绑定通配地址
- 客户端获得服务器的IPv4地址后,调用connect发送IPv4 SYN分节到服务器
- 服务器响应IPv4的SYN/ACK,调用accept后返回IPv6地址
- 客户端和服务器之间的所有通信都是使用IPv4数据报
- 除非服务器显式检查这个IPv6地址是不是一个IPv4映射的IPv6地址,否则它永远不会知道自己在于一个IPv4客户通信,这个细节由双协议栈处理;同样IPv4客户也不知道自己在和IPv6服务器通信
- 双栈主机收到的IPv4数据报或IPv6数据报的处理流程
- 如果接收到一个目的地址为IPv4套接字的IPv4数据报,无需特殊处理
- 如果接收到一个目的地址为IPv6套接字的IPv6数据报,无需特殊处理
- 如果接收到一个目的地址为IPv6套接字的IPv4数据报,内核把源IPv4地址映射为IPv6地址后作为accept和recvfrom的返回地址
- 上一点的相反面是不成立的
- 双栈主机在处理监听套接字时的规则
- IPv4监听的套接字只能接受来自IPv4的连接
- 如果服务器由一个绑定了通配地址的IPv6套接字并且未设置
IPV6_V6ONLY
选项,那么该套接字可以接受来自IPv4和IPv6的连接,将IPv4的源地址映射为IPv6地址 - 如果服务器有一个IPv6地址但是绑定了非通配地址或者绑定通配地址但是设置了
IPV6_V6ONLY
选项,那么只能接受来自IPv6的连接
- IPv6客户与IPv4服务器(IPv4地址映射为IPv6地址由解析器完成)
- IPv6客户和IPv4服务器进行通信的步骤
- IPv4服务器创建一个IPv4的监听套接字
- IPv6客户端调用getaddrinfo查找IPv6地址(设置了
AI_V4MAPPED
标志),返回的是服务器IPv4地址映射为IPv6的地址 - IPv6客户端调用connect,内核检测到映射地址后自动发送IPv4 SYN到服务器
- 服务器响应IPv4 SYN/ACK,连接通过IPv4数据报建立
- 客户端通信处理流程
- 如果IPv4的客户指定IPv4地址调用connect或者sendto,无需特殊处理
- 如果IPv6客户指定IPv6地址调用connect或者sendto,无需特殊处理
- 如果IPv6客户指定IPv4地址映射的IPv6地址调用connect或者sendto,内核检测到映射地址后发送IPv4数据报
- IPv4客户不能指定IPv6地址
- IPv6客户和IPv4服务器进行通信的步骤
- 互操作性的总结见图12-5:IPv4双栈服务器只能接收IPv4数据报,能够发送IPv4和IPv6数据报;IPv6双栈服务器能够发送和接收IPv4和IPv6数据报
- IPv6地址测试宏
int IN6_IS_ADDR_V4MAPPED(const struct in6_addr *aptr)
宏测试IPv6地址是否由IPv4映射而来
- 源代码可移植性
- IPv4和IPv6之间的移植
第十三章 守护进程和inetd超级服务器
- 概述
- 守护进程(daemon)是在后台运行且不与任何控制终端关联的进程
- 守护进程有多种启动方法
- 由系统初始化脚本启动,这些脚本通常位于/etc/rc开头的某个目录中,这些进程拥有超级用户权限,例如inetd超级服务器,Web服务器,邮件服务器,syslog,cron
- 由inetd超级服务器启动,inetd监听请求,启动相应的实际服务器(Telnet,FTP)
- 由cron按照规则定期启动程序
- 由at命令指定在某个时刻启动程序
- 从用户终端前台或后台启动
- 守护进程没有控制终端,它们利用syslog函数将消息发送给syslogd
- syslogd守护进程
- syslog函数
void syslog(int priority, const char *message, ...)
- priority是level和facility的或,这么做能够配置同一设施的所有消息级别,或者统一配置相同级别的所有消息,level的默认值为
LOG_NOTICE
,facility默认值为LOG_USER
- message类似于printf的格式串,但是增加了%m,替换为当前errno对应的出错消息
- priority是level和facility的或,这么做能够配置同一设施的所有消息级别,或者统一配置相同级别的所有消息,level的默认值为
- 当syslog首次调用时,创建一个Unix域套接字,调用connect连接到syslogd的Unix域套接字的众所周知的路径名,这个套接字一直打开,直到进程终止为止,进程也可以调用openlog和closelog函数来打开和关闭连接
void openlog(const char *ident, int options, int facility)
- ident是一个由syslog冠于每个日志消息之前的字符串,它通常是程序名,注意ident指向的内存不能在栈上分配,要一直有效
- openlog并不立即创建套接字,而是syslog被调用时才创建,除非options制定
LOG_NDELAY
- facility指定设备的默认值,如果syslog没有指定设备,那么就使用这个默认值
void closelog(void)
- logger命名可用在shell脚本中想syslogd发送消息
daemon_init
函数- 当会话首进程终止时,会话中的所有进程都会收到SIGHUP信号
- 改变工作目录:如果守护进程在某个文件系统中启动,并且一直在运行,那么该文件系统就无法被卸载
- 守护进程不会收到来自内核的SIGINT,SIGHUP,SIGWINCH信号,这些信号可以安全地作为系统管理员的通知手段
- inetd守护进程
- 解决的问题
- 通过inetd处理普通守护进程的大部分启动细节简化了守护程序的编写
- 单个进程就能为多个服务等待外来的客户请求,以此取代每个服务一个进程的做法,减少了系统中进程总数
- 细节略过
- 解决的问题
daemon_inetd
函数
第十四章 高级I/O函数
- 套接字超时
- 在涉及套接字的I/O操作上设置超时的方法
- 使用alarm,缺点是可能干扰进程中现有的alarm调用
- 使用select自带的延时
- 使用
SO_RCVTIMEO
和SO_SNDTIMEO
套接字选项,缺点是并非所有的实现都支持这两个选项,不适用于connect;设置套接字选项将影响在套接字上所有的读和写操作 - 使用SIGALARM为connect设置超时
- 本技术只能缩短connect的超时时间,不能延长超时间
- 本技术利用的是系统调用的可中断能力,前提是我们能够直接处理系统调用的中断返回,对于一个封装的函数库这个前提不一定成立
- 在多线程正确使用信号非常困难,因此应该只在单线程中程序中使用这项技术
- 使用SIGALARM为recvfrom设置超时
- 使用select为recvfrom设置超时
- 使用
SO_RCVTIMEO
为recvfrom设置超时
- 在涉及套接字的I/O操作上设置超时的方法
- recv和send函数
ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags)
ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags)
- flags不是一个值结果类型的参数,如果需要内核向进程传回标志,使用recvmsg和sendmsg
- readv和writev函数
- 这两个函数允许在单个系统调用中读入或写出一个或多个缓冲区,称为分散读(scatter read)和聚集写(gather write)
ssize_t readv(int filedes, const struct iovec *iov, int iovcnt)
ssize_t writev(int filedes,const struct iovec *iov, int iovcnt)
- iovcnt表示iov数据中元素的个数,受到
IOV_MAX
的限制
- iovcnt表示iov数据中元素的个数,受到
- writev是一个原子操作,意味着对于UDP套接字而言,一个writev调用只产生单个UDP数据报
- 对于多个小分组的write操作可能引起Nagle算法从而产生延迟,可以使用writev来避免
- recvmsg和sendmsg函数
- 这两个函数是最通用的I/O函数,可以替代其他的I/O函数
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags)
ssize_t sendmsg(int sockfd, struct msghdr *msg, int flags)
- 对struct msghdr的说明见课本P307
- 辅助数据(ancillary data)
- 辅助数据可通过调用sendmsg和recvmsg函数,使用msghdr结构中的
msg_control
和msg_controllen
发送和接收,辅助数据又称为控制信息(control information) - 辅助数据的用途总结见图14-11
- 辅助数据由一个或多个辅助数据对象(ancillary data object)构成,每个对象以一个struct cmsghdr开头;并不是所有的实现都支持在控制缓冲区中存放多个辅助数据对象
- 由
msg_control
指向的辅助数据必须为cmsghdr结构适当地对齐 - 为了简化对辅助数据的处理,定义了一些宏,见
man CMSG
- 辅助数据可通过调用sendmsg和recvmsg函数,使用msghdr结构中的
- 排队的数据量
- 在不真正读取数据的前提下获悉已排队的数据量
- 如果只是想避免I/O操作阻塞,可以使用非阻塞I/O
- 如果既想要查看数据,又想让数据保留在接收队列中,可以使用
MSG_PEEK
标志;为了避免该操作阻塞,可以使用非阻塞套接字或者结合使用标志MSG_DONTWAIT
- 对于TCP套接字而言,在两次recv操作之间可能接收到新的数据导致排队数据量发生变化
- 对于UDP套接字而言,在两次recvfrom操作之间即使有新的数据报到达,都是返回同一个数据报的大小
- 一些实现支持ioctl的FIONREAD命令
- 对于TCP套接字返回接收队列中总的字节数
- 对于UDP套接字返回接收队列中总的数据报字节数,在源自Berkeley实现中,返回值还包括一个套接字地址结构的空间
- 在不真正读取数据的前提下获悉已排队的数据量
- 套接字和标准I/O
- Unix I/O:类似read,write,recv,send围绕描述符(descriptor)工作的I/O
- Standard I/O library:标准I/O函数库由ANSI C标准规范,意在便于移植到支持ANSI C的非Unix系统上
- 标准I/O函数库可用于套接字,不过需要考虑以下问题
- fdopen可以从任何一个描述符创建一个标准I/O流;fileno可以获取一个给定标准I/O流对应的描述符
- TCP和UDP套接字是全双工的,标准I/O流也可以是全双工的
- 必须在调用输出函数之后插入fflush、fseek、fsetpos或rewind才能接着调用一个输入函数
- 必须在调用输入函数之后插入fseek、fsetpos或rewind才能接着调用一个输出函数,除非输入遇到EOF,因为读和写共用一块缓冲区
- fseek、fsetpos或rewind都是调用lseek,然而lseek在套接字上会失败,因此最简单的方法就是为一个给定套接字打开两个标准I/O流,一个用于读,另一个用于写
- 标准I/O使用三种类型的缓冲
- 完全缓冲(fully buffering):当缓冲区满、调用fflush或者exit函数时才会冲洗缓冲区
- 行缓冲(line buffering):在完全缓冲的条件上如果遇到换行符也会冲洗缓冲区
- 不缓冲(unbuffering):每次调用标准I/O输出函数都会发生I/O
- 标准I/O函数库的大多数Unix实现使用下列的缓冲规则
- 标准错误是不缓冲的
- 标准输入和标准输出是全缓冲的,除非它们指向终端设备(通常为行缓冲)
- 其它输入输出是全缓冲的,除非它们指向终端设备(通常是行缓冲)
- 标准I/O对于套接字默认使用的是全缓冲,可以通过setvbuf改变缓冲类型或者每次调用fflush冲洗缓冲区,但还是存在一些问题(Nagle算法);因此大多数情况下应该避免对套接字使用标准I/O函数库
- 高级轮询技术
- Solaris上的/dev/poll
- FreeBSD的kqueue
- T/TCP:事务目的TCP