网络协议二

一、套接字Socket

基于 TCP UDP 协议的 Socket 编程,在讲 TCP 和 UDP 协议的时候,我们分客户端和服务端,在写程序的时候,我们也同样这样分。
在网络层,Socket 函数需要指定到底是 IPv4 还是 IPv6,分别对应设置为 AF_INET 和 AF_INET6。另外,还要指定到底是 TCP 还是 UDP。还记得咱们前面讲过的,TCP 协议是基于数据流的,所以设置为 SOCK_STREAM,而 UDP 是基于数据报的,因而设置为 SOCK_DGRAM。

监听的 Socket 和真正用来传数据的 Socket 是两个,一个叫作监听 Socket,一个叫作已连接 Socket

比喻:接待客人

想象你在家里准备接待客人,你有一个门铃和一个客厅。门铃相当于监听 Socket,而客厅相当于已连接 Socket。

  1. 监听 Socket(门铃)

    • 你家的门铃一直在等待有人按下它,这就像服务器上的监听 Socket 一直在等待新的连接请求。
    • 当有人按下门铃时,你知道有客人到访了,但你还不知道是谁,也还没有开始与客人交谈。
  2. 已连接 Socket(客厅)

    • 当你开门迎接客人,并带他们到客厅后,你就开始与客人交流了,这时候的客人就相当于已连接 Socket。
    • 在客厅里,你可以与每个客人进行独立的对话,不会相互干扰。

技术解释

在网络编程中,特别是 TCP 服务器程序中,监听 Socket 和已连接 Socket 是两个不同的概念和用途:

  1. 监听 Socket

    • 作用:用来监听和接受新的连接请求。
    • 创建:在服务器启动时创建,并绑定到特定的 IP 地址和端口。
    • 工作方式:服务器调用 listen() 方法,使这个 Socket 进入监听状态,等待客户端的连接请求。当有客户端请求连接时,服务器调用 accept() 方法,从监听 Socket 接受连接请求。
  2. 已连接 Socket

    • 作用:用来与客户端进行实际的数据传输。
    • 创建:当服务器调用 accept() 方法并成功接收一个客户端连接后,会生成一个新的已连接 Socket。这个 Socket 专门用于与该客户端进行通信。
    • 工作方式:服务器使用这个已连接 Socket 调用 send()recv() 方法,与客户端交换数据。

基于 TCP 协议的 Socket 程序函数调用过程

在这里插入图片描述

write() 和 read():适用于所有类型的文件描述符,包括 Socket,功能简单直接。
send() 和 recv():专为网络 Socket 设计,提供额外的功能和灵活性,通过 flags 参数控制行为。

基于 UDP 协议的 Socket 程序函数调用过程

对于 UDP 来讲,过程有些不一样。UDP 是没有连接的,所以不需要三次握手,也就不需要调用 listen 和 connect,但是,UDP 的的交互仍然需要 IP 和端口号,因而也需要 bind。UDP 是没有维护连接状态的,因而不需要每对连接建立一组 Socket,而是只要有一个 Socket,就能够和多个客户端通信。也正是因为没有连接状态,每次通信的时候,都调用 sendto 和 recvfrom,都可以传入 IP 地址和端口。
在这里插入图片描述

服务器如何接更多的项目?

会了这几个基本的 Socket 函数之后,你就可以轻松地写一个网络交互的程序了。就像上面的过程一样,在建立连接后,进行一个 while 循环。客户端发了收,服务端收了发。
当然这只是万里长征的第一步,因为如果使用这种方法,基本上只能一对一沟通。如果你是一个服务器,同时只能服务一个客户,肯定是不行的。这就相当于老板成立一个公司,只有自己一个人,自己亲自上来服务客户,只能干完了一家再干下一家,这样赚不来多少钱。
那作为老板你就要想了,我最多能接多少项目呢?当然是越多越好。
我们先来算一下理论值,也就是最大连接数,系统会用一个四元组来标识一个 TCP 连接。
{本机 IP, 本机端口, 对端 IP, 对端端口}
服务器通常固定在某个本地端口上监听,等待客户端的连接请求。因此,服务端端 TCP 连接四元组中只有对端 IP, 也就是客户端的 IP 和对端的端口,也即客户端的端口是可变的,因此,最大 TCP 连接数 = 客户端 IP 数×客户端端口数。对 IPv4,客户端的 IP 数最多为 2 的 32 次方,客户端的端口数最多为 2 的 16 次方,也就是服务端单机最大 TCP 连接数,约为 2 的 48 次方。
当然,服务端最大并发 TCP 连接数远不能达到理论上限。首先主要是文件描述符限制,按照上面的原理,Socket 都是文件,所以首先要通过 ulimit 配置文件描述符的数目;另一个限制是内存,按上面的数据结构,每个 TCP 连接都要占用一定内存,操作系统是有限的。

所以,作为老板,在资源有限的情况下,要想接更多的项目,就需要降低每个项目消耗的资源数目。

文件描述符的操作包括:
open():打开一个文件或资源,并返回一个文件描述符。
read():通过文件描述符读取数据。
write():通过文件描述符写入数据。
close():关闭文件描述符,释放资源。

方式一:将项目外包给其他公司(多进程方式)

