应用层协议——http协议

目录

认识URL

协议方案名

登录信息

服务器地址

服务器端口号

带层次的文件路径

查询字符串

UrlEncode(编码)和UrlDecode(解码)

见一见http协议的格式(包含如何编码构建一个像网站一样的可以提供服务的服务器)

http请求的格式

http响应的格式(包含web根目录的知识点)

http请求中的请求方法字段

http响应中的状态码字段

http请求和http响应中常见的报头字段

http协议的局限性


认识URL

URL(Uniform Resource Lacator)叫做统一资源定位符,也就是我们通常所说的网址,一个URL的格式通常如下图所示,下文中会对每个部分都进行介绍。

1a5b661ca8024d0da44e3a5807f361ed.png

协议方案名

上图的http://表示的是协议名称,表示请求时需要使用的协议,通常使用的是HTTP协议或安全协议HTTPS。HTTPS是以安全为目标的HTTP通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性。

常见的应用层协议:

  • DNS(Domain Name System)协议:域名系统。
  • FTP(File Transfer Protocol)协议:文件传输协议。
  • TELNET(Telnet)协议:远程终端协议。
  • HTTP(Hyper Text Transfer Protocol)协议:超文本传输协议。
  • HTTPS(Hyper Text Transfer Protocol over SecureSocket Layer)协议:安全数据传输协议。
  • SMTP(Simple Mail Transfer Protocol)协议:电子邮件传输协议。
  • POP3(Post Office Protocol - Version 3)协议:邮件读取协议。
  • SNMP(Simple Network Management Protocol)协议:简单网络管理协议。
  • TFTP(Trivial File Transfer Protocol)协议:简单文件传输协议。

登录信息

上图的usr:pass表示的是登录认证信息,包括登录用户的用户名和密码。虽然登录认证信息可以在URL中体现出来,但绝大多数URL的这个字段都是被省略的,因为登录信息可以通过其他方案交付给服务器。

服务器地址

上图的www.example.jp表示的是服务器的IP地址,也叫做域名,比如www. alibaba. com ,www. qq. com,www. baidu. com。

为什么在IP地址已经能标识出全网中的一台主机的情况下,还要搞出一个域名来呢?

  • 如果用户看到的是两个IP地址(即两个奇怪的数字),即使之前使用过这两个ip地址对应的网站,但用户在访问这些网站之前也大概率不知道这两个IP地址对应的是什么网站,也就不知道该网站是干嘛的,这是因为IP地址难以记忆;但如果用户看到的是www.baidu.comwww.qq.com这两个域名,如果用户之前使用过这些网站,那么用户大概率知道这个网站是干嘛的,因为域名更方便人类记忆。因此域名具有更好的自描述性。

所以实际我们可以认为域名和IP地址是等价的,在计算机当中使用的时候既可以使用域名,也可以使用IP地址。但URL呈现出来是可以让用户看到的,因此URL当中是以域名的形式表示服务器的IP地址的。

服务器端口号

上图的80表示的是服务器端口号,表示让80端口号对应的进程给别人提供服务,基于HTTP协议的服务端进程需要绑定的端口号通常默认是80(注意只是通常情况下是80,而不一定是80,具体是多少可以由编码者控制的,基于其他协议实现的服务端进程的端口号也一样)。

基于下面这些常见的应用层协议所实现的服务端进程通常默认需要绑定的端口号:

  • HTTP:80
  • HTTPS:443
  • SSH:22

问题:如下图,为什么通过百度的URL访问百度时,其中不需要加入端口号呢?

be9f9dadeecc4584b4b4d30a510ce97c.png

答案:基于某种应用层协议实现的服务端进程通常需要绑定该协议对应的默认端口号,比如这里是https协议,所以该服务端进程通常默认绑定的端口号就是443。然后所有不同种类的浏览器都有一种共识,在访问一个URL时,它会直接看协议方案名,比如发现是https协议,那么当你没有显示传入需要访问的端口号是多少时,浏览器就默认访问指定IP下的443端口,既然浏览器能自动访问443端口,所以我们在访问的时候就不需要自己添加端口号了。

所以从上面能得到一个结论:平时我们在访问某网址时实际是不需要指明端口号的,所以在URL当中,服务器的端口号一般也是被省略的。

带层次的文件路径

我们平时上网,只会有两种可能:1、我想获取资源。2、我想上传资源。(一张图片、一段视频或者一段文字,这些内容都可以被称为资源)

比如说我们想要获取存储在服务器(即一台主机)上的一张图片资源,想让服务器给我发过来时,如果客户端在访问服务端时不指明想获取的图片资源的路径,那么服务端进程就晕了,说【我这里的图片资源是有很多的,你想要哪一个呢?】所以客户端想要获取某资源时就需要指明资源所在的路径,当服务端进程在该路径上找到指定的图片资源后,就open打开这个图片文件,并将图片文件写进内存,进而再发送到网络中由客户端接收。

所以综上可知:在文章开头那张图中的 /dir/index. htm 表示的是要访问的资源所在的路径(说一下,第一个/不是根目录,只是web根目录)。访问服务器的目的是获取服务器上的某种资源,通过前面的域名和端口已经能够找到对应的主机中的对应的服务端进程了,此时要做的就是指明该资源所在的路径。此外我们可以看到,这里的路径分隔符是 /,而不是\,这也就证明了实际很多服务都是部署在Linux上的。

查询字符串

如文章开头那张图所示,如果一个URL中出现了字符 ?,则该字符 ?后面的都是URL的参数,uid = 1表示的是请求时提供的额外的参数,这些参数是以键值对的形式(uid是key,1是value),通过&符号分隔开的。再举个例子,如下:

  • 比如我们在浏览器上面搜百度,此时可以看到URL中有很多参数,而在这众多的参数当中有一个参数wd(word),表示的就是我们搜索时的搜索关键字wd = 百度。从这里也能看出双方在进行网络通信时,客户端是能够通过URL将用户的数据传输给服务端的,服务端会收到夹在URL中的参数。6b5a33279b034bb989e07c44a539965b.png

UrlEncode(编码)和UrlDecode(解码)

如果在搜索时搜索框中出现了像 / ?: 这样的字符,由于这些字符已经被URL当作特殊意义理解了,因此URL在呈现时会对这些特殊字符进行转义。

转义的规则:将需要转码的字符的阿斯克码转为十六进制,如果十六进制的数字大于了2位,则从低位到高位取2位,前面加上%,编码成%XY格式;如果少于两位,则从高位补0,最后在前面加上%,编码成%XY格式。

根据规则,如下图在搜索c++时,因为+也被URL当成特殊字符,所以会将+的阿斯克码转化成16进制数0x2B,所以wd显示c++时显示出的就是wd = c%2B%2B

00ba656657314fe7a2baee76d084ef51.png

这里分享一个在线编码工具:

选中其中的URL编码/解码模式,在输入文本C++后点击编码就能得到编码后的结果;再点击解码就能得到原来输入的C++。

f418a9ae05924133a7ea71c66807f143.png

实际当服务器拿到对应的URL后,也需要对编码后的参数进行解码,此时服务器才能拿到你想要传递的参数,解码实际就是编码的逆过程。 

见一见http协议的格式(包含如何编码构建一个像网站一样的可以提供服务的服务器)

http请求的格式

HTTP请求协议格式如下图所示。

7555dfac5b4549879121ceaff1c45936.png

HTTP请求由以下四部分组成:

  • 请求行:请求方法(一个空格)url (一个空格) http版本
  • 请求报头:请求的属性,这些属性都是以key: value的形式按行陈列的。
  • 空行:遇到空行表示请求报头结束。
  • 请求正文:请求正文允许为空字符串,如果请求正文存在,则在请求报头中会有一个Content-Length属性来标识请求正文的长度。
  • 其中,前面三部分是一般是HTTP协议自带的,是由HTTP协议自行设置的,而请求正文一般是用户的相关信息或数据,如果用户在请求时没有信息要上传给服务器,此时请求正文就为空字符串。

如何将HTTP请求的报头与有效载荷进行分离?

当应用层收到一个HTTP请求时,它必须想办法将HTTP的报头与有效载荷进行分离。对于HTTP请求来讲,这里的请求行和请求报头就是HTTP的报头信息,而这里的请求正文实际就是HTTP的有效载荷。

我们可以根据HTTP请求当中的空行来进行分离,当服务器收到一个HTTP请求后,就可以按行进行读取,如果读取到空行则说明已经将报头读取完毕,实际HTTP请求当中的空行就是用来分离报头和有效载荷的。

如果将HTTP请求想象成一个大的线性结构,此时每行的内容都是用\n隔开的,因此在读取过程中,如果连续读取到了两个\n,就说明已经将报头读取完毕了,后面剩下的就是有效载荷了。

怎么证明是上图这样的呢? 

咱们设计一个简单的http服务器,业务就是将浏览器发来的http请求cout打印出来,看看其格式是否符合上图的情况即可证明(由于我们的服务器是直接用TCP套接字接口读取浏览器发来的HTTP请求,此时在服务端没有应用层对这个HTTP请求进行过任何解析,因此我们可以直接将浏览器发来的HTTP请求进行打印输出,此时就能看到HTTP请求的基本构成)。因为代码比较简单,这里直接上代码。

