本文概述

Web Sockets的目标是在一个单独的持久连接上提供全双工、双向通信。在Javascript创建了Web Socket之后,会有一个HTTP请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会将HTTP升级从HTTP协议交换为WebSocket协议。
由于WebSocket使用自定义的协议,所以URL模式也略有不同。未加密的连接不再是http://,而是ws://;加密的连接也不是https://,而是wss://。在使用WebSocket URL时,必须带着这个模式,因为将来还有可能支持其他的模式。
使用自定义协议而非HTTP协议的好处是,能够在客户端和服务器之间发送非常少量的数据,而不必担心HTTP那样字节级的开销。由于传递的数据包很小,所以WebSocket非常适合移动应用。
上文中只是对Web Sockets进行了笼统的描述,接下来的篇幅会对Web Sockets的细节实现进行深入的探索,本文接下来的四个小节不会涉及到大量的代码片段,但是会对相关的API和技术原理进行分析,相信大家读完下文之后再来看这段描述,会有一种豁然开朗的感觉。

一、WebSocket复用了HTTP的握手通道

“握手通道”是HTTP协议中客户端和服务端通过"TCP三次握手"建立的连接通道。客户端和服务端使用HTTP协议进行的每次交互都需要先建立这样一条“通道”,然后通过这条通道进行通信。我们熟悉的ajax交互就是在这样一个通道上完成数据传输的,下面是HTTP协议中建立“握手通道”的过程示意图:

上文中我们提到:在Javascript创建了WebSocket之后,会有一个HTTP请求发送到浏览器以发起连接,然后服务端响应,这就是“握手“的过程,在这个握手的过程当中,客户端和服务端主要做了两件事情:

  1. 建立了一条连接“握手通道”用于通信(这点和HTTP协议相同,不同的是HTTP协议完成数据交互后就释放了这条握手通道,这就是所谓的“短连接”,它的生命周期是一次数据交互的时间,通常是毫秒级别的。)
  2. 将HTTP协议升级到WebSocket协议,并复用HTTP协议的握手通道,从而建立一条持久连接。

    说到这里可能有人会问:HTTP协议为什么不复用自己的“握手通道”,而非要在每次进行数据交互的时候都通过TCP三次握手重新建立“握手通道”呢?答案是这样的:虽然“长连接”在客户端和服务端交互的过程中省去了每次都建立“握手通道”的麻烦步骤,但是维持这样一条“长连接”是需要消耗服务器资源的,而在大多数情况下,这种资源的消耗又是不必要的,可以说HTTP标准的制定经过了深思熟虑的考量。到我们后边说到WebSocket协议数据帧时,大家可能就会明白,维持一条“持久连接”服务端和客户端需要做的事情太多了。

    说完了握手通道,我们再来看HTTP协议如何升级到WebSocket协议的。

二、HTTP协议升级为WebSocket协议

  1. 升级协议需要客户端和服务端交流,服务端怎么知道要将HTTP协议升级到WebSocket协议呢?它一定是接收到了客户端发送过来的某种信号。下面是我从谷歌浏览器中截取的“客户端发起协议升级请求的报文”,通过分析这段报文,我们能够得到有关WebSocket中协议升级的更多细节。

  1. 首先,客户端发起协议升级请求。采用的是标准的HTTP报文格式,且只支持GET方法。下面是重点请求的首部的意义:
  1. Connection:Upgrade:表示要升级的协议
  2. Upgrade: websocket:表示要升级到websocket协议
  3. Sec-WebSocket-Version: 13:表示websocket的版本
  4. Sec-WebSocket-Key:UdTUf90CC561cQXn4n5XRg== :与Response Header中的响应首部Sec-WebSocket-Accept: GZk41FJZSYY0CmsrZPGpUGRQzkY=是配套的,提供基本的防护,比如恶意的连接或者无意的连接。

    其中Connection就是我们前边提到的,客户端发送给服务端的信号,服务端接受到信号之后,才会对HTTP协议进行升级。那么服务端怎样确认客户端发送过来的请求是否是合法的呢?在客户端每次发起协议升级请求的时候都会产生一个唯一码:Sec-WebSocket-Key。服务端拿到这个码后,通过一个算法进行校验,然后通过Sec-WebSocket-Accept响应给客户端,客户端再对Sec-WebSocket-Accept进行校验来完成验证。这个算法很简单:

