TCP/IP 协议栈系列(九):KCP 协议详解
TCP/IP 协议栈系列(八):HTTPS 性能与优化
了解了之前文章介绍的HTTPS原理后,HTTPS 具备身份验证、信息加密与完整性校验等安全特性,且未对TCP和HTTP协议做任何修改。但通过增加新协议以实现更安全的通信必然需要付出代价,所以 HTTPS 的性能损耗有以下几个方面
TCP/IP 协议栈系列(七):TLS/SSL 握手过程
TCP/IP 协议栈系列(六):HTTPS 基础知识
HTTP协议传输的数据都是未加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常不安全。为了保证这些隐私数据能加密传输,于是网 景公司设计了SSL(Secure Sockets Layer)协议用于对HTTP协议传输的数据进行加密,从而就诞生了HTTPS。
TCP/IP 协议栈系列(五):HTTP2 协议详解
TCP/IP 协议栈系列(四):HTTP1.0 与 HTTP1.1 区别
http1.1 是当前 web 应用使用的主流的 http 协议版本,http1.1 在 http1.0 的基础上做了一些的优化,我们看看具体的区别有哪些。
http/1.0
- 多格式文件支持,这使得互联网不仅可以传输文字,还能传输图像、视频、二进制文件。
- 多请求方式支持,除了GET命令,还引入了POST命令和HEAD命令。
- HTTP请求和回应的格式也变了。除了数据部分,每次通信都必须包括头信息(HTTP header),用来描述一些元数据。
- 其他的新增功能还包括状态码(status code)、多字符集支持、多部分发送(multi-part type)、权限(authorization)、缓存(cache)、内容编码(content encoding)等。
缺点:
- 每个TCP连接只能发送一个请求。发送数据完毕,连接就关闭,如果还要请求其他资源,就必须再新建一个连接。
- TCP连接的新建成本很高,因为需要客户端和服务器三次握手,并且开始时发送速率较慢(slow start)
http/1.1
- 引入了持久连接(persistent connection),即TCP连接默认不关闭,可以被多个请求复用,不用声明Connection: keep-alive,使用connection: close关闭TCP连接
- 引入了管道机制(pipelining),即在同一个TCP连接里面,客户端可以同时发送多个请求。这样就进一步改进了HTTP协议的效率。但是服务器还是要按照顺序来回应请求。
- 可以使用分块传输编码。
- 新增了许多动词方法:PUT、PATCH、 OPTIONS、DELETE。
- 客户端请求的头信息新增了 Host 字段,用来指定服务器的域名。
缺点:
虽然1.1版允许复用TCP连接,但是同一个TCP连接里面,所有的数据通信是按次序进行的。服务器只有处理完一个回应,才会进行下一个回应。要是前面的回应特别慢,后面就会有许多请求排队等着。这称为 ”队头堵塞“。
为了避免这个问题,只有两种方法:一是减少请求数,二是同时多开持久连接。
http1.0 串行连接
早期的 http 协议对于每一个单独的请求都独占一个 tcp 连接,并且请求处理都是串行化的。例如有三个 css 文件需要加载,同属于一个协议、域名、端口。那浏览器需要顺序的发起 4 个请求,每次都需要重新打开一个 tcp 的通道,在请求资源完成后断开连接,再开启下一个新的连接去处理队列中的请求。显然当资源数量越来越大,页面发起的请求越来越多情况下,页面加载时间会过长,页面白的几率越高。
并行连接
为了提高网络的吞吐能力,改进后的 http 协议允许客户端同时打开多个 TCP 连接,并行的去请求多个资源,充分利用带宽。请求的传输时间是重叠的,单总体比串行连接低很多。考虑到每一个连接都会消耗系统资源,并且服务器需要处理海量的用户并发请求,浏览器会对并发请求数量做一定的限制。一般浏览器的限制是 6 个。
http1.1 keep-alive 连接
通过一个图示来说明,详细描述参考本系列的文章 HTTP协议概述 的 Keep-Alive 机制
所以,我们看到,并行连接和持久连接这两种优化是相辅相成的,并行连接使得首次加载页面可以同时打开多个 TCP 连接,而持久连接保证了后续的请求复用已打开的 TCP 连接,这也是现代 Web 页面的普遍机制。
需要注意的是:不管是 http 短连接还是长连接,它们的请求和响应都有有序的,都是等上一次请求响应后,才接着下一个请求的,那能不能不等第一次请求回来,我就开始发第二次请求呢?这就引出 http 管道化连接
http1.1 pipline 管道连接
HTTP 管道允许客户端在同一个 TCP 通道内连续发起多个请求,而不必等待前一个请求的响应,消除了往返延迟时间差;但是实际情况是由于 http1.x 的限制,不允许数据在返回通道中交错返回,也就是说必须顺序返回,否则无法识别出对应标示,会显示错乱。那么这种情况下,例如,我们发送了 4 个请求,其中三个请求的 css 资源很快反回,但其中一个 html 资源服务端处理很慢会导致在 html 后面的 css 资源必须先等待 html 资源返回才能正常返回,严重时会造成缓冲区溢出,这个就是典型的 队首阻塞
问题。正是因为这个问题,所以 pipline 管道技术并没有大面积的推广和使用。
注意:队首阻塞问题在 http2.0 中得到了解决,在 http2.0 中在详细介绍
参考资料
TCP/IP 协议栈系列(三):HTTP 协议概述
HTTP 简介
HTTP协议是 Hyper Text Transfer Protocol(超文本传输协议)的缩写, 是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议。
HTTP 是一个基于 TCP/IP 协议来传递数据(网页、文件等)的应用层协议。
HTTP 特点
1、简单:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有 GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP 协议简单,使得 HTTP 服务器的程序规模小,因而通信速度很快。
2、灵活:HTTP 允许传输任意类型的数据对象。正在传输的类型由 Content-Type 加以标记。
3、无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
4、无状态:HTTP 协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
5、支持B/S及C/S模式。
请求过程
在浏览器输入:https://www.qq.com/ 浏览器向网站所在的服务器发送了一个 Request 请求,服务器接收到这个 Request 之后进行处理和解析,然后返回一个 Response 响应,然后传回给浏览器,Response 里面就包含了页面的 html 源代码等内容,浏览器再对其进行解析便将网页呈现了出来。
HTTP 头部
HTTP 头部是一个传递额外重要信息的 键值对
。主要分为:通用头部,请求头部,响应头部和实体头部。
通用头部
:
- Connection:客户端和服务端使用的 tcp 连接类型
- Date:报文的时间
- Cache-Control:缓存的控制
- Transfer-Encoding:报文的传输方式;chunked 分块传输
请求头部
:
- Accept:告诉服务器允许的媒体类型 Accept: text/plain
- Accept-Encoding:客户端支持的接收的编码方法 Accept-Encoding: gzip, deflate
- User-Agent:浏览器的身份标识字符串 User-Agent: Mozilla/……
- Referer:浏览器所访问的前一个页面
响应头部
:
- Server:告知客户端服务器信息 Server: Apache/1.3.27 (Unix) (Red-Hat/Linux)
- Location:表示重定向后的 URL
实体头部
:
- Allow:对某网络资源的有效的请求行为,不允许则返回405
- Content-encoding:返回内容的编码方式
- Content-Length:返回内容的字节长度
- Content-Language:响应体的语言
Keep-Alive 机制
这里的 keep-alive 是指 http 协议里 header 头里设置的 Connection: keep-alive
,请求头设置了 keep-alive 之后就会告诉对方这个请求响应完成后不要关闭,下一次咱们还用这个请求继续交流,我们用一个示意图来更加生动的表示两者的区别:
为啥需要 keep-alive?
在 HTTP/1.0 中,浏览器每次发起 HTTP 请求都要与服务器创建一个新的 TCP 连接,服务器完成请求处理后立即断开 TCP 连接,服务器不跟踪每个客户也不记录过去的请求。创建和关闭 TCP 连接的过程需要消耗资源和时间,为了减少对 TCP 资源消耗,缩短响应时间,就需要重用 TCP 连接。
特别注意:所以我们需要清楚概念,有些地方也叫 keep-alive 为 http 长连接,这个说法并不是准确;因为 http 压根连连接都没有,真正连接是传输层的 tcp 才回建立连接,所以 http 不存在长连接一说,只有底层的 tcp 才存在长连接。这里 keep-alive 说的是当前的 tcp 连接是可复用的也叫 tcp 长连接或者叫 tcp 保活。
什么时候应该使用 keep-alive?
应该说当前大部分的场景已经使用 http1.1 都默认使用的 keep-alive tcp 长连接的方式;对于 http1.0,如果 http 客户端比较多,请求比较频繁,是需要手动设置 keep-alive
keep-alive 缺点
长时间的保持 TCP 连接时容易导致系统资源被无效占用,若对 Keep-Alive 模式配置不当,将有可能比非 Keep-Alive 模式带来的损失更大。因此,我们需要正确地设置 keep-alive timeout 参数,当 TCP 连接在传送完最后一个 HTTP 响应,该连接会保持 keepalive_timeout 秒,之后就开始关闭这个链接。
HTTP 报文长度
如果服务器预先知道报文大小,会直接返回的 header 头中的 Content-Length 标示报文的长度
如果服务器采用分块传输机制,就会采用 Transfer-Encoding: chunked 的方式来代替 Content-Length
分块传输编码(Chunked transfer encoding)是 HTTP/1.1 中引入的一种数据传输机制,其允许 HTTP 由服务器发送给客户端的数据可以分成多个部分,当数据分解成一系列数据块发送时,服务器就可以发送数据而不需要预先知道发送内容的总大小,每一个分块包含十六进制的长度值和数据,最后一个分块长度值为0,表示实体结束,客户机可以以此为标志确认数据已经接收完毕。
GET 长度限制
HTTP 中的 GET 方法是通过 URL 传递数据的,而 URL 本身并没有对数据的长度进行限制,真正限制 GET 长度的是浏览器,例如 IE 浏览器对 URL 的最大限制为 2000多个字符,大概 2KB左右,像 Chrome, FireFox 等浏览器能支持的 URL 字符数更多,其中 FireFox 中 URL 最大长度限制为 65536 个字符,Chrome 浏览器中 URL 最大长度限制为 8182 个字符
状态码
1XX 指示信息–表示请求正在处理
2XX 成功–表示请求已被成功处理完毕
3XX 重定向–要完成的请求需要进行附加操作
4XX 客户端错误–请求有语法错误或者请求无法实现,服务器无法处理请求
5XX 服务器端错误–服务器处理请求出现错误
参考资料
TCP/IP 协议栈系列(二):TCP/IP 协议概述
本章将自底向上来说明 TCP/IP协议栈各层的具体工作流程
传输介质
首先,我们应该都知道计算机之间必须通过一定的传输媒介才能将数据相互传递,例如,光缆,光纤,或者无线电波,不同的传输媒介决定了电信号(0 1)的传输方式,同时也影响了电信号的传输速率、传输带宽。
链路层
我们自然的会去思考:
如何才能将 0 1 的电信号通过传输媒介传输到对方的主机?
很好解决:我们将每个计算机都安装一个能接收数据和发送数据的设备,然后将 0 1 的电信号分组,也就是组成字节的形式发送出去。
为什么要组成字节发送,因为单纯的 0 1 是没有意义的,计算机用 8 个 0 或 1 的二进制位来表示一个字节,字节才是我们发送数据的最小单位
那么这里我们提到的接收数据和发送数据的设备,就是网卡。我们规定,数据包必须是从一块网卡到另一块网卡,而网卡的地址(即我们常说的 MAC 地址)就是数据包要发送的地址和接收的地址。
MAC 地址就像网卡的身份证一样,必须具有全球唯一性。MAC 地址采用 16 进制表示,共 6 个字节,前 3 个字节是厂商的编号,后三个字节是网卡流水号。例如:5C-0F-6E-13-D1-18
解决了数据传输设备,接下来解决如何发送,所以就有人设计出来了一套发送数据的规范,也就是以太网协议。
以太网协议规定,一组电信号就是一个数据包,一个数据包也被称为一帧。一个以太网数据包的格式如下:
1 | +--------------+----------------------+--------------+ |
整个数据包由三部分组成,头部,数据,和尾部,头部占14个字节,包含原MAC地址,目标MAC地址和类型;数据区最短 46个字节,最大 1500 个字节,如果发送的数据大于 1500 个字节,则必须拆开多个数据包来发送。
尾部为 4 个字节,用来存数据帧的校验序列,用来验证整个数据包是否完整。
以太网数据包发送过程:
以太网协议会通过广播的形式将以太网数据包发送给在同一个子网的所有主机,这些主机接受到数据包之后,会取出数据包的头部里的 MAC 地址和自己的 MAC 地址进行比较,如果相等,就会接着处理数据,如果不相等,则会丢弃这个数据包。
总结:链路层的工作就是将 0,1的电信号分组并组装成以太网数据包,然后网卡通过传输媒介以广播的形式将数据包发送给在同一个子网的接收方
注意:以太网协议始终是以广播的形式将数据包发给在同一个子网的主机
网络层
首先再回过头来看一下链路层,为了能让链路层工作,我们必须知道对方主机的 MAC 地址,而且还要知道对方的 MAC 地址是否和自己处于同一网络。
- 如果我们使用 MAC 地址来传输数据,那就必须记住每个 MAC 地址,但是去记一串这样长( 5C-0F-6E-13-D1-18 )的地址显然是不友好的
- 即使我们能记住 MAC 地址,MAC 地址也只于厂商有关,和网络无关,怎么能知道是不是在一个子网?
- 如果不是一个子网,那怎么办,以太网的协议难道不能发生以太网数据包了?
别急,能提出问题,那自然有解决方案,当没有解决办法的时候,那就设计一套新的协议来弥补这些问题。
为了解决以上的问题,我们的前辈们设计了三个协议:IP 协议,ARP 协议,路由协议。同时呢将这三个协议放在了网路层。
IP 协议
为了解决 1 和 2,必须指定了一套新的地址。使得我们能够区分两个主机是否在同一个网络。IP 地址分为 IPV4 和 IPV6 两种,现在普遍还在使用的是 IPV4 地址,IPV4 地址由 4 个字节 32 位组成,每个字节可以用一个十进制的数表示,通常,我们使用 . 隔开每个十进制数来表示 ip 地址,例如:192.168.12.11 。
同时,IPV4 对 IP 地址进行了分类,主要是 A B C D 四类,以 C 类地址 192.168.12.11 为例,其中前 24 位就是网络地址,后 8 位就是主机地址。那么网络地址相同的就是在一个局域网子网内
为了判断 IP 地址中的网络地址,IP 协议还引入了子网掩码,通过子网掩码和 IP 地址按位与运算,就能得到网络地址。
因为在上层的传输层中,开发者会将 IP 地址传入,所以我们只用通过子网掩码进行运算后就能判断两个 IP 是否在一个子网内。
这里简单介绍一下 IP 数据包:
1 | +-----------------+--------------------+ |
ARP 协议
我们解决了问题1和问题2,但是随之问题又来
现在设计的 IP 协议解决了在不在一个子网内的问题,但是如果用 IP 协议,以太网协议必须知道目标主机的 MAC 地址才能传输,怎么获取到目前主机的 MAC 地址?
为了解决这个问题,ARP 协议被设计出来,即 IP 地址解析协议,主要作用就是通过 IP 来获取到对应的 MAC 地址。
ARP 协议的具体工作过程:
ARP 会首先发起一个数据包,数据包里面包含了目标主机的 IP 地址,然后发送到链路层再次包装成以太网数据包,最终由以太网广播给自己当前子网的所有主机,主机接受到这个数据包之后,取出数据包的 IP 地址,和自己的 IP 地址进行对比如果相同就返回自己的 MAC 地址,如果不同就丢弃这个数据包。ARP 接受消息来确定目标主机的 MAC 地址。如果查询到了 MAC 地址,ARP 还会将该 IP 的 MAC 地址缓存到本机保留一段时间
等下次再有请求查询,直接先从缓存里取出。这样可以节约资源,提高查询效率。
相反的 RARP 协议是用来解析 MAC 地址为 IP 地址的协议
路由协议
我们发现 ARP 协议通过 IP 获取 MAC 地址依然是局限在子网内,那不在子网的 IP 地址,ARP 不就拿不到 MAC 地址了?这也就是我们开始提出的第三个问题还没有解决。
为了解决这个问题,前辈们又设计出了另一种协议-路由协议,路由协议必须借助路由设备来完成,即路由器或交换机,路由器扮演着交通枢纽的角色,会根据信道的情况,选择合适的路径来转发数据包
因此,刚刚我们的那个问题得到解决,首先是通过 IP 协议来判断两个 IP 是否在同一个子网内,如果在一个子网,那就通过 ARP 协议去获取 MAC 地址,然后再通过以太网协议将数据包广播到子网内;
如果不在一个子网内,以太网会将数据包先转发到本子网的网关进行路由,网关会进行多次转发,并最终将数据包转发到目标 IP 的子网内,然后再通过 ARP 协议获取目标主机的 MAC 地址,最终再通过以太网
协议将数据包发送到目标 MAC 地址。
总结一下:网络层的工作主要是定义网络地址、划分网段、查询 MAC 地址、对不是同一网段的数据包进行路由转发
传输层
依靠传输层和链路层的工作,数据已经能够正常的从一台主机发送到另一台主机,但是,我们在一台主机中往往不可能只有一个网络程序,所以当有多个网络程序同时工作的时候,我们依然会发现有如下问题
如何在多个网络程序运行的主机间进行数据传输,更简单的来说,就是如何实现一个主机的某个应用程序发出,然后由对方主机的应用程序接收?
为了解决这个问题,聪明的前辈们又想到了解决办法,为每一个网络程序分配一个不同的数字来表示,发送数据的时候指定发送到某台主机的某个数字的网络程序不就可以了。这个数字就是端口。
端口用 2 个字节来表示,范围是 0 ~ 65535,也就是最大 65535 个端口。一般情况下是足够用了。有了端口,我们来简单介绍下传输层的两种协议。
TCP
我们知道网络层的数据包 IP 数据包都是不保证可靠性的,也就是说将数据发送出去,并不保证数据可达,并且数据发送也不保证有序。所以,为了满足一些对数据可靠性和有序性的应用。前辈们设计了新的协议 TCP 协议。TCP 协议保证了数据的可靠性,有序性,是面向连接的传输协议。如果发现有一个数据包收不到确认,就会重新发送数据包。
有了 TCP 协议,我们的应该程序可以将数据有序的,可靠的发送到对方指定端口的网络程序中。TCP 数据包格式
1 | +-----------------+--------------------+ |
TCP 建立连接需要经过 3 次握手,断开连接需要经过 4 次挥手。后面章节会详细来讲解整个过程,在此不详细介绍
UDP
不一定是所有的场景都必须要求数据的可靠性和有序性,有些应用程序只要求数据能快速高效的发送出去,至于可靠性并不十分关系,那这个时候 TCP 协议似乎不能满足这种需求。
其实 IP 协议的数据包就可以满足我们的新的需求,但是 IP 协议是网络层,不存在端口。而我们在传输层规定了端口,所以干脆就在传输层新设计一个协议 UDP 协议,UDP 协议其实就是在 IP 协议的基础上指定了端口(简单理解)。
UDP 是面向用户的(非连接的)传输层协议,这是因为 UDP 不像 TCP 需要 3 次握手建立连接的机制。UDP 协议相较于 TCP 来说实现比较简单,没有确认机制,数据包一旦发出,不保证数据可达和有序。不过一般情况下 UDP的数据包也不会有那么差的可靠性,还是能保证一定的可靠性。但是相较于 TCP ,UDP 的发送效率是比较快的。UDP 数据包的格式如下
1 | +-----------------+--------------------+ |
应用层
有了上面介绍的三层协议的支持,我们可以满足各种情况下,将我们的数据包发送到指定的端口的网络程序中,但是,传输的数据都是字节流,程序并不能很好的识别,操作性相对比较差。因此,在应用层
规范了各种各样的协议来规范我们的数据格式,同时也使得我们程序开发更为便利。常见的应用层协议有:HTTP、FTP、SMTP 等。针对不同类型的应用程序开发,可以使用不同的协议来开发。
每天在浏览器浏览网页,我们最熟悉的莫过于 HTTP 协议了。后面我会有专门的章节来详细讲解 HTTP 协议,这里不做过多介绍。
总结
有了分层的模型,每一层基于协议又有非常明确的分工,使得计算机之间的数据传输有条不紊的进行。我们来自顶向下回顾一下每一层的数据传输过程:
- 应用层:应用层将用户输入的数据规范化,并根据应用层协议(如 HTTP 协议)封装成数据消息,传输给传输层
- 传输层:传输层拿到应用层消息,根据传输层协议,将数据再次包装,并加上传输层协议头,发给网络层
- 网络层:拿到传输层数据包,根据 IP 协议,将数据再次包装,加上 IP 协议头,发送给链路层
- 链路层:链路层拿到网络层数据包,再次包装层以太网数据包,加上以太网协议头。通过网卡发送给对方主机
再来自顶向下回顾一下每一层的职责:
- 应用层:按照应用层协议解析和规范用户数据
- 传输层:定义端口,确定要发送的目标主机上的应用程序,根据协议的不同,控制数据的传输
- 网络层:定义 IP 地址;分配网络地址和主机地址;解析 MAC 地址;将不在同一子网的数据包路由转发
- 链路层:对 0 1进行分组,定义数据帧,确认对方主机 MAC 地址。通过物理媒介传输到对方主机的网卡
本文只是对 TCP/IP 协议栈各个层工作的一个概述,具体每一层的协议,后面会有专门的章节来介绍。