HttpServer类的代码如下。(以下是http_server.h的整体代码)

#include<iostream>
using namespace std;
#include <unistd.h>//提供close和fork
#include <signal.h>//提供signal
#include<functional>//提供包装器function
//以下四个文件被称为网络四件套,包含后绝大多数网络接口就能使用了
#include<sys/types.h>//系统库,提供socket编程需要的相关的接口
#include<sys/socket.h>//系统库,提供socket编程需要的相关的接口
#include<netinet/in.h>//提供sockaddr_in结构体
#include<arpa/inet.h>//提供sockaddr_in结构体


class HttpServer
{
    using func_t = function<void(int)>;//需要参数int是因为要给子进程传服务套接字文件的描述符
public:
    HttpServer(uint16_t port, func_t f)
        :_port(port)
        ,_f(f)
    {
        _listen_sock = socket(AF_INET, SOCK_STREAM, 0);
        sockaddr_in local;
        local.sin_port = htons(_port);
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY;
        if(bind(_listen_sock, (sockaddr*)&local, sizeof(local)) != 0)
        {
            cout<<"http_server绑定失败"<<endl;
            exit(1);
        }
        if(listen(_listen_sock, 20) == -1)
        {
            cout<<"http_server监听失败"<<endl;
            exit(1);
        }
        cout<<"服务端初始化成功"<<endl;
    }

    ~HttpServer()
    {
        if(_listen_sock > 0)
            close(_listen_sock);
    }

    void start()
    {
        signal(SIGCHLD, SIG_IGN);
        while(1)
        {
            sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int service_sock = accept(_listen_sock, (sockaddr*)&peer, &len);
            cout<<"服务端accept建立连接成功"<<endl;
            if(fork() == 0)//子进程走这里
            {
                _f(service_sock);
                exit(0);
            }
            //父进程走这里
            close(service_sock);
        }
    }

private:
    int _listen_sock;
    uint16_t _port;
    func_t _f;
};

服务端的代码如下。(以下是整个http_server.cc的代码)

说一下,下面的recv是有问题的,没有通过协议保证接收到的数据一定是完整的,但因为接收到的数据不完整的概率太低了,并且因为这里的业务比较简单,只是为了将客户端发来的http请求的格式cout打印出来,即使接收到的数据不完整也没关系,所以在下面这样使用recv也没问题。

#include"http_server.h"
#include<memory>


// ./http_server port
void usage(char* c)
{
    cout<<"usage:"<<c<<" port"<<endl;
}

void httpfunc(int sockfd)
{
    char buffer[10240];
    ssize_t s = recv(sockfd, buffer, sizeof(buffer)-1, 0);
    if(s > 0)
    {
        buffer[s] = 0;
        cout<<buffer<<"————————————分割线—————————————————"<<endl;
    }
}

// ./http_server port
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }

    unique_ptr<HttpServer>ptr(new HttpServer(atoi(argv[1]), httpfunc));
    ptr->start();
    return 0;

}




编译并运行http_server.cc程序后,然后用浏览器访问它,此时我们的服务器就会收到浏览器发来的HTTP请求,此时对于浏览器来说,因为目前程序员没有在服务端的代码中编写向发起http请求的一方通过send或者write函数发送响应信息的逻辑,所以浏览器就什么信息也收不到,所以浏览器的表现如下图1;而此时对于服务端进程来说,根据我们服务端进程的编码逻辑,服务端就会将收到的HTTP请求进行打印输出,如下图2所示,说明一下图2:

  • 浏览器向我们的服务器发起HTTP请求后,因为我们的服务器没有对进行响应,此时浏览器就会认为服务器没有收到,然后再不断发起新的HTTP请求,因此虽然我们只用浏览器访问了一次,但会收到多次HTTP请求。
  • 由于浏览器发起请求时默认用的就是HTTP协议,因此我们在浏览器的url框当中输入网址时可以不用指明HTTP协议。url当中的第一个 / 不能称之为我们云服务器上根目录,这个 / 表示的是web根目录,这个web根目录可以是你的机器上的任何一个目录,这个是可以自己指定的,不一定就是Linux的根目录,但注意如果程序员不编码进行干涉,那么这里url中的 / 就是Linux的根目录(会在下文中证明)。更详细的内容以及如何指定会在下文讲解http响应的格式的部分中进一步说明。
  • 浏览器发出的http请求中的请求行部分是不携带服务器的ip以及端口号的,ip和端口号会在http请求的请求报头部分中的Host字段进行指明,请求行当中的url表示你要访问这个服务器上的哪一路径下的资源。如果浏览器在访问服务器时指明要访问的资源路径,那么浏览器发起的HTTP请求中的请求行部分的url就是该路径;如果没有指明任何路径,那么浏览器发起的HTTP请求中的请求行部分的url就是默认路径 /,这个 / 也只表示web根目录,而不一定是Linux的根目录。(说一下,并不是说HTTP请求中的请求行部分中的url有一个路径,http服务器就会自动把这个路径的资源发给客户端,这是需要程序员编码才能做到的,浏览器发给服务端的http请求只是单纯的一长串C语言字符串而已,要想让服务端能够把浏览器发送的http请求中的指定路径上的资源发给浏览器,就需要服务端在调用recv或者read读取到浏览器发来的http请求这一长串C语言字符串后,把里面的指定路径这一子串给截取出来,然后fopen(字串)打开对应文件,然后fgets读取文件的内容读到一个创建的字符数组中,然后通过一个string对象+=这个字符数组,通过这样不断循环直到文件的内容被读完后,然后将string中的内容通过调用c_str()函数一次交给send函数以发送给浏览器,这样浏览器才能得到自己指定的路径中的资源;如果服务端的代码里没有这些逻辑,那么无论在浏览器的搜索框中输入什么奇奇怪怪的路径,服务端在收到http请求后也会什么不做,因为在服务端看来这也不过是一些字符串而已,程序员没有通过编码让我处理它,那我就不理会它即可。)
  • 浏览器发起的http请求中的请求报头部分中全部都是以key: value形式按行陈列的各种请求属性,请求属性陈列完后紧接着的就是一个空行,空行后的就是本次HTTP请求的请求正文,此时请求正文为空字符串,因此这里有两个空行。

图1如下。

0e6796050ff14147af30f17697a2d2ca.png

图2如下。

7145c75242d0415ab1336c7d3df82548.png

综上可以看到,服务器实际收到的http请求的格式和我们最开始给出的图中的格式是一致的,也就证明了http请求的格式就是最开始给出的图中的格式。

http响应的格式(包含web根目录的知识点)

HTTP响应协议格式如下图所示。

4fb258976df04d71b42023751914b6f4.png

HTTP响应由以下四部分组成:

  • 状态行:http版本(一个空格)状态码(一个空格)状态码描述
  • 响应报头:响应的属性,这些属性都是以key: value的形式按行陈列的。
  • 空行:遇到空行表示响应报头结束。
  • 响应正文:响应正文允许为空字符串,如果响应正文存在,则响应报头中会有一个Content-Length属性来标识响应正文的长度。比如服务器返回了一个html页面,那么这个html页面的内容就是在响应正文当中的。

如何将HTTP响应的报头与有效载荷进行分离?

对于HTTP响应来讲,这里的状态行和响应报头就是HTTP的报头信息,而这里的响应正文实际就是HTTP的有效载荷。与HTTP请求相同,当应用层收到一个HTTP响应时,也是根据HTTP响应当中的空行来分离报头和有效载荷的。当客户端收到一个HTTP响应后,就可以按行进行读取,如果读取到空行则说明报头已经读取完毕。

怎么证明是上图这样的呢? 会在下文中证明。

问题:在上文讲解http请求的格式时说过关于web根目录更详细的内容以及如何指定web根目录会在讲解http响应的格式时说明,而现在就是在讲http响应的格式,所以这里咱们就好好说一下这些内容。咱们选择最直观的方式、即通过编写代码来感受一下什么是web根目录以及如何指定web根目录,如何编写呢?

答案:首先上文中说过,并不是说浏览器发给服务端的HTTP请求中的请求行部分中的url有一个路径,http服务器就会自动把这个路径的资源发给客户端,这是需要程序员编码才能做到的,浏览器发给服务端的http请求只是单纯的一长串C语言字符串而已,要想让服务端能够把浏览器发送的http请求中的指定路径上的资源发给浏览器,就需要服务端在调用recv或者read读取到浏览器发来的http请求这一长串C语言字符串后,把里面的指定路径这一子串给截取出来,然后fopen(字串)打开对应文件,然后fgets读取文件的内容读到一个创建的字符数组中,然后通过一个string对象+=这个字符数组,通过这样不断循环直到文件的内容被读完后,然后将string中的内容通过调用c_str()函数一次交给send函数以发送给浏览器,这样浏览器才能得到自己指定的路径中的资源;如果服务端的代码里没有这些逻辑,那么无论在浏览器的搜索框中输入什么奇奇怪怪的路径,服务端在收到http请求后也会什么都不做,因为在服务端看来这也不过是一些字符串而已,程序员没有通过编码让我处理它,那我就不理会它即可。咱们这里就通过代码复现这一思路,并在这个过程中把什么是web根目录以及如何指定web根目录给说清楚。

