注册

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

WebSocket 的由来



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

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



  • 上面两种方案都有比较明显的缺点:

1、HTTP 协议包含的较长的请求头,有效数据只占很少一部分,浪费带宽
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

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 ... |
+
---------------------------------------------------------------+

帧头(Frame Header)


  • FIN(1比特): 表示这是消息的最后一个帧。如果消息分成多个帧,FIN 位在最后一个帧上设置为 1。
  • RSV1、RSV2、RSV3(各1比特): 保留位,用于将来的扩展。
  • Opcode(4比特): 指定帧的类型,如文本帧、二进制帧、连接关闭等。

WebSocket 定义了几种帧类型,其中最常见的是文本帧(Opcode  0x1)和二进制帧(Opcode  0x2)。其他帧类型包括连接关闭帧、Ping 帧、Pong 帧等。


  • 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!

维持连接



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

关闭连接



  • WebSocket 是全双工通信,当客户端发送关闭请求时,服务端不一定立即响应,而是等服务端也同意关闭时再进行异步响应。
  • 下面是一个客户端关闭的例子:

Client: FIN=1, opcode=0x8, msg="1000"
Server: FIN=1, opcode=0x8, msg="1000"

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



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

简易聊天室


前端源码


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>

后端源码 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);
}
}

总结



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

参考



个人简介


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


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


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


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


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


作者:Lorin洛林
来源:juejin.cn/post/7304182487684415514

0 个评论

要回复文章请先登录注册