前言
上篇文章我们用STOMP子协议实现了在线群聊和一对一聊天室等功能,本篇我们继续WebSocket这个话题,这次我们换个实现维度:用原生的WebSocket来实现,看看这两者在实现上的差别有多大。
实战WebSocket的要点
一、WebSocket重要属性
属性 |
备注 |
Socket.readyState |
只读属性 readyState 表示连接状态,可以是以下值: 0 - 表示连接尚未建立。 1 - 表示连接已建立,可以进行通信。 2 - 表示连接正在进行关闭。 3 - 表示连接已经关闭或者连接不能打开。 |
Socket.bufferedAmount |
只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。 |
二、WebSocket核心事件
事件 |
事件处理程序 |
备注 |
open |
Socket.onopen |
连接建立时触发 |
message |
Socket.onmessage |
客户端接收服务端数据时触发 |
error |
Socket.onerror |
通信发生错误时触发 |
close |
Socket.onclose |
连接关闭时触发 |
三、WebSocket核心方法
方法 |
备注 |
Socket.send() |
使用连接发送数据 |
Socket.close() |
连接关闭 |
代码设计实现
一、服务端部分
/**
*
@author andychen https://blog.51cto.com/14815984
*
@description:WebSocket配置
*/
@Configuration
public class WebSocketConfig {
/**
* 注册并开启WebSocket
*
@return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
/**
*
@author andychen https://blog.51cto.com/14815984
*
@description:WebSocket通信业务类
*/
@ServerEndpoint("/ws/server")
@Component
public class WebSocketController {
private static final Logger log = LoggerFactory.getLogger(WebSocketController.class);
/**
* 服务端连接计数器
*/
private static final AtomicInteger counter = new AtomicInteger(0);
/**
* 定义客户端会话安全容器
* 缓存客户端会话对象(正式环境,这里可以直接做分布式缓存)
*/
private static final CopyOnWriteArraySet<Session> sessionContainer = new CopyOnWriteArraySet<>();
/**
* 定义客户端会话和用户身份映射安全容器
*/
private static final Map<String,String> sessionMap = new ConcurrentHashMap<>();
/**
* 消息分隔字符窜
*/
private static final String MSG_SPLIT_STR = "@#@";
/**
* 消息角色
*/
private static final String[] MSG_ROLES = {"sender","recevier"};
/**
* WebSocket连接打开事件
*
@param session 客户端连接会话
*/
@OnOpen
public void open(Session session){
//缓存会话
sessionContainer.add(session);
//会话Id
String sessionId = session.getId();
if
(!sessionMap.containsKey(sessionId)){
String receiver = this.getRecevier(session);
boolean
isMass = (null == receiver);
//消息用户:群聊为发送者,单聊时为发送者和接收者
String usrInfo = parseMsgParameter(session, MSG_ROLES[0]);
if
(isMass){
sessionMap.put(sessionId, usrInfo);
}else{
usrInfo += MSG_SPLIT_STR+receiver;
sessionMap.put(usrInfo, sessionId);
}
//发送新用户加入消息
if(isMass){
sendMass("系统消息"+MSG_SPLIT_STR+"用户["+usrInfo+"]加入群聊");
}
log.info("会话[{}]加入,当前连接数为:{}", sessionId, counter.incrementAndGet());
}
}
/**
* 接收客户端消息事件
*
@param message 文本消息(也支持对象、二进制Buffer)
*
@param session 客户端连接会话
*/
@OnMessage
public void accept(String message, Session session){
String sender = null;
String sessionId = session.getId();
String sessionId2 = null;
String msg =null;
String recevier = getRecevier(session);
if
(null == recevier){
msg = sessionMap.get(sessionId)+MSG_SPLIT_STR+message;
sendMass(msg);
}else{
sender = parseMsgParameter(session, MSG_ROLES[0]);
msg = sender+MSG_SPLIT_STR+message;
//发送者sessionId
sessionId = sender+MSG_SPLIT_STR+recevier;
sessionId = sessionMap.get(sessionId);
//接收者sessionId
sessionId2 = recevier+MSG_SPLIT_STR+sender;
sessionId2 = sessionMap.get(sessionId2);
sendSingle(sessionId, sessionId2, msg);
}
log.info("已接收客户端[{}]消息:{},请求地址:{}", sessionId, message, session.getRequestURI().toString());
}
/**
* 连接关闭事件
*
@param session 客户端连接会话
*/
@OnClose
public void close(Session session){
String sessionId = session.getId();
sessionContainer.remove(session);
String recevier =getRecevier(session);
if
(null == recevier){
//群聊发送退群消息
String sender = sessionMap.get(sessionId);
sessionMap.remove(sessionId);
sendMass("系统消息"+MSG_SPLIT_STR+"用户["+sender+"]退出群聊");
}else{
sessionId = parseMsgParameter(session, MSG_ROLES[0])+MSG_SPLIT_STR+recevier;
sessionId = sessionMap.get(sessionId);
sessionMap.remove(sessionId);
}
log.info("会话[{}]关闭连接,当前连接数为:{}", sessionId, counter.decrementAndGet());
}
/**
* 连接发生错误事件
*
@param session 客户端连接会话
*
@param error 错误对象
*/
@OnError
public void error(Session session, Throwable error){
log.error("连接发生错误:{}, \n\n客户端会话ID[{}],请求地址:{}", error.getMessage(),
session.getId(), session.getRequestURI().toString());
error.printStackTrace();
}
/**
* 是否单聊
*
@param session 客户会话id
*
@return
*/
private String getRecevier(Session session){
return parseMsgParameter(session, MSG_ROLES[1]);
}
/**
* 解析消息参数
*
@param session 客户端会话
*
@param name 参数名称
*
@return
*/
private static String parseMsgParameter(Session session, String name){
//获取会话中包含的参数信息
Map<String, List<String>> params = session.getRequestParameterMap();
if
(params.containsKey(name)){
return params.get(name).get(0);
}
return null;
}
/**
* 发送消息
*
@param session 客户端会话
*
@param msg 消息内容
*/
private static boolean send(Session session, String msg){
try {
//异步转发文本消息(也可发送消息对象,二进制流等)
session.getAsyncRemote().sendText(msg);
return true;
} catch (Exception e) {
log.error("消息发送失败:{}", e.getMessage());
e.printStackTrace();
}
return false;
}
/**
* 群发消息
*
@param msg 消息内容
*/
private static void sendMass(String msg){
for (Session session : sessionContainer){
if(session.isOpen()){
//发送
send(session, msg);
}
}
}
/**
* 发送聊消息
*
@param senderSid 发送者会话id
*
@param recevSid 接收者会话id
*
@param msg 消息内容
*/
private static void sendSingle(String senderSid, String recevSid, String msg){
String id = null;
int
count = 0;
for
(Session s : sessionContainer) {
id = s.getId();
if
(senderSid.equals(id)) {
count++;
send(s, msg);
}
if (recevSid.equals(id)) {
count++;
send(s, msg);
}
if(2 == count){break;}
}
if(2 > count){
log.warn("未找到指定会话[ID: {}或{}]", senderSid, recevSid);
}
}
}
二、客户端部分
<!DOCTYPE html>
<html
xmlns:th="http://www.thymeleaf.org">
<head>
<meta
charset="UTF-8">
<meta
name="aplus-terminal" content="1">
<meta
name="apple-mobile-web-app-title" content="">
<meta
name="apple-mobile-web-app-capable" content="yes">
<meta
name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta
name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<meta
name="format-detection" content="telephone=no, address=no">
<title>
WebSocket在线聊天室</title>
<link
rel="stylesheet" th:href="@{/css/chatroom.css}" type="text/css"/>
</head>
<body>
<div>
<div
class="window_frame">
<span><e
style="font-weight: bold;">选择你的网名:</e>
<select
id="selectSender">
<option
value="">请选择..</option>
<option
value="zhangsan">zhangsan</option>
<option
value="lisi">lisi</option>
<option
value="wangwu">wangwu</option>
<option
value="zhaoliu">zhaoliu</option>
<option
value="chenqi">chenqi</option>
<option
value="qianba">qianba</option>
</select>
<e
style="font-weight: bold;">群聊:</e>
</span>
<div
class="chatWindow">
<section
class="chatRecord">
<div
id="mass_div" class="mobile-page"></div>
</section>
<section
class="sendWindow">
<textarea
name="txtContent" id="txtContent" class="send_box"></textarea>
<input
type="button" id="btnSend" value="发送" class="send_btn"/>
</section>
</div>
</div>
<div
class="window_frame">
<span><e
style="font-weight: bold;">选择聊天的对象:</e>
<select
id="selectRecevier">
<option
value="">请选择..</option>
<option
value="zhangsan">zhangsan</option>
<option
value="lisi">lisi</option>
<option
value="wangwu">wangwu</option>
<option
value="zhaoliu">zhaoliu</option>
<option
value="chenqi">chenqi</option>
<option
value="qianba">qianba</option>
</select>
<e
style="font-weight: bold;">单聊:</e>
</span>
<div
class="chatWindow">
<section
class="chatRecord">
<div
id="single_div" class="mobile-page"></div>
</section>
<section
class="sendWindow">
<textarea
name="txtContent2" id="txtContent2" class="send_box"></textarea>
<input
type="button" id="btnSend2" value="发送" class="send_btn"/>
</section>
</div>
</div>
</div>
<script
type="text/javascript" th:src="@{/js/jquery-1.9.1.min.js}"></script>
<script
type="text/javascript" th:src="@{/js/wschatroom.js}"></script>
</body>
</html>
/**
* WS-WebSocket在线聊天室类
* 负责实现群聊和单聊相关的聊天业务
*/
WsChatRoom = {
socket: null,
sys_msg_tag:'系统消息',
msg_split_str:'@#@',//消息分隔
isMass: true //是否群发
};
/**
* 选择发送者
*/
WsChatRoom.selectSender = function () {
let sender = $("#selectSender").val();
if("" === sender){
alert("请选择你的聊天身份!");
return;
}
WsChatRoom.switchUser(sender);
};
/**
* 选择接收者
*/
WsChatRoom.selectRecevier = function () {
let sender = $("#selectSender").val();
if("" === sender){
alert("请选择你的聊天身份!");
return;
}
let recevier = $("#selectRecevier").val();
if("" === recevier){
alert("请选择对方的聊天身份!");
return;
}
WsChatRoom.switchUser(sender, recevier);
};
/**
* 切换用户
*/
WsChatRoom.switchUser = function (sender, recevier) {
//先关闭之前连接
WsChatRoom.close();
//连接服务器端
let url = "ws://localhost:8089/ws/server?sender="+sender;
if(recevier && null !== recevier && "" !== recevier){
url += ("&recevier="+recevier);
WsChatRoom.isMass = false;
}else{
WsChatRoom.isMass = true;
}
WsChatRoom.socket = new WebSocket(url);
//打开连接事件
WsChatRoom.socket.onopen = function (data) {
console.log("Socket连接已建立");
}
//接收消息事件
WsChatRoom.socket.onmessage = function (msg) {
let aData = msg.data.split(WsChatRoom.msg_split_str);
let sender = aData[0];
let content = aData[1];
let container = $("#mass_div");
let current = $("#selectSender").val();
if(!WsChatRoom.isMass){
container = $("#single_div");
}
//当前用户发的消息 WsChatRoom.isMass &&
if(current === sender && WsChatRoom.sys_msg_tag !== sender){
container.append("<div class='user-group'>" +
" <div class='user-msg'>" +
" <span class='user-reply'>"+content+"</span>" +
" <i class='triangle-user'></i>" +
" </div><span style='padding-top:10px;'>" +sender+
" </span></div>");
}
else{
//系统消息
if(WsChatRoom.sys_msg_tag === sender){
$("#mass_div").append(" <div class='admin-group'><span class='msg_head'>"+
sender+
"</span><div class='admin-msg'>"+
" <i class='triangle-sys'></i>"+
" <span class='sys-reply'>"+content+"</span>"+
"</div>"+
"</div>");
}else{
container.append(" <div class='admin-group'><span class='msg_head'>"+
sender+
"</span><div class='admin-msg'>"+
" <i class='triangle-admin'></i>"+
" <span class='admin-reply'>"+content+"</span>"+
"</div>"+
"</div>");
}
}
}
//关闭连接事件
WsChatRoom.socket.onclose = function (data) {
console.log("Socket连接已关闭");
}
//连接异常事件
WsChatRoom.socket.onerror = function (e) {
console.log("Socket连接出错:"+e);
}
};
/**
* 发送消息
*/
WsChatRoom.send = function () {
let sender = $("#selectSender").val();
if("" === sender){
alert("请选择你的聊天身份!");
return;
}
let content = "";
if(WsChatRoom.isMass){
content = $("#txtContent").val().trim();
}else{
content = $("#txtContent2").val().trim();
}
if("" === content){
alert("发送的消息不能为空!");
return;
}
if(!WsChatRoom.isMass && "" === $("#selectRecevier").val()){
alert("请选择对方的聊天身份!");
return;
}
//发送消息
WsChatRoom.socket.send(content);
if(WsChatRoom.isMass){
$("#txtContent").val("");
}else{
$("#txtContent2").val("");
}
};
/**
* 关闭连接
*/
WsChatRoom.close = function(){
if(null != WsChatRoom.socket){
WsChatRoom.socket.close();
console.log("连接已关闭");
}
}
/**
* 窗口关闭时,关闭连接
*/
window.onload = function(){
WsChatRoom.close();
}
/**
* 页面加载完毕事件
*/
$(function () {
//注册事件
$("#selectSender").change(function () {
WsChatRoom.selectSender();
});
$("#selectRecevier").change(function () {
WsChatRoom.selectRecevier();
});
$("#btnSend").click(function () {
//发送时为单聊,这里需要切换
if(!WsChatRoom.isMass){
WsChatRoom.selectSender();
}
WsChatRoom.send();
});
$("#btnSend2").click(function () {
//发送时为群聊,这里需要切换
if(WsChatRoom.isMass){
WsChatRoom.selectRecevier();
}
WsChatRoom.send();
});
});
结果验证
一、群聊效果
二、单聊效果
总结
从实现角度原生HTML5的WebSocket在客户端比STOMP协议的实现方式要简洁清晰一些,不要额外依赖第三放的组件或插件;而服务器端的实现比STOMP协议实现上略为复杂一点(需要对客户端Session进行管理)。从功能维度讲,原生WebSocket不只支持文本数据传输,同时也支持对象和二进制流等传输方式,功能更强大;而STOMP只支持文本消息。从通信效率上看,STOMP协议的实现服务器端延迟更少(实现更简单高效)。这两种方式,我们可根据项目的具体业务场景选择使用。后面我们将看看Netty在实现这类通信实时性要求较高场景的表现,请继续关注!
来源:oschina
链接:https://my.oschina.net/u/4308002/blog/4365666