一、基本概念
WebSocket 是一种网络通信协议,如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数 Web 应用程序将通过频繁的异步请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)所以这里使用WebSocket 通过登录后跳转到首页,向后台WebSocket 建立长链接来达到"即使通讯",随着用户页面打开或关闭后台群发消息来实时更改页面显示的人数,当然这里目前不涉及登录后的上线下线以及帐号登录挤掉功能,如果需要可以通过发送消息来改变。
二、SpringBoot 后台实现WebSocket
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
WebSocketConfig.java
package com.kero99.socket;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocketController.java
package com.kero99.socket;
import java.io.IOException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/scoket")
public class WebSocketController {
// @Autowired
// private RedisOperator redisOperator;
@RequestMapping(value="/sendAll", method=RequestMethod.GET)
String sendAllMessage(@RequestParam(required=true) String message){
try {
WebSocketServer.BroadCastInfo(message);
} catch (IOException e) {
e.printStackTrace();
}
return "ok";
}
@RequestMapping(value="/sendOne", method=RequestMethod.GET)
String sendOneMessage(@RequestParam(required=true) String message,@RequestParam(required=true) String id){
try {
WebSocketServer.SendMessage(id,message);
} catch (IOException e) {
e.printStackTrace();
}
return "ok";
}
}
WebSocketServer.java
package com.kero99.socket;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@ServerEndpoint(value = "/websocket")
@Component
public class WebSocketServer {
private final static Logger log = LoggerFactory.getLogger(WebSocketServer.class);
private static final AtomicInteger OnlineCount = new AtomicInteger(0);
// concurrent包的线程安全Set,用来存放每个客户端对应的Session对象。
private static CopyOnWriteArraySet<Session> SessionSet = new CopyOnWriteArraySet<Session>();
@OnOpen
public void onOpen(Session session) throws IOException {
SessionSet.add(session);
int personCount = OnlineCount.incrementAndGet(); // 在线数加1
System.out.println("有连接加入,当前连接数为:"+personCount);
log.info("有连接加入,当前连接数为:{}", personCount);
// SendMessage(session, "连接成功,当前连接人数为:"+personCount);
// SendMessage(session,String.valueOf(personCount));
BroadCastInfo(String.valueOf(OnlineCount.get()));
}
@OnClose
public void onClose(Session session) throws IOException {
int personCount = OnlineCount.decrementAndGet();
System.out.println("有连接关闭,当前连接数为:"+personCount);
log.info("有连接关闭,当前连接数为:{}", personCount);
SessionSet.remove(session);
}
@OnMessage
public void onMessage(String message, Session session) throws IOException {
log.info("来自客户端的消息:{}",message);
// System.out.println("来自客户端的消息:"+message);
// SendMessage(session, "收到消息,消息内容:"+message);
if(message.equals("管理平台")) {
System.out.println("收到平台类型:"+message);
}
// if(message.equals("新增人数")) {
// System.out.println("打开页面:"+message);
// BroadCastInfo(String.valueOf(OnlineCount.get()+1));
// }
if(message.equals("关闭页面")) {
System.out.println("收到关闭页面:"+message);
//在线数加-1
BroadCastInfo(String.valueOf(OnlineCount.get()-1));
}
}
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误:{},Session ID: {}",error.getMessage(),session.getId());
System.out.println("发生错误:{},Session ID: "+error.getMessage()+session.getId());
error.printStackTrace();
}
public static void SendMessage(Session session, String message) {
try {
session.getBasicRemote().sendText(message);
// session.getBasicRemote().sendText(String.format("%s (From Server,Session ID=%s)",message,session.getId()));
} catch (IOException e) {
log.error("发送消息出错:{}", e.getMessage());
System.out.println("发送消息出错:{}"+e.getMessage());
e.printStackTrace();
}
}
public static void BroadCastInfo(String message) throws IOException {
for (Session session : SessionSet) {
if(session.isOpen()){
SendMessage(session, message);
}
}
}
public static void SendMessage(String sessionId,String message) throws IOException {
Session session = null;
for (Session s : SessionSet) {
if(s.getId().equals(sessionId)){
session = s;
break;
}
}
if(session!=null){
SendMessage(session, message);
}
else{
log.warn("没有找到你指定ID的会话:{}",sessionId);
System.out.println("没有找到你指定ID的会话:"+sessionId);
}
}
}
三、React+Umi+Antd 实现前端 WebSocket 通讯
js 需要安装 socket.io
命令: npm socket.io 或者 yarn add socket.io
componentDidMount() {
let ws = new WebSocket("ws://localhost:12935/20191108_V1.0_xdnx/websocket");
if (typeof (WebSocket) == "undefined") {
console.log("遗憾:您的浏览器不支持WebSocket");
} else {
console.log("恭喜:您的浏览器支持WebSocket");
ws.onopen = (evt)=> {
console.log("Connection open ...");
ws.send("管理平台");
ws.send("新增人数");
};
ws.onmessage = (evt)=> {
console.log( "Received Message: " + evt.data);
// alert(evt.data)
//this.state.messageData 为接受数据的变量
let messageData=this.state.messageData;
this.setState({
messageData:evt.data
})
// ws.close();
};
ws.onclose = (evt)=> {
// alert(evt.data)
console.log("Connection closed.");
// ws.close();
};
ws.onerror = (evt)=> {
console.log("error")
};
window.onbeforeunload = (event)=> {
console.log("关闭WebSocket连接!");
ws.send("关闭页面");
event.close();
}
}
}
render html
这里用的antd的统计数值控件
<div style={{
background: '#ececec',
padding: '10px',
width:'20%',
float:'left',marginTop:'20px'
}}>
<Row gutter={16}>
<Col span={12}>
<Card>
<Statistic
title="管理平台当前在线人数"
value={this.state.messageData}
precision={0}
valueStyle={{ color: '#3f8600' }}
suffix="人"
/>
</Card>
</Col>
</Row>
</div>
四、实现结果
五、关于部署
如果使用Tomcat部署war包下面这段可以注释掉,由Tomcat管理,页面路径需要改成和服务器一致的,不用加Http开头
eg: ws://localhost:12935/20191108_V1.0_xdnx/websocket
//package com.kero99.socket;
//
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration;
//import org.springframework.stereotype.Component;
//import org.springframework.web.socket.server.standard.ServerEndpointExporter;
//
//
//@Configuration
//@Component
//public class WebSocketConfig {
//
// @Bean
// public ServerEndpointExporter serverEndpointExporter() {
// return new ServerEndpointExporter();
// }
//
//}