HTTP和TCP之间的关系,POST和GET请求之间的关系

网络分层

       聊今天的话题之前,先普及个关于网络架构的分层的知识,在当前的世界中,有两套网络的参考模型,一套是OSI参考模型,一套是TCP/IP的参考模型。对于java开发而言,我们只需要知道现在用的是TCP/IP模型,这个模型分为层就可以了。

OSI参考模型Open Systems Interconnection Reference Model):包括层,这个模型过于理想化,未能在因特网中进行广泛推广。

TCP/IP参考模型:包括层,事实上的国际标准。

HTTP和TCP之间的关系

       http协议:属于应用层的协议。主要责任是去定义数据,在两台计算机相互传递信息时,http规定了每段数据以什么形式表达才是能够被另外一台计算机理解。

       tcp协议:属于传输层的协议。主要责任是数据在网络中进行传输,tcp的任务是保证连接的可靠,他只管连接,它不管连接之后要传什么数据。http的协议不一定要建立在tcp的连接上。

       互联网的模型被分为层,从上至下每一层都依赖其底层协议。换言之,Application(应用层) 的协议操作成功的前提是Transport(运输层)的存在。没有运输层就没有应用层。好比没有任何道路的前提下就没有汽车可以行驶。而这种层次上的抽象是让开发者在设定某个层面的协议时不去考虑其他层面的问题。比如我要在运输层设计协议时,我唯一要考虑的是如何将数据从一台计算机传到另外一台,我需要着重的是其稳定性效率。在解决运输层的问题时我不需要考虑传达的数据是什么类型或内容,因为这样的问题是应用层所要操心的。在上图中可以看到HTTPTCP是存在于不同层面的网络协议,所以他们之间必然存在着依赖关系。确切的说是HTTP所设定的所有规则都建立在一个假设之上,那就是运输层的协议有在正常运作

http协议的特点:

       1、http的标准建立在将两台计算机视为不同的角色:客户端和服务器。客户端会向服务器传送不同的请求(request),而服务器会对应每个请求给出回应(response)。

       2、 http属于无状态协议(Stateless)。这表示每一个请求之间是没有相关性的。在该协议的规则中服务器是不会记录任何客户端操作,每一次请求都是独立的。(记录用户浏览行为会通过其他技术实现)。

       3、客户端的请求被定义在几个动词意义范围内。最长用到的是GETPOST,其他动词还包括DELETE、HEAD等等。

       4、服务器的回应被定义在几个状态码之间:5开头表示服务器错误,4开头表示客户端错误,3开头表示需要做进一步处理,2开头表示成功,1开头表示在请求被接受处理的同时提供的额外信息。

       5、不管是客户端的请求信息还是服务器的回应,双方都拥有一块头部信息(Header)。头部信息是自定义,其用途在于传递额外信息(浏览器信息、请求的内容类型、相应的语言)。

tcp协议的特点:

       1、在http的规范内,两台计算机的交互被视为requestresponse的传递。而在实际的tcp操作中,信息传递会比单纯的传递requestresponse要复杂。通过tcp建立的通讯往往需要计算机之间多次的交换信息才能完成一次requestresponse

       2、tcp的传输数据的核心是在于将数据分为若干段并将每段数据按顺序标记。标记后的顺序可以以不同的顺序被另一方接收并集成回完整的数据。计算机对每一段数据的成功接收都会做出相应,确保所有数据的完整性。

       3、tcp在传递数据时依赖于实现定义好的几个标记(Flags)去向另一方表态传达数据和连接的状态,如下所示:

* F : FIN - 结束; 结束会话
* S : SYN - 同步; 表示开始会话请求
* R : RST - 复位;中断一个连接
* P : PUSH - 推送; 数据包立即发送
* A : ACK - 应答
* U : URG - 紧急
* E : ECE - 显式拥塞提醒回应
* W : CWR - 拥塞窗口减少

       4、也正是基于这些标志tcp才可以实现三次(three ways handshake)和四次握手 (four ways tear down)。三次握手是初步建立连接的机制,而四次握手则是断开链接。两者之间大致操作是一样的,A发出建立链接(SYN)或者断开链接(FIN)的请求,B认可(ACK)其请求然后发出同样的请求给A并等待A的认可。在双方认可后,链接正式成立或者断开。

       5、为什么A发出请求并且得到认可后B还有重复同样的动作?在建立连接的过程中SYN标记代表的是一个随机序列号,因为当文件被切断的时候并不是从0或者1开始标记每段的顺序,所以双方都需要通过传递SYN来告知文件片段的第一个序列是多少号。

       6、为什么同样的机制,建立链接和断开链接需要握手的次数不同?三次和四次握手的区别在于,在建立连接时,B的ACKSYN会一起发送回A,而在断开链接时因为B发送ACK之后还要做其他处理后才能返回FIN,因此将两步拆开。

