第八十一期:Node中的网络协议(http协议)

266 阅读4分钟

这里记录工作中遇到的技术点,以及自己对生活的一些思考,周三或周五发布。

封面图

Node中的 网络协议

在前端的日常开发中,网络协议使用的似乎并不多。但是其实我们每天都在用,比如http, websocket , smtp。

http是什么?全称我记得好像叫hyper text transform protocol 超文本传输协议。

websocket呢,则是基于tcp的双工通信协议,双工这个词我们可以联想到之前提到多的双工流,我们可以理解为是双向的,也就是说它是一个双向通信协议。

smtp 的全称是simple mail tranfer protocol,简单的邮件传输协议。

这是他们的一个基本概念。

Node和Php这种以模板为中心的语言不同的一点是,我们可以在不牺牲简单内容控制的情况下可以对想要的行为进行控制。

比如我们可以自定义服务,同时在代码层面分发不同的内容。

http服务

http协议是我们日常开发应用时使用最多的协议,我们先拿它来举个例子。

我们创建server.js

const http = require('http')

const host = process.env.HOST || '0.0.0.0'
const port = process.env.port || '4040'

const server = http.createServer((req, res) => {
  if (req.method === 'get') {
    return error(res, 405)
  }

  if (req.url === '/users') {
    return users(res)
  }

  if (req.url === '/') {
    return index(res)
  }

  error(res, 404)
})

function index(res) {
  res.end(`{
    data:'我的第一个http服务'
    version:'1.0.0'
  }`)
}

function users(res) {
  res.end(`{
    data:[
      name:'terrence',
      age:'28'
    ]
  }`)
}

function error(res, code) {
  res.statusCode = code
  res.end(`{
    msg:${http.STATUS_CODES[code]}
  }`)
}

server.listen(port, host)

然后我们启动这个服务:

node server.js

我们重新开启一个终端窗口,用来发送请求,则可以看到下面的结果:

我们用http的createServer方法创建了一个服务,这个方法接收一个回调函数,回调函数有两个参数,第一个是接收到的请求,第二个是响应给客户端的方法,我们可以通过对收到的请求req做不同的拦截,然后给客户端响应不同的数据。

当请求的方方法是get的时候,返回一个405的状态码,表示请求方法不被支持。而当其他url的时候,则返回对应的你内容。

这里有个细节是错误处理方法:

function error(res, code) {
  res.statusCode = code
  res.end(`{
    msg:${http.STATUS_CODES[code]}
  }`)
}

我们的状态码其实可以通过http.STATUS_CODES进行匹配,它会输出不同的错误信息。

这只是个简单的例子,有些更有意思的问题,比如怎么样让我们的服务绑定到一个随机的端口上呢?或者可以根据不同的url动态返回对应的内容呢?

随机端口

这个比较简答,我们只需要将port设置为0即可。

我们简单修改下我们的代码,将port设置为0,同时在用listen方法监听时打印出服务的相关信息。

const http = require('http')

const host = process.env.HOST || '0.0.0.0'
const port = process.env.port || 0

const server = http.createServer((req, res) => {
  if (req.method === 'get') {
    return error(res, 405)
  }

  if (req.url === '/users') {
    return users(res)
  }

  if (req.url === '/') {
    return index(res)
  }

  error(res, 404)
})

function index(res) {
  res.end(`{
    data:'我的第一个http服务'
    version:'1.0.0'
  }`)
}

function users(res) {
  res.end(`{
    data:[
      name:'terrence',
      age:'28'
    ]
  }`)
}

function error(res, code) {
  res.statusCode = code
  res.end(`{
    msg:${http.STATUS_CODES[code]}
  }`)
}

server.listen(port, host, () => {
  console.log(JSON.stringify(server.address()))
})

再次执行 node server.js 可以看到服务运行在不同的随机端口上。

因为listen方法中的回调函数在服务绑定到端口上的时候就会被触发。

动态处理内容

很多人学习Node第一个例子就是上面那个,用Node创建一个静态服务。但是现在看来其实意义不大,因为Node的优势在于能够快速的处理动态内容。

我们可以给我们上面的代码加一个简单的过虑功能。

const http = require('http')
const qs = require('querystring')
const url = require('url')

const host = process.env.HOST || '0.0.0.0'
const port = process.env.port || 4040

const userList = [
  { name: 'terrence', age: 28, type: 'zh' },
  { name: 'apple', age: 29, type: 'cn' },
  { name: 'seven', age: 27, type: 'zh' },
]

const server = http.createServer((req, res) => {
  const { pathname, query } = url.parse(req.url)
  if (req.method === 'get') {
    return error(res, 405)
  }
  if (req.url === '/') {
    return index(res)
  }

  if (pathname === '/users') {
    return users(query, res)
  }

  error(res, 404)
})

function index(res) {
  res.end(`{
    data:'我的第一个http服务'
    version:'1.0.0'
  }`)
}

function users(query, res) {
  const { type } = qs.parse(query)
  const list = !type ? userList : userList.filter((user) => user.type)
  res.end(`{
    data:${JSON.stringify(list)}
  }`)
}

