英文:
Heroku H15 Error on web socket close
问题
我有一个在Heroku上作为Web套接字服务器的Go服务。客户端每20秒向服务器发送ping请求,似乎保持连接处于打开状态。问题是,当套接字连接关闭时,Heroku路由器会抛出H15错误,认为请求花费了太长时间。例如,如果Web套接字连接已经打开了300秒,Heroku日志会显示:
….H15…. dyno=web.1 connect=1ms service=300000ms status=503 bytes=147….
有人遇到过这个问题吗?
英文:
I have a Go service serving as a web socket server on Heroku. The client pings the server every 20 seconds and it seems to keep the connection open. The problem is that when the socket connection is closed, Heroku router throws H15 error thinking that the request took too much time. For example, if web socket connection has been open for 300 seconds, Heroku log would show:
….H15…. dyno=web.1 connect=1ms service=300000ms status=503 bytes=147….
Anyone has experienced this?
答案1
得分: 7
是的!我经历过这种情况,并经过一些深入的调试,得出结论这只是Heroku路由引擎中的一个“误报”。我的调试过程如下:
- 在客户端和在Heroku上运行的WebSocket服务器之间创建一个WebSocket连接。
- 如果你使用的是socket.io或类似的库,它很可能在切换到WebSocket协议之前先建立一个HTTP轮询连接(你可以在Heroku路由日志中看到这些GET/POST请求)。
- 一旦使用WebSocket协议建立了连接,客户端将通过发送心跳来保持连接活跃(实际上服务器也可以这样做)。最初,你在Heroku路由日志中看不到这个请求,因为它仍然是挂起状态(它是打开的,并且每隔几秒钟发送一些数据包,即心跳)。你可以使用Chrome DevTools的网络选项卡来监视这个过程。
- 心跳防止Heroku路由终止请求,因为每次在客户端和服务器之间传输几个字节时,Heroku路由的超时计时器(55秒)会被重置。
- 然而,每当你的客户端关闭连接(例如:关闭浏览器选项卡,刷新页面,断开与互联网的连接等),Heroku路由会检测到请求被终止,并且(根据我的调试推测)因为请求处于“挂起”状态,它会像空闲状态一样反应,空闲时间为从请求接收到关闭的时间,这时你会看到类似这样的日志:
service=79859ms status=101
。
总结一下:每当你的客户端应用程序终止一个已经运行了X毫秒的WebSocket连接时,Heroku路由将记录一个错误15行。因此,在许多情况下,这可能只是用户离开你的应用程序。
希望这对大家有所帮助,让你们少了一件要担心的事情
英文:
yes! I have experienced that situation and after some deep debugging, I came to the conclusion that this is just a "false positive" in the Heroku Router engine. My debugging went like this:
- Create a WebSocket connection between the client and the WebSocket server running on Heroku.
- If you're using socket.io or a similar library it will most likely establish an HTTP polling connection before switching protocols to WebSocket. (You can see these GET/POST requests in the Heroku Router logs).
- Once the connection is established using the WebSocket protocol, the client will keep it alive by sending a heartbeat (actually the server can do this too). Initially, you won't see this request in the Heroku Router logs because it's still pending (it's open and sending packages of data every few seconds, the heartbeat). You can monitor this by using the Chrome DevTools Network tab.
- The heartbeat prevents Heroku Router from terminating the request, since every time a few bytes are transferred between the client and the server the Heroku Router timeout timer (55secs) is reset.
- However, whenever your client closes the connection (ie: closes the browser tab, refreshes the page, disconnects from the internet, etc); the Heroku Router detects the request being terminated and (here comes is my guess based on debugging) because the request was "pending" it reacts as if it was idle for X amount of milliseconds, where X is the time from when the request was received until it was closed, that's when you see logs like this:
service=79859ms status=101
As a conclusion: Heroku Router will log an Error 15 line whenever your client app terminates a WebSocket connection which was working perfectly fine for X amount of milliseconds. So, in many cases, it can be just users leaving your app.
I hope this helps people and that you guys can go to sleep with one less thing to worry about
答案2
得分: 4
我通过从服务器每秒发送ping来解决了这个问题,就像heroku nodejs示例中的做法一样:https://github.com/heroku-examples/node-websockets/blob/master/server.js
英文:
I solved the problem by sending pings from the server every second, as in the heroku nodejs example: https://github.com/heroku-examples/node-websockets/blob/master/server.js
答案3
得分: 0
如果有人从Java
的角度来寻找解决方案,以下是我的WebSocket配置在Spring Boot中的代码:
@Component
public class SocketHandler extends TextWebSocketHandler {
public static Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String userName = (String) session.getAttributes().get("userName");
sessions.put(userName, session);
super.afterConnectionEstablished(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
String userName = (String) session.getAttributes().get("userName");
sessions.remove(userName);
super.afterConnectionClosed(session, status);
}
}
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new SocketHandler(), "/webSocket/AppName/*").addInterceptors(auctionInterceptor());
}
@Bean
public HandshakeInterceptor auctionInterceptor() {
return new HandshakeInterceptor() {
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
// Get the URI segment corresponding to the auction id during handshake
String path = request.getURI().getPath();
String userName = path.substring(path.lastIndexOf('/') + 1);
// This will be added to the websocket session
attributes.put("userName", userName);
return true;
}
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
// Nothing to do after handshake
}
};
}
}
上述代码是我在Spring Boot中的WebSocket配置,我只想创建连接并将其与用户名和会话详细信息一起保存在一个map
中。
然后在Spring Boot的主方法中添加了一个每30秒执行一次的ping操作,如下所示:
public static void main(String[] args) {
SpringApplication.run(MyAppName.class, args);
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
Runnable r = () -> {
if(SocketHandler.sessions != null && !SocketHandler.sessions.isEmpty()) {
for (Map.Entry<String, WebSocketSession> stringWebSocketSessionEntry : SocketHandler.sessions.entrySet()) {
try {
// Dummy ping to keep connection alive.
stringWebSocketSessionEntry.getValue().sendMessage(new TextMessage("Dummy Ping"));
} catch (IOException e) {
e.printStackTrace();
}
}
}
};
scheduler.scheduleAtFixedRate(r, 0, 30, TimeUnit.SECONDS);
}
这样做的目的是每30秒向订阅者发送ping,以防止默认的55
秒超时影响WebSocket连接,正如Heroku文档中建议的那样。
超时
Heroku的HTTP路由超时规则适用于WebSocket连接。客户端或服务器可以通过定期发送ping数据包来防止连接处于空闲状态。
英文:
If anyone comes here looking for a solution from Java
perspective:
@Component
public class SocketHandler extends TextWebSocketHandler {
public static Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String userName = (String) session.getAttributes().get("userName");
sessions.put(userName, session);
super.afterConnectionEstablished(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
String userName = (String) session.getAttributes().get("userName");
sessions.remove(userName);
super.afterConnectionClosed(session, status);
}
}
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new SocketHandler(), "/webSocket/AppName/*").addInterceptors(auctionInterceptor());;
}
@Bean
public HandshakeInterceptor auctionInterceptor() {
return new HandshakeInterceptor() {
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
// Get the URI segment corresponding to the auction id during handshake
String path = request.getURI().getPath();
String userName = path.substring(path.lastIndexOf('/') + 1);
// This will be added to the websocket session
attributes.put("userName", userName);
return true;
}
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
// Nothing to do after handshake
}
};
}
}
Above is my WebSocket configuration in Spring boot, as I only want the connection to be created and keep it in a map
with userName and session details.
After that in Springboot main method just added a ping every 30 seconds like this :
public static void main(String[] args) {
SpringApplication.run(MayAppName.class, args);
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
Runnable r = () -> {
if(SocketHandler.sessions != null && !SocketHandler.sessions.isEmpty()) {
for (Map.Entry<String, WebSocketSession> stringWebSocketSessionEntry : SocketHandler.sessions.entrySet()) {
try {
// Dummy ping to keep connection alive.
stringWebSocketSessionEntry.getValue().sendMessage(new TextMessage("Dummy Ping"));
} catch (IOException e) {
e.printStackTrace();
}
}
}
};
scheduler.scheduleAtFixedRate(r, 0, 30, TimeUnit.SECONDS);
}
What this does is, it keeps pinging the subscriber every 30 seconds so that the default 55
seconds does not affect websocket connection, as suggested in Heroku docs.
Timeouts
The normal Heroku HTTP routing timeout rules apply to WebSocket connections. Either client or server can prevent the connection from idling by sending an occasional ping packet over the connection.
答案4
得分: 0
在尝试发送之前,请检查yourSocket.READYSTATE === WebSocket.OPEN
是否为真。
如果不是打开状态,请确保通过实现类似这样的方法来打开它。
仅仅实现一个ping-pong解决方案是不足以保持连接的开放状态的。
英文:
Check for yourSocket.READYSTATE === WebSocket.OPEN
before attempting to send.
If not open then make sure it is open by implementing something like this
Just implementing a ping-pong solution wasn't enough to keep my connections open.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论