分布式Node.js--02-协议

119 阅读5分钟

实现进程间的通信有多种方法,文件读写机制或者使用IPC( Inter-Process Communication ),但是这些方法都只能在一台计算机上进行进程通信,进程也可以通过网络进行通信,每台计算机的能提供的资源是有限的(亚马逊的AWS云服务器就是通过暴露API接口,来访问服务器资源)。

协议

协议是一种给通信双方提供的标准格式,一但,在没有协议的情况下通信双方会出现信息无法正确理解,或者不能完全理解信息。

谈到协议那肯定离不开OSI(Open Systems Interconnection)七层模型,这里主要用的是应用层HTTP协议,图放到下面,就不赘述了。

使用HTTP协议进行请求和响应

HTTP协议是基于文本的协议,位于TCP协议之上,当传输需要保证时是首选协议,这个协议基于请求,由客户端生成一个HTTP会话,和响应响应一样,由服务器返回给客户端。

HTTP压缩

可以通过压缩HTTP响应体,减少在网络上传输的数据,当客户端支持了压缩,可以设置Accept-Encoding 头,服务器一但遇到这个头就会选择压缩算法对响应体进行压缩,其中gzip是最常用的压缩算法。

基本使用

创建一个index.html文件

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
  </head>
  <body>
    <h1>哈哈哈哈哈哈哈哈哈哈</h1>
  </body>
</html>

创建server-gzip.js文件,引入http、zlib和fs

const http = require('http')

const zlib = require('zlib')
const fs = require('fs')

http.createServer((request, response) => {
  //读取文件流
  const raw = fs.createReadStream(__dirname + '/index.html');
  // 设置accept-encoding
  const acceptEncoding = request.headers['accept-encoding'] || '';
  
  response.setHeader('Content-Type', 'text/plain');
  console.log(acceptEncoding);

  //接收的头是否有gzip
  if (acceptEncoding.includes('gzip')) {
    console.log('使用了 gzip');
    // 设置gzip响应头
    response.setHeader('Content-Type', 'gzip');
    raw.pipe(zlib.createGzip()).pipe(response)
  } else {
    console.log('没有压缩');
    raw.pipe(response);
  }
}).listen(process.env.PORT || 8866)

压缩显示二进制信息

curl -H 'Accept-Encoding: gzip' http://localhost:8866/ | xxd

解压缩查看内容

curl -H 'Accept-Encoding: gzip' http://localhost:8866/ | gunzip

成功!

给自己补充的扩展:curl命令的行为类似客户端在网络上使用服务,这个服务打印请求是否使用压缩解释正在发生的行为

比较压缩前后的大小

之前是236之后是183,还是有可见的缩小的

HTTP压缩只压缩body内容,不压缩请求头

HTTP/TLS

加密,TLS(Transport Layer Security)是传输层安全,用于HTTP传输,也就是HTTPS。与gzip不同TLS对请求头也有效,TLS是CPU密集型操作,应该由外部进程执行,TLS取代了过时的SSL(Secure Sockets Layer )。

TLS使用证书进行验证,证书有两种

包含公钥public key的证书,可以被任何人持有

包含私钥private key的证书,只可以被少数特定人持有,且要保密

对于HTTP,服务器会提供它的公钥,客户端请求加密会使用这个公钥,当客户端与服务器首次通信时会产生大量的随机数和会话的密码,用公钥加密信息后发送到服务器,临时密码加密TLS会话。

通过openssl创建私钥和证书

创建私钥

openssl req -nodes -new -x509 -keyout basic-private-key.key

创建证书

openssl req -nodes -new -x509 -out basic-certificate.cert

(友情提醒:和我一样是macOS系统的小伙伴注意了,如果是新手且制作过Mac的证书的,千万不要自作聪明,如果使用的不是-node是创建的你的服务一定跑不起来,别问我咋知道的,这是个悲伤的故事,以下是错误示范)

openssl genrsa -out basic-private-key.key
openssl req -new -key basic-private-key.key -out basic-certificate.crt

(友情提醒2:创建私钥和证书一定要用一条命令一起创建出来,否则就会得到不匹配的错误)

Error: error:0B080074:x509 certificate routines:X509_check_private_key:key values mismatch

以下是正确示范

openssl req -nodes -new -x509 -keyout basic-private-key.key -out basic-certificate.cert

-------------------------------分割线------------------------------------------

在之前的食谱API进行补充,将生成的私钥和证书放入项目中

对producer_http_basci.js进行简单的修改,引入fs模块对私钥和证书进行同步读取创建HTTPS服务,其余部分基本不变


const fs = require('fs')
//fastify创建服务
const server = require('fastify')({
  https: {
    // 同步读取私钥和证书
    key: fs.readdirSync(__dirname+'路径'),
    cert: fs.readFileSync(__dirname+'路径')
  }
})

const HOST = process.env.HOST || '127.0.0.1'
const PORT = process.env.PORT || 4000

