
1. WebSocket 소개
1.1 WebSocket?
WebSocket은 클라이언트와 서버 간의 양방향 통신을 지원하는 네트워크 프로토콜입니다. 일반적으로 HTTP는 요청-응답 구조로 한 번 요청하면 한 번 응답을 받는 형태로 작동하지만, WebSocket은 연결이 유지된 상태에서 클라이언트와 서버가 실시간으로 데이터를 주고받을 수 있습니다. 이는 소켓을 이용해 데이터 스트림을 주고받는 것과 유사한 형태로, 특히 실시간 애플리케이션에서 자주 사용됩니다.
- HTTP와 WebSocket의 차이점
- 연결 방식:
HTTP는 기본적으로 단일 요청 후 연결이 종료되는 방식이지만, WebSocket은 한 번 연결이 성립되면 지속적으로 연결을 유지하면서 데이터를 주고받습니다. - 양방향 통신:
HTTP는 클라이언트가 요청하면 서버가 응답하는 단방향 통신 방식이지만, WebSocket은 서버도 클라이언트로 데이터를 푸시할 수 있는 양방향 통신 방식입니다. - 프로토콜:
HTTP는 텍스트 기반 프로토콜인 반면, WebSocket은 프레임 기반으로 데이터가 전송되며, 텍스트나 바이너리 데이터 전송을 모두 지원합니다. - 실시간성:
HTTP로 실시간 애플리케이션을 만들려면 반복적인 요청(Polling)을 사용해야 하지만, WebSocket은 실시간으로 이벤트를 주고받으므로 불필요한 요청을 줄일 수 있습니다.
2. WebSocket 과정

웹소켓은 전체적으로 다음의 과정을 거칩니다.
- Opening Handshake:
- 클라이언트는 서버로 WebSocket 연결 요청을 보내고, 서버가 이를 수락하면 연결이 성립됩니다.
- Data Transfer:
- 연결이 성립된 후, 클라이언트와 서버는 양방향으로 자유롭게 데이터를 주고받을 수 있습니다.
- Closing Handshake:
- 연결을 종료할 때, 클라이언트 또는 서버가 먼저 종료 신호를 보내면 상대방이 이를 수락하며 연결이 종료됩니다.
2.1 WebSocket 연결 (Connection Establishment)
WebSocket 연결의 초기 단계는 HTTP와 WebSocket 프로토콜이 상호작용하는 과정입니다. 클라이언트와 서버가 TCP 3-way handshake를 통해 연결을 설정한 후, WebSocket은 HTTP 핸드셰이크를 통해 프로토콜을 업그레이드합니다.
1) TCP 3-way Handshake
WebSocket 연결은 기본적으로 TCP 프로토콜 위에서 동작합니다. 따라서 WebSocket이 연결되기 전에 클라이언트와 서버는 TCP 3-way handshake로 안정적인 연결을 수립합니다.
- Step 1 (SYN): 클라이언트가 서버에게 연결을 요청하며 TCP 패킷의 SYN 플래그를 설정한 패킷을 보냅니다.
- Step 2 (SYN-ACK): 서버가 요청을 수신하고, 클라이언트에게 TCP 패킷의 SYN-ACK 플래그를 설정한 응답 패킷을 보냅니다.
- Step 3 (ACK): 클라이언트는 서버의 응답을 확인한 후 ACK 플래그가 설정된 패킷을 보내며, 이로써 TCP 연결이 수립됩니다.
TCP 연결이 완료된 후, WebSocket의 핸드셰이크 과정이 시작됩니다.
2) HTTP Upgrade 핸드셰이크
WebSocket은 HTTP 프로토콜을 통해 서버와의 연결을 설정합니다. HTTP 핸드셰이크 요청/응답은 다음과 같은 절차로 이루어집니다
1. 클라이언트 요청
- 클라이언트는 서버에게 HTTP 요청을 보내고, 이 요청에 Upgrade: websocket 헤더를 추가하여 WebSocket 프로토콜로의 업그레이드를 요청합니다.
- 또한, 요청 헤더에 Connection: Upgrade를 명시하고, Sec-WebSocket-Key, Sec-WebSocket-Version 등의 정보를 포함합니다.
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
2. 서버 응답
- 서버는 클라이언트의 요청을 수신하고, WebSocket 프로토콜로 업그레이드가 가능한지 검토합니다. 업그레이드가 가능하면, 101 Switching Protocols 상태 코드와 함께 다음과 같은 응답을 반환합니다.
- Sec-WebSocket-Accept는 클라이언트가 보낸 Sec-WebSocket-Key를 서버에서 해시 처리한 값입니다.
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
3. 업그레이드 완료
- 클라이언트와 서버는 HTTP에서 WebSocket으로 프로토콜을 전환하게 됩니다. 이후로는 HTTP와의 상관관계가 끊어지고, TCP 연결을 유지한 채로 프레임 기반 통신이 시작됩니다.
2.2 WebSocket 데이터 전송 (Data Transmission)
WebSocket의 데이터 전송은 양방향이며, 프레임 기반 구조로 데이터를 주고받습니다. 클라이언트와 서버는 데이터 전송 시 텍스트 또는 바이너리 데이터로 구성된 WebSocket 프레임을 사용합니다.
1) 프레임 구조

