炸裂!万字长文拿下HTTP 我在鹅厂等你!

3 不同版本的HTTP


HTTP/0.9

当时网络资源匮乏,0.9版本相对简单,采用纯文本格式,且设置为只读,所以当时只能使用"Get"的方式从服务器获得HTML文档,响应以后则关闭。如下图所示

GET /Mysite.html

响应中只包含了文档本身。响应内容无响应头,无错误码,无状态码,可以说是"裸奔"。

Hello worldHTTP/1.0

此时HTTP/0.9请求过程如下

  • 应用层的HTTP建立在传输层的TCP之上并运用TCP可靠性等特性,先三次握手建立连接

  • 客户端请求建立连接(此时只有GET)

  • 服务端响应请求,数据以 ASCII 字符流返回给客戶端

  • 传输完成,断开连接。

HTTP 0.9

HTTP1.0

随着时代的进步,仅仅文本的传输无法满足需求,更多情况需要采用图文的方式才能生动的表达出自己的观点。随着1995年开发出Apache,同时其他的多媒体等技术发展迅速,从而进一步的促使HTTP新功能的出现。HTTP1.0在1996年诞生,增加了一下几个方面:

  • 之前只有Get方法,现在增加Post(加参数),Head方法

  • 加入协议版本号,同时添加文件处理类型

  • 加入HTTP Header,让HTTP处理请求更加灵活

  • 增加响应状态码,标记出错的原因

  • 提供国际化(不同语言)支持

典型的请求过程

GET /image.html HTTP/1.0User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64)200 OKDate: Tue, 17 Nov 2020 09:15:31 GMTContent-Type: text/html 一个包含图片的页面

HTTP1.0通信过程

HTTP1.0

HTTP /1.1

1995年是不平凡的一年,网景公司和微软开启浏览器大战,谁都想当老大。1999年HTTP/1.1发布并成为标准,写入RFC,以为以后不管是网关还是APP等,只要你要使用HTTP,就得遵守这个标准。

  • 继续增加了PUT等方法

  • 允许持久连接

随着文件越来越大,图片等信息越来越复杂,如果每一次上传下载文件都需要建立连接断开连接的过程将增加大量的开销。为此,提出了持久连接,也就是一次TCP连接可以具有多个HTTP请求。当然持久连接是可选择的,如果考虑关闭,只需要使用Connecttion:close关闭即可。长连接如下图所示

长连接

  • 强制要求Host头

我们知道,在电商系统中,经常会因为促销活动导致流量飙升,为了缓解流量,其中有种方法即加缓存或者加服务器。如果是单台服务器负载过大,数据库可能分库分表。数据结构算法中分而治之方法亦是如此。那么HTTP中,同样的道理,如果文件太大,就大文件切分为小文件块发送。

HTTP /2

HTTP/1.1的出现,几年间出来大量牛掰的互联网公司,发展实在是太快,但是HTTP1.1中这几点成为诟病

  • 原因1 TCP自带慢启动

顾名思义,"慢启动"从0到1循循渐进。轿车启动不会按下按钮就直接起飞,而是缓慢调节到适合的速度。这不是挺好的?为什么会带来性能问题呢。我们知道一个页面有静态数据,动态页面,很多小文件在加载的过程中就会直接发起请求,这样导致太多的请求都会经历慢启动过程,花费时间太多。

  • 原因2 多条TCP连接带宽竞争

带宽固定,多条TCP连接同时发起竞争带宽资源,由于各个TCP连接之间没有通信机制,也无法得知哪些资源优先级更高,从而导致想快速下载的资源反而延迟下载。

  • 原因3 头部阻塞

阻塞,在网络编程中,我们采用异步,多路复用(epoll)方式尽量让cpu少等待多干事。在HTTP1.1中,虽然大家共用了一条TCP通道,但是第一个请求没有结束,第二请求就可能阻塞等待,也就是说不能同时发送接收数据。那么一个网页很多数据文件,如果能够同时发出请求,让部分数据文件能够得到响应并预处理,这样就大大的利用了带宽和cpu的资源。基于这些因素,在HTTP2中出现了新的方案

如何解决头部阻塞呢?

HTTP是一问一答的模式,大家都在这个队列排队导致堵塞,那就多个队列并发进行,也就是"对同一个域名发起多个长连接"。举个例子,在火车站排队买票的时候,如果只有一个窗口可用,大家只能苦等,多开几个窗口就可缓解这个问题。

这个时候用户数 * 并发数(上限6-8)已经不错得效果,但是互联网速度太快,火车站就这么大,窗口也就这么多,怎么办,建新的火车站进行分流(大部分城市都有什么东站 西站)。在这里叫做"域名分片",使用多个域名,这些域名指向同一服务器。

HTTP/3

HTTP/2看似很完美了吧,但是Google轮子哥可不服,其他人在研究HTTP/2的时候,它们就在琢磨QUIC。那QUIC有啥牛掰的地方呢

QUIC是Google开发的一个基于UDP且能像TCP一样具有可靠性特点的协议。具备像HTTP/2一样的应用数据二进制分帧传输。其主要解决的问题有两个。

  1. 进一步解决线头阻塞问题。通过独立不同流,让各个流之间实现相互独立传输,互不干扰

  2. 切换网络时的连接保持。wifi和3G/4G经常需要来回切换。基于TCP的协议,会因为网络的切换导致IP地址的改变。而基于UDP的QUIC协议,及时切换也可以恢复之前与服务器的连接。(这里推荐大家可以去看看MPTCP)

