协议栈精要

TCP/IP 精要

《TCP/IP详解学习笔记》系列文章的学习整理,点击标题查看原文。

基本概念

为什么会有TCP/IP协议

为了可以在多个单机的计算机之间进行通信,可以使用电线将他们连接在一起。但是简单的连接在一起还不够,好比语言不同的两个人见面后并不能正确的交流信息。因此需要定义一些共通的东西来进行交流,TCP/IP就是为此而生。

TCP/IP不是一个协议,而是一个协议簇的统称。里面包含了IP协议、IMCP协议、TCP协议,以及我们更加熟悉的HTTP、FTP、POP3协议等。计算机有了这些,就好像大家都统一使用英语来交流一样。

TCP/IP协议分层

协议分层经常会提到IOS-OSI七层协议经典架构,但是TCP/IP协议族的结构稍有不同。如图:

NAME

TCP/IP协议族按照层次由上到下,层层包装。最上面是应用层,里面包含HTTP、FTP等我们熟悉的协议。第二层是传输层,著名的TCP和UDP协议就在这层。第三层是网络层,包含IP协议,负责对数据加上IP信息和其他数据以确定传输的目标。第四层叫做数据链路层,为待传送的数据加入一个以太网协议头,并进行CRC编码,为最后的数据传输做准备。再往下则是硬件层次了,负责网络的传输,这个层次的定义包括网线的制式,网卡的定义等。

发送数据的主机从上自下将数据按照协议封装,而接收数据的主机则按照协议将得到的数据包解开,最后拿到需要的数据。

基本常识

互联网地址

网络上每一个节点都必须有一个独立的internet地址(即IP地址),现在常用的是IPV4地址,又被分为5类,常用的是B类地址。需要注意的是IP地址是网络号+主机号的组合,这非常重要。

域名系统

域名系统是一个分布的数据库,它提供将主机名(即网址)转换成IP地址。

RFC

RFC就是TCP/IP协议栈的标准文档,文档中可以看到一个很长的定义列表,现在一共有4000多个协议的定义,然而我们要学习使用的也就10多个。

端口号

这个号码是用在TCP和UDP上的一个逻辑号码,并不是一个硬件端口。平时所说的封掉某个端口,也只是在IP层次上把带有这个号码的IP包给过滤掉而已。

应用编程接口

现在常用的编程接口有socket和TLI。

数据链路层

数据链路层有三个目的:

  1. 为IP模块发送和接收IP数据报
  2. 为ARP模块发送ARP请求和接收ARP应答
  3. 为RARP发送RARP请求和接收RARP应答

ARP叫做地址解析协议,是用IP地址换MAC地址的一种协议,而RARP叫做逆地址解析协议。

数据链路层的协议还是很多的,有我们最常用的以太网(网卡)协议,也有不太常用的令牌环,还有FDDI,还有国内现在相当普及的PPP(adsl宽带),以及一个loopback协议。

在Linux终端中使用ifconfig -a命令,这个命令通常会得到如下结果:

lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
	options=3<RXCSUM,TXCSUM>
	inet6 ::1 prefixlen 128
	inet 127.0.0.1 netmask 0xff000000
	inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1
	nd6 options=1<PERFORMNUD>
gif0: flags=8010<POINTOPOINT,MULTICAST> mtu 1280
stf0: flags=0<> mtu 1280
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	ether ac:bc:32:8e:41:57
	inet6 fe80::aebc:32ff:fe8e:4157%en0 prefixlen 64 scopeid 0x4
	inet 192.168.10.203 netmask 0xffffff00 broadcast 192.168.10.255
	inet6 fd3d:5f4e:424b::aebc:32ff:fe8e:4157 prefixlen 64 autoconf
	inet6 fd3d:5f4e:424b::806d:59f8:8422:b7ca prefixlen 64 autoconf temporary
	nd6 options=1<PERFORMNUD>
	media: autoselect
	status: active

其中,eth0就是以太网接口,而lo则是lookback接口。这也说明这个主机在网络链路层上至少支持lookback协议和以太网协议。

以太网的定义是指:数字设备公司、英特尔公司和Xerox公司在1982年联合公布的一个标准,这个标准里面使用了一种称作CSMA/CD的接入方法。而IEEE802提供的标准集802.3(还有一部分定义在802.2中)也提供了一个CSMA/CD的标准。这两个标准稍有不同,TCP/IP对这种情况的处理方式如下:

  1. 以太网的IP数据报封装在RFC894中定义,而IEEE802网络的IP数据报封装在RFC1042中定义。
  2. 一台主机一定要能发送和接收RFC894定义的数据报。
  3. 一台主机可以接收RFC894和RFC1042的封装格式的混合数据报。
  4. 一台主机也许能够发送RFC1042数据报。如果主机能够同时发送两种类型的分组数据,那么发送的分组必须是可以设置的,而且默认的情况下必须是RFC894分组。

可见,RFC1042在TCP/IP里处于一个配角的地位。

PPP(点对点协议)是SLIP的替代品。他们都提供了一种低速接入的解决方案。而每一种数据链路层协议,都有一个MTU(最大传输单元)定义,在这个定义下面,如果IP数据报过大,则要进行分片(fragmentation),使得每片都小于MTU。注意PPP和MTU并不是一个物理定义,而是指一个逻辑定义(个人认为就是用程序控制)。可以用netstat打印MTU的结果,比如命令netstat -in,可以看到各协议的MTU值。

