0xAA55 发表于 2014-12-10 20:02:49

【HTTP】C语言写的简单的HTTP下载器

原帖链接:http://www.0xaa55.com/thread-1093-1-1.html
转载请注明出处。

别指望这东西有多强大的功能——它就是用来下载一个直链资源。给一个URL然后就能完成下载。目前只支持HTTP协议(以http://开头的链接)至于什么ED2K、种子、磁力链,或者ftp、https等,它是不支持的。

这个程序使用了Winsock库,TCP/IP协议。
原理就是建立一个SOCKET套接字(调用socket(AF_INET,SOCK_STREAM,IPPROTO_TCP)得到一个基于TCP/IP协议的SOCKET),分析要下载的文件的URL(比如http://www.baidu.com/img/bdlogo.png),取得协议(http://)、主机名(www.baidu.com)、路径(/img/bdlogo.png),然后建立一个http请求头,将其发送到服务器。服务器会回复一些内容,通过分析服务器发回的内容决定进一步的处理。

HTTP请求头是文本格式,一行一个描述符,然后以两个换行符为结尾。我直接参考了IE浏览器的HTTP请求头(用fiddler抓包)
通过参考了IE浏览器的HTTP请求头,我的下载器的请求头是这个样子的:GET /路径 HTTP/1.1
Host: 域名
Connection: Keep-Alive
Accept: */*
Accept-Language: zh-CN
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
如果我要从文件的中间开始下载,我的请求头会在这里添加Range: 开始字节-结束字节(其中结束字节是可选的)
我并不打算让我的下载器支持gzip、deflate,否则我就需要再给自己的代码整合一个zlib。
然后经过我的测试,当我下载一个7z文件的时候,服务器会返回这么一个HTTP头:HTTP/1.1 200 OK
Date: Mon, 27 Oct 2014 14:10:09 GMT
Server: Apache
Last-Modified: Mon, 27 Oct 2014 07:32:24 GMT
Accept-Ranges: bytes
Content-Length: 内容大小
Connection: close
Content-Type: application/x-7z-compressed
如果Accept-Ranges的值是none,那么表示这个文件不能从中间开始下载,只能从头下载。
Content-Length大概是文件的大小,但是并不是每种MIME类型都会提供这个域,如果我是要下载一个纯文本、html文件,可能就没有Content-Length域。
而有些服务器因为资源被转移了,它会返回一个重定向信息。HTTP/1.1 301 Moved Permanently
Date: Thu, 11 Dec 2014 10:53:19 GMT
Server: Apache/2.2.15 (CentOS)
Location: 新URL
Content-Length: 错误页面的大小
Connection: close
Content-Type: text/html; charset=字符集
这个时候就要注意第一行的HTTP/1.1后面的数字了,200是正常,3XX是重定向,4XX是无法访问特定资源,5XX一般是服务器错误或者其它。
如果是3XX重定向,我们就要注意Location域的内容,它指定了新的URL。

根据如上的经验,我写了这个简单的下载器。
经过测试,它能正确下载文件。


其中下载的文件是可以被正常打开的,有图为证:


而且对于3XX重定向的链接,它也能正确下载:



我这个程序使用了三个文件,为了使代码可复用。
download.c
download.h
entry.c

其中最重要的函数是DownFile,它的原型如下://=============================================================================
//函数:DownFile
//描述:根据指定的URL下载一个文件,返回下载的文件大小。
//用法:提供网络资源地址,以及一些可选的参数,然后使用两个回调函数取得下载到的
//      内容。下载到的内容通过fnOnGetData回调函数返回。
//-----------------------------------------------------------------------------
DownLDFunc(FileSize,DownFile)
(
        char*szURL,                                //网络资源地址
        int Limited,                        //是否限制大小
        FileSize cbStartByte,        //开始字节
        FileSize cbEndByte,                //结束字节
        fnOnGetSize pfnGetSize,        //取得下载到的数据的大小时调用的函数
        fnOnGetData pfnGetData,        //取得下载到的数据时调用的函数
        fnErrReporter ErrReport,//报告错误的函数
        void*pUserData                        //传递给用户回调函数的用户自定义参数
);全部代码如下:
download.h//=============================================================================
//作者:0xAA55
//网站:http://0xaa55.com
//版权所有(C) 技术宅的结界
//请保留原作者信息,否则视为侵权
//
//download.h:
//下载器的头文件
//-----------------------------------------------------------------------------
#ifndef _Download_
#define        _Download_

#include<stddef.h>                        //取得size_t的定义

#ifndef DownLDImpExp                //符号的导出或导入前缀
# ifdef __cplusplus
#   define        DownLDImpExp        extern"C"
# else
#   define        DownLDImpExp        extern
# endif // !__cplusplus
#endif // !DownLDType

#ifndef DownLDCall                        //下载器的调用约定
#define        DownLDCall                        _cdecl
#endif // !DownLDCall

#ifndef DownLDCBCall                //下载器的回调函数调用约定
#define        DownLDCBCall                _cdecl
#endif // !DownLDCBCall

#ifndef DownLDInternal                //内部符号,不导出
#define        DownLDInternal                static
#endif // !DownLDInternal

#ifndef DownLDFunc                        //下载器的函数定义
#define        DownLDFunc(r,f)                DownLDImpExp r DownLDCall f
#endif // !DownLDFunc

#ifndef DownLDCBFunc                //下载器的函数定义
#define        DownLDCBFunc(r,f)        DownLDImpExp r DownLDCBCall f
#endif // !DownLDFunc

typedef        unsigned long long        FileSize;

//=============================================================================
//函数类型:fnErrReporter
//描述:报告错误的函数
//-----------------------------------------------------------------------------
typedef void(DownLDCBCall*fnErrReporter)(char*szFormat,...);

//=============================================================================
//函数类型:fnOnGetFileLen
//描述:取得下载到的数据的大小时调用的函数
//      用户使用此回调函数取得要下载的文件的大小。文件的大小通过FileSize返回
//      当无法取得大小时,FileSize返回Size_Unknown
//                用户如果要拒绝下载,使此回调函数返回零即可,否则返回非零
//-----------------------------------------------------------------------------
typedef        int(DownLDCBCall*fnOnGetSize)
(
        FileSize,                        //内容大小
        void*pUserData                //用户自定义参数
);

#define        Size_Unknown        ((size_t)~0)

//=============================================================================
//函数类型:fnOnGetData
//描述:取得下载到的数据时调用的函数
//                用户如果要拒绝下载,使此回调函数返回零即可,否则返回非零
//-----------------------------------------------------------------------------
typedef        int(DownLDCBCall*fnOnGetData)
(
        FileSize        Position,        //位置
        void                *pData,                //数据指针
        size_t                cbData,                //数据大小
        void                *pUserData        //用户自定义参数
);

//=============================================================================
//函数:DownFile
//描述:根据指定的URL下载一个文件,返回下载的文件大小。
//用法:提供网络资源地址,以及一些可选的参数,然后使用两个回调函数取得下载到的
//      内容。下载到的内容通过fnOnGetData回调函数返回。
//-----------------------------------------------------------------------------
DownLDFunc(FileSize,DownFile)
(
        char*szURL,                                //网络资源地址
        int Limited,                        //是否限制大小
        FileSize cbStartByte,        //开始字节
        FileSize cbEndByte,                //结束字节
        fnOnGetSize pfnGetSize,        //取得下载到的数据的大小时调用的函数
        fnOnGetData pfnGetData,        //取得下载到的数据时调用的函数
        fnErrReporter ErrReport,//报告错误的函数
        void*pUserData                        //传递给用户回调函数的用户自定义参数
);

//=============================================================================
//函数:DefErrReport
//描述:默认的报告错误的函数。
//-----------------------------------------------------------------------------
DownLDCBFunc(void,DefErrReport)(char*szFormat,...);

#endif // !_AA55Download_download.c//=============================================================================
//作者:0xAA55
//网站:http://0xaa55.com
//版权所有(C) 技术宅的结界
//请保留原作者信息,否则视为侵权
//
//download.c:
//下载器的源码
//-----------------------------------------------------------------------------
#include"download.h"
#include<stdio.h>
#include<stdarg.h>
#include<string.h>
#include<WinSock2.h>
#include<WS2tcpip.h>

#define        MaxSendBuf                        0x2000        /*最大发送缓冲区大小*/
#define        MaxBuf                                0x2000        /*最大缓冲区大小*/
#define        MaxPath                                0x400        /*最大路径长度*/
#define        MaxHost                                0x100        /*最大主机名长度*/
#define        MaxServ                                0x40        /*最大服务号长度*/
#define        MaxWaitTime                        3000        /*最长等待时间,3秒*/
#define        MaxZeroByteRecv                3                //能接受的最大0字节包裹数

//=============================================================================
//函数:DoNothingErrReport
//描述:什么也不做的错误报告程序,当用户不提供错误报告程序的时候调用这个。
//-----------------------------------------------------------------------------
DownLDCBFunc(void,DoNothingErrReport)(char*szFormat,...)
{
        //什么也不做
}

//=============================================================================
//函数:DefErrReport
//描述:报告错误。
//-----------------------------------------------------------------------------
DownLDCBFunc(void,DefErrReport)(char*szFormat,...)
{
        va_list ap;
        va_start(ap,szFormat);
        vfprintf(stderr,szFormat,ap);
        va_end(ap);
}

//=============================================================================
//函数:h_error_String
//描述:打印h_errno的内容
//-----------------------------------------------------------------------------
DownLDInternal
char*DownLDCall h_error_String()
{
    switch(h_errno)
    {
    case WSAEACCES:
      return"WSAEACCES";
    case WSAEADDRINUSE:
      return"WSAEADDRINUSE";
    case WSAEADDRNOTAVAIL:
      return"WSAEADDRNOTAVAIL";
    case WSAEAFNOSUPPORT:
      return"WSAEAFNOSUPPORT";
    case WSAEALREADY:
      return"WSAEALREADY";
    case WSAECONNABORTED:
      return"WSAECONNABORTED";
    case WSAECONNREFUSED:
      return"WSAECONNREFUSED";
    case WSAECONNRESET:
      return"WSAECONNRESET";
    case WSAEDESTADDRREQ:
      return"WSAEDESTADDRREQ";
    case WSAEFAULT:
      return"WSAEFAULT";
    case WSAEHOSTDOWN:
      return"WSAEHOSTDOWN";
    case WSAEHOSTUNREACH:
      return"WSAEHOSTUNREACH";
    case WSAEINPROGRESS:
      return"WSAEINPROGRESS";
    case WSAEINTR:
      return"WSAEINTR";
    case WSAEINVAL:
      return"WSAEINVAL";
    case WSAEISCONN:
      return"WSAEISCONN";
    case WSAEMFILE:
      return"WSAEMFILE";
    case WSAEMSGSIZE:
      return"WSAEMSGSIZE";
    case WSAENETDOWN:
      return"WSAENETDOWN";
    case WSAENETRESET:
      return"WSAENETRESET";
    case WSAENETUNREACH:
      return"WSAENETUNREACH";
    case WSAENOBUFS:
      return"WSAENOBUFS";
    case WSAENOPROTOOPT:
      return"WSAENOPROTOOPT";
    case WSAENOTCONN:
      return"WSAENOTCONN";
    case WSAENOTSOCK:
      return"WSAENOTSOCK";
    case WSAEOPNOTSUPP:
      return"WSAEOPNOTSUPP";
    case WSAEPFNOSUPPORT:
      return"WSAEPFNOSUPPORT";
    case WSAEPROCLIM:
      return"WSAEPROCLIM";
    case WSAEPROTONOSUPPORT:
      return"WSAEPROTONOSUPPORT";
    case WSAEPROTOTYPE:
      return"WSAEPROTOTYPE";
    case WSAESHUTDOWN:
      return"WSAESHUTDOWN";
    case WSAESOCKTNOSUPPORT:
      return"WSAESOCKTNOSUPPORT";
    case WSAETIMEDOUT:
      return"WSAETIMEDOUT";
    case WSATYPE_NOT_FOUND:
      return"WSATYPE_NOT_FOUND";
    case WSAEWOULDBLOCK:
      return"WSAEWOULDBLOCK";
    case WSAHOST_NOT_FOUND:
      return"WSAHOST_NOT_FOUND";
    case WSAEINVALIDPROCTABLE:
      return"WSAEINVALIDPROCTABLE";
    case WSAEINVALIDPROVIDER:
      return"WSAEINVALIDPROVIDER";
    case WSANOTINITIALISED:
      return"WSANOTINITIALISED";
    case WSANO_DATA:
      return"WSANO_DATA";
    case WSANO_RECOVERY:
      return"WSANO_RECOVERY";
    case WSAEPROVIDERFAILEDINIT:
      return"WSAEPROVIDERFAILEDINIT";
    case WSASYSCALLFAILURE:
      return"WSASYSCALLFAILURE";
    case WSASYSNOTREADY:
      return"WSASYSNOTREADY";
    case WSATRY_AGAIN:
      return"WSATRY_AGAIN";
    case WSAVERNOTSUPPORTED:
      return"WSAVERNOTSUPPORTED";
    case WSAEDISCON:
      return"WSAEDISCON";
    default:
      return"";
    }
}

//=============================================================================
//函数:GetLineLen
//描述:取得一行字符串的长度
//-----------------------------------------------------------------------------
size_t GetLineLen(char*pLine)
{
        char*pLineEnd;
        pLineEnd=strstr(pLine,"\r\n");
        if(!pLineEnd)
                pLineEnd=strchr(pLine,'\n');
        if(!pLineEnd)
                pLineEnd=&pLine;
        return(size_t)pLineEnd-(size_t)pLine;
}

//=============================================================================
//函数:LineOut
//描述:使用报错函数打印一行内容
//-----------------------------------------------------------------------------
DownLDInternal
void DownLDCall LineOut
(
        char*pLine,                                //行起始
        fnErrReporter ErrReport        //报错函数
)
{
        char*pLineEnd;
        char ChrEnd;
        pLineEnd=strstr(pLine,"\r\n");
        if(!pLineEnd)
                pLineEnd=strchr(pLine,'\n');
        if(!pLineEnd)
                pLineEnd=&pLine;
        ChrEnd=*pLineEnd;
        *pLineEnd='\0';
        ErrReport("%s\n",pLine);
        *pLineEnd=ChrEnd;
}

//=============================================================================
//函数:ParseURL
//描述:分析一个URL,取得域名、路径等信息。
//-----------------------------------------------------------------------------
DownLDInternal
void DownLDCall ParseURL
(
        char*szURL,                //URL
        char*szProtocol,//协议
        char*szHostOut,        //主机名
        char*szServOut,        //服务名、端口号
        char*szPathOut        //路径
)
{
        char*pChr;
        size_t cbStr;
        char*pPath;
        char*pHost;
       
        pChr=strchr(szURL,':');//从开头到第一个冒号之间的字符串是协议,比如http,ftp
        if(!pChr)
                return;

        //如果需要返回协议类型
        if(szProtocol)
        {
                cbStr=(size_t)pChr-(size_t)szURL;
                memset(szProtocol,0,cbStr+1);
                memcpy(szProtocol,szURL,cbStr);
        }

        //准备处理主机名和服务名
        pHost=pChr+3;//冒号往后3字节开始
        pPath=strchr(pHost,'/');//路径
        if(!pPath)
                pPath=&pHost;//如果没有路径则指向字符串最后一个字节(0)

        pChr=strchr(pHost,':');//找端口号
        if(pChr && (pPath?(size_t)pChr<(size_t)pPath:1))//如果指定了端口号
        {
                if(szHostOut)//如果需要返回主机名
                {
                        size_t cbHost=(size_t)pChr-(size_t)pHost;//主机名字符串长度
                        strncpy(szHostOut,pHost,cbHost);
                        szHostOut='\0';
                }
                if(szServOut)//如果需要返回服务名
                {
                        size_t cbServ=(size_t)pPath-(size_t)pChr-1;//服务名字符串长度
                        strncpy(szServOut,pChr+1,cbServ);
                        szServOut='\0';
                }
        }
        else
        {
                if(szHostOut)//如果需要返回主机名
                {
                        size_t cbHost=(size_t)pPath-(size_t)pHost;//主机名字符串长度
                        strncpy(szHostOut,pHost,cbHost);
                        szHostOut='\0';
                }
                if(szServOut)//如果需要返回服务名
                        strcpy(szServOut,"80");
        }

        if(szPathOut)//如果需要返回路径
        {
                size_t LineLen=GetLineLen(pPath);
                strncpy(szPathOut,pPath,LineLen);
                szPathOut='\0';
        }
}

//=============================================================================
//函数:ConnectToHost
//描述:让一个Socket连接到一个域名,成功返回非零
//-----------------------------------------------------------------------------
DownLDInternal
int DownLDCall ConnectToHost
(
        SOCKET sock,                        //套接字
        char*szHost,                        //主机名
        char*szServ,                        //服务名
        fnErrReporter ErrReport        //报错函数
)
{
        struct addrinfo        Hints;                                //用于解析域名的Hint
        struct addrinfo*pAddrInfoFirst=NULL;//解析取得的域名
        struct addrinfo*pAddrInfo;                        //用于走链表

        //解析域名
        Hints.ai_flags=0;
        Hints.ai_family=AF_INET;
        Hints.ai_socktype=SOCK_STREAM;
        Hints.ai_protocol=IPPROTO_TCP;
        Hints.ai_addrlen=0;
        Hints.ai_canonname=NULL;
        Hints.ai_addr=NULL;
        Hints.ai_next=NULL;
        if(getaddrinfo(szHost,szServ,&Hints,&pAddrInfoFirst))
        {
                ErrReport("Unable to resolve host name:%s.%s\n",
                        szHost,h_error_String());
                goto Cleanup;
        }

        //遍历列表直到能连接。
        pAddrInfo=pAddrInfoFirst;
        do
        {
#                ifdef _DEBUG
                ErrReport("Trying:%u.%u.%u.%u:%u\n",
                        ((SOCKADDR_IN*)(pAddrInfo->ai_addr))->sin_addr.s_net,
                        ((SOCKADDR_IN*)(pAddrInfo->ai_addr))->sin_addr.s_host,
                        ((SOCKADDR_IN*)(pAddrInfo->ai_addr))->sin_addr.s_lh,
                        ((SOCKADDR_IN*)(pAddrInfo->ai_addr))->sin_addr.s_impno,
                        htons(((SOCKADDR_IN*)(pAddrInfo->ai_addr))->sin_port));
#                endif
                if(!connect(sock,pAddrInfo->ai_addr,(int)(pAddrInfo->ai_addrlen)))
                        break;//连接上了就跳出循环
                pAddrInfo=pAddrInfo->ai_next;//寻到链表下一个
        }while(pAddrInfo);

        if(!pAddrInfo)//链表到头了
        {
                ErrReport("Unable to retrieve the host name:%s.%s\n",
                        szHost,h_error_String());
                goto Cleanup;
        }

        //释放整个链表
        freeaddrinfo(pAddrInfoFirst);
        return 1;
Cleanup:
        if(pAddrInfoFirst)
                freeaddrinfo(pAddrInfoFirst);
        return 0;
}

//=============================================================================
//函数:SendRequestHeader
//描述:通过SOCKET发送HTTP请求头
//-----------------------------------------------------------------------------
DownLDInternal
int DownLDCall SendRequestHeader
(
        SOCKET sock,                        //套接字
        char*szHost,                        //主机名
        char*szServ,                        //服务名
        char*szPath,                        //请求路径
        int Limited,                        //是否限制大小
        FileSize cbStartByte,        //开始字节
        FileSize cbEndByte,                //结束字节
        fnErrReporter ErrReport        //报告错误的函数
)
{
        char szBuf;        //发送缓冲区

        strcpy(szBuf,"GET ");
        strcat(szBuf,szPath);//路径
        strcat(szBuf," HTTP/1.1\r\nHost: ");
        strcat(szBuf,szHost);//主机
        strcat(szBuf,"\r\nConnection: Keep-Alive\r\n"//保持连接
                "Accept: */*\r\n"//接受任何MIME类型
                "Accept-Language: zh-CN\r\n"//语言:简体中文
                "User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0)"
                " like Gecko\r\n");//Win7 x64里的IE11的User-Agent值,居然没有MSIE字样

        //如果限制了大小和范围,则给出Range域
        if(Limited)
                sprintf(szBuf,"%sRange: %u-%u\r\n\r\n",szBuf,cbStartByte,cbEndByte);
        else if(cbStartByte)//否则判断是否要指定下载开始位置
                sprintf(szBuf,"%sRange: %u-\r\n\r\n",szBuf,cbStartByte);
        else
                strcat(szBuf,"\r\n");

        //发送HTTP请求包
#        ifdef _DEBUG
        ErrReport("%s",szBuf);
#        endif
        if(send(sock,szBuf,(int)strlen(szBuf),0)==SOCKET_ERROR)
        {
                ErrReport("Failed to send HTTP header.%s\n",h_error_String());
                goto Cleanup;
        }
        return 1;
Cleanup:
        return 0;
}

//=============================================================================
//函数:DownFile
//描述:根据指定的URL下载一个文件,返回下载的文件大小。
//-----------------------------------------------------------------------------
DownLDFunc(FileSize,DownFile)
(
        char*szURL,                                //网络资源地址
        int Limited,                        //是否限制大小
        FileSize cbStartByte,        //开始字节
        FileSize cbEndByte,                //结束字节
        fnOnGetSize pfnGetSize,        //取得下载到的数据的大小时调用的函数
        fnOnGetData pfnGetData,        //取得下载到的数据时调用的函数
        fnErrReporter ErrReport,//报告错误的函数
        void*pUserData                        //传递给用户回调函数的用户自定义参数
)
{
        char szBuf;                                //缓冲区
        char szHost;                                //域名
        char szServ;                                //服务名或端口号
        char szPath;                                //路径
        int cbRecv;                                                        //接收到的字节数
        int HeaderFinished=0;                                //是否已读取完HTTP头
        int StatusCode=0;                                        //状态代码(301、404、502等)
        FileSize ContentLength=Size_Unknown;//内容长度
        FileSize ContentRecv=0;                                //接收到的内容长度
        FileSize CurPos=cbStartByte;                //当前文件位置

        int Retry=0;                                                //是否重试
        unsigned RcvTimeOut=MaxWaitTime;        //recv最长等待时间
        unsigned NbZeroByteRecv=0;                        //接收到的0字节包裹数

        SOCKET sockClient=INVALID_SOCKET;        //用于下载的套接字

        //如果用户不提供报错回调函数,则报错的时候什么也不做
        if(!ErrReport)
                ErrReport=DoNothingErrReport;

        //直接判断前7字节是不是http://,不使用ParseURL返回的“协议”
        if(strnicmp(szURL,"http://",7))
        {
                //必须是http协议。
                ErrReport("Protocol must be HTTP.\n");
                goto Cleanup;
        }

        //解析URL,取得主机名、服务名、路径等信息
        ParseURL(szURL,NULL,szHost,szServ,szPath);

        do//while(Retry--);
        {
                //=====================================================================
                //这一层循环用于提供重试的机会。每次“尝试”相当于以下操作:
                //建立Socket
                //连接到域名
                //发送HTTP请求
                //分析服务器传回的内容
                //---------------------------------------------------------------------
                //初始化套接字,TCP/IP协议
                sockClient=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
                if(sockClient==INVALID_SOCKET)
                {
                        //初始化套接字失败。
                        ErrReport("Unable to initialize socket.%s\n",h_error_String());
                        goto Cleanup;
                }

                //设置recv函数的超时时间
                setsockopt(sockClient,SOL_SOCKET,SO_RCVTIMEO,
                        (const char*)&RcvTimeOut,sizeof(RcvTimeOut));

                //连接Socket到域名
                if(!ConnectToHost(sockClient,szHost,szServ,ErrReport))
                        goto Cleanup;

                //发送HTTP请求包
                if(!SendRequestHeader(sockClient,szHost,szServ,
                        szPath,Limited,cbStartByte,cbEndByte,ErrReport))
                        goto Cleanup;
               
                //=====================================================================
                //接收内容
                //---------------------------------------------------------------------
                do//while(ContentRecv<ContentLength);
                {
                        int iErrno;
                        //HTTP头的长度可能超过我们一个缓冲区的大小
                        //因此我们进行分块接收
                        memset(szBuf,0,sizeof(szBuf));
                        cbRecv=recv(sockClient,szBuf,MaxBuf,0);
                        if(cbRecv==SOCKET_ERROR)
                        {
                                ErrReport("Failed to receive data.%s\n",h_error_String());
                                goto Cleanup;
                        }

                        //检查错误代码
                        iErrno=h_errno;
                        if(        iErrno==WSAENOTCONN||
                                iErrno==WSAESHUTDOWN ||
                                iErrno==WSAENETRESET ||
                                iErrno==WSAECONNRESET||
                                iErrno==WSAECONNABORTED)
                        {
                                ErrReport("The network has been disconnected.%s\n",
                                        h_error_String());
                                break;
                        }

                        //超时
                        if(iErrno==WSAETIMEDOUT)
                        {
                                ErrReport("Respond timeout.%s\n",h_error_String());
                                Retry++;
                                break;
                        }

                        if(!cbRecv)//零字节包裹
                        {
                                NbZeroByteRecv++;//记录零字节包裹数
                                if(NbZeroByteRecv>=MaxZeroByteRecv)
                                {//如果接收到的0字节包裹数超过容忍度
                                        Retry++;//重连
                                        NbZeroByteRecv=0;//重新统计零字节包裹数
                                        break;
                                }
                                continue;//否则继续
                        }
                       
                        //=================================================================
                        //已读取HTTP头,接收文件内容
                        //-----------------------------------------------------------------
                        if(HeaderFinished)
                        {
                                size_t cbDataLen=cbRecv;
                                ContentRecv+=cbDataLen;//统计已经下载到的字节数。

                                if(pfnGetData && cbDataLen)
                                {//将数据传递给用户
                                        if(!pfnGetData(CurPos,szBuf,cbDataLen,pUserData))
                                        {
                                                ErrReport("Download aborted.\n");
                                                goto Cleanup;
                                        }
                                }
                                CurPos+=cbDataLen;
                        }
                        //=================================================================
                        //未读取完HTTP头,解析HTTP头
                        //-----------------------------------------------------------------
                        else
                        {
                                char*pLinePtr;//准备一行一行分析http头
                                char*pChr;//字符指针
                                pLinePtr=szBuf;//从缓冲区开头开始
                                while(pLinePtr && (size_t)pLinePtr<(size_t)szBuf+cbRecv)
                                {//当行指针并没有超出缓冲区边界
                                        //如果读取到两个换行符,意味着HTTP头结束
                                        if(!strncmp(pLinePtr,"\r\n\r\n",4))
                                        {
                                                size_t cbDataLen;
                                                pLinePtr+=4;//跳过两个回车,转到内容

                                                //计算剩余的数据长度
                                                cbDataLen=cbRecv-((size_t)pLinePtr-(size_t)szBuf);
                                                HeaderFinished=1;//已读取Http头
                                                ContentRecv+=cbDataLen;//统计已经下载到的字节数。

                                                if(pfnGetData && cbDataLen)
                                                {//将数据传递给用户
                                                        if(!pfnGetData(CurPos,pLinePtr,
                                                                cbDataLen,pUserData))
                                                        {
                                                                ErrReport("Download aborted.\n");
                                                                goto Cleanup;
                                                        }
                                                }
                                                CurPos+=cbDataLen;
                                                break;
                                        }

                                        //跳过行开头的空格、Tab等
                                        while(        *pLinePtr==' '||
                                                        *pLinePtr=='\t' ||
                                                        *pLinePtr=='\r' ||
                                                        *pLinePtr=='\n')
                                                pLinePtr++;

                                        //=========================================================
                                        //按照条件分析HTTP头的每行的数据
                                        //---------------------------------------------------------
#                                        ifdef _DEBUG
                                        //调试模式下打印出HTTP头的内容
                                        LineOut(pLinePtr,ErrReport);
#                                        endif // _DEBUG

                                        //=========================================================
                                        //判断HTTP状态代码
                                        if(!strnicmp(pLinePtr,"HTTP/1.1",8))
                                        {
                                                //如果是3XX表示已经移动,4XX表示无法访问,5XX则直接放弃
                                                //"HTTP/1.1"后面可能有空格
                                                pChr=pLinePtr+8;
                                                while(        *pChr==' '||
                                                                *pChr=='\t')
                                                        pChr++;
                                                LineOut(pChr,ErrReport);
                                                //读取状态代码
                                                StatusCode=atoi(pChr);
                                                if(StatusCode>=400)//超过400则放弃下载。
                                                        goto Cleanup;
                                        }else
                                        //=========================================================
                                        //如果是Accept-Ranges域
                                        if(!strnicmp(pLinePtr,"accept-ranges:",14))
                                        {
                                                //判断是不是"Accept-Ranges:none",它意味着只能从头下载。
                                                //"Accept-Ranges:"后面可能有空格
                                                pChr=pLinePtr+14;
                                                while(        *pChr==' '||
                                                                *pChr=='\t')
                                                        pChr++;
                                                if(!strnicmp(pChr,"none",4))
                                                        CurPos=0;
                                        }else
                                        //=========================================================
                                        //如果是Content-Length域
                                        if(!strnicmp(pLinePtr,"content-length:",15))
                                        {
                                                //"Content-Length"域指定了内容的长度
                                                //"Content-Length:"后面可能有空格
                                                pChr=pLinePtr+15;
                                                while(        *pChr==' '||
                                                                *pChr=='\t')
                                                        pChr++;
                                                ContentLength=(FileSize)_atoi64(pChr);
                                                if(pfnGetSize)//这个回调函数可以是NULL
                                                {
                                                        if(!pfnGetSize(ContentLength,pUserData))
                                                        {
                                                                ErrReport("Download aborted.\n");
                                                                goto Cleanup;
                                                        }
                                                }
                                        }else
                                        //=========================================================
                                        //如果是Location:域,并且之前指定了3XX重定向
                                        if(StatusCode>=300 && !strnicmp(pLinePtr,"Location:",9))
                                        {
                                                //"Location"域指定了跳转的新位置
                                                //"Location:"后面可能有空格
                                                pChr=pLinePtr+9;
                                                while(        *pChr==' '||
                                                                *pChr=='\t')
                                                        pChr++;

                                                //判断前7字节是不是http://
                                                if(strnicmp(pChr,"http://",7))
                                                {
                                                        //必须是http协议。
                                                        ErrReport("Protocol must be HTTP.\n");
                                                        goto Cleanup;
                                                }
                                               
                                                //解析URL,取得主机名、服务名、路径等信息
                                                ParseURL(pChr,NULL,szHost,szServ,szPath);
                                                Retry++;

#                                                ifdef _DEBUG
                                                ErrReport("\nRedirecting to %s\n",szHost);
#                                                endif
                                                break;
                                        }

                                        pLinePtr=strstr(pLinePtr,"\r\n");//转到下一行
                                }//while(pLinePtr && (size_t)pLinePtr<(size_t)szBuf+cbRecv)
                        }//if(!HeaderFinished)

                        //如果已经决定重试,则不再等待接收数据。
                        if(Retry)
                                break;
                        //接收总内容达到“说好的”大小,认定接收完成
                }while(ContentRecv<ContentLength);
                closesocket(sockClient);
        }while(Retry--);

        return ContentRecv;
Cleanup:
        if(sockClient!=INVALID_SOCKET)
                closesocket(sockClient);
        return ContentRecv;
}entry.c//=============================================================================
//作者:0xAA55
//网站:http://0xaa55.com
//版权所有(C) 技术宅的结界
//请保留原作者信息,否则视为侵权
//-----------------------------------------------------------------------------
#include"download.h"
#include<io.h>
#include<stdio.h>
#include<conio.h>
#include<WinSock2.h>

//=============================================================================
//函数:Usage
//描述:介绍程序的用法
//-----------------------------------------------------------------------------
void Usage(char*argv0)
{
        fprintf(stderr,"Usage:\n"
"%s URL TargetFilePath \n"
"URL should be started with \"http://\", used to specify which file you want\n"
"to download.\n"
"TargetFilePath should be the location you want to download to.\n"
" is optional, syntax is ###-[###].\n",argv0);
}

//=============================================================================
//函数:OnGetSize
//描述:取得文件大小时的回调函数
//-----------------------------------------------------------------------------
int DownLDCBCall OnGetSize(FileSize Size,void*pUserData)
{
        if(Size==Size_Unknown)//如果文件大小未知
                fputs("Unknown content length.\n",stderr);
        //else
        //        fprintf(stderr,"Content length:%.I64u\n",Size);
        return 1;
}

//=============================================================================
//函数类型:OnGetData
//描述:取得下载到的数据时调用的函数
//-----------------------------------------------------------------------------
int DownLDCBCall OnGetData
(
        FileSize        Position,        //位置
        void                *pData,                //数据指针
        size_t                cbData,                //数据大小
        void                *pUserData        //用户自定义参数
)
{
        FILE*fp;

        fprintf(stderr,"Received %u bytes.\n",cbData);

        fp=fopen((const char*)pUserData,"ab");
        if(fp)
        {
                fwrite(pData,1,cbData,fp);
                fclose(fp);
        }

        return 1;
}

char*h_error_String();

//=============================================================================
//函数:main
//描述:程序入口点
//-----------------------------------------------------------------------------
int main(int argc,char**argv)
{
        WSADATA wsaData;
        int Limited=0;
        size_t StartByte=0;
        size_t EndByte=0;

        //至少两个参数:URL和存放的文件位置
        if(argc<3)
        {
                Usage(argc?argv:"HTTPDOWN");
                return 1;
        }

        //第三个参数:可选的下载文件的开始位置和结束位置
        if(argc>3)
        {
                Limited=1;
                if(sscanf(argv,"%u-%u",&StartByte,&EndByte)!=2)
                {
                        fputs("Invalid parameters.\n",stderr);
                        return 1;
                }
        }

        //先删除已经存在的文件。
        if(!_access(argv,0))
                unlink(argv);
       
        //初始化Winsock
        if(WSAStartup(WINSOCK_VERSION,&wsaData))
                goto Cleanup;
       
        //下载并打印字节数
        fprintf(stderr,"%u bytes downloaded.\n",
                DownFile(argv,Limited,StartByte,EndByte,OnGetSize,OnGetData,DefErrReport,argv));
       
        WSACleanup();
        return 0;
Cleanup:
        fprintf(stderr,"%s\n",h_error_String());
        WSACleanup();
        return 2;
}


//=============================================================================
//函数:h_error_String
//描述:打印h_errno的内容
//-----------------------------------------------------------------------------
char*h_error_String()
{
        switch(h_errno)
        {
        case WSAEACCES:
                return"WSAEACCES";
        case WSAEADDRINUSE:
                return"WSAEADDRINUSE";
        case WSAEADDRNOTAVAIL:
                return"WSAEADDRNOTAVAIL";
        case WSAEAFNOSUPPORT:
                return"WSAEAFNOSUPPORT";
        case WSAEALREADY:
                return"WSAEALREADY";
        case WSAECONNABORTED:
                return"WSAECONNABORTED";
        case WSAECONNREFUSED:
                return"WSAECONNREFUSED";
        case WSAECONNRESET:
                return"WSAECONNRESET";
        case WSAEDESTADDRREQ:
                return"WSAEDESTADDRREQ";
        case WSAEFAULT:
                return"WSAEFAULT";
        case WSAEHOSTDOWN:
                return"WSAEHOSTDOWN";
        case WSAEHOSTUNREACH:
                return"WSAEHOSTUNREACH";
        case WSAEINPROGRESS:
                return"WSAEINPROGRESS";
        case WSAEINTR:
                return"WSAEINTR";
        case WSAEINVAL:
                return"WSAEINVAL";
        case WSAEISCONN:
                return"WSAEISCONN";
        case WSAEMFILE:
                return"WSAEMFILE";
        case WSAEMSGSIZE:
                return"WSAEMSGSIZE";
        case WSAENETDOWN:
                return"WSAENETDOWN";
        case WSAENETRESET:
                return"WSAENETRESET";
        case WSAENETUNREACH:
                return"WSAENETUNREACH";
        case WSAENOBUFS:
                return"WSAENOBUFS";
        case WSAENOPROTOOPT:
                return"WSAENOPROTOOPT";
        case WSAENOTCONN:
                return"WSAENOTCONN";
        case WSAENOTSOCK:
                return"WSAENOTSOCK";
        case WSAEOPNOTSUPP:
                return"WSAEOPNOTSUPP";
        case WSAEPFNOSUPPORT:
                return"WSAEPFNOSUPPORT";
        case WSAEPROCLIM:
                return"WSAEPROCLIM";
        case WSAEPROTONOSUPPORT:
                return"WSAEPROTONOSUPPORT";
        case WSAEPROTOTYPE:
                return"WSAEPROTOTYPE";
        case WSAESHUTDOWN:
                return"WSAESHUTDOWN";
        case WSAESOCKTNOSUPPORT:
                return"WSAESOCKTNOSUPPORT";
        case WSAETIMEDOUT:
                return"WSAETIMEDOUT";
        case WSATYPE_NOT_FOUND:
                return"WSATYPE_NOT_FOUND";
        case WSAEWOULDBLOCK:
                return"WSAEWOULDBLOCK";
        case WSAHOST_NOT_FOUND:
                return"WSAHOST_NOT_FOUND";
        case WSAEINVALIDPROCTABLE:
                return"WSAEINVALIDPROCTABLE";
        case WSAEINVALIDPROVIDER:
                return"WSAEINVALIDPROVIDER";
        case WSANOTINITIALISED:
                return"WSANOTINITIALISED";
        case WSANO_DATA:
                return"WSANO_DATA";
        case WSANO_RECOVERY:
                return"WSANO_RECOVERY";
        case WSAEPROVIDERFAILEDINIT:
                return"WSAEPROVIDERFAILEDINIT";
        case WSASYSCALLFAILURE:
                return"WSASYSCALLFAILURE";
        case WSASYSNOTREADY:
                return"WSASYSNOTREADY";
        case WSATRY_AGAIN:
                return"WSATRY_AGAIN";
        case WSAVERNOTSUPPORTED:
                return"WSAVERNOTSUPPORTED";
        case WSAEDISCON:
                return"WSAEDISCON";
        default:
                return"";
        }
}BIN下载:
**** Hidden Message *****

元始天尊 发表于 2014-12-10 21:10:47

会用fiddler就可以写很多小东西了,比如自动登录什么的

0xAA55 发表于 2014-12-11 00:47:03

内容已经更新,现在可以下载百度的内容了。其实百度的服务器会单独发送HTTP头和内容。。。很奇葩。估计是百度自己弄的http服务程序,而不是Apache的httpd或MS的IIS。

0xAA55 发表于 2014-12-11 20:30:15

内容再次更新。现在可以下载经过3XX重定向的内容了。

xueyifan 发表于 2015-5-27 17:04:16

终于找到HTTP用C语言实现下载的文章啦!!!

0x01810 发表于 2015-6-6 16:57:21

到现在才知道原来有fiddler这种神器....

huoyong1985 发表于 2015-6-23 10:15:09

谢谢分享正好需要这个

Battle 发表于 2015-12-14 17:04:20

好东西啊,来看看,顶起~

9998887776 发表于 2016-1-17 21:46:01

谢谢分享,来一个试试。。。

老麦 发表于 2016-2-21 14:51:10

谢谢,学习中

Larpx 发表于 2016-2-26 08:46:35

捣鼓下来看看~

jason 发表于 2016-8-24 21:20:00

新人学习一下

jason 发表于 2016-8-24 21:20:18

#在这里快速回复#新人学习一下

jasonchen 发表于 2016-11-17 10:33:51

支持    !!

tianguo0826 发表于 2016-11-23 19:18:18

xieh tahnyoufasd

浩浩 发表于 2016-11-25 13:24:51

谢谢楼主分享

浩浩 发表于 2016-11-25 13:26:36

谢谢楼主分享

Ink 发表于 2017-4-19 08:48:37

不错不错

qytom 发表于 2017-4-21 17:06:12

很好的技术文章。:hug:

wk1993 发表于 2017-5-2 22:13:20

网络学者,前来拜访
页: [1] 2
查看完整版本: 【HTTP】C语言写的简单的HTTP下载器