WebSocket使用实例及集成原理

目录

一、概述

1.简介

2.优点

二、WebSocket请求数据格式

1.request请求数据格式

2.response相应数据格式

三、spring集成实例

1.jar包

2.前端页面

3.后台代码

四、集成原理

1.Tomcat请求流程

2.切入点ServerEndpointExporter类

3.创建连接及发送消息等原理

3.1 创建连接

3.2 发送消息

3.3 关闭和抛异常


一、概述

1.简介

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

HTTP有1.1和1.0之说,即所谓的keep-alive,把多个HTTP请求合并为一个,但是Websocket其实是一个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器,所以在握手阶段使用了HTTP协议的101状态码进行握手。

使用场景最多的便是代替Ajax间隔和long轮询。

2.优点

相较于HTTP1.0/1.1协议的优点:

  • 建立在 TCP 协议之上,服务器端的实现比较容易;
  • 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器;
  • 数据格式比较轻量,性能开销小,通信高效;
  • 可以发送文本,也可以发送二进制数据;
  • 没有同源限制,客户端可以与任意服务器通信;
  • 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

二、WebSocket请求数据格式

1.request请求数据格式

典型的WebSocket请求数据格式:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

可以看到相较于常用的HTTP协议多了下面几个参数:

Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

其中“Upgrade: websocket”和“Connection: Upgrade”则是告诉Apache、Nginx等服务器现在发起的请求是Websocket协议,后续在讲解集成WebSocket时会具体降到这两个参数的使用。

Sec-WebSocket-Key是一个Base64的值,这个是浏览器随机生成的,用来验证websocket请求。Sec_WebSocket-Protocol是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。Sec-WebSocket-Version是告诉服务器所使用的WebSocket Draft(协议版本),在最初的时候,WebSocket协议还在Draft阶段,各种奇奇怪怪的协议都有,因此需要指定协议版本。·

2.response相应数据格式

当请求后服务器会返回以下数据格式:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

依然是固定的,告诉客户端即将升级的是WebSocket协议,而不是其它的协议类型。Sec-WebSocket-Accept这个则是经过服务器确认,并且加密过后的Sec-WebSocket-Key。 Sec-WebSocket-Protocol则是表示最终使用的协议。

至此,HTTP已经完成它所有工作了,接下来就是完全按照WebSocket协议进行了。

三、spring集成实例

1.jar包

所需jar包,二选其一:

<!-- Springboot WebSocket包 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<!-- 普通Spring项目WebSocket包 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-messaging</artifactId>
    <version>5.1.8.RELEASE</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-websocket</artifactId>
    <version>5.1.8.RELEASE</version>
    <scope>compile</scope>
</dependency>

2.前端页面

html页面如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebSocket test</title>
</head>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
    var socket;
    function openSocket() {
        if (typeof(WebSocket) == "undefined") {
            console.log("浏览器不支持WebSocket");
        } else {
            console.log("浏览器支持WebSocket");
            var socketUrl = "ws://localhost:8888/test/message/" + document.getElementById("user-id-text").value;
            console.log("socketUrl:" + socketUrl);
            if (socket != null) {
                socket.close();
                socket = null;
            }
            socket = new WebSocket(socketUrl);

            socket.onopen = function () {
                console.log("Websocket已打开");
            };

            socket.onmessage = function (msg) {
                console.log(msg.data);
                appendMessageContent("服务器消息:" + msg.data);
            };

            socket.onclose = function () {
                console.log("Websocket已关闭");
            };

            socket.onerror = function () {
                console.log("Websocket异常");
            };
        }
    }

    function appendMessageContent(message) {
        var messageContentDiv = document.getElementById("message-content");
        var h5 = document.createElement("h5");
        h5.innerText = message;
        messageContentDiv.appendChild(h5);
    }

    function sendMessage() {
        if (typeof(WebSocket) == "undefined") {
            console.log("浏览器不支持WebSocket");
        } else {
            var sendMessageText = document.getElementById("send-message-text").value;
            console.log("发送的消息内容为:" + sendMessageText);
            socket.send(sendMessageText);
        }
    }
