Something about Networking
记录了一些很基础的但经常被问到的问题
1. 为什么多个tcp连接会比单个tcp连接快?(tmd面试傻了,居然没答出来这个)
一开始看见,这不是显而易见的吗???
后来发现,其实他想听到的答案是:
tcp的流量窗口rwnd(接收方),拥塞控制cwnd(发送方)
如下图(盗图): 绿色为 发送者发送,且接收者acked 黄色为 发送者发送,接收者未确认(in-flight) 蓝色为 可用但为发送
cwnd= width(in-flight)+width(not sent)
发送速率: rate = cwnd / RTT byte/sec 即发送速率在RTT(往返时延)一定的情况下,只受cwnd影响
慢启动,拥塞避免
注意: 建立连接后,client和server都有一个窗口,每次进行调整窗口都是调整自己的窗口控制发送速率(当然可以传送报文,让对方调整速率);
慢启动
(其实一点也不慢): tcp会进行直到丢包,每接到一个ack就会把窗口(cwnd)×2
,超过ssThreshold
就进行拥塞避免每一轮RTT
只增加 1/cwnd
(但是以上这个已经是废弃了!!! )
拥塞避免
: 到达sshThreshold
后会以线性速度上升,但是如果超时(出现拥塞),就将ssThreshold
设置为出现拥塞的窗口的一半,然后将窗口设为1,重新开始慢启动
快恢复
: 现在慢启动已经废弃了,在收到3个重复确认的ACK时就采用快恢复重传算法,ssThreshold
设置为出现拥塞的窗口的一半,将cwnd设为ssThreshold
的一半但不采用慢开始;
当出现丢包的,有以下两种状况:
- 接收者发送给发送者的ACK丢失,会导致 timeout
- 发送者发送给接收者的数据丢失,发送者会收到接受者的重复ACK,如果收到三个重复的ACK,可以确认为丢包
这里盗一幅图:
- 路由器(多个TCP有拥塞控制) 给出带宽为R,有K个连接经过 最后每个连接平均分的都会是 R/K
终上:
一个tcp连接很可能不能把当前路由的带宽都用完,而且连接也有重传的情况,所以多个tcp连接可以最大保证速率
但是这个问题在HTTP/2则不是一回事了,因为其靠帧来实现有序性,而且是连接复用(同域名下),所以多个连接反而会浪费资源
总结
回答问题,要从特么原理开始一步步推导来說,不能想当然
2. 各种握手挥手(我求求我自己把这些gdx记得滚瓜烂熟,每次都漏一点点)
TCP三次握手连接(three-way handshake)
直接上他🐎图:
- client发送server
SYN=1
(同步位,这种报文不能携带数据,但要消耗一个序号) 自己的序号 Seq=client_w ( isn ) 给 server (期间client从CLOSED到SYN-SENT状态,server从CLOSED到LISTEN状态)
ps: isn泛指一种计算序号的算法,有一种是每4μs+1(动态随机,增加安全性,为了避免被第三方猜测到,从而被第三方伪造的RST报文Reset),直到2^32归零,因为2MSL的限制,所以几乎不可能重复; 意义: 发送方的字节数据编号的原点,让对方生成一个合法的接收窗口;
-
server收到报文,把确认报文段中的SYN和ACK都设为1 SYN=1(同理要消耗一个序号) 和 ACK=1 自己的序号 Seq=server_w, ack= client_w + 1 到client (期间client仍然是SYN-SENT状态,server从LISTEN状态到SYN-RCVD状态)
-
client收到确认报文后,还要给server发送确认收到。 把确认ACK=1,ack=server_w+1(之前SYN消耗了序号) 自己的序号Seq=client_w+1 (因为没有SYN,所以可以携带数据,但如果不携带数据则不消耗序号,这里
(可以)
携带了数据,所以Seq是上一次的序号+1);(client端进入established状态) -
server收到确认报文后,进入
established
状态,全部连接完成
半连接状态(即服务器SYN-RECV状态)
服务器维护一个半连接队列(BackLogs
表示半连接队列的最大容纳数目),存放半连接。该队列为每个客户端的SYN包(syn=j)开设一个条目,该条目表明服务器已收到SYN包,并向客户发出确认,正在等待客户的ACK确认包。这些条目所标识的连接在服务器处于SYN_RECV
状态,当服务器收到客户的确认包时,删除该条目,服务器进入ESTABLISHED
状态。
SYN-ACK重传次数
服务器发送完SYN-ACK
包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传,如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。注意,每次重传等待的时间不一定相同;
三次握手能避免啥🐔儿东西呢(为啥两次不行?)
我们假设client给server发了个连接请求,但是请求丢失,然后client再发一个,server收到这个然后建立连接,这里面client发送了两个请求 会有以下情况出现:
client发出的第一个请求没有丢,只是网络阻塞; 但接下来,server收到后以为是新的一个连接,server返回一个确认报文,但client收到server确认报文会发现自己并没有建立连接请求,所以会忽视server的确认报文,也不会向server发送数据 但server会一直开着连接等client的数据,白白浪费资源! (但采用三次握手的话就没得事,server端没有接到client的第三次确认,就知道client没有要建立连接)
TCP四次挥手
-
client 发送
FIN=1
(终止位,跟同步位一样都在header里面,自己特喵去看下面给你画一个算了,但这里注意,无论带不带数据,这厮都要消耗一个序号!)seq=client_u
(这里序号是前面一个传送过的数据最后一个字节的序号+1);(client进入FIN-WAIT-1
状态) -
server收到释放报文返回
ACK=1
Seq=server_u
(同理,也是前面一个传送过的数据的最后一个字节序号+1);(server进入CLOSE-WAIT状态,但实际上是个半关闭状态,server->client方向的连接保持,但client->server已经没有数据要传输了) -
client收到server的确认后,进入
FIN-WAIT2
状态,等待server的连接释放报文 -
如果server没有数据要发给client了(server持续发送数据到client,也可以不发), 携带报文
FIN=1
,ACK=1
,seq=server_u2
(新,可能发送了一些数据)ack=client_u+1
(重复上次已发送的确认号) (server进入LAST-ACK状态) -
(紧接3)client收到释放报文后,返回确认报文: ACK=1 确认号ack=server_u2+1 序号seq=client_u+1 (前面1的FIN报文消耗了一个序号client_u) 然后client进入TIME_WAIT状态,然后经过2MSL(RFC 793设为2mins,但其实应该要用更小的数值: 60s 在linux内
/proc/sys/net/ipv4/tcp_fin_timeout
)再进入CLOSED状态 client撤销相应的TCB(传输控制块)后,结束连接
为啥要等2MSL
-
保证client发送的最后一个ACK报文能够到达server; 因为这个报文可能会丢失,然后处在LAST-ACK的server收不到已发送的FIN+ACK报文的确认。正常情况下,server会重传FIN+ACK报文,client接着重传最后一个ACK报文,重新计时; 但如果client不等待,发送完ACK报文直接释放连接进入CLOSED,client就没法收到server重传的FIN+ACK报文,也不会再发送一次确认报文(因为关闭了啊!),这样server重传的东西client就收不到,server也就不会正常进入CLOSED状态
-
防止出现 ‘已失效的连接请求报文段’(如同三次握手时间的client第一次发出的报文); client发送完最后一个ACK报文段后,经过2MSL,可以确定本连接持续时间内的所有报文段都从网络消失,这样就不会出现旧的连接报文段了
为啥连接用三次,断开要四次呢?
- 主要区别还是在连接时,server收到连接请求后可以直接返回SYN+ACK报文;
- 但是断开时,server收到了client的FIN报文后,可能还有数据没有传完,只能先发回一个ACK,等到最后数据都传完了才会发FIN,所以为了避免没传完数据就关闭了的情况,只能加多一次连接;如果是三次(最后一次server返回的ACK+FIN还有一堆数据合并为最后一次response),会造成长时间阻塞,导致客户端以为上一次的FIN没有到server,然后重传;
PS:server的cclosed状态要比client的要早一点 MSL>=TTL
连接当中如果用HTTPS,参考之前写的
keepalive timer
存在header里面的keep-alive是http协议的,可以维持长连接,HTTP/1.1开启;
2MSL是被client的一个TIME-WAIT timer设置的,实际上TCP还有另外一个keepalive timer,主要用来探测端到端的连接有没有失效 linux命令
sysctl -a | grep keepalive
默认7200s检测一次,一次最多重传9个包,每个包间隔75s
但是因为该设置不太合理,比如检测间隔太长等,很多应用没有开启
TCP fast open
顾名思义是一个减少连接时延的方法,实现在第二次握手就可以传输响应数据,主要就是使用SYN cookie实现 具体做法:
-
首轮的三次握手中,服务端接到SYN不会立即回复SYN+ACK,而是通过计算得到一个SYN Cookie,然后将这个Cookie放到TCP报文的fast open选项,然后返回,接下来就是正常的三次握手余下的流程;
-
接下来的握手中,客户端会将之前缓存的cookie,SYN和HTTP request 一起发给服务端,如果合法(不合法可能是过期的原因)直接返回SYN+ACK;
-
(重要!!!)接下来服务端就可以直接发送数据而不用等客户端的ACK了!!!
-
但是确保三次握手协议不变,client端最后还是会返回ACK
linux可以通过
cat /proc/sys/net/ipv4/tcp_fastopen
检查是否开启,1为client端,2为server,3为both
注意一下这个功能可能被一些防火墙隔离
TCP 粘包问题
这个其实没有特定的术语指明粘包这种东西,只是一种现象,主要是应用层对缓冲区中的数据解析出了问题(因为tcp是流式传输
,字节流可能没有传完,所以一般在接收端要重组分组),没有正确处理消息边界的原因(发送方可能用了nagle
或者接收方接收到没有立即处理);
处理方法:
- 发送方设置
TCP_NODELAY
; - 接收方在应用层,要和发送方商量,让
发送方带上消息边界(这时也要保证消息内容不含消息边界)
或者消息长度(http自带的header就可以content-length解析)
这里又有了,UDP会不会有这个问题呢?
- 不会;因为UDP是基于
数据报(datagram)
来发送的,不会有分组,在接收端是有一个skbuff
是一个链表来接收一个个数据报;
3. 有哪些字段,一些字段的意义
比较关键的字段
- 明确方向 源端口(2B),目的端口(2B)
- 明确报文本身信息 头部长度(4bit)
- 可靠性 ack(8B),seq号码(8B) , 窗口大小(16B),校验和(16B)
- 一些标志的bit ACK,SYN,FIN ,RST,PSH(不放入缓存直接被程序用),…???
- 额外的字段 kind(1B)+length(1B)+info(8B) 比如有timestamp字段: kind = 8 , length = 10, info由timestamp(4B)和timestamp echo(4B)组成
4. 一些拥塞控制优化吞吐量的算法
Nagle
有一种情景是client端不断发给server端很小的包,一次1B,这样发1KB就要1K次; Nagle就是解决这种问题,具体做法
- 第一次发1B,立即发送
- 后面的发送要满足
- 数据要达到MSS
- 之前所有包ACK都收到
延迟确认
指的是把一段时间内ACK合并然后延迟回复,tcp要求这个时延必须小于500ms,unix一般不超过200ms 但是有一些不能延迟确认
- 接收到一个大于一个帧的报文,且需要调整窗口大小
- TCP处于quick ack模式(tcp_in_quick_mode)
- 有乱序的包
Byte Queue Limits (BQL)
TCP Small Queues (TSQ)
Early Departure Time (EDT)
5. 其他
UDP有什么是TCP不可以代替的?
-
授时协议,tcp的重传会加大时间计算的误差
-
广播
如果要将UDP包装成TCP:
- 增加ACK/Seq机制
- 发送和接收缓冲区
- 超时重传机制
TCP Backlogs
针对SYN floods攻击,linux下使用了**两个队列(模型)**来缓解:
-
一个是有最小metadata的SYN Backlog(未完成的连接队列);
每个这样的syn分节对应其中一项:已由某个客户发出到达服务器,而服务器正在等待完成相应的TCP三鹿握手的过程,这些套接字处于SYN_RCVD状态。
-
一个是监听功能的Listen SYN Backlog(已完成的连接队列);
每个已经完成的三路握手的客户对应其中的一员,这些套接字处于ESTABLISHED状态。
如图
TCP在未完成队列接收SYN的request,当三次握手完成(established)后,就会将这个request移到已完成的连接队列的尾部;
除了两个队列模型外,TCP listen path也用无锁方式来改进???(似乎有点问题)4.7内核版本改进
TCP复用端口
端口复用最常用的用途应该是防止服务器重启时之前绑定的端口还未释放或者程序突然退出而系统没有释放端口。此时如果设定了端口复用,则新启动的服务器进程可以直接绑定端口;
一般来说,一个端口释放后会等待两分钟之后或三十秒才能再被使用,SO_REUSEADDR
是让端口释放后立即就可以被再次使用;
SO_REUSEADDR
套接字选项通知内核,如果TCP状态位于 TIME_WAIT
,可以重用端口。如果TCP状态位于其他状态,重用端口时依旧得到一个错误信息,指明"地址已经使用中"。
如果你的服务程序停止后想立即重启,而新套接字依旧使用同一端口,此时SO_REUSEADDR
选项非常有用。
端口复用允许在一个应用程序可以把 n 个套接字绑在一个端口上而不出错。同时,这 n 个套接字发送信息都正常,没有问题;
但是,这些套接字并不是所有都能读取信息,只有最后一个套接字会正常接收数据,这个特性都为后门程序所应用
DNS协议
首先这🐔是应用层协议!!!
功能主要就是把域名转换为IP地址
然后,主要是在UDP上面跑(为什么说主要,因为协议其实规定在超过512B时,应该用TCP
进行重试),端口53
长度最多是512Bytes,若过多要用 EDNS (最多支持4096B)
为什么选UDP/TCP?
-
UDP 协议
DNS 查询的数据包较小、机制简单;
UDP 协议的额外开销小、有着更好的性能表现;
-
TCP 协议
DNS 查询由于 DNSSEC 和 IPv6 的引入迅速膨胀,导致 DNS 响应经常超过 MTU 造成数据的分片和丢失,我们需要依靠更加可靠的 TCP 协议完成数据的传输;
随着 DNS 查询中包含的数据不断增加,TCP 协议头以及三次握手带来的额外开销比例逐渐降低,不再是占据总传输数据大小的主要部分;
URI 和 URL 和域名
URI(统一资源标识符) 包括 URL(统一资源定位符)和URN(统一资源名称)
URI通用模式:
scheme:[// [user:password @] host [:port]] [/] path [?查询] [#片段]
URL:主要用于连接网页或部分部件,借助访问的协议(http,ftp等)来检索定位资源位置 URL包含了
- 访问资源的协议
- 服务器位置
- 端口
- 资源在服务器上的位置
- 片段标识符(就是锚点#something)
域名(domain name)指的是任一主机或路由器连接在因特网上都有唯一的 层次结构的名字,只是一个逻辑概念
6. 浏览器输入url发生了什么 (个人感觉按照这个🐔来复习会比较好)
- 输入URL,浏览器会解析url,这里面是通过DNS域名解析,找到url对应的服务器地址,其中可能会从HOSTS文件找
- 找到主机地址,就会连接主机,这里面tcp三次握手,发送HTTP请求 然后封装发送的包,http包放在tcp包里,tcp包放在IP包里,层层往下,每一层会通过网管(gateway)
- 到了IP协议处会将其通过ARP解析出相应的在链路层的物理地址(通过路由器),在网络层和以上使用都是IP地址,以下都是硬件地址了,所以数据链路层看不见IP地址了
- 网络层通过物理地址找到路由器,把层层封装的包通过网桥(或桥接器等)等发送到链路层上,当前只能看见MAC帧,链路层负责开始传输数据了
- 物理层只是把传输数据 变成电信号真正地传到网线上
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!