etc

WebSocket Server에서 Connection `close` 이벤트를 받지 못할 때

WebSocket 서버를 운영하다보면 가끔 Client WebSocket Connection이 끊겨졌음에도 서버에서는 close이벤트를 수신하지 못할 때가 있다.
아마 WebSocket 서버에서 WebSocket Connection이 close될 때 해당 Connection과 관련된 데이터를 정리하는 코드를 작성해놨을텐데,
close 이벤트가 제대로 동작하질 않으니 이 데이터 정리 코드가 실행되지 않을 것이고 이는 곧 버그로 이어질 가능성이 높다.

나도 WebSocket을 사용한 실시간 서비스 서버를 개발하면서 이러한 문제를 겪었고 그 원인에 대해 찾아보고 대처를 한 것에 대해 정리하고자 이 글을 포스팅한다.


먼저 내가 해당 문제를 재현한 방법은 WebSocket Connection이 생성된 Client에서 Network를 끊어버리는 것이었다.

단순히 생각하면 Network가 끊겼기 때문에 결국 연결이 끊겨 close이벤트가 발생해야하는게 아니냐라고 생각할 수 있지만 해당 원인을 이해하기 위해서는 WebSocket Protocol에 대해 어느정도 알 필요가 있다.

WebSocket Protocol에서 연결을 맺고, 종료하는 과정은 일련의 handshake로 이루어져있다.

출처: https://docs.axway.com/bundle/axway-open-docs/page/docs/apim_policydev/apigw_gw_instances/general_websocket/index.html

Close하는 부분을 보면 마치 TCP/IP의 handshake와 비슷하다.

출처: http://aurumme.com/atech/tcp-3-way-handshake-process/

WebSocket Protocl RFC 표준 문서Closing handshake 관련 부분을 보면 다음과 같이 설명을 한다.

Either peer can send a control frame with data containing a specified control sequence to begin the closing handshake (detailed in Section 5.5.1).
둘 중 어느 피어가 closing handshake를 시작하기 위한 특정 control sequence를 포함한 control frame을 보낼 수 있다.
Upon receiving such a frame, the other peer sends a Close frame in response, if it hasn’t already sent one.
그러한 frame을 수신하면, 다른 피어는 Close frame을 응답 안에 보낸다, 만약 그것을 이미 보내지 않았다면.
Upon receiving that control frame, the first peer then closes the connection, safe in the knowledge that no further data is forthcoming.
해당 (응답의) control frame을 수신하면, 첫번째 피어는 연결을 닫고, 더 이상 데이터는 오지 않을 것이라고 판단한다.

위 다이어그램과 표준 문서의 설명에서 보여주듯, WebSocket Connection을 Close 할 때 Server와 Client 사이의 일련의 handshake 과정을 거쳐야 한다.
그렇기 때문에 만약 Client 쪽에서 네트워크에 문제가 생겨 일방적으로 연결이 끊기게 되면 handshake 과정이 이뤄지지 않아 서버에서는 해당 Connection이 연결이 끊겼는지 알 수 없다.

그렇다면 이러한 경우 어떻게 서버가 해당 Connection이 Close 되었는지 판단할 수 있을까?

javascript WebSocket library인 ws는 일정 주기로 서버에 붙은 WebSocket Connection들에게 ping을 보내서 해당 Connection이 정상적으로 연결되어있는지 확인하라고 한다.

How to detect and close broken connections?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const WebSocket = require('ws');

function noop() {}

function heartbeat() {
this.isAlive = true;
}

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', function connection(ws) {
ws.isAlive = true;
ws.on('pong', heartbeat);
});

// 주기적으로 모든 WebSocket Connection에게 ping을 보내 connection 상태 확인
const interval = setInterval(function ping() {
wss.clients.forEach(function each(ws) {
if (ws.isAlive === false) return ws.terminate();

ws.isAlive = false;
ws.ping(noop);
});
}, 30000);

wss.on('close', function close() {
clearInterval(interval);
});

WebSocket은 실시간 통신을 매우 쉽고 편리하게 구현할 수 있지만 실제 서비스에서 사용하기 위해서는 위와 같은 여러 edge case를 따져봐야 한다.

특히 위와 같이 Client 쪽에서 Network가 끊기는 경우는 Mobile Application에서는 충분히 일어날 수 있는 일이기 때문에 해당의 케이스는 꼭 확인하도록 하자.


참고:

Share