c apache2模塊開發--根據自定義業務邏輯實現文件下載

1.需求概述 

    最近和公司其他項目平臺對接,有這樣一個需求:提供一個HTTP Server,從URL中解析出文件ID等信息,然後調用我方項目開發的接口,從我方平臺中下載這個文件,根據URL中的參數再對其做一些簡單處理,然後再將文件以HTTP方式發送給對方平臺。由於只用到一個查詢接口,get即可滿足,因此不用rest庫。且受限於軟硬件條件,不用java,需使用c/c++開發。


2.總體思路:
        使用apache2搭建http server,然後開發一個模塊處理http請求,在該模塊中解析URL、調用我方平臺接口下載文件、對文件做二次處理、封裝http響應報文,將請求返回給客戶端。


3.apache2 模塊開發
       關於apache2的安裝,這裏不做贅述,請自行百度。
       apache2 模塊開發步驟,網上資料也比較多,請主要參考 《將 Apache httpd 作爲應用開發平臺》。
       簡單的說,就是通過apache2提供的apxs工具,生成一套框架代碼、Makefile以及部署腳本,然後基於該框架代碼進一步添加自己的業務邏輯:
       1) apxs -g -n mymodule   生成模塊代碼框架(mymodule 是自己的模塊名): apache2會生成名爲 mod_mymodule的目錄,其中包含 mod_mymodule.c以及Makefile等文件
       2) 修改 mod_mymodule.c ,添加自己的業務邏輯(稍後再詳細介紹)
       3) apxs -i -c -a  mod_mymodule.c   會將so文件釋放到apache2的lib目錄,例如:
          /usr/lib64/httpd/modules
       4) 修改apache2 配置,加載 mymodule模塊,並執行 apachectrl -k  restart 重啓apache2服務。
LoadModule mymodule_module /usr/lib64/httpd/modules/mod_mymodule.so
<Location /mymodule>
      SetHandler mymodule
</Location>

4.  下面主要介紹如何修改自己的模塊代碼,實現文件下載
4.1 檢查參數,解析URL
URL 格式定義如下:
http://192.168.1.100:8088/mymodule?file=/home/test/abc.txt&type=2
/home/test/abc.txt 表示文件路徑,這裏僅僅是爲了演示說明,因此用了本地目錄。實際可能需要調用一些接口去獲得這個文件;
type=2表示對原始文件如何處理,比如0-表示直接傳輸給客戶端;1-表示將文件壓縮後再傳輸,等等。

4.2、設置HTTP頭
     百度知,下載文件通常的http響應報文header要包含以下字段。因此需要在代碼中設置這些信息,並讀取文件數據。
header("Content-type: application/octet-stream");                   //高速瀏覽器傳遞的是文件流
header("Accept-Length: 2048");                                                  //文件大小
header("Content-Disposition: attachment; filename=abc.txt");  //指定文件名

