http短连接
- 为什么Http是无状态的短连接呢?而TCP是有状态的长连接?Http不是建立在TCP的基础上吗,为什么还能是短连接?现在明白了,Http就是在每次请求完成后就把TCP连接关了,所以是短连接。而我们直接通过Socket编程使用TCP协议的时候,因为我们自己可以通过代码区控制什么时候打开连接什么时候关闭连接,只要我们不通过代码把连接关闭,这个连接就会在客户端和服务端的进程中一直存在,相关状态数据会一直保存着
- HTTP 1.0 初衷主要是解决WEB文档在网络中的传输问题,因为传输文件是一个低频的请求,没必要进行长时间连接,所以HTPP 1.0 被设计成短连接,每进行一次HTPP通信后就会断开TCP连接。
HTTP 1.1版本随着互联网的发展,HTTP 不再只是传送简单的文件信息,多样化的文本信息开始广泛应用,像html 这样的网页访问的同时会同时附带非常多的图片之类的信息,如果每个请求都要进行TCP连接和断开(三次握手和四次挥手),这样势必会造成很多额外的通信开销。
所以为了解决此问题 HTTP 协议1.1版本会在请求的时候,只要任意一端没有明确的提出断开连接则保持TCP连接状态。
socket是什么
- 套接字(socket)是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。
三次握手
- 第一次握手:
客户主机发起连接请求,设置SYN
标志位为1
,同时客户端随机
选择了一个初始序号client_isn
,并且存放在TCP报文
字段的序号
中
- 第二次握手:
接下来,当服务端接收到该报文后,会为其分配TCP 缓存和变量
(这使得TCP容易受到被称为SYN 洪泛攻击
的拒绝服务攻击)紧接着,服务端会返回一个SYNACK 报文
到客户端,其中SYN
标志位为1
,确认号
设置为client_isn + 1
,并且选一个自己的初始序号server_isn
,并放置在序号
字段中
- 第三次握手:
当收到服务器发来的SYNACK
报文段后,客户端也需要给该连接分配缓存和变量,然后再次发送一个确认报文给服务端,其中,SYN
标志位设置为0
,将确认号
设置为server_isn + 1
,另外,此次报文可以携带负载数据
问题
为什么要三次握手而不是两次?
简单来说,三次握手的目的是为了让双方验证各自的接收能力和发送能力。
- 第一次握手,A 发送
SYN
到B
,B
接收到了后,能确认什么呢? 显然,B
能确认A
的发送
能力和B
的接收
能力; - 第二次握手,
B
发送SYNACK
到A
,A
接收到后,能确认什么呢?A
能确认B
的发送能力和A
自己的接收能力,此外,A
收到了SYNACK
,那么说明前面A
发的SYN
成功到达B
的手中,所以也能确认A
自己的发送
能力和B
的接收
能力;至此,A
已经确认了双方各自的发送能力和接收能力都是OK
的,因此转为ESTABLISHED
状态; - 第三次握手,
A
发送ACK
到B
,B
接收后,能确认什么呢?
直接的,B
能确认A
的发送
能力和B
的接收
能力,另外由于B
能收到ACK
说明前面发送的SYNACK
已经成功被接受了,说明能确认A
的接收
能力和B
的发送
能力。
如果使用两次握手,就不能确认上述所说的四种能力,那么就会导致问题。
假定不采用第三次报文握手,那么只要B发出确认,新的连接就建立了。
现假定一种异常情况,即A
发出的SYN
报文段并没有丢失,而是在某些网络节点长时间滞留了,以致延误到连接释放后的某个时间才到达B
。本来这是一个早已失效的报文段。但B
收到此失效的连接请求报文段后,却误以为是A
又发出一次新的连接请求,于是就向A
发出确认报文段,同意建立连接。
由于现在A
并没有发出建立连接的请求,因此不会理睬B
的确认,也不会向B
发送数据,但B
却以为新的运输连接已经建立了,并一直等待A
发来的数据。B
的许多资源就这样白白浪费了。
ACK报文丢失导致第三次握手失败
当客户端收到服务端的SYNACK
应答后,其状态变为ESTABLISHED
,并会发送ACK
包给服务端,准备发送数据了。如果此时ACK
在网络中丢失(如上图所示),过了超时计时器后,那么服务端会重新发送SYNACK
包,重传次数根据/proc/sys/net/ipv4/tcp_synack_retries
来指定,默认是5
次。如果重传指定次数到了后,仍然未收到ACK
应答,那么一段时间后,Server
自动关闭这个连接。
问题就在这里,客户端已经认为连接建立,而服务端则可能处在SYN-RCVD
或者CLOSED
,接下来我们需要考虑这两种情况下服务端的应答:
- 服务端处于
CLOSED
,当接收到连接已经关闭的请求时,服务端会返回RST 报文
,客户端接收到后就会关闭连接,如果需要的话则会重连,那么那就是另一个三次握手了。 - 服务端处于
SYN-RCVD
,此时如果接收到正常的ACK 报文
,那么很好,连接恢复,继续传输数据;如果接收到写入数据等请求呢?注意了,此时写入数据等请求也是带着ACK 报文
的,实际上也能恢复连接,使服务器恢复到ESTABLISHED
状态,继续传输数据。
(ISN)是固定的吗?
不固定,client_isn
是随机生成的,而server_isn
则需要根据SYN 报文
中的源、ip和端口
,加上服务器本身的密码数
进行相同的散列得到,显然这也不是固定的。
三次握手过程中可以携带数据吗?
第三次握手是可以携带数据的
,而前两次不行。
四次挥手
首先,当前客户端和服务器的状态都为ESTABLISHED
- 第一次挥手
客户主机发起连接释放的请求,设置FIN
为1
,当然,序号seq
也会带上,这里假设为u
;发送完毕后,客户端进入 FIN-WAIT-1
状态
- 第二次挥手
服务端接收到FIN 报文
后,会返回一个ACK 报文
回去,此时设置ACK
为1
,确认号
为u + 1
;表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。发送完毕后,服务器端进入 CLOSE-WAIT
状态,客户端接收到这个确认包之后,进入 FIN-WAIT-2
状态,等待服务器端关闭连接
- 第三次挥手
服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN
置为1
;发送完毕后,服务器端进入 LAST-ACK
状态,等待来自客户端的最后一个ACK
- 第四次挥手
客户端接收到服务端传来的FIN 报文
后,知道服务器已经准备好关闭了,发送一个确认包,并进入 TIME-WAIT
状态,等待可能出现的要求重传的ACK 报文
;服务器端接收到这个确认包之后,关闭连接,进入 CLOSED
状态。
客户端等待了某个固定时间(两个最大段生命周期,2MSL
,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK
,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED
状态
四次挥手重要的是TIME-WAIT
状态,为什么需要这个状态呢?
要确保服务器是否已经收到了我们的ACK 报文
,如果没有收到的话,服务器会重新发FIN 报文
给客户端,那么客户端再次收到FIN 报文
之后,就知道之前的 ACK 报文
丢失了,就会再次发送ACK 报文
。
为什么握手只要三次,挥手却要四次?
关键就在中间两步。
- 建立连接时,当服务器收到客户端的
SYN 报文
后,可以直接发送SYNACK 报文
。其中ACK
是用来应答的,SYN
是用来同步的。 - 但是关闭连接时,当服务器收到
FIN 报文
时,很可能并不会立即关闭SOCKET
,所以只能先回复一个ACK 报文
,告诉客户端,“你发的FIN 报文
我收到了”。只有等到服务器所有的报文都发送/接收完了,我才能发送FIN 报文
,因此不能一起发送,需要四次握手
为什么 TIME_WAIT 状态需要经过 2MSL 才能转换到 CLOSE 状态?
- 第一,为了保证客户端发送的最后一个
ACK 报文
能够到达服务器。我们必须假设网络是不可靠的,ACK 报文
可能丢失。如果服务端发出FIN 报文
后没有收到ACK 报文
,就会重发FIN 报文
,此时处于TIME-WAIT
状态的客户端就会重发ACK 报文
。当然,客户端也不能无限久的等待这个可能存在的FIN 报文
,因为如果服务端正常接收到了ACK 报文
后是不会再发FIN 报文
的。因此,客户端需要设置一个计时器,那么等待多久最合适呢?所谓的MSL
(Maximum Segment Lifetime)指一个报文在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL
时间后,客户端都没有再次收到FIN 报文
,那么客户端推断ACK 报文
已经被服务器成功接收,所以结束TCP 连接
。 - 第二,防止已失效的连接请求报文段出现在新的连接中。客户端在发送完最后一个
ACK 报文
后,再经过时间2MSL
,就可以使由于网络不通畅产生的滞留报文段失效。这样下一个新的连接中就不会出现旧的连接请求报文
HTTP连接方式的进化史:
HTTP/0.9时代:短连接
每个HTTP请求都要经历一次DNS解析、三次握手、传输和四次挥手。反复创建和断开TCP连接的开销巨大,在现在看来,这种传输方式简直是糟糕透顶。
HTTP/1.0时代:持久连接概念提出
人们认识到短连接的弊端,提出了持久连接的概念,在HTTP/1.0中得到了初步的支持。
持久连接,即一个TCP连接服务多次请求:
客户端在请求header中携带Connection:
Keep-Alive,即是在向服务端请求持久连接。如果服务端接受持久连接,则会在响应header中同样携带Connection:
Keep-Alive,这样客户端便会继续使用同一个TCP连接发送接下来的若干请求。(Keep-Alive的默认参数是[timout=5,
max=100],即一个TCP连接可以服务至多5秒内的100次请求)
当服务端主动切断一个持久连接时(或服务端不支持持久连接),则会在header中携带Connection: Close,要求客户端停止使用这一连接。
HTTP/1.1时代:持久连接成为默认的连接方式;提出pipelining概念
HTTP/1.1开始,即使请求header中没有携带Connection: Keep-Alive,传输也会默认以持久连接的方式进行。
目前所有的浏览器都默认请求持久连接,几乎所有的HTTP服务端也都默认开启对持久连接的支持,短连接正式成为过去式。(HTTP/1.1的发布时间是1997年,最后一次对协议的补充是在1999年,我们可以夸张地说:HTTP短连接这个概念已经过时了近20年了。)
同时,持久连接的弊端被提出 —— HOLB(Head of Line Blocking)
即持久连接下一个连接中的请求仍然是串行的,如果某个请求出现网络阻塞等问题,会导致同一条连接上的后续请求被阻塞。
所以HTTP/1.1中提出了pipelining概念,即客户端可以在一个请求发送完成后不等待响应便直接发起第二个请求,服务端在返回响应时会按请求到达的顺序依次返回,这样就极大地降低了延迟。
然而pipelining并没有彻底解决HOLB,为了让同一个连接中的多个响应能够和多个请求匹配上,响应仍然是按请求的顺序串行返回的。所以pipelining并没有被广泛接受,几乎所有代理服务都不支持pipelining,部分浏览器不支持pipelining,支持的大部分也会将其默认关闭。
SPDY和HTTP/2:multiplexing
multiplexing即多路复用,在SPDY中提出,同时也在HTTP/2中实现。
multiplexing技术能够让多个请求和响应的传输完全混杂在一起进行,通过streamId来互相区别。这彻底解决了holb问题,同时还允许给每个请求设置优先级,服务端会先响应优先级高的请求。
现在Chrome、FireFox、Opera、IE、Safari的最新版本都支持SPDY,Nginx/Apache HTTPD/Jetty/Tomcat等服务端也都提供了对SPDY的支持。
http2
- HTTP/1里的header对应HTTP/2里的 HEADERS frame
- HTTP/1里的payload对应HTTP/2里的 DATA frame
下面从一个真实的gRPC SayHello
请求,查看它在HTTP/2上是怎样实现的
可以看到下面这些Header:
- Header: :authority: localhost:50051
- Header: :path: /helloworld.Greeter/SayHello
- Header: :method: POST
- Header: :scheme: http
- Header: content-type: application/grpc
- Header: user-agent: grpc-java-netty/1.11.0
然后请求的参数在DATA frame里:
- GRPC Message: /helloworld.Greeter/SayHello, Request
简而言之,gGRPC把元数据放到HTTP/2 Headers里,请求参数序列化之后放到 DATA frame里
HTTP / 2 主要有两个规范组成
- Hypertext Transfer Protocol version 2 (超文本传输协议版本 2)
- HPACK - HTTP / 2 的头压缩 (HPACK 是一种头部压缩算法)
HTTP2 的特性
HTTP / 2 支持 HTTP / 1.1 的所有核心功能,但旨在通过多种方式提高效率
- HTTP/2 采用二进制传输数据,而非 HTTP/1 的文本格式传输
- HTTP / 2 基本协议单元是帧,比如 head(头部信息)帧,data(传输数据细信息)帧
- HTTP / 2 使用流技术支持多路复用,也就是说提供了在单个连接上复用 HTTP 请求和响应的能力, 多个请求或响应可以同时在一个连接上使用流.
- HTTP / 2 支持压缩头部帧,允许将多个请求压缩成成一个分组,而且在客户端和服务器端分别头部信息建立索引,相同的表头只需要传输索引就可以。
- HTTP / 2 支持对请求划分优先级(就是流的优先级)
- HTTP / 2 支持 Server Push 技术
HTTP2 的原理
多路复用
HTTP/2 将每一个请求变成流,每一个流都有自己的 ID,有自己的优先级,这些流可以由客户端发送到服务端,也可以由服务端发送到客户端,将数据划分为帧,头部信息为 head 帧,实体信息为 data 帧,最后将这些流乱序发送到一个 TCP 连接中,如下图:
- HTTP/2 中,在一个浏览器同域名下的所有请求都是在单个连接中完成,这个连接可以承载任意数量的双向数据流,每个数据流都以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,根据帧首部的流标识可以将多个帧重新组装成一个流。
- 在 HTTP/1 中,如果想并发发送多个请求,必须创建多个 TCP 连接,而且浏览器为了减少负载,会对同一域名下的 TCP 连接做限制,这样当请求量比较大时,会引起阻塞
服务器推送
- HTTP /1 中客户端往服务端发送请求严格遵守一个请求,一个响应,比如客户端请求展示网页时,服务端发挥 HTML 内容,浏览器解析时发送 css,js 请求,服务端又返回 css,js 文件,那么服务端为什么不能在返回网页时就推送 css,js 内容给客户端呢,在 HTTP /2 中这已功能已经支持,
- 服务端主动推送也会遵守同源策略,不会随便推送第三方的资源到客户端
如果服务端推送资源是呗客户端缓存过的,客户端是有权力拒绝服务端的推送的,浏览器可以通过发送 RST_STREAM 帧来拒收。
- 每一个服务端推送的资源都是一个流
头部压缩
HTTP /1 的请求头较大,而且是以纯文本发送,HTTP/2 对消息头进行了压缩,采用的是 HACK 算法,能够节省消息头占用的网络流量,其主要是在两端建立了索引表,消息头在传输时可以采用索引,而 HTTP/1.x 每次请求,都会携带大量冗余头信息,浪费了很多带宽资源
grpc
gRPC 是 Google 基于 HTTP/2 以及 protobuf 的,要了解 gRPC 协议,只需要知道 gRPC 是如何在 HTTP/2 上面传输就可以了。
gRPC 通常有四种模式,unary,client streaming,server streaming 以及 bidirectional streaming,对于底层 HTTP/2 来说,它们都是 stream,并且仍然是一套 request + response 模型。
Request
gRPC 的 request 通常包含
- Request-Headers
- 0 或者多个 Length-Prefixed-Message
- EOS
- Request-Headers 直接使用的 HTTP/2 headers,在 HEADERS 和 CONTINUATION frame 里面派发。定义的 header 主要有 Call-Definition 以及 Custom-Metadata。Call-Definition 里面包括 Method(其实就是用的 HTTP/2 的 POST),Content-Type 等。而 Custom-Metadata 则是应用层自定义的任意 key-value,key 不建议使用 grpc- 开头,因为这是为 gRPC 后续自己保留的。
- Length-Prefixed-Message 主要在 DATA frame 里面派发,它有一个 Compressed flag 用来表示改 message 是否压缩,如果为 1,表示该 message 采用了压缩,而压缩算法定义在 header 里面的 Message-Encoding 里面。然后后面跟着四字节的 message length 以及实际的 message。
- EOS(end-of-stream) 会在最后的 DATA frame 里面带上了 END_STREAM 这个 flag。用来表示 stream 不会在发送任何数据,可以关闭了。
Response
Response 主要包含
- Response-Headers
- 0 或者多个 Length-Prefixed-Message
- Trailers,如果遇到了错误,也可以直接返回 Trailers-Only。
- Response-Headers 主要包括 HTTP-Status,Content-Type 以及 Custom-Metadata 等。
- Trailers-Only 也有 HTTP-Status ,Content-Type 和 Trailers。
- Trailers 包括了 Status 以及 0 或者多个 Custom-Metadata。
HTTP-Status 就是我们通常的 HTTP 200,301,400 这些,很通用就不再解释。Status 也就是 gRPC 的 status, 而 Status-Message 则是 gRPC 的 message。Status-Message 采用了 Percent-Encoded 的编码方式,具体参考这里。
如果在最后收到的 HEADERS frame 里面,带上了 Trailers,并且有 END_STREAM 这个 flag,那么就意味着 response 的 EOS。
Protobuf
gRPC 的 service 接口是基于 protobuf 定义的,我们可以非常方便的将 service 与 HTTP/2 关联起来。
- Path : /Service-Name/{method name}
- Service-Name : ?( {proto package name} "." ) {service name}
- Message-Type : {fully qualified proto message name}
- Content-Type : "application/grpc+proto"
grpc的好处
- 跨语言,protobuf
- 性能好,基于http2协议实现的,http2协议提供了很多新的特性,并且在性能上也比http1提高了许多
- 标准化状态码
- 负载均衡,服务发现,日志,监控等都支持可插拔机制
四类服务方法
基于 http2 协议的特性:gRPC 允许定义如下四类服务方法
- 单项 RPC:客户端发送一次请求,等待服务端响应结构,会话结束,就像一次普通的函数调用这样简单
- 服务端流式 RPC:客户端发起一起请求,服务端会返回一个流,客户端会从流中读取一系列消息,直到没有结果为止
- 客户端流式 RPC:客户端提供一个数据流并写入消息发给服务端,一旦客户端发送完毕,就等待服务器读取这些消息并返回应答
- 双向流式 RPC:客户端和服务端都一个数据流,都可以通过各自的流进行读写数据,这两个流是相互独立的,客户端和服务端都可以按其希望的任意顺序独写
protobuf的原理
- Base 128 varint
这是一个编码算法,我们都知道,int32 占四个字节,int64 占 8 个字节,这是固定的,不管这个数字是 1 还是 123456,占的字节数是一样,那有没有一种能根据数字大小变长编码的算法呢?Base 128 varint 就是,在设置二进制网络协议通信时,这种好处是可观的,能够带来性能上的提升。为什么叫 128 呢,就是因为采用 7bit 的空间存储数据(一个字节占 8bit,但只采用 7bit),7bit 最大当然只能存储 128 了,那么最高位干啥呢?最高位用来当作一个标识 (flag), 如果最高位是 0 就表示这个最后一个字节了。
示例:
我们用一个数字 10 和数字 300 来讲解一下上面的 Base 128 varint
先说数字 10,转化为二进制后是:0000 1010,为什么只有八位呢,因为 10 用一个字节表示已经足够了,最高位为 0(加粗的那个),表示这是最后一个字节了,不需要再用额外的字节来存储了
再来看数字 300,转化为二进制后是:00010010_1100, 转化成 varint,如下步骤:
按照 7 位进行分开, 0000010_0101100,不够的补 0
进行反转:0101100_0000010
最高位补数,第一个字节最高位补 1,第二个字节最高位补 0:10101100_00000010
ProtoBuffer 序列化后的存储格式
Tag,Length,Value ,这是序列化后存储的二进制的格式,Tag 大家简单理解为就是 proto 文件中字段后面的编号,Length 是这个字段对应的值的字节长度,Value 就是具体的值了,最终将所有数据拼装成一个流,如下图:
T 代表的 tag 是由 fieldNumber(字段编号)和 wireType(上图中最左边的 0,1,2...)组成的,fieldNumber 保证了字段不重复和他在数据流中的位置,wireType 标记了数据类型,如果是 varint 编码,fieldNumber 也保证了数据字节的长度 (L)
参考文章
https://www.zhihu.com/search?...
https://zhuanlan.zhihu.com/p/...
https://learnku.com/articles/...