web服务器开发就像一棵大树,从根出发,所有的知识点逐渐细分,具有层次型,层层连接。
你如果想完整的做出来,就需要对这“树系统”,从头到尾的掌握清晰熟练。
那么web服务器开发应该从哪里出发?
web服务器的实现原理就是对客户端发来的数据进行解析,处理和回送的过程。
在你熟知http协议组成的基础上,出发点就是对客户端http数据的解析,处理和回送
围绕这四个点,逐渐展开去写代码
web服务器数据操作的过程代码要封装为一个对象,然后每一部分又封装为各自的功能函数,层层内部封装。
而web服务器数据操作封装的对象内部从头到尾的执行,需要一个入口函数(也就是程序的开端)。
也是方便给外部调用。
在C++主要的编程作用的就是封装
web服务器制作时,要将HTTP的所有函数和变量数据都封装为一个类,方便建立对象,使用对象,比如建立线程池时需要的工作对象参数就是HTTP类的对象
http状态机
1,服务器读取和解析请求的处理结果
2,行的读取状态(行---HTTP请求行+消息头)
3,解析客户数据时,状态机所处的状态(处理请求行/处理消息头/处理消息体)
4,HTTP的请求方法
1:
enum HTTP_RESULT{
NO_REQUEST,//没有客户数据在缓冲区中,或者数据不完整
GET_REQUEST,//得到完整数据
BAD_REQUEST,//请求数据有语法错误
FORBIDDEN_REQUEST,//客户对资源没有足够的访问权限
INTERNAL_ERROR,//服务器内部错误
CLIENT_CLOSED //客户端已经关闭连接
NO_RESOURCE,//文件不在本服务器
DO_REQUEST_SUCCESS //HTTP解析成功,接下来进行写操作
};//8种状态
GET_REQUEST,//得到完整数据,注意一定是得到完整HTTP请求才返回这种状态,而不是解析过程中每一步成功就返回这种状态
2:
enum LINE_STATE{
LINE_OK,//READ SUCCESS
LINE_BAD,//行的语法有错
LINE_LACK//行数据还不完整
};
3种
3:
enum PARSE_STATE{
PARSE_REQUEASTLINE,//解析请求行
PARSE_HEADER,//解析消息头
PARSE_CONTENT//解析消息体
};
3种状态
4:
enum METHED{
GET,
POST,
HEAD,
PUT,
DELETE,
TRACE,
OPTIONS,
CONNECT,
PATCH
};
web服务器程序编程过程:
建立套接字,注册如事件表,套接字连接,将连接好的套接字注册入事件表,I/O复用继续检测事件表中是否有套接字需要读或者写数据,有就进行数据的处理
套接字的处理
1,除了监听套接字需要循环利用,其他的套接字都注册为EPOLLONESHOT,在处理HTTP请求的过程中,对于一切正常的套接字(包括完成写数据,或者read时errno==EAGAIN||errno==EWOULDBLOCK),处理完成数据之后,进行再次注册;对于处理过程中出现异常的套接字,直接断开连接。
web代码实现逻辑:
1,读取客户数据
main函数只需要监听,连接套接字和读取,发送数据。
数据的解析不是在main函数中进行,即启动不是从main函数开始,而是需要将客户对象加入线程池的工作队列中,由线程池来启动数据解析功能。
读取前的操作
1):进行连接时,注意判断连接数量不要超过允许的最大连接数量,之后统计客户链接数
2):所有的客户数量统计的变量一定要是静态的,而且所有事件都要注册到一个事件表中,所以事件表描述符也要是静态的
3):往事件表中添加连接好的套接字
读取数据的操作
1,开辟一个读缓冲区的大小READ_BUFFER,只要套接字不断开,他的每一次请求数据一直往后存储,而不是请求完一次,对缓冲区清空。(一后可以试试请求一次,清空一次)
2,既然缓冲区有内存限制,那么读取之前要先判断之前读取的数据有没有达到或者超出READ_BUFFER,如果没有才继续读取,如果已经达到或者超出READ_BUFFER的大小,就让这个套接字断开,防止数据溢出。如果需要再次请求,就再次连接,重新建立。
3,和缓冲区有关的变量:
1)表示总数据大小的,作为最后一个元素的下一个元素下标的变量:m_read_idx
2)表示访问到的元素的下标:now_array_idx
3)本次取所取数据的起始地址:
关闭,删除套接字:
先从事件表中移除套接字,再关闭套接字
客户数量减一
void removefd(int mfd,int fd)
{
epoll_ctl(mfd,EPOLL_CTL_DEL,fd,0);
close(fd);
}
void http_conn::close_conn(bool real_close)
{
if(real_close&&(m_sockfd!=-1))
{
removefd(m_epollfd,m_sockfd);
m_sockfd=-1;
users_count--;
}
}
2,解析客户数据
http(对象)的入口函数
入口函数中包含两部分:
1,从缓冲区读取数据并解析处理(的函数入口)---process_read()
读取和处理的结果大体就三种:
成功,失败,数据还需要读取
成功和失败往后还需要处理
但是如果还需要读取数据,就没必要往后了,就再次注册套接字(因为使用了EPOLLONESHOT)
2,对处理的结构进行判断之后做出处理(的函数入口)---process_write()
写数据如果失败就关闭套接字
如果成功就注册套接字的写属性
process_read()中,如何从缓冲区读取和解析数据
也就是process_read()中如何代码的完成原理:
http请求消息分两部分,两次读取:
while((line_parse_state==PARSE_CONTENT&&line_state==LINE_OK)||(line_state=read_line())==LINE_OK){}
第一次读取行(请求行,消息头)
当解析完请求行和消息头之后,再次执行消息体的读取(主要是为了将下标移动到本次HTTP请求数据再缓冲区的最后位置,以便下次开始),
每一次用于解析的数据段的起始位置是本次读取的起始位置,也就是上次读取的结束位置,所以每一次访问到哪个下标,要记录下来,供给下次取数据段使用:
char* http_conn::once_start_line()
{
return read_array+start_line_idx;//返回缓冲区地址
}
text=once_start_line();//获取本次数据段起始地址
start_line_idx=now_array_idx;//记录上次访问到的下标
读取行(请求行+消息头)
如果读取成功
解析请求行和消息头
如果读取失败
解析请求行
如果解析失败
如果解析成功
解析消息头
如果解析失败
如果解析成功
解析消息体
如果解析失败
如果解析成功
需要读取哪些部分的数据
HTTP组成和连接
想要正确解析HTTP请求,一定要知道各部分之间的连接字符
可见,HTTP每一行都以”回车换行“符(\r\n)结尾,
而消息头和消息体之间有两个回车换行符,也就是多出一个空行
1)每次读取一行数据
读取原理:
HTTP请求消息每一行都是以回车换行符结尾,每次以读到这两个连续字符为标志,读取一行数据
而消息头和消息体之间又是以两个回车换行符间隔,且每一次读取到回车换行都将这两个字符的位置变为'\0',且每一次取一行,如果取到的那行是'\0',那就是消息头和消息体之间的间隔行,主状态机的状态就变为”解析消息体“
寻找\r\n分三种情况:
(1)读取到\r
(2)读取到\n
(3)数据读取完毕都没有\r和\n----则直接返回错误。
而(1)又分三种情况处理:
(1)--1:\r是本次读取的最后一个字符----则继续读取
(1)--2:\r不是本次读取的最后一个字符,且下一个就是\n----则成功到读取请求行和消息头
(1)--3:\r之后不是\n,那么http请求有问题----则返回错误。
而(2)又分两种情况处理:
(2)--1:\n之前是\r-----则成功到读取请求行和消息头。
(2)--!:如果\n之前不是\r,那么http请求有问题----则返回错误。
读取请求行之后为什么要将\r\n的位置置0,且数组标记位还要往后加1
置0是为了将HTTP请求进行分段,每一次进行HTTP解析都是取一段数据出去进行解析
数组标记位往后加1,是为了将处理吧标记位移到下一段的开头
2)解析请求行:
只需要读取一次就可以读取完整的请求行
建立两个指针:URL和version,对于HTTP版本,建立一个版本枚举类型的变量,将获取的版本和目标版本进行比较,如果是,直接用枚举元素给这个变量赋值。
寻找到一个空白符,利用strpbrk函数,将空白符设置为'\0',此时,得到两个字符串,设置两个指针指向这两个字符串。
这里检索空白符的格式为:'' \t" 空格+\t
第一个字符串就是HTTP的方法,再利用linux的特有函数strcasecmp()比较第一个字符串是不是我们需要的HTTP方法。
再找第二个空白符的地址,设置指针获取这个地址,将空白符设置为'\0'.
同样,可以利用strcasecmp()检测HTTP版本
接下来就是解析URL:
先将指向URL的指针移动到"http://"之后,因为文件在本服务器的路径的起始位置是在域名之后的第一个'/'之后,利用strchr()寻找'/'
与“HTTP://”比较,如果URL前缀是"http://",就进行移动和寻找“/”
即使比较不成功,也有可能是URL不包含"http://",直接判断有没有"/",没有就是HTTP请求错误
如果以上都操作成功,就改变主状态机的状态,继续分析消息头。
找到方法-----进行对比,设置
找到URL-----要让指针指向域名之后的第一个“/”的位置
找到版本------判断HTTP八本即可,不是所需版本,返回错误
3)解析消息头:
消息头要循环读取,直到遇到间隔的空行
解析消息头之后,要判断是否有消息体需要处理,如果没有,就对解析到的客户数据进行处理,
如果有,主状态机转为处理消息体,处理完成消息体之后再处理客户数据
获取HTTP中的数据信息,并做出相应的设置,这里需要懂得HTTP中消息头中各部分的功能。
在获取HTTP中首部字段和首部值之后时候,因为消息体不一定有,所以先判定有没有消息体,没有---就直接退出进行数据处理,如果有消息体-----主状态机状态转移,去获取消息体。
那么怎么判定有没有消息体呢:
在消息头中有一个头部字段----Content-Length,这个头部字段后面的字符代表消息体的字符数,利用atol()这类函数将之后的字符转为整数,记录下来。
当读取空行的时候,处理的标志位就已经移到消息体的开头了,所以没必要再次读取数据,指向数据的指针此时直接利用就是指向消息体的,所以,循环条件此时设置位满足条件,直接进入循环
解析请求首部字段的原理:
每一个首部字段极其内容占据一行,每一次读取的数据段就是一个首部字段和其值
if..else逐个判断
4)解析消息体:
解析HTTP请求的三个部分可以放在一个switch中,解析完读取的一行,继续读取,继续解析
怎么样获取消息体中的数据:
3,处理客户数据
当解析完一段数据(行或者消息体)之后,需要做什么
1)将服务器根目录和文件路径进行拼接---strcpy,strncpy
2)判断文件是否存在------用stat获取文件信息,获取成功则存在,失败则不存在
3)判断文件的属性---------是否可读,是文件,还是目录。
4)文件存在就打开文件获取文件描述符
5)文件映射
6)关闭文件描述符
《7)写文件映射的时候也要写一个解出映射的函数,以供后续读完映射取内容之后释放内存。》
到处读取和解析完成
4,生成响应报文
无论读取和解析HTTP的结果如何,都要生成响应报文发送给客户端
生成响应报文的原理
根据HTTP请求报文解析的不同结果,往缓冲区写入不同的数据(字符串)
注意:HTTP响应报文的生成也要按照标准的HTTP响应报文格式生成
每一行都以"\r\n"结尾
消息头和消息体之间有空行(“\r\n”)
所以每一种生成响应报文的步骤都是:
添加状态行消息
添加消息头消息
添加消息体消息
获取文件失败的响应报文:
添加状态行消息
添加消息头消息
添加消息体消息
失败的响应报文消息体数据就只有一个内存中的数据(响应行,消息头,消息体的字符串),直接写入一个内存地址就可以
获取文件成功的响应报文:
成功的响应报文的消息体数据除了存放响应行,消息头,消息体的数据之外,还有文件的数据。
这就需要使用内存数据合并发送的writev()了。
还要注意判断文件内容是否为空。
如果不为空,消息体就不需要设置,因为消息体就是文件内容
方法1:
设置可变参数函数,利用va_list类函数(宏)和vsnprintf()函数将指定的数据写入写缓冲区。
5,回送数据给客户
writev发送几块缓冲区的数据
遇到的问题
1:linux上80端口号只有超级用户才可以使用
2:recev一直返回错误,errno=88----非套接字的套接字错误
编程的细节错误------http对象数组的下标要使用socket,而不是epoll_wait出来的i
编译运行演示
浏览器输入网址和获得文件的结果
项目总结
线程池的创建
1,利用的并发模式:半同步/半异步模式---子线程没有自己的套接字,需要获取
2,使用list容器来创建队列,用来存放准备就绪的HTTP对象。
3,创建互斥锁来控制线程对队列的操作。
4,创建信号量来控制线程对队列中HTTP对象的获取。
注意:线程获取的不是套接字,因为套接字就存放在HTTP对象中,没有必要获取,子线程只需要获取这个准备好的HTTP对象,然后启动它去工作就可以了。
http主线程(main函数)
1,采用的事件处理模式-----reactor模式,主线程只负责监听,主线程负责读写等操作。
2,主线开始就创建号线程池和HTTP对象
3,虽然这种并发模式子线程中没有注册套接字,但是当有套接字连接上时,这个套接字就初始化赋给指定的HTTP对象,只要这个套接字不断开重新连接,那么这个套接字的处理都由这个HTTP对象负责。只要主线程监听到套接字有事件需要处理,往队列中注册这个HTTP对象。
(
半同步/半异步:子线程处理自己注册的套接字事件
半同步/半反应堆:子线程获取的套接字存储在HTTP对象中,子线程处理的HTTP对象,即套接字随机
)
HTTP的类
1,定义有所有处理HTTP请求的函数和需要的变量等。
2,涉及到多块缓冲区发送----writev()
使用的设备
os:Linux
编译器:g++ (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0