WebSocket 프레임은 메시지의 헤더와 페이로드로 구성됩니다. 이 프레임들은 클라이언트와 서버 간에 상호교환되며, 다양한 데이터 형식이 가능합니다.
- FIN: 메시지의 마지막 프레임인지 여부를 나타냅니다. FIN 비트가 1이면 메시지가 종료됨을 의미합니다.
- RSV1-3 (3 bits): 예약된 필드로, 기본적으로 0으로 설정되며, 확장이 필요한 경우 사용됩니다.
- Opcode: 데이터가 텍스트인지 바이너리인지, 제어 프레임인지 등을 나타내는 4비트 필드입니다.
- 0x0: 이전 프레임과 이어짐
- 0x1: 텍스트 프레임
- 0x2: 바이너리 프레임
- 0x3 ~ 0x7: 나중에 추가될 프레임을 위한 부분
- 0x8: 연결 종료
- 0x9: 핑 (ping)
- 0xA: 퐁 (pong)
- 0xB ~ 0xF: 나중에 추가될 프레임을 위한 부분
- MASK: MASK는 마스킹 사용 여부를 정의하며, 1로 설정될 경우 마스킹 키를 설정해야 합니다. 마스크 값을 1로 설정할 경우 클라이언트는 마스킹 키를 사용하여 XOR 연산으로 데이터를 인코딩하여 전송합니다.
- Payload Length: 기본적으로 7비트를 사용하여 payload의 길이를 나타냅니다.
- 0-125: 실제 payload 크기입니다.
- 126: 16비트 확장 길이 필드 사용.
- 127: 64비트 확장 길이 필드 사용.
- Masking Key : 클라이언트가 서버에 데이터를 전송할 때는 사용하는 마스크 키입니다. 이때 MASK는 1이어야 합니다. 서버는 클라이언트가 보낸 마스킹된 데이터를 수신한 후 마스크 키를 사용하여 데이터를 복호화합니다.
- Payload Data: 실제 전송할 텍스트 또는 바이너리 데이터입니다.
2) 메시지 분할 (Fragmentation)
WebSocket 통신에서 데이터는 여러 프레임에 나뉘어 전송될 수 있습니다. WebSocket 프로토콜은 메시지 프레임을 결합하여 큰 메시지를 전송할 수 있도록 지원합니다.
하나의 메시지가 여러 개의 프레임에 나뉘어 전송되는 경우, 각 프레임의 FIN 비트와 Opcode 필드가 메시지의 시작, 중간, 끝을 나타냅니다.
- 클라이언트가 단일 프레임으로 메시지를 보냄
- FIN=1, Opcode=0x1, Payload="Hello"
- 클라이언트가 다중 프레임으로 메시지를 보냄
- 첫 번째 프레임: FIN=0, Opcode=0x1, Payload="and a"
- 두 번째 프레임: FIN=0, Opcode=0x0, Payload="happy new"
- 마지막 프레임: FIN=1, Opcode=0x0, Payload="year!"
서버는 첫 번째 프레임에서 새로운 메시지가 시작되었음을 인지하고, 이후의 프레임을 결합하여 최종 메시지를 완성합니다.
3) ping과 pong: WebSocket의 하트비트
WebSocket에서 Ping과 Pong 프레임은 연결 상태를 확인하기 위해 사용됩니다. 어느 한 쪽이 핑 프레임(Opcode 0x9)을 보내면, 상대는 반드시 동일한 데이터를 포함한 퐁 프레임(Opcode 0xA)으로 응답해야 합니다.
- Ping 프레임: 서버나 클라이언트는 언제든지 핑을 전송할 수 있습니다.
- Pong 프레임: 핑 프레임을 받으면 즉시 같은 데이터를 담은 퐁 프레임을 전송해야 합니다. 핑을 보내지 않았더라도 퐁을 받을 수 있으며, 이 경우는 무시합니다.
2.3 WebSocket 연결 종료 (Connection Closed)
WebSocket 연결 종료 과정은 기본적으로 다음의 두 단계로 구성됩니다:
- Close 프레임 전송: 클라이언트 또는 서버 중 한 쪽이 "close" 제어 프레임을 전송합니다. 이 프레임은 FIN 비트가 설정되며(예: FIN=1), opcode가 0x8로 설정되어 종료 요청임을 나타냅니다. 이 프레임에는 선택적으로 종료 상태 코드와 이유가 포함될 수 있습니다.
- 상대방의 응답: 두 번째 단계에서는 상대방이 또 다른 "close" 프레임으로 응답합니다. 이 프레임에서도 FIN 비트가 설정되고(예: FIN=1) opcode는 0x8로, 상대방의 종료 요청 수락을 나타냅니다.
이 과정을 통해 WebSocket 연결은 CLOSED 상태로 전환되며, 연결된 TCP 소켓도 안전하게 종료됩니다. 그러나 만약 한 쪽에서 TCP 연결을 갑자기 닫는 경우, WebSocket 연결은 여전히 종료된 것으로 간주됩니다.
3. AWS API Gateway Websocket
AWS API Gateway WebSocket은 클라이언트와 서버 간에 실시간 양방향 통신을 가능하게 하는 서비스입니다. HTTP 기반의 REST API와는 달리, WebSocket API는 연결이 유지된 상태에서 양쪽에서 메시지를 주고받을 수 있습니다.
API Gateway WebSocket을 사용하면 서버리스 아키텍처에서 실시간 통신을 쉽게 구축할 수 있으며, 메시지 처리 및 라우팅은 AWS Lambda, DynamoDB 등과 통합되어 자동으로 확장 및 관리됩니다.
3.1 WebSocket 라우팅
WebSocket API에서는 각 메시지를 특정 라우팅 규칙에 따라 처리합니다. 메시지의 유형에 따라 다른 Lambda 함수나 리소스를 호출할 수 있도록 라우팅을 정의합니다.
1) 사전 정의된 라우팅
- $connect: 클라이언트가 WebSocket API에 처음 연결할 때 트리거됩니다. 주로 클라이언트와의 세션을 관리하거나 초기 상태를 설정하는 데 사용됩니다.
- $disconnect: 클라이언트가 WebSocket 연결을 종료할 때 트리거됩니다. 이때 세션 데이터를 정리하거나 로그아웃 처리 등을 수행할 수 있습니다.
- $default: 라우트가 명시적으로 정의되지 않은 메시지가 전송될 때 호출됩니다. 이 라우트는 모든 메시지에 대한 기본 동작을 정의하는 데 사용됩니다.
2) 사용자 정의 라우팅
사용자는 특정 메시지 유형에 대한 라우팅을 정의할 수 있습니다. 메시지 본문이나 경로에 따라 다른 Lambda 함수나 리소스가 호출되도록 설정하여 비즈니스 로직을 적용할 수 있습니다. 예를 들어, 특정 이벤트에 대한 실시간 처리를 위해 메시지의 action 필드를 기준으로 라우팅을 설정할 수 있습니다.
3. 라우팅 선택 표현식 (routeSelectionExpression)
API Gateway는 WebSocket 메시지를 어떤 경로로 라우팅할지 결정하기 위해 routeSelectionExpression을 사용합니다. 이 표현식은 JSON 본문에서 특정 필드를 기준으로 라우트를 결정하는 데 사용됩니다.
예를 들어, action 필드를 기준으로 라우팅을 설정하려면 routeSelectionExpression을 $request.body.action으로 설정할 수 있습니다. 그러면 메시지 본문 내의 action 값에 따라 각각 다른 사용자 정의 라우트로 메시지가 전달됩니다.
3.2 AWS API Gateway Websocket 생성
1. API Gateway 메뉴에서 WebScoket API 구축을 누릅니다.

