【问题标题】:TCP client\server - client doesn't always readTCP 客户端\服务器 - 客户端并不总是读取
【发布时间】:2012-09-21 02:14:02
【问题描述】:

客户代码:

TcpClient client = new TcpClient();
NetworkStream ns;
private void Form1_Load(object sender, EventArgs e)
{
    try
    {
        client.Connect("127.0.0.1", 560);
        ns = client.GetStream();
        byte[] buffer = ReadFully(ns, client.Available);

        //working with the buffer...
    }
    catch
    {
        //displaying error...
    }
}

public static byte[] ReadFully(NetworkStream stream , int initialLength)
{
    // If we've been passed an unhelpful initial length, just
    // use 32K.
    if (initialLength < 1)
    {
        initialLength = 32768;
    }

    byte[] buffer = new byte[initialLength];
    long read = 0;

    int chunk;
    while ((chunk = stream.Read(buffer, (int)read, buffer.Length - (int)read)) > 0)
    {
        read += chunk;

        // If we've reached the end of our buffer, check to see if there's
        // any more information
        if (read == buffer.Length)
        {
            int nextByte = stream.ReadByte();

            // End of stream? If so, we're done
            if (nextByte == -1)
            {
                return buffer;
            }

            // Nope. Resize the buffer, put in the byte we've just
            // read, and continue
            byte[] newBuffer = new byte[buffer.Length * 2];
            Array.Copy(buffer, newBuffer, buffer.Length);
            newBuffer[read] = (byte)nextByte;
            buffer = newBuffer;
            read++;
        }
    }
    // Buffer is now too big. Shrink it.
    byte[] ret = new byte[read];
    Array.Copy(buffer, ret, read);
    return ret;
}

服务器代码:

    private static TcpListener tcpListener;
        private static Thread listenThread;
        private static int clients;
        static void Main(string[] args)
        {
            tcpListener = new TcpListener(IPAddress.Any, 560);
            listenThread = new Thread(new ThreadStart(ListenForClients));
            listenThread.Start();
        }

        private static void ListenForClients()
        {
            tcpListener.Start();
            Console.WriteLine("Server started.");

            while (true)
            {
                //blocks until a client has connected to the server
                TcpClient client = tcpListener.AcceptTcpClient();

                //create a thread to handle communication
                //with connected client
                Thread clientThread = new Thread(new ParameterizedThreadStart(HandleClientComm));
                clientThread.Start(client);
            }
        }

        private static void HandleClientComm(object client)
        {
            clients++;
            TcpClient tcpClient = (TcpClient)client;
            NetworkStream clientStream = tcpClient.GetStream();
            ASCIIEncoding encoder = new ASCIIEncoding();
            Console.WriteLine("Client connected. ({0} connected)", clients.ToString());

            #region sendingHandler
            byte[] buffer = encoder.GetBytes(AddressBookServer.Properties.Settings.Default.contacts);

            clientStream.Write(buffer, 0, buffer.Length);
            clientStream.Flush();
            #endregion
        }

从代码中可以看出,我正在尝试将AddressBookServer.Properties.Settings.Default.contacts(一个字符串,非空)发送到连接的客户端。

问题是有时(这是奇怪的部分)客户端收到字符串,有时它一直被阻止在 ns.Read 线上等待接收。

我尝试通过在ns.Read 之后的行上放置一个断点来进行调试,我发现当它不起作用时它永远不会到达该行,因此它不会收到服务器发送的消息。

我的问题:我该如何解决?

我的假设:服务器在客户端接收之前发送消息,因此客户端永远不会收到它。

【问题讨论】:

  • 一方面,您的Read 通话中断。您假设您将一口气阅读整个内容,并且您忽略了返回值。你不应该做这两件事。
  • 我是网络编程新手,能不能给个固定的代码和解释?
  • 是的,就是这样。 “读取操作读取尽可能多的数据,最多为 size 参数指定的字节数。”至于修复它,如果不了解您的协议,这并不容易。客户端应该如何知道要读取多少数据?
  • 我已经编辑了我的客户端代码,请看一下。要读取多少数据client.Available 应该告诉你,不是吗?
  • 不要使用 Available 来检测逻辑消息的结束。只有决定策略(异步与同步、缓冲区与进程等)才有用。如果您预期多条消息,则需要实施“框架”策略,通常是“换行终止”(用于文本)或“长度前缀”(用于二进制)

