Java最新WebSocket通信原理和在Tomcat中实现源码详解(万字爆肝),搞懂开源框架设计思想真的这么重要吗

最后

分享一套我整理的面试干货,这份文档结合了我多年的面试官经验,站在面试官的角度来告诉你,面试官提的那些问题他最想听到你给他的回答是什么,分享出来帮助那些对前途感到迷茫的朋友。

面试经验技巧篇
  • 经验技巧1 如何巧妙地回答面试官的问题
  • 经验技巧2 如何回答技术性的问题
  • 经验技巧3 如何回答非技术性问题
  • 经验技巧4 如何回答快速估算类问题
  • 经验技巧5 如何回答算法设计问题
  • 经验技巧6 如何回答系统设计题
  • 经验技巧7 如何解决求职中的时间冲突问题
  • 经验技巧8 如果面试问题曾经遇见过,是否要告知面试官
  • 经验技巧9 在被企业拒绝后是否可以再申请
  • 经验技巧10 如何应对自己不会回答的问题
  • 经验技巧11 如何应对面试官的“激将法”语言
  • 经验技巧12 如何处理与面试官持不同观点这个问题
  • 经验技巧13 什么是职场暗语

面试真题篇
  • 真题详解1 某知名互联网下载服务提供商软件工程师笔试题
  • 真题详解2 某知名社交平台软件工程师笔试题
  • 真题详解3 某知名安全软件服务提供商软件工程师笔试题
  • 真题详解4 某知名互联网金融企业软件工程师笔试题
  • 真题详解5 某知名搜索引擎提供商软件工程师笔试题
  • 真题详解6 某初创公司软件工程师笔试题
  • 真题详解7 某知名游戏软件开发公司软件工程师笔试题
  • 真题详解8 某知名电子商务公司软件工程师笔试题
  • 真题详解9 某顶级生活消费类网站软件工程师笔试题
  • 真题详解10 某知名门户网站软件工程师笔试题
  • 真题详解11 某知名互联网金融企业软件工程师笔试题
  • 真题详解12 国内某知名网络设备提供商软件工程师笔试题
  • 真题详解13 国内某顶级手机制造商软件工程师笔试题
  • 真题详解14 某顶级大数据综合服务提供商软件工程师笔试题
  • 真题详解15 某著名社交类上市公司软件工程师笔试题
  • 真题详解16 某知名互联网公司软件工程师笔试题
  • 真题详解17 某知名网络安全公司校园招聘技术类笔试题
  • 真题详解18 某知名互联网游戏公司校园招聘运维开发岗笔试题

资料整理不易,点个关注再走吧

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

}

@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;

proxy_set_header Upgrade $http_upgrade;

proxy_set_header Connection “upgrade”;

proxy_connect_timeout 4s;

proxy_read_timeout 7200s;

proxy_send_timeout 12s;

}

默认情况下,如果代理服务器在60秒内没有传输任何数据,连接将被关闭。这个超时可以通过proxy_read_timeout指令来增加。或者,可以将代理服务器配置为定期发送WebSocket PING帧以重置超时并检查连接是否仍然活跃。

具体可参考:http://nginx.org/en/docs/http/websocket.html

鬼怪

五、WebSocket在Tomcat中的源码实现


所有兼容Java EE的应用服务器,必须遵循JSR356 WebSocket Java API标准,Tomcat也不例外。而且Tomcat也是支持WebSocket最早的Web应用服务器框架(之一),在还没有出JSR356标准时,就已经自定义了一套WebSocket API,但是JSR356一出,不得不改弦更张。

通过前面的讲解,在使用上完全没有问题,但是有几个问题完全是黑盒的:

  • Server Endpoint 是如何被扫描加载的?

  • WebSocket是如何借助HTTP 进行握手升级的?

  • WebSocket建立连接后如何保持连接不断,互相通信的?

(如下源码解析,需要对Tomcat连接器源码有一定了解)

1、WsSci初始化