方式二:将项目转包给独立的项目组(多线程方式)

上面这种方式你应该也能发现问题,如果每次接一个项目,都申请一个新公司,然后干完了,就注销掉这个公司,实在是太麻烦了。毕竟一个新公司要有新公司的资产,有新的办公家具,每次都买了再卖,不划算。
于是你应该想到了,我们可以使用线程。相比于进程来讲,这样要轻量级的多。如果创建进程相当于成立新公司,购买新办公家具,而创建线程,就相当于在同一个公司成立项目组。一个项目做完了,那这个项目组就可以解散,组成另外的项目组,办公家具可以共用。

上面基于进程或者线程模型的,其实还是有问题的。新到来一个 TCP 连接,就需要分配一个进程或者线程。一台机器无法创建很多进程或者线程。有个C10K,它的意思是一台机器要维护 1 万个连接,就要创建 1 万个进程或者线程,那么操作系统是无法承受的。如果维持 1 亿用户在线需要 10 万台服务器,成本也太高了。

方式三:一个项目组支撑多个项目(IO 多路复用,一个线程维护多个 Socket)

当然,一个项目组可以看多个项目了。这个时候,每个项目组都应该有个项目进度墙,将自己组看的项目列在那里,然后每天通过项目墙看每个项目的进度,一旦某个项目有了进展,就派人去盯一下。

由于 Socket 是文件描述符,因而某个线程盯的所有的 Socket,都放在一个文件描述符集合 fd_set 中,这就是项目进度墙,然后调用 select 函数来监听文件描述符集合是否有变化。一旦有变化,就会依次查看每个文件描述符。那些发生变化的文件描述符在 fd_set 对应的位都设为 1,表示 Socket 可读或者可写,从而可以进行读写操作,然后再调用 select,接着盯着下一轮的变化。。

方式四:一个项目组支撑多个项目(IO 多路复用,从“派人盯着”到“有事通知”)

上面 select 函数还是有问题的,因为每次 Socket 所在的文件描述符集合中有 Socket 发生变化的时候,都需要通过轮询的方式,也就是需要将全部项目都过一遍的方式来查看进度,这大大影响了一个项目组能够支撑的最大的项目数量。因而使用 select,能够同时盯的项目数量由 FD_SETSIZE 限制。

如果改成事件通知的方式,情况就会好很多,项目组不需要通过轮询挨个盯着这些项目,而是当项目进度发生变化的时候,主动通知项目组,然后项目组再根据项目进展情况做相应的操作。

能完成这件事情的函数叫 epoll,它在内核中的实现不是通过轮询的方式,而是通过注册 callback 函数的方式,当某个文件描述符发送变化的时候,就会主动通知。

假设进程打开了 Socket m, n, x 等多个文件描述符,现在需要通过 epoll 来监听是否这些 Socket 都有事件发生。其中 epoll_create 创建一个 epoll 对象,也是一个文件,也对应一个文件描述符,同样也对应着打开文件列表中的一项。在这项里面有一个红黑树,在红黑树里,要保存这个 epoll 要监听的所有 Socket。
当 epoll_ctl 添加一个 Socket 的时候,其实是加入这个红黑树,同时红黑树里面的节点指向一个结构,将这个结构挂在被监听的 Socket 的事件列表中。当一个 Socket 来了一个事件的时候,可以从这个列表中得到 epoll 对象,并调用 call back 通知它。

这种通知方式使得监听的 Socket 数据增加的时候,效率不会大幅度降低,能够同时监听的 Socket 的数目也非常的多了。上限就为系统定义的、进程打开的最大文件描述符个数。因而,epoll 被称为解决 C10K 问题的利器

名词解释

文件描述符限制是指一个操作系统中能够同时打开的文件和网络连接的数量上限。为了理解这个概念,我们可以先了解什么是文件描述符,然后解释为什么它会限制并发 TCP 连接数。

什么是文件描述符?

在操作系统中,每个文件(包括网络连接)在打开时,都会被分配一个唯一的标识符,这个标识符就叫做文件描述符(File Descriptor,简称 FD)。文件描述符是一个非负整数,用来引用一个打开的文件或网络连接。

  • 文件:任何类型的文件,比如文本文件、图片文件等。
  • 网络连接:TCP 连接、UDP 连接等。
  • 其他资源:如管道、设备等。

文件描述符限制

操作系统对每个进程能够同时打开的文件描述符数量有限制,这是出于资源管理和安全的考虑。这个限制通常可以分为两个层次:

  1. 软限制:用户或进程可以更改的限制,一般默认较小,但可以通过修改系统设置或在程序中动态调整。
  2. 硬限制:系统级的限制,只有管理员可以更改,通常比软限制要大。

举个例子

假设你在编写一个服务器程序,这个服务器需要处理很多客户端的连接,每个连接对应一个文件描述符。

  • 默认限制操作系统可能默认限制每个进程只能打开 1024 个文件描述符。如果你有超过 1024 个客户端同时连接到服务器,新的连接将无法建立,因为文件描述符已经用完。
  • 调整限制:你可以通过修改系统配置来增加文件描述符的限制。例如,在 Linux 系统中,你可以通过修改 /etc/security/limits.conf 文件或使用 ulimit 命令来调整这个限制。

为什么文件描述符限制会影响并发连接数?

每个 TCP 连接在服务器端都需要一个文件描述符来表示和管理。如果文件描述符用完了,服务器将无法接受新的连接,即使硬件和其他资源还能够处理更多的连接。这就导致了并发 TCP 连接数远不能达到理论上的上限。


应用层协议

二、HTTP协议:看个新闻原来这么麻烦

HTTP 是基于 TCP 协议的,要先建立 TCP 连接
建立了连接以后,浏览器就要发送 HTTP 的请求。

HTTP 请求的创建

在这里插入图片描述

第一部分:请求行

GET POST PUT DELETE
POST 往往是用来创建一个资源的,而 PUT 往往是用来修改一个资源的。

第二部分:首部字段

例如,Accept-Charset,表示客户端可以接受的字符集。防止传过来的是另外的字符集,从而导致出现乱码。

再如,Content-Type是指正文的格式。例如,我们进行 POST 的请求,如果正文是 JSON,那么我们就应该将这个值设置为 JSON。

在 HTTP 协议中,Cache-ControlIf-Modified-Since 是用于控制缓存行为和条件请求的头字段。让我们通俗易懂地解释它们的作用和工作方式。

Cache-Control 头字段用于指定缓存机制的指令,这些指令告诉浏览器和中间缓存服务器如何缓存 HTTP 响应。它可以帮助提高网站性能和减少带宽消耗。

常见指令
  1. public:响应可以被任何缓存(包括浏览器、代理服务器等)缓存。

    • 例子Cache-Control: public
  2. private:响应只能被用户的浏览器缓存,不能被共享缓存(如代理服务器)缓存。

    • 例子Cache-Control: private
  3. no-cache:缓存可以存储响应,但在使用前必须先验证其有效性(向服务器发送请求确认)。

    • 例子Cache-Control: no-cache
  4. no-store:不允许缓存响应,所有内容每次都必须从服务器获取。

    • 例子Cache-Control: no-store
  5. max-age:指定响应在缓存中可以保存的最大时间(以秒为单位),在此时间内缓存内容被认为是新鲜的。

    • 例子Cache-Control: max-age=3600(缓存内容在1小时内有效)
场景:用户访问网页
  1. 第一次访问

    • 用户浏览器向服务器请求网页。
    • 服务器返回网页内容,并在响应头中包含 Cache-Control: max-age=3600Last-Modified 头字段。
    • 浏览器将网页缓存1小时。
  2. 在1小时内再次访问

    • 浏览器检查缓存,发现缓存仍然有效(未超过 max-age)。
    • 浏览器直接从缓存中加载网页,无需向服务器发送请求。
  3. 超过1小时再次访问

    • 浏览器向服务器发送请求,包含If-Modified-Since头字段,指示上次接收到的 Last-Modified 时间。
    • 服务器检查资源是否自该时间以来有修改:
      • 如果没有修改,返回 304 Not Modified,浏览器使用缓存内容。
      • 如果有修改,返回新的网页内容和新的 Last-Modified 时间,浏览器更新缓存。

HTTP 请求的发送

就是 TCP 传输

HTTP 2.0

HTTP/2 和 HTTP/1.1 是两个版本的超文本传输协议,它们有许多不同之处,主要目的是提高性能和效率。以下是 HTTP/2 和 HTTP/1.1 的主要区别,通俗易懂地解释这些技术细节:

1. 多路复用

  • HTTP/1.1:每个请求-响应对都需要一个独立的 TCP 连接。这意味着如果一个网页上有多个资源(如图片、CSS 文件、JavaScript 文件等),每个资源的请求通常需要单独的连接,导致了“队头阻塞”(Head-of-Line Blocking)问题:一个请求阻塞了,后续请求也无法进行。
  • HTTP/2:引入了多路复用技术,多个请求和响应可以在一个单一的 TCP 连接中同时进行。这样可以有效地利用网络资源,减少延迟。

2. 二进制分帧

  • HTTP/1.1:使用纯文本格式来传输数据,包括请求和响应头部。这种格式在解析时效率较低。
  • HTTP/2:使用二进制分帧层(Binary Framing Layer),将所有传输的信息(头部和数据)编码为二进制格式。这种方式更高效、解析更快,并且更容易实现多路复用。

3. 头部压缩

  • HTTP/1.1:每次请求都会携带完整的头部信息,头部信息往往很大且包含重复的内容,浪费了带宽。
  • HTTP/2:使用 HPACK 压缩算法对头部信息进行压缩,大大减少了头部的大小和冗余信息,提高了传输效率。

4. 服务器推送

  • HTTP/1.1:只有客户端可以主动请求资源,服务器只能被动响应。
  • HTTP/2:引入了服务器推送功能,服务器可以在客户端请求某个资源时,主动推送其他相关资源到客户端,这样客户端就不需要再单独请求这些资源了。例如,当客户端请求一个 HTML 页面时,服务器可以提前推送相关的 CSS 和 JavaScript 文件。

5. 流量控制

  • HTTP/1.1:没有针对流量控制的机制,所有请求-响应对共享带宽,可能导致性能不稳定。
  • HTTP/2:引入了流量控制机制,可以更好地管理和分配带宽,确保各个请求的传输速度和效率。

