服务器架设笔记——多模块和全局数据
? ? ? ? 隨著項目工程的發展,多模塊設計和性能優化是在所難免的。本文我將基于一些現實中可能遇到的需求,講解如何在Apache的Httpd插件體系中實現這些功能。(轉載請指明出于breaksoftware的csdn博客)
? ? ? ? 之前我碰到兩個需求:
- 需要將從數據庫中查詢到的某些字段替換成另一個字符,替換的映射關系在另外一張表里面(這張表很小,且幾乎不變動)。
- 需要返回一個可配置的字符串(基本不變動)。
? ? ? ? 對于需求1,我們最簡單的辦法就是:每次請求過來都去查詢一下映射關系數據表,然后替換相關字符。但是這個方法對于一個優秀的實現來說,還是挺low的。我們可以注意到這個需求的特點——幾乎不變動、且數據量少,那我們應該可以把他們放到我們內存里。
? ? ? ? 對于需求2,可以想到的最簡單的辦法就是:在代碼中硬編碼,將可配置的字符串寫死在代碼里。然后如果一旦有修改,那么我們就需要修改代碼文件中的硬編碼字段,然后編譯后上線。這種方式非常麻煩,且可能會帶來不穩定因素——說不定誰誰忘記了給待轉義字符增加轉義符呢。而且代碼中字符串一堆雙引號、單引號或者轉義符看著實在令人難受。我們還是通過動態加載配置文件的形式,將這段配置加載進來比較靠譜。
? ? ? ? 那么我就想,我需要設計一個模塊,用于預處理以上的需求——將數據加載到內存中。我給這個模塊取名為prepare。至于插件模塊的創建可以參見《服務器架設筆記——編譯Apache及其插件》,本文我不在贅述。
? ? ? ? prepare中的處理handler需要執行于其他業務handler之前。我們需要對httpd.conf做如下配置
<Location /query_board_list>SetHandler prepareSetHandler query_board_list
</Location><Location /query_project_info>SetHandler prepareSetHandler query_project_info
</Location><Location /query_board_info>SetHandler prepareSetHandler query_board_info
</Location>
? ? ? ? 上例的寫法,便將prepare的執行于其他handler之前。但是僅僅如此是不夠的,還有個隱藏的配置困擾了我很久,最后我開始“迷性”順序關系才找到問題的所在。見httpd.conf的模塊加載配置段
LoadModule prepare_module modules/mod_prepare.so
LoadModule query_board_list_module modules/mod_query_board_list.so
LoadModule query_project_info_module modules/mod_query_project_info.so
LoadModule query_board_info_module modules/mod_query_board_info.so
? ? ? ? 這一點一定要切記:要把需要起始執行的模塊,在之后處理的模塊之前加載。如果我們把mod_prepare.so加載于mod_query_board_list.so之后,那么prepare將不會在query_board_list之前執行。
? ? ? ? 然后我們來看下prepare內部的書寫。
static int prepare_handler(request_rec *r)
{apr_status_t rv;const char* user_data = NULL;const char* front_page_key = "front_page";const char* front_page_conf_path = "/usr/local/apache2/conf/front_page.template";const char* select_page_key = "select_page";const char* select_page_conf_path = "/usr/local/apache2/conf/select_page.template";apr_pool_userdata_setn(r, "request_rec_ptr", NULL, r->pool);rv = prepare_data(r->server->process->pool, front_page_key, front_page_conf_path);rv = prepare_data(r->server->process->pool, select_page_key, select_page_conf_path);prepare_map_from_db(r->server->process->pool, "LocationTable", "location");prepare_map_from_db(r->server->process->pool, "SourceTable", "source");prepare_map_from_db(r->server->process->pool, "ScopeTable", "scope");prepare_map_from_db(r->server->process->pool, "StageTable", "stage");return DECLINED;
}
? ? ? ? 這段代碼,需要注意的有四個部分:
- 將request_rec指針r保存到r->pool的內存池中,從而實現了在請求級別的“全局數據”——之后的一些模塊,可能沒有傳入request_rec指針。
- 通過prepare_data將配置文件內存保存到進程級別的內存池中,這樣一個進程只加載一次。之后通過判斷key是否存在來知道是否已經加載。
- 通過prepare_map_from_db將數據庫中不同表的數據保存到內存中。這樣的操作也是進程級別的。
- 返回DECLINED。返回這個值,告訴httpd還需要繼續向后執行其他handler。
? ? ? ? 以下是代碼的羅列
int prepare_data_from_db(apr_pool_t* pool, const char* database_table, pchar_ptr_map ptr_char_ptr_map) {const apr_dbd_driver_t* driver = NULL;apr_dbd_t* handle = NULL;apr_dbd_results_t* res = NULL;char* sql_cmd = NULL;apr_dbd_row_t* row = NULL;apr_status_t status = APR_SUCCESS;const char* value = NULL;apr_pool_t* pool_db = NULL;int rows_count = 0;pchar_item ptr_char_item = NULL;int index = 0;do {if (!pool || !database_table) {status = 41;break;}apr_pool_create(&pool_db, pool);if (!pool_db) {status = 42;break;}apr_dbd_init(pool_db);status = apr_dbd_get_driver(pool_db, "mysql", &driver);if (APR_SUCCESS != status) {status = 43;break;}status = apr_dbd_open(driver, pool_db, "host=localhost;user=root;pass=password;dbname=database_name", &handle);if (APR_SUCCESS != status) {status = 44;break;}sql_cmd = apr_psprintf(pool, "select * from %s", database_table);status = apr_dbd_select(driver, pool_db, handle, &res, sql_cmd, 0);if (APR_SUCCESS != status || !res) {status = 45;break;}rows_count = 64;ptr_char_ptr_map->array = apr_palloc(pool, rows_count * sizeof(pchar_item));while (0 == apr_dbd_get_row(driver, pool_db, res, &row, -1) && row) {ptr_char_item = apr_palloc(pool, sizeof(char_item));status = 0;value = apr_dbd_get_entry(driver, row, 0);if (value) {ptr_char_item->key = apr_psprintf(pool, "%s", value);}else {ptr_char_item->key = apr_psprintf(pool, "%s", "");}value = apr_dbd_get_entry(driver, row, 1);if (value) {ptr_char_item->value = apr_psprintf(pool, "%s", value);}else {ptr_char_item->value = apr_psprintf(pool, "%s", "");}ptr_char_ptr_map->array[index] = ptr_char_item;index++;}ptr_char_ptr_map->count = index;} while(0);if (driver && handle) {apr_dbd_close(driver, handle);}if (pool_db) {apr_pool_destroy(pool_db);}return status;
}int prepare_map_from_db(apr_pool_t* pool, const char* table_name, const char* key) {pchar_ptr_map ptr_char_ptr_map = NULL; if (APR_SUCCESS != apr_pool_userdata_get((void**)&ptr_char_ptr_map, key, pool) || !ptr_char_ptr_map) {ptr_char_ptr_map = apr_palloc(pool, sizeof(char_ptr_map)); prepare_data_from_db(pool, table_name, ptr_char_ptr_map);apr_pool_userdata_setn(ptr_char_ptr_map, key, NULL, pool);}return APR_SUCCESS;
}static apr_status_t save_file_to_mem(apr_pool_t* pool, const char* key, const char* file_path) {apr_status_t rv;apr_size_t buf_size = 0;apr_file_t* file_in = NULL;const char* file_buf = NULL;apr_size_t real_size = 0;apr_off_t offset = 0;if (!pool || !key || !file_path) {return 10;}rv = apr_file_open(&file_in, file_path, APR_FOPEN_READ | APR_FOPEN_BUFFERED, APR_OS_DEFAULT, pool);if (APR_SUCCESS != rv) {return rv;}do {buf_size = apr_file_buffer_size_get(file_in);if (0 == buf_size) {rv = 11;break;}real_size = buf_size;file_buf = apr_palloc(pool, buf_size);if (!file_buf) {rv = 12;break;}rv = apr_file_read(file_in, (void*)file_buf, &real_size);if (APR_SUCCESS != rv) {break;}apr_pool_userdata_setn(file_buf, key, NULL, pool);} while(0);apr_file_close(file_in);return rv;
}static apr_status_t prepare_data(apr_pool_t* pool, const char* key, const char* file_path) {const char* user_data = NULL;if (APR_SUCCESS != apr_pool_userdata_get((void**)&user_data, key, pool) || !user_data) {return save_file_to_mem(pool, key, file_path);}return APR_SUCCESS;
}
? ? ? ? 不可否認的一點是,在插件中寫數據庫訪問的邏輯還是挺麻煩的。因為總是會遇到一些意想不到的問題,比如在上例中:
- 直接使用傳入的pool操作數據庫——雖然已經apr_dbd_init了,可能會導致進程意外退出。
- 調用apr_dbd_select最后一個參數傳1,可能會導致進程意外退出。
- 調用apr_dbd_select最后一個參數傳0,計算結果個數的apr_dbd_num_tuples函數將錯誤。這個問題與2結合導致我只能硬編碼結果上線——low了一下。
? ? ? ? 當然可能是我哪兒不得要領,但是從快速開發的角度來說,或許“下雪天,PHP和httpd更配哦”。
總結
以上是生活随笔為你收集整理的服务器架设笔记——多模块和全局数据的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 服务器架设笔记——搭建用户注册和验证功能
- 下一篇: 服务器架设笔记——httpd插件支持my