标签: c# tcp network-programming tcpclient networkstream


【解决方案1】:

正如 Mark Gravell 所指出的,这是一个框架问题。这是一个简单的客户端和服务器,向您展示如何在消息上使用长度前缀来构建消息。请记住,这只是帮助您入门的示例。我不会认为它是生产就绪代码:

客户代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;

namespace SimpleClient
{
    internal class Client
    {
        private static void Main(string[] args)
        {
            try
            {
                TcpClient client = new TcpClient();
                NetworkStream ns;
                client.Connect("127.0.0.1", 560);
                ns = client.GetStream();
                byte[] buffer = ReadNBytes(ns, 4);
                    // read out the length field we know is there, because the server always sends it.
                int msgLenth = BitConverter.ToInt32(buffer, 0);
                buffer = ReadNBytes(ns, msgLenth);

                //working with the buffer...
                ASCIIEncoding encoder = new ASCIIEncoding();
                string msg = encoder.GetString(buffer);
                Console.WriteLine(msg);
                client.Close();
            }
            catch
            {
                //displaying error...
            }
        }

        public static byte[] ReadNBytes(NetworkStream stream, int n)
        {
            byte[] buffer = new byte[n];
            int bytesRead = 0;

            int chunk;
            while (bytesRead < n)
            {
                chunk = stream.Read(buffer, (int) bytesRead, buffer.Length - (int) bytesRead);
                if (chunk == 0)
                {
                    // error out
                    throw new Exception("Unexpected disconnect");
                }
                bytesRead += chunk;
            }
            return buffer;
        }
    }
}

服务器代码:

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace SimpleServer
{
    class Server
    {
        private static TcpListener tcpListener;
        private static int clients;
        static void Main(string[] args)
        {
            tcpListener = new TcpListener(IPAddress.Any, 560);
            tcpListener.Start();
            Console.WriteLine("Server started.");

            while (true)
            {
                //blocks until a client has connected to the server
                TcpClient client = tcpListener.AcceptTcpClient();

                //create a thread to handle communication
                //with connected client
                Thread clientThread = new Thread(new ParameterizedThreadStart(HandleClientComm));
                clientThread.Start(client);
            }
        }

        private static void HandleClientComm(object client)
        {
            int clientCount = Interlocked.Increment(ref clients);
            TcpClient tcpClient = (TcpClient)client;
            NetworkStream clientStream = tcpClient.GetStream();
            ASCIIEncoding encoder = new ASCIIEncoding();
            Console.WriteLine("Client connected. ({0} connected)", clientCount);

            #region sendingHandler
            byte[] buffer = encoder.GetBytes("Some Contacts as a string!");
            byte[] lengthBuffer = BitConverter.GetBytes(buffer.Length);
            clientStream.Write(lengthBuffer, 0, lengthBuffer.Length);
            clientStream.Write(buffer, 0, buffer.Length);
            clientStream.Flush();
            tcpClient.Close();
            #endregion
        }
    }
}

您的代码有时有效,有时失败的原因是 client.Available 可以返回 0。当它执行时,您将要读取的字节设置为 32k,因此 read 调用正在等待这些字节进入。它们从不确实如此,并且由于服务器从未关闭套接字,因此读取也不会出错。

希望这能让你朝着正确的方向前进。

编辑:

我忘了在我原来的帖子中提到字节序。您可以在此处查看有关字节序和使用 BitConverter 的文档:http://msdn.microsoft.com/en-us/library/system.bitconverter(v=vs.100).aspx

基本上,您需要确保服务器和客户端都在具有相同字节序的架构上运行,或者根据需要处理从一种字节序到另一种字节序的转换。

