实战整了一个后台服务,真香!

大家好,我是编程君!

HTTP 服务是重中之重,今天分享一下 一个HTTP服务的实现。

项目介绍

本项目实现的是一个HTTP服务器,项目中将会通过基本的网络套接字读取客户端发来的HTTP请求并进行分析,最终构建HTTP响应并返回给客户端。

HTTP在网络应用层中的地位是不可撼动的,无论是移动端还是PC端浏览器,HTTP无疑是打开互联网应用窗口的重要协议。

该项目将会把HTTP中最核心的模块抽取出来,采用CS模型实现一个小型的HTTP服务器,目的在于理解HTTP协议的处理过程。

该项目主要涉及C/C++、HTTP协议、网络套接字编程、CGI、单例模式、多线程、线程池等方面的技术。

网络协议栈介绍

协议分层

网络协议栈的分层情况如下:

实战整了一个后台服务,真香!

网络协议栈中各层的功能如下:

  1. 应用层:根据特定的通信目的,对数据进行分析处理,以达到某种业务性的目的。
  2. 传输层:处理传输时遇到的问题,主要是保证数据传输的可靠性。
  3. 网络层:完成数据的转发,解决数据去哪里的问题。
  4. 链路层:负责数据真正的发生过程。

数据的封装与分用

数据封装与分用的过程如下:

实战整了一个后台服务,真香!也就是说,发送端在发生数据前,该数据需要先自顶向下贯穿网络协议栈完成数据的封装,在这个过程中,每一层协议都会为该数据添加上对应的报头信息。接收端在收到数据后,该数据需要先自底向上贯穿网络协议栈完成数据的解包和分用,在这个过程中,每一层协议都会将对应的报头信息提取出来。

而本项目要做的就是,在接收到客户端发来的HTTP请求后,将HTTP的报头信息提取出来,然后对数据进行分析处理,最终将处理结果添加上HTTP报头再发送给客户端。

需要注意的是,该项目中我们所处的位置是应用层,因此我们读取的HTTP请求实际是从传输层读取上来的,而我们发送的HTTP响应实际也只是交给了传输层,数据真正的发送还得靠网络协议栈中的下三层来完成,这里直接说“接收到客户端的HTTP请求”以及“发送HTTP响应给客户端”,只是为了方便大家理解,此外,同层协议之间本身也是可以理解成是在直接通信的。

HTTP相关知识介绍

HTTP的特点

HTTP的五大特点如下:

  1. 客户端服务器模式(CS,BS):在一条通信线路上必定有一端是客户端,另一端是服务器端,请求从客户端发出,服务器响应请求并返回。
  2. 简单快速:客户端向服务器请求服务时,只需传送请求方法和请求资源路径,不需要发送额外过多的数据,并且由于HTTP协议结构较为简单,使得HTTP服务器的程序规模小,因此通信速度很快。
  3. 灵活:HTTP协议对数据对象没有要求,允许传输任意类型的数据对象,对于正在传输的数据类型,HTTP协议将通过报头中的Content-Type属性加以标记。
  4. 无连接:每次连接都只会对一个请求进行处理,当服务器对客户端的请求处理完毕并收到客户端的应答后,就会直接断开连接。HTTP协议采用这种方式可以大大节省传输时间,提高传输效率。
  5. 无状态:HTTP协议自身不对请求和响应之间的通信状态进行保存,每个请求都是独立的,这是为了让HTTP能更快地处理大量事务,确保协议的可伸缩性而特意设计的。
  6. 微信搜索公众号:Java项目精选,回复:java 领取资料 。

说明一下:

  1. 随着HTTP的普及,文档中包含大量图片的情况多了起来,每次请求都要断开连接,无疑增加了通信量的开销,因此HTTP1.1支持了长连接Keey-Alive,就是任意一端只要没有明确提出断开连接,则保持连接状态。(当前项目实现的是1.0版本的HTTP服务器,因此不涉及长连接)
  2. HTTP无状态的特点无疑可以减少服务器内存资源的消耗,但是问题也是显而易见的。比如某个网站需要登录后才能访问,由于无状态的特点,那么每次跳转页面的时候都需要重新登录。为了解决无状态的问题,于是引入了Cookie技术,通过在请求和响应报文中写入Cookie信息来控制客户端的状态,同时为了保护用户数据的安全,又引入了Session技术,因此现在主流的HTTP服务器都是通过Cookie+Session的方式来控制客户端的状态的。

URL格式

URL(Uniform Resource Lacator)叫做统一资源定位符,也就是我们通常所说的网址,是因特网的万维网服务程序上用于指定信息位置的表示方法。

一个URL大致由如下几部分构成:实战整了一个后台服务,真香!简单说明:

  1. http://表示的是协议名称,表示请求时需要使用的协议,通常使用的是HTTP协议或安全协议HTTPS。
  2. user:pass表示的是登录认证信息,包括登录用户的用户名和密码。(可省略)
  3. www.example.jp表示的是服务器地址,通常以域名的形式表示。
  4. 80表示的是服务器的端口号。(可省略)
  5. /dir/index.html表示的是要访问的资源所在的路径(/表示的是web根目录)。
  6. uid=1表示的是请求时通过URL传递的参数,这些参数以键值对的形式通过&符号分隔开。(可省略)
  7. ch1表示的是片段标识符,是对资源的部分补充。(可省略) 注意:
  8. 如果访问服务器时没有指定要访问的资源路径,那么浏览器会自动帮我们添加/,但此时仍然没有指明要访问web根目录下的哪一个资源文件,这时默认访问的是目标服务的首页。
  9. 大部分URL中的端口号都是省略的,因为常见协议对应的端口号都是固定的,比如HTTP、HTTPS和SSH对应的端口号分别是80、443和22,在使用这些常见协议时不必指明协议对应的端口号,浏览器会自动帮我们进行填充。

URI、URL、URN

URI、URL、URN的定义如下:

  1. URI(Uniform Resource Indentifier)统一资源标识符:用来唯一标识资源。
  2. URL(Uniform Resource Locator)统一资源定位符:用来定位唯一的资源。
  3. URN(Uniform Resource Name)统一资源名称:通过名字来标识资源,比如mailto:java-net@java.sun.com。URI、URL、URN三者的关系 URL是URI的一种,URL不仅能唯一标识资源,还定义了该如何访问或定位该资源,URN也是URI的一种,URN通过名字来标识资源,因此URL和URN都是URI的子集。

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

实战整了一个后台服务,真香!

URI有绝对和相对之分:

  1. 绝对的URI:对标识符出现的环境没有依赖,比如URL就是一种绝对的URI,同一个URL无论出现在什么地方都能唯一标识同一个资源。
  2. 相对的URI:对标识符出现的环境有依赖,比如HTTP请求行中的请求资源路径就是一种相对的URI,这个资源路径出现在不同的主机上标识的就是不同的资源。

HTTP的协议格式

HTTP请求协议格式如下:

实战整了一个后台服务,真香!HTTP请求由以下四部分组成:

  1. 请求行:[请求方法] + [URI] + [HTTP版本]。
  2. 请求报头:请求的属性,这些属性都是以key: value的形式按行陈列的。
  3. 空行:遇到空行表示请求报头结束。
  4. 请求正文:请求正文允许为空字符串,如果请求正文存在,则在请求报头中会有一个Content-Length属性来标识请求正文的长度。HTTP响应协议格式如下:实战整了一个后台服务,真香!HTTP响应由以下四部分组成:
  5. 状态行:[HTTP版本] + [状态码] + [状态码描述]。
  6. 响应报头:响应的属性,这些属性都是以key: value的形式按行陈列的。
  7. 空行:遇到空行表示响应报头结束。
  8. 响应正文:响应正文允许为空字符串,如果响应正文存在,则在响应报头中会有一个Content-Length属性来标识响应正文的长度。

HTTP的请求方法

HTTP常见的请求方法如下:实战整了一个后台服务,真香!GET方法和POST方法
HTTP的请求方法中最常用的就是GET方法和POST方法,其中GET方法一般用于获取某种资源信息,而POST方法一般用于将数据上传给服务器,但实际GET方法也可以用来上传数据,比如百度搜索框中的数据就是使用GET方法提交的。

GET方法和POST方法都可以带参,其中GET方法通过URL传参,POST方法通过请求正文传参。由于URL的长度是有限制的,因此GET方法携带的参数不能太长,而POST方法通过请求正文传参,一般参数长度没有限制。微信搜索公众号:Java项目精选,回复:java 领取资料 。

HTTP的状态码

实战整了一个后台服务,真香!实战整了一个后台服务,真香!

HTTP常见的Header

  1. Content-Type:数据类型(text/html等)。
  2. Content-Length:正文的长度。
  3. Host:客户端告知服务器,所请求的资源是在哪个主机的哪个端口上。
  4. User-Agent:声明用户的操作系统和浏览器的版本信息。
  5. Referer:当前页面是哪个页面跳转过来的。
  6. Location:搭配3XX状态码使用,告诉客户端接下来要去哪里访问。
  7. Cookie:用户在客户端存储少量信息,通常用于实现会话(session)的功能。

CGI机制介绍

CGI(Common Gateway Interface,通用网关接口)是一种重要的互联网技术,可以让一个客户端,从网页浏览器向执行在网络服务器上的程序请求数据。CGI描述了服务器和请求处理程序之间传输数据的一种标准。

实际我们在进行网络请求时,无非就两种情况:

  1. 浏览器想从服务器上拿下来某种资源,比如打开网页、下载等。
  2. 浏览器想将自己的数据上传至服务器,比如上传视频、登录、注册等。实战整了一个后台服务,真香!

通常从服务器上获取资源对应的请求方法就是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程序:

  1. 如果请求方法为GET方法,那么用户是通过URL传递参数的,此时可以在子进程进行进程程序替换之前,通过putenv函数将参数导入环境变量,由于环境变量也不受进程程序替换的影响,因此被替换后的CGI程序就可以通过getenv函数来获取对应的参数。
  2. 如果请求方法为POST方法,那么用户是通过请求正文传参的,此时父进程直接将请求正文中的数据写入管道传递给CGI程序即可,但是为了让CGI程序知道应该从管道读取多少个参数,父进程还需要通过putenv函数将请求正文的长度导入环境变量。

说明一下:请求正文长度、URL传递的参数以及请求方法都比较短,通过写入管道来传递会导致效率降低,因此选择通过导入环境变量的方式来传递。

也就是说,使用CGI模式时如果请求方法为POST方法,那么CGI程序需要从管道读取父进程传递过来的数据,如果请求方法为GET方法,那么CGI程序需要从环境变量中获取父进程传递过来的数据。

但被替换后的CGI程序实际并不知道本次HTTP请求所对应的请求方法,因此在子进程在进行进程程序替换之前,还需要通过putenv函数将本次HTTP请求所对应的请求方法也导入环境变量。因此CGI程序启动后,首先需要先通过环境变量得知本次HTTP请求所对应的请求方法,然后再根据请求方法对应从管道或环境变量中获取父进程传递过来的数据。

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

CGI机制的意义

CGI机制的处理流程如下:

实战整了一个后台服务,真香!处理HTTP请求的步骤如下:

  1. 判断请求方法是GET方法还是POST方法,如果是GET方法带参或POST方法则进行CGI处理,如果是GET方法不带参则进行非CGI处理。

  2. 非CGI处理就是直接根据用户请求的资源构建HTTP响应返回给浏览器。

  3. CGI处理就是通过创建子进程进行程序替换的方式来调用CGI程序,通过创建匿名管道、重定向、导入环境变量的方式来与CGI程序进行数据通信,最终根据CGI程序的处理结果构建HTTP响应返回给浏览器。

  4. CGI机制就是让服务器将获取到的数据交给对应的CGI程序进行处理,然后将CGI程序的处理结果返回给客户端,这显然让服务器逻辑和业务逻辑进行了解耦,让服务器和业务程序可以各司其职。

  5. CGI机制使得浏览器输入的数据最终交给了CGI程序,而CGI程序输出的结果最终交给了浏览器。这也就意味着CGI程序的开发者,可以完全忽略中间服务器的处理逻辑,相当于CGI程序从标准输入就能读取到浏览器输入的内容,CGI程序写入标准输出的数据最终就能输出到浏览器。

日志编写

服务器在运作时会产生一些日志,这些日志会记录下服务器运行过程中产生的一些事件。本项目中的日志格式如下:

实战整了一个后台服务,真香!日志说明:

  1. 日志级别:分为四个等级,从低到高依次是INFO、WARNING、ERROR、FATAL。
  2. 时间戳:事件产生的时间。
  3. 日志信息:事件产生的日志信息。
  4. 错误文件名称:事件在哪一个文件产生。
  5. 行数:事件在对应文件的哪一行产生。日志级别说明:
  6. INFO:表示正常的日志输出,一切按预期运行。
  7. WARNING:表示警告,该事件不影响服务器运行,但存在风险。
  8. ERROR:表示发生了某种错误,但该事件不影响服务器继续运行。
  9. FATAL:表示发生了致命的错误,该事件将导致服务器停止运行。

日志函数编写   我们可以针对日志编写一个输出日志的Log函数,该函数的参数就包括日志级别、日志信息、错误文件名称、错误的行数。如下:

void Log(std::string level, std::string message, std::string file_name, int line)
{
  std::cout<<"["<<level<<"]["<<time(nullptr)<<"]["<<message<<"]["<<file_name<<"]["<<line<<"]"<<std::endl;
}

说明一下:调用time函数时传入nullptr即可获取当前的时间戳,因此调用Log函数时不必传入时间戳。

