网络协议层TCP/UDP处于运输层
UDP
UDP报文UDP报文比较简单:主要包含有源端口号和目的端口号。从而也看出UDP简单粗暴,只要有了目的端口号,就可以随意发送。
UDP有以下几个特点:
- 沟通简单,协议简单,无需关注太多细节,默认网络是相同的并且可达的;
- 信任对方,无需建立连接,可以随意的往任意端口传输数据,也可以监听该端口,无论谁都可以往该端口传输数据
- 不会对网络环境做出改变,无论网络环境多恶劣,你叫我发我就发。
UDP与TCP的区别:
TCP是面向连接的,即在相互通信之前,会进行3次握手,建立连接。所谓的建立连接,是为了在客户端和服务端维护连接,而建立一定的数据结构来维护双方交互的状态,用这样的数据结构来保证所谓的面向连接的特性。
UDP是无连接的。
- TCP提供可靠交付,IP是不提供可靠交付的,通过TCP连接传输的数据,有序、无差错、不丢失。UDP则继承了IP的特性,不保证交付,不保证丢失,不保证按序到达。
- TCP是面向字节流的,发送的时候没头没尾,流可以分成一个个IP包进行发送,接收端可以按照TCP中的序号对数据进行排序,然后累计,一次性向上交付。UDP继承了IP的特性,基于数据包的,一个一个IP包的发送,接收端接收到一个向上交付一个。
- TCP是有拥塞控制的,如果意识到网络环境不好了,会主动降低发送速率。UDP则不关心网络环境,只要上层叫我发数据,我就发。
- TCP是一个有状态服务,发送端清除的记录着那些包发送了,哪些包没法送,哪些包确认了,应该从哪开始发等信息。UDP则是无状态的,只要我发出去了,我就不管了。
TCP
网络天然是恶劣的,丢包、乱序、重传、拥塞是常有的是,TCP的提出就是要解决这些问题的,用来解决在不可靠的信道上面进行可靠的传输。
如何确定一个TCP连接:五元组,(协议、本机ip、本机端口、远程主机ip、远程主机端口)
TCP报文首先,源端口号和目的端口号是必须的,这2个参数和源IP、目标IP的组合将决定该数据要提交给哪个应用。
序号(32位):序号的存在是用来解决乱序的问题,有了序号,即使数据包到达接收端是乱序的,接收端依然可以重新组成有序报文。
确认序号(32位):确认序号的存在是用来解决重传问题,接收方在接收到报文后如果不通知发送方我已经接收到该报文,发送方怎么知道接收方收到没,自己需不需要重传。
状态位:引起连接状态的变更。SYN,同步序号标志位;ACK,确认序号标志位;RST,重置连接标志位;FIN,释放连接标志位。
窗口:窗口的存在是用来做流量控制,发送方通过该值通知对方自己所能处理的窗口大小,从而控制对方发送数据的速率。
校验和:奇偶校验,用来检验TCP报文段在传输过程中是否出错。
TCP3次握手的原因
- 3次握手可以看做是双方的单向通讯,A向B发起连接请求,并受到B的应答请求,对于A来说,它的消息是有去有回的,因此认为连接建立了;B在发送应答之后,也在等A的应答之应答,只有收到该应答之应答,对于B来说,消息才是有去有回的,才能认为连接是建立的;
- 避免网络上无效的连接请求,如果A的连接请求重发了多次,A和B再重发的请求下面完成了一次连接,此时之前的请求报文又到了B,B无法知道这个是不是之前已经发起的连接请求,会再发送应答给A,此时由于A压根没有发起请求,忽略该应答。由于B很久没有收到A的应答之应答报文,于是关闭该连接,该连接不成立。
- 确认双发的数据包序号,双发每次在新的连接的序号都不是从1开始的,而是随着时间的变化而变化的,这样的做法是防止与旧连接的数据产生冲突。这个序号的起始序号是随着时间变化的,可以看成一个 32 位的计数器,每4ms加一,如果计算一下,如果到重复,需要4个多小时,旧连接的数据早就由于TTL到达而被丢弃了,因此不会产生序号冲突的问题。
TCP4次挥手的原因
FIN报文只能关闭一方的连接,而另一方连接是否关闭需要由对方决定,而每一次FIN报文都需要一个ACK报文确定,因此总共需要4次交互。
TCP状态流转图在这个图中,加黑加粗的部分,是上面说到的主要流程,其中阿拉伯数字的序号,是连接过程中的顺序,而大写中文数字的序号,是连接断开过程中的顺序。加粗的实线是客户端 A 的状态变迁,加粗的虚线是服务端 B 的状态变迁。
上面可以看到A在FIN-WAIT-2
收到B的FIN报文会进入TIME-WAIT
状态,这个状态会持续一段时间,通常设置为2MSL,MSL表示Maximum Segment Lifetime
,即报文最大生存时间,也就是数据包在网络上存在的最长时间,超过这个时间的报文会被网络丢弃。实际应用中常用的是30秒,1分钟和2分钟等。需要等候一段时间的原因有2个:
- 保证TCP的连接能够正常的关闭:如果A再发送ACK之后直接进入
CLOSED
状态,如果B再等待一段时间后没有收到该ACK报文,就会重新发送FIN报文,此时由于A已经进入CLOSED
状态,就会返回RST
报文,那么对于这个TCP连接来说是异常关闭的; - 保证这次发送的所有重复报文在网络中消失:如果A直接进入
CLOSED
状态,那么此时B在重新发起一个连接(同样的端口,不过一般这种可能性较低)那么就会生成一个新的连接;此时网络上可能存在滞后的上一个连接的数据包,这些数据包在新连接建立之后才到达A,就会造成数据的混淆。
至于为什么要等2MSL,是因为A发送ACK报文最多需要MSL到达B,B如果没有收到ACK报文,会重新发送FIN报文,又最多需要MSL到达A,那么一来一回就需要2MSL的时间。
题外话:socket的SO_REUSEADDR
选项可以让处于TIME-WAIT状态的端口可以直接被使用。
TCP报文的累计确认和超时重传
累计确认
TCP是保证可靠交付的,每发出的报文都要求对方发送ACK确认报文,但如果对没收到一个包都发送一次应答,那么效率比较低。因此TCP使用了累计重传方式来提高效率,对于接收到的数据包,不是一个一个去应答,而是会应答一个ID,表示这个ID之前的包都收到了。
为了记录发送和接收的数据包,发送端和接收端会维护一个缓存。如下图:
对于发送端,会维护一个滑动窗口,滑动窗口之间的数据可以无须等待前面数据的确认即可发送。每当收到新的确认包,滑动可以往前移动。滑动只有不动和往前移动两种状态。
TCP发送端缓存LastByteAcked:表示之前的数据包已经发送并得到了确认;
LastByteSent:表示之前的数据包已经发送;
MaxRcvBuffer:表示接受可以缓存的数据包;
LastByteRead:表示之前的数据包已经被应用读取;
超时重传
RTT:报文往返时间,指一个报文发出的时间,以及接收到相应的ACK确认包之间所跨越的时间,TCP通常用来计算超时重传时间。
RTO(Restransimission Time-Out):超时重传时间,发送方在规定的RTO时间内没有收到对方的确认报文,就要重传已发送的报文段。
平滑RTT:SRTT = SRTT + α (RTT - SRTT)
加权移动平均:DevRTT = (1-β) DevRTT + β |RTT-SRTT|
RTO = μ * SRTT + ν * DevRTT
在Liunx下,α = 0.125, β = 0.25, μ = 1, ν = 4
TCP的流量控制
对于数据包的确认报文中,带有接收方能够处理的窗口大小,接收方可以通过该字段控制发送方的滑动窗口大小,甚至可以将滑动窗口的大小控制为0。当滑动窗口为0时,发送方会定时发送窗口探测报文,看看是否有机会可以调整滑动窗口的大小。
TCP的拥塞控制
拥塞控制主要来避免两种现象,包丢失和超时重传。一旦出现了这些现象就说明,发送速度太快了,要慢一点。
拥塞控制也是通过窗口的大小来控制的,前面的滑动窗口rwnd是怕把接收端的缓存填满,拥塞控制cwnd的窗口是怕把网络填满。因此,最终的wnd的大小是这么决定的:wnd = min(cwnd, rwnd)
。
cwnd窗口大小控制如下:
慢启动
TCP开始传输数据时,cwnd设置为一个报文段,一次只能发送一个,即cwnd=1;当收到这个报文段的ACK时,cwnd+1变为2,一次能够发送两个;当收到这两个报文段的ACK,每个确认ACK会导致cwnd+1,两个报文段会使cwnd+2变为4,一次能够发送四个;当收到这个四个报文段的ACK,cwnd+4变为8,以此类推,可以看出,这是一个指数增长的过程。这就是慢启动算法。
拥塞避免
当然,这个不能一直无限的按指数增长下去,会有个慢启动门限(ssthresh,通常为65535个字节),当超过这个门限值,每收到一个报文段的ACK,cwnd就增加1/cwnd,比如一次可以发送8个字节,当收到这个8个字节的ACK时,cwnd=cwnd+1/88=9,于是一次可以发送9个报文段,可以看出,当超过ssthresh之后,变成了线性增加*,这样就可以避免增长过快导致网络拥塞。
在增长到一定阶段时,只要网络出现拥塞(根据触发了RTO重传),此时将ssthresh
设置为cwnd/2
,将cwnd设置为1,重新开始慢启动算法。
快重传和快恢复
上面的拥塞控制只要遇到RTO重传就回到慢启动算法,未免太过激进。并且RTO的时间也比较长。TCP中有一个快重传的方法,当接收端收到一个中间序号报文(即前面有部分序号报文没有收到),就立马发送发送冗余的ACK报文,那么接收端在接收到连续的3个ACK报文之后,就可以立即重传丢失的报文,无需等待RTO到期。这就是快重传算法。
快恢复是在快重传的基础之上的,在发送端收到3个连续的ACK报文时,认为网络环境并没有那么糟糕(毕竟还是有数据包到达对面的),没必要采取过激的算法(直接重新开始慢启动),而是将ssthresh设置为原来的一半(cwnd/2),cwnd设置为ssthresh+3,然后开始拥塞避免算法。
TCP拥塞控制有以下问题:
- 丢包并不代表着通道满了,也可能是管子本来就漏水。例如公网上带宽不满也会丢包,这个时候就认为拥塞了,退缩了,其实是不对的。
- TCP 的拥塞控制要等到将中间设备都填充满了,才发生丢包,从而降低速度,这时候已经晚了。其实 TCP 只要填满管道就可以了,不应该接着填,直到连缓存也填满。
进阶:见Google提出的BBR算法。
DNS
Domain Name System,域名系统,用来将域名转化为IP地址,域名系统一定是高可用、高并发、分布式的。
根域名服务器:返回顶级域名服务器的IP地址;
顶级域名服务器:返回权威域名服务器的IP地址;
权威域名服务器:返回相应的主机IP。
DNS的解析流程(访问www.163.com为例):
- 电脑客户端会优先访问本地
/etc/hosts
文件,如果存在配置的IP地址,则直接返回;没有则在本地DNS缓存查看是否存在IP,如果能够找到,则直接返回;否则电脑客户端会发出一个DNS请求,首先发给本地DNS服务器,本地DNS服务器是由网络服务商提供; - 本地DNS服务器收到来自客户端的请求,本地DNS首先在本地缓存表中查看是否存在对应的IP地址,如果有,则返回IP地址;如果没有,本地DNS服务器会去它的根域名服务器,“能够告诉我www.163.com的地址吗”,根域名服务器是最高层次,全球共有13套,它不直接用于域名解析,但是可以告诉本地DNS服务器去哪查;
- 根域名服务器收到来自本地DNS服务器的请求,发现后缀是
.com
,指明这个域名是由.com
顶级域名服务器管理,于是告诉本地DNS服务器去顶级服务器查; - 顶级域名服务器:顶级就是
.com
、.org
这些一级域名,它负责管理二级域名,比如163.com
,所以它可以告诉本地DNS去哪里查询; - 顶级域名服务器收到来自本地DNS的请求,告诉本地DNS负责管理
www.163.com
的权威DNS服务器的地址; - 本地DNS服务器转而向权威DNS服务器询问IP;
- 权威DNS服务器将对应的IP地址告诉本地DNS服务器;
- 本地DNS服务器再将IP返回客户端,客户端与目标建立连接。
HTTPDNS
传统DNS域名解析存在的问题:
- 域名缓存问题:缓存会存在过期问题,再者,上一次请求获得的IP地址在该次的情况下请求不一定是最优的;
- 域名转发问题:DNS解析的请求返回的IP地址可能是跨运营商,这样会导致每次访问机器慢;
- 出口NAT问题:出口NAT会将原来的IP地址替换掉,那么权威域名服务器无法通过源端IP地址提供最优的解析结果;
- 域名更新问题:如果IP对应的IP更新了的话,在全网生效需要一定的时间,那么此时部分用户可能会访问错误的IP地址;
- 解析延迟问题:DNS的查询需要经过多次的DNS递归查询才能获得最终的解析结果,这回带来一定的延时。
为了解决上述问题,HTTPDNS应运而生。HTTPNDS不走传统的DNS解析,而是自己搭建基于HTTP协议的DNS服务器集群,分布在多个地点和多个运营商。当客户端需要DNS解析的时候,直接通过HTTP协议进行请求这个服务器集群,得到就近的地址。
工作模式:当手机应用要访问一个地址的时候,首先看是否有本地的缓存,如果有就直接返回。这个缓存和本地 DNS 的缓存不一样的是,这个是手机应用自己做的,而非整个运营商统一做的。如何更新、何时更新,手机应用的客户端可以和服务器协调来做这件事情。
如果本地没有,就需要请求 HTTPDNS 的服务器,在本地 HTTPDNS 服务器的 IP 列表中,选择一个发出 HTTP 的请求,会返回一个要访问的网站的 IP 列表。
手机客户端自然知道手机在哪个运营商、哪个地址。由于是直接的 HTTP 通信,HTTPDNS 服务器能够准确知道这些信息,因而可以做精准的全局负载均衡。
请求示例:
curl http://106.2.xxx.xxx/d?dn=c.m.163.com
{"dns":[{"host":"c.m.163.com","ips":["223.252.199.12"],"ttl":300,"http2":0}],"client":{"ip":"106.2.81.50","line":269692944}}
网友评论