整体工作流程

Tinyhttpd整体工作流程
(1)服务器启动,在指定端口或者随机端口绑定httpd服务
(2)在收到客户端请求后 ,服务器(主线程)accept这个连接,然后创建一个子线程,执行accept_request函数对这个请求进行处理。
(3)通过accept_request函数 提取出 请求的 方法,url 从而判断客户端的请求 ,如果客户端请求的是静态页面,执行 serve_file函数,直接将请求的数据发送给客户端。如果客户端请求的是cgi文件,执行execute_cgi函数,调用cgi文件,服务器将cgi执行得到的结果发送给客户端。

int main(void)
{
int server_sock = -1;
u_short 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)
{
client_sock = accept(server_sock,
                       (struct sockaddr *)&client_name,
                       &client_name_len);
if (client_sock == -1)//accept失败
   error_die("accept");
if (pthread_create(&newthread , NULL, accept_request,(void *)&client_sock) != 0)
   perror("pthread_create");
}
return(0);
}

各功能模块详述

startup

初始化httpd服务, 建立套接字、绑定端口、进行监听
socket()----bind()—listen()

int startup(u_short * port )
 {
    int httpd=0;
 struct sockaddr_in name; // 套接字地址 (地址族/端口地址/ip地址)

 /*int socket(int domain,int type,int protocol)*/
 httpd=socket(PF_INET,SOCK_STREAM,0);
 if(http=-1) //初始化套接口失败 
     error_die("socket");
 /*清空套接字地址*/
 memset(&name,0,sizeof(name));
 name.sin_family=AF_INET;//ipv4
    name.sin_port= htons(*port);//大小端字节表示转换
    name.sin_addr.s_addr=htonl(INADDR_ANY);//INADDR_ANY表示本机ip
     if(bind(httpd,(struct sockaddr *)&name, sizeof(name))<0)// 绑定失败
     error_die("bind");
  if(*port == 0) /*动态分配一个端口*/
  {
  int namelen = sizeof(name);
  /*在以端口0调用bind 后 ,getsockname 用于返回由内核赋予(动态)的本地端口号*/
  if(getsockname(httpd,(struct sockaddr*)&name,&namelen)==-1) //失败
  error_die("getsockname");
  *port= ntohs(name.sinport);// 网络字节顺序(大端)转换为主机字节顺序
  }
  if(listen(httpd,5)<0) //服务器监听客户端请求。连接队列的最大连接个数设为5
  error_die("listen");
  return(httpd);

涉及一些网络编程的函数
1.socket()
初始化接口,返回socket描述符

int socket(int domain,int type,int protocol);
/*
domain用于设置网络通信的域 PF_INET (Ipv4) 
type 接口类型
SOCK_STREAM TCP套接口
SOCK_DGRAM UDP套接口
protocol 用于制定某个协议的特定类型
通常设为0
*/
  1. bind()
    绑定套接口,成功返回0失败返回-1
int bind(int sockfd,const struct sockaddr * addr,
socklen_t addrlen);
/*
sockfd: socket()返回的socket描述符
addr:该套接字的地址  
struct sockaddr {
   sa_family_t sa_family;
   char        sa_data[14];
}
IP地址和端口一起储存

struct sockaddr_in
      {
        sa_family_t   sin_family; //地址族
 
        uinit16_t sin_port; //16位端口地址
        struct in_addr sin_addr;//32位IP地址
        char zero[8];//不使用 //与 struct sockaddr一样大
       }
      struct in_addr
       {
      in_addr_t s_addr;
       }
 而在 struct sockaddr_in 结构体中 ip地址和端口号是分开储存。
通常 使用struct sockaddr_in设置后,让后将其强制转换为struct sockaddr类型,然后传递给bind函数即可。
addrlen:所指定的结构体变量的大小
*/

3.listen()
使主动连接套接口变为被连接套接口,listen函数只是将 sockfd 和 backlog 告诉内核, 然后就返回。
成功返回0,失败返回1。

int listen(int sockfd,int backlog);
/*
sockfd:socket描述符
backlog:tcp连接队列上限
*/

4.accept()
它提取出所监听套接字的已连接队列中第一个连接请求,创建一个新的套接字,并返回指向该套接字的文件描述符。新建立的套接字不在监听状态,原来所监听的套接字也不受该系统调用的影响。新建立的套接字用于与客户端发送和接受消息。

int accept(int sockfd ,struct sockaddr *addr,socklen_t * addrlen);
/*
sockfd: socket函数返回socket描述符
addr:获得客户端的套接字地址
addrlen:指定的结构体变量的大小
*/

accept_request

子线程回调函数。负责处理客户端发送的请求,解析请求报文,判断客户端请求方式,若客户端请求静态文件直接返回对应的html文件,若客户端请求的是cgi文件,执行cgi程序,服务器将cgi执行得到的结果发送给客户端。

在这里插入图片描述

void accept_request(void *pclient)
  {
      int client = *(int*)pclient;//客户端socket描述符
      char buf[1024];//读取行数据时的缓冲区
      size_t numchars; //读取了多少字符
      char method[255];//方法字段保存在method变量中
      char url[255];//url
      char path[512];//路径
      size_t i, j;
      struct stat st; //文件信息的结构体 
      int cgi = 0;     
      char *query_string=NULL; //GET的请求参数
   /*读取一行数据*/ 
   numchars= get_line(client,buf,sizeof(bud));
   i=0;
   j=0;
      /*ISspace(char ) 判断一个字符是否为空*/
   while(! ISspace(buf[j]) &&  (i<sizeof(method)-1)) //获取请求方法
   {                             
       mothod[i]=buf[j];
    i++;
    j++;
   }
   method[i]=='\0';
   // 该 web服务器只能处理 GET 和 POST方法
   if(strcasecmp(method,"GET") && strcasecmp(method,"POST"))//请求的方法非GET 、POST
   {
   /* int strcasecmp(const char *s1,const char *s2);
      忽略大小写比较字符串
   相等返回0
   s1>s2 返回大于0的值
   s1<s2则返回小于0的值 
   
   */
   unimplemented(client);
   return NULL;
   }
      //POST 的时候开启cgi

   if(strcasecmp(method,"POST")==0)
      cgi=1;
      i=0;
    //读取url地址 第一个while为了跳过空格。方便从url实际开始字符串读取
    while(ISspace(buf[j]) && (j<sizeof(buf)))
     j++;
  while(!ISspace(buf[j]) && (i<sizeof(url)-1)&& (j<sizeof(buf)))
  {
  /*存下url*/
  url[i]=buf[j];
  i++;
  j++;
  }
  url[i]='\0';
  
  /*如果客户端发来的是GET请求*/
  if(strcasecmp(method,"GET")==0)
  {
   /*GET 请求参数 和对应的值附加在url后面,利用?代表 URL的结尾与请求参数的开始
   如 index.jsp?10023 其中10023就是要传递的参数 
   url储存 url(路径)
   query_string储存get的请求参数
   */
   
  query_string=url;// query_string指向url
  while( (*query_string !='?') && (*query_string!='\0'))
  {
    query_string++; //使query_string 指向?
  }
  if(*query_string =='?')
  {
  cgi=1;
  *query_string='\0' // 将?替换成\0 之前的真正的url;
  query_string++;//query_string 指向get参数开头
  }
  }
     /*格式化路径*/
  sprintf(path ,"htdocs%s",url); // 格式化url到path数组,html文件都在htdoce中
  /*
  int sprintf(char *str,const char *format);
  发送格式化输出到str所指的字符串 有点像 printf
  */
  if(path[strlen(path)-1]=='/')
  /*默认情况下为 index.html*/
  strcat(path,"index.html");
  /*
  char* strcat(char *dest,const char *src)*
  把src所指向的字符串追加到dest所指向的字符串的结尾中
  */
     if(stat(path,&st)==-1) //失败
  {
  /*
  int stat(const char* pathname,struct stat *statbuf);
  取得指定文件的文件属性 ,将文件属性储存在结构体stat中 
  返回-1失败 0成功
  */
 while((numchars>0) && strcmp("\n",buf)) //读入的请求行不为空
  numchars =get_line(client,buf,sizeof(buf));
  /*把请求headers的信息都丢弃 */
  not_found(client);
  }
  else  //获取文件信息成功
  {
   /*如果该文件是个目录,默认使用该目录下index.html*/
   if(st.st_mode & S_IFMT)==S_IFDIR)
   strcat(path,"/index.html");
   if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH)
   /*文件所有者具有可执行权限 用户组具有可执行权限  其他人具有可执行权限
   如果是可执行的文件 就要执行cgi
   */
   {
   cgi=1;
   }
   
   if(!cgi) //不使用cgi直接返回服务器文件,否则执行cgi
   serve_file(client,path);
   else
   execute_cgi(client,path,method,query_string);
  }
  
  close(client); //服务端断开与客户端的连接
  }