Tomcat 提供了一个org.apache.tomcat.websocket.server.WsSci类来初始化、加载WebSocket。从类名上顾名思义,利用了Sci加载机制,何为Sci加载机制?就是实现接口 jakarta.servlet.ServletContainerInitializer,在Tomcat部署装载Web项目(org.apache.catalina.core.StandardContext#startInternal)时主动触发ServletContainerInitializer#onStartup,做一些扩展的初始化操作。

WsSci主要做了一件事,就是扫描加载Server Endpoint,并将其加到WebSocket容器里jakarta.websocket.WebSocketContainer

WsSci主要会扫描三种类:

  • 加了@ServerEndpoint的类。

  • Endpoint的子类。

  • ServerApplicationConfig的子类。

(1)WsSci#onStartup

@HandlesTypes({ServerEndpoint.class, ServerApplicationConfig.class,

Endpoint.class})

public class WsSci implements ServletContainerInitializer {

@Override

public void onStartup(Set<Class<?>> clazzes, ServletContext ctx)

throws ServletException {

WsServerContainer sc = init(ctx, true);

if (clazzes == null || clazzes.size() == 0) {

return;

}

// Group the discovered classes by type

Set serverApplicationConfigs = new HashSet<>();

Set<Class<? extends Endpoint>> scannedEndpointClazzes = new HashSet<>();

Set<Class<?>> scannedPojoEndpoints = new HashSet<>();

try {

// wsPackage is “jakarta.websocket.”

String wsPackage = ContainerProvider.class.getName();

wsPackage = wsPackage.substring(0, wsPackage.lastIndexOf(‘.’) + 1);

for (Class<?> clazz : clazzes) {

JreCompat jreCompat = JreCompat.getInstance();

int modifiers = clazz.getModifiers();

if (!Modifier.isPublic(modifiers) ||

Modifier.isAbstract(modifiers) ||

Modifier.isInterface(modifiers) ||

!jreCompat.isExported(clazz)) {

// Non-public, abstract, interface or not in an exported

// package (Java 9+) - skip it.

continue;

}

// Protect against scanning the WebSocket API JARs

// 防止扫描WebSocket API jar

if (clazz.getName().startsWith(wsPackage)) {

continue;

}

if (ServerApplicationConfig.class.isAssignableFrom(clazz)) {

// 1、clazz是ServerApplicationConfig子类

serverApplicationConfigs.add(

(ServerApplicationConfig) clazz.getConstructor().newInstance());

}

if (Endpoint.class.isAssignableFrom(clazz)) {

// 2、clazz是Endpoint子类

@SuppressWarnings(“unchecked”)

Class<? extends Endpoint> endpoint =

(Class<? extends Endpoint>) clazz;

scannedEndpointClazzes.add(endpoint);

}

if (clazz.isAnnotationPresent(ServerEndpoint.class)) {

// 3、clazz是加了注解ServerEndpoint的类

scannedPojoEndpoints.add(clazz);

}

}

} catch (ReflectiveOperationException e) {

throw new ServletException(e);

}

// Filter the results

Set filteredEndpointConfigs = new HashSet<>();

Set<Class<?>> filteredPojoEndpoints = new HashSet<>();

if (serverApplicationConfigs.isEmpty()) {

// 从这里看出@ServerEndpoint的服务器端是可以不用ServerApplicationConfig的

filteredPojoEndpoints.addAll(scannedPojoEndpoints);

} else {

// serverApplicationConfigs不为空,

for (ServerApplicationConfig config : serverApplicationConfigs) {

Set configFilteredEndpoints =

config.getEndpointConfigs(scannedEndpointClazzes);

if (configFilteredEndpoints != null) {

filteredEndpointConfigs.addAll(configFilteredEndpoints);

}

// getAnnotatedEndpointClasses 对于 scannedPojoEndpoints起到一个过滤作用

// 不满足条件的后面不加到WsServerContainer里

Set<Class<?>> configFilteredPojos =

config.getAnnotatedEndpointClasses(

scannedPojoEndpoints);

if (configFilteredPojos != null) {

filteredPojoEndpoints.addAll(configFilteredPojos);

}

}

}

try {

// 继承抽象类Endpoint的需要使用者手动封装成ServerEndpointConfig

// 而加了注解@ServerEndpoint的类 Tomcat会自动封装成ServerEndpointConfig

// Deploy endpoints

for (ServerEndpointConfig config : filteredEndpointConfigs) {

sc.addEndpoint(config);

}

// Deploy POJOs

for (Class<?> clazz : filteredPojoEndpoints) {

sc.addEndpoint(clazz, true);

}

} catch (DeploymentException e) {

throw new ServletException(e);

}

}

static WsServerContainer init(ServletContext servletContext,

boolean initBySciMechanism) {

WsServerContainer sc = new WsServerContainer(servletContext);

servletContext.setAttribute(

Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE, sc);

// 注册监听器WsSessionListener给servletContext,

// 在http session销毁时触发 ws session的关闭销毁

servletContext.addListener(new WsSessionListener(sc));

// Can’t register the ContextListener again if the ContextListener is

// calling this method

if (initBySciMechanism) {

// 注册监听器WsContextListener给servletContext,

// 在 servletContext初始化时触发WsSci.init

// 在 servletContext销毁时触发WsServerContainer的销毁

// 不过呢,只在WsSci.onStartup时注册一次

servletContext.addListener(new WsContextListener());

}

return sc;

}

}

从上述源码中可以看出ServerApplicationConfig起到一个过滤的作用:

  • 当没有ServerApplicationConfig时,加了@ServerEndpoint的类会默认全部加到一个Set集合(filteredPojoEndpoints),所以加了@ServerEndpoint的类可以不需要自定义实现ServerApplicationConfig

  • 当有ServerApplicationConfig时,ServerApplicationConfig#getEndpointConfigs用来过滤Endpoint子类,并且Endpoint子类必须封装成一个ServerEndpointConfig

  • ServerApplicationConfig#getAnnotatedEndpointClasses用来过滤加了注解@ServerEndpoint的类,一般空实现就行了(如果不想某个类被加到WsServerContainer里,那不加@ServerEndpoint不就可以了)。

过滤之后的Endpoint子类和加了注解@ServerEndpoint的类会分别调用不同形参的WsServerContainer#addEndpoint,将其加到WsServerContainer里。

(2)WsServerContainer#addEndpoint
  • Endpoint子类加到WsServerContainer里,调用的是形参为ServerEndpointConfigaddEndpoint

public void addEndpoint(ServerEndpointConfig sec) throws DeploymentException {

addEndpoint(sec, false);

}

因为Endpoint子类需要使用者封装成ServerEndpointConfig,不需要Tomcat来封装。

  • 将加了注解@ServerEndpoint的类加到WsServerContainer,调用的是形参为Class<?>addEndpointfromAnnotatedPojo参数暂时在这个方法里没什么用处):

