• 3分钟使用 WebSocket 搭建属于自己的聊天室(WebSocket 原理、应用解析)


    WebSocket 的由来

    • 在 WebSocket 出现之前,我们想实现实时通信、变更推送、服务端消息推送功能,我们一般的方案是使用 Ajax 短轮询、长轮询两种方式:
    • 比如我们想实现一个服务端数据变更时,立即通知客户端功能,没有 WebSocket 之前我们可能会采用以下两种方案:短轮询或长轮询

    短轮询、长轮询(来源:即时通讯网)

    • 上面两种方案都有比较明显的缺点:
    1、HTTP 协议包含的较长的请求头,有效数据只占很少一部分,浪费带宽
    2、短轮询频繁轮询对服务器压力较大,即使使用长轮询方案,客户端较多时仍会对客户端造成不小压力
    
    • 1
    • 2
    • 在这种情况下,HTML5 定义了 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

    WebSocket 是什么

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

    短轮询和WebSocket的区别(来源:即时通讯网)

    WebSocket 优缺点

    优点

    • 实时性: WebSocket 提供了双向通信,服务器可以主动向客户端推送数据,实现实时性非常高,适用于实时聊天、在线协作等应用。
    • 减少网络延迟: 与轮询和长轮询相比,WebSocket 可以显著减少网络延迟,因为不需要在每个请求之间建立和关闭连接。
    • 较小的数据传输开销: WebSocket 的数据帧相比于 HTTP 请求报文较小,减少了在每个请求中传输的开销,特别适用于需要频繁通信的应用。
    • 较低的服务器资源占用: 由于 WebSocket 的长连接特性,服务器可以处理更多的并发连接,相较于短连接有更低的资源占用。
    • 跨域通信: 与一些其他跨域通信方法相比,WebSocket 更容易实现跨域通信。

    缺点

    • 连接状态保持: 长时间保持连接可能会导致服务器和客户端都需要维护连接状态,可能增加一些负担。
    • 不适用于所有场景: 对于一些请求-响应模式较为简单的场景,WebSocket 的实时特性可能并不是必要的,使用 HTTP 请求可能更为合适。
    • 复杂性: 与传统的 HTTP 请求相比,WebSocket 的实现和管理可能稍显复杂,尤其是在处理连接状态、异常等方面。

    WebSocket 适用场景

    • 实时聊天应用: WebSocket 是实现实时聊天室、即时通讯应用的理想选择,因为它能够提供低延迟和高实时性。
    • 在线协作和协同编辑: 对于需要多用户协同工作的应用,如协同编辑文档或绘图,WebSocket 的实时性使得用户能够看到其他用户的操作。
    • 实时数据展示: 对于需要实时展示数据变化的应用,例如股票行情、实时监控系统等,WebSocket 提供了一种高效的通信方式。
    • 在线游戏: 在线游戏通常需要快速、实时的通信,WebSocket 能够提供低延迟和高并发的通信能力。
    • 推送服务: 用于实现消息推送服务,向客户端主动推送更新或通知。

    主流浏览器对 WebSocket 的兼容性

    主流浏览器对 WebSocket 的兼容性

    • 由上图可知:目前主流的 Web 浏览器都支持 WebSocket,因此我们可以在大多数项目中放心地使用它。

    WebSocket 通信过程以及原理

    建立连接

    • WebSocket 协议属于应用层协议,依赖传输层的 TCP 协议。它通过 HTTP/1.1 协议的 101 状态码进行握手建立连接。
    具体过程
    • 客户端发送一个 HTTP GET 请求到服务器,请求的路径是 WebSocket 的路径(类似 ws://example.com/socket)。请求中包含一些特殊的头字段,如 Upgrade: websocket 和 Connection: Upgrade,以表明客户端希望升级连接为 WebSocket。
    • 服务器收到这个请求后,会返回一个 HTTP 101 状态码(协议切换协议)。同样在响应头中包含 Upgrade: websocket 和 Connection: Upgrade,以及一些其他的 WebSocket 特定的头字段,例如 Sec-WebSocket-Accept,用于验证握手的合法性。
    • 客户端和服务器之间的连接从普通的 HTTP 连接升级为 WebSocket 连接。之后,客户端和服务器之间的通信就变成了 WebSocket 帧的传输,而不再是普通的 HTTP 请求和响应。
    示例
    // 客户端请求
    GET ws://localhost:8888/ HTTP/1.1
    Host: localhost:8888
    Connection: Upgrade
    Upgrade: websocket
    Origin: http://localhost:63342
    Sec-WebSocket-Version: 13
    Accept-Encoding: gzip, deflate, br
    Accept-Language: zh-CN,zh;q=0.9,ja;q=0.8,en;q=0.7
    Sec-WebSocket-Key: b7wpWuB9MCzOeQZg2O/yPg==
    Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
    
    // 服务端响应
    HTTP/1.1 101 Web Socket Protocol Handshake
    Connection: Upgrade
    Date: Wed, 22 Nov 2023 08:15:00 GMT
    Sec-WebSocket-Accept: Q4TEk+qOgJsKy7gedijA5AuUVIw=
    Server: TooTallNate Java-WebSocket
    Upgrade: websocket
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    Sec-WebSocket-Key
    • 与服务端响应头部的 Sec-WebSocket-Accept 是配套的,提供基本的防护,比如恶意的连接,或者无意的连接;这里的“配套”指的是:Sec-WebSocket-Accept 是根据请求头部的 Sec-WebSocket-Key 计算而来,计算过程大致为基于 SHA1 算法得到摘要并转成 base64 字符串。
    Sec-WebSocket-Extensions
    • 用于协商本次连接要使用的 WebSocket 扩展。

    数据通信

    • WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧并将关联的帧重新组装成完整的消息。
    数据帧
          0                   1                   2                   3
          0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
         +-+-+-+-+-------+-+-------------+-------------------------------+
         |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
         |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
         |N|V|V|V|       |S|             |   (if payload len==126/127)   |
         | |1|2|3|       |K|             |                               |
         +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
         |     Extended payload length continued, if payload len == 127  |
         + - - - - - - - - - - - - - - - +-------------------------------+
         |                               |Masking-key, if MASK set to 1  |
         +-------------------------------+-------------------------------+
         | Masking-key (continued)       |          Payload Data         |
         +-------------------------------- - - - - - - - - - - - - - - - +
         :                     Payload Data continued ...                :
         + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
         |                     Payload Data continued ...                |
         +---------------------------------------------------------------+
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    帧头(Frame Header)
    • FIN(1比特): 表示这是消息的最后一个帧。如果消息分成多个帧,FIN 位在最后一个帧上设置为 1。
    • RSV1、RSV2、RSV3(各1比特): 保留位,用于将来的扩展。
    • Opcode(4比特): 指定帧的类型,如文本帧、二进制帧、连接关闭等。
    WebSocket 定义了几种帧类型,其中最常见的是文本帧(Opcode 为 0x1)和二进制帧(Opcode 为 0x2)。其他帧类型包括连接关闭帧、Ping 帧、Pong 帧等。
    
    • 1
    • Mask(1比特): 指示是否使用掩码对负载进行掩码操作。
    • Payload Length: 指定数据的长度。如果小于 126 字节,直接表示数据的长度。如果等于 126 字节,后面跟着 16 比特的无符号整数表示数据的长度。如果等于 127 字节,后面跟着 64 比特的无符号整数表示数据的长度。
    掩码(Masking)
    • 如果 Mask 位被设置为 1,则帧头后面的 4 字节即为掩码,用于对负载数据进行简单的异或操作,以提高安全性。
    负载数据(Payload Data)
    • 实际要传输的数据,可以是文本、二进制数据等
    来自 MDN 的一个小例子
    Client: FIN=1, opcode=0x1, msg="hello"
    Server: (process complete message immediately) Hi.
    
    Client: FIN=0, opcode=0x1, msg="and a"
    Server: (listening, newmessage containing text started)
    
    Client: FIN=0, opcode=0x0, msg="happy new"
    Server: (listening, payload concatenated to previous message)
    
    Client: FIN=1, opcode=0x0, msg="year!"
    Server: (process complete message) Happy new year to you too!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    维持连接

    • 当建立连接后,连接可能因为网络等原因断开,我们可以使用心跳的方式定时检测连接状态。若连接断开,我们可以告警或者重新建立连接。

    关闭连接

    • WebSocket 是全双工通信,当客户端发送关闭请求时,服务端不一定立即响应,而是等服务端也同意关闭时再进行异步响应。
    • 下面是一个客户端关闭的例子:
    Client: FIN=1, opcode=0x8, msg="1000"
    Server: FIN=1, opcode=0x8, msg="1000"
    
    • 1
    • 2

    使用 WebSocket 实现一个简易聊天室

    • 下面是一个简易聊天室小案例,任何人打开下面的网页都可以加入我们聊天室进行聊天,然后小红和小明加入了聊天:

    简易聊天室

    前端源码

    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>WebSocket Chattitle>
    head>
    <body>
    <div id="chat">div>
    <input type="text" id="messageInput" placeholder="Type your message">
    <button onclick="sendMessage()">Sendbutton>
    
    <script>
        const socket = new WebSocket('ws://localhost:8888');
    
        socket.onopen = (event) => {
            console.log('WebSocket connection opened:', event);
        };
    
        socket.onmessage = (event) => {
            const messageDiv = document.getElementById('chat');
            const messageParagraph = document.createElement('p');
            messageParagraph.textContent = event.data;
            messageDiv.appendChild(messageParagraph);
        };
    
        socket.onclose = (event) => {
            console.log('WebSocket connection closed:', event);
        };
    
        function sendMessage() {
            const messageInput = document.getElementById('messageInput');
            const message = messageInput.value;
            socket.send(message);
            messageInput.value = '';
        }
    script>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    后端源码 Java

    package chat;
    
    import org.java_websocket.WebSocket;
    import org.java_websocket.handshake.ClientHandshake;
    import org.java_websocket.server.WebSocketServer;
    
    import java.net.InetSocketAddress;
    
    public class ChatServer extends WebSocketServer {
    
        public ChatServer(int port) {
            super(new InetSocketAddress(port));
        }
    
        @Override
        public void onOpen(WebSocket conn, ClientHandshake handshake) {
            System.out.println("New connection from: " + conn.getRemoteSocketAddress().getAddress().getHostAddress());
        }
    
        @Override
        public void onClose(WebSocket conn, int code, String reason, boolean remote) {
            System.out.println("Closed connection to: " + conn.getRemoteSocketAddress().getAddress().getHostAddress());
        }
    
        @Override
        public void onMessage(WebSocket conn, String message) {
            System.out.println("Received message: " + message);
            // Broadcast the message to all connected clients
            broadcast(message);
        }
    
        @Override
        public void onError(WebSocket conn, Exception ex) {
            System.err.println("Error on connection: " + ex.getMessage());
        }
    
        @Override
        public void onStart() {
        }
    
        public static void main(String[] args) {
            int port = 8888;
            ChatServer server = new ChatServer(port);
            server.start();
            System.out.println("WebSocket Server started on port: " + port);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    总结

    • WebSocket 是一种在客户端和服务器之间建立实时双向通信的协议。具备全双工、低延迟等优点,适用于实时聊天、多人协助、实时数据展示等场景。

    参考

    个人简介

    👋 你好,我是 Lorin 洛林,一位 Java 后端技术开发者!座右铭:Technology has the power to make the world a better place.

    🚀 我对技术的热情是我不断学习和分享的动力。我的博客是一个关于Java生态系统、后端开发和最新技术趋势的地方。

    🧠 作为一个 Java 后端技术爱好者,我不仅热衷于探索语言的新特性和技术的深度,还热衷于分享我的见解和最佳实践。我相信知识的分享和社区合作可以帮助我们共同成长。

    💡 在我的博客上,你将找到关于Java核心概念、JVM 底层技术、常用框架如Spring和Mybatis 、MySQL等数据库管理、RabbitMQ、Rocketmq等消息中间件、性能优化等内容的深入文章。我也将分享一些编程技巧和解决问题的方法,以帮助你更好地掌握Java编程。

    🌐 我鼓励互动和建立社区,因此请留下你的问题、建议或主题请求,让我知道你感兴趣的内容。此外,我将分享最新的互联网和技术资讯,以确保你与技术世界的最新发展保持联系。我期待与你一起在技术之路上前进,一起探讨技术世界的无限可能性。

    📖 保持关注我的博客,让我们共同追求技术卓越。

  • 相关阅读:
    ELF 文件介绍
    java基于SpringBoot+vue的餐厅点餐外卖系统 elementui 前后端分离
    适合新手小白的前端基础。会用最简单的话配最简单的代码告诉您各种标签、属性的用法,定期更新,直至完结
    8.基于SpringBoot3+Security6+JWT实现鉴权机制(二)
    STM32F0单片机基于Hal库温控智能风扇
    JavaScript字符串常用方法
    辉芒PWM输出代码例程---1.5HZ
    C# 实现腾讯云多路直播流的云端混合录制
    HashMap略解
    通信笔记:RSRP、RSRQ、RSNNR
  • 原文地址:https://blog.csdn.net/qq_35578171/article/details/134559849