1.将Sec-WebSocket-Key跟全局唯一的(GUID,[RFC4122])标识:258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接

2.通过SHA1计算出摘要,并转成base64字符串

258EAFA5-E914-47DA-95CA-C5AB0DC85B11这个字符串又叫“魔串",至于为什么要使用它作为Websocket握手计算中使用的字符串,这点我们无需关心,只需要知道它是RFC标准规定就可以了,官方的解析也只是简单的说此值不大可能被不明白WebSocket协议的网络终端使用。我们还是用世界上最好的语言来描述一下这个算法吧。

  1. public function dohandshake($sock, $data, $key) {
  2. if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $data, $match)) {
  3. $response = base64_encode(sha1($match[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
  4. $upgrade = "HTTP/1.1 101 Switching Protocol\r\n" .
  5. "Upgrade: websocket\r\n" .
  6. "Connection: Upgrade\r\n" .
  7. "Sec-WebSocket-Accept: " . $response . "\r\n\r\n";
  8. socket_write($sock, $upgrade, strlen($upgrade));
  9. $this->isHand[$key] = true;
  10. }
  11. }

服务端响应客户端的头部信息和HTTP协议的格式是相同的,所以这里Sec-WebSocket-Accept字段后边的两个换行符是少不了的,这和我们使用curl工具模拟get请求是一个道理。这样展示结果似乎不太直观,我们使用命令行CLI来根据上图中的Sec-WebSocket-Key和握手算法来计算一下服务端返回的Sec-WebSocket-Accept是否正确:

从图中可以看到,通过算法算出来的base64字符串和Sec-WebSocket-Accept是一样的。那么假如服务端在握手的过程中返回一个错误的Sec-WebSocket-Accept字符串会怎么样呢?当然是客户端会报错,连接会建立失败,大家最好尝试一下,例如将全局唯一标识符258EAFA5-E914-47DA-95CA-C5AB0DC85B11改为258EAFA5-E914-47DA-95CA-C5AB0DC85B12。

三、WebSocket的帧和数据分片传输

下图是我做的一个测试:将小说《飘》的第一章内容复制成文本数据,通过客户端发送到服务端,然后服务端响应相同的信息完成了一次通信。

可以看到一篇足足有将近15000字节的数据在客户端和服务端完成通信只用了150ms的时间。我们还可以清晰的看到浏览器控制台中frame栏中显示的客户端发送和服务端响应的文本数据,你一定惊讶WebSocket通信强大的数据传输能力。数据是否真的像frame中展示的那样客户端直接将一大篇文本数据发送到服务端,服务端接收到数据之后,再将一大篇文本数据返回给客户端呢?这当然是不可能的,我们都知道HTTP协议是基于TCP实现的,HTTP发送数据也是分包转发的,就是将大数据根据报文形式分割成一小块一小块发送到服务端,服务端接收到客户端发送的报文后,再将小块的数据拼接组装。关于HTTP的分包策略,大家可以查看相关资料进行研究,websocket协议也是通过分片打包数据进行转发的,不过策略上和HTTP的分包不一样。frame(帧)是websocket发送数据的基本单位,下边是它的报文格式:

报文内容中规定了数据标示,操作代码、掩码、数据、数据长度等格式。不太理解没关系,下面我通过讲解大家只要理解报文中重要标志的作用就可以了。首先我们明白了客户端和服务端进行Websocket消息传递是这样的:

  1. 客户端:将消息切割成多个帧,并发送给服务端。
  2. 服务端:接收消息帧,并将关联的帧重新组装成完整的消息。

服务端在接收到客户端发送的帧消息的时候,将这些帧进行组装,它怎么知道何时数据组装完成的呢?这就是报文中左上角FIN(占一个比特)存储的信息,1表示这是消息的最后一个分片(fragment)如果是0,表示不是消息的最后一个分片。websocket通信中,客户端发送数据分片是有序的,这一点和HTTP不一样,HTTP将消息分包之后,是并发无序的发送给服务端的,包信息在数据中的位置则在HTTP报文中存储,而websocket仅仅需要一个FIN比特位就能保证将数据完整的发送到服务端。
接下来的RSV1,RSV2,RSV3三个比特位的作用又是什么呢?这三个标志位是留给客户端开发者和服务端开发者开发过程中协商进行拓展的,默认是0。拓展如何使用必须在握手的阶段就协商好,其实握手本身也是客户端和服务端的协商。

四、Websocket连接保持和心跳检测

Websocket是长连接,为了保持客户端和服务端的实时双向通信,需要确保客户端和服务端之间的TCP通道保持连接没有断开。但是对于长时间没有数据往来的连接,如果依旧保持着,可能会浪费服务端资源。但是不排除有些场景,客户端和服务端虽然长时间没有数据往来,仍然需要保持连接,就比如说你几个月没有和一个QQ好友聊天了,突然有一天他发QQ消息告诉你他要结婚了,你还是能在第一时间收到。那是因为,客户端和服务端一直再采用心跳来检查连接。客户端和服务端的心跳连接检测就像打乒乓球一样:

  • 发送方->接收方:ping
  • 接收方->发送方:pong

等什么时候没有ping、pong了,那么连接一定是存在问题了。
说了这么多,接下来我使用Go语言来实现一个心跳检测,Websocket通信实现细节是一件繁琐的事情,直接使用开源的类库是比较不错的选择,我使用的是:gorilla/websocket。这个类库已经将websocket的实现细节(握手,数据解码)封装的很好啦。下面我就直接贴代码了:

  1. package main
  2. import (
  3. "net/http"
  4. "time"
  5. "github.com/gorilla/websocket"
  6. )
  7. var (
  8. //完成握手操作
  9. upgrade = websocket.Upgrader{
  10. //允许跨域(一般来讲,websocket都是独立部署的)
  11. CheckOrigin:func(r *http.Request) bool {
  12. return true
  13. },
  14. }
  15. )
  16. func wsHandler(w http.ResponseWriter, r *http.Request) {
  17. var (
  18. conn *websocket.Conn
  19. err error
  20. data []byte
  21. )
  22. //服务端对客户端的http请求(升级为websocket协议)进行应答,应答之后,协议升级为websocket,http建立连接时的tcp三次握手将保持。
  23. if conn, err = upgrade.Upgrade(w, r, nil); err != nil {
  24. return
  25. }
  26. //启动一个协程,每隔1s向客户端发送一次心跳消息
  27. go func() {
  28. var (
  29. err error
  30. )
  31. for {
  32. if err = conn.WriteMessage(websocket.TextMessage, []byte("heartbeat")); err != nil {
  33. return
  34. }
  35. time.Sleep(1 * time.Second)
  36. }
  37. }()
  38. //得到websocket的长链接之后,就可以对客户端传递的数据进行操作了
  39. for {
  40. //通过websocket长链接读到的数据可以是text文本数据,也可以是二进制Binary
  41. if _, data, err = conn.ReadMessage(); err != nil {
  42. goto ERR
  43. }
  44. if err = conn.WriteMessage(websocket.TextMessage, data); err != nil {
  45. goto ERR
  46. }
  47. }
  48. ERR:
  49. //出错之后,关闭socket连接
  50. conn.Close()
  51. }
  52. func main() {
  53. http.HandleFunc("/ws", wsHandler)
  54. http.ListenAndServe("0.0.0.0:7777", nil)
  55. }

借助go语言很容易搭建协程的特点,我专门开启了一个协程每秒向客户端发送一条消息。打开客户端浏览器可以看到,frame中每秒的心跳数据一直在跳动,当长链接断开之后,心跳就没有了,就像人没有了心跳一样:

webSocket原理探索的更多相关文章

  1. WebSocket原理

    一 . WebSocket原理 1.1.背景 WebSocket 是基于Http 协议的改进,Http 为无状态协议,基于短连接,需要频繁的发起请求,第二 Http 只能客户端发起请求,服务端无法主动 ...

  2. sql注入--双查询报错注入原理探索

    目录 双查询报错注入原理探索 part 1 场景复现 part 2 形成原因 part 3 报错原理 part 4 探索小结 双查询报错注入原理探索 上一篇讲了双查询报错查询注入,后又参考了一些博客, ...

  3. python全栈开发day115、116-websocket、websocket原理、websocket加解密、简单问答机器人实现

    1.websocket 1.websocket 与轮询 轮询: 不断向服务器发起询问,服务器还不断的回复 浪费带宽,浪费前后端资源 保证数据的实时性 长轮询: 1.客户端向服务器发起消息,服务端轮询, ...

  4. WebSocket原理与实践(四)--生成数据帧

    WebSocket原理与实践(四)--生成数据帧 从服务器发往客户端的数据也是同样的数据帧,但是从服务器发送到客户端的数据帧不需要掩码的.我们自己需要去生成数据帧,解析数据帧的时候我们需要分片. 消息 ...

  5. WebSocket原理与实践(三)--解析数据帧

    WebSocket原理与实践(三)--解析数据帧 1-1 理解数据帧的含义:   在WebSocket协议中,数据是通过帧序列来传输的.为了数据安全原因,客户端必须掩码(mask)它发送到服务器的所有 ...

  6. WebSocket原理与实践(二)---WebSocket协议

    WebSocket原理与实践(二)---WebSocket协议 WebSocket协议是为了解决web即时应用中服务器与客户端浏览器全双工通信问题而设计的.协议定义ws和wss协议,分别为普通请求和基 ...

  7. WebSocket原理与实践(一)---基本原理

    WebSocket原理与实践(一)---基本原理 一:为什么要使用WebSocket?1. 了解现有的HTTP的架构模式:Http是客户端/服务器模式中请求-响应所用的协议,在这种模式中,客户端(一般 ...

  8. python 全栈开发,Day139(websocket原理,flask之请求上下文)

    昨日内容回顾 flask和django对比 flask和django本质是一样的,都是web框架. 但是django自带了一些组件,flask虽然自带的组件比较少,但是它有很多的第三方插件. 那么在什 ...

  9. websocket 原理

    自己写一个websocket import socket, hashlib, base64 sock = socket.socket() sock.bind(('127.0.0.1', 9000)) ...

随机推荐

  1. BSOJ5086题解

    题意略. 我们设 \([x^k]G_n(x)\) 代表深度为 \(n\) 的树,距离为 \(k\) 的点对数量,\([x^k]F_n(x)\) 为深度为 $ n $ 的树中,深度为 \(k\) 的节点 ...

  2. LGP7884题解

    是的,这是一篇使用 min25 筛的题解... 本题解参考command_block大佬的博客,代码是对其在 LOJ 上的提交卡常后写出来的. ML 板子把数据开到 \(10^{13}\) 速度还和供 ...

  3. git同步代码到另一分支

    将dev分支的代码同步到master 方法一:用git命令 1.git checkout master 2.git merge dev 3.git push --set-upstream origin ...

  4. Go 循环语句

    Go 循环语句 一.概述 在不少实际问题中有许多具有规律性的重复操作,因此在程序中就需要重复执行某些语句. 循环程序的流程图: Go 语言提供了以下几种类型循环处理语句: 循环类型 描述 for 循环 ...

  5. CentOS 7.5 安装配置tigervnc-server

    系统版本: [root@s10 ~]# cat /etc/redhat-release CentOS Linux release 7.5.1804 (Core) 1.安装 Gnome 包 [root@ ...

  6. Spring事件监听机制源码解析

    Spring事件监听器使用 1.Spring事件监听体系包括三个组件:事件.事件监听器,事件广播器. 事件:定义事件类型和事件源,需要继承ApplicationEvent. package com.y ...

  7. @Transactional注解的失效场景

    一口气说出 6种,@Transactional注解的失效场景 计算机java编程 发布时间: 20-03-1912:35优质科技领域创作者 引言 昨天公众号粉丝咨询了一个问题,说自己之前面试被问@Tr ...

  8. VUE开发--环境配置(一)(转)

    无剑_君关注 0.312019.05.09 11:53:43字数 1,073阅读 19,627        https://www.jianshu.com/p/a494417def99?utm_so ...

  9. (转)String,StringBuilder,StringBuffer区别

    Java中的String,StringBuilder,StringBuffer三者的区别 注:转自-博客园-酥风 最近在学习Java的时候,遇到了这样一个问题,就是String,StringBuild ...

  10. 关于Oracle数据库的PIVOT分组函数的使用

    官方文档挺详细的,在新功能那里有介绍到:http://www.oracle-developer.net/display.php?id=506 PIVOT的语法:https://docs.oracle. ...