断点续传——据说很多人感兴趣

北京理工大学  20981  陈罡
要做手机断点续传了,网上的代码有很多,但是多数要么是过于复杂,要么是用java,pascal之类的语言编写的。都不适合直接用在手机上,无奈之下我这个懒人开始动手自己写了。
 
(1)手机断点续传的未来?
手机上开发应用程序的时候,或多或少都要用到gprs连接互联网,从互联网上的服务器中把数据取出来,然后存储到手机上,利用专门的客户端来查看。这就可以美其名曰“在线更新”了。随着智能手机的处理能力越来越强以及gprs升级在即(也就是传说中的2.5G或3G了),手机的网络应用更加惹人注意,尤其是在RSS手机新闻组、手机mail下载大附件、手机电视实时缓冲视频流、在或者在线听mp3、下载图铃之类的3G手机网络应用上,是否具有断点续传的功能尤其重要。这项技术还将发展相当长的一段时间(除非移动把什么cmwap,cmnet都统一了,目前还没有看到有统一的迹象)。
 
(2)手机断点续传的实质?
手机上的应用越花哨就必然对应着需要下载的数据文件就会越大。目前绝大多数的手机浏览器,都支持gprs下载功能,所不同的是,绝大多数都没有断点续传的功能。比如你要下载一首几百K的mp3,下载到2/3的时候,突然进了地铁或者信号不好,断开了,那就意味着刚刚的那2/3已经浪费了。再次下载的时候,就需要重新下载了。断点续传这个技术就是用来解决这个问题的,它的实质就是如果程序开始准备重新下载的时候,先检查一下,已经下载了多少了,然后再接着刚刚下载过的地方继续,接着下载。
 
(3)传统断点续传的原理?
首先,断点续传不是什么高深的技术,它是标准的http协议中已经定义了很久的东西;其次,需要服务器支持,我这边使用的是apache的服务器,对断点续传支持得很好。
 
其实断点续传的原理很简单,就是在http的请求上和一般的下载有所不同而已。
假设服务器域名为
www.5mbox.com ,文件名为/bbs/mp1.mp3
当web浏览器请求从服务器上的一个文时,所发出的请求如下:
GET /bbs/mp1.mp3 HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-
excel, application/msword, application/vnd.ms-powerpoint, */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)
Connection: Keep-Alive

服务器收到请求以后,会回应如下内容:
200
Content-Length=106786028
Accept-Ranges=bytes
Date=Mon, 30 Apr 2001 12:56:11 GMT
ETag=W/"02ca57e173c11:95b"
Content-Type=application/octet-stream
Server=Microsoft-IIS/5.0
Last-Modified=Mon, 30 Apr 2001 12:56:11 GMT
...后面跟着就是数据了。
 
断点续传,也就是要从文件已经下载的地方开始继续下载。所以在客户端浏览器传给
web服务器的时候要多加一条信息--从哪里开始。
下面是用自己编的一个"浏览器"来传递请求信息给web服务器,要求从2000070字节开始。
GET /bbs/mp1.mp3 HTTP/1.0
User-Agent: NetFox
RANGE: bytes=2000070-
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2

仔细看一下就会发现多了一行RANGE: bytes=2000070-
这一行的意思就是告诉服务器mp1.mp3这个文件从2000070字节开始传,前面的字节不用传了。
服务器收到这个请求以后,返回的信息如下:
206
Content-Length=106786028
Content-Range=bytes 2000070-106786027/106786028
Date=Mon, 30 Apr 2001 12:55:20 GMT
ETag=W/"02ca57e173c11:95b"
Content-Type=application/octet-stream
Server=Microsoft-IIS/5.0
Last-Modified=Mon, 30 Apr 2001 12:55:20 GMT
。。。二进制数据

和前面服务器返回的信息比较一下,就会发现增加了一行:
Content-Range=bytes 2000070-106786027/106786028
返回的代码也改为206了,而不再是200了。

知道了以上原理,就可以进行断点续传的编程了。
先写到这里,下一篇才是实现部分。

我们继续上一篇的内容,需要了解原理的朋友可以参照“断点续传(1)”所描述的部分。
 
(4)断点续传的实现
懒得一个个解释,直接上代码了(请注意,下面的代码是在win32上做验证用的,不是给手机用的),
采用c++编写,dev-c++ 4.9.9.2编译的,我相信g++都行vs系列自然不在话下:
 
M5HttpDown.h文件的内容:
#ifndef _M5_HTTP_DOWN_H_
#include <process.h>
#include <wininet.h>
#include <winsock2.h>
#include <stdio.h>
#define HTTP_DEBUG_MODE 1
 
#define HTTP_WEB_PORT            80
#define HTTP_TEMP_BUF_LEN        120
#define HTTP_SEND_BUF_LEN        256
#define HTTP_RECV_BUF_LEN        4096
#define HTTP_HDR_OK              "200 OK"
#define HTTP_HDR_FILE_LEN        "Content-Length: "
#define HTTP_HDR_DIV             "/r/n"
#define HTTP_HDR_END             "/r/n/r/n"
#define HTTP_PREFIX              "http://"
#define HTTPS_PREFIX             "https://"
 
// 这里分成了两个get字符串的定义,主要是为了兼容普通的http下载
// 以及支持断点续传的http下载
#define HTTP_COMMON_GET "GET /%s HTTP/1.1/r/n/
User-Agent: Opera 8.0/r/n/
Host: %s:%d/r/nAccept: *//
*/r/nConnection: Keep-Alive/r/n/r/n"
 
#define HTTP_RESUME_GET "GET /%s HTTP/1.1/r/n/
User-Agent: Opera 8.0/r/n/
Host: %s:%d/r/nAccept: *//
*/r/nRANGE: bytes=%d-/r/n/
Connection: Keep-Alive/r/n/r/n"
 
// 这里为了方便起见,就没有用什么notifier或者虚拟函数之类的东西了,直接回调
// recv_buf:里面装着二进制数据,就是要下载的文件中的数据部分
// recv_len:数据的长度
// data:既然是回调函数,就需要允许caller把相关的数据结构也带进来。
typedef void (*RECV_CALLBACK)(char * recv_buf, int recv_len, void * data) ;

class CM5HttpDown {
protected:
    // socket data
    SOCKET        m_sock ;    
    bool          m_running ;  // 标志是否运行
    bool          m_is_first_resp ;  // 第一次收到数据的标志,用于跳过服务器的http头
    char *        m_web_addr ; // 存放从uri中解析出来的网址
    char *        m_web_fname ; // 存放uri中的文件名
    int           m_web_port ;  // uri中的服务器端口好,缺省值是80
    char *        m_recv_buf ;  // 接收缓冲区
    int           m_total_bytes ; // uri中文件的总大小,单位字节
    int           m_recv_bytes;   // 已经接收了多少字节,用于断点续传中接着传
   
    // custom defined receive handler 
    RECV_CALLBACK m_custom_callback ; // 回调函数指针
    void *        m_custom_data ; // call的自定义数据结构指针
public:
    // common receive thread func
    static void recv_thread(void * data) ; // 线程函数,必须是静态的
    void recv_thread_handler() ; // 在线程函数中调用,是实际上的接收函数
   
protected:
    // uri解析函数,能够把诸如 http://www.5mbox.com/bbs/mp1.mp3 的uri分解为
    // web_addr : www.5mbox.com
    // web_fname : bbs/mp1.mp3
    bool parse_uri(char * uri,
                   char * web_addr, char * web_fname, int * web_port) ; 
    
    // 第一次收到http回应的时候,解析出来文件的大小(toal_length),以及需要跳过的长度
    // (jumplen),这样就可以只把有用数据给call传过去了,而无用的http头就丢弃了。
    bool parse_webfile_info(char * recv_buf,
                            int * total_length, int * jump_len) ; 
    
    // 用于把指定的field的值字符串从http头中读取出来,例如回应http的头为:
    // 206
    // Content-Length: 106786028
    // Content-Range: bytes 2000070-106786027/106786028
    // Date=Mon, 30 Apr 2001 12:55:20 GMT
    // ETag=W/"02ca57e173c11:95b"
    // Content-Type: application/octet-stream
    // Server=Microsoft-IIS/5.0
    // Last-Modified=Mon, 30 Apr 2001 12:55:20 GMT
    // 该函数就可以把比方说"Content-Length: "的数据"106786028"给取出来
    bool get_resp_field(char * recv_buf,
                        char * field_name, char * end_flag, char * res) ; 
    
    // socket的常规操作函数
    bool init_sock(char * server_name, int server_port) ;
    bool send_req(char * req_str, int req_len) ;
    bool close_sock() ;
public:
    CM5HttpDown() ;
    ~CM5HttpDown() ;
   