该方法主要职责就是解析@ServerEndpoint,获取pathdecodersencodersconfigurator等构建一个ServerEndpointConfig对象

@ServerEndpoint-andEndpoint

最终调用的都是如下这个比较复杂的方法,fromAnnotatedPojo表示是否是加了@ServerEndpoint的类。主要做了两件事:

  • 对加了@ServerEndpoint类的生命周期方法(@OnOpen@OnClose@OnError@OnMessage)的扫描和映射封装。

  • path的有效性检查和path param解析。

addEndpoint

(3)PojoMethodMapping方法映射和形参解析

PojoMethodMapping构造函数比较长,主要是对加了@OnOpen@OnClose@OnError@OnMessage的方法进行校验和映射,以及对每个方法的形参进行解析和校验,主要逻辑总结如下:

  • 对当前类以及其父类中的方法进行扫描。

  • 当前类中不能存在多个相同注解的方法,否则会抛出Duplicate annotation异常。

  • 父类和子类中存在相同注解的方法,子类必须重写该方法,否则会抛出Duplicate annotation异常。

  • 对于@OnMessage,可以有多个,但是接收消息的类型必须不同,消息类型大概分为三种:PongMessage心跳消息、字节型、字符型。

  • 如果扫描到对的注解都是父类的方法,子类重写了该方法,但是没有加响应的注解,则会被清除。

  • 形参解析。

