如何在Node.js与Express中无额外框架实现服务发现?
用Node.js + Express实现无额外框架的服务发现
服务发现的核心是注册中心管理服务实例的地址与状态,服务启动时注册、定期发心跳保活,客户端从注册中心查询可用服务。以下是具体实现步骤和代码示例:
实现步骤
- 搭建注册中心:用Express实现HTTP服务,维护服务注册表,提供注册、心跳、查询接口
- 服务注册逻辑:每个服务启动时向注册中心提交自身信息,定期发送心跳更新状态
- 服务发现逻辑:客户端向注册中心查询指定服务的可用实例,获取地址后发起调用
- 过期清理:注册中心定期移除超时未发心跳的服务实例
代码示例
1. 注册中心服务
const express = require('express'); const app = express(); app.use(express.json()); // 服务注册表:key为服务名,value为实例数组 const serviceRegistry = new Map(); // 注册服务接口 app.post('/register', (req, res) => { const { serviceName, serviceAddress } = req.body; if (!serviceName || !serviceAddress) { return res.status(400).json({ error: '缺少serviceName或serviceAddress参数' }); } // 生成唯一实例ID const serviceId = `${serviceName}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const serviceInstance = { id: serviceId, address: serviceAddress, lastHeartbeat: Date.now() }; serviceRegistry.has(serviceName) ? serviceRegistry.get(serviceName).push(serviceInstance) : serviceRegistry.set(serviceName, [serviceInstance]); res.status(200).json({ serviceId }); }); // 心跳更新接口 app.post('/heartbeat', (req, res) => { const { serviceId } = req.body; if (!serviceId) return res.status(400).json({ error: '缺少serviceId参数' }); let updated = false; for (const instances of serviceRegistry.values()) { const instance = instances.find(inst => inst.id === serviceId); if (instance) { instance.lastHeartbeat = Date.now(); updated = true; break; } } updated ? res.status(200).json({ message: '心跳更新成功' }) : res.status(404).json({ error: '服务实例未找到' }); }); // 查询可用服务实例接口 app.get('/discover/:serviceName', (req, res) => { const { serviceName } = req.params; if (!serviceRegistry.has(serviceName)) { return res.status(404).json({ error: '该服务无可用实例' }); } // 过滤超时实例(30秒未发心跳视为不可用) const validInstances = serviceRegistry.get(serviceName).filter(inst => { return Date.now() - inst.lastHeartbeat < 30000; }); if (validInstances.length === 0) { return res.status(404).json({ error: '该服务无可用实例' }); } // 随机选择一个实例(简单负载均衡) const randomInstance = validInstances[Math.floor(Math.random() * validInstances.length)]; res.status(200).json(randomInstance); }); // 定期清理过期服务(每10秒执行一次) setInterval(() => { const now = Date.now(); for (const [serviceName, instances] of serviceRegistry.entries()) { const validInstances = instances.filter(inst => now - inst.lastHeartbeat < 30000); if (validInstances.length === 0) { serviceRegistry.delete(serviceName); } else { serviceRegistry.set(serviceName, validInstances); } } }, 10000); const PORT = 3000; app.listen(PORT, () => { console.log(`注册中心运行在 http://localhost:${PORT}`); });
2. 服务实例(以用户服务为例)
const express = require('express'); const http = require('http'); // 用原生http模块替代第三方请求库 const app = express(); // 服务配置 const SERVICE_NAME = 'user-service'; const SERVICE_PORT = 3001; const REGISTRY_URL = 'http://localhost:3000'; let serviceId = null; // 业务接口 app.get('/api/users/:id', (req, res) => { res.json({ id: req.params.id, name: '张三', email: 'zhangsan@example.com' }); }); // 发送HTTP请求的工具函数(原生实现) function sendHttpRequest(options, data = null) { return new Promise((resolve, reject) => { const req = http.request(options, (res) => { let body = ''; res.on('data', chunk => body += chunk); res.on('end', () => { try { const response = JSON.parse(body); resolve({ data: response, status: res.statusCode }); } catch (err) { reject(err); } }); }); req.on('error', reject); if (data) { req.write(JSON.stringify(data)); } req.end(); }); } // 注册服务到注册中心 async function registerService() { try { const options = { hostname: 'localhost', port: 3000, path: '/register', method: 'POST', headers: { 'Content-Type': 'application/json' } }; const response = await sendHttpRequest(options, { serviceName: SERVICE_NAME, serviceAddress: `http://localhost:${SERVICE_PORT}` }); serviceId = response.data.serviceId; console.log(`服务注册成功,ID: ${serviceId}`); // 每15秒发送一次心跳 setInterval(sendHeartbeat, 15000); } catch (error) { console.error('服务注册失败:', error.message); // 注册失败5秒后重试 setTimeout(registerService, 5000); } } // 发送心跳 async function sendHeartbeat() { try { const options = { hostname: 'localhost', port: 3000, path: '/heartbeat', method: 'POST', headers: { 'Content-Type': 'application/json' } }; await sendHttpRequest(options, { serviceId }); } catch (error) { console.error('心跳发送失败:', error.message); } } // 启动服务并注册 app.listen(SERVICE_PORT, () => { console.log(`用户服务运行在 http://localhost:${SERVICE_PORT}`); registerService(); });
3. 客户端/调用方(以订单服务为例)
const express = require('express'); const http = require('http'); const app = express(); const REGISTRY_URL = 'http://localhost:3000'; // 原生HTTP请求工具函数 function sendHttpRequest(options, data = null) { return new Promise((resolve, reject) => { const req = http.request(options, (res) => { let body = ''; res.on('data', chunk => body += chunk); res.on('end', () => { try { const response = JSON.parse(body); resolve({ data: response, status: res.statusCode }); } catch (err) { reject(err); } }); }); req.on('error', reject); if (data) { req.write(JSON.stringify(data)); } req.end(); }); } // 订单业务接口,调用用户服务 app.get('/api/orders/:orderId', async (req, res) => { try { // 从注册中心获取用户服务实例 const discoverOptions = { hostname: 'localhost', port: 3000, path: '/discover/user-service', method: 'GET' }; const serviceRes = await sendHttpRequest(discoverOptions); const userServiceAddr = serviceRes.data.address; // 解析用户服务地址,拆分hostname和port const urlParts = new URL(userServiceAddr); const userServiceOptions = { hostname: urlParts.hostname, port: urlParts.port, path: '/api/users/1', method: 'GET' }; const userRes = await sendHttpRequest(userServiceOptions); res.json({ orderId: req.params.orderId, product: '智能手机', userInfo: userRes.data }); } catch (error) { res.status(500).json({ error: '服务调用失败', details: error.message }); } }); const PORT = 3002; app.listen(PORT, () => { console.log(`订单服务运行在 http://localhost:${PORT}`); });
扩展优化建议
- 持久化注册表:将服务信息存储到文件或数据库,避免注册中心重启后数据丢失
- 服务注销:添加注销接口,服务优雅关闭时主动通知注册中心移除自身实例
- 负载均衡策略:扩展轮询、权重分配等更复杂的实例选择逻辑
- 健康检查:除心跳外,可添加服务健康状态上报,更精准判断服务可用性
内容的提问来源于stack exchange,提问作者Jon Sud
相关产品推荐
相关产品推荐

