四层协议
服务器收到数据包后,层层拆包(很多工作是内核完成的),直到传输层,获取当前的端口号,将数据包发往监听当前端口号的应用程序(可能是tomcat(8080)也可能是浏览器(80))
只要是在网络上跑的包,都是完整的。可以有下层没上层,绝对不可能有上层没下层。所以对于TCP协议来说,三次握手/重试 只要想发出包,就要经过IP层和MAC层一个在网络传递的包如下所示
IP地址
ip地址类比大学宿舍 mac地址类比身份证id
ip分为公有ip 和 私有ip 一般公司都会有一个统一的公有ip 和 许多私有ip
同一个局域网内的设备ip地址不能相同,不然ARP根本找不到目标机器
CIDR
无类型域间选路(CIDR)
网络号 + 主机号 10.100.122.2/24 前24位是网络号 后8位是主机号
广播地址 10.100.122.255 发送到这个地址所有的10.100.122.x都能收到
子网掩码 255.255.255.0
子网掩码和IP地址执行AND 就可以得到网络号
DHCP
自动分配ip-DHCP协议(池化思想,用后即还)
- 新机器加入网络,以0.0.0.0为ip地址(具有本机的mac地址),向255.255.255.255发送广播包
- DHCP Server会收到广播包,并且将分配给新人的ip一并返回给新人(此时目标ip还是255.255.255.255)
- 再次发送广播包,ip和第一次发的一样,内容是接收哪一个DHCP Server分配的ip
- DHCP Server再次发送和2步骤类似的包,表示ACK
二层设备
二层设备(mac):解决多路访问的堵车问题 局域网LAN
- 集线器hub(广播模式),通过mac地址解决『广播』问题,此时仍然广播但是由各个主机判断是否接收当前消息;通过信道划分解决消息混乱无序问题
- 交换机:相比hub,它知道每个口对应的mac地址,它会有记忆功能(凡是它转发过的都会牢记),一段时间后,就能知道整个网络的结构了,此时可以精准转发不再需要广播(注意交换机最开始还是广播的,它是逐渐变得聪明的)
- VLAN 虚拟局域网 在二层头部新增一个TAG 里面有VLAN ID
- 支持VLAN的交换机可以连接多个不同LAN的机器,VLAN交换机间通过Trunk口连接
- 交换机环路问题,两个交换机左侧是相同的lan 右侧也是相同的lan
- STP:解决环路问题,将有环路的图变成无环路的树
- ARP:已知IP地址求MAC地址 基本靠吼
三层设备
三层设备:ip 路由器
- ICMP:ping指令基于该协议,ICMP报文封装在IP包中
- 最重要的两个参数,类型(主动请求是8 主动请求的应答是0 还有很多差错报文类型表示不同的问题)和序号
- 一个电脑有多个网卡就可以充当路由器,一个网卡连接外网网口,一个网卡连到交换机
- 网关地址一定和源ip地址在同一个网段
- 网关往往是一个路由器,是一个三层转发设备(三层设备:拆下mac头和ip头接下来进行转发)
- 路由器是一台设备,它有五个网口或者网卡,相当于有五只手,分别连着五个局域网。每只手的 IP 地址都和局域网的IP地址相同的网段,每只手都是它握住的那个局域网的网关。任何一个想发往其他局域网的包,都会到达其中一只手,被拿进来,拿下 MAC 头和 IP 头,看看,根据自己的路由算法,选择另一只手,加上 IP 头和 MAC 头,然后扔出去。(可以在我们家用的路由器外边再抽象出一个路由器)
- 静态路由:配置一条条规则(寻找下一跳),想玩三国杀要从2号口出去
- 转发网关:只改变MAC地址,不改变IP地址 (前提是在不同局域网IP地址不冲突)
- NAT网关:改变IP地址,可能我在中国的地址和美国的mike地址完全一样,这个时候就要把我的地址映射为一个在国际上肯定不会冲突的地址(比如加上国家的名字);当前局域网的ip和国际ip需要有个映射关系并且记录在当前网关内,在国际上(不同局域网间)目标ip地址不会改变了 这种场景很常用,家用网段基本都一样 我想和邻居沟通的话 肯定要做一层映射改变当前的家用ip
- 动态路由:网络->图 寻找最短路径
- OSPF:基于链路状态的路由协议/寻找最短路径(适用于内网,随便怎么走不需要别人允许)
- BGP:外网路由协议(除了路径长短还要考虑当地政策问题)
TCP
传输层:TCP/UDP
- IP头部会标识当前是tcp还是udp
- TCP面向连接(在交互前tcp会先建立连接/双方建立一定的数据结构来维护交互状态)、面向字节流(流:没头没尾 而udp继承了ip特性 基于数据包 一个一个地发 一个一个地收)、具有拥塞控制
- UDP(可以说是ip的亲儿子)源端口号+目的端口号+udp长度+udp校验和+数据
- TCP:源端口号+目的端口号+序号(解决乱序问题)+确认序号(解决丢包问题)+窗口大小(流量控制)+状态位(SYN ACK RST FIN)
- TCP三次握手:保证基本的消息有来有回所以最少三次
握手与挥手
在内核中,会为每个Socket维护两个队列,分别是已经建立连接的队列(established)以及还未完全建立连接点队列(syn_rcvd)
注意上图中的几种状态
重点是TIME-WAIT状态(计时等待)
- 主动发起关闭侧在close之前最后的一个状态,持续时间2MSL
- 如果最后一个ACK发送失败,右侧会重新发送FIN,左侧再次发送ACK
- 另一个作用是保证,右侧发到左侧的包都在网络中彻底过期,不然可能该端口新的应用会收到混乱的消息(化身)
【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?
答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,”你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
Client发送FIN表示,我没话想跟你说了,但是Server会先回一个我知道了,但是可能Server还有话对Client说,所以大多数时候FIN和ACK不会同时发送到Client端。
【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
答:
1)2MSL可以保证从我这里发送的所有包都不会有『有效』应答了,那么我可以放心的closed了
2)注意client是在收到FIN后才变为time-wait状态的,假设网络状况极差ACK到达服务端已经过了MSL,这时候服务端要再次发送FIN而这个FIN的最大值也是MSL,所以这里取2MSL
【问题3】 为什么seq要和时间戳挂钩?不能每次都从1递增?
答:假如A发送了1,2,3到B,然后A下线了,不久又上线了如果再次从1发送可能会收到无效的B发来的应答
状态机
缓存区
发送端
- 发送了并且已经确认的包
- 发送了并且尚未确认的包
- 未发送并且等待发送的包
- 未发送并且暂时不发送的包
这里面2+3要等于接收端滑动窗口(Advertised window)的大小
接收端
- 接收并确认过(还未被应用层读取到)
- 未接收马上能接收的(能承受的最大工作量,也就是Advertised window)
- 第二部分收到的包可能不是有序的,会出现空挡,只有和第一部分连续的,可以马上回复,中间空着的部分需要等待,哪怕后面的已经来了
- 未接收且没法接收的
容错机制
超时重试
对每个已经发送但是没收到ACK的包都有一个定时器,如果时间到了会重新尝试,这个时间一般稍大于往返时间RTT
发送端策略:自适应重传算法
:RTT通过采样计算加权平均,这个值随着网络状况不断变化超时间隔加倍
:每当遇到一次超时重传,都会将下次超时时间设置为先前的两倍
接收端策略:快重传
:接收方收到一个序号大于期望序号的报文时候,会发送三个冗余的ACK,发送方收到后,会在定时器到期之前,重传丢失的报文段(一般和拥塞控制的快恢复一起使用)SACK
:每次ACK的时候将接收方缓存的报文情况一并发送到发送方,这样发送方就知道哪个包丢失了
拥塞控制
拥塞窗口cwnd,防止把网络塞满 目的是为了避免包丢失和超时重传
- 慢开始(指数启动):涨到一定的值就不能再这么快地涨了(ssthresh)
- 拥塞避免(线性启动):涨到一定的值就不能再涨了(代表当前出现了堵塞),接着把ssthresh设置为当前cwnd的一半,并且把cwnd置为1
- 急刹车:无论慢开始阶段还是拥塞避免阶段,一旦出现网络堵塞(ACK超时),就要把ssthresh设置为当前cwnd的一半,并且把cwnd置为1
- 算法假设丢包都是因为过程设备缓存满了,如果真的满了那么其实已经为时过晚;而如果是普通的丢包就使用默认的快速下降策略未免有些不妥,为此引入了BBR算法
快恢复:上面那种cwnd直接减半并置为1的做法过于激进,相当于开车开到了200码突然停下来,于是就有了快恢复。快恢复一般和快重传一起使用,当收到连续三个冗余ACK后(表示有丢包但网络情况不是特别差),此时执行快恢复算法,令ssthresh = cwnd/2,cwnd = ssthresh,接着执行拥塞避免(线性启动)
BBR拥塞算法:并非将整个网络都塞满(包括设备缓存区)or 丢包才执行拥塞算法,它力图寻找高带宽和低延迟的平衡点,它触发的点是管道填满,但是中间设备缓存不填满(缓存被填满,时延会增大)
1 设备缓存会导致延时?
假如经过设备的包都不需要进入缓存,那么得到的速度是最快的。进入缓存且等待,等待的时间就是额外的延时。BBR就是为了避免这些问题:充分利用带宽;降低buffer占用率。
2 降低发送packet的速度,为何反而提速了?
标准TCP拥塞算法是遇到丢包的数据时快速下降发送速度,因为算法假设丢包都是因为过程设备缓存满了。快速下降后重新慢启动,整个过程对于带宽来说是浪费的。通过packet速度-时间的图来看,从积分上看,BBR充分利用带宽时发送效率才是最高的。可以说BBR比标准TCP拥塞算法更正确地处理了数据丢包。对于网络上有一定丢包率的公网,BBR会更加智慧一点。回顾网络发展过程,带宽的是极大地改进的,而最小延迟会受限与介质传播速度,不会明显减少。BBR可以说是应运而生。
Socket编程
- 在内核中,会为每个Socket维护两个队列,分别是已经建立连接的队列(established)以及还未完全建立连接点队列(syn_rcvd)
- 接着服务端调用accept方法,会从内核中拿出一个处于established状态的socket,如果还没有的话会一直等待
- Socket在linux中以文件的形式存在,所以会对应内核中的一个文件描述符 Ps:每个进程都有一个名为task_struct的数据结构,里面指向一个文件描述符数组,列出这个进程打开的所有文件的文件描述符。所以各种io 会有文件描述符数量的限制
- select操作的底层:由于 Socket 是文件描述符,因而某个线程盯的所有的 Socket,都放在一个文件描述符集合 fd_set 中,这就是项目进度墙,然后调用 select 函数来监听文件描述符集合是否有变化。一旦有变化,就会依次查看每个文件描述符。那些发生变化的文件描述符在 fd_set 对应的位都设为 1,表示 Socket 可读或者可写,从而可以进行读写操作,然后再调用 select,接着盯着下一轮的变化。因为每次集合发生变动都要轮询整个集合。所以select操作同时管理的项目(Socket)数量受限。
- epoll操作的底层:通过注册callback函数的方式,如果某个文件描述符发生变化,就会主动通知,当前epoll要监听的Socket都放在红黑树中
HTTP
浏览器的80端口:http请求底层tcp协议的源端口,用于接收服务器返回的html Ps在浏览器里发送的请求 源端口号都是80
tomcat的8080端口:http请求底层tcp协议的目的端口,用于接收并处理客户端发送的http请求
HTTP 1.1
- 默认开启了keep-Alive 这样建立的tcp连接,可以在多次请求中复用
- 但是无法简洁地实现请求的并行
- 以纯文本的形式进行通信,每次都要完整的HTTP头
HTTP 2.0
- 对HTTP的头进行了一定压缩,将原来每次都要携带的大量 key value 在两端建立一个索引表,对相同的头只发送索引表中的索引
- HTTP 2.0 协议将一个 TCP 的连接中,切分成多个流,每个流都有自己的 ID 而且流可以是客户端发往服务端,也可以是服务端发往客户端
- 压缩、分帧、二进制编码、多路复用(多个请求stream共享一个tcp连接的方式,并且响应可以同时返回)、请求优先级(为共享同一个tcp连接的请求设置优先级)