【问题标题】:Do TCP sockets automatically close after 64kB send?发送 64kB 后 TCP 套接字会自动关闭吗?
【发布时间】:2015-11-28 16:58:21
【问题描述】:

我正在尝试使用 C# TcpClient 通过 TCP 发送多个文件,对于 64kB 以下的文件效果很好,但是当我有更多文件时,它会抛出主机断开连接的异常。

这是我的代码:

服务器端(发送)。 1) SocketServer类

public abstract class SocketServer
{
    private Socket serverSocket;

    public SocketServer(IPEndPoint localEndPoint)
    {
        serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        serverSocket.Bind(localEndPoint);
        serverSocket.Listen(0);

        serverSocket.BeginAccept(BeginAcceptCallback, null);
    }

    private void BeginAcceptCallback(IAsyncResult ar)
    {
        Socket clientSocket = serverSocket.EndAccept(ar);

        Console.WriteLine("Client connected");

        ClientConnection clientConnection = new ClientConnection(this, clientSocket);
        Thread clientThread = new Thread(new ThreadStart(clientConnection.Process));
        clientThread.Start();

        serverSocket.BeginAccept(BeginAcceptCallback, null);
    }

    internal abstract void OnReceiveMessage(ClientConnection client, byte header, byte[] data);
}

这里是客户端连接:

public class ClientConnection
{
    private SocketServer server;
    private Socket clientSocket;

    private byte[] buffer;
    private readonly int BUFFER_SIZE = 8192;

    public ClientConnection(SocketServer server, Socket clientSocket)
    {
        this.server = server;
        this.clientSocket = clientSocket;

        buffer = new byte[BUFFER_SIZE];
    }

    public void Process()
    {
        clientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.Peek, BeginReceiveCallback, null);
    }

    private void BeginReceiveCallback(IAsyncResult ar)
    {
        int bytesReceived = clientSocket.EndReceive(ar);

        if (bytesReceived >= 4)
        {
            clientSocket.Receive(buffer, 0, 4, SocketFlags.None);

            // message size
            int size = BitConverter.ToInt32(buffer, 0);

            // read message 
            int read = clientSocket.Receive(buffer, 0, size, SocketFlags.None);

            // if data still fragmented, wait for it
            while (read < size)
            {
                read += clientSocket.Receive(buffer, read, size - read, SocketFlags.None);
            }

            ProcessReceivedData(size);
        }

        clientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.Peek, BeginReceiveCallback, null);
    }

    private void ProcessReceivedData(int size)
    {
        using (PacketReader pr = new PacketReader(buffer))
        {
            // message header = 1 byte 
            byte header = pr.ReadByte();

            // next message data
            byte[] data = pr.ReadBytes(size - 1);

            server.OnReceiveMessage(this, header, data);
        }
    }

    public void Send(byte[] data)
    {
        // first of all, send message length
        clientSocket.Send(BitConverter.GetBytes(data.Length), 0, 4, SocketFlags.None);

        // and then message
        clientSocket.Send(data, 0, data.Length, SocketFlags.None);
    }
}

FileServer类的实现:

public class FileServer : SocketServer
{
    string BinaryPath;

    public FileServer(IPEndPoint localEndPoint) : base(localEndPoint) 
    {
        BinaryPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
    }

    internal override void OnReceiveMessage(ClientConnection client, byte hdr, byte[] data)
    {
        // Convert header byte to ENUM
        Headers header = (Headers)hdr;

        switch(header)
        {
            case Headers.Queue:
                Queue(client, data);
                break;
            default:
                Console.WriteLine("Wrong header received {0}", header);
                break;
        }
    }

    private void Queue(ClientConnection client, byte[] data) 
    {
        // this message contains fileName
        string fileName = Encoding.ASCII.GetString(data, 1, data.Length - 1);

        // combine path with assembly location
        fileName = Path.Combine(BinaryPath, fileName);
        if (File.Exists(fileName))
        {
            FileInfo fileInfo = new FileInfo(fileName);
            long fileLength = fileInfo.Length;

            // pass the message that is now start a file transfer, contains:
            // 1 byte = header
            // 16 bytes = file length
            using (PacketWriter pw = new PacketWriter())
            {
                pw.Write((byte)Headers.Start);
                pw.Write(fileLength);
                client.Send(pw.GetBytes());
            }

            // 
            using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read))
            {
                int read = 0, offset = 0;
                byte[] fileChunk = new byte[8191];
                while (offset < fileLength)
                {
                    // pass message with file chunks, conatins
                    // 1 byte = header
                    // 8195 bytes = file chunk
                    using (PacketWriter pw = new PacketWriter())
                    {
                        fs.Position = offset;
                        read = fs.Read(fileChunk, 0, fileChunk.Length);
                        pw.Write((byte)Headers.Chunk);
                        pw.Write(fileChunk, 0, read);
                        client.Send(pw.GetBytes());
                    }

                    offset += read;
                }
            }
        }
    }
}

