浅读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协议的内容大致如下:

GET /chat HTTP/1.1Host: server.example.comUpgrade: websocketConnection: UpgradeSec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==Sec-WebSocket-Protocol: chat, superchatSec-WebSocket-Version: 13Origin: http://example.com
我们可以看到其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协议来完成后续的连接操作。




阅读全文
'); })();
0 0
原创粉丝点击
热门IT博客
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 秋风引古诗的意思 念君欢 不念过去不畏将来图片 新人进群念一遍图片 不畏将来不念过往图片 女子打坐念佛图片 念珠包皮炎图片 念夕 念念有夕全文免费阅读 一夕千念 一个石一个夕念什么 辰少宠妻入骨 念夕夏 念夕空汉服照片 张恒娶了念夕空 奔向深空 夕什 虫夫念什么 口夫念什么 孕夫计划 陸念忆 夫夫车念什么 两个夫一个车念什么 将女贵夫 念非卿 兽夫妖娆 步念城 一个口一个夫念什么 念夫子作品 星际萌夫 念夫子 剑神在星际 念夫子 念夫子 剑神在星际 念奴娇 念奴娇过洞庭 念奴娇中秋 夜夜念奴娇 念奴娇追思焦裕禄 念奴娇昆仑意思 念奴娇昆仑朗诵 念奴娇昆仑的意思 念奴娇昆仑全诗 念奴娇焦裕禄 许我不爱你 念奴娇 念娇奴 念如娇 念笯娇作品