WebSocket前后端通信
1. 前后端如何连接
服务端利用SpringBoot启动一个WebSocket服务,同时暴露出该服务的应用路径,客户端则利用该应用路径进行连接。需要注意的是,在服务端只需要启动一个WebSocket服务,而每一个客户端就是一个WebSocket应用。
2. 前端初始化websocket
ws://和jdbc://、http://一样都是协议名,同样的,Websocket还支持更加安全的wss://,/websocket即该服务的应用路径名
const ws = new WebSocket('ws://localhost:8000/websocket/')
2.1 事件监听
- onopen事件监听 与服务端连接成功会触发
- onerror事件监听 与服务端连接异常时触发
- onclose事件监听 与服务端连接关闭时触发
- onmessage事件监听 接收到来自服务端的消息时触发
原生js初始化websocket
const ws = new WebSocket(webSocketUrl)
//onopen事件监听
ws.addEventListener('open',e=>{
console.log('与服务端连接打开->',e)
},false)
//onclose事件监听
ws.addEventListener('close',e=>{
console.log('与服务端连接关闭->',e)
},false)
//onmessage事件监听
ws.addEventListener('message',e=>{
console.log('来自服务端的消息->',e)
},false)
//onerror事件监听
ws.addEventListener('error',e=>{
console.log('与服务端连接异常->',e)
},false)
ws对象的addEventListener( )方法,为WebSocket绑定事件监听,从而在各个事件监听中处理事务
2.2 Vue 初始化案例
export default {
name: "Home",
data() {
return {
webSocketObject: null,
}
},
created() {
//初始化WebSocket
this.webSocketInit()
},
methods: {
webSocketInit(){
const webSocketUrl = 'ws://localhost:8000/websocket/'+this.username
this.webSocketObject = new WebSocket(webSocketUrl);
this.webSocketObject.onopen = this.webSocketOnOpen
this.webSocketObject.onmessage = this.webSocketOnMessage
this.webSocketObject.onerror = this.webSocketOnError
this.webSocketObject.onclose = this.webSocketOnClose
},
webSocketOnOpen(e){
console.log('与服务端连接打开->',e)
},
webSocketOnMessage(e){
console.log('来自服务端的消息->',e)
},
webSocketOnError(e){
console.log('与服务端连接异常->',e)
},
webSocketOnClose(e){
console.log('与服务端连接关闭->',e)
},
},
}
</script>
同样的,利用methods分别定义好OnOpen、OnMessage、OnError、OnClose四个事件监听,然后进行初始化并且绑定就可以了。
这样就完成了WebSocket对象以及事件监听的初始化。
3. 后端初始化websocket
3.1 基本配置
SpringBoot自带的WebSocket有以下5个注解需要注意:
1.@ServerEndpoint
暴露出的ws应用的路径,支持RESTful风格传参,类似/websocket/{username}
2.@OnOpen
与当前客户端连接成功,有入参Session对象(当前连接对象),同时可以利用@PathParam()获取上述应用路径中传递的参数,比 如@PathParam("username") String username。
3.@OnClose
与当前客户端连接失败,有入参Session对象(当前连接对象),同时也可以利用@PathParam()获取上述应用路径中传递的参数。
4.@OnError
与当前客户端连接异常,有入参Session对象(当前连接对象)、Throwable对象(异常对象),同时也可以利用@PathParam()获取 上述应用路径中传递的参数。
5.@OnMessage
当前客户端发送消息,有入参Session对象(当前连接对象)、String message对象(当前客户端传递过来的字符串消息)
利用SpringBoot创建项目,需要引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
在application.yaml中定义好该服务的端口号:
server:
port: 8000
注册配置类:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter,
* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
定义Websocket主业务类:
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
@Component
@Slf4j
@ServerEndpoint("/websocket/{username}") //暴露的ws应用的路径
public class WebSocket {
/**
* 客户端与服务端连接成功
* @param session
* @param username
*/
@OnOpen
public void onOpen(Session session,@PathParam("username") String username){
/*
do something for onOpen
与当前客户端连接成功时
*/
}
/**
* 客户端与服务端连接关闭
* @param session
* @param username
*/
@OnClose
public void onClose(Session session,@PathParam("username") String username){
/*
do something for onClose
与当前客户端连接关闭时
*/
}
/**
* 客户端与服务端连接异常
* @param error
* @param session
* @param username
*/
@OnError
public void onError(Throwable error,Session session,@PathParam("username") String username) {
}
/**
* 客户端向服务端发送消息
* @param message
* @param username
* @throws IOException
*/
@OnMessage
public void onMsg(Session session,String message,@PathParam("username") String username) throws IOException {
/*
do something for onMessage
收到来自当前客户端的消息时
*/
}
}
3.2 WebSocketServer模板(参考)
/**
* WebSocket的操作类
*/
@Component
@Slf4j
/**
* html页面与之关联的接口
* var reqUrl = "http://localhost:8081/websocket/" + cid;
* socket = new WebSocket(reqUrl.replace("http", "ws"));
*/
@ServerEndpoint("/websocket/{sid}")
public class WebSocketServer {
/**
* 静态变量,用来记录当前在线连接数,线程安全的类。
*/
private static AtomicInteger onlineSessionClientCount = new AtomicInteger(0);
/**
* 存放所有在线的客户端
*/
private static Map<String, Session> onlineSessionClientMap = new ConcurrentHashMap<>();
/**
* 连接sid和连接会话
*/
private String sid;
private Session session;
/**
*
* @param sid 每次页面建立连接时传入到服务端的id,比如用户id等。可以自定义。
* @param session 与某个客户端的连接会话,需要通过它来给客户端发送消息
*/
@OnOpen
public void onOpen(@PathParam("sid") String sid, Session session) {
/**
* session.getId():当前session会话会自动生成一个id,从0开始累加的。
*/
log.info("连接建立中 ==> session_id = {}, sid = {}", session.getId(), sid);
//加入 Map中。将页面的sid和session绑定或者session.getId()与session
//onlineSessionIdClientMap.put(session.getId(), session);
onlineSessionClientMap.put(sid, session);
//在线数加1
onlineSessionClientCount.incrementAndGet();
this.sid = sid;
this.session = session;
sendToOne(sid, "连接成功");
log.info("连接建立成功,当前在线数为:{} ==> 开始监听新连接:session_id = {}, sid = {},。", onlineSessionClientCount, session.getId(), sid);
}
/**
*
* @param sid
* @param session
*/
@OnClose
public void onClose(@PathParam("sid") String sid, Session session) {
//onlineSessionIdClientMap.remove(session.getId());
// 从 Map中移除
onlineSessionClientMap.remove(sid);
//在线数减1
onlineSessionClientCount.decrementAndGet();
log.info("连接关闭成功,当前在线数为:{} ==> 关闭该连接信息:session_id = {}, sid = {},。", onlineSessionClientCount, session.getId(), sid);
}
/**
* * 当服务端执行toSession.getAsyncRemote().sendText(xxx)后,前端的socket.onmessage得到监听。
*
* @param message
* @param session
*/
@OnMessage
public void onMessage(String message, Session session) {
/**
* html界面传递来得数据格式,可以自定义.
* {"sid":"user-1","message":"hello websocket"}
*/
JSONObject jsonObject = JSON.parseObject(message);
String toSid = jsonObject.getString("sid");
String msg = jsonObject.getString("message");
log.info("服务端收到客户端消息 ==> fromSid = {}, toSid = {}, message = {}", sid, toSid, message);
/**
* 模拟约定:如果未指定sid信息,则群发,否则就单独发送
*/
if (toSid == null || toSid == "" || "".equalsIgnoreCase(toSid)) {
sendToAll(msg);
} else {
sendToOne(toSid, msg);
}
}
/**
* 发生错误调用的方法
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("WebSocket发生错误,错误信息为:" + error.getMessage());
error.printStackTrace();
}
/**
* 群发消息
*
* @param message 消息
*/
private void sendToAll(String message) {
// 遍历在线map集合
onlineSessionClientMap.forEach((onlineSid, toSession) -> {
// 排除掉自己
if (!sid.equalsIgnoreCase(onlineSid)) {
log.info("服务端给客户端群发消息 ==> sid = {}, toSid = {}, message = {}", sid, onlineSid, message);
toSession.getAsyncRemote().sendText(message);
}
});
}
/**
* 指定发送消息
*
* @param toSid
* @param message
*/
private void sendToOne(String toSid, String message) {
// 通过sid查询map中是否存在
Session toSession = onlineSessionClientMap.get(toSid);
if (toSession == null) {
log.error("服务端给客户端发送消息 ==> toSid = {} 不存在, message = {}", toSid, message);
return;
}
// 异步发送
log.info("服务端给客户端发送消息 ==> toSid = {}, message = {}", toSid, message);
toSession.getAsyncRemote().sendText(message);
/*
// 同步发送
try {
toSession.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("发送消息失败,WebSocket IO异常");
e.printStackTrace();
}*/
}
}