頭文件可以作為接口定義,再加上static修飾符,就很容易定義私有的接口,每一個(gè)具體的實(shí)現(xiàn),即所有包含所有私有接口的頭文件,都必須要完整實(shí)現(xiàn)所有已聲明但未實(shí)現(xiàn)的函數(shù),否則gcc編譯不過去。廢話不多說,進(jìn)入步驟吧。
以毫無用處的blog為例,簡單兩個(gè)方法就行了,需要每一個(gè)實(shí)現(xiàn)暴露一個(gè)可以外部調(diào)用函數(shù)。
blog.h:
這個(gè)頭文件定義了一個(gè)對(duì)象??梢宰杂傻谋话?,包含之后自由使用blog_t結(jié)構(gòu)。
blog_impl.h:
這個(gè)文件聲明定義了若干static屬性的函數(shù),因此,只要包含此頭文件的C源文件,必須實(shí)現(xiàn)welcome與init函數(shù),否則gcc編譯不通過。需要注意此頭文件私有。
blog1.c,僅僅一個(gè)實(shí)現(xiàn):
僅有一個(gè)對(duì)外入口:gen_blog1_ptr
,也就是此實(shí)現(xiàn)對(duì)外唯一的交互方式。
blog2.c,默認(rèn)的實(shí)現(xiàn):
此文件對(duì)外唯一入口為:gen_blog2_ptr
。
其實(shí)兩個(gè)實(shí)現(xiàn)已經(jīng)可以了,但多一個(gè)說明接口單一,實(shí)現(xiàn)多樣性,再說錦上添花也是人們喜歡做的事情。 blog3.c,添花版:
一樣可以看到類似約定好的對(duì)外函數(shù)名稱gen_blog3_ptr
我們以app.c作為應(yīng)用入口:
這里分別調(diào)用blog1.c, blog2.c, blog3.c,唯一入口,執(zhí)行簡單的邏輯。
編譯命令行代碼很簡單:
gcc -o app app.c blog1.c blog2.c blog3.c
運(yùn)行:
./app
運(yùn)行效果:
the blog1's actions ...
the blog owner is blog1
here is the blog1 haha !the blog2's actions ... Here is the default blog init action !
The system's welcome action !the blog3's actions ...
Hi, blog3
you are welcome blog3!
這里借助兩個(gè)頭文件,模擬了私有接口,公有結(jié)構(gòu)體對(duì)象,三個(gè)具體子類實(shí)現(xiàn)。
在c_socket.io_server
項(xiàng)目中,作用于具體的實(shí)現(xiàn),以及定義了傳輸通道模型和實(shí)現(xiàn),互相不干擾。
當(dāng)然和JAVA相比,模擬對(duì)象程度稍低了一些,但夠用了。這個(gè)世界不僅僅只有面向?qū)ο螅€有面向并發(fā)的函數(shù)式Erlang,還有面向軟件工程的大型語言Go。嗯,面向?qū)ο蟛贿^是這個(gè)世界其中一角,天生存在缺陷,也不是被所有人喜歡。組件公用、庫的概念,倒是大部分語言都很自然的欣然接受。面向過程,面向?qū)ο?,不過是大部分人給與的標(biāo)簽,怎么用才重要。
Google天才工程師們使用一個(gè)稱為“htmlfile”的 ActiveX 解決了在 IE 中的加載顯示問題,具體是封裝了一個(gè)基于 iframe 和 htmlfile 的 JavaScript comet 對(duì)象,支持 IE、Mozilla Firefox 瀏覽器,但需要服務(wù)器端配合使用。
稍微熟悉一下有關(guān)Transfer-Encoding: chunked的同學(xué),會(huì)感覺一點(diǎn)技術(shù)含量都沒有。但那是他們的事情,笨鳥先飛,記錄下來,以作備忘。
我們做一個(gè)時(shí)間顯示,每隔一秒自動(dòng)顯示在頁面上。那么做這件事情的順序,就很簡單。
chunked塊傳輸,需要瀏覽器支持,服務(wù)器需要提前告訴瀏覽器端:
#define HTMLFILE_RESPONSE_HEADER \
"HTTP/1.1 200 OK\r\n" \
"Connection: keep-alive\r\n" \
"Content-Type: text/html; charset=utf-8\r\n" \
"Transfer-Encoding: chunked\r\n" \
"\r\n"......
write_ori(client, HTMLFILE_RESPONSE_HEADER);
在socket.io服務(wù)器中,數(shù)據(jù)量不大,傳輸內(nèi)容無須gzip壓縮,畢竟壓縮算法要耗費(fèi)一些CPU時(shí)間。
這部分不是必須的,為了調(diào)用客戶端javascript方便,可以提前定義好調(diào)用函數(shù)。
#define HTMLFILE_RESPONSE_FIRST \
"<html><head><title>htmlfile chunked example</title><script>var _ = function (msg) { document.getElementById('div').innerHTML = msg; };</script></head><body><div id=\"div\"></div>"...... char target_message[strlen(HTMLFILE_RESPONSE_FIRST) + 20]; sprintf(target_message, "%X\r\n%s\r\n", (int)strlen(HTMLFILE_RESPONSE_FIRST), HTMLFILE_RESPONSE_FIRST); write_ori(client, target_message);
除了http header頭部輸出,剩下內(nèi)容的輸出,需要注意輸出的簡單格式:
具體輸出內(nèi)容長度16進(jìn)制數(shù)字表示\r\n具體輸出內(nèi)容\r\n
如
2D <script>_('now time is 1364040943');</script>
掌握了格式要求之后,其它的,就沒有什么難點(diǎn)。
client->timeout.data = client;
ev_timer_init(&client->timeout, timeout_cb, 1.0, 1.0);
ev_timer_start(loop, &client->timeout);
時(shí)間觸發(fā)函數(shù)timeout_cb每一秒會(huì)定時(shí)觸發(fā):
static void timeout_cb(EV_P_ struct ev_timer *timer, int revents) { if (EV_ERROR & revents) {
fprintf(stderr, "error event in timer_beat\n");
return ; } if (timer == NULL) {
fprintf(stderr, "the timer is NULL now !\n"); } client_t *client = timer->data; if (client == NULL) {
fprintf(stderr, "Timeout the client is NULL !\n");
return; } char target_msg[50]; snprintf(target_msg, 50, "now time is %d", (int)ev_time()); write_body(client, target_msg); }
OK,基本功能完畢。
編譯一下:
gcc htmlfile.c -o htmlfile ../include/libev.a -lm
運(yùn)行它:
./htmlfile
打開瀏覽器,輸入地址 http://192.168.190.150:8080/htmlfile,可以看到時(shí)間一點(diǎn)點(diǎn)的流逝,諸如:
now time is 1364043695
當(dāng)你習(xí)慣了現(xiàn)有WEB服務(wù)器,諸如nginx、apache,JAVA應(yīng)用服務(wù)器Tomcat等,你就不能不注意HTTP請(qǐng)求的響應(yīng)超時(shí)時(shí)間,需要小心,尤其是反向代理時(shí)。當(dāng)你可以自由控制請(qǐng)求timeout超時(shí)時(shí),那是怎樣一個(gè)快意。
在libev中使用timeout,沒有像java那樣封裝的完善,一切都很原始,但確實(shí)鋒利多了。
一般長輪詢需要定義超時(shí)時(shí)間,一旦超時(shí),服務(wù)器端會(huì)主動(dòng)斷開連接。無論是xhr形式的長輪詢,還是jsonp長輪詢,在服務(wù)器端處理沒有多大差別,輸出數(shù)據(jù)有異。
一般優(yōu)先輸出頭部,告訴瀏覽器,需要保持長連接,當(dāng)然,這需要瀏覽器支持http 1.1協(xié)議,并且明確的注明當(dāng)前連接為一直保持著:keep-alive:
連接什么時(shí)候關(guān)閉,需要在代碼中手動(dòng)控制,除非瀏覽器端在發(fā)出請(qǐng)求等待響應(yīng)期間出現(xiàn)異常,無故斷開了連接。設(shè)服務(wù)器端設(shè)定好連接持續(xù)時(shí)間為30秒,那么就應(yīng)該啟動(dòng)一個(gè)定時(shí)器,除非所使用的語言層面提供了內(nèi)置支持。
定時(shí)器start之后,觸發(fā)的函數(shù)timeout_cb:
可以看到,定時(shí)器觸發(fā)之后,本例中將輸出一串預(yù)先定義好的文字,然后關(guān)閉連接。
如何關(guān)閉觸發(fā)器,則很簡單:
編譯一下:
gcc longpolling.c -o longpolling ../include/libev.a ../include/http-parser/http_parser.o -lm
運(yùn)行它:
./long_polling
然后測試:
curl -i http://192.168.190.150:9000/long_polling
可以先看到頭部:
HTTP/1.1 200 OK Content-Type: text/plain; charset=UTF-8 Connection: keep-alive
等到30秒后輸出具體的文字內(nèi)容:
The timeout(30s) had passed, you are welcome ~!
所演示的長輪詢,沒有什么難度,HTTP 1.1頭部輸出,定時(shí)器啟動(dòng),然后等待輸出。
libev內(nèi)含的timer組件簡單易用,控制方便,但不算是最佳實(shí)踐,官方文檔給出了若干種最佳實(shí)踐方式。具體可參閱:
http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#codeevtimercoderelativeandopti
在編
有關(guān)libev的文章,中文資料不多,英文資料也不多。這里推薦三篇:
-
-
- 官方文檔
這里把在編寫c_socket.io_server
程中使用libev的一些地方做些筆記,記錄下來,也方便以后查閱。
所有代碼的編寫、編譯、測試和運(yùn)行等,都在Ubuntu下進(jìn)行,另外實(shí)例嚴(yán)重依賴libev和
其它依賴,可以從
這里設(shè)計(jì)一個(gè)靜態(tài)文件WEB服務(wù)器,非常簡單,僅僅滿足socket.io服務(wù)器最基本的需求,因此別苛求太多。但比網(wǎng)上很多大把類似文章多了一點(diǎn)寫入管道時(shí)緩沖區(qū)已滿問題的處理。
這里簡單說一下處理靜態(tài)文件的思路。
char file_path[200];
sprintf(file_path, "%s%s", static_folder, url_str);
char file_path[200];
sprintf(file_path, "%s%s", static_folder, url_str);
int file = open(file_path, O_RDONLY);
struct stat info;
if (fstat(file, &info) == -1) {
fprintf(stderr, "the file %s is NULL\n", file_path);
write(client->fd, RESPONSE_404, strlen(RESPONSE_404));
close(file);
free_res(loop, client);
return 0;
}
char file_ext[50];
get_extension(file_path, file_ext);
char content_type[50];
get_content_type(file_ext, content_type);
int file_len = info.st_size;
char head_msg[200] = "";
sprintf(head_msg, RESPONSE_TEMPLATE, content_type, file_len);
write(client->fd, head_msg, strlen(head_msg));
很顯然,這里引入fcntl.h頭部文件,調(diào)用fstat初始化stat結(jié)構(gòu),可判斷文件是否存在,以及文件大小等。
int read_count;
int buf_size = 8 * 1024;//8096;
char buffer[buf_size + 1];
while ((read_count = read(file, buffer, buf_size)) > 0) {
int bytes_left = read_count;
char *ptr = buffer;
int need_break = 0;
while (bytes_left > 0) {
ssize_t write_len = write(client->fd, ptr, bytes_left);
if (write_len == -1) {
fprintf(stderr, "write failed(errno = %d): %s\n", errno, strerror(errno));
switch (errno) {
case EAGAIN:
case EINTR:
case EINPROGRESS:
fprintf(stderr, "now sleep 0.2s\n");
ev_sleep(0.2);
break;
default:
need_break = 1;
break;
}
} else if (write_len == 0) {
need_break = 1;
fprintf(stderr, "write_len is zero, and break now\n");
break;
} else if (write_len < bytes_left) {
bytes_left -= write_len;
ptr += write_len;
fprintf(stderr, "write client with something wrong wtih bytes_left = %d & write_len = %d and write the left data !\n", (int)bytes_left, (int)write_len);
} else {
break;
}
}
if (need_break) {
break;
}
}
close(file);
需要注意,構(gòu)造的一個(gè)大約8K+1的緩沖區(qū)buffer,不是每次都可以正常完整輸出到socket對(duì)端,write輸出不完整,會(huì)返回-1,系統(tǒng)返回errno值,在errno = EAGAIN,EINTR,EINPROGRESS時(shí),需要再次將緩沖區(qū)中尚未寫入的剩下數(shù)據(jù)再次寫入到請(qǐng)求端。這樣可以避免常見的半包、包不完整問題。
free_res(loop, client);
請(qǐng)求完成,一定要記得關(guān)閉socket描述符,釋放相應(yīng)資源等。 這樣一個(gè)較為完整的HTTP請(qǐng)求,靜態(tài)文件就處理完畢了。
先編譯:
gcc staticserver.c -o staticserver ../include/libev.a ../include/http-parser/http_parser.o -lm
運(yùn)行之:
./static_server ../static
命令輸入錯(cuò)誤,如輸入靜態(tài)路徑為空,會(huì)報(bào)錯(cuò)的,哈哈:
Error: invald path parmeter
Usage: ./static_server
Example:
./staticserver ../static
./staticserver /home/yongboy/yourstaticfolderEnjoy it~
測試一下吧
curl -i http://192.168.190.150:8000/index.html
在瀏覽器內(nèi),測試一下,支持圖片樣式等,完好顯示。
需要注意,要傳入靜態(tài)文件目錄路徑,相對(duì)的路徑,或絕對(duì)的路徑,都是可以接受的。 最后,附上完整代碼:
哈,這又是一個(gè)socket.io服務(wù)端實(shí)現(xiàn),本意是,拿C練練手,加強(qiáng)對(duì)C和linux系統(tǒng)的理解,寫著寫著,就寫成了一個(gè)socket.io服務(wù)器端實(shí)現(xiàn)了。以為半成品,那就正式托管在github站點(diǎn)上吧,以便記錄一下,可讓大家批評(píng)與指正,加強(qiáng)內(nèi)功的修煉等。
項(xiàng)目地址為
yongboy/c_socket.io_server
以下部分文字,偷懶,摘錄自項(xiàng)目的README.md文件
這是一個(gè)純C語言版本的socket.io服務(wù)器端實(shí)現(xiàn),目前僅支持linux系統(tǒng),嚴(yán)重依賴libev and glib等基礎(chǔ)庫。
在運(yùn)行socket.io_server之前,需要安裝以下依賴:
sudo apt-get install uuid-dev
sudo apt-get install libglib2.0-dev
對(duì)外的API,可以在頭文件 endpoint_impl.h 看到其定義,其繼承了另外一個(gè)公用的頭文件 endpoint.h, 其完整定義為:
完整定義.
在example目錄中,你可以看到聊天室演示 chatroom 和在線白板示范 whiteboard .
因?yàn)镃語言中沒有散列表,只好借助于成熟的glib庫實(shí)現(xiàn)。
項(xiàng)目不太成熟,期待大家的參與,您的建議、批評(píng)和指正,都是一種激勵(lì),再次表示感謝。
最近在學(xué)習(xí)Erlang,順便寫了一個(gè)socket.io server作為練練手,感受函數(shù)式/面向并發(fā)編程的好處。
毫無疑問,同樣兼容最新的socket.io spec 1.0。
無論哪一種語言,從頭開始構(gòu)建HTTP協(xié)議支持,都是很痛苦的,站在巨人肩上,總是可以讓你更專注于業(yè)務(wù)。Java社區(qū)可選擇netty,Erlang社區(qū)可以選擇非常輕量級(jí)的mochiweb和cowboy等,這里要感謝一下尤日華同學(xué)(http://www.cnblogs.com/yourihua/),特別熱心,一一給我們分析了以上兩個(gè)http框架的源碼,我等新手獲益良多。就是在其文章的幫助下,開始構(gòu)建一個(gè)socket.io erlang server。
mochiweb,有些歷史,已趨于穩(wěn)定,但目前不支持websocket協(xié)議。
cowboy,模塊/協(xié)議自由替換,使用二進(jìn)制傳輸基本保證了低內(nèi)存占用和快速傳輸,內(nèi)置非常贊的dispatch URL分發(fā)器,內(nèi)置對(duì)長連接的支持,目前使用的是0.6.1版本。推薦使用!
閑話少說,目前已經(jīng)釋出0.1版本,項(xiàng)目地址為:
https://code.google.com/p/erlang-scoketio/
Erlang 版本的socket.io服務(wù)器實(shí)現(xiàn)
檢出地址: https://erlang-scoketio.googlecode.com/svn/socket.io_cowboy
兼容 socket.io-spec(https://github.com/LearnBoost/socket.io-spec) 1.0
支持xhr-polling/jsonp-polling/htmlfile/websocket/flashsocket等通訊協(xié)議
支持CJK語言,UTF-8編碼下很少出現(xiàn)亂碼
現(xiàn)在可以作為0.1版本釋出,具有一個(gè)chat示范
檢出地址: https://erlang-scoketio.googlecode.com/svn/socket.io_mochiweb
兼容 socket.io-spec(https://github.com/LearnBoost/socket.io-spec) 1.0
僅支持xhr-polling/jsonp-polling/htmlfile等通訊協(xié)議
暫時(shí)精力有限,停止更新,假若有需要,可以進(jìn)一步有償商談
- 下載示范文件http://code.google.com/p/erlang-scoketio/downloads/detail?name=socket.io.zip
- 在Linux下解壓
- #unzip socket*.zip
- #cd socket.io
- #make
- #sh start.sh
- 打開瀏覽器,訪問http://yourip:8080/index.html
這里介紹一個(gè)聊天示范,看代碼,很簡單,也很短,才80行。
與Java相比,Erlang代碼顯得少多了。
剛?cè)腴T,項(xiàng)目代碼寫的有些草;您若慷慨,希望給些指點(diǎn),謝謝 :))
使用了EventBus(事件總線)方式可以很好的處理事件訂閱者/事件的發(fā)布者解耦,發(fā)布者不知道訂閱者,訂閱者只需要自身注冊(cè),等待通知便可。EventBus是一種簡單,高效,優(yōu)雅,良好的客戶端架構(gòu)方式。嗯,還好,javascritp本身支持函數(shù)作為參數(shù)進(jìn)行傳遞,要不還是很麻煩的。
構(gòu)建一個(gè)最簡單的EventBus javascript庫,也不難:
簡單不到40行代碼,提供了事件訂閱,事件取消,事件廣播/發(fā)布等,雖簡單,但已經(jīng)滿足最簡單的頁面端EventBus模型,可以一窺全貌了。
客戶端使用事件總線代碼:
看著和socket.io的客戶端使用方式有所類似,但socket.io的處理方式復(fù)雜多了,并且多了一些內(nèi)置的事件,這里不過是簡化了很多。
嗯,有空談一談JAVA是如何做到事件總線(EventBus)的。
針對(duì)JAVA開發(fā)者,socketio-netty是一個(gè)socket.io的服務(wù)器端選擇,又是目前兼容最新0.9+ – 1.0的JAVA服務(wù)器端實(shí)現(xiàn)。
從http://socket.io官網(wǎng)來看,最近版本升級(jí)趨于緩和,幾乎是沒修正一個(gè)Bug,小版本就增加一次。已經(jīng)是非常穩(wěn)定的版本了,可以真正使用了。
貌似國內(nèi)使用socket.io少之又少,可惜了,這么優(yōu)秀的全功能型實(shí)時(shí)推送實(shí)現(xiàn),小眾范圍內(nèi)被知曉。
嗯,就最近當(dāng)前項(xiàng)目修改做一些簡單記載。
有時(shí)間,會(huì)聊聊更具體的實(shí)時(shí)Web一些心得,以及更為具體的示范等。