• 深入浅出计算机网络OSI模型之应用层(此文重点剖析http协议)


    应用层概述

    应用层是OSI模型中的最高层。它为应用程序的通信提供服务并且规定应用程序在通信时的协议。

    应用层协议定义的内容:

    • 1、应用程序的报文类型,是请求还是响应?
    • 2、各种报文类型的语法,例如各报文中的各个字段及其详细描述
    • 3、字段的语义(包含在字段中的信息的含义)
    • 4、进程何时发送、如何发送报文,以及对报文进行响应的规则。

    应用层的功能:

    • 文件的传输、访问和管理
    • 电子邮件
    • 虚拟终端
    • 查询服务和远程作业登录

    应用层的重要协议:

    • FTP
    • SMTP、POP3
    • HTTP
    • DNS

    域名和DNS

    用户和因特网上的某个主机进行通信的时候,IP地址是难以记忆的。不要妄图将需要通信的主机IP地址记住,因为不可能只和一台主机进行通信(而且一台主机可能会有多个IP地址)。所以用户更愿意去叫主机的名字。类似于日常生活中,人们不会去特意地记忆和称呼对方的身份证号,而是去叫对方的名字。

    早在21世纪以前,使用一个名为hosts的文件存储着IP地址与主机名的一一对应关系,到现在还依然沿用。
    Linux系统中也有hosts文件。可使用命令cat /etc/hosts可以查看
    实质上,主机名的含义是机器本身的名字,域名
    在局域网中,主机名基本上都是不同的。但在整个因特网中可能会出现主机名相同的情况,为了能够在整个因特网中标识唯一一台主机,所以引入了域名。域名是包含了主机名和该主机所属组织机构的一种分层结构的名字。

    //一个域名
    www.baidu.com
    
    • 1
    • 2

    DNS域名系统是互联网的一项服务,它被设计成一个域名和IP地址相互映射的联机分布式数据库系统,采用客户服务器方式。主要作用是将域名解析成IP地址(由域名系统中的域名服务器完成)。

    主机通过域名访问的过程
    在这里插入图片描述

    首先是一台主机得到了一个需要访问的域名,其次:
    【第一步】:应用进程调用解析程序,成为DNS的一个客户,把该域名放入DNS请求报文,以UDP用户数据报方式发送给域名服务器。
    【第二步】:域名服务器查找域名和域名对应的IP地址,并将IP地址放在回答报文中,返回给客户。
    【第三步】:应用进程获得目的主机的IP地址,开始进行和目的主机的通信,发送请求报文给目的主机。
    【第四步】:目的主机提供服务,发送响应报文给主机。

    整个因特网不仅仅只有一个域名服务器,如果只使用一个域名服务器,因特网的庞大规模一定会造成域名服务器负荷而无法正常工作,一旦这个域名服务器出现了故障,那么整个网络也就瘫痪了。为什么瘫痪,因为域名服务器瘫痪,我们无法通过域名访问到目的主机。所以要访问的域名有可能没有装入这个域名服务器,就会按照如下的方式去查找:
    在这里插入图片描述

    域名结构

    在这里插入图片描述
    在这里插入图片描述
    域名内,子域与子域之间使用.分隔。
    DNS关于域名的规定:

    • 不区分大小写,BAIDU和baidu是等效的
    • 域名由英文字母和数字组成,并且每一个标号(分隔的一段)不超过63个字符。
    • 标号不能使用标点符号,除了连字符-
    • 域名的级别从左到右逐渐增高
    • 一个完整域名不少于1个字符且最多不超过255个字符。

    【顶级域名】
    顶级域名TLD(Top Level Domain)有265个
    划分成三大类:

    • (1) 国家顶级域名nTLD

    据2006年统计,国家顶级域名共有247个
    常记为ccTLD(cc 为country-code缩写,表示国家代码)
    采用的是ISO3166规定:cn表示中国;us表示美国,uk表示英国……

    • (2) 通用顶级域名gTLD

    据2006年统计,通用顶级域名共有18个
    可自行查阅

    • (3) 基础结构域名

    基础结构域名只有一个:arpa
    用于反向域名解析,所以又称反向域名

    【二级域名】
    二级域名由各国国家规定,我国规定的二级类域有类别域名行政域名

    • 类别域名

    类别域名共有7个:ac(科研机构)、com(工、商、金融等企业)、edu(中国教育机构)、gov(中国政府机构)、mil(中国国防机构)、net(提供互联网络服务的机构)、org(非营利性的组织)

    • 行政域名

    适用于我国各省、自治区、直辖市。基本以拼音的缩写表示,例如:gz(贵州省)

    可使用域名树表示域名系统:
    在这里插入图片描述

    域名服务器的分类

    DNS服务器的管辖是以区为单位的。一个服务器负责管辖的范围称为,每一个区设置相应的权限域名服务器。

    根据域名服务器提供的作用,可分为以下四类:

    • 根域名服务器
      根域名服务器是整个DNS系统中最高层次的域名服务器。
      本地域名服务器如果要进行域名解析,而自己无法解析(没有装入这个要解析的域名和IP地址的映射关系),首先该本地域名服务器就会把自己当作根域名服务器的一个客户,求助于根域名服务器。

      目前为止共有13个不同IP地址的根域名服务器,它们是13套装置并且域名有一定的规则。中国有3个根域名服务器,分别在北京、香港和台北。根域名服务器采用了任播技术,可以加快域名解析的过程。
      在这里插入图片描述

      注意:本地域名服务器如果无法解析某个域名,求助于根域名服务器,根域名服务器给出的帮助是:告诉本地域名服务器下一步要去找哪一个顶级域名服务器。

    • 顶级域名服务器
      也称TLD域名服务器,负责管理该顶级域名服务器注册的所有二级域名。当收到DNS查询请求,TLD域名服务器就会查找该域名,找到了就发送该域名对应的IP地址回去;如果没有找到,就给出下一步应当去找哪一个域名服务器的IP地址。
      在这里插入图片描述

    • 权限域名服务器
      权限域名服务器负责一个区的域名服务器,如果NDS客户求助于权限域名服务器,而权限域名服务器没有查询到,就会告诉客户,下一步应该找的权限域名服务器的IP地址。

    • 本地域名服务器
      也称为默认域名服务器,进行域名解析时,客户首先就会把请求报文发送给本地域名服务器。

    为了提高域名服务器的可靠性,DNS域名服务器会把数据复制到几个域名服务器来保存,其中一个是主域名服务器,另外的是辅助域名服务器

    文件传送协议

    文件传送协议提供不同种类主机系统(软、硬件体系结构等都可不同)之间文件的传输能力。

    文件传送协议有:

    • 基于TCP的文件传送协议FTP(File Transfer Protocol)
    • 基于UDP的简单文件传送协议TFTP(Trivial File Transfer Protocol)

    它们都是文件共享协议中的一大类,拷贝整个文件,特点:如果要存取某个文件,必须先获得一个本地的文件副本。如果尝试修改文件,只能对文件的副本进行修改,然后再将修改后的文件副本传回原节点。
    在这里插入图片描述

    • 从客户端拷贝文件到服务器称为上传
    • 从服务器拷贝文件到客户端称为下载

    FTP的工作原理

    FTP只提供文件传输的一些基本服务,采用客户/服务器工作方式。使用TCP协议 实现可靠传输。

    依照FTP协议提供服务,进行文件传送的计算机就是FTP服务器;连接FTP服务器,遵循FTP协议与FTP服务器传送文件的计算机称为FTP客户端。一个FTP服务器可以同时给多个进程提供服务。服务器采用的是多进程版本基于TCP的通信,所以有一个进程,负责接收请求,该进程称为主进程;其他的进程有多个,负责处理请求,称为从属进程

    服务器的主进程工作过程:

    • (1)打开孰知端口号(端口号21),使客户进程能够连接
    • (2)等待客户进程发出请求
    • (3)启动从属进程处理请求。处理完毕后,从属进程终止
    • (4)回到等待的状态,继续处理其他客户进程的请求

    由于采用多进程版本的TCP通信,所以主进程和从属进程是并发进行的。
    在这里插入图片描述
    FTP的客户/服务器相较于其他客户/服务器的独特优势就是:文件传输时,客户和服务器之间建立两个并行的TCP连接。
    在这里插入图片描述
    优点在于:使得协议更加简单和易于实现,并且提高了FTP的效率。

    注意:
    FTP服务器进程使用自己传送数据的熟知端口20和客户进程提供的端口号建立数据连接。事实上,具体是否使用端口20还取决于文件的传输模式。主动的方式使用的是端口20,而被动的方式由FTP服务器和客户端协商决定(端口 >1024 && 端口 < 65535)。

    FTP传输方式

    • ASCLL传输方式
    • 二进制传输方式

    FTP匿名服务器

    使用FTP时用户必须首先进行登录(用户名 & 密码),在远程主机上获取到相应权限之后,才有资格上传和下载文件。但是互联网上有如此多的FTP主机,不可能要求用户在每一台主机上都有账号。所以引入了匿名服务器,有匿名服务器后用户可以不需要进行登记注册和获取FTP服务器的授权,就能够建立连接,可以从匿名服务器上下载文件。

    有了匿名服务器后,我们可以进行匿名登录。登录的用户名是标识符anonymous,密码可以是任意的字符串。

    不是所有的FTP主机都能够匿名登录,只有提供了这项服务的FTP主机才支持匿名登录。

    电子邮件协议

    电子邮件E-mail是应用层的一种异步通信方式,不需要通信双方在场。
    收件人和发件人的格式:用户名@邮件服务域名

    电子邮件的组成结构

    在这里插入图片描述

    用户代理:用户与email系统的接口。
    发送人用户代理的功能:编写;展示;处理;通信。
    SMTP服务器的功能:发送&接收邮件;回报发送结果。

    其中的协议

    • SMTP:邮件发送协议,客户端推送邮件到SMTP服务器
    • POP3:邮件接收协议,向SMTP服务器拉取邮件。

    电子邮件支持异步通信:发送邮件服务器会把邮件发送到接收方邮件服务器,接收方邮件服务器可以选择接收并删除、接收并保存,如果选择接收并保存,就可以等接收方用户需要时,接收方用户再去请求读取邮件。

    SMTP

    简单电子邮件传送协议SMTP(Simple Mail Transfer Protocol)属于TCP/IP协议族,它规定了两个相互通信的SMTP进程之间如何交换信息,并且帮助SMTP客户找到下一个目的地。

    遵循SMTP协议负责发送邮件的进程就是SMTP客户;遵循SMTP协议负责接收邮件的进程就是SMTP服务器。当然,SMTP服务器也可作为SMTP客户。

    采用客户/服务器工作方式、使用TCP协议 实现可靠传输。SMTP服务器进程使用孰知端口25。

    【SMTP通信三阶段】

    • (1)建立连接
      在这里插入图片描述
      接收方邮件服务器如果有能力接收邮件,就会“确认”,确认回复的是250 OK。如果现在没有能力接收邮件,就会给发送邮件服务器回复421 Service not available
    • (2)邮件传送
      邮件传送的细节这里不做介绍。
    • (3)释放连接
      邮件发完之后,SMTP客户发送QUIT命令,SMTP如果返回221,就表示同意释放TCP连接

    【SMTP的特点】
    SMTP是简单电子邮件传输协议,有一些缺点比较明显:

    • 1、不能发送可执行文件或者二进制对象
    • 2、仅仅只能发送7位以内的ASCLL码,并且只能传送英语。
    • 3、SMTP服务器会拒绝超过一定长度的邮件。

    然而日常生活中,用户基本上不会发送7位以内的ASCLL码并且发送的邮件长度也是不固定的,所以针对SMTP协议的这些缺点,扩充了MIME

    MIME

    多用途网际邮件扩充MIME是对SMTP的扩充,定义了传送非ASCLL码的规则。不要混淆了,它没有打破SMTP只能发送7位以内的ASCLL码的规则。
    在这里插入图片描述

    新增:

    1、新增了5个首部字段:MIME版本/内容描述/内容标识/内容传送编码/内部内容
    2、定义了邮件内容格式,对多媒体邮件进行了标准化
    3、定义了传送编码,可对任意格式内容转换。

    MIME使得电子邮件系统可以支持声音、图像、视频、多种国家语言等等,让电子邮件的传送内容变得丰富多彩。

    POP3

    POP3是简单的邮局读取协议(Post Office Protocol)。
    在这里插入图片描述
    通信的方式采用拉取(pull)
    属于TCP/IP协议族、通过TCP端口110建立连接。

    IMAP

    互联网信息访问协议IMAP,是一种优于POP3并且也比POP3更复杂的新协议。当用户打开IMAP服务器的邮箱时,可以看到邮箱的首部,通过邮箱首部的内容(比如:邮件标题,发送方名字等)再决定是否下载。

    属于TCP/IP协议族、通过TCP端口143建立连接。

    电子邮件协议TCP连接端口
    SMTP25
    POP3110
    IMAP143

    HTTP

    万维网

    万维网WWW(World Wide Web )是一个规模庞大,联机式的信息存储所,是无数文档的集合,简称Web。这里的文档就是我们所说的页面

    在这里插入图片描述
    页面都是一些超文本信息,可用于描述文本、图片、视频等多媒体信息,而这些多媒体信息称为超媒体
    Web上的信息由彼此相联的文档组成,相联的方式就是超链接(有时也称链接),可以实现从一个站点跳转到另一个站点。例如,这就是一个超链接点击此处可跳转
    在这里插入图片描述
    这些文档有可能相互分隔在千里之外,但必须连接在因特网上。整个因特网上的万维网文档是如此之多,它是如何精准地从一个站点跳转到另一个站点的?需要在因特网上唯一的标识一个万维网文档,所以引入了统一资源定位符URL。

    URL

    URL是统一资源定位符,就是我们俗说的“网址”。它用来在互联网上唯一的标识某个资源的地址,这里的资源指因特网上任何可以被访问的对象。

    URL的一般格式:
    在这里插入图片描述

    • 最常用的协议是http协议,其次是FTP协议。
    • 主机部分可以是域名,也可以是该资源所在主机的IP地址。
    • 使用http协议,默认端口是80,通常可以省略。
    • 路径也可以省略的,如果省略了路径,那么URL定位的资源就是某个主页。
    • URL的字符串不区分大小写

    像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现。比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.

    转义的规则:将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式
    在这里插入图片描述

    所以用户浏览页面的方法有两种:

    • 1、在浏览器的地址窗口输入要跳转到的页面的URL。
    • 2、点击页面的超链接

    HTTP概述

    Web之所以能够完成这样宏大的一项工作,离不开它背后的各种协议,这些协议被称为Web协议族。HTTP协议就是Web协议族中的一个典型代表,HTTP协议定义了浏览器(万维网客户进程)怎样向万维网请求万维网文档,以及服务器怎样把文档传送给浏览器。
    在这里插入图片描述
    HTTP采用了TCP作为运输层协议,但HTTP协议本身是无连接的(通信双方在交换HTTP报文之前不需要建立HTTP连接)。

    HTTP/1.1使用了持续连接:服务器在发送响应后仍然在一段时间内保持着这条连接。
    在这里插入图片描述

    HTTP报文结构

    HTTP的两类报文:

    • 请求报文。客户->服务器
    • 响应报文。服务器->客户
      HTTP报文是面向文本的,所以在HTTP报文中的每一个字段都是一些ASCII码串。
      在这里插入图片描述
    • 开始行
      用于区分该报文是请求报文还是响应报文。请求报文内的开始行称为请求行, 响应报文内的开始行称为状态行。请求行和状态行都分成了三个部分,中间以空格为间隔。
    • 首部行
      首部行采用了键值对的形式首部字段名 : 值

    以上就是报文的报头部分,报头和实体主体之间使用一行空行分隔。其中响应报文的状态行内的短语,用于描述状态码的。例如我们常见的404 Not found,404是状态码,而Not found就是状态码描述。

    现在我们模拟实现一个http服务器。并且能够查看一个实际的请求报文。
    Sock.hpp的代码我放在了server.cc的后面

    //server.cc文件
    #include 
    #include 
    #include 
    #include "sock.hpp"
    #include 
    #include 
    
    void UsePage();
    void *HandleRequest(void *arg);
    
    //命令格式:./server server_port
    int main(int argc, char *argv[])
    {
        if (argc != 2)
        {
            UsePage();
            return 1;
        }
        uint16_t port = atoi(argv[1]);
        int listen_sockfd = Sock::GetSock();
        Sock::Bind(listen_sockfd, port);
        Sock::Listen(listen_sockfd);
    
        std::cout << "server ready……" << std::endl;
        while (true)
        {
            int new_sock = Sock::Accept(listen_sockfd);
            if (new_sock > 0)
            {
                //创建一个从属线程,处理客户请求
                int *pram = new int(new_sock);
                pthread_t pid;
                pthread_create(&pid, nullptr, HandleRequest, pram);
            }
        }
        return 0;
    }
    
    void UsePage()
    {
        std::cout << "命令格式:./server server_port" << std::endl;
    }
    void *HandleRequest(void *arg)
    {
        int sockfd = *(int *)arg;
        delete (int *)arg;
        pthread_detach(pthread_self());
    
        //接收客户端请求
        char buffer[1024 * 10];
        memset(buffer, 0, sizeof(buffer));
        ssize_t s = recv(sockfd, buffer, sizeof(buffer), 0);
    
        //成功读取到请求报文,可以构建一个响应报文给客户
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << buffer; //查看http的请求格式
    
            //开始构建响应报文
            std::string response = "http/1.0 200 OK\n";
            response += "Content-Type : text/plain\n"; // text/plain,表示正文是普通文本
            response += "\n";                          //空行
            response += "LHY \n i love you\n---YDY\n";
    
            //发送响应报文
            send(sockfd, response.c_str(), response.size(), 0);
        }
    
        //完成后,关闭用于处理请求的套接字文件描述符
        close(sockfd);
        return nullptr;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    //Sock.hpp文件
    #pragma once
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    using namespace std;
    class Sock
    {
    public:
        static int GetSock()
        {
            int fd = socket(AF_INET, SOCK_STREAM, 0); // TCP创建流式套接字
            if (fd < 0)
            {
                std::cout << "create socket failed" << std::endl;
                exit(1); //没必要执行下去,直接终止程序
            }
            std::cout << "create socket success" << std::endl;
            return fd;
        }
        static void Bind(int sockfd, uint16_t port)
        {
            //描述要绑定的套接字地址
            struct sockaddr_in server;
            server.sin_family = AF_INET;
            server.sin_port = htons(port);
            server.sin_addr.s_addr = INADDR_ANY;
    
            if (bind(sockfd, (struct sockaddr *)&server, sizeof(server)) < 0)
            {
                std::cout << " server bind socket address failed" << std::endl;
                exit(2);
            }
            std::cout << "bind socket success" << std::endl;
        }
        static void Listen(int listen_sockfd)
        {
            if (listen(listen_sockfd, 5) < 0)
            {
                std::cout << "server listen client failed" << std::endl;
                exit(3);
            }
            std::cout << "listen success" << std::endl;
        }
        static int Accept(int listen_sockfd)
        {
            //输出型参数,用于获取客户套接字地址
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
    
            int new_fd = accept(listen_sockfd, (struct sockaddr *)&peer, &len);
            if (new_fd < 0)
            {
                //创建新的套接字,并取出请求队列中的队头套接字地址连接失败,则返回-1
                return -1;
            }
            std::cout << "get a new link" << std::endl;
            return new_fd;
        }
        //客户请求连接服务器
        static int Connect(int sockfd, std::string ip, uint16_t port)
        {
            //描述服务器的套接字地址
            struct sockaddr_in server;
            server.sin_family = AF_INET;
            server.sin_port = htons(port);
            server.sin_addr.s_addr = inet_addr(ip.c_str());
    
            if (connect(sockfd, (struct sockaddr *)&server, sizeof(server)) < 0)
            {
                std::cout << "client connect server failed" << std::endl;
            }
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

    有了http服务器,那么http客户端呢?http的客户进程就是浏览器!!!

    所以要测试,直接在浏览器的地址窗口输入:运行服务端进程的公网IP:服务端进程绑定的端口号,例如:1.14.103.25:8080

    这里需要注意的是,运行我们模拟的http服务端进程时,要开放你绑定的端口。如何开放端口请自行查阅。

    我们就可以在服务器上看到浏览器发送的请求报文:
    在这里插入图片描述
    http请求报文的结构果然一一对应
    浏览器显示内容:
    在这里插入图片描述

    请求方法

    在上面的代码测试中,会发现服务器一次不止接收到一个请求报文,而是多个。日常工作中,http服务器一次接收到的报文也不止一个,那么它如何做到精确读取到一个完整的报文?

    • 报头的读取:判断是否读取到空行,读取到空行代表报文部分到此为止。
    • 正文(实体主体)的读取:如果有正文内容,首部行会包含一个字段Content-Length,表示该报文的正文有多少个字节,按照这个字段给的值读取字节数,就可以做到精确地读取正文了。
      在这里插入图片描述

    在这里插入图片描述

    方法说明支持的HTTP版本
    GET获取资源1.0 、1.1
    POST传输实体主体1.0 、1.1
    HEAD获取报文首部1.0、1.1
    PUT传输文件1.0、1.1
    DELETE删除文件1.0、1.1
    OPTIONS询问支持的方法1.1
    TRACE追踪路径1.1
    CONNECT要求用隧道协议连接代理1.1
    LINK建立和资源之间的联系1.0
    UNLINE断开连接关系1.0

    其中最常用的是GET、POST,其次是HEAD

    为了测试GET和POST请求,我们将改写上面的代码。
    在改写之前,我们需要补充一些预备知识。

    浏览器发给服务器:请求报文的请求行的第二部分(URL),是Web目录。
    在这里插入图片描述

    在这里插入图片描述
    http请求的/是Web根目录,如果请求/,意味着要访问该网站的首页。

    在这里插入图片描述
    而我们一般要请求的不是首页,而是具体的某个资源。

    现在我们给浏览器响应一个实体主体内容是文件的报文。并且引入一些HTML,控制浏览器给我们模拟的服务器发送GET、POST方法,方便查看这两种方法出现的现象和总结。
    在这里插入图片描述
    其中Sock.hpp,代码在文章前部分已经给出。

    //http.cc
    #include 
    #include 
    #include 
    #include "sock.hpp"
    #include 
    #include 
    #include 
    #include 
    
    #define WWWROOT "./WebRoot/"
    #define HOME_PAGE "index.html"
    
    void UsePage();
    void *HandleRequest(void *arg);
    
    //命令格式:./server server_port
    int main(int argc, char *argv[])
    {
        if (argc != 2)
        {
            UsePage();
            return 1;
        }
        uint16_t port = atoi(argv[1]);
        int listen_sockfd = Sock::GetSock();
        Sock::Bind(listen_sockfd, port);
        Sock::Listen(listen_sockfd);
    
        std::cout << "server ready……" << std::endl;
        while (true)
        {
            int new_sock = Sock::Accept(listen_sockfd);
            if (new_sock > 0)
            {
                //创建一个从属线程,处理客户请求
                int *pram = new int(new_sock);
                pthread_t pid;
                pthread_create(&pid, nullptr, HandleRequest, pram);
            }
        }
        return 0;
    }
    
    void UsePage()
    {
        std::cout << "命令格式:./server server_port" << std::endl;
    }
    void *HandleRequest(void *arg)
    {
        int sockfd = *(int *)arg;
        delete (int *)arg;
        pthread_detach(pthread_self());
    
        //接收客户端请求
        char buffer[1024 * 10];
        memset(buffer, 0, sizeof(buffer));
        ssize_t s = recv(sockfd, buffer, sizeof(buffer), 0);
    
        //成功读取到请求报文,可以构建一个响应报文给客户
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << buffer; //查看http的请求格式
    
            //将要打开的文件描述一下,便于后续操作
            std::string html_file = WWWROOT;
            html_file += HOME_PAGE;
            std::ifstream in(html_file);
    
            if (!in.is_open()) //打开文件失败
            {
                //构建响应报文
                std::string http_response = "http/1.0 404 NOT FOUND\n"; //状态行
                http_response += "Content-Type: text/html; charset = utf8\n";
                http_response += "\n"; //空行
                http_response += "

    The resource you want to access does not exist

    "
    ; //发送响应报文 send(sockfd,http_response.c_str(), http_response.size(), 0); } else //打开文件成功 { //构建响应报文 // 1、获取要打开文件的字节数,这个信息在获取到st中 // 用于增加报头中的字段Content-Length struct stat st; stat(html_file.c_str(), &st); // 2、构建响应报文的报头 std::string http_response = "http/1.0 200 OK\n"; //状态行 http_response += "Content-Type: text/html; charset = utf8\n"; http_response += "Content-Length: "; http_response += std::to_string(st.st_size); http_response += "\n"; http_response += "\n"; //空行 // 3、获取实体主体 std::string content; std::string line; while (std::getline(in, line)) { content += line; } // 4、将实体主体加入响应报文 http_response += content; in.close(); //发送响应报文 send(sockfd,http_response.c_str(), http_response.size(), 0); } } //完成后,关闭用于处理请求的套接字文件描述符 close(sockfd); return nullptr; }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116

    【浏览器请求GET方法时的现象】

    在这里插入图片描述浏览器提交表单时,浏览器的变化:
    在这里插入图片描述
    提交表单,服务器接收到的请求报文:
    在这里插入图片描述

    【浏览器请求POST方法的现象】

    在这里插入图片描述
    浏览器提交表单时,浏览器的变化:
    在这里插入图片描述
    提交表单,服务器接收到的请求报文:
    在这里插入图片描述

    GET、POST方法的总结】
    总结一(概念):

    • GET方法,作用是获取资源(下载),是最常用的获取资源方法;默认获取所有的网页,都是GET方法。如果GET需要提交参数,通过URL进行参数拼接,从而提交给服务端。
    • POST方法,作用是推送资源(上传),是最常用的提交参数方法;如果GET需要提交参数,不通过URL的参数拼接,而是直接通过正文(实体主体)提交。不要忘记首部行的字段Content-Length

    总结二(区别):

    • 参数提交的位置不同,POST方法比较私密(私密 != 安全),不会回显到浏览器的URL输入框;GET方法不私密,会把重要的信息回显到浏览器的URL输入框。
    • GET通过URL传参,URL有大小限制,不同的浏览器可能会有不同的限制;POST方法由实体主体传参,无大小限制。

    状态码

    状态码类别原因短语(描述状态码)
    1XXXInformational(信息性状态码)接收的请求正在处理
    2XXXSuccess(成功状态码)请求正常处理完毕
    3XXXRdirection(重定向状态码)需要进行附加操作以完成请求
    4XXXClient Error(客户端错误状态码)服务器无法处理请求
    5XXXServer Error(服务器错误状态码服务器处理请求出错

    重点提及3XXX。
    在这里插入图片描述
    如果响应报文的状态码是3XXX,那么响应报文的首部行就会有一个字段Location,这个字段包含重定向到哪一个文档的URL。重定向是浏览器支持的,使用的浏览器必须能够识别301、302、307状态码。
    在这里插入图片描述

    #include 
    #include 
    #include 
    #include "sock.hpp"
    #include 
    #include 
    #include 
    #include 
    
    #define WWWROOT "./WebRoot/"
    #define HOME_PAGE "index.html"
    
    void UsePage();
    void *HandleRequest(void *arg);
    
    //命令格式:./server server_port
    int main(int argc, char *argv[])
    {
        if (argc != 2)
        {
            UsePage();
            return 1;
        }
        uint16_t port = atoi(argv[1]);
        int listen_sockfd = Sock::GetSock();
        Sock::Bind(listen_sockfd, port);
        Sock::Listen(listen_sockfd);
    
        std::cout << "server ready……" << std::endl;
        while (true)
        {
            int new_sock = Sock::Accept(listen_sockfd);
            if (new_sock > 0)
            {
                //创建一个从属线程,处理客户请求
                int *pram = new int(new_sock);
                pthread_t pid;
                pthread_create(&pid, nullptr, HandleRequest, pram);
            }
        }
        return 0;
    }
    
    void UsePage()
    {
        std::cout << "命令格式:./server server_port" << std::endl;
    }
    void *HandleRequest(void *arg)
    {
        int sockfd = *(int *)arg;
        delete (int *)arg;
        pthread_detach(pthread_self());
    
        //接收客户端请求
        char buffer[1024 * 10];
        memset(buffer, 0, sizeof(buffer));
        ssize_t s = recv(sockfd, buffer, sizeof(buffer), 0);
    
        //成功读取到请求报文,可以构建一个响应报文给客户
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << buffer; //查看http的请求格式
            //构建响应报文
            std::string response = "http/1.1 301 Permanently moved\n";
            response += "Location: https://www.qq.com/\n";
            response += "\n";
            send(sockfd, response.c_str(), response.size(), 0);
        }
        //完成后,关闭用于处理请求的套接字文件描述符
        close(sockfd);
        return nullptr;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    上面发送的响应报文就是:
    在这里插入图片描述
    当我们运行服务器,然后在浏览器的URL输入框中输入IP地址:端口号,就会跳转到https://www.qq.com/这个网站。

    首部行字段

    上面解释了部分首部行字段,这里给出较为常见首部行字段的清单:

    • Content-Type:数据类型
    • Content-Length:(正文)实体主体的字节数
    • Location:配合3XXX状态码使用,告诉客户端接下来要去哪里访问
    • Host:客户端告诉服务器,它所请求的资源在哪一台主机上的哪一个端口
    • User-Agent:声明请求用户的操作系统、浏览器的版本信息
    • Referer:表明当前页面是从哪一个页面跳转过来的
    • Cookie:用于在客户端存储少量信息。
    • Connect:表明使用的连接方式是持续连接还是短链接。

    Cookie和Session

    HTTP协议是无状态的(HTTP服务器是无状态服务器),HTTP服务器会把每一次客户请求作为与之前任何请求都无关的独立事务,不需要记住曾经访问过的某个客户信息。无状态简化了HTTP服务器的设计,使得它更为容易支持大量并发的HTTP客户请求。
    在这里插入图片描述
    当我们进行各种页面跳转的时候,本质是浏览器进行各种请求。http协议本身是无状态的,但我们发现网站是“认识我”的,这提高了用户访问网站或者平台的体验。这项工作并不是http协议本身要解决的,但http可以提供一些技术支持,来保证网站具有“会话保持”的功能。

    这个工作由Cookie完成。

    • 对于浏览器:
      Cookie是存储在用户主机的文本文件,记录了一段时间内某用户的私密信息。
    • 对于http
      一旦该网站对应有Cookie,在发起任何请求的时候,都会在request中携带Cookie信息。

    响应报文的首部行可以包含一个字段Set-Cookie,将Cookie信息设置进浏览器的Cookie文本文件。
    在这里插入图片描述
    多次请求:
    第一次浏览器请求的时候,没有Cookie信息,所以请求时没有携带Cookie信息。
    第一次请求后,服务器将Cookie信息设置进浏览器的Cookie文件。
    在这里插入图片描述

    后续请求的时候,浏览器有Cookie信息了,请求的时候就把Cookie信息携带上
    在这里插入图片描述
    单纯地使用Cookie是有安全隐患的,如果被人盗取了Cookie文件:
    1、盗取者可以我的身份进行认证访问特定的资源。
    2、如果保存的是我们的用户名和信息,那么情况就更加糟糕。

    所以Cookie一般和Session配套使用,Session是保存在服务器上的文本文件。
    在这里插入图片描述
    每当有一个用户认证的时候服务器就会生成一个唯一的Session文件,用户保存一段时间内用户的私密信息。然后服务器将Session文件的id等设置进用户的Cookie文件。

    虽然这样避免了Cookie明文地保存用户的私密信息,但还是有被盗取的风险,虽然没有办法避免被盗取,但现在已经衍生出了许多应对的办法。例如,认证的时候,可以检查IP归属地,如果不在用户经常登录的IP归属地,就禁止登录访问/要求重新认证。

    HTTPS

    HTTPS是HTTP+TLS/SSL的结合。数据在网络中总是要进行加密的,而TLS/SSL是http数据的加密解密层。
    在这里插入图片描述

    加密方式

    有两种加密的方式:

    • (1)对称加密。
      对称加密,只有一个密钥M,使用M对数据加密,也用M解密得到数据。

    做个假设:data是传送的数据。
    加密:result = data ^ M;
    解密:data = result ^ M;
    这不是具体过程,仅仅作为一个抽象的理解。

    • (2)非对称加密。
      非对称加密,使用一对密钥:公钥私钥
      如果使用公钥对数据加密,那么就用私钥解密;如果使用私钥对数据加密,那么就用公钥解密。一般而言,公钥对于全世界是公开的,而私钥必须自己私有保存。

    如何选择加密算法?
    假设单纯使用对称加密:
    在这里插入图片描述
    这并不是安全的,因为在将密钥传给对方的时候,密钥此时是数据并且没有被加密,盗取者能够拿到密钥,并使用密钥对数据解密,那么后续的加密工作就毫无作用。

    假设单纯使用非对称加密:
    在这里插入图片描述
    在将公钥传给对方的时候,公钥此时是数据并且没有被加密,盗取者能够拿到公钥,但我们并不使用公钥解密,所以没有多大关系。这种方式数据被盗取的风险就远小于单纯使用对称加密的方式,但是它相对于前者要消耗了太多的时间。

    一种结合了上述优点的加密算法:对称加密 + 非对称加密。
    在这里插入图片描述

    只需要一对公钥+私钥,其目的是将密钥加密传给对方。后续只需要这一个密钥就可以安全地传送数据了。

    注意:
    安全的含义指,即使盗取者拿到了加密后的内容,也没有办法拿到里面的数据。

    补充知识

    如何防止文本在传输的过程中数据被篡改?以及识别到文本内容是否被篡改。

    对文本加密过程:
    在这里插入图片描述
    不同的文本,哪怕只是一些标点符号不同,形成的Hash结果都会不同。

    识别文本内容是否被篡改:
    收到文本方的校验
    在这里插入图片描述

    证书

    采用上述的对称加密+非对称加密的方式仍然是不安全的,因为可能会有中间人获取到数据。中间人就是哪些试图在别人进行网络通信中截取数据的“人”。

    在这里插入图片描述
    在密钥协商阶段,其中一方server将自己的公钥放在报文内,试图传送给另一方client。这个过程报文内的数据是没有被加密的,所以中间人可以截取到报文,拿到公钥G并将原本要发送的公钥G,修改成了中间人自己的公钥G2, 这样client拿到的就是公钥G2而且它对此并不知情。client方拿到公钥G2后就开始使用公钥G2对密钥进行加密,然后把含有加密后的密钥的报文发送给另一方server。这个过程中,中间人就可能会将该报文截取,并用中间人自己的私钥S2对数据解密,拿到client方的密钥。中间人再用之前截取到的公钥G对密钥重新加密,并重新封装成报文发送给server方,server方对此也并不知情。密钥都被中间人拿到了!!! 那么数据就不安全了。

    上面情况的本质就是:client无法判定发来的密钥协商报文是不是从合法的服务方发来的,也无法判定发来的密钥协商报文有没有被其他人修改过。

    针对这种可能出现的问题引入了证书

    证书由名叫“CA”的机构负责签发、认证和管理,所以人们也将证书称为CA证书
    证书的流程:
    在这里插入图片描述

    • 第一步:服务方向CA机构申请证书。

    • 第二部:CA机构根据服务方的基本信息创建一个证书。例如,域名、公钥等。
      创建证书的过程如下:
      在这里插入图片描述
      CA机构有自己的公钥和私钥,使用CA自己的私钥对数据摘要加密形成数字签名。CA的私钥只有它自己知道,换而言之,整个世界上只有CA能够重新形成对应的数字签名。

    • 第三步:将生成的证书颁发给申请的服务方。

    证书能够保证数据的安全的原理:
    在这里插入图片描述
    client能够对数字签名解密,是因为它有CA的公钥(数字签名用CA的私钥加密,而解密用CA的公钥)。
    1、CA的公钥一般是内置的。
    2、访问网址的时候,浏览器会提示用户进行安装。

  • 相关阅读:
    c++类对象的初始化笔记(1)
    如何用Redis实现分布式锁?
    RDD算子介绍
    渗透测试之Web安全系列教程(二)
    Java数据类型
    Django笔记二十一之使用原生SQL查询数据库
    第 4 章 串(串的堆分配存储实现)
    SICP:求值和环境模型(Python实现)
    txt文件恢复如何操作?详细图文教程
    vue3使用西瓜播放器播放flv、hls、mp4视频
  • 原文地址:https://blog.csdn.net/qq_56870066/article/details/126641712