【http学习笔记六】探索篇

【http学习笔记六】探索篇

一、Nginx:高性能的Web服务器

进程池

Nginx 是个“轻量级”的 Web 服务器,那么这个所谓的“轻量级”是什么意思呢?

“轻量级”是相对于“重量级”而言的。“重量级”就是指服务器进程很“重”,占用很多资源,当处理 HTTP 请求时会消耗大量的 CPU 和内存,受到这些资源的限制很难提高性能。

而 Nginx 作为“轻量级”的服务器,它的 CPU、内存占用都非常少,同样的资源配置下就能够为更多的用户提供服务,其奥秘在于它独特的工作模式。

在 Nginx 之前,Web 服务器的工作模式大多是“Per-Process”或者“Per-Thread”,对每一个请求使用单独的进程或者线程处理。这就存在创建进程或线程的成本,还会有进程、线程“上下文切换”的额外开销。如果请求数量很多,CPU 就会在多个进程、线程之间切换时“疲于奔命”,平白地浪费了计算时间。

Nginx 则完全不同,“一反惯例”地没有使用多线程,而是使用了“进程池 + 单线程”的工作模式。

Nginx 在启动的时候会预先创建好固定数量的 worker 进程,在之后的运行过程中不会再fork 出新进程,这就是进程池,而且可以自动把进程“绑定”到独立的 CPU 上,这样就完全消除了进程创建和切换的成本,能够充分利用多核 CPU 的计算能力。

在进程池之上,还有一个“master”进程,专门用来管理进程池。它的作用有点像是supervisor(一个用 Python 编写的进程管理工具),用来监控进程,自动恢复发生异常的worker,保持进程池的稳定和服务能力。

不过 master 进程完全是 Nginx 自行用 C 语言实现的,这就摆脱了外部的依赖,简化了Nginx 的部署和配置。


I/O 多路复用

使用多线程能够很容易实现并发处理。

但多线程也有一些缺点,除了刚才说到的“上下文切换”成本,还有编程模型复杂、数据竞争、同步等问题,写出正确、快速的多线程程序并不是一件容易的事情。

所以 Nginx 就选择了单线程的方式,带来的好处就是开发简单,没有互斥锁的成本,减少系统消耗。

那么,疑问也就产生了:为什么单线程的 Nginx,处理能力却能够超越其他多线程的服务器呢?

这要归功于 Nginx 利用了 Linux 内核里的一件“神兵利器”,I/O 多路复用接口,“大名鼎鼎”的 epoll。

Web 服务器从根本上来说是“I/O 密集型”而不是“CPU 密集型”,处理能力的关键在于网络收发而不是 CPU 计算(这里暂时不考虑 HTTPS 的加解密),而网络 I/O 会因为各式各样的原因不得不等待,比如数据还没到达、对端没有响应、缓冲区满发不出去等等。

这种情形就有点像是 HTTP 里的“队头阻塞”。对于一般的单线程来说 CPU 就会“停下来”,造成浪费。而多线程的解决思路有点类似“并发连接”,虽然有的线程可能阻塞,但由于多个线程并行,总体上看阻塞的情况就不会太严重了。

Nginx 里使用的 epoll,就好像是 HTTP/2 里的“多路复用”技术,它把多个 HTTP 请求处理打散成碎片,都“复用”到一个单线程里,不按照先来后到的顺序处理,而是只当连接上真正可读、可写的时候才处理,如果可能发生阻塞就立刻切换出去,处理其他的请求。

通过这种方式,Nginx 就完全消除了 I/O 阻塞,把 CPU 利用得“满满当当”,又因为网络收发并不会消耗太多 CPU 计算能力,也不需要切换进程、线程,所以整体的 CPU 负载是相当低的。

在这里插入图片描述

epoll 还有一个特点,大量的连接管理工作都是在操作系统内核里做的,这就减轻了应用程序的负担,所以 Nginx 可以为每个连接只分配很小的内存维护状态,即使有几万、几十万的并发连接也只会消耗几百 M 内存,而其他的 Web 服务器这个时候早就“Memory not enough”了。


多阶段处理

有了“进程池”和“I/O 多路复用”,Nginx 是如何处理 HTTP 请求的呢?

Nginx 在内部也采用的是“化整为零”的思路,把整个 Web 服务器分解成了多个“功能模块”,就好像是乐高积木,可以在配置文件里任意拼接搭建,从而实现了高度的灵活性和扩展性。

Nginx 的 HTTP 处理有四大类模块:

  1. handler 模块:直接处理 HTTP 请求;
  2. filter 模块:不直接处理请求,而是加工过滤响应报文;
  3. upstream 模块:实现反向代理功能,转发请求到其他服务器;
  4. balance 模块:实现反向代理时的负载均衡算法。

“设计模式”其中有一个非常有用的模式叫做“职责链”。它就好像是工厂里的流水线,原料从一头流入,线上有许多工人会进行各种加工处理,最后从另一头出来的就是完整的产品。

Nginx 里的 handler 模块和 filter 模块就是按照“职责链”模式设计和组织的,HTTP 请求报文就是“原材料”,各种模块就是工厂里的工人,走完模块构成的“流水线”,出来的就是处理完成的响应报文。

小结

  1. Nginx 是一个高性能的 Web 服务器,它非常的轻量级,消耗的 CPU、内存很少;
  2. Nginx 采用“master/workers”进程池架构,不使用多线程,消除了进程、线程切换的成本;
  3. Nginx 基于 epoll 实现了“I/O 多路复用”,不会阻塞,所以性能很高;
  4. Nginx 使用了“职责链”模式,多个模块分工合作,自由组合,以流水线的方式处理HTTP 请求。

二、WAF:保护我们的网络服务

Web 服务遇到的威胁

第一种叫“DDoS”攻击(distributed denial-of-service attack),有时候也叫“洪水攻击”。

黑客会控制许多“僵尸”计算机,向目标服务器发起大量无效请求。因为服务器无法区分正常用户和黑客,只能“照单全收”,这样就挤占了正常用户所应有的资源。如果黑客的攻击强度很大,就会像“洪水”一样对网站的服务能力造成冲击,耗尽带宽、CPU 和内存,导致网站完全无法提供正常服务。

“DDoS”攻击方式比较“简单粗暴”,虽然很有效,但不涉及 HTTP 协议内部的细节,“技术含量”比较低。

网站后台的 Web 服务经常会提取出 HTTP 报文里的各种信息,应用于业务,有时会缺乏严格的检查。因为 HTTP 报文在语义结构上非常松散、灵活,URI 里的 query 字符串、头字段、body 数据都可以任意设置,这就带来了安全隐患,给了黑客“代码注入”的可能性。

黑客可以精心编制 HTTP 请求报文,发送给服务器,服务程序如果没有做防备,就会“上当受骗”,执行黑客设定的代码。

SQL 注入”(SQL injection)应该算是最著名的一种“代码注入”攻击了,它利用了服务器字符串拼接形成 SQL 语句的漏洞,构造出非正常的 SQL 语句,获取数据库内部的敏感信息。

另一种“HTTP 头注入”攻击的方式也是类似的原理,它在“Host”“User-Agent”“XForwarded-For”等字段里加入了恶意数据或代码,服务端程序如果解析不当,就会执行预设的恶意代码。


网络应用防火墙

传统“防火墙”工作在三层或者四层,隔离了外网和内网,使用预设的规则,只允许某些特定 IP 地址和端口号的数据包通过,拒绝不符合条件的数据流入或流出内网,实质上是一种网络数据过滤设备

WAF 也是一种“防火墙”,但它工作在七层,看到的不仅是 IP 地址和端口号,还能看到整个 HTTP 报文,所以就能够对报文内容做更深入细致的审核,使用更复杂的条件、规则来过滤数据。

说白了,WAF 就是一种“HTTP 入侵检测和防御系统”

通常一款产品能够称为 WAF,要具备下面的一些功能:

  • IP 黑名单和白名单,拒绝黑名单上地址的访问,或者只允许白名单上的用户访问;

  • URI 黑名单和白名单,与 IP 黑白名单类似,允许或禁止对某些 URI 的访问;

  • 防护 DDoS 攻击,对特定的 IP 地址限连限速;

  • 过滤请求报文,防御“代码注入”攻击;

  • 审计日志,记录所有检测到的入侵操作。

它就像是平时编写程序时必须要做的函数入口参数检查,拿到 HTTP 请求、响应报文,用字符串处理函数看看有没有关键字、敏感词,或者用正则表达式做一下模式匹配,命中了规则就执行对应的动作,比如返回 403/404。

如果你比较熟悉 Apache、Nginx、OpenResty,可以自己改改配置文件,写点 JS 或者Lua 代码,就能够实现基本的 WAF 功能。

WAF 实质上是模式匹配与数据过滤,所以会消耗 CPU,增加一些计算成本,降低服务能力,使用时需要在安全与性能之间找到一个“平衡点”。

HTTPS 为什么不能防御 DDoS、代码注入等攻击呢?

DDoS攻击从产生攻击对象的协议的角度来看可以分为L4攻击和L7攻击,前者其实是针对TCP状态机的恶意hack,比如攻击三次握手机制的SYN Flood和攻击四次握手的TIME WAIT2等,这些方面的防范超出了HTTPS的范畴,需要特殊的安全网管来过滤,清洗和识别。

https是做数据加密防止泄露,而ddos是以数量取胜(伪装成正常请求)


三、CDN:加速我们的网络服务

为什么要有网络加速?

如果仅用现有的 HTTP 传输方式,大多数网站都会访问速度缓慢、用户体验糟糕。


什么是 CDN?

它就是专门为解决“长距离”上网络访问速度慢而诞生的一种网络应用服务。

从名字上看,CDN 有三个关键词:“内容”“分发”和“网络”。

先看一下“网络”的含义。CDN 的最核心原则是“就近访问”,如果用户能够在本地几十公里的距离之内获取到数据,那么时延就基本上变成 0 了。

所以 CDN 投入了大笔资金,在全国、乃至全球的各个大枢纽城市都建立了机房,部署了大量拥有高存储高带宽的节点,构建了一个专用网络。这个网络是跨运营商、跨地域的,虽然内部也划分成多个小网络,但它们之间用高速专有线路连接,是真正的“信息高速公路”,基本上可以认为不存在网络拥堵。

有了这个高速的专用网之后,CDN 就要“分发”源站的“内容”了,用到的就是“缓存代理”技术。使用“推”或者“拉”的手段,把源站的内容逐级缓存到网络的每一个节点上。

于是,用户在上网的时候就不直接访问源站,而是访问离他“最近的”一个 CDN 节点,术语叫“边缘节点”(edge node),其实就是缓存了源站内容的代理服务器,这样一来就省去了“长途跋涉”的时间成本,实现了“网络加速”。

在这里插入图片描述

那么,CDN 都能加速什么样的“内容”呢?

在 CDN 领域里,“内容”其实就是 HTTP 协议里的“资源”,比如超文本、图片、视频、应用程序安装包等等。

资源按照是否可缓存又分为“静态资源”和“动态资源”。所谓的“静态资源”是指数据内容“静态不变”,任何时候来访问都是一样的,比如图片、音频。所谓的“动态资源”是指数据内容是“动态变化”的,也就是由后台服务计算生成的,每次访问都不一样,比如商品的库存、微博的粉丝数等。

很显然,只有静态资源才能够被缓存加速、就近访问,而动态资源只能由源站实时生成,即使缓存了也没有意义。不过,如果动态资源指定了“Cache-Control”,允许缓存短暂的时间,那它在这段时间里也就变成了“静态资源”,可以被 CDN 缓存加速。


CDN 的负载均衡

CDN 是具体怎么运行的,它有两个关键组成部分:全局负 载均衡和缓存系统,对应的是 DNS和缓存代理技术。

全局负载均衡(Global Sever Load Balance)一般简称为 GSLB,它是 CDN 的“大脑”,主要的职责是当用户接入网络的时候在 CDN 专网中挑选出一个“最佳”节点提供服务,解决的是用户如何找到“最近的”边缘节点,对整个 CDN 网络进行“负载均衡”。

GSLB 最常见的实现方式是“DNS 负载均衡”,不过 GSLB 的方式要略微复杂一些。

原来没有 CDN 的时候,权威 DNS 返回的是网站自己服务器的实际 IP 地址,浏览器收到DNS 解析结果后直连网站。

