🌊

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

2024/06/18に公開

基本知識

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

参考資料

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

TCP/IPとは?通信プロトコルの階層モデルを図解で解説

ソケットとは

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

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

ソケット通信とは

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

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

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

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

  • TCP/IPでは:
  • 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

    • ライブラリのインポート

      package main
      
      import (
      	"fmt"
      	"log"
      	"net/http"
      
      	"github.com/gorilla/websocket"
      )
      
    • WebSocketのアップグレーダ

      この処理では、HTTP接続からWebSocket接続に変換する役割を持っている。

      webソケットは、コネクションの確立はHTTPを使い通信の確率以降はストリームで通信するので、リクエストレスポンスからリアルタイムに切り替えるためにこの処理が必要。

      var upgrader = websocket.Upgrader{
      	ReadBufferSize:  1024,
      	WriteBufferSize: 1024,
      	CheckOrigin: func(r *http.Request) bool {
      		return true
      	},
      }
      
    • Client構造体

      各webソケットクライアントを管理するための構造体。

      • conn:WebSocket接続。
      • send:メッセージを送信するためのチャネル。
      type Client struct {
      	conn *websocket.Conn
      	send chan []byte
      }
      
    • Hub構造体

      • Hub:接続されているクライアントを管理し、メッセージをブロードキャストするための構造体。
      • clients:接続されているクライアントのマップ。
      • broadcast:メッセージをブロードキャストするためのチャネル。
      • register:新しいクライアントを接続するためのチャネル。
      • unregister:クライアントを接続解除するためのチャネル。
      • newHub:新しいハブを作成する関数。
      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メソッド

      これが上記ハブを実行する無限ループ。

      リアルタイムな双方向通信なので、メッセージの送受信を常に行い続けている状態。

      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ソケット周辺の管理を行う。

    • 接続を通じたデータの書き込み、読み込みと、さっき出てきたアップグレーダを使った接続の切り替え、クライアントのハブへの登録。

      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関数

      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

    // 開発用
    // 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

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

Discussion