记录计算机网络相关内容

🔗:cyc2018&huihut&CavsZhouyou

基本概念

网络

不同主机用网络连接,不同网络用internet连接,Internet是连接全球的internet

主机间通信方式

P2P:不区分服务的请求方和提供方,C/S:客户端为请求方,服务端为提供方

电路交换和分组交换

  • 电路交换:用户之间进行通信的整个过程中,始终占用一条专用物理链路,利用率低
  • 分组交换
    • 各个分组记录自身的首部和尾部,并携带源端和目的端等信息
    • 同一传输线路可传输多个分组,互不影响

时延

总时延 = 排队时延 + 处理时延 + 传输时延 + 传播时延

  • 排队:根据当前网络的通信量,分组可能在路由器的输入/输出队列中排队等待
  • 处理:主机/路由器收到分组后进行的一系列处理操作(分析首部、提取数据、差错检验、路由选择…)
  • 传输:主机/路由器传输数据帧的耗时=数据帧长度(bit)/传输速率(bit/s)
  • 传播:电磁波在信道中的传播时间=信道长度(m)/光速(m/s)

计算机网络体系结构

计算机网络体系结构

  • 应用层:以报文为单位在具体应用程序之间提供传输数据服务(HTTP、DNS)
  • 传输层:以报文段/用户数据报的形式在进程之间提供数据传输服务(TCP/UDP)
  • 网络层:将传输层数据以分组形式进行封装,通过路由器主机之间提供数据传输服务(IP/ARP)
  • 链路层:将网络层数据进一步封装成,通过网桥/交换机同一链路的主机之间提供数据传输服务
  • 物理层:屏蔽底层物理硬件设备差异,确保传输前后的比特流不变,对于链路层的比特流来说,底层硬件是透明的

⚠️TCP/IP不严格遵循 OSI 分层概念,应用层可能会直接使用 IP 层或者网络接口层

物理层

  • 数据传输单位:比特

  • 通信方式

    • 单工:单向传输(广播)
    • 半双工:双方交替传输
    • 全双工:双方同时传输
  • 带通调制:将离散的数字信号转换为连续的模拟信号

数据链路层

  • 封装成帧:首部+网络层数据报+尾部,构成一帧
  • 透明传输:IP数据报中内容和首部/尾部相似,则需通过转义字符对帧的首尾进行界定
  • 差错检测:通过循环冗余检验(CRC)来检查传输比特流中可能出现的差错

信道分类

  • 点对点信道:一对一通信,简单,使用PPP协议(用户主机和互联网服务提供商ISP的通信)
  • 广播信道:通过信道复用CSMA/CD协议,避免在同一广播信道发送数据时产生冲突
    • 单播:给局域网指定的MAC地址发送帧,一对一
    • 多播:给局域网多个站点发送帧,一对多
    • 广播:给局域网内所有站点发送帧,一对全体

信道复用

  • 频分:相同时间内,不同主机占用不同频率的带宽资源,信道利用率低
  • 时分:不同主机在不同的时间段内,占用相同的带宽频率,信道利用率低
  • 统计时分:对时分复用的改进,不固定不同主机(数据)在帧中的位置,构成一帧即可发送
  • 波分:光的频分,但光频率高,常用波长表示
  • 码分:不同主机使用不同的码,可以在同一时间使用相同频带通信,分配 m bit 的码片,则发送数据量为原先的m倍

CSMA/CD 协议

  • 基本原理
    • 多点接入:所有节点都共享网络传输信道(总线型网络)
    • 载波监听:节点在发送数据之前,首先检测信道是否空闲,如果信道空闲则发送,否则就等待
    • 碰撞检测:在发送出信息后,再对冲突进行检测,当发现冲突时,则取消发送
  • 争用期:端到端传播时延为t,一个来回最大2t,2t时间还未发生碰撞,则肯定这次发生不会冲突,2t为争用期
  • 重传等待时间:发生碰撞时需要等待一段时间后再发送,从集合 {0, 1, …, (2k-1)} 中随机取出一个数 r,取 r 倍的争用期作为重传等待时间

局域网

典型的广播信道,地理范围、站点数量有限,按照拓扑结构分:星型、环形、总线等

以太网

一种星型拓扑结构的局域网

帧格式

每个以太网帧都有最小的大小64bytes最大不能超过1518bytes

以太网帧格式

  • 地址;源地址和目的地址均为MAC地址
  • 类型:标记上层使用的协议(IP/ARP)
  • 数据:长度在 46-1500 之间,如果太小则需要填充;最大传输单元MTU,默认1500字节
  • FCS:帧检验序列,使用的是 CRC 检验方法;

连接方式

  • 集线器
    • 早期连接方式,作用于比特,物理层设备
    • 一个比特到达接口后被重新生成,能量强度放大,从而扩大网络传输距离,之后再发送至所有其他接口
    • 一个集线器同时收到两个不同接口的比特,则表明发送碰撞
  • 交换机
    • 集线器的替代,链路层设备
    • 根据MAC地址进行存储转发,不会发生碰撞
    • 自动将MAC地址及其端口映射更新到交换表

网络层

将传输层的报文段/用户数据报封装成分组/包进行转发,向上提供无连接、最大努力交付的服务

  • IP协议:规定了网络层的编址和转发方式
  • 路由选择协议:决定了数据报从源到目的地所流经的路径

IP数据报格式

注意首部固定20字节,除非含有选项字段

⚠️传输次序:0-7,8-15,大端序,又称为网络字节序;若主机为小端序,则需要转为大端序

IP数据报格式

  • 版本:4(IPV4)、6(IPV6)两个值
  • 首部长度:占4位,最大值15,值为1表示4字节(1个32位字),固定部分有20字节,因此最小值为5
  • 填充:通过填充字段,将可选长度修正为4的倍数
  • 总长度:首部+数据的长度
  • 生存时间TTL:以路由器跳数为单位,为0则丢弃数据报,防止在网络中一直无法交付而占用信道
  • 协议:明确需要将数据上交的的具体协议(TCP、UDP…)
  • 标识:数据报过长时需要进行分片,同一数据报的分片标识一致
  • 片偏移:8字节为单位,确定不同分片在数据报中的偏移值

IP分片和重组

🔗https://cloud.tencent.com/developer/article/1173790

⚠️不得以才做分片,一旦分就需要额外增加首部,接收还要重组,开销明显增大

分片原因:

  • 以太网帧格式(非巨帧),MTU限制IP数据报46~1500,超过MTU则分片,不一定按序到达,但IP首部标识可以重组

MTU进一步=IP首部+TCP首部+MSS

因此MSS理论最大=1500-20-20=1460字节,默认536(MTU标准576-20-20)

如果超过MSS则TCP分段,TCP按序号重组

🚨 发送端进行TCP分段后就一定不会在IP层进行分片,TCP层分段满足了MSS限制,也就满足了MTU的物理限制;UDP没有限制则超过MTU一定分片

  • 但TCP分段后仍然可能IP分片,TCP分段仅满足了通信两端的MTU要求,传输路径上如经过MTU值比该MTU值更小的链路,则再次分片

  • 两个通信主机直连,TCP连接协商得到的MTU值(两者网卡MTU较小值)就是路径MTU,发送端只要做了TCP分段,则在整个通信过程中一定不会发生IP分片

  • 另注意:分片后传输层的首部仅出现在第一个分片中,剩余分片仅包含IP首部

  • 一旦路径中分片丢失,TCP接收端收不到完整的报文,超时后发送端收到三次冗余ACK后整体重传

避免分片:

  • 路径MTU:是指一条因特网传输路径中,从源地址到目的地址所经过的“路径”上的所有IP跳的最大传输单元的最小值
  • 路径MTU发现:TraceRoute,确定两个IP主机之间路径最大传输单元的技术

编址方式

  • 分类:网络号+主机号,不同类别网络号长度固定A8、B16、C24
  • 子网划分:网络号+子网号+主机号,从主机号划分出子网号,结合子网掩码确定所属子网网段
  • CIDA:IP地址/网络前缀,前缀可变,有效减少路由表项数量(超网)

地址解析协议ARP

⚠️在OSI模型中ARP协议属于链路层;而在TCP/IP模型中,ARP协议属于网络层,另参考📖https://zhuanlan.zhihu.com/p/28771785

  • 实现IP地址到MAC地址的转换(RARP相反),从功能上讲是链路层的服务
  • 通信过程中,IP 数据报的源地址和目的地址始终不变,而 MAC 地址随着链路的改变而改变
  • 存在于以太网帧格式中的类型(IP/ARP)字段,帧位于链路层,因此从层次上讲可以看作网络层

工作原理:

  • 主机ARP高速缓存中没有目的IP到MAC的映射,则广播发送ARP请求,包含本机IP+本机MAC+目的IP
  • 目的IP收到ARP请求,返回ARP响应,包含目的IP的MAC地址

网际控制报文协议ICMP

基于IP协议,数据封装在IP数据报中,加强IP报文交付成功的机会,另参见📖https://zhuanlan.zhihu.com/p/45110873

  • 差错报文:traceroute,当IP数据报发送错误时将错误信息封装传回源主机
  • 询问报文:ping查询、子网掩码查询、时间戳查询

Ping

检测主机之间网络连通情况,基本原理:

  • 源主机构建ICMP请求报文(包含目的IP,ICMP报文中类型字段为8),交付给IP协议构建IP数据报

  • 通过ARP协议获得目的主机MAC地址后加入IP报文,交付给链路层封装成帧

  • 向目的主机发送包含ICMP echo 请求报文的数据帧

  • 目的主机收到后,确认MAC,拆解数据帧,获得IP报文后进一步交付ICMP协议处理

  • 处理完成后,目的主机构建ICMP echo 回答报文(ICMP报文中类型字段为0)

  • 根据时间和成功响应次数估算往返时间和丢包率

Traceroute(tracert)

利用差错报文遍历传输路径所有路由器,基本原理:

  • 源主机发送TTL为1的UDP探测报文,每经过路由器TTL值减一,为0时路由器向源主机发送ICMP TTL Exceed包
  • 源主机收到后显示本次传输信息,并将TTL加一,继续上述步骤直至目的主机收到探测报文,返回ICMP Dest Unreachable包
  • 源主机收到后停止Traceroute

⚠️当目的主机收到探测报文时TTL不为0,由于每次的UDP报文中会设置一个不可达的端口号,因此返回端口不可达的ICMP差错报文

网络地址转换NAT

将专用网内部IP地址转换为全球IP地址,将两个IP一一对应的利用率不高,因此将传输层的端口号加入转换,内部主机公用一个全球IP(NAPT)

路由器

从功能上划分为路由选择分组转发

路由选择协议

不同的自治系统AS可使用不同的路由协议

  • 内部网关协议:
    • RIP:基于距离向量算法交换相邻路由表的信息,实现简单开销小,但最大跳数15限制了网络规模,网络故障经过较长时间才能通知所有路由器
    • OSPF:当链路发生改变时,向AS中所有路由器发送自身与相邻路由器的链路状态(距离、时延、带宽等),克服RIP缺点,收敛更快
  • 外部网关协议:BGP,每个AS配置发言人,两个相邻BGP建立TCP连接交换路由信息,最终寻找一条比较好的路由

分组转发流程

  • 从数据报首部提取目的IP地址和目的网络地址,查询路由表依次进行如下判断:
    • 目的网络地址和当前路由器直接相连,则直接交付
    • 表中存在目的IP地址指向的特定主机路由,将数据报转发到该特定主机路由
    • 表中存在目的网络地址对应的路由,将数据报转发到该路由
    • 表中存在默认路由,将数据报转发到该默认路由
    • 分组转发失败,向生成数据报的应用程序返回“主机/网络不可达”错误

⚠️特定主机路由:为特定目的主机指明的路由,使网络管理人员能更方便地控制、测试网络,同时也可在需要考虑某种安全问题时采用这种特定主机路由。

路由表项

🔗http://docs.52im.net/extend/docs/book/tcpip/vol1/3/

  • 目的地址:主机地址/网络地址,具体由标志字段指定
  • 下一跳地址:目的地址指向的下一跳路由器IP/网络IP,由标志指定
  • 标志:一个指定目的地址,另一个指定下一跳地址
  • 为数据包传输指定的网络接口

除此之外还会包含特定主机路由、默认路由以及其他附加信息(花费、服务质量等)

传输层

网络层只在主机间传输分组,之后需要传输层提供主机间进程的通信

用户数据报协议UDP

基本特点

  • 无连接、尽最大可能交付
  • 面向报文(UDP首部+应用层报文)
  • 套接字只使用目的地址和目的端口来标识
  • 支持一对一、一对多、多对一、多对多通信
  • 传输单位:用户数据报
  • 不会因为网络拥塞而降低源主机发送速率,发送速率无限制
  • 使用场景:对实时性要求高

首部格式

UDP首部格式

⚠️UDP首部8字节,4部分均为2字节,为计算检验和临时添加伪首部

  • 检验和:只是检验,对于差错的恢复无能为力
  • 长度:整个报文长度(首部+数据)

UDP数据报中数据最大长度:IP报文首部16位指定总长度(65535字节)-IP报文首部20字节-UDP首部8字节=65507字节

上述为理论值,通常MTU默认最小576字节,因此UDP通常在576-20-8=548字节内

若按MTU最大1500字节计算:UDP最大数据1500-20-8=1472字节

传输控制协议TCP

基本特点

  • 面向连接、可靠交付
  • 面向字节流(粘包问题)
    • 粘包原因:字节流,数据无边界,两个报文数据可能重叠
    • 解决方法:
      • 双方指定一个定长的数值,作为一个数据包
      • 在包头说明包体长度,先接收头部,根据说明的长度确定包体
      • 通过在数据包刻意添加特殊字符来指定边界,但如果数据本身就包含类似特殊字符就会发生误判
      • 使用更为复杂的应用层协议
  • 一对一全双工通信
  • 传输单位:报文段
  • 提供流量控制、拥塞控制

首部格式

TCP首部格式

⚠️TCP首部中包含固定大小的20字节

  • 序号:对字节流编号,确保信息按序到达,4字节,则最大对4GB编号
  • 确认号:希望收到的下一个报文段的序号
  • 数据偏移:数据起始位距离报文起始位的偏移(1:4)(首部长度),4bit最大值15,最大偏移15*4字节,TCP首部最长60字节
  • ACK:值为1时确认号有效,建立连接后的所有报文ack均要置1
  • SYN:建立连接时同步序号,连接请求:SYN=1,ACK=0,同意建立连接:SYN=1,ACK=1
  • FIN:置为1,表示该报文的发送方数据已经发完,请求释放连接
  • 窗口:接收方的缓存有限,通过该字段让发送方明确可以发送的数据量

⚠️MSS

  • Maximum Segment Size ,TCP提交给IP层最大分段大小

  • MSS是TCP用来限制应用层最大的发送字节数,最常见的可选字段,第一次握手时指定

  • 如果底层物理接口MTU= 1500 byte,则 MSS = 1500- 20(IP Header) -20 (TCP Header) = 1460 byte

  • 如果应用层有2000 byte发送,需要两个segment才可以完成发送,第一个TCP segment = 1460,第二个TCP segment = 540。

三次握手

握手

⚠️主动发送连接请求的为客户端

  • 服务端创建传输控制块TCB,从CLOSED状态转为LISTEN(监听)状态,等待客户端的连接请求
  • 客户端创建TCB,发送连接请求报文(SYN=1不能携带数据但消耗序号),初始化随机序号seq=x,状态转为SYN-SENT(同步-发送)
  • 服务端收到后分配资源,发送确认报文(SYN=1,ACK=1),确认号ack=x+1,初始化自身随机序号seq=y,状态转为SYN-RCVD(同步-接收)
  • 客户端收到后分配资源,对服务端的确认进行确认(ACK=1,可以携带数据但需消耗序号),ack=y+1,seq=x+1,状态转为ESTABLISHED(连接已建立)
  • 服务端收到确认后,状态转为ESTAB-LISHED(连接已建立)

⚠️服务端向客户端发送的连接确认也可分为两步(四次握手),但效率不高

关于初始化序号:🔗http://docs.52im.net/extend/docs/book/tcpip/vol1/18/

  • 初始化序号随时间改变,因此每次建立连接时都不同
  • 序号可以看成是32比特的计数器,每4ms+1

作者:小谷围coder
链接:https://www.nowcoder.com/discuss/489210?channel=666&source_id=home_feed
来源:牛客网

TCP三次握手时的第一次的seq序号是怎样产生的

第一次的序号是随机序号,但也不是完全随机,它是使用一个ISN算法得到的。

seq = C + H (源IP地址,目的IP地址,源端口,目的端口)。其中,C是一个计时器,每隔一段时间值就会变大,H是消息摘要算法,输入是一个四元组(源IP地址,目的IP地址,源端口,目的端口)

三次握手?

双方都需要确认对方收到了自己发送的序列号,确认过程最少要进行三次通信

两次握手?

防止已失效的连接请求报文再次被服务端收到:

  • 客户端先发了A连接请求,网络原因报文滞留,但没有丢失
  • 客户端未收到服务端确认,等待超时重传时间到,再次发送连接请求B,成功建立连接,数据发送完毕后断开连接
  • 此时A连接再次被服务端收到,服务端向客户端回发确认报文,若为两次握手则此时连接建立
  • 客户端忽略服务端的确认报文,服务端一直等待客户端数据,资源浪费
  • 因此为三次握手,当服务端第一次没收到客户端确认后就明确客户端并未发送连接请求

四次挥手

挥手

⚠️主动断开连接的为客户端

  • 客户端进程发送连接释放请求报文(FIN=1,如果不携带数据也需消耗序号),seq=u(此前发送的数据最后一字节序号+1),状态转为FIN-WAIT-1
  • 服务端收到请求后,先立即发出确认报文(ACK=1)ack=u+1,自身序号seq=v(此前发送的数据最后一字节序号+1),状态转为CLOSE-WAIT
  • ⚠️此时为半关闭状态,客户端无数据发送,但可能还需要接收服务端尚未发完的数据
  • 客户端收到服务端确认后,状态转为FIN-WAIT-2,接收服务端数据并等待服务端发出请求连接释放报文
  • 服务端发完数据后,发送连接释放报文(FIN=1,ACK=1)ack=u+1(上次对于客户端的确认序号),自身序号seq=w,状态转为LAST-ACK
  • 客户端收到连接释放请求后,发送确认报文(ACK=1)ack=w+1,seq=u+1,进入TIME-WAIT状态
  • ⚠️此时客户端还需要等待(时间等待计时器)2MSL(最长报文寿命)后进入CLOSE彻底关闭连接,撤销TCB释放资源
  • 服务端若收到客户端的ACK则撤销TCB,状态转为CLOSE并断开连接释放资源
TIME-WAIT?
  • 客户端等待2MSL,4 分钟(MSL 为 2 分钟)的时间后,如果还是没收到服务端可能因为未收到自己的ACK报文,而超时重发的FIN报文,说明服务端已经收到ACK释放了资源
  • ⚠️服务端没收到ACK后,超时重传FIN,客户端收到后重置2MSL等待时间
  • 防止“已失效的报文“再次被收到,2MSL的时间足够让本次TCP整个连接的所有报文全部从网络上消失
四次挥手?

服务端先发送对客户端的ACK,但还需将未发完的数据继续发送,之后再发送FIN断开连接

保活计时器

正常的TCP连接中,客户端因故障而失去收发能力:

  • 服务端每次收到客户端数据后,重置保活计时器
  • 计时结束,还未收到客户端数据,服务端每隔一定时间后发送探测数据报文,若10个探测报文后客户端仍无响应,服务端主动关闭连接

可靠传输

  • 将应用数据分割成块,并进行编号,接收端将排序好的有序数据反馈给应用层
  • 通过TCP首部的检验和,检测数据变化,若有差错,直接丢弃不进行确认
  • 接收端丢弃重复的数据
  • 通过可变大小的滑动窗口实现流量控制
  • 网络出现拥堵,通过拥塞控制减少数据发送量
  • 停止等待:每发一个分组就停止发送,等待收到确认;确认后再发下一个
  • 超时重传
    • RTT:报文从发送到收到确认经历时间
    • 当在一个RTT时间内仍未收到确认报文,则触发超时重传机制,确保TCP的可靠传输
ARQ协议

自动重传请求,通过超时重传保证可靠交付,细分为停止等待和连续ARQ

  • 确认丢失:ack报文丢失
    • 发送方未收到确认报文,重传之前的报文
    • 接收方收到重复报文后直接丢弃,再次发送确认报文
  • 确认迟到:ack报文未能在超时计数结束前到达发送方
    • 发送方未收到确认报文,重传之前的报文
    • 接收方收到重复报文丢弃,再次发送确认
    • 发送方收到确认,发送新的报文
    • 在此期间,发送方又收到之前的确认报文,直接丢弃
