计算机网络

应用程序

  • 计算机组成原理
    • 描述计算机是什么?计算是怎么回事?硬件如何为应用提供计算?
  • 操作系统
    • 最大的实践意义是:如何合理规划应用的生命周期以及资源使用。比如如何处理高并发、如何提升系统的稳定性、如何节约硬件成本等。
  • 计算机网络:
    • 讲的是应用之间如何进行通信、如何设计应用之间的契约,形成稳定、高效、规范的协作关系(也就是协议),通过优化网络的性能,最终节省成本或者让用户满意
      • 为了让页面秒开、服务秒回,所作出的努力
      • 为了优化网络传输细节,去调整TCP的滑动窗口
      • 为了提升网络的吞吐量、减少延迟,去开启多路复用能力
      • 为了避免Downtime,去调整网络的连接池和线程数
      • 为了开发某个应用,尝试理解应用层协议,如:SSH、RTCP、HTTP2.0、MQTT
      • 为了做好日常开发,去理解一些基本概念,如:DNS、CDN、NAT、IPv4/6等
  • 算法和数据结构【重学数据结构与算法,公瑾,笔记:https://www.cnblogs.com/jmcui/p/15224732.html】
    • 算法是一个设计过程,数据结构是数据的组织方式
    • 在给定资源的条件下,最低的延迟、最少的计算时间、最大的空间利用率
  • 图形学
    • AI是将数据看作为图片,从图形中找到概率特征
  • 编程技巧
    • 需要深入学习
  • 编译原理
    • 程序语言如何被实现,源代码如何被编译

常见问题

TCP队头阻塞,没有预备方案,导致分布式集群中部分发生延迟,导致系统雪崩

DDoS

DNS

跨机房通信问题

TCP 为什么要三次握手

HTTPS协议的TTFB传输时间

DNS故障排查

Linux下 诸如 nslookup, telnet, lsof, netstat等网络相关的指令一知半解导致不知道如何定位问题。

使用TCP连接时需注意问题

只考虑收发数据、不能考虑到队头阻塞、多路复用等问题,导致经常出现系统负载不高、但是吞吐量很低的情况

网络调试、网络优化

常识问题

  • telnet调试远程服务、用whireshark抓包定位网络故障

  • 把ulimit设置成多少?

  • Dubbo异步单一连接扛不住了该怎么办?

  • 用HTTP协议的Keep-Alive维持心跳可不可行?

  • 开发应用或程序时,用什么协议?用哪个网络框架?

  • 没有办法优化参数,或者当承接了系统优化的工作时,由于计算机网络知识不扎实,会陷入无穷无尽的学习

  • TCP队头阻塞

  • 滑动窗口

  • ARP和路由算法

frame

网络的基础机构

路由器、交换机、终端、基站等以及背后隐含的网络

网络的工作原理

  • 算法问题
    • 滑动窗口、路由和寻址
  • 细节问题
    • 封包格式
  • 工作原理
    • 多路复用、缓存设计、Socket、I/O模型等

网络的应用场景

HTTPS 协议握手的过程

RPC 是如何工作的

IM系统是如何工作的

抓包用什么工具

要注意什么安全攻防

网络出了问题怎么排查


  • 计算机网络
    • 互联网和传输层协议
      • 互联网的整体架构
      • TCP协议
      • TCP的封包格式
      • TCP的原理和算法
      • UDP协议
    • 网络层协议
      • IP协议
      • IPv6协议
      • 局域网和NAT
      • 实战:TCP/IP抓包
    • 网络编程
      • Socket是什么
      • Socket和I/O模型
      • 流和缓冲区
      • BIO, NIO, AIO
      • RPC框架原理
    • Web技术
      • DNS
      • CDN
      • HTTP协议
      • 流媒体技术
      • 爬虫和反爬虫
    • 网络安全
      • 加密、解密和证书
      • 信任链
      • 常见攻防手段

互联网和传输层协议

  • 互联网的体系和整体架构
  • 硬件设备以及作用
  • 传输层协议TCP和UDP

1. 漫游互联网: 什么是蜂窝移动网络

frame
frame

交换技术的本质:让数据切换路径

网络中的数据是以分组封包(Packet)的形式

具备交换的网络设备: 路由器(Router)链路层交换机(Link-Layer Switch)

在移动网络中,无线信号构成了通信链路,通信的核心被称为蜂窝塔(Cellular Tower),有时候也被称为基站(BaseStation)。ISP 将网络供给处于蜂窝网络边缘的路由器,路由器连接蜂窝塔,再通过蜂窝塔(基站)提供处于六边形地区中的设备。

在蜂窝网络一定范围内的区域,离用户较近的地方还可以部署服务器,帮助用户完成计算。这相当于计算资源的下沉,称为 “边缘计算”。相比中心化的计算,边缘计算延迟低、链路短,能够将更好的体验带给距离边缘计算集群最近的节点。从而让用户享受到更优质、延迟更低、算力更强的服务。

通信链路是一个抽象概念,这里说的抽象,就是面向对象中抽象类和继承类的关系,比如公司网络使用同轴电缆作为通信链路、移动网络使用无线信号的发送接收器作为通信链路、家用网络使用蓝牙信道作为通信链路等;

我们可以把网络传输分为两类,一类是端对端(Host-to-Host)的能力,由 TCP/IP 协议群提供;另一类是广播的能力,是一对多、多对多的能力,可以看作端对端(Host-to-Host)能力的延伸。

2. 传输层协议TCP: TCP为什么是握手3次、挥手是4次

TCP(Transport Control Protocol)是一个传输层协议,提供 Host-To-Host 数据的可靠传输,支持全双工,是一个连接导向的协议。

TCP/IP 协议群

  • 应用层提供的是应用到应用(Application-To-Application)的协议,比如微信和微信服务器;
  • 传输层提供的是主机到主机(Host-To-Host)的协议,比如手机、平板、Linux 主机等;
  • 网络层提供的是地址到地址(Address-To-Address)的协议,IP 协议就在这一层工作;
  • 链路层提供的是设备到设备(Device-To-Device)的协议;
  • 物理层提供最底层的传输能力,当信号在两个设备间传递的时候,物理层封装最底层的物理设备、传输介质等

TCP(Transport Control Protocol)是一个传输层协议,提供 Host-To-Host 数据的可靠传输,支持全双工,是一个连接导向的协议。

TCP 上层的应用层协议使用 TCP 能力的时候,需要告知 TCP 是哪个应用 —— 这就是端口号,端口号用于区分应用。

连接(Connection)是传输层的概念,是数据传输双方的契约;会话(Session)是应用层的概念,是应用的行为;

TCP 是一个双工协议,数据任何时候都可以双向传输,那么什么是双工/单工:

  • 单工:在任何一个时刻,数据只能单向发送。单工需要至少一条线路;
  • 半双工:在某个时刻数据可以向一个方向传输,也可以向另一个方向反方向传输,而且交替进行。半双工需要至少一条线路;
  • 全双工:任何时刻数据都可以双向发送。全双工需要大于一条线路;
  • 客户端和服务端在TCP协议中都被称为Host(主机)

可靠性(数据保证无损传输):如果发送方按照顺序发送,然后数据无序地在网络间传递,就必须有一种算法在接收方将数据恢复原有的顺序;如果发送方同时把消息发送给多个接收方,这种情况叫做多播,可靠性要求每个接收方都收到相同的副本。

TCP是一个连接导向的协议设计有建立(握手)和断开连接(挥手)的过程。

TCP协议的基本操作:

  • 如果一个Host主动向另一个Host发起连接,成为SYN(Synchronization),请求同步
  • 如果一个Host主动断开请求,称为FIN(Finish),请求完成
  • 如果一个Host给另一个Host发送数据,成为PSH(Push),数据推送

    接收方收到数据后,都需要给发送一个ACK响应,保持连接和可靠性约束,TCP协议要保证每一条发出的数据必须给返回

三次握手: C–(SYN)–S–(SYN-ACK)–C–(ACK)–S

四次挥手: C–(FIN)–S–(ACK) 然后 (FIN) –C–ACK–S

为TCP协议增加协议头,在协议头中取多个位(bit),其中SYN,ACK,PSH都占有1个位,这种设计成为标识(Flag)04

TCP是一个面向连接的协议(Connection-oriented Protool),就是说TCP协议参与双方(Host)在手法数据前会先建立连接。而UDP是一面发送报文(Datagram-oriented)的协议,不需要建立连接,直接发送报文(数据)

因此,TCP3次握手4次挥手的原因:TCP是一个双工协议,为了让双方都保证,建立连接的时候,连接双方都需要向对方发起SYC(同步请求)和ACK(响应),握手阶段双方都没有繁琐的工作,因此一方向另一方发起同步(SYN)之后,另一方可以将自己的ACK和SYN打包作为一条消息回复,因此是3次握手需要3次数据传输;挥手阶段,双方都有可能未完成的工作,收到挥手请求的一方,必须马上响应(ACK),表示接收到了挥手请求,最后等所有工作结束,再发送请求中断连接(FIN),因此是4次挥手

思考:一台内存为8G左右的服务器可以同时维护多少个连接?

100w?连接是内存中的状态对象,从理论上分析,连接本身不太占用内存。不同语言连接对象大小不等,但是通常很小。当单机建立太多链接,会爆出 Cannot assign requested address 异常,这是由于没建立一个连接,操作系统就会为客户端分配端口号,端口号很快就被占用用尽所以核心问题是,通信需要缓冲区,通讯需要 I/O 。这是因为通讯占用资源,连接本身占用资源少。

压力测试最常用的工具是 Apache Benchmark (简称 AB)或者用JMeter(有界面)

1
2
3
linux 可执行以下命令安装yum install httpd-tools
// or
apt-get install apache2-utils

3. TCP的封包格式:TCP为什么要粘包和拆包


问题:传输层的协议为什么不选择将文件一次发送呢?

- 为了**稳定性**,一次发送的数据越多,出错的概念就越大
- 为了效率,网络中有时候存在着**并行**的路径,拆分数据包就能更好的利用这些并行的路径
- 发送和接收数据的时候,都存在**缓冲区**;如果随意发送很大的数据,可能导致网卡处理不过来,而导致其他应用实时性遭到破坏;
- 内存最小的分配单位是**页表**,如果数据的大小超过一个页表,可能会存在页面置换问题,造成性能的损失
- 传输层封包不能太大,以缓冲区大小为单位,TCP协议会将数据拆分成不超过缓冲区大小的一个个部分,每个部分都有一个独特的名词,叫做**TCP段**(TCP Segment)

拆包:将数据拆分成多个TCP段传输;
粘包:将多个数据合并成一个TCP段传输;

TCP分组格式示意图
TCPheader

  • Source Port/Destination Port描述的是发送端口号和目标端口号,代表发送数据的应用程序和接受数据的应用程序;

  • Data Offset是一个偏移量,原因TCP Header部分的长度可变,需要一个数值描述数据从哪个字节开始

  • Reserved 是很多协议设计会保留的一个区域,用于日后扩展能力

  • URG…FIN标志位描述TCP段行为

    • CWR —— Congestion Window Reduced 用于通知发送方,降低发送速率
    • ECE —— ECN ECHO 通知发送方收到拥塞控制
    • URG —— 为 1 表示高优先级数据包
    • ACK —— 为 1 表示确认号码字段有效
    • PSH —— 为 1 是带有 “PUSH” 标志的数据,指示接收方应当尽快将这个报文段交给应用层,而不用等待缓冲区装满
    • RST —— 为 1 表示出现严重差错,需要重置 TCP 连接
    • SYN —— 为 1 表示这是连接请求或者连接接受请求,用于创建连接和使序列号同步
    • FIN —— 为 1 表示发送方没有数据要传输了,要求释放连接
  • Window是TCP保证稳定性并进行流量控制的工具,窗口的大小,即接收方将要接受的字节数,该字段占 16 bit,因此窗口大小最大为 65535 bytes。

  • Checksum是校验和,用于校验TCP段有没有损坏、丢失,其算法本质上与 IP 中的校验和算法相同。

  • Urgent pointer只想最后一个紧急数据的序号(Sequence Number),在 URG 设置为 1 时生效,这个指针表示紧急数据在整个流中的位置。

  • TCP 可选项,TCP 头中的最后一个字段是一些附加的可选项,原始规范中提供了 3 个选项,但后来的规范中,不断增加了新的选项:

    • MSS(Maximum Segment Size): 该 TCP 协议实现的可以接收的最大 TCP 段的大小,比较典型的例子是 IPv4 中 TCP 的最大段为 1460 bytes
      • 如果设置过大,会导致服务器拒绝接收,或用户挤占用服务器太多资源;如果设置太小,会浪费传输资源(降低吞吐量)
    • SACK(Selective Acknowledgment): 这个选项优化了数据包大量丢失并且接受者的数据窗口存在漏洞的情况。主要是因为 TCP 接收到的分组必须要能够根据其顺序组成完整的信息,丢掉其中任何一个都需要整个重传。而 SACK 就是允许 TCP 协议接收不连续的块,最后只需要重传丢失的块就可以了。
    • Window Scale: 窗口缩放选项,用于把窗口的大小从 65535 bytes 扩大到 1 G。具有更大的数据窗口,有利于批量的数据传输
    • Timestamps: TCP 时间戳,可以用这个时间戳计算每个 ACK 的 RTT,可以用来计算 TCP 重传超时
  • Padding存在的意义是因为Option的长度不固定,需要pading对齐

  • 紧急指针,是在 URG 设置为 1 时生效,这个指针表示紧急数据在整个流中的位置。

  • 序列号码表示 TCP 段的窗口索引,TCP 流中的每个字节都被编号串联起来。在进行握手的阶段,会先生成一个初始序列号码,之后在此基础上递增。

IP协议拆分太多的封包并没意义,1)导致同个TCP段的封包被不用不同的网络线路传输,加大延迟;拆包需要消耗硬件和计算资源


问题:TCP如何恢复数据的顺序的?TCP拆包和粘包的作用是什么?

TCP拆包的作用:将任务拆分处理,降低整体任务出错的概率,以及减小底层网络处理的压力,粘包过程需要保证数据经过网络的传输,又能恢复到原始的数据。

中间,需要数学提供保证顺序的理论依据,TCP利用(发送字节数、接受字节数)的唯一性来确定封包之间的顺序关系。

TCP粘包的作用:防止数据量过小导致大量的传输而将多个TCP段合并成一个发送


4. TCP的稳定性:滑动窗口和流速控制是怎么回事(保证顺序的算法,同时保证更高的吞吐量)

TCP作为一个传输层协议,最核心的能力是传输,传输需要保证可靠性,还需要控制流速,这两个核心能力均有滑动窗口提供。[基于滑动窗口可设计分布式RPC框架,实现消息队列或分布式文件系统]

