又产生的问题
自从上次做过优化之后,貌似程序跑的还行,但是,最近发现日志中有报这样的错:
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 就成了一条专发消息的线程了,但是这是处理用户请求的线程啊,下边可能还有事情呢。。。全给耽误了不是。。。
当然这是极端情况,只是为了说明我的实现方法可以避免这种情况,具体采用呢,老话说了,么有银弹,还是得看场景。