浅析TCP协议

一、TCP简介

1、TCP与UDP

TCP与UDP作为运输层的协议,经常被用来比较,它们有着各自的特点。

UDP是一种不提供不必要服务的轻量级运输协议,它仅提供最小服务。

  • UDP是无连接的,因此在两个进程通信前没有握手过程。
  • UDP提供不可靠的数据传送服务。
  • UDP也不保证输出顺序。

TCP是因特网运输层的面向连接的可靠的运输协议,主要有一下几个特点

  1. 面向连接的服务:在应用层的数据报开始流动之前,TCP让客户和服务器之间相互交换运输层控制信息。在握手阶段后,一个TCP连接就在两个进程的套接字之间建立了。而且这条接连是全双工的。
  2. 可靠的数据传送服务:通信进程能够依靠TCP无差错、按适当顺序交付所有发送的数据。
  3. TCP还具有拥塞控制机制,这种机制虽然不一定能为通信进程带来直接好处,但能为因特网带来整体好处。

2、TCP报文

pic3

上图为TCP报文段结构,一般TCP的首部是20个字节,而UDP只有8个字节。

具体的含义不用每个都知道,了解几个比较常用的就可以了。

源端口与目的端口

主要用于多路复用与分解

序号和确认号

序号和确认号TCP报文段首部中两个最重要的字段,它们是TCP可靠传输的关键部分。

序列号用来标识TCP发端向TCP收端发送的数据字节流。

确认号只有在ACK标志位为1的时候才有效。同时,它也表示从期望接收端接收的下一个字节的序列号

标志字段

ACK用于指示确认号是有效的,即该报文包括一个对已被接收报文段的确认。

RST、SYN、FIN用于连接建立和拆除,也就是三次握手和四次挥手。

二、连接管理

1、三次握手

接下来就是最著名的TCP三次握手

pic4

  • 第一步: 客户端TCP向服务器发送一个特殊的TCP报文。该报文不含应用层数据,但有两个关键信息。将首部中一个标志位SYN置为1,因此称此报文段为SYN报文段。另外,客户端会随机选取(为了避免安全性攻击)一个初始序列号client_isn,一起发送给服务器端。

  • 第二步: 服务器端接收到该SYN报文段后,就会分配TCP缓存和变量(没错,在三次握手完成之前就会分配缓存和变量,这也使得TCP容易遭受SYN洪泛攻击),然后返回一个也是不含应用层信息的报文段。

    这个报文段包含三个重要信息:首先,SYN比特位被置为1。其次,报文段的确认号字段被置为client_isn+1。最后,服务器选择自己的初始序列号server_isn。这个报文段被称为SYNACK报文段。

  • 第三步: 在收到SYNACK报文段后,客户端也开始分配缓存和变量,然后再向服务器发送最后一个确认报文段。因为连接已经建立,所以SYN会置为0,然后将server_isn+1作为确认号字段。

2、SYN洪泛攻击

攻击原理:

SYN Flood是当前最流行的DoS(拒绝服务攻击)与DDoS(分布式拒绝服务攻击)的方式之一,这是一种利用TCP协议缺陷,发送大量伪造的TCP连接请求,常用假冒的IP或IP号段发来海量的请求连接的第一个握手包(SYN包),被攻击服务器回应第二个握手包(SYN+ACK包),因为对方是假冒IP,对方永远收不到包且不会回应第三个握手包。导致被攻击服务器保持大量SYN_RECV状态的“半连接”,并且会重试默认5次回应第二个握手包,塞满TCP等待连接队列,资源耗尽(CPU满负荷或内存不足),让正常的业务请求连接不进来。

防御手段:

一种比较有效的防御手段就是SYN cookie。

  • 当服务器收到一个SYN请求时,它并不知道这个请求是合法的还是攻击的,所以它不会直接分配缓存和变量。而是生成一个初始的TCP序列号,该序列号由SYN请求报文段的源和目的IP与端口号以及仅有服务器知道的秘密数经过一个复杂的哈希函数计算而成。这个被精心计算出来的序列号就被称为“cookie”,服务器将含有这个cookie的SYNACK分组返回给客户端。

  • 如果客户是合法的,那么它将会返回一个ACK报文段。服务器收到ACK后需要验证它与前面的某个SYN是对应的,这时就要用到上面的cookie了。

    一个合法的ACK的确认字段,肯定是服务器返回的SYNACK中序号字段(就是上面计算出来的cookie)的值+1。所以服务器只需要再次根据源和目的IP与端口号按照上面的方法计算出cookie,因为源和目的IP与端口号没有改变,所以计算出来的cookie肯定与上面的相同,如果这个cookie值+1与ACK报文段中的确认字段相等,你们服务器就认为该ACK对应于前面的SYN,因此它是合法的。然后生成一个具有套接字的全开连接。

    如果客户是非法的也就是没有返回ACK报文段,那么服务器也并没有为它分配任何资源。

