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

7
如何正确关闭WebSocket并在服务器发生内部错误时向客户端提供干净、清晰的响应?在当前情况下,客户端连接时必须提供参数,我试图处理OnOpen接收到的不正确或缺少的参数。这个例子建议我可以在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")); 

编辑:虽然有一个单独的错误掩盖了它,但这最终被证明是正确的。


编辑2:澄清一下:HTTP是我自己的静态公用类。通过使用HTTP.getRequiredLongParameter()从客户端的初始请求获取查询参数。

session.getRequestParameterMap().get(name)

并进行进一步的处理。


OnOpen 中抛出异常可能会在调用 OnError 之前关闭会话,您是否看到记录了 "Throwable in closed websocket" - Remy Lebeau
@Remy 不,"throwable in closed..." 没有被记录。 - Saturn5
编辑了新信息。从功能上讲,它现在是可以接受的。但我真正寻求的是“最佳实践”答案,这对我的服务器客户有帮助,并且可能与我正在做的完全不同。 - Saturn5
你有没有考虑在前面加一个过滤器?不相关的问题,但是为什么你的类同时用了 ServerEndpointClientEndpoint 注解? 如果你不想使用过滤器,你也可以考虑自定义配置,在那里你将检查参数是否存在以及它的取值。 - Al-un
@ASE 我对过滤器知之甚少。不过我认为我有一个不错的答案(见答案)。在我发现的第一个例子中,ClientEndpointServerEndpoint被合并了。感谢您指出,已经将其删除。 - Saturn5
显示剩余2条评论
2个回答

2

为了展开我之前提到的点,针对你关于“如何处理必需参数”的问题,我可以提供以下几个选项。首先,让我们考虑终端点:

@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”建议由BalusC在我的问题中提出,以支持异步消息发送。

总结

如果您需要在连接时操作客户端提供的GET参数,则Filter可以是一种解决方案,如果您满意于纯HTTP响应(403状态代码等),则可以采用该解决方案。

配置器

正如您可能已经注意到的那样,我已添加了configuration = MyWebsocketConfiguration.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...
}

希望这可以帮助到您。

我不太了解SO的礼仪,但我猜你应该将编辑内容附加到你的帖子中。顺便说一下,你的编辑包含非常重要的信息 :) - Al-un

1

我相信我应该已经放置了...

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

当错误发生时,需要在@OnOpen()内部进行处理。(对于通用错误,请使用CloseCodes.UNEXPECTED_CONDITION。)

客户端接收到:

onClose(1003, some text)

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


当出现异常时,将触发onError。正如Remy Lebeau所建议的那样,在任何地方抛出异常,特别是在@OnOpen期间,都会调用@OnError。如果您希望在@OnOpen期间关闭连接而不抛出异常,则最好的方法是在try/catch中捕获异常并关闭连接。 - Al-un

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接