Windows下基于TCP协议的大文件传输(流形式)

简单实现TCP下的大文件高效传输

在TCP下进行大文件传输,不像小文件那样直接打包个BUFFER发送出去,因为文件比较大可能是1G,2G或更大,第一效率问题,第二TCP粘包问题。针对服务端的设计来说就更需要严紧些。下面介绍简单地实现大文件在TCP的传输应用。

粘包出现原因:在流传输中出现,UDP不会出现粘包,因为它有消息边界(参考Windows 网络编程)

1 发送端需要等缓冲区满才发送出去,造成粘包

2 接收方不及时接收缓冲区的包,造成多个包接收

解决办法:

为了避免粘包现象,可采取以下几种措施:

一是对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;

二是对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象;

三是由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。

对于基于TCP开发的通讯程序,有个很重要的问题需要解决,就是封包和拆包.

为什么基于TCP的通讯程序需要进行封包和拆包?

TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,是连成一片的,其间是没有分界线的.但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包.由于TCP"流"的特性以及网络状况,在进行数据传输时会出现以下几种情况.

假设我们连续调用两次send分别发送两段数据data1和data2,在接收端有以下几种接收情况(当然不止这几种情况,这里只列出了有代表性的情况).

A.先接收到data1,然后接收到data2.

B.先接收到data1的部分数据,然后接收到data1余下的部分以及data2的全部.

C.先接收到了data1的全部数据和data2的部分数据,然后接收到了data2的余下的数据.

D.一次性接收到了data1和data2的全部数据.

对于A这种情况正是我们需要的,不再做讨论.对于B,C,D的情况就是大家经常说的"粘包",就需要我们把接收到的数据进行拆包,拆成一个个独立的数据包.为了拆包就必须在发送端进行封包.

另:对于UDP来说就不存在拆包的问题,因为UDP是个"数据包"协议,也就是两段数据间是有界限的,在接收端要么接收不到数据要么就是接收一个完整的一段数据,不会少接收也不会多接收.

二.为什么会出现B.C.D的情况.

"粘包"可发生在发送端也可发生在接收端.

1.由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法.简单的说,当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间,看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去.这是对Nagle算法一个简单的解释,详细的请看相关书籍.象C和D的情况就有可能是Nagle算法造成的.

2.接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据.当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据.

三.怎样封包和拆包.

最初遇到"粘包"的问题时,我是通过在两次send之间调用sleep来休眠一小段时间来解决.这个解决方法的缺点是显而易见的,使传输效率大大降低,而且也并不可靠.后来就是通过应答的方式来解决,尽管在大多数时候是可行的,但是不能解决象B的那种情况,而且采用应答方式增加了通讯量,加重了网络负荷. 再后来就是对数据包进行封包和拆包的操作.

封包:

封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(以后讲过滤非法包时封包会加入"包尾"内容).包头其实上是个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其他的结构体成员可根据需要自己定义.根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包.

对于拆包目前我最常用的是以下两种方式.

1.动态缓冲区暂存方式.之所以说缓冲区是动态的是因为当需要缓冲的数据长度超出缓冲区的长度时会增大缓冲区长度.

大概过程描述如下:

A,为每一个连接动态分配一个缓冲区,同时把此缓冲区和SOCKET关联,常用的是通过结构体关联.

B,当接收到数据时首先把此段数据存放在缓冲区中.

C,判断缓存区中的数据长度是否够一个包头的长度,如不够,则不进行拆包操作.

D,根据包头数据解析出里面代表包体长度的变量.

E,判断缓存区中除包头外的数据长度是否够一个包体的长度,如不够,则不进行拆包操作.

F,取出整个数据包.这里的"取"的意思是不光从缓冲区中拷贝出数据包,而且要把此数据包从缓存区中删除掉.删除的办法就是把此包后面的数据移动到缓冲区的起始地址.

这种方法有两个缺点.1.为每个连接动态分配一个缓冲区增大了内存的使用.2.有三个地方需要拷贝数据,一个地方是把数据存放在缓冲区,一个地方是把完整的数据包从缓冲区取出来,一个地方是把数据包从缓冲区中删除.第二种拆包的方法会解决和完善这些缺点.

