以tinyWebServer为例,按代码逻辑顺序对代码进行详解
目录
零、涉及的一些通用函数
1.getcwd()
将当前工作目录的绝对路径复制到参数buffer所指的内存空间中,参数size为buf的空间大小。
#include<unistd.h>char*getcwd(char*buf,size_t size);
2.字符串操作函数
strlen()
计算一个字符串的长度
str是一个指向以\0字符结尾的字符串的指针,函数返回值是一个无符号整型(size_t),表示字符串的长度。
size_t strlen(constchar*str);
strcpy()和strncpy()
功能:将一个字符串复制到另一个字符串中。会覆盖原字符串内容
- strncpy可以指定复制的字符数量
char*strcpy(char*dest,constchar*src);char*strncpy(char*dest,constchar*src, size_t n);
- dest:目标字符串的指针,即要将源字符串复制到的目标位置。
- src:源字符串的指针,即要被复制的字符串。
- n:要拷贝的源字符串的个数
strcat()
追加字符串,将一个字符串中的内容追加到另一个字符串后面(不会覆盖原字符串内容)
- Destination:追加字符串的目的地
- Source :需要追加的字符串
结果为Destination+Source
char*strcat(char*Destination,constchar*Source );
strcmp()
比较字符串,按顺序比较两个字符串内的字符的ASCII码。
string1大则返回1,等则返回0,小则返回-1
intstrcmp(constchar*string1,constchar*string2 );
c_str()
- 将string转化为C的字符串数组,生成一个const char *指针,指向字符串的首地址。
- 不能直接赋值给char*,所以就需要我们进行相应的操作转化。
- 返回的是可读不可改的指向字符数组的指针,不需要手动释放或删除这个指针。
- 这个数组的数据是临时的,当有一个改变这些数据的成员函数被调用后,其中的数据就会失效。要么现用现转换,要么把它的数据复制到用户自己可以管理的内存中
#include<cstring>constchar*c_str();// 用法一:现用现转换
string s ="1234";constchar* c = s.c_str();
cout<<c<<endl;//1234,如果为*c,则输出结果为1
s ="abcde";
cout<<c<<endl;//abcde// 用法二:使用strcpy等函数把需要的数据拷贝到另一个内存中char* c=newchar[20];
string s="1234";//c = s.c_str();strcpy(c,s.c_str());
cout<<c<<endl;//输出:1234
s="abcd";
cout<<c<<endl;//输出:1234
strrchr()
查找一个字符串在另一个字符串中 末次 出现的位置
- 返回 str 中最后一次出现字符 c 的位置
- 如果未能找到指定字符,那么函数将返回一个空指针
#include<string.h>char*strrchr(char*str,char c);
- str – C 字符串。
- c – 要搜索的字符。以 int 形式传递,但是最终会转换回 char 形式。
snprintf()
格式化字符串,并将结果存储在指定的字符数组中
#include<stdio.h>intsnprintf(char*str, size_t size,constchar*format[,argument...]);
- str:指向一个字符数组,用于存储格式化后的字符串,该数组的大小至少为 size。
- size:指定写入 str 数组中字符的最大个数(包括最后的空字符 ‘\0’)。
- format:包含格式说明符的字符串,它定义了后续参数的输出格式。
- [,argument…]:可变参数列表,与格式字符串中的格式说明符相匹配。
3.内存初始化
3.1 memset()
memset是一个初始化函数,作用是将某一块内存中的全部设置为指定的值。
void*memset(void*s,int c, size_t n);
- s指向要填充的内存块。
- c是要被设置的值。
- n是要被设置该值的字符数。
- 返回类型是一个指向存储区s的指针。
3.2 bzero()
用于将一段内存区域清零
#include<string.h>voidbzero(void*s,int n);// 一般来说n通常取sizeof(s),将整块空间清零
bzero()函数已经被标记为废弃函数,不再建议使用。
常用于清零操作,特别是在对socket地址结构执行清0操作时。
4.时间类
4.1 time()
返回自1970年1月1日0点以来经过的秒数,每秒变化一次.
#include<time.h>
time_t time(time_t *arg);
- arg不是空指针,那么函数返回time_t类型的calendar time,并且把结果保存在arg指向的对象;
- 如果arg == NULL,那么函数只是返回一个值,值不能存储在空指针指向的对象。
4.2 localtime()
用于将时间戳(time_t 类型)转换为本地时间的结构体。
接受一个指向 time_t 类型的指针作为参数,并返回一个指向 tm 结构体的指针,该结构体包含了年、月、日、时、分、秒等时间信息。
#include<ctime>intmain(){
time_t currentTime =time(NULL);structtm* localTime =localtime(¤tTime);// 获取本地时间信息int year = localTime->tm_year +1900;// 年份需要加上 1900int month = localTime->tm_mon +1;// 月份从 0 开始,需要加上 1int day = localTime->tm_mday;int hour = localTime->tm_hour;int minute = localTime->tm_min;int second = localTime->tm_sec;}
5.文件操作类
5.1 fopen()
【C标准库】详解fopen函数 一篇让你搞懂fopen函数
创建并打开与文件相关联的文件流
FILE *fopen(constchar*filename,constchar*mode);
参数:
- filename:要打开的文件名
- mode:打开模式
返回值类型是一个指向FILE类型的指针,FILE是个结构:
- 打开文件成功,则创建一个FILE类型结构的实例,并返回指向该结构实例的指针,程序使用该指针来操作文件;
- 打开文件失败,则返回NULL;
5.2 fclose()
关闭文件流
intfclose( FILE * stream )
5.3 fputs()
把字符串写入到指定的流 stream 中,但不包括空字符。
intfputs(constchar*str, FILE *stream)
str – 这是一个数组,包含了要写入的以空字符终止的字符序列。
stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符串的流。
5.4 fcntl()
掌握文件控制:深入解析 Linux fcntl 函数
认识 fcntl 接口函数(文件非阻塞设置)
操作一个文件的文件描述符。
#include<fcntl.h>#include<unistd.h>intfcntl(int fd,int cmd,.../* arg */);
- fd 是要操作的文件描述符。
- cmd 是控制操作的命令。
- arg 是与命令相关联的可选参数。
cmd的取值:
- F_GETFL:获取文件描述符的状态标志。
- F_SETFL:设置文件描述符的状态标志。
- F_GETLK:获取文件锁。
- F_SETLK:设置或释放文件锁。
- F_SETLKW:阻塞地设置或释放文件锁。
文件状态标志:
- O_RDONLY:只读打开。
- O_WRONLY:只写打开。
- O_RDWR:读写打开。
- O_APPEND:追加写入。
- O_CREAT:如果文件不存在则创建文件。
- O_EXCL:与O_CREAT一起使用,如果文件存在则报错。
- O_TRUNC:如果文件存在且为只写或读写,则将其长度截断为0。
文件描述符标志:
- O_NONBLOCK:非阻塞模式,用于文件描述符,使得对文件的读写操作不会阻塞进程。
- O_SYNC:使得每次write都等到物理 I/O 操作完成后才返回。
- O_DIRECTORY:如果文件名是目录,则打开失败。
- O_DSYNC:等待物理 I/O 数据完成,不等待文件属性更新。
- O_NOATIME:不更新访问时间戳。
- O_NOCTTY:如果设备是终端,不将其分配为控制终端。
6.MySQL接口
Mysql接口API相关函数详细使用说明——mysql_init,mysql_real_connect,mysql_query,mysql_close等相关
7.线程类
在C++开发中,原生的线程库主要有两个,一个是C++11提供的< thread>(std::thread类),另一个是Linux下的<pthread.h>(pthread类)
- pthread类: C++ 多线程编程(二):pthread的基本使用
- thread类 C++ 多线程编程(一):std::thread的使用 C++多线程:thread类 C++多线程编程——thread线程创建与使用(2W字保姆级介绍)
8.网络编程
浅谈 Linux 网络编程 - Server 端模型、sockaddr、sockaddr_in 结构体
【Socket网络编程】12. send()、recv()、sendto() 和 recvfrom() 函数解析
server 端的套路:
①创建 socket()
②绑定 ip + port,bind()
③设置连接上限,listen()
④阻塞,监听客户端的连接,accept()
⑤业务逻辑 ,read()/write()
⑥关闭 socket,close()
8.1 socket()
socket()函数介绍
建立一个协议族为domain、协议类型为type、协议编号为protocol的套接字文件描述符。
如果函数调用成功,会返回一个标识这个套接字的文件描述符,失败的时候返回-1。
#include<sys/types.h>#include<sys/socket.h>intsocket(int domain,int type,int protocol);
8.2 setsockopt()
setsockopt()函数
setsockopt()函数功能介绍
获取或者设置与某个套接字关联的选项。
#include<sys/socket.h>intsetsockopt(int sockfd,int level,int optname,constvoid*optval, socklen_t optlen);
参数说明:
- int sockfd: 很简单,socket句柄
- int level: 选项定义的层次;目前仅支持SOL_SOCKET和IPPROTO_TCP层次
- int optname: 需设置的选项
- const void *optval: 指针,指向存放选项值的缓冲区; - 对于getsockopt(),指向返回选项值的缓冲。- 对于setsockopt(),指向包含新选项值的缓冲。
- socklen_t optlen: optval缓冲区的长度。 - 对于getsockopt(),作为入口参数时,选项值的最大长度。作为出口参数时,选项值的实际长度。- 对于setsockopt(),现选项的长度。
返回值:成功执行时,返回0。失败返回-1,errno被设为以下的某个值
- EBADF:sock不是有效的文件描述词
- EFAULT:optval指向的内存并非有效的进程空间
- EINVAL:在调用setsockopt()时,optlen无效
- ENOPROTOOPT:指定的协议层不能识别选项
- ENOTSOCK:sock描述的不是套接字
8.3 sockaddr_in结构体
sockaddr_in详解
用于表示Internet地址和端口号。通常与套接字(socket)API一起使用。
#include<netinet/in.h>structsockaddr_in{
sa_family_t sin_family;//地址簇(Address Family)uint16_t sin_port;//16位TCP/UDP端口号structin_addr sin_addr;//32位IP地址char sin_zero[8];//不使用}structin_addr{
In_addr_t s_addr;// 32为IPv4地址}
参数说明:
- sin_family:地址族,通常设置为AF_INET表示IPv4协议。
- sin_port:端口号,以网络字节序表示。
- sin_addr:IP地址,以网络字节序表示。
- sin_zero:填充字段,通常设置为0。
使用sockaddr_in结构体时,需要将其类型转换为sockaddr类型,因为套接字API中的大多数函数都需要传入sockaddr类型的指针作为参数。
- 可以使用强制类型转换将sockaddr_in类型转换为sockaddr类型
structsockaddr_inaddr;// 设置addr的字段值...// 将addr转换为sockaddr类型structsockaddr* sa =(structsockaddr*)&addr;
8.4 bind()
给 socket 绑定一个 地址结构 (IP+port)
#include<arpa/inet.h>intbind(int sockfd,conststructsockaddr*addr, socklen_t addrlen);
返回值:
- 成功:0
- 失败:-1 errno
8.5 listen()
设置 server 连接上限,设置同时与服务器建立连接的上限数。(同时进行3次握手的客户端数量)
intlisten(int sockfd,int backlog);
参数说明:
- sockfd: socket() 函数的返回值
- backlog:上限数值。最大值 128.
返回值:
- 成功:0
- 失败:-1 errno
8.6 socketpair()
socketpair的用法和理解
用于创建一对无名的、相互连接的套接字。
#include<sys/types.h>#include<sys/socket.h>intsocketpair(int d,int type,int protocol,int sv[2]);
如果函数成功,则返回0,创建好的套接字分别是sv[0]和sv[1];否则返回-1,错误码保存于errno中。
基本用法:
- 这对套接字可以用于全双工通信,每一个套接字既可以读也可以写。例如,可以往sv[0]中写,从sv[1]中读;或者从sv[1]中写,从sv[0]中读;
- 如果往一个套接字(如sv[0])中写入后,再从该套接字读时会阻塞,只能在另一个套接字中(sv[1])上读成功;
- 读、写操作可以位于同一个进程,也可以分别位于不同的进程,如父子进程。如果是父子进程时,一般会功能分离,一个进程用来读,一个用来写。因为文件描述副sv[0]和sv[1]是进程共享的,所以读的进程要关闭写描述符, 反之,写的进程关闭读描述符。
8.7 accept()
阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的 socket 文件描述符。
- accept 返回的 socket 才是真正与 client 建立连接的 socket。
intaccept(int sockfd,structsockaddr*addr, socklen_t *addrlen);
9.epoll
epoll详解(使用、原理、实验)
- epoll_create():创建一个 eventpoll 对象
- epoll_ctl():添加或删除所要监听的socket
- epoll_wait():收集在epoll监控的事件中已经发生的事件
9.1 epoll_create()
epoll_create详解
创建一个epoll实例并返回该实例对应的文件描述符fd。
该文件描述符用于随后的所有对epoll的调用接口。每创建一个epoll句柄,会占用一个fd,因此当不再需要时,应使用close关闭epoll_create()返回的文件描述符,否则可能导致fd被耗尽。
#include<sys / epoll.h>
nfd =epoll_creat(max_size);
max_size:这个监听的数目最大有多大.
9.2 epoll_wait()
等待监听的所有fd相应事件的产生.
intepoll_wait(int epfd,structepoll_event*events,int maxevents,int timeout);
参数说明:
- int epfd: epoll_create()函数返回的epoll实例的句柄。
- struct epoll_event * events: 接口的返回参数,epoll把发生的事件的集合从内核复制到 events数组中。events数组是一个用户分配好大小的数组,数组长度大于等于maxevents。(events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存)
- int maxevents: 表示本次可以返回的最大事件数目,通常maxevents参数与预分配的events数组的大小是相等的。
- int timeout: 表示在没有检测到事件发生时最多等待的时间,超时时间(>=0),单位是毫秒ms,-1表示阻塞,0表示不阻塞。
返回需要处理的事件数目。失败返回0,表示等待超时。
9.3 epoll_event结构体
#include<sys/epoll.h>structepoll_event{uint32_t events;// epoll 事件类型,包括可读,可写等
epoll_data_t data;// 用户数据,可以是一个指针或文件描述符等};
events字段表示要监听的事件类型,可以是以下值之一:
- EPOLLIN:表示对应的文件描述符上有数据可读
- EPOLLOUT:表示对应的文件描述符上可以写入数据
- EPOLLRDHUP:表示对端已经关闭连接,或者关闭了写操作端的写入
- EPOLLPRI:表示有紧急数据可读
- EPOLLERR:表示发生错误
- EPOLLHUP:表示文件描述符被挂起
- EPOLLET:表示将epoll设置为边缘触发模式
- EPOLLONESHOT:表示将事件设置为一次性事件
data字段表示用户数据,它的类型是一个union,可以存放一个指针或文件描述符等数据。
typedefunion epoll_data {void*ptr;int fd;uint32_t u32;uint64_t u64;} epoll_data_t;
- ptr可以指向任何类型的用户数据
- fd表示文件描述符
- u32和u64分别表示一个32位和64位的无符号整数。
使用时,用户可以将自己需要的数据存放到这个字段中,当事件触发时,epoll系统调用会返回这个数据,以便用户处理事件。
9.4 epoll_ctl()
添加或删除所要监听的socket
intepoll_ctl(int epfd,int op,int fd,structepoll_event*event)
一、主流程
1.main
- 修改访问的数据库信息,包括登录名、密码、库名
- 创建Config类的对象config对命令行进行解析,
- 创建WebServer类的对象server
- 使用Config类的对象config对server进行初始化
- 初始化日志,包括设置写方式、创建文件
- 初始化数据库连接池及数据库读取表,获取用户名与密码
- 初始化线程池,创建一个http_conn的线程池,创建新线程并使其独立
- 初始化触发模式,LT/ET + LT/ET共四种
- 创建套接字,绑定地址,创建注册事件表
- 运行服务器,开始监听socket接口,并处理不同的事件
2.config类
声明及定义位于文件config.h和config.cpp内。
用于初始化webserver类。
类成员变量
- 主要有:端口号,日志写入方式、触发模式、线程池线程数量、数据库连接池数量等,用于配置服务器属性。
类成员函数
- 构造函数:给成员变量赋默认值
- 参数解析parse_arg:根据程序传入的参数修改成员变量的值,即修改服务器属性
3.WebServer类
声明及定义位于文件webserver.h和webserver.cpp内。
利用config类的成员变量进行初始化。
类成员变量:
- 基础成员变量:端口、日志写入方式、触发模式等,还有通信管段、epollfd、用户
- 数据库相关:
- 线程池相关:
- epoll事件相关
- 定时器相关:
类成员函数:
- 构造函数:给http连接对象分配空间,初始化root文件夹路径,给定时器分配空间
- init:将服务器属性参数配置赋值好
- log_write:初始化日志
- sql_pool:初始化数据库连接池,初始化数据库读取表
- thread_pool:创建线程池,创建新线程并加入
- trig_mode:设定监听和连接的触发模式
- eventListen:创建套接字,绑定地址,注册内核事件
eventListen
- 创建一个socket句柄
- 设定socket句柄的选项
- 处理网络通信的地址,将socket和地址(IP+port)绑定
- epoll创建内核事件表,注册事件 - 初始化定时器- 创建epoll实例,注册事件- 创建管道- 设置信号函数
eventLoop
- 循环运行
- 获取要处理的事件数
- 获取要处理的事件的文件描述符fd,对不同事件采取不同的处理: - 新到客户连接:判断是否来自监听的socket,是的话建立新连接,创建定时器进行管理- 文件描述符被关闭、被挂起或发生错误:服务器段关闭连接,移除对应的定时器- 管道上来了可读的数据:处理管道信号的数据- 客户连接上文件描述符上有数据可读:处理- 客户连接上文件描述符上可以写入数据
二、各部件
1.日志系统
Log类
使用了懒汉模式,确保只有一个实例。
类成员函数:
- get_instance():获取一个实例,返回实例的指针
- init:对像初始化 - 若是异步,则设置异步标志位,创建阻塞队列,创建线程- 记录时间并设置- 设置日志文件名- 创建日志文件并打开
- write_log:对日志文件进行写入
- flush:刷新写入流缓冲区
block_queue类
阻塞队列类,主要是解决异步写入日志做准备。
为了线程安全,每个操作前都要先加互斥锁,操作完后,再解锁。也就是每个函数在进入时先上锁,退出函数时解锁。
使用模板,根据传入的类型创建对应类型的队列。
类成员函数:
- 构造与析构
- clear():将成员变量都初始化
- full():判断队列是否满了
- empty():判断队列是否为空
- front(T &value):返回队首元素
- back(T &value):返回队尾元素
- size():获取队列长度
- max_size():获取队列容量
2.数据库连接池
connection_pool类
使用单例模式,使用list来实现连接池,静态大小
类成员函数:
- GetInstance():获取实例
- connection_pool:构造函数,初始化url、端口、用户等信息
- init:初始化连接池,按最大连接数初始化连接- 实现连接,连接上MySQL数据库- 把连接成功的指针加入连接池- 初始化信号量
- GetConnection:从数据库连接池中返回一个可用连接,更新使用和空闲连接数- 申请信号量- 上锁- 获取连接- 更新使用和空闲连接数- 解锁
- ReleaseConnection:释放当前使用的连接- 上锁- 释放- 更新使用和空闲连接数- 解锁- 释放信号量
- DestroyPool:销毁数据库连接池- 上锁- 关闭全部MySQL连接- 使用和空闲连接数置零,连接池清空- 解锁
- GetFreeConn:获取当前空闲的连接数- 返回当前空闲的连接数
connectionRAII类
用来获取连接
输入连接句柄的地址和的连接池,将该句柄与连接池的一个可用连接 连接起来
3.HTTP连接http_conn类
根据状态转移,通过主从状态机封装了http连接类。其中,主状态机在内部调用从状态机,从状态机将处理状态和数据传给主状态机。
- 客户端发出http连接请求
- 从状态机读取数据,更新自身状态和接收数据,传给主状态机
- 主状态机根据从状态机状态,更新自身状态,决定响应请求还是继续读取
类成员变量:
- 使用四个enum枚举类型列举请求方法、检查状态、http代码、监听状态
- 定义http响应的一些状态信息 -如400、403、404、500等
类成员函数:
- initmysql_result:初始化数据库读取表 - 从连接池中取出一个MySQL连接- 向数据库中执行user检索- 记录返回的结果集,将结果用map记录
- setnonblocking:设置文件描述符为非阻塞
- addfd:将内核事件表注册读事件
- removefd:从内核事件表删除某个文件描述符
- modfd:将事件重置为EPOLLONESHOT
- init:初始化新接受的连接,check_state默认为分析请求行状态
4.线程池threadpool类
半同步/半反应堆线程池,使用一个工作队列完全解除了主线程和工作线程的耦合关系:主线程往工作队列中插入任务,工作线程通过竞争来取得任务并执行它。
采用模板化设计
类成员函数:
- threadpool:构造函数 - 初始化线程池数组,数组里的每一个值都是一个线程标识符- 创建新线程,并使其与主线程分离
- ~threadpool:析构函数 - 删除线程池
- append:增加一个线程,并设定好状态
- append_p:增加一个线程,但不设定状态
- worker:启动线程池,运行run()函数
- run():循环运行 - 获取信号量,上锁- 获取工作队列的头部队列
5.定时器
通过实现一个服务器定时器,处理非活跃连接,释放连接资源。
- 利用alarm函数周期性地触发SIGALRM信号,该信号的信号处理函数利用管道通知主循环执行定时器链表上的定时任务
- 基于升序链表的定时器
client_data类:记录每个客户端的地址、socket接口、定时器
util_timer类:定时器类,作为链表的结点
- *prev:指向上一个定时器
- *next:指向下一个定时器
- *user_data:指向定时器对应的用户
- expire:时间
sort_timer_lst类:基于升序链表的定时器
- *head:指向头定时器
- *tail:指向尾定时器
- add_timer():添加定时器
- del_timer():删除定时器
- adjust_timer():
- tick():
Utils类:定时器链表的方法类,包含一个sort_timer_lst类的对象
- init:设置最小超时单位
- setnonblocking:对文件描述符设置非阻塞
- addfd:将内核事件表注册读事件
- sig_handler:信号处理函数
- addsig:设置信号函数
- timer_handler:不断触发SIGALRM信号
6.锁类Locker
信号量sem:
- 构造函数:inti初始化
- 析构函数:destory
- wait:申请信号量
- post:释放信号量
锁locker:
- lock:上锁
- unlock:解锁
- geit:获取锁的状态
条件变量cond
三、压力测试
压测利器Webbench(附源码)
3.1 基本原理
- 父进程fork若干个子进程,每个子进程在用户要求时间或默认的时间内对目标web循环发出实际访问请求
- 父子进程通过管道进行通信,子进程通过管道写端向父进程传递在若干次请求访问完毕后记录到的总信息,父进程通过管道读端读取子进程发来的相关信息
- 子进程在时间到后结束,父进程在所有子进程退出后统计并给用户显示最后的测试结果,然后退出。
3.2 主要流程
- 解析参数:用户通过命令行参数指定目标网址、并发连接数、测试时长等参数。webbench 首先会解析这些参数,确定压测的目标和条件。
- 创建并发连接:webbench 根据用户指定的并发连接数,在测试开始时创建相应数量的并发连接。这些连接将模拟多个客户端同时访问目标网站。
- 建立连接:对于每个并发连接,webbench 发起一个 HTTP 请求并尝试与目标服务器建立连接。这些连接可能是非阻塞的,允许同时处理多个连接。
- 发送请求:一旦连接建立成功,webbench 就会向目标服务器发送 HTTP 请求。这些请求可以是简单的 GET 请求,也可以包含其他 HTTP 方法和自定义的请求头信息。
- 接收响应:webbench 在发送请求后会等待目标服务器的响应。它可以根据用户指定的超时时间来确定是否需要等待响应,或者在超时后放弃等待并关闭连接。
- 记录结果:在测试过程中,webbench 会记录每个请求的响应时间、状态码等信息。这些信息将用于后续的性能分析和结果报告。
- 重复测试:根据用户指定的测试时长,webbench 会在一定时间内不断重复发送请求和接收响应,以模拟持续的并发访问情况。
- 汇总结果:在测试结束后,webbench 会汇总所有请求的结果,并计算出一些统计信息,如平均响应时间、成功率等。这些信息将作为压测结果向用户展示。
- 生成报告:最后,webbench 会根据测试结果生成一份报告,包括压测的详细信息、性能指标和可能的改进建议。用户可以根据这份报告来评估服务器的性能表现和优化方向。
版权归原作者 Reicher 所有, 如有侵权,请联系我们删除。