Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http #49

Open
Genluo opened this issue Sep 6, 2019 · 0 comments
Open

http #49

Genluo opened this issue Sep 6, 2019 · 0 comments
Labels
Node Node相关

Comments

@Genluo
Copy link
Owner

Genluo commented Sep 6, 2019

http

http是比较重要的一个模块,是建立服务器的基础,学好这个模块非常重要,首先知道http是应用层协议,那么与net模块相比,相比只是多了http协议报文解析的步骤、同时将解析的内容和stream进行结合,让我们更加方便使用http模块进行数据通信。

文档部分

(1) Agent类(类似工具类)

Agent 负责管理 HTTP 客户端的连接持久性和重用。 它为给定的主机和端口维护一个待处理请求队列,为每个请求重用单独的套接字连接,直到队列为空,此时套接字被销毁或放入连接池,以便再次用于请求到同一个主机和端口。 销毁还是放入连接池取决于 keepAlive 选项

连接池中的连接已启用 TCP Keep-Alive,但服务器仍可能关闭空闲连接,在这种情况下,它们将从连接池中删除,并且当为该主机和端口发出新的 HTTP 请求时将建立新连接。 服务器也可以拒绝通过同一连接允许多个请求,在这种情况下,必须为每个请求重新建立连接,并且不能放入连接池。 Agent 仍将向该服务器发出请求,但每个请求都将通过新连接发生。

当客户端或服务器关闭连接时,它将从连接池中删除。 连接池中任何未使用的套接字都将被销毁,以便当没有未完成的请求时不用保持 Node.js 进程运行。 (参阅 socket.unref())。

当不再使用时最好 destroy() Agent 实例,因为未使用的套接字会消耗操作系统资源。

  • 方法
    • createConnection:此函数与net.createConnection 相同
    • keepSocketAlive
    • reuseSocket
    • destroy:销毁代理当前使用的所有套接字,通常没有必要调用,但是如果开启keep-alive选项,最好在代理不在使用这些socket的时候调用, 否则,在服务器终止套接字之前,套接字可能会挂起很长时间。
    • getName
  • 属性
    • freeSockets:一个对象,其中包含当启用 keepAlive 时代理正在等待使用的套接字数组。
    • maxFreeSockets
    • maxSockets
    • requests: 一个对象,包含尚未分配socket的请求队列
    • sockets:当前代理正在使用的套接字数组

(2) ClientRequest类

此对象由 http.request() 内部创建并返回。 它表示正在进行的请求,且其请求头已进入队列。 请求头仍然可以使用 setHeader(name, value)getHeader(name)removeHeader(name) 改变。 实际的请求头将与第一个数据块一起发送,或者当调用 request.end() 时发送。

要获得响应,则为请求对象添加 'response' 事件监听器。 当接收到响应头时,将、则会从请求对象触发 'response' 事件。 'response' 事件执行时有一个参数,该参数是 http.IncomingMessage 的实例。

'response' 事件期间,可以添加监听器到响应对象,比如监听 'data' 事件。

如果没有添加 'response' 事件处理函数,则响应将被完全丢弃。 如果添加了 'response' 事件处理函数,则必须消费完响应对象中的数据,每当有 'readable' 事件时调用 response.read()、或添加 'data' 事件处理函数、或通过调用 .resume() 方法。 在消费完数据之前,不会触发 'end' 事件。 此外,在读取数据之前,它将占用内存,最终可能导致进程内存不足的错误。

Node.js 不检查 Content-Length 和已传输的主体的长度是否相等。

  • 事件
    • abort:客户端中止
    • connect
    • continue
    • information:获得主响应之前的触发的事件
    • response:当收到此请求的响应时触发,此事件仅仅触发一次
    • socket:将套接字分给这个请求时候触发
    • timeout:底层套接字因不活动而超时触发,必须手动终止请求
    • upgrade:每次服务器响应升级调用,比如websocket的应用
    • drain: 缓冲区调整
  • 方法
    • abort:调用此方法将导致响应中剩余的数据被丢弃并且套接字被销毁
    • end
    • flushHeaders
    • getHeaders
    • removeHeaders
    • setHeader
    • setNoDelay
    • setSocketKeepAlive
    • setTimeout
    • write
  • 属性
    • aborted
    • connection
    • finished
    • maxHeadersCount
    • socket

