TinyHttp学习(二): 代码分块解释以及相关问题

本文详细介绍了一个简易HTTP服务器的搭建过程,包括监听建立、连接处理、请求解析、静态文件传输及CGI处理等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

源码见:

这里我们按照时间线性过程来描述整个代码过程,并在每一块提出一个问题,来解答为什么要这么选择。

一、建立Listen监听

我们启动服务器程序,进入main函数

则我们先建立了服务器的监听,便于与即将来到的请求启动TCP连接。  建立监听的函数为startup

int startup(u_short *port)
{
 int httpd = 0;
 struct sockaddr_in name;
 //建立一个socket描述符
 httpd = socket(PF_INET, SOCK_STREAM, 0);
 if (httpd == -1)  //这种出错一般在于描述符过多,或其他。
  error_die("socket");
 memset(&name, 0, sizeof(name));  //对name结构体清零(也可用bzero等函数)
 //设置服务器的监听元素: socket族,端口, 可接受的地址
 name.sin_family = AF_INET;
 name.sin_port = htons(*port); //主机序转网络序的函数
 name.sin_addr.s_addr = htonl(INADDR_ANY);//可接受任何地址的访问
 
 //将描述符和这个socket结构体进行绑定
 if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
  error_die("bind");

 //若port之前为0,则bind后,开启的端口为随机端口
 //这时候我们需要获取该端口是多少
 if (*port == 0)  {
  socklen_t namelen = sizeof(name);
  if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
   error_die("getsockname");
  *port = ntohs(name.sin_port);
 }
 //建立监听,最大连接数为5.
 if (listen(httpd, 5) < 0)
  error_die("listen");
 return(httpd);
}

注意,这个函数的输入参数为port,  当port为非0整数时, 则指代开启port端口用作连接。 

若port为0,则为随机端口。

问题: 为什么要设置随机端口?  一直固定1个端口不好么,或者我们自己输入一个端口?

答:   一是我们无法判断自己输入的端口是否被占用或者正确(即并非公用端口)

          二是当我们重启服务器,或者关闭服务器再打开时, 不需要去进行MSL等待。(即虽然服务器程序关闭,连接终止,但是端口会等待2MSL的时间)


二、连接建立和处理(主要在main函数中)

监听建立后, 服务器便进入阻塞。 一旦有客户连接到来时,才会解除阻塞。

当我们在浏览器上输入  服务器IP:端口/url      时,则会发起TCP连接。

此时服务器需要处理这个连接,用一个线程处理即可, 主程序继续进行监听。

int main(void)
{
 int server_sock = -1;
 u_short port = 54834;  //port为0时,最终开启的端口为随机端口
 int client_sock = -1;
 struct sockaddr_in client_name;
 socklen_t client_name_len = sizeof(client_name);
 pthread_t newthread;
 //建立服务器监听
 server_sock = startup(&port);
 printf("httpd running on port %d\n", port);
 while (1){
  //等待连接建立,并返回1个客户端描述符,且客户端信息存入client_name。 
  client_sock = accept(server_sock,
                       (struct sockaddr *)&client_name,
                       &client_name_len);
  if (client_sock == -1)
   error_die("accept");
 //新建一个线程,处理这个客户的连接(非持久连接) 
 if (pthread_create(&newthread , NULL, accept_request, (void *)&client_sock) != 0)
   perror("pthread_create");
 }
 close(server_sock);
 return(0);
}

问题:为什么不用多进程,而是用多线程????

答:因为HTTP是 请求-响应模式的,

即1次请求,只有 1次响应(这个是HTTP非持续连接的特征,  HTTP的持续连接这里暂时不需要实现)

因此每次响应不需要太多复杂逻辑的处理, 时间也不会太长, 没有必要使用资源开销巨大的多进程模型。



此时我们启用了线程,并进行了1个accept_request的处理。

三、请求行处理

我们先读取出请求方法

再读取出url路径

再判断究竟是直接返回一个静态文件即可,还是返回一个需要经过cgi脚本处理的数据。

void *accept_request(void *client1){
 int client = *(int *)client1;
 char buf[1024];
 int numchars;
 char method[255];
 char url[255];
 char path[512];
 size_t i, j;
 struct stat st;
 int cgi = 0;      /* becomes true if server decides this is a CGI
                    * program */
 char *query_string = NULL;
 //读取第一行,即请求行
 numchars = get_line(client, buf, sizeof(buf));
 i = 0; j = 0;
 
 //找出请求方法(Get或者POST)
 while (!ISspace(buf[j]) && (i < sizeof(method) - 1))
 {
  method[i] = buf[j];
  i++; j++;
 }
 method[i] = '\0';

 if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
 {
  unimplemented(client);
  return NULL;
 }

 //如果是POST,肯定要调用cgi脚本获得响应数据
 if (strcasecmp(method, "POST") == 0){
    printf("This is Post!");
    cgi = 1;
  }

 //找出url
 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';  //字符串结束符不要忘记加。

 //如果是get, 且get的url中有?,则?后的是
 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++;
  }
 }

 //这里先默认路径为htdoc中的文件,故添加上htdoc
 sprintf(path, "htdocs%s", url);
 
 //如果最后一个是'\',说明没输入文件名,故默认为首页index.html
 if (path[strlen(path) - 1] == '/')
  strcat(path, "index.html");

