用TCP协议传送文件可防止粘包

有时候TCP传输会出现粘包情况,网上流行几种说法,一种是在发送的时候加个sleep,让发送不至于过快引起粘包,但这种会影响发送效率,这个sleep并且微秒级的,也就是说sleep(1)与sleep(40)差别不大,废话不多说,下面这段代码经测试可预防粘包。


//以下是测试结构
typedef struct tagRequest
{
    UINT nMagic;
    UINT ncbSize;
    //
    // 这中间可以写点别的
    //
    //
    //数据长度
    UINT nContentLength;

}REQUEST,*PREQUEST,*LPREQUEST;

#define     REQUEST_MAGIC        0x12345678
#define  REQUEST_SIZE            (sizeof(tagRequest))

typedef struct tagResponse
{
    UINT nMagic;
    UINT ncbSize;
    //
    // 这中间可以写点别的
    //
    //
    //数据长度
    UINT nContentLength;
}RESPONSE,*PRESPONSE,*LPRESPONSE;

#define RESPONSE_MAGIC        0x87654321
#define RESPONSE_SIZE            (sizeof(tagResponse))

void PopErrorMessage(DWORD dwErrorCode)
{
    LPVOID lpMsgBuf = NULL;
    if( ::FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | //这个是要自动分配一块内存,使用完要通过LocalFree来回收
        FORMAT_MESSAGE_FROM_SYSTEM |
        FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        dwErrorCode,//这里是要翻译的代码
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // 默认提示语言(也可以随便指定一种)
        (LPTSTR) &lpMsgBuf,
        0,
        NULL
        ) >0 )
    {
        // 弹一个对话框出来.
        ::MessageBox( NULL, (LPCTSTR)lpMsgBuf, "Error", MB_OK | MB_ICONINFORMATION );
        // 这里释放掉返回的内存......................
        ::LocalFree( lpMsgBuf );
    }

}

//发送数据
int SendHunk(SOCKET h,char *lpBuf,int nBufLen)
{
    int nSend = -1 ;
    int nSendAll = 0;

    if( h != SOCKET_ERROR && lpBuf != NULL && nBufLen > 0  )
    {
        //循环发送,直到所有数据都发送完毕或出错就返回
        do {

            nSend = ::send(h,                //已经连接的句柄
                            lpBuf+nSendAll,   //跳过已经发送的数据
                            nBufLen-nSendAll, //计算剩余要发送的数据长度
                            0);

            if( nSend > 0 )
                nSendAll +=nSend;//累加已经成功发送的数据长度
            else
                break;

        } while( nSendAll < nBufLen );     }          return nSendAll; }   //接收数据 int  RecvHunk(SOCKET h,char *lpBuf,int nBufLen,int nWillLen) {     int nRecv = -1 ;     int nRecvAll = 0;       if( h != SOCKET_ERROR && lpBuf != NULL && nBufLen > 0  && nWillLen >0 && nBufLen >= nWillLen  )
    {
        //循环接收,直到数据达到要接收的长度或出错就返回
        do {

            nRecv = ::recv(h, //已经连接的句柄
                            lpBuf+nRecvAll,//跳过已经接收的数据
                            nWillLen-nRecvAll,//计算剩余要接收的数据长度
                            0);
            if( nRecv >0 )
                nRecvAll = nRecv;//当前已经接收的长度
            else
                break;

        } while( nRecvAll < nWillLen );
    }

    return nRecvAll;
}

