WebSocket通信原理和在Tomcat中实现源码详解(万字爆肝)

WebSocket全双工通信协议,在客户端和服务端建立连接后,可以持续双向通信,和HTTP同属于应用层协议,并且都依赖于传输层的TCP/IP协议。

虽然WebSocket有别于HTTP,是一种新协议,但是RFC 6455中规定:

it is designed to work over HTTP ports 80 and 443 as well as to support HTTP proxies and intermediaries.

  • WebSocket通过HTTP端口80和443进行工作,并支持HTTP代理和中介,从而使其与HTTP协议兼容。

  • 为了实现兼容性,WebSocket握手使用HTTP Upgrade头从HTTP协议更改为WebSocket协议。

  • Websocket使用wswss的统一资源标志符(URI),分别对应明文和加密连接。

(2)握手(建立连接)

在双向通信之前,必须通过握手建立连接。Websocket通过 HTTP/1.1 协议的101状态码进行握手,首先客户端(如浏览器)发出带有特殊消息头(UpgradeConnection)的请求到服务器,服务器判断是否支持升级,支持则返回响应状态码101,表示协议升级成功,对于WebSocket就是握手成功。

客户端请求示例:

GET /test HTTP/1.1

Host: server.example.com

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Key: tFGdnEL/5fXMS9yKwBjllg==

Origin: http://example.com

Sec-WebSocket-Protocol: v10.stomp, v11.stomp, v12.stomp

Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

Sec-WebSocket-Version: 13

  • Connection必须设置Upgrade,表示客户端希望连接升级。

  • Upgrade: websocket表明协议升级为websocket

  • Sec-WebSocket-Key字段内记录着握手过程中必不可少的键值,由客户端(浏览器)生成,可以尽量避免普通HTTP请求被误认为Websocket协议。

  • Sec-WebSocket-Version 表示支持的Websocket版本。RFC6455要求使用的版本是13。

  • Origin字段是必须的。如果缺少origin字段,WebSocket服务器需要回复HTTP 403 状态码(禁止访问),通过Origin可以做安全校验。

服务端响应示例:

HTTP/1.1 101 Switching Protocols

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Accept: HaA6EjhHRejpHyuO0yBnY4J4n3A=

Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15

Sec-WebSocket-Protocol: v12.stomp

Sec-WebSocket-Accept的字段值是由握手请求中的Sec-WebSocket-Key的字段值生成的。成功握手确立WebSocket连接之后,通信时不再使用HTTP的数据帧,而采用WebSocket独立的数据帧。

WebSocket通信-来自图解HTTP

(3)消息帧

WebSocket使用二进制消息帧作为双向通信的媒介。何为消息帧?发送方将每个应用程序消息拆分为一个或多个帧,通过网络将它们传输到目的地,并重新组装解析出一个完整消息。

有别于HTTP/1.1文本消息格式(冗长的消息头和分隔符等),WebSocket消息帧规定一定的格式,以二进制传输,更加短小精悍。二者相同之处就是都是基于TCP/IP流式协议(没有规定消息边界)。

如下是消息帧的基本结构图:

Base Framing Protocol

  • FIN: 1 bit,表示该帧是否为消息的最后一帧。1-是,0-否。

  • RSV1,RSV2,RSV3: 1 bit each,预留(3位),扩展的预留标志。一般情况为0,除非协商的扩展定义为非零值。如果接收到非零值且不为协商扩展定义,接收端必须使连接失败。

  • Opcode: 4 bits,定义消息帧的操作类型,如果接收到一个未知Opcode,接收端必须使连接失败。(0x0-延续帧,0x1-文本帧,0x2-二进制帧,0x8-关闭帧,0x9-PING帧,0xA-PONG帧(在接收到PING帧时,终端必须发送一个PONG帧响应,除非它已经接收到关闭帧),0x3-0x7保留给未来的非控制帧,0xB-F保留给未来的控制帧)

  • Mask: 1 bit,表示该帧是否为隐藏的,即被加密保护的。1-是,0-否。Mask=1时,必须传一个Masking-key,用于解除隐藏(客户端发送消息给服务器端,Mask必须为1)。

  • Payload length: 7 bits, 7+16 bits, or 7+64 bits,有效载荷数据的长度(扩展数据长度+应用数据长度,扩展数据长度可以为0)。

if 0-125, that is the payload length. If 126, the following 2 bytes interpreted as a 16-bit unsigned integer are the payload length. If 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the most significant bit MUST be 0) are the payload length.

  • Masking-key: 0 or 4 bytes,用于解除帧隐藏(加密)的key,Mask=1时不为空,Mask=0时不用传。

  • Payload data: (x+y) bytes,有效载荷数据包括扩展数据(x bytes)和应用数据(y bytes)。有效载荷数据是用户真正要传输的数据。

这样的二进制消息帧设计,与HTTP协议相比,WebSocket协议可以提供约500:1的流量减少和3:1的延迟减少。

(4)挥手(关闭连接)

挥手相对于握手要简单很多,客户端和服务器端任何一方都可以通过发送关闭帧来发起挥手请求。发送关闭帧的一方,之后不再发送任何数据给对方;接收到关闭帧的一方,如果之前没有发送过关闭帧,则必须发送一个关闭帧作为响应。关闭帧中可以携带关闭原因。

在发送和接收一个关闭帧消息之后,就认为WebSocket连接已关闭,且必须关闭底层TCP连接。

除了通过关闭握手来关闭连接外,WebSocket连接也可能在另一方离开或底层TCP连接关闭时突然关闭。

4、WebSocket优点

  • 较少的控制开销。在连接建立后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对于HTTP请求每次都要携带完整的头部,显著减少。

  • 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少。

  • 保持连接状态。与HTTP不同的是,Websocket需要先建立连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。

  • 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。

  • 支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。

  • 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著提高压缩率。

鬼怪

三、Java API for WebSocket(JSR356)


JSR356Java EE7时归为Java EE标准的一部分(后来Java EE更名为Jakarta EE,世上再无Java EE,以下统一称Jakarta EE),所有兼容Jakarta EE的应用服务器,都必须遵循JSR356标准的WebSocket协议API。

WebSocket API

根据JSR356规定, 建立WebSocket连接的服务器端和客户端,两端对称,可以互相通信,差异性较小,抽象成API,就是一个个Endpoint(端点),只不过服务器端的叫ServerEndpoint,客户端的叫ClientEndpoint。客户端向服务端发送WebSocket握手请求,建立连接后就创建一个ServerEndpoint对象。(这里的EndpointTomcat连接器里的AbstractEndpoint名称上有点像,但是两个毫不相干的东西,就像周杰伦和周杰的关系。)

ServerEndpointClientEndpoint在API上差异也很小,有相同的生命周期事件(OnOpenOnCloseOnErrorOnMessage),不同之处是ServerEndpoint作为服务器端点,可以指定一个URI路径供客户端连接,ClientEndpoint没有。

1、服务端API

服务器端的Endpoint有两种实现方式,一种是注解方式@ServerEndpoint,一种是继承抽象类Endpoint

(1)注解方式@ServerEndpoint

首先看看@ServerEndpoint有哪些要素:

  • value,可以指定一个URI路径标识一个Endpoint

  • subprotocols,用户在WebSocket协议下自定义扩展一些子协议。

  • decoders,用户可以自定义一些消息解码器,比如通信的消息是一个对象,接收到消息可以自动解码封装成消息对象。

  • encoders,有解码器就有编码器,定义解码器和编码器的好处是可以规范使用层消息的传输。

  • configuratorServerEndpoint配置类,主要提供ServerEndpoint对象的创建方式扩展(如果使用TomcatWebSocket实现,默认是反射创建ServerEndpoint对象)。

ServerEndpoint

@ServerEndpoint可以注解到任何类上,但是想实现服务端的完整功能,还需要配合几个生命周期的注解使用,这些生命周期注解只能注解在方法上:

  • @OnOpen 建立连接时触发。

  • @OnClose 关闭连接时触发。

  • @OnError 发生异常时触发。

  • @OnMessage 接收到消息时触发。

(2)继承抽象类Endpoint

继承抽象类Endpoint,重写几个生命周期方法。

抽象类Endpoint

怎么没有onMessage方法,实现onMessage还需要继承实现一个接口jakarta.websocket.MessageHandlerMessageHandler接口又分为PartialWhole,实现的MessageHandler需要在onOpen触发时注册到jakarta.websocket.Session中。

MessageHandler

继承抽象类Endpoint的方式相对于注解方式要麻烦的多,除了继承Endpoint和实现接口MessageHandler外,还必须实现一个jakarta.websocket.server.ServerApplicationConfig来管理Endpoint,比如给Endpoint分配URI路径。

ServerApplicationConfig

encodersdecodersconfigurator等配置信息由jakarta.websocket.server.ServerEndpointConfig管理,默认实现jakarta.websocket.server.DefaultServerEndpointConfig

所以如果使用 Java 版WebSocket服务器端实现首推注解方式。

2、客户端API

对于客户端API,也是有注解方式和继承抽象类Endpoint方式。

  • 注解方式,只需要将@ServerEndpoint换成@ClientEndpoint

  • 继承抽象类Endpoint方式,需要一个jakarta.websocket.ClientEndpointConfig来管理encodersdecodersconfigurator等配置信息,默认实现jakarta.websocket.DefaultClientEndpointConfig

3、上下文Session

WebSocket是一个有状态的连接,建立连接后的通信都是通过jakarta.websocket.Session保持状态,一个连接一个Session,每一个Session有一个唯一标识Id。

Session的主要职责涉及:

  • 基础信息管理(request信息(getRequestURIgetRequestParameterMapgetPathParameters等)、协议版本getProtocolVersion、子协议getNegotiatedSubprotocol等)。

  • 连接管理(状态判断isOpen、接收消息的MessageHandler、发送消息的异步远程端点RemoteEndpoint.Async和同步远程端点RemoteEndpoint.Basic等)。

4、HandshakeRequest 和 HandshakeResponse

HandshakeRequestHandshakeResponse了解即可,这两个接口主要用于WebScoket握手升级过程中握手请求响应的封装,如果只是单纯使用WebSocket,不会接触到这两个接口。

(1)HandshakeRequest

HandshakeRequest

(2)HandshakeResponse

HandshakeResponse

Sec-WebSocket-Accept根据客户端传的Sec-WebSocket-Key生成,如下是Tomcat10.0.6 WebSocket源码实现中生成Sec-WebSocket-Accept的算法:

private static String getWebSocketAccept(String key) {

byte[] digest = ConcurrentMessageDigest.digestSHA1(

key.getBytes(StandardCharsets.ISO_8859_1), WS_ACCEPT);

return Base64.encodeBase64String(digest);

}

5、WebSocketContainer

jakarta.websocket.WebSocketContainer顾名思义,就是WebSocket的容器,集大成者。其主要职责包括但不限于connectToServer,客户端连接服务器端,基于浏览器的WebSocket客户端连接服务器端,由浏览器支持,但是基于Java版的WebSocket客户端就可以通过WebSocketContainer#connectToServer向服务端发起连接请求。

WebSocketContainer局部

鬼怪

四、WebSocket基于Tomcat应用


(如下使用的是javax.websocket包,未使用最新的jakarta.websocket,主要是测试项目基于SpringBoot+Tomcat9.x的,Java API for WebSocket版本需要保持一致。)

1、服务器端实现

(1)@ServerEndpoint注解方式

import javax.websocket.*;

import javax.websocket.server.PathParam;

import javax.websocket.server.ServerEndpoint;

import java.io.IOException;

import java.util.concurrent.ConcurrentHashMap;

import java.util.concurrent.atomic.AtomicLong;

@ServerEndpoint(value = “/ws/test/{userId}”, encoders = {MessageEncoder.class}, decoders = {MessageDecoder.class}, configurator = MyServerConfigurator.class)

