【Linux网络编程八】实现最简单Http服务器(基于Tcp套接字)

Ⅰ.Http请求和响应格式

1.请求格式

http请求由4部分构成:请求行,请求报头,空行,请求正文。每个部分都以‘\r\n’结束。

1.请求行:由Method URL HTTP version \r\n构成。也就是由请求方法,请求的资源,http的版本组成。
2.请求报头:里面存放的都是一些请求属性值,并且以Key:Values的形式存储。并且每种属性之间也是用’\r\n’分割。比如里面有一个Content-Length字段,表明的请求正文的长度是多少。
3.空行:单纯的空行,直接存储’\r\n’
4.请求正文:就是客户端要上传的内容,可以没有。
在这里插入图片描述

【注意】

这其实是我们在电脑上看到的格式,但在内存中,虽然每个部分都以’\r\n’结束,但’\r\n’也是字符。所以一个http请求就是一个字符串,只不过这个长字符串中间有很多个’\r\n’.
不过打印出来是多行的。

在这里插入图片描述

【空行的作用是什么?】

如何准确将报头和有效载荷分离?通过添加一个空行。空行之前就是整体报头,之后就是有效内容

【服务器如何读取一个完整的报文?】
1.在tcp缓冲区里,对整个宇符串(可能包含好几个请求)分析,那么你能确定服务器端读取报文时,就可以准确的能够读取到完整的一个报文吗?

不能!只能保证可以读取到完整的报头,因为如果读取到空行,那么空行之前的就是报头而之后的正文部分无法确定多长

2.那http是如何确保能够读取到一个完整的报文的?

①.首先完整的读取到一个报头
②.根据报头里的,报文属性Content-Length,得到正文部分的长度,然后从空行之后按照长度读取。

只有能保证服务器端读完整了,才可以进行网络序列化和反序列化。

示例:
在这里插入图片描述

2.响应格式

http响应也有4部分构成:状态行,响应报头,空行,响应正文,每个部分都以‘\r\n’结束。

1.状态行:由HTTP Version 状态码 状态码描述 ‘\r\n’组成,是描述本次请求的状态。
2.响应报文:与请求报头一样,里面是一些响应报文的一些属性,以Key: Value的形式存储。
3.空行:单纯’\r\n’。
4.响应正文:就是要请求的资源,比如图片,网站等。
在这里插入图片描述
【注意】
客户端在读取时,也无法确保能够准确的读取到完整的一个报文,所以需要添加一个空行。响应属性里也具有响应正文的长度。这样就可以读取到一个完整的报文了。

示例:
在这里插入图片描述
总结:
在这里插入图片描述

3.http中请求格式中细节字段

  • 1.1 HTTP请求中的请求方法

在这里插入图片描述

浏览器提交数据给服务器都是通过表单提交的!
在这里插入图片描述

  • 1.2 GET与POST的区别
    如果你今天想去啊?获取网页你就只用get方法就可以好不管是图片视频音频啊,我们就用get方法呃,但是如果你想把你的参数提交给服务端那么此时你可以选择post方法也可以可以选择get.
    实际上,我们通常是通过一个叫做表单的东西来提交参数的,然后我们去提参的时候,如果我们对应的表单提参方法采用的是get方法,那么其中它对应的参数呢,就会拼接到我们url的后面。然后用?作为分隔符,然后呢左侧是你要访问的资源右侧是呃右侧是你要提交的参数。参数参数之间用&分割。

在这里插入图片描述在这里插入图片描述

POST方法在提交参数时,不是通过url来提交的,而是通过正文部分将参数提交给服务器的。
在这里插入图片描述
所以POST方法也支持参数提交,采用请求的正文提交参数!
【总结】
在这里插入图片描述

4.http中响应格式中细节字段

Http响应的状态码:

在这里插入图片描述
最常见的状态码,比如200(OK),404(Not Found),403(Forbidden),302(Redirect,重定向),504(Bad Gateway)。
404错误本质就是因为客户端发起的请求不合理。

HTTP常见Header:

