TCP 协议讲解(合集)

关于Tcp封包、粘包、半包

转自 http://www.cnblogs.com/jiangtong/archive/2012/03/22/2411985.html


很多朋友已经对此作了不少研究,也花费不少心血编写了实现代码和blog文档。当然也充斥着一些各式的评论,自己看了一下,总结一些心得。

首先我们学习一下这些朋友的心得,他们是:

http://blog.youkuaiyun.com/stamhe/article/details/4569530

http://www.cppblog.com/tx7do/archive/2011/05/04/145699.html

//………………

当然还有太多,很多东西粘来粘区也不知道到底是谁的原作,J

看这些朋友的blog是我建议亲自看一下TCP-IP详解卷1中的相关内容【原理性的内容一定要看】。

TCP大致工作原理介绍:

工作原理

TCP-IP详解卷1第17章中17.2节对TCP服务原理作了一个简明介绍(以下蓝色字体摘自《TCP-IP详解卷1第17章17.2节》):

尽管T C PU D P都使用相同的网络层( I P),T C P却向应用层提供与U D P完全不同的服务。T C P提供一种面向连接的、可靠的字节流服务。

面向连接意味着两个使用T C P的应用(通常是一个客户和一个服务器)在彼此交换数据之前必须先建立一个T C P连接。这一过程与打电话很相似,先拨号振铃,等待对方摘机说“喂”,然后才说明是谁。在第1 8章我们将看到一个T C P连接是如何建立的,以及当一方通信结束后如何断开连接。

在一个T C P连接中,仅有两方进行彼此通信。在第1 2章介绍的广播和多播不能用于T C P

T C P通过下列方式来提供可靠性:

• 应用数据被分割成T C P认为最适合发送的数据块。这和U D P完全不同,应用程序产生的数据报长度将保持不变。由T C P传递给I P的信息单位称为报文段或段( s e g m e n t)(参见图1 - 7)。在1 8 . 4节我们将看到T C P如何确定报文段的长度。

• T C P发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。在第2 1章我们将了解T C P协议中自适应的超时及重传策略。

• T C P收到发自T C P连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒,这将在1 9 . 3节讨论。

• T C P将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错, T C P将丢弃这个报文段和不确认收到此报文段(希望发端超时并重发)。

• 既然T C P报文段作为I P数据报来传输,而I P数据报的到达可能会失序,因此T C P报文段的到达也可能会失序。如果必要, T C P将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层。

• 既然I P数据报会发生重复, T C P的接收端必须丢弃重复的数据。

• T C P还能提供流量控制。T C P连接的每一方都有固定大小的缓冲空间。T C P的接收端只允许另一端发送接收端缓冲区所能接纳的数据。这将防止较快主机致使较慢主机的缓冲区溢出。两个应用程序通过T C P连接交换8 bit字节构成的字节流。T C P不在字节流中插入记录标识符。我们将这称为字节流服务( byte stream service)。如果一方的应用程序先传1 0字节,又传2 0字节,再传5 0字节,连接的另一方将无法了解发方每次发送了多少字节。收方可以分4次接收这8 0个字节,每次接收2 0字节。一端将字节流放到T C P连接上,同样的字节流将出现在T C P连接的另一端。另外,T C P对字节流的内容不作任何解释。T C P不知道传输的数据字节流是二进制数据,还是A S C I I字符、E B C D I C字符或者其他类型数据。对字节流的解释由T C P连接双方的应用层解释。这种对字节流的处理方式与U n i x操作系统对文件的处理方式很相似。U n i x的内核对一个应用读或写的内容不作任何解释,而是交给应用程序处理。对U n i x的内核来说,它无法区分一个二进制文件与一个文本文件。

T C P如何确定报文段的长度

       我仍然引用官方解释《TCP-IP详解卷1》第18章18.4节:

最大报文段长度( M S S)表示T C P传往另一端的最大块数据的长度。当一个连接建立时【三次握手】,连接的双方都要通告各自的M S S。我们已经见过M S S都是1 0 2 4。这导致I P数据报通常是4 0字节长:2 0字节的T C P首部和2 0字节的I P首部。

在有些书中,将它看作可“协商”选项。它并不是任何条件下都可协商。当建立一个连

接时,每一方都有用于通告它期望接收的M S S选项(M S S选项只能出现在S Y N报文段中)。如果一方不接收来自另一方的M S S值,则M S S就定为默认值5 3 6字节(这个默认值允许2 0字节的I P首部和2 0字节的T C P首部以适合5 7 6字节I P数据报)

一般说来,如果没有分段发生, M S S还是越大越好(这也并不总是正确,参见图2 4 - 3和图2 4 - 4中的例子)。报文段越大允许每个报文段传送的数据就越多,相对I PT C P首部有更高的网络利用率。当T C P发送一个S Y N时,或者是因为一个本地应用进程想发起一个连接,或者是因为另一端的主机收到了一个连接请求,它能将M S S值设置为外出接口上的M T U长度减去固定的I P首部和T C P首部长度。对于一个以太网, M S S值可达1 4 6 0字节。使用IEEE 802.3的封装(参见2 . 2节),它的M S S可达1 4 5 2字节。

如果目的I P地址为“非本地的( n o n l o c a l )”,M S S通常的默认值为5 3 6。而区分地址是本地还是非本地是简单的,如果目的I P地址的网络号与子网号都和我们的相同,则是本地的;如果目的I P地址的网络号与我们的完全不同,则是非本地的;如果目的I P地址的网络号与我们的相同而子网号与我们的不同,则可能是本地的,也可能是非本地的。大多数T C P实现版都提供了一个配置选项(附录E和图E - 1),让系统管理员说明不同的子网是属于本地还是非本地。这个选项的设置将确定M S S可以选择尽可能的大(达到外出接口的M T U长度)或是默认值5 3 6

M S S让主机限制另一端发送数据报的长度。加上主机也能控制它发送数据报的长度,这将使以较小M T U连接到一个网络上的主机避免分段。

只有当一端的主机以小于5 7 6字节的M T U直接连接到一个网络中,避免这种分段才会有效。

如果两端的主机都连接到以太网上,都采用5 3 6M S S,但中间网络采用2 9 6M T U,也将会

出现分段。使用路径上的M T U发现机制(参见2 4 . 2节)是关于这个问题的唯一方法。

       以上说明MSS的值可以通过协商解决,这个协商过程会涉及MTU的值的大小,前面说了:【MSS=外出接口上的MTU-IP首部-TCP首部】,我们来看看数据进入TCP协议栈的封装过程:

       clip_image002

最后一层以太网帧的大小应该就是我们的出口MTU大小了。当目的主机收到一个以太网数据帧时,数据就开始从协议栈中由底向上升,同时去掉各层协议加上的报文首部。每层协议盒都要去检查报文首部中的协议标识,以确定接收数据的上层协议。这个过程称作分用( D e m u l t i p l e x i n g),图1 - 8显示了该过程是如何发生的。

clip_image004

那么什么是MTU呢,这实际上是数据链路层的一个概念,以太网和802.3这两种局域网技术标准都对“链路层”的数据帧有大小限制:

clip_image006

l         最大传输单元MTU

 

正如在图2 - 1看到的那样,以太网和8 0 2 . 3对数据帧的长度都有一个限制,其最大值分别是1 5 0 01 4 9 2字节。链路层的这个特性称作M T U,最大传输单元。不同类型的网络大多数都有一个上限。

如果I P层有一个数据报要传,而且数据的长度比链路层的M T U还大,那么I P层就需要进行分片( f r a g m e n t a t i o n),把数据报分成若干片,这样每一片都小于M T U。我们将在11 . 5节讨论I P分片的过程。

2 - 5列出了一些典型的M T U值,它们摘自RFC 1191[Mogul and Deering 1990]。点到点的链路层(如S L I PP P P)的M T U并非指的是网络媒体的物理特性。相反,它是一个逻辑限制,目的是为交互使用提供足够快的响应时间。在2 . 1 0节中,我们将看到这个限制值是如何计算出来的。在3 . 9节中,我们将用n e t s t a t命令打印出网络接口的M T U

l         路径MTU

 

当在同一个网络上的两台主机互相进行通信时,该网络的M T U是非常重要的。但是如果

两台主机之间的通信要通过多个网络,那么每个网络的链路层就可能有不同的M T U。重要的

不是两台主机所在网络的M T U的值,重要的是两台通信主机路径中的最小M T U。它被称作路

M T U

两台主机之间的路径M T U不一定是个常数。它取决于当时所选择的路由。而选路不一定

是对称的(从AB的路由可能与从BA的路由不同),因此路径M T U在两个方向上不一定是

一致的。

RFC 1191[Mogul and Deering 1990]描述了路径M T U的发现机制,即在任何时候确定路径

M T U的方法。我们在介绍了I C M PI P分片方法以后再来看它是如何操作的。在11 . 6节中,我

们将看到I C M P的不可到达错误就采用这种发现方法。在11 . 7节中,还会看到, t r a c e r o u t e程序

也是用这个方法来确定到达目的节点的路径M T U。在11 . 8节和2 4 . 2节,将介绍当产品支持路

M T U的发现方法时,U D PT C P是如何进行操作的。

TCP的超时与重传

前面谈到TCP如何保证传输可靠性是说到“当T C P发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段”,下面我看一下TCP的超时与重传。

T C P提供可靠的运输层。它使用的方法之一就是确认从另一端收到的数据。但数据和确认都有可能会丢失。T C P通过在发送时设置一个定时器来解决这种问题。如果当定时器溢出时还没有收到确认,它就重传该数据。对任何实现而言,关键之处就在于超时和重传的策略,即怎样决定超时间隔和如何确定重传的频率。

对每个连接,T C P管理4个不同的定时器。

1) 重传定时器使用于当希望收到另一端的确认。

2) 坚持( p e r s i s t )定时器使窗口大小信息保持不断流动,即使另一端关闭了其接收窗口。

3) 保活( k e e p a l i v e )定时器可检测到一个空闲连接的另一端何时崩溃或重启。

4) 2MSL定时器测量一个连接处于T I M E _ WA I T状态的时间。

T C P超时与重传中最重要的部分就是对一个给定连接的往返时间( RT T)的测量。由于路由器和网络流量均会变化,因此我们认为这个时间可能经常会发生变化, T C P应该跟踪这些变化并相应地改变其超时时间。

大多数源于伯克利的T C P实现在任何时候对每个连接仅测量一次RT T值。在发送一个报文段时,如果给定连接的定时器已经被使用,则该报文段不被计时。

具体RTT值的估算比较麻烦,需要可以参考《TCP-IP详解卷121章》

TCP经受延时的确认

交互数据总是以小于最大报文段长度的分组发送。对于这些小的报文段,接收方使用经受时延的确认方法来判断确认是否可被推迟发送,以便与回送数据一起发送。这样通常会减少报文段的数目       