public class WebSocketServerEndpoint {

private Session session;

private String userId;

@OnOpen

public void OnOpen(Session session, @PathParam(value = “userId”) String userId) {

this.session = session;

this.userId = userId;

// 建立连接后,将连接存到一个map里

endpointMap.put(userId, this);

Message message = new Message(0, "connected, hello " + userId);

sendMsg(message);

}

@OnClose

public void OnClose() {

// 关闭连接时触发,从map中删除连接

endpointMap.remove(userId);

System.out.println(“server closed…”);

}

@OnMessage

public void onMessage(Message message) {

System.out.println(“server recive message=” + message.toString());

}

@OnError

public void onError(Throwable t) throws Throwable {

this.session.close(new CloseReason(CloseReason.CloseCodes.CLOSED_ABNORMALLY, “系统异常”));

t.printStackTrace();

}

/**

  • 群发

  • @param data

*/

public void sendAllMsg(Message data) {

for (WebSocketServerEndpoint value : endpointMap.values()) {

value.sendMsgAsync(data);

}

}

/**

  • 推送消息给指定 userId

  • @param data

  • @param userId

*/

public void sendMsg(Message data, String userId) {

WebSocketServerEndpoint endpoint = endpointMap.get(userId);

if (endpoint == null) {

System.out.println("not conected to " + userId);

return;

}

endpoint.sendMsgAsync(data);

}

private void sendMsg(Message data) {

try {

this.session.getBasicRemote().sendObject(data);

} catch (IOException ioException) {

ioException.printStackTrace();

} catch (EncodeException e) {

e.printStackTrace();

}

}

private void sendMsgAsync(Message data) {

this.session.getAsyncRemote().sendObject(data);

}

// 存储建立连接的Endpoint

private static ConcurrentHashMap<String, WebSocketServerEndpoint> endpointMap = new ConcurrentHashMap<String, WebSocketServerEndpoint>();

}

每一个客户端与服务器端建立连接后,都会生成一个WebSocketServerEndpoint,可以通过一个Map将其与userId对应存起来,为后续群发广播和单独推送消息给某个客户端提供便利。

注意:@ServerEndpointencodersdecodersconfigurator等配置信息在实际使用中可以不定义,如果项目简单,完全可以用默认的。

如果通信消息被封装成一个对象,如示例的Message(因为源码过于简单就不展示了,属性主要有codemsgdata),就必须提供编码器和解码器。也可以在每次发送消息时硬编码转为字符串,在接收到消息时转为Message。有了编码器和解码器,显得比较规范,转为字符串由编码器做,字符串转为对象由解码器做,但也使得架构变复杂了,视项目需求而定。

MessageEncoder

MessageDecoder

Configurator的用处就是自定义Endpoint对象创建方式,默认Tomcat提供的是通过反射。WebScoket是每个连接都会创建一个Endpoint对象,如果连接比较多,很频繁,通过反射创建,用后即毁,可能不是一个好主意,所以可以搞一个对象池,用过回收,用时先从对象池中拿,有就重置,省去实例化分配内存等消耗过程。

MyServerConfigurator

如果使用SpringBoot内置TomcatundertowNetty等,接入WebSocket时除了加@ServerEndpoint还需要加一个@Component,再给Spring注册一个ServerEndpointExporter类,这样,服务端Endpoint就交由Spring去扫描注册了。

@Configuration

public class WebSocketConfig {

@Bean

public ServerEndpointExporter serverEndpointExporter() {

ServerEndpointExporter serverEndpointExporter = new ServerEndpointExporter();

return serverEndpointExporter;

}

}

外置Tomcat就不需要这么麻烦,Tomcat会默认扫描classpath下带有@ServerEndpoint注解的类。(SpringBoot接入Websocket后续会单独出文章讲解,也挺有意思的)

(2)继承抽象类Endpoint方式

import javax.websocket.*;

import java.io.IOException;

import java.util.concurrent.ConcurrentHashMap;