前面提到过这种方法的缺点.下面给出一个改进办法, 即采用环形缓冲.但是这种改进方法还是不能解决第一个缺点以及第一个数据拷贝,只能解决第三个地方的数据拷贝(这个地方是拷贝数据最多的地方).第2种拆包方式会解决这两个问题.

环形缓冲实现方案是定义两个指针,分别指向有效数据的头和尾.在存放数据和删除数据时只是进行头尾指针的移动.

2.利用底层的缓冲区来进行拆包

由于TCP也维护了一个缓冲区,所以我们完全可以利用TCP的缓冲区来缓存我们的数据,这样一来就不需要为每一个连接分配一个缓冲区了.另一方面我们知道recv或者wsarecv都有一个参数,用来表示我们要接收多长长度的数据.利用这两个条件我们就可以对第一种方法进行优化.

对于阻塞SOCKET来说,我们可以利用一个循环来接收包头长度的数据,然后解析出代表包体长度的那个变量,再用一个循环来接收包体长度的数据.

tcp是流,没有界限.也就无所谓包;tcp是协议,而socket是一种接口。本文以流的形式发单个大文件,也就无所谓封包和拆包问题,见下面代码;但是要连续发送多个大文件,封包和拆包就是要考虑的问题了!

#ifndef TCPRECVFILE
#define TCPRECVFILE

#include <stdio.h>
#include <winsock2.h>
#include <iostream>
#include <time.h>
#define DESTADDRESS "192.168.27.170"
#define FILENAME "D:\\file.jpg"
#define SERVER_PORT 5210 //侦听端口 

#define BEGIN_NUM 19900711
#define DATA_NUM  20160113
#define END_NUM   11700991
#define BLOCK_DATA_SIZE (10 * 1024)
#define FILE_HEAD  4
#define BLOCK_HEAD 4

class CTCPRecvfile
{
public:

    CTCPRecvfile();

    ~CTCPRecvfile();

    void Recvfile();

    void Recv();

private:

    void InitSocket();

    void CloseSocket();

    SOCKET m_Listen, m_Server; //侦听套接字,连接套接字 

    struct sockaddr_in ServerAddr, ClientAddr; //地址信息
};

#endif

1

1

#include "TCPRecvfile.h"

CTCPRecvfile::CTCPRecvfile()
{
    InitSocket();
}
CTCPRecvfile::~CTCPRecvfile()
{
    CloseSocket();
}
void CTCPRecvfile::InitSocket()
{
    WORD wVersionRequested = MAKEWORD(2, 2); //希望使用的WinSock DLL 的版本
    WSADATA wsaData;
    //WinSock初始化
    int ret = WSAStartup(wVersionRequested, &wsaData);
    if (ret != 0)
    {
        printf("WSAStartup() failed!\n");
        //return 0;
    }
    //创建Socket,使用TCP协议
    m_Listen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (m_Listen == INVALID_SOCKET)
    {
        printf("socket() faild!\n");
        //return 0;
    }
    //构建本地地址信息
    ServerAddr.sin_family = AF_INET; //地址家族
    ServerAddr.sin_port = htons(SERVER_PORT); //注意转化为网络字节序
    ServerAddr.sin_addr.S_un.S_addr = INADDR_ANY; //使用INADDR_ANY 指示任意地址  

    //绑定
    ret = bind(m_Listen, (struct sockaddr *)&ServerAddr, sizeof(ServerAddr));
    if (ret == SOCKET_ERROR)
    {
        printf("bind() faild! code:%d\n", WSAGetLastError());
        //return 0;
    }
    //侦听连接请求
    ret = listen(m_Listen, 1);
    if (ret == SOCKET_ERROR)
    {
        printf("listen() faild! code:%d\n", WSAGetLastError());
    }
    int length = sizeof(ServerAddr);
    m_Server = accept(m_Listen, (struct sockaddr *)&ServerAddr, &length);
    if (m_Server == INVALID_SOCKET)
    {
        printf("accept() faild! code:%d\n", WSAGetLastError());
        return;
    }
    else
        printf("Server is Connected!\n");
}

