Java 集成WebSocket实现实时通讯

去年独立负责开发了一个小程序拼单的功能,要求多个设备同时在线点单,点单内容实时共享,最后再统一进行结算和支付。当时还支持付款人发起群收款等功能,这个功能以后再介绍。

之前用PHP集成Swoole写过视频直播的聊天功能,一开始准备使用Websocket来实现这个功能,但结合项目复杂性考虑,最后采用轮询购物车版本号的方式来实现这个功能。在面对实时性要求很高的功能,Websocket依然是很好的选择。

这里就简单将Websocket集成到SpringBoot中,简单实现聊天房间在线用户和消息列表。

在SpringBoot的pom.xml文件里面加入Websocket扩展包:

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

创建配置文件 WebsocketConfig,引入ServerEndpointExporter

package com.demo.www.config.websocket;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * WebSocket服务配置
 * @author AnYuan
 */

@Configuration
public class WebsocketConfig {

    /**
     * 注入一个ServerEndpointExporter
     * 该Bean会自动注册使用@ServerEndpoint注解申明的websocket endpoint
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

 创建一个的消息模版类,统一接受和发送消息的数据字段和类型:

package com.demo.www.config.websocket;

import lombok.Data;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

/**
 * 消息模版
 * @author AnYuan
 */

@Data
public class WebsocketMsgDTO {

    /**
     * 发送消息用户
     */
    private String uid;
    /**
     * 接收消息用户
     */
    private String toUId;
    /**
     * 消息内容
     */
    private String content;
    /**
     * 消息时间
     */
    private String dateTime;
    /**
     * 用户列表
     */
    private List<String> onlineUser;

    /**
     * 统一消息模版
     * @param uid 发送消息用户
     * @param content 消息内容
     * @param onlineUser 在线用户列表
     */
    public WebsocketMsgDTO(String uid, String content, List<String> onlineUser) {
        this.uid = uid;
        this.content = content;
        this.onlineUser = onlineUser;
        this.dateTime = localDateTimeToString();
    }


    /**
     * 获取当前时间
     * @return String 12:00:00
     */
    private String localDateTimeToString() {
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");
        return dateTimeFormatter.format( LocalDateTime.now());
    }
}

逻辑代码:@ServerEndpoint(value="") 这个是Websocket服务url前缀,{uid}类似于ResutFul风格的参数

package com.demo.www.config.websocket;

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * WebSocketServer服务
 * @author AnYuan
 */

@ServerEndpoint(value = "/webSocket/{uid}")
@Component
@Slf4j
public class WebSocketServer {

    /**
     * 机器人发言名称
     */
    private static final String SPOKESMAN_ADMIN = "机器人";

    /**
     * concurrent包的线程安全Set
     * 用来存放每个客户端对应的Session对象
     */
    private static final ConcurrentHashMap<String, Session> SESSION_POOLS = new ConcurrentHashMap<>();

    /**
     * 静态变量,用来记录当前在线连接数。
     * 应该把它设计成线程安全的。
     */
    private static final AtomicInteger ONLINE_NUM = new AtomicInteger();

    /**
     * 获取在线用户列表
     * @return List<String>
     */
    private List<String> getOnlineUsers() {
        return new ArrayList<>(SESSION_POOLS.keySet());
    }

    /**
     * 用户建立连接成功调用
     * @param session 用户集合
     * @param uid     用户标志
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "uid") String uid) {
        // 将加入连接的用户加入SESSION_POOLS集合
        SESSION_POOLS.put(uid, session);
        // 在线用户+1
        ONLINE_NUM.incrementAndGet();
        sendToAll(new WebsocketMsgDTO(SPOKESMAN_ADMIN, uid + " 加入连接!", getOnlineUsers()));
    }

    /**
     * 用户关闭连接时调用
     * @param uid 用户标志
     */
    @OnClose
    public void onClose(@PathParam(value = "uid") String uid) {
        // 将加入连接的用户移除SESSION_POOLS集合
        SESSION_POOLS.remove(uid);
        // 在线用户-1
        ONLINE_NUM.decrementAndGet();
        sendToAll(new WebsocketMsgDTO(SPOKESMAN_ADMIN, uid + " 断开连接!", getOnlineUsers()));
    }

    /**
     * 服务端收到客户端信息
     * @param message 客户端发来的string
     * @param uid     uid 用户标志
     */
    @OnMessage
    public void onMessage(String message, @PathParam(value = "uid") String uid) {
        log.info("Client:[{}], Message: [{}]", uid, message);

        // 接收并解析前端消息并加上时间,最后根据是否有接收用户,区别发送所有用户还是单个用户
        WebsocketMsgDTO msgDTO = JSONObject.parseObject(message, WebsocketMsgDTO.class);
        msgDTO.setDateTime(localDateTimeToString());

        // 如果有接收用户就发送单个用户
        if (Strings.isNotBlank(msgDTO.getToUId())) {
            sendMsgByUid(msgDTO);
            return;
        }
        // 否则发送所有人
        sendToAll(msgDTO);
    }

    /**
     * 给所有人发送消息
     * @param msgDTO msgDTO
     */
    private void sendToAll(WebsocketMsgDTO msgDTO) {
        //构建json消息体
        String content = JSONObject.toJSONString(msgDTO);
        // 遍历发送所有在线用户
        SESSION_POOLS.forEach((k, session) ->  sendMessage(session, content));
    }

    /**
     * 给指定用户发送信息
     */
    private void sendMsgByUid(WebsocketMsgDTO msgDTO) {
        sendMessage(SESSION_POOLS.get(msgDTO.getToUId()), JSONObject.toJSONString(msgDTO));
    }

    /**
     * 发送消息方法
     * @param session 用户
     * @param content 消息
     */
    private void sendMessage(Session session, String content){
        try {
            if (Objects.nonNull(session)) {
                // 使用Synchronized锁防止多次发送消息
                synchronized (session) {
                    // 发送消息
                    session.getBasicRemote().sendText(content);
                }
            }
        } catch (IOException ioException) {
            log.info("发送消息失败:{}", ioException.getMessage());
            ioException.printStackTrace();
        }
    }

    /**
     * 获取当前时间
     * @return String 12:00:00
     */
    private String localDateTimeToString() {
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");
        return dateTimeFormatter.format( LocalDateTime.now());
    }
}

启动后就开启了一个WebSocket后端服务了,前端再协议握手就可以了。

简单写一下前端样式和Js代码,创建一个Admin用户,一个user用户,同时连接这个WebSocket服务,实现展现在线用户和通告列表的功能。

第一个文件:admin.html

<!DOCTYPE html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<head>
    <title>Admin Hello WebSocket</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.2.1/jquery.js"></script>
    <script src="app.js"></script>
    <style>
        body {
            background-color: #f5f5f5;
        }
        #main-content {
            max-width: 940px;
            padding: 2em 3em;
            margin: 0 auto 20px;
            background-color: #fff;
            border: 1px solid #e5e5e5;
            -webkit-border-radius: 5px;
            -moz-border-radius: 5px;
            border-radius: 5px;
        }
    </style>
</head>
<body>
<div id="main-content" class="container">
    <div class="row">
        <div class="col-md-6">
            <form class="form-inline">
                <div class="form-group">
                    <input id="userId" value="Admin" hidden>
                    <label for="connect">建立连接通道:</label>
                    <button id="connect" class="btn btn-default" type="submit">Connect</button>
                    <button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect
                    </button>
                </div>
            </form>
        </div>
        <div class="col-md-6">
            <form class="form-inline">
                <div class="form-group">
                    <label>发布新公告</label>
                    <input type="text" id="content" class="form-control" value="" placeholder="发言框..">
                </div>
                <button id="send" class="btn btn-default" type="submit">发布</button>
            </form>
        </div>
    </div>
    <div class="row" style="margin-top: 30px">
        <div class="col-md-12">
            <table id="userlist" class="table table-striped">
                <thead>
                <tr>
                    <th>实时在线用户列表<span id="onLineUserCount"></span></th>
                </tr>
                </thead>
                <tbody id='online'>
                </tbody>
            </table>
        </div>
        <div class="col-md-12">
            <table id="conversation" class="table table-striped">
                <thead>
                <tr>
                    <th>游戏公告内容</th>
                </tr>
                </thead>
                <tbody id="notice">
                </tbody>
            </table>
        </div>
    </div>
</div>
</body>
</html>

第二个文件:user.html

<!DOCTYPE html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<head>
    <title>User1 Hello WebSocket</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.2.1/jquery.js"></script>
    <script src="app.js"></script>
    <style>
        body {
            background-color: #f5f5f5;
        }
        #main-content {
            max-width: 940px;
            padding: 2em 3em;
            margin: 0 auto 20px;
            background-color: #fff;
            border: 1px solid #e5e5e5;
            -webkit-border-radius: 5px;
            -moz-border-radius: 5px;
            border-radius: 5px;
        }
    </style>
</head>
<body>
<div id="main-content" class="container">
    <div class="row">
        <div class="col-md-6">
            <form class="form-inline">
                <div class="form-group">
                    <input id="userId" value="user1" hidden>
                    <label for="connect">建立连接通道:</label>
                    <button id="connect" class="btn btn-default" type="submit">Connect</button>
                    <button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect
                    </button>
                </div>
            </form>
        </div>

    </div>
    <div class="row" style="margin-top: 30px">
        <div class="col-md-12">
            <table id="userlist" class="table table-striped">
                <thead>
                <tr>
                    <th>实时在线用户列表<span id="onLineUserCount"></span></th>
                </tr>
                </thead>
                <tbody id='online'>
                </tbody>
            </table>
        </div>
        <div class="col-md-12">
            <table id="conversation" class="table table-striped">
                <thead>
                <tr>
                    <th>游戏公告内容</th>
                </tr>
                </thead>
                <tbody id="notice">
                </tbody>
            </table>
        </div>
    </div>
</div>
</body>
</html>

最后重点的Js文件:app.js。 将其与admin.html、user.html放在同一个目录下即可引用

var socket;

function setConnected(connected) {
    $("#connect").prop("disabled", connected);
    $("#disconnect").prop("disabled", !connected);
    if (connected) {
        $("#conversation").show();
    } else {
        $("#conversation").hide();
    }
    $("#notice").html("");
}
// WebSocket 服务操作 
function openSocket() {
    if (typeof (WebSocket) == "undefined") {
        console.log("浏览器不支持WebSocket");
    } else {
        console.log("浏览器支持WebSocket");
        //实现化WebSocket对象,指定要连接的服务器地址与端口  建立连接
        if (socket != null) {
            socket.close();
            socket = null;
        }
        // ws 为websocket连接标识,localhost:9999 为SpringBoot的连接地址,webSocket 为后端配置的前缀, userId 则是参数
        socket = new WebSocket("ws://localhost:9999/webSocket/" + $("#userId").val());
        //打开事件
        socket.onopen = function () {
            console.log("websocket已打开");
            setConnected(true)
        };
        //获得消息事件
        socket.onmessage = function (msg) {
            const msgDto = JSON.parse(msg.data);
            console.log(msg)
            showContent(msgDto);
            showOnlineUser(msgDto.onlineUser);
        };
        //关闭事件
        socket.onclose = function () {
            console.log("websocket已关闭");
            setConnected(false)
            removeOnlineUser();
        };
        //发生了错误事件
        socket.onerror = function () {
            setConnected(false)
            console.log("websocket发生了错误");
        }
    }
}

//2、关闭连接
function disconnect() {
    if (socket !== null) {
        socket.close();
    }
    setConnected(false);
    console.log("Disconnected");
}

function sendMessage() {
    if (typeof (WebSocket) == "undefined") {
        console.log("您的浏览器不支持WebSocket");
    } else {
        var msg = '{"uid":"' + $("#userId").val() + '", "toUId": null, "content":"' + $("#content").val() + '"}';
        console.log("向服务端发送消息体:" + msg);
        socket.send(msg);
    }
}

// 订阅的消息显示在客户端指定位置
function showContent(serverMsg) {
    $("#notice").html("<tr><td>" + serverMsg.uid + ": </td> <td>" + serverMsg.content + "</td><td>" + serverMsg.dateTime + "</td></tr>" +  $("#notice").html())
}

//显示实时在线用户
function showOnlineUser(serverMsg) {
    if (null != serverMsg) {
        let html = '';
        for (let i = 0; i < serverMsg.length; i++) {
            html += "<tr><td>" + serverMsg[i] + "</td></tr>";
        }
        $("#online").html(html);
        $("#onLineUserCount").html(" ( " + serverMsg.length + " )");
    }
}

//显示实时在线用户
function removeOnlineUser() {
    $("#online").html("");
    $("#onLineUserCount").html("");
}

$(function () {
    $("form").on('submit', function (e) {
        e.preventDefault();
    });
    $("#connect").click(function () {
        openSocket();
    });
    $("#disconnect").click(function () {
        disconnect();
    });
    $("#send").click(function () {
        sendMessage();
    });
});

打开admin.html和user.html页面:

分别点击Connect连接Websocket服务,然后使用admin页面的[发布新通告]进行消息发布:

这里简单实现了管理员群发消息,可以通过修改用户列表的样式,增加一对一聊天的功能,然后在app.js里,发送消息时指定发送对象字段 [toUId] 就可以实现一对一聊天了

 

本篇代码Github:https://github.com/Journeyerr/cnblogs/tree/master/websocket

posted @ 2021-06-15 18:13  安逺  阅读(3163)  评论(0)    收藏  举报