1. 程式人生 > >WebSocket+Java 私聊、群聊例項

WebSocket+Java 私聊、群聊例項

  前言

  之前寫畢業設計的時候就想加上聊天系統,當時已經用ajax長輪詢實現了一個(還不懂什麼是輪詢機制的,猛戳這裡:https://www.cnblogs.com/hoojo/p/longPolling_comet_jquery_iframe_ajax.html),但由於種種原因沒有加到畢設裡面。後來回校答辯後研究了一下websocket,並參照網上資料寫了一個簡單的聊天,現在又重新整理並記錄下來。   以下介紹來自維基百科:   WebSocket是一種在單個TCP連線上進行全雙工通訊的協議。WebSocket通訊協議於2011年被IETF定為標準RFC 6455,並由RFC7936補充規範。WebSocket API也被W3C定為標準。

WebSocket使得客戶端和伺服器之間的資料交換變得更加簡單,允許服務端主動向客戶端推送資料。在WebSocket API中,瀏覽器和伺服器只需要完成一次握手,兩者之間就直接可以建立永續性的連線,並進行雙向資料傳輸。   這裡可以看一下官網介紹:http://www.websocket.org/aboutwebsocket.html   官網裡面介紹非常詳細,我就不做搬運工了,要是有像我一樣英語不好的同學,右鍵->翻譯成簡體中文   spring對websocket的支援:https://docs.spring.io/spring/docs/4.3.13.RELEASE/spring-framework-reference/htmlsingle/#websocket

  效果

  上下線有提示

  如果這時候傳送訊息給離線的人,則會收到系統提示訊息

  群聊

  huanzi一發送群聊,laowang跟xiaofang都不是在當前群聊視窗,出現小圓點+1

  huanzi一發送群聊,xiaofang在當前群聊視窗,直接追加訊息,老王不在對應的聊天視窗,出現小圓點+1

  xiaofang回覆,huanzi直接追加訊息,laowang依舊小圓點+1

  laowang點選群聊視窗,小圓點消失,追加群聊訊息

  laowang參與群聊

  xiaofang切出群聊視窗,laowang在群聊傳送訊息,xiaofang出現小圓點+1

  切回來,小圓點消失,聊天資料正常接收追加

  三方正常參與聊天

  私聊

  huanzis私聊xiaofang,xiaofang聊天視窗在群聊,小圓點+1,而laowang不受影響

   xiaofang切到私聊視窗,小圓點消失,資料正常追加;huanzi剛好處於私聊視窗,資料直接追加

   效果演示到此結束,下面貼出程式碼

  程式碼編寫

  首先先介紹一下專案背景:springboot + thymeleaf ,websocket用的是spring的

  maven

        <!-- springboot websocket -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

        <!--fast json -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.1.41</version>
        </dependency>
socketChart.css樣式
body{
    background-color: #efebdc;
}
#hz-main{
    width: 700px;
    height: 500px;
    background-color: red;
    margin: 0 auto;
}

#hz-message{
    width: 500px;
    height: 500px;
    float: left;
    background-color: #B5B5B5;
}

#hz-message-body{
    width: 460px;
    height: 340px;
    background-color: #E0C4DA;
    padding: 10px 20px;
    overflow:auto;
}

#hz-message-input{
    width: 500px;
    height: 99px;
    background-color: white;
    overflow:auto;
}

#hz-group{
    width: 200px;
    height: 500px;
    background-color: rosybrown;
    float: right;
}

.hz-message-list{
    min-height: 30px;
    margin: 10px 0;
}
.hz-message-list-text{
    padding: 7px 13px;
    border-radius: 15px;
    width: auto;
    max-width: 85%;
    display: inline-block;
}

.hz-message-list-username{
    margin: 0;
}

.hz-group-body{
    overflow:auto;
}

.hz-group-list{
    padding: 10px;
}

.left{
    float: left;
    color: #595a5a;
    background-color: #ebebeb;
}
.right{
    float: right;
    color: #f7f8f8;
    background-color: #919292;
}
.hz-badge{
    width: 20px;
    height: 20px;
    background-color: #FF5722;
    border-radius: 50%;
    float: right;
    color: white;
    text-align: center;
    line-height: 20px;
    font-weight: bold;
    opacity: 0;
}
socketChart.html頁面
<!DOCTYPE>
<!--解決idea thymeleaf 表示式模板報紅波浪線-->
<!--suppress ALL -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>聊天頁面</title>
    <!-- jquery線上版本 -->
    <script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
    <!--引入樣式-->
    <link th:href="@{/css/socketChart.css}" rel="stylesheet" type="text/css"/>
