Tio - IM
https://www.tiocloud.com/doc/tio/85?pageNumber=1
<dependency> <groupId>org.t-io</groupId> <artifactId>tio-core</artifactId> <version>3.7.0.v20201010-RELEASE</version></dependency><dependency> <groupId>org.t-io</groupId> <artifactId>tio-websocket-server</artifactId> <version>3.7.0.v20201010-RELEASE</version></dependency>
##http 配置http.port = 80http.page = classpath:pagehttp.404 = /showcase/404http.500 = /showcase/500# 页面文件缓存时间,开发时设置成0,生产环境可以设置成1小时(3600),10分钟(600)等,单位:秒http.maxLiveTimeOfStaticRes=0# file of keystore,如果以classpath:开头,则从classpath中查找,否则从文件路径中查找# --例1: classpath:config/ssl/keystore.jks# --例2: /ssl/ca/keystore.jks#ssl.keystore=classpath:ssl/cert-1538199102261_t-io.org.jks# file of truststore,如果以classpath:开头,则从classpath中查找,否则从文件路径中查找# --例1: classpath:config/ssl/keystore.jks# --例2: /ssl/ca/keystore.jks#ssl.truststore=classpath:ssl/cert-1538199102261_t-io.org.jks# password for keystore#ssl.pwd=08gUMx4x
1、C/S
1.1 Entity
public class Const { /** * 服务器地址 */ public static final String SERVER = "127.0.0.1"; /**HelloPacket * 监听端口 */ public static final int PORT = 6789; /** * 心跳超时时间 */ public static final int TIMEOUT = 5000; /** * 用于群聊的group id */ public static final String GROUP_ID = "showcase-websocket";}
public class HelloPacket extends Packet { private static final long serialVersionUID = -172060606924066412L; /** * 消息头的长度 */ public static final int HEADER_LENGHT = 4; public static final String CHARSET = "utf-8"; private byte[] body; /** * @return the body */ public byte[] getBody() { return body; } /** * @param body the body to set */ public void setBody(byte[] body) { this.body = body; }}
1.2 Client
public class HelloClientAioHandler implements ClientAioHandler { private static final HelloPacket heartbeatPacket = new HelloPacket(); /** * 解码:把接收到的ByteBuffer,解码成应用可以识别的业务消息包 * 总的消息结构:消息头 + 消息体 * 消息头结构: 4个字节,存储消息体的长度 * 消息体结构: 对象的json串的byte[] */ @Override public HelloPacket decode(ByteBuffer buffer, int limit, int position, int readableLength, ChannelContext channelContext) throws AioDecodeException { // 收到的数据组不了业务包,则返回null以告诉框架数据不够 if (readableLength < HelloPacket.HEADER_LENGHT) { return null; } // 读取消息体的长度 int bodyLength = buffer.getInt(); // 数据不正确,则抛出AioDecodeException异常 if (bodyLength < 0) { throw new AioDecodeException("bodyLength [" + bodyLength + "] is not right, remote:" + channelContext.getClientNode()); } // 计算本次需要的数据长度 int neededLength = HelloPacket.HEADER_LENGHT + bodyLength; // 收到的数据是否足够组包 int isDataEnough = readableLength - neededLength; // 不够消息体长度(剩下的buffe组不了消息体) if (isDataEnough < 0) { return null; } else { // 组包成功 HelloPacket imPacket = new HelloPacket(); if (bodyLength > 0) { byte[] dst = new byte[bodyLength]; buffer.get(dst); imPacket.setBody(dst); } return imPacket; } } /** * 编码:把业务消息包编码为可以发送的ByteBuffer * 总的消息结构:消息头 + 消息体 * 消息头结构: 4个字节,存储消息体的长度 * 消息体结构: 对象的json串的byte[] */ @Override public ByteBuffer encode(Packet packet, TioConfig tioConfig, ChannelContext channelContext) { HelloPacket helloPacket = (HelloPacket) packet; byte[] body = helloPacket.getBody(); int bodyLen = 0; if (body != null) { bodyLen = body.length; } // bytebuffer的总长度是 = 消息头的长度 + 消息体的长度 int allLen = HelloPacket.HEADER_LENGHT + bodyLen; //创建一个新的bytebuffer ByteBuffer buffer = ByteBuffer.allocate(allLen); // 设置字节序 buffer.order(tioConfig.getByteOrder()); // 写入消息头----消息头的内容就是消息体的长度 buffer.putInt(bodyLen); // 写入消息体 if (body != null) { buffer.put(body); } return buffer; } /** * 处理消息 */ @Override public void handler(Packet packet, ChannelContext channelContext) throws Exception { HelloPacket helloPacket = (HelloPacket) packet; byte[] body = helloPacket.getBody(); if (body != null) { String str = new String(body, HelloPacket.CHARSET); System.out.println("收到消息:" + str); } } /** * 此方法如果返回null,框架层面则不会发心跳;如果返回非null,框架层面会定时发本方法返回的消息包 */ @Override public Packet heartbeatPacket(ChannelContext channelContext) { return heartbeatPacket; }}
public class HelloClientStarter { /** * 服务器节点 */ public static Node serverNode = new Node(Const.SERVER, Const.PORT); /** * handler, 包括编码、解码、消息处理 */ public static ClientAioHandler tioClientHandler = new HelloClientAioHandler(); /** * 事件监听器,可以为null,但建议自己实现该接口,可以参考showcase了解些接口 */ public static ClientAioListener aioListener = null; /** * 断链后自动连接的,不想自动连接请设为null */ private static ReconnConf reconnConf = new ReconnConf(5000L); /** * 一组连接共用的上下文对象 */ public static ClientTioConfig clientTioConfig = new ClientTioConfig(tioClientHandler, aioListener, reconnConf); public static TioClient tioClient = null; public static ClientChannelContext clientChannelContext = null; /** * 启动程序入口 */ public static void main(String[] args) throws Exception { clientTioConfig.setHeartbeatTimeout(Const.TIMEOUT); tioClient = new TioClient(clientTioConfig); clientChannelContext = tioClient.connect(serverNode); //连上后,发条消息玩玩 send(); } private static void send() throws Exception { HelloPacket packet = new HelloPacket(); packet.setBody("hello world".getBytes(HelloPacket.CHARSET)); Tio.send(clientChannelContext, packet); } }
1.3 Serve
public class HelloServerAioHandler implements ServerAioHandler { /** * 解码:把接收到的ByteBuffer,解码成应用可以识别的业务消息包 * 总的消息结构:消息头 + 消息体 * 消息头结构: 4个字节,存储消息体的长度 * 消息体结构: 对象的json串的byte[] */ @Override public HelloPacket decode(ByteBuffer buffer, int limit, int position, int readableLength, ChannelContext channelContext) throws AioDecodeException { //提醒:buffer的开始位置并不一定是0,应用需要从buffer.position()开始读取数据 //收到的数据组不了业务包,则返回null以告诉框架数据不够 if (readableLength < HelloPacket.HEADER_LENGHT) { return null; } //读取消息体的长度 int bodyLength = buffer.getInt(); //数据不正确,则抛出AioDecodeException异常 if (bodyLength < 0) { throw new AioDecodeException("bodyLength [" + bodyLength + "] is not right, remote:" + channelContext.getClientNode()); } //计算本次需要的数据长度 int neededLength = HelloPacket.HEADER_LENGHT + bodyLength; //收到的数据是否足够组包 int isDataEnough = readableLength - neededLength; // 不够消息体长度(剩下的buffe组不了消息体) if (isDataEnough < 0) { return null; } else { //组包成功 HelloPacket imPacket = new HelloPacket(); if (bodyLength > 0) { byte[] dst = new byte[bodyLength]; buffer.get(dst); imPacket.setBody(dst); } return imPacket; } } /** * 编码:把业务消息包编码为可以发送的ByteBuffer * 总的消息结构:消息头 + 消息体 * 消息头结构: 4个字节,存储消息体的长度 * 消息体结构: 对象的json串的byte[] */ @Override public ByteBuffer encode(Packet packet, TioConfig tioConfig, ChannelContext channelContext) { HelloPacket helloPacket = (HelloPacket) packet; byte[] body = helloPacket.getBody(); int bodyLen = 0; if (body != null) { bodyLen = body.length; } //bytebuffer的总长度是 = 消息头的长度 + 消息体的长度 int allLen = HelloPacket.HEADER_LENGHT + bodyLen; //创建一个新的bytebuffer ByteBuffer buffer = ByteBuffer.allocate(allLen); //设置字节序 buffer.order(tioConfig.getByteOrder()); //写入消息头----消息头的内容就是消息体的长度 buffer.putInt(bodyLen); //写入消息体 if (body != null) { buffer.put(body); } return buffer; } /** * 处理消息 */ @Override public void handler(Packet packet, ChannelContext channelContext) throws Exception { HelloPacket helloPacket = (HelloPacket) packet; byte[] body = helloPacket.getBody(); if (body != null) { String str = new String(body, HelloPacket.CHARSET); System.out.println("收到消息:" + str); HelloPacket resppacket = new HelloPacket(); resppacket.setBody(("收到了你的消息,你的消息是:" + str).getBytes(HelloPacket.CHARSET)); Tio.send(channelContext, resppacket); } }}
public class HelloServerStarter { /** * handler, 包括编码、解码、消息处理 */ public static ServerAioHandler aioHandler = new HelloServerAioHandler(); /** * 事件监听器,可以为null,但建议自己实现该接口,可以参考showcase了解些接口 */ public static ServerAioListener aioListener = null; /** * 一组连接共用的上下文对象 */ public static ServerTioConfig serverTioConfig = new ServerTioConfig("hello-tio-server", aioHandler, aioListener); /** * tioServer对象 */ public static TioServer tioServer = new TioServer(serverTioConfig); /** * 有时候需要绑定ip,不需要则null */ public static String serverIp = null; /** * 监听的端口 */ public static int serverPort = Const.PORT; /** * 启动程序入口 */ public static void main(String[] args) throws IOException { serverTioConfig.setHeartbeatTimeout(Const.TIMEOUT); tioServer.start(serverIp, serverPort); }}
2、 B/S
2.1 Serve
public class HttpServerInit { private static final Logger log = LoggerFactory.getLogger(HttpServerInit.class); public static HttpConfig httpConfig; public static HttpRequestHandler requestHandler; public static HttpServerStarter httpServerStarter; public static ServerTioConfig serverTioConfig; public static void init() throws Exception { //启动端口 int port = P.getInt("http.port"); // html/css/js等的根目录,支持classpath:,也支持绝对路径 String pageRoot = P.get("http.page"); httpConfig = new HttpConfig(port, null, null, null); httpConfig.setPageRoot(pageRoot); httpConfig.setMaxLiveTimeOfStaticRes(P.getInt("http.maxLiveTimeOfStaticRes")); httpConfig.setPage404(P.get("http.404")); httpConfig.setPage500(P.get("http.500")); httpConfig.setUseSession(false); httpConfig.setCheckHost(false); //第二个参数也可以是数组 requestHandler = new DefaultHttpRequestHandler(httpConfig, ShowcaseWebsocketStarter.class); httpServerStarter = new HttpServerStarter(httpConfig, requestHandler); serverTioConfig = httpServerStarter.getServerTioConfig(); httpServerStarter.start(); //启动http服务器 String protocol = SslUtils.isSsl(serverTioConfig) ? "https" : "http"; if (log.isInfoEnabled()) { log.info("\r\nTio Http Server启动完毕:\r\n访问地址:{}://127.0.0.1:{}", protocol, port); } else { System.out.println("\r\nTio Http Server启动完毕:,\r\n访问地址:" + protocol + "://127.0.0.1:" + port); } }}
public abstract class ShowcaseServerConfig { /** * 协议名字(可以随便取,主要用于开发人员辨识) */ public static final String PROTOCOL_NAME = "showcase"; public static final String CHARSET = "utf-8"; /** * 监听的ip */ public static final String SERVER_IP = null;//null表示监听所有,并不指定ip /** * 监听端口 */ public static final int SERVER_PORT = 9326; /** * 心跳超时时间,单位:毫秒 */ public static final int HEARTBEAT_TIMEOUT = 1000 * 60; /** * ip数据监控统计,时间段 * @author tanyaowu * */ public interface IpStatDuration { Long DURATION_1 = Time.MINUTE_1 * 5; Long[] IPSTAT_DURATIONS = new Long[] { DURATION_1 }; }}
public class ShowcaseWsMsgHandler implements IWsMsgHandler { private static Logger log = LoggerFactory.getLogger(ShowcaseWsMsgHandler.class); public static final ShowcaseWsMsgHandler me = new ShowcaseWsMsgHandler(); private ShowcaseWsMsgHandler() {} /** * 握手时走这个方法,业务可以在这里获取cookie,request参数等 */ @Override public HttpResponse handshake(HttpRequest request, HttpResponse httpResponse, ChannelContext channelContext) throws Exception { String clientip = request.getClientIp(); String myname = request.getParam("name"); Tio.bindUser(channelContext, myname); log.info("收到来自{}的ws握手包\r\n{}", clientip, request.toString()); return httpResponse; } /** * @param httpRequest * @param httpResponse * @param channelContext * @throws Exception * @author tanyaowu */ @Override public void onAfterHandshaked(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception { //绑定到群组,后面会有群发 Tio.bindGroup(channelContext, Const.GROUP_ID); int count = Tio.getAll(channelContext.tioConfig).getObj().size(); String msg = "{name:'admin',message:'" + channelContext.userid + " 进来了,共【" + count + "】人在线" + "'}"; //用tio-websocket,服务器发送到客户端的Packet都是WsResponse WsResponse wsResponse = WsResponse.fromText(msg, ShowcaseServerConfig.CHARSET); //群发 Tio.sendToGroup(channelContext.tioConfig, Const.GROUP_ID, wsResponse); } /** * 字节消息(binaryType = arraybuffer)过来后会走这个方法 */ @Override public Object onBytes(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception { return null; } /** * 当客户端发close flag时,会走这个方法 */ @Override public Object onClose(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception { Tio.remove(channelContext, "receive close flag"); return null; } /** * 字符消息(binaryType = blob)过来后会走这个方法 */ @Override public Object onText(WsRequest wsRequest, String text, ChannelContext channelContext) throws Exception { WsSessionContext wsSessionContext = (WsSessionContext) channelContext.get(); // 获取 websocket 握手包 HttpRequest httpRequest = wsSessionContext.getHandshakeRequest(); if (log.isDebugEnabled()) { log.debug("握手包:{}", httpRequest); } if (Objects.equals("心跳内容", text)) { return null; } //channelContext.getToken() //String msg = channelContext.getClientNode().toString() + " 说:" + text; String msg = "{name:'" + channelContext.userid + "',message:'" + text + "'}"; //用tio-websocket,服务器发送到客户端的Packet都是WsResponse WsResponse wsResponse = WsResponse.fromText(msg, ShowcaseServerConfig.CHARSET); //群发 Tio.sendToGroup(channelContext.tioConfig, Const.GROUP_ID, wsResponse); //返回值是要发送给客户端的内容,一般都是返回null return null; }}
public class ShowcaseWebsocketStarter { private WsServerStarter wsServerStarter; private ServerTioConfig serverTioConfig; /** * * @author tanyaowu */ public ShowcaseWebsocketStarter(int port, ShowcaseWsMsgHandler wsMsgHandler) throws Exception { wsServerStarter = new WsServerStarter(port, wsMsgHandler); serverTioConfig = wsServerStarter.getServerTioConfig(); serverTioConfig.setName(ShowcaseServerConfig.PROTOCOL_NAME); //设置ip统计时间段 serverTioConfig.ipStats.addDurations(ShowcaseServerConfig.IpStatDuration.IPSTAT_DURATIONS); //设置心跳超时时间 serverTioConfig.setHeartbeatTimeout(ShowcaseServerConfig.HEARTBEAT_TIMEOUT);// if (P.getInt("ws.use.ssl", 1) == 1) {// // 通过wss来访问,要有SSL证书(证书必须和域名相匹配,否则可能访问不了ssl)//// String keyStoreFile = "classpath:config/ssl/keystore.jks";//// String trustStoreFile = "classpath:config/ssl/keystore.jks";//// String keyStorePwd = "214323428310224";//// String keyStoreFile = P.get("ssl.keystore", null);// String trustStoreFile = P.get("ssl.truststore", null);// String keyStorePwd = P.get("ssl.pwd", null);// serverTioConfig.useSsl(keyStoreFile, trustStoreFile, keyStorePwd);// } } /** * @author tanyaowu * @throws IOException */ public static void start() throws Exception { ShowcaseWebsocketStarter appStarter = new ShowcaseWebsocketStarter(ShowcaseServerConfig.SERVER_PORT, ShowcaseWsMsgHandler.me); appStarter.wsServerStarter.start(); } public static void main(String[] args) throws Exception { //启动http server,这个步骤不是必须的,但是为了用页面演示websocket,所以先启动http P.use("app.properties"); HttpServerInit.init(); //启动websocket server start(); }}
2.2 Browser
<!DOCTYPE html><html lang="zh"><head> <meta charset="utf-8"> <title>tio-ws演示</title></head><body> <div id="app"> <iframe src="./im.html" style="width:500px; height:850px;"></iframe> <iframe src="./im.html" style="width:500px; height:850px;"></iframe> <iframe src="./im.html" style="width:500px; height:850px;"></iframe> </div></body></html>
<!DOCTYPE html><html lang="zh"><head><meta charset="UTF-8"><title>IM Now!</title><link rel="stylesheet" type="text/css" href="css/im.css" /></head><body><div id="container"> <div class="header"> <span style="float: left;">微信聊天界面</span> <span id="mtime" style="float: right;"></span> </div> <ul class="content"></ul> <div class="footer"> <div id="user_face_icon"> <img src="image/t2.jpg" alt=""> </div> <input id="messageText" type="text" placeholder="说点什么吧..." > <span id="btn" onclick="sendMsg()">发送</span> </div> </div><script src="js/tiows.js"></script><script src="js/name.js"></script><script src="js/imHandler.js"></script><script src="js/im.js"></script></body></html>
*{margin:0;padding:0;list-style:none;}#container{width:450px;height:780px;background:#eee;margin:5pxauto0;position:relative;box-shadow:20px20px55px#777;}.header{background:#000;height:40px;color:#fff;line-height:34px;font-size:20px;padding:010px;}.footer{width:430px;height:50px;background:#666;position:absolute;bottom:0;padding:10px;}.footerinput{width:275px;height:45px;outline:none;font-size:20px;text-indent:10px;position:absolute;border-radius:6px;right:80px;}.footerspan{display:inline-block;width:62px;height:48px;background:#ccc;font-weight:900;line-height:45px;cursor:pointer;text-align:center;position:absolute;right:10px;border-radius:6px;}.footerspan:hover{color:#fff;background:#999;}#user_face_icon{display:inline-block;background:red;width:60px;height:60px;border-radius:30px;position:absolute;bottom:6px;left:14px;cursor:pointer;overflow:hidden;}img{width:60px;height:60px;}.content{font-size:20px;width:435px;height:662px;overflow:auto;padding:5px;}.contentli{margin-top:10px;padding-left:10px;width:412px;display:block;clear:both;overflow:hidden;}.contentliimg{float:left;}.contentlispan{background:#7cfc00;padding:10px;border-radius:10px;float:left;margin:6px10px010px;max-width:310px;border:1pxsolid#ccc;box-shadow:003px#ccc;}.contentliimg.imgleft{float:left;}.contentliimg.imgright{float:right;}.contentlispan.nickleft{float:left;background:#eee;padding:0px;border-radius:0px;margin:6px10px010px;max-width:310px;border:0pxsolid#ccc;box-shadow:000px#ccc;width:90%;text-align:left;font-size:16px;color:#aaa;}.contentlispan.nickright{float:right;background:#eee;padding:0px;border-radius:0px;margin:6px10px010px;max-width:310px;border:0pxsolid#ccc;box-shadow:000px#ccc;width:90%;text-align:right;font-size:16px;color:#aaa;}.contentlidiv.sysinfo{font-size:16px;border-radius:5px;background:#ddd;color:#fff;text-align:center;width:auto;}.contentlispan.spanleft{float:left;background:#fff;}.contentlispan.spanright{float:right;background:#7cfc00;}
const ws_protocol = 'ws'; // ws 或 wssconst ip = '127.0.0.1'const port = 9326const heartbeatTimeout = 5000; // 心跳超时时间,单位:毫秒const reconnInterval = 1000; // 重连间隔时间,单位:毫秒const binaryType = 'blob'; // 'blob' or 'arraybuffer';//arraybuffer是字节const handler = new IMHandler()let mtime = "";let myname = "";let tiows;function initWs () { myname=getRandomName(); const queryString = 'name=' + myname; const param = ""; tiows = new tio.ws(ws_protocol, ip, port, queryString, param, handler, heartbeatTimeout, reconnInterval, binaryType) tiows.connect()}//回车事件绑定document.onkeydown = function(e){ const ev = document.all ? window.event : e; if(ev.keyCode===13) { sendMsg(); }}function showTime(){ const myDate = new Date(); let m = myDate.getMinutes(); if (m<10) m="0"+m; let s = myDate.getSeconds(); if (s<10) s="0"+s; mtime=myDate.getHours()+":"+m+":"+s; document.getElementById("mtime").innerText=mtime;}function sendMsg () { const msg = document.getElementById('messageText'); tiows.send(msg.value); msg.value="";}function clearMsg () { document.getElementById('contentId').innerHTML = ''}initWs ()setInterval(showTime,1000);
const IMHandler = function () { this.onopen = function (event, ws) { // ws.send('hello 连上了') //document.getElementById('contentId').innerHTML += 'hello 连上了<br>'; } /** * 收到服务器发来的消息 * @param {*} event * @param {*} ws */ this.onmessage = function (event, ws) { const data = event.data; const msgBody = eval('(' + data + ')'); let ico, imgcls, spancls; const content = document.getElementsByTagName('ul')[0]; let nickcls; if (myname === msgBody.name) { // 头像 ico = "image/t2.jpg"; imgcls = "imgright"; spancls = "spanright"; nickcls = "nickright"; } else { // 头像 ico = "image/t1.jpg"; imgcls = "imgleft"; spancls = "spanleft"; nickcls = "nickleft" } if (msgBody.name === "admin") { content.innerHTML += '<li><div class="sysinfo">' + msgBody.message + '</div></li>'; } else { content.innerHTML += '<li><img src="' + ico + '" class="' + imgcls + '"><span class="' + nickcls + '">' + msgBody.name + '</span><span class="' + spancls + '">' + msgBody.message + '</span></li>'; } console.log("-----------") content.scrollTop = content.scrollHeight; } this.onclose = function (e, ws) { // error(e, ws) } this.onerror = function (e, ws) { // error(e, ws) } /** * 发送心跳,框架会自动定时调用该方法,请在该方法中发送心跳 * @param {*} ws */ this.ping = function (ws) { // log("发心跳了") ws.send('心跳内容') }};
function getRandomName(){ const firstNames = ["赵", "钱", "孙", "李"]; const secondNames = ["三", "四", "五", "六"]; const firstLength = firstNames.length; const secondLength = secondNames.length; const i = parseInt(Math.random() * firstLength); const j = parseInt(Math.random() * secondLength); return firstNames[i] + secondNames[j];}
if (typeof(tio) == "undefined") { tio = {};}tio.ws = {};/** * @param {*} ws_protocol wss or ws * @param {*} ip * @param {*} port * @param {*} paramStr 加在ws url后面的请求参数,形如:name=张三&id=12 * @param {*} param 作为tio.ws对象的参数,由业务自己使用,框架不使用 * @param {*} handler * @param {*} heartbeatTimeout 心跳时间 单位:毫秒 * @param {*} reconnInterval 重连间隔时间 单位:毫秒 * @param {*} binaryType 'blob' or 'arraybuffer';//arraybuffer是字节 */tio.ws = function (ws_protocol, ip, port, paramStr, param, handler, heartbeatTimeout, reconnInterval, binaryType) { this.url = ws_protocol + '://' + ip + ':' + port this.binaryType = binaryType || 'arraybuffer' if (paramStr) { this.url += '?' + paramStr this.reconnUrl = this.url + "&" } else { this.reconnUrl = this.url + "?" } this.reconnUrl += "tiows_reconnect=true"; this.handler = handler this.heartbeatTimeout = heartbeatTimeout this.reconnInterval = reconnInterval this.lastInteractionTime = function () { if (arguments.length === 1) { this.lastInteractionTimeValue = arguments[0] } return this.lastInteractionTimeValue } this.heartbeatSendInterval = heartbeatTimeout / 2 this.connect = function (isReconnect) { let _url = this.url; if (isReconnect) { _url = this.reconnUrl; } const ws = new WebSocket(_url); this.ws = ws ws.binaryType = this.binaryType; // 'arraybuffer'; // 'blob' or 'arraybuffer';//arraybuffer是字节 const self = this; ws.onopen = function (event) { self.handler.onopen.call(self.handler, event, ws) self.lastInteractionTime(new Date().getTime()) self.pingIntervalId = setInterval(function () { self.ping(self) }, self.heartbeatSendInterval) } ws.onmessage = function (event) { self.handler.onmessage.call(self.handler, event, ws) self.lastInteractionTime(new Date().getTime()) } ws.onclose = function (event) { clearInterval(self.pingIntervalId) // clear send heartbeat task try { self.handler.onclose.call(self.handler, event, ws) } catch (error) {} self.reconn(event) } ws.onerror = function (event) { self.handler.onerror.call(self.handler, event, ws) } return ws } this.reconn = function (event) { const self = this; setTimeout(function () { self.ws = self.connect(true) }, self.reconnInterval) } this.ping = function () { const iv = new Date().getTime() - this.lastInteractionTime(); // 已经多久没发消息了 // 单位:秒 if ((this.heartbeatSendInterval + iv) >= this.heartbeatTimeout) { this.handler.ping(this.ws) } }; this.send = function(data){ this.ws.send(data); };}