但加入 CDN 后就不一样了,权威 DNS 返回的不是 IP 地址,而是一个 CNAME(Canonical Name ) 别名记录,指向的就是 CDN 的 GSLB。它有点像是 HTTP/2 里“Alt-Svc”的意思,告诉外面:“我这里暂时没法给你真正的地址,你去另外一个地方再查查看吧。”

因为没拿到 IP 地址,于是本地 DNS 就会向 GSLB 再发起请求,这样就进入了 CDN 的全局负载均衡系统,开始“智能调度”,主要的依据有这么几个:

  1. 看用户的 IP 地址,查表得知地理位置,找相对最近的边缘节点;
  2. 看用户所在的运营商网络,找相同网络的边缘节点;
  3. 检查边缘节点的负载情况,找负载较轻的节点;
  4. 其他,比如节点的“健康状况”、服务能力、带宽、响应时间等。

GSLB 把这些因素综合起来,用一个复杂的算法,最后找出一台“最合适”的边缘节点,把这个节点的 IP 地址返回给用户,用户就可以“就近”访问 CDN 的缓存代理了。


CDN 的缓存代理

缓存系统是 CDN 的另一个关键组成部分,相当于 CDN 的“心脏”。如果缓存系统的服务能力不够,不能很好地满足用户的需求,那 GSLB 调度算法再优秀也没有用。

但互联网上的资源是无穷无尽的,不管 CDN 厂商有多大的实力,也不可能把所有资源都缓存起来。所以,缓存系统只能有选择地缓存那些最常用的那些资源。

这里就有两个 CDN 的关键概念:“命中”和“回源”

“命中”就是指用户访问的资源恰好在缓存系统里,可以直接返回给用户;“回源”则正相反,缓存里没有,必须用代理的方式回源站取。

相应地,也就有了两个衡量 CDN 服务质量的指标:“命中率”和“回源率”。命中率就是命中次数与所有访问次数之比,回源率是回源次数与所有访问次数之比。显然,好的 CDN应该是命中率越高越好,回源率越低越好。现在的商业 CDN 命中率都在 90% 以上,相当于把源站的服务能力放大了 10 倍以上。

怎么样才能尽可能地提高命中率、降低回源率呢?

首先,最基本的方式就是在存储系统上下功夫,硬件用高速 CPU、大内存、万兆网卡,再搭配 TB 级别的硬盘和快速的 SSD。软件方面则不断“求新求变”,各种新的存储软件都会拿来尝试,比如 Memcache、Redis、Ceph,尽可能地高效利用存储,存下更多的内容。

其次,缓存系统也可以划分出层次,分成一级缓存节点和二级缓存节点。一级缓存配置高一些,直连源站,二级缓存配置低一些,直连用户。回源的时候二级缓存只找一级缓存,一级缓存没有才回源站,这样最终“扇入度”就缩小了,可以有效地减少真正的回源。

第三个就是使用高性能的缓存服务,据我所知,目前国内的 CDN 厂商内部都是基于开源软件定制的。最常用的是专门的缓存代理软件 Squid、Varnish,还有新兴的 ATS(ApacheTraffic Server),而 Nginx 和 OpenResty 作为 Web 服务器领域的“多面手”,凭借着强大的反向代理能力和模块化、易于扩展的优点,也在 CDN 里占据了不少的份额。


四、WebSocket:沙盒里的TCP

单从名字上看,“Web”指的是 HTTP,“Socket”是套接字调用。

“WebSocket”就是运行在“Web”,也就是HTTP 上的 Socket 通信规范,提供与“TCP Socket”类似的功能,使用它就可以像“TCPSocket”一样调用下层协议栈,任意地收发数据。

更准确地说,“WebSocket”是一种基于 TCP 的轻量级网络通信协议,在地位上是与HTTP“平级”的。

WebSocket 和 HTTP/2 都是用来弥补HTTP协议的一些缺陷和不足,WebSocket 主要
解决双向通信、全双工问题,HTTP/2 主要解决传输效率的问题,两者在二进制帧的格式
上也不太一样,HTTP/2 有多路复用、优先级和流的概念。

为什么要有 WebSocket

不过,已经有了被广泛应用的 HTTP 协议,为什么要再出一个 WebSocket 呢?它有哪些好处呢?