(3) Server类

此类继承自 net.Server 并具有以下额外的事件:

  • 事件
    • checkContinue: 发送请求的拦截事件,调用Response实例的writeContinue的方法才能继续传递请求
    • checkExpectation: 每次收到带有 HTTP Expect 请求头的请求时触发
    • clientError:如果客户端连接触发 'error' 事件,则会在此处转发。 此事件的监听器负责关闭或销毁底层套接字。
    • close
    • connect:当客户端请求connect方法时触发
    • connection:建立新的TCP流会触发此事件
    • request:每次有请求都会触发,每个连接可能有多个请求
    • upgrade:http协议升级触发的事件
  • 方法
    • close:停止服务器接受新连接
    • listen:启动Http服务器监听连接
    • setTimeout:设置套接字的超时时间
  • 属性
    • maxHeadersCount
    • headersCounts
    • listening
    • HeadersTimeout
    • timeout
    • keepAliveTimeout

(4) ServerResponse类

此对象由 HTTP 服务器在内部创建,而不是由用户创建。 它作为第二个参数传给 'request' 事件。

  • 事件
    • close:表明调用end或者能够刷新之前终止了底层连接
    • finish:响应发送后触发,并不意味着客户端收到信息
  • 方法
    • addTrailers
    • end
    • getHeader / getHeaders
    • getHeaderNames
    • hasHeader
    • removeHeader
    • setHeader
    • setTimeout
    • write
    • writeContinue
    • writeHead:比setHead优先级更高
    • writeProcessing:表明可以发送请求主体
  • 属性
    • connection
    • finished
    • headerSent
    • sendDate
    • socket
    • statusCode
    • statusMessage

(5) IncomingMessage类

IncomingMessage 对象由 http.Serverhttp.ClientRequest 创建,并分别作为第一个参数传给 'request''response' 事件。 它可用于访问响应状态、消息头、以及数据。

  • 事件
    • aborted:当请求中止时触发
    • close:表明底层连接已经关闭,和end事件一样,每个响应只触发一次此事件
  • 方法
    • destroy
    • setTimeout
  • 属性
    • aborted
    • complete
    • headers
    • httpVersion
    • method
    • rawHeaders
    • rawTrailers
    • socket
    • statusCode
    • statusMessage
    • trailers
    • url

(6) 模块上的方法属性

  • METHODS
  • STATUS_CODES
  • createServer
  • request
  • get
  • globalAgent
  • maxHeaderSize

( 7) 整个类的理解

可以这样理解,一共包含ServerAgentServerResponseIncomingMessageClientRequest

  • 大的话可以分为两类,一类是客户端,另一类是服务器端,Server对应服务器端,ClientRequst对应客户端
  • 大概是这样 Server继承net.Server,构造函数接受一个函数,这个函数有两个参数,一个是IncomingMessage继承于stream.Readable,一个 是ServerResponse继承于stream.Writable ,如果不传入函数,需要添加request事件来处理这个请求
  • 这个ServerClientRequest是一个层级,一个代表一个请求或者一个代表一个服务器,监听他们事件或者他们的回调函数会生成ServerResponseIncomingMessage类型的参数

理解部分

(1) http服务器和net.server 中区别

我们知道http.Server继承与net.Server,那么相比net.Server只是多了一个解析包装过程,因为Http是应用层协议,所以当我们通过net.Server拿到数据之后,我们需要根据数据解析出Http的相关参数,先监听socket的data事件,然后等事件发生的时候,进行解析 解析出请求头,在创建请求对象,再根据请求对象创建响应对象

(2)http场景分析

  • keep-alive

    对于前端应用,HTTP请求瞬间数量比较多,但每个请求传输的数据一般不大;这时,用同一个TCP连接处理同一个用户发出的HTTP请求可以显著提高性能。但是keep-alive也不是万能的,如果用户每次只发起一个请求,它反而会因为延长连接的生存时间,浪费服务器资源。

