在進行服務器端開發的時候需要考慮一些算法和性能問題,經過了幾年的開發,對這方面有了一些經驗,現在寫下來跟大家分享和討論。
我主要是在Linux下進行C語言的開發,所以後面的實現都是基於Linux操作系統並用C語言來講解。其它平台和語言需要考慮的問題是類似的只不過可能是實現細節上有一些差異,我盡量減少這些差異吧。注意一下講解的所有內容都是基於32位系統的開發!
服務器程序開發核心是穩定,在穩定的前提下需要考慮效率。其中主要的公共模塊是內存池和線程池。因為服務器程序一般都會長時間的運行,而且頻繁的進行創建和釋放內存的操作,這時如果使用系統的malloc和free方法,則會使系統中產生很多內存碎片,從而影響效率和穩定性。內存池的主要思想是先調用系統的malloc開辟一個很大的內存,然後對這個大內存進行管理,程序中要使用內存時,內存池分配內存,程序要釋放內存時,只是通知內存池,而不真正釋放內存。線程池,原理類似,並發處理一些任務的時候,需要使用很多線程,我們可以先創建很多線程,然後每次有任務需要處理,則找到一個空閒的線程讓它來處理任務,處理完成後線程掛起。這樣省去了每次一個任務都創建線程的時間開銷。
下面介紹一下在進行庫的封裝時用到的一些技巧。因為我們是使用C語言進行開發,所以為了使結構和效率得到很好的平衡,需要一些技巧來進行封裝,從而使程序在保證C語言效率的前提下足夠的模塊化。
首先介紹一下這個宏,這個宏是我在linux內核中看到的,它用於通過一個結構中的某個成員的指針,推算出整個結構的指針,先舉一個例子吧。例如有如下一個結構:
1: struct my_struct
2: {
3: int a;
4: int b;
5: char c;
6: short d;
7: };
看看下面的代碼:
1: int
2: main ()
3: {
4: struct my_struct mys; // 創建結構對象
5: struct my_struct* pmys; // 聲明一個指向我們的結構的指針
6: char* pc = &(mys.c); // pc指向結構中c成員
7: int* pb = &(mys.b); // pb指向結構中b成員
8: pmys = container_of (pc, struct my_struct, c); // pmys實際上指向mys結構
9: pmys = container_of (pb, struct my_struct, b); // pmys還是指向mys結構
10: // 上面兩行代碼與 pmys = &mys; 的功能一樣
11: ...
12: }
上面的代碼說明,通過調用container_of可以通過某個結構中某個成員的指針(如pc或者pb),獲得整個結構的指針。
這個宏需要三個參數,第一個參數是結構中某個成員的指針,第二個參數是結構的類型,第三個參數是,第一個參數中的成員指針指向的成員在結構的聲明中的成名名稱。這個宏定義如下:
1: #define offsetof(TYPE, MEMBER)((size_t) &((TYPE *)0)->MEMBER)
2:
3: #ifdef WIN32
4: // WIN32下(不支持類型安全檢查)
5: #define container_of(ptr, type, member) /
6: ((type*)(ptr)-offsetof(type, member))
7:
8: #else
9: // linux下
10: #define container_of(ptr, type, member)({/
11: const typeof( ((type *)0)->member) *_mptr = (ptr); /
12: (type *)((char*)_mptr-offsetof(type, member));})
13:
14: #endif /* */
在上面的實現中,首先定義了一個offsetof宏,這個宏用於計算結構中的某個成員的地址相對於這個結構的起始地址的偏移。WIN32下可以使用系統自帶offset宏。
container_of宏實際上是用結構中某個成員的地址減去這個成員相對於結構起始的偏移,就得到了結構的地址也就是指向這個結構的指針。linux下實現的時候:const typeof( ((type *)0)->member)* _mptr = (ptr);這句實際上是在做類型安全檢查。保證給宏的第一個參數成員指針的類型就是指向這個成員類型的指針。typeof是gcc擴展,用於獲得某個表達式的類型。在WIN32下我沒有找到typeof的代替品,所以沒有做類型安全檢查。
有了container_of宏,我們可以做些什麼工作呢?舉個例子,如果我們寫一個鏈表,教科書上一般將鏈表中的數據區域當作一個int。實際上可能是一個復雜的結構,而我作為編寫鏈表的人,不知道使用者會在鏈表中存儲什麼數據。通常的做法是,數據區域就是一個void*,這樣使用者可以用這個指針存儲任何對象了。如果使用者要存儲自己的數據,可以創建自定義結構的對象,然後用這個指針指向這個結構。鏈表中的每一個元素除了數據區占用的內存外還占用了一個void*.在服務器端開發時,數據量是很大的,這樣就浪費了很多的void*。我們可以通過如下的方法來解決這個問題。
定義鏈表中的元素結構:
1: struct link_item
2: {
3: struct link_item* next;
4: };
而提供的操作鏈表的方法都是基於struct link_item* 指針的方法。用戶在使用的時候聲明自己的鏈表元素結構:
1: struct user_link_item
2: {
3: struct link_item lk_item;
4: int my_data1;
5: short my_data2;
6: // ...
7: };
這個結構中擁有struct link_item類型的一個成員lk_item。我們在使用鏈表的時候,總是將lk_item的地址傳遞給鏈表的方法。獲取數據的時候獲得的也是指向struct link_item的指針,但是我們可以使用container_of宏用鏈表方法返回的struct link_item的指針推算出struct user_link_item的指針,這樣作為用戶就可以獲得存儲在鏈表中的數據了。這樣的實現中具體元素的內存也是外部使用者來分配的。
這種方法有些類似c++的繼承機制。遇到繼承的問題可以考慮使用這種方法。
在開發服務器程序的時候,我們經常是多線程的並行處理。對於要處理的數據就有一個線程安全的問題。最簡單的線程安全處理是原子操作。每一次處理都是使用一條指令完成。其次是使用線程鎖,進行線程同步。對於一個存儲在某個數據結構中的數據來說,可能會在某個線程進行讀取操作,而在另外的一個線程這個數據被刪除了。我們可以模仿WIN32的COM處理這個問題時使用的方法。增加一個引用計數器,每次要對數據進行訪問的時候都要遞增這個數據的引用計數器,當使用完成的時候,遞減這個數據的引用計數器。當引用計數器的值被遞減為0的時候,這個數據就會真正的被刪除。當然,引用計數器的遞增和遞減操作需要是線程安全的,完全可以使用原子操作來實現。
所有需要保證線程安全的數據對象,我們都可以通過上面講到的使用container_of宏的實現的繼承方式來實現。
服務器端通訊通常會設計一套協議。在協議中通常是如下形式:
|4字節表示後面的字節數|1字節表示某個標志|1字節表示某個標志|2字節表示某個標志|n個字節表示內容|
上面的這中協議,我們如何來將它轉換為一個結構呢?看看如下定義的結構:
1: // 錯誤的結構
2: struct wrong_struct
3: {
4: long size;
5: char f1;
6: char f2;
7: short f3;
8: char* content;
9: };
上面的結構明顯是錯誤的,主要是content成員。content是一個指向字符串的指針它覆蓋了協議中“n個字節表示內容”中的最前面的4個字節。如何解決這個問題呢?很簡單:
1: struct right_struct
2: {
3: long size;
4: char f1;
5: char f2;
6: short f3;
7: char content[1];
8: };
我們將content聲明為只有一個元素的字符串數組。字符串數組又是字符串指針,我們可以使用content成員來引用“n個字節表示內容”了。
當然這設計通訊協議的時候一定要考慮到字節對齊,關於字節對齊,這裡就不再詳細介紹了,可以參考一些C/C++的資料。