停止等待

发送端每发送一个分组就停止发送,等待接收端的确认报文,收到之后再发下一个分组

  • 发送端设置超时计时器,计时结束未收到确认则需重传
  • 接收端收到重复分组直接丢弃,同时再次发送确认;收到损坏的分组直接丢弃
  • 简单,信道利用率低
连续ARQ

在发送窗口内的分组直接发送,接收方仅对按序到达的最后一个分组确认,提高信道利用率,易实现,确认报文丢失不必重传

滑动窗口

窗口

TCP连接的双方都有自己的窗口,用来暂存字节流,体现在报文中的窗口字段

  • 已发送并受到确认:当前字节流已经成功完成发送,滑动窗口右移
  • 已发送但未收到确认:虽然已经发送,但是暂时没收到确认报文,设置定时器来决定是否重发
    • 定时器针对的是:最早发送但未收到确认的分组
    • 在定时器时间内收到确认,则窗口右移到确认号ack的位置
    • 如果还存在已发送未确认分组,重置定时器;否则关闭定时器
    • 如果超时,重传所有未确认的分组;另外将超时的间隔设置为以前的两倍
  • 未按序收到:接收窗口中存在乱序的报文段,若无选择重传机制,则直接丢弃乱序分组并重新发送最后一个有序分组的确认
  • 允许发送但未发送:位于窗口内部,可能由于接收端已经饱和,无法继续接收;或因为存在未确认的报文段
  • 允许接收:接收窗口可以继续接收的窗口大小
  • 不允许发送/接收:窗口已满,暂时不能进行发送/接收操作

滑动窗口协议的缺点是因为使用了累计确认的机制,如果出现了只是窗口中的第一个分组丢失,而后面的分组都按序到达的情况 的话,那么滑动窗口协议会重新发送所有的分组,这样就。

⚠️Go-Back-N问题

一旦前面有分组丢失,即便之后的分组已按序到达,但发送端只根据确认号从丢失的分组开始全部重传,造成大量不必要的丢弃和重传

解决方法:选择重传协议

选择重传协议

与滑动窗口的不同:

  • 发送方窗口内部增加已确认的乱序分组
  • 每个分组单独设置定时器,而不是仅在最早发送的分组上设置

对于发送方:

  • 收到某个分组的确认后,取消其定时器;另外判断是否存在以窗口首部打头的连续分组
  • 若存在连续分组,则右移窗口;否则将该分组标记为已确认的乱序分组
  • 某个计时器超时,则重传该分组

对于接收方:

  • 只要是接收到的分组,无论连续与否,均缓存
  • 当所有乱序分组排序为一段整体有序的分组后,再交付
  • 分组出现差错或不能接受,则直接忽略
流量控制

通过获取接收方确认报文中的窗口字段,控制发送方的发送速度,以便接收方来得及接受,减少数据包丢失

  • 窗口为0,则不能发送,但有例外⚠️
    • 允许发送紧急数据:即报文段中URG(紧急比特)置为1时,配合首部的紧急指针
    • 发送1字节的数据报:通知接收方自身的窗口大小以及下一字节序号
拥塞控制

为降低整个网络的拥堵程度,减少数据包丢失,以便网络始终保持在一个相对畅通的状态

⚠️发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个

拥塞控制

慢开始&拥塞避免
  • 发送方维护一个拥塞窗口(cwnd),初始值=1,单次最大传输1个报文段
  • 发送方收到确认后,cwnd=2,4,8······,呈指数增长
  • 当cwnd >= 慢开始门限(ssthresh),即进入拥塞避免阶段,每个收发轮次cwnd+1
  • 当出现超时,ssthresh = cwnd / 2,cwnd=1,进入慢开始,重复上述步骤

⚠️若MSS为256字节,则cwndssthresh的初始值分别为256和65535字节;ssthresh初始值可以无穷大

快重传&快恢复
  • 接收方对每一个已经按序收到的报文段进行确认
  • 若出现未按序到达的报文段,则接收方会一直向发送方反馈未按序到达的那个报文段
  • 当发送方收到连续三个同样的ack后启动快重传,在定时器结束前,发送所有已发送但还未接收到确认的报文段。
  • 此时并非因为网络拥堵而造成重传,因此执行快恢复:ssthresh = cwnd / 2 ,cwnd = ssthresh,注意此时直接进入拥塞避免

📖对于TCP有限状态机11种状态详解:https://developer.aliyun.com/article/434307

  • FIN_WAIT_2:当发送端等待接收端发完数据时,发送端的状态;

    ⚠️如果在FIN_WAIT_1状态直接收到ACK和FIN的报文,则直接进入TIME_WAIT

  • CLOSING:双方同时收到对方的FIN请求,罕见

多路复用与多路分解

多路复用:从源端不同的Socket中收集数据后,分别加上对应首部封装成报文段,并传递到网络层的过程

多路分解:目的端收到传输层报文段后,将其中的数据交付给对应Socket的过程

UDP:不同报文段的UDP套接字二元组(目的IP+目的端口)如果相同,则会在目的端使用同样的Socket定位

TCP:使用套接字四元组(源IP+源端口+目的IP+目的端口)将不同报文段定位到不同Socket

端口

  • IP定位主机,端口定位进程
  • TCP/UDP端口均16位,0~65535
  • 0~1023为熟知端口号:DNS-53,HTTP-80
  • 1024~49151需要登记以防重复
  • 其余为客户端运行时动态分配,存在时间短暂

Socket

🔗https://github.com/JerryC8080/understand-tcp-udp/blob/master/chapter6.md

🔗https://blog.csdn.net/G_BrightBoy/article/details/12854117

  • 从 Linux 内核的角度来看,一个套接字就是通信的一个端点

  • 从 Linux 程序的角度来看,套接字是一个有相应描述符的文件

常用接口函数