环回接口(lookback),平时我们用127.0.0.1测试本机服务器是否可以使用,走的就是这个环回接口。对于环回接口,有如下三点值得注意:

  1. 传给换回地址(127.0.0.1)的任何数据均作为IP输入
  2. 传给广播地址或多播地址的数据报复制一份传给环回接口,然后发送到以太网上。这是因为广播传送和多播传送的定义包含主机本身
  3. 任何传给主机IP地址的数据均送到环回接口

IP,ARP,RARP

这三个协议均属于网络层。ARP协议用于找到目标主机的Ethernet网卡MAC地址,IP要承载发送的消息。数据链路层可以从ARP得到数据的传送信息,而从IP得到要传输的数据的信息。

IP协议

IP协议用于将多个包交换网络连接起来,它在源地址可目标地址之间传送一种称为数据包的东西,并提供对数据包大小的重新组装功能,以适应不同网络对包大小的要求。

IP协议实现两个基本功能:寻址和分段。IP可以根据数据包包头中包括的目的地址将数据报传送到目的地址,在此过程中IP负责选择传送的道路,称为路由。如果有些网络内只能传输小数据报,IP可以将数据报重新组装并在包头域内注明。

IP协议是TCP/IP协议栈的核心,所有的TCP、UDP、IMCP、IGCP的数据都是以IP数据格式传输的。要注意的是,IP不是可靠的协议,就是说,IP协议没有提供一种数据未到达以后的处理机制,这被认为是上层协议–即TCP和UDP要做的事情。所以也就出现了TCP是一个可靠的协议,而UDP就没有那么可靠的区别。

IP协议头

NAME

其中8位的TTL字段规定该数据报在穿过多少个路由之后才被丢弃(即它不保证数据被送达),某个IP数据包没穿过一个路由,该数据报的TTL值就会减少1,当该数据报的TTL值为0,它就会自动被丢弃。这个字段的值最大为255,也就是说一个协议包在路由里穿行255次就会被丢弃,根据系统的不同,这个值的大小也不一样,一般是32或64。Tracrouter这个工具就是用这个原理工作的,其-m选项要求最大值是255,也就是说这个TTL在IP协议里面只有8bit。

先在的IP版本号是4,即成为IPV4,同时还有现在使用越来越广泛的IPV6。

IP路由选择

当一个IP数据包准备好之后,IP数据包(或路由器)是如何将数据包送到目的地的呢?它是如何选择一个合适的路径来“送货”?

最特殊的情况是主机和目的主机直连,这时主机根本不用寻找路由,直接将数据传送过去。至于是怎么直接传递的,会用到ARP协议。

稍微一般一点的情况是,主机通过若干个路由器和目的主机连接。那么路由器要用IP包的信息来为IP包找到一个合适的目标进行传递,比如合适的主机,或者合适的路由。路由或主机将会用如下的方式来处理一个IP数据包:

  1. 如果IP数据包的TTL值已经为0,则丢弃该IP数据包;
  2. 搜索路由表,有限搜索匹配主机,如果能找到和IP地址完全一致的目标主机,则将该包发向目标主机;
  3. 搜索路由表,如果匹配主机失败,则匹配同子网的路由器,这需要子网掩码(参考下面一节的子网寻址)的协助。如果找到路由器,则发送该数据包;
  4. 搜索路由器,如果匹配相同子网路由器失败,则匹配同网号路哟器。如果找到,则发送该数据包;
  5. 搜索路由表,如果以上都失败,就搜索默认路由,如果默认路由存在,则发包;
  6. 如果都失败,丢弃该包。

这在一起说明,IP包是不可靠的,因为它不保证送达。

子网寻址

IP地址的定义是网络号+主机号。但是现在所有的主机都要求子网编址,也就是说,把主机号再细分成子网号+主机号。最终一个IP地址就成为:网络号码+子网号+主机号。例如一个B类地址:210.30.109.134。一般情况下,这个IP地址的红色部分就是网络号,蓝色部分就是子网号,绿色部分就是主机号。至于有多少位代表子网号这个问题,没有一个硬性的规定,取而代之的子网掩码,在校园网的设定里面有一个225.225.225.0的东西,就是子网掩码。

ARP协议

在数据链路层的以太网协议中,每一个数据包都有一个MAC地址头。每一块以太网卡都有一个MAC地址,这个地址是唯一的,那么IP包是如何知道这个MAC地址呢,这就是ARP的工作。

ARP(地址解析)协议是一种解析协议,本来主机是不知道这个IP对应的是哪个主机的哪个接口,当主机发送一个IP包的时候,首先会查一下自己的ARP告诉缓存(IP-MAC地址对应缓存),如果查询的IP-MAC不存在,那么主机就发送一个ARP协议广播包,这个广播包中包含待查询的IP地址,而直接收到这个广播包的所有主机都会查询自己的IP地址,如果其中一个主机发现自己符合条件,那么就准备好一个包含自己MAC地址的ARP包传送给发送ARP广播的主机,然后广播主机拿到ARP包后会更新自己的ARP缓存。发送广播的主机就会用新的ARP缓存数据准备好数据链路层的数据包发送工作。

一个典型的ARP缓存信息如下,在系统中使用arp -a命令。

这个高速缓存的时限是20分钟。

ICMP协议

由于IP协议并不是一个可靠的协议,因此保证数据送达的工作就会由其他模块来完成,其中一个重要的模块就是IMCP(网络控制报文)协议。

当IP数据包发生错误,比如主机不可达、路由不可达等,ICMP就会将错误信息封包,然后传送回给主机。给主机一个处理错误的机会,这也就是为什么说建立在IP层以上的协议是能够做到安全的原因。ICMP数据包由8bit的错误类型+8bit的代码+16bit的校验和组成。而前16bit就组成了ICMP要传递的信息。