通常T C P在接收到数据时并不立即发送A C K;相反,它推迟发送,以便将A C K与需要沿该方向发送的数据一起发送(有时称这种现象为数据捎带A C K)。绝大多数实现采用的时延为200 ms,也就是说,T C P将以最大200 ms 的时延等待是否有数据一起发送。

我们看看另一位朋友的blog对此的介绍:

摘要:当使用TCP传输小型数据包时,程序的设计是相当重要的。如果在设计方案中不对TCP数据包的 
延迟应答,Nagle算法,Winsock缓冲作用引起重视,将会严重影响程序的性能。这篇文章讨论了这些 
问题,列举了两个案例,给出了一些传输小数据包的优化设计方案。

背景:当Microsoft TCP栈接收到一个数据包时,会启动一个200毫秒的计时器。当ACK确认数据包 
发出之后,计时器会复位,接收到下一个数据包时,会再次启动200毫秒的计时器。为了提升应用程序 
在内部网和Internet上的传输性能,Microsoft TCP栈使用了下面的策略来决定在接收到数据包后 
什么时候发送ACK确认数据包: 
1
、如果在200毫秒的计时器超时之前,接收到下一个数据包,则立即发送ACK确认数据包。 
2
、如果当前恰好有数据包需要发给ACK确认信息的接收端,则把ACK确认信息附带在数据包上立即发送。 
3
、当计时器超时,ACK确认信息立即发送。 
为了避免小数据包拥塞网络,Microsoft TCP栈默认启用了Nagle算法,这个算法能够将应用程序多次 
调用Send发送的数据拼接起来,当收到前一个数据包的ACK确认信息时,一起发送出去。下面是Nagle 
算法的例外情况: 
1
、如果Microsoft TCP栈拼接起来的数据包超过了MTU值,这个数据会立即发送,而不等待前一个数据 
包的ACK确认信息。在以太网中,TCPMTU(Maximum Transmission Unit)值是1460字节。 
2
、如果设置了TCP_NODELAY选项,就会禁用Nagle算法,应用程序调用Send发送的数据包会立即被 
投递到网络,而没有延迟。 
为了在应用层优化性能,Winsock把应用程序调用Send发送的数据从应用程序的缓冲区复制到Winsock 
内核缓冲区。Microsoft TCP栈利用类似Nagle算法的方法,决定什么时候才实际地把数据投递到网络。 
内核缓冲区的默认大小是8K,使用SO_SNDBUF选项,可以改变Winsock内核缓冲区的大小。如果有必要的话, 
Winsock
能缓冲大于SO_SNDBUF缓冲区大小的数据。在绝大多数情况下,应用程序完成Send调用仅仅表明数据 
被复制到了Winsock内核缓冲区,并不能说明数据就实际地被投递到了网络上。唯一一种例外的情况是: 
通过设置SO_SNDBUT0禁用了Winsock内核缓冲区。

Winsock使用下面的规则来向应用程序表明一个Send调用的完成: 
1
、如果socket仍然在SO_SNDBUF限额内,Winsock复制应用程序要发送的数据到内核缓冲区,完成Send调用。 
2
、如果Socket超过了SO_SNDBUF限额并且先前只有一个被缓冲的发送数据在内核缓冲区,Winsock复制要发送 
的数据到内核缓冲区,完成Send调用。 
3
、如果Socket超过了SO_SNDBUF限额并且内核缓冲区有不只一个被缓冲的发送数据,Winsock复制要发送的数据 
到内核缓冲区,然后投递数据到网络,直到Socket降到SO_SNDBUF限额内或者只剩余一个要发送的数据,才 
完成Send调用。

案例
一个Winsock TCP客户端需要发送10000个记录到Winsock TCP服务端,保存到数据库。记录大小从20字节到100 
字节不等。对于简单的应用程序逻辑,可能的设计方案如下: 
1
、客户端以阻塞方式发送,服务端以阻塞方式接收。 
2
、客户端设置SO_SNDBUF0,禁用Nagle算法,让每个数据包单独的发送。 
3
、服务端在一个循环中调用Recv接收数据包。给Recv传递200字节的缓冲区以便让每个记录在一次Recv调用中 
被获取到。

性能: 
在测试中发现,客户端每秒只能发送5条数据到服务段,总共10000条记录,976K字节左右,用了半个多小时 
才全部传到服务器。

分析: 
因为客户端没有设置TCP_NODELAY选项,Nagle算法强制TCP栈在发送数据包之前等待前一个数据包的ACK确认 
信息。然而,客户端设置SO_SNDBUF0,禁用了内核缓冲区。因此,10000Send调用只能一个数据包一个数据 
包的发送和确认,由于下列原因,每个ACK确认信息被延迟200毫秒: 
1
、当服务器获取到一个数据包,启动一个200毫秒的计时器。 
2
、服务端不需要向客户端发送任何数据,所以,ACK确认信息不能被发回的数据包顺路携带。 
3
、客户端在没有收到前一个数据包的确认信息前,不能发送数据包。 
4
、服务端的计时器超时后,ACK确认信息被发送到客户端。

如何提高性能: 
在这个设计中存在两个问题。第一,存在延时问题。客户端需要能够在200毫秒内发送两个数据包到服务端。 
因为客户端默认情况下使用Nagle算法,应该使用默认的内核缓冲区,不应该设置SO_SNDBUF0。一旦TCP 
栈拼接起来的数据包超过MTU值,这个数据包会立即被发送,不用等待前一个ACK确认信息。第二,这个设计 
方案对每一个如此小的的数据包都调用一次Send。发送这么小的数据包是不很有效率的。在这种情况下,应该 
把每个记录补充到100字节并且每次调用Send发送80个记录。为了让服务端知道一次总共发送了多少个记录, 
客户端可以在记录前面带一个头信息。

案例二: 
一个Winsock TCP客户端程序打开两个连接和一个提供股票报价服务的Winsock TCP服务端通信。第一个连接 
作为命令通道用来传输股票编号到服务端。第二个连接作为数据通道用来接收股票报价。两个连接被建立后, 
客户端通过命令通道发送股票编号到服务端,然后在数据通道上等待返回的股票报价信息。客户端在接收到第一 
个股票报价信息后发送下一个股票编号请求到服务端。客户端和服务端都没有设置SO_SNDBUFTCP_NODELAY 
选项。

性能: 
测试中发现,客户端每秒只能获取到5条报价信息。

分析:

这个设计方案一次只允许获取一条股票信息。第一个股票编号信息通过命令通道发送到服务端,立即接收到 
服务端通过数据通道返回的股票报价信息。然后,客户端立即发送第二条请求信息,send调用立即返回, 
发送的数据被复制到内核缓冲区。然而,TCP栈不能立即投递这个数据包到网络,因为没有收到前一个数据包的 
ACK
确认信息。200毫秒后,服务端的计时器超时,第一个请求数据包的ACK确认信息被发送回客户端,客户端 
的第二个请求包才被投递到网络。第二个请求的报价信息立即从数据通道返回到客户端,因为此时,客户端的 
计时器已经超时,第一个报价信息的ACK确认信息已经被发送到服务端。这个过程循环发生。

如何提高性能: 
在这里,两个连接的设计是没有必要的。如果使用一个连接来请求和接收报价信息,股票请求的ACK确认信息会 
被返回的报价信息立即顺路携带回来。要进一步的提高性能,客户端应该一次调用Send发送多个股票请求,服务端 
一次返回多个报价信息。如果由于某些特殊原因必须要使用两个单向的连接,客户端和服务端都应该设置TCP_NODELAY 
选项,让小数据包立即发送而不用等待前一个数据包的ACK确认信息。

提高性能的建议: 
上面两个案例说明了一些最坏的情况。当设计一个方案解决大量的小数据包发送和接收时,应该遵循以下的建议: 
1、如果数据片段不需要紧急传输的话,应用程序应该将他们拼接成更大的数据块,再调用Send。因为发送缓冲区 
很可能被复制到内核缓冲区,所以缓冲区不应该太大,通常比8K小一点点是很有效率的。只要Winsock内核缓冲区 
得到一个大于MTU值的数据块,就会发送若干个数据包,剩下最后一个数据包。发送方除了最后一个数据包,都不会 
被200毫秒的计时器触发。 
2、如果可能的话,避免单向的Socket数据流接连。 
3、不要设置SO_SNDBUF为0,除非想确保数据包在调用Send完成之后立即被投递到网络。事实上,8K的缓冲区适合大多数 
情况,不需要重新改变,除非新设置的缓冲区经过测试的确比默认大小更高效。 
4、如果数据传输不用保证可靠性,使用UDP。

结论:

1.         TCP提供了面向“连续字节流”的可靠的传输服务,TCP并不理解流所携带的数据内容,这个内容需要应用层自己解析。

2.         “字节流”是连续的、非结构化的,而我们的应用需要的是有序的、结构化的数据信息,因此我们需要定义自己的“规则”去解读这个“连续的字节流“,那解决途径就是定义自己的封包类型,然后用这个类型去映射“连续字节流”。

如何定义封包,我们回顾一下前面这个数据进入协议栈的封装过程图:

clip_image007

封包其实就是将上图中进入协议栈的用户数据[即用户要发送的数据]定义为一种方便识别和交流的类型,这有点类似信封的概念,信封就是一种人们之间通信的格式,信封格式如下:

信封格式:

       收信人邮编

       收信人地址

       收信人姓名

       信件内容

那么在程序里面我们也需要定义这种格式:在C++里面只有结构和类这种两种类型适合表达这个概念了。网络上很多朋友对此表述了自己的看法并贴出了代码:比如

       /************************************************************************/

/* 数据封包信息定义开始                                                 */

/************************************************************************/

 

#pragma pack(push,1)   //将原对齐方式压栈,采用新的1字节对齐方式

 

/* 封包类型枚举[此处根据需求列举] */

typedef enum{

              NLOGIN=1,

              NREG=2,

              NBACKUP=3,

              NRESTORE=3,

              NFILE_TRANSFER=4,

              NHELLO=5

} PACKETTYPE;

 

/* 包头 */

typedef struct tagNetPacketHead{

       byte version;//版本

       PACKETTYPE ePType;//包类型

       WORD nLen;//包体长度

} NetPacketHead;

 

/* 封包对象[包头&包体] */

typedef struct tagNetPacket{

       NetPacketHead netPacketHead;//包头

       char * packetBody;//包体

} NetPacket;

 

#pragma pack(pop)

/**************数据封包信息定义结束**************************/

3.         发包顺序与收包问题

a)         由于TCP要通过协商解决发送出去的报文段的长度,因此我们发送的数据很有可能被分割甚至被分割后再重组交给网络层发送,而网络层又是采用分组传送,即网络层数据报到达目标的顺序完全无法预测,那么收包会出现半包、粘包问题。举个例子,发送端连续发送两端数据msg1和msg2,那么发送端[传输层]可能会出现以下情况:

                                       i.              Msg1和msg2小于TCP的MSS,两个包按照先后顺序被发出,没有被分割和重组

                                     ii.              Msg1过大被分割成两段TCP报文msg1-1、msg2-2进行传送,msg2较小直接被封装成一个报文传送

                                    iii.              Msg1过大被分割成两段TCP报文msg1-1、msg2-2,msg1-1先被传送,剩下的msg1-2和msg2[较小]被组合成一个报文传送

                                   iv.              Msg1过大被分割成两段TCP报文msg1-1、msg2-2,msg1-1先被传送,剩下的msg1-2和msg2[较小]组合起来还是太小,组合的内容在和后面再发送的msg3的前部分数据组合起来发送

                                     v.              ……………………….太多……………………..

