• Spring Boot 中使用 WebSocket 实现一对多聊天及一对一聊天


    为什么需要WebSocket?

    我们已经有了http协议,为什么还需要另外一个协议?有什么好处?

    比如我想得到价格变化,只能是客户端想服务端发起请求,服务器返回结果,HTTP协议做不到服务器主动向客户端推送消息,

    这种单向请求的特点,如果服务器有连续的状态变化,客户端要获取指定只能轮询,每隔一段时间,调一次接口,了解服务器有没有新的价格信息

    轮询的效率低,且非常浪费资源,因此工程师们一直在思考,有没有更好的方法。websocket就是这样发明的。

    是什么?

    WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

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

    websocket协议特点

    1 推送功能

    支持服务器端向客户端推送功能。服务器可以直接发送数据而不用等待客户端的请求。

    2 减少通信量

    只要建立起websocket连接,就一直保持连接,在此期间可以源源不断的传送消息,直到关闭请求。也就避免了HTTP的非状态性。
    和http相比,不但每次连接时的总开销减少了,而且websocket的首部信息量也小 ,通信量也减少了。

    3 减少资源消耗

    那么为什么他会解决服务器上消耗资源的问题呢?
    其实我们所用的程序是要经过两层代理的,即HTTP协议在Nginx等服务器的解析下,然后再传送给相应的Handler(PHP等)来处理

    简单地说,我们有一个非常快速的接线员(Nginx),他负责把问题转交给相应的客服(Handler)。本身接线员基本上速度是足够的,但是每次都卡在客服(Handler)了,老有客服处理速度太慢。导致客服不够。Websocket就解决了这样一个难题,建立后,可以直接跟接线员建立持久连接,有信息的时候客服想办法通知接线员,然后接线员在统一转交给客户。这样就可以解决客服处理速度过慢的问题了。

    概念说完了 开整

    第一步 导包

          <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-websocket</artifactId>
            </dependency>
            <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>27.1-jre</version>
            </dependency>

    其中guava包提供了一些集合Map的简单创建等,可以不用导

    第二步 配置类

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

    第三步 控制层

    @Slf4j
    @Controller
    public class SocketController {
    
        @RequestMapping("/websocket/{name}")
        public String webSocket(@PathVariable String name, Model model){
            try{
                log.info("跳转到websocket的页面上");
                model.addAttribute("username",name);
                return "index";
            }
            catch (Exception e){
                log.info("跳转到websocket的页面上发生异常,异常信息是:"+e.getMessage());
                return "error";
            }
        }
    
    
    
    }

    此处注意用的是@Controller而不是@RestController 

    因为RestController多了一个注解 @ResponseBody  ,这注解个干啥我就不解释了

    第四步

    @Slf4j
    @Component
    @ServerEndpoint("/websocket/{username}")
    public class WebSocket {
    
        /**
         * 在线人数
         */
        public static volatile AtomicInteger onlineNumber = new AtomicInteger(0);
    
        /**
         * 以用户的姓名为key,WebSocket为对象保存起来
         */
        private static Map<String, WebSocket> clients = new ConcurrentHashMap<>();
    
        /**
         * 会话
         */
        private Session session;
    
        /**
         * 用户名称
         */
        private String username;
    
        /**
         * 建立连接
         *
         * @param session
         */
        @OnOpen
        public void onOpen(@PathParam("username") String username, Session session)
        {
            System.out.println(this);
            onlineNumber.addAndGet(1);
            log.info("现在来连接的客户id:"+session.getId()+"用户名:"+username);
            this.username = username;
            this.session = session;
            log.info("有新连接加入! 当前在线人数" + onlineNumber);
            try {
                //messageType 1代表上线 2代表下线 3代表在线名单 4代表普通消息
                //先给所有人发送通知,说我上线了
                Map<String,Object> map1 = Maps.newHashMap();
                map1.put("messageType",1);
                map1.put("username",username);
                sendMessageAll(JSON.toJSONString(map1),username);
    
                //把自己的信息加入到map当中去
                clients.put(username, this);
                //给自己发一条消息:告诉自己现在都有谁在线
                Map<String,Object> map2 = Maps.newHashMap();
                map2.put("messageType",3);
                //移除掉自己
                Set<String> set = clients.keySet();
                map2.put("onlineUsers",set);
                sendMessageTo(JSON.toJSONString(map2),username);
            }
            catch (IOException e){
                log.info(username+"上线的时候通知所有人发生了错误");
            }
    
    
    
        }
    
        @OnError
        public void onError(Session session, Throwable error) {
            log.info("服务端发生了错误"+error.getMessage());
            //error.printStackTrace();
        }
        /**
         * 连接关闭
         */
        @OnClose
        public void onClose()
        {
            onlineNumber.addAndGet(-1);
            //webSockets.remove(this);
            clients.remove(username);
            try {
                //messageType 1代表上线 2代表下线 3代表在线名单  4代表普通消息
                Map<String,Object> map1 = Maps.newHashMap();
                map1.put("messageType",2);
                map1.put("onlineUsers",clients.keySet());
                map1.put("username",username);
                sendMessageAll(JSON.toJSONString(map1),username);
            }
            catch (IOException e){
                log.info(username+"下线的时候通知所有人发生了错误");
            }
            log.info("有连接关闭! 当前在线人数" + onlineNumber);
        }
    
        /**
         * 收到客户端的消息
         *
         * @param message 消息
         * @param session 会话
         */
        @OnMessage
        public void onMessage(String message, Session session)
        {
            try {
                log.info("来自客户端消息:" + message+"客户端的id是:"+session.getId());
                JSONObject jsonObject = JSON.parseObject(message);
                String textMessage = jsonObject.getString("message");
                String fromusername = jsonObject.getString("username");
                String tousername = jsonObject.getString("to");
                //messageType 1代表上线 2代表下线 3代表在线名单  4代表普通消息
                Map<String,Object> map1 = Maps.newHashMap();
                map1.put("messageType",4);
                map1.put("textMessage",textMessage);
                map1.put("fromusername",fromusername);
                if(tousername.equals("All")){
                    map1.put("tousername","所有人");
                    sendMessageAll(JSON.toJSONString(map1),fromusername);
                }
                else{
                    map1.put("tousername",tousername);
                    sendMessageTo(JSON.toJSONString(map1),tousername);
                }
            }
            catch (Exception e){
                log.info("发生了错误了");
            }
    
        }
    
        public void sendMessageTo(String message, String ToUserName) throws IOException {
            WebSocket webSocket = clients.get(ToUserName);
            webSocket.session.getBasicRemote().sendText(message);
    //        for (WebSocket item : clients.values()) {
    //            if (item.username.equals(ToUserName) ) {
    //                item.session.getAsyncRemote().sendText(message);
    //                break;
    //            }
    //        }
        }
    
        public void sendMessageAll(String message,String FromUserName) throws IOException {
            for (WebSocket item : clients.values()) {
                item.session.getAsyncRemote().sendText(message);
            }
        }
    
        public static synchronized AtomicInteger getOnlineCount() {
            return onlineNumber;
        }
    
    }

    看完代码有没有发现我在map中放的是this 为什么?

    因为@ServerEndpoint不是单例模式

    onlineNumber这个是线程安全的计数类 如果想深入了解就看看CAS吧

    5 页面目录

    放置resources/remplates下 

    6 页面代码

    <!DOCTYPE html>
    <html>
    <head>
        <title>websocket</title>
        <script type="text/javascript" src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.min.js"></script>
        <script src="http://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
        <script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
    </head>
    
    <body>
    <div style="margin: auto;text-align: center">
        <h1>Welcome to websocket</h1>
    </div>
    <br/>
    <div style="margin: auto;text-align: center">
        <select id="onLineUser">
            <option>--所有--</option>
        </select>
        <input id="text" type="text" />
        <button onclick="send()">发送消息</button>
    </div>
    <br>
    <div style="margin-right: 10px;text-align: right">
        <button onclick="closeWebSocket()">关闭连接</button>
    </div>
    <hr/>
    <div id="message" style="text-align: center;"></div>
    <input  type="text" th:value="${username}" id="username" style="display: none" />
    </body>
    
    
    <script type="text/javascript">
        var webSocket;
        var commWebSocket;
        if ("WebSocket" in window)
        {
            webSocket = new WebSocket("ws://localhost:8080/websocket/"+document.getElementById('username').value);
    
            //连通之后的回调事件
            webSocket.onopen = function()
            {
                //webSocket.send( document.getElementById('username').value+"已经上线了");
                console.log("已经连通了websocket");
                setMessageInnerHTML("已经连通了websocket");
            };
    
            //接收后台服务端的消息
            webSocket.onmessage = function (evt)
            {
                var received_msg = evt.data;
                console.log("数据已接收:" +received_msg);
                var obj = JSON.parse(received_msg);
                console.log("可以解析成json:"+obj.messageType);
                //1代表上线 2代表下线 3代表在线名单 4代表普通消息
                if(obj.messageType==1){
                    //把名称放入到selection当中供选择
                    var onlineName = obj.username;
                    var option = "<option>"+onlineName+"</option>";
                    $("#onLineUser").append(option);
                    setMessageInnerHTML(onlineName+"上线了");
                }
                else if(obj.messageType==2){
                    $("#onLineUser").empty();
                    var onlineName = obj.onlineUsers;
                    var offlineName = obj.username;
                    var option = "<option>"+"--所有--"+"</option>";
                    for(var i=0;i<onlineName.length;i++){
                        if(!(onlineName[i]==document.getElementById('username').value)){
                            option+="<option>"+onlineName[i]+"</option>"
                        }
                    }
                    $("#onLineUser").append(option);
    
                    setMessageInnerHTML(offlineName+"下线了");
                }
                else if(obj.messageType==3){
                    var onlineName = obj.onlineUsers;
                    var option = null;
                    for(var i=0;i<onlineName.length;i++){
                        if(!(onlineName[i]==document.getElementById('username').value)){
                            option+="<option>"+onlineName[i]+"</option>"
                        }
                    }
                    $("#onLineUser").append(option);
                    console.log("获取了在线的名单"+onlineName.toString());
                }
                else{
                    setMessageInnerHTML(obj.fromusername+""+obj.tousername+"说:"+obj.textMessage);
                }
            };
    
            //连接关闭的回调事件
            webSocket.onclose = function()
            {
                console.log("连接已关闭...");
                setMessageInnerHTML("连接已经关闭....");
            };
        }
        else{
            // 浏览器不支持 WebSocket
            alert("您的浏览器不支持 WebSocket!");
        }
        //将消息显示在网页上
        function setMessageInnerHTML(innerHTML) {
            document.getElementById('message').innerHTML += innerHTML + '<br/>';
        }
    
        function closeWebSocket() {
            //直接关闭websocket的连接
            webSocket.close();
        }
    
        function send() {
            var selectText = $("#onLineUser").find("option:selected").text();
            if(selectText=="--所有--"){
                selectText = "All";
            }
            else{
                setMessageInnerHTML(document.getElementById('username').value+""+selectText+"说:"+ $("#text").val());
            }
            var message = {
                "message":document.getElementById('text').value,
                "username":document.getElementById('username').value,
                "to":selectText
            };
            webSocket.send(JSON.stringify(message));
            $("#text").val("");
    
        }
    </script>
    
    </html>

     总结一下

    此处注意用的是@Controller而不是@RestController 

    因为RestController多了一个注解 @ResponseBody

    看完代码有没有发现我在map中放的是this 为什么?

    因为@ServerEndpoint不是单例模式

    onlineNumber这个是线程安全的计数类 如果想深入了解就看看CAS吧

  • 相关阅读:
    P3381 【模板】最小费用最大流
    Android Studio安装插件提示was not installed: Cannot download的解决
    webpack 引用vconsole
    vue ...mapMutations 的第一个参数默认为 数据对象state
    vue 如何实现在函数中触发路由跳转
    axios 使用
    rem 的使用
    vscode 习惯配置
    修改默认的inout输入框背景颜色
    vue -本地服务配置
  • 原文地址:https://www.cnblogs.com/liaohailong/p/10997802.html
Copyright © 2020-2023  润新知