基於C語言實現http下載器
C語言實現http的下載器。
例:做OTA升級功能時,我們能直接拿到的往往隻是升級包的鏈接,需要我們自己去下載,這時候就需要用到http下載器。
這裡分享一個:
功能
1、支持chunked方式傳輸的下載
2、被重定向時能下載重定向頁面
3、要實現的接口為int http_download(char *url, char *save_path)
思路
1、解析輸入的URL,分離出主機,端口號,文件路徑的信息
2、解析主機的DNS
3、填充http請求的頭部,給服務器發包
4、解析收到的http頭,提取狀態碼,Content-length, Transfer-Encoding等字段信息
(1)如果是普通的頭則進行接下來的正常收包流程
(2)如果狀態碼為302,則從頭裡提取出重定向地址,用新的地址重新開始下載動作
(3)如果傳送方式是chunked的,則進行分段讀取數據並拼接
(4)如果是404或其他狀態碼則打印錯誤信息
缺陷
太多錯誤處理,讓代碼看起來不太舒服
其他
1、如何移植到沒有文件系統的系統中?
修改sava_data接口裡面的保存就好瞭
2、如何提高下載速度?
增大讀寫buffer緩沖區
改為多線程,使用Range字段分段讀取,最後再拼在一起
代碼
/************************************************************ Copyright (C), 2016, Leon, All Rights Reserved. FileName: download.c coding: UTF-8 Description: 實現簡單的http下載功能 Author: Leon Version: 1.0 Date: 2016-12-2 10:49:32 Function: History: <author> <time> <version> <description> Leon ************************************************************/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h> #include <fcntl.h> #include <unistd.h> #include <netdb.h> #include <errno.h> #define HOST_NAME_LEN 256 #define URI_MAX_LEN 2048 #define RECV_BUF 8192 #define RCV_SND_TIMEOUT (10*1000) //收發數據超時時間(ms) typedef struct { int sock; //與服務器通信的socket FILE *in; //sock描述符轉為文件指針,方便讀寫 char host_name[HOST_NAME_LEN]; //主機名 int port; //主機端口號 char uri[URI_MAX_LEN]; //資源路徑 char buffer[RECV_BUF]; //讀寫緩沖 int status_code; //http狀態碼 int chunked_flag; //chunked傳輸的標志位 int len; //Content-length裡的長度 char location[URI_MAX_LEN]; //重定向地址 char *save_path; //保存內容的路徑指針 FILE *save_file; //保存內容的文件指針 int recv_data_len; //收到數據的總長度 time_t start_recv_time; //開始接受數據的時間 time_t end_recv_time; //結束接受數據的時間 } http_t; /* 打印宏 */ #define MSG_DEBUG 0x01 #define MSG_INFO 0x02 #define MSG_ERROR 0x04 static int print_level = /*MSG_DEBUG |*/ MSG_INFO | MSG_ERROR; #define lprintf(level, format, argv...) do{ \ if(level & print_level) \ printf("[%s][%s(%d)]:"format, #level, __FUNCTION__, __LINE__, ##argv); \ }while(0) #define MIN(x, y) ((x) > (y) ? (y) : (x)) #define HTTP_OK 200 #define HTTP_REDIRECT 302 #define HTTP_NOT_FOUND 404 /* 不區分大小寫的strstr */ char *strncasestr(char *str, char *sub) { if(!str || !sub) return NULL; int len = strlen(sub); if (len == 0) { return NULL; } while (*str) { if (strncasecmp(str, sub, len) == 0) { return str; } ++str; } return NULL; } /* 解析URL, 成功返回0,失敗返回-1 */ /* http://127.0.0.1:8080/testfile */ int parser_URL(char *url, http_t *info) { char *tmp = url, *start = NULL, *end = NULL; int len = 0; /* 跳過http:// */ if(strncasestr(tmp, "http://")) { tmp += strlen("http://"); } start = tmp; if(!(tmp = strchr(start, '/'))) { lprintf(MSG_ERROR, "url invaild\n"); return -1; } end = tmp; /*解析端口號和主機*/ info->port = 80; //先附默認值80 len = MIN(end - start, HOST_NAME_LEN - 1); strncpy(info->host_name, start, len); info->host_name[len] = '\0'; if((tmp = strchr(start, ':')) && tmp < end) { info->port = atoi(tmp + 1); if(info->port <= 0 || info->port >= 65535) { lprintf(MSG_ERROR, "url port invaild\n"); return -1; } /* 覆蓋之前的賦值 */ len = MIN(tmp - start, HOST_NAME_LEN - 1); strncpy(info->host_name, start, len); info->host_name[len] = '\0'; } /* 復制uri */ start = end; strncpy(info->uri, start, URI_MAX_LEN - 1); lprintf(MSG_INFO, "parse url ok\nhost:%s, port:%d, uri:%s\n", info->host_name, info->port, info->uri); return 0; } /* dns解析,返回解析到的第一個地址,失敗返回-1,成功則返回相應地址 */ unsigned long dns(char* host_name) { struct hostent* host; struct in_addr addr; char **pp; host = gethostbyname(host_name); if (host == NULL) { lprintf(MSG_ERROR, "gethostbyname %s failed\n", host_name); return -1; } pp = host->h_addr_list; if (*pp!=NULL) { addr.s_addr = *((unsigned int *)*pp); lprintf(MSG_INFO, "%s address is %s\n", host_name, inet_ntoa(addr)); pp++; return addr.s_addr; } return -1; } /* 設置發送接收超時 */ int set_socket_option(int sock) { struct timeval timeout; timeout.tv_sec = RCV_SND_TIMEOUT/1000; timeout.tv_usec = RCV_SND_TIMEOUT%1000*1000; lprintf(MSG_DEBUG, "%ds %dus\n", (int)timeout.tv_sec, (int)timeout.tv_usec); //設置socket為非阻塞 // fcntl(sock ,F_SETFL, O_NONBLOCK); //以非阻塞的方式,connect需要重新處理 // 設置發送超時 if(-1 == setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, sizeof(struct timeval))) { lprintf(MSG_ERROR, "setsockopt error: %m\n"); return -1; } // 設置接送超時 if(-1 == setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(struct timeval))) { lprintf(MSG_ERROR, "setsockopt error: %m\n"); return -1; } return 0; } /* 連接到服務器 */ int connect_server(http_t *info) { int sockfd; struct sockaddr_in server; unsigned long addr = 0; unsigned short port = info->port; sockfd = socket(AF_INET, SOCK_STREAM, 0); if (-1 == sockfd) { lprintf(MSG_ERROR, "socket create failed\n"); goto failed; } if(-1 == set_socket_option(sockfd)) { goto failed; } if ((addr = dns(info->host_name)) == -1) { lprintf(MSG_ERROR, "Get Dns Failed\n"); goto failed; } memset(&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(port); server.sin_addr.s_addr = addr; if (-1 == connect(sockfd, (struct sockaddr *)&server, sizeof(struct sockaddr))) { lprintf(MSG_ERROR, "connect failed: %m\n"); goto failed; } info->sock = sockfd; return 0; failed: if(sockfd != -1) close(sockfd); return -1; } /* 發送http請求 */ int send_request(http_t *info) { int len; memset(info->buffer, 0x0, RECV_BUF); snprintf(info->buffer, RECV_BUF - 1, "GET %s HTTP/1.1\r\n" "Accept: */*\r\n" "User-Agent: Mozilla/5.0 (compatible; MSIE 5.01; Windows NT 5.0)\r\n" "Host: %s\r\n" "Connection: Close\r\n\r\n", info->uri, info->host_name); lprintf(MSG_DEBUG, "request:\n%s\n", info->buffer); return send(info->sock, info->buffer, strlen(info->buffer), 0); } /* 解析http頭 */ int parse_http_header(http_t *info) { char *p = NULL; // 解析第一行 fgets(info->buffer, RECV_BUF, info->in); p = strchr(info->buffer, ' '); //簡單檢查http頭第一行是否合法 if(!p || !strcasestr(info->buffer, "HTTP")) { lprintf(MSG_ERROR, "bad http head\n"); return -1; } info->status_code = atoi(p + 1); lprintf(MSG_DEBUG, "http status code: %d\n", info->status_code); // 循環讀取解析http頭 while(fgets(info->buffer, RECV_BUF, info->in)) { // 判斷頭部是否讀完 if(!strcmp(info->buffer, "\r\n")) { return 0; /* 頭解析正常 */ } lprintf(MSG_DEBUG, "%s", info->buffer); // 解析長度 Content-length: 554 if(p = strncasestr(info->buffer, "Content-length")) { p = strchr(p, ':'); p += 2; // 跳過冒號和後面的空格 info->len = atoi(p); lprintf(MSG_INFO, "Content-length: %d\n", info->len); } else if(p = strncasestr(info->buffer, "Transfer-Encoding")) { if(strncasestr(info->buffer, "chunked")) { info->chunked_flag = 1; } else { /* 不支持其他編碼的傳送方式 */ lprintf(MSG_ERROR, "Not support %s", info->buffer); return -1; } lprintf(MSG_INFO, "%s", info->buffer); } else if(p = strncasestr(info->buffer, "Location")) { p = strchr(p, ':'); p += 2; // 跳過冒號和後面的空格 strncpy(info->location, p, URI_MAX_LEN - 1); lprintf(MSG_INFO, "Location: %s\n", info->location); } } lprintf(MSG_ERROR, "bad http head\n"); return -1; /* 頭解析出錯 */ } /* 保存服務器響應的內容 */ int save_data(http_t *info, const char *buf, int len) { int total_len = len; int write_len = 0; // 文件沒有打開則先打開 if(!info->save_file) { info->save_file = fopen(info->save_path, "w"); if(!info->save_file) { lprintf(MSG_ERROR, "fopen %s error: %m\n", info->save_path); return -1; } } while(total_len) { write_len = fwrite(buf, sizeof(char), len, info->save_file); if(write_len < len && errno != EINTR) { lprintf(MSG_ERROR, "fwrite error: %m\n"); return -1; } total_len -= write_len; } } /* 讀數據 */ int read_data(http_t *info, int len) { int total_len = len; int read_len = 0; int rtn_len = 0; while(total_len) { read_len = MIN(total_len, RECV_BUF); // lprintf(MSG_DEBUG, "need read len: %d\n", read_len); rtn_len = fread(info->buffer, sizeof(char), read_len, info->in); if(rtn_len < read_len) { if(ferror(info->in)) { if(errno == EINTR) /* 信號中斷瞭讀操作 */ { ; /* 不做處理繼續往下走 */ } else if(errno == EAGAIN || errno == EWOULDBLOCK) /* 超時 */ { lprintf(MSG_ERROR, "socket recvice timeout: %dms\n", RCV_SND_TIMEOUT); total_len -= rtn_len; lprintf(MSG_DEBUG, "read len: %d\n", rtn_len); break; } else /* 其他錯誤 */ { lprintf(MSG_ERROR, "fread error: %m\n"); break; } } else /* 讀到文件尾 */ { lprintf(MSG_ERROR, "socket closed by peer\n"); total_len -= rtn_len; lprintf(MSG_DEBUG, "read len: %d\n", rtn_len); break; } } // lprintf(MSG_DEBUG, " %s\n", info->buffer); total_len -= rtn_len; lprintf(MSG_DEBUG, "read len: %d\n", rtn_len); if(-1 == save_data(info, info->buffer, rtn_len)) { return -1; } info->recv_data_len += rtn_len; } if(total_len != 0) { lprintf(MSG_ERROR, "we need to read %d bytes, but read %d bytes now\n", len, len - total_len); return -1; } } /* 接收服務器發回的chunked數據 */ int recv_chunked_response(http_t *info) { long part_len; //有chunked,content length就沒有瞭 do{ // 獲取這一個部分的長度 fgets(info->buffer, RECV_BUF, info->in); part_len = strtol(info->buffer, NULL, 16); lprintf(MSG_DEBUG, "part len: %ld\n", part_len); if(-1 == read_data(info, part_len)) return -1; //讀走後面的\r\n兩個字符 if(2 != fread(info->buffer, sizeof(char), 2, info->in)) { lprintf(MSG_ERROR, "fread \\r\\n error : %m\n"); return -1; } }while(part_len); return 0; } /* 計算平均下載速度,單位byte/s */ float calc_download_speed(http_t *info) { int diff_time = 0; float speed = 0.0; diff_time = info->end_recv_time - info->start_recv_time; /* 最小間隔1s,避免計算浮點數結果為inf */ if(0 == diff_time) diff_time = 1; speed = (float)info->recv_data_len / diff_time; return speed; } /* 接收服務器的響應數據 */ int recv_response(http_t *info) { int len = 0, total_len = info->len; if(info->chunked_flag) return recv_chunked_response(info); if(-1 == read_data(info, total_len)) return -1; return 0; } /* 清理操作 */ void clean_up(http_t *info) { if(info->in) fclose(info->in); if(-1 != info->sock) close(info->sock); if(info->save_file) fclose(info->save_file); if(info) free(info); } /* 下載主函數 */ int http_download(char *url, char *save_path) { http_t *info = NULL; char tmp[URI_MAX_LEN] = {0}; if(!url || !save_path) return -1; //初始化結構體 info = malloc(sizeof(http_t)); if(!info) { lprintf(MSG_ERROR, "malloc failed\n"); return -1; } memset(info, 0x0, sizeof(http_t)); info->sock = -1; info->save_path = save_path; // 解析url if(-1 == parser_URL(url, info)) goto failed; // 連接到server if(-1 == connect_server(info)) goto failed; // 發送http請求報文 if(-1 == send_request(info)) goto failed; // 接收響應的頭信息 info->in = fdopen(info->sock, "r"); if(!info->in) { lprintf(MSG_ERROR, "fdopen error\n"); goto failed; } // 解析頭部 if(-1 == parse_http_header(info)) goto failed; switch(info->status_code) { case HTTP_OK: // 接收數據 lprintf(MSG_DEBUG, "recv data now\n"); info->start_recv_time = time(0); if(-1 == recv_response(info)) goto failed; info->end_recv_time = time(0); lprintf(MSG_INFO, "recv %d bytes\n", info->recv_data_len); lprintf(MSG_INFO, "Average download speed: %.2fKB/s\n", calc_download_speed(info)/1000); break; case HTTP_REDIRECT: // 重啟本函數 lprintf(MSG_INFO, "redirect: %s\n", info->location); strncpy(tmp, info->location, URI_MAX_LEN - 1); clean_up(info); return http_download(tmp, save_path); case HTTP_NOT_FOUND: // 退出 lprintf(MSG_ERROR, "Page not found\n"); goto failed; break; default: lprintf(MSG_INFO, "Not supported http code %d\n", info->status_code); goto failed; } clean_up(info); return 0; failed: clean_up(info); return -1; } /**************************************************************************** 測試用例: (1)chunked接收測試 ./a.out "http://www.httpwatch.com/httpgallery/chunked/chunkedimage.aspx" test.aspx (2)重定向測試 ./a.out "192.168.10.1/main.html" test.txt (3)錯誤輸入測試 ./a.out "32131233" test.txt (4)根目錄輸入測試 ./a.out "www.baidu.com/" test.txt (5)端口號訪問測試 ./a.out "192.168.0.200:8000/FS_AC6V1.0BR_V15.03.4.12_multi_TD01.bin" test.txt ****************************************************************************/ int main(int argc, char *argv[]) { if(argc < 3) return -1; http_download(argv[1], argv[2]); return 0;
以上就是基於C語言實現http下載器的詳細內容,更多關於C語言http下載器的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- python使用socket高效傳輸視頻數據幀(連續發送圖片)
- Python 基於TCP 傳輸協議的網絡通信實現方法
- python實現套接字創建
- C語言時間函數之strftime()詳解
- 詳談C++ socket網絡編程實例(2)