2008年9月25日 星期四

[轉載]斷點續傳技術

北京理工大學 20981 陳罡

要做手機斷點續傳了,網上的代碼有很多,但是多數要麼是過於復雜,要麼是用java,pascal之類的語言編寫的。都不適合直接用在手機上,無奈之下我這個懶人開始動手自己寫了。
(1)手機斷點續傳的未來?
手機上開發應用程序的時候,或多或少都要用到gprs連接互聯網(Internet),從互聯網上的服務器中把數據取出來,然後存儲到手機上,利用專門的客戶端來查看。這就可以美其名曰“在線更新”了。隨着智能手機的處理能力越來越強以及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了。

知道了以上原理,就可以進行斷點續傳的編程了。

(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下載
<pre style="font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; color: #000000; background-color: #999;font-size: 12px;border: 1px dashed #999999;line-height: 14px;padding: 5px; overflow: auto; width: 100%">#define HTTP_COMMON_GET &quot;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&quot;

#define HTTP_RESUME_GET &quot;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&quot;
</pre>

// 這里為了方便起見,就沒有用什麼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
運行nettest.exe,就會在其當前目錄生成一個叫test.mp3的文件,螢幕上還顯示下載進度。
可以隨時關閉,然後再次打開,看看斷點續傳的效果。
下面的這個是將上述代碼,通過RSocket移植到symbian s60 2nd平台上的測試程序(我做了一些修改使之可以同時支持cmwap和cmnet的gprs環境,再此鄙視一下移動的行為),
出于公司的利益考慮,就不開放代碼了。
NetTestSIS.rar
當調用cmwap conn的時候,接入點要選擇"移動夢網"或者"nokia.com";
當調用cmnet conn的時候,接入點要選擇"gprs連接互聯網";
看到螢幕上顯示"connected"的時候,選擇"resume",就會開始斷點續傳過程。
對於cmwap由於移動有推送頁面,在程序里面加入了效驗,如果有推送頁面,
程序會顯示"check failed",這時,再按一次"resume"即可。
更換連網方式之前,需要選擇"stop",斷開gprs連接,然後再連。
還有一個小小的意外是對于移動的網關來說,Content-Length這是標准http服務器返回的;
但是移動的代理返回的結果是Content-length這個"l"是小寫的,這個細節一定要注意才行喔!!

呵呵,期待5mbox網路版早日成功。

資料來源:
http://blog.chinaunix.net/u/26691/showart_433628.html
http://blog.chinaunix.net/u/26691/showart_433631.html

沒有留言: