• spring websocket 和socketjs实现单聊群聊,广播的消息推送详解


    spring websocket 和socketjs实现单聊群聊,广播的消息推送详解

    WebSocket简单介绍

      随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了。近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据。

      我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应 用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:轮询(polling)Comet技术。其实后者本质上也是一种轮询,只不过有所改进。

      轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。

      Comet技术又可以分为长轮询流技术长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。

      这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。

      伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这 样的:浏览器通过JavaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小 了很多。本文不详细地介绍WebSocket规范,主要介绍下WebSocket在Java Web中的实现。

      JavaEE 7中出了JSR-356:Java API for WebSocket规范。不少Web容器,如Tomcat,Nginx,Jetty等都支持WebSocket。Tomcat从7.0.27开始支持 WebSocket,从7.0.47开始支持JSR-356,下面的Demo代码也是需要部署在Tomcat7.0.47以上的版本才能运行。

    项目结构图:

    相关代码:

    pom.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
    
      <groupId>com</groupId>
      <artifactId>websocket-singlechat</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>war</packaging>
    
      <name>websocket-singlechat Maven Webapp</name>
      <!-- FIXME change it to the project's website -->
      <url>http://www.example.com</url>
    
      <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.7</maven.compiler.source>
        <maven.compiler.target>1.7</maven.compiler.target>
      </properties>
    
      <dependencies>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.11</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>javax</groupId>
          <artifactId>javaee-api</artifactId>
          <version>7.0</version>
        </dependency>
        <dependency>
          <groupId>com.google.code.gson</groupId>
          <artifactId>gson</artifactId>
          <version>2.7</version>
        </dependency>
      </dependencies>
    
      <build>
        <finalName>websocket-singlechat</finalName>
        <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
          <plugins>
            <plugin>
              <artifactId>maven-clean-plugin</artifactId>
              <version>3.0.0</version>
            </plugin>
            <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
            <plugin>
              <artifactId>maven-resources-plugin</artifactId>
              <version>3.0.2</version>
            </plugin>
            <plugin>
              <artifactId>maven-compiler-plugin</artifactId>
              <version>3.7.0</version>
            </plugin>
            <plugin>
              <artifactId>maven-surefire-plugin</artifactId>
              <version>2.20.1</version>
            </plugin>
            <plugin>
              <artifactId>maven-war-plugin</artifactId>
              <version>3.2.0</version>
            </plugin>
            <plugin>
              <artifactId>maven-install-plugin</artifactId>
              <version>2.5.2</version>
            </plugin>
            <plugin>
              <artifactId>maven-deploy-plugin</artifactId>
              <version>2.8.2</version>
            </plugin>
          </plugins>
        </pluginManagement>
      </build>
    </project>
    
    

    ChatSocket:

    package com.home.chat;
    
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    
    import javax.websocket.OnClose;
    import javax.websocket.OnMessage;
    import javax.websocket.OnOpen;
    import javax.websocket.Session;
    import javax.websocket.server.ServerEndpoint;
    import com.google.gson.Gson;
    import com.home.vo.ContentVo;
    import com.home.vo.Message;
    
    /**
     * 总通信管道
     *
     */
    @ServerEndpoint("/chatSocket")
    public class ChatSocket {
    
        //定义一个全局变量集合sockets,用户存放每个登录用户的通信管道
        private  static  Set<ChatSocket>  sockets=new HashSet<ChatSocket>();
        //定义一个全局变量Session,用于存放登录用户的用户名
        private  Session  session;
        //定义一个全局变量map,key为用户名,该用户对应的session为value
        private  static  Map<String, Session>  map=new HashMap<String, Session>();
        //定义一个数组,用于存放所有的登录用户,显示在聊天页面的用户列表栏中
        private  static  List<String>names=new ArrayList<String>();
        private String username;
        private Gson  gson=new Gson();
    
        /*
         * 监听用户登录
         */
        @OnOpen
        public void open(Session session){
            System.out.println("建立了一个socket通道" + session.getId());
            this.session = session;
            //将当前连接上的用户session信息全部存到scokets中
            sockets.add(this);
            //拿到URL路径后面所有的参数信息
            String queryString = session.getQueryString();
            System.out.println();
            //截取=后面的参数信息(用户名),将参数信息赋值给全局的用户名
            this.username = queryString.substring(queryString.indexOf("=")+1);
            //每登录一个用户,就将该用户名存入到names数组中,用于刷新好友列表
            names.add(this.username);
            //将当前登录用户以及对应的session存入到map中
            this.map.put(this.username, this.session);
            System.out.println("用户"+this.username+"进入聊天室");
            Message message = new Message();
            message.setAlert("用户"+this.username+"进入聊天室");
            //将当前所有登录用户存入到message中,用于广播发送到聊天页面
            message.setNames(names);
            //将聊天信息广播给所有通信管道(sockets)
            broadcast(sockets, gson.toJson(message) );
        }
    
        /*
         * 退出登录
         */
        @OnClose
        public void close(Session session){
            //移除退出登录用户的通信管道
            sockets.remove(this);
            //将用户名从names中剔除,用于刷新好友列表
            names.remove(this.username);
            Message message = new Message();
            System.out.println("用户"+this.username+"退出聊天室");
            message.setAlert(this.username+"退出当前聊天室!!!");
            //刷新好友列表
            message.setNames(names);
            broadcast(sockets, gson.toJson(message));
        }
    
        /*
         * 接收客户端发送过来的消息,然后判断是广播还是单聊
         */
        @OnMessage
        public void receive(Session  session,String msg) throws IOException{
            //将客户端消息转成json对象
            ContentVo vo = gson.fromJson(msg, ContentVo.class);
            //如果是群聊,就像消息广播给所有人
            if(vo.getType()==1){
                Message message = new Message();
                message.setDate(new Date().toLocaleString());
                message.setFrom(this.username);
                message.setSendMsg(vo.getMsg());
                broadcast(sockets, gson.toJson(message));
            }else{
                Message message = new Message();
                message.setDate(new Date().toLocaleString());
                message.setFrom(this.username);
                message.setAlert(vo.getMsg());
                message.setSendMsg("<font color=red>正在私聊你:</font>"+vo.getMsg());
                String to  = vo.getTo();
                //根据单聊对象的名称拿到要单聊对象的Session
                Session to_session = this.map.get(to);
                //如果是单聊,就将消息发送给对方
                to_session.getBasicRemote().sendText(gson.toJson(message));
            }
        }
    
        /*
         * 广播消息
         */
        public void broadcast(Set<ChatSocket>sockets ,String msg){
            //遍历当前所有的连接管道,将通知信息发送给每一个管道
            for(ChatSocket socket : sockets){
                try {
                    //通过session发送信息
                    socket.session.getBasicRemote().sendText(msg);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    

    ServerConfig:

    package com.home.config;
    
    import java.util.Set;
    
    import javax.websocket.Endpoint;
    import javax.websocket.server.ServerApplicationConfig;
    import javax.websocket.server.ServerEndpointConfig;
    /**
     * 项目启动时会自动启动,类似与ContextListener.
     * 是webSocket的核心配置类。
     *
     */
    public class ServerConfig implements ServerApplicationConfig {
    
        //扫描src下所有类@ServerEndPoint注解的类。
        @Override
        public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scan) {
            System.out.println("扫描到"+scan.size()+"个服务端程序");
            return scan;
        }
    
        //获取所有以接口方式配置的webSocket类。
        @Override
        public Set<ServerEndpointConfig> getEndpointConfigs(
                Set<Class<? extends Endpoint>> point) {
            System.out.println("实现EndPoint接口的类数量:"+point.size());
            return null;
        }
    
    }
    

    LoginServlet:

    package com.home.servlet;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    public class LoginServlet extends HttpServlet {
    
        public void doGet(HttpServletRequest request,
                          HttpServletResponse response)throws IOException,ServletException {
    
        }
    
    
        public void doPost(HttpServletRequest request,
                           HttpServletResponse response)throws IOException,ServletException{
    
            String username = request.getParameter("username");
            System.out.println("doPost当前登录用户为"+username);
            request.getSession().setAttribute("username",username);
            //这里只是简单地模拟登录,登陆之后直接跳转到聊天页面
            response.sendRedirect("chat.jsp");
        }
    }
    

    ContentVo:

    package com.home.vo;
    
    /**
     * 客户端发送给服务端消息实体
     *
     */
    public class ContentVo {
    
        private String to;
        private String msg;
        private Integer type;
        public String getTo() {
            return to;
        }
        public void setTo(String to) {
            this.to = to;
        }
        public String getMsg() {
            return msg;
        }
        public void setMsg(String msg) {
            this.msg = msg;
        }
        public Integer getType() {
            return type;
        }
        public void setType(Integer type) {
            this.type = type;
        }
    
    }
    

    Message:

    package com.home.vo;
    
    import java.util.Date;
    import java.util.List;
    
    /**
     * 服务端发送给客户端消息实体
     *
     */
    public class Message {
    
        private  String  alert;   //
    
        private  List<String>  names;
    
        private  String  sendMsg;
    
        private String  from;
    
        private String  date;
    
    
        public String getDate() {
            return date;
        }
    
        public void setDate(String date) {
            this.date = date;
        }
    
        public String getSendMsg() {
            return sendMsg;
        }
    
        public void setSendMsg(String sendMsg) {
            this.sendMsg = sendMsg;
        }
    
        public String getFrom() {
            return from;
        }
    
        public void setFrom(String from) {
            this.from = from;
        }
    
        public String getAlert() {
            return alert;
        }
    
        public void setAlert(String alert) {
            this.alert = alert;
        }
    
        public List<String> getNames() {
            return names;
        }
    
        public void setNames(List<String> names) {
            this.names = names;
        }
    
        public Message() {
            super();
        }
    }
    
    

    web.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app version="2.5"
             xmlns="http://java.sun.com/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
    	http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
      <display-name>Archetype Created Web Application</display-name>
    
      <servlet>
        <description></description>
        <display-name>LoginServlet</display-name>
        <servlet-name>LoginServlet</servlet-name>
        <servlet-class>com.home.servlet.LoginServlet</servlet-class>
      </servlet>
    
      <servlet-mapping>
        <servlet-name>LoginServlet</servlet-name>
        <url-pattern>/LoginServlet</url-pattern>
      </servlet-mapping>
    
      <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
      </welcome-file-list>
    </web-app>
    
    

    chat.jsp:

    <%@ page language="java" contentType="text/html; charset=utf-8"
             pageEncoding="utf-8"%>
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>Insert title here</title>
        <script type="text/javascript" src="./jquery-3.3.1/jquery-3.3.1.js"></script>
        <script type="text/javascript">
            var ws;
            var userName='${sessionScope.username}';
            //通过URL请求服务端(chat为项目名称)
            var url = "ws://localhost:8080/chatSocket?username="+userName;
          
            //进入聊天页面就是一个通信管道
            window.onload = function() {
                console.log(url);
    
                if ('WebSocket' in window) {
                    ws = new WebSocket(url);
                } else if ('MozWebSocket' in window) {
                    ws = new MozWebSocket(url);
                } else {
                    alert('WebSocket is not supported by this browser.');
                    return;
                }
                ws.onopen=function(){
                    // showMsg("webSocket通道建立成功!!!");
                    console.log("webSocket通道建立成功!!!");
                };
    
                //监听服务器发送过来的所有信息
                ws.onmessage = function(event) {
                    eval("var result=" + event.data);
    
                    //如果后台发过来的alert不为空就显示出来
                    if (result.alert != undefined) {
                        $("#content").append(result.alert + "<br/>");
                    }
    
                    //如果用户列表不为空就显示
                    if (result.names != undefined) {
                        //刷新用户列表之前清空一下列表,免得会重复,因为后台只是单纯的添加
                        $("#userList").html("");
                        $(result.names).each(
                            function() {
                                $("#userList").append(
                                    "<input  type=checkbox  value='"+this+"'/>"
                                    + this + "<br/>");
                            });
                    }
    
                    //将用户名字和当前时间以及发送的信息显示在页面上
                    if (result.from != undefined) {
                        $("#content").append(
                            result.from + " " + result.date + " 说:<br/>"
                            + result.sendMsg + "<br/>");
                    }
    
                };
            };
    
            //将消息发送给后台服务器
            function send() {
                //拿到需要单聊的用户名
                //alert("当前登录用户为"+userName);
                var ss = $("#userList :checked");
                console.log("ss==>"+ss);
                console.log(" ss.length()=="+ss.length);
                //alert("群聊还是私聊"+ss.size());
                var to = $('#userList :checked').val();
                if (to == userName) {
                    alert("你不能给自己发送消息啊");
                    return;
                }
                //根据勾选的人数确定是群聊还是单聊
                var value = $("#msg").val();
                //alert("消息内容为"+value);
                var object = null;
    
                if (ss.length == 0) {
                    object = {
                        msg : value,
                        type : 1, //1 广播 2单聊
                    };
                } else {
                    object = {
                        to : to,
                        msg : value,
                        type : 2, //1 广播 2单聊
                    };
                }
                //将object转成json字符串发送给服务端
                var json = JSON.stringify(object);
                //alert("str="+json);
                ws.send(json);
                //消息发送后将消息栏清空
                $("#msg").val("");
            }
        </script>
    </head>
    <body>
    
    <h3>欢迎 ${sessionScope.username }使用本聊天系统!!</h3>
    
    <div id="content"
         style="border: 1px solid black;  400px; height: 300px; float: left; color: #7f3f00;"></div>
    <div id="userList"
         style="border: 1px solid black;  120px; height: 300px; float: left; color: #00ff00;"></div>
    
    <div style="clear: both;" style="color:#00ff00">
        <input id="msg" />
        <button onclick="send();">发送消息</button>
    </div>
    </body>
    </html>
    

    login.jsp:

    <%@ page language="java" contentType="text/html; charset=utf-8"
             pageEncoding="utf-8"%>
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>Insert title here</title>
        <script type="text/javascript"  src="jquery-1.4.4.min.js"></script>
    </head>
    <body>
    <form  name="ff"  action="LoginServlet" method="post" >
        用户名:<input name="username"  /><br/>
        <input type="submit"  value="登录"/>
    </form>
    </body>
    </html>
    

    项目演示:

  • 相关阅读:
    wince串口打印信息的屏蔽与打开
    2440 6.0BSP 移植过程之OAL
    2440 6.0BSP移植过程之电池驱动
    2440 6.0BSP移植过程之SD卡驱动
    如何在wince6.0 模拟器上跑以前编译好的EVC MFC程序
    2440 休眠唤醒的实现过程(作者:wogoyixikexie@gliet)
    CETK的使用(USB连接方式)
    如何扩展ARM内存(OEMGetExtensionDRAM和pNKEnumExtensionDRAM函数可以解决问题)
    ZLG7290(wince下)驱动之不停执行同一动作的解决办法(作者:wogoyixikexie@gliet)
    2440 中断优先级问题(作者wogoyixikexie@gliet)
  • 原文地址:https://www.cnblogs.com/charlypage/p/9484055.html
Copyright © 2020-2023  润新知