找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 25701|回复: 24

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

[复制链接]
发表于 2014-12-10 20:02:49 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

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

根据如上的经验,我写了这个简单的下载器。
经过测试,它能正确下载文件。
20141211201514.png
20141211201533.png
其中下载的文件是可以被正常打开的,有图为证:
20141210195928.png

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

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

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

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

  13. #ifndef DownLDImpExp                //符号的导出或导入前缀
  14. # ifdef __cplusplus
  15. #   define        DownLDImpExp        extern"C"
  16. # else
  17. #   define        DownLDImpExp        extern
  18. # endif // !__cplusplus
  19. #endif // !DownLDType

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

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

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

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

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

  35. typedef        unsigned long long        FileSize;

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

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

  53. #define        Size_Unknown        ((size_t)~0)

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

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

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

  88. #endif // !_AA55Download_
复制代码
download.c
  1. //=============================================================================
  2. //作者:0xAA55
  3. //网站:http://0xaa55.com
  4. //版权所有(C) 技术宅的结界
  5. //请保留原作者信息,否则视为侵权
  6. //
  7. //download.c:
  8. //下载器的源码
  9. //-----------------------------------------------------------------------------
  10. #include"download.h"
  11. #include<stdio.h>
  12. #include<stdarg.h>
  13. #include<string.h>
  14. #include<WinSock2.h>
  15. #include<WS2tcpip.h>

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

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

  31. //=============================================================================
  32. //函数:DefErrReport
  33. //描述:报告错误。
  34. //-----------------------------------------------------------------------------
  35. DownLDCBFunc(void,DefErrReport)(char*szFormat,...)
  36. {
  37.         va_list ap;
  38.         va_start(ap,szFormat);
  39.         vfprintf(stderr,szFormat,ap);
  40.         va_end(ap);
  41. }

  42. //=============================================================================
  43. //函数:h_error_String
  44. //描述:打印h_errno的内容
  45. //-----------------------------------------------------------------------------
  46. DownLDInternal
  47. char*DownLDCall h_error_String()
  48. {
  49.     switch(h_errno)
  50.     {
  51.     case WSAEACCES:
  52.         return"WSAEACCES";
  53.     case WSAEADDRINUSE:
  54.         return"WSAEADDRINUSE";
  55.     case WSAEADDRNOTAVAIL:
  56.         return"WSAEADDRNOTAVAIL";
  57.     case WSAEAFNOSUPPORT:
  58.         return"WSAEAFNOSUPPORT";
  59.     case WSAEALREADY:
  60.         return"WSAEALREADY";
  61.     case WSAECONNABORTED:
  62.         return"WSAECONNABORTED";
  63.     case WSAECONNREFUSED:
  64.         return"WSAECONNREFUSED";
  65.     case WSAECONNRESET:
  66.         return"WSAECONNRESET";
  67.     case WSAEDESTADDRREQ:
  68.         return"WSAEDESTADDRREQ";
  69.     case WSAEFAULT:
  70.         return"WSAEFAULT";
  71.     case WSAEHOSTDOWN:
  72.         return"WSAEHOSTDOWN";
  73.     case WSAEHOSTUNREACH:
  74.         return"WSAEHOSTUNREACH";
  75.     case WSAEINPROGRESS:
  76.         return"WSAEINPROGRESS";
  77.     case WSAEINTR:
  78.         return"WSAEINTR";
  79.     case WSAEINVAL:
  80.         return"WSAEINVAL";
  81.     case WSAEISCONN:
  82.         return"WSAEISCONN";
  83.     case WSAEMFILE:
  84.         return"WSAEMFILE";
  85.     case WSAEMSGSIZE:
  86.         return"WSAEMSGSIZE";
  87.     case WSAENETDOWN:
  88.         return"WSAENETDOWN";
  89.     case WSAENETRESET:
  90.         return"WSAENETRESET";
  91.     case WSAENETUNREACH:
  92.         return"WSAENETUNREACH";
  93.     case WSAENOBUFS:
  94.         return"WSAENOBUFS";
  95.     case WSAENOPROTOOPT:
  96.         return"WSAENOPROTOOPT";
  97.     case WSAENOTCONN:
  98.         return"WSAENOTCONN";
  99.     case WSAENOTSOCK:
  100.         return"WSAENOTSOCK";
  101.     case WSAEOPNOTSUPP:
  102.         return"WSAEOPNOTSUPP";
  103.     case WSAEPFNOSUPPORT:
  104.         return"WSAEPFNOSUPPORT";
  105.     case WSAEPROCLIM:
  106.         return"WSAEPROCLIM";
  107.     case WSAEPROTONOSUPPORT:
  108.         return"WSAEPROTONOSUPPORT";
  109.     case WSAEPROTOTYPE:
  110.         return"WSAEPROTOTYPE";
  111.     case WSAESHUTDOWN:
  112.         return"WSAESHUTDOWN";
  113.     case WSAESOCKTNOSUPPORT:
  114.         return"WSAESOCKTNOSUPPORT";
  115.     case WSAETIMEDOUT:
  116.         return"WSAETIMEDOUT";
  117.     case WSATYPE_NOT_FOUND:
  118.         return"WSATYPE_NOT_FOUND";
  119.     case WSAEWOULDBLOCK:
  120.         return"WSAEWOULDBLOCK";
  121.     case WSAHOST_NOT_FOUND:
  122.         return"WSAHOST_NOT_FOUND";
  123.     case WSAEINVALIDPROCTABLE:
  124.         return"WSAEINVALIDPROCTABLE";
  125.     case WSAEINVALIDPROVIDER:
  126.         return"WSAEINVALIDPROVIDER";
  127.     case WSANOTINITIALISED:
  128.         return"WSANOTINITIALISED";
  129.     case WSANO_DATA:
  130.         return"WSANO_DATA";
  131.     case WSANO_RECOVERY:
  132.         return"WSANO_RECOVERY";
  133.     case WSAEPROVIDERFAILEDINIT:
  134.         return"WSAEPROVIDERFAILEDINIT";
  135.     case WSASYSCALLFAILURE:
  136.         return"WSASYSCALLFAILURE";
  137.     case WSASYSNOTREADY:
  138.         return"WSASYSNOTREADY";
  139.     case WSATRY_AGAIN:
  140.         return"WSATRY_AGAIN";
  141.     case WSAVERNOTSUPPORTED:
  142.         return"WSAVERNOTSUPPORTED";
  143.     case WSAEDISCON:
  144.         return"WSAEDISCON";
  145.     default:
  146.         return"";
  147.     }
  148. }

  149. //=============================================================================
  150. //函数:GetLineLen
  151. //描述:取得一行字符串的长度
  152. //-----------------------------------------------------------------------------
  153. size_t GetLineLen(char*pLine)
  154. {
  155.         char*pLineEnd;
  156.         pLineEnd=strstr(pLine,"\r\n");
  157.         if(!pLineEnd)
  158.                 pLineEnd=strchr(pLine,'\n');
  159.         if(!pLineEnd)
  160.                 pLineEnd=&pLine[strlen(pLine)];
  161.         return(size_t)pLineEnd-(size_t)pLine;
  162. }

  163. //=============================================================================
  164. //函数:LineOut
  165. //描述:使用报错函数打印一行内容
  166. //-----------------------------------------------------------------------------
  167. DownLDInternal
  168. void DownLDCall LineOut
  169. (
  170.         char*pLine,                                //行起始
  171.         fnErrReporter ErrReport        //报错函数
  172. )
  173. {
  174.         char*pLineEnd;
  175.         char ChrEnd;
  176.         pLineEnd=strstr(pLine,"\r\n");
  177.         if(!pLineEnd)
  178.                 pLineEnd=strchr(pLine,'\n');
  179.         if(!pLineEnd)
  180.                 pLineEnd=&pLine[strlen(pLine)];
  181.         ChrEnd=*pLineEnd;
  182.         *pLineEnd='\0';
  183.         ErrReport("%s\n",pLine);
  184.         *pLineEnd=ChrEnd;
  185. }

  186. //=============================================================================
  187. //函数:ParseURL
  188. //描述:分析一个URL,取得域名、路径等信息。
  189. //-----------------------------------------------------------------------------
  190. DownLDInternal
  191. void DownLDCall ParseURL
  192. (
  193.         char*szURL,                //URL
  194.         char*szProtocol,//协议
  195.         char*szHostOut,        //主机名
  196.         char*szServOut,        //服务名、端口号
  197.         char*szPathOut        //路径
  198. )
  199. {
  200.         char*pChr;
  201.         size_t cbStr;
  202.         char*pPath;
  203.         char*pHost;
  204.        
  205.         pChr=strchr(szURL,':');//从开头到第一个冒号之间的字符串是协议,比如http,ftp
  206.         if(!pChr)
  207.                 return;

  208.         //如果需要返回协议类型
  209.         if(szProtocol)
  210.         {
  211.                 cbStr=(size_t)pChr-(size_t)szURL;
  212.                 memset(szProtocol,0,cbStr+1);
  213.                 memcpy(szProtocol,szURL,cbStr);
  214.         }

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

  220.         pChr=strchr(pHost,':');//找端口号
  221.         if(pChr && (pPath?(size_t)pChr<(size_t)pPath:1))//如果指定了端口号
  222.         {
  223.                 if(szHostOut)//如果需要返回主机名
  224.                 {
  225.                         size_t cbHost=(size_t)pChr-(size_t)pHost;//主机名字符串长度
  226.                         strncpy(szHostOut,pHost,cbHost);
  227.                         szHostOut[cbHost]='\0';
  228.                 }
  229.                 if(szServOut)//如果需要返回服务名
  230.                 {
  231.                         size_t cbServ=(size_t)pPath-(size_t)pChr-1;//服务名字符串长度
  232.                         strncpy(szServOut,pChr+1,cbServ);
  233.                         szServOut[cbServ]='\0';
  234.                 }
  235.         }
  236.         else
  237.         {
  238.                 if(szHostOut)//如果需要返回主机名
  239.                 {
  240.                         size_t cbHost=(size_t)pPath-(size_t)pHost;//主机名字符串长度
  241.                         strncpy(szHostOut,pHost,cbHost);
  242.                         szHostOut[cbHost]='\0';
  243.                 }
  244.                 if(szServOut)//如果需要返回服务名
  245.                         strcpy(szServOut,"80");
  246.         }

  247.         if(szPathOut)//如果需要返回路径
  248.         {
  249.                 size_t LineLen=GetLineLen(pPath);
  250.                 strncpy(szPathOut,pPath,LineLen);
  251.                 szPathOut[LineLen]='\0';
  252.         }
  253. }

  254. //=============================================================================
  255. //函数:ConnectToHost
  256. //描述:让一个Socket连接到一个域名,成功返回非零
  257. //-----------------------------------------------------------------------------
  258. DownLDInternal
  259. int DownLDCall ConnectToHost
  260. (
  261.         SOCKET sock,                        //套接字
  262.         char*szHost,                        //主机名
  263.         char*szServ,                        //服务名
  264.         fnErrReporter ErrReport        //报错函数
  265. )
  266. {
  267.         struct addrinfo        Hints;                                //用于解析域名的Hint
  268.         struct addrinfo*pAddrInfoFirst=NULL;//解析取得的域名
  269.         struct addrinfo*pAddrInfo;                        //用于走链表

  270.         //解析域名
  271.         Hints.ai_flags=0;
  272.         Hints.ai_family=AF_INET;
  273.         Hints.ai_socktype=SOCK_STREAM;
  274.         Hints.ai_protocol=IPPROTO_TCP;
  275.         Hints.ai_addrlen=0;
  276.         Hints.ai_canonname=NULL;
  277.         Hints.ai_addr=NULL;
  278.         Hints.ai_next=NULL;
  279.         if(getaddrinfo(szHost,szServ,&Hints,&pAddrInfoFirst))
  280.         {
  281.                 ErrReport("Unable to resolve host name:%s.%s\n",
  282.                         szHost,h_error_String());
  283.                 goto Cleanup;
  284.         }

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

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

  307.         //释放整个链表
  308.         freeaddrinfo(pAddrInfoFirst);
  309.         return 1;
  310. Cleanup:
  311.         if(pAddrInfoFirst)
  312.                 freeaddrinfo(pAddrInfoFirst);
  313.         return 0;
  314. }

  315. //=============================================================================
  316. //函数:SendRequestHeader
  317. //描述:通过SOCKET发送HTTP请求头
  318. //-----------------------------------------------------------------------------
  319. DownLDInternal
  320. int DownLDCall SendRequestHeader
  321. (
  322.         SOCKET sock,                        //套接字
  323.         char*szHost,                        //主机名
  324.         char*szServ,                        //服务名
  325.         char*szPath,                        //请求路径
  326.         int Limited,                        //是否限制大小
  327.         FileSize cbStartByte,        //开始字节
  328.         FileSize cbEndByte,                //结束字节
  329.         fnErrReporter ErrReport        //报告错误的函数
  330. )
  331. {
  332.         char szBuf[MaxSendBuf];        //发送缓冲区

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

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

  349.         //发送HTTP请求包
  350. #        ifdef _DEBUG
  351.         ErrReport("%s",szBuf);
  352. #        endif
  353.         if(send(sock,szBuf,(int)strlen(szBuf),0)==SOCKET_ERROR)
  354.         {
  355.                 ErrReport("Failed to send HTTP header.%s\n",h_error_String());
  356.                 goto Cleanup;
  357.         }
  358.         return 1;
  359. Cleanup:
  360.         return 0;
  361. }

  362. //=============================================================================
  363. //函数:DownFile
  364. //描述:根据指定的URL下载一个文件,返回下载的文件大小。
  365. //-----------------------------------------------------------------------------
  366. DownLDFunc(FileSize,DownFile)
  367. (
  368.         char*szURL,                                //网络资源地址
  369.         int Limited,                        //是否限制大小
  370.         FileSize cbStartByte,        //开始字节
  371.         FileSize cbEndByte,                //结束字节
  372.         fnOnGetSize pfnGetSize,        //取得下载到的数据的大小时调用的函数
  373.         fnOnGetData pfnGetData,        //取得下载到的数据时调用的函数
  374.         fnErrReporter ErrReport,//报告错误的函数
  375.         void*pUserData                        //传递给用户回调函数的用户自定义参数
  376. )
  377. {
  378.         char szBuf[MaxBuf+1];                                //缓冲区
  379.         char szHost[MaxHost];                                //域名
  380.         char szServ[MaxServ];                                //服务名或端口号
  381.         char szPath[MaxPath];                                //路径
  382.         int cbRecv;                                                        //接收到的字节数
  383.         int HeaderFinished=0;                                //是否已读取完HTTP头
  384.         int StatusCode=0;                                        //状态代码(301、404、502等)
  385.         FileSize ContentLength=Size_Unknown;//内容长度
  386.         FileSize ContentRecv=0;                                //接收到的内容长度
  387.         FileSize CurPos=cbStartByte;                //当前文件位置

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

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

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

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

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

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

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

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

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

  447.                         //检查错误代码
  448.                         iErrno=h_errno;
  449.                         if(        iErrno==WSAENOTCONN  ||
  450.                                 iErrno==WSAESHUTDOWN ||
  451.                                 iErrno==WSAENETRESET ||
  452.                                 iErrno==WSAECONNRESET||
  453.                                 iErrno==WSAECONNABORTED)
  454.                         {
  455.                                 ErrReport("The network has been disconnected.%s\n",
  456.                                         h_error_String());
  457.                                 break;
  458.                         }

  459.                         //超时
  460.                         if(iErrno==WSAETIMEDOUT)
  461.                         {
  462.                                 ErrReport("Respond timeout.%s\n",h_error_String());
  463.                                 Retry++;
  464.                                 break;
  465.                         }

  466.                         if(!cbRecv)//零字节包裹
  467.                         {
  468.                                 NbZeroByteRecv++;//记录零字节包裹数
  469.                                 if(NbZeroByteRecv>=MaxZeroByteRecv)
  470.                                 {//如果接收到的0字节包裹数超过容忍度
  471.                                         Retry++;//重连
  472.                                         NbZeroByteRecv=0;//重新统计零字节包裹数
  473.                                         break;
  474.                                 }
  475.                                 continue;//否则继续
  476.                         }
  477.                        
  478.                         //=================================================================
  479.                         //已读取HTTP头,接收文件内容
  480.                         //-----------------------------------------------------------------
  481.                         if(HeaderFinished)
  482.                         {
  483.                                 size_t cbDataLen=cbRecv;
  484.                                 ContentRecv+=cbDataLen;//统计已经下载到的字节数。

  485.                                 if(pfnGetData && cbDataLen)
  486.                                 {//将数据传递给用户
  487.                                         if(!pfnGetData(CurPos,szBuf,cbDataLen,pUserData))
  488.                                         {
  489.                                                 ErrReport("Download aborted.\n");
  490.                                                 goto Cleanup;
  491.                                         }
  492.                                 }
  493.                                 CurPos+=cbDataLen;
  494.                         }
  495.                         //=================================================================
  496.                         //未读取完HTTP头,解析HTTP头
  497.                         //-----------------------------------------------------------------
  498.                         else
  499.                         {
  500.                                 char*pLinePtr;//准备一行一行分析http头
  501.                                 char*pChr;//字符指针
  502.                                 pLinePtr=szBuf;//从缓冲区开头开始
  503.                                 while(pLinePtr && (size_t)pLinePtr<(size_t)szBuf+cbRecv)
  504.                                 {//当行指针并没有超出缓冲区边界
  505.                                         //如果读取到两个换行符,意味着HTTP头结束
  506.                                         if(!strncmp(pLinePtr,"\r\n\r\n",4))
  507.                                         {
  508.                                                 size_t cbDataLen;
  509.                                                 pLinePtr+=4;//跳过两个回车,转到内容

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

  514.                                                 if(pfnGetData && cbDataLen)
  515.                                                 {//将数据传递给用户
  516.                                                         if(!pfnGetData(CurPos,pLinePtr,
  517.                                                                 cbDataLen,pUserData))
  518.                                                         {
  519.                                                                 ErrReport("Download aborted.\n");
  520.                                                                 goto Cleanup;
  521.                                                         }
  522.                                                 }
  523.                                                 CurPos+=cbDataLen;
  524.                                                 break;
  525.                                         }

  526.                                         //跳过行开头的空格、Tab等
  527.                                         while(        *pLinePtr==' '  ||
  528.                                                         *pLinePtr=='\t' ||
  529.                                                         *pLinePtr=='\r' ||
  530.                                                         *pLinePtr=='\n')
  531.                                                 pLinePtr++;

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

  539.                                         //=========================================================
  540.                                         //判断HTTP状态代码
  541.                                         if(!strnicmp(pLinePtr,"HTTP/1.1",8))
  542.                                         {
  543.                                                 //如果是3XX表示已经移动,4XX表示无法访问,5XX则直接放弃
  544.                                                 //"HTTP/1.1"后面可能有空格
  545.                                                 pChr=pLinePtr+8;
  546.                                                 while(        *pChr==' '  ||
  547.                                                                 *pChr=='\t')
  548.                                                         pChr++;
  549.                                                 LineOut(pChr,ErrReport);
  550.                                                 //读取状态代码
  551.                                                 StatusCode=atoi(pChr);
  552.                                                 if(StatusCode>=400)//超过400则放弃下载。
  553.                                                         goto Cleanup;
  554.                                         }else
  555.                                         //=========================================================
  556.                                         //如果是Accept-Ranges域
  557.                                         if(!strnicmp(pLinePtr,"accept-ranges:",14))
  558.                                         {
  559.                                                 //判断是不是"Accept-Ranges:none",它意味着只能从头下载。
  560.                                                 //"Accept-Ranges:"后面可能有空格
  561.                                                 pChr=pLinePtr+14;
  562.                                                 while(        *pChr==' '  ||
  563.                                                                 *pChr=='\t')
  564.                                                         pChr++;
  565.                                                 if(!strnicmp(pChr,"none",4))
  566.                                                         CurPos=0;
  567.                                         }else
  568.                                         //=========================================================
  569.                                         //如果是Content-Length域
  570.                                         if(!strnicmp(pLinePtr,"content-length:",15))
  571.                                         {
  572.                                                 //"Content-Length"域指定了内容的长度
  573.                                                 //"Content-Length:"后面可能有空格
  574.                                                 pChr=pLinePtr+15;
  575.                                                 while(        *pChr==' '  ||
  576.                                                                 *pChr=='\t')
  577.                                                         pChr++;
  578.                                                 ContentLength=(FileSize)_atoi64(pChr);
  579.                                                 if(pfnGetSize)//这个回调函数可以是NULL
  580.                                                 {
  581.                                                         if(!pfnGetSize(ContentLength,pUserData))
  582.                                                         {
  583.                                                                 ErrReport("Download aborted.\n");
  584.                                                                 goto Cleanup;
  585.                                                         }
  586.                                                 }
  587.                                         }else
  588.                                         //=========================================================
  589.                                         //如果是Location:域,并且之前指定了3XX重定向
  590.                                         if(StatusCode>=300 && !strnicmp(pLinePtr,"Location:",9))
  591.                                         {
  592.                                                 //"Location"域指定了跳转的新位置
  593.                                                 //"Location:"后面可能有空格
  594.                                                 pChr=pLinePtr+9;
  595.                                                 while(        *pChr==' '  ||
  596.                                                                 *pChr=='\t')
  597.                                                         pChr++;

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

  609. #                                                ifdef _DEBUG
  610.                                                 ErrReport("\nRedirecting to %s\n",szHost);
  611. #                                                endif
  612.                                                 break;
  613.                                         }

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

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

  624.         return ContentRecv;
  625. Cleanup:
  626.         if(sockClient!=INVALID_SOCKET)
  627.                 closesocket(sockClient);
  628.         return ContentRecv;
  629. }
复制代码
entry.c
  1. //=============================================================================
  2. //作者:0xAA55
  3. //网站:http://0xaa55.com
  4. //版权所有(C) 技术宅的结界
  5. //请保留原作者信息,否则视为侵权
  6. //-----------------------------------------------------------------------------
  7. #include"download.h"
  8. #include<io.h>
  9. #include<stdio.h>
  10. #include<conio.h>
  11. #include<WinSock2.h>

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

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

  37. //=============================================================================
  38. //函数类型:OnGetData
  39. //描述:取得下载到的数据时调用的函数
  40. //-----------------------------------------------------------------------------
  41. int DownLDCBCall OnGetData
  42. (
  43.         FileSize        Position,        //位置
  44.         void                *pData,                //数据指针
  45.         size_t                cbData,                //数据大小
  46.         void                *pUserData        //用户自定义参数
  47. )
  48. {
  49.         FILE*fp;

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

  51.         fp=fopen((const char*)pUserData,"ab");
  52.         if(fp)
  53.         {
  54.                 fwrite(pData,1,cbData,fp);
  55.                 fclose(fp);
  56.         }

  57.         return 1;
  58. }

  59. char*h_error_String();

  60. //=============================================================================
  61. //函数:main
  62. //描述:程序入口点
  63. //-----------------------------------------------------------------------------
  64. int main(int argc,char**argv)
  65. {
  66.         WSADATA wsaData;
  67.         int Limited=0;
  68.         size_t StartByte=0;
  69.         size_t EndByte=0;

  70.         //至少两个参数:URL和存放的文件位置
  71.         if(argc<3)
  72.         {
  73.                 Usage(argc?argv[0]:"HTTPDOWN");
  74.                 return 1;
  75.         }

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

  86.         //先删除已经存在的文件。
  87.         if(!_access(argv[2],0))
  88.                 unlink(argv[2]);
  89.        
  90.         //初始化Winsock
  91.         if(WSAStartup(WINSOCK_VERSION,&wsaData))
  92.                 goto Cleanup;
  93.        
  94.         //下载并打印字节数
  95.         fprintf(stderr,"%u bytes downloaded.\n",
  96.                 DownFile(argv[1],Limited,StartByte,EndByte,OnGetSize,OnGetData,DefErrReport,argv[2]));
  97.        
  98.         WSACleanup();
  99.         return 0;
  100. Cleanup:
  101.         fprintf(stderr,"%s\n",h_error_String());
  102.         WSACleanup();
  103.         return 2;
  104. }


  105. //=============================================================================
  106. //函数:h_error_String
  107. //描述:打印h_errno的内容
  108. //-----------------------------------------------------------------------------
  109. char*h_error_String()
  110. {
  111.         switch(h_errno)
  112.         {
  113.         case WSAEACCES:
  114.                 return"WSAEACCES";
  115.         case WSAEADDRINUSE:
  116.                 return"WSAEADDRINUSE";
  117.         case WSAEADDRNOTAVAIL:
  118.                 return"WSAEADDRNOTAVAIL";
  119.         case WSAEAFNOSUPPORT:
  120.                 return"WSAEAFNOSUPPORT";
  121.         case WSAEALREADY:
  122.                 return"WSAEALREADY";
  123.         case WSAECONNABORTED:
  124.                 return"WSAECONNABORTED";
  125.         case WSAECONNREFUSED:
  126.                 return"WSAECONNREFUSED";
  127.         case WSAECONNRESET:
  128.                 return"WSAECONNRESET";
  129.         case WSAEDESTADDRREQ:
  130.                 return"WSAEDESTADDRREQ";
  131.         case WSAEFAULT:
  132.                 return"WSAEFAULT";
  133.         case WSAEHOSTDOWN:
  134.                 return"WSAEHOSTDOWN";
  135.         case WSAEHOSTUNREACH:
  136.                 return"WSAEHOSTUNREACH";
  137.         case WSAEINPROGRESS:
  138.                 return"WSAEINPROGRESS";
  139.         case WSAEINTR:
  140.                 return"WSAEINTR";
  141.         case WSAEINVAL:
  142.                 return"WSAEINVAL";
  143.         case WSAEISCONN:
  144.                 return"WSAEISCONN";
  145.         case WSAEMFILE:
  146.                 return"WSAEMFILE";
  147.         case WSAEMSGSIZE:
  148.                 return"WSAEMSGSIZE";
  149.         case WSAENETDOWN:
  150.                 return"WSAENETDOWN";
  151.         case WSAENETRESET:
  152.                 return"WSAENETRESET";
  153.         case WSAENETUNREACH:
  154.                 return"WSAENETUNREACH";
  155.         case WSAENOBUFS:
  156.                 return"WSAENOBUFS";
  157.         case WSAENOPROTOOPT:
  158.                 return"WSAENOPROTOOPT";
  159.         case WSAENOTCONN:
  160.                 return"WSAENOTCONN";
  161.         case WSAENOTSOCK:
  162.                 return"WSAENOTSOCK";
  163.         case WSAEOPNOTSUPP:
  164.                 return"WSAEOPNOTSUPP";
  165.         case WSAEPFNOSUPPORT:
  166.                 return"WSAEPFNOSUPPORT";
  167.         case WSAEPROCLIM:
  168.                 return"WSAEPROCLIM";
  169.         case WSAEPROTONOSUPPORT:
  170.                 return"WSAEPROTONOSUPPORT";
  171.         case WSAEPROTOTYPE:
  172.                 return"WSAEPROTOTYPE";
  173.         case WSAESHUTDOWN:
  174.                 return"WSAESHUTDOWN";
  175.         case WSAESOCKTNOSUPPORT:
  176.                 return"WSAESOCKTNOSUPPORT";
  177.         case WSAETIMEDOUT:
  178.                 return"WSAETIMEDOUT";
  179.         case WSATYPE_NOT_FOUND:
  180.                 return"WSATYPE_NOT_FOUND";
  181.         case WSAEWOULDBLOCK:
  182.                 return"WSAEWOULDBLOCK";
  183.         case WSAHOST_NOT_FOUND:
  184.                 return"WSAHOST_NOT_FOUND";
  185.         case WSAEINVALIDPROCTABLE:
  186.                 return"WSAEINVALIDPROCTABLE";
  187.         case WSAEINVALIDPROVIDER:
  188.                 return"WSAEINVALIDPROVIDER";
  189.         case WSANOTINITIALISED:
  190.                 return"WSANOTINITIALISED";
  191.         case WSANO_DATA:
  192.                 return"WSANO_DATA";
  193.         case WSANO_RECOVERY:
  194.                 return"WSANO_RECOVERY";
  195.         case WSAEPROVIDERFAILEDINIT:
  196.                 return"WSAEPROVIDERFAILEDINIT";
  197.         case WSASYSCALLFAILURE:
  198.                 return"WSASYSCALLFAILURE";
  199.         case WSASYSNOTREADY:
  200.                 return"WSASYSNOTREADY";
  201.         case WSATRY_AGAIN:
  202.                 return"WSATRY_AGAIN";
  203.         case WSAVERNOTSUPPORTED:
  204.                 return"WSAVERNOTSUPPORTED";
  205.         case WSAEDISCON:
  206.                 return"WSAEDISCON";
  207.         default:
  208.                 return"";
  209.         }
  210. }
复制代码
BIN下载: HttpDown.exe (87.5 KB, 下载次数: 43)
游客,如果您要查看本帖隐藏内容请回复

本帖被以下淘专辑推荐:

回复

使用道具 举报

发表于 2014-12-10 21:10:47 | 显示全部楼层
会用fiddler就可以写很多小东西了,比如自动登录什么的
回复 赞! 靠!

使用道具 举报

 楼主| 发表于 2014-12-11 00:47:03 | 显示全部楼层
内容已经更新,现在可以下载百度的内容了。其实百度的服务器会单独发送HTTP头和内容。。。很奇葩。估计是百度自己弄的http服务程序,而不是Apache的httpd或MS的IIS。
回复 赞! 靠!

使用道具 举报

 楼主| 发表于 2014-12-11 20:30:15 | 显示全部楼层
内容再次更新。现在可以下载经过3XX重定向的内容了。
回复 赞! 靠!

使用道具 举报

发表于 2015-5-27 17:04:16 | 显示全部楼层
终于找到HTTP用C语言实现下载的文章啦!!!
回复 赞! 靠!

使用道具 举报

发表于 2015-6-6 16:57:21 | 显示全部楼层
到现在才知道原来有fiddler这种神器....
回复 赞! 靠!

使用道具 举报

发表于 2015-6-23 10:15:09 | 显示全部楼层
谢谢分享  正好需要这个
回复 赞! 靠!

使用道具 举报

发表于 2015-12-14 17:04:20 | 显示全部楼层
好东西啊,来看看,顶起~
回复 赞! 靠!

使用道具 举报

发表于 2016-1-17 21:46:01 | 显示全部楼层
谢谢分享,来一个试试。。。
回复 赞! 靠!

使用道具 举报

发表于 2016-2-21 14:51:10 | 显示全部楼层
谢谢,学习中
回复 赞! 靠!

使用道具 举报

发表于 2016-2-26 08:46:35 | 显示全部楼层
捣鼓下来看看~
回复 赞! 靠!

使用道具 举报

发表于 2016-8-24 21:20:00 | 显示全部楼层
新人学习一下
回复 赞! 靠!

使用道具 举报

发表于 2016-8-24 21:20:18 | 显示全部楼层
#在这里快速回复#新人学习一下
回复 赞! 靠!

使用道具 举报

发表于 2016-11-17 10:33:51 | 显示全部楼层
支持    !!
回复 赞! 靠!

使用道具 举报

发表于 2016-11-23 19:18:18 | 显示全部楼层
xieh tahnyoufasd
回复 赞! 靠!

使用道具 举报

发表于 2016-11-25 13:24:51 | 显示全部楼层
谢谢楼主分享
回复 赞! 靠!

使用道具 举报

发表于 2016-11-25 13:26:36 | 显示全部楼层
谢谢楼主分享
回复 赞! 靠!

使用道具 举报

发表于 2017-4-19 08:48:37 | 显示全部楼层
不错不错
回复

使用道具 举报

发表于 2017-4-21 17:06:12 | 显示全部楼层
很好的技术文章。
回复 赞! 靠!

使用道具 举报

发表于 2017-5-2 22:13:20 | 显示全部楼层
网络学者,前来拜访
回复 赞! 靠!

使用道具 举报

本版积分规则

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-11-22 06:58 , Processed in 0.045718 second(s), 30 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表