什么是 WebSocket

WebSocket 是 HTML5 开始提供的一个在 TCP 连接上进行的一个全双工通讯的协议。WebSocket 通信协议于 2011 年被 IETF 定为标准 RFC 6455,并由 RFC7936 补充规范。WebSocket API 也被 W3C 定为标准。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

eating.jpg

比起 ajax 的轮询,更加节省了许多资源。

如何通信

要直接建立 WebSocket 肯定是不行的!它首先使用 http 协议来进行握手,连接成功之后就直接使用 WebSocket 协议进行连接了。

连接的过程如下所示

109cfd4c35de32462512724d306d04.jpg

由上图 WebSocket 分为两部分,握手和数据传输。

客户端握手时携带着类似的如下信息

GET ws://localhost/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGh1IHNhbXBSzSBub25jzQ==
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-version: 13

服务端握手时携带着类似的如下信息

HTTP/1.1 101 switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-Websocket-Extensions : permessage-def1ate

字段说明

头名称 说明
Connection: Upgrade 标识该 HTTP 请求是一个协议升级请求
Upgrade: websocket 协议升级 WebSocket 协议
Sec-WebSocket-Key: 客户端采用 base64 编码的 24 位随机字符序列,服务器接受客户端 HTTP 协议升级的证明。要求服务端响应一个对应加密的 Sec-Websocket-Accept 头信息作为应答
Sec-WebSocket-Extensions: 协议扩张类型
Sec-WebSocket-version: 13 客户端支持 WebSocket 的版本

实现

客户端(WebSocket)实现

实现 WebSockets 的 web 浏览器将通过 WebSocket 对象公开所有必需的客户端功能(主要指支持 Html5 的浏览器)。

创建对象的代码如下:

var ws = new WebSocket(url);

WebSocket 事件触发

它的对象中有如下事件。

事件 事件处理程序 描述
open ws.onopen 连接时建立触发
message ws.onmessage 客户端接收服务端数据时触发
error ws.onerror 通信发生错误时触发
close ws.close 连接关闭时触发

WebSocket 方法

它的对象中有如下事件。

方法 描述
send() 使用连接发送数据

服务端实现

服务端实现有很多种,有 Java、Python、Go 等等多种后端实现方式。这里使用的是 Java 的实现方式,Tomcat 是用 Java 进行编写的。在 Tomcat 7.0.5 开始,就开始支持 WebSocket ,并且实现了 Java WebSocket 的规范(JSR356)。

Java WebSocket 应用由一系列的 WebSocketEndpoint 组成。Endpoint 是一个 java 对象,代表 WebSocket 链接的一端,对于服务端,我们可以视为处理具体 WebSocket 消息的接口,就像 Servlet 之与 http 请求一样。

我们可以通过两种方式定义 Endpoint:

  • 编程式:继承自 javax.websocket.Endpoint 并实现其方法。
  • 注解式:即定义一个 POJO(Plain Ordinary Java Object,普通 Java 对象),并添加 @ServerEndpoint 相关注解。

EndPoint 对象是在 WebSocket 握手时创建,并在客户端与服务端的链接过程中有效,在 Endpoint 接口中定义了与其生命周期相关的方法,确保在各个阶段调用实例的相关方法,其生命周期的相关方法如下。

方法 描述 注解
onClose 当会话关闭时调用。 @OnClose
onOpen 当开启一个新的会话时调用,该方法是客户端与服务端握手成功之后调用的方法。 @OnOpen
onError 当连接的过程中出现异常时调用。 @OnError

如何接收来自客户端的数据

通过为 Session 添加 MessageHandler 消息处理器来接收消息,当采用注解方式定义 Endpoint 时,我们还可以通过 @OnMessage 注解指定接收消息的方法。

如何主动推送数据给客户端

发送消息则由 RemoteEndpoint 完成,其实例由 Session 维护。

Session.getBasicRemote 获取同步消息发送的实例,然后调用其 sendXxx() 方法就可以发送消息。

通过 Session.getAsyncRemote 获取异步消息发送实例。