1.Content-Type:数据类型(text/html等)Content-Length: Body的长度
2.Host:客户端告知服务器,所请求的资源是在哪个主机的哪个端口上;
3.User-Agent:声明用户的操作系统和浏览器版本信息:
4.referer:当前页面是从哪个页面跳转过来的:
5.location:搭配3xx状态码使用,告诉客户端接下采要去哪里访回;
6.Cookie:用于在客户端存储少量信息.通常用于实现会话(session)的功能;**

Ⅱ.域名ip与URL

Ⅲ.web根目录

第一组知识:

就是我们将来收到一个请求的时候,我们给别人进行响应,响应的时候,我们自己要添加我们自己默认的一些报头还有它的一些状态码了协议版本了,空行了这些字段.我们的网页,图片,也就是资源呢,其实是会被拼接到响应的正文部分!

第二组知识:

那么别人请求的时候呢?他通常会通过在请求里面包含他要请求什么网页,他会包含我要请求什么样的资源,把路径呢通过url给我们呈现出来,那么所以呢,我们的服务器就可以获取到路径,就可以动态的根据路径去挑选,他想要的资源,而我们的资源其实都是存放在一个个的文件里。

在这里插入图片描述在这里插入图片描述

【web根目录本质】

我在我的服务器进程路径下存在一个wwwroot的文件夹好,这是什么意思呢?将来我想把我的所写的所有的网页所有的图片所有的视频包括我的网站的首页全放在这个目录里面好.以目录以树状结构的形式,从这个目录开始让别人去访问我们把这个目录下的文件,这叫做web根目录.本质就是存放资源的一个统一路径。

http协议呢,它其实,一定要有自己的web根目录,这个web根目录可以是linux的根目录也可以由你自己去指定。

【浏览器(Client)指定路径下将请求的文件内容】
在这里插入图片描述
【存在问题】
客户端不管请求什么目录文件,都访问的是wwwroot目录下html文件的内容所以我们想让客户端请求什么目录文件,服务器就返回该目录下的文件内容,该如何做呢?

必须解析请求行中的url

Ⅳ.Http服务器是如何工作的?

重点:我们要理解一个http服务器,它是怎么工作的,http服务器它无非就是在对http请求的内容在进行,我们对应的解析。

我们的重点是什么呢?重点是你要去理解http的请求,它无非就是通过套接字,我们(http服务器)读到了它的http的请求,读到了它的请求然后按照我们http的上面的这个格式(反序列化),把它给它解析出来提取url然后呢,我们给它构建响应把响应再给它返回去此时,这个就叫做http协议。

我们的浏览器访问服务器的本质实际上是把我们的服务器上的指定路径下的网页资源获取到浏览器当中然后由浏览器进行解释。最后就看到了网页效果。

【总结】
其实所有的协议本质都是对‘字符串′做分析处理

一.获取请求

从网络里获取到的请求是字符串形式的,但字符串形式服务器并不认识,需要反序列化成结构体(客户端和服务器端都认识的结构体)
在这里插入图片描述

二.分析请求

2.1反序列化

将读取到的字符串分割构成服务器认识的请求格式,也就是最上面讲到到http请求格式,由请求行,请求报头,空行,请求报文组成,我们需要构建一个request类。

这个工作。其实就是把他的请求做字符串做分析,按行做分析(反序列化)。把一整个字符串分割成若干个字符串。
分析完之后之后呢?然后我们再把正文部分直接也拿出来,把它的每一行也都分别push到vector里就可以了(构建初始化req对象)最后直接将正文部分放进text中。

class HttpRequest
{
public:
   void Deserialize(std::string req)//反序列化将字符串分割构建结构体对象
   {
    while(true)
    {
       std::size_t pos=req.find(seq);
       if(pos==std::string::npos)break;
       std::string temp =req.substr(0,pos);
       if(temp.empty())break;
       req_header.push_back(temp);
       req.erase(0,pos+seq.size());
    }
    text=req;
public:
  std::vector<std::string> req_header;
  std::string text;
  //服务器分析的结果
  std::string method;//请求方法
  std::string url;//请求资源
  std::string http_version;//http版本
  std::string resource_path;//资源路径
  std::string suffix;//文件后缀
};

【注意】

存在问题:默认从套接字里读取到的就是一个完整的报文没有做处理

2.2解析url

解析url的目的就是为了让服务器返回客户端所指定的资源(客户端通过url形式指定资源路径)
就是将url中的资源路径resource_path给获取到。

而进一步解析:解析请求行(在vector容器中的第一个元素)

void Split()
   {
    std::stringstream ss(req_header[0]);
    ss>>method>>url>>http_version;
    resource_path=wwwtew;
    if(url=="/"||url=="/index.html")
    {
        resource_path+="/";
        resource_path+=homepage;

    }
    else
    {
        resource_path+=url;
    }
}

如果服务器收到的这个http请求他的url只是一个斜杠,这其实表明的就是他要请求我们的web根目录那么此时,我们只会把整个网站的叫做首页给你返回那么,这个首页呢,我们就叫做index.html

在这里插入图片描述

【跳转网页】
在这里插入图片描述

三.构建响应

服务器获取到请求后,分析请求,将请求的资源,以HTTP响应的方式返回给客户端。

3.1构建响应正文

先将浏览器请求的资源构建出来,因为服务器已经解析出url中资源路径,所以服务器直接可以通过资源路径定位到对应的文件中,并从文件中读取出来,存放在正文中。

服务器动态读取资源文件,构建响应正文

 static std::string ReadHtmlContent(const std::string &htmlpath)
     {
        //这里存在一个坑--二进制读取存在问题
        //std::ifstream in(htmlpath);
        std::ifstream in(htmlpath,std::ios::binary);
        if(!in.is_open()) return " ";//如果打开文件失败说明客户端的请求不合理返回空
        
        //获取文件的大小
        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());
        /*std::string content;
        std::string line;
        while(std::getline(in,line))
        {
            content+=line;
        }
        */

        in.close();
  
        return content;
        
     }

3.2构建响应状态

①404错误

当服务器读取资源文件时,可能会读取失败,因为客户端不合理的请求,请求不存在的资源。这时服务器就会访问到空资源。

当服务器访问到空资源时,我们就让浏览器再重新请求404网页资源,也就是服务器将404网页资源返回给浏览器。
在这里插入图片描述

   std::string text;
        bool ok =true;
        text=ReadHtmlContent(req.resource_path);//可能会读取失败,因为客户端不合理的请求,请求不存在的资源
        if(text.empty())//访问不存在的资源,就让浏览器申请404网页资源
        {
            ok=false;
            std::string err_html=wwwtew;
            err_html+="/";
            err_html+="err.html";//404网页资源
            text=ReadHtmlContent(err_html);
        }
        std::string response_line;//并将状态码设置成404

        if(ok)
        response_line="HTTP/1.0 200 OK\r\n";
        else
        response_line="HTTP/1.0 404 Not Found\r\n";

err.html:


<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404 Not Found</title>
<style>
body {
text-align: center;
padding: 150px;
}
h1 {
font-size: 50px;
}
body {
font-size: 20px;
}
a {
color: #008080;
text-decoration: none;
}
a:hover {
color: #005F5F;
text-decoration: underline;
}
</style>
</head>
<body>
<div>
<h1>404</h1>
<p>页面未找到<br></p>
<p>
您请求的页面可能已经被删除、更名或者您输入的网址有误。<br>
请尝试使用以下链接或者自行搜索:<br><br>
<a href="https://www.baidu.com">百度一下></a>
</p>
</div>
</body>
</html>
②302重定向

3XX重定向原理(依赖Location字段)
1.重定向的意思:让服务器指导我们的浏览器访问新的地址,这个新的地址就存在location属性字段中。
2.3XX对应的意思
第一我要访问的资源,服务器无法给我提供对应服务。
第二,我们对应的这个要去的新地址呢,有location指定。
所以你的这个浏览器它会二次发起请求,二次发起请求的时候,那么,这个时候,它请求的就不再是这个了服务器了,而是根据它给location的地址重新构建http请求,去访问新的地址,

在这里插入图片描述

其实就是http response通过3XX这样的状态码,然后配合location让浏览器发起二次请求就可以了

3.3构建响应报头

