背景知識Web服務器使用HTTP協(xié)議與客戶端(即瀏覽器)通信,而HTTP協(xié)議又基于TCP/IP協(xié)議。因此我們" />

国产成人精品无码青草_亚洲国产美女精品久久久久∴_欧美人与鲁交大毛片免费_国产果冻豆传媒麻婆精东

15158846557 在線咨詢 在線咨詢
15158846557 在線咨詢
所在位置: 首頁 > 營銷資訊 > 網(wǎng)站運營 > 用C語言制作Web服務器

用C語言制作Web服務器

時間:2023-05-19 06:09:01 | 來源:網(wǎng)站運營

時間:2023-05-19 06:09:01 來源:網(wǎng)站運營

用C語言制作Web服務器:本文,我們將使用C語言從零開始實現(xiàn)一個支持靜態(tài)、動態(tài)網(wǎng)頁的Web服務器。我們把這個服務器叫做Tiny。

背景知識

Web服務器使用HTTP協(xié)議與客戶端(即瀏覽器)通信,而HTTP協(xié)議又基于TCP/IP協(xié)議。因此我們要做的工作就是利用Linux系統(tǒng)提供的TCP通信接口來實現(xiàn)HTTP協(xié)議。

而Linux為我們提供了哪些網(wǎng)絡(luò)編程接口呢?沒錯,就是socket(套接字),我們會在后面詳細介紹該接口的使用方式。

另外我們應該清楚Linux的系統(tǒng)I/O和文件系統(tǒng)的關(guān)系。在Linux中,所有I/O設(shè)備都被看作一個個文件,I/O設(shè)備的輸入輸出被認做讀寫文件。網(wǎng)絡(luò)作為一種I/O設(shè)備,同樣被看作文件,而且是一類特殊的文件,即套接字文件。

我們還要對網(wǎng)絡(luò)通信協(xié)議TCP/IP有一個大致的了解,知道IP地址和端口的作用。

接下來我們講解客戶端-服務器編程模型。

客戶端-服務器編程模型

客戶端-服務器編程模型是一個典型的進程間通信模型??蛻舳诉M程和服務器進程通常分處兩個不同的主機,如下圖所示,客戶端發(fā)送請求給服務器,服務器從本地資源庫中查找需要的資源,然后發(fā)送響應給客戶端,最后客戶端(通常是瀏覽器)處理這個響應,把結(jié)果顯示在瀏覽器上。

這個過程看起來很簡單,但是我們需要深入具體的實現(xiàn)細節(jié)。我們知道,TCP是基于連接的,需要先建立連接才能互相通信。在Linux中,socket為我們提供了方便的解決方案。

每一對網(wǎng)絡(luò)連接稱為一個socket對,包括兩個端點的socket地址,表示如下

(cliaddr : cliport, servaddr : servport)其中, cliaddrcliport分別是客戶端IP地址和客戶端端口號,servaddrservport分別是服務器IP地址和服務器端口。舉例說明如下:

這對地址和端口唯一確定了連接的雙方,在TCP/IP協(xié)議網(wǎng)絡(luò)中就能輕松地找到對方。

使用socket處理請求與響應

熟悉TCP協(xié)議的朋友們應該很容易理解下面的流程圖。

服務器調(diào)用socket函數(shù)獲取一個socket,然后調(diào)用bind函數(shù)綁定本機的IP地址和端口,再調(diào)用listen函數(shù)開啟監(jiān)聽,最后調(diào)用accept函數(shù)等待直到有客戶端發(fā)起連接。

另一方面,客戶端調(diào)用socket函數(shù)獲取一個socket,然后調(diào)用connect函數(shù)向指定服務器發(fā)起連接請求,當連接成功或出現(xiàn)錯誤后返回。若連接成功,服務器端的accept函數(shù)也會成功返回,返回另一個已連接的socket(不是最初調(diào)用socket函數(shù)得到的socket),該socket可以直接用于與客戶端通信。而服務器最初的那個socket可以繼續(xù)循環(huán)調(diào)用accept函數(shù),等待下一次連接的到來。

