순수 Python 소켓을 사용하여 HTTP/2 및 WebSocket 프로토콜 마스터하기
James Reed
Infrastructure Engineer · Leapcell

순수 Python 소켓을 사용하여 HTTP/2 및 WebSocket 프로토콜 마스터하기
소개
네트워크 프로토콜은 인터넷의 기초 역할을 합니다. HTTP/1.0, HTTP/2.0 및 WebSocket은 각각 다른 시나리오에서 최신 웹 애플리케이션을 지원합니다. 이 문서에서는 이러한 세 가지 프로토콜의 핵심 로직을 순수 Python 소켓을 사용하여 구현하여 기본 통신 원칙에 대한 심층적인 이해를 얻습니다. 이 문서의 모든 예제 코드는 Python 3.8+ 환경에서 검증되었으며 네트워크 프로그래밍, 프로토콜 파싱 및 바이트 스트림 처리와 같은 핵심 기술을 다룹니다.
1. HTTP/1.0 프로토콜 구현
1.1 HTTP/1.0 프로토콜 개요
HTTP/1.0은 TCP 연결을 기반으로 하는 초기 상태 비저장 요청-응답 프로토콜입니다. 기본적으로 짧은 연결을 사용합니다(각 요청 후 연결을 닫음). 해당 요청은 요청 라인, 요청 헤더 및 요청 본문으로 구성되며, 응답은 상태 라인, 응답 헤더 및 응답 본문을 포함합니다.
1.2 서버 측 구현 단계
1.2.1 TCP 소켓 생성
import socket def create_http1_server(port=8080): server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server_socket.bind(('0.0.0.0', port)) server_socket.listen(5) print(f"HTTP/1.0 Server listening on port {port}") return server_socket
1.2.2 요청 데이터 파싱
정규식을 사용하여 요청 라인과 헤더를 파싱합니다.
import re REQUEST_PATTERN = re.compile( r'^([A-Z]+)\s+([^\s]+)\s+HTTP/1.\d\r\n' r'(.*?)\r\n\r\n(.*)', re.DOTALL | re.IGNORECASE ) def parse_http1_request(data): match = REQUEST_PATTERN.match(data.decode('utf-8')) if not match: return None method, path, headers_str, body = match.groups() headers = {k: v for k, v in (line.split(': ', 1) for line in headers_str.split('\r\n') if line)} return { 'method': method, 'path': path, 'headers': headers, 'body': body }
1.2.3 응답 데이터 생성
def build_http1_response(status_code=200, body='', headers=None): status_line = f'HTTP/1.0 {status_code} OK\r\n' header_lines = ['Content-Length: %d\r\n' % len(body.encode('utf-8'))] if headers: header_lines.extend([f'{k}: {v}\r\n' for k, v in headers.items()]) return (status_line + ''.join(header_lines) + '\r\n' + body).encode('utf-8')
1.2.4 주요 처리 루프
def handle_http1_connection(client_socket): try: request_data = client_socket.recv(4096) if not request_data: return request = parse_http1_request(request_data) if not request: response = build_http1_response(400, 'Bad Request') elif request['path'] == '/hello': response = build_http1_response(200, 'Hello, HTTP/1.0!') else: response = build_http1_response(404, 'Not Found') client_socket.sendall(response) finally: client_socket.close() if __name__ == '__main__': server_socket = create_http1_server() while True: client_socket, addr = server_socket.accept() handle_http1_connection(client_socket)
1.3 주요 기능 설명
- 짧은 연결 처리: 각 요청을 처리한 후 즉시 연결을 닫습니다(
client_socket.close()
). - 요청 파싱: 정규식을 사용하여 요청 구조와 일치시켜 일반적인
GET
요청을 처리합니다. - 응답 생성: 상태 라인, 응답 헤더 및 응답 본문을 수동으로 구성하여 Content-Length 헤더의 정확성을 보장합니다.
2. HTTP/2.0 프로토콜 구현 (단순화된 버전)
2.1 HTTP/2.0의 핵심 기능
HTTP/2.0은 이진 프레이밍 레이어를 기반으로 하며 멀티플렉싱, 헤더 압축(HPACK) 및 서버 푸시와 같은 기능을 지원합니다. 핵심은 요청/응답을 프레임으로 분해하고 스트림을 통해 통신을 관리하는 것입니다.
2.2 프레임 구조 정의
HTTP/2.0 프레임은 다음 부분으로 구성됩니다.
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+---------------+-------------------------------+
| Stream Identifier (31) |
+-----------------------------------------------+
| Frame Payload |
+-----------------------------------------------+
2.3 단순화된 구현 접근 방식
HTTP/2.0의 복잡성이 높기 때문에 이 예제에서는 다음 기능을 구현합니다.
GET
요청 헤더 프레임(HEADERS Frame) 및 데이터 프레임(DATA Frame)을 처리합니다.- HPACK 압축을 구현하지 않고 원시 헤더를 직접 전송합니다.
- 단일 스트림 처리, 멀티플렉싱을 지원하지 않습니다.
2.4 서버 측 코드 구현
2.4.1 프레임 생성자
def build_headers_frame(stream_id, headers): """헤더 프레임 빌드 (HPACK 압축이 없는 단순화된 버전)""" header_block = ''.join([f'{k}:{v}\r\n' for k, v in headers.items()]).encode('utf-8') length = len(header_block) + 5 # 헤더 프레임에 대한 추가 오버헤드 frame = ( length.to_bytes(3, 'big') + b'\x01' # TYPE=HEADERS (0x01) b'\x00' # FLAGS (단순화된 처리, 추가 플래그 없음) stream_id.to_bytes(4, 'big', signed=False)[:3] # 31비트 스트림 ID b'\x00\x00\x00' # 의사 헤더 (단순화, END_STREAM 플래그 없음) header_block ) return frame def build_data_frame(stream_id, data): """데이터 프레임 빌드""" length = len(data) frame = ( length.to_bytes(3, 'big') + b'\x03' # TYPE=DATA (0x03) b'\x01' # FLAGS=END_STREAM (0x01) stream_id.to_bytes(4, 'big', signed=False)[:3] data ) return frame
2.4.2 연결 처리 로직
def handle_http2_connection(client_socket): try: # HTTP/2 서문 보내기 client_socket.sendall(b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n') # 클라이언트 프레임 읽기 (단순화된 처리, 첫 번째 프레임이 HEADERS 프레임이라고 가정) frame_header = client_socket.recv(9) if len(frame_header) != 9: return length = int.from_bytes(frame_header[:3], 'big') frame_type = frame_header[3] stream_id = int.from_bytes(frame_header[5:8], 'big') | (frame_header[4] << 24) if frame_type != 0x01: # 비-HEADERS 프레임 client_socket.close() return # 헤더 데이터 읽기 (단순화된 처리, HPACK 파싱 안 함) header_data = client_socket.recv(length - 5) # 의사 헤더 길이 빼기 headers = {line.split(b':', 1)[0].decode(): line.split(b':', 1)[1].decode().strip() for line in header_data.split(b'\r\n') if line} # 요청 경로 처리 path = headers.get(':path', '/') if path == '/hello': response_headers = { ':status': '200', 'content-type': 'text/plain', 'content-length': '13' } response_data = b'Hello, HTTP/2.0!' else: response_headers = {':status': '404'} response_data = b'Not Found' # 응답 프레임 보내기 headers_frame = build_headers_frame(stream_id, response_headers) data_frame = build_data_frame(stream_id, response_data) client_socket.sendall(headers_frame + data_frame) except Exception as e: print(f"HTTP/2 Error: {e}") finally: client_socket.close()
2.5 구현 제한 사항
- HPACK 압축이 구현되지 않음: 표준 HTTP/2와 달리 일반 텍스트 헤더를 직접 전송합니다.
- 단일 스트림 처리: 각 연결은 하나의 스트림만 처리하고 멀티플렉싱을 구현하지 않습니다.
- 단순화된 프레임 파싱:
HEADERS
및DATA
프레임만 처리하고 오류 프레임, 설정 프레임 등을 처리하지 않습니다.
3. WebSocket 프로토콜 구현
3.1 WebSocket 프로토콜 개요
WebSocket은 HTTP 핸드셰이크를 기반으로 연결을 설정하고 이진 프레임을 통해 전이중 통신을 구현합니다. 핵심 프로세스는 다음과 같습니다.
- HTTP 핸드셰이크: 클라이언트가 업그레이드 요청을 보내고 서버가 프로토콜 전환을 확인합니다.
- 프레임 통신: 특정 형식의 이진 프레임을 사용하여 데이터를 전송하고 텍스트, 이진 및 닫기와 같은 작업을 지원합니다.
3.2 핸드셰이크 프로토콜 구현
3.2.1 핸드셰이크 요청 파싱
import base64 import hashlib def parse_websocket_handshake(data): headers = {} lines = data.decode('utf-8').split('\r\n') for line in lines[1:]: # 요청 라인 건너뛰기 if not line: break key, value = line.split(': ', 1) headers[key.lower()] = value return { 'sec_websocket_key': headers.get('sec-websocket-key'), 'origin': headers.get('origin') }
3.2.2 핸드셰이크 응답 생성
def build_websocket_handshake_response(key): guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" hash_data = (key + guid).encode('utf-8') sha1_hash = hashlib.sha1(hash_data).digest() accept_key = base64.b64encode(sha1_hash).decode('utf-8') return ( "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" f"Sec-WebSocket-Accept: {accept_key}\r\n" "\r\n" ).encode('utf-8')
3.3 프레임 프로토콜 구현
3.3.1 프레임 구조
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Extended payload length continued, if payload len == 127 |
+/-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Masking-key, if MASK set |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Payload Data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
3.3.2 수신된 프레임 파싱
def parse_websocket_frame(data): if len(data) < 2: return None first_byte, second_byte = data[0], data[1] fin = (first_byte >> 7) & 0x01 opcode = first_byte & 0x0F mask = (second_byte >> 7) & 0x01 payload_len = second_byte & 0x7F if payload_len == 126: payload_len = int.from_bytes(data[2:4], 'big') offset = 4 elif payload_len == 127: payload_len = int.from_bytes(data[2:10], 'big') offset = 10 else: offset = 2 if mask: mask_key = data[offset:offset+4] offset += 4 payload = bytearray() for i, b in enumerate(data[offset:]): payload.append(b ^ mask_key[i % 4]) else: payload = data[offset:] return { 'fin': fin, 'opcode': opcode, 'payload': payload }
3.3.3 전송용 프레임 빌드
def build_websocket_frame(data, opcode=0x01): # Opcode 0x01은 텍스트 프레임을 나타냅니다. payload = data.encode('utf-8') if isinstance(data, str) else data payload_len = len(payload) frame = bytearray() frame.append(0x80 | opcode) # FIN=1, opcode 설정 if payload_len < 126: frame.append(payload_len) elif payload_len <= 0xFFFF: frame.append(126) frame.extend(payload_len.to_bytes(2, 'big')) else: frame.append(127) frame.extend(payload_len.to_bytes(8, 'big')) frame.extend(payload) return bytes(frame)
3.4 완전한 서버 측 구현
def handle_websocket_connection(client_socket): try: # 핸드셰이크 요청 읽기 handshake_data = client_socket.recv(1024) handshake = parse_websocket_handshake(handshake_data) if not handshake['sec_websocket_key']: return # 핸드셰이크 응답 보내기 response = build_websocket_handshake_response(handshake['sec_websocket_key']) client_socket.sendall(response) # 메시지 루프 시작 while True: frame_data = client_socket.recv(4096) if not frame_data: break frame = parse_websocket_frame(frame_data) if not frame: break if frame['opcode'] == 0x01: # 텍스트 프레임 message = frame['payload'].decode('utf-8') print(f"Received: {message}") response_frame = build_websocket_frame(f"Echo: {message}") client_socket.sendall(response_frame) elif frame['opcode'] == 0x08: # 닫기 프레임 break except Exception as e: print(f"WebSocket Error: {e}") finally: client_socket.close()
4. 프로토콜 비교 및 실제 권장 사항
4.1 프로토콜 기능 비교
기능 | HTTP/1.0 | HTTP/2.0 | WebSocket |
---|---|---|---|
연결 방법 | 짧은 연결 | 긴 연결 (멀티플렉싱) | 긴 연결 (전이중) |
프로토콜 형식 | 텍스트 | 이진 프레이밍 | 이진 프레임 |
일반적인 시나리오 | 단순 웹 요청 | 고성능 웹사이트 | 실시간 통신 (채팅, 푸시) |
헤더 처리 | 일반 텍스트 | HPACK 압축 | 압축 없음 (확장 가능) |
4.2 순수 소켓 구현의 한계 및 응용
- 제한 사항:
- 프로토콜 세부 사항 처리 안 함 (예: HTTP/2 흐름 제어, 오류 프레임).
- 성능 문제 (연결 풀링, 비동기 처리 부족).
- 보안 격차 (TLS 암호화 구현 안 됨).
- 응용 가치:
- 프로토콜의 기본 원칙을 배우고 요청-응답의 본질을 이해합니다.
- 경량 서비스 개발 (예: 임베디드 장치 통신).
- 디버깅 도구 개발 (사용자 정의 프로토콜 분석).
4.3 프로덕션 환경에 대한 권장 사항
- HTTP/1.0/2.0:
requests
(클라이언트),aiohttp
(서버) 또는h2
(HTTP/2 특정)와 같은 성숙한 라이브러리를 사용합니다. - WebSocket: 권장 라이브러리는 비동기 통신 및 표준 프로토콜을 지원하는
websockets
입니다. - 성능 최적화: 비동기 프레임워크(예:
asyncio
) 또는 HTTP 서버(예:uvicorn
)와 결합하여 동시성을 향상시킵니다.
5. 결론
순수 소켓을 사용하여 세 가지 프로토콜을 구현함으로써 네트워크 통신의 기본 메커니즘에 대한 심층적인 이해를 얻었습니다.
- HTTP/1.0은 간단한 시나리오에 적합한 기본 요청-응답 모델입니다.
- HTTP/2.0은 이진 프레이밍 및 멀티플렉싱을 통해 성능을 향상시키지만 구현 복잡성이 크게 증가합니다.
- WebSocket은 실시간 통신을 위한 효율적인 전이중 채널을 제공하며 최신 웹 애플리케이션에서 널리 사용됩니다.
실제 개발에서는 성숙한 라이브러리 및 프레임워크를 사용하는 데 우선순위를 두어야 하지만 수동 구현은 프로토콜에 대한 이해를 심화시키는 데 도움이 됩니다. 네트워크 프로토콜을 배우려면 사양 문서(예: RFC 2616, RFC 7540, RFC 6455)를 실제 디버깅과 결합하여 설계 개념 및 엔지니어링 구현을 점진적으로 마스터해야 합니다.
Leapcell: 최고의 서버리스 웹 호스팅
마지막으로, Python 서비스를 배포하기 위한 최고의 플랫폼인 **Leapcell**을 추천합니다.
🚀 즐겨 사용하는 언어로 빌드
JavaScript, Python, Go 또는 Rust로 간편하게 개발하세요.
🌍 무제한 프로젝트를 무료로 배포
사용한 만큼만 지불하세요. 요청도 없고, 요금도 없습니다.
⚡ 종량제, 숨겨진 비용 없음
유휴 요금 없이, 원활한 확장성만 제공합니다.
🔹 Twitter에서 팔로우하세요: @LeapcellHQ