像TCP协议一样。它之所以这么复杂,那是因为它秉承的是“性恶论”。它天然认为网络环境是恶劣的,丢包、乱序、重传,拥塞都是常有的事情,一言不合就可能送达不了,因而要从算法层面来保证可靠性。
拥塞控制和流量控制的区别,流量控制是针对端的,拥塞控制是针对网络的。
包的序号。为什么要给包编号呢?当然是为了解决乱序的问题。不编好号怎么确认哪个应该先来,哪个应该后到呢。编号是为了解决乱序问题。既然是社会老司机,做事当然要稳重,一件件来,面临再复杂的情况,也临危不乱。
还应该有的就是确认序号。发出去的包应该有确认,要不然我怎么知道对方有没有收到呢?如果没有收到就应该重新发送,直到送达。这个可以解决不丢包的问题。作为老司机,做事当然要靠谱,答应了就要做到,暂时做不到也要有个回复。
TCP是靠谱的协议,但是这不能说明它面临的网络环境好。从IP层面来讲,如果网络状况的确那么差,是没有任何可靠性保证的,而作为IP的上一层TCP也无能为力,唯一能做的就是更加努力,不断重传,通过各种算法保证。也就是说,对于TCP来讲,IP层你丢不丢包,我管不着,但是我在我的层面上,会努力保证可靠性。
接下来有一些状态位。例如SYN是发起一个连接,ACK是回复,RST是重新连接,FIN是结束连接等。TCP是面向连接的,因而双方要维护连接的状态,这些带状态位的包的发送,会引起双方的状态变更。
窗口大小。TCP要做流量控制,通信双方各声明一个窗口,标识自己当前能够的处理能力,别发送的太快,撑死我,也别发的太慢,饿死我。
除了做流量控制以外,TCP还会做拥塞控制,对于真正的通路堵车不堵车,它无能为力,唯一能做的就是控制自己,也即控制发送的速度。不能改变世界,就改变自己嘛。
所有的问题,首先都要先建立一个连接,所以我们先来看连接维护问题:
为什么序号要打乱????
为了维护这个连接,双方都要维护一个状态机,在连接建立的过程中,双方的状态变化时序图就像这样。
1.三次握手,第一次客户端发起主动连接的状态信息,seq,syn,syn=1代表发起连接
第二次,服务端发给应答,发送ack=来的seq+1,以及服务端的seq号
第三次,客户端给服务端发送ack=来的seq+1
只有第一次的话,不能建立连接,client不知道server是否能收到自己信息,并且client还不知道服务端的序列号。
只有两次的话,服务端不知道客户端是否收到了自己的seq号,所以也不敢冒然发信息,比如服务端确认了client端收到了自己的信息,才能建立连接。因为也有可能在二次握手之后,网络断了,所以服务端必须确认客户端知道了自己的序列号,才可以发送数据。“对于B来说,这个应答包也是一入网络深似海,不知道能不能到达A。这个时候B自然不能认为连接是建立好了,因为应答包仍然会丢,会绕弯路,或者A已经挂了都有可能。”“A建立连接的时候,请求包重复发了几次,有的请求包绕了一大圈又回来了,B会认为这也是一个正常的的请求的话,因此建立了连接”
为啥要有第三次握手,因为b不知道这个包是不是a的撩骚包,也许是a发送的多个撩骚包中的一个,这是个迟来的包,没必要接受的。
“三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误”
B发送的应答可能会发送多次,但是只要一次到达A,A就认为连接已经建立了,因为对于A来讲,他的消息有去有回。A会给B发送应答之应答,而B也在等这个消息,才能确认连接的建立,只有等到了这个消息,对于B来讲,才算它的消息有去有回。
好在大部分情况下,A和B建立了连接之后,A会马上发送数据的,一旦A发送数据,则很多问题都得到了解决。例如A发给B的应答丢了,当A后续发送的数据到达的时候,B可以认为这个连接已经建立,或者B压根就挂了,A发送的数据,会报错,说B不可达,A就知道B出事情了。
当然你可以说A比较坏,就是不发数据,建立连接后空着。我们在程序设计的时候,可以要求开启keepalive机制,即使没有真实的数据包,也有探活包。
三次握手的正常发包和应答,以及双端的状态扭转我们已经讲了,接下来就来看看在这三次握手的过程中,出现的异常情况。
1. 客户端第一个「SYN」包丢了。
如果客户端第一个「SYN」包丢了,也就是服务端根本就不知道客户端曾经发过包,那么处理流程主要在客户端。
而在 TCP 协议中,某端的一组「请求-应答」中,在一定时间范围内,只要没有收到应答的「ACK」包,无论是请求包对方没有收到,还是对方的应答包自己没有收到,均认为是丢包了,会触发超时重传机制。
所以此时会进入重传「SYN」包。根据《TCP/IP详解卷Ⅰ:协议》中的描述,此时会尝试三次,间隔时间分别是 5.8s、24s、48s,三次时间大约是 76s 左右,而大多数伯克利系统将建立一个新连接的最长时间,限制为 75s。
也就是说三次握手第一个「SYN」包丢了,会重传,总的尝试时间是 75s。
2. 服务端收到「SYN」并回复的「SYN,ACK」包丢了。
此时服务端已经收到了数据包并回复,如果这个回复的「SYN,ACK」包丢了,站在客户端的角度,会认为是最开始的那个「SYN」丢了,那么就继续重传,就是我们前面说的「错误 1 流程」。
而对服务端而言,如果发送的「SYN,ACK」包丢了,在超时时间内没有收到客户端发来的「ACK」包,也会触发重传,此时服务端处于 SYN_RCVD 状态,会依次等待 3s、6s、12s 后,重新发送「SYN,ACK」包。
而这个「SYN,ACK」包的重传次数,不同的操作系统下有不同的配置,例如在 Linux 下可以通过 tcp_synack_retries
进行配置,默认值为 5。如果这个重试次数内,仍未收到「ACK」应答包,那么服务端会自动关闭这个连接。
同时由于客户端在没有收到「SYN,ACK」时,也会进行重传,当客户端重传的「SYN」收到后,会立即重新发送「SYN,ACK」包。
3. 客户端最后一次回复「SYN,ACK」的「ACK」包丢了。
如果最后一个「ACK」包丢了,服务端因为收不到「ACK」会走重传机制,而客户端此时进入 ESTABLISHED 状态。
多数情况下,客户端进入 ESTABLISHED 状态后,则认为连接已建立,会立即发送数据。但是服务端因为没有收到最后一个「ACK」包,依然处于 SYN-RCVD 状态。
那么这里的关键,就在于服务端在处于 SYN-RCVD 状态下,收到客户端的数据包后如何处理?
这也是比较有争议的地方,有些资料里会写到当服务端处于 SYN-RCVD 状态下,收到客户端的数据包后,会直接回复 RTS 包响应,表示服务端错误,并进入 CLOSE 状态。
但是这样的设定有些过于严格,试想一下,服务端还在通过三次握手阶段确定对方是否真实存在,此时对方的数据已经发来了,那肯定是存在的。
所以当服务端处于 SYN-RCVD 状态下时,接收到客户端真实发送来的数据包时,会认为连接已建立,并进入 ESTABLISHED 状态。
前面一直在说正常的异常逻辑,双方都还算友善,按规矩做事,出现异常主要也是因为网络等客观问题,接下来说一个恶意的情况。
如果客户端是恶意的,在发送「SYN」包后,并收到「SYN,ACK」后就不回复了,那么服务端此时处于一种半连接的状态,虽然服务端会通过 tcp_synack_retries
配置重试的次数,不会无限等待下去,但是这也是有一个时间周期的。
如果短时间内存在大量的这种恶意连接,对服务端来说压力就会很大,这就是所谓的 SYN FLOOD 攻击。
2.四次挥手
建立一个连接需要三次握手,而终止一个连接要经过 4次握手。这由 TCP的半关闭(halfclose)造成的。既然一个TCP连接是全双工(即数据在两个方向上能同时传递),因此每个方向必须单独地进行关闭。这原则就是当一方完成它的数据发送任务后就能发送一个 FIN来终止这个方向连接。当一端收到一个 FIN,它必须通知应用层另一端几经终止了那个方向的数据传送。
收到一个FIN只意味着在这一方向上没有数据流动。一个TCP连接在收到一个FIN后仍能 发送数据。因此需要两个方向都收到FIN。
我认为是因为tcp是全双工的,两边都有读端写端,我认为正常管道的关闭原则是写端先关闭,哈哈~~~
简单说就是不写了,不读并且确认下,不写了并且让别人确认下。
半关闭状态是A确认了B也关闭了B的读端,伤心欲绝~~~此时的A确认B不再读他的数据了,所以才进入办关闭状态,因为和B还保持着半条连接,单工模式,B可以写给A呢。
A主动方:关闭写端,告诉B我不写了(我还能读)FIN=1
B被动方:关闭读端,向应用程序交付EOF,告诉A,既然你不写了,那我关闭读端了(我还能写)---ACK
B被动方也主动一次关闭写端:关闭写端,告诉A我不写了FIN=1
A被动方现在也被动关闭读端:彻底关闭,告诉B我已经把读写都切断了
为啥四次握手呢,2/3可以合在一起不,我觉得不能合在一起,是因为合在一起代表着B关闭读端,之后嘟嘟嘟,又关闭写端,万一A还有数据要读,就读不到了。给A留缓冲。好了,我没忽悠自己信
B回复确认之后,A还是可以读的,B也可以写,B写完了之后,告诉A,不写了,
https://zhuanlan.zhihu.com/p/140434455 有空看下常见面试tcp的
https://www.zhihu.com/topic/19614019/hot 非常好,有动画
我们知道,TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议,而且 TCP 是全双工模式。
对于初学者来说,定义太枯燥、无味,其实意思就是你和你女朋友聊天是面向连接的,只有连接起来才可以通信的。
可靠就是你发送的信息可以保证送达到对方,全双工意思就是你不仅可以给你女朋友发消息,而且她也可以给你发信息。
为什么非要进行 TCP 四次分手?我们接着上回说到,你现在和第二个女孩子恋爱了,突然有一天发现第一个女孩子是因为没有收到你的表白而错过了在一起的时机,那么你要和第二个女孩子分手。
那过程对应在 TCP 四次分手是怎么样子的?
为什要有 2MSL 等待延迟
对应这样一种情况,最后客户端发送的 ACK=1 给服务端的过程中丢失了,服务端没收到,服务端怎么认为的?我已经发送完数据了,怎么客户端没回应我?是不是中途丢失了?
然后服务端再次发起断开连接的请求,一个来回就是 2MSL,这里的两个来回由那一个来回组成的?
客户端给服务端发送的 ACK=1 丢失,服务端等待 1MSL 没收到,然后重新发送消息需要 1MSL。
如果再次接收到服务端的消息,则重启 2MSL 计时器,发送确认请求。客户端只需等待 2MSL,如果没有再次收到服务端的消息,就说明服务端已经接收到自己确认消息;此时双方都关闭的连接,TCP 四次分手完毕。
如果双方建立连接,一方出问题
如果双方建立连接,一方出问题怎么办?为了防止出现上述恋爱故事中千年等一回的情况,已经建立连接,但是服务端一直等待接收,发送端出现问题一直不能发送。
所以设计一个保活的计时器,如果一方出现问题,另一方过了这个计时器的时间,就发送试探报文,以后每隔 75 秒发送一次。若一连发送 10 个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
作者:Linux干货铺
链接:https://zhuanlan.zhihu.com/p/138676797
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
在建立Socket的时候,应该设置什么参数呢?Socket编程进行的是端到端的通信,往往意识不到中间经过多少局域网,多少路由器,因而能够设置的参数,也只能是端到端协议之上网络层和传输层的。
在网络层,Socket函数需要指定到底是IPv4还是IPv6,分别对应设置为AF_INET和AF_INET6。另外,还要指定到底是TCP还是UDP。还记得咱们前面讲过的,TCP协议是基于数据流的,所以设置为SOCK_STREAM,而UDP是基于数据报的,因而设置为SOCK_DGRAM。
TCP的服务端要先监听一个端口,一般是先调用bind函数,给这个Socket赋予一个IP地址和端口。为什么需要端口呢?要知道,你写的是一个应用程序,当一个网络包来的时候,内核要通过TCP头里面的这个端口,来找到你这个应用程序,把包给你。为什么要IP地址呢?有时候,一台机器会有多个网卡,也就会有多个IP地址,你可以选择监听所有的网卡,也可以选择监听一个网卡,这样,只有发给这个网卡的包,才会给你。
当服务端有了IP和端口号,就可以调用listen函数进行监听。在TCP的状态图里面,有一个listen状态,当调用这个函数之后,服务端就进入了这个状态,这个时候客户端就可以发起连接了。
在内核中,为每个Socket维护两个队列。一个是已经建立了连接的队列,这时候连接三次握手已经完毕,处于established状态;一个是还没有完全建立连接的队列,这个时候三次握手还没完成,处于syn_rcvd的状态。
接下来,服务端调用accept函数,拿出一个已经完成的连接进行处理。如果还没有完成,就要等着。
在服务端等待的时候,客户端可以通过connect函数发起连接。先在参数中指明要连接的IP地址和端口号,然后开始发起三次握手。内核会给客户端分配一个临时的端口。一旦握手成功,服务端的accept就会返回另一个Socket。
这是一个经常考的知识点,就是监听的Socket和真正用来传数据的Socket是两个,一个叫作监听Socket,一个叫作已连接Socket。
连接建立成功之后,双方开始通过read和write函数来读写数据,就像往一个文件流里面写东西一样。
在域名和IP的映射过程中,给了应用基于域名做负载均衡的机会,可以是简单的负载均衡,也可以根据地址和运营商做全局的负载均衡。
~~~CDN的诞生
DNS的两项功能,第一是根据名称查到具体的地址,另外一个是可以针对多个地址做负载均衡,而且可以在多个地址中选择一个距离你近的地方访问。
另外,有的运营商会把一些静态页面,缓存到本运营商的服务器内,这样用户请求的时候,就不用跨运营商进行访问,这样既加快了速度,也减少了运营商之间流量计算的成本。在域名解析的时候,不会将用户导向真正的网站,而是指向这个缓存的服务器。