最简单的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, 0, sizeof(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

时间: 2024-10-27 16:12:48

最简单的TCP网络封包解包的相关文章

最简单的TCP网络封包解包(补充)-序列化

将数据能够在TCP中进行传输的两种方法1.直接拷贝struct就可以了: 2.序列化. 拷贝Struct存在的问题 1.不能应付可变长类型的数据,比如STL中的那些容器,他们的长度都是不确定的.当然,STL的容器归根到底就是一个class: 2.内存对齐的问题,Windows默认的对齐是4字节,如果不去刻意关闭掉对齐的话,那么可能会多出不少没必要的字节数,有时候,这个损耗是客观的.但是如果关闭了,内存拷贝又会慢一些,内存IO相对于网络IO来说,速度是快的,略微的增加内存IO的压力来调优网络IO是

c语言-C语言实现封包解包,有一个消息由标识位,消息头,消息体和校验码组成,如何用C实现对它的封包和解包?

问题描述 C语言实现封包解包,有一个消息由标识位,消息头,消息体和校验码组成,如何用C实现对它的封包和解包? 有一个消息由标识位,消息头,消息体和校验码组成,如何用C实现对它的封包和解包? 解决方案 直接定义成结构体 解决方案二: 定义结构体,然后里面用不同字段定义标识位,消息头,消息体,校验码等 解决方案三: 是呀,如果都是按字节来分的,定位为结构体是一个好方法.

TCP网络编程封包解包问题

问题描述 用socket+TCP协议编程存在读取数据100字节但是,数据不足100字节的问题,转换数据总是有问题,看了一些书籍,都说发送前封包,接收的时候解包为对象,但是没有例子不是很直观,到底在发送前怎么封装,接收时怎么解封,如果我读取100条数据,前50是我要的,后50是下一条的,那么又要怎么处理呢?请各位指教,谢谢,最好有实例 解决方案 解决方案二:你可以首先发送整条数据的长度,当接收到指定长度之后就可以算下一条了解决方案三:c#传送的是byte[],先把这个数组的长度传过去,然后等响应后

解决TCP网络传输“粘包”问题

当前在网络传输应用中,广泛采用的是TCP/IP通信协议及其标准的socket应用开发编程接口(API).TCP/IP传输层有两个并列的协议:TCP和UDP.其中TCP(transport control protocol,传输控制协议)是面向连接的,提供高可靠性服务.UDP(user datagram protocol,用户数据报协议)是无连接的,提供高效率服务.在实际工程应用中,对可靠性和效率的选择取决于应用的环境和需求.一般情况下,普通数据的网络传输采用高效率的udp,重要数据的网络传输采用

处理TCP网络传输“粘包”疑难

在应用开发过程中,笔者发现基于TCP网络传输的应用程序有时会出现粘包现象(即发送方发送的若干包数据到接收方接收时粘成一包).针对这种情况,我们进行了专题研究与实验.本文重点分析了TCP网络粘包问题,并结合实验结果提出了解决该问题的对策和方法,供有关工程技术人员参考. 一.TCP协议简介 TCP是一个面向连接的传输层协议,虽然TCP不属于iso制定的协议集,但由于其在商业界和工业界的成功应用,它已成为事实上的网络标准,广泛应用于各种网络主机间的通信. 作为一个面向连接的传输层协议,TCP的目标是为

解决TCP网络传输“粘包”问题,互联网营销

当前在网络传输应用中,广泛采用的是TCP/IP通信协议及其标准的socket应用开发编程接口(API).TCP/IP传输层有两个并列的协议:TCP和UDP.其中TCP(transport control protocol,传输控制协议)是面向连接的,提供高可靠性服务.UDP(user datagram protocol,用户数据报协议)是无连接的,提供高效率服务.在实际工程应用中,对可靠性和效率的选择取决于应用的环境和需求.一般情况下,普通数据的网络传输采用高效率的udp,重要数据的网络传输采用

JAVA包装类及自动封包解包示例代码

在学习上是一个知识点,但不知如何与实际串起来... 悲哀,真是悲哀!!! 代码: 1 public class Wrapper { 2 public static void main(String[] args) { 3 int i = 500; 4 Integer t = new Integer(i); 5 int j = t.intValue(); 6 String s = t.toString(); 7 System.out.println(t); 8 Integer t1 = new I

JAVA包装类及自动封包解包实例代码_java

复制代码 代码如下: public class Wrapper {     public static void main(String[] args) {         int i = 500;         Integer t = new Integer(i);         int j = t.intValue();         String s = t.toString();         System.out.println(t);         Integer t1 =

Linux下套接字详解(四)----简单的TCP套接字应用(迭代型)

前面我们已经将了TCP/UDP的基本知识,还说了并发服务器与迭代服务器的区别,我们大致了解大多数TCP服务器是并发的,大多数UDP服务器是迭代的 ,即我们在进行数据传送的时候,往往使用服务器与客户但之间无连接的UDP报文,但是在用户需要上传下载文件时,就会在客户端和服务器之间建立一条TCP连接,进行文件的传送 那么我们下面就来实现一个简单的TCP服务器. TCP套接字编程模型图 我们首先看一下TCP客户端与服务端的编程模型和流程. 此模型不仅适合迭代服务器,也适合并发服务器,不管服务器是并发的还