</head>
<body>
<div id="hz-main">
    <div id="hz-message">
        <!-- 頭部 -->
        正在與<span id="toUserName"></span>聊天
        <hr style="margin: 0px;"/>
        <!-- 主體 -->
        <div id="hz-message-body">
        </div>
        <!-- 功能條 -->
        <div id="">
            <button>表情</button>
            <button>圖片</button>
            <button id="videoBut">視訊</button>
            <button onclick="send()" style="float: right;">傳送</button>
        </div>
        <!-- 輸入框 -->
        <div contenteditable="true" id="hz-message-input">

        </div>
    </div>
    <div id="hz-group">
        登入使用者:<span id="talks" th:text="${username}">請登入</span>
        <br/>
        線上人數:<span id="onlineCount">0</span>
        <!-- 主體 -->
        <div id="hz-group-body">

        </div>
    </div>
</div>
</body>
<script type="text/javascript" th:inline="javascript">
    //專案根路徑
    var ctx = /*[[@{/}]]*/'';

    //登入名
    var username = /*[[${username}]]*/'';
</script>
<script th:src="@{/js/socketChart.js}"></script>
</html>
socketChart.js 邏輯程式碼
    //訊息物件陣列
    var msgObjArr = new Array();

    var websocket = null;

    //判斷當前瀏覽器是否支援WebSocket, springboot是專案名
    if ('WebSocket' in window) {
        websocket = new WebSocket("ws://localhost:10086/springboot/websocket/"+username);
    } else {
        console.error("不支援WebSocket");
    }

    //連線發生錯誤的回撥方法
    websocket.onerror = function (e) {
        console.error("WebSocket連線發生錯誤");
    };

    //連線成功建立的回撥方法
    websocket.onopen = function () {
        //獲取所有線上使用者
        $.ajax({
            type: 'post',
            url: ctx + "/websocket/getOnlineList",
            contentType: 'application/json;charset=utf-8',
            dataType: 'json',
            data: {username:username},
            success: function (data) {
                if (data.length) {
                    //列表
                    for (var i = 0; i < data.length; i++) {
                        var userName = data[i];
                        $("#hz-group-body").append("<div class=\"hz-group-list\"><span class='hz-group-list-username'>" + userName + "</span><span id=\"" + userName + "-status\">[線上]</span><div id=\"hz-badge-" + userName + "\" class='hz-badge'>0</div></div>");
                    }

                    //線上人數
                    $("#onlineCount").text(data.length);
                }
            },
            error: function (xhr, status, error) {
                console.log("ajax錯誤!");
            }
        });
    }

    //接收到訊息的回撥方法
    websocket.onmessage = function (event) {
        var messageJson = eval("(" + event.data + ")");

        //普通訊息(私聊)
        if (messageJson.type == "1") {
            //來源使用者
            var srcUser = messageJson.srcUser;
            //目標使用者
            var tarUser = messageJson.tarUser;
            //訊息
            var message = messageJson.message;

            //最加聊天資料
            setMessageInnerHTML(srcUser.username,srcUser.username, message);
        }

        //普通訊息(群聊)
        if (messageJson.type == "2"){
            //來源使用者
            var srcUser = messageJson.srcUser;
            //目標使用者
            var tarUser = messageJson.tarUser;
            //訊息
            var message = messageJson.message;

            //最加聊天資料
            setMessageInnerHTML(username,tarUser.username, message);
        }

        //對方不線上
        if (messageJson.type == "0"){
            //訊息
            var message = messageJson.message;

            $("#hz-message-body").append(
                "<div class=\"hz-message-list\" style='text-align: center;'>" +
                    "<div class=\"hz-message-list-text\">" +
                        "<span>" + message + "</span>" +
                    "</div>" +
                "</div>");
        }

        //線上人數
        if (messageJson.type == "onlineCount") {
            //取出username
            var onlineCount = messageJson.onlineCount;
            var userName = messageJson.username;
            var oldOnlineCount = $("#onlineCount").text();

            //新舊線上人數對比
            if (oldOnlineCount < onlineCount) {
                if($("#" + userName + "-status").length > 0){
                    $("#" + userName + "-status").text("[線上]");
                }else{
                    $("#hz-group-body").append("<div class=\"hz-group-list\"><span class='hz-group-list-username'>" + userName + "</span><span id=\"" + userName + "-status\">[線上]</span><div id=\"hz-badge-" + userName + "\" class='hz-badge'>0</div></div>");
                }
            } else {
                //有人下線
                $("#" + userName + "-status").text("[離線]");
            }
            $("#onlineCount").text(onlineCount);
        }

    }

    //連線關閉的回撥方法
    websocket.onclose = function () {
        //alert("WebSocket連線關閉");
    }

    //將訊息顯示在對應聊天視窗    對於接收訊息來說這裡的toUserName就是來源使用者,對於傳送來說則相反
    function setMessageInnerHTML(srcUserName,msgUserName, message) {
        //判斷
        var childrens = $("#hz-group-body").children(".hz-group-list");
        var isExist = false;
        for (var i = 0; i < childrens.length; i++) {
            var text = $(childrens[i]).find(".hz-group-list-username").text();
            if (text == srcUserName) {
                isExist = true;
                break;
            }
        }
        if (!isExist) {
            //追加聊天物件
            msgObjArr.push({
                toUserName: srcUserName,
                message: [{username: msgUserName, message: message, date: NowTime()}]//封裝資料
            });
            $("#hz-group-body").append("<div class=\"hz-group-list\"><span class='hz-group-list-username'>" + srcUserName + "</span><span id=\"" + srcUserName + "-status\">[線上]</span><div id=\"hz-badge-" + srcUserName + "\" class='hz-badge'>0</div></div>");
        } else {
            //取出物件
            var isExist = false;
            for (var i = 0; i < msgObjArr.length; i++) {
                var obj = msgObjArr[i];
                if (obj.toUserName == srcUserName) {
                    //儲存最新資料
                    obj.message.push({username: msgUserName, message: message, date: NowTime()});
                    isExist = true;
                    break;
                }
            }
            if (!isExist) {
                //追加聊天物件
                msgObjArr.push({
                    toUserName: srcUserName,
                    message: [{username: msgUserName, message: message, date: NowTime()}]//封裝資料
                });
            }
        }

        // 對於接收訊息來說這裡的toUserName就是來源使用者,對於傳送來說則相反
        var username = $("#toUserName").text();

        //剛好開啟的是對應的聊天頁面
        if (srcUserName == username) {
            $("#hz-message-body").append(
                "<div class=\"hz-message-list\">" +
                    "<p class='hz-message-list-username'>"+msgUserName+":</p>" +
                "<div class=\"hz-message-list-text left\">" +
                    "<span>" + message + "</span>" +
                "</div>" +
                "<div style=\" clear: both; \"></div>" +
                "</div>");
        } else {
            //小圓點++
            var conut = $("#hz-badge-" + srcUserName).text();
            $("#hz-badge-" + srcUserName).text(parseInt(conut) + 1);
            $("#hz-badge-" + srcUserName).css("opacity", "1");
        }
    }

    //傳送訊息
    function send() {
        //訊息
        var message = $("#hz-message-input").html();
        //目標使用者名稱
        var tarUserName = $("#toUserName").text();
        //登入使用者名稱
        var srcUserName = $("#talks").text();
        websocket.send(JSON.stringify({
            "type": "1",
            "tarUser": {"username": tarUserName},
            "srcUser": {"username": srcUserName},
            "message": message
        }));
        $("#hz-message-body").append(
            "<div class=\"hz-message-list\">" +
            "<div class=\"hz-message-list-text right\">" +
            "<span>" + message + "</span>" +
            "</div>" +
            "</div>");
        $("#hz-message-input").html("");
        //取出物件
        if (msgObjArr.length > 0) {
            var isExist = false;
            for (var i = 0; i < msgObjArr.length; i++) {
                var obj = msgObjArr[i];
                if (obj.toUserName == tarUserName) {
                    //儲存最新資料
                    obj.message.push({username: srcUserName, message: message, date: NowTime()});
                    isExist = true;
                    break;
                }
            }
            if (!isExist) {
                //追加聊天物件
                msgObjArr.push({
                    toUserName: tarUserName,
                    message: [{username: srcUserName, message: message, date: NowTime()}]//封裝資料[{username:huanzi,message:"你好,我是歡子!",date:2018-04-29 22:48:00}]
                });
            }
        } else {
            //追加聊天物件
            msgObjArr.push({
                toUserName: tarUserName,
                message: [{username: srcUserName, message: message, date: NowTime()}]//封裝資料[{username:huanzi,message:"你好,我是歡子!",date:2018-04-29 22:48:00}]
            });
        }
    }

    //監聽點選使用者
    $("body").on("click", ".hz-group-list", function () {
        $(".hz-group-list").css("background-color", "");
        $(this).css("background-color", "whitesmoke");
        $("#toUserName").text($(this).find(".hz-group-list-username").text());

        //清空舊資料,從物件中取出並追加
        $("#hz-message-body").empty();
        $("#hz-badge-" + $("#toUserName").text()).text("0");
        $("#hz-badge-" + $("#toUserName").text()).css("opacity", "0");
        if (msgObjArr.length > 0) {
            for (var i = 0; i < msgObjArr.length; i++) {
                var obj = msgObjArr[i];
                if (obj.toUserName == $("#toUserName").text()) {
                    //追加資料
                    var messageArr = obj.message;
                    if (messageArr.length > 0) {
                        for (var j = 0; j < messageArr.length; j++) {
                            var msgObj = messageArr[j];
                            var leftOrRight = "right";
                            var message = msgObj.message;
                            var msgUserName = msgObj.username;
                            var toUserName = $("#toUserName").text();

                            //當聊天視窗與msgUserName的人相同,文字在左邊(對方/其他人),否則在右邊(自己)
                            if (msgUserName == toUserName) {
                                leftOrRight = "left";
                            }

                            //但是如果點選的是自己,群聊的邏輯就不太一樣了
                            if (username == toUserName && msgUserName != toUserName) {
                                leftOrRight = "left";
                            }

                            if (username == toUserName && msgUserName == toUserName) {
                                leftOrRight = "right";
                            }

                            var magUserName = leftOrRight == "left" ? "<p class='hz-message-list-username'>"+msgUserName+":</p>" : "";

                            $("#hz-message-body").append(
                                "<div class=\"hz-message-list\">" +
                                magUserName+
                                "<div class=\"hz-message-list-text " + leftOrRight + "\">" +
                                    "<span>" + message + "</span>" +
                                "</div>" +
                                "<div style=\" clear: both; \"></div>" +
                                "</div>");
                        }
                    }
                    break;
                }
            }
        }
    });

    //獲取當前時間
    function NowTime() {
        var time = new Date();
        var year = time.getFullYear();//獲取年
        var month = time.getMonth() + 1;//或者月
        var day = time.getDate();//或者天
        var hour = time.getHours();//獲取小時
        var minu = time.getMinutes();//獲取分鐘
        var second = time.getSeconds();//或者秒
        var data = year + "-";
        if (month < 10) {
            data += "0";
        }
        data += month + "-";
        if (day < 10) {
            data += "0"
        }
        data += day + " ";
        if (hour < 10) {
            data += "0"
        }
        data += hour + ":";
        if (minu < 10) {
            data += "0"
        }
        data += minu + ":";
        if (second < 10) {
            data += "0"
        }
        data += second;
        return data;
    }

  java程式碼有三個類,MyEndpointConfigure,WebSocketConfig,WebSocketServer;

  MyEndpointConfigure

