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

发布于:2022-12-25 ⋅ 阅读:(250) ⋅ 点赞:(0)

应用层概述

应用层是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

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连接端口
SMTP 25
POP3 110
IMAP 143

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 <iostream>
#include <cstring>
#include <cstdlib>
#include "sock.hpp"
#include <pthread.h>
#include <unistd.h>

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;
}
//Sock.hpp文件
#pragma once
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

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;
        }
    }
};

有了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 <iostream>
#include <cstring>
#include <cstdlib>
#include "sock.hpp"
#include <pthread.h>
#include <unistd.h>
#include <fstream>
#include <sys/stat.h>

#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 += "<html><p>The resource you want to access does not exist</p></html>";
            //发送响应报文
            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;
}

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

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

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

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

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

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

总结二(区别):

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

状态码

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

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

#include <iostream>
#include <cstring>
#include <cstdlib>
#include "sock.hpp"
#include <pthread.h>
#include <unistd.h>
#include <fstream>
#include <sys/stat.h>

#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;
}

上面发送的响应报文就是:
在这里插入图片描述
当我们运行服务器,然后在浏览器的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、访问网址的时候,浏览器会提示用户进行安装。


网站公告

今日签到

点亮在社区的每一天
去签到