get_line

获得客户端http请求的一行储存在 buf中 返回获取数据的长度,获取失败返回0

int get_line(int sock,char *buf,int size)
 {
 
 //sock 服务端与客户端通信的socket 
 int i=0;
 char c='\0';
 int n;
 
 while((i<size-1) && (c != '\n'))
 {
 /*这里是一个字符一个字符拷贝*/
 n=recv(sock ,&c,1,0);
 {
 if(c=='\r')// h=回车符
 {
  n=recv(sock,&c,1,MSG_PEEK);
 /*MSG_PEEK: 从 接收缓冲区中仅仅拷贝数据,不把已读取的数据从 接收缓冲区中删除,下次调用recv仍可以读到刚才读到的数据
 */
 if( (n>0) && (c=='\n'))
     recv(sock,&c,1,0);
 else
 c='\n'; 
 //将 \r 、 \r\n 都转为符合linux换行的'\n'
 }
buf[i++]=c;
 }
 else // 读取失败
 c='\n';
 }
 buf[i]='\0';
 return (i);//返回该字符串长度(拷贝字符个数)
 }

涉及一些网络编程的函数
recv()

ssize_t recv(int sockfd, void* buf ,size_t len,int flags);
 flags表示信息,默认设置为0
 recv用于 从 sockfd的接受缓冲区(内核)中拷贝一份值放入buf(用户态)中
 返回值<0 出错
 >0 接收到的数据大小
 =0 对方调用了close 来关闭连接 