想要复现上一段中的思路,首先创建一个工具类Util,然后在里面实现一个将整个字符串划分成若干个子串并将每个子串存进vector的函数cutString,以此能让程序员拿到夹在http请求中的url,进而才能让服务端把浏览器想要的资源给浏览器发过去。

代码如下(以下是整个Util.h的代码)。第一个参数是表示需要将哪个字符串划分成若干个子串,第二个参数表示在将完整字符串划分成若干子串时以什么为依据划分,第三个参数是一个输出型参数,我们将划分出的字串全放进v中。

#include<iostream>
using namespace std;
#include<vector>
#include<string>


class Util
{
public:
    //asdasd\nasdasdsad\n
    void cutString(const string& str, const string& sep, vector<string>& v)
    {
        int start=0;
        while(start < str.size())
        {
            size_t pos = str.find(sep,start);
            if(pos == string::npos)
                break;
            v.push_back(str.substr(start,pos-start));
            start = pos;
            start += sep.size();
        }
        //当数据是最后一行、没有\n时,比如为asdada,那么上面的逻辑就无法处理这行数据,需要在if分支中处理这行数据
        if(start < str.size())
        {
            v.push_back(str.substr(start));
        }
    }
};

工具类Util和成员函数cutString被编写完毕后,就需要使用它们达到我们的目的了,即拿到夹在浏览器发来的http请求中的浏览器想要获取的资源的路径url。

方式很简单,我们先把在讲解【http请求的格式】时编写的的http_server.cc的代码拿过来,然后往里增加一些内容即可达成我们的目的。新增内容的逻辑是把recv收到的浏览器发过来的http请求(本质就是C语言字符串)里的内容通过上面编写的cutString函数按照\n划分成若干个子串存进v1中,然后把v1中的第一个子串(即对应http请求的格式图中的请求行的子串)再当作一个整体,按照空格划分成若干子串存进v2中,这样一来v2的第一个元素就是http请求的请求方法,第二个元素就是服务端进程需要知道的浏览器想要访问的资源的路径了。

新增内容后,http_server.cc的代码如下所示。

#include"http_server.h"
#include"Util.h"
#include<memory>


// ./http_server port
void usage(char* c)
{
    cout<<"usage:"<<c<<" port"<<endl;
}

void httpfunc(int sockfd)
{
    char buffer[10240];
    ssize_t s = recv(sockfd, buffer, sizeof(buffer)-1, 0);
    cout<<s<<endl;
    if(s > 0)
    {
        buffer[s] = 0;
        cout<<buffer<<"————————————分割线—————————————————"<<endl;
    }
    Util u;
    vector<string> v1;
    u.cutString(buffer, "\n", v1);
    for(auto e: v1)
    {
        cout<<"--切分--"<<e<<endl;
    }
    vector<string> v2;
    u.cutString(v1[0], " ", v2);
    for(auto e: v2)
    {
        cout<<e<<endl;
    }
}

// ./http_server port
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }

    unique_ptr<HttpServer>ptr(new HttpServer(atoi(argv[1]), httpfunc));
    ptr->start();
    return 0;

}




咱们编译并运行一下http_server.cc,目的是检查一下上面Util类中的cutString成员函数是否具备我们想要功能。如何检测呢?

先将服务端进程启动,然后如下图1这样,通过浏览器访问服务器中指定路径下的资源。浏览器进行访问后,在服务端进程中就会收到如下图2的区域1这样的http请求,然后按照咱们服务端的编码逻辑,就会将这一整个http请求按照\n划分成若干个子串,如下图2的区域2,对比区域1和区域2的内容,可以发现的确实现了按行划分的功能,但我们的目的是拿到夹在http请求中的浏览器想要访问的资源的路径,按照咱们服务端的编码逻辑,此时就会将http请求中的第一行,也就是请求行再当成一个整体,按照空格划分成若干个子串,如下图2的区域3,对比区域3和区域4的内容可以发现的确实现了按空格划分的功能。通过这两次cutString函数的调用,也就证明了cutString的编码是没有问题的,能完成将整个字符串划分成若干个子串并将每个子串存进vector的任务,以此也就能让程序员拿到夹在http请求中的url。

图1如下。

9db3065e2a4b48bfb00e34118116146c.png

图2如下。 

a97001506fb24723b55b80fcf23359f1.png

走到这里我们已经能轻而易举的拿到浏览器想要访问的资源的路径了,接下来我们就随便创建一个文件让浏览器访问,如下图的httptest.txt文件。ca0c470224784d8c80279b432f496cec.png

走到这里,所有的准备工作已经完毕,剩下的工作就简单了,思路为:

  • 在http_server.cc中先通过Util类的成员函数cutString拿到从http请求的请求行部分中划分出来的路径。
  • 然后fopen以r模式打开这个划分出来的路径上的文件(注意不能以w模式打开,否则OS发现该文件不存在时会自动创建;OS发现该文件存在时还会清空。这都不合理),如果不存在该文件,直接按照最开始在标题附近就给出的http响应的格式图创建一个表示找不到目标的http响应字符串(如果不按照格式创建响应字符串,会发生错误,在下文中会证明这一点),然后将它通过send函数发给浏览器即可;如果存在该文件,则按照最开始在标题附近就给出的http响应的格式图创建一个表示获取资源成功的http响应字符串,然后读取文件的内容,读取完毕后,最后将响应字符串和读取到的内容合并在一起发给浏览器即可。

根据上面的思路,将http_server.cc的代码修改成如下样子。

#include"http_server.h"
#include"Util.h"
#include<memory>
#include<string.h>

// ./http_server port
void usage(char* c)
{
    cout<<"usage:"<<c<<" port"<<endl;
}

void httpfunc(int sockfd)
{
    char buffer1[10240];
    ssize_t s = recv(sockfd, buffer1, sizeof(buffer1)-1, 0);
    Util u;
    vector<string> v1;
    u.cutString(buffer1, "\n", v1);
    vector<string> v2;
    u.cutString(v1[0], " ", v2);
    cout<<"浏览器要访问的资源的路径是:"<<v2[1]<<endl;

    string target = v2[1];

    char buffer2[1024];
    memset(buffer2, 0 ,sizeof(buffer2));
    FILE* p = fopen(target.c_str(), "r+");
    string response;
    if(p == nullptr)
    {
        response = "http/1.1 404 NotFound\n";
        response += "\n";
        send(sockfd, response.c_str(), response.size(), 0);
    }
    else
    {
        response = "http/1.1 200 OK\n";
        response += "\n";
        while(1)
        {
            char* c = fgets(buffer2, sizeof(1024), p);
            response += buffer2;
            if(c == nullptr)
                break;
            memset(buffer2, 0 ,sizeof(buffer2));
        }
        send(sockfd, response.c_str(), response.size(), 0);
    }
}

// ./http_server port
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }

    unique_ptr<HttpServer>ptr(new HttpServer(atoi(argv[1]), httpfunc));
    ptr->start();
    return 0;
}




将刚编写好的服务端进程http_server.cc编译并运行后,浏览器就可以访问服务端了。在上面说过,如果程序员不编码进行干涉,那么浏览器发给服务端的http请求的请求行部分中的url中的第一个 / 就表示Linux的根目录。怎么证明呢?如下。

  • 到这里我们还没有编写任何与web根目录有关的代码,即没有进行干涉,所以从理论上说第一个 \ 就表示Linux的根目录,那么浏览器想要访问到httptest.txt文件,就得在浏览器的搜索框中输入从Linux的根目录到httptest文件的绝对路径,这样服务端进程http_server.cc中的fopen函数才能通过这个绝对路径查找并打开httptest.txt文件。如下图,输入后发现的确能够成功访问,这就证明了如果程序员不编码进行干涉,那么浏览器发给服务端的http请求的请求行部分中的url中的第一个 / 就表示Linux的根目录。(说一下,实际上稍微仔细看过http_server.cc的代码的小伙伴们就知道,这里的证明是有点多此一举的,因为代码的逻辑就是把http请求中的代表浏览器要访问的资源的路径截取出来并通过这个路径fopen打开这个文件,然后再读取内容发给浏览器,所以当然第一个 \ 就是Linux的根目录了)c393d785364c4189a6f9a7abf17b5878.png

那程序员怎样编码进行干涉,就能让浏览器发给服务端的http请求的请求行部分中的url中的第一个 / 不表示Linux的根目录,而是web根目录呢?

举个例子,比如说我想让第一个 / 表示我服务端进程http_server.cc所在的目录(该目录不是Linux的根目录),换句话说就是如果我想把服务端进程http_server.cc所在的目录设置成web根目录,那么只需要在上面http_server.cc代码的基础上稍作修改,如下对比图所示,只需要将左边红框处的部分改成右边红框处的部分即可达成目的。这是什么原理呢?如下。

  • 既然服务端http_server.cc的代码的逻辑就是把http请求中的代表浏览器要访问的资源的路径截取出来并通过这个路径fopen打开这个文件,然后再读取内容发给浏览器。那么我想要让服务端进程http_server.cc所在的目录变成web根目录只需要把截取出来的路径拼凑在服务端进程http_server.cc所在的目录的路径的后面,然后让fopen时不要直接fopen截取出的路径,而是fopen我拼接的路径即可,这样一来,比如你在浏览器的搜索框中输入/a/b/c,这个内容到达服务端进程经过我程序员的干涉后,fopen看见的内容就变成了  ./a/b/c, . 就是fopen函数所在的进程http_server.cc所在的目录,这样一来,在浏览器的搜索框中输入的第一个 / 就只表示服务端进程http_server.cc所在的目录,也就是web根目录了,换句话说我们也就把服务端进程http_server.cc所在的目录设置成web根目录了。

