web socketを使用してリアルタイムチャットを作成する

## 基本知識

ソケット、Webソケットの基礎的な部分について軽くまとめる。

### 参考資料

以下のページが個人的には綺麗に整理されている印象。

[TCP/IPとは？通信プロトコルの階層モデルを図解で解説](https://www.itmanage.co.jp/column/tcp-ip-protocol/)

### ソケットとは

TCP/IPの層よりも上位レイヤのプログラミング実装が、TCP/IPにアクセスするために必要なのが「ソケット」というインターフェース。
`（アプリケーション層：HTTP, FTP, POP3…）`

アプリケーション層とトランスポート層のインターフェースになるコンポーネント。

### ソケット通信とは

上記の「ソケット」以下の層、webエンジニアの視点から見ると、TCP/IP層の技術を利用して通信を行う事を指す。

また、それに準じて何らかの実装を行う事を「ソケットプログラミング」と呼ぶらしい。

### TCP/IP層とHTTP層の大きな違い

通信のレイヤが異なる。「どの程度通信が抽象化」されているかが異なる。

- TCP/IPでは：
    - トランスポート層とネットワーク層のプロトコルである。
    - データはバイトストリームとして扱う。
        💡 バイトストリーム：        
        [バイトストリームとは - IT用語辞典](https://e-words.jp/w/バイトストリーム.html)
- HTTPでは：
    - アプリケーション層のプロトコルである。
    - データはテキスト形式（HTML、JSON、XMLなど）で扱われる。

整理すると、

HTTPでは、通信方式がプロトコルによって高レベルに標準化、抽象化されているため、リクエスト/レスポンスのフォーマットに則ってより簡易的にアプリケーションの実装が可能となる。

TCP/IPではより低レベルのデータにアクセスが可能であるため、細かい制御が可能になるが、その分実装が必要な量、考慮範囲が増える。

## 実際に作成してみる

簡単なチャットアプリケーションを作成してみる。

https://github.com/T-unity/socket

### 実装内容

- サーバー側（Go
    
    Githubに上がっているmain.goが全てなので、ブロックに分けて解説していく。
    
    https://github.com/T-unity/socket/blob/main/web_socket/server/main.go
    
    - ライブラリのインポート
        
        ```go
        package main
        
        import (
        	"fmt"
        	"log"
        	"net/http"
        
        	"github.com/gorilla/websocket"
        )
        ```
        
    - WebSocketのアップグレーダ
        
        この処理では、HTTP接続からWebSocket接続に変換する役割を持っている。
        
        webソケットは、コネクションの確立はHTTPを使い通信の確率以降はストリームで通信するので、リクエストレスポンスからリアルタイムに切り替えるためにこの処理が必要。
        
        ```go
        var upgrader = websocket.Upgrader{
        	ReadBufferSize:  1024,
        	WriteBufferSize: 1024,
        	CheckOrigin: func(r *http.Request) bool {
        		return true
        	},
        }
        ```
        
    - Client構造体
        
        各webソケットクライアントを管理するための構造体。
        
        - **`conn`**：WebSocket接続。
        - **`send`**：メッセージを送信するためのチャネル。
        
        ```go
        type Client struct {
        	conn *websocket.Conn
        	send chan []byte
        }
        ```
        
    - Hub構造体
        - **`Hub`**：接続されているクライアントを管理し、メッセージをブロードキャストするための構造体。
        - **`clients`**：接続されているクライアントのマップ。
        - **`broadcast`**：メッセージをブロードキャストするためのチャネル。
        - **`register`**：新しいクライアントを接続するためのチャネル。
        - **`unregister`**：クライアントを接続解除するためのチャネル。
        - **`newHub`**：新しいハブを作成する関数。
        
        ```go
        type Hub struct {
        	clients    map[*Client]bool
        	broadcast  chan []byte
        	register   chan *Client
        	unregister chan *Client
        }
        
        func newHub() *Hub {
        	return &Hub{
        		clients:    make(map[*Client]bool),
        		broadcast:  make(chan []byte),
        		register:   make(chan *Client),
        		unregister: make(chan *Client),
        	}
        }
        ```
        
    - runメソッド
        
        これが上記ハブを実行する無限ループ。
        
        リアルタイムな双方向通信なので、メッセージの送受信を常に行い続けている状態。
        
        ```go
        func (h *Hub) run() {
        	for {
        		select {
        		case client := <-h.register:
        			h.clients[client] = true
        			log.Printf("Client registered: %v", client.conn.RemoteAddr())
        		case client := <-h.unregister:
        			if _, ok := h.clients[client]; ok {
        				delete(h.clients, client)
        				close(client.send)
        				log.Printf("Client unregistered: %v", client.conn.RemoteAddr())
        			}
        		case message := <-h.broadcast:
        			log.Printf("Broadcasting message: %s", message)
        			for client := range h.clients {
        				select {
        				case client.send <- message:
        					log.Printf("Sent message to client: %v", client.conn.RemoteAddr())
        				default:
        					close(client.send)
        					delete(h.clients, client)
        					log.Printf("Failed to send message, client removed: %v", client.conn.RemoteAddr())
        				}
        			}
        		}
        	}
        }
        ```
        
- webソケットハンドラ
    
    webソケット周辺の管理を行う。
    
    - 接続を通じたデータの書き込み、読み込みと、さっき出てきたアップグレーダを使った接続の切り替え、クライアントのハブへの登録。
        
        ```go
        func (c *Client) readPump(h *Hub) {
        	defer func() {
        		h.unregister <- c
        		c.conn.Close()
        	}()
        	for {
        		_, message, err := c.conn.ReadMessage()
        		if err != nil {
        			log.Println("Read error:", err)
        			break
        		}
        		log.Printf("Message received: %s", message)
        		h.broadcast <- message
        	}
        }
        
        func (c *Client) writePump() {
        	for message := range c.send {
        		err := c.conn.WriteMessage(websocket.TextMessage, message)
        		if err != nil {
        			log.Println("Write error:", err)
        			break
        		}
        		log.Printf("Message sent: %s", message)
        	}
        }
        
        func wsHandler(h *Hub, w http.ResponseWriter, r *http.Request) {
        	conn, err := upgrader.Upgrade(w, r, nil)
        	if err != nil {
        		log.Println("Upgrade error:", err)
        		return
        	}
        	client := &Client{conn: conn, send: make(chan []byte, 256)}
        	h.register <- client
        
        	go client.writePump()
        	client.readPump(h)
        }
        ```
        
    - main関数
        
        ```go
        func main() {
        	hub := newHub()
        	go hub.run()
        
        	http.Handle("/ws", addCSP(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        		wsHandler(hub, w, r)
        	})))
        	fmt.Println("Server started on :8080")
        	log.Fatal(http.ListenAndServe(":8080", nil))
        }
        ```
        
- クライアント側（JS
    
    https://github.com/T-unity/socket/blob/main/web_socket/client/main.js
    
    [WebSocket クライアントアプリケーションの記述 - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications)
    
    ```jsx
    // 開発用
    // const socket = new WebSocket('ws://localhost:8080/ws');
    
    // 本番用 http / https
    const socket = new WebSocket('ws://socket.gynga.org/ws');
    // const socket = new WebSocket('wss://socket.gynga.org/ws');
    
    socket.onopen = function(event) {
        console.log('WebSocket is open now.');
        document.getElementById('messages').innerHTML += '<p class="system">WebSocket is open now.</p>';
    };
    
    socket.onmessage = function(event) {
        console.log('Message from server:', event.data);
        document.getElementById('messages').innerHTML += `<p class="message">${event.data}</p>`;
    };
    
    socket.onerror = function(error) {
        console.log('WebSocket Error:', error);
        document.getElementById('messages').innerHTML += '<p class="error">WebSocket Error.</p>';
    };
    
    socket.onclose = function(event) {
        console.log('WebSocket is closed now.');
        document.getElementById('messages').innerHTML += '<p class="system">WebSocket is closed now.</p>';
    };
    
    function sendMessage() {
        const message = document.getElementById('messageInput').value;
        socket.send(message);
        document.getElementById('messages').innerHTML += `<p class="sent">Sent: ${message}</p>`;
        document.getElementById('messageInput').value = '';
    }
    ```
    

## まとめ

動作としては面白いけど、あまり活用する場面はなさそう。
為替のチャート表示とかで使われてるのかな？
ちなみに、webソケットはもう古くてwebトランスポートの方がいけているらしい。

[WebSocketの次の技術！？WebTransportについての解説とチュートリアル - Qiita](https://qiita.com/yuki_uchida/items/d9de148bb2ee418563cf)

webRTCを使ってビデオ通話とか作ってみたいな。