重头梳理网络协议栈

532 阅读21分钟

本文正在参与 “网络协议必知必会”征文活动

Web Protocol

前言

从 1989 年 HTTP 0.9 发布开始,互联网发展已近三十年,各种概念已层出不穷。如 HTTPs, WebSocket, HTTP 2.0, Spdy 等等。作为计算机的基础学科,相信大家或多或少已了解其中的点。在这里呢,想通过另一个面,来看看它们是如何演化,背后的设计初衷,解决的问题,以及一些实践来带大家重头回顾。

我们先抛出几个问题,在最后让我们一起来寻找答案

  1. 打开浏览器访问网页时,它如何将数据安全的传送给我们?

  2. HTTP 2.0 为何时隔10年才出现,它解决了什么问题?

  3. Socket 和 Http 的关系?

  4. Web Socket 又是什么?它是工作在哪一层的?

  5. 想要更轻量,更有效率的数据传输如何做?

声明:原文最早发表于四年前个人博客,本文已做部分更新,如仍有资料过时请指正~

1. 网络协议

1.1 七层协议

OSI model: The Open Systems Interconnection model

一种概念模型,由国际标准化组织(ISO)提出,一个试图使各种计算机在世界范围内互连为网络的标准框架。

通俗的说,为了全世界的计算机之间相互通信,而设计的一种协议框架,所有需要联网的设备或软件都需要遵循它的规则。另外,为了尽可能避免所有功能混杂在一起,让不同公司或组织更聚焦在一个领域上,才有了现在的分层模型。

OSI

我们可以看到在每一层,根据实际应用场景,都有对应的协议被设计出来。

从上面的模型我们可以知道,大的结构上,分为面向上层应用与面向网络媒介两个部分。应用层上主要是面向数据与数据的传输,网络媒介主要是面向网络通信与硬件。

1.2 TCP/IP

基于 OSI 协议模型衍生而来,现今使用最多的就是 TCP/IP 协议栈了。注意这里的 TCP/IP 协议栈并不只是指 TCP 与 IP 协议的合集,它是一种更简化的 OSI 模型。

通过下图可以看到它与 OSI 模型区别:

TCP/IP 与 OSI 对应模型

从 tcp/ip 协议规范来看,应用层,表示层与传输层被合并在一起。主要原因是这三层,在当今的实际使用中有相当多耦合关系,也很难将它们刻意的分离。

实际上现在的 TCP/IP 模型更加简单明了,更适应现在的应用场景。

TCP/IP 模型

除了以上的协议栈外,还有当今两个重要的协议:TLS 与 SPDY 。

1.3 TLS/SSL

TLS/SSL 是当今一个比较重要的安全传输协议,它不在上面所列的协议栈中。但它被设计位于应用层与传输层之间。以确保数据在传输之前是安全的,但又不破坏原有的数据协议。应用层协议(例如:HTTP、FTP、Telnet等等)能透明的创建于TLS 协议之上。

我们经常听到的 HTTPS, 这个 S 指的就是 secure (安全),同时分为 SSL 和 TLS 协议。它被 Netscape 公司在 1994 年推出。

SSL: (Secure Socket Layer,1994 年推出, 第一版作者: 塔希尔·盖莫尔)。

SSL通过互相认证、使用数字签名确保完整性、使用加密确保私密性,以实现客户端和服务器之间的安全通讯。

TLS: (Transport Layer Security, 1999 年推出)

它建立在SSL 3.0协议规范之上,作用同于 SSL, 与 SSL 3.0 差别极小,可以理解为SSL 3.1。

无论是 SSL 还是 TLS 都分为两层:  握手协议与记录协议

SSL/TLS Protocol

SSL 因为被报有各种安全问题,所以基本上现在所使用的都是 TLS。

以下是建立连接的过程:

SSL/TLS authentication

它必须建立在可靠的数据传输基础上,所以一般是在TCP之上。

同时,如果一但使用了 SSL/TLS 做安全验证,建立连接的时间实际上是会变长的,如图:

SSL/TLS Handshake Issue

1.4 SPDY

SPDY(发音如英语:speedy),2010 年 9 月 一种开放的网络传输协议,由 Google 开发,用来发送网页内容的协议诞生,用以最小化网络延迟,提升网络速度,优化用户的网络使用体验。它基于 TCP 的应用层协议,也是 HTTP/2 的前身。它最早被应用在 Chrome 6。

设计 SPDY 的目的在于降低网页加载时间。通过优先级和多路复用,SPDY 使得只需要创建一个 TCP 连接即可传送网页内容及图片等资源。SPDY 中广泛应用了 TLS 加密,传输内容也均以 gzip 或 DEFLATE 格式压缩(与 HTTP 不同,HTTP的头部并不会被压缩)。另外,除了像 HTTP 的网页服务器被动的等待浏览器发起请求外,SPDY 的网页服务器还可以主动推送内容。

SPDY

SPDY 并不用于取代 HTTP,它只是修改了 HTTP 的请求与应答在网络上传输的方式;这意味着只需增加一个 SPDY 传输层,现有的所有服务端应用均不用做任何修改。 当使用 SPDY 的方式传输,HTTP 请求会被处理、标记简化和压缩。比如,每一个 SPDY 端点会持续跟踪每一个在之前的请求中已经发送的 HTTP 报文头部,从而避免重复发送还未改变的头部。而还未发送的报文的数据部分将在被压缩后被发送。

Google 之所以改动 HTTP 协议而不是 TCP/IP,是因为改 HTTP 只需更新 Browser 和 web server 就行了,而改动 TCP/IP 牵扯面太广,需要更新所有的路由器,服务器和客户端的操作系统。

SPDY Connections over the Mobile Web

1.3 TCP

无论应用层的协议如何定义、使用,都要基于一条已经连接的通路去发送与接收数据,我们可以试着从我们最熟悉的数据传输层开始,来了解连接的建立与传输。

这一层有两个重要的协议,TCP 与 UDP。

TCP 协议的运行可分为三个阶段:连接创建(connection establishment)、数据传送(data transfer)和连接终止(connection termination)

连接创建

TCP 用三次握手(three-way handshake)过程创建一个连接。在连接创建过程中,很多参数要被初始化,例如序号被初始化以保证按序传输和连接的强壮性。

一对终端同时初始化一个它们之间的连接是可能的。但通常是由一端打开一个套接字(socket)然后监听来自另一方的连接,这就是通常所指的被动打开(passive open)。服务器端被被动打开以后,用户端就能开始创建主动打开(active open)。

  1. 客户端通过向服务器端发送一个 SYN 来创建一个主动打开,作为三路握手的一部分。客户端把这段连接的序号设定为随机数A。

  2. 服务器端应当为一个合法的 SYN 回送一个 SYN/ACK。ACK 的确认码应为 A+1,SYN/ACK 包本身又有一个随机产生的序号B。

  3. 最后,客户端再发送一个 ACK。当服务端受到这个 ACK 的时候,就完成了三路握手,并进入了连接创建状态。此时包的序号被设定为收到的确认号 A+1,而响应号则为 B+1。

TCP 的三次握手

通俗的讲是: A: 在吗? B: 在,来!A: 好!

连接重试

如果服务器端接到了客户端发的SYN后回了SYN-ACK后客户端掉线了,服务器端没有收到客户端回来的ACK,那么,这个连接处于一个中间状态,即没成功,也没失败。于是,服务器端如果在一定时间内没有收到的TCP会重发SYN-ACK。在Linux下,默认重试次数为5次,重试的间隔时间从1s开始每次都翻倍,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s才知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s,TCP才会断开这个连接。

数据传输

主机收到一个 TCP 包时,用两端的IP地址与端口号来标识这个 TCP 包属于哪个session。

TCP 或 UDP 使用了端口号(Port number)的概念来标识发送方和接收方的应用层,通常也称为 Internet sockets。对每个 TCP 连接的一端都有一个相关的16位的无符号端口号分配给它们。端口被分为三类:众所周知的、注册的和动态/私有的。众所周知的端口号是由因特网赋号管理局(IANA)来分配的,并且通常被用于系统一级或根进程。众所周知的应用程序作为服务器程序来运行,并被动地侦听经常使用这些端口的连接。例如:FTP (20 and 21), SSH (22), TELNET (23), SMTP (25), HTTP over SSL/TLS (443), and HTTP (80)等。注册的端口号通常被用来作为终端用户连接服务器时短暂地使用的源端口号,但它们也可以用来标识已被第三方注册了的、被命名的服务。动态/私有的端口号在任何特定的TCP连接外不具有任何意义。可能的、被正式承认的端口号有65535个。

超时重传

另外,对于 TCP 数据传输 ,有一个重要的概念提到的就是 超时重传。这个机制是进一步来证明为何 TCP 是一种可靠的传输。

发送方使用一个保守估计的时间作为收到数据包的确认的超时上限。如果超过这个上限仍未收到确认包,发送方将重传这个数据包。每当发送方收到确认包后,会重置这个重传定时器。

这个超时定义就是 RTT (来回通讯延迟(Round-trip delay time)) 在的定义。

连接终止

使用了四次握手 (four-way handshake) ,终止一个 TCP 连接

image.png

通俗的讲:A: 关了吧。 B: 好,稍等!  B: 关吧! A: Bye~

更详细资料 Wikipedia

1.4 UDP

除了可靠的 TCP 连接,还有一种称为不可靠的连接机制: 用户数据包协议(User Datagram Protocol,缩写为UDP),是一个简单的面向数据报的传输层协议。此协议由 David P. Reed 在 1980 年设计出来,并纳入 RFC 768 成为正式的规范。

两者各有利弊,UDP 无需建立连接,不保证传输的可靠性,也没有重试机制,所以它的效率更高,当然数据传输的质量也不保证。

在TCP/IP模型中,UDP为网络层以上和应用层以下提供了一个简单的接口。UDP只提供数据的不可靠传递,它一旦把应用程序发给网络层的数据发送出去,就不保留数据备份(所以UDP有时候也被认为是不可靠的数据报协议)。UDP在IP数据报的头部仅仅加入了复用和数据校验(字段)。

网络上的众多使用UDP协议的关键应用一定程度上是相似的。这些应用包括域名系统(DNS)、简单网络管理协议(SNMP)、动态主机配置协议(DHCP)、路由信息协议(RIP)和某些影音流服务等等。

1.5 Socket

上面我们提到了 Socket ,按字面意思是插座,也就是建立一个通道,进行通信或连接。

Socket 工作流

根据 OSI 模型,它是基于传输层 (TCP/UDP) 上的一种具体实现。有了 TCP/UDP 协议,双方可以按照协议的规定,搭建起连接,一但连接搭建起来,我们就称它为 Socket 连接已建立,相当于插到插座上了。

在计算机科学中

它通常代表网络套接字(Network socket),又译网络插槽,是电脑网络中进程间互相通信的一种机制。使用互联网协议(Internet Protocol)为通信基础的网络套接字,称为 Internet socket。因为互联网协议的流行,现代绝大多数的网络套接字,都是属于 Internet socket。

在操作系统中

它是一种提供进程间通信的机制。通常会为应用程序提供一组应用程序接口(API),称为套接字接口(socket API)。应用程序可以通过套接字接口,来使用网络套接字,以进行数据交换。最早的套接字接口来自于4.2 BSD,因此现代常见的套接字接口大多源自 Berkeley 套接字(Berkeley sockets)标准。在套接字接口中,以IP地址及通信端口组成套接字地址(socket address)。远程的套接字地址,以及本地的套接字地址完成连接后,再加上使用的协议(protocol),这个五元组(five-element tuple),作为套接字对(socket pairs),之后就可以彼此交换数据。

例如,再同一台计算机上,TCP 协议与 UDP 协议可以同时使用相同的port而互不干扰。 操作系统根据套接字地址,可以决定应该将数据送达特定的进程或线程。这就像是电话系统中,以电话号码加上分机号码,来决定通话对象一般

所以也可以理解为 socket = IP + Port

这里有 C 写的 Socket Demo 大家可以参考。其中 accept 函数也就是实现了三次了握手的过程。

2. 应用层

2.1 HTTP

应用层里最常用的协议之一,Hypertext Transfer Protocol (HTTP) 超文本传输协议。

HTTP 历史

- 1965 年,一个叫 Ted Nelson的 26 岁年轻人,提出了超文本与超媒体的概念。(真是英雄出少年)

- 1989 年,时隔多年,一个叫 Tim Berners-Lee 和他的团队在 CERN(欧洲核子研究组织)基于超文本概念,开始了 WorldWideWeb 项目。也就是我们现在耳熟能详的 WWW (万维网),将超文本系统与互联网结合在一起,将超文本数据发送出去。第一个版本只有一个方法,叫 GET ,它可以从服务器请求一个页面到客户端,这个页面就是 HTML 页。

- 1991 年,第一个 HTTP 的文档 HTTP 0.9 定义出来。世界上第一个网站在 CERN 搭建。

- 1996 年,HTTP 1.0 标准发布。Dave Raggett 领导的 HTTP Working Group (HTTP WG) 扩充了 HTTP 的协议,增加了富媒体信息,安全协议,header 域等。

