如何通过 WebSockets 正确地向客户端报告错误

How to properly report an error to client through WebSockets

当我的服务器发生内部错误时,如何正确关闭 websocket 并向客户端提供干净、信息丰富的响应?在我目前的情况下,客户端在连接时必须提供一个参数,而我正在尝试处理 OnOpen 收到的不正确或丢失的参数。

This example 建议我可以在 OnOpen 中抛出一个异常,它最终会调用 OnError,我可以在其中关闭并提供原因和消息。它有点工作,但客户端只收到 EOF,1006,CLOSE_ABNORMAL。

另外,因为我没有找到其他讨论,所以我无法判断什么是最佳实践。

我正在使用 JSR-356 规范,如下所示:

@ClientEndpoint
@ServerEndpoint(value="/ws/events/")
public class WebSocketEvents
{
    private javax.websocket.Session session;
    private long token;

    @OnOpen
    public void onWebSocketConnect(javax.websocket.Session session) throws BadRequestException
    {
        logger.info("WebSocket connection attempt: " + session);
        this.session = session;
        // this throws BadRequestException if null or invalid long
        // with short detail message, e.g., "Missing parameter: token"
        token = HTTP.getRequiredLongParameter(session, "token");
    }

    @OnMessage
    public void onWebSocketText(String message)
    {
        logger.info("Received text message: " + message);
    }

    @OnClose
    public void onWebSocketClose(CloseReason reason)
    {
        logger.info("WebSocket Closed: " + reason);
    }

    @OnError
    public void onWebSocketError(Throwable t)
    {
        logger.info("WebSocket Error: ");

        logger.debug(t, t);
        if (!session.isOpen())
        {
            logger.info("Throwable in closed websocket:" + t, t);
            return;
        }

        CloseCode reason = t instanceof BadRequestException ? CloseReason.CloseCodes.PROTOCOL_ERROR : CloseReason.CloseCodes.UNEXPECTED_CONDITION;
        try
        {
            session.close(new CloseReason(reason, t.getMessage()));
        }
        catch (IOException e)
        {
            logger.warn(e, e);
        }

    }
}

编辑: 每个链接示例抛出的异常看起来很奇怪,所以现在我在 OnOpen 中捕获异常并立即执行

session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "some text")); 

编辑:结果证明这是正确的,尽管有一个单独的错误掩盖了一段时间。


Edit2:澄清:HTTP 是我自己的静态实用程序 class。 HTTP.getRequiredLongParameter()使用

从客户端初始请求获取查询参数
session.getRequestParameterMap().get(name)

并做进一步处理。

我相信我应该放...

session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "some text"));

...发生错误的地方,在@OnOpen() 中。 (对于一般错误,使用 CloseCodes.UNEXPECTED_CONDITION。)

客户端收到:

onClose(1003, some text)

这当然是显而易见的答案。我想我被引用的例子误导了,从 @OnOpen() 中抛出异常。正如 Remy Lebeau 所建议的那样,套接字可能因此而关闭,阻止我在 @OnError() 中进行任何进一步处理。 (其他一些错误可能掩盖了所讨论的证据。)

为了发展我提到的要点,你关于"how to handle a required parameter"的问题,我可以看到以下选项。首先,让我们考虑端点:

@ServerEndpoint(value = "/websocket/myendpoint", 
                configuration = MyWebsocketConfiguration.class)
public class MyEndpoint{
    // @OnOpen, @OnClose, @OnMessage, @OnError...
}

过滤

客户端和服务器之间的第一次联系是 HTTP 请求。您可以使用过滤器对其进行过滤,以防止发生 websocket 握手。过滤器可以阻止请求或让它通过:

import javax.servlet.Filter;

public class MyEndpointFilter implements Filter{
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // nothing for this example
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        // if the connection URL is /websocket/myendpoint?parameter=value
        // feel free to dig in what you can get from ServletRequest
        String myToken = request.getParameter("token");

        // if the parameter is mandatory
        if (myToken == null){
            // you can return an HTTP error code like:
            ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // if the parameter must match an expected value
        if (!isValid(myToken)){
            // process the error like above, you can
            // use the 403 HTTP status code for instance
            return;
        }

        // this part is very important: the filter allows
        // the request to keep going: all green and good to go!
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        //nothing for this example
    }

    private boolean isValid(String token){
         // how your token is checked? put it here
    }
}

如果您使用过滤器,则必须将其添加到 web.xml:

<web-app ...>

    <!-- you declare the filter here -->
    <filter>
        <filter-name>myWebsocketFilter</filter-name>
        <filter-class>com.mypackage.MyEndpointFilter </filter-class>
        <async-supported>true</async-supported>
    </filter>
    <!-- then you map your filter to an url pattern. In websocket
         case, it must match the serverendpoint value -->
    <filter-mapping>
        <filter-name>myWebsocketFilter</filter-name>
        <url-pattern>/websocket/myendpoint</url-pattern>
    </filter-mapping>

</web-app>

建议 async-supported 支持异步消息发送。

TL,DR

如果您需要在连接时操作客户端提供的 GET 参数,如果您对纯 HTTP 应答(403 状态码等)感到满意,Filter 可以作为一种解决方案

配置器

您可能已经注意到,我添加了 configuration = MyWebsocketConfiguration.class。这样 class 看起来像:

public class MyWebsocketConfigurationextends ServerEndpointConfig.Configurator {

    // as the name suggests, we operate here at the handshake level
    // so we can start talking in websocket vocabulary
    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {

        // much like ServletRequest, the HandshakeRequest contains
        // all the information provided by the client at connection time
        // a common usage is:
        Map<String, List<String>> parameters = request.getParameterMap();

        // this is not a Map<String, String> to handle situation like
        // URL = /websocket/myendpoint?token=value1&token=value2
        // then the key "token" is bound to the list {"value1", "value2"}
        sec.getUserProperties().put("myFetchedToken", parameters.get("token"));
    }
}

好的,太好了,这与过滤器有何不同?最大的区别是您在握手期间在用户属性中添加了一些信息。这意味着 @OnOpen 可以访问此信息:

@ServerEndpoint(value = "/websocket/myendpoint", 
                configuration = MyWebsocketConfiguration.class)
public class MyEndpoint{

     // you can fetch the information added during the
     // handshake via the EndpointConfig
     @OnOpen
     public void onOpen(Session session, EndpointConfig config){
         List<String> token = (List<String>) config.getUserProperties().get("myFetchedToken");

         // now you can manipulate the token:
         if(token.isEmpty()){
             // for example: 
             session.close(new CloseReasons(CloseReason.CloseCodes.CANNOT_ACCEPT, "the token is mandatory!");
         }
     }

    // @OnClose, @OnMessage, @OnError...
}

TL;DR

您想对一些参数进行操作,但以websocket方式处理可能出现的错误?创建您自己的配置。

Try/catch

我还提到了 try/catch 选项:

@ServerEndpoint(value = "/websocket/myendpoint")
public class MyEndpoint{

     @OnOpen
     public void onOpen(Session session, EndpointConfig config){

         // by catching the exception and handling yourself
         // here, the @OnError will never be called. 
         try{
             Long token = HTTP.getRequiredLongParameter(session, "token");
             // process your token
         }
         catch(BadRequestException e){
             // as you suggested:
             session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "some text"));
         }
     }

    // @OnClose, @OnMessage, @OnError...
}

希望对您有所帮助