【转】运输层TCP协议详细介绍
TCP是TCP/IP协议族中非常复杂的一个协议。它具有以下特点:
1:面向连接的运输层协议。在使用TCP协议之前,首先需要建立TCP连接。传送数据完毕后,必须释放已经建立的TCP连接。
2:一条TCP连接有两个端点,连接是点对点的。
3:提供可靠交付的服务。通过TCP连接传送的数据,不会出现差错不会丢失并且按序到达。
4:提供全双工通信。TCP允许通信双方的应用程序进程在任何时候都能发送数据。TCP连接的两端都设有缓存,分为发送缓存和接收缓存,用来临时存放双向通信的数据。发送时,应用程序把数据传给TCP的缓存后,就可以做自己的事情了。TCP会在合适的时候把数据发送出去。接收时,TCP把接收到的数据放入缓存,供上层的应用程序读取。
5:面向字节流。虽然应用程序和TCP的交互是一次一个数据块,但TCP把应用程序交下来的数据看成一串无结构的字节流。TCP对应用进程一次把多长的报文发送到TCP缓存中是不关心的。TCP根据对方给出的窗口值和当前网络的拥塞程度决定一个报文段包含多少字节。如果应用程序传到TCP缓存的数据块太长,TCP就会把它划分短些再传送。如果应用程序发来的数据太少时,TCP将会等待积累足够多的字节后再构成报文段发送出去。
TCP连接的端点叫做套接字。它是由IP和端口号构成。每一条TCP连接唯一的被通信两端的两个端点所确定。同一个IP地址可以有多个不同的TCP连接,而同一个端口号也可以出现在多个不同的TCP连接中。
TCP发送的报文段是交付给IP层传送的,但IP层只提供尽最大努力的交付。也就是说TCP下面的网络所提供的是不可靠的服务。可靠传输必须依靠TCP来实现。停止等待协议就是一种方式。
停止等待就是每发送完一个分组就停止发送,等待接收方的确认,在收到确认后再传送下一个分组。如果发送方在一段时间后仍然没有收到确认,就认为刚才发送的分组丢失了,因而重传前面发送过的分组,这被称为超时重传。要实现超时重传,就要在每发送完一个分组后设置一个超时计时器。如果在超时计时器到期之前收到了对方的确认,就撤销已设置的超时计时器。因此发送方在发送完一个分组之后,必须暂时保存已发送的分组的副本。只有在收到响应的确认后才能清除暂时保留的分组的副本。分组和确认分组都必须进行编号,这样才能知道哪一个发送过的数据已被确认,那些没有收到确认。超时计时器设置的重传时间应该比数据在分组传输的平均往返时间更长一些。超时重传时间的设定是非常复杂的,因为已发送的分组到底经过那些网络,以及这些网络会产生多大的延迟都是不确定的。
如果发送方发送了数据后,在超时时间内没有收到接收方的确认,它会重传数据。如果此时接受方再次接收到了此数据,它会将此数据丢弃,并向发送方发送确认。发送方没有收到确认,可能是因为接受方的确认出错或丢失。发送方还可能收到重复的确认,对待重复的确认只需要丢弃即可。上述可靠的传输协议被称为自动重传请求ARQ(Automatic Repeat Request)。采用停止等待协议可能会导致信道利用率非常低,为了提高传输效率,需要使用流水线传输。也就是说发送方可以连续发送多个分组,不必没发完一个分组就停下来等待对方的确认。这样可以使信道上一直有数据不间断的在传送。这被称为连续ARQ协议或滑动窗口协议。它比较复杂但却是TCP协议的精髓。
所谓滑动窗口就是说位于此窗口内的分组可以被连续的发送出去,而不需要等待对方的确认。发送方每收到一个确认,就会把发送窗口向前滑动一个分组的位置。假设此时1-5个分组位于发送窗口内,这5个分组就会被连续的发送出去。当发送方收到第一个分组的确认后,就会向后移动一个分组的位置,此时就可以发送第六个分组了。接收方一般都采用累积确认的方式。也就是说,接收方不必对收到的每个分组逐个发送确认,而是可以收到几个分组后,对按序到达的最后一个分组发送确认。这样就表示到这个分组为止的所有分组都被正确接收。如果此时发送方发送了前5个分组,而第三个分组丢失了,这是接收方只能对前两个分组发送确认。发送方不知道后面三个分组的下落。实际上仅仅第三个分组没有收到,但是发送方仍然会发送后三个分组。
TCP是面向字节流的,但是TCP传送的数据却是报文段。一个TCP报文段分为首部和数据两部分。只有真正弄清TCP的首部各字段的作用才能掌握TCP的原理。
TCP的前20个字节是固定的。后面的40字节是根据需要增加的。因此TCP首部的最小长度时20。
1:源端口和目的端口:各占两个字节。
2:序号:四个字节。共2的32次方个序号。TCP连接中传送的字节流中的每一个字节都按顺序编号。此处的序号字段指的是本报文段所发送的数据第一个字节的序号。
3:确认号:4个字节。表示期望收到对方下一个报文段的第一个数据字节的序号。如确认号是N,则表明序号N-1之前的数据都正确收到。确认号也是4个字节,可以对4GB数据进行编号。
4:数据偏移,4位。它指出TCP报文段的数据起始处距离TCP报文段起始处有多远。这个字段实际上指出了TCP的首部。由于首部中有长度不确定的选项字段,因此此数据偏移是必要的。数据偏移只有4位,但是它的一个单位代表4字节。因此数据偏移的最大值是60(15*4)。即TCP首部的最大长度。
5:保留 6位。保留为以后使用。都是0.
6:紧急URG 当此处为1时,表明紧急指针字段有效。它告诉系统此报文段有紧急数据,需要尽快传送。系统会把紧急数据插入TCP缓存的最前面。它和紧急指针字段配合使用。
7:确认ACK:当ACK=1时,确认号字段才有效。当ACK=0时确认号无效。当连接建立前ACK=0,建立之后ACK就一直是1了。
8:推送PSH。当PSH=1时,系统会立即将此报文段发送出去,而不再等待整个缓存都被填满后才向上交付。
9:复位RST。当其为1,时,表明TCP出现严重错误,必须释放连接然后再重新建立连。
10:同步SYN:在连接建立时用来同步序号。当其为1而ACK=0时,表明这是一个(同步)连接请求报文段。当对方同意后,应在响应报文段中使用SYN=1,ACK=1。因此SYN=1,要么表示连接请求要么表示连接接受报文。
11:终止FIN:用来释放连接。当其为1时,表明此报文段的发送方的数据已经发送完毕,要求释放连接。
12:窗口:2字节。窗口指发送方的接收窗口。它告诉对方:从本报文段首部中的确认号算起,接收方目前允许对方发送的数据量。之所以有此限制是因为接收方的数据缓存空间是有限的。它指出现在允许对方发送数据量。由于接收缓存不断变化,因此窗口值也不断变化。
13:检验和:2字节。检验和检验 的是首部和数据这两部分。在计算时还应加上12字节的伪首部。
14:紧急指针:2字节,此字段只在紧急URG=1时才有意义。它指出本报文段中紧急数据的字节数。由于紧急数据放在了缓冲区最前方,紧急指针指出的是紧急数据的末尾在报文段的位置。当所有的紧急数据都处理完后,TCP 就告诉应用程序恢复正常操作。
15:选项:长度可变,最长可达40字节。当没有选项时,首部长度时20.
MSS是选项的一种。它被称为最大报文段长度,是每个TCP报文段中数据字段的最大长度。它加上TCP首部才是整个TCP报文段。再加上20字节的IP首部才能组装成一个IP数据报。当MSS非常小时,网络的利用率就低。如果发送只含一个字节的数据时,在IP层传输至少需要40字节的开销(IP头20字节,TCP头至少20)。到链路层还需要开销。如果MSS非常大,在IP层传输时就可能要分片,到终点后再将各个分片组装成原来的TCP报文段,这也会使开销增大。因此,MSS可以尽量大,只要在IP层传输时不分片就行了。MSS的默认长度时536。
窗口扩大选项是为了扩大窗口,窗口字段长度是16位,因此最大的窗口大小是64字节。窗口扩大选项占3个字节。每一个字节表示移位值。新的窗口值等于原来的16+窗口扩大选项的值。这相当于把窗口值向左移动移位值位。
时间戳选项占10字节,最主要的两个地段是:时间戳值和时间戳回送回答字段。
时间戳选项具有以下两个功能
1:计算往返时间。
2:用于处理TCP序号超过2的32次方的情况。这又被称为防止序号绕回。在报文段中加入时间戳就可以区分新的报文段和迟到很久的报文段。
socket的两端分别有两个窗口,发送窗口和接收窗口。滑动窗口的单位是字节,假设A收到了B的确认报文段,其中窗口时20,而确认号是31,这表明B期望接受的下一个序号是31,到序号30为止的数据已经收到了。根据这两个数据,A就构造自己的发送窗口。它的起始值为31,而末尾值为50。发送窗口表示在没有收到B的确认前,A可以连续把窗口内的数据都发送出去。
凡是已经发送过得数据,在未收到确认之前都必须暂时保留,以便在在超时重传。发送窗口内的序号表示允许发送的序号。
发送窗口与缓存的关系。
发送窗口存储应用程序将要发送的数据,以及发送但却为收到确认的数据。 发送窗口只是发送缓存的一部分,已经被确认过的数据应当从发送缓存中删除。因此发送缓存和发送窗口的后沿是重合的。
接收缓存用来暂时存放按需到达的,但还未被应用程序读取的数据以及未按序到达的数据。 如果收到的分组有差错,就要丢弃。如果接收应用程序来不及读取,接收缓存就会被填满,就会减少接受窗口,直到减为0。反之就可以增大。
虽然发送方的发送窗口是根据接收方方接收窗口设置但是,但同一时刻,发送窗口并不一定与接收窗口相同。这是因为通过网络传送窗口值需要经历一定的延迟。
TCP要求接收方必须有累计确认的功能,这样可以减小传输开销。接收方可以在合适的时候发送确认,也可以在自己有数据发送时把确认信息捎上。但应该注意的是接收方不应过分推迟发送确认,否则会导致发送方不必要的重传,这反而会浪费网络资源。
TCP利用滑动窗口来实现流量控制。所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接受。在连接建立时,发送方告诉接收方我的接受窗口是x,接收方收到后会设置自己的发送窗口。发送方的发送窗口不能超过接收方给出的接收窗口的数值。TCP窗口的单位是字节,而不是报文段。在每次发送数据是报文段内会将此时接受窗口告诉发送者。发送者会根据收到的报文段接收窗口的值调整自己的发送窗口。当接收方接收缓存满时就发送给发送方零窗口,告诉发送方停止发送。假设过一段时间接收方调整接收窗口为100,而此报文段在传送过程中丢失,这就导致发送方等待接收方的非零窗口通知,而接收方在等待发送方的数据。这样就导致了死锁。为了防止这种情况,TCP为每个连接设置一个持续计时器。TCP连接的一方收到零窗口通知后,就启动计时器,设置的时间到期后它会发送一个探测报文段。如果此时返回的仍然是零窗口,则重新设定计时器。如果窗口不是零,那么死锁的僵局就可以被打破了。
TCP是面向连接的协议。分为三个阶段:
一:连接建立
二:数据传送
三:连接释放。
在TCP连接建立的过程中需要解决一下三个问题:
1:要使每一方能够通知对方的存在。
2:允许双方协商一些参数。如窗口最大值、是否使用窗口扩大选项和时间戳选项。
3:能够对运输实体资源,如缓存进行分配。
TCP连接的建立采取客户服务器方式。主动发起连接的叫做客户。被动等待连接建立的应用程序叫做服务器。
TCP的建立连接需要客户服务器通信三次。这就是常说的三次握手。
首先服务器创建socket,绑定端口并进入监听模式等待客户端的请求。客户端B向服务器发送连接请求报文段。此时同部位SYN=1,初始序号seq=x,ACK=0(连接未建立,确认字段无效)。此时客户端进入同步--已发送状态。
服务器收到连接请求,如同意建立连接,则会发送确认。此时SYN=1,ACK=1,确认号ack=x+1。设置自己的初始序号seq=y。此时服务器进入同步--收到状态。
客户端收到确认后,还要给服务器进行确认,此时ACK=1,ack=y+1.seq=x+1。此时的报文段已经可以携带数据了。此时客户端进入已经建立状态。
服务器收到后也进入已建立状态。
很多人会很疑问会什么客户端还需要发送一次确认,这主要是防止已失效的连接请求报文段突然又传送到服务器。比如客户端发出连接请求,但此请求丢失,没有收到服务器发来的额确认。于是一段时间后客户端继续请求,此请求被服务器接收,客户端收到了服务器的请求。数据传输完毕后,连接被释放。如果此时客户端第一次发送的报文段在网络中滞留一段时间后到达服务器,服务器收到这个失效的请求,却误以为是客户端发出的新的连接请求。于是向客户端发送同一连接请求。由于客户端没有发出连接请求,当然会将此报文丢弃。而服务器却一直等待客户端的回复。
TCP连接的释放。
数据传输结束后任何一方都可以申请释放连接。A向B发送连接释放报文段,并停止发送数据。主动关闭TCP连接。此时 A的连接释放报文段首部FIN应置为1,序号seq=x,等于前面已传送过得数据的最后一个序号加一。此时A进入终止--等待(FIN-WAIT-1)状态1。等待B的确认。TCP规定FIN报文段即使不携带数据也要消耗一个序号。
B收到连接释放报文段后即发出确认,确认号ack是x+1。seq=y。然后B就进入了关闭--等待(CLOSE_WAIT)状态。此时 从A到B这个方向的连接已经释放了,这是TCP处于半关闭状态。即A不会在发送数据,但B要发送数据A 仍要接收。A收到B的确认后就进入了终止--等待(FIN-WAIT-2)状态2,等待B发出的连接释放报文段。若B没有 数据要发送给A,它发出连接释放报文段将FIN置为1,ACK=1,seq=w,ack=u+1.此时B处于最后确认状态。 等待A的确认。A在收到B的连接释放报文段后,必须对此进行确认。将ACK置为1,确认号ack=w+1, seq=u+1。然后进入时间--等待(TIME-WAIT)状态,此时TCP连接还没有被释放,还必须经过时间计时器设置的2MSL后 A才进入关闭状态。MSL为Maximum segment Lifetime。即最长报文段寿命。
之所以要设置这个最大报文段寿命,有两个原因:
1:为保证A发送的最后一个ACK报文能够到达B。因为它有可能丢失,此时便导致B收不到对方对FIN+ACK报文段的确认。此后B会超时重传。由于A设置了2MSL计时器,使得A有机会收到B的报文。如果A不设置2MSL,而是发送完ACK报文段后立即释放,就无法收到B重传的FIN+ACK报文段。B也就无法按正常步骤进入CLOSED状态。
2:防止已失效的连接请求报文段出现。A发送完最后一个ACK报文段后,等待2MSL就可使本此连接所产生的所有报文段都从网络中消失,不会对下一次的连接造成影响。
除等待计时器外,TCP还设置一个保活计时器。如果客户端主动与服务器建立起连接,而此后客户端突然出现故障,此后服务器不能收到从客户端发来的数据,此时应采取一定的措施,是服务器不白白等待。服务器每收到一次客户端的数据就重置保活计时器,时间通常是2小时。若两小时没有收到客户端的数据,服务器就发送探测报文,以后每隔75分钟发送一次,若一连发送10探测报文段后仍无客户端的响应,服务器就认为客户端出现故障,接着就关闭了连接。
在客户--服务器模型中,通常务器分配的一个socket对某一端口进行监听。一旦监听到有连接请求。就调用accept,accept将会产生新的socket。然后服务器创建新的线程,新的socket将会作为新线程的参数。系统会为这个新的socket分配一个服务器端的自由端口号,这个套接字专用于在连接建立后与客户端交换数据。它一般被称为响应套接字。
看一个例子:
-
SADATA wsd;
-
int err;
-
err=WSAStartup(MAKEWORD(2,2), &wsd);
-
if(err!=0)//网络有问题
-
{
-
printf("发生未知网络错误,程序将退出");
-
-
return 0;
-
}
-
SOCKET ListenSocket=socket(AF_INET,SOCK_STREAM,IPPROTO_IP);;
-
struct sockaddr_in local,client;
-
int iAddrSize;
-
local.sin_addr.s_addr = htonl(INADDR_ANY);
-
local.sin_family = AF_INET;
-
local.sin_port = htons(80);
-
if(bind(ListenSocket, (struct sockaddr *)&local,sizeof(local))==SOCKET_ERROR)//绑定到80端口。
-
{
-
printf("bind error\r\n");
-
return 0;
-
}
-
if(listen(ListenSocket, SOMAXCONN)==SOCKET_ERROR)//监听失败
-
{
-
printf("Listen error!\n");
-
}
-
printf("Listen on 80!\n");
-
int num=1;
-
while(1)
-
{
-
iAddrSize = sizeof(client);
-
SOCKET sServerSocket = accept(ListenSocket,(struct sockaddr *)&client,&iAddrSize);
-
-
if(sServerSocket!=INVALID_SOCKET)
-
{
-
-
printf("IP: %s 连接\r\n",inet_ntoa(client.sin_addr));
-
DWORD TID;
-
HANDLE hHandle= CreateThread(NULL,NULL,RecvThreadDirect,(LPVOID)sServerSocket,0,&TID);//创建线程处理请求,参数为请求连接的socket。
-
printf("创建第%d个线程",num);
-
num++;
-
//WaitForSingleObject(hHandle,INFINITE);
-
CloseHandle(hHandle);
-
Sleep(100);
-
-
}
-
else
-
break;
-
}
-