Huangjj

  • 首页

  • 标签

  • 分类

  • 归档

  • 搜索

koa-session2源码分析与使用方法

发表于 2019-05-28 | 更新于 2019-06-03 | 分类于 后端
本文字数: 8.3k | 阅读时长 ≈ 8 分钟

store.js

store.js 用来作为存储session的仓库,它定义了一个Store类,含有sessions和__timer两个map类型的属性,都是通过一个sid获取对应的session和timer。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const { randomBytes } = require('crypto');

class Store {
constructor() {
this.sessions = new Map();
this.__timer = new Map();
}

getID(length) {
return randomBytes(length).toString('hex');
}

get(sid) {
if (!this.sessions.has(sid)) return undefined;
// We are decoding data coming from our Store, so, we assume it was sanitized before storing
return JSON.parse(this.sessions.get(sid));
}

set(session, { sid = this.getID(24), maxAge } = {}) {
// Just a demo how to use maxAge and some cleanup
if (this.sessions.has(sid) && this.__timer.has(sid)) {
const __timeout = this.__timer.get(sid);
if (__timeout) clearTimeout(__timeout);
}

if (maxAge) {
this.__timer.set(sid, setTimeout(() => this.destroy(sid), maxAge));
}
try {
this.sessions.set(sid, JSON.stringify(session));
} catch (err) {
console.log('Set session error:', err);
}

return sid;
}

destroy(sid) {
this.sessions.delete(sid);
this.__timer.delete(sid);
}
}

module.exports = Store;
  • 构造函数:初始化sessions和__timer两个map;
  • getID(length):生成给定长度的随机二进制串,转为十六进制字符串后返回,作为session的id;
  • get(sid):通过sid,获取对应的session对象,若sid无效,返回undefined;
  • set(session, { sid = this.getID(24), maxAge } = {}):参数1是一个对象,将被转化为json字符串存入仓库中;参数2是一个对象,使用解构赋值,赋值给sid,maxAge:若参数2的对象没有sid,则会生成一个新的sid,若参数2没有maxAge,则maxAge为undefined, 该session将不会过期。set函数功能为:给传来的sid、或新生成的sid设置一个对应的json对象字符串,存储在sessionsmap中,若maxAge不为空,则给该sid设置一个对应的定时器,maxAge毫秒后执行destroy(sid)函数。若原sid存在,可以重置过期时间。
  • destroy(sid):删除sessions和__timer两个map中,sid对应的项。

koa-session2/index.js

index.js 是koa-session2的入口文件,向外暴露一个函数,参数是一个配置对象,返回一个异步函数async (ctx, next) => {}用作中间件。代码和注释如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
const Store = require('./libs/store.js');

module.exports = (opts = {}) => {
//key默认为"koa:sess",用于作为传给前端cookie的name;若opts中含有key,则使用opts中的值
//store默认为Store的一个实例;若opts中含有store,则使用opts中的store
//关于cookie所需的其他配置信息如maxAge等在opts参数中,可以为空
const { key = "koa:sess", store = new Store() } = opts;

return async (ctx, next) => {
//首先获得请求头中的cookie中名为key的cookie值作为session id
let id = ctx.cookies.get(key, opts);
let need_refresh = false;

//若id不存在,则新建一个空的session
if(!id) {
ctx.session = {};
} else {
//根据id从store中查找session
ctx.session = await store.get(id, ctx);

//若未查到session,则说明原id(请求头中的cookie值)无效,
//需要重新生成一个sid,发送给前端作为cookie
// reassigning session ID if current is not found
if (ctx.session == null) {
id = await store.getID(24);
need_refresh = true;
}

// check session must be a no-null object
if(typeof ctx.session !== "object" || ctx.session == null) {
ctx.session = {};
}
}

//把原session对象转为json字符串存储在old变量中,在后面用于比较判断session是否改变
const old = JSON.stringify(ctx.session);

// add refresh function
ctx.session.refresh = () => {need_refresh = true}

//将控制权交给其他中间件处理,在其他中间件中,可以通过ctx.session修改session
await next();

// remove refresh function
if(ctx.session && 'refresh' in ctx.session) {
delete ctx.session.refresh
}

//将现在的session对象转化为json字符串,与原session字符串进行比较
const sess = JSON.stringify(ctx.session);

//若未改变,且need_refresh为假(在其他中间件中未调用过ctx.session.refresh()函数)
// if not changed
if(!need_refresh && old == sess) return;

// if is an empty object
if(sess == '{}') {
ctx.session = null;
}

//若id存在,且现在session为空,
//则在store中删除该id对应session和timer,且给前端设置cookie key的值为空
// need clear old session
if(id && !ctx.session) {
await store.destroy(id, ctx);
ctx.cookies.set(key, null);
return;
}

//在store中生成id对应的session,
//session值为现在ctx中的session,
//opts中的配置信息如maxAge会被传入store.set的第二个参数对象中(maxAge可以不存在,则store中会创建一个不过期的session),
//id也会被传入store.set的第二个参数对象中作为sid(id可以不存在,则store会新建一个sid,并将sid返回)
// set/update session
const sid = await store.set(ctx.session, Object.assign({}, opts, {sid: id}), ctx);
//3种情况会给前端发送set-cookie头:
//1. 原id不存在,即原请求头中不含key对应的cookie,而后端为其创建了一个session和对应的sid,所以需要向前端发送set-cookie头
//2. id !== sid
//3. need_refresh为真(在其他中间件调用过ctx.session.refresh()函数)
if(!id || id !== sid || need_refresh) ctx.cookies.set(key, sid, opts);
}
}

