一 、完整发送数据
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+=