内容简介:WebSocket的故事系列计划分五大篇六章,旨在由浅入深的介绍WebSocket以及在Springboot中如何快速构建和使用WebSocket提供的能力。本系列计划包含如下几篇文章:
WebSocket的故事系列计划分五大篇六章,旨在由浅入深的介绍WebSocket以及在Springboot中如何快速构建和使用WebSocket提供的能力。本系列计划包含如下几篇文章:
第二篇,Spring中如何利用STOMP快速构建WebSocket广播式消息模式
第三篇,Springboot中,如何利用WebSocket和STOMP快速构建点对点的消息模式(1)
第四篇,Springboot中,如何利用WebSocket和STOMP快速构建点对点的消息模式(2)
第五篇,Springboot中,实现网页聊天室之自定义WebSocket消息代理
第六篇,Springboot中,实现更灵活的WebSocket
本篇的主线
本篇将通过一个接近真实的网页聊天室Demo,来详细讲述如何利用WebSocket来实现一些具体的产品功能。本篇将只采用WebSocket本身,不再使用STOMP等这些封装。亲自动手实现消息的接收、处理、发送以及WebSocket的会话管理。 这也是本系列的最重要的一篇,不管你们激不激动,反正我是激动了 。下面我们就开始。
本篇适合的读者
想了解如何在Springboot上自定义实现更为复杂的WebSocket产品逻辑的同学以及各路有志青年。
小小网页聊天室的需求
为了能够目标明确的表达本文中所要讲述的技术要点,我设计了一个小小聊天室产品,先列出需求,这样大家在看后面的实现时能够知其所以然。
以上就是我们本篇要实现的需求。简单说,就是:
用户可加入,退出某房间,加入后可向房间内所有人发送消息,也可向某个人发送悄悄话消息。
需求分析和设计
设计用户存储
很容易想到我们设计的主体就是用户、会话和房间,那么在用户管理上,我们就可以用下面这个图来表示他们之间的关系:
这样我们就可以用一个简单的Map来存储 房间<->用户组 这样的映射关系,在用户组内我们再使用一个Map来存储 用户名<->会话Session 这样的映射关系(假设没有重名)。这样,我们就解决了房间和用户组、用户和会话,这些关系的存储和维护。
设计用户行为与用户的关系
有兄弟看到这说了,“你讲这么半天了,跟之前几篇讲的什么STOMP,什么消息代理,有毛线的关系?”大兄弟你先消消气,我们学STOMP,学消息代理,学点对点消息,重要的是学思想,你说对不?下面我们就用上了。
当用户加入到某房间之后,房间里有任何风吹草动,即有人加入、退出或者发公屏消息,都会“通知”给该用户。到此,我们就可以将创建房间理解成“ 创建消息代理 ”,将用户加入房间,看成是对房间这个“ 消息代理 ”的一个“ 订阅 ”,将用户退出房间,看成是对房间这个“ 消息代理 ”的一个“ 解除订阅 ”。
那么,第一个加入房间的人,我们定义为“ 创建房间 ”,即创建了一个消息代理。为了好理解,上图:
其中红色的小人表示第一个加入房间的用户,即创建房间的人。当某用户发送消息时,如果选择将消息发送给聊天室的所有人,即相当于在房间里发送了一个广播,所有订阅这个房间的用户,都会收到这个广播消息;如果选择发送悄悄话,则只将消息发送给特定用户名的用户,即点对点消息。
总结一下我们要实现的要点:
- 用户存储,即用户,房间,会话之间的关系和对象访问方式。
- 动态创建消息代理(房间),并实现用户对房间的绑定(订阅)。
- 单独发送给某个用户消息的能力。
大体设计就到此为止,还有一些细节,我们先来看一下演示效果,再来看通过代码来讲解实现。
聊天室效果展示
用浏览器打开客户端页面后,展示输入框和加入按钮。输入 房间号1 和用户名 小铭 , 点击进入房间 。
进入房间成功后,展示 当前房间人数和欢迎语 。
当有其他人加入或退出房间时,展示通知信息。可以发送公屏消息和私聊消息。
下面就让我们看一下这些主要功能如何来实现吧。
代码实现
按照我们上述的设计,我会着重介绍重点部分的代码设计和技术要点。
服务端实现
1. 配置WebSocket
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyHandler(), "/webSocket/{INFO}").setAllowedOrigins("*")
.addInterceptors(new WebSocketInterceptor());
}
}
复制代码
要点解析:
- 注册
WebSocketHandler(MyHandler),这是用来处理WebSocket建立以及消息处理的类,后面会详细介绍。 - 注册
WebSocketInterceptor拦截器,此拦截器用来在客户端向服务端发起初次连接时,记录客户端拦截信息。 - 注册WebSocket地址,并附带了
{INFO}参数,用来注册的时候携带用户信息。
以上都会在后续代码中详细介绍。
2. 实现握手拦截器
public class WebSocketInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
if (serverHttpRequest instanceof ServletServerHttpRequest) {
String INFO = serverHttpRequest.getURI().getPath().split("INFO=")[1];
if (INFO != null && INFO.length() > 0) {
JSONObject jsonObject = new JSONObject(INFO);
String command = jsonObject.getString("command");
if (command != null && MessageKey.ENTER_COMMAND.equals(command)) {
System.out.println("当前session的ID="+ jsonObject.getString("name"));
ServletServerHttpRequest request = (ServletServerHttpRequest) serverHttpRequest;
HttpSession session = request.getServletRequest().getSession();
map.put(MessageKey.KEY_WEBSOCKET_USERNAME, jsonObject.getString("name"));
map.put(MessageKey.KEY_ROOM_ID, jsonObject.getString("roomId"));
}
}
}
return true;
}
}
复制代码
要点解析:
-
HandshakeInterceptor用来拦截客户端第一次连接服务端时的请求,即客户端连接/webSocket/{INFO}时,我们可以获取到对应INFO的信息。 - 实现
beforeHandshake方法,进行用户信息保存,这里我们将用户名和房间号保存到Session上。
3. 实现消息处理器WebSocketHandler
public class MyHandler implements WebSocketHandler {
//用来保存用户、房间、会话三者。使用双层Map实现对应关系。
private static final Map<String, Map<String, WebSocketSession>> sUserMap = new HashMap<>(3);
//用户加入房间后,会调用此方法,我们在这个节点,向其他用户发送有用户加入的通知消息。
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("成功建立连接");
String INFO = session.getUri().getPath().split("INFO=")[1];
System.out.println(INFO);
if (INFO != null && INFO.length() > 0) {
JSONObject jsonObject = new JSONObject(INFO);
String command = jsonObject.getString("command");
String roomId = jsonObject.getString("roomId");
if (command != null && MessageKey.ENTER_COMMAND.equals(command)) {
Map<String, WebSocketSession> mapSession = sUserMap.get(roomId);
if (mapSession == null) {
mapSession = new HashMap<>(3);
sUserMap.put(roomId, mapSession);
}
mapSession.put(jsonObject.getString("name"), session);
session.sendMessage(new TextMessage("当前房间在线人数" + mapSession.size() + "人"));
System.out.println(session);
}
}
System.out.println("当前在线人数:" + sUserMap.size());
}
//消息处理方法
@Override
public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) {
try {
JSONObject jsonobject = new JSONObject(webSocketMessage.getPayload().toString());
Message message = new Message(jsonobject.toString());
System.out.println(jsonobject.toString());
System.out.println(message + ":来自" + webSocketSession.getAttributes().get(MessageKey.KEY_WEBSOCKET_USERNAME) + "的消息");
if (message.getName() != null && message.getCommand() != null) {
switch (message.getCommand()) {
//有新人加入房间信息
case MessageKey.ENTER_COMMAND:
sendMessageToRoomUsers(message.getRoomId(), new TextMessage("【" + getNameFromSession(webSocketSession) + "】加入了房间,欢迎!"));
break;
//聊天信息
case MessageKey.MESSAGE_COMMAND:
if (message.getName().equals("all")) {
sendMessageToRoomUsers(message.getRoomId(), new TextMessage(getNameFromSession(webSocketSession) +
"说:" + message.getInfo()
));
} else {
sendMessageToUser(message.getRoomId(), message.getName(), new TextMessage(getNameFromSession(webSocketSession) +
"悄悄对你说:" + message.getInfo()));
}
break;
//有人离开房间信息
case MessageKey.LEAVE_COMMAND:
sendMessageToRoomUsers(message.getRoomId(), new TextMessage("【" + getNameFromSession(webSocketSession) + "】离开了房间,欢迎下次再来"));
break;
default:
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 发送信息给指定用户
*/
public boolean sendMessageToUser(String roomId, String name, TextMessage message) {
if (roomId == null || name == null) return false;
if (sUserMap.get(roomId) == null) return false;
WebSocketSession session = sUserMap.get(roomId).get(name);
if (!session.isOpen()) return false;
try {
session.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 广播信息给某房间内的所有用户
*/
public boolean sendMessageToRoomUsers(String roomId, TextMessage message) {
if (roomId == null) return false;
if (sUserMap.get(roomId) == null) return false;
boolean allSendSuccess = true;
Collection<WebSocketSession> sessions = sUserMap.get(roomId).values();
for (WebSocketSession session : sessions) {
try {
if (session.isOpen()) {
session.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
allSendSuccess = false;
}
}
return allSendSuccess;
}
//退出房间时的处理
@Override
public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) {
System.out.println("连接已关闭:" + closeStatus);
Map<String, WebSocketSession> map = sUserMap.get(getRoomIdFromSession(webSocketSession));
if (map != null) {
map.remove(getNameFromSession(webSocketSession));
}
}
}
复制代码
要点解析:
- 使用
sUserMap这个静态变量来保存用户信息。对应我们上述的关系图。 - 实现
afterConnectionEstablished方法,当用户进入房间成功后,保存用户信息到Map,并调用sendMessageToRoomUsers广播新人加入信息。 - 实现
handleMessage方法,处理用户加入,离开和发送信息三类消息。 - 实现
afterConnectionClosed方法,用来处理当用户离开房间后的信息销毁工作。从Map中清除该用户。 - 实现
sendMessageToUser、sendMessageToRoomUsers两个向客户端发送消息的方法。直接通过Session即可发送结构化数据到客户端。sendMessageToUser实现了点对点消息的发送,sendMessageToRoomUsers实现了广播消息的发送。
客户端实现
客户端我们就使用HTML5为我们提供的WebSocket JS接口即可。
<html>
<script type="text/javascript">
function ToggleConnectionClicked() {
if (SocketCreated && (ws.readyState == 0 || ws.readyState == 1)) {
lockOn("离开聊天室...");
SocketCreated = false;
isUserloggedout = true;
var msg = JSON.stringify({'command':'leave', 'roomId':groom , 'name': gname,
'info':'离开房间'});
ws.send(msg);
ws.close();
} else if(document.getElementById("roomId").value == "请输入房间号!") {
Log("请输入房间号!");
} else {
lockOn("进入聊天室...");
Log("准备连接到聊天服务器 ...");
groom = document.getElementById("roomId").value;
gname = document.getElementById("txtName").value;
try {
if ("WebSocket" in window) {
ws = new WebSocket(
'ws://localhost:8080/webSocket/INFO={"command":"enter","name":"'+ gname + '","roomId":"' + groom + '"}');
}
else if("MozWebSocket" in window) {
ws = new MozWebSocket(
'ws://localhost:8080/webSocket/INFO={"command":"enter","name":"'+ gname + '","roomId":"' + groom + '"}');
}
SocketCreated = true;
isUserloggedout = false;
} catch (ex) {
Log(ex, "ERROR");
return;
}
document.getElementById("ToggleConnection").innerHTML = "断开";
ws.onopen = WSonOpen;
ws.onmessage = WSonMessage;
ws.onclose = WSonClose;
ws.onerror = WSonError;
}
};
function WSonOpen() {
lockOff();
Log("连接已经建立。", "OK");
$("#SendDataContainer").show();
var msg = JSON.stringify({'command':'enter', 'roomId':groom , 'name': "all",
'info': gname + "加入聊天室"})
ws.send(msg);
};
</html>
复制代码
要点解析:
- 发起服务端连接时,注意地址信息:
'ws://localhost:8080/webSocket/INFO={"command":"enter","name":"'+ gname + '","roomId":"' + groom + '"}',这里我们在INFO后加入了用户个人信息,服务端收到后,即可根据这个信息标记此会话。 - 连接建立后,发送给房间内其他人一条加入信息。通过
ws.send()方法实现。
至此代码部分就介绍完了,过多的代码就不再堆叠了,更详细的代码,请参见后面的Github地址。
本篇总结
通过一个相对完整的网页聊天室例子,介绍了我们自己使用WebSocket时的几个细节:
- 服务端想在建立连接,即握手阶段搞事情,实现
HandshakeInterceptor。 - 服务端想在建立连接之后和处理客户端发来的消息,实现
WebSocketHandler。 - 服务端通过
WebSocketSession即可向客户端发送消息,通过用户和Session的绑定,实现对应关系。
想加深理解的同学,还是要深入到代码中仔细体会。限于篇幅,而且在文章中加入大量代码本身也不容易读下去。所以大家还是要实际对着代码理解比较好。
本篇涉及到的代码
欢迎持续关注原创,喜欢的别忘了收藏关注,码字实在太累,你们的鼓励就是我坚持的动力!
小铭出品,必属精品
欢迎关注xNPE技术论坛,更多原创干货每日推送。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Pro Django
Marty Alchin / Apress / 2008-11-24 / USD 49.99
Django is the leading Python web application development framework. Learn how to leverage the Django web framework to its full potential in this advanced tutorial and reference. Endorsed by Django, Pr......一起来看看 《Pro Django》 这本书的介绍吧!