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

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

问题描述

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

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. 您的客户端代码尝试处理用户输入并在处理从服务器接收数据的同一线程中写入服务器.这至少会导致几个问题:
    • 在用户下次提供某些输入之前,不可能从另一个客户端接收数据.
    • 因为在单次读取操作中您可能只收到一个字节,即使在用户提供输入之后,您仍然可能无法收到之前发送的完整消息.

这是解决这两个问题的代码版本:

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 语句来确保 list_clients 对象的线程的独占访问.
  • 必须在消息广播的整个过程中保持锁定,以确保在枚举集合时没有客户端被移除,并且在另一个线程尝试在套接字上发送时没有客户端被一个线程关闭.
  • 在这个版本中,不需要 Box 对象.集合本身由所有正在执行的方法都可以访问的静态字段引用,并且分配给每个客户端的 int 值作为线程参数传递,因此线程可以查找适当的客户端对象.
  • 服务器和客户端都会监视并处理以 0 字节计数完成的读取操作.这是用于指示远程端点已完成发送的标准套接字信号.端点使用 Shutdown() 方法指示它已完成发送.为了启动优雅的关闭,Shutdown() 被调用并带有send"原因,表明端点已停止发送,但仍将接收.另一个端点一旦完成向第一个端点的发送,就可以调用 Shutdown() ,原因是both",以表明它已完成发送和接收.
  • 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 =>的使用ReceiveData((TcpClient)o) 作为 ParameterizedThreadStart 委托是我更喜欢转换线程参数的习惯用法.它允许线程入口点保持强类型.尽管如此,该代码并不是我通常编写的方式;我一直严格遵守你的原始代码,同时仍然利用这个机会来说明这个习语.但实际上,我会使用使用无参数 ThreadStart 委托的构造函数重载,并让 lambda 表达式捕获必要的方法参数:Thread thread = new Thread(() => ReceiveData(客户));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(),以确保您不会过早关闭套接字.)
  • 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天全站免登陆