b)        接收端[传输层]可能出现的情况

                                       i.              先收到msg1,再收到msg2,这种方式太顺利了。

                                     ii.              先收到msg1-1,再收到msg1-2,再收到msg2

                                    iii.              先收到msg1,再收到msg2-1,再收到msg2-2

                                   iv.              先收到msg1和msg2-1,再收到msg2-2

                                     v.              //…………还有很多………………

c)        其实“接收端网络层”接收到的分组数据报顺序和发送端比较可能完全是乱的,比如发“送端网络层”发送1、2、3、4、5,而接收端网络层接收到的数据报顺序却可能是2、1、5、4、3,但是“接收端的传输层”会保证链接的有序性和可靠性,“接收端的传输层”会对“接收端网络层”收到的顺序紊乱的数据报重组成有序的报文[即发送方传输层发出的顺序],然后交给“接收端应用层”使用,所以“接收端传输层”总是能够保证数据包的有序性,“接收端应用层”[我们编写的socket程序]不用担心接收到的数据的顺序问题。

d)        但是如上所述,粘包问题和半包问题不可避免。我们在接收端应用层需要自己编码处理粘包和半包问题。一般做法是定义一个缓冲区或者是使用标准库/框架提供的容器循环存放接收到数据,边接收变判断缓冲区数据是否满足包头大小,如果满足包头大小再判断缓冲区剩下数据是否满足包体大小,如果满足则提取。详细步骤如下:

1.         接收数据存入缓冲区尾部

2.         缓冲区数据满足包头大小否

3.         缓冲区数据不满足包头大小,回到第1步;缓冲区数据满足包头大小则取出包头,接着判断缓冲区剩余数据满足包头中定义的包体大小否,不满足则回到第1步。

4.         缓冲区数据满足一个包头大小和一个包体大小之和,则取出包头和包体进行使用,此处使用可以采用拷贝方式转移缓冲区数据到另外一个地方,也可以为了节省内存直接采取调用回调函数的方式完成数据使用。

5.         清除缓冲区的第一个包头和包体信息,做法一般是将缓冲区剩下的数据拷贝到缓冲区首部覆盖“第一个包头和包体信息”部分即可。

粘包、半包处理具体实现很多朋友都有自己的做法,比如最前面贴出的链接,这里我也贴出一段参考:

缓冲区实现头文件:

#include <windows.h>

 

#ifndef _CNetDataBuffer_H_

#define _CNetDataBuffer_H_

 

#ifndef TCPLAB_DECLSPEC

#define TCPLAB_DECLSPEC _declspec(dllimport)

#endif

 

/************************************************************************/

/* 数据封包信息定义开始                                                 */

/************************************************************************/

 

#pragma pack(push,1)   //将原对齐方式压栈,采用新的1字节对齐方式

 

/* 封包类型枚举[此处根据需求列举] */

typedef enum{

              NLOGIN=1,

              NREG=2,

              NBACKUP=3,

              NRESTORE=3,

              NFILE_TRANSFER=4,

              NHELLO=5

} PACKETTYPE;

 

/* 包头 */

typedef struct tagNetPacketHead{

       byte version;//版本

       PACKETTYPE ePType;//包类型

       WORD nLen;//包体长度

} NetPacketHead;

 

/* 封包对象[包头&包体] */

typedef struct tagNetPacket{

       NetPacketHead netPacketHead;//包头

       char * packetBody;//包体

} NetPacket;

 

#pragma pack(pop)

/**************数据封包信息定义结束**************************/

 

//缓冲区初始大小

#define BUFFER_INIT_SIZE 2048

 

//缓冲区膨胀系数[缓冲区膨胀后的大小=原大小+系数*新增数据长度]

#define BUFFER_EXPAND_SIZE 2

 

//计算缓冲区除第一个包头外剩下的数据的长度的宏[缓冲区数据总长度-包头大小]

#define BUFFER_BODY_LEN (m_nOffset-sizeof(NetPacketHead))

 

//计算缓冲区数据当前是否满足一个完整包数据量[包头&包体]

#define HAS_FULL_PACKET ( \

                                                 (sizeof(NetPacketHead)<=m_nOffset) && \

                                                 ((((NetPacketHead*)m_pMsgBuffer)->nLen) <= BUFFER_BODY_LEN) \

                                          )

 

//检查包是否合法[包体长度大于零且包体不等于空]

#define IS_VALID_PACKET(netPacket) \

       ((netPacket.netPacketHead.nLen>0) && (netPacket.packetBody!=NULL))

 

//缓冲区第一个包的长度

#define FIRST_PACKET_LEN (sizeof(NetPacketHead)+((NetPacketHead*)m_pMsgBuffer)->nLen)

 

/* 数据缓冲 */

class /*TCPLAB_DECLSPEC*/ CNetDataBuffer

{

       /* 缓冲区操作相关成员 */

private:

       char *m_pMsgBuffer;//数据缓冲区

       int m_nBufferSize;//缓冲区总大小

       int m_nOffset;//缓冲区数据大小

public:

       int GetBufferSize() const;//获得缓冲区的大小

       BOOL ReBufferSize(int);//调整缓冲区的大小

       BOOL IsFitPacketHeadSize() const;//缓冲数据是否适合包头大小

       BOOL IsHasFullPacket() const;//缓冲区是否拥有完整的包数据[包含包头和包体]      

       BOOL AddMsg(char *pBuf,int nLen);//添加消息到缓冲区

       const char *GetBufferContents() const;//得到缓冲区内容

       void Reset();//缓冲区复位[清空缓冲区数据,但并未释放缓冲区]

       void Poll();//移除缓冲区首部的第一个数据包

public:

       CNetDataBuffer();

       ~CNetDataBuffer();

};

 

#endif

 

缓冲区实现文件:

#define TCPLAB_DECLSPEC _declspec(dllexport)

 

#include "CNetDataBuffer.h"

 

/* 构造 */

CNetDataBuffer::CNetDataBuffer()

{

       m_nBufferSize = BUFFER_INIT_SIZE;//设置缓冲区大小

       m_nOffset = 0;//设置数据偏移值[数据大小]为0

       m_pMsgBuffer = NULL;

       m_pMsgBuffer = new char[BUFFER_INIT_SIZE];//分配缓冲区为初始大小

       ZeroMemory(m_pMsgBuffer,BUFFER_INIT_SIZE);//缓冲区清空   

}

 

/* 析构 */

CNetDataBuffer::~CNetDataBuffer()

{

       if (m_nOffset!=0)

       {

              delete [] m_pMsgBuffer;//释放缓冲区

              m_pMsgBuffer = NULL;

              m_nBufferSize=0;

              m_nOffset=0;

       }

}

 

 

 

/************************************************************************/

/* Description:       获得缓冲区中数据的大小                                  */

/* Return:              缓冲区中数据的大小                                                                   */

/************************************************************************/

INT CNetDataBuffer::GetBufferSize() const

{

       return this->m_nOffset;

}

 

 

/************************************************************************/

/* Description:       缓冲区中的数据大小是否足够一个包头大小                  */

/* Return:              如果满足则返回True,否则返回False

/************************************************************************/

BOOL CNetDataBuffer::IsFitPacketHeadSize() const

{

       return sizeof(NetPacketHead)<=m_nOffset;

}

 

/************************************************************************/

/* Description:       判断缓冲区是否拥有完整的数据包(包头和包体)              */

/* Return:              如果缓冲区包含一个完整封包则返回True,否则False                   */

/************************************************************************/

BOOL CNetDataBuffer::IsHasFullPacket() const

{

       //如果连包头大小都不满足则返回

       //if (!IsFitPacketHeadSize())

       //     return FALSE;

 

       return HAS_FULL_PACKET;//此处采用宏简化代码

}

 

/************************************************************************/

/* Description:       重置缓冲区大小                                                  */

/* nLen:          新增加的数据长度                                                                      */

/* Return:              调整结果                                                                                    */

/************************************************************************/

BOOL CNetDataBuffer::ReBufferSize(int nLen)

{

       char *oBuffer = m_pMsgBuffer;//保存原缓冲区地址

       try

       {

              nLen=(nLen<64?64:nLen);//保证最小增量大小

              //新缓冲区的大小=增加的大小+原缓冲区大小

              m_nBufferSize = BUFFER_EXPAND_SIZE*nLen+m_nBufferSize;          

              m_pMsgBuffer = new char[m_nBufferSize];//分配新的缓冲区,m_pMsgBuff指向新缓冲区地址

              ZeroMemory(m_pMsgBuffer,m_nBufferSize);//新缓冲区清零

              CopyMemory(m_pMsgBuffer,oBuffer,m_nOffset);//将原缓冲区的内容全部拷贝到新缓冲区

       }

       catch(...)

       {

              throw;

       }

 

       delete []oBuffer;//释放原缓冲区

       return TRUE;

}

 

/************************************************************************/

/* Description:       向缓冲区添加消息                                        */

/* pBuf:          要添加的数据                                                                             */

/* nLen:          添加的消息长度

/* return:        添加成功返回True,否则False                                                      */

/************************************************************************/

BOOL CNetDataBuffer::AddMsg(char *pBuf,int nLen)

{

       try

       {

              //检查缓冲区长度是否满足,不满足则重新调整缓冲区大小

              if (m_nOffset+nLen>m_nBufferSize)

                     ReBufferSize(nLen);

             

              //拷贝新数据到缓冲区末尾 

              CopyMemory(m_pMsgBuffer+sizeof(char)*m_nOffset,pBuf,nLen);

             

              m_nOffset+=nLen;//修改数据偏移

       }

       catch(...)

       {

              return FALSE;

       }

       return TRUE;

}

 

/* 得到缓冲区内容 */

const char * CNetDataBuffer::GetBufferContents() const

{

       return m_pMsgBuffer;

}

 

/************************************************************************/

/* 缓冲区复位                                                           */

/************************************************************************/

void CNetDataBuffer::Reset()

{

      

       if (m_nOffset>0)

       {

              m_nOffset = 0;

              ZeroMemory(m_pMsgBuffer,m_nBufferSize);

       }

}

 

/************************************************************************/

/* 移除缓冲区首部的第一个数据包                                         */

/************************************************************************/

void CNetDataBuffer::Poll()

{

       if(m_nOffset==0 || m_pMsgBuffer==NULL)

              return;

       if (IsFitPacketHeadSize() && HAS_FULL_PACKET)

       {           

              CopyMemory(m_pMsgBuffer,m_pMsgBuffer+FIRST_PACKET_LEN*sizeof(char),m_nOffset-FIRST_PACKET_LEN);

       }

}

 