public class WebSocketServerEndpoint extends Endpoint {

private Session session;

private String userId;

@Override

public void onOpen(Session session, EndpointConfig endpointConfig) {

this.session = session;

this.userId = session.getPathParameters().get(“userId”);

session.addMessageHandler(new MessageHandler());

endpointMap.put(userId, this);

Message message = new Message(0, "connected, hello " + userId);

sendMsg(message);

}

@Override

public void onClose(Session session, CloseReason closeReason) {

endpointMap.remove(userId);

}

@Override

public void onError(Session session, Throwable throwable) {

throwable.printStackTrace();

}

/**

  • 群发

  • @param data

*/

public void sendAllMsg(Message data) {

for (WebSocketServerEndpoint value : endpointMap.values()) {

value.sendMsgAsync(data);

}

}

/**

  • 推送消息给指定 userId

  • @param data

  • @param userId

*/

public void sendMsg(Message data, String userId) {

WebSocketServerEndpoint endpoint = endpointMap.get(userId);

if (endpoint == null) {

System.out.println("not conected to " + userId);

return;

}

endpoint.sendMsgAsync(data);

}

private void sendMsg(Message data) {

try {

this.session.getBasicRemote().sendObject(data);

} catch (IOException ioException) {

ioException.printStackTrace();

} catch (EncodeException e) {

e.printStackTrace();

}

}

private void sendMsgAsync(Message data) {

this.session.getAsyncRemote().sendObject(data);

}

private class MessageHandler implements javax.websocket.MessageHandler.Whole {

@Override

public void onMessage(Message message) {

System.out.println(“server recive message=” + message.toString());

}

}

private static ConcurrentHashMap<String, WebSocketServerEndpoint> endpointMap = new ConcurrentHashMap<String, WebSocketServerEndpoint>();

}

继承抽象类Endpoint方式比加注解@ServerEndpoint方式麻烦的很,主要是需要自己实现MessageHandlerServerApplicationConfig@ServerEndpoint的话都是使用默认的,原理上差不多,只是注解更自动化,更简洁。

MessageHandler做的事情,一个@OnMessage就搞定了,ServerApplicationConfig做的URI映射、decodersencodersconfigurator等,一个@ServerEndpoint就可以了。

import javax.websocket.Decoder;

import javax.websocket.Encoder;

import javax.websocket.Endpoint;

import javax.websocket.server.ServerApplicationConfig;

import javax.websocket.server.ServerEndpointConfig;

import java.util.ArrayList;

import java.util.HashSet;

import java.util.List;

import java.util.Set;

public class MyServerApplicationConfig implements ServerApplicationConfig {

@Override

public Set getEndpointConfigs(Set<Class<? extends Endpoint>> set) {

Set result = new HashSet();

List<Class<? extends Decoder>> decoderList = new ArrayList<Class<? extends Decoder>>();

decoderList.add(MessageDecoder.class);

List<Class<? extends Encoder>> encoderList = new ArrayList<Class<? extends Encoder>>();

encoderList.add(MessageEncoder.class);

if (set.contains(WebSocketServerEndpoint3.class)) {

ServerEndpointConfig serverEndpointConfig = ServerEndpointConfig.Builder

.create(WebSocketServerEndpoint3.class, “/ws/test3”)

.decoders(decoderList)

.encoders(encoderList)

.configurator(new MyServerConfigurator())

.build();

result.add(serverEndpointConfig);

}

return result;

}

@Override

public Set<Class<?>> getAnnotatedEndpointClasses(Set

return set;

}

}

如果使用SpringBoot内置Tomcat,则不需要ServerApplicationConfig了,但是需要给Spring注册一个ServerEndpointConfig

@Bean

public ServerEndpointConfig serverEndpointConfig() {

List<Class<? extends Decoder>> decoderList = new ArrayList<Class<? extends Decoder>>();

decoderList.add(MessageDecoder.class);

List<Class<? extends Encoder>> encoderList = new ArrayList<Class<? extends Encoder>>();

encoderList.add(MessageEncoder.class);

ServerEndpointConfig serverEndpointConfig = ServerEndpointConfig.Builder

.create(WebSocketServerEndpoint3.class, “/ws/test3/{userId}”)

.decoders(decoderList)

.encoders(encoderList)

.configurator(new MyServerConfigurator())

.build();

return serverEndpointConfig;

}

(3)早期Tomcat7中Server端实现对比

Tomcat7早期版本7.0.47之前还没有出JSR 356时,自己搞了一套接口,其实就是一个Servlet

和遵循JSR356标准的版本对比,有一个比较大的变化是,createWebSocketInbound创建生命周期事件处理器StreamInbound的时机是WebSocket协议升级之前,此时还可以通过用户线程缓存(ThreadLocal等)的HttpServletRequest对象,获取一些请求头等信息。

而遵循JSR356标准的版本实现,创建生命周期事件处理的Endpoint是在WebSocket协议升级完成(经过HTTP握手)之后创建的,而WebSocket握手成功给客户端响应101前,会结束销毁HttpServletRequest对象,此时是获取不到请求头等信息的。

import org.apache.catalina.websocket.StreamInbound;

import org.apache.catalina.websocket.WebSocketServlet;

import javax.servlet.annotation.WebServlet;

import javax.servlet.http.HttpServletRequest;

@WebServlet(urlPatterns = “/ws/test”)

public class MyWeSocketServlet extends WebSocketServlet {

@Override

protected StreamInbound createWebSocketInbound(String subProtocol, HttpServletRequest request) {

MyMessageInbound messageInbound = new MyMessageInbound(subProtocol, request);

return messageInbound;

}

}

import org.apache.catalina.websocket.MessageInbound;

import org.apache.catalina.websocket.WsOutbound;

import javax.servlet.http.HttpServletRequest;

import java.io.IOException;

import java.nio.ByteBuffer;

import java.nio.CharBuffer;

public class MyMessageInbound extends MessageInbound {

private String subProtocol;

private HttpServletRequest request;

public MyMessageInbound(String subProtocol, HttpServletRequest request) {

this.subProtocol = subProtocol;

this.request = request;

}

@Override

protected void onOpen(WsOutbound outbound) {

String msg = “connected, hello”;

ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());

try {

outbound.writeBinaryMessage(byteBuffer);

} catch (IOException e) {

e.printStackTrace();

}

}

@Override

protected void onClose(int status) {

}

@Override

protected void onBinaryMessage(ByteBuffer byteBuffer) throws IOException {

// 接收到客户端信息

}

@Override

protected void onTextMessage(CharBuffer charBuffer) throws IOException {

// 接收到客户端信息

}

}

2、客户端实现

(1)前端js版

js版的客户端主要依托浏览器对WebScoket的支持,在生命周期事件触发上和服务器端的差不多,这也应证了建立WebSocket连接的两端是对等的。

编写WebSocket客户端需要注意以下几点:

  • 和服务器端商议好传输的消息的格式,一般为json字符串,比较直观,编码解码都很简单,也可以是其他商定的格式。

  • 需要心跳检测,定时给服务器端发送消息,保持连接正常。

  • 正常关闭连接,即关闭浏览器窗口前主动关闭连接,以免服务器端抛异常。

  • 如果因为异常断开连接,支持重连。

// 对websocket进行简单封装

WebSocketOption.prototype = {

// 创建websocket操作

createWebSocket: function () {

try {

if(‘WebSocket’ in window) {

this.ws = new WebSocket(this.wsUrl);

} else if(‘MozWebSocket’ in window) {

this.ws = new MozWebSocket(this.wsUrl);

} else {

alert(“您的浏览器不支持websocket协议,建议使用新版谷歌、火狐等浏览器,请勿使用IE10以下浏览器,360浏览器请使用极速模式,不要使用兼容模式!”);

}

this.lifeEventHandle();

} catch(e) {

this.reconnect(this.wsUrl);

console.log(e);

}

},

// 生命周期事件操作

lifeEventHandle: function() {

var self = this;

this.ws.onopen = function (event) {

self.connectCount = 1;

//心跳检测重置

if (self.heartCheck == null) {

self.heartCheck = new HeartCheckObj(self.ws);

}

self.sendMsg(5, “”)

self.heartCheck.reset().start();

console.log(“websocket连接成功!” + new Date().toUTCString());

};

this.ws.onclose = function (event) {

// 全部设置为初始值

self.heartCheck = null;

self.reconnect(self.wsUrl);

console.log(“websocket连接关闭!” + new Date().toUTCString());

};

this.ws.onerror = function () {

self.reconnect(self.wsUrl);

console.log(“websocket连接错误!”);

};

//如果获取到消息,心跳检测重置

this.ws.onmessage = function (event) {

//心跳检测重置

if (self.heartCheck == null) {

self.heartCheck = new HeartCheckObj(self.ws);

}

self.heartCheck.reset().start();

console.log(“websocket收到消息啦:” + event.data);

// 业务处理

// 接收到的消息可以放到localStorage里,然后在其他地方取出来

}

},

// 断线重连操作

reconnect: function() {

var self = this;

if (this.lockReconnect) return;

console.log(this.lockReconnect)

this.lockReconnect = true;

//没连接上会一直重连,设置延迟避免请求过多,重连时间设置按倍数增加

setTimeout(function () {

self.createWebSocket(self.wsUrl);

self.lockReconnect = false;

self.connectCount++;

}, 10000 * (self.connectCount));

},

// 发送消息操作

sendMsg: function(cmd, data) {

var sendData = {“cmd”: cmd, “msg”: data};

try {

this.ws.send(JSON.stringify(sendData));

} catch(err) {

console.log(“发送数据失败, err=” + err)

}

},

// 关闭websocket接口操作

closeWs: function() {

this.ws.close();

}

}

/**

  • 封装心跳检测对象

*/

function HeartCheckObj(ws) {

this.ws = ws;

// 心跳时间

this.timeout = 10000;

// 定时事件

this.timeoutObj = null;

// 自动断开事件

this.serverTimeoutObj = null;

}

HeartCheckObj.prototype = {

setWs: function(ws) {

this.ws = ws;

},

reset: function() {

clearTimeout(this.timeoutObj);

clearTimeout(this.serverTimeoutObj);

return this;

},

// 开始心跳检测

start: function() {

var self = this;

this.timeoutObj = setTimeout(function() {

//这里发送一个心跳,后端收到后,返回一个心跳消息,

//onmessage拿到返回的心跳就说明连接正常

var ping = {“cmd”:1, “msg”: “ping”};

self.ws.send(JSON.stringify(ping));

//如果onmessage那里超过一定时间还没重置,说明后端主动断开了

self.serverTimeoutObj = setTimeout(function() {

//如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次

self.ws.close();

}, self.timeout)

}, self.timeout)

}

}

/**


  • 创建websocket的主流程 *


*/

var currentDomain = document.domain;

var wsUrl = “ws://” + currentDomain + “/test”

var webSocketOption = new WebSocketOption(wsUrl)

webSocketOption.createWebSocket()

// 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。

window.onbeforeunload = function() {

webSocketOption.closeWs();

}

这里推荐一个在线测试WebSocket连接和发送消息的网站easyswoole.com/wstool.html

wstool

真的很牛逼,很方便,很简单。还有源码github:https://github.com/easy-swoole/wstool,感兴趣可以看看。

(2)@ClientEndpoint注解方式

Java版客户端不用多说,把@ServerEndpoint换成@ClientEndpoint就可以了,其他都一样。@ClientEndpoint@ServerEndpoint就少了一个value,不需要设置URI。

@ClientEndpoint(encoders = {MessageEncoder.class}, decoders = {MessageDecoder.class})

public class WebSocketClientEndpoint {

private Session session;

@OnOpen

public void OnOpen(Session session) {

this.session = session;

Message message = new Message(0, “connecting…”);

sendMsg(message);

}

@OnClose

public void OnClose() {

Message message = new Message(0, “client closed…”);

sendMsg(message);

System.out.println(“client closed”);

}

@OnMessage

public void onMessage(Message message) {

System.out.println(“client recive message=” + message.toString());

}

@OnError

public void onError(Throwable t) throws Throwable {

t.printStackTrace();

}

public void sendMsg(Message data) {

try {

this.session.getBasicRemote().sendObject(data);

} catch (IOException ioException) {

ioException.printStackTrace();

} catch (EncodeException e) {

e.printStackTrace();

}

}

public void sendMsgAsync(Message data) {

this.session.getAsyncRemote().sendObject(data);

}

}

连接服务器端:

WebSocketContainer container = ContainerProvider.getWebSocketContainer();

container.connectToServer(WebSocketClientEndpoint.class,

new URI(“ws://localhost:8080/ws/test”));

(3)继承抽象类Endpoint方式

继承抽象类Endpoint方式也和服务器端的差不多,但是不需要实现ServerApplicationConfig,需要实例化一个ClientEndpointConfigEndpoint实现类和服务器端的一样,就省略了,如下是连接服务器端的代码:

ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().build();

container.connectToServer(new WebSocketClientEndpoint(),clientEndpointConfig,

new URI(“ws://localhost:8080/websocket/hello”));

3、基于Nginx反向代理注意事项

一般web服务器会用Nginx做反向代理,经过Nginx反向转发的HTTP请求不会带上UpgradeConnection消息头,所以需要在Nginx配置里显式指定需要升级为WebSocket的URI带上这两个头:

location /chat/ {

proxy_pass http://backend;

proxy_http_version 1.1;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
img

总结

我在成长过程中也是一路摸爬滚打,没有任何人的指点,所以走的很艰难。例如在大三的时候,如果有个学长可以阶段性的指点一二,如果有已经工作的师兄可以告诉我工作上需要什么,我应该前面的三年可以缩短一半;后来去面试bat,失败了有5、6次,每次也不知道具体是什么原因,都是靠面试回忆去猜测可能是哪方面的问题,回来学习和完善,当你真正去招人的时候,你就会知道面试记录是多么重要,面试官可以从面试记录里看到你的成长,总是去面试,总是没有成长,就会被定义为缺乏潜力。

image
image

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

r.connectToServer(new WebSocketClientEndpoint(),clientEndpointConfig,

new URI(“ws://localhost:8080/websocket/hello”));

3、基于Nginx反向代理注意事项

一般web服务器会用Nginx做反向代理,经过Nginx反向转发的HTTP请求不会带上UpgradeConnection消息头,所以需要在Nginx配置里显式指定需要升级为WebSocket的URI带上这两个头:

location /chat/ {

proxy_pass http://backend;

proxy_http_version 1.1;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-mgxkmgwl-1712781643093)]
[外链图片转存中…(img-8LpEK9AC-1712781643094)]
[外链图片转存中…(img-xw2M1AqW-1712781643094)]
[外链图片转存中…(img-XVPFjKKf-1712781643095)]
[外链图片转存中…(img-mJjEc8LL-1712781643095)]
[外链图片转存中…(img-9Q6xax5a-1712781643095)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-xtJ8fwlT-1712781643096)]

总结

我在成长过程中也是一路摸爬滚打,没有任何人的指点,所以走的很艰难。例如在大三的时候,如果有个学长可以阶段性的指点一二,如果有已经工作的师兄可以告诉我工作上需要什么,我应该前面的三年可以缩短一半;后来去面试bat,失败了有5、6次,每次也不知道具体是什么原因,都是靠面试回忆去猜测可能是哪方面的问题,回来学习和完善,当你真正去招人的时候,你就会知道面试记录是多么重要,面试官可以从面试记录里看到你的成长,总是去面试,总是没有成长,就会被定义为缺乏潜力。

[外链图片转存中…(img-5q84w8C5-1712781643096)]
[外链图片转存中…(img-1x8GR6nz-1712781643096)]

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-gMLgVNgW-1712781643097)]

  • 19
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值