Linux C Http 文件服务器实现

来源:互联网 发布:刺客信条起源 知乎 编辑:程序博客网 时间:2024/04/29 02:50

备注:该文件服务器是根据Tinyhttpd的http源码的基础上实现的

整体思路: 创建socket监听请求,收到请求后线程分离,在线程中进行请求数据的解析;根据解析到的消息我们分类处理,目前该文件服务器可提供的文件包括html、css、jpg、gif;

1.监听请求,; 首先设置了端口复用,这个从字面理解就好了,就是说该端口上可以绑定多个socket;分配套接字,Http文件服务器是基于TCP传输协议; 初始化监听的地址结构体; 套接字与地址结构体绑定;

  /*端口复用*/  int opt = 1;  setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));  /*分配一个TCP套接字*/  server_fd = Socket(AF_INET, SOCK_STREAM, 0);     //TCP  /*初始化*/  bzero(&server_addr, server_addr_len);            //清空  server_addr.sin_family      = AF_INET;  server_addr.sin_port        = htons(SERVER_PORT);  server_addr.sin_addr.s_addr = htonl(INADDR_ANY);   /*绑定*/  bind(server_fd, (struct sockaddr*)&server_addr, server_addr_len);  listen(server_fd, 128); //最多可支持128个请求连接同时在服务器 监听套接字 上等待处理                                            printf("httpServer running on port %d \n", SERVER_PORT);

2.循环监听,这里的accept函数是从步骤1中的listen函数的等待请求队列里接受一个连接; 自定义结构体,打包了文件描述符,地址,地址大小; 创建线程并分离,所有的处理工作在do_work函数中;

/*  自定义结构体    struct t_client_info{       int                 t_fd;       struct sockaddr_in  t_addr;       socklen_t           t_addr_len;    };*/while(1)  {    client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_addr_len);    t_info.t_fd       = client_fd;                 //该部分是我自定义的结构体    t_info.t_addr     = client_addr;               //用于打包客户端套接字信息    t_info.t_addr_len = client_addr_len;           //方便传递到下个线程    /*线程*/    pthread_create(&t_client, NULL, do_work, (void *)&t_info); //创建线程    pthread_detach(t_client);    }

3.do_work()函数

3.1 初始化

 /*初始化*/  Pt_this = (struct t_client_info *)arg;   //从父线程上传递来的自定义结构体  int                 client_fd        = Pt_this->t_fd;  struct sockaddr_in  client__addr     = Pt_this->t_addr;  socklen_t           client_addr_len  = Pt_this->t_addr_len;

3.2 进入函数后不要盲目的直接调用开始处理请求行,我们先判断这个客户端的下一步动作,
如果recv返回了但是num为0,说明客户端套接字主动断开连接,这里直接goto到线程
收尾工作;

//判断缓冲区是否有东西可读,用MSG_PEEK,不会移动滑动窗口  int num=0;  char c;  num = recv(client_fd, &c, 1, MSG_PEEK);  if (num != 1)     goto workEND;

3.3 处理请求头的第一行数据,一般格式是 (方法 文件路径 协议版本)
例如:GET /index.html HTTP/1.1
注意: #define ISsp(x) isspace((int)(x)) //用于判断空格;
get_line()函数 获取一行数据,自定义函数,以 \r\n判断;

/*接收请求行的数据*/  numchars = get_line(client_fd, buf, sizeof(buf));  i=0; j=0;  //获取方法字段   while (!ISsp(buf[i]) && (i<sizeof(method)-1))  //留一格用在‘\0’   {     method[i] = buf[i];      i++;   }   j = i; //记录当前空格的位置   i = 0;   method[i] = '\0'; //判断方法   if (strcasecmp(method, "GET")     && strcasecmp(method, "POST")) //忽略大小写比较字符串,相同返回0;                                    //这里是除了get和post以外都返回      goto workEND;    //如果请求方法是POST,我们用调用cgi程序,cgi==1 调用cgi cgi==0 提供静态文件   if (strcasecmp(method, "POST") == 0)  //POST       cgi = 1;