4 HTTP报文详解


客户端与服务端进行交互的信息为报文。客户端为请求报文,服务端为响应报文。我们先用wireshark抓一个博客看看

报文层次结构

GET /article/12 HTTP/1.1Host: www.xxx.cnConnection: keep-aliveCache-Control: max-age=0Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.106 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.9Accept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9Cookie: SESSION=so9nlsvenminor5abs65sh9dsa

HTTP/1.1 200 OKServer: nginxDate: Sun, 17 May 2020 17:04:29 GMTContent-Type: text/html; charset=UTF-8Transfer-Encoding: chunkedConnection: keep-aliveVary: Accept-EncodingX-Powered-By: blade-2.0.6-BETAContent-Encoding: gzip

请求报文

请求报文

请求报文通常由三部分组成:

起始行:描述请求或者响应的基本信息

头部字段集合:key-value形式说明报文

消息正文:实际传输诸如图片等信息。具体如下图试试

1 请求方法:一共有八种方法选择,如下图所示。采用不同的方法获取不同的资源

HTTP请求方法详解

说一下非常常见的几种请求方法

Get:从服务器中取资源。可以请求图片,视频等

HEAD:和Get类似,但是从服务器请求的资源不会反悔请求的实体数据,只会反悔响应头

POST/PUT:对应于GET,向服务器发送数据

2 URI

统一资源标识符(Uniform Resource Identifier),严格来说不等于网址,它包含URL和URN,可是URL太出名了以致于URL=“网址”。无论开发,测试运维配置都离不开URI,所以好好掌握。

网络层的IP主要目的是解决路由和寻址。现在的IP地址按照".“分割,总共2的32次方大约42亿。对于计算机来说比较方便,但是对于人类来说还是不容易记忆,此时出现DNS了,他把IP地址映射为我们平时常见的"redis.org”,按照".“分割域名,从左到右级别越高,最右边为"顶级域名”。如下图所示

域名体系

好了,现在TCP提供可靠(数据不丢失)且字节流(数据完整性),而且也有方便我们记忆的域名,但是互联网资源千万种,也不知道访问什么(图片,文字,视频一大堆),这个时候URI(统一资源标识符)出现了,那长啥样?

URI格式

协议名:HTTP协议,另外还有ftp等协议。告知访问资源时使用什么协议。

紧接着是分隔符:“😕/”

主机名:标记互联网主机,可以是IP也可以是域名,如果不写端口则使用默认端口,例如HTTP为80,HTTPS为443.

登录认证信息:登录主机时的用户名密码(不建议,直接告诉了别人你的隐私信息)

主机名:此处可以是域名也可以是IP,如果不写端口号则是默认端口。比如HTTP默认端口为80,HTTPS默认端口为443

资源所在位置:资源在主机上的位置,使用“/”分隔多级目录,在这里是“/en/download.html”。注意,必须"/"开头

参数:用"?“开始,表示额外的请求要求。通常使用"key=value"的方式存在,如果多个"key=value"则使用”&"相连。

看几个例子

http://nginx.org/en/download.html

file:///E:/Demo/index/

这里注意是三个"///“,因为前面”😕/“作为分隔符,资源路径按照”/"开头。

既然规则这么多,对于接收方而言需要完成的解析也需要遵守规则,全球用户很多使用HTTP,每个国家地区所使用语言不同,HTTP为了能对其进行统一处理,引入了URI编码,方法比较简单,将非ASCII或者特殊字符全部转换为十六进制字节值,同时在前面加入"%“。比如空格被转换为”%20",“中国"就编码为”%E4%B8%AD%E5%9B%BD%0A"。

3 请求体

响应报文

响应报文

状态行----服务器响应的状态

<1> 版本号:使用的HTTP什么版本

<2> 状态码:不同数字代表不同的结果,就如我们在编码时,通过返回不同的值代表不同的语义。

状态码一共分为5类。

1××:处于中间状态,还需后续操作

2××:成功收到报文并正确处理

“200 OK”

最常见的成功状态码,表示一切正常,客户端获得期许的处理结果。如果不是Head请求,那么在响应头中通常会有body数据。

“204 No Content”

这个的含义和"200"很相似,不同之处在于它的响应头中没有body数据。

“206 Partial Content”

是 HTTP 分块下载或断点续传的基础,在客户端发送“范围请求”、要求获取资源的部分数据时出现,它与 200 一样,也是服务器成功处理了请求,但 body 里的数据不是资源的全部,而是其中的一部分。状态码 206 通常还会伴随着头字段“Content-Range”,表示响应报文里 body 数据的具体范围,供客户端确认,例如“Content-Range: bytes 0-99/5000”,意思是此次获取的是总计 5000 个字节的前 100 个字节。

3××:重定向到其他资源位置

“301 Moved Permanently”

“永久重定向”,意思是本地请求的资源以及不存在,使用新的URI再次访问。

“302 Found”

“Moved Temporarily”,“临时重定向”,临时则所请求的资源暂时还在,但是目前需要用另一个URI访问。

