Just a chat room with java websocket

162 阅读2分钟

talk is cheap; show me the code;

code construction

image.png

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.mpy</groupId>
    <artifactId>chat</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>chat</name>
    <description>Demo project for websocket</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <!-- 排除自带的logback依赖 -->
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j</artifactId>
            <version>1.3.8.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

config > WebSocketConfig

package com.mpy.chat.config;

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

@Configuration
public class WebSocketConfig {

    //注入一个ServerEndpointExporter
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

controller > ChatWebSocketController

package com.mpy.chat.controller;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSON;
import com.mpy.chat.entity.MyMessage;
import com.mpy.chat.utils.StringUtil;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.log4j.Logger;

/**
 * 聊天控制器
 * @ServerEndpoint("/chat/{userId}")中的userId是前端创建会话窗口时当前用户的id,即消息发送者的id
 */
@ServerEndpoint("/chat/{userId}")
@Component
public class ChatWebSocketController {

    private final Logger logger = Logger.getLogger(ChatWebSocketController.class);

    //onlineCount:在线连接数
    private static AtomicInteger onlineCount = new AtomicInteger(0);

    //webSocketSet:用来存放每个客户端对应的MyWebSocket对象。
    public static List<ChatWebSocketController> webSocketSet = new ArrayList<>();

    //存放所有连接人信息
    public static List<String> userList  = new ArrayList<>();

    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;

    //用户ID
    public String userId = "";

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        this.session = session;
        this.userId = userId;
        this.userList.add(userId) ;
        //加入set中
        webSocketSet.add(this);
        //在线数加1
        onlineCount.incrementAndGet();
        logger.info("有新连接加入!" + userId + "当前在线用户数为" + onlineCount.get());
        JSONObject msg = new JSONObject();
        try {
            msg.put("msg", "连接成功");
            msg.put("status", "SUCCESS");
            msg.put("userId", userId);
            sendMessage(JSON.toJSONString(msg));
        } catch (Exception e) {
            logger.debug("IO异常");
        }
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(@PathParam("userId") String userId ) {
        //从set中删除
        webSocketSet.remove(this);
        onlineCount.decrementAndGet(); // 在线数减1
        logger.info("用户"+ userId +"退出聊天!当前在线用户数为" + onlineCount.get());
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public  void onMessage(String message, @PathParam("userId") String userId ) {
        //客户端输入的消息message要经过处理后封装成新的message,后端拿到新的消息后进行数据解析,然后判断是群发还是单发,并调用对应的方法
        logger.info("来自客户端" + userId + "的消息:" + message);
        try {
            MyMessage myMessage = JSON.parseObject(message, MyMessage.class);
            String messageContent = myMessage.getMessage();//messageContent:真正的消息内容
            String messageType = myMessage.getMessageType();
            if("1".equals(messageType)){ //单聊
                String recUser = myMessage.getUserId();//recUser:消息接收者
                sendInfo(messageContent,recUser,userId);//messageContent:输入框实际内容 recUser:消息接收者  userId 消息发送者
            }else{ //群聊
                sendGroupInfo(messageContent,userId);//messageContent:输入框实际内容 userId 消息发送者
            }
        } catch (Exception e) {
            logger.error("解析失败:{}", e);
        }
    }

    /**
     * 发生错误时调用的方法
     *
     * @OnError
     **/
    @OnError
    public void onError(Throwable error) {
        logger.debug("Websocket 发生错误");
        error.printStackTrace();
    }

    public synchronized void sendMessage(String message) {
        this.session.getAsyncRemote().sendText(message);
    }

    /**
     * 单聊
     * message : 消息内容,输入的实际内容,不是拼接后的内容
     * recUser : 消息接收者
     * sendUser : 消息发送者
     */
    public void sendInfo( String message , String recUser,String sendUser) {
        JSONObject msgObject = new JSONObject();//msgObject 包含发送者信息的消息
        for (ChatWebSocketController item : webSocketSet) {
            if (StringUtil.equals(item.userId, recUser)) {
                logger.info("给用户" + recUser + "传递消息:" + message);
                //拼接返回的消息,除了输入的实际内容,还要包含发送者信息
                msgObject.put("message",message);
                msgObject.put("sendUser",sendUser);
                item.sendMessage(JSON.toJSONString(msgObject));
            }
        }
    }

    /**
     * 群聊
     * message : 消息内容,输入的实际内容,不是拼接后的内容
     * sendUser : 消息发送者
     */
    public  void sendGroupInfo(String message,String sendUser) {
        JSONObject msgObject = new JSONObject();//msgObject 包含发送者信息的消息
        if (StringUtil.isNotEmpty(webSocketSet)) {
            for (ChatWebSocketController item : webSocketSet) {
//                if(!StringUtil.equals(item.userId, sendUser)) { //排除给发送者自身回送消息,如果不是自己就回送
                    logger.info("回送消息:" + message);
                    //拼接返回的消息,除了输入的实际内容,还要包含发送者信息
                    msgObject.put("message",message);
                    msgObject.put("sendUser",sendUser);
                    item.sendMessage(JSON.toJSONString(msgObject));
//                }
            }
        }
    }

    /**
     * Map/Set的key为自定义对象时,必须重写hashCode和equals。
     * 关于hashCode和equals的处理,遵循如下规则:
     * 1)只要重写equals,就必须重写hashCode。
     * 2)因为Set存储的是不重复的对象,依据hashCode和equals进行判断,所以Set存储的对象必须重写这两个方法。
     * 3)如果自定义对象做为Map的键,那么必须重写hashCode和equals。
     *
     * @param o
     * @return
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        ChatWebSocketController that = (ChatWebSocketController) o;
        return Objects.equals(session, that.session);
    }

    @Override
    public int hashCode() {
        return Objects.hash(session);
    }
}

entity > MyMessage

package com.mpy.chat.entity;

import java.io.Serializable;

public class MyMessage implements Serializable {

    private static final long serialVersionUID = 1L;

    private String userId;
    private String message;//消息内容
    private String messageType;//消息类型  1 代表单聊 2 代表群聊

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getMessageType() {
        return messageType;
    }

    public void setMessageType(String messageType) {
        this.messageType = messageType;
    }
}

utils > StringUtil

package com.mpy.chat.utils;

import java.lang.reflect.Array;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public final class StringUtil {

    /**
     * 对象为空
     *
     * @param object
     * @return
     */
    public static boolean isEmpty(Object object) {
        if (object == null) {
            return true;
        }
        if (object instanceof String && "".equals(((String) object).trim())) {
            return true;
        }
        if (object instanceof List && ((List) object).size() == 0) {
            return true;
        }
        if (object instanceof Map && ((Map) object).isEmpty()) {
            return true;
        }
        if (object instanceof CharSequence && ((CharSequence) object).length() == 0) {
            return true;
        }
        if (object instanceof Arrays && (Array.getLength(object) == 0)) {
            return true;
        }
        return false;
    }

    /**
     * 对象不为空
     *
     * @param object
     * @return
     */
    public static boolean isNotEmpty(Object object) {
        return !isEmpty(object);
    }

    /**
     * 查询字符串中某个字符首次出现的位置 从1计数
     *
     * @param string 字符串
     * @param c
     * @return
     */
    public static int strFirstIndex(String c, String string) {
        Matcher matcher = Pattern.compile(c).matcher(string);
        if (matcher.find()) {
            return matcher.start() + 1;
        } else {
            return -1;
        }
    }

    /**
     * 两个对象是否相等
     *
     * @param obj1
     * @param obj2
     * @return
     */
    public static boolean equals(Object obj1, Object obj2) {
        if (obj1 instanceof String && obj2 instanceof String) {
            obj1 = ((String) obj1).replace("\*", "");
            obj2 = ((String) obj2).replaceAll("\*", "");
            if (obj1.equals(obj2) || obj1 == obj2) {
                return true;
            }
        }
        if (obj1.equals(obj2) || obj1 == obj2) {
            return true;
        }
        return false;
    }

    /**
     * 根据字节截取内容
     *
     * @param bytes   自定义字节数组
     * @param content 需要截取的内容
     * @return
     */
    public static String[] separatorByBytes(double[] bytes, String content) {
        String[] contentArray = new String[bytes.length];
        double[] array = new double[bytes.length + 1];
        array[0] = 0;
        //复制数组
        System.arraycopy(bytes, 0, array, 1, bytes.length);
        for (int i = 0; i < bytes.length; i++) {
            content = content.substring((int) (array[i] * 2));
            contentArray[i] = content;
        }
        String[] strings = new String[bytes.length];
        for (int i = 0; i < contentArray.length; i++) {
            strings[i] = contentArray[i].substring(0, (int) (bytes[i] * 2));
        }
        return strings;
    }

    /**
     * 获取指定字符串出现的次数
     *
     * @param srcText  源字符串
     * @param findText 要查找的字符串
     * @return
     */
    public static int appearNumber(String srcText, String findText) {
        int count = 0;
        Pattern p = Pattern.compile(findText);
        Matcher m = p.matcher(srcText);
        while (m.find()) {
            count++;
        }
        return count;
    }


    /**
     * 将字符串str每隔2个分割存入数组
     *
     * @param str
     * @return
     */
    public static String[] setStr(String str) {
        int m = str.length() / 2;
        if (m * 2 < str.length()) {
            m++;
        }
        String[] strings = new String[m];
        int j = 0;
        for (int i = 0; i < str.length(); i++) {
            if (i % 2 == 0) {
                //每隔两个
                strings[j] = "" + str.charAt(i);
            } else {
                strings[j] = strings[j] + str.charAt(i);
                j++;
            }
        }
        return strings;
    }


    /**
     * 定义一个StringBuffer,利用StringBuffer类中的reverse()方法直接倒序输出
     * 倒叙字符串
     *
     * @param s
     */
    public static String reverseString2(String s) {
        if (s.length() > 0) {
            StringBuffer buffer = new StringBuffer(s);
            return buffer.reverse().toString();
        } else {
            return "";
        }
    }



    /**
     * 获取随机字符串
     *
     * @param length
     * @return
     */
    public static String getRandomString(int length) {
        String base = "abcdefghijklmnopqrstuvwxyz0123456789";
        Random random = new Random();
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < length; i++) {
            int number = random.nextInt(base.length());
            sb.append(base.charAt(number));
        }
        return sb.toString();
    }
}

resources > static > index.html

<!DOCTYPE HTML>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>WebSocket Chat Demo</title>
    <style>
        html,body {
            margin: 0;
            padding: 0;
        }
        * {
            box-sizing: content-box;
        }
        .bottom-box {
            height: 50px;
            position: fixed;
            bottom: 0;
            left: 0;
            right: 0;
            background: #000;
            display: flex;
        }
        input {
            flex: 1;
        }
        ul {
            padding: 10px;
        }
        li {
            margin: 20px 0;
        }
        .left {
            text-align: left;
        }
        .left span, .right span {
            display: inline-block;
            border-radius: 5px;
            padding: 5px 10px;
        }
        .left span {
            background: #ff9900;
            color: #000;
        }
        .right {
            text-align: right;
        }
        .right span {
            background: #2db7f5;
            color: #000;
        }
        .sendBtn {
            width: 150px;
            text-align: center;
            color: #fff;
            background: #19be6b;
            line-height: 50px;
        }
        ul, li {
            list-style: none;
        }
    </style>
</head>

<body>
<ul class="msg-list">
<!--    <li class="left">-->
<!--        <span>测试1</span>-->
<!--    </li>-->
<!--    <li class="right">-->
<!--        <span>测试2</span>-->
<!--    </li>-->
<!--    <li class="right">-->
<!--        <span>测试3</span>-->
<!--    </li>-->
<!--    <li class="left">-->
<!--        <span>测试4</span>-->
<!--    </li>-->


</ul>
<div class="bottom-box">
    <input id="inputContent" type="text" style="width:600px;"/>
    <div class="sendBtn" onclick="send()">Send</div>
</div>
<!--<button onclick="closeConnection()">Close</button>-->
<!--<div id="msg"></div>-->
</body>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script type="text/javascript">