其实 WebSocket 与 HTTP/2 一样,都是为了解决 HTTP 某方面的缺陷而诞生的。HTTP/2针对的是“队头阻塞”,而 WebSocket 针对的是“请求 - 应答”通信模式。

那么,“请求 - 应答”有什么不好的地方呢?

“请求 - 应答”是一种“半双工”的通信模式,虽然可以双向收发数据,但同一时刻只能一个方向上有动作,传输效率低。更关键的一点,它是一种“被动”通信模式,服务器只能“被动”响应客户端的请求,无法主动向客户端发送数据。

虽然后来的 HTTP/2、HTTP/3 新增了 Stream、Server Push 等特性,但“请求 - 应答”依然是主要的工作方式。这就导致 HTTP 难以应用在动态页面、即时消息、网络游戏等要求“实时通信”的领域。

在 WebSocket 出现之前,在浏览器环境里用 JavaScript 开发实时 Web 应用很麻烦。因为浏览器是一个“受限的沙盒”,不能用 TCP,只有 HTTP 协议可用,所以就出现了很多“变通”的技术,“轮询”(polling)就是比较常用的的一种。

简单地说,轮询就是不停地向服务器发送 HTTP 请求,问有没有数据,有数据的话服务器就用响应报文回应。如果轮询的频率比较高,那么就可以近似地实现“实时通信”的效果。但轮询的缺点也很明显,反复发送无效查询请求耗费了大量的带宽和 CPU 资源,非常不经济。

所以,为了克服 HTTP“请求 - 应答”模式的缺点,WebSocket 就“应运而生”了。它原来是 HTML5 的一部分,后来“自立门户”,形成了一个单独的标准,RFC 文档编号是6455。


WebSocket 的特点

WebSocket 是一个真正“全双工”的通信协议,与 TCP 一样,客户端和服务器都可以随时向对方发送数据,而不用像 HTTP“你拍一,我拍一”那么“客套”。于是,服务器就可
以变得更加“主动”了。一旦后台有新的数据,就可以立即“推送”给客户端,不需要客户端轮询,“实时通信”的效率也就提高了。

WebSocket 采用了二进制帧结构,语法、语义与 HTTP 完全不兼容,但因为它的主要运行环境是浏览器,为了便于推广和应用,就不得不“搭便车”,在使用习惯上尽量向 HTTP靠拢,这就是它名字里“Web”的含义。

服务发现方面,WebSocket 没有使用 TCP 的“IP 地址 + 端口号”,而是延用了 HTTP 的URI 格式,但开头的协议名不是“http”,引入的是两个新的名字:“ws”和“wss”,分别表示明文和加密的 WebSocket 协议。

WebSocket 的默认端口也选择了 80 和 443,因为现在互联网上的防火墙屏蔽了绝大多数的端口,只对 HTTP 的 80、443 端口“放行”,所以 WebSocket 就可以“伪装”成HTTP 协议,比较容易地“穿透”防火墙,与服务器建立连接。


WebSocket 的帧结构

不过 WebSocket 和 HTTP/2 的关注点不同,WebSocket 更侧重于“实时通信”,而HTTP/2 更侧重于提高传输效率,所以两者的帧结构也有很大的区别。

WebSocket 虽然有“帧”,但却没有像 HTTP/2 那样定义“流”,也就不存在“多路复用”“优先级”等复杂的特性,而它自身就是“全双工”的,也就不需要“服务器推送”。

下图就是 WebSocket 的帧结构定义,长度不固定,最少 2 个字节,最多 14 字节。

在这里插入图片描述

开头的两个字节是必须的,也是最关键的。

第一个字节的第一位“FIN”是消息结束的标志位,相当于 HTTP/2 里的“END_STREAM”,表示数据发送完毕。一个消息可以拆成多个帧,接收方看到“FIN”后,就可以把前面的帧拼起来,组成完整的消息。

“FIN”后面的三个位是保留位,目前没有任何意义,但必须是 0。

第一个字节的后 4 位很重要,叫“Opcode”,操作码,其实就是帧类型,比如 1 表示帧内容是纯文本,2 表示帧内容是二进制数据,8 是关闭连接,9 和 10 分别是连接保活的PING 和 PONG。

