自主开发的小型Web服务器

1. 技术特点

  • 网络编程(http协议,TCP/IP协议,socket流式套接字)
  • 多线程技术(线程池)
  • CGI技术
  • Mysql

2. 具体步骤

  • HttpServer.hpp
  1. InitServer()
  2. Start()
  3. 并且将我们的HttpServer设计为单例模式中的懒汉模式(是一个线程安全的单例模式,既要保证安全所以要加锁,但是还要保证效率所以需要双检测
  4. 守护进程(守护进程就是通常讲Daemon进程,是linux后台执行的一种服务进程,特点是独立于控制终端、周期性地执行某种任务或等待处理某些发生事件,不会随终端关闭而停止,直到接受停止信息才会结束,且一般采用以d结尾的名字。)
  • Sock.hpp,对于网络接口的分装
  1. socket()
  2. bind()
  3. listen()
  4. accept()
  5. 线程池技术进行处理所监听到的sock
  6. Getline()一个专门从套接字中一次读取一行的接口,因为Http的request都是以行为分隔符来区分请求行、请求报头、空行、正文。
    补充知识:但是不同的浏览器可能传过来的分隔符是不相同的,有可能是\r、\r\n、\n三种,对于recv的第四个参数flags可以设置为MSG_PEEK,这相当于一个窥探的功能,也就是从内核上把数据读到,但是并没有拷贝到用户缓冲区中
  • Log.hpp
  1. 【日志级别】【message】【时间戳】【filename】【line】
  2. 日志级别:Notice、Warning、Error、fatal4个级别,级别属性依次增加
  3. message:解释
  • Protocol.hpp(最重要)
  1. Entry class 可以进行条件编译的一个类
  2. HttpRequest class 专门处理接收上来的请求文本
  3. HttpResponse class 专门处理准备发送的响应文本
  1. EndPoint class 专门负责通信
  • RecvRequest()
  1. 从socket读取到请求行、请求报头、空行、正文分别设置进HttpRequest class。
  • MakeResponse() 分析并且制作响应
  1. 从请求行需要得到method、url、version,对于本次的method只考虑POST和GET方法,如果不是这两种方法,则会直接返回响应,且错误码为404,对于url如果是GET方法需要区分是否带参数)(考虑到传过来的方式大小写要忽略的问题可以使用strcasecmp()接口,如果相等就返回0,不相等返回-1)

  2. 首先应该得到完整的path,有可能发过来的是一个目录,但是对于任意的一个Web服务器上的目录都有一个默认的index.html(/a/b这个b文件是一个目录,所以首先应该给它拼接上一个/然后再加上index.html),也有可能访问的就是一个普通的html(/a/b/html),还有可能会是访问一个可执行程序(a/b/exe)
    补充知识:

    struct stat {
    mode_t st_mode; // file type & mode(permissions)
    ino_t st_ino; // i-node number(serial number)
    dev_t st_dev; // device number(filesystem)
    dev_t st_rdev; // device number for specials files
    nlink_t st_nlink; // number of links
    uid_t st_uid; // user ID of owner
    gid_t st_gid; // group ID of owner
    off_t st_size; // size in bytes, for regular files
    time_t st_atime; // time of last access
    time_t st_mtime; // time of last modification
    time_t st_ctime; // time of last file status change
    long st_blksize; // best I/O block size
    long st_blocks; -//number of 512-byte blocks allocated
    };

  3. 在GET方法url中有参数,POST方法正文有数据,或者是要取访问一个可执行程序的时候就会触发CGI技术。

  • SendResponse()
  1. 因为已经将制作好的状态行、响应报头设置进了HttpResponse class中,所以可以直接调用所提供的接口进行发送状态行和响应报头
  2. 此时就剩下发送HttpResponse body了,但是需要判断是否CGI模式①GET方法中url有参数②POST方法有正文部分③分析出来的路径最终想要访问的是一个可执行程序。
  3. 判断完是否是CGI模式以后,执行两个不同的函数ExecCgi()或者ExecNonCgi(),对于执行ExecNonCgi()函数来说,目的就是打开一个文件然后通过socket返回(这里使用了一个sendfile()的系统调用接口,可以直接在内核进行两个文件的交互,不需要在先拷贝到用户再从用户拷贝给内核)
    ssize_t sendfile(int out_fd,int in_fd, off_t *offset,size_t count);
    在这里面in_fd should be file descriptor opend for reading
  4. 在处理ExecCgi()函数的时候,创建子进程,然后要把父进程的数据传递给子进程,并且想要这里严格的遵守CGI的传参标准,对于GET方法就是环境变量传参,对于POST因为有可能body很长,所以选择使用管道进行传参。环境变量具有全局属性,所以method可以通过putenv()进行设置,创建子进程,对于这个子进程最大的作用就是要去做程序替换,所以真正意义上是WEB服务器在和CGI程序打交道。这里使用到了进程间通信、进程替换、重定向技术。对于子进程分析方法如果是GET,那么就把query_string通过环境变量传递给CGI,但是对于CGI程序来说,在POST方法下,使用管道进行传参,传递给的只是子进程,CGI程序并拿不到数据,所以这里做了一个约定就是CGI程序可以从标准输入中读取,从标准输出中写入,那么这里就要使用到dup2()重定向接口。
  • Util.hpp
  1. 里面有一个Util class 存放个中方法比如StringToInt()等接口,来使用
  • ThreadPool.hpp
  1. 每监听到一个socket,就把该套接字封装成一个Task,然后塞进任务队列中,然后就是唤醒线程去执行特定的函数,检测到任务队列不为空拿出该任务进行Run()。
  • wwwroot(web根目录)
  1. 每一个目录都有一个默认的index.html
  2. 在W3C上下载了一个静态网页

