【Springboot实例】WebSocket即时聊天室设计与实现

一、基本介绍

1.1 什么是WebSocket?

WebSocket 是 HTML5 提供的一种在单个 TCP 连接上进行全双工通讯的协议。它是客户端与服务器端的交互更加简便,在客户端与服务器端建立连接后,两者可以创建持久的连接,服务器端可主动发送数据给客户端,并进行双向通信。

1.2 与传统Ajax轮查的区别?

传统的Ajax技术是在设立一定的时间间隔后(比如1s)对服务器端发送HTTP请求进行查询,这种轮查是有很明显的问题,那就是浪费带宽资源,与传统Ajax轮查对比下,WebSocket技术对资源的使用就更加合理了。

【Springboot实例】WebSocket即时聊天室设计与实现_第1张图片

二、WebSocket功能介绍

2.1 HTML

2.1.1 HTML中WebSocket方法属性

方法 处理程序 描述
open socket.onopen 与服务器连接时触发
close socket.onclose 与服务器断开时触发
message socket.onmessage 收到服务器发送消息时触发
error socket.onerror 通信错误时触发

2.1.2 HTML中WebSocket基本方法

方法 描述
socket.send() 发送数据
socket.close() 关闭连接

2.1.2 HTML中WebSocket使用方法

  1. 实例化一个WebSocket对象
  2. 指定要连接的端口
  3. 注意端口连接名称(ws:http请求;wss:https请求),注意证书名

2.2 Springboot

2.2.1 pom.xml文件添加WebSocket方法

<dependency>
	<groupId>org.springframework.bootgroupId>
	<artifactId>spring-boot-starter-websocketartifactId>
dependency>

2.2.2 Springboot中WebSocket属性

注解方法 描述
@ServerEndpoint 声明接口的注解
@PostConstruct 初始化调用的方法
@OnOpen 连接建立成功调用的方法
@OnClose 连接关闭调用的方法
@OnMessage 收到客户端消息后调用的方法
@OnError 出现错误调用的方法

三、即时聊天室设计实例代码

3.1后端代码

3.1.1 WebSocketServer

package cn.chairc.platform.utils;

import cn.chairc.platform.config.WebSocketConfig;
import cn.chairc.platform.entity.Chat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @auther chairc
 * @date 2021/2/8 15:34
 */
@ServerEndpoint(value = "/chatOnline/websocket", encoders = {WebSocketServerEncoder.class}, configurator = WebSocketConfig.class)
@Component
public class WebSocketServer {

    private static Logger log = LoggerFactory.getLogger(WebSocketServer.class); //slf4j

    private static final AtomicInteger onlineCount = new AtomicInteger(0);
    //当前在线数
    private static int cnt;
    // concurrent包的线程安全Set,用来存放每个客户端对应的Session对象。
    private static CopyOnWriteArraySet<Session> SessionSet = new CopyOnWriteArraySet<Session>();

    /**
     * 初始化
     */

    @PostConstruct
    public void init() {
        log.info("websocket 已加载加载!!!");
    }

    /**
     * 连接建立成功调用的方法
     *
     * @param session 加入连接的session
     * @throws IOException IO异常
     */

    @OnOpen
    public void onOpen(Session session) throws IOException {
        Chat chat = new Chat();
        SessionSet.add(session);    //在数据集中添加新打开的session
        cnt = onlineCount.incrementAndGet();    //当前在线数加1
        log.info("有连接加入,当前连接数为:{}", cnt);
        chat.setChatPrivateId("SystemIn");
        String username = (String) session.getUserProperties().get("username");
        String sid = (String) session.getUserProperties().get("sid");
        chat.setChatText("当前" + username + "(" + sid + ")进入聊天室");
        BroadCastInfo(chat);
    }

    /**
     * 连接关闭调用的方法
     *
     * @param session 加入连接的session
     * @throws IOException IO异常
     */