编辑 2(回答 cmets 中的问题):

1) 为什么client.available可以返回0?

这是一个时间问题。客户端连接到服务器,然后立即询问哪些字节可用。根据正在运行的其他进程、可用处理器的时间片等,客户端可能会在服务器有机会发送任何内容之前询问可用的内容。在这种情况下,它将返回 0。

2) 为什么我使用 Interlocked 来增加客户端?

您最初编写的代码是在新创建的运行 HandleClientComm(...) 的线程中递增客户端。如果两个或多个客户端同时连接,则可能会在多个线程试图增加客户端时发生争用情况。最终结果将是客户数量少于应有数量。

3) 为什么我要更改 ReadFully 方法?

您的 ReadFully 版本(我将其更改为 ReadNBytes)接近正确,但存在一些缺陷:

  • 如果原始 initialLength 为零或更小,则将 initialLenth 设置为 32768。您永远不应该猜测您需要从套接字读取多少字节。正如 Mark Gravell 提到的,您需要使用长度前缀或某种分隔符来构建消息。
  • NetworkStream.Read 阻塞直到读取了一些字节,如果套接字从其下方关闭,则返回 0。无需调用 stream.ReadByte,因为如果套接字断开连接,chunk 已经为 0。进行该更改简化了方法,尤其是因为我们根据简单的标头确切地知道需要读取多少字节。
  • 现在我们知道要阅读多少内容,我们可以预先准确分配我们需要的内容。这消除了在返回时重新调整缓冲区大小的必要性。我们可以返回我们分配的内容。

4) 我如何知道长度缓冲区大小为 4?

我序列化的长度是 32 位。您可以在 BitConverter.GetBytes(int value) 的文档中看到这一点。我们知道一个字节是 8 位,所以只需将 32 除以 8 得到 4。

5) 为什么这还没有准备好生产/我该如何改进代码?

  • 基本上没有真正的错误处理。 NetworkStream.Read() 可以抛出几个异常,我没有处理这些异常。添加适当的错误处理将大大有助于使其做好生产准备。

  • 除了粗略的运行之外,我还没有真正测试过代码。它需要在各种条件下进行测试。

  • 没有规定客户端重新连接或重试,尽管您可能不需要这样做。

  • 这是一个简单的示例,实际上可能无法满足您尝试满足的要求。不知道这些要求,我不能声称这已经为您的生产环境做好了准备(无论是什么)。

  • 从概念上讲,ReadNBytes 没问题,但如果有人向您发送恶意消息,声称消息长度为 2 GB 或其他内容,您将尝试盲目分配 2 GB。大多数字节级协议(对通过网络传输的内容的描述)指定消息的最大大小。这需要检查,如果消息实际上可能很大,您需要以不同于分配缓冲区的方式处理它,可能在您读取它时写入文件或其他输出流。同样,不知道您的全部要求,我不确定那里需要什么。

希望这会有所帮助。

【讨论】:

  • 哇,非常感谢,就像魔术一样工作 :) 我有一些问题请回答: 1. 为什么client.Available 可以返回 0? 2. 为什么你使用Interlocked 类来增加客户端?起初它是如何运作良好的 3. 为什么你改变了ReadFully 类? 4. 你怎么知道长度缓冲区大小是4? 5. 为什么这不是“生产就绪”,我该如何改进这段代码?再次感谢! (在您尝试回答我的问题后,我会接受答案)
  • @DanBarzilay:我在帖子中添加了我的答案,因为它们太长了,无法放入评论中。如果您对套接字编程感兴趣,我推荐amazon.com/UNIX-Network-Programming-Networking-Sockets/dp/…amazon.com/C-Network-Programming-Richard-Blum/dp/0782141765 第一个是特定于unix 的,但也涵盖了许多一般的网络概念。第二个特定于 C#。还有其他好书,但你必须从某个地方开始。 :)
猜你喜欢
  • 1970-01-01
  • 2020-01-28
  • 2013-08-20
  • 2012-01-04
  • 2013-02-17
  • 2012-11-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多