    document.onkeydown = function (e) { // 回车提交表单
    // 兼容FF和IE和Opera
        var theEvent = window.event || e;
        var code = theEvent.keyCode || theEvent.which || theEvent.charCode;
        if (code == 13) {
            send();
        }
    }

    var websocket = null;

    //声明自己搭建的websocket服务
    if ('WebSocket' in window) {
        var userId = parseInt(Math.random() * 1000000) + "";
        websocket = new WebSocket("ws://xuewangshi.com:8005/chat/"+ userId);
    } else {
        alert('Not support websocket')
    }

    //连接发生错误的回调方法
    websocket.onerror = function() {
        // setMessageInnerHTML("error");
    };

    //连接成功建立的回调方法
    websocket.onopen = function(event) {
        //setMessageInnerHTML("open");
    }

    //接收到消息的回调方法
    websocket.onmessage = function(event) {

        let data = event.data ? JSON.parse(event.data) : {};
        console.log(data)
        data.message && setMessageInnerHTML(data);
    }

    //连接关闭的回调方法
    websocket.onclose = function() {
        setMessageInnerHTML("close");
    }

    //监听窗口关闭事件,当窗口关闭时关闭对应websocket连接
    window.onbeforeunload = function() {
        websocket.close();
    }

    //将消息回显在页面上
    function setMessageInnerHTML(data) {
        let id = 'id'+ Date.now();
        let htmlStr = `
            <li id="${id}" class="${data.sendUser == userId ? 'right' : 'left'}">
                ${data.sendUser !== userId ? `user${data.sendUser}:` : ''}<span>${data.message}</span>
            </li>
        `
        $('.msg-list').append(htmlStr)
        setTimeout(() => {
            document.getElementById(id).scrollIntoView(true);
        },100)
        // document.getElementById('msg').innerHTML += innerHTML + '<br/>';
    }

    //关闭连接
    function closeConnection() {
        websocket.close();
    }

    //发送消息
    function send() {
        var message = document.getElementById('inputContent').value;
        let mesObj = {
            message,
            messageType: '2',
            userId
        }
        message && websocket.send(JSON.stringify(mesObj));
        document.getElementById('inputContent').value = '';
    }
</script>
</html>

resources > application.yml


logging:
  # 设置logback.xml位置
  #  config: classpath:log/logback.xml
  # 设置log4j.properties位置
  config: classpath:log4j.properties
server:
  port: 8005

resources > log4j.properties

#定义根节点
log4j.rootLogger=DEBUG,error,CONSOLE,info

#设置控制台打印
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender     
#设置为格式化打印 PatternLayout
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout     
log4j.appender.CONSOLE.layout.ConversionPattern=%d{yyyy-MM-dd-HH-mm} [%t] [%c] [%p] - %m%n    

done! online test with my friends

image.png

参考来源 juejin.cn/post/700533…