与多个客户端的C#TCP / IP简单聊天 [英] C# TCP/IP simple chat with multiple-clients

查看:72
本文介绍了与多个客户端的C#TCP / IP简单聊天的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在学习c#套接字编程。因此,我决定进行TCP聊天,其基本思想是,一个客户端将数据发送到服务器,然后服务器为所有客户端在线广播数据(在这种情况下,所有客户端都在字典中)。

I'm learning c# socket programming. 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).

当连接了1个客户端时,它按预期方式工作,当连接了1个以上客户端时,就会出现此问题。

When there is 1 client connected, it works as expected, the problem is occurred when there is more than 1 client connected.

服务器:

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个参数。

I created this box, so I can pass 2 args for the Thread.start().

客户端:

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. 您不会以线程安全的方式访问字典,这意味着可能在字典中添加项目的侦听线程可以在客户端服务线程尝试检查字典的同时对对象进行操作。但是,加法操作不是原子的。这意味着在添加项目的过程中,词典可能会暂时处于无效状态。这将对尝试同时读取它的任何客户端服务线程造成问题。

  2. 您的客户端代码尝试处理用户输入并在与处理该线程相同的线程中写入服务器从服务器接收数据。这可能会导致至少两个问题:


    • 只有当用户下次提供某些输入时,才能从另一个客户端接收数据。 / li>
    • 因为即使在用户提供输入之后,在一次读取操作中您可能收到的字节数也很少,所以您可能仍未收到之前发送的完整消息。

以下是您的代码的版本,可以解决以下两个问题:

Here is a version of your code that addresses these two issues:

服务器代码:

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 语句来确保通过<$ c $的线程进行独占访问c> list_clients 对象。

  • 必须在消息广播的整个过程中保持该锁,以确保枚举集合时不会删除任何客户端,并且不要客户端被一个线程关闭,而另一个线程正在尝试在套接字上发送。

  • 在此版本中,不需要 Box 对象。集合本身由所有执行方法都可以访问的静态字段引用,并且分配给每个客户端的 int 值作为线程参数传递,因此线程可以查找

  • 服务器和客户端都监视并处理读取操作,该操作以字节数 0 完成。这是标准套接字信号,用于指示远程端点已完成发送。端点指示已通过使用 Shutdown()方法完成发送。要启动正常关闭,请使用发送原因调用 Shutdown(),这表明端点已停止发送,但仍会接收。另一个端点发送到第一个端点后,可以使用两个的原因调用 Shutdown(),以指示它已完成发送和接收。 / li>
  • This version uses the lock statement to ensure exclusive access by a thread of the list_clients object.
  • 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. 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.
  • Both server and client watch for and handle a read operation that completes with a byte count of 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. To initiate the graceful closure, Shutdown() is called with the "send" reason, indicating that the endpoint has stopped sending, but will still receive. 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.

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

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.



附录:

一些其他注释,以解决评论中的后续问题:

Some additional notes to address follow-up questions from the comments:


  • 客户端在接收线程上调用 Thread.Join()(以等待该线程退出),以确保

  • 使用 o =>它会启动正常的关闭过程,实际上不会关闭套接字,直到远程终结点通过关闭其末端进行响应为止。作为 ParameterizedThreadStart 委托的ReceiveData((TcpClient)o)是一个惯用语,而不是强制转换thread参数。它允许线程入口点保持强类型。但是,该代码与我通常编写的代码并不完全相同。我一直坚持使用您的原始代码,同时仍然利用这个机会来说明这一惯用法。但实际上,我将使用无参数的 ThreadStart 委托来使用构造函数重载,并让lambda表达式捕获必要的方法参数: Thread thread = new线程(()=> ReceiveData(client)); thread.Start(); 然后,根本就不需要进行强制转换(如果任何参数是值类型,则无需任何装箱/拆箱开销即可处理它们;在这种情况下,这通常不是关键问题,

  • 将这些技术应用于Windows Forms项目无疑会带来一些复杂性。在非UI线程中接收(无论是专用的每个连接线程,还是使用多个异步API进行网络I / O之一)时,在与UI对象进行交互时,您都需要回到UI线程。这里的解决方案与往常一样:最基本的方法是使用 Control.Invoke()(或 Dispatcher.Invoke(),在WPF程序中);一种更复杂(也是更好的IMHO)的方法是将 async / await 用于I / O。如果您正在使用 StreamReader 接收数据,则该对象已经具有等待的 ReadLineAsync()和类似的方法。如果直接使用 Socket ,则可以使用 Task.FromAsync() 方法来包装 BeginReceive() EndReceive()方法。无论哪种方式,结果都是尽管I / O异步发生,但仍在UI线程中处理补全,您可以在其中直接访问UI对象。 (在这种方法中,您将等待代表接收代码的任务,而不是使用 Thread.Join(),以确保不会过早关闭套接字。)

  • The client calls Thread.Join() on the receiving thread (i.e. 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.
  • The use of o => ReceiveData((TcpClient)o) as the ParameterizedThreadStart delegate is an idiom I prefer over the casting of the thread argument. 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(); 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. 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. 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); a more sophisticated (and IMHO, superior) approach is to use async/await for the I/O. If you are using StreamReader to receive data, that object already has an awaitable ReadLineAsync() and similar methods. If using the Socket directly, you can use the Task.FromAsync() method to wrap the BeginReceive() and EndReceive() methods in an awaitable. 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. (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.)

这篇关于与多个客户端的C#TCP / IP简单聊天的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