Help us understand the problem. What is going on with this article?

RustとWebSocketでチャットルームを実現してみる

PONOS Advent Calendar 2019の22日目の記事です。

はじめに

PONOS Advent Calendar 2019の17日目の記事で、RustとWebSocketにUnityで通信する記事を書かせていただきました。
その記事の最後に「ゲームオブジェクトのXYZ座標を共有できるようなサーバ側、クライアント側の実装をしていきたいと思います。」と記述しましたが、WS-RSライブラリやC#のWebSocketの知識を深めるためにまずはチャットルームの実現とそれに接続するクライアントを作成してみようかと考えました。

実装

RustでWebSocketを実現したサーバとチャットツールは将来Unityでクライアントを実装すると考えて、C#(.NET Core)で実装したチャットクライアントを実装していきます。実装環境としてはローカル(macOS)で実施することを前提しています。

開発環境 バージョン
macOS 10.14.1
Rust(rustc、cargo) 1.39.0
WS-RS 0.9.1
C# 8.0
.NET Core 3.1.100

サーバ側の実装

サーバ側はRustを導入しなくてはいけませんが、導入する方法は17日目の記事で記載しておりますので、今回は省略します。

  1. 必要なパッケージを導入する

    Cargo.toml
    [dependencies]
    env_logger = "0.7.1"
    ws = "0.9.1"
    chrono = "0.4.10
    
  2. WS-RSライブラリを使用したメッセージを配信する仕組みを実装する

    main.rs
    extern crate ws;
    extern crate env_logger;
    
    use chrono::{Local, DateTime};
    use ws::{listen, CloseCode, Sender, Handler, Handshake, Message, Result};
    
    fn main() {
        // ロガーの初期化
        env_logger::init();
    
        // WebSocketの開始
        listen("127.0.0.1:3012", |out| {
            Server { 
                out: out,
                user_name: String::new()
            }
        }).unwrap();
    
        struct Server {
            out: Sender,
            user_name: String
        }
    
        impl Handler for Server {
    
            // WebSocketとのコネクション接続を開始した場合
            fn on_open(&mut self, handshake: Handshake) -> Result<()> {
                let hashed_key: String = handshake.request.hashed_key().unwrap();
                let headers: &Vec<(String, Vec<u8>)> = handshake.request.headers();
    
                // ヘッダーで送信されてきたユーザ名を取得する
                for (k, v) in headers {
                    if k == "User-Name" {
                        self.user_name = String::from_utf8(v.to_vec()).unwrap();
                    }
                }
    
                // ログイン情報を接続している全てのクライアントに配信する
                println!("[{}] {} Connected. hash_key: {}", str_datetime(), self.user_name, hashed_key);
                let send_message: String = format!("[{}] {} Join the Chat Room.", str_datetime(), self.user_name);
                return self.out.broadcast(send_message);
            }
    
            // メッセージを受信した場合
            fn on_message(&mut self, message: Message) -> Result<()> {
                // 受信したメッセージを接続している全てのクライアントに配信する
                let send_message: String = format!("[{}] {}: {}", str_datetime(), self.user_name, message);
                println!("{}", send_message);
                return self.out.broadcast(send_message);
            }
    
            // WebSocketとのコネクション接続が閉じた場合
            fn on_close(&mut self, code: CloseCode, reason: &str) {
                // ログイン情報を接続している全てのクライアントに配信する
                println!("[{}] {} Disconnected for ({:?}) {}", str_datetime(), self.user_name, code, reason);
                let send_message: String = format!("[{}] {} Left the Chat Room.", str_datetime(),self.user_name);
                let _ = self.out.broadcast(send_message);
            }
        }
    
        // 日付の文字列を取得
        fn str_datetime() -> String {
            // メッセージに日付を付与
            let local_datetime: DateTime<Local> = Local::now();
            let formatted_local_datetime: String = local_datetime.format("%Y-%m-%d %T").to_string();
            return formatted_local_datetime;
        }
    }
    
  3. 実行

    $ cargo run
    

    ※ 実行した際に不足しているパッケージは自動でビルドされます。

クライアント側の実装

