1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Go/React】WebSocketでリアルタイムメッセージングを実装する

Posted at

こんにちは!フリーランスエンジニアのこたろうです。
今回は、GinとReactを使用したWebSocketによるリアルタイムメッセージング機能の実装方法について解説します。

1. バックエンド実装(Gin)

WebSocketハンドラーの作成

// handlers/websocket.go
package handlers

import (
    "github.com/gin-gonic/gin"
    "github.com/gorilla/websocket"
    "log"
    "sync"
)

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return true  // 開発環境用の設定
    },
}

type WebSocketHandler struct {
    // クライアントの管理
    clients    map[*websocket.Conn]bool
    clientsMux sync.Mutex
}

func NewWebSocketHandler() *WebSocketHandler {
    return &WebSocketHandler{
        clients: make(map[*websocket.Conn]bool),
    }
}

func (h *WebSocketHandler) HandleWebSocket(c *gin.Context) {
    // WebSocketにアップグレード
    conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
    if err != nil {
        log.Printf("Websocket upgrade failed: %v", err)
        return
    }

    // クライアントの登録
    h.clientsMux.Lock()
    h.clients[conn] = true
    h.clientsMux.Unlock()

    // クライアント切断時の処理
    defer func() {
        h.clientsMux.Lock()
        delete(h.clients, conn)
        h.clientsMux.Unlock()
        conn.Close()
    }()

    // メッセージの受信ループ
    for {
        var msg Message
        err := conn.ReadJSON(&msg)
        if err != nil {
            if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
                log.Printf("websocket error: %v", err)
            }
            break
        }

        // 全クライアントにブロードキャスト
        h.broadcastMessage(msg)
    }
}

func (h *WebSocketHandler) broadcastMessage(msg Message) {
    h.clientsMux.Lock()
    defer h.clientsMux.Unlock()

    for client := range h.clients {
        err := client.WriteJSON(msg)
        if err != nil {
            log.Printf("broadcast error: %v", err)
            client.Close()
            delete(h.clients, client)
        }
    }
}

ルーティングの設定

// main.go
func main() {
    r := gin.Default()
    
    wsHandler := handlers.NewWebSocketHandler()
    
    r.GET("/ws", wsHandler.HandleWebSocket)
    
    r.Run(":8080")
}

2. フロントエンド実装(React)

WebSocketコンポーネント

// components/Chat.tsx
import React, { useEffect, useState } from 'react';

interface Message {
    id: string;
    content: string;
    sender: string;
    timestamp: string;
}

export const Chat: React.FC = () => {
    const [messages, setMessages] = useState<Message[]>([]);
    const [ws, setWs] = useState<WebSocket | null>(null);
    const [inputMessage, setInputMessage] = useState('');

    useEffect(() => {
        // WebSocket接続の確立
        const socket = new WebSocket('ws://localhost:8080/ws');

        socket.onopen = () => {
            console.log('Connected to WebSocket');
        };

        socket.onmessage = (event) => {
            const message = JSON.parse(event.data);
            setMessages(prev => [...prev, message]);
        };

        socket.onclose = () => {
            console.log('Disconnected from WebSocket');
        };

        setWs(socket);

        // クリーンアップ
        return () => {
            socket.close();
        };
    }, []);

    const sendMessage = (e: React.FormEvent) => {
        e.preventDefault();
        if (!ws || !inputMessage.trim()) return;

        const message = {
            content: inputMessage,
            sender: 'User',
            timestamp: new Date().toISOString(),
        };

        ws.send(JSON.stringify(message));
        setInputMessage('');
    };

    return (
        <div className="chat-container">
            <div className="messages">
                {messages.map((msg, index) => (
                    <div key={index} className="message">
                        <span className="sender">{msg.sender}</span>
                        <span className="content">{msg.content}</span>
                        <span className="timestamp">
                            {new Date(msg.timestamp).toLocaleTimeString()}
                        </span>
                    </div>
                ))}
            </div>
            <form onSubmit={sendMessage} className="input-form">
                <input
                    type="text"
                    value={inputMessage}
                    onChange={(e) => setInputMessage(e.target.value)}
                    placeholder="メッセージを入力..."
                />
                <button type="submit">送信</button>
            </form>
        </div>
    );
};

スタイリング

/* styles/Chat.css */
.chat-container {
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
}

.messages {
    height: 500px;
    overflow-y: auto;
    border: 1px solid #ccc;
    padding: 10px;
    margin-bottom: 20px;
}

.message {
    margin-bottom: 10px;
    padding: 10px;
    background-color: #f5f5f5;
    border-radius: 5px;
}

.sender {
    font-weight: bold;
    margin-right: 10px;
}

.timestamp {
    color: #666;
    font-size: 0.8em;
    margin-left: 10px;
}

.input-form {
    display: flex;
    gap: 10px;
}

input {
    flex: 1;
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
}

button {
    padding: 10px 20px;
    background-color: #007bff;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
}

button:hover {
    background-color: #0056b3;
}

3. エラーハンドリングと再接続機能

再接続機能の実装

// hooks/useWebSocket.ts
import { useState, useEffect, useCallback } from 'react';

const RECONNECT_INTERVAL = 3000; // 3秒

export const useWebSocket = (url: string) => {
    const [ws, setWs] = useState<WebSocket | null>(null);
    const [isConnected, setIsConnected] = useState(false);

    const connect = useCallback(() => {
        const socket = new WebSocket(url);

        socket.onopen = () => {
            setIsConnected(true);
            console.log('Connected to WebSocket');
        };

        socket.onclose = () => {
            setIsConnected(false);
            console.log('Disconnected from WebSocket');
            // 再接続
            setTimeout(() => {
                connect();
            }, RECONNECT_INTERVAL);
        };

        socket.onerror = (error) => {
            console.error('WebSocket error:', error);
        };

        setWs(socket);
    }, [url]);

    useEffect(() => {
        connect();
        return () => {
            if (ws) {
                ws.close();
            }
        };
    }, [connect]);

    const sendMessage = useCallback((message: any) => {
        if (ws && isConnected) {
            ws.send(JSON.stringify(message));
        }
    }, [ws, isConnected]);

    return { ws, isConnected, sendMessage };
};

4. セキュリティ対策

バックエンドでのバリデーション

// handlers/websocket.go
func (h *WebSocketHandler) validateMessage(msg Message) error {
    if len(msg.Content) > 1000 {
        return errors.New("message too long")
    }
    if msg.Content == "" {
        return errors.New("message cannot be empty")
    }
    return nil
}

// メッセージ送信前にバリデーション
if err := h.validateMessage(msg); err != nil {
    conn.WriteJSON(gin.H{"error": err.Error()})
    continue
}

まとめ

実装のポイント:

  1. WebSocket接続の適切な管理
  2. エラーハンドリングと再接続機能
  3. メッセージのバリデーション
  4. UIの使いやすさ

これらの実装により、リアルタイムで双方向のコミュニケーションが可能なチャット機能を実現できます。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?