server.get('/recipes/:id', async(req, reply) => {
  console.log(`work pid =${process.pid}`);
  const id = Number(req.params.id)
  if (id !== 42) {
    reply.statusCode = 404
    return {error: 'not found'}
  } 
  return {
    producer_id:process.pid,
    recipe: {
      id, name: "Chicken Tikka Masala",
      steps: "Throw it in a pot...",
      ingredients: [
        { id: 1, name: "Chicken", quantity: "1 lb", },
        { id: 2, name: "Sauce", quantity: "2 cups", }
      ]
    }
  }
})

server.listen(PORT, HOST, () => {
  console.log(`running at https:${HOST}:${PORT}`);
})

成功启动

通过浏览器访问

这个原因很简单就是,因为签名是我们自己的,可以通过命令行加 --insecure进行测试

curl --insecure https://127.0.0.1:4000/recipes/42

成功获取数据

之前的web-api也需要更新一下


const server = require('fastify')()
const fetch = require('node-fetch')
const https = require('https')
const fs = require('fs')

const HOST = process.env.HOST || '127.0.0.1'
const PORT = process.env.PORT || 3000
const TARGET = process.env.TARGET || 'localhost:4000'

const options = {
  agent: new https.Agent({
    ca:fs.readFileSync('/Users/guofengrui/JS/distribute_node/shared/tls/basic-certificate.cert')
  })
}

server.get('/', async () => {
  const req = await fetch(`https://${TARGET}/recipes/42`, options)

  const payload = await req.json()

  return {
    consumer_pid: process.pid,
    producer_data: payload
  }
})

server.listen(PORT, HOST, () => {
  console.log(`Consumer running at https://${HOST}:${PORT}/`);
 
})

由于证书是自签名的,实际请求会有错误,但路子没问题。

GraphQL

GraphQL是Facebook设计的一种查询协议,非常适用于facade(外观) service(服务)这种开发模式,是位于多个其它服务和数据源服务之前,GraphQL尤其擅长给客户端返回最小量的数据。

注意:GraphQL没有规定底层需要使用的协议,通常HTTP协议即可

编写一个基本的gql文件

type Query {
  recipe(id: ID): Recipe
  pid: Int
}

type Recipe {
  id: ID!
  name: String!
  step: String
  ingredients: [Ingredient]!
}

type Ingredient {
  id: ID!
  name: String!
  quantity: String
}

第一个实体Query,代表根查询,由consumer提供,这段代码有两个必须设置的信息,pid和recipe。

接下来使用gql数据实体的方式改造之前的项目

producer部分

第一步:安装fastify-gql

第二步:设置实体解析器

第三步:注册解析器

consumer部分

第四步:设置查询参数

第五步:把参数包装为JSON字符串

producer-graphql.js


const server = require('fastify')()

const graphql = require('fastify-gql')
const fs = require('fs')

const schema = fs.readFileSync('/Users/guofengrui/JS/distribute_node/shared/graphql-schema.gql').toString()

const HOST = process.env.HOST || '127.0.0.1'
const PORT = process.env.PORT || 4000

// resolvers对象告诉graphql对象如何响应
const resolvers = {
  //查询实体的顶级
  Query: {
    pid: () => process.id,
    recipe: async (_obj, { id }) => {
      if (id != 42) throw new Error(`recipe ${id} 找不到`)
      return {
        id, name: 'Chicken Tikka Masala',
        steps: 'Throw it in a pot...'
      }
    }
  },
  // 当Recipe取到值,运行Recipe解析器
  Recipe: {
    ingredients: async (obj) => {
      return (obj.id != 42) ? [] : [
        { id: 1, name: "Chicken", quantity: "1 lb", },
        { id: 2, name: "Sauce", quantity: "2 cups", }
      ]
    }
  }
}
// 注册解析器、gql
server.register(graphql, {schema, resolvers, graphql:true}).listen(PORT, HOST, () => {
  console.log(`Producer running at http://${HOST}:${PORT}/graphql`);
})

consumer-graphql.js

const server = require('fastify')();
const fetch = require('node-fetch'); //2.6版本
const HOST = process.env.HOST || '127.0.0.1'
const PORT = process.env.PORT || 3000
const TARGET = process.env.TARGET || 'localhost:4000'
// gql查询参数
const complex_query = `query kitchenSink ($id:ID){
  recipe(id: $id) {
    id name
    ingredients {
      name quantity
    }
  }
  pid
}`

server.get('/', async () => {
  const req = await fetch(`http://${TARGET}/graphql`, {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    //参数通过JSON包裹起来,发送JSON字符串
    body: JSON.stringify({
      query: complex_query,
      variables: {id: "42"}
    })
  })
  return {
    consumer_pid: process.pid,
    producer_data: await req.json()
  }
})

运行,分布式方式3000访问4000,gql数据实体,成功获取数据