301 和 302 通过在字段Location中表明需要跳转的URI。两者最大的不同在于一个是临时改变,一个是永久改变。举个例子,有时候需要将网站全部升级为HTTPS,这种永久性改变就需要配置永久的"301"。有时候晚上更新系统,系统暂时不可用,可以配置"302"临时访问,此时不会做缓存优化,第二天还会访问原来的地址。

“304 Not Modified”

运用于缓存控制。它用于 If-Modified-Since 等条件请求,表示资源未修改,可以理解成“重定向已到缓存的文件”(即“缓存重定向”)。

4××:请求报文有误,服务器无法处理

"400 Bad Request”

通用错误码,表示请求报文有错误,但是这个错误过于笼统。不知道是客户端还是哪里的错误,所以在实际应用中,通常会返回含有明确含义的状态码。

“403 Forbidden”

注意了,这一个是表示服务器禁止访问资源。原因比如涉及到敏感词汇、法律禁止等。当然,如果能让客户端有一个清晰的认识,可以考虑说明拒绝的原因并返回即可。

“404 Not Found”

这可能是我们都知道且都不想看到的状态码之一,它的本意是想要的资源在本地未找到从而无法提供给服务端,但是现在,只要服务器"耍脾气"就会给你返回 404,而我们也无从得知后面到底是真的未找到,还是有什么别的原因,

“405 Method Not Allowed”

获取资源的方法好几种,我们可以对某些方法进行限制。例如不允许 POST 只能 GET;

“406 Not Acceptable”

客户端资源无法满足客户端请求的条件,例如请求需要中文但只有英文;

“408 Request Timeout”

请求超时,服务器等待了过长的时间;

“409 Conflict”:

多个请求发生了冲突,可以理解为多线程并发时的竞态;

413 Request Entity Too Large:

请求报文里的 body 太大;

414 Request-URI Too Long:请求行里的 URI 太大;

429 Too Many Requests:客户端发送了太多的请求,

通常是由于服务器的限连策略;

431 Request Header Fields Too Large:请求头某个字

段或总体太大;

5××:服务器错误,服务器对请求出的时候发生内部错误。

“500 Internal Server Error”

和400 类似,属于一个通用的错误码,但是服务器到底是什么错误我们不得而知。其实这是好事,尽量少的将服务器资源暴露外网,尽量保证服务器的安全。

“502 Bad Gateway”

通常是服务器作为网关或者代理时返回的错误码,表示服务器自身工作正常,访问后端服务器时发生了错误,但具体的错误原因也是不知道的。

“503 Service Unavailable”

表示服务器当前很忙,暂时无法响应服务,我们上网时有时候遇到的“网络服务正忙,请稍后重试”的提示信息就是状态码 503。

503 是一个“临时”的状态,

暂时比较忙,稍后提供服务。在响应报文中的“Retry-After”字段,指示客户端可以在多久以后再次尝试发送请求。

4 请求体

上面大部分都是涉及到header部分,还有非常重要的body,everybody

头字段注意事项

<1> 字段名不区分大小写,例如“Host”也可以写成“host”,但首字母大写的可读性更好;

<2> 字段名里不允许出现空格,可以使用连字符“-”,但不能使用下划线"_"。例如,“test-name”是合法的字段名,而“test name”“test_name”是不正确的字段名;

<3> 字段名后面必须紧接着“:”,不能有空格,而":"后的字段值前可以有多个空格;

<4> 字段的顺序是没有意义的,可以任意排列不影响语义;

<5> 字段原则上不能重复,除非这个字段本身的语义允许,例如 Set-Cookie。

HTTP的body常常被分为这几种的类别

<1> text:超文本text/html,纯文本text/plain

<2> audio/video:音视频数据

<3> application: 可能是文本,也可能是二进制,交给上层应用处理

<4> image: 图像文件。image/png等

但是带宽一定,数据大了通常考虑使用压缩算法进行压缩,在HTTP中使用Encoding type表示,常用的压缩方式有下面几种

<1> gzip:

一种数据格式,默认且目前仅使用deflate算法压缩data部分

<2> deflate:

deflate是一种压缩算法,是huffman编码的一种加强

<3> br:

br通过变种的LZ77算法、Huffman编码以及二阶文本建模等方式进行数据压缩,其他压缩算法相比,它有着更高的压塑压缩效率

使用相应的压缩方法在带宽一定的情况下确实有不错的效果,但是gzip等主要针对文件压缩效果不错,但是对视频就不行了。这个时候是不是可以使用数据结构中常用的分而治之,大化小再合并的方式呢,

文件拆分

ok,在报文中使用"Transer-Encoding:chunked"表示,代表body部分数据是分块传输的。另外在body中存在一个content-length字段表示body的长度,两者不能共存,另外很多时候是流式数据,body中没有指明content-length,这个时候一般就是chunked传输了。

现在可以通过采用分块的方式增强带宽的利用率,那他的编码规则如何呢

<1> 每一个分块包含长度和数据块

<2> 长度头按照CRLF结束

<3> 数据块在长度快后,且最后CRLF结尾

<4> 使用长度0表示结束,“0\r\n\r\n”

我们还是看图加深印象

chunked分块

分块解决了咋们一部分问题,但是有的时候我们想截断发送怎么办呢。在HTTP中提供了使用字段“Accept - Ranges: bytes”,明确告知客户端:“我是支持范围请求的”。那么Range范围是怎样的呢,Range从0开始计算,比如Range:0-5则读取前6个字节,服务器收到了这个请求,将如何回应呢

<1> 合法性检查。比如一共只有20字节,但是请求range:100-200。此时会返回416----“范围请求有误”

<2> 范围正常,则返回216,表示请求数据知识一部分

<3> 服务器端在相应投资端增加Content-Range,格式"bytes x-y/length"。

敲黑板:断点续传怎么操作?

<1> 查看服务器是否支持范围请求并记录文件大小

<2> 多个线程分别负责不同的range

<3> 下载同时记录进度,即使因为网络等原因中断也没事,Range请求剩余即可

现在我们通过MIME-TYPE和Encoding-type可以知道body部分的类型,下一步将是对内容进行协商。HTTP中,请求体中使用Accept告诉服务端需要什么类型数据(我能处理哪些类型数据),响应头中使用Content表明发送了什么类型数据,具体如下图所示

好了,为了各个国家民族顺利友好的沟通和明确的区分。HTTP请求头中使用"type-subtype",注意此时分隔符是"-"。比如en-GB表示英式英语,zh-CN表示常用的汉语,那对于客户端而言,它通过Accept-Language来标记自己可以理解的自然语言,对应的服务端使用Content-Language表明实体数据使用的语言类型,如下图所示。

字符集和编码

Cookie机制

HTTP是无状态、无记忆的,Cookie机制的出现让其有记忆功能,是怎么个实现呢

Cookie

从上图我们可以知道Cookie是由浏览器负责存储,并不是操作系统负责,我们换个浏览器打开同样的网页,服务就认不出来了。

Cookie常见的应用一个是身份识别,一个是广告追踪,比如我们在访问网页视频或者图片的时候,广告商会悄悄给我们Cookie打上标记,方便做关联分析和行为分析,从而给我推荐一些相关内容。

HTTP代理

之前介绍的都是一问一答的情景,但是在大部分的情况下都会存在多台服务器进行通信服务。其中比较常见的就是在请求方与应答方中间增加一个中间代理。

代理

代理作为中间位置,相对请求方为服务端,相当于后端服务端为请求方。代理常见的功能为负载均衡。在负载均衡中需要区分正向代理与反向代理,其中也就会涉及调度算法,比如轮询,一致性哈希等。

正向代理与反向代理

那么问题来了,代理作为隐藏身份,相当于隐藏了真实的客户端与服务端,那在是不是

5 HTTPS


好人占多数,坏人也不少。总有些要搞坏事,因为HTTP是明文,所以需要想办法保护明文,从而出现了https。

安全是什么

安全四要素

机密性

对信息进行保密,只能可信的人可以访问(让我想起时间管理者)。

完整性

数据在传输过程中内容不被"篡改"。虽然机密性对数据进行保密了,但是有上策也有下策(hack)

身份认证

证明自己的身份是本人,保证其消息发给可信的人

不可否认

君子一言驷马难追,说话算数,说过的话做过的事要有所保证

HTTPS

HTTP和HTTPS

从上图我们知道HTTPS无非是在传输层和应用层中间加了一层TLS,正是TLS紧跟当代密码学的步伐,尽全力的保障用户的安全。老规矩,我们用wireshark看看长什么样子。

TLS

可以看出在交互的过程中多了不少新东西,了解TLS,TLS由SSL握手协议,SSL修改密码规范协议,SSL警报协议,SSL记录协议组成。

TLS组成

SSL握手协议:

相对于三次握手

记录协议

记录为TLS发送接收数据的基本单位。它的自协议需要通过记录协议发出。如果多个纪录数据则可以一个TCP包一次性发出。

警报协议

类似HTTP状态码,通过反馈不同的消息进行不同的策略。

变更密码规范协议

告诉对方,从此刻开始,后续的数据将使用加密算法进行加密再传输。

对称加密与非对称加密

对称加密

对称加密,顾名思义,加密方与解密方使用同一钥匙(秘钥)。具体一些就是,发送方通过使用相应的加密算法和秘钥,对将要发送的信息进行加密;对于接收方而言,使用解密算法和相同的秘钥解锁信息,从而有能力阅读信息。

对称加密

非对称加密

在对称加密中,发送方与接收方使用相同的秘钥。那么在非对称加密中则是发送方与接收方使用的不同的秘钥。其主要解决的问题是防止在秘钥协商的过程中发生泄漏。比如在对称加密中,小蓝将需要发送的消息加密,然后告诉你密码是123balala,ok,对于其他人而言,很容易就能劫持到密码是123balala。那么在非对称的情况下,小蓝告诉所有人密码是123balala,对于中间人而言,拿到也没用,因为没有私钥。所以,非对称密钥其实主要解决了密钥分发的难题。如下图

非对称加密

其实我们经常都在使用非对称加密,比如使用多台服务器搭建大数据平台hadoop,为了方便多台机器设置免密登录,是不是就会涉及到秘钥分发。再比如搭建docker集群也会使用相关非对称加密算法。

混合加密