連接成功后,無論是客戶端還是服務器,只要向socket讀寫數(shù)據(jù)就可以實現(xiàn)與對方socket的通信。圖中rio_readlinebrio_written是作者封裝的I/O讀寫函數(shù),與Linux系統(tǒng)提供的readwrite作用基本相同,詳細介紹見參考資料。

客戶端關(guān)閉連接時會發(fā)送一個EOF到服務器,服務器讀取后關(guān)閉連接,進入下一個循環(huán)。

這里面用到的所有Linux網(wǎng)絡(luò)編程接口都定義在<sys/socket.h>頭文件中,為了更清晰地幫助大家理解每個函數(shù)的使用方法,我們列出它們的函數(shù)聲明。

#include <sys/types.h>#include <sys/socket.h>/**獲取一個socket descriptor@params: domain: 此處固定使用AF_INET type: 此處固定使用SOCK_STREAM protocol: 此處固定使用0@returns: nonnegative descriptor if OK, -1 on error.*/int socket(int domain, int type, int protocol);/**客戶端socket向服務器發(fā)起連接@params: sockfd: 發(fā)起連接的socket descriptor serv_addr: 連接的目標地址和端口 addrlen: sizeof(*serv_addr)@returns: 0 if OK, -1 on error*/int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);/**服務器socket綁定地址和端口@params: sockfd: 當前socket descriptor my_addr: 指定綁定的本機地址和端口 addrlen: sizeof(*my_addr)@returns: 0 if OK, -1 on error*/int bind(int sockfd, struct sockaddr *my_addr, int addrlen);/**將當前socket轉(zhuǎn)變?yōu)榭梢员O(jiān)聽外部連接請求的socket@params: sockfd: 當前socket descriptor backlog: 請求隊列的最大長度@returns: 0 if OK, -1 on error*/int listen(int sockfd, int backlog);/**等待客戶端請求到達,注意,成功返回得到的是一個新的socket descriptor,而不是輸入?yún)?shù)listenfd。@params: listenfd: 當前正在用于監(jiān)聽的socket descriptor addr: 客戶端請求地址(輸出參數(shù)) addrlen: 客戶端請求地址的長度(輸出參數(shù))@returns: 成功則返回一個非負的connected descriptor,出錯則返回-1*/int accept(int listenfd, struct sockaddr *addr, int *addrlen);

HTTP協(xié)議與靜/動態(tài)網(wǎng)頁

HTTP協(xié)議的具體內(nèi)容在此不再講述,不熟悉的朋友們可以查看參考資料中的第二篇文章。

現(xiàn)在我們有必要說明一下所謂的靜態(tài)網(wǎng)頁和動態(tài)網(wǎng)頁。靜態(tài)網(wǎng)頁是指內(nèi)容固定的網(wǎng)頁,通常是事先寫好的html文檔,每次訪問得到的都是相同的內(nèi)容。而動態(tài)網(wǎng)頁是指多次訪問可以得到不同內(nèi)容的網(wǎng)頁,現(xiàn)在流行的動態(tài)網(wǎng)頁技術(shù)有JavaScript、PHP、JSP、ASP等。我們將要實現(xiàn)的服務器同時支持靜態(tài)網(wǎng)頁和動態(tài)網(wǎng)頁,但動態(tài)網(wǎng)頁并不采用上述幾種技術(shù)實現(xiàn),而是使用早期流行的CGI(Common Gateway Interface)。CGI是一種動態(tài)網(wǎng)頁標準,規(guī)定了外部應用程序(CGI程序)如何與Web服務器交換信息,但由于有許多缺點,現(xiàn)在幾乎已經(jīng)被淘汰。不過,由于歷史原因,本文介紹的服務器底層仍然使用CGI實現(xiàn)。

關(guān)鍵代碼解析

Web服務器主進程從main函數(shù)開始,代碼如下。