第二个字节第一位是掩码标志位“MASK”,表示帧内容是否使用异或操作(xor)做简单的加密。目前的 WebSocket 标准规定,客户端发送数据必须使用掩码,而服务器发送则必须不使用掩码。

第二个字节后 7 位是“Payload len”,表示帧内容的长度。它是另一种变长编码,最少 7位,最多是 7+64 位,也就是额外增加 8 个字节,所以一个 WebSocket 帧最大是2^64。

长度字段后面是“Masking-key”,掩码密钥,它是由上面的标志位“MASK”决定的,如果使用掩码就是 4 个字节的随机数,否则就不存在。

这么分析下来,其实 WebSocket 的帧头就四个部分:“结束标志位 + 操作码 + 帧长度 +掩码”,只是使用了变长编码的“小花招”,不像 HTTP/2 定长报文头那么简单明了。


WebSocket 的握手

和 TCP、TLS 一样,WebSocket 也要有一个握手过程,然后才能正式收发数据。

这里它还是搭上了 HTTP 的“便车”,利用了 HTTP 本身的“协议升级”特性,“伪装”成 HTTP,这样就能绕过浏览器沙盒、网络防火墙等等限制,这也是 WebSocket 与HTTP 的另一个重要关联点。

WebSocket 的握手是一个标准的 HTTP GET 请求,但要带上两个协议升级的专用头字段:

“Connection: Upgrade”,表示要求协议“升级”; “Upgrade: websocket”,表示要“升级”成 WebSocket 协议。

另外,为了防止普通的 HTTP 消息被“意外”识别成 WebSocket,握手消息还增加了两个额外的认证用头字段(所谓的“挑战”,Challenge):

Sec-WebSocket-Key:一个 Base64 编码的 16 字节随机数,作为简单的认证密钥; Sec-WebSocket-Version:协议的版本号,当前必须是 13。

在这里插入图片描述

服务器收到 HTTP 请求报文,看到上面的四个字段,就知道这不是一个普通的 GET 请求,而是 WebSocket 的升级请求,于是就不走普通的 HTTP 处理流程,而是构造一个特殊的“101 Switching Protocols”响应报文,通知客户端,接下来就不用 HTTP 了,全改用WebSocket 协议通信。

WebSocket 的握手响应报文也是有特殊格式的,要用字段“Sec-WebSocket-Accept”验证客户端请求报文,同样也是为了防止误连接。

具体的做法是把请求头里“Sec-WebSocket-Key”的值,加上一个专用的 UUID“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,再计算 SHA-1 摘要。

encode_base64(
	sha1(
		Sec-WebSocket-Key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' ))

客户端收到响应报文,就可以用同样的算法,比对值是否相等,如果相等,就说明返回的报文确实是刚才握手时连接的服务器,认证成功。

握手完成,后续传输的数据就不再是 HTTP 报文,而是 WebSocket 格式的二进制帧了。

小结
浏览器是一个“沙盒”环境,有很多的限制,不允许建立 TCP 连接收发数据,而有了WebSocket,我们就可以在浏览器里与服务器直接建立“TCP 连接”,获得更多的自由。

不过自由也是有代价的,WebSocket 虽然是在应用层,但使用方式却与“TCPSocket”差不多,过于“原始”,用户必须自己管理连接、缓存、状态,开发上比 HTTP复杂的多,所以是否要在项目中引入 WebSocket 必须慎重考虑。

  1. HTTP 的“请求 - 应答”模式不适合开发“实时通信”应用,效率低,难以实现动态页面,所以出现了 WebSocket;
  2. WebSocket 是一个“全双工”的通信协议,相当于对 TCP 做了一层“薄薄的包装”,让它运行在浏览器环境里;
  3. WebSocket 使用兼容 HTTP 的 URI 来发现服务,但定义了新的协议名“ws”和“wss”,端口号也沿用了 80 和 443;
  4. WebSocket 使用二进制帧,结构比较简单,特殊的地方是有个“掩码”操作,客户端发数据必须掩码,服务器则不用;
  5. WebSocket 利用 HTTP 协议实现连接握手,发送 GET 请求要求“协议升级”,握手过程中有个非常简单的认证机制,目的是防止误连接。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

程序媛小y

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

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

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

打赏作者

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

抵扣说明:

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

余额充值