fastCGI研究記錄

Just4life發表於2013-07-23

本來構思的OJ是在將前端放在虛擬空間上,在自己的機器上開Judge服務,通過動態DNS來連線。可是自己電腦就得一直開著,不好辦。最後還是想租一臺主機,如果經濟不允許的話就買個便宜的VPS。可是VPS最大的問題是記憶體一超就當機。特別是執行JVM之類特別消耗內在的程式。而且OJ也只是一個CMS,前端主要處理資料的顯示,沒有過多複雜的業務邏輯,也不需要複雜的關係模型。為了簡化應用的架構,打算採用C/C++來編寫前臺和後臺,將它們都放在VPS上執行。

用C/C++來寫Web應用,想到就是CGI,可是CGI的效能和效率也不好,特別是處理併發的時候。雖然不熱門的OJ訪問量不會很大,但為了給以後有一點點的擴充套件空間,還是希望它支援併發,至少也可以多執行緒處理請求。便選擇了fastCGI。

fastCGI並不是什麼新技術,上個世紀已出現,而且很多企業都多得很成熟,特別是在電信業和要求資訊處理效率比較高的行業中使用很廣泛。因為程式都是編譯成原生程式碼,執行的效率要比Java,.NET,PHP等都高點,而且佔用的資源可以更加少。應用的結構也比較簡單所以程式設計的難度也不高,只要開發前做一個整體設計,把模組分清楚,要使用類似MVC的模型也完全可以做到。

在fastCGI裡的開發庫裡(可以在http://www.fastcgi.com/drupal/node/5下載),有兩種API可以使用,一種是包含標頭檔案“fcgi_stdio.h”,可以直接通過標準輸出流進行response資料的輸出;而另一個“fcgiapp.h”則通過自身的特定函式來輸出資料。我選擇後者,因為前者在使用時一定要保證在程式中引用的庫裡都沒有“stdio.h”,否則容易出現IO錯誤。而且前者也不支援多執行緒,所以只能選擇後者了。下面的內容都是使用“fcgiapp.h”。

fastCGI程式的一般模式就是一個請求迴圈,因為fastCGI需要有守護程式,程式執行之後就像服務一樣,一直在等待請求,處理請求,返回response,然後又等待請求……如此一直迴圈,迴圈體就類似於。

while (FCGX_Accept(&in, &out, &err, &envp) >= 0) 
{        
     char *contentLength = FCGX_GetParam("CONTENT_LENGTH", envp); 
     int len = 0;   
     FCGX_FPrintF(out, 
                 "Content-type: text/htmlrn" 
                 "rn"
                 "<title>FastCGI echo (fcgiapp version)</title>"
                 "<h1>FastCGI echo (fcgiapp version)</h1>n" 
                 "Request number %d,  Process ID: %d<p>n", ++count, getpid());
} /* while */

但是上面的倒子沒法處理多執行緒,下面是我在Windows環境下寫的可以處理多執行緒的fastCGI的例子。例子中使用了互斥鎖,對一個計數器進行累加,避免多執行緒同時讀寫同一個資源而導致資料出錯或不同步。

#include <windows.h> 
#include "fcgi/fcgi_config.h"
#include "fcgi/fcgiapp.h"

#pragma comment(lib, "libfcgi.lib") 

#define THREAD_COUNT 20 

DWORD WINAPI foo(LPVOID params);
static void PrintEnv(FCGX_Stream *out, char *label, char **envp); 
static HANDLE accept_mutex = CreateMutex(NULL, FALSE, NULL);
static HANDLE count_mutex = CreateMutex(NULL, FALSE, NULL); 
static int count = 0; 

int _tmain(int argc, _TCHAR* argv[]) 
{
     HANDLE tid[THREAD_COUNT];
     FCGX_Init();

     for (int i = 1; i < THREAD_COUNT; i++) 
     {		
          tid[i] = CreateThread(NULL, 0, foo, (LPVOID) i, 0, NULL);	
     } 	

     foo(0);     

     return 0;
}

 static void PrintEnv(FCGX_Stream *out, char *label, char **envp) 
{
     FCGX_FPrintF(out, "%s:<br>n<pre>n", label);    

     for( ; *envp != NULL; envp++) 
     {      
          FCGX_FPrintF(out, "%sn", *envp);    
     }    

     FCGX_FPrintF(out, "</pre><p>n");
} 

DWORD WINAPI foo(LPVOID params) 
{
     int rc;	

     FCGX_Request request;	
     FCGX_InitRequest(&request, 0, 0);	

     char* server_name;	
  
     int tid = (int) params;	

     unsigned long id = GetCurrentThreadId(); 	

     while (1)
     {
           WaitForSingleObject(accept_mutex, INFINITE);		
           rc = FCGX_Accept_r(&request);
           ReleaseMutex(accept_mutex); 		

           if (rc < 0)
           {			
               break;		
           } 		

           server_name = FCGX_GetParam("SERVER_NAME", request.envp);         
           FCGX_FPrintF(request.out,            
                        "Content-type: text/htmlrn"
                        "rn"            
                        "<h1>"			
                        "THREAD_ID: %d<br/>"			
                        "CurrentThreadId: %u<br/>"		
                	"Server Name: %s<br/>"			
                        "</h1>",            
                        tid, id, server_name ? server_name : "?"); 		

           Sleep(2); 		
           
           WaitForSingleObject(count_mutex, INFINITE);		

           ++count;		

           FCGX_FPrintF(request.out, "%d<br/>", count);	

   	   ReleaseMutex(count_mutex); 		

           PrintEnv(request.out, "Request environment", request.envp);        

           PrintEnv(request.out, "Initial environment", environ); 		

           FCGX_Finish_r(&request);	
     } 	

     return 0;
}

經過適當的封裝和改造就可以變成一個控制器。

要執行fastCGI程式,首先要有一個支援fastCGI反向代理的伺服器,如Apache,Nginx,Lighttpd等。能夠支援fastCGI的伺服器的詳細列表可以檢視http://www.fastcgi.com/drupal/node/3

但是有伺服器的支援還不夠,因為fastCGI需要作為守護程式來執行才可以正常被訪問。因此還需要將編譯好的fastCGI程式繫結到指定的埠,並一直監聽才行。而能夠完成這項工作的工具可以選擇fastCGI開發庫中的cgi-fcgi程式,該程式只提供原始碼,可以在Windows和Unix環境下編譯,編譯的時候注意去掉不屬性編譯環境下的標頭檔案,以避免出現找不到標頭檔案的編譯錯誤。如在Windows下編譯,就應該去掉包括sys/param.h,sys/time.h和unistd.h。在編譯時還得上libfcgi.lib。

其實lighttpd專案也有一個類似的程式spawn-fcgi,也可以在Windows和Unix下平臺編譯執行。關於cgi-fcgi和spawn-fcgi在Linux下的使用可以檢視http://stackoverflow.com/questions/2149709/c-language-fastcgi-with-nginx

發現上面連結中說明的方法在Windows下有所不同。可以是不同系統對套介面支援的差異。下面是在Windows下,使用Nginx伺服器來啟動fastCGI的方法。

首先配置Nginx,在nginx.conf檔案中加入下面的配置引數,就是將字首為“http://host/cgi-bin”的請求反向代理到9000埠進行處理:


location /cgi-bin { 
     fastcgi_pass 127.0.0.1:9000;
     include fastcgi_params; 
}
然後啟動Nginx服務程式,在執行下面的命令即可啟動fastCGI程式

cgi-fcgi -start -connect localhost:9000 testcgi.exe

通過瀏覽器訪問http://host/cgi-bin,即fastCGI程式進行請求