Unity网路编程-TCP实现细节备忘

定制协议#

TCP是基于二进制数据流的,因此发送数据时,需要告诉接受方,消息之间应该如何分割。
一个消息是我们定义好的协议的实例,比如登录消息,消息内含有用户名和密码字段。假设现在有2个10K长度的消息,TCP会根据网络情况,分多次接受到数据,可能是5K+6K+9K收完,也可能是一次收到20K,也可能是其他情况。假如我们第一次收到了5K,那么我们需要先缓存起来,等待消息的剩余5K收到进行处理。第二次是6K,假设我们规定每个包一定是10K,那么,6K中的5K是第一个包的,剩下1K是第二个包的,我们得到了一个完整的第一个包(10K)和的第二个包的一部分(1K)
所以我们需要明确的知道,每个消息的长度,以便对接受到的数据进行分割。另外,包体内的不定长数据,比如字符串,也需要告诉接收方长度。
消息结构一般采用固定包头+包体的形式,包头中包含数据包体的长度。
这样客户端在收到socket的数据流之后,根据包头内告知的包体长度进行分包,如果分包逻辑实现的不正确,会发生“粘包”。

Copy
public void Write(string value) { byte[] bytes = Encoding.UTF8.GetBytes(value); Write(bytes.Length); //告知接收方,字符串长度 Write(bytes); }

字节流处理相关#

网络字节流规定约定使用大端表示法#

RFC1700 stated it must be so. (and defined network byte order as big-endian).The convention in the documentation of Internet Protocols is to express numbers in decimal and to picture data in "big-endian" order [COHEN]. That is, fields are described left to right, with the most significant octet on the left and the least significant octet on the right.

BitConverter的大小端问题#

GetBytes 方法重载返回的数组中的字节顺序(以及由 DoubleToInt64Bits 方法返回的整数中的位顺序和 ToString(Byte[]) 方法返回的十六进制字符串的顺序)取决于计算机体系结构是小字节序还是大字节序。体系结构的字节排序方式由 IsLittleEndian 属性表示,该属性在小 endian 系统上返回 true,在大字节序系统上返回 false。

如果发送和接收数据的系统可以具有不同的字节顺序,则始终按特定顺序传输数据。 这意味着数组中的字节顺序可能需要在发送它们之前或接收后进行反向。 常见的约定是以网络字节顺序(大字节序顺序)传输数据。

如果发送和接收数据的系统可以具有不同的字节序,并且要传输的数据包含带符号的整数,则调用 IPAddress.HostToNetworkOrder 方法将数据转换为网络字节顺序,并使用 IPAddress.NetworkToHostOrder 方法将其转换为收件人所需的顺序。

MemoryStream细节#

1.ToArray()会产生新的内存拷贝导致内存使用增加,尽量避免频繁使用
2.始终是按小端处理数据
3.在构造参数中使用byte[],无法自动扩容

Copy
byte[] buffer = new byte[10]; MemoryStream ms = new MemoryStream(buffer); BinaryWriter br = new BinaryWriter(ms); br.Write(100); br.Write(200); br.Write(300);      //出错,无法扩容buffer

Array.Copy和Buffer.BlockCopy进行数据缓存操作#

1.如果 sourceArray 和 destinationArray 重叠,此方法的行为就像在覆盖 destinationArray 之前,在临时位置保留 sourceArray 的原始值。

2.Buffer.BlockCopy是基于bytes的,比Array.Copy更快

3.当socket收到数据包不完整的时候,需要保留这部分数据,等待剩下的数据。
但已经完整的包的bytes需要从byte[]缓存中移除,将剩下的不完整包数据拷贝到bytes的头位置,如此循环往复

可以使用环形缓冲区实现接受数据处理结构#

The useful property of a circular buffer is that it does not need to have its elements shuffled around when one is consumed. (If a non-circular buffer were used then it would be necessary to shift all elements when one is consumed.) In other words, the circular buffer is well-suited as a FIFO buffer while a standard, non-circular buffer is well suited as a LIFO buffer.

NetworkStream#

可以在 NetworkStream 类的实例上同时执行读写操作,而无需进行同步。 只要有一个唯一的线程用于写入操作,另一个线程用于读取操作,读取和写入线程之间就不会有交叉干扰,也不需要进行同步。

2. socket使用篇#

Connect属性#

connected只表示  是在上次 还是 操作时连接到远程主机。如果在这之后[连接的另一方]断开了,它还一直返回true, 除非你再通过socket来发送数据。所以通过个属性来判断是行不通的!

BeginReceive()方法#

我们调用BeginReceive的时候,系统会开启一个独立的线程(使用线程池),用以执行回调函数及对EndReceive的阻塞(block),直到EndReceive从socket的缓冲区中读到数据或者socket引发异常。
简单的理解为,如果在回调函数调用之前使用EndReceive方法,那么线程会被阻塞,直到读到了数据,或者发生错误。所以,最常用的写法是,在回调函数中使用EndReceive方法。EndReceive方法是必须要使用的。EndReiceive方法有一个返回值,代表接受数据的长度。

socket连接超时#

socket没有提供连接超时功能,需要写一个timer定时器,来处理连接超时

close之前始终调用shutdown#

使用面向连接的 Socket时,请在关闭 Socket之前始终调用 Shutdown 方法。 这可确保所有数据在连接的套接字关闭之前都已发送和接收。调用 Close 方法,释放与 Socket关联的所有托管资源和非托管资源。 请勿尝试在关闭后重用 Socket。

对于面向连接的协议,建议你在调用 Close 方法之前调用 Shutdown。 这可确保所有数据在连接的套接字关闭之前都已发送和接收。

Close 方法会关闭远程主机连接,并释放与 Socket关联的所有托管资源和非托管资源。 关闭时,Connected 属性设置为 false。

如果远程主机使用 Shutdown 方法关闭 Socket 连接,并且收到所有可用数据,则 EndReceive 方法会立即完成并返回零字节。

需要注意的是,客户端调用ShutDown,服务端收到0字节但没有做断开处理,会导致客户端卡住无法断开连接

多线程问题#

异常处理#

心跳机制#

断线重连#

参考连接#

C# Socket 如何判断连接已断开

posted @   jeoyao  阅读(349)  评论(0编辑  收藏  举报
编辑推荐:
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示
目录