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

时间:2017-09-02 02:10:34

标签: java java-websocket javax

如何在我的服务器上发生内部错误时正确关闭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是我自己的静态实用工具类。 HTTP.getRequiredLongParameter()使用

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

并做进一步处理。

2 个答案:

答案 0 :(得分:1)

我相信我应该放置......

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

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

客户收到:

onClose(1003, some text)

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

答案 1 :(得分:1)

为了开发我提到的要点,关于“如何处理所需参数”的问题,我可以看到以下选项。首先,让我们考虑一下端点:

3

过滤

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

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

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

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-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> by BalusC in my question以支持异步消息发送。

<强> TL,DR

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

配置

您可能已经注意到,我添加了async-supported。这样的课程看起来像:

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

<强> TL; DR

您想要操纵一些参数但是以websocket方式处理可能的错误?创建自己的配置。

尝试/捕获

我还提到了try / catch选项:

@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...
}

希望这个帮助