void Client()
{
        //发送一个变长的数据块

    //
    //创建一个句柄(注:默认的情况下会返回一个阻塞句柄.)
    SOCKET hSocket = ::socket(AF_INET,        // IPV4
                              SOCK_STREAM,    // 数据流 (像河水一样连绵不断,当然也有断流的时候.^-^)
                              IPPROTO_TCP    // TCP协议(一部分书上说填0,让协议自己选择)
                              );

    if( hSocket == SOCKET_ERROR )
    {
        //哇,出错了,//看看是什么错.
        DWORD dwErrorCode = ::WSAGetLastError();
        //怎么是数字啊!!看不懂,翻译一下.^-^
        PopErrorMessage(dwErrorCode);
        //没办法继续干活了,退出吧!
        return ;
    }

    //定义一个结构用来保存要连接的远程地址和端口
    sockaddr_in stRemote = {0};

    stRemote.sin_family=AF_INET; //IPV4
    stRemote.sin_addr.s_addr = inet_addr("127.0.0.1"); //远程地址(这里用的本机回路地址)
    stRemote.sin_port=htons(12345);//一个端口(这里是随便写的)
    //连接远程主机
    int nReConn = ::connect(hSocket,//前面创建的句柄
                            (SOCKADDR*)&stRemote, //上面的结构(已经填好远程主机的地址和端口)
                            sizeof(stRemote)      //结构体长度
                            );

    if( nReConn == SOCKET_ERROR )
    {
        //哇,出错了,//看看是什么错.
        DWORD dwErrorCode = ::WSAGetLastError();
        //怎么是数字啊!!看不懂,翻译一下.^-^
        PopErrorMessage(dwErrorCode);
        //关闭句柄,并初化为一个错误值
        ::closesocket(hSocket);
        hSocket = SOCKET_ERROR;
        //没办法继续干活了,退出吧!
        return ;
    }

    char * szText[] = {"小鸟,小鸟,我是鸟巢,收到请回答!",
                      "老鸟,老鸟,我是小鸟,收到请回答!",
                      "呜呜,为什么没有回答呀?",
                      "下雪了,该收衣服了!",
                      "天冷了."};

    //随机选一个字符串发过去
    char * szRequestText = szText[rand()%(sizeof(szRequestText)/sizeof(szRequestText[0]))];

    tagRequest stReq = {0};

    stReq.nMagic = REQUEST_MAGIC; //结构体识别编码(随便什么都行,不一定要用数值)
    stReq.ncbSize = REQUEST_SIZE; //填上结构体大小,以识别目前能不能处理.
                                  //有可能结构体大小发生变化,这种情通常出现在产品的不同版本中,本例不考虑这种情况.
    stReq.nContentLength = ::lstrlen(szRequestText)+1;// 要发送变长数据体长度 (这里的加1,是要把末尾的结束符"\0"也发送过去)

    //这里用的是封装后的send 具体到上面看吧
    //发送请求头
    //返回是已经发送的数据长度
    int nSend = ::SendHunk(hSocket, //前面创建的句柄
                            (char*)&stReq, // 请求结构体指针
                            REQUEST_SIZE  // 请示结构体长度
                            );

    if( nSend != REQUEST_SIZE )
    {
        //如果返回的长度不等于结构体长度,表示出错了.

        //哇,出错了,//看看是什么错.
        DWORD dwErrorCode = ::WSAGetLastError();
        //怎么是数字啊!!看不懂,翻译一下.^-^
        PopErrorMessage(dwErrorCode);
        //关闭句柄,并初化为一个错误值
        ::closesocket(hSocket);
        hSocket = SOCKET_ERROR;
        //没办法继续干活了,退出吧!
        return ;
    }

    //发送完结体后,接着发送变长体数据
    nSend = ::SendHunk(hSocket, //前面创建的句柄
                        szRequestText, // 变长数据
                        stReq.nContentLength  // 变长数据长度
                        );

    if( nSend != stReq.nContentLength )
    {
        //如果返回的长度不等于变长数据长度,表示出错了.

        //哇,出错了,//看看是什么错.
        DWORD dwErrorCode = ::WSAGetLastError();
        //怎么是数字啊!!看不懂,翻译一下.^-^
        PopErrorMessage(dwErrorCode);
        //关闭句柄,并初化为一个错误值
        ::closesocket(hSocket);
        hSocket = SOCKET_ERROR;

        //没办法继续干活了,退出吧!
        return ;
    }

    //关闭句柄,并初化为一个错误值
    ::closesocket(hSocket);
    hSocket = SOCKET_ERROR;
}

void OnAccept(SOCKET hClient)
{

    tagRequest stReq = {0};

    //接收数据
    //先接受请求头
    int nRecv = ::RecvHunk(hClient, //上面接收连接句柄
        (char*)&stReq,         //接收缓冲区
        REQUEST_SIZE, //可用的缓冲区大小
        REQUEST_SIZE   //将要接收的数据(这里填的请求头大小,也就是小于这个的将不与处理)
        );

    if( nRecv != REQUEST_SIZE )
    {
        //哇,出错了,//看看是什么错.
        DWORD dwErrorCode = ::WSAGetLastError();
        //怎么是数字啊!!看不懂,翻译一下.^-^
        PopErrorMessage(dwErrorCode);
        //关闭连接句柄
        ::closesocket(hClient);
        hClient = SOCKET_ERROR;

        //没办法继续干活了,退出吧!
        return ;

    }

    //验证结构是否合法
    if( stReq.nMagic != REQUEST_MAGIC || stReq.ncbSize != REQUEST_SIZE)
    {
        //哇,非法数据,警告!!这不是我们自己人!!!

        //关闭连接句柄
        ::closesocket(hClient);
        hClient = SOCKET_ERROR;

        //没办法继续干活了,退出吧!
        return ;
    }

    //在这里可以验证一下其它请求头参数(注:本例不验证).

    char * pszRequestText = new char[stReq.nContentLength];

    if( pszRequestText )
    {
        //再接收变长数据
        int nRecv = ::RecvHunk(hClient, //上面接收连接句柄
            pszRequestText,    //接收缓冲区
            stReq.nContentLength, //可用的缓冲区大小
            stReq.nContentLength   //将要接收的数据大小
            );

        if( nRecv != stReq.nContentLength)
        {
            //唉!数据呢?????

            //关闭连接句柄
            ::closesocket(hClient);
            hClient = SOCKET_ERROR;

            //回收上面申请的内存
            delete []pszRequestText;
            pszRequestText = NULL;

            //没办法继续干活了,退出吧!
            return ;
        }

        //
        ::MessageBox(NULL,pszRequestText,"瞧瞧发来是什么",MB_OK | MB_ICONINFORMATION);

        //回收上面申请的内存
        delete []pszRequestText;
        pszRequestText = NULL;

    }

    //关闭连接句柄
    ::closesocket(hClient);
    hClient = SOCKET_ERROR;
}