3.4 获取URL,即获取客户端想要请求的文件路径;

 //j当前的位置是method后面的空格     while (ISsp(buf[j]) && (j<numchars))           j++;    //获取URL    while (!ISsp(buf[j]) && (i<sizeof(url)-1) && (j<numchars))    {       url[i] = buf[j];       i++; j++;    }    url[i] = '\0';

3.5 客户端请求的URL一般是请求的Http文件服务器的站点目录
如果请求的是 /index.html ===> 那么在我的设置里对应的 是/root/Project/WebFileServer/File/index.html
一定要区分客户端请求的根目录其实是服务器上的站点根目录

/*保存有效的url并加上默认主页索引, 默认在 */    /*      #define ROOT /root/Project/WebFileServer/File%s    */     sprintf(path, ROOT, url);    //最后结尾是‘/’代表请求的是一个目录文件服务器应该返回该目录中的index.html    if (path[strlen(path)-1] == '/')              strcat(path, "index.html");//获取了URL后,再获取该URL的文件信息,如果获取失败,那就结束这次响应/*通过文件名path获取文件信息,并保存在st所指向的结构体stat中*/    if (stat(path, &st) == -1)   //获取失败,丢弃剩下的数据    {       while ((numchars>0) && strcmp("\n", buf))  //不是\n返回1          numchars = get_line(client_fd, buf, sizeof(buf));        /*           这里可以返回404页面        */        goto workEND;    }    if ((st.st_mode & S_IFMT) == S_IFDIR) //目录类型       strcat(path, "/index.html");       /*检查文件路径是否存在*/       int exist = access(path, F_OK);       if (exist == -1) //不存在

3.6 返回文件根据cgi的值,cgi等于1代表是POST,我们提交到cgi程序处理;
cgi等于0代表是普通的GET,文件服务器直接返回静态文件;

/*返回文件*/       if (!cgi)  //返回静态文件           server_file(client_fd, path);    //自定义函数,返回静态文件       else       {           /*              这里实现调用cgi程序           */       }workEND:    close(client_fd);

4.get_line()函数 : 该函数为Tinyhttpd源码,可以放心的使用