/**
 * 解決注入其他類的問題,詳情參考這篇帖子:webSocket無法注入其他類:https://blog.csdn.net/tornadojava/article/details/78781474
 */
public class MyEndpointConfigure extends ServerEndpointConfig.Configurator implements ApplicationContextAware {

    private static volatile BeanFactory context;

    @Override
    public <T> T getEndpointInstance(Class<T> clazz){
        return context.getBean(clazz);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        MyEndpointConfigure.context = applicationContext;
    }
}

  WebSocketConfig

/**
 * WebSocket配置
 */
@Configuration
public class WebSocketConfig{


    /**
     * 用途:掃描並註冊所有攜帶@ServerEndpoint註解的例項。 @ServerEndpoint("/websocket")
     * PS:如果使用外部容器 則無需提供ServerEndpointExporter。
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

    /**
     * 支援注入其他類
     */
    @Bean
    public MyEndpointConfigure  newMyEndpointConfigure (){
        return new MyEndpointConfigure ();
    }
}

  WebSocketServer

/**
 * WebSocket服務
 */
@RestController
@RequestMapping("/websocket")
@ServerEndpoint(value = "/websocket/{username}", configurator = MyEndpointConfigure.class)
public class WebSocketServer {

    /**
     * 線上人數
     */
    private static int onlineCount = 0;

