当前位置:   article > 正文

WebSocket_textwebsockethandler afterconnectionclosed 回调没执行

textwebsockethandler afterconnectionclosed 回调没执行

WebSocket的故事(五)—— Springboot中,实现网页聊天室之自定义消息代理

概述

WebSocket的故事系列计划分五大篇六章,旨在由浅入深的介绍WebSocket以及在Springboot中如何快速构建和使用WebSocket提供的能力。本系列计划包含如下几篇文章:

第一篇,什么是WebSocket以及它的用途
第二篇,Spring中如何利用STOMP快速构建WebSocket广播式消息模式
第三篇,Springboot中,如何利用WebSocket和STOMP快速构建点对点的消息模式(1)
第四篇,Springboot中,如何利用WebSocket和STOMP快速构建点对点的消息模式(2)
第五篇,Springboot中,实现网页聊天室之自定义WebSocket消息代理
第六篇,Springboot中,实现更灵活的WebSocket

本篇的主线

本篇将通过一个接近真实的网页聊天室Demo,来详细讲述如何利用WebSocket来实现一些具体的产品功能。本篇将只采用WebSocket本身,不再使用STOMP等这些封装。亲自动手实现消息的接收、处理、发送以及WebSocket的会话管理。这也是本系列的最重要的一篇,不管你们激不激动,反正我是激动了。下面我们就开始。

本篇适合的读者

想了解如何在Springboot上自定义实现更为复杂的WebSocket产品逻辑的同学以及各路有志青年。

小小网页聊天室的需求

为了能够目标明确的表达本文中所要讲述的技术要点,我设计了一个小小聊天室产品,先列出需求,这样大家在看后面的实现时能够知其所以然。

以上就是我们本篇要实现的需求。简单说,就是:

用户可加入,退出某房间,加入后可向房间内所有人发送消息,也可向某个人发送悄悄话消息

需求分析和设计

设计用户存储

很容易想到我们设计的主体就是用户、会话和房间,那么在用户管理上,我们就可以用下面这个图来表示他们之间的关系:

这样我们就可以用一个简单的Map来存储房间<->用户组这样的映射关系,在用户组内我们再使用一个Map来存储用户名<->会话Session这样的映射关系(假设没有重名)。这样,我们就解决了房间和用户组、用户和会话,这些关系的存储和维护。

设计用户行为与用户的关系

有兄弟看到这说了,“你讲这么半天了,跟之前几篇讲的什么STOMP,什么消息代理,有毛线的关系?”大兄弟你先消消气,我们学STOMP,学消息代理,学点对点消息,重要的是学思想,你说对不?下面我们就用上了。

当用户加入到某房间之后,房间里有任何风吹草动,即有人加入、退出或者发公屏消息,都会“通知”给该用户。到此,我们就可以将创建房间理解成“创建消息代理”,将用户加入房间,看成是对房间这个“消息代理”的一个“订阅”,将用户退出房间,看成是对房间这个“消息代理”的一个“解除订阅”。

那么,第一个加入房间的人,我们定义为“创建房间”,即创建了一个消息代理。为了好理解,上图:

其中红色的小人表示第一个加入房间的用户,即创建房间的人。当某用户发送消息时,如果选择将消息发送给聊天室的所有人,即相当于在房间里发送了一个广播,所有订阅这个房间的用户,都会收到这个广播消息;如果选择发送悄悄话,则只将消息发送给特定用户名的用户,即点对点消息。

总结一下我们要实现的要点:

  • 用户存储,即用户,房间,会话之间的关系和对象访问方式。
  • 动态创建消息代理(房间),并实现用户对房间的绑定(订阅)。
  • 单独发送给某个用户消息的能力。

大体设计就到此为止,还有一些细节,我们先来看一下演示效果,再来看通过代码来讲解实现。

聊天室效果展示

用浏览器打开客户端页面后,展示输入框和加入按钮。输入房间号1和用户名小铭点击进入房间

进入房间成功后,展示当前房间人数和欢迎语

当有其他人加入或退出房间时,展示通知信息。可以发送公屏消息和私聊消息。

下面就让我们看一下这些主要功能如何来实现吧。

代码实现

按照我们上述的设计,我会着重介绍重点部分的代码设计和技术要点。

服务端实现

1. 配置WebSocket
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new MyHandler(), "/webSocket/{INFO}").setAllowedOrigins("*")
                .addInterceptors(new WebSocketInterceptor());
    }
}
复制代码
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

要点解析:

  • 注册WebSocketHandlerMyHandler),这是用来处理WebSocket建立以及消息处理的类,后面会详细介绍。
  • 注册WebSocketInterceptor拦截器,此拦截器用来在客户端向服务端发起初次连接时,记录客户端拦截信息。
  • 注册WebSocket地址,并附带了{INFO}参数,用来注册的时候携带用户信息。

