长链接这种业务的需求,在前端的应用场景还是比较多的,比如我上一家公司做的 IM 及时通讯, 这家公司需要用到的实时状态更新,今天就让我们来简单搞一个本地就能调试的前后端长链接服务好了!
最终我们连接成功以后可以这样调用一下子, 本地通过 node 启动的服务来互相之间进行通信, 这里长链接的心跳检测原理就不解释了,已经有很多人解释的比较全面啦~ , 长链接的链接 和建立 从逻辑来讲还是很清晰的。
准备
- 首先我们需要两个依赖包
"socket.io": "^4.5.4","socket.io-client": "^4.5.4", - 目录结构如下,就是自己本地搭一下 简单的不行
这里,后面我后端的代码,可以直接放在里面, 前端代码我进行了一层封装, 封装了一个公共类, 使用继承的方式来使用。
然后我们可以使用 node 文件名 启动后端node服务, 前端的我们可以把代码插入到我们任何一个项目当中,随便在一个项目的启动文件或者页面中都可以。
前端
- 首先我们依赖的包,主要是
socket.io-client, - 公共类
/*
* @Description:长链接包源头使用
* @Autor: codeBo
* @Date: 2023-06-21 10:21:18
* @LastEditors: gjzxlihaibo@163.com
* @LastEditTime: 2023-06-25 14:01:26
*/
import io from 'socket.io-client'
// 实例化 io 类
class socketIO {
socket: any
constructor(opt: { url: string; query: any }) {
// 最新版直接使用 io 调用,不需要使用 io.connnet , 第一个参数是 url ,第二个参数是配置项 后端可通过 on connection 事件 参数中 解构出来 handshake 读取
this.socket = io(opt.url, {
reconnection: true, // 是否从新链接 默认 true
transports: ['websocket'],
query: opt.query || {},
})
}
// 派发事件
emit(event: string, param: any): void {
console.log('=============send:', event, ':', param)
this.socket.emit(event, param)
}
// 侦听事件, callback 是回调, 后端直接调用传参给前端
addListener(event: string, callback: (res: any) => undefined) {
this.socket.on(event, (data: any) => {
callback(data)
})
}
// 开启长链接
open(): void {
console.log('=============socket-open')
this.socket.open()
}
// 链接
connect(): void {
this.socket.connect()
}
// 关闭
close(): void {
this.socket.close()
}
}
export default socketIO
- 使用公共类
下面的 socketIO 就是导入我们这个公共类, EventEmitter 是一个方法类, 是一个 事件bus,代码在这个之后
import EventEmitter from '../util/event'
import { MSG_EVENT_CONSTANT } from '../util/constant'
import socketIO from './socket'
import { INTERFACE_SOCKET_OPT_BASE } from './interface'
export class ProcessSocket extends EventEmitter {
socket: any
opt: INTERFACE_SOCKET_OPT_BASE
constructor(opt: INTERFACE_SOCKET_OPT_BASE) {
super()
// opt socket 配置项, 可以根据需求自行更改, socketUrl 必填
this.opt = Object.assign(
{
socketUrl: '',
token: '',
userId: '',
loginType: '',
userType: '',
},
opt,
)
this.socket = new socketIO({
url: opt.socketUrl,
query: {
systemId: this.opt.userId,
token: this.opt.token,
userType: this.opt.userType,
loginType: this.opt.loginType,
},
})
this.bindEvent()
}
bindEvent = (): void => {
// 连接成功
this.socket.addListener('connect', () => {
console.log('=============connected:')
this.emit(MSG_EVENT_CONSTANT.CONNECT_CHANGE, 1)
// 用户信息校验
const param = {
cmd: 'register',
userId: this.opt.userId,
role: this.opt.userRole || 'default',
deviceVersion: 'web service system',
token: this.opt.token,
}
this.socket.emit('my_event', param)
})
// 连接断开
this.socket.addListener('disconnect', (reason: string) => {
console.log('=============discounnect:', reason)
// if (reason === "io server disconnect") {
// this.socket.connect();
// }
this.emit(MSG_EVENT_CONSTANT.CONNECT_CHANGE, 0)
//this.socket.connect();
})
// 连接错误
this.socket.addListener('error', (reason: string) => {
console.log('=============error连接错误:', reason)
this.emit(MSG_EVENT_CONSTANT.CONNECT_CHANGE, 0)
})
// 连接错误
this.socket.addListener('connect_error', (reason: string) => {
console.log('=============connect_error连接错误', reason)
this.emit(MSG_EVENT_CONSTANT.CONNECT_CHANGE, 0)
})
// 接收消息
this.socket.addListener('connect_msg', (reason: string) => {
console.log('=============接收消息', reason)
})
}
// 登录
login(): void {
this.socket.open()
}
// 发送消息 我们可以使用这个方法 来调试动态消息的发送与接受,页面中可以加一个按钮,然后调用实体类的这个方法
sendMessage(dataObj: any): void {
const sendData = {
cmd: 'chat',
message: dataObj,
}
this.socket.emit('my_event', sendData)
}
}
- 事件类
export default class EventEmitter {
_eventMap: Map<string, any[]>
constructor() {
this._eventMap = new Map()
}
emit(type: string, ...args: any) {
if (this._eventMap.has(type)) {
const cbs: any = this._eventMap.get(type)
for (const fn of cbs) {
fn.apply(this, args)
}
return true
} else {
return false
}
}
off(type: string, fn: any) {
if (type === undefined) {
this._eventMap.clear()
} else if (this._eventMap.has(type)) {
if (fn === undefined) {
this._eventMap.delete(type)
} else {
const cbs: any[] | undefined = this._eventMap.get(type)
if (cbs && cbs.length > 1) {
cbs.splice(cbs.indexOf(fn), 1)
} else {
this._eventMap.delete(type)
}
}
}
return this
}
on(type: string, fn: any) {
if (this._eventMap.has(type)) {
const cbs: any[] | undefined = this._eventMap.get(type)
cbs && cbs.push(fn)
} else {
this._eventMap.set(type, [fn])
}
return this
}
once(type: string, fn: any) {
this.on(type, (...args: any) => {
this.off(type, fn)
fn.apply(this, args)
})
return this
}
}
在业务代码中使用我们上面的方法类
- 侦听我们
node服务启动的对应端口号socketHandle可以放在我们按钮的点击事件上
// 调试长链接服务, 请 本地启动 src/sdk/server/io.js 服务进行调试, 感谢
const socket = new ProcessSocket({ socketUrl: 'http://127.0.0.1:23456' })
const socketHandle = () => {
socket.sendMessage('传输给后端的消息')
}
后端
- 我们前端可以自己本地启动一个服务, 使用
socket.io, 使用http启动一个端口号的服务, 期间要解决跨域问题, 使用io建立链接, 其回调函数中有一个参数是 socket , 我们就是用这个类 来侦听 和派发事件。 这里面onmy_event就是侦听前端sendMessage事件中的消息啦~ - 上代码
import { createServer } from 'http'
import { Server } from 'socket.io'
// 此文件如无法 在项目中 cd 进去使用的时候, 可自己搞一个
const connectList = ['']
const testRoomList = []
const httpServer = createServer()
const io = new Server(httpServer, {
cors: {
//解决跨域问题
origin: '*',
methods: ['GET', 'POST'],
},
})
let numUsers = 0
connectList.length = 0
testRoomList.length = 0
io.on('connection', (socket) => {
// ...
console.log('cconnection---->' + socket.id)
socket.on('disconnect', () => {
// ...
console.log('断开连接---->')
let index = connectList.indexOf(socket.id)
connectList.splice(index, index + 1)
let index1 = testRoomList.indexOf(socket.id)
connectList.splice(index1, index1 + 1)
})
// 收到消息 , 参数就是前端传过来了, 1,2,3 这样排列,回调函数可以放在任意位置, cb 消息
socket.on('my_event', (res) => {
console.log('收到消息', res)
})
socket.on('leave', (arg) => {
// console.log(`socket ${arg.id} has leave room ${arg.room}`);
console.log('leave-room --->' + arg) // world
socket.leave(arg)
io.to(arg).emit(
'leavecall',
`socket ${socket.username} 离开了房间: ${arg}`,
)
})
socket.on('hello', (arg) => {
console.log('on hello --->' + arg) // world
socket.emit('server-back', {
message: 'server give you a msg',
})
})
// when the client emits 'add user', this listens and executes
socket.on('do login', (user_name) => {
let index = connectList.indexOf(socket.id)
console.log('index---->' + index)
if (index > -1) {
let str = user_name + '已经登录---->'
console.log(str)
socket.emit('login', {
isLogin: true,
numUsers: numUsers,
username: user_name,
message: str,
})
} else {
console.log('登陆成功---->')
connectList.push(socket.id)
socket.username = user_name
++numUsers
socket.emit('login', {
isLogin: true,
numUsers: numUsers,
username: user_name,
message: '登陆成功',
})
}
})
// when the client emits 'add user', this listens and executes
socket.on('join test room', (user_name) => {
socket.username = user_name
let index = testRoomList.indexOf(socket.id)
console.log('index---->' + index)
if (index > -1) {
let str = user_name + '已经加入房间---->'
console.log(str)
socket.emit('user joined', str)
} else {
let str = '欢迎 ' + user_name + '加入---->'
console.log(str)
testRoomList.push(socket.id)
socket.join('testroom')
io.to('testroom').emit('user joined', str)
if (user_name === '王二麻子') {
socket.join('test1')
socket.join('test12')
} else {
socket.join('test3')
socket.join('test4')
}
}
})
socket.on('obj message', (arg, callback) => {
console.log('obj message --->' + JSON.stringify(arg)) // world
callback('1111111111111111111111111111111')
})
socket.on('obj callback', (arg, callback) => {
console.log('obj callback --->' + JSON.stringify(arg)) // world
callback({
name: 'server',
data: 'with params callback',
})
})
socket.on('new message', (arg) => {
console.log('new message --->' + JSON.stringify(arg)) // world
socket.emit('new message', {
username: socket.username,
message: arg + '的服务器回复',
})
if (testRoomList.length > 1) {
socket
.to(testRoomList[0])
.emit('private message', socket.username, arg + '的服务器回复')
}
})
socket.on('get message', (arg, callback) => {
console.log('get message --->' + arg) // world
// io.to("testroom").emit("msg", {
// message: arg + "的服务器回复"
// });
callback(arg + '的服务器回复')
})
socket.emit('send', 1, '2', { 3: '4', 5: Buffer.from([6]) })
io.sockets.emit('broadcast', {
message: 'this is a broadcast msg',
})
socket.on('compress', (arg) => {
console.log('compress --->' + arg) // world
socket.compress(true).emit('compress', 'this is a compress msg')
})
socket.on('my-event', (arg, callback) => {
console.log('my-event --->' + arg) // world
setTimeout(() => {
callback({
message: 'this is a timeout msg',
})
}, 1000)
// socket.emit('my-event-back','this is a timeout msg')
})
socket.on('update item', (arg1, arg2, callback) => {
console.log(arg1) // 1
console.log(arg2) // { name: "updated" }
callback({
status: 'ok',
})
})
io.local.emit('localmsg', {
message: 'this is local msg',
})
socket.emit('new message', '测试删除所以')
socket.emit('msg', '测试单独string回复')
setTimeout(() => {
socket.emit('connect_msg', { msg: '发送消息给客户端111' })
}, 20000)
})
httpServer.listen(23456, function () {
console.log('client listening.....')
})
大家有问题可以留言,工作日24h以内就会回复的。