【问题标题】:C# TCP/IP simple chat with multiple-clientsC# TCP/IP 与多客户端的简单聊天
【发布时间】:2017-04-15 20:55:12
【问题描述】:

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

【问题讨论】:

  • 您能描述一下连接多个客户端时会出现什么问题吗?现在我们只知道它“没有按预期工作”。
  • 预期如果我向服务器发送“123”,那么服务器会将这个“123”发送给所有客户端。当只有 1 个客户端时它可以工作,但是当有超过 1 个客户端时,它就会出现问题,老实说,我不知道,也许我没有以正确的方式处理线程。
  • 不清楚你在问什么。你的问题陈述太模糊了。您的代码有许多可以改进的地方。我看到的最严重的缺陷是在客户端代码中,您只执行一次读取操作,因为继续从用户那里获得更多输入。但是 TCP 不保证您会从任何已完成的读取操作中获得超过一个字节。因此,如果您的问题是客户端正在接收部分消息,这就是原因。请改进问题。请参阅 How to Ask 以及该页面底部的相关链接以获取建议。
  • 如果你真的想学习网络编程,你应该从仔细而透彻地阅读Winsock Programmer's FAQ开始。它不是特定于 .NET,但 .NET API(实际上是大多数主流的低级网络 API)是基于套接字构建的,无论如何,该指南中的大多数建议都与协议有关,而不是与协议有关。 API。您还应该阅读许多其他有用的资源,但您应该从那里开始。
  • “它只是变得有问题”——也不是一个精确的问题陈述。代码中的另一个问题是字典类型不是线程安全的,因此如果侦听线程修改字典而另一个线程试图读取字典以发送消息,则读取线程将看到无效的损坏状态字典。

标签: c# sockets tcp chat tcplistener


【解决方案1】:

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

  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() 被调用并带有“发送”原因,表示端点已停止发送,但仍将接收。另一个端点,一旦完成发送到第一个端点,就可以调用Shutdown(),原因为“both”,表示它已完成发送和接收。

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


附录:

一些额外的注释来解决来自 cmets 的后续问题:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-01
    • 1970-01-01
    • 2016-05-29
    • 2015-10-05
    • 1970-01-01
    • 2015-12-17
    相关资源
    最近更新 更多