    /**
     * 線上使用者的Map集合,key:使用者名稱,value:Session物件
     */
    private static Map<String, Session> sessionMap = new HashMap<String, Session>();

    /**
     * 注入其他類(換成自己想注入的物件)
     */
    @Autowired
    private UserService userService;

    /**
     * 連線建立成功呼叫的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("username") String username) {
        //在webSocketMap新增上線使用者
        sessionMap.put(username, session);

        //線上人數加加
        WebSocketServer.onlineCount++;

        //通知除了自己之外的所有人
        sendOnlineCount(session, "{'type':'onlineCount','onlineCount':" + WebSocketServer.onlineCount + ",username:'" + username + "'}");
    }

    /**
     * 連線關閉呼叫的方法
     */
    @OnClose
    public void onClose(Session session) {
        //下線使用者名稱
        String logoutUserName = "";

        //從webSocketMap刪除下線使用者
        for (Entry<String, Session> entry : sessionMap.entrySet()) {
            if (entry.getValue() == session) {
                sessionMap.remove(entry.getKey());
                logoutUserName = entry.getKey();
                break;
            }
        }
        //線上人數減減
        WebSocketServer.onlineCount--;

        //通知除了自己之外的所有人
        sendOnlineCount(session, "{'type':'onlineCount','onlineCount':" + WebSocketServer.onlineCount + ",username:'" + logoutUserName + "'}");
    }

