c++框架一 :tinyhttpd

写代码从学习优秀的代码开始!

开始学习c++的框架,从最小型的开始,tinyhttpd最适合不过了,总共500来行代码。从socket服务建立,到多线程和创建子进程,并在父子进程间通过管道通信,同时又可以了解http的一些基本原理,可以说非常适合学习。

代码总体架构如下:

代码阅读顺序(为方便阅读,只写关键代码)

1、首先是main(),在main中调用startup()函数建立服务端监听,如果传参为0,则会自动选择一个可用端口,并用getsockname()获取服务信息,这里主要是获取监听的端口号。

while()循环等待客户端连接,每客户端到来,就新建线程处理。

int main(void)
{
 //startup()函数就是socket()、bind()、listen(),返回创建的fd
 server_sock = startup(&port);  

 while (1)
 {
  client_sock = accept(server_sock,(struct sockaddr *)&client_name,
                       &client_name_len);
 
 //每来一个客户端连接,就创建一个线程处理。
 pthread_create(&newthread , NULL, accept_request, client_sock);
 }
}

2、线程处理函数

读取浏览器发送的http协议行,分离出:GET/POST 和 请求的目录,

确定是读取index.html文件,还是执行cgi文件。

void accept_request(void *pfd)
{
 int client = *((int*)pfd);

 /*get_line()从缓冲区读取一行
   因为是第一行,有几种情况
   1、GET / HTTP/1.1
   2、POST /color.cgi HTTP/1.1
   3、GET /color.cgi?color=green HTTP/1.1
     (浏览器发送:http://127.0.0.1:35239/color.cgi?color=green)
      
*/
 numchars = get_line(client, buf, sizeof(buf));

 while (!ISspace(buf[j]) && (i < sizeof(method) - 1))
 {
  method[i] = buf[j];
  i++; j++;
 }
 method[i] = '\0';
//此时mtthod = GET/POST

 if (strcasecmp(method, "POST") == 0)
  cgi = 1;

 i = 0;
 while (ISspace(buf[j]) && (j < sizeof(buf)))
  j++;
 while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf)))
 {
  url[i] = buf[j];
  i++; j++;
 }
 url[i] = '\0';
//此时url=/ 或 url=/color.cgi 或 url=/color.cgi?color=green

/*如果是GET且url带参数,如上例: query_string=color=green
  且这种情况cgi=1,也就是说要执行cgi文件(cgi=0直接读取index.html)
*/
 if (strcasecmp(method, "GET") == 0)
 {
  query_string = url;
  while ((*query_string != '?') && (*query_string != '\0'))
   query_string++;
  if (*query_string == '?')
  {
   cgi = 1;
   *query_string = '\0';
   query_string++;
  }
 }

/*拼接文件路径
  path = htdocs/index.html 或
  path = htdocs/color.cgi  或
  path = htdocs//color.cgi?color=green
*/
 sprintf(path, "htdocs%s", url);
 if (path[strlen(path) - 1] == '/')
  strcat(path, "index.html");
 
//如果文件存在,且有执行全新,cgi=1 ,index.html无执行权限,只有cgi文件有执行权限
if (stat(path, &st) == -1) 
  not_found(client);
 else
 {
  if ((st.st_mode & S_IXUSR) ||(st.st_mode & S_IXGRP) ||(st.st_mode & S_IXOTH) )
   cgi = 1;
 }
 
if (!cgi)
   serve_file(client, path);  //浏览器输ip:port,就读取index.html
  else
   execute_cgi(client, path, method, query_string); //post 或带参数的get走此处
 }

 close(client);
}

3、serve_file

把index.html文件内容发送给客户端。

void serve_file(int client, const char *filename)
{
 FILE *resource = NULL;
 int numchars = 1;
 char buf[1024];

//把缓冲区其他内容读出来,清空缓冲区,因为http除了第一行还要发好多行数据
 buf[0] = 'A'; buf[1] = '\0';
 while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
  numchars = get_line(client, buf, sizeof(buf));

//打开htdocs/index.html文件
 resource = fopen(filename, "r");

/*发送http头
  HTTP/1.0 200 OK\r\n
  Content-Type: text/html\r\n
  \r\n
*/
 headers(client, filename);

//发送文件内容
 cat(client, resource);

 fclose(resource);
}