442dd0baebd04e4a996eead30163289d.png

修改过的http_server.cc的代码如下(即位于上图右半部分的http_server.cc的代码如下)。 

#include"http_server.h"
#include"Util.h"
#include<memory>
#include<string.h>

#define Root "."

// ./http_server port
void usage(char* c)
{
    cout<<"usage:"<<c<<" port"<<endl;
}

void httpfunc(int sockfd)
{
    char buffer1[10240];
    ssize_t s = recv(sockfd, buffer1, sizeof(buffer1)-1, 0);
    Util u;
    vector<string> v1;
    u.cutString(buffer1, "\n", v1);
    vector<string> v2;
    u.cutString(v1[0], " ", v2);
    cout<<"浏览器要访问的资源的路径是:"<<v2[1]<<endl;

    string target = Root;
    target += v2[1];

    char buffer2[1024];
    memset(buffer2, 0 ,sizeof(buffer2));
    FILE* p = fopen(target.c_str(), "r+");
    string response;
    if(p == nullptr)
    {
        response = "http/1.1 404 NotFound\n";
        response += "\n";
        send(sockfd, response.c_str(), response.size(), 0);
    }
    else
    {
        response = "http/1.1 200 OK\n";
        response += "\n";
        while(1)
        {
            char* c = fgets(buffer2, sizeof(1024), p);
            response += buffer2;
            if(c == nullptr)
                break;
            memset(buffer2, 0 ,sizeof(buffer2));
        }
        send(sockfd, response.c_str(), response.size(), 0);
    }
}

// ./http_server port
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }

    unique_ptr<HttpServer>ptr(new HttpServer(atoi(argv[1]), httpfunc));
    ptr->start();
    return 0;
}

将上面的http_server.cc的代码编译并运行后,浏览器就可以访问服务端了。因为此时的服务端进程httpserver.cc已经将自己所在的目录文件设置成了web根目录,而httptest.txt文件和httpserver.cc文件是在同一个目录文件中,所以此时httptest.txt文件和httpserver.cc文件所在的目录文件就都是web根目录,所以浏览器访问httptest.txt文件时也就只需要在搜索框中输入/httptest.txt了,如下图所示,可以发现是能成功访问的。

3ba74032a7644aa8868b2d84e5cab121.png

到这里还有一点不完美,在上文中说过,如果浏览器在访问服务器时不指明任何路径(即搜索框中只有ip和端口),那么浏览器发起的HTTP请求中的请求行部分的url就是默认路径 / ,那么根据上面的理论,经过程序员的编码干涉,此时服务端进程httpserver.cc中的fopen函数看见的路径就是 ./,也就是让fopen打开httpserver.cc所在的目录文件,httpserver.cc后序代码的逻辑甚至还要读取这个目录文件的资源并将数据发给浏览器,注意这都是不行的,因为fopen、fgets、fwrite、fread这些接口都只能操作普通文件,而不能操作目录文件,所以如果浏览器在访问服务器时不指明任何路径(即搜索框中只有ip和端口),那浏览器就会变成下图这样。

0484665a24414a6b80ab3b480e784e7a.png

那该怎么办呢?因为根据我们上网的经验,可以发现如下图的百度的路径,它也是只有ip和端口号(baidu.com就是ip,百度所用的应用层协议也是HTTP,所以如果不显示指定端口号,浏览器就默认访问80号端口,因为能访问成功,所以百度的端口号就是80,这里只是隐藏了这个80,而不代表没有),但却能访问到一个页面,这和我们的服务端进程发生的情况截然不同,是怎么做到的呢?

很简单,我们的服务端进程也可以这样,只需要在http_server.cc所在的目录中创建一个表示首页的文件(一般来说是方便浏览器解释的html文件,这里咱们就先用txt文件),如下图1,然后增加一个if else判断即可,如下图2所示,只需要将下图2的左边红框处的部分改成右边红框处的部分即可达成目的。原理也很简单,既然浏览器在访问服务器时不指明任何路径(即搜索框中只有ip和端口)就会让浏览器发起的HTTP请求中的请求行部分的url变成默认路径 / ,那我就在服务端进程http_server.cc截取出HTTP请求中的路径后进行判断,如果发现是 / ,则就不要让web根目录(即当前路径 . )只去和 / 组成最后需要被fopen打开的路径了,而是先让web根目录(即当前路径 . )去和 / 进行拼接,然后再和我们#define出来的常量C语言字符串“page.txt”进行拼接,最后需要被fopen打开的路径就变成了 ./page.txt 。

  • 图1如下。41e712fc50aa493c9d1f530831b4dc64.png
  • 图2如下。db8ed778a1004204aa2f38abbc94c04a.png

修改过的http_server.cc的代码如下(即位于上图右半部分的http_server.cc的代码如下)。

#include"http_server.h"
#include"Util.h"
#include<memory>
#include<string.h>

#define Root "."
#define HomePage "page.txt"

// ./http_server port
void usage(char* c)
{
    cout<<"usage:"<<c<<" port"<<endl;
}

void httpfunc(int sockfd)
{
    char buffer1[10240];
    ssize_t s = recv(sockfd, buffer1, sizeof(buffer1)-1, 0);
    Util u;
    vector<string> v1;
    u.cutString(buffer1, "\n", v1);
    vector<string> v2;
    u.cutString(v1[0], " ", v2);
    cout<<"浏览器要访问的资源的路径是:"<<v2[1]<<endl;

    string target = Root;
    if (v2[1] == "/")
    {
        target += v2[1];
        target += HomePage;        
    }
    else
    {
        target += v2[1];
    }
    
    char buffer2[1024];
    memset(buffer2, 0 ,sizeof(buffer2));
    FILE* p = fopen(target.c_str(), "r+");
    string response;
    if(p == nullptr)
    {
        response = "http/1.1 404 NotFound\n";
        response += "\n";
        send(sockfd, response.c_str(), response.size(), 0);
    }
    else
    {
        response = "http/1.1 200 OK\n";
        response += "\n";
        while(1)
        {
            char* c = fgets(buffer2, sizeof(1024), p);
            response += buffer2;
            if(c == nullptr)
                break;
            memset(buffer2, 0 ,sizeof(buffer2));
        }
        send(sockfd, response.c_str(), response.size(), 0);
    }
}

// ./http_server port
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }

    unique_ptr<HttpServer>ptr(new HttpServer(atoi(argv[1]), httpfunc));
    ptr->start();
    return 0;
}

将上面的http_server.cc的代码编译并运行后,浏览器就可以访问服务端了。此时我们的服务端进程也和正常的服务器一样,浏览器在访问我们的服务端进程时,即使不带路径,也能成功的访问到一个首页页面了,如下图所示。

e0a958762fde4c2a99bb57b4f02c7905.png

(结合下图思考)走到这里,http_server.cc的代码就写完了。我们可以发现,这个服务器就已经相当于是建立了一个网站了,比如说我们可以在服务端进程http_server.cc所在的目录文件中创建一个目录文件wwwRoot和一个代表首页的homePage.html文件,然后将wwwRoot设置成web根目录,然后在wwwRoot目录文件中创建各种普通文件或者目录文件,对于这些【wwwRoot目录中的普通文件】和【wwwRoot目录中的其他子目录文件中的普通文件】,它们可能是html文件,也可能是txt文件或者音视频文件,这些都是可以让用户通过在浏览器的搜索框中输入 ip:port/路径访问的位于我们服务器上的资源,如果浏览器访问服务器时不指定资源,则将代表首页的homePage.html文件发给浏览器即可。可以看到,这和我们平时上网时看到的一个网站的工作模式是一样的,所以才说我们写的服务器就已经相当于是建立了一个网站了。其中前端工作者就将自己写的前端页面文件放到wwwRoot中,后端工作者就负责wwwRoot目录文件外的内容。

78160a28b17947a9acb7201a5def0db0.png

说一下,在上文中讲解构建响应字符串时,我们说过如果不按照最开始在标题附近就给出的http响应的格式图创建,则会发生错误,走到这里咱们已经能够让浏览器访问服务器上的指定资源了,所以现在就有了很好的实验条件,经过实验发现,如果把下图1的两个红框处的代码部分删除或者是全部删除又或者是修改得极其不符合最开始在标题附近就给出的http响应的格式图,也就是如果不按照格式创建http响应字符串(说一下,构建http响应时是可以不带响应报头部分的,但其他内容一定不能少,否则浏览器就会变得和下图2一样或者是变成全白什么也看不见),则不管需要fopen打开并访问的文件存不存在,此时浏览器都会变得和下图2一样或者是变成全白什么也看不见,这也就证明了http响应的格式就是最开始在标题附近就给出的http响应的格式图那样(如果感觉这样的解释有点牵强的话,下文中讲解http的GET方法时会给出更有说服力的证明)。其中被我们fopen打开并fgets读取数据的文件,其文件的内容就被我们通过编码(比如下图的response += buffer2)拼凑在了http响应的最后,它就叫做http响应的响应正文。

  • 图1如下。2975e9421eab47ea9eef27f989f5aa6f.png
  • 图2如下 0dcdfca2304a4c56bd5f9bb20dcbf265.png

http请求中的请求方法字段

e02ed313ac79461588c544305b77bded.png

平时我们上网的行为就两种:1、将数据提交到服务端上。2、将数据从服务端拿来。所以上图中常用的方法也就只有两种,分别是POST(对应行为1)和GET(对应行为2)。说一下GET除了获取数据,也是能用于提交数据的,比如百度提交数据时实际使用的就是GET方法,所以GET也能对应行为1。

上图中的方法,除了GET和POST,其他的方法要么被服务端禁用,要么不支持,因为服务器的资源是直接推送给客户的,直接面对客户的服务器在需要支持自己理应支持的最基本的服务外,还要尽量的避免无用服务的暴露,进而降低服务器出现漏洞的可能性,毕竟客户是有善良和邪恶之分的,就是有人会恶意利用漏洞攻击服务器,所以结合这些因素考虑,通常服务器对于HTTP方法的态度是能禁用就禁用,所以虽然HTTP的方法如上图一样有很多,但我们并不一定能使用。

先介绍一下用于获取资源的GET方法

3b1f24bef0294c45ae35d30b614daed4.png

(结合上图思考)通过telnet baidu.com 80 远程连接百度这台服务器后,我们可以通过GET方法访问服务器(或者说获取服务器的资源),即可以通过GET / HTTP/1.1获取到百度的首页(根据上文编写http_server.cc时的经验,可知在访问服务器时只有一个 / 时,此时 / 不应该拼凑在web根目录的后面,因为web根目录/ 这个路径还是web根目录,fopen是无法操作目录文件的,而应该是在发现截取到的路径是 / 的时候就直接把首页文件的内容发给客户端,所以GET / HTTP/1.1中的 / 也理应表示首页),上图中的响应正文也就是表示百度首页的html文件。

因为如上图所示,收到的http响应是符合在上文中标题为http响应的格式的部分中给出的格式图的,所以从这里就可以证明http响应的格式就是在上文中给出的图那样。

问题:说一下,如下图是在浏览器中查看百度网页的源代码时截取出来的(下图只是代码的一部分),可以发现它是很工整的,是一行一行的,而我们上图收到的http响应的响应正文部分也是百度的源代码,为什么代码是全挤在一起的呢?即为什么没有换行和回车?

答案:换行符\n和回车\r本身也是字符,为了减少网络传输的成本,所以百度在发送http响应时没有带上这些符号,浏览器中做了换行回车那是因为浏览器在收到百度这台服务器的http响应后,把响应正文拿到后浏览器自己做了处理,而不是说http响应的响应正文中本来就有这些东西。2fb984a1f83e41c6bc4cf350c2c12508.png

然后再介绍一下用于提交资源的POST方法

在上文中说过GET方法和POST方法都可以用于提交数据。说一下,POST或者GET方法想要提交数据给服务器,都是需要依托于网页的表单的,表单的作用就是在浏览器的网页中提供若干个输入框和提交按钮,等用户把数据填入表单(即等用户把数据填入表单提供的输入框)并点击提交按钮后,表单就会被浏览器提交给服务器以此把数据推送给服务器(本质是表单中的数据会被转化成http请求的一部分),而表单在被提交时是需要指明提交方法的,最常见的方法就是这里的POST方法和GET方法。

  • 问题:为什么POST和GET方法想要提交数据给服务器需要依托于网页的表单呢?答案:这么做是考虑到作为不懂编程的普通用户而言,让他们去构建http请求肯定是不可能的,所以就提供了表单这种方式,等用户在输入框中输入数据后,表单中的数据就自动被浏览器转化成http请求的一部分,然后浏览器再把完整的http请求发给服务器。

因为回答下面这个问题所需的板块太大,所以写个开始和结束的标记用于分隔不同板块。本板块从这里开始

问题:既然上文中说过GET方法和POST方法都可以用于提交数据,那这两种方法在提交数据时有什么区别呢?

答案:会在下面逐步揭晓。

把上文代码中的代表首页的page.txt给删除后,先编写红框处的homePage.html文件,说明一下该文件:

  • 注意当前表单中填写的提交表单的方式是GET,如下图1的method字段是GET,html是大小写不敏感的,所以下图1中是小写get。
  • 表单中的参数 action字段填的是/a/b/haha.html,表示将来在输入框中输入账号密码并点击登录后是要访问服务器上路径为/a/b/haha.html的资源,这是一个胡乱填的路径,服务器上在这个路径上没有资源,所以到时候浏览器访问时肯定是访问不到的,这个对于完成我们的目的来说不重要,不必关心。

然后把下图1的http_server.cc中的#define处从 #define HomePage “page.txt” 变成 #define HomePage “HomePage.html”,然后添加2行cout语句用于打印别人给我发来的http请求,然后重新编译并在8080号端口运行服务端进程http_server.cc。

然后通过在浏览器中输入【服务端进程的公网ip:8080】访问服务端进程就会如下图2所示,输入账号密码并点击登录后就会如下图3的下半部分所示,观察下图3的下半部分的【搜索框中的路径】和【服务端进程收到的http请求的请求行部分中的路径】,即http://8.130.96.195:8080/a/b/haha.html?user=zhang+san&pwd=zs1234567890,其中位于 ?后面的内容就是咱们通过GET方法提交给服务端进程的数据,可以发现咱们通过GET方法提交给服务端进程的数据竟然在搜索框中和服务端进程收到的http请求的请求行部分中回显出来了,从这里就可以发现GET方法最大的特性就是GET方法会把需要提交给服务器的数据放在路径url的后面,通过这样的方式把数据提交给服务器。

然后把homePage.html文件中的表单中填写的提交表单的方式从GET改成POST,如下图4的method字段是POST(html是大小写不敏感的,所以下图4中是小写post),其他内容不用变。然后重新访问服务器的首页,重新输入账号密码并点击登录后就会如下图5的下半部分所示,观察下图5的下半部分的【搜索框中的路径】和【服务端进程收到的http请求的请求行部分中的路径】,可以发现和使用GET方法提交数据时不一样,这两个位置的路径中都已经不再包含咱们通过POST方法提交给服务端进程的数据(即在输入框中输入的账号密码数据)了,通过POST方法提交给服务器的数据只会被放在http请求的请求正文部分,也就是说POST方法最大的特性就是POST方法会把需要提交给服务器的数据放在浏览器发起的http请求的请求正文部分的后面,通过这样的方式把数据提交给服务器。

  • 图1如下。1128b9d3f6c0451dad0c1ad1c2c94bc5.png
  • 图2如下。9297fab24b0043dbb1185e2174d5bcb6.png
  • 图3如下。4701d0e529b34e81af5c9087b7c75066.png 
  • 图4如下。7868bab261a84572b0ece4778f2f4e58.png
  • 图5如下。c81a543db79548f4811a0d2c374d0fc1.png

到了出结论的时候了,咱们先梳理一下之前的内容。我们说GET方法既能用于获取数据也能用于提交数据,所以上文的流程是咱们先通过浏览器(本质是通过包含在浏览器发起的http请求中的GET方法)访问服务器,通过GET方法获取到一个表单(本质就是服务端进程http_server.cc给浏览器发来的一个html文件的内容),然后再通过表单把数据提交给服务器,提交的方式就是POST和GET。我们得到的关于【POST方法和GET方法都在被用于提交数据时的区别】的结论是:POST方法会把需要提交的数据放在http请求的请求正文后面;而GET方法会把需要提交的数据放在http请求的请求行部分中的路径后面,并将提交的数据回显到浏览器的搜索框中。所以主要的区别就是它们将需要提交的数据放在了不同的位置,衍生出来的区别就是GET方法不太私密,而POST方法是私密的(衍生出来的区别可能需要将下面的内容阅读一点后才能理解)。

说一下,因为GET方法会把提交的数据回显到浏览器的搜索框中,而提交的数据是用户输入进输入框里的,这个数据可能是私密信息,所以一般来说GET方法不够私密性,而POST方法就不存在这个问题,POST的私密性是有保证的,所以如果用户输入的数据不是很私密的内容,则可以使用GET方法提交数据:

  • 比如用户使用浏览器访问百度服务器进行搜索时,浏览器提交用户输入的数据给百度时就是通过GET方法提交,浏览器获取到的百度的首页html文件中的method字段就默认是GET,理解了上文就知道如下图在搜索时用户输入的数据被放在搜索框的路径后面就可以证明这一点;a0f9abb2a801430b9b5072e099a50248.png

而如果用户输入的数据很重要,则浏览器就要使用POST方法将数据提交给服务器,所以程序员在编写服务器的首页html文件时就要把其中的method字段设置成POST:

  • 比如通过浏览器注册或者登录时就需要浏览器使用POST方法提交数据给服务器,需要程序员在编写服务器的首页html文件时就要把其中的method字段设置成POST。