// Reeexport Store to not use reference to internal files
module.exports.Store = Store;

koa-session2使用方法

由上述代码及注释,可以总结出以下几点使用方法:

1. 向app添加koa-session2中间件

将koa-session2中间件添加在其他需要使用session的中间件之前。后端在收到请求时,先通过koa-session2中间件向ctx添加了session属性,然后将控制器交给其他中间件,其他中间件中可以使用ctx.session,最后在其他中间件执行完后回到koa-session2,koa-session2会根据当前状态决定是否给前端发送cookie。

1
2
3
4
5
app.use(session({
store: new Store(), //session仓库,可以使用Redis
key: "SESSIONID", //cookie的name
maxAge: 6000000, //cookie有效时间(毫秒),若不设置,则cookie永久有效
}));

2. 面对前端发送请求的各种情况,后端处理该请求的中间异步函数如何处理能够利用koa-session实现seesion机制?

  1. 前端请求无对应的cookie(sid)时,分析以下三种情况:
    1. 进行登录请求:从源码可以看出,当sid不存在时,koa-session2首先会创一个空的session对象,若在其他中间件中修改了ctx.session导致old != sess或者在其他中间件中调用了ctx.session.refresh()使need_refresh为true,可以使koa-session2继续下面的步骤,为该session创建一个sid,将sid作为cookie返回给前端。一般我们可以在用户无cookie进行登录操作时,验证完用户名、密码后,给ctx.session添加一个username属性,这样既可以时session与用户关联,也能时koa-session2将sid发给前端。
    2. 进行不需要cookie的请求:由于我们不希望在这里给该请求生成session和cookie,因此要注意在处理该请求时,不能给ctx.session添加属性或调用ctx.session.refresh()。
    3. 进行需要cookie的请求:若该请求需要有效身份信息,而前端请求无cookie,则后端返回一个重定向至登录界面即可。如何判断是请求否有对应cookie:(1)可以在处理该请求的异步函数中检查是否有ctx.cookies.get(key);但可能前端有对应cookie,但该cookie无效,因此推荐方法(2)。(2)检查ctx.session.username是否存在。因为对于一个有效的cookie,我们在之前登录操作中就给该session添加了一个username属性,因此若ctx.session.username不存在,则说明该cookie无效,后端返回一个重定向即可。
  2. 前端请求对应的cookie(sid)无效时:
    1. 进行登录请求:由源码可知koa-session2首先根据该无效sid从store中查找session时,发现session为空,则会生成一个新的sid,并且生成一个空的session对象。在经过其他中间件后判断if(id && !ctx.session),若为真,则koa-session2会删除该session对象,并将前端对应cookie置为空。因此,与情况1.1一样处理即可:验证完用户名、密码后,给ctx.session添加一个username属性。
    2. 进行不需要cookie的请求:与情况1.2一样,我们不希望在这里给该请求生成session和cookie,因此我们在处理该请求时,不给ctx.session添加属性即可,这样后端会将前端该错误cookie清空。
    3. 进行需要cookie的请求:与情况1.3一样,检查到ctx.session.username不存在,则说明cookie无效,返回重定向登录界面即可。
  3. 前端拥有正确的cookie时:

    1. 进行登录操作:有两种处理方式:(1)检查到ctx.session.username存在,说明cookie正确,返回重定向至主页面即可,前端在请求主页面时,会自动携带cookie,使用对应的身份进行操作。(2)在验证完用户名和密码后,先将ctx.session置为空对象,再给ctx.session添加一个username属性。无论ctx.session是否与原先相同,后端是否发送cookie给前端,前端存的都是原来的cookie,而此cookie就是现在登录的用户对应session的sid。总结:推荐方法(2),因为方法二可以处理登录新用户的情况,这样在登录新用户的同时相对于登出了原用户;且方法(2)处理登录请求时无需加额外条件判断,无论前端是否携带cookie,均先将ctx.session置为空对象,再给ctx.session添加一个username属性即可。因为若请求未携带cookie,则ctx.session本身就只含一个refresh函数,且在回到koa-session2后首先会将refresh从ctx.session中清除,先将ctx.session置为空对象并无影响。

    2. 进行登出操作:根据以下源码可知,当在其他中间件中将ctx.session置为空对象时,回到koa-session2后,koa-session2会自动删除store中存储的对应session,并且会将前端对应cookie置空,因此直接执行ctx.session = {}即可实现登出。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // if is an empty object
      if(sess == '{}') {
      ctx.session = null;
      }

      //若id存在,且现在session为空,
      //则在store中删除该id对应session和timer,且给前端设置cookie key的值为空
      // need clear old session
      if(id && !ctx.session) {
      await store.destroy(id, ctx);
      ctx.cookies.set(key, null);
      return;
      }
  1. 进行其他请求时,根据用户身份进行相应的逻辑处理即可。

