HTTP文件下载始末及简单实现

        每个人在人生中第一次打开网页时总会迷惑于以http开头的一串字符,网站名很好理解,就好像是门牌号一样,而http://这样的七个字符看起来好像总是那么多余。而所有的浏览器都可以不需要输入那七个字符即可顺利打开网页,当然,事情总是并非表面上看起来那样无足轻重,探究一下事物的始末会发现平静的外表下是无数翻滚的01

        在进入正式的分析前先插入一个前些天的事件。起因是有位小朋友做FTP上传功能,用的是第三方一个现成的类,在传输方法完成之后一段时间内服务器上的文件都还在持续的增长中,直至过了一段时间才完成。他苦恼的是无法判断文件是否传完,我说那就在上传调用结束后定时探测服务器文件长度,一旦和本地文件长度一致就可以认为传完了。小朋友说类里没有这个方法,我告诉他说可以通过FTP协议命令取得,他嗯了一下。过了一会再问我:难道FTP文件可以不用下载到本地就取得长度吗?我忽然明白小朋友最大的问题在于对FTP协议的陌生,所以过分依赖于现有的库或类,我建议他花点时间熟悉一下FTP协议,掌握几个相关的命令就可以实现了。不知道他后来有没有照我说的去做,但我想,他若能尽早了解典型如http此类应用层协议的工作方式,对其他协议的原理就不会那么茫然了。

        如果以一句简单的话来描述浏览器打开页面的原理,那就是通过socketBSDPOSIX)网络通信方式从WEB SERVER的指定端口(常用80端口)将文件或数据下载至本地,再由浏览器负责解析并呈现。所以无论如何,第一件要做的事情就是把文件或数据依据HTTP协议下载至本地。HTTP协议的英文全称是hyper text transfer protocol,用中文翻着就是超文本传输协议。不必在意其抽象的称谓,在了解一些简单的事实之后会发现http其实是很平易近人的。

        为了讨论的方便,将以静态文件下载说明,而不掺和动态语言脚本phpjspaspxcgi之类的东东了,这些东东的出现其实也是因为当初定义http的那帮大神们没有想到会有这样的应用要求,于是就有了后来的各种动态脚本语言的群魔乱舞了。此处的讨论需要网络通信的基础,所以先对网络通信的基本方法进行说明,而使用的开发语言则是以标准C为摹画。假设我们现在已经配置好一个web server,该server使用80默认端口,我们知道在server上有一个index.html文件,现在要把它下载到本地来。

        若以人类现实行为进行比拟的话,则是首先要向组织提交申请,组织发放一个许可,这个许可就是一个socket句柄:int sock = socket();。获得此许可意味着可以进行下面你想要的操作了,但若组织不予通过,则一定要退出,否则后果自负。第二步是设置被访问的远程主机地址及端口号,这就是说在你出门前先确定要去的地址及门牌号。第三步是建立连接并发送HTTP下载请求,连接是connect()方法,基于TCP协议的连接方式,与之相对的则是UDP,简单来说前者称之为可靠传输,意即发送端会保证接收端按顺序接收完成,后者数据报文协议也称为不可靠连接协议,发送者在发送之后不负责接收端的完整有序接收。第四步是使用recv()方法循环接收数据,因为一个较大的文件一般不会被一次性读到内存中送入发送方法,因为那样非常占用资源,更普遍的做法是多次、分段读取文件,循环送入发送方法。

还是让代码说话吧:

int GetFile(const char *szip, int nPort, const char *szSave)

{

        int nret = -1;

        SOCKET sock = 0;


        do

        {

                sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 向组织申请通行证

                if ( sock < 0 ) 

                {

                        printf ( "fail socket\n" );

                        break;

                }

                struct sockaddr_in servAddr;

                memset(&servAddr, 0, sizeof(struct sockaddr_in));

                servAddr.sin_family = AF_INET;

                struct hostent *site = NULL;

                

                site = gethostbyname(szip);

                if (site == NULL)

                {

                        printf ( "fail get ip\n" );

                        break;

                }

  //出门前确定好门牌号

                memcpy(&servAddr.sin_addr, site->h_addr_list[0], site->h_length);

                servAddr.sin_family = AF_INET;

                servAddr.sin_port = htons(client.nPort);

  //连接远程服务端

                if (connect(sock, (struct sockaddr *)&servAddr, sizeof(servAddr)) < 0)

                {

                        printf ( "fail connect\n" );

                        break;

                }

                //发送下载请求,此函数实现在后面给出

                if ( Request((SOCKET)sock, client) < 0)

                {

                        break;

                }

                //将数据保存至文件,实现在后面给出

                nret = SaveFile(sock, szSave);

        }while(0);

 //特别注意:一定要有始有终,用完了,记得要还给组织,不然,还是后果自负。。。

        close(sock);        

return nret;

}

        以上代码段其实更多的是一个基于TCP方式的通用客户端代码框架,也就是说只要服务器是在TCP端口守候的话,就可以这么着初始化、设置、连接、发送数据了。我用的是标准C实现,习惯于其他面向对象语言的同学们一定会念念不忘try--catch机制,而在C中若使用颇有争议的goto必会让学院派们口诛笔伐。所以有一个巧妙的办法,即是do{...}while(0);方式,如果只是单纯看,这仿佛只是一句废话,但它的妙用就在于程序中若出现异常情况时只一句break即可跳出循环,而转到最终清理代码中,而程序运行一切正常时则自然退出此循环。

接下来的Request方法则是特定于HTTP协议的实现。

int Request(SOCKET sock, const char *szFile, const char *szAddress)