void CTCPRecvfile::Recv()
{
    char *eachBuf = new char[BLOCK_DATA_SIZE + 2 * FILE_HEAD];
    memset(eachBuf, 0, BLOCK_DATA_SIZE + 2 * FILE_HEAD);
    FILE *fp;
    UINT dwFileSize = 0;
    unsigned int RecvNum = 0, flag_status = 0, flag_recv = 1;
    fp = fopen(FILENAME, "wb");
    //1、读取第一组数据,获取文件大小,建立连接

    recv(m_Server, eachBuf, 2 * FILE_HEAD, 0);//////----------------recv
    char charFileSize[4] = { 0 };
    memcpy(charFileSize, eachBuf, FILE_HEAD); //拷贝前4个字节
    for (int i = 0; i < 4; i++)
    {
        flag_status += ((UCHAR)charFileSize[i]) << (8 * (4 - i - 1)); //获取文件起始符
    }
    memcpy(charFileSize, eachBuf + FILE_HEAD, FILE_HEAD); //拷贝第5-8个字节
    for (int i = 0; i < 4; i++)
    {
        dwFileSize += ((UCHAR)charFileSize[i]) << (8 * (4 - i - 1)); //获取文件大小
    }
    int start = clock();
    {
        //开辟接收内存
        int DataPos = 0;
        char *FileBuffer = new char[dwFileSize];
        memset(FileBuffer, 0, dwFileSize);
        while (1)
        {
            int ret = recv(m_Server, eachBuf, BLOCK_DATA_SIZE, 0);
            if (ret <= 0)
                break;
            memcpy(FileBuffer + DataPos, eachBuf, ret);
            DataPos = DataPos + ret;
        }
        fwrite(FileBuffer, dwFileSize, 1, fp);
        fclose(fp);
    }
    int end = clock();
    std::cout << "time:" << end - start << "ms" << RecvNum << std::endl;
}

void CTCPRecvfile::CloseSocket()
{
    closesocket(m_Server); //关闭套接字
    closesocket(m_Listen);
    WSACleanup();
}

1

1

#ifndef TCPSENDFILE
#define TCPSENDFILE

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#include <iostream>
#include <tchar.h>
#include <time.h>

#define SERVER_PORT 5210 //侦听端口
#define DESTADDRESS "192.168.27.170"
#define FILENAME "D:\\10M.jpg"

#define BEGIN_NUM 19900711
#define DATA_NUM  20160113
#define END_NUM   11700991
#define BLOCK_DATA_SIZE (10 * 1024)
#define FILE_HEAD  4
#define BLOCK_HEAD 4 

class CTCPSendfile
{
public:

    CTCPSendfile();

    ~CTCPSendfile();

    void Sendfile();

    void Send();

private:

    void InitSocket();

    void CloseSocket();

    SOCKET m_Client; //连接套接字
    struct sockaddr_in m_ClientAddr; //服务器地址信息
};

#endif

1

1

#include "TCPSendfile.h"