注意私密性并不代表安全性,因为私密性只是能让计算机小白看不到我们发送出去的数据,即便你使用POST方法,只要数据被发送进网络中时是明文发送,即没有经过加密解密,那么有心之人要是想看到我们发的数据,他是能看到的。

本版块到这里结束

http响应中的状态码字段

fc255c58093c44c8af5dedcdb2d59a20.png

http响应中的状态码字段如上图所示:

1、根据上面的表格可知当客户端收到的http响应中的状态码字段是1XX时,则表示客户端发起的http请求比较复杂,服务器需要一段时间进行处理,于是服务器先发一个http响应告诉客户端,我需要一会时间进行处理,你等一等吧。

2、根据上面的表格可知当客户端收到的http响应中的状态码字段是2XX时,比如是200时,则表示客户端发起的http请求被服务器处理成功了,客户端要的数据或者资源已经在http响应的响应正文中了。

3、根据上面的表格可知当客户端收到的http响应中的状态码字段是3XX时,则表示服务端希望客户端重定向访问位于服务器上的其他数据或者说资源。服务端希望客户端访问什么数据或者说资源呢?如下图所示,这个数据或者说资源的路径会被包含在服务端发给客户端的http响应中的响应报头部分的Location字段中,客户端在获取到这个路径后就会再次构建http请求发给服务器,访问这个路径上的资源。为什么会有重定向跳转这种需求呢?

  • 举个例子,比如说我公司刚刚发展时用的域名是gs1.com,并有100w人经常通过这个域名访问我公司的服务,但经过发展公司提供了很多新的服务,因为gs1.com这台服务器已经无法容纳这么多服务了,所以就把新旧所有的服务全部部署到gs2.com这台服务器上。注意此时是不能直接将旧的服务器gs1.com丢弃的,因为大量用户都不知道你公司的域名更换了,如果直接将旧的服务器gs1.com丢弃了,那么就会流失掉大量用户。问题来了,虽然gs1.com这台服务器还在用,但新旧所有服务全部在gs2.com这台服务器上,用户直接访问gs1.com是无法被服务的,这该怎么办呢?所以此时就需要进行重定向跳转,用户访问gs1.com时,就让gs1.com这台服务器给客户端发送状态码为3XX的http响应,并把gs2.com这个url路径包含在该http响应的响应正文中,让客户端重定向访问gs2.com,这样就能提供服务了。
  • 再举个例子,比如说我们看腾讯视频时,打开一个vip视频时会要求用户登录,那么就会跳转到登录页面,当用户登录后,此时就不应该再让页面跳转到主页,而应跳转到刚刚打开的vip视频上,怎么做到这一点呢?此时就需要服务端向客户端发送状态码为3XX的http响应并把希望让客户端访问的网页文件的路径放在这个http响应的响应报头部分的Location字段中,这样客户端就会重定向访问服务端希望让客户端访问的指定页面文件了。

cbfeda89a3364e83bdf7674609cfaa10.png

咱们再来通过编码演示一下重定向功能。先将演示GET方法和POST方法的区别时所用的http_server.cc以及homePage.html等等一系列代码拿过来,然后如下图所示只需在原来http_server.cc代码的基础上稍作修改即可完成重定向的功能,将下图左边红框处的代码改成右边红框处的代码即可。说明一下其含义:

  • 原来的代码,即下图左边的代码的红框处逻辑是,如果浏览器发给我服务端进程的路径在我服务器上不存在,则服务端进程给浏览器发送一个状态码字段为404的http响应,此时浏览器收到该响应后就知道是自己请求的资源不存在,也就不会再纠缠了;
  • 而修改后的代码,即下图右边的代码的红框处逻辑是,如果浏览器发给我服务端进程的路径在我服务器上不存在,则服务端进程给浏览器发送一个【状态码字段为301的、响应报头的Location字段为csdn首页路径的】http响应,此时浏览器收到该响应后就知道自己需要再次构建http请求去访问csdn首页。

e3f4d841cdd14289a9207d584642458e.png

服务端进程http_server.cc的代码修改完毕后,将其进行编译并在8080号端口运行它,然后就可以进行测试了,到底能不能完成重定向的功能呢?

如下图所示,在浏览器输入【服务器的ip:8080号端口】访问服务器的首页后,不必输入账号密码,直接点击登录即可(之前输入账号密码只是因为要观察通过POST方法和GET方法将数据提交给服务端进程的区别,而现在没有这个需求,所以直接登录即可),点击登录后,当前浏览器的页面就会从登录页面直接跳转到csdn的首页了,所以可以发现是能完成重定向的功能的(详细流程在下一段)。

7bb352b700f54c5a81d1eaa0a552fac6.png

说一下上面的执行过程是怎样的:

  • 因为在浏览器输入【服务器的ip:8080号端口】访问服务器的首页时,首页的代码是如下图,action字段是/a/b/haha.html,表示将来在首页中点击登录按钮时浏览器会访问该路径上的资源,但是该路径是一个胡乱填的路径,服务器上根本就没有这个资源,所以是一定访问不到的,此时在服务端进程的代码中fopen就会返回nullptr,然后进入if分支构建一个【状态码为301的、响应报头的Location字段为csdn首页路径的】http响应,然后浏览器收到该响应后就知道自己需要再次构建http请求去访问csdn首页。7868bab261a84572b0ece4778f2f4e58.png

4、根据上面的表格可知当客户端收到的http响应中的状态码字段是4XX时,比如是404时,则表示服务器无法处理客户端发起的http请求;再比如是403时,则表示客户端的权限不够,服务器直接拒绝了客户端的访问。说一下,上图中把4XX状态码称为客户端错误状态码,这是因为当客户端收到状态码为4XX比如404的http响应时,错误原因是在客户端而不是服务端,比如服务器上/a/b/ 路径下没有apple.html这个资源,但客户端就是要请求它,这就是非法请求,这是客户端的错误,和服务端没有关系,所以才把4XX称为客户端错误状态码。

5、根据上面的表格可知当客户端收到的http响应中的状态码字段是5XX时,则表示服务器内部错误,即服务端进程中在执行代码时发生了错误,比如服务端创建子进程或者新线程失败,再比如服务端进程中在创建某个对象时开辟空间失败,这些情况都叫做服务器内部错误,此时服务端就会给客户端发送状态码为5XX的http响应。说一下,一般来说客户端是收不到状态码为5开头的http响应的,当用户通过客户端访问服务器的某个资源失败时,即使客户端发起的http请求是合法请求,即使真的是服务端内部发生了错误,此时客户端收到的http响应中的状态码也一般是4XX,表示是你用户通过客户端发起了非法请求导致访问错误,而不是我公司的服务端的锅;或者是3XX,强行给你跳转让你去访问其他内容。为什么要这样呢?也很好理解,因为大部分公司并不想承认错误是自己的问题。

http请求和http响应中常见的报头字段

http请求中常见的报头字段

1、Content-Length:表示http请求中的请求正文的长度。

2、Content-Type:表示请求正文的数据类型,比如正文是html文件时,则正文的数据类型就是text/html,正文也有可能是其他文件比如png、mp3。总之不管是什么文件,反正这个字段最后就填 text/文件的后缀名 即可。

3、Host:一般来说,http请求中的Host字段填的就是 服务端进程的ip:port ,表示客户端告知服务器,我所请求的资源是在哪个主机的哪个端口上。

4、User-Agent:http请求中的User-Agent字段填的就是用户的操作系统和浏览器的版本信息。有什么用呢?举个例子,如下图红框处,当我们用浏览器访问百度这台服务器并通过浏览器将数据(微信这个参数就是数据)提交给百度这台服务器时,为什么百度返回给浏览器的响应正文(即下图这个网页)中会直接推荐我们下载电脑版呢?换句话说百度这台服务器怎么知道我要的就是电脑版呢?很简单,浏览器在发起http请求时将主机的OS和浏览器的版本信息填充到了http请求的报头部分中的User-Agent字段,百度这台服务器收到http请求后就得知了这些信息,从而把Windows版的微信优先推荐给了浏览器,这也就是为什么不同的设备在搜索时呈现的结果不一样。

c9d7a03078d14a5689155189e7a71cb4.png

5、Referer:在上文中讲解http响应中的状态码时说过,当客户端收到服务端发给自己的状态码为3XX的http响应时,客户端就会再次构建http请求并发给服务端,以此访问【填充在刚刚收到的http响应中的响应报头部分中的Location字段上的路径】上的资源。当客户端再次构建http请求时,就会把自己当前正在访问的位于服务器上的资源的路径给填充进http请求中的请求报头部分中的Referer字段上,告知服务器我是从哪个资源重定向到即将要访问的资源上的。什么意思呢?

举个例子,比如我浏览器访问的是gs1.com/a/b,但服务端发送了表示需要浏览器进行重定向跳转的状态码为3XX的http响应,导致浏览器最后访问到的网页的路径是gs1.com/a/c,在这个过程中,浏览器第二次构建http响应访问gs1.com/a/c时,填进http响应中的响应报头部分中的Referer字段的就是gs1.com/a/b。

6、Cookie(在http请求中)和set-Cookie(在http响应中):讲解这个前,要先说一些前置知识点。http协议的特性有如下几点:

  • 简单快速:客户端通过http协议在互联网中请求各种资源时,底层采用套接字系统接口和TCP协议。比如客户端在向服务端请求资源时会通过http请求中的请求行部分中的url告诉服务端我要什么资源,服务端拿到这个url后再拿到这个url路径上的资源并快速响应,即给客户端发送一个http响应,将客户端请求的资源放进http响应的响应正文中。
  • 无连接:http协议是无连接的。有人可能会说【http协议底层在收发数据时用的是send和recv这些基于TCP协议的套接字接口,而之前在编写基于TCP协议的网络服务器时又说过服务端和客户端在进行TCP通信前需要先进行连接,那既然这样的话http协议也应该是有连接的。】这里笔者想说的是,的确TCP是面向连接的,但这里说的是http协议本身,在上面编写代码的过程中,咱们编码构建http请求和http响应的过程中应该可以感受到http协议是完全不支持维护客户端和服务端的双方连接的,是底层的TCP协议在维护双方的连接。
  • 无状态:就是说客户端在访问服务端的某个资源后,服务端靠http协议是不知道客户端曾经访问过服务端的哪些资源的。这在某些场景下就会出现一个问题,比如说腾讯视频,当我点开一个vip视频(这是页面1)、即发起第一次http请求时,这时腾讯视频这台服务器会发出http响应使我重定向跳转到登录页面(这是页面2)以要求我先登录,当我完成了登录(即发起第二次http请求),此时腾讯视频这台服务器就会再发出http响应使我重定向跳转到页面1以让我观看视频。现在问题来了,我们说过客户端在访问服务端的某个资源后,服务端靠http协议是不知道客户端曾经访问过服务端的哪些资源的,这也就是说http协议压根不记录用户的登录状态,这就会导致我完成了登录后准备看视频时,因为http协议无法让腾讯视频这台服务器知道我曾经访问过登录页面这个资源(即服务器不知道我曾经登录过),所以当我准备看视频时它又让我去登录页面登录,后续也会不断重复这个流程,这样一来vip视频就看不了了。

针对上面所说的无状态这种特性,根据我们的上网经验可知这种情况在实际中是不会发生的,不存在讲解无状态时所说的这种问题,这是怎么回事呢?

这是因为我们使用了其他办法解决这个问题。说一下,虽然说客户端在访问服务端的某个资源后,服务端靠http协议是不知道客户端曾经访问过服务端的哪些资源的(因为http协议只负责比如说你关心什么资源,什么格式的,大小是多少,我快速发给你,即http协议就是一个普通的文件传输协议,用于把文件的内容读到后发给别人,被称为超文本传输也只不过是因为可以传输的文件种类多,比如文本文件、音视频文件、网页html文件),但如果只是想解决讲解无状态时所说的问题,并不是只能通过让服务端知道客户端曾经访问过服务端的哪些资源,我们是有其他的解决问题的方案的。方案是什么呢?方案是让服务器进行会话管理。

说一下,会话管理就是一种用于让服务器保持并维护用户的登录状态的策略,那一个服务器是如何进行会话管理解决上面的问题的呢?如下:

  • 客户端在登录前,发送http请求访问服务器上的资源时是不会把账号密码信息包含在http请求中的。如果客户端发送http请求访问服务器上的某种vip资源,是需要把账号密码信息包含在http请求中的,因为服务端需要收到该http请求后会进行认证,即先查看收到的http请求中是否携带账号密码信息,如果没有,客户端就会访问登录页面,如果有,则查看包含在http请求中的账号密码是否匹配以及是否具有vip资格,如果匹配成功且具有vip身份,才会让该客户端访问vip资源。综上也就是说,如果客户端想访问vip资源,就得在发送的http请求中加上自己的账号密码信息,如果http请求中没有账号密码信息,客户端就会重定向访问到登录页面;如果有账号密码信息,则看是否能通过认证,如果能,则就能成功访问vip资源了。
  • 客户端在登录后(点击登录就会发送一个http请求给服务器,根据上文的编码经验可知该http请求中要请求访问的文件的路径是登录html网页源码中action字段上的路径,又因为提交的数据是账号密码这种私密信息,可知提交数据给服务器时用的是POST方法,即网页源码中的method填入的是POST,此时账号密码信息就会被放在http请求的请求正文后面),服务器在收到客户端的http请求后,会把http请求中的账号密码提取出来进行认证,即观察账号密码是否匹配,认证通过后,服务端就会给客户端发送一个状态码为3XX的http响应让客户端进行重定向访问服务器的首页或者其他页面。在客户端收到这个http响应后就知道自己的请求通过了,用户输入的账号密码是正确的,此时客户端就会把这个账号密码信息存在客户端的某个磁盘文件或者内存文件上(比如可能是浏览器的安装目录中的一个位于磁盘的文件,也可能是客户端malloc开辟了一块内存以当作临时内存文件),此后客户端每次再向服务器发送http请求时,都会把这个账号密码信息自动填充进http请求中,服务端每次认证也就会发现http请求中的账号密码信息是存在,并能匹配的,这样一来因为每次http请求中就都有了账号密码信息,所以根据上一段最后的结论可知也就不会重定向访问到登录页面,而是访问我们想要的资源了。也就是说,因为服务器收到的http请求中包含了账号密码信息,服务端也就知道了客户端是处于登录状态的,也就能让客户端去访问一些处于登录状态或者说vip状态下才能访问的资源了。

【tips:有一些客户端软件访问服务器并不是说访问vip资源才需要进行登录,而是你想访问任何资源都得进行登录,这些客户端保持登录状态的原理和上面是一模一样的,都是通过在客户端创建一个文件,把账号密码这种私密信息放到该文件里,后序客户端构建http请求访问服务器上的任意资源时都把该文件里的账号密码信息放进http请求中,服务端检测发来的http请求发现其中携带账号密码信息,此时就让你通过并成功访问(注意如果没有携带账号密码信息是不让你访问任意资源的)。】

可以看到,综上可以发现,只需要第一次登录成功后,账号密码会被放在客户端创建出的一个文件里,后序每次客户端访问服务端都不用再让用户手动输入账号密码,而是客户端自动加上这些信息从而使得客户端能有权限访问服务端上的资源。在这个过程中,客户端创建出来的用于保存用户私密信息的文件就被称为Cookie文件。如何判断Cookie文件是内存文件还是磁盘文件也很简单,比如说浏览器访问b站时,b站就是服务端,浏览器就是客户端,当我们登录b站后,再把浏览器叉掉并重新打开浏览器和b站,发现自己的账号还能自动登录,则说明该Cookie文件是位于浏览器的安装目录中的某个磁盘文件;如果发现账号需要重新登录,则说明该Cookie文件是浏览器进程中malloc出的一个临时内存空间(或者说临时内存文件)。说一下,如下图当我们把Cookie文件删除后,再刷新网页,此时就需要用户重新输入账号密码进行登录了,因为浏览器没有了Cookie文件就无法自动把账号密码信息放进http请求中,也就无法自动构建一个用于进行登录的http请求发给服务器,也就无法自动登录了。

走到这里,会话管理的过程才说了一半,上面的会话管理是不完善的,因为用户信息的安全没法被保证。比如说我因为点了某些链接访问了一些恶意网站,导致黑客在我电脑上安装了木马,木马的逻辑是在我电脑上找到浏览器的Cookie文件并将其内容通过网络发给黑客,黑客在拿到这个Cookie文件后,将其放到了他的浏览器的安装目录下,那么他通过浏览器访问一些我访问的网站时,他的浏览器也会自动拿着Cookie文件中的账号密码信息构建用于登录的http请求,相当于他就能直接以免密码的形式登录网站,这就将我们的号盗取了,更重要的是,如果Cookie文件当中的账号密码是可以被黑客看见是多少的,那么用户的信息就被严重泄漏了,因为其中的密码很可能其他地方也在用,比如qq、微信、银行密码都是这个。(说一下,如果Cookie文件是内存级文件,那黑客是拿不到该Cookie文件的,只有当Cookie文件是磁盘文件时黑客才有机会拿到Cookie文件)

所以在实际中,真正的会话管理是不可能把账号密码本身保存在Cookie文件中的,而是让其他信息替代账号密码被保存在Cookie文件中,这个信息就叫做session id。session id是怎么来的呢?当我们第一次在客户端输入账号密码构建http请求发给服务端进行登录后,服务端在收到http请求并进行认证,如果认证通过,则会通过设置的算法给该客户生成一个唯一的(即和其他客户不同的)session id,并把账号密码信息和session id放到服务端的某个文件里,然后再构建一个包含刚刚生成的session id的http响应发给客户端,客户端收到http响应后再把其中的session id提取出来并放到客户端创建的Cookie文件中,此后客户端每次向服务端发送http请求以访问服务器上的资源时就把该session id放到http请求中,客户端在收到http请求后就先看请求中是否包含session id信息,如果没有,则发一个状态码为3XX的http响应让客户端访问登录页面;如果有,则进行认证,检查服务器中用于记录用户私密信息的文件中是否存在该session id,如果存在,则让该客户端访问资源(即服务端将资源的内容通过http响应发给客户端),反之则不让它访问。

可以看到,引入session id这样的方案后,黑客拿到我们浏览器的Cookie文件后,顶多能免密码登录我们登录过的一些网站,但再也不会导致用户的信息泄漏了,因为Cookie文件中只保存了一个session id信息,黑客顶多只能看见session id是多少。

说一下,既然别人能免密码登录我们的账号,他要是捣乱,给别人发骚扰信息或者把消费券用完,这不就给我们造成了很大的损失嘛,有没有什么方法能避免让别人免密码登录我们的账号呢?答案是避免不了,顶多编码让服务端通过一些策略减少别人能免密码登录的情况,因为用户的session id被盗取这种情况是是不可能杜绝的,毕竟我们不能完全识别出哪些软件是恶意的,哪些是善意的可供我们使用的。

那如何通过策略减少别人能免密码登录的情况发生呢?比如说客户端发起http请求时,底层是通过基于TCP协议的套接字接口send或者write把http请求的内容发给服务端的,send或者write在被调用时会把客户端的ip地址也发给服务端,所以如果黑客盗取了我的session id免密码登录后,他访问任何资源都会把自己的ip发给服务端,如果服务端识别到ip地址发生了很大的变化,比如我一般在ip地址属于湖北省的某些主机上访问服务端,而黑客在缅甸盗取我的session id后通过ip地址属于缅甸的设备访问服务端时,服务端就会识别到用户的ip地址发生了很大的变化(即发现了用户异地访问),服务端进程就会执行相关代码让该session id失效,要求用户重新输入账号密码进行认证,此时因为黑客只能拿到我的session id,而拿不到我的账号密码,他也就没有办法再使用我的账号了。有人就会说【既然如此,那我就养一养,我拿到session id后不立刻在缅甸登录,而是等个3天,3天时间以及完全足够从中国的任意一个位置跑到缅甸了,通过这样的方式骗一骗服务端。】这里笔者想说,的确是可以这样的,但经过时代的发展,制作服务端的人当然也考虑过这种情况,所以会设置一些策略,比如经过多长时间就执行代码让session id失效,所以也能有效的防止黑客盗取别人的session id从而免密码登录别人的账号的情况。所以总结就是:服务端是可以编码通过一些策略(即达到某种条件就让session id失效)来减少别人能免密码登录的情况发生的,但只能减少不能杜绝,因为这个世界是有防守的策略就会有攻击的策略,你定制的策略再完善,随着时代的发展、时间的推移也是会被人找到漏洞从而破解的。

到了出结论的时候了,既然上文中说第一次登录时,服务端会把session id包含在http响应中发给客户端,客户端后序给服务端发送http请求访问服务器时会把session id包含在http请求中,那session到底被包含在了http请求和http响应中的哪呢?答案:我们会在http请求的请求报头部分中加上Cookie字段、会在http响应的响应报头部分中加上set - Cookie字段,这些字段上就填充的是这个session id的值。想必到这里读者你就能完全理解Cookie 和 set - Cookie是干什么的了。

接下来咱们通过编写代码复现一下上面的过程。即我们通过浏览器发起http请求访问我们编写的服务端进程http_server.cc后,服务端进程给浏览器发来http响应后,我们期望在浏览器的Cookie文件中能看见包含在http响应中的响应报头部分的set-Cookie字段上的值(该值可以在服务端进程http_server.cc中随意设置),并且期望后序客户端给服务端发起http请求访问服务器上的资源时,在服务端进程收到http请求并将它完整地打印出来后,能在http请求中的请求报头部分中的Cookie字段看见【和之前看到的set-Cookie的值相等的值】,如果这两个现象都能看见,则证明上文中所说的关于Cookie和set-Cookie的知识点以及相关理论都是正确的。需要怎么进行实验呢?

先将在标题为http响应中的状态码字段部分中用于演示重定向时所用的http_server.cc以及homePage.html等等一系列代码拿过来,然后如下图红框处所示只需在原来http_server.cc代码的基础上加上2行代码即可完成编码,原来的代码位于下图左边,修改后的代码位于下图右边。

服务端进程http_server.cc的代码修改完毕后,将其进行编译并在8080号端口运行它,然后就可以进行测试了。如下图红框处所示,在浏览器输入【服务器的ip:8080号端口】访问到服务器的首页后,就已经能在浏览器中看见我们期望看见的现象之一了,点击登录后(无所谓是否输入账号密码,上文中需要输入账号密码是为了演示POST和GET方法在提交数据时有什么区别),就能在shell界面上看见我们期望看见的现象之二了。因为两个现象都能看见,所以也就证明了上文中所说的关于Cookie和set-Cookie的知识点以及相关理论都是正确的。

说明一下下图的执行流程:当浏览器作为客户端访问服务端进程http_server.cc后,因为搜索框中不带路径,http请求中的url就默认是 / ,此时根据服务端进程的编码逻辑,识别到路径为 / 时就会进行拼凑,即把web根目录 . 拼在 / 的前面,再把表示首页的homePage.html拼凑在 / 的后面,最后需要服务端进程fopen的路径就是 ./homePage.html ,读取到文件的内容后,服务端进程就会构建http响应,并把 ./homePage.html 路径上的资源的内容放进http响应的响应正文中,同时把设置的set-Cookie字段也放进http响应中,然后浏览器收到http响应后也就访问到服务器的首页了,并且也就拿到了set-Cookie字段表示的session id值并把该session id的值存进了浏览器创建的Cookie文件中,此后浏览器每次构建http请求访问服务器上的资源时就会将session id的值放进http请求中的请求报头部分中的Cookie字段中。

7、Connection:该字段表示是否是长连接,比如值是keep-alive就是长连接;值是close就是短连接。

短连接相对于长连接,它的特点是在需要发送数据时才建立连接,数据传送完成后立刻断开连接,每次连接只完成一项业务的发送,这就避免了长连接维护时所用的开销,但同时这就会导致一个问题,当业务量很大的时候就需要频繁的断开连接又建立连接,所以当业务量大时就不应该使用短连接,而是长连接。

长连接指在一个连接上可以连续发送多个数据包,而连接保持在一定时间内。在这段时间内,可以不断地进行数据交换,而不需要重新建立连接,对比短链接就省去了频繁重新建立连接的开销,当业务量大的时候,就需要使用长连接,这时长连接的效率就会比短链接高。

http响应中常见的报头字段

1、Content-Length:表示http响应中的响应正文的长度。

2、Content-Type:表示响应正文的数据类型,比如正文是html文件时,则正文的数据类型就是text/html,正文也有可能是其他文件比如png、mp3。总之不管是什么文件,反正这个字段最后就填 text/文件的后缀名 即可。

3、Location:在上文中讲解http响应的状态码时已经进行过详细说明了,这里就简单说明一下。当服务器向客户端发起表示需要让客户端进行重定向的状态码为3XX的http响应时,客户端就需要进行重定向访问服务端指定让客户端访问的资源。服务端指定让客户端访问什么资源呢?这个数据或者说资源的路径会被包含在服务端发给客户端的http响应中的响应报头部分的Location字段中,客户端在获取到这个路径后就会再次构建http请求发给服务器,访问这个路径上的资源。

4、set-Cookie:详细内容在上面讲解【http请求中常见的报头字段】时讲解过了,请在上文中阅读。

http协议的局限性

如下图的下半部分,这是一款名叫fiddler的软件, 它能够捕捉本机向别人发起的所有http请求,比如当你点击下图上半部分的登录后,在fiddler中就会捕捉到这个用于登录的http请求,更重要的是可以发现fiddler不光捕捉到了该http请求,而且如下图的下半部分的红框处,还把用户输入的私密账号密码信息给捕捉到了,如果有恶意份子通过类似的方法捕捉我们发起的所有http请求,这就会造成用户的信息泄漏,这也是为什么在上文中说http请求的请求方法是POST时顶多比GET方法私密一点,但http协议绝对是不安全的,这就是http协议的局限性。为了保证用户的输入的信息不被泄漏,现在客户端和服务端之间使用的主流的应用层协议是https协议,https协议的内容咱们在其他文章中再详细介绍。

fiddler的原理是什么呢?很简单,如下图,如果没有打开fiddler,则客户端和服务端是直接通信的,即客户端直接把http请求发给服务端,服务端之间把http响应发给客户端;而如果打开的fiddler,则客户端只会给fiddler发出http请求,然后fiddler收到后再由fiddler向服务端发起http请求,服务端也只会给fiddler发出http响应,然后fiddler收到后再由fiddler向服务端发起http响应。

既然因为http请求会把用户输入的数据明文显示在http请求的请求正文部分(当请求方法是POST方法时就会这样)或者是http请求的请求行部分(当请求方法是GET方法时就会这样),进而导致http协议并不安全,导致现在客户端和服务端之间使用的主流的应用层协议是https协议,那为什么还要学习http协议呢?

因为http协议不需要保证用户输入的数据不被泄漏,那么相对于https协议,http协议所需要做的事情就一定是更少的,那么客户端和服务端之间通过http协议进行传输数据或者说通信时的效率一定是比通过https协议的效率高的。当环境比较安全时,比如是公司的内网,即使数据泄漏也只会在内网中泄漏,并不会损失公司的利益,那么此时就优先考虑通信的效率,此时就需要使用http协议了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值