</script>
<body>
    <div id="user-id">
        <p>连接的userId:</p><br/>
        <label for="user-id-text"><input type="text" id="user-id-text"/></label>
        <input type="button" value="提交" onclick="openSocket()"/>
    </div>
    <div id="send-message">
        <p>给服务器发送的内容:</p><br/>
        <label for="user-id-text"><input type="text" id="send-message-text"/></label>
        <input type="button" value="提交" onclick="sendMessage()"/>
    </div>
    <div id="message-content">
        <h2>这是消息展示区域</h2>
    </div>
</body>
</html>

3.后台代码

Websocket实例代码:

@Slf4j
@Component
@ServerEndpoint("/test/message/{openId}")
public class TestWebSocket extends ServerEndpointExporter {

    private static final ConcurrentHashMap<String, TestWebSocket> 
            WEBSOCKET_MAP = new ConcurrentHashMap<>();
    private Session session;

    /**
     * websocket新建连接调用方法
     *
     * @param session
     * @param openId
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("openId") String openId) {
        this.session = session;
        if (WEBSOCKET_MAP.putIfAbsent(openId, this) == null) {
            createNewWebSocket(openId);
        }

        log.info("current user number:{}", WEBSOCKET_MAP.size());
    }

    /**
     * 新增openId触发方法
     *
     * @param openId
     */
    private void createNewWebSocket(String openId) {
        log.info("add new openId:{}", openId);
    }

    @OnClose
    public void onClose(@PathParam("openId") String openId) {
        if (WEBSOCKET_MAP.remove(openId) != null) {
            deleteOneWebSocket(openId);
        }

        log.info("a openId is remove, current openId number:{}", 
                WEBSOCKET_MAP.size());
    }

    /**
     * 用户退出触发
     *
     * @param openId
     */
    private void deleteOneWebSocket(String openId) {
        log.info("delete a openId:{}", openId);
    }

    /**
     * 用户发送消息
     *
     * @param openId
     * @param message
     */
    @OnMessage
    public void onMessage(@PathParam("openId") String openId, 
            String message) {
        log.info("从服务器接收到了{}消息:{}", openId, message);
        if (!StringUtils.isEmpty(message) && 
                WEBSOCKET_MAP.containsKey(openId)) {
            try {
                TestWebSocket webSocket = WEBSOCKET_MAP.get(openId);
                webSocket.session.getBasicRemote().sendText(message);
                log.info("给{}发送了消息:{}", openId, message);
            } catch (IOException e) {
                log.error("{}发送信息\"{}\"失败", openId, message, e);
                e.printStackTrace();
            }
        } else {
            log.info("用户不存在,openId:{}", openId);
        }
    }

    /**
     * 发生错误时
     *
     * @param openId
     * @param session
     * @param error
     */
    @OnError
    public void onError(@PathParam("openId") String openId, 
            Session session, Throwable error) {
        log.error("用户{}错误,原因:{}", openId, error.getMessage());
        log.error("error:", error);
    }

}

当然也可以把ServerEndpointExporter类当成一个单独的Bean配置在@Configuration注解的类中,但效果是一样的,都是需要Spring框架读取到ServerEndpointExporter类,从而开始将WebSocket集成到Spring框架中。

四、集成原理

1.Tomcat请求流程

要想知道WebSocket和Spring如何集成,首先我们需要知道Tomcat一般请求到后台是如何处理的,到后面分析源码时我们便可以将整体流程串起来了。有兴趣的可以看下Tomcat系列文章(一)Tomcat架构及运行原理之基本架构

