SpringBoot + WebSocket


在HTTP协议中,所有的请求都是由客户端发起、服务端响应,服务端无法向客户端推送消息,但是在一些需要即时通信的应用中(比如本次的web聊天室),不可避免的需要服务端向客户端推送消息。

传统的解决方案:

  • 轮询:客户端固定的时间间隔下不停地向服务端发送请求,查看服务端是否有最新的数据。如果没有,返回一个空的JSON或者XML文档。客户端每次都要新建HTTP请求,极大的浪费服务端资源。
  • 长轮询:服务端不会每次都立即响应客户端请求,只有在有最新数据时才会立即响应客户端请求。 弊端:TCP和HTTP都有连接超时,所谓的长轮询不能一直持续,需要定期的连接和关闭再连接。
  • Applet和Flash:已成为历史…

WebSocket 协议

WebSocket是一种在单个TCP连接上进行全双工通信的协议,已被W3C定为标准。WebSocket 允许服务端主动向客户端推送数据,双方只需要完成一次握手就可以直接创建持久性的连接,并进行双向数据传输。

一个WebSocket请求首先使用非正常的HTTP请求以特定的模式访问一个URL(两种模式:ws或wss,对应HTTP和HTTPS)。

请求头中有一个Connection:Upgrade字段,表示客户端想要对协议进行升级。还有一个Upgrade:websocket字段,表示客户端想要将请求协议升级为WebSocket协议。

这两个字段共同告诉服务器要将连接升级为WebSocket,如果服务端同意,那么握手完成之后,文本消息或其他二进制消息就可以同时在两个方向上进行发送。此时客户端与服务端关系是对等的。

特点:

  • 有状态,使用时需要先创建连接。
  • 与HTTP使用相同的端口 80(ws)或者443(wss),基本不会被防火墙阻止
  • 使用HTTP协议进行握手,自然而然的集成到浏览器和HTTP服务器,不需要额外的成本
  • 心跳信息(ping、pong)将被反复发送,以保持WebSocket连接一直处于活跃状态
  • 当信息启动或者到达时,服务端和客户端都可以知道
  • 关闭时将发送一个特殊的关闭消息
  • 支持跨域,可以避免Ajax的限制
  • HTTP规范要求浏览器将并发连接数限制为每个主机名两个连接,WebSocket握手完成后不存在此限制
  • 支持扩展,用户可以扩展协议,实现部分自定义的子协议
  • 更好的二进制支持、更好的压缩效果

应用场景:

  • 在线股票网站

  • 即时聊天

  • 多人在线游戏

  • 应用集群通信

SpringBoot + WebSocket

用SpringBoot + WebSocket实现一个简单的Web群聊。

去GitHub查看

1.创建项目、添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<!-- 使用jar包的形式对所需要的前端库统一管理  -->
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>webjars-locator-core</artifactId>
</dependency>

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>sockjs-client</artifactId>
    <version>1.1.2</version>
</dependency>
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>stomp-websocket</artifactId>
    <version>2.3.3</version>
</dependency>
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.3.1</version>
</dependency>

2.WebSocketConfig

STOMP是一个简单的可互操作的协议,通常被用于通过中间服务器在客户端之间进行异步消息传递。

Spring框架提供了基于WebSocket的STOMP支持。

/*
 * WebSocket配置类
 * spring提供了基于websocket的stomp支持
 * stomp是一个简单的可互操作的协议,通常被用于通过中间服务器在客户端之间进行异步消息传递。
 */

@Configuration

// 此注解用于开启WebSocket消息代理
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer{

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 设置消息代理的前缀。即如果消息的前缀是“/topic”就会将消息转发给消息代理(broker)
        // 再由消息代理将消息广播给当前连接的客户端。
        config.enableSimpleBroker("/topic");

        // 配置一个或多个前缀,通过前缀过滤出需要被注解方法处理的消息。其他的将被直接交给broker处理。
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 定义一个前缀为“/chat”的endPoint,并开启sockjs支持。
        // sockjs可以解决浏览器对websocket的兼容性问题,客户端将通过这里配置的URL来建立websocket连接
        registry.addEndpoint("/chat").withSockJS();
    }
}

3.Controller

定义一个Controller来实现对消息的处理

@Controller
public class GreetingController {

    // 注解的方法将用来接收“/app/hello”路径发送来的消息,处理后将消息转发到@SendTo定义的路径上。
    @MessageMapping("/hello")

    // 将处理过的消息交给消息代理broker,再由broker进行广播
    @SendTo("/topic/greetings")
    public Message greeting(Message message) throws Exception {
        return message;
    }
}

4.聊天页面

在 resources/static下创建 chat.html页面作为聊天页面

<!DOCTYPE html>
<html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>群聊</title>
        <script src="/webjars/jquery/jquery.min.js"></script>
        <script src="/webjars/sockjs-client/sockjs.min.js"></script>
        <script src="/webjars/stomp-websocket/stomp.min.js"></script>
        <script src="/app.js"></script>
    </head>

    <body>
        <div>
            <label for="name">请输入用户名</label>
            <input type="text" id="name" placeholder="用户名">
        </div>
        <div>
            <button id="connect" type="button">连接</button>
            <button id="disconnect" type="button" disabled="disabled">断开连接</button>
        </div>
        <div id="chat" style="display:none;">
            <div>
                <label for="name">请输入聊天内容</label>
                <input type="text" id="content" placeholder="聊天内容">
            </div>
            <button id="send" type="button">发送</button>
            <div id="greetings">
                <div id="conversation" style="display: none">群聊进行中...</div>
            </div>
        </div>
    </body>
</html>

自定义的 app.js

var stompClient = null;
function setConnected(connected) {
    $("#connect").prop("disabled", connected);
    $("#disconnect").prop("disabled", !connected);
    if (connected) {
        $("#conversation").show();
        $("#chat").show();
    }
    else {
        $("#conversation").hide();
        $("#chat").hide();
    }
    $("#greetings").html("");
}

// 建立一个websocket连接,用户必须先输入用户名才能建立
function connect() {
    if (!$("#name").val()) {
        return;
    }

    // 首先使用sockJS建立连接,然后创建一个STOMP实例发起连接请求
    // 在连接成功的回调方法中,首先调用setConnected(true)方法进行页面的设置
    var socket = new SockJS('/chat');
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function (frame) {
        setConnected(true);

        // 然后调用STOMP中的subscribe方法订阅服务端发送回来的消息
        // 并将服务端发送来的消息展示出来(使用showGreeting方法)
        stompClient.subscribe('/topic/greetings', function (greeting) {
            showGreeting(JSON.parse(greeting.body));
        });
    });
}

function disconnect() {
    if (stompClient != null) {

        // 调用STOMP中的disconnect方法断开一个websocket连接
        stompClient.disconnect();
    }
    setConnected(false);
}

function sendName() {
    stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val(), 'content':$("#content").val()}));
}

function showGreeting(message) {
    $("#greetings").append("<div>" +message.name+ ":" + message.content + "</div>");
}

$(function () {
    $("#connect" ).click(function() { connect(); });
    $("#disconnect" ).click(function() { disconnect(); });
    $("#send" ).click(function() { sendName(); });

});

5.运行

用两个浏览器或者多用户,运行结果如下:

输入自定义的用户名,点击连接,进入聊天室。

实现两个或两个以上用户进行聊天,聊天内容房间内所有人都可以看到,实现一个简单的Web群聊。


  TOC