非对称加密算法,大多数是从数学问题演变而来,运算速度较慢。混合加密所谓取长补短。通信过程中使用RSA等解决密钥交换问题,然后使用随机数产生的在对称算法中的会话密钥,最后使用加密。对方使用私钥解密得到的密文取出会话秘钥,这样就实现了密钥交换。

混合加密

通过混淆加密等方式完成了机密性任务,作为Hack只需要伪造发布公钥或者作为之间人窃听密文。但是我们知道安全是四要素,还需要保证数据的完整性,身份认证等。

摘要

摘要算法可以理解为一种特殊的"单向"加密算法,无密钥,不可逆。在平时项目中,应该大家都是用过MD5,SHA-1。但是在TLS中使用SHA-2。

假设小A转账5000给小C,小A加上SHA-2摘要。网站计算摘要并对比,如果一致则完整可信。

面试宝典

面试必问知识点、BATJ历年历年面试真题+解析

学习经验总结

(一)调整好心态
心态是一个人能否成功的关键,如果不调整好自己的心态,是很难静下心来学习的,尤其是现在这么浮躁的社会,大部分的程序员的现状就是三点一线,感觉很累,一些大龄的程序员更多的会感到焦虑,而且随着年龄的增长,这种焦虑感会越来越强烈,那么唯一的解决办法就是调整好自己的心态,要做到自信、年轻、勤奋。这样的调整,一方面对自己学习有帮助,另一方面让自己应对面试更从容,更顺利。

(二)时间挤一挤,制定好计划
一旦下定决心要提升自己,那么再忙的情况下也要每天挤一挤时间,切记不可“两天打渔三天晒网”。另外,制定好学习计划也是很有必要的,有逻辑有条理的复习,先查漏补缺,然后再系统复习,这样才能够做到事半功倍,效果才会立竿见影。

(三)不断学习技术知识,更新自己的知识储备
对于一名程序员来说,技术知识方面是非常重要的,可以说是重中之重。要面试大厂,自己的知识储备一定要非常丰富,若缺胳膊少腿,别说在实际工作当中,光是面试这一关就过不了。对于技术方面,首先基础知识一定要扎实,包括自己方向的语言基础、计算机基础、算法以及编程等等。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》
点击传送门,即可获取!

人窃听密文。但是我们知道安全是四要素,还需要保证数据的完整性,身份认证等。

摘要

摘要算法可以理解为一种特殊的"单向"加密算法,无密钥,不可逆。在平时项目中,应该大家都是用过MD5,SHA-1。但是在TLS中使用SHA-2。

假设小A转账5000给小C,小A加上SHA-2摘要。网站计算摘要并对比,如果一致则完整可信。

面试宝典

面试必问知识点、BATJ历年历年面试真题+解析

[外链图片转存中…(img-wbjp3J9A-1715340370537)]

学习经验总结

(一)调整好心态
心态是一个人能否成功的关键,如果不调整好自己的心态,是很难静下心来学习的,尤其是现在这么浮躁的社会,大部分的程序员的现状就是三点一线,感觉很累,一些大龄的程序员更多的会感到焦虑,而且随着年龄的增长,这种焦虑感会越来越强烈,那么唯一的解决办法就是调整好自己的心态,要做到自信、年轻、勤奋。这样的调整,一方面对自己学习有帮助,另一方面让自己应对面试更从容,更顺利。

(二)时间挤一挤,制定好计划
一旦下定决心要提升自己,那么再忙的情况下也要每天挤一挤时间,切记不可“两天打渔三天晒网”。另外,制定好学习计划也是很有必要的,有逻辑有条理的复习,先查漏补缺,然后再系统复习,这样才能够做到事半功倍,效果才会立竿见影。

(三)不断学习技术知识,更新自己的知识储备
对于一名程序员来说,技术知识方面是非常重要的,可以说是重中之重。要面试大厂,自己的知识储备一定要非常丰富,若缺胳膊少腿,别说在实际工作当中,光是面试这一关就过不了。对于技术方面,首先基础知识一定要扎实,包括自己方向的语言基础、计算机基础、算法以及编程等等。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》
点击传送门,即可获取!