POST和GET请求之间的关系

    http最早被用来做浏览器与服务器之间交互html和表单的通讯协议;后来又被被广泛的扩充到接口格式的定义上。所以在讨论GETPOST区别的时候,需要现确定下到底是浏览器使用的GET/POST,还是用HTTP作为接口传输协议的场景。

浏览器的GET和POST请求

       这里特指浏览器中非AjaxHTTP请求,即从HTML和浏览器诞生就一直使用的HTTP协议中的GET/POST。浏览器用GET请求来获取一个html页面、图片cssjs等资源;用POST来提交一个<form>表单,并得到一个结果的网页。

浏览器中GET的定义:

       ”读取“一个资源。比如Get到一个html文件。反复读取不应该对访问的数据有副作用。比如”GET一下,用户就下单了,返回订单已受理“,这是不可接受的。没有副作用被称为“幂等“(Idempotent)。

        因为GET是读取,就可以对GET请求的数据做缓存。这个缓存可以做到浏览器本身上(彻底避免浏览器发请求),也可以做到代理上(如nginx),或者做到server端(用Etag,至少可以减少带宽消耗)。

浏览器中POST的定义:

       在页面里<form> 标签会定义一个表单。点击其中的submit元素会发出一个POST请求让服务器做一件事。这件事往往是有副作用的,不幂等的。不幂等也就意味着不能随意多次执行。因此也就不能缓存。比如通过POST下了一个单,服务器创建了新的订单,然后返回订单成功的界面。这个页面不能被缓存。试想一下,如果POST请求被浏览器缓存了,那么下单请求就可以不向服务器发请求,而直接返回本地缓存的“下单成功界面”,却又没有真的在服务器下单。那是一件多么滑稽的事情。

       因为POST可能有副作用,所以浏览器实现为不能把POST请求保存为书签。想想,如果点一下书签就下一个单,是不是很恐怖?

小结:

       GETPOST携带数据的格式也有区别。当浏览器发出一个GET请求时,就意味着要么是用户自己在浏览器的地址栏输入,要不就是点击了htmla标签的href中的url。所以其实并不是GET只能用url,而是浏览器直接发出的GET只能由一个url触发。所以没办法,若GET想要在url之外带一些参数就只能依靠url上附带queryString。但是HTTP协议本身并没有这个限制。

       浏览器的POST请求都来自表单提交。每次提交,表单的数据被浏览器用编码到HTTP请求的body里。浏览器发出的POST请求的body主要有有两种格式,一种是application/x-www-form-urlencoded用来传输简单的数据,大概就是"key1=value1&key2=value2"这样的格式。另外一种是传文件,会采用multipart/form-data格式。采用后者是因为application/x-www-form-urlencoded的编码方式对于文件这种二进制的数据非常低效。

       浏览器在POST一个表单时,url上也可以带参数,只要<form action="url" >里的urlquerystring就行。只不过表单里面的那些用<input> 等标签经过用户操作产生的数据都在会在body里。

       因此我们一般会泛泛的说”GET请求没有body,只有url,请求数据放在urlquerystring中;POST请求的数据在body中“。但这种情况仅限于浏览器发请求的场景。