function error(res, code) {
  res.statusCode = code
  res.end(`{
    msg:${http.STATUS_CODES[code]}
  }`)
}

server.listen(port, host, () => {
  console.log(JSON.stringify(server.address()))
})

然后重启服务,重新在终端发起请求,就可以实现一个简单的根据参数请求的demo。

当然,这里用url模块儿对请求地址做了解析,当pathname等于users的时候根据请求的参数type对userList做了过虑,然后返回到客户端。

处理POST请求

如果我们需要接收post请求,那么我们肯定需要修改我们的代码,让他能接受以及处理post请求方法。

另一方面,需要提到的是,在以I/O为主要运行时的语言中,访问后的数据将被视为访问属性。

怎么理解这句话呢?就好比在PHP中我们可以通过$_post['filedname']来获取post中的参数,在Node中,我们可以用同样的方法来获取参数。

我们可以新建个form.html

<html>

  <head></head>

  <body>
    <form method="POST" action="http://localhost:4040">
      <input type="text" name="username"><br>
      <input type="text" name="age">
      <input type="submit">
    </form>
  </body>

</html>

然后重新修改我们的代码:

const http = require('http')
const fs = require('fs')
const path = require('path')
const url = require('url')
const form = fs.readFileSync(path.join(__dirname, 'form.html'))

const host = process.env.HOST || '0.0.0.0'
const port = process.env.port || 4040

const server = http.createServer((req, res) => {
  // const { pathname, query } = url.parse(req.url)
  if (req.method === 'GET') {
    get(res)
    return
  }
  reject(405, 'Method Not Allowed', res)
})

function get(res) {
  res.writeHead(200, { 'Content-Type': 'text/html' })
  res.end(form)
}

function reject(code, msg, res) {
  res.statusCode = code
  res.end(msg)
}

server.listen(port, host, () => {
  console.log(JSON.stringify(server.address()))
})

这时候启动服务,然后用终端发起请求,会直接返回表单代码的结构。因为我们用fs读取了form.html中的内容,并在get方法时返回给了客户端。

我们接着添加post方法的处理:

const http = require('http')
const fs = require('fs')
const qs = require('querystring')
const path = require('path')
const url = require('url')
const form = fs.readFileSync(path.join(__dirname, 'public', 'form.html'))

const host = process.env.HOST || '0.0.0.0'
const port = process.env.port || 4040

const maxData = 2 * 1024 * 1024 // 2MB

const server = http.createServer((req, res) => {
  // const { pathname, query } = url.parse(req.url)
  if (req.method === 'GET') {
    get(res)
    return
  }

  if (req.method === 'POST') {
    post(req, res)
    return
  }
  reject(405, 'Method Not Allowed', res)
})

function get(res) {
  res.writeHead(200, { 'Content-Type': 'text/html' })
  res.end(form)
}

function post(req, res) {
  if (req.headers['content-type'] !== 'application/x-www-form-urlencoded') {
    reject(415, 'Unsupported Media type', res)
    return
  }
  const size = parseInt(req.headers['content-length'], 10)
  if (isNaN(size)) {
    reject(400, 'Bad Request', res)
    return
  }
  if (size > maxData) {
    reject(413, 'Too Large Data', res)
    return
  }
  const buffer = Buffer.allocUnsafe(size)
  let pos = 0
  req
    .on('data', (chunk) => {
      const offset = pos + chunk.length
      if (offset > size) {
        reject(413, 'Too Large Data', res)
        return
      }
      chunk.copy(buffer, pos)
      pos = offset
    })
    .on('end', () => {
      if (pos !== size) {
        reject(400, 'Bad Request', res)
        return
      }
      const data = qs.parse(buffer.toString())
      console.log('用户post数据:', data)
      res.end('用户post数据:', +JSON.stringify(data))
    })
}

function reject(code, msg, res) {
  res.statusCode = code
  res.end(msg)
}

server.listen(port, host, () => {
  console.log(JSON.stringify(server.address()))
})

然后用表单提交数据,这时候就可以看到用户用post提交的数据。

如果我们细心的话,我们可以看到代码中我们最发送的内容的Content-type以及Content-Size做了检测,这其实可以防止各种类型的攻击,从某些方面来说可以提高我们应用的安全性。

还有一点是,我们对请求数据的大小做了限制,并且用buffer创建了一块儿内存,当数据接收完成后,在end事件中,我们对数据的大小做了判断。如果不做判断,就有可能造成内存泄露。

这个场景其实是为了防止一些恶意的攻击,从而导致服务器内存过载。

最后

  • 公众号《JavaScript高级程序设计》
  • 公众号内回复”vue-router“ 或 ”router“即可收到 VueRouter源码分析的文档。
  • 回复”vuex“ 或 ”Vuex“即可收到 Vuex 源码分析的文档。

全文完,如果喜欢。

请点赞和"在看"吧,最好也加个"关注",或者分享到朋友圈。