对TCP发包和收包进行简单封装:

头文件:

#include <windows.h>

#include "CNetDataBuffer.h"

 

// #ifndef TCPLAB_DECLSPEC

// #define TCPLAB_DECLSPEC _declspec(dllimport)

// #endif

 

 

#ifndef _CNETCOMTEMPLATE_H_

#define _CNETCOMTEMPLATE_H_

 

 

//通信端口

#define TCP_PORT 6000

 

/* 通信终端[包含一个Socket和一个缓冲对象] */

typedef struct {

       SOCKET m_socket;//通信套接字

       CNetDataBuffer m_netDataBuffer;//该套接字关联的数据缓冲区

} ComEndPoint;

 

/* 收包回调函数参数 */

typedef struct{

       NetPacket *pPacket;

       LPVOID processor;

       SOCKET comSocket;

} PacketHandlerParam;

 

class CNetComTemplate{     

       /* Socket操作相关成员 */

private:

      

public:

       void SendPacket(SOCKET m_connectedSocket,NetPacket &netPacket);//发包函数

       BOOL RecvPacket(ComEndPoint &comEndPoint,void (*recvPacketHandler)(LPVOID)=NULL,LPVOID=NULL);//收包函数

public:

       CNetComTemplate();

       ~CNetComTemplate();

};

 

#endif

 

 

实现文件:

 

#include "CNetComTemplate.h"

 

CNetComTemplate::CNetComTemplate()

{

 

}

 

CNetComTemplate::~CNetComTemplate()

{

 

}

 

/************************************************************************/

/* Description:发包                                                     */

/* m_connectedSocket:建立好连接的套接字                                                             */

/* netPacket:要发送的数据包                                                                                   */

/************************************************************************/

void CNetComTemplate::SendPacket(SOCKET m_connectedSocket,NetPacket &netPacket)

{

       if (m_connectedSocket==NULL || !IS_VALID_PACKET(netPacket))//如果尚未建立连接则退出

       {

              return;

       }

       ::send(m_connectedSocket,(char*)&netPacket.netPacketHead,sizeof(NetPacketHead),0);//先发送包头

       ::send(m_connectedSocket,netPacket.packetBody,netPacket.netPacketHead.nLen,0);//在发送包体

}

 

/**************************************************************************/

/* Description:收包                                                       */

/* comEndPoint:通信终端[包含套接字和关联的缓冲区]                                     */

/* recvPacketHandler:收包回调函数,当收到一个包后调用该函数进行包的分发处理*/

/**************************************************************************/

BOOL CNetComTemplate::RecvPacket(ComEndPoint &comEndPoint,void (*recvPacketHandler)(LPVOID),LPVOID pCallParam)

{

       if (comEndPoint.m_socket==NULL)

              return FALSE;

      

       int nRecvedLen = 0;

       char pBuf[1024];

       //如果缓冲区数据不够包大小则继续从套接字读取tcp报文段

       while (!(comEndPoint.m_netDataBuffer.IsHasFullPacket()))

       {

              nRecvedLen = recv(comEndPoint.m_socket,pBuf,1024,0);

             

              if (nRecvedLen==SOCKET_ERROR || nRecvedLen==0)//若果Socket错误或者对方连接已经正常关闭则结束读取

                     break;

              comEndPoint.m_netDataBuffer.AddMsg(pBuf,nRecvedLen);//将新接收的数据存入缓冲区

       }

 

       //执行到此处可能是三种情况:

       //1.已经读取到的数据满足一个完整的tcp报文段

       //2.读取发生socket_error错误

       //3.在还未正常读取完毕的过程中对方连接已经关闭

      

 

       //如果没有读取到数据或者没有读取到完整报文段则返回返回

       if (nRecvedLen==0 || (!(comEndPoint.m_netDataBuffer.IsHasFullPacket())))

       {

              return FALSE;

       }

 

       if (recvPacketHandler!=NULL)

       {

              //构造准备传递给回调函数的数据包

              NetPacket netPacket;

              netPacket.netPacketHead = *(NetPacketHead*)comEndPoint.m_netDataBuffer.GetBufferContents();

              netPacket.packetBody = new char[netPacket.netPacketHead.nLen];//动态分配包体空间

             

              //构造回调函数参数

              PacketHandlerParam packetParam;

              packetParam.pPacket = &netPacket;

              packetParam.processor = pCallParam;

 

              //呼叫回调函数

              recvPacketHandler(&packetParam);

 

              delete []netPacket.packetBody;

       }

 

       //移除缓冲区的第一个包

       comEndPoint.m_netDataBuffer.Poll();

 

       return TRUE;

}



=============================================================================

TCP协议

转自 http://learn.akae.cn/media/ch36s07.html


7. TCP协议 请点评

7.1. 段格式 请点评

TCP的段格式如下图所示(该图出自[TCPIP])。

图 36.12. TCP段格式

TCP段格式

和UDP协议一样也有源端口号和目的端口号,通讯的双方由IP地址和端口号标识。32位序号、32位确认序号、窗口大小稍后详细解释。4位首部长度和IP协议头类似,表示TCP协议头的长度,以4字节为单位,因此TCP协议头最长可以是4x15=60字节,如果没有选项字段,TCP协议头最短20字节。URG、ACK、PSH、RST、SYN、FIN是六个控制位,本节稍后将解释SYN、ACK、FIN、RST四个位,其它位的解释从略。16位检验和将TCP协议头和数据都计算在内。紧急指针和各种选项的解释从略。

7.2. 通讯时序 请点评

下图是一次TCP通讯的时序图。

图 36.13. TCP连接建立断开

TCP连接建立断开

在这个例子中,首先客户端主动发起连接、发送请求,然后服务器端响应请求,然后客户端主动关闭连接。两条竖线表示通讯的两端,从上到下表示时间的先后顺序,注意,数据从一端传到网络的另一端也需要时间,所以图中的箭头都是斜的。双方发送的段按时间顺序编号为1-10,各段中的主要信息在箭头上标出,例如段2的箭头上标着SYN, 8000(0), ACK 1001, <mss 1024>,表示该段中的SYN位置1,32位序号是8000,该段不携带有效载荷(数据字节数为0),ACK位置1,32位确认序号是1001,带有一个mss选项值为1024。

建立连接的过程:

  1. 客户端发出段1,SYN位表示连接请求。序号是1000,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况,另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由于发了SYN位,因此下次再发送应该用序号1001。mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大帧长度,就必须在IP层分片,为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。

  2. 服务器发出段2,也带有SYN位,同时置ACK位表示确认,确认序号是1001,表示“我接收到序号1000及其以前所有的段,请你下次发送序号为1001的段”,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求,同时声明最大尺寸为1024。

  3. 客户端发出段3,对服务器的连接请求进行应答,确认序号是8001。

在这个过程中,客户端和服务器分别给对方发了连接请求,也应答了对方的连接请求,其中服务器的请求和应答在一个段中发出,因此一共有三个段用于建立连接,称为'''三方握手(three-way-handshake)'''。在建立连接的同时,双方协商了一些信息,例如双方发送序号的初始值、最大段尺寸等。

在TCP通讯中,如果一方收到另一方发来的段,读出其中的目的端口号,发现本机并没有任何进程使用这个端口,就会应答一个包含RST位的段给另一方。例如,服务器并没有任何进程使用8080端口,我们却用telnet客户端去连接它,服务器收到客户端发来的SYN段就会应答一个RST段,客户端的telnet程序收到RST段后报告错误Connection refused:

$ telnet 192.168.0.200 8080
Trying 192.168.0.200...
telnet: Unable to connect to remote host: Connection refused

数据传输的过程:

  1. 客户端发出段4,包含从序号1001开始的20个字节数据。

  2. 服务器发出段5,确认序号为1021,对序号为1001-1020的数据表示确认收到,同时请求发送序号1021开始的数据,服务器在应答的同时也向客户端发送从序号8001开始的10个字节数据,这称为piggyback。

  3. 客户端发出段6,对服务器发来的序号为8001-8010的数据表示确认收到,请求发送序号8011开始的数据。

在数据传输过程中,ACK和确认序号是非常重要的,应用程序交给TCP协议发送的数据会暂存在TCP层的发送缓冲区中,发出数据包给对方之后,只有收到对方应答的ACK段才知道该数据包确实发到了对方,可以从发送缓冲区中释放掉了,如果因为网络故障丢失了数据包或者丢失了对方发回的ACK段,经过等待超时后TCP协议自动将发送缓冲区中的数据包重发。

这个例子只描述了最简单的一问一答的情景,实际的TCP数据传输过程可以收发很多数据段,虽然典型的情景是客户端主动请求服务器被动应答,但也不是必须如此,事实上TCP协议为应用层提供了全双工(full-duplex)的服务,双方都可以主动甚至同时给对方发送数据。

如果通讯过程只能采用一问一答的方式,收和发两个方向不能同时传输,在同一时间只允许一个方向的数据传输,则称为'''半双工(half-duplex)''',假设某种面向连接的协议是半双工的,则只需要一套序号就够了,不需要通讯双方各自维护一套序号,想一想为什么。

关闭连接的过程:

  1. 客户端发出段7,FIN位表示关闭连接的请求。

  2. 服务器发出段8,应答客户端的关闭连接请求。

  3. 服务器发出段9,其中也包含FIN位,向客户端发送关闭连接请求。

  4. 客户端发出段10,应答服务器的关闭连接请求。

建立连接的过程是三方握手,而关闭连接通常需要4个段,服务器的应答和关闭连接请求通常不合并在一个段中,因为有连接半关闭的情况,这种情况下客户端关闭连接之后就不能再发送数据给服务器了,但是服务器还可以发送数据给客户端,直到服务器也关闭连接为止,稍后会看到这样的例子。

7.3. 流量控制 请点评

介绍UDP时我们描述了这样的问题:如果发送端发送的速度较快,接收端接收到数据后处理的速度较慢,而接收缓冲区的大小是固定的,就会丢失数据。TCP协议通过'''滑动窗口(Sliding Window)'''机制解决这一问题。看下图的通讯过程。

图 36.14. 滑动窗口

滑动窗口

  1. 发送端发起连接,声明最大段尺寸是1460,初始序号是0,窗口大小是4K,表示“我的接收缓冲区还有4K字节空闲,你发的数据不要超过4K”。接收端应答连接请求,声明最大段尺寸是1024,初始序号是8000,窗口大小是6K。发送端应答,三方握手结束。

  2. 发送端发出段4-9,每个段带1K的数据,发送端根据窗口大小知道接收端的缓冲区满了,因此停止发送数据。

  3. 接收端的应用程序提走2K数据,接收缓冲区又有了2K空闲,接收端发出段10,在应答已收到6K数据的同时声明窗口大小为2K。

  4. 接收端的应用程序又提走2K数据,接收缓冲区有4K空闲,接收端发出段11,重新声明窗口大小为4K。

  5. 发送端发出段12-13,每个段带1K数据,段13同时还包含FIN位。

  6. 接收端应答接收到的2K数据(6145-8192),再加上FIN位占一个序号8193,因此应答序号是8194,连接处于半关闭状态,接收端同时声明窗口大小为2K。

  7. 接收端的应用程序提走2K数据,接收端重新声明窗口大小为4K。

  8. 接收端的应用程序提走剩下的2K数据,接收缓冲区全空,接收端重新声明窗口大小为6K。

  9. 接收端的应用程序在提走全部数据后,决定关闭连接,发出段17包含FIN位,发送端应答,连接完全关闭。

