首页 > 其他分享 >Unix C的Http服务器技术实现原理

Unix C的Http服务器技术实现原理

时间:2023-07-11 22:45:55浏览次数:50  
标签:cgi Http int pipe Unix file printf 服务器 include

基于tiny-httpd的一个http server,可处理 GET和POST请求。

知识范围:

POSIX接口

pipe(int arr[2])
pipe(int arr[2]); 使用pipe会创建通道,arr[0]为读,arr[1]为写。

dup2 - 复制文件描述符

这个fd我目前理解是用来读数据的,使用dup2相当于直接复制了oldfd对应的数据

dup2(oldfd,newfd)
dup2(arr[1],1);
dup2(arr[0],0);

官方文档内容:

On program startup, the integer file descriptors associated with the streams stdin, stdout, and stderr are 0, 1, and 2, respectively.
The preprocessor symbols STDIN_FILENO, STDOUT_FILENO, and STDERR_FILENO are defined with these values in <unistd.h>.

  • fd也就是文件描述符

    • 0 表示 STDIN 标准输入流
    • 1 表示 STDOUT 标准输出流
    • 2 表示 STDERR 标准错误输出流

I/O 操作

fopen的介绍,fopen(filename,mode)
mode 填写 w 就是创建和覆盖的意思,上传文件的时候可以直接使用w

       The fopen() function opens the file whose name is the string pointed to
       by pathname and associates a stream with it.

       The argument mode points to a string beginning with one of the  follow‐
       ing sequences (possibly followed by additional characters, as described
       below):

       r      Open text file for reading.  The stream is positioned at the be‐
              ginning of the file.

       r+     Open  for  reading and writing.  The stream is positioned at the
              beginning of the file.

       w      Truncate file to zero length or create text  file  for  writing.
              The stream is positioned at the beginning of the file.

       w+     Open  for  reading  and writing.  The file is created if it does
              not exist, otherwise it is truncated.  The stream is  positioned
              at the beginning of the file.

       a      Open  for  appending (writing at end of file).  The file is cre‐
              ated if it does not exist.  The stream is positioned at the  end
              of the file.

       a+     Open  for  reading  and appending (writing at end of file).  The
              file is created if it does not exist.  Output is always appended
              to  the  end  of  the file.  POSIX is silent on what the initial
              read position is when using this mode.  For glibc,  the  initial
              file  position  for reading is at the beginning of the file, but
              for Android/BSD/MacOS, the initial file position for reading  is
              at the end of the file.

需要实现一个类似于把文件读取 到连接的文件描述符中

文件读取和写入

#include <stdio.h>

void create_newfile(char *filename){
  FILE* res = NULL;
  res = fopen(filename, "w");
  char bus[1024];
  sprintf(bus, "{\n\t\"a\":\"%s\"\n}",filename);
  
  fputs(bus, res);
  fflush(res);
  fclose(res);
}

int main(void) {
  char *a = "./b.txt";

  FILE *res = NULL;
  char buf[1024];
  int i = -1;
  
    // 创建文件并写文件
  create_newfile(a);
  
    // 读文件
  res = fopen(a, "r");
  printf("\n");
  fgets(buf, sizeof buf, res);
  
  while(!(i = feof(res))) {
    printf("%s",buf);

    fgets(buf, sizeof buf, res);
  }
  printf("%s",buf);
  
  printf("\n");
  printf("i = %d\n",i);
  fclose(res);
  
  return 0;
}

使用子进程执行cgi程序

这里的重点是需要了解 父进程是可能比子进程 早完成,所以需要使用wait() 或者 waitpid()进行等待子进程完成

// child thread cgi
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

int main(void) {
  char *path = "c.cgi";
  int pid = -1,status = -1;
    
  if((pid = fork()) < 0) {
    perror("CreateP");
    exit(1);
  }
  if(pid == 0) {
    execl(path,path,NULL);
    exit(0);
  }else {
    printf("Faker\n");
    // 如果不等待子进程完成,那么父进程可能比子进程先执行完,那么子进程就变成了孤儿进程
    // 会导致 程序无法正常退出,所以需要使用wait() 或 waitpid()
    waitpid(pid, &status, 0);
    printf("Status:%d\n",status);
    printf("Pid:%d\n",pid);
  }
  return 0;
}