尽管在大多数情况下,错误的包传送应该给出ICMP报文,但是在特殊情况下,是不产生ICMP报文的:

  1. ICMP错误报文不会产生ICMP错误报文(出ICMP查询报文),防止ICMP的无线产生和传送
  2. 目的地址是广播地址或多播地址的IP数据包
  3. 作为链路层广播的数据包
  4. 不是IP分片的第一片
  5. 原地址不是单个主机的数据包

这里的一切规定,都是为了防止ICMP报文的无线传播而定义的。

ICMP协议大致分两类,一种是查询报文,一种是错误报文。其中查询报文的用途:

  1. ping查询
  2. 子网掩码查询
  3. 时间戳查询

ICMP的应用-ping

ping可以说是ICMP的最注明应用,可以通过ping一个网址来查看其是否可用。

原理是用类型码为0的ICMP发请求,收到请求的主机则用类型码为8的ICMP回应。ping程序来计算时间间隔,并计算有多少包被送达。用户就可以判断大致的网络情况。

ping还给我们一个看到目的主机路由的机会,这是因为,ICMP的ping请求数据包在没经过一个路由的时候,路由器会把自己的IP放到该数据包中。而目的主机则会把这个IP列表复制到回应ICMP数据包中发回给主机。但是这个信息比较有限,如果想要查看更详细的路由,可以使用tracerouter。

ICMP的应用-tracerouter

Tracerouter用来侦测主机到目的主机之间所经路由情况的重要工具,也是最便利的工具。

它的原理是,它收到目的主机的IP后,首先给目的主机发送一个TTL=1的UDP数据包,而经过的第一个路由器收到这个包之后就自动把TTL减1,这时TTL为0,路由器就把这个包丢弃了,并同时产生一个主机不可达的ICMP数据包给主机。主机收到这个数据包以后再发一个TTL=2的UDP数据包给目的主机,然后刺激第二个路由器给主机发送ICMP数据包。如此往复直到到达目的主机,这样,tracerouter就拿到了所有路由IP,从而避免了IP头只能记录有限路由IP的问题。

但是tracerouter是如何直到是否到达目的主机了呢。这就涉及到一个技巧问题,TCP和UDP协议有一个端口号定义,普通的网络程序只监控少数几个号码较小的端口,如80、23等。而tracerouter发送的端口号>30000,所以到达主机的时候,目的主机只能发送一个端口不可达的ICMP数据报给主机,主机接收到这个报告以后就知道主机到了。

IP选路、动态选路

UDP协议

UDP是传输层协议,和TCP处于同一个分层中,但是于TCP不同,UDP不提供超时重传,出错重传等功能,也就是说它是不可靠协议。

协议头

UDP端口号

由于很多软件要用到UDP协议,所以UDP协议必须通过某个标志用以区分不同的程序所需要的数据包,这就是端口号的功能。例如一个UDP程序在A系统中注册了3000端口,以后从外部传进来的端口号为3000的数据包就会交给该程序。

UDP检验和

这是一个可选的选项,并不是所有的系统都对UDP数据包加以检验和数据(相对TCP的必须来说),但是RFC中标准要求,发送端应该计算校验和。

UDP校验和覆盖UDP协议头和数据,这个IP的检验和是不同的,IP协议的检验和只是覆盖IP数据头,并不覆盖所有的数据。UDP和TCP都包含一个伪首部,这是为了计算校验和而设置的。伪首部甚至包含IP地址这样IP协议里面都有的数据,目的是让UDP两次检查数据是否正确到达目的地。如果发送端没有打开校验和选项,而接收端计算校验和有差错,那么UDP数据将会被悄悄的丢掉(不保证送达),而不会产生任何错误报文。

UDP长度

UDP可以很长,长达65535字节。但是一般网络在传输的时候,一次传输不了那么长的协议(MTU),就只好对数据分片,当然,这些是对UDP上层协议透明的,UDP不需要关心IP层如何对数据分片。

IP分片

IP是在从上层接到数据以后,根据IP地址来判断从哪个接口发送数据,并进行MTU查询,如果数据大小超过MTU就进行分片。数据的分片对上层和下层透明,而数据在到达目的地后会重新组装,IP层提供了足够的信息进行数据的再组装。

UDP服务器设计

UDP协议的特性将会影响我们的服务器程序设计,大致总结如下:

  1. 关于客户IP和地址:服务器必须有根据客户IP地址和端口号判断数据包是否合法的能力;
  2. 关于目的地址:服务器必须要有过滤广播地址的能力;
  3. 关于数据输入:通常服务器系统的每一个端口都会和一块输入缓冲区对应,进来的数据根据先来后到的原则等待服务器的处理,所以难免会出现缓冲区溢出的问题,这种情况可能会出现UDP被丢弃,而应用服务器并不知道这个问题;
  4. 服务器应该限制本地IP地址,就是说他应该可以把自己绑定到某一个网络接口的某一个端口上。

广播与多播、IGMP协议

单播、多播、组播

单播

单播是对特定的主机进行数据的传送。例如给某一个主机发送IP数据包。这时,数据链路层给出的数据头里面是非常具体的目的地址,对于以太网来说就是MAC地址。现在的具有路由功能的主机应该可以将单播数据定向转发,而目的主机的网络接口则可以过滤掉和自己MAC地址不一致的数据。

广播

广播是主机针对某一网络上的所有主机发送数据。这个网络可能是网络、子网,或所有子网。如果是网络,例如A类地址的广播就是netid.255.255.255,如果是子网,则是netid.netid.subnetid.255,如果是所有子网(B类IP),则是netid.netid.255.255。广播所用的MAC地址是FF-FF-FF-FF-FF-FF,网络内所有的主机都会收到这个广播数据,网卡只要把MAC地址为FF-FF-FF-FF-FF-FF的数据交给内核就行了。一般来说,ARP或者路由协议RIP应该是广播的形式播发的。

多播

可以说广播的多播的特例,多播就是给一组特定的主机(多播组)发送数据。这样,数据的播发范围会小一些,多播的MAC地址是最高字节的低位为1,例如:01-00-00-00-00-00,多播组的IP是D类IP,规定是224.0.0.0-239.255.255.255

IGMP协议

IGMP协议的作用在于,让其他所有需要知道自己处于哪个多播组的主机和路由器知道自己的状态。一般多播路由器根本不需要知道某一个多播组里有多少个主机,而只需要知道自己的子网内还有没有处于某个多播组的主机就行了。只要某一个多播组还有一台主机,多播路由器就会把数据传输过去,这样,接受方就会通过网卡过滤功能来得到自己想要的数据。为了知道多播组的信息,多播路由器需要定时的发送IGMP查询,各个多播组里面的主机需要根据查询来回复自己的状态。路由器来决定有几个多播组,自己要对某一个多播组发送什么样的数据。

TCP协议

TCP和UDP同样处于运输层,但是TCP和UDP最不同的地方是,TCP提供了一种可靠的的数据传输服务,TCP是面向连接的,也就是说,利用TCP通信的两台主机首先要精力一个拨打电话的过程,等到通信准备就绪才开始传输数据,最后结束通话。所以TCP要比UDP可靠的多,UDP是直接把数据发过去,而不管对方是不是在收信,就算是UDP无法送达,也不会产生ICMP差错报文。

TCP保证可靠性的工作原理:

  1. 应用数据被分割成TCP认为最适合发送的数据块。
  2. 当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段,如果不能及时收到这个确认,将重发这个报文段。
  3. 当TCP收到发自TCP另一端的数据,它将发送一个确认。这个确认不是立即发送的,通常推迟几分之一秒。
  4. TCP将保持它首部和数据的校验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP将丢弃这个报文段和不确认收到此报文段。
  5. 既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能输失序,因此TCP报文段的到达也可能会失序。如果必要,TCP将对收到的数据进行重新排序,将收到数据以正确的顺序交给应用层。
  6. TCP还能提供流量控制。TCP连接的每一方都有固定大小的缓冲空间。TCP的接收端只允许另一端发送 接收端缓冲区所能接纳的数据。这将放置比较块的主机致使比较慢主机的缓冲区溢出。

由此可见,TCP中保持可靠性的方式就是超时重发。最可靠的方式就是只要不得到确认,就重新发送数据报,直到得到对方的确认为止。

TCP和UDP的首部一样。都有发送端口号和接收端口号。显然TCP的首部信息会更多,提供了发送和确认所需要的所有必要的信息。可以想象一个TCP数据的发送过程:

  1. 双方建立连接
  2. 发送方给接收方TCP数据报,然后等待对方的确认TCP数据报,有则发送下一个,没有则等待重发
  3. 接收方等待发送方的数据报,如果得到数据并检查无误,就发送ACK数据报,并等待下一个数据报
  4. 终止连接

DNS域名系统

DNS系统介绍

DNS的全称是“Domain Name Syetem”。它负责把FQDN翻译成一个IP,最初是一个巨大的host.txt文件,最终发展到现在的分布式数据库。

DNS是一个巨大的树,最上方是一个无名树根,下一层是“arpa,com,edu,gov,int,mil,us,cn”等。其中arpa是域名反解析树的顶端。

一个独立管理的DNS子树叫做zone,最常见的区域就是二级域名,比如说.com.cn,还可以把这个二级域名划分成更小的区域,比如sina.com.cn。

DNS系统是一个分布式数据库,当一个数据库发现并没有某查询所需要的数据时,它将把查询转发出去,而转发的目的地通常是根服务器,根服务器从上自下层层转发查询,直到找到目标为止。DNS的另一特点是使用高速缓存,DNS把查询过的数据缓存在某处,以便于下次查询时使用。

DNS协议

DNS协议定义了一个既可以查询也可以响应的报文格式,各个字段的解释如下:

  1. 最前面的16个bit唯一的标识了问题号码,用于查询端区别自己的查询
  2. 紧接着的16歌bit又可以做进一步的细分,标示了报文的性质和一些细节,比如说是查询报文还是响应报文,需要递归查询与否
  3. 查询问题后面有查询类型,包括“A,NS,CNAME,HINFO,MX”
  4. 响应报文可以回复多个IP,也就是说,域名可以和多个IP地址对应,并且有很多CNAME

反向查询

正向是指通过域名查询IP,反向是指通过IP查询域名。例如用host命令,host ip就可以得到服务器的域名,host domainname得到IP地址。

DNS服务器高速缓存

BIND9默认作为一个高速缓存服务器,其将所有的查询都交到服务器上去,然后得到的结果放在本地的缓存区,以加速查询。

用UDP还是TCP

DNS服务器同时支持UDP和TCP两种协议的查询方式,而且端口都是53,大多数都是UDP查询,需要TCP查询的一般有两种情况:

  1. 当查询过大以至于产生了数据截断(TC标志为1),这时,需要利用TCP的分片能力来进行数据传输
  2. 当master和slave服务器之间通信,辅服务器要拿到主服务器的zone信息的时候。

TCP数据包内容

NAME

TCP处于7层模型中的传输层,主要是用来建立可靠的连接。而建立连接的基础,就是其非常丰富的报文。首先,TCP3次握手用的报文就是绿色部分的TCP Flags内容。通过发送ACK、SYN包实现。具体涉及的Tag详见:

  1. Source Port/Destination Post:即客户端和服务端端口号,端口号用于区分主机中不同的进程,通过结合源IP和目的IP,得出唯一的TCP连接;
  2. Sequence Number(seqNumber):一般由客户端发送,用来表示报文段中第一个数据字节在数据流中的序号,主要用来解决网络包乱序问题;
  3. Acknowledgment Number(ACK):就是用来存放客户端发来的seqNumber的下一个信号(seqNumber+1)。只有当TCP flags中的ACK为1时才有效。主要用来解决不丢包的问题。
  4. TCP flags:TCP中有6个首部,用来控制TCP连接的状态,取值为0或1。分别是:URG、ACK、PSH、RST、SYN、FIN:
    1. URG为1时,用来保证TCP连接不被中断。并且将该次TCP内容数据的紧急程度提升(即告诉计算机,首先处理该连接)
    2. ACK通常是服务端返回的。用来表示应答是否有效。
    3. PSH表示当数据包得到后,立马给应用程序使用(PUSH到最顶端)
    4. RST用来确保TCP连接的安全。该flag用来表示一个连接复位的请求。如果发生错误连接,则reset一次,重新连。同时可以用来拒绝非法数据包。
    5. SYN同步的意思,通常由客户端发出,用来建立连接。第一次握手时:SYN为1,ACK为0;第二次握手时:SYN为1,ACK为1。
    6. FIN用来表示是否结束该次TCP连接。通常当你的数据发送完后,会自动带上FIN然后断开连接。

TCP连接的建立与终止

TCP是一个面向连接的协议,所以在连接双方发送数据前,都需要建立一条连接。TCP连接的建立需要3次握手,终止需要4次握手。

建立连接

在建立连接时,客户端首先向服务器申请打开某一个端口(用SYN段等于1的TCP报文),然后服务器返回一个ACK报文通知客户端请求报文收到,客户端收到确认报文以后再次发送一个确认报文确认刚才服务器发出的确认报文,至此,连接建立完成,被称为3次握手。如果打算让双发都做好准备的话,一定要发送三次报文,而且只需要三次报文就可以了。

结束连接

TCP有一个特别的概念叫做half-close,TCP的连接是全双工(可以同时接收和发送)连接,因此在关闭连接的时候,必须关闭传个送两个方向上的连接。客户端给服务器一个FIN为1的TCP报文,然后服务器返回一个确认ACK报文,并且发送一个FIN报文,当客户机回复ACK报文后,连接就结束了。

最大报文长度

在建立连接时,通信的双方要互相确认对方的最大报文长度(MSS),以便通信,一般这个SYN长度是MTU长度减去固定IP首都和TCP首部长度。对于一个以太网,一般可以达到1460字节。当然如果对于非本地的IP,这个MSS可能只有536字节,而且,如果中间的传输网络的MSS更小的话,这个值会变得更小。

TCP的状态迁移图

包含两个部分,服务器状态和客户端状态,如果从某一个角度会更加清晰,这里面的服务器和客户端都不是绝对的,发送数据的就是客户端,接收数据的就是服务器。

NAME

另一种描述方式:

NAME

客户端路线

客户端状态可以用一下流程来表示:

CLOSER --> SYN_SENT -->ESTABLISHED --> FIN_WAIT_1 --> FIN_WAIT_2 --> TIME_WAIT -- CLOSED

该流程是在程序正常时应该有的流程,在建立连接时,当客户端收到SYN的ACK报文以后,客户端就打开了数据交互的连接。而结束连接则通常是客户端主动结束的,客户端结束应用程序以后,需要经历FIN_WAIT_1FIN_WAIT_2等状态,这些状态的迁移就是前面提到的结束连接的4次握手。

服务器路线

服务器状态的流程:

CLOSED --> LISTEN --> SYN收到 --> ESTABLISHED --> CLOSE_WAIT --> LAST_ACK --> CLOSED

在建立连接的时候,服务器端就是在三次握手之后才进入数据交互状态,而关闭连接则是在关闭连接的第二次握手之后,而不是第四次握手之后。关闭以后还要等待客户端给出最后的ACK才能进入初始状态。

建立连接的三次握手流程

  1. 第一次握手:客户端向服务端发送一个SYN包,并且添加上seqNumber(假设为x),然后进入SYN_SEND状态,并且等待服务器的确认;
  2. 第二次握手:服务器接收SYN包,并进行确认,如果该请求有效,则将TCP flags中的ACK标记为1,然后将AckNumber置为(seqNumber+1),并且再添加上自己的seqNumber(y),完成后,返回给客户端。服务器进入SYN_RECV状态(这里服务端是发送SYN+ACK包);
  3. 第三次握手:客户端接收ACK+SYN报文后,获取到服务器发送的AckNumber(y),并且将新头部的AckNumber变为(y+1),然后发送给服务端,完成TCP的三次握手,建立连接。此时服务器和客户端都进入ESTABLISHED状态。