6. 优先级和依赖关系

  • HTTP/1.1:没有内置的请求优先级机制,所有请求的处理顺序主要取决于到达服务器的顺序。
  • HTTP/2:允许客户端为每个请求分配优先级,并建立依赖关系,使得重要的资源可以优先传输,优化了页面加载顺序和速度。
    在这里插入图片描述

QUIC

http是应用层协议,QUIC是传输层协议。QUIC虽然在某些方面优于TCP,但它并不是在所有情况下都是最佳选择。例如,在某些网络环境中,TCP可能仍然是更好的选择,尤其是在需要保证数据顺序和可靠性的场景中。
QUIC也是一种传输层协议,但它与TCP和UDP的不同之处在于,QUIC是为了在UDP之上提供类似于TCP的可靠性和顺序传输而设计的。
随着时间的推移,QUIC可能会在越来越多的应用中取代TCP和UDP,尤其是在需要快速、可靠网络连接的场景中。

尽管 HTTP/2 引入了多路复用技术,使得多个流可以在一个 TCP 连接上并行传输,但由于底层使用的是 TCP 协议,TCP 必须保证数据包按顺序和完整性传输。如果某个数据包出现问题,整个连接上的所有数据传输都会被阻塞,直到问题数据包被正确重传和接收。这就意味着,即使在 HTTP/2 中,某个流的数据包出现问题,其他流的数据传输也会受到影响,无法完全避免队头阻塞的问题

于是,就又到了从 TCP 切换到 UDP。这就是 Google 的 QUIC 协议

机制一:自定义连接机制

我们都知道,一条 TCP 连接是由四元组标识的,分别是源 IP、源端口、目的 IP、目的端口。一旦一个元素发生变化时,就需要断开重连,重新连接。在移动互联情况下,当手机信号不稳定或者在 WIFI 和 移动网络切换时,都会导致重连,从而进行再次的三次握手,导致一定的时延。

这在 TCP 是没有办法的,但是基于 UDP,就可以在 QUIC 自己的逻辑里面维护连接的机制,不再以四元组标识,而是以一个 64 位的随机数作为 ID 来标识,而且 UDP 是无连接的,所以当 IP 或者端口变化的时候,只要 ID 不变,就不需要重新建立连接。

机制二:自定义重传机制

在 TCP 里面超时的采样存在不准确的问题。例如,发送一个包,序号为 100,发现没有返回,于是再发送一个 100,过一阵返回一个 ACK101。这个时候客户端知道这个包肯定收到了,但是往返时间是多少呢?是 ACK 到达的时间减去后一个 100 发送的时间,还是减去前一个 100 发送的时间呢?事实是,第一种算法把时间算短了,第二种算法把时间算长了。

QUIC 也有个序列号,是递增的。任何一个序列号的包只发送一次,下次就要加一了。例如,发送一个包,序号是 100,发现没有返回;再次发送的时候,序号就是 101 了;如果返回的 ACK 100,就是对第一个包的响应。如果返回 ACK 101 就是对第二个包的响应,RTT 计算相对准确。

但是这里有一个问题,就是怎么知道包 100 和包 101 发送的是同样的内容呢?QUIC 定义了一个 offset 概念。QUIC 既然是面向连接的,也就像 TCP 一样,是一个数据流,发送的数据在这个数据流里面有个偏移量 offset,可以通过 offset 查看数据发送到了哪里,这样只要这个 offset 的包没有来,就要重发;如果来了,按照 offset 拼接,还是能够拼成一个流。
在这里插入图片描述

机制三:无阻塞的多路复用

同 HTTP 2.0 一样,同一条 QUIC 连接上可以创建多个 stream,来发送多个 HTTP 请求。但是,QUIC 是基于 UDP 的,一个连接上的多个 stream 之间没有依赖。这样,假如 stream2 丢了一个 UDP 包,后面跟着 stream3 的一个 UDP 包,虽然 stream2 的那个包需要重传,但是 stream3 的包无需等待,就可以发给用户。

HTTP/2:支持并行传输多个流,但由于它基于 TCP 协议,TCP 的顺序保证和重传机制会导致某个流的数据包丢失时,所有流的传输都可能被阻塞,直到丢失的数据包被重传。这导致的效果是,尽管流是并行传输的,但在遇到丢包时表现得像是“串行”处理。
QUIC:同样支持并行传输多个流,但由于它基于 UDP,并且每个流是独立的,某个流的数据包丢失不会阻塞其他流的传输。每个流可以独立地处理丢包和重传,这使得多个流之间没有依赖关系,即使一个流出现问题,其他流仍然可以继续传输数据。

机制四:自定义流量控制

这段文字在比较 TCP 协议和 QUIC 协议在流量控制和确认机制上的区别。下面我会尝试用通俗易懂的语言解释这段内容:

TCP 流量控制和确认机制:
  1. 滑动窗口协议:TCP 使用滑动窗口协议来进行流量控制,也就是接收端告诉发送端自己还能接收多少数据。窗口的起始点是接收端下一个预期要接收的数据包。
  2. 累计确认(ACK):TCP 的确认机制是基于序列号的。如果发送端收到了一个序列号的确认,那么意味着所有之前序列号的数据包也都被成功接收了。
  3. 问题:如果数据包没有按照顺序到达,即使后续的数据包先到了,接收端也不能调整窗口来接收这些后续的数据包,因为必须先接收到缺失的数据包。这可能导致发送端因为超时而重传已经成功送达的数据包,从而浪费带宽。