int get_line(int sock, char *buf, int size){  printf("##get_line## \n");          // 1.字符   2.字符\r    3.字符\r\n  int i = 0;      char c = '\0';      int n;      /*把终止条件统一为 \n 换行符,标准化 buf 数组*/      while ((i < size - 1) && (c != '\n'))      {          /*一次仅接收一个字节*/          n = recv(sock, &c, 1, 0);           if (n > 0)          {              /*收到 \r 则继续接收下个字节,因为换行符可能是 \r\n */              if (c == '\r')              {                  /*使用 MSG_PEEK 标志使下一次读取依然可以得到这次读取的内容,可认为接收窗口不滑动*/                  n = recv(sock, &c, 1, MSG_PEEK);                    /*但如果是换行符则把它吸收掉*/                  if ((n > 0) && (c == '\n'))                      recv(sock, &c, 1, 0);                  else                      c = '\n';              }              /*存到缓冲区*/              buf[i] = c;              i++;          }          else              c = '\n';      }      buf[i] = '\0';      /*返回 buf 数组大小*/      return(i);} 

5.提供静态文件(其中包括html、css、jpg、gif、下载文件)

void server_file(int fd, const char *filename){  printf("##server_file## \n");  char *string = NULL;  string = &filename[strlen(filename)-4];  else if (strcasecmp(string, ".jpg") == 0)     pic(fd, filename);  else if (strcasecmp(string, ".gif") == 0)     gif(fd, filename);  else if (strcasecmp(string, ".css") == 0)     css(fd, filename);  string = &filename[strlen(filename)-5];  if (strcasecmp(string, ".html") == 0)     html(fd, filename);  /*如果不是,则下载*/   Download(fd, filename);}

6.这里给出只给出提供html文件的函数和下载文件函数;其他函数实现类似

/*    返回html,注意按照Http协议格式      |    |    |    |    |    |    |    |    |    |    |    |      v    v    v    v    v    v    v    v    v    v    v    v*/void html(int fd, const char *filename){  FILE *resource = NULL;  int numchars = 1;  char buf[BUFSIZ];  buf[0] = 'A'; buf[1] = '\0';  while ((numchars > 0) && strcmp("\n", buf))      /*这里要读完请求头的所有数据*/      numchars = get_line(fd, buf, sizeof(buf));  resource = fopen(filename, "r");  if (resource == NULL)  {    fclose(resource);    return ;  }  else  {       //注意:可以使用filename的信息来返回文件类型        strcpy(buf, "HTTP/1.1 200 OK\r\n");        send(fd, buf, strlen(buf), 0);        strcpy(buf, SERVER_STRING);        send(fd, buf, strlen(buf), 0);        sprintf(buf, "Content-Type: text/html\r\n");        send(fd, buf, strlen(buf), 0);        strcpy(buf, "\r\n");        send(fd, buf, strlen(buf), 0);        fgets(buf, sizeof(buf), resource);        while (!feof(resource))   //其功能是检测流上的文件结束符,如果文件结束,则返回非0值,否则返>回 0,文件结束符只能被clearerr()清除        {            send(fd, buf, strlen(buf), 0);            fgets(buf, sizeof(buf), resource);        }                                 }  fclose(resource);}

下载文件需要注意的是返回类型,在html返回中服务器的发送的响应头里,Content-Type设置的是text/html;在下载类型中需要采用application/octet-stream,此为下载类型,用二进制传输文件;
注意:该下载文件实现是用的大文件分块传输,切记发送数据时要保证文件要关闭,不要在打开文件的状态传输数据,当客户端关闭时服务器会退出;

void Download(int fd, const char *filename){  printf("##Download## fd:%d \n", fd);  FILE *resource = NULL;  char *p = NULL;  int numchars = 1;  char buf[BUFSIZ];  long file_len=0;  buf[0] = 'A'; buf[1] = '\0';                 //拿出所有的请求消息  while ((numchars > 0) && strcmp("\n", buf))      numchars = get_line(fd, buf, sizeof(buf));  resource = fopen(filename, "r");  if (resource == NULL)  {     printf("file  not found \n");     fclose(resource);     return ;  }     fseek(resource, 0, SEEK_END); //一直到文件尾     file_len = ftell(resource);    //长度     //rewind(resource);             //将文件指针指向开头     fclose(resource);     //注意:可以使用filename的信息来返回文件类型     strcpy(buf, "HTTP/1.1 200 OK\r\n");     send(fd, buf, strlen(buf), 0);     strcpy(buf, SERVER_STRING);          //自定义宏,服务器信息     send(fd, buf, strlen(buf), 0);       sprintf(buf, "Connection: keep-alive\r\nContent-type:application/octet-stream\r\n");     send(fd, buf, strlen(buf), 0);     sprintf(buf, "Content-Length: %ld\r\n\r\n", file_len);     send(fd, buf, strlen(buf), 0);     //在\r\n\r\n后面就该放数据了,这里采用大文件分块     //写入文件内容      long seek=0;      long wait_len = file_len - seek;      long send_len=0;      double mb=0;      if (wait_len > FILELEN)   //自定义宏,暂时设为1MB            send_len = FILELEN;      else        send_len = wait_len;     /*动态分配空间,在这里没有做错误处理*/      p = (char *)malloc(sizeof(char)*send_len);      int sendnum=0;       while(1)      {        resource = fopen(filename, "r");        if (resource == NULL)        {          printf("file open false\n");          fclose(resource);          return ;        }        if (!fread(p, sizeof(unsigned char), send_len, resource))break;        fclose(resource);        //(自定义函数,代码在下面)判断套接字是否断开        //如果在对方断开时继续send,将会出现一些异常        if (isConnection(fd) == -1)  //0连接 -1断开           break;        send(fd, p, send_len, 0);        printf("sendnum:-%d-\n", sendnum);        seek = seek + send_len;        wait_len = file_len - seek;        if (wait_len > FILELEN)                 send_len = FILELEN;        else          send_len = wait_len;        mb = (double)((double)seek/(double)1048576);        printf("seek:-%ld-===>-%lfMB-\n", seek, mb);      }  }       free(p);     p = NULL;     printf("Download end.\n");//自定义函数,判断连接是否断开int isConnection(int fd){   struct tcp_info info;    int len=sizeof(info);    getsockopt(fd, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *)&len);    if((info.tcpi_state == TCP_ESTABLISHED))      return 0;   else      return -1;}

7.文件上传
首先是客户端,一般采用POST方法提交到文件服务器上的一个cgi程序,在步骤3.6中我们实现了提供静态文件,这里来描述如果利用cgi上传文件到服务器;
步骤为:客户端按下按键发送一个带有文件的POST请求 ==> 服务器接收请求并获取文件信息 ==> 服务器接收文件数据并存储

客户端的提交信息:
POST /CGI/down.cgi?filename=xxx HTTP/1.1\r\n
… …\r\n\r\n
(file data)

这里请求了根目录下CGI目录中的down.cgi这个程序,提交的文件名放在xxx这个位置,这部分是在客户端实现的;服务器收到请求调用down.cgi ; 该cgi可以用C写也可以用Shell,Python… … 本次采用的C语言实现cgi程序;

void execute_postFile(int fd, char *path, char *method, char *requestline){    char file[] = "/root/Project/WebFileServer/File/CanUseFile/";  //这是我保存文件的路径    char buf[BUFSIZ];    int numchars = 1;    int content_len = -1;    buf[0]='A';buf[1]="\0";                //如果是GET我们不处理;拿出所有请求数据    if (strcasecmp(method, "GET") == 0)       while ((numchars>0) && strcmp("\n", buf))          numchars = get_line(fd, buf, sizeof(buf));    else if (strcasecmp(method, "POST") == 0)    {       /*请求头中我们需要Content-Length*/       numchars = get_line(fd, buf, sizeof(buf));       while ((numchars>0) && strcmp("\n", buf))       {            buf[15] = '\0';          if (strcasecmp(buf, "Content-Length:") == 0)             content_len = atoi(&(buf[16]));          numchars = get_line(fd, buf, sizeof(buf));          printf("--%s-- \n", buf);       }       if (content_len == -1)       {            printf("bad request \n");          return ;       }    }    else    {      //other method    }    /*获取requestline中的文件名,  ?filename=xxx*/    char *post_string = requestline;    char filename[BUFSIZ];    int filenum = 0;    while ((*post_string != '?') && (*post_string != '\0'))       post_string++;    if (*post_string == '?')    {      post_string = post_string + 10;      while (!ISsp(*post_string))      {         filename[filenum] = *post_string;         filenum++;         post_string++;      }      filename[filenum] = '\0';    }    strcat(file, filename);    printf("file:-%s-", file);    FILE *fp = NULL;    fp = fopen(file, "wb");   //以二进制写入,创建文件    if (fp == NULL)    {       printf("file  not found \n");       fclose(fp);       return ;    }    int i;    char c;    for(i=0; i<content_len; i++)    {      recv(fd, &c, 1, 0);      fwrite(&c, sizeof(unsigned char), 1, fp);    }    fclose(fp);    /*    这里可以向客户端返回保存信息    */   }

感谢你耐心的看完了这篇文章,希望能够帮助到你

原创粉丝点击