對應代碼修改如下:
static int mymodule_handler(request_rec *r)
{
    if (strcmp(r->handler, "mymodule")) {
        return DECLINED;
    }
/*    r->content_type = "text/html";      */                    /*這是apxs模板生成的代碼 */
    r->content_type = "application/octet-stream";      /*設置Content-type*/
    /*request_rec 結構中沒有定義與Content-Disposition直接對應的字段,但header_out包含了所有response的header信息,我們可以手動把這個字段add進來(注意:不能用apr_table_set,會把其他header信息覆蓋掉)*/
    apr_table_add(r->headers_out,"Content-Disposition","attachment;filename=abc.txt");
    ……
    /* 獲取(本地)文件長度 */
    apr_finfo_t  info;
    apr_stat(&info, r->filename, APR_FINFO_SIZE, r->pool);
    len = (apr_size_t)info.size;
    char file_len[64];
    memset(file_len, 0, sizeof(file_len));
    snprintf(file_len, sizeof(file_len)-1, "%d", (int)info.size);
    apr_table_add(r->headers_out,"Content-Length", file_len);

4.3、 獲取文件:
      本人所在用項目中,主要是調用自己項目裏的一些接口將文件從遠程下載到本地內存,因過程比較簡單且不具有通用性,不再贅述。假設兩種比較典型的情況:
4.3.1 文件在磁盤(文件系統中),調用apache接口直接讀取、發送文件
通過 apr_file_open 打開文件,ap_send_fd 發送文件,apr_file_close 關閉文件。需要主要send調用應該是一個循環,代碼比較簡單:
	/* call apr_file_open,ap_send_fd to open and send file from local file system */
	apr_file_t *f = NULL;
	apr_status_t rv;
	apr_off_t offset = 0;
	apr_size_t bytes = 0;
	apr_size_t len = 0;
	rv = apr_file_open( &f, file_path, APR_READ | APR_SENDFILE_ENABLED, APR_OS_DEFAULT, r->pool );
	if( NULL == f ){
		ap_log_error( APLOG_MARK, APLOG_ERR, 0, r->server, "file(%s) permissions deny server access", file_path );
		return -1;
	}
	if( !r->header_only ){
		while( offset < len ){
                        /*ap_flush_conn(r->connection);*/
			ap_send_fd( f, r, offset, len, &bytes );
			offset += bytes;
		}
	}
	apr_file_close( f );



4.3.2 文件信息內存中
     以本人實際遇到的項目爲例,收到http請求後,會調用自己的sdk接口,遠程下載文件數據,爲了提高效率,所以文件肯定是先到內存,再落文件。爲了提高效率,可以不落文件,當數據還在內存中的時候就直接返回。
     這時需要用到的是ap_rwrite  接口。
       爲了演示,這裏自己調用fopen,fread 打開本地文件並讀取數據到內存,然後調用 ap_write 將其發送出去。
    /* suppose that we have already downloaded files from other platform, and all the file datas are in the memory.
     * so just return the memory data to client */
    FILE*  fp = fopen(file_path, "r");
    if ( NULL == fp ) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server,"failed to open file %s", file_path);
        return -1;
    }
    int read_ret = 0;
    char read_buf[FILE_BUF_SIZE];
	while( !feof( fp ) ){
		memset( read_buf, 0, sizeof( read_buf ) );
		read_ret = fread( read_buf, 1, 1024, fp );
		if( ferror( fp ) ){
			/* todo log error */
			return -1;
		}
		/*send data to client*/
		int send_bytes = 0;
		while( send_bytes < read_ret ){
			/*ap_flush_conn(r->connection);*/
			int send_ret = ap_rwrite( read_buf, read_ret - send_bytes, r );
			if( send_ret >= 0 ) {
				send_bytes += send_ret;
			} else {
				/* todo log error */
				return -1;
			}
		}
	}
	fclose(fp);


4.3.3 其他發送接口
如apxs框架生成的代碼中用到的ap_rputs,可以在http響應報文中設置一個字符串。 類似這些交口比較簡單,可以直接查看頭文件中的定義。


附件: 完整的示例代碼如下
/* 
**  mod_helloworld.c -- Apache sample helloworld module
**  [Autogenerated via ``apxs -n helloworld -g'']
**
**  To play with this sample module first compile it into a
**  DSO file and install it into Apache's modules directory 
**  by running:
**
**    $ apxs -c -i mod_helloworld.c
**
**  Then activate it in Apache's httpd.conf file for instance
**  for the URL /helloworld in as follows:
**
**    #   httpd.conf
**    LoadModule helloworld_module modules/mod_helloworld.so
**    <Location /helloworld>
**    SetHandler helloworld
**    </Location>
**
**  Then after restarting Apache via
**
**    $ apachectl restart
**
**  you immediately can request the URL /helloworld and watch for the
**  output of this module. This can be achieved for instance via:
**
**    $ lynx -mime_header http://localhost/helloworld 
**
**  The output should be similar to the following one:
**
**    HTTP/1.1 200 OK
**    Date: Tue, 31 Mar 1998 14:42:22 GMT
**    Server: Apache/1.3.4 (Unix)
**    Connection: close
**    Content-Type: text/html
**  
**    The sample page from mod_helloworld.c
*/ 

#include "httpd.h" 
#include "http_config.h" 
#include "http_protocol.h" 
/*#include "http_connection.h"*/
#include "ap_config.h" 
#include "ap_regex.h" 
#include "http_log.h" 
#include <stdio.h>

#define  MAX_PATH_LEN          256
#define  MAX_FILE_LEN_DIGITS   64
#define  FILE_BUF_SIZE         1024
/*#define  RETURN_FROM_MEMORY*/

/* get file name from the abolute path
 * eg:  input   /home/downloads/test.so
 *      output  test.so
 */
const char* get_file_name(const char* path)
{
    if (NULL == path) {
        return NULL;
    }
    int  path_len = strlen(path);
    const char *pos = path + path_len;
    while (*pos != '/' && pos != path) {
        pos--;
    }
    if (pos == path) {
        return path+1;
    }else {
        int len = len - (pos - path);
        return (pos + 1);
    }
}

int get_file_length(const char* file_path, request_rec *r)
{
    int len = 0;
    apr_finfo_t  info;
    apr_stat(&info, file_path, APR_FINFO_SIZE, r->pool);
    len = (apr_size_t)info.size;
    ap_log_error(APLOG_MARK, APLOG_DEBUG, 0,r->server, "file :%s, len:%d", file_path, len);
    return len;
}