①Content-Length正文长度
        std::string response_header="Content-length: ";//构建响应报头 
        response_header+=std::to_string(text.size());
        response_header+="\r\n";
②Connection:keep-alive长短链接
  • 短链接

一个巨大的页面是会包含非常多的元素的!每一个元素就是一个资源!

一个完整的网页了,每一张图片就是一个文件资源,所以要让浏览器得到这个图片就要发起一次http请求,所以一个网页上面有100张图片,浏览器会先.
获取这个网页,获取这个网页的同时呢?我们这个网页内部可能还需要包含很多很多的图片,那么,浏览器可能需要再二次三次十次八次继续发起请求好,也就是说对我们今天我们的服务器来讲呢,要获取一个100张图片网页我们要发起101次http的请求。就可能一瞬间就要创建几十,甚至上百个线程来把每一个资源都推送给我们的浏览器然后让让它呢帮我们把我们对应的资源呢返回返回到我们的浏览器让它去渲染。

这种基于请求一次,响应一次然后关闭链接这种我们称之为叫做短链接响应(http/1.0)一次请求只拿回来一个资源,然后关闭链接
在这里插入图片描述

  • 长连接

建立一个TCP连接,发送和返回多个http的request和response。这个链接在本轮请求没有完全结束之前链接不关闭,本轮请求比如,你把一个网页该要的100个资源全拿到了本轮请求全部完毕,那么此时,该链接再断开这种我们称之为叫做长链接(http/1.1),一次请求拿回来多个资源。

Connection: keep-alive
所以呢,当我们通信双方在进行最开始几次那么进行http请求时,服务器和客户端双方是需要,协商http版本的,协商http版本来证明,我们双方是否支持叫做长链接技术然后呢?但是在协商,我们都支持支持之后,我可以不用呀。那么所以怎么样能证明,我们2个都要选择使用长链接呢?
那么此时就有connection选项叫做keep alive选项它表明的是我们是否在双方通信时,选择使用长链接,当客户端和服务器connection选项都是keep alive时双方即会采用长链接的方案来进行通信

③Content Type报头属性

根据短链接,我们知道,如果一个网页中包含图片时,浏览器在访问这个网页时,首先会发起链接访问网页资源,然后识别到还有图片,则会自动发起二次链接获取图片资源。然后将图片资源也显示出来。
在这里插入图片描述
但浏览器请求的资源非常多啊,并且我的服务器上呢,这个图片呢,有png的也有jpg的,那么就是服务器怎么知道浏览器要请求的是什么类型的文件呢?因为只有它的请求的文件的类型确定了,服务器才好填我们的Content type,这样将响应返回时,浏览器才能按照对应的格式显示出来。
该怎么做呢?图片的类型完全根据文件的后缀!
所以服务器只要找到对应的图片的文件路径,并将文件的后缀提取出来,就可以找到浏览器想要什么类型的图片了,就可以将图片类型填写到Content-Type属性中。

两件工作
第一件工作,需要我(服务器)能够在读取你的请求时,就已经知道你的http请求的资源的后缀是什么。通过url路径中资源路径就可以找到。
在这里插入图片描述

第二个工作,要维护了一个简单的映射表。存储的是文件后缀对应的Content-Type类型。(利用unordermap做一个content的映射表)

在这里插入图片描述

        response_header+="Content-Type: ";
        response_header+=SuffixToDesc(req.suffix);
        response_header+="\r\n";

[问题]
在这里插入图片描述

④Set-Cookie会话保存

http协议呢?本身它是没有状态的,请求什么资源就是什么资源。

应用场景:访问B站
浏览器你发的任意一个链接请求都有要你登录验证,为什么?
因为间每一个资源都要到对你进行身份认证,因为我们必须得是登录验证状态才能问某些资源,那么我们登入B站后,再访问n多个视频(n个资源)时为什么都不需要你再登入验证呢?

也就B站服务器怎么知道我一直是处于登录状态的呢。这就是我们的会话保持的功能的一个概念。

Set-Cookie原理
第一次登入B站时,访问B站登入页面服务器,服务器如果发现你的请求报文里面没有携带cookie信息,此时,服务器就直接跳转到登录页面让你去登录验证.但是呢,只要你曾经注注册并登录过,然后你的浏览器请求一次之后呢认证通过之后,就会把密码和账号信息就写到了这个浏览器的cookie文件往后每次请求都会携带cookie,所以我们此时b站就会自动进行认证了
一自动携带,二自动认证所以从此往后你就不需要再去输入什么账号和密码。

验证:

        response_header+="Set-Cookie: name=tao&&passwd=123456789";
        response_header+="\r\n";

第一次访问服务器将对应的cookie返回,则浏览器会将cookie信息保存到cookie文件中,等下次请求时,就默认会将cookie信息带上。
在这里插入图片描述

【cookie的保存方法】
我们对应的浏览器要保存这个cookie文件,它通常有两种保存方法第一种是文件级,第2种是内存级。

意思就是说浏览器,它收到了这个cookie,因为浏览器本身是一个进程,那这个进程内部是可以new malloc空间的,所以呢它收到它登入成功之后,它就可以把cookie信息就残留在浏览器进程的这个内存当中,它不往磁盘上写。
所以这个时候呢,你不关浏览器,你可以一直访问你一旦把浏览器关掉了。那么,你再把浏览器重新打开再访问b站你又要重新登录了好这是内存级.

如果你的浏览器它设置的cookie文件的保存呢,是直接把它保存到文件当中,这个文件呢,本身就是浏览器在浏览器的特定的安装路径下就会存在这样的文件文件级的cookie那么文件级的cookie呢,大家都知道文件呢,是在磁盘当中的所以你把浏览器即便关掉了好即便你关掉了,你照样可以不用登录了好这就是文件级。

四.返回响应

       std::string blank_line="\r\n";//构建空白行
        std::string response=response_line;//添加状态行
        response+=response_header;//添加响应报头
        response+=blank_line;//添加空行
        response+=text;//添加响应正文
        
        write(sockfd,response.c_str(),response.size());//发送给客户端

Ⅵ.实现最简单Http服务器

HttpServer.hpp

#include <iostream>
#include <pthread.h>
#include "Socket.hpp"
#include "Log.hpp"
#include <sstream>
#include <string>
#include <fstream>
#include <vector>
#include <unordered_map>
Log log;
const uint16_t defaultport=8888;
const std::string wwwtew="./wwwtew";
const std::string homepage="index.html";
const std::string seq="\r\n";
class HttpServer;
class pthreadData
{
public:
    pthreadData(int sockfd,HttpServer*s):_sockfd(sockfd),svr(s)
    {}
public:

    int _sockfd;
    HttpServer* svr;
};
class HttpRequest
{
public:
   void Deserialize(std::string req)//反序列化将字符串分割构建结构体对象
   {
    while(true)
    {
       std::size_t pos=req.find(seq);
       if(pos==std::string::npos)break;
       std::string temp =req.substr(0,pos);
       if(temp.empty())break;
       req_header.push_back(temp);
       req.erase(0,pos+seq.size());
    }
    text=req;
   }
   void Split()
   {
    std::stringstream ss(req_header[0]);
    ss>>method>>url>>http_version;
    resource_path=wwwtew;
    if(url=="/"||url=="/index.html")
    {
        resource_path+="/";
        resource_path+=homepage;

    }
    else
    {
        resource_path+=url;
    }
    //分割出资源路径后,就可以获取对应的文件后缀
    auto pos=resource_path.rfind(".");
    if(pos==std::string::npos)suffix=".html";//默认是html类型
    else suffix=resource_path.substr(pos);

   }
    void DebugPrint()
    {
        for(auto &line : req_header)
        {
            std::cout << "--------------------------------" << std::endl;
            std::cout << line << "\n\n";
        }

        std::cout << "method: " << method << std::endl;
        std::cout << "url: " << url << std::endl;
        std::cout << "http_version: " << http_version << std::endl;
         std::cout << "resource_path: " << resource_path << std::endl;
        std::cout << text << std::endl;
    } 
public:
  std::vector<std::string> req_header;
  std::string text;
  //服务器分析的结果
  std::string method;
  std::string url;
  std::string http_version;
  std::string resource_path;
  std::string suffix;//文件后缀
};
class HttpServer
{
public:
     HttpServer(uint16_t port=defaultport):_port(port)
     {

     }
     bool Start()
     {
      _listensock.Socket();

      _listensock.Bind(_port);

      _listensock.Listen();

      for(;;)
      {
        std::string clientip;
        uint16_t clientport;
        int sockfd=_listensock.Accept(&clientip,&clientport);
        log(Info,"connect success sockfd:%d",sockfd);
        pthreadData* td=new pthreadData(sockfd,this);
        pthread_t tid;
        pthread_create(&tid,nullptr,ThreadFunc,td);

      }  

     }
    static std::string ReadHtmlContent(const std::string &htmlpath)
     {
        //这里存在一个坑--二进制读取存在问题
        //std::ifstream in(htmlpath);
        std::ifstream in(htmlpath,std::ios::binary);
        if(!in.is_open()) return " ";//如果打开文件失败说明客户端的请求不合理返回空
        
        //获取文件的大小
        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());
        /*std::string content;
        std::string line;
        while(std::getline(in,line))
        {
            content+=line;
        }
        */

