赞
踩
目录
4、在三次握手过程中,如果服务器一直收不到客户端的ack会发生什么?
3、socket中的close是一次就关闭的吗?半关闭状态是怎么产生的?
1、为什么需要TIME_WAIT状态?为什么 TIME_WAIT 状态还需要等 2MSL后才能返回到 CLOSED 状态?
五、connect()、listen()和accept()三者之间的关系
TCP数据包每次能够传输的最大长度 = MTU(1500B) - IP头(20B)- TCP头(20B)= 1460Bytes。
【重要的字段】:
源端口号与目的端口号:标识了发送方与接收方的地址,IP地址和端口号合称为套接字。
序列号和确认号:32位序列号与32位确认序号:序列号与确认号可以理解成两个通信进程在收发数据的时候互相应答的信息。比如说:A进程从序列号1000开始给B进程发送数据,发送五个数据。那么在B收到数据回复的时候,这里A的确认序列号应该是从1006,如果不是1006,比如说是1003,那就意味着1004、1005数据包B没有收到,于是A启动重发机制。这也就保证了数据的可靠性,也是TCP的特点之一。序列号是进程发送消息的号码,而确认号是期望目的进程返回的号码。进行比对,从而验证数据包是否到达。
4位TCP报头长度:这里的四位TCP报头长度,可以理解成四个比特位表示长度,四位比特位表示的值乘以四就是该TCP头部的长度。由图可知,报头最短长度为20字节,也就是说这里的四位TCP报头长度默认为0101。并且TCP报头长度不可超过15*4=60个字节。
标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下:
16位窗口大小:窗口大小标志着TCP缓冲区内部剩余空间的大小,起到一个流量控制的作用。如果窗口满了,那么这个时候是不允许数据接收的。后面到达的数据会被丢失。
16位校验和:这里的校验和由发送端填充,CRC校验。接收端校验数据的时候如果校验不通过,那么认为数据有问题。此处的校验和不仅仅校验TCP首部,还校验数据部分。
16位紧急指针:标识哪部分的数据为紧急数据。
在 TCP/IP 协议中,TCP 协议提供可靠的连接服务,采用三次握手建立一个连接。
第一次握手:Client先产生一个初始序列号Seq = ISN(C), 作为SYN并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。
第二次握手:Server收到数据包后也发送自己的SYN报文作为响应,并初始化序列号Seq = ISN(S),为了确认Client的Seq,Server将Client发送的Seq加1,作为ACK发送给Client,Server进入SYN_RCVD状态。(SYN为synchronize的缩写,ACK为acknowledgment的缩写)。
第三次握手:为了确认Server的SYN,Client将Server发送的Seq加1,作为ACK发送给Server。Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。
通过这样的三次握手,客户端与服务端建立起可靠的双工的连接,开始传送数据。 三次握手的最主要目的是保证连接是双工的,可靠更多的是通过重传机制来保证的。
半连接状态:发生在TCP三次握手过程中,客户端向服务器发起连接,服务器也进行了回应,但是客户端却不进行第3次握手。
半打开状态:在TCP连接中,如果某一端关闭了连接或者是异常关闭,则该连接处于半打开状态。解决半打开问题:引入心跳机制就可以察觉半打开状态。
半关闭状态:当TCP链接中客户端向服务器发送 FIN 请求关闭,服务端回应ACK之后,并没有立即发送 FIN 给客户端,客户端就处于半关闭状态,此时客户端可以接收服务器发送的数据,但是客户端已经不能再向服务器发送数据。
在三次握手过程中,服务器发送SYN-ACK之后,收到客户端的ACK之前的TCP连接称为半连接(half-open connect)。此时服务器处于SYN_RCVD状态。当收到ACK后,服务器转入ESTABLISHED状态。
Syn攻击就是攻击客户端在短时间内伪造大量不存在的IP地址,向服务器不断地发送syn包,服务器回复确认包,并等待客户的确认,由于源地址是不存在的,服务器需要不断的重发直至超时,这些伪造的SYN包将长时间占用未连接队列,正常的SYN请求被丢弃,目标系统运行缓慢,严重者引起网络堵塞甚至系统瘫痪。
- // 检测是否被Syn攻击
- netstat -n -p TCP|grep SYN_RCVD
一般较新的TCP/IP协议栈都对这一过程进行修正来防范Syn攻击,修改tcp协议实现。主要方法有SynAttackProtect保护机制、SYN cookies技术、增加最大半连接和缩短超时时间等。但是不能完全防范syn攻击。
为了保证服务端能收接受到客户端的信息并能做出正确的应答而进行前两次(第一次和第二次)握手,为了保证客户端能够接收到服务端的信息并能做出正确的应答而进行后两次(第二次和第三次)握手。可以用聊天来类比:
服务端会给每个待完成的半连接都设一个定时器,如果超过时间还没有收到客户端的ACK消息,则重新发送一次SYN-ACK消息给客户端,直到重试超过一定次数时才会放弃。这个时候服务器需要分配内核资源维护半连接。
这样做主要是为了保证网络安全,如果不是随机产生初始序列号,黑客将会以很容易的方式获取到你与其他主机之间通信的初始化序列号,并且伪造序列号进行攻击,这已经成为一种很常见的网络攻击手段。
第一次挥手:Client发送一个FIN,Seq=K,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态;
第二次挥手:Server收到FIN后,发送一个ACK(K+1)给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态;
第三次挥手:Server发送一个FIN,Seq=L,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态;
第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK(L+1)给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
在三次握手时,服务端可以把 ACK 和 SYN(ACK 起应答作用,而 SYN 起同步作用)放在一个报文里来发送。但是四次挥手时,服务端发送的FIN与ACK是分开发送的。
原因在于:首先FIN信号是由于调用close所以才发送的,而ACK是由内核发送的,所以ACK报文和FIN报文在发送的时间上都是分开的,不一定能同时发送。但是三次握手的时候发送SYN是由内核直接完成的,所以这就可以达到一个同步发送的情况。
如果服务器的代码没有调用close,那么意味着并没有发送FIN结束报文段。那么也就是说,此连接的服务器长期保持在CLOSE_WAIT状态,这会有什么影响?
服务器长期保持在CLOSE_WAIT状态,也就是说分配的文件描述符并没有关闭并归还。那么大量的CLOSE_WAIT存在的话,就会导致一种资源的泄漏,可能到最后就没有可分配的文件描述符了,那么就会使一些客户端无法连接,从而造成不可估量的影响。
使用close中止一个连接,但它只是减少文件描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为0时才关闭连接。
客户端的半关闭状态:收到服务器的ACK后,暂时关闭写端,但是读端并没有关闭,依然可以接受来自服务器的数据。
TCP设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75分钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
TIME_WAIT状态对大并发服务器的影响,应尽可能在服务器避免出现TIME_WAIT状态,如果服务器端主动断开连接,服务端就会进入TIME_WAIT状态(主动发起关闭连接的一方),协议设计上,应该让客户端主动断开连接,这样就把TIME_WAIT状态分散到大量的客户端。如果客户端不活跃了,一些客户端不断开连接,这样子就会占用服务器端的连接资源。服务器端也得有个机制踢掉不活跃的连接。
保证可靠(可靠性是TCP最根本的特征)地终止TCP连接:处于TIME_WAIT状态的客户端会向服务端发送ACK,如果此时ACK丢失,目的端会超时重传FIN报文段,目的端收到重传的报文段最少需要2MSL,所以发送端会等待2MSL时间;
客户端在发送ACK后,再等待2MSL时间,可以使本次连接所产生的数据段从网络中消失,从而保证关闭连接后不会有还在网络中滞留的数据段去骚扰服务端。
我们知道服务端收到ACK,关闭连接。但是客户端无法知道ACK是否已经到达服务端,于是开始等待?等待什么呢?假如ACK没有到达服务端,服务端会为FIN这个消息超时重传 timeout retransmit ,那如果客户端等待时间足够,又收到FIN消息,说明ACK没有到达服务端,于是再发送ACK,直到在足够的时间内没有收到FIN,说明ACK成功到达。这个等待时间至少是:服务端的timeout + FIN的传输时间,为了保证可靠,采用更加保守的等待时间2MSL。
客户端发出ACK,等待ACK到达对方的超时时间 MSL(最大报文生存时间),等待FIN的超时重传,也是MSL,所以如果2MSL时间内没有收到FIN,说明对方安全收到ACK。
作为服务器,短时间内关闭了大量的Client连接,就会造成服务器上出现大量的TIME_WAIT连接,占据大量的tuple,严重消耗着服务器的资源;
作为客户端,短时间内大量的短连接,会大量消耗Client机器的端口,毕竟端口只有65535个,端口被耗尽了,后续就无法再发起新的连接了。
通过调整内核参数解决,编辑文件/etc/sysctl.conf,加入以下内容:
- /*表示开启SYN Cookies。当出现SYN等待队列溢出时, 启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭*/
- net.ipv4.tcp_syncookies = 1;
- /*表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接, 默认为0,表示关闭*/
- net.ipv4.tcp_tw_reuse = 1;
- /*表示开启TCP连接中TIME-WAIT sockets的快速回收, 默认为0,表示关闭*/
- net.ipv4.tcp_tw_recycle = 1;
- /*修改系默认的 TIMEOUT 时间 然后执行 /sbin/sysctl -p 让参数生效*/
- net.ipv4.tcp_fin_timeout = 30;
对于客户端的 connect() 函数,该函数的功能为客户端主动连接服务器,建立连接是通过三次握手,而这个连接的过程是由内核完成,不是这个函数完成的,这个函数的作用仅仅是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手连接,最后把连接的结果返回给这个函数的返回值(成功连接为0, 失败为-1)。通常的情况,客户端的 connect() 函数默认会一直阻塞(可以使用fcntl()函数或者ioctl()函数把connect()变为非阻塞),直到三次握手成功或超时失败才返回(正常的情况,这个过程很快完成)。
对于服务器,它是被动连接的。这里需要注意的是,listen()函数不会阻塞,它主要做的事情为,将该套接字和套接字对应的连接队列长度告诉 Linux 内核,然后,listen()函数就结束。
所以,只要 TCP 服务器调用了 listen(),客户端就可以通过connect() 和服务器建立连接,而这个连接的过程是由内核完成。
在被动状态的socket有两个队列,一个是正在进行三次握手的socket队列,一个是完成三次握手的socket队列。在握手完成后会从正在握手队列移到握手完成的队列,此时已经建立连接。
accept()函数功能是,从连接队列头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。TCP 的连接队列满后,会延时连接。
在确认应答机制中,对每一个发送的数据段,都要给一个ACK确认应答,收到ACK后再发送下一个数据段。这样做有一个比较大的缺点,就是性能较差。尤其是数据往返时间较长的时候。那么我们可不可以一次发送多个数据段呢:滑动窗口。
所谓的流量控制就是让发送方的发送速率不要太快,让接收方来得及接收。利用滑动窗口机制可以很方便的在TCP连接上实现对发送方的流量控制。
接收端窗口rwnd(recv window):接收端缓冲区大小。接收端将此窗口值放在 TCP 报文的首部中的窗口字段,传送给发送端;
拥塞窗口cwnd(congestion window):发送端缓冲区大小;
发送窗口swnd(send window):发送窗口的上限值 = Min [rwnd, cwnd]。
如图,假设窗口大小为4000。那么就是是说图上1001-5001之间的数据是可以一次性全部发送,并且不需要等待ACK响应的。当发送完这4000的数据后,此时等待。等到ACK响应中确认收到1001-2000的数据时,滑动窗口右移。
以此类推。滑动窗口内部的数据都是已经发送但是没有收到ACK应答的数据,滑动窗口左侧都是已经收到了ACK应答的数据,滑动窗口右侧是未发送的数据。
【发送端发送的数据丢包】:
这里1001-2000的数据报文丢失,此时发送端并不知道,继续发送滑动窗口内的报文。这时候接收到了三条重复的ACK响应(TCP协议规定,收到三条相同的ACK响应就出发重传)。此时重传1001-2000的数据报文。重传完毕后,等待接收端的响应。此时接收端响应为下一条是6001的数据报文,也就是说在2001-6000的报文都接收到了,这段报文被放到了接收端的接收缓冲区中。那么此时滑动窗口继续移动,发送端继续发送。
接收到三个重复的ACK响应,就开始重传响应所要求的报文的机制就是快重传速机制。
【接收端ACK响应丢包】:
接连三条的ACK响应都丢包,但是第四条ACK响应到达。此时并不会影响发送端的发送。因为ACK响应的是下一条数据包是4001,这意味着接收端已接收到前面的所以报文数据。所以接收端ACK响应丢包其实对发送端发送的影响并不是那么大,后续的ACK响应能够处理好这个问题。
假如发送方发送segment “1234567”,接收方接到的是“1235”,接收方接到5的那一瞬间,知道4有可能丢了,也有可能乱序了,至于是哪种情况,接收方无从知晓。
如果连续收到两个ACK,极有可能是乱序问题,如果收到三个ACK,那么很大概率是丢包了,这个值在实际过程中也不一定就是三,可能远大于3,只是一个经验值。
拥塞控制也就是考虑当前的网络环境,动态调整窗口大小,没有发生拥塞情况,则窗口增大,拥塞了窗口减小,如此往复,最终应该接近与接收端的窗口大小。
在开始发送信息时,由于不知道具体的网络环境,为避免大量信息造成的拥塞现象,此时的拥塞窗口以最小值(即拥塞窗口和接收端窗口中的较小值)进行数据发送,并设定门限值作为慢启动算法和拥塞避免算法的分割点。慢启动是指以最小的拥塞窗口按照指数形式递增,达到门限值后,以拥塞避免算法,即线性递增方式增大拥塞窗口(这里递增时间间隔为一个往返时间RTT)。
在上述过程中,无论是窗口大小指数递增或者线性递增,当发生拥塞现象,则门限值更新为当前窗口大小的一半,拥塞窗口大小变为最小值,重复上述递增过程(此时属于网络环境限制,所以在接收端和拥塞窗口两个限制条件中选择拥塞窗口作为限制)。
当发送端连续收到三个重复的ack时,表示该数据段已经丢失,需要重发。当收到三个表示同一个数据段的ack时,不需要等待计时器超时,立即重新发送数据段(当时这三个ack要在超时之前到达发送端),因为能够收到接收端的ack确认信息,所以数据段只是单纯的丢失,而不是因为网络拥塞导致,所以此时不需要拥塞窗口更新为最小值进行慢启动(如果这样的话,反倒因为拥塞窗口的增长需要时间,可能导致性能降低),此时需要设置拥塞窗口大小为:门限值大小+3,当然此处的门限值已经更新为拥塞窗口值的一半大小,该行为也就是所谓的“乘法减少”,更新之后按照拥塞避免算法继续进行。
不连续的数据段会严重影响TCP的传输效率。而快速修复这种不连续,会释放掉占用的空间,加快发送方的传输效率。
窗口大小首先以指数递增去探测一下网络的拥塞程度,执行拥塞避免算法后,拥塞窗口线性缓慢增大,防止网络过早出现拥塞。
TCP是基于字节流传输的,只维护发送出去多少,确认了多少,没有维护消息与消息之间的边界,因而可能导致粘包问题。
粘包问题本质上要在应用层维护消息与消息的边界。解决方案如下:
在接收端接收的时候采用定长的方式接收;
在数据包尾添加一些分隔符;
在数据包头部加上数据包长度;
更复杂的应用层协议。
【服务器(实线)】:
【客户端(虚线)】:
参考:http://harlon.org/2018/04/11/networksocket4/
https://bbs.csdn.net/topics/390566881
https://blog.csdn.net/lianghe_work/article/details/46443691
https://blog.csdn.net/htyurencaotang/article/details/11569905
https://www.cnblogs.com/popduke/p/5823801.html#1ref
https://blog.csdn.net/qzcsu/article/details/72861891
https://blog.csdn.net/omnispace/article/details/52701752
https://blog.csdn.net/lianghe_work/article/details/46458889
https://blog.csdn.net/sinat_36629696/article/details/80740678
https://blog.csdn.net/q1007729991/article/details/70142341?utm_source=blogxgwz0
https://www.cnblogs.com/gaopeng527/p/5255757.html
https://blog.csdn.net/wo16fafafa/article/details/52317050
http://harlon.org/2018/04/08/networksocket2/
https://www.cnblogs.com/thrillerz/p/6464203.html
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。