/* The sample content handler */
static int helloworld_handler(request_rec *r)
{
    if (strcmp(r->handler, "helloworld")) {
        return DECLINED;
    }

    /* only support GET or POST request */
    if ((r->method_number != M_GET) && (r->method_number != M_POST)) {
        return HTTP_METHOD_NOT_ALLOWED;
    }

    /* full url : http://172.25.3.121:8088/helloworld?file=/home/test.txt&type=2*/
    /* r->parsed_uri.query : file=/home/test.txt&type=2 */
    if ( NULL == r->parsed_uri.query ){
        ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server,"uri param is empty");
        return HTTP_BAD_REQUEST;
    }
    /* parse file name from uri param */
    char file_path[MAX_PATH_LEN];
    memset(file_path, 0, sizeof(file_path));
    int  file_type=0;
    int ret = sscanf(r->parsed_uri.query, "file=%[^&]&type=%d", file_path, &file_type);
    if ( ret != 2 ) {
         ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server, "failed to parse file path and type from uri:%s,ret:%d", r->parsed_uri.query, ret);
         return HTTP_BAD_REQUEST;
    }

    /* set response headers */
    /* Content-Type:application/octet-stream */
    r->content_type = "application/octet-stream";                   /* "text/html" */
    /* Content-Disposition:attachment;filename=test.txt */
    char file_name[24 + (MAX_PATH_LEN)] = {0};   /* length of "attachment;filename=" is 20 */
    sprintf(file_name, "attachment;filename=%s", get_file_name(file_path));
    apr_table_add(r->headers_out,"Content-Disposition", file_name);
    /* Content-Length:xxxx */
    char file_len[MAX_FILE_LEN_DIGITS];
    memset(file_len, 0, sizeof(file_len));
    int file_length = get_file_length(file_path, r);
    snprintf(file_len, sizeof(file_len)-1, "%d", file_length);
    apr_table_add(r->headers_out,"Content-Length", file_len);

#ifdef RETURN_FROM_MEMORY
    /* suppose that we have already downloaded files from other platform, and all the file datas are in the memory.
     * so just return the memory data to client */
    FILE*  fp = fopen(file_path, "r");
    if ( NULL == fp ) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server,"failed to open file %s", file_path);
        return -1;
    }
    int read_ret = 0;
    char read_buf[FILE_BUF_SIZE];
    while( !feof( fp ) ){
        memset( read_buf, 0, sizeof( read_buf ) );
        read_ret = fread( read_buf, 1, 1024, fp );
        if( ferror( fp ) ){
            /* todo log error */
            return -1;
        }
        /*send data to client*/
        int send_bytes = 0;
        while( send_bytes < read_ret ){
            /*ap_flush_conn(r->connection);*/
            int send_ret = ap_rwrite( read_buf, read_ret - send_bytes, r );
            if( send_ret >= 0 ) {
                send_bytes += send_ret;
            } else {
                /* todo log error */
                return -1;
            }
        }
    }
    fclose(fp);
#else
    /* call apr_file_open,ap_send_fd to open and send file from local file system */
    apr_file_t *f = NULL;
    apr_status_t rv;
    apr_off_t offset = 0;
    apr_size_t bytes = 0;
    apr_size_t len = file_length;
    rv = apr_file_open( &f, file_path, APR_READ | APR_SENDFILE_ENABLED, APR_OS_DEFAULT, r->pool );
    if( NULL == f ){
        ap_log_error( APLOG_MARK, APLOG_ERR, 0, r->server, "file(%s) permissions deny server access", file_path );
        return -1;
    }
    if( !r->header_only ){
        while( offset < len ){
                        ap_flush_conn(r->connection);
            ap_send_fd( f, r, offset, len, &bytes );
            offset += bytes;
        }
    }
    apr_file_close( f );
#endif
    return OK;
}

static void helloworld_register_hooks(apr_pool_t *p)
{
    ap_hook_handler(helloworld_handler, NULL, NULL, APR_HOOK_MIDDLE);
}

/* Dispatch list for API hooks */
module AP_MODULE_DECLARE_DATA helloworld_module = {
    STANDARD20_MODULE_STUFF, 
    NULL,                  /* create per-dir    config structures */
    NULL,                  /* merge  per-dir    config structures */
    NULL,                  /* create per-server config structures */
    NULL,                  /* merge  per-server config structures */
    NULL,                  /* table of config file commands       */
    helloworld_register_hooks  /* register hooks                      */
};








發佈了32 篇原創文章 · 獲贊 25 · 訪問量 23萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章