WebSocket 入门 Java Springboot + Html5 JavaScript 简单实现
2022-01-17
阅读 {{counts.readCount}}
评论 {{counts.commentCount}}
<br><br>
## 前言
入门 WebSocket
后端 Springboot
前端 HTML5 (原生方法)
简单介绍下WebSocket,是类似于HTTP的基于TCP连接协议,相比于HTTP是单次单向传输的短连接,WebSocket是多次双向传输的长连接,更适合用于游戏、聊天室等有实时监听的需求
本来是设想直接用H5,其中的一个房主作为主机,就可以省去服务端,但考虑到可能存在公网ip变动、端口不开放、防火墙等问题,决定放弃该方案
所以本文还是使用主流方案实现前后端websocket交互,即所有用户与服务器端建立连接,服务器端代理实现用户端所有请求和响应
<br><br>
## 折腾
Java SpringBoot端
新建一个空的SpringBoot项目
我这里选择 JDK8 + maven + war包 (可以根据需要自行选择)
(过程省略)
<br><br>
修改访问端口
`application.yml`
```yml
server:
port: 3000
```
<br><br>
增加maven依赖
(这里如果只提供socket接口,可以把不需要的去掉,例如web依赖可以不要,只保留websocket)
`pom.xml`
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
```
<br><br>
新建 websocket配置
`WebSocketConfig.java`
```java
package com.zzzmh.ws.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @author zzzmh
* @date 2021/10/11
*/
@Configuration
public class WebSocketConfig {
/**
* 注入一个ServerEndpointExporter,该Bean会自动注册使用@ServerEndpoint注解申明的websocket endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
```
<br><br>
新建 websocket接口
`TestSocket.java`
```java
package com.zzzmh.ws.socket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestParam;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author zzzmh
* @date 2021/10/11
*/
@Component
@ServerEndpoint(value = "/test/{username}")
public class TestSocket {
private Logger log = LoggerFactory.getLogger(getClass());
/**
* 记录当前在线连接数 (线程安全)
*/
private static AtomicInteger onlineCount = new AtomicInteger(0);
/**
* 存放所有在线的客户端 (线程安全)
*/
private static Map<String, Session> clients = new ConcurrentHashMap<>();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session) {
clients.put(session.getId(), session);
// 在线人数加1
onlineCount.incrementAndGet();
log.info("有新连接加入:{},当前在线人数为:{}", session.getId(), onlineCount.get());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session) {
onlineCount.decrementAndGet(); // 在线数减1
clients.remove(session.getId());
log.info("有一连接关闭:{},当前在线人数为:{}", session.getId(), onlineCount.get());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("服务端收到客户端[{}]的消息:{}", session.getId(), message);
this.sendMessage(message, session);
}
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误");
error.printStackTrace();
}
/**
* 群发消息
*
* @param message 消息内容
*/
private void sendMessage(String message, Session fromSession) {
for (Map.Entry<String, Session> sessionEntry : clients.entrySet()) {
Session toSession = sessionEntry.getValue();
String username = toSession.getPathParameters().get("username");
// 排除掉自己
if (!fromSession.getId().equals(toSession.getId())) {
String fromUsername = fromSession.getPathParameters().get("username");
log.info("服务端给客户端[{}]发送消息{}", toSession.getId(), message);
toSession.getAsyncRemote().sendText(message + "( 发送者: " + fromUsername + ", 接受者: " + username + ")");
}
}
}
}
```
前端html代码 (需要放在容器里才能执行,本地执行无法请求接口,我这里用webstorm编辑和执行,也可以用vscode)
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>websocket</title>
</head>
<body>
<input id="text" type="text"/>
<button onclick="send()">Send</button>
<button onclick="closeWebSocket()">Close</button>
<div id="message"></div>
<script type="text/javascript">
if (!'WebSocket' in window) {
alert('当前浏览器不支持WebSocket 无法继续进行游戏,请更换浏览器或设备!')
}
let username;
while (!username) {
username = prompt("请输入你的游戏昵称:");
}
const websocket = new WebSocket("ws://localhost:3000/test/" + username);
//连接发生错误的回调方法
websocket.onerror = function () {
setMessageInnerHTML("连接错误");
};
//连接成功建立的回调方法
websocket.onopen = function (event) {
setMessageInnerHTML("已连接");
}
//接收到消息的回调方法
websocket.onmessage = function (event) {
setMessageInnerHTML("已收到消息:" + event.data);
}
//连接关闭的回调方法
websocket.onclose = function () {
setMessageInnerHTML("已断开");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
websocket.close();
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += '<br>' + innerHTML;
}
//关闭连接
function closeWebSocket() {
websocket.close();
}
//发送消息
function send() {
const message = document.getElementById('text').value;
setMessageInnerHTML("已发送消息:" + message)
websocket.send(message);
}
</script>
</body>
</html>
```
<br><br>
**服务端大致目录如下**
```shell
ws
├── src
│ ├── main
│ │ ├── java
│ │ │ ├── com.zzzmh.ws
│ │ │ │ ├── config
│ │ │ │ │ ├── WebSocketConfig.java
│ │ │ │ ├── socket
│ │ │ │ │ ├── TestSocket.java
│ │ │ │ ├── ServletInitializer.java
│ │ │ │ ├── WsApplication.java
│ │ ├── resources
│ │ │ ├── application.yml
├── pom.xml
```
<br><br>
## 效果展示
首先我这里打开3个页面模拟3个用户,分别输入昵称 `user1` `user2` `user3`
分别都可以看到已连接 说明连接websocket正常
在user1 中输入666并点击发送
在user2 和 user3可以看到效果
截图如下
<br>
`user1`
![](/api/file/getImage?fileId=61654bc4da74050013006e23)
<br>
`user2`
![](/api/file/getImage?fileId=61654bd8da74050013006e24)
<br>
`user3`
![](/api/file/getImage?fileId=61654bf2da74050013006e25)
<br><br>
## END
后续打算写几个能联机的多人游戏玩一玩
当其中一个人进行操作,就可以用ws实时同步到其他玩家的界面
包括实时的多人在线聊天也可以用这个实现
用户本地缓存数据用 localStorage
还可以实现断线重连
<br>
最终代码
[https://gitee.com/tczmh/ws-java](https://gitee.com/tczmh/ws-java)
[https://gitee.com/tczmh/websocket-html](https://gitee.com/tczmh/websocket-html)
<br><br>
参考
[https://www.cnblogs.com/xuwenjin/p/12664650.html](https://www.cnblogs.com/xuwenjin/p/12664650.html)