int main(int argc, char **argv) { int listenfd, connfd; socklen_t clientlen; struct sockaddr_storage clientaddr; /* Check command line args */ if (argc != 2) { fprintf(stderr, "usage: %s <port>/n", argv[0]); exit(1); } listenfd = Open_listenfd(argv[1]); while (1) { clientlen = sizeof(clientaddr); connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); doit(connfd); Close(connfd); }}主函數(shù)參數(shù)需要傳入服務器綁定的端口號碼,得到這個號碼后,調(diào)用Open_listenfd函數(shù),該函數(shù)完成socket、bind、listen等一系列操作。接著調(diào)用accept函數(shù)等待客戶端請求。注意,Acceptaccept的包裝函數(shù),用來自動處理可能發(fā)生的異常,我們只需把它們當成一樣的就行了。當accept成功返回后,我們拿到了connected socket descriptor,然后調(diào)用doit函數(shù)處理請求。

doit函數(shù)定義如下。

void doit(int fd) { int is_static; struct stat sbuf; char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE]; char filename[MAXLINE], cgiargs[MAXLINE]; rio_t rio; /* Read request line and headers */ Rio_readinitb(&rio, fd); if (!Rio_readlineb(&rio, buf, MAXLINE)) return; printf("%s", buf); sscanf(buf, "%s %s %s", method, uri, version); if (strcasecmp(method, "GET")) { clienterror(fd, method, "501", "Not Implemented", "Tiny does not implement this method"); return; } read_requesthdrs(&rio); /* Parse URI from GET request */ is_static = parse_uri(uri, filename, cgiargs); if (stat(filename, &sbuf) < 0) { clienterror(fd, filename, "404", "Not found", "Tiny couldn't find this file"); return; } if (is_static) { /* Serve static content */ if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) { clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't read the file"); return; } serve_static(fd, filename, sbuf.st_size); } else { /* Serve dynamic content */ if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) { clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't run the CGI program"); return; } serve_dynamic(fd, filename, cgiargs); }}為了更接近現(xiàn)實,假設(shè)現(xiàn)在接收到的HTTP請求如下。該請求的請求頭是空的。

GET /cgi-bin/adder?15000&213 HTTP/1.0代碼中,Rio_readlinebsscanf負責讀入請求行并解析出請求方法、請求URI和版本號。接下來調(diào)用parse_uri函數(shù),該函數(shù)利用請求uri得到訪問的文件名、CGI參數(shù),并返回是否按照靜態(tài)網(wǎng)頁處理。如果是,則調(diào)用serve_static函數(shù)處理,否則調(diào)用serve_dynamic函數(shù)處理。

serve_static函數(shù)定義如下。

void serve_static(int fd, char *filename, int filesize) { int srcfd; char *srcp, filetype[MAXLINE], buf[MAXBUF]; /* Send response headers to client */ get_filetype(filename, filetype); sprintf(buf, "HTTP/1.0 200 OK/r/n"); sprintf(buf, "%sServer: Tiny Web Server/r/n", buf); sprintf(buf, "%sConnection: close/r/n", buf); sprintf(buf, "%sContent-length: %d/r/n", buf, filesize); sprintf(buf, "%sContent-type: %s/r/n/r/n", buf, filetype); Rio_writen(fd, buf, strlen(buf)); printf("Response headers:/n"); printf("%s", buf); /* Send response body to client */ srcfd = Open(filename, O_RDONLY, 0); srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0); Close(srcfd); Rio_writen(fd, srcp, filesize); Munmap(srcp, filesize);}直接看最后幾行代碼。Open以只讀方式打開請求的文件,Mmap將該文件直接讀取到虛擬地址空間中的任意位置,然后關(guān)閉文件。接下來Rio_written把內(nèi)存中的文件寫入fd指定的connected socket descriptor,靜態(tài)頁面響應完成。Munmap刪除剛才在虛擬地址空間申請的內(nèi)存。關(guān)于mmap函數(shù)的更多介紹見參考資料。

serve_dynamic函數(shù)定義如下。

