web服务器开发基础

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值