前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >通过tinyhttpd-0.1.0源码理解服务器原理

通过tinyhttpd-0.1.0源码理解服务器原理

作者头像
theanarkh
发布2020-04-14 17:21:58
4070
发布2020-04-14 17:21:58
举报
文章被收录于专栏:原创分享原创分享原创分享

tinyhttpd是一个demo版的服务器。代码几百行。源码分析在http://suo.im/6bkZlt。从中可用一窥服务器的基础原理。他采用的是一个请求新开一个线程处理的方式。里面涉及了多进程、多线程、进程间通信等知识。 我们从main函数开始分析。

int main(void)
{
 int server_sock = -1;
 u_short port = 0;
 int client_sock = -1;
 struct sockaddr_in client_name;
 int client_name_len = sizeof(client_name);
 pthread_t newthread;
// 拿到一个监听的文件描述符
 server_sock = startup(&port);

 while (1)
 {
   // 等待连接到来
  client_sock = accept(server_sock,
                       (struct sockaddr *)&client_name,
                       &client_name_len);
 // 每一个连接用一个线程处理,主函数是accept_request
 if (pthread_create(&newthread , NULL, accept_request, client_sock) != 0)
   perror("pthread_create");
 }
// 服务器退出
 close(server_sock);

 return(0);
}

main函数的逻辑很简单。首先调用startup拿到一个监听型的文件描述符。然后启动服务器。阻塞在accept等待请求的到来。我们看看startup。

// 传统的服务器启动流程
int startup(u_short *port)
{
 int httpd = 0;
 struct sockaddr_in name;
 // 拿到一个用于监听的文件描述符
 httpd = socket(PF_INET, SOCK_STREAM, 0);
 memset(&name, 0, sizeof(name));
 name.sin_family = AF_INET;
 name.sin_port = htons(*port);
 name.sin_addr.s_addr = htonl(INADDR_ANY);
  // 绑定一个地址到socket,没有的话ip是本机地址,端口随机
 if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
  error_die("bind");
 // port等于0,系统会随机分配一个端口(bind函数里实现)。这里通过文件描述符拿到系统随机分配的端口
 if (*port == 0)  /* if dynamically allocating a port */
 {
  int namelen = sizeof(name);
  // 获取socket绑定的地址信息
  if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
   error_die("getsockname");
  *port = ntohs(name.sin_port);
 }
 if (listen(httpd, 5) < 0)
  error_die("listen");
 return(httpd);
}

startup函数是经典的socket编程流程。这时候服务器已经启动。等待请求的到来。我们回忆main函数里的accept函数。他返回的是一个和客户端通信的文件描述符。然后新开一个线程,线程里执行accept_request函数。把这个描述符传给线程,让他处理。accept_request函数的主要逻辑如下。

// 文件路径htdocs下
 sprintf(path, "htdocs%s", url);
 // 最后一个字符是/说明是个目录,则取该目录下的index.html文件
 if (path[strlen(path) - 1] == '/')
  strcat(path, "index.html");
  // 找不到该文件返回404
 if (stat(path, &st) == -1) {
  while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
   numchars = get_line(client, buf, sizeof(buf));
  not_found(client);
 }
 else
 {
   // 是个目录
  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;
  // 返回静态文件给客户端
  if (!cgi)
   serve_file(client, path);
  else
  // 执行cgi程序
   execute_cgi(client, path, method, query_string);
 }

主要是两个逻辑。 1 静态文件的请求,则直接读取文件内容,然后返回给客户端。线程退出。 2 执行动态脚本。 下面我们只分析2。通过执行execute_cgi函数执行动态脚本。该函数比较长,分开分析。

void execute_cgi(int client, const char *path,
                 const char *method, const char *query_string)
{
 // 一系列简单解析http协议的逻辑
 // 获取两个匿名管道
 if (pipe(cgi_output) < 0) {
  cannot_execute(client);
  return;
 }
 if (pipe(cgi_input) < 0) {
  cannot_execute(client);
  return;
 }
}

申请两个管道。

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

创建一个进程。我们看看父进程和子进程都做了什么事情。先看子进程

char meth_env[255];
  char query_env[255];
  char length_env[255];
  // 先断开文件描述符1和标准输出file结构的关联,然后使1指向cgi_ouput[1]指向的file结构
  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);

子进程输入关闭管道的一端,然后输入环境变量给cgi进程。然后执行cgi程序。再看父进程。

  close(cgi_output[1]);
  close(cgi_input[0]);
  if (strcasecmp(method, "POST") == 0)
   for (i = 0; i < content_length; i++) {
    /*
      读数据然后写入写端cgi_input[1],对端是子进程的cgi_input[0],作为子进程的标准读入,
      即子进程可以读到这里写入的数据
    */
    recv(client, &c, 1, 0);
    write(cgi_input[1], &c, 1);
   }
   /*
     等待子进程写入,然后返回给客户端,cgi_output[1]是子进程的标准输出端,
     从cgi_output[1]写入的数据可以从cgi_output[0]读取
   */
  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);

父进程同样关闭管道一端。如果是post则把客户端的body输入给子进程。然后在read函数阻塞等待子进程的输入。最后两个进程退出。整个服务器的处理过程是,每次来一个请求(假设是cgi)。新开一个处理线程。主线程继续监听。然后新开的处理线程fork出一个进程执行cgi。这时候就相当于有两个进程。父子进程互相通信完成一个客户端的处理,然后退出。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-04-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程杂技 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档