学以至用-搭建一个HTTP主机的实战指南 (学以至用搭配下一句)

文章编号:11595 更新时间:2024-03-07 分类:互联网资讯 阅读次数:

资讯内容

HTTP服务是重中之重,当蠢才享一下一个HTTP服务的成功。

名目引见

本名目成功的是一个HTTP主机,名目中将会经过基本的网络套接字读取客户端发来的HTTP恳求并启动剖析,最终构建HTTP照应并前往给客户端。

HTTP在网络运行层中的位置是无法撼动的,无论是移动端还是PC端阅读器,HTTP无疑是关上互联网运行窗口的关键协定。

该名目将会把HTTP中最外围的模块抽取进去,驳回CS模型成功一个小型的HTTP主机,目的在于了解HTTP协定的处置环节。

该名目关键触及C/C++、HTTP协定、网络套接字编程、CGI、单例形式、多线程、线程池等方面的技术。

网络协定栈引见

协定分层

网络协定栈的分层状况如下:

网络协定栈中各层的配置如下:

数据的封装与分用

数据封装与分用的环节如下:

也就是说,发送端在出现数据前,该数据须要先自顶向下贯通网络协定栈成功数据的封装,在这个环节中,每一层协定都会为该数据参与上对应的报头消息。接纳端在收到数据后,该数据须要先自底向上贯通网络协定栈成功数据的解包和分用,在这个环节中,每一层协定都会将对应的报头消息提取进去。

而本名目要做的就是,在接纳到客户端发来的HTTP恳求后,将HTTP的报头消息提取进去,而后对数据启动剖析处置,最终将处置结果参与上HTTP报头再发送给客户端。

须要留意的是,该名目中咱们所处的位置是运行层,因此咱们读取的HTTP请务实践是从传输层读取过去的,而咱们发送的HTTP照应实践也只是交给了传输层,数据真正的发送还得靠网络协定栈中的下三层来成功,这里间接说接纳到客户端的HTTP恳求以及发送HTTP照应给客户端,只是为了繁难大家了解,此外,同层协定之间自身也是可以了解成是在间接通讯的。

HTTP相关常识引见

HTTP的特点

HTTP的五大特点如下:

说明一下:

相关视频介绍

面试中正派八股文网络原理tcp/udp,网络编程epoll/reactor

6种epoll的设计,让你吊打面试官,而且他不能还嘴

epoll原理剖析以及三握四挥的处置

LinuxC++后盾主机开发架构师收费学习地址

【文章福利】:小编整顿了一些团体感觉比拟好的学习书籍、视频资料共享在群文件外面,有须要的可以自行参与哦!~点击832218493参与(须要自取)

URL格局

URL(UniformResourceLacator)叫做一致资源定位符,也就是咱们理论所说的网址,是因特网的万维网服务程序上用于指定消息位置的示意方法。

一个URL大抵由如下几局部造成:

繁难说明:

URI、URL、URN

URI、URL、URN的定义如下:

URI、URL、URN三者的相关如下:

URI有相对和相对之分:

HTTP的协定格局

HTTP恳求协定格局如下:

HTTP恳求由以下四局部组成:

HTTP照应由以下四局部组成:

HTTP的恳求方法

HTTP经常出现的恳求方法如下:

GET方法和POST方法HTTP的恳求方法中最罕用的就是GET方法和POST方法,其中GET方法普通用于失掉某种资源消息,而POST方法普通用于将数据上行给主机,但实践GET方法也可以用来上行数据,比如百度搜查框中的数据就是经常使用GET方法提交的。

GET方法和POST方法都可以带参,其中GET方法经过URL传参,POST方法经过恳求注释传参。由于URL的长度是有限度的,因此GET方法携带的参数不能太长,而POST方法经过恳求注释传参,普通参数长度没有限度。

HTTP的形态码

HTTP经常出现的Header

CGI机制引见

CGI(CommonGatewayInterface,通用网关接口)是一种关键的互联网技术,可以让一个客户端,从网页阅读器向口头在网络主机上的程序恳求数据。CGI形容了主机和恳求处置程序之间传输数据的一种规范。

实践咱们在启动网络恳求时,无非就两种状况:

理论从主机上失掉资源对应的恳求方法就是GET方法,而将数据上行至主机对应的恳求方法就是POST方法,但实践GET方法有时也会用于上行数据,只不过POST方法是经过恳求注释传参的,而GET方法是经过URL传参的。

用户将自己的数据上行至主机并不只仅是为了上行,用户上行数据的目的是为了让HTTP或相关程序对该数据启动处置,比如用户提交的是搜查关键字,那么主机就须要在后端启动搜查,而后将搜查结果前往给阅读器,再由阅读器对HTML文件启动渲染刷新展现给用户。

