首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

初看HTTP协议

到目前为之,我们的 TCP 服务器都还只是一个 demo,并没有提供什么有意思的功能。前文中实现的cho服务器只能说明通道是通的,别的什么作用也没有。从本节开始,我们打算写点有用的东西。

如果问2000年以来什么发展最快,相信很多人会说是互联网。我记得2000年那会儿我们那个边远的小县城才刚刚开始有网吧出现,网吧为了招揽客户会拉横幅,“两根电话线同时上网,速度有保障”。现如今光纤、4G、5G通信技术的发展,网速的问题已经不再那么重要了。小时候我们描绘小康生活还是,“楼上楼下,电灯电话”。现在的因为百度、阿里、腾讯、美团、京东等等各个互联网巨头企业的发展,我的日常生活已经变成了,“线上线下,买这买那”。十分幸运,从童年到中年,我见证了整个互联网行业,从起步到兴旺蓬勃的发展过程。

可以说,web是贯穿整个互联网发展始终的关键技术。我们打开的网页,APP中访问的链接都离不开web。HTTP 则是把web页面从服务器端搬运到浏览器或者APP中的必要手段。市面上有很多出色的网络服务器,比如Apache,Nginx,Tomcat等。但我们还是想再写一个 HTTP 服务器,练手而已。从本节开始,我们将围绕着提供可靠的 HTTP 服务开发小土的网络工具箱

1. Http协议简介

HTTP 的全称是 HyperText Transfer Protocol 超文本传输协议。按照一般的四层协议(链路层、网络层IP、运输层TCP、应用层)划分,它建立在 TCP/IP 的通信基础之上,属于应用层的协议。

我们通过浏览器访问web页面的时候,实际上是通过浏览器与远程的web服务器,建立了一个或者若干个 TCP 连接,并在这些连接上通过发送 HTTP 请求。服务器做出应答后,通过 HTTP 返回请求内容到浏览器, 浏览器再通过html5, css, javascript等前端技术把页面呈现出来。整个过程如右图所示。

关于 TCP 的协议这里就不再详细介绍了,读者可以回顾套接字与TCP通信。 作为 TCP 协议的上层,HTTP的报文封装在 TCP 的数据部分中。如作图所示,浏览器把用户数据打包成正文部分,再添加上起始行和首部,构成了一帧HTTP报文。再依次通过 TCP 套接字、IP协议栈、 以太网驱动逐层添加 TCP、IP、以太网报文的首部和尾部,发送到网络上。

以太网报文在网络上几经辗转之后,被服务器端的驱动程序接收到,然后反过来依次去除 IP 和 TCP 报文的首部,就得到了 HTTP 报文,解析报文就可以获知来自浏览器的请求。 服务器响应请求的过程类似,只是整个过程反过来执行一遍。

HTTP 报文是以一行行文本消息构成的,所以人类也可以直接读懂 HTTP 报文。右图是一个简单的HTTP GET请求与响应的报文,讲述的是,浏览器请求获取 "/DouNiWan.txt"文件,服务器找到了这个文件, 并把文件内容放到响应报文的正文中返回。

HTTP 报文分为起始行、首部和正文三个部分。在图中为了突出这三个部分,我在中间加了两条横线,实际的报文中是没有的。起始行和首部的每一行都是以回车换行(\r\n)结尾的。 首部的每一行是以 ':' 为分隔符的键值对,首部与正文之间由一个空行(即,只有"\r\n")分隔。另外 HTTP 报文中的正文不是必须要有的,比如这里的请求报文就没有正文。

请求报文的起始行是在告诉服务器,请求的方法以及目标URL,同时还会附带上协议版本。这里的请求报文就是一个 GET 请求,目的是从服务器那里获取一个文档。 以空格分隔的“/DouNiWan.txt”就是一个URL,指示了希望获取的文档路径。HTTP一共定义了7种方法,常用的有 GET, POST, HEAD, PUT, DELETE五种,如下表所示,TRACE和OPTIONS比较高级没怎么用过, 不太清楚具体作用,下表就不再罗列了。起始行的最后是协议版本。其实在 HTTP 1.0 之前的版本中是不需要这个版本号的。

方法 描述 是否包含正文
GET 从服务器那里获取一份文档
POST 向服务器发送一些数据,请求服务器处理
HEAD 与GET类似,只是服务器返回的报文中只有起始行和首部没有正文,一般用来查询一个URL连接是否有效。
PUT 向服务器发送一份文档,请求保存到服务器上。
DELETE 请求服务器删除指定的文档。

响应报文的起始行则是用来给浏览器反馈,其请求的响应情况。它是由协议版本、状态码(status-code)和原因短语(reason-phrase)三部分构成。这里示例的响应报文中的200就是状态码, 表示请求被成功响应了。“OK”是原因短语正如字面意思那样,一切都挺好的。除了200之外,还定义了很多其它状态码,比如著名的 404,后续我们将在实现类型 HttpResponnse 的时候详细介绍。

2. say hello to browser