QUIC 流量控制和确认机制:
  1. 基于 offset 的 ACK:QUIC 使用基于 offset(数据偏移量)的确认机制。每个数据包都有一个唯一的 offset,只要接收到这个 offset 对应的数据包,就可以发送确认,而不管其他数据包是否到达。
  2. 独立的确认:在 QUIC 中,即使某些数据包丢失,只要其他包到达了,就可以独立确认,不需要等待丢失的包。
  3. 窗口控制:QUIC 为每个 stream(可以理解为数据流)单独控制窗口大小,这意味着它可以更精细地管理数据的接收能力。
  4. 窗口起始位置:QUIC 中的窗口起始位置是当前收到的最大 offset,窗口的大小是从这个 offset 到 stream 能容纳的最大缓存量。
通俗解释:

想象一下,TCP 就像一个严格的图书馆,只有当你按顺序收到所有借书证(数据包)时,图书馆(接收端)才会让你进去(开始接收数据)。如果借书证丢失了,即使后来的借书证到了,图书馆也不让你进去,你必须等待丢失的借书证。
而 QUIC 则像一个灵活的书店,你不需要按顺序拿到所有的收据(数据包),只要拿到任何一个收据,书店(接收端)就会给你相应的书(数据)。书店会告诉你它现在能放多少书(窗口大小),并且如果你丢了一张收据,书店会等待或者重新给你一张,而不会要求你必须按顺序拿到所有的收据。
总的来说,QUIC 的设计使得数据传输更加高效,减少了因为数据包乱序到达或丢失而导致的不必要的重传,提高了网络传输的效率。

想象你在一个邮局工作,你的工作是接收和整理来自各地的包裹(数据包)。每个包裹都有一个标签,上面写着它是第几个到达的(offset)。你的工作台上有一个篮子(stream 的缓存),这个篮子有固定的容量。
当你收到一个包裹,你首先检查它的标签,看看它是第几个到达的。如果它是目前为止最后一个(最大 offset),你就把这个包裹放在篮子的最前面。
然后,你看看篮子里还能放多少包裹。这个空间(从最后一个包裹到篮子边缘)就是你的"窗口大小",它表示你还能接收多少新的包裹。
如果篮子满了,你就不能再接收新的包裹,除非你先处理掉一些已经到达的包裹,为新包裹腾出空间。

三、HTTPS协议

客户端和卖家都有自己的公钥和私钥
这样,客户端给外卖网站发送的时候,用外卖网站的公钥加密。而外卖网站给客户端发送消息的时候,使用客户端的公钥。这样就算有黑客企图模拟客户端获取一些信息,或者半路截获回复信息,但是由于它没有私钥,这些信息它还是打不开。

数字证书

每个人都可以自己创建私钥和公钥,如果有人冒充是卖家怎么办,就需要权威部门证明其身份。权威部门CA颁发的证书(Certificate)。
证书里面有什么呢?

当然应该有公钥,这是最重要的;还有证书的所有者,就像户口本上有你的姓名和身份证号,说明这个户口本是你的;另外还有证书的发布机构和证书的有效期,这个有点像身份证上的机构是哪个区公安局,有效期到多少年。
这个证书是怎么生成的呢?会不会有人假冒权威机构颁发证书呢?就像有假身份证、假户口本一样。生成证书需要发起一个证书请求,然后将这个请求发给一个权威机构去认证,这个权威机构我们称为CA( Certificate Authority)。

过程详细解释

发送方:小明

  1. 信息的哈希计算
    小明对他写的邮件内容(比如“请在明天下午3点来我办公室谈一谈项目进展。谢谢!”)进行哈希计算,得到一个哈希值。假设哈希值是 a

  2. 生成签名
    小明用他的私钥对这个哈希值 a 进行加密,生成签名。这个签名表示小明的身份,并且可以用来验证邮件内容的完整性。

  3. 发送信息和签名
    小明将邮件内容和签名一起发送给小红。具体来说,发送的信息包括:

    • 邮件的原文(明文内容)
    • 签名(用小明的私钥加密的哈希值)

接收方:小红

  1. 验证签名
    小红收到邮件和签名后,进行以下步骤来验证邮件的完整性和发送者的身份:

    • 用小明的公钥解密签名,得到一个哈希值 a'
    • 用同样的哈希算法对收到的邮件内容(原文)进行哈希计算,得到哈希值 b
  2. 比较哈希值

    • 小红将 a'b 进行比较。
    • 如果 a'b 相等,说明邮件内容没有被篡改,并且确实是小明发的。
    • 如果 a'b 不相等,说明邮件内容在传输过程中可能被篡改了,或者签名不是小明的。

这下好了,你不会从外卖网站上得到一个公钥,而是会得到一个证书,这个证书有个发布机构 CA,你只要得到这个发布机构 CA 的公钥,去解密外卖网站证书的签名,如果解密成功了,Hash 也对的上,就说明这个外卖网站的公钥没有啥问题。

