c# - 与多个客户端的C#TCP/IP简单聊天

标签 c# sockets tcp chat tcplistener

我正在学习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();        
    }
}

最佳答案

从您的问题尚不清楚您具体遇到了什么问题。但是,检查代码揭示了两个重要问题:

  • 您不会以线程安全的方式访问字典,这意味着监听线程(可能向字典中添加项目)可以在客户端服务线程尝试检查字典的同时对对象进行操作。但是,加法操作不是原子的。这意味着在添加项目的过程中,词典可能暂时处于无效状态。对于任何试图同时读取它的客户端服务线程,这都会引起问题。
  • 您的客户端代码尝试处理用户输入,并在处理从服务器接收数据的同一线程中写入服务器。这可能会导致至少两个问题:
  • 在下一次用户提供输入之前,无法从另一个客户端接收数据。
  • 因为即使在用户提供输入之后,在一次读取操作中您可能收到的字节数也很少,所以您可能仍未收到之前发送的完整消息。

  • 这是您的代码的一个版本,解决了这两个问题:

    服务器代码:

    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(),指示端点已停止发送,但仍会接收。另一个端点一旦发送到第一个端点,便可以使用“both”的原因调用Shutdown(),以指示它已完成发送和接收。

  • 代码中仍然存在各种各样的问题。上面仅解决了最明显的问题,并将代码带到了非常基本的服务器/客户端体系结构的工作演示的合理传真中。

    附录:

    一些其他注释,用于解决评论中的后续问题:
  • 客户端在接收线程上调用Thread.Join()(即等待该线程退出),以确保在启动正常关闭过程后,它实际上不会关闭套接字,直到远程端点通过关闭其末端进行响应为止。
  • o => ReceiveData((TcpClient)o)用作ParameterizedThreadStart委托(delegate)是一种习惯用法,我更喜欢使用thread参数的强制转换。它允许线程入口点保持强类型。但是,该代码与我通常编写的代码并不完全相同。我一直坚持使用您的原始代码,同时仍然利用这个机会来说明这一惯用法。但实际上,我将使用无参数的ThreadStart委托(delegate)来使用构造函数重载,并让lambda表达式捕获必要的方法参数:Thread thread = new Thread(() => ReceiveData(client)); thread.Start();然后,根本就不需要强制转换(并且如果有任何参数是值类型,则无需进行任何处理即可)任何装箱/拆箱的开销……在这种情况下通常都不是关键问题,但仍然让我感觉更好:))。
  • 毫无疑问,将这些技术应用于Windows Forms项目会增加一些复杂性。在非UI线程中接收(无论是专用的每个连接线程,还是使用多个异步API进行网络I/O之一)时,在与UI对象进行交互时,您都需要回到UI线程。这里的解决方案与通常的解决方案相同:最基本的方法是使用Control.Invoke()(或WPF程序中的Dispatcher.Invoke())。一种更复杂(也是更好的IMHO)的方法是对I/O使用async/await。如果您正在使用StreamReader接收数据,则该对象已经具有可等待的ReadLineAsync()和类似的方法。如果直接使用Socket,则可以使用 Task.FromAsync() 方法将BeginReceive()EndReceive()方法包装在一个等待中。无论哪种方式,结果都是尽管I/O异步发生,但补全仍在UI线程中处理,您可以在其中直接访问UI对象。 (在这种方法中,您将等待代表接收代码的任务,而不是使用Thread.Join(),以确保您不会过早关闭套接字。)
  • 关于c# - 与多个客户端的C#TCP/IP简单聊天,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43431196/

    相关文章:

    c# - 更改 DataGridView CellMouseClick 上的单元格背景色

    c# - BinaryFormatter.Deserialize 如何创建新对象?

    c - 在 Windows 上将 select() 与 STDIN 一起使用?

    python - 通过 TCP 套接字 python 发送一个字节

    linux - 我可以监控 Linux 套接字缓冲区满度吗?

    go - HTTP重用连接条件

    c# - 如何获取WinForms项目中的所有表单和用户控件?

    c# - 存储库模式中的可重用模型

    C# TCP Socket "Blocking"属性不一致

    sockets - 将来自 Hololens 的麦克风语音输入发送到 PC