最简单的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
目录
相关文章
|
7月前
|
域名解析 网络协议 安全
计算机网络TCP/IP四层模型
本文介绍了TCP/IP模型的四层结构及其与OSI模型的对比。网络接口层负责物理网络接口,处理MAC地址和帧传输;网络层管理IP地址和路由选择,确保数据包准确送达;传输层提供端到端通信,支持可靠(TCP)或不可靠(UDP)传输;应用层直接面向用户,提供如HTTP、FTP等服务。此外,还详细描述了数据封装与解封装过程,以及两模型在层次划分上的差异。
1387 13
|
5月前
|
弹性计算 运维 Kubernetes
看阿里云操作系统控制台如何一招擒拿网络丢包
阿里云操作系统控制台帮忙客户快速定位问题,不仅成功完成业务部署并实现稳定运行,更有效遏制了持续性成本消耗。
|
9月前
|
网络协议 物联网
VB6网络通信软件上位机开发,TCP网络通信,读写数据并处理,完整源码下载
本文介绍使用VB6开发网络通信上位机客户端程序,涵盖Winsock控件的引入与使用,包括连接服务端、发送数据(如通过`Winsock1.SendData`方法)及接收数据(利用`Winsock1_DataArrival`事件)。代码实现TCP网络通信,可读写并处理16进制数据,适用于自动化和工业控制领域。提供完整源码下载,适合学习VB6网络程序开发。 下载链接:[完整源码](http://xzios.cn:86/WJGL/DownLoadDetial?Id=20)
352 12
|
9月前
|
Kubernetes Shell Windows
【Azure K8S | AKS】在AKS的节点中抓取目标POD的网络包方法分享
在AKS中遇到复杂网络问题时,可通过以下步骤进入特定POD抓取网络包进行分析:1. 使用`kubectl get pods`确认Pod所在Node;2. 通过`kubectl node-shell`登录Node;3. 使用`crictl ps`找到Pod的Container ID;4. 获取PID并使用`nsenter`进入Pod的网络空间;5. 在`/var/tmp`目录下使用`tcpdump`抓包。完成后按Ctrl+C停止抓包。
347 12
|
负载均衡 网络协议 算法
不为人知的网络编程(十九):能Ping通,TCP就一定能连接和通信吗?
这网络层就像搭积木一样,上层协议都是基于下层协议搭出来的。不管是ping(用了ICMP协议)还是tcp本质上都是基于网络层IP协议的数据包,而到了物理层,都是二进制01串,都走网卡发出去了。 如果网络环境没发生变化,目的地又一样,那按道理说他们走的网络路径应该是一样的,什么情况下会不同呢? 我们就从路由这个话题聊起吧。
321 4
不为人知的网络编程(十九):能Ping通,TCP就一定能连接和通信吗?
|
10月前
|
网络协议 测试技术 Linux
Golang 实现轻量、快速的基于 Reactor 模式的非阻塞 TCP 网络库
gev 是一个基于 epoll 和 kqueue 实现的高性能事件循环库,适用于 Linux 和 macOS(Windows 暂不支持)。它支持多核多线程、动态扩容的 Ring Buffer 读写缓冲区、异步读写和 SO_REUSEPORT 端口重用。gev 使用少量 goroutine,监听连接并处理读写事件。性能测试显示其在不同配置下表现优异。安装命令:`go get -u github.com/Allenxuxu/gev`。
246 0
|
12月前
|
前端开发 网络协议 安全
【网络原理】——HTTP协议、fiddler抓包
HTTP超文本传输,HTML,fiddler抓包,URL,urlencode,HTTP首行方法,GET方法,POST方法
|
12月前
|
网络协议
TCP报文格式全解析:网络小白变高手的必读指南
本文深入解析TCP报文格式,涵盖源端口、目的端口、序号、确认序号、首部长度、标志字段、窗口大小、检验和、紧急指针及选项字段。每个字段的作用和意义详尽说明,帮助理解TCP协议如何确保可靠的数据传输,是互联网通信的基石。通过学习这些内容,读者可以更好地掌握TCP的工作原理及其在网络中的应用。
|
Web App开发 网络协议 安全
网络编程懒人入门(十六):手把手教你使用网络编程抓包神器Wireshark
Wireshark是一款开源和跨平台的抓包工具。它通过调用操作系统底层的API,直接捕获网卡上的数据包,因此捕获的数据包详细、功能强大。但Wireshark本身稍显复杂,本文将以用抓包实例,手把手带你一步步用好Wireshark,并真正理解抓到的数据包的各项含义。
4070 2
|
监控 网络协议 网络性能优化
网络通信的核心选择:TCP与UDP协议深度解析
在网络通信领域,TCP(传输控制协议)和UDP(用户数据报协议)是两种基础且截然不同的传输层协议。它们各自的特点和适用场景对于网络工程师和开发者来说至关重要。本文将深入探讨TCP和UDP的核心区别,并分析它们在实际应用中的选择依据。
406 3

热门文章

最新文章