美文网首页JavaScript
带你手写一个H5聊天室

带你手写一个H5聊天室

作者: h2coder | 来源:发表于2023-07-19 06:38 被阅读0次

    效果预览

    H5-WebSocket.png

    项目说明

    • 前端
      • 原生JS + 原生WebSocket
    • 后端
      • Java8 + SpringBoot + SpringBoot-WebSocket

    需要完整代码,可以clone仓库

    简介

    什么是WebSocket协议

    WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。

    WebSocket的优点

    • 较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。
    • 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于 HTTP 请求需要等待客户端发起请求服务端才能响应,延迟明显更少。
    • 保持连接状态。与 HTTP 不同的是,WebSocket 需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。
    • 更好的二进制支持。WebSocket 定义了二进制帧,相对 HTTP,可以更轻松地处理二进制内容。
      可以支持扩展。WebSocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议。

    WebSocket的应用场景

    • 聊天室、直播间的IM聊天功能
    • 大屏可视化的实时数据变更
    • 物联网设备监控

    前端

    前端API说明

    WebSocket对象

    • 第一个参数 url, 指定连接的 URL。第二个参数 protocol 是可选的,指定了可接受的子协议
    var Socket = new WebSocket(url, [protocol] );
    

    WebSocket属性

    属性 描述
    Socket.readyState 只读属性 readyState 表示连接状态,可以是以下值: 0 - 表示连接尚未建立。 1 - 表示连接已建立,可以进行通信。 2 - 表示连接正在进行关闭。 3 - 表示连接已经关闭或者连接不能打开。
    Socket.bufferedAmount 只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。

    WebSocket事件

    事件 回调函数 描述
    open socket.onopen 连接建立时触发
    message socket.onmessage 客户端接收服务端数据时触发
    error socket.onerror 通信发生错误时触发
    close socket.onclose 连接关闭时触发

    WebSocket方法

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

    检查当前浏览器,是否支持WebSocket

    function isSupportWebSocket() {
        return "WebSocket" in window;
    }
    
    if (isSupportWebSocket()) {
        console.log('浏览器支持WebSocket!');
    } else {
        alert('您的浏览器不支持WebSocket功能');
    }
    

    定义服务端连接地址和用户Id

    // 目标ip地址
    const ipAddress = '127.0.0.1:9001';
    
    // 用户Id
    const userId = 10086;
    

    封装一个对象,避免变量和函数污染全局变量

    // 封装一个WebSocket对象
    const webSocket = {
        // 创建Socket对象
        socket: new WebSocket(`ws://${ipAddress}/ws/${userId}`),
        // 判断浏览器,是否支持WebSocket
        isSupportWebSocket() {
            return "WebSocket" in window;
        },
        // 初始化WebSocket
        setup() {
            // 连接成功
            this.socket.onopen = function () {
                console.log('WebSocket 连接成功...');
            };
            // 接收到服务端的消息
            this.socket.onmessage = function (evt) {
                // 获取消息文本
                var receivedMsg = evt.data;
                console.log('WebSocket 收到消息:' + receivedMsg);
            };
            this.socket.onerror = function () {
                console.log('WebSocket 连接发生错误...');
            };
            // 连接已关闭
            this.socket.onclose = function () {
                console.log('WebSocket 连接已关闭...');
            };
        },
        // 发送消息
        sendMessage(msg) {
            this.socket.send(msg);
        },
        // 关闭连接
        close() {
            this.socket.close();
        }
    };
    

    发起连接

    // 初始化,并发起连接
    if (webSocket.isSupportWebSocket()) {
        console.log('浏览器支持WebSocket!');
        webSocket.setup();
    } else {
        alert('您的浏览器不支持WebSocket功能');
    }
    

    发送消息

    在点击发送按钮时,发送消息给服务端

    function handleSend() {
        const content = chatInput.value.trim();
        if (content === '') {
            // alert('请输入你要发送的内容');
            return;
        }
        // 添加我的消息
        addChatMsg2List(content, true);
        // 清空输入框
        chatInput.value = '';
        // 发送消息
        webSocket.sendMessage(content);
    }
    
    // 发送按钮的点击事件
    sendBtn.addEventListener('click', function (e) {
        handleSend();
    });
    // 键盘的回车事件
    window.addEventListener('keyup', function (e) {
        if (e.key === 'Enter') {
            handleSend();
        }
    });
    

    监听服务端消息

    // 接收到服务端的消息
    this.socket.onmessage = function (evt) {
        // 获取消息文本
        var receivedMsg = evt.data;
        console.log('WebSocket 收到消息:' + receivedMsg);
        // 添加对方的消息
        addChatMsg2List(receivedMsg, false);
    };
    

    根据聊天数据,渲染列表页面

    function render() {
        const newList = list.map(({ isMe, text }) => {
            if (isMe) {
                return `
                    <li class="right">
                    <span>${text}</span>
                    <img src="./assets/me.png" alt="">
                    </li>
            `;
            } else {
                return `
                    <li class="left">
                    <img src="./assets/you.png" alt="">
                    <span>${text}</span>
                    </li>
                 `;
            }
        });
        chatList.innerHTML = newList.join('');
    }
    

    滚动聊天列表到底部

    function scollList2Bottom() {
        chatDiv.scrollTop = chatDiv.scrollHeight
    }
    

    服务端

    添加依赖

    • pom.xml
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    

    服务端口

    • application.yml
    server:
      port: 9001
    

    启动类

    @SpringBootApplication
    //打印日志注解,是lombok提供的,打印日志时,会带有类全名、方法名、标识哪里输出的日志
    @Slf4j
    public class AppApplication {
        public static void main(String[] args) {
            SpringApplication.run(AppApplication.class, args);
            log.info("项目启动成功");
        }
    }
    

    WebSocket配置类

    @Configuration
    public class WebSocketConfig {
        /**
         * 注入ServerEndpointExporter,
         * 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
         */
        @Bean
        public ServerEndpointExporter serverEndpointExporter() {
            return new ServerEndpointExporter();
        }
    }
    

    发送消息的方法

    • 广播消息,也就是群发
    String msg = "我是消息内容";
    webSocket.sendAllMessage(msg);
    
    • 单点发送,就是一对一
    webSocket.sendOneMessage(userId, msg);
    
    • 一对多,传用户Id数组
    webSocket.sendMoreMessage(userIds, msg);
    

    WebSocket消息处理类

    /**
     * WebSocket
     */
    @Component
    @Slf4j
    // 接口路径 ws://localhost:9001/ws/userId
    @ServerEndpoint("/ws/{userId}")
    public class WebSocket {
        /**
         * 与某个客户端的连接会话,需要通过它来给客户端发送数据
         */
        private Session session;
        /**
         * 用户ID
         */
        private String userId;
    
        /**
         * concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
         * 虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
         * 注:底下WebSocket是当前类名
         */
        private static final Set<WebSocket> sWebSockets = new CopyOnWriteArraySet<>();
    
        /**
         * 用来存在线连接用户信息
         */
        private static final Map<String, Session> sSessionPool = new ConcurrentHashMap<>();
    
        /**
         * 链接成功调用的方法
         */
        @OnOpen
        public void onOpen(Session session, @PathParam(value = "userId") String userId) {
            try {
                this.session = session;
                this.userId = userId;
                sWebSockets.add(this);
                sSessionPool.put(userId, session);
                log.info("【WebSocket消息】有新的连接,总数为:" + sWebSockets.size());
            } catch (Exception e) {
                e.printStackTrace();
            }
            // 群发一个欢迎消息
            sendAllMessage("欢迎光临!");
        }
    
        /**
         * 链接关闭调用的方法
         */
        @OnClose
        public void onClose() {
            try {
                sWebSockets.remove(this);
                sSessionPool.remove(this.userId);
                log.info("【WebSocket消息】连接断开,总数为:" + sWebSockets.size());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 收到客户端消息后调用的方法
         */
        @OnMessage
        public void onMessage(String message) {
            log.info("【WebSocket消息】收到客户端消息:" + message);
            // 马上回复一个消息给客户端
            String replay = String.valueOf(message)
                    .replace("吗", "")
                    .replace("?", "!")
                    .replace("?", "!");
            sendOneMessage(userId, replay);
        }
    
        /**
         * 发送错误时的处理
         */
        @OnError
        public void onError(Session session, Throwable error) {
            log.error("用户错误,原因:" + error.getMessage());
            error.printStackTrace();
        }
    
        /**
         * 发送广播消息(群发)
         */
        public void sendAllMessage(String message) {
            log.info("【WebSocket消息】广播消息:" + message);
            for (WebSocket webSocket : sWebSockets) {
                try {
                    if (webSocket.session.isOpen()) {
                        webSocket.session.getAsyncRemote().sendText(message);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    
        /**
         * 单点消息
         */
        public void sendOneMessage(String userId, String message) {
            Session session = sSessionPool.get(userId);
            if (session != null && session.isOpen()) {
                try {
                    log.info("【WebSocket消息】 单点消息:" + message);
                    session.getAsyncRemote().sendText(message);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    
        /**
         * 单点消息(多人)
         */
        public void sendMoreMessage(String[] userIds, String message) {
            for (String userId : userIds) {
                Session session = sSessionPool.get(userId);
                if (session != null && session.isOpen()) {
                    try {
                        log.info("【WebSocket消息】 单点消息:" + message);
                        session.getAsyncRemote().sendText(message);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    

    相关文章

      网友评论

        本文标题:带你手写一个H5聊天室

        本文链接:https://www.haomeiwen.com/subject/xwonudtx.html