C# 发送TCP消息的二进制格式化程序-确定消息的结尾

C# 发送TCP消息的二进制格式化程序-确定消息的结尾,c#,serialization,tcp,binaryformatter,C#,Serialization,Tcp,Binaryformatter,我正在使用BinaryFormatter.Serialize方法发送TCP消息。我正在序列化的类的格式为: [Serializable] public class Message { public int senderId; public int metaData; public foo moreMetaData; public object[] message; } 我知道,一般来说,有三种方法可以确定任何消息的结尾: 前置大

我正在使用BinaryFormatter.Serialize方法发送TCP消息。我正在序列化的类的格式为:

[Serializable]
public class Message {
        public int senderId;
        public int metaData;
        public foo moreMetaData;
        public object[] message;
}
我知道,一般来说,有三种方法可以确定任何消息的结尾:

  • 前置大小字节
  • 追加消息结尾字节
  • 固定消息长度

第三种选择似乎是个糟糕的主意。如果使用第二个选项,我如何向流追加一个字节,并且仍然能够在接收端调用BinaryFormatter.deserialize?如果我使用第一个选项(很抱歉向后浏览列表),我会遇到与选项二相同的问题(除了前置),并且在序列化之前还要确定序列化的大小,如果不序列化两次(一次到一个伪变量中以确定大小),这似乎是不可能的,然后再进入真正的流缓冲区。这里通常做什么?

二进制格式化程序已经在内部实现了“前置大小字节”。您只需将
NetworkStream
对象传入该方法,它就可以自己计算出需要读取多少字节

注意:二进制格式化程序对程序集中的版本差异非常敏感。如果一端有一个版本的程序,另一端有一个稍旧的版本,则两端可能无法相互通信。我建议使用不将模型与程序集版本号绑定的二进制序列化程序。是一个很好的图书馆

编辑:下面是一个如何进行编辑的示例

private async Task MessageLoop(NetworkStream networkStream)
{
    //Lets pretend our protocall sends a byte with:
    // - 1 if the next object will be a Foo,
    // - 2 if the next object will be a Bar
    // - 3 if the next object will be a Int32.

    var formatter = new BinaryFormatter();
    byte[] buffer = new byte[1024];

    while (true)
    {
        var read = await networkStream.ReadAsync(buffer, 0, 1).ConfigureAwait(false);
        if (read < 0)
        {
            await LogStreamDisconnectAsync();
        }

        switch (buffer[0])
        {
            case 1:
                //If we are on a SynchronizationContext run the deseralize function on a new thread because that call will block.
                Func<Foo> desearalize = ()=> (Foo)formatter.Deserialize(networkStream);
                Foo foo;
                if (SynchronizationContext.Current != null)
                {
                    foo = await Task.Run(desearalize).ConfigureAwait(false);
                }
                else
                {
                    foo = desearalize();
                }

                await ProcessFooAsync(foo).ConfigureAwait(false);
                break;
            case 2:
                var bar = await Task.Run(() => (Bar)formatter.Deserialize(networkStream)).ConfigureAwait(false);
                await ProcessBarAsync(bar).ConfigureAwait(false);
                break;
            case 3:

                //We have to loop on Read because we may not get 4 bytes back when we do the call, so we keep calling till we fill our buffer.
                var bytesRead = 0;
                while (bytesRead < 4)
                {
                    //We don't want to overwrite the buffer[0] so we can see the value in the debugger if we want, so we do 1 + bytesRead as the offset.
                    bytesRead += await networkStream.ReadAsync(buffer, 1 + bytesRead, 4 - bytesRead).ConfigureAwait(false);
                }

                //This assumes both ends have the same value for BitConverter.IsLittleEndian
                int num = BitConverter.ToInt32(buffer, 1);

                await DoSomethingWithANumberAsync(num).ConfigureAwait(false);

                return;
            default:
                await LogInvaidRequestTypeAsync(buffer[0]).ConfigureAwait(false);
                return;
        }
    }

}
专用异步任务MessageLoop(NetworkStream NetworkStream)
{
//让我们假设我们的protocall发送一个字节,其中包含:
//-1如果下一个对象是Foo,
//-2如果下一个对象是条形图
//-3如果下一个对象是Int32。
var formatter=新的二进制格式化程序();
字节[]缓冲区=新字节[1024];
while(true)
{
var read=await networkStream.ReadAsync(缓冲区,0,1).ConfigureAwait(false);
如果(读取<0)
{
等待LogStreamDisconnectAsync();
}
开关(缓冲区[0])
{
案例1:
//如果我们在SynchronizationContext上,请在新线程上运行反序列化函数,因为该调用将被阻止。
Func desearalize=()=>(Foo)格式化程序。反序列化(networkStream);
富富,;
if(SynchronizationContext.Current!=null)
{
foo=await Task.Run(desearalize).ConfigureAwait(false);
}
其他的
{
foo=desearalize();
}
wait processfoosync(foo).configurewait(false);
打破
案例2:
var bar=await Task.Run(()=>(bar)格式化程序.反序列化(networkStream)).ConfigureAwait(false);
wait ProcessBarAsync(bar).configurewait(false);
打破
案例3:
//我们必须在读取时循环,因为我们在调用时可能无法返回4个字节,所以我们会一直调用,直到填满缓冲区。
var bytesRead=0;
while(字节数<4)
{
//我们不想覆盖缓冲区[0],因此如果需要,可以在调试器中查看该值,因此我们将1+字节读取作为偏移量。
bytesRead+=await networkStream.ReadAsync(缓冲区,1+bytesRead,4-bytesRead)。ConfigureAwait(false);
}
//这假定BitConverter.IsLittleEndian的两端具有相同的值
int num=BitConverter.ToInt32(缓冲区,1);
wait dosomethingWithumberasync(num).configurewait(false);
返回;
违约:
等待LoginWaid请求类型异步(缓冲区[0])。配置等待(false);
返回;
}
}
}

我使用的是
字节[]buf=新字节[1024];等待networkStream.ReadAsync(buf,0,buf.Length)
从缓冲区读取我的消息。如何确定何时调用BinaryFormatter.deserialize?@craskFish如果使用ReadAsync读取的内容仅足以说明下一个消息类型,则将流交给Desearalize函数。我用一个例子更新了我的答案。谢谢。它变得越来越清晰。然而,我仍然有一些关于这个例子的问题。这如何确定在
var read=await networkStream.ReadAsync(缓冲区,0,1)、ConfigureAwait(false)之后何时接收其余数据。我假设这与SynchronizationContext.Current
和ConfigureAwait(false)有关,但我不明白这些是什么或做什么。还有,为什么这里有这么多异步调用?一旦接收到数据,我们就不能同步进行反序列化()和processFoo()之类的调用吗?如果您在UI线程上,并且等待的事情在调用返回之前就完成了(例如,如果您从
ReadAsync
读取的数据已经在操作系统的网络缓冲区中)调用从不切换上下文,因此
ConfigureAwait(false)
对其没有影响。我之所以进行如此多的异步调用,是因为我想让一些函数处理数据,并且我想表明这些进程不会阻塞线程。至于“何时”处理数据,只要我们读入的第一个字节可用,它就会开始处理,然后
格式化程序.反序列化(networkStream)
将一直阻止,直到NetworkStream传输完所有数据,这就是为什么我们将其置于
任务中。如果我们处于同步上下文中,请运行
,这样我们就不会阻止UI线程。