计算机网络(六) —— http协议详解

news2024/9/20 7:53:53

目录

一,预备知识

1.1 关于域名

1.2 关于URL

1.3 urlencode和urldecode

二,关于http

2.1 什么是http

2.2 http协议格式

2.2.1 网络协议栈

2.2.2 http请求协议格式*

2.2.3 http响应协议格式*

三,http细节字段

3.1  http方法

3.2 状态码

3.3 http常见的Header

3.4 http版本

3.5 Cookie和Session

四,http服务器代码优化

4.1 支持显示图片等其它数据

4.2 逻辑梳理


一,预备知识

1.1 关于域名

  • 我们一般访问一个网站的时候,不是直接拿着“点分十进制”的IP和端口去访问的,比如我要访问“百度”,是通过https://www.baidu.com去访问的,而想要访问一个服务,只需要知道IP地址和端口号就可以了,但是在日常中,我们一般不直接使用IP,而是使用域名这样的东西
  • 其实域名本身和技术相关的东西关联程度并不大,只是相比较IP地址的数字,比较容易记,而且我们也不能让用户直接拿IP去搞,这样用户体验会非常差
  • 域名会被域名解析服务解析成IP地址的,这个我们不用关心,浏览器会帮我们搞好

总结:域名只是为了方便用户访问,其最终访问服务器还是要用IP地址的 

1.2 关于URL

URL(Uniform Resource Lacator),称为统一资源定位符,本质是一个字符串,也就是我们常说的网址,如名字一样,能够在互联网当中精确定位唯一的某种资源(这里的资源指的是文本数据,音频数据,视频数据等)

我们来介绍一下上面的东西:

  • 协议方案名:表示使用的协议,通常为http和https,后面会介绍
  • 登录信息:一般由登录用户名密码两部分构成,可以通过URL传给服务器,但是大家都懂,这样会直接暴露,不安全,所以一般通过其它方式提交登录信息
  •  服务器地址:这个就是我们前面讲的域名,是将指定服务器的IP做映射后得到的一个字符串,能方便用户访问
  • 服务器端口号:我们在浏览器输入一个IP之后,默认使用的就是http或https,http默认绑定的端口是80,https默认绑定的是443是固定的;但是我们用户一般不自己传端口,因为大部分知名网站,它们的端口一般是众所周知的,就像110,119等紧急电话一样,必须是严格的一对一匹配,所以我们访问一些知名网站时,不带端口号,但是浏览器依旧会知道端口号,浏览器在请求里会默认添加端口号
  • 带层次的文件路径:访问服务器目的是获取服务器上的某种资源,所以文件路径就是干这个的,找打服务器上对应路径的资源(而且很多的路径分隔符是 / ,这说明还是Linux做服务器的多,Windows的少)
  • 查询字符串:表示请求时额外提供的参数,以键值对的形式,用户可以通过url向服务器传输数据
  • 片段标识符:对资源的补充,了解一下即可

1.3 urlencode和urldecode

以上面的图为例,URL里面也有很多特殊字符的;但是如果我们用户自己输入的关键字当中出现类似 /?: 这样的字符,为了避免URL字符串解析冲突,浏览器会对我们搜索关键字的特殊字符进行转义,如下图:

转义规则

  • 将需要转码的字符转为十六进制,然后从左到右,取4位(不足4位直接处理),没两位做一位,前面加上 % ,编码成 %XY 的格式
  • 比如上面的图, + 转为十六进制后为 0x2B ,所以一个 + 号被转成 %2B 

总结:上网行为就两种,把我的资源传上去,把别人的资源拿过来;我们自己上传的符号可能回合URL某些字符冲突,所以浏览器会自己做编码

二,关于http

2.1 什么是http

HTTP(Hyper Text Transfer Protocol),又叫做超文本传输协议,底层是Tcp协议,是一个简单的“请求-响应”协议

我们前面已经实现了一个网络版计算器的自定义协议:计算机网络(五) —— 自定义协议简单网络程序-CSDN博客

所以我们可以自己定制协议,但是互联网发展了这么多年,许多优秀的工程师和程序员早已经搞出了许多非常优秀的应用层协议,并经过这么多年的维护,许多协议已经非常成熟,我们直接用就行,而其中应用最广泛,最典型的就是HTTP协议 

2.2 http协议格式

2.2.1 网络协议栈

 网络协议栈四层的常用协议如上,其中下三层是由操作系统或者驱动帮我们完成的,主要负责通信细节。如果应用层不考虑下三层,那么在应用层看来,它就可以任务是自己在和对方的应用层通信

HTTP是基于“请求-响应”的应用层服务,客户端可以向服务器发起请求,服务器收到请求之后,会根据http协议解析请求,然后就会知道你想要访问什么资源,然后服务器再构建响应,完成这一次HTTP请求

所以,学习HTTP的请求格式和响应格式,是我们学习HTTP的重点

2.2.2 http请求协议格式*

http请求协议简略图如下: 

请求行

  • 请求方法Method:有很多,但我们主要讲GET和POST方法,为获取和上传,因为这两个方法占所有方法使用率的95%以上
  • url:就是我们前面讲的url,我们的url是通过http协议报头传给服务器的
  • http版本:http Version,为协议的版本,最常见的是1.1,长连接,1.0就是短连接

请求报头: 

  • 都是以key : value 的形式按行陈列的,都是http为了应对各种情况,需要和双方协商的字段

空行

  • 该行的主要作用,就是服务器在读取报文的时候,入股遇到了空行,说明报头读取完毕,所以空行能够将报头和有效载荷分离

请求正文

  • 请求正文一般是用户相关信息或数据,如果用户没有要上传给服务器的信息,请求正文就是空字符串
  • 如果有信息要上传,在请求报头中会有一个Content-Length,表示正文的长度,这样就能读取到完整的正文了

说多了也没用,下面我们直接写代码

既然你说http请求报文是这样那样,那它到底是个啥样?我们下面来写一个简单的服务器代码,使用浏览器访问,当浏览器访问我们的服务器时,就会发送http请求报文,而我们的服务器就能接收到报文,从而打印出来,如下代码:

需要用到的文件为:

 其中Socket.hpp,Log.hpp,makefile三个文件前面已经实现过,这里不再赘述:计算机网络(五) —— 自定义协议简单网络程序-CSDN博客

下面是HttpServer.hpp的代码: 

#pragma once

#include "Socket.hpp"
#include "Log.hpp"
#include <iostream>
#include <string>
#include <pthread.h>

static const int defaultport = 8080;
struct ThreadData
{
    int sockfd;
};

class HttpServer
{
public:
    HttpServer(uint16_t port = defaultport)
        : _port(port)
    {
    }
    ~HttpServer()
    {
    }
    bool Start()
    {
        _listensock.Socket();    // 创建套接字
        _listensock.Bind(_port); // 绑定套接字
        _listensock.Listen();    // 监听套接字
        while (true)
        {
            std::string clientip;
            uint16_t clientport;
            int sockfd = _listensock.Accept(&clientip, &clientport); // 获取连接
            pthread_t tid;
            ThreadData *td = new ThreadData;
            td->sockfd = sockfd;
            pthread_create(&tid, nullptr, ThreadRun, td);
        }
    }
    static void *ThreadRun(void *args)
    {
        pthread_detach(pthread_self()); // 线程分离
        char buffer[10240];
        ThreadData *td = static_cast<ThreadData *>(args); // 表示把args的类型强转为THreadData
        // man 2 recv 也是可以进行Tcp套接字读取的,它的返回值和使用方法和read几乎一模一样,recv比read多了个参数,flags,标识读取方式,当flags为0时,它的作用就和read一样了
        ssize_t n = recv(td->sockfd, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << buffer; // 读到什么内容就打印什么内容
        }
        close(td->sockfd);
        delete td;
        return nullptr;
    }

private:
    Sock _listensock;
    uint16_t _port;
};

下面是HttpServer.cc的代码:

#include "HttpServer.hpp"

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        exit(1);
    }
    uint16_t port = std::stoi(argv[1]);
    // std::unique<HttpServer> svr(new HttpServer);
    HttpServer *svr = new HttpServer(port);
    svr->Start();
    return 0;
}

解释:

  • 浏览器发起HTTP请求,但是我们目前的服务器未返回任何响应,所以浏览器会认为我们没收到请求,所以浏览器会多次重新发请求,所以会打印多个http报文
  • 由于浏览器发起请求时默认是http协议,所以我们在浏览器的输入框里,不需要指名是http协议
  • 对于GET方法后面的“ / ”,很快能发现这是一个根目录,但是这个根目录不是Linux系统的根目录,是web根目录,我们可以自己设置这个根目录,当url有路径时,就从web根目录开始找资源的

2.2.3 http响应协议格式*

响应报文其实和请求报文很相似,有区别的就是状态码,这个我们后面具体讲

下面我们继续写代码,浏览器给我发了请求,所以我就要构建响应报文并返回回去,如下代码:

由于我们需要返回相应,所以我们需要知道请求报文里的内容,所以我们先定义一个请求类,类中包含分离有效载荷,和解析有效载荷两种方法:

class HttpRequest
{
public:
    void Deserialize(std::string req) // 反序列化,其实就是做字符串切割,把一个http请求字符串变成多个字符串并存起来
    {
        while (true)
        {
            size_t pos = req.find(sep); //找换行符
            if (pos == std::string::npos)
                break; // 找到结尾了,代表全部截完了
            // 开始截取字符串
            std::string temp = req.substr(0, pos);
            if (temp.empty())
                break;                      // 碰到了空行,代表当前报文的报头反序列化完成
            req_header.push_back(temp);     // 把每一个截取到的数据搞到vector里面去
            req.erase(0, pos + sep.size()); // 把原来http第一行的内容去掉,这样下次再找的时候就找第二行,这样找一行移一行,就能在遇到空行前把数据全搞到数组里面去
        }
        text = req; //报头全截取完删完后,剩下的就是请求正文
    }
    void Parse() // 解析
    {
        std::stringstream ss(req_header[0]); // req_header数组保存着第一行字符串,使用stringstream需要以空格为分隔符,以流的方式拆成4个字符串
        ss >> method >> url >> http_version; // 这样就分开了,而且顺序不能改,这是http的规定
        file_path = wwwroot;
        // 我们需要保证开始访问的路径是从web根目录开始的
        if (url == "/" || url == "/index.html") // 如果请求的是“ / ”也就是首页,就加上首页的web地址
        {
            file_path += "/";
            file_path += homepage; // 最后变成 ./wwwroot/index.html
        }
        else //如果url是其它地址就加上其它地址
        {
            file_path += url; //  /a/b/c/d.html -> ./wwwroot/a/b/c/d.html
        }
        //文件都是二进制数据,但是不同的文件有不同的二进制序列,所以需要告诉浏览器,这个文件是什么格式的,后面再讲,这里先放着
        auto pos = file_path.rfind("."); // 从后往前找点,截取文件后缀
        if (pos == std::string::npos)
        {
            suffix = ".html";
        }
        else
        {
            suffix = file_path.substr(pos); // 从点的位置往后截取直到结尾,拿到文件后缀
        }
    }
    // 测试打印反序列化结果
    void DebugPrint()
    {
        for (auto &line : req_header)
        {
            std::cout << line << std::endl;
            std::cout << "---------------------" << std::endl;
        }
        std::cout << "method: " << method << std::endl;
        std::cout << "url: " << url << std::endl;
        std::cout << "http_version: " << http_version << std::endl;
        std::cout << "file_path: " << file_path << std::endl;
        std::cout << text << std::endl;
    }

public:
    std::vector<std::string> req_header; // 请求行
    std::string text;                    // 请求正文

    // 存储解析之后的结果
    std::string method; //请求得方法
    std::string url; //请求的url
    std::string http_version; // 请求的http版本
    std::string file_path;
    std::string suffix; // 文件后缀
};