    bool is_running() {return m_running ; }
    int  http_total_size() { return m_total_bytes ; }
    int  http_recv_size() { return m_recv_bytes ; }
 
    // 这个就是主要的http下载函数了,使用的时候直接调用这个函数就行了。
    // uri : 要下载的web文件地址,例如 http://www.5mbox.com/bbs/mp1.mp3
    // custom_func : 回调函数,收到二进制数据的时候,会自动调用该函数
    // custom_data : call的自定义数据类型
    // recv_bytes : 已经接收了多少个字节的数据,用于断点续传。如果该数值为0
    // 则采用普通的get方法下载;如果不为0,则采用断点续传接着下载
    bool http_down(char * uri,
                   RECV_CALLBACK custom_func, void * custom_data,
                   int recv_bytes = 0) ;
 
    // 下载过程中强制关闭下载用的
    bool http_stop() ;
} ;
 
#endif
 
M5HttpDown.cpp的文件内容:(作家侯杰曾经说过,源码之下了无秘密,不需要我解释什么了吧。。。)
#include "M5HttpDown.h"
#include <string.h>
CM5HttpDown::CM5HttpDown()
{
    m_sock = (SOCKET)(NULL) ;
    m_running = false ;
    m_is_first_resp = true ;
    m_web_addr = NULL ;
    m_web_fname = NULL ;
    m_custom_callback = NULL ;
    m_custom_data = NULL ;
    m_total_bytes = 0 ;
    m_recv_bytes = 0 ;
    m_recv_buf = new char [HTTP_RECV_BUF_LEN] ;
    memset(m_recv_buf, 0, HTTP_RECV_BUF_LEN) ; 
}
CM5HttpDown::~CM5HttpDown()
{
    if(m_recv_buf) delete [] m_recv_buf ;
    if(m_web_addr) delete [] m_web_addr ;
    if(m_web_fname) delete [] m_web_fname ;
}
void CM5HttpDown::recv_thread(void * data)
{
    CM5HttpDown * http_down_ptr = static_cast<CM5HttpDown *>(data) ;
    http_down_ptr->recv_thread_handler() ; 
    _endthread() ;    
}
void CM5HttpDown::recv_thread_handler()
{
    fd_set recv_fd ;
    struct timeval tmv ;
    int    recv_bytes ;
    int    jump_length ;
   
    while(m_running) {
        FD_ZERO(&recv_fd) ;
        FD_CLR(m_sock, &recv_fd) ;
        FD_SET(m_sock, &recv_fd) ;
        tmv.tv_sec = 1 ;
        tmv.tv_usec = 0 ;
        if(select(m_sock+1, &recv_fd, NULL, NULL, &tmv) < 0) {
#ifdef HTTP_DEBUG_MODE
            printf("select recv failed !/n") ;
            fflush(stdout) ;
#endif
            return ;
        }
       
        if(FD_ISSET(m_sock, &recv_fd)) {
            // time to read
            recv_bytes = 0 ;
            jump_length = 0 ;
            memset(m_recv_buf, 0, HTTP_RECV_BUF_LEN) ;
            recv_bytes = recv(m_sock, m_recv_buf, HTTP_RECV_BUF_LEN, 0) ;
            if(recv_bytes > 0) {
                if(m_is_first_resp) {
                    if(parse_webfile_info(m_recv_buf, &m_total_bytes, &jump_length)) {
                        // 这里比较乱,意思是:如果是断点续传的话,第一次收到response
                        // 的时候,m_recv_bytes就有数据,此时整个文件的大小应该是
                        // 服务器返回的content-length长度加上已经接收过了的数据长度
                        if(m_recv_bytes > 0) m_total_bytes += m_recv_bytes ;
#ifdef HTTP_DEBUG_MODE
                        printf("file length : %d/n", m_total_bytes) ;
#endif
                        m_recv_bytes += (recv_bytes - jump_length) ; 
                        (*m_custom_callback)(m_recv_buf + jump_length,
                                             recv_bytes - jump_length,
                                             m_custom_data) ;
                    }
                    m_is_first_resp = false ;
                    continue ;
                } else {
                    // common receive procdure
                    if((m_recv_bytes + recv_bytes) > m_total_bytes) {
                        recv_bytes = m_total_bytes - m_recv_bytes ;
                        m_recv_bytes = m_total_bytes ;
                    } else {
                        m_recv_bytes += recv_bytes ;   
                    }
                    (*m_custom_callback)(m_recv_buf, recv_bytes, m_custom_data) ;
                }
            } else if(recv_bytes == 0) {
                // conn down
#ifdef HTTP_DEBUG_MODE
                printf("disconn.../n") ;
#endif
                m_running = false ;
            }
        }
    }
}
bool CM5HttpDown::send_req(char * req_str, int req_len)
{
    fd_set send_fd ;
    struct timeval tmv ;
    int    send_bytes ;
    if(!m_sock || req_len <= 0 || req_str == NULL) return false ;
   
    FD_ZERO(&send_fd) ;
    FD_CLR(m_sock, &send_fd) ;
    FD_SET(m_sock, &send_fd) ;
    tmv.tv_sec = 1 ;
    tmv.tv_usec = 0 ;
    if(select(m_sock+1, NULL, &send_fd, NULL, &tmv) < 0) {
#ifdef HTTP_DEBUG_MODE
        printf("select send failed !/n") ;
        fflush(stdout) ;
#endif
        return false ;
    }
   
    if(FD_ISSET(m_sock, &send_fd)) {
        send_bytes = send(m_sock, req_str, req_len, 0) ;
        if(req_len != send_bytes) return false ;
        return true ;
    }
    return false  ;
}
bool CM5HttpDown::parse_uri(char * uri, char * web_addr,
                            char * web_fname, int * web_port)
{
    char * ptr_a = NULL ;
    char * ptr_b = NULL ;
   
    *web_port = HTTP_WEB_PORT ;
    if(!uri) return false ;
    // search for http or https prefix
    ptr_a = uri ;
    if(!strncmp(ptr_a, HTTP_PREFIX, strlen(HTTP_PREFIX)))
        ptr_a = uri + strlen(HTTP_PREFIX) ;
    else if(!strncmp(ptr_a, HTTPS_PREFIX, strlen(HTTPS_PREFIX))) 
        ptr_a = uri + strlen(HTTPS_PREFIX) ;
    // get web_addr without "http://" or "https://" prefix
    ptr_b = strchr(ptr_a, '/');
    if(ptr_b) {
        memcpy(web_addr, ptr_a, strlen(ptr_a) - strlen(ptr_b));
        if(ptr_b + 1)  {
            // get web file name
            memcpy(web_fname, ptr_b + 1, strlen(ptr_b) - 1);
            web_fname[strlen(ptr_b) - 1] = '/0' ;
        }
    } else  memcpy(web_addr, ptr_a, strlen(ptr_a)) ;
    if(ptr_b)  web_addr[strlen(ptr_a) - strlen(ptr_b)] = '/0' ;
    else  web_addr[strlen(ptr_a)] = '/0' ;
    // search for uri port number
    ptr_a = strchr(web_addr, ':') ;
    if(ptr_a)  *web_port = atoi(ptr_a + 1);
    else *web_port = HTTP_WEB_PORT ;
    return true ;
}
bool CM5HttpDown::get_resp_field(char * recv_buf, char * field_name, char * end_flag, char * res)
{
  char * start_ptr = NULL ;
  char * end_ptr = NULL ;
 
  start_ptr = strstr(recv_buf, field_name) ;
  if(start_ptr == NULL) return false ;
 
  start_ptr += strlen(field_name) ;
  end_ptr = strstr(start_ptr, end_flag) ;
 
  if(end_ptr == NULL) return false ;
  memcpy(res, start_ptr, end_ptr - start_ptr) ;
  res[end_ptr - start_ptr] = '/0' ;
  return true ;
}
bool CM5HttpDown::parse_webfile_info(char * recv_buf, int * file_length, int * jump_len)
{
    char   tmp_str[50] ;
    char * offset_str = NULL ;
 
#ifdef HTTP_DEBUG_MODE
    printf("%s/n", recv_buf) ;
#endif
    // get file length
    if(!get_resp_field(recv_buf, HTTP_HDR_FILE_LEN, HTTP_HDR_DIV, tmp_str))
        return false ;
   
    *file_length = atoi(tmp_str) ;
 
    // get current offset
    offset_str = strstr(recv_buf, HTTP_HDR_END) ;
    if(offset_str == NULL) return false ;
    *jump_len = (int)(offset_str + strlen(HTTP_HDR_END) - recv_buf) ;
    return true ;
}
bool CM5HttpDown::init_sock(char * server_name, int server_port)
{
    struct sockaddr_in sock_in ;
    struct hostent * he ;
    {
        // only worked in dos
        WSADATA wsadata ;
        if (WSAStartup(0x0202, &wsadata) != 0) return false ;
    }
   
    // get server ip address
    he = gethostbyname(server_name) ;
    if (!he) sock_in.sin_addr.s_addr = inet_addr(server_name) ;
    else {
        sock_in.sin_addr.s_addr = *(unsigned long *)(he->h_addr_list[0]) ;
#ifdef HTTP_DEBUG_MODE
        printf("ip : %s/n", inet_ntoa(sock_in.sin_addr)) ;
#endif
    }
   
    sock_in.sin_family = AF_INET;
    sock_in.sin_port = htons(server_port);
    m_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) ;
   
    if(!connect(m_sock,(struct sockaddr *)(&sock_in), sizeof(sock_in))) {
       HANDLE thread_handle ;
       m_running = true ;
       thread_handle = (HANDLE)(_beginthread(CM5HttpDown::recv_thread, 0,(void *)(this))) ;
       return true ;
    }
    return false ;
}
bool CM5HttpDown::close_sock()
{
    if(m_running) {
        m_running = false ;
        Sleep(1000) ;
        if(m_sock) closesocket(m_sock) ;
    }
    {
        // only worked in dos
        WSACleanup() ;
    }
    return true ;
}
bool CM5HttpDown::http_down(char * uri,
                            RECV_CALLBACK custom_func, void * custom_data,
                            int recv_bytes)
{
    char buffer[HTTP_SEND_BUF_LEN] ;    
    memset(buffer, 0, HTTP_TEMP_BUF_LEN) ;
   
    if(uri == NULL) return false ; 
    m_recv_bytes = recv_bytes ;
    m_custom_callback = custom_func ;
    m_custom_data = custom_data ;
   
    m_web_addr = new char [HTTP_TEMP_BUF_LEN] ;
    m_web_fname = new char [HTTP_TEMP_BUF_LEN] ;
    
    memset(m_web_addr, 0, HTTP_TEMP_BUF_LEN) ;
    memset(m_web_fname, 0, HTTP_TEMP_BUF_LEN) ;
   
    parse_uri(uri, m_web_addr, m_web_fname, &m_web_port) ;
    if(m_recv_bytes == 0) {
        snprintf(buffer, HTTP_SEND_BUF_LEN, HTTP_COMMON_GET,
                 m_web_fname, m_web_addr, m_web_port) ;
    } else {
        snprintf(buffer, HTTP_SEND_BUF_LEN, HTTP_RESUME_GET,
                 m_web_fname, m_web_addr, m_web_port, m_recv_bytes) ;
    }
#ifdef HTTP_DEBUG_MODE
    printf("%s/n", buffer) ;
#endif
    m_running = true ;
    if(!init_sock(m_web_addr, m_web_port)) return false ;
   
    // send the request
    return send_req(buffer, strlen(buffer)) ;
}
bool CM5HttpDown::http_stop()
{
    return close_sock() ;
}
 
