== Scenarios used in the project ==

Account scan code login, wechat scan code authorization, message real-time reminder, configuration results response, client data synchronization… In the previous project, I used round robin to do instant communication, which was inefficient and wasted resources. Later several projects began to use Websocket with KOA and Redis to achieve it. Now I have a thorough understanding of the whole instant communication implementation process.

Prerequisite: Packages to be installed

  1. Koa == was created by the same people behind Express and is much more elegant and clean to use than Node
  2. Navigation of koa – the router = = is routing, can be thought of in the actual application node interface www.npmjs.com/package/koa…
  3. Koa – websocket = = koa middleware encapsulation of websocket www.npmjs.com/package/koa…
  4. Redis = = we used here is subscribe to release function of redis www.npmjs.com/package/red…
  5. Dotenv = = get project of environment variables can also be front end in the config file. The js wrote death in www.npmjs.com/package/dot…

First, set up the basic socket service

The setup method is similar to node service, with more KOA-WebSocket middleware

const Koa = require('koa')
const router = require('koa-router') ()const websockify = require('koa-websocket')
const app = websockify(new Koa())

// Create a socket interface
router.all('/kapi/socket/init', async ctx => {
   const { channel } = ctx.query // The channel id will be passed by the client interface
   console.log(channel)
   ctx.websocket.send('message')})// The registered route allows the use of middleware
app.ws.use(router.routes()).use(router.allowedMethods())

// Dynamic port number can be used after the port number
app.listen(3131, () =>
 console.log(`socket listening on port ${Config.socket.port}`)
)
Copy the code

Link to subscribe to redis channel

On the basis of socket interface, added redis link, channel subscription, here only show the key code, will be done later

const Koa = require('koa')
const router = require('koa-router') ()const websockify = require('koa-websocket')
const app = websockify(new Koa())
const redis = require('redis')

// As with databases, these parameters are required to link redis channels
// Use dotenv dynamic fetch
const redisConfig = { 
   host: '18.8.1.3',
   port: '32',
   password: '1232343',
   db: 4
}
// Create a socket interface
router.all('/kapi/socket/init', async ctx => {
  const { channel } = ctx.query // Accept the channel id passed by the client and subscribe to Redis
  console.log(channel)
  
  Redis HTTP: / / https://www.npmjs.com/package/redis / / link
  let client = redis.createClient(redisConfig)
  
  // Subscribe to redis channel
  client.subscribe(channel)
  
  // Receive the message
  client.on('message', async (channel, message) => {
    console.log(
      'Received subscribe message, channel [%s] message [%s]',
      channel,
      message
    )
    // The message is received and returned to the client through the interface
    await  ctx.websocket.send(message) 
  })
})
 
// The registered route allows the use of middleware
app.ws.use(router.routes()).use(router.allowedMethods())

// Dynamic port number can be used after the port number
app.listen(3131, () =>
  console.log(`socket listening on port ${Config.socket.port}`)
)
Copy the code

Client call

Do not forget to start the socket before the client call, you can use PM2 or NPM to run koA files, showing the key code, as to listen for error, open, close

import React, { useState, useEffect, UseCallback} from 'react' import createSocket from 'SRC/API /socket' // Import socket export default ()=>{const [mess, setMess] = useState(' no message ') const [channel] = useState(2) const sendSocket = useCallback(() => {// Create socket request, The node on the interface, the channel id wsServer = new WebSocket (` localhost: 3131 / kapi/socket/init? Channel = ${channel} `) / / listening news return wsServer addEventListener (' message ', socketMessage) const socketMessage = (event) => { const data = JSON.parse(event.data) if(data.code===200){ The console. The log (data) setmess (data) / / get the corresponding message to cancel listening wsServer. RemoveEventListener (' message 'socketMessage)}}}, UseEffect (() => {sendSocket()}, [sendSocket]) return (<div>{mess}</div>)}Copy the code

Fourth, splitting and sorting refinement

First, split the original socket service and then refine it

const Koa = require('koa')
const router = require('koa-router') ()const websockify = require('koa-websocket')
const app = websockify(new Koa())
const Config = require('.. /.. /config/const')
const socketApiRoutes = require('./route')
// socket route
socketApiRoutes(router)
// Register the route
app.ws.use(router.routes()).use(router.allowedMethods())

app.listen(Config.socket.port, () =>
  console.log(`socket listening on port ${Config.socket.port}`)
)
Copy the code

2.redis.js

const redis = require('redis')
const dotenv = require('dotenv').config({
  path: process.env.NODE_ENV == 'production' ? '.env' : '.env.local'
})
// The parameter above is written dead, can be obtained dynamically
const ENV = dotenv.parsed
const redisConfig = {
  host: ENV.REDIS_HOST,
  port: ENV.REDIS_PORT,
  password: ENV.REDIS_PASSWORD,
  db: ENV.REDIS_DB
}

