三、大话HTTP协议-HTTP协议之Session、Cookie和Token

我们知道HTTP协议是无状态的,那么真的就没有办法让他有记忆了吗?我们可以通过Cookie、Session和Token来稍加弥补HTTP没有记忆的问题。

一、Cookie
我们先来聊聊Cookie。

用户浏览器第一次访问服务器的时候,服务器不认识本次请求身份,因此可以创建一个身份标识数据,格式是”key=value“放到 Set-Cookie 字段里,随着响应报文一同发给浏览器。

浏览器收到响应报文后,看到了Set-Cookie,知道是服务器返回的身份标识数据,就会保存起来。用户下次打开浏览器的时候,如果这个身份标识数据还在,浏览器则可以直接使用它。

我们用yummyfood商城项目为例,其中购物车的信息和用户信息都会保存在Cookie中,我们来下效果:

如何来实现的呢?我们拿用户信息的Cookie为例。

在JAVA后端代码中,拿到用户名密码后,进行校验,校验通过后生成Cookie写入HttpServletResponse返回给前端浏览器。当然了,前端JS也是可以设置Cookie的。

接口层:

@ApiOperation(value = “用户登录”,notes = “用户登录”,httpMethod = “POST”)
@PostMapping(“/login”)
public CommonJsonResult login(@ApiParam(name = “UserBO”,value = “用户登录实体”,required = true)
@RequestBody UserBO userBO,
HttpServletRequest request,
HttpServletResponse response) {
String username = userBO.getUsername();
String password = userBO.getPassword();
// 1、判断用户名和密码不能为空
if(StringUtils.isBlank(username) || StringUtils.isBlank(password)){
return CommonJsonResult.errorMsg(“用户名或密码不能为空”);
}
// 2、实现登录
Users userResult = userService.queryUserForLogin(username,password);
if(userResult == null){
return CommonJsonResult.errorMsg(“用户名或密码不正确”);
}
// 3、去除一些敏感信息返回给前端
userResult = setNullProperty(userResult);
// 4、设置cookie
CookieUtils.setCookie(request,response, “user”,
JsonUtils.objectToJson(userResult),true);

//TODO 生成用户TOKEN,存入redis
//TODO 同步购物车数据
return CommonJsonResult.ok(userResult);
}

其中工具类写Cookie的逻辑为:

private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
try {
if (cookieValue == null) {
cookieValue = “”;
} else if (isEncode) {
cookieValue = URLEncoder.encode(cookieValue, “utf-8”);
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0)
cookie.setMaxAge(cookieMaxage);
if (null != request) {// 设置域名的cookie
String domainName = getDomainName(request);
logger.info(“========== domainName: {} ==========”, domainName);
if (!”localhost”.equals(domainName)) {
cookie.setDomain(domainName);
}
}
cookie.setPath(“/”);
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}

在这段代码中,我们对Cookie进行了一些属性上的设置。我们可以设置过期时间,一旦超过这个期限浏览器就认为是 Cookie 失效,在存储里删除,也不会发送给服务器。同时设定了domain和path,分别指定Cookie的所属域名和路径,浏览器在发送 Cookie 前会从 URI 中提取出 host 和 path 部分,对比 Cookie 的属性。如果不满足条件,就不会在请求头里发送 Cookie。

此外也有一定的安全性考虑,比如进行了urlencode稍微加点密,比如设置Secure属性,只有在使用HTTPS的时候才会传输,而在HTTP协议中禁止发送。

这样,浏览器就可以根据返回的Cookie信息存储起来,前端JS则读取此信息在页面展示,只要这个Cookie没有过期,下次打开浏览器就仍然可以看到此信息。我们把这种前端缓存的小片段信息称为Cookie。

通过抓取登录报文,可以看到以下信息:

经过上一章节的学习,我们基本可以看懂请求报文和响应报文了。我们会看到有个字段叫做:

Set-Cookie: user=%7B%22id%22%3A%22201118H62G5ZWPH0%22%2C%22username%22%3A%22fossi%22%2C%22password%22%3Anull%2C%22nickname%22%3A%22fossi%22%2C%22realname%22%3Anull%2C%22face%22%3A%22http%3A%2F%2Fbloghello.oursnail.cn%2Favatar.png%22%2C%22mobile%22%3Anull%2C%22email%22%3Anull%2C%22sex%22%3A2%2C%22birthday%22%3Anull%2C%22createdTime%22%3Anull%2C%22updatedTime%22%3Anull%7D; Domain=.oursnail.cn; Path=/