你有没有发现,又有新问题了。要想验证证书,需要 CA 的公钥,问题是,你怎么确定 CA 的公钥就是对的呢?

所以,CA 的公钥也需要更牛的 CA 给它签名,然后形成 CA 的证书。要想知道某个 CA 的证书是否可靠,要看 CA 的上级证书的公钥,能不能解开这个 CA 的签名。就像你不相信区公安局,可以打电话问市公安局,让市公安局确认区公安局的合法性。这样层层上去,直到全球皆知的几个著名大 CA,称为root CA,做最后的背书。通过这种层层授信背书的方式,从而保证了非对称加密模式的正常运转。
除此之外,还有一种证书,称为Self-Signed Certificate,就是自己给自己签名。这个给人一种“我就是我,你爱信不信”的感觉。这里我就不多说了。

HTTPS 的工作模式

非对称加密在性能上不如对称加密,那是否能将两者结合起来呢?例如,公钥私钥主要用于传输对称加密的秘钥,而真正的双方大数据量的通信都是通过对称加密进行的。

当然是可以的。这就是 HTTPS 协议的总体思路。
下面是细节举例

当你登录一个外卖网站的时候,由于是 HTTPS,客户端会发送 Client Hello 消息到服务器,以明文传输 TLS 版本信息、加密套件候选列表、压缩算法候选列表等信息。另外,还会有一个随机数,在协商对称密钥的时候使用。
这就类似在说:“您好,我想定外卖,但你要保密我吃的是什么。这是我的加密套路,再给你个随机数,你留着。”
然后,外卖网站返回 Server Hello 消息, 告诉客户端,服务器选择使用的协议版本、加密套件、压缩算法等,还有一个随机数,用于后续的密钥协商。
这就类似在说:“您好,保密没问题,你的加密套路还挺多,咱们就按套路 2 来吧,我这里也有个随机数,你也留着。”
然后,外卖网站会给你一个服务器端的证书,然后说:“Server Hello Done,我这里就这些信息了。”
你当然不相信这个证书,于是你从自己信任的 CA 仓库中,拿 CA 的证书里面的公钥去解密外卖网站的证书。如果能够成功,则说明外卖网站是可信的。这个过程中,你可能会不断往上追溯 CA、CA 的 CA、CA 的 CA 的 CA,反正直到一个授信的 CA,就可以了。
证书验证完毕之后,觉得这个外卖网站可信,于是客户端计算产生随机数字 Pre-master,发送 Client Key Exchange,用证书中的公钥加密,再发送给服务器,服务器可以通过私钥解密出来。
到目前为止,无论是客户端还是服务器,都有了三个随机数,分别是:自己的、对端的,以及刚生成的 Pre-Master 随机数。通过这三个随机数,可以在客户端和服务器产生相同的对称密钥。
有了对称密钥,客户端就可以说:“Change Cipher Spec,咱们以后都采用协商的通信密钥和加密算法进行加密通信了。”
然后发送一个 Encrypted Handshake Message,将已经商定好的参数等,采用协商密钥进行加密,发送给服务器用于数据与握手验证。
同样,服务器也可以发送 Change Cipher Spec,说:“没问题,咱们以后都采用协商的通信密钥和加密算法进行加密通信了”,并且也发送 Encrypted Handshake Message 的消息试试。当双方握手结束之后,就可以通过对称密钥进行加密传输了。
这个过程除了加密解密之外,其他的过程和 HTTP 是一样的,过程也非常复杂。
上面的过程只包含了 HTTPS 的单向认证,也即客户端验证服务端的证书,是大部分的场景,也可以在更加严格安全要求的情况下,启用双向认证,双方互相验证证书。

四、流媒体协议:如何在直播里看到美女帅哥?

每一张图片,我们称为一帧。只要每秒钟帧的数据足够多,也即播放得足够快。比如每秒 30 帧,以人的眼睛的敏感程度,是看不出这是一张张独立的图片的,这就是我们常说的帧率(FPS)。

每一张图片,都是由像素组成的,假设为 1024*768(这个像素数不算多)。每个像素由 RGB 组成,每个 8 位,共 24 位。

我们来算一下,每秒钟的视频有多大?

30 帧 × 1024 × 768 × 24 = 566,231,040Bits = 70,778,880Bytes

如果一分钟呢?4,246,732,800Bytes,已经是 4 个 G 了。

是不是不算不知道,一算吓一跳?这个数据量实在是太大,根本没办法存储和传输。如果这样存储,你的硬盘很快就满了;如果这样传输,那多少带宽也不够用啊!

怎么办呢?人们想到了编码,就是看如何用尽量少的 Bit 数保存视频,使播放的时候画面看起来仍然很精美。编码是一个压缩的过程。

ITU-T(国际电信联盟电信标准化部门,ITU Telecommunication Standardization Sector)与 MPEG 联合制定了 H.264/MPEG-4 AVC,这才是我们这一节要重点关注的。

经过编码之后,生动活泼的一帧一帧的图像,就变成了一串串让人看不懂的二进制,这个二进制可以放在一个文件里面,按照一定的格式保存起来(AVI、MPEG、RMVB、MP4、MOV、FLV、WebM、WMV、ASF、MKV。例如 RMVB 和 MP4)

