https://yq.aliyun.com/articles/11332?spm=5176.8091938.0.0.uEdHju
TCP建立连接时,为什么要进行三次挥手?
每一次TCP连接都需要三个阶段:连接建立、数据传送和连接释放。
三次握手就发生在连接建立阶段。
解释一 | 1.为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。2.为了解决网络中存在延迟的重复分组的问题。 | client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用三次握手,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。采用三次握手的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。 |
---|---|---|
有趣解释二 | 问题的本质是,信道不可靠, 但是通信双发需要就某个问题达成一致. 而要解决这个问题, 无论你在消息中包含什么信息, 三次通信是理论上的最小值. 所以三次握手不是TCP本身的要求, 而是为了满足"在不可靠信道上可靠地传输信息"这一需求所导致的 | 请注意这里的本质需求,信道不可靠, 数据传输要可靠.三次达到了, 那后面你想接着握手也好, 发数据也好, 跟进行可靠信息传输的需求就没关系了.因此,如果信道是可靠的, 即无论什么时候发出消息, 对方一定能收到, 或者你不关心是否要保证对方收到你的消息, 那就能像UDP那样直接发送消息就可以了. 。 |
举个打电话的例子:
A : 你好我是A,你听得到我在说话吗
B : 听到了,我是B,你听到我在说话吗
A : 嗯,听到了
建立连接,开始聊天!
为什么TCP协议终止链接要四次?
1、当主机A确认发送完数据且知道B已经接受完了,想要关闭发送数据口(当然确认信号还是可以发),就会发FIN给主机B。
2、主机B收到A发送的FIN,表示收到了,就会发送ACK回复。
3、但这是B可能还在发送数据,没有想要关闭数据口的意思,所以FIN与ACK不是同时发送的,而是等到B数据
发送完了,才会发送FIN给主机A。
4、A收到B发来的FIN,知道B的数据也发送完了,回复ACK, A等待2MSL以后,没有收到B传来的任何消息,知道B已经收到自己的ACK了,A就关闭链接,B也关闭链接了。
- 大家知道,由于socket是全双工的工作模式,一个socket的关闭,是需要四次握手来完成的。
- 主动关闭连接的一方,调用close();协议层发送FIN包
- 被动关闭的一方收到FIN包后,协议层回复ACK;然后被动关闭的一方,进入CLOSE_WAIT状态, 主动关闭的一方等待对方关闭,则进入FIN_WAIT_2状态;此时,主动关闭的一方 等待 被动关闭一方的应用程序,调用close操作
- 被动关闭的一方在完成所有数据发送后,调用close()操作;此时,协议层发送FIN包给主动关闭的一方,等待对方的ACK,被动关闭的一方进入LAST_ACK状态;
- 主动关闭的一方收到FIN包,协议层回复ACK;此时,主动关闭连接的一方,进入TIME_WAIT状态;而被动关闭的一方,进入CLOSED状态
- 等待2MSL时间,主动关闭的一方,结束TIME_WAIT,进入CLOSED状态
通过上面的一次socket关闭操作,你可以得出以下几点: 1.主动关闭连接的一方 – 也就是主动调用socket的close操作的一方,最终会进入TIME_WAIT状态 2.被动关闭连接的一方,有一个中间状态,即CLOSE_WAIT,因为协议层在等待上层的应用程序,主动调用close操作后才主动关闭这条连接 3.TIME_WAIT会默认等待2MSL时间后,才最终进入CLOSED状态; 4.在一个连接没有进入CLOSED状态之前,这个连接是不能被重用的! 所以,这里凭你的直觉,TIME_WAIT并不可怕(not really,后面讲),CLOSE_WAIT才可怕,因为CLOSE_WAIT很多,表示说要么是你的应用程序写的有问题,没有合适的关闭socket;要么是说,你的服务器CPU处理不过来(CPU太忙)或者你的应用程序一直睡眠到其它地方(锁,或者文件I/O等等),你的应用程序获得不到合适的调度时间,造成你的程序没法真正的执行close操作。 这里又出现两个问题: 上文提到的连接重用,那连接到底是个什么概念? 协议层为什么要设计一个TIME_WAIT状态?这个状态为什么默认等待2MSL时间才会进入CLOSED
A为什么等待2MSL,从TIME_WAIT到CLOSE?
在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MS
L的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,
那么Client推断ACK已经被成功接收,则结束TCP连接。
这个网上转载的例子不错:
三次握手:
A:“喂,你听得到吗?”A->SYN_SEND
B:“我听得到呀,你听得到我吗?”应答与请求同时发出 B->SYN_RCVD | A->ESTABLISHED
A:“我能听到你,今天balabala……”B->ESTABLISHED
四次挥手:
A:“喂,我不说了。”A->FIN_WAIT1
B:“我知道了。等下,上一句还没说完。Balabala…..”B->CLOSE_WAIT | A->FIN_WAIT2
B:”好了,说完了,我也不说了。”B->LAST_ACK
A:”我知道了。”A->TIME_WAIT | B->CLOSED
A等待2MSL,保证B收到了消息,否则重说一次”我知道了”,A->CLOSED
API Gateway内部HttpClient调优:
<backends>
<hc>
<global>
<maxTotal>200</maxTotal>
<maxPerRoute>100</maxPerRoute>
<connectionTimeout>1000</connectionTimeout>
<connectTimeout>5000</connectTimeout>
<soTimeout>6000</soTimeout>
<keepAlive>3000</keepAlive>
</global>
<route host="127.0.0.1" port="8080">
<max>100</max>
</route>
</hc>
</backends>
1.Time_Wait问题修复route的KeepAlive实效问题,提前到1分钟结束TimeWait,高并发情况下,重用速度很快(下面不需要,连接池会自己做)
#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
net.ipv4.tcp_tw_reuse = 1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout
net.ipv4.tcp_keepalive_*
这几个参数。
net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle的开启都是为了回收处于TIME_WAIT状态的资源。
net.ipv4.tcp_fin_timeout这个时间可以减少在异常情况下服务器从FIN-WAIT-2转到TIME_WAIT的时间。
net.ipv4.tcp_keepalive_*一系列参数,是用来设置服务器检测连接存活的相关配置。
TIME_WAIT很多,可怕吗?
如果你通过 ss -tan state time-wait | wc -l 发现,系统中有很多TIME_WAIT,很多人都会紧张。多少算多呢?几百几千?如果是这个量级,其实真的没必要紧张。第一,这个量级,因为TIME_WAIT所占用的内存很少很少;因为记录和寻找可用的local port所消耗的CPU也基本可以忽略。
会占用内存吗?当然!任何你可以看到的数据,内核里都需要有相关的数据结构来保存这个数据啊。一条Socket处于TIME_WAIT状态,它也是一条“存在“的socket,内核里也需要有保持它的数据:
内核里有保存所有连接的一个hash table,这个hash table里面既包含TIME_WAIT状态的连接,也包含其他状态的连接。主要用于有新的数据到来的时候,从这个hash table里快速找到这条连接。不同的内核对这个hash table的大小设置不同,你可以通过dmesg命令去找到你的内核设置的大小:
还有一个hash table用来保存所有的bound ports,主要用于可以快速的找到一个可用的端口或者随机端口:
由于内核需要保存这些数据,必然,会占用一定的内存。
会消耗CPU吗?当然!每次找到一个随机端口,还是需要遍历一遍bound ports的吧,这必然需要一些CPU时间。
TIME_WAIT很多,既占内存又消耗CPU,这也是为什么很多人,看到TIME_WAIT很多,就蠢蠢欲动的想去干掉他们。其实,如果你再进一步去研究,1万条TIME_WAIT的连接,也就多消耗1M左右的内存,对现代的很多服务器,已经不算什么了。至于CPU,能减少它当然更好,但是不至于因为1万多个hash item就担忧。
2.清空失效连接和定制长连接策略
http://blog.csdn.net/u014704496/article/details/40863045
http://blog.csdn.net/wanghao109/article/details/53121436
IdleConnectionMonitorThread类负责监控httpclient中的连接,进行清理操作。
// 启动定时清楚失效连接进程
IdleConnectionMonitorThread thread = new IdleConnectionMonitorThread(cm);
thread.start();
// 这个线程负责使用连接管理器清空失效连接和过长连接
public static class IdleConnectionMonitorThread extends Thread {
private final HttpClientConnectionManager connMgr;
private volatile boolean shutdown;
public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
super();
this.connMgr = connMgr;
}
@Override
public void run() {
try {
while (!shutdown) {
synchronized (this) {
wait(1000);
System.out.println("清空失效连接...");
// Close expired connections
connMgr.closeExpiredConnections();
// Optionally, close connections
// that have been idle longer than 30 sec
// 关闭空闲超过30秒的连接
connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
}
}
} catch (InterruptedException ex) {
// terminate
}
}
public void shutdown() {
shutdown = true;
synchronized (this) {
// 让run方法不再wait
notifyAll();
}
}
}
/**
*
* http长连接策略:
* 可以根据须要定制所须要的长连接策略,可根据服务器指定的超时时间,也可根据主机名自己指定超时时间;
*/
private static void test11(){
//参考第一章的DefaultConnectionKeepAliveStrategy类
ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
public long getKeepAliveDuration(HttpResponse response,HttpContext context) {
// 遍历response的header
HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
HeaderElement he = it.nextElement();
String param = he.getName();
String value = he.getValue();
if (value != null && param.equalsIgnoreCase("timeout")) {//如果头部包含timeout信息,则使用
try {
//超时时间设置为服务器指定的值
return Long.parseLong(value) * 1000;
} catch (NumberFormatException ignore) {
}
}
}
//获取主机
HttpHost target = (HttpHost) context.getAttribute(HttpClientContext.HTTP_TARGET_HOST);
if ("webservice.webxml.com.cn".equalsIgnoreCase(target.getHostName())) {
// 如果访问webservice.webxml.com.cn主机则设置长连接时间为5秒
return 5* 1000;
} else {
// 其他为30秒
return 30 * 1000;
}
}
};
CloseableHttpClient client = HttpClients.custom().setKeepAliveStrategy(myStrategy).build();
3.Close_Wait,代码不严谨未主动关闭流,导致一直在Clost_Wait
finally {
if(response != null) {
EntityUtils.consume(response.getEntity()); //会自动释放连接
}
//如下方法也是可以的,但是存在一些风险;不要用
//InputStream is = response.getEntity().getContent();
//is.close();
}
http://blog.csdn.net/kobejayandy/article/details/17655739
http://blog.oldboyedu.com/tcp-wait/
https://www.8090st.com/server-time_wait-close_wait.html