您好,登錄后才能下訂單哦!
使用C 語言如何實現 一個web 服務器?很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
一、了解 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 將數據進行返回。
首先在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, 1), &wsaData) != 0) { exit(1); }
初始化完畢后開始創建套接字,套接字創建使用函數,函數原型為:
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; }
創建完 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; }
綁定成功后開始對端口進行監聽。查看 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, 1), &wsaData) != 0) { exit(1); } //socket創建 int skt = socket(PF_INET, SOCK_STREAM, 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), '\0', 8); //綁定 if (bind(skt, (struct sockaddr *)&server_addr,sizeof(server_addr)) == -1){ return -1; } //監聽 if (listen(skt, 10) == -1 ) { return -1; } printf("Listening ... ...\n"); }
運行代碼可得知代碼無錯誤,并且輸出 listening:
在這里插入圖片描述
監聽完成后開始獲取請求。受限需要使用 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, (struct sockaddr *)&c_skt, &s_size); //獲取數據 char buf[1024]; if (recv(access_skt, buf, 1024, 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, (struct sockaddr *)&c_skt, &s_size); //獲取數據 char buf[1024]; if (recv(access_skt, buf, 1024, 0) == -1) { exit(1); } printf("%s",buf); }
接下來我們對請求頭進行對應的操作。
得到請求后開始編寫處理層。繼續接著代碼往下寫沒有層級,編寫一個函數名為 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, length_buf, strlen(length_buf), 0); send(client_sock, "\n", 1, 0); send(client_sock, "\r\n", 2, 0);
最后發送數據:
//·數據發送 char read_buf[1024]; len = fread(read_buf ,1 , 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, 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; } void req(char* request, int client_sock) { 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: "; 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, head, &len); len = strlen(ctype); send_(client_sock, ctype, &len); struct stat statbuf; 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, length_buf, strlen(length_buf), 0); send(client_sock, "\n", 1, 0); send(client_sock, "\r\n", 2, 0); char read_buf[1024]; len = fread(read_buf ,1 , statbuf.st_size, rfile); if (send_(client_sock, read_buf, &len) == -1) { printf("error!"); } return; } int main(){ WSADATA wsaData; if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) { exit(1); } int skt = socket(PF_INET, SOCK_STREAM, 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), '\0', 8); if (bind(skt, (struct sockaddr *)&server_addr,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, (struct sockaddr *)&c_skt, &s_size); char buf[1024]; if (recv(access_skt, buf, 1024, 0) == -1) { exit(1); } req(buf, access_skt); } }
看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。