直播过程

在这里插入图片描述
中间接流的是服务器,分发到距离客户端最近的服务器

编码

好的,我来通俗易懂地解释一下这段话。

视频压缩的基本概念

视频其实是由一张一张的图片(帧)组成的,但如果每一帧都是完整的图片,视频文件会非常大。因此,我们用了一些压缩技术,把视频分成了三种不同类型的帧:I 帧、P 帧和 B 帧。

三种帧的解释

  1. I 帧(关键帧)

    • I 帧是完整的图片,包含所有信息。它不需要参考其他帧就能显示出完整的画面。可以把它想象成一本漫画书中的一幅完整的图画。
  2. P 帧(前向预测编码帧)

    • P 帧不是完整的图片,而是记录了这一帧和之前的某个关键帧(或 P 帧)之间的差别。解码时需要先拿到之前的帧,再加上这个差别,才能还原出当前帧的画面。就像在上一幅漫画图画的基础上,增加一些变化,得到新的图画。
  3. B 帧(双向预测内插编码帧)

    • B 帧更复杂一点,它记录的是这一帧与前后两帧的差别。解码时需要参考前后两帧的画面,再加上 B 帧记录的差别,才能还原出当前帧的画面。可以想象成在前一幅和后一幅漫画图画的基础上,取它们的共同点,再加上一些变化,得到新的图画。

帧的排列方式

通常情况下,视频会按照 IBBP 的顺序排列帧。这是什么意思呢?这是说在视频压缩中,一段视频会先有一个 I 帧,接着是两个 B 帧,然后是一个 P 帧,如此循环。具体来说:

  • I(完整的关键帧)
  • B(需要前后两帧帮助解码的帧)
  • B(同上)
  • P(需要前一帧帮助解码的帧)

这种排列方式利用了时间上的相关性,即前后帧之间的相似性,使得视频在保证质量的同时能达到更高的压缩率。

这里需要进一步澄清关于 B 帧的概念和解码顺序。

B 帧的解码细节

B 帧确实是双向预测编码帧,这意味着它在编码时参考的是前后的帧,但是解码时也必须按照特定的顺序进行,以确保解码的正确性。具体来说,虽然 B 帧在编码时参考的是前后帧,但在解码时需要确保这些参考帧已经被解码。

详细解释和示例

假设我们有一段视频帧的顺序:IBBPBBP

  1. I 帧:完整的帧,可以独立解码。
  2. 第一个 B 帧:需要 I 帧和第一个 P 帧作为参考。
  3. 第二个 B 帧:需要 I 帧和第一个 P 帧作为参考。
  4. P 帧:需要 I 帧作为参考。

实际解码顺序

为了确保所有帧都能正确解码,解码器会按照不同于编码顺序的顺序进行解码。让我们具体看看:

  1. 解码顺序

    • 首先解码 I 帧(因为它独立解码)。
    • 然后解码第一个 P 帧(因为它只依赖 I 帧)。
    • 接下来解码第一个 B 帧(此时 I 帧和第一个 P 帧都已经解码)。
    • 然后解码第二个 B 帧(此时 I 帧和第一个 P 帧都已经解码)。
  2. 在这个示例中

    • 编码顺序是:I, B, B, P
    • 解码顺序是:I, P, B, B

在这里插入图片描述
在一帧中,分成多个片,每个片中分成多个宏块,每个宏块分成多个子块,这样将一张大的图分解成一个个小块,可以方便进行空间上的编码。

NALU

NALU(Network Abstraction Layer Unit)是网络抽象层单元的缩写,它在视频编码领域,特别是在H.264/AVC和H.265/HEVC视频编码标准中扮演着非常重要的角色。NALU是视频编码数据的基本单位,用于封装编码后的视频数据以及相关的控制信息。

在视频编码中,不同类型的NALU用于传递不同类型的信息:

  1. 视频数据:这指的是实际编码后的视频帧数据,包括I帧、P帧和B帧。

  2. 序列参数集(SPS, Sequence Parameter Set):SPS包含了整个视频序列的全局参数,例如图像尺寸、帧率、色彩空间等。解码器需要这些信息来正确解码视频序列。

  3. 图像参数集(PPS, Picture Parameter Set):PPS包含了单个图像(帧)的编码参数,如编码模式、量化参数等。

举例说明:
假设你正在观看一个H.264编码的视频,视频文件由多个NALU组成。视频文件的开始部分可能包含SPS和PPS,它们提供了解码整个视频序列所需的基本信息。随后,视频数据NALU包含了实际的视频帧数据,解码器会根据SPS和PPS中的参数来解码这些帧。

例如,一个视频文件可能包含以下NALU序列:

  • NALU 1: SPS,定义了视频的分辨率为1920x1080,帧率为30fps。
  • NALU 2: PPS,定义了编码参数,如宏块的编码模式和量化级别。
  • NALU 3: 视频数据,包含一个I帧的数据。
  • NALU 4: 视频数据,包含一个P帧的数据。
  • 以此类推…

在视频播放或编辑过程中,解码器会读取这些NALU,根据SPS和PPS中的参数来解码视频帧,并最终渲染出连续的视频画面。

