光城歸來之C語言開發網站
- 2019 年 10 月 6 日
- 筆記
C語言開發網站
0.導語
最近要把防火牆項目做個頁面,而底層全部c語言實現,那麼就得做個web頁面,想了一下,C大法這麼厲害,也應該可以的,然後大家就見到了這篇文章。
本篇文章主要講使用C語言如何開發網站,CGI,Nginx+CGI如何部署等問題。
1.Socket通信
初探網站開發,直接上手熟悉的Socket通信編程,這方面網上資料非常多。以網上一張圖片為例:

圖片來自:https://www.jianshu.com/p/dd580395bf11
本次實踐以Get/Post
提交表單為例,學習如何解析Html,後端與前端如何通信,Socket如何使用的問題。
直接先放主函數,然後再從每個函數講解。
int main(){ //1.創建監聽套接字,返回是套接字描述符 int sockfd = create_listenfd(); int fd; while (1){ //2.等待客戶端響應 int fd = accept(sockfd,NULL,NULL); //3.處理客戶端發來的請求 handle_request(fd); close(fd); } close(sockfd); }
Socket操作分別為:
- 創建監聽套接字,返回是套接字描述符
- 接收客戶端發來的請求
- 處理客戶端發來的請求
上述便是Socket通信的核心三步驟。
1.1 創建套接字
下面一一來分析上述三步如何寫。
引入相關頭文件
#include <sys/types.h> #include <sys/socket.h>
核心函數解析
(1)獲取一個socket descriptor
/** 獲取一個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);
(2)客戶端socket向服務器發起連接
/** 客戶端socket向服務器發起連接 @params: sockfd: 發起連接的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);
(3)綁定
/** 服務器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)
(4)監聽
/** 將當前socket轉變為可以監聽外部連接請求的socket @params: sockfd: 當前socket descriptor backlog: 請求隊列的最大長度 @returns: 0 if OK, -1 on error */ int listen(int sockfd, int backlog);
開始對上述操作進行封裝,上述封裝函數如下:
//監聽套接字創建 int create_listenfd(){ //創建Tcp連接 int fd = socket(AF_INET,SOCK_STREAM,0); int option = 1; setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option)); struct sockaddr_in sin; bzero(&sin,sizeof(sin)); sin.sin_family=AF_INET; sin.sin_port=htons(80); sin.sin_addr.s_addr=INADDR_ANY; int res = bind(fd,(struct sockaddr *)&sin,sizeof(sin)); if (res==-1){ perror("bind"); } listen(fd,100); return fd; }
1.2 等待客戶端請求到達
等待客戶端請求到達
/** 等待客戶端請求到達,注意,成功返回得到的是一個新的socket descriptor, 而不是輸入參數listenfd。 @params: listenfd: 當前正在用於監聽的socket descriptor addr: 客戶端請求地址(輸出參數) addrlen: 客戶端請求地址的長度(輸出參數) @returns: 成功則返回一個非負的connected descriptor,出錯則返回-1 */ int accept(int listenfd, struct sockaddr *addr, int *addrlen);
在main函數中,使用while 1循環來進行等待:
while (1){ int fd = accept(sockfd,NULL,NULL); }
1.3 處理客戶端發來的請求
上述完成後,其實就可以運行代碼,通過:
gcc -o main main.c ./main
即可完成socket的web server搭建,而客戶端與服務端的更多交互操作,則需要更深入的學習,那麼接下來就是來做這方面工作的。
首先來看一下我們的運行結果:

index.html圖

post圖

info.html圖

客戶端post前服務端接受數據圖

客戶端post後服務端接受數據圖
該函數完成了如下操作:
分別有兩個頁面,分別是index.html
與info.html
。
當第一次打開index.html
時候,會通過get方式獲取相關資源,如下圖所示:
我們看到了獲取index.html與2.jpg,所以我們看到了index頁面信息。
而當我們發送post請求跳轉到info.html時,我們會在info.html中看到post後的數據。
接下來就來看代碼如何實現:
首先來看服務器端如何獲取數據呢(也就是終端打印數據):
char buffer[40*1024]={0}; int nread=read(fd,buffer,sizeof(buffer)); printf("讀到的請求是:n"); printf("%s",buffer); printf("n--------------------n"); sscanf(buffer,"%s /%s HTTP/1.1",method,filename);
這裡直接通過read函數傳遞一個socket描述符,然後通過read便可獲取到當前index.html的數據。
接下來就是get請求:
在上述sscanf
函數中,我們解析出來了文件名與請求方法,然後根據請求方法做判斷即可!
打開文件並發送該文件內容給瀏覽器,瀏覽器便可以接收到服務器端的響應數據!
char filename[10]={0}; char method[10]={0}; if(strcmp(method,"GET")==0){ printf("n解析出來的文件名是%sn",filename); char mime[40*1024]={0}; get_filetype(filename,mime); char response[40*1024]={0}; //rnrn表示換行後有一個空行 sprintf(response, "HTTP/1.1 200 OKrnContent-Type: %srnrn", mime); int headlen = strlen(response); //打開文件,讀取內容,構建響應,發回給客戶端 int filefd = open(filename,O_RDONLY); int filelen = read(filefd,response+headlen,sizeof(response)-headlen); //發送響應頭+內容 write(fd,response,headlen+filelen); close(filefd); }
最後就是post請求:
上述我們通過post數據後到了info.html頁面,那麼這個如何做到的呢,就是通過解析post方法,然後對客戶端,也就是瀏覽器做出響應即可!
char username[50] = { 0 }; char sex[50] = { 0 }; char email[50] = { 0 }; else if(strcmp(method,"POST")==0){ char response[40*1024]={0}; char mime[40*1024]={0}; get_filetype(filename,mime); //rnrn表示換行後有一個空行 sprintf(response, "HTTP/1.1 200 OKrnContent-Type: %srnrn", mime); int headlen = strlen(response); char *postbuffer = strstr(buffer, "username="); if(postbuffer){ // char * after = strchr(buffer, '&'); printf("------>%sn",postbuffer); // printf("------>%sn",postbuffer+9); char *sexbuffer = strstr(postbuffer, "&sex="); char *emailbuffer = strstr(sexbuffer, "&email="); printf("email:%sn",emailbuffer); strncpy(username,(const char *)postbuffer,strlen(postbuffer)-strlen(sexbuffer)); printf("用戶名=%sn",urldecode(username+9)); strncpy(sex,(const char *)sexbuffer,strlen(sexbuffer)-strlen(emailbuffer)); printf("性別=%sn",urldecode(sex)); printf("郵箱=%sn",urldecode(emailbuffer+7)); // char *email = strstr(buffer, "&email="); // int sex_email = email-sex; } char *res[100]={0}; //發送數據給瀏覽器 sprintf(res,"<!DOCTYPE html><html><head><meta charset="UTF-8"><title>結果</title></head><body><p>POST結果:%s</p></body></html>",urldecode(postbuffer)); sprintf(response,"%s",res); write(fd,response,headlen+strlen(res)); printf("%sn",response); // close(filefd); printf("n解析出來的文件名是%sn",filename); }
除此之外,上述post後,碰到中文會出現亂碼,以%
的數據發送了過去,也就是通常的url編碼與解碼,這裡直接用c實現解碼函數即可:
// 解碼url char * urldecode(char url[]) { int i = 0; int len = strlen(url); int res_len = 0; char res[BURSIZE]; for (i = 0; i < len; ++i) { char c = url[i]; if (c != '%') { res[res_len++] = c; } else { char c1 = url[++i]; char c0 = url[++i]; int num = 0; num = hex2dec(c1) * 16 + hex2dec(c0); res[res_len++] = num; } } res[res_len] = '