HTTP 协议详解(二)
前面一篇已经说过了 HTTP 的基本特性,HTTP 的发展史,前情回顾。这一篇就更详细的 HTTP 协议使用过程一些参数配置,缓存,Cookie设置相关的细节做一些梳理。
数据类型与编码
在 TCP/IP 协议栈里,传输数据基本上都是 header + body
的格式。但 TCP、UDP 因为是传输层的协议,它们不会关心 body 数据是什么,只要把数据发送到对方就算是完成了任务。
而 HTTP 协议则不同,它是应用层的协议,数据到达之后工作只能说是完成了一半,还必须要告诉上层应用这是什么数据才行,否则上层应用就会 不知所措 。
你可以设想一下,假如 HTTP 没有告知数据类型的功能,服务器把 一大坨 数据发给了浏览器,浏览器看到的是一个 黑盒子 ,这时候该怎么办呢?
当然,它可以 猜 。因为很多数据都是有固定格式的,所以通过检查数据的前几个字节也许就能知道这是个 GIF 图片、或者是个 MP3 音乐文件,但这种方式无疑十分低效,而且有很大几率会检查不出来文件类型。
幸运的是,早在 HTTP 协议诞生之前就已经有了针对这种问题的解决方案,不过它是用在电子邮件系统里的,让电子邮件可以发送 ASCII 码以外的任意数据,方案的名字叫做 多用途互联网邮件扩展 (Multipurpose Internet Mail Extensions),简称为 MIME。
MIME 是一个很大的标准规范,但 HTTP 只 顺手牵羊 取了其中的一部分,用来标记 body 的数据类型,这就是我们平常总能听到的 MIME type 。
MIME 把数据分成了八大类,每个大类下再细分出多个子类,形式是 type/subtype 的字符串,巧得很,刚好也符合了 HTTP 明文的特点,所以能够很容易地纳入 HTTP 头字段里。
这里简单列举一下在 HTTP 里经常遇到的几个类别:
- text:即文本格式的可读数据,我们最熟悉的应该就是
text/html
了,表示超文本文档,此外还有纯文本text/plain
、样式表text/css
等。 - image:即图像文件,有
image/gif
、image/jpeg
、image/png
等。 - audio/video:音频和视频数据,例如
audio/mpeg
、video/mp4
等。 - application:数据格式不固定,可能是文本也可能是二进制,必须由上层应用程序来解释。常见的有
application/json
,application/javascript
、application/pdf
等,另外,如果实在是不知道数据是什么类型,像刚才说的 黑盒 ,就会是application/octet-stream
,即不透明的二进制数据。
但仅有 MIME type 还不够,因为 HTTP 在传输时为了节约带宽,有时候还会压缩数据,为了不要让浏览器继续 猜 ,还需要有一个 Encoding type ,告诉数据是用的什么编码格式,这样对方才能正确解压缩,还原出原始的数据。
比起 MIME type 来说,Encoding type 就少了很多,常用的只有下面三种:
- gzip:GNU zip 压缩格式,也是互联网上最流行的压缩格式;
- deflate:zlib(deflate)压缩格式,流行程度仅次于 gzip;
- br:一种专门为 HTTP 优化的新压缩算法(Brotli)。
大文件传输问题
数据压缩
通常浏览器在发送请求时都会带着 Accept-Encoding 头字段,里面是浏览器支持的压缩格式列表,例如 gzip、deflate、br 等,这样服务器就可以从中选择一种压缩算法,放进 Content-Encoding 响应头里,再把原数据压缩后发给浏览器。
如果压缩率能有 50%,也就是说 100K 的数据能够压缩成 50K 的大小,那么就相当于在带宽不变的情况下网速提升了一倍,加速的效果是非常明显的。
不过这个解决方法也有个缺点,gzip 等压缩算法通常只对文本文件有较好的压缩率,而图片、音频视频等多媒体数据本身就已经是高度压缩的,再用 gzip 处理也不会变小(甚至还有可能会增大一点),所以它就失效了。
分块传输
压缩是把大文件整体变小,我们可以反过来思考,如果大文件整体不能变小,那就把它 拆开 ,分解成多个小块,把这些小块分批发给浏览器,浏览器收到后再组装复原。
这种 化整为零 的思路在 HTTP 协议里就是 chunked 分块传输编码,在响应报文里用头字段 Transfer-Encoding: chunked 来表示,意思是报文里的 body 部分不是一次性发过来的,而是分成了许多的块(chunk)逐个发送。
分块传输也可以用于 流式数据 ,例如由数据库动态生成的表单页面,这种情况下 body 数据的长度是未知的,无法在头字段 Content-Length 里给出确切的长度,所以也只能用 chunked 方式分块发送。
Transfer-Encoding: chunked
和 Content-Length
这两个字段是互斥的,也就是说响应报文里这两个字段不能同时出现,一个响应报文的传输要么是长度已知,要么是长度未知(chunked),这一点你一定要记住。
下面我们来看一下分块传输的编码规则,其实也很简单,同样采用了明文的方式,很类似响应头。
- 每个分块包含两个部分,长度头和数据块;
- 长度头是以 CRLF(回车换行,即\r\n)结尾的一行明文,用 16 进制数字表示长度;
- 数据块紧跟在长度头后,最后也用 CRLF 结尾,但数据不包含 CRLF;
- 最后用一个长度为 0 的块表示结束,即 0\r\n\r\n 。
范围请求
有了分块传输编码,服务器就可以轻松地收发大文件了,但对于上 G 的超大文件,还有一些问题需要考虑。
比如,你在看当下正热播的某穿越剧,想跳过片头,直接看正片,或者有段剧情很无聊,想拖动进度条快进几分钟,这实际上是想获取一个大文件其中的片段数据,而分块传输并没有这个能力。
HTTP 协议为了满足这样的需求,提出了 范围请求 (range requests)的概念,允许客户端在请求头里使用专用字段来表示只获取文件的一部分,相当于是**客户端的 化整为零 **。
范围请求不是 Web 服务器必备的功能,可以实现也可以不实现,所以服务器必须在响应头里使用字段 Accept-Ranges: bytes 明确告知客户端: 我是支持范围请求的 。
如果不支持的话该怎么办呢?服务器可以发送 Accept-Ranges: none
,或者干脆不发送 Accept-Ranges
字段,这样客户端就认为服务器没有实现范围请求功能,只能老老实实地收发整块文件了。
请求头 Range 是 HTTP 范围请求的专用字段,格式是 bytes=x - y ,其中的 x 和 y 是以字节为单位的数据范围。
要注意 x、y 表示的是偏移量 ,范围必须从 0 计数,例如前 10 个字节表示为 0-9 ,第二个 10 字节表示为 10-19 ,而 0-10 实际上是前 11 个字节。
Range 的格式也很灵活,起点 x 和终点 y 可以省略,能够很方便地表示正数或者倒数的范围。假设文件是 100 个字节,那么:
0-
表示从文档起点到文档终点,相当于 0-99 ,即整个文件;10-
是从第 10 个字节开始到文档末尾,相当于 10-99 ;-1
是文档的最后一个字节,相当于 99-99 ;-10
是从文档末尾倒数 10 个字节,相当于 90-99 。
服务器收到 Range 字段后,需要做四件事。
- 它必须检查范围是否合法,比如文件只有 100 个字节,但请求 200-300 ,这就是范围越界了。服务器就会返回状态码 416,意思是你的范围请求有误我无法处理,请再检查一下 。
- 如果范围正确,服务器就可以根据 Range 头计算偏移量,读取文件的片段了,返回状态码 206 Partial Content ,和 200 的意思差不多,但表示 body 只是原数据的一部分。
- 服务器要添加一个响应头字段 Content-Range,告诉片段的实际偏移量和资源的总大小,格式是 bytes x-y/length ,与 Range 头区别在没有 = ,范围后多了总长度。例如,对于 0-10 的范围请求,值就是 bytes 0-10/100 。
- 最后剩下的就是发送数据,直接把片段用 TCP 发给客户端,一个范围请求就算是处理完了。
多段数据
刚才说的范围请求一次只获取一个片段,其实它还支持在 Range 头里使用多个 x - y ,一次性获取多个片段数据。
这种情况需要使用一种特殊的 MIME 类型: multipart/byteranges ,表示报文的 body 是由多段字节序列组成的,并且还要用一个参数 boundary=xxx 给出段之间的分隔标记。
多段数据的格式与分块传输也比较类似,但它需要用分隔标记 boundary 来区分不同的片段,可以通过图来对比一下。
每一个分段必须以 - -boundary
开始(前面加两个 - ),之后要用 Content-Type
和 Content-Range
标记这段数据的类型和所在范围,然后就像普通的响应头一样以回车换行结束,再加上分段数据,最后用一个 - -boundary- -
(前后各有两个 - )表示所有的分段结束。
要注意这四种方法不是互斥的,而是可以混合起来使用,例如压缩后再分块传输,或者分段后再分块。
HTTP的重定向和跳转
前面讲过 HTTP 状态码的时候说过:3×× 状态码,301 是 永久重定向 ,302 是 临时重定向 ,浏览器收到这两个状态码就会跳转到新的 URI。
那么,它们是怎么做到的呢?难道仅仅用这两个代码就能够实现跳转页面吗?
先在实验环境里看一下重定向的过程吧,用 Chrome 访问 URI /18-1 ,它会使用 302 立即跳转到 /index.html 。
从这个实验可以看到,这一次 重定向 实际上发送了两次 HTTP 请求,第一个请求返回了 302,然后第二个请求就被重定向到了 /index.html 。但如果不用开发者工具的话,你是完全看不到这个跳转过程的,也就是说,重定向是 用户无感知 的。
我们再来看看第一个请求返回的响应报文:
这里出现了一个新的头字段 Location: /index.html
,它就是 301/302 重定向跳转的秘密所在。
Location 字段属于响应字段,必须出现在响应报文里。但只有配合 301/302 状态码才有意义,它标记了服务器要求重定向的 URI,这里就是要求浏览器跳转到 index.html 。
浏览器收到 301/302 报文,会检查响应头里有没有 Location 。如果有,就从字段值里提取出 URI,发出新的 HTTP 请求,相当于自动替我们点击了这个链接。
在 Location 里的 URI 既可以使用绝对 URI,也可以使用相对 URI。所谓 绝对 URI ,就是完整形式的 URI,包括 scheme、host:port、path 等。所谓 相对 URI ,就是省略了 scheme 和 host:port,只有 path 和 query 部分,是不完整的,但可以从请求上下文里计算得到。
HTTP 的 cookie 机制
HTTP 是 无状态 的,这既是优点也是缺点。优点是服务器没有状态差异,可以很容易地组成集群,而缺点就是无法支持需要记录状态的事务操作。
那该怎么样让原本无 记忆能力 的服务器拥有 记忆能力 呢?服务器记不住,那就在外部想办法记住。相当于是服务器给每个客户端都贴上一张小纸条,上面写了一些只有服务器才能理解的数据,需要的时候客户端把这些信息发给服务器,服务器看到 Cookie,就能够认出对方是谁了。
Cookie 的工作过程
那么,Cookie 这张小纸条是怎么传递的呢?
这要用到两个字段:响应头字段 Set-Cookie 和请求头字段 Cookie。
当用户通过浏览器第一次访问服务器的时候,服务器肯定是不知道他的身份的。所以,就要创建一个独特的身份标识数据,格式是 key=value ,然后放进 Set-Cookie 字段里,随着响应报文一同发给浏览器。
浏览器收到响应报文,看到里面有 Set-Cookie,知道这是服务器给的身份标识,于是就保存起来,下次再请求的时候就自动把这个值放进 Cookie 字段里发给服务器。
因为第二次请求里面有了 Cookie 字段,服务器就知道这个用户不是新人,之前来过,就可以拿出 Cookie 里的值,识别出用户的身份,然后提供个性化的服务。
不过因为服务器的 记忆能力 实在是太差,一张小纸条经常不够用。所以,服务器有时会在响应头里添加多个 Set-Cookie,存储多个 key=value 。但浏览器这边发送时不需要用多个 Cookie 字段,只要在一行里用 ; 隔开就行。
Cookie 的属性
首先,我们应该设置 Cookie 的生存周期,也就是它的有效期,让它只能在一段时间内可用,就像是食品的 保鲜期 ,一旦超过这个期限浏览器就认为是 Cookie 失效,在存储里删除,也不会发送给服务器。
Cookie 的有效期可以使用 Expires
和 Max-Age
两个属性来设置。
Expires 俗称 过期时间 ,用的是绝对时间点,可以理解为 截止日期 (deadline)。 Max-Age 用的是相对时间,单位是秒,浏览器用收到报文的时间点再加上 Max-Age
,就可以得到失效的绝对时间。
Expires
和 Max-Age
可以同时出现,两者的失效时间可以一致,也可以不一致,但浏览器会优先采用 Max-Age 计算失效期。
比如在这个例子里,Expires 标记的过期时间是 GMT 2019 年 6 月 7 号 8 点 19 分 ,而 Max-Age 则只有 10 秒,如果现在是 6 月 6 号零点,那么 Cookie 的实际有效期就是 6 月 6 号零点过 10 秒 。
其次,我们需要设置 Cookie 的作用域,让浏览器仅发送给特定的服务器和 URI,避免被其他网站盗用。
作用域的设置比较简单, Domain 和 Path 指定了 Cookie 所属的域名和路径,浏览器在发送 Cookie 前会从 URI 中提取出 host 和 path 部分,对比 Cookie 的属性。如果不满足条件,就不会在请求头里发送 Cookie。
使用这两个属性可以为不同的域名和路径分别设置各自的 Cookie,比如 /19-1 用一个 Cookie, /19-2 再用另外一个 Cookie,两者互不干扰。不过现实中为了省事,通常 Path 就用一个 / 或者直接省略,表示域名下的任意路径都允许使用 Cookie,让服务器自己去挑。
最后要考虑的就是Cookie 的安全性了,尽量不要让服务器以外的人看到。
写过前端的同学一定知道,在 JS 脚本里可以用 document.cookie
来读写 Cookie 数据,这就带来了安全隐患,有可能会导致 跨站脚本 (XSS)攻击窃取数据。
属性 HttpOnly 会告诉浏览器,此 Cookie 只能通过浏览器 HTTP 协议传输,禁止其他方式访问,浏览器的 JS 引擎就会禁用 document.cookie
等一切相关的 API,脚本攻击也就无从谈起了。
另一个属性 SameSite 可以防范 跨站请求伪造 (XSRF)攻击,设置成SameSite=Strict
可以严格限定 Cookie 不能随着跳转链接跨站发送,而 SameSite=Lax
则略宽松一点,允许 GET/HEAD 等安全方法,但禁止 POST 跨站发送。
还有一个属性叫 Secure ,表示这个 Cookie 仅能用 HTTPS 协议加密传输,明文的 HTTP 协议会禁止发送。但 Cookie 本身不是加密的,浏览器里还是以明文的形式存在。
HTTP的缓存控制
由于链路漫长,网络时延不可控,浏览器使用 HTTP 获取资源的成本较高。所以,非常有必要把 来之不易 的数据缓存起来,下次再请求的时候尽可能地复用。这样,就可以避免多次请求 - 应答的通信成本,节约网络带宽,也可以加快响应速度。
服务器的缓存控制
- 浏览器发现缓存无数据,于是发送请求,向服务器获取资源;
- 服务器响应请求,返回资源,同时标记资源的有效期;
- 浏览器缓存资源,等待下次重用。
服务器标记资源有效期使用的头字段是 Cache-Control ,里面的值 max-age=30 就是资源的有效时间,相当于告诉浏览器, 这个页面只能缓存 30 秒,之后就算是过期,不能用。
你可能要问了,让浏览器直接缓存数据就好了,为什么要加个有效期呢?
这是因为网络上的数据随时都在变化,不能保证它稍后的一段时间还是原来的样子。就像生鲜超市给你快递的西瓜,只有 5 天的保鲜期,过了这个期限最好还是别吃,不然可能会闹肚子。
Cache-Control
字段里的 max-age
和上一讲里 Cookie 有点像,都是标记资源的有效期。
这里的 max-age
是 生存时间 (又叫 新鲜度 缓存寿命 ,类似 TTL,Time-To-Live),时间的计算起点是响应报文的创建时刻(即 Date 字段,也就是离开服务器的时刻),而不是客户端收到报文的时刻,也就是说包含了在链路传输过程中所有节点所停留的时间。
比如,服务器设定 max-age=5
,但因为网络质量很糟糕,等浏览器收到响应报文已经过去了 4 秒,那么这个资源在客户端就最多能够再存 1 秒钟,之后就会失效。
max-age 是 HTTP 缓存控制最常用的属性,此外在响应报文里还可以用其他的属性来更精确地指示浏览器应该如何使用缓存:
- no_store:不允许缓存,用于某些变化非常频繁的数据,例如秒杀页面;
- no_cache:它的字面含义容易与 no_store 搞混,实际的意思并不是不允许缓存,而是可以缓存,但在使用之前必须要去服务器验证是否过期,是否有最新的版本;
- must-revalidate:又是一个和 no_cache 相似的词,它的意思是如果缓存不过期就可以继续使用,但过期了如果还想用就必须去服务器验证。
听的有点糊涂吧。没关系,我拿生鲜速递来举例说明一下:
- no_store:买来的西瓜不允许放进冰箱,要么立刻吃,要么立刻扔掉;
- no_cache:可以放进冰箱,但吃之前必须问超市有没有更新鲜的,有就吃超市里的;
- must-revalidate:可以放进冰箱,保鲜期内可以吃,过期了就要问超市让不让吃。
我把服务器的缓存控制策略画了一个流程图,对照着它你就可以在今后的后台开发里明确 Cache-Control 的用法了。
客户端的缓存控制
现在冰箱里已经有了 缓存 的西瓜,是不是就可以直接开吃了呢?
你可以在 Chrome 里点几次 刷新 按钮,估计你会失望,页面上的 ID 一直在变,根本不是缓存的结果,明明说缓存 30 秒,怎么就不起作用呢?
其实不止服务器可以发 Cache-Control 头,浏览器也可以发 Cache-Control ,也就是说 请求 - 应答 的双方都可以用这个字段进行缓存控制,互相协商缓存的使用策略。
当你点 刷新 按钮的时候,浏览器会在请求头里加一个 Cache-Control: max-age=0 。因为 max-age 是 生存时间 ,max-age=0 的意思就是 我要一个最最新鲜的西瓜 ,而本地缓存里的数据至少保存了几秒钟,所以浏览器就不会使用缓存,而是向服务器发请求。服务器看到 max-age=0,也就会用一个最新生成的报文回应浏览器。
Ctrl+F5 的 强制刷新 又是什么样的呢?
它其实是发了一个 Cache-Control: no-cache ,含义和 max-age=0 基本一样,就看后台的服务器怎么理解,通常两者的效果是相同的。
条件请求
浏览器用 Cache-Control 做缓存控制只能是刷新数据,不能很好地利用缓存数据,又因为缓存会失效,使用前还必须要去服务器验证是否是最新版。
那么该怎么做呢?
浏览器可以用两个连续的请求组成 验证动作 :先是一个 HEAD,获取资源的修改时间等元信息,然后与缓存数据比较,如果没有改动就使用缓存,节省网络流量,否则就再发一个 GET 请求,获取最新的版本。
但这样的两个请求网络成本太高了,所以 HTTP 协议就定义了一系列 If 开头的 条件请求 字段,专门用来检查验证资源是否过期,把两个请求才能完成的工作合并在一个请求里做。而且,验证的责任也交给服务器,浏览器只需 坐享其成 。
条件请求一共有 5 个头字段,我们最常用的是 if-Modified-Since 和 If-None-Match 这两个。需要第一次的响应报文预先提供 Last-modified 和 ETag ,然后第二次请求时就可以带上缓存里的原值,验证资源是否是最新的。
如果资源没有变,服务器就回应一个 304 Not Modified ,表示缓存依然有效,浏览器就可以更新一下有效期,然后放心大胆地使用缓存了。
Last-modified 很好理解,就是文件的最后修改时间。ETag 是什么呢?
ETag 是 实体标签 (Entity Tag)的缩写,是资源的一个唯一标识,主要是用来解决修改时间无法准确区分文件变化的问题。
比如,一个文件在一秒内修改了多次,但因为修改时间是秒级,所以这一秒内的新版本无法区分。
再比如,一个文件定期更新,但有时会是同样的内容,实际上没有变化,用修改时间就会误以为发生了变化,传送给浏览器就会浪费带宽。
使用 ETag 就可以精确地识别资源的变动情况,让浏览器能够更有效地利用缓存。
ETag 还有 强 、弱 之分。
强 ETag 要求资源在字节级别必须完全相符,弱 ETag 在值前有个 W/
标记,只要求资源在语义上没有变化,但内部可能会有部分发生了改变(例如 HTML 里的标签顺序调整,或者多了几个空格)。
还是拿生鲜速递做比喻最容易理解:
你打电话给超市, 我这个西瓜是 3 天前买的,还有最新的吗? 。超市看了一下库存,说: 没有啊,我这里都是 3 天前的。 于是你就知道了,再让超市送货也没用,还是吃冰箱里的西瓜吧。这就是 if-Modified-Since 和 Last-modified 。
但你还是想要最新的,就又打电话: 有不是沙瓤的西瓜吗? ,超市告诉你都是沙瓤的(Match),于是你还是只能吃冰箱里的沙瓤西瓜。这就是 If-None-Match 和 弱 ETag 。
第三次打电话,你说 有不是 8 斤的沙瓤西瓜吗? ,这回超市给了你满意的答复: 有个 10 斤的沙瓤西瓜 。于是,你就扔掉了冰箱里的存货,让超市重新送了一个新的大西瓜。这就是 If-None-Match 和 强 ETag 。
条件请求里其他的三个头字段是 If-Unmodified-Since
If-Match
和 If-Range
,其实只要你掌握了 if-Modified-Since
和 If-None-Match
,可以轻易地举一反三 。
HTTP 协议详解(二)的更多相关文章
- [置顶] SNMP协议详解<二>
上一篇文章讲解了SNMP的基本架构,本篇文章将重点分析SNMP报文,并对不同版本(SNMPv1.v2c.v3)进行区别! 四.SNMP协议数据单元 在SNMP管理中,管理站(NMS)和代理(Agent ...
- linux高性能服务器编程 (二) --IP协议详解
第二章 IP协议详解 什么是IP协议:IP 协议是TCP/IP协议族的动力,它为上层提供了无状态.无连接.不可靠的服务. IP 头部信息:头部信息会出现在每一个IP数据报上,便于记录IP通信的源端IP ...
- HTTP协议详解(转)
转自:http://blog.csdn.net/gueter/archive/2007/03/08/1524447.aspx Author :Jeffrey 引言 HTTP是一个属于应用层的面向对象的 ...
- HTTP协议详解
Author :Jeffrey 引言 HTTP 是一个属于应用层的面向对象的协议,由于其简捷.快速的方式,适用于分布式超媒体信息系统.它于1990年提出,经过几年的使用与发展,得到不断地完善和 扩展. ...
- ASP.NET知识总结(3.HTTP协议详解)
引言 HTTP是一个属于应用层的面向对象的协议,由于其简捷.快速的方式,适用于分布式超媒体信息系统.它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展.目前在WWW中使用的是HTTP/1. ...
- 接口测试之HTTP协议详解
引言 HTTP是一个属于应用层的面向对象的协议,由于其简捷.快速的方式,适用于分布式超媒体信息系统.它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展.目前在WWW中使用的是HTTP/1. ...
- OSPF协议详解
CCNP OSPF协议详解 2010-02-24 20:30:22 标签:CCNP 职场 OSPF 休闲 OSPF(Open Shortest Path Fitst,ospf)开放最短路径优先协议,是 ...
- HTTP协议详解(真的很经典)
HTTP 是一个属于应用层的面向对象的协议,由于其简捷.快速的方式,适用于分布式超媒体信息系统.它于1990年提出,经过几年的使用与发展,得到不断地完善和 扩展.目前在WWW中使用的是HTTP/1.0 ...
- HTTP协议详解--转载http://blog.csdn.net/gueter/article/details/1524447
引言 HTTP是一个属于应用层的面向对象的协议,由于其简捷.快速的方式,适用于分布式超媒体信息系统.它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展.目前在WWW中使用的是HTTP/1. ...
- 关于http协议详解
Author :Jeffrey 引言 HTTP是一个属于应用层的面向对象的协议,由于其简捷.快速的方式,适用于分布式超媒体信息系统.它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展.目前 ...
随机推荐
- Java实现 LeetCode 436 寻找右区间
436. 寻找右区间 给定一组区间,对于每一个区间 i,检查是否存在一个区间 j,它的起始点大于或等于区间 i 的终点,这可以称为 j 在 i 的"右侧". 对于任何区间,你需要存 ...
- Java实现 LeetCode 166 分数到小数
166. 分数到小数 给定两个整数,分别表示分数的分子 numerator 和分母 denominator,以字符串形式返回小数. 如果小数部分为循环小数,则将循环的部分括在括号内. 示例 1: 输入 ...
- java实现第四届蓝桥杯猜灯谜
猜灯谜 题目描述 A 村的元宵节灯会上有一迷题: 请猜谜 * 请猜谜 = 请边赏灯边猜 小明想,一定是每个汉字代表一个数字,不同的汉字代表不同的数字. 请你用计算机按小明的思路算一下,然后提交&quo ...
- 云服务器部署Web项目
接着上一篇整好MySQL数据库了,部署Web项目的重要一步就完成了,接下来就是整Tomcat发布项目了,这个博主用的是宝塔面板,所以,也很简单,直接在宝塔面板软件商店选择对应的Tomcat版本安装就行 ...
- 经典文本特征表示方法: TF-IDF
引言 在信息检索, 文本挖掘和自然语言处理领域, IF-IDF 这个名字, 从它在 20 世纪 70 年代初被发明, 已名震江湖近半个世纪而不曾衰歇. 它表示的简单性, 应用的有效性, 使得它成为不同 ...
- xmake v2.3.4 发布, 更加完善的工具链支持
为了让xmake更好得支持交叉编译,这个版本我重构了整个工具链,使得工具链的切换更加的方便快捷,并且现在用户可以很方便地在xmake.lua中扩展自己的工具链. 关于平台的支持上,我们新增了对*BSD ...
- Grafana邮箱告警
1.grafana-server 配置 smtp 服务器 vim /etc/grafana/grafana.ini #修改一下内容 ################################## ...
- 对Activity启动模式的理解
对Activity启动模式的理解 应用场景 在已打开多个Activity应用B的前提下,应用A调用应用B后点击返回按钮,需要直接返回到A应用,而不是打开B应用的上一个Activity 一个Task可以 ...
- arduino 的analogRead() 和analogWrite()
模拟输入analogRead()函数的返回值范围是0 到1023; 而模拟输出analogWrite()函数的输出值范围是0 到255; 所以: val = analogRead(potpin); / ...
- css3中的@font-face你真的了解吗
css3中的自定义字体方法@font-face @font-face属性可以让我们自定义网站字体属性,然后引用到想要应用该字体的元素上. 基本语法: @font-face { font-family: ...