浅读Tomcat源码(六)---webSocket
来源:互联网 发布:使用sql语句创建用户 编辑:程序博客网 时间:2024/05/19 13:57
自从HTML5出现以后,webSocket机制立马引起了很多人的关注,相比起以前使用Ajax轮询的机制实现服务端数据向客户端的推送,webSocket的长连接显得更加高效实用,我们这次就来看下Tomcat中是如何实现WebSocket的服务端的。先帖一段Tomcat的webSocket实现的例子:
@ServerEndpoint("/TestWebSocket")public class TestWebSocket {@OnMessage public void onMessage(String message, Session session) throws IOException, InterruptedException { } @OnOpen public void onOpen() { System.out.println("Client connected"); } @OnClose public void onClose() { System.out.println("Connection closed"); } }
首先我们要了解下webSocket的协议原理,首先webSocket并不是单纯的http协议,这一点一定要清楚,它是一种基于TCP的协议,但是他的请求最初需要由http协议来建立连接,其最初的http协议的内容大致如下:
我们可以看到其Connection设置为Upgrade,然后我们转入Tomcat的源码。在前文中我们可以了解到Tomcat接收http请求的详细逻辑,其中有一步是调用了AbstractProtocol的process方法,而这步方法中前文说到最核心的一步是:
state = processor.process(wrapper, status);
现在我们继续往下看,在执行了这一步以后,Tomcat对state的值进行了一次判断,
if (state == SocketState.UPGRADING) { // Get the HTTP upgrade handler UpgradeToken upgradeToken = processor.getUpgradeToken(); // Retrieve leftover input ByteBuffer leftOverInput = processor.getLeftoverInput(); if (upgradeToken == null) { // Assume direct HTTP/2 connection UpgradeProtocol upgradeProtocol = getProtocol().getUpgradeProtocol("h2c"); if (upgradeProtocol != null) { processor = upgradeProtocol.getProcessor( wrapper, getProtocol().getAdapter()); wrapper.unRead(leftOverInput); // Associate with the processor with the connection connections.put(socket, processor); } else { if (getLog().isDebugEnabled()) { getLog().debug(sm.getString( "abstractConnectionHandler.negotiatedProcessor.fail", "h2c")); } return SocketState.CLOSED; } } else { HttpUpgradeHandler httpUpgradeHandler = upgradeToken.getHttpUpgradeHandler(); // Release the Http11 processor to be re-used release(processor); // Create the upgrade processor processor = getProtocol().createUpgradeProcessor(wrapper, upgradeToken); wrapper.unRead(leftOverInput); // Mark the connection as upgraded wrapper.setUpgraded(true); // Associate with the processor with the connection connections.put(socket, processor); // Initialise the upgrade handler (which may trigger // some IO using the new protocol which is why the lines // above are necessary) // This cast should be safe. If it fails the error // handling for the surrounding try/catch will deal with // it. if (upgradeToken.getInstanceManager() == null) { httpUpgradeHandler.init((WebConnection) processor); } else { ClassLoader oldCL = upgradeToken.getContextBind().bind(false, null); try { httpUpgradeHandler.init((WebConnection) processor); } finally { upgradeToken.getContextBind().unbind(false, oldCL); } } }
这里就判断了state是否为upgrade,如果是的话,调用到了一步非常核心的代码:httpUpgradeHandler.init((WebConnection) processor);
我们转入这个init方法,看到其实现类WsHttpUpgradleHandler:
@Override public void init(WebConnection connection) { if (ep == null) { throw new IllegalStateException( sm.getString("wsHttpUpgradeHandler.noPreInit")); } String httpSessionId = null; Object session = handshakeRequest.getHttpSession(); if (session != null ) { httpSessionId = ((HttpSession) session).getId(); } // Need to call onOpen using the web application's class loader // Create the frame using the application's class loader so it can pick // up application specific config from the ServerContainerImpl Thread t = Thread.currentThread(); ClassLoader cl = t.getContextClassLoader(); t.setContextClassLoader(applicationClassLoader); try { wsRemoteEndpointServer = new WsRemoteEndpointImplServer(socketWrapper, webSocketContainer); wsSession = new WsSession(ep, wsRemoteEndpointServer, webSocketContainer, handshakeRequest.getRequestURI(), handshakeRequest.getParameterMap(), handshakeRequest.getQueryString(), handshakeRequest.getUserPrincipal(), httpSessionId, negotiatedExtensions, subProtocol, pathParameters, secure, endpointConfig); wsFrame = new WsFrameServer(socketWrapper, wsSession, transformation, applicationClassLoader); // WsFrame adds the necessary final transformations. Copy the // completed transformation chain to the remote end point. wsRemoteEndpointServer.setTransformation(wsFrame.getTransformation()); ep.onOpen(wsSession, endpointConfig); webSocketContainer.registerSession(ep, wsSession); } catch (DeploymentException e) { throw new IllegalArgumentException(e); } finally { t.setContextClassLoader(cl); } }
看到其取到了对应的endpoint,并调用了OnOpen方法,这个OnOpen无非是个反射的实现,大家可以自己去看,关键是这个endPoint的获取,
这个endPonit的获取分为了两步,第一步是启动之初,会给每个标注了serverEndPoint注解的类一个config,我们看下这个注解的源码:
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public @interface ServerEndpoint { /** * URI or URI-template that the annotated class should be mapped to. * @return The URI or URI-template that the annotated class should be mapped * to. */ String value(); String[] subprotocols() default {}; Class<? extends Decoder>[] decoders() default {}; Class<? extends Encoder>[] encoders() default {}; public Class<? extends ServerEndpointConfig.Configurator> configurator() default ServerEndpointConfig.Configurator.class;}然后看configurator被调用的地方:
@Override public void addEndpoint(Class<?> pojo) throws DeploymentException { ServerEndpoint annotation = pojo.getAnnotation(ServerEndpoint.class); if (annotation == null) { throw new DeploymentException( sm.getString("serverContainer.missingAnnotation", pojo.getName())); } String path = annotation.value(); // Validate encoders validateEncoders(annotation.encoders()); // ServerEndpointConfig ServerEndpointConfig sec; Class<? extends Configurator> configuratorClazz = annotation.configurator(); Configurator configurator = null; if (!configuratorClazz.equals(Configurator.class)) { try { configurator = annotation.configurator().newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new DeploymentException(sm.getString( "serverContainer.configuratorFail", annotation.configurator().getName(), pojo.getClass().getName()), e); } } sec = ServerEndpointConfig.Builder.create(pojo, path). decoders(Arrays.asList(annotation.decoders())). encoders(Arrays.asList(annotation.encoders())). subprotocols(Arrays.asList(annotation.subprotocols())). configurator(configurator). build(); addEndpoint(sec); }至于这个addEndPoint方法被调用的地方,脑子想想都知道是Tomcat启动的时候完成的,所以我们也不用特别关心,关键在于这个add的过程将注解了serverEndPoint的类添加进入了endPoint的map中。
而这个类WsServerContainer唯一提供了这个map给外部是在一个findMapping方法中:
public WsMappingResult findMapping(String path) { // Prevent registering additional endpoints once the first attempt has // been made to use one if (addAllowed) { addAllowed = false; } // Check an exact match. Simple case as there are no templates. ServerEndpointConfig sec = configExactMatchMap.get(path); if (sec != null) { return new WsMappingResult(sec, Collections.<String, String>emptyMap()); } // No exact match. Need to look for template matches. UriTemplate pathUriTemplate = null; try { pathUriTemplate = new UriTemplate(path); } catch (DeploymentException e) { // Path is not valid so can't be matched to a WebSocketEndpoint return null; } // Number of segments has to match Integer key = Integer.valueOf(pathUriTemplate.getSegmentCount()); SortedSet<TemplatePathMatch> templateMatches = configTemplateMatchMap.get(key); if (templateMatches == null) { // No templates with an equal number of segments so there will be // no matches return null; } // List is in alphabetical order of normalised templates. // Correct match is the first one that matches. Map<String,String> pathParams = null; for (TemplatePathMatch templateMatch : templateMatches) { pathParams = templateMatch.getUriTemplate().match(pathUriTemplate); if (pathParams != null) { sec = templateMatch.getConfig(); break; } } if (sec == null) { // No match return null; } return new WsMappingResult(sec, pathParams); }这里可以看出,这个方法在项目启动最初添加的endpoint的集合中查找当前path对应的endpoint,我们利用ide的快捷键可以看到这个方法最终是被过滤器调用的,换句话说endpoint是在Tomcat启动时候被注册,收到请求时候通过过滤器被获取,然后在请求处理中被调用,最初调用的是OnOpen,而OnOpen完了以后我们可以看到调用了:webSocketContainer.registerSession(ep, wsSession);
这个方法最终就是把Open时候传入的session注册进入Set中以备后用。至此,WebSocket建立连接部分就完成了,Http的任务也完成了。
那么问题来了,之后WebSocket的相互传送是用什么来完成的呢?由于传送是双向的,我们简单起见看下服务端给客户端推送数据的过程,找到wsSession的sendMessage操作,通过一系列的调用,发现最终调用到了WsRemoteEndPoint类的onWritePossible方法:
public void onWritePossible(boolean useDispatch) { ByteBuffer[] buffers = this.buffers; if (buffers == null) { // Servlet 3.1 will call the write listener once even if nothing // was written return; } boolean complete = false; try { socketWrapper.flush(false); // If this is false there will be a call back when it is true while (socketWrapper.isReadyForWrite()) { complete = true; for (ByteBuffer buffer : buffers) { if (buffer.hasRemaining()) { complete = false; socketWrapper.write( false, buffer.array(), buffer.arrayOffset(), buffer.limit()); buffer.position(buffer.limit()); break; } } if (complete) { socketWrapper.flush(false); complete = socketWrapper.isReadyForWrite(); if (complete) { wsWriteTimeout.unregister(this); clearHandler(null, useDispatch); if (close) { close(); } } break; } } } catch (IOException | IllegalStateException e) { wsWriteTimeout.unregister(this); clearHandler(e, useDispatch); close(); } if (!complete) { // Async write is in progress long timeout = getSendTimeout(); if (timeout > 0) { // Register with timeout thread timeoutExpiry = timeout + System.currentTimeMillis(); wsWriteTimeout.register(this); } } }
我们看到这个传输数据最终还是用了socketWrapper完成的,对前面的文章熟悉的人应该可以记得,Tomcat接收到Socket连接的时候就是把socket放入了socketWrapper中的。
至此,我们可以总结出全部的过程了,无论http还是webSocket,其本质都是Socket连接,对于一般的http请求,socket在最后都会断开,而如果是upgrade类型的http请求,则会将socket转入wsSession中,由webSocket协议来完成后续的连接操作。