浏览器根据此响应字段写入Cookie,缓存起来用户相关的信息,前端项目在用到用户信息展示的地方都可以从这个Cookie中获取。

从这张图中我们也能够看到,Cookie 是由浏览器负责存储的,而不是操作系统。所以,它是“浏览器绑定”的,只能在本浏览器内生效。

如果你换个浏览器或者换台电脑,新的浏览器里没有服务器对应的 Cookie,就好像是脱掉了贴着纸条的衣服,“健忘”的服务器也就认不出来了,只能再走一遍 Set-Cookie 流程。

二、Session
Session是另一种记录客户状态的机制,保存在服务器上,客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。

客户端浏览器再次访问时只需要从该Session中查找该客户的状态即可。好了,Session就是这么个情况,实际上就是用ConcurrentHashMap结构把会话信息存储在内存中。相同的会话过来时,服务器可以根据SessonID找到对应的用户信息。

Session 可以与 Cookie 一起使用,下面展示Session Cookie的流程,即会话 Cookie,

首先,客户端会发送一个http请求到服务器端。
服务器端接受客户端请求后,建立一个session对象,并发送一个http响应到客户端,这个响应头,其中就包含Set-Cookie头部。该头部包含了sessionId:Set-Cookie:JSESSIONID=XXXXXXX
在客户端发起的第二次请求,假如服务器给了set-Cookie,浏览器会自动在请求头中添加cookie
服务器接收请求,分解cookie,验证信息,核对成功后返回response给客户端

三、Session VS Cookie
存放位置不同:Cookie在客户端浏览器,而Session存放在服务端。
安全性不同:基于第一点,因此Session更加安全。
对服务器压力不同:基于第一点,Session存放在内存,当客户端连接佷多时,会占用佷多内存资源,对服务器压力较大。
存储大小不同: 基于上一点,单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务器资源。
有效期不同: Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭(默认情况下)或者 Session 超时都会失效。
存取值的类型不同:Cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,Session 可以存任意数据类型。
四、Token令牌
熟悉Session的同学知道,Session在一台服务器上逻辑是没问题的,但是当遇到了集群或者分布式的时候,就会有问题。因为多服务器不共享Session,而请求可能会打到任意一台机器上,就会出现读取不到Session的问题。

这个时候可以使用Token来解决。

Token 也称为令牌,token在客户端一般存放于localStorage,cookie,或sessionStorage中。在服务器一般存于数据库中。

token 的认证流程与cookie很相似:

用户登录,成功后服务器返回Token给客户端。
客户端收到数据后保存在客户端
客户端再次访问服务器,将token放入headers中
服务器端采用filter过滤器校验。校验成功则返回请求数据,校验失败则返回错误码
面对集群或者分布式的场景时,就可以根据存储在数据库的Token进行校验,而不会有Session这种问题。

Token是令牌的意思,它偏重于认证和授权的含义,认证是针对用户,授权是针对 App 。其目的是让某 App 有权利访问某用户的信息。如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 Token 。

而Session只提供一种简单的认证,即只要有此 SessionID ,即认为有此 User 的全部权利。是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方 App。

小小总结下:

session存储于服务器,可以理解为一个状态列表,拥有一个唯一识别符号sessionId,通常存放于cookie中。服务器收到cookie后解析出sessionId,再去session列表中查找,才能找到相应session。依赖cookie
cookie类似一个令牌,装有sessionId,存储在客户端,浏览器通常会自动添加。
token也类似一个令牌,无状态,用户信息都被加密到token中,服务器收到token后解密就可知道是哪个用户。需要开发者手动添加。

二、大话HTTP协议-HTTP报文结构整体窥探

在了解了HTTP的前世今生后,就有必要去深入了解下HTTP协议了,本节从HTTP报文整体结构出发,奠定对HTTP协议的整体认识,方便今后的深入学习。

一、HTTP协议特点

HTTP的特点总结如下:

支持客户端/服务器模式
客户端/服务器模式工作的方式是由客户端主动向服务器发出请求,服务器端响应请求,并行相应服务。
简单快速
客户向服务器请求服务时,只需传送请求方法和路径。
请求方法常用的有GRT、HEAD、POST。每种方法规定了客户端与服务器联系的类型不同。
由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
灵活
HTTP允许传输任意类型的数据对象。
正在传输的类型由Content-Type加以标记
无连接
无连接的含义时限制每次连接只处理一个请求,服务器处理完客户的请求,并收到客户端应答后,即断开连接。早期的时候,由于大部分时间是空闲的,为了尽快释放资源。不过现在HTTP1.1改为默认启用keepalive长连接机制,因此现在的HTTP已经不再是无连接的了。
无状态
HTTP协议是无状态协议。
无状态是指每个请求都是互相独立、毫无关联的,协议不要求客户端或服务器记录请求相关的信息。
另一方面,在服务器不需要先前信息时,它的应答就较快。这样,HTTP协议就比较简单,不记仇。
二、蜜汁兄弟-URL和URI
啊,为什么搞这么多概念来闹晕我们。

