tomcat websocket 并发问题解决(四)

又产生的问题

自从上次做过优化之后,貌似程序跑的还行,但是,最近发现日志中有报这样的错:

java.lang.IllegalStateException: The remote endpoint was in state [TEXT_PARTIAL_WRITING] which is an invalid state for called method
        at org.apache.tomcat.websocket.WsRemoteEndpointImplBase$StateMachine.checkState(WsRemoteEndpointImplBase.java:1224)
        at org.apache.tomcat.websocket.WsRemoteEndpointImplBase$StateMachine.textPartialStart(WsRemoteEndpointImplBase.java:1182)
        at org.apache.tomcat.websocket.WsRemoteEndpointImplBase.sendPartialString(WsRemoteEndpointImplBase.java:222)
        at org.apache.tomcat.websocket.WsRemoteEndpointBasic.sendText(WsRemoteEndpointBasic.java:49)
        at org.springframework.web.socket.adapter.standard.StandardWebSocketSession.sendTextMessage(StandardWebSocketSession.java:203)
        at org.springframework.web.socket.adapter.AbstractWebSocketSession.sendMessage(AbstractWebSocketSession.java:101)

这是为啥呢?不是同一个 session 都已经做同步处理了么?

仔细看这里,跟之前的报错不一样,这回的 state 是 TEXT_PARTIAL_WRITING,那这个状态是怎么来的呢?为啥会报错呢?我们根据异常栈信息去看代码。


	@Override
	protected void sendTextMessage(TextMessage message) throws IOException {
		getNativeSession().getBasicRemote().sendText(message.getPayload(), message.isLast());
	}
	
   @Override
    public void sendText(String fragment, boolean isLast) throws IOException {
        base.sendPartialString(fragment, isLast);
    }
    
    public void sendPartialString(String fragment, boolean isLast)
            throws IOException {
        if (fragment == null) {
            throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullData"));
        }
        stateMachine.textPartialStart();
        sendMessageBlock(CharBuffer.wrap(fragment), isLast);
    }
    
    public synchronized void textPartialStart() {
        checkState(State.OPEN, State.TEXT_PARTIAL_READY);
        state = State.TEXT_PARTIAL_WRITING;
    }
    
    void sendMessageBlock(CharBuffer part, boolean last) throws IOException {
        long timeoutExpiry = getTimeoutExpiry();
        boolean isDone = false;
        while (!isDone) {
            encoderBuffer.clear();
            CoderResult cr = encoder.encode(part, encoderBuffer, true);
            if (cr.isError()) {
                throw new IllegalArgumentException(cr.toString());
            }
            isDone = !cr.isOverflow();
            encoderBuffer.flip();
            sendMessageBlock(Constants.OPCODE_TEXT, encoderBuffer, last && isDone, timeoutExpiry);
        }
        stateMachine.complete(last);
    }
    

原来,StandardWebSocketSession.sendTextMessage 调用的是 sendPartialString 方法,这个方法在发送前检查 state 是否为 OPEN 或 TEXT_PARTIAL_READY ,检查通过后将 state 设为 TEXT_PARTIAL_WRITING ,发送完毕后再通过 stateMachine.complete 方法将 state 复位为 TEXT_PARTIAL_READY 或 OPEN (取决于消息是否已发送完,即参数 last = true|false)

理论上来说,同一个 session 的消息都在同一条线程(JobHandler)内发送,state 一定是按照 stateMachine 设定状态变换的,不会出现问题。 但是假定一种情况,即在最后一步 sendMessageBlock 方法内,此时连接因为网络异常关闭了,发送方法抛出了异常,那么复位的代码 stateMachine.complete(last) 就得不到执行,state 就维持在 TEXT_PARTIAL_WRITING 不变了,但是此时队列里还有此 session 未发送完的消息,那么后边的任务一执行到 checkState(State.OPEN, State.TEXT_PARTIAL_READY) 就会抛出上述的异常。

解决办法

这个问题倒是不大,连接既然已经关闭掉了,发送消息失败就失败吧。不过第一有错误日志会逼死强迫症,第二会造成一些资源浪费。

能想到的解决办法是在上层接到这个异常,打 trace/debug 日志然后做忽略处理即可,然后程序内长期持有 session 的地方使用 WeakReference 管理。不过,在 session 失效到被回收之间的这个空档,还是难免会造成浪费。

这样看来的话,其实最好的还是办法借用 Spring 提供的 ConcurrentWebsocketSessionDecorator 把 session 包一层,这样由于消息队列是由 session 持有的,一旦抛出异常,session 失效,剩下未发送的消息也一同失效了,避免了内存和 CPU 资源的浪费。大厂到底是大厂。。。不要再和我说什么叉腰了!

补充 (2018年03月06日)

今天又想到一些要补充的内容,其实我的处理方法对比 ConcurrentWebsocketSessionDecorator 也不算完败。

我的实现方法消息的发送是异步完成的,处理用户请求的线程不会阻塞,丢到队列后就立即返回了,因为发送消息实际是由 JobHandler 线程完成的。 ConcurrentWebsocketSessionDecorator 的发送消息方法是会阻塞线程的,在处理用户请求的 http 线程内进行的。

考虑一种极端情况,某一条用户线程 A 拿到锁之后,发送消息,然后另一条线程 B 丢消息进 buffer ,尝试拿锁,拿不到返回了。A 发完消息,发现 buffer 不为空,拿出来继续发,然后这当有一条线程 C 进来,丢消息进 buffer,拿不到锁返回,A 回来发现 buffer 里又有东西了。。。如此往复,线程 A 就成了一条专发消息的线程了,但是这是处理用户请求的线程啊,下边可能还有事情呢。。。全给耽误了不是。。。

当然这是极端情况,只是为了说明我的实现方法可以避免这种情况,具体采用呢,老话说了,么有银弹,还是得看场景。

猜你喜欢

转载自my.oschina.net/HY1024/blog/1630002