Gorilla WebSocketを使ってターミナル上で動くチャットツールを作る

この記事はHRBrain Advent Calendar 2024 23日目の記事です。

https://qiita.com/advent-calendar/2024/hrbrain

## はじめに
最近今年初の鍋を食べた[yakiniku0220](https://twitter.com/yakiniku040220)です。
好きな鍋はもつ鍋ときりたんぽです。

今回はGorilla WebSocketを使ってターミナル上で動くチャットツールを作ってみました。
自分はGoを始めてまだ1年が経っていないため勉強も兼ねて今回作ってみました。

## デモ
実際の画面です。

![output1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271450/2c577d6e-9e36-db8f-8abd-60e90b936419.gif)

このような感じでクライアント同士でのチャットをターミナル上でできます。

client側とserver側でどういう処理をしているかの説明です。

まず初めにserverディレクトリのmain.go側の説明です。

こちらはWebSocketの設定と接続管理をやってます。
```server/main.go
var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
}
```

次に接続処理です。
```server/main.go
func handleConnections(w http.ResponseWriter, r *http.Request) {
	ws, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Printf("Error upgrading connection: %v", err)
		return
	}
	defer ws.Close()

	clients[ws] = true
	log.Println("New client connected")

	for {
		var msg string
		err := ws.ReadJSON(&msg)
		if err != nil {
			log.Printf("Error reading message: %v", err)
			delete(clients, ws)
			break
		}
		broadcast <- msg
	}
}
```

```go
ws, err := upgrader.Upgrade(w, r, nil)
```
まず`gorilla/websocket`ライブラリのUpgrader構造体を使って、HTTP接続をWebSocket接続に切り替える処理をしています。
その後接続ができた場合、成功時のメッセージをlog.Printlnを使用して出力します。

```go
for {
    var msg string
    err := ws.ReadJSON(&msg)
    if err != nil {
        log.Printf("Error reading message: %v", err)
        delete(clients, ws)
        break
    }
    broadcast <- msg
}
```
次の処理ではクライアント側からのメッセージを取得し文字列に変換後、broadcastチャネルにメッセージを送信してます。

```go
func handleMessages() {
	for {
		msg := <-broadcast
		for client := range clients {
			err := client.WriteJSON(msg)
			if err != nil {
				log.Printf("Error sending message: %v", err)
				client.Close()
				delete(clients, client)
			}
		}
	}
}
```
こちらではメッセージの送信処理をしています。
broadcastチャネルからメッセージを受け取り、全クライアントに送信しています。

次にclient側です。

```client/main.go
func main() {
	serverAddr := "ws://localhost:8080/ws"
	conn, _, err := websocket.DefaultDialer.Dial(serverAddr, nil)
	if err != nil {
		log.Fatalf("Failed to connect to server: %v", err)
	}
	defer conn.Close()

	go func() {
		for {
			var msg string
			err := conn.ReadJSON(&msg)
			if err != nil {
				log.Printf("Error reading message: %v", err)
				break
			}
			fmt.Println("Message:", msg)
		}
	}()

	scanner := bufio.NewScanner(os.Stdin)
	fmt.Println("Enter messages (type 'exit' to quit):")
	for scanner.Scan() {
		text := scanner.Text()
		if text == "exit" {
			break
		}
		err := conn.WriteJSON(text)
		if err != nil {
			log.Printf("Error sending message: %v", err)
			break
		}
	}
}
```
クライアント側ではサーバー側に接続したのち、メッセージの受信/送信をしています。

## ユーザー名を付与してみる。
作ってみたコードだとどちらのクライアントがメッセージを送信しているのかが分かりにくくなっています。
なのでユーザー名を入力する処理を加えてみます。

```go
type Message struct {
	Username string `json:"username"`
	Text     string `json:"text"`
}

reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter your username: ")
username, _ := reader.ReadString('\n')
username = username[:len(username)-1]
```

msgで定義していた構造体を上記のように変更し、新たにユーザーを入力するメッセージを定義しました。

これで以下のような挙動になりました。

![output2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271450/d144ea71-a038-39a1-f876-10122f8be83d.gif)

## まとめ
ターミナル上でのチャットツールを作ってみましたが、以前紹介したGUIと併用すればGUI上でのチャットツールも作れると思うので、色々とできそうかと思いました。

今回作ったコードは[こちら](https://github.com/ebarakazuhiro/blog/tree/main/20241223)です。
GUIのブログは[こちら](https://qiita.com/yakiniku0220/items/af1faa6228ad69d0b525)です。

## 最後に
株式会社HRBrainでは新しいメンバーを募集中です。
興味がある方は下記のリンクから宜しくお願い致します。

https://www.hrbrain.co.jp/recruit