贴上整个dev-c++的工程,感兴趣的朋友直接下了玩玩。
文件:nettest.rar
大小:15KB
下载:下载
运行nettest.exe,就会在其当前目录生成一个叫test.mp3的文件,屏幕上还显示下载进度。
可以随时关闭,然后再次打开,看看断点续传的效果。
下面的这个是将上述代码,通过RSocket移植到symbian s60 2nd平台上的测试程序(我做了一些修改
使之可以同时支持cmwap和cmnet的gprs环境,再此鄙视一下移动的行为),
出于公司的利益考虑,就不开放代码了。
文件:NetTestSIS.rar
大小:9KB
下载:下载
当调用cmwap conn的时候,接入点要选择"移动梦网"或者"nokia.com";
当调用cmnet conn的时候,接入点要选择"gprs连接互联网";
看到屏幕上显示"connected"的时候,选择"resume",就会开始断点续传过程。
对于cmwap由于移动有推送页面,在程序里面加入了效验,如果有推送页面,
程序会显示"check failed",这时,再按一次"resume"即可。
更换连网方式之前,需要选择"stop",断开gprs连接,然后再连。
还有一个小小的意外是对于移动的网关来说,Content-Length这是标准http服务器返回的;
但是移动的代理返回的结果是Content-length这个"l"是小写的,这个细节一定要注意才行喔!!
 
呵呵,期待5mbox网络版早日成功。
 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值