使用 kubernetes 和微服务的 nodejs 上的 socket.io 问题

issue with socket.io on nodejs with kubernetes and microservices

很长一段时间我不必在这里写 post。我想我真的被困住了...... 很久以前,我构建了一个基于 React 和 Express 的单体应用程序,它正在处理与 socket.io 的聊天。我记得我确实有点挣扎,但最后我成功了。

我现在正在使用 kubernetes(在 GKE 上)将同一个应用程序重新转换为微服务,在构建聊天后端和前端之后,我就是无法进行聊天。 socket.io 实例似乎以某种方式未连接。我尝试了很多不同的事情,现在我正在寻求帮助。我将在下面分享暗示它的代码部分。

使用 EXPRESS 聊天后端:

我正在声明一个中间件以将 io 作为 req.io 传递,以便能够在特定端点中使用。这部分工作正常(至少在我看来)

require("express-async-errors")
const express = require("express")
const helmet = require("helmet")
const socket = require("socket.io")
const http = require("http")
const compression = require("compression")
const bodyParser = require("body-parser")
const cookieSession = require("cookie-session")
const cookieParser = require("cookie-parser")

// IMPORT ROUTES
const routes = require("./routes")

// IMPORT MIDDLWARES
const Import = require("@archsplace/archsplace_commun")
const isError = Import("middlewares", "isError")
const isCurrentUser = Import("middlewares", "isCurrentUser")
const { NotFoundError } = Import("factory", "errors")

// LAUNCH EXPRESS
const app = express()
const server = http.createServer(app)
const io = socket(server)

const secure = process.env.NODE_ENV !== "test"

// USE MAIN MIDDLWWARE
app.set("trust proxy", true)
app.use(helmet())
app.disable("x-powered-by")
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())
app.use(cookieSession({ signed: false, secure }))
app.use(isCurrentUser)
app.use(cookieParser())
app.use(compression())
app.use((req, res, next) => {
  req.io = io
  next()
})

// USE ROUTES
routes.map(route => app.use(route.url, route.path))
app.all("*", async (req, res) => {
  throw new NotFoundError()
})

// USE CUSTOM MIDDLWWARE
app.use(isError)

module.exports = server

然后我有一个端点来发出聊天事件,它与我用来 POST 我的 mongoDB 数据库中的消息的相同端点。我认为这部分以某种方式起作用,但不确定它到底在哪里发射

发出套接字事件的终点

const express = require("express")
const db = require("mongoose")

// DATABASE AND LIBRARIES
const Import = require("@archsplace/archsplace_commun")
const isAuthenticated = Import("middlewares", "isAuthenticated")
const isActivated = Import("middlewares", "isActivated")
const Chat = Import("models", "Chat", "chat")
const Room = Import("models", "ChatRoom", "chat")

// EVENTS
const { NatsWrapper } = require("../../../services/natsWrapper")
const { RoomUpdatedPub } = require("../../../events/publishers/roomUpdatedPub")
const { ChatCreatedPub } = require("../../../events/publishers/chatCreatedPub")

// VALIDATES
const { BadRequestError, DatabaseConnectionError } = Import("factory", "errors")

const router = express.Router()

// @route  POST api/chat/private/message/:roomId
// @desc   Post Chat message by chatroom id
// @access Private
router.post("/:roomId", isAuthenticated, isActivated, async (req, res) => {
  // DEFINE QUERIES
  let chat
  const { message, avatar } = req.body
  const { roomId } = req.params

  // ENSURE ROOM EXIST FOR USER
  const room = await Room.findOne({ $and: [{ _id: roomId }, { _users: { $elemMatch: { _user: req.user._id } } }] })
  if (!room) {
    throw new BadRequestError("This chatroom doesn't exist")
  }

  // CREATE CHAT
  const chatFields = {
    message,
    _emitter: req.user._id,
    _chatId: roomId
  }

  // EMIT TO SOCKET
  req.io.emit(roomId, {
    ...chatFields,
    avatar: avatar,
    read: 1,
    role: req.user.authorities
  })

  // HANDLE MONGODB TRANSACTIONS
  const SESSION = await db.startSession()
  try {
    // CREATE CHAT
    await SESSION.startTransaction()
    chat = await new Chat(chatFields).save()
    await room.set({ lastUpdated: Date.now() }).save()
    await new RoomUpdatedPub(NatsWrapper.client()).publish(room)
    await new ChatCreatedPub(NatsWrapper.client()).publish(chat)
    await SESSION.commitTransaction()

    // RETURN AND FINALIZE ENDPOINT
    res.status(201).json(chat)
  } catch (e) {
    // CATCH ANY ERROR DUE TO TRANSACTION
    await SESSION.abortTransaction()
    console.error(e)
    throw new DatabaseConnectionError()
  } finally {
    // FINALIZE SESSION
    SESSION.endSession()
  }
})