程序流程解析图
在这里插入图片描述

3. CGI技术

CGI(Common Gateway Interface) 是WWW技术中最重要的技术之一,有着不可替代的重要地位。CGI是外部应用程序(CGI程序)与WEB服务器之间的接口标准,是在CGI程序和Web服务器之间传递信息的过程。

浏览器除了从服务器下获得资源(网页,图片,文字等),有时候还有能上传一些东西(提交表单,注册用户之类的),看看我们目前的http只能进行获得资源,并不能够进行上传资源,所以目前http并不具有交互式。为了让我们的网站能够实现交互式,我们需要使用CGI完成,时刻记着,我们目前是要写一个http,所以,CGI的所有交互细节,都需要我们来完成。

在我们实现上,要理解CGI,首先的理解GET方法和POST方法的区别:

  • GET方法从浏览器传参数给http服务器时,是需要将参数跟到URI后面的,具体如下:
    在这里插入图片描述
  • POST方法从浏览器传参数给http服务器时,是需要将参数放的请求正文的。
  • GET方法,如果没有传参,http按照一般的方式进行,返回资源即可
  • GET方法,如果有参数传入,http就需要按照CGI方式处理参数,并将执行结果(期望资源)返回给浏览器
  • POST方法,一般都需要使用CGI方式来进行处理
    在这里插入图片描述
    需要使用到创建子进程,然后数据还在父进程上,要传给子进程所以使用进程间通信,在程序替换(进程替换),去帮你执行,然后拿到想要的结果在返回给Web服务器,Web服务器在返回给浏览器的过程。

4. Mysql连接

  • 在官网中下载适合自己平台的Mysql connect库(指明include库路径和动态库路径以及要具体链接的动态库中的哪一个库,ldd命令可以查看文件所依赖的动态库,如果还找不到所以要导入一个环境变量export LD_LIBRARY_PATH=/…/…/…)
  • 通过mysql_get_client_info()函数,来验证我们的引入是否成功
  • Mysql接口介绍
  1. 初始化mysql_init(),如:MYSQL *mfp = mysql_init(NULL) 生成一个MYSQL的句柄
  2. 初始化完毕之后,必须先链接数据库,在进行后续操作。
    在这里插入图片描述
  3. 下发mysql命令mysql_query()
    int mysql_query(MYSQL *mysql, const char *q);
    第一个参数上面已经介绍过,第二个参数为要执行的sql语句,如“select * from table”。(增删改相对简单一些,直接封装成sql语句就好,查询反而还更复杂一些)
  4. 获取执行结果mysql_store_result() ,因为你查询出来的结果还保存在sql的缓冲区当中,所以需要一个这个接口能够从Mysql的缓冲区当中把数据读出来,原型:
    MYSQL_RES *mysql_store_result(MYSQL *mysql);同时该函数会返回MYSQL_RES 这样一个变量,该变量主要用于保存查询的结果。同时该函数malloc了一片内存空间来存储查询过来的数据,所以我们一定要记的 free(result),不然是肯定会造成内存泄漏的。 执行完mysql_store_result以后,其实数据都已经在MYSQL_RES 变量中了,下面的api基本就是读取MYSQL_RES 中的数据。
  5. 获取结果行数mysql_num_rows
    原型:my_ulonglong mysql_num_rows(MYSQL_RES *res);
  6. 获取结果列数mysql_num_fields
    原型:unsigned int mysql_num_fields(MYSQL_RES *res);
  7. 获取列名mysql_fetch_fields
    原型:MYSQL_FIELD *mysql_fetch_fields(MYSQL_RES *res);
  8. 获取行结果内容mysql_fetch_row
    原型:MYSQL_ROW mysql_fetch_row(MYSQL_RES *result);
  • 简单来说使用方式:比如表单形式提交name和passward,那么CGI程序获得http所发过来的query_string(name和passward),然后进行数据库连接,然后把name和passward封装成一条sql语句然后插入。如果是查询是否存在,那么前面步骤一样,只是封装成一个sql以后去查询,如果在那么验证通过,如果不存在那么就给返回一个注册的页面。但是在本次中只是通过url的类似方式直接显示的访问cgi目录下的sql_cgi的可执行程序。

5. Gitee原码链接

Gitee原码链接: https://gitee.com/meanswer/tiny-web-server

主体框架:自主开发的小型WEB服务器,可以获取静态以及交互式网页的请求与返回,服务器主要使用的是http协议和套接字以及多线程加上系统编程所实现。

6. 参考Blog

  • 开源Tinyhttp原码链接: link.

  • MSG_PEEK详解链接: link.

  • Linux 中的struct stat结构体详解链接: link.

  • gettimeofday()函数详解链接: link.

  • sendfile()函数详解链接: link.

  • CGI获取POST方法和GET方法传参详解链接: link.

  • 管道链接: link.

  • 单例模式链接: link.

  • 线程池链接: link.

  • 守护进程链接: link.

  • 压测工具(无法确定到底是服务器的上限还是带宽的上限)链接: link.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值