spring boot Websocket(使用笔记)

本文只作为个人笔记,大部分代码是引用其他人的文章的。

在 springboot 项目中使用 websocket 做推送,虽然挺简单的,但初学也踩过几个坑,特此记录。


  使用 websocket 有两种方式:1 是使用 sockjs,2 是使用 h5 的标准。使用 Html5 标准自然更方便简单,所以记录的是配合 h5 的使用方法。

1、pom

  核心是 @ServerEndpoint 这个注解。这个注解是 Javaee 标准里的注解,tomcat7 以上已经对其进行了实现,如果是用传统方法使用 tomcat 发布项目,只要在 pom 文件中引入 javaee 标准即可使用。

    <dependency>
      <groupId>javax</groupId>
      <artifactId>javaee-api</artifactId>
      <version>7.0</version>
      <scope>provided</scope>
    </dependency>

  但使用 springboot 的内置 tomcat 时,就不需要引入 javaee-api 了,spring-boot 已经包含了。使用 springboot 的 websocket 功能首先引入 springboot 组件。

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

  顺便说一句,springboot 的高级组件会自动引用基础的组件,像 spring-boot-starter-websocket 就引入了 spring-boot-starter-web 和 spring-boot-starter,所以不要重复引入。

2、使用 @ServerEndpoint 创立 websocket endpoint

  首先要注入 ServerEndpointExporter,这个 bean 会自动注册使用了 @ServerEndpoint 注解声明的 Websocket endpoint。要注意,如果使用独立的 servlet 容器,而不是直接使用 springboot 的内置容器,就不要注入 ServerEndpointExporter,因为它将由容器自己提供和管理。

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();}

}

  接下来就是写 websocket 的具体实现类,很简单,直接上代码:

