WebSocket协议是基于TCP的一种新的协议。WebSocket最初在HTML5规范中被引用为TCP连接,作为基于TCP的套接字API的占位符。它实现了浏览器与服务器全双工(full-duplex)通信。其本质是保持TCP连接,在浏览器和服务端通过Socket进行通信。
正所谓技术服务于业务,如果技术不能解决业务上的痛点,那它存在的意义在哪?咱先分析几个业务场景:
1. 订单支付
2. 系统通知
3. 即时通讯
在很多电商项目中,当我们利用支付宝或者微信扫码支付后,我们要等待网页响应,然后跳转支付结果页面。其实在整个支付过程中,浏览器先向服务器发送支付请求,然后服务端给浏览器返回一个二维码,然后客户扫码支付,但是支付没支付,浏览器是不知道的啊,只有服务端知道,所以浏览器要不停去问服务端:“xxx订单支付成功了吗?”,就像下图这样
这种实现方法很简单,就是ajax轮询。
系统通知:比如有些论坛,当你发了一篇帖子,过一会有人回复你了,这个时候一般系统都会推送给你,告诉你有个新回复。
即时通讯:网页上的在线客服,你与客服的对话。
对于实时性要求不高的业务场景,我们可以用ajax轮询基本就可以实现需求。但是对于实时性要求特别高的呢,比如即时通讯,这你不能一直ajax去轮询吧。
所以我们希望服务端能自己主动通知浏览器,而不是浏览器每次都是自己去问服务端。
所以!WebSocket协议,它来了!
当浏览器和服务端建立WebSocket协议后,他两的对话就变成下图这样了
花了亿点点时间算是大概介绍了一下websocket,和它的作用。
现在到正题了,用Java写一个聊天室!
实现功能如下:好友列表,好友在线状态提醒,一对一聊天,收到新消息提醒。
后端(主要):Spring Boot + Socket.io + Mybatis Plus + Mysql
前端(主要):Vue + Element UI + Socket.io
npm install vue-socket.io --save
// webSocket
import VueSocketIO from 'vue-socket.io'
Vue.use(new VueSocketIO({
debug: true,
connection: `http://127.0.0.1:9999/?token=${token}`, //这里的token是用户登录成功之后服务端给的一个token,用来验证用户
vuex: {
store,
actionPrefix: 'SOCKET_',
mutationPrefix: 'SOCKET_'
}
}))
<template>
//这里是组件内容
</template>
<script>
export default {
name: 'demo',
data() {
return {
}
},
sockets: {
connect() {
console.log('connect连接服务端')
},
disconnect() {
console.log('从服务器断开连接....')
},
// reconnect重连
reconnect() {
console.log('正在尝试重新连接....')
},
//监听服务端的server_event,要与服务端一致
server_event(data) {
console.log('收到服务端的通知',data)
}
}
}
</script>
<dependency>
<groupId>com.corundumstudio.socketiogroupId>
<artifactId>netty-socketioartifactId>
<version>1.7.18version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
dependency>`
# SocketIO配置
socketio:
host: 127.0.0.1
# SocketIO端口
port: 9999
bossCount: 1
# 连接数大小
workCount: 100
# 允许客户请求
allowCustomRequests: true
# 协议升级超时时间(毫秒),默认10秒,HTTP握手升级为ws协议超时时间
upgradeTimeout: 10000
# Ping消息超时时间(毫秒),默认60秒,这个时间间隔内没有接收到心跳消息就会发送超时事件
pingTimeout: 60000
# Ping消息间隔(毫秒),默认25秒。客户端向服务器发送一条心跳消息间隔
pingInterval: 25000
# 设置HTTP交互最大内容长度
maxHttpContentLength: 1048576
# 设置最大每帧处理数据的长度,防止他人利用大数据来攻击服务器
maxFramePayloadLength: 1048576
@Data
@ConfigurationProperties("socketio")
public class AppProperties {
/**
* host
*/
private String host;
/**
* port
*/
private Integer port;
/**
* bossCount
*/
private int bossCount;
/**
* workCount
*/
private int workCount;
/**
* allowCustomRequests
*/
private boolean allowCustomRequests;
/**
* upgradeTimeout
*/
private int upgradeTimeout;
/**
* pingTimeout
*/
private int pingTimeout;
/**
* pingInterval
*/
private int pingInterval;
private int maxHttpContentLength;
private int maxFramePayloadLength;
}
@Slf4j
@Configuration
@EnableConfigurationProperties({
AppProperties.class,
})
public class AppConfiguration {
@Bean
public SocketIOServer socketIoServer(AppProperties appProperties) {
SocketConfig socketConfig = new SocketConfig();
socketConfig.setTcpNoDelay(true);
socketConfig.setSoLinger(0);
com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
config.setSocketConfig(socketConfig);
config.setHostname(appProperties.getHost());
config.setPort(appProperties.getPort());
config.setBossThreads(appProperties.getBossCount());
config.setWorkerThreads(appProperties.getWorkCount());
config.setAllowCustomRequests(appProperties.isAllowCustomRequests());
config.setUpgradeTimeout(appProperties.getUpgradeTimeout());
config.setPingTimeout(appProperties.getPingTimeout());
config.setPingInterval(appProperties.getPingInterval());
config.setMaxHttpContentLength(appProperties.getMaxHttpContentLength());
config.setMaxFramePayloadLength(appProperties.getMaxFramePayloadLength());
return new SocketIOServer(config);
}
}
这里很关键!!!主要是发送和接受消息!
@Slf4j
@Service
@RequiredArgsConstructor
public class MsgSendService {
//这里存放和浏览器的链接,键:SessionId
private static final Map<String, SocketIOClient> CLIENT_MAP = new ConcurrentHashMap<>();
//这里保存用户对应的SessionId的链接,键:UserId 值:SessionId
private static final Map<String, String> USER_CLIENT_MAP = new ConcurrentHashMap<>();
private final SocketIOServer socketIOServer;
/**
* 启动的时候会被调用一次
*/
@PostConstruct
private void autoStart() {
log.info("start ws");
socketIOServer.addConnectListener(client -> {
String token = getClientToken(client, "token");
//这里做了安全校验
if (checkToken(client, token)) {
CLIENT_MAP.put(client.getSessionId().toString(), client);
} else {
client.disconnect();
}
log.info("目前在线用户有:{}", CLIENT_MAP.size());
});
socketIOServer.addDisconnectListener(client -> {
String token = getClientToken(client, "token");
CLIENT_MAP.remove(client.getSessionId().toString());
client.disconnect();
log.info("移除client:{}", client.getSessionId());
});
//监听'sendMsg'的事件
socketIOServer.addEventListener("sendMsg",Integer.class,((socketIOClient, integer, ackRequest) -> {
System.out.println(integer);
}));
socketIOServer.start();
log.info("start finish");
}
private String getClientToken(SocketIOClient client, String key) {
Map<String, List<String>> params = client.getHandshakeData().getUrlParams();
List<String> list = params.get(key);
if (CollUtil.isNotEmpty(list)) {
//这里获取的是用户登录成功后由系统签发的token,所以我们只要校验这个token就行了
return list.get(0);
}
return null;
}
//这里就是主要的验证token方法,根据具体的业务来
private boolean checkToken(SocketIOClient client, String token) {
log.info("检查token是否有效:{}", token);
if (StrUtil.isNotEmpty(token) && !"undefined".equals(token)){
//这里写验证代码
Boolean tokenFlag = true;
if(tokenFlag){
//验证通过,绑定用户和sessionId
USER_CLIENT_MAP.put(userId,client.getSessionId().toString());
return true;
}
}
}
return false;
}
@PreDestroy
private void onDestroy() {
if (socketIOServer != null) {
socketIOServer.stop();
}
}
//具体给浏览器发送通知的方法
public void sendMsg(Object demo) {
CLIENT_MAP.forEach((key, value) -> {
UUID sessionId = value.getSessionId();
System.out.println(sessionId);
//这里很关键server_event一定前后端的事件名称一定要一致
value.sendEvent("server_event", demo);
log.info("发送数据成功:{}", key);
});
}
}
首先,我要道歉,我没有放出具体实现的代码,但是我把最基础的代码放出来了,就是服务端通知浏览器的代码。
主要是因为目前我代码写的太乱了,完全就是一个练手项目。其实我说的功能基本都是基于那两个集合实现的,有兴趣的可以自己捣鼓。