精华内容
下载资源
问答
  • 由于自己需要做一个页面监控后台数据的自己了解了下websocket做了个小实验 小测试类 package com.pw.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind....

    由于自己需要做一个页面监控后台数据的自己了解了下websocket做了个小实验

    小测试类

    package com.pw.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    @RequestMapping("/hello")
    public class Hello {
    	@RequestMapping("/toff")
        public String toff() throws Exception{
    		WebSocketTest ws=new WebSocketTest();
    		ws.onMessage("c1##to##你好啊", "c2");
    		System.out.println("你好啊");
    		boolean stt=true;
    		while(stt){
    			if(WebSocketTest.str!=null&&!"".equals(WebSocketTest.str)){
    				stt=false;
    			}
    			System.out.println("11111");
    		}
    		System.out.println(WebSocketTest.str);
    	return "sds";
    }
    }
    
    java后台websocket类

    package com.pw.controller;
    
    import java.text.SimpleDateFormat;  
    import java.util.Date;  
     
    import javax.websocket.OnClose;  
    import javax.websocket.OnError;  
    import javax.websocket.OnMessage;  
    import javax.websocket.OnOpen;  
    import javax.websocket.Session;  
    import javax.websocket.server.PathParam;  
    import javax.websocket.server.ServerEndpoint;  
      
    /** 
     * 这里使用注释的方式来向系统指明,我的这个WebSocketTest是一个webservice。 同时指定了路径为/websocket/{uCode} 
     * 其中的{uCode}是一个变化的参数,在网页端动态的输入。这样我就可以通过@PathParam("uCode")这个注释在参数中获得用户的信息等等了。 
     *  
     * */  
    @ServerEndpoint("/websocket/{uCode}")  
    public class WebSocketTest {  
         public static String str="";
        /** 
         * @param message 
         *            这里是客户端传来的消息,我这里只是简单的测试文本消息,消息的种类可以有很多种。 
         * @param uCode 
         *            这就是{uCode}中传来的路径参数,可以用来传递用户的信息。例如帐号。 
         * @throws Exception 
         *             偷懒的人总是抛出一个EXCEPTION 
         */  
        @OnMessage  
        // 当有消息传来的时候进行处理的方法  
        public void onMessage(String message, @PathParam("uCode") String uCode)  
                throws Exception {  
        	if("c1".equals(uCode)){
        		str=message;
        	}else{
            System.out.println("revived:" + message);// 输出一下接收到的消息  
            String tem[] = message.split("##to##");// 消息的格式是  
                                                    // tousername##to##message,分隔以后第一个就是要发送的用户名,第二个就是消息了  
            if (SessionUtils.hasConnection(tem[0])) {// 从sessionUtils中判断一下是否要发送的用户名是否已经登录,登录的话做以下操作  
                /* 弄个时间开始 */  
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
                String x = sdf.format(new Date());  
                /* 弄个时间结束 */
                /* 
                 * 消息的发送,可以选择AsyncRemote也可以选择 
                 * BasicRemote,区别在于AsyncRemote是不会阻塞的,系统不会去管消息是否发送完成直接占用通道去发 
                 * 。使用它的话要用户自己去实现控制,所以我选择BasicRemote, 
                 */  
                SessionUtils.get(tem[0]).getBasicRemote()  
                        .sendText(uCode + "##to##" + x + ":" + tem[1]);  
            } else {// 没有登录的话做以下操作  
                SessionUtils.get(uCode).getBasicRemote().sendText("用户不在线");  
            } 
        	}
        }  
      
        /** 
         * @param uCode 
         *            同上 
         * @param session 
         *            这个是用户建立的session信息,用来唯一标识这个用户的通信 
         * @throws Exception 
         *             你懂得 
         */  
        @OnOpen  
        // 用户建立链接的时候执行的方法  
        public void onOpen(@PathParam("uCode") String uCode, Session session)  
                throws Exception {  
      
            if (SessionUtils.hasConnection(uCode)) {// 判断缓存中是否有uCode,如果有执行括号内的方法  
      
                SessionUtils.get(uCode).close();// 因为已经登录了,那么将已经登录的下线  
                SessionUtils.remove(uCode);// 移除掉缓存中的<uCode,Session>  
                SessionUtils.put(uCode, session);// 添加新的<uCode,Session>  
      
                System.out.println(uCode + "has join server");  
            } else {  
                System.out.println(uCode + "has join server");  
                /* 如果没有缓存相关的<uCode,Session>,那么直接添加 */  
                SessionUtils.put(uCode, session);  
            }  
      
        }  
      
        @OnClose  
        //客户端断开链接时执行的方法  
        public void onClose(@PathParam("uCode") String uCode) {  
            System.out.println(uCode + "has left server");  
            SessionUtils.remove(uCode);//直接移除就好了,session已经关闭了  
            System.out.println("left deal was finished");  
        }  
      
        @OnError  
        //客户端出错误的时候执行的方法  
        public void onError(Throwable e, Session session) {  
              
            if (SessionUtils.clients.containsValue(session)) {//移除出错的session  
                SessionUtils.remove(session);  
            }  
      
        }  
      
    }  
    session帮助类

    package com.pw.controller;
    
    import java.util.Iterator;  
    import java.util.Map;  
    import java.util.concurrent.ConcurrentHashMap;  
      
    import javax.websocket.Session;  
      /**
       * Session帮助类
       * @author Administrator
       *
       */
    public class SessionUtils {  
        /*<uCode,Session>的缓存*/  
        public static Map<String, Session> clients = new ConcurrentHashMap<String, Session>();  
      
        public static void put(String uCode, Session session) {//添加  
            clients.put(uCode, session);  
        }  
      
        public static Session get(String uCode) {//根据uCode来获取  
            return clients.get(uCode);  
        }  
      
        public static void remove(String uCode) {//根据uCode来移除  
            clients.remove(uCode);  
        }  
      
        public static void remove(Session session) {//根据session来移除  
            Iterator<java.util.Map.Entry<String, Session>> ito = clients.entrySet()  
                    .iterator();  
            while (ito.hasNext()) {  
                java.util.Map.Entry<String, Session> entry = ito.next();  
                if (entry.getKey().equals(session))  
                    remove(entry.getKey());  
            }  
        }  
      
        public static boolean hasConnection(String uCode) {//根据uCode来判断是否包含对应用户名的<uCode,Session>  
            return clients.containsKey(uCode);  
        }  
    }  

    html5页面代码

    <%@ page language="java" contentType="text/html; charset=utf-8"
    	pageEncoding="utf-8"%>
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
    <!DOCTYPE html>
    <html>
    <head>
    <title>WebSoket Demo</title>
    <script type="text/JavaScript">   
                 //获取链接参数
                function getQueryString(name) {
                var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
                var r = window.location.search.substr(1).match(reg);
                if (r != null) return unescape(r[2]); return null;
                } 
                    //验证浏览器是否支持WebSocket协议  
                    if (!window.WebSocket) {   
                        alert("WebSocket not supported by this browser!");   
                    }   
                     var ws;  
                     var id=getQueryString("id");
                    function display() {   
                        //创建webSocket,后面的${id}与{uCode}相对应  
                        ws=new WebSocket("ws://localhost:8080/inter/websocket/"+id);   
                        //监听消息  
                        ws.onmessage = function(event) {   
                            log(event.data);  
                        };   
                        //绑定关闭事件   
                        ws.onclose = function(event) {  
                            /*这段代码的作用是,如果同一个id在其他的页面登录,那么就强制当前的页面下线,关闭。  
                            在服务器端我们在同一个id登录时关闭了以前的socket,所以这里在关闭事件中进行操作  
                            */  
                            var opened=window.open('about:blank','_self');   
                            opened.opener=null;   
                            opened.close();  
                        };   
                        //建立websocket的事件,可以用来做一些初始化操作;比如如果用户不在线其他人发送了消息我可以放在数据库里,用户一上线就调用查询方法  
                        ws.onopen = function(event) {   
                        };   
                        //出现错误的时候的方法  
                        ws.onerror =function(event){  
                        };  
                    }   
                     
                    var log = function(s) {//打印消息的方法  
                    if (document.readyState !== "complete") {    
                     log.buffer.push(s);    
                    } else {    
                     var ss=(s + "\n");
                     document.getElementById("contentId").innerHTML += (s + "\n");    
                    }    
                    }  
                      
                      
                    function sendMsg(){  
                        //这里是发送消息,我制做了c1、c2之间的通话  
                        var msg=document.getElementById("messageId");  
                        var client=id;  
                        if(client=='c1') { 
                        ws.send("c2##to##"+msg.value);//发送消息 
                        document.getElementById("contentId").innerHTML='c1'+msg.value
                        }else{  
                        ws.send("c1##to##"+msg.value);  
                        document.getElementById("contentId").innerHTML='c2'+msg.value;
                        }
                    }  
                </script>
    </head>
    <body οnlοad="display();">
    	<div id="valueLabel"></div>
    	<textarea rows="20" cols="30" id="contentId"></textarea>
    	<br />
    	<input name="message" id="messageId" />
    	<button id="sendButton" onClick="javascript:sendMsg()">Send</button>
    </body>
    </html>
    


    展开全文
  • 文章目录1 摘要2 核心 Maven 依赖3 配置信息4 核心 Java 类4.1 websocket 会话管理类4.2 websocket 握手拦截器4.3 websocket 连接拦截器4.4 websocket 拦截器配置类4.5 SpringBoot 应用启动类5 测试6 参考资料推荐7 ...



    1 摘要

    在 SpringBoot 中使用原生注解简易集成 websocket 可参考:

    Spring Boot 2.2 原生注解简易集成 websocket

    本文将介绍使用 Spring 封装的注解简易集成 websocket

    2 核心 Maven 依赖

    ./demo-websocket-spring/pom.xml
    
            <!-- Websocket -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-websocket</artifactId>
            </dependency>
    
            <!-- Hutool java 工具类集合 -->
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
                <version>${hutool.version}</version>
            </dependency>
    

    其中 springboot 版本为 2.2.5.RELEASE, ${hutool.version} 版本为 5.2.3

    3 配置信息

    ./demo-websocket-spring/src/main/resources/application.yml
    
    ## config
    
    ## server
    server:
      port: 8201
    

    4 核心 Java 类

    4.1 websocket 会话管理类

    ./demo-websocket-spring/src/main/java/com/ljq/demo/springboot/websocketspring/web/SocketSessionManager.java
    
    package com.ljq.demo.springboot.websocketspring.web;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.socket.WebSocketSession;
    
    import java.io.IOException;
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * @Description: websocket 会话管理
     * @Author: junqiang.lu
     * @Date: 2020/3/19
     */
    @Slf4j
    public class SocketSessionManager {
    
        /**
         * websocket 会话池
         */
        private static ConcurrentHashMap<String, WebSocketSession> webSocketSessionMap = new ConcurrentHashMap<>();
    
        /**
         * 添加 websocket 会话
         *
         * @param key
         * @param session
         */
        public static void add(String key, WebSocketSession session) {
            webSocketSessionMap.put(key, session);
        }
    
        /**
         * 移除 websocket 会话,并将该会话内容返回
         *
         * @param key
         * @return
         */
        public static WebSocketSession remove(String key) {
            return webSocketSessionMap.remove(key);
        }
    
        /**
         * 删除 websocket,并关闭连接
         *
         * @param key
         */
        public static void removeAndClose(String key) {
            WebSocketSession session = remove(key);
            if (session != null) {
                try {
                    session.close();
                } catch (IOException e) {
                    log.error("Websocket session close exception ",e);
                }
            }
        }
    
        /**
         * 获取 websocket 会话
         *
         * @param key
         * @return
         */
        public static WebSocketSession get(String key) {
            return webSocketSessionMap.get(key);
        }
    
        /**
         * 获取会话数量
         *
         * @return
         */
        public static int count() {
            return webSocketSessionMap.size();
        }
    
    }
    

    4.2 websocket 握手拦截器

    在进行 websocket 握手成功之前进行身份验证

    ./demo-websocket-spring/src/main/java/com/ljq/demo/springboot/websocketspring/interceptor/SocketInterceptor.java
    
    package com.ljq.demo.springboot.websocketspring.interceptor;
    
    import cn.hutool.core.util.StrUtil;
    import cn.hutool.http.HttpUtil;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.server.ServerHttpRequest;
    import org.springframework.http.server.ServerHttpResponse;
    import org.springframework.stereotype.Component;
    import org.springframework.web.socket.WebSocketHandler;
    import org.springframework.web.socket.server.HandshakeInterceptor;
    
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * @Description: websocket 握手拦截器
     * @Author: junqiang.lu
     * @Date: 2020/3/19
     */
    @Slf4j
    @Component
    public class SocketInterceptor implements HandshakeInterceptor {
    
        private static final String TOKEN_FIELD = "token";
    
        /**
         * websocket 握手之前
         *
         * @param serverHttpRequest
         * @param serverHttpResponse
         * @param webSocketHandler
         * @param map
         * @return
         * @throws Exception
         */
        @Override
        public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse,
                                       WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
            log.debug("websocket starts handshaking");
            // 获取请求参数
            HashMap<String, String> paramMap = HttpUtil.decodeParamMap(serverHttpRequest.getURI().getQuery(), "utf-8");
            String token = paramMap.get(TOKEN_FIELD);
            if (StrUtil.isNotBlank(token)) {
                map.put("token", token);
                log.debug("用户 [ {} ]握手成功", token);
                return true;
            }
            log.debug("用户登录已失效");
            return false;
        }
    
        /**
         * websocket 握手之后
         *
         * @param serverHttpRequest
         * @param serverHttpResponse
         * @param webSocketHandler
         * @param e
         */
        @Override
        public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse,
                                   WebSocketHandler webSocketHandler, Exception e) {
            log.debug("握手完成!");
        }
    }
    

    4.3 websocket 连接拦截器

    在 websocket 握手成功之后,管理已经创建的 websocket 连接

    ./demo-websocket-spring/src/main/java/com/ljq/demo/springboot/websocketspring/interceptor/SocketAuthHandler.java
    
    package com.ljq.demo.springboot.websocketspring.interceptor;
    
    import com.ljq.demo.springboot.websocketspring.web.SocketSessionManager;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Component;
    import org.springframework.web.socket.CloseStatus;
    import org.springframework.web.socket.TextMessage;
    import org.springframework.web.socket.WebSocketSession;
    import org.springframework.web.socket.handler.TextWebSocketHandler;
    
    import java.time.LocalDateTime;
    import java.util.Objects;
    
    /**
     * @Description: websocket 连接拦截器
     * @Author: junqiang.lu
     * @Date: 2020/3/19
     */
    @Slf4j
    @Component
    public class SocketAuthHandler extends TextWebSocketHandler {
    
        private static final String TOKEN_FIELD = "token";
    
        /**
         * 握手成功之后
         *
         * @param session
         * @throws Exception
         */
        @Override
        public void afterConnectionEstablished(WebSocketSession session) throws Exception {
            Object token = session.getAttributes().get(TOKEN_FIELD);
            if (Objects.nonNull(token)) {
                // 用户连接成功,缓存用户会话
                log.debug("用户[ {} ]创建连接", token);
                SocketSessionManager.add(String.valueOf(token), session);
            } else {
                throw new RuntimeException("用户登录已失效");
            }
        }
    
        /**
         * 接收客户端消息
         *
         * @param session
         * @param message
         * @throws Exception
         */
        @Override
        protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
            // 读取客户端消息
            Object token = session.getAttributes().get(TOKEN_FIELD);
            String payload = message.getPayload();
            log.debug("收到用户 [{}] 的消息,消息内容为: {}",token, payload);
    
            StringBuilder responseBuilder = new StringBuilder();
            responseBuilder.append("服务端已接收到用户 [").append(token).append("] 的消息,消息内容为:");
            responseBuilder.append(payload).append(",当前服务器时间: ");
            responseBuilder.append(LocalDateTime.now());
    
            session.sendMessage(new TextMessage(responseBuilder.toString()));
        }
    
        @Override
        public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
            Object token = session.getAttributes().get(TOKEN_FIELD);
            if (Objects.nonNull(token)) {
                log.debug("用户 [{}] 断开连接", token);
                SocketSessionManager.remove(String.valueOf(token));
            }
        }
    }
    

    方法说明:

    afterConnectionEstablished() 方法是在 websocket 握手成功之后,创建 socket 连接的时候触发,等价于 socket 的原生注解 @OnOpen

    handleTextMessage() 方法是在客户端向服务端发送文本消息的时候触发,等价于 socket 的原生注解 @OnMessage 。Spring 封装的还有 handleMessage() 方法,所有客户端发送消息都会触发该方法,无论什么类型的数据。

    afterConnectionClosed() 方法是在客户端(请求)断开连接的时候触发,等价于 socket 的原生注解 @OnClose

    4.4 websocket 拦截器配置类

    ./demo-websocket-spring/src/main/java/com/ljq/demo/springboot/websocketspring/interceptor/SocketConfig.java
    
    package com.ljq.demo.springboot.websocketspring.interceptor;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.socket.config.annotation.EnableWebSocket;
    import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
    import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
    
    /**
     * @Description: websocket 拦截器配置
     * @Author: junqiang.lu
     * @Date: 2020/3/19
     */
    @Configuration
    @EnableWebSocket
    public class SocketConfig implements WebSocketConfigurer {
    
        @Autowired
        private SocketAuthHandler socketAuthHandler;
        @Autowired
        private SocketInterceptor socketInterceptor;
    
        private static final String WEB_SOCKET_PATH = "socketSpring";
    
        /**
         *
         * @param webSocketHandlerRegistry
         */
        @Override
        public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
            webSocketHandlerRegistry.addHandler(socketAuthHandler, WEB_SOCKET_PATH)
                    .addInterceptors(socketInterceptor)
                    .setAllowedOrigins("*");
        }
    }
    

    4.5 SpringBoot 应用启动类

    ./demo-websocket-spring/src/main/java/com/ljq/demo/springboot/websocketspring/DemoWebsocketSpringApplication.java
    
    package com.ljq.demo.springboot.websocketspring;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    /**
     * @author junqiang.lu
     */
    @SpringBootApplication(scanBasePackages = {"com.ljq.demo.springboot"})
    public class DemoWebsocketSpringApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(DemoWebsocketSpringApplication.class, args);
        }
    
    }
    

    5 测试

    在线 websocket 测试地址:

    WebSocket在线测试工具

    在线WebSocket测试工具

    启动项目,在测试网站输入 websocket 地址:

    ws://127.0.0.1:8201/socketSpring?token=demo1
    

    127.0.0.1 为本机 ip 地址,可根据需要自行更改 ip 地址

    demo1 为用户名,测试时可自行更换

    连接成功,无返回结果,控制台日志:

    2020-03-20 15:00:30 | DEBUG | http-nio-8201-exec-5 | c.l.d.s.w.interceptor.SocketInterceptor 39| websocket starts handshaking
    2020-03-20 15:00:30 | DEBUG | http-nio-8201-exec-5 | c.l.d.s.w.interceptor.SocketInterceptor 45| 用户 [ demo1 ]握手成功
    2020-03-20 15:00:30 | DEBUG | http-nio-8201-exec-5 | c.l.d.s.w.interceptor.SocketInterceptor 63| 握手完成!
    2020-03-20 15:00:30 | DEBUG | http-nio-8201-exec-5 | c.l.d.s.w.interceptor.SocketAuthHandler 36| 用户[ demo1 ]创建连接
    

    发送消息,消息内容为:

    55555
    

    返回结果为:

    服务端已接收到用户 [demo1] 的消息,消息内容为:55555,当前服务器时间: 2020-03-20T15:01:20.990
    

    控制台日志为:

    2020-03-20 15:01:20 | DEBUG | http-nio-8201-exec-6 | c.l.d.s.w.interceptor.SocketAuthHandler 55| 收到用户 [demo1] 的消息,消息内容为: 55555
    

    断开连接,无返回结果,控制台日志为:

    2020-03-20 15:03:08 | DEBUG | http-nio-8201-exec-7 | c.l.d.s.w.interceptor.SocketAuthHandler 69| 用户 [demo1] 断开连接
    

    至此,在 SpringBoot 中使用 Spring 封装注解简易集成 websocket 已实现

    6 参考资料推荐

    A Guide to the Java API for WebSocket

    Spring Boot系列十六 WebSocket简介和spring boot集成简单消息代理

    【websocket】spring boot 集成 websocket 的四种方式

    WebSocket在线测试工具

    在线WebSocket测试工具

    7 Github 源码

    Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo

    个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.
    404Code

    展开全文
  • websocket

    2020-12-19 19:33:09
    websocket一、websocket简介二、flask-websocket的安装及初始化操作三、创建socket连接四、基于事件接受信息1、基于未定义事件进行通信2、基于自定义事件进行通信五、服务端响应信息六、基于房间管理分发信息七、...

    一、websocket简介

    一直以来,HTTP是无状态、单向通信的网络协议,即客户端请求一次,服务器回复一次,默认情况下,只允许浏览器向服务器发出请求后,服务器才能返回相应的数据。如果想让服务器消息及时下发到客户端,需要采用类似于轮询的机制,大部分情况就是客户端通过定时器使用ajax频繁地向服务器发出请求。这样的做法效率很低,而且HTTP数据包头本身的字节量较大,浪费了大量带宽和服务器资源。

    为了提高效率,HTML5推出了WebSocket技术。

    WebScoket是一种让客户端和服务器之间能进行全双工通信(full-duplex)的技术。它是HTML最新标准HTML5的一个协议规范,本质上是个基于TCP的协议,它通过HTTP/HTTPS协议发送一条特殊的请求进行握手后创建了一个TCP连接,此后浏览器/客户端和服务器之间便可随时随地以通过此连接来进行双向实时通信,且交换的数据包头信息量很小。

    同时为了方便使用,HTML5提供了非常简单的操作就可以让前端开发者直接实现socket通讯,开发者只需要在支持WebSocket的浏览器中,创建Socket之后,通过onopen、onmessage、onclose、onerror四个事件的实现即可处理Socket的响应。

    注意:websocket是HTML5技术的一部分,但是websocket并非只能在浏览器或者HTML文档中才能使用,事实上在python或者C++等语言中只要能实现websocket协议报文,均可使用。

    客户端报文:

    GET /mofang/websocket HTTP/1.1
    Host: 127.0.0.1
    Origin: http://127.0.0.1:5000
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==      # Sec-WebSocket-Key 是随机生成的
    Sec-WebSocket-Version: 13
    

    服务端报文:

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= # 结合客户端提供的Sec-WebSocket-Key基于固定算法计算出来的
    Sec-WebSocket-Protocol: chat
    

    在本文中,因为我们使用的是flask框架,所以我们使用flask-websocket

    • 注意:websocket存在版本对应关系,关系表如下:

    关系表

    二、flask-websocket的安装及初始化操作

    安装

    pip install flask-socketio
    pip install gevent-websocket
    

    初始化:

    在application/__init__文件中进行初始化操作-socketio(部分模块是项目其他模块,请忽略):

    from flask import Flask
    from flask_script import Manager
    from flask_sqlalchemy import SQLAlchemy
    from flask_redis import FlaskRedis
    from flask_session import Session
    from flask_migrate import Migrate,MigrateCommand
    from application.utils.logger import Log
    from flask_jsonrpc import JSONRPC
    from flask_marshmallow import Marshmallow
    from flask_jwt_extended import JWTManager
    from flask_admin import Admin
    from flask_babelex import Babel
    from faker import Faker
    from flask_pymongo import PyMongo
    from flask_qrcode import QRcode
    from flask_socketio import SocketIO
    import os,sys
    
    from application.utils.config import load_config
    from application.utils.session import init_session
    from application.utils.commands import load_command
    from application.utils import init_blueprint
    
    
    manager=Manager()
    
    db=SQLAlchemy()
    
    redis=FlaskRedis()
    
    session_store = Session()
    
    migrate=Migrate()
    
    log=Log()
    
    jwt=JWTManager()
    
    admin = Admin()
    babel = Babel()
    
    mongo=PyMongo()
    
    QRcode=QRcode()
    # 初始化jsonrpc模块
    jsonrpc = JSONRPC(service_url="/api")
    
    # 数据转换器的数据创建
    ma = Marshmallow()
    
    # socketio
    socketio = SocketIO()
    
    def init_app(config_path):
    	# 创建app应用对象
    	app = Flask(__name__)
    
    	# 项目根目录
    	app.BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    
    
    	# 加载配置
    	Config=load_config(config_path)
    	app.config.from_object(Config)
    
    	# session存储初始化
    	init_session(app)
    	session_store.init_app(app)
    
    	# 数据转换器初始化
    	ma.init_app(app)
    
    	# 数据库初始化
    	db.init_app(app)
    	redis.init_app(app)
    	app.db = db
    	mongo.init_app(app)
    
    	# 数据迁移初始化
    	migrate.init_app(app,db)
    	# 添加数据迁移命令到终端脚本工具中
    	manager.add_command("db",MigrateCommand)
    
    	# 日志初始化
    	app.log=log.init_app(app)
    
    	# 蓝图注册
    	init_blueprint(app)
    
    	#  初始化jsonrpc
    	# jsonrpc.service_url="/api"
    	jsonrpc.init_app(app)
    
    	# jwt初始化
    	jwt.init_app(app)
    
    	#初始化终端脚本工具
    	manager.app=app
    
    
    	# admin站点
    	admin.init_app(app)
    
    	# 项目语言
    	babel.init_app(app)
    
    	# qrcode初始化配置
    	QRcode.init_app(app)
    
    	# 数据种子生成器
    	app.faker = Faker(app.config.get('LANGUAGE'))
    
    	# socketio
    	socketio.init_app(app,cors_allowed_origins=app.config["CORS_ALLOWED_ORIGINS"],async_mode=app.config["ASYNC_MODE"],debug=app.config["DEBUG"])
    
    	# 改写runserver
    	if sys.argv[1] == "runserver":
    		manager.add_command("run",socketio.run(app,host=app.config["HOST"],port=app.config["PORT"]))
    
    	# 注册自定义命令
    	load_command(manager)
    
    	return manager
    
    

    如代码中所示,sockio的配置参数需要在在dev文件中取:

    	# socketio配置信息
    	DEBUG = True
    	CORS_ALLOWED_ORIGINS = "*"
    	ASYNC_MODE = None
    	HOST = "0.0.0.0"
    	PORT = 5000
    

    在加载蓝图的过程中,自动加载socket的api:

    from flask import Blueprint
    from importlib import import_module
    
    def init_blueprint(app):
    	# 自动注册蓝图
    	blueprint_path_list = app.config.get("INSTALLED_APPS")
    	print(blueprint_path_list)
    
    	# 加载admin站点总配置文件
    	try:
    		import_module(app.config.get("ADMIN_PATH"))
    	except:
    		pass
    
    	for blueprint_path in blueprint_path_list:
    		blueprint_name = blueprint_path.split(".")[-1]
    		# 自动创建蓝图对象
    		blueprint = Blueprint(blueprint_name,blueprint_path)
    		# 蓝图自动注册和绑定视图的子路由
    		url_module=import_module(blueprint_path+".urls") # 加载蓝图下子路由文件
    		for url in url_module.urlpatterns: #遍历子路由的所有路由关系
    			blueprint.add_url_rule(**url) #注册到蓝图下
    
    		# 读取路由总文件
    		url_path=app.config.get("URL_PATH")
    		urlpatterns = import_module(url_path).urlpatterns
    		url_prefix = ""
    		for urlpattern in urlpatterns:
    			if urlpattern["blueprint_path"] == blueprint_name + ".urls":
    				url_prefix = urlpattern["url_prefix"]
    				break
    
    		# 注册模型
    		import_module(blueprint_path + ".models")
    		try:
    			import_module(blueprint_path + ".admin")
    		except:
    			pass
    
    		# 加载蓝图内部的socket接口
    		try:
    			import_module(blueprint_path+".socket")
    		except:
    			pass
    
    		# 注册蓝图对象到app应用对象中
    		app.register_blueprint(blueprint,url_prefix=url_prefix)
    def path(rule,func_view):
    	# 把蓝图下视图和路由之间的映射关系处理成字典结构,方便后面注册蓝图的时候,直接传参
    	return {"rule":rule,"view_func":func_view}
    
    def include(url_prefix, blueprint_path):
    	return {"url_prefix":url_prefix,"blueprint_path":blueprint_path}
    
    

    生成蓝图目录:

    cd application/apps/
    python ../../manage.py blue -n=orchard
    

    dev文件中注册蓝图:

    # 注册蓝图
        INSTALLED_APPS = [
            "application.apps.home",
            "application.apps.users",
            "application.apps.marsh",
            "application.apps.orchard",
        ]
    

    前端我们使用vue,新建一个orchard.html文件作为测试html文件,因为我们是基于python-socketio模块提供的服务端,所以此文件中(客户端)必须引入socketIO.js才可通过js与服务端通信:

    <!DOCTYPE html>
    <html>
    <head>
    	<title>用户中心</title>
    	<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
    	<meta charset="utf-8">
    	<link rel="stylesheet" href="../static/css/main.css">
    	<script src="../static/js/vue.js"></script>
    	<script src="../static/js/axios.js"></script>
    	<script src="../static/js/main.js"></script>
    	<script src="../static/js/uuid.js"></script>
    	<script src="../static/js/settings.js"></script>
    	<script src="../static/js/socket.io.js"></script>
    </head>
    <body>
    	<div class="app orchard" id="app">
        <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
        <div class="orchard-bg">
    			<img src="../static/images/bg2.png">
    			<img class="board_bg2" src="../static/images/board_bg2.png">
    		</div>
        <img class="back" @click="go_index" src="../static/images/user_back.png" alt="">
    
    	</div>
    	<script>
    	apiready = function(){
    		init();
    		new Vue({
    			el:"#app",
    			data(){
    				return {
              music_play:true,
              namespace: '/mofang_orchard',
              token:"",
              socket: null,
              timeout: 0,
    					prev:{name:"",url:"",params:{}},
    					current:{name:"orchard",url:"orchard.html",params:{}},
    				}
    			},
          created(){
            this.checkout();
    
          },
    		methods:{
            go_index(){
              this.game.outWin("orchard");
            },
    			}
    		});
    	}
    	</script>
    </body>
    </html>
    

    css样式:

    .app .orchard-bg{
    	margin: 0 auto;
    	width: 100%;
    	max-width: 100rem;
    	position: absolute;;
    	z-index: -1;
      top: -6rem;
    }
    .app .orchard-bg .board_bg2{
      position: absolute;
      top: 1rem;
    }
    .orchard .back{
    	position: absolute;
    	width: 3.83rem;
    	height: 3.89rem;
      z-index: 1;
      top: 2rem;
      left: 2rem;
    }
    .orchard .music{
      right: 2rem;
    }
    .orchard .header{
      position: absolute;
      top: 0rem;
      left: 0;
      right: 0;
      margin: auto;
      width: 32rem;
      height: 19.28rem;
    }
    
    .orchard .info{
      position: absolute;
      z-index: 1;
      top: 0rem;
      left: 4.4rem;
      width: 8rem;
      height: 9.17rem;
    }
    .orchard .info .avata{
      width: 8rem;
      height: 8rem;
      position: relative;
    }
    .orchard .info .avatar_bf{
      position: absolute;
      z-index: 1;
      margin: auto;
      width: 6rem;
      height: 6rem;
      top: 0;
      bottom: 0;
      left: 0;
      right: 0;
    }
    .orchard .info .user_avatar{
      position: absolute;
      z-index: 1;
      width: 6rem;
      height: 6rem;
      margin: auto;
      top: 0;
      bottom: 0;
      left: 0;
      right: 0;
      border-radius: 1rem;
    }
    .orchard .info .avatar_border{
      position: absolute;
      z-index: 1;
      margin: auto;
      top: 0;
      bottom: 0;
      left: 0;
      right: 0;
      width: 7.2rem;
      height: 7.2rem;
    }
    .orchard .info .user_name{
      position: absolute;
      left: 8rem;
      top: 1rem;
      width: 11rem;
      height: 3rem;
      line-height: 3rem;
      font-size: 1.5rem;
      text-shadow: 1px 1px 1px #aaa;
      border-radius: 3rem;
      background: #ff9900;
      text-align: center;
    }
    
    .orchard .wallet{
      position: absolute;
      top: 3.4rem;
      right: 4rem;
      width: 16rem;
      height: 10rem;
    }
    .orchard .wallet .balance{
      margin-top: 1.4rem;
      float: left;
      margin-right: 1rem;
    }
    .orchard .wallet .title{
      color: #fff;
      font-size: 1.2rem;
      width: 6.4rem;
      text-align: center;
    }
    .orchard .wallet .title img{
      width: 1.4rem;
      margin-right: 0.2rem;
      vertical-align: sub;
      height: 1.4rem;
    }
    .orchard .wallet .num{
      background: url("../images/btn3.png") no-repeat 0 0;
      background-size: 100%;
      width: 6.4rem;
      font-size: 0.8rem;
      color: #fff;
      height: 2rem;
      line-height: 1.8rem;
      text-indent: 1rem;
    }
    .orchard .header .menu-list{
      position: absolute;
      top: 9rem;
      left: 2rem;
    }
    .orchard .header .menu-list .menu{
      color: #fff;
      font-size: 1rem;
      float: left;
      width: 4rem;
      height: 4rem;
      text-align: center;
      margin-right: 2rem;
    }
    .orchard .header .menu-list .menu img{
      width: 3.33rem;
      height: 3.61rem;
      display: block;
      margin: auto;
      margin-bottom: 0.4rem;
    }
    .orchard .footer{
      position: absolute;
      width: 100%;
      height: 6rem;
      bottom: -2rem;
      background: url("../images/board_bg3.png") no-repeat -1rem 0;
      background-size: 110%;
    }
    .orchard .footer .menu-list{
      width: 100%;
      height: 4rem;
      display: flex;
      position: absolute;
      top: -1rem;
    }
    .orchard .footer .menu-list .menu,
    .orchard .footer .menu-list .menu-center{
      float: left;
      width: 4.44rem;
      height: 5.2rem;
      font-size: 1.5rem;
      color: #fff;
      line-height: 4.44rem;
      text-align: center;
      background: url("../images/btn5.png") no-repeat 0 0;
      background-size: 100%;
      flex: 1;
      margin-left: 4px;
      margin-right: 4px;
    }
    .orchard .footer .menu-list .menu-center{
      background: url("../images/btn6.png") no-repeat 0 0;
      background-size: 100%;
      flex: 2;
    }
    

    三、创建socket连接

    先在后端蓝图下面创建socket.py文件,并提供连接接口, orchard/socket.py:

    from application import socketio
    from flask import request
    from application.apps.users.models import User
    from flask_socketio import join_room, leave_room
    
    @socketio.on("connect",namespace="/mofang")
    def user_connect():
    	# request.sid socketIO基于客户端生成唯一会话id
    	print("用户%s连接过来了" % request.sid)
    
    @socketio.on("disconnect",namespace="/mofang")
    def user_disconnect():
    	print("用户%s离开了" % request.sid)
    

    前端vue连接后端socketio,我们只截取相关函数:

    checkout(){
                    var token = this.game.get("access_token") || this.game.fget("access_token");
                    this.game.checkout(this,token,(new_access_token)=>{
                        this.connect();
                    })
                },
    connect(){
                    // socket连接
                    this.socket = io.connect(this.settings.socket_server + this.settings.socket_namespace,{transports:["websocket"]});
                    this.socket.on("connect",()=>{  
                            this.game.print("开始连接服务器");
                    })
                },
    

    注意:checkout函数为token检测机制,checkout函数需要使用created方法挂载才能使页面加载时自动执行相关函数,这里不再展示

    效果展示:
    app登陆后进入相应界面:

    服务端:
    进入

    离开相应界面:

    服务端:
    离开
    如图,连接是成功的。

    四、基于事件接受信息

    1、基于未定义事件进行通信

    客户端:

    js可通过通过send方法可以直接发送数据给后端,数据格式为json格式

    checkout(){
                    var token = this.game.get("access_token") || this.game.fget("access_token");
                    this.game.checkout(this,token,(new_access_token)=>{
                        this.connect();
                    })
                },
    connect(){
                    // socket连接
                    this.socket = io.connect(this.settings.socket_server + this.settings.socket_namespace,{transports:["websocket"]});
                    this.socket.on("connect",()=>{  
                            this.game.print("开始连接服务器");
                            this.login()
                    })
                },
                login(){
                    var id = this.game.get("id")
                    this.socket.send({"uid":id})
                },
    

    服务端:

    后端通过形参来接收数据

    from application import socketio
    from flask import request
    from application.apps.users.models import User
    from flask_socketio import join_room, leave_room
    
    
    @socketio.on("message",namespace="/mofang")
    def user_message(data):
    	id = data.get("uid")
    	print("用户id为%s" % id)
    

    后端输出结果:
    接收

    2、基于自定义事件进行通信

    客户端:

    前端通过emit方法可触发自定义事件进行通信

    checkout(){
                    var token = this.game.get("access_token") || this.game.fget("access_token");
                    this.game.checkout(this,token,(new_access_token)=>{
                        this.connect();
                    })
                },
    connect(){
                    // socket连接
                    this.socket = io.connect(this.settings.socket_server + this.settings.socket_namespace,{transports:["websocket"]});
                    this.socket.on("connect",()=>{  
                            this.game.print("开始连接服务器");
                            this.login()
                    })
                },
                login(){
                    var id = this.game.get("id")
                    this.socket.emit("login",{"uid":id})
                },
    

    服务端:
    服务端装饰器第一个参数需和emit方法定义的事件名一致

    from application import socketio
    from flask import request
    from application.apps.users.models import User
    from flask_socketio import join_room, leave_room
    
    
    @socketio.on("login",namespace="/mofang")
    def user_message(data):
    	id = data.get("uid")
    	print("自定义通信用户id为%s" % id)
    

    五、服务端响应信息

    服务端:
    服务端使用emit方法向客户端响应数据:

    from application import socketio
    from flask import request
    from application.apps.users.models import User
    from flask_socketio import join_room, leave_room
    
    
    @socketio.on("login",namespace="/mofang")
    def user_message(data):
    	id = data.get("uid")
    	print("自定义通信用户id为%s" % id)
    	# 主动响应数据给客户端(例:从数据库中统计所有用户数量返回给客户端)
    	count = User.query.count()
    	print(count)
    	socketio.emit("server_response",{"count":count},namespace="/mofang")
    

    客户端:
    客户端通过对应事件名来接收服务端传过来的数据

    checkout(){
                    var token = this.game.get("access_token") || this.game.fget("access_token");
                    this.game.checkout(this,token,(new_access_token)=>{
                        this.connect();
                    })
                },
    connect(){
                    // socket连接
                    this.socket = io.connect(this.settings.socket_server + this.settings.socket_namespace,{transports:["websocket"]});
                    this.socket.on("connect",()=>{  
                            this.game.print("开始连接服务器");
                            this.login();
                            this.get_count()
                    })
                },
       login(){
                    var id = this.game.get("id")
                    this.socket.emit("login",{"uid":id})
                },
    get_count(){
                    this.socket.on("server_response",(res)=>{
                        this.game.print(res.count);
                        alert(`共有${res.count}人在种植园忙碌着~`)
                    });
                },
    

    app展示效果:

    app

    六、基于房间管理分发信息

    使用room方法,可通过一定条件,如用户id,将不同的响应信息私有化分发给对应用户,不会进行广播操作

    服务端:

    from application import socketio
    from flask import request
    from application.apps.users.models import User
    from flask_socketio import join_room, leave_room
    
    
    @socketio.on("login",namespace="/mofang")
    def user_message(data):
    	id = data.get("uid")
    	print("自定义通信用户id为%s" % id)
    	# 基于用户id分配不同房间
    	join_room(id)
    	socketio.emit("server_response",{"login":"登陆成功"},namespace="/mofang",room=id)
    

    客户端:

    checkout(){
                    var token = this.game.get("access_token") || this.game.fget("access_token");
                    this.game.checkout(this,token,(new_access_token)=>{
                        this.connect();
                    })
                },
     connect(){
                    // socket连接
                    this.socket = io.connect(this.settings.socket_server + this.settings.socket_namespace,{transports:["websocket"]});
                    this.socket.on("connect",()=>{  
                            this.game.print("开始连接服务器");
                            this.login();
                            this.get_count()
                    })
                },
        login(){
                    var id = this.game.get("id")
                    this.socket.emit("login",{"uid":id})
                },
    get_count(){
                    this.socket.on("server_response",(res)=>{
                        this.game.print(res.login);
                        alert(res.login)
                    });
                },
    

    七、服务端定时推送数据

    我们需要开启一个线程来让数据定时变化,使用random模块每隔一秒页面打印随机数

    服务端:

    from application import socketio
    from flask import request
    from application.apps.users.models import User
    from flask_socketio import join_room, leave_room
    from threading import Lock
    import random
    
    
    thread = None
    thread_lock=Lock()
    
    @socketio.on("login",namespace="/mofang")
    def user_message(data):
    	global thread
    	with thread_lock:
    		if thread is None:
    			thread = socketio.start_background_task(target=background_thread)
    def background_thread():
    	while True:
    		# 定时1秒
    		socketio.sleep(1)
    		count=random.randint(1,100)
    		socketio.emit("server_response",{"count":count},namespace="/mofang")
    

    客户端:

    因为需要vue数据驱动视图,故将前端代码截全

    <!DOCTYPE html>
    <html>
    <head>
    	<title>用户中心</title>
    	<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
    	<meta charset="utf-8">
    	<link rel="stylesheet" href="../static/css/main.css">
    	<script src="../static/js/vue.js"></script>
    	<script src="../static/js/axios.js"></script>
    	<script src="../static/js/main.js"></script>
    	<script src="../static/js/uuid.js"></script>
    	<script src="../static/js/settings.js"></script>
    	<script src="../static/js/socket.io.js"></script>
    </head>
    <body>
    	<div class="app orchard" id="app">
        <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
        <div class="orchard-bg">
    			<img src="../static/images/bg2.png">
    			<img class="board_bg2" src="../static/images/board_bg2.png">
    		</div>
        <img class="back" @click="go_index" src="../static/images/user_back.png" alt="">
        <h1 style="position:absolute;top:20rem;">{{num}}</h1>
    
    	</div>
    	<script>
    	apiready = function(){
    		init();
    		new Vue({
    			el:"#app",
    			data(){
    				return {
              music_play:true,
              token:"",
              num:"",
              socket: null,
              timeout: 0,
    					prev:{name:"",url:"",params:{}},
    					current:{name:"orchard",url:"orchard.html",params:{}},
    				}
                },
            created(){
                this.checkout()
            },
    		methods:{
                checkout(){
                    var token = this.game.get("access_token") || this.game.fget("access_token");
                    this.game.checkout(this,token,(new_access_token)=>{
                        this.connect();
                    })
                },
                connect(){
                    // socket连接
                    this.socket = io.connect(this.settings.socket_server + this.settings.socket_namespace,{transports:["websocket"]});
                    this.socket.on("connect",()=>{  
                            this.game.print("开始连接服务器");
                            this.login();
                            this.get_count()
                    })
                },
                login(){
                    var id = this.game.get("id")
                    this.socket.emit("login",{"uid":id})
                },
                get_count(){
                    this.socket.on("server_response",(res)=>{
                        
                        this.num = res.count
                    });
                },
                go_index(){
                    this.game.outWin("orchard")
                }
    			}
    		});
    	}
    	</script>
    </body>
    </html>
    

    app展示效果:

    每隔1秒h1标签中的数字会发生变化,此处我们截不同时刻的两张图

    app1
    app2

    展开全文
  • WebSocket

    2019-09-18 10:10:13
    WebSocket的故事系列计划分五大篇六章,旨在由浅入深的介绍WebSocket以及在Springboot中如何快速构建和使用WebSocket提供的能力。本系列计划包含如下几篇文章: 第一篇,什么是WebSocket以及它的用途 第二篇,Spring...

    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());
        }
    }
    复制代码

    要点解析:

    • 注册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;
        }
    }
    

    复制代码

    要点解析:

    • 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));
        }
    }
    

    }
    复制代码

    要点解析:

    • 使用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);
        };
    

    </html>
    复制代码

    要点解析:

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

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

    本篇总结

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

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

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

    本篇涉及到的代码

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

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

    小铭出品,必属精品

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

    展开全文
  • 看到许多博客里都是通过前端建立websocket请求时将用户信息传递给后端并绑定到websocket的实例中,这样也能满足需求,但是针对粒度要达到会话级别的时候,同时系统采用异步编程时,这个做法就无法满足业务场景了,...
  • 做项目有时候需要实现客服会话 聊天等功能。我们平常的HTTP无法满足长连接,如果一直请求服务器会造成资源的浪费,所以http协议就行不通了,这时候就需要使用WebSocket技术。 废话不多说,我整理了使用的操作步骤,...
  • websocket-extensions提供了一个用于注册扩展插件的容器,并提供了通过Sec-WebSocket-Extensions标头协商在会话期间使用哪些扩展所需的所有功能。 通过实现本文档中定义的API,基于此框架的任何WebSocket库都可以...
  • var websocket;//websocket对象 var url = ''; $(function(){ url = encodeURI('wss://'+'${oladress }'); createWebSocket(url); } //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没...
  • 欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ ...然而,微服务还有可横向伸缩的云原生应用这一现代趋势揭露了现今的会话管理技术在设计上的一些缺陷,挑战着我们在过去 20 多年来对这一设...
  • Spring Session 实现分布式会话管理

    千次阅读 2018-06-28 23:17:56
    Spring Session 实现分布式会话管理 1、分布式会话管理是什么?在Web项目开发中,会话管理是一个很重要的部分,用于存储与用户相关的数据。通常是由符合session规范的容器来负责存储管理,也就是一旦容器关闭,重启...
  • 会话管理一直是企业Java的一部分,以至于它逐渐淡出人们对解决问题的意识,并且在最近的记忆中,该领域没有任何重大创新。 但是,微服务和水平可伸缩的云本机应用程序的现代趋势挑战了过去20年设计和构建会话管理...
  • WebSocket系列:爱奇艺号 WebSocket集群推送网关
  • 目录 概述 特性 快速开始 ...Spring会话MongoDB提供了一个API和实现,用于通过利用Spring Data MongoDB来管理存储在MongoDB中的用户会话信息。 特性 Spring Session MongoDB提供以下功能:...
  • 目录 ...Spring Session提供了用于管理用户会话信息的API和实现。 特性 Spring Session使得支持集群会话变得微不足道,而不依赖于特定于应用程序容器的解决方案。它还提供透明集成:  HttpSess...
  • WebSocket传输图片

    千次阅读 2016-06-24 17:20:00
    本文只是打通了图像采集->服务器,再由服务器推送到客户端的过程。中间全程使用Image Base64字符串传输(效率不高...socket作为主线程,用于处理图像源上传请求以及管理WebSocket会话。当socket收到图像上传请求...
  • WebSocket 对象简介

    2021-06-15 13:56:11
    WebSockets 是一种先进的技术。它可以在用户的浏览器和服务器之间打开交互式通信会话。...WebSocket 对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。 使用WebSoc.
  • websocket通信

    2019-10-31 16:11:30
    前言: 前一段时间面试XX影音公司,技术总监发来一套openfire代码...于是产生了研究一波的心态,最后逛了半天博客看到一篇关于websocket的文章,感觉很nice。今天记录一下! websocket的优缺点这里我就不一一总结了,...
  • Spring Websocket

    千次阅读 2018-10-06 16:12:08
    Spring Websocket https://www.yuque.com/wangji-yunque/xrbpeo/iyw8ce Spring Websocket 出现的背景是什么?spring 秉承一贯的作风,将我们的工作变得更加的简单方便,好使用。使用原生的API太麻烦,而且对于不同的...
  • WebSocket会话中,设备由其唯一的deviceId决定。 可以从您的云帐户中检索设备ID: 感兴趣的设备数据对象由路径格式的topic指定。 可以从您的云帐户中的“设备管理器”中检索主题。 根据设备主题的不同,可以是...
  • springboot Websocket

    2018-01-12 10:43:14
    在springboot项目中使用websocket做推送,虽然挺简单的,但初学也踩过几个坑,特此记录。  使用websocket有两种方式:1是使用sockjs,2是使用h5的标准。使用Html5标准自然更方便简单,所以记录的是配合h5的使用...
  • WebSocket学习

    2019-03-03 18:38:18
    搬运自博文:https://www.cnblogs.com/tohxyblog/p/7112917.html ... 一、建立WebSocket连接 1.与HTTP的关系: 对HTTP的一种补充(Upgrade),两者之间有交集 2.连...
  • websocket安全

    千次阅读 2016-11-30 08:43:40
     WebSocket为web应用和服务提供了双向实时通信信道,这篇论文概述了Websocket协议和这个API,并且描述了它提供的便利。本文的主要贡献是回顾和分析了与WS相关的安全问题,讨论了可能的解决方法以及部署WS的最佳实践...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 6,687
精华内容 2,674
关键字:

websocket会话管理