はじめに
TCP。普段よく聞きますが、実際にコードで直接触ったことはありますか?ゲームサーバーやチャットサーバーの実装を通じてソケット通信を学んできましたが、生のTCPの仕組みをもっと深く理解したいと思い、実際にコードを書きながら学んだ内容をまとめた記事です。
前提知識
この記事では以下の前提知識が必要です
- Go言語の基本的な文法
- ネットワークの基本的な概念
- 並行処理の基本的な理解
実装するもの
今回作成するアプリケーションは、以下の機能を持つシンプルなチャットサーバーです
- 複数のクライアントが接続可能
- 新しいクライアントの参加と退出を全員に通知
- クライアント間でメッセージをブロードキャスト
サーバー側の実装
プレイヤー管理
// プレイヤー情報を格納する構造体
type Player struct {
ID string
Conn net.Conn
}
var (
players = make(map[string]Player) // プレイヤーの管理
mutex = sync.Mutex{} // 排他的制御のためのMutex
playerID = 0 // プレイヤーIDを生成するカウンタ
)
このコードでは、接続されたクライアント(プレイヤー)を管理するための構造体と変数を定義しています。sync.Mutex
を使用して、並行アクセス時のデータ競合を防止します。
1. sync.Mutex
sync.Mutex は 「排他的制御」 を行うためのものです。
複数のゴルーチン(並行実行される処理)が共有データを操作する場合、データの競合(データが壊れたり予期しない値になること)が起こる可能性があります。
sync.Mutex を使うことで 1つのゴルーチンだけ がデータを操作できるようにします。
mutex.Lock()
: ロックをかけて、他のゴルーチンがデータに触れないようにする。
mutex.Unlock()
: ロックを解除して、他のゴルーチンにデータを操作する権限を渡す。
2. net.Conn
クライアントとの通信のために必要な接続情報の保持
conn.Write([]byte(message))
: message
(文字列)を []byte
に変換し、クライアントに送信します。
conn.Read(buffer)
: クライアントから送信されたデータを受け取ります。
conn.Close()
: 通信が終了した後、接続を閉じます。
クライアント接続の処理とメッセージ処理
// クライアントの接続を処理する
func handleClient(conn net.Conn) {
defer conn.Close()
// プレイヤーIDを割り当てる
mutex.Lock()
playerID++
id := fmt.Sprintf("Player%d", playerID)
players[id] = Player{ID: id, Conn: conn}
mutex.Unlock()
fmt.Printf("%s connected\n", id)
broadcastMessage(fmt.Sprintf("%s has joined the game!\n", id), id)
reader := bufio.NewReader(conn)
for {
// クライアントからのメッセージを読み取る
message, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("%s disconnected\n", id)
removePlayer(id)
broadcastMessage(fmt.Sprintf("%s has left the game.\n", id), "")
return
}
// メッセージを処理して他のプレイヤーに通知
message = strings.TrimSpace(message)
fmt.Printf("[%s]: %s\n", id, message)
broadcastMessage(fmt.Sprintf("[%s]: %s\n", id, message), id)
}
}
各クライアント接続に対して、一意のIDを割り当て、接続情報を保存します。
プレイヤーを削除
// プレイヤーを削除する
func removePlayer(id string) {
mutex.Lock()
defer mutex.Unlock()
delete(players, id)
}
メッセージのブロードキャスト
// 他のプレイヤーにメッセージを送信する
func broadcastMessage(message string, senderID string) {
mutex.Lock()
defer mutex.Unlock()
for id, player := range players {
if id != senderID { // 送信者には送り返さない
player.Conn.Write([]byte(message))
}
}
}
送信者以外の全クライアントにメッセージを送信する関数です。
サーバーのメイン処理
func main() {
listener, err := net.Listen("tcp", ":8000")
if err != nil {
log.Fatal("Server start error:", err)
}
defer listener.Close()
for {
conn, err := listener.Accept()
if err != nil {
log.Println("Connection error:", err)
continue
}
go handleClient(conn)
}
}
TCP接続をリッスンし、新しい接続が来るたびに別のゴルーチンで処理します。
クライアントの実装
func main() {
// サーバーに接続
conn, err := net.Dial("tcp", "localhost:8000")
if err != nil {
fmt.Println("Connection error:", err)
return
}
defer conn.Close()
// 受信を別ゴルーチンで処理
go func() {
for {
message, _ := bufio.NewReader(conn).ReadString('\n')
fmt.Print(message)
}
}()
// メッセージ送信ループ
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter message: ")
for {
text, _ := reader.ReadString('\n')
conn.Write([]byte(text))
}
}
クライアントは、受信と送信を別々のゴルーチンで処理することで、効率的な通信を実現しています。
コードの完成形
tree
.
├── client
│ └── client.go
├── go.mod
├── go.sum
└── server
└── server.go
server.go
package main
import (
"bufio"
"fmt"
"log"
"net"
"strings"
"sync"
)
// プレイヤー情報を格納する構造体
type Player struct {
ID string
Conn net.Conn
}
var (
players = make(map[string]Player) // プレイヤーの管理
mutex = sync.Mutex{} // 排他的制御のためのMutex
playerID = 0 // プレイヤーIDを生成するカウンタ
)
// クライアントの接続を処理する
func handleClient(conn net.Conn) {
defer conn.Close()
// プレイヤーIDを割り当てる
mutex.Lock()
playerID++
id := fmt.Sprintf("Player%d", playerID)
players[id] = Player{ID: id, Conn: conn}
mutex.Unlock()
fmt.Printf("%s connected\n", id)
broadcastMessage(fmt.Sprintf("%s has joined the game!\n", id), id)
reader := bufio.NewReader(conn)
for {
// クライアントからのメッセージを読み取る
message, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("%s disconnected\n", id)
removePlayer(id)
broadcastMessage(fmt.Sprintf("%s has left the game.\n", id), "")
return
}
// メッセージを処理して他のプレイヤーに通知
message = strings.TrimSpace(message)
fmt.Printf("[%s]: %s\n", id, message)
broadcastMessage(fmt.Sprintf("[%s]: %s\n", id, message), id)
}
}
// 他のプレイヤーにメッセージを送信する
func broadcastMessage(message string, senderID string) {
mutex.Lock()
defer mutex.Unlock()
for id, player := range players {
if id != senderID { // 送信者には送り返さない
player.Conn.Write([]byte(message))
}
}
}
// プレイヤーを削除する
func removePlayer(id string) {
mutex.Lock()
defer mutex.Unlock()
delete(players, id)
}
func main() {
// サーバーの起動
listener, err := net.Listen("tcp", ":8000")
if err != nil {
log.Fatal("Server start error:", err)
}
defer listener.Close()
fmt.Println("Game server listening on :8000...")
for {
conn, err := listener.Accept()
if err != nil {
log.Println("Connection error:", err)
continue
}
go handleClient(conn)
}
}
client.go
package main
import (
"bufio"
"fmt"
"net"
"os"
)
func main() {
// サーバーに接続
conn, err := net.Dial("tcp", "localhost:8000")
if err != nil {
fmt.Println("Connection error:", err)
return
}
defer conn.Close()
// 受信を別ゴルーチンで処理
go func() {
for {
message, _ := bufio.NewReader(conn).ReadString('\n')
fmt.Print(message)
}
}()
// メッセージ送信ループ
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter message: ")
for {
text, _ := reader.ReadString('\n')
conn.Write([]byte(text))
}
}
依存関係
go mod init demo
go mod tidy
実行
cd server
go run server.go
cd client
go run client.go
参考