上图在接收端用小方块表示1K数据,实心的小方块表示已接收到的数据,虚线框表示接收缓冲区,因此套在虚线框中的空心小方块表示窗口大小,从图中可以看出,随着应用程序提走数据,虚线框是向右滑动的,因此称为滑动窗口。

从这个例子还可以看出,发送端是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),在底层通讯中这些数据可能被拆成很多数据包来发送,但是一个数据包有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。


===============================================================================

TCP通信流程解析

转自 http://blog.youkuaiyun.com/phunxm/article/details/5836034


B/S 通信简述

整个计算机网络的实现体现为协议的实现, TCP/IP 协议是 Internet 的核心协议, HTTP 协议是比 TCP 更高层次的应用层协议。

HTTP ( HyperText Transfer Protocol ,超文本传输协议)是互联网上应用最为广泛的一种网络协议。所有的 WWW 文件都必须遵守这个标准。设计 HTTP 的初衷是为了提供一种发布和接收 HTML 页面的方法。

浏览器( Web Browser )负责与服务器建立连接,下载网页(包括资源文件及 JS 脚本文件)到本地,并最终渲染出页面。 JS 脚本文件运行在客户端,负责客户端一些行为响应或预处理,例如提交表单前的数据校验、鼠标事件处理等交互。由此可见,浏览器( Browser )一方面充当了 C/S 通信架构中 C 角色 ,另一方面它是 HTML/JavaScript 的解析渲染引擎( Analyze Render Engine )。

在浏览器地址栏敲入 “http://www.baidu.com/ ” ,按下回车键,浏览器中呈现出百度首页。这样一种情景我们再熟悉不过,本文通过 wireshark 抓取这一过程的 TCP/IP 数据包,结合 TCP 协议分析 HTTP 通信的基本流程。

MTU 和 MSS

本文用到的抓包工具为 wireshark ,它的前身是赫赫有名的 Ethereal 。 wireshark 以太网帧的封包格式为:

Frame = Ethernet Header + IP Header + TCP Header + TCP Segment Data

( 1 ) Ethernet Header = 14 Byte = Dst Physical Address ( 6 Byte ) +  Src Physical Address ( 6 Byte ) + Type ( 2 Byte ),以太网帧头以下称之为数据帧 。

( 2 ) IP Header = 20 Byte ( without options field ),数据在 IP 层称为 Datagram ,分片称为 Fragment 

( 3 ) TCP Header = 20 Byte ( without options field ),数据在 TCP 层称为 Stream ,分段称为 Segment ( UDP 中称为 Message ) 。

( 4 ) 54 个字节后为 TCP 数据负载部分( Data Portion ),即应用层用户数据。

Ethernet Header 以下的 IP 数据报最大传输单位为 MTU ( Maximum Transmission Unit , Effect of short board ),对于大多数使用以太网的局域网来说,MTU=1500 。

TCP 数据包每次能够传输的最大数据分段为 MSS ,为了达到最佳的传输效能,在建立 TCP 连接时双方协商 MSS 值,双方提供的 MSS 值的最小值为这次连接的最大 MSS 值。 MSS 往往基于 MTU 计算出来,通常 MSS=MTU-sizeof(IP Header)-sizeof(TCP Header)=1500-20-20=1460 。

这样,数据经过本地 TCP 层分段后,交给本地 IP 层,在本地 IP 层就不需要分片了。但是在下一跳路由( Next Hop )的邻居路由器上可能发生 IP 分片!因为路由器的网卡的 MTU可能小于需要转发的 IP 数据报的大小。这时候,在路由器上可能发生两种情况:

( 1 ) . 如果源发送端设置了这个 IP 数据包可以分片( May Fragment , DF=0 ),路由器将 IP 数据报分片后转发。

( 2 ) . 如果源发送端设置了这个 IP 数据报不可以分片( Don’t Fragment , DF=1 ),路由器将 IP 数据报丢弃,并发送 ICMP 分片错误消息给源发送端。

关于 MTU 的探测,参考《 Path MTU discovery 》。我们 可以通过基于 ICMP 协议的 ping 命令来探测从本机出发到目标机器上路由上的 MTU ,详见下文。

TCP 和 UDP

在基于传输层( TCP/UDP )的应用开发中,为了最后的程序优化,应避免端到端的任何一个节点上出现 IP 分片。 TCP 的 MSS 协商机制加上序列号确认机制,基本上能够保证数据的可靠传输。

UDP 协议在 IP 协议的基础上,只增加了传输层的端口( Source Port+Destination Port )、 UDP 数据包长( Length = Header+Data )以及检验和( Checksum )。因此,基于 UDP 开发应用程序时,数据包需要结合 IP 分片情况考虑。对于以太局域网,往往取 UDP 数据包长 Length<=MTU-sizeof(IP Header)=1480 ,故 UDP 数据负载量小于或等于1472 ( Length-UDP Header );对于公网, ipv4 最小 MTU 为 576 , UDP 数据负载量小于或等于 536 。

“ 向外” NAT 在内网和公网之间提供了一个“ 不对称” 桥的映射。“ 向外” NAT 在默认情况下只允许向外的 session 穿越 NAT :从外向内的的数据包都会被丢弃掉,除非 NAT设备事先已经定义了这些从外向内的数据包是已存在的内网 session 的一部分。对于一方在 LAN ,一方在 WAN 的 UDP 通信,鉴于 UDP 通信不事先建立虚拟链路, NAT 后面的 LAN 通信方需先发送消息给 WAN 通信方以洞穿 NAT ,然后才可以进行双向通信,这即是常提到的 “UDP 打洞( Hole Punching ) ” 问题。

TCP 连接百度过程解析

下文对百度的完整抓包建立在不使用 缓存的基础上。如若主机存有百度站点的 cookie 和脱机缓存( Offline Cache ),则不会再请求地址栏图标 favicon.ico ;请求/js/bdsug.js?v=1.0.3.0 可能回应 “HTTP/1.1 304 Not Modified” 。可在浏览器打开百度首页后,Ctrl+F5强制刷新,不使用缓存,也可参考《 浏览器清除缓存方法 》。

以下为访问百度过程, wireshark 抓包数据。对于直接通过 Ethernet 联网的机器, Wireshark Capture Filter 为 host www.baidu.com ;对于通过 PPP over Ethernet ( PPPoE )联网的机器, Wireshark Capture Filter 为 pppoes and host www.baidu.com 。以下抓包示例 直接通过 Ethernet 联网访问百度的过程。可点击图片超链接下载pcap文件,使用wireshark软件查看。

为方便起见,以下将客户端(浏览器)简称为 C ,将服务器(百度)简称为 S 。


 

1 . TCP 三次握手建立连接

“http://” 标识 WWW 访问协议为 HTTP ,根据规则,只有底层协议建立连接之后才能进行更高层协议的连接。在浏览器地址栏输入地址后按下回车键的瞬间, C 建立与 S (机器名为www.baidu.com , DNS 解析出来的 IP 为 220.181.6.175 )的 TCP 80 连接( HTTP 默认使用 TCP 80 端口)。

以下为三次握手建立 TCP 连接的数据包( Packet1-Packet3 )。

1   192.168.89.125:5672 → 220.181.6.175:80    TCP( 协议 ) 62( 以太网 帧长 )

amqp > http [SYN] Seq=0 Win=65535 Len=0 MSS=1460 SACK_PERM =1

2   220.181.6.175:80 → 192.168.89.125:5672 TCP 62

http > amqp [SYN, ACK] Seq=0 Ack=1 Win=8192 Len=0 MSS=1460 SACK_PERM=1

3   192.168.89.125:5672 → 220.181.6.175:80    TCP 54

amqp > http [ACK] Seq=1 Ack=1 Win=65535 Len=0

三次握手建立 TCP 连接的流程如下:

     C(Browser)                                     S(www.baidu.com)

  1.  CLOSED                                              LISTEN

  2.  SYN-SENT      → <SEQ=0><CTL=SYN>               → SYN-RECEIVED

  3.  ESTABLISHED ← <SEQ=0><ACK=1><CTL=SYN,ACK>  ← SYN-RECEIVED

  4.  ESTABLISHED → <SEQ=1><ACK=1><CTL=ACK>         → ESTABLISHED

3-Way Handshake for Connection Synchronization

三次握手的 socket 层执行逻辑

S 调用 socket 的 listen 函数进入监听状态; C 调用 connect 函数连接 S : [SYN] , S 调用 accept 函数接受 C 的连接并发起与 C 方向上的连接: [SYN,ACK] 。 C 发送[ACK] 完成三次握手, connect 函数返回; S 收到 C 发送的 [ACK] 后, accept 函数返回。

关于 Seq 和 Ack

Seq 即 Sequence Number , 为源端 ( source ) 的发送序列号 ; Ack 即 Acknowledgment Number , 为目的端 ( destination ) 的接收确认序列号 。在 Wireshark Display Filter 中,可使用 tcp.seq 或 tcp.ack 过滤。

在 Packet1 中, C:5672 向 S:80 发送 SYN 握手包, Seq=0(relative sequence number) ;在 Packet2 中 , S:80 向 C:5672 发送 ACK 握手回应包, Ack=1(relative sequence number) ,同时发送 SYN 握手包, Seq=0(relative sequence number) ;在 Packet3 中, C:5672 向 S:80 发送 ACK 握手回应包, Seq=1 , Ack=1 。

至此, Seq=1 为 C 的 Initial Sequence Number ( ISN ),后期某一时刻的 Seq=ISN+ 累计发送量 (cumulative sent) ; Ack=1 为 C 的 Initial Acknowledge Number ( IAN ),后期某一时刻的 Ack=IAN+ 累计接收量 (cumulative received) 。对于 S 而言, Seq 和 Ack 情同此理。

参考 :《TCP Analyze Sequence Numbers 》、《Understanding TCP Sequence and Acknowledgement Numbers 》

2 . TCP 获取网站数据流程

连接建立后,下一步发送( “GET / HTTP/1.1” )请求( Request ) HTML 页面,这里 “/” 表示 S 的默认首页, “GET” 为 HTTP Request Method ; “/” 为 Request-URI,这里为相对地址; HTTP/1.1 表示使用的 HTTP 协议版本号为 1.1 。

以下为 HTTP GET 请求数据包( Packet4 )。

4   192.168.89.125:5672 → 220.181.6.175:80 HTTP 417

GET / HTTP/1.1

