C 語言實現一個簡單的 web 伺服器的原理解析
說到 web 伺服器想必大多數人首先想到的協議是 http,那麼 http 之下則是 tcp,本篇文章將通過 tcp 來實現一個簡單的 web 伺服器。
本篇文章將著重講解如何實現,對於 http 與 tcp 的概念本篇將不過多講解。
一、瞭解 Socket 及 web 服務工作原理
既然是基於 tcp 實現 web 伺服器,很多學習 C 語言的小夥伴可能會很快的想到套接字 socket。socket 是一個較為抽象的通訊程序,或者說是主機與主機進行資訊互動的一種抽象。socket 可以將資料流送入網路中,也可以接收資料流。
socket 的資訊互動與本地檔案資訊的讀取從表面特徵上看類似,但其中所存在的編寫複雜度是本地 IO 不能比擬的,但卻有相似點。在 win 下 socket 的互動互動步驟為:WSAStartup 進行初始化--> socket 建立套接字--> bind 繫結--> listen 監聽--> connect 連線--> accept 接收請求--> send/recv 傳送或接收資料--> closesocket 關閉 socket--> WSACleanup 最終關閉。
瞭解完了一個 socket 的基本步驟後我們瞭解一下一個基本 web 請求的使用者常規操作,操作分為:開啟瀏覽器-->輸入資源地址 ip 地址-->得到資源。當目標伺服器接收到該操作產生掉請求後,我們可以把伺服器的響應流程步驟看為:獲得 request 請求-->得到請求關鍵資料-->獲取關鍵資料-->傳送關鍵資料。伺服器的這一步流程是在啟動socket 進行監聽後才能響應。通過監聽得知接收到請求,使用 recv 接收請求資料,從而根據該引數得到進行資源獲取,最後通過 send 將資料進行返回。
二、建立sokect完成監聽
2.1 WSAStartup初始化
首先在c語言標頭檔案中引入依賴 WinSock2.h:
#include <WinSock2.h>
在第一點中對 socket 的建立步驟已有說明,首先需要完成 socket 的初始化操作,使用函式 WSAStartup,該函式的原型為:
int WSAStartup( WORD wVersionRequired,LPWSADATA lpWSAData );
該函式的引數 wVersionRequired 表示 WinSock2 的版本號;lpWSAData 引數為指向 WSADATA 的指標,WSADATA 結構用於 WSAStartup 初始化後返回的資訊。
wVersionRequired 可以使用 MAKEWORD 生成,在這裡可以使用版本 1.1 或版本2.2,1.1 只支援 TCP/IP,版本 2.1 則會有更多的支援,在此我們選擇版本 1.1。
首先宣告一個 WSADATA 結構體 :
WSADATA wsaData;
隨後傳參至初始化函式 WSAStartup 完成初始化:
WSAStartup(MAKEWORD(1,1),&wsaData)
WSAStartup 若初始化失敗則會返回非0值:
if (WSAStartup(MAKEWORD(1,&wsaData) != 0) { exit(1); }
2.2 建立socket 套接字
初始化完畢後開始建立套接字,套接字建立使用函式,函式原型為:
SOCKET WSAAPI socket( int af,int type,int protocol );
在函式原型中,af 表示 IP 地址型別,使用 PF_INET 表示 IPV4,type 表示使用哪種通訊型別,例如 SOCK_STREAM 表示 TCP,protocol 表示傳輸協議,使用 0 會根據前 2 個引數使用預設值。
int skt = socket(PF_INET,SOCK_STREAM,0);
建立完 socket 後,若為 -1 表示建立失敗,進行判斷如下:
if (skt == -1) { return -1; }
2.3 繫結伺服器
建立完 socket 後需要對伺服器進行繫結,配置埠資訊、IP 地址等。 首先檢視 bind 函式需要哪一些引數,函式原型如下:
int bind( SOCKET socket,const sockaddr *addr,int addrlen );
引數 socket 表示繫結的 socket,傳入 socket 即可;addr 為 sockaddr_in 的結構體變數的指標,在 sockaddr_in 結構體變數中配置一些伺服器資訊;addrlen 為 addr 的大小值。
通過 bind 函式原型得知了我們所需要的資料,接下來建立一個 sockaddr_in 結構體變數用於配置伺服器資訊:
struct sockaddr_in server_addr;
隨後配置地址家族為AF_INET對應TCP/IP:
server_addr.sin_family = AF_INET;
接著配置埠資訊:
server_addr.sin_port = htons(8080);
再指定 ip 地址:
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
ip 地址若不確定可以手動輸入,最後使用神器 memset 初始化記憶體,完整程式碼如下:
//配置伺服器 struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8080); server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); memset(&(server_addr.sin_zero),'\0',8);
隨後使用 bind 函式進行繫結且進行判斷是否繫結成功:
//繫結 if (bind(skt,(struct sockaddr *)&server_addr,sizeof(server_addr)) == -1) { return -1; }
2.4 listen進行監聽
繫結成功後開始對埠進行監聽。檢視 listen 函式原型:
int listen( int sockfd,int backlog )
函式原型中,引數 sockfd 表示監聽的套接字,backlog 為設定核心中的某一些處理(此處不進行深入講解),直接設定成 10 即可,最大上限為 128。使用監聽並且判斷是否成功程式碼為:
if (listen(skt,10) == -1 ) { return -1; }
此階段完整程式碼如下:
#include <WinSock2.h> #include<stdio.h> int main(){ //初始化 WSADATA wsaData; if (WSAStartup(MAKEWORD(1,&wsaData) != 0) { exit(1); } //socket建立 int skt = socket(PF_INET,0); if (skt == -1) { return -1; } //配置伺服器 struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8080); server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); memset(&(server_addr.sin_zero),8); //繫結 if (bind(skt,sizeof(server_addr)) == -1){ return -1; } //監聽 if (listen(skt,10) == -1 ) { return -1; } printf("Listening ... ...\n"); }
執行程式碼可得知程式碼無錯誤,並且輸出 listening:
2.5 獲取請求
監聽完成後開始獲取請求。受限需要使用 accept 對套接字進行連線,accept 函式原型如下:
int accept( int sockfd,struct sockaddr *addr,socklen_t *addrlen );
引數 sockfd 為指定的套接字;addr 為指向 struct sockaddr 的指標,一般為客戶端地址;addrlen 一般設定為設定為 sizeof(struct sockaddr_in) 即可。程式碼為:
struct sockaddr_in c_skt; int s_size=sizeof(struct sockaddr_in); int access_skt = accept(skt,(struct sockaddr *)&c_skt,&s_size);
接下來開始接受客戶端的請求,使用recv函式,函式原型為:
ssize_t recv( int sockfd,void *buf,size_t len,int flags )
引數 sockfd 為 accept 建立的通訊;buf 為快取,資料存放的位置;len 為快取大小;flags 一般設定為0即可:
//獲取資料 char buf[1024]; if (recv(access_skt,buf,1024,0) == -1) { exit(1); }
此時我們再到 accpt 和 recv 外層新增一個迴圈,使之流程可重複:
while(1){ //建立連線 printf("Listening ... ...\n"); struct sockaddr_in c_skt; int s_size=sizeof(struct sockaddr_in); int access_skt = accept(skt,&s_size); //獲取資料 char buf[1024]; if (recv(access_skt,0) == -1) { exit(1); } }
並且可以在瀏覽器輸入 127.0.0.1:8080 將會看到客戶端列印了 listening 新建了連結:
我們新增printf語句可檢視客戶端請求:
while(1){ //建立連線 printf("Listening ... ...\n"); struct sockaddr_in c_skt; int s_size=sizeof(struct sockaddr_in); int access_skt = accept(skt,0) == -1) { exit(1); } printf("%s",buf); }
接下來我們對請求頭進行對應的操作。
2.6 請求處理層編寫
得到請求後開始編寫處理層。繼續接著程式碼往下寫沒有層級,編寫一個函式名為 req,該函式接收請求資訊與一個建立好的連線為引數:
void req(char* buf,int access_socket) { }
然後先在 while 迴圈中傳遞需要的值:
req(buf,access_skt);
接著開始編寫 req 函式,首先在 req 函式中標記當前目錄下:
char arguments[BUFSIZ]; strcpy(arguments,"./");
隨後分離出請求與引數:
char command[BUFSIZ]; sscanf(request,"%s%s",command,arguments+2);
接著我們標記一些頭元素:
char* extension = "text/html"; char* content_type = "text/plain"; char* body_length = "Content-Length: ";
接著獲取請求引數,若獲取 index.html,就獲取當前路徑下的該檔案:
FILE* rfile= fopen(arguments,"rb");
獲取檔案後表示請求 ok,我們先返回一個 200 狀態:
char* head = "HTTP/1.1 200 OK\r\n"; int len; char ctype[30] = "Content-type:text/html\r\n"; len = strlen(head);
接著編寫一個傳送函式 send_:
int send_(int s,char *buf,int *len) { int total; int bytesleft; int n; total=0; bytesleft=*len; while(total < *len) { n = send(s,buf+total,bytesleft,0); if (n == -1) { break; } total += n; bytesleft -= n; } *len = total; return n==-1?-1:0; }
send 函式功能並不難在此不再贅述,就是一個遍歷傳送的邏輯。隨後傳送 http 響應與檔案型別:
send_(send_to,head,&len); len = strlen(ctype); send_(send_to,ctype,&len);
隨後獲得請求檔案的描述,需要新增標頭檔案#include <sys/stat.h>
使用fstat,且向已連線的通訊發生必要的資訊 :
//獲取檔案描述 struct stat statbuf; char read_buf[1024]; char length_buf[20]; fstat(fileno(rfile),&statbuf); itoa( statbuf.st_size,length_buf,10 ); send(client_sock,body_length,strlen(body_length),0); send(client_sock,strlen(length_buf),0); send(client_sock,"\n",1,"\r\n",2,0);
最後傳送資料:
//·資料傳送 char read_buf[1024]; len = fread(read_buf,statbuf.st_size,rfile); if (send_(client_sock,read_buf,&len) == -1) { printf("error!"); }
最後訪問地址 http://127.0.0.1:8080/index.html,得到當前目錄下 index.html 檔案資料,並且在瀏覽器渲染:
所有程式碼如下:
#include <WinSock2.h> #include<stdio.h> #include <sys/stat.h> int send_(int s,int *len) { int total; int bytesleft; int n; total=0; bytesleft=*len; while(total < *len) { n = send(s,0); if (n == -1) { break; } total += n; bytesleft -= n; } *len = total; return n==-1?-1:0; } void req(char* request,int client_sock) { char arguments[BUFSIZ]; strcpy(arguments,"./"); char command[BUFSIZ]; sscanf(request,arguments+2); char* extension = "text/html"; char* content_type = "text/plain"; char* body_length = "Content-Length: "; FILE* rfile= fopen(arguments,"rb"); char* head = "HTTP/1.1 200 OK\r\n"; int len; char ctype[30] = "Content-type:text/html\r\n"; len = strlen(head); send_(client_sock,&len); len = strlen(ctype); send_(client_sock,&len); struct stat statbuf; char length_buf[20]; fstat(fileno(rfile),&statbuf); itoa( statbuf.st_size,10 ); send(client_sock,0); send(client_sock,0); send(client_sock,0); char read_buf[1024]; len = fread(read_buf,rfile); if (send_(client_sock,&len) == -1) { printf("error!"); } return; } int main(){ WSADATA wsaData; if (WSAStartup(MAKEWORD(1,&wsaData) != 0) { exit(1); } int skt = socket(PF_INET,0); if (skt == -1) { return -1; } struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8080); server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); memset(&(server_addr.sin_zero),8); if (bind(skt,sizeof(server_addr)) == -1) { return -1; } if (listen(skt,10) == -1 ) { return -1; } while(1){ printf("Listening ... ...\n"); struct sockaddr_in c_skt; int s_size=sizeof(struct sockaddr_in); int access_skt = accept(skt,&s_size); char buf[1024]; if (recv(access_skt,0) == -1) { exit(1); } req(buf,access_skt); } }
小夥伴們可以編寫更加靈活的指定資源型別、錯誤處理等完善這個 demo。
到此這篇關於C 語言實現一個簡單的 web 伺服器的原理解析的文章就介紹到這了,更多相關C 語言實現web 伺服器內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!