3、四次挥手

接下来看一下TCP断开连接的过程,需要注意的一点是下图中的客户端与服务器不是绝对的,实际情况中任何一端都可以请求中断,我们习惯把先请求中断的叫做客户端。

pic5

  • 首先,客户端发送一个特殊的报文段,将首部中的FIN标志位置为1,说明客户端已经没有数据要发送了。该报文段也叫FIN报文段。
  • 服务器接受到FIN报文段后,回复一个确认报文段。但是服务器仍然可以向客户端发送数据。
  • 当服务器也没有数据要发送了,就会同样向客户端发送一个FIN报文段。
  • 最后,客户端对这个FIN报文段进行确认,在上图中可以看到,客户端发送ACK后没有直接关闭,而是计时等待一段时间后再关闭。最后,两台主机上的所有资源都被释放了。

3、状态序列

整个过程Client端所经历的状态如下:

pic7

而Server端所经历的过程如下:

pic6

4、常见问题

4.1、为什么是三次握手而不是两次?

直接引用谢希仁版《计算机网络》中的例子:

“已失效的连接请求报文段”的产生在这样一种情况下:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送ack包。

4.2、为什么是四次挥手三次握手

因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,”你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

4.3、计时等待

在第四步中,客户端发送ACK报文段后会进入TIME_WAIT状态,这是因为如果客户端的ACK如果丢失了,它将重新发送,TIME_WAIT状态中所需要的时间是依赖于实现方法的。典型的值为30秒、1分钟和2分钟。等待之后连接正式关闭,并且所有的资源(包括端口号)都被释放。

4.4、CLOSE_WAIT状态

在小米一面的时候面试官问知不知道CLOSE_WAIT状态,我回答说是四次挥手中的状态,然后解释了四次挥手的全过程。不知道面试官想问的到底是什么,后来上网查了一下发现关于这个状态是有一个比较常见的问题的,在这个http://blog.csdn.net/ximenying/article/details/1489810链接中。

问题描述:

不久前,我的Socket Client程序遇到了一个非常尴尬的错误。它本来应该在一个socket长连接上持续不断地向服务器发送数据,如果socket连接断开,那么程序会自动不断地重试建立连接。

有一天发现程序在不断尝试建立连接,但是总是失败。用netstat查看,这个程序竟然有上千个socket连接处于CLOSE_WAIT状态,以至于达到了上限,所以无法建立新的socket连接了。

CLOSE_WAIT产生的原因:

当发起主动关闭的左边这方发送一个FIN过去后,右边被动关闭的这方要回应一个ACK,这个ACK是TCP回应的,而不是应用程序发送的,此时,被动关闭的一方就处于CLOSE_WAIT状态了。如果此时被动关闭的这一方不再继续调用closesocket,那么他就不会发送接下来的FIN,导致自己老是处于CLOSE_WAIT。只有被动关闭的这一方调用了closesocket,才会发送一个FIN给主动关闭的这一方,同时也使得自己的状态变迁为LAST_ACK。

我觉得产生这个的原因应该就是:一端的Socket调用close后,另一端的Socket没有调用close

4.5、长连接与短连接

面试官在问完上面那个问题后又问我知不知道长连接与短连接,所以我觉得这两者之间应该是有关联的面试官在提示我,上面那个问题描述中也说到socket长连接上不断发送数据,二者的定义如下:

  • 长连接:client方与server方先建立连接,连接建立后不断开,然后再进行报文发送和接收。这种方式下由于通讯连接一直存在。此种方式常用于P2P通信。
  • 短连接:Client方与server每进行一次报文收发交易时才进行通讯连接,交易完毕后立即断开连接。此方式常用于一点对多点通讯。

操作过程:

  • 短连接:建立连接——数据传输——关闭连接…建立连接——数据传输——关闭连接
  • 长连接:建立连接——数据传输…(保持连接)…数据传输——关闭连接