文件名称和行数的问题

通过C语言中的预定义符号__FILE__和__LINE__,分别可以获取当前文件的名称和当前的行数,但最好在调用Log函数时不用调用者显示的传入__FILE__和__LINE__,因为每次调用Log函数时传入的这两个参数都是固定的。

需要注意的是,不能将__FILE__和__LINE__设置为参数的缺省值,因为这样每次获取到的都是Log函数所在的文件名称和所在的行数。而宏可以在预处理期间将代码插入到目标地点,因此我们可以定义如下宏:

#define LOG(level, message) Log(level, message, __FILE__, __LINE__)

后续需要打印日志的时候就直接调用LOG,调用时只需要传入日志级别和日志信息,在预处理期间__FILE__和__LINE__就会被插入到目标地点,这时就能获取到日志产生的文件名称和对应的行数了。

日志级别传入问题   我们后续调用LOG传入日志级别时,肯定希望以INFO、WARNING这样的方式传入,而不是以”INFO”、”WARNING”这样的形式传入,这时我们可以将这四个日志级别定义为宏,然后通过#将宏参数level变成对应的字符串。如下:

#define INFO    1
#define WARNING 2
#define ERROR   3
#define FATAL   4

#define LOG(level, message) Log(#level, message, __FILE__, __LINE__)  

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

LOG(INFO, "This is a demo"); //LOG使用示例

套接字相关代码编写

们可以将套接字相关的代码封装到TcpServer类中,在初始化TcpServer对象时完成套接字的创建、绑定和监听动作,并向外提供一个Sock接口用于获取监听套接字。

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

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

#define BACKLOG 5

//TCP服务器
class TcpServer{
    private:
        int _port;              //端口号
        int _listen_sock;       //监听套接字
        static TcpServer* _svr; //指向单例对象的static指针
    private:
        //构造函数私有
        TcpServer(int port)
            :_port(port)
            ,_listen_sock(-1)
        {}
        //将拷贝构造函数和拷贝赋值函数私有或删除(防拷贝)
        TcpServer(const TcpServer&)=delete;
        TcpServer* operator=(const TcpServer&)=delete;
    public:
        //获取单例对象
        static TcpServer* GetInstance(int port)
        {
            static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; //定义静态的互斥锁
            if(_svr == nullptr){
                pthread_mutex_lock(&mtx); //加锁
                if(_svr == nullptr){
                    //创建单例TCP服务器对象并初始化
                    _svr = new TcpServer(port);
                    _svr->InitServer();
                }
                pthread_mutex_unlock(&mtx); //解锁
            }
            return _svr; //返回单例对象
        }
        //初始化服务器
        void InitServer()
        {
            Socket(); //创建套接字
            Bind();   //绑定
            Listen(); //监听
            LOG(INFO, "tcp_server init ... success");
        }
        //创建套接字
        void Socket()
        {
            _listen_sock = socket(AF_INET, SOCK_STREAM, 0);
            if(_listen_sock < 0){ //创建套接字失败
                LOG(FATAL, "socket error!");
                exit(1);
            }
            //设置端口复用
            int opt = 1;
            setsockopt(_listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
            LOG(INFO, "create socket ... success");
        }
        //绑定
        void Bind()
        {
            struct sockaddr_in local;
            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, (struct sockaddr*)&local, sizeof(local)) < 0){ //绑定失败
                LOG(FATAL, "bind error!");
                exit(2);
            }
            LOG(INFO, "bind socket ... success");
        }
        //监听
        void Listen()
        {
            if(listen(_listen_sock, BACKLOG) < 0){ //监听失败
                LOG(FATAL, "listen error!");
                exit(3);
            }
            LOG(INFO, "listen socket ... success");
        }
        //获取监听套接字
        int Sock()
        {
            return _listen_sock;
        }
        ~TcpServer()
        {
            if(_listen_sock >= 0){ //关闭监听套接字
                close(_listen_sock);
            }
        }
};
//单例对象指针初始化为nullptr
TcpServer* TcpServer::_svr = nullptr;

说明一下:

  1. 如果使用的是云服务器,那么在设置服务器的IP地址时,不需要显式绑定IP地址,直接将IP地址设置为INADDR_ANY即可,此时服务器就可以从本地任何一张网卡当中读取数据。此外,由于INADDR_ANY本质就是0,因此在设置时不需要进行网络字节序列的转换。
  2. 在第一次调用GetInstance获取单例对象时需要创建单例对象,这时需要定义一个锁来保证线程安全,代码中以PTHREAD_MUTEX_INITIALIZER的方式定义的静态的锁是不需要释放的,同时为了保证后续调用GetInstance获取单例对象时不会频繁的加锁解锁,因此代码中以双检查的方式进行加锁。