以上都会在后续代码中详细介绍。

2. 实现握手拦截器
public class WebSocketInterceptor implements HandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
        if (serverHttpRequest instanceof ServletServerHttpRequest) {
            String INFO = serverHttpRequest.getURI().getPath().split("INFO=")[1];
            if (INFO != null && INFO.length() > 0) {
                JSONObject jsonObject = new JSONObject(INFO);
                String command = jsonObject.getString("command");
                if (command != null && MessageKey.ENTER_COMMAND.equals(command)) {
                    System.out.println("当前session的ID="+ jsonObject.getString("name"));
                    ServletServerHttpRequest request = (ServletServerHttpRequest) serverHttpRequest;
                    HttpSession session = request.getServletRequest().getSession();
                    map.put(MessageKey.KEY_WEBSOCKET_USERNAME, jsonObject.getString("name"));
                    map.put(MessageKey.KEY_ROOM_ID, jsonObject.getString("roomId"));
                }
            }
        }
        return true;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

复制代码

要点解析:

  • HandshakeInterceptor用来拦截客户端第一次连接服务端时的请求,即客户端连接/webSocket/{INFO}时,我们可以获取到对应INFO的信息。
  • 实现beforeHandshake方法,进行用户信息保存,这里我们将用户名和房间号保存到Session上。
3. 实现消息处理器WebSocketHandler
public class MyHandler implements WebSocketHandler {
<span class="hljs-comment">//用来保存用户、房间、会话三者。使用双层Map实现对应关系。</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> Map&lt;String, Map&lt;String, WebSocketSession&gt;&gt; sUserMap = <span class="hljs-keyword">new</span> HashMap&lt;&gt;(<span class="hljs-number">3</span>);

<span class="hljs-comment">//用户加入房间后,会调用此方法,我们在这个节点,向其他用户发送有用户加入的通知消息。</span>
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">afterConnectionEstablished</span><span class="hljs-params">(WebSocketSession session)</span> <span class="hljs-keyword">throws</span> Exception </span>{
    System.out.println(<span class="hljs-string">"成功建立连接"</span>);
    String INFO = session.getUri().getPath().split(<span class="hljs-string">"INFO="</span>)[<span class="hljs-number">1</span>];
    System.out.println(INFO);
    <span class="hljs-keyword">if</span> (INFO != <span class="hljs-keyword">null</span> &amp;&amp; INFO.length() &gt; <span class="hljs-number">0</span>) {
        JSONObject jsonObject = <span class="hljs-keyword">new</span> JSONObject(INFO);
        String command = jsonObject.getString(<span class="hljs-string">"command"</span>);
        String roomId = jsonObject.getString(<span class="hljs-string">"roomId"</span>);
        <span class="hljs-keyword">if</span> (command != <span class="hljs-keyword">null</span> &amp;&amp; MessageKey.ENTER_COMMAND.equals(command)) {
            Map&lt;String, WebSocketSession&gt; mapSession = sUserMap.get(roomId);
            <span class="hljs-keyword">if</span> (mapSession == <span class="hljs-keyword">null</span>) {
                mapSession = <span class="hljs-keyword">new</span> HashMap&lt;&gt;(<span class="hljs-number">3</span>);
                sUserMap.put(roomId, mapSession);
            }
            mapSession.put(jsonObject.getString(<span class="hljs-string">"name"</span>), session);
            session.sendMessage(<span class="hljs-keyword">new</span> TextMessage(<span class="hljs-string">"当前房间在线人数"</span> + mapSession.size() + <span class="hljs-string">"人"</span>));
            System.out.println(session);
        }
    }
    System.out.println(<span class="hljs-string">"当前在线人数:"</span> + sUserMap.size());
}