HTTP GET 报文长 =417-54=363 个字节,其中 Next sequence number: 364(relative sequence number) 表示,若 在规定的时间内收到S 响应 Ack=364 ,表明该报文发送成功,可以发送下一个报文( Seq=364 );否则重传(TCP Retransmitssion )。序列号确认机制是 TCP 可靠性传输的保障。

S ( http )收到 HTTP GET 报文(共 363 个字节),向 C ( amqp )发送 TCP 确认报文 ( Packet5 )。

5   220.181.6.175:80 →   192.168.89.125:5672  TCP 60

http > amqp [ACK] Seq=1 Ack=364 Win=6432 Len=0

这里 Seq=1, 为 S 的 ISN ,意为已发送过 SYN 。 Packet2 中, Ack=1 为 S 的 IAN 。这里的 Ack-IAN=364-1=363 表示 S 已经从 C 接收到 363 个字节,即 HTTP GET 报文。同时,Ack=364也是S期待C发送的下一个TCP报文序列号(上面分析的 Next sequence number) 。

接下来, S 向 C 发送 Http Response ,根据 HTTP 协议,先发响应头( Response Header ),再发百度首页 HTML 文件。

Http Response Header 报文 ( Packet6 ) 如下 。

6   220.181.6.175:80 →   192.168.89.125:5672  TCP 465

TCP segment of a reassembled PDU ]

其部分内容如下:

======================================

HTTP/1.1 200 OK

……

Content-Length: 2139

Content-Type: text/html;charset=gb2312

Content-Encoding: gzip

======================================

S 响应 C 的 “GET / HTTP/1.1” 请求,先发送带 [PSH ] 标识的 411 个字节的 Http Response Header ( Packet 6 )。

TCP 头部 [PSH] 标识置位,敦促 C 将缓存的数据推送给应用程序,即先处理 Http Response Header ,实际上是一种 “ 截流 ” 通知。相应 C 的 socket 调用 send 时 在IPPROTO_TCP 选项级别设置 TCP_NODELAY 为 TRUE 禁用 Nagle 算法可以 “ 保留发送边界 ” ,以防粘连。

尽管握手协商的 MSS 为 1460 ,但服务器或者代理平衡服务器,每次发送过来的 TCP 数据最多只有 1420 个字节 。 可以使用 ping -f -l size target_name 命令向指定目标target_name 发送指定字节量的 ICMP 报文,其中 -l size 指定发送缓冲区的大小; -f 则表示在 IP 数据报中设置不分片( Don’t Fragment ),这样便可探测出到目标路径上的MTU 。

执行“ ping -f -l 1452 www.baidu.com ”的结果如下:

220.181.6.18 的 Ping 统计信息 :

    数据包 : 已发送 = 4 ,已接收 = 4 ,丢失 = 0 (0% 丢失 )

执行“ ping -f -l 1453 www.baidu.com ”的结果如下:

需要拆分数据包但是设置 DF 。

220.181.6.18 的 Ping 统计信息 :

    数据包 : 已发送 = 4 ,已接收 = 0 ,丢失 = 4 (100% 丢失 )

从以上 ping 结果可知,在不分片时,从本机出发到百度的路由上能通过的最大数据量为 1452 ,由此推算出 MTU{local,baidu}=sizeof(IP Header)+ sizeof(ICMP Header)+sizeof(ICMP Data Portion)=20+8+1452=1480 。

S 调用 socket 的 send 函数发送 2139 个字节的 Http Response Content ( Packet 7 、 Packet 9 ),在 TCP 层将分解为两段( segment )后再发出去。

7   220.181.6.175:80 →   192.168.89.125:5672  TCP 1474

[TCP segment of a reassembled PDU]

由 “Content-Length: 2139” 可知, HTML 文件还有 2139-(1474-54)=719 个字节。但此时, C 已经发送了确认报文 ( Packet8 ) 。

8   192.168.89.125:5672 →    220.181.6.175:80  TCP 54

amqp > http [ACK] Seq=364 Ack=1832 Win=65535 Len=0

Seq-ISN=364-1=363 ,表示 C 已经发出了 363 个字节,上边已经收到了 S 的确认。 Ack-IAN=1832-1=(465-54)+(1474-54) ,表示 C 至此已经接收到 S 发来的 1831 个字节。

接下来, C 收到 HTML 文件剩余的 719 个字节,报文 ( Packet9 )如下。

9   220.181.6.175:80 →   192.168.89.125:5672  HTTP   773

HTTP/1.1 200 OK

至此, C 收到 S 发送过来的全部 HTTP 响应报文,即百度首页 HTML 内容 (text/html) 。

Packet6 、 Packet7 和 Packet9 的 ACK 都是 364 ,这是因为这三个segment都是针对 Packet4 的 TCP 响应。S将百度首页HTML文件(一个完整的HTTP报文)按照MSS分段提交给TCP层。 在 Wireshark 中可以看到 Packet9 的报文中有以下 reassemble 信息:

