牵着老婆满街逛

严以律己,宽以待人. 三思而后行.
GMail/GTalk: yanglinbo#google.com;
MSN/Email: tx7do#yahoo.com.cn;
QQ: 3 0 3 3 9 6 9 2 0 .

最简单的TCP网络封包解包

如若描述或者代码当中有谬误之处,还望指正。




TCP为什么需要进行封包解包?

        TCP采用字节流的方式,即以字节为单位传输字节序列。那么,我们recv到的就是一串毫无规则的字节流。如果要让这无规则的字节流有规则,那么,就需要我们去定义一个规则。那便是所谓的“封包规则”。

封包结构是怎么样的?
        封包就像是信,信是由:信封、信内容。两部分组成。而网络封包也是由两部分组成:包头、数据。包头域是定长的,数据域是不定长的。包头必然包含两个信息:操作码、包长度。包头可能还包含别的信息,这个呢就要视乎情况去定了。操作码是该网络数据包的标识符,这就和UI里面的事件ID什么的差不多。其中,操作码有的只有一级,有的则有两级甚至多级操作码,这个的设计也要看情况去了,不过,这些底层的东西,定好了,基本就不能动了,就像房子都砌起来了,再去动地基,那就欧也了。
以下是网络数据包的伪代码:
struct NetPacket
{
包头;
数据;
};
以下是包头的伪代码:
struct NetPacketHeader
{
操作码;
包长度;
};

收包中存在的一个问题(粘包,半包)
        在现实的网络情况中,网络传输往往是不可靠的,因此会有丢包之类的情况发生,对此,TCP相应的有一个重传的机制。对于接收者来说,它接收到的数据流中的数据有可能不是完整的数据包,或是只有一部分,或是粘着别的数据包,因此,接收者还需要对接收到的数据流的数据进行分包。

服务器客户端逻辑描述
        服务等待一个客户端的连接,客户端连接上了以后,服务器向客户端发送5个数据包,客户端接收服务器端的数据并解包然后做相应的逻辑处理。

需要注意的事项
1.服务器客户端是阻塞的,而不是非阻塞的套接字,这是为了简单;
2.当客户端收到了5个数据包之后,就主动和服务器断开连接,这个是硬代码;
3.阻塞套接字其实没有必要这样处理数据包,主要应用在非阻塞的套接字上;
4.此段代码只支持POD数据,不支持变长的情况;
5.各平台下字节对齐方式不一样,如Windows下默认字节对齐为4,这是此方式需要注意的。


服务器CPP代码:

#include 
"stdafx.h"
#include 
"TCPServer.h"

TCPServer::TCPServer()
: mServerSocket(INVALID_SOCKET)
{
    
// 创建套接字
    mServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
    
if (mServerSocket == INVALID_SOCKET)
    
{
        std::cout 
<< "创建套接字失败!" << std::endl;
        
return;
    }


    
// 填充服务器的IP和端口号
    mServerAddr.sin_family        = AF_INET;
    mServerAddr.sin_addr.s_addr    
= INADDR_ANY;
    mServerAddr.sin_port        
= htons((u_short)SERVER_PORT);

    
// 绑定IP和端口
    if ( ::bind(mServerSocket, (sockaddr*)&mServerAddr, sizeof(mServerAddr)) == SOCKET_ERROR)
    
{
        std::cout 
<< "绑定IP和端口失败!" << std::endl;
        
return;
    }


    
// 监听客户端请求,最大同时连接数设置为10.
    if ( ::listen(mServerSocket, SOMAXCONN) == SOCKET_ERROR)
    
{
        std::cout 
<< "监听端口失败!" << std::endl;
        
return;
    }


    std::cout 
<< "启动TCP服务器成功!" << std::endl;
}


TCPServer::
~TCPServer()
{
    ::closesocket(mServerSocket);
    std::cout 
<< "关闭TCP服务器成功!" << std::endl;
}