还有辅助类:

public class PacketWriter : BinaryWriter
{
    private MemoryStream memoryStream;

    public PacketWriter() : base()
    {
        memoryStream = new MemoryStream();
        OutStream = memoryStream;
    }

    public byte[] GetBytes()
    {
        byte[] data = memoryStream.ToArray();

        return data;
    }
}

public class PacketReader : BinaryReader
{
    public PacketReader(byte[] data) : base(new MemoryStream(data))
    {
        binaryFormatter = new BinaryFormatter();
    }
}

客户端的代码几乎相同。接收除外:

internal override void OnReceiveMessage(ServerConnection client, byte hdr, byte[] data)
{
    Headers header = (Headers)hdr;

    switch(header)
    {
        case Headers.Start:
            Start(data); // save length of file and prepare for receiving it
            break;
        case Headers.Chunk:
            Chunk(data); 
            break;
        default:
            Console.WriteLine("Wrong header received {0}", header);
            break;
    }
}

private void Chunk(byte[] data)
{
    // Process reveived data, write it into a file
}

【问题讨论】:

  • fileLength的数据类型是什么?
  • fileLength 是整数
  • 那不是一个类型,它是一组类型

标签: c# .net networking tcp


【解决方案1】:

发送 64kB 后 TCP 套接字会自动关闭吗?

不,TCP 套接字不会在发送 64 KB 数据后自动关闭,也不会在发送任何个数据量后自动关闭。它们保持打开状态,直到任一端关闭连接或发生某种网络错误。


不幸的是,您的问题非常模糊,几乎没有提供任何上下文。但我会指出您的客户端代码中的一个潜在严重错误:您没有将客户端读取的数据限制为您期望当前文件读取的数据。如果服务器发送多个文件,则完全有可能并且很可能在到达当前文件的数据末尾时,包含最终字节序列的读取也将包含初始序列下一个文件的字节数。

因此,表达式totalBytesRead == fileLenght 将永远不会是true,并且您的代码将尝试读取服务器发送的所有数据,就好像它们都是第一个文件的一部分一样。

检查这种情况的一种简单方法是查看客户端写入的文件的大小。如果是上述问题,它将比预期的要大得多。

您可以通过最多只接收您实际期望剩余的字节数来修复代码:

while ((bytesRead = binaryReader.Read(
    buffer, 0, Math.Min(fileLenght - totalBytesRead, buffer.Length))) > 0)
{
    fs.Write(buffer, 0, bytesRead);

    totalBytesRead += bytesRead;

    if (totalBytesRead == fileLenght)
    {
        break;
    }
}

当您使用它时,您可能会修复名为fileLenght 的变量的拼写。我知道 Intellisense 可以很容易地键入变量名,无论它是否拼写正确,但如果你有一天必须通过 grep 查找包含“长度”值的代码的源代码,你会错过那个变量。


编辑:

在您最初随问题发布的代码中指出了问题后,在您最近对该问题进行编辑后,我现在发现自己正在查看完全不同的代码。该示例仍然不是a good, minimal, complete code example,因此建议仍然必须具有有限的性质。但我可以在您的接收处理中看到至少两个主要问题:

  1. 您正在混合Receive() 和BeginReceive()。总的来说,这可能不是太可怕;在您的情况下真正糟糕的是在初始异步接收的完成处理程序中,您在使用同步Receive() 来(尝试)接收其余部分时阻塞线程数据,然后更糟糕的是,在 FileServer 的情况下,您在将文件数据传输到客户端时继续阻塞该线程。

    I/O 完成处理程序必须不在任何相当长的时间内阻塞线程;他们必须检索已完成操作的数据,并尽快返回。有很大的回旋余地,但不足以证明为整个文件传输占用 IOCP 线程是合理的。
  2. 虽然第一个问题很糟糕,但它可能还不足以真正导致连接重置。在低容量场景中,特别是如果您现在一次只与一个客户端打交道,我怀疑阻塞用于调用您的 I/O 完成回调的 IOCP 线程会导致任何容易观察到的问题。
    更糟糕的是,您分配了一次 buffer 数组,长度为 8192,然后继续尝试填充正在发送的整个计数字节流。假设客户端将发送短消息,这在服务器端可能没问题。但是在客户端,size 的值很容易大于buffer 数组中分配的 8192 字节。

    我发现您的 BeginReceiveCallback() 方法很可能抛出了 @987654335 @ 在接收到前 8192 个字节后,由于您向 Receive() 方法传递了一个超过 buffer 数组长度的 size 值。我无法证明,因为代码示例不完整。您必须自己跟进该理论。