窗口大小的单位不是TCP段的数量,而是多少个字节

有了窗口,发送方利用滑动窗口算法发送消息;接收方构造缓冲区接收消息,并给发送方ACK

滑动窗口时TCP协议控制可靠性的核心,发送方将数据拆包,变成多个分组,然后将数据放入一个拥有滑动窗口的数组,依次发出,仍然遵循先入先出(FIFO)的顺序,但是窗口中的分组会一次性发送。窗口中序号最小的分组如果收到ACK,窗口就会发生滑动。如果最小序号的分组长时间没有收到ACK,就会触发整个窗口的数据重新发送。

思考:发送方有窗口,那么接收方也需要窗口吗?

5. UDP协议:TCP协议和UDP协议的优势和劣势

TCP和UDP是应用最广泛的传输层协议,拥有最核心的垄断地位,TCP最核心的价值保证可靠性,UDP的核心价值是灵活性

UDP(User Datagram Protocol),目标是在传输层提供直接发送报文(Datagram)的能力,Datagram是数据传输的最小单位,UDP协议不会帮助拆分数据,他的目标只有一个,就是发送报文

TCPheader

区别 TCP UDP
目的差异 提供可靠的网络传输 在提供报文交换能力基础上尽可能地简化协议轻装上阵
可靠性差异 在保证可靠性下提供更好的访问 只管发送数据封包
连接vs无链接 面向连接的协议(Connection-oriented Protocol) 无连接协议(Connection-less Protocol)
流控技术(Flow Control) 流控技术 在发送缓冲区中存储数据,并在接收缓冲区中接收数据 没有提供
传输速度 协议简化,封包小,没有连接、可靠性检查等,速度更快
场景差异 不适合高速数据传输场景[不适合网络游戏、视频传输] Ping和DNSLookup只需要一次简单的请求/返回,不需要建立连接
- - -
传输 无损传输 传输更快
协议 HTTP协议更可靠 HTTP3.0协议更多从功能上出发

任何一个用TCP协议构造的成熟应用层协议,都可以用UDP重构

TCP应用场景

  • 远程控制(SSH)
  • File Transfer Protocol(FTP)
  • 邮件(SMTP、IMAP)等
  • 点对点文件传出(微信等)

UDP应用场景

  • 网络游戏
  • 音视频传输
  • DNS
  • Ping
  • 直播

模糊应用场景

  • HTTP(目前以TCP为主)
  • 文件传输

UDP不提供可靠性,但不代表不能解决可靠性;UDP的核心价值:灵活、轻量,构造了最小版本的传输层协议;可以实现连接(可靠性),实现会话(Session),实现可靠性(Reliability)…

总结:TCP比较严谨(序号的设计、滑动窗口的设计、快速重发的设计、内在状态机制的设计),而UDP更加简单专注,报文传输可靠性流量控制连接和会话

问题解析:TCP最核心的价值就是提供封装好的一套解决可靠性的优秀方案;TCP在确保吞吐量、延迟、丢包率的基础上,保证可靠性;UDP提供了最小版本的实现,支持Checksum,UDP最核心的价值:轻量、灵活、传输速度快

问题:Moba类游戏的网络应用应该用TCP还是UDP?

内存状态在同时刻只能有一个状态,所以多线程的操作必须有先后

对于游戏,在线竞技游戏,每个事件(英雄放大招),游戏服务器必须给一个唯一的时序编号。服务器要尽快响应多个客户单提交的事件,并以最快的速度分配自增序号,然后返回给客户端。

所以moba

网络层协议(局域网和IP协议)

6. IPv4协议:路由和寻址的区别

问题回答:寻址(Addressing)就是通过地址找到设备;路由(Routing)本质是路径的选择。就好像知道地址,但是到了每个十字路口,还需要选择具体的路径。因此,路由和寻址,是相辅相成的关系。

IP 协议(Internet Protocol)是一个处于垄断地位的网络层协议。IPv4 就是 IP 协议的第 4 个版本,是目前互联网的主要网络层协议。IP协议需要底的数据链路层的支持。IP 协议并不负责数据的可靠性,可靠性是 IP 协议上方的 Host-To-Host 协议(即传输层协议)保证的。

IP 协议接收 IP 协议上方的 Host-To-Host 协议(即传输层协议)传来的数据,然后进行拆分,这个能力叫做分片(Fragmentation);然后 IP 协议为每个分片增加一个 IP 头(Header),组成一个 IP 封包(Datagram);之后,IP 协议调用底层的局域网(数据链路层)传送数据。最后 IP 协议通过寻址和路由能力最终把封包送达目的地。

IP协议存在的问题:

  • 封包损坏:数据传输过程中损坏
  • 丢包:数据发送中丢失
  • 重发:数据被重发,比如中间设备通过2个路径传递数据
  • 乱序:到达目的地时数据和发送数据不一致

IP协议的工作原理

  1. 分片:是把数据切分成片,IP协议通过它下层的局域网(链路层)协议传输数据,因此需要适配底层网络的传输能力
  2. 增加协议头,如下图:
    IPheader
  • IHL描述协议头大小
  • Type Of Service为了在延迟、吞吐量和丢包率做出选择
  • time to time描述封包存活时间
  • Protocol 上层协议,TCP=7,UDP=17

描述网络的三个指标:

  • 延迟(Latency):1bit 的数据从网络的一个终端传送到另一个终端需要的时间;
  • 吞吐量(Throughput):单位时间内可以传输的平均数据量,单位:bps=bit/s;
  • 丢包率(Packet loss):丢包率指的发送出去的封包没有达到目的地的比率;

IPv4地址是4个8位(Octet)排列而成,总共可以编址43亿个地址。

IP: 103 16 3 1
Octet: 01100111 00010000 00000011 00010001

寻址步骤:

  • 找到顶层网络,例如103.16.3.1最顶层的网络号可以和255.0.0.0(子网掩码)做位与运算得到103.16.3.1&255.0.0.0=103.0.0.0
  • 找到下一层网络,就需要IP地址和下一级的子网掩码做位与运算103.16.3.1&255.255.0.0=103.16.0.0
  • 找到再下一级网络,就需要IP地址和下一级的子网掩码做位与运算103.16.3.1&255.255.255.0=103.16.3.0
  • 定位设备,设备就是子网103.16.3.0中,最终找到设备号是1

在寻址过程中,数据总存在于某个局域网内,如果目的地在局域网中,可以直接定位到设备,如果目的不在局域网中,就需要去往其他网络。由于网络与网络之间是网关在连接,如果目的地的IP地址不在局域网中,就需要被IP封包,选择通往下一个网络的路径,其实就是选择下一个网关。如果一个网络和多个网络接壤,就会存在多个网关。例如:路由器为ip 14.215.117.38寻址,路由所在的编号为16.0.0.0,就需要知道去往14.0.0.0网络的gateway地址,如果用route查看路由表可以看到Destination:14.0.0.0 Gateway:16.12.1.100 Mask 255.0.0.0 Iface:16.12.1.1 14.215.117.38先要和mask进行位与运算,然后进行查表看到14.0.0.0得知去的网卡是16.12.1.1


思考:127.0.0.1,localhost,0.0.0.0有什么不同?
127.0.0.1是本地回环地址(loopback),发送到 loopback 的数据会被转发到本地应用。

localhost 指代的是本地计算机,用于访问绑定在 loopback 上的服务。localhost 是一个主机名,不仅仅可以指向 IPv4 的本地回环地址,也可以指向 IPv6 的本地回环地址 [::1]。

0.0.0.0是一个特殊目的 IP 地址,称作不可路由 IP 地址,它的用途会被特殊规定。通常情况下,当我们把一个服务绑定到0.0.0.0,相当于把服务绑定到任意的 IP 地址。比如一台服务器上有多个网卡,不同网卡连接不同的网络,如果服务绑定到 0.0.0.0 就可以保证服务在多个 IP 地址上都可以用。


子网掩码的作用就是帮忙找到对应的子网

7. IPv6协议:Tunnel技术是什么

IPv4 用 32 位整数描述地址,最多只能支持 43 亿设备,显然是不够用的,这也被称作 IP 地址耗尽问题。

为了解决这个问题,有一种可行的方法是拆分子网。拆分子网,会带来很多问题,比如说内外网数据交互,需要网络地址转换协议(NAT 协议),增加传输成本。再比如说,多级网络会增加数据的路由和传输链路,降低网络的速度。理想的状态当然是所有设备在一个网络中,互相可以通过地址访问。

为了解决这个问题,1998 年互联网工程工作小组推出了全新款的 IP 协议——IPv6 协议。但是目前 IPv6 的普及程度还不够高.

既然不能做到完全普及,也就引出了关联的一道面试题目:什么是 Tunnel 技术?

IPv6 的工作原理和 IPv4 类似,分成切片(Segmentation)增加封包头路由(寻址) 这样几个阶段去工作。

IPv6 同样接收上方主机到主机(Host-to-Host)协议传递来的数据,比如一个 TCP 段(Segment),然后将 TCP 段再次切片做成一个个的 IPv6 封包(Datagram or Packet),再调用底层局域网能力(数据链路层)传输数据。

具体的过程如下图所示:

datatrans

区别 IPv4 IPv6
位数 4个8位(octeat),共32位 8个16位(hextet),共128位
分割 用.分割,如103.28.7.35 用:分割,如0123:4567:89ab:cdef:0123:4567:89ab:cde

IPv6 的寻址,和 IPv4 相同,寻址的目的是找到设备,以及规划到设备途经的路径。与IPv4 相同,IPv6寻址最核心的内容就是要对网络进行划分。IPv6 地址很充裕,因此对网络的划分和 IPv4 有很显著的差异。

IPv6 的寻址分类包括:全局单播寻址本地单播分组多播任意播

全局单播寻址:就是将消息从一个设备传到另一个设备,目标就是定位网络中的设备(和 IPv4 地址作用差不多,在互联网中通过地址查找一个设备,简单来说,单播就是 1 对 1)只不过格式略有差异。总的来说,IPv6 地址太多,因此不再需要子网掩码,而是直接将 IPv6 的地址分区即可;在实现全局单播时,IPv6 地址通常分成 3 个部分:

  • 站点前缀(Site Prefix)48bit,一般是由 ISP(Internet Service Providor,运营商)或者RIR(Regional Internet Registry, 地区性互联网注册机构),RIR 将 IP 地址分配给运营商;
  • 子网号(Subnet ID),16bit,用于站点内部区分子网;
  • 接口号(Interface ID), 64bit,用于站点内部区分设备

IPv6addressing

因此 IPv6 也是一个树状结构,站点前缀需要一定资质,子网号和接口号内部定义。IPv6 的寻址过程就是先通过站点前缀找到站点,然后追踪子网,再找到接口(也就是设备的网卡)。

本地单播(类似 IPv4 里的一个内部网络,要求地址必须以fe80开头,类似我们 IPv4 中127开头的地址);理论上,虽然 IPv6 可以将所有的设备都连入一个网络。但在实际场景中,很多公司还是需要一个内部网络的。这种情况在 IPv6 的设计中属于局域网络。

在局域网络中,实现设备到设备的通信,就是本地单播。IPv6 的本地单播地址组成如下图所示:
IPv6addressing

这种协议比较简单,本地单播地址必须以fe80开头,后面 64 位的 0,然后接上 54 位的设备编号。上图中的 Interface 可以理解成网络接口,其实就是网卡

分组多播(Group Multicast),类似今天我们说的广播,将消息发送给多个接收者;有时候,我们需要实现广播。所谓广播,就是将消息同时发送给多个接收者。IPv6 中设计了分组多播,来实现广播的能力。当 IP 地址以 8 个 1 开头,也就是ff00开头,后面会跟上一个分组的编号时,就是在进行分组多播。这个时候,我们需要一个广播设备,在这个设备中已经定义了这些分组编号,并且拥有分组下所有设备的清单,这个广播设备会帮助我们将消息发送给对应分组下的所有设备。

任意播(Anycast),,本质是将消息发送给多个接收方,并选择一条最优的路径。这样说有点抽象,比如说在一个网络中有多个授时服务,这些授时服务都共享了一个任播地址。当一个客户端想要获取时间,就可以将请求发送到这个任播地址。客户端的请求扩散出去后,可能会找到授时服务中的一个或者多个,但是距离最近的往往会先被发现。这个时候,客户端就使用它第一次收到的授时信息修正自己的时间。

IPv6 和 IPv4 的兼容:目前 IPv6 还没有完全普及,大部分知名的网站都是同时支持 IPv6 和 IPv4。这个时候我们可以分成 2 种情况讨论:

  • 一个 IPv4 的网络和一个 IPv6 的网络通信;
    1. 客户端通过 DNS64 服务器查询 AAAA 记录。DNS64 是国际互联网工程任务组(IETF)提供的一种解决 IPv4 和 IPv6 兼容问题的 DNS 服务。这个 DNS 查询服务会把 IPv4 地址和 IPv6 地址同时返回。
    2. DNS64 服务器返回含 IPv4 地址的 AAAA 记录。
    3. 客户端将对应的 IPv4 地址请求发送给一个 NAT64 路由器
    4. 由这个 NAT64 路由器将 IPv6 地址转换为 IPv4 地址,从而访问 IPv4 网络,并收集结果。
    5. 消息返回到客户端。
  • 一个 IPv6 的网络和一个 IPv6 的网络通信,但是中间需要经过一个 IPv4 的网络
    • 这种情况在普及 IPv6 的过程中比较常见,IPv6 的网络一开始是一个个孤岛,IPv6 网络需要通信,就需要一些特别的手段。
    • 隧道的本质就是在两个 IPv6 的网络出口网关处,实现一段地址转换的程序

IPv64

IPv64

总结: IPv6 解决的是地址耗尽的问题。因为解决了地址耗尽的问题,所以很多其他问题也得到了解决,比如说减少了子网,更小的封包头部体积,最终提升了性能等。


问题:Tunnel 技术是什么

Tunnel 就是隧道,这和现实中的隧道是很相似的。隧道不是只有一辆车通过,而是每天都有大量的车辆来来往往。两个网络,用隧道连接,位于两个网络中的设备通信,都可以使用这个隧道。隧道是两个网络间用程序定义的一种通道。具体来说,如果两个 IPv6 网络被 IPv4 分隔开,那么两个 IPv6 网络的出口处(和 IPv4 网络的网关处)就可以用程序(或硬件)实现一个隧道,方便两个网络中设备的通信。


IPv6 和 IPv4 究竟有哪些区别

IPv6 和 IPv4 最核心的区别是地址空间大小不同。IPv6 用 128 位地址,解决了 IP 地址耗尽问题。因为地址空间大小不同,它们对地址的定义,对路由寻址策略都有显著的差异。

在路由寻址策略上,IPv6 消除了设备间地址冲突的问题,改变了划分子网的方式。在 IPv4 网络中,一个局域网往往会共享一个公网 IP,因此需要 NAT 协议和外网连接。

在划分子网的时候,IPv4 地址少,需要子网掩码来处理划分子网。IPv6 有充足的地址,因此不需要局域网共享外网 IP。也正因为 IPv6 地址多,可以直接将 IPv6 地址划分成站点、子网、设备,每个段都有充足的 IP 地址。

因为 IPv6 支持的 IP 地址数量大大上升,一个子网可以有 248 个 IP 地址,这个子网可能是公司网络、家庭网络等。这样 IP 地址的分配方式也发生了变化,IPv4 网络中设备分配 IP 地址的方式是中心化的,由 DHCP(动态主机协议)为局域网中的设备分配 IP 地址。而在 IPv6 网络中,因为 IP 地址很少发生冲突,可以由设备自己申请自己的 IP 地址。

另外因为 IPv6 中任何一个节点都可以是一个组播节点,这样就可以构造一个对等的网络,也就是可以支持在没有中心化的路由器,或者一个网络多个路由器的情况下工作。节点可以通过向周围节点类似打探消息的方式,发现更多的节点。这是一个配套 IPv6 的能力,叫作邻居发现(ND)。


8. 局域网:NAT是如何工作的

广域网是由很多的局域网组成的,比如公司网络、家庭网络、校园网络等。之前我们一直在讨论广域网的设计,今天我们到微观层面,看看局域网是如何工作的。

IPv4 的地址不够,因此需要设计子网。当一个公司申请得到一个公网 IP 后,会在自己的公司内部设计一个局域网。这个局域网所有设备的 IP 地址,通常会以 192.168 开头。

假设小明,上班时间玩王者荣耀。当他用 UDP 协议向王者荣耀的服务器发送信息时,消息的源 IP 地址是一个内网 IP 地址,而王者荣耀的服务,是一个外网 IP 地址。

数据到王者荣耀服务器可以通过寻址和路由找到目的地,但是数据从王者荣耀服务器回来的时候,王者荣耀服务器如何知道192.168开头的地址应该如何寻址呢?

要想回答这个问题,就涉及网络地址转换协议(NAT 协议)。


内部网络和外部网络
对一个组织、机构、家庭来说,我们通常把内部网络称为局域网,外部网络就叫作外网。下图是一个公司多个部门的网络架构。

companynetwork

我们会看到外网通过路由器接入整个公司的局域网,和路由器关联的是三台交换机,代表公司的三个部门。交换机,或者称为链路层交换机,通常工作在链路层;而路由器通常也具有交换机的能力,工作在网络层和链路层。

光纤是一种透明的导光介质,多束光可以在一个介质中并行传播,不仅信号容量大,重量轻,并行度高而且传播距离远。当然,光纤不能弯曲,因此办公室里用来连接交换机和个人电脑的线路肯定不能是光纤,光线通常都用于主干网络。


局域网数据交换(MAC 地址)同一个局域网中的设备如何交换消息。

首先,先明确一个概念,设备间通信的本质其实是设备拥有的网络接口(网卡)间的通信。为了区别每个网络接口,互联网工程任务组(IETF)要求每个设备拥有一个唯一的编号,这个就是 MAC 地址

IP 地址不也是唯一的吗?其实不然,一旦设备更换位置,比如你把你的电脑从北京邮寄的广州,那么 IP 地址就变了,而电脑网卡的 MAC 地址不会发生变化。总的来说,IP 地址更像现实生活中的地址,而 MAC 地址更像你的身份证号。

然后,再明确另一个基本的概念。在一个局域网中,我们不可以将消息从一个接口(网卡)发送到另一个接口(网卡),而是要通过交换机。为什么是这样呢?因为两个网卡间没有线啊!所以数据交换,必须经过交换机,因为线路都是由网卡连接交换机的

总结:数据的发送方,将自己的 MAC 地址目的地 MAC 地址,以及数据作为一个 分组(Packet),也称作 Frame 或者封包,发送给交换机。交换机再根据目的地 MAC 地址,将数据转发到目的地的网络接口(网卡)。

最后一个问题,这个分组或者 Frame,是不是 IP 协议的分组呢?不是,这里提到的是链路层的数据交换,它支持 IP 协议工作,是网络层的底层。所以,如果 IP 协议要传输数据,就要将数据转换成为链路层的分组,然后才可以在链路层传输

链路层分组大小受限于链路层的网络设备、线路以及使用了链路层协议的设计。你有时候可能会看到 **MTU 这个缩写词,它指的是 Maximun Transmission Unit,最大传输单元,意思是链路层网络允许的最大传输数据分组的大小。因此IP 协议要根据 MTU 拆分封包**。

介绍 TCP 协议滑动窗口的时候,还提到过一个词,叫作 MSS,这里我们复习下,MSS(Maximun Segment Size,最大段大小)是 TCP 段,或者称为 TCP 分组(TCP Packet)的最大大小。MSS 是传输层概念,MTU 是链路层概念,因此,它们的关系如下是对的吗?

1
MTU = MSS + TCP Header + IP Header

这个思路有一定道理,但是不对。先说说这个思路怎么来的,你可能会这么思考:TCP 传输数据大于 MSS,就拆包。每个封包加上 TCP Header ,之后经过 IP 协议,再加上 IP Header。于是这个加上 IP 头的分组(Packet)不能超过 MTU。固然这个思路很有道理,可惜是错的。因为 TCP 解决的是广域网的问题,MTU 是一个链路层的概念,要知道不同网络 MTU 是不同的,所以二者不可能产生关联。这也是为什么 IP 协议还可能会再拆包的原因。


地址解析协议(ARP)

链路层通过 MAC 地址定位网络接口(网卡)。在一个网络接口向另一个网络接口发送数据的时候,至少要提供这样 3 个字段:

  1. 源 MAC 地址
  2. 目标 MAC 地址
  3. 数据

这里我就要思考一个问题,对于一个网络接口,它如何能知道目标接口的 MAC 地址呢? 我们在使用传输层协议的时候,清楚地知道目的地的 IP 地址,但是我们不知道 MAC 地址。这个时候,就需要一个中间服务帮助根据 IP 地址找到 MAC 地址——这就是地址解析协议(Address Resolution Protocol,ARP)

整个工作过程和 DNS 非常类似,如果一个网络接口已经知道目标 IP 地址对应的 MAC 地址了,它会将数据直接发送给交换机,交换机将数据转发给目的地,这个过程如下图所示:

companynetwork

如果网络接口不知道目的地地址呢?这个时候,地址解析协议就开始工作了。发送接口会发送一个广播查询给到交换机,交换机将查询转发给所有接口。

companynetwork

如果某个接口发现自己就是对方要查询的接口,则会将自己的 MAC 地址回传。接下来,会在交换机和发送接口的 ARP 表中,增加一个缓存条目。也就是说,接下来发送接口再次向 IP 地址 2.2.2.2 发送数据时,不需要再广播一次查询了。

companynetwork

前面提到这个过程和 DNS 非常相似,采用的是逐级缓存的设计减少 ARP 请求。发送接口先查询本地的 ARP 表,如果本地没有数据,然后广播 ARP 查询。这个时候如果交换机中有数据,那么查询交换机的 ARP 表;如果交换机中没有数据,才去广播消息给其他接口。注意,ARP 表是一种缓存,也要考虑缓存的设计。通常缓存的设计要考虑缓存的

  • **失效时间(Time to Live)**,为每个缓存条目增加一个失效时间
  • 更新策略,可以考虑利用 老化(Aging)算法 模拟LRU
  • 数据结构

最后请你思考路由器和交换机的异同点。不知道你有没有在网上订购过家用无线路由器,通常这种家用设备也会提供局域网,具备交换机的能力。同时,这种设备又具有路由器的能力。所以,很多同学可能会分不清路由器和交换机。

总的来说,家用的路由器,也具备交换机的功能。但是当 ARP 表很大的时候,就需要专门的、能够承载大量网络接口的交换设备。就好比,如果用数组实现 ARP 表,数据量小的时候,遍历即可;但如果数据量大的话,就需要设计更高效的查询结构和设计缓存【重学操作系统:存储器分级:L1 Cache比内存和SSD快多少倍;内存管理单元:什么情况下使用大内存分页;缓存置换算法:LRU用什么数据结构实现更合理】。


连接内网,有时候,公司内部有多个子网。这个时候一个子网如果要访问另一个子网,就需要通过路由器。也就是说,图中的路由器,其实充当了两个子网通信的桥梁。在上述过程中,发送接口不能直接通过 MAC 地址发送数据到接收接口,因为子网 1 的交换机不知道子网 2 的接口。这个时候,发送接口需要通过 IP 协议,将数据发送到路由器,再由路由器转发信息到子网 2 的交换机。这里提一个问题,子网 2 的交换机如何根据 IP 地址找到接收接口呢?答案是通过查询 ARP 表

innet_connection


连接外网(网络地址转换技术,NAT),IPv4 协议因为存在网络地址耗尽的问题,不能为一个公司提供足够的地址,因此内网 IP 可能会和外网重复。比如内网 IP 地址192.168.0.1发送信息给22.22.22.22,这个时候,其实是跨着网络的。

outernet_connection

跨网络必然会通过多次路由,最终将消息转发到目的地。但是这里存在一个问题,寻找的目标 IP 地址22.22.22.22是一个公网 IP,可以通过正常的寻址 + 路由算法定位。当22.22.22.22寻找192.168.0.1的时候,是寻找一个私网 IP,这个时候是找不到的。解决方案就是网络地址转换技术(Network Address Translation)

NAT 技术转换的是 IP 地址,私有 IP 通过 NAT 转换为公网 IP 发送到服务器。服务器的响应,通过 NAT 转换为私有 IP,返回给客户端。通过这种方式,就解决了内网和外网的通信问题。

NAT

总结:链路层发送数据靠的是 MAC 地址,MAC 地址就好像人的身份证一样。局域网中,数据不可能从一个终端直达另一个终端,而是必须经过交换机交换。

交换机也叫作链路层交换机,它的工作就是不断接收数据,然后转发数据。通常意义上,交换机不具有路由功能,路由器往往具有交换功能。但是往往路由器交换的效率,不如交换机。

已知 IP 地址,找到 MAC 地址的协议,叫作地址解析协议(ARP)

网络和网络的衔接,必须有路由器(或者等价的设备)。一个网络的设备不能直接发送链路层分组给另一个网络的设备,而是需要通过 IP 协议让路由器转发。


问题: 网络地址转换协议是如何工作的?

  • 网络地址解析协议(NAT)解决的是内外网通信的问题。
  • NAT 通常发生在内网和外网衔接的路由器中,由路由器中的 NAT 模块提供网络地址转换能力。

从设计上看,NAT 最核心的能力,就是能够将内网中某个 IP 地址映射到外网 IP,然后再把数据发送给外网的服务器。当服务器返回数据的时候,NAT 又能够准确地判断外网服务器的数据返回给哪个内网 IP。

NAT 是能做到上面这一点,需要做两件事:

  • NAT 需要作为一个中间层替换 IP 地址。 发送的时候,NAT 替换源 IP 地址(也就是将内网 IP 替换为出口 IP);接收的时候,NAT 替换目标 IP 地址(也就是将出口 IP 替换回内网 IP 地址)。
  • NAT 需要缓存内网 IP 地址和出口 IP 地址 + 端口的对应关系。也就是说,发送的时候,NAT 要为每个替换的内网 IP 地址分配不同的端口,确保出口 IP 地址+ 端口的唯一性,这样当服务器返回数据的时候,就可以根据出口 IP 地址 + 端口找到内网 IP。

IPv6 协议还需要 NAT 吗?

  • IPv6 解决了 IP 耗尽的问题,为机构、组织、公司、家庭等网络提供了充足的 IP 资源,从这个角度看是不是就不需要 NAT 协议了呢?
  • 在没有 IPv6 之前,NAT 是 IP 资源耗尽的主流解决方案。在一个内网中的全部设备通过 NAT 协议共 享一个外网的 IPv4 地址,是目前内外网对接的主要方式。IPv6 地址资源充足,可以给全球每个设备一个独立的地址。从这个角度看 IPv6 的确不需要 NAT 协议。
  • 但是目前的情况,是 IPv6 网络还没有完全普及。尽管很多公司已经支持自己的互联网产品可以使用 IPv6 访问,但是公司内部员工使用的内部网络还是 IPv4。如果要连接 IPv6 和 IPv4 网络,仍然需要 NAT 协议(NAT64),这个协议可以让多个 IPv6 的设备共享一个 IPv4 的公网地址。

9. TCP实战:如何进行TCP抓包调试[略]

网络编程

  • 围绕Socket讨论网络编程
  • 各种网络I/O模型和编程方式的优缺点
  • 以RPC框架设计为题落地学到的知识和实现

做网络编程的时候都会碰到 Socket 对象 ,或者在配置代理的时候, 碰到配置 Socket 地址。 还经常会碰到 I/O 模型、异步编程、内存映射等概念。再往更深层次学习, 还会碰到 epoll/select 等编程模型。

有没有一种一团糟的感觉——其实学习好这些知识有一条主线,就是抓住操作系统对 Socket 文件的设计。

10. Socket编程:epoll为什么用红黑树

Socket 是一种编程的模型

下图中,从编程的角度来看,客户端将数据发送给在客户端侧的Socket 对象,然后客户端侧的 Socket 对象将数据发送给服务端侧的 Socket 对象。

Socket 对象负责提供通信能力,并处理底层的 TCP 连接/UDP 连接。

对服务端而言,每一个客户端接入,就会形成一个和客户端对应的 Socket 对象,如果服务器要读取客户端发送的信息,或者向客户端发送信息,就需要通过这个客户端 Socket 对象。

socket

但是如果从另一个角度去分析,Socket 还是一种文件,准确来说是一种双向管道文件

管道文件: 管道会将一个程序的输出,导向另一个程序的输入
双向管道文件呢:双向管道文件连接的程序是对等的,都可以作为输入和输出

1
2
var serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(80));

上面代码,创建的是一个服务端 Socket 对象,但如果单纯看这个对象,它又代表什么呢?如果我们理解成代表服务端本身合不合理呢——这可能会比较抽象,在服务端存在一个服务端 Socket。但如果我们从管道文件的层面去理解它,就会比较容易了。其一,这是一个文件;其二,它里面存的是所有客户端 Socket 文件的文件描述符。

当一个客户端连接到服务端的时候,操作系统就会创建一个客户端 Socket 的文件。然后操作系统将这个文件的文件描述符写入服务端程序创建的服务端 Socket 文件中。服务端 Socket 文件,是一个管道文件。如果读取这个文件的内容,就相当于从管道中取走了一个客户端文件描述符

socket1

如上图所示,服务端 Socket 文件相当于一个客户端 Socket 的目录,线程可以通过 accept() 操作每次拿走一个客户端文件描述符。拿到客户端文件描述符,就相当于拿到了和客户端进行通信的接口。

前面我们提到 Socket 是一个双向的管道文件,当线程想要读取客户端传输来的数据时,就从客户端 Socket 文件中读取数据;当线程想要发送数据到客户端时,就向客户端 Socket 文件中写入数据。客户端 Socket 是一个双向管道,操作系统将客户端传来的数据写入这个管道,也将线程写入管道的数据发送到客户端。

总结下,Socket 首先是文件,存储的是数据。

对服务端而言,分成服务端 Socket 文件和客户端 Socket 文件。

  • 服务端 Socket 文件存储的是客户端 Socket 文件描述符;
  • 客户端 Socket 文件存储的是传输的数据。

读取客户端 Socket 文件,就是读取客户端发送来的数据;写入客户端文件,就是向客户端发送数据。对一个客户端而言, Socket 文件存储的是发送给服务端(或接收的)数据。

综上,Socket 首先是文件,在文件的基础上,又封装了一段程序,这段程序提供了 API 负责最终的数据传输。


服务端 Socket 的绑定

  • Nginx监听80端口
  • Node监听3000端口
  • SSH监听22端口
  • Tomcat监听8080端口

对于一个服务端 Socket 文件,我们要设置它监听的端口。比如 Nginx 监听 80 端口、Node 监听 3000 端口、SSH 监听 22 端口、Tomcat 监听 8080 端口。端口监听不能冲突,不然客户端连接进来创建客户端 Socket 文件,文件描述符就不知道写入哪个服务端 Socket 文件了。

这样操作系统就会把连接到不同端口的客户端分类,将客户端 Socket 文件描述符存到对应不同端口的服务端 Socket 文件中。

因此,服务端监听端口的本质,是将服务端 Socket 文件和端口绑定,这个操作也称为 bind。有时候我们不仅仅绑定端口,还需要绑定 IP 地址。这是因为有时候我们只想允许指定 IP 访问我们的服务端程序。


扫描和监听

对于一个服务端程序,可以定期扫描服务端 Socket 文件的变更,来了解有哪些客户端想要连接进来。如果在服务端 Socket 文件中读取到一个客户端的文件描述符,就可以将这个文件描述符实例化成一个 Socket 对象。

socket2

之后,服务端可以将这个 Socket 对象加入一个容器(集合),通过定期遍历所有的客户端 Socket 对象,查看背后 Socket 文件的状态,从而确定是否有新的数据从客户端传输过来。

socket3

上述的过程,我们通过一个线程就可以响应多个客户端的连接,也被称作I/O 多路复用技术


响应式(Reactive)
在 I/O 多路复用技术中,服务端程序(线程)需要维护一个 Socket 的集合(可以是数组、链表等),然后定期遍历这个集合。这样的做法在客户端 Socket 较少的情况下没有问题,但是如果接入的客户端 Socket 较多,比如达到上万,那么每次轮询的开销都会很大。

从程序设计的角度来看,像这样主动遍历,比如遍历一个 Socket 集合看看有没有发生写入(有数据从网卡传过来),称为命令式的程序。这样的程序设计就好像在执行一条条命令一样,程序主动地去查看每个 Socket 的状态。

socket_reactive

命令式会让负责下命令的程序负载过重,例如,在高并发场景下,上述讨论中循环遍历 Socket 集合的线程,会因为负担过重导致系统吞吐量下降。

命令式相反的是响应式(Reactive),响应式的程序就不会有这样的问题。在响应式的程序当中,每一个参与者有着独立的思考方式,就好像拥有独立的人格,可以自己针对不同的环境触发不同的行为。

从响应式的角度去看 Socket 编程,应该是有某个观察者会观察到 Socket 文件状态的变化,从而通知处理线程响应。线程不再需要遍历 Socket 集合,而是等待观察程序的通知。

socket_reactive2

最合适的观察者其实是操作系统本身,只有操作系统非常清楚每一个 Socket 文件的状态。原因是对 Socket 文件的读写都要经过操作系统。在实现这个模型的时候,有几件事情要注意。

  • 线程需要告诉中间的观察者自己要观察什么,或者说在什么情况下才响应?比如具体到哪个 Socket 发生了什么事件?是读写还是其他的事件?这一步我们通常称为注册。
  • 中间的观察者需要实现一个高效的数据结构(通常是基于红黑树的二叉搜索树)。这是因为中间的观察者不仅仅是服务于某个线程,而是服务于很多的线程。当一个 Socket 文件发生变化的时候,中间观察者需要立刻知道,究竟是哪个线程需要这个信息,而不是将所有的线程都遍历一遍

问题:为什么用红黑树?

关于为什么要红黑树, 再仔细解释一下。考虑到中间观察者最核心的诉求有两个。

  • 第一个核心诉求,是让线程可以注册自己关心的消息类型。

比如线程对文件描述符 =123 的 Socket 文件读写都感兴趣,会去中间观察者处注册。当 FD=123 的 Socket 发生读写时,中间观察者负责通知线程,这是一个响应式的模型。

  • 第二个核心诉求,是当 FD=123 的 Socket 发生变化(读写等)时,能够快速地判断是哪个线程需要知道这个消息

所以,中间观察者需要一个快速能插入(注册过程)、查询(通知过程)一个整数的数据结构,这个整数就是 Socket 的文件描述符。综合来看,能够解决这个问题的数据结构中,跳表和二叉搜索树都是不错的选择。

因此,在 Linux 的 epoll 模型中,选择了红黑树。红黑树是二叉搜索树的一种,红与黑是红黑树的实现者才关心的内容,对于我们使用者来说不用关心颜色,Java 中的 TreeMap 底层就是红黑树


总结:Socket 既是一种编程模型,或者说是一段程序,同时也是一个文件,一个双向管道文件。可以理解,Socket API 是在 Socket 文件基础上进行的一层封装,而 Socket 文件是操作系统提供支持网络通信的一种文件格式。

在服务端有两种 Socket 文件,每个客户端接入之后会形成一个客户端的 Socket 文件,客户端 Socket 文件的文件描述符会存入服务端 Socket 文件。通过这种方式,一个线程可以通过读取服务端 Socket 文件中的内容拿到所有的客户端 Socket。这样一个线程就可以负责响应所有客户端的 I/O,这个技术称为 I/O 多路复用。

主动式的 I/O 多路复用,对负责 I/O 的线程压力过大,因此通常会设计一个高效的中间数据结构作为 I/O 事件的观察者,线程通过订阅 I/O 事件被动响应,这就是响应式模型。在 Socket 编程中,最适合提供这种中间数据结构的就是操作系统的内核,事实上 epoll 模型也是在操作系统的内核中提供了红黑树结构。


问题: epoll为什么是红黑树

在 Linux 的设计中有三种典型的 I/O 多路复用模型 select、poll、epoll。

  • select 是一个主动模型,需要线程自己通过一个集合存放所有的 Socket,然后发生 I/O 变化的时候遍历。在 select 模型下,操作系统不知道哪个线程应该响应哪个事件,而是由线程自己去操作系统看有没有发生网络 I/O 事件,然后再遍历自己管理的所有 Socket,看看这些 Socket 有没有发生变化。
  • poll 提供了更优质的编程接口,但是本质和 select 模型相同。因此千级并发以下的 I/O,你可以考虑 select 和 poll,但是如果出现更大的并发量,就需要用 epoll 模型。
  • epoll 模型在操作系统内核中提供了一个中间数据结构,这个中间数据结构会提供事件监听注册,以及快速判断消息关联到哪个线程的能力(红黑树实现)。因此在高并发 I/O 下,可以考虑 epoll 模型,它的速度更快,开销更小。

11. 流和缓冲区:缓冲区的 flip 是怎么回事?

流和缓冲区都是用来描述数据的。

计算机中,数据往往会被抽象成流,然后传输。比如读取一个文件,数据会被抽象成文件流;播放一个视频,视频被抽象成视频流。处理节点为了防止过载,又会使用缓冲区削峰(减少瞬间压力)。在传输层协议当中,应用往往先把数据放入缓冲区,然后再将缓冲区提供给发送数据的程序。发送数据的程序,从缓冲区读取出数据,然后进行发送。


流代表数据,具体来说是随着时间产生的数据,类比自然界的河流。你不知道一个流什么时候会完结,直到你将流中的数据都读完。

读取文件的时候,文件被抽象成流。流的内部构造,决定了你每次能从文件中读取多少数据。从流中读取数据的操作,本质上是一种迭代器。流的内部构造决定了迭代器每次能读出的数据规模。比如你可以设计一个读文件的流,每次至少会读出 4k 大小,也可以设计一个读文件的程序,每次读出一个字节大小。

通常情况读取数据的流,是读取流;写入数据的流,是写入流。那么一个写入流还能被理解成随着时间产生的数据吗?其实是一样的,随着时间产生的数据,通过写入流写入某个文件,或者被其他线程、程序拿走使用。

思考一个问题:流中一定有数据吗?

看上去的确是这样。对于文件流来说,打开一个文件,形成读取流。读取流的本质当然是内存中的一个对象。当用户读取文件内容的时候,实际上是通过流进行读取,看上去好像从流中读取了数据,而本质上读取的是文件的数据。从这个角度去观察整体的设计,数据从文件到了流,然后再到了用户线程,因此数据是经过流的。

但是仔细思考这个问题,可不可以将数据直接从文件传输到用户线程呢?比如流对象中只设计一个整数型指针,一开始指向文件的头部,每次发生读取,都从文件中读出内容,然后再返回给用户线程。做完这次操作,指针自增。通过这样的设计,流中就不需要再有数据了。可见,流中不一定要有数据。再举一个极端的例子,如果我们设计一个随机数的产生流,每次读取流中的数据,都调用随机数函数生成一个随机数并返回,那么流中也不需要有数据的存储。


为什么要缓冲区?
在上面的例子当中,我们讨论的时候发现,设计文件流时,可以只保留一个位置指针,不用真的将整个文件都读入内存,像下图这样:

stram

把文件看作是一系列线性排列连续字节的合集,用户线程调用流对象的读取数据方法,每次从文件中读取一个字节。流中只保留一个读取位置 position,指向下一个要读取的字节。

看上去这个方案可行,但实际上性能极差。因为从文件中读取数据这个操作,是一次磁盘的 I/O 操作,非常耗时。正确的做法是每次读取 2k、4k 这样大小的数据,这是因为操作系统中的内存分页通常是这样的大小,而磁盘的读写往往是会适配页表大小。而且现在的文件系统主要都是日志文件系统,存储的并不是原始数据本身,也就是说多数情况下你看到的文件并不是一个连续紧密的字节线性排列,而是日志。

当你向磁盘读取 2k 数据,读取到的不一定是 2k 实际的数据,很有可能会比 2k 少,这是因为文件内容是以日志形式存储,会有冗余

stram

如上图所示,内核每次从文件系统中读取到的数据是确定的,但是里边的有效数据是不确定的。

流对象的设计,至少应该支持两种操作:一种是读取一个字节,另一种是读取多个字节。而无论读取一个字节还是读取多个字节,都应该适配内核的底层行为。也就是说,每次流对象读取一个字节,内核可能会读取 2k、4k 的数据。这样的行为,才能真的做到减少磁盘的 I/O 操作。

那内核为什么不一次先读取几兆数据或者读取更大的数据呢?这有两个原因。

  • 如果是高并发场景下,并发读取数据时内存使用是根据并发数翻倍的,如果同时读取的数据量过大,可能会导致内存不足。
  • 读取比 2k/4k……大很多倍的数据,比如 1M/2M 这种远远大于内存分页大小的数据,并不能提升性能。

stram

上图中内核中的缓冲区,用于缓冲读取文件中的数据。流中的缓冲区,用于缓冲内核中拷贝过来的数据。

为什么不把内核的缓冲区直接给到流呢?这是因为流对象工作在用户空间,内核中的缓冲区工作在内核空间。用户空间的程序不可以直接访问内核空间的数据,这是操作系统的一种保护策略。

当然也存在一种叫作内存映射的方式,就是内核通过内存映射,直接将内核空间中的一块内存区域分享给用户空间只读使用,这样的方式可以节省一次数据拷贝。这个能力在 Java 的 NIO 中称作 DirectMemory,对应 C 语言是 mmap。


缓冲区
上面的设计中,我们已经开始用缓冲区解决问题了。那么具体什么是缓冲区呢?缓冲区就是一块用来做缓冲的内存区域。在上面的例子当中,为了应对频繁的字节读取,我们在内存当中设置一个 2k 大小缓冲区。这样读取 2048 次,才会真的发生一次读取。同理,如果应对频繁的字节写入,也可以使用缓冲区。

不仅仅如此,比如说你设计一个秒杀系统,如果同时到达的流量过高,也可以使用缓冲区将用户请求先存储下来,再进行处理。这个操作我们称为削峰,削去流量的峰值。

缓冲区中的数据通常具有朴素的公平,说白了就是排队,先进先出(FIFO)。从数据结构的设计上,缓冲区像一个队列。在实际的使用场景中,缓冲区有一些自己特别的需求,比如说缓冲区需要被重复利用。多次读取数据,可以复用一个缓冲区,这样可以节省内存,也可以减少分配和回收内存的开销。

文件流 -> 缓冲区 -> 网络流

举个例子:读取一个流的数据到一个缓冲区,然后再将缓冲区中的数据交给另一个流。 比如说读取文件流中的数据交给网络流发送出去。首先,我们要将文件流的数据写入缓冲区,然后网络流会读取缓冲区中的数据。这个过程会反反复复进行,直到文件内容全部发送。

这个设计中,缓冲区需要支持这几种操作:

  • 写入数据
  • 读出数据
  • 清空(应对下一次读写)

那么具体怎么设计这个缓冲区呢?如下图所示:

buffering

将 position 设置为 0,limit 不变的操作称为flip操作,flip 本意是翻转,在这个场景中是读、写状态的切换。

读取操作可以控制循环从 position 一直读取到 limit,这样就可以读取出 a,b,c,d。那么如果要继续写入应该如何操作呢? 这个时候就需要用到缓冲区的clear操作,这个操作会清空缓冲区。具体来说,clear操作会把 position,limit 都设置为 0,而不需要真的一点点擦除缓冲区中已有的值,就可以做到重复利用缓冲区了。

写入过程从 position = 0 开始,position 和 limit 一起自增。读取时,用flip操作切换缓冲区读写状态。读取数据完毕,用clear操作重置缓冲区状态。


总结: 流是随着时间产生的数据。数据抽象成流,是因为客观世界存在着这样的现象。数据被抽象成流之后,我们不需要把所有的数据都读取到内存当中进行计算和迭代,而是每次处理或者计算一个缓冲区的数据。

