异步 TCP 服务器 - 消息帧建议 [英] Async TCP Server - Message Framing Advice

查看:20
本文介绍了异步 TCP 服务器 - 消息帧建议的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在 C# 中有一个异步 TCP 套接字服务器,我使用 Socket 的 TcpListener/TcpClient 包装器制作了它.我对网络一般很陌生,所以我不知道 TCP 如何处理发送的数据,以及它如何不保留消息边界.经过一番研究,我想我已经提出了一个可靠的解决方案,但我想知道是否有人对我有更多建议.

I have an asynchronous TCP socket server in C# that I've made using the TcpListener/TcpClient wrappers for Socket. I'm pretty new to networking in general so I was unaware of how TCP treats data sent, and how it doesn't preserve the message boundaries. After a little research I think I've come up with a solid solution but I'm wondering if anyone has more advice for me.

目前我发送的数据是使用 protobuf(Google 的数据交换库)的序列化类对象的 byte[] https://code.google.com/p/protobuf/

Currently the data I'm sending is a byte[] of a serialized class object using protobuf (Google's data exchange library) https://code.google.com/p/protobuf/

在我序列化我的数据之后,在发送之前,我决定在字节数组的开头附加一个 4 字节的 Int32.我的想法是,当数据包发送到服务器时,它会解析出 Int32,然后等到它收到那个字节数后再对数据做任何事情,否则只需再次调用 BeginRead.

After I serialize my data, and before it's sent, I've decided to append a 4 byte Int32 at the beginning of the byte array. My idea is that when the packet is sent to the server, it will parse out the Int32 and then wait until it's received that number of bytes before doing anything with the data, otherwise just calling BeginRead again.

这是在我写入数据之前运行的代码,它似乎工作正常,但我愿意接受任何性能建议:

Here is the code it's ran through before I write the data, and it seems to work fine, but I'm open to any performance suggestions:

public byte[] PackByteArrayForSending(byte[] data)
{
    // [0-3] Packet Length 
    // [3-*] original data

    // Get Int32 of the packet length
    byte[] packetLength = BitConverter.GetBytes(data.Length);
    // Allocate a new byte[] destination array the size of the original data length plus the packet length array size
    byte[] dest = new byte[packetLength.Length + data.Length];

    // Copy the packetLength array to the dest array
    Buffer.BlockCopy(packetLength, 0, dest, 0, packetLength.Length);
    // Copy the data array to the dest array
    Buffer.BlockCopy(data, 0, dest, packetLength.Length, data.Length);

    return dest;
}

我在服务器端有点卡住了.我让它读取 packetLength 变量,方法是使用 Buffer.BlockCopy 复制前 4 个字节,然后使用 BitConverter.ToInt32 读取我应该得到的长度.我不确定是否应该不断地将传入的数据读入特定于客户端的 Stream 对象,或者只使用 while 循环.这是到目前为止我在服务器端的代码示例:

I'm a little stuck on the server end. I have it reading the packetLength variable by using Buffer.BlockCopy to copy the first 4 bytes and then BitConverter.ToInt32 to read the length I should be getting. I'm not sure if I should constantly read the incoming data into a client-specific Stream object, or just use a while loop. Here's an example of the code I have on the server end so far:

NetworkStream networkStream = client.NetworkStream;
int bytesRead = networkStream.EndRead(ar);

if (bytesRead == 0)
{
  Console.WriteLine("Got 0 bytes from {0}, marking as OFFLINE.", client.User.Username);
  RemoveClient(client);
}

Console.WriteLine("Received {0} bytes.", bytesRead);

// Allocate a new byte array the size of the data that needs to be deseralized.
byte[] data = new byte[bytesRead];

// Copy the byte array into the toDeserialize buffer
Buffer.BlockCopy(
  client.Buffer,
  0,
  data,
  0,
  bytesRead);

// Read the first Int32 tp get the packet length and then use the byte[4] to get the packetLength
byte[] packetLengthBytes = new byte[4];
Buffer.BlockCopy(
  data,
  0,
  packetLengthBytes,
  0,
  packetLengthBytes.Length);

int packetLength = BitConverter.ToInt32(packetLengthBytes, 0);

// Now what do you recommend?

// If not all data is received, call 
// networkStream.BeginRead(client.Buffer, 0, client.Buffer.Length, ReadCallback, client);
// and preserve the initial data in the client object

感谢您的时间和建议,我期待更多地了解这个主题.

Thanks for your time and advice, I look forward to learning more about this subject.

推荐答案

TCP 保证塞入流的一端的字节会以相同的顺序从另一端掉出,不会丢失或重复.仅此而已,当然不支持大于一个字节的实体.

TCP guarantees that the bytes stuffed into the stream at one end will fall out the other end in the same order and without loss or duplication. Expect nothing more, certainly not support for entities larger than a byte.

通常我会在标题前加上消息长度、类型和子类型.通常会提供相关 ID 以将请求与响应进行匹配.

Typically I have prepended a header with a message length, type and subtype. There is often a correlation id provided to match requests to responses.

基本模式是获取字节并将它们附加到缓冲区.如果缓冲区中的数据足以包含消息头,则提取消息长度.如果缓冲区中的数据足以包含该消息,则从缓冲区中删除该消息并对其进行处理.对任何剩余数据重复,直到没有完整的消息要处理.根据您的应用程序,这可能是等待读取或检查流以获取其他数据的点.某些应用程序可能需要不断从单独的线程读取流,以避免限制发送方.

The basic pattern is to get bytes and append them to a buffer. If the data in the buffer is sufficient to contain a message header then extract the message length. If the data in the buffer is sufficient to contain the message then remove the message from the buffer and process it. Repeat with any remaining data until there are no complete messages to process. Depending on your application this may be the point to wait on a read or check the stream for additional data. Some applications may need to keep reading the stream from a separate thread to avoid throttling the sender.

请注意,您不能假设您拥有完整的消息长度字段.处理完一条消息后,您可能还剩下三个字节,并且无法提取 int.

Note that you cannot assume that you have a complete message length field. You may have three bytes left after processing a message and cannot extract an int.

根据您的消息,使用循环缓冲区可能比每次处理消息时对任何剩余字节进行混洗更有效.

Depending on your messages it may be more efficient to use a circular buffer rather than shuffling any remaining bytes each time a message is processed.

这篇关于异步 TCP 服务器 - 消息帧建议的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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