Tyrus websocket:IllegalStateException 无法为非异步请求设置 WriteListener

Tyrus websocket: IllegalStateException cannot set WriteListener for non-async request

我有一个基于 Tyrus 实现的标准 websocket 端点,它有时会触发 java.lang.IllegalStateException: Cannot set WriteListener for non-async or non-upgrade request。我们在 Payara 4.1 运行。

我的标准实现

@ServerEndpoint(value = "...", decoders=MessageDecoder.class, encoders=MessageEncoder.class)
public class EndpointImpl extends AbstractEndpoint{
    // onOpen, onClose, onMessage, onError methods
}

其中摘要class是

public abstract class AbstractEndpoint{

    // irrelevant onOpen, onOpen handling method

117        protected void sendMessage(Session session, Message message){
118            if(message == null){
119                LOGGER.error("null message");
120            } else if(!session.isOpen()){
121                LOGGER.error("session is not opened");
122            } else{
>>>123                session.getAsyncRemote().sendObject(message, (result) -> {
124                    if (result.isOK()) {
125                        LOGGER.info("success! yeah!");
126                    } else {
127                        LOGGER.error("error when sending message", result.getException());
128                    }
129                });
130            }
    } 
}

IllegalStateException

到目前为止,没有什么特别的。我可以完美地交流和响应我收到的请求,而且,websocket FTW,我可以推送信息并取回反馈。但是,我时常收到异常:

java.lang.IllegalStateException: Cannot set WriteListener for non-async or non-upgrade request
        at org.apache.catalina.connector.OutputBuffer.setWriteListener(OutputBuffer.java:536)
        at org.apache.catalina.connector.CoyoteOutputStream.setWriteListener(CoyoteOutputStream.java:223)
        at org.glassfish.tyrus.servlet.TyrusServletWriter.write(TyrusServletWriter.java:140)
        at org.glassfish.tyrus.core.ProtocolHandler.write(ProtocolHandler.java:486)
        at org.glassfish.tyrus.core.ProtocolHandler.send(ProtocolHandler.java:274)
        at org.glassfish.tyrus.core.ProtocolHandler.send(ProtocolHandler.java:332)
        at org.glassfish.tyrus.core.TyrusWebSocket.sendText(TyrusWebSocket.java:317)
        at org.glassfish.tyrus.core.TyrusRemoteEndpoint.sendSyncObject(TyrusRemoteEndpoint.java:429)
        at org.glassfish.tyrus.core.TyrusRemoteEndpoint$Async.sendAsync(TyrusRemoteEndpoint.java:352)
        at org.glassfish.tyrus.core.TyrusRemoteEndpoint$Async.sendObject(TyrusRemoteEndpoint.java:249)
        at com.mycompany.websocket.AbstEndpoint.sendMessage(AbstEndpoint.java:123)

第二次 sendMessage 方法尝试

起初,我以为我的异步端点配置错误所以我尝试了 Future<> 方式而不是回调方式:

RemoteEndpoint.Async async = session.getAsyncRemote();
async.setSendTimeout(5000); // 5 seconds
Future<Void> future = async.sendObject(message);
try{
    future.get();
}
catch(InterruptedException | ExecutionException ex){
    LOGGER.error("error when sending message", ex);
}

我也遇到了异常

到目前为止和症状

奇怪的是,我只发现one link在谈论这个问题。

  1. github link 突出了缓冲区大小问题。我不使用部分消息,只使用整个消息。此外,无论我使用默认缓冲区大小还是设置新缓冲区大小,都会出现异常
  2. 我找不到关于如何重现错误的全局规则
  3. 异常抛出后,客户端可以一直发送消息,服务端会处理,但是服务端一直没有回复客户端。传出通信通道似乎已被阻止
  4. 由于服务器一直在处理传入的消息,异常后websocket通道没有关闭

深入挖掘 Tyrus 实施

我浏览了 tyrus-core 实现,发现发送方法依赖于某个 Grizzly 组件。我对 Grizzly 一无所知,但由于某些 Grizzly 限制,发送似乎必须是同步的

问题

  1. 有人遇到过这种情况吗?如果是,异常是否真的意味着某处存在瓶颈还是意味着其他原因?
  2. tyrus异步端点真的是异步的吗,即像"process-and-forget"?
  3. 我还没有找到任何方法让消息和传出消息排队:如果消息 A 很长,请等待消息 A 发送完成,然后再发送消息 B。有没有办法在 websocket 或异步端点是唯一的方法吗?
  4. 我想确保发送没有遇到任何问题,因此我选择了异步解决方案。我应该回到同步方式吗?

我没有详细说明我对 Tyrus 的调查。如果您觉得相关,请随时提出,我很乐意开发。

java.lang.IllegalStateException: Cannot set WriteListener for non-async or non-upgrade request

为了使请求完全异步,请求-响应链中的 any Filter 必须明确设置为支持异步请求。特别是映射到 /*.

的那些 "catch-all" 过滤器

如果过滤器是通过 web.xml 中的 <filter> 条目注册的,这可以通过将子元素 <async-supported> 设置为 true 来完成。

<filter>
    ...
    <async-supported>true</async-supported>
</filter>

如果过滤器是通过 @WebFilter 注释注册的,这可以通过将其 asyncSupported 属性设置为 true.

来完成
@WebFilter(..., asyncSupported="true")

如果过滤器是通过 ServletContext#addFilter(), this can be done by setting Registration.Dynamic#setAsyncSupported(). 注册到 true

Dynamic filter = servletContext.addFilter(name, type);
filter.setAsyncSupported(true);

原因是,WebSocket 实现内部使用 ServletRequest#startAsync() during the handshake request in order to keep the request-response pipeline "forever" open until the response is explicitly closed. Its javadoc 表示如下:

Throws
IllegalStateException - if this request is within the scope of a filter or servlet that does not support asynchronous operations (that is, isAsyncSupported() returns false), or if this method is called again without any asynchronous dispatch (resulting from one of the AsyncContext.dispatch() methods), is called outside the scope of any such dispatch, or is called again within the scope of the same dispatch, or if the response has already been closed

isAsyncSupported() 默认为 false 是为了不破坏现有的 web 应用程序,这些应用程序使用实施不当的 servlet 过滤器。从技术上讲,仅将目标 Servlet 标记为支持异步并保留过滤器就足够了。一个理智的 "catch-all" Filter 不会明确地向 HTTP 响应写入任何内容,但 Servlet API 从未禁止这样做,因此不幸的是,这样的过滤器可能存在。

如果您有一个这样的过滤器,那么您应该将其修复为不再向响应写入任何内容,以便您可以安全地将其标记为支持异步请求,或者将其 URL 模式调整为不包括 WebSocket 请求。 IE。不要再将其映射到 /*