接口中的GET和POST请求

       这里是指通过浏览器的Ajax api,或者ios/AndroidApphttp clientjavacommons-httpclient/okhttp或者是curlpostman之类的工具发出来的GETPOST请求。此时GET/POST不光能用在前端和后端的交互中,还能用在后端各个子服务的调用中(即当一种RPC协议使用)。尽管RPC有很多协议,比如thriftgrpc,但是http本身已经有大量的现成的支持工具可以使用,并且对人类很友好,容易debugHTTP协议在微服务中的使用是相当普遍的。

       当用HTTP实现接口发送请求时,就没有浏览器中那么多限制了,只要是符合HTTP格式的就可以发。HTTP请求的格式,大概是这样的一个字符串(为了美观,我在\r\n后都换行一下):

<METHOD> <URL> HTTP/1.1\r\n
<Header1>: <HeaderValue1>\r\n
<Header2>: <HeaderValue2>\r\n
...
<HeaderN>: <HeaderValueN>\r\n
\r\n
<Body Data....>

       其中的<METHOD>可以是GET也可以是POST,或者其他的HTTP Method,如PUTDELETEOPTION……。从协议本身看,并没有什么限制说GET一定不能没有bodyPOST就一定不能把参放到<URL>queryString上。因此可以更加自由的去利用格式。比如Elastic Search_search api就用了带bodyGET;也可以自己开发接口让POST一半的参数放在urlqueryString里,另外一半放body里;你甚至还可以让所有的参数都放Header里——可以做各种各样的定制,只要请求的客户端和服务器端能够约定好。

       当然,太自由也带来了另一种麻烦,开发人员不得不每次讨论确定参数是放urlpath里、queryString里、body里、header里这种问题,太低效了。于是就有了一些接口规范/风格。其中名气最大的当属RESTREST充分运用GETPOSTPUTDELETE,约定了这4个接口分别获取创建替换删除“资源”,REST最佳实践还推荐在请求体使用json格式。这样仅仅通过看HTTPmethod就可以明白接口是什么意思,并且解析格式也得到了统一。

json相对于x-www-form-urlencoded的优势在于
  1)可以有嵌套结构。 
  2)可以支持更丰富的数据类型。通过一些框架,json可以直接被服务器代码映射为业务实体。用起来十分方便。但是如果是写一个接口支持上传文件,那么还是multipart/form-data格式更合适。

       RESTGETPOST不是随便用的。在REST中, 【GET】 + 【资源定位符】被专用于获取资源或者资源列表,比如:

GET http://foo.com/books          获取书籍列表
GET http://foo.com/books/:bookId  根据bookId获取一本具体的书

       与浏览器的场景类似,REST GET也不应该有副作用,于是可以被反复无脑调用。浏览器(包括浏览器的Ajax请求)对于这种GET也可以实现缓存(如果服务器端提示了明确需要Caching);但是如果用非浏览器,有没有缓存完全看客户端的实现了。当然,也可以从整个App角度,也可以完全绕开浏览器的缓存机制,实现一套业务定制的缓存框架。

       REST POST】+ 【资源定位符】则用于“创建一个资源”,比如下面的例子。这里你就能留意到浏览器中用来实现表单提交的POST,和REST里实现创建资源的POST语义上的不同。

POST http://foo.com/books
{
  "title": "大宽宽的碎碎念",
  "author": "大宽宽",
  ...
}

       顺便讲下REST POSTREST PUT的区别。有些api是使用PUT作为创建资源的MethodPUTPOST的区别在于,PUT的实际语义是“replace”。REST规范里提到PUT的请求体应该是完整的资源,包括id在内。比如上面的创建一本书的api也可以定义为:

PUT http://foo.com/books
{
  "id": "BOOK:affe001bbe0556a",
  "title": "大宽宽的碎碎念",
  "author": "大宽宽",
  ...
}

       服务器应该先根据请求提供的id进行查找,如果存在一个对应id的元素,就用请求中的数据整体替换已经存在的资源;如果没有,就用“把这个id对应的资源从【空】替换为【请求数据】“。直观看起来就是“创建”了。

       与PUT相比,POST更像是一个“factory”,通过一组必要的数据创建出完整的资源。至于到底用PUT还是POST创建资源,完全要看是不是提前可以知道资源所有的数据(尤其是id),以及是不是完整替换。比如对于AWS S3这样的对象存储服务,当想上传一个新资源时,其id就是“ObjectName”可以提前知道;同时这个api也总是完整的replace整个资源。这时的apiPUT的语义更合适;而对于那些id是服务器端自动生成的场景,POST更合适一些。

关于安全性

       我们常听到GET不如POST安全,因为POSTbody传输数据,而GETurl传输,更加容易看到。但是从攻击的角度,无论是GET还是POST都不够安全,因为HTTP本身是明文协议每个HTTP请求和返回的每个byte都会在网络上明文传播,不管是url,header还是body。这完全不是一个“是否容易在浏览器地址栏上看到“的问题。

       为了避免传输中数据被窃取,必须做从客户端到服务器的端端加密。业界的通行做法就是https——即用SSL协议协商出的密钥加密明文的http数据。这个加密的协议和HTTP协议本身相互独立。如果是利用HTTP开发公网的站点/App,要保证安全,https是最最基本的要求。

当然,端端加密并不一定非得用https。比如国内金融领域都会用私有网络,也有GB的加密协议SM系列。但除了军队,金融等特殊机构之外,似乎并没有必要自己发明一套类似于ssl的协议。

       回到HTTP本身,的确GET请求的参数更倾向于放在url上,因此有更多机会被泄漏。比如携带私密信息的url会展示在地址栏上,还可以分享给第三方,就非常不安全了。此外,从客户端到服务器端,有大量的中间节点,包括网关,代理等。他们的access log通常会输出完整的url,比如nginx的默认access log就是如此。如果url上携带敏感数据,就会被记录下来。但请注意,就算私密数据在body里,也是可以被记录下来的,因此如果请求要经过不信任的公网,避免泄密的唯一手段就是https。这里说的“避免access log泄漏“仅仅是指避免可信区域中的http代理的默认行为带来的安全隐患。比如你是不太希望让自己公司的运维同学从公司主网关的log里看到用户的密码吧。

       另外,上面讲过,如果是用作接口,GET实际上也可以带bodyPOST也可以在url上携带数据。所以实际上到底怎么传输私密数据,要看具体场景具体分析。当然,绝大多数场景,用POST + body里写私密数据是合理的选择。一个典型的例子就是“登录”:

POST http://foo.com/user/login
{
  "username": "dakuankuan",
  "passowrd": "12345678"
}

       安全是一个巨大的主题,有由很多细节组成的一个完备体系,比如返回私密数据的maskXSSCSRF跨域安全前端加密钓鱼salt,…… POSTGET在安全这件事上仅仅是个小角色。因此单独讨论POSTGET本身哪个更安全意义并不是太大。只要记得一般情况下,私密数据传输用POST + body就好

关于编码:

       常见的说法有,比如GET的参数只能支持ASCII,而POST能支持任意binary,包括中文。但其实从上面可以看到,GETPOST实际上都能用urlbody。因此所谓编码确切地说应该是httpurl用什么编码,body用什么编码。

       先说下urlurl只能支持ASCII的说法源自于RFC1738,实际上这里规定的仅仅是一个ASCII的子集[a-zA-Z0-9$-_.+!*'(),]。它们是可以“不经编码”在url中使用。比如尽管空格也是ASCII字符,但是不能直接用在url里。

Thus, only alphanumerics, the special characters "$-_.+!*'(),", 
and reserved characters used for their reserved purposes may be used  unencoded within a URL.

       那这个“编码”是什么呢?如果有了特殊符号和中文怎么办呢?一种叫做percent encoding的编码方法就是干这个用的,这也就是为啥我们偶尔看到url里有一坨%16位数字组成的序列。使用Percent Encoding,即使是binary data,也是可以通过编码后放在URL上的。

       但要特别注意,这个编码方式只管把字符转换成URL可用字符,但是却不管字符集编码(比如中文到底是用UTF8还是GBK)这块早期一直都相当乱,也没有什么统一规范。比如有时跟网页编码一样,有的是操作系统的编码一样。最要命的是浏览器的地址栏是不受开发者控制的。这样,对于同样一个带中文的url,如果有的浏览器一定要用GBK(比如老的IE8),有的一定要用UTF8(比如chrome)。后端就可能认不出来。对此常用的办法是避免让用户输入这种带中文的url。如果有这种形式的请求,都改成用户界面上输入,然后通过Ajax发出的办法。Ajax发出的编码形式开发者是可以100%控制的。

不过目前基本上utf8已经大一统了。现在的开发者除非是被国家规定要求一定要用GB系列编码的场景,基本上不会再遇到这类问题了

浏览器的POST需要发两个请求吗?

       上文中的"HTTP 格式“清楚的显示了HTTP请求可以被大致分为“请求头”和“请求体”两个部分。使用HTTP时大家会有一个约定,即所有的“控制类”信息应该放在请求头中,具体的数据放在请求体里“。于是服务器端在解析时,总是会先完全解析全部的请求头部。这样,服务器端总是希望能够了解请求的控制信息后,就能决定这个请求怎么进一步处理,是拒绝,还是根据content-type去调用相应的解析器处理数据,或者直接用zero copy转发。

       比如在用Java写服务时,请求处理代码总是能从HttpSerlvetRequestgetParameter/Header/url。这些信息都是请求头里的,框架直接就解析了。而对于请求体,只提供了一个inputstream,如果开发人员觉得应该进一步处理,就自己去读取和解析请求体。这就能体现出服务器端对请求头和请求体的不同处理方式。

       举个实际的例子,比如写一个上传文件的服务,请求url中包含了文件名称,请求体中是个尺寸为几百兆的压缩二进制流。服务器端接收到请求后,就可以先拿到请求头部,查看用户是不是有权限上传,文件名是不是符合规范等。如果不符合,就不再处理请求体的数据了,直接丢弃。而不用等到整个请求都处理完了再拒绝。

       为了进一步优化,客户端可以利用HTTPContinued协议来这样做:客户端总是先发送所有请求头给服务器,让服务器校验。如果通过了,服务器回复“100 - Continue”,客户端再把剩下的数据发给服务器。如果请求被拒了,服务器就回复个400之类的错误,这个交互就终止了。这样,就可以避免浪费带宽传请求体。但是代价就是会多一次Round Trip。如果刚好请求体的数据也不多,那么一次性全部发给服务器可能反而更好。

       基于此,客户端就能做一些优化,比如内部设定一次POST的数据超过1KB就先只发“请求头”否则就一次性全发。客户端甚至还可以做一些Adaptive的策略,统计发送成功率,如果成功率很高,就总是全部发等等。不同浏览器,不同的客户端(curlpostman)可以有各自的不同的方案。不管怎样做,优化目的总是在提高数据吞吐和降低带宽浪费上做一个折衷。

       因此到底是发一次还是发N次,客户端可以很灵活的决定。因为不管怎么发都是符合HTTP协议的,因此我们应该视为这种优化是一种实现细节,而不用扯到GETPOST本身的区别上。更不要当个什么世纪大发现。

到底什么算请求体?

       看完了上面的内容后,读者也许会对“什么是请求体”感到困惑不已,比如x-www-form-endocded编码的body算不算“请求体”呢?从HTTP协议的角度,“请求头”就是Method +URL(含querystring)+ Headers;再后边的都是请求体。但是从业务角度,如果你把一次请求立即为一个调用的话。比如上面的

POST http://foo.com/books
{
  "title": "大宽宽的碎碎念",
  "author": "大宽宽",
  ...
}

Java写大概等价于:

createBook("大宽宽的碎碎念", "大宽宽");

       那么这一行函数名和两个参数都可以看作是一个请求,不区分头和体。即便用HTTP协议实现,titleauthor编码到了HTTP请求体中。JavaHttpServletRequest支持用getParameter方法获取x-www-url-form-encoded中的数据,表达的意思就是“请求“的”参数“。

       对于HTTP,需要区分【头】和【体】Http RequestHttp Response都这么区分。Http这么干主要用作

  • 对于HTTP代理
  • 支持转发规则,比如nginx先要解析请求头,拿到URL和Header才能决定怎么做(转发proxy_pass,重定向redirect,rewrite后重新判断……)
  • 需要用请求头的信息记录log。尽管请求体里的数据也可以记录,但一般只记录请求头的部分数据。
  • 如果代理规则不涉及到请求体,那么请求体就可以不用从内核态的page cache复制一份到用户态了,可以直接zero copy转发。这对于上传文件的场景极为有效。
  • ……
  • 对于HTTP服务器
  • 可以通过请求头进行ACL控制,比如看看Athorization头里的数据是否能让认证通过
  • 可以做一些拦截,比如看到Content-Length里的数太大,或者Content-Type自己不支持,或者Accept要求的格式自己无法处理,就直接返回失败了。
  • 如果body的数据很大,利用Stream API,可以方便支持一块一块的处理数据,而不是一次性全部读取出来再操作,以至于占用大量内存

       但从高一级的业务角度,我们在意的其实是【请求】和【返回】。当我们在说“请求头”这三个字时,也许实际的意思是【请求】。而用HTTP实现【请求】时,可能仅仅用到【HTTP的请求头】(比如大部分GET请求),也可能是【HTTP请求头】+【HTTP请求体】(比如用POST实现一次下单)。

       总之,这里有两层,不要混哦。

关于URL的长度

       因为上面提到了不论是GETPOST都可以使用URL传递数据,所以我们常说的“GET数据有长度限制“其实是指”URL的长度限制“。

       HTTP协议本身对URL长度并没有做任何规定。实际的限制是由客户端/浏览器以及服务器端决定的。

       先说浏览器。不同浏览器不太一样。比如我们常说的2048个字符的限制,其实是IE8的限制。并且原始文档的说的其实是“URL的最大长度是2083个字符,path的部分最长是2048个字符“。见https://support.microsoft.com/en-us/help/208427/maximum-url-length-is-2-083-characters-in-internet-explorer。IE8之后的IE URL限制我没有查到明确的文档,但有些资料称IE 11的地址栏只能输入法2047个字符,但是允许用户点击html里的超长URL。我没实验,哪位有兴趣可以试试。

       为啥要限制呢?如果写过解析一段字符串的代码就能明白,解析的时候要分配内存。对于一个字节流的解析,必须分配buffer来保存所有要存储的数据。而URL这种东西必须当作一个整体看待,无法一块一块处理,于是就处理一个请求时必须分配一整块足够大的内存。如果URL太长,而并发又很高,就容易挤爆服务器的内存;同时,超长URL的好处并不多,我也只有处理老系统的URL时因为不敢碰原来的逻辑,又得追加更多数据,才会使用超长URL

       对于开发者来说,使用超长的URL完全是给自己埋坑,需要同时要考虑前后端,以及中间代理每一个环节的配置。此外,超长URL会影响搜索引擎的爬虫,有些爬虫甚至无法处理超过2000个字节的URL。这也就意味着这些URL无法被搜到,坑爹啊。

       其实并没有太大必要弄清楚精确的URL最大长度限制。我个人的经验是,只要某个要开发的资源/api的URL长度有可能达到2000个bytes以上,就必须使用body来传输数据,除非有特殊情况。至于到底是GET + body还是POST + body可以看情况决定。

留意,1个汉字字符经过UTF8编码 + percent encoding后会变成9个字节,别算错哦。

总结:

       上面讲了一大堆,是希望读者不要死记硬背GETPOST的区别,而是能从更广的层面去看待和思考这个问题。

       最后,协议都是人定的。只要客户端和服务器能彼此认同,就能工作。在常规的情况下,用符合规范的方式去实现系统可以减少很多工作量——大家都约定好了,就不要折腾了。但是,总会有一些情况用常规规范不合适,不满足需求。这时思路也不能被规范限制死,更不要死抠RFC。这些规范也许不能处理你遇到的特殊问题。比如:

  • Elastic Search的_search接口使用GET,却用body来表达查询,因为查询很复杂,用querystring很麻烦,必须用json格式才舒服,在请求体用json编码更加容易,不用折腾percent encoding。
  • 用POST写一个接口下单时可能也要考虑幂等,因为前端可能实现“下单按键”有bug,造成用户一次点击发出N个请求。你不能说因为POST by design应该是不幂等就不管了。

       协议是死的,人是活的。遇到实际的问题时灵活的运用手上的工具满足需求就好。

参考博客:滑动验证页面https://www.cnblogs.com/smallzhen/p/12709161.htmlHTTP和TCP之间的关系_http tcp-CSDN博客GET 和 POST 到底有什么区别? - 知乎

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

快乐的小三菊

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

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

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

打赏作者

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

抵扣说明:

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

余额充值