但实践对数据的处置与HTTP的相关并不大,而是取决于下层详细的业务场景的,因此HTTP不对这些数据做处置。但HTTP提供了CGI机制,下层可以在主机中部署若干个CGI程序,这些CGI程序可以用任何程序设计言语编写,当HTTP失掉到数据后会将其提交给对应CGI程序启动处置,而后再用CGI程序的处置结果构建HTTP照应前往给阅读器。

其中HTTP失掉到数据后,如何调用指标CGI程序、如何传递数据给CGI程序、如何拿到CGI程序的处置结果,这些都属于CGI机制的通讯细节,而本名目就是要成功一个HTTP主机,因此CGI的一切交互细节都须要由咱们来成功。

只需用户恳求主机时上行了数据,那么主机就须要经常使用CGI形式对用户上行的数据启动处置,而假设用户只是单纯的想恳求主机上的某个资源文件则不须要经常使用CGI形式,此时间接将用户恳求的资源文件前往给用户即可。

此外,假设用户恳求的是主机上的一个可口头程序,说明用户想让主机运转这个可口头程序,此时也须要经常使用CGI形式。

CGI机制的成功步骤

一、创立子进程启动程序交流

主机失掉到新衔接后普通会创立一个新线程为其提供服务,而要口头CGI程序肯定须要调用exec系列函数启动进程程序交流,但主机创立的新线程与主机进程经常使用的是同一个进程地址空间,假设间接让新线程调用exec系列函数启动进程程序交流,此时主机进程的代码和数据就会间接被交流掉,相当于HTTP主机在口头一次性CGI程序后就间接分开了,这必需是不正当的。因此新线程须要先调用fork函数创立子进程,而后让子进程调用exec系列函数启动进程程序交流。

二、成功管道通讯信道的建设

调用CGI程序的目的是为了让其启动数据处置,因此咱们须要经过某种方式将数据交给CGI程序,并且还要能够失掉到CGI程序处置数据后的结果,也就是须要启动进程间通讯。由于这里的主机进程和CGI进程是父子进程,因此优先选用经常使用匿名管道。

由于父进程不只须要将数据交给子进程,还须要从子进程那里失掉数据处置的结果,而管道是半双工通讯的,为了成功双向通讯于是须要借助两个匿名管道,因此在创立调用fork子进程之前须要先创立两个匿名管道,在创立子进程后还须要父子进程区分封锁两个管道对应的读写端。

三、成功重定向相关的设置

创立用于父子进程间通讯的两个匿名管道时,父子进程都是各自用两个变量来记载管道对应读写端的文件形容符的,然而关于子进程来说,当子进程调用exec系列函数启动程序交流后,子进程的代码和数据就被交流成了指标CGI程序的代码和数据,这也就象征着被交流后的CGI程序无法得悉管道对应的读写端,这样父子进程之间也就无法启动通讯了。

须要留意的是,进程程序交流只交流对应进程的代码和数据,而关于进程的进程管理块、页表、关上的文件等外核数据结构是不做任何交流的。因此子进程启动进程程序交流后,底层创立的两个匿名管道依然存在,只不过被交流后的CGI程序不知道这两个管道对应的文件形容符罢了。

这时咱们可以做一个商定:被交流后的CGI程序,从规范输入读取数据等价于从管道读取数据,向规范输入写入数据等价于向管道写入数据。这样一来,一切的CGI程序都不须要得悉管道对应的文件形容符了,当须要读取数据时间接从规范输入中启动读取,而数据处置的结果就间接写入规范输入就行了。

当然,这个商定并不是你说有就有的,要成功这个商定须要在子进程被交流之行启动重定向,将0号文件形容符重定向到对应管道的读端,将1号文件形容符重定向到对应管道的写端。

四、父子进程交付数据这时父子进程曾经能够经过两个匿名管道启动通讯了,接上去就应该探讨父进程如何将数据交给CGI程序,以及CGI程序如何将数据处置结果交给父进程了。

父进程将数据交给CGI程序:

说明一下:恳求注释长度、URL传递的参数以及恳求方法都比拟短,经过写入管道来传递会造成效率降落,因此选用经过导入环境变量的方式来传递。

也就是说,经常使用CGI形式时假设恳求方法为POST方法,那么CGI程序须要从管道读取父进程传递过去的数据,假设恳求方法为GET方法,那么CGI程序须要从环境变量中失掉父进程传递过去的数据。

但被交流后的CGI程序实践并不知道本次HTTP恳求所对应的恳求方法,因此在子进程在启动进程程序交流之前,还须要经过putenv函数将本次HTTP恳求所对应的恳求方法也导入环境变量。因此CGI程序启动后,首先须要先经过环境变量得悉本次HTTP恳求所对应的恳求方法,而后再依据恳求方法对应从管道或环境变量中失掉父进程传递过去的数据。