public PojoMethodMapping(Class<?> clazzPojo, List<Class<? extends Decoder>> decoderClazzes, String wsPath,

InstanceManager instanceManager) throws DeploymentException {

this.wsPath = wsPath;

List decoders = Util.getDecoders(decoderClazzes, instanceManager);

Method open = null;

Method close = null;

Method error = null;

Method[] clazzPojoMethods = null;

Class<?> currentClazz = clazzPojo;

while (!currentClazz.equals(Object.class)) {

Method[] currentClazzMethods = currentClazz.getDeclaredMethods();

if (currentClazz == clazzPojo) {

clazzPojoMethods = currentClazzMethods;

}

for (Method method : currentClazzMethods) {

if (method.isSynthetic()) {

// Skip all synthetic methods.

// They may have copies of annotations from methods we are

// interested in and they will use the wrong parameter type

// (they always use Object) so we can’t used them here.

continue;

}

if (method.getAnnotation(OnOpen.class) != null) {

checkPublic(method);

if (open == null) {

open = method;

} else {

if (currentClazz == clazzPojo ||

!isMethodOverride(open, method)) {

// Duplicate annotation

// 抛出Duplicate annotation异常的两种情况:

// 1. 当前的类有多个相同注解的方法,如有两个@OnOpen

// 2. 当前类时父类,有相同注解的方法,但是其子类没有重写这个方法

// 即 父类和子类有多个相同注解的方法,且没有重写关系

throw new DeploymentException(sm.getString(

“pojoMethodMapping.duplicateAnnotation”,

OnOpen.class, currentClazz));

}

}

} else if (method.getAnnotation(OnClose.class) != null) {

checkPublic(method);

if (close == null) {

close = method;

} else {

if (currentClazz == clazzPojo ||

!isMethodOverride(close, method)) {

// Duplicate annotation

throw new DeploymentException(sm.getString(

“pojoMethodMapping.duplicateAnnotation”,

OnClose.class, currentClazz));

}

}

} else if (method.getAnnotation(OnError.class) != null) {

checkPublic(method);

if (error == null) {

error = method;

} else {

if (currentClazz == clazzPojo ||

!isMethodOverride(error, method)) {

// Duplicate annotation

throw new DeploymentException(sm.getString(

“pojoMethodMapping.duplicateAnnotation”,

OnError.class, currentClazz));

}

}

} else if (method.getAnnotation(OnMessage.class) != null) {

checkPublic(method);

MessageHandlerInfo messageHandler = new MessageHandlerInfo(method, decoders);

boolean found = false;

// 第一次扫描OnMessage时,onMessage为空,不会走下面的for,然后就把messageHandler加到onMessage里

// 如果非首次扫描到这里,即向上扫描父类,允许有多个接收消息类型完全不同的onmessage

for (MessageHandlerInfo otherMessageHandler : onMessage) {

// 如果多个onmessage接收的消息类型有相同的,则可能会抛出Duplicate annotation

// 1. 同一个类中多个onmessage有接收相同类型的消息

// 2. 父子类中多个onmessage有接收相同类型的消息,但不是重写关系

if (messageHandler.targetsSameWebSocketMessageType(otherMessageHandler)) {

found = true;

if (currentClazz == clazzPojo ||

!isMethodOverride(messageHandler.m, otherMessageHandler.m)) {

// Duplicate annotation

throw new DeploymentException(sm.getString(

“pojoMethodMapping.duplicateAnnotation”,

OnMessage.class, currentClazz));

}

}

}

if (!found) {

onMessage.add(messageHandler);

}

} else {

// Method not annotated

}

}

currentClazz = currentClazz.getSuperclass();

}

// If the methods are not on clazzPojo and they are overridden

// by a non annotated method in clazzPojo, they should be ignored

if (open != null && open.getDeclaringClass() != clazzPojo) {

// open 有可能是父类的,子类即clazzPojo有重写该方法,但是没有加OnOpen注解

// 则 open置为null

if (isOverridenWithoutAnnotation(clazzPojoMethods, open, OnOpen.class)) {

open = null;

}

}

if (close != null && close.getDeclaringClass() != clazzPojo) {

if (isOverridenWithoutAnnotation(clazzPojoMethods, close, OnClose.class)) {

close = null;

}

}

if (error != null && error.getDeclaringClass() != clazzPojo) {

if (isOverridenWithoutAnnotation(clazzPojoMethods, error, OnError.class)) {

error = null;

}

}

List overriddenOnMessage = new ArrayList<>();

for (MessageHandlerInfo messageHandler : onMessage) {

if (messageHandler.m.getDeclaringClass() != clazzPojo

&& isOverridenWithoutAnnotation(clazzPojoMethods, messageHandler.m, OnMessage.class)) {

overriddenOnMessage.add(messageHandler);

}

}

// 子类重写了的onmessage方法,但没有加OnMessage注解的需要从onMessage list 中删除

for (MessageHandlerInfo messageHandler : overriddenOnMessage) {

onMessage.remove(messageHandler);

}

this.onOpen = open;

this.onClose = close;

this.onError = error;

// 参数解析

onOpenParams = getPathParams(onOpen, MethodType.ON_OPEN);

onCloseParams = getPathParams(onClose, MethodType.ON_CLOSE);

onErrorParams = getPathParams(onError, MethodType.ON_ERROR);

}

