我在Heroku上有一个Go服务作为web套接字服务器。客户端每20秒对服务器进行一次ping,似乎保持了连接的打开状态。问题是,当套接字连接关闭时,Heroku路由器会抛出H15错误,认为请求花费了太多时间。例如,如果web套接字连接已打开300秒,Heroku日志将显示:
….H15….dyno=web.1连接=1ms服务=300000ms状态=503字节=147…。
有人经历过这种情况吗?
是的!我经历过这种情况,经过一些深入的调试,我得出的结论是,这只是Heroku路由器引擎中的一个"假阳性"。我的调试是这样的:
- 在客户端和运行在Heroku上的WebSocket服务器之间创建WebSocket连接
- 如果您使用的是socket.io或类似的库,它很可能会在将协议切换到WebSocket之前建立HTTP轮询连接。(您可以在Heroku路由器日志中看到这些GET/POST请求(
- 一旦使用WebSocket协议建立了连接,客户端将通过发送心跳来保持连接的有效性(实际上服务器也可以这样做(最初,您不会在Heroku路由器日志中看到此请求,因为它仍处于挂起状态(它处于打开状态,每隔几秒钟发送一次数据包,即心跳(。您可以使用Chrome DevTools"网络"选项卡对此进行监控
- 心跳阻止Heroku路由器终止请求,因为每次在客户端和服务器之间传输几个字节时,Heroku路由超时计时器(55秒(就会重置
- 但是,每当您的客户端关闭连接时(即:关闭浏览器选项卡、刷新页面、断开与互联网的连接等(;Heroku路由器检测到请求被终止,并且(这是我基于调试的猜测(因为请求是"挂起的",所以它的反应就好像它在X毫秒内处于空闲状态,其中X是从接收到请求到关闭的时间,这时你会看到这样的日志:
service=79859ms status=101
总之:每当您的客户端应用程序终止WebSocket连接时,Heroku路由器都会记录一行错误15,该连接在X毫秒内运行良好。因此,在许多情况下,可能只是用户离开了你的应用程序。
我希望这能帮助人们,你们可以少担心一件事:(
我通过每秒从服务器发送ping来解决问题,如heroku-nodejs示例所示:https://github.com/heroku-examples/node-websockets/blob/master/server.js
如果有人来这里从Java
的角度寻找解决方案:
@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引导中的WebSocket配置,因为我只想创建连接,并将其保存在具有用户名和会话详细信息的map
中。
在那之后,在Springboot中,主方法只是每30秒添加一次ping,如下所示:
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);
}
这样做的目的是,它每30秒对订阅者进行一次ping,这样默认的55
秒就不会影响websocket连接,正如Heroku文档中所建议的那样。
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.
在尝试发送之前检查yourSocket.READYSTATE === WebSocket.OPEN
。
如果没有打开,那么通过实现类似的东西来确保它是打开的
仅仅实现乒乓球解决方案还不足以保持我的连接畅通。