针对同一个连接,Node.js会维持一个incoming队列和一个outgoing队列。应用程序通过监听request事件,可以访问ServerResponse和IncomingMessage对象,当请求处理完成之后(调用response.end()),ServerResponse会响应finish事件。如果它是本次连接上最后一个response对象,则准备关闭连接;否则,继续触发request事件。每个连接最长超时时间默认为2分钟,可以通过http.Server.setTimeout调整。
现在把我们的Node.js版hello world修改一下

  • Expect头

如果客户端在发送POST请求之前,由于传输的数据量比较大,期望向服务器确认请求是否能被处理;这种情况下,可以先发送一个包含头Expect:100-continue的http请求。如果服务器能处理此请求,则返回响应状态码100(Continue);否则,返回417(Expectation Failed)。默认情况下,Node.js会自动响应状态码100;同时,http.Server会触发事件checkContinuecheckExpectation来方便我们做特殊处理。具体规则是:当服务器收到头字段Expect时:如果其值为100-continue,会触发checkContinue事件,默认行为是返回100;如果值为其它,会触发checkExpectation事件,默认行为是返回417。

  • Http代理实现

在实际开发时,用到http代理的机会还是挺多的,比如,测试说线上出bug了,触屏版页面显示有问题;我们一般第一时间会去看api返回是否正常,这个时候在手机上设置好代理就能轻松捕获HTTP请求了。老牌的代理工具有fiddler,charles。其实,nodejs下也有,例如node-http-proxyanyproxy。基本思路是监听request事件,当客户端与代理建立HTTP连接之后,代理会向真正请求的服务器发起连接,然后把两个套接字的流绑在一起。我们可以实现一个简单的代理服务器

import http, { Agent, ClientRequest, Server, ServerResponse, IncomingMessage, RequestOptions } from 'http';
import net from 'net';
import url from  'url';
// 实现一个代理服务器
const PORT = 8000;

const server = http.createServer((req, res) => {
  if (!req.url) { return };

  const urlOption = url.parse(req.url);
  const options: RequestOptions = {
    host: urlOption.host,
    port: urlOption.port || 80,
    method: req.method,
    headers: req.headers,
    path: urlOption.path,
  };

  const clientRequest = http.request(options, (clientRes) => {
    clientRes.pipe(res);
  })

  req.pipe(clientRequest);
})

server.listen(PORT, () => {
  console.log(`代理服务器开启,监听端口${PORT}`);
})

完整的代理服务器请参见下一节的实现,现在只是简单的实现,没有针对代理服务器的相关报文进行处理。

(3) 隧道代理

为了确保数据通信的安全,HTTPS已广泛应用于互联网,浏览器与服务器之间的HTTPS通信都是加密的。然而当浏览器需要通过代理服务器发起HTTPS请求时,由于请求的站点地址和端口号都是加密保存于HTTPS请求头中的,代理服务器是如何既确保通信是加密的(代理服务器自身也无法读取通信内容)又知道该往哪里发送请求呢?为了解决这个问题,浏览器需要先通过明文HTTP形式向代理服务器发送一个CONNECT请求告诉它目标站点地址及端口号。当代理服务器收到这个请求后,会在对应的端口上与目标站点建立一个TCP连接,连接建立成功后返回一个HTTP 200状态码告诉浏览器与该站点的加密通道已建成。接下来代理服务器仅仅是来回传输浏览器与该服务器之间的加密数据包,代理服务器并不需要解析这些内容以保证HTTPS的安全性,这个通道就是隧道代理。

  • 什么是隧道代理

connect请求主要用于代理,通过connect请求告诉代理,客户端接下去可能要建立一条HTTPS隧道,不要干扰我的数据。所以一般都是用户通过代理发起和目标网站的SSL握手从而进行HTTPS数据传输。

CONNECT请求是浏览器发给HTTP代理的,实质内容只有目标服务器的域名与端口,意思是请求代理建立一条到目标服务器的该端口的TCP连接,然后代理会在浏览器与目标服务器之间做字节流盲转发。

  • 什么情况下发起connect请求?
  1. 当浏览器配置使用代理服务器的时候,首先发送connect请求
  2. 发送方法设置为connect的请求

(4) 参考资料

@Genluo Genluo added the Node Node相关 label Sep 6, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Node Node相关
Projects
None yet
Development

No branches or pull requests

1 participant