nodejs源码分析之connect
今天我们来分析connect函数。connect是发起tcp连接的api。本质上是对底层tcp协议connect函数的封装。我们看一下nodejs里做了什么事情。我们首先看一下connect函数的入口定义。
// connect(options, [cb])
// connect(port, [host], [cb])
// connect(path, [cb]);
// 对socket connect的封装
function connect(...args) {
// 处理参数
var normalized = normalizeArgs(args);
var options = normalized[0];
// 申请一个socket表示一个连接
var socket = new Socket(options);
// 设置连接超时时间
if (options.timeout) {
socket.setTimeout(options.timeout);
}
// 调用socket的connect
return Socket.prototype.connect.call(socket, normalized);
}
从代码中可以发现,connect函数是对Socket对象的封装。Socket表示一个tcp连接。我们分成三部分分析。 1 new Socket 2 setTimeout 3 Socket的connect
1 new Socket 我们看看新建一个Socket对象,做了什么事情。
function Socket(options) {
if (!(this instanceof Socket)) return new Socket(options);
// 是否正在建立连接,即三次握手中
this.connecting = false;
// 触发close事件时,该字段标记是否由于错误导致了close事件
this._hadError = false;
// 对应的底层handle,比如tcp
this._handle = null;
// 定时器id
this[kTimeout] = null;
options = options || {};
// 双工
stream.Duplex.call(this, options);
// 还不能读写,先设置成false
// these will be set once there is a connection
this.readable = this.writable = false;
this.on('finish', onSocketFinish);
this.on('_socketEnd', onSocketEnd);
// 是否允许单工
this.allowHalfOpen = options && options.allowHalfOpen || false;
}
其实也没有做太多的事情,就是初始化一些属性。
2 setTimeout
Socket.prototype.setTimeout = function(msecs, callback) {
// 清除之前的,如果有的话
clearTimeout(this[kTimeout]);
// 0代表清除
if (msecs === 0) {
if (callback) {
this.removeListener('timeout', callback);
}
} else {
// 开启一个定时器,超时时间是msecs,超时回调是_onTimeout
this[kTimeout] = setUnrefTimeout(this._onTimeout.bind(this), msecs);
// 监听timeout事件,定时器超时时,底层会调用nodejs的回调,nodejs会调用用户的回调callback
if (callback) {
this.once('timeout', callback);
}
}
return this;
};
setTimeout做的事情就是设置一个超时时间,如果超时则执行回调,在回调里再触发用户传入的回调。我们看一下超时处理函数_onTimeout。
Socket.prototype._onTimeout = function() {
this.emit('timeout');
};
直接触发timeout函数,回调用户的函数。
3 connect函数
// 建立连接,即三次握手
Socket.prototype.connect = function(...args) {
let normalized;
/* 忽略参数处理 */
var options = normalized[0];
var cb = normalized[1];
if (this.write !== Socket.prototype.write)
this.write = Socket.prototype.write;
this._handle = new TCP(TCPConstants.SOCKET);
this._handle.onread = onread;
// 连接成功,执行的回调
if (cb !== null) {
this.once('connect', cb);
}
// 正在连接
this.connecting = true;
this.writable = true;
// 可能需要dns解析,解析成功再发起连接
lookupAndConnect(this, options);
return this;
};
connect 函数主要是三个逻辑 1 首先创建一个底层的handle,比如我们这里是tcp(对应tcp_wrap.cc的实现)。 2 设置一些回调 3 做dns解析(如果需要的话),然后发起三次握手。 我们不展开dns解析的逻辑,这个留给分析dns模块的时候。我们直接看dns解析成功(或者不需要dns)时的逻辑。
function internalConnect(
self,
// 需要连接的远端ip、端口
address,
port,
addressType,
// 用于和对端连接的本地ip、端口(如果不设置,则操作系统自己决定)
localAddress,
localPort) {
var err;
// 如果传了本地的地址或端口,则tcp连接中的源ip和端口就是传的,否则由操作系统自己选
if (localAddress || localPort) {
// ip v4
if (addressType === 4) {
localAddress = localAddress || '0.0.0.0';
// 绑定地址和端口到handle,类似设置handle对象的两个属性
err = self._handle.bind(localAddress, localPort);
} else if (addressType === 6) {
localAddress = localAddress || '::';
err = self._handle.bind6(localAddress, localPort);
}
// 绑定是否成功
err = checkBindError(err, localPort, self._handle);
if (err) {
const ex = exceptionWithHostPort(err, 'bind', localAddress, localPort);
self.destroy(ex);
return;
}
}
if (addressType === 6 || addressType === 4) {
// 新建一个请求对象,是一个c++对象
const req = new TCPConnectWrap();
// 设置一些列属性
req.oncomplete = afterConnect;
req.address = address;
req.port = port;
req.localAddress = localAddress;
req.localPort = localPort;
// 调用底层对应的函数
if (addressType === 4)
err = self._handle.connect(req, address, port);
else
err = self._handle.connect6(req, address, port);
}
// 非阻塞调用,可能在还没发起三次握手之前就报错了,而不是三次握手出错,这里进行出错处理
if (err) {
// 获取socket对应的底层ip端口信息
var sockname = self._getsockname();
var details;
if (sockname) {
details = sockname.address + ':' + sockname.port;
}
const ex = exceptionWithHostPort(err, 'connect', address, port, details);
self.destroy(ex);
}
}
这里的代码比较多,除了错误处理外,主要的逻辑是bind和connect。bind函数的逻辑很简单(即使是底层的bind),他就是在底层的一个对象上设置了两个字段的值。所以我们主要来分析connect。我们把关于connect的这段逻辑拎出来。
const req = new TCPConnectWrap();
// 设置一些列属性
req.oncomplete = afterConnect;
req.address = address;
req.port = port;
req.localAddress = localAddress;
req.localPort = localPort;
// 调用底层对应的函数
self._handle.connect(req, address, port);
void TCPWrap::Connect(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
TCPWrap* wrap;
ASSIGN_OR_RETURN_UNWRAP(&wrap,
args.Holder(),
args.GetReturnValue().Set(UV_EBADF));
CHECK(args[0]->IsObject());
CHECK(args[1]->IsString());
CHECK(args[2]->IsUint32());
Local<Object> req_wrap_obj = args[0].As<Object>();
// 要连接的ip和端口
node::Utf8Value ip_address(env->isolate(), args[1]);
int port = args[2]->Uint32Value();
sockaddr_in addr;
int err = uv_ip4_addr(*ip_address, port, &addr);
if (err == 0) {
// 新建一个request代表本次的connect操作
ConnectWrap* req_wrap = new ConnectWrap(env,
req_wrap_obj,
AsyncWrap::PROVIDER_TCPCONNECTWRAP);
err = uv_tcp_connect(req_wrap->req(),
&wrap->handle_,
reinterpret_cast<const sockaddr*>(&addr),
AfterConnect);
req_wrap->Dispatched();
if (err)
delete req_wrap;
}
args.GetReturnValue().Set(err);
}
我们不深入底层分析connect函数的实现,有兴趣的可以参考之前的一些文章。这里主要是申请一个request对象,然后针对该handle,进行connect操作(libuv中的handle和request)。因为是非阻塞式调用,所以设置了回调AfterConnect。假设连接建立,这时候就会执行AfterConnect。AfterConnect就会执行js层的oncomplete函数,oncomplete函数指向的是afterConnect。
function afterConnect(status, handle, req, readable, writable) {
var self = handle.owner;
handle = self._handle;
self.connecting = false;
self._sockname = null;
// 连接成功
if (status === 0) {
self.readable = readable;
self.writable = writable;
self.emit('connect');
if (readable && !self.isPaused())
self.read(0);
}
}
// 错误处理
}
连接成功后js层调用了self.read(0)注册等待可读事件(可参考之前的文章 记一次nodejs问题排查)。至此,connect函数分析完毕。本文对connect函数进行了粗略的分析,如果有兴趣,欢迎交流。
更多阅读 1 记一次nodejs问题排查 2 nodejs源码分析之c++层的通用逻辑 3 libuv源码分析之stream第二篇 4 深入理解TCP/IP协议的实现之connect(基于linux1.2.13)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- codeforces 1256C (贪心+构造)
- codeforces 722C(带权并查集+反向思维)
- codeforces 1144D(思维)
- 经典的SparkSQL/Hive-SQL/MySQL面试-练习题
- codeforces 1249E(dp)
- Redis-KV数据库Java连接以及Jedis包的使用
- codeforces 1203D1(暴力)
- codeforces 1366B(线段相交)
- 一文搞懂Python自动化测试框架
- codeforces 1005D(数学)
- JSP开发简单实例演示
- Linux笔记【003】| Linux系统目录结构与基本命令
- codeforces1322A(括号匹配)
- codeforces 1296D(贪心)
- codeforces 1399D