"죽은 소켓은 조용한 소켓이랑 똑같이 생겼어."
요청과 응답으론 모자랄 때
화면이 열려 있는 동안 내내 프레임이 양쪽으로 흘러야 하는 클라이언트가 있어. 살아 있는 세션에 붙는 터미널 앱의 폰 클라이언트, 코딩 엔진의 원격 게이트웨이, Pippa 백엔드로 이어지는 브리지들이 그래. 프로토콜은 URLSessionWebSocketTask가 맡아. send(.string(…))은 텍스트 프레임을, send(.data(…))는 바이너리 프레임을 보내. 파이썬 websockets 서버에 대고 보내보니 딱 그대로 도착했어. 이 차이는 받는 쪽에 중요해. 가족 파이썬 브리지는 텍스트 프레임에서 JSON을 읽고, 터미널 게이트웨이는 바이너리를 기대하거든. 그래서 공유 연결은 따로 지정하지 않으면 텍스트로 보내.
틀리기 쉬운 것 세 가지
공유 연결 타입은 재연결을 저마다 따로 구현했던 앱 세 개의 코드를 모아 만들었는데, 완성도가 셋 다 달랐어. 조사해보니 차이를 가른 건 이거였어.
- 세대. 연결을 내릴 때마다 새 토큰을 만들고, 비동기 콜백은 상태를 건드리기 전에 매번 그 토큰과 비교해. 이게 없으면 재연결 두 번 전에 취소한 소켓의 수신 콜백이 지난 프레임을 배달하거나, 실패를 보고해서 그 뒤에 들어온 새 소켓을 내려버릴 수 있어.
- 연결을 원하는 것, 앱이 활성인 것, 실제로 연결된 것은 다 달라. "사용자가 연결을 원했다", "앱이 앞에 있다", "소켓이 열려 있다"는 각각 다른 불리언 세 개야. 앱이 백그라운드로 가면 사용자가 원했다는 사실은 그대로 두고 소켓만 내려. 연결이 실패하면 앞의 두 값이 참일 때만 다시 시도하고.
- 죽은 소켓이 조용한 소켓처럼 보여. 레슨을 준비하며 세션 도중에 테스트 서버를
SIGSTOP으로 얼려봤어. 5초 뒤에도 클라이언트 작업은.running이었어. 3초 마감을 건 핑은 침묵이라고 보고했고. 세 앱 중 하나는 하트비트가 아예 없어서, tailnet 링크가 끊겨도 뭔가 보내려고 하기 전까진 계속 "돌고 있음"으로 표시됐어.
연결은 다시 시도해도, 명령은 절대 재전송하지 않아
소켓이 끊기면 연결은 알아서 다시 이어. 처음엔 1초를 기다리고, 그 뒤로는 두 배씩 늘리다가 30초에서 멈추고, 기본값으로는 끝없이 시도해. 집에 있는 Mac이 그냥 자고 있는 거라면 기다려주는 게 맞으니까. 절대 하지 않는 건 전송 중이던 프레임을 다시 보내는 거야. 터미널로 보낸 명령은 끊기기 전에 이미 실행됐을 수도 있고, 다시 보내면 두 번 돌 수 있어. 그래서 잃어버린 요청을 다시 보낼지 버릴지는 앱이 정해. 같은 타입에서 나온 작은 규칙이 둘 더 있어. 목적지는 connect(to:)를 명시적으로 불러야만 확정돼. 그래서 설정에서 호스트 칸을 고쳐도, 살아 있는 연결이 다음에 재연결할 곳은 바뀌지 않아. 그리고 연결의 이벤트는 연결하기 전에 구독해. 연결 단계는 상태라서 새 구독자한테도 다시 전해주지만, 프레임은 그렇지 않아. 늦게 구독하면 처음 몇 개를 놓칠 수 있어.