@ServerEndpoint(value = "/websocket")
@Component
public class MyWebSocket {
    //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static int onlineCount = 0;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。</span>
<span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> CopyOnWriteArraySet&lt;MyWebSocket&gt; webSocketSet = <span style="color: rgba(0, 0, 255, 1)">new</span> CopyOnWriteArraySet&lt;MyWebSocket&gt;<span style="color: rgba(0, 0, 0, 1)">();

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">与某个客户端的连接会话,需要通过它来给客户端发送数据</span>
<span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> Session session;

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 连接建立成功调用的方法</span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
@OnOpen
</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> onOpen(Session session) {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.session =<span style="color: rgba(0, 0, 0, 1)"> session;
    webSocketSet.add(</span><span style="color: rgba(0, 0, 255, 1)">this</span>);     <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">加入set中</span>
    addOnlineCount();           <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">在线数加1</span>
    System.out.println("有新连接加入!当前在线人数为" +<span style="color: rgba(0, 0, 0, 1)"> getOnlineCount());
    </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> {
        sendMessage(CommonConstant.CURRENT_WANGING_NUMBER.toString());
    } </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (IOException e) {
        System.out.println(</span>"IO异常"<span style="color: rgba(0, 0, 0, 1)">);
    }
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 连接关闭调用的方法
 </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
@OnClose
</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> onClose() {
    webSocketSet.remove(</span><span style="color: rgba(0, 0, 255, 1)">this</span>);  <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">从set中删除</span>
    subOnlineCount();           <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">在线数减1</span>
    System.out.println("有一连接关闭!当前在线人数为" +<span style="color: rgba(0, 0, 0, 1)"> getOnlineCount());
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 收到客户端消息后调用的方法
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> message 客户端发送过来的消息</span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
@OnMessage
</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> onMessage(String message, Session session) {
    System.out.println(</span>"来自客户端的消息:" +<span style="color: rgba(0, 0, 0, 1)"> message);

    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">群发消息</span>
    <span style="color: rgba(0, 0, 255, 1)">for</span><span style="color: rgba(0, 0, 0, 1)"> (MyWebSocket item : webSocketSet) {
        </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> {
            item.sendMessage(message);
        } </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (IOException e) {
            e.printStackTrace();
        }
    }
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 发生错误时调用</span><span style="color: rgba(0, 0, 0, 1)">
@OnError
</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> onError(Session session, Throwable error) {
    System.out.println(</span>"发生错误"<span style="color: rgba(0, 0, 0, 1)">);
    error.printStackTrace();
}
public void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); //this.session.getAsyncRemote().sendText(message); }
</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 群发自定义消息
 * </span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">void</span> sendInfo(String message) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> IOException {
    </span><span style="color: rgba(0, 0, 255, 1)">for</span><span style="color: rgba(0, 0, 0, 1)"> (MyWebSocket item : webSocketSet) {
        </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> {
            item.sendMessage(message);
        } </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (IOException e) {
            </span><span style="color: rgba(0, 0, 255, 1)">continue</span><span style="color: rgba(0, 0, 0, 1)">;
        }
    }
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">synchronized</span> <span style="color: rgba(0, 0, 255, 1)">int</span><span style="color: rgba(0, 0, 0, 1)"> getOnlineCount() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> onlineCount;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">synchronized</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> addOnlineCount() {
    MyWebSocket.onlineCount</span>++<span style="color: rgba(0, 0, 0, 1)">;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">synchronized</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> subOnlineCount() {
    MyWebSocket.onlineCount</span>--<span style="color: rgba(0, 0, 0, 1)">;
}

}

使用 springboot 的唯一区别是要 @Component 声明下,而使用独立容器是由容器自己管理 websocket 的,但在 springboot 中连容器都是 spring 管理的。

虽然 @Component 默认是单例模式的,但 springboot 还是会为每个 websocket 连接初始化一个 bean,所以可以用一个静态 set 保存起来。

3、前端代码

   

<!DOCTYPE HTML>
<html>
<head>
    <title>My WebSocket</title>
</head>

<body>
Welcome
<br/>
<input id="text" type="text" /><button onclick="send()">Send</button> <button onclick="closeWebSocket()">Close</button>
<div id="message">
</div>
</body>

<script type="text/javascript">
var websocket = null;

</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">判断当前浏览器是否支持WebSocket</span>
<span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">if</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">(</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">'</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">WebSocket</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">'</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">in</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> window){
    websocket </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">=</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">new</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> WebSocket(</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">"</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">ws://localhost:8084/websocket</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">"</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">);
}
</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">else</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">{
    alert(</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">'</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">Not support websocket</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">'</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">)
}

</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">连接发生错误的回调方法</span>

websocket.onerror = function(){
setMessageInnerHTML(
"error");
};

</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">连接成功建立的回调方法</span>

websocket.onopen = function(event){
setMessageInnerHTML(
"open");
}

</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">接收到消息的回调方法</span>

websocket.onmessage = function(event){
setMessageInnerHTML(event.data);
}

</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">连接关闭的回调方法</span>

websocket.onclose = function(){
setMessageInnerHTML(
"close");
}

</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。</span>

window.onbeforeunload = function(){
websocket.close();
}

</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">将消息显示在网页上</span>
<span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">function</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> setMessageInnerHTML(innerHTML){
    document.getElementById(</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">'</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">message</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">'</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">).innerHTML </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">+=</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> innerHTML </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">+</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">'</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">&lt;br/&gt;</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">'</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">;
}

</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">关闭连接</span>
<span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">function</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> closeWebSocket(){
    websocket.close();
}

</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">发送消息</span>
<span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">function</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> send(){
    </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">var</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> message </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">=</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> document.getElementById(</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">'</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">text</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">'</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">).value;
    websocket.send(message);
}

</script>
</html>

 

  4、总结

  springboot 已经做了深度的集成和优化,要注意是否添加了不需要的依赖、配置或声明。由于很多讲解组件使用的文章是和 spring 集成的,会有一些配置,在使用 springboot 时,由于 springboot 已经有了自己的配置,再这些配置有可能导致各种各样的异常。