send()

ssize_t send(int sockfd, void* buf ,size_t len,int flags);
send将buf中的数据拷贝到sockfd的发送缓冲区中。

在这里插入图片描述

serve_file

cgi=0,服务器将客户端请求的静态页面发送给客户端

void serve_file(int client, const char * filename)
 {
 FILE *resource = NULL;
 int numchars=1;j
 char buf[1024];
 
 buf[0]='A';
 buf[1]='\0';
 //将请求头部读取并丢弃
 while((numchars>0) && strcmp("\n",buf))
 numchars=get_line(client,buf,sizeof(buf));

 //打开文件 index.html
resource= fopen(filename,"r");
 if(resource == NULL)
      not_find(client);//文件打开失败
 else
 {
 //模拟http响应 先向客户端发送响应头部 
    headers(client,filename);
 // 调用cat()函数发送数据实体部分 即文件内容 
    cat(client,resource);
 }
fclose(resource);
 }
 /*把HTTP响应的头部拷贝到套接字的发送缓冲区中*/
void headers(int client, const char *filename)
{
char buf[1024];  
    (void)filename;  /* could use filename to determine file type 没什么用? */  
    /*正常的 HTTP header */  
    strcpy(buf, "HTTP/1.0 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);
 }
 }

execute_cgi

cgi=1,服务端指向cgi程序,将执行后的结果返回给客户端

cgi:通用网关接口(Common Gateway Interface)是一个Web服务器主机提供信息服务的标准接口。通过CGI接口,Web服务器就能够获取客户端提交的信息,转交给服务器端的CGI程序进行处理,最后返回结果给客户端。
在这里插入图片描述
cgi程序通过标准输入输出以及环境变量与服务器进行通信。
因此 建立两个管道,cgi_input和cgi_output,并fork一个子进程。
在子进程 中:设置环境变量,且将 cgi_input[0]重定向为标准输入,将cgi_output[1]重定向为标准输出,调用cgi程序,使得 cgi 程序能从 cgi_input中获取数据,向cgi_output中输出数据。
在父进程中,父进程将POST请求的参数写入cgi_input ,将 cgi_output中的数据发送给客户端。

