简介
超文本传输协议,这决定了协议传输的内容。
如果你想了解一个http协议,就用一门语言基于socket包写一个特定的响应,然后基于浏览器访问它。
是什么驱动了http 协议的变革
- 对于同一个域名,浏览器限制只能开6~8多个连接 ==> 连接复用
- 复杂,现在的页面,一个页面上有几十个资源文件是很常见的事儿 ==> 并行请求,请求压缩,优先处理CSS、后传图片
- 安全
- 服务器推送,服务器在客户端没有请求的情况下主动向客户端推送消息。
http1.0
基本特点是“一来一回”:客户端发起一个TCP连接,在连接上发一个http request 到服务器,服务器返回一个http response,然后连接关闭。
主要有两个问题
- 性能问题,连接的建立、关闭都是耗时操作。为此设计了Keep-Alive机制实现Tcp连接的复用。
- 服务器推送问题
http1.1
一些改进:
- Keep-Alive 成为默认
- 支持Chunk 机制
问题:
- 在上一个请求的响应没有收到之前,无法发送下一个请求。学名pipeline,reids 因为服务端是单线程结构,所以支持pipeline
- 服务器推送问题
long polling
http long-polling(推送),服务端故意不响应(一段时间),也不断连接。参见面试时如何优雅的谈论HTTP/1.0/1.1/2.0
Content-Type
Content-Type实体首部字段基本要点:
- Content-Type说明了http body的MIME类型的 header字段。
- MIME类型由一个主媒体类型(比如text,image,audio等)后面跟一条斜线以及一个子类型组成,子类型用于进一步描述媒体类型。
对于post请求,默认情况下, http 会对表单数据进行编码提交。笔者实现分片文件上传时,上传分片二进制数据,若是不指定Content-Type: application/octet-stream
则http对二进制进行了一定的变化,导致服务端和客户端对不上。
Content-Encoding
http协议中有 Content-Encoding(内容编码)。Content-Encoding 通常用于对实体内容进行压缩编码,目的是优化传输,例如用 gzip 压缩文本文件,能大幅减小体积。内容编码通常是选择性的,例如 jpg / png 这类文件一般不开启,因为图片格式已经是高度压缩过的。
内容编码针对的只是传输正文。在 HTTP/1 中,头部始终是以 ASCII 文本传输,没有经过任何压缩,这个问题在 HTTP/2 中得以解决。
Transfer-Encoding
Transfer-Encoding 用来改变报文格式。这涉及到一个通信协议的重要问题:如何定义协议数据的边界
- 发送完就断连接(非持久连接)
- 协议头部设定content-length
- 以特殊字符结尾
content-length有几个问题:
- 发送数据时,对某些场景,计算数据长度本身是一个比较耗时的事情,同时会占用一定的memory。
- 接收数据时,从协议头拿到数据长度,接收不够这个长度的数据,就不能解码后交给上层处理。
Transfer-Encoding 当下只有一个可选值:分块编码(chunked)。这时,报文中的实体需要改为用一系列分块来传输。每个分块包含十六进制的长度值和数据,长度值独占一行,长度不包括它结尾的 CRLF(\r\n),也不包括分块数据结尾的 CRLF。最后一个分块长度值必须为 0,对应的分块数据没有内容,表示实体结束。
require('net').createServer(function(sock) {
sock.on('data', function(data) {
sock.write('HTTP/1.1 200 OK\r\n');
sock.write('Transfer-Encoding: chunked\r\n');
sock.write('\r\n');
sock.write('b\r\n');
sock.write('01234567890\r\n');
sock.write('5\r\n');
sock.write('12345\r\n');
sock.write('0\r\n');
sock.write('\r\n');
});
}).listen(9090, '127.0.0.1');
server push
服务器可以对一个客户端请求发送多个响应。服务器向客户端推送资源无需客户端明确地请求。
http2
HTTP/2协议–特性扫盲篇HTTP/2的通过支持请求与响应的多路复用来减少延迟,通过压缩HTTP首部字段将协议开销降至最低,同时增加对请求优先级和服务器端推送的支持。
HTTP/2 把 HTTP 分解成了“语义”和“语法”两个部分
- “语义”层不做改动,与 HTTP/1 完全一致(即 RFC7231)。比如请求方法、URI、状态码、头字段等概念都保留不变,这样就消除了再学习的成本,基于 HTTP 的上层应用也不需要做任何修改,可以无缝转换到 HTTP/2。
- HTTP/2 在“语法”层做了“天翻地覆”的改造
备注:语义是对数据符号的解释,而语法则是对于这些符号之间的组织规则和结构关系的定义。
HTTP2引入了三个新概念:
- Frame:HTTP2通信的最小单位,二进制头封装,封装HTTP头部或body
- Message:逻辑/语义上的HTTP消息,请求或者响应,可以包含多个 frame
- Stream: 已经建立连接的双向字节流,用唯一ID标示,可以传输一个或多个frame
HTTP/2 in GO(一)Message 和 Stream 只在端上存在,链路中只存在 frame,这些概念的关系是这样的:
- 所有的通信都在一个 tcp 链接上完成,会建立一个或多个 stream 来传递数据
- 每个 stream 都有唯一的 id 标识和一些优先级信息,客户端发起的 stream 的 id 为单数,服务端发起的 stream id 为偶数
- 每个 message 就是一次 Request 或 Response 消息,包含一个或多个帧,比如只返回 header 帧,相当于 HTTP 里 HEAD method 请求的返回;或者同时返回 header 和 Data 帧,就是正常的 Response 响应。
- Frame 是最小的通信单位,承载着特定类型的数据,例如 Headers, Data, Ping, Setting 等等。 来自不同 stream 的 frame 可以交错发送,然后再根据每个 Frame 的 header 中的数据流标识符重新组装。
二进制格式
http2把原来的“Header+Body”的消息“打散”为数个小片的二进制“帧”(Frame),用“HEADERS”帧存放头数据、“DATA”帧存放实体数据。这种做法有点像是“Chunked”分块编码的方式,也是“化整为零”的思路,但 HTTP/2 数据分帧后“Header+Body”的报文结构就完全消失了,协议看到的只是一个个的“碎片”。
Http2Frame 类型
type值 | |||
---|---|---|---|
data | 0x0 | ||
header | 0x1 | ||
PRIORITY | 0x2 | ||
RST_STREAM | 0x3 | 流结束帧,用于终止异常流 | |
SETTINGS | 0x4 | 连接配置参数帧 | 设置帧由两个终端在连接开始时发送,连接生存期的任意时间发送;设置帧的参数将替换参数中现有值;client和server都可以发送;设置帧总是应用于连接,而不是一个单独的流; |
PUSH_PROMISE | 0x5 | 推送承诺帧 | |
PRIORITY | 0x6 | 检测连接是否可用 | |
GOAWAY | 0x7 | 通知对端不要在连接上建新流 | |
WINDOW_UPDATE | 0x8 | 实现流量控制 | |
CONTINUATION | 0x9 |
我们可以将frame笼统的分为data frame和 control frame,每一种类型的payload都是有自己的结构。可以参考下 go http2 实现 HTTP/2 in GO(三)
多路复用
消息的“碎片”到达目的地后应该怎么组装起来呢?HTTP/2 为此定义了一个“流”(Stream)的概念,它是二进制帧的双向传输序列,同一个消息往返的帧会分配一个唯一的流 ID。
因为流是虚拟的,实际上并不存在(除了Frame 结构里有一个StreamId),所以 HTTP/2 就可以在一个 TCP 连接上用“流”同时发送多个“碎片化”的消息,这就是常说的“多路复用”( Multiplexing)——多个往返通信都复用一个连接来处理。
在“流”的层面上看,消息是一些有序的“帧”序列,而在“连接”的层面上看,消息却是乱序收发的“帧”。在概念上,一个 HTTP/2 的流就等同于一个 HTTP/1 里的“请求 - 应答”。在 HTTP/1 里一个“请求 - 响应”报文来回是一次 HTTP 通信,在 HTTP/2 里一个流也承载了相同的功能。
浏览器渲染一个页面需要一个html文件,一个css文件,一个js文件,n个图片文件
备注:对于接收来说,缓冲区让 接收数据从字节到数据包有了完整性,port和streamid 则为数据包 赋予了“身份”。
HTTP/2 的流有哪些特点呢?
- 流是可并发的,一个 HTTP/2 连接上可以同时发出多个流传输数据,也就是并发多请求,实现“多路复用”;
- 客户端和服务器都可以创建流,双方互不干扰;
- 流是双向的,一个流里面客户端和服务器都可以发送或接收数据帧,也就是一个“请求 - 应答”来回;
- 流之间没有固定关系,彼此独立,但流内部的帧是有严格顺的;
- 流可以设置优先级,让服务器优先处理,比如先传 HTML/CSS,后传图片,优化用户体验;
- 流 ID 不能重用,只能顺序递增,客户端发起的 ID 是奇数,服务器端发起的 ID 是偶数;
- 在流上发送“RST_STREAM”帧可以随时终止流,取消接收或发送;
- 第 0 号流比较特殊,不能关闭,也不能发送数据帧,只能发送控制帧,用于流量控制。
http2连接过程
不同于http1直接发送请求
流量控制
简单说,就是发送方启动是有个窗口大小(默认64K-1),发送了10K的DATA帧,就要在窗口里扣血(减掉10K),如果扣到0或者负数,就不能再发送;接收方收到后,回复WINDOW_UPDATE帧,里面包含一个窗口大小,数据发送方收到这个窗口大小,就回血,如果回血到正数,就又能发不超过窗口大小的DATA帧。
这种流控方式就带来一些问题:
- 如果接收方发的WINDOW_UPDATE frame丢了,当然tcp会保证重传,但在WINDOW_UPDATE重传之前,就限制了发送方发送数据
- 一旦发送方初始windows size确定,那么发送方的发送速度是由接收方 + 网络传输决定的,如果发送方的速度大于接收方的应答,那么就会有大量的数据pending。
流控只限定data类型的frame,其它限定参见http2-frame-WINDOW_UPDATE
https
来自《http权威指南》
对web服务器发起请求时,我们需要一种方式来告知web服务器去执行http的安全协议版本,这是通过url中设定http或https来实现的。
- 如果是http,客户端就会打开一条到服务器80端口的连接
- 如果是https,客户端就会打开一条到服务器443端口的连接,一旦建立连接,client和server就会初始化ssl layer,对加密参数进行沟通,并交换密钥。ssl握手(SSLHandshake)完成之后,ssl layer初始化完成了。剩下的就是,browser将数据从http layer发到tcp layer之前,要经过ssl layer加密。
Java 和 HTTP 的那些事(四) HTTPS 和 证书
其它
get 和 post 的区别
2018.05.11 补充
- GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。这个可以说出来十几条。
- 对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。当然,这并不是强约束,firefox对post 就还只是发了一次。
http1.1 http2 https之间的关系
cookie 和 header
accept:image/webp,image/apng,image/*,*/*;q=0.8
accept-encoding:gzip, deflate, br
accept-language:en-US,en;q=0.9
cache-control:no-cache
cookie:GeoIP=US:CA:Los_Angeles:34.05:-118.26:v4; CP=H2; WMF-Last-Access=23-Feb-2018; WMF-Last-Access-Global=23-Feb-2018
cookie 是header的一种,cookie被浏览器自动添加,特殊处理
- 浏览器自动存储cookie,存储时按域名组织,并在发送请求时自动带上cookie(这导致某些数据不适合放在cookie中,因为会浪费网络流量)
- cookie既可以由服务端来设置(通过set-cookie header),也可以由客户端来设置(js
document.cookie = "name=Jonh; ";
)。 - HTTP cookieAn HTTP cookie is a small piece of data sent from a website and stored on the user’s computer by the user’s web browser while the user is browsing. Cookies were designed to be a reliable mechanism for websites to remember stateful information。The term “cookie” was coined by web browser programmer Lou Montulli. cookie 由一个 browser programmer 提出,由browser存储,目的是为了存储用户的状态信息。
对笔者个人来说,有以下几点要矫正:
-
header 分为
- 通用header,比如Date
- 请求特有header,比如Accept、Authorization、Cookie
- 响应特有header,比如Server、Set-Cookie
- body相关header,比如Content-Type
- 自定义header
因为springmvc 等framework,开发时不需要了解header,但framework确实进行了必要的设置
-
对于服务端开发,我们比较熟悉,将用户数据保存在数据库中,通过http请求改变用户记录的状态。其实,反向的过程也是支持的,常用的本地存储——cookie篇,随着浏览器的处理能力不断增强,越来越多的网站开始考虑将数据存储在「客户端」,提供了许多本地存储的手段。浏览器提供数据存储能力,服务器通过http响应来更改用户记录的状态。
并行性
对于处理多个“活儿”,每个“活儿”多个步骤:
- HTTP/1.1 with one connection,说一个活儿,干一个活儿, 干完一个再说下一个
- HTTP/1.1 with pipelining,一次说完,走排期,依次干活
- HTTP/2,一次说完,自己看着干
- HTTP/1.1 with multiple connections,把活儿分派给多个人
实现一个简单的http server
基于node.js socket写一个简单的http server
require('net').createServer(function(sock) {
sock.on('data', function(data) {
sock.write('HTTP/1.1 200 OK\r\n');
sock.write('\r\n');
sock.write('hello world!');
sock.destroy();
});
}).listen(9090, '127.0.0.1');
scala版本
object SocketServer {
def main(args: Array[String]): Unit = {
try {
val listener = new ServerSocket(8080);
val socket = listener.accept()
val data = "HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nhello world!"
socket.getOutputStream.write(data.getBytes())
socket.close()
listener.close()
}
catch {
case e: IOException =>
System.err.println("Could not listen on port: 80.");
System.exit(-1)
}
}
}