CGI程序读取到父进程传递过去的数据后,就可以启动对应的数据处置了,最终将数据处置结果写入到管道中,此时父进程就可以从管道中读取CGI程序的处置结果了。

CGI机制的意义

CGI机制的处置流程如下:

处置HTTP恳求的步骤如下:

日志编写

主机在运作时会发生一些日志,这些日志会记载下主机运转环节中发生的一些事情。本名目中的日志格局如下:

日志说明:

日志函数编写咱们可以针对日志编写一个输入日志的Log函数,该函数的参数就包括日志级别、日志消息、失误文件称号、失误的行数。如下:

voidLog(std::stringlevel,std::stringmessage,std::stringfile_name,intline){std::cout<<"["<

说明一下:调用TIME函数时传入nullptr即可失掉以后的时期戳,因此调用Log函数时不用传入时期戳。

文件称号和行数的疑问

经过C言语中的预约义符号__FILE__和__LINE__,区分可以失掉以后文件的称号和以后的行数,但最好在调用Log函数时不用调用者显示的传入__FILE__和__LINE__,由于每次调用Log函数时传入的这两个参数都是固定的。

须要留意的是,不能将__FILE__和__LINE__设置为参数的缺省值,由于这样每次失掉到的都是Log函数所在的文件称号和所在的行数。而宏可以在预处置时期将代码拔出到指标地点,因此咱们可以定义如下宏:

#defineLOG(level,message)Log(level,message,__FILE__,__LINE__)

后续须要打印日志的时刻就间接调用LOG,调用时只须要传入日志级别和日志消息,在预处置时期__FILE__和__LINE__就会被拔出到指标地点,这时就能失掉到日志发生的文件称号和对应的行数了。

日志级别传入疑问咱们后续调用LOG传入日志级别时,必需宿愿以INFO、WARNING这样的方式传入,而不是以"INFO"、"WARNING"这样的方式传入,这时咱们可以将这四个日志级别定义为宏,而后经过#将宏参数level变成对应的字符串。如下:

#defineINFO1#defineWARNING2#defineERROR3#defineFATAL4#defineLOG(level,message)Log(#level,message,__FILE__,__LINE__)

此时以INFO、WARNING的方式传入LOG的宏参数,就会被转换成对应的字符串传递给Log函数的level参数,后续咱们就可以以如下方式输入日志了:

LOG(INFO,"Thisisademo");//LOG经常使用示例

套接字相关代码编写

们可以将套接字相关的代码封装到TcpServer类中,在初始化TcpServer对象时成功套接字的创立、绑定和监听举措,并向外提供一个sock接口用于失掉监听套接字。

此外,可以将TcpServer设置成单例形式:

将TcpServer类的结构函数设置为私有,并将拷贝结构和拷贝赋值函数设置为私有或删除,防止外部创立或拷贝对象。提供一个指向单例对象的static指针,并在类外将其初始化为nullptr。提供一个全局访问点失掉单例对象,在单例对象第一次性被失掉的时刻就创立这个单例对象并启动初始化。代码如下:

#defineBACKLOG5//TCP主机classTcpServer{private:int_port;//端口号int_listen_sock;//监听套接字staticTcpServer*_svr;//指向单例对象的static指针private://结构函数私有TcpServer(intport):_port(port),_listen_sock(-1){}//将拷贝结构函数和拷贝赋值函数私有或删除(防拷贝)TcpServer(constTcpServer&)=delete;TcpServer*operator=(constTcpServer&)=delete;public://失掉单例对象staticTcpServer*GetInstance(intport){staticpthread_mutex_tmtx=PTHREAD_MUTEX_INITIALIZER;//定义静态的互斥锁if(_svr==nullptr){pthread_mutex_lock(&mtx);//加锁if(_svr==nullptr){//创立单例TCP主机对象并初始化_svr=newTcpServer(port);_svr->InitServer();}pthread_mutex_unlock(&mtx);//解锁}return_svr;//前往单例对象}//初始化主机voidInitServer(){Socket();//创立套接字Bind();//绑定Listen();//监听LOG(INFO,"tcp_serverinit...success");}//创立套接字voidSocket(){_listen_sock=socket(AF_INET,SOCK_STREAM,0);if(_listen_sock<0){//创立套接字失败LOG(FATAL,"socketerror!");exit(1);}//设置端口复用intopt=1;setsockopt(_listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));LOG(INFO,"createsocket...success");}//绑定voidBind(){structsockaddr_inlocal;memset(&local,0,sizeof(local));local.sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=INADDR_ANY;if(bind(_listen_sock,(structsockaddr*)&local,sizeof(local))<0){//绑定失败LOG(FATAL,"binderror!");exit(2);}LOG(INFO,"bindsocket...success");}//监听voidListen(){if(listen(_listen_sock,BACKLOG)<0){//监听失败LOG(FATAL,"listenerror!");exit(3);}LOG(INFO,"listensocket...success");}//失掉监听套接字intSock(){return_listen_sock;}~TcpServer(){if(_listen_sock>=0){//封锁监听套接字close(_listen_sock);}}};//单例对象指针初始化为nullptrTcpServer*TcpServer::_svr=nullptr;

说明一下:

HTTP主机主体逻辑

咱们可以将HTTP主机封装成一个HttpServer类,在结构HttpServer对象时传入一个端口号,之后就可以调用Loop让主机运转起来了。主机运转起来后要做的就是,先失掉单例对象TcpServer中的监听套接字,而后始终从监听套接字中失掉新衔接,每当失掉到一个新衔接后就创立一个新线程为该衔接提供服务。

代码如下:

#definePORT8081//HTTP主机classHttpServer{private:int_port;//端口号public:HttpServer(intport):_port(port){}//启动主机voidLoop(){LOG(INFO,"loopbegin");TcpServer*tsvr=TcpServer::GetInstance(_port);//失掉TCP主机单例对象intlisten_sock=tsvr->Sock();//失掉监听套接字while(true){structsockaddr_inpeer;memset(&peer,0,sizeof(peer));socklen_tlen=sizeof(peer);intsock=accept(listen_sock,(structsockaddr*)&peer,&len);//失掉新衔接if(sock<0){continue;//失掉失败,继续失掉}//打印客户端相关消息std::stringclient_ip=inet_ntoa(peer.sin_addr);intclient_port=ntohs(peer.sin_port);LOG(INFO,"getanewlink:["+client_ip+":"+std::to_string(client_port)+"]");//创立新线程处置新衔接动员的HTTP恳求int*p=newint(sock);pthread_ttid;pthread_create(&tid,nullptr,CallBack::HandlerRequest,(void*)p);pthread_detach(tid);//线程分别}}~HttpServer(){}};

说明一下:

运转主机时要求指定主机的端口号,咱们用这个端口号创立一个HttpServer对象,而后调用Loop函数运转主机,此时主机就会始终失掉新衔接并创立新线程来处置衔接。

staticvoidUsage(std::stringproc){std::cout<<"Usage:\n\t"<Loop();//启动主机return0;}

HTTP恳求结构设计

咱们可以将HTTP恳求封装成一个类,这个类当中包括HTTP恳求的内容、HTTP恳求的解析结果以及能否须要经常使用CGI形式的标志位。后续处置恳求时就可以定义一个HTTP恳求类,读取到的HTTP恳求的数据就存储在这个类当中,解析HTTP恳求后失掉的数据也存储在这个类当中。

代码如下:

//HTTP恳求classHttpRequest{public://HTTP恳求内容std::string_request_line;//恳求行std::vector_request_header;//恳求报头std::string_blank;//空行std::string_request_body;//恳求注释//解析结果std::string_method;//恳求方法std::string_uri;//URIstd::string_version;//版本号std::unordered_map_header_kv;//恳求报头中的键值对int_content_length;//注释长度std::string_path;//恳求资源的门路std::string_query_string;//uri中携带的参数//CGI相关bool_cgi;//能否须要经常使用CGI形式public:HttpRequest():_content_length(0)//自动恳求注释长度为0,_cgi(false)//自动不经常使用CGI形式{}~HttpRequest(){}};

HTTP照应结构设计

HTTP照应也可以封装成一个类,这个类当中包括HTTP照应的内容以及构建HTTP照应所须要的数据。后续构建照应时就可以定义一个HTTP照应类,构建照应须要经常使用的数据就存储在这个类当中,构建后失掉的照应内容也存储在这个类当中。

代码如下:

//HTTP照应classHttpResponse{public://HTTP照应内容std::string_status_line;//形态行std::vector_response_header;//照应报头std::string_blank;//空行std::string_response_body;//照应注释(CGI相关)//所需数据int_status_code;//形态码int_fd;//照应文件的fd(非CGI相关)int_size;//照应文件的大小(非CGI相关)std::string_suffix;//照应文件的后缀(非CGI相关)public:HttpResponse():_blank(LINE_END)//设置空行,_status_code(OK)//形态码默以为200,_fd(-1)//照应文件的fd初始化为-1,_size(0)//照应文件的大小默以为0{}~HttpResponse(){}};
标签: 主机主机IBMHTTP

本文地址: https://yihaiquanyi.com/article/e4e8c0d63db62f31d4de.html

上一篇:M5110阵列卡实现完美变身直通卡利用85元入...
下一篇:基于RoCE技术的UCloud高性能网络架构设计基...

发表评论