使用dup2改变标准输入输出流

作用就是可以直接拿子进程的输入输出流的数据,如果需要输入数据那么就操作 STDIN(0) ,获取子进程的输出数据 STDOUT(1)

因为使用的是操作 文件描述符,适合底层操作,因为POSIX中用的就是文件描述符,TCP 通讯也是 文件描述符 (就是那个 fd)

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int creat(const char *pathname, mode_t mode);
                   
/*
 flags
 
O_RDONLY:以只读方式打开文件。
O_WRONLY:以只写方式打开文件。
O_RDWR:以读写方式打开文件。
O_CREAT:如果文件不存在,则创建一个新文件。
O_APPEND:在文件末尾附加数据,而不覆盖现有内容。
O_TRUNC:如果文件已存在,则截断(清空)文件。

mode
S_IRUSR:用户(owner)可读权限
S_IWUSR:用户(owner)可写权限
S_IXUSR:用户(owner)可执行权限

*/
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>

int main(void) {
  char *path = "c.cgi";
  char *log = "log";
  int pid = -1,status = -1;
  int pfd = -1;
  pfd = open(log, O_WRONLY | O_CREAT | O_TRUNC,S_IRUSR | S_IWUSR);
  
  if((pid = fork()) < 0) {
    perror("CreateP");
    exit(1);
  }
  
  if(pid == 0) {
    // 直接将 STDOUT 输入到 文件夹
    dup2(pfd,1);
    execl(path,path,NULL);
    exit(0);
  }else {
            
    printf("Faker\n");
    // 如果不等待子进程完成,那么父进程可能比子进程先执行完,那么子进程就变成了孤儿进程
    // 会导致 程序无法正常退出,所以需要使用wait() 或 waitpid()
    waitpid(pid, &status, 0);
    close(pfd);
    printf("Status:%d\n",status);
    printf("Pid:%d\n",pid);
  }
  return 0;
}

使用管道进行文件描述转换

pipe(pipe_array) 创建一个管道

使用 dup2() 复制 STDOUT 到 pipe_array[1]

等程序执行完成后,再把 pipe_array[0] 中的数据写到文件中去

// child thread cgi
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>

int main(void) {
  char *path = "c.cgi";
  char *log = "log";
  char buf[1024];
  char c;
  int pid = -1,status = -1;
  int pfd = -1;
  int pipe_output[2];

  // disable umask
  // 还需要重新学一下怎么配置umask
  // umask control file privilege,if you dont disable or set umask, than you will cant change some privilege about new file
  umask(0);
    pfd = open(log, O_WRONLY | O_CREAT | O_TRUNC, 0777);
  printf("FILE P:%d\n", S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);

  if(pipe(pipe_output) < 0){
    perror("PIPE");
    exit(1);
  }

  printf("PIPE_OUTPUT:%d\n",pipe_output[1]);
  
  if((pid = fork()) < 0) {
    perror("CreateP");
    exit(1);
  }
  
  if(pid == 0) {
    // 直接将 STDOUT 输入到 管道中
    dup2(pipe_output[1],1);
    execl(path,path,NULL);
    // finished writing, should close the file descriptor
    close(pipe_output[0]);
    exit(0);
  }else {
    // in parent process, the first should close the writing pipe 
    close(pipe_output[1]);
    
    printf("Faker\n");
    
    waitpid(pid, &status, 0);
    int read_len = read(pipe_output[0],&buf,sizeof buf);
    printf("read_len:%d\n",read_len);
    
    while( read_len > 0) {
      // if data dont have 1024 bytes size, shoud add \0 in the end of content
      if(read_len < 1024 ) {
        buf[read_len] = '\0';
      }
      write(pfd,&buf,read_len);
      read_len = read(pipe_output[0],&buf,sizeof buf);
    }

    printf("Buf:\n%s",buf);
    printf("read_len: %d\n",read_len);
    close(pfd);
    close(pipe_output[1]);
    
  }
  return 0;
}

设置Socket状态

