Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

如何理解mqtt用到的101交换协议 #250

Open
FrankKai opened this issue May 25, 2021 · 3 comments
Open

如何理解mqtt用到的101交换协议 #250

FrankKai opened this issue May 25, 2021 · 3 comments

Comments

@FrankKai
Copy link
Owner

使用过mqtt的同学都知道,mqtt连接时,在Network面板中的status是101。

Name Status Time
mqtt 101(Switching Protocols) Pending

那么101(Switching Protocols)到底是什么意思呢?
这篇文章将带你理解101交换协议是什么,以及101交换协议运用的协议升级机制。

  • 101交换协议
  • 协议升级机制
@FrankKai
Copy link
Owner Author

101交换协议

HTTP的101交换协议意味着client向server发送的消息中包含了Upgrade请求头,server会根据client发送的这个请求头切换协议。

server会在response中添加一个Upgrade响应头,来指示server切换到的协议。

用一句话来说就是:client通过在请求头中添加Upgrade告诉server切换协议,server在响应头中添加upgrade说明切换后的协议。

再简单一点就是:客户端告诉服务端去切换协议。

General

Request URL: wss://foo.bar
Request Method: GET
Status Code: 101 Switching Protocols

Request Headers

Connection: Upgrade 
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: xxx
Sec-WebSocket-Protocol: mqtt
Sec-WebSocket-Version: 13
Upgrade: websocket // client告诉server使用websocket协议
...

Response Headers

connection: Upgrade
sec-websocket-accept: fNs9ByuvC+rD75+tj2GMQAzbJms= // server基于client发出的Sec-WebSocket-Key:xxx计算得出,计算过程文章末尾有介绍
sec-websocket-protocol: mqtt
Upgrade: websocket // server告诉client,我们(client,server)使用的是websocket协议
...

其中这些请求头是什么意思呢?Connection,Sec-WebSocket-Extensions,Sec-WebSocket-Key,Sec-WebSocket-Protocol,Sec-WebSocket-Version等等。
响应头呢?sec-websocket-accept,sec-websocket-protocol。

看了下面的协议升级机制就明白了。

@FrankKai
Copy link
Owner Author

协议升级机制

HTTP1.1版本的协议有一个特殊的机制:升级一个已经建立的连接为另外一个协议,一般是通过Upgrade头来实现。

这个机制是可选的,它不能强制协议改变。虽然实现支持新协议,但是也可以选择不升级。在实际应用中,通常这个机制用于引导WebSocket进行连接

注意,HTTP2.0版本明确禁止使用这个机制。只能用于HTTP1.1。

升级HTTP/1.1连接

client可以使用Upgrade头去邀请服务器去切换为协议列表中的某一项,按照降序。

因为Upgrade是一个逐跳头,因此它需要一个Connection头。
这也就意味着一个典型的包含Upgrade报文头的请求为:

GET /index.html HTTP/1.1
Host: www.example.com
Connection: upgrade
Upgrade: example/1, foo/2

其他的头一般是依赖请求协议的;例如,WebSocket升级允许额外的头去配置WebSocket连接,并在打开时就有一定的安全性。

如果服务器决定去升级连接,分为升级成功和升级失败两种情况:

  • 升级成功:它会返回一个101 Switching Protocols响应状态,并且Upgrade头中指明它切换后的协议。
  • 升级失败:如果服务端不能升级连接,它会忽略Upgrade头,然后返回一个常规的响应(例如 200 OK)

服务器发送完101状态码之后,它可以立即开始使用新协议,并且进行与其他协议的handshake。一旦连接建立完成,连接就变为双向管道,初始化升级的请求可以再协议之上初始化。

协议升级机制的常规用法

Upgrade这个头会用在哪些场景呢?而且与WebSocket连接有关的请求头都有哪些呢?

  • 升级为WebSocket连接
  • 与WebSocket连接有关的请求头
    • Sec-WebSocket-Extensions
    • Sec-WebSocket-Key
    • Sec-WebSocket-Protocol
    • Sec-WebSocket-Version
    • Sec-WebSocket-Accept(只读)
升级为WebSocket连接

最常见的升级HTTP连接的场景,就是使用WebSockets的场景,通常是通过升级HTTP或者HTTPS连接的方式来实现。如果你使用WebSocket API去开启一个连接,或者任何WebSockets的库,大多数或者说所有的事情都已经为你做好了。

