こんにちは!フリーランスエンジニアのこたろうです。
今回は、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
}
まとめ
実装のポイント:
- WebSocket接続の適切な管理
- エラーハンドリングと再接続機能
- メッセージのバリデーション
- UIの使いやすさ
これらの実装により、リアルタイムで双方向のコミュニケーションが可能なチャット機能を実現できます。