缓冲区的作用是缓冲,它在高频的 I/O 操作中很有意义。针对不同场景,也不只有这一种缓冲区的设计,比如用双向链表实现队列(FIFO 结构)可以作为缓冲区Redis 中的列表可以作为缓冲区RocketMQ,Kafka 等也可以作为缓冲区。针对某些特定场景,比如高并发场景下的下单处理,可能会用订单队列表(MySQL 的表)作为缓冲区。

因此从这个角度来说,作为开发者我们首先要有缓冲的意识,去减少 I/O 的次数,提升 I/O 的性能,然后才是思考具体的缓冲策略。


问题:在缓存区的设计中,还通常有一个rewind操作,这个操作用来做什么的。

**12. 网络I/O模型:BIO, NIO和AIO有什么区别

在处理 I/O 的时候,要结合具体的场景来思考程序怎么写。从程序的 API 设计上,我们经常会看到 3 类设计:BIO、NIO 和 AIO 。

从本质上说,讨论 BIO、NIO、AIO 的区别,其实就是在讨论 I/O 的模型,我们可以从下面 3 个方面来思考 。

  • 编程模型:合理设计 API,让程序写得更舒服。
  • 数据的传输和转化成本:比如减少数据拷贝次数,合理压缩数据等。
  • 高效的数据结构:利用好缓冲区、红黑树等

I/O编程模型

BIO(Blocking I/O,阻塞 I/O),API 的设计会阻塞程序调用。

NIO(None Blocking I/O, 非阻塞I/O),API的设计不会阻塞程序的调用

比如:

1
byte a = readKey()

假设readKey方法从键盘读取一个按键,如果是非阻塞 I/O 的设计,readKey不会阻塞当前的线程。你可能会问:那如果用户没有按键怎么办?在阻塞 I/O 的设计中,如果用户没有按键线程会阻塞等待用户按键,在非阻塞 I/O 的设计中,线程不会阻塞,没有按键会返回一个空值,比如 null。

线程的上下文切换(Context Switch): 从一个线程执行切换到另一个线程执行

最后我们说说 AIO(Asynchronous I/O, 异步 I/O),API 的设计会多创造一条时间线。比如

1
2
3
4
func callBackFunction(byte keyCode) {
// 处理按键
}
readKey( callBackFunction )

在异步 I/O 中,readKey方法会直接返回,但是没有结果。结果需要一个回调函数callBackFunction去接收。从这个角度看,其实有两条时间线。第一条是程序的主干时间线,readKey的执行到readKey下文的程序都在这条主干时间线中。而callBackFunction的执行会在用户按键时触发,也就是时间不确定,因此callBackFunction中的程序是另一条时间线也是基于这种原因产生的,我们称作异步,异步描述的就是这种时间线上无法同步的现象,你不知道callbackFunction何时会执行。

但是我们通常说某某语言提供了异步 I/O,不仅仅是说提供上面程序这种写法,上面的写法会产生一个叫作回调地狱的问题,本质是异步程序的时间线错乱,导致维护成本较高。

1
2
3
4
5
6
7
8
9
request("/order/123", (data1) -> {
//..
request("/product/456", (data2) -> {
// ..
request("/sku/789", (data3) -> {
//...
})
})
})

比如上面这段程序(称作回调地狱)维护成本较高,因此通常提供异步 API 编程模型时,我们会提供一种将异步转化为同步程序的语法。比如下面这段伪代码:

1
2
3
4
5
6
7
8
Future future1 = request("/order/123")
Future future2 = request("/product/456")
Future future3 = request("/sku/789")
// ...
// ...
order = future1.get()
product = future2.get()
sku = future3.get()

request 函数是一次网络调用,请求订单 ID=123 的订单数据。本身 request 函数不会阻塞,会马上执行完成,而网络调用是一次异步请求,调用不会在request(“/order/123”)下一行结束,而是会在未来的某个时间结束。因此,我们用一个 Future 对象封装这个异步操作。future.get()是一个阻塞操作,会阻塞直到网络调用返回。

在request和future.get之间,我们还可以进行很多别的操作,比如发送更多的请求。 像 Future 这样能够将异步操作再同步回主时间线的操作,我们称作异步转同步,也叫作异步编程。


数据的传输和转化成本

上面我们从编程的模型上对 I/O 进行了思考,接下来我们从内部实现分析下 BIO、NIO 和 AIO。无论是哪种 I/O 模型,都要将数据从网卡拷贝到用户程序(接收),或者将数据从用户程序传输到网卡(发送)

另一方面,有的数据需要编码解码,比如 JSON 格式的数据。还有的数据需要压缩和解压。数据从网卡到内核再到用户程序是 2 次传输。注意,将数据从内存中的一个区域拷贝到另一个区域,这是一个 CPU 密集型操作。数据的拷贝归根结底要一个字节一个字节去做。

网卡 -> 内核 -> 用户程序

从网卡到内核空间的这步操作,可以用 DMA(Direct Memory Access)技术控制。DMA 是一种小型设备,用 DMA 拷贝数据可以不使用 CPU,从而节省计算资源。遗憾的是,通常我们写程序的时候,不能直接控制 DMA,因此 DMA 仅仅用于设备传输数据到内存中。

不过,从内核到用户空间这次拷贝,可以用内存映射技术,将内核空间的数据映射到用户空间

无论I/O的编程模型如何选择,数据传输和转化成本是逃不掉的,通过DMA技术内存映射技术,就可以节省成本:

  • 减少数据传输
  • 数据压缩解压
  • 数据编码解码

数据结构运用

在处理网络 I/O 问题的时候,还有一个重点问题要注意,就是数据结构的运用。

缓冲区

是一种在处理 I/O 问题中常用的数据结构,一方面缓冲区起到缓冲作用,在瞬时 I/O 量较大的时候,利用排队机制进行处理;另一方面,缓冲区起到一个批处理的作用,比如 1000 次 I/O 请求进入缓冲区,可以合并成 50 次 I/O 请求,那么整体性能就会上一个档次。

举个例子,比如你有 1000 个订单要写入 MySQL,如果这个时候你可以将这 1000 次请求合并成 50 次,那么磁盘写入次数将大大减少。同理,假设有 10000 次网络请求,如果可以合并发送,会减少 TCP 协议握手时间,可以最大程度地复用连接;另一方面,如果这些请求都较小,还可以粘包复用 TCP 段。在处理 Web 网站的时候,经常会碰到将多个 HTTP 请求合并成一个发送,从而减少整体网络开销的情况。

除了上述两方面原因,缓冲区还可以减少实际对内存的诉求。数据在网卡到内核,内核到用户空间的过程中,建议都要使用缓冲区。当收到的某个请求较大的时候,抽象成流,然后使用缓冲区可以减少对内存的使用压力。这是因为使用了缓冲区和流,就不需要真的准备和请求数据大小一致的内存空间了。可以将缓冲区大小规模的数据分成多次处理完,实际的内存开销是缓冲区的大小

I/O 多路复用模型

在运用数据结构的时候,还要思考 I/O 的多路复用用什么模型。

假设你在处理一个高并发的网站,每秒有大量的请求打到你的服务器上,你用多少个线程去处理 I/O 呢?对于没有需要压缩解压的场景,处理 I/O 的主要开销还是数据的拷贝。那么一个 CPU 核心每秒可以完成多少次数据拷贝呢?

拷贝,其实就是将内存中的数据从一个地址拷贝到另一个地址。再加上有 DMA,内存映射等技术,拷贝是非常快的。不考虑 DMA 和内存映射,一个 3GHz 主频的 CPU 每秒可以拷贝的数据也是百兆级别的。当然,速度还受限于内存本身的速度。因此总的来说,I/O 并不需要很大的计算资源。通常我们在处理高并发的时候,也不需要大量的线程去进行 I/O 处理。

对于多数应用来说,处理 I/O 的成本小于处理业务的成本。处理高并发的业务,可能需要大量的计算资源。每笔业务也可能会需要更多的 I/O,比如远程的 RPC 调用等。

因此我们在处理高并发的时候,一种常见的 I/O 多路复用模式就是由少量的线程处理大量的网络接收、发送工作。然后再由更多的线程,通常是一个线程池处理具体的业务工作。

在这样一个模式下,有一个核心问题需要解决,就是当操作系统内核监测到一次 I/O 操作发生,它如何具体地通知到哪个线程调用哪段程序呢?

这时,一种高效的模型会要求我们将线程、线程监听的事件类型,以及响应的程序注册到内核。具体来说,比如某个客户端发送消息到服务器的时候,我们需要尽快知道哪个线程关心这条消息(处理这个数据)。例如 epoll 就是这样的模型,内部是红黑树。我们可以具体地看到文件描述符构成了一棵红黑树,而红黑树的节点上挂着文件描述符对应的线程、线程监听事件类型以及相应程序。

讲了这么多,缓冲区 和 BIO、AIO、NIO 有什么关系?这里有两个联系。

首先是无论哪种编程模型都需要使用缓冲区,也就是说 BIO、AIO、NIO 都需要缓冲区,因此关系很大。在我们使用任何编程模型的时候,如果内部没有使用缓冲区,那么一定要在外部增加缓冲区。另一个联系是类似 epoll 这种注册+消息推送的方式,可以帮助我们节省大量定位具体线程以及事件类型的时间。这是一个通用技巧,并不是独有某种 I/O 模型才可以使用。

不过从能力上分析,使用类似 epoll 这种模型,确实没有必要让处理 I/O 的线程阻塞,因为操作系统会将需要响应的事件源源不断地推送给处理的线程,因此可以考虑不让处理线程阻塞(比如用 NIO)


总结: 从 3 个方面讨论了 I/O 模型。

第一个是编程模型,阻塞、非阻塞、异步 3 者 API 的设计会有比较大的差异。通常情况下我们说的异步编程是异步转同步。异步转同步最大的价值,就是提升代码的可读性。可读,就意味着维护成本的下降以及扩展性的提升。

第二个在设计系统的 I/O 时,另一件需要考虑的就是数据传输以及转化的成本。传输主要是拷贝,比如可以使用内存映射来减少数据的传输。但是这里要注意一点,内存映射使用的内存是内核空间的缓冲区,因此千万不要忘记回收。因为这一部分内存往往不在我们所使用的语言提供的内存回收机制的管控范围之内。

最后是关于数据结构的运用,针对不同的场景使用不同的缓冲区,以及选择不同的消息通知机制,也是处理高并发的一个核心问题。

从上面几个角度去看 I/O 的模型,你会发现,编程模型是编程模型、数据的传输是数据的传输、消息的通知是消息的通知,它们是不同的模块,完全可以解耦,也可以根据自己不同的业务特性进行选择。虽然在一个完整的系统设计中,往往提出的是一套完整的解决方案 ,但实际上我们还是应该将它们分开去思考,这样可以产生更好的设计思路。

问题: BIO、NIO 和 AIO 有什么区别?

总的来说,这三者是三个 I/O 的编程模型。BIO 接口设计会直接导致当前线程阻塞。NIO 的设计不会触发当前线程的阻塞。AIO 为 I/O 提供了异步能力,也就是将 I/O 的响应程序放到一个独立的时间线上去执行。但是通常 AIO 的提供者还会提供异步编程模型,就是实现一种对异步计算封装的数据结构,并且提供将异步计算同步回主线的能力。

通常情况下,这 3 种 API 都会伴随 I/O 多路复用。如果底层用红黑树管理注册的文件描述符和事件,可以在很小的开销内由内核将 I/O 消息发送给指定的线程。另外,还可以用 DMA,内存映射等方式优化 I/O。

问题: I/O 多路复用用协程和用线程的区别?

线程是执行程序的最小单位。I/O 多路复用时,会用单个线程处理大量的 I/O。还有一种执行程序的模型,叫协作程,协程是轻量级的线程。操作系统将执行资源分配给了线程,然后再调度线程运行。如果要实现协程,就要利用分配给线程的执行资源,在这之上再创建更小的执行单位。协程不归操作系统调度,协程共享线程的执行资源。

而 I/O 多路复用的意义,是减少线程间的切换成本。因此从设计上,只要是用单个线程处理大量 I/O 工作,线程和协程是一样的,并无区别。如果是单线程处理大量 I/O,使用协程也是依托协程对应线程执行能力。

13. 面试中如何回答“怎样实现RPC框架”的问题

随着微服务架构的盛行,远程调用成了开发微服务必不可少的能力,RPC 框架作为微服务体系的底层支撑,也成了日常开发的必备工具。当下,RPC 框架已经不仅是进行远程调用的基础工具,还需要提供路由、服务发现、负载均衡、容错等能力。那么今天,我们就以“怎样实现 RPC 框架”为引,从设计者角度看看如何设计一个 RPC 框架。

RPC(Remote Procedure Call)远程过程调用,顾名思义最基本的能力当然是远程调用一个过程。放到今天的面向对象的语言中,其实就是调用一个远程的方法。在远程我们必须先定义这个方法,然后才可以通过 RPC 框架调用该方法,远程调用不仅可以传参数、获取到返回值,还可以捕捉调用过程中的异常。RPC 让远程调用就像本地调用一样。

假设我们实现了一个rpc对象,其中的invoke方法可以实现远程调用。下面这段伪代码在调用远程的greetings方法(RPC 调用),并向远程方法传递参数arg1``arg2,然后再接收到远程的返回值。

1
var result = rpc.invoke("greetings", arg1, arg2, ...)

复制
这段程序将本地看作 一个 RPC 的客户端,将远程看作一个 RPC 的服务端。如下图所示:

rpc

服务 A 发起远程方法调用,RPC 客户端通过某种协议将请求发送给服务 B,服务 B 解析请求,进行本地方法的调用,将结果返回到服务 B 的 RPC 服务端,最终返回到服务 A。

对服务 A 来说,调用的是一个函数,从接口到返回值的设计,和调用本地函数并没有太大的差别。

当然,我们不能完全忽略这是一次远程方法调用,因为远程调用的开销较大。如果程序员没有意识到调用远程方法有网络开销,就可能会写出下面这段程序:

1
2
3
for(int i = 0; i < 1000000; i++) {
rpc.invoke(...)
}

之所以写出上面的程序,是因为 没有意识到 rpc.invoke 是一次远程调用。在实际的操作过程中,rpc.invoke可能被封装到了某个业务方法中,程序员调用的时候便容易忽视这是一次远程操作。所以 RPC 调用时就要求我们对性能有清晰的认识。


多路复用的优化

RPC 提供的是远程方法的调用,但本质上是数据的传递,传递数据有一个最基本的问题要处理,就是提升吞吐量(单位时间传递的数据量)

如果**为每个远程调用(请求)建立一个连接,就会造成资源的浪费,因此通常我们会考虑多个请求复用一个连接,叫作多路复用**。

