WebSocket网络传输协议

1. 概述

WebSocket 协议在2008年诞生,2011年成为国际标准。现在所有浏览器都已经支持了。WebSocket 的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话。

  1. WebSocket可以在浏览器里使用
  2. 支持双向通信
  3. 使用很简单

1731591210504.png

WebSocket 的其他特点:

  • 建立在 TCP 协议之上,服务器端的实现比较容易。
  • 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
  • 数据格式比较轻量,性能开销小,通信高效。
  • 可以发送文本,也可以发送二进制数据。
  • 没有同源限制,客户端可以与任意服务器通信。
  • 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

2. 使用

2.1 构造函数

**WebSocket()**构造函器会返回一个 WebSocket 对象。

语法

1
var aWebSocket = new WebSocket(url [, protocols]);

参数

  • url

    要连接的 URL;这应该是 WebSocket 服务器将响应的 URL。

  • protocols 可选

    一个协议字符串或者一个包含协议字符串的数组。这些字符串用于指定子协议,这样单个服务器可以实现多个 WebSocket 子协议(例如,你可能希望一台服务器能够根据指定的协议(protocol)处理不同类型的交互)。如果不指定协议字符串,则假定为空字符串。

抛出异常

desktop mobile server
Chrome Edge Firefox Opera Safari Chrome Android Firefox for Android Opera Android Safari on iOS Samsung Internet WebView Android WebView on iOS Deno Node.js
WebSocket() constructor 5Toggle history 12Toggle history 11moreToggle history 12.1Toggle history 5Toggle history 18Toggle history 14moreToggle history 12.1Toggle history 4.2Toggle history 1.0Toggle history 4.4Toggle history 4.2Toggle history 1.4Toggle history 22.0.0Toggle history
url parameter allows https, http, and relative URLs 125Toggle history 125Toggle history 124Toggle history 111Toggle history 17.3Toggle history 125Toggle history 124Toggle history 83Toggle history 17.3Toggle history 27.0Toggle history 125Toggle history 17.3Toggle history NoToggle history NoToggle history

3. 简单实例

下面将实现一个简单的html和spring项目在两个页面之间同步传递消息:

3.1 创建一个基础的spring项目,在pom.xml中加入如下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//websocket依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
//日志输出依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</dependency>

3.2 构建一个WebSocketConfig启动项

为启动项注入bean,交给spring管理该项目:

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}

}

3.3 WebSocketServer服务实现

WebSocketServer是用于处理WebSocket连接的服务器端组件。在实现一个WebSocketServer时,以下是一些通常必须实现的方法:

  1. handle - 这是处理客户端连接请求的核心方法。当一个新的WebSocket连接被建立时,这个方法会被调用。
  2. handle_open - 当一个新的WebSocket连接打开时调用,通常用于初始化连接或设置必要的变量。
  3. handle_close - 当WebSocket连接被关闭时调用,可以在这里进行资源清理。
  4. handle_message - 当服务器接收到来自客户端的消息时调用,用于处理接收到的数据。
  5. handle_error - 在处理WebSocket连接时发生错误时调用,用于错误处理
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138

import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

/**
* Created with IntelliJ IDEA.
* @ Auther: 马超伟
* @ Date: 2020/06/16/14:35
* @ Description:
* @ ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,
* 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
*/
@Component
@Slf4j
@Service
@ServerEndpoint(value="/api/websocket/{sid}")
public class WebSocketServer {
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();

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

//接收sid
private String sid = "";

/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
this.session = session;
webSocketSet.add(this); //加入set中
this.sid = sid;
addOnlineCount(); //在线数加1
try {
// 发送 JSON 格式的消息
sendMessage("{\"msg\":\"conn_success\"}");
log.info("有新窗口开始监听:" + sid + ",当前在线人数为:" + getOnlineCount());
} catch (IOException e) {
log.error("websocket IO Exception");
}
}

/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
webSocketSet.remove(this); //从set中删除
subOnlineCount(); //在线数减1
//断开连接情况下,更新主板占用情况为释放
log.info("释放的sid为:"+sid);
//这里写你 释放的时候,要处理的业务
log.info("有一连接关闭!当前在线人数为" + getOnlineCount());

}

/**
* 收到客户端消息后调用的方法
* @ Param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("收到来自窗口" + sid + "的信息:" + message);
//群发消息
for (WebSocketServer item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}

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

/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
log.info("推送" + sid + "的信息:" + message);
this.session.getBasicRemote().sendText(message);
}

/**
* 群发自定义消息
*/
public static void sendInfo(String message, @PathParam("sid") String sid) throws IOException {
log.info("推送消息到窗口" + sid + ",推送内容:" + message);

for (WebSocketServer item : webSocketSet) {
try {
//这里可以设定只推送给这个sid的,为null则全部推送
if (sid == null) {
// item.sendMessage(message);
} else if (item.sid.equals(sid)) {
item.sendMessage(message);
}
} catch (IOException e) {
continue;
}
}
}

public static synchronized int getOnlineCount() {
return onlineCount;
}

public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}

public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}

public static CopyOnWriteArraySet<WebSocketServer> getWebSocketSet() {
return webSocketSet;
}
}

3.4 html页面搭建

在html页面中我们主要做WebSocket对象的创建和提交数据和实时显示数据的操作,其中主要的操作为创建对象:

  • new WebSocket("ws://localhost:8080/api/websocket/100"):这一行代码尝试创建一个新的WebSocket连接到指定的URL,这里必须指定到**@ServerEndpoint(value=”/api/websocket/{sid}”)**这个路径之下,如果需要创建两个不同的对象,只需要将其中的id修改即可,这样可以达到动态创建不同的对象。
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Example</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<div id="main" style="width: 1200px;"></div>
Welcome<br/>
<input id="text" type="text" />
<button onclick="send()">发送消息</button>
<hr/>
<button onclick="closeWebSocket()">关闭WebSocket连接</button>
<button onclick="openWebSocket()">开启WebSocket连接</button>
<hr/>
<div id="message"></div>

<script type="text/javascript">
var websocket = null;

// 判断当前浏览器是否支持WebSocket
function openWebSocket() {
if ('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:8080/api/websocket/100");

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

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

// 接收到消息的回调方法
websocket.onmessage = function(event) {
console.log(event.data);
try {
var data = JSON.parse(event.data); // 解析传入的 JSON
setMessageInnerHTML("接收:"+data.msg); // 显示 msg 字段
} catch (e) {
console.error("解析消息失败:", e);
}
};

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

} else {
alert('当前浏览器不支持 WebSocket');
}
}

// 将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}

// 关闭WebSocket连接
function closeWebSocket() {
if (websocket) {
websocket.close();
websocket = null; // 清空 websocket 变量
}
}

// 发送消息
function send() {
var message = document.getElementById('text').value;
if (message) { // 确保消息不为空
var jsonMessage = JSON.stringify({ msg: message });
if (websocket && websocket.readyState === WebSocket.OPEN) {
websocket.send(jsonMessage);
setMessageInnerHTML("发送: " + message);
document.getElementById('text').value = ''; // 发送后清空输入框
} else {
alert("WebSocket连接未开启!");
}
} else {
alert("请输入消息!");
}
}
</script>
</body>
</html>

4. 测试

其中是两个不同的id对应的用户

1731592520874.png

5. 扩展

5.1 项目启动

项目直接由spring启动,不需要使用到tomcat

5.2 优化

指定用户发送?

区分发送和接收?