void serve_dynamic(int fd, char *filename, char *cgiargs) { char buf[MAXLINE], *emptylist[] = { NULL }; /* Return first part of HTTP response */ sprintf(buf, "HTTP/1.0 200 OK/r/n"); Rio_writen(fd, buf, strlen(buf)); sprintf(buf, "Server: Tiny Web Server/r/n"); Rio_writen(fd, buf, strlen(buf)); if (Fork() == 0) { /* Child */ /* Real server would set all CGI vars here */ setenv("QUERY_STRING", cgiargs, 1); Dup2(fd, STDOUT_FILENO); /* Redirect stdout to client */ Execve(filename, emptylist, environ); /* Run CGI program */ } Wait(NULL); /* Parent waits for and reaps child */}對于動態(tài)網(wǎng)頁請求,我們的方法是創(chuàng)建一個子進程,在子進程中執(zhí)行CGI程序??创a,Fork函數(shù)創(chuàng)建子進程,熟悉Linux進程的朋友們應該知道,該函數(shù)會返回兩次,一次在父進程中返回,返回值不等于0,另一次在子進程中返回,返回值為0,因此if判斷內(nèi)部是子進程執(zhí)行的代碼。首先設(shè)置環(huán)境變量,用于把請求參數(shù)傳遞給CGI程序。接下來調(diào)用Dup2函數(shù)將標準輸出重定向到connected socket descriptor,這樣一來使用標準輸出輸出的內(nèi)容將會直接發(fā)送給客戶端。然后調(diào)用Execve函數(shù)在子進程中執(zhí)行filename指定的CGI程序。最后在父進程中調(diào)用了Wait函數(shù)用于收割子進程,當子進程終止后該函數(shù)才會返回。因此該Web服務器不能同時處理多個訪問,只能一個一個處理。

我們給出了一個CGI程序的實例adder,用于計算兩個參數(shù)之和。代碼如下。

/* * adder.c - a minimal CGI program that adds two numbers together */int main(void) { char *buf, *p; char arg1[MAXLINE], arg2[MAXLINE], content[MAXLINE]; int n1=0, n2=0; /* Extract the two arguments */ if ((buf = getenv("QUERY_STRING")) != NULL) { p = strchr(buf, '&'); *p = '/0'; strcpy(arg1, buf); strcpy(arg2, p+1); n1 = atoi(arg1); n2 = atoi(arg2); } /* Make the response body */ sprintf(content, "Welcome to add.com: "); sprintf(content, "%sTHE Internet addition portal./r/n<p>", content); sprintf(content, "%sThe answer is: %d + %d = %d/r/n<p>", content, n1, n2, n1 + n2); sprintf(content, "%sThanks for visiting!/r/n", content); /* Generate the HTTP response */ printf("Connection: close/r/n"); printf("Content-length: %d/r/n", (int)strlen(content)); printf("Content-type: text/html/r/n/r/n"); printf("%s", content); fflush(stdout); exit(0);}這段代碼就非常簡單了,從環(huán)境變量中取出請求參數(shù),得到兩個加數(shù)的值,相加后輸出。需要注意的是,由于剛才已經(jīng)重定向標準輸出,因此使用printf就可以把內(nèi)容輸出給客戶端。輸出內(nèi)容需要遵照HTTP協(xié)議的格式,才能在瀏覽器中正確顯示出來。

實驗效果與源碼

輸入如下命令啟動Web服務器,并綁定8000端口:

./tiny 8000靜態(tài)網(wǎng)頁效果:訪問http://localhost:8000

動態(tài)網(wǎng)頁效果:訪問http://localhost:8000/cgi-bin/adder?1&2

至此,我們的Web服務器終于大功告成。大家可以下載源碼,并在自己的計算機上部署測試。

參考資料

Linux IO操作詳解——RIO包 金樽對月的成長腳步
深入理解HTTP協(xié)議 micro36
CGI與Servlet的比較 YTTCJJ
我所了解的cgi 撣塵
Linux內(nèi)存管理之mmap詳解 heavent2010

關(guān)鍵詞:服務,語言

74
73
25
news

版權(quán)所有? 億企邦 1997-2025 保留一切法律許可權(quán)利。

為了最佳展示效果,本站不支持IE9及以下版本的瀏覽器,建議您使用谷歌Chrome瀏覽器。 點擊下載Chrome瀏覽器
關(guān)閉