DWORD WINAPI ThreadOnAccept(LPVOID lpData)
{
    OnAccept((SOCKET)lpData);

    return 0;
}

void Server()
{
    //接收一个变长的数据块

    //
    //创建一个句柄(注:默认的情况下会返回一个阻塞句柄.)
    SOCKET hSocket = ::socket(AF_INET,        // IPV4
        SOCK_STREAM,    // 数据流 (像河水一样连绵不断,当然也有断流的时候.^-^)
        IPPROTO_TCP    // TCP协议(一部分书上说填0,让协议自己选择)
        );

    if( hSocket == SOCKET_ERROR )
    {
        //哇,出错了,//看看是什么错.
        DWORD dwErrorCode = ::WSAGetLastError();
        //怎么是数字啊!!看不懂,翻译一下.^-^
        PopErrorMessage(dwErrorCode);
        //没办法继续干活了,退出吧!
        return ;
    }

    //定义一个结构用来保存本机地址和端口
    sockaddr_in stLocal = {0};

    stLocal.sin_family=AF_INET; //IPV4
    stLocal.sin_addr.s_addr = inet_addr("127.0.0.1"); //本机要使用IP地址(这里用的本机回路地址)
    stLocal.sin_port=htons(12345);//一个端口(这里是随便写的)

    int nReBind = ::bind(hSocket,//前面创建的句柄
        (SOCKADDR*)&stLocal, //上面的结构(已经填好远程主机的地址和端口)
        sizeof(stLocal)      //结构体长度
        );

    if( nReBind == SOCKET_ERROR )
    {
        //哇,出错了,//看看是什么错.
        DWORD dwErrorCode = ::WSAGetLastError();
        //怎么是数字啊!!看不懂,翻译一下.^-^
        PopErrorMessage(dwErrorCode);
        //关闭句柄,并初化为一个错误值
        ::closesocket(hSocket);
        hSocket = SOCKET_ERROR;
        //没办法继续干活了,退出吧!
        return ;
    }

    //设置监听数量
    int nRelisten = ::listen(hSocket,//前面创建的句柄
                            SOMAXCONN //同时连接数,(注:这里的连接数,指的是同时可以接受的请求连接数.不是指可以连接到这个端口的数量)
                            );

    if( nRelisten == SOCKET_ERROR )
    {
        //哇,出错了,//看看是什么错.
        DWORD dwErrorCode = ::WSAGetLastError();
        //怎么是数字啊!!看不懂,翻译一下.^-^
        PopErrorMessage(dwErrorCode);
        //关闭句柄,并初化为一个错误值
        ::closesocket(hSocket);
        hSocket = SOCKET_ERROR;
        //没办法继续干活了,退出吧!
        return ;
    }

    do {
        //定义一个结构用来保存要连接的远程地址和端口
        sockaddr_in stRemote = {0};
        int nRemoteLen = sizeof(stRemote);
        //等待远程连接到的到来.(从理论上讲,只要资源足够,可以接受足够多个连接).
        //返回一个新的连接句柄
        SOCKET hSocketClient = ::accept(hSocket,//前面创建的句柄
            (SOCKADDR*)&stRemote, //保存已经连接远程主机地址
            &nRemoteLen            //保存已经连接远程主机地址长度(注:这个值在传入时表示可用的缓冲区长度,传出时表示已经保存的地址长度)
            );

        if( hSocketClient == SOCKET_ERROR )
        {
            //哇,出错了,//看看是什么错.
            DWORD dwErrorCode = ::WSAGetLastError();
            //怎么是数字啊!!看不懂,翻译一下.^-^
            PopErrorMessage(dwErrorCode);
            //关闭句柄,并初化为一个错误值
            ::closesocket(hSocket);
            hSocket = SOCKET_ERROR;
            //没办法继续干活了,退出吧!
            return ;
        }

        HANDLE hThClient = ::CreateThread(NULL,0,ThreadOnAccept,
            (LPVOID)hSocketClient,//这个句柄会在线程内关闭
            0,NULL);

        //关闭线句柄(注:线程不会退出的!!)
        ::CloseHandle(hThClient);
        hThClient = NULL;

    } while(TRUE);

    return;
}

DWORD WINAPI ThreadServer(LPVOID lpData)
{
    Server();

    return 0;
}

Comments are closed.