一、简介
HTTP(Hypertext Transfer Protocol)是一种用于在客户端和服务器之间传输超文本数据的协议。它是互联网上应用最广泛的协议之一,用于在Web浏览器和Web服务器之间进行通信。
超文本:超文本(Hypertext)是一种文本的扩展形式,它通过使用超链接(Hyperlink)将文本与其他文本、图像、音频、视频等多媒体资源进行关联。超文本通过在文本中嵌入可点击的链接,使读者能够非线性地浏览和导航到相关的内容。如常用的HTML文本就属于超文本,它使用标记(Tags)和元素(Elements)来描述文本的结构和语义,并通过超链接来建立文本之间的关联。
HTTP的基本工作原理如下:
- 客户端(通常是Web浏览器)向服务器发送HTTP请求。
- 服务器接收到请求后进行处理,并返回一个HTTP响应。
- 客户端接收到响应后进行解析,并根据需要进行相应的处理,例如显示Web页面或执行其他操作。
HTTP的特点包括:
- 简单:HTTP的请求和响应都是基于文本的,易于理解和编写。
- 无状态:每个请求和响应之间是独立的,服务器不会保留客户端的状态信息。这导致每个请求都需要携带完整的信息,例如身份验证凭据。
- 可扩展:HTTP协议支持使用不同的方法(如GET、POST、PUT、DELETE等)进行不同类型的操作,同时支持扩展头部字段和自定义的数据格式。
常见的HTTP方法包括:
- GET:从服务器获取资源。
- POST:向服务器提交数据,通常用于提交表单数据或发送请求体。
- PUT:向服务器上传或替换资源。
- DELETE:从服务器删除资源。
二、HTTP报文
HTTP报文是在HTTP协议中用于在客户端和服务器之间传输数据的格式化结构。它包括请求报文和响应报文两种类型。
HTTP报文是文本格式的,通常使用ASCII字符编码。它们遵循特定的语法规则和格式,以确保在客户端和服务器之间进行有效的通信和数据传输。
2.1 请求报文
请求报文由客户端发送给服务器,用于请求特定的资源或执行特定的操作。一个典型的请求报文包括以下几个部分:
- 请求行(Request Line):包括请求方法、请求的URL和HTTP协议版本。如 GET /path/to/resource HTTP/1.1
- 请求头部(Request Header):由key/value对组成,每行为一对,key和value之间通过冒号(:)分割,主要用于通知服务端有关于客户端的请求信息,如Accept、User-Agent、Content-Type等。
- 空行(Blank Line):用于分隔请求头部和请求体。
- 请求体(Request Body):可选的,用于传输请求的数据,例如在POST请求中传递表单数据或上传文件。
2.1.1 请求头
请求头部(Request Header)是HTTP请求中的一部分,包含了客户端向服务器发送的关于请求的附加信息。请求头部以键值对的形式组织,每个键值对占据一行,以冒号分隔键和值,并以回车换行符(CRLF)结束。
以下是一些常见的请求头部字段及其作用:
- Host:指定要访问的服务器的主机名和端口号。例如: Host:
- User-Agent:标识发起请求的客户端应用程序或用户代理(如浏览器)的信息。例如: User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36
- Accept:指定客户端能够接受的响应内容类型。例如: Accept: text/html, application/json
- Content-Type:指定请求体中的数据类型。例如: Content-Type: application/x-www-form-urlencoded
- Content-Length:指定请求体(请求头部空行后面的部分)的长度(以字节为单位)。例如: Content-Length: 1024
- Authorization:用于进行身份验证的凭据信息,例如基本身份验证(Basic Authentication)的用户名和密码。例如: Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l
- Cookie:包含客户端在之前的请求中服务器设置的Cookie信息。例如: Cookie: sessionId=abc123
- Referer:指定当前请求的来源URL,常用于防止跨站请求伪造(CSRF)攻击。例如: Referer: http:///page1
- Cache-Control:指定客户端请求或服务器响应的缓存行为。例如: Cache-Control: no-cache
- Connection:是否启用长连接,HTTP/1.1默认开启。例如:开启 Connection: keep-alive ,关闭 Connection: close
Content-Type
常见的Content-Type:
类型 |
说明 |
text/html |
html格式,用于表示网页内容 |
text/plain |
纯文本格式,没有特定的格式要求 |
text/css |
CSS格式 |
text/javascript |
js格式 |
image/gif |
gif图片格式,图像文件的MIME类型,用于传输JPEG、PNG、GIF等图片格式 |
image/jpeg |
jpg图片格式 |
image/png |
png图片格式 |
audio/mpeg、audio/wav |
音频文件的MIME类型,用于传输MP3、WAV等音频格式 |
video/mp4、video/mpeg |
视频文件的MIME类型,用于传输MP4、MPEG等视频格式 |
application/pdf |
PDF(Portable Document Format)文件类型,用于传输PDF文档 |
application/x-www-form-urlencoded |
POST专用:HTML表单数据类型,键值对以URL编码形式进行编码 |
application/json |
POST专用:用来告诉服务端消息主体是序列化后的 JSON 字符串 |
text/xml |
POST专用:发送xml数据 |
multipart/form-data |
POST专用:用以支持向服务器发送二进制数据,以便可以在 POST 请求中实现文件上传等功能 |
2.2 响应报文
响应报文(Response Message)是在HTTP通信中服务器发送给客户端的消息。它包含了服务器对客户端请求的响应信息,以及相应的数据(如HTML页面、JSON数据等)。响应报文由三个部分组成:状态行、响应头部和响应体。
- 状态行(Status Line):状态行包含了HTTP协议版本、状态码和对应的状态消息。
- 响应头部(Response Header):响应头部包含了服务器对响应的附加信息,以键值对的形式组织。
- 响应体(Response Body):响应体包含了服务器返回给客户端的实际数据,如HTML页面、JSON数据等。响应体的内容和格式取决于服务器处理请求的结果。
示例:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
<!DOCTYPE html>
<html>
<head>
<title>Example</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
2.2.1 返回状态码
类别 |
状态码 |
说明 |
1xx(Informational,信息性状态码):表示请求已被接收,需要进一步处理 |
100 |
服务器已接收到请求的初始部分,客户端应继续发送剩余部分 |
101 |
服务器要求客户端切换协议 |
|
2xx(Success,成功状态码):表示请求已成功被服务器接收、理解和处理 |
200 |
请求成功,服务器返回所请求的数据 |
201 |
请求成功并且服务器创建了新资源 |
|
204 |
请求成功,但响应报文不包含实体主体部分 |
|
3xx(Redirection,重定向状态码):表示客户端需要采取进一步操作才能完成请求 |
301 |
永久重定向:请求的资源已永久移动到新位置 |
302 |
临时重定向:请求的资源临时移动到新位置 |
|
304 |
客户端的缓存副本仍然有效,可以直接使用缓存的数据 |
|
4xx(Client Error,客户端错误状态码):表示客户端发送的请求有错误 |
400 |
请求无效,服务器无法理解 |
401 |
请求需要进行身份验证 |
|
403 |
拒绝执行该请求,常是访问受限 |
|
404 |
请求的资源未找到 |
|
5xx(Server Error,服务器错误状态码):表示服务器在处理请求时发生了错误 |
500 |
服务器遇到了未知的错误 |
502 |
Bad Gateway,响应失败或响应超时,一般为网关或代理服务器出现错误 |
|
503 |
服务器暂时无法处理请求,通常是因为过载或维护 |
|
504 |
Gateway Timeout 网关超时 ,也就是上游服务器未能响应或无法连接,返回502状态码 |
2.2.2 响应头
响应头(Response Headers)是在服务器响应客户端请求时,包含在响应中的一组元数据信息。这些头部字段提供了关于响应的附加信息,如内容类型、缓存策略、安全性等。客户端收到响应后,可以使用这些头部字段来处理响应和采取适当的行动。
以下是一些常见的响应头部字段及其用途:
Content-Type
:指定响应的内容类型,如文本、HTML、JSON、图片等。例如:Content-Type: text/html; charset=utf-8
。Content-Length
:指定响应正文的长度(以字节为单位)。Cache-Control
:指定响应的缓存控制策略,如禁止缓存、缓存的有效期等。例如:Cache-Control: no-cache, max-age=3600
。Expires
:指定响应的过期时间,用于缓存控制。例如:Expires: Wed, 21 Feb 2024 10:00:00 GMT
。Last-Modified
:指定响应资源的最后修改时间。客户端可以使用此字段来判断资源是否已更改。例如:Last-Modified: Wed, 21 Feb 2024 08:30:00 GMT
。Location
:用于重定向响应,指定新的资源位置。例如:Location: http:///new-location
。Set-Cookie
:设置响应中的 Cookie。例如:Set-Cookie: sessionid=abc123; Path=/; Secure; HttpOnly
。Access-Control-Allow-Origin
:用于跨域请求,指定允许访问资源的源地址。例如:Access-Control-Allow-Origin: http://
。Content-Encoding
:指定响应正文的编码方式,如 gzip、deflate 等。
三、HTTP缓存
HTTP缓存是一种机制,用于在客户端和服务器之间存储和重用先前请求的资源副本,以提高性能和减少网络流量。通过使用缓存,可以减少对服务器的请求次数,加快页面加载速度,并降低带宽消耗。
3.1 私有缓存,共享缓存
私有缓存是指缓存在单个用户或客户端浏览器中的缓存副本。当用户或浏览器请求资源时,如果资源可以使用私有缓存,则浏览器会直接从自己的缓存中获取资源,而无需向服务器发送请求。私有缓存仅对当前用户可见,不会被其他用户共享。它可以存储静态资源(如图片、样式表、脚本文件等)以及动态内容(如页面片段、AJAX响应等)。
私有缓存常用的设置方法是通过设置请求/响应头部字段来控制缓存的行为,如Cache-Control
和Expires
。通过设置适当的缓存策略,可以使浏览器在一定时间内直接使用缓存副本,提高页面加载速度和减少对服务器的请求次数。
共享缓存是指缓存在多个用户之间共享的缓存副本。多个用户可以共享同一个缓存副本,从而减少对服务器的请求次数和网络带宽消耗。共享缓存通常位于代理服务器或者CDN(内容分发网络)等中间节点上,它可以缓存常用的静态资源,如图片、CSS文件、JavaScript等,并为多个用户提供服务。共享缓存可以通过缓存机制和缓存策略来提高整体的系统性能和资源利用率。
共享缓存常见的实现方式包括反向代理缓存和CDN缓存。反向代理缓存是在代理服务器上缓存服务器上的资源,为多个用户提供服务;CDN缓存则是通过在全球各地的边缘节点上缓存资源,并根据用户的地理位置选择最近的节点提供服务。
注:本文叙述的是私有(浏览器)缓存,其他共享缓存暂不叙述。
3.2 缓存控制策略
3.2.1 强制缓存/Cache-Control
强制缓存是一种基于时间的缓存策略,通过设置 Cache-Control
头字段中的 max-age
或者 Expires
字段来指定资源的有效期限。当客户端再次请求资源时,在有效期内,客户端可以直接使用本地缓存副本,而无需发送请求到服务器。
强制缓存可以提供快速的访问速度和减轻服务器负载,适用于那些在一段时间内保持不变的静态资源。
Cache-Control字段在HTTP协议中既可以出现在请求头部也可以出现在响应头部,用于控制缓存的行为;在一次请求里,可设置多个多个Cache-Control
的值,每个值之间使用逗号进行分隔。例: Cache-Control: max-age=3600, no-cache, private
注:Cache-Control字段控制的是相对时间,绝对时间由Expires字段控制,不过这个是HTTP/1.0提出的,现已被Cache-Control替代,这里不做叙述
在请求头时:
请求头 |
说明 |
Cache-Control: no-store |
不使用缓存 |
Cache-Control: no-cache |
使用缓存前,无论本地副本是否过期,都需要请求源服务器进行验证(协商缓存验证) |
Cache-Control: max-age=秒 |
设置缓存存储的最大期限,超过这个期限缓存被认为过期,时间是相对于请求的时间 |
Cache-Control: max-stale=秒 |
客户端愿意接收一个已经过期的资源。可以设置一个可选的秒数,表示响应不能已经过时超过该给定的时间 |
Cache-Control: min-fresh=秒 |
客户端希望获取一个能在指定的秒数内保持其最新状态的响应 |
在响应头时:
响应头 |
说明 |
Cache-Control: no-store |
不使用缓存 |
Cache-Control: no-cache |
使用缓存前,无论本地副本是否过期,都需要请求源服务器进行验证(协商缓存验证) |
Cache-Control: max-age=秒 |
设置缓存存储的最大期限,超过这个期限缓存被认为过期,时间是相对于请求的时间 |
Cache-Control: s-maxage=秒 |
同 max-age,仅适用于共享缓存 |
Cache-Control: private |
私有缓存,响应只能被单个客户端缓存 |
Cache-Control: public |
共享缓存,即由缓存代理服务器提供的缓存,响应可以被多个客户端缓存 |
Cache-Control: must-revalidate |
如果本地副本未过期,则可继续供客户端使用,不需要向源服务器再验证;如果本地副本已过期(比如已经超过 |
Cache-Control: proxy-revalidate |
同 |
缺点:
- 如果资源在缓存期间发生了变化,客户端将无法获得最新的版本,因为它仍然使用着过期的缓存副本。
- 强制缓存对于频繁更新的资源(如新闻文章、动态内容)不够灵活。即使资源在有效期内,客户端也需要经常检查服务器以获取最新的内容。
3.2.2 协商缓存
协商缓存是一种基于验证的缓存策略,通过使用 ETag
和 Last-Modified
头字段来验证资源的有效性。当客户端再次请求资源时,客户端会发送上一次获取的 ETag
和/或 Last-Modified
给服务器,服务器根据这些验证信息判断资源是否发生了变化。如果资源未发生变化,服务器返回 304 Not Modified 状态码,客户端可以继续使用本地缓存副本;如果资源发生了变化,服务器返回新的资源副本。
协商缓存解决了强制缓存无法及时获取最新资源的问题,并在资源未发生变化时避免了不必要的数据传输;但会增加额外的请求和处理开销,增加服务器负载。
适用于动态内容、频繁更新的资源以及需要确保数据一致性的场景。
过程:
- 客户端第一次请求:第一次无缓存,请求头正常设置
- 服务端第一次响应:响应头中携带ETag字段(实体标签,唯一标识符,用于标识和验证资源的版本),Last-Modified字段(用于指示服务器上资源的最后修改时间)
- 客户端将返回数据在本地缓存一份
- 客户端第二次请求:请求头中携带If-None-Match字段,值为第一次请求获取到的ETag字段值;If-Modified-Since字段,值为第一次请求获取到的Last-Modified字段值
- 服务端第二次响应:服务器将请求头中携带的If-None-Match字段值与当前服务器存储的ETag字段值作比较,If-Modified-Since字段与Last-Modified字段值作比较,如果值相同,则表示资源没有发生变化,返回304状态码,不返回数据;如果值不同,则返回数据和状态码200
- 客户端若收到304状态码,则从本地缓存中获取所需资源
3.2.2.1 Last-Modified + If-Modified-Since
Last-Modified(最后修改时间)是HTTP协议中的一个响应头字段,用于指示服务器上资源的最后修改时间。它表示资源在服务器上最后一次被修改的时间戳。
If-Modified-Since 是HTTP协议中的一个请求头字段,用于条件性获取资源。它通常与服务器返回的响应头字段 Last-Modified 一起使用,用于验证资源是否发生了修改。
Last-Modified,If-Modified-Since 的精度通常是秒级别,因为大多数文件系统只记录文件的最后修改时间到秒。在比较 Last-Modified 时,如果两个时间戳非常接近,例如相差不到一秒,服务器可能会将它们视为相等,从而返回 304 Not Modified 状态码。
通过使用 Last-Modified+If-Modified-Since,服务端可以通过对比Last-Modified字段来验证资源是否发生了变化,可以减少不必要的数据传输并提高缓存效率。
然而,需要注意的是,Last-Modified 依赖于服务器和文件系统的时间戳准确性,并且对于某些动态生成的资源可能无法准确表示其修改状态。
适用于不需要高度精确的资源验证或者对时间戳精度要求不高的应用,或者资源的最后修改时间是可靠且准确的,并且可以满足资源验证的需求的场景
因此,对于某些情况下更精确的资源验证,可以使用 ETag 字段作为替代或补充。
3.2.2.2 ETag + If-None-Match
ETag(实体标签)是HTTP协议中的一个响应头字段,用于标识和验证资源的版本,它是由服务器生成的一个唯一标识符。
If-None-Match 是HTTP协议中的一个请求头字段,用于条件性获取资源。它通常与服务器返回的响应头字段 ETag 一起使用,用于验证资源是否发生了变化。
ETag 的生成方式可以是多种多样的,常见的方式包括:
基于内容的哈希值:服务器根据资源的内容计算哈希值,例如使用 MD5、SHA1 等算法生成唯一标识符。
时间戳和资源大小:结合资源的最后修改时间和大小生成一个唯一标识符。
通过使用ETag + If-None-Match,可以减少不必要的数据传输,只有在资源发生变化时才获取更新的副本。
但是,生生成和比较 ETag 值可能需要额外的计算和处理,对服务器资源消耗略高,对于某些静态资源,如图片或视频等,生成唯一的 ETag 可能会导致性能下降。
适用于当资源的内容经常变化,但最后修改时间不一定准确或者不可靠时,或者对于需要精确控制缓存的应用的场景。也可与Last-Modified一起使用。
注:If-None-Match 字段可以同时包含多个 ETag 值,以支持多个资源的验证。服务器在比较时需要检查客户端发送的 ETag 值是否与任意一个资源的 ETag 值匹配。
3.3 浏览器缓存图示
四、HTTP发展历程
4.1 HTTP/0.9
HTTP/0.9是最早的版本之一,于1991年发布。它是超文本传输协议(HTTP)的起始版本之一。
HTTP/0.9 极其简单:请求由单行指令构成,以唯一可用方法GET开头,其后跟目标资源的路径,它没有定义任何请求头或响应头,也没有定义HTTP方法(如GET、POST等),响应也极其简单的:只包含响应文档本身。
4.2 HTTP/1
4.2.1 HTTP/1.0
HTTP/1.0是超文本传输协议(HTTP)的一个版本,于1996年发布。它是HTTP/0.9的改进版,引入了一些新的特性和标准化,以提供更丰富的功能和更好的性能。
HTTP/1.0的主要特性包括:
- 请求头和响应头:HTTP/1.0引入了请求头和响应头的概念,通过标准化的首部字段来传递各种元数据信息。请求头和响应头可以包含不同的首部字段,用于传递关于请求和响应的各种信息,如内容类型、内容长度、缓存控制、认证等。
- HTTP方法:HTTP/1.0定义了几种常见的HTTP方法,包括GET、POST、HEAD、PUT、DELETE等。这些方法可以用于对服务器上的资源进行不同类型的操作,如获取资源、提交表单数据、修改资源等。
- 缓存控制:HTTP/1.0引入了一些缓存控制的首部字段,如Cache-Control和Expires,用于控制客户端和代理服务器的缓存行为。这些字段允许服务器指定资源的缓存策略,从而改善性能并减少网络流量。
- 持久连接:HTTP/1.0允许在单个TCP连接上处理多个请求和响应,以减少连接建立和关闭的开销。这种持久连接的机制可以提高性能,并减少了每个请求的延迟。
- Host头字段:HTTP/1.0引入了Host头字段,用于指定请求的目标主机。这使得在同一台服务器上托管多个网站成为可能,通过使用不同的Host值来区分不同的网站。
然而,HTTP/1.0仍然存在一些性能限制,如每个请求需要建立新的TCP连接、请求-响应阻塞等问题。
4.2.2 HTPP/1.1
HTTP/1.1是超文本传输协议(HTTP)的一个版本,于1999年发布。它是HTTP/1.0的改进版,引入了一些重要的特性和改进,以提供更好的性能、可扩展性和功能。
HTTP/1.1的主要特性包括:
- 持久连接:HTTP/1.1默认启用持久连接,即在单个TCP连接上可以处理多个请求和响应。这避免了在每个请求之后都建立和关闭TCP连接的开销,从而提高了性能和效率。
- 请求管道化:HTTP/1.1支持请求管道化,允许客户端在发送请求之前不必等待先前的响应。客户端可以同时发送多个请求,并且服务器可以按照请求的顺序逐个响应。这减少了请求的延迟,提高了吞吐量。
- 分块传输编码:HTTP/1.1引入了分块传输编码(Chunked Transfer Encoding),允许服务器将响应分成多个块进行传输。这对于动态生成的响应或大文件的传输非常有用,可以让客户端在接收到部分响应时开始处理,而无需等待整个响应完成。
- 虚拟主机:HTTP/1.1扩展了HTTP/1.0中的Host头字段,并将其作为请求头的一部分。这使得在同一台服务器上托管多个网站成为可能,服务器可以根据Host头字段的值来区分不同的网站。
- 缓存控制:HTTP/1.1引入了更多的缓存控制机制,如Cache-Control、ETag和Last-Modified等首部字段。这些字段允许服务器和客户端更精确地控制缓存的行为,提高了缓存的效率和一致性。
- 断点续传:HTTP/1.1支持断点续传,允许客户端在下载大文件时可以从中断的位置继续下载,而无需重新开始。这对于大文件的下载和网络不稳定的情况下非常有用。
缺点:
- 队头阻塞:HTTP/1.1采用串行的请求-响应模型,每个请求必须等待前一个请求的响应完成后才能发送,导致队头阻塞问题。这可能会导致延迟增加和性能下降。
- 头部冗余:每个HTTP请求都需要携带完整的头部信息,包括重复的字段,例如Cookie、User-Agent等。这样会导致头部冗余,增加了数据传输的开销。
- 请求限制:HTTP/1.1对并发请求数量的限制较为严格,通常一个浏览器对同一域名的并发连接数是有限制的,这可能会导致性能瓶颈。
- 无状态:HTTP/1.1是无状态的协议,每个请求和响应之间没有关联。这意味着服务器无法直接知道请求的上下文和状态,需要通过Cookie等机制来维护会话状态。
- 安全性限制:HTTP/1.1在传输数据时没有加密,容易受到窃听和篡改的风险。为了提供安全性,需要使用额外的安全层,如HTTPS。
- 半双工:客户端和服务器之间的通信是基于请求-响应模型的。这意味着在一个连接上,客户端发送请求,服务器返回响应,然后连接被关闭。在这个过程中,通信是单向的,即客户端发送请求,服务器返回响应,没有同时进行。
4.2.2.1 长连接
HTTP的Keep-Alive是一种HTTP协议的机制,用于在单个TCP连接上保持多个HTTP请求和响应的持久性,也被称为HTTP 长连接。
在HTTP的早期版本中,每次请求都需要建立一个新的TCP连接,这会产生较大的开销。为了减少连接建立的开销,HTTP的Keep-Alive机制允许多个HTTP请求和响应通过同一个TCP连接进行传输,从而提高性能。
在HTTP/1.0中,长连接(Keep-Alive)是可选的,并且需要在请求头中显式指定Connection: keep-alive
。而在HTTP/1.1中,默认开启了长连接,除非在请求头中显式指定Connection: close
来关闭连接。
示例(go):
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
// 创建一个自定义的 HTTP 客户端
client := &http.Client{
Transport: &http.Transport{
// 开启或关闭 HTTP 长连接
DisableKeepAlives: true, // 设置为 true 关闭长连接,设置为 false 开启长连接
},
Timeout: 10 * time.Second, // 设置超时时间为 10 秒
}
// 发送 GET 请求
resp, err := client.Get("http://")
if err != nil {
fmt.Println("Error sending request:", err)
return
}
defer resp.Body.Close()
// 处理响应
// ...
}
4.2.2.2 管道网络传输
管道网络传输是指通过在一个持久的TCP连接上发送多个HTTP请求来减少延迟并提高性能的技术。它是在HTTP/1.1中引入的特性。
在传统的非管道化HTTP中,每个HTTP请求都需要在发送请求后等待服务器的响应,然后才能发送下一个请求。这样的请求-响应模式会导致较高的延迟,尤其是在高延迟的网络环境下。
而在管道化的HTTP中,客户端可以在同一个TCP连接上发送多个请求,而无需等待先前的响应。服务器可以按照请求的顺序逐个响应这些请求。这样可以有效地利用网络带宽和连接,并减少整体的延迟。
管道化网络传输具有以下优点:
- 减少延迟:由于多个请求可以同时在一个连接上发送,而无需等待先前的响应,因此可以显著减少请求的延迟。
- 提高吞吐量:通过并行发送多个请求,可以更充分地利用网络带宽,从而提高整体的吞吐量。
- 资源利用率:由于减少了连接建立和关闭的开销,以及同时使用一个连接来发送多个请求,可以提高服务器和客户端的资源利用率。
需要注意的是,尽管HTTP管道化可以提供性能优势,但它也存在一些限制和注意事项:
- 不适用于所有场景:HTTP管道化适用于并发请求较多的情况,例如批量请求或资源密集型的Web应用。但对于某些特定的请求,如需要先前请求的响应结果作为依赖的请求,管道化可能不适用。
- 服务器支持:要实现管道化网络传输,服务器必须正确地处理和响应管道化请求。不是所有的服务器都完全支持管道化。
- 队头阻塞:HTTP管道化是按照请求的顺序依次发送的,服务器也按照请求的顺序依次响应。这意味着如果前面的请求耗时较长或出现延迟,后续的请求将会被阻塞,直到前面的请求完成并收到响应。
开启/关闭管道网络传输,每个程序有不同的方式,以下示例使用go语言:
package main
import (
"fmt"
"net/http"
)
func main() {
// 创建一个自定义的HTTP传输对象
transport := &http.Transport{
// 开启或关闭HTTP管道化
DisableKeepAlives: !true, // 设为true关闭管道化,设为false开启管道化
MaxConnsPerHost: 10, // 控制每个主机的最大并发连接数
}
// 创建一个自定义的HTTP客户端
client := &http.Client{
Transport: transport,
}
// 发送多个并发请求
for i := 0; i < 10; i++ {
go func() {
// 创建HTTP请求
req, err := http.NewRequest("GET", "http://", nil)
if err != nil {
fmt.Println("创建请求失败:", err)
return
}
// 发送HTTP请求
resp, err := client.Do(req)
if err != nil {
fmt.Println("请求失败:", err)
return
}
defer resp.Body.Close()
// 处理响应
// ...
fmt.Println("请求成功")
}()
}
// 等待所有请求完成
// ...
// 关闭HTTP传输连接
transport.CloseIdleConnections()
}
4.3 HTPP/2.0
HTTP/2.0(简称HTTP/2)是一种网络协议,用于在Web浏览器和服务器之间进行通信。它是HTTP/1.1的后续版本,旨在改进性能、效率和安全性。
HTTP/2.0相对于HTTP/1.1的主要改进包括:
- 多路复用:HTTP/2.0引入了二进制分帧层,允许多个请求和响应同时在同一个连接上进行,通过多路复用提高传输效率。这意味着可以避免HTTP/1.1中的队头阻塞问题,提高并发性能。
- 头部压缩:HTTP/2.0使用了HPACK压缩算法对头部信息进行压缩,减少了头部的数据量,降低了传输延迟和带宽消耗。
- 服务器推送:HTTP/2.0支持服务器主动推送数据,服务器可以在客户端请求之前主动将相关资源推送给客户端,提高页面加载性能。
- 优先级和依赖性:HTTP/2.0支持请求的优先级和依赖性设置,可以指定请求的重要性和依赖关系,确保关键资源的优先加载。
- 流量控制:HTTP/2.0引入了流量控制机制,可以动态地控制数据的发送速率,避免过载和拥塞。
- 二进制格式传输:数据在传输时使用二进制格式进行编码和解码,可以减少传输数据的大小,减少了网络带宽的占用,提高了传输效率。
HTTP/2相对于HTTP/1.1的改进使得网页加载速度更快,尤其在高延迟和带宽限制的网络环境下效果更为明显。然而,它仍然兼容HTTP/1.1,并在传输层使用TLS加密以提供更好的安全性。
缺点:
- 服务器支持:首先,需要确保Web服务器支持HTTP/2.0。大多数主流的Web服务器(例如Apache、Nginx)都已经支持HTTP/2.0。检查服务器配置,并确保已启用HTTP/2.0模块或插件。
- TLS加密:虽然HTTP/2.0不是强制要求使用TLS加密,但大多数浏览器只支持通过TLS加密的HTTP/2.0连接。因此,为了获得最佳兼容性和安全性,建议在使用HTTP/2.0时启用TLS加密,需要为域名配置有效的SSL证书。
- 更新浏览器:HTTP/2.0已经得到现代浏览器的广泛支持。确保使用的浏览器是最新版本,并支持HTTP/2.0。大多数主流浏览器(例如Chrome、Firefox、Safari)在较新的版本中已经支持HTTP/2.0。
- 队头阻塞:当一个流的数据传输遇到延迟或阻塞时,后续的流也会受到影响,无法及时传输。
例:nginx启用http2
server {
listen 443 ssl http2;
server_name ;
ssl_certificate /path/to/ssl_certificate.crt;
ssl_certificate_key /path/to/ssl_certificate.key;
...
}
4.3.1 头部压缩
在HTTP/2中,头部压缩是通过使用HPACK(HTTP/2 Static Table and Huffman Encoding)算法来实现的。它可以减少请求和响应头部的数据量,从而降低带宽占用和传输延迟。
HPACK算法的主要原理如下:
- 静态表(Static Table):HPACK定义了一个静态表,其中包含一些常见的HTTP头部字段和值的键值对。这些键值对在每个HTTP/2连接中都是固定的,无需重复传输。当头部字段和值与静态表中的条目匹配时,只需发送对应的索引号即可,减少了重复传输的数据量。
- 动态表(Dynamic Table):HPACK还维护了一个动态表,用于存储连接特定的头部字段和值对。动态表的大小是可配置的。当头部字段和值在动态表中找到匹配项时,只需发送对应的索引号即可。
- Huffman 编码:HPACK使用 Huffman 编码对头部字段和值进行压缩。Huffman 编码是一种变长编码,用于表示常见的头部字段和值,以减少其占用的比特数。这样可以进一步减小头部数据的传输量。
通过使用静态表、动态表和 Huffman 编码,HPACK可以有效地压缩头部数据,以便更高效地传输。在实际的请求和响应中,头部字段和值将被编码为索引号、字面量或组合的方式,并通过帧的形式在HTTP/2连接上传输。
需要注意的是,头部压缩是一种有损压缩算法,因为它会引入一些开销来管理静态表和动态表。但是,由于头部数据通常是重复的且具有较高的冗余性,HPACK的压缩效果通常是显著的,可以提供可观的性能优势。
4.3.2 二进制帧
在HTTP/2中,与HTTP/1.x不同,数据在传输时使用二进制格式进行编码和解码。这意味着HTTP/2的帧、流和头部等信息都是以二进制形式传输的。
HTTP/2使用二进制格式的好处包括:
- 效率:二进制格式相对于文本格式更紧凑,可以减少传输数据的大小。这减少了网络带宽的占用,提高了传输效率。
- 解析速度:解析二进制数据比解析文本数据更高效。二进制格式的帧和头部可以更快地被解码和处理,从而减少了延迟。
- 扩展性:二进制格式更容易扩展,可以在未来添加新的帧类型和扩展字段,而不会破坏现有的协议结构。
帧(Frames)
在HTTP/2中,数据传输通过帧(Frames)进行。帧是HTTP/2协议中最基本的数据单元,用于在客户端和服务器之间传输数据。
每个帧都由二进制数据组成,并包含以下字段:
- 长度(Length):指示帧有效负载的长度,以字节为单位。长度字段是一个无符号的24位整数,限制了帧有效负载的最大大小为16,777,215字节。
- 类型(Type):指示帧的类型,用于标识帧的用途和处理方式。HTTP/2定义了多种帧类型,例如头部帧(Headers Frame)、数据帧(Data Frame)、设置帧(Settings Frame)等。每种类型的帧都有特定的结构和语义。
- 标志(Flags):帧的标志字段提供了关于帧的附加信息。不同类型的帧具有不同的标志字段,用于指示特定的条件或行为。
- 流标识符(Stream Identifier):用于标识帧所属的流。每个流都有唯一的标识符,用于在连接上区分不同的数据流。流标识符是一个无符号的31位整数。
- 有效负载(Payload):帧的有效负载是实际的数据内容。它可以是请求头部、响应头部、数据块等,具体取决于帧的类型和用途。
HTTP/2定义了多种帧类型,包括但不限于:
- 数据帧(Data Frame):用于传输消息体的数据块。
- 头部帧(Headers Frame):用于传输请求头部或响应头部。
- 优先级帧(Priority Frame):用于指定流的优先级。
- 设置帧(Settings Frame):用于传输连接参数和配置信息。
- GOAWAY帧(GOAWAY Frame):用于通知对端关闭连接。
- 窗口更新帧(Window Update Frame):用于调整流或连接的流量控制窗口大小。
4.3.3 多路复用,并发传输
流(Stream)
在HTTP/2中,流(Stream)是建立在单个TCP连接上的双向数据传输通道,用于在客户端和服务器之间传输请求和响应。每个 Stream 都有唯一的标识符,称为 Stream Identifier,用于区分不同的流。
每个流都有自己的优先级(Priority),用于指定流的重要性和处理顺序。优先级信息通过设置帧(Settings Frame)进行传输,并在流的创建和处理过程中进行考虑,以优化资源分配和响应顺序。
流可以承载一个或多个 HTTP 请求和响应,用于在客户端和服务器之间传输数据。
流之间是独立的,它们可以以任意顺序发送和接收,并且可以并发处理;但是每个流都是有序的,即在接收方解析流的时候,必须按照流的顺序进行处理。这意味着在接收到流的数据之前,必须先处理之前流的数据。这样可以确保数据的顺序性和完整性。
消息(Message)
Message 是在一个 Stream 上发送的逻辑上相关的一组帧(Frame),一个 HTTP 请求或响应可以由一个或多个 Message 组成,每个 Message 都有唯一的 Stream Identifier,Message 是按照顺序发送和接收的,保证了消息的完整性和有序性。
在HTTP/2的通信过程中,消息被分割成多个帧进行传输,这些帧可以通过多个流同时进行传输。帧的顺序可以与流的顺序不同,但通过帧中的标识符和其他控制信息,可以将帧重新组装成消息,并在正确的顺序中进行处理。
客户端和服务器之间的流可以以任意顺序建立,并且双方都可以主动发起流的创建。客户端可以通过发送请求帧来创建新的流,并使用奇数编号的标识符。服务器可以通过发送推送帧(PUSH_PROMISE)来推送资源给客户端,并使用偶数编号的标识符。
多路复用(Multiplexing)
HTTP/2引入了多路复用(Multiplexing)的机制,是HTTP/1.x的一个重要改进之一。多路复用允许在单个HTTP/2连接上同时发送和接收多个请求和响应,而不需要按照顺序逐个处理。
在同一个TCP连接上并发发送和接收多个请求和响应。多个HTTP/2流(Stream)可以在同一个连接上同时传输,每个流都有唯一的标识符,用于区分和关联请求和响应。
多路复用的优势包括:
- 更高的性能:通过在单个连接上并行传输多个请求和响应,可以有效地利用网络带宽和资源,提高性能和吞吐量。相比于HTTP/1.x中串行处理请求的方式,多路复用可以显著减少延迟和等待时间。
- 减少连接数量:由于多个请求可以共享同一个连接,HTTP/2可以减少连接的建立和管理开销。这对于移动设备和高延迟网络环境下的性能和资源消耗非常重要。
- 请求优先级:HTTP/2支持对请求设置优先级,可以指定哪些请求更重要,应该优先处理。这使得服务器可以根据优先级调整资源分配和响应顺序,优化用户体验。
注:多路复用在HTTP/2的传输层实现,且还是会有队头阻塞的问题,当一个流的数据传输遇到延迟或阻塞时,后续的流也会受到影响,无法及时传输。
4.3.4 服务器推送
HTTP/2引入了服务器推送(Server Push)的功能,它允许服务器在客户端请求之前主动推送相关资源给客户端,以提高性能和减少延迟。
服务器推送的工作流程如下:
- 客户端发送一个请求给服务器,请求某个资源(比如HTML页面)。
- 服务器接收到该请求,并开始处理。
- 在处理该请求的过程中,服务器检测到客户端可能需要额外的资源,例如CSS文件、JavaScript文件、图像等。
- 服务器可以主动推送这些额外的资源给客户端,而无需等待客户端发起请求。
- 客户端接收到服务器推送的资源,并可以立即使用它们,而无需再次发起请求。
通过服务器推送,服务器可以预测客户端可能需要的资源,并主动推送给客户端,从而减少了客户端发起额外请求的延迟。这可以有效地减少页面加载时间,提高用户体验。
例1:nginx开启服务器推送,并制定路径
http2_push_preload on;
http2_push /path/to/style.css;
http2_push /path/to/script.js;
例2:使用go语言建立服务,并开启推送
package main
import (
"net/http"
"/gin-gonic/gin"
)
func main() {
router := gin.Default()
// 启用HTTP/2支持
http2Server := &http.Server{
Addr: ":8080",
Handler: router,
}
http2Server.ListenAndServeTLS("server.crt", "server.key")
// 定义路由和处理函数
router.GET("/", func(c *gin.Context) {
// 使用Pusher对象进行服务器推送
pusher, ok := c.Writer.(http.Pusher)
if ok {
// 推送CSS文件
err := pusher.Push("/path/to/style.css", nil)
if err != nil {
// 处理推送错误
}
// 推送JavaScript文件
err = pusher.Push("/path/to/script.js", nil)
if err != nil {
// 处理推送错误
}
}
// 处理请求
c.HTML(http.StatusOK, "index.html", gin.H{
"title": "Server Push Example",
})
})
// 运行服务器
router.RunTLS(":8080", "server.crt", "server.key")
}
4.3.5 HTTP/2.0的全双工
半双工:在半双工通信中,数据传输只能在一个方向上进行,但在这个方向上可以进行双向通信。这意味着,在任何给定的时间点,通信双方只能进行单向的数据传输。例如,当一个设备发送数据时,另一个设备必须等待接收完成后才能发送数据。典型的半双工通信包括对讲机和早期的以太网(10BASE-T)。
全双工:在全双工通信中,数据传输可以同时在两个方向上进行,允许双向的同时通信。这意味着通信双方可以同时发送和接收数据,而不需要等待对方完成。典型的全双工通信包括电话系统、现代以太网(如1000BASE-T)和双向无线电通信。
在传统的 HTTP/1.x 中,客户端和服务器之间的通信是基于请求-响应模型的。客户端发送请求给服务器,服务器处理请求并返回响应。这种模型要求客户端在接收到服务器响应之前等待,导致了一定的延迟。
而在 HTTP/2 中,引入了全双工的能力(服务器推送 Server Push),允许在同一个 TCP 连接上同时进行双向的数据传输。这意味着客户端可以发送多个请求给服务器,而不需要等待每个请求的响应。服务器也可以主动推送数据给客户端,而不需要等待客户端的请求。
通过全双工的特性,HTTP/2 可以更有效地利用网络带宽,减少延迟,并提升性能。它允许并发的请求和响应,从而提供更快的页面加载速度和更好的用户体验。
http2的全双工与websocket的全双工
HTTP/2 的全双工通信和 WebSocket 的全双工通信是两种不同的协议和技术,尽管它们都支持双向的数据传输。
HTTP/2 是一种应用层协议,是对传统 HTTP/1.x 的改进和扩展。它通过多路复用、二进制传输、头部压缩等技术,实现了全双工通信的能力。在 HTTP/2 中,客户端和服务器之间可以同时发送和接收多个请求和响应,从而提高了通信的效率和性能。
WebSocket 是一种独立的通信协议,位于应用层,它提供了基于 TCP 的全双工通信通道。WebSocket 协议允许客户端和服务器之间建立长久的连接,双方可以通过这个连接实时地发送和接收数据,而不需要每次都发送 HTTP 请求。
虽然 HTTP/2 和 WebSocket 都支持全双工通信,但它们的实现方式和应用场景有所不同。HTTP/2 是为 Web 应用的请求和响应模型进行优化的协议,适用于常规的 Web 页面加载和通信,如实时更新的新闻网站或在线聊天应用等。
而 WebSocket 则是专门设计用于实现实时、持久的双向通信,适用于实时应用和实时数据传输的场景,如在线游戏、实时协作工具、股票市场数据传输等。
4.4 HTTP/3.0
HTTP/3.0(简称HTTP/3)是一种网络协议,用于在Web浏览器和服务器之间进行通信。它是HTTP/2的后续版本,旨在进一步改进性能、效率和安全性。
HTTP/3采用了一种名为QUIC(Quick UDP Internet Connections)的传输协议作为基础,而不是HTTP/2中使用的TCP协议。QUIC是一种基于UDP的协议,具有更低的延迟和更好的拥塞控制机制,可以提供更快的连接建立和数据传输。
以下是HTTP/3的一些主要特性和改进:
- 基于UDP的传输:HTTP/3使用QUIC协议作为传输层,使用UDP而不是TCP。UDP具有较低的延迟和更好的拥塞控制,可以减少连接建立时间并提高数据传输速度。
- 多路复用:与HTTP/2类似,HTTP/3支持多路复用,允许在单个连接上同时进行多个请求和响应,提高了并发性和效率。
- 头部压缩:HTTP/3使用与HTTP/2相同的头部压缩算法(HPACK)来减小数据传输的大小。
- 0-RTT连接建立:HTTP/3引入了0-RTT(零往返时间)连接建立机制,允许在客户端和服务器之间建立更快的连接,从而减少延迟。
- 更好的拥塞控制:QUIC协议内置了更先进的拥塞控制机制,可以更好地适应网络拥塞情况,提供更稳定的性能。
HTTP/3的目标是提供更快的网页加载速度和更好的性能,特别是在高延迟和丢包率较高的网络环境下。它还提供了更好的安全性,因为它使用TLS进行加密,并且在连接建立过程中包含了安全性的增强措施。
需要注意的是,HTTP/3仍然处于发展阶段,并且在实际应用中的广泛采用可能需要一定时间。
五、Cookie和Session
前面提到,HTTP是无状态,每个请求和响应之间是独立的,服务器不会保留客户端的状态信息。比如用户登录一个web网站,在点击页面跳转或者刷新时,需要携带完整信息重新发送请求,也就会需要重新登录一次,这显然会带来不好的用户体验。
为了识别和管理会话状态,引用了cookie与session机制。通过使用Cookie,服务器可以将会话标识符(Session ID)嵌入到每个请求中。当客户端首次访问网站时,服务器根据用户提交的信息,创建对应的session(包含身份验证信息),并生成一个唯一的Session ID,将其存储在Cookie中发送给客户端,随后,客户端在每个请求中都会将这个Cookie添加到请求头里发送,服务器根据Session ID识别和管理与该用户相关的会话状态。这样,同个用户在一个会话内的操作请求,无需再重复验证。
注:会话状态存储在服务器端,通常在内存或持久存储中;cookie存储在客户端,即用户的浏览器中,通常在本地生成一个文件并存储。
会话状态:指在Web应用程序中跟踪和维护与特定用户相关的信息和数据的机制。
- 会话(Session):会话是指从用户开始访问网站直到用户结束访问的整个过程。会话可以跨越多个HTTP请求和响应。在Web开发中,服务器为每个会话分配一个唯一的会话标识符(Session ID),通常通过Cookie在客户端保存。
- 会话状态(Session State):会话状态是指与特定会话相关的数据和信息。它可以包括用户的身份验证状态、购物车内容、用户偏好设置等。服务器可以使用会话标识符来识别和存储与特定会话相关的状态信息。
- 会话管理(Session Management):会话管理是指在Web应用程序中处理和维护会话状态的过程。它通常涉及以下方面:会话的创建和终止、会话标识符的生成和传递、会话状态的存储和检索。会话管理可以使用Cookie、URL重写或隐藏表单字段等机制来传递会话标识符。
通过会话状态,服务器可以在无状态的HTTP协议上维护用户的相关信息,提供个性化的用户体验。例如,当用户登录到一个网站时,服务器可以将登录状态存储在会话状态中,以便在用户的后续请求中识别用户身份。服务器还可以使用会话状态跟踪用户在购物网站上添加到购物车中的商品,以便在结账时获取正确的商品信息。
5.1 cookie
Cookie是一种在Web应用程序中用于存储和传递有关用户和网站之间交互的数据的机制。它是由服务器在HTTP响应中设置的小型文本文件,然后在后续的请求中由客户端自动包含在HTTP头中发送回服务器。
响应报文示例:
HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: exampleCookie=cookieValue; Expires=Wed, 01-Mar-2025 12:00:00 GMT; Path=/
在上述示例中,服务器通过"Set-Cookie"头字段将一个名为"exampleCookie"的Cookie发送给客户端。该Cookie的值为"cookieValue"。此外,还设置了Cookie的过期时间(Expires)为2025年3月1日12:00:00 GMT,并指定了Cookie的作用路径(Path)为根路径(/)。
注:HTTP响应报文中可以包含多个"Set-Cookie"头字段,以设置多个Cookie。每个"Set-Cookie"头字段对应一个要设置的Cookie。客户端在接收到这些Cookie后,会分别保存在本地。
请求报文示例:
GET /example HTTP/1.1
Host: www.
Cookie: exampleCookie=cookieValue; anotherCookie=anotherValue
在上述示例中,客户端发送了一个GET请求到服务器上的"/example"路径,并在"Cookie"头字段中包含了两个Cookie信息。
第一个Cookie是名为"exampleCookie"的Cookie,其值为"cookieValue"。第二个Cookie是名为"anotherCookie"的Cookie,其值为"anotherValue"。
5.1.1 Set-Cookie字段属性
当服务器在HTTP响应中设置Cookie时,可以使用多个属性来定义Cookie的行为和特性。以下是常见的Set-Cookie字段属性:
- Name=Value:指定Cookie的名称和值。例如,"Set-Cookie: exampleCookie=cookieValue"。
- Expires:指定Cookie的过期时间。可以是一个具体的日期和时间,例如"Set-Cookie: exampleCookie=cookieValue; Expires=Wed, 01-Mar-2023 12:00:00 GMT"。过期时间过去后,浏览器将不再发送该Cookie。
- Max-Age:指定Cookie的最大存活时间,以秒为单位。例如,"Set-Cookie: exampleCookie=cookieValue; Max-Age=3600"表示该Cookie将在设置后的3600秒(1小时)后过期。
- Domain:指定Cookie适用的域名。例如,"Set-Cookie: exampleCookie=cookieValue; Domain="表示该Cookie仅在域名下可用。
- Path:指定Cookie适用的路径。例如,"Set-Cookie: exampleCookie=cookieValue; Path=/path"表示该Cookie仅在/path路径下可用。
- Secure:指定该Cookie是否仅通过HTTPS连接发送。例如,"Set-Cookie: exampleCookie=cookieValue; Secure"表示该Cookie仅在通过HTTPS连接的请求中发送。
- HttpOnly:指定该Cookie是否只能通过HTTP请求发送,而不能通过客户端脚本(如JavaScript)访问。例如,"Set-Cookie: exampleCookie=cookieValue; HttpOnly"。
- SameSite:指定Cookie的跨站点请求行为。它可以有三个值:Strict、Lax和None。Strict表示只在同一站点的请求中发送Cookie,Lax表示在导航到目标URL的安全上下文中发送Cookie(例如,从外部站点链接到目标站点),None表示始终发送Cookie,无论请求来源是否跨站点。例如,"Set-Cookie: exampleCookie=cookieValue; SameSite=Lax"。
注:当同时指定了Expires和Max-Age属性,Max-Age优先级更高,会覆盖Expires属性。
注:当Cookie过期后,服务器端可以在处理下一次请求时返回新的Cookie给客户端,以便更新和替换过期的Cookie。浏览器在接收到新的Cookie后会将其保存,并在后续的请求中自动发送新的Cookie。
5.1.2 服务器端设置cookie
可以在程序中设置HTTP响应头中的Set-Cookie字段来设置Cookie的属性,具体的实现方式取决于使用的编程语言和框架。
go:
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", setCookieHandler)
http.ListenAndServe(":8080", nil)
}
func setCookieHandler(w http.ResponseWriter, r *http.Request) {
cookie := &http.Cookie{
Name: "cookieName",
Value: "cookieValue",
Expires: time.Now().Add(1 * time.Hour),
Secure: true,
HttpOnly: true,
}
http.SetCookie(w, cookie)
fmt.Fprint(w, "Setting cookie")
}
nginx:
server {
listen 80;
server_name ;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
5.2 session
会话(Session)是一种在网络应用程序中跟踪用户状态的机制。它允许服务器在用户与应用程序进行交互时存储和检索有关用户的信息。会话通常用于存储和共享用户的身份验证状态、用户首选项、购物车内容等。
以下是会话的基本工作原理:
- 会话标识符(Session ID): 当用户首次访问应用程序时,服务器会为其分配一个唯一的会话标识符(Session ID)。这个标识符通常以Cookie的形式存储在用户的浏览器中,也可以通过URL参数或其他机制传递。
- 会话数据存储: 服务器使用会话标识符来关联用户的请求。服务器可以将用户相关的数据存储在内存、数据库或其他持久化存储中,与会话标识符关联起来。这样,在用户的后续请求中,服务器可以根据会话标识符检索和更新相应的会话数据。
- 会话管理: 会话管理涉及创建、更新和销毁会话。当用户进行身份验证或与应用程序进行交互时,服务器可以创建新的会话或更新现有会话的状态。会话还可以设置过期时间,以便在一段时间内保持活动,超过过期时间后,会话将被销毁。
- 会话安全性: 为了确保会话的安全性,会话标识符需要保密,并采取一些安全措施,如使用加密、使用HTTPS等。服务器还需要验证和授权用户访问会话数据的权限,以防止未经授权的访问。
会话提供了一个持久化的机制来跟踪用户状态,使得用户在不同的请求之间可以保持一致的体验。它允许应用程序存储和访问用户相关的数据,从而实现个性化和交互性的功能。
会话管理具体的实现方式和可用的选项可能因所使用的编程语言、框架和会话管理库而有所不同,
go:
store := sessions.NewCookieStore([]byte("your-secret-key"))
store.Options.MaxAge = 3600 // 过期时间为1小时,以秒为单位
session, err := store.Get(request, "session-name")
if err != nil {
// 处理获取会话时的错误
}
// 设置会话值
session.Values["key"] = value
// 保存会话
err = session.Save(request, response)
if err != nil {
// 处理保存会话时的错误
}
5.2.1 分布式session
由于现在的服务架构基本是分布式负载均衡,用户登录时不一定连接到之前的服务器上,这样就会出现登录失效的问题。
解决方案:
- 粘性会话(Session Affinity): 也称为会话保持或会话粘性,这种方法将特定用户的请求始终发送到同一台服务器,以保持会话的连续性。负载均衡器使用某种算法(如IP地址哈希或会话标识符哈希)来确定将请求路由到哪个服务器。这样,用户的会话数据可以在同一台服务器上保持一致性。但是,这种方法可能导致负载不均衡,因为某些服务器可能承载更多的负载。
- 共享存储: 在负载均衡集群中,使用共享存储(例如共享文件系统、网络文件系统或数据库)来存储会话数据。所有服务器都可以访问相同的存储位置,以读取和写入会话数据。这种方法确保了会话数据的一致性,但可能对共享存储系统造成较大的负载,并成为系统的瓶颈。
- 外部存储: 将会话数据存储在外部存储系统(如数据库或缓存)中,而不是存储在每个服务器的本地内存中。负载均衡集群中的所有服务器都可以访问相同的外部存储系统来读取和写入会话数据。这种方法可以提供高度的可伸缩性和灵活性,并减轻了共享存储的负载问题。但是,需要确保外部存储系统的性能和可靠性。
- 无状态会话(Stateless Session): 在无状态架构中,会话数据不存储在服务器端,而是存储在客户端或外部存储系统中。服务器无需关心特定用户的会话状态,每个请求都被视为独立的。这样,请求可以在负载均衡集群中的任何服务器上进行处理,而不需要共享会话数据。无状态会话可以实现高度的可伸缩性和灵活性,但可能需要牺牲某些会话功能和状态跟踪能力。
实现无状态会话(Stateless Session)需要将会话数据从服务器端移至客户端或外部存储系统。以下是一些实现无状态会话的常见方法:
- 客户端存储: 将会话数据存储在客户端,如浏览器的Cookie或本地存储(如Web Storage)。服务器只需生成一个唯一的会话标识符,并将其发送给客户端。客户端在后续的请求中携带该标识符,服务器根据标识符来识别和验证用户。这种方法简单且易于实现,但会话数据存储在客户端,可能存在安全风险。
- 令牌(Token)验证: 在无状态架构中,服务器使用令牌进行会话验证而不存储会话数据。当用户进行身份验证后,服务器生成一个令牌并返回给客户端。客户端在每个请求中携带令牌,服务器通过验证令牌来识别和验证用户。这种方法可以在客户端和服务器之间实现无状态通信,但需要进行令牌的生成和验证。
- 外部存储系统: 将会话数据存储在外部存储系统,如数据库或缓存。会话数据与具体的服务器无关,可以在负载均衡集群中的任何服务器上进行处理。服务器仅负责生成和验证会话标识符,并从外部存储系统中读取和写入会话数据。这种方法可以提供高度的可伸缩性和灵活性,但依赖于外部存储系统的性能和可靠性。
- JWT(JSON Web Token): JWT是一种基于JSON的开放标准,用于在客户端和服务器之间传输信息。服务器生成JWT,并将其作为令牌发送给客户端。JWT包含了一些声明和签名,可以用于识别和验证用户。客户端在每个请求中携带JWT,服务器通过验证JWT的签名来验证用户身份。这种方法可以实现无状态的会话验证,且JWT本身包含了会话数据。
5.2.2 token
令牌(Token)是在计算机系统中用于表示用户身份、访问权限或其他授权信息的一种数据结构。令牌通常是一个字符串,由服务器生成,并在客户端和服务器之间进行传递。以下是关于令牌的一些常见概念和用法:
- 身份验证令牌: 在身份验证过程中,服务器可以生成一个身份验证令牌,并将其发送给客户端作为用户身份的标识。客户端在后续的请求中携带该令牌,以证明其身份。服务器可以验证令牌的有效性,并根据令牌确定用户的权限和访问级别。
- 访问令牌: 在授权和访问控制的场景中,访问令牌用于授予客户端对受保护资源的访问权限。当用户通过身份验证后,服务器生成一个访问令牌,并将其发送给客户端。客户端在每个请求中携带该令牌,以证明其有权访问受保护的资源。服务器可以验证令牌的有效性,并根据令牌的权限范围决定是否允许访问请求。
- JSON Web Token(JWT): JWT是一种开放标准(RFC 7519),用于在客户端和服务器之间传输信息。它由三个部分组成:头部(Header)、载荷(Payload)和签名(Signature)。头部包含令牌类型和签名算法等信息,载荷包含令牌的声明和数据,签名用于验证令牌的完整性和真实性。JWT在无状态会话中广泛应用,可以用于身份验证、授权和信息传递等场景。
- 刷新令牌: 刷新令牌用于在访问令牌过期后获取新的访问令牌。当访问令牌过期时,客户端可以使用刷新令牌向服务器请求新的访问令牌,而不需要重新进行身份验证。刷新令牌通常具有更长的有效期,并且需要进行额外的安全措施来保护其安全性。
令牌的使用可以有效地实现身份验证、授权和访问控制,同时减少对服务器端状态的依赖,使系统更具可伸缩性和灵活性。但需要注意令牌的生成、验证和保护,以确保系统的安全性。
go示例:
package main
import (
"fmt"
"log"
"net/http"
"time"
"/dgrijalva/jwt-go"
)
var signingKey = []byte("your-secret-key")
func main() {
http.HandleFunc("/login", loginHandler)
http.HandleFunc("/protected", protectedHandler)
fmt.Println("启动服务器,监听端口8080...")
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal(err)
}
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
// 假设用户认证通过,生成令牌
tokenString, err := generateToken(1, time.Minute)
if err != nil {
http.Error(w, "无法生成令牌", http.StatusInternalServerError)
return
}
// 将令牌设置到响应头中
w.Header().Set("Authorization", "Bearer "+tokenString)
w.WriteHeader(http.StatusOK)
}
func protectedHandler(w http.ResponseWriter, r *http.Request) {
// 检查令牌是否存在于请求头中
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "未提供令牌", http.StatusUnauthorized)
return
}
// 解析令牌
tokenString := authHeader[len("Bearer "):]
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return signingKey, nil
})
if err != nil {
http.Error(w, "无效的令牌", http.StatusUnauthorized)
return
}
// 验证令牌的有效性
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
http.Error(w, "无效的令牌", http.StatusUnauthorized)
return
}
// 检查令牌是否过期
expirationTime := time.Unix(int64(claims["exp"].(float64)), 0)
if time.Now().After(expirationTime) {
// 令牌已过期,刷新令牌
newTokenString, err := generateToken(1, time.Minute)
if err != nil {
http.Error(w, "无法刷新令牌", http.StatusInternalServerError)
return
}
// 将刷新后的令牌设置到响应头中
w.Header().Set("Authorization", "Bearer "+newTokenString)
}
// 认证通过,继续处理请求
w.WriteHeader(http.StatusOK)
w.Write([]byte("成功访问受保护的资源"))
}
// 生成令牌
func generateToken(userID int64, duration time.Duration) (string, error) {
// 设置过期时间
expirationTime := time.Now().Add(duration)
// 创建声明(Claim)
claims := jwt.MapClaims{
"userID": userID,
"exp": expirationTime.Unix(),
"iat": time.Now().Unix(),
}
// 创建令牌
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 签名生成令牌字符串
tokenString, err := token.SignedString(signingKey)
if err != nil {
return "", err
}
return tokenString, nil
}
六、HTPP与HTTPS
HTTP 是一种明文协议,数据在传输过程中是以明文形式进行传输的。这意味着在 HTTP 中,数据在传输过程中是不加密的,可能被窃听者获取和篡改。
- 窃听风险,比如通信链路上可以获取通信内容
- 篡改风险,比如强制植入垃圾广告,视觉污染
- 冒充风险,比如冒充淘宝网站
HTTPS 则是基于 HTTP 的安全版本。它使用了 SSL(Secure Sockets Layer)或其继任协议 TLS(Transport Layer Security)来加密 HTTP 数据。通过使用公钥加密和私钥解密,HTTPS 可以确保在客户端和服务器之间传输的数据是加密的,从而提供更高的安全性。
HTTP 连接建立相对简单, TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 在 TCP 三次握手之后,还需进行 SSL/TLS 的握手过程,才可进入加密报文传输。
HTTP 默认端口号是 80,HTTPS 默认端口号是 443。
HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。
需要注意的是,HTTPS 在握手阶段和通信过程中使用不同的加密算法。
- 握手阶段加密算法:
在握手阶段,HTTPS 使用公钥加密算法来确保通信的机密性和完整性。具体来说,主要使用以下算法:
- 非对称加密算法(公钥加密算法):HTTPS 使用非对称加密算法(如 RSA、Diffie-Hellman)来协商会话密钥。这些算法允许服务器使用私钥对公钥进行加密,而客户端使用服务器的公钥进行解密。
- 数字签名算法:HTTPS 使用数字签名算法(如 RSA、DSA、ECDSA)来验证服务器的身份和证书的有效性。这些算法允许客户端使用服务器的公钥对证书进行验证,确保证书的完整性和真实性。
- 通信过程中加密算法:
在握手阶段完成后,HTTPS 使用对称加密算法来加密和解密实际的通信数据。具体来说,主要使用以下算法:
- 对称加密算法:HTTPS 使用对称加密算法(如 AES、3DES)来加密和解密数据。对称加密算法使用相同的密钥进行加密和解密,因此在握手阶段通过非对称加密算法协商的会话密钥将用于对称加密算法。
- 消息认证码(MAC)算法:HTTPS 使用 MAC 算法(如 HMAC)来验证数据的完整性。MAC 算法使用密钥对数据进行哈希计算,生成一个摘要,用于验证数据是否被篡改。
6.1 加密算法
6.1.1 对称加密
对称加密是一种加密方法,使用相同的密钥进行数据的加密和解密。在对称加密中,发送方使用密钥对数据进行加密,接收方使用相同的密钥对加密的数据进行解密。
对称加密的特点包括:
- 单一密钥:对称加密使用相同的密钥进行加密和解密操作。因此,密钥的安全性对于保护加密数据的机密性至关重要。
- 高效性:对称加密算法通常具有高速度和高效性,因为加密和解密的过程使用相同的密钥和算法。
- 适用性:对称加密适用于对大量数据进行加密和解密的情况,因为它的计算和处理开销相对较小。
一些常见的对称加密算法包括:
- AES(Advanced Encryption Standard):AES 是目前最常用的对称加密算法,它支持不同的密钥长度(如 AES-128、AES-256)。
- DES(Data Encryption Standard):DES 是一种较早的对称加密算法,已经不推荐使用,因为其密钥长度较短,易受到暴力破解攻击。
- 3DES(Triple Data Encryption Standard):3DES 是 DES 的改进版本,通过多次应用 DES 算法来增加密钥长度和安全性。
缺点:
- 密钥管理:对称加密算法要求发送方和接收方共享相同的密钥。因此,密钥的分发和管理可能会面临困难,特别是在大规模和分布式系统中。
- 安全性限制:对称加密算法的安全性取决于密钥的保密性。如果密钥被泄露,攻击者可以使用该密钥解密加密的数据。因此,对称加密算法在密钥管理和保护方面面临挑战。
- 缺乏身份验证和密钥交换:对称加密算法本身不提供身份验证和密钥交换机制。这意味着在通信开始之前,发送方和接收方需要使用其他方法来确保密钥的安全性和身份的真实性。
- 缺乏前向保密性:对称加密算法在密钥泄露之前无法提供前向保密性。一旦密钥被泄露,攻击者可以解密过去和未来的通信数据。
6.1.2 非对称加密
非对称加密(Asymmetric Encryption),也称为公钥加密,是一种加密方法,使用一对密钥进行加密和解密操作,这对密钥包括一个公钥和一个私钥,其中公钥用于加密数据,而私钥用于解密数据。
非对称加密的主要特点如下:
- 公钥加密:使用公钥对数据进行加密,只有持有相应的私钥才能解密数据。公钥是公开的,并且可以向任何人分发。
- 私钥保密:私钥用于解密加密数据,必须严格保密,只有私钥的所有者可以访问和使用它。
- 数字签名:私钥可以用于生成数字签名,用于验证数据的真实性和完整性。
- 密钥分发:非对称加密解决了对称加密中密钥分发和管理的问题。发送方可以使用接收方的公钥进行加密,而接收方使用自己的私钥进行解密。
非对称加密算法的常见例子包括:
- RSA(Rivest-Shamir-Adleman):RSA 是一种基于大数分解难题的非对称加密算法,目前被广泛应用。
- Diffie-Hellman:Diffie-Hellman 是一种密钥交换协议,用于在公开信道上协商共享密钥。
非对称加密算法具有以下优点和缺点:
优点:
- 安全性:相比对称加密,非对称加密提供更高的安全性。即使公钥被泄露,也不会影响密文的安全性,因为只有私钥才能解密数据。
- 密钥分发:非对称加密解决了对称加密中密钥分发和管理的问题。发送方可以使用接收方的公钥进行加密,而接收方使用自己的私钥进行解密。
- 数字签名:非对称加密可以用于生成和验证数字签名,用于确保数据的完整性和身份验证。
缺点:
- 效率:相比对称加密,非对称加密算法通常更加复杂,计算和处理开销更大,因此速度较慢。
- 密钥长度:非对称加密算法的密钥长度通常要比对称加密算法的密钥长度长得多,这可能会增加存储和处理的成本。
6.2 数字证书
数字证书是一种用于验证网络通信中实体身份的安全工具。它是由可信任的证书颁发机构(Certificate Authority,简称 CA)颁发的电子文件,其中包含了一个实体(如个人、组织或服务器)的公钥以及与该公钥相关的身份信息。数字证书使用非对称加密技术,确保证书的完整性和真实性。
数字证书的主要组成部分包括:
- 主体信息:数字证书中包含了证书的主体信息,如实体的名称、电子邮件地址等。这些信息用于标识证书的持有者。
- 公钥:数字证书中包含了证书持有者的公钥,用于加密和验证数据。公钥是公开的,可以被其他人用来加密数据或验证数字签名。
- 签名:数字证书中包含了证书颁发机构对证书内容的数字签名。签名使用证书颁发机构的私钥生成,用于确保证书的完整性和真实性。
通过使用数字证书,可以实现以下功能:
- 身份验证:数字证书可以用于验证通信中的实体身份。接收方可以使用证书中的公钥来验证证书的签名,从而确认证书的真实性和完整性,并确保与该实体进行安全通信。
- 数据加密:数字证书中包含了公钥,可以用于加密数据。发送方可以使用证书中的公钥加密数据,只有证书持有者的私钥才能解密数据。
- 数字签名:数字证书可以用于生成和验证数字签名。证书持有者可以使用私钥生成数字签名,用于证明数据的来源和完整性,而其他人可以使用证书中的公钥来验证签名的有效性。
6.3 TLS建联
HTTPS(Hypertext Transfer Protocol Secure)是一种通过加密和身份验证来保护网络通信的安全版本的HTTP协议。下面是建立HTTPS连接的基本过程:
- 客户端发起连接请求:客户端(例如Web浏览器)向服务器发起连接请求,请求建立安全连接。
- 服务器发送证书:服务器接收到客户端的连接请求后,会将服务器的数字证书发送给客户端。证书包含服务器的公钥、证书的有效期、颁发机构(CA)的签名等信息。
- 客户端验证证书:客户端收到服务器的证书后,会验证证书的有效性和真实性。验证包括检查证书的签名是否有效、证书是否在有效期内以及证书是否与访问的域名匹配等。
- 客户端生成密钥:如果服务器的证书验证通过,客户端会生成一个随机的对称密钥(会话密钥),用于后续的加密和解密过程。
- 客户端使用服务器公钥加密会话密钥:客户端使用服务器证书中的公钥对会话密钥进行加密,并发送给服务器。
- 服务器使用私钥解密会话密钥:服务器使用自己的私钥对接收到的加密会话密钥进行解密,从而获取会话密钥。
- 建立安全连接:客户端和服务器现在都拥有相同的会话密钥,可以使用对称加密算法来加密和解密数据。他们使用会话密钥来加密和解密通过连接发送的所有数据。
- 安全通信:客户端和服务器之间的通信现在是通过HTTPS进行加密的,保护数据的隐私和完整性。
6.3.1 密钥交换算法
在TLS握手过程中,存在多种密钥交换算法,用于协商会话密钥(Session Key)的生成。以下是一些常见的密钥交换算法:
- RSA(Rivest-Shamir-Adleman):RSA是一种非对称加密算法,广泛用于密钥交换和数字签名。在RSA密钥交换中,服务器的公钥用于加密Pre-Master Secret,然后服务器使用私钥解密。RSA密钥交换要求服务器具有有效的数字证书。
- Diffie-Hellman(DH):Diffie-Hellman是一种基于离散对数问题的密钥交换协议。在TLS握手中,Diffie-Hellman协议用于在客户端和服务器之间协商会话密钥。可以使用基于有限域(Finite Field)的Diffie-Hellman(DH)或者基于椭圆曲线(Elliptic Curve)的椭圆曲线Diffie-Hellman(ECDH)。
- Ephemeral Diffie-Hellman(DHE):Ephemeral Diffie-Hellman是一种在每次握手时都生成新的临时Diffie-Hellman密钥对的扩展。这种方法可以提供前向安全性(Forward Secrecy),即即使长期私钥泄漏,过去的会话也不会受到威胁。DHE通常与RSA或ECDSA(Elliptic Curve Digital Signature Algorithm)一起使用,用于服务器身份验证和数字签名。
- Ephemeral Elliptic Curve Diffie-Hellman(ECDHE):Ephemeral Elliptic Curve Diffie-Hellman是一种在每次握手时都生成新的临时椭圆曲线Diffie-Hellman密钥对的扩展。ECDHE提供与DHE类似的前向安全性,但使用椭圆曲线算法而不是传统Diffie-Hellman算法。
不同的密钥交换算法,TLS握手过程也有所不同,这里不做详细描述。
6.4 HTTPS数据完整性
HTTPS使用TLS记录协议来保证应用数据的完整性。
- 分割数据:TLS记录协议将上层协议(如HTTP)产生的数据分割成适当大小的记录块,以便在传输过程中进行处理。
- 添加首部:对于每个记录块,TLS记录协议添加一个首部,用于标识记录类型、记录长度等信息。
- 加密:如果协商的加密算法要求加密,TLS记录协议将对记录块的内容进行加密。加密后的记录块保证数据在传输过程中的机密性。
- 认证:对于加密的记录块,TLS记录协议还会计算并添加一个消息认证码(MAC),以验证数据在传输过程中是否被篡改。接收方使用相同的密钥和MAC算法来计算接收到的记录块的MAC,并将其与接收到的MAC进行比较以验证数据的完整性。
- 压缩:在TLS 1.2及之前的版本中,TLS记录协议支持压缩算法,用于减小记录块的大小。然而,由于压缩算法可能存在安全漏洞,TLS 1.3已经移除了对压缩的支持。
七、补充
7.1 跨域
什么是跨域?
跨域(Cross-Origin)指的是在浏览器环境中,当一个网页的 JavaScript 代码尝试访问不同源(Origin)的资源时,就会发生跨域问题。不同源指的是协议、域名或子域名、端口号任一不同。
协议不同 |
http:// |
https:// |
http:// |
ftp:// |
|
域名不同 |
http:// |
http:// |
子域名不同 |
http:// |
http://subdomain. |
端口号不同 |
http:// |
http://:8080 |
注:浏览器的同源策略(Same-Origin Policy)是一种安全机制,它限制了网页中的 JavaScript 代码只能与同一来源的资源进行交互,防止恶意网站窃取用户的敏感信息或进行恶意操作。
无法跨域带来的限制:
- 数据访问限制:跨域请求受到浏览器的限制,无法直接访问其他域下的资源。这意味着无法通过 JavaScript 直接获取跨域资源的数据,包括 DOM 元素、Cookies、LocalStorage 等。
- API 调用受限:跨域问题会影响 AJAX 请求和 Fetch API 调用,浏览器会根据同源策略限制跨域请求的发送和接收。通常,跨域请求会被阻止、返回部分数据或触发 CORS(跨域资源共享)预检请求。
- Cookie 限制:浏览器的同源策略限制了跨域情况下对其他域下的 Cookie 进行读取。这意味着无法直接通过 JavaScript 访问跨域资源的 Cookie。
- 开发调试困难:由于跨域限制,开发和调试跨域请求变得更加困难。需要使用特定的解决方案(如 JSONP、CORS、代理服务器等)或工具来处理跨域请求和调试。
如何解决跨域?
- CORS(跨域资源共享):CORS是一种现代的跨域解决方案,通过服务端设置响应头中的相关字段来允许跨域访问。可以通过设置
Access-Control-Allow-Origin
字段来指定允许访问的源,以及其他可选的字段来控制跨域请求的行为。 - JSONP(JSON with Padding):JSONP是一种利用
<script>
标签的跨域技术。通过动态创建<script>
元素,将请求的数据包装在回调函数中返回。JSONP只支持GET请求,需要服务端配合返回相应的数据和回调函数。 - 代理服务器:通过在自己的服务器上设置一个代理,将跨域请求发送到目标服务器,并将响应返回给浏览器。代理服务器可以在同源策略下发送请求,从而解决了跨域问题。常见的代理服务器包括nginx、Apache等。
- WebSocket:WebSocket协议不受同源策略的限制,可以实现跨域通信。通过建立WebSocket连接,客户端和服务端可以进行双向通信。
- 跨域资源共享iframe(CORS iframe):在父页面中通过iframe加载跨域资源,并在子页面中使用CORS来与父页面进行通信。通过在父页面和子页面之间传递消息,实现跨域数据交互。
- PostMessage:使用HTML5的PostMessage API,在不同窗口或不同域之间进行安全的跨域通信。通过向目标窗口发送消息,实现数据的传递和交互。
响应头字段:
|
|
Access-Control-Allow-Origin |
指定允许访问资源的源,如 |
Access-Control-Allow-Methods |
指定允许的请求方法,如GET, POST |
Access-Control-Allow-Headers |
指定允许的请求头字段,用于限制浏览器发送的自定义请求头。如 |
Access-Control-Allow-Credentials |
指定是否允许发送身份凭证(如Cookie、HTTP认证等)。设置为 |
Access-Control-Expose-Headers |
指定允许暴露给客户端的响应头字段。默认情况下,浏览器只能访问一些常见的响应头字段,其他自定义的响应头字段是不可访问的。通过该字段,可以指定额外的响应头字段供客户端访问。 |
示例1:Node.js和Express框架
const express = require('express');
const app = express();
// 设置CORS响应头
app.use((req, res, next) => {
// 允许所有来源访问资源
res.setHeader('Access-Control-Allow-Origin', '*');
// 允许的请求方法
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
// 允许的请求头字段
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// 允许发送身份凭证
res.setHeader('Access-Control-Allow-Credentials', 'true');
// 允许暴露的响应头字段
res.setHeader('Access-Control-Expose-Headers', 'Content-Length, X-My-Custom-Header');
next();
});
// 处理其他路由和请求
// ...
// 启动服务器
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
示例2:go
package main
import (
"net/http"
)
func main() {
// 创建路由处理函数
http.HandleFunc("/", handler)
// 启动服务器并监听端口
http.ListenAndServe(":3000", nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
// 检查请求方法是否为OPTIONS(预检请求)
if r.Method == "OPTIONS" {
// 设置允许的请求方法
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
// 设置允许的请求头字段
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
// 设置预检请求的有效期
w.Header().Set("Access-Control-Max-Age", "86400") // 24小时
// 结束处理,不再继续执行后续代码
return
}
// 设置允许的来源(允许所有来源)
w.Header().Set("Access-Control-Allow-Origin", "*")
// 设置允许发送身份凭证(如Cookie)
w.Header().Set("Access-Control-Allow-Credentials", "true")
// 处理其他逻辑
// ...
// 返回响应
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, CORS!"))
}
预检请求:
在跨域请求中,浏览器会发送一个预检请求(Preflight Request)来判断实际请求是否安全。预检请求是一种OPTIONS请求,用于向服务器询问是否允许发送实际请求。
当浏览器发送跨域请求时,如果请求方法为非简单请求(Simple Request),例如包含自定义的请求头字段,浏览器会先发送一个预检请求,以确认服务器是否允许实际请求。
预检请求的目的是确保服务器对实际请求的处理是安全和合法的。服务器在收到预检请求后,需要进行相应的处理来验证请求是否被允许。
在Go语言中,通过检查请求方法是否为OPTIONS来判断当前请求是否为预检请求。如果是预检请求,可以在响应中设置相应的CORS响应头,以指示允许的请求方法、请求头字段等。
需要注意的是,预检请求与实际请求是分开的,预检请求仅用于探测服务器是否允许实际请求。在预检请求之后,浏览器才会发送实际请求。因此,在处理预检请求时,可以直接返回响应,而不需要执行实际请求的业务逻辑。这样可以减轻服务器的负担,并提高性能。