在日常开发中,当设计到 TCP 通讯的时候,我们都会默认的认为它是可靠的,没有任何风险的,然而并不是这样,在网络环境复杂的情况下,tcp通讯并不是你想象中的那么厉害,强悍到完美的程度。
cp通讯的优缺点总结;
- TCP的优点: 可靠,稳定 TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源。
- TCP的缺点: 慢,效率低,占用系统资源高,易被攻击 TCP在传递数据之前,要先建连接,这会消耗时间,而且在数据传递时,确认机制、重传机制、拥塞控制机制等都会消耗大量的时间,而且要在每台设备上维护所有的传输连接,事实上,每个连接都会占用系统的CPU、内存等硬件资源。 而且,因为TCP有确认机制、三次握手机制,这些也导致TCP容易被人利用,实现DOS、DDOS、CC等攻击。
为了兼容tcp通讯的收发消息,一定程度上保证其安全可靠,我们需要对接收的数据做一些列处理,最简单的就是数据查找与拼接了。
当网络环境复杂的情况下,我们有可能只收到一半数据,或者,数据真的由于电气环境而发生了改变,导致数据成为无效数据,这样的请开给你下,我们需要在应用层添加一些列方法来避免这些错误对业务层的影响。
下面的代码时最简单的通过socket进行tcp通讯的一个例子,使用网络调试工具向服务器发送数据并检测数据,通过发送不同的数据模拟环境故障,看应用层处理是否可兼容该故障。
废话少说,代码如下(仅数据处理部分):
int rdBscpGetOnePacket(struct bufferevent *pBufev, BscpParam *pParam)
{
int ret = SERVER_ERR; //宏定义值为 -1
int result = SERVER_ERR;
int recvLen = 0;
int payloadLength = 0;
MSG_CHECK_NULL(pBufev, SERVER_ERR, ret, goto EXIT); //宏定义检测指针是否为空
MSG_CHECK_NULL(pParam, SERVER_ERR, ret, goto EXIT);
/**
* 接收bscp header + body
*/
if(pParam->bscpRecvLen < (int)sizeof(BscpPacket)) //当接收长度小于sizeof(BscpPacket)
{
result = rdReadBscpPacket(pBufev, pParam); // 读取一定长度的数据 sizeof(BscpPacket)
if(SERVER_OK != result)
{
MSG_OUT("rdReadBscpPacket() error!result:%d.\n", result);
goto EXIT;
}
}
/**
* 计算负载数据长度,申请空间
*/
payloadLength = pParam->bscp.head.length - sizeof(BscpBody);
if(pParam->pInBuf == NULL)
{
pParam->pInBuf = (char *)malloc(payloadLength);
if(NULL == pParam->pInBuf)
{
MSG_OUT("malloc err. len = %d\n", payloadLength);
goto EXIT;
}
pParam->inBufUsed = 0;
memset(pParam->pInBuf, 0x00, payloadLength);
}
/**
* 读取负载数据
*/
if(pParam->inBufUsed < payloadLength)
{
recvLen = bufferevent_read(pBufev, (pParam->pInBuf + pParam->inBufUsed), (payloadLength - pParam->inBufUsed)); // buffevent的数据读取功能函数
if(recvLen > (payloadLength - pParam->inBufUsed))
{
MSG_OUT("bscp data recv err. comd = 0x%x len1 = %u len2 = %d\n", pParam->bscp.body.command, recvLen, (payloadLength - pParam->inBufUsed));
assert(0);
}
pParam->inBufUsed += recvLen;
/* 如果数据没有接收全,不能发送到下一个模块 */
if(pParam->inBufUsed < payloadLength)
{
MSG_OUT("bscp data recv not complete,recv next . comd = 0x%x recved = %d dataLen = %d\n", pParam->bscp.body.command, pParam->inBufUsed, payloadLength);
ret = SERVER_ERR;
goto EXIT;
}
}
ret = SERVER_OK;
EXIT:
return ret;
}
上述代码逻辑清晰,结构简单,其中使用了一个固定读取数据的函数接口 rdReadBscpPacket(pBufev, pParam);,其源代码如下:、
static int rdReadBscpPacket(struct bufferevent *pBufev, BscpParam *pParam)
{
int ret = SERVER_ERR;
int len = 0;
int recvLen = 0;
int markPos = 0;
int recvTotal = 0;
char *pBuff = NULL;
MSG_CHECK_NULL(pBufev, SERVER_ERR, ret, goto EXIT);
MSG_CHECK_NULL(pParam, SERVER_ERR, ret, goto EXIT);
do
{
/**
* 读取数据, 每次读取24字节并判断有无标志头
*/
len = sizeof(BscpPacket) - pParam->bscpRecvLen;
pBuff = (char *)&pParam->bscp + pParam->bscpRecvLen;
recvLen = bufferevent_read(pBufev, pBuff, len); /* 从缓存区中读取数据 */
if(0 == recvLen)
{
MSG_OUT("bscp read cache is null, wait a moment.\n");
break;
}
if(recvLen > len) //当读取数据长度超出想要读取的长度时,代表出错,
{
MSG_OUT("read bscp packet err. recvLen = %u len = %d\n", recvLen, len);
pParam->bscpRecvLen = 0;
break;
}
/**
* 接收到的数据不足一个bscpPacket则继续接收
*/
pParam->bscpRecvLen += recvLen;
recvTotal += recvLen;
if(pParam->bscpRecvLen != sizeof(BscpPacket))
{
MSG_OUT("bscp packet read part, to continue. bscpRecvLen = %d\n", pParam->bscpRecvLen);
continue; //在while中继续接收数据,直到读取到固定长度的数据
}
/**
* 分析数据找bscp mark
*/
markPos = msgMarkCheck((char *)&pParam->bscp, pParam->bscpRecvLen);
if(markPos == 0)
{
MSG_OUT("bscp read ok. cmd = 0x%x dataLen = %d\n", pParam->bscp.body.command, pParam->bscp.head.length);
ret = 0;
break;
}
else if(markPos > 0 && markPos < (int)sizeof(BscpPacket)) //查找数据标志头,当标志头在数据中间时,记录其位置,继续接收数据至指定长度的数据
{
MSG_OUT("bscp mark find it. pos = %d\n", markPos);
memmove((char *)&pParam->bscp, (char *)&pParam->bscp + markPos, (sizeof(BscpPacket) - markPos));
pParam->bscpRecvLen = (sizeof(BscpPacket) - markPos);
continue;
}
else
{
MSG_OUT("no bscp mark.\n");
pParam->bscpRecvLen = 0;
continue;
}
}while(1);
ret = SERVER_OK;
EXIT:
return ret;
}
此代码采用do while 结构,当数据不足固定长度时,一直continue接收,直到接收够固定长度的数据,然后对固定长度的数据进行标志头查找,查找标志头函数原型如下:
static int msgMarkCheck(char *pData, int dataLen)
{
int result = SERVER_ERR;
int i = 0;
MSG_CHECK_NULL(pData, SERVER_ERR, result, goto EXIT);
if(1 > dataLen || STR_LEN_512 < dataLen)
{
MSG_OUT("dataLen err. dataLen = %d\n", dataLen);
result = SERVER_ERR;
goto EXIT;
}
for(i = 0; i < dataLen - 1; i++)
{
if(pData[i] == 'B' && pData[i+1] == 'S')
{
result = i;
goto EXIT;
}
}
/* 当最后一个字符满足查找要求时,仍需记录其位置 */
if(pData[dataLen - 1] == 'B')
{
result = dataLen - 1;
}
EXIT:
return result;
}
此项示例的标志头为‘B’、 ‘S’, 对数据每一位做对比,寻找数据标志头,并记录其位置。
通过网络调试助手,向服务器端发送不同的数据,模拟可能出现的问题故障,看服务器端是否能够正常迅速响应。
eg:
异常1: 因为标志头为’B’、‘S’, 其16进制数据为 42和 53,利用网络调试助手发送其16进制数据,查看服务器端打印是否能够找到标志头。
当数据中没有标志头时,服务器端能够检测出没有数据头
异常2: 标志头不在数据的其实位置,而在数据的中间位置 具体数据及服务器响应如下图:
对于其他的情况,由于网络调试i助手的限制,不太好测试测试出来,大家可以书写client代码进行实际测试。
在实际应用中,开发人员基于tcp通信机制,还有好多要考虑的事情,比如,阻塞、效率、 安全等问题,具体问题需要具体分析,tcp通信原理上只是提供给我们一种通信机制,但是这种通信机制遭遇到复杂的网络环境和繁杂的业务处理的情况下,需要考虑的事情就多了,希望大家在做项目的时候多想一想。