module.exports = router

入口 NGINX CONGIS

这可能是错误的,我在互联网上看到我们需要使用此注释 nginx.ingress.kubernetes.io/websocket-services 并且我将其归因于我建立聊天的服务器(以及我所在的位置)使用 socket.io)

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: ingress-service
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/use-regex: "true"
    nginx.ingress.kubernetes.io/permanent-redirect-code: "301"
    nginx.ingress.kubernetes.io/from-to-www-redirect: "true"
    # SOCKET CONFIGURATIONS
    nginx.ingress.kubernetes.io/websocket-services: "chat-srv"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "1800"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "1800"
spec:
  rules:
    - host: www.archsplace.dev
      http:
        paths:
          - path: /api/architect/?(.*)
            backend:
              serviceName: architect-srv
              servicePort: 3000
          - path: /?(.*)
            backend:
              serviceName: client-website-srv
              servicePort: 3000
    - host: architects.archsplace.dev
      http:
        paths:
          - path: /auth/?(.*)
            backend:
              serviceName: auth-srv
              servicePort: 3000
          - path: /api/account/?(.*)
            backend:
              serviceName: account-srv
              servicePort: 3000
          - path: /api/architect/?(.*)
            backend:
              serviceName: architect-srv
              servicePort: 3000
          - path: /api/chat/?(.*)
            backend:
              serviceName: chat-srv
              servicePort: 3000
          - path: /?(.*)
            backend:
              serviceName: client-architects-srv
              servicePort: 3000
    - host: users.archsplace.dev
      http:
        paths:
          - path: /auth/?(.*)
            backend:
              serviceName: auth-srv
              servicePort: 3000
          - path: /api/account/?(.*)
            backend:
              serviceName: account-srv
              servicePort: 3000
          - path: /api/architect/?(.*)
            backend:
              serviceName: architect-srv
              servicePort: 3000
          - path: /api/chat/?(.*)
            backend:
              serviceName: chat-srv
              servicePort: 3000
          - path: /?(.*)
            backend:
              serviceName: client-users-srv
              servicePort: 3000
    - host: partners.archsplace.dev
      http:
        paths:
          - path: /auth/?(.*)
            backend:
              serviceName: auth-srv
              servicePort: 3000
          - path: /api/account/?(.*)
            backend:
              serviceName: account-srv
              servicePort: 3000
          - path: /api/chat/?(.*)
            backend:
              serviceName: chat-srv
              servicePort: 3000
          - path: /?(.*)
            backend:
              serviceName: client-partners-srv
              servicePort: 3000
    - host: admin.archsplace.dev
      http:
        paths:
          - path: /auth/?(.*)
            backend:
              serviceName: auth-srv
              servicePort: 3000
          - path: /api/account/?(.*)
            backend:
              serviceName: account-srv
              servicePort: 3000
          - path: /api/architect/?(.*)
            backend:
              serviceName: architect-srv
              servicePort: 3000
          - path: /api/chat/?(.*)
            backend:
              serviceName: chat-srv
              servicePort: 3000
          - path: /?(.*)
            backend:
              serviceName: client-admin-srv
              servicePort: 3000
    - host: business.archsplace.dev
      http:
        paths:
          - path: /?(.*)
            backend:
              serviceName: client-business-srv
              servicePort: 3000

然后在客户端,我在我的 React 应用程序上使用库 socket.io-client。

我声明 IO 库的客户端实用程序

自从我使用 architects.archsplace.dev,我假设聊天服务器只能在 /api/chat 可用,因为它是在入口 nginx 上定义的,但我不确定..

import io from "socket.io-client"

export const socket = io(`${window.location.host}/api/chat`, {
  reconnect: true
})

然后我用聊天构建了一个 UI,我实际上正在尝试接收聊天信息并将其存储到反应状态:

使用影响组件来加载套接字

在这里您还可以看到使用效果我正在使用roomId连接到我之前在后端部分发出的同一事件。 (这是在整体上工作但不是在这里)

import React, { useState, useEffect } from "react"
import { connect } from "react-redux"
import Timestamp from "react-timestamp"

// IMPORT ACTIONS
import { sendMessage, getMessages } from "@actions/chatActions"

