光城歸來之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.htmlinfo.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] = '';      strcpy(url, res);      return url;  }  

最後就會得到post後的含有特殊字符與中文的結果!

2.CGI+Nginx

2.1 概念初探

CGI

通用網關接口Common Gateway Interface/CGI描述了客戶端和服務器程序之間傳輸數據的一種標準,可以讓一個客戶端,從網頁瀏覽器向執行在網絡服務器上的程序請求數據。CGI 獨立於任何語言的,CGI 程序可以用任何腳本語言或者是完全獨立編程語言實現,只要這個語言可以在這個系統上運行。Unix shell script, Python, Ruby, PHP, perl, Tcl,C/C++, 和 Visual Basic 都可以用來編寫 CGI 程序。

如下圖所示:

FastCGI

快速通用網關接口(Fast Common Gateway Interface/FastCGI)是通用網關接(CGI)的改進,描述了客戶端和服務器程序之間傳輸數據的一種標準。FastCGI致力於減少Web服務器與CGI程式之間互動的開銷,從而使服務器可以同時處理更多的Web請求。與為每個請求創建一個新的進程不同,FastCGI使用持續的進程來處理一連串的請求。這些進程由FastCGI進程管理器管理,而不是web服務器。

Nginx

Nginx是異步框架的Web服務器,也可以用作反向代理,負載平衡器 和 HTTP緩存。

Nginx+CGI

nginx 不能直接執行外部可執行程序,並且cgi是接收到請求時才會啟動cgi進程,不像fastcgi會在一開就啟動好,這樣nginx天生是不支持 cgi 的。nginx 雖然不支持cgi,但它支持fastCGI。所以,我們可以考慮使用fastcgi包裝來支持 cgi。原理大致如下圖所示:pre-fork幾個通用的代理fastcgi程序——fastcgi-wrapper,fastcgi-wrapper啟動執行cgi然後將cgi的執行結果返回給nginx(fork-and-exec)。

fastcgi-wrapper安裝:進入下面地址:

https://github.com/gnosek/fcgiwrap

autoreconf -i  ./configure  make && make install  

啟動fastcgi-wrapper

spawn-fcgi -f /usr/local/sbin/fcgiwrap -p 9000  

nginx源碼安裝同上(不用執行第一行auto操作)。

安裝完後,進入conf目錄進行fcgiwrap配置:

location ~ ^/cgi-bin/.*$ {      root /home/light/nginx/; # 填寫自己的nginx目錄      #cgi path      if (!-f $document_root$fastcgi_script_name) {          return 404;      }      fastcgi_pass 127.0.0.1:9000;      include fastcgi.conf;  }  

在nginx目錄下新建一個cgi-bin目錄用於放置cgi程序。

編寫cgi程序main.c

#include <stdio.h>  #include <stdlib.h>    int main(void)  {      int count = 0;      printf("Content-type: text/html;charset=utf-8rn"          "rn"          "<title>光城第一次使用CGI!</title>"            "<h1>光城第一次使用CGI!</h1>"          "Request number %d running on host <i>%s</i>n",          ++count, getenv("SERVER_NAME"));      return 0;  }  

編譯程序:

gcc -o main main.c  

開始部署,移動main到cgi-bin目錄:

然後啟動nginx:

./sbin/nginx  

打開瀏覽器:

看到如上頁面,成功!