{

        int nret = -1;

        char szbuf[1024] = {0};

        sprintf(szbuf,  "GET %s HTTP/1.1\r\n"

                        "Accept: */*\r\n"

                        "Accept-Language: zh-cn\r\n"

                        "User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)\r\n"

                        "Host: %s\r\n"

                        "Connection: Close\r\n\r\n", szFile, szAddress);

        

        nret = send(sock, szbuf, (int)strlen(szbuf), 0);

        return nret;

}


 

        都说HTTP是超文本语言,在这个头里终于体现出了文本的意义。没错,这个HTTP头是以文本格式封装的,每行后面的"\r\n"是什么?就是回车换行,我十分怀疑制定HTTP协议的那帮大神们(又提到了他们)当初就是习惯了在命令行下工作,喜欢敲一行命令就回车,然后接着输入下一个命令,把这几个命令给组合完了就叭叭敲两下回车算是提交这个任务了。。。(纯属个人臆测,如有雷同,实属巧合)。当然,头部信息的内容不止上文代码中那些,但第一个大写的GET即是说要把东西取回来,文件名紧跟在GET之后。其他的参数则是说协议版本、本地语言、主机地址、是否保持连接等。Web Server在收到这样一个文本字符串之后即解析请求内容将文件发回客户端。在此要说明另一个关键命令POST,大概有不少做了NWEB开发的同学们并不真的能说清GETPOST的区别,反正参数不长的就用GET方法提交,参数内容太长的就用POST方法提交。现在我们知道了,POST是发送,它与GET相对,一个是下载,一个上传,就这么简单。而GET之所以不能在?后面的参数写太长,是因为它是放在HTTP头部里提交,这个长度是有限制的,超出了这个限制自然会认为请求出错。而POST发送时要在头部中写明待上传数据字节数,并且上传数据跟在两个回车换行之后,所以再长的数据也能发送上去。现在想想大神们有时候想法也是挺简单的,但人家厉害就在把复杂的事情简单化,我们凡人恐怕简单的事情也要复杂化。

下面是从HTTP返回的数据中保存为文件。

#define  MU_PACK_LEN  32768

int SaveFile(SOCKET sock, const char *szfile)

{

        char *ptr = NULL;

        char buf[MU_PACK_LEN] = {0};

        int  nlen = 0;

        int  nheadlen = 0;

        int  nfilelen = 0;

        int  nputlen  = 0;

        int  ndownlen = 0;

        const char szHeadTag[] = "\r\n\r\n";//传说中的两下回车,文件数据是跟在这个标识之后。

        FILE *fp = NULL;

        int nret = -1;

        do

        {

  //接收服务端返回数据

                nlen = recv(sock, buf, MU_PACK_LEN, 0); 

                nputlen = nlen;

                ptr = buf;

                if (nheadlen <= 0)

                {

      //下面的内容是说文件数据有没有和头部数据放一块返回,据我的测试大部分Web server都是放一块返回,但仍然还有server是先返回头部数据,再发送文件数据。

                        ptr = strstr(buf, szHeadTag);

                        if (ptr)

                        {

                                ptr += strlen(szHeadTag);

                                nheadlen = (int)(ptr - buf);

//从HTTP头部中取得待下载文件长度,所以,如果你仅是想知道一个在HTTP服务器上的文件长度是多少的话,取完了这个值你就可以直接关闭socket了。代码在后面给出。

                                nfilelen = GetFileLenFromHead(buf); 

                                nputlen  = nlen - nheadlen;

                                if (nheadlen == nlen && nfilelen > 0)

                                {

                                        continue;

                                }

                        }

                }

                if ( !fp && nfilelen > 0)

                {

                        fp = fopen(szfile, "wb");

                }

  //来一段数据就写一段到文件中

                if (nlen > 0 && nfilelen > 0)

                {

                        fwrite(ptr, nputlen, 1, fp);

                        memset(buf, 0, MU_PACK_LEN);

                        ndownlen += nputlen;

                }

                else

                {

                        break;

                }

                

        }while(nlen > 0);

        if (fp)

        {

                fclose(fp);

        }

        if (ndownlen == nfilelen && nfilelen > 0)

        {

                nret = 0;

        }

        else

        {

                cout << "down len: " << ndownlen << endl;

                cout << "file len: " << nfilelen << endl;

        }

        

        return nret;

}

取得文件长度功能。

int  GetFileLenFromHead(const char *phead)

{

        char *plen = NULL;

        int nret = -1;

        const char szLenTag[]  = "Content-Length: "; //文本标识,在这个冒号后面的是一个十进制的数字,以一个回车换行为结束。

        int i = 0;

        char szvalue[32] = {0};

        plen = strstr(phead, szLenTag);

        if (plen)

        {

                plen += strlen(szLenTag);

                while(plen[i] != '\r')

                {

                        szvalue[i] = plen[i];

                        i++;

                }

               nret = atoi(szvalue);

        }

        return nret;

}

        以上代码只是一个简单实现,工程应用中还有很多细节需要注意,服务器返回的头部文本在此并未展示,只是给同学们一个悬念吧,有兴趣的话看看那到底是一串什么东东。(我能提点意见么,这个发布文章的编辑区也太不好用了吧,行首按下TAB键整段漂移。。不知道预览功能在哪,一定要发布以后才能看到整个文章的排版。最假的还是登录机制,我先是在IE上,退出以后发现排版不对想重新登录,输入密码后倒是通过了,可进入博客却怎么都是未登录状态,换一个浏览器chrome又可以正常登录,疯狂的崩溃中,这就是程序员做的网站么,这不得让那一大帮子大牛们笑翻滚了么。。。)

你可能感兴趣的:(socket,http服务器,struct,语言,浏览器,server)