在视频编码中,宏块(Macroblock)的编码模式和量化级别是两个关键的参数,它们对视频的压缩效率和质量有直接影响。

  1. 宏块的编码模式
    宏块是视频编码中的基本处理单元,通常由16x16像素块组成。编码模式决定了宏块是如何预测和编码的。编码模式主要有以下几种:

    • I模式(内码模式):使用先前编码的帧(通常是关键帧)进行预测,适用于随机运动的场景。
    • P模式(前向预测模式):使用单一先前帧进行预测,适用于预测误差较小的场景。
    • B模式(双向预测模式):使用两个先前或未来的帧进行预测,通常用于减少时间上的预测误差。
  2. 量化级别
    量化是视频编码中用于减少数据量的关键步骤。量化级别决定了量化过程中的压缩强度。量化级别越高,压缩率越高,但同时视频质量也越差,因为量化误差会增加。

一个视频,可以拆分成一系列的帧,每一帧拆分成一系列的片,每一片都放在一个 NALU 里面,NALU 之间都是通过特殊的起始标识符分隔,在每一个 I 帧的第一片前面,要插入单独保存 SPS 和 PPS 的 NALU,最终形成一个长长的 NALU 序列。

推流和拉流: RTMP 协议

rtmp 通常运行在TCP端口1935上
TSP运行在TCP或UDP端口554上
RTMP适合于需要低延迟和高互动性的直播场景,如在线游戏直播、互动直播课堂等。
RTSP适合于需要精细控制播放过程的场景,如视频点播服务、远程监控等。

需要将这个二进制的流打包成网络包进行发送,这里我们使用RTMP 协议

那这个格式是不是就能够直接在网上传输到对端,开始直播了呢?其实还不是,还需要将这个二进制的流打包成网络包进行发送,这里我们使用RTMP 协议。这就进入了第二个过程,推流。

RTMP 是基于 TCP 的,因而肯定需要双方建立一个 TCP 的连接。在有 TCP 的连接的基础上,还需要建立一个 RTMP 的连接,也即在程序里面,你需要调用 RTMP 类库的 Connect 函数,显示创建一个连接。

RTMP 为什么需要建立一个单独的连接呢?

因为它们需要商量一些事情,保证以后的传输能正常进行。主要就是两个事情,一个是版本号,如果客户端、服务器的版本号不一致,则不能工作。另一个就是时间戳,视频播放中,时间是很重要的,后面的数据流互通的时候,经常要带上时间戳的差值,因而一开始双方就要知道对方的时间戳。

未来沟通这些事情,需要发送六条消息:客户端发送 C0、C1、 C2,服务器发送 S0、 S1、 S2。

首先,客户端发送 C0 表示自己的版本号,不必等对方的回复,然后发送 C1 表示自己的时间戳。

服务器只有在收到 C0 的时候,才能返回 S0,表明自己的版本号,如果版本不匹配,可以断开连接。

服务器发送完 S0 后,也不用等什么,就直接发送自己的时间戳 S1。客户端收到 S1 的时候,发一个知道了对方时间戳的 ACK C2。同理服务器收到 C1 的时候,发一个知道了对方时间戳的 ACK S2。

于是,握手完成。

双方需要互相传递一些控制信息,例如 Chunk 块的大小、窗口大小等。

在 RTMP 中,数据被分成较小的块(Chunk)进行传输。Chunk 块的大小(Chunk Size)指的是每个块的字节数。默认情况下,RTMP 的 Chunk 大小是 128 字节,但在实际使用中,服务器和客户端可以协商并调整这个值。
作用
调整 Chunk 大小有助于优化传输效率和减少延迟。如果 Chunk 块过大,可能会导致传输延迟;如果过小,可能会增加协议开销。
通过设定窗口大小,RTMP 可以控制流量,确保网络负载在可控范围内。接收方在接收指定窗口大小的数据后,会发送一个确认消息,告知发送方可以继续发送数据。

实际传输过程示例
假设:

Chunk 块的大小:512 字节
窗口大小:4096 字节
传输 8192 字节的视频数据:

服务器发送数据:

数据被分成 16 个 Chunk,每个 Chunk 512 字节。
在不等待确认的情况下,服务器先发送 8 个 Chunk(4096 字节)。
客户端接收数据:

客户端接收了前 8 个 Chunk,并发送确认消息。
服务器收到确认消息后,继续发送剩下的 8 个 Chunk。

真正传输数据的时候,还是需要创建一个流 Stream,然后通过这个 Stream 来推流 publish。

推流的过程,就是将 NALU 放在 Message 里面发送,这个也称为RTMP Packet 包。

拉流

先读到的是 H.264 的解码参数,例如 SPS 和 PPS,然后对收到的 NALU 组成的一个个帧,进行解码,交给播发器播放,一个绚丽多彩的视频画面就出来了。

五、P2P协议:我下小电影,99%急死你

下载一个文件可以使用 HTTP 或 FTP,这两种都是集中下载的方式,而 P2P 则换了一种思路,采取非中心化下载的方式;

P2P 也是有两种,一种是依赖于 tracker 的,也即元数据集中,文件数据分散;另一种是基于分布式的哈希算法,元数据和文件数据全部分散。

  • 10
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值