在具体实现多路复用的时候,也会有不同的策略。假设我们要发送数据 A、B、C、D,那么一种方式是建立一个连接,依次将 A、B、C、D 发过去,就像下图这样:

rpc

在 A 较大的时候,B,C,D 就只能等 A 完全传送完成才能发生传送。这样的模型对于 RPC 请求/响应大小不平均的网络不太友好,体积小的请求/响应可能会因为一些大体积的请求/响应而延迟。

因此还有另一种常见的多路复用方案,就是将 A,B,C,D切片一起传输,如上图(3)所示

上图中,我们用不同颜色代表不同的传输任务。采用顺序传输方案将 A、B、C、D 用一个连接传输节省了握手、挥手的成本。切片传输的方案在这之上,将数据切片可以保证大、小任务并行,不会因为大任务阻塞小任务。

另外还有一个需要考虑的点,是单个 TCP 连接的极限传输速度受到窗口大小、缓冲区等因素的制约,不一定可以用满网络资源。如果传输量特别大的时候,有可能需要考虑提供多个连接,每个连接再去考虑多路复用的情况。


调用约定和命名

接下来,我们一起思考下服务的命名。远程调用一个函数,命名空间+类名+方法名是一个比较好的选择,简而言之,每个可以远程调用的方法就是一个字符串。

比如远程调用一个支付服务对象 PayService 的 pay 方法

  • 命名空间可能是 trade.payment
  • 对象名称是 PayService
  • 方法名称是 pay

组合起来可以是一个完整的字符串,例如用 # 分割trade.payment#PayService#pay

在进行远程调用的时候,给远程方法命名是调用约定的一部分。我们通过调用命名空间下完整的名称调用远程方法。在面向对象的语言中,还有一种常见的做法是先不具体指定调用的方法,而是先创造一个远程对象的实例。比如上面例子中我们先通过 RPC 框架构造一个 PayService 对象的实例。这里会用到一些特别的编程技巧,比如代理设计模式、动态接口生成等。

不过归根结底,我们调用的本质就是字符串名称。而实现这个调用,你需要知道两件事情:

  • IP 是多少,也就是方法在哪台机器上调用;
  • 端口是多少,也就是哪个服务提供这个调用。

注册和发现

调用的时候,我们需要根据字符串(命名)去获取 IP 和端口(机器和服务)

机器可以是虚拟机、容器、实体机,也可以是某个拥有虚拟网卡的代理。在网络的世界中,需要的只是网络接口和 IP 地址。而操作系统区分应用需要的是端口。所以,在调用过程中,我们需要的是一个注册表,存储了字符串和 IP + 端口的对应关系。

聪明的同学可能马上会想到,用 Redis 的hash对象存储这个对应关系就很不错。当我们上线一个服务的时候,就在 Redis 的某个hash对象中存储它和它对应的 IP 地址 + 端口列表。为什么是存一个列表?因为一个服务可能由多个机器提供。

通常我们**将写这个hash对象的过程,也就是服务被记录的过程称作注册。我们远程调用一个 RPC 服务的时候,调用端提供的是 RPC 服务的名称(例如:命名空间+对象+方法),根据名称查找到提供服务的 IP + 端口清单并指定某个 IP + 端口(提供服务)的过程称作发现**。

当然,我们不能就这样简单理解成:注册就是写一个共享的哈希表,发现就是查哈希表再决定服务的响应者。在实际的设计中,要考虑的因素会更多。

比如基于 Redis 的实现,如果所有 RPC 调用都需要去 Redis 查询,会造成负责发现的中间件压力较大。

  • 实际的操作过程中,往往会增加缓存。也就是 RPC 调用者会缓存上一次调用的 IP + 端口。但是这样设计,缓存又可能会和注册表之间产生数据不一致的问题。
  • 这个时候,可以考虑由分布式共识服务比如 ZooKeeper 提供订阅,让 RPC 调用者订阅到服务地址的变更,及时更新自己的缓存。

设计注册和发现两个功能的最大的价值是让客户端不再需要关注服务的部署细节,这样方便在全局动态调整服务的部署策略


负载均衡的设计

在设计 RPC 框架的时候,负载均衡器的设计往往需要和 RPC 框架一起考虑。因为 RPC 框架提供了注册、发现的能力,提供发现能力的模块本身就是一个负载均衡器。因此负载均衡可以看作发现模块的一个子组件。请求到达 RPC 的网关(或某个路由程序)后,发现组件会提供服务对应的所有实例(IP + 端口),然后负载均衡算法会指定其中一个响应这个请求。


可用性和容灾

  • 当一个服务实例崩溃的时候(不可用),因为有发现模块的存在,可以及时从注册表中删除这个服务实例。
    • 只要服务本身有足够多的实例,比如多个容器而且部署在不同的机器上,那么完全不可能用的风险会大大降低。当然,可用性是不可能 100% 实现的。
  • 另外,注册表和 RPC 调用者之间必然存在不一致现象,而且注册表的更新本身也可能滞后。比如确认一个服务有没有崩溃,可能需要一个心跳程序持续请求这个服务,因此 RPC 的调用者如果调用到一个不存在的服务,或者调用到一个发生崩溃的服务,需要自己重新去发现组件申请新的服务实例(地址 + 端口)。
  • 如果遇到临时访问量剧增,需要扩容的场景。这个时候只需要上线更多的容器,并且去注册即可。当然这要求部署模块和注册模块之间有较高的协同,这块可以用自动化脚本衔接

总结

设计一个 RPC 框架最基础的能力就是实现远程方法的调用。这里需要一个调用约定,

  • 比如怎么描述一个远程的方法,
  • 发送端怎么传递参数,
  • 接收方如何解析参数?
  • 如果发生异常应该如何处理?

具体来说,这些事情都不难实现,只是比较烦琐。

  • 其实不仅仅在 RPC 调用时有调用约定
  • 编译器在实现函数调用的时候,也会有调用约定。
  • 另外,还有一些在 RPC 基础上建立起来的更复杂、更体系化的约定,比如说面向服务架构(SOA)。

在实现了基本调用能力的基础上,**接下来就是提供服务的注册、发现能力。有了这两个能力,就可以向客户端完全屏蔽服务的部署细节,并衍生出容灾、负载均衡的设计。[我不理解]**

当然,程序员还需要思考底层具体网络的传输问题。

  • 如果用 TCP 要思考多路复用以及连接数量的问题;
  • 如果是 UDP,需要增加对于可靠性保证的思考。
  • 如果使用了消息队列,还需要考虑服务的幂等性设计等。

问题: 如何理解Dubbo的几个组成部分Consumer, Provider, Monitor和Registry?

Web技术

  • HTTP协议->Web技术生态

14. DNS域名解析系统:CNAME记录的作用是

在浏览器中输入一个 URL,或者用curl请求一个网址……域名系统(Domain Name System)就开始工作了。作为互联网的一个重要成员,域名系统是将互联网资源和地址关联起来的一个分布式数据库。


统一资源定位符(URL, Uniform Resource Locator)可以通过字符串定位一个互联网的资源,比如视频、图片、文件、网页。

下图是一个 URL 的示例:

url

  • Scheme 部分代表协议,不只有 https,还有 ftp、ssh 等。不同协议代表着不同类型的应用在提供资源。
  • Host 部分代表站点,我们今天介绍的 DNS 主要作用就是根据 Host 查找 IP 地址。
  • Port 是端口,代表提供服务的应用。
  • Path 是路径,代表资源在服务中的路径。
  • Query 是查询条件,代表需要的是资源中的某一个部分。
  • Fragment 是二级查询条件,通常不在服务端响应,而是用于前端展示定位内容。

总的来说,URL 是一种树状的设计, Host 代表主机(对应的 IP 地址由 DNS 服务提供);Port 代表应用;Path 代表资源在应用中的路径;Query 代表对资源的查询条件。通过这种设计,互联网中万亿级别的资源都可以得到有效区分。

树状的设计在今天计算机中也非常常见,比如

  • 文件目录的设计
  • 源代码块的嵌套设计
  • JSON 和 XML 的设计,都是树状关系。

域名系统 DNS(Domain Name System,域名系统)是一个将域名和 IP 地址相互映射的分布式服务

根域名服务器 (Root Name Server) 位于最顶层的是根域名服务器

DNS 本身是一个出色的分布式架构。

人们在全世界范围内搭建了多台根域名服务器,2016 年的统计数据中,全世界目前有 13 台 IPv4 根服务器,25 台 IPv6 根服务器。

根域名服务器存储的不是域名和 IP 的映射关系,而是一个目录。

如果将所有的域名记录都存放到根域名服务器,从存储量上来说,不会非常巨大。要知道一个域名记录——域名、IP 地址和额外少量信息,并不需要大量存储空间。

但是如果全世界所有的 DNS 请求都集中在少量的根服务器上,这个访问流量就会过于巨大。而且一旦发生故障,很容易导致大面积瘫痪。

而且因为根服务器较少,所以如果全部都走根服务器,不同客户端距离根服务器距离不同,感受到的延迟也不一样,这样对用户来说不太友好。

因此,因为流量、防止单点故障、平衡地理分布等问题,根域名服务器只是一个目录,并不提供具体的数据。

域名分级和数据分区

我们知道中文字典可以按照偏旁部首以及拼音索引,和字典类似,根服务器提供的目录也有一定的索引规则。

  • 在域名的世界中,通过分级域名的策略建立索引。伴随着域名的分级策略,实际上是域名数据库的拆分。
  • 通过域名的分级,可以将数据库划分成一个个区域。

平时我们看到的.com.cn.net等,称为顶级域名。比如对于 www.artisan.com 这个网址来说,com是顶级域名,artisan是二级域名,www是三级域名

域名分级当然是为了建立目录和索引,并对数据存储进行分区。

  • 根DNS服务器
    • com DNS服务器
      • baidu
      • taobao
    • net DNS服务器
    • org DNS服务器
    • DNS 的存储设计是一个树状结构。叶子节点中才存放真实的映射关系,中间节点都是目录。存储分成 3 层:
  • 顶部第一级是根 DNS 存储,存储的是顶级域的目录,被称作根 DNS 服务器;
  • 第二级是顶级域存储,存储的是二级域的目录,被称作顶级域 DNS 服务器(Top Level DNS,TLD);
  • 最后一级是叶子节点,存储的是具体的 DNS 记录,也被称作权威 DNS 服务器。

DNS 查询过程
当用户在浏览器中输入一个网址,就会触发 DNS 查询。这个时候在上述的 3 个层级中,还会增加本地 DNS 服务器层级。本地 DNS 服务器包括用户自己路由器中的 DNS 缓存、小区的 DNS 服务器、ISP 的 DNS 服务器等。
主要步骤如下:

  • 用户输入网址,查询本地 DNS。
    • 本地 DNS 是一系列 DNS 的合集,比如 ISP 提供的 DNS、公司网络提供的 DNS。本地 DNS 是一个代理,将 DNS 请求转发到 DNS 网络中。如果本地 DNS 中已经存在需要的记录,也就是本地 DNS 缓存中找到了对应的 DNS 条目,就会直接返回,而跳过之后的步骤。
  • 客户端请求根 DNS 服务器。
    • 如果本地 DNS 中没有对应的记录,那么请求会被转发到根 DNS 服务器。根 DNS 服务器只解析顶级域,以“www.artisan.com”为例,根 DNS 服务器只看 com 部分。
  • 根 DNS 服务器返回顶级 DNS 服务器的 IP。
  • 客户端请求顶级 DNS 服务器,顶级 DNS 服务器中是具体域名的目录。
  • 顶级 DNS 服务器返回权威 DNS 服务器的 IP。
  • 客户端请求权威 DNS 服务器。在权威 DNS 服务器上存有具体的 DNS 记录。以 artisan为例,权威 DNS 服务器中可能有和 artisan.com 相关的上百条甚至更多的 DNS 记录,会根据不同的 DNS 查询条件返回。
  • 权威 DNS 服务器返回 DNS 记录到本地 DNS 服务器。
  • 本地 DNS 服务器返回具体的 DNS 记录给客户端。

在上述 8 个过程全部结束后,客户端通过 DNS 记录中的 IP 地址,可以找到请求服务的主机。 客户端最终可以找到 对应的 IP 地址,从而获得 Web 服务。


关于缓存

在上面的例子当中,每一步都有缓存的设计。浏览器会缓存 DNS,此外,操作系统、路由器、本地 DNS 服务器也会……因此,绝大多数情况,请求不会到达根 DNS 服务器

以artisan为例,如果在某个时刻同一个区域内有一个用户触发过上述 1~8 的过程,另一个同区域的用户就可以在本地 DNS 服务器中获得 DNS 记录,而不需要再走到根 DNS 服务器。这种设计,我们称作分级缓存策略

在分级缓存策略中,每一层都会进行缓存,经过一层层的缓存,最终命中根 DNS 服务、顶级 DNS 服务器以及权威 DNS 服务的请求少之又少。这样,互联网中庞大流量的 DNS 查询就不需要大量集中的资源去响应。


DNS 记录

1
2
;定义www.example.com的ip地址
www.example.com. IN A 139.18.28.5;

上面的就是一条 DNS 记录,纯文本即可。

  • IN 代表记录用于互联网,是 Intenet 的缩写。在历史上 Internet 起源于阿帕网,在同时代有很多竞争的网络,IN 这个描述也就保留了下来。
  • www.example.com 是要解析的域名
  • A 是记录的类型,A 记录代表着这是一条用于解析 IPv4 地址的记录。从这条记录可知,www.example.com的 IP 地址是 139.18.28.5。
  • ;是语句块的结尾,也是注释

那么除了 A 记录,还有哪些 DNS 记录的类型呢?DNS 记录的类型非常多,有 30 多种。其中比较常见的有 A、AAAA、CNAME、MX,以及 NS 等

CNAME(Canonical Name Record) 用于定义域名的别名,如下面这条 DNS 记录:

1
2
; 定义www.example.com的别名
a.example.com. IN CNAME b.example.com.

当你想把一个网站迁移到新域名,旧域名仍然保留的时候;还有当你想将自己的静态资源放到 CDN 上的时候,CNAME 就非常有用

AAAA 记录 前面我们提到,A 记录是域名和 IPv4 地址的映射关系。和 A 记录类似,AAAA 记录则是域名和 IPv6 地址的映射关系。

MX 记录(Mail Exchanger Record)
MX 记录是邮件记录,用来描述邮件服务器的域名。

在工作中,我们经常会发邮件到某个同事的邮箱。比如说,发送一封邮件到 xiaoming@artisan.com,那么artisan如何知道哪个 IP 地址是邮件服务器呢?

这个时候就可以用到下面这条 MX 记录:

1
IN MX mail.artisan.com

这样凡是 @artisan的邮件都会发送到 mail.artisan.com 中,而 mail.artisan.com 的 IP 地址可以通过查询 mail.artisan.com 的 A 记录和 AAAA 记录获得。