void TCPServer::run()
{
    
// 接收客户端的连接
    acceptClient();

    
int nCount = 0;
    
for (;;)
    
{
        
if (mAcceptSocket == INVALID_SOCKET) 
        
{
            std::cout 
<< "客户端主动断开了连接!" << std::endl;
            
break;
        }


        
// 发送数据包
        NetPacket_Test1 msg;
        msg.nIndex 
= nCount;
        strncpy(msg.arrMessage, 
"[1]你好[2]你好[3]你好"sizeof(msg.arrMessage) );
        
bool bRet = SendData(NET_TEST1, (const char*)&msg, sizeof(msg));
        
if (bRet)
        
{
            std::cout 
<< "发送数据成功!" << std::endl;
        }

        
else
        
{
            std::cout 
<< "发送数据失败!" << std::endl;
            
break;
        }


        
++nCount;
    }

}


void TCPServer::closeClient()
{
    
// 判断套接字是否有效
    if (mAcceptSocket == INVALID_SOCKET) return;

    
// 关闭客户端套接字
    ::closesocket(mAcceptSocket);
    std::cout 
<< "客户端套接字已关闭!" << std::endl;
}


void TCPServer::acceptClient()
{
    
// 以阻塞方式,等待接收客户端连接
    int nAcceptAddrLen = sizeof(mAcceptAddr);
    mAcceptSocket 
= ::accept(mServerSocket, (struct sockaddr*)&mAcceptAddr, &nAcceptAddrLen);
    std::cout 
<< "接受客户端IP:" << inet_ntoa(mAcceptAddr.sin_addr) << std::endl;
}