使用时机:

  • 长连接:短连接多用于操作频繁,点对点的通讯,而且连接数不能太多的情况。每个TCP连接的建立都需要三次握手,每个TCP连接的断开要四次握手。如果每次操作都要建立连接然后再操作的话处理速度会降低,所以每次操作下次操作时直接发送数据就可以了,不用再建立TCP连接。
  • 短连接:web网站的http服务一般都用短连接。因为长连接对于服务器来说要耗费一定的资源。像web网站这么频繁的成千上万甚至上亿客户端的连接用短连接更省一些资源。
    试想如果都用长连接,而且同时用成千上万的用户,每个用户都占有一个连接的话,可想而知服务器的压力有多大。所以并发量大,但是每个用户又不需频繁操作的情况下需要短连接。总之:长连接和短连接的选择要视需求而定。

三、流量控制

前面提到TCP在主机会建立缓存来接收数据,然后应用程序从缓存中将接收到的数据读出,所以如果应用程序由于某种原因无法立刻读取数据,而发送方又一直发送数据,就会使缓存溢出。在UDP中,这种现象确实会发生,但是TCP就不会,因为为了防止这种情况,TCP为应用程序提供了流量控制服务。

1、作用

流量控制很容易与下面的拥塞控制混淆,虽然它们都是用于决定向对方发送数据的速率,但是它们是针对完全不同的原因而采取的措施。

流量控制是为了防止接收方缓存溢出而对发送方进行遏制。

拥塞控制是防止IP网络拥塞而对发送方进行遏制。

2、原理

TCP通过让发送方维护一个称为接收窗口(rwnd)的变量来提供流量控制。它表示接收方还有多少可用的缓存空间。

以主机A与B的TCP连接为例

  • 主机B把当前的rwnd值放入发送给A的报文段接收串口字段中,通知A它在该连接中还有多少缓存可用。
  • A维护两个变量,表示A发送到连接中但是没有被确认的数据量,通过将未确认的数据量控制在rwnd以内,就可以保证A不会使B的接收缓存溢出。
  • 有一个小问题就是当B的接收缓存已满使得rwnd为0,那么A就不会再向B发送数据。这时如果B没有其他数据发送到A,那么即使它缓存出现空闲可以继续接收它也无法通知A,就会导致A一直无法发送数据。为例防止这种情况,TCP规范中要求:当B的接收窗口为0时,A继续发送只有一个字节数据的报文段。这些报文段会被接收方确认,如果缓存出现空闲,那么确认报文段中将含有一个非0的rwnd值,连接就可以继续了。

四、拥塞控制

TCP还拥有拥塞控制机制,通过让发送方根据所感知到的网络拥塞程度来限制其能向连接方发送流量的速率。要实现该机制,只要考虑一下三个问题

1、发送方如何限制发送速率

TCP的发送方还会维持一个变量叫拥塞窗口(cwnd),它与上面的rwnd很相似,都是对TCP发送方能向网络中发送的流量进行限制。在一个发送方中未被确认的数据量不会超过cwnd与rwnd的最小值。

2、如何感知网络拥堵

首先定义发送方丢包事件:要么出现超时,要么收到来自接收方的3个冗余ACK。

当出现过度拥塞时,沿着这条路径上的一台或多台路由器会出现缓存溢出,导致数据报被丢弃。丢弃的数据报会引发发送方的丢包事件,发送方就认为路径上出现了拥塞。

3、控制速率算法

TCP的拥塞控制算法很复杂,恕我表达能力有限,不想写了。记住几个关键词就行了,慢启动、拥塞避免和快速恢复。

慢启动:当TCP连接开始时,cwnd的值通常被初始化为一个非常小的值,如果没有拥塞就直接翻倍,也就是说虽然发送速率起始很慢,但是在慢启动阶段以指数增长。直到发生丢包事件,记录下慢启动阈值ssthresh,设置为cwnd的一半,然后再次将cwnd设置为那个很小的值。

拥塞避免:在记录了ssthresh后,在指数增长阶段如果cwnd的值等于ssthresh了,那么如果再次翻倍,可能还会导致拥塞,所以就不会再指数增长了,而是线性增长,就是拥塞避免状态。

正常来说,肯定是再次丢包后就重新开始慢启动了,但是它偏不。因为上面说到丢包有两个原因:超时或者受到三个冗余ACK。而对着两种情况的处理方法是不同的。对于超时,那么和慢启动一样,ssthresh更新为cwnd的一半,cwnd被设置为很小的值。但是如果是由冗余ACK造成的丢包,那么会将cwnd减一半,将ssthresh设置为cwnd的一半,然后进入快速恢复状态。

快速恢复:没太看明白。

五、TCP安全与SSL

见HTTPS

六、参考地址

http://blog.csdn.net/whuslei/article/details/6667471/

http://blog.sina.com.cn/s/blog_6d39b5be0101k6v4.html