HTTP服务器主体逻辑

我们可以将HTTP服务器封装成一个HttpServer类,在构造HttpServer对象时传入一个端口号,之后就可以调用Loop让服务器运行起来了。服务器运行起来后要做的就是,先获取单例对象TcpServer中的监听套接字,然后不断从监听套接字中获取新连接,每当获取到一个新连接后就创建一个新线程为该连接提供服务。

代码如下:

#define PORT 8081

//HTTP服务器
class HttpServer{
    private:
        int _port; //端口号
    public:
        HttpServer(int port)
            :_port(port)
        {}

        //启动服务器
        void Loop()
        {
            LOG(INFO, "loop begin");
            TcpServer* tsvr = TcpServer::GetInstance(_port); //获取TCP服务器单例对象
            int listen_sock = tsvr->Sock(); //获取监听套接字
            while(true){
                struct sockaddr_in peer;
                memset(&peer, 0, sizeof(peer));
                socklen_t len = sizeof(peer);
                int sock = accept(listen_sock, (struct sockaddr*)&peer, &len); //获取新连接
                if(sock < 0){
                    continue; //获取失败,继续获取
                }

                //打印客户端相关信息
                std::string client_ip = inet_ntoa(peer.sin_addr);
                int client_port = ntohs(peer.sin_port);
                LOG(INFO, "get a new link: ["+client_ip+":"+std::to_string(client_port)+"]");
                
                //创建新线程处理新连接发起的HTTP请求
                int* p = new int(sock);
                pthread_t tid;
                pthread_create(&tid, nullptr, CallBack::HandlerRequest, (void*)p);
                pthread_detach(tid); //线程分离
            }
        }
        ~HttpServer()
        {}
};

说明一下:

  1. 服务器需要将新连接对应的套接字作为参数传递给新线程,为了避免该套接字在新线程读取之前被下一次获取到的套接字覆盖,因此在传递套接字时最好重新new一块空间来存储套接字的值。
  2. 新线程创建后可以将新线程分离,分离后主线程继续获取新连接,而新线程则处理新连接发来的HTTP请求,代码中的HandlerRequest函数就是新线程处理新连接时需要执行的回调函数。

运行服务器时要求指定服务器的端口号,我们用这个端口号创建一个HttpServer对象,然后调用Loop函数运行服务器,此时服务器就会不断获取新连接并创建新线程来处理连接。

static void Usage(std::string proc)
{
    std::cout<<"Usage:nt"<<proc<<" port"<<std::endl;
}
int main(int argc, char* argv[])
{
    if(argc != 2){
        Usage(argv[0]);
        exit(4);
    }
    int port = atoi(argv[1]); //端口号
    std::shared_ptr<HttpServer> svr(new HttpServer(port)); //创建HTTP服务器对象
    svr->Loop(); //启动服务器
    return 0;
}

HTTP请求结构设计

我们可以将HTTP请求封装成一个类,这个类当中包括HTTP请求的内容、HTTP请求的解析结果以及是否需要使用CGI模式的标志位。后续处理请求时就可以定义一个HTTP请求类,读取到的HTTP请求的数据就存储在这个类当中,解析HTTP请求后得到的数据也存储在这个类当中。

代码如下:

//HTTP请求
class HttpRequest{
    public:
        //HTTP请求内容
        std::string _request_line;                //请求行
        std::vector<std::string> _request_header; //请求报头
        std::string _blank;                       //空行
        std::string _request_body;                //请求正文

        //解析结果
        std::string _method;       //请求方法
        std::string _uri;          //URI
        std::string _version;      //版本号
        std::unordered_map<std::string, std::string> _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响应
class HttpResponse{
    public:
        //HTTP响应内容
        std::string _status_line;                  //状态行
        std::vector<std::string> _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()
        {}
};

来源:blog.csdn.net/chenlong_cxy/article/details/127906255

推荐阅读:

一行代码搞定Http请求,强得离谱~

Nginx 可视化神器!复杂配置一键生成,监控管理一条龙!

SpringBoot 项目打成 .exe 程序,实战来了,超级详细!

实战整了一个后台服务,真香!

PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。“在看”支持我们吧!

本篇文章来源于微信公众号:程序IT圈

原创文章,作者:software,如若转载,请注明出处:https://www.sldh123.com/6905.html

(0)
上一篇 12月 18, 2022 5:16 上午
下一篇 12月 18, 2022 5:16 上午

相关推荐

发表回复

您的电子邮箱地址不会被公开。