WebSocket详解:WebSocket入门、技术原理与实现介绍
一. 前言
众所周知,Web 应用的交互过程通常是客户端通过浏览器发出一个请求,服务器端接收请求后进行处理并返回结果给客户端,客户端浏览器将信息呈现,这种机制对于信息变化不是特别频繁的应用尚可,但对于实时要求高、海量并发的应用来说显得捉襟见肘,尤其在当前业界移动互联网蓬勃发展的趋势下,高并发与用户实时响应是 Web 应用经常面临的问题,比如金融证券的实时信息,Web 导航应用中的地理位置获取,社交网络的实时消息推送等。
传统的请求-响应模式的 Web 开发在处理此类业务场景时,通常采用实时通讯方案,常见的是:
-
轮询: 原理简单易懂,就是客户端通过一定的时间间隔以频繁请求的方式向服务器发送请求,来保持客户端和服务器端的数据同步。问题很明显,当客户端以固定频率向服务器端发送请求时,服务器端的数据可能并没有更新,带来很多无谓请求,浪费带宽,效率低下。
-
基于 Flash:
AdobeFlash 通过自己的 Socket 实现完成数据交换,再利用 Flash 暴露出相应的接口为 JavaScript 调用,从而达到实时传输目的。此方式比轮询要高效,且因为 Flash 安装率高,应用场景比较广泛,但在移动互联网终端上 Flash 的支持并不好。IOS 系统中没有 Flash 的存在,在 Android 中虽然有 Flash 的支持,但实际的使用效果差强人意,且对移动设备的硬件配置要求较高。2012 年 Adobe 官方宣布不再支持 Android4.1+系统,宣告了 Flash 在移动终端上的死亡。
传统 Web 模式在处理高并发及实时性需求的时候,会遇到难以逾越的瓶颈,我们需要一种高效节能的双向通信机制来保证数据的实时传输。在此背景下,基于HTML5 规范的、有 Web TCP 之称的 WebSocket 应运而生。
二. 什么是Socket?什么是WebSocket?
在网络中的两个应用程序(进程)需要全双工相互通信(全双工即双方可同时向对方发送消息),需要用到的就是socket,它能够提供端对端通信;
对于程序员来讲,他只需要在某个应用程序的一端(客户端)创建一个socket实例并且提供它所要连接一端(服务端)的IP地址和端口,而另外一端(服务端)创建另一个socket并绑定本地端口进行监听,然后客户端进行连接服务端,服务端接受连接之后双方建立了一个端对端的TCP连接,在该连接上就可以双向通讯了
而且一旦建立这个连接之后,通信双方就没有客户端或服务端之分了,提供的就是端对端通信 了。我们可以采取这种方式构建一个桌面版的IM 程序,让不同主机上的用户发送消息。从本质上来说,socket并不是一个新的协议,它只是为了便于我们进行网络编程而对TCP/IP协议族通信机制的一种封装。
websocket是HTML5规范中的一个部分,它借鉴了socket这种思想,为web应用程序客户端和服务端之间提供了一种全双工通信机制。同时,它又是一种新的应用层协议,websocket协议是为了提供web应用程序和服务端全双工通信而专门制定的一种应用层协议,通常它表示为:ws://echo.websocket.org/?encoding=text HTTP/1.1
,可以看到除了前面的协议名和http不同之外,它的表示地址就是传统的url地址。
可以看到,websocket并不是简单地将socket这一概念在浏览器环境中的移植
三. WebSocket技术出现之前,Web端实现即时通讯的方法有哪些?
✅ 1、定期轮询的方式
客户端按照某个时间间隔不断地向服务端发送请求,请求服务端的最新数据然后更新客户端显示。这种方式实际上浪费了大量流量并且对服务端造成了很大压力
✅ 2、SSE(Server-Sent Event,服务端推送事件)
SSE(Server-Sent Event,服务端推送事件)是一种允许服务端向客户端推送新数据的HTML5技术。与由客户端每隔几秒从服务端轮询拉取新数据相比,这是一种更优的解决方案。
相较于WebSocket,它也能从服务端向客户端推送数据。WebSocket能做的,SSE也能做,反之亦然,但在完成某些任务方面,它们各有千秋。
✅ 3、Comet技术
Comet并不是一种新的通信技术,它是在客户端请求服务端这个模式上的一种hack技术,通常来讲,它主要分为以下两种做法:
(1)基于长轮询的服务端推送技术 具体来讲,就是客户端首先给服务端发送一个请求,服务端收到该请求之后如果数据没有更新则并不立即返回,服务端阻塞请求的返回,直到数据发生了更新或者发生了连接超时,服务端返回数据之后客户端再次发送同样的请求,如下所示:
(2)基于流式数据传输的长连接 通常的做法是在页面中嵌入一个隐藏的iframe(框架),然后让这个iframe(框架)的src属性指向我们请求的一个服务端地址,并且为了数据更新,我们将页面上数据更新操作封装为一个js函数,将函数名当做参数传递到这个地址当中。
服务端收到请求后解析地址取出参数(客户端js函数调用名),每当有数据更新的时候,返回对客户端函数的调用,并且将要更新的数据以js函数的参数填入到返回内容当中,例如返回"<script type="text/javascript">update("data")</script>"
这样一个字串,意味着以data为参数调用客户端update函数进行客户端view更新。基本模型如下所示:
可以看到comet技术是针对客户端请求服务器响应模型而模拟出的一个服务端推送数据实时更新技术。而且由于浏览器兼容性不能够广泛应用。
四. WebSocket的通信原理和机制
websocket既然是基于浏览器端的web技术 ,那么它的通信肯定少不了http,websocket本身虽然也是一种新的应用层协议,但是它也不能够脱离http而单独存在
✅ 1、websocket和HTTP的交互区别
我们先看一下websocket和HTTP的交互区别:
🏷 非 WebSocket 模式传统 HTTP 客户端与服务器的交互如下图所示:
🏷 使用 WebSocket 模式客户端与服务器的交互如下图:
上图对比可以看出,相对于传统 HTTP 每次请求-应答都需要客户端与服务端建立连接的模式
WebSocket 是类似 Socket 的 TCP 长连接的通讯模式,一旦 WebSocket 连接建立后,后续数据都以帧序列的形式传输。
在客户端断开 WebSocket 连接或 Server 端断掉连接前,不需要客户端和服务端重新发起连接请求。使用 webSocket 浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送,改变了原有的B/S模式。
在海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显。
✅ 2、 WebSocket 通讯与传统 HTTP 报文区别
我们再通过客户端和服务端交互的报文看一下 WebSocket 通讯与传统 HTTP 的不同。
在客户端,new WebSocket 实例化一个新的 WebSocket 客户端对象,连接类似 ws://yourdomain:port/path 的服务端 WebSocket URL,WebSocket 客户端对象会自动解析并识别为 WebSocket 请求,从而连接服务端端口,执行双方握手过程。客户端发送数据格式类似于下面的内容:
可以看到,这是一个http get请求报文,注意该报文中有一个upgrade首部 ,作用是告诉服务端需要将通信协议切换到websocket,
Upgrade字段仅限HTTP/1.1版本协议,不适合HTTP/2.0版本协议.
"Sec-WebSocket-Key" 是 WebSocket 客户端发送的一个 base64 编码的密文 ,要求服务端必须返回一个对应加密的"Sec-WebSocket-Accept"应答,否则客户端会抛出 "Error during WebSocket handshake" 错误,并关闭连接。
如果服务端支持websocket协议,那么它就会将自己的通信协议切换到websocket,同时发给客户端类似于以下的一个响应报文头:
返回的状态码为101,表示同意客户端协议转换请求,并将它转换为websocket协议。
101 Switching Protocols 是HTTP协议状态码,不是websocket协议状态码
"Sec-WebSocket-Accept" 的值是服务端采用与客户端一致的密钥计算出来后返回客户端的,"HTTP/1.1 101 Switching Protocols"表示服务端接受 WebSocket 协议的客户端连接,经过这样的请求-响应处理后,客户端服务端的 WebSocket 连接握手成功, 后续就可以进行 TCP 通讯了。
常见的状态码
-
连接成功状态码
- 101:HTTP协议切换为WebSocket协议。
-
连接关闭状态码
- 1000:正常断开连接。
- 1001:服务器断开连接。
- 1002:websocket协议错误。
- 1003:客户端接受了不支持数据格式(只允许接受文本消息,不允许接受二进制数据,是客户端限制不接受二进制数据,而不是websocket协议不支持二进制数据)。
- 1006:异常关闭。
- 1007:客户端接受了无效数据格式(文本消息编码不是utf-8)。
- 1009:传输数据量过大。
- 1010:客户端终止连接。
- 1011:服务器终止连接。
- 1012:服务端正在重新启动。
- 1013:服务端临时终止。
- 1014:通过网关或代理请求服务器,服务器无法及时响应。
- 1015:TLS握手失败。
-
连接关闭状态码是WebSocket对象的onclose属性返回的
以上过程都是利用http通信完成 的,称之为websocket协议握手(websocket Protocol handshake),进过这握手之后,客户端和服务端就建立了websocket连接,以后的通信走的都是websocket协议了。
所以websocket握手需要借助于http协议,建立连接后通信过程使用websocket协议。
同时该websocket连接还是基于我们刚才发起http连接的那个TCP连接。一旦建立连接之后,我们就可以进行数据传输了,websocket提供两种数据传输:文本数据和二进制数据。
基于以上分析,我们可以看到,websocket能够提供低延迟,高性能的客户端与服务端的双向数据通信。它颠覆了之前web开发的请求处理响应模式,并且提供了一种真正意义上的客户端请求,服务器推送数据的模式,特别适合实时数据交互应用开发。
四、WebSocket 实现
spring boot 集成 websocket 的四种方式
- 原生注解
- Spring封装
- TIO
- STOMP
每种方式都各自的特点,这里我主要介绍的是原生注解的方式实现:
1、pom.xml
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2、WebSocketConfig
这个配置类很简单,通过这个配置 spring boot 才能去扫描后面的关于 websocket 的注解
less
@Configuration
@EnableWebSocket
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpoint() {
return new ServerEndpointExporter();
}
}
3、WsServerEndpoint
typescript
@ServerEndpoint("/myWs")
@Component
public class WsServerEndpoint {
/**
* 连接成功
*
* @param session
*/
@OnOpen
public void onOpen(Session session) {
System.out.println("连接成功");
}
/**
* 连接关闭
*
* @param session
*/
@OnClose
public void onClose(Session session) {
System.out.println("连接关闭");
}
/**
* 接收到消息
*
* @param text
*/
@OnMessage
public String onMsg(String text) throws IOException {
return "servet 发送:" + text;
}
}
4、说明
这里有几个注解需要注意一下,首先是他们的包都在 javax.websocket 下。并不是 spring 提供的,而 是jdk 自带的,下面是他们的具体作用。
- @ServerEndpoint
- 通过这个 spring boot 就可以知道你暴露出去的 ws 应用的路径,有点类似我们经常用的@RequestMapping。比如你的启动端口是 8080,而这个注解的值是 ws,那我们就可以通过 ws://127.0.0.1:8080/ws 来连接你的应用
- @OnOpen
- 当 websocket 建立连接成功后会触发这个注解修饰的方法,注意它有一个 Session 参数
- @OnClose
- 当 websocket 建立的连接断开后会触发这个注解修饰的方法,注意它有一个 Session 参数
- @OnMessage
- 当客户端发送消息到服务端时,会触发这个注解修改的方法,它有一个 String 入参表明客户端传入的值
- @OnError
- 当 websocket 建立连接时出现异常会触发这个注解修饰的方法,注意它有一个 Session 参数
另外一点就是服务端如何发送消息给客户端,服务端发送消息必须通过上面说的 Session 类,
通常是在@OnOpen
方法中,当连接成功后把session
存入 Map 的 value,key 是与 session 对应的用户标识,当要发送的时候通过 key 获得 session 再发送,这里可以通过 session.getBasicRemote().sendText("内容")
来对客户端发送消息。
五、结语
从上面的即例子我们可以看到,要想做一个点对点的IM应用,websocket采取的方式是让所有客户端连接服务端,服务器将不同客户端发送给自己的消息进行转发或者广播,而对于原始的socket,只要两端建立连接之后,就可以发送端对端的数据,不需要经过第三方的转发,这也是websocket不同于socket的一个重要特点。