8-1 用QPainter绘图(Painting with QPainter) 2011-10-26 19:56:04| 分类: 默认分类 | 标签: |字号大中小 订阅 8-1 用QPainter绘图(Painting with QPainter) 分类: C++ GUI Programming with Qt 4 2007-05-29 21:52 8228人阅读 评论(3) 收藏 举报 要在绘图设备(paint device,一般是一个控件)上开始绘制,我们只要创建一个QPainter,把绘图设备指针传给QPainter对象。例如: oid MyWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); ... } 使用QPainter的draw…()函数我们可以绘制各种图形。图8.1给出了主要的一些。绘制的方式由QPainter的设置决定。设置的一部分是从绘图设备得到的,其他是初始化时的默认值。三个主要的设置为:画笔,刷子和字体。 画笔用来绘制直线和图形的边框。包含颜色,宽度,线型,角设置和连接设置。 刷子是填充几何图形的方式。包含颜色,方式设置,也可以是一个位图或者渐变色。 字体用来绘制文本。字体的属性很多,如字体名,字号等。 这些设置随时可以改变,可用QPen,QBrush,QFont对象调用setPen(),setBrush(),setFont()修改。 Figure 8.1. QPainter's most frequently used draw...() functions Figure 8.2. Cap and join styles < XMLNAMESPACE PREFIX ="O" /> < XMLNAMESPACE PREFIX ="V" /> Figure 8.3. Pen styles Figure 8.4. Predefined brush styles 现在来看看具体的例子。下面的代码是绘制图8.5(a)中椭圆的代码: QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); painter.setPen(QPen(Qt::black, 12, Qt::DashDotLine, Qt::RoundCap)); painter.setBrush(QBrush(Qt::green, Qt::SolidPattern)); painter.drawEllipse(80, 80, 400, 240); 调用函数setRenderHint(QPainter::Antialiasing,true),使绘制时边缘平滑,使用颜色浓度的变化,把图形的边缘转换为象素时引起的扭曲变形尽可能减少,在支持这一功能的平台或者绘图设备上得到一个平滑的边缘。 Figure 8.5. Geometric shape examples 下面的代码是图8.5(b)中绘制扇形的代码: QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); painter.setPen(QPen(Qt::black, 15, Qt::SolidLine, Qt::RoundCap, Qt::MiterJoin)); painter.setBrush(QBrush(Qt::blue, Qt::DiagCrossPattern)); painter.drawPie(80, 80, 400, 240, 60 * 16, 270 * 16); 函数drawPie()的最后两个参数值的单位为一度的十六分之一。 下面的代码是图8.5(c)中绘制贝赛尔曲线的代码: QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); QPainterPath path; path.moveTo(80, 320); path.cubicTo(200, 80, 320, 80, 480, 320); painter.setPen(QPen(Qt::black, 8)); painter.drawPath(path); 通过连接基本图形元素,直线,椭圆,多段线,圆弧,二次和三次贝塞尔曲线等,QPainterPath类能确定任何矢量图形。因此,绘图路径(Painter paths)是最基本的绘制元素,任何图形和图形的组合都可以同路径(path)表示。 一个路径能够确定一个轮廓,由这个轮廓确定的区域可以由刷子来填充。在图8.5(c)中我们没有设置刷子,因此只绘制了轮廓。 以上的三个例子都是使用了默认的刷子(Qt::SolidePattern,Qt::DiagCrossPattern,Qt::NoBrush)。在现在的应用程序中,单色填充已经很少使用,渐变色填充开始收到欢迎。渐变是依靠颜色的变化实现两种或者多种颜色之间平滑的过渡。渐变通常用来处理3D效果,如使用Plastique渐变方式来表现QPushButtons。 Qt支持三种类型的渐变:线形渐变,圆锥渐变和圆形渐变(linear, conical, and radial)。下一节的OvenTimer例子就是在一个控件中使用了所有这三种渐变。 线形渐变由两个控制点和直线上的一系列颜色点组成。图8.6由下面的代码得到:在两个控制点之间,在三个不同的位置确定了三个不同的颜色值。位置有0到1的浮点数得到,0为第一个控制点,1为第二个控制点。不同位置点之间的颜色由差值计算得到。 LinearGradient gradient(50, 100, 300, 350); gradient.setColorAt(0.0, Qt::white); gradient.setColorAt(0.2, Qt::green); gradient.setColorAt(1.0, Qt::black); Figure 8.6. QPainter's gradient brushes 圆形渐变由颜色组,圆心(xc,yc),半径r和焦点(xf,yf)定义。圆心和半径定义一个圆,颜色从焦点开始扩散到周围,焦点可以是圆心也可以是圆内的任意一个点。 圆锥渐变由圆心(xc,yc)和一个角度a定义。颜色从圆心开始像表的秒针一样扩散。 我们已经提到了QPainter的画笔,刷子和字体设置。此外,QPainter还有其他一些设置影响图形和文字的绘制: 1. 背景刷子,当背景模式为Qt::OpaqueMode(缺省值为Qt::transparentMode)时,背景刷子用来填充几何图形,文字,和位图的背景(在绘图刷子的下面) 2. 刷子的起点:刷子的起始绘制点,通常为控件的左上角。 3. 剪辑区域,剪辑区域为绘图设备上可以绘制的区域,在剪辑区域意外进行的绘制是无效的。 4. 视口,窗口,世界坐标:这三个决定了QPainter的逻辑坐标映射到物理坐标的方式。通常,逻辑坐标和物理坐标是重合的。坐标系统在下一节介绍。 5. 组合方式:组合方式决定绘制设备上新绘制的象素和已经存在的象素的影响方式。缺省方式为覆盖式(source over),新象素画在已有元素的上面。只是有限一个绘图设备支持组合方式的设置,将在本章后面介绍 在任何时候,我们可以调用save()把QPainter当前的设置保存在一个内部栈里,然后调用restore()进行恢复。我们能够临时改变QPainter的一些设置,然后恢复先前的值 Qt的二位图形引擎是基于 QPainter 类的。它既可以绘制几何形状,也可以绘制像素映射、图像和文字。自定义窗口部件需要重新实现 QWidget::paintEvent() 。 void Widget:: paintEvent( QPaintEvent * /*paintEvent*/ ) { draw(); drawBezier(); } void Widget:: draw() { QPainter painter( this ); painter. setPen( QPen ( Qt :: black, 10 , Qt :: DashDotLine, Qt :: RoundCap)); painter. setBrush( QBrush ( Qt :: green, Qt :: SolidPattern)); painter. drawEllipse( 10 , 10 , 400 , 240 ); painter. drawRect( QRect ( 10 , 300 , 400 , 140 )); } 首先需要创建一个 QPainter ,将需要绘图的设备的指针传递给 QPainter 。 QPainter 有各种 draw…() 可以绘制不同的图形。画笔、画刷、字体是最重要的三种设置。 画笔用来画线和边缘, QPainter ::setPen() 可以对画笔进行设置,包括颜色、宽度、线型、拐点风格等。 画刷用来填充几何形状的图案, QPainter ::setBrush() 可以对画刷进行设置,包括颜色和纹理风格。 绘制文本时需要对字体进行设置, QPainter ::setFont() 对字体进行设置,包括字体族和磅值。 void Widget:: drawBezier() { QPainter painter( this ); painter. setRenderHint( QPainter :: Antialiasing, true ); QPainterPath path; path. moveTo( 10 , 320 ); path. cubicTo( 200 , 80 , 400 , 80 , 480 , 320 ); painter. setPen( QPen ( Qt :: black, 8 )); painter. setBrush( QBrush ( Qt :: green, Qt :: DiagCrossPattern)); painter. drawPath( path); } QPainterPath 可以通过连接基本的图形单元元素来确定任意的矢量形状,包括:直线、椭圆、多边形、弧形、贝赛尔曲线等。 首先使用 QPainter ::moveto() 来确定图形的起点,然后通过 cubicTo() 来确定绘制的线路。前 4 个参数确定了两个控制点,后两个参数是结束点的位置。 修改函数如下: void Widget:: drawBezier() { QPainter painter( this ); painter. setRenderHint( QPainter :: Antialiasing, true ); QPainterPath path; path. moveTo( 10 , 320 ); path. cubicTo( 200 , 80 , 400 , 80 , 480 , 320 ); painter. setPen( QPen ( Qt :: black, 2 )); painter. drawPath( path); painter. setPen( QPen ( Qt :: red, 4 )); painter. drawPoint( 10 , 320 ); painter. drawPoint( 200 , 80 ); painter. drawPoint( 400 , 80 ); painter. drawPoint( 480 , 320 ); } 画笔样式 为了尝试画笔的样式,这里故意使用了一个新的画笔: painter.setPen(QPen(Qt::black, 5, Qt::DashDotLine, Qt::RoundCap)); 我们对照着API去看,第一个参数是画笔颜色,这里设置为黑色;第二个参数是画笔的粗细,这里是5px;第三个是画笔样式,我们使用了 DashDotLine,正如同其名字所示,是一个短线和一个点相间的类型;第四个是RoundCap,也就是圆形笔帽。然后我们使用一个黄色的画刷填充,画了一个椭圆。 后面的一个和前面的十分相似,唯一的区别是多了一句 painter.setRenderHint(QPainter::Antialiasing, true); 不过这句也很清楚,就是设置Antialiasing属性为true.如果你学过图形学就会知道,这个长长的单词就是"反走样".经过这句设置,我们就打开了QPainter的反走样功能。还记得我们曾经说过,QPainter是一个状态机,因此,只要这里我们打开了它,之后所有的代码都会是反走样绘制的了。 看到这里你会发现,反走样的效果其实比不走样要好得多,那么,为什么不默认打开反走样呢?这是因为,反走样是一种比较复杂的算法,在一些对图像质量要求不高的应用中,是不需要进行反走样的。为了提高效率,一般的图形绘制系统,如Java2D、OpenGL之类都是默认不进行反走样的。 还有一个疑问,既然反走样比不反走样的图像质量高很多,不进行反走样的绘制还有什么作用呢?前面说的是一个方面,也就是,在一些对图像质量要求不高的环境下,或者说性能受限的环境下,比如嵌入式和手机环境,是不必须要进行反走样的。另外还有一点,在一些必须精确操作像素的应用中,也是不能进行反走样的。请看下面的图片: 上图是使用Photoshop的铅笔和画笔工具画的1像素的点在放大到3200%视图下截下来的。Photoshop里面的铅笔工具是不进行反走样,而画笔是要进行反走样的。在放大的情况下就会知道,有反走样的情况下是不能进行精确到1像素的操作的。因为反走样很难让你控制到1个像素。这不是 Photoshop画笔工具的缺陷,而是反走样算法的问题。如果你想了解为什么这样,请查阅计算机图形学里面关于反走样的原理部分。 反走样是图形学中的重要概念,用以防止“锯齿”现象的出现。很多系统的绘图API里面都会内置了反走样的算法,不过默认一般都是关闭的,Qt也不例外。下面我们来看看代码。这段代码仅仅给出了paintEvent函数,相信你可以很轻松地替换掉前面章节中的相关代码。 void PaintedWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.setPen(QPen(Qt::black, 5, Qt::DashDotLine, Qt::RoundCap)); painter.setBrush(Qt::yellow); painter.drawEllipse(50, 150, 200, 150); painter.setRenderHint(QPainter::Antialiasing, true); painter.setPen(QPen(Qt::black, 5, Qt::DashDotLine, Qt::RoundCap)); painter.setBrush(Qt::yellow); painter.drawEllipse(300, 150, 200, 150); } 声明:本文原创于yafeilinux的百度博客,http://hi.baidu.com/yafeilinux 转载请注明出处。 前面一节我们讲解了图片的显示,其中很多都用到了坐标的变化,这一节我们简单讲一下Qt的坐标系统,其实也还是主要讲上一节的那几个函数。这里我们先讲解一下Qt的坐标系,然后讲解那几个函数,它们分别是: translate()函数,进行平移变换;scale()函数,进行比例变换;rotate()函数,进行旋转变换;shear()函数,进行扭曲变换。 最后介绍两个有用的函数save()和restore(),利用它们来保存和弹出坐标系的状态,从而实现快速利用几个变换来绘图。 一、坐标系简介。 Qt中每一个窗口都有一个坐标系,默认的,窗口左上角为坐标原点,然后水平向右依次增大,水平向左依次减小,垂直向下依次增大,垂直向上依次减小。原点即为(0,0)点,然后以像素为单位增减。 例如: void Dialog::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setBrush(Qt::red); painter.drawRect(0,0,100,100); painter.setBrush(Qt::yellow); painter.drawRect(-50,-50,100,100); } 我们先在原点(0,0)绘制了一个长宽都是100像素的红色矩形,又在(-50,-50)点绘制了一个同样大小的黄色矩形。可以看到,我们只能看到黄色矩形的一部分。效果如下图。 二、坐标系变换。 坐标系变换是利用变换矩阵来进行的,我们可以利用QTransform类来设置变换矩阵,因为一般我们不需要进行更改,所以这里不在涉及。下面我们只是对坐标系的平移,缩放,旋转,扭曲等应用进行介绍。 1.利用translate()函数进行平移变换。 void Dialog::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setBrush(Qt::yellow); painter.drawRect(0,0,50,50); painter.translate(100,100); //将点(100,100)设为原点 painter.setBrush(Qt::red); painter.drawRect(0,0,50,50); painter.translate(-100,-100); painter.drawLine(0,0,20,20); } 效果如下。 这里将(100,100)点作为了原点,所以此时(100,100)就是(0,0)点,以前的(0,0)点就是 (-100,-100)点。要想使原来的(0,0)点重新成为原点,就是将(-100,-100)设为原点。 2.利用scale()函数进行比例变换,实现缩放效果。 void Dialog::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setBrush(Qt::yellow); painter.drawRect(0,0,100,100); painter.scale(2,2); //放大两倍 painter.setBrush(Qt::red); painter.drawRect(50,50,50,50); } 效果如下。 可以看到,painter.scale(2,2),是将横纵坐标都扩大了两倍,现在的(50,50)点就相当于以前的 (100,100)点。 3.利用shear()函数就行扭曲变换。 void Dialog::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setBrush(Qt::yellow); painter.drawRect(0,0,50,50); painter.shear(0,1); //纵向扭曲变形 painter.setBrush(Qt::red); painter.drawRect(50,0,50,50); } 效果如下。 这里,painter.shear(0,1),是对纵向进行扭曲,0表示不扭曲,当将第一个0更改时就会对横行进行扭曲,关于扭曲变换到底是什么效果,你观察一下是很容易发现的。 4.利用rotate()函数进行比例变换,实现缩放效果。 void Dialog::paintEvent(QPaintEvent *) { QPainter painter(this); painter.drawLine(0,0,100,0); painter.rotate(30); //以原点为中心,顺时针旋转30度 painter.drawLine(0,0,100,0); painter.translate(100,100); painter.rotate(30); painter.drawLine(0,0,100,0); } 效果如下。 因为默认的rotate()函数是以原点为中心进行顺时针旋转的,所以我们要想使其以其他点为中心进行旋转,就要先进行原点的变换。这里的painter.translate(100,100)将(100,100)设置为新的原点,想让直线以其为中心进行旋转,可是你已经发现效果并非如此。是什么原因呢?我们添加一条语句,如下: void Dialog::paintEvent(QPaintEvent *) { QPainter painter(this); painter.drawLine(0,0,100,0); painter.rotate(30); //以原点为中心,顺时针旋转30度 painter.drawLine(0,0,100,0); painter.rotate(-30); painter.translate(100,100); painter.rotate(30); painter.drawLine(0,0,100,0); } 效果如下。 这时就是我们想要的效果了。我们加的一句代码为painter.rotate(-30),这是因为前面已经将坐标旋转了30度,我们需要将其再旋转回去,才能是以前正常的坐标系统。不光这个函数如此,这里介绍的这几个函数均如此,所以很容易出错。下面我们将利用两个函数来很好的解决这个问题。 三、坐标系状态的保护。 我们可以先利用save()函数来保存坐标系现在的状态,然后进行变换操作,操作完之后,再用restore()函数将以前的坐标系状态恢复,其实就是一个入栈和出栈的操作。 例如: void Dialog::paintEvent(QPaintEvent *) { QPainter painter(this); painter.save(); //保存坐标系状态 painter.translate(100,100); painter.drawLine(0,0,50,50); painter.restore(); //恢复以前的坐标系状态 painter.drawLine(0,0,50,50);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值