带你手写一个H5聊天室
2023-07-19 本文已影响0人
h2coder
效果预览
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();
}
}
}
}
}