我们继续上一篇的内容,需要了解原理的朋友可以参照“断点续传(1)”所描述的部分。
(4)断点续传的实现
懒得一个个解释,直接上代码了(请注意,下面的代码是在win32上做验证用的,不是给手机用的),
采用c++编写,dev-c++ 4.9.9.2编译的,我相信g++都行vs系列自然不在话下:
M5HttpDown.h文件的内容:
#ifndef _M5_HTTP_DOWN_H_
#include
#include
#include
#include
#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解析函数,能够把诸如的uri分解为
// web_addr :
// 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文件地址,例如
// 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
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(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网络版早日成功。