例如:建立一个WebSocket连接非常简单,只需要这样既可:

webSocket = new WebSocket("ws://destination.server.ext", "optionalProtocol")

WebSockek()构造函数为开发者在内部做了所有创建一个HTTP/1.1连接,握手和升级的事情。

可以用"wss://" 去建立一个安全的WebSocket连接。

如果你想自己手动建立一个WebSocket连接的话,你需要自己去处理握手过程。在创建完HTTP/1.1会话之后,你需要在请求上添加Upgrade和Connection这两个请求头。

Connection: Upgrade
Upgrade: websocket
与WebSocket连接有关的请求头

下面这些请求头是WebSocket升级过程中包含的请求头。与Upgrade和Connection头不同,下面这些请求头

Sec-WebSocket-Extensions

声明一个或者多个协议级的WebSocket扩展区告诉服务器使用。在一个请求里使用一个或者多个Sec-WebSocket-Extension头是可以的;放在一起用分号隔开也可以。

Sec-WebSocket-Extensions: extensions

extensions需要用分号分开。需要从插件列表里选择。

例如:Sec-WebSocket-Extensions: superspeed, colormode; depth=16

我们上面例子中的permessage-deflate也在其中。

permessage-deflate | WebSocket Per-Message Deflate | [RFC7692] | None | [RFC7692]

Sec-WebSocket-Key

向服务端提供客户端有权升级为WebSocket的信息。这个头可用于不安全的HTTP想要升级时,为了提供某种程度的保护,防止滥用。key的值使用在WebSocket规范中定义的算法生成,所以这并不保证安全性。

这个key是为了放置非WebSocket的客户端无意中进行websocket连接或者滥用。
本质上,这个key代表着:“是的,我确实是要开启一个WebSocket连接的。”

这个头会自动被使用它的客户端添加,不能被XMLHttpRequest.setRequestHeader() 添加

Sec-WebSocket-Key: key

基于这个key,服务器会在响应的Sec-WebSocket-Accept头中加一个基于这个key的计算数据。

Sec-WebSocket-Protocol

这个头会声明一个或者多个你想要使用的WebSocket协议。
请求头发送Sec-WebSocket-Protocol,响应头也会返回Sec-WebSocket-Protocol。

Sec-WebSocket-Protocol: subprotocols

subprotocols包括以下这些协议:https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name

上面示例中的mqtt也在其中:mqtt | mqtt | [MQTT Version 5.0]

Sec-WebSocket-Version

作为请求头:
声明客户端使用的WebSocket协议版本。

Sec-WebSocket-Version: version

服务器与客户端通信的WebSocket协议版本:https://www.iana.org/assignments/websocket/websocket.xml#version-number

最常用的是13。
image

作为响应头:
如果服务器不支持WebSocket协议,会返回类似426(Upgrade Required)并且在Sec-WebSocket-Version头中返回支持的WebSocket版本列表。如果不支持,不返回Sec-WebSocket-Version头。

Sec-WebSocket-Version: supportedVersions
Sec-WebSocket-Accept

服务器与客户端建立握手过程中的响应头。至多出现一次:

Sec-WebSocket-Accept: hash

如果有Sec-WebSocket-Key,将字符串“ 258EAFA5-E914-47DA-95CA-C5AB0DC85B11”连接到该字符串,并且取SHA-1的20位 hash值。最后进行base64编码。

在我们的例子中:

Sec-WebSocket-Key: xxx
Sec-WebSocket-Accept: fNs9ByuvC+rD75+tj2GMQAzbJms=

通过Sec-WebSocket-Key生成Sec-WebSocket-Accept的编码过程如下:

const SecWebSocketKey = "xxx"
const helper = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
const result = SecWebSocketKey + helper

const crypto = require('crypto')
const shasum = crypto.createHash('sha1')
shasum.update(result)
const SecWebSocketAccept = shasum.digest('base64');
console.log(SecWebSocketAccept) // fNs9ByuvC+rD75+tj2GMQAzbJms=

在线demo:https://www.jdoodle.com/ia/e3G

既然key到accept的算法已经很明晰了,那么可以通过accept反向求解出key吗?
答案是否定的。这是因为用到了sha1加密。

密码强哈希函数有两个特点:其中一个很重要的特点就是不可逆。不可逆性意味着原始数据无法从其散列中重建,所以不能通过accept反向求解出key。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant