WebSocket是基于TCP的一种新的网络协议,最大的特点就是实现了浏览器与服务器全双工(full-duplex)通信,2011年WebSocket协议被接受为国际标准
HTML5开始提供WebSocket协议的支持。
什么叫全双工通讯? 其实就是通信的双方都可以主动发送消息,又可以接受消息.WebSocket协议之前,全双工通信是通过多个http链接来实现,这导致了效率低下。WebSocket的出现就是为了解决这个问题。
先说一下我们在项目中经常会遇到的一种需求:
现在很多项目都分为app 端和管理端, 经常就会有一些需求要求管理端页面能够实时的监控一些用户的动作或消息.
比如: 订单超时时,要求管理平台能够实时弹出订单超时提醒. 如果这种实时的消息推送是从服务端向app推送消息,通常使用极光推送就可以完成.
但是一般的管理端都是浏览器,而不是app,而服务器是无法主动向浏览器推送消息的。对于这种需求之前的做法就是不得不使用轮询让浏览器每隔几秒钟就向后台请求一次数据 ,但是这无疑会带来巨大的性能开销.而有了WebSocket我们就可以让服务器主动的向浏览器去推送数据了。
下边我们就使用WebSocket完成一个简单的网页聊天室小功能,来感受一下WebSocket的消息推送能力吧
测试环境:spingBoot + tomcat7 + jdk8 + chrome浏览器(个别浏览器可能不支持)
服务端:
添加WebSocket依赖:
<!--websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
配置ServerEndpointExporter
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
发布一个EndPoint
其中各个方法的作用都写在了注释中了
@Slf4j
@ServerEndpoint("/websocket/{userId}")
@Component
public class WebSocketServer {
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
//创建一个线程安全的map
private static Map<String,WebSocketServer> users = Collections.synchronizedMap(new HashMap());
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
//放入map中的key,用来表示该连接对象
private String username;
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String username) {
this.session=session;
this.username =username;
users.put(username,this); //加入map中,为了测试方便使用username做key
addOnlineCount(); //在线数加1
log.info(username+"加入!当前在线人数为" + getOnlineCount());
try {
this.session.getBasicRemote().sendText("连接成功");
} catch (IOException e) {
log.error("websocket IO异常");
}
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
users.remove(this.username); //从set中删除
subOnlineCount(); //在线数减1
log.info("一个连接关闭!当前在线人数为" + getOnlineCount());
}
/**
* 收到客户端消息后触发的方法
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message) {
log.info("来自客户端的消息:" + message);
//群发消息
try {
if (StringUtils.isEmpty(message)){
return ;
}
//如果给所有人发消息携带@ALL, 给特定人发消息携带@xxx@xxx#message
String[] split = message.split("#");
if (split.length>1){
String[] users = split[0].split("@");
if (users.length<2){return;}
String firstuser = users[1].trim();
if (StringUtils.isEmpty(firstuser)||"ALL".equals(firstuser.toUpperCase())){
String msg =username +": "+ split[1];
sendInfo(msg);//群发消息
}else{//给特定人员发消息
for (String user : users) {
if (!StringUtils.isEmpty(user.trim())){
sendMessageToSomeBody(user.trim(),split[1]);
}
}
}
}else{
sendInfo(username +": "+message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误 session: "+session);
error.printStackTrace();
}
// 给特定人员发送消息
public void sendMessageToSomeBody(String username,String message) throws IOException {
if(users.get(username)==null){
return;
}
users.get(username).session.getBasicRemote().sendText(message);
this.session.getBasicRemote().sendText(this.username+"@"+username+": "+message);
}
/**
* 群发自定义消息
*/
public void sendInfo(String message) throws IOException {
for (WebSocketServer item : users.values()) {
try {
item.session.getBasicRemote().sendText(message);
} catch (IOException e) {
continue;
}
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
}
页面(客户端)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="jquery.js"></script>
<script type="application/javascript">
var socket ;
//登录过后初始化socket连接
function initSocket(userId) {
if(typeof(WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
}else {
console.log("您的浏览器支持WebSocket/websocket");
}
//socket连接地址: 注意是ws协议
socket = new WebSocket("ws://localhost/websocket/"+userId);
socket.onopen = function() {
console.log("Socket 已打开");
};
//获得消息事件
socket.onmessage = function(msg) {
var histroy = $("#history").val();
$("#history").val(histroy+"\r\n"+msg.data);
console.log($(msg));
};
//关闭事件
socket.onclose = function() {
console.log("Socket已关闭");
};
//错误事件
socket.onerror = function() {
alert("Socket发生了错误");
}
$(window).unload(function(){
socket.close();
});
}
//点击按钮发送消息
function send() {
console.log("发送消息: "+$("#msg").val());
socket.send($("#msg").val());
}
//登录
function login() {
$.ajax({
url: "/login",
data: $("#loginForm").serialize(),
type: "POST",
success: function (userId) {
if ( userId){
console.log("登录成功!");
initSocket(userId);
}
}
});
}
</script>
</head>
<body>
<form id="loginForm" >
用户名: <input name="username"><br>
密 码: <input name="password">
<br>
<input type="button" value="登录" onclick="login()" />
</form>
<textarea id="msg" placeholder="格式:@xxx#消息 , 或者@ALL#消息" style="width: 500px;height: 50px" ></textarea>
<input type="button" onclick="send()" value="发送消息" >
<br>
<textarea id="history" style="width: 500px;height: 200px ; max-lines: 10" >
</textarea>
</body>
</html>
对应的Controller
/**
* Created by Sky
*/
@RestController
public class WebSocketController {
@Autowired
WebSocketServer server;
@PostMapping("/login")
public String login(String username,String password) throws IOException {
//TODO: 校验密码
server.sendInfo(username + "进入了聊天室!");
return username;
}
}
代码比较完整,大家可以先直接拿去跑起来,然后再去细看其中的使用方式
测试流程:
- 填写username / password 登录(username会最为一个用户的标识,不要重复!)
- 在新窗口, 再登录两个用户
-
登录成功就可以发送群体消息或者给指定用户发送消息了,
消息格式:
@xxx#消息 , 给指定用户发送消息发送
@ALL#消息 , 发送群体消息, 直接发送也是群体消息
希望这个示例能够帮助大家学习WebSocket
最终效果图:
image.png
网友评论