具体请求流程如下:

  1. Connector组件的Acceptor监听客户端套接字连接并接收Socket;
  2. 将连接交给线程池Executor处理,开始执行请求响应任务;
  3. Processor组件读取消息报文,解析请求行、请求体、请求头,封装成Request对象;
  4. Mapper组件根据请求行的URL值和请求头的Host值匹配由哪个Host容器、Context容器、Wrapper容器处理请求;
  5. CoyoteAdaptor组件负责将Connector组件和Engine容器关联起来,把生成的Request对象和响应对象Response传递到Engine容器中,调用 Pipeline;
  6. Engine容器的管道开始处理,管道中包含若干个Valve、每个Valve负责部分处理逻辑。执行完Valve后会执行基础的 Valve--StandardEngineValve,负责调用Host容器的Pipeline;
  7. Host容器的管道开始处理,流程类似,最后执行 Context容器的Pipeline;
  8. Context容器的管道开始处理,流程类似,最后执行 Wrapper容器的Pipeline;
  9. Wrapper容器的管道开始处理,流程类似,最后执行 Wrapper容器对应的Servlet对象的处理方法。

2.切入点ServerEndpointExporter类

首先看到该类的部分关键源码:

public class ServerEndpointExporter extends WebApplicationObjectSupport
      implements InitializingBean, SmartInitializingSingleton {
    @Nullable
    private List<Class<?>> annotatedEndpointClasses;
    @Nullable
    private ServerContainer serverContainer;
    @Override
    public void afterPropertiesSet() {
       Assert.state(getServerContainer() != null, 
               "javax.websocket.server.ServerContainer not available");
    }
    @Override
    public void afterSingletonsInstantiated() {
       registerEndpoints();
    }
    protected void registerEndpoints() {
        ...
        for (Class<?> endpointClass : endpointClasses) {
           registerEndpoint(endpointClass);
        }
        ...
    }
    private void registerEndpoint(Class<?> endpointClass) {
        ServerContainer serverContainer = getServerContainer();
        ...
        try {
            serverContainer.addEndpoint(endpointClass);
        }
        ...
    }
}

serverContainer对象为websocket的注册容器,当没有手动注入时,将会取ServletContext中的javax.websocket.server.ServerContainer属性赋值。

annotatedEndpointClasses集合对象存储的则是项目中所有被@ServerEndpoint注解的类。

在registerEndpoints方法中除了将@ServerEndpoint注解的类搜寻出来注册进serverContainer之外,还会将Spring容器中的ServerEndpointConfig类注册进serverContainer,这些将会在之后的创建连接、消息发送和关闭连接中起到至关重要的作用。

至于afterSingletonsInstantiated方法会在何时调用,那么就需要看一下Spring源码了,其调用位置是在ConfigurableListableBeanFactory接口中的preInstantiateSingletons方法中。

在上面registerEndpoint方法调用链中会调用到WsServerContainer类的addEndpoint方法,这个方法便是读取@ServerEndpoint注解类其中的@OnOpen、@OnMessage等注解方法。

WsServerContainer部分关键源码如下:

public class WsServerContainer extends WsWebSocketContainer
        implements ServerContainer {
    @Override
    public void addEndpoint(Class<?> pojo) throws DeploymentException {
        ...
        try {
            ServerEndpoint annotation = 
                    pojo.getAnnotation(ServerEndpoint.class);
            ...
            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);
    }
    @Override
    public void addEndpoint(ServerEndpointConfig sec) 
            throws DeploymentException {
        ...
        try {
            String path = sec.getPath();
            PojoMethodMapping methodMapping = 
                    new PojoMethodMapping(sec.getEndpointClass(),
                            sec.getDecoders(), path);
            ...
        }
        ...
    }
}

PojoMethodMapping类是一个关键类,看这个类的名字便能够看出来这个类的主要作用便是记录POJO的方法映射,即对应的OnOpen,OnMessage等消息信息。其源码如下:

public class PojoMethodMapping {
    private static final StringManager sm =
        StringManager.getManager(PojoMethodMapping.class);