        in.close();
  
        return content;
        
     }

    //获取文件后缀对应的类型 
    std::string SuffixToDesc(const std::string &suffix)
    {
        auto iter=suffix_type.find(suffix);
        if(iter==suffix_type.end())return suffix_type[".html"];
        else return suffix_type[suffix];

    }
    void HanderHttp(int sockfd)
     {
        char buffer[10240];
        //1.服务器获取客户端发起的请求(字符串形式服务器并不认识,需要反序列化成结构体)
        ssize_t n=read(sockfd,buffer,sizeof(buffer)-1);
        if(n>0)
        {
            buffer[n]=0;
            std::cout<<buffer;
            HttpRequest req;
            req.Deserialize(buffer);
            req.Split();
            //req.DebugPrint();
            //1.1服务器对请求进行解析,分析url


        //2.服务器返回响应给客户端
        //2.1构建响应
        
        //std::string text=ReadHtmlContent(req.resource_path);//构建正文 动态的从文件里读取对应的资源内容,并且根据客户端url来搜索资源

        //std::string response_line="HTTP/1.0 200 OK\r\n";//构建响应状态行

        std::string text;
        bool ok =true;
        text=ReadHtmlContent(req.resource_path);//可能会读取失败,因为客户端不合理的请求,请求不存在的资源
        if(text.empty())//访问不存在的资源,就让浏览器申请404网页资源
        {
            ok=false;
            std::string err_html=wwwtew;
            err_html+="/";
            err_html+="err.html";
            text=ReadHtmlContent(err_html);
        }
        std::string response_line;//并将状态码设置成404

        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配合location实现重定向
        
        std::string response_header="Content-length: ";//构建响应报头 
        response_header+=std::to_string(text.size());
        response_header+="\r\n";
        response_header+="Location: https://www.baidu.com\r\n";
        
        response_header+="Content-Type: ";
        response_header+=SuffixToDesc(req.suffix);
        response_header+="\r\n";
        response_header+="Set-Cookie: name=tao&&passwd=123456789";
        response_header+="\r\n";

        
        std::string blank_line="\r\n";//构建空白行
        std::string response=response_line;
        response+=response_header;
        response+=blank_line;
        response+=text;
        
        write(sockfd,response.c_str(),response.size());
        
        }
        close(sockfd);
     }
     static void *ThreadFunc(void *avgs)
     {
        pthreadData*td=static_cast<pthreadData*>(avgs);
        pthread_detach(pthread_self());
         
        td->svr->HanderHttp(td->_sockfd);  
        delete td;
        return nullptr;
     }

      ~HttpServer()
      {}
private:
    Sock _listensock;//本质是sockfd
    uint16_t _port;
    std::unordered_map<std::string,std::string> suffix_type;
};

HttpServer.cc

#include <iostream>
#include "HttpServer.hpp"
#include <memory>
using namespace std;

int main()
{
    HttpServer*svr=new HttpServer();
    svr->Start();
    
}
  • 18
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小陶来咯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值