[Reassembled TCP segments (2555 bytes): #6(411),#7(1420),#9(719)]

[Frame: 6, payload: 0-410(411 bytes)]

[Frame: 7, payload: 411-1830(1420 bytes)]

[Frame: 9, payload: 1831-2549(719 bytes)]

C ( amqp )接收到百度首页的 HTML 文件后,开始解析渲染。在解析过程中,发现页面中含有百度的 logo 资源 baidu_logo.gif ,并且需要 bdsug.js 脚本。

<img src=" http://www.baidu.com/img/baidu_logo.gif " width="270" height="129" usemap="#mp">

{d.write('<script src=http://www.baidu.com/js/bdsug.js?v=1.0.3.0><//script>')}

于是上面那个连接( C:5672 )继续向 S 请求 logo 图标资源,报文( Packet10 )如下。

10  192.168.89.125:5672 →    220.181.6.175:80  HTTP 492

GET /img/baidu_logo.gif HTTP/1.1

与此同时, C ( jms )新建一个连接( TCP 5 673 )向 S 请求 js 脚本文件。 报文( Packet11 )如下。

11  192.168.89.125:5673 →    220.181.6.175:80  TCP 62

jms > http [SYN] Seq=0 Win=65535 Len=0 MSS=1460 SACK_PERM=1

( Packet12 ) Packet13 、 Packet14 、 Packet16 和 Packet17 为对 Packet10 的 TCP 响应(它们的 Ack=802 ), 在逻辑上它们是一个完整的 TCP 报文。其 Http Response Content 为图片文件 baidu_logo.gif 。我们在 Wireshark 中可以看到 Packet17 的报文中有以下 reassemble 信息:

[Reassembled TCP segments (1801 bytes): #13(312),#14(1420),#16(28) ,#17(41)]

[Frame: 13, payload: 0-311(312 bytes)]

[Frame: 14, payload: 312-1731(1420 bytes)]

[Frame: 16, payload: 1732-1759(28 bytes)]

[Frame: 17, payload: 1760-1800(41 bytes)]

Packet11-Packet19-Packet20 完成新连接的三次握手。然后, C ( jms )发送 “ GET /js/bdsug.js?v=1.0.3.0 HTTP/1.1 ” 报文( Packet21 ),以获取 bdsug.js脚本文件。

21  192.168.89.125:5673 →    220.181.6.175:80  HTTP 465

GET /js/bdsug.js?v=1.0.3.0 HTTP/1.1

( Packet22 ) Packet23 、 Packet24 、 Packet26 和 Packet27 为对 Packet21 的 TCP 响应(它们的 Ack=412 ), 在逻辑上它们是一个完整的 TCP 报文。其 Http Response Content 为脚本文件 bdsug.js 。我们在 Wireshark 中可以看到 Packet27 的报文中有以下 reassemble 信息:

[Reassembled TCP segments (3897 bytes): #23(310),#24(1420),#26(1420) ,#27(747)]

[Frame: 23, payload: 0-309(310 bytes)]

[Frame: 24, payload: 310-1729(1420 bytes)]

[Frame: 26, payload: 1730-3149(1420 bytes)]

[Frame: 27, payload: 3150-3896(747 bytes)]

通常,浏览器会自动的搜索网站的根目录,只要它发现了 favicon.ico 这个文件,就把它下载下来作为网站地址栏图标。于是, C ( amqp )还将发起 “ GET /favicon.ico HTTP/1.1 ” 请求 网站地址栏图标,见报文 Packet29 。

3 . TCP 四次挥手关闭连接

经 Packet28 确认收到了完整的 japplication/javascript 文件后,链路 1 (本地端口 5673 )使命结束, S 关闭该链路,进入四次挥手关闭双向连接。

( Packet30 ) Packet31 和 Packet32 为对 Packet29 的 TCP 响应(它们的 Ack=1201 )。 经 Packet33 确认收到了完整的 image/x-icon 文件后,链路 2 (本地端口5672 )使命结束, S 关闭该链路,进入四次挥手关闭双向连接。

    为什么握手是三次,而挥手是四次呢?这是因为握手时,服务器往往在答应建立连接时,也建立与客户端的连接,即所谓的双向连接。所以,在 Packet2 中,服务器将 ACK 和 SYN 打包发出。挥手,即关闭连接,往往只是表明挥手方不再发送数据(无数据可发),而接收通道依然有效(依然可以接受数据)。当对方也挥手时,则表明对方也无数据可发了,此时双向连接真正关闭。

 

参考:

《 浏览器 /网页工作原理 》《 What really happens when you navigate to a URL 

《 HTTP通信过程分析 

《 究竟什么是 HTTP连接 

《 一次完整的 HTTP通信步骤 

《 SOCKET与 TCP/IP与 HTTP的关系 

《 TCP连接、 Http连接与 Socket连接 



=============================================================

TCP通讯方式

转自 http://blog.sina.com.cn/s/blog_502d765f0100kofn.html


        TCP/IP通信程序设计的丰富多样性

        刚接触TCP/IP通信设计的人根据范例可以很快编出一个通信程序,据此一些人可能会认为TCP/IP编程很简单。其实不然,TCP/IP编程具有较为丰富的内容。其编程的丰富性主要体现在通信方式和报文格式的多样性上。


一. 通信方式

        主要有以下三大类:

(一)SERVER/CLIENT方式

        1.一个Client方连接一个Server方,或称点对点(peer to peer):
        2.多个Client方连接一个Server方,这也是通常的并发服务器方式。
        3.一个Client方连接多个Server方,这种方式很少见,主要用于一个客户向多个服务器发送请求情况。


(二)连接方式

        1.长连接

        Client方与Server方先建立通讯连接,连接建立后不断开,然后再进行报文发送和接收。这种方式下由于通讯连接一直存在,可以用下面命令查看连接是否建立:

        netstat –f inet|grep 端口号(如5678)。

        此种方式常用于点对点通讯。


        2.短连接

        Client方与Server每进行一次报文收发交易时才进行通讯连接,交易完毕后立即断开连接。此种方式常用于一点对多点通讯,比如多个Client连接一个Server.


(三)发送接收方式

        1.异步

        报文发送和接收是分开的,相互独立的,互不影响。这种方式又分两种情况:

        (1)异步双工:接收和发送在同一个程序中,有两个不同的子进程分别负责发送和接收
        (2)异步单工:接收和发送是用两个不同的程序来完成。

        2.同步

        报文发送和接收是同步进行,既报文发送后等待接收返回报文。同步方式一般需要考虑超时问题,即报文发上去后不能无限等待,需要设定超时时间,超过该时间发送方不再等待读返回报文,直接通知超时返回。 


        实际通信方式是这三类通信方式的组合。比如一般书上提供的TCP/IP范例程序大都是同步短连接的SERVER/CLIENT程序。有的组合是基本不用的,比较常用的有价值的组合是以下几种:

        同步短连接Server/Client
        同步长连接Server/Client
        异步短连接Server/Client
        异步长连接双工Server/Client
        异步长连接单工Server/Client

        其中异步长连接双工是最为复杂的一种通信方式,有时候经常会出现在不同银行或不同城市之间的两套系统之间的通信。比如金卡工程。由于这几种通信方式比较固定,所以可以预先编制这几种通信方式的模板程序。


二.报文格式

        通信报文格式多样性更多,相应地就必须设计对应的读写报文的接收和发送报文函数。

        (一)阻塞与非阻塞方式 

        1.非阻塞方式

        读函数不停地进行读动作,如果没有报文接收到,等待一段时间后超时返回,这种情况一般需要指定超时时间。

        2.阻塞方式

        如果没有报文接收到,则读函数一直处于等待状态,直到有报文到达。

 

        (二)循环读写方式

        1.一次直接读写报文

        在一次接收或发送报文动作中一次性不加分别地全部读取或全部发送报文字节。

        2.不指定长度循环读写

        这一般发生在短连接进程中,受网络路由等限制,一次较长的报文可能在网络传输过程中被分解成了好几个包。一次读取可能不能全部读完一次报文,这就需要循环读报文,直到读完为止。

        3.带长度报文头循环读写

        这种情况一般是在长连接进程中,由于在长连接中没有条件能够判断循环读写什么时候结束,所以必须要加长度报文头。读函数先是读取报文头的长度,再根据这个长度去读报文.实际情况中,报头的码制格式还经常不一样,如果是非ASCII码的报文头,还必须转换成ASCII,常见的报文头码制有:
                (1)n个字节的ASCII码
                (2)n个字节的BCD码
                (3)n个字节的网络整型码


        以上是几种比较典型的读写报文方式,可以与通信方式模板一起预先提供一些典型的API读写函数。当然在实际问题中,可能还必须编写与对方报文格式配套的读写API.

        在实际情况中,往往需要把我们自己的系统与别人的系统进行连接,
        有了以上模板与API,可以说连接任何方式的通信程序都不存在问题。




=============================================================

TCP三次握手及原理

  TCP/IP是很多的不同的协议组成,实际上是一个协议组,TCP用户数据报表协议(也称作TCP传输控制协议,Transport Control Protocol。可靠的主机到主机层协议。这里要先强调一下,传输控制协议是OSI网络的第四层的叫法,TCP传输控制协议是TCP/IP传输的6个基本协议的一种。两个TCP意思非相同。 )。TCP是一种可靠的面向连接的传送服务。它在传送数据时是分段进行的,主机交换数据必须建立一个会话。它用比特流通信,即数据被作为无结构的字节流。 通过每个TCP传输的字段指定顺序号,以获得可靠性。是在OSI参考模型中的第四层,TCP是使用IP的网间互联功能而提供可靠的数据传输,IP不停的把报文放到 网络上,而TCP是负责确信报文到达。在协同IP的操作中TCP负责:握手过程、报文管理、流量控制、错误检测和处理(控制),可以根据一定的编号顺序对非正常顺序的报文给予从新排列顺序。关于TCP的RFC文档有RFC793、RFC791、RFC1700。

 

  在TCP会话初期,有所谓的“三握手”:对每次发送的数据量是怎样跟踪进行协商使数据段的发送和接收同步,根据所接收到的数据量而确定的数据确认数及数据发送、接收完毕后何时撤消联系,并建立虚连接。为了提供可靠的传送,TCP在发送新的数据之前,以特定的顺序将数据包的序号,并需要这些包传送给目标机之后的确认消息。TCP总是用来发送大批量的数据。当应用程序在收到数据后要做出确认时也要用到TCP。由于TCP需要时刻跟踪,这需要额外开销,使得TCP的格式有些显得复杂。下面就让我们看一个TCP的经典案例,这是后来被称为MITNICK攻击中KEVIN开创了两种攻击技术:

  TCP会话劫持 
  SYN FLOOD(同步洪流)

  在这里我们讨论的时TCP会话劫持的问题。

  先让我们明白TCP建立连接的基本简单的过程。为了建设一个小型的模仿环境我们假设有3台接入互联网的机器。A为攻击者操纵的攻击机。B为中介跳板机器(受信任的服务器)。C为受害者使用的机器(多是服务器),这里把C机器锁定为目标机器。A机器向B机器发送SYN包,请求建立连接,这时已经响应请求的B机器会向A机器回应SYN/ACK表明同意建立连接,当A机器接受到B机器发送的SYN/ACK回应时,发送应答ACK建立A机器与B机器的网络连接。这样一个两台机器之间的TCP通话信道就建立成功了。

  B终端受信任的服务器向C机器发起TCP连接,A机器对服务器发起SYN信息,使C机器不能响应B机器。在同时A机器也向B机器发送虚假的C机器回应的SYN数据包,接收到SYN数据包的B机器(被C机器信任)开始发送应答连接建立的SYN/ACK数据包,这时C机器正在忙于响应以前发送的SYN数据而无暇回应B机器,而A机器的攻击者预测出B机器包的序列号(现在的TCP序列号预测难度有所加大)假冒C机器向B机器发送应答ACK这时攻击者骗取B机器的信任,假冒C机器与B机器建立起TCP协议的对话连接。这个时候的C机器还是在响应攻击者A机器发送的SYN数据。

  TCP协议栈的弱点:TCP连接的资源消耗,其中包括:数据包信息、条件状态、序列号等。通过故意不完成建立连接所需要的三次握手过程,造成连接一方的资源耗尽。

  通过攻击者有意的不完成建立连接所需要的三次握手的全过程,从而造成了C机器的资源耗尽。序列号的可预测性,目标主机应答连接请求时返回的SYN/ACK的序列号时可预测的。(早期TCP协议栈,具体的可以参见1981年出的关于TCP雏形的RFC793文档)


  TCP头结构

  TCP协议头最少20个字节,包括以下的区域(由于翻译不禁相同,文章中给出相应的英文单词):

  TCP源端口(Source Port):16位的源端口其中包含初始化通信的端口。源端口和源IP地址的作用是标示报问的返回地址。

  TCP目的端口(Destination port):16位的目的端口域定义传输的目的。这个端口指明报文接收计算机上的应用程序地址接口。

  TCP序列号(序列码,Sequence Number):32位的序列号由接收端计算机使用,重新分段的报文成最初形式。当SYN出现,序列码实际上是初始序列码(ISN),而第一个数据字节是ISN+1。这个序列号(序列码)是可以补偿传输中的 不一致。

  TCP应答号(Acknowledgment Number):32位的序列号由接收端计算机使用,重组分段的报文成最初形式。,如果设置了ACK控制位,这个值表示一个准备接收的包的序列码。

  数据偏移量(HLEN):4位包括TCP头大小,指示何处数据开始。

  保留(Reserved):6位值域,这些位必须是0。为了将来定义新的用途所保留。

  标志(Code Bits):6位标志域。表示为:紧急标志、有意义的应答标志、推、重置连接标志、同步序列号标志、完成发送数据标志。按照顺序排列是:URG、ACK、PSH、RST、SYN、FIN。

  窗口(Window):16位,用来表示想收到的每个TCP数据段的大小。

  校验位(Checksum):16位TCP头。源机器基于数据内容计算一个数值,收信息机要与源机器数值 结果完全一样,从而证明数据的有效性。

  优先指针(紧急,Urgent Pointer):16位,指向后面是优先数据的字节,在URG标志设置了时才有效。如果URG标志没有被设置,紧急域作为填充。加快处理标示为紧急的数据段。

  选项(Option):长度不定,但长度必须以字节。如果 没有 选项就表示这个一字节的域等于0。

  填充:不定长,填充的内容必须为0,它是为了数学目的而存在。目的是确保空间的可预测性。保证包头的结合和数据的开始处偏移量能够被32整除,一般额外的零以保证TCP头是32位的整数倍。 

  标志控制功能

  URG:紧急标志 

  紧急(The urgent pointer) 标志有效。紧急标志置位,

  ACK:确认标志 

  确认编号(Acknowledgement Number)栏有效。大多数情况下该标志位是置位的。TCP报头内的确认编号栏内包含的确认编号(w+1,Figure:1)为下一个预期的序列编号,同时提示远端系统已经成功接收所有数据。

  PSH:推标志

  该标志置位时,接收端不将该数据进行队列处理,而是尽可能快将数据转由应用处理。在处理 telnet 或 rlogin 等交互模式的连接时,该标志总是置位的。

  RST:复位标志

  复位标志有效。用于复位相应的TCP连接。

  SYN:同步标志

  同步序列编号(Synchronize Sequence Numbers)栏有效。该标志仅在三次握手建立TCP连接时有效。它提示TCP连接的服务端检查序列编号,该序列编号为TCP连接初始端(一般是客户端)的初始序列编号。在这里,可以把TCP序列编号看作是一个范围从0到4,294,967,295的32位计数器。通过TCP连接交换的数据中每一个字节都经过序列编号。在TCP报头中的序列编号栏包括了TCP分段中第一个字节的序列编号。

  FIN:结束标志

  带有该标志置位的数据包用来结束一个TCP回话,但对应端口仍处于开放状态,准备接收后续数据。

  服务端处于监听状态,客户端用于建立连接请求的数据包(IP packet)按照TCP/IP协议堆栈组合成为TCP处理的分段(segment)。

  分析报头信息: TCP层接收到相应的TCP和IP报头,将这些信息存储到内存中。

  检查TCP校验和(checksum):标准的校验和位于分段之中(Figure:2)。如果检验失败,不返回确认,该分段丢弃,并等待客户端进行重传。

  查找协议控制块(PCB{}):TCP查找与该连接相关联的协议控制块。如果没有找到,TCP将该分段丢弃并返回RST。(这就是TCP处理没有端口监听情况下的机制) 如果该协议控制块存在,但状态为关闭,服务端不调用connect()或listen()。该分段丢弃,但不返回RST。客户端会尝试重新建立连接请求。

  建立新的socket:当处于监听状态的socket收到该分段时,会建立一个子socket,同时还有socket{},tcpcb{}和pub{}建立。这时如果有错误发生,会通过标志位来拆除相应的socket和释放内存,TCP连接失败。如果缓存队列处于填满状态,TCP认为有错误发生,所有的后续连接请求会被拒绝。这里可以看出SYN Flood攻击是如何起作用的。

  丢弃:如果该分段中的标志为RST或ACK,或者没有SYN标志,则该分段丢弃。并释放相应的内存。


  发送序列变量

  SND.UNA : 发送未确认

  SND.NXT : 发送下一个

  SND.WND : 发送窗口

  SND.UP : 发送优先指针

  SND.WL1 : 用于最后窗口更新的段序列号

  SND.WL2 : 用于最后窗口更新的段确认号

  ISS : 初始发送序列号

  接收序列号

  RCV.NXT : 接收下一个

  RCV.WND : 接收下一个

  RCV.UP : 接收优先指针

  IRS : 初始接收序列号

 

  当前段变量

  SEG.SEQ : 段序列号

  SEG.ACK : 段确认标记

  SEG.LEN : 段长

  SEG.WND : 段窗口

  SEG.UP : 段紧急指针

  SEG.PRC : 段优先级


  CLOSED表示没有连接,各个状态的意义如下:

  LISTEN : 监听来自远方TCP端口的连接请求。

  SYN-SENT : 在发送连接请求后等待匹配的连接请求。

  SYN-RECEIVED : 在收到和发送一个连接请求后等待对连接请求的确认。

  ESTABLISHED : 代表一个打开的连接,数据可以传送给用户。

  FIN-WAIT-1 : 等待远程TCP的连接中断请求,或先前的连接中断请求的确认。

  FIN-WAIT-2 : 从远程TCP等待连接中断请求。

  CLOSE-WAIT : 等待从本地用户发来的连接中断请求。

  CLOSING : 等待远程TCP对连接中断的确认。

  LAST-ACK : 等待原来发向远程TCP的连接中断请求的确认。

  TIME-WAIT : 等待足够的时间以确保远程TCP接收到连接中断请求的确认。

  CLOSED : 没有任何连接状态。

  TCP连接过程是状态的转换,促使发生状态转换的是用户调用:OPEN,SEND,RECEIVE,CLOSE,ABORT和STATUS。传送过来的数据段,特别那些包括以下标记的数据段SYN,ACK,RST和FIN。还有超时,上面所说的都会时TCP状态发生变化。


  序列号

  请注意,我们在TCP连接中发送的字节都有一个序列号。因为编了号,所以可以确认它们的收到。对序列号的确认是累积性的。TCP必须进行的序列号比较操作种类包括以下几种:

  ①决定一些发送了的但未确认的序列号。

  ②决定所有的序列号都已经收到了。

  ③决定下一个段中应该包括的序列号。


  对于发送的数据TCP要接收确认,确认时必须进行的:

  SND.UNA = 最老的确认了的序列号。

  SND.NXT = 下一个要发送的序列号。

  SEG.ACK = 接收TCP的确认,接收TCP期待的下一个序列号。

  SEG.SEQ = 一个数据段的第一个序列号。

  SEG.LEN = 数据段中包括的字节数。

  SEG.SEQ+SEG.LEN-1 = 数据段的最后一个序列号。

  如果一个数据段的序列号小于等于确认号的值,那么整个数据段就被确认了。而在接收数据时下面的比较操作是必须的:

  RCV.NXT = 期待的序列号和接收窗口的最低沿。

  RCV.NXT+RCV.WND:1 = 最后一个序列号和接收窗口的最高沿。

  SEG.SEQ = 接收到的第一个序列号。

  SEG.SEQ+SEG.LEN:1 = 接收到的最后一个序列号.


=============================================================

tcp要点学习-断开连接

转自 http://www.cppblog.com/kevinlynx/default.html?page=6


Author : Kevin Lynx

主要部分,四次握手:

断开连接其实从我的角度看不区分客户端和服务器端,任何一方都可以调用close(or closesocket)之类
的函数开始主动终止一个连接。这里先暂时说正常情况。当调用close函数断开一个连接时,主动断开的
一方发送FIN(finish报文给对方。有了之前的经验,我想你应该明白我说的FIN报文时什么东西。也就是
一个设置了FIN标志位的报文段。FIN报文也可能附加用户数据,如果这一方还有数据要发送时,将数据附
加到这个FIN报文时完全正常的。之后你会看到,这种附加报文还会有很多,例如ACK报文。我们所要把握
的原则是,TCP肯定会力所能及地达到最大效率,所以你能够想到的优化方法,我想TCP都会想到。

当被动关闭的一方收到FIN报文时,它会发送ACK确认报文(对于ACK这个东西你应该很熟悉了)。这里有个
东西要注意,因为TCP是双工的,也就是说,你可以想象一对TCP连接上有两条数据通路。当发送FIN报文
时,意思是说,发送FIN的一端就不能发送数据,也就是关闭了其中一条数据通路。被动关闭的一端发送
了ACK后,应用层通常就会检测到这个连接即将断开,然后被动断开的应用层调用close关闭连接。

我可以告诉你,一旦当你调用close(or closesocket),这一端就会发送FIN报文。也就是说,现在被动
关闭的一端也发送FIN给主动关闭端。有时候,被动关闭端会将ACK和FIN两个报文合在一起发送。主动
关闭端收到FIN后也发送ACK,然后整个连接关闭(事实上还没完全关闭,只是关闭需要交换的报文发送
完毕),四次握手完成。如你所见,因为被动关闭端可能会将ACK和FIN合到一起发送,所以这也算不上
严格的四次握手---四个报文段。

在前面的文章中,我一直没提TCP的状态转换。在这里我还是在犹豫是不是该将那张四处通用的图拿出来,
不过,这里我只给出断开连接时的状态转换图,摘自<The TCP/IP Guide>:

tcpclose

给出一个正常关闭时的windump信息:

14:00:38.819856 IP cd-zhangmin.1748 > 220.181.37.55.80: F 1:1(0) ack 1 win 65535
14:00:38.863989 IP 220.181.37.55.80 > cd-zhangmin.1748: F 1:1(0) ack 2 win 2920
14:00:38.864412 IP cd-zhangmin.1748 > 220.181.37.55.80: . ack 2 win 65535 

 

补充细节:

关于以上的四次握手,我补充下细节:
1. 默认情况下(不改变socket选项),当你调用close( or closesocket,以下说close不再重复)时,如果
发送缓冲中还有数据,TCP会继续把数据发送完。
2. 发送了FIN只是表示这端不能继续发送数据(应用层不能再调用send发送),但是还可以接收数据。
3. 应用层如何知道对端关闭?通常,在最简单的阻塞模型中,当你调用recv时,如果返回0,则表示对端
关闭。在这个时候通常的做法就是也调用close,那么TCP层就发送FIN,继续完成四次握手。如果你不调用
close,那么对端就会处于FIN_WAIT_2状态,而本端则会处于CLOSE_WAIT状态。这个可以写代码试试。
4. 在很多时候,TCP连接的断开都会由TCP层自动进行,例如你CTRL+C终止你的程序,TCP连接依然会正常关
闭,你可以写代码试试。

特别的TIME_WAIT状态:

从以上TCP连接关闭的状态转换图可以看出,主动关闭的一方在发送完对对方FIN报文的确认(ACK)报文后,
会进入TIME_WAIT状态。TIME_WAIT状态也称为2MSL状态。

什么是2MSL?MSL即Maximum Segment Lifetime,也就是报文最大生存时间,引用<TCP/IP详解>中的话:“
它(MSL)是任何报文段被丢弃前在网络内的最长时间。”那么,2MSL也就是这个时间的2倍。其实我觉得没
必要把这个MSL的确切含义搞明白,你所需要明白的是,当TCP连接完成四个报文段的交换时,主动关闭的
一方将继续等待一定时间(2-4分钟),即使两端的应用程序结束。你可以写代码试试,然后用netstat查看下。

为什么需要2MSL?根据<TCP/IP详解>和<The TCP/IP Guide>中的说法,有两个原因:
其一,保证发送的ACK会成功发送到对方,如何保证?我觉得可能是通过超时计时器发送。这个就很难用
代码演示了。
其二,报文可能会被混淆,意思是说,其他时候的连接可能会被当作本次的连接。直接引用<The TCP/IP Guide>
的说法:The second is to provide a “buffering period” between the end of this connection 
and any subsequent ones. If not for this period, it is possible that packets from different 
connections could be mixed, creating confusion.

TIME_WAIT状态所带来的影响:

当某个连接的一端处于TIME_WAIT状态时,该连接将不能再被使用。事实上,对于我们比较有现实意义的
是,这个端口将不能再被使用。某个端口处于TIME_WAIT状态(其实应该是这个连接)时,这意味着这个TCP
连接并没有断开(完全断开),那么,如果你bind这个端口,就会失败。

对于服务器而言,如果服务器突然crash掉了,那么它将无法再2MSL内重新启动,因为bind会失败。解决这
个问题的一个方法就是设置socket的SO_REUSEADDR选项。这个选项意味着你可以重用一个地址。

对于TIME_WAIT的插曲:

当建立一个TCP连接时,服务器端会继续用原有端口监听,同时用这个端口与客户端通信。而客户端默认情况
下会使用一个随机端口与服务器端的监听端口通信。有时候,为了服务器端的安全性,我们需要对客户端进行
验证,即限定某个IP某个特定端口的客户端。客户端可以使用bind来使用特定的端口。

对于服务器端,当设置了SO_REUSEADDR选项时,它可以在2MSL内启动并listen成功。但是对于客户端,当使
用bind并设置SO_REUSEADDR时,如果在2MSL内启动,虽然bind会成功,但是在windows平台上connect会失败。
而在linux上则不存在这个问题。(我的实验平台:winxp, ubuntu7.10)

要解决windows平台的这个问题,可以设置SO_LINGER选项。SO_LINGER选项决定调用close时,TCP的行为。
SO_LINGER涉及到linger结构体,如果设置结构体中l_onoff为非0,l_linger为0,那么调用close时TCP连接
会立刻断开,TCP不会将发送缓冲中未发送的数据发送,而是立即发送一个RST报文给对方,这个时候TCP连
接就不会进入TIME_WAIT状态。

如你所见,这样做虽然解决了问题,但是并不安全。通过以上方式设置SO_LINGER状态,等同于设置SO_DONTLINGER
状态。

断开连接时的意外:
这个算不上断开连接时的意外,当TCP连接发生一些物理上的意外情况时,例如网线断开,linux上的TCP实现
会依然认为该连接有效,而windows则会在一定时间后返回错误信息。

这似乎可以通过设置SO_KEEPALIVE选项来解决,不过不知道这个选项是否对于所有平台都有效。

总结:

个人感觉,越写越烂。接下来会讲到TCP的数据发送,这会涉及到滑动窗口各种定时器之类的东西。我真诚
希望各位能够多提意见。对于TCP连接的断开,我们只要清楚:
1. 在默认情况下,调用close时TCP会继续将数据发送完毕;
2. TIME_WAIT状态会导致的问题;
3. 连接意外断开时可能会出现的问题。
4. maybe more...


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值