1、WebSocket
1.1、介绍
WebSocket是一种网络通信协议,是HTML5开始提供的一种在单个TCP连接上进行全双工通讯的协议。RFC6455定义了它的通信标准。
1.2、对比HTTP协议
回顾HTTP 协议,它是一种无状态的、无连接的、单向的应用层协议。它采用了请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理。这种通信模型有一个很明显的弊端:
这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数web应用程序将通过频繁的异步AJAX请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。
1.3、协议头
WebSocket协议有两部分,握手和数据传输
- 握手时基于HTTP协议的,客户端发送的握手请求如下形式
GET ws://localhost/chat HTTP/1.1
Host: localhost
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Key: dGh1IHNhbXBSZSBub25jzQ==
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Version:13
HTTP/1.1 101 Switching Protocols
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOO=
Sec-WebSocket-Extensions: permessage-deflate
字段说明
头名称 | 说明 |
---|
Connection: Upgrade | 标识该HTTP请求时一个协议升级请求 | Upgrade: WebSocket | 协议升级为WebSocket协议 | Sec-WebSocket-Version: 13 | 客户端支持WebSocket的版本 | Sec-WebSocket-Key | 客户端采用base64编码的24位随机字符序列,服务器接收客户端HTTP协议升级的证明,要求服务端响应一个使用一定算法生成的对应加密的Sec-WebSocket-Accept头信息作为应答 | Sec-WebSocket-Extensions | 协议扩展类型 |
1.4、客户端(浏览器)实现
1.3.1、WebSocket对象
实现WebSocket的Web浏览器将通过WebSocket对象公开所有必须的客户端功能(主要指支持Html5的浏览器),创建WebSocket对象代码如下:
var ws = new WebSocket(url);
- 其中url格式需要为:
ws://ip:port/index
1.3.2、WebSocket事件
WebSocket对象的相关事件
事件 | 事件处理程序 | 描述 |
---|
open | WebSocket对象.onopen | 连接建立时触发 | message | WebSocket对象.onmessage | 客户端接收服务端数据时触发 | error | WebSocket对象.onerror | 通信发生错误时触发 | close | WebSocket对象.onclose | 连接关闭时触发 |
1.3.3、WebSocket方法
相关常用方法为:
1.5、服务端实现
Tomcat的7.0.5版本开始支持WebSocket,并且实现了Java WebSocket规范(JSR356)。
Java WebSocket应用由一系列的WebSocketEndpoint组成。Endpoint是一个Java对象,代表WebSocket链接的一端,即多个客户端分别对应多个Endpoint,对于服务端,我们可以视为处理具体WebSocket消息的接口,就像Servlet之与Http请求一样。我们可以通过两种方式定义Endpoint:
- 编程式:即继承类 javax.websocket.Endpoint并实现其方法
- 注解式:即定义一个Model,并添加**@ServerEndpoint**注解
Endpoint实例在WebSocket握手时创建,并在客户端与服务端链接过程中有效,最后在链接关闭时结束。在Endpoint接口中明确定义了与其生命周期相关的方法,规范实现者确保生命周期的各个阶段调用实例的相关方法。生命周期方法如下:
方法 | 含义描述 | 注解 |
---|
onClose | 当会话关闭时调用 | @OnClose | onOpen | 当开启一个新的会话时调用,该方法时客户端与服务端握手成功后调用的方法 | @OnOpen | onError | 当连接过程中异常时调用 | @OnError |
服务端需要通过属于WebSocket协议里面的Session对象,为其添加MessageHandler消息处理器来接收客户端发送的数据,当采用注解方式定义Endpoint时,我们便可以通过**@OnMessage**注解指定接收消息的方法
发送消息则由RemoteEndpoint完成,其实例由Session维护,根据使用情况,我们可以通过Session.getBasicRemote获取同步的消息发送实例,然后调用其sendXxx()方法就可以发送消息,除此之外,还可以通过Session.getAsyncRemote获取异步消息发送实例
@EqualsAndHashCode
@ServerEndpoint("/webSocket")
public class WebSocketEndpoint {
private static final Set<WebSocketEndpoint> WEBSOCKET_SET = new HashSet<>();
private Session session;
@OnMessage
public void onMessage(String messgae, Session session) throws IOException {
System.out.println("接收的消息是:" + messgae);
System.out.println(session);
for (WebSocketEndpoint demo : WEBSOCKET_SET) {
if (demo != this) {
demo.session.getBasicRemote().sendText(messgae);
}
}
}
@OnOpen
public void onOpen(Session session) {
this.session = session;
WEBSOCKET_SET.add(this);
}
@OnClose
public void onClose(Session session) {
System.out.println("连接关闭了");
}
@OnError
public void onError(Session session, Throwable error) {
System.out.println("出错了...." + error.getMessage());
}
}
2、聊天室应用练习
2.1、注入ServerEndpointExporter
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
2.2、会话连接池
public class SessionPool {
public static final Map<String, Session> SESSIONS = new ConcurrentHashMap<>();
public static void close(String sessionId) throws IOException {
Iterator<Map.Entry<String, Session>> mapIterator = SESSIONS.entrySet().iterator();
while (mapIterator.hasNext()) {
Map.Entry<String, Session> entry = mapIterator.next();
Session session = entry.getValue();
if (session.getId().equals(sessionId)) {
session.close();
mapIterator.remove();
return;
}
}
}
public static void sendMessage(String sessionId, String message) {
Session session = SESSIONS.get(sessionId);
if (session == null) {
throw new RuntimeException("Session未找到");
}
session.getAsyncRemote().sendText(message);
}
public static void sendMessage(String message) {
SESSIONS.values().forEach(session -> session.getAsyncRemote().sendText(message));
}
public static void sendMessage(HashMap<String, Object> message) {
String toUserId = message.get("toUserId").toString();
String msg = message.get("msg").toString();
String fromUserId = message.get("fromUserId").toString();
msg = "来自" + fromUserId + "的消息 :" + msg;
Session session = SESSIONS.get(toUserId);
if (session == null) {
throw new RuntimeException("用户" + toUserId + "未找到");
}
session.getAsyncRemote().sendText(msg);
}
}
2.3、处理WebSocket消息接口
@ServerEndpoint(value = "/ws/{userId}")
@Component
@Slf4j
public class WebSocketEndpoint {
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
log.info("new session : {}", userId);
SessionPool.SESSIONS.put(userId, session);
}
@OnClose
public void onClose(Session session) throws IOException {
SessionPool.close(session.getId());
}
@OnMessage
public void onMessage(String message, Session session) throws IOException {
if (message.equalsIgnoreCase("ping")) {
try {
Map<String, Object> params = new HashMap<>();
params.put("type", "pong");
session.getBasicRemote().sendText(JSON.toJSONString(params));
} catch (Exception e) {
throw new RemoteException("心跳检测出现错误");
}
} else {
HashMap<String, Object> params = JSON.parseObject(message, HashMap.class);
SessionPool.sendMessage(params);
}
}
}
2.4、前端页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
<script id="code ">
let wsObj = null;
let wsUri = null;
let userId = -1;
let lockReconnect = false;
let wsCreateHandler = null;
function createWebSocket() {
const host = window.location.host;
userId = GetQueryString("userId");
wsUri = "ws://" + host + "/ws/" + userId;
try {
wsObj = new WebSocket(wsUri);
initWsEventHandle();
} catch (e) {
writeToScreen("执行关闭事件,开始重连");
reconnect();
}
}
function initWsEventHandle() {
try {
wsObj.onopen = function (event) {
onWsOpen(event);
};
wsObj.onmessage = function (event) {
onWsMessage(event);
};
wsObj.onclose = function (event) {
writeToScreen("执行关闭事件");
onWsClose(event);
reconnect();
};
wsObj.onerror = function (event) {
writeToScreen("执行error事件");
onWsError(event);
reconnect();
};
} catch (e) {
writeToScreen("绑定事件没有成功");
reconnect();
}
}
function onWsOpen(event) {
writeToScreen("Connected");
}
function onWsClose(event) {
writeToScreen("DisConnected");
}
function onWsError(event) {
writeToScreen(event.data);
}
function writeToScreen(message) {
const debuggerInfo = $("#debuggerInfo");
if (DEBUG_FLAG) {
debuggerInfo.val(debuggerInfo.val() + "\n" + message);
}
}
function GetQueryString(name) {
let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
let r = window.location.search.substr(1).match(reg);
let context = "";
if (r != null) {
context = r[2];
}
reg = null;
r = null;
return context == null || context === "" || context === "undefined" ? "" : context;
}
function reconnect() {
if (lockReconnect) {
return;
}
writeToScreen("1秒后重连");
lockReconnect = true;
wsCreateHandler && clearTimeout(wsCreateHandler);
wsCreateHandler = setTimeout(function () {
writeToScreen("重连" + wsUri);
createWebSocket();
lockReconnect = false;
}, 1000)
}
let heartCheck = {
timeout: 15000,
timeoutObj: null,
serverTimeoutObj: null,
reset: function () {
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
this.start();
},
start: function () {
let self = this;
this.timeoutObj && clearTimeout(this.serverTimeoutObj);
this.timeoutObj = setTimeout(
function () {
writeToScreen("发送ping到后台");
try {
wsObj.send("ping");
} catch (e) {
writeToScreen("发送ping异常");
}
self.serverTimeoutObj = setTimeout(function () {
writeToScreen("没有收到后台的数据,关闭连接");
reconnect();
}, self.timeout);
}, this.timeout);
}
}
const DEBUG_FLAG = true;
debugger
$(function () {
createWebSocket();
});
function onWsMessage(event) {
const jsonStr = event.data;
writeToScreen(jsonStr);
}
function sendMessageBySocket() {
const toUserId = $("#userId").val();
const msg = $("#msg").val();
const data = {"fromUserId": userId, "toUserId": toUserId, "msg": msg};
wsObj.send(JSON.stringify(data));
}
</script>
</head>
<body>
<label for="debuggerInfo"></label><textarea id="debuggerInfo"></textarea>
<div>用户:<label for="msg"></label><label for="userId"></label><input type="text" id="userId"/></div>
<div>消息:<label for="msg"></label><input type="text" id="msg"/></div>
<div><input type="button" value="发送消息" onclick="sendMessageBySocket()"/></div>
</body>
</html>
|