私はc#ソケットプログラミングを学んでいます。したがって、私はTCP=チャットをすることを決定しました。基本的な考え方は、クライアントがサーバーにデータを送信し、サーバーがすべてのクライアントにオンラインでブロードキャストすることです(この場合、すべてのクライアントは辞書で)。
接続されているクライアントが1つある場合、期待どおりに機能し、接続されているクライアントが複数ある場合に問題が発生します。
サーバー:
_class Program
{
static void Main(string[] args)
{
Dictionary<int,TcpClient> list_clients = new Dictionary<int,TcpClient> ();
int count = 1;
TcpListener ServerSocket = new TcpListener(IPAddress.Any, 5000);
ServerSocket.Start();
while (true)
{
TcpClient client = ServerSocket.AcceptTcpClient();
list_clients.Add(count, client);
Console.WriteLine("Someone connected!!");
count++;
Box box = new Box(client, list_clients);
Thread t = new Thread(handle_clients);
t.Start(box);
}
}
public static void handle_clients(object o)
{
Box box = (Box)o;
Dictionary<int, TcpClient> list_connections = box.list;
while (true)
{
NetworkStream stream = box.c.GetStream();
byte[] buffer = new byte[1024];
int byte_count = stream.Read(buffer, 0, buffer.Length);
byte[] formated = new Byte[byte_count];
//handle the null characteres in the byte array
Array.Copy(buffer, formated, byte_count);
string data = Encoding.ASCII.GetString(formated);
broadcast(list_connections, data);
Console.WriteLine(data);
}
}
public static void broadcast(Dictionary<int,TcpClient> conexoes, string data)
{
foreach(TcpClient c in conexoes.Values)
{
NetworkStream stream = c.GetStream();
byte[] buffer = Encoding.ASCII.GetBytes(data);
stream.Write(buffer,0, buffer.Length);
}
}
}
class Box
{
public TcpClient c;
public Dictionary<int, TcpClient> list;
public Box(TcpClient c, Dictionary<int, TcpClient> list)
{
this.c = c;
this.list = list;
}
}
_
このボックスを作成したので、Thread.start()
に2つの引数を渡すことができます。
クライアント:
_class Program
{
static void Main(string[] args)
{
IPAddress ip = IPAddress.Parse("127.0.0.1");
int port = 5000;
TcpClient client = new TcpClient();
client.Connect(ip, port);
Console.WriteLine("client connected!!");
NetworkStream ns = client.GetStream();
string s;
while (true)
{
s = Console.ReadLine();
byte[] buffer = Encoding.ASCII.GetBytes(s);
ns.Write(buffer, 0, buffer.Length);
byte[] receivedBytes = new byte[1024];
int byte_count = ns.Read(receivedBytes, 0, receivedBytes.Length);
byte[] formated = new byte[byte_count];
//handle the null characteres in the byte array
Array.Copy(receivedBytes, formated, byte_count);
string data = Encoding.ASCII.GetString(formated);
Console.WriteLine(data);
}
ns.Close();
client.Close();
Console.WriteLine("disconnect from server!!");
Console.ReadKey();
}
}
_
あなたの質問から、具体的にどのような問題が発生しているかは明らかではありません。ただし、コードを検査すると、2つの重要な問題が明らかになります。
これらの2つの問題に対処するコードのバージョンを次に示します。
サーバーコード:
_class Program
{
static readonly object _lock = new object();
static readonly Dictionary<int, TcpClient> list_clients = new Dictionary<int, TcpClient>();
static void Main(string[] args)
{
int count = 1;
TcpListener ServerSocket = new TcpListener(IPAddress.Any, 5000);
ServerSocket.Start();
while (true)
{
TcpClient client = ServerSocket.AcceptTcpClient();
lock (_lock) list_clients.Add(count, client);
Console.WriteLine("Someone connected!!");
Thread t = new Thread(handle_clients);
t.Start(count);
count++;
}
}
public static void handle_clients(object o)
{
int id = (int)o;
TcpClient client;
lock (_lock) client = list_clients[id];
while (true)
{
NetworkStream stream = client.GetStream();
byte[] buffer = new byte[1024];
int byte_count = stream.Read(buffer, 0, buffer.Length);
if (byte_count == 0)
{
break;
}
string data = Encoding.ASCII.GetString(buffer, 0, byte_count);
broadcast(data);
Console.WriteLine(data);
}
lock (_lock) list_clients.Remove(id);
client.Client.Shutdown(SocketShutdown.Both);
client.Close();
}
public static void broadcast(string data)
{
byte[] buffer = Encoding.ASCII.GetBytes(data + Environment.NewLine);
lock (_lock)
{
foreach (TcpClient c in list_clients.Values)
{
NetworkStream stream = c.GetStream();
stream.Write(buffer, 0, buffer.Length);
}
}
}
}
_
クライアントコード:
_class Program
{
static void Main(string[] args)
{
IPAddress ip = IPAddress.Parse("127.0.0.1");
int port = 5000;
TcpClient client = new TcpClient();
client.Connect(ip, port);
Console.WriteLine("client connected!!");
NetworkStream ns = client.GetStream();
Thread thread = new Thread(o => ReceiveData((TcpClient)o));
thread.Start(client);
string s;
while (!string.IsNullOrEmpty((s = Console.ReadLine())))
{
byte[] buffer = Encoding.ASCII.GetBytes(s);
ns.Write(buffer, 0, buffer.Length);
}
client.Client.Shutdown(SocketShutdown.Send);
thread.Join();
ns.Close();
client.Close();
Console.WriteLine("disconnect from server!!");
Console.ReadKey();
}
static void ReceiveData(TcpClient client)
{
NetworkStream ns = client.GetStream();
byte[] receivedBytes = new byte[1024];
int byte_count;
while ((byte_count = ns.Read(receivedBytes, 0, receivedBytes.Length)) > 0)
{
Console.Write(Encoding.ASCII.GetString(receivedBytes, 0, byte_count));
}
}
}
_
ノート:
lock
ステートメントを使用して、_list_clients
_オブジェクトのスレッドによる排他的アクセスを保証します。Box
オブジェクトは必要ありません。コレクション自体は、実行中のすべてのメソッドからアクセス可能な静的フィールドによって参照され、各クライアントに割り当てられたint
値がスレッドパラメータとして渡されるため、スレッドは適切なクライアントオブジェクトを検索できます。0
_のバイトカウントで完了する読み取り操作を監視して処理します。これは、リモートエンドポイントの送信が完了したことを示すために使用される標準のソケット信号です。エンドポイントは、Shutdown()
メソッドを使用して送信が完了したことを示します。グレースフルクロージャを開始するために、Shutdown()
が「送信」理由で呼び出され、エンドポイントが送信を停止したが、引き続き受信することを示します。もう一方のエンドポイントは、最初のエンドポイントへの送信が完了すると、「両方」の理由でShutdown()
を呼び出して、送信と受信の両方が完了したことを示すことができます。コードにはまださまざまな問題があります。上記は最も明白なものだけを扱っており、非常に基本的なサーバー/クライアントアーキテクチャの実際のデモンストレーションの合理的な複製にコードをもたらします。
補遺:
コメントからのフォローアップ質問に対処するためのいくつかの追加メモ:
Thread.Join()
を呼び出して(つまり、そのスレッドが終了するのを待ちます)、正常なクローズプロセスを開始した後、シャットダウンによってリモートエンドポイントが応答するまで実際にソケットを閉じないようにします。その終わり。ParameterizedThreadStart
デリゲートとしてのo => ReceiveData((TcpClient)o)
の使用は、スレッド引数のキャストよりも私が好むイディオムです。これにより、スレッドのエントリポイントを厳密に型指定されたままにすることができます。ただし、そのコードは、私が通常それを書いた方法とまったく同じではありません。そのイディオムを説明する機会をまだ使用しながら、私は元のコードに固執していました。しかし、実際には、パラメーターなしのThreadStart
デリゲートを使用してコンストラクターオーバーロードを使用し、ラムダ式に必要なメソッド引数をキャプチャさせます:Thread thread = new Thread(() => ReceiveData(client)); thread.Start();
次に、キャストを行う必要はありません(そして引数が値型の場合、ボックス化/ボックス化解除のオーバーヘッドなしで処理されます。通常、このコンテキストでは重要な問題ではありませんが、気分がよくなります:))。Control.Invoke()
(またはWPFプログラムでDispatcher.Invoke()
)を使用することです。より洗練された(そしてIMHOより優れた)アプローチは、I/Oにasync
/await
を使用することです。 StreamReader
を使用してデータを受信している場合、そのオブジェクトにはすでに待機可能なReadLineAsync()
および同様のメソッドがあります。 Socket
を直接使用する場合は、 Task.FromAsync()
メソッドを使用して、BeginReceive()
およびEndReceive()
メソッドを待ってます。どちらの方法でも、I/Oは非同期で発生しますが、UIオブジェクトに直接アクセスできるUIスレッドで完了が処理されます。 (このアプローチでは、Thread.Join()
を使用する代わりに、受信コードを表すタスクを待機して、ソケットが途中で閉じないようにします。)