CTCPSendfile::CTCPSendfile()
{
    InitSocket();
}
CTCPSendfile::~CTCPSendfile()
{
    CloseSocket();
}
void CTCPSendfile::InitSocket()
{
    WORD wVersionRequested = MAKEWORD(2, 2); //希望使用的WinSock DLL的版本
    WSADATA wsaData;
    int ret = WSAStartup(wVersionRequested, &wsaData);  //加载套接字库
    if (ret != 0)
    {
        printf("WSAStartup() failed!\n");
        //return 0;
    }
    //确认WinSock DLL支持版本2.2
    if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
    {
        //释放为该程序分配的资源,终止对winsock动态库的使用
        printf("Invalid WinSock version!\n");
        //return 0;
    }

    //WinSock初始化
    //创建Socket,使用TCP协议
    m_Client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (m_Client == INVALID_SOCKET)
    {
        printf("socket() failed!\n");
        //return 0;
    }

    //构建服务器地址信息
    m_ClientAddr.sin_family = AF_INET; //地址家族
    m_ClientAddr.sin_port = htons(SERVER_PORT); //注意转化为网络节序
    m_ClientAddr.sin_addr.S_un.S_addr = inet_addr(DESTADDRESS);

    //连接服务器
    do
    {
        ret = connect(m_Client, (struct sockaddr *)&m_ClientAddr, sizeof(m_ClientAddr));
        if (ret == SOCKET_ERROR)
        {
            printf("connect() failed! Try it again!\n");
        }
        else
            printf("Client is Connected\n");
        Sleep(1000);
    } while (ret == SOCKET_ERROR);
}
void CTCPSendfile::Sendfile()
{
    HANDLE  hFile;
    DWORD   dwHighSize, dwBytesRead;
    DWORD  dwFileSize;
    hFile = CreateFile(_T(FILENAME), GENERIC_READ, FILE_SHARE_READ, NULL,
        OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL);
    dwFileSize = GetFileSize(hFile, &dwHighSize);
    std::cout << "dwFileSize=" << dwFileSize << std::endl;
    //2、读文件内容到 BYTE * fileData 中
    BOOL bSuccess;
    char *fileData = new char[dwFileSize];
    bSuccess = ReadFile(hFile, fileData, dwFileSize, &dwBytesRead, NULL);
    CloseHandle(hFile);
    //3、判断文件是否成功读取
    if (!bSuccess || (dwBytesRead != dwFileSize))
    {
        std::cout << "读取失败" << std::endl;;
        free(fileData);
        return;
    }

   //发送数据帧
    DWORD  retval = 0;
    UINT   DataPos = 0;
    char   *eachBuf = new char[BLOCK_DATA_SIZE + 2 * FILE_HEAD];
    memset(eachBuf, 0, BLOCK_DATA_SIZE + 2 * FILE_HEAD);
    eachBuf[DataPos++] = BEGIN_NUM >> 24 & 0xff;//文件起始标识符
    eachBuf[DataPos++] = BEGIN_NUM >> 16 & 0xff;
    eachBuf[DataPos++] = BEGIN_NUM >> 8 & 0xff;
    eachBuf[DataPos++] = BEGIN_NUM & 0xff;
    eachBuf[DataPos++] = dwFileSize >> 24 & 0xff;
    eachBuf[DataPos++] = dwFileSize >> 16 & 0xff;
    eachBuf[DataPos++] = dwFileSize >> 8 & 0xff;
    eachBuf[DataPos++] = dwFileSize & 0xff;
    retval = send(m_Client, eachBuf, 2 * FILE_HEAD, 0);
    int start = clock();
    {
        retval = send(m_Client, fileData, dwFileSize, 0);
        if (retval == -1)
            std::cout << "send error!";
        int end = clock();
    }
}

void CTCPSendfile::CloseSocket()
{
    closesocket(m_Client); //关闭套接字
    WSACleanup();
}
时间: 06-20

Windows下基于TCP协议的大文件传输(流形式)的相关文章

用scala实现一个基于TCP Socket的快速文件传输程序