2. 이름과 라우팅 표현식은 기본값인 request.body.action으로 합니다.

3. 라우팅은 경로는 $connect, $disconnect, $default를 추가하고 사용자 지정 경로도 추가합니다.

4. 각 라우팅 경로별로 AWS 서비스에 통합해서 사용할 수 있습니다. 여기서는 생성된 Lambda를 매핑해줍니다.

5. 스테이지를 추가합니다.

6. 생성이 완료되면 라우팅 경로를 확인 할 수 있습니다.

7. 스테이지 메뉴에 가면 웹소켓 주소와 connections url를 확인 할 수 있습니다. 클라이언트가 웹소켓 요청시에는 웹소켓 URL을 사용합니다. connections url은 연결된 클라이언트에게 직접 메시지를 전송할 때 사용합니다.

3.3 AWS API Gateway Websocket 테스트
1) $connect: 연결 요청 단계
웹소켓에서 설명 했던 것처럼 처음의 핸드셰이크 과정(tcp 연결, 웹소켓 전환)이후에 WebSocket 연결이 성립되면 $connect 라우트가 트리거됩니다. 이 단계에서 Lambda 함수를 호출하거나 초기 데이터를 DynamoDB에 저장하는 등 클라이언트 연결과 관련된 작업을 수행할 수 있습니다.
- 주요 역할:
- 클라이언트의 연결 상태를 관리
- 연결 초기화 시 처리할 데이터 생성 (예: 인증 토큰 검증, 클라이언트 정보 저장 등)
1. 로그를 확인하기 위해 $connect와 매핑된 람다함수에 event를 출력하는 함수를 작성합니다.
import json
def lambda_handler(event, context):
print(event)
return {'statusCode': 200}
2. 웹소켓 테스트 사이트에서 생성된 웹소켓 url로 요청을 보냅니다.