    @OnClose
    public void onClose(Session session) throws IOException {
        Chat chat = new Chat();
        SessionSet.remove(session);     //在数据集中移除关闭调用的session
        cnt = onlineCount.decrementAndGet();    //当前在线数减1
        chat.setChatPrivateId("SystemOut");
        log.info("有连接关闭,当前连接数为:{}", cnt);
        String username = (String) session.getUserProperties().get("username");
        String sid = (String) session.getUserProperties().get("sid");
        chat.setChatText("当前" + username + "(" + sid + ")离开聊天室");
        BroadCastInfo(chat);
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     * @param session 加入连接的session
     */

    @OnMessage
    public void onMessage(String message, Session session) {
        Chat chat = new Chat();
        //chat.setChatText("收到消息,消息内容:" + message);
        //log.info("来自客户端的消息:{}", message);
        SendMessage(session, chat);
    }

    /**
     * 出现错误
     *
     * @param session 加入连接的session
     * @param error   错误
     */

    @OnError
    public void onError(Session session, Throwable error) {
        //log.error("发生错误:{},Session ID: {}", error.getMessage(), session.getId());
        error.printStackTrace();
    }

    /**
     * 发送消息
     *
     * @param session 加入连接的session
     * @param chat    聊天
     */

    private static void SendMessage(Session session, Chat chat) {
        try {
            //session.getBasicRemote().sendText(String.format("%s (来自服务器,Session ID=%s)", chat.getChat_text(), session.getId()));
            //ObjectMapper objectMapper = new ObjectMapper();
            //session.getBasicRemote().sendText(objectMapper.writeValueAsString(chat));
            chat.setChatroomPeople(cnt);
            session.getBasicRemote().sendObject(chat);//需要解码器
        } catch (IOException | EncodeException e) {
            log.error("发送消息出错:{}", e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 群发消息(单对多聊天)
     *
     * @param chat 聊天
     * @throws IOException IO异常
     */

    public static void BroadCastInfo(Chat chat) throws IOException {
        for (Session session : SessionSet) {
            if (session.isOpen()) {
                SendMessage(session, chat);
            }
        }
    }

    /**
     * 指定Session发送消息(单对单聊天)
     *
     * @param sessionId 加入连接的session
     * @param chat      聊天
     * @throws IOException IO异常
     */

    public static void SendMessage(Chat chat, String sessionId) throws IOException {
        Session session = null;
        for (Session s : SessionSet) {
            if (s.getId().equals(sessionId)) {
                session = s;
                break;
            }
        }
        if (session != null) {
            SendMessage(session, chat);
        } else {
            log.warn("没有找到你指定ID的会话:{}", sessionId);
        }
    }

}

3.1.2 ChatController

package cn.chairc.platform.controller;

import cn.chairc.platform.entity.Chat;
import cn.chairc.platform.entity.ResultSet;
import cn.chairc.platform.service.UserService;
import cn.chairc.platform.service.ChatService;
import cn.chairc.platform.utils.CommonUtil;
import cn.chairc.platform.utils.TimeUtil;
import org.apache.commons.text.StringEscapeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.text.ParseException;

/**
 * @auther chairc
 * @date 2021/2/8 15:46
 */
@Controller
@RequestMapping("/api/websocket")
public class ChatController {

    private static Logger log = LoggerFactory.getLogger(ChatController.class); //slf4j

    private UserService userService;

    private ChatService chatService;

    @Autowired
    public ChatController(UserService userService, ChatService chatService) {
        this.userService = userService;
        this.chatService = chatService;
    }

    @Value("${upload-file.head-file-path}")
    private String UPLOAD_HEAD_PATH;

    @Value("${head-image.user-head-image-path}")
    private String USER_HEAD_IMAGE_PATH;

    /**
     * 群发消息
     *
     * @param message 消息
     * @return ResultSet结果
     */

    @RequestMapping("/sendAll")
    @ResponseBody
    public ResultSet sendAllMessage(@RequestParam(required = true, value = "chatText") String message,
                                    HttpServletRequest request) throws ParseException {
        String headUrl;
        Chat chat = new Chat();
        chat.setChatPrivateId(CommonUtil.createRandomPrivateId("chat"));
        chat.setChatUserPrivateId(CommonUtil.sessionValidate("privateId"));
        chat.setChatUsername(CommonUtil.sessionValidate("username"));
        chat.setChatText(StringEscapeUtils.escapeHtml3(message));
        chat.setChatTime(TimeUtil.exchangeTimeTypeDateToString(TimeUtil.getServerTime()));
        chat.setChatIp(CommonUtil.getUserIp(request));
        chat.setChatBrowser(CommonUtil.getBrowserVersion(request));
        chat.setChatSystem(CommonUtil.getSystemVersion(request));
        File file = new File(UPLOAD_HEAD_PATH + CommonUtil.sessionValidate("privateId") + "thumbnail.jpg");
        if (file.exists()) {
            headUrl = USER_HEAD_IMAGE_PATH + CommonUtil.sessionValidate("privateId") + "thumbnail.jpg?r=" + (int) (Math.random() * 10000);
        } else {
            headUrl = USER_HEAD_IMAGE_PATH + "default-head-image.svg?=" + (int) (Math.random() * 10000);
        }
        chat.setHeaderUrl(headUrl);
        return chatService.sendChatAll(message, chat);
    }
}

3.1.3 ChatService

package cn.chairc.platform.service;

import cn.chairc.platform.entity.Chat;
import cn.chairc.platform.entity.ResultSet;


/**
 * @auther chairc
 * @date 2021/2/8 15:38
 */
public interface ChatService {

    /**
     * 群发消息
     *
     * @param message 消息
     * @param chat chat
     * @return ResultSet结果集
     */

    ResultSet sendChatAll(String message, Chat chat);
}

3.1.4 ChatServiceImpl

package cn.chairc.platform.service.impl;

import cn.chairc.platform.entity.Chat;
import cn.chairc.platform.entity.ResultSet;
import cn.chairc.platform.service.ChatService;
import cn.chairc.platform.utils.CommonUtil;
import cn.chairc.platform.utils.TimeUtil;
import cn.chairc.platform.utils.WebSocketServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.IOException;
import java.text.ParseException;

/**
 * @auther chairc
 * @date 2021/2/8 15:38
 */

@Service
public class ChatServiceImpl implements ChatService {

    private static Logger log = LoggerFactory.getLogger(ChatServiceImpl.class); //slf4j

    /**
     * 群发消息
     *
     * @param message 消息
     * @param chat    chat
     * @return ResultSet结果集
     */

    @Override
    public ResultSet sendChatAll(String message, Chat chat) {
        ResultSet resultSet = new ResultSet();
        if (CommonUtil.isUserOnline()) {
            try {
                WebSocketServer.BroadCastInfo(chat);
                resultSet.ok("ok");
                log.info("用户{}发送消息({})成功,ip{},浏览器{},系统{}", chat.getChatUsername(), chat.getChatText(),
                        chat.getChatIp(), chat.getChatBrowser(), chat.getChatSystem());
            } catch (IOException e) {
                log.error(e.toString());
            }
        } else {
            //未登录
            log.info("发送消息失败,用户未登录");
            resultSet.fail("用户未登录");
        }
        return resultSet;
    }
}

3.1.5 Chat

package cn.chairc.platform.entity;

/**
 * @auther chairc
 * @date 2021/2/8 15:36
 */
public class Chat {
    private String chatPrivateId;           //聊天信息私有ID
    private String chatUserPrivateId;       //聊天用户私有ID
    private String chatUsername = "System"; //聊天用户名
    private String chatText;                //聊天文本
    private String chatTime;                //时间
    private String chatIp;                  //IP
    private String chatBrowser;             //浏览器
    private String chatSystem;              //系统
    private int chatroomPeople;             //聊天室用户数
    private String headerUrl;               //头像

    public String getChatPrivateId() {
        return chatPrivateId;
    }

    public void setChatPrivateId(String chatPrivateId) {
        this.chatPrivateId = chatPrivateId;
    }

    public String getChatUserPrivateId() {
        return chatUserPrivateId;
    }

    public void setChatUserPrivateId(String chatUserPrivateId) {
        this.chatUserPrivateId = chatUserPrivateId;
    }

    public String getChatUsername() {
        return chatUsername;
    }

    public void setChatUsername(String chatUsername) {
        this.chatUsername = chatUsername;
    }

    public String getChatText() {
        return chatText;
    }

    public void setChatText(String chatText) {
        this.chatText = chatText;
    }

    public String getChatTime() {
        return chatTime;
    }

    public void setChatTime(String chatTime) {
        this.chatTime = chatTime;
    }

    public String getChatIp() {
        return chatIp;
    }

    public void setChatIp(String chatIp) {
        this.chatIp = chatIp;
    }

    public String getChatBrowser() {
        return chatBrowser;
    }

    public void setChatBrowser(String chatBrowser) {
        this.chatBrowser = chatBrowser;
    }

    public String getChatSystem() {
        return chatSystem;
    }

    public void setChatSystem(String chatSystem) {
        this.chatSystem = chatSystem;
    }

    public int getChatroomPeople() {
        return chatroomPeople;
    }

    public void setChatroomPeople(int chatroomPeople) {
        this.chatroomPeople = chatroomPeople;
    }

    public String getHeaderUrl() {
        return headerUrl;
    }

    public void setHeaderUrl(String headerUrl) {
        this.headerUrl = headerUrl;
    }

    @Override
    public String toString() {
        return "Chat{" +
                "chatPrivateId='" + chatPrivateId + '\'' +
                ", chatUserPrivateId='" + chatUserPrivateId + '\'' +
                ", chatUsername='" + chatUsername + '\'' +
                ", chatText='" + chatText + '\'' +
                ", chatTime='" + chatTime + '\'' +
                ", chatIp='" + chatIp + '\'' +
                ", chatBrowser='" + chatBrowser + '\'' +
                ", chatSystem='" + chatSystem + '\'' +
                ", chatroomPeople=" + chatroomPeople +
                ", headerUrl='" + headerUrl + '\'' +
                '}';
    }
}

3.1.6 ResultSet

package cn.chairc.platform.entity;

/**
 * @auther chairc
 * @date 2021/1/14 20:44
 */
//返回前端的验证结果集
public class ResultSet {

    private String code;            //返回码
    private String msg;             //返回信息
    private Object data = "";     //返回数据,默认设为空,需要返回数据时,使用setData()方法

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    @Override
    public String toString() {
        return "ResultSet{" +
                "code='" + code + '\'' +
                ", msg='" + msg + '\'' +
                ", data=" + data +
                '}';
    }

    /**
     * 自定义成功返回文本
     * @param msg 自定义文本
     */

    public void ok(String msg){
        this.code = "200";
        this.msg = msg;
    }

    /**
     * Bad Request 请求存在错误或参数错误
     * @param msg 自定义文本
     */

    public void fail(String msg){
        this.code = "400";
        this.msg = msg;
    }

    /**
     * OK 返回成功
     */

    public void ok() {
        this.code = "200";
        this.msg = "ok";
    }

    /**
     * Unauthorized 请求需要有HTTP认证或者认证失败
     */

    public void unauthorized() {
        this.code = "401";
        this.msg = "用户未登录,需要身份认证";
    }

    /**
     * 请求资源的访问被服务器拒绝
     */

    public void forbidden() {
        this.code = "403";
        this.msg = "服务器拒绝请求";
    }

    /**
     * 请求资源服务器未找到
     */

    public void notFound() {
        this.code = "404";
        this.msg = "请求资源不存在";
    }

    /**
     * 服务器执行请求出错
     */

    public void interServerError() {
        this.code = "500";
        this.msg = "服务器内部错误";
    }
}

3.2 前端代码


<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>聊天室title>
    <link rel="icon" th:href="@{/static/images/ico/favicon.ico}">
    <link rel="stylesheet" th:href="@{/static/css/bootstrap.min.css}">
    <link rel="stylesheet" th:href="@{/static/css/font-awesome.min.css}">
    <link rel="stylesheet" th:href="@{/static/css/animate.min.css}">
    <link rel="stylesheet" th:href="@{/static/css/main.css}">
    <link rel="stylesheet" th:href="@{/static/css/responsive.css}">
head>
<body>
<div class="platform" id="user-val" th:value="${userPrivateId}">
    <header th:replace="header.html">header>
    <main class="main-container">
        <div class="main-nav">div>
        <div class="main-left main-left-normal">
            <div class="main-left-container fadeInDown animated animated-setting">
                <div class="main-box shadow chatroom-container">
                    <h2 class="main-title">聊天室h2>
                    <div class="main-context" id="chat-data">

                    div>
                    <div class="main-context">
                        <div style="width: 80%;height: 80px;float: left;box-sizing: border-box;padding: 10px 5%;">
                            <textarea id="chat-text">textarea>
                        div>
                        <div style="width: 20%;height: 70px;float: left;box-sizing: border-box;margin: auto;line-height: 70px;">
                            <button class="btn btn-info" onclick="sendChat()" style="width: 80%">发送button>
                        div>
                    div>
                div>
            div>
        div>
        <div class="main-right main-right-normal">
            <div class="main-left-container fadeInDown animated animated-setting">
                <div class="main-box shadow">
                    <h2 class="main-title">聊天室公告h2>
                    <div class="main-context">
                        <div class="form-group">
                            <div class="main-context">
                                <b>聊天室要求b>
                                <p>聊天时:文明用语,文明交流。p>
                                <p>同学间:互相尊重,相互帮助。p>
                            div>
                        div>
                    div>
                div>
                <div class="main-box shadow">
                    <h2 class="main-title">聊天室状态h2>
                    <div class="main-context">
                        <div class="form-group">
                            <div class="main-context">
                                <p id="chatroom-people">p>
                                <p id="chatroom-status">p>
                            div>
                        div>
                    div>
                div>
                <div class="main-box shadow">
                    <footer th:replace="footer.html">footer>
                div>
            div>
        div>
    main>
div>
<div class="message-box-warp">
    <div id="message-box" class="message-box">
        <p id="message-box-text">p>
    div>
div>
body>
<script type="text/javascript" th:src="@{/static/js/jquery-min.js}">script>
<script type="text/javascript" th:src="@{/static/js/bootstrap.min.js}">script>
<script type="text/javascript" th:src="@{/static/js/main.js}">script>
<script>
    var socket;
    if (typeof (WebSocket) == "undefined") {
        console.log("遗憾:您的浏览器不支持WebSocket");
    } else {
        console.log("恭喜:您的浏览器支持WebSocket");
        //实现化WebSocket对象
        //指定要连接的服务器地址与端口建立连接
        //注意ws、wss使用不同的端口。我使用自签名的证书测试,
        //无法使用wss,浏览器打开WebSocket时报错
        //ws对应http、wss对应https。
        url = "ws://" + window.location.host + "/chatOnline/websocket";
        //console.log(url);
        socket = new WebSocket(url);
        //连接打开事件
        socket.onopen = function () {
            console.log("Socket 已打开");
            //socket.send("消息发送测试(来自客户端)");
        };
        //收到消息事件
        socket.onmessage = function (data) {
            console.log(data.data);
            var currentUser = $("#user-val").attr("value");
            var map = eval("(" + data.data + ")");
            var html;
            if(currentUser !== map["chatUserPrivateId"] && "SystemIn" !== map["chatPrivateId"] && "SystemOut" !== map["chatPrivateId"]){
                html = "
\n" + "
\n" + " " + "
" + " "+ map["chatUsername"] +"\n" + " "+ map["chatTime"] +"" + "
"
+ "
"
+ "
" + "
" + "

"+ map["chatText"] +"

"
+ "
\n"
+ "
\n"
+ "
"
; }else if("SystemIn" === map["chatPrivateId"] || "SystemOut" === map["chatPrivateId"]){ html = "

" + map["chatText"]+ "来了

"
}else if(currentUser === map["chatUserPrivateId"]){ html = "
\n" + "
\n" + " " + "
" + " "+ map["chatTime"] +"" + " "+ map["chatUsername"] +"\n" + "
"
+ "
"
+ "
" + "
" + "

"+ map["chatText"] +"

"
+ "
\n"
+ "
\n"
+ "
"
; } $("#chat-data").append(html); $("#chatroom-people").html("

当前在线数:"+ map["chatroomPeople"] +"

"
) //原生DOM var divscll = document.getElementById("chat-data"); divscll.scrollTop = divscll.scrollHeight; }; //连接关闭事件 socket.onclose = function () { console.log("Socket已关闭"); }; //发生了错误事件 socket.onerror = function () { alert("Socket发生了错误"); }; //窗口关闭时,关闭连接 window.unload = function () { socket.close(); }; } $(document).keypress(function (e) { // 回车键事件 if (e.which === 13) { sendChat(); } }); function sendChat() { var chatText = $("#chat-text").val(); $.ajax({ url: "/api/websocket/sendAll", dataType: "JSON", data: { "chatText": chatText }, contentType: "application/json; charset=utf-8", success: function (data) { if (data.code === "200") { //提交成功 $("#chat-text").val(""); $("#chat-text").focus(); } else { messageBoxFailure(data); messageBoxSetTimeout(); } } }) }
script> html>

3.3 实例图

3.3.1 登录系统

【Springboot实例】WebSocket即时聊天室设计与实现_第2张图片

3.3.2 进入聊天室

【Springboot实例】WebSocket即时聊天室设计与实现_第3张图片

3.3.3 进行聊天

【Springboot实例】WebSocket即时聊天室设计与实现_第4张图片

3.3.4 离开聊天室

【Springboot实例】WebSocket即时聊天室设计与实现_第5张图片

四、最后

  1. 目前只放出关于WebSocket即时聊天室源码,后期整个项目会上传到GitHub
  2. 对文章有疑问的地方可以留言给我,我会定时看消息
  3. 我的github
  4. 我的个人网站

你可能感兴趣的:(Springboot,Java,java,websocket,js,spring,boot,html)