4、execute_cgi

执行cgi文件走此处,包括post 和带参数的get ,

带参数的get如:127.0.0.1:8899/color.cgi?color=red

创建管道,创建子进程,父进程把cgi文件路径发给子进程,子进程执行完把结果发给父进程。

父进程再发给浏览器。

void execute_cgi(int client, const char *path,
                 const char *method, const char *query_string)
{


/*
  如果是GET (带参数的get才会走此处)
  读取所有内容,情况缓冲区
*/
 if (strcasecmp(method, "GET") == 0)
  while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
   numchars = get_line(client, buf, sizeof(buf));
 
/*如果是POST,因为POST会携带内容,所以要过滤出来报文体内容
  读所有行,当读到"Content-Length:"字符串时,后跟body的长度,获取长度
  http的头最后一行是\r\n, 只有\n读到了buf,一次判断循环结束,
  具体的http内容,在后续读。
*/
else    /* POST */
 {
  numchars = get_line(client, buf, sizeof(buf));
  while ((numchars > 0) && strcmp("\n", buf))
  {
   buf[15] = '\0';
   if (strcasecmp(buf, "Content-Length:") == 0)
    content_length = atoi(&(buf[16]));
   numchars = get_line(client, buf, sizeof(buf));
  }
  if (content_length == -1) {
   bad_request(client);
   return;
  }
 }


//先发送http头
 sprintf(buf, "HTTP/1.0 200 OK\r\n");
 send(client, buf, strlen(buf), 0);

//创建两个管道,即后续父子进程都拥有这俩管道
pipe(cgi_output);
pipe(cgi_input); 

 if ( (pid = fork()) < 0 ) {
  cannot_execute(client);
  return;
 }

//子进程
 if (pid == 0)  
 {
 
  //重定向管道文件描述符,并关闭不需要的管道端口
  dup2(cgi_output[1], 1);
  dup2(cgi_input[0], 0);
  close(cgi_output[0]);
  close(cgi_input[1]);

  //设置环境变量 执行cgi文件需要
  sprintf(meth_env, "REQUEST_METHOD=%s", method);
  putenv(meth_env);
  if (strcasecmp(method, "GET") == 0) 
 {
   sprintf(query_env, "QUERY_STRING=%s", query_string);
   putenv(query_env);
  }
  else   /* POST */
  {   
   sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
   putenv(length_env);
  }

  /*执行cgi文件,子进程要执行单独的程序,
  */
  execl(path, path, NULL);
  exit(0);
 } 

//父进程
else    
{
  close(cgi_output[1]);
  close(cgi_input[0]);

/* 上文POST的时候,循环读取完了头信息,因为头最后是一行\r\n,只有\n读到了buf,而结束了循环
   此处开始读后续内容,内容长度为上文读到的Content-Length,
   比如内容为:color=red,读取后通过管道发送给子进程。
 */
  if (strcasecmp(method, "POST") == 0)
   for (i = 0; i < content_length; i++) {
    recv(client, &c, 1, 0);
    write(cgi_input[1], &c, 1);
   }

/*子进程处理完成后,通过管道发送回来
  父进程再发送给浏览器
*/
  while (read(cgi_output[0], &c, 1) > 0)
   send(client, &c, 1, 0);

  close(cgi_output[0]);
  close(cgi_input[1]);
  waitpid(pid, &status, 0);
 }
}

======================================================================

效果:

1、GET,浏览器输入127.0.0.1:36393

 2、POST,

输入内容如:red,点击Submit Query,此时请求url变为:127.0.0.1:36393/color.cgi

3、带参数get

浏览器直接输入:127.0.0.1:36393/color.cgi?color=green

 

 

 

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值