3. 总结后端请求处理函数的处理方法

  1. 登录:验证用户名密码成功后,执行ctx.session = {username: <username>}即可,否则返回登录失败。
  2. 登出:验证ctx.session.username存在后,说明已经是登录状态,执行ctx.session = {}即可。
  3. 需要正确cookie的请求:若ctx.session.username不存在,则返回重定向至登录界面(注:若为xhr请求,前端收到重定向后要自行处理进行页面跳转);否则,可以根据ctx.session.username,得到用户身份进行正常请求处理,可以向ctx.session添加其他属性存储会话信息。
  4. 不需要cookie的请求:注意不要修改ctx.session或调用ctx.session.refresh()即可。

http协议

发表于 2019-04-16 | 更新于 2019-05-31 | 分类于 web
本文字数: 11k | 阅读时长 ≈ 10 分钟

HTTP 简介

HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网服务器传输超文本到本地浏览器的传送协议。

HTTP是一个基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。

HTTP 工作原理

HTTP协议工作于客户端-服务端架构上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。

Web服务器有:Apache服务器,IIS服务器(Internet Information Services)等。

Web服务器根据接收到的请求后,向客户端发送响应信息。

HTTP默认端口号为80,但是你也可以改为8080或者其他端口。

HTTP 消息结构

客户端请求消息

客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。

服务器响应消息

HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。

HTTP状态码

当浏览者访问一个网页时,浏览者的浏览器会向网页所在服务器发出请求。当浏览器接收并显示网页前,此网页所在的服务器会返回一个包含HTTP状态码的信息头(server header)用以响应浏览器的请求。

HTTP状态码分类

HTTP状态码由三个十进制数字组成,第一个十进制数字定义了状态码的类型,后两个数字没有分类的作用。HTTP状态码共分为5种类型:

分类 分类描述
1** 信息,服务器收到请求,需要请求者继续执行操作
2** 成功,操作被成功接收并处理
3** 重定向,需要进一步的操作以完成请求
4** 客户端错误,请求包含语法错误或无法完成请求
5** 服务器错误,服务器在处理请求的过程中发生了错误

常见HTTP状态码列表

状态码 状态码英文名称 中文描述
100 Continue 继续。客户端应继续其请求
101 Switching Protocols 切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议,例如,切换到HTTP的新版本协议
200 OK 请求成功。一般用于GET与POST请求
206 Partial Content 部分内容。服务器成功处理了部分GET请求
300 Multiple Choices 多种选择。请求的资源可包括多个位置,相应可返回一个资源特征与地址的列表用于用户终端(例如:浏览器)选择
301 Moved Permanently 永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替
302 Found 临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI
303 See Other 查看其它地址。与301类似。使用GET和POST请求查看
304 Not Modified 未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源
307 Temporary Redirect 临时重定向。与302类似。使用GET请求重定向
400 Bad Request 客户端请求的语法错误,服务器无法理解
401 Unauthorized 请求要求用户的身份认证
402 Payment Required 保留,将来使用
403 Forbidden 服务器理解请求客户端的请求,但是拒绝执行此请求
404 Not Found 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置”您所请求的资源无法找到”的个性页面
500 Internal Server Error 服务器内部错误,无法完成请求
501 Not Implemented 服务器不支持请求的功能,无法完成请求

常用的HTTP请求头