    /**
     * 伺服器接收到客戶端訊息時呼叫的方法
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        try {
            //JSON字串轉 HashMap
            HashMap hashMap = new ObjectMapper().readValue(message, HashMap.class);

            //訊息型別
            String type = (String) hashMap.get("type");

            //來源使用者
            Map srcUser = (Map) hashMap.get("srcUser");

            //目標使用者
            Map tarUser = (Map) hashMap.get("tarUser");

            //如果點選的是自己,那就是群聊
            if (srcUser.get("username").equals(tarUser.get("username"))) {
                //群聊
                groupChat(session,hashMap);
            } else {
                //私聊
                privateChat(session, tarUser, hashMap);
            }

            //後期要做訊息持久化

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 發生錯誤時呼叫
     */
    @OnError
    public void onError(Session session, Throwable error) {
        error.printStackTrace();
    }

    /**
     * 通知除了自己之外的所有人
     */
    private void sendOnlineCount(Session session, String message) {
        for (Entry<String, Session> entry : sessionMap.entrySet()) {
            try {
                if (entry.getValue() != session) {
                    entry.getValue().getBasicRemote().sendText(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 私聊
     */
    private void privateChat(Session session, Map tarUser, HashMap hashMap) throws IOException {
        //獲取目標使用者的session
        Session tarUserSession = sessionMap.get(tarUser.get("username"));

        //如果不線上則傳送“對方不線上”回來源使用者
        if (tarUserSession == null) {
            session.getBasicRemote().sendText("{\"type\":\"0\",\"message\":\"對方不線上\"}");
        } else {
            hashMap.put("type", "1");
            tarUserSession.getBasicRemote().sendText(new ObjectMapper().writeValueAsString(hashMap));
        }
    }

    /**
     * 群聊
     */
    private void groupChat(Session session,HashMap hashMap) throws IOException {
        for (Entry<String, Session> entry : sessionMap.entrySet()) {
            //自己就不用再發送訊息了
            if (entry.getValue() != session) {
                hashMap.put("type", "2");
                entry.getValue().getBasicRemote().sendText(new ObjectMapper().writeValueAsString(hashMap));
            }
        }
    }

    /**
     * 登入
     */
    @RequestMapping("/login/{username}")
    public ModelAndView login(HttpServletRequest request, @PathVariable String username) {
        return new ModelAndView("socketChart.html", "username", username);
    }

    /**
     * 登出
     */