C语言开发网站
0.导语
最近要把防火墙项目做个页面,而底层全部c语言实现,那么就得做个web页面,想了一下,C大法这么厉害,也应该可以的,然后大家就见到了这篇文章。
本篇文章主要讲使用C语言如何开发网站,CGI,Nginx+CGI如何部署等问题。
1.Socket通信
初探网站开发,直接上手熟悉的Socket通信编程,这方面网上资料非常多。以网上一张图片为例:
图片来自:https://www.jianshu.com/p/dd580395bf11
本次实践以Get/Post
提交表单为例,学习如何解析Html,后端与前端如何通信,Socket如何使用的问题。
直接先放主函数,然后再从每个函数讲解。
1int main(){
2 //1.创建监听套接字,返回是套接字描述符
3 int sockfd = create_listenfd();
4 int fd;
5 while (1){
6 //2.等待客户端响应
7 int fd = accept(sockfd,NULL,NULL);
8 //3.处理客户端发来的请求
9 handle_request(fd);
10 close(fd);
11 }
12 close(sockfd);
13}
Socket操作分别为:
创建监听套接字,返回是套接字描述符
接收客户端发来的请求
处理客户端发来的请求
上述便是Socket通信的核心三步骤。
1.1 创建套接字
下面一一来分析上述三步如何写。
引入相关头文件
1#include
2#include
核心函数解析
(1)获取一个socket descriptor
1/** 2获取一个socket descriptor 3@params: 4 domain: 此处固定使用AF_INET 5 type: 此处固定使用SOCK_STREAM 6 protocol: 此处固定使用0 7@returns: 8 nonnegative descriptor if OK, -1 on error. 9*/
10int socket(int domain, int type, int protocol);
(2)客户端socket向服务器发起连接
1/** 2客户端socket向服务器发起连接 3@params: 4 sockfd: 发起连接的socket descriptor 5 serv_addr: 连接的目标地址和端口 6 addrlen: sizeof(*serv_addr) 7@returns: 8 0 if OK, -1 on error 9*/
10int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
(3)绑定
1/** 2服务器socket绑定地址和端口 3@params: 4 sockfd: 当前socket descriptor 5 my_addr: 指定绑定的本机地址和端口 6 addrlen: sizeof(*my_addr) 7@returns: 8 0 if OK, -1 on error 9*/
10int bind(int sockfd, struct sockaddr *my_addr, int addrlen)11
(4)监听
1/**2将当前socket转变为可以监听外部连接请求的socket3@params:4 sockfd: 当前socket descriptor5 backlog: 请求队列的最大长度6@returns:7 0 if OK, -1 on error8*/
9int listen(int sockfd, int backlog);
开始对上述操作进行封装,上述封装函数如下:
1//监听套接字创建
2int create_listenfd(){
3 //创建Tcp连接
4 int fd = socket(AF_INET,SOCK_STREAM,0);
5 int option = 1;
6 setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option));
7 struct sockaddr_in sin;
8 bzero(&sin,sizeof(sin));
9 sin.sin_family=AF_INET;
10 sin.sin_port=htons(80);
11 sin.sin_addr.s_addr=INADDR_ANY;
12
13 int res = bind(fd,(struct sockaddr *)&sin,sizeof(sin));
14 if (res==-1){
15 perror("bind");
16 }
17 listen(fd,100);
18 return fd;
19}
1.2 等待客户端请求到达
等待客户端请求到达
1/** 2等待客户端请求到达,注意,成功返回得到的是一个新的socket descriptor, 3而不是输入参数listenfd。 4@params: 5 listenfd: 当前正在用于监听的socket descriptor 6 addr: 客户端请求地址(输出参数) 7 addrlen: 客户端请求地址的长度(输出参数) 8@returns: 9 成功则返回一个非负的connected descriptor,出错则返回-110*/
11int accept(int listenfd, struct sockaddr *addr, int *addrlen);
在main函数中,使用while 1循环来进行等待:
1while (1){
2 int fd = accept(sockfd,NULL,NULL);
3}
1.3 处理客户端发来的请求
上述完成后,其实就可以运行代码,通过:
1gcc -o main main.c
2./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后的数据。
接下来就来看代码如何实现:
首先来看服务器端如何获取数据呢(也就是终端打印数据):
1char buffer[40*1024]={0};
2int nread=read(fd,buffer,sizeof(buffer));
3printf("读到的请求是:\n");
4printf("%s",buffer);
5printf("\n--------------------\n");
6sscanf(buffer,"%s /%s HTTP/1.1",method,filename);
这里直接通过read函数传递一个socket描述符,然后通过read便可获取到当前index.html的数据。
接下来就是get请求:
在上述sscanf
函数中,我们解析出来了文件名与请求方法,然后根据请求方法做判断即可!
打开文件并发送该文件内容给浏览器,浏览器便可以接收到服务器端的响应数据!
1char filename[10]={0};
2char method[10]={0};
3
4if(strcmp(method,"GET")==0){
5
6 printf("\n解析出来的文件名是%s\n",filename);
7 char mime[40*1024]={0};
8 get_filetype(filename,mime);
9 char response[40*1024]={0};
10
11 //\r\n\r\n表示换行后有一个空行
12 sprintf(response, "HTTP/1.1 200 OK\r\nContent-Type: %s\r\n\r\n", mime);
13 int headlen = strlen(response);
14 //打开文件,读取内容,构建响应,发回给客户端
15 int filefd = open(filename,O_RDONLY);
16 int filelen = read(filefd,response+headlen,sizeof(response)-headlen);
17
18 //发送响应头+内容
19 write(fd,response,headlen+filelen);
20 close(filefd);
21
22}
最后就是post请求:
上述我们通过post数据后到了info.html页面,那么这个如何做到的呢,就是通过解析post方法,然后对客户端,也就是浏览器做出响应即可!
1char username[50] = { 0 };
2char sex[50] = { 0 };
3char email[50] = { 0 };
4else if(strcmp(method,"POST")==0){
5 char response[40*1024]={0};
6 char mime[40*1024]={0};
7 get_filetype(filename,mime);
8 //\r\n\r\n表示换行后有一个空行
9 sprintf(response, "HTTP/1.1 200 OK\r\nContent-Type: %s\r\n\r\n", mime);
10 int headlen = strlen(response);
11 char *postbuffer = strstr(buffer, "username=");
12 if(postbuffer){
13 // char * after = strchr(buffer, '&');
14 printf("------>%s\n",postbuffer);
15 // printf("------>%s\n",postbuffer+9);
16 char *sexbuffer = strstr(postbuffer, "&sex=");
17 char *emailbuffer = strstr(sexbuffer, "&email=");
18 printf("email:%s\n",emailbuffer);
19 strncpy(username,(const char *)postbuffer,strlen(postbuffer)-strlen(sexbuffer));
20 printf("用户名=%s\n",urldecode(username+9));
21 strncpy(sex,(const char *)sexbuffer,strlen(sexbuffer)-strlen(emailbuffer));
22 printf("性别=%s\n",urldecode(sex));
23 printf("邮箱=%s\n",urldecode(emailbuffer+7));
24 // char *email = strstr(buffer, "&email=");
25 // int sex_email = email-sex;
26 }
27 char *res[100]={0};
28 //发送数据给浏览器
29 sprintf(res,"html>结果
POST结果:%s
",urldecode(postbuffer));
30 sprintf(response,"%s",res);
31 write(fd,response,headlen+strlen(res));
32 printf("%s\n",response);
33
34 // close(filefd);
35 printf("\n解析出来的文件名是%s\n",filename);
36
37}
除此之外,上述post后,碰到中文会出现乱码,以%
的数据发送了过去,也就是通常的url编码与解码,这里直接用c实现解码函数即可:
1// 解码url
2char * urldecode(char url[]) 3{
4 int i = 0;
5 int len = strlen(url);
6 int res_len = 0;
7 char res[BURSIZE];
8 for (i = 0; i 9 {
10 char c = url[i];
11 if (c != '%')
12 {
13 res[res_len++] = c;
14 }
15 else
16 {
17 char c1 = url[++i];
18 char c0 = url[++i];
19 int num = 0;
20 num = hex2dec(c1) * 16 + hex2dec(c0);
21 res[res_len++] = num;
22 }
23 }
24 res[res_len] = '\0';
25 strcpy(url, res);
26 return url;
27}
最后就会得到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
1autoreconf -i
2./configure
3make && make install
启动fastcgi-wrapper
:
1spawn-fcgi -f /usr/local/sbin/fcgiwrap -p 9000
nginx
源码安装同上(不用执行第一行auto
操作)。
安装完后,进入conf
目录进行fcgiwrap
配置:
1location ~ ^/cgi-bin/.*$ {
2 root /home/light/nginx/; # 填写自己的nginx目录
3 #cgi path
4 if (!-f $document_root$fastcgi_script_name) {
5 return 404;
6 }
7 fastcgi_pass 127.0.0.1:9000;
8 include fastcgi.conf;
9}
在nginx目录下新建一个cgi-bin
目录用于放置cgi程序。
编写cgi程序main.c
:
1#include
2#include
3
4int main(void) 5{
6 int count = 0;
7 printf("Content-type: text/html;charset=utf-8\r\n"
8 "\r\n"
9 "光城第一次使用CGI!"
10
11 "
光城第一次使用CGI!
"
12 "Request number %d running on host %s\n",
13 ++count, getenv("SERVER_NAME"));
14 return 0;
15}
编译程序:
1gcc -o main main.c
开始部署,移动main到cgi-bin目录:
然后启动nginx:
1./sbin/nginx
打开浏览器:
看到如上页面,成功!