当我们在浏览器的地址栏中键入"http://localhost:65530/douniwan.html"然后敲下回车都发生了什么事情呢?当然现在浏览器除了报错说无法连接服务器之外,别的什么都不会发生。 接下来,我们将实现一个简单的 HTTP 服务器来响应浏览器的这一条请求。

作为浏览器,我们首先得了解地址栏中输入的这一长串字符都是什么意思。如右图所示,这串字符被称为 URL (统一资源定位符, Uniform/Universal Resource Locator)。 它有三个部分,最前面的"http://"告诉浏览器使用 HTTP 协议访问资源。第二部分说明了资源所在的主机名,这里使用的是"localhost",表示本地。更多的时候,这个主机名是一个域名。 最后就是告诉浏览器所要访问的资源在目标主机上的路径。浏览器拿到这个 URL 解析之后就要在网络上寻找目标主机,请求资源了。

我们已经知道 HTTP 是建立在 TCP/IP 之上的应用层协议,TCP/IP负责具体的网络连接和数据传输的任务。只要知道了源和目标的IP地址和端口号,就可以建立一个TCP连接。 对于浏览器而言,源IP地址和端口可以由其宿主机来提供。在刚才的那个URL中目标端口已经显式的给出了是65530,目标IP地址隐藏在了 localhost 中。 浏览器可以在宿主机的"/etc/hosts"中找到一条关于 localhost 的记录,对应着一个 "127.0.0.1" 的IP地址,这正是IP协议中规定的本地回环网络中宿主机自己的IP地址。 更多的时候,这个主机名是一个域名,此时浏览器会委托宿主机去找一个DNS服务器去查询域名所对应的IP地址,这个过程就是所谓的域名解析。

此时,如果读者运行我们之前的echo服务器例程的话, 应该可以看到浏览器建立的网络连接,并且会把其发送的 GET 请求报文打印出来,如下左图所示。 因为echo服务器会把接收到的数据原样返回,所以此时我的chrome浏览器会报一个"ERR_INVALID_HTTP_RESPONSE"的错。

为了能够跟浏览器对上话,我们在对echo服务器做了简单的调整,如下面的例程中的第28行到32行中,无论TCP连接上接收到什么消息,我们都直接返回一个 200 的 HTTP 响应报文,然后关闭连接。 此时刷新浏览器,就可以看到上面右图所示的浏览器中显示一个hello。读者运行该例程时,需要在源码tag:v0.0.7a的基础上进行编译, 把例程考下来放到utils目录下,修改CMakeLists.txt添加add_executable编译项。

        /******************************************************************************
         * 
         * u_say_hello_to_browser 
         * 
         * 单线程
         * 
         *****************************************************************************/
        
        #include <XiaoTuNetBox/TcpServer.h>
        #include <XiaoTuNetBox/InBufObserver.h>
        
        #include <memory>
        #include <vector>
        #include <functional>
        
        using namespace std::placeholders;
        using namespace xiaotu::net;
        
        class Session {
            public:
                Session(ConnectionPtr const & conn)
                    : mConn(conn)
                {
                    mObserver = conn->GetInputBuffer().CreateObserver();
                    mObserver->SetRecvCallBk(std::bind(&Session::Echo, this));
                }
        
                void Echo()
                {
                    mConn->SendString("HTTP/1.1 200 OK\r\n\r\nhello");
                    mConn->Close();
                }
        
            private:
                ConnectionPtr mConn;
                InBufObserverPtr mObserver;
        };
        
        typedef std::shared_ptr SessionPtr;
        std::vector sessions;
        
        void OnNewConnection(ConnectionPtr const & conn) {
            std::cout << "新建连接:" << conn->GetInfo() << std::endl;
            conn->GetHandler()->SetNonBlock(true);
        
            sessions.push_back(SessionPtr(new Session(conn)));
        }
        
        void OnCloseConnection(ConnectionPtr const & conn) {
            std::cout << "关闭连接:" << conn->GetInfo() << std::endl;
        }
        
        
        int main() {
            PollLoopPtr loop = CreatePollLoop();
            TcpServer tcp(loop, 65530, 3);
        
            tcp.SetTimeOut(10, 0, 5);
            tcp.SetNewConnCallBk(std::bind(OnNewConnection, _1));
            tcp.SetCloseConnCallBk(std::bind(OnCloseConnection, _1));
        
            loop->Loop(10);
        
            return 0;
        }

从我们在浏览器的地址栏键入网址之后,先后发生了7件事情:

  1. 浏览器解析用户输入的URL,获知目标主机名和端口和请求的资源路径
  2. 浏览器通过域名解析获知目标主机的IP地址
  3. 浏览器从宿主机中获得源IP地址和端口
  4. 浏览器根据建立到服务器的TCP连接
  5. 浏览器通过TCP连接发送HTTP请求报文
  6. 服务器接收报文,并发送响应报文到浏览器
  7. 浏览器显示网页,关闭TCP连接。

4. 完

本文中,我们介绍了 HTTP 协议及其与TCP的关系,讨论了 HTTP 报文的格式和5种常用的请求方法,并通过一个简单的例程u_say_hello_to_browser介绍了从浏览器键入网址到显示出网页的过程。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1