Как правильно сообщить об ошибке клиенту через WebSockets

Как правильно закрыть веб-сокет и предоставить клиенту чистый, информативный ответ, когда на моем сервере возникает внутренняя ошибка? В моем текущем случае клиент должен предоставить параметр при подключении, и я пытаюсь обработать неправильные или отсутствующие параметры, полученные 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")); 

Редактировать: Это оказалось правильным, хотя отдельная ошибка на некоторое время замаскировала это.


Edit2: Пояснение: HTTP — это мой собственный статический служебный класс. HTTP.getRequiredLongParameter() получает параметры запроса из первоначального запроса клиента, используя

session.getRequestParameterMap().get(name)

и выполняет дальнейшую обработку.


person Saturn5    schedule 02.09.2017    source источник
comment
Возможно ли, что исключение в OnOpen закрывает сеанс до вызова OnError? Вы видите, что "Throwable in closed websocket" регистрируется?   -  person Remy Lebeau    schedule 02.09.2017
comment
@Remy Нет, бросаемый в закрытом... не регистрируется.   -  person Saturn5    schedule 02.09.2017
comment
Отредактировано с новой информацией. Он функционально приемлем как есть. Но то, что я действительно ищу, - это лучший практический ответ, который полезен для клиентов моего сервера и может не иметь ничего общего с тем, что я делаю.   -  person Saturn5    schedule 02.09.2017
comment
Вы не думали о том, чтобы поставить фильтр впереди? Несвязанный вопрос, но почему ваш класс аннотирован ServerEndpoint и ClientEndpoint одновременно? Если вы не хотите использовать фильтр, вы также можете рассмотреть пользовательскую конфигурацию, в которой вы проверите, есть ли параметр здесь или нет, и какое значение он имеет.   -  person Al-un    schedule 09.09.2017
comment
@ASE Я мало знаю о фильтрах. Однако я думаю, что у меня есть хороший ответ (см. ответ). ClientEndpoint было объединено с ServerEndpoint в первом примере, который я нашел. Спасибо за указание; удален сейчас.   -  person Saturn5    schedule 09.09.2017
comment
@ Реми, теперь я верю, что у тебя есть ответ. Это было скрыто, потому что OnError даже не был введен. Если вы сделаете ответ, я выберу его.   -  person Saturn5    schedule 09.09.2017
comment
@ Saturn5 Мне было лень искать соответствующую ссылку, поэтому я опубликовал ответ, который может дать вам пищу для размышлений. Это может не помочь напрямую решить эту проблему, но определенно помогло мне в моем дизайне веб-сокета.   -  person Al-un    schedule 10.09.2017


Ответы (2)


Чтобы развить те моменты, которые я упомянул, вашу проблему о том, «как обрабатывать требуемый параметр», я вижу следующие варианты. Прежде всего, давайте рассмотрим конечную точку:

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

Фильтрация

Первый контакт между клиентом и сервером — это HTTP-запрос. Вы можете отфильтровать его с помощью фильтра, чтобы предотвратить рукопожатие веб-сокета. Фильтр может либо заблокировать запрос, либо пропустить его:

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 в моем вопросе для поддержки асинхронной отправки сообщений.

TL, DR

если вам нужно манипулировать параметрами GET, предоставленными клиентом во время подключения, фильтр может быть решением, если вас устраивает чистый ответ 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

Вы хотите манипулировать некоторыми параметрами, но обработать возможную ошибку с помощью веб-сокета? Создайте свою собственную конфигурацию.

Попробуйте поймать

Я также упомянул опцию 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...
}

Надеюсь, это поможет

person Al-un    schedule 09.09.2017
comment
Я действительно не знаю этикета SO, но я думаю, вам лучше добавить свое редактирование к своему сообщению. Кстати, ваше редактирование содержит очень важную информацию :) - person Al-un; 11.09.2017

Я считаю, что должен был поставить...

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

... прямо там, где возникает ошибка, в @OnOpen(). (Для общих ошибок используйте CloseCodes.UNEXPECTED_CONDITION.)

Клиент получает:

onClose(1003, some text)

Это, конечно, очевидный ответ. Я думаю, что приведенный пример ввел меня в заблуждение, вызвав исключение из @OnOpen(). Как предположил Реми Лебо, сокет, вероятно, был закрыт из-за этого, блокируя любую дальнейшую обработку с моей стороны в @OnError(). (Возможно, какая-то другая ошибка скрыла обсуждаемые доказательства.)

person Saturn5    schedule 09.09.2017
comment
onError срабатывает при исключении. Как предположил Реми Лебо, если где-либо возникает исключение, в вашем конкретном случае во время @OnOpen, то вызывается @OnError. Если вы хотите закрыть соединение во время @OnOpen без исключения, я думаю, вам лучше всего поставить попытку/поймать и закрыть соединение в вашем улове - person Al-un; 10.09.2017