クライアント側は今回Unityではなく、.NET Coreで実装しようかと考えました。Unity側はC#ですので、その言語でコンソールアプリを実装できるフレームワークを選択しました。

  1. .NET Coreの導入
    Download .NETにある「Download .NET Core SDK」を押下し、パッケージをダウンロードする。そのパッケージを展開しインストールをする。

  2. 新規プロジェクトを作成

    $ dotnet new console -n ChatClient
    
  3. チャットツールの実装

    $ cd ChatClient
    $ vi src/Program.cs
    
    Program.cs
    using System;
    using System.Text;
    using System.Collections.Generic;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Net.WebSockets;
    
    namespace ChatClient
    {
        class Program
        {
            private const string clientName = "-- Chat Client for WebSocket --";
            private const string url = "ws://localhost:3012";
            private static List<string> messageList = new List<string>();
            private static ClientWebSocket webSocket = null;
            private static string userName = "";
    
            private static void Main(string[] args)
            {
                // 表示の初期化
                InitializeDisplay();
                if (String.IsNullOrWhiteSpace(userName)) {
                    Console.ForegroundColor = ConsoleColor.Red;
                    Console.WriteLine("Please tell me your name.");
                    Console.ResetColor();
                    return;
                }
                // WebSocketの接続(接続するまで待つ)
                Connect(url).Wait();
    
                // メッセージの受信する機構を非同期で実行
                ReceiveMessage();
                // メッセージを送信する機構を非同期で実行
                SendMessage();
    
                // メッセージを送信ループから抜けたらWebSocketの接続を閉じる
                if (webSocket != null) webSocket.Dispose();
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine("WebSocket Client Closed.");
                Console.ResetColor();
            }
    
            // WebSocketの接続
            private static async Task Connect(string uri)
            {
                webSocket = new ClientWebSocket();
                // ヘッダーでユーザ名を送信しておく
                webSocket.Options.SetRequestHeader("User-Name", userName);
                await webSocket.ConnectAsync(new Uri(uri), CancellationToken.None);
            }
    
            // メッセージ送信
            private static async void SendMessage()
            {
                string inputMessage = "";
    
                // 接続中はループする
                while (webSocket.State == WebSocketState.Open) {
                    // メッセージの入力待ち
                    inputMessage = Console.ReadLine();
                    // 何も入力がない場合に終了
                    if (inputMessage == "") break;
                    // WebSocketにメッセージを送信する
                    ArraySegment<byte> buff = new ArraySegment<byte>(Encoding.UTF8.GetBytes(inputMessage));
                    await webSocket.SendAsync(buff, WebSocketMessageType.Text, true, CancellationToken.None);
                    // 表示の更新
                    UpdateDisplay();
                }
                return;
            }
    
            // メッセージ受信
            private static async void ReceiveMessage()
            {
                byte[] buff = new byte[1024];
                ArraySegment<byte> segment = new ArraySegment<byte>(buff);
                WebSocketReceiveResult receiveResult;
    
                // 接続中はループする
                while (webSocket.State == WebSocketState.Open) {
                    // 送られているメッセージを受信する
                    receiveResult = await webSocket.ReceiveAsync(segment, CancellationToken.None);
                    // メッセージがある場合は過去含めて再表示
                    if (receiveResult.Count != 0) {
                        messageList.Add(System.Text.Encoding.UTF8.GetString(buff, 0, receiveResult.Count));
                        UpdateDisplay();
                    }
                }
                return;
            }
    
            // 表示の初期化
            private static void InitializeDisplay()
            {
                Console.Clear();
                messageList = new List<string>();
                Console.WriteLine(clientName);
                string loginMessage = "May I ask your name?: ";
                Console.Write(loginMessage);
                userName = Console.ReadLine();
                messageList.Add(loginMessage + userName);
                Console.Write("Message: ");
            }
    
            // 表示の更新
            private static void UpdateDisplay()
            {
                Console.Clear();
                Console.WriteLine(clientName);
                foreach (string display in messageList) {
                    Console.WriteLine(display);
                }
                Console.Write("Message: ");
            }
        }
    }
    
  4. 実行

    $ dotnet run
    

動作確認

20191219_movie.gif

上記で貼り付けている動画のように、2台のチャットクライアントがWebSocketを介して双方向通信をし、片方のクライアントがメッセージを送信したら他のクライアントに配信していることがわかりました。

おわりに

前回の記事については、クライアント側はwebsocket-sharpのライブラリを使用しました。
ここから双方向通信の実装を実施していく上でクライアント側のWebSocket処理がいまいち理解することができませんでした。
そこでクライアント側はライブラリを使用せずにC#が用意しているSystem.Net.WebSocketsを使用して通信を行うクライアントの実装をしていこうかと考えました。

次回は、前回実装しようとしたUnityを複数台接続してゲームオブジェクトのXYZ座標を共有できるようなサーバ側、クライアント側の実装をしていきたいと考えております。
また、公式サイトのサンプルを主に参考にして実装していますが、チャットツールとして自分なりに作成したので間違った部分もあるとは思います。それについてはコメントでしていきしていただけると助かります。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした