J'apprends la programmation de socket c #. J'ai donc décidé de faire un TCP chat, l'idée de base est qu'un client envoie des données au serveur, puis le serveur les diffuse en ligne pour tous les clients (dans ce cas, tous les clients sont dans un dictionnaire).
Lorsqu'il y a 1 client connecté, cela fonctionne comme prévu, le problème se produit lorsqu'il y a plus d'un client connecté.
Serveur:
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;
}
}
J'ai créé cette boîte, donc je peux passer 2 arguments pour la Thread.start()
.
Client:
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();
}
}
Il n'est pas clair à partir de votre question quels problèmes spécifiquement que vous rencontrez. Cependant, l'inspection du code révèle deux problèmes importants:
Voici une version de votre code qui résout ces deux problèmes:
Code serveur:
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);
}
}
}
}
Code client:
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));
}
}
}
Remarques:
lock
pour garantir un accès exclusif par un thread de l'objet list_clients
.Box
. La collection elle-même est référencée par un champ statique accessible par toutes les méthodes en cours d'exécution et la valeur int
affectée à chaque client est transmise en tant que paramètre de thread, afin que le thread puisse rechercher l'objet client approprié.0
. Il s'agit du signal de socket standard utilisé pour indiquer que le point de terminaison distant a terminé l'envoi. Un point de terminaison indique que l'envoi est terminé à l'aide de la méthode Shutdown()
. Pour lancer la fermeture progressive, Shutdown()
est appelée avec la raison "send", indiquant que le point de terminaison a cessé d'envoyer, mais recevra toujours. L'autre point de terminaison, une fois l'envoi envoyé au premier point de terminaison, peut alors appeler Shutdown()
avec la raison "les deux" pour indiquer qu'il est effectué à la fois en envoi et en réception.Il y a encore une variété de problèmes dans le code. Ce qui précède ne concerne que les plus flagrants et apporte le code à un fac-similé raisonnable d'une démonstration fonctionnelle d'une architecture serveur/client très basique.
Addendum:
Quelques notes supplémentaires pour répondre aux questions complémentaires des commentaires:
Thread.Join()
sur le thread de réception (c'est-à-dire attend que ce thread se termine), pour s'assurer qu'après le démarrage du processus de fermeture en douceur, il ne ferme pas réellement le socket jusqu'à ce que le point de terminaison distant réponde en s'arrêtant sa fin.o => ReceiveData((TcpClient)o)
en tant que délégué ParameterizedThreadStart
est un idiome que je préfère à la conversion de l'argument thread. Il permet au point d'entrée du thread de rester fortement typé. Cependant, ce code n'est pas exactement comme je l'aurais écrit habituellement; Je m'en tenais étroitement à votre code d'origine, tout en profitant de l'occasion pour illustrer cet idiome. Mais en réalité, j'utiliserais la surcharge du constructeur en utilisant le délégué sans paramètre ThreadStart
et je laisserais simplement l'expression lambda capturer les arguments de méthode nécessaires: Thread thread = new Thread(() => ReceiveData(client)); thread.Start();
Ensuite, aucun transtypage n'a lieu (et si des arguments sont des types de valeur, ils sont traités sans surcharge de boxe/unboxing… ce n'est généralement pas un problème critique dans ce contexte, mais ça me fait quand même du bien :)).Control.Invoke()
(ou Dispatcher.Invoke()
, dans un programme WPF); une approche plus sophistiquée (et à mon humble avis, supérieure) consiste à utiliser async
/await
pour les E/S. Si vous utilisez StreamReader
pour recevoir des données, cet objet possède déjà une ReadLineAsync()
et des méthodes similaires attendues. Si vous utilisez directement Socket
, vous pouvez utiliser la méthode Task.FromAsync()
pour encapsuler les méthodes BeginReceive()
et EndReceive()
dans un en attente. Dans les deux cas, le résultat est que même si les E/S se produisent de manière asynchrone, les achèvements sont toujours traités dans le thread d'interface utilisateur où vous pouvez accéder directement à vos objets d'interface utilisateur. (Dans cette approche, vous attendriez la tâche représentant le code de réception, au lieu d'utiliser Thread.Join()
, pour vous assurer de ne pas fermer le socket prématurément.)