虽然方法名可以随意,但是形参却有着强制限制:

  • @onOpen方法,可以有的参数SessionEndpointConfig@PathParam,不能有其他参数。

  • @onError方法,可以有的参数Session@PathParam, 必须有Throwable,不能有其他参数。

  • @onClose方法,可以有的参数Session, CloseReason, @PathParam,不能有其他参数。

getPathParams

2、协议升级(握手)

Tomcat中WebSocket是通过UpgradeToken机制实现的,其具体的升级处理器为WsHttpUpgradeHandlerWebSocket协议升级的过程比较曲折,首先要通过过滤器WsFilter进行升级判断,然后调用org.apache.catalina.connector.Request#upgrade进行UpgradeToken的构建,最后通过org.apache.catalina.connector.Request#coyoteRequest回调函数actionUpgradeToken回传给连接器为后续升级处理做准备。

WebSocket升级过程

(1)WsFilter

WebSocket协议升级的过程比较曲折。带有WebSocket握手的请求会平安经过Tomcat的Connector,被转发到Servlet容器中,在业务处理之前经过过滤器WsFilter判断是否需要升级(WsFilterorg.apache.catalina.core.ApplicationFilterChain过滤链中触发):

  • 首先判断WsServerContainer是否有进行Endpoint的扫描和注册以及请头中是否有Upgrade: websocket

  • 获取请求path即uriWsServerContainer中找对应的ServerEndpointConfig

  • 调用UpgradeUtil.doUpgrade进行升级。

WsFilter

(2)UpgradeUtil#doUpgrade

UpgradeUtil#doUpgrade主要做了如下几件事情:

  • 检查HttpServletRequest的一些请求头的有效性,如Connection: upgradeSec-WebSocket-Version:13Sec-WebSocket-Key等。

  • HttpServletResponse设置一些响应头,如Upgrade:websocketConnection: upgrade、根据Sec-WebSocket-Key的值生成响应头Sec-WebSocket-Accept的值。

  • 封装WsHandshakeRequestWsHandshakeResponse

  • 调用HttpServletRequest#upgrade进行升级,并获取WsHttpUpgradeHandler(具体的升级流程处理器)。

// org.apache.tomcat.websocket.server.UpgradeUtil#doUpgrade

public static void doUpgrade(WsServerContainer sc, HttpServletRequest req,

HttpServletResponse resp, ServerEndpointConfig sec,

Map<String,String> pathParams)