请求头 说明 示例 状态
Accept 可接受的响应内容类型(Content-Types)。 Accept: text/plain 固定
Accept-Charset 可接受的字符集 Accept-Charset: utf-8 固定
Accept-Encoding 可接受的响应内容的编码方式。 Accept-Encoding: gzip, deflate 固定
Accept-Language 可接受的响应内容语言列表。 Accept-Language: en-US 固定
Accept-Datetime 可接受的按照时间来表示的响应内容版本 Accept-Datetime: Sat, 26 Dec 2015 17:30:00 GMT 临时
Authorization 用于表示HTTP协议中需要认证资源的认证信息 Authorization: Basic OSdjJGRpbjpvcGVuIANlc2SdDE== 固定
Cache-Control 用来指定当前的请求/回复中的,是否使用缓存机制。 Cache-Control: no-cache 固定
Connection 客户端(浏览器)想要优先使用的连接类型 Connection: keep-alive Connection: Upgrade 固定
Cookie 由之前服务器通过Set-Cookie(见下文)设置的一个HTTP协议Cookie Cookie: $Version=1; Skin=new; 固定:标准
Content-Length 以8进制表示的请求体的长度 Content-Length: 348 固定
Content-MD5 请求体的内容的二进制 MD5 散列值(数字签名),以 Base64 编码的结果 Content-MD5: oD8dH2sgSW50ZWdyaIEd9D== 废弃
Content-Type 请求体的MIME类型 (用于POST和PUT请求中) Content-Type: application/x-www-form-urlencoded 固定
Date 发送该消息的日期和时间(以RFC 7231中定义的”HTTP日期”格式来发送) Date: Dec, 26 Dec 2015 17:30:00 GMT 固定
Expect 表示客户端要求服务器做出特定的行为 Expect: 100-continue 固定
From 发起此请求的用户的邮件地址 From: user@itbilu.com 固定
Host 表示服务器的域名以及服务器所监听的端口号。如果所请求的端口是对应的服务的标准端口(80),则端口号可以省略。 Host: www.itbilu.com:80 Host: www.itbilu.com 固定
If-Match 仅当客户端提供的实体与服务器上对应的实体相匹配时,才进行对应的操作。主要用于像 PUT 这样的方法中,仅当从用户上次更新某个资源后,该资源未被修改的情况下,才更新该资源。 If-Match: “9jd00cdj34pss9ejqiw39d82f20d0ikd” 固定
If-Modified-Since 允许在对应的资源未被修改的情况下返回304未修改 If-Modified-Since: Dec, 26 Dec 2015 17:30:00 GMT 固定
If-None-Match 允许在对应的内容未被修改的情况下返回304未修改( 304 Not Modified ),参考 超文本传输协议 的实体标记 If-None-Match: “9jd00cdj34pss9ejqiw39d82f20d0ikd” 固定
If-Range 如果该实体未被修改过,则向返回所缺少的那一个或多个部分。否则,返回整个新的实体 If-Range: “9jd00cdj34pss9ejqiw39d82f20d0ikd” 固定
If-Unmodified-Since 仅当该实体自某个特定时间以来未被修改的情况下,才发送回应。 If-Unmodified-Since: Dec, 26 Dec 2015 17:30:00 GMT 固定
Max-Forwards 限制该消息可被代理及网关转发的次数。 Max-Forwards: 10 固定
Origin 发起一个针对跨域资源共享的请求(该请求要求服务器在响应中加入一个Access-Control-Allow-Origin的消息头,表示访问控制所允许的来源)。 Origin: http://www.itbilu.com 固定: 标准
Pragma 与具体的实现相关,这些字段可能在请求/回应链中的任何时候产生。 Pragma: no-cache 固定
Proxy-Authorization 用于向代理进行认证的认证信息。 Proxy-Authorization: Basic IOoDZRgDOi0vcGVuIHNlNidJi2== 固定
Range 表示请求某个实体的一部分,字节偏移以0开始。 Range: bytes=500-999 固定
Referer 表示浏览器所访问的前一个页面,可以认为是之前访问页面的链接将浏览器带到了当前页面。Referer其实是Referrer这个单词,但RFC制作标准时给拼错了,后来也就将错就错使用Referer了。 Referer: http://itbilu.com/nodejs 固定
TE 浏览器预期接受的传输时的编码方式:可使用回应协议头Transfer-Encoding中的值(还可以使用”trailers”表示数据传输时的分块方式)用来表示浏览器希望在最后一个大小为0的块之后还接收到一些额外的字段。 TE: trailers,deflate 固定
User-Agent 浏览器的身份标识字符串 User-Agent: Mozilla/…… 固定
Upgrade 要求服务器升级到一个高版本协议。 Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 固定
Via 告诉服务器,这个请求是由哪些代理发出的。 Via: 1.0 fred, 1.1 itbilu.com.com (Apache/1.1) 固定
Warning 一个一般性的警告,表示在实体内容体中可能存在错误。 Warning: 199 Miscellaneous warning 固定

常用的HTTP响应头

