想了半天,也没想出个合适的标题,还是描述问题吧
(1)客户端向服务端发送指令,期望获得回答
(2)服务端响应客户端请求,并返回答案
这看起来是一个非常简单的过程,比如客户端发送一条json格式的指令
{"id":"2342"}
服务器收到消息后解析json数据,返回id为2342的用户的信息
{"name":"sheng","age":"29"}
很好哦,顺利通信。但是呢,由于客户端极为频繁的发送数据,且网络状况不佳,会出现这样的情况,两条请求同时到达服务端,之前,服务端调用recv函数只收到一个json格式的请求,但现在,却收到了两个,那么服务端就无法再像之前那样有效的解析这条命令了。到此为止你发现,之前的做法是有问题的,之前的做法默认一次recv函数调用从缓冲区里获得指令是一条,但现实的情况是也可能是两条。
这个就是我们在工作中真真实实遇到的问题。还有一种情况,由于网络问题,一条完整的消息被分成了多段被接收。说到这里,有些同学已经疑惑了,客户端把一条指令发送过来,服务端怎么还分段来接收呢?这就涉及到socket的消息传输机制了。当我们调用send函数时,我们以为把一条消息发送出去了,但事实上呢,并没有。这个send函数仅仅是把我们的消息写入到了发送缓冲区,这个大小通常为64K,接下来,由socket协议自己来决定什么时候把这部分消息发送出去。当我们调用recv函数时,并不是说从客户端获得数据,客户端发来的数据要进入到服务端的接收缓冲区,recv只是说从那里获得数据。客户端发来的数据,一部分已经到了,一部分还在路上呢,这个时候你去接收,那么只是接收已经到了的部分,这种情况在网络状况不好的时候容易发生。
现在,你明白了,socket通信可不像我们平时对话,我说一句,你就听到一句,而可能是我说了一句,你没听见,一秒钟以后我又说了一句,虽然两句话间隔了一秒钟,但是他们都被写入到发送缓冲区中然后首尾相连一起发送给你了,那么你收到的时候还以为是一句话呢!我们为了保证socket长连接,还要发送心跳包,同理,这些心跳包也可能与我们正常发送的数据掺杂在一起,网络状况好或者消息发送不频繁时这些问题不会出现,但网络状况不总是好,我们也不总是那么清闲。
有什么简单的解决办法呢?
既然消息可能分段到达,那么我们就用一种方法把分段到达的数据拼接起来;既然多个消息可能同时到达,我们就把他们拆开。如何做到,我们自己建立一个消息的缓冲区,同时,在消息的最前面加一个标识用来标识本条消息的长度。
假设每一次发送的消息的长度都小于10万,那么我们用长度为5的标识位就足够了,假如一条消息本身长1088,那么就在这个消息的最前面加上标识位01088。消息到达时,我们先用recv函数接收,然后放入我们自己的缓冲区,然后再来解析缓冲区里的内容,第一次调用recv时,不管来了几条消息还是一条消息的某一部分,我们只要确定缓冲区里的数据的长度大于5就可以了,因为大于5,我们就知道了消息的长度,那么只需要判断省下来的数据长度到底够不够一条消息,如果够就从缓冲区里取出来,这就是消息的拆分,如果不够呢,就继续接收,这就是消息的拼接。
下面是C#实现的代码
namespace TestUpLoadAndDownLoad
{
public class CmdContainer
{
public byte[] m_CmdBuff { get; set; } // 缓存收到的指令
public int m_BuffLen { get; set; } // 缓存区指令的长度
public int m_BuffSize { get; set; } // 缓存区容量
public Queue<string> m_CmdQueue { set; get; } // 存储已经收到的指令,该指令是多条完整的指令
private const int m_iHead = 5; // 在每条指令的前端加5个数字标识该条指令的长度,00324 标识指令长324
public CmdContainer(int iSize)
{
m_CmdBuff = new byte[iSize];
m_BuffLen = 0;
m_BuffSize = iSize;
m_CmdQueue = new Queue<string>();
}
// 尝试获得指令,如果有指令可以获取就返回true
public bool GetCmd(byte[] buff, int len)
{
// 装不下的时候,扩容
if(m_BuffLen + len > m_BuffSize)
{
byte[] tmp = new byte[m_BuffLen + len];
Array.Copy(m_CmdBuff, 0, tmp, 0, m_BuffLen);
m_CmdBuff = tmp;
m_BuffSize = m_BuffLen + len;
}
Array.Copy(buff, 0, m_CmdBuff, m_BuffLen, len);
m_BuffLen += len;
GetCmd();
return m_CmdQueue.Count==0?false:true;
}
// 根据缓存区的内容提取指令
private void GetCmd()
{
int iStart = 0;
while(true)
{
//无法读取命令长度
if (m_BuffLen - iStart < m_iHead)
{
m_BuffLen = m_BuffLen - iStart;
return;
}
// 读取命令长度
string strLenth = Encoding.GetEncoding("utf-8").GetString(m_CmdBuff, iStart, m_iHead);
int iCmdLen = Int32.Parse(strLenth);
// 剩余的字符串不能组成一个有效的命令
if (m_BuffLen - iStart - m_iHead < iCmdLen)
{
int tmpLen = m_BuffLen - iStart ;
byte[] tmp = new byte[tmpLen];
Array.Copy(m_CmdBuff, iStart, tmp, 0, tmpLen);
Array.Copy(tmp, 0, m_CmdBuff, 0, tmpLen);
m_BuffLen = tmpLen;
return;
}
else
{
// 命令可以被截取
string strCmd = Encoding.GetEncoding("utf-8").GetString(m_CmdBuff, iStart + m_iHead, iCmdLen);
m_CmdQueue.Enqueue(strCmd);
iStart = iStart + m_iHead + iCmdLen;
}
}
}
}
}
服务端代码
private void processClient(object obj)
{
CmdContainer cmd = new CmdContainer(1024);
Socket socket = (Socket)obj;
while(true)
{
byte[] data = null;
int len = 0;
try
{
data = new Byte[1024];//client.Available
len = socket.Receive(data, SocketFlags.None);//接收客户端套接字数据
}
catch (Exception e){
}
if(!cmd.GetCmd(data,len))
{
continue;
}
while(cmd.m_CmdQueue.Count!=0)
{
string strCmd = cmd.m_CmdQueue.Dequeue();
//从这里开始,我们就可以根据指令进行应答操作了
}
}
}
抱歉我这里没有客户端代码,测试时我为了方便,用了一个socket工具,即便如此,如果你已经看懂了上面两端代码,客户端也就随手写出来了