在linux中相关方法基本定义在sys/socket.h中:

  • int socket(int domain, int type, int protocol);
    
    1
    2
    3
    4
    5
    6
    7
    8

    - 返回socket描述符(非负-成功,-1-失败),但没有完全打开,不能读写(主动套接字)
    - domain:协议域定义通信地址类型,AF_INET(ipv4)、AF_INET6(ipv6)、AF_LOCAL(绝对路径)
    - type:指定socket类型,SOCK_STREAM(面向连接TCP)、SOCK_DGRAM(无连接UDP)
    - protocol:指定协议,IPPROTO_TCP(TCP)、IPPTOTO_UDP(UDP),置为0,则为type默认对应的协议

    - ```cpp
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    - 服务端调用,把一个地址族(ipv4/ipv6)中的特定地址和socket绑定(0-成功,-1-失败) - sockfd:指定由 socket() 函数创建的主动套接字描述符 - addr:一个`sockaddr`类型的结构体指针,指向要绑定给sockfd的结构体地址,``结构体成员中包含了IP+端口号``(注意网络字节序) :warning:sockaddr结构体内容和地址族相关,ipv4=>sockaddr_in,ipv6=>sockaddr_in6,都可以强制转换成sockaddr - addrlen:表示sockaddr结构体的长度,如果是 ipv4 的 TCP 连接,一般为 `sizeof(sockaddr_in)`;
  • int listen(int sockfd, int backlog);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    - 服务端调用(``状态从CLOSED转为LISTEN``),将``主动套接字转化为监听套接字``, 该套接字可以接收来自客户端的连接请求(0-成功,-1-失败)

    :warning:在默认情况下,操作系统内核会认为 socket 函数创建的描述符对应于``主动套接字``(active socket)

    :warning:主动套接字:调用socket后得到的描述符可以调用connect()进行连接

    - sockfd:指定将要转换的主动套接字

    - backlog:指定在``队列``中的最大socket描述符个数,进入的连接请求将在队列中等待 accept() 它们

    :red_circle:对于给定的``监听套接字``,内核需要维护两个队列,两个队列之和数量不得超过backlog

    - 已完成连接队列:存放已经完成三次握手的socket连接,均为ESTABLISHED状态
    - 未完成连接队列:存放已经到达服务端,但正等待完成对应三次握手的socket连接,均为SYN-RCVD状态
    - 当队列已满(默认128),则向客户端返回RST报文

    :warning:另注意此时服务端并未阻塞

    - ```cpp
    int accept(int listenfd, struct sockaddr *addr, int *addrlen);
    - 服务器调用,若``已连接socket队列为空则阻塞``,等待来自客户端的连接请求;返回``已连接socket描述符``,用来与客户端进行数据传输 - listenfd:指定服务端的监听socket,即先由socket()创建,再经过listen()转换的socket - addr:存放发出连接请求的客户端信息,其中包含的是``客户端的IP+端口`` - addrlen:注意类型为int*,不同于bind/connect中的addr参数是直接指定,因此不能直接通过addr前16字节确定地址类型 :warning:accept中的addr是来自于客户端,无法确定地址类型ipv4/ipv6,因此需要该参数进行长度说明 :red_circle: 执行成功,返回内核自动生成的一个新的socket(已连接socket)代表与客户端的连接 - 已连接socket:完成三次握手,服务端用于和客户端传输数据的socket,由accept()返回,位于已连接队列,连接断开时关闭 - 监听socket:服务端通过socket()创建,再通过listen()得到的socket,一直存在直到服务器关闭,专门用于建立连接的socket
  • int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    - 客户端调用(`状态从CLOSED转为SYN-SENT`),自动分配端口号,与目的服务器的套接字建立一个连接(0-成功,-1-失败)

    - clientfd:本机客户端的socket描述符,直接由socket()创建

    - addr:一个`sockaddr`类型的结构体指针,包含服务端IP+端口

    - addrlen:表示sockaddr结构体的长度(协议地址长度)

    - 通过此函数建立于TCP服务器的连接,实际是发起三次握手过程,仅在连接成功(`状态从SYN-SENT转为ESTABLISHED`)或失败后返回

    :warning:UDP的connect函数没有三次握手过程,内核只是记录对方的ip和端口号,他们包含在传递给connect的套接口地址结构中,并立即返回给客户端

    - ```cpp
    int close(int fd);
    - 服务结束,客户端调用断开fd指定的socket连接,立即返回;但尝试将发送缓冲区的数据进行发送,最后再4次挥手 - 服务端一般close的是用于和对应客户端传输数据的`已连接socket`,而监听socket一直存在,除非关闭服务器

Socket缓冲区

数据收发

Linux环境下不区分套接字和普通文件的读写,一律使用read/write方法

1
ssize_t read(int fd, void *buf, size_t nbytes);
  • read 函数是负责从socket中读取nbytes字节的内容到buf,理解为客户端接收服务端的数据,win中对应recv()
  • 当读成功时,read 返回实际所读的字节数
  • 如果返回的值是 0 表示已经读到文件的结束了,小于 0 表示出现了错误
1
ssize_t write(int fd, const void *buf, size_t nbytes);
  • write 函数将 buf 中的nbytes个字节的内容写入socket,理解为客户端向服务端发送数据,win中对应send()
  • 成功时返回写的字节数,返回值大于 0,表示写了部分或者是全部的数据;失败时返回 -1

缓冲区

🔗http://c.biancheng.net/cpp/html/3040.html

⚠️每个 socket 被创建后,都会分配两个缓冲区,输入(接收)缓冲区和输出(发送)缓冲区

  • write()只是将要发送的数据暂时写入发送缓冲区就直接返回,不管数据是否真正发送,之后由TCP协议将数据从缓冲区再发送到目标机器

  • 数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络

  • read()同理,接收端只是从接收缓冲区中读取数据,而不是直接从网络中读取

I/O缓冲区特性如下:

  • I/O缓冲区在每个TCP套接字中单独存在;
  • I/O缓冲区在创建套接字时自动生成;
  • 即使关闭套接字也会继续传送发送缓冲区中遗留的数据;
  • 关闭套接字将丢失接收缓冲区中的数据。

I/O模型

一个输入(read)操作通常包括两个阶段:

  • 等待数据准备好(数据到达socket接收缓冲区)
  • 从内核向进程复制数据(将socket接收缓冲区中的数据read到进程缓冲区)

Unix I/O有五种模型:

IO

阻塞式I/O

TCP套接字默认为阻塞模式,只是阻塞当前进程,其他进程正常占用CPU时间运行,CPU利用率高:

  • read()/recv()
    • 接收缓冲区有数据则read,否则阻塞,直到数据到来
    • 需要read的数据长度小于接收缓冲区中数据长度,缓冲区中数据将积压,等待再次read
    • 一旦成功read到数据,则返回,否则一直阻塞
  • write()/send()
    • 发送缓冲区可用长度小于数据长度,write阻塞,直到缓冲区数据被发送,腾出空间后再write
    • TCP正在向网络发数据,发送缓冲区被锁定,write同样阻塞,直到发送完毕,缓冲区解锁后再write
    • 数据长度大于发送缓冲区最大长度,数据将分批发送
    • 直到所有数据已经被write到发送缓冲区后,write再返回

非阻塞式I/O

轮询(polling):

  • 进程执行系统调用后,发现socket缓冲区无法进行读写操作,内核返回一个错误码
  • 此时应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,CPU利用率低

I/O复用

使用 select 或者 poll 等待(阻塞)多个套接字中的任何一个缓冲区变为可操作,从而让单个进程具有处理多个 I/O 事件的能力

  • 没有 I/O 复用,每个 Socket 连接都要创建一个线程处理
  • 同时有几万个连接,那么就需要创建相同数量的线程
  • 相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销

select/poll/epoll 都是 I/O 多路复用的具体实现:

I/O复用 速度 监听描述符数量 其他 移植性 应用场景
select 慢(拷贝全部fd) 1024 会修改描述符 所有系统几乎支持 实时性高,fd数量少
poll 慢(拷贝全部fd) 无限制 不会修改 较新的系统 fd数量多,活跃多
epoll 快(处理就绪fd) 无限制 修改描述符状态 Linux fd数量多,但活跃的少

IO多路复用与epoll原理探究

同步IO:阻塞、非阻塞、信号、IO复用

同步IO与异步IO:

  • 同步:由用户进程将数据从内核缓冲区拷贝到用户空间
  • 异步:由内核负责拷贝数据到用户空间,完成后直接通知用户进程

阻塞IO与非阻塞IO:主要针对同步IO

  • 阻塞:IO操作完成后,返回用户空间进行后续操作
  • 非阻塞:调用IO操作后直接返回状态值,用户进程无需等待
select

fd_set的数据结构,实际上是一个long类型的数组,每一位代表一个对应的文件描述符

缺点:

  • 每次调用select,需要把fd_set整个集合从用户态拷贝到内核态
  • fd多但活跃少时,效率低
  • fd增多,遍历集合开销增大
  • 大小受限,1024个fd,若修改可能重新编译内核
poll

唯一改进select受限,fd理论65535

epoll

fd-65535,epoll_ctrl不太频繁调用,epoll_wait是非常频繁调用

  • 水平触发(LT):默认工作模式,表示当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;下次调用epoll_wait时,会再次通知此事件。
  • 边缘触发(ET): 当epoll_wait检测到事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次通知此事件。边缘触发只在状态由未就绪变为就绪时只通知一次。

核心数据结构在于红黑树+双向链表

使用场景
  • fd数目较小,且都比较活跃用select,超过1024用poll;
  • fd数目非常大,且单位时间只有小一部分fd处于就绪状态,这个时候使用epoll

信号驱动I/O

  • 应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行(非阻塞)
  • 当缓冲区收到数据,内核向应用进程发送 SIGIO 信号,进程收到之后从缓冲区read数据(可以开始进行I/O操作
  • 相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高

异步I/O

  • 应用进程执行 aio_read 系统调用会立即返回,应用进程可以继续执行(非阻塞)
  • 内核会在所有操作完成之后向应用进程发送信号(I/O操作已经完成

TCP协议下Socket交互流程

Socket

  • 服务端根据地址类型、协议类型调用socket()创建主动套接字
  • 服务端调用bind()为创建的套接字绑定IP和端口
  • 服务端调用listen()将主动socket转为监听socket,该socket仅用于监听客户端请求并建立连接,此时服务端状态由CLOSED转为LISTEN
  • 服务端继续调用accept(),由于已连接队列为空,进入阻塞,等待客户端连接
  • 客户端创建socket后根据服务端IP+端口调用connect()请求连接,此时客户端状态从CLOSED转为SYN-SENT
  • 服务端的监听套接字收到客户端socket请求后,状态转为SYN-RCVD,并将连接信息计入未完成连接队列,发送ack和自身syn请求
  • 客户端收到后,connect()返回成功,状态转为ESTABLISHED,并向服务端发送ack
  • 服务端收到ack后,accept()返回成功,并由内核创建已连接socket用于数据传输,状态转为ESTABLISHED,并将连接信息计入已连接队列
  • 至此连接建立,下面数据传输read/write套接字
  • ----------=======================================================================================================================================================================================================================================
  • 交互结束,客户端调用close()断开连接,方法直接返回,并最后将发送缓冲区的数据发完后,关闭socket,四次挥手CLOSED
  • 服务端调用close()关闭对应的socket连接,连接信息从已连接队列移除,监听socket继续监听接受下一次客户端的连接

应用层

应用层协议定义不同主机进程之间的交互规则

DHCP协议

基于UDP,动态主机配置协议,自动配置IP、掩码、网关IP,流程如下:

  • 客户端在局域网广播发送Discover报文,如果DHCP服务器和客户端不在同一子网,还需要中继代理
  • 服务端收到后发送给客户端的Offer报文中包含IP信息(IP 地址、DNS 服务器的 IP 地址、默认网关路由器的 IP 地址和子网掩码)
  • 客户端可能受到多个Offer,需进行选择;然后给一个服务端发送Request报文
  • 服务端收到后,再回发ack确认报文,表明客户端可以使用提供的IP信息
  • 客户端最后收到ack确认报文,即可开始使用动态分配的IP

DNS协议

🔗http://docs.52im.net/extend/docs/book/tcpip/vol1/14/

🔗https://developer.aliyun.com/article/396757

  • DNS系统:分布式的数据库,每个站点只保留自己的数据

  • 域名解析器:通常是应用程序的一部分,Unix中库函数gethostbyname接收主机名返回IP,gethostbyaddr通过IP查找主机名

  • 域名服务器:一个区域包含一个主域名服务器和至少一个辅助域名服务器,独立且冗余

  • 域名解析:实现主机名和IP地址的互相转换,端口53,UDP传输

  • 高速缓存:一个域名服务器收到主机和IP的映射时放入缓存,减少DNS通信量,缓存记录字段如下:

    • TTL:表示该记录被其他DNS服务器缓存的时间

    • Type:A、NS、CNAME、MX,对应的Name-Value意义如下:

      Type Name Value
      A 主机名 主机IP(列表),标准的主机名到 IP 地址映射
      NS 域名 负责该域名的 DNS 服务器的主机名
      CNAME 别名 规范的主机名,针对复杂的主机名提供便于记忆的简单别名
      MX 邮件服务器别名 邮件服务器的规范主机名

⚠️多数情况使用UDP进行传输,要求域名解析器和域名服务器自己保证可靠性

🚨以下情况使用TCP传输:

  • 返回的响应超过512字节(删减标志位TC=1),域名解析器使用TCP重新发送查询

    ⚠️512字节保证UDP报文不会在IP层因为MTU限制而分片,这个限制可通过扩展的DNS协议打破🔗https://www.cnblogs.com/protosec/p/11673326.html

  • 辅助域名服务定时(3h)向主域名服务器查询数据是否变动,如果发生变动则进行区域传送,数据量往往很大,使用TCP

负载均衡

大型网站一个域名对应多个IP地址,DNS请求时返回域名对应的IP集合,客户端一般选择最前面的IP发送请求,类似循环队列放入队尾来均分DNS服务端压力

查询过程

域名层次结构:主机名.次级域名.顶级域名.根域名

  • 先将DNS请求发送至本地DNS服务器(递归查询,用户只发送一次请求),缓存中无记录,则向根域名发送查询请求(迭代查询
  • 根域名服务器返回对应顶级域名服务器IP列表,本地DNS服务器向其中一个顶级服务器发送查询请求(返回,继续迭代查询)
  • 顶级域名服务器返回对应权威(次级)域名服务器IP列表,本地DNS服务器向其中一个服务器发送查询请求(返回,继续迭代查询)
  • 权威域名服务器返回一个对应的主机名的 IP 地址列表,最后本地DNS服务器返回给客户端(返回,迭代结束,递归开始回溯)

HTTP协议

超文本传输协议HTTP基于TCP,定义客户端和服务器之间交换报文的格式和方式,默认使用 80 端口

HTTP 是一个无状态的协议,HTTP 服务器不会保存关于客户的任何信息

请求报文

请求

  • 首行为请求行:GET /index.html HTTP/1.1,第二项为请求的资源页面,第三项为HTTP版本

    • GET:HTTP方法,常用如下:

      请求类型 作用
      GET 获取资源
      POST 传输实体主题(提交一个表格),可能导致新资源建立或已有资源修改
      HEAD 获取报文首部(不包含资源),确认URL有效、资源更新时间等
      PUT 上传文件,无验证机制,不安全;完全替换原始资源,一般不用
      DELETE 删除指定文件,无验证机制
      PATCH 不同于PUT,支持部分修改资源
      OPTIONS 查询URL支持的方法,返回类似:Allow: GET, POST, HEAD, OPTIONS
      CONNECT 与代理服务器通信时建立隧道,使用SSL和TLS协议把通信内容加密后经网络隧道传输
      TRACE 追踪路径,服务器返回通信路径,易受攻击
  • 之后为请求头,与请求行构成请求信息头

  • 最后为请求信息体,与信息头中间空了一行

GET和POST区别
对比维度 GET POST
作用 获取资源 传输实体主体
参数 以查询字符出现在URL中 存储在实体主体中
安全 不会改变服务器状态,安全 上传表单后,服务器进行存储,状态改变,不安全
幂等性 执行多次效果一样,服务器状态也一样,幂等 执行多次增加多次记录,非幂等
可缓存 GET方法本身可以缓存 POST 在多数情况下不可缓存的
报文格式 报文中实体部分为空 实体部分一般为向服务器发送的数据

响应报文

响应

  • 首行为状态行:HTTP/1.1 200 OK,第一项为版本

    • 200 OK:状态码,常用如下:

      状态码 类别 含义
      100 Continue 信息性状态码 一切正常
      200 OK 成功状态码 请求正常处理完毕
      3XX 重定向状态码 需进行额外操作来完成请求
      404 Not Found 客户端错误状态码 请求的页面没找到
      403 Forbidden 客户端错误状态码 请求被拒绝
      401 Unauthorized 客户端错误状态码 发送的请求需要有认证信息,如果之前已进行过一次请求,则表示用户认证失败
      400 Bad Request 客户端错误状态码 请求报文中存在语法错误
      500 Internal Server Error 服务器错误状态码 服务器执行请求时发生错误
      503 Service Unavailable 服务器错误状态码 服务器暂时处于超负载或正在进行停机维护,现在无法处理请求
  • 之后为回复头,与状态行构成回复信息头

  • 最后为回复信息体,即请求的页面,同样与信息头隔了一行

连接方式

  • 长/短连接

    • 短连接:HTTP/1.1之前默认短连接,每一次HTTP通信新建一个TCP连接,使用 Connection : Keep-Alive长连接,有一个保持时间
    • 长连接:HTTP/1.1开始默认长连接,只需要建立一次 TCP 连接就能进行多次 HTTP 通信,使用 Connection : close断开连接
  • 流水线

    • HTTP 请求默认按顺序发出,上一个请求收到响应后才能发出下一个请求
    • 受网络延迟和带宽限制,下一个请求就可能需要等待很长时间
    • 流水线是在同一条长连接上连续发出请求,不用等待响应返回,减少延迟

Cookie

HTTP协议本身无状态,简单的协议更能处理大量事务。1.1开始引入Cookie保存状态信息

  • Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据
  • 之后每次向同一服务器再次发起请求时被携带,告知服务端请求是否来自同一浏览器,因此会带来额外的性能开销
  • 随着浏览器支持的存储方式多样化,Cookie 渐渐被淘汰

Session

利用 Session 将用户信息存储在服务器端的文件、数据库或者内存,也可以存在 Redis内存型数据库中,效率更高,更安全

Session 维护用户登录状态过程:

  • 用户登录提交包含用户名和密码的表单,放入 HTTP 请求报文
  • 服务器验证用户信息,如果正确则存储到 Redis 中,其中的Key 称为 Session ID
  • 服务器返回的响应报文中Set-Cookie首部字段包含Session ID,客户端收到后将该 Cookie 值存入浏览器
  • 客户端之后对同一服务器发送的请求包含该 Cookie 值,服务器收到之后提取对应Session ID,进一步从 Redis 中查找用户信息并返回

⚠️Session ID经常重新生成以保证安全,另按照安全性要求还会额外附加验证码、登录的操作

🚨cookie和session对比:

  • Cookie 只能存 ASCII 码,Session对数据类型无限制
  • Cookie 存储在浏览器中安全性低,可将 Cookie 值加密,在服务器解密
  • 用户所有信息都存储在 Session 中开销非常大

HTTP/1.1缺点

  • 长连接情况下多个请求复用同一个TCP连接,连接中的通信次序是固定的
  • 服务器按次序挨个处理请求,如果前面请求响应慢,则之后的请求都在排队等待(队头阻塞)
  • 避免:减少请求数,同时打开多个长连接

HTTP/2

二进制协议(头信息和数据体都是二进制,统称为“帧”),而1.1版本报头信息必须是ASCII文本

  • 同样复用TCP连接,但连接双方均可不用按照次序同时发送多个请求或响应,解决队头阻塞问题
  • 由于无序,则需要对包中的数据做标记,将每个请求或回应的所有数据包称为一个数据流并通过唯一编号进行区分
  • 通过头信息压缩提高速度;另外通过服务器推送主动向客户端推送静态资源

缺点:多个数据流复用同一个TCP连接,一个流进入拥塞,之后的流均被阻塞(与本身设计无关,TCP协议的问题)

  • HTTP/3 协议:基于 UDP 协议的 QUIC 协议,并且使用在了 HTTP/3 上。 QUIC 协议在 UDP 协议上实现了多路复用、有序交付、重传等等功能

HTTP协议缺点:

  • 报文为明文,容易被获取,获取后易被修改
  • 通信双方没有认证,容易被冒充

HTTPS协议

  • HTTPS 是基于 HTTP 协议,使用 TLS/SSL 来对数据加密
  • 提供了一种校验机制检测信息是否被篡改
  • 通过身份证书防止被冒充

🔗https://zhuanlan.zhihu.com/p/43789231

TLS握手

  1. 客户端向服务器发起请求,请求中包含使用的协议版本号、生成的一个随机数、以及客户端支持的加密方法
  2. 服务器端接收到请求后,确认双方使用的加密方法、并给出服务器的证书、以及一个服务器生成的随机数
  3. 客户端确认服务器证书有效后,生成一个新的随机数,并使用数字证书中的公钥加密这个随机数,最后将Hash后的值发给服务器用来供服务器检验
  4. 服务器使用自己的私钥解密客户端发送过来的随机数,并提供前面所有内容的 hash 值来供客户端检验
  5. 客户端和服务器端根据约定的加密方法使用前面的三个随机数生成对话秘钥,以后的对话过程都使用这个秘钥来加密信息

实现原理

TLS 的握手过程主要用到了三个方法来保证传输的安全:

  • 对称加密:双方使用同一个秘钥对数据进行加密和解密,但秘钥还是会通过网络传输
  • 非对称加密:公钥公开,私钥保密,使用公钥对数据进行加密,使用私钥进行解密,但是加密过程慢
  • 两者结合:对称加密无法保证秘钥安全传输,因此非对称加密用于对称加密的秘钥,而通信使用对称加密的方式来加密

⚠️没有办法确定公钥是安全的,公钥也可以被截获后伪装

数字证书

最重要的是CA(认证中心)的可靠性,浏览器内置一些顶层的认证中心的证书

  • 使用Hash算法对公钥等信息加密生成信息摘要

  • CA 用自己的私钥对信息摘要加密形成数字签名

  • 信息摘要+数字签名=数字证书

  • 接收方对收到的数字证书作如下处理:

    • 取得数字证书中的信息摘要,并使用同样的 Hash 算法生成一个摘要
    • 使用CA的公钥来对数字证书中的数字签名进行解密

    🚨CA的公钥也是通过数字证书验证,浏览器添加信任的CA公钥对应的证书

    • 将解密的摘要和生成的摘要进行对比,从而确定公钥是否被更改
为啥要Hash

证书信息hash之后,数据为定长,此时进行加密解密就很快

每次请求都要TLS握手?

使用session,在服务器存储密钥到对应的session ID,客户端每次请求携带ID,服务端查询密钥解密即可

www.google.com(Web请求过程)

🔗http://www.178linux.com/1055

URL合法性检验

  • 分析所需要使用的传输协议和请求的资源的路径
  • 如果资源已经在缓存中并且没有失效就直接使用,否则再发起新的请求

DHCP配置主机信息

  • 如果主机还没有IP信息,则通过DHCP配置主机信息
  • 主机发送广播,封装为DHCP请求报文,DHCP服务器收到后返回IP相关信息
  • 双方经过确认后(参考DHCP流程),主机可以开始使用IP地址发送报文了

ARP获取网关MAC地址

  • DHCP返回信息中只包含网关的IP地址,因此还需要ARP协议解析出MAC地址

  • ARP缓存中找不到网关IP对应MAC地址,则主机ARP广播自身IP和MAC,询问网关IP的MAC

  • 网关收到后向主机返回自己的MAC地址,主机可以向网关发送DNS请求了

DNS解析URL获取目的IP

  • 知道网关MAC后,如果本地没有URL对应IP地址缓存,则主机向网关发送DNS请求,网关根据转发表确认下一跳转发
  • 路由器包含内外网关协议,因此无论DNS服务器在内外网,经过网关路由器转发一定可以到达DNS服务器
  • DNS服务器收到查询请求后,如果缓存中找不到域名对应IP,则进行迭代查询
  • DSN服务端收到权威域名服务器返回的IP地址集合后,转到网关路由器最终到达主机,主机获得目的IP地址

ARP获取目的主机MAC地址

  • 由于应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号
  • 然后下发给网络层,网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址
  • 然后下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机MAC地址作为源MAC地址
  • 目的 MAC 地址分情况处理:
    • 通过将 IP 地址与我们本机的子网掩码相与,判断是否与请求主机在同一个子网
    • 在同一个子网,使用 APR 协议获取到目的主机的 MAC 地址
    • 不在一个子网,请求应该转发到网关,最终通过多次转发后ARP请求发给目的主机
    • 目的主机发送自己的MAC地址到源主机

获取到Web服务器IP后,主机作为客户端和服务端进行TCP三次握手过程

如果使用HTTPS,正式通信之前还额外需要TLS握手来保证传输的安全性

HTTP请求页面

  • TCP连接建立,TLS握手成功之后,浏览器生成HTTP GET报文,并发送到Web服务端
  • 服务端收到请求后,从TCP socket读取GET报文,将请求的页面作为数据封装为HTTP响应报文,发送回主机
  • 主机浏览器收到后,抽取出页面数据并进行渲染,最终显示Web页面

最终通信结束,四次握手释放连接

面经补充

服务端大量TIME-WAIT

🔗https://www.cnblogs.com/dadonggg/p/8778318.html

🔗https://blog.csdn.net/acs713/article/details/28427181

🔗https://jishuin.proginn.com/p/763bfbd25a93

出现场景:高并发短连接的HTTP请求

🔴 如果 connection 头部取值被设置为 close 时,基本都由「服务端」发起主动关闭连接

HTTP短连接如果由服务端发出FIN信号,则在高并发情况下,服务端会出现大量TIME-WAIT状态

  • 如果此时服务端重启,则之前TIME-WAIT链接还没断开,对应端口还未释放
  • 当服务端再次尝试bind相同端口时,端口绑定失败,客户端也就链接失败

服务端大量CLOSE-WAIT

作者:小谷围coder
链接:https://www.nowcoder.com/discuss/489210?channel=666&source_id=home_feed
来源:牛客网

close_wait状态是在TCP四次挥手的时候收到FIN但是没有发送自己的FIN时出现的,服务器出现大量close_wait状态的原因有两种:

  • 服务器内部业务处理占用了过多时间,都没能处理完业务;或者还有数据需要发送;或者服务器的业务逻辑有问题,没有执行close()方法
  • 服务器的父进程派生出子进程,子进程继承了socket,收到FIN的时候子进程处理但父进程没有处理该信号,导致socket的引用不为0无法回收

处理方法:

  • 停止应用程序
  • 修改程序里的bug

SO_REUSEADDR和SO_REUSEPORT

🔗https://zhuanlan.zhihu.com/p/37278278

服务端调用bind时增加的参数

SO_REUSEADDR:字面意思re use addr,地址重用

  • 确保当服务端出现timewait状态的链接时,server能够重启成功

⚠️如果不需要2MSL等待时间,而需要立即释放端口,也可以设置该参数

SO_REUSEPORT:re use port,端口重用

  • 使得多进程/线程创建多个绑定同一个ip:port的监听socket,提高服务器的并发能力

  • 每一个进程有一个独立的监听socket,并且bind相同的ip:port,独立的listen()和accept()

  • 避免了惊群效应

惊群效应

🔗https://www.yanxurui.cc/posts/linux/2017-09-07-thundering-herd-and-reuseport/

惊群(thundering herd)问题:

  • 多个进程等待同一个事件(比如同一个socket的可读事件)
  • 当事件发生时,内核唤醒所有的进程,但该事件只需要被一个进程处理
  • 浪费CPU资源

listen_accept_reuseport.png

当SO_REUSEPORT选项启用时:

  • 每个进程都创建独立的listening socket,监听相同的ip端口
  • accept的时候只有一个进程会获得连接。这样就可以避免加锁的开销,提高CPU利用率

可靠UDP

🔗https://www.jianshu.com/p/6c73a4585eba

UDP本身在传输层,不可靠,因此需要在应用层实现可靠传输

  • 添加seq/ack机制,确保数据发送到对端
  • 添加发送和接收缓冲区,主要是用户超时重传
  • 添加超时重传机制

详细说明:

  1. 发送数据时,生成一个随机seq=x,然后每一片按照数据大小分配seq
  2. 数据到达接收端后放入接收端缓存,并发送一个ack=x的包,表示对方已经收到了数据
  3. 发送端收到了ack包后,删除缓冲区对应的数据,发送下一个字段
  4. 超时时间到后,定时任务检查是否需要重传数据