小试一下

接下来,利用 Spring Boot 做一个简单的 WebSocket 的程序。就是实时展示服务器的时间,每隔 500 毫秒发送时间。不过,如果你是 Spring Boot 的项目,先引入如下三个依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 这个是 springboot 的 websocket 的依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 导入模板引擎的 starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

导入完成之后,先编写前端页面的代码。

前端部分

前端界面的 html 代码是非常简单的。其中给 p 标签对设置了一个 id ,这个 id 是用于显示消息的。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>websocket例子</title>
</head>
<body>
<p>使用websocket实时展示服务端的时间</p>
<p id="realTime"></p>
<button onclick="ws.close()">断开连接</button>
</body>
<script>
    // websocket 的相关代码
</script>
</html>

然后通过 JavaScript 创建一个 WebSocket 对象,创建对象非常简单。 其中需要携带一个请求路径,此时的请求的协议是 ws 而不是 http

var ws = new WebSocket("ws://localhost:8080/showTime");

然后这个对象中有监听事件嘛,调用相关的监听方法即可。本示例中设置了四个监听事件,分别为 onopenonmessageonerroronclose 四个监听事件,目前这里也就用到了 3 个。

// 这是建立连接之后触发的事件
ws.onopen = function () {
    document.getElementById("realTime").innerText = "连接成功,正在获取时间......"
};
// 收到服务端发送过来的消息所触发的事件
ws.onmessage = function (e) {
    // console.log(e.data);
    document.getElementById("realTime").innerText = "现在的时间是:" + e.data; // 将时间填充到 p 标签对中
};
// 服务端出现异常时建立的事件
ws.onerror = function () {

};
// 关闭连接时所触发的事件
ws.onclose = function () {
    document.getElementById("realTime").innerText = "已经和服务器断开连接";
}

至此,前端部分的代码已经完成了。接下来写后端的代码。

后端部分

先新建一个控制类,用于访问我们写的 index.html 页面。

@Controller
public class IndexController {
    @GetMapping("/index")
    public String index() {
        return "index";
    }
}

然后新建一个 EndPoint 类,用于接收来自客户端发过来的数据。我们通过 ServerEndpoint 注解类来标记它,该请求是基于 WebSocket 协议,发送过来的消息都由它来接收。由于这是 Spring Boot 的项目,所以需要标记 Component 注解将它注册到容器中。

这里设置了一个定时任务,使用 Java 自带的 Timer 定时器类,每隔 500ms 将格式化的时间发送给浏览器。

@Component
@ServerEndpoint("/showTime")
public class ShowTimeEndPoint {

    // 创建一个定时任务
    private Timer timer = new Timer();

    // 连接建立时被调用
    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                try {
                    String dateTime = sdf.format(new Date());
                    session.getBasicRemote().sendText(dateTime);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }, 3000, 500);
    }

    // 接收到客户端发送的数据时被调用
    @OnMessage
    public void onMessage(String message, Session session) {

    }

    // 连接关闭时调用该方法
    @OnClose
    public void onClose(Session session) {
        timer.cancel(); // 关闭计时器
    }

}

但是这样子还是不够,需要注册一个 ServerEndpointExporter 类到容器中,这样做的目的就在于,让它去自动识别那些带 ServerEndpoint 注解的组件,使其真正的生效起来。

@Configuration
public class WebsocketConfig {
    // 注册这个组件的目的是,让它去自动识别那些带 ServerEndpoint 注解的组件
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

这样,后端部分完成了。

运行测试

运行 Spring Boot 程序,访问 http://localhost:8080/ 页面,运行的结果如下。

lj2dk-2ov9w.gif

用户在刷新页面的时候,创建 WebSocket 对象的时候就已经触发了 onopen 方法,表示我已经连接了。之后有一个三秒的等待,也就是后端的定时器部分。三秒之后,后端就开始发送消息到浏览器,每隔 500 ms 发送一次消息。

就有实时显示的效果了。

最后点击断开连接是调用了 close 方法,这样就和服务器断开了连接,时间也就不再显示出来了。