本文会演示如何构建一个简单的UDP客户端与服务端应用,该应用是一个改进的股票监控器。服务端会发送有关幻想世界股票数据给客户端,客户端负责跟踪到达的消息是否为最近的。如果客户端检测到到达消息出现故障,仅仅丢弃消息(不对过期股票价格感兴趣)。
从服务端开始:
const dgram = require('dgram');
const server = dgram.createSocket('udp4');
const host = '0.0.0.0';
const port = 41100;
server.on('error', (err) => {
console.log(err.stack);
server.close();
});
server.on('message', (msg, rinfo) => {
console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`);
const reply = new Buffer('Got [' + msg + ']');
server.send(reply, 0, reply.length, rinfo.port, rinfo.address, (err, bytes) => {
if (err){
console.log(err.stack);
}
});
});
server.on('listening', () => {
const address = server.address();
console.log(`server listening ${address.address}:${address.port}`);
});
server.bind(port, host);
该代码片段说明了任何一个网络服务端应用所有重要的特征:绑定端口、接收消息、应答、应错。
既然已经有了服务端,为其构建一个客户端。
const dgram = require('dgram');
const client = dgram.createSocket('udp4');
const host = '0.0.0.0';
const port = 41100;
const message = new Buffer('Hello Server');
client.on('message', (message, remote) => {
console.log('Server: ' + message);
});
client.send(message, 0, message.length, port, host, (err, bytes) => {
if (err) {
throw err;
}
console.log('Message sent');
});
如果运行服务端与客户端,可以看到相互交换消息。现在将这段代码与典型TCP/IP服务端进行比较。注意到的第一件事情会是没有连接或者套接字的概念,从某个远程套接字接收消息,但是不能分辨出来该套接字是否仍然存活并处于监听之中。即使发送消息,也不能检查,除非客户端发送回应答或者心跳。
现在来做一个小实验:改变客户端端口号地址,尝试再次发送消息。会发现即使没有UDP服务端监听那个端口,也不会得到任何错误。底线是“如果想要得到任何额外的保证,比如:消息传送通知或者连接客户端列表,必须自己实现。
那是我们会在代码下一部分中所做的事情。为了给所有连接客户端发送股票数据,需要在服务端保存主机与端口号列表。不会进行检查它们是否仍然存活并处于监听之中,只是保存注册。
更新的服务端代码:
const dgram = require('dgram');
const server = dgram.createSocket('udp4');
const host = '0.0.0.0';
const port = 41100;
const clients = [];
server.on('error', (err) => {
console.log(err.stack);
server.close();
});
server.on('message', (msg, rinfo) => {
console.log(`Connected client at ${rinfo.address}:${rinfo.port}`);
clients.push(rinfo);
});
server.on('listening', () => {
const address = server.address();
console.log(`server listening ${address.address}:${address.port}`);
});
setInterval(() => {
const price = Math.floor(1000+ Math.random()*100);
const time = Date.now();
const data = new Buffer(price + ',' + time);
clients.forEach((rinfo) => {
server.send(data, 0, data.length, rinfo.port, rinfo.address, (err, bytes) => {
if (err){
console.log(err.stack);
}
});
})
}, 1000);
server.bind(port, host);
增加setInterval()方法每一秒发送随机股票价格数据,针对这个例子,设计了一个简单基于文本的协议。发送消息以逗号分割字段列表,第一个字段是股票价格,第二个字段是发送该价格时的服务端时间。
再次运行这个客户端与服务端,会发现客户端现在展示来自服务端的消息。通过使得客户端更智能一些完成这个简单的示例。客户端现在会解析数据,如果看到更接近的消息则会丢弃之前的价格。
const dgram = require('dgram');
const client = dgram.createSocket('udp4');
const host = '0.0.0.0';
const port = 41100;
const message = new Buffer('Hello Server');
const parseTick = (message) => {
const parts = message.toString().split(',').map((part) => +part);
return {
price: parts[0],
time: parts[1]
}
};
let latestTickTime = -1;
client.on('message', (message, remote) => {
const tick = parseTick(message);
if (tick.time > latestTickTime) {
console.log('Price is', tick.price);
latestTickTime = tick.time
} else {
console.log('Price is outdated, discard');
}
});
client.send(message, 0, message.length, port, host, (err, bytes) => {
if (err) {
throw err;
}
console.log('Message sent');
});
现在客户端解析消息,也会检查消息是否应该忽略,既然已经得到一个更接近的。