//判断一下目录下是否存在该文件
 if (stat(path, &st) == -1) {
   //如果不存在,需要把剩余的内容都读完,这个操作在后面会非常常见。
  while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
   numchars = get_line(client, buf, sizeof(buf));
   printf("path = -1, discrad headrs: %s\n",buf);
  not_found(client);
 }
 else
 {
	  
  //如果访问的是目录,也认为访问的是首页index.html
  if ((st.st_mode & S_IFMT) == S_IFDIR)
   strcat(path, "/index.html");
  
  //如果访问的文件是可执行文件,则认为调用的cgi处理脚本,而非静态文件。
  if ((st.st_mode & S_IXUSR) ||
      (st.st_mode & S_IXGRP) ||
      (st.st_mode & S_IXOTH)    ){
	   cgi = 1;
	}

  //根据cgi的值,来判断要传输静态文件数据, 还是做cgi脚本的处理
  if (!cgi)
   serve_file(client, path);
  else
   execute_cgi(client, path, method, query_string);
 }

 close(client);
 return NULL;
}

问题:POST和GET的区别是什么?

可以从字面上去理解,  GET就是单纯获取数据,POST则常常会伴有对后端数据的修改。

故POST一定会有cgi的处理, 而GET则是可能有,可能没有。


如果不需要cgi处理,则我们进入:

四、静态文件传输

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

 //传静态文件时,暂时不需要请求头,故都先读掉。
 buf[0] = 'A'; buf[1] = '\0';
 while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
  numchars = get_line(client, buf, sizeof(buf));
 
 resource = fopen(filename,"r");

 if (resource == NULL)
  not_found(client);
 else
 {
   headers(client, filename);  //发送响应头
   cat(client, resource);     //发送响应数据
 }
 fclose(resource);
}

void headers(int client, const char *filename)
{
 char buf[1024];
 (void)filename;  /* could use filename to determine file type */
 
 strcpy(buf, "HTTP/1.1 200 OK\r\n");
 send(client, buf, strlen(buf), 0);
 strcpy(buf, SERVER_STRING);
 send(client, buf, strlen(buf), 0);
 sprintf(buf, "Content-Type: text/html\r\n");
 send(client, buf, strlen(buf), 0);
 strcpy(buf, "\r\n");
 send(client, buf, strlen(buf), 0);
}
//传输静态文件数据
//注意这里只能传文本类型,故采用的行读取
/**********************************************************************/
void cat(int client, FILE *resource)
{
 char buf[1024];

 fgets(buf, sizeof(buf), resource);
 while (!feof(resource))
 {
  send(client, buf, strlen(buf), 0);
  fgets(buf, sizeof(buf), resource);
 }
}
问题:



五、CGI处理,响应数据

这里首先获取一些POST可能需要的头信息(例如长度)

或者GET中的query_string

以形成需要传输给cgi的输入参数。

接着把这些参数传入给cgi,获取cgi的返回,并把数据发回给客户端。

/**********************************************************************/
/* 如何处理脚本,并发回响应?
/**********************************************************************/
void execute_cgi(int client, const char *path,
                 const char *method, const char *query_string)
{
 char buf[1024];
 int cgi_output[2];
 int cgi_input[2];
 pid_t pid;
 int status;
 int i;
 char c;
 int numchars = 1;
 int content_length = -1;

 buf[0] = 'A'; buf[1] = '\0';
 
 //如果是GET请求,则请求头暂时没有用。我们舍弃掉。
 if (strcasecmp(method, "GET") == 0)
  while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
  {
     numchars = get_line(client, buf, sizeof(buf));
     //printf("%s\n",buf); 
 }
 else    /* POST */
 {
  //如果是POST请求,我们需要知道content-Lenth的内容	 
  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;
  }
 }

 
 //这个ok响应必须添加
 //其他响应头 通过cgi脚本去发出。
 sprintf(buf, "HTTP/1.0 200 OK\r\n");
 send(client, buf, strlen(buf), 0);


 //建立2个管道
 if (pipe(cgi_output) < 0) {
  cannot_execute(client);
  return;
 }
 if (pipe(cgi_input) < 0) {
  cannot_execute(client);
  return;
 }

 
 //建立子进程
 if ( (pid = fork()) < 0 ) {
  cannot_execute(client);
  return;
 }
 
 //子进程负责将  请求参数输入到cgi脚本中,并返回处理后的数据
 if (pid == 0)  
 {
  char meth_env[255];
  char query_env[255];
  char length_env[255];

  dup2(cgi_output[1], 1); //在子进程中,把out1描述符重定向为标准输出
  dup2(cgi_input[0], 0);  //把in0描述符重定向为标准输入
  close(cgi_output[0]);  //关闭无用的2个方向。
  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);
  }
  //调用execl执行
  execl(path, path,NULL);
  exit(0);
 } else {    
	/* 父进程,负责把参数传入子进程
              把子进程中cgi处理得到的数据发给浏览器*/
  close(cgi_output[1]);
  close(cgi_input[0]);
  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);
 }
}

问题:为什么父子进程要使用管道来做输入输出?  父进程直接获得子进程的cgi输出不可以吗

管道的特点是,一旦有数据传入,就会立刻传出,并常常被我们设立为单向管道。

这里如果不用管道,那么当子进程用excl调用cgi时,所获得的输出将会不知所向, 正常情况下是输出给标准输出(描述符为1),但我们还需要传给客户端, 而传给客户端这个工作,就交给父进程来执行, 所以建立了2个管道,即


父进程 从管道A推参数, 推到子进程的输入中

子进程用cgi处理数据,并把print的数据,从管道B,推回父进程

父进程从管道B获取数据,发回到客户端中。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值