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日目の記事で記載しておりますので、今回は省略します。
-
必要なパッケージを導入する
Cargo.toml[dependencies] env_logger = "0.7.1" ws = "0.9.1" chrono = "0.4.10
-
WS-RSライブラリを使用したメッセージを配信する仕組みを実装する
main.rsextern 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; } }
-
実行
$ cargo run
※ 実行した際に不足しているパッケージは自動でビルドされます。
クライアント側の実装
クライアント側は今回Unityではなく、.NET Coreで実装しようかと考えました。Unity側はC#ですので、その言語でコンソールアプリを実装できるフレームワークを選択しました。
-
.NET Coreの導入
Download .NETにある「Download .NET Core SDK」を押下し、パッケージをダウンロードする。そのパッケージを展開しインストールをする。 -
新規プロジェクトを作成
$ dotnet new console -n ChatClient
-
チャットツールの実装
$ cd ChatClient $ vi src/Program.cs
Program.csusing 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: "); } } }
-
実行
$ dotnet run
動作確認
上記で貼り付けている動画のように、2台のチャットクライアントがWebSocketを介して双方向通信をし、片方のクライアントがメッセージを送信したら他のクライアントに配信していることがわかりました。
おわりに
前回の記事については、クライアント側はwebsocket-sharpのライブラリを使用しました。
ここから双方向通信の実装を実施していく上でクライアント側のWebSocket処理がいまいち理解することができませんでした。
そこでクライアント側はライブラリを使用せずにC#が用意しているSystem.Net.WebSocketsを使用して通信を行うクライアントの実装をしていこうかと考えました。
次回は、前回実装しようとしたUnityを複数台接続してゲームオブジェクトのXYZ座標を共有できるようなサーバ側、クライアント側の実装をしていきたいと考えております。
また、公式サイトのサンプルを主に参考にして実装していますが、チャットツールとして自分なりに作成したので間違った部分もあるとは思います。それについてはコメントでしていきしていただけると助かります。