当请求报文解析完成后,接下来要做的就是构建响应报文:

  • 构建响应报文,我们需要自己构建响应报头,自己加上状态行和响应报头等信息,并且以“ \r\n ”隔开
  • 然后就是把资源放到报头后面,这里我们返回一个浏览器页面,也就是html文件,浏览器会解析这个html文件,然后显示页面,这属于前端的知识
  • 还有就是,如果请求中的url里的路径,在我们的服务器上并不存在,所以就应该返回一个404页面,这个大家应该都知道

所以我们就在当前目录下建立一个“wwwroot”目录作为我们的web根目录,再建立 index.html 首页文件和 err.html 404页面,然后再建立几个其它的测试文件,这个直接从我的gitee下载即可:

hello.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <form action="./a/b/hello.html" method="get"> 
        name:<input type="text" name="name"><br>
        password:<input type="password" name="passwd"><br>
        <input type="submit" value="提交">
    </form>
</body>

</html>

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        a {
            color: blue;
            text-decoration: none;
        }

        a:hover {
            text-decoration: underline;
        }

        table {
            width: 536px
        }

        .title .col-1 {
            font-size: 20px;
            font-weight: bolder;
        }

        .col-1 {
            width: 80%;
            text-align: left;
            /*居左*/
        }

        .col-2 {
            width: 20%;
            text-align: center;
        }

        .icon {
            background-image: url(./male.png);
            width: 24px;
            height: 24px;
            background-size: 100% 100%;
            display: inline-block;
            /*加上后图片才能显示出来*/
            vertical-align: bottom;
            /*使垂直对齐*/
        }

        .content {
            font-size: 18px;
            line-height: 30px;
        }

        .content .col-1,
        .content .col-2 {
            border-bottom: 2px solid #f3f3f3;
        }

        .num {
            font-size: 20px;
            color: #fffff3;
        }

        .first {
            background-color: #f54545;
            padding-right: 8px;
        }

        .second {
            background-color: #ff8547;
            padding-right: 8px;
        }

        .third {
            background-color: #ffac38;
            padding-right: 8px;
        }

        .other {
            background-color: #81b9f5;
            padding-right: 8px;
        }
    </style>
</head>

<body>
    <table cellspacint="0px">
        <th class="title col-1">热搜</th>
        <th class="title col-2"><a href="./a/b/hello.html">登录<span class="icon"></span></a></th>
        <tr class="content">
            <td class="col-1"><span class="num first">1</span><a
                    href="https://github.com/"
                    target="blank">GitHub</a>
            </td>
            <td class="col-2">666万</td>
        </tr>
        <tr class="content">
            <td class="col-1"><span class="num second">2</span><a href="https://www.csdn.net/"
                    target="blank">CSDN</a></td>
            <td class="col-2">666万</td>
        </tr>
        <tr class="content">
            <td class="col-1"><span class="num third">3</span><a href="https://gitee.com/"
                    target="blank">Gitee</a></td>
            <td class="col-2">666万</td>
        </tr>
        <tr class="content">
            <td class="col-1"><span class="num other">4</span><a href="https://leetcode.cn/"
                    target="blank">LeetCode</a></td>
            <td class="col-2">666万</td>
        </tr>
        <tr>
            <td>
                <a href="./image.html" target="blank">你好</a>
            </td>
        </tr>
    </table>
</body>

</html>

image.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <img src="/image/1.png" alt="你好" weigh="800px" width="800px">
    <img src="/image/2.png" alt="你好" weigh="800px" width="800px">
    <img src="/image/3.png" alt="你好" weigh="800px" width="800px">
</body>

</html>

err.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <h1>404 Not Found</h1>
    <h21>您好,您访问的页面不存在</h21>
</body>

</html>

然后就是针对HttpServer.hpp的修改了:

#pragma once

#include "Socket.hpp"
#include "Log.hpp"
#include <iostream>
#include <string>
#include <pthread.h>
#include <vector>
#include<sstream>
#include<fstream>

class HttpServer;

const std::string wwwroot = "./wwwroot"; // web根目录
// 配置文件里面就是一堆的路径,服务开始时以配置文件来初始化根目录
const std::string sep = "\r\n";
const std::string homepage = "index.html";

static const int defaultport = 8080;
class ThreadData
{
public:
    ThreadData(int fd, HttpServer *s)
        : sockfd(fd), svr(s)
    {
    }
    int sockfd;
    HttpServer *svr;
};

class HttpRequest
{
public:
    void Deserialize(std::string req) // 反序列化,其实就是做字符串切割,把一个http请求字符串变成多个字符串并存起来
    {
        while (true)
        {
            size_t pos = req.find(sep); //找换行符
            if (pos == std::string::npos)
                break; // 找到结尾了,代表全部截完了
            // 开始截取字符串
            std::string temp = req.substr(0, pos);
            if (temp.empty())
                break;                      // 碰到了空行,代表当前报文的报头反序列化完成
            req_header.push_back(temp);     // 把每一个截取到的数据搞到vector里面去
            req.erase(0, pos + sep.size()); // 把原来http第一行的内容去掉,这样下次再找的时候就找第二行,这样找一行移一行,就能在遇到空行前把数据全搞到数组里面去
        }
        text = req;
    }
    void Parse() // 解析
    {
        std::stringstream ss(req_header[0]); // req_header数组保存着第一行字符串,使用stringstream需要以空格为分隔符,以流的方式拆成4个字符串
        ss >> method >> url >> http_version; // 这样就分开了,而且顺序不能改,这是http的规定
        file_path = wwwroot;
        // 我们需要保证开始访问的路径是从web根目录开始的
        if (url == "/" || url == "/index.html") // 如果请求的是首页
        {
            file_path += "/";
            file_path += homepage; // 最后变成 ./wwwroot/index.html
        }
        else
        {
            file_path += url; //  /a/b/c/d.html -> ./wwwroot/a/b/c/d.html
        }
        auto pos = file_path.rfind("."); // 从后往前找点,截取文件后缀
        if (pos == std::string::npos)
        {
            suffix = ".html";
        }
        else
        {
            suffix = file_path.substr(pos); // 从点的位置往后截取直到结尾,拿到文件后缀
        }
    }
    // 测试打印反序列化结果
    void DebugPrint()
    {
        for (auto &line : req_header)
        {
            std::cout << line << std::endl;
            std::cout << "---------------------" << std::endl;
        }
        std::cout << "method: " << method << std::endl;
        std::cout << "url: " << url << std::endl;
        std::cout << "http_version: " << http_version << std::endl;
        std::cout << "file_path: " << file_path << std::endl;
        std::cout << text << std::endl;
    }

public:
    std::vector<std::string> req_header; // 请求行
    std::string text;                    // 请求正文

