Go言語チャットサーバー

 
 
このセクションでは、これまでに学んだ知識に基づいて、複数のユーザー間でテキスト メッセージをブロードキャストできるチャット サンプル プログラムを開発します。

 

サーバープログラム

サーバープログラムにはメインゴルーチンとブロードキャスト(ブロードキャスター)ゴルーチンの4つのゴルーチンが含まれており、各コネクションには接続処理(handleConn)ゴルーチンとクライアントライター(clientwriter)ゴルーチンが含まれています。

ブロードキャスターは 3 つの異なるメッセージに応答する必要があるため、select の使用方法の仕様です。

メインのゴルーチンの仕事は、ポートをリッスンし、接続しているクライアントからのネットワーク接続を受け入れ、接続ごとに新しい handleConn ゴルーチンを作成することです。

完全なサンプルコードは次のとおりです。

package main

import (
  "bufio"
  "fmt"
  "log"
  "net"
)

func main() {
  listener、err : = net.Listen( "tcp"、 "localhost:8000")
  if err!= nil {
    log.Fatal(err)
  }

  go broadcaster()
  for {
    conn、err : = listener.Accept()
    if err!= nil {
      log.Print(err)
      continue
    }
    go handleConn(conn)
  }
}

type client chan<- string // 外部に送信するメッセージのチャンネル

var (
  entering = make (chan client)
  leaving  = make (chan client)
  messages = make (chan string) // 全接続クライアント
)

func broadcaster () {
  clients := make (map [client] bool)
  for {
    select {
    case msg:= <-messages:
      // 受信したメッセージをすべてのクライアントにブロードキャストする
      // メッセージ送信チャンネル
      for cli:= range clients {
        cli <- msg
      }

    case cli:= <-entering:
      clients [cli] = true

    case cli:= <-leaving:
      delete (clients、 cli)
      close(cli)
    }
  }
}
func handleConn (conn net.Conn) {
  ch := make (chan string) // クライアントメッセージを外部に送信するチャネル
  go clientWriter (conn、 ch)

  who:= conn.RemoteAddr().String()
  ch <- "ようこそ、" + who
  messages <- who + "がオンラインになりました"
  entering <- ch

  input := bufio.NewScanner(conn)
  for input.Scan() {
    messages <- who + ":" + input.Text()
  }
  // 注意:input.Err()中の可能性のあるエラーを無視する

  leaving <- ch
  messages <- who + "がオフラインになりました"
  conn.Close()
}

func clientWriter (conn net.Conn、 ch<-chan string) {
  for msg : = range ch {
    fmt.Fprintln (conn、 msg) // 注意:ネットワークレベルのエラーを無視してください
  }
} 

コード中の main 関数に書かれているコードは非常に単純で、実際にサーバーが行うべきことは、リスナー オブジェクトを取得し、リンクから conn オブジェクトを継続的に取得し、最後にこれらのオブジェクトを処理リンクに投げることです。加工するための関数です。

handleConn メソッドを使用して conn オブジェクトを処理する場合、異なる接続の goroutine を開始して各 conn を同時に処理するため、待つ必要がありません。

すべてのオンライン ユーザーにメッセージを送信する必要があり、異なるユーザーの conn オブジェクトは異なるゴルーチン内にありますが、異なるゴルーチン間のメッセージ送信を処理するための Go 言語のチャネルがあるため、ここでは異なるゴルーチン内のチャネルを使用することを選択します。ブロードキャスト メッセージはゴルーチンで配信されます。

ブロードキャスターを紹介しましょう。ブロードキャスターは、ローカル変数 client を使用して、現在接続されているクライアントのコレクションを記録します。各クライアントに記録される唯一の情報は、送信メッセージ チャネルの ID です。詳細は次のとおりです:

 type client chan<- string // 送信用の外部チャネル

var (
    entering = make(chan client)
    leaving  = make(chan client)
    messages = make(chan string) // すべての接続されたクライアント用
)

func broadcaster() {
    clients := make(map[client]bool)
    for {
        select {
        case msg := <-messages:
            // 受信したすべてのメッセージを全てのクライアントにブロードキャストする
            // 送信用のチャネル
            for cli := range clients {
                cli <- msg
            }

        case cli := <-entering:
            clients[cli] = true

        case cli := <-leaving:
            delete(clients, cli)
            close(cli)
        }
    }
} 

main 関数では、ゴルーチンを使用して、すべてのユーザーが送信したメッセージをブロードキャストするブロードキャスト機能を開きます。

ここではユーザー クライアントを保存するためにディクショナリが使用されており、ディクショナリのキーは各接続によって宣言された一方向同時キューです。

select でマルチプレックスを開始します。

  • ブロードキャスト メッセージがメッセージから送信されるたびに、クライアントはループして内部の各チャネルにメッセージを送信します。
  • メッセージが入力から送信されるたびに、新しいキーと値が生成されます。これは、新しいクライアントをクライアントに追加するのと同じです。
  • メッセージが退出から送信されるたびに、キーと値のペアを削除し、対応するチャネルを閉じます。

各顧客独自の goroutine を見てみましょう。

handleConn 関数は、外部にメッセージを送信するための新しいチャネルを作成し、入力チャネルを通じて新しい顧客の到着をブロードキャスターに通知し、顧客から送信されたテキストの各行を読み取り、各行をブロードキャスターはグローバル受信メッセージ チャネルを通じて送信され、各メッセージには送信者 ID がプレフィックスとして付けられます。メッセージがクライアントから読み取られると、handleConn はクライアントに離脱チャネルを通じて離脱するよう通知し、接続を閉じます。

 func handleConn(conn net.Conn) {
    ch := make(chan string) // 外部に送信するクライアントのメッセージ用チャネル
    go clientWriter(conn, ch)

    who := conn.RemoteAddr().String()
    ch <- "ようこそ " + who +" さん"
    messages <- who + "が接続しました。"
    entering <- ch

    input := bufio.NewScanner(conn)
    for input.Scan() {
        messages <- who + ": " + input.Text()
    }
    // 注意:input.Err() 中の可能性のあるエラーを無視する

    leaving <- ch
    messages <- who + "が切断しました。"
    conn.Close()
}

func clientWriter(conn net.Conn, ch <-chan string) {
    for msg := range ch {
        fmt.Fprintln(conn, msg) // 注意:ネットワーク層面のエラーは無視する。
    }
} 

handleConn 関数は、処理される conn ごとに新しいチャネルを作成し、新しい goroutine を開始して、このチャネルに送信されたメッセージを conn に書き込みます。

handleConn 関数の実行プロセスは、次の手順のように簡単に要約できます。

  • 接続されている IP アドレスとポート番号を取得します。
  • ウェルカム情報をチャネルに書き込み、クライアントに返します。
  • ブロードキャスト メッセージを生成し、それをメッセージに書き込みます。
  • このチャネルをクライアント コレクションに追加します。つまり、「<- ch;」と入力します。
  • クライアントによって conn に書き込まれたデータをリッスンし、スキャンされるたびにこのメッセージをブロードキャスト チャネルに送信します。
  • クライアントが閉じられている場合は、キューを離れ、ブロードキャスト関数に Leave を書き込み、クライアントを削除してクライアントを閉じます。
  • クライアントが閉じていることを他のクライアントに通知するためにブロードキャストします。
  • 最後にクライアント接続 Conn.Close() を閉じます。

クライアントプログラム

サーバーについては前に簡単に紹介しましたが、クライアントについては以下で紹介します。ここでは「netcat.go」という名前です。完全なコードは次のとおりです。

 // netcatはシンプルなTCPサーバー読み書きクライアントです。
package main

import (
  "io"
  "log"
  "net"
  "os"
)

func main() {
  conn, err := net.Dial("tcp", "localhost:8000")
  if err != nil {
    log.Fatal(err)
  }
  done := make(chan struct{})
  go func() {
    io.Copy(os.Stdout, conn) // 注意:エラーは無視
    log.Println("完了")
    done <- struct{}{} // メインGoroutineに信号を送る
  }()
  mustCopy(conn, os.Stdin)
  conn.Close()
  <-done // バックグラウンドgoroutineの完了を待つ
}

func mustCopy(dst io.Writer, src io.Reader) {
  if _, err := io.Copy(dst, src); err != nil {
    log.Fatal(err)
  }
} 

n 個のクライアント セッションが接続されている場合、プログラムは相互に通信する2n+2ゴルーチンを同時に実行し、暗黙的なロック操作を必要としません。クライアント マップはブロードキャスターの 1 つのゴルーチンでアクセスできるように制限されているため、同時にアクセスすることはできません。複数のゴルーチンによって共有される唯一の変数は、チャネルと net.Conn のインスタンスであり、両方とも同時に安全です。

go buildコマンドを使用してサーバーとクライアントをコンパイルし、結果の実行可能ファイルを実行します。

以下の図は、同じコンピューター上で実行されているサーバーと 3 つのクライアントを示しています。

 
 

「 Go言語チャットサーバー」についてわかりやすく解説!絶対に観るべきベスト2動画

GO言語でAPI開発「 gRPC 」入門
Go言語で何ができるの?どこで使われてる?現役エンジニアが解説