简体   繁体   English

C# TCP/IP 多客户端简单聊天

[英]C# TCP/IP simple chat with multiple-clients

I'm learning c# socket programming.我正在学习 c# 套接字编程。 So, I decided to make a TCP chat, the basic idea is that A client send data to the server, then the server broadcast it for all the clients online (in this case all the clients are in a dictionary).所以,我决定做一个TCP聊天,基本思想是A客户端向服务器发送数据,然后服务器将它广播给所有在线的客户端(在这种情况下所有客户端都在一个字典中)。

When there is 1 client connected, it works as expected, the problem is occurred when there is more than 1 client connected.当有1个客户端连接时,它按预期工作,当连接超过1个客户端时出现问题。

Server:服务器:

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;
    }

}

I created this box, so I can pass 2 args for the Thread.start() .我创建了这个框,所以我可以为Thread.start()传递 2 个参数。

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();        
    }
}

It is not clear from your question what problems specifically it is you are having.从您的问题中不清楚您遇到的具体问题是什么。 However, inspection of the code reveals two significant problems:但是,检查代码发现了两个重大问题:

  1. You do not access your dictionary in a thread-safe way, which means that the listening thread, which may add items to the dictionary, can operate on the object at the same time that a client-serving thread is trying to examine the dictionary.您不会以线程安全的方式访问您的字典,这意味着可能会向字典添加项目的侦听线程可以在客户端服务线程尝试检查字典的同时对对象进行操作。 But, the add operation is not atomic.但是,添加操作不是原子的。 Meaning that during the course of adding an item, the dictionary may temporarily be in an invalid state.这意味着在添加项目的过程中,字典可能会暂时处于无效状态。 This would cause problems for any client-serving thread that is trying to read it concurrently.这会导致任何尝试并发读取它的客户端服务线程出现问题。
  2. Your client code attempts to process the user input and writes to the server in the same thread that is handling receiving data from the server.您的客户端代码尝试处理用户输入并在处理从服务器接收数据的同一线程中写入服务器。 This can result in at least a couple of problems:这至少会导致几个问题:
    • It is not possible to receive data from another client until the next time the user provides some input.直到下次用户提供某些输入时,才能从另一个客户端接收数据。
    • Because you may receive as little as a single byte in a single read operation, even after the user provides input, you may still not receive the complete message that was sent previously.因为在单次读取操作中您可能只收到一个字节,即使在用户提供输入之后,您可能仍然无法收到之前发送的完整消息。

Here is a version of your code that addresses these two issues:这是解决这两个问题的代码版本:

Server code:服务器代码:

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);
            }
        }
    }
}

Client code:客户端代码:

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));
        }
    }
}

Notes:笔记:

  • This version uses the lock statement to ensure exclusive access by a thread of the list_clients object.此版本使用lock语句来确保list_clients对象的线程进行独占访问。
  • The lock has to be maintained throughout the broadcast of messages, to ensure that no client is removed while enumerating the collection, and that no client is closed by one thread while another is trying to send on the socket.在整个消息广播过程中必须保持锁定,以确保在枚举集合时没有客户端被删除,并且在另一个线程试图在套接字上发送时没有客户端被一个线程关闭。
  • In this version, there is no need for the Box object.在这个版本中,不需要Box对象。 The collection itself is referenced by a static field accessible by all the methods executing, and the int value assigned to each client is passed as the thread parameter, so the thread can look up the appropriate client object.集合本身由所有执行方法都可以访问的静态字段引用,分配给每个客户端的int值作为线程参数传递,因此线程可以查找适当的客户端对象。
  • Both server and client watch for and handle a read operation that completes with a byte count of 0 .服务器和客户端都会监视并处理以字节计数0完成的读取操作。 This is the standard socket signal used to indicate that the remote endpoint is done sending.这是用于指示远程端点已完成发送的标准套接字信号。 An endpoint indicates it's done sending by using the Shutdown() method.端点使用Shutdown()方法指示它已完成发送。 To initiate the graceful closure, Shutdown() is called with the "send" reason, indicating that the endpoint has stopped sending, but will still receive.为了启动优雅的关闭, Shutdown()被调用并带有“send”原因,表明端点已停止发送,但仍将接收。 The other endpoint, once done sending to the first endpoint, can then call Shutdown() with the reason of "both" to indicate that it is done both sending and receiving.另一个端点一旦完成向第一个端点的发送,就可以调用Shutdown() ,原因是“both”,以表明它已完成发送和接收。