<span class="hljs-comment">//消息处理方法</span>
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">handleMessage</span><span class="hljs-params">(WebSocketSession webSocketSession, WebSocketMessage&lt;?&gt; webSocketMessage)</span> </span>{
    <span class="hljs-keyword">try</span> {
        JSONObject jsonobject = <span class="hljs-keyword">new</span> JSONObject(webSocketMessage.getPayload().toString());
        Message message = <span class="hljs-keyword">new</span> Message(jsonobject.toString());
        System.out.println(jsonobject.toString());
        System.out.println(message + <span class="hljs-string">":来自"</span> + webSocketSession.getAttributes().get(MessageKey.KEY_WEBSOCKET_USERNAME) + <span class="hljs-string">"的消息"</span>);
        <span class="hljs-keyword">if</span> (message.getName() != <span class="hljs-keyword">null</span> &amp;&amp; message.getCommand() != <span class="hljs-keyword">null</span>) {
            <span class="hljs-keyword">switch</span> (message.getCommand()) {
                    <span class="hljs-comment">//有新人加入房间信息</span>
                <span class="hljs-keyword">case</span> MessageKey.ENTER_COMMAND:
                    sendMessageToRoomUsers(message.getRoomId(), <span class="hljs-keyword">new</span> TextMessage(<span class="hljs-string">"【"</span> + getNameFromSession(webSocketSession) + <span class="hljs-string">"】加入了房间,欢迎!"</span>));
                    <span class="hljs-keyword">break</span>;
                    <span class="hljs-comment">//聊天信息</span>
                <span class="hljs-keyword">case</span> MessageKey.MESSAGE_COMMAND:
                    <span class="hljs-keyword">if</span> (message.getName().equals(<span class="hljs-string">"all"</span>)) {
                        sendMessageToRoomUsers(message.getRoomId(), <span class="hljs-keyword">new</span> TextMessage(getNameFromSession(webSocketSession) +
                                <span class="hljs-string">"说:"</span> + message.getInfo()
                        ));
                    } <span class="hljs-keyword">else</span> {
                        sendMessageToUser(message.getRoomId(), message.getName(), <span class="hljs-keyword">new</span> TextMessage(getNameFromSession(webSocketSession) +
                                <span class="hljs-string">"悄悄对你说:"</span> + message.getInfo()));
                    }
                    <span class="hljs-keyword">break</span>;
                    <span class="hljs-comment">//有人离开房间信息</span>
                <span class="hljs-keyword">case</span> MessageKey.LEAVE_COMMAND:
                    sendMessageToRoomUsers(message.getRoomId(), <span class="hljs-keyword">new</span> TextMessage(<span class="hljs-string">"【"</span> + getNameFromSession(webSocketSession) + <span class="hljs-string">"】离开了房间,欢迎下次再来"</span>));
                    <span class="hljs-keyword">break</span>;
                    <span class="hljs-keyword">default</span>:
                        <span class="hljs-keyword">break</span>;
            }
        }
    } <span class="hljs-keyword">catch</span> (Exception e) {
        e.printStackTrace();
    }
}

<span class="hljs-comment">/**
 * 发送信息给指定用户
 */</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">sendMessageToUser</span><span class="hljs-params">(String roomId, String name, TextMessage message)</span> </span>{
    <span class="hljs-keyword">if</span> (roomId == <span class="hljs-keyword">null</span> || name == <span class="hljs-keyword">null</span>) <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    <span class="hljs-keyword">if</span> (sUserMap.get(roomId) == <span class="hljs-keyword">null</span>) <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    WebSocketSession session = sUserMap.get(roomId).get(name);
    <span class="hljs-keyword">if</span> (!session.isOpen()) <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    <span class="hljs-keyword">try</span> {
        session.sendMessage(message);
    } <span class="hljs-keyword">catch</span> (IOException e) {
        e.printStackTrace();
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    }
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
}

<span class="hljs-comment">/**
 * 广播信息给某房间内的所有用户
 */</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">sendMessageToRoomUsers</span><span class="hljs-params">(String roomId, TextMessage message)</span> </span>{
    <span class="hljs-keyword">if</span> (roomId == <span class="hljs-keyword">null</span>) <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    <span class="hljs-keyword">if</span> (sUserMap.get(roomId) == <span class="hljs-keyword">null</span>) <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    <span class="hljs-keyword">boolean</span> allSendSuccess = <span class="hljs-keyword">true</span>;
    Collection&lt;WebSocketSession&gt; sessions = sUserMap.get(roomId).values();
    <span class="hljs-keyword">for</span> (WebSocketSession session : sessions) {
        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">if</span> (session.isOpen()) {
                session.sendMessage(message);
            }
        } <span class="hljs-keyword">catch</span> (IOException e) {
            e.printStackTrace();
            allSendSuccess = <span class="hljs-keyword">false</span>;
        }
    }

    <span class="hljs-keyword">return</span> allSendSuccess;
}

<span class="hljs-comment">//退出房间时的处理</span>
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">afterConnectionClosed</span><span class="hljs-params">(WebSocketSession webSocketSession, CloseStatus closeStatus)</span> </span>{
    System.out.println(<span class="hljs-string">"连接已关闭:"</span> + closeStatus);
    Map&lt;String, WebSocketSession&gt; map = sUserMap.get(getRoomIdFromSession(webSocketSession));
    <span class="hljs-keyword">if</span> (map != <span class="hljs-keyword">null</span>) {
        map.remove(getNameFromSession(webSocketSession));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 1
  • 2

}
复制代码

