构建TCP客户端
TCP协议位于IP协议的上一层,是引用最广泛的互联网传输协议之一,在其之上就是类似HTTP这样的应用层协议。TCP是一种面向连接的协议,即一个终端需要和另一个终端建立专门的连接。该连接为双向数据流,两个终端可以同时控制收或者发数据。
注意:
TCP写入数据的时候,不会收到对方发送来的确认收到信号,而底层的网络实现也许会随机的切包和路由数据包,所以如果出现网络错误或者内部错误,接收方也许只能收到一部分发送的信息。
就是说,TCP只能保证数据包被应用程序按顺序接收,而不能保证它们被全部接收。
但是,TCP并不是面向消息的,它只会提供一个连续的数据流。如果需要识别单独的消息,那么就必须实现一个帧协议,指定每个消息的其实位置和结束位置。
例如,HTTP实现了响应机制,并且请求和响应消息都被明确地制定了界线。
连接服务器
可以使用net模块连接TCP服务器:
const net = require('net');
const port = 1234;
// 还可主机名为第二个参数,为可选参数,默认为localhost
// 最后一个参数可以传入一个回调函数,但是这里不这样做,我采用第二种写法:监听连接事件
const connect = net.createConnection(port);
connect.on('connect', (e) => {
console.log("New Connection!");
})
发送和接收数据:
const net = require('net');
const port = 1234;
const connect = net.createConnection(port);
connect.on('connect', (e) => {
console.log("New Connection!");
// 可以写入一个缓冲区
connect.write("Hello Server!","utf-8");
})
connect.on('data', (data)=>{
console.log(data);
})
write方法的第二参数还可以接收一个字符串指定编码。
在上述变量之后还可以传入一个回调,但是并不会在服务器接收数据时被调用,而是只会在数据被写入网络的时候被调用。
如果没有指定流的编码格式,那么传入的数据默认为一个缓冲区,如果想要这个缓冲区在发送之前被编码,需要使用setEncoding
指定:
connect.setEncoding("base64");
终止连接
使用connect.end()
终止连接。
还可以这样:
connect.end("Goodbye~",'utf-8');
这将会以指定字符集发送数据(缓冲区或字符串),并且在数据发送完毕之后关闭连接。
注意:
当终止连接的时候,实际上是将该操作放入队列之中,当队列清空之前连接是不会被关闭的。
所以,当终止连接之后,你还可以继续监听data事件并收到消息。
错误处理
通过监听error
事件来捕获错误
- error.message :错误信息
- error.code (字符串)错误码,不是http404那种错误码
创建命令行TCP客户端示例
连接服务器
启用之前学过并写过的TCP服务器,然后使用下面代码连接
const net = require('net');
const port = 1234;
const connect = net.createConnection(port);
connect.on('connect', () => {
console.log("Connect Success!");
})
connect.on('error', (err) => {
console.error("ERROR! \N" + err.message);
})
向服务器发送命令行
当Node进程启动的时候,就会准备好一个process.stdin
流来接收用户的键盘输入。这个可读流初始处于停止状态,只有使用resume
方法恢复之后才能发射data
事件。
process.stdin.resume();
现在,有了到服务器的可写流,也有来自用户的输入流,那么可是使用pipe方法将用户输入传送到可写流,写到服务器。
process.stdin.pipe(connect);
打印服务器消息
可以手动将服务器发送给进程标准输出流的消息都打印出来,可以这样做:
connect.pipe(process.stdout);
但是这样做有一个问题,pipe函数会在源流结束时终止目标流,即进程的标准输出流会在连接关闭之后关闭,不过,可以通过pipe的配置传参,让连接关闭之后不终止标准输出流(你不希望每次连接断开功能直接失败吧,就像QQ,就算用户网断了,但是用户输入并发送的消息都会进行缓存,然后尝试重连和发送,而不是当网络断掉之后直接就不让用户输入了)
connect.pipe(process.stdout,{
end: false
});
连接终止则重新连接
TCP连接也许会被远程服务器关闭,也许会因为网络波动关闭,甚至可能因为长时间没有活动,被超时机制关闭。如果你想关闭之后马上尝试重连,那么这样做:
const port = 1234;
const net = require('net');
const retryInterval = 3000; // 间隔三秒重试,稍定一下让环境稳定一点儿
const retriedTimes = 0; // 已经重连了多少次
const maxTrys = 10; // 最多重连十次
process.stdin.resume();
// 初始立即调用一次连接
(function connect() {
// 重连方法
function reconnect() {
if (retriedTimes >= maxTrys) {
throw new Error("I can't reconnect,sorry.");
}
retriedTimes++;
// 等到一段时间后重连,注意connect和reconnect
setTimeout(connect, retryInterval);
}
// 思考一下为什么在这儿实例化连接对象而不是一开始就实例化
const connect = net.createConnection(port);
// 监听连接成功
connect.on('connect', () => {
// 连接成功就把尝试连接次数清零
retriedTimes = 0;
console.log("Connect Sucess!");
})
// 如果出错了
connect.on('error', (err) => {
console.log("ERROR! \n" + err.message);
})
// 连接关闭之后尝试重连
connect.on('close', () => {
console.log("Reconnecting......");
reconnect();
})
// 把用户输入传递到服务器
process.stdin.pipe(connect, {
end: false
})
}());
关闭连接
connect.end();
如果你需要用户输入一个命令,例如quit
的时候断开连接,可以检测这个字符串:
process.stdin.on('data', (data)=>{
if (data.toString().trim().toLowerCase() == 'quit') {
connect.end();
process.stdin.pause();
}
})
这段代码有缺点,就是,你还是可以尝试向服务器发送数据,而不管这些数据是不是字符串quit
。向一个已经停止接收数据的连接发送数据肯定是不好的。
所以可以使用下面代码去替代:
// 原来if内部的代码替换成
connect.end();
process.stdin.end();// 删除连接
当然,关闭连接会触发重连机制,所以需要设置一个变量,表示这次退出是由用户操作的。
操作起来很简单,就是在手动退出的时候修改变量状态,而在监听关闭事件的时候使用判断该变量,如果是正常退出就跳过重连。
小结
TCP是面向连接的协议,可以为例按顺序处理消息和控制数据流。TCP是传输层的协议,可以基于它创建客户端-服务器协议。
在Node中构建TCP客户端很容易,当尝试与TCP服务器建立连接的时候,通过net.createConnection()
返回一个net.Socket
实例,他是一个双向流,可是通过该实例传输数据和接受数据。数据既可以是字符串,也可以是指定了编码或未指定编码的缓冲区。
在重连的时候,在多次重连之间需设置间隔时间,同时也需要设置最大尝试次数。