純粋なPythonソケットで学ぶHTTP/2とWebSocketの完全ガイド

[![Group269.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4075813/fc1fe7f7-0bdd-44a4-ade5-6f6313f2b31c.png)](https://leapcell.io/)


**[Leapcell: 最高のサーバーレスWebホスティング](https://leapcell.io/)**

# 純粋なPythonソケットを用いたHTTP/1.0、HTTP/2.0、WebSocketプロトコルの実装

## はじめに
ネットワークプロトコルはインターネットの基盤となっています。HTTP/1.0、HTTP/2.0、WebSocketはそれぞれ異なるシナリオで現代的なWebアプリケーションをサポートしています。この記事では、純粋なPythonソケットを使用してこれら3つのプロトコルのコアロジックを実装し、その根底にある通信原理を深く理解することを目指します。記事内のすべてのサンプルコードはPython 3.8+環境で検証されており、ネットワークプログラミング、プロトコル解析、バイトストリーム処理などのコア技術を網羅しています。


## 1. HTTP/1.0プロトコルの実装
### 1.1 HTTP/1.0プロトコルの概要
HTTP/1.0はTCP接続に基づく初期のステートレスなリクエストレスポンスプロトコルです。デフォルトでは短い接続（各リクエスト後に接続を閉じる）を使用します。そのリクエストはリクエストライン、リクエストヘッダー、リクエストボディで構成され、レスポンスはステータスライン、レスポンスヘッダー、レスポンスボディで構成されます。

### 1.2 サーバー側の実装手順
#### 1.2.1 TCPソケットの作成
```python
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 リクエストデータの解析
正規表現を使用してリクエストラインとヘッダーを解析します：
```python
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 レスポンスデータの生成
```python
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 メイン処理ループ
```python
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の複雑さが高いため、この例では以下の機能を実装します：
1. `GET`リクエストヘッダーフレーム（HEADERSフレーム）とデータフレーム（DATAフレーム）を処理します。
2. HPACK圧縮を実装せず、生のヘッダーを直接送信します。
3. シングルストリーム処理で、多重化をサポートしません。

### 2.4 サーバー側のコード実装
#### 2.4.1 フレームコンストラクタ
```python
def build_headers_frame(stream_id, headers):
    """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):
    """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 接続処理ロジック
```python
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とは異なります。
- **シングルストリーム処理**：各接続で1つのストリームのみを処理し、多重化を実装していません。
- **簡易フレーム解析**：`HEADERS`と`DATA`フレームのみを処理し、エラーフレーム、設定フレームなどは処理しません。


## 3. WebSocketプロトコルの実装
### 3.1 WebSocketプロトコルの概要
WebSocketはHTTPハンドシェイクに基づいて接続を確立し、その後バイナリフレームを通じて全二重通信を実現します。そのコアプロセスは以下の通りです：
1. HTTPハンドシェイク：クライアントがアップグレードリクエストを送信し、サーバーがプロトコル切り替えを確認します。
2. フレーム通信：特定の形式のバイナリフレームを使用してデータを送信し、テキスト、バイナリ、クローズなどの操作をサポートします。

### 3.2 ハンドシェイクプロトコルの実装
#### 3.2.1 ハンドシェイクリクエストの解析
```python
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 ハンドシェイクレスポンスの生成
```python
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 受信フレームの解析
```python
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 送信フレームの構築
```python
def build_websocket_frame(data, opcode=0x01):  # オペコード0x01はテキストフレームを示す
    payload = data.encode('utf-8') if isinstance(data, str) else data
    payload_len = len(payload)
    frame = bytearray()
    
    frame.append(0x80 | opcode)  # FIN=1, オペコードを設定
    
    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 完全なサーバー側実装
```python
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         |
|----------------------|----------------|------------------|-------------------|
| 接続方法             | 短い接続       | 長い接続（多重化） | 長い接続（全二重） |
| プロトコル形式       | テキスト       | バイナリフレーミング | バイナリフレーム   |
| 典型的なシナリオ     | 単純なWebリクエスト | 高性能Webサイト   | リアルタイム通信（チャット、プッシュ） |
| ヘッダー処理         | 平文           | HPACK圧縮        | 圧縮なし（拡張可能） |

### 4.2 純粋なソケット実装の制限と応用
- **制限事項**：
  1. プロトコルの詳細を処理しません（例：HTTP/2のフロー制御、エラーフレーム）。
  2. パフォーマンスの問題（接続プーリング、非同期処理の欠如）。
  3. セキュリティのギャップ（TLS暗号化未実装）。
  
- **応用価値**：
  1. プロトコルの根底にある原理を学び、リクエストレスポンスの本質を理解する。
  2. 軽量サービスの開発（例：組み込みデバイス通信）。
  3. デバッグツールの開発（カスタムプロトコル解析）。

### 4.3 本番環境での推奨事項
- **HTTP/1.0/2.0**：`requests`（クライアント）、`aiohttp`（サーバー）、または`h2`（HTTP/2専用）などの成熟したライブラリを使用します。
- **WebSocket**：推奨ライブラリは`websockets`で、非同期通信と標準プロトコルをサポートしています。
- **パフォーマンス最適化**：非同期フレームワーク（例：`asyncio`）またはHTTPサーバー（例：`uvicorn`）と組み合わせて並行性を向上させます。


## 5. まとめ
純粋なソケットを使用して3つのプロトコルを実装することで、ネットワーク通信の根底にあるメカニズムを深く理解することができました：
- HTTP/1.0は基本的なリクエストレスポンスモデルで、単純なシナリオに適しています。
- HTTP/2.0はバイナリフレーミングと多重化によってパフォーマンスを向上させますが、実装の複雑さが大幅に増加します。
- WebSocketはリアルタイム通信のための効率的な全二重チャネルを提供し、現代的なWebアプリケーションで広く使用されています。

実際の開発では、まず成熟したライブラリとフレームワークの使用を優先すべきですが、手動実装はプロトコルの理解を深めるのに役立ちます。ネットワークプロトコルの学習には、仕様書（例：[RFC 2616](https://tools.ietf.org/html/rfc2616)、[RFC 7540](https://tools.ietf.org/html/rfc7540)、[RFC 6455](https://tools.ietf.org/html/rfc6455)）と実践的なデバッグを組み合わせて、徐々にその設計コンセプトとエンジニアリング実装を習得する必要があります。

# [Leapcell: 最高のサーバーレスWebホスティング](https://leapcell.io/)

最後に、Pythonサービスをデプロイするための最高のプラットフォームをお勧めします：**[Leapcell](https://leapcell.io/)**

[![brandpic7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4075813/0a186ad2-b69f-479f-a6eb-4648f427b005.png)](https://leapcell.io/)


### 🚀 好きな言語で構築
JavaScript、Python、Go、またはRustで簡単に開発できます。

### 🌍 無制限のプロジェクトを無料でデプロイ
使用した分だけ支払う—リクエストがなければ料金は発生しません。

### ⚡ 従量課金、隠れたコストはありません
アイドル料金はなく、シームレスなスケーラビリティが保証されます。

[![Frame3-withpadding2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4075813/92b38fd6-b1d1-4020-a772-8032cc47b787.png)](https://leapcell.io/)


[📖 ドキュメントを探索する](https://docs.leapcell.io/)

🔹 Twitterでフォロー：[@LeapcellHQ](https://x.com/LeapcellHQ)