要点解析:

  • 使用sUserMap这个静态变量来保存用户信息。对应我们上述的关系图。
  • 实现afterConnectionEstablished方法,当用户进入房间成功后,保存用户信息到Map,并调用sendMessageToRoomUsers广播新人加入信息。
  • 实现handleMessage方法,处理用户加入,离开和发送信息三类消息。
  • 实现afterConnectionClosed方法,用来处理当用户离开房间后的信息销毁工作。从Map中清除该用户。
  • 实现sendMessageToUsersendMessageToRoomUsers两个向客户端发送消息的方法。直接通过Session即可发送结构化数据到客户端。sendMessageToUser实现了点对点消息的发送,sendMessageToRoomUsers实现了广播消息的发送。

客户端实现

客户端我们就使用HTML5为我们提供的WebSocket JS接口即可。

<html>
    <script type="text/javascript">
        function ToggleConnectionClicked() {
            if (SocketCreated && (ws.readyState == 0 || ws.readyState == 1)) {
                lockOn("离开聊天室...");
                SocketCreated = false;
                isUserloggedout = true;
                var msg = JSON.stringify({'command':'leave', 'roomId':groom , 'name': gname,
                    'info':'离开房间'});
                ws.send(msg);
                ws.close();
            } else if(document.getElementById("roomId").value == "请输入房间号!") {
                Log("请输入房间号!");
            } else {
                lockOn("进入聊天室...");
                Log("准备连接到聊天服务器 ...");
                groom = document.getElementById("roomId").value;
                gname = document.getElementById("txtName").value;
                try {
                    if ("WebSocket" in window) {
                        ws = new WebSocket(
                            'ws://localhost:8080/webSocket/INFO={"command":"enter","name":"'+ gname + '","roomId":"' + groom + '"}');
                    }
                    else if("MozWebSocket" in window) {
                        ws = new MozWebSocket(
                            'ws://localhost:8080/webSocket/INFO={"command":"enter","name":"'+ gname + '","roomId":"' + groom + '"}');
                    }
                    SocketCreated = true;
                    isUserloggedout = false;
                } catch (ex) {
                    Log(ex, "ERROR");
                    return;
                }
                document.getElementById("ToggleConnection").innerHTML = "断开";
                ws.onopen = WSonOpen;
                ws.onmessage = WSonMessage;
                ws.onclose = WSonClose;
                ws.onerror = WSonError;
            }
        };
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">WSonOpen</span>(<span class="hljs-params"></span>) </span>{
        lockOff();
        Log(<span class="hljs-string">"连接已经建立。"</span>, <span class="hljs-string">"OK"</span>);
        $(<span class="hljs-string">"#SendDataContainer"</span>).show();
        <span class="hljs-keyword">var</span> msg = <span class="hljs-built_in">JSON</span>.stringify({<span class="hljs-string">'command'</span>:<span class="hljs-string">'enter'</span>, <span class="hljs-string">'roomId'</span>:groom , <span class="hljs-string">'name'</span>: <span class="hljs-string">"all"</span>,
            <span class="hljs-string">'info'</span>: gname + <span class="hljs-string">"加入聊天室"</span>})
        ws.send(msg);
    };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

</html>
复制代码

要点解析:

  • 发起服务端连接时,注意地址信息:'ws://localhost:8080/webSocket/INFO={"command":"enter","name":"'+ gname + '","roomId":"' + groom + '"}',这里我们在INFO后加入了用户个人信息,服务端收到后,即可根据这个信息标记此会话。
  • 连接建立后,发送给房间内其他人一条加入信息。通过ws.send()方法实现。

至此代码部分就介绍完了,过多的代码就不再堆叠了,更详细的代码,请参见后面的Github地址。

本篇总结

通过一个相对完整的网页聊天室例子,介绍了我们自己使用WebSocket时的几个细节:

  • 服务端想在建立连接,即握手阶段搞事情,实现HandshakeInterceptor
  • 服务端想在建立连接之后和处理客户端发来的消息,实现WebSocketHandler
  • 服务端通过WebSocketSession即可向客户端发送消息,通过用户和Session的绑定,实现对应关系。

想加深理解的同学,还是要深入到代码中仔细体会。限于篇幅,而且在文章中加入大量代码本身也不容易读下去。所以大家还是要实际对着代码理解比较好。

本篇涉及到的代码

完整代码实现-小小网页聊天室

欢迎持续关注原创,喜欢的别忘了收藏关注,码字实在太累,你们的鼓励就是我坚持的动力!

小铭出品,必属精品

欢迎关注xNPE技术论坛,更多原创干货每日推送。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/457909
推荐阅读
相关标签
  

闽ICP备14008679号