UDP和TCP
UDP(User Datagram Protocol,用户数据报协议)是一个无连接协议,不保证UDP数据报会到达其最终目的地,不保证各数据报的先后顺序跨网络后保持不变,也不保证每个数据报只到达一次。
UDP提供无连接的服务,因为UDP客户与服务器之间不必存在任何长期的关系。一个UDP客户可以使用一个套接字发送数据报给多个服务器,一个UDP服务器也可以用同一个套接字从不同的客户接收数据报。
每个UDP数据报都一个长度,数据报的长度会随数据一同传递给接收端进程;而TCP是一个字节流协议,没有任何记录边界。
TCP(Transmission Control Protocl,传输控制协议)是一个面向连接的协议,为用户进程提供可靠的全双工字节流。
TCP提供客户与服务器之间的连接。TCP客户先与某个给定的服务器建立连接,再跨该连接与那个服务器交换数据,然后终止这个连接。
TCP提供了可靠性。当TCP向另一端发送数据时,它要求对端返回一个确认。如果没有收到确认,TCP就自动重传数据并等待更长时间,在数次重传失败后才放弃。TCP含有用于动态估算客户和服务器之间的往返时间(round-trip time,RTT)的算法,以便知道等待一个确认需要多少时间。TCP通过给其中每个字节关联一个序列号对所发送的数据进行排序,接收端根据收到的分节的序列号重新排序,并且根据序列号判断并丢弃重复数据。
TCP提供流量控制。TCP总是告知对端在任何时刻它一次能够从对端接收多少字节的数据,这称为通告窗口,该窗口指出接收缓冲区当前可用的空间量,从而确保发送端发送的数据不会使接收缓冲区溢出。
TCP连接是全双工的,这意味着在一个给定的连接上应用可以在任何时刻在进出两个方向上既发送数据又接收数据。
TCP连接建立和终止
TCP建立一个连接需要3个分节,称为TCP的三路握手,而终止一个连接则需要4个分节。下图展示了一个完整的TCP连接所发生的实际分组交换情况,包括连接建立、数据传送和链接终止3个阶段,还展示了每个端点所历经的TCP状态。
每一个SYN选项可以含有多个TCP选项,下面是常用的选项。
- MSS选项。发送SYN的TCP一端使用本选项通告对端它的最大分节大小,即MSS(Maximum Segment Size),也就是它在本连接的每个TCP分节中愿意接受的最大数据量。
- 窗口规模选项。TCP能通告的最大窗口大小是65535,因为TCP首部中相应的字段占16位。
- 时间戳选项。它可以防止失而复现的分组可能造成的数据损坏。
TCP涉及连接建立和连接终止的操作可以用状态转换图来说明。
TIME_WAIT状态有两个存在的理由:
- 可靠地实现TCP全双工连接的终止。假设最终的ACK丢失了,服务器将重新发送它的最终那个FIN,因此客户必须维护状态信息,以允许它重新发送最终的那个ACK。
- 允许老的重复分节在网络中消逝。假设在关闭一个连接一段时间后在相同的IP地址和端口之间建立了另一个连接,即前一个连接的化身,TCP必须防止来自某个连接的老的重复分组在该连接已终止后再现,从而被误解为属于其化身的分组。因此TCP将不给处于TIME_WAIT状态的连接发起新的化身,TIME_WAIT状态持续时间为2MSL(Maximum Segment Lifetime,最长分节生命期),就足以让某个方向上的分组和另一方向上的应答最多存活MSL被丢弃。
一个TCP套接字对是一个定义该连接的两个端点的四元组:本地IP地址、本地TCP端口号、外地IP地址、外地TCP端口号,套接字对唯一标识一个网络上的每个TCP连接。
缓冲区大小及限制
许多网络有一个可由硬件规定的MTU(Maximum Transmission Unit,最大传输单元),如以太网的MTU是1500字节。在两个主机之间的路径最小MTU称为路径MTU,两个主机之间相反的两个方向上路径MTU可以不一致,因为因特网中路由选择往往是不对称的。当一个IP数据报将从某个接口发出时,如果它的大小超过相应链路的MTU,IP将执行分片,这些分片在到达最终目的地之前通常不会被重组。
IPv4和IPv6都定义了最小重组缓冲区大小,它是IPv4或IPv6的任何实现都必须保证支持的最小数据报大小,其值对于IPv4为576字节。
TCP的MSS用于向对端TCP通告对端在每个分节中能发送的最大TCP数据量。MSS的目的是告诉对端其重组缓冲区大小的实际值,从而视图避免分片。MSS通常设置成MTU减去IP和TCP首部固定长度(都为20字节),如在以太网中使用IPv4的MSS为1460(1500-20-20)。
每一个TCP套接字有一个发送缓冲区,当某个应用进程调用write时,内核从该应用进程的缓冲区复制所有数据到所写套接字的发送缓冲区。对于阻塞套接字,如果发送缓冲区容不下该应用进程的所有数据,进程将被投入睡眠,直到应用进程缓冲区中的所有数据都复制到套接字发送缓冲区。因此,从写一个TCP套接字的write调用成功返回仅仅表示我们可以重新使用原来的应用进程缓冲区,并不表明对端的TCP或应用进程已接收到数据。TCP提取套接字发送缓冲区中的数据并把它发送给对端TCP,在对端ACK到达后,本端TCP才能从发送缓冲区中丢弃已确认的数据。TCP数据经由IP传递给数据链路,每个数据链路都有一个输出队列,如果该队列已满,新的分组将被丢弃,并沿协议栈向上返回一个错误到TCP,TCP将注意到这个错误,并在以后某个时刻重传相应的分节,这个过程对应用进程透明。
UDP是不可靠的,它不必保存应用进程数据的副本,因此UDP套接字没有发送缓冲区,但有发送缓冲区大小,表示可写到改套接字的UDP数据报大小的上限,如果一个应用进程写一个大于套接字发送缓冲却大小的数据报,内核将返回给进程一个EMSGSIZE错误。由于没有类似TCP的MSS,UDP应用进程在发送大数据报比TCP更可能被分片。从写一个UDP套接字的write调用成功返回表示所写的数据报或其所有片段已被缴入数据链路层输出队列,如果该队列没有足够的空间存放改数据报或它的某个片段,内核通常会返回一个ENOBUFS给它的应用进程。
套接字地址结构
IPv4套接字结构为sockaddr_in,定义在头文件<netinet/in.h>中。
套接字函数为了能支持任何协议族的套接字地址结构,使用了一个通用套接字地址结构的指针作为参数,头文件<sys/socket.h>中定义了这个通用的套接字地址结构sockaddr。
下面是网络地址在点分十进制数串和网络字节序二进制值之间转换的函数。
inet_addr出错时返回INADDR_NONE(通常是一个32为均为1的值),这意味着255.255.255.255不能由该函数处理。如今inet_addr已被废弃,新的代码应该改用inet_aton函数。
inet_ntoa返回值所指向的字符串驻留在静态内存中,这意味着该函数是不可重入的。
TCP套接字编程
基本TCP客户/服务器程序的套接字函数使用如下:
socket函数
为了执行网络IO,一个进程必须做的第一件事就是调用socket函数。
其中family指明协议域:
type参数指明套接字类型:
connect函数
TCP客户用connect函数来建立与TCP服务器的链接。
如果是TCP套接字,调用connect函数将触发TCP的三路握手过程,而且仅在连接建立成功或出错时才返回,出错返回可能是以下几种情况。
- 若TCP客户没有收到SYN分节的响应,经过一定的重试后返回ETIMEDOUT错误。
- 若对客户的SYN的响应是RST,则表明该服务器主机在我们指定的端口上没有进程在等待与之连接,客户收到RST返回ECONNREFUSED错误。
- 若客户发出的SYN在中间的某个路由器上引发一个"destination unreachable"ICMP错误,经过一定的重试后返回EHOSTUNREACH或ENETUNREACH错误。
bind函数
bind函数把一个本地协议地址赋予一个套接字。
bind可以指定一个也可以端口号,或指定一个IP地址,也可以两者都指定,或者都不指定。
若一个TCP客户或服务器未调用bind绑定一个端口,当调用connect或listen时,内核就要为相应的套接字选择一个临时端口。
TCP服务器绑定到某个IP,这就限定该套接字只接收目的地址为该IP的连接。TCP客户通常不绑定IP,连接套接字时,内核将根据外出网络接口来选择源IP。若TCP服务器未绑定IP,内核把客户发送的SYN的目的地址作为服务器的源IP。
从bind函数返回的一个常见错误是EADDRINUSE("Address aready in use",地址已使用)。
listen函数
listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。
backlog参数规定了内核应该为相应套接字排队的最大连接数。内核为任何一个给定的监听套接字维护队列:
- 未完成连接队列,每个这样的SYN分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。
- 已完成链接队列,每个已完成TCP三路握手过程的客户对应其中一项。
当一个客户SYN到达时,若这些队列是满的,TCP就忽略该分节,也就是不发送RST。
accept函数
accept函数用于从已完成连接队列对头返回下一个已完成连接。
如果accpet成功,其返回已连接套接字的描述符,这是一个不同于监听套接字的新套接字。我们称它的第一个参数为监听套接字描述符,称它的返回值为已连接套接字描述符。
如果我们对返回客户协议地址不感兴趣,可以把cliaddr和addrlen均置为空指针。
close函数
close函数用来关闭套接字,并终止TCP连接。
close一个TCP套接字的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程。TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列。
关闭已连接套接字只是导致相应描述符的引用计数减1,如果引用计数值仍大于0,close调用并不引发TCP四分组连接终止序列。如果确实想在某个TCP连接上发送一个FIN,可以改用shutdown函数代替close。
recv和send函数(305)
getsockname和getpeernanme函数
这两个函数或者返回与某个套接字关联的本地协议地址(getsockname),或者返回与某个套接字关联的外地协议地址(getpeername)。
recv和send函数
TCP异常情况
服务器进程终止
服务器进程终止后,进程中所有打开的描述符都被关闭,这就导致向客户发送一个FIN,而客户TCP响应一个ACK。
如果客户继续发送数据给服务器,服务器收到来自客户的数据时,由于先前打开那个套接字的进程已经终止,于是响应一个RST。
然而客户端进程看不到这个RST,由于前面接收到的FIN,调用read会立即返回0。如果进程忽略该错误,继续发送数据到服务器,将返回EPIPE错误。
当一个进程向某个已收到RST的套接字执行写操作时,内核向进程发送一个SIGPIPE信号,写操作会分会EPIPE错误。
服务器主机崩溃
当服务器主机崩溃时,客户TCP持续重传数据分节,试图从服务器上接收一个ACK。当客户TCP最终放弃时,给客户进程返回一个错误。假设服务器主机已崩溃,从而对客户的数据分节没有响应,那么所返回的错误是ETIMEDOUT;如果某个中间路由器判定服务器主机不可达,从而响应一个"destination unreachable"ICMP消息,那么返回的错误是EHOSTUNREACH或ENETUNREACH。
服务器主机崩溃后重启
当服务器主机崩溃后重启时,它的TCP丢失了崩溃钱的所有连接信息,因此服务器TCP对于所收到的来自客户的数据分节响应一个RST。
当客户TCP收到该RST时,客户的read调用返回ECONNRESET错误。
服务器主机关机
系统关机时,init进程通常先给所有进程发送SIGTERM信号,然后给所有仍在运行的进程发送SIGKILL信号。当服务器进程终止时,它的所有打开着的描述符都被关闭。
I/O模型
- 阻塞式I/O。默认情形下所有套接字都是阻塞的。
- 非阻塞式I/O。进程把一个套接字设置成非阻塞是在通知内核:当所有请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。
- I/O复用模型。I/O复用是调用select或poll,阻塞在这两个系统调用中的某一个之上,而不是阻塞在真正的I/O系统调用上。
- 信号驱动式I/O模型。让内核在描述符就绪时发送SIGIO信号通知进程。
- 异步I/O模型。函数的工作机制是:告知内核启动某个操作,并让内核在整个操作完成后通知我们。
套接字选项
有以下方法来获取和设置影响套接字的选项:
- getsockopt和setsockopt函数
- fcntl函数
- ioctl函数
getsockopt和setsockopt函数
getsockopt和setsockopt仅用于套接字。
可获取和设置的套接字选项如下:
SO_KEEPALIVE
给一个TCP套接字设置保持存活选项后,如果2小时内在该套接字的任一方向上都没有数据交换,TCP就自动给对端发送一个保持存活探测分节。
SO_LINGER
本选项指定close函数对面向连接的协议如何操作。默认操作是close立即返回,但是如果有数据残留在套接字发送缓冲区中,系统将试着把这些数据发送给对端。本选项在用户进程和内核间传递如下结构,它在头文件<sys/socket.h>中定义:
本选项有以下情形:
- 如果l_onoff为0,那么关闭本选项,TCP默认设置生效,即close立即返回。
- 如果l_onoff为非0且l_linger为0,那么当close某个连接时TCP将终止该连接。就是说TCP将丢弃保留在套接字发送缓冲区中的任何数据,并发送一个RST给对端,而没有通常的四分组连接终止序列,这样避免了TCP的TIME_WAIT状态。
- 如果l_onoff为非0且l_linger为非0,那么当关闭套接字时内核将拖延一段时间。就是说如果在套接字发送缓冲区中仍残留数据,那么进程将被投入睡眠,直到数据都已发送且被对端确认或延滞时间到。
SO_RCVBUF和SO_SNDBUF
对于TCP来说,套接字接收缓冲区中可用空间的大小限定了TCP通告对端的窗口大小。对于UDP来说,当接收到的数据报装不进套接字接收缓冲区时,改数据报就被丢弃。
由于TCP的窗口规模选项是在建立连接时用SYN分节与对端互换得到的。对于客户端,SO_RCVBUF选项必须在调用connect之前设置;对于服务器,该选项必须在调用listen之前给监听套接字设置。
SO_RCVTIMEO和SO_SNDTIMEO
这两个选项允许我们给套接字的接收和发送设置一个超时值。
SO_REUSEADDR
本选项能起到以下4个不同的作用:
- 允许启动一个监听服务器并绑定某个端口,即使以前建立的将该端口用作他们的本地端口的连接仍存在。
- 允许在同一端口上启动同一服务器的多个实例,只要每个实例绑定一个不同的本地IP地址即可。
- 允许单进程绑定同一端口到多个套接字上,只要每次绑定不同的本地IP地址即可。
- 允许完全重复的绑定,即同样的IP地址和端口可以绑定到多个套接字上,本特性仅支持UDP套接字。
TCP_NODELAY
开启本选项将禁止TCP的Nagle算法,默认情况下该算法是启动的。Nagle算法的目的是为了减少广域网上小分组的数目。该算法的思想是:如果某个给定的连接上有待确认的数据,那么原本应该作为用户写操作之响应的在该连接上立即发送相应小分组的行为就不会发生。
fcntl函数
fcntl函数可执行各种描述符控制操作。
fcntl提供的与网络编程相关的特性主要是设置非阻塞式I/O,通过使用F_SETFL命令设置O_NONBLOCK文件状态标志,可以把一个套接字设置为非阻塞型。典型代码如下:
UDP套接字编程
UDP客户/服务器程序程序所用的套接字函数如下:
recvfrom和sendto函数
flags总是置为0。
写一个长度为0的数据报是可行的,这会形成一个只包含IP首部和UDP首部而没有数据的IP数据报。recvfrom返回0是可接受的:它并不像TCP套接字上read返回0表示对端已关闭连接。
如果recvfrom的from参数是一个空指针,那么相应的addrlen也必须是一个空指针,表示不关心数据发送者的协议地址。
客户的临时端口是在第一次调用sendto时一次性选定的,不能改变;然而客户的IP地址却可以随客户发送的每个UDP数据报而变动。
服务器进程未运行
如果服务器进程未运行,客户数据报发出,服务器主机响应一个"port unreachable"ICMP消息,不过这个ICMP错误不返回给客户进程。我们称这个ICMP错误为异步错误,该错误由sendto引起,但sendto本身却成功返回。一个基本规则是:对于一个UDP套接字,由它引发的异步错误却并不返回给它,除非它已连接。
UDP的connect函数
我们可以给UDP套接字调用connect,内核只是检查是否存在立即可知的错误(例如一个显然不可达的目的地),记录对端的IP地址和端口号,然后立即返回到调用进程。
UDP客户进程或服务器进程只有在使用自己的UDP套接字与确定的唯一对端进行通信时,才可以调用connect。
对于已连接UDP套接字,与默认的未连接UDP套接字相比,有以下不同:
- 不能给输出操作指定目的IP地址和端口号,即不使用sendto而改用write或send。
- 不必使用recvfrom以获悉数据报的发送者,而改用read、recv或recvmsg。在一个已连接UDP套接字上,由内核为输入操作返回的数据报只有那些来自connect所指定协议地址的数据报。
- 由已连接UDP套接字引发的异步错误会返回给它们所在的进程,而未连接UDP套接字不接收任何异步错误。
对于TCP套接字,connect只能调用一次。对于一个已连接的UDP套接字出于这两个目的之一可以再次调用connect:指定新的IP地址和端口号;断开套接字。
Unix域协议
Unix域协议并不是一个实际的协议族,而是单个主机上执行客户/服务器通信的一种方法。Unix域提供两类套接字:字节流套接字(类似TCP)和数据报套接字(类似UDP)。有以下理由使用Unix域套接字:
- 在某些系统中,Unix域套接字比位于同一主机的TCP套接字通信要快。
- Unix域套接字可用于在同一主机上的不同进程之间传递描述符。
- Unix域套接字吧客户的凭证(用户ID和组ID)提供给服务器,从而能够提供额外的安全检查措施。
地址结构
Unix域套接字地址结构在头文件<sys/un.h>中定义:
socketpair函数
socketpair函数创建两个连接起来的套接字。
family参数必须为AF_LOCAL,protocol参数必须为0,type参数既可以是SOCK_STREAM,也可以是SOCK_DGRAM,新创建的两个套接字描述符作为sockfd[0]和sockfd[1]返回。
套接字函数
在connect调用中指定的路径名必须是一个当前绑定在某个打开的Unix域套接字上的路径名,而且它们套接字类型也必须一致。
Unix域字节流套接字类似TCP套接字:它们都为进程提供一个无记录边界的字节流接口;Unix域数据报套接字类似UDP套接字:它们都提供一个保留记录边界的不可靠的数据报服务。
在一个未绑定的Unix域套接字上发送数据报不会自动给这个套接字捆绑一个路径名,这一点不同于UDP套接字。