bool TCPServer::SendData( unsigned short nOpcode, const char* pDataBuffer, const unsigned int& nDataSize )
{
    NetPacketHeader
* pHead = (NetPacketHeader*) m_cbSendBuf;
    pHead
->wOpcode = nOpcode;

    
// 数据封包
    if ( (nDataSize > 0&& (pDataBuffer != 0) )
    
{
        CopyMemory(pHead
+1, pDataBuffer, nDataSize);
    }


    
// 发送消息
    const unsigned short nSendSize = nDataSize + sizeof(NetPacketHeader);
    pHead
->wDataSize = nSendSize;
    
int ret = ::send(mAcceptSocket, m_cbSendBuf, nSendSize, 0);
    
return (ret > 0? true : false;
}



客户端CPP代码:

#include 
"stdafx.h"
#include 
"TCPClient.h"


TCPClient::TCPClient()
{
    memset( m_cbRecvBuf, 
0sizeof(m_cbRecvBuf) );
    m_nRecvSize 
= 0;

    
// 创建套接字
    mServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
    
if (mServerSocket == INVALID_SOCKET)
    
{
        std::cout 
<< "创建套接字失败!" << std::endl;
        
return;
    }


    
// 填充服务器的IP和端口号
    mServerAddr.sin_family        = AF_INET;
    mServerAddr.sin_addr.s_addr    
= inet_addr(SERVER_IP);
    mServerAddr.sin_port        
= htons((u_short)SERVER_PORT);

    
// 连接到服务器
    if ( ::connect(mServerSocket, (struct sockaddr*)&mServerAddr, sizeof(mServerAddr)))
    
{
        ::closesocket(mServerSocket);
        std::cout 
<< "连接服务器失败!" << std::endl;
        
return;    
    }

}


TCPClient::
~TCPClient()
{
    ::closesocket(mServerSocket);
}


void TCPClient::run()
{
    
int nCount = 0;
    
for (;;)
    
{
        
// 接收数据
        int nRecvSize = ::recv(mServerSocket,
            m_cbRecvBuf
+m_nRecvSize, 
            
sizeof(m_cbRecvBuf)-m_nRecvSize, 0);
        
if (nRecvSize <= 0)
        
{
            std::cout 
<< "服务器主动断开连接!" << std::endl;
            
break;
        }


        
// 保存已经接收数据的大小
        m_nRecvSize += nRecvSize;

        
// 接收到的数据够不够一个包头的长度
        while (m_nRecvSize >= sizeof(NetPacketHeader))
        
{
            
// 收够5个包,主动与服务器断开
            if (nCount >= 5)
            
{
                ::closesocket(mServerSocket);
                
break;
            }


            
// 读取包头
            NetPacketHeader* pHead = (NetPacketHeader*) (m_cbRecvBuf);
            
const unsigned short nPacketSize = pHead->wDataSize;

            
// 判断是否已接收到足够一个完整包的数据
            if (m_nRecvSize < nPacketSize)
            
{
                
// 还不够拼凑出一个完整包
                break;
            }


            
// 拷贝到数据缓存
            CopyMemory(m_cbDataBuf, m_cbRecvBuf, nPacketSize);

            
// 从接收缓存移除
            MoveMemory(m_cbRecvBuf, m_cbRecvBuf+nPacketSize, m_nRecvSize);
            m_nRecvSize 
-= nPacketSize;

            
// 解密数据,以下省略一万字
            
// 

            
// 分派数据包,让应用层进行逻辑处理
            pHead = (NetPacketHeader*) (m_cbDataBuf);
            
const unsigned short nDataSize = nPacketSize - (unsigned short)sizeof(NetPacketHeader);
            OnNetMessage(pHead
->wOpcode, m_cbDataBuf+sizeof(NetPacketHeader), nDataSize);

            
++nCount;
        }

    }


    std::cout 
<< "已经和服务器断开连接!" << std::endl;
}


bool TCPClient::OnNetMessage( const unsigned short& nOpcode, 
                             
const char* pDataBuffer, unsigned short nDataSize )
{
    
switch (nOpcode)
    
{
    
case NET_TEST1:
        
{
            NetPacket_Test1
* pMsg = (NetPacket_Test1*) pDataBuffer;
            
return OnNetPacket(pMsg);
        }

        
break;

    
default:
        
{
            std::cout 
<< "收取到未知网络数据包:" << nOpcode << std::endl;
            
return false;
        }

        
break;
    }

}


bool TCPClient::OnNetPacket( NetPacket_Test1* pMsg )
{
    std::cout 
<< "索引:" << pMsg->nIndex << "  字符串:" << pMsg->arrMessage << std::endl;
    
return true;
}



源代码打包下载:
testNetPacket.rar

posted on 2011-05-04 23:59 杨粼波 阅读(17398) 评论(4)  编辑 收藏 引用

评论

# re: 最简单的TCP网络封包解包 2011-05-05 10:02 百思寒

bucuo,收藏了  回复  更多评论   

# re: 最简单的TCP网络封包解包 2011-05-05 13:38 jc_ontheroad

尝试一下。  回复  更多评论   

# re: 最简单的TCP网络封包解包 2011-05-05 14:30 finalday

用sizeof(class)的方式打包是糟糕透顶的主意。
1)每台机软硬件平台不一致:LE BE, 32位机器 or 64位机器, 对齐字节数……,这些不一致都会导致错误的结果
2)即使软硬件平台完全一致,class里面有指针怎么办?有vector怎么办?有虚函数怎么办?这种打包解包方式对class有很多要求。但是很不幸:违反这些规则时编译链接一点问题都没有,因为只用到sizeof和指针转换,运行起来才会把进程崩掉。

Anyway,敢扔代码总是好事,比单纯夸夸其谈好。
封包解包不难,但也没这么容易,去看看Google Protocal Buffer吧。我之前参考他代码实现了一套,后面发现完全没必要,直接用它的就很OK了。  回复  更多评论   

# re: 最简单的TCP网络封包解包[未登录] 2011-05-05 15:40 杨粼波

@finalday

忘记说明了,此种方式只支持POD类型数据,这是我描述的缺失。

字节对齐,这个是可以指定的,问题倒不大,比如windows可以用:
#pragma pack(1)

Google Protocal Buffer使用的是序列化的方式,而且提供了它自己的协议描述语言,对于跨语言的情况下,是非常好用的。

这个文章,这段代码是讲解如何封包解包的。当然,缺点是,无法应付变长的情况。这一切都只是为了“简单”:套接字用的是阻塞的,不考虑非POD数据变长数据的情况。  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理