前言
我们所要实现的是一个高并发服务器的组件,使服务器的性能更加高效,是一个高并发服务器的组件,并不包含实际的业务。
首先需要先明确我们所要实现的目标是什么
- 第一点,实现一个高并发的服务器
- 第二点,在服务器的基础上提供应用协议的支持
HTTP服务器
Server模块
服务器模块是对所有连接以及线程进行管理,让他们各司其职,在合适的时间做对应的事,最终完成高性能服务器组件的实现,具体管理分为以下几个方面。
- 要想获取新连接,必须先搭建一个监听套接字来获取新连接,需要对监听连接进行管理
- 获取新连接之后就有了通信连接的套接字,需要有一个通信连接的管理
- 如果有一些恶意的客户端,连接上我的服务器之后并不进行通信,就是占着我服务器的资源,所以就需要进行超时连接的管理。
Buffer模块
- 缓冲区模块,当一次从socket读取数据时可能读到的信息不是完整的,因此需要对读到的数据进行缓存,当服务器应答时,向socket套接字写入时,可能socket已经满了,或者写不了一个完整的数据时,会进入到阻塞状态,所以应该在套接字可写入的情况下发送
- 功能设计:向缓冲区中添加数据;从缓冲区中读取数据;
Socket模块- 对socket套接字的各项操作的封装,让程序中对套接字的操作更加方便
- 功能设计:创建套接字、绑定地址信息、开始监听、向服务器发起连接、获取新连接、接收连接、发送连接、关闭套接字,为了方便我们需要对基础操作做一个集成,例如:要创建一个监听套接字,需要一个个的调用,集成一个创建监听套接字
- 在基础操作之上集成的新功能:创建监听套接字、客户端连接
Channel模块
功能设计:- 设置可写事件监控、设置可读事件监控、
- 删除可写事件监控、删除可读事件监控、删除所有事件监控
- 判断是否设置了可写事件、判断是否设置了可读事件
- 可读、可写、挂断、错误、任意五个事件需要处理,就需要5个回调函数
Poller模块- Poller模块是对IO多路转接epoll的封装,对于一个描述符的可读、可写的事件监控操作更加简单。
- 功能设计:添加事件监控、修改事件监控、移除事件监控
Connection模块- 是对连接、通信管理的模块,前面的模块是单独独立的模块,根据Connection模块所实现的内容,需要将前面独立模块组织起来,Connection模块中一个连接有任何的怎么处理都是有这个模块来进行处理的,因为组件的设计也不知道使用者要如何处理事件,因此只能提供一些回调函数由使用者来处理。
- 功能设计:关闭连接、发送数据、 连接建立完成的回调、新数据接收成功的回调、连接关闭时的回调、产生任何事件进行的回调。
- 不同协议有不同的处理方式,所以需要有一个协议切换的功能,本质上是替换回调函数,当有一些恶意的连接,连上之后很长时间不发,数据,一直占用的资源,所以需要有超时连接释放的功能,启动非活跃连接超时释放的功能、该功能可以默认是关闭的,在需要使用的时候再打开,但是在有些情况不需要该功能,所以该功能最好做成可选的功能,所以再添加一个关闭非活跃连接超时释放的功能。
Acceptor模块- Acceptor模块也是一个管理模块,Connection模块是针对通信套接字进行管理的模块,Acceptor模块是针对监听套接字进行管理的模块,都需要做哪些管理呢?
- 当一个监听套接字已经开始监听了,通过Socke模块中的调用Accept成员获取新建连接,Accept返回的是一个文件描述符,文件描述符对于我们来说太原始了,接下来需要多通信套接字进行操作,所以我们需要将该文件描述符封装成Connection对象,该连接是一个新连接,是在此处设置相关的回调。
- 功能设计:当前Acceptor模块也不知道具体的回调函数,所以需要对外提供一个回调函数设置功能
TimerQueue模块- 定时任务模块,让一个任务在指定的时间之后被执行
- 功能设计:添加定时任务、刷新定时任务、取消定时任务
EventLoop模块
功能设计:
将一个任务添加到执行任务的队列的功能,因为一个连接所有的操作都要在EventLoop中完成,但是一个EventLoop要监控很多个连接,假设上层的线程池处理完一个业务,要发送数据,发送数据不能在自己的线程完成,发送的任务要在EventLoop模块中完成,这样才能避免线程安全的问题。一个线程一个EventLoop,一个EventLoop有多个Connection(一个EventLoop不止监控一个连接,要监控很多个连接),一个Connection只能在一个EventLoop中
对于服务器中所有的事件都是由EventLoop模块来完成的,为什么要这样去做呢?因为每一个Connection连接都会绑定一个EventLoop模块和线程,因为外界对于连接的所有操作都要放到同一个线程中进行操作,EventLoop模块为了保证整个服务器的线程安全问题,对于Connection的所有操作⼀定要在其对应的EventLoop线程内完成,不能在其他线程中进行。
具体操作流程:
- 通过Poller模块对当前模块管理内的所有描述符进⾏IO事件监控,有描述符事件就绪后,通过描述符对应的Channel进⾏事件处理。
- 所有就绪的描述符IO事件处理完毕后,对任务队列中的所有操作顺序进⾏执⾏。
- 由于epoll的事件监控,有可能会因为没有事件到来⽽持续阻塞,导致任务队列中的任务不能及时得到执⾏,因此创建了eventfd,添加到Poller的事件监控中,⽤于实现每次向任务队列添加任务的时
- 候,通过向eventfd写⼊数据来唤醒epoll的阻塞。
TcpSever模块
前面的所有模块不是功能模块就是管理模块,而这个模块是提供给使用者,让组件的使用者使用起来更加容易。
功能设计:
- 对监听连接的管理,获取一个新连接之后如何处理,由TcpSever模块设置
- 对通信连接的管理,一个连接产生了某个事件如何处理,由TcpSever模块设置
- 对超时连接的管理,连接非活跃超时是否关闭连接,由TcpSever模块设置
- 对事件监控的管理,启动多少个线程,有多少个EventLoop,由TcpSever模块设置
- 事件回调函数的设置,设置给TcpSever,TcpSever设置给各个Connection连接
前置知识
- Linux系统提供的定时功能
创建一个定时器
cpp#include <sys/timerfd.h> int timerfd_create(int clockid, int flags);
- clockid参数:CLOCK_REALTIME启动时以当前系统时间为起始时间开始,使用该方式为基准值,如果在已经启动定时器后修改了系统时间就会发生问题,CLOCK_MONOTONIC以系统启动开始递增的时间为基准值,不会随着系统时间改变而改变
- flags:0表示阻塞操作
- 返回值:返回一个文件描述符
启动定时器
cppint timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);
- fd参数:timerfd_create的返回值
- flags参数:默认设置为0,表示使用相对时间
- new_value参数:设置的超时时间
- old_value参数:用于接收当前定时器原有的超时时间设置
cppstruct timespace { time_t tv_sec;/*秒*/ long tv_nsec;/*纳米*/ } struct itimerspec { struct timespec it_interval;/*第一次超时后以后每次多长时间超时*/ struct timespec it_value; /*启动定时器后第一次多长时间超时*/ }
- 正则库的使用
对HTTP协议请求行的提取
3.任意类型存储的设计实现
我们的组件内部从socket中读到的信息存在多种情况,有可能不足一条完整的信息,有可能是多条数据,这就需要记录当前请求处理到那条信息了,一个连接有一个用来记录上下文信息的。我们当前实现的组件是支持任何协议的,并不是只支持HTTP协议,所以记录处理当前到什么阶段的信息的容器应该是保存任意类型的容器。 在boost库和C++17给我们提供了通用类型any来使用,但是考虑到代码的移植性和版本适应性,不用boost库和C++17给提供的any,自己实现一个any通用类型。 any类的设计,首先any不能通过模版来实现,因为在实例化any对象时需要指定T类型,使用类嵌套类, 内部类是一个模版,外部类的成员变量是内部类对象的指针,但是在实例化any对象时仍需要指定T类型,怎么解决,使用多态思路解决,外部类的成员变量是父类对象的指针,那在实例化any对象时就不用指定T类型。
4. C++17中any的使用
Buffer模块
对数据的暂时保存,能够放入数据和取出数据,缓冲区底层空间使用的容器是vector,而不采用string来管理空间,是因为字符串操作时遇到'\0'字符串结尾标志,就停止操作,但是我们网络传输数据时会遇到各种数据,可能传输的数据中有0,我们并不能因为遇到0就停止处理。
所需成员变量:默认空间大小、当前读取数据的位置、当前写入数据的位置
具体操作思路:
写入数据:当前写入位置指向哪里,就从哪里开始写入,如果后续剩余空间不够了,先看整体缓冲区空闲空间是否足够,
- 足够:将数据移动到起始位置即可,然后再将数据插入到缓冲区中。
- 不够:为了保证程序的效率,就不将数据移动到起始位置,直接扩容,从当前写位置开始扩容足够大小
数据一旦写入成功,当前写位置就要向后偏移。
读取数据:当前的数据位置指向哪里,就从哪里开始读取,前提是有数据可读
可读数据大小:当前写入位置-当前读取位置
日志宏函数的编写
#include <stdio.h>`
`int` `printf(const` `char` `*`format`,` `...);`
`int` `fprintf(`FILE `*`stream`,` `const` `char` `*`format`,` `...);`
`
但是打印的日志信息中没有文件名、行号的信息
- FILE:宏标识,是当前文件的文件名字符串
- LINE:宏标识,是当前所在的行号
#define LOG(msg) fprintf(stdout, "[%s:%d]%s", __FILE__, __LINE__, msg)`
`
目前的日志宏只能打印固定的字符串,要想设计出通用的日志宏,对于打印的格式是不知道的,以及参数也是不确定的(参数类型、参数个数)。
- 将打印的格式作为参数传进来
- ...:表示形参个数不定
- VA_ARGS:代表传的不定参
#define LOG(format, ...) fprintf(stdout, "[%s:%d]" format, __FILE__, __LINE__, __VA_ARGS__)`
`
打印格式format会和"[%s:%d]"进行拼接
但是我们打印日志是习惯只传一个打印的信息
在编译时发生了编译报错,这时因为宏函数的接口是LOG(format, ...),而我们调用宏函数是LOG(str.c_str()),str.c_str()作为format形参,但是没有传递不定参数,所以编译报错
- ##VA_ARGS:给__VA_ARGS__前加上##表示有不定参数就格式化打印,没有就不进行格式化打印
#define LOG(format, ...) fprintf(stdout, "[%s:%d]" format, __FILE__, __LINE__, ##__VA_ARGS__)`
`
将我们的日志答应更加完善一些,加上时间,让我们能够方便知道这条信息是什么时候打印的,在项目完成之后会有一个超市测试都会用到这个时间。
#include <time.h>`
size_t `strftime(char` `*`s`,` size_t max`,` `const` `char` `*`format`,` `const` `struct` tm `*`tm`);`
`
- 将tm结构体中保存的时间以指定的格式转换成字符串
宏代码必须在一行,通过续航符''对代码续航
添加日志等级
通过一键控制让我们的这个项目中不同等级的日志信息打印受到控制
没有打印出来的原因是因为我们设置的打印日志等级是DEBUG,而我们所打印的日志等级是INFO,所以并没有打印,我们将日志打印的等级设置为INFO,就能够看到打印的信息
Socket模块
设置套接字选项,开启地址端口重用
` 一个连接绑定了地址和端口号之后,一旦主动断开连接,就会进入到timewait的保护状态,进入到这个状态之后我们的套接字并不会立即被释放,此时地址和端口会被占用,这个通常是用来保护客户端的,但是在服务器的使用过程中服务器一旦出现问题,如:崩溃、退出,有可能会造成服务器无法立即重新启动,所以要开启地址重用,让该地址和端口重新被用起来。
`
send函数声明:
`ssize_t `send(int` sockfd`,` `const` `void` `*`buf`,` size_t len`,` `int` flags`);`
`
参数解释:
- sockfd:套接字描述符,表示已连接的套接字。
- buf:指向要发送的数据缓冲区。
- len:要发送的数据字节数。
- flags:控制数据发送行为的标志,可以为 0 或者使用一些选项(例如 MSG_DONTWAIT,非阻塞发送)
返回值: - 成功时,返回发送的字节数。
- 失败时,返回 -1,并设置相应的 errno 错误码。
recv 函数函数声明:
`ssize_t `recv(int` sockfd`,` `void` `*`buf`,` size_t len`,` `int` flags`);`
`
参数解释:
- sockfd:套接字描述符,表示已连接的套接字。
- buf:指向接收数据的缓冲区。
- len:缓冲区的大小,最大接收字节数。
- flags:控制接收行为的标志,如 MSG_WAITALL(等待接收完整数据),MSG_PEEK(查看缓冲区中的数据,但不将其从缓冲区移除)。
返回值: - 成功时,返回接收到的字节数。
- 如果对端正常关闭连接,返回 0。
- 失败时,返回 -1,并设置 errno
设置套接字选项
#include <sys/types.h>`
`#include <sys/socket.h>`
`int` `setsockopt(int` sockfd`,` `int` level`,`
`int` optname`,` `const` `void` `*`val`,`
socklen_t optlen`);`
`
- sockfd:套接字描述符
- level:设置的选项等级,须填写为SOL_SOCKET
- optname:设置的选项的名称
查看套接字选项有哪些
设置文件描述符属性
#include <unistd.h>`
`#include <fcntl.h>`
`int` `fcntl(int` fd`,` `int` op`,` `...` `/* arg */` `);`
`
size_t和ssize_t类型
- size_t无符号类型
- ssize_t有符号类型,包含在<unistd.h>的头文件中
- 一直循环报出socket连接失败的错误信息,只是因为创建的监听套接字设置为了非阻塞属性
Channel模块
`EPOLLIN `//可读事件监控`
EPOLLOUT `//可写事件监控`
EPOLLRDHUP `//用于检测TCP连接的半关闭状态,检测对端关闭了连接或关闭了连接的写端`
EPOLLHUP `//断开`
EPOLLERR `//错误`
EPOLLPRI `//优先数据`
`
EPOLLRDHUP 是 Linux epoll 机制中的一个事件标志,它用于检测对端是否关闭了连接(或至少关闭了写方向),避免了进行不必要的 read() 调用,提高了事件驱动程序的效率。TCP 半关闭是 TCP 协议提供的一种特性,允许连接的一端在停止发送数据后,仍然能够接收来自另一端的数据。
与 EPOLLHUP 的区别:
- EPOLLRDHUP: 对端关闭了连接(或写端),连接可能仍处于半关闭状态(本端还可以写数据)。
- EPOLLHUP: 表示发生了挂起。这通常意味着连接完全断开(双方都已关闭,或者发生了错误导致连接不可用)。收到 EPOLLHUP 后,套接字通常已经不可用。
EPOLLERR表示在关联的套接字发生了一个错误,收到 EPOLLERR 通常意味着该套接字连接已经不可用或处于错误状态,后续的读写操作很可能会失败。
EPOLLPRI 用于表示带外数据或紧急数据可读通知,在 TCP 中,带外数据通过设置报文头的 URG 标志和紧急指针实现。
Poller模块
epoll的操作句柄
struct epoll_event结构的数组,用来记录每次有哪些套接字上的事件就绪了
套接字和Channel对象的哈希映射
- 将套接字设置进内核进行监控,通过Channel才能知道套接字需要监控什么事件
- 当套接字上的时间就绪了,通过套接字在hash表中找到对应的Channel,才能知道事件如何处理。
暂时将Poller和Chanel进行连调,目的是为了先对Poller类和Chanel类代码测试,及时发现有什么语法或者功能错误
gdb ./server调试运行程序
bt进入函数调用栈,发现出现的错误是在server.hpp的594行
这是因为客户端程序终止,服务端程序recv()就会返回0,我们的操作是移除监控、delete chanel,chanel就不存在了,后面再调用_Event()回调函数就会出错,
还有一个BUG:HandleClose操作中先是移除,然后才能关闭fd,否则报错
eventLoop模块
eventfd 是 Linux 内核提供的一种用于线程或进程间通知的机制,它通过一个文件描述符来实现进程间的同步和事件通知。该函数在多线程和多进程编程中非常有用,尤其是在实现事件驱动和异步通知机制时。
#include <sys/eventfd.h>`
`int` `eventfd(unsigned` `int` initval`,` `int` flags`);`
`
参数说明:
- initval:当创建 eventfd 时,内核会初始化一个内部计数器,并将其值设置为 initval
- flsgs:控制 eventfd 行为的标志位
- EFD_CLOEXEC: 设置文件描述符的 close-on-exec 标志,表示在执行 exec 系列函数时自动关闭该文件描述符。
- EFD_NONBLOCK: 设置文件描述符为非阻塞模式。如果计数器为 0,读操作将立即返回 EAGAIN 错误,而不是阻塞。
- EFD_SEMAPHORE: 将 eventfd 设置为信号量模式。在这种模式下,每次读操作只会将计数器减 1,而不是读取整个计数器的值。
返回值: - 成功时,返回一个新的文件描述符,用于后续的读写操作。
- 失败时,返回 -1 并设置 errno 以指示错误原因。
使用场景: - 线程间通信:一个线程可以通过写入 eventfd 来通知另一个线程。
- 进程间通信:父子进程或无关进程可以通过共享 eventfd 文件描述符来进行事件通知。
- 与 epoll 或 select 结合使用:eventfd 可以与其他 I/O 多路复用机制一起使用,以实现高效的事件驱动编程。
每次给eventfd中写入一个1,就表示通知一次,连续写入三次后,再去read读取出来的数字就是3,读取之后计数清0。
- 在EventLoop模块中实现线程间的事件通知功能,每当向eventfd中写入一个数值用于表示事件通知的次数。
``
怎么保证一个连接的所有操作都在eventLoop对应的线程中?``
给EventLoop模块中添加一个任务队列,对连接的所有操作进行一次封装,对连接的操作并不是直接执行,而是当成任务添加到任务队列中。
Connection模块
管理:
- 套接字的管理,能够进行套接子的操作
- 连接事件的管理:可读、可写、错误、挂断、任意
- 缓冲区的管理,便于socket数据的发送和接收
- 上下文的管理,记录请求数据的处理过程
- 回调函数的管理(我们只是提供一个服务器组件,数据接收后该如何处理,需要由用户来决定,因此必须有业务处理回调函数,并且一个连接建立完成之后该如何处理,也由用户来决定,例如: 假设有一个聊天的场景,有一个连接上来了,需要给其他连接通知有新连接上来了,因此也需要连接建立成功后的回调函数,也是由用户来设置的,以及连接关闭前该如何处理的回调函数,任意事件产生的回调函数,例如:进行了IO就需要刷新一次活跃度)
功能: - 发送数据的功能,这个接口是提供给用户发送数据的接口(这个接口内部并不是直接发送,而是把数据放到发送缓冲区,然后启动写事件监控)
- 关闭连接功能,这个接口是提供给用户关闭连接的接口(在实际释放连接之前,先看输入和输出缓冲区是否有数据待处理,如果没有直接释放连接,如果有将连接设置为待释放状态)
- 启动非活跃连接超时销毁功能
- 取消非活跃连接超时销毁功能(有可能我们的连接就是长连接,所以就不需要超时连接销毁功能)
- 协议切换
存在这样一个场景:Connection模块是对连接管理的模块,对连接的所有操作都是通过这个模块来完成的,对连接进行操作的时候,但是连接已经被释放了,导致内存访问错误,最终程序崩溃,假设一个线程对连接进行了释放,但是另一个线程需要对连接进行操作,将这两个操作如果放到任务队列中,会有一个时序的问题,假如先执行的释放连接的操作,后执行对连接操作的任务,这就出现了问题。
解决方法:使用智能指针shared_ptr对Connection对象进行管理,只要计数不为0就不会释放Connection对象,这就能够保证任意一个地方对Connection对象进行操作的时候,都保存了一份shared_ptr,因此就算其他地方进行操作,也只是对shared_ptr的计数-1,并不会导致Connection的实际释放。
LoopThread模块
将线程和LoopThread模块整合起来
关键点:
- EventLoop模块和线程一一对应的
- 实例化EventLoop对象,在构造时就会初始化thread_id
LoopThreadPool模块:针对LoopThread设计一个线程池
TcpSever模块
测试服务器组件
void` `OnConnected(const` PtrConnection `&`conn`)` `{`
`DEBUG_LOG("NEW CONNECTION:%p",` conn`.get());`
`}`
`void` `OnClosed(const` PtrConnection `&`conn`)` `{`
`DEBUG_LOG("CLOSE CONNECTION:%p",` conn`.get());`
`}`
`void` `OnMessage(const` PtrConnection `&`conn`,` Buffer `*`buf`)` `{`
`DEBUG_LOG("%s",` buf`->ReadPos());`
buf`->ReadIdxMove(`buf`->ReadAbleSize());`
std`::`string str `=` `"Hello World";`
conn`->Send(`str`.c_str(),` str`.size());`
conn`->Shutdown();`
`}`
`int` `main()`
`{`
TcpServer `server(8500);`
`//server.EnableInactiveRelease(10);`
server`.SetThreadCount(2);`
server`.SetConnectedCallBack(`std`::bind(`OnConnected`,` std`::`placeholders`::`_1`));`
server`.SetCloseCallBack(`std`::bind(`OnClosed`,` std`::`placeholders`::`_1`));`
server`.SetMessageCallBack(`std`::bind(`OnMessage`,` std`::`placeholders`::`_1`,` std`::`placeholders`::`_2`));`
server`.Start();`
`return` `0;`
`}`
`
启动运行成程序发现程序崩溃了,一般是逻辑上的问题,这种报错相对不是很好找,需要调试来观察逻辑上哪的问题
调试运行server程序
运行客户端./client
查看函数调用栈
从栈顶开始看,说在1189行有个错误
单凭这些也看不出来到底是啥问题,这行代码就是返回_loops中的元素,我们接下来通过在这里打一个调试断点来看一下_next_idx的值是多少,和_loops的相关信息
break .../source/server.hpp:1189打一个断点然后重新再运行一下,服务端程序运行起来后客户端程序也重新运行起来
查看对应值
显示_loops这个容器成员为0,那就会造成越界访问,这个问题出现在哪呢
我们在创建TcpServer对象时就创建线程池,此时线程池中从属线程的个数为0,但接着又设置了线程数为2,这就形成了逻辑上的问题,所以应该启动TcpSever时再调用Creat()创建从属线程(创建从属线程时就启动了EventLoop)
修改后再次运行程序,发现程序能够正常运行
测试提供给组件使用这关闭连接的功能是否正常
NetWork类
有这样一种情况就是主机A和主机B进行通信,A端关闭了自己的连接,但是B端还继续进行写,这时B主机的操作系统就会发送SIGPIPE信号来终止进程,这就会导致程序的退出,所以需要忽略这个信号,不让进程退出。
Http协议模块
Util模块
需要提供的工具接口
- 文件的读写(向文件写入、读取文件内容,例如客户端可能需要上传一个文件,那就涉及到向文件中写入,如果客户端需要请求资源例如获取一个网页(html),这就涉及到读取文件)
- URL的编码和解码
- 通过状态码获取对应的描述信息
- 根据文件后缀名获取mime(浏览器通常采用MIME类型而不是文件扩展名来确定如何处理URL和文件内容,所以在响应头需要添加标准的mimetype,否则浏览器将不能以正常的方式来处理解析文件内容)
- 判断一个文件是否是目录
- 判断一个文件是否是一个普通文件
- 判断HTTP请求的资源路径是否是有效性(不能让用户请求目录下的任意资源)
HttpRequest模块接收到一个请求,按照HTTP请求格式进行解析,解析出各个关键要素放到Request中
关键的成员变量:请求方法、URL、协议版本、头部字段、正文
需要提供的接口:
- 头部字段的插入和获取
- 查询字符串的插入和获取
- 长连接和短连接的判断
- 正文长度的获取
HttpResponse模块
进行业务处理的同时,向Response中填充响应要素,完毕后将其组织成为HTTO响应格式的数据,发送给客户端
关键成员变量:协议版本、响应状态码、状态码描述信息、头部字段、响应正文
需要提供的接口:
- 头部字段的插入和获取
- 长连接短连接的设置与判断
- 正文的设置
Util模块
用于查看文件的属性,使用stat()只能查看已存在的文件,否则会返回0
`#include <sys/stat.h>
int stat(const char *pathname, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);
`
- pathname:用于指定一个需要查看属性的文件路径。
- buf:struct stat 类型指针,用于指向一个 struct stat 结构体变量。调用 stat 函数的时候需要传入一个 struct stat 变量的指针,获取到的文件属性信息就记录在 struct stat 结构体中。
- 返回值:成功返回 0;失败返回-1,并设置 error。
`
struct stat
{
dev_t st_dev; /* 文件所在设备的 ID */
ino_t st_ino; /* 文件对应 inode 节点编号 */
mode_t st_mode; /* 文件对应的模式 */
nlink_t st_nlink; /* 文件的链接数 */
uid_t st_uid; /* 文件所有者的用户 ID */
gid_t st_gid; /* 文件所有者的组 ID */
dev_t st_rdev; /* 设备号(指针对设备文件) */
off_t st_size; /* 文件大小(以字节为单位) */
blksize_t st_blksize; /* 文件内容存储的块大小 */
blkcnt_t st_blocks; /* 文件内容所占块数 */
struct timespec st_atim; /* 文件最后被访问的时间 */
struct timespec st_mtim; /* 文件内容最后被修改的时间 */
struct timespec st_ctim; /* 文件状态最后被改变的时间 */
};
`
通过向一下宏函数传st.st_mode类型的变量能够判断出是否是对应文件类型
`#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK)
#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
#define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR)
#define S_ISBLK(m) (((m) & S_IFMT) == S_IFBLK)
#define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO)
#define S_ISSOCK(m) (((m) & S_IFMT) == S_IFSOCK)
`
HttpRequest模块
HTTP请求大的分为请求行、请求头部、空行、正文,
` 请求行中包括:请求方法、URL、协议版本,
URL包含:资源路径、查询字符串(GET /serach?word=qq&en=utf8 HTTP/1.1)
请求头部(请求头部是key: value\r\nkey: value这样格式的信息)
Content-Length: 0\r\n(请求正文中内容的长度)
`
Connection: keep-alive/close(不存在Connection字段就是短连接、存在但是close也是短连 接、存在了是keep-alive那就是长连接)
空行
正文
综上能够得出HttpReques需要的要素:请求方法、资源路径、查询字符串、协议版本,头部字段、正文、_matches(std::smath _matches)
为了便于访问,将成员变量设置为公有成员。
所需接口
- 提供查询字符串和头部字段的查询、获取、插入功能(查询字符串和头部字段分别使用一个哈希表来存储起来)
- 获取正文长度
- 判断长连接还是短连接
HttpResponse模块
http响应分为响应行、头部、空行、响应正文
响应行包括:协议版本、状态码、状态码描述(但是HttpResponse需要存储的关键性信息就一个状态码,状态码描述信息不需要存储,可以通过状态码来获取,协议版本也不需要存储,协议版本在HttpRequest中有存储)
头部字段
响应正文
综上HttpResponse类中需要的成员变量有状态码、头部字段、响应正文、重定向信息(重定向信息分为两个,是否进行重定向、重定向的路径)
所需接口
- 头部字段的添加、获取、查询
- 响应正文设置
- 重定向的设置
- 长短链接的判断
请求接收的上下文模块
用于记录HTTP请求接收的进度
有可能出现接收的数据并不是完整的HTTP请求数据,也就是请求的处理需要在多次收到数据后才能完成,因此在每次处理的时候,就需要将处理进度记录起来,以便于下次从当前进度继续向下处理。
需要的成员变量
有一个用于记录当前这个HttpRequest处理到什么阶段(接收请求行阶段、接收头部字段、接收正文阶段、接收完毕阶段、接收数据出错)
响应状态码(因为在请求报文解析的过程中,可能出现各种不同的问题,访问的资源不对、没有权限)
已经接受并处理的请求信息
所需要提供的接口
提取和解析对应的部分
返回解析完毕的请求信息
HttpSever模块
#include <stdio.h>
int
printf(const
char
format
,
...);
int
fprintf(FILE
stream
,
const
char
*format
,
...);