throws ServletException, IOException {

// Validate the rest of the headers and reject the request if that

// validation fails

String key;

String subProtocol = null;

// 检查请求头中是否有 Connection: upgrade

if (!headerContainsToken(req, Constants.CONNECTION_HEADER_NAME,

Constants.CONNECTION_HEADER_VALUE)) {

resp.sendError(HttpServletResponse.SC_BAD_REQUEST);

return;

}

// 检查请求头中的 Sec-WebSocket-Version:13

if (!headerContainsToken(req, Constants.WS_VERSION_HEADER_NAME,

Constants.WS_VERSION_HEADER_VALUE)) {

resp.setStatus(426);

resp.setHeader(Constants.WS_VERSION_HEADER_NAME,

Constants.WS_VERSION_HEADER_VALUE);

return;

}

// 获取 Sec-WebSocket-Key

key = req.getHeader(Constants.WS_KEY_HEADER_NAME);

if (key == null) {

resp.sendError(HttpServletResponse.SC_BAD_REQUEST);

return;

}

// Origin check,校验 Origin 是否有权限

String origin = req.getHeader(Constants.ORIGIN_HEADER_NAME);

if (!sec.getConfigurator().checkOrigin(origin)) {

resp.sendError(HttpServletResponse.SC_FORBIDDEN);

return;

}

// Sub-protocols

List subProtocols = getTokensFromHeader(req,

Constants.WS_PROTOCOL_HEADER_NAME);

subProtocol = sec.getConfigurator().getNegotiatedSubprotocol(

sec.getSubprotocols(), subProtocols);

// Extensions

// Should normally only be one header but handle the case of multiple

// headers

List extensionsRequested = new ArrayList<>();

Enumeration extHeaders = req.getHeaders(Constants.WS_EXTENSIONS_HEADER_NAME);

while (extHeaders.hasMoreElements()) {

Util.parseExtensionHeader(extensionsRequested, extHeaders.nextElement());

}

// Negotiation phase 1. By default this simply filters out the

// extensions that the server does not support but applications could

// use a custom configurator to do more than this.

List installedExtensions = null;

if (sec.getExtensions().size() == 0) {

installedExtensions = Constants.INSTALLED_EXTENSIONS;

} else {

installedExtensions = new ArrayList<>();

installedExtensions.addAll(sec.getExtensions());

installedExtensions.addAll(Constants.INSTALLED_EXTENSIONS);

}

List negotiatedExtensionsPhase1 = sec.getConfigurator().getNegotiatedExtensions(

installedExtensions, extensionsRequested);

// Negotiation phase 2. Create the Transformations that will be applied

// to this connection. Note than an extension may be dropped at this

// point if the client has requested a configuration that the server is

// unable to support.

List transformations = createTransformations(negotiatedExtensionsPhase1);

List negotiatedExtensionsPhase2;

if (transformations.isEmpty()) {

negotiatedExtensionsPhase2 = Collections.emptyList();

} else {

negotiatedExtensionsPhase2 = new ArrayList<>(transformations.size());

for (Transformation t : transformations) {

negotiatedExtensionsPhase2.add(t.getExtensionResponse());

}

}

// Build the transformation pipeline

Transformation transformation = null;

StringBuilder responseHeaderExtensions = new StringBuilder();

boolean first = true;

for (Transformation t : transformations) {

if (first) {

first = false;

} else {

responseHeaderExtensions.append(‘,’);

}

append(responseHeaderExtensions, t.getExtensionResponse());

if (transformation == null) {

transformation = t;

} else {

transformation.setNext(t);

}

}

// Now we have the full pipeline, validate the use of the RSV bits.

if (transformation != null && !transformation.validateRsvBits(0)) {

throw new ServletException(sm.getString(“upgradeUtil.incompatibleRsv”));

}

最后总结我的面试经验

2021年的金三银四一眨眼就到了,对于很多人来说是跳槽的好机会,大厂面试远没有我们想的那么困难,摆好心态,做好准备,你也可以的。

另外,面试中遇到不会的问题不妨尝试讲讲自己的思路,因为有些问题不是考察我们的编程能力,而是逻辑思维表达能力;最后平时要进行自我分析与评价,做好职业规划,不断摸索,提高自己的编程能力和抽象思维能力。

BAT面试经验

实战系列:Spring全家桶+Redis等

其他相关的电子书:源码+调优

面试真题:

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

f the client has requested a configuration that the server is

// unable to support.

List transformations = createTransformations(negotiatedExtensionsPhase1);

List negotiatedExtensionsPhase2;

if (transformations.isEmpty()) {

negotiatedExtensionsPhase2 = Collections.emptyList();

} else {

negotiatedExtensionsPhase2 = new ArrayList<>(transformations.size());

for (Transformation t : transformations) {

negotiatedExtensionsPhase2.add(t.getExtensionResponse());

}

}

// Build the transformation pipeline

Transformation transformation = null;

StringBuilder responseHeaderExtensions = new StringBuilder();

boolean first = true;

for (Transformation t : transformations) {

if (first) {

first = false;

} else {

responseHeaderExtensions.append(‘,’);

}

append(responseHeaderExtensions, t.getExtensionResponse());

if (transformation == null) {

transformation = t;

} else {

transformation.setNext(t);

}

}

// Now we have the full pipeline, validate the use of the RSV bits.

if (transformation != null && !transformation.validateRsvBits(0)) {

throw new ServletException(sm.getString(“upgradeUtil.incompatibleRsv”));

}

最后总结我的面试经验

2021年的金三银四一眨眼就到了,对于很多人来说是跳槽的好机会,大厂面试远没有我们想的那么困难,摆好心态,做好准备,你也可以的。

另外,面试中遇到不会的问题不妨尝试讲讲自己的思路,因为有些问题不是考察我们的编程能力,而是逻辑思维表达能力;最后平时要进行自我分析与评价,做好职业规划,不断摸索,提高自己的编程能力和抽象思维能力。

[外链图片转存中…(img-i0hrlr52-1715415368504)]

BAT面试经验

实战系列:Spring全家桶+Redis等

[外链图片转存中…(img-lKtBzwTb-1715415368505)]

其他相关的电子书:源码+调优

[外链图片转存中…(img-5LRryIfK-1715415368505)]

面试真题:

[外链图片转存中…(img-HfYZ43Du-1715415368505)]

[外链图片转存中…(img-xvuNl4Zu-1715415368506)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 18
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java实现WebSocket可以使用Java API for WebSocket(R 356)来实现。下面是一个简单的示例代码: 首先,你需要创建一个WebSocket端点类,该类需要继承javax.websocket.Endpoint类,并实现其onOpen、onClose、onError和onMessage方法。例如: ```java import javax.websocket.*; import javax.websocket.server.ServerEndpoint; @ServerEndpoint("/websocket") public class MyWebSocketEndpoint { @OnOpen public void onOpen(Session session) { System.out.println("WebSocket opened: " + session.getId()); } @OnClose public void onClose(Session session, CloseReason reason) { System.out.println("WebSocket closed: " + session.getId() + ", Reason: " + reason.getReasonPhrase()); } @OnError public void onError(Session session, Throwable throwable) { System.out.println("WebSocket error: " + session.getId() + ", Error: " + throwable.getMessage()); } @OnMessage public void onMessage(String message, Session session) { System.out.println("Received message from " + session.getId() + ": " + message); // 处理接收到的消息 } } ``` 然后,你需要配置一个WebSocket容器,例如使用Tomcat作为容器。在web.xml文件添加以下配置: ```xml <web-app> <display-name>My WebSocket Application</display-name> <servlet> <servlet-name>websocket</servlet-name> <servlet-class>org.apache.tomcat.websocket.server.WsSci</servlet-class> <init-param> <param-name>javax.websocket.endpointClasses</param-name> <param-value>com.example.MyWebSocketEndpoint</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>websocket</servlet-name> <url-pattern>/websocket</url-pattern> </servlet-mapping> </web-app> ``` 最后,你可以使用WebSocket客户端连接到你的WebSocket端点。例如,使用JavaScript的WebSocket API: ```javascript var socket = new WebSocket("ws://localhost:8080/your-webapp/websocket"); socket.onopen = function() { console.log("WebSocket opened"); }; socket.onclose = function(event) { console.log("WebSocket closed, Code: " + event.code + ", Reason: " + event.reason); }; socket.onerror = function(error) { console.log("WebSocket error: " + error); }; socket.onmessage = function(event) { console.log("Received message: " + event.data); }; ``` 这样,你就可以在Java实现WebSocket了。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值