// http://redis.js.org/
const createRedisClient = (channel, callback) => {
  // https://www.npmjs.com/package/redis
  let client = redis.createClient(redisConfig)
  / / subscribe
  client.subscribe(channel)
  // Listen for Redis ready
  client.on('ready', () => {
    console.log(
      'Redis [%s:%s/%s] is connected and ready for subscribe channel [%s] use.',
      redisConfig.host,
      redisConfig.port,
      redisConfig.database,
      channel
    )
  })
  // Listen on redis connect
  client.on('connect', () => {
    console.log('Redis connect')})// Receive the message
  client.on('message', async (channel, message) => {
    console.log(
      'Received subscribe message, channel [%s] message [%s]',
      channel,
      message
    )
    await callback(channel, message)
  })

  // Listen on redis connect
  client.on('reconnecting', err => {
    console.log('Redis reconnecting:' + err)
  })

  // Listen for errors in redis
  client.on('error', err => {
    console.log('Redis Error:' + err)
  })

  // Listen for redis subscription events
  client.on('subscribe', (channel, count) => {
    console.log(
      'client subscribed to ' + channel + ', ' + count + ' total subscriptions')})// Listen for redis unsubscribe events
  client.on('unsubscribe', (channel, count) => {
    console.log(
      'client unsubscribed from' +
        channel +
        ', ' +
        count +
        ' total subscriptions')})return client
}

module.exports = createRedisClient

Copy the code

3.route.js

const createRedisClient = require('./redis')

const socketApiRoutes = router => {
  router.all('/kapi/socket/init', async ctx => {
    const { channel } = ctx.query
    console.log(channel)

    createRedisClient(channel, (channel, message) => {
      console.log(`on message channel: ${channel}`)
      waitForSocketConnection(ctx.websocket, () => {
        // Wait for the connection to open before sending a message
        ctx.websocket.send(message)
      })
    })

    function waitForSocketConnection(socket, callback) {
      setTimeout(() => {
        if (socket.readyState == 1) {
          if(callback ! =null) {
            callback()
          }
        } else {
          waitForSocketConnection(socket, callback)
        }
      }, 1000)}// Listen for web messages
    ctx.websocket.on('message', function(res) {
      // console.log('ctx websocket web message', res)
    })

    // Listen for close events
    ctx.websocket.on('close', function() {
      // client.unsubscribe()
      console.log('ctx websocket close')
    })

    ctx.websocket.body = {
      status: true}})}module.exports = socketApiRoutes

Copy the code

Second, encapsulate the client socket call

import { isSupportSocket } from 'src/utils/util'

interface ScoketEvent { open? : () = >voidmessage? : (event: MessageEvent) =>voiderror? : () = >voidclose? : () = >void
}

constcreateSocket = (pathname: string, eventOption? : ScoketEvent) => { let wsServer: WebSocket |null = null

  if (isSupportSocket()) {
    / / request socket
    wsServer = new WebSocket(
      `${window.location.protocol === 'https:' ? 'wss' : 'ws'} :/ / ${
        window.location.host
      }${pathname}`
    )
    // Initialize the socket event
    let _interval: NodeJS.Timer | null = null
    let _setTimeout: NodeJS.Timer | null = null

    if (wsServer) {
      const socketOpen = () => {
        if (eventOption && eventOption.open) {
          eventOption.open()
        }
        // Heartbeat check
        wsServer && wsServer.send('socket heart check')
        _interval = setInterval(() => {
          wsServer && wsServer.send('socket heart check')},30 * 1000)}const socketMessage = (event: MessageEvent) => {
        if (eventOption && eventOption.message) {
          eventOption.message(event)
          console.log(event, 'socketMessage')}}const socketError = () => {
        if (eventOption && eventOption.error) {
          eventOption.error()
        }
        if (_interval) clearInterval(_interval)
        wsServer = null
        console.log('websocket error')}const socketClose = () => {
        if (eventOption && eventOption.close) {
          eventOption.close()
        }

        if (wsServer) {
          wsServer.removeEventListener('open', socketOpen)
          wsServer.removeEventListener('message', socketMessage)
          wsServer.removeEventListener('error', socketError)
        }
        if (_interval) clearInterval(_interval)
        if (_setTimeout) clearTimeout(_setTimeout)
      }

      // Triggered when the connection is established
      wsServer.addEventListener('open', socketOpen)
      // Triggered when the listening message client receives data from the server
      wsServer.addEventListener('message', socketMessage)
      // Triggered when a communication error occurs
      wsServer.addEventListener('error', socketError)

      // Triggered when a communication error occurs
      wsServer.addEventListener('close', socketClose)
    }
  } else {
    console.error('This browser does not support sockets, please install a new version')}return wsServer
}

export default createSocket

Copy the code

Third, the client socket call

Call the socket encapsulated above in the page

import React, { useState, useEffect, useCallback } from 'react' import { RouteConfig } from 'react-router-config' import createSocket from 'src/api/socket' interface IProps extends RouteConfig {} export default (props: IProps) => {const [channel] = useState<number>(2) const [mess,setmess] = useState<string>(' no message ') const sendSocket = useCallback(() => { const wsServer: WebSocket | null = createSocket( `/kapi/socket/init? channel=${channel}`, { message: socketMessage } ) function socketMessage(event: MessageEvent) { const data = JSON.parse(event.data) console.log(data) setmess(data) wsServer && wsServer.close() } return () => { if (wsServer) { wsServer.removeEventListener('message', socketMessage) wsServer.close() } } }, [channel]) useEffect(() => { sendSocket() }, [sendSocket]) return ( <div>{mess}</div> ) }Copy the code

Finishing, I hope to help my friends.