3. 해당 람수함수의 CloudWatch로 print(event)의 내용을 확인 할 수 있는데, 일부 내용을 요약하면 다음과 같습니다.

- requestContext["connectionId"]:
- WebSocket 연결에 대해 고유한 연결 ID를 제공합니다. 이 ID는 해당 클라이언트의 WebSocket 세션을 식별하는 데 사용되며, 메시지를 주고받거나 연결을 해제할 때 사용됩니다.
- 예시: 'connectionId': 'et4F5dn9oE0CJ8Q='
- requestContext["eventType"]:
- 현재 이벤트가 어떤 유형인지 나타냅니다. 여기서는 CONNECT 이벤트로, 클라이언트가 WebSocket 연결을 시도할 때 발생하는 이벤트입니다.
- 예시: 'eventType': 'CONNECT'
- requestContext["identity"]["sourceIp"]:
- 연결을 시도한 클라이언트의 IP 주소입니다. 클라이언트의 위치를 추적하거나 로그에 기록할 때 유용합니다.
- 예시: 'sourceIp': '210.23.230.12'
- headers["Origin"]:
- 연결을 시도한 클라이언트의 원본 도메인 정보입니다. 보안 및 인증 목적으로 사용할 수 있습니다.
- 예시: 'Origin': 'https://piehost.com'
- requestContext["routeKey"]:
- WebSocket의 라우팅 키를 정의합니다. $connect, $disconnect, $default와 같은 예약된 키 외에 사용자 정의 라우팅 키가 올 수 있습니다.
- 예시: 'routeKey': '$connect'
2) 양방향 데이터 통신: 메시지 전송 과정
연결이 성립된 후 클라이언트는 WebSocket을 통해 서버로 메시지를 전송할 수 있으며, 서버도 반대로 클라이언트로 메시지를 보낼 수 있습니다. WebSocket은 지속적인 연결 상태를 유지하므로 별도의 새로운 요청 없이 양방향 통신이 가능합니다.
1. "dev" 라우팅 경로와 매핑된 람다함수에 apigateway에 접근할 수 있는 권한을 추가합니다.