There are still a variety of issues in the code.代码中仍然存在各种问题。 The above addresses only the most glaring, and brings the code to some reasonable facsimile of a working demonstration of a very basic server/client architecture.以上只解决了最明显的问题,并将代码带到了一个非常基本的服务器/客户端架构的工作演示的合理复制品中。


Addendum:附录:

Some additional notes to address follow-up questions from the comments:一些附加说明以解决评论中的后续问题:

  • The client calls Thread.Join() on the receiving thread (ie waits for that thread to exit), to ensure that after it's starting the graceful closure process, it does not actually close the socket until the remote endpoint responds by shutting down its end.客户端在接收线程上调用Thread.Join() (即等待该线程退出),以确保在它开始正常关闭过程后,它实际上不会关闭套接字,直到远程端点通过关闭其结束来响应.
  • The use of o => ReceiveData((TcpClient)o) as the ParameterizedThreadStart delegate is an idiom I prefer over the casting of the thread argument.使用o => ReceiveData((TcpClient)o)作为ParameterizedThreadStart委托是我更喜欢转换线程参数的习惯用法。 It allows the thread entry point to remain strongly-typed.它允许线程入口点保持强类型。 Though, that code is not exactly how I would have ordinarily written it;但是,该代码并不是我通常编写的方式; I was sticking closely to your original code, while still using the opportunity to illustrate that idiom.我一直严格遵守你的原始代码,同时仍然利用这个机会来说明这个习语。 But in reality, I would use the constructor overload using the parameterless ThreadStart delegate and just let the lambda expression capture the necessary method arguments: Thread thread = new Thread(() => ReceiveData(client)); thread.Start();但实际上,我会使用无参数ThreadStart委托使用构造函数重载,并让 lambda 表达式捕获必要的方法参数: Thread thread = new Thread(() => ReceiveData(client)); thread.Start(); Thread thread = new Thread(() => ReceiveData(client)); thread.Start(); Then, no casting at all has to happen (and if any arguments are value types, they are handled without any boxing/unboxing overhead…not usually a critical concern in this context, but still makes me feel better :) ).然后,根本不需要转换(如果任何参数是值类型,它们的处理没有任何装箱/拆箱开销……在这种情况下通常不是一个关键问题,但仍然让我感觉更好:))。
  • Applying these techniques to a Windows Forms project adds some complication, unsurprisingly.毫不奇怪,将这些技术应用于 Windows 窗体项目会增加一些复杂性。 When receiving in a non-UI thread (whether a dedicated per-connection thread, or using one of the several asynchronous APIs for network I/O), you will need to get back to the UI thread when interacting with the UI objects.在非 UI 线程中接收时(无论是专用的每个连接线程,还是使用多个异步 API 之一进行网络 I/O),在与 UI 对象交互时,您将需要返回 UI 线程。 The solution that here is the same as usual: the most basic approach is to use Control.Invoke() (or Dispatcher.Invoke() , in a WPF program);这里的解决方案和往常一样:最基本的方法是使用Control.Invoke() (或Dispatcher.Invoke() ,在 WPF 程序中); a more sophisticated (and IMHO, superior) approach is to use async / await for the I/O.一种更复杂(恕我直言,更高级)的方法是对 I/O 使用async / await If you are using StreamReader to receive data, that object already has an awaitable ReadLineAsync() and similar methods.如果您使用StreamReader接收数据,则该对象已经具有可等待的ReadLineAsync()和类似方法。 If using the Socket directly, you can use the Task.FromAsync() method to wrap the BeginReceive() and EndReceive() methods in an awaitable.如果直接使用Socket ,则可以使用Task.FromAsync()方法将BeginReceive()EndReceive()方法包装在可等待对象中。 Either way, the result is that while the I/O occurs asynchronously, completions still get handled in the UI thread where you can access your UI objects directly.无论哪种方式,结果都是当 I/O 异步发生时,完成仍然在 UI 线程中处理,您可以直接访问 UI 对象。 (In this approach, you would wait on the task representing the receiving code, instead of using Thread.Join() , to ensure you don't close the socket prematurely.) (在这种方法中,您将等待表示接收代码的任务,而不是使用Thread.Join() ,以确保您不会过早关闭套接字。)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM