如何通过 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...
}
希望对您有所帮助
当我的服务器发生内部错误时,如何正确关闭 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...
}
希望对您有所帮助