TCP虽然是面向字节流的,但TCP传送的数据单元却是报文段
。一个TCP报文段分为首部和数据两部分,而TCP的全部功能都体现在它首部中各字段的作用,因此,我们必须要弄清TCP首部各字段的作用才能掌握TCP的工作原理。
我们首先来看一下TCP报文段的结构:
TCP报文段首部的前20个字节是固定的,后面可以根据需要加选项,因此TCP首部的最小长度是20字节。首部固定部分各字段的意义如下:
源端口和目的端口:各占2个字节,分别写入源端口号,目的端口号。TCP的分用功能也是通过端口实现的。
序列号:占4字节,序号范围位[0,232-1],共232个序号,序号增加到232-1后,下一个序号就又回到0,即序号使用mod 232运算。序列号(seq)表示一次TCP通信(TCP连接建立到断开)过程中某一个方向上的字节流的每个字节的编号,首部中的序号字段值指的是本报文段所发送的数据的第一个字节的序号。序列号不会从0或1开始,是建立连接时由计算机生成的随机数作为其初始值ISN,通过SYN包传给接收端主机,然后再将每次转发过去的字节数累加到初始值上表示数据的位置。即:
seq=ISN+该报文段所携带数据的第一个字节在整个字节流中的偏移
。
此外,在建立连接和断开连接时发送的SYN包和FIN包虽然并不携带数据,但是也会作为一个字节增加对应的序列号
。例如,一报文段的序号字段值为301,seq=301,携带的数据共有100字节,这就表明,本报文段的数据的第一个字节的序号是301,最后一个字节序号为400,那么下一个报文段的seq应该从401开始,这里的301,401就是报文段序号。
确认号:占4字节,可对4GB数据编号,可重复使用,一般表示为ack,是期望收到下一个报文段的第一个数据字节的序号。例如,B正确收到了A发送过来的一个报文段,其序号字段值是seq=501,数据长度是200字节(序号501~700),这表明B正确收到了A发送的从501到700为止的数据,因此,B期望收到A的下一个数字序号为701,于是B在发送给A的确认报文段中把确认号ack置为701,所以:
ack=收到报文段序号值(seq)+数据长度+1
。
若确认号为N,则表明,到序号N-1为止的所有数据都已正确收到。
数据偏移:占4位。它指出TCP报文段的数据起始处距离TCP报文段的起始处有多远,表示了当前数据位于整个报文段的什么位置。所以可以根据数据偏移最大值算出TCP首部的最大长度。注意: 数据偏移的单位是32位字,即4字节;4位二进制能够表示的最大十进制数是15,即0001表示偏移1个单位,那就是偏移4字节;故最大偏移15*4=60字节,那么TCP首部长度就是60字节
,固定头部20字节,所以选项长度不能超过40字节。
保留:占6位,保留为今后使用,现在为0。
紧急URG控制位:当URG=1时,表明紧急指针字段有效,它告诉系统此报文段中有紧急数据,应尽快送达(此数据为高优先级数据),发送TCP会把紧急数据插入到本报文段数据最前面, 而不是按原来排队顺序来传送,这时要与首部中紧急指针字段配合使用。如中断命令,必须立即中断,而不是等所有数据处理完了再中断,这就要用到紧急数据,紧急控制位。
确认ACK:仅当ACK=1时确认号字段才有效,当ACK=0时,确认号无效。TCP规定,在连接建立后所有传送的报文段都必须把ACK置为1。 RFC793明确规定,除了第一个握手报文SYN除外,其它所有报文必须将ACK = 1。
推送PSH:当两个应用进程进行交互式通信时,有时在一端的应用进程希望再键入一个命令后立即就能够收到对方的响应。在这种情况下,TCP就可以使用推送操作。这时,发送方TCP把PSH置为1,并立即创建一个报文段发送过去,接收方TCP收到PSH=1的报文段,就尽快地(即推送向前)交付给接收应用进程,而不再等到整个缓存都填满了后再向上交付
。
复位RST:当RST=1时,表明TCP连接中出现严重差错,如由于主机崩溃或其他原因,必须释放连接,然后再重新建立传输连接。RST置1还用来拒绝一个非法的报文段或拒绝打开一个连接。RST也可称为重建位或重置位。
同步位SYN:在连接建立时用来同步序号,当SYN=1而ACK=0时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使SYN=1,ACK=1。因此,SYN置为1就表示这时一个连接请求或连接接受报文。
终止FIN:用来释放一个连接。当FIN=1时,表明此报文段的发送方的数据已发送完毕,并要求释放运输连接。
窗口:占2字节,窗口值是[0,216-1]之间的整数。窗口指的是发送本报文段的一方接收窗口
(而不是自己的发送窗口)。窗口值告诉对方:从本报文段首部中的确认号算起,接收方目前允许对方发送过的数据量。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。总之,窗口值作为接收方让发送方设置其发送窗口的依据。
如:设确认号为701,窗口字段是1000,这就表明,从701号算起,发送报文段的一方还有接收1000个字节数据(字节序号是701~1700)的接收缓存空间。记住:窗口字段明确指出了现在允许对方发送的数据量,窗口值是经常在动态变化着。
检验和:占2字节。检验和字段检验的范围包括首部和数据这两部分,和UDP用户数据报一样,在计算检验和时,要在TCP报文段的前面加上12字节的伪首部。伪首部的格式与UDP一样,字段有些许差异。接受方收到此报文段后,仍要加上这个伪首部来计算检验和。
紧急指针:占2字节,紧急指针仅在URG=1时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据)。因此紧急指针指出了紧急数据的末尾在报文段中的位置,当所有紧急数据都处理完时,TCP就告诉应用程序恢复到正常操作。值得注意的是:即使窗口为零时也可发送紧急数据
。
选项:长度可变,最长可达40字节,当没有使用选项时,TCP的首部长度是20字节。常用选项有:MSS选项表示每一个TCP报文段中数据字段的最大长度,未设置默认536字节,因此,所有在因特网上的主机都应能接受的报文段长度未536+20=556字节
;窗口扩大选项;时间戳选项等。
在TCP网络编程中我们详细讲解了如何实现TCP传输数据的过程。现在我们需要知道在TCP三次握手建立连接时,函数的关系:
1. listen()函数
服务器调用listen()函数被动接受连接,将套接字设置为监听状态
,创建一个监听队列以存放待处理的客户连接并将其长度告诉Linux内核。故 只要TCP服务器调用listen(),客户端就可以通过connect()和服务器进行连接
,这个连接过程是通过内核完成的。
注意:
listen()函数不会阻塞
。2. connect()函数
客户端调用connect()函数主动与服务器建立连接,即进行三次握手
,这个过程是在listen()之后,accept()之前。
注意:
三次握手䣌连接过程是内核完成的,不是这个函数完成的
,这个函数的作用仅仅是通知给Linux内核,让Linux内核自动完成TCP三次握手连接。connnect()会阻塞
,直到三次握手成功,或超时失败。3. accept()函数
服务器调用accept函数从内核已完成三次握手的监听队列中接受一个连接。
注意:
如果此时队列中没有完成三次握手的连接,accept函数就会阻塞
,直到队列中有完成三次握手的连接为止。我们画出TCP三次握手建立连接,假定主机A运行的是TCP客户程序,而B运行TCP服务器程序。最初两端的TCP进程都处于CLOSED(关闭)状态。注意:A客户端主动打开连接,B服务器被动打开连接:
【1. 服务器初始化状态】
B的TCP服务器进程先创建传输控制TCB,这时socket,bind,listen函数都已经执行完毕,服务器准备接受客户进程的连接请求,所以此时 服务器进程处于LISTEN监听状态
,等待客户的连接请求,注意listen并没有阻塞。如有,即作出响应。
【2. 客户端发起连接,发送连接信息,第一次握手】
A的TCP客户进程也是首先创建传输控制模块TCB,然后执行connect函数,向B服务器发出连续请求报文段,这时TCP报文首部的同步位SYN=1,同时选择一个初始序号seq=x,这个x就是随机产生的整数ISN。TCP规定,SYN报文段(即SYN=1的报文段)不能携带数据,但要消耗掉一个序号。这时客户端A进入SYN-SENT同步已发送状态
。
【3. 服务器同意建立连接,回复确认信息,第二次握手】
B服务器收到连接请求报文段后,如同意建立连接,则向A发送确认。在确认报文段中SYN=1,ACK=1,确认号为ack=x+1(没有数据,所以长度为0,直接seq+1即可),同时也要为自己选择一个初始序号seq=y,请注意,这个报文段也不能携带数据,因为SYN=1,但同样要消耗掉一个序号。这时 TCP服务器进程进入SYN-RCVD同步收到状态
,客户端B进入ESTABLISHED已建立连接状态
。
【4. 客户端确认连接,发送确认连接信息,第三次握手】
TCP客户进程收到B服务器的确认后,还要向B给出确认。确认报文段的ACK置为1,确认号ack=y+1,而自己的序号seq=x+1,TCP标准规定,ACK报文段可以携带数据,但如果不携带数据则不消耗序号,即不用随机重新生成序号。在这种情况下,下一个数据报文段的序号仍是seq=x+1,这时,TCP连接已经建立,A服务器进入ESTABLISHED已建立连接状态
。
所以我们可以看到三次握手最后结果是:服务器知道了客户端发送数据的初始序列号,客户端也知道了服务器发送数据的初始序列号,它两也彼此知道对方知道了。 故总结来说TCP连接握手,握的就是通信双方数据初始的序列号, 三次握手的作用是:
假设现在有两个人A,B需要进行数据通信,那么就有下列的过程:
这时,A,B都知道了彼此知道自己的初始序列号,A,B也知道从对方发过来的ISN正确的位置开始读数据,故都可以放心的发数据,实现数据通信。 那么我们总结三次握手完成了两个重要功能:
服务器知道客户端发来数据的其实序列号,服务器也知道客户端知道自己的;客户端同理。即双方都做好了发送数据的准备工作,也知道对方准备好了
。在握手的过程中,双方对初始序列号seq进行协商,确认
。现在假设这样一种情况:A客户端发出的第一个连接请求报文段在某些网络结点长时间滞留了,但是并没有丢失,导致此报文段延误到连接释放后的某个时间才到达服务器B。这个请求连接报文段是一个已失效的报文段,但是B不知道,它以为A客户端又发出一次信的连接请求,于是就向A发出确认报文段,同意建立连接,那么现在我们考虑三次握手,两次握手出现的情况:
明显的可以判断出来 两次握手不可以 ,两次握手可能会产生以下的问题:
造成服务器资源的浪费
;从上面的例子我们可以看出,不进行第三次握手的确认,服务器可能就会维护许多不成功的连接,这些连接都不能正常通信,只能白白浪费服务器资源。
服务器给 每个处于SYN-RCVD状态的服务器都设定一个定时器
,如果 超过时间
还没有收到客户端第三次握手的回复,将会重新发送第二次握手的信息,知道重试超过一定次数时才会放弃。
seq序列号表示发送数据的起始字节位置,服务器/客户端可以通过序列号正确读取数据。如果不是随机分配序列号,那么黑客就会很容易获取到你与其他主机之间通信的初始化序列号
,并伪造序列号让其他主机读取病毒信息,发起攻击。
SYN flood攻击:
SYN flood攻击:又称为洪泛攻击,在第二次握手时受到的攻击:
我们可以通过下面这个图来看一下:
缩短SYN Timeout时间
;由于SYN Flood 攻击的效果取决于服务器上保持的SYN半连接数,这个值=SYN攻击的频度 x SYN Timeout ,所以通过缩短从接收到SYN报文到确定这个报文无效并丢弃该连接的时间,例如设置为20秒以下(过低的SYN Timeout 设置可能会影响客户的正常访问),可以成倍地降低服务器的负荷。设置 SYN Cookie
;就是给每一个请求连接的IP地址分配一个Cookie,如果短时间内连续受到某个IP的重复SYN报文,就认定是受到了攻击。以后从这个IP地址来的包会被丢弃。硬件防火墙
;SYN Flood攻击很容易就能被防火墙拦截。第二个阶段可能异常,如果 服务器相应的端口未打开,会恢复RST复位报文,握手失败,或者,listen监听队列达到上限,也可能失败。
数据传输结束后,通信双方都可以释放连接,现在A,B都处于ESTABLISHED状态。如下图所示:
描述整个过程:
【1. A客户端主动断开连接,发送连接释放报文段】
A客户端进程先向其TCP发出连接释放报文段,并停止再发送数据,主动关闭TCP连接。连接释放报文段首部的FIN=1,seq=u,seq等前面已传送过的数据的最后一个字节号好加1。A客户端进入FIN-WAIT-1终止等待1状态
,等待B服务器的确认。注意,TCP规定,FIN报文段即使不携带数据,它也消耗掉一个序号,所以给服务器发送确认报文时,seq=u+1。
【2. B服务器收到A的释放报文段,发出确认报文段】
B服务器收到连接释放报文段后即发出确认,确认号是ack=u+1,而这个报文段的序号是v,是服务器前面已传送数据的最后一个字节的序号加1。然后 B就进入CLOSE-WAIT关闭等待状态
。TCP服务器这时应通知高层应用进程,此时从A到B这个方向的连接就释放了,此时TCP连接处于半关闭状态
,即A客户端已经没有数据要发送了,但B服务器若发送数据,A客户端仍要接收,也就是说,从B到A这个方向的连接并未关闭,这个状态可能可以持续一些时间。
A客户端收到B的确认后,就 进入FIN-WAIT-2终止等待2状态
,等待B发出的连接释放报文段。
【3. B服务器释放连接,发出连接释放报文段】
若B服务器已经没有向A发送的数据,其应该用进程就通知TCP释放连接,这时B发出的连接释放报文段必须使FIN=1。现假定B序号为w,再半关闭状态B可能又发送了一些数据,B还必须重复上次已经发过的确认号ack=u+1,这时 B服务器就进入LSAT-ACK最后确认状态
,等待A的确认。
【4. A客户端收到B的释放报文段,发出确认报文段】
A客户端进入到TIME-WAIT(时间等待)状态
。现在TCP连接还没有释放掉,必须经过时间等待计时器(TIME-WAIT)设置的时间2MSL后,A客户端才进入到CLOSED状态
。时间MSL叫做最长报文段寿命,即报文段存活的最大时间,RFC793建议设为2分钟,现在可以根据情况使用更小的MSL值。因此从A进入到TIME-WAIT状态后,要经过4分钟才能进入到CLOSED状态,才可以建立下一个新的连接,当A撤销相应的传输控制块TCB后,就结束了这次的TCP连接。B服务器收到A的确认报文段,就会进入CLOSED状态
。 B在撤销相应的传输控制块TCB后,就结束了这次的TCP连接。B结束TCP连接的时间要比A早一些。【1. 三次握手】
在三次握手时,第一次握手我们收到客户端发来的SYN时,会做两件事:
在建立连接时,服务器收到客户端的SYN时,序列号后,不能进行通信等其他处理,所以收到客户端的,就可以把自己的也赶紧发过去,故这两件事可以一起完成,一起发送。没有必要分两次完成,故不是四次握手,是三次握手。
【2. 四次挥手】
在四次挥手时,第一次挥手我们收到客户端发来的FIN时,会做两件事:
在断开连接时,客户端断开了连接,表示我现在没有数据要给服务器了,服务器收到后这个报文段后,不可能出现:它知道客户端要断开,它也放下自己正在处理的事,或一会要发送的数据,也断开连接,直接将确认号和断开连接标识即ACK+FIN一起发过去,在大多数情况下这是不存在的。所以这两件事,你不可能同时去做,毕竟服务器可能还有未处理的数据。所以必须两次进行:
这样客户端两次,服务器两次,最后就是四次挥手。
可以,当特殊情况发生时,客户端断开连接发送FIN,服务器收到后,刚好已经处理完成了所有的数据,这时就可以将ACK+FIN一起发送过去,这样就是三次挥手,但是这种情况很少。
当服务器收到客户端的FIN,并回复ACK后,会进入CLOSE-WAIT状态,此时TCP连接处于半关闭状态, 即客户端断开了连接,不能发数据,但是服务器还可以发数据。
此状态如果一直存在则表示服务器没有close()发送FIN释放报文段,如果服务器长期处于这个状态,就会一直占用socket文件描述符,大量的CLOSE-WAIT状态存在就会导致文件描述符被占用,一些客户端无法连接。
我们在TCP编程中说过,close系统调用并非总是立即关闭一个连接,而是将fd的引用计数减1,只有当fd的引用技术为0时,才能真正的关闭。所以半关闭状态是指客户端colse关闭了连接,但是服务器没有关闭,此时还可以向客户端不断发送信息。
TCP中还设有一个保活计时器,当客户端主机出现故障,服务器不能再收到客户端发来的数据,因此,应当有措施使服务器不要再白白等待下去,这就是保活计时器。服务器每收到一次客户的数据,就重新设置保活计时器,时间的设置通常是两小时,若两小时没有收到客户的数据,服务器就发送一个探测报文段,以后则每隔75分钟发送一次。若连续发送10个探测报文段后仍无响应,服务器就认为客户端除了故障,接着关闭这个连接。
TIME-WAIT状态是主动关闭的一方收到了对方发来的FIN报文并且本端发送了ACK报文确认后的状态,TIME_WAIT 的引入是为了让 TCP 报文得以自然消失,同时为了让被动关闭方能够正常关闭。
主要有两个原因:
MSL是TCP报文的最大生存时间,2MSL 的时间是从客户端接收到 FIN 后发送给服务器 ACK 开始计时的,考虑到了重传的因素,那么就需要服务器再次给客户端传FIN+ACK报文段。故就有两个传输方向:
保证在两个传输方向上的尚未被接收或迟到的报文段都消失,理论上保证最后一个报文可靠到达,就需要2MSL,一个方向一个1MSL。
因为服务器的TCP连接没有完全断开之前是不允许重新监听,在某些情况下是不合理的。因为一般我们的服务器都会处理大量的客户端的连接,由于 请求量很大所以可能导致TIME_WAIT的连接数很多,每个连接都会占用一个通信tuple。所以 我们可以使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但是IP地址不同的多个socket描述符。
CLOSE-WAIT是被动关闭的一端在接收到另一端关闭请求(FIN报文段)并且将ACK发送出去后所处的状态,这种状态表示:收到了对端关闭的情况,但是本端还没有完成工作,未关闭。
TIME-WAIT是主动关闭的一端在本段已经关闭的前提下,收到对端的关闭请求并且将ACK发送过去后所处的状态,这种状态表示:双方都已经完成工作,只是为了确保迟来的数据报能被识别丢弃,可靠的终止TCP连接。
半连接:三次握手中,主动发起握手的一方不发最后一次ACK,使得服务器端阻塞在SYN_RECV状态
半关闭连接: A向B发送 FIN 请求关闭,另一端B回应ACK之后,并没有立即发送 FIN 给A,A方处于FIN_WAIT_2状态,此时A可以接收B发送的数据,但是A已经不能再向B发送数据,此时的TCP连接为半关闭连接。
半打开连接: 如果一方异常关闭(断网,断电),而另一方并不知情。处于半打开的状态,如果双方不进行数据通信,是无法发现问题的。可以引入心跳机制,以检测半打开状态,检测到了发送RST重新建立连接。
半关闭和半打开连接的最大区别就是:半关闭是一端收到另一方的FIN,也回复了ACK报文,即明确知道另一端已经关闭了;半打开连接则是一端没有收到另一方的FIN,也没有回复ACK,它不知道另一端已经因为故障关闭了。
紫色框框是TCP状态,红色是服务器进程的正常状态转换,蓝色是客户端进程的正常状态转换,黑色是异常变迁,即出现问题时的状态变换。
【服务器正常状态转换:】
【客户端正常状态转换:】
加油哦!。