URI:Uniform Resource Identifier,统一资源标识符
URL:Uniform Resource Location,统一资源定位符
URN:Uniform Resource Name,统一资源名称
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wkMTrVE8-1613545405556)(http://bloghello.oursnail.cn/network25-1.png)]

从上图我们可以看到,URI=URN+URL。URL时URI的子集。如何理解呢?

URI 就是来独一无二的表示一种资源(这种资源可以是人、某台电脑,手机等)。那么问题来了,你怎么去实现这种独一无二的表示?或者说,你怎么去独一无二的表示地球上的一个人?你会说我用身份证号码。好了,那这个身份证号码,就是URI的一种实现方式。那怎么来独一无二的来标记网上的资源,你通过这个标记,来定位网上某个独一无二的资源呢?那就是通过URL,所以说URL是URI的一种具体实现。

三、HTTP报文结构
还记得我们的传输层、网络层吗,他们都有自己的头部信息来标识。HTTP 协议也是与 TCP/UDP 类似,同样也需要在实际传输的数据前附加一些头数据,不过与 TCP/UDP 不同的是,它是一个“纯文本”的协议,所以头数据都是 ASCII 码的文本,可以很容易地用肉眼阅读,不用借助程序解析也能够看懂。

用于HTTP的协议交互的信息被称为HTTP报文。请求端的HTTP报文叫做请求报文,响应端的叫做响应报文。

3.1 请求报文
HTTP的这两种报文都由四部分组成:请求行(起始行)、请求头(首部字段)、空行、请求体(报文主体)。

请求行:包含【请求的方法如GET、POST】,【请求URI】和【HTTP版本】,中间用空格分隔
请求头:包含请求的各种条件和属性,使用key:value的形式展示
空行:它的作用是通过一个空行,告诉服务器请求头部到此为止。
请求体:实际传输的数据,不一定是文本,可以是图片视频等二进制文件。若方法字段是GET,则此项为空,没有数据;若方法字段是POST,则通常来说此处放置的就是要提交的数据。
其中,请求行和请求头可以合称为请求头,即常说的 header ,而请求体是我们常说的 body 。

请求头整体结构大概就是:

HTTP协议规定报文必须有 header ,可以没有 body ,比如常用的 GET 请求。且 header 和 body 中间必须有个空行。

一个GET请求一般实际上长这个样子:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5voO5V9J-1613545405570)(http://bloghello.oursnail.cn/network25-3.png)]

在这个浏览器发出的请求报文里,第一行“GET / HTTP/1.1”就是请求行,其中 GET 就是请求方法, / 就是请求的 URI ,HTTP/1.1 就是HTTP协议及版本;而后面的“Host”、“Connection”等等都属于 header,报文的最后是一个空白行结束,没有 body。

对于POST请求而言,几乎一样,只是请求方法变成POST,空行下面会有请求参数构成的Body。

3.2 响应报文
同样的,HTTP响应报文也由四部分组成:响应行(状态行)、响应头、空行、响应体(报文主体)。

响应行:响应行一般由【协议版本】、【状态码如200】及其【描述】组成 比如 HTTP/1.1 200 OK
响应头:响应头用于描述服务器的基本信息,以及数据的描述,服务器通过这些数据的描述信息,可以通知客户端如何处理等一会儿它回送的数据。
空行:它的作用是通过一个空行,告诉客户端请求头部到此为止。
响应体:响应体就是响应的消息体,如果是纯数据就是返回纯数据,如果请求的是HTML页面,那么返回的就是HTML代码,如果是JS就是JS代码,如此之类。
响应行和响应头整体结构大概为:

来个实际例子,如请求一个HTML页面的请求报文和响应报文:

请求头和响应头的结构是基本一样的,唯一的区别是起始行。

HTTP 头字段非常灵活,不仅可以使用标准里的 Host、Connection 等已有头,也可以任意添加自定义头,这就给 HTTP 协议带来了无限的扩展可能。

四、HTTP请求方法

实际上GET和POST用的最多,其他的用的都比较少。GET和POST有一点区别:
GET请求的参数是放在URL后面拼接的,而POST是放在了body中
浏览器对GET请求参数的长度是有限制的,而POST则没有此限制
基于第一个,安全性上,POST比GET要强点,比如用户名密码拼接在URL后面的话,那么下一个用户可能根据浏览器浏览器记录找到你的登录链接,从而登录上你的账号密码
POST和PUT很类似了,不过也有区别:
他两本意上的区别:新建一条记录的话就用post,而更新一条记录的话就用put,PUT可以认为是具备幂等性的,而POST是非幂等的。
PUT基本不用,无验证机制存在安全问题,POST可以借助代码逻辑实现幂等性
HEAD是简略版的GET,只用获取响应头即可,不需要获取具体内容。
关于幂等性,它的定义是:

Methods PUT and DELETE are defined to be idempotent, meaning that multiple identical requests should have the same effect as a single request.

这意味着,PUT这样的幂等方法在多次请求时,产生和单次请求同样的效果。即没有产生新的资源。反之,如果多次请求时产生了副作用,则应该使用非幂等方法,对应POST。

DELETE为什么是幂等的?因为删除一次和删除多次是一样的影响。GET为什么是幂等的就不赘述了。

五、HTTP状态码
状态代码的第一个数字代表当前响应的类型:

1xx 消息:Informational(信息性状态码),表示请求已被服务器接收,继续处理。
2xx 消息:Success(成功状态码),表示请求已成功被服务器接收、理解、并接受。
200 OK:表示成功
204 No Content:服务器成功处理了请求,但不需要返回任何实体内容。如果客户端是浏览器的话,那么用户浏览器应保留发送了该请求的页面,而不产生任何文档视图上的变化。
206 Partial Content:服务器已经成功处理了部分 GET 请求。类似于 FlashGet 或者迅雷这类的 HTTP 下载工具都是使用此类响应实现断点续传或者将一个大文档分解为多个下载段同时下载。该请求必须包含 Range 头信息来指示客户端希望得到的内容范围。
3xx 消息:Redirection(重定向状态码),表示需要后续操作才能完成这一请求。
301 Moved Permanently:被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个 URI 之一。如果可能,拥有链接编辑功能的客户端应当自动把请求的地址修改为从服务器反馈回来的地址。
302 Found:请求的资源现在临时从不同的 URI 响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。
4xx 消息:Client Error(客户端错误状态码),表示请求含有语法错误或者无法被执行。
400 Bad Request:语义有误,当前请求无法被服务器理解。除非进行修改,否则客户端不应该重复提交这个请求。请求参数有误。
401 Unauthorized:当前请求需要用户验证。
403 Forbidden:服务器已经理解请求,但是拒绝执行它。
404 Not Found:请求失败,请求所希望得到的资源未被在服务器上发现。
5xx 消息:Server Error(服务器错误状态码),表示服务器在处理某个正确请求时发生错误。
500 Internal Server Error:服务器遇到了不知道如何处理的情况。这个时候一般是服务出现BUG了。
502 Bad Gateway:此错误响应表明服务器作为网关需要得到一个处理这个请求的响应,但是得到一个错误的响应。
503 Service Unavailable:表明服务器暂时处于超负载或正在进行停机维护,无法处理请求。
504 Gateway Timeout:当服务器作为网关,不能及时得到响应时返回此错误代码。
六、HTTP的头部字段
头部的字段帮助客户端和服务端更好地交互,这里先拿几个重要字段说明下。

6.1 Accept和Content-Type
数据交互的时候,我们需要解决三个问题:数据类型协商(数据类型、压缩算法)、语言类型协商、字符集编码类型协商。解决了这三个问题,两者之间就可以愉快地进行交流了。

【①数据类型协商和压缩算法协商】

首先我们要知道数据是有佷多类型的,比如分为文本类型有:text/html、text/plain、text/css等;有图片image/gif、image/png等,有音频和视频:audio/mepg、audio/mp4等。也有我们常用的不固定的数据格式,由上层应用来解释的比如application/json。

这些类型有个名字,叫做:MIME Type,但是HTTP传输的时候会进行数据压缩,因此还有一个字段叫做:Encoding Type,告诉对方数据用了什么压缩格式,好让对方还原出来原始数据。Encoding Type类型就少了佷多,一般用的是:gzip、deflate、br等压缩算法。

好了,有了 MIME type 和 Encoding type,无论是浏览器还是服务器就都可以轻松识别出 body 的类型,也就能够正确处理数据了。通过什么字段呢?

客户端可以通过 Accept 字段告诉服务器希望接收什么格式的数据,服务器可以用 content-type 告诉客户端实际发送了什么格式的数据。

上图中,意思是客户端告诉服务器,我只能看懂HTML、XML的文本和数据,还有就是webp和png格式的图片,其他的我不认识。服务器返回的时候告诉客户端本次返回内容的格式和压缩算法。

【②语言类型协商】

除了格式的协商,还有语言和编码的协商,如果客户端只认识英文,而服务器返回了中文,不就尴尬了。

客户端会发送Accept-Language表明自己看得懂的语言,比如zh-CN 的汉语文字。

Accept-Language: zh-CN, zh, en

而服务器会在响应头中用Content-Language告诉客户端实体数据用的语言类型。

Content-Language: zh-CN

【③字符集编码类型协商】

而字符集编码是客户端发送Accept-Charset字段来说明,比如自己只能处理GBK和UTF8字符编码,其他的不行。服务器如果返回UTF8编码如何返回呢?逐一响应头没有对应的Content-Charset,而是在ontent-Type字段的数据类型后面用“charset=xxx”来表示。

Accept-Charset: gbk, utf-8

Content-Type: text/html; charset=utf-8

这边不能乱,总结下:

数据类型表示实体数据的类型,是文本还是图片等,相关的头字段是 Accept 和 Content-Type;
压缩算法表示实体数据的压缩方式,相关的头字段是 Accept-Encoding 和 Content-Encoding;
语言类型表示实体数据的自然语言,相关的头字段是 Accept-Language 和 Content-Language;
字符集表示实体数据的编码方式,相关的头字段是 Accept-Charset和 Content-Type;

不过现在的浏览器都支持多种字符集,通常不会发送 Accept-Charset,而服务器也不会发送 Content-Language,因为使用的语言完全可以由字符集推断出来,所以在请求头里一般只会有 Accept-Language 字段,响应头里只会有 Content-Type字段:

6.2 Host、Connection、Referer、User-Agent
Host:指定被请求资源的IP和PORT
Referer:告诉服务器我是从哪个页面链接过来的
User-Agent:客户端所使用的操作系统和浏览器名称、版本
Connection:控制是否开启长连接,如果是keep-alive说明是开启长连接,同一个客户端的请求可以复用一个TCP连接。反之close就不复用TCP连接。
HTTP的头佷多,下面用几张图来预览下,后续将根据实际情况再对某些头部字段进行详解。

常用的HTTP请求头

常用的HTTP响应头

一、大话HTTP协议-HTTP的前世今生

HTTP协议是我们的老朋友,它自身也是一路升级打怪走过来,挺不容易的,本文来说说HTTP的前世今生,加油!

一、Web的起源
在学习大名鼎鼎的HTTP协议之前,有必要先说下Web。在 IT 领域,Web 是 World Wide Web(万维网,一般简写为 WWW)的简称。WWW 可以让 Web 客户端(例如我们常用的浏览器,如 Chrome)访问 Web 服务器上的页面。 是一个由许多互相链接的超文本组成的系统,通过互联网访问。

在这个系统中,每个有用的事物,称为一样“资源”,并且由一个 URI(Uniform Resource Identifier 的缩写,表示“统一资源标识符”)标识。这些资源通过 HTTP(HyperText Transfer Protocol 的缩写,表示“超文本传输协议”)协议传送给用户,用户通过点击链接来获得资源。

注意,Internet 不等于 Web ,Web 是 Internet 的一部分,除了 Web,Internet 还包含其他服务:E-mail、FTP等服务。

我们在本系列第一篇文章中就提到了阿帕网,这是互联网的前身。后来,E-mail 等 Internet 的服务开始出现:1972年,e-mail 服务出现。最后,1991年终于诞生了Web。

Web 是谁发明的呢?那就是大名鼎鼎的万维网之父Tim Berbers Lee(蒂姆·伯纳斯·李)。

7月27日,在伦敦奥林匹克体育场举行的2012年伦敦奥运会开幕式上,一位英国科学家隆重登场,接受全场掌声,这个“感谢蒂姆”的场面惊动全球,成为开幕式的一个亮点。他就是互联网的发明者、被业界公认为“互联网之父”的英国人蒂姆•伯纳斯•李(Tim Berners-Lee)。在全世界的注目下,他在一台电脑前象征性地打出了一句话:“This is for Everyone”,含义是:互联网献给所有人。蒂姆•伯纳斯•李不仅被视为英国人的骄傲,他同样无可争辩地赢得了全世界的尊重。

在他之前,没有浏览器,没有 WWW,网络世界一片空白。1994 年,Tim Berners Lee 创立了著名的 W3C 组织,因为他觉得 Web 发展迅猛,需要有一个类似基金会或委员会的机构来规范,以达成全球统一标准。W3C 后来发明了一系列的语言和规范:HTML,CSS,XML 等。现在的 HTML5 也是他们规定的。W3C 最重要的工作是发展 Web 规范(被称为 recommendations,表示“推荐”),这些规范描述了 Web 的通信协议(比如 HTML 和 XHTML)和其他的构建模块。

二、浏览器背后的故事
我们知道,依靠Web服务,我们才有了如今丰富多彩的网页,Web 的操作主要基于 HTTP 协议,它也是 OSI 第 7 层(应用层)协议。HTTP 是 HyperText Transfer Protocol 的缩写,表示“超文本传输协议”,所有的 Web 文件都必须遵守这个标准。

设计 HTTP 最初的目的是为了提供一种发布和接收 HTML(超文本标记语言) 页面的方法。我们一般浏览网页,看到的网页地址都是 http:// 这样开头的,后面接域名。例如:http://www.google.com 。

HTTP:超文本传输协议,是一种通信协议,它允许将超文本标记语言(HTML)文档从Web服务器传送到客户端浏览器。

简单来说,就是传输超文本的一种协议,什么是超文本呢?

超文本大概就是,不仅仅是文字,还有多媒体:视频、图片、动画等。还有就是有超链接,点击一个按钮可以调到另一个页面去,每个页面都可以这样跳来跳去,就形成一个网络。

HTTP 基于 TCP 协议。当我们在浏览器的地址栏中输入一个 URL(Uniform Resource Locator 的缩写,表示“统一资源定位符”),按下回车后浏览器会分析出 URL 上面的域名,然后通过 DNS 服务器查询出域名映射的 IP 地址,浏览器根据查询到的 IP 地址与 Web 服务器进行通信,而通信的协议就是 HTTP 协议。大致流程可以用下图来表示:

我们知道,网页是由 HTML 标签组成的。浏览器拿到响应数据后按照一定的规则进行解析和页面渲染,从而让我们看到了谷歌首页。

三、HTTP协议的前世今生

3.1 HTTP/0.9版本
HTTP 协议最早是由我们的 Tim 大神提出的,早在1989年,Tim 还在 CERN 担任研究员的时候,提出了一个提案,此提案描绘了他对万维网最初的设想,他希望在 CERN 内部建立一个网络,满足内部信息交换的需求。一开始 CERN 并没有重视。到了1990年,Tim大神完成了3项 WWW 构建技术,分别是:

URI,统一资源标识符,作为互联网上的唯一标识。
HTML,超文本标记语言,描述超文本。
HTTP ,超文本传输协议,传输超文本。
1990年11月,CERN 成功研制出世界上第一台 Web 服务器和 Web 浏览器。1991年,Tim 发表了一篇 HTTP 协议文章,后来被称为 HTTP/0.9。 实际上这篇文章比较简短,很难被称为标准的协议,只是 Tim 个人的作品,解释了程序的实现过程。不过鉴于 Tim 对互联网的开创性贡献,后人将其HTTP的文章作为HTTP的第一版标准。

最早的版本协议非常简单,特性只有如下:

内容非常简单,只有一个命令GET
没有HEADER等描述数据的信息
服务器发送完毕,就关闭TCP连接(一个HTTP请求在一个TCP连接中完成)
协议规定,服务器只能回应HTML格式的字符串,不能回应别的格式。也没有状态码来区分正确和错误消息。
比如发起一个GET请求:

GET /index.html

服务器响应:


Hello World

1
2
3
那时候是互联网初期,计算机的处理能力包括网速等等都很弱,所以 HTTP 也逃脱不了那个时代的约束,因此设计的非常简单,而且也是纯文本格式。

李老当时的想法是文档存在服务器里面,我们只需要从服务器获取文档,因此只有 “GET”,也不需要啥请求头,并且拿完了就结束了,因此请求响应之后连接就断了。

这就是为什么 HTTP 设计为文本协议,并且一开始只有“GET”、响应之后连接就断了的原因了。

在我们现在看来这协议太简陋了,但是在当时这是互联网发展的一大步!一个东西从无到有是最困难的。

这时候的 HTTP 还没有版本号的,之所以称之为 HTTP / 0.9 是后人加上去了,为了区别之后的版本。

3.2 HTTP/1.0版本
人们的需求是无止尽的,随着图像和音频的发展,浏览器也在不断的进步予以支持。需求促使添加各种特性来满足用户的需求,经过了一系列的草案 HTTP/1.0 于 1996 年正式发布。HTTP 1.0 扩展了0.9版,其中主要增加了几个变化:

在请求中加入了HTTP版本号,如:GET /coolshell/index.html HTTP/1.0
HTTP 开始有 header了,不管是request还是response 都有header了。
增加了HTTP Status Code 标识相关的状态码。
还有 Content-Type 可以传输其它的文件了。
可以看到引入了新的方法,填充了操作的语义,像 HEAD 还可以只拿元信息不必传输全部内容,提高某些场景下的效率。

引入的响应状态码让请求方可以得知服务端的情况,可以区分请求出错的原因,不会一头雾水。

引入了头部,使得请求和响应更加的灵活,把控制数据和业务实体进行了拆分,也是一种解耦。

新增了版本号表明这是一种工程化的象征,说明走上了正途,毕竟没版本号无法管理。

引入了 Content-Type,支持传输不同类型的数据,丰富了协议的载体,充实了用户的眼球。

举个例子,请求如下:

GET / HTTP/1.0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
Accept: */*
1
2
3
可以看到,这个格式与0.9版有很大变化。
第一行是请求命令,必须在尾部添加协议版本(HTTP/1.0)。后面就是多行头信息,描述客户端的情况。

客户端请求的时候,可以使用Accept字段声明自己可以接受哪些数据格式。上面代码中,客户端声明自己可以接受任何格式的数据。

服务器的回应如下:

HTTP/1.0 200 OK
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84


Hello World

1
2
3
4
5
6
7
8
9
10
回应的格式是”头信息 + 一个空行(\r\n) + 数据”。其中,第一行是”协议版本 + 状态码(status code) + 状态描述”。

关于字符的编码,1.0版规定,头信息必须是 ASCII 码,后面的数据可以是任何格式。因此,服务器回应的时候,必须告诉客户端,数据是什么格式,这就是Content-Type字段的作用。

3.2 HTTP/1.1版本
HTTP/1.1 主要解决了HTTP 1.0的网络性能的问题,以及增加了一些新的东西:

新增了连接管理即 keepalive ,允许HTTP重用TCP链接。重用TCP链接可以省了每次请求都要在广域网上进行的TCP的三次握手的巨大开销。
支持 pipeline,无需等待前面的请求响应,即可发送第二次请求。可以减少整体的响应时间。(注:非幂等的POST 方法或是有依赖的请求是不能被pipeline化的)
允许响应数据分块(chunked),即响应的时候不标明Content-Length,客户端就无法断开连接,直到收到服务端的 EOF ,利于传输大文件。
新增缓存的控制和管理。
协议头注增加了 Language, Encoding, Type 等等头,让客户端可以跟服务器端进行更多的协商。
还正式加入了一个很重要的头—— HOST,这样的话,服务器就知道你要请求哪个网站了。因为可以有多个域名解析到同一个IP上,要区分用户是请求的哪个域名,就需要在HTTP的协议中加入域名的信息,而不是被DNS转换过的IP信息。

3.3 HTTP/2版本

虽然 HTTP/1.1 已经开始变成应用层通讯协议的一等公民了,但是还是有性能问题。在2010年时,Google 就在搞一个实验型的协议,这个协议叫SPDY,这个协议成为了HTTP/2的基础(也可以说成HTTP/2就是SPDY的复刻),HTTP/2 是2015年推出的,其发布后,Google 宣布移除对SPDY的支持,拥抱标准的 HTTP/2。主要新的特性有:

是二进制协议,不再是纯文本。
支持一个 TCP 连接发起多请求,移除了 pipeline。
利用 HPACK 压缩头部,减少数据传输量。
允许服务端主动推送数据。
从文本到二进制其实简化了整齐的复杂性,解析数据的开销更小,数据更加紧凑,减少了网络的延迟,提升了整体的吞吐量。

支持一个 TCP 连接发起多请求,即支持多路复用,像 HTTP/1.1 pipeline 还是有阻塞的情况,需要等前面的一个响应返回了后面的才能返回。

而多路复用就是完全异步化,这减少了整体的往返时间(RTT),解决了 HTTP 队头阻塞问题,也规避了 TCP 慢启动带来的影响。直观对比下HTTP1.1和HTTP2的速度区别:

HPACK 压缩头部,采用了静态表、动态表和哈夫曼编码,在客户端和服务器都维护请求头的列表,所以只需要增量和压缩过的头部信息,服务端拿到之后组装一下就能得到完整的头部信息。

形象一点就是如下图所示:

服务端主动推送数据,这个其实就是减少了请求的次数,比如客户端请求 1.html,我把 1.html 需要的 js 和 css 也一块送过去,省的之后客户端再请求我要 js ,我要这个 css。

3.4 HTTP/3版本

虽然 HTTP/2 解决了很多之前旧版本的问题,但是它还是存在一个巨大的问题,主要是底层支撑的 TCP 协议造成的。

上文提到 HTTP/2 使用了多路复用,一般来说同一域名下只需要使用一个 TCP 连接。但当这个连接中出现了丢包的情况,那就会导致 HTTP/2 的表现情况反倒不如 HTTP/1 了。

在出现丢包的情况下,整个 TCP 都要开始等待重传,也就导致了后面的所有数据都被阻塞了,这就是队头阻塞问题。另外 TLS 协议层面也有一个队头阻塞,因为 TLS 协议都是按照 record 来处理数据的,如果一个 record 中丢失了数据,也会导致整个 record 无法正确处理。

但是对于 HTTP/1.1 来说,可以开启多个 TCP 连接,出现这种情况反到只会影响其中一个连接,剩余的 TCP 连接还可以正常传输数据。

那么可能就会有人考虑到去修改 TCP 协议,其实这已经是一件不可能完成的任务了。因为 TCP 存在的时间实在太长,已经充斥在各种设备中,并且这个协议是由操作系统实现的,更新起来不大现实。

基于这个原因,Google 就更起炉灶搞了一个基于 UDP 协议的 QUIC 协议,并且使用在了 HTTP/3 上,HTTP/3 之前名为 HTTP-over-QUIC,从这个名字中我们也可以发现,HTTP/3 最大的改造就是使用了 QUIC(Quick UDP Internet Connections)。

没错,它的名字就叫做“快”!!!我们先来说说这个QUIC!说完它就差不多说完了HTTP/3。

首先,队头阻塞的问题,因为 TCP 不认识每个流分别是哪个请求的,所以它只能全部阻塞住,而 QUIC 知道,因此比如请求 A 丢包了,我就把 A 卡住了就行,请求 B 完全可以全部放行,丝毫不受影响。

快总得有个指标吧?我们用RTT这个参数来衡量为什么QUIC快,我们以建立HTTPS连接为例来说明。

RTT:RTT是Round Trip Time的缩写,通俗地说,就是通信一来一回的时间。

对于RTT,举个例子,比如TCP三次握手:一去(SYN),二回(SYN+ACK),三去(ACK),相当于是一个半来回,故TCP连接时间等于1.5RTT。

对于HTTP,一去(HTTP Request),一回(HTTP Response),相当于一个来回,故HTTP交易时间是1RTT。

当然了,我们是HTTPS,中间有一个TLS协议,TLS1.2版本,一去(Client hello),二回(Server Hello),三去(Key Exchange),因此TLS需要1.5个RTT。

对于TLS1.2协议来说,咱们HTTPS通信的时间总和=TCP连接时间+TLS连接时间+HTTP交易时间=1.5RTT+1.5RTT+1RTT=4RTT。(可以直观看出来为什么建立新的TCP连接很慢了吧)

而咱们的QUIC协议,是基于UDP,UDP不需要连接,因此不会有附带的RTT时间。谷歌的QUIC协议集成TCP可靠传输机制、TLS安全加密、HTTP2流量复用技术,可以看到没有了TCP三次握手的1.5RTT,那么只需要2.5RTT时间。

TLS1.2+TCP QUIC+UDP

此外,完成QUIC交易的连接的Session ID会缓存在浏览器内存里,如果用户再次打开该页面,无需建立TLS连接,直接使用缓存的Session ID对应的加密参数,服务器可以根据Session ID在缓存中查找对应的加密参数,并完成加密。因此重连TLS连接是一个0RTT事件,用户索要等待的事件是=HTTP交易事件=1RTT。

这个特性很重要,因为 TCP 是基于四元组(源IP,源端口,目标IP,目标端口)来确定连接的,而在移动网络的情况下 IP 地址会频繁的换,这会导致反复的建链。

咱们的HTTP3基于QUIC,无论是队头阻塞问题,还是建链效率问题都得到了提升,但是HTTP3更进一步,使用的QUIC集成的是TLS1.3版本,此版本更加简练,建立TLS连接只要1RTT,是因为浏览器帝赐以就把自己的密钥交换的素材发给了服务器,省去了第三次去的消息。那么整体时间就变为:

第一次连接时间=TLS1.3连接时间+HTTP交易时间=1RTT+1RTT=2RTT
重连时间=HTTP交易时间=1RTT
从整体来看,HTTP3=HTTP2+QUIC。而QUIC=TLS1.3+UDP。期待QUIC普及,期待UDP翻身。不过可以预想到由于种种兼容问题,HTTP3的普及仍需要很久很久的时间。