关闭连接的四次挥手流程

  1. 第一次挥手:A机感觉此时如果keepalive比较浪费资源,则它提出了分手的请求。设置SeqNumber和AckNumber之后,向B机发送FIN包,表示我这已经没有数据给你了,然后A机进入FIN_WAIT_1状态;
  2. 第二次挥手:B机收到了A机的FIN包,已经知道了A机没有数据再发送了。此时B会给A发送一个ACK包,并且将AckNumber变为A传输来的SeqNumber+1。当A接收到之后,则变为FIN_WAIT_2状态。表示已经得到B机的许可,可以进行关闭操作。不过此时,B机还是可以向A机发送请求的。
  3. 第三次挥手:B机向A机发送FIN包,请求关闭,相当于告诉A机,我这里也没有你要的数据了。然后B进入CLOSE_WAIT状态(同时带上SeqNumber);
  4. 第四次挥手:A接收到B的FIN包之后,然后同样,发送一个ACK包给B。B接收到之后就断开了。而A会等待2MSL的时间之后,如果没有回复,确保服务端确实是关闭了。然后A机也可以关闭连接。A、B都进入CLOSE状态。

2MSL的意思是2 x MSL。MSL的其实是 ”Maximum Segment Lifetime“,报文最大生存时间。RFC793中规定为2分钟,实际应用中常用的是30秒、1分钟等。同样上面的TIME_WAIT状态其实也就是2MSL状态,如果超过该时间,则会将报文丢弃,直接进入CLOSE状态。

其他状态迁移

图中还有一些其他状态的迁移,针对服务端和客户端两方面总结如下:

  1. LISTEN --> SYN_SENT:指服务器有时候也需要打开连接
  2. SYN --> SYN收到:服务器和客户端在SYN_SENT状态下如果收到SYN数据报,则都需要发送SYN的ACK数据报并把自己的状态调整到SYN收到状态,准备进入ESTABLISHED
  3. SYN_SENT --> CLOSED:才发送超时的情况下,会返回到CLOSED状态
  4. SYN收到 --> LISTEN:如果收到RST包,会返回到LISTEN状态
  5. SYN收到 --> FIN_WAIT_1:这个迁移是说,可以不用到ESTABLISHED状态,可以直接跳转到FIN_WAIT_1状态并等待关闭

2MSL等待状态

图中有一个TIME_WAIT等待状态,又称为2MSL状态,说的是在TIME_WAIT_2发送了最后一个ACK数据报以后,要进入TIME_WAIT状态,这个状态是防止最后一次握手的数据报没有传送到对方那里准备的(注意这不是4次握手,这是第4次握手的保险状态),这个状态在很大程度上都保证了双方都可以正常结束,但是也伴随着问题。

由于插口的2MSL状态(插口是IP和端口对的意思,socket),使得应用程序在2MSL时间内无法再次使用同一个插口对,对于客户端程序还好,但是对于服务器程序,例如httpd,他总是要使用同一个端口来进行服务,而在2MSL时间内,启动httpd就会出现错误(插口被使用)。为了避免这个错误,服务器给出了一个平静时间的概念,在2MSL时间内,虽然可以重新启动服务器,但是这个服务器还是要平静的等待2MSL时间的过去才能进行下一次连接。

FIN_WAIT_2状态

这是著名的半关闭状态,在关闭连接时,客户端和服务器两次握手之后的状态。这个状态下,应用程序还有接收数据的能力,但是已经无法发送数据,但是也有一种可能,客户端一直处于FIN_WAIT_2状态,而服务器一直出去WAIT_CLOSE状态,而直到应用层来决定关闭这个状态。

RST,同时打开和同时关闭

RST是另一种关闭连接的方式,应用程序可以判断RST包的真实性,即是否为异常终止。而同时开发和同时关闭时两种特殊的TCP状态,发生的概率很小。

TCP服务器设计

在前面的UDP服务器设计中,完全不需要所谓的并发机制,它只需要建立一个数据输入队列就可以。但是TCP不同,TCP服务器对于每一个连接都需要建立一个独立的进程(或者轻量级的线程),来宝成对话的独立性。所以TCP服务器是并发的。而TCP服务器还需要配备一个呼入连接请求队列,来为每一个连接请求建立对话进程,这也就是为什么各种TCP服务器都有一个最大连接数的限制。而根据源主机的IP和端口号,服务器可以很轻松的区别不同的会话,来进行数据的分发。

TCP交互数据流、成块数据流

目前建立在TCP协议上的网络协议很多,有telnet、ssh、ftp、http等。这些协议又可以根据数据吞吐量大致分为两类:

  1. 交互数据类型:例如telnet、ssh,这种协议通常只做小流量的数据交换,比如按下键盘,回显文字等;
  2. 数据成块类型:例如ftp。这种类型的协议要求TCP尽量的运载数据,把数据的吞吐量做到最大,并尽可能的提高效率。

TCP的交互流数据

对于交互性要求比较高的应用,TCP给出了两个策略来提高效率和减少网络负担:捎带ACK、Nagle算法(一次尽量多的发数据)。通常在网络速度很快的情况下,比如用lo接口进行telnet通信,当按下字母键并要求回显的时候,客户端和服务器将经历发送按键数据 --> 服务器发送按键数据的ACK --> 服务器端发送回显数据 --> 客户端发送回显数据的ACK的过程,而其中的数据流将是40bit + 41bit + 41bit + 40bit = 162bit,如果在广域网里面,这种小分组的TCP流量将会造成很大的网络负担。

捎带ACK的发送方式

这个策略是说,当主机收到远程主机的TCP数据报的时候,通常不马上发送ACK数据报,而是登上一个短暂的时间,如果这段时间内主机还有发送到远程主机的TCP数据报,那么就把这个ACK数据报捎带着发过去,把原本两个数据报整合成一个发送。一般这个时间是200ms。可以很明显的看到这个策略把TCP的数据报的利用率提高很多。

Nagle算法

Nagle算法是指,当A给B发送了一个TCP数据报并进入等待B的ACK数据报的状态时,TCP的输出缓冲区中只能有一个TCP数据报,并且,这个数据报不断的收集后来的数据,整合成一个大的数据报,等到B的ACK包一到,就把这些数据一股脑的发送出去。

