Socket 完整发送和高效接受数据流

发布日期:2025-06-30 20:38:15 分类:365外围用手机注册吗 浏览:5050

一 、完整发送数据

Send方法会把要发送的数据存入操作系统的发送缓冲区,然后返回成功写入的字节数。这句话的另一层含义是,**对于那些没有成功发送的数据,程序需要把它们保存起来,在适当的时机再次发送。**由于在网络通畅的环境下,Send只发送部分数据的概率并不高,但是也有小概率情况发生,需要解决该问题。

1.1 不完整发送示例

以异步聊天客户端为例,假设操作系统缓冲区被设置得很小,只有8个字节,再假设网络环境很差,缓冲区的数据没能及地的发送出去。如图所示,假设客户端发送字符串“hero”,发送后,Send返回6(包含两字节的长度),数据全部存入操作系统缓冲区中。但此时网络拥堵,TCP尚未把数据发送给服务端。此时,客户端又发送了字符串“cat”,由于操作系统的发送缓冲区只剩下2字节空位,只有代表数据长度的“03”被写入缓冲区(图4-27步骤②)。此时,网络环境有所改善,TCP成功把缓冲区的数据发送给服务端,操作系统缓冲区被清空,如图4-27步骤③所示。稍后,客户端又发送了字符串“hi”,数据成功发送。

对于服务端而言,接收到的数据是“04hero0302hi”,第一个字符串“hero”可以被解析,但对于后续的“0302hi”,服务端会解析成一串3个字节的数据“02h”,以及不完整的长度信息“i”。“04hero”往后的数据全部无法解析,通信失败。

说人话:在操作系统缓存区很小,网络环境很差的情况下。消息体不完整无法解析“0302hi”导致通信失败

1.2 如何解决发送不完整问题

要让数据能够发送完整,需要在发送前将数据保存起来;如果发送不完整,在Send回调函数中继续发送数据,

//定义发送缓冲区

byte[] sendBytes = new byte[1024];

//缓冲区偏移值

int readIdx = 0;

//缓冲区剩余长度

int length = 0;

//点击发送按钮

public void Send()

{

sendBytes = 要发送的数据;

length = sendBytes.Length; //数据长度

readIdx = 0;

socket.BeginSend(sendBytes, 0, length, 0, SendCallback, socket);

}

//Send回调

public void SendCallback(IAsyncResult ar){

//获取state

Socket socket = (Socket) ar.AsyncState;

//EndSend的处理

int count = socket.EndSend(ar);

readIdx + =count;

length -= count;

//继续发送

if(length > 0){

socket.BeginSend(sendBytes,

readIdx, length, 0, SendCallback, socket);

}

}

代码解析:

//定义发送缓冲区

byte[] sendBytes = new byte[1024];

//缓冲区偏移值

int readIdx = 0;

//缓冲区剩余长度

int length = 0;

•定义发送缓冲区

•readIdx表示读取位置;length表示缓冲区中数据长度

//点击发送按钮

public void Send()

{

sendBytes = 要发送的数据;

length = sendBytes.Length; //数据长度

readIdx = 0;

socket.BeginSend(sendBytes, 0, length, 0, SendCallback, socket);

}

•发送函数

•获得发送数据的长度

•调用开始发送方法,参数为发送缓冲区(sendBytes)、发送数据的开始索引(0)、发送数据的最大长度(length)、回调函数(SendCallback),socket。

//Send回调

public void SendCallback(IAsyncResult ar){

//获取state

Socket socket = (Socket) ar.AsyncState;

//EndSend的处理

int count = socket.EndSend(ar);

readIdx + =count;

length -= count;

//继续发送

if(length > 0){

socket.BeginSend(sendBytes,

readIdx, length, 0, SendCallback, socket);

}

}

•发送回调函数

•结束发送处理。读取索引的值增加为已经发送消息的长度;发送缓存区数据长度减去已经发送消息的长度。

•继续发送。如果发送缓存区还有数据,再调用开始发送方法。

一步一步来解析上面的代码。假如要发送的数据是“08hellolpy”,在调用BeginSend时,缓冲区sendBytes的数据如图4-28所示。

图解:

假设Socket只发送了6个数据,即发送了“08hell”,在SendCallback中,count返回6,程序会调整readIdx和length,使缓冲区相关的数据如图4-29所示。

此时length>0,于是程序再次调用BeginSend,发送剩余的数据。BeginSend的参数解释如下:

socket.BeginSend(sendBytes, //发送缓冲区

readIdx, //从索引为6的数据开始发送

length, //因为缓冲区只剩下4个数据,最多发送4个数据

0, //标志位,设置为0即可

SendCallback, //回调函数

socket); //传给回调函数的对象

如果再次调用的BeginSend能够把数据发完,那万事大吉。如果不能完整发送,第二次BeginSend的回调函数也会把剩余的数据发送出去。

缓存区中有多条数据,用读取索引(readIdx)来获得应该发送的数据的开始位置

1.3 写入队列

上面的方案解决了一半问题,因为调用BeginSend之后,可能要隔一段时间才会调用回调函数,如果玩家在SendCallback被调用之前再次点击发送按钮,按照前面的写法,会重置readIdx和length, SendCallback也就不可能正确工作了。为此我们设计了加强版的发送缓冲区,叫作写入队列(writeQueue)。

说人话:为了防止再次点击发送重置readIdx和length设计写入队列,让每条消息都有readIdx和length

上图展示了一个包含三个缓冲区的写入队列,当玩家点击发送按钮时,数据会被写入队列的末尾,比如一开始发送“08hellolpy”,那么就在队列里添加一个缓冲区,这个缓冲区和本节前面介绍的缓冲区一样,包含一个bytes数组,以及指向缓冲区开始位置的readIdx、缓冲区剩余长度的length。Send方法会做这样的处理,示意代码如下:

代码解析:

public void Send() {

sendBytes = 要发送的数据;

writeQueue.Enqueue(ba); //假设ba封装了readbuff、readIdx、length等数据

if(writeQueue只有一条数据){

socket.BeginSend(参数略);

}

}

public void SendCallback(IAsyncResult ar){

count = socket.EndSend(ar);

ByteArray ba = writeQueue.First(); //ByteArray后面再介绍

ba.readIdx+=count; //length的处理略

if(发送不完整){

取出第一条数据,再次发送

}

else if(发送完整,且writeQueue还有数据){

删除第一条数据

取出第二条数据,如有,发送

}

}

说人话:

sendBytes = 要发送的数据;

writeQueue.Enqueue(ba); //假设ba封装了readbuff、readIdx、length等数据

•把ba数据写入到写入(Enqueue)队列(writeQueue)的末尾。

if(writeQueue只有一条数据){

socket.BeginSend(参数略);

}

•如果写入队列只有一条数据,那么发送这条数据

public void SendCallback(IAsyncResult ar){

count = socket.EndSend(ar);

ByteArray ba = writeQueue.First(); //ByteArray后面再介绍

ba.readIdx+=