websocket介绍:
WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。WebSocket 协议在 2011 年由 IETF 标准化为 RFC 6455,后由 RFC 7936 补充规范。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
接下来用一张图看一下 XHR Polling(短轮询) 与 WebSocket 之间的区别。
综上,websocket就是方便服务端与客户端可以实时通信,进行双向通信。
WebSocket学习过程中的易错常识
1 WebSocket 与 HTTP 有什么关系?
WebSocket 是一种与 HTTP 不同的协议。两者都位于 OSI 模型的应用层,并且都依赖于传输层的 TCP 协议。
2 WebSocket 与长轮询有什么区别?
长轮询就是:客户端发起一个请求,服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将这个请求挂起,然后判断请求的数据是否有更新。如果有更新,则进行响应,如果一直没有数据,则等待一定的时间后才返回。
长轮询的本质还是基于 HTTP 协议,它仍然是一个一问一答(请求 — 响应)的模式。而 WebSocket 在握手成功后,就是全双工的 TCP 通道,数据可以主动从服务端发送到客户端。
3 什么是 WebSocket 心跳?
网络中的接收和发送数据都是使用 Socket 进行实现。但是如果此套接字已经断开,那发送数据和接收数据的时候就一定会有问题。
可是如何判断这个套接字是否还可以使用呢?这个就需要在系统中创建心跳机制。
所谓 “心跳” 就是定时发送一个自定义的结构体(心跳包或心跳帧),让对方知道自己 “在线”,以确保链接的有效性。
而所谓的心跳包就是客户端定时发送简单的信息给服务器端告诉它我还在而已。代码就是每隔几分钟发送一个固定信息给服务端,服务端收到后回复一个固定信息,如果服务端几分钟内没有收到客户端信息则视客户端断开。
在 WebSocket 协议中定义了 心跳 Ping 和 心跳 Pong 的控制帧:
- 1)心跳 Ping 帧包含的操作码是 0x9:如果收到了一个心跳 Ping 帧,那么终端必须发送一个心跳 Pong 帧作为回应,除非已经收到了一个关闭帧。否则终端应该尽快回复 Pong 帧;
- 2)心跳 Pong 帧包含的操作码是 0xA:作为回应发送的 Pong 帧必须完整携带 Ping 帧中传递过来的 “应用数据” 字段。
针对第2)点:如果终端收到一个 Ping 帧但是没有发送 Pong 帧来回应之前的 Ping 帧,那么终端可以选择仅为最近处理的 Ping 帧发送 Pong 帧。此外,可以自动发送一个 Pong 帧,这用作单向心跳。
PS:这里有篇WebSocket心跳方面的IM实战总结文章,有兴趣可以阅读《Web端即时通讯实践干货:如何让你的WebSocket断网重连更快速?》。
websocket的使用:
websocket实现消息实时推送的原理:
websocket建立连接实现私人之间聊天的话是两个人都要连接到websocket,用户1建立websocket连接,然后发送消息到服务端,然后搜寻与接受者的会话,然后再通过与接收者建立的会话连接发送消息给接收者。
建立连接的时候,传递一个当前用户的id或者与当前用户有关的唯一标识,利用Map存储当前用户的websocket连接,然后发送消息的时候,传递一个接收者的id就可以了,根据Map,通过接收者id搜索已经创建的websocket的session会话,然后发送消息,就实现了接收者不用刷新,实时通信。
websocket实际操作:vue+springboot中websocket聊天的简单实现
前端代码:
// 更可靠的WebSocket初始化
const initWebSocket = (userId: number) => {
// 清理现有连接
closeWebSocket();
// 添加认证token(示例)
const token = Cookies.get('access_token');
const wsUrl = "ws://localhost:8080/ws/" + userId;
websocket.value = new WebSocket(wsUrl);
// 下面都是websocket中的方法,在方法内部进行逻辑处理
// 使用箭头函数避免this指向问题
websocket.value.onerror = (error) => {
console.error("WebSocket错误:", error);
attemptReconnect(userId);
};
websocket.value.onopen = () => {
console.log("WebSocket连接成功");
reconnectAttempts.value = 0; // 重置重连计数器
};
websocket.value.onmessage = (event) => {
console.log("收到消息:", event);
try {
const message: Message = JSON.parse(event.data);
messages.value?.push(message);
} catch (e) {
console.error("消息解析失败:", e);
}
};
websocket.value.onclose = (event) => {
if (event.wasClean) {
console.log(`连接正常关闭,code=${event.code}`);
} else {
console.warn('连接异常断开');
attemptReconnect(userId);
}
};
};
最主要的是 根据后端的地址建立websocket连接,其余的方法是建立连接后,接收消息时,连接关闭时websocket会执行的方法。
const wsUrl = "ws://localhost:8080/ws/" + userId;
websocket.value = new WebSocket(wsUrl);
对了,还有一个最重要的发送消息的方法
// 安全的发送消息
const sendMessage = (message: Message) => {
if (!websocket.value || websocket.value.readyState !== WebSocket.OPEN) {
console.error("消息发送失败:WebSocket未连接");
return false;
}
try {
websocket.value.send(JSON.stringify(message));
messages.value?.push(message)
console.log("messages", messages.value)
return true;
} catch (error) {
console.error("消息发送失败:", error);
return false;
}
};
上述就是前端的一些基础代码,进入聊天界面的时候,调用initwebsocket就可以想服务端发送请求请求连接websocket。
下面编写一下后端的编写websocket端口地址及处理连接的代码:
采用注解式定义websocket的连接端口地址,这里定义端口的时候传了一个用户id用来唯一存储创立websocket连接的时候的Session对象。
Session 对象可以用于管理 WebSocket 连接的生命周期,包括连接的建立、消息的接收与发送、以及连接的关闭。服务器可以通过 Session 来监听连接的建立和关闭事件,并执行相应的业务逻辑
先要定义一个配置类,可以将@ServerEndpoint注解的类导出为 WebSocket 端点
WebSocketConfig.java
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
websocket服务类,WebSocketServer.java
/**
* WebSocket服务
*/
@Component
@ServerEndpoint(value = "/ws/{userid}", decoders = MessageDecoder.class) // note userid是当前用户的id,创建连接的时候的用户id
public class WebSocketServer {
private ChatMessageService chatMessageService = SpringUtils.getBean(ChatMessageServiceImpl.class);
//存放会话对象
private static Map<String, Session> sessionMap = new HashMap();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userid") String userid) {
//note userid本来是long的,但String更加稳定
System.out.println("客户端:" + userid + "建立连接");
sessionMap.put(userid, session);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
* String message
*/
@OnMessage
public void onMessage(ChatMessage message, @PathParam("userid") String userid) {
System.out.println("收到来自客户端:" + userid + "的信息:" + message);
if (chatMessageService == null) {
System.out.println("chatMessageService 未初始化");
return;
}
chatMessageService.save(message);
try {
// 使用了解码器
// JSONObject json = JSONObject.parseObject(message);
String reciverid = String.valueOf(message.getReceiverId());
String jsonMessage = JSONObject.toJSONString(message);
// 获取目标用户的会话
Session targetSession = sessionMap.get(reciverid);
if (targetSession != null && targetSession.isOpen()) {
// 发送消息给目标用户
targetSession.getBasicRemote().sendText(jsonMessage);
System.out.println("消息已发送给:" + reciverid);
} else {
System.out.println("目标用户:" + reciverid + "不在线");
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 连接关闭调用的方法
*
* @param userid
* */
@OnClose
public void onClose(@PathParam("userid") String userid) {
System.out.println("连接断开:" + userid);
sessionMap.remove(userid);
}
/**
* 群发
*
* @param message
*/
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();
for (Session session : sessions) {
try {
//服务器向客户端发送消息
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
将上述两端代码运行就可以建立websocket连接,进行简单的聊天信息的推送。
前后端还有一些更复杂的封装的相关websocket包,库供我们使用,这是基于websocket本身自带的方法做的连接。
如有疑问或不足之处,欢迎批评指正。