    private final Method onOpen;
    private final Method onClose;
    private final Method onError;
    private final PojoPathParam[] onOpenParams;
    private final PojoPathParam[] onCloseParams;
    private final PojoPathParam[] onErrorParams;
    private final List<MessageHandlerInfo> onMessage = new ArrayList<>();
    private final String wsPath;
    public PojoMethodMapping(Class<?> clazzPojo,
        List<Class<? extends Decoder>> decoderClazzes, String wsPath)
                throws DeploymentException {
        ...
        Method open = null;
        Method close = null;
        Method error = null;
        ...
        while (!currentClazz.equals(Object.class)) {
            ...
            for (Method method : currentClazzMethods) {
                ...
                if (method.getAnnotation(OnOpen/OnError/OnClose.class) 
                        != null) {
                    checkPublic(method);
                    if (open == null) {
                        open = method;
                    } else {
                        if (currentClazz == clazzPojo ||
                                !isMethodOverride(open, method)) {
                            // Duplicate annotation
                            throw new DeploymentException(sm.getString(
                                "pojoMethodMapping.duplicateAnnotation",
                                OnOpen.class, currentClazz));
                        }
                    }
                } else if (method.getAnnotation(OnMessage.class) != null) {
                    checkPublic(method);
                    MessageHandlerInfo messageHandler = 
                            new MessageHandlerInfo(method, decoders);
                    boolean found = false;
                    for (MessageHandlerInfo otherMessageHandler : 
                            onMessage) {
                        if (messageHandler
                                .targetsSameWebSocketMessageType(
                                otherMessageHandler)) {
                            found = true;
                            if (currentClazz == clazzPojo ||
                                !isMethodOverride(messageHandler.m, 
                                        otherMessageHandler.m)) {
                                throw new DeploymentException(sm.getString(
                                "pojoMethodMapping.duplicateAnnotation",
                                OnMessage.class, currentClazz));
                            }
                        }
                    }
                    if (!found) {
                        onMessage.add(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);
    }
    private static PojoPathParam[] getPathParams(Method m, 
            MethodType methodType) throws DeploymentException {
        ...
        for (Annotation paramAnnotation : paramAnnotations) {
            if (paramAnnotation.annotationType().equals(
                    PathParam.class)) {
                result[i] = new PojoPathParam(type,
                        ((PathParam) paramAnnotation).value());
                break;
            }
        }
        ...
    }
    private static class MessageHandlerInfo {
        private final Method m;
        private int indexString = -1;
        private int indexByteArray = -1;
        private int indexByteBuffer = -1;
        private int indexPong = -1;
        private int indexBoolean = -1;
        private int indexSession = -1;
        private int indexInputStream = -1;
        private int indexReader = -1;
        private int indexPrimitive = -1;
        private Map<Integer,PojoPathParam> indexPathParams 
                = new HashMap<>();
        private int indexPayload = -1;
        private DecoderMatch decoderMatch = null;
        private long maxMessageSize = -1;
        public MessageHandlerInfo(Method m, 
                List<DecoderEntry> decoderEntries)
                throws DeploymentException {
            // 处理@PathParam注解以及参数的类型
        }
    }
}

可以看到,虽然这个方法流程有点长,但是我们通过PojoMethodMapping类的构造方法便完成了各个注解方法的参数类型、@PathParam路径参数的解析,以便在调用注解方法时直接通过这些参数便能够完成赋值等操作,特别是MessageHandlerInfo类。由于@OnMessage只允许数据类型,即普通Java类型,二进制类型以及Pong类型,因此在MessageHandlerInfo类中还需要记录哪些参数是被允许的,而其在方法中具体又是哪个位置,而让用户使用起来感觉参数随便放置,达到一个类能够处理各种数据类型的要求。这种思想是值得我们学习的,只提供给开发者各种使用方法,但具体如何使用归于开发者所想,最大限度的实现使用灵活性。

3.创建连接及发送消息等原理

3.1 创建连接

如果要理解WebSocket是如何创建的,就离不开Tomcat容器的支持,因此需要从Tomcat分析起。刚刚我们知道了Tomcat的请求大致流程,因此我们可以从Tomcat维持的线程池开始看起:

TaskThread类:

public class TaskThread extends Thread {
    private static final Log log = LogFactory.getLog(TaskThread.class);
    private final long creationTime;
    public TaskThread(ThreadGroup group, Runnable target, String name) {
        super(group, new WrappingRunnable(target), name);
        this.creationTime = System.currentTimeMillis();
    }
    public TaskThread(ThreadGroup group, Runnable target, String name,
            long stackSize) {
        super(group, new WrappingRunnable(target), name, stackSize);
        this.creationTime = System.currentTimeMillis();
    }
    public final long getCreationTime() {
        return creationTime;
    }
    private static class WrappingRunnable implements Runnable {
        private Runnable wrappedRunnable;
        WrappingRunnable(Runnable wrappedRunnable) {
            this.wrappedRunnable = wrappedRunnable;
        }
        @Override
        public void run() {
            try {
                wrappedRunnable.run();
            } catch(StopPooledThreadException exc) {
                log.debug("Thread exiting on purpose", exc);
            }
        }
    }
}

TaskThread是Tomcat的一个线程工具类,用来封装需要运行的子线程,而在Tomcat运行期间wrappedRunnable则是ThreadPoolExecutor的实例对象。

ThreadPoolExecutor类:

public class ThreadPoolExecutor extends AbstractExecutorService {
    private final class Worker extends AbstractQueuedSynchronizer
            implements Runnable {
        public void run() {
            runWorker(this);
        }
    }
    final void runWorker(Worker w) {
        ...
        try {
            task.run();
        }
        ...
    }
}

当有新的请求进来时都会把请求封装有套接字的线程放进线程池运行,在该类中task的具体类型是NioEndpoint中的内部类SocketProcessor。

SocketProcessor的父类SocketProcessorBase<T>类:

public abstract class SocketProcessorBase<S> implements Runnable {
    protected SocketWrapperBase<S> socketWrapper;
    protected SocketEvent event;
    public SocketProcessorBase(SocketWrapperBase<S> socketWrapper, 
            SocketEvent event) {
        reset(socketWrapper, event);
    }
    public void reset(SocketWrapperBase<S> socketWrapper, 
            SocketEvent event) {
        Objects.requireNonNull(event);
        this.socketWrapper = socketWrapper;
        this.event = event;
    }
    @Override
    public final void run() {
        synchronized (socketWrapper) {
            if (socketWrapper.isClosed()) {
                return;
            }
            doRun();
        }
    }
    protected abstract void doRun();
}

SocketProcessor实现内部类:

public class NioEndpoint 
        extends AbstractJsseEndpoint<NioChannel,SocketChannel> {
    protected class SocketProcessor 
            extends SocketProcessorBase<NioChannel> {
        @Override
        protected void doRun() {
            try {
                int handshake = -1;
                ...
                if (socket.isHandshakeComplete()) {
                    handshake = 0;
                }
                ...
                if (handshake == 0) {
                    SocketState state = SocketState.OPEN;
                    if (event == null) {
                        ...
                    } else {
                        state = getHandler().process(socketWrapper, event);
                    }
                }
            }
        }
    }
}

getHandler()方法的类型是AbstractProtocol类中的ConnectionHandler。

接下来才是真正的重点,根据传过来的参数来判断具体是什么协议,从而进行相应的操作。我们直接看到内部类ConnectionHandler源码部分:

protected static class ConnectionHandler<S> 
        implements AbstractEndpoint.Handler<S> {
    @Override
    public SocketState process(SocketWrapperBase<S> wrapper, 
            SocketEvent status) {
        ...
        S socket = wrapper.getSocket();
        Processor processor = connections.get(socket);
        ...
        try {
            ...
            state = processor.process(wrapper, status);
            if (state == SocketState.UPGRADING) {
                if (upgradeToken == null) {
                    ...
                } else {
                    if (upgradeToken.getInstanceManager() == null) {
                        httpUpgradeHandler
                                .init((WebConnection) processor);
                    }
                    ...
                }
            }
            ...
        }
        ...
    }
}

在该方法中,会根据前端传过来的协议参数来判断具体由哪个processor和handler来处理这次请求,而httpUpgradeHandler的类型则是WsHttpUpgradeHandler类。

WsHttpUpgradeHandler类源码:

public class WsHttpUpgradeHandler implements InternalHttpUpgradeHandler {
    private final Log log = LogFactory.getLog(WsHttpUpgradeHandler.class);
    private static final StringManager sm = 
            StringManager.getManager(WsHttpUpgradeHandler.class);
    private final ClassLoader applicationClassLoader;
    private SocketWrapperBase<?> socketWrapper;
    
    private Endpoint ep;
    private ServerEndpointConfig serverEndpointConfig;
    private WsServerContainer webSocketContainer;
    private WsHandshakeRequest handshakeRequest;
    private List<Extension> negotiatedExtensions;
    private String subProtocol;
    private Transformation transformation;
    private Map<String,String> pathParameters;
    private boolean secure;
    private WebConnection connection;
    
    private WsRemoteEndpointImplServer wsRemoteEndpointServer;
    private WsFrameServer wsFrame;
    private WsSession wsSession;
    @Override
    public void init(WebConnection connection) {
        ...
        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, serverEndpointConfig);
            wsFrame = new WsFrameServer(socketWrapper, wsSession, 
                    transformation, applicationClassLoader);
            wsRemoteEndpointServer.setTransformation(
                    wsFrame.getTransformation());
            ep.onOpen(wsSession, serverEndpointConfig);
            webSocketContainer.registerSession(serverEndpointConfig
                    .getPath(), wsSession);
        }
        ...
    }
}

在这个类中,我们终于看到了老朋友ServerEndpointConfig和ServerEndpointConfig两个类,而在实例中的Session其具体类型也就是这个类里面的WsSession,因此需要知道顶层的那些参数怎么来的只需要来这个类中一探究竟便可。

在WsHttpUpgradeHandler类中ep的具体类型是PojoEndpointServer,该类源码及其父类源码如下:

public abstract class PojoEndpointBase extends Endpoint {
    protected final void doOnOpen(Session session, EndpointConfig config) {
        PojoMethodMapping methodMapping = getMethodMapping();
        Object pojo = getPojo();
        Map<String,String> pathParameters = getPathParameters();
        
        for (MessageHandler mh : methodMapping.getMessageHandlers(pojo,
                pathParameters, session, config)) {
            session.addMessageHandler(mh);
        }
        if (methodMapping.getOnOpen() != null) {
            try {
                methodMapping.getOnOpen().invoke(pojo,
                    methodMapping.getOnOpenArgs(
                            pathParameters, session, config));
            }
            ...
        }
    }
}
public class PojoEndpointServer extends PojoEndpointBase {
    @Override
    public void onOpen(Session session, EndpointConfig endpointConfig) {
        ServerEndpointConfig sec = (ServerEndpointConfig) endpointConfig;
        Object pojo;
        try {
            pojo = sec.getConfigurator().getEndpointInstance(
                    sec.getEndpointClass());
        } catch (InstantiationException e) {
            throw new IllegalArgumentException(sm.getString(
                    "pojoEndpointServer.getPojoInstanceFail",
                    sec.getEndpointClass().getName()), e);
        }
        setPojo(pojo);
        
        @SuppressWarnings("unchecked")
        Map<String,String> pathParameters =
                (Map<String, String>) sec.getUserProperties().get(
                        Constants.POJO_PATH_PARAM_KEY);
        setPathParameters(pathParameters);
        
        PojoMethodMapping methodMapping =
                (PojoMethodMapping) sec.getUserProperties().get(
                        Constants.POJO_METHOD_MAPPING_KEY);
        setMethodMapping(methodMapping);
        
        doOnOpen(session, endpointConfig);
    }
    
}

在该方法中会获得被@ServerEndpoint注解的类,以及类中@PathParam注解的参数等,随后调用doOnOpen方法,去调用被@OnOpen注解的方法,进行打开链接的相应操作。

3.2 发送消息

发送消息前面的流程便不用多说,差不多都是一样的,直到在AbstractProtocol的内部类ConnectionHandler中,在该类中Socket由于已经连接,因此获得的processor变量的类型则是UpgradeProcessorInternal类,因此我们先看到该类和父类的源码:

public abstract class AbstractProcessorLight implements Processor {
    @Override
    public SocketState process(SocketWrapperBase<?> socketWrapper, 
            SocketEvent status) throws IOException {
        ...
        if (dispatches != null) {
            ...
        }  else if (isAsync() || isUpgrade() || 
                state == SocketState.ASYNC_END) {
            state = dispatch(status);
            ...
        }
        ...
    }
}
public class UpgradeProcessorInternal extends UpgradeProcessorBase {
    private final InternalHttpUpgradeHandler internalHttpUpgradeHandler;
    @Override
    public SocketState dispatch(SocketEvent status) {
        return internalHttpUpgradeHandler.upgradeDispatch(status);
    }
}

UpgradeProcessorBase继承了AbstractProcessorLight类,变量internalHttpUpgradeHandler的具体类型为WsHttpUpgradeHandler,在分析创建连接过程中已经贴过其成员变量以及初始化方法init源码,现展示upgradeDispatch相关源码:

public class WsHttpUpgradeHandler implements InternalHttpUpgradeHandler {
    @Override
    public SocketState upgradeDispatch(SocketEvent status) {
        switch (status) {
            case OPEN_READ:
                try {
                    return wsFrame.notifyDataAvailable();
                } catch (WsIOException ws) {
                    close(ws.getCloseReason());
                } catch (IOException ioe) {
                    onError(ioe);
                    CloseReason cr = new CloseReason(
                            CloseCodes.CLOSED_ABNORMALLY, ioe.getMessage());
                    close(cr);
                }
                return SocketState.CLOSED;
            case OPEN_WRITE:
                wsRemoteEndpointServer.onWritePossible(false);
                break;
            case STOP:
                CloseReason cr = new CloseReason(CloseCodes.GOING_AWAY,
                        sm.getString("wsHttpUpgradeHandler.serverStop"));
                try {
                    wsSession.close(cr);
                } catch (IOException ioe) {
                    onError(ioe);
                    cr = new CloseReason(
                            CloseCodes.CLOSED_ABNORMALLY, ioe.getMessage());
                    close(cr);
                    return SocketState.CLOSED;
                }
                break;
            case ERROR:
                String msg = 
                        sm.getString("wsHttpUpgradeHandler.closeOnError");
                wsSession.doClose(
                        new CloseReason(CloseCodes.GOING_AWAY, msg),
                        new CloseReason(CloseCodes.CLOSED_ABNORMALLY, msg));
                //$FALL-THROUGH$
            case DISCONNECT:
            case TIMEOUT:
                return SocketState.CLOSED;
        
        }
        if (wsFrame.isOpen()) {
            return SocketState.UPGRADED;
        } else {
            return SocketState.CLOSED;
        }
    }
}

可以看到这个方法一看到就有种熟悉感,看到这如果稍加思考便能够知道这个类可以说是处理WebSocket的初始接口入口类,初始化是在这个类中完成的,同时不同类型的方法转发和状态判断也是在这里完成,因此把这个类说成WebSocket的入口类也不过分。

发送消息时status的状态是OPEN_READ,wsFrame对象的类型是WsFrameServer,因此我们看到该类的notifyDataAvailable方法以及父类相关源码:

public class WsFrameServer extends WsFrameBase {
    SocketState notifyDataAvailable() throws IOException {
        ...
        try {
            return doOnDataAvailable();
        }
        ...
    }
    private SocketState doOnDataAvailable() throws IOException {
        onDataAvailable();
        ...
    }
    private void onDataAvailable() throws IOException {
        ...
        while (isOpen() && !isSuspended()) {
            ...
            processInputBuffer();
        }
        ...
    }
    protected void processInputBuffer() throws IOException {
        ...
        if (state == State.DATA) {
            if (!processData()) {
                break;
            }
        }
    }
    @Override
    protected void sendMessageText(boolean last) throws WsIOException {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        try {
            ...
            super.sendMessageText(last);
        } 
        ...
    }
}
public abstract class WsFrameBase {
    private boolean processData() throws IOException {
        ...
        if (Util.isControl(opCode)) {
            ...
        } else if (textMessage) {
            if (textMsgHandler == null) {
                result = swallowInput();
            } else {
                result = processDataText();
            }
        }
        ...
    }
    private boolean processDataText() throws IOException {
        ...
        while (true) {
            ...
            if (...)){
                ...
            } else {
                messageBufferText.flip();
                sendMessageText(true);
                newMessage();
                return true;
            }
        }
    }
    protected void sendMessageText(boolean last) throws WsIOException {
        ...
        try {
            if (textMsgHandler instanceof MessageHandler.Partial<?>) {
                ...
            } else {
                ((MessageHandler.Whole<String>) textMsgHandler)
                        .onMessage(messageBufferText.toString());
            }
        }
        ...
    }
}

根据其源码可以大致看到在该类中会处理各种类型、状态的WebSocket连接,对于普通的文本类型运行调用链简单来看便是上述方法,同时WebSocket也支持一个消息分成多个部分发送的情况,其就对应MessageHandler.Partial类型。而一次性处理消息textMsgHandler类型则为PojoMessageHandlerWholeBase类型,其源码如下:

public abstract class PojoMessageHandlerWholeBase<T>
        extends PojoMessageHandlerBase<T> 
        implements MessageHandler.Whole<T> {
    @Override
    public final void onMessage(T message) {
        if (params.length == 1 && params[0] instanceof DecodeException) {
            ((WsSession) session).getLocal().onError(session,
                    (DecodeException) params[0]);
            return;
        }

        // Can this message be decoded?
        Object payload;
        try {
            payload = decode(message);
        } catch (DecodeException de) {
            ((WsSession) session).getLocal().onError(session, de);
            return;
        }

        if (payload == null) {
            // Not decoded. Convert if required.
            if (convert) {
                payload = convert(message);
            } else {
                payload = message;
            }
        }

        Object[] parameters = params.clone();
        if (indexSession != -1) {
            parameters[indexSession] = session;
        }
        parameters[indexPayload] = payload;

        Object result = null;
        try {
            result = method.invoke(pojo, parameters);
        } catch (IllegalAccessException | InvocationTargetException e) {
            handlePojoMethodException(e);
        }
        processResult(result);
    }
}

对于params、session以及indexPayload这些参数怎么来的稍后分析@OnMessage这些注解时再说,该方法的主要流程便是组装@OnMessage注解方法需要的参数,再将这些参数通过映射传到具体的实现方法中,完成消息的传送。至此,WebSocket调用到@OnMessage注解方法中流程便结束了。

3.3 关闭和抛异常

对于关闭和抛异常如果看过源码,稍微扩展一下看看便能够知道这两个操作和发送消息大体流程是差不多的,在WsHttpUpgradeHandler类的upgradeDispatch方法中便完成了各种类型的转发,其处理都是通过直接调用WsSession方法完成调用的,因此我们可以看到Session接口中有close方法。其中OnError方法实际上是OnClose的另一个分支而已,因为结果上这两种操作都要关闭连接。这两个流程便不做过多分析。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值