建立套接字后需要设置套接字的状态,须使用 setsockopt()

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int getsockopt(int sockfd, int level, int optname,
                void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,
                const void *optval, socklen_t optlen);

解释一下参数的意思
sockfd, 就是sock的文件描述符
level就是sock所处的层次 一般用SOL_SOCKET 代表TCP
optname 表示需要设置的选项

```
SO_DEBUG:打开或关闭排错模式
SO_REUSEADDR:在bind()函数中允许本地IP地址可重复使用
SO_TYPE:返回socket形态
SO_ERROR:返回socket已发生的错误原因
SO_DONTROUTE:发送的数据包不要利用路由设备来传输
SO_BROADCAST:使用广播方式传送
SO_SNDBUF:设置发送的暂存区大小
SO_RCVBUF:设置接收的暂存区大小
SO_KEEPALIVE:定期确定连线是否已终止
SO_OOBINLINE:当接收到OOB 数据时会马上送至标准输入设备
SO_LINGER:确保数据可以安全可靠地传送出去。
```

optval 设置参数返回值的指针
optlen表示optval的长度。
setsockopt 执行成功返回 0 ,执行失败返回1

Makefile 简单使用

单文件的,先简单使用一下,主要是先用起来,后面再补

CC = gcc

BIN = make_test

BIN_DIR = bin
OBJS_DIR = obj
SRC_DIR = src

OBJS = ../$(OBJS_DIR)/%.o

all:COMPILE

COMPILE: CHECK_DIR
	$(CC) -c $(SRC_DIR)/*.c
	mv *.o $(OBJS_DIR)
	$(CC) -o $(BIN) $(OBJS_DIR)/*.o

CHECK_DIR: ECHO
	@mkdir -p $(BIN_DIR)
	@mkdir -p $(OBJS_DIR)

ECHO: 
	@echo $(SUBDIRS)
	@echo begin compile

clean:
	@$(RM) $(OBJS_DIR)/*.o
	@rm -rf $(BIN_DIR)

Tiny Httpd实现步骤

  1. 创建一个 listenfd 用于 绑定端口

    • 设置 sockfd 的状态为 setsockopt(connfd, SOL_SOCKET, SO_REUSEADDR, &re, sizeof(re)) ;
    • 使用htons 绑定端口, htonl(INADDR_ANY) 绑定 0.0.0.0 的IP 地址
  2. 通过accept接收一个关于listenfd的 链接 connfd

  3. 使用recv接收connfd的请求头数据,如下所示

    RECV N: 82
    GET /path HTTP/1.1
    Host: 127.0.0.1:8081
    User-Agent: curl/7.74.0
    Accept: */*
    
  4. 获取特定的函数处理以上请求头,获取请求体

  5. 在发送数据之前先把请求头写到connfd中

    • 还需要完成 一些错误处理的函数
    • 根据 数字返回请求头 headers(int connfd,int HttpStatus)
    • HttpStatus 用枚举可好? HTTP_OK , HTTP_NOT_FOUD
  6. 判断是否要执行 cgi,就是说 如果请求的 /path 是 cgi的名字,那么就执行cgi

    1. 先把准备好的请求参数放到子进程的运行环境中
    2. cgi程序需要位于 /cgi目录于 请求时 类似于 请求 http://127.0.0.1/cgi/test.cgi
    3. 如果 path 前缀是 /cgi 则判断文件 是否存在,存在则执行,不存在则报404
  7. 判断是否是静态文件,反正不在 /cgi 的都是静态文件,默认 请求的path为 / ,指向 /index.htmlindex.cgi

    • index.html为静态文件
    • index.cgi为 c语言的可执行程序
    • 如果这两个都不存在,则报 404
  8. 执行完后,需要close(connfd)

标签:cgi,Http,int,pipe,Unix,file,printf,服务器,include
From: https://www.cnblogs.com/pphboy/p/17546148.html

相关文章

  • 老杜 JavaWeb 讲解(九) ——模板方法设计模式、HttpServlet源码分析
    (十一)模板方法设计模式、HttpServlet源码分析对应视频:20-HttpServlet源码分析及web欢迎页11.1模板方法设计模式不用使用在上面右侧表格中,Person就是模板方法设计模式当中的模板类,通常是抽象类。day()方法就是模板方法设计模式当中的模板方法。模......
  • pycharm git找不到远程服务器新建的分支
    1、从Termianl打开终端,进到.get目录2、执行命令:gitremoteupdateorigin--prune3、从Pycharm编写代码界面的右下角,可以看到一个master的按钮,搜索到最新分支4、checkout就切换到对应分支了......
  • NBD(Network Block Device)是一种用于网络存储的协议和技术。NBD服务器是一种提供网络块
    NBD(NetworkBlockDevice)是一种用于网络存储的协议和技术。NBD服务器是一种提供网络块设备服务的服务器,它允许用户通过网络连接来访问和管理块设备(如硬盘、SSD等),就像本地设备一样。NBD服务器的工作原理如下:NBD服务器将物理或虚拟块设备暴露为网络上的NBD设备。客户端使用NBD客......
  • shell脚本-监控多台服务器磁盘利用率
    shell脚本-监控多台服务器磁盘利用率介绍第一步:实现免密登录服务器,为后续脚本免密登录做好准备。第二步:把要监控服务器的ip地址root用户端口port写入host.info文件中以便后续脚本从这个文件读取服务器信息。第三步:写shell脚本,先从host.info中拿到信息连接各个服务器,读取......
  • nvm安装node报错Get "https://nodejs.org/dist/latest/SHASUMS256.txt": dial tcp 104
    windows上通过nvm管理node版本,在本地安装了nvm后,通过nvm安装node,报错了,信息:Couldnotretrievehttps://nodejs.org/dist/latest/SHASUMS256.txt.Gethttps://nodejs.org/dist/latest/SHASUMS256.txt:dialtcp104.20.23.46:443:i/otimeout 有了这样的信息,我......
  • 云服务器的连接,客户端出现“服务器发送了一个意外的数据包。received: 3,expected: 2
    云服务器的连接,客户端出现“服务器发送了一个意外的数据包。received:3,expected:20的解决方案编辑/etc/ssh/sshd_config文件并添加四行代码ClientAliveInterval60ClientAliveCountMax3PermitRootLoginyesKexAlgorithmscurve25519-sha256@libssh.org,ecdh-sha2-......
  • 服务器数据恢复-RAID5故障导致ESXI虚拟机数据丢失的数据恢复案例
    服务器数据恢复环境:曙光某型号光纤存储柜,16块光纤磁盘组建了2组RAID5磁盘阵列,每组raid5阵列中有7块成员盘,另外2块磁盘配置为全局热备盘使用。第一组RAID5阵列划分了3个LUN:1个LUN分配给linux主机、第2个LUN分配给sun小型机,第3个LUN分配给esxi主机。第二组RAID5阵列全部分配给一台......
  • finshell 连接不到服务器,报Session.connect: java.net.SocketException: Connection r
    用finshell一段时间了,非常不错,但是有段时间突然连接不上服务器,各种重装,重启服务器都不行,在网上搜方法也没有好的办法。在我一次实在烦的不得了的时候,让我发现一个好的解决方案。先上图: 是不是出现这个问题,那么我的解决方案是啥呢?看我的手速,就是点击红色的闪电图标,一般连续点击......
  • Tenable Nessus 10.5.3 (Unix, Linux, Windows) - #1 漏洞评估解决方案
    TenableNessus10.5.3(Unix,Linux,Windows)-#1漏洞评估解决方案发布Nessus试用版自动化安装程序,支持macOSVentura、RHEL9和Ubuntu22.04请访问原文链接:https://sysin.org/blog/nessus-10/,查看最新版。原创作品,转载请保留出处。作者主页:sysin.orgNessus漏洞评......
  • WinHttpSetCredentials
    WinHttpOpen、WinHttpConnect:初始化WinHTTP函数的使用并返回WinHTTP会话句柄。这里的会话和前文说到的服务端维护的会话不是一回事,个人理解是类似于Socket编程中返回的一个套接字描述符,后续代码利用这个描述符来进行网络编程。在与服务器交互之前,必须通过调用WinHttpOpen来初......