繁体   English   中英

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

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

我正在学习 c# 套接字编程。 所以,我决定做一个TCP聊天,基本思想是A客户端向服务器发送数据,然后服务器将它广播给所有在线的客户端(在这种情况下所有客户端都在一个字典中)。

当有1个客户端连接时,它按预期工作,当连接超过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();        
    }
}

从您的问题中不清楚您遇到的具体问题是什么。 但是,检查代码发现了两个重大问题:

  1. 您不会以线程安全的方式访问您的字典,这意味着可能会向字典添加项目的侦听线程可以在客户端服务线程尝试检查字典的同时对对象进行操作。 但是,添加操作不是原子的。 这意味着在添加项目的过程中,字典可能会暂时处于无效状态。 这会导致任何尝试并发读取它的客户端服务线程出现问题。
  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()被调用并带有“send”原因,表明端点已停止发送,但仍将接收。 另一个端点一旦完成向第一个端点的发送,就可以调用Shutdown() ,原因是“both”,以表明它已完成发送和接收。

代码中仍然存在各种问题。 以上只解决了最明显的问题,并将代码带到了一个非常基本的服务器/客户端架构的工作演示的合理复制品中。


附录:

一些附加说明以解决评论中的后续问题:

  • 客户端在接收线程上调用Thread.Join() (即等待该线程退出),以确保在它开始正常关闭过程后,它实际上不会关闭套接字,直到远程端点通过关闭其结束来响应.
  • 使用o => ReceiveData((TcpClient)o)作为ParameterizedThreadStart委托是我更喜欢转换线程参数的习惯用法。 它允许线程入口点保持强类型。 但是,该代码并不是我通常编写的方式; 我一直严格遵守你的原始代码,同时仍然利用这个机会来说明这个习语。 但实际上,我会使用无参数ThreadStart委托使用构造函数重载,并让 lambda 表达式捕获必要的方法参数: Thread thread = new Thread(() => ReceiveData(client)); thread.Start(); Thread thread = new Thread(() => ReceiveData(client)); thread.Start(); 然后,根本不需要转换(如果任何参数是值类型,它们的处理没有任何装箱/拆箱开销……在这种情况下通常不是一个关键问题,但仍然让我感觉更好:))。
  • 毫不奇怪,将这些技术应用于 Windows 窗体项目会增加一些复杂性。 在非 UI 线程中接收时(无论是专用的每个连接线程,还是使用多个异步 API 之一进行网络 I/O),在与 UI 对象交互时,您将需要返回 UI 线程。 这里的解决方案和往常一样:最基本的方法是使用Control.Invoke() (或Dispatcher.Invoke() ,在 WPF 程序中); 一种更复杂(恕我直言,更高级)的方法是对 I/O 使用async / await 如果您使用StreamReader接收数据,则该对象已经具有可等待的ReadLineAsync()和类似方法。 如果直接使用Socket ,则可以使用Task.FromAsync()方法将BeginReceive()EndReceive()方法包装在可等待对象中。 无论哪种方式,结果都是当 I/O 异步发生时,完成仍然在 UI 线程中处理,您可以直接访问 UI 对象。 (在这种方法中,您将等待表示接收代码的任务,而不是使用Thread.Join() ,以确保您不会过早关闭套接字。)

暂无
暂无

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

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