    // 存储解析之后的结果
    std::string method; //请求得方法
    std::string url; //请求的url
    std::string http_version; // 请求的http版本
    std::string file_path;
    std::string suffix; // 文件后缀
};

class HttpServer
{
public:
    HttpServer(uint16_t port = defaultport)
        : _port(port)
    {
    }
    ~HttpServer()
    {
    }
    bool Start()
    {
        _listensock.Socket();    // 创建套接字
        _listensock.Bind(_port); // 绑定套接字
        _listensock.Listen();    // 监听套接字
        while (true)
        {
            std::string clientip;
            uint16_t clientport;
            int sockfd = _listensock.Accept(&clientip, &clientport); // 获取连接
            if (sockfd < 0)
                continue;
            pthread_t tid;
            ThreadData *td = new ThreadData(sockfd, this); //把this指针传过去,使静态成员方法能够访问类内成员
            pthread_create(&tid, nullptr, ThreadRun, td);
        }
    }

static std::string ReadHtmlContent(const std::string &htmlpath) //获取url中的地址中的文件资源
    {
        std::ifstream in(htmlpath, std::ios::binary); // 以二进制方式来读
        if (!in.is_open())
            return "";
        // 以字符串方式读,传文本还好,传图片视频等二进制数据时,就不行了,所以要在上面ifstream的第二个参数带上std::ios::binary,表示以二进制方式来读
        // std::string line;
        // std::string content;
        // while (std::getline(in, line)) // 第一个是流,第二个是string,从流读到line里面
        // {
        // content += line;
        // }
        // C++读取二进制大小
        //读取前需要知道文件的大小,先把文件读写位置放到结尾,就可以得到大小,然后再把文件读写位置放到开始
        in.seekg(0, std::ios_base::end); // 将文件读写开始位置定位到结尾
        auto len = in.tellg();           // 返回文件大小
        in.seekg(0, std::ios_base::beg); // 再把文件开始位置返回开头

        std::string content;
        content.resize(len); //把缓冲区大小调整为文件的大小

        in.read((char *)content.c_str(), content.size()); // 二进制读

        in.close();
        return content;
    }

    void HandlerHttp(int sockfd) //构建响应,发回去浏览器
    {
        char buffer[10240];
        // man 2 recv 也是可以进行Tcp套接字读取的,它的返回值和使用方法和read几乎一模一样,recv比read多了个参数,flags,标识读取方式,当flags为0时,它的作用就和read一样了
        ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            buffer[n] = 0;
            // 假设我们读取到的就是一个完整的,独立的http请求
            std::cout << buffer << std::endl;
            // 读到什么内容就打印什么内容
            HttpRequest req;
            req.Deserialize(buffer); //解析有效载荷
            req.Parse();
            //req.DebugPrint(); //可以测试响应报文构建成功不成功

            // 请求解析完成之后,返回响应
            std::string text;
            bool ok = true;
            text = ReadHtmlContent(req.file_path); // 将指定路径下的文件的内容返回给text,就是我们前面构建的html文件的内容
            if (text.empty())                      // 如果读取的内容不存在,就返回一个404页面
            {
                ok = false;
                std::string err_html = wwwroot;
                err_html += "/";
                err_html += "err.html";

                text = ReadHtmlContent(err_html); //讲响应正文变成404页面的内容
            }
            std::string response_line;
            if (ok)
            {
                response_line = "HTTP/1.0 200 OK\r\n"; //①添加响应行,包含协议名,协议版本,和状态码
            }
            else
            {
                response_line = "HTTP/1.0 404 Not Found\r\n";
            }
            std::string response_header = "Content-Length: "; // ②添加响应报头
            response_header += std::to_string(text.size());   // 报头添加正文的长度
            response_header += sep;

            std::string response = response_line;
            response += response_header; //加报头
            response += sep;
            response += text; // 加有效载荷
            //最后到这里后就是一个完整的报文了

            // 把消息发回去 man 2 send
            send(sockfd, response.c_str(), response.size(), 0);
        }
        close(sockfd);
    }
    static void *ThreadRun(void *args)
    {
        pthread_detach(pthread_self()); // 线程分离
        char buffer[10240];
        ThreadData *td = static_cast<ThreadData *>(args); // 表示把args的类型强转为THreadData
        td->svr->HandlerHttp(td->sockfd);
        delete td;
        return nullptr;
    }

private:
    Sock _listensock;
    uint16_t _port;
};

 就能显示我们刚刚写的html页面了,“你好”选项是显示图片的,但是目前显示不出来,我们后面解释

三,http细节字段

3.1  http方法

有很多:

方法

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

95%都是GET和POST这两个方法,然后这95%当中80是GET,20是POST

问题:我们日常访问某些网站的时候,是如何把我们的数据提交给服务器的呢?

解答:

就拿我们上面的hello.html为例,是一个输入账号和密码的页面,而在代码中,我们使用的方法是get:

 而我们输入账号密码后,再来看服务器收到的报文:

可以发现,我们的数据是直接嵌套进URL再传给服务器的,是通过“表单”提交的

 但是我们一般不用URL传参数,原因有:

  • 当参数过多时,URL就会很长,不便于服务器解析
  • URL会直接暴露出去,有安全问题 