只要我正在查看您发布的代码,当我扫描它时,就会发现其他一些违规行为:

  1. 在传输文件时使用 8191 的缓冲区长度有点奇怪。
  2. 您的BeginReceiveCallback() 方法会丢弃最初发送的任何字节。您调用EndReceive(),它返回接收到的字节数,但下面的代码假定流中的size 值仍在等待读取,此时它很容易在第一个字节块中发送.实际上,您的比较 bytesReceived &gt;= 4 似乎是为了避免处理接收到的数据,直到您获得该长度值。另一个原因是错误的:
  3. 如果您的标头是 1 字节后跟 4 字节的字节计数值,那么您确实需要 bytesReceived &gt; 4。但更糟糕的是,如果 TCP 真的愿意,它可以一次滴出一个字节(它不会,但如果你正确地编写代码,那就没关系了)。由于您仅在获得至少 4 个字节时才处理接收到的数据,并且由于如果您没有获得 4 个字节,您只需忽略到目前为止发送的任何内容,理论上您可能会忽略 all已发送的数据。

您有可能完全忽略任何数据的风险是可怕的。但是上面的#2 保证它,而#3 只是雪上加霜。

  1. 当接收到size 值时,您正在读取一个 32 位整数,但是当发送 size 值(即对于文件本身)时,您正在写入一个 64 位整数。你会在接收端得到正确的数字,因为BinaryWriter 使用小端,这意味着任何小于 2^31 的 64 位整数的前四个字节与 32 位的四个字节相同相同数字的整数表示。但是在那之后你仍然会得到四个额外的零字节;如果将其视为文件数据,则可能会损坏您的文件。

【讨论】:

  • 是的,这段代码并不完美,但它可以工作。主要问题是为什么在发送 64kB 后连接会中断。实际上,TCP 的内部缓冲区大小是 64kB,但我怎样才能清除其中的数据呢?服务器发送长度为 1024 字节的“数据包”或“块”(我知道,TCP 使用流)(TCP 缓冲区现在增加 1024 字节)。该数据开始传输到客户端。客户端接收该块并附加到文件。这些步骤会重复几次,TCP 缓冲区就会增长。为什么?数据数据已发送和接收!我有更复杂和干净的代码,这只是示例
  • “但它有效”——我不同意。如果代码有效,您就不会发布您的问题,并且我在上面的回答中至少描述了它不起作用的一种重要方式(以及回答的原始问题是否TCP 套接字自动关闭)。如果没有更完整的代码示例和更好的上下文,就不可能提供比上述更具体的建议。但是您的问题足以确定代码实际上不起作用。
  • 至于您评论中关于“TCP 缓冲区”的具体问题,我不清楚您在说哪个 缓冲区。但是,如果您以某种方式检查网络堆栈中 TCP 协议的底层实现,请记住 TCP 包含一个“窗口”,它允许传输的数据先于确认的数据;这需要一个中间缓冲区,该缓冲区将在达到其最大长度之前随着时间的推移而填充,然后仅在客户端接收到最后一个字节时才清空。
  • 我得到了错误代码 10053 的这个“主机断开”异常。我仍然无法解决问题,知道如何以正确方式发送文件吗?
  • “知道如何以正确的方式发送文件”——你认为我上面的回答是为了什么?您是否尝试过我对您的代码提出的修复?由于您没有提供好的minimal reproducible example,因此我不知道您的代码中存在任何其他具体问题,但以上内容将解决我看到的一个明显问题。你应该从那里开始;如果您还有其他问题,请发布一个新问题,并提供一个很好的代码示例。
猜你喜欢
  • 2016-10-08
  • 2013-01-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-09
  • 2014-12-15
  • 1970-01-01
相关资源
最近更新 更多