继上一篇介绍了基于Nodejs的http服务和文件操作的内容后,本篇内容主要介绍前端工程师在日常工作中较少接触到的TCP相关知识内容,从Nodejs的TCP模块入手,通过实例看看TCP是怎么一回事。
tcp是什么?
tcp是一种面向连接的、可靠的、基于字节流的传输层通信协议,TCP层是位于IP层之上,应用层之下的中间层。与我们接触最频繁的http请求就是基于它,相比于http,它没有超时时间,正常情况下它可以一直保持连接。
tcp的特点
1.三次握手
很多人都知道TCP经典的”三次握手四次挥手“,如此繁琐的确认发包所带来了什么呢?
首先要明确三次握手到底要干什么。客户端要与服务器进行数据交换,但是服务器在云端,客户端也不知道服务器在不在线,所以要寻找一种方式核验一下远端的服务器在不在线,”三次握手“正是核验的方式。
再来看看步骤,先是客户机发起一个请求连接包,表明自己要连接到服务器上,然后服务器收到请求后,会回复一个请求,这个请求会做两件事,先要告诉远端的客户机你刚刚连了我的那步操作我收到了,还要确定自己也能连上远端的客户机。客户端接收到后,先知道了自己发过的第一步请求没问题,知道自己可以放心的给服务器发请求了,但是服务器却不知道能不能给客户端放心的发数据,所以客户端还要发起一次回答服务器的请求,这次请求的目的就是让服务器确定自己是可以连上客户端的。三次握手完成了,可以开心的发送数据了。
2.四次挥手
四次挥手我理解更多的意义是在于机器资源的释放。
来看看步骤,当客户端与服务端完成数据传输后,客户端发出请求包,表明我的数据传输完了,但是服务器并没有传输完成,所以会一边传输自己的数据一边给客户端确认收到结束的标志,从而释放自己与客户端的相关等待资源,然后服务端继续发自己未完成的数据,发送完成后,再次发送一个请求包,服务端的数据也发完了,客户端此时收到请求包后进行确认,客户端确认完成回复客户端,连接可断开,资源释放。
为什么更多的意义是一种资源释放的作用呢,如果两端把数据都发完了后均只发送一次包告诉对方数据完了,而不发送给对方确认包可以吗?我理解是可以的,但是为了保证发的第一次结束确认包能得到对方回复确实收到了而不是丢失,所以各自要多一次确认包,如果丢失了回传的确认包,则发起的一方不管是过去时候丢了还是回来的时候丢了都会重新发起确认,从而耗费资源。
Hello World入门
使用Nodejs的net模块来建立一个TCP服务器。
const net = require('net');
net.createServer(function(socket){
console.log('recive a connect');
console.log(socket);
}).listen(8000, function(){
console.log('TCPServer listen on 8000');
})
使用telnet发起TCP请求进行测试。
telnet 127.0.0.1 8000
#Trying 127.0.0.1...
#Connected to localhost.
#Escape character is '^]'.
telnet命令简介
telnet命令一般用来做远程登录,跟ssh命令一个作用,属于是TCP/IP协议族中的一员,是Internet远程登陆服务的标准协议和主要方式。但是为什么很少见甚至没听过telnet还能远程登录呢?是因为telnet采用明文传送报文,安全性不好,所以现在用的多且熟悉的远程登录都是通过ssh(security shell)来完成的。
而telnet命令还有一个非常强大的作用,用来确定远程服务端口是否开启可用,它的实质其实就是发起一个的数据包然后通过能否接收到回传的包来进行测试。
价值过亿的AI机器人核心代码
在上面tcp服务器的代码之上稍稍修改一下,一段价值过亿的AI机器人代码写好了。
// tcpServer.js
const net = require('net');
net.createServer(function(socket){
console.log('recive a connect');
/*
* @description 添加事件监听器,当client发送数据给服务器时,事件会触发
*/
socket.on('data', function (data) {
const message = data.toString().trim();
let response = `机器人:${message}`;
if (response.indexOf('?') > -1) {
response = String.prototype.slice.apply(response, [0, -2]) + '!';
}
// 过滤空消息
if (message) {
socket.write(response, function(){
console.log(`${response} has send!`);
})
}
});
}).listen(8000, function(){
console.log('TCPServer listen: 8000');
})
使用node tcpServer.js启动TCP服务器,进行输入内容测试。
这里因为在telnet命令下,这里输入中文会乱码,所以笔者使用nc命令进行测试,nc是一个更强大的网络工具命令,被称之为网络工具界的”瑞士军刀“,这里只用了简单的探测功能,笔者之前使用过它做端口扫描与文件传输,强大到令人惊艳,后续有机会专门介绍一下这个命令,没有安装nc的可以先安装一下,当然如果你的机器telnet下不乱码的话,也可以使用telenet进行测试。
实现一个简易版的“微信”
这里简单做一个类似于微信实时通讯工具,来看看 TCP链接下多客户端之间的通信是怎么做的。
服务器端代码
const net = require('net');
// 缓存在线的用户
const users = {};
// 创建TCP服务器
net.createServer(function(socket){
/* 发送数据 */
socket.write(JSON.stringify({
type: 'system',
message: '你已经成功连接了!'
}));
/* 监听data事件 */
socket.on('data',function(data){
const msg = JSON.parse(data.toString());
if (msg.type === 'registe') { // 注册用户,暂存socketj进全局变量users
users[msg.userId] = socket;
} else if(msg.type === 'singleMsg') { // 发送消息
if (users[msg.targetId]) { //首先查看全局变量里是否存在用户
users[msg.targetId].write(data, function(){
console.log(`${JSON.stringify(msg)} 已经被发送!`);
});
} else { //不存在则认为不在线
const resMsg = JSON.stringify({
type: 'error',
message: `发送失败了~,${msg.targetId}用户不在线!`
})
users[msg.userId].write(resMsg, function(){
console.log(`${JSON.stringify(resMsg)} 已经被发送!`);
});
}
}
})
}).listen(8000,function(){
console.log('TCPServer listen: 8000');
})
客户端代码:
/**
* 构建TCP客户端
*/
const client = require('net').Socket();
function genClient(userId) {
return new Promise(resolve => {
// 设置连接的服务器
client.connect(8000, '127.0.0.1', function () {
// 向服务器发送数据
client.write(JSON.stringify({"type": "registe", userId }));
resolve(client);
})
// 监听服务器传来的data数据
client.on('data', function (data) {
let msg;
try {
msg = JSON.parse(data.toString())
} catch(err) {
msg = data.toString();
}
const { type, userId, message } = msg;
// 判断是哪类消息,除了系统消息与错误消息均认为用户消息
if (type === 'error') {
console.log(message);
} else if (type === 'system') {
console.log(`系统消息:${message}`);
} else {
console.log(`${userId}用户说:${message}`);
}
})
})
}
module.exports = genClient;
const genClient = require('./tcpClient');
const userId = 'userId:100001';
genClient(userId).then(client => {
const msg = {
userId,
type: 'singleMsg',
targetId: 'userId:100002',
message: '你好'
};
// 用定时器模拟用户在发送消息
setInterval(()=>{
client.write(JSON.stringify(msg));
}, 3000)
})
代码运行结果:
这里模拟了两个用户之间的即时消息通讯,比较简单。简单说一下思路,当一个新用户来的时候,将其带来的userId作为主键,存进全局变量中,当有另一用户要发消息时,先从在线用户缓存之中查找其带来的接收方ID中是否存在,存在即代表在线,可以发送消息,否则告知用户,接收方不在线。