1. 程式人生 > >spring websocket 和socketjs實現單聊群聊,廣播的消息推送詳解

spring websocket 和socketjs實現單聊群聊,廣播的消息推送詳解

退出 rec oid -name 返回 classes sockets 們的 tomcat7.0

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; width: 400px; height: 300px; float: left; color: #7f3f00;"></div>
<div id="userList"
     style="border: 1px solid black; width: 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>

項目演示:

技術分享圖片

spring websocket 和socketjs實現單聊群聊,廣播的消息推送詳解