所以我们可以用post方法,就是讲我们的数据放在请求正文里传给服务器:

 很多人说GET不安全,其实POST也不安全,这个和加不加密有关系,所以POST比GET安全一些,但总体都不安全,具体要看主页面对数据做的加密

3.2 状态码

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

 最常见的就是404 not found,403就是Forbidden,无访问权限,504就是Bad Gateway,服务器错误,比如服务器收到一个请求,然后要创建一个线程在提供服务,但是这个线程创建失败了,叫做服务器内部出错

其它的都好理解,但是3开头的重定向状态码,我们需要解释一下:

  • 重定向就是通过各种方法讲网络请求重新定位转到其它位置,此时这个服务器相当于提供一个引路的服务
  • 分为“临时重定向(302,307)”和“永久重定向(301)”,本质是影响客户端的标签,决定客户端是否需要更新目标地址
  • 如果某个网站是永久重定向,那么第一次访问该网站时浏览器会帮你跳转,后续访问就是直接访问永久重定向后的网站了
  • 如果是临时重定向,则每次都要浏览器来做一次跳转工作

下面我们来演示一下重定向:

报头字段中有一条是Location字段,表明所要重定向到的目标网站:

 只需要在报头字段添加302状态码和Location就可以了

3.3 http常见的Header

  • Content-Type:数据类型(text/html等)
  • Content-Length:正文的长度
  • Host:客户端告知服务器,所请求得资源是在哪个主机得哪个端口上
  • User-Agent:用户得操作系统和浏览器版本等信息
  • Referer:当前页面是由哪个页面跳转过来的
  • Location:搭配3XX状态码用,重定向功能
  • Cookie:用于在客户端存储少量信息,通常用于实现会话(session)的功能

问题:客户端访问服务器,为什么还要告诉服务器它要访问的服务对应的IP和端呢?

解答:因为有些服务器提供的是代理服务,也就是代替客户端向其它服务器发起请求,所以需要Host信息 

我们用电脑或者手机去访问百度等知名网站,可以发现电脑和手机浏览器显示的页面都不一样,因为User-Agent表明着客户端的一些设备信息,所以服务器就能根据这个字段返回适配不同设备的主页面

3.4 http版本

我们目前见到的版本就是1.0和1.1,1.0提供的是短链接服务,1.1提供的是长链接服务

  • 短链接一次请求响应一个资源,关闭连接,比如一个网页要显示100张图片,所以我就要发送101个http请求,这时候服务器就要在一个时刻建立101个线程,每个线程都单独建立基于Tcp套接字的连接,传输好图片资源后再关闭连接,这样的操作要101次,效率无疑是非常低下的。
  • 长链接:所以今天就是把Tcp连接好,然后我把我要发送的多个http请求串联起来一起发给服务器,然后服务器就一个个处理,一个一个发送,但是我么只建立一次Tcp连接,发送和返回多个http的request和response,等http全部处理完后再关闭连接
  • HTTP请求报头中的Connect字段,如果值是Keep-Alive,那么就是支持长链接

问题:为什么要交互http版本

解答:主要还是为了兼容性问题,因为服务器和客户端使用的可能是不同的http版本,为了让不同版本的客户端都能受到服务,就要求双方进行版本通信

3.5 Cookie和Session

HTTP是一种无状态协议,就是HTTP的每次“请求-响应”之间是没有任何关系的。但是我们在一些网站登录之后,在往后一段时间内,都不需要再次登录,每次打开就是默认登录的。

这就是通过Cookie技术实现的,Cookie可以在浏览器上查看:

下面解释一下什么是Cookie:

  • 浏览器关掉再打开,B站不需要再次登录,电脑关机再开机后也一样,所以浏览器把我的登录状态记录了
  • 我们登录时通过post,在http正文部分把我们的账号密码交给了B站后端进行认证,再发送http给浏览器表示认证通过,再做重定向,这时候会在报头添加Set-Cookie,浏览器就会把Cookie保存起来放到一个小文件里,这个文件我们就叫做Cookie文件

浏览器保存Cookie文件有两种方法,一种是磁盘级,一种是内存级,不同的浏览器保存方式可能不一样,俗称的“盗号”就是盗取的Cookie(1,个人的Cookie被盗取由别人来冒充我的身份    2,还有个人私有信息被泄露问题)

所以对于盗号问题,服务器也有解决方案:

  • 浏览器输入账号密码,传输给服务器进行认证,服务器端为我们用户创建一个session文件,里面记录用户的登录相关的内容,里面会随机生成一个全服务器唯一的一个个序号叫做session_id
  • 然后http响应时调用Set-Cookie,然后把当前用户的Session_id写回到浏览器的Cookie文件中,之后浏览器再次发送请求会把账号密码和session_id一起传上去,之后通过session_id是否合法来同意用户登录
  • 然后服务端服务器就把上亿个session文件先描述,再组织,大部分企业都是把整个session信息同一托管给redis集群来管的
  • 但是黑客还是可以盗走你的cookie冒充你的身份,但是你的个人信息不会再泄漏了,因为你的个人信息都是交给服务器维护了,以前都是浏览器维护的用户信息
  • session_id是由服务器端生成分配的,这也代表着服务器端可以随时回收这个id,我前一分钟还在湖南登录,一分钟后我又在缅北登录了,而用户信息是由服务器维护的,所以服务器就能识别到登录地区的异常,直接在服务器端干掉你这个session_id给你重新分配,当然缅北那个也就要重新登录,而重新登录就要手机号等等,安全性就有了(服务器对用户的行为做检测)

所以我们也可以在我们服务器的响应报头加上Cookie字段:

四,http服务器代码优化

4.1 支持显示图片等其它数据

上面我们写的服务器中,网页可以正常显示,也可以正常跳转, 但是图片却无法显示,这是因为网页的html文件,其内容全部都是字符串内容,可以直接传输,浏览器也可以直接解析,但是图片等数据不一样,它是二进制数据,需要以特定方式去排列,所以我们服务器也需要告诉浏览器数据的类型

所以就要用到报头的Content-Type字段,表示文件的类型,所以我们服务器需要做的步骤有两个:

  • 取得请求文件的后缀
  • 将后缀与类型做映射,并添加到响应报头中返回给浏览器