NS 记录(Name Server)记录是描述 DNS 服务器网址。从 DNS 的存储结构上说,Name Server 中含有权威 DNS 服务的目录。也就是说,NS 记录指定哪台 Server 是回答 DNS 查询的权威域名服务器。

当一个 DNS 查询看到 NS 记录的时候,会再去 NS 记录配置的 DNS 服务器查询,得到最终的记录。如下面这个例子:

1
2
a.com.     IN      NS      ns1.a.com.
a.com. IN NS ns2.a.com.

当解析 a.com 地址时,我们看到 a.com 有两个 NS 记录,所以确定最终 a.com 的记录在 ns1.a.com 和 ns2.a.com 上。从设计上看,ns1 和 ns2 是网站 a.com 提供的智能 DNS 服务器,可以提供负载均衡、分布式 Sharding 等服务。比如当一个北京的用户想要访问 a.com 的时候,ns1 看到这是一个北京的 IP 就返回一个离北京最近的机房 IP。

上面代码中 a.com 配置了两个 NS 记录。通常 NS 不会只有一个,这是为了保证高可用,一个挂了另一个还能继续服务。通常数字小的 NS 记录优先级更高,也就是 ns1 会优先于 ns2 响应。

配置了上面的 NS 记录后,如果还配置了 a.com 的 A 记录,那么这个 A 记录会被 NS 记录覆盖。

DNS

总结

用树状结构来分类和索引符合人类的直觉和习惯,URL 的设计遵循的依然是人的思考方式。

URL 中的 HOST 部分需要被解析为 IP 地址,于是就有了域名系统(DNS)。域名系统是一个分级的分布式系统,整体设计也是一个树状结构。

顶层的根域名服务器和中间的顶级域名服务器,存储的是目录,最终的 DNS 记录由权威域名服务器提供。DNS 记录并不仅仅只有映射 IP 一种能力,DNS 记录还可以设置网站的别名、邮件服务器、DNS 记录位置等能力。

问题: CNAME 记录的作用是?

CNAME 是一种 DNS 记录,它的作用是将一个域名映射到另一个域名。域名解析的时候,如果看到 CNAME 记录,则会从映射目标重新开始查询。

15. 内容分发网络:请简述CDN回源是如何工作

对于一些体量较大的应用来说,如果把大量资源集中到单一节点进行分发,恐怕很难有某个机房可以支撑得住这么大的流量。

例如一个日活在 100W 的小型互联网产品,如果每次请求需要 1M 的数据,那就刚好是近 1TB 数据。对于这样的数据规模而言,完全由单一节点进行分发是不现实的。

因此现在互联网应用在分发内容的时候,并不是从自己架设的服务器上直分发内容,而是走一个叫作内容分发网络(Content Dilivery Network)的互联网底层建设。

域名系统类似,内容分发网络(Content Dilivery Network,CDN)是一个专门用来分发内容的分布式应用

CDN 构建在现有的互联网之上,通过在各地部署数据中心,让不同地域的用户可以就近获取内容。

这里的内容通常指的是文件、图片、视频、声音、应用程序安装包等,它们具有一个显著的特征——无状态,或者说是静态的。这些资源不像订单数据、库存数据等,它们一旦发布,就很少会发生变化。另一个显著的特征,是这些资源往往会被大量的用户需要,因此分发它们的流量成本是较高的

为什么不能集中提供这些静态资源呢?这和域名系统的 DNS 记录不能集中提供是一个道理,需要考虑到流量、单点故障、延迟等因素。

  • 在离用户更近的地理位置提供资源,可以减少延迟。
  • 按照地理位置分散地提供资源,也可以降低中心化带来的服务压力。

因此,CDN 的服务商会选择在全球布点,或者在某个国家布点。具体要看 CDN 服务提供商的服务范围。目前国内的阿里云、腾讯云等也在提供 CDN 业务。

内容的分发

CDN 是一个分布式的内容分发网络。

  • 当用户请求一个网络资源时,用户请求的是 CDN 提供的资源。
  • 和域名系统类似,当用户请求一个资源时,首先会接触到一个类似域名系统中目录的服务,这个服务会告诉用户究竟去哪个 IP 获取这个资源。

事实上,很多大型的应用,会把 DNS 解析作为一种负载均衡的手段。当用户请求一个网址的时候,会从该网站提供的智能 DNS 中获取网站的 IP。

例如当你请求百度的时候,具体连接到哪个百度的 IP,是由百度使用的智能 DNS 服务决定的。域名系统允许网站自己为自己的产品提供 DNS 解析

CDN

当用户请求一个静态资源的时候,首先会触发域名系统的解析。域名系统会将解析的责任交由 CDN 提供商来处理,CDN 的智能 DNS 服务会帮助用户选择离自己距离最近的节点,返回这个节点的 A(或 AAAA)记录。然后客户端会向 CDN 的资源节点发起请求,最终获得资源。

在上面整个过程当中,CDN 的智能 DNS 还充当了负载均衡的作用。如果一个节点压力过大,则可以将流量导向其他的节点。


回源

讨论了 CDN 的主要设计和架构,但是还有一个问题没有解决——就是资源怎么进入内容分发网络? 资源的生产者,也是 CDN 的购买者,目的是向用户提供网络服务。

  • 那么服务提供者的静态资源如何进入 CDN 呢?
  • 手动上传、用接口推送,还是通过其他别的方式呢?

可以把 CDN 想象成一个分布式的分级缓存,再加上数据库的两层设计,如下图所示:

CDN

用户的请求先到达缓存层,如果缓存被穿透,才到达最终的存储层。缓存的设计必须是分布式的,因为绝大多数的资源使用都会发生在缓存上,只有极少数的请求才会穿透到底层的存储。通常这种设计,我们期望缓存层至少需要帮挡住 99% 的流量。既然缓存层能挡住 99% 的流量,那么实际的数据存储就可以交由源站点完成。

值得一提的是,在程序设计当中有一个核心的原则,叫作单一数据源(Single Souce of Truth, SSOT)。这个原则指的是,在程序设计中,应该尽可能地减少数据的来源,最好每个数据来源只有单独一份。这样能够避免大量的数据不一致以及同步数据的问题。

  • 基于这样的设计,谁来提供资源的存储呢?
  • 谁来提供这个单一的数据源呢?当然是服务提供者本身。如果 CDN 再提供 一份资源的存储,不就有两个数据源了吗?而且,只有服务的提供者才能更好地维护这个资源仓库。

在 CDN 的设计当中,CDN 实际上提供的是数据的缓存。而原始数据,则由服务的提供者提供

dig

CDN

用户请求静态资源通常用自己的域名(防止跨域和一些安全问题)。为了让用户请求的是自己的网站,而使用的是 CDN 的服务,这里会使用 CNAME 让自己的域名作为 CDN 域名的一个别名。当请求到 CDN 服务的时候,会首先由 CDN 的 DNS 服务帮助用户选择一个最优的节点,这个 DNS 服务还充当了负载均衡的作用。接下来,用户开始向 CDN 节点请求资源。**如果这个时候资源已经过期或者还没有在 CDN 节点上,就会从源站读取数据,这个步骤称为CDN 的回源**。

另一方面,CDN 上缓存的资源通常也会伴随失效时间的设置,当失效之后同样会触发回源。另一种情况是可以通过开放的 API 或者 CDN 管理后台直接删除缓存(让资源失效),这个操作结束后,同样会触发回源。

总结

CDN 是一种网络应用,作用是分发互联网上的资源。CDN 服务的提供商,会在世界(或国家)范围内设立数据中心,帮助分发资源。用户请求的资源会被 CDN 分发到最临近的节点获取。

CDN 作为一门生意,CDN 的服务商会大批量的从运营商处获取流量,然后再以较高但是可以接受的价格卖给服务提供方。

对于中小型互联网公司来说,购买一定的 CDN 流量成本可控,比如 1G 流量在 1 元以内。对于大型的互联网公司,特别是对 CDN 依赖严重的公司,可能还需要自己建设。比如 2021 年抖音每天分发的数据量在 50PB 左右(1PB=1024TB),如此庞大的数据量如果换算成钱是非常高的。按照阿里云的报价,50PB 的价格是 480W 人民币。按照这种体量计算,抖音每天要花 480W 人民币,一年是 17 亿。

所以当你设计一个内容分发的方案时,除了要考虑到其中的技术细节,也要从成本上进行思考,看看能不能从数据压缩、资源格式角度做一些文章。


问题: 请简述 CDN 回源是如何工作的?

CDN 回源就是 CDN 节点到源站请求资源,重新设置缓存。通常服务提供方在使用 CDN 的时候,会在自己的某个域名发布静态资源,然后将这个域名交给 CDN。

比如源站在 s.example.com 中发布静态资源,然后在 CDN 管理后台配置了这个源站。在使用 CDN 时,服务提供方会使用另一个域名,比如说 b.example.com。然后配置将 b.example.com 用 CNAME 记录指向 CDN 的智能 DNS。

这个时候,如果用户下载b.example.com/a.jpg,CDN 的智能 DNS 会帮用户选择一个最优的 IP 地址(最优的 CDN 节点)响应这次资源的请求。如果这个 CDN 节点没有 a.jpg,CDN 就会到 s.example.com 源站去下载,缓存到 CDN 节点,然后再返回给用户。

CDN 回源有 3 种情况,

  • 一种是 CDN 节点没有对应资源时主动到源站获取资源;
  • 另一种是缓存失效后,CDN 节点到源站获取资源;
  • 还有一种情况是在 CDN 管理后台或者使用开放接口主动刷新触发回源。

如果你的应用需要智能 DNS 服务,你将如何实现?

首先你可以在你的域名解析系统中增加两条(或以上)ns 记录。比如说你的域名是 example.com,那么你可以增加 ns1.exmaple.com, ns2.example.com。当然,指定这两个域名的 IP 还需要配置两个 A 记录。

然后你需要两台机器(也可以是容器或者虚拟机),对应 ns1 和 ns2。最好用不在同一个物理机上的两个容器,这样可以避免一台物理机故障导致服务瘫痪。然后在每个容器(虚拟机)上安装一个 Named 服务。

Named 是一个专门用来提供 DNS 服务的工具,在虚拟机上安装完成 Named 后,这个虚拟机就变成了一个权威服务器节点。

配置好 Named 后,你需要写几个脚本文件,给要提供 DNS 的域名配置信息。Named 配套使用的有个叫作 GeoDNS 的插件,可以提供基于地理位置的智能 DNS 服务。

16. HTTP协议面试通关:强制缓存和协商缓存的区别是

超文本传输协议(HyperText Transfer Protocol,HTTP) 是目前使用最广泛的应用层协议。

1990 年蒂姆·伯纳斯·李开发了第一个浏览器,书写了第一个 Web 服务器程序和第一张网页。网页用的语言后来被称作超文本标记语言(HTML),而在服务器和客户端之间传输网页的时候,伯纳斯·李没有直接使用传输层协议,而是在 TCP 的基础上构造了一个应用层协议,这个就是超文本传输协议 HTTP。

万维网(World Wide Web, WWW) 是伯纳斯·李对这一系列发明,包括 Web 服务、HTTP 协议、HTML 语言等一个体系的综合。

请求响应和长连接

HTTP 协议采用请求/返回模型。客户端(通常是浏览器)发起 HTTP 请求,然后 Web 服务端收到请求后将数据回传。

HTTP 的请求和响应都是文本,可以简单认为 HTTP 协议利用 TCP 协议传输文本。当用户想要看一张网页的时候,就发送一个文本请求到 Web 服务器,Web 服务器解析了这段文本,然后给浏览器将网页回传。

那么这里有一个问题,是不是每次发送一个请求,都建立一个 TCP 连接呢? 当然不能这样,为了节省握手、挥手的时间。当浏览器发送一个请求到 Web 服务器的时候,Web 服务器内部就设置一个定时器。在一定范围的时间内,如果客户端继续发送请求,那么服务器就会重置定时器。如果在一定范围的时间内,服务器没有收到请求,就会将连接断开。这样既防止浪费握手、挥手的资源,同时又避免一个连接占用时间过长无法回收导致内存使用效率下降

这个能力可以利用 HTTP 协议头进行配置,比如下面这条请求头:

1
Keep-Alive: timeout=5s

会告诉 Web 服务器连接的持续时间是 5s,如果 5s 内没有请求,那么连接就会断开。


Keep-Alive 并不是伯纳斯·李设计 HTTP 协议时就有的能力。伯纳斯·李设计的第一版 HTTP 协议是 0.9 版,后来随着协议逐渐完善,有了 1.0 版。而 Keep-Alive 是 HTTP 1.1 版增加的功能,目的是应对越来越复杂的网页资源加载。从 HTTP 协议诞生以来,网页中需要的资源越来越丰富,打开一张页面需要发送的请求越来越多,于是就产生了 Keep-Alive 的设计。

同样,当一个网站需要加载的资源较多时,浏览器会尝试并发发送请求(利用多线程技术)。浏览器会限制同时发送并发请求的数量,通常是 6 个,这样做一方面是对用户本地体验的一种保护,防止浏览器抢占太多网络资源;另一方面也是对站点服务的保护,防止瞬时流量过大。

在 HTTP 2.0 之后,增加了多路复用能力。和 RPC 框架时提到的多路复用类似,请求、返回会被拆分成切片,然后混合传输。这样请求、返回之间就不会阻塞。你可以思考,对于一个 TCP 连接,在 HTTP 1.1 的 Keep-Alive 设计中,第二个请求,必须等待第一个请求返回。如果第一个请求阻塞了,那么后续所有的请求都会阻塞。而 HTTP 2.0 的多路复用,将请求返回都切分成小片,这样利用同一个连接,请求相当于并行的发出,互相之间不会有干扰。


HTTP 方法和 RestFul 架构

伴随着 HTTP 发展,也诞生了一些著名的架构,比如 RestFul。在面试中,经常会遇到 RestFul,RestFul 是 3 个单词的合并缩写:

  • Re(Representational)
  • st(State)
  • Ful(Transfer)

这个命名非常有趣,让我联想到 grep 命令的命名,global regular pattern match。这是一种非常高端的命名技巧,提取词汇中的一个部分组合成为一个读起来朗朗上口的新词汇,建议在实战命名的时候也可以考虑试试。

在 RestFul 架构中,状态仅仅存在于服务端,前端无状态。

  • 状态(State)可以理解为业务的状态,这个状态是由服务端管理的。这个无状态和服务端目前倡导的无状态设计不冲突,现在服务端倡导的无状态设计指的是容器内的服务没有状态,状态全部存到合适的存储中去。所以 Restful 中的 State,是服务端状态。

