­

光城归来之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  

打开浏览器:

看到如上页面,成功!