在编写接口程序的时候,可以通过TCP_NODELAY来关闭这个算法。同时使用这个算法需要据情况而定,比如基于TCP的X窗口协议,如果处理鼠标事件还是用这个算法的话延迟就会非常大了。

TCP的成块流数据

对于FTP这样对数据吞吐量有较高的要求,将总是希望每次尽量多的发送数据到对方主机,就算是有点延迟也无所谓。TCP也提供了一整套的策略来支持这样的需求。TCP协议中有16个bit表示窗口的大小,这是这些策略的核心。

传输数据是ACK的问题

在解释滑动窗口前,需要看看ACK的应答策略,一般来说,发送端发送一个TCP数据报,那么接收端就应该发送一个ACK数据报。但是事实上并不是这样,发送端将会连续发送数据尽量填满接收方的缓冲区,而接收方只要对这些数据发送一个ACK报文来回应就可以了,这就是ACK的累积特性,这个特性大大减少了发送端和接收端的负担。

滑动窗口

滑动窗口本质上是描述接收方的TCP数据报缓冲区大小的数据,发送方根据这个数据来计算自己最多能发送多长的数据。如果发送方收到接收方的窗口大小为0的TCP数据报,那么发送方将停止发送数据,等到接收方发送窗口大小不为0的数据报的到来。

关于滑动窗口协议,还有三个术语:

  1. 框框合拢:当窗口从左边向右边靠近的时候,这种现象发生在数据报被发送和确认的时候;
  2. 窗口张开:当窗口的右边沿向右边移动的时候,这种现象发生在接收端处理了数据以后;
  3. 窗口收缩:当窗口的右边沿向左边移动的时候,这种现象不常发生。

TCP就是利用这个窗口,慢慢的从数据的左边移动到右边,把处于窗口范围内的数据发送出去(但不是发送所有,只是处于窗口内的数据可以发送)。这就是窗口的意义。窗口的大小是可以通过socket来指定的,4096并不是最理想的窗口大小,而16384则可以使吞吐量大大的增加。

数据拥堵

上面的策略用于局域网内传输还可以,但是用在广域网中就可能出现问题,最大的问题就是当传输时出现了瓶颈(比如一定要经过一个slip低速链路)所产生的大量数据拥堵问题,为了解决这个问题,TCP发送方需要确认连接双方的线路的数据最大吞吐量是多少。

拥堵窗口的原理很简单,TCP发送方首先发送一个数据报,然后等待对方的回应,得到回应后就把这个窗口的大小加倍,然后连续发送两个数据报,等到对方回应以后,再把这个窗口加倍(先是2的指数倍,到了一定程度后变成线性增长,即慢启动),发送更多的数据报,直到出现超时错误。这样,发送端就了解了通信双方的线路承载能力,也就确定了拥堵窗口的大小,发送方就用这个拥堵窗口的大小发送数据。比如下载的时候一开始很慢,慢慢加速后变成匀速。

TCP的超时与重传

超时重传是TCP保证数据可靠性的另一个重要机制。其原理是在发送某一个数据以后就开启一个计时器,在一定的时间内如果没有得到发出数据的ACK报文,就重新发送数据,直到发送成功。

超时

超时时间的计算是超时的核心部分,TCP要求这个算法能大致估算出当前的网络状况,虽然这确实很困难。要求精确的原因有两个:1、定时长久会造成网络利用率不高;2、定时太短会造成多次重传,使得网络拥堵。所以书中(《TCP/IP详解:卷一》)给出了一套经验公式,和其他的保证计时器准确的措施。

递推公式概述

最早的TCP计算网络状况的公式:

R<-aR+(1-a)M
RTP=Rb

其中a是一个经验系数0.1,b通常为2,注意这是经验,没有推导过程,这个数值是可以被修改的。这个公式是说用旧的RTT(R)和新的RTT(M)综合到一起考虑新的RTT(R)的大小。但是,这种估计在网络变化很大的情况下完全不能做出灵敏的反应,于是就有下面的修正公式:

Err=M-A
A<-A+gErr
D<-D+h(|Err|-D)
RTO=A+4D

详细解释参考P228。这个递推公式甚至提到了方差这种统计概念,使得偏差更小。而且,必须要指出的是,这两组公司更新,都是在数据成功传输的情况下才进行,在发生数据重新传输的情况下,并不使用上面的公式进行网络国际,理由很简单,因为程序已经不再正常状态下了,估计出来的数据也是没有意义的。

RTO的初始化

RTO的初始化是由公式决定的,例如最初的公式,初始的值应该是1。而修正公式,初始RTO应该是A+4D。

RTO的更新

当输出传输正常的情况下,我们就会用上面的公式来更新各个数据,并重开定时器,来保证下一个数据被顺利传输。要注意的是:**重传的情况下,RTO不用上面的公式计算,而是采用一种叫”指数退避“的方式。**例如:当RTO为1S的情况下,发生了数据重传,我们就用RTO=2S的定时器来重新传输数据,下一次用4S。一直增加到64S为止。

估计器的初始化

在这里,SYN用的估计器初始化似乎和传输用的估计器不一样???

估计器的更新

Karn算法

应该称为一个策略,说的是更新RTO和估计器的时机选择问题。

计时器的使用

  1. 一个连接中,有且仅有一个测量定时器被使用。也就是说,如果TCP连续发出三组数据,只有一组数据会被测量;
  2. ACK数据报不会被测量,原因很简单,没有ACK的ACK回应可以供结束定时器测量。

重传

有了超时就有重传,但是会根据一定的策略重传,而不是将数据简单的发送。

重传时发送数据的大小

前面曾经提到过,数据在传输时不能只是用一种窗口协议,我们还需要有一个拥堵窗口来控制数据的流量,使得数据不会一下子都跑到网络中引起拥堵。也提到过,拥堵窗口最初使用指数增长的速度来增加自身的窗口,直到发生超时重传,在进行一次微调。但是没有提到,如何进行微调,拥塞避免算法和慢启动门限就是为此而生。

慢启动门限是说,当拥堵窗口超过这个门限的时候,就使用拥塞避免算法,而在门限以内就使用慢启动算法。所以这个标准才叫做门限,通常,拥塞窗口记做cwnd,慢启动门限记做ssthresh。

算法概要:

  1. 对一个给定的连接,初始化cwnd为1个报文段,ssthresh为65535字节;
  2. TCP输出历程的输出不能超过cwnd和接收方通告窗口的大小。拥塞避免是发送方使用的流量控制,而通告窗口则是接收方进行的流量控制。前者是发送发感受到网络拥堵的估计,而后者则与接收方在该连接上的可用缓存大小有关;
  3. 当拥堵发生时(超时或收到重复确认),sshthresh被设置为当前窗口大小的一半(cwnd和接收方通告窗口大小的最小值,但最少为2个报文段)。此外,如果是超时引起了阻塞,则cwnd被设置为一个报文段(这就是慢启动)。
  4. 当心的数据被对方确认时,就增加cwnd,但增加的方法依赖于我们是否正在进行慢启动或拥塞避免。如果cwnd小于或等于ssthresh,则正在进行慢启动,否则正在进行拥塞避免。慢启动一直持续到我们回到拥发生时所处位置的半时候才停止(因为我们记录了在步骤2中给我们制造麻烦的窗口大小的一半),然后转为执行拥塞避免。

快速重传和快速恢复算法

这是数据丢包的情况下给出的一种修补机制。一般来说,重传发生在超时之后,但是如果发送端收到超过3个以上的重复ACK的情况下,就应该意识到,数据丢了,需要重新传递。这个机制是不需要等到重传计时器溢出的,所以叫做快速重传,而重新传递以后,因为走的不是慢启动而是拥塞避免算法,所以又被称为快速恢复算法。流程如下:

  1. 当收到3个重复的ACK时,将ssthresh设置为当前拥堵窗口cwnd的一半,重传丢失的报文段。设置cwnd为ssthresh加上3倍的报文段大小;
  2. 每次收到另一个重复的ACK时,cwnd增加1个报文段大小并发送一个分组(如果新的cwnd允许发送);
  3. 当下一个确认新数据的ACK到达时,设置cwnd为ssthresh(在第一步中设置的值)。这个ACK应该是在重传后的一个往返时间内对步骤1中重传的确认。另外,这个ACK应该是对丢失的分组和收到的第一个重复的ACK之间的所有中间报文段的确认。这一步采用的是拥塞避免,因为当分组丢失时我们将当前的速率减半。

ICMP会引起重新传递吗

不会,TCP会坚持自己的定时器,但是TCP会保留下ICMP的错误并通知用户。

重新分组

TCP为了提高自己的效率。允许再重新传输的时候,只要传输包含重传数据报文的报文就可以,而不用只重传需要传输的报文。

TCP坚持定时器、TCP保活定时器

TCP一共提供了四个主要的定时器,前面已经说过最复杂的超时定时器,另外的三个是:

  1. 坚持定时器
  2. 保活定时器
  3. 2MSL定时器

坚持定时器

当TCP服务器收到了客户端的0滑动窗口报文时,就启动一个定时器计时,并在定时器溢出的时候想客户端查询窗口是否已经增大,如果得到非0的窗口就重新开始发送数据,如果得到0窗口就再开一个新的定时器准备下一次查询。通过观察可知,TCP的坚持定时器使用1、2、4、8、16、…64秒这样的普通指数退避序列来作为每一次的溢出时间。

糊涂窗口综合征

TCP的窗口协议,会引起一种叫做糊涂窗口综合征的问题,具体表现为,当客户端通告一个小的非0窗口时,服务器立即发送小数据给客户端并充满气缓冲区,一来二去就会让网络中充满小TCP数据报,从而影响网络利用率。对于发送方和接收端的这种糊涂行为,TCP给出了一些建议、规定:

  1. 接收方不通告小窗口。通常的算法是接收方不通告一个比当前窗口大的窗口(可以为0),除非窗口可以增加一个报文段大小(也就是将要接收的MSS),或者可以增加接收方缓存空间的一半,不论实际有多少;
  2. 发送方避免出现糊涂窗口综合症的措施是只有一下条件之一满足时才发送数据:
    • 可以发送一个满长度的报文段
    • 可以发送至少是接收方通告窗口大小一半的报文段
    • 可以发送任何数据并且不希望接收ACK(也就是说,我们还没有未被确认的数据)或者该连接上不能使用Nagle算法

可以发现TCP的很多规定都是为了在一次发送中发送尽量多的数据,例如捎带ACK的策略,Nagle算法,重传时发送包含数据报文的策略,等等。

保活定时器

保活定时器更加简单,还记得FTP或者HTTP服务器都有Session Time机制吗?因为TCP是面向连接的,所以就会出现只连接不传数据的”半开放连接“,服务器当然要检测这种连接并且在某些情况下释放这些连接,这就是保活定时器的作用。其时限根据服务器的实现不同而不同。另外,当其中一端如果崩溃并重启的情况时,如果收到该端”前生“的保活探查,则要发送一个RST数据报文帮助另一端结束连接。