- 1997 年,HTTP 1.1 标准被制定。

- 2015 年,HTTP 2.0 发布

关于 HTTP Session

一个 HTTP session 是一个网络的 request-response 序列。一个 HTTP 客户端初始时请求建立一个 TCP 连接到服务器的指定端口(通常为 80)。一个 HTTP 服务端一直监听来自客户端的 request 信息。基于接收的 request, 服务器返回状态行,如 "HTTP/1.1 200 OK"。

2.1 HTTP 0.9 ~ 2.0 进化之路

HTTP0.9~2.0

HTTP 0.9

只有 GET 一只方法,不支持请求头

HTTP 1.0

增加了请求头,除了 GET 外,多了很多方法,如 POST,  HEAD, PUT 等。

HTTP 1.1

由于 HTTP 1.0 只保持短暂的连接,每次请求后,TCP 连接都会被关闭,导致大量的时间耗费在了建立连接上。另一个问题是, HTTP 1.0 的请求像一个队列,遵循 FIFO (先进先出)的原则,当第一个请求的 Response 返回后,第二个 Request 才能发出,这就是所说的 head of line blocking (HOL blocking)。

HTTP 1.1 对两个主要问题进行了改进

  1. 支持了持久连接,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。

  2. 允许客户端不用等待上一次请求结果返回,就可以发出下一次请求,但服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果,以保证客户端能够区分出每次请求的响应内容,这样也显著地减少了整个下载过程所需要的时间。

此外,1.1 协议中还支持了身份认证、状态管理和 Cache 缓存等机制,可以很方便的实现如断点续传等场景。

HTTP 1.0 与 HTTP 1.1 的主要区别可以看这里:Key Differences between HTTP/1.0 and HTTP/1.1

image.png

HTTP Pipelining

它是将多个HTTP 请求整批提交的技术,而在发送过程中不需先等待服务端的回应。

请求结果管线化使得 HTML 网页加载时间动态提升,特别是在具有高延迟的连接环境下。在宽带连接中,加速不是那么显著的,因为需要服务器端应用 HTTP/1.1 协议:服务器端必须按照客户端的请求顺序恢复请求,这样整个连接还是先进先出的,对头阻塞(HOL blocking)可能会发生,造成延迟。

image.png

HTTP 2.0

实际上 HTTP 1.1 使用了很长时间后,一直没有经过大的改进,基本上也就是在原来的基础上进行小范围的协议优化与扩充。直到 Google 推出了 SPDY 后,HTTP 2.0 基于 SPDY 的基础上,被正式提出。

它主要解决了 HTTP 1.0 或 1.1 中始终存在的 HOL blocking 问题。

HTTP 2.0 二进制分帧

它并没有破坏原有的 HTTP 结构,仅仅是将数据采用了二进制传输,比起以前的文本传输,更紧凑与高效。在二进制分帧层上,HTTP2.0 会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码,其中 HTTP1.x 的首部信息会被封装到 Headers 帧,而我们的 request body 则封装到 Data 帧里面。

HTTP 2.0 连接

新的二进制分帧机制改变了客户端与服务器之间交换数据的方式。 为了说明这个过程,我们需要了解 HTTP/2 的三个概念:

数据流:已建立的连接内的双向字节流,可以承载一条或多条消息。

消息:与逻辑请求或响应消息对应的完整的一系列帧。

:HTTP/2 通信的最小单位,每个帧都包含帧头,至少也会标识出当前帧所属的数据流。

这些概念的关系总结如下:

  1. 所有通信都在一个 TCP 连接上完成,此连接可以承载任意数量的双向数据流。

  2. 每个数据流都有一个唯一的标识符和可选的优先级信息,用于承载双向消息。

  3. 每条消息都是一条逻辑 HTTP 消息(例如请求或响应),包含一个或多个帧。

  4. 帧是最小的通信单位,承载着特定类型的数据,例如 HTTP 标头、消息负载,等等。 来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。

这里的关键是数据流有了优先级,而不必像以前按顺序处理,服务端可以根据自定义的优先级处理 request。

还有一个大的改进是服务器推送,HTTP 2.0 不必像以前一样,必须依赖 request 才发送 response ,打破了严格的请求-响应语义。即使客户端没有 Request 请求,服务端也可以发送 Response 给客户端。

大家可以在命令行下,输入以下命令,看看两者有何不同。