前端(浏览器、应用等)没有业务状态,却又要展示内容,因此前端拥有的是状态的表示,也就是 Representation。

  • 比如一个订单,状态存在服务端(数据库中),前端展示订单只需要部分信息,不需要全部信息。前端只需要展示数据,展示数据需要服务端提供。所以服务端提供的不是状态,而是状态的表示。

前端没有状态,当用户想要改变订单状态的时候,比如支付,这个时候前端就向服务端提交表单,然后服务端触发状态的变化。这个过程我们称为转化(Transfer)。从这个角度来看,Restful 讲的是一套前端无状态、服务端管理状态,中间设计转化途径(请求、函数等)的架构方法。这个方法可以让前后端职责清晰,前端负责渲染, 服务端负责业务。前端不需要业务状态,只需要展示。服务端除了关心状态,还要提供状态的转换接口

缓存
在 HTTP 的使用中,我们经常会遇到两种缓存,强制缓存和协商缓存,接下来举两个场景来说明。

  • 强制缓存
    • 举个例子: 公司用版本号管理某个对外提供的 JS 文件。比如说 libgo.1.2.3.js,就是 libgo 的 1.2.3 版本。其中 1 是主版本,2 是副版本,3 是补丁编号。每次你们有任何改动,都会更新 libgo 版本号。在这种情况下,当浏览器请求了一次 libgo.1.2.3.js 文件之后,还需要再请求一次吗?
    • 整理下我们的需求,浏览器在第一次进行了GET /libgo.1.2.3.js这个操作后,如果后续某个网页还用到了这个文件(libgo.1.2.3.js),我们不再发送第二次请求。这个方案要求浏览器将文件缓存到本地,并且设置这个文件的失效时间(或者永久有效)。这种请求过一次不需要再次发送请求的缓存模式,在 HTTP 协议中称为强制缓存。当一个文件被强制缓存后,下一次请求会直接使用本地版本,而不会真的发出去。
    • 使用强制缓存时要注意,千万别把需要动态更新的数据强制缓存。一个负面例子就是小明把获取用户信息数据的接口设置为强制缓存,导致用户更新了自己的信息后,一直要等到强制缓存失效才能看到这次更新。
  • 协商缓存
    • 我们再说一个场景:小明开发了一个接口,这个接口提供全国省市区的 3 级信息。先问你一个问题,这个场景可以用强制缓存吗?小明一开始觉得强制缓存可以,然后突然有一天接到运营的通知,某市下属的两个县合并了,需要调整接口数据。小明错手不急,更新了接口数据,但是数据要等到强制缓存失效。
    • 为了应对这种场景,HTTP 协议还设计了协商缓存。协商缓存启用后,第一次获取接口数据,会将数据缓存到本地,并存储下数据的摘要。第二次请求时,浏览器检查到本地有缓存,将摘要发送给服务端。服务端会检查服务端数据的摘要和浏览器发送来的是否一致。如果不一致,说明服务端数据发生了更新,服务端会回传全部数据。如果一致,说明数据没有更新,服务端不需要回传数据
    • 从这个角度看,协商缓存的方式节省了流量。对于小明开发的这个接口,多数情况下协商缓存会生效。当小明更新了数据后,协商缓存失效,客户端数据可以马上更新。和强制缓存相比,协商缓存的代价是需要多发一次请求

总结

目前 HTTP 协议已经发展到了 2.0 版本,不少网站都更新到了 HTTP 2.0。大部分浏览器、CDN 也支持了 HTTP 2.0。 可以自行查阅更多关于 HTTP 2.0 解决队头阻塞、HPack 压缩算法、Server Push 等资料

另外 HTTP 3.0 协议也在建设当中,HTTP 3.0 对 HTTP 2.0 兼容,主要调整发生在网络底层。HTTP 3.0 开始采用 UDP 协议,并在 UDP 协议之上,根据 HTTP 协议的需求特性,研发了网络层、应用层去解决可靠性等问题

17. 流媒体技术:直播网站是如何实现的

如何将视频抽象成流?就是传输一部分即可播放一部分

  • 在实际的操作当中,设计了一种类似目录的格式,将音频数据进行切片,这部分能力利用现有的工具FFmpeg就可以轻松做到,安装FFmpeg,利用如下指令处理一个mp4,就可以生成很多切片(切割成HTTP Live Streaming可以播放的切片)和一个目录文件
1
2
ffmpeg -i input.mp4 -c:v libx264 -c:a aac -strict -2 -fhls output.m3u8
ls # 可查看目录文件,下载视频时可根据`.m3u8`内容下载对应的`.ts`文件

流媒体的架构

  • 视频录制得到MP4等格式的文件
  • 上传到服务器进行编码[编码产生不同清晰度文件],产生上述切片文件
  • 切片文件存储到流媒体服务器中
  • 然后从视频目录读取

直播

  • 录制吨不断上传视频内容
  • 视频内容编码后由流媒体服务器负责分发
  • 如果观看人数较多,可以使用CDN回源到流媒体服务器
  • m3u8文件可以看作一个动态的文件,能够不断产生新的数据,因此直播技术中,可以将获取m3u8文件设计成一个接口,不断由播放器获取新的m3u8文件

其他音视频网站

  • 将视频编码后切片
  • 然后利用CDN分发目录和切片文件,就可以播放了

视频的编码和解码

  • 视频文件较大,因此在传输前需要压缩
  • 在播放前需要解码

视频的压缩技术:是针对视频的特征进行特别处理的压缩技术,视频的压缩算法本质上是对图片的压缩,主要依靠人类视觉的残留效应

H264 就是国际标准化组织在推广的一种编码格式。在 H264 的视频编码技术中,有一个叫作宏块的概念。宏块,就是将画面分成大小不等的区域。比如说 8x8、16x16 等。当播放两个连续的画面的时候,你可以理解成两张图片。如果基于图片分析,那么播放的就是很多个宏块。在这连续的两帧画面中,并不是所有的宏块都发生了变化。

点到点视频技术

在视频会议、面对面聊天等场景下,需要点到点的视频技术

videoh2h

如果是1对1的视频聊天,可以考虑点到点的服务

  • Host1 <–(UDP等)–> Host2

videoh2h2
在NAT通信中,往往需要在内网的主机发起连接,这个时候NAT模块识别发起的端口并记录。如上图,如果客户是公网IP,Host1可以找到该客户建立连接,但是客户是无法主动连接Host1

如下图,如果双方都在内网,都需要NAT场景,就无法建立连接。Host1 发送请求但由于客户没有建立连接而被拒绝,反之亦然,类似多线程的死锁问题无法解决。这是就需要第三方服务器,这台服务器可以作,为NAT模块辅助功能,让双方的NAT模块以为和对方发起过连接请求,这个解决方案叫做NAT穿透

videoh2h2

在WebRTC协议中,可以提供网页版的1对1聊天,如果需要连接两个内网的机器,就需要架设第三方服务。

如果在线会议,人数较少可以点到点,但人数较多就需要考虑以下方案:

  • 放弃点到点技术,直接采用类似直播架构的中心化服务
  • 利用边缘计算,让距离相近的参会者利用共同的离自己最近的服务器交换数据

总结
流媒体,就是把多媒体数据抽象成为流进行传输。视频本质上是一张张图片在播放,因此非常适合流传输。要知道,流是随着时间产生的数据。

通常在一个网络中,等价成本下吞吐量、丢包率和延迟 3 者不能兼得

对延迟要求较高的场景,可能需要降低视频质量或者部署边缘服务器

人数较少,可以采用点对点技术,但是要考虑NAT穿透问题


问题: 直播网站是如何实现的

  • 录制端: 负责录制视频直播视频,用流的形式上传
  • 计算集群:专门负责编码上传的流数据,然后进行压缩、转码、切片等工作
  • 对象存储:存储原视频和转码后的视频(相当于CDN的源,回源用)
  • CDN: 将转码后的内容分发到离用户较近的节点,方便用户获取
  • 直播APP:给用户看直播时使用

作业:写一张网页:用webrtc实现点到点通信

18. 爬虫和反爬虫:如何防止黑产爬取我的数据

反爬虫的手段主要有:robots.txt、用户识别、字符加密算法、数据加密算法。

robots.txt文件规定哪些数据可以爬虫、哪些不可以爬虫;

针对自己账号范围实现某个功能,如对建立筛选,不属于违法行为;

爬虫的原理:本质上就是一次网络请求,然后将返回的数据保存下来

对于搜索引擎的爬虫而言,通常会在请求头中加上自己的标识,比如百度会加上baidu字符串,这样方便网站服务器识别。

爬虫如果是非法的,往往就需要伪装成浏览器,通常会用到浏览器内核,模拟发出网络请求、

chronium(Chrome的开源内核)

  • 用chronium发请求的时候,对于服务提供方的反爬虫系统,请求就变成了一次标准的用户行为,如果对方网站需要登陆才能爬取数据,不法分子还会模拟登陆行为。如果仅仅输入用户名和密码,那这个网站的登陆行为非常容易模拟,只需要找到对应的接口,用户和密码传输过去,就可以拿到访问资源的令牌

验证码–通过深度学习模型训练图片,进行识别;更难的就是加滑块

模拟用户动作

将原始数据存储,然后进行分析

如果爬取网页数据,后续会用到HTML解析器(Parser)

如果爬取的接口数据,通常就是分析json

IP的反追踪,就是利用代理,增加追踪的成本。可以通过大量购买IP然后模拟多用户攻击【临时租用大量的IP地址的价格低廉,降低犯罪成本】

反爬虫基本操作

  • robots.txt 从法律上告诉爬虫哪些页面是不可爬取的
  • 用户识别
    • 对高频访问的IP加以限制,但有时候有些公司共用一个IP出口,也不是很有效
    • 设备指纹:利用设备上的信息,生成一个具有唯一性的字符串,这种算法是非标准化的,因此不同的数据安全团队会有自己的算法,比限制IP好
    • 根据唯一用户设置数据安全策略,访问频次,黑名单等
  • 字体加密, 爬虫爬取的通常就是用户本身可以看到的内容,将UTF8编码中的汉字顺序打乱,然后将对应的数据换序。
  • 加密传输 APP的数据抓取依赖APP数据传输使用的标准协议,比如用HTTPS协议传输数据的App,爬虫可以在App端安装证书,然后利用代理实现中间人抓包。如果数据用自己的协议加密,抓包的同时,必须破解这个加密协议

网络安全

  • 基础设施(证书、加解密、公私钥体系、信任链等)
  • 具体的攻击手段(DDos、XSS、SQL注入、ARP攻击、中间人攻击等)
  • 防御手段

19. 网络安全概述:对称、非对称加密的区别

  • 对称加密:数据加密标准(DES)算法在 1976 年被美国国家标准局定为使用标准,DES 采用的 56 位密钥,每次计算加密 64 位的数据,目前已经被证明可以被暴力破解,所谓暴力破解,就是遍历所有可能的密钥解析数据的方法;为了应对暴力破解等问题,很多团队选择对称加密算法时开始使用高级加密标准(AES),这个加密法用 128 位密钥,并设计了更难破解的算法。

  • 非对称加密:目前最常见且广泛使用的非对称加密算法是 RSA 算法。RSA 依赖的是大整数的分解,以及一些和素数相关的算法。目前没有理论可以破译 RSA 算法。总体来说,RSA 密钥越长破解成本就越高,因此仍然被广泛使用。

  • 对称加密和解密可以用同一套秘钥

  • 非对称加密利用数学的方法生成公私钥对,公钥加密的数据私钥可以解密,私钥加密的数据公钥可以解密

  • 公钥不能解密公钥加密的数据,私钥也不能解密私钥加密的数据

20. 信任链:为什么可以相信一个HTTPS网站

当用户用浏览器打开一个 HTTPS 网站时,会到目标网站下载目标网站的证书。接下来,浏览器会去验证证书上的签名,一直验证到根证书。如果根证书被预装,那么就会信任这个网站。也就是说,网站的信用是由操作系统的提供商、根证书机构、中间证书机构一起在担保。

摘要和签名

  • MD5
  • SHA-1 摘要算法

摘要是一种数学证明

在摘要上用私钥加密就是签名,签名可以防止数据被篡改、伪造等

在摘要和签名的基础上,可以利用原本的社会关系,让一些信用优秀的结构提供信用

21. 攻防手段介绍:如何低于SYN拒绝攻击

DDoS

拒绝服务攻击(Denial of Service Attack, DoS),利用大量的流量迅速向一个网站发送出去,攻击者一般没有足够的经济实力购买机器,利用中病毒、木马的肉机组织流量攻击,这种方式也被称为分布式拒绝服务攻击(Distributed Denial of Service Attack, DDoS)

  • 直接不停发送Ping消息的,利用底层的ICMP协议,称为ICMP攻击
  • 走UDP协议的,称为**UDP洪水(UDP Flood)
  • 不停的用TCP协议发送SYN消息的,也叫SYN攻击

防范措施:

  • 防火墙根据特征识别出攻击行为,通过这样的方式将攻击行为过滤掉,让系统不会因为DDos而过载造成崩溃
  • 切换流量,从日常生产环境-同城灾备环境-异地灾备环境
  • CDN 是大量缓存节点,DDoS攻击CDN的时候用不上力
    • 设计一台吞吐量极高的代理服务器,作为反向代理挡在所有服务器前面,如果遇到DDoS,代理服务器可以识别出一些特征并丢弃一些流量

在遇到攻击的时候,对服务适当降级也是有必要的,通过允许防火墙造成一部分的误伤来识别更多的攻击流量

前端框架 React和 Vue开发基本杜绝XSS攻击


中间人攻击

不法分子利用自己的伪装基站设备伪装成基站


跨站脚本攻击(XSS)

跨站脚本(Cross Site Scripting),利用漏洞将脚本注入网页,例如提交个人信息的输入框,如果在服务端没有处理好,就可能出发夸张脚本攻击


如何抵御 SYN 拒绝攻击?

  • SYN 攻击是 DDoS 攻击的一种形式。这种形式攻击者伪装成终端不停地向服务器发起 SYN 请求。
  • 通常攻击者的“肉鸡”,发送了 SYN 之后,不等给服务端 ACK,就下线了。
  • 这样攻击者不断发送 SYN ,然后下线,而服务端会等待一段时间(通常会在 3s 以上),等待 ACK。这样就导致了大量的连接对象在服务端被积累。

针对这个特点,可以实现TCP代理(防火墙)

哪些情况服务器的/etc/passwd文件会被黑客拿走

漫游互联网: 什么是蜂窝移动网络

参考:

https://www.bilibili.com/video/BV1B34y1e7kU?p=8&spm_id_from=pageDriver&vd_source=27f6135965c74480fdc752d98427d3b2

https://cloud.tencent.com/developer/article/1862663

https://www.cnblogs.com/jmcui/p/15003579.html#top

非常好的网络基础教程:https://docs.oracle.com/cd/E19253-01/819-7058/oviewtm-1/index.html