4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

TCP。普段よく聞きますが、実際にコードで直接触ったことはありますか?ゲームサーバーやチャットサーバーの実装を通じてソケット通信を学んできましたが、生のTCPの仕組みをもっと深く理解したいと思い、実際にコードを書きながら学んだ内容をまとめた記事です。

前提知識

この記事では以下の前提知識が必要です

  • Go言語の基本的な文法
  • ネットワークの基本的な概念
  • 並行処理の基本的な理解

実装するもの

今回作成するアプリケーションは、以下の機能を持つシンプルなチャットサーバーです

  • 複数のクライアントが接続可能
  • 新しいクライアントの参加と退出を全員に通知
  • クライアント間でメッセージをブロードキャスト

サーバー側の実装

プレイヤー管理

server.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(): 通信が終了した後、接続を閉じます。

クライアント接続の処理とメッセージ処理

server.go
// クライアントの接続を処理する
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を割り当て、接続情報を保存します。

1. defer関数

deferは、関数が終了する直前に実行される遅延関数呼び出しを定義するキーワードです。

プレイヤーを削除

server.go
// プレイヤーを削除する
func removePlayer(id string) {
	mutex.Lock()
	defer mutex.Unlock()
	delete(players, id)
}

メッセージのブロードキャスト

server.go
// 他のプレイヤーにメッセージを送信する
func broadcastMessage(message string, senderID string) {
	mutex.Lock()
	defer mutex.Unlock()
	for id, player := range players {
		if id != senderID { // 送信者には送り返さない
			player.Conn.Write([]byte(message))
		}
	}
}

送信者以外の全クライアントにメッセージを送信する関数です。

サーバーのメイン処理

server.go
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接続をリッスンし、新しい接続が来るたびに別のゴルーチンで処理します。

1. net.Listen("tcp", ":8000")

  • サーバーがクライアント接続を待ち受けるためのリスナーを作成
  • 引数: プロトコル("tcp")とアドレス(":8000" は全てのインターフェースの8000番ポート)

listener.Accept(): 新しい接続を受け入れる メソッド
listener.Close(): リスナーを閉じる メソッド(Acceptメソッドを以降呼び出せない)

クライアントの実装

client.go
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

参考

4
2
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
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?