在这里插入图片描述

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'; //这是什么作用 就是初始化一下buf?
 /*get请求 执行cgi*/
 if(strcasecmp(method,"GET")==0)
 {
    /*把所有http 的header 读取并丢弃*/
   while((numchars>0) && strcmp("\n",buf))
     numchars=get_line(client,buf,sizeof(buf));  
 }
 
 else /*POST请求*/
{
 /*对POST的http请求中找出 content_length 
 http请求头部字段 每一行都有
 key:(空格)value \r\n
 */
 numchars=get_line(client,buf,sizeof(buf));
 while((numchars>0) && strcmp("\n",buf))
 {
 buf[15]='\0';
 /*查找 Content-Length 并把其他首部字段丢弃*/
 
 if(strcasecmp(buf,"Content-Length:")==0)
 /* int atoi(const char *str)
 字符中有非数字字符会被忽视!
 */
 content_length=atoi(&(buf[16]));
 numchars=get_line(client,buf,sizeof(buf));
 }
 
 /*没有找到 content_length*/
 if(content_length==-1)
 {
 /*错误请求*/
 bad_request(client);
 return;
 }
 }
 /*建立管道*/
 /*
 int pipe( int fd[2]);
 成功返回0 失败返回-1
 */
  if(pipe(cgi_output)<0)
  {
  /*错误处理*/
  cannot_execute(client);
  return;
  }
  /*
 int pipe( int fd[2]);
 成功返回0 失败返回-1
 */
  if(pipe(cgi_intput)<0)
  {
  /*错误处理*/
  cannot_execute(client);
  return;
  }
 
/*创建子进程*/
  /*
  pid_t fork(void)
  子进程返回0
  父进程返回子进程PID
  出错 返回-1
  */
  if((pid =fork())<0)
  {
  cannot_execute(client);
  return;
  }
   /* 正确  发送 请求成功 响应*/
 sprintf(buf,"HTTP/1.0 200 OK\r\n");
 send(client,buf,strlen(buf),0);

  /*子进程调用cgi脚本*/
 if(pid == 0) //子进程
 {
 char meth_env[255];
 char query_env[255];
 char length_env[255];
 //重定向管道 
 /*dup2(fd,1)
 关闭标准输出,本来应该写入到stdout的内容,标准输出重定向到fd的目标文件中
 dup2(fd,0)
 关闭标准输入 目标文件fd中的内容被当做标准输入
 */
 dup2(cgi_output[1],1); //把(屏幕)STDOUT重定向到cgi_output的写入端
 dup2(cgi_input[0],0);  //把(键盘)STDIN重定向到cgi_input的读取端
 close(cgi_output[0]);
 close(cgi_input[1]);
 /*
 设置环境变量
 */
 /*1.request_method的环境变量*/
 sprintf(meth_env,"REQUEST_METHOD=%s",method);
 putenv(meth_env);
 /*get方法 设置 query_string的环境变量*/
 if(strcasecmp(method,"GET")==0)
 {
  
  sprintf(query_env,"QUERY_STRING=%s",query_string);
  putenv(query_env);
 }
 else
 {
  /*POST 设置 content_ength的环境变量*/
  sprintf(length_env,"CONTENT_LENGTH=%d",content_length);
  putenv(length_env);
  
 }
 /*执行cgi程序 */
 execl(path,path,NULL);
 /*
 执行文件函数
 int execl(const char*path,const char*arg,...);
 path:文件路径 
 arg 参数数组 
 最后一个参数必须用空指针作结束
 */
 exit(0);
 }
 /*将请求参数写入 cgi_input管道*/
 else //父进程
 {
 close(cgi_output[1]);
 close(cgi_input[0]);
 if(strcasecmp(method,"POST")==0)
 {
  /*post  接收post传过来的数据*/
  
  for(int i=0;i<content_length;i++)
  {
   recv(client,&c,1,0);
   //一个字节一个字节从接收缓冲区中读取数据
   write(cgi_input[1],&c,1);// 写入 cgi_input管道 重定向到STDIN
   
  }
 }
 /*等待 cgi执行完返回数据 
 父进程将数据返回给客户端 (发送缓冲区)
 */
 while(read(cgi_cgi_output[0],&c,1)>0)
  send(client,&c,1,0);
  
 close(cgi_output[0]);
 close(cgi_input[1]);
 
 /*等待子进程 避免僵尸进程*/
  waitpid(pid,&status,0);
}

测试功能

在这里插入图片描述

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