这是用scala实现的一个简单的文件传输程序. 服务端 package jpush import java.io.{DataInputStream, File, FileOutputStream} import java.net.ServerSocket import scala.collection.JavaConversions._ /** * Created by dingb on 2016/6/3. */ object Server extends App { def port = 88

MySQL5.5在Windows下的安装(解压为文件夹的形式)

MySQL 5.5.10 的msi安装总是失败,在实例配置中总是无法通过,这样只能通过Archive进行安装了. 1.首先下载 Windows (x86, 64-bit), ZIP Archive安装包. 2.解压到任意目录如:E:\MySQL\MySQL Server 5.5 3.选择安装目录下的*.ini文件(这里选择my-medium.ini)复制并改名为my.ini文件.添加如下内容: [client]default-character-set=utf8 [mysqld] basedir

基于TCP协议的简单Socket通信笔记(JAVA)

好久没写博客了,前段时间忙于做项目,耽误了些时间,今天开始继续写起~ 今天来讲下关于Socket通信的简单应用,关于什么是Socket以及一些网络编程的基础,这里就不提了,只记录最简单易懂实用的东西. 1.首先先来看下基于TCP协议Socket服务端和客户端的通信模型: Socket通信步骤:(简单分为4步) 1.建立服务端ServerSocket和客户端Socket 2.打开连接到Socket的输出输入流 3.按照协议进行读写操作 4.关闭相对应的资源 2.相关联的API: 1.首先先来看下S

用c++开发基于tcp协议的文件上传功能

用c++开发基于tcp协议的文件上传功能 2005我正在一家游戏公司做程序员,当时一直在看<Windows网络编程> 这本书,把里面提到的每种IO模型都试了一次,强烈推荐学习网络编程的同学阅读,比 APUE 讲的更深入 这是某个银行广告项目(p2p传输视频)的一部分 IO模型采用的阻塞模式,文件一打开就直接上传 用vc 2003编译,生成win32 dll 麻雀虽小五脏俱全,CSimpleSocket,CReadStream dll 输出一虚类 extern "C" __d

(1)网络编程的常识 (2)基于tcp协议的编程模型 (3)tcp协议和udp协议的比较 (4)基于udp协议的编程模型

1.网络编程的常识 目前主流的网络通讯软件有:微信.QQ.YY.陌陌.探探.飞信.阿里旺旺.... 在吗? 1.1 七层网络模型(熟悉) 为了保证数据传递的可靠安全等等,ISO(国际标准委员会组织)将数据的传递从逻辑上划分为以下七层: 应用层.表示层.会话层.传输层.网络层.数据链路层.物理层. 在发送数据之前要按照上述七层协议从上到下一层一层进行加包处理,再发送出去; 在接收数据之后要按照上述七层协议从下到上一层一层进行拆包处理,再解析出来: 1.2 常用的协议(熟悉) http协议 - 超文

Android基础入门教程——7.6.3 基于TCP协议的Socket通信(2)

Android基础入门教程--7.6.3 基于TCP协议的Socket通信(2) 标签(空格分隔): Android基础入门教程 本节引言: 上节中我们给大家接触了Socket的一些基本概念以及使用方法,然后写了一个小猪简易聊天室的 Demo,相信大家对Socket有了初步的掌握,本节我们来学习下使用Socket来实现大文件的断点续传! 这里讲解的是别人写好的一个Socket上传大文件的例子,不要求我们自己可以写出来,需要的时候会用 就好! 1.运行效果图: 1.先把我们编写好的Socket服务

支持断点续传的大文件传输协议

文件传输协议(FTP)是一个被广泛应用的网络协议,FTP技术作为文件传输的重要手段,在数据通信领域一直发挥着举足轻重的作用,不支持断点续传,是Internet上最早也是最广泛使用的应用之一. 从1971年A.K.Bhushan提出第一个FTP协议版本(RFC114)到现在,人们对FTP的应用已经历了40余年的时间,同时,许多基于FTP协议的数据传输软件也应运而生.如Windows操作系统下经常使用的支持FTP协议的软件有:CuteFTP.FlashFXP.迅雷(Thunder).快车(Flash

Learning-Python【28】:基于TCP协议通信的套接字

什么是 Socket Socket 是应用层与 TCP/IP 协议通信的中间软件抽象层,它是一组接口.在设计模式中,Socket 其实就是一个门面模式,它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面,对用户来说,一组简单的接口就是全部,让 Socket 去组织数据,以符合指定的协议. 所以,我们无需深入理解 TCP/UDP 协议,socket 已经为我们封装好了,我们只需要遵循 socket 的规定去编程,写出的程序自然就是遵循 TCP/UDP 标准的. 套接字的分类: 基于文件

Java中的基于Tcp协议的网络编程

一:网络通信的三要素? IP地址     端口号     通信协议 IP地址:是网络中设备的通信地址.由于IP地址不易记忆,故可以使用主机名.本地环回地址,127.0.0.1   本地主机名localhost 端口号:发送端准备的数据要发送到指定的目的应用程序上,为了标识这些应用程序,所以用网络数字来标识这些不同的应用程序,这些数 字称为端口号.端口号是不同进程之间的标识.一般来说,有0~65535的端口可供使用,但是1~1024系统使用,或者称作保留端口. 通信协议:指定义的通信规则,这个规则