こんにちは。KOUKIです。
Go言語で、簡単なチャットアプリを作ろう!というのが本記事の目的です。
<目次>
チャットアプリケーションについて
本記事で作成するチャットアプリケーションは、CLI上で動作します。
イメージ図は、以下の通りです。

一番右側のコマンドラインでは、チャットサーバーを起動しています。
それ以外のコマンドラインでは、メッセージのやり取りを行なっています。
使用技術について
- Go言語(go version go1.14 darwin/amd64)
- gRPC
gRPCについて
チャットアプリケーションでは、gRPCを使います。
gRPCの概要やセットアップは、以下の記事を参考にしてください。
WebSockets版
実装
プロジェクトの作成
1 2 3 4 5 6 7 8 9 10 11 12 |
mkdir golang-chatservice cd golang-chatservice mkdir chat touch chat/chat.proto mkdir server touch server/server.go touch server/main.go touch mkdir client touch client/client.go touch client/main.go touch generate.sh |
チャットサービスの作成
最初にgRPCにて、チャットサービスを作ります。
gRPCのサービスは、chat.protoファイルに記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
syntax = "proto3"; package chat; option go_package="chat"; service Chat { rpc Chat(stream ChatMessage) returns (stream ChatMessage) {} } message ChatMessage { string user = 1; string message = 2; } |
上記では、ユーザ名とメッセーを受け取れるChatMessageを定義しています。
また、Service Chatの引数としてstreamを指定しており、 複数データを送信できるようにしています。
protoファイルをコンパイルし、Goのコード内で使用できるようにするために、generate.shファイルに以下の記述をします。
1 2 3 4 |
#!/bin/bash export PATH=$PATH:/Users/user/go/bin/ protoc chat/chat.proto --go_out=plugins=grpc:. |
terminal上から以下のコマンドを実行し、protoファイルをコンパイルします。
1 2 3 4 |
$ chmod 777 generate.sh $ ./generate.sh $ ls chat/ chat.pb.go chat.proto |
コンパイルに成功すると「chat.pb.go」ファイルが自動で生成されます。
ChatServerの実装
ChatServerのコードを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
// server/server.go package main import ( "fmt" "io" "sync" "github.com/hoge/golang-chatservice/chat" ) // Connectionの状態を格納 type Connection struct { conn chat.Chat_ChatServer send chan *chat.ChatMessage quit chan struct{} } // Connectoinのコンストラクタ関数 func NewConnection(conn chat.Chat_ChatServer) *Connection { c := &Connection{ conn: conn, send: make(chan *chat.ChatMessage), quit: make(chan struct{}), } go c.start() return c } func (c *Connection) Close() error { close(c.quit) close(c.send) return nil } func (c *Connection) Send(msg *chat.ChatMessage) { defer func() { recover() }() c.send <- msg } func (c *Connection) start() { running := true for running { select { case msg := <-c.send: c.conn.Send(msg) case <-c.quit: running = false } } } // メッセージ取得 func (c *Connection) GetMessage(broadcast chan<- *chat.ChatMessage) error { for { msg, err := c.conn.Recv() if err == io.EOF { c.Close() return nil } else if err != nil { c.Close() return err } go func(msg *chat.ChatMessage) { fmt.Println("block?") select { case broadcast <- msg: case <-c.quit: } }(msg) } } // ChatServerの状態を格納 type ChatServer struct { broadcast chan *chat.ChatMessage quit chan struct{} connections []*Connection connLock sync.Mutex } // ChatServerのコンストラクタ func NewChatServer() *ChatServer { srv := &ChatServer{ broadcast: make(chan *chat.ChatMessage), quit: make(chan struct{}), } go srv.start() return srv } func (c *ChatServer) Close() error { close(c.quit) return nil } // ChatServer起動 func (c *ChatServer) start() { running := true for running { select { case msg := <-c.broadcast: c.connLock.Lock() for _, v := range c.connections { go v.Send(msg) } c.connLock.Unlock() case <-c.quit: running = false } } } /* chat.pb.go type ChatServer interface { Chat(Chat_ChatServer) error } Chatを実装しておかないとmain.goのRegisterChatServerでサーバーを登録できない */ func (c *ChatServer) Chat(stream chat.Chat_ChatServer) error { // コネクションを作成 conn := NewConnection(stream) // コネクションの排他制御 c.connLock.Lock() c.connections = append(c.connections, conn) c.connLock.Unlock() err := conn.GetMessage(c.broadcast) c.connLock.Lock() for i, v := range c.connections { if v == conn { c.connections = append(c.connections[:i], c.connections[i+1:]...) } } c.connLock.Unlock() return err } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// server/main.go package main import ( "fmt" "log" "net" "github.com/hoge/golang-chatservice/chat" "google.golang.org/grpc" ) func main() { lst, err := net.Listen("tcp", ":8080") if err != nil { log.Fatal(err) } // Server作成 server := grpc.NewServer() chatSrv := NewChatServer() // Chatサーバー登録 chat.RegisterChatServer(server, chatSrv) fmt.Println("Chatサーバーを8080番ポートで起動します。") log.Fatal(server.Serve(lst)) } |
Clientの実装
client側を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
// client/client.go package main import ( "bufio" "fmt" "io" "log" "os" "github.com/hoge/golang-chatservice/chat" ) var waitc = make(chan struct{}) // コマンドライン引数のバリデーションチェック func argsValidate() bool { if len(os.Args) != 3 { fmt.Println("第一引数: URL, 第二引数: ユーザー名が必要です。") return false } return true } // streamで送信されてくるメッセージを受け取り表示する func streamRecv(stream chat.Chat_ChatClient) { for { msg, err := stream.Recv() if err == io.EOF { close(waitc) return } else if err != nil { log.Fatal(err) } fmt.Println(msg.User + ": " + msg.Message) } } // コネクション確立 func connectEstablish(stream chat.Chat_ChatClient) { fmt.Println("コネクションが確立しました。" + "\"quit\"を押下するか\"ctrl+c\"にてプログラムを停止できます。") scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { msg := scanner.Text() if msg == "quit" { err := stream.CloseSend() if err != nil { log.Fatal(err) } break } err := stream.Send(&chat.ChatMessage{ User: os.Args[2], Message: msg, }) if err != nil { log.Fatal(err) } } <-waitc } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
// client/main.go package main import ( "context" "log" "os" "github.com/hoge/golang-chatserver/chat" "google.golang.org/grpc" ) func init() { log.SetPrefix("Client: ") } func main() { // コマンドライン引数のバリデーション ok := argsValidate() if !ok { return } // contextを作成 ctx := context.Background() // grpc接続ダイアル設定 conn, err := grpc.Dial(os.Args[1], grpc.WithInsecure()) if err != nil { log.Fatal(err) } defer conn.Close() c := chat.NewChatClient(conn) stream, err := c.Chat(ctx) if err != nil { log.Fatal(err) } // メッセージ受付処理実行 go streamRecv(stream) // コネクション確立 connectEstablish(stream) } |
実行
terminal(コマンドライン)を3つ立ち上げます。
1 2 3 4 5 6 7 |
# terminal1 chatサーバーを起動 $ pwd /Users/hoge/go/src/github.com/hoge/golang-chatservice/server # サーバー起動 $ go run main.go server.go |
1 2 3 4 5 6 7 |
# terminal2 client1を立ち上げ $pwd /Users/hoge/go/src/github.com/hoge/golang-chatservice/client # client起動 go run main.go client.go localhost:8080 User1 |
1 2 3 4 5 6 7 |
# terminal3 client2を立ち上げ $pwd /Users/hoge/go/src/github.com/hoge/golang-chatservice/client # client起動 go run main.go client.go localhost:8080 User2 |
まとめ
解説はほぼないですが、コードを見ていただければ、なんとなくわかると思います。
Go言語はこのようにgRPCと組み合わせることでChatアプリケーションを簡単に実装できます。
もっと色々と試して見たいですね。
それでは、また!
最近のコメント