一、计算机网络的发展背景
1、网络的定义
网络是指将多个计算机或设备通过通信线路、传输协议和网络设备连接起来,形成一个相互通信和共享资源的系统。
2、局域网 LAN
相对于广域网 WAN 而言,局域网 LAN 主要是指在相对较小的范围内的计算机互联网络
主要特点:传输速度快、延迟低,并且用于连接同一组织内部的计算机和设备
- 例如,一家公司内部的多台电脑通过路由器或交换机连接在一起形成的网络就是局域网
私网、内网,即局域网
3、广域网 WAN
就是通常所说的 Internet,是指跨越较大地理范围的计算机网络
通常由多个局域网或城域网互相连接而成,是一个遍及全世界的网络,将远隔千里的计算机都连在一起,可以覆盖多个城市、省份甚至国家之间的分支机构或办公地点,并且利用互联网等公共网络进行数据传输
公网、外网,即广域网
局域网和广域网只是一个相对的概念,如果硬要区分的话可以看路由器,没有路由器就是局域网,需要路由器横跨就是广域网,但不绝对
二、协议
1、概念
协议是指多方之间达成的一种约定或规定,用于指导各方在特定情况下的行为和相互之间的关系
在网络中,为了确保数据传输的有效性、互联互通、统一标准、安全性和资源管理,网络也有属于自己的协议
2、本质
为了让不同厂商生产的计算机能够相互顺畅的通信,还需要计算机对应的硬件厂商以同样的规范来处理 0/1 问题,需要约定一个共同的标准让大家都遵守,这就是网络协议(TCP/IP)
协议的本质是一层软件层,是为了让双方通信的本质更高效
计算机的内部有很多组件,而它们在计算机内部是用线连接起来的,所以一台计算机的内部本质上也是一个小型的网络结构,计算机内部设备和设备之间也存在协议,比如驱动程序访问硬件就得通过协议
- 假设这些线足够长,将硬盘放到几百公里之外,那么此时再去存储数据就由原来的写入本地变成通过网络写入远端了,再把 CPU 等都放到远处,就相当于把一台计算机的各个功能用多台计算机构建起来,通过网络来进行连接
在计算机中,体系结构中有网络,网络中有体系结构
因为多台主机的距离较远,为了减少通信成本,所以需要有协议的存在
- 通信的复杂问题本质上是跟距离成正相关的
3、协议分层
在实际定制网络协议时,是以层状划分的
(1)原因
通信的场景复杂
通过分层可以完成不同协议之间的解耦,也便于人们对其进行各自维护
(2)好处
最大的好处在于 “封装”
把软件进行模块划分,可以很好的进行解耦,对任何一层进行修改并不会影响到其他层
(3)依据
把功能比较集中、耦合度较高的的模块放在一层,也就是高内聚,而每一层都是要解决特定的问题
(4)数据传输的条件
处理数据的能力:数据传递过去主机要识别这是什么,然后才能使用这个数据 —— 应用层
丢包问题:另一台机器可能没有收到发送过去的信息 —— 传输层
定位问题:有成千上万个主机,需要确定给哪一台机器 —— 网络层
解决下一跳问题:当两台相离很远的主机之间要传递数据,那么要先有传递一台主机数据的能力,也就是数据包交付能力,然后一台一台的 “蹦” 到目标主机 —— 数据链路层
有信号传输的能力:物理层,但不属于软件
三、OSI 七层模型
OSI(Open System Interconnection,开放系统互连)把网络从逻辑上分为了七层,每一层都有相关的物理设备
是一种框架性的设计方法
最主要的功能:帮助不同类型的主机实现数据传输,解决异种网络互连时所遇到的兼容性问题
1、服务、接口和协议的相关概念
服务说明某一层为上一层提供一些什么功能
接口说明上一层如何使用下层的服务
协议涉及如何实现本层的服务
2、优点
通过七个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯
各层之间具有很强的独立性,互连网络中各实体采用什么样的协议是没有限制的,只要向上提供相同的服务且不改变相邻层的接口即可
使网络的不同功能模块 / 层次分担起不同的职责
- 减轻问题的复杂程度,一旦网络发生故障,可迅速定位故障所处层次,便于查找和纠错
- 在各层分别定义标准接口,使具备相同对等层的不同网络设备能实现相互操作,各层之间则相对独立,一种高层协议可放在多种低层协议上运行
- 能有效刺激网络技术革新,因为每次更新都可以在小范围内进行,不需对整个网络动大手术
3、缺点
- 既复杂又不实用,在工程实践中把应用层表示层和会话层压缩成一层
4、会话层(Session Layer)
通过传输层(端口号:传输端口与接收端口)建立数据传输的通路
在系统之间发起会话或者接受会话请求
- 设备之间需要互相认识可以是 IP,也可以是 MAC 或者是主机名
负责在网络中的两节点之间建立、维持和终止通信
(1)功能
- 建立通信链接,保持会话过程通信链接的畅通
- 同步两个节点之间的对话,决定通信是否被中断以及通信中断时决定从何处重新发送
(2)网络通信的 “交通警察”
- 当通过拨号向你的 ISP(因特网服务供应商)请求连接到因特网时,ISP 服务器上的会话层向你和你的 PC 客户机上的会话层进行协商连接。若你的电话线偶然从墙上插孔脱落时,你终端机上的会话层将检测到连接中断并重新发起连接
- 会话层通过决定节点通信的优先级和通信时间长短来设置通信期限
5、表示层(Presentation Layer)
确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取
应用程序和网络之间的翻译官,数据将按照网络能理解的方案进行格式化,这种格式化也会因所使用网络类型的不同而不同
- 例如,PC 程序与另一台计算机进行通信,其中一台计算机使用 EBCDIC,而另一台则使用 ASCII 来表示相同的字符。如有必要,表示层会通过使用一种通格式来实现多种数据格式之间的转换
表示层管理数据的解密与加密,如系统口令的处理
- 在 Internet 上查询银行账户使用的是一种安全连接。账户数据在发送前被加密,在网络的另一端,表示层将对接收到的数据解密。除此之外,表示层协议还对图片和文件格式信息进行解码和编码
四、TCP / IP 五层模型
TCP/IP 是一组协议的代名词,它还包括许多协议,组成了 TCP/IP 协议簇
TCP/IP 通讯协议采用了 5 层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求
1、物理层(Physical Layer)
OSI 模型的最低层或第一层,包括物理连网媒介,如电缆连线连接器
- 主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等
主要作用:传输比特流
- 由 1 和 0 转化为电流强弱来进行传输,到达目的地后再转化为 1 和 0,也就是常说的数模转换与模数转换
- 这一层的数据叫做比特
物理层的协议产生并检测电压,以便发送和接收携带数据的信号
桌面 PC 插入网络接口卡,就建立了计算机连网的基础
- 提供了一个物理层,尽管物理层不提供纠错服务,但它能设定数据传输速率并监测数据出错率
- 网络物理问题,比如电线断开,将会影响物理层
用户要传递信息就要利用一些物理媒体,如双绞线、同轴电缆等,但具体的物理媒体并不在 OSI 的七层之内,有人把物理媒体当做第 0 层,物理层的任务就是为它的上一层提供一个物理连接,以及它们的机械、电气、功能和过程特性
- 比如规定使用电缆和接头的类型、传送信号的电压等
- 在这一层,数据还没有被组织,仅作为原始的位流或电气电压处理
负责光 / 电信号的传递方式
- 现在以太网通用的网线,双绞线
- 早期以太网采用的的同轴电缆,现在主要用于有线电视
- 光纤
- WiFi 无线网使用电磁波
物理层的能力决定了最大传输速率、传输距离、抗干扰性等
集线器(Hub)工作在物理层
2、数据链路层(Datalink Layer)
OSI 模型的第二层,它控制网络层与物理层之间的通信
定义了如何让格式化数据进行传输,以及如何让控制对物理介质的访问
提供错误检测和纠错,以确保数据的可靠传输
主要功能:在不可靠的物理线路上进行数据的可靠传递
- 为了保证传输,从网络层接收到的数据被分割成特定的可被物理层传输的帧,帧是用来移动数据的结构包,它不仅包括原始数据,还包括发送方和接收方的物理地址以及检错和控制信息
- 地址确定了帧将发送到何处,而纠错和控制信息则确保帧无差错到达
- 如果在传送数据时,接收点检测到所传数据中有差错,就要通知发送方重发这一帧
有一些连接设备,比如交换机(Switch),由于它们要对帧解码并使用帧信息将数据发送到正确的接收方,所以它们是工作在数据链路层的
数据链路层在物理层提供比特流服务的基础上,建立相邻结点之间的数据链路,通过差错控制提供数据帧(Frame)在信道上无差错的传输,并进行各电路上的动作系列
作用:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等
数据链路层协议的代表包括:SDLC、HDLC、PPP、STP、帧中继等
负责设备之间的数据帧的传送和识别
- 网卡设备的驱动
- 帧同步:从网线上检测到什么信号算作新帧的开始
- 冲突检测:如果检测到冲突就自动重发
- 数据差错校验
- 有以太网、令牌环网,无线 LAN 等标准
3、网络层(Network Layer)
OSI 模型的第三层,主要功能是将网络地址翻译成对应的物理地址,并决定如何将数据从发送方路由到接收方
在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择:地址管理和路由选择
- 比如在 IP 协议中,通过 IP 地址来标识一台主机,并通过路由表的方式规划出两台主机之间的数据传输的线路(路由)
网络层通过综合考虑发送优先权、网络拥塞程度、服务质量以及可选路由的花费来决定从一个网络中节点 A 到另一个网络中节点 B 的最佳路径
- 由于网络层处理,并智能指导数据传送,路由器连接网络各段,所以路由器(Router)属于网络层
- 在网络中,“路由” 是基于编址方案、使用模式以及可达性来指引数据的发送
网络层负责在源机器和目标机器之间建立它们所使用的路由,这一层本身没有任何错误检测和修正机制,因此网络层必须依赖于端与端之间的可靠传输服务
网络层用于本地 LAN 网段之上的计算机系统建立通信,之所以可以这样做,是因为它有自己的路由地址结构,这种结构与第二层机器地址是分开的、独立的,这种协议称为路由或可路由协议
网络层是可选的,它只用于当两个计算机系统处于不同的由路由器分隔开的网段的这种情况,或者当通信应用要求某种网络层或传输层提供的服务、特性或者能力时
- 例如,当两台主机处于同一个 LAN 网段的直接相连这种情况,它们之间的通信只使用 LAN 的通信机制就可以了,即 OSI 参考模型的一二层
4、传输层(Transport Layer)
OSI 模型中最重要的一层
定义了一些传输数据的协议和端口号
- 比如协议:TCP、UDP,端口号:WWW 端口 80
- 主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组,常常把这一层数据叫做段
负责两台主机之间的数据传输
传输协议同时进行流量控制或是基于接收方可接收数据的快慢程度规定适当的发送速率
传输层按照网络能处理的最大尺寸将较长的数据包进行强制分割
- 例如,以太网无法接收大于 1500 字节的数据包
发送方节点的传输层将数据分割成较小的数据片,同时对每一数据片安排一序列号,以便数据到达接收方节点的传输层时,能以正确的顺序重组,该过程即被称为排序
工作在传输层的一种服务是 TCP/IP 协议套中的 TCP,另一项传输层服务是 IPX/SPX 协议集的 SPX
5、应用层(Application Layer)
是最靠近用户的 OSI 层
主要负责对软件提供接口,使程序能使用网络服务
术语 “应用层” 并不是指运行在网络上的某个特别应用程序
应用层提供的服务
- 包括文件传输、文件管理以及电子邮件的信息处理
负责应用程序间的网络服务
- 比如 SMTP、FTP、Telnet 等
网络编程主要就是针对应用层
6、物理层考虑的比较少,因此很多时候也可以称为 TCP/IP 四层模型
对于一台主机,它的操作系统内核实现了从传输层到物理层的内容
对于一台路由器,它实现了从网络层到物理层
对于一台交换机,它实现了从数据链路层到物理层
对于集线器,它只实现了物理层
上述内容并不绝对,很多交换机也实现了网络层的转发,很多路由器也实现了部分传输层的内容,比如端口转发
7、OS 层次图
数据链路层中的网卡层是驱动程序的一部分
网络层和传输层是操作系统内部自己实现的,所有的操作系统都一样,所以全球的主机都能互联
在应用层和传输层的之间会有系统调用接口,主要是文件类的系统调用接口
五、网络传输基本流程
1、网络传输流程
局域网中同一个网段内的两台主机是可以直接进行通信(文件传输)的
客户在应用层发送数据,为了保证数据安全、完整和网络之间的路径选择等问题,必须自顶向下经过应用层、传输层、网络层、链路层,再通过局域网发送给对方
跨网段的主机的文件传输,数据从一台计算机到另一台计算机传输过程中要经过一个或多个路由器
一个设备至少要横跨两个网络,才能实现数据包跨网络转发
- 路由器必须要横跨两个网络,也就是必须有两张网卡
每一层都会有协议,而每一个协议的最终表现就是报头
- 协议通常是通过协议报头来表达的,每一份数据在每一层都要有自己的报头
- 每层都有自己的协议定制方案,每层协议都要有自己的协议报头,从上到下交付数据时,都会添加对应的报头
(1)报头(Header)
A. 概念
在计算机网络通信中,报头是一部分数据包的固定结构,它包含了关于该数据包的元信息和控制信息
报头位于数据包的前部,用于标识和管理数据包的传输
通常由多个字段组成,每个字段用于存储特定类型的信息
- 目标地址:指示接收数据包的目标设备或主机的地址
- 这个地址可以是物理地址或逻辑地址,比如 MAC 地址或 IP 地址
- 源地址:标识发送数据包的源设备或主机的地址
- 协议:指示数据包使用的协议类型,例如 TCP / UDP
- 长度:指明整个数据包的长度,包括报头和数据部分
- 校验和:校验数据包在传输过程中是否出现了错误
- 接收端可以通过计算校验和来验证数据包的完整性
- 服务质量:用于指示数据包的优先级和处理要求
- 例如差错检测、传输延迟、带宽需求等
- 标记:存储一些额外的控制或标识信息,用于特定的协议或网络处理
协议报头就是收到的报文当中多出来的内容
B. 作用
承载了传输过程中所需的元信息和控制信息
- 标识和定位:报头中的目标地址字段和源地址字段用于标识和定位数据包的接收方和发送方
- 通过指定目标地址,数据包可以准确地传递给目标设备或主机
- 数据处理和路由:报头中的协议字段指明了数据包所使用的传输协议,如 TCP、UDP 等
- 不同的协议可能需要进行不同的数据处理和路由方式,因此报头能够帮助网络设备正确地处理和路由数据包
- 错误检测和纠正:校验和字段可以用于验证数据包在传输过程中是否出现错误
- 接收端可以通过计算校验和来检测数据包的完整性
- 对于出现错误的情况,一些纠错技术可以根据校验和字段的信息来恢复原始数据
- 服务质量管理:报头中的服务质量字段可以指示数据包的优先级和处理要求
- 有助于网络设备在网络拥塞或负载高的情况下,根据不同的服务质量需求进行优先级处理,保证关键数据的传输效果
- 特定协议需求:某些协议可能需要特定的控制或标识信息来辅助数据包的处理和传输
- 报头中的标记字段可以承载这些额外的控制信息,以满足特定协议的需求
通过解析报头中的字段信息,网络设备和计算机能够理解和处理数据包,识别其源和目标,并根据需要采取适当操作,比如路由转发、错误检测、数据重组等,确保数据包按照正确的方式传输并被正确处理
C. 添加报头的原因
数据识别和标识:在数据传输中,报头包含关于数据的元信息,比如数据类型、数据长度、传输协议等。通过添加报头,可以对数据进行识别和标识,确保接收方能够正确地解析和处理数据
协议规范:不同的数据传输协议通常会规定报头的格式和内容,以便确保传输的正确性和可靠性。报头中可能包含有关源地址、目标地址、校验位等必要信息,这些信息使得数据能够按照协议规范进行有效的传输
数据完整性校验:在数据传输过程中,为了确保数据的完整性,通常会使用校验和或哈希值等方式对数据进行校验。报头中可以包含校验和或哈希值等信息,接收方在接收到数据后可以根据报头中的校验信息验证数据的完整性,以避免数据损坏或篡改的情况
数据流控制和错误处理:报头中可能还包含有关数据流控制和错误处理的信息,比如序列号、确认号等。这些信息可以用于在数据传输过程中进行流量控制、处理丢包和重传等情况,从而提高数据传输的可靠性和效率
(2)局域网通信原理
两台局域网的主机是能够直接通信的
命令:ifconfig
每一台主机都有自己的网卡,每一张网卡都有自己的地址,叫做 MAC 地址,就像我们的身份证一样,标识网卡的唯一性
- 虽然 MAC 地址全球唯一,但是不应用于全球,只是在局域网中标识自己的唯一
- 这里的 MAC 地址是个虚拟地址
在局域网中有很多主机,假设现在 MAC3 想要跟 MAC5 发送消息,那么其它主机都能收到该信息,但其他主机在做协议判断时发现并不是发送给自己的,就会自动丢弃
(3)以太网和令牌环网
A. 以太网
以太网就是一种具体的局域网
以太网的通信方式就是局域网的通信方式:发出的消息所有人都能收到
当 MAC3 想把消息发给 MAC5,MAC2 想把消息发给 MAC7 时,它们不能同时发消息,因为数据会发生覆盖
以太网发送消息的原则:只允许一个主机在任何一个时刻在局域网中发消息,否则就会发生碰撞,如果发生了碰撞就把消息作废然后重发
站在系统的角度看待网络资源就是临界资源
B. 令牌环网
解决方式:谁持有令牌环,就谁发送数据,没有令牌的主机就不能发送消息,类似于系统中的锁
2、数据包封装和分用
两个人在用户层直接进行聊天,而实际上却是数据向下交付(封装),再向上解包(分用)才实现的聊天
数据包封装:在向下交付的时候每一层都会添加自己的报头,报文 = 报头+有效载荷,再把报文向下交付
数据包分用:在向上解包时,因为同一层有相同的协议,所以能识别报头,它会解开报头,把有效载荷在向上解包
- 解包的本质就是去掉报头,展开分析
这样就形成了对称的结构,同层报头和有效载荷完全一样,左边如何发送的,右边就是如何收到的,可以认为是在同层协议中直接通信,也可以理解为是向下交付
在路由器部分传递给令牌环驱动程序时,加上的报头就是令牌环协议(重新封装),这样就跟左边不一样了,但是不影响上面的对称性
IP 层的作用:屏蔽底层网络的差异
(1)不同的协议层对数据包有不同的称谓
- 在传输层叫做数据段(segment)
- 在网络层叫做数据报(datagram)
- 在链路层叫做数据帧(frame)
应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部,称为封装
- 首部信息中包含了一些类似于首部有多长,载荷有多长,上层协议是什么等信息
数据封装成帧之后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,根据首部中的 “上层协议字段” 将数据交给对应的上层协议处理
(2)数据封装的过程
(3)数据分用的过程
六、网络中的地址管理
MAC 地址通常在局域网使用,IP 地址通常在广域网使用
1、IP 地址(Internet Protocol Address)
IP 地址是用于在互联网上唯一标识和定位设备的一组数字
由 32 位二进制数 / 4 个八位二进制数组成的十进制数表示
分为两部分
- 网络地址:用于标识所连接的网络
- 主机地址:用于标识具体的设备
用于在网络上进行数据包的传输和路由选择
每个设备在网络中都有一个唯一的 IP 地址,以便进行通信和数据传输
每台主机都有 IP 地址,没有 IP 地址就无法上网
(1)IP 协议有两个版本
A. IPv4
对于 IPv4 来说,IP 地址是一个 4 字节,32 位的整数
- 它由 4 个八位二进制数组成,通过 “点分十进制” 的字符串表示
用点分割的每一个数字表示一个字节,范围是 0~255
- 这个 IP 地址可能用于一个局域网中的路由器或计算机
B. IPv6
- 是一个 16 字节,128 位的二进制数,并使用冒号分隔
- 主要目的:扩展互联网的地址空间,以支持未来更多的设备连接
在使用 TCP/IP 协议的网络中,IP 及其向上的协议看到的报文都是一样的
将数据从一台主机传递到另一台主机并不是目的,真正通信的其实是应用层上的软件
2、MAC 地址(Media Access Control Address)
MAC 帧地址也称为物理地址或硬件地址,用来识别数据链路层中相连的节点
它是一个用于在局域网中唯一标识网络适配器(比如网卡)的长度为 48 位(即 6 个字节)的二进制数,一般用 16 进制数字加上冒号的形式来表示
每个网络适配器都有一个唯一的 MAC 地址
- 虚拟机中的 MAC 地址不是真实的 MAC 地址,可能会冲突,也有些网卡支持用户配置 MAC 地址
它由厂商在生产时烧录到适配器中,在网卡出厂时就确定了,不能修改
MAC 帧地址用于在局域网中寻找目标设备,它是数据链路层的一部分,用于将数据包从源设备传输到目标设备
在以太网中,MAC 地址是数据包在局域网中传输所必需的信息
(1)作用
唯一标识设备:每个网络设备都有一个唯一的 MAC 地址,用于在局域网中识别和寻址设备
- 因为以太网是一个共享介质的网络,通过 MAC 地址可以准确地将数据包传送到目标设备
确保交付:通过将目标 MAC 地址设置为接收方的 MAC 地址,数据包可以被有针对性地转发到正确的设备,确保数据包的正确交付
IP 地址提供的是方向,而 MAC 地址提供的是可行路径
3、网络数据流同样有大小端之分
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
网络数据流的地址规定:先发出的数据是低地址,后发出的数据是高地址
TCP/IP 协议规定:网络数据流应采用大端字节序,即低地址高字节
- 不管这台主机是大端机还是小端机,都会按照这个 TCP/IP 规定的网络字节序来发送/接收数据。如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可
七、端口号
用于标识在一台设备上运行的不同网络应用程序或服务的数字标识符
当一个应用程序或服务需要通过网络进行通信时,它会打开一个特定的端口,并侦听该端口上的连接,这个侦听过程称为绑定
- 当其他设备或应用程序尝试连接到此端口时,操作系统会将连接转发给已经绑定到该端口的应用程序进程
1、端口号和进程之间存在一对一的映射关系
一个进程可以 bind 多个端口号
- 假设绑定了两个端口号 A 和 B,这两个端口号标识的是同一个进程 ,这与端口号用来标识进程的唯一性并不冲突
一个端口号不可以被多个进程 bind
- 因为端口号的作用就是标识唯一的一个进程,如果绑定一个已经被绑定的端口号就会出现绑定失败的问题
同一台设备上的不同进程可以绑定不同的端口号,这样就使得多个应用程序能够同时进行网络通信,而无需担心冲突
2、端口号存在的原因
- pid 是系统规定的,而 port 是网络规定的,可以把系统和网络进行解耦
- port 标识服务器的唯一性不能做任何改变,要让客户端能找到服务器就不能被改变,而 pid 每次启动进程,pid 都会发生改变
- 不是所有的进程都需要提供网络服务或请求,也就不需要 port,但每个进程都需要 pid
为了更好的表示一台主机上服务进程的唯一性,规定用端口号标识服务进程、客户端进程的唯一性
IP 地址(标识唯一主机)+ 端口号(标识唯一进程)能够标识网络上的某一台主机的某一个进程(全网唯一的进程)
port 有源端口号和目的端口号
- 在发送数据时也要把自己的 IP 和端口号发送过去,因为数据还要被发送回来,所以发送数据时一定会多出一部分数据,以协议的形式呈现
端口号是传输层协议的内容
3、五元组标识一个通信
因为主机上存在不同的服务,从网络中获取到的数据在进行向上交付时,在传输层就会提取出该数据对应的目的端口号,进而确定该数据应该交付给当前主机上的哪一个服务进程,即一台主机上可以同时部署端口号不同的服务
在 TCP/IP 协议中,用源IP,源端口号,目的 IP,目的端口号,协议号这样一个五元组来标识一个通信
(1)通信流程
先提取出数据当中的目的 IP 和目的端口号,确定该数据是发送给当前服务进程的
然后提取出数据当中的协议号,为该数据提供对应类型的服务
最后提取出数据当中的源 IP 地址和源端口号,将其作为响应数据的目的 IP 地址和目的端口号,将响应结果发送给对应的客户端进程
4、端口号范围划分
端口号是一个 16 位的整数,它的取值范围是 0~65535
知名端口号(Well-Know Port Number):0~1023
- 20 和 21:FTP 服务器
- 22:SSH 服务器
- 23:Telnet 服务器
- 25:SMTP 服务器
- 53:DNS 服务器
- 80:HTTP 服务器
- 443 端口:HTTPS 服务器
操作系统动态分配的端口号:1024~65535
- 客户端程序的端口号就是由操作系统从这个范围分配的,允许用户手动绑定
5、netstat:查看网络状态
- n:拒绝显示别名,能显示数字的全部转化成数字
- l:仅列出有在 listen 的服务状态
- p:显示建立相关链接的程序名
- t:仅显示 TCP 相关选项
- u:仅显示 UDP 相关选项
- a:显示所有选项,默认不显示 listen 相关
6、pidof:通过进程名来查看服务器进程的 pid
八、应用层协议
主要目的是为了保证数据安全
1、HTTP(HyperText Transfer Protocol,超文本传输协议)
HTTP 协议具有大量的文本分析和协议处理
(1)URL(Uniform Resource Locator,统一资源定位符)
- 就是平时俗称的 “网址”,在全球范围内,只要找到 URL 就能访问该资源
- 要访问一个服务器,ip 地址和端口号是必须要有的,有 ip 地址就可以找到这台唯一的机器,能够访问到端口号就可以找到提供服务对应端口的进程
- 但一般在请求时,端口号是被省略的,因为在请求网络服务时,对应的端口号都是众所周知的,即客户端知道
使用浏览器访问 URL
- 通过域名找到唯一一台网络主机,而域名后面就是该机器提供服务的进程
- 接着通过资源路径找到想要的文件名
- 可能是图片/文本,把客户想访问的资源路径+客户要的文件名返回给浏览器
(2)urlencode 和 urldecode
- 像 / ? : 等这样的字符被 url 当做特殊意义,所以不能随意出现
- 如果用户想在 url 中包含 url 本身用来作为特殊字符的字符,那么在 url 形式时,浏览器会自动进行编码
- 转义规则:取出字符的 ASCII 码,将其转成十六进制,然后再在前面加上百分号即可
- 比如 "+" 被转成了 "%2B",这个过程就叫做 encode,decode 就是把特殊符号转回去
HTTP 的本质就是通过 HTTP 协议从服务端拿下文件资源
- 因为文件资源的种类特别多,而 HTTP 都能搞定,所以叫做超文本传输协议
(3)HTTP 协议格式
HTTP 是基于请求和响应的应用层服务,底层采用 TCP
作为客户端可以向服务器发起 request,服务器收到这个 request 之后,会对这个 request 做数据分析,得出想要访问的资源,然后服务器再构建 response,完成这一次 HTTP 的请求,返回响应
由于 HTTP 是基于请求和响应的应用层访问,所以必须要知道 HTTP 对应的请求格式和响应格式
- CS 模式
保证请求和响应被应用层完整读取
- HTTP 所有请求字段都是按行为单位的字符串
- 比如对于 HTTP 请求,可以用 while 循环按行读取,直到遇到空行为止,这样就可以保证把请求行和请求报头读完
- 报头的 key: val 结构有一个属性是 Content-Length: XXX,它表示的是正文的长度,所以正文也能被完整读取
- 如果现在想获得 name 的 key 值,需要把数据从字符串中反序列化
- 对于报头部分,请求 / 响应报头不止包含 key: val,后边还有字符串分隔符:key: val\r\n,序列化直接发送就行,但如果想要反序列化可以按照 \r\n 来按行提取,所以 HTTP 报头是用特殊字符进行信息分离
- 对于正文部分,不需要做特殊处理,如果需要的话,可以设计自定义序列化与反序列化方案
A. HTTP 请求
- 首行:[方法] + [url] + [版本]
- Header:请求的属性,冒号分割的键值对,每组属性之间使用 \n 分隔,遇到空行就表示 Header 部分结束
- Body:空行后面的内容都是 Body,Body 允许为空字符串。如果 Body 存在,则在 Header 中会有一个 Content-Length 属性来标识 Body 的长度
B. HTTP 响应
- 首行:[版本号] + [状态码] + [状态码解释]
- Header:请求的属性,冒号分割的键值对,每组属性之间使用 \n 分隔,遇到空行表示 Header 部分结束
- Body:空行后面的内容都是 Body,Body 允许为空字符串。如果 Body 存在,则在 Header 中会有一个 Content-Length 属性来标识 Body 的长度,如果服务器返回了一个 html 页面,那么 html 页面内容就是在 body 中
(4)HTTP 的请求方法
A. GET 方法
- 通过 URL 传递参数,回显到浏览器的域名当中
- GET 方法会回显输入的私密信息,不够私密
B. POST 方法
通过请求正文提交参数
因为 POST 方法是通过正文传参的,所以一般不会回显,用户看不到,私密性更好
私密性不等于安全性,加解密才具有安全性
- 无论是 GET 还是 POST 方法都不安全,因为 HTTP 请求都是可以被抓到的,想要安全必须加密,使用 HTTPS 协议
- 一般情况下,传递大字段或者较为私密的数据时使用 POST 方法,其他的使用 GET 方法
(5)HTTP 的状态码
一般情况下,HTTP 的状态码都要匹配上状态码的描述
A. 常见的状态码
- 200:OK,请求成功
- 302:Redirect,重定向
- 403:Forbidden,禁止
- 404:Not Found,未找到
- 504:Bad Gateway,错误的网关
B. 3xx —— Redirection(重定向状态码)
重定向就是通过各种方法将各种网络请求重新定个方向转到其它位置,此时服务器相当于提供了一个引路的服务
当发送请求给服务端,服务端返回一个新的 URL,状态码是 3,浏览器自动用这个新的 URL 继续发送请求给新的地址,所以重定向是由客户端完成的
重定向又分为临时重定向和永久重定向
- 状态码 301(Moved Permanently)表示的就是永久重定向
- 状态码 302(Found)和 307(Temporary Redirect)表示的是临时重定向
临时重定向和永久重定向的本质是影响客户端的标签,决定客户端是否需要更新目标地址
- 如果某个网站是永久重定向,那么第一次访问该网站时由浏览器进行重定向,但后续再访问该网站时就不需要浏览器再进行重定向了,此时访问的就是重定向后的网站
- 如果某个网站是临时重定向,那么每次访问该网站时如果需要进行重定向,都需要浏览器来完成重定向跳转到目标网站
(6)HTTP 常见的 Header 信息
- Content-Type:数据类型(TEXT / HTML 等)
- Content-Length:Body 长度
- Host:客户端告知服务器,所请求的资源是在哪个主机的哪个端口上
- User-Agent:声明用户的操作系统和浏览器的版本信息
- referer:当前页面是从哪个页面跳转过来的
- location:搭配 3xx 状态码使用,告诉客户端接下来要去哪里访问
- Cookie:用于在客户端存储少量信息,通常用于实现会话的功能
(7)HTTP 会话保持(Cookie & Session)
A. HTTP 的特征
简单快捷
无连接
无状态
- 每次请求并不会记录它历史上请求过什么,HTTP 的每次请求 / 响应之间是没有任何关系的
- 在使用浏览器时发现不是这样的,比如在登录某个网站后,就算把网站关闭甚至重启电脑,当再次打开同一个网站时,并没有要求再次输入账号和密码,这实际上是通过 Cookie 技术实现的,点击浏览器中锁的标志就可以看到对应网站的各种 Cookie 数据
- 用户在第一次输入账号和密码时,浏览器会保存 Cookie,近期再次访问同一个网站,即发送 http 请求,浏览器会自动将用户信息添加到报头中推送给服务器。这样只要用户首次输入密码,一段时间内都不用再做登录操作了
- 这些 cookie 数据实际都是对应的服务器写的,如果将对应的某些 cookie 删除,那么可能就需要重新进行登录认证了,因为删除的可能正好就是登录时所设置的 cookie 信息
B. Cookie
把用户名和密码保存起来的技术叫做 Cookie 技术
Cookie 就是在浏览器中的一个小文件,文件里记录的就是用户的私有信息
a. 分类
内存级别的 Cookie 文件
- 将浏览器关掉后再打开,访问之前登录过的网站,如果需要重新输入账号和密码,说明之前登录时浏览器中保存的 Cookie 信息是内存级别的
文件级别的 Cookie 文件
- 将浏览器关掉甚至将电脑重启再打开,访问之前登录过的网站,如果不需要重新输入账户和密码,说明之前登录时浏览器当中保存的 Cookie 信息是文件级别的,是真实的文件,保存在磁盘,进程退出也不影响
b. 安全问题
本地的 Cookie 如果被不法分子拿到了,那么此时这个非法用户就可以利用 Cookie 信息,冒充身份去访问我们曾经访问过的网站,将这种现象称为 Cookie 被盗取了
为了保证安全,可以把信息保存在服务端,在服务端形成一个文件:Session 文件
- 因为有很多 Session 文件,所以给每个文件一个名字:Session ID,并将其返回给浏览器,浏览器存的 Cookie 其实是 Session id
- 接下来把 Session ID 放到请求中,然后发送到服务端,在服务端获取登录信息
- 接下来只能靠服务端的安全策略保障安全,例如账号被异地登录了,服务端察觉后只要让 session id 失效即可,这样异地登录就会让用户重新验证账号密码,一定程度上保障了信息的安全
写入 Cookie 信息
- 向发送给浏览器的响应中写入报头中
会话保持不是 HTTP 协议天然具备的特点,而是浏览器为了满足用户的使用需求,做了相应的工作
(8)HTTP 长连接
HTTP 请求是基于 TCP 协议的,而 TCP 是需要进行连接的。对于一个完整的网页来说,可能包含多种元素资源,就需要发起多次 Connect
为了减少连接次数,需要客户端和服务器均支持长连接,在报头信息中会有 Connection 字段,建立一条连接,传输完后不断开连接,一直传递资源,不用频繁创建连接
如果是短连接,那么请求一份资源后就会自动关闭连接
2、HTTPS(Hypertext Transfer Protocol Secure,超文本传输安全协议)
- TLS/SSL:可选的,一般负责加密和解密
- HTTPS 是在 HTTP 协议的基础上引入了⼀个加密层
- 发送和接收必须用同一种方式(HTTP / HTTPS),区分就用端口号
HTTP 协议内容都是按照文本的方式明文传输的,这就导致了在传输过程中出现⼀些被篡改的情况
HTTPS 由于经过加密层,所以在网络中是密文发送,在应用层是明文的,保证了数据在网络中的安全
(1)加密和解密
在加密和解密的过程中,往往需要⼀个 / 多个中间的数据来辅助进行这个过程,这样的数据称为密钥
安全:破解的成本远远大于破解的收益
因为 HTTP 的内容是明文传输的,明文数据会经过路由器、WiFi 热点、通信服务运营商、代理服务器等多个物理节点,如果信息在传输过程中被劫持,那么传输内容就完全暴露了
劫持者可以篡改传输的信息且不被双方察觉,这就是中间人攻击,所以才需要对信息进行加密
不止运营商可以劫持,其他的黑客也可以用类似的手段进行劫持,以此来窃取用户的隐私信息或者篡改内容
HTTPS 就是在 HTTP 的基础上进行了加密,进一步的来保证用户的信息安全
A. 常见的加密方式
a. 对称加密(单密钥加密)
采用单钥密码系统的加密方法,同⼀个密钥可以同时用作信息的加密和解密
特征
- 加密和解密所用的密钥是相同的
常见对称加密算法
- DES
- 3DES
- AES
- TDEA
- Blowfish
- RC2
特点
- 算法公开
- 计算量小
- 加密速度快
- 加密效率高
按位异或就是一个简单的对称加密
- 对于字符串的对称加密也是同理,每一个字符都可以表示一个数字
b. 非对称加密
需要两个密钥来进行加密和解密:公钥和私钥
- 通过公钥对明文加密变成密文,通过私钥对密文解密变成明文
- 也可以反着用,通过私钥对明文加密变成密文,通过公钥对密文解密变成明文
特征
- 公钥和私钥是配对的
常见非对称加密算法
- RSA
- DSA
- ECDSA
特点
- 算法强度复杂,使得加密解密速度没有对称加密解密的速度快
- 举例:A 要给 B ⼀些重要的文件,但是 B 可能不在,于是 A 和 B 提前做出约定:B 说:“我桌子上有个盒子,然后我给你⼀把锁,你把文件放盒子里用锁锁上,然后我回头拿着钥匙来开锁取文件。”
- 这把锁就相当于公钥
- 钥匙就是私钥
- 公钥给谁都行,不怕泄露,但是私钥只有 B 自己持有,持有私钥的人才能解密
- 举例:A 要给 B ⼀些重要的文件,但是 B 可能不在,于是 A 和 B 提前做出约定:B 说:“我桌子上有个盒子,然后我给你⼀把锁,你把文件放盒子里用锁锁上,然后我回头拿着钥匙来开锁取文件。”
- 安全性依赖于算法与密钥
- 加密的安全性
- 不存在不可被破解的加密
- 可以从算力成本角度来分析:比如加密的成本是 100 块,而解密的花费却要 100 亿,这种就可以称为是安全的
B. 数据摘要(数据指纹)
现在有一篇很长的文章,可以通过哈希函数把这篇文章处理成一个固定长度的字符串,现在就修改了一个标点符号,字符串也会变化。把这个固定长度的字符串就叫做 hash 摘要,而这个过程就叫做数据摘要
a. 基本原理
- 利用单向散列函数(Hash 函数)对信息进行运算,生成⼀串固定长度的数字摘要
- 任意的文本经过 Hash 形成的摘要都是不一样的
- 数字指纹并不是⼀种加密机制,但可以用来判断数据有没有被篡改
b. 摘要常见算法
MD5
- 定长:无论多长的字符串,计算出来的 MD5 值都是固定长度
- 分散:源字符串只要改变⼀点,最终得到的 MD5 值都会差别很大
- 不可逆:通过源字符串⽣成 MD5 很容易,但通过 MD5 还原成原串理论上是不可能的
SHA 系列
- SHA1
- SHA256
- SHA512
- 算法把无限映射成有限的,因此可能会有碰撞,也就是两个不同的信息,算出的摘要相同,但是概率非常低
c. 摘要的特征
与加密算法的区别:
- 摘要严格意义不是加密,因为没有解密
- 从摘要很难反推出原信息,通常用来进行前后数据对比,观察数据是否被修改过
- 可以用于实现网盘的秒传功能、公司数据库密码存储等
C. 数字签名
对数据摘要再加密就得到数字签名
(2)HTTPS 的工作过程
A. 方案一(只使用对称加密)
如果通信双方都各自持有同⼀个密钥 X,且没有别人知道,这两方的通信安全当然是可以被保证的,除非密钥被破解
服务器同一时刻是给很多客户端提供服务的,每个客户端用的密钥肯定是不同的,所以服务器就需要维护每个客户端和每个密钥之间的关联关系,这也是个很麻烦的事情
理想做法:在客户端和服务器建立连接时,双方协商确定这次的密钥是什么
但如果直接把密钥明文传输,那么黑客也就能获得密钥了,此时后续的加密操作就形同虚设了,所以密钥的传输也必须加密传输,但是要想对密钥进行对称加密,就仍然需要先协商确定一个个 “密钥的密钥”,那此时密钥的传输再用对称加密就行不通了,所以在进行正常加密数据通信之前,首先要解决的是密钥如何被对方安全的收到
B. 方案二(只使用非对称加密)
非对称加密既可以使用公钥加密,也可以使用私钥加密
使用公钥加密必须使用私钥解密,使用私钥加密必须使用公钥解密
即使中间人在通信过程中获取了公钥,但是没有私钥也无法进行解密,由此保证了从客户端发送给服务端数据的安全
如果服务器先把公钥以明文方式传输给浏览器,之后浏览器向服务器传数据前都先用这个公钥加密好再传,只有服务器有相应的私钥能解开公钥加密的数据,但是从客户端到服务器信道其实也是有安全问题的
如果服务器用它的私钥加密数据传给浏览器,那么浏览器用公钥可以解密它,而这个公钥是⼀开始通过明文传输给浏览器的,若这个公钥被中间人劫持到了,那他也能用该公钥解密服务器传来的信息了
C. 方案三(双方都使用非对称加密)
a. 步骤
- 1、服务端拥有公钥 S 与对应的私钥 S',客户端拥有公钥 C 与对应的私钥 C'
- 2、客户和服务端交换公钥
- 3、客户端给服务端发信息:先用 S 对数据加密再发送,只能由服务器解密,因为只有服务器有私钥 S'
- 4、服务端给客户端发信息:先用 C 对数据加密再发送,只能由客户端解密,因为只有客户端有私钥 C'
- 但这样做速度慢、效率低,其次是这样做也会有安全问题
D. 方案四(非对称加密 + 对称加密)
a. 解决效率问题
- 使用非对称加密让双方知道对称密钥,后续再使用对称加密的方式进行通信
- 只有首次是使用非对称加密,后续所有的通信都采用对称加密
- 对称加密的速度快,大大提高了通信速度
- 客户端发起 HTTPS 请求,获取服务端公钥 S
- 客户端在本地生成对称密钥 C,通过公钥 S 加密,发送给服务器
- 由于中间的网络设备没有私钥,即使截获了数据也无法还原出内部的原文,也就无法获取到对称密钥
- 服务器通过私钥 S' 解密还原出客户端发送的对称密钥 C,并且使用这个对称密钥加密给客户端返回的响应数据
- 后续客户端和服务器的通信都只用对称加密即可,由于该密钥只有客户端和服务器两个主机知道,其他设备不知道密钥,所以即使截获数据也没有意义
- 由于对称加密的效率比非对称加密高很多,因此只是在开始阶段协商密钥时使用非对称加密,后续的传输仍然使用对称加密,但依旧有安全问题
E. 中间人攻击方式(Man-in-the-MiddleAttack,“MITM 攻击”)
在方案二、三、四中,客户端获取到公钥 S 之后,对客户端形成的对称密钥 C,用服务端给客户端的公钥 S 进行加密,即使中间人窃取到了数据,但此时中间人确实无法解出客户端形成的密钥 C,因为只有服务器有私钥 S'
只要已经交换了密钥,中间人就来迟了,但中间人如果在最开始时就可以进行篡改替换
在方案四中,如果中间人的攻击是在最开始握手协商的时候进行,就不⼀定了(同样适用于方案二、三)
- 服务器具有非对称加密算法的公钥 S、私钥 S'
- 中间人具有非对称加密算法的公钥 M、私钥 M'
- 客户端向服务器发起请求,服务器明文传送公钥 S 给客户端
- 中间人劫持数据报文,提取公钥 S 并保存好,然后将被劫持报文中的公钥 S 替换成为自己的公钥 M,并将伪造报文发给客户端
- 客户端收到报文,提取公钥 M,此时客户端不知道公钥被更换过了,自己形成对称密钥 C,用公钥 M 加密 C,形成报文发送给服务器
- 中间人劫持后,直接用自己的私钥 M' 进行解密,得到通信密钥 C,再用曾经保存的服务端公钥 S 加密后将报文推送给服务器
- 服务器拿到报文,用自己的私钥 S' 解密,得到通信密钥 C
- 双方开始采用 C 进行对称加密进行通信,但这⼀切都在中间人的掌握中,劫持数据、进行窃听甚至修改都是可以的
a. 中间人攻击能够成功的本质
- 中间人能够对数据做篡改且客户端无法确定收到的公钥是合法的,也无法确定其含有公钥的数据报文就是目标服务器发送过来的
- 中间人得到了 C,利用 C 先解密再加密后发送给服务端,那么即使修改了数据客户端和服务端也不知道中间人的存在
- 该场景的本质问题是服务器在返回公钥时,被中间人截取并替换了公钥,并且客户端没有能力辨别公钥是否合法,所以需要客户端具有判别公钥是否合法的能力
F. 数字证书
为了解决前面的问题,Client 需要对服务器的合法性进行认证
a. CA 认证
服务端在使用 HTTPS 前,需要向 CA 机构(权威机构)申领⼀份数字证书(CA 证书),数字证书里含有证书申请者信息、公钥信息等
服务器把证书传输给浏览器,浏览器从证书里获取公钥即可,证书就如同身份证,证明服务端公钥的权威性, 是服务端公钥的身份证明
这个证书可以理解成是⼀个结构化的字符串,只有证书是合法时才会进行非对称加密, 里面包含的信息有:
- 证书发布机构
- 证书有效期
- 公钥
- 证书所有者
- 签名
b. 数据签名
- 签名的形成是基于非对称加密算法的,数据签名的本质是防止被篡改
- 暂时和 HTTPS 没有关系,不要和 HTTPS 中的公钥和私钥搞混了
签名的过程:
- 假设现在有了明文信息,把这个数据进行摘要+形成数据摘要,然后把数据摘要用签名者的私钥,比如 CA 机构的私钥进行加密形成签名,然后再把签名和明文信息放在一起形成数字签名的数据,比如证书
CA 证书的申请和验证:
当服务端申请 CA 证书时,CA 机构会对该服务端进行审核,并专门为该网站形成数字签名
1、生成证书
- 1)CA 机构拥有非对称加密的私钥 A 和公钥 A'
- 2)CA 机构对服务端申请的证书明文数据进行 hash 摘要,形成数据摘要
- 3)CA 机构用 CA 私钥 A' 加密数据摘要,得到数字签名 S
- 4)CA 机构把明文数据和签名结合起来形成证书
- 服务端申请的证书明文和数字签名 S 共同组成了数字证书,这样⼀份数字证书就可以颁发给服务端了
因为使用的是 CA 形成的数据签名,所以只有 CA 能形成可信任的证书,此时服务器会把证书响应给客户端,证书里面包含了公钥
2、验证证书合法性(公钥的合法性)
- 1)判断证书的有效期是否过期
- 2)判定证书的发布机构是否受信任
- 操作系统中已内置的受信任的证书发布机构
- 3)把数据签名和明文信息分开
- 4)对明文信息进行相同的 hash 摘要,形成数据摘要
- 5)用 CA 的公钥把用私钥加密过的数据签名解密,得到数据摘要
- 6)把两个数据摘要进行对比,散列值相等就说明内容没有被篡改,不相等说明有人篡改了签名或者数据
因为浏览器里内置了 CA 的私钥,那么替换的证书必须是一个真正的证书,因为假证书没办法解密,而证书里面的域名信息是唯一的,所以中间人做不到整体替换
G. 方案五(非对称加密 + 对称加密 + 证书认证)
非对称加密 + 对称加密保证了通信的安全,数字证书保证了通信之前交换密钥的安全
在客户端和服务器刚建立连接时,服务器给客户端返回⼀个证书,该证书包含了之前服务端的公钥,也包含了网站的身份信息,由此可以验证公钥的合法性
当客户端获取到这个证书后,会对证书进行校验,防止证书是伪造的
因为中间人没有 CA 私钥,所以无法制作假的证书,所以中间人只能向 CA 申请真证书,然后用自己申请的证书进行掉包,虽然这个确实能够做到证书的整体掉包,但是证书明文中包含了域名等服务端认证信息,如果整体掉包,客户端依旧能够识别出来
被传输的哈希值不能传输明文,需要传输密文,所以对证书明文("hello")hash 形成散列摘要,然后 CA 使用自己的私钥加密形成签名,将 hello 和加密的签名合起来形成 CA 证书颁发给服务端,当客户端请求时就发送给客户端,中间人截获了,因为没有 CA 私钥,就无法更改或者整体掉包,就能安全的证明证书的合法性。最后,客户端通过操作系统里已经存好的证书发布机构的公钥进行解密,还原出原始的哈希值,再进行校验
不选择直接加密,而是要先 hash 形成摘要是因为可以缩小签名密文的长度,加快数字签名的验证签名的运算速度
H. 完整流程
左客户端,右服务器:
I. HTTPS 整个工作过程中涉及的密钥有三组
第⼀组(非对称加密):用于校验证书是否被篡改。服务器持有私钥,私钥在形成 CSR 文件与申请证书时获得,客户端持有公钥。服务器在客户端请求时,返回携带签名的证书。客户端通过这个公钥进行证书验证,保证证书的合法性,进⼀步保证证书中携带的服务端公钥的权威性
- 操作系统包含了可信任的 CA 认证机构,同时持有对应的公钥
第⼆组(非对称加密):用于协商生成对称加密的密钥。客户端用收到的 CA 证书中的公钥,它是可被信任的,给随机生成的对称加密的密钥加密,传输给服务器,服务器通过私钥解密获取到对称加密密钥
第三组(对称加密):客户端和服务器后续传输的数据都是通过这个对称密钥加密解密
- ⼀切的关键都是围绕着这个对称加密的密钥,其他的机制都是辅助这个密钥工作的
- 第⼆组非对称加密的密钥是为了让客户端把这个对称密钥传给服务器,第⼀组非对称加密的密钥是为了让客户端拿到第⼆组非对称加密的公钥
九、传输层协议
负责数据能够从发送端传输接收端,保证数据能够可靠地传送到目标地址
1、TCP(Transmission Control Protocol,传输控制协议)
(1)特点
传输层协议
有连接
- 在正式通信前要先建立连接
可靠传输
- 在内部做可靠传输工作
面向字节流
(2)基于 TCP 协议的客户端/服务器程序的一般流程
A. 服务器初始化
- 调用 socket,创建文件描述符
- 调用 bind,将当前的文件描述符和 ip / port 绑定在一起,如果这个端口已经被其他进程占用了,就会 bind 失败
- 调用 listen,声明当前文件描述符作为一个服务器的文件描述符,为后面的 accept 做好准备
- 调用 accecpt 并阻塞,等待客户端连接过来
B. 建立连接的过程(三次握手)
- 调用 socket,创建文件描述符
- 调用 connect,向服务器发起连接请求
- connect 会发出 SYN 段并阻塞等待服务器应答(第一次)
- 处于 LISTEN 状态的服务器收到客户端的 SYN,将该连接放入内核等待队列中,紧接着应答一个 SYN-ACK 段表示 “同意建立连接”,此时服务器向客户端发送的报文当中的 SYN 位和 ACK 位均被设置为 1,状态变为 SYN_RCVD(第二次)
- 客户端收到 SYN-ACK 后会从 connect() 返回,同时应答一个 ACK 段,此时客户端的连接已经建立,状态变为 ESTABLISHED(第三次)
- 服务器收到客户端发来的最后一次握手后,连接也建立成功,此时服务器的状态也变成 ESTABLISHED
- TCP 是面向连接的通信协议,在通信之前需要进行三次握手来进行连接的建立
- 数据传输的过程
- 建立连接后,TCP 协议提供全双工的通信服务
- 服务器从 accept() 返回后立刻调用 read(),读 socket 就像读管道一样,如果没有数据到达就阻塞等待
- 此时客户端调用 write() 发送请求给服务器,服务器收到后从 read() 返回,对客户端的请求进行处理,在此期间客户端调用 read() 阻塞等待服务器的应答
- 服务器调用 write() 将处理结果发回给客户端,再次调用 read() 阻塞等待下一条请求
- 客户端收到后从 read() 返回,发送下一条请求,如此循环下去
C. 断开连接的过程(四次挥手)
如果客户端没有更多的请求了,为了与服务器断开连接,就调用 close() 关闭连接,客户端会向服务器发送 FIN 段,此时客户端的状态变为 FIN_WAIT_1(第一次)
服务器收到 FIN 后,会回应一个 ACK,此时服务器的状态变为 CLOSE_WAIT,同时 read 会返回 0(第二次)
read 返回之后,服务器就知道客户端关闭了连接,当服务器没有数据需要发送给客户端时,也调用 close 关闭连接,此时服务器会向客户端发送一个 FIN,等待最后一个 ACK 到来,此时服务器的状态变为 LASE_ACK(第三次)
客户端收到 FIN,再返回一个 ACK 给服务器,此时客户端进入 TIME_WAIT 状态。当服务器收到客户端发来的最后一个响应报文时,服务器会彻底关闭连接,变为 CLOSED 状态(第四次)
因为 TCP 是基于确定应答来保证单项可靠性的,如果对方给我发消息,我也给对方进行应答,那么就能够保证双向的可靠性,所以发出去断开连接的过程需要应答
当客户端断开连接时,要保证客户端到服务的连接被成功关闭,所以需要调用一次,而服务端除了要释放自身创建好的文件描述符,也要关闭从服务端到客户端对应的连接,因为双方都要调用 close() 各自两次,那么一来一来就绪各自需要两次挥手,加起来就是四次挥手。
(3)因为 TCP 是面向字节流的,所以要明确报文与报文的分界
保证报文读取完整性的方法
- 定长:规定长度,每次就读取这么多
- 特殊字符:添加特殊字符
- 自描述方式: 比如在报文前面带上四个字节的字段,标识报文长度
创建一个 TCP 的 socket,同时在内核中创建一个发送缓冲区和一个接收缓冲区
调用 write 时,数据会先写入发送缓冲区中
- 如果发送的字节数太长,会被拆分成多个 TCP 的数据包发出
- 如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了或者其他合适的时机发送出去
接收数据时,数据是从网卡驱动程序到达内核的接收缓冲区,然后应用程序可以调用 read 从接收缓冲区拿数据
UDP 不是面向字节流的,发 1 次必须就要读 1 次,发 10 次就必须读 10 次,这种报文和报文在传输层有明显边界的的协议就叫做面向数据报
(4)协议段格式
TCP 报头是一个结构化对象,也是内核创建一块内存,后边就拷贝有效载荷,前面就强转成结构化数据然后填写每个字段
A. 32 位序号 / 确认号:分别代表 TCP 报文当中每个字节数据的编号以及对对方的确认
在一台机器内,IO 总线和系统总线的长度很短,所以传输数据发生错误的概率很小,但是如果要通信的两个机器相隔很远,即通过网络,那么传输数据丢包的概率也会大大增加,所以网络传输的不可靠问题本质就是距离变长了
- 连接内存和外设之间的 “线” 叫做 IO 总线
- 连接内存和 CPU 之间的 “线” 叫做系统总线
TCP 保证可靠性的机制之一就是确认应答机制,只要一个报文收到了对应的应答,就能保证发出去的数据被对方收到了
双方进行通信时除了正常的数据段,可能还会包含确认数据段
- 双方通信使用串行方式,也就是只有收到了确认应答才会继续发送数据,但这样的效率是非常低的
实际工作中并不会使用串行方式,而是一方同时发送多条数据段,只要保证所有数据段都有应答即可
这些数据段到达对面的顺序不一定就是发送的顺序
任何一方都会收到报文,报文中会携带序号,根据报文收到的多个序号进行排序,报文也就可以按序到达
TCP 将发送出去的每个数据段都进行了编号,这个编号叫作序列号,以此保证了传递数据段的有序性
- 每个 ACK 都带有对应的确认序列号,意思是告诉发送者已经收到了哪些数据,下一次从哪里开始发起
- 假设现在要分 4 次发送 4000 字节的数据,也就需要发送 4 个 TCP 报文,此时这 4 个 TCP 报文中的 32 位序号填的就是发送数据中首个字节的序列号,因此分别填的是 1、1001、2001 和 3001,当主机 B 接收到这 4 个 TCP 报文时,就可以利用这 4 个报头中的序号字段进行排序
TCP 报头中的 32 位确认序号是告诉对端当前已经收到了哪些数据,下一次应该从哪里开始发起
- 客户端发送的数据段的序号是 1,报文中含有 1000 字节的数据,如果服务端收到了,那么就会把返回给客户端的响应报头中的 32 位确认序号填写成 1001,那么这个 1001 就有两层含义
- 告诉主机 A,序列号在 1001 之前的字节数据已经收到了
- 告诉主机 A,下次发送数据时应该从序列号为 1001 的字节数据开始发送
要有两组序号是因为任何通信的一方的工作方式都是 TCP 全双工的,双方可能同时要给对方发送消息,也就是说在发送确认时可能携带新的数据。双方发出的报文中不仅需要填充 32 位序号来表明自己当前发送数据的序号,还需要填充 32 位确认序号,对对方上一次发送的数据进行确认,告诉对方下一次应该从哪一字节序号开始进行发送
B. 4 位 TCP 报头长度
TCP 头部有多少个 32 位 bit,即有多少个 4 字节,所以 TCP 头部最大长度是 4*15=60
因为报头最少为 20 字节,所以规定 TCP 报头当中的 4 位首部长度描述的基本单位是 4 字节,这样取值范围就是 0~60
整个报头的大小范围是 20~60,那么报头中选项字段的长度最多是 40 字节
- 4 位首部长度取值范围是 5~15,转换成二进制就是 0101~1111,对应的字节范围是 20~60 字节
C. 6 位标志位:TCP 报头中暂时未使用的 6 个比特位
TCP 的报文也是有类型的,比方说正常通信的常规报文,建立连接时发送的报文,断开连接发送的报文,确认报文,其它类型的报文等
- 针对不同类型的报文有对应的动作,如果收到的是正常通信的报文,就需要把数据放到缓冲区中,如果收到的是建立连接的报文,就要进行三次握手
- 6 个标志位就是为了区分不同报文的类型
URG:紧急指针是否有效
- 紧急标志位,报文当中 URG 被设置为 1,是告诉对方这个数据是要特殊尽快处理
- 因为 TCP 是可靠传输,具有按序到达机制,所以数据段一定是有序的被接收方收到,但是如果有数据段想要插队就可以设置 URG
- URG 一般用来发送带外数据,它不用走 TCP 流,因为接收方直接处理
- 比方说现在发了很多数据,对方正在处理,但突然发现不需要这些数据了,此时就可以发送紧急带外数据,把套接字关了
ACK:确认号是否有效
- 确认应答标志位,凡是该报文具有应答特征,那么其中的 ACK 都会被设置为 1,除了第一个请求报文没有设置 ACK 以外,大部分的网络报文 ACK 都被设置为 1,表明该报文可以对收到的报文进行确认
PSH:提示接收端应用程序立刻从 TCP 缓冲区把数据读走
- push,报文当中的 PSH 被设置为 1,是在告诉对方上层尽快去取走数据
- 因为接收方的窗口值可能比较小,发送方就需要阻塞等待接收方取走缓冲区的数据后才能发送,此时就可以用 PSH 标志位来催促
RST:对方要求重新建立连接,把携带 RST 标识的称为复位报文段
- reset,报文当中的 RST 被设置为 1,表示需要让对方重新建立连接
- 在通信双方连接未建立好的情况下,一方向另一方发数据,此时另一方发送的响应报文当中的 RST 标志位就会被置 1
- 还有可能是服务端网线被拔了,连接被断开了,但客户端不知道,他还会发消息,这时服务端就会把 RST 设置为 1,让客户端建立一个新连接
SYN:请求建立连接,把携带 SYN 标识的称为同步报文段
- 报文当中的 SYN 被设置为 1,表明该报文是一个连接建立的请求报文
- 只有在连接建立阶段,SYN 才被设置,正常通信时 SYN 不会被设置
FIN:通知对方本端要关闭了,称携带 FIN 标识的为结束报文段
- 报文当中的 FIN 被设置为 1,表明该报文是一个断开连接的请求报文
- 只有在断开连接阶段,FIN 才被设置,正常通信时 FIN 不会被设置
D. 16 位窗口大小:保证 TCP 可靠性机制和效率提升机制的重要字段
从本质上来说,TCP 通信是双方在进行通信时,发送方的发送缓冲区和接收方的接收缓冲区,以及接收方的发送缓冲区和发送方的接收缓冲区,两个缓冲区之间来回拷贝
- 上层调用 write/send 并不是把数据直接 write/send 到网络中,而是把应用层中的数据拷贝到发送缓冲区
- 上层调用 read/recv,实际上是把数据拷贝到接收缓冲区
如果发送数据过快,会导致接收缓冲区被打满,剩下的报文都会被丢弃掉;如果发送数据过慢,会影响到上层的业务处理
TCP 支持根据接收端的接收数据的能力来决定发送端发送数据的速度,所以接收方要给发送方同步自己的接收能力,也就是接收缓冲区剩余空间的大小,这种策略称为 “流量控制”(Flow Control)
- 接收端处理数据的速度是有限的
- 如果发送端发的太快,导致接收端的缓冲区被打满,此时如果发送端继续发送就会造成丢包,继而引起丢包重传等一系列连锁反应
通过 16 位窗口的字段填写发送方的剩余缓冲区的大小,那么接收方知道了以后就会调整发送速度
- 窗口大小字段越大说明接收端接收数据的能力越强,此时发送端可以提高发送数据的速度,说明网络的吞吐量越高
- 窗口大小字段越小说明接收端接收数据的能力越弱,此时发送端可以减小发送数据的速度
- 如果窗口大小的值为 0,说明接收端接收缓冲区已经被打满了,此时发送端就不应该再发送数据了
- 但是需要定期发送一个窗口探测数据段,发送端会通过 2 种方式来得知何时可以继续发送数据
- 等待告知
- 主动询问
- 但是需要定期发送一个窗口探测数据段,发送端会通过 2 种方式来得知何时可以继续发送数据
因为窗口有 16 位,所以窗口最大的内存为 64K,如果数据量太大可以用选项字段的一些选项把窗口扩大
- TCP 窗口最大是 65535 字节
- 实际上 TCP 首部 40 字节选项中还包含了一个窗口扩大因子 M,实际窗口大小是窗口字段的值左移 M 位
- 但窗口大小的调整还跟对方接收缓冲区的大小有关,如果对扩大因子进行了调整,那么可能就需要重新编译源代码的内核或者对 TCP 进行重新配置,需要更改操作系统的接收缓冲区的大小
- 在进行流量控制时,发送方在第一次发送数据时就能够得知对方的接收能力
- 是通过交换报文实现的,但第一次发送数据不等同于第一次交换报文
- 在通信之前已经三次握手了,即第一次交换报文完成了,TCP 报文就是窗口大小
- 在握手期间,就可以互相交换窗口大小了
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 ”窗口大小” 字段,通过 ACK 端通知发送端
- 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值来通知给发送端,发送端接受到这个窗口之后,就会减慢自己的发送速度
- 滑动窗口
- 在发送数据后没收到应答前,必须要把数据先保存在滑动窗口中,以支持后续可能出现的超时重传
- 把发送缓冲区分成 3 个部分
- 已经发送并且已经收到 ACK 的数据,上层考虑数据时可以直接覆盖掉
- 操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答,只有确认应答过的数据才能从缓冲区删掉
- 本质:发送方可以一次性向对方推送数据的上限
- 滑动窗口发送缓冲区的一部分,通过不断地滑动来重新划分三段区间
- 把缓冲区看成一个数组,那么滑动窗口的移动就是更新下标
- 不管如何滑动,都要保证对方能够正常接收,即滑动窗口大小 <= 对方的接受能力
- 滑动窗口可能会向右滑动,也可能保持不变,因为数据可能在对方的接收缓冲区中迟迟没有被拿走,那就会导致滑动窗口的左侧不断向右移动,而右侧不动
- 滑动窗口的左端是通过确认序号确定的,右端是通过左端和对方接收缓冲区的剩余空间决定的
当发送端收到对方的应答时,如果应答报文中的确认序号为 ACK_SEQ,收到的应答报文中的滑动窗口大小为 tcp_win,此时就可以将 win_start 更新为 ACK_SEQ,win_end 更新为 win_start + tcp_win
- 滑动窗口一直向右滑动,但不会出现越界问题,TCP 的发送缓冲区被内核组织成了环形结构
- 滑动窗口的侧重点在于提高效率,而可靠为辅,它可以限制缓冲区的范围,能够一次性向对方发送大量的数据
- 如果连续收到三个同样的确认序号,就会触发重传机制,这种机制被称为 “高速重发控制”,也叫 “快重传”,而不像超时重传需要通过设置重传定时器,在固定的时间后才会进行重传
- 快重传和超时重传不是对立的,而是协作的
E. 16 位检验和:由发送端填充,采用 CRC 校验
检验和包含 TCP 首部+数据部分
接收端校验不通过,则认为接收到的数据有问题
F. 16 位紧急指针
标识哪部分数据是紧急数据,即标识紧急数据在报文中的偏移量,需要配合标志字段当中的 URG 字段统一使用
因为紧急指针只有一个,它只能标识数据段中的一个位置,因此紧急数据只能发送 1 个字节
G. 选项字段:TCP 报头当中允许携带额外的选项字段,最多 40 字节
将报头与有效载荷进行分离:先提取 20 字节,再提取其中的 4 位首部长度,再将首部长度*4 表示的就是报头大小
- 如果结果等于 20,说明把报头读完了,否则说明没读完
- 此时再读取前面的结果-20 的长度对应的字节数据,就把选项读完了,也就读完了报头
- 读取完 TCP 的基本报头和选项字段后,剩下的就是有效载荷了
向上交付有效载荷:因为应用层的每个进程都会绑定一个端口号,所以服务端显示绑定一个端口号,客户端由操作系统自动绑定一个端口号。把报头提取出来,而报头里含有目的端口,就可以向上找到对应的协议了
内核中用哈希的方式维护了端口号与进程 ID 之间的映射关系,所以传输层可以通过端口号快速找到其对应的进程 ID,进而找到对应的应用层进程
- 绑定映射关系的时机:bind 端口时
(5)ACK 应答机制
TCP 保证可靠性的机制之一就是确认应答机制
确认应答机制是靠 TCP 报头中的 32 位序号和 32 位确认序号实现的,收到的确认应答说明该序号之前的数据全部被收到了
- 发送方发送数据时报头中所填的序号,实际上就是发送的若干字节数据中,首个字节数据在发送缓冲区当中对应的下标
- 接收方接收到数据进行响应时,响应报头中的确认序号实际上就是接收缓冲区中接收到的最后一个有效数据的下一个位置所对应的下标
- 当发送方收到接收方的响应后,就可以从下标为确认序号的位置继续进行发送
A. 超时重传机制
发送的数据报文丢失了,此时发送端在一定时间内收不到对应的响应报文,就会进行超时重传
对方发来的响应报文丢包了,此时发送端也会因为收不到对应的响应报文而超时重传。但是主机 A 未收到 B 发来的确认应答,也可能是因为 ACK 丢失了,所以主机 B 会收到很多重复数据,因为重复的报文也是不可靠的一种,那么 TCP 协议需要能够识别出哪些包是重复的包,并且把重复的丢弃掉,此时就可以利用前面提到的序列号,就可以很容易做到去重的效果
因为需要超时重传,所以数据发送出去后不会立即清除,而是保留一段时间,直到收到该数据的响应报文后,发送缓冲区中的这部分数据才可以被删除 / 覆盖
数据发送的时间是由网络状况决定的,而网络会因为环境的变化不断变化,所以超时重传的时间一定不是固定的
最理想的情况下,找到一个最小的单元时间,保证确认应答一定能在这个时间内返回,但是这个时间的长短随着网络环境的不同是有差异的
- 如果超时时间设的太长会影响整体的重传效率
- 如果超时时间设的太短可能会频繁发送重复的包
- TCP 为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间
超时以 500ms 为一个单位进行控制,每次判定超时重发的超时时间都是 500ms 的整数倍
- 如果重发一次之后,仍然得不到应答,等待 2*500ms 后再进行重传
- 如果仍然得不到应答,等待 4*500ms 进行重传,依次类推,以指数形式递增
- 当累计到一定的重传次数后,TCP 认为网络或者对端主机出现异常,强制关闭连接
(6)连接管理机制
A. 面向连接
面向连接就是为了保证数据的可靠性
- 通过要连接的两台主机分别在自己的主机上开辟一块区域,然后通过 TCP 协议来共同维护这两块区域
- 根据给定的目标 IP 和端口号发送一些信息到网络中来确认目标主机是否存在,如果不存在则不能完成接下来的网络通信
面向连接是需要先建立连接才能进行网络通信的,建立连接就是确定对方存在并协商好一些控制量来确保接下来的通信是可靠的
面向连接的协议维护了分组之间的状态,使用这种协议的应用程序通常都会进行长期对话
- 记住这些状态,协议就可以提供可靠的传输
- 发送端可以记住哪些数据已经发送出去了但还未被确认,以及数据是什么时候发送的,如果在某段时间间隔内没有收到确认,发送端可以重传数据
- 接收端可以记住已经收到了哪些数据,并将重复的数据丢弃,如果分组不是按序到达的,接收端可以将其保存下来,直到逻辑上先于它的分组到达为止
无连接协议中的分组被称为数据报,每个分组都是独立寻址,并由应用程序发送的
面向连接协议的三个阶段
- 1、在对等实体间建立连接
- 2、数据传输阶段,数据在对等实体间传输
- 3、当对等实体完成数据传输时,连接被拆除
- 使用无连接协议就像寄信,而使用面向连接的协议就像打电话
- 连接的本质就是内核的一种数据结构类型,建立连接成功就是在内存中创建对应的连接对象,再对多个连接对象进行某种数据结构的组织
- 维护连接是有成本的:内存+CPU
- 连接不能直接保证可靠性
- 只要建立了连接,就会有连接结构体,里面包含了超时重传、按序到达、流量控制、拥塞控制等策略以及通信状态和报文属性等
- 连接结构体就是保证数据可靠性的基础,而三次握手是建立连接结构体的基础,所以三次握手间接保证了可靠性
- UDP 不需要通信状态和报文属性等,所以不需要建立连接
B. 三次握手
- 服务端接收的时间一定晚于客户端发送的时间
- 最开始时,客户端和服务器都处于 CLOSED 状态,服务器为了能够接收客户端发来的连接请求,需要由 CLOSED 状态变为 LISTEN 状态
建立连接不是一定成功的,三次握手中的任何一次都有可能出现丢包的情况,前两次握手能够保证被对方收到,因为它们都有应答,就算没有也可以进行超时重传
当客户端发送 ACK 应答的一瞬间,它就会认为三次握手已经建立成功了,此时如果 ACK 应答丢了,就会连接建立失败,但它会重传第二次握手,客户端就会意识到连接没有被建立成功
- 就算客户端已经发送数据,因为只有三次握手成功才能发送消息,所以服务端会返回 RST 报文要求客户端重新建立连接
一次 / 两次握手会导致单机攻击服务器(SYN 洪水)的本质原因
- 客户端还没有建立连接时,服务端已经建立好连接了,所以必须让客户端先建立连接,再让服务端建立连接,有效规避单主机对服务器攻击问题
三次握手是用最小的成本验证全双工通信信道是通畅的
虽然四次握手也可以,但没必要,会降低效率
- 服务端把第二次握手的 SYN 和 ACK 分开发送,处于优化的目的,这两个既然可以合并发送,就没有必要分开分两次来发送
DDoS 攻击(服务拒绝攻击)
- 三次握手并不能解决安全问题,当大量的主机同时发送 TCP 请求也会导致服务端崩溃
- 假设黑客黑掉了很多主机,同时给服务端发送 TCP 连接请求,此时再有客户端发送连接请求,服务端就提供不了服务了,这种攻击手段就是 DDoS 攻击
套接字和三次握手之间的关系
- 在客户端发起连接建立请求之前,服务器需要先进入 LISTEN 状态,此时就需要服务器调用对应 listen 函数设置套接字属性
- 当服务器进入 LISTEN 状态后,客户端就可以向服务器发起三次握手了,此时客户端对应调用的就是 connect 函数
- connect 函数不参与底层的三次握手,connect 的作用只是发起三次握手
- 当 connect 函数返回时,要么是底层已经成功完成了三次握手连接建立成功,要么是底层三次握手失败了
- 如果服务器端与客户端成功完成了三次握手,此时在服务器端就会建立一个连接,但这个连接在内核的等待队列当中,服务器端需要通过调用 accept 函数将这个建立好的连接获取上来,此时双方就可以通过调用 read/recv 函数和 write/send 函数进行数据交互
C. 四次挥手
这里的第二、三次挥手是有可能合并为一次的
客户端最后会等待一个 2MSL 才会进入 CLOSED 状态
- 把从发送方到接收方经过的最大时间叫做 MSL(Maximum Segment Lifetime,报文最大生存时间)
哪边不想给对方发送数据了就要发送断开连接请求
- 假设客户端要断开连接
- 客户端发送断开连接请求,服务端返回 ACK 应答,就已经两次挥手了。服务端也要断开连接发送请求,客户端返回 ACK 应答,一共就是四次挥手
- 客户端发起断开连接请求对应客户端主动调用 close 函数关闭套接字,服务器发起断开连接请求对应服务器主动调用 close 函数关闭套接字
- 一个 close 对应的就是两次挥手,双方都要调用 close,因此就是四次挥手
- 主动断开连接的一方最终状态是 TIME_WAIT
- 四次挥手中的前三次如果发生了丢包,都可以利用超时重传机制
- 如果客户端在发出第四次挥手后立即进入 CLOSED 状态,此时服务器虽然进行了超时重传,但已经得不到客户端的响应了,因为客户端已经将连接关闭了
- 服务器在经过若干次超时重发后得不到响应,最终也一定会将对应的连接关闭,但在服务器不断进行超时重传期间还需要维护这条废弃的连接,这样对服务器是非常不友好的
- 为了避免这种情况,所以客户端在四次挥手后没有立即进入 CLOSED 状态,而是进入 TIME_WAIT 状态进行等待,此时要是第四次挥手的报文丢包了,客户端也能收到服务器重发的报文然后进行响应
- TIME_WAIT 会保证最后一个 ACK 应答尽量被对方收到,而且可能断开之前发送的报文还滞留在网络中,那么 TIME_WAIT 就可以保证双方通信信道上的数据在网络中尽可能的消散
- 如果使用 Ctrl+C 终止 Server,那么 Server 是主动关闭连接的一方,在 TIME_WAIT 状态期间,Server 的端口仍然被视为 “占用”,因此它不能重新 bind 到同样的端口,这是为了防止在网络延迟情况下,旧的连接报文误认为是新的连接请求
- 服务器挂掉无法立即重启的危害
- 服务器需要处理非常大量的客户端的连接,此时如果服务器端主动关闭连接,如果请求量很大就可能导致 TIME_WAIT 的连接数很多,每个连接都会占用一个通信五元组,其中服务器的 IP 和端口和协议是固定的。如果新来的客户端连接的 IP 和端口号和 TIME_WAIT 占用的链接重复了,就会出现问题
- 设置套接字复用
- 使用 setsockopt() 设置 socket 描述符的选项 SO_REUSEADDR 为 1,表示允许创建端口号相同但 IP 地址不同的多个 socket 描述符
- 服务器挂掉无法立即重启的危害
- 被动断开连接的一方,两次挥手完成后的状态是 CLOSE_WAIT
- 如果服务器没有主动关闭对应不需要的文件描述符 sockfd,此时在服务器端就会存在大量处于 CLOSE_WAIT 状态的连接,那么每个连接都会占用服务器的资源,最终导致服务器可用资源越来越少
- 主动断开连接的一方最终状态是 TIME_WAIT
这里的不发送数据的数据指的是用户数据,即应用层不发数据了,但并不代表底层没有报文交互
在挥手前客户端和服务器都处于连接建立后的 ESTABLISHED 状态
D. 服务端状态转化
- CLOSED -> LISTEN:服务器端调用 listen 后进入 LISTEN 状态,等待客户端连接
- LISTEN -> SYN_RCVD:一旦监听到连接请求,就将该连接放入内核等待队列中,并向客户端发送 SYN 确认报文
- SYN_RCVD -> ESTABLISHED:服务端一旦收到客户端的确认报文,就进入 ESTABLISHED 状态,可以进行读写数据了
- ESTABLISHED -> CLOSE_WAIT:当客户端主动关闭连接,服务器会收到结束报文段,服务器返回确认报文段并进入 CLOSE_WAIT
- CLOSE_WAIT -> LAST_ACK:进入 CLOSE_WAIT 后说明服务器准备关闭连接,需要处理完之前的数据。当服务器真正调用 close 关闭连接时会向客户端发送 FIN,此时服务器进入 LAST_ACK 状态,等待最后一个 ACK 到来,这个 ACK 是客户端确认收到了 FIN
- LAST_ACK -> CLOSED:服务器收到了对 FIN 的 ACK,彻底关闭连接
E. 客户端状态转化
- CLOSED -> SYN_SENT:客户端调用 connect,发送同步报文段
- SYN_SENT -> ESTABLISHED:connect 调用成功,则进入 ESTABLISHED 状态,开始读写数据
- ESTABLISHED -> FIN_WAIT_1:客户端主动调用 close 时,向服务器发送结束报文段,同时进入 FIN_WAIT_1
- FIN_WAIT_1 -> FIN_WAIT_2:客户端收到服务器对结束报文段的确认,则进入 FIN_WAIT_2,开始等待服务器的结束报文段
- FIN_WAIT_2 -> TIME_WAIT:客户端收到服务器发来的结束报文段,进入 TIME_WAIT,并发出 LAST_ACK
- TIME_WAIT -> CLOSED:客户端要等待一个 2MSL 的时间,才会进入 CLOSED 状态
F. TCP 状态转换汇总
- 较粗的虚线表示服务端的状态变化情况
- 较粗的实线表示客户端的状态变化情况
- CLOSED 是一个假想的起始点,不是真实状态
(7)拥塞控制
虽然 TCP 有滑动窗口,能够高效可靠的发送大量数据,但如果在刚开始阶段就发送大量数据丢包情况,TCP 会考虑是网络拥塞问题,此时重传就没用了,只会加重网络故障问题,应该少发数据甚至不发数据,等待网络状况恢复后双方再慢慢恢复数据的传输速率
网络拥塞时影响的不只是一台主机,而几乎是该网络中的所有主机,此时所有使用 TCP 的主机都会执行拥塞避免算法
TCP 引入慢启动机制:在刚开始通信时先发送少量数据,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据
A. 拥塞窗口
单台主机一次向网络中发送大量数据时,可能会引发网络拥塞的上限值,超过拥塞窗口这个值时就可能引发网络拥塞问题
发送开始时定义为 1,每次接收到一个 ACK 应答就+1,每次发送数据包时,将拥塞窗口和接收端主机反馈的窗口大小作比较,取较小值作为实际发送数据的窗口大小,即滑动窗口的大小
- 滑动窗口大小 = min(拥塞窗口,对方窗口大小[接收能力])
每收到一个 ACK 应答拥塞窗口的值就 +1,此时拥塞窗口的增长速度是以指数级别进行增长的,如果先不考虑对方接收数据的能力,那么滑动窗口的大小就只取决于拥塞窗口的大小,此时拥塞窗口的大小变化为 1 2 4 8... 但指数增长是非常恐怖的,此时就有可能导致网络再次拥塞
- 慢启动只是指刚开始时慢,但是增长速度非常快,为了不增长那么快,因此不能使拥塞窗口单纯的加倍
- 当拥塞窗口的大小超过慢启动的阈值时,就不再按指数的方式增长,而是按线性增长
- 前期慢开始是为了让网络自主恢复,后面快是为了尽快恢复通信
- 当 TCP 开始启动时,慢启动阈值设置为对方窗口大小的最大值
- 在每次超时重发时,慢启动阈值会变成原来的一半,同时拥塞窗口重置回 1,如此循环下去
拥塞控制归根结底是 TCP 想尽可能快的把数据传输给对方,但又要避免给网络造成太大压力的折中方案
少量丢包只是触发超时重传,大量丢包则认为是网络拥塞
当 TCP 通信开始后,网络吞吐量会逐渐上升,随着网络发生拥堵,吞吐量会立刻下降
(8)延迟应答
目的不是保证可靠性,而是留出一点时间让接收缓冲区中的数据尽可能被上层应用层消费掉,此时再进行 ACK 响应时报告的窗口大小就可以更大,从而增大网络吞吐量,提高数据的传输效率,而如果接收数据的主机立刻返回 ACK 应答,此时返回的窗口可能比较小
假设现在接收方缓冲区有很多数据,但应用层大概率会马上把数据拿走,如果等一等再应答就可以返回更大的窗口
不是所有的数据包都可以延迟应答
- 数量限制:每隔 N 个包就应答一次
- 时间限制:超过最大延迟时间就应答一次,这个时间不会导致误超时重传
- 延迟应答具体的数量和超时时间依操作系统不同也有差异,一般 N 取 2,超时时间取 200ms
(9)捎带应答
接收方收到数据要给发送方一个应答,如果刚好接收方也要发送数据,可以直接一起返回
捎带应答最直观的角度实际上是发送数据的效率,此时双方通信时就可以不用再发送单纯的确认报文了
(10)粘包问题
因为 TCP 是面向字节流的,所以需要应用层来分开这些报文,如果处理不好就会出现多读 / 少读而影响后续报文,这种问题就叫做粘包
粘包问题中的 “包” 是指应用层的数据包
- 在 TCP 的协议头中,虽然没有如同 UDP 一样的 “报文长度” 这样的字段,但是有一个序号这样的字段
- 站在传输层的角度,TCP 是一个个报文过来的,按照序号排好序放在缓冲区中
- 站在应用层的角度,看到的只是一串连续的字节数据,那么应用程序就不知道从哪个部分开始到哪个部分是一个完整的应用层数据包
明确两个包之间的边界,解决粘包问题的本质就是要确定报文与报文之间的边界
- 对于定长的包,保证每次都按固定大小读取即可
- 对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置,也可以选择在包和包之间使用明确的分隔符
UDP 的报文与报文之间的边界是明确的,当它收到报文时,去掉报头剩下的就是有效载荷,就能够保证读到的就是完整的报头
- UDP 是把一个个数据交付给应用层,本身就有很明确的数据边界
- 站在应用层的角度,使用 UDP 时,要么收到完整的 UDP 报文,要么不收,不会出现 “半个” 的情况
(11)TCP 异常情况
进程终止
- 连接本身也是文件,而文件描述符是随进程的,进程退出,操作系统就会 close 掉这个文件,所以操作系统会正常四次挥手断开连接,跟自己 close 掉没区别
- 进程终止会释放文件描述符,仍然可以发送 FIN,和正常关闭没有什么区别
机器重启
- 当重启主机时,操作系统会先杀掉所有进程然后再进行关机重启,因此机器重启和进程终止的情况是一样的,此时双方操作系统也会正常完成四次挥手,然后释放对应的连接资源
机器掉电/网络断开
- 当客户端掉线后,服务端在短时间内无法知道客户端掉线了,因此在服务端会维持与客户端建立的连接,但这个连接也不会一直维持,因为 TCP 是有保活策略的
- 正常的一方会不停的询问对方连接是否还存在,发现不在了就直接断开
- 接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了就会进行 reset
- 即使没有写入操作,TCP 自己也内置了一个保活定时器,会定期询问对方是否还在
- 如果对方不在,也会把连接释放
应用层某些协议也有一些检测机制,比如 HTTP 长连接也会定期检测对方的状态
(12)可靠性
- 校验和
- 序列号
- 确认应答
- 超时重发
- 连接管理
- 流量控制
- 拥塞控制
(13)提高性能
- 滑动窗口
- 快速重传
- 延迟应答
- 捎带应答
(14)基于 TCP 应用层协议
- HTTP
- HTTPS
- SSH
- Telnet
- FTP
- SMTP
2、UDP(User Datagram Protocol,用户数据报协议)
(1)特点
UDP 传输的过程类似于寄信
传输层协议
无连接
- 知道对端的 IP 和端口号就能直接进行传输,不需要建立连接
不可靠传输
- 没有确认机制,没有重传机制
- 如果因为网络故障导致丢包或数据包乱序、重复等问题,也不会给应用层返回任何错误信息
- 在网络通信中,现在的主流网络出现丢包的概率并不大,即使出现了丢包的情况,在有些场景下也是可以容忍的
面向数据报
- 不能够灵活的控制读写数据的次数和数量
- 发送了一个报文,要么不读,要么 recvfrom 等到读取完一整个报文再返回
- 应用层交给 UDP 多长的报文,UDP 原样发送,既不会拆分,也不会合并
无论是多线程读还是写,用的 socket 都是一个,socket 代表的就是文件,UDP 的 socket 既能读也能写,所以是全双工的,可以同时进行收发而不受干扰
(2)协议段格式
- 16 位 UDP 长度表示整个数据报(首部+数据)的最大长度
- 如果 UDP 报文的校验和出错,就会直接将报文丢弃
- UDP 采用的是固定报头,报头中只包含 4 个字段,每个字段的长度都是 16 位,总共 8 字节,所以直接提取前 8 个字节就是报头,其他的就是有效载荷
send 数据并不是直接发送到网络里,而是发给了传输层
应用层的每个进程都有绑定端口号,UDP 就是通过报头当中的目的端口号来找到对应的应用层进程,把有效载荷交出去
(3)报文
一种结构化数据对象(位段)
UDP 数据封装过程
- 创建一块内存,计算出有效载荷的起始地址,拷贝有效载荷,强转填写报头部分,最后形成 UDP 报文
UDP 数据交付过程
- 因为是定长报头,所以直接取出目的端口号,把有效载荷向上交付给指定协议
(4)缓冲区
- UDP 没有真正意义上的发送缓冲区
- 因为它没有可靠机制,不需要把数据暂存起来
- 直接调用 sendto 会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作
- UDP 具有接收缓冲区
- 但是这个接收缓冲区不能保证收到的 UDP 报的顺序和发送 UDP 报的顺序一致,如果缓冲区满了,再到达的 UDP 数据就会被丢弃
- 如果 UDP 没有接收缓冲区,那么就要求上层及时将 UDP 获取到的报文读取上去,如果一个报文在 UDP 没有被读取,那么此时 UDP 从底层获取上来的报文数据就会被迫丢弃
(5)UDP 传输的最大长度
- UDP 协议首部中有一个 16 位的最大长度,因此一个 UDP 能传输的数据最大长度是 64K,包含 UDP 报头的大小
- 64K 是一个非常小的数字,如果要传输的数据大于 64K,就需要在应用层进行手动分包,多次发送并在接收端进行手动拼装
(6)基于 UDP 的应用层协议
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议,用于无盘设备启动
- DNS:域名解析协议
可不可靠在这里只是一个中性词,是一个特点,没有绝对的好坏之分
- 可靠性是需要付出大量的编码和数据的处理成本的,往往在维护和编码上都比较复杂
- 不可靠没有成本,使用起来也简单
- 二者要分场景使用
- 不可靠问题场景
- 丢包
- 乱序:网络阻塞
- 校验错误:比特位翻转
- 重复
- 不可靠问题场景
- 双方通信一定会存在最新消息,最新发送消息的一方无法保证发送出去的数据被对方收到,所以没有绝对的可靠性,只有相对的可靠性
- 在网络中不存在 100% 可靠的协议,但在局部上,即发出去的消息有匹配的应答,能做到 100% 可靠
3、TCP / UDP 对比
- TCP 用于可靠传输的情况,应用于文件传输,重要状态更新等场景
- UDP 用于对高速传输和实时性要求较高的通信领域,比如早期的 QQ,视频传输等,另外 UDP 可以用于广播
十、Socket 套接字
把 IP+port 就叫做套接字 socket
套接字是计算机网络编程中用于实现网络通信的一个抽象概念
它提供了一种编程接口,允许不同计算机之间通过网络进行数据传输和通信
- 套接字可以看作是通信的两个端点
- 一个是服务器端的套接字
- 一个是客户端的套接字
通过套接字,服务器端和客户端可以相互发送和接收数据
在网络通信中,套接字使用网络协议,比如 TCP/IP、UDP 等,来完成数据的传输和通信
- 根据所使用的网络协议不同,套接字可以分为 2 种类型
- 流套接字(Stream Socket,也称为面向连接的套接字)
- 基于 TCP 协议,提供可靠的、面向连接的通信
- 使用流套接字时,数据可以按照发送的顺序和完整性进行传输,确保数据的准确性
- 流套接字的通信方式类似于电话通信,需要在通信前先建立连接
- 数据报套接字(Datagram Socket,也称为无连接的套接字)
- 基于 UDP 协议,提供不可靠的、无连接的通信
- 使用数据报套接字时,数据以数据包的形式进行传输,不保证数据的顺序和完整性
- 数据报套接字适用于一次性发送不需要可靠传输的数据
- 流套接字(Stream Socket,也称为面向连接的套接字)
1、常见 API
socket API 是一层抽象的网络编程接口,适用于各种底层网络协议,比如:IPv4、IPv6,以及 UNIX Domain Socket,这些函数都在 sys/socket.h 中
(1)socket
- int socket(int domain, int type, int protocol);
- domain:是一个域,标识了这个套接字的通信类型
- AF_INET:表示网络通信
- AF_UNIX:表示本地通信
- type:套接字提供服务的类型
- SOCK_STREAM:流式套接字(TCP)
- SOCK_DGRAM:数据报套接字(UDP)
- protocol:想使用的协议,默认为 0
- 因为前两个参数就已经决定了是什么协议
- domain:是一个域,标识了这个套接字的通信类型
- 创建 socket 文件描述符(TCP/UDP,客户端+服务器)
- 在通信之前要先把网卡文件打开,函数作用:打开一个文件,把文件和网卡关联起来
(2)bind
- int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 绑定端口号(TCP/UDP,服务器)
- 将参数 sockfd 和 addr 绑定在一起,使 sockfd 这个用于网络通讯的文件描述符监听 addr 所描述的地址和端口号
- 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,服务器需要调用 bind 来绑定一个固定的网络地址和端口号
- struct sockaddr* 是一个通用指针类型,addr 参数实际上可以接受多种协议的 sockaddr 结构体,而它们的长度各不相同,所以需要第三个参数 addrlen 指定结构体的长度
- 要先定义一个 sockaddr_in 结构体填充数据,再传递进去,创建结构体后要先清空数据,进行初始化,可以用 memset,也可以用系统接口:void bzero(void* s, size_t n);
- 填充端口号的时候要注意端口号是两个字节的数据,涉及到大小端问题
- 对于 IP,首先要先转成整数,再解决大小端问题,系统给了直接能解决这两个问题的接口
- in_addr_t inet_addr(const char *cp);
- 把一个点分十进制的字符串转化成整数再进行大小端处理
- in_addr 结构
- s_addr 可以取 INADDR_ANY,实际上就是 0,这样绑定之后再发送到这台主机上所有的数据,只要是访问绑定的端口的,服务器都能收到
- 这样就不会因为绑定一个具体的 IP 而漏掉其他 IP 的信息了,其实就是让服务器在工作的过程中,可以从任意的 IP 中获取数据
- 填充端口号的时候要注意端口号是两个字节的数据,涉及到大小端问题
- 客户端必须绑定 IP 和端口号来表示主机唯一性和进程唯一性,同样也是需要 bind,但不需要显示的 bind,而服务端则需要显示绑定 port
- 因为服务器的端口号是都知道的,不能改变,如果改变了就找不到服务器了
- 客户端只需要有就可以,只用标识唯一性即可
- 操作系统会自动形成端口进行绑定,即在发送数据时自动绑定,所以创建客户端只要创建套接字即可
服务器也不是必须调用 bind(),但如果服务器不调用 bind(),内核会自动给服务器分配监听端口,这样每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦
(3)listen
- int listen(int socket, int backlog);
- backlog
- static const int gbacklog = 10;
- 底层全连接队列的长度 = backlog+1
- 如果上层来不及调用 accept,而且对端还来了大量连接,但并不需要将所有的连接都先建立好。服务器在进行获取连接时,服务器本身要维护一个连接队列,不能没有也不能太长
- 如果全连接队列满时, 就无法继续让当前连接的状态进入 ESTABLISHED 状态了,会出现客户端状态正常, 但是服务器端出现 SYN_RECV 状态,这是因为 Linux 内核协议栈为一个 TCP 连接管理使用 2 个队列
- 半连接队列:生命周期很短,用来保存处于 SYN_SENT 和 SYN_RECV 状态的请求
- 全连接队列:accpetd 队列,用来保存处于 ESTABLISHED 状态,但是应用层没有调用 accept 取走的请求
- 声明 sockfd 处于监听状态,并且最多允许有 backlog 个客户端处于连接等待状态,如果接收到更多的连接请求就忽略,这里设置不会太大
- backlog
- 开始监听 socket(TCP,服务器)
- 因为 TCP 是面向连接的,当正式通信时需要先建立连接,而 UDP 则不需要
- 要把 socket 套接字的状态设置为 LISTEN 状态,只有这样才能一直获取新连接,接收新的连接请求
(4)accept
- int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict addrlen);
- sockfd:监听套接字的文件描述符
- 成员变量中的 _sock 并不是通信用的套接字,而是获取连接的套接字,为了方便观察,一般会把 _sock 换成 _listensock
- fork 后子进程会复制父进程的文件描述符,但子进程并不需要 _listensock 文件描述符,所以最好关闭
- 成员变量中的 _sock 并不是通信用的套接字,而是获取连接的套接字,为了方便观察,一般会把 _sock 换成 _listensock
- addr:输入输出型参数,是一个结构体,用来获取客户端的地址和端口号
- 如果传参为 NULL,表示不关心客户端的地址
- addrlen:输入输出型参数,客户端传过来的结构体大小
- 如果 accept 成功,会返回一个新的套接字描述符,该描述符用于与连接的客户端进行通信
- sockfd:监听套接字的文件描述符
- 接收请求(TCP,服务器)
- accept 不需要参与三次握手,accept 从底层直接获取已经建立好的连接,需要先建立好连接,然后才能 accept 获取对应的连接
- TCP 不能直接发送数据,因为它是面向连接的,所以必须要先建立连接
- 三次握手完成后,服务器调用 accept() 接受连接,如果服务器调用 accept() 时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来
- sockfd 的作用就是把连接从底层获取上来,返回值的作用就是跟客户端通信
- 子进程是来提供服务的,不需要知道监听 socket,子进程只需要知道 accpet 的返回值即可,sockfd 与它无关,尽量让进程关闭掉它所不需要的套接字
(5)connect
- int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 建立连接(TCP,客户端)
- connect 和 bind 的参数形式一致,区别在于 bind 的参数是自己的地址,而 connect 的参数是对方的地址,addr 和 addrlen 填入的是服务端信息
- 在 UDP 通信中,客户端在 sendto 时会自动绑定 IP 和 port,而 TCP 是在 connect 时进行绑定
- connect 是系统调用接口,所以在调用时会自动绑定当前客户端的 ip 和 port,进而可以在后续使用 sockfd 进行通信
(6)读取数据
- ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
- sockfd:从哪个套接字读
- buf:数据放入的缓冲区
- len:缓冲区长度
- flags:读取方式,0 代表阻塞式读取
- src_addr 和 addrlen:输出型参数,返回对应的消息内容是从哪一个客户端发出的
- 第一个是自己定义的结构体
- 第二个是结构体长度
(7)地址转换函数
- 获取 IP 地址:char* inet_ntoa(struct in_addr in);
- 将网络序列转成主机序列,同时把它转换成点分十进制
- 获取端口号:uint16_t ntohs(uint16_t netshort);
- 由网络序列转成主机序列
- 字符串转 in_addr 的函数
- inet_aton
- inet_addr
- inet_pton
- in_addr 转字符串的函数
- inet_ntoa
- inet_ntop
(8)发送数据
- ssize_t sendto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);
- 这里的结构体内部需要自己填充目的 IP 和目的端口号
2、sockaddr 结构
套接字有不少类型,常见的有 3 种
- 原始 socket
- 可以跨过传输层(TCP/IP 协议)访问底层的数据
- 域间 socket
- 只能在本地通信
- 网络 socket
- 主要运用于跨主机之间的通信,也能支持本地通信
两个具体的套接字类型:sockaddr_in 和 sockaddr_un
- 两个不同的通信场景,区分它们就用 16 地址类型协议家族的标识符
- 比如说想用网络通信,虽然参数是 const struct sockaddr *addr,但实际传递进去的却是 sockaddr_in 结构体,注意要强制类型转换。在函数内部全部看成 sockaddr 类型,然后根据前两个字节判断到底是什么通信类型,然后再强转回去
- 好处:程序的通用性,可以接收 IPv4,IPv6 以及 UNIX Domain Socket 各种类型的 sockaddr 结构体指针作为参数
- IPv4 和 IPv6 的地址格式定义在 netinet/in.h 中,IPv4 地址用 sockaddr_in 结构体表示,包括 16 位地址类型,16 位端口号和 32 位 IP 地址
- IPv4、IPv6 地址类型分别定义为常数 AF_INET、AF_INET6
- 只要取得某种 sockaddr 结构体的首地址,不需要知道具体是哪种类型的 sockaddr 结构体,就可以根据地址类型字段确定结构体中的内容
可以把 sockaddr 看成基类,把 sockaddr_in 和 sockaddr_un 看成派生类,构成了多态体系
127.0.0.1 叫做本地环回
client 和 server 发送数据只在本地协议栈中进行数据流动,不会将数据发送到网络中
作用:用来做本地网络服务器代码测试的,如果绑定的 IP 是 127.0.0.1 的话,在应用层发送的消息不会进入物理层,也就不会发送出去
云服务器是虚拟化服务器,不是真实的 IP,所以不能直接绑定公网 IP,而内网 IP 可以绑定,说明这个 IP 是属于这个服务器的,但如果这里不是一个内网的就无法找到
当 IO 完之后要记得关闭文件描述符 sock,否则会导致可用描述符越来越少
对比 UDP 服务器,TCP 服务器多了获取新连接和监听的操作,因为 TCP 是面向字节流的,所以接收和发送数据都是 IO 操作,也就是文件操作
十一、网络层
在复杂的网络环境中确定一个合适的路径
1、IP 协议
(1)TCP 与 IP 的关系
- IP 层的核心作用是定位主机,具有将数据从主机 A 发送到主机 B 的能力,但并不能保证一定能够做到,所以此时就需要 TCP 起作用了,TCP 可以通过超时重传、拥塞控制策略来保证数据能够发送到 B 主机
- TCP 提供的是策略,保证可靠,而 IP 付出的是行动
(2)网络层要解决的问题:将数据从一台主机送到另一台主机,也就是数据的路由
- 主机:配有 IP 地址,但不进行路由控制的设备
- IP = 目标网络+目标主机
- IP 地址是进行路由的根本
- 路由器:配有 IP 地址,又能进行路由控制
- 数据进行网络传输一般都是跨网络的,而路由器就是连接多个网络的硬件设备,因此数据在进行跨网络传输时一定要经过多个路由器
- 节点:主机和路由器的统称
(3)IP 协议格式
- 4 位版本号
- 指定 IP 协议的版本:IPv4/IPv6
- 对于 IPv4 来说,就是 4
- 指定 IP 协议的版本:IPv4/IPv6
- 4 位头部长度
- 表示 IP 报头的长度,以 4 字节为单位
- IP 头部的长度是多少个 32 bit,也就是 length*4 的字节数
- 4bit 表示最大的数字是 15,因此 IP 头部最大长度是 60 字节
- 有效载荷 = 16位总长度-4位首部长度*4
- 8 位服务类型
- 找到最优路径
- 3 位优先权字段,已经弃用
- 4 位 TOS 字段
- 分别表示:最小延时,最大吞吐量,最高可靠性,最小成本,这四者相互冲突只能选择一个
- 对于 ssh/telnet 这样的应用程序,最小延时比较重要
- 对于 ftp 这样的程序,最大吞吐量比较重要
- 分别表示:最小延时,最大吞吐量,最高可靠性,最小成本,这四者相互冲突只能选择一个
- 1 位保留字段,必须置为 0
- 16 位总长度
- IP 报文(IP 报头+有效载荷)的总长度,IP 数据报整体占多少个字节,用于将各个 IP 报文进行分离
- 16 位标识
- 唯一的标识主机发送的报文
- 如果 IP 报文在数据链路层被分片了,那么每一个片里面的这个 ID 都是相同的
- 3 位标志字段
- 第一位保留,即暂时没有规定该字段的意义
- 第二位置为 1 表示禁止分片,这时候如果报文长度超过 MTU,IP 模块就会丢弃报文
- 第三位表示 “更多分片”
- 如果报文没有进行分片,则该字段设置为 0
- 如果报文进行了分片,则除了最后一个分片报文设置为 0 以外,其余分片报文均设置为 1,类似于一个结束标记
- 13 位分片偏移
- 分片相对于原始 IP 报文开始处的偏移,表示当前分片在原数据中的偏移位置,实际偏移的字节数是这个值*8 得到的
- 除了最后一个报文之外,其他报文的长度必须是 8 的整数倍,否则报文就不连续了
- 8 位生存时间
- 数据报到达目的地的最大报文跳数,一般是 64
- 每次经过一个路由 TTL-1,一直减到 0 还没到达,那么该报文就会被自动丢弃
- 这个字段主要是用来防止出现路由循环,报文变成废弃的游离报文的情况
- 8 位协议
- 表示上层协议的类型
- 16 位首部校验和
- 使用 CRC 进行校验,来鉴别数据报的首部是否损坏,但不检验数据部分
- 32 位源 IP 地址和 32 位目的 IP 地址
- 发送数据时不需要指明发送数据的源 IP 地址和源端口号,因为传输层和网络层都是在操作系统内核当中实现的,数据在进行封装时操作系统会自行填充上对应的源 IP 地址和源端口号
- 选项字段
- 不定长,最多 40 字节
2、网段划分
(1)子网划分的原因
- 互联网中的每一个主机都隶属于一个子网, 以方便定位到具体主机,提高查找效率
(2)IP 地址的划分
IP 地址分为两个部分
- 网络号,表征的是不同的区域
- 保证相互连接的两个网段具有不同的标识
- 在不同的查找过程中,不断变大且是收敛的
- 主机号
- 同一网段内,主机之间具有相同的网络号,但是必须有不同的主机号
路由器连接了两个网段,一般主机标识都是 1
不同的子网其实就是把网络号相同的主机放到一起
- 如果在子网中新增一台主机,则这台主机的网络号和这个子网的网络号一致,但是主机号不能和子网中的其他主机重复
一个子网内有很多主机,这些主机是由路由器管理的
当子网中有主机断开网络时需要将其 IP 地址进行回收,便于分配给后续新增的主机使用
DHCP 技术
- DHCP 通常被应用在大型的局域网环境中,其主要作用就是集中地址管理、分配 IP 地址,使网络环境中的主机动态获得 IP 地址、Gateway 地址、DNS 服务器地址等信息,避免手动管理 IP 的不便,并且能够提升地址的使用率
- 是一个基于 UDP 的应用层协议,一般的路由器都带有 DHCP 功能,因此路由器也可以看作一个 DHCP 服务器
- 当连接 WiFi 时,本质就是路由器会动态分配一个 IP 地址给用户,然后就可以基于这个 IP 地址来进行上网
(3)分类划分法
曾经的一种划分网络号和主机号的方案,把所有 IP 地址分为五类
- 当要判断一个 IP 地址是属于哪一类时,只需要遍历 IP 地址的前 5 个比特位
- 第几个比特位最先出现 0 值,那么这个 IP 地址对应就属于 A、B、C、D、E 类地址
随着 Internet 的飞速发展,这种划分方案的局限性很快显现出来,大多数组织都申请 B 类网络地址,导致 B 类地址很快就分配完了,而 A 类却浪费了大量地址
CIDR(Classless Interdomain Routing,无类别域间路由)
- 在分类划分法的基础上出现了一种新的划分方案,引入一个额外的子网掩码(subnet mask)来区分网络号和主机号
- 子网掩码也是一个 32 位的正整数,通常用一串 "0" 来结尾
- 将 IP 地址和子网掩码进行 “按位与” 操作,得到的结果就是网络号
- 目标网络和子网掩码是路由器内置好的
- 网络号和主机号的划分与这个 IP 地址是 A 类、B 类还是 C 类无关
- 此时一个网络就被更细粒度的划分成了一个个更小的子网,通过不断的子网划分,子网中 IP 地址对应的主机号就越来越短,因此子网当中可用 IP 地址的个数也就越来越少,也就避免了 IP 地址被大量浪费的情况
IP 地址和子网掩码还有一种更简洁的表示方法,例如 140.252.20.68/24,表示 IP 地址为 140.252.20.68,子网掩码的高 24 位是 1,也就是 255.255.255.0
(4)特殊的 IP 地址
不是所有的 IP 都会在公网中使用,有些 IP 地址是有特殊作用的
- 将 IP 地址中的主机地址全部设为 0,就成为了网络号,代表这个局域网
- 将 IP 地址中的主机地址全部设为 1,就成为了广播地址,用于给同一个链路中相互连接的所有主机发送数据包
- 127.* 的 IP 地址用于本机环回测试,通常是 127.0.0.1
(5)IP 地址的数量限制
- IP 地址(IPv4)是一个 4 字节 32 位的正整数,那么一共只有 2^32 个 IP 地址,大概是 43 亿左右
- IP 地址并非是按照主机台数来配置的,而是每一个网卡都需要配置一个 / 多个 IP 地址
- CIDR 只是提高了利用率,在一定程度上缓解了 IP 地址不够用的问题,减少了浪费,但是 IP 地址的绝对上限并没有增加,仍然不是很够用
- 解决 IP 地址不足问题
- 动态分配 IP 地址:只给接入网络的设备分配 IP 地址
- 因此同一个 MAC 地址的设备,每次接入互联网中,得到的 IP 地址不一定是相同的
- NAT 技术(Network Address Translation,网络地址转换):能够让不同局域网当中同时存在两个相同的 IP 地址,私有 IP 和公网 IP 做转换
- 不仅能解决 IP 地址不足的问题,还能够有效避免来自网络外部的攻击,隐藏并保护网络内部的计算机
- 如果一个组织内部组建局域网,IP 地址只用于局域网内的通信,而不直接连到 Internet上
- 理论上使用任意 IP 地址都可以,但是 RFC 1918 规定了用于组建局域网的私有 IP 地址
- 10.*,前 8 位是网络号,共 16777216 个地址
- 172.16. 到 172.31.,前 12 位是网络号,共 1048576 个地址
- 192.168.*,前 16 位是网络号,共 65536 个地址
- 包含在这个范围中的都成为私有 IP,其余的则称为公网 IP或全局 IP
- 因为私有 IP 只能在局域网内出现,所以不同的子网内可能有相同的私有 IP,这样就解决了 IP 不足的问题
- 可以通过命令:ifconfig 来查看这台机器的私网 IP
- 理论上使用任意 IP 地址都可以,但是 RFC 1918 规定了用于组建局域网的私有 IP 地址
- IPv6:IPv6 用 16 字节 128 位来表示一个 IP 地址,能够大大缓解 IP 地址不足的问题
- 但 IPv6 并不是 IPv4 的简单升级版,它们是互不相干的两个协议,彼此并不兼容,因此目前 IPv6 还没有普及
- 动态分配 IP 地址:只给接入网络的设备分配 IP 地址
(6)LAN 口 IP (Local Area Network,子网 IP)与 WAN(Wide Area Network)口 IP
- 家用路由器
- 对内:面对自己构建的子网
- 对外:自己本身也是别人构建子网的一个主机
- 路由器是可以构建局域网的,而且一定是横跨两个子网,所以路由器至少会配置两个 IP:LAN 口和 WAN 口
- 对内:LAN 口 IP:表示连接本地网络的端口,局域网,主要与家庭网络中的交换机、集线器或 PC 相连
- 对外:WAN 口 IP:表示连接广域网的端口,自己所在上级子网给自己分配的 IP,一般指互联网
- 同一个局域网中,两台主机可以直接进行通信
路由器 LAN 口连接的主机都从属于当前这个路由器的子网中
不同的路由器,子网 IP 都是一样的,通常是 192.168.1.1,子网内的主机 IP 地址不能重复,但是子网之间的 IP 地址就可以重复
每一个家用路由器又作为运营商路由器的子网中的一个节点,这样的运营商路由器可能会有很多级,最外层的运营商路由器,WAN 口 IP 就是一个公网 IP
如果希望自己实现的服务器程序能够在公网上被访问到,就需要把程序部署在一台具有外网 IP 的服务器上,这样的服务器可以在阿里云/腾讯云上进行购买
路由器天然的会构建局域网 —— 子网
数据刚出来时并不是直接到公网上,而是先交给家用路由器,再交给运营商路由器做内网转发,转发到一定程度后再转发到公网,最后由公网到达目标服务器
从运营商机房拉出的网线插在了家用路由器的 WAN 口上,个人设备是插在家用路由器的 LAN 口上
这两个运营商路由器现在属于一个子网中,通过更改子网掩码可以让他们划分到不同的子网
家里要上网的步骤:
- 首先要有运营商在家附近,且家附近有网络覆盖
- 联系运营商进行光纤入户
- 工作人员上门,调制解配器(猫),无线路由器
- 开户,账号,密码,配置路由器(运营商认证的账号、密码)
- 配置路由器 —— 设置路由器的 WiFi 名称+密码,路由器认证的
- 正常上网,按月 / 年交费
(7)数据包的转发流程(NAT 技术)
路由器要做的任务
- 将报文中的源 IP 替换成路由器的 WAN 口 IP
- 每经过一个运营商的内网路由器,都要做这个工作,但公网路由不需要
自己的主机判断目标 IP 并不在当前局域网,所以会把数据包直接转给家用路由器,家用路由器也发现目标 IP 并不在当前局域网,就交给运营商路由器,运营商路由器直接连在公网,IP 是唯一确定的,所以就找到了目标服务器,把数据包交给目标服务器即可
当数据包到达家用路由器时,路由器会把源 IP 替换成 WAN 口 IP,运营商路由器收到报文后,也会重复该操作
子网内的主机需要和外网进行通信,在转发报文时,路由器不断地将源 IP 首部中的 IP 地址在不同内网、不同层级的网络节点中转发,替换成 WAN 口 IP,最终数据包中的 IP 地址成为一个公网 IP,这种技术称为 NAT
3、路由
(1)数据路由过程
在复杂的网络结构中找出一条通往终点的路线
模拟问路(路由)过程:自己就是数据包,目的地就是目标 IP,问的路人就是路由器,而路人思考的过程就是查路由表
- “一跳” 就是数据链路层中的一个区间,具体在以太网中指从源 MAC 地址到目的 MAC 地址之间的帧传输区间
- 决定将数据交付给下一跳路由器时,下一跳路由器一定和我在同一个局域网
- “一跳一跳” 的过程叫作局域网(子网)转发
- 宏观上,我们的网络本质就是一个个子网构成的
IP 数据包的传输过程中会遇到很多路由器,这些路由器会帮助数据包进行路由转发,每当数据包遇到一个路由器后,对应路由器都会查看该数据的目的 IP 地址,路由器决定这个数据包是能够直接发送给目标主机,还是需要发送给下一个路由器,依次反复,一直到达目标 IP 地址
依靠每个节点内部维护一个路由表,来判定当前这个数据包该发送到哪里
路由器的查找结果可能有 3 种:
- 路由器经过路由表查询后,得知该数据下一跳应该跳到哪一个子网
- 路由器经过路由表查询后,没有发现匹配的子网,此时路由器会将该数据转发给默认路由
- 路由器经过路由表查询后,得知该数据的目标网络就是当前所在的网络,此时路由器会将该数据转给当前网络中对应的主机
(2)路由表
每个路由器内部都会维护一个路由表
A. 查看路由表
- Windows 下可以用命令:route PRINT
- Linux 下可以通过命令:route
- 如果目的 IP 命中了路由表,直接转发即可
- 路由表中的最后一行主要由下一跳地址和发送接口两部分组成,当目的地址与路由表中其它行都不匹配时,就按缺省路由条目规定的接口发送到下一跳地址
B. 查询路由表的过程
- 当 IP 数据包到达路由器时,遍历路由表的每一个条目,拿着目的 IP 依次与路由表中的子网掩码 Genmask 进行 “按位与” 操作,来确定该报文要去的目标网络,对比运算结果与目标网络
- 如果一样,就将该数据包通过对应的发送接口 Iface 发出
- 如果都没有匹配上目标网络,此时路由器就会将这个数据包发送到默认路由,也就是路由表中目标网络地址中的 default
- IP 没有解决设备转发的具体功能,IP 提供的是转发的策略,核心不是转发,而是路径选择
4、分片与组装
在路由器之间传递的确实是 IP 报文,但真正在网线上跑的是 MAC 帧,MAC 帧是数据链路层的协议
(1)最大传输单元 —— MTU
MAC 帧会将 IP 传下来的数据封装成数据帧,然后发送到网络当中
MAC 帧携带的有效载荷的最大长度是有限制的,也就是说 IP 交给 MAC 帧的报文不能超过某个值,也就是链路层一次可以转发到网络的报文大小的限制,这个值是可修改的,一般大小为 1500 字节
IP 不能决定单个报文的大小,在网络中决定报文大小的是 TCP,所以如果 IP 层要传送的数据超过了 1500 字节,那么就需要先在 IP 层对该数据进行分片,然后再将分片后的数据交给下层 MAC 帧进行发送
发送方的 IP 层负责分片,接收方的 IP 层负责封装
分片是通过 IP 协议报头的这三个字段完成的
- 没有被分片的标志:更多分片的标志位是 0,13 位片偏移也为 0
(2)分片流程
开始:更多分片 1,片偏移 0
中间:更多分片 1,片偏移不是 0
- 根据偏移量进行升序排序,结合偏移量+自身大小 = 下一个报文的偏移量扫描整个报文,如果不匹配,中间一定会有丢失,如果成功计算到结尾,就一定收取完整了
结尾:更多分片 0,片偏移不是 0
假设 IP 层要发送 2980 字节大小的数据,如果 IP 报头不含选项字段,那么加上报头就是 3000 字节,超过了最大传输单元,需要切分
- 每个切分下来的都是纯数据,每部分数据都要添加报头
- 可以先切下 1500 字节,含原始的报头,然后还剩下 1500 的纯数据,再切下 1480 的纯数据,加上 20 字节的报头,刚好构成 1500 字节,还剩下 20 字节的纯数据,把这 20 个字节纯数据也加上报头,就是 40 字节
- 最终就分成了三部分,总长度分别为:1500,1500,40
分片之前一定是一个独立的 IP 报文
分片之后,为了支持组装,所以是每一个分片都要有 IP 报头
(3)组装流程
通过 16 位标识来确定这些报文曾经属于一个报文,通过 3 位标志中的更多分片加上 13 位片偏移来确定该报文有没有被切分以及首尾和顺序
先找到分片报文中 13 位片偏移为 0 的分片报文,然后提取出其 IP 报头当中的 16 位总长度字段,通过计算即可得出下一个分片报文所对应的 13 位片偏移,按照此方式依次将各个分片报文拼接起来
直到拼接到一个 “更多分片” 标志位为 0 的分片报文,此时表明分片报文组装完毕
(4)分片的影响(坏处)
分片行为不是主流,在网络通信里,严重推荐不分片
在网络层分片和组装的过程,应用层和传输层并不知道
分片会增加丢包的概率
- 因为一个报文被切成了多个报文,只要有一个报文丢失就会造成拼接失败,因为不知道是哪个报文丢了,TCP 不关心分片
- 因为网络层有校验和,所以如果报文丢弃了,就导致传输层重传,对整个报文进行重传
- 传输层如果是 TCP 就是数据重传,而 UDP 就直接丢包了,所以对 UDP 的影响更大
十二、数据链路层
1、以太网
要理解数据跨网络转发原理就要先理解一个局域网中数据是如何转发的,它就是以太网协议
“以太网” 不是一种具体的网络,而是一种技术标准
- 既包含了数据链路层的内容,又包含了一些物理层的内容
- 比如规定了网络拓扑结构,访问控制方式,传输速率等
以太网中的网线必须使用双绞线,传输速率有 10M,100M,1000M 等
以太网是当前应用最广泛的局域网技术,和以太网并列的还有令牌环网,无线 LAN 等
(1)以太网帧格式
局域网两台主机之间通信必须要封装 MAC 帧
源地址和目的地址是指网卡的硬件地址,也叫 MAC 地址,长度是 48 位,是在网卡出厂时固化的
帧协议类型字段有三种值,分别对应 IP 协议、ARP 协议和 RARP 协议
帧末尾是 CRC 校验码
红色圈出来的部分就是报头,中间则是数据部分,数据部分包含上层的报头+有效载荷(HTTP、TCP、IP 的封装)
- MAC 帧的分离方式就是采用定长报头,直接对前面的 14 个和后边的 4 个进行提取,剩下的就是有效载荷
在 MAC 帧的帧头当中有 2 个字节的类型字段,因此在分离出报头和有效载荷后,根据该字段将有效载荷交付给对应的上层协议即可
(2)MAC 地址
虚拟机中的 MAC 地址不是真实的 MAC 地址,可能会冲突,也有些网卡支持用户配置 MAC 地址
每一台机器都要配一张网卡,每一个网卡都有一个序列号,这个序列号就是该网卡的 MAC 地址,用来识别数据链路层中相连的节点,在全球范围内具有唯一性,其实在局域网内保证唯一性就够了
长度为 48 位,6 个字节,一般用 16 进制数字加上冒号的形式来表示
- 比如 08:00:27:03:fb:19
MAC 地址在网卡出厂时就确定了,不能修改
IP 地址描述的是路途总体的起点和终点,MAC 地址描述的是路途上每一个区间的起点和终点
- IP 是一个大目标,MAC 就是实现大目标的每一个小目标
- 数据在路由过程中,源 IP 地址和目的 IP 地址可以理解成是不会变化的,而数据每进行一跳后其源 MAC 地址和目的 MAC 地址都会变化
(3)局域网转发原理(基于协议)
假设 MAC1 发送数据给 MAC7,那么首先需要封装一个 MAC 帧
每台主机的数据链路层都会收到这个 MAC 帧,然后进行报头和有效载荷的分离,查看目的 IP 地址是 MAC7
- 如果发现不是自己,直接把数据帧丢弃,上层根本就不知道收到了这个数据帧
- 如果发现是自己,就把有效载荷向上交付
处理结束后,MAC7 也会给 MAC1 一个应答,应答的过程和发送类似
在局域网中,网卡有一种混杂模式:不丢弃任何的数据帧,全部向上交付,这就是局域网抓包工具的原理
- 可以看出 HTTPS 数据加密的必要性
A. 数据碰撞
由于以太网中的所有主机共享一个通信信道,所以多台主机同时发送数据,数据之间就可能会产生数据碰撞问题
解决方法:在同一时刻只允许有一台主机发送数据
保证我在发送数据时,别人也发送数据成功的两种方法
- 令牌环:谁拿牌谁就能发消息,类比互斥锁
- 以太网:如果发生了碰撞,就暂时不发数据,发送主机会休息一段时间再尝试发送,时间随机,这种方法叫做主机的碰撞检测和碰撞避免算法
B. 交换机
交换机可以识别到局部性的碰撞,对碰撞的数据不做转发
- 比如交换机左侧发生了碰撞,这并不会影响到 MAC3 给 MAC4 发消息
交换机对正常发送的数据不会做转发
- 比如 MAC1 是给 MAC5 发消息,就没必要让交换机的右侧收到消息,右侧的碰撞概率就减小了
核心作用:划分碰撞域
(4)MTU
A. MTU 对 IP 协议的影响
由于数据链路层 MTU 的限制,如果 IP 层一次发送的字节数超过了 MTU,就需要进行切片
B. MTU 对 UDP 协议的影响
一旦 UDP 携带的数据超过 1472(1500-20-8), 那么就会在网络层分成多个 IP 数据报
多个 IP 数据报丢失任意一个都会引起接收端网络层重组失败,就意味着如果 UDP 数据报在网络层被分片,那么整个数据被丢失的概率就增加了
C. MTU 对于 TCP 协议的影响
数据在路由器转发的过程,路由器也可能进行切分,因为不同网络的 MTU 是不同的
可以把 IP 协议中的不可切分字段置为 1,如果遇到 MTU 较小的,直接舍弃数据重发,接着重新选择路径,就可以选出一条吞吐量大的路径
TCP 需要控制有效载荷数据不能超过某一阈值,这还是受制于 MTU
TCP 单个数据报的最大消息长度,称为 MSS
- TCP 在建立连接的过程中,通信双方会进行 MSS 协商
- 最理想的情况下,MSS 的值正好是在 IP 不会被分片处理的最大长度,但这个长度仍然受制于数据链路层的 MTU
- MAC 帧的有效载荷最大为 MTU,TCP 的有效载荷最大为 MSS
- 由于 TCP 和 IP 常规情况下报头的长度都是 20 字节,所以一般情况下 MSS=MTU-20-20
- MTU 的值一般是 1500 字节,因此 MSS 的值一般就是 1460 字节
- 一般建议 TCP 将发送的数据控制在 1460 字节以内,此时能够降低数据分片的可能性
- 也就解释了为什么滑动窗口范围内会有多个报文段不能直接一起发送,是因为一次不允许发送太大的单个数据段
- 双方在发送 SYN 时会在 TCP 头部写入自己能支持的 MSS 值,然后双方得知对方的 MSS 值之后,选择较小的作为最终 MSS
- MSS 的值就是在 TCP 首部的 40 字节变长选项中,kind=2
MSS 和 MTU 的关系:
2、ARP 协议
ARP 不是一个单纯的数据链路层的协议, 而是一个介于数据链路层和网络层之间的协议
(1)作用
当跨不同子网的两主机 A 和 B 通信,最终数据会送到主机 B 局域网中的路由器 D,D 和 B 属于同一个局域网,那么就得封装 MAC 帧进行通信,但是报文中只含有 B 的 IP 地址,并不知道 B 的 MAC 地址,此时就需要有一个过程让路由器获取主机 B 的 MAC 地址
- ARP 协议建立了主机 IP 地址和 MAC 地址的映射关系,其作用就是根据 IP 地址来获取目标主机的 MAC 地址
- 在网络通讯时,源主机的应用程序知道目的主机的 IP 地址和端口号,却不知道目的主机的硬件地址
- 数据包首先是被网卡接收到再去处理上层协议的,如果接收到的数据包的硬件地址与本机不符,则直接丢弃,因此在通讯前必须获得目的主机的硬件地址
(2)工作流程
- 当路由器收到数据要发送给目标主机时,就会封装 ARP 报文,广播报文,寻找匹配的目标 IP,目标主机收到后会封装一个 ARP 应答,该应答里包含了自己的 MAC 地址
- 此时路由器知道了目标主机的 MAC 地址,才会把数据包封装 MAC 帧进行发送
源主机发出 ARP 请求,并将这个请求广播到本地网段
- 以太网帧首部的硬件地址填 FF:FF:FF:FF:FF:FF 表示广播
目标主机接收到广播的 ARP 请求,发现其中的 IP 地址与本机相符,则发送一个 ARP 应答数据包给源主机,将自己的硬件地址填写在应答包中
每台主机都维护一个 ARP 缓存表,可以用 arp -a 命令查看
(3)ARP 数据报的格式
因为 ARP 里包含了 IP,所以 ARP 协议属于 MAC 帧的上层协议,所以 MAC 帧在封装时,不仅有 IP 报文,还有可能是 ARP 请求/应答
源 MAC 地址、目的 MAC 地址在以太网首部和 ARP 请求中各出现一次,对于链路层为以太网的情况是多余的,但如果链路层是其它类型的网络则有可能是必要的
这里的前三个字段是 MAC 帧的报头,所以真实的 ARP 请求只有后面的部分
- 硬件类型:链路层的网络类型,1 为以太网
- 协议类型:要转换的地址类型,0x0800 为 IP 地址
- 硬件地址长度:对于以太网地址为 6 字节,因为 MAC 地址是 48 位的
- 协议地址长度:对于 IP 地址为 4 字节,因为 IP 地址是 32 位的
- op 字段:为 1 表示 ARP 请求,op 字段为 2 表示 ARP 应答
- 后边四个字段就是用来 ARP 的请求和响应的,如果后边一些字段不清楚,比如目的 MAC 地址,就可以填成全 F 标识没有被设置
(4)ARP 的请求过程
路由器 A 构建 ARP 请求发送给 B
- 以太网地址和发送端 IP 地址对应路由器 A 的 MAC 地址和 IP 地址
- 目的以太网地址和目的 IP 地址对应就是主机 B 的 MAC 地址和 IP 地址,因为不知道主机 B 的 MAC 地址,所以填全 F
这个报文实际上是在 ARP 层封装的
报文要先向下交付,数据链路层进行封装,才会发送到局域网,所以需要添加以太网帧的报头
- 目的 MAC 地址并不知道,所以填全 F
- 源地址就填路由器 A 的 MAC 地址
- 类型就填 0806,因为 MAC 帧当中的帧类型字段设置为 0806
- 最后要加上 CRC 校验
- MAC 帧封装完毕后,路由器 A 就可以将封装好的 MAC 帧以广播的方式发送到局域网当中了
假设现在 MAC2 主机收到了这个报文,解包后发现目标 MAC 是全 F,说明是广播的,当识别到 MAC 帧当中的帧类型字段为 0806 后,便知道这是一个 ARP 的请求或应答的数据包,于是会将 MAC 帧的有效载荷向上交付给 ARP 层
当 ARP 收到数据包后,先比对 op 字段,判断是请求还是响应
- 1 是请求,然后提取目的 IP 字段,发现不是自己,就在 ARP 层直接丢弃数据包
(5)ARP 的应答过程
构建 ARP 响应:
- op 填 2,表示应答
- 目标 MAC 就填路由器 A
- 其他同理 ARP 请求
- 为了发送到局域网,所以接下来封装 MAC 帧报头
MAC 帧封装完毕后,主机 B 就可以将封装好的 MAC 帧发送到局域网当中
所有主机都会收到这个 MAC 帧,看到 MAC 帧报头中的目的 IP 如果不是自己的,就直接丢弃了,不会传递到 ARP 层
当路由器 A 的 ARP 层收到这个数据包后,先看的 op 字段为 2,于是判定这是一个 ARP 应答,然后就会提取发送端以太网的地址和发送端 IP 地址,此时路由器 D 就拿到了主机 B 的 MAC 地址
任何主机可能之前向目标主机发送过 ARP 请求,也就注定了未来一定会收到对应的 ARP 应答
任何一台主机也有可能收到别人发起的 ARP 请求
局域网中任何一台主机收到 ARP 时,可能是一个应答,也可能是一个请求
- 所有 ARP 层收到数据包后,都会先看 op 字段,如果是 1 请求,那么就构建应答,如果是 2 应答,那么就提取源 IP 和源 MAC 地址,就可以知道对方的 IP 和 MAC 地址了
ARP 可能在网络中的任意一条路径中发生
(6)ARP 缓存表
arp 请求成功之后,请求方会暂时将 IP:MAC 地址的映射关系保存起来
每次发起 ARP 请求后都会建立对应主机 IP 地址和 MAC 地址的映射关系
缓存表中的表项有过期时间,如果 20 分钟内没有再次使用某个表项,那么这个表项就会失效,下次使用时就需要重新发起 ARP 请求来获得目的主机的硬件地址,这主要是因为 IP 地址是会发生变化的
(7)RARP 协议
RARP(反向地址转换协议)是根据 MAC 地址获取 IP 地址的协议
在同一局域网内,MAC 地址可以直接给主机发送消息,因此可以直接发消息询问对方的 IP 地址
(8)ARP 欺骗
假设现在有一个局域网,每个主机内部都有 ARP 缓存表
此时来了一个中间人,它封装大量假的 ARP 请求发送给 MAC1,里面写的是 IP4:MAC3,同理给路由器发送 IP1:MAC3
MAC3 就成为了中间人,这种操作就叫作 ARP 欺骗
十三、其他重要协议或技术
1、DNS(Domain Name System)
TCP/IP 中使用 IP 地址和端口号来确定网络上的一台主机的一个程序,但 IP 地址不方便记忆,于是发明了一种叫主机名的东西,是一个字符串,并且使用 hosts 文件来描述主机名和 IP 地址的关系
- DNS 是应用层协议
- DNS 底层使用 UDP 进行解析
- 浏览器会缓存 DNS 结果
一个组织的系统管理机构,维护系统内的每个主机的 IP 和主机名的对应关系
- 如果新计算机接入网络,将这个信息注册到数据库中
- 用户输入域名时,会自动查询 DNS 服务器,由 DNS 服务器检索数据库,得到对应的 IP 地址
计算机仍然保留了 hosts 文件,在域名解析的过程中仍然会优先查找 hosts 文件的内容
- 查看 hosts 文件内容命令:cat /etc/hosts
A. 域名
主域名是用来识别主机名称和主机所属的组织机构的一种分层结构的名称,比如 www.baidu.com,域名使用 . 来连接
- com:一级域名,表示这是一个企业域名
- 同级的还有 .net(网络提供商),.org(开源组织或非盈利组织)等
- baidu:二级域名,公司名
- www:只是一种习惯用法
- 之前在使用域名时,往往命名成类似于 ftp.xxx.xxx/www.xxx.xxx 这样的格式来表示主机支持的协议
域名解析服务
- 域名可以让用户用起来更方便,辨识度更高
- 组织把全世界的主机名和 IP 地址的映射关系全部写成了一套网络服务,这个网络服务允许任何人请求,然后把域名转换成 IP 地址返回去,这也叫做域名解析服务
B. 从输入网址到获得页面的过程
浏览器中输入 URL 后,会发生的事情
- DNS 解析获得 IP 地址
- 浏览器获得域名对应的 IP 地址后,浏览器向服务器请求建立连接,发起三次握手
- TCP/IP 连接建立完成后,浏览器向服务器发送 HTTP 请求
- 服务器接收到这个请求,并根据路径参数映射到特定的请求处理器进行处理,并将处理结果及相应的视图返回给浏览器
2、ICMP
ICMP 协议是一个网络层协议
IP 协议不仅要有通信能力,还需要有故障排查能力。ICMP 是一个处于网络层和传输层之间的协议,它主要是用来确认报文是否丢失
ICMP 也是基于 IP 协议工作的,但它并不是传输层的功能,因此仍然把它归结为网络层协议
一个新搭建好的网络往往需要先进行一个简单的测试来验证网络是否畅通,但是 IP 协议并不提供可靠传输,如果丢包了,IP 协议并不能通知传输层是否丢包以及丢包的原因
(1)功能
确认 IP 包是否成功到达目标地址
通知在发送过程中 IP 包丢弃的原因
ICMP 只能搭配 IPv4 使用,如果是 IPv6 的情况下,需要使用 ICMPv6
假设主机 A 和主机 B 之间有多个路由器,但数据在最后一个路由器到达不了 B,此时这路由器就会多次发送 ARP 请求,如果还得不到应答,最后就会返回信息给主机 A,此时主机 A 就知道自己发送的数据无法到达主机 B
(2)ICMP 的报文格式
ICMP 大概分为两类报文
- 通知出错原因
- 用于诊断查询
(3)ping 命令
- ping 的是域名,而不是 url,一个域名可以通过 DNS 解析成 IP 地址
- ping 命令不光能验证网络的连通性,同时也会统计响应时间和 TTL
- ping 命令会先发送一个 ICMP Echo Request 给对端,对端接收到之后,会返回一个 ICMP Echo Reply
ping 命令底层是通过 ICMP 协议设置 TTL(跳数)来检测网络连通性
ping 的端口号
- ping 命令基于 ICMP,是在网络层,而端口号是传输层的内容
- 在 ICMP 中根本就不关注端口号这样的信息,所以 ping 根本没有端口号,ping 命令实际是绕过了传输层的直接访问底层 ICMP 协议的一种做法
(4)traceroute 命令
基于 ICMP 协议实现,能够打印出可执行程序主机,一直到目标主机之前经历多少路由器
3、NAT
IPv4 协议的 IP 地址数量不足,NAT 技术就是当前解决 IP 地址不够用的主要手段,并且能够有效地避免来自网络外部的攻击,隐藏并保护网络内部的计算机,是路由器的一个重要功能
- NAT 路由器将源地址从 10.0.0.10 替换成全局的 IP 202.244.174.37
- NAT 路由器收到外部的数据时,又会把目标 IP 从 202.244.174.37 替换回 10.0.0.10
- 在 NAT 路由器内部,有一张自动生成的,用于地址转换的表
- 当 10.0.0.10 第一次向 163.221.120.9 发送数据时就会生成表中的映射关系
(1)NAPT(地址转换表)
如果局域网内有多个主机都访问同一个外网服务器,那么对于服务器返回的数据中,目的 IP 都是相同的,那么 NAT 路由器无法判定将这个数据包转发给哪个局域网的主机,而 NAPT 能很好的解决数据返回的问题, 使用 IP+port 来建立这个关联关系
路由器在进行源地址转换的过程中,可能不只改变了源 IP,必要时源端口也要被替换,还维护了一张地址转化表
路由器在 NAT 转换的过程中,除了单纯的替换,还会根据报文请求的四元组构建一个映射关系
源 IP 表示唯一的一台主机,源端口表示该主机上唯一的一个进程,所以源 IP+源端口表示唯一的一个进程
无论是从内向外,还是从外向内,都能在各自的网络中表示唯一性,所以这个映射关系是互为 key 值的
这两个家用路由器在转发到运营商路由器时,此时运营商路由器就会建立地址转化表
- 左侧表里面的就是 IP:端口的四元组形式,标识了局域网内的唯一一台主机
- 右侧就是替换完成后的源地址和目标地址
- 这种关联关系也是由 NAT 路由器自动维护的,比如在 TCP 的情况下,建立连接时就会生成这个表项。在断开连接后,就会删除这个表项
- 在数据进行转发时,每个路由器都会维护一张 NAPT 地址转换表
- 同一个局域网的两台主机用的同一个客户端,就有相同的端口号, 此时经过外网路由器要替换源 IP,就导致无法区分这两台主机了
- 解决办法就是向外转发时发现两个主机有相同的端口号,那么就在转发出去时改变端口号
- NAT 路由器除了做 WAN 口 IP 的替换,还会进行端口号的替换
- 如果一台主机从来都没有访问过外网的服务器,那么这个服务器就无法找到这台主机,因为路由器没有建立映射关系,无法从 NAT 外部向内部服务器建立连接,因为外部无法知道内部的私网 IP,也就无法主动与内部服务器建立连接
- 有很多基于 NAT 原理的软件可以帮助进行从外网访问内网,比如内网穿透
(2)缺陷
- 无法从 NAT 外部向内部服务器建立连接
- 转换表的生成和销毁都需要额外开销
- 通信过程中一旦 NAT 设备异常,即使存在热备,所有的 TCP 连接也都会断开
(3)代理服务器
NAT 和代理服务器都是代替我们向服务器发起数据请求的,代理服务器看起来和 NAT 设备有一点像,客户端向代理服务器发送请求,代理服务器将请求转发给真正要请求的服务器,服务器返回结果后,代理服务器又把结果回传给客户端
代理服务器的功能就是代理网络用户去取得网络信息
A. 分类
a. 正向代理
用于请求的转发,比如借助代理绕过反爬虫
客户端并不直接访问目标服务器,而是先访问代理服务器,由代理服务器代替客户端去访问对应的目标服务器,并将目标服务器的响应结果返回给客户端
当多台主机都要访问外网的同一个资源,那么正向代理服务器就可以将对应的资源缓存到本地,此时当其他主机要访问该资源时,直接在正向代理服务器就可以获取,而不需要再次进行外网访问
b. 反向代理
往往作为一个缓存
对于客户端而言,反向代理服务器就相当于目标服务器,它不做任何业务的处理,只负责将请求推送到后端的指定主机
用户不需要知道目标服务器的地址,用户只需要访问反向代理服务器就可以获得目标服务器提供的服务
比如 www.baidu.com 对应的服务器实际就是一个反向代理服务器
- 百度内部实际并不是只有一台服务器,但不同地区的人们都可以通过访问 www.baidu.com 享受到百度提供的服务,实际我们访问的就是百度的反向代理服务器
- 当这台反向代理服务器收到客户端的数据请求后,就会将数据请求转发给百度内部的某台服务器进行数据处理,然后再将数据处理的结果返回给客户端
对于客户端而言,离客户端近的就是正向代理,离服务端近的就是反向代理
正向代理代理的是客户端,反向代理代理的是服务端,一个为客户端服务,一个为服务端服务
正向代理帮助客户端访问其无法访问的服务器资源的,而反向代理则是帮助服务器做负载均衡、安全防护等工作的
正向代理中,服务器不知道真正的客户端到底是谁,服务器认为正向代理服务器就是真实的客户端,而反向代理中,客户端不知道真正的服务器是谁,客户端认为反向代理服务器就是真实的服务器
B. NAT 和代理服务器的区别
- 从应用上讲,NAT 设备是网络基础设备之一,解决的是 IP 不足的问题;代理服务器则是更贴近具体应用,比如通过代理服务器进行翻墙,另外像迅游这样的加速器,也是使用代理服务器
- 从底层实现上讲,NAT 工作在网络层,直接对 IP 地址进行替换;而代理服务器往往工作在应用层
- 从使用范围上讲,NAT 一般在局域网的出口部署;代理服务器可以在局域网做,也可以在广域网做,也可以跨网
- 从部署位置上看,NAT 一般集成在防火墙,路由器等硬件设备上;代理服务器则是一个软件程序,需要部署在服务器上
十四、高级 IO
任何 IO 过程中,都包含两个步骤:等待和拷贝
在实际应用场景中,等待消耗的时间往往远高于拷贝的时间,为了让 IO 更高效,最核心的办法就是在单位时间内让等待的时间尽量少
1、五种 IO 模型
(1)阻塞 IO
在内核将数据准备好之前,系统调用会一直等待
阻塞 IO 是最常见的 IO 模型,所有的套接字,默认都是阻塞方式
在 recvfrom 函数等待数据就绪期间,在用户看来该进程就阻塞了,本质就是操作系统将该进程 / 线程的状态设置为了某种非 R 状态,然后将其放入等待队列中,当数据就绪后操作系统再将其从等待队列中唤醒,然后该进程 / 线程再将数据从内核拷贝到用户空间
以阻塞方式进行 IO 操作的进程 / 线程,在 “等” 和 “拷贝” 期间都不会返回
系统中大部分的接口都是阻塞式接口,如使用 read 函数从标准输入中读取数据
(2)非阻塞 IO
如果内核还没将数据准备好,系统调用仍然会直接返回,并且返回 EWOULDBLOCK 错误码
非阻塞 IO 往往需要循环的方式反复尝试读写文件描述符,这个过程称为轮询
- 对 CPU 来说是较大的浪费,一般只有特定场景下才使用
- 当调用 recvfrom 函数,以非阻塞方式从某个套接字上读取数据时,如果底层数据还没有准备好,那么 recvfrom 函数会立马错误返回,而不会让该进程 / 线程进行阻塞等待
- 因为没有读取的数据,所以该进程 / 线程后续还需要继续调用 recvfrom 函数轮询检测底层数据是否就绪,若没有就绪则继续错误返回,直到某次检测到底层数据就绪后,再将数据从内核拷贝到用户空间然后进行成功返回
- 每次调用 recvfrom 函数读取数据时,就算底层数据没有就绪,recvfrom 函数也会立马返回,在用户看来该进程或线程就没有被阻塞,因此被称为非阻塞 IO
如果要以非阻塞的方式打开某个文件,那么就需要在使用 open 函数打开文件时携带 O_NONBLOCK 或 O_NDELAY 选项,此时就可以以非阻塞的方式打开文件
- fcntl
- int fcntl(int fd, int cmd, ...);
- fd:已打开的文件描述符
- cmd:需要进行的操作
- F_DUPFD:复制一个现有的描述符
- F_GETFD / F_SETFD:获得/设置文件描述符标记
- F_GETFL / F_SETFL:获得/设置文件状态标记
- F_GETOWN / F_SETOWN:获得/设置异步 I/O 所有权
- F_GETLK, F_SETLK / F_SETLKW:获得/设置记录锁
- …:可变参数,传入的 cmd 值不同,后面追加的参数也不同
- 一个文件描述符,默认都是阻塞 IO
- int fcntl(int fd, int cmd, ...);
阻塞 IO 和非阻塞 IO 的区别:
阻塞 IO 当数据没有就绪时,后续检测数据是否就绪的工作是由操作系统发起的,而非阻塞 IO 当数据没有就绪时,后续检测数据是否就绪的工作是由用户发起的
(3)信号驱动 IO
内核将数据准备好时,使用 SIGIO 信号通知应用程序进行 IO 操作
当底层数据就绪的时候会向当前进程或线程递交 SIGIO 信号,因此可以通过 signal 或 sigaction 函数将 SIGIO 的信号处理程序自定义为需要进行的 IO 操作,当底层数据就绪时就会自动执行对应的 IO 操作
- 当底层数据就绪时,操作系统会递交 SIGIO 信号,此时就会自动执行定义好的信号处理程序,进程将数据从内核拷贝到用户空间
- 比如需要调用 recvfrom 函数从某个套接字上读取数据,可以将该操作定义为 SIGIO 的信号处理程序
信号的产生是异步的,但信号驱动 IO 是同步 IO 的一种
- 信号的产生是异步的,因为信号在任何时刻都可能产生
- 信号驱动 IO 是同步 IO 的一种,因为当底层数据就绪时,当前进程或线程需要停下正在做的事情,转而进行数据的拷贝操作,当前进程或线程仍然需要参与 IO 过程
- 判断一个 IO 过程是同步还是异步的,其本质就是看当前进程或线程是否需要参与 IO 过程,若参与即为同步 IO,否则为异步 IO
(4)IO 多路转接(IO 多路复用)
虽然从流程图上看起来和阻塞 IO 类似,IO 多路转接也被称为 IO 多路复用,实际上最核心在于 IO 多路转接能够同时等待多个文件描述符的就绪状态
A. 思想
因为 IO 过程分为 “等” 和 “拷贝” 两个步骤,因此使用的 recvfrom 等接口的底层实际上都做了两件事
- 第一件事是数据不就绪时需要等
- 第二件事是数据就绪后需要进行拷贝
虽然 recvfrom 等接口也有 “等” 的能力,但这些接口一次只能 “等” 一个文件描述符上的数据或空间就绪,IO 效率太低,因此系统提供了三组接口,即 select、poll 和 epoll,这些接口的核心工作就是 “等”,可将所有 “等” 的工作都交给这些多路转接接口
- 因为这些多路转接接口是一次 “等” 多个文件描述符的,能将 “等” 的时间重叠,所以数据就绪后再调用对应的 recvfrom 等函数进行数据拷贝,此时就能够直接进行拷贝,而不需要 “等” 了
IO 多路转接就像是帮人排队的黄牛,因为多路转接接口实际并没有进行数据拷贝,黄牛可以一次帮多个人排队,此时就将多个人排队的时间进行了重叠
B. select
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- nfds:需要监视的文件描述符中,最大的文件描述符值 +1
- readfds:调用时用户告知内核需要监视哪些文件描述符的读事件是否就绪,返回时内核告知用户哪些文件描述符的读事件已就绪
- writefds:调用时用户告知内核需要监视哪些文件描述符的写事件是否就绪,返回时内核告知用户哪些文件描述符的写事件已就绪
- exceptfds:调用时用户告知内核需要监视哪些文件描述符的异常事件是否就绪,返回时内核告知用户哪些文件描述符的异常事件已就绪
- timeout:调用时由用户设置 select 的等待时间,返回时表示 timeout 的剩余时间
- NULL / nullptr:select 调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪
- 0:select 调用后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,select 检测后都会立即返回
- 特定的时间值:select 调用后在指定的时间内进行阻塞等待,若被监视的文件描述符上一直没有事件就绪,则在该时间后 select 进行超时返回
- readfds、writefds 和 exceptfds 都是输入输出型参数。当 select 函数返回时这些参数中的值已经被修改了,因此每次调用 select 函数时都需对其进行重新设置,timeout 也是如此
select 是系统提供的一个多路转接的接口,可以用来实现多路复用 IO 模型
- select 系统调用可以让程序同时监视多个文件描述符上的状态变化
- select 核心工作就是等,当监视的文件描述符中有一个 / 多个事件就绪时,也就是直到被监视的文件描述符有一个 / 多个发生了状态改变,select 才会成功返回并将对应文件描述符的就绪事件告知调用者
返回值
- 若函数调用成功,则返回事件就绪的文件描述符个数
- 若 timeout 时间耗尽,则返回 0
- 若函数调用失败,则返回 -1,同时错误码被设置,此时其它参数的值变成不可预测
- 只要有一个 fd 数据就绪 / 空间就绪,就可以进行返回了
fd_set 结构
- fd_set 结构与 sigset_t 结构类似,它就是一个整数数组,严格来说 fd_set 本质也是一个位图,用位图中对应的位来表示要监视的文件描述符
- 调用 select 函数之前就需要用 fd_set 结构定义出对应的文件描述符集,然后将需要监视的文件描述符添加到文件描述符集中,这个添加的过程本质就是在进行位操作,但这个位操作不需要用户自己进行,系统提供了一组专门的接口,用于对 fd_set 类型的位图进行各种操作
- 取 fd_set 长度为 1 字节,fd_set 中的每一 bit 可以对应一个文件描述符 fd,则 1 字节长的 fd_set 最大可以对应 8 个 fd
- 执行 fd_set set; FD_ZERO(&set); 则 set 用位表示是 0000,0000
- 若 fd=5,执行 FD_SET(fd,&set); 后 set 变为 0001,0000
- 若再加入 fd=2,fd=1,则 set 变为 0001,0011
- 执行 select(6, &set, 0, 0, 0) 阻塞等待
- 若 fd=1,fd=2 上都发生可读事件,则 select 返回,此时 set 变为 0000,0011
- 没有事件发生的 fd=5 被清空
fd_set 是一个固定大小的位图,直接决定了 select 能同时关心的 fd 的个数是有上限的
timeval 结构
- timeout 是一个指向 timeval 结构的指针
- timeval 结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为 0
- 该结构中包含两个成员
- tv_sec 表示秒
- tv_usec 表示微秒
- select 等待多个 fd,等待策略有 3 种
- 阻塞式(nullptr)
- 非阻塞式({0, 0})
- 可以设置 timeout 时间,时间内阻塞,时间到了就立马返回({5, 0})
- 基本工作流程
- 1、先初始化服务器,完成套接字的创建、绑定和监听
- 2、定义一个 _fd_array 数组用于保存监听套接字和已经与客户端建立连接的套接字,初始化时就可将监听套接字添加到 _fd_array 数组中
- 3、然后服务器开始循环调用 select 函数,检测读事件是否就绪,若就绪则执行对应操作
- 4、每次调用 select 函数之前,都需要定义一个读文件描述符集 readfds,并将 _fd_array 中的文件描述符依次设置进 readfds 中,表示让 select 监视这些文件描述符的读事件是否就绪
- select 服务器只是读取客户端发来的数据,因此只要让 select 监视特定文件描述符的读事件,若要同时让 select 监视特定文件描述符的读事件和写事件,则需要分别定义 readfds 和 writefds,并定义两个数组分别保存需要被监视读事件和写事件的文件描述符,便于每次调用 select 函数前对 readfds 和 writefds 进行重新设置
- 由于调用 select 时还需要传入被监视的文件描述符中最大文件描述符值 +1,因此每次在遍历 _fd_array 对 readfds 进行重新设置时,还需要记录最大文件描述符值
- 5、当 select 检测到数据就绪时会将读事件就绪的文件描述符设置进 readfds 中,此时就能够得知哪些文件描述符的读事件就绪,并对这些文件描述符进行对应操作
- 6、若读事件就绪的是监听套接字,则调用 accept 函数从底层全连接队列获取已建立的连接,并将该连接对应的套接字添加到 _fd_array 数组中
- 7、若读事件就绪的是与客户端建立连接的套接字,则调用 read 函数读取客户端发来的数据并进行打印输出
- 8、服务器与客户端建立连接的套接字读事件就绪,也可能是客户端将连接关闭了,此时服务器应该调用 close 关闭该套接字,并将该套接字从 _fd_array 数组中清除,不需要再监视该文件描述符的读事件了
- 优点
- 可以同时等待多个文件描述符,且只负责等待(有大量的连接,但只有少量是活跃的,节省资源),实际的 IO 操作由 accept、read、write 等接口完成,保证接口在进行 IO 时不会被阻塞
- select 同时等待多个文件描述符,因此可以将 “等” 的时间重叠,提高 IO 效率
- 上述优点也是所有多路转接接口的优点
- 缺点
- 每次调用 select 都需手动设置 fd 集合,接口使用非常不便
- 每次调用 select 都需要把 fd 集合从用户态拷贝到内核态,在内核遍历传递进来的所有 fd,这个开销在 fd 很多时会很大
- 因为几乎每一个参数都是输入输出型的,也就决定了 select 一定会频繁的进行用户到内核、内核到用户的参数数据拷贝
- select 能够同时管理的 fd 个数是有上限的,即可监控的文件描述符数量太少
- 为了维护第三方数组,select 服务器会充满大量的遍历操作,OS 底层关心 fd 时,也要进行遍历
- 每一次都要对 select 输出参数进行重新设定
- 编码较复杂
- 适用场景
- 多路转接接口一般适用于多连接,且多连接中只有少部分连接比较活跃
- 比如聊天工具,登录 QQ 后大部分时间其实是没有聊天的,此时服务器端不可能调用一个 read 函数阻塞等待读事件就绪
- 因为少量连接比较活跃,也意味着几乎所有的连接在进行 IO 操作时,都需要花费大量时间来等待事件就绪,此时使用多路转接接口就可以将这些等的事件进行重叠,提高 IO 效率
- 对于多连接中大部分连接都很活跃的场景,其实并不适合使用多路转接
- 因为每个连接都很活跃,也意味着任何时刻每个连接上的事件基本都是就绪的,此时根本不需要动用多路转接接口来进行等待,毕竟使用多路转接接口也是需要花费系统的时间和空间资源的
- 多连接中大部分连接都很活跃,比如企业当中进行数据备份时,两台服务器之间不断在交互数据,此时连接是特别活跃的,几乎不需要等的过程,也就没必要使用多路转接接口了
- 多路转接接口一般适用于多连接,且多连接中只有少部分连接比较活跃
C. poll
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- fds:一个 poll 函数监视的结构列表,每一个元素都包含 3 部分内容:文件描述符、监视的事件集合、就绪的事件集合
- nfds:表示 fds 数组的长度
- timeout:表示 poll 函数的超时时间,单位是毫秒
- -1:poll 调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪
- 0:poll 调用后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,poll 检测后都会立即返回
- 特定的时间值:poll 调用后在指定的时间内进行阻塞等待,若被监视的文件描述符上没有事件就绪,则在该时间后 poll 进行超时返回
返回值
- 若函数调用成功,则返回有事件就绪的文件描述符个数
- 若 timeout 时间耗尽,表示超时,则返回 0,表示 poll 以非阻塞方式等待
- 若函数调用失败,则返回 -1,poll 要以阻塞方式等待,同时错误码被设置
struct pollfd 结构
- fd:特定的文件描述符,若设置为负值,则忽略 events 字段并且 revents 字段返回 0
- events:需要监视该文件描述符上的哪些事件
- 在调用 poll 函数之前,可以通过或运算符将要监视的事件添加到 events 成员中
- revents:poll 函数返回时告知用户该文件描述符上的哪些事件已经就绪
- 在 poll 函数返回后,可以通过与运算符检测 revents 成员中是否包含特定事件,以得知对应文件描述符的特定事件是否就绪
优点
- struct pollfd 结构中包含了 events 和 revents,相当于将 select 的输入输出型参数进行分离,因此在每次调用 poll 之前,不需像 select 一样重新对参数进行设置,接口使用比 select 方便
- poll 可监控的文件描述符数量没有限制,但是数量过大后性能也是会下降
- fd_set 类型只有 1024 个 bit 位,所以 select 函数最多只能监视 1024 个文件描述符
- 而 poll 函数能监视多少文件描述符由 poll 函数的第 2 个参数决定
- poll 可以同时等待多个文件描述符,提高 IO 效率
- 有大量的连接,但只有少量是活跃的,节省资源
缺点
- 当 poll 中监听的文件描述符数目增多时
- 和 select 函数一样,当 poll 返回后,需要遍历 _fds 数组来获取就绪的文件描述符,轮询 pollfd 来获取就绪的描述符
- 每次调用 poll 都需要将大量的 struct pollfd 结构从用户态拷贝到内核态,这个开销会随着 poll 监视的文件描述符数目增多而增大
- 同时连接的大量客户端在一时刻可能只有很少处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降
- 每次调用 poll 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大
- poll 的代码也比较复杂,但比 select 容易一些
D. epoll
epoll 在命名上比 poll 多了一个 e,可以理解成是 extend,epoll 就是为了同时处理大量文件描述符而改进的 poll
几乎具备了 select 和 poll 所有优点,它被公认为 Linux2.6 下性能最好的多路 I/O 就绪通知方法
a. 相关系统调用
int epoll_create(int size);
- 当不再使用时,必须调用 close 函数关闭 epoll 模型对应的文件描述符,当所有引用 epoll 实例的文件描述符都已关闭时,内核将销毁该实例并释放相关资源
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 参数
- epfd:epoll_create 函数的返回值,是 epoll 的句柄
- op:表示具体的动作,用三个宏来表示
- fd:需要监视的文件描述符
- event:需要监视该文件描述符上的哪些事件
- 返回值
- 函数调用成功返回 0
- 调用失败返回 -1,同时错误码会被设置
- struct epoll_event 结构
- 第一个成员 events 表示的是需监视的事件
- 第二个成员 data 为联合体结构
- 它不同于 select() 的点:select 是在监听事件时告诉内核要监听什么类型的事件,而 epoll 是先注册要监听的事件类型
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 参数
- epfd
- events:内核会将已就绪的事件拷贝到 events 数组中
- 不能是空指针,内核只负责将就绪事件拷贝到该数组,不会在用户态中分配内存
- maxevents:events 数组中的元素个数,该值不能大于创建 epoll 模型时传入的 size 值
- timeout:表示 epoll_wait 函数的超时时间,单位是毫秒
- -1:epoll_wait 调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪
- 0:epoll_wait 调用后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,epoll_wait 检测后都会立即返回
- 特定的时间值:epoll_wait 调用后在特定的时间内阻塞等待,若被监视的文件描述符上没有事件就绪,则在该时间后 epoll_wait 超时返回
- 返回值
- 若函数调用成功,则返回有事件就绪的文件描述符个数
- 若 timeout 时间耗尽,则返回 0
- 若函数调用失败,则返回 -1,同时错误码会被设置
b. 工作原理
红黑树和就绪队列
当某一进程调用 epoll_create 函数,Linux 内核会创建一个 eventpoll 结构体,即 epoll 模型
eventpoll 结构体中的成员 rbr、rdlist 与 epoll 的使用方式密切相关
epitem 结构中的成员 ffd 记录的是指定的文件描述符值,event 成员记录的就是该文件描述符对应的事件
- 对于 epitem 结构中 rbn 成员而言,ffd 与 event 的含义是:需要监视 ffd 上的 event 事件是否就绪
- 对于 epitem 结构中的 rdlink 成员而言,ffd 与 event 的含义是:ffd 上的 event 事件已就绪
- 红黑树是一种二叉搜索树,必须有键值 key,文件描述符可以天然的作为红黑树 key 值,调用 epoll_ctl 向红黑树中新增节点时,若设置了 EPOLLONESHOT 选项,监听完这次事件后,若还需继续监听该文件描述符则需重新将其添加到 epoll 模型中,本质就是当设置了 EPOLLONESHOT 选项的事件就绪时,操作系统会自动将其从红黑树中删除
- 若调用 epoll_ctl 向红黑树中新增节点时没有设置 EPOLLONESHOT,那么该节点插入红黑树后就会一直存在,除非用户调用 epoll_ctl 将该节点从红黑树中删除
- 回调机制
- 所有添加到红黑树中的事件都与设备驱动程序建立回调方法,该回调方法在内核中被称为 ep_poll_callback
- 因此只有红黑树中对应的事件就绪时,才会执行对应的回调方法将其添加到就绪队列
- 对于 epoll 而言,操作系统不需要主动进行事件的检测,当红黑树中监视的事件就绪时,会自动调用对应回调方法,将就绪的事件添加到就绪队列中
- 当不断有监视的事件就绪时,会不断调用回调方法向就绪队列中插入节点,而上层也会不断调用 epoll_wait 函数从就绪队列中获取节点,即典型的生产者消费者模型
- 当用户调用 epoll_wait 函数获取就绪事件时,只需关注底层就绪队列是否为空,若不为空则将就绪队列中的就绪事件拷贝给用户
- 采用回调机制最大的好处:不再需要操作系统主动对就绪事件进行检测,当事件就绪时会自动调用对应的回调函数进行处理
- 由于就绪队列可能被多个执行流同时访问,所以必须要使用互斥锁进行保护,eventpoll 结构中的 lock 和 mtx 就是用于保护临界资源的,因此 epoll 本身是线程安全的,eventpoll 结构中的 wq 即等待队列,当多个执行流想同时访问同一个 epoll 模型时,就需要在该等待队列下进行等待
- 所有添加到红黑树中的事件都与设备驱动程序建立回调方法,该回调方法在内核中被称为 ep_poll_callback
c. 优点(和 select 的缺点对应)
- 接口使用方便:拆分成了三个函数,使用起来更方便高效,不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离开, 不至于冗杂
- 数据拷贝轻量:只在新增监视事件时调用 epoll_ctl 将数据从用户拷贝到内核中,而 select 和 poll 每次都需要重新将需要监视的事件从用户拷贝到内核。调用 epoll_wait 获取就绪事件时,只会拷贝就绪的事件,不进行不必要的拷贝操作
- 事件回调机制:避免操作系统主动轮询检测事件就绪,而是采用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中。调用 epoll_wait 时直接访问就绪队列就知道哪些文件描述符已就绪,检测是否有文件描述符就绪的时间复杂度是 O(1),因为本质只需要判断就绪队列是否为空即可,即使文件描述符数目很多,效率也不会受到影响
- 没有数量限制:监视的文件描述符数目无上限,只要内存允许,可一直向红黑树中新增节点
d. 与 select 和 poll 的不同之处
- 在使用 select 和 poll 时,都需借助第三方数组来维护历史上的文件描述符以及需要监视的事件,第三方数组由用户自行维护,对该数组的增删改操作都需要用户进行
- 使用 epoll 时,不需要用户维护第三方数组,epoll 底层的红黑树就充当了这个第三方数组的功能,并且该红黑树的增删改操作都是由内核维护的,用户只要调用 epoll_ctl 让内核对该红黑树进行对应的操作即可
- 在使用多路转接接口时,数据流都有两个方向,一个是用户告知内核,一个是内核告知用户。select 和 poll 将这两件事情都交给了同一个函数来完成,而 epoll 在接口层面上就将这两件事进行了分离,epoll 通过调用 epoll_ctl 完成用户告知内核,通过调用 epoll_wait 完成内核告知用户
e. 工作方式
水平触发(Level Triggered)
- 只要底层有事件就绪,epoll 就会一直通知用户
- 当 epoll 检测到底层读事件就绪时,可以不立即进行处理或者只处理一部分,因为只要底层数据没有处理完,下一次 epoll 还会通知用户事件就绪
- epoll 默认状态下就是 LT 工作模式
- select 和 poll 就是工作是 LT 模式下的
- 支持阻塞读写和非阻塞读写
边缘触发(Edge Triggered)
- 如果在第一步将 socket 添加到 epoll 描述符时使用了 EPOLLET 标志,epoll 就进入 ET 工作模式
- 只有底层就绪事件数量由无到有或由有到多发生变化时,epoll 才会通知用户
- 因此当 epoll 检测到底层读事件就绪时,必须立即进行处理且全部处理完毕,因为有可能此后底层再也没有事件就绪,那么 epoll 就再也不会通知用户进行事件处理,此时没有处理完的数据就丢失了
- 这就逼迫用户当读事件就绪时必须一次性将数据全部读取完毕,当写事件就绪时必须一次性将发送缓冲区写满,否则可能再也没有机会进行读写了
- 读数据时必须循环调用 recv 函数进行读取
- 当底层读事件就绪时,循环调用 recv 函数进行读取,直到某次调用 recv 读取时,实际读取到的字节数小于期望读取的字节数,则说明本次底层数据已读取完毕了
- 但有可能最后一次调用 recv 读取时,刚好实际读取的字节数和期望读取的字节数相等,但此时底层数据也恰好读取完毕了,若再调用 recv 函数进行读取,那么 recv 就会因为底层没有数据而被阻塞
- 这里阻塞是非常严重的,就比如博客写的服务器都是单进程的服务器,若 recv 被阻塞住,并且此后该数据再也不就绪,那么就相当于服务器挂掉了,因此在 ET 工作模式下循环调用 recv 函数进行读取时,必须将对应的文件描述符设置为非阻塞状态
- 写数据需循环调用 send 函数进行数据的写入,且必须将对应的文件描述符设置为非阻塞状态
- 读数据时必须循环调用 recv 函数进行读取
- epoll 通知用户的次数一般比 LT 少,使用 ET 能够减少 epoll 触发的次数,但需要在一次响应就绪过程中就把缓冲区的所有的数据都处理完,因此 ET 的性能一般比 LT 性能更高
- 应用层尽快的取走了缓冲区中的数据,那么在单位时间内,该模式下工作的服务器就可以在一定程度上给发送方发送一个更大的接收窗口,所以对方就有更大的滑动窗口,一次就能发送更多数据,从而提高了 IO 吞吐
- Nginx 就是默认采用 ET 模式使用 epoll 的
- 只支持非阻塞的读写
- recv 和 send 操作的文件描述符必须设置为非阻塞状态
- epoll 既可以支持 LT,也可以支持 ET
- ET 的编程难度比 LT 更高
- 在 ET 模式下,一个文件描述符就绪后,用户不会反复收到通知,看起来比 LT 更高效,但若在 LT 模式下能够做到每次都将就绪的文件描述符立即全部处理,不让操作系统反复通知用户的话,其实 LT 和 ET 的性能是一样的
f. 使用场景
对于多连接且多连接中只有一部分连接比较活跃时,比较适合使用 epoll
- 比如一个需要处理上万个客户端的服务器,好比各种互联网 APP 的入口服务器
(5)异步 IO
由内核在数据拷贝完成时,通知应用程序
信号驱动是告诉应用程序何时可以开始拷贝数据
异步 IO 需要调用一些异步 IO 接口,异步 IO 接口调用后会立即返回,因为异步 IO 不需要发起者进行 “等” 和 “拷贝” 的操作,都由操作系统来完成,只需发起 IO
当 IO 完成后操作系统会通知应用程序,因此进行异步 IO 的进程 / 线程并不参与 IO 的所有细节
2、同步通信 VS 异步通信(Synchronous Communication / Asynchronous Communication)
同步和异步关注的是消息通信机制
(1)同步
发出一个调用,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了
- 就是由调用者主动等待这个调用的结果
(2)异步
调用在发出之后,这个调用就直接返回了,所以没有返回结果
- 当一个异步过程调用发出后,调用者不会立刻得到结果,而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用
在访问临界资源时,一定要弄清楚这个 “同步” 是同步通信异步通信的同步,还是同步与互斥的同步
- 进程 / 线程同步:指的是在保证数据安全的前提下,让进程 / 线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,谈论的是进程 / 线程间的一种工作关系
- 同步 IO:指的是进程 / 线程与操作系统之间的关系,谈论的是进程 / 线程是否需要主动参与 IO 过程
3、阻塞 VS 非阻塞
阻塞和非阻塞关注的是程序在等待调用结果时的状态
阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
4、其他高级 IO
- 纪录锁
- 系统 V 流机制
- readv 和 writev 函数
- 存储映射 IO( mmap )
5、Reactor 模式
Reactor 反应器模式,也被称为分发者模式或通知者模式,是一种将就绪事件派发给对应服务处理程序的事件设计模式
(1)角色构成
(2)工作流程
- 当向初始分发器注册具体事件处理器时,会标识出该事件处理器希望初始分发器在某个事件发生时向其通知,该事件与 Handle 关联
- 初始分发器会要求每个事件处理器向其传递内部的 Handle,该 Handle 向操作系统标识了事件处理器
- 当所有的事件处理器注册完毕后,启动初始分发器的事件循环,此时初始分发器会将每个事件处理器的 Handle 合并起来,并使用同步事件分离器等待这些事件的发生
- 当某个事件处理器的 Handle 变为 Ready 状态时,同步事件分离器会通知初始分发器
- 初始分发器会将 Ready 状态的 Handle 作为 key,来寻找其对应的事件处理器
- 初始分发器会调用其对应事件处理器中对应的回调方法来响应该事件