响应头 说明
Allow 服务器支持哪些请求方法(如GET、POST等)。
Content-Encoding 文档的编码(Encode)方法。只有在解码之后才可以得到Content-Type头指定的内容类型。利用gzip压缩文档能够显著地减少HTML文档的下载时间。Java的GZIPOutputStream可以很方便地进行gzip压缩,但只有Unix上的Netscape和Windows上的IE 4、IE 5才支持它。因此,Servlet应该通过查看Accept-Encoding头(即request.getHeader(“Accept-Encoding”))检查浏览器是否支持gzip,为支持gzip的浏览器返回经gzip压缩的HTML页面,为其他浏览器返回普通页面。
Content-Length 表示内容长度。只有当浏览器使用持久HTTP连接时才需要这个数据。如果你想要利用持久连接的优势,可以把输出文档写入 ByteArrayOutputStream,完成后查看其大小,然后把该值放入Content-Length头,最后通过byteArrayStream.writeTo(response.getOutputStream()发送内容。
Content-Type 表示后面的文档属于什么MIME类型。Servlet默认为text/plain,但通常需要显式地指定为text/html。由于经常要设置Content-Type,因此HttpServletResponse提供了一个专用的方法setContentType。
Date 当前的GMT时间。你可以用setDateHeader来设置这个头以避免转换时间格式的麻烦。
Expires 应该在什么时候认为文档已经过期,从而不再缓存它?
Last-Modified 文档的最后改动时间。客户可以通过If-Modified-Since请求头提供一个日期,该请求将被视为一个条件GET,只有改动时间迟于指定时间的文档才会返回,否则返回一个304(Not Modified)状态。Last-Modified也可用setDateHeader方法来设置。
Location 表示客户应当到哪里去提取文档。Location通常不是直接设置的,而是通过HttpServletResponse的sendRedirect方法,该方法同时设置状态代码为302。
Refresh 表示浏览器应该在多少时间之后刷新文档,以秒计。除了刷新当前文档之外,你还可以通过setHeader(“Refresh”, “5; URL=http://host/path")让浏览器读取指定的页面。 注意这种功能通常是通过设置HTML页面HEAD区的<META HTTP-EQUIV=”Refresh” CONTENT=”5;URL=http://host/path">实现,这是因为,自动刷新或重定向对于那些不能使用CGI或Servlet的HTML编写者十分重要。但是,对于Servlet来说,直接设置Refresh头更加方便。 注意Refresh的意义是”N秒之后刷新本页面或访问指定页面”,而不是”每隔N秒刷新本页面或访问指定页面”。因此,连续刷新要求每次都发送一个Refresh头,而发送204状态代码则可以阻止浏览器继续刷新,不管是使用Refresh头还是<META HTTP-EQUIV=”Refresh” …>。 注意Refresh头不属于HTTP 1.1正式规范的一部分,而是一个扩展,但Netscape和IE都支持它。
Server 服务器名字。Servlet一般不设置这个值,而是由Web服务器自己设置。
Set-Cookie 设置和页面关联的Cookie。Servlet不应使用response.setHeader(“Set-Cookie”, …),而是应使用HttpServletResponse提供的专用方法addCookie。参见下文有关Cookie设置的讨论。
WWW-Authenticate 客户应该在Authorization头中提供什么类型的授权信息?在包含401(Unauthorized)状态行的应答中这个头是必需的。例如,response.setHeader(“WWW-Authenticate”, “BASIC realm=\”executives\””)。 注意Servlet一般不进行这方面的处理,而是让Web服务器的专门机制来控制受密码保护页面的访问(例如.htaccess)。

HTTP1.0

早先1.0的HTTP版本,是一种无状态、无连接的应用层协议。

HTTP1.0规定浏览器和服务器保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器处理完成后立即断开TCP连接(无连接),服务器不跟踪每个客户端也不记录过去的请求(无状态)。

这种无状态性可以借助cookie/session机制来做身份认证和状态记录。而下面两个问题就比较麻烦了。

首先,无连接的特性导致最大的性能缺陷就是无法复用连接。每次发送请求的时候,都需要进行一次TCP的连接,而TCP的连接释放过程又是比较费事的。这种无连接的特性会使得网络的利用率非常低。

其次就是队头阻塞(head of line blocking)。由于HTTP1.0规定下一个请求必须在前一个请求响应到达之前才能发送。假设前一个请求响应一直不到达,那么下一个请求就不发送,同样的后面的请求也给阻塞了。

为了解决这些问题,HTTP1.1出现了。

HTTP1.1

对于HTTP1.1,不仅继承了HTTP1.0简单的特点,还克服了诸多HTTP1.0性能上的问题。

首先是长连接,HTTP1.1增加了一个Connection字段,通过设置Keep-Alive可以保持HTTP连接不断开,避免了每次客户端与服务器请求都要重复建立释放建立TCP连接,提高了网络的利用率。如果客户端想关闭HTTP连接,可以在请求头中携带Connection: false来告知服务器关闭请求。

其次,是HTTP1.1支持请求管道化(pipelining)。基于HTTP1.1的长连接,使得请求管线化成为可能。管线化使得请求能够“并行”传输。举个例子来说,假如响应的主体是一个html页面,页面中包含了很多img,这个时候keep-alive就起了很大的作用,能够进行“并行”发送多个请求。(注意这里的“并行”并不是真正意义上的并行传输,具体解释如下。)

需要注意的是,服务器必须按照客户端请求的先后顺序依次回送相应的结果,以保证客户端能够区分出每次请求的响应内容。

也就是说,HTTP管道化可以让我们把先进先出队列从客户端(请求队列)迁移到服务端(响应队列)。

如图所示,客户端同时发了两个请求分别来获取html和css,假如说服务器的css资源先准备就绪,服务器也会先发送html再发送css。

换句话来说,只有等到html响应的资源完全传输完毕后,css响应的资源才能开始传输。也就是说,不允许同时存在两个并行的响应。

可见,HTTP1.1还是无法解决队头阻塞(head of line blocking)的问题。同时“管道化”技术存在各种各样的问题,所以很多浏览器要么根本不支持它,要么就直接默认关闭,并且开启的条件很苛刻…而且实际上好像并没有什么用处。

那我们在谷歌控制台看到的并行请求又是怎么一回事呢?

如图所示,绿色部分代表请求发起到服务器响应的一个等待时间,而蓝色部分表示资源的下载时间。按照理论来说,HTTP响应理应当是前一个响应的资源下载完了,下一个响应的资源才能开始下载。而这里却出现了响应资源下载并行的情况。这又是为什么呢?

其实,虽然HTTP1.1支持管道化,但是服务器也必须进行逐个响应的送回,这个是很大的一个缺陷。实际上,现阶段的浏览器厂商采取了另外一种做法,它允许我们打开多个TCP的会话。也就是说,上图我们看到的并行,其实是不同的TCP连接上的HTTP请求和响应。这也就是我们所熟悉的浏览器对同域下并行加载6~8个资源的限制。而这,才是真正的并行!

此外,HTTP1.1还加入了缓存处理(强缓存和协商缓存[传送门])新的字段如cache-control,支持断点传输,以及增加了Host字段(使得一个服务器能够用来创建多个Web站点)。

HTTP2.0

HTTP2.0的新特性大致如下:

二进制分帧

HTTP2.0通过在应用层和传输层之间增加一个二进制分帧层,突破了HTTP1.1的性能限制、改进传输性能。

可见,虽然HTTP2.0的协议和HTTP1.x协议之间的规范完全不同了,但是实际上HTTP2.0并没有改变HTTP1.x的语义。
简单来说,HTTP2.0只是把原来HTTP1.x的header和body部分用frame重新封装了一层而已。

多路复用(连接共享)

下面是几个概念:

  • 流(stream):已建立连接上的双向字节流。
  • 消息:与逻辑消息对应的完整的一系列数据帧。
  • 帧(frame):HTTP2.0通信的最小单位,每个帧包含帧头部,至少也会标识出当前帧所属的流(stream id)。

从图中可见,所有的HTTP2.0通信都在一个TCP连接上完成,这个连接可以承载任意数量的双向数据流。

每个数据流以消息的形式发送,而消息由一或多个帧组成。这些帧可以乱序发送,然后再根据每个帧头部的流标识符(stream id)重新组装。

举个例子,每个请求是一个数据流,数据流以消息的方式发送,而消息又分为多个帧,帧头部记录着stream id用来标识所属的数据流,不同属的帧可以在连接中随机混杂在一起。接收方可以根据stream id将帧再归属到各自不同的请求当中去。

另外,多路复用(连接共享)可能会导致关键请求被阻塞。HTTP2.0里每个数据流都可以设置优先级和依赖,优先级高的数据流会被服务器优先处理和返回给客户端,数据流还可以依赖其他的子数据流。

可见,HTTP2.0实现了真正的并行传输,它能够在一个TCP上进行任意数量HTTP请求。而这个强大的功能则是基于“二进制分帧”的特性。

头部压缩

在HTTP1.x中,头部元数据都是以纯文本的形式发送的,通常会给每个请求增加500~800字节的负荷。

比如说cookie,默认情况下,浏览器会在每次请求的时候,把cookie附在header上面发送给服务器。(由于cookie比较大且每次都重复发送,一般不存储信息,只是用来做状态记录和身份认证)

HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。高效的压缩算法可以很大的压缩header,减少发送包的数量从而降低延迟。

服务器推送

服务器除了对最初请求的响应外,服务器还可以额外的向客户端推送资源,而无需客户端明确的请求。

总结

HTTP1.0

  • 无状态、无连接

HTTP1.1

  • 持久连接
  • 请求管道化
  • 增加缓存处理(新的字段如cache-control)
  • 增加Host字段、支持断点传输等

HTTP2.0

  • 二进制分帧
  • 多路复用(或连接共享)
  • 头部压缩
  • 服务器推送

HTTPS和HTTP的区别:

  • https协议需要到ca申请证书,一般免费证书很少,需要交费。
  • HTTP协议运行在TCP之上,所有传输的内容都是明文,HTTPS运行在SSL/TLS之上,SSL/TLS运行在TCP之上,所有传输的内容都经过加密的。
  • http和https使用的是完全不同的连接方式用的端口也不一样,前者是80,后者是443。
  • http的连接很简单,是无状态的。
  • HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。
  • HTTPS是在HTTP的基础上和ssl/tls证书结合起来的一种协议,保证了传输过程中的安全性,减少了被恶意劫持的可能.很好的解决了解决了http的三个缺点(被监听、被篡改、被伪装)

HTTP的请求过程

  • 域名解析
  • TCP三次握手
  • 发起HTTP请求

https建立连接过程

2.2.1 客户端访问https连接

这一步,就是相当于我们在浏览器上输入url回车的过程。这个时候浏览器或者客户端(接下来统一为客户端)会把我们客户端支持的加密算法Cipher Suite(密钥算法套件)带给服务端。

2.2.2 - 2.2.3 服务端发送证书(公钥)给客户端

服务端接收Cipher后,和自己支持的加密算法进行比对,如果不符合,则断开连接。否则,服务端会把符合的算法和证书发给客户端,包括证书时间、证书日期、证书颁发的机构。

2.2.4- 2.2.5 客户端验证服务端的证书

1、客户端验证证书,包括颁发证书的机构是否合法与是否过期,证书中包含的网站地址是否与正在访问的地址一致等

2、验证通过后(或者用户接受了不信任的证书),客户端会生成一个随机字符串,然后用服务端的公钥进行加密。这里就保证了只有服务端才能看到这串随机字符串(因为服务端拥有公钥对应的私钥,RSA解密,可以知道客户端的随机字符串)。

3、生成握手信息 用约定好的HASH算法,对握手信息进行取HASH,然后用随机字符串加密握手信息和握手信息的签名HASH值,把结果发给服务端。这里之所以要带上握手信息的HASH是因为,防止信息被篡改。如果信息被篡改,那么服务端接收到信息进行HASH时,就会发现HASH值和客户端传回来的不一样。这里就保证了信息不会被篡改。

2.2.6 - 2.2.7 服务端接收加密信息,解密得到客户端提供的随机字符串

服务端接收到加密信息后,首先用私钥解密得到随机字符串。然后用随机字符串解密握手信息,获得握手信息和握手信息的HASH值,服务端对握手信息进行HASH,比对客户端传回来的HASH。如果相同,则说明信息没有被篡改。

服务端验证完客户端的信息以后,同样使用随机字符串加密握手信息和握手信息的HASH值发给客户端。

2.2.8 客户端验证服务端返回的握手信息,完成握手

客户端接收到服务端发回来的握手信息后,用一开始生成的随机字符串对密文进行解密,得到握手信息和握手信息的HASH值,像一步服务端验证一样对握手信息进行校验,校验通过后,握手完毕。从这里开始,客户端和服务端的通信就使用那串随机字符串进行AES对称加密通信。

2.3 验证总结

使用RSA非对称算法,服务端向客户端发送公钥,公钥包含了域名、颁发机构、过期日期。保证了公钥的合法性和服务端的身份正确性(而不会被黑客冒充)
客户端向第三方验证公钥的合法性,验证通过后向服务端约定对称加密的随机字符号。保证了随机字符串只有通信双方知道。
接下来的通信就使用这个随机字符号进行加密通信。因为随机字符串只有双方知道,所以信息不会被截获。

https://segmentfault.com/a/1190000013028798

https://www.ibm.com/developerworks/cn/web/wa-http2-under-the-hood/index.html

https://www.jianshu.com/p/33d0f8631f90

http协议无状态的含义

发表于 2019-04-15 | 更新于 2019-05-31 | 分类于 web
本文字数: 2.8k | 阅读时长 ≈ 3 分钟

有状态协议

常见的许多七层协议实际上是有状态的,例如SMTP协议,它的第一条消息必须是HELO,用来握手,在HELO发送之前其他任何命令都是不能发送的;接下来一般要进行AUTH阶段,用来验证用户名和密码;接下来可以发送邮件数据;最后,通过QUIT命令退出。可以看到,在整个传输层上,通信的双方是必须要时刻记住当前连接的状态的,因为不同的状态下能接受的命令是不同的;另外,之前的命令传输的某些数据也必须要记住,可能会对后面的命令产生影响。这种就叫做有状态的协议。

为什么说http协议是无状态协议

相反,为什么说HTTP是无状态的协议呢?因为它的每个请求都是完全独立的,每个请求包含了处理这个请求所需的完整的数据,发送请求不涉及到状态变更。即使在HTTP/1.1上,同一个连接允许传输多个HTTP请求的情况下,如果第一个请求出错了,后面的请求一般也能够继续处理(当然,如果导致协议解析失败、消息分片错误之类的自然是要除外的)。可以看出,这种协议的结构是要比有状态的协议更简单的,一般来说实现起来也更简单,不需要使用状态机,一个循环就行了。

为什么不改进http协议使之有状态

最初的http协议只是用来浏览静态文件的,无状态协议已经足够,这样实现的负担也很轻(相对来说,实现有状态的代价是很高的,要维护状态,根据状态来操作。)。随着web的发展,它需要变得有状态,但是不是就要修改http协议使之有状态呢?是不需要的。因为我们经常长时间逗留在某一个网页,然后才进入到另一个网页,如果在这两个页面之间维持状态,代价是很高的。其次,历史让http无状态,但是现在对http提出了新的要求,按照软件领域的通常做法是,保留历史经验,在http协议上再加上一层实现我们的目的(“再加上一层,你可以做任何事”)。所以引入了其他机制来实现这种有状态的连接。

无状态协议的优缺点

和许多人想象的不同,会话(Session)支持其实并不是一个缺点,反而是无状态协议的优点,因为对于有状态协议来说,如果将会话状态与连接绑定在一起,那么如果连接意外断开,整个会话就会丢失,重新连接之后一般需要从头开始(当然这也可以通过吸收无状态协议的某些特点进行改进);而HTTP这样的无状态协议,使用元数据(如Cookies头)来维护会话,使得会话与连接本身独立起来,这样即使连接断开了,会话状态也不会受到严重伤害,保持会话也不需要保持连接本身。另外,无状态的优点还在于对中间件友好,中间件无需完全理解通信双方的交互过程,只需要能正确分片消息即可,而且中间件可以很方便地将消息在不同的连接上传输而不影响正确性,这就方便了负载均衡等组件的设计。
无状态协议的主要缺点在于,单个请求需要的所有信息都必须要包含在请求中一次发送到服务端,这导致单个消息的结构需要比较复杂,必须能够支持大量元数据,因此HTTP消息的解析要比其他许多协议都要复杂得多。同时,这也导致了相同的数据在多个请求上往往需要反复传输,例如同一个连接上的每个请求都需要传输Host、Authentication、Cookies、Server等往往是完全重复的元数据,一定程度上降低了协议的效率。

web应用中实现有状态的方法

在http协议的基础上,web应用引入cookies, session, application。这样的东西来保持web应用之间的状态。

cookies, session,application都不是标准协议,但是各种网络应用提供商,实现语言、web容器等,都默认支持它。当然这种支持与对网络标准协议的支持是不同的,标准协议规定的接口,而这种机制,只是规定了思想。大的概念上,jsp和ASP的session机制所要实现的功能和实现的方法不会有太大的出入。

有人将web应用中有无状态的情况,比着顾客逛商店的情景。

顾客:浏览器访问方;

商店:web服务器;

一次购买:一次http访问;

我们知道,上一次顾客购买,并不代表顾客下一个小时一定会买(当然也不能代表不会)。也就是说同一个顾客的不同购买之间的关系是不定的。所以说实在的,这种情况下,让商店保存所有的顾客购买的信息,等到下一次购买可以知道这个顾客以前购买的内容代价非常大的。所以商店为了避免这个代价,索性就认为每次的购买都是一次独立的新的购买。浅台词:商店不区分对待老顾客和新过客。这就是无状态的。

但是,商店为了提高收益。她是想鼓励顾客购买的。所以告诉你,只要你在一个月内购买了5瓶以上的啤酒,就送你一个酒杯。

我们看看这种情况我们怎么去实现呢?

A,给顾客发放一个磁卡,里面放有顾客过去的购买信息。

这样商店就可以知道了。这就是cookie.

B,给顾客发放一个唯一号码,号码制定的顾客的消费信息,存储在商店的服务器中。这就是session。

最后,商店可以全局的决定,是5瓶为送酒杯还是6瓶。这就是application。

其实,这些机制都是在无状态的传统购买过程中加入了一点东西,使整个过程变得有状态。Web应用就是这样的。

HTTP协议是无状态协议,这句话本身到底对不对?

实际上,并不全对。HTTP/1.1中有一个Expect: 100-Continue的功能,它是这么工作的:

  1. 在发送大量数据的时候,考虑到服务端有可能直接拒收数据,客户端发出请求头并附带Expect: 100-Continue的HTTP头,不发送请求体,先等待服务器响应
  2. 服务器收到Expect: 100-Continue的请求,如果允许上传,发送100 Continue的HTTP响应(同一个请求可以有任意个1xx的响应,均不是最后的Response,只起到提示性作用);如果不允许,例如不允许上传数据,或者数据大小超出限制,直接返回4xx/5xx的错误
  3. 客户端收到100 Continue的响应之后,继续上传数据

可以看出,这实际上很明显是一个有状态协议的套路,它需要先进行一次握手,然后再真正发送数据。不过,HTTP协议也规定,如果服务端不进行100 Continue的响应,建议客户端在等待较短的时间之后仍然上传数据,以达成与不支持Expect: 100-Continue功能的服务器的兼容,这样可以算是“能有状态则有状态,否则回到无状态的路上”,这样说HTTP 1.x是无状态的协议也是没错的。

至于HTTP/2,它应该算是一个有状态的协议了(有握手和GOAWAY消息,有类似于TCP的流控),所以以后说“HTTP是无状态的协议”就不太对了,最好说“HTTP 1.x是无状态的协议”

[参考]

https://www.zhihu.com/question/23202402/answer/527748675

http://blog.sina.com.cn/s/blog_93b45b0f0101a4ix.html

Hello World

发表于 2019-03-15
本文字数: 430 | 阅读时长 ≈ 1 分钟

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

HuangJunjie

HuangJunjie

4 日志
2 分类
3 标签
GitHub
© 2019 HuangJunjie | 22k | 20 分钟
由 Hexo 强力驱动 v3.8.0
|
主题 – NexT.Mist v7.0.1