// IMPORT COMPONENTS
import ChatMessage from "./ChatMessage"

// IMPORT UTILS
import { socket, isEmpty, imageRender } from "@utils"

const ChatRoom = ({ sendMessage, getMessages, classes, room, user, chat: { messages } }) => {
  // HOOKS
  const [state, setState] = useState({
    message: "",
    messages: [],
    errors: {},
    typing: false,
    trigger: false,
    isInteracted: false,
    page: 0,
    limit: 20
  })
  const target = room.users.find(({ _id }) => _id !== user._id)
  const isOnline = Math.floor(Date.now() - new Date(target.lastConnectionDate).getTime() / 1000) && target.isOnline

  // USE EFFECT
  useEffect(() => {
    getMessages(state.page, state.limit, room._id)
  }, [getMessages, state.page, state.limit, room._id])

  useEffect(() => {
    const handleMessageSocket = () => {
      const addMessage = data => setState(prevStates => ({ ...prevStates, messages: [...state.messages, data] }))
      socket.on(room._id, data => addMessage(data))
    }
    const clearMessageSocket = () => {
      socket.off(room._id)
      setState(prevStates => ({ ...prevStates, messages: [] }))
    }
    // LOAD DATA FROM REDUCER
    setState(prevStates => ({ ...prevStates, messages }))
    // LOAD SOCKETS
    handleMessageSocket()
    return () => clearMessageSocket()
  }, [room._id, state.messages, messages])

  // HANDLE FUNCTIONS
  const handleMessage = e => setState(prevStates => ({ ...prevStates, message: e.target.value }))
  const clearMessage = () => setState(prevStates => ({ ...prevStates, message: "" }))
  const handleSubmit = e => {
    e.preventDefault()
    const chatMessage = { message: state.message, avatar: imageRender(user.avatar, "tr:n-user_avatar_small") }
    !isEmpty(state.message) && sendMessage(chatMessage, room._id)
    clearMessage()
  }
  // RENDER CHATROOM ITEM
  const renderAvatar = () => {
    return (
      <div className={classes.chatRoomItemAvatar}>
        <img src={imageRender(target.avatar, "tr:n-user_avatar_small")} alt={target.name} />
        {isOnline && <div className={classes.chatRoomItemActive} />}
      </div>
    )
  }
  const renderInfo = () => {
    return (
      <div className={classes.chatRoomItemInfo}>
        <h3>{target.name}</h3>
        <p>
          <i>access_time</i>
          <Timestamp className="request-item-timestamp" relative date={target.lastConnectionDate} autoUpdate />
        </p>
      </div>
    )
  }
  const renderChatItem = () => {
    return (
      <div className={classes.chatRoomItem}>
        {renderAvatar()}
        {renderInfo()}
      </div>
    )
  }
  // RENDER MESSAGE AREA
  const renderMessages = () => {
    return <div className={classes.chatMessages}>{JSON.stringify(state.messages.map(i => i.message))}</div>
  }

  // RENDER INPUT AREA
  const renderInput = () => {
    return (
      <form className={classes.chatInputWrapper} autoComplete="off" onSubmit={e => handleSubmit(e)}>
        <div className={classes.chatInput}>
          <input type="text" placeholder="Message" value={state.message} onChange={handleMessage} />
          <button type="submit">
            <i>reply</i>
          </button>
        </div>
      </form>
    )
  }

  // MAIN
  return (
    <div className={classes.chatRoom}>
      {renderChatItem()}
      {renderMessages()}
      {renderInput()}
    </div>
  )
}

const mapStateToProps = state => ({
  chat: state.chat
})

export default connect(mapStateToProps, { sendMessage, getMessages })(ChatRoom)

所以如果有人遇到过同样的问题,并且知道我可能做错了什么,我完全被困住了。我什至尝试设置一个 redis 服务并通过 redis io 适配器传递套接字,但也没有用...

我在那里为那些可能像我一样挣扎的人找到了解决方案...我认为它有点老套,但效果很好。

我在前面观察到套接字一直在 /socket.io/.... 下触发,如果您查看我的入口 nginx,它会查看我的 React 应用程序,并且return 可能是一个 404 页面。

所以我使用以下代码强制我的 chat-srv 出现在这个特定端点上:

 - path: /socket.io/?(.*)
   backend:
     serviceName: chat-srv
     servicePort: 3000

然后一旦我这样做了,它实际上是在查看我的聊天后端并解决了。我还需要指定我使用 socket.io

的 2.2.0 版本

我试过版本 3,但没用。