telnet www.google.com 80

  • GET / HTTP/1.0
  • GET / HTTP/1.1

2.2. HTTPS

关于 HTTPS 中的 S ,即安全传输,之前已经讲到了,它是在介于传输层与应用层之间的一层安全协议,基于可靠传输(如 TCP )之后,对信息的一层加密封装。

相信大家可以在很多地方搜到 HTTPS 的详细解释。这里就不逐一展开了。我们只要记住以下几点即可。

首先是,对称加密与非对称加密,两种加密方式最大的区别在于,对称加密没有数据长度限制,非对称有长度限制,所以要使用非对称加密传输对称加密的密钥。

其次,非对称加密中的公钥,不能直接发送给对方,否则会产生中间人攻击的场景。必须有一个可靠的第三方做中间人。这就是我们大家所熟知的 CA (数字证书的颁发机构)。

这个 CA 通常会根据域名来做服务器端的认证,而且 CA 的公钥是被内置在浏览器或操作系统中的,所以 CA 的公钥是不用传输的。这样就避免了中间人的证书伪造。

之前有人已经写过一篇 非常通俗易懂的文章,大家可以做参考。

2.3 WebSocket

提到应用层协议,其实还有一个比较流行的协议就是 WebSocket ,它在 2011 年被被IETF 定为标准 RFC 6455。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

为什么叫 WebSocket 呢?它和 Socket 什么关系?

首先 WebSocket 和 Socket 没有半毛钱关系,它之所以这么叫,完全是为了利于理解。之所以强调 Web,是因为它可以基于 HTTP 连接进行协议切换。还记得之前 HTTP /1.1 里的 request header 吗?我们可以定义 Upgrade: websocket Connection: Upgrade。然后服务器的 response 会返回:  HTTP/1.1 101 Switching Protocols。即可切换到 WebSocket 协议。

WebSocket communication

web socket 使用 ws 或 wss 做为统一的标识符。如:ws://www.abc.com

参考实践:基于 node JS 的 WebSocket 客户端与服务端开源项目

3. 总结

回答文章开头所提出的问题

1. 当我们打开浏览器访问一个网页时,它是如何将数据安全的发送给我们的?

如果访问的是有域名的网址,先通过 DNS 服务器解析域名。如果是 IP 就直接访问。然后排开最底层的网络协议不看,从传输层开始,先进行三次握手,有 HTTPs 的先建立加密通道,拿到传输密钥。然后基于应用层协议,比如 HTTP,进行 Request 与 Response 的数据传输。根据使用的上层协议(HTTP/FTP/Others)不同,建立的连接有可能是持续的,也有可能是一次性的。

2. Http 2.0 为何时隔10年才出现,它解决了什么问题?

HTTP 1.1 出来后,基本上在上面做补充与小调整,没有实质的变化。直到十年后,凭借 Google 强大的技术研发能力,加上现代的网络基础设施,提出了二进制流传输方式,解决了 HOL blocking 问题。并最终成为了 HTTP /2.0 的标准。目前,在应用层上,可优化的空间越来越小,接下来的瓶颈更多是传输层了,但传输的层的变化,会牵扯所有现有的浏览器与操作系统,所以这一块的演化没有想像的那么快。也许将来有一天,会有更新的技术出现。

3. Socket 和 Http 什么关系?

Socket 是通道,在网络领域,它是一个 Network Socket ,建立两个端之间的一条管道,进行数据的传输。HTTP 是基于这个管理,进行超文本的传输。简单的讲, Socket 一但建立,你可以传输任何东西,HTTP 也好, FTP 也好。取决于你的应用协议是什么。

4. Web socket 又是什么?它是工作在哪一层的?

Web Socket 只是一个双工通信概念,它为了解决 HTTP 只能通过 Request 与 Response 的形式发送信息的模式,无论是客户端还是服务端都可以主动的发送数据。HTTP /2.0 也加入了此概念。但 WebSocket 发明的比它早,所以它是前辈。

它工作在传输之上,应用层的一种,可以理解为 HTTP 的升级版。因为它可以利用 HTTP 的协议,进行升级使用,所以称为 Web Socket.

5. 想要更轻量,更有效率的传输如何做?

你可以直接面向 Socket 编程,传输层之上进行开发,自己发明更轻量,更高效的应用层协议。现有的操作系统,对 TCP 接口有非常标准,完善的编程接口,开发也不是难事了。所以你可根据应用的场景,定制更合适的应用层协议。推荐买一本 Socket 网络编程开始学。