2. "dev" 라우팅 경로와 매핑된 람다함수에 다음 코드를 작성합니다.
import json
import boto3
client = boto3.client("apigatewaymanagementapi", endpoint_url="웹소켓에서 확인한 @connections URL")
def lambda_handler(event, context):
connectionId = event["requestContext"]["connectionId"]
req = json.loads(event['body'])['message']
#입력받은 데이터를 양쪽에 HELLO 붙혀서 반환하기
output = {
"output": "HELLO " + req + " HELLO"
}
res = client.post_to_connection(ConnectionId=connectionId, Data=json.dumps(output))
return {'statusCode': 200}
2. request body내용에 "action": "dev"를 지정하여 라우팅경로를 설정할 수 있습니다. 메시지를 보내면 람다함수가 이를 처리하여 사용자가 보낸 데이터에 "HELLO"를 양쪽에 붙혀서 응답하는 것을 확인 할 수 있습니다.

3) $default: 정의되지 않은 라우팅 경로
만약 {"action": "암거나"} 처럼 APigatewayt 생성시 정의되지 않은 라우팅 경로에 요청을 보내면 $default를 트리거하게 됩니다.
1. $default와 매핑된 람다 함수에 다음 코드를 작성하고 action에 아무데이터나 넣고 요청합니다.
def lambda_handler(event, context):
print("잘못된 경로")
return {'statusCode': 200}
2. $default 경로가 트리거 되었음을 확인 할 수 있습니다.

4) $disconnect: 연결 종료 단계
- WebSocket 연결은 클라이언트가 명시적으로 연결을 끊거나, 네트워크 상태 등의 이유로 연결이 끊길 때 종료됩니다.
- 클라이언트가 연결 해제 요청을 보내면, WebSocket 프로토콜에 따라 CLOSE 프레임이 전송됩니다.
- API Gateway는 연결이 종료되었음을 인식하고, $disconnect 라우트를 트리거합니다.
- 이 단계에서는 연결이 끊긴 클라이언트의 세션 데이터를 정리하거나, 클라이언트 상태를 갱신하는 작업을 수행할 수 있습니다.
- 예를 들어, 연결 중이던 채팅 세션에서 사용자를 제거하거나, 특정 클라이언트와 관련된 리소스를 정리합니다.
- API Gateway는 내부적으로 클라이언트와의 WebSocket 연결을 종료하며, 더 이상 해당 클라이언트로부터 메시지를 받을 수 없게 됩니다.
WebSocket와 API Gateway가 담당하는 역할을 정리하면 다음과 같습니다.
- HTTP Upgrade 요청 처리: API Gateway는 클라이언트의 연결 요청을 받아들여 HTTP 101 응답을 전송하고 WebSocket 연결을 성립시킵니다.
- 라우팅 처리: 연결 이후 주고받는 메시지는 API Gateway에서 라우팅 규칙에 따라 적절한 Lambda 함수나 다른 AWS 서비스로 전달됩니다.
- 상태 관리: API Gateway는 연결 상태($connect/$disconnect)와 메시지 처리($default 또는 사용자 정의 라우트)를 관리합니다.
- 실시간 양방향 통신 지원: API Gateway는 지속적인 연결을 통해 클라이언트와 서버 간의 실시간 양방향 통신을 유지하며, 서버리스 환경에서 손쉽게 확장 가능한 구조를 제공합니다.
- 레퍼런스
https://vishalrana9915.medium.com/understanding-websockets-in-depth-6eb07ab298b3
https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers
'컴퓨터 > AWS' 카테고리의 다른 글
| AWS - CloudFormation (0) | 2024.08.29 |
|---|---|
| AWS - Serverless Architecture(AWS Lambda) (0) | 2024.07.25 |
| AWS - CloudFront(1) (0) | 2024.07.04 |
| AWS - RDS(Relational Database Service) (0) | 2024.07.02 |
| AWS - EBS(Elastic Block Storage)/EFS(Elastic File System) (0) | 2024.06.25 |