对于不同的文件后缀,Content-Type也有对应的表示:常见 content-type对应表_xlsx contenttype-CSDN博客

 我们先可以在构造函数中,添加我们的映射:

然后就是获取文件后缀并映射返回的函数:

 std::string SuffixToDesc(const std::string &suffix) // 把读取到的文件后缀通过映射转化成Content-type所需要的参数
    {
        auto iter = content_type.find(suffix);
        if (iter == content_type.end())
        {
            return content_type[".html"]; // 没找到默认返回 text/html
        }
        else
        {
            return content_type[suffix]; // 返回映射后的参数
        }
    }

 最后,就是将类型添加进报头中:

完整代码在后面 

4.2 逻辑梳理

完整代码:

#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <fstream>
#include <vector>
#include <sstream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unordered_map>

#include "Socket.hpp"
#include "Log.hpp"

const std::string wwwroot = "./wwwroot"; // web根目录
// 配置文件里面就是一堆的路径,服务开始时以配置文件来初始化根目录
const std::string sep = "\r\n";
const std::string homepage = "index.html";

static const int defaultport = 8080;

class HttpServer;

class ThreadData
{
public:
    ThreadData(int fd, HttpServer *s)
        : sockfd(fd), svr(s)
    {
    }
    int sockfd;
    HttpServer *svr;
};

class HttpRequest
{
public:
    void Deserialize(std::string req) // 反序列化,其实就是做字符串切割,把一个http请求字符串变成多个字符串并存起来
    {
        while (true)
        {
            size_t pos = req.find(sep); //找换行符
            if (pos == std::string::npos)
                break; // 找到结尾了,代表全部截完了
            // 开始截取字符串
            std::string temp = req.substr(0, pos);
            if (temp.empty())
                break;                      // 碰到了空行,代表当前报文的报头反序列化完成
            req_header.push_back(temp);     // 把每一个截取到的数据搞到vector里面去
            req.erase(0, pos + sep.size()); // 把原来http第一行的内容去掉,这样下次再找的时候就找第二行,这样找一行移一行,就能在遇到空行前把数据全搞到数组里面去
        }
        text = req;
    }
    void Parse() // 解析
    {
        std::stringstream ss(req_header[0]); // req_header数组保存着第一行字符串,使用stringstream需要以空格为分隔符,以流的方式拆成4个字符串
        ss >> method >> url >> http_version; // 这样就分开了,而且顺序不能改,这是http的规定
        file_path = wwwroot;
        // 我们需要保证开始访问的路径是从web根目录开始的
        if (url == "/" || url == "/index.html") // 如果请求的是首页
        {
            file_path += "/";
            file_path += homepage; // 最后变成 ./wwwroot/index.html
        }
        else
        {
            file_path += url; //  /a/b/c/d.html -> ./wwwroot/a/b/c/d.html
        }
        auto pos = file_path.rfind("."); // 从后往前找点,截取文件后缀
        if (pos == std::string::npos)
        {
            suffix = ".html";
        }
        else
        {
            suffix = file_path.substr(pos); // 从点的位置往后截取直到结尾,拿到文件后缀
        }
    }
    // 测试打印反序列化结果
    void DebugPrint()
    {
        for (auto &line : req_header)
        {
            std::cout << line << std::endl;
            std::cout << "---------------------" << std::endl;
        }
        std::cout << "method: " << method << std::endl;
        std::cout << "url: " << url << std::endl;
        std::cout << "http_version: " << http_version << std::endl;
        std::cout << "file_path: " << file_path << std::endl;
        std::cout << text << std::endl;
    }

public:
    std::vector<std::string> req_header; // 请求行
    std::string text;                    // 请求正文

    // 存储解析之后的结果
    std::string method; //请求得方法
    std::string url; //请求的url
    std::string http_version; // 请求的http版本
    std::string file_path;
    std::string suffix; // 文件后缀
};

class HttpServer
{
public:
    HttpServer(uint16_t port = defaultport)
        : _port(port)
    {
        content_type.insert({".html", "text/html"}); // 初始化,有很多,这里只插入这两个哈
        content_type.insert({".png", "image/png"});  // 如果有需求就通过配置文件去搞
        //如果要返回更多形式的文件,比如视频,就继续加
    }
    ~HttpServer()
    {
    }
    bool Start()
    {
        _listensock.Socket();    // 创建套接字
        _listensock.Bind(_port); // 绑定套接字
        _listensock.Listen();    // 监听套接字
        while (true)
        {
            std::string clientip;
            uint16_t clientport;
            int sockfd = _listensock.Accept(&clientip, &clientport); // 获取连接
            if (sockfd < 0)
                continue;
            pthread_t tid;
            ThreadData *td = new ThreadData(sockfd, this);
            pthread_create(&tid, nullptr, ThreadRun, td);
        }
    }

    static std::string ReadHtmlContent(const std::string &htmlpath)
    {
        std::ifstream in(htmlpath, std::ios::binary); // 以二进制方式来读
        if (!in.is_open())
            return "";
        // 以字符串方式读,传文本还好,传图片视频等二进制数据时,就不行了,所以要在上面ifstream的第二个参数带上std::ios::binary,表示以二进制方式来读
        // std::string line;
        // std::string content;
        // while (std::getline(in, line)) // 第一个是流,第二个是string,从流读到line里面
        // {
        // content += line;
        // }
        // C++读取二进制大小
        //读取前需要知道文件的大小,先把文件读写位置放到结尾,就可以得到大小,然后再把文件读写位置放到开始
        in.seekg(0, std::ios_base::end); // 将文件读写开始位置定位到结尾
        auto len = in.tellg();           // 返回文件大小
        in.seekg(0, std::ios_base::beg); // 再把文件开始位置返回开头

        std::string content;
        content.resize(len); //把缓冲区大小调整为文件的大小

        in.read((char *)content.c_str(), content.size()); // 二进制读

        in.close();
        return content;
    }

    std::string SuffixToDesc(const std::string &suffix) // 把读取到的文件后缀通过映射转化成Content-type所需要的参数
    {
        auto iter = content_type.find(suffix);
        if (iter == content_type.end())
        {
            return content_type[".html"]; // 没找到默认返回 text/html
        }
        else
        {
            return content_type[suffix]; // 返回映射后的参数
        }
    }

    void HandlerHttp(int sockfd) //构建响应,发回去浏览器
    {
        char buffer[10240];
        // man 2 recv 也是可以进行Tcp套接字读取的,它的返回值和使用方法和read几乎一模一样,recv比read多了个参数,flags,标识读取方式,当flags为0时,它的作用就和read一样了
        ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            buffer[n] = 0;
            // 假设我们读取到的就是一个完整的,独立的http请求
            std::cout << buffer << std::endl;
            // 读到什么内容就打印什么内容
            HttpRequest req;
            req.Deserialize(buffer); //解析有效载荷
            req.Parse();
            // req.DebugPrint();

            // 读取成功之后,返回响应
            std::string text;
            bool ok = true;
            text = ReadHtmlContent(req.file_path); // 将指定路径下的文件的内容返回给text
            if (text.empty())                      // 如果读取的内容不存在,就返回一个404页面
            {
                ok = false;
                std::string err_html = wwwroot;
                err_html += "/";
                err_html += "err.html";

                text = ReadHtmlContent(err_html);
            }
            std::string response_line;
            if (ok)
            {
                response_line = "HTTP/1.0 200 OK\r\n"; //①添加响应行
            }
            else
            {
                response_line = "HTTP/1.0 404 Not Found\r\n";
            }
            // response_line = "HTTP/1.0 302 Found\r\n"; //当用户访问一个页面时,但是可能这个页面已经弃用,所以使用302状态码。就可以直接跳转到其它的指定地址
            std::string response_header = "Content-Length: "; // ②添加响应报头
            response_header += std::to_string(text.size());   // 报头添加正文的长度
            response_header += "\r\n";
            response_header += "Content-Type: "; //图片是二进制的,就是这个响应报文的有效载荷本身是二进制的,所以我们需要告诉浏览器,这个二进制是什么类型的数据,就通过这个参数来告诉浏览器
            response_header += SuffixToDesc(req.suffix); // 根据后缀转化为该属性的内容,使浏览器能识别其它后缀的文件
            response_header += "\r\n";
            response_header += "Set-Cookie: name=haha&&passwd=12345";
            response_header += "\r\n";

            //response_header += "Location: https://www.baidu.com\r\n";

            std::string response = response_line;
            response += response_header; //加报头
            response += sep;
            response += text; // 加有效载荷
            //最后到这里后就是一个完整的报文了

            // 把消息发回去 man 2 send
            send(sockfd, response.c_str(), response.size(), 0);
        }
        close(sockfd);
    }

    static void *ThreadRun(void *args)
    {
        pthread_detach(pthread_self());                   // 线程分离
        ThreadData *td = static_cast<ThreadData *>(args); // 表示把args的类型强转为THreadData
        td->svr->HandlerHttp(td->sockfd); //将响应发回浏览器
        delete td;
        return nullptr;
    }

private:
    Sock _listensock;
    uint16_t _port;
    std::unordered_map<std::string, std::string> content_type;
};

下面是各种函数和成员变量的大致作用:

#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <vector>
#include "Socket.hpp"

class HttpServer;
class HttpRequest
{
public:
    void Deserialize(std::string req); //将http报头的各种字段分别截取存储起来,并分离请求正文
    void Parse(); //对分离出来的请求报文做解析,方便后续服务器处理
public:
    std::vector<std::string> req_header; // 请求行
    std::string text;                    // 请求正文

    // 存储解析之后的结果
    std::string method; //请求的方法
    std::string url; //请求的url
    std::string http_version; // 请求的http版本
    std::string file_path; //访问的文件路径
    std::string suffix; // 文件后缀
};

class HttpServer
{
public:
    bool Start(); //包含套接字基本流程,创建线程,执行线程函数
    static std::string ReadHtmlContent(const std::string &htmlpath); //获取url路径中的文件的数据
    std::string SuffixToDesc(const std::string &suffix); //获取资源文件的后缀
    void HandlerHttp(int sockfd); //响应主函数,接收反序列化并解析请求,构建响应报头和响应正文,将两个合为http响应报文,并发给客户端
    static void *ThreadRun(void *args); //线程函数
private:
    Sock _listensock;
    uint16_t _port;
};

逻辑梳理:

  • 首先服务器收到http请求,首先对其做反序列化,并且解析请求报头中的各个字段
  • 然后根据url提供的地址,获取web根目录下的文件大小,内容,后缀,然后都拼成一个长字符串,然后再发给客户端,所以主要逻辑其实很简单

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2135007.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《A++ 敏捷开发》- 26 根与翼

中国社会一直非常强调家庭价值观&#xff0c;希望实现家族的持续传承&#xff0c;家族有族谱&#xff0c;代代相传的关系对每个家庭成员的成长产生深远影响。我们每个人都只是人类进化过程中的短暂过渡。父母普遍希望把最好的东西传承给下一代。然而我们需要问自己&#xff0c;…

【Go】Go语言中延迟函数、函数数据的类型、匿名函数、闭包等高阶函数用法与应用实战

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

每日处理250亿个事件,Canva如何应对数据洪流

在这个数据被称为“新石油”的时代&#xff0c;如何有效地处理海量信息流显得尤为重要。作为广受欢迎的设计平台&#xff0c;Canva不仅因其用户友好的界面而备受关注&#xff0c;还因其高效利用Amazon Kinesis管理每日高达250亿个事件而成为热议焦点。让我们深入探讨Canva是如何…

【案例70】invalid secrity token(null)

问题现象 系统登录时提示invalid secrity token(null) 问题分析 排查发现令牌种子没有配置或被人为修改 解决方案 1、登录环境。代码路径下bin下有个sysconfig.bat。左侧选“系统配置”。右侧点“安全”。读取保存一下。 2、或者找一个好用的环境。把ierp/bin下的prop.xml文…

Redis简介、常用命令及优化

文章目录 一、关系数据库​​与非关系型数据库概述1. 关系型数据库2. 非关系型数据库3.关系数据库与非关系型数据库区别 二、Redis简介1.Redis的单线程模式2.Redis 优点3.Redis 缺点 三、安装redis四、Redis 命令工具五、Redis 数据库常用命令六、Redis 多数据库常用命令七、Re…

【算法专题--回文】最长回文子串 -- 高频面试题(图文详解,小白一看就懂!!)

目录 一、前言 二、题目描述 三、预备知识 &#x1f95d; 什么回文串 &#xff1f; 四、题目解析 五、总结与提炼 六、共勉 一、前言 最长回文子串 这道题&#xff0c;可以说是--回文专题 --&#xff0c;最经典的一道题&#xff0c;也是在面试中频率最高…

哈希表和双向链表实现LRU

LRU&#xff08;Least Recently Used&#xff09;即最近最少使用&#xff0c;是一种内存管理算法。最近在Linux的缓冲区管理也看到了使用LRU算法&#xff0c;即利用哈希表进行 O(1) 复杂度的快速查找&#xff0c;利用双向链表&#xff08;里面的元素是缓冲头&#xff09;对缓冲…

再次进阶 舞台王者 第八季完美童模全球赛代言人【吴浩美】赛场+秀场超燃合集

7月20-23日&#xff0c;2024第八季完美童模全球总决赛在青岛圆满落幕。在盛大的颁奖典礼上&#xff0c;一位才能出众的少女——吴浩美迎来了她舞台生涯的璀璨时刻。 代言人——吴浩美&#xff0c;以璀璨童星之姿&#xff0c;优雅地踏上完美童模盛宴的绚丽舞台&#xff0c;作为开…

【趣学Python算法100例】兔子产子

问题描述 有一对兔子&#xff0c;从出生后的第3个月起每个月都生一对兔子。小兔子长到第3个月后每个月又生一对兔子&#xff0c;假设所有的兔子都不死&#xff0c;问30个月内每个月的兔子总对数为多少&#xff1f; 题目解析 兔子产子问题是一个有趣的古典数学问题&#xff0c…

Office关闭安全提示

每次启动都要提示这个&#xff0c;怎么关&#xff1f;

大数据-135 - ClickHouse 集群 - 数据类型 实际测试

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

Nuxt Kit 自动导入功能:高效管理你的模块和组合式函数

title: Nuxt Kit 自动导入功能:高效管理你的模块和组合式函数 date: 2024/9/14 updated: 2024/9/14 author: cmdragon excerpt: 通过使用 Nuxt Kit 的自动导入功能,您可以更高效地管理和使用公共函数、组合式函数和 Vue API。无论是单个导入、目录导入还是从第三方模块导入…

GMB外链是什么?

GMB外链其实就是百万外链&#xff0c;它是一种通过大量反向链接来提升网站页面权重的方法。如果你刚建了一个新网站&#xff0c;想在短时间内被收录并获得排名&#xff0c;GMB外链能帮你做到这点。它不像传统SEO那样需要等待好几个月的效果&#xff0c;GMB外链能在24小时内帮你…

vector(2)

前言 通过上一节的学习&#xff0c;我们知道了vector中可以存放各种类型的数据&#xff0c;这就意味着vector之中不仅仅可以存放int、char等内置类型&#xff0c;还可以存放vector和string等类型&#xff0c;我们结合底层的具体情况来具体分析 vector的复用&#xff08;套娃&a…

光控资本:股票增发是什么意思?股票增发的形式?

股票增发配售是已上市的公司通过指定投资者&#xff08;如大股东或组织投资者&#xff09;或全部投资者额定发行股份搜集资金的融资办法。 留意&#xff1a;股票增发后&#xff0c;股价会除权下降。由于增发后股本扩大了&#xff0c;那么每股收益与每股净资产均下降&#xff0…

今天一次讲明白C++条件变量

在C中&#xff0c;std::condition_variable 条件变量是一个同步原语&#xff0c;它允许一个或多个线程在某个条件成立时&#xff0c;被另一个线程唤醒。std::condition_variable 条件变量通常与互斥锁&#xff08;std::mutex&#xff09;一起使用&#xff0c;以保护共享数据和同…

David Baker 任科学顾问,初创公司发布世界最大蛋白质相互作用数据库,已获 8 轮融资

蛋白质-蛋白质相互作用 (Protein-Protein Interactions, PPI) 是细胞生命活动的重要组成部分&#xff0c;在调控和维持细胞的生理功能中&#xff08;如细胞的信号传导、代谢反应和基因表达&#xff09;发挥着不可或缺的作用。 然而目前 PPl 数据库中的数据相对较少&#xff0c…

穿越病毒区-第15届蓝桥省赛Scratch中级组真题第2题

[导读]&#xff1a;超平老师的《Scratch蓝桥杯真题解析100讲》已经全部完成&#xff0c;后续会不定期解读蓝桥杯真题&#xff0c;这是Scratch蓝桥杯真题解析第187讲。 如果想持续关注Scratch蓝桥真题解读&#xff0c;可以点击《Scratch蓝桥杯历年真题》并订阅合集&#xff0c;…

CCF201912_1

题解&#xff1a; #include<bits/stdc.h>using namespace std;int n;bool shouldSkip(int num) {if (num % 7 0){return true;}while (num > 0){if (num % 10 7){return true;}num / 10;}return false; } int main() {scanf("%d", &n);int b[4] { 0…

Android Studio 安装配置教程(Windows最详细版)

目录 前言 Android Studio 下载 Android Studio 安装 Android Studio 使用 一、创建默认项目&#xff08;Compose&#xff09; 二、创建常规项目 三、使用ViewBinding 四、查看Gradle版本、SDK版本、JDK版本 ① Gradle版本 ② SDK版本 ③ JDK版本 前言 Android开发…