仰々しいタイトルですが、要は同期+スレッドと非同期のどちらがいいのかという話しです。
「C#でTCPサーバーの実装を作ったよ」というレポートはかなり多くて、www.codeproject.comなんかで検索すると10件近く出てきます。日本語の情報もいくつかあり、チュートリアル的な情報では、まずは同期式で勉強してから非同期でという流れが多いですね。
Unityを始めてまず「オンラインゲームのしくみ」を読んだのですが、この本の場合は非同期で作るのは大変だから同期でつくろうよというスタンス。この本のライブラリを使ってコーディングを始めたのですが、このライブラリが複数接続に対応しておらず、結局作り直しになりました。
で、色々と見て回ったところ同期ベースか非同期ベースかというところで大きく分かれているので、自分はどちらでいこうかと迷い、メリット・デメリットを検討してみました。で、同期の方のメリットは、結局、スレッドプログラミングになれている場合には、プログラムの構造がわかりやすい、つまり書きやすいというところに尽きるようで、他のメリットは見あたりませんでした。
僕の場合、マルチスレッドは使ったことが無いのであまりメリットは感じられず、スレッドがメモリを食う、オーバーヘッドが大きいというデメリットの方が大きく感じました。サンプルコードを見ていると、非同期式でもそれほど複雑なプログラムにはならなさそうなので、非同期で実験してみることにしました。
で、MSDNのサンプルコードを使ってUnity上でTCPサーバーを非同期で実行するためのサンプルがこちら。いわゆるチャットサーバーですね。いくつかはまりポイントがまたありましたが、概ねスムーズに書けました。ncコマンドで複数の端末から接続すると、それぞれに固有番号を振った上で、ある端末のメッセージが全端末に送信されます。接続切断はまだ未実装です。
ちょっとこまったなと思うのはどうにもコードが肥大する感じで見にくいです。クラス化しようかと思ったのですが、コールバックはビジネスロジック側で実装しないと意味が無くて、サイズを取っているのがコールバックなので、クラスにしてもあまり意味が無い感じがしました。デリゲートで作れるとは思うのですが、このあたりはまだ慣れていないので余力があれば書こうと思います。
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 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
// There could be Copyright (c) of Microsoft and Yasuo Kawachi // If there is, this code is licensed under Microsoft Limited Public License // https://msdn.microsoft.com/en-US/cc300389 See "Exhibit B" // I belive there is no copyright due to non creativity using UnityEngine; using System.Collections; using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Collections.Generic; public class Main : MonoBehaviour { // Use this for initialization void Start () { StartListening(); } // Update is called once per frame void Update () { } // State object for reading client data asynchronously public class StateObject { // Client socket. public Socket workSocket = null; // Size of receive buffer. public const int BufferSize = 1024; // Receive buffer. public byte[] buffer = new byte[BufferSize]; // Received data string. public StringBuilder sb = new StringBuilder(); } List<StateObject> activeConnections = new List<StateObject>(); public void StartListening() { // Data buffer for incoming data. //って書いてるけど使ってないのでコメントアウト //byte[] bytes = new Byte[1024]; // Establish the local endpoint for the socket. // The DNS name of the computer // running the listener is "host.contoso.com". //この表現だとResolveがobsoleteだと注意されるのでGetHostEntryやGetIPAddressを使う //IPHostEntry ipHostInfo = Dns.Resolve(Dns.GetHostName()); //IPAddress ipAddress = ipHostInfo.AddressList[0]; IPAddress ipAddress = IPAddress.Parse(GetIPAddress("localhost")); IPEndPoint localEndPoint = new IPEndPoint(ipAddress, 11000); // Create a TCP/IP socket. Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp ); // Bind the socket to the local endpoint and listen for incoming connections. try { listener.Bind(localEndPoint); listener.Listen(10); // Start an asynchronous socket to listen for connections. listener.BeginAccept( new AsyncCallback(AcceptCallback),listener ); } catch (Exception e) { Debug.Log(e.ToString()); } } public void AcceptCallback(IAsyncResult ar) { // Get the socket that handles the client request. Socket listener = (Socket) ar.AsyncState; Socket handler = listener.EndAccept(ar); // Create the state object. StateObject state = new StateObject(); state.workSocket = handler; handler.BeginReceive( state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReadCallback), state); //確立した接続のオブジェクトをリストに追加 activeConnections.Add (state); Debug.LogFormat ("there is {0} connections", activeConnections.Count); //接続待ちを再開しないと次の接続を受け入れなくなる listener.BeginAccept( new AsyncCallback(AcceptCallback),listener ); } public void ReadCallback(IAsyncResult ar) { String content = String.Empty; // Retrieve the state object and the handler socket // from the asynchronous state object. StateObject state = (StateObject) ar.AsyncState; Socket handler = state.workSocket; // Read data from the client socket. int bytesRead = handler.EndReceive(ar); if (bytesRead > 0) { // There might be more data, so store the data received so far. state.sb.Append(Encoding.ASCII.GetString(state.buffer,0,bytesRead)); // Check for end-of-file tag. If it is not there, read // more data. content = state.sb.ToString(); //MSDNのサンプルはEOFを検知して出力をしているけれどもncコマンドはEOFを改行時にLFしか飛ばさないので\nを追加 if (content.IndexOf("\n") > -1 || content.IndexOf("<EOF>") > -1) { // All the data has been read from the // client. Display it on the console. Debug.LogFormat("Read {0} bytes from socket. \n Data : {1}", content.Length, content ); // Echo the data back to the client. //Send(handler, content); foreach (StateObject each in activeConnections) { //string message = string.Format ("You are client No.{0}", i); // Send (each.workSocket, message); //eachをactiveConnectionの中から見つけてそのインデックスを取得する方法がこれ int num_of_each = activeConnections.FindIndex (delegate(StateObject s) {return s == each;}); //state:送信者の番号 int num_of_from = activeConnections.FindIndex (delegate(StateObject s) {return s == state;}); string message = string.Format ("you:{0} / from:{1} / data:{2}\n", num_of_each, num_of_from, content); Send (each.workSocket, message); } //clear data in object before next receive //StringbuilderクラスはLengthを0にしてクリアする state.sb.Length = 0;; // Not all data received. Get more. handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReadCallback), state); } else { // Not all data received. Get more. handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReadCallback), state); } } } private void Send(Socket handler, String data) { // Convert the string data to byte data using ASCII encoding. byte[] byteData = Encoding.ASCII.GetBytes(data); // Begin sending the data to the remote device. handler.BeginSend(byteData, 0, byteData.Length, 0, new AsyncCallback(SendCallback), handler); } private void SendCallback(IAsyncResult ar) { try { // Retrieve the socket from the state object. Socket handler = (Socket) ar.AsyncState; // Complete sending the data to the remote device. int bytesSent = handler.EndSend(ar); Debug.LogFormat("Sent {0} bytes to client.", bytesSent); //この2つはセットでつかるらしい //handler.Shutdown(SocketShutdown.Both); //handler.Close(); } catch (Exception e) { Debug.Log(e.ToString()); } } private string GetIPAddress(string hostname) { IPHostEntry host; host = Dns.GetHostEntry(hostname); foreach (IPAddress ip in host.AddressList) { if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) { //System.Diagnostics.Debug.WriteLine("LocalIPadress: " + ip); return ip.ToString(); } } return string.Empty; } } |
.






納得したらすぐにシェア!
One thought on “Unityでワンソースなシステム開発:C#でTCPサーバーを作る場合のベストプラクティスは”
Comments are closed.