服务器之家

服务器之家 > 正文

深入理解Node.js的Inspector

时间:2021-08-05 22:33     来源/作者:编程杂技

深入理解Node.js的Inspector

前言:Node.js提供的Inspector不仅可以用来调试Node.js代码,还可以实时收集Node.js进程的内存,CPU等数据,同时支持静态、动态开启,是一个非常强大的工具,本文从使用和原理详细讲解Inspector。

Node.js的文档中对inspector的描述很少,但是如果深入探索,其实里面的内容还是挺多的。我们先看一下Inspector的使用。

1 Inspector的使用

 

1.1 本地调试

我们先从一个例子开始。下面是一个http服务器。

  1. const http = require('http'); 
  2. http.createServer((req, res) => { 
  3.     res.end('ok'); 
  4.  
  5. }).listen(80); 

然后我们以node --inspect httpServer.js的方式启动。我们可以看到以下输出。

  1. Debugger listening on ws://127.0.0.1:9229/fbbd9d8f-e088-48cc-b1e0-e16bfe58db44 
  2. For help, see: https://nodejs.org/en/docs/inspector 

9229端口是Node.js默认选择的端口,当然我们也可以自定义,具体可参考文档。这时候我们去浏览器打开开发者工具,菜单栏多了一个调试Node.js的按钮。

深入理解Node.js的Inspector

点击这个按钮。我们可以看到以下界面。

深入理解Node.js的Inspector

我们可以选择某一行代码打断点,比如我在第三行,这时候我们访问80端口,开发者工具就会停留在断点处。这时候我们可以看到一些执行上下文。

深入理解Node.js的Inspector

1.2 远程调试

但很多时候我们可能需要远程调试。比如我在一台云服务器上部署以上服务器代码。然后执行

  1. node --inspect=0.0.0.0:8888 httpServer.js 

不过这时候我们打开开发者工具就会发现按钮置灰或者找不到我们远程服务器的信息。这时候我们需要用另一种方式。通过在浏览器url输入框输入devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws={host}:{port}/{path}的方式(替换{}里面的内容为你执行Node.js时输出的信息),浏览器就会去连接你输入的地址,比如1.1.1.1:9229/abc。这种比较适合于对于通用的场景。

1.3 自动探测

如果是我们自己调试的话,这种方式看起来就有点麻烦,我们可以使用浏览器提供的自动探测功能。1 url输入框输入chrome://inspect/#devices我们会看到以下界面

深入理解Node.js的Inspector

2 点击configure按钮,在弹出的弹框里输入你远程服务器的地址

深入理解Node.js的Inspector

3 配置完毕后,我们会看到界面变成这样了,或者打开新的tab,我们看到开发者工具的调试按钮也变亮了。

深入理解Node.js的Inspector

4 这时候我们点击inspect按钮、Open dedicated DevTools for Node按钮或者打开新tab的开发者工具,就可以开始调试。而且还可以调试Node.js的原生js模块。

深入理解Node.js的Inspector
深入理解Node.js的Inspector

2 Inspector调试的原理

 

下面以通过url的方式调试(可以看到network),来看看调试的时候都发生了什么,浏览器和远程服务器建立连接后,是通过websocket协议通信的。

深入理解Node.js的Inspector

我们看一下这命令是什么意思,首先看Debugger.scriptParsed。

  • Debugger.scriptParsed # Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger.

从说明中我们看到,当V8解析脚本的时候就会触发这个事件,那就会告诉浏览器这个信息。

深入理解Node.js的Inspector

我们发现返回的都是一些元数据,没有脚本的具体代码内容,这时候浏览器会再次发起请求,

深入理解Node.js的Inspector

我们看到这个脚本的scriptId是103。所以请求里带了这个scriptId。对应的请求id是11。接着看一下响应。

深入理解Node.js的Inspector

至此,我们了解了获取脚本内容的过程,然后我们看看调试的时候是怎样的过程。当我们在浏览器上点击某一行设置断点的时候,浏览器就会发送一个请求。

深入理解Node.js的Inspector

这个命令的意义顾名思义,我们看一下具体定义。

  • Debugger.setBreakpointByUrl # Sets JavaScript breakpoint at given location specified either by URL or URL regex. Once this command is issued, all existing parsed scripts will have breakpoints resolved and returned in locations property. Further matching script parsing will result in subsequent breakpointResolved events issued. This logical breakpoint will survive page reloads.

接着服务返回响应。

深入理解Node.js的Inspector

这时候我们从另外一个tab访问80端口。服务器就会在我们设置的断点处停留,并且通知浏览器。

深入理解Node.js的Inspector

我们看一下这个命令的意思。

深入理解Node.js的Inspector

这个命令就是当服务器执行到断点时通知浏览器,并且返回执行的一些上下文,比如是哪个执行到哪个断点停留了。这时候浏览器侧也会停留在对应的地方,当我们hover某个变量时,就会看到对应的上下文。这些都是通过具体的命令获取的数据。就不一一分析了,可以参考具体文档。

深入理解Node.js的Inspector

3 Inspector的实现

 

大致了解了浏览器和服务器的交互过程和协议后,我们再来深入了解一下关于inspector的一些实现。当然这里不是分析V8中Inspector的实现,而是分析如何使用V8的Inspector以及Node.js中关于Inspector的实现部分。

3.1 开源实现

因为Node.js的实现比较复杂,这里先以一个简单版的调试工具源码来分析inspector的原理。我们先看一下初始化代码。

  1. inspector = std::unique_ptr<Inspector>(new Inspector(v8Platform, context, port)); 
  2. inspector->startAgent(); 

首先新建一个Inspector。然后启动它。接下来看看Inspector里的逻辑。

  1. Inspector::Inspector( 
  2.         const std::unique_ptr<v8::Platform> &platform, 
  3.         const v8::Local<v8::Context> &context, 
  4.         const int webSocketPort) { 
  5.  
  6.     context_ = context; 
  7.     // 新建一个websocket server用于和客户端通信 
  8.     websocket_server_ = std::unique_ptr<WebSocketServer>( 
  9.             new WebSocketServer( 
  10.                     webSocketPort, 
  11.                     // 收到客户的的消息后执行onMessage回调 
  12.                     std::bind(&Inspector::onMessage, this, std::placeholders::_1) 
  13.                 ) 
  14.             ); 
  15.     // 新建一个inspector client和V8通信 
  16.     inspector_client_ = std::unique_ptr<V8InspectorClientImpl>( 
  17.             new V8InspectorClientImpl( 
  18.                     platform, 
  19.                     context_, 
  20.                     // 收到V8的消息后调用sendMessage回复给客户的 
  21.                     std::bind(&Inspector::sendMessage, this, std::placeholders::_1), 
  22.                     std::bind(&Inspector::waitForFrontendMessage, this) 
  23.                 ) 
  24.             ); 
  25.  

代码看起来很复杂,不过我们不需要深究。主要是两个部分,一个是新建一个websocket服务器,一个是新建一个inspector客户端(用于和V8 Inspector通信),整体架构如下。

深入理解Node.js的Inspector

接下来分别看一下websocket服务器和inspector客户端的实现。首先看一下websocket服务器的构造函数。

  1. WebSocketServer::WebSocketServer(int port, std::function<void(std::string)> onMessage){ 
  2.     port_ = port; 
  3.     onMessage_ = std::move(onMessage); 
  4.  

WebSocketServer构造函数的实现很简单,只是初始化一些字段。接着看inspector客户端的实现。

  1. V8InspectorClientImpl:: V8InspectorClientImpl(const std::unique_ptr<v8::Platform> &platform, const v8::Local<v8::Context> &context, const std::function<void(std::string)> &onResponse, const std::function<int(void)> &onWaitFrontendMessageOnPause) { 
  2.  
  3.     platform_ = platform.get(); 
  4.     context_ = context; 
  5.     onWaitFrontendMessageOnPause_ = onWaitFrontendMessageOnPause; 
  6.     isolate_ = context_->GetIsolate(); 
  7.     // 创建一个channel和inspector通信,收到V8消息时会执行onResponse 
  8.     channel_.reset(new V8InspectorChannelImp(isolate_, onResponse)); 
  9.     // 新建一个V8提供的inspector 
  10.     inspector_ = v8_inspector::V8Inspector::create(isolate_, this); 
  11.     // 创建一个和inspector通信的session。 
  12.     session_ = inspector_->connect(kContextGroupId, channel_.get(), v8_inspector::StringView()); 
  13.     context_->SetAlignedPointerInEmbedderData(1, this); 
  14.     v8_inspector::StringView contextName = convertToStringView("inspector"); 
  15.     inspector_->contextCreated(v8_inspector::V8ContextInfo(context, kContextGroupId, contextName)); 
  16.     terminated_ = true
  17.     run_nested_loop_ = false
  18.  

上面代码很多,主要是根据V8提供的API来就行。这里主要有三个概念

1 V8Inspector是V8提供的类。

2 session表示和V8 inspector通信的会话。

3 channel用于和V8 inspector通信,从API来看,channel只能从V8获取数据,写入数据是另外的API。

这时候的架构如下

深入理解Node.js的Inspector

至此,websocket服务器和inspector客户端就分析完毕了,回到最开始的代码,初始化完毕后会执行startAgent。

  1. void Inspector::startAgent() { 
  2.     websocket_server_->run(); 
  3.  
  4.  
  5.  
  6.  
  7. // 启动websocket服务器 
  8.  
  9. void WebSocketServer::run() { 
  10.  
  11.     auto const address = net::ip::make_address("127.0.0.1"); 
  12.     net::io_context ioc{1}; 
  13.     tcp::acceptor acceptor{ioc, {address, static_cast<unsigned short>(port_)}}; 
  14.     tcp::socket socket{ioc}; 
  15.     acceptor.accept(socket); 
  16.     ws_ = std::unique_ptr<websocket::stream<tcp::socket>>(new websocket::stream<tcp::socket>(std::move(socket))); 
  17.     startListening(); 
  18.  
  19.  
  20.  
  21.  
  22. // 等待连接 
  23.  
  24. void WebSocketServer::startListening(){ 
  25.  
  26.    ws_->accept(); 
  27.    while (true) { 
  28.        waitFrontendMessage(); 
  29.    }}// 读取连接中的消息void WebSocketServer::waitFrontendMessage(){ 
  30.     beast::flat_buffer buffer; 
  31.     ws_->read(buffer); 
  32.     std::string message = boost::beast::buffers_to_string(buffer.data()); 
  33.     onMessage_(std::move(message)); 
  34.  

startAgent的逻辑就是启动websocket服务器。启动完毕后就等待客户的连接。连接成功后执行onMessage_。我们看一下onMessage的实现。

  1. void Inspector::onMessage(const std::string& message) { 
  2.     std::cout << "CDT message: " << message << std::endl; 
  3.     // StringView是V8要求的格式 
  4.     v8_inspector::StringView protocolMessage = convertToStringView(message); 
  5.     // 通知V8 Inspector 
  6.     inspector_client_->dispatchProtocolMessage(protocolMessage); 
  7.  

onMessage通过Inspector客户端把消息交给V8 Inspector处理。V8 Inspector处理完后,通过channel通知Inspector客户端,对应的函数是sendResponse。V8InspectorChannelImp是继承V8提供的Channel,sendResponse是一个纯虚函数,由V8InspectorChannelImp实现。

  1. void V8InspectorChannelImp::sendResponse(int callId, std::unique_ptr<v8_inspector::StringBuffer> message) { 
  2.     const std::string response = convertToString(isolate_, message->string()); 
  3.     onResponse_(response); 
  4.  

onResponse_是在Chnnel初始化时设置的,对应函数是inspector客户端的sendMessage。

  1. void Inspector::sendMessage(const std::string& message) { 
  2.     websocket_server_->sendMessage(message); 
  3.  

sendMessage通过websocket服务器把V8 Inspector返回的消息返回给客户的。至此,整个通信流程就完成了。

3.2 Node.js的实现(v14)

Node.js的实现非常复杂并且很绕,本文根据也无法通俗易懂地介绍和分析,只能按照我自己的思路大致讲解一下流程,有兴趣的同学可以自行阅读圆源码。

3.2.1 初始化

在Node.js初始化的时候,会创建一个inspector::Agent并启动。

  1. inspector_agent_ = std::make_unique<inspector::Agent>(this); 
  2. env->InitializeInspector({}); 

接着看InitializeInspector。

  1. inspector_agent_->Start(inspector_path, 
  2.                         options_->debug_options(), 
  3.                         inspector_host_port(), 
  4.                         is_main_thread()); 

Start中会执行StartIoThread进行进一步操作。

  1. bool Agent::StartIoThread() { 
  2.   io_ = InspectorIo::Start(client_->getThreadHandle(), 
  3.                            path_, 
  4.                            host_port_, 
  5.                            debug_options_.inspect_publish_uid); 
  6.   return true
  7.  

继续看InspectorIo::Start

  1. std::unique_ptr<InspectorIo> InspectorIo::Start( 
  2.     std::shared_ptr<MainThreadHandle> main_thread, 
  3.     const std::string& path, 
  4.     std::shared_ptr<HostPort> host_port, 
  5.     const InspectPublishUid& inspect_publish_uid) { 
  6.  
  7.   auto io = std::unique_ptr<InspectorIo>( 
  8.       new InspectorIo(main_thread, 
  9.                       path, 
  10.                       host_port, 
  11.                       inspect_publish_uid)); 
  12.   return io; 
  13.  

InspectorIo::Start其实是创建了一个InspectorIo对象,我们看一下构造函数里的逻辑。

  1. InspectorIo::InspectorIo(std::shared_ptr<MainThreadHandle> main_thread, 
  2.                          const std::string& path, 
  3.                          std::shared_ptr<HostPort> host_port, 
  4.                          const InspectPublishUid& inspect_publish_uid) 
  5.     : main_thread_(main_thread), 
  6.       host_port_(host_port), 
  7.       inspect_publish_uid_(inspect_publish_uid), 
  8.       thread_(), 
  9.       script_name_(path), 
  10.       id_(GenerateID()) { 
  11.   Mutex::ScopedLock scoped_lock(thread_start_lock_); 
  12.   CHECK_EQ(uv_thread_create(&thread_, InspectorIo::ThreadMain, this), 0); 
  13.   thread_start_condition_.Wait(scoped_lock); 
  14.  

构造函数里创建了一个线程并在线程中执行InspectorIo::ThreadMain。我们接着看ThreadMain。

  1. void InspectorIo::ThreadMain(void* io) { 
  2.   static_cast<InspectorIo*>(io)->ThreadMain(); 
  3.  
  4.  
  5.  
  6.  
  7. void InspectorIo::ThreadMain() { 
  8.  
  9.   uv_loop_t loop; 
  10.   loop.data = nullptr; 
  11.   int err = uv_loop_init(&loop); 
  12.   CHECK_EQ(err, 0); 
  13.   std::shared_ptr<RequestQueueData> queue(new RequestQueueData(&loop), 
  14.                                           RequestQueueData::CloseAndFree); 
  15.   std::string script_path = ScriptPath(&loop, script_name_); 
  16.   // 创建一个处理请求的delegate对象 
  17.   std::unique_ptr<InspectorIoDelegate> delegate( 
  18.       new InspectorIoDelegate(queue, main_thread_, id_, 
  19.                               script_path, script_name_)); 
  20.   // 创建一个服务器                             
  21.   InspectorSocketServer server(std::move(delegate), 
  22.                                &loop, 
  23.                                host_port_->host(), 
  24.                                host_port_->port(), 
  25.                                inspect_publish_uid_); 
  26.   request_queue_ = queue->handle(); 
  27.   // Its lifetime is now that of the server delegate 
  28.   queue.reset(); 
  29.   { 
  30.     Mutex::ScopedLock scoped_lock(thread_start_lock_); 
  31.     // 启动服务器 
  32.     if (server.Start()) { 
  33.       host_port_->set_port(server.Port()); 
  34.     } 
  35.     thread_start_condition_.Broadcast(scoped_lock); 
  36.   } 
  37.   // 进入事件循环 
  38.   uv_run(&loop, UV_RUN_DEFAULT); 
  39.   CheckedUvLoopClose(&loop); 
  40.  

我们分析一下服务器相关的逻辑。先看一下创建一个服务器的逻辑

  1. InspectorSocketServer::InspectorSocketServer( 
  2.     std::unique_ptr<SocketServerDelegate> delegate, uv_loop_t* loop, 
  3.     const std::string& host, int port, 
  4.     const InspectPublishUid& inspect_publish_uid, FILE* out
  5.     : loop_(loop), 
  6.       delegate_(std::move(delegate)), 
  7.       host_(host), 
  8.       port_(port), 
  9.       inspect_publish_uid_(inspect_publish_uid), 
  10.       next_session_id_(0), 
  11.       out_(out) { 
  12.   // 把服务器对象关联到delete对象中 
  13.   delegate_->AssignServer(this); 
  14.   // 状态为初始化 
  15.   state_ = ServerState::kNew; 
  16.  

接着看启动服务器的逻辑。

  1. bool InspectorSocketServer::Start() { 
  2.   std::unique_ptr<SocketServerDelegate> delegate_holder; 
  3.   // We will return it if startup is successful 
  4.   delegate_.swap(delegate_holder); 
  5.   struct addrinfo hints; 
  6.   memset(&hints, 0, sizeof(hints)); 
  7.   hints.ai_flags = AI_NUMERICSERV; 
  8.   hints.ai_socktype = SOCK_STREAM; 
  9.   uv_getaddrinfo_t req; 
  10.   const std::string port_string = std::to_string(port_); 
  11.   // 获取地址信息 
  12.   int err = uv_getaddrinfo(loop_, &req, nullptr, host_.c_str(), 
  13.                            port_string.c_str(), &hints); 
  14.   for (addrinfo* address = req.addrinfo; address != nullptr; 
  15.        address = address->ai_next) { 
  16.     // 真正创建一个服务器 
  17.     auto server_socket = ServerSocketPtr(new ServerSocket(this)); 
  18.     // 监听地址,服务器启动 
  19.     err = server_socket->Listen(address->ai_addr, loop_); 
  20.     if (err == 0) 
  21.       server_sockets_.push_back(std::move(server_socket)); 
  22.   } 
  23.   uv_freeaddrinfo(req.addrinfo); 
  24.   delegate_.swap(delegate_holder); 
  25.   state_ = ServerState::kRunning; 
  26.   // 打印提示,让用户知道连接到哪个url 
  27.   PrintDebuggerReadyMessage(host_, 
  28.                             server_sockets_, 
  29.                             delegate_->GetTargetIds(), 
  30.                             inspect_publish_uid_.console, 
  31.                             out_); 
  32.   return true
  33.  

启动服务器的本质就是监听某些IP+端口,所以我们接着分析一下ServerSocket对象的Listen函数。ServerSocket是对Libuv结构体uv_tcp_t的封装,表示一个基于TCP的socket。另外ServerSocket还保存了所属的InspectorSocketServer对象。

  1. int ServerSocket::Listen(sockaddr* addr, uv_loop_t* loop) { 
  2.   uv_tcp_t* server = &tcp_socket_; 
  3.   // 初始化Libuv handle 
  4.   CHECK_EQ(0, uv_tcp_init(loop, server)); 
  5.   // 在handle上绑定地址 
  6.   uv_tcp_bind(server, addr, 0); 
  7.   // 监听socket,监听成功后执行SocketConnectedCallback 
  8.   uv_listen(reinterpret_cast<uv_stream_t*>(server), 511, ServerSocket::SocketConnectedCallback); 
  9.   return err; 
  10.  

继续看SocketConnectedCallback

  1. void ServerSocket::SocketConnectedCallback(uv_stream_t* tcp_socket, 
  2.                                            int status) { 
  3.   // 监听成功                                         
  4.   if (status == 0) { 
  5.     // uv_tcp_t获取所属的ServerSocket对象 
  6.     ServerSocket* server_socket = ServerSocket::FromTcpSocket(tcp_socket); 
  7.     // Memory is freed when the socket closes. 
  8.     server_socket->server_->Accept(server_socket->port_, tcp_socket); 
  9.   } 
  10.  

接着看InspectorSocketServer::Accept。

  1. void InspectorSocketServer::Accept(int server_port, 
  2.                                    uv_stream_t* server_socket) { 
  3.   // 新建一个session表示一个连接对应的会话                                  
  4.   std::unique_ptr<SocketSession> session( 
  5.       new SocketSession(this, next_session_id_++, server_port)); 
  6.   // 申请一个Delegate处理连接中的数据 
  7.   InspectorSocket::DelegatePointer delegate = 
  8.       InspectorSocket::DelegatePointer( 
  9.           new SocketSession::Delegate(this, session->id())); 
  10.   // 摘取一个连接 
  11.   InspectorSocket::Pointer inspector = 
  12.       InspectorSocket::Accept(server_socket, std::move(delegate)); 
  13.   if (inspector) { 
  14.     // 设置session对应的InspectorSocket 
  15.     session->Own(std::move(inspector)); 
  16.     connected_sessions_[session->id()].second = std::move(session); 
  17.   } 
  18.  

接着看InspectorSocket::Accept

  1. InspectorSocket::Pointer InspectorSocket::Accept(uv_stream_t* server, 
  2.                                                  DelegatePointer delegate) { 
  3.   // 摘取一个TCP连接                                                
  4.   auto tcp = TcpHolder::Accept(server, std::move(delegate)); 
  5.   // 操作成功 
  6.   if (tcp) { 
  7.     // 新建一个InspectorSocket表示和客户的通信的对象 
  8.     InspectorSocket* inspector = new InspectorSocket(); 
  9.     // 设置处理连接数据的协议handler,即把数据当作HTTP协议处理 
  10.     inspector->SwitchProtocol(new HttpHandler(inspector, std::move(tcp))); 
  11.     return InspectorSocket::Pointer(inspector); 
  12.   } else { 
  13.     return InspectorSocket::Pointer(nullptr); 
  14.   } 
  15.  

接着看TcpHolder::Accept

  1. TcpHolder::Pointer TcpHolder::Accept( 
  2.     uv_stream_t* server, 
  3.     InspectorSocket::DelegatePointer delegate) { 
  4.   // 新建一个TcpHolder表示TCP连接 
  5.   TcpHolder* result = new TcpHolder(std::move(delegate)); 
  6.   uv_stream_t* tcp = reinterpret_cast<uv_stream_t*>(&result->tcp_); 
  7.   // 初始化 
  8.   uv_tcp_init(server->loop, &result->tcp_); 
  9.   // 这才是真正的摘取连接逻辑,uv_accept会把连接的fd保存到tcp结构体中 
  10.   uv_accept(server, tcp); 
  11.   // 设置等待可读事件,回调是OnDataReceivedCb 
  12.   uv_read_start(tcp, allocate_buffer, OnDataReceivedCb); 
  13.  

当数据到来时,我们看一下OnDataReceivedCb是怎么处理的。

  1. void TcpHolder::OnDataReceivedCb(uv_stream_t* tcp, ssize_t nread, 
  2.                                  const uv_buf_t* buf) { 
  3.   TcpHolder* holder = From(tcp); 
  4.   holder->ReclaimUvBuf(buf, nread); 
  5.   if (nread < 0 || nread == UV_EOF) { 
  6.     holder->handler_->OnEof(); 
  7.   } else { 
  8.     holder->handler_->OnData(&holder->buffer); 
  9.   } 
  10.  

OnDataReceivedCb会调用handler_的onData函数,handler_就是HttpHandler。

  1. void OnData(std::vector<char>* data) override { 
  2.     llhttp_errno_t err; 
  3.     // 解析http协议 
  4.     err = llhttp_execute(&parser_, data->data(), data->size()); 
  5.  
  6.     if (err == HPE_PAUSED_UPGRADE) { 
  7.       err = HPE_OK; 
  8.       llhttp_resume_after_upgrade(&parser_); 
  9.     } 
  10.     // 省略一些步骤,第一个请求是一个升级http协议到websocket协议的请求 
  11.     delegate()->OnSocketUpgrade(event.host, event.path, event.ws_key); 
  12.  } 

看看HttpHandler的delegate(ProtocolHandler是HttpHandler的基类)

  1. InspectorSocket::Delegate* ProtocolHandler::delegate() { 
  2.   return tcp_->delegate(); 
  3.  
  4.  
  5.  
  6. InspectorSocket::Delegate* TcpHolder::delegate() { 
  7.   return delegate_.get(); 
  8.  

获取的是TcpHolder的delegate。而TcpHolder的delegate是SocketSession::Delegate。即最后调用的是SocketSession::Delegate的OnSocketUpgrade函数。

  1. void SocketSession::Delegate::OnSocketUpgrade(const std::string& host, 
  2.                                               const std::string& path, 
  3.                                               const std::string& ws_key) { 
  4.   std::string id = path.empty() ? path : path.substr(1); 
  5.   server_->SessionStarted(session_id_, id, ws_key); 
  6.  

继续看server_->SessionStarted。

  1. void InspectorSocketServer::SessionStarted(int session_id, 
  2.                                            const std::string& id, 
  3.                                            const std::string& ws_key) { 
  4.   SocketSession* session = Session(session_id); 
  5.   connected_sessions_[session_id].first = id; 
  6.   // 处理wesocket和客户端的会话 
  7.   session->Accept(ws_key); 
  8.   // 开启一个服务器中agent和V8 inspector会话 
  9.   delegate_->StartSession(session_id, id); 
  10.  

server_->SessionStarted主要有三个逻辑

1 保存一个和客户端通信会话的关系

2 完成http协议到websocket协议的升级

3 新建一个和V8 Inspector的会话,Node.js类似一个代理。

我们先看如何处理websocket和客户端的会话

  1. void Accept(const std::string& ws_key) { 
  2.     ws_socket_->AcceptUpgrade(ws_key); 

ws_socket_是session对应的InspectorSocket对象。

  1. void InspectorSocket::AcceptUpgrade(const std::string& ws_key) { 
  2.   protocol_handler_->AcceptUpgrade(ws_key); 
  3.  

InspectorSocket::AcceptUpgrade根据当前处理协议的handler进一步处理,目前是HTTP协议。

  1. void AcceptUpgrade(const std::string& accept_key) override { 
  2.     char accept_string[ACCEPT_KEY_LENGTH]; 
  3.     generate_accept_string(accept_key, &accept_string); 
  4.     // 回复101 Switching Protocols给客户端说明同意协议升级 
  5.     const char accept_ws_prefix[] = "HTTP/1.1 101 Switching Protocols\r\n" 
  6.                                     "Upgrade: websocket\r\n" 
  7.                                     "Connection: Upgrade\r\n" 
  8.                                     "Sec-WebSocket-Accept: "
  9.     const char accept_ws_suffix[] = "\r\n\r\n"
  10.     std::vector<char> reply(accept_ws_prefix, 
  11.                             accept_ws_prefix + sizeof(accept_ws_prefix) - 1); 
  12.     reply.insert(reply.end(), accept_string, 
  13.                  accept_string + sizeof(accept_string)); 
  14.     reply.insert(reply.end(), accept_ws_suffix, 
  15.                  accept_ws_suffix + sizeof(accept_ws_suffix) - 1); 
  16.     if (WriteRaw(reply, WriteRequest::Cleanup) >= 0) { 
  17.        // 切换协议为websocket,协议升级成功后,后续的数据被当作websocket协议处理 
  18.       inspector_->SwitchProtocol(new WsHandler(inspector_, std::move(tcp_))); 
  19.     } else { 
  20.       tcp_.reset(); 
  21.     } 

AcceptUpgrade完成了协议升级,接着服务器还需要和V8 inspector建立一个会话。

  1. void InspectorIoDelegate::StartSession(int session_id, 
  2.                                        const std::string& target_id) { 
  3.   auto session = main_thread_->Connect
  4.       std::unique_ptr<InspectorSessionDelegate>( 
  5.           new IoSessionDelegate(request_queue_->handle(), session_id)), true); 
  6.   if (session) { 
  7.     sessions_[session_id] = std::move(session); 
  8.     fprintf(stderr, "Debugger attached.\n"); 
  9.   } 
  10.  

main_thread_是MainThreadHandle对象。

  1. std::unique_ptr<InspectorSession> MainThreadHandle::Connect
  2.     std::unique_ptr<InspectorSessionDelegate> delegate, 
  3.     bool prevent_shutdown) { 
  4.   return std::unique_ptr<InspectorSession>( 
  5.       new CrossThreadInspectorSession(++next_session_id_, 
  6.                                       shared_from_this(), 
  7.                                       std::move(delegate), 
  8.                                       prevent_shutdown)); 
  9.  

这里新建了一个CrossThreadInspectorSession对象。我们看看CrossThreadInspectorSessionde 构造函数。

  1. CrossThreadInspectorSession( 
  2.       int id, 
  3.       std::shared_ptr<MainThreadHandle> thread, 
  4.       std::unique_ptr<InspectorSessionDelegate> delegate, 
  5.       bool prevent_shutdown) 
  6.       : state_(thread, std::bind(MainThreadSessionState::Create
  7.                                  std::placeholders::_1, 
  8.                                  prevent_shutdown)) { 
  9.     state_.Call(&MainThreadSessionState::Connect, std::move(delegate)); 

构造函数中执行了MainThreadSessionState::Connect函数

  1. void Connect(std::unique_ptr<InspectorSessionDelegate> delegate) { 
  2.     Agent* agent = thread_->inspector_agent(); 
  3.     if (agent != nullptr) 
  4.       session_ = agent->Connect(std::move(delegate), prevent_shutdown_); 

Connect最后调用了agent的Connect。

  1. std::unique_ptr<InspectorSession> Agent::Connect
  2.     std::unique_ptr<InspectorSessionDelegate> delegate, 
  3.     bool prevent_shutdown) { 
  4.   CHECK_NOT_NULL(client_); 
  5.   int session_id = client_->connectFrontend(std::move(delegate), 
  6.                                             prevent_shutdown); 
  7.   return std::unique_ptr<InspectorSession>( 
  8.       new SameThreadInspectorSession(session_id, client_)); 
  9.  

继续看client_->connectFrontend

  1. int connectFrontend(std::unique_ptr<InspectorSessionDelegate> delegate, 
  2.                       bool prevent_shutdown) { 
  3.     int session_id = next_session_id_++; 
  4.     channels_[session_id] = std::make_unique<ChannelImpl>(env_, 
  5.                                                           client_, 
  6.                                                           getWorkerManager(), 
  7.                                                           std::move(delegate), 
  8.                                                           getThreadHandle(), 
  9.                                                           prevent_shutdown); 
  10.     return session_id; 

新建了一个ChannelImpl对象,ChannelImpl维护了一个和V8 Inspector的会话。

  1. explicit ChannelImpl(Environment* env, 
  2.                        const std::unique_ptr<V8Inspector>& inspector, 
  3.                        std::shared_ptr<WorkerManager> worker_manager, 
  4.                        std::unique_ptr<InspectorSessionDelegate> delegate, 
  5.                        std::shared_ptr<MainThreadHandle> main_thread_, 
  6.                        bool prevent_shutdown) 
  7.       : delegate_(std::move(delegate)), prevent_shutdown_(prevent_shutdown), 
  8.         retaining_context_(false) { 
  9.     /// 连接到V8 inspector获得一个session     
  10.     session_ = inspector->connect(CONTEXT_GROUP_ID, this, StringView()); 

至此就完成了连接建立,协议升级的处理。架构如下

深入理解Node.js的Inspector

3.2.2 客户端数据到V8 Inspector的处理流程

我们继续看业务数据的处理过程。我们刚才已经分析过,当连接上有数据到来时会执行OnDataReceivedCb。

  1. void TcpHolder::OnDataReceivedCb(uv_stream_t* tcp, ssize_t nread, 
  2.                                  const uv_buf_t* buf) { 
  3.   TcpHolder* holder = From(tcp); 
  4.   holder->ReclaimUvBuf(buf, nread); 
  5.   if (nread < 0 || nread == UV_EOF) { 
  6.     holder->handler_->OnEof(); 
  7.   } else { 
  8.     holder->handler_->OnData(&holder->buffer); 
  9.   } 
  10.  

这时候handler变成了websocket handler。

  1. void OnData(std::vector<char>* data) override { 
  2.     // 1. Parse. 
  3.     int processed = 0; 
  4.     do { 
  5.       // 解析数据 
  6.       processed = ParseWsFrames(*data); 
  7.       // 2. Fix the data size & length 
  8.       if (processed > 0) { 
  9.         remove_from_beginning(data, processed); 
  10.       } 
  11.     } while (processed > 0 && !data->empty()); 

我们看一下解析数据的逻辑。

  1. int ParseWsFrames(const std::vector<char>& buffer) { 
  2.     int bytes_consumed = 0; 
  3.     std::vector<charoutput
  4.     bool compressed = false
  5.     // 按照websocket协议解析数据 
  6.     ws_decode_result r =  decode_frame_hybi17(buffer, 
  7.                                               true /* client_frame */, 
  8.                                               &bytes_consumed, &output
  9.                                               &compressed); 
  10.     delegate()->OnWsFrame(output); 

delegate我们刚才已经分析过是SocketSession::Delegate。

  1. void SocketSession::Delegate::OnWsFrame(const std::vector<char>& data) { 
  2.   server_->MessageReceived(session_id_, std::string(data.data(), data.size())); 
  3.  

和处理协议升级时类似的逻辑。我们看server_->MessageReceived。

  1. void MessageReceived(int session_id, const std::string& message) { 
  2.     delegate_->MessageReceived(session_id, message); 
  3.   } 
  4.  
  5.  
  6.  
  7. void InspectorIoDelegate::MessageReceived(int session_id, 
  8.  
  9.                                           const std::string& message) { 
  10.   // 通过id找到对应的对象                                         
  11.   auto session = sessions_.find(session_id); 
  12.   if (session != sessions_.end()) 
  13.     session->second->Dispatch(Utf8ToStringView(message)->string()); 
  14.  

sessions_里维护的是MainThreadSessionState对象,我们看MainThreadSessionState的Dispatch。

  1. void Dispatch(std::unique_ptr<StringBuffer> message) { 
  2.     session_->Dispatch(message->string()); 

MainThreadSessionState对象里的session_是SameThreadInspectorSession对象。

  1. void SameThreadInspectorSession::Dispatch( 
  2.     const v8_inspector::StringView& message) { 
  3.   auto client = client_.lock(); 
  4.   if (client) 
  5.     client->dispatchMessageFromFrontend(session_id_, message); 
  6.  

继续看dispatchMessageFromFrontend

  1. void dispatchMessageFromFrontend(int session_id, const StringView& message) { 
  2.     channels_[session_id]->dispatchProtocolMessage(message); 

dispatchMessageFromFrontend通过session_id找到对应的channel,继续调用channel的dispatchProtocolMessage。

 

  1. void dispatchProtocolMessage(const StringView& message) { 
  2.     std::string raw_message = protocol::StringUtil::StringViewToUtf8(message); 
  3.     // 解析数据 
  4.     std::unique_ptr<protocol::DictionaryValue> value = 
  5.         protocol::DictionaryValue::cast(protocol::StringUtil::parseMessage( 
  6.             raw_message, false)); 
  7.     int call_id; 
  8.     std::string method; 
  9.     node_dispatcher_->parseCommand(value.get(), &call_id, &method); 
  10.     if (v8_inspector::V8InspectorSession::canDispatchMethod( 
  11.             Utf8ToStringView(method)->string())) { 
  12.       // 把数据传给V8 inspector 
  13.       session_->dispatchProtocolMessage(message); 
  14.     } else { 
  15.       node_dispatcher_->dispatch(call_id, method, std::move(value), 
  16.                                  raw_message); 
  17.     } 

这里通过canDispatchMethod判断请求的指令是否是给V8 inspector的。

  1. bool V8InspectorSession::canDispatchMethod(const StringView& method) { 
  2.   return stringViewStartsWith(method, 
  3.                               protocol::Runtime::Metainfo::commandPrefix) || 
  4.          stringViewStartsWith(method, 
  5.                               protocol::Debugger::Metainfo::commandPrefix) || 
  6.          stringViewStartsWith(method, 
  7.                               protocol::Profiler::Metainfo::commandPrefix) || 
  8.          stringViewStartsWith( 
  9.              method, protocol::HeapProfiler::Metainfo::commandPrefix) || 
  10.          stringViewStartsWith(method, 
  11.                               protocol::Console::Metainfo::commandPrefix) || 
  12.          stringViewStartsWith(method, 
  13.                               protocol::Schema::Metainfo::commandPrefix); 
  14.  

我们看到是通过前缀判断的,如果我们对文章开始的指令还有印象的话,就会知道大概有哪些指令。这就完成了数据从客户端到V8 inspector的传递。

3.2.3 V8 Inspector数据到客户端的处理流程

接着看从V8 inspector到客户端的数据传递逻辑。从前面分析我们知道V8 inspector通过channel的sendResponse函数传递输出的数据。

  1. void sendResponse( 
  2.       int callId, 
  3.       std::unique_ptr<v8_inspector::StringBuffer> message) override { 
  4.  
  5.     sendMessageToFrontend(message->string()); 
  6.   } 
  7.  
  8.  void sendMessageToFrontend(const StringView& message) { 
  9.     delegate_->SendMessageToFrontend(message); 
  10.  } 

delegate_是IoSessionDelegate对象。

  1. void SendMessageToFrontend(const v8_inspector::StringView& message) override { 
  2.     request_queue_->Post(id_, TransportAction::kSendMessage, 
  3.                          StringBuffer::create(message)); 

request_queue_是RequestQueueData对象。

  1. void Post(int session_id, 
  2.             TransportAction action
  3.             std::unique_ptr<StringBuffer> message) { 
  4.  
  5.     Mutex::ScopedLock scoped_lock(state_lock_); 
  6.     bool notify = messages_.empty(); 
  7.     messages_.emplace_back(action, session_id, std::move(message)); 
  8.     if (notify) { 
  9.       CHECK_EQ(0, uv_async_send(&async_)); 
  10.       incoming_message_cond_.Broadcast(scoped_lock); 
  11.     } 

Post首先把消息入队,然后通过异步的方式通知async_接着看async_的处理函数(在子线程的事件循环里执行)。

  1. int err = uv_async_init(loop, &async_, [](uv_async_t* async) { 
  2.       // 拿到async对应的上下文 
  3.       RequestQueueData* wrapper = node::ContainerOf(&RequestQueueData::async_, async); 
  4.       // 执行RequestQueueData的DoDispatch 
  5.       wrapper->DoDispatch(); 
  6.     });void DoDispatch() { 
  7.     for (const auto& request : GetMessages()) { 
  8.       request.Dispatch(server_); 
  9.     } 

request是RequestToServer对象。

  1. void Dispatch(InspectorSocketServer* server) const { 
  2.     switch (action_) { 
  3.       case TransportAction::kSendMessage: 
  4.         server->Send( 
  5.             session_id_, 
  6.             protocol::StringUtil::StringViewToUtf8(message_->string())); 
  7.         break; 
  8.     } 

接着看InspectorSocketServer的Send。

  1. void InspectorSocketServer::Send(int session_id, const std::string& message) { 
  2.   SocketSession* session = Session(session_id); 
  3.   if (session != nullptr) { 
  4.     session->Send(message); 
  5.   } 
  6.  

session代表可客户端的一个websocket连接。

  1. void SocketSession::Send(const std::string& message) { 
  2.   ws_socket_->Write(message.data(), message.length());} 

接着调用websocket handler的Write。

  1. void Write(const std::vector<char> data) override { 
  2.     std::vector<charoutput = encode_frame_hybi17(data); 
  3.     WriteRaw(output, WriteRequest::Cleanup); 

WriteRaw是基类ProtocolHandler实现的。

  1. int ProtocolHandler::WriteRaw(const std::vector<char>& buffer, 
  2.                               uv_write_cb write_cb) { 
  3.   return tcp_->WriteRaw(buffer, write_cb); 
  4.  

最终是通过TCP连接返回给客户端。

  1. int TcpHolder::WriteRaw(const std::vector<char>& buffer, uv_write_cb write_cb) { 
  2.   // Freed in write_request_cleanup 
  3.   WriteRequest* wr = new WriteRequest(handler_, buffer); 
  4.   uv_stream_t* stream = reinterpret_cast<uv_stream_t*>(&tcp_); 
  5.   int err = uv_write(&wr->req, stream, &wr->buf, 1, write_cb); 
  6.   if (err < 0) 
  7.     delete wr; 
  8.   return err < 0; 
  9.  

新建一个写请求,socket可写的时候发送数据给客户端。

4 动态开启Inspector

 

默认打开Inspector能力是不安全的,这意味着能连上websocket服务器的客户端都能通过协议控制Node.js进程,通常我们是在Node.js进程出现问题的时候,动态开启Inspector。

  1. const http = require('http');const inspector = require('inspector');const fs = require('fs'); 
  2.  
  3. http.createServer((req, res) => { 
  4.     if (req.url == 'debug') { 
  5.           const session = new inspector.Session(); 
  6.           session.connect(); 
  7.           session.post('Profiler.enable', () => { 
  8.           session.post('Profiler.start', () => { 
  9.             session.post('Profiler.stop', (err, { profile }) => { 
  10.               if (!err) { 
  11.                 fs.writeFileSync('./profile.cpuprofile', JSON.stringify(profile)); 
  12.               } 
  13.               session.disconnect(); 
  14.               res.end('ok'); 
  15.             }); 
  16.           }); 
  17.         }); 
  18.     } else { 
  19.         res.end('ok'); 
  20.     } 
  21.  
  22. }).listen(80); 

我们可以通过url参数控制Inspector的能力,本地调试时可以在vscode里可以直接看到数据。

深入理解Node.js的Inspector

5 收集数据

 

V8 inspector是一个非常强大的工具,调试只是它其中一个能力,他还可以获取内存、CPU等数据,具体能力请参考文档。

深入理解Node.js的Inspector

后记:Node.js的inspector是在Node.js额外线程里开启的一个非常强大的工具,通过Node.js作为中间人,完成客户端和V8 inspector的通信(调试、收集数据),是我们调试和诊断Node.js进程非常好的方式。

参考内容:

1 Debugging Guide

https://nodejs.org/en/docs/guides/debugging-getting-started

2 inspector

https://nodejs.org/dist/latest-v16.x/docs/api/inspector.html

3 开源的inspector agent实现

https://github.com/ahmadov/v8_inspector_example

4 inpector协议文档

https://chromedevtools.github.io/devtools-protocol/v8/Debugger/

5 Debugging Node.js with Chrome DevTools

https://medium.com/@paul_irish/debugging-node-js-nightlies-with-chrome-devtools-7c4a1b95ae27

原文链接:https://mp.weixin.qq.com/s/m9zHjRZXeiSSHYdk3Jg9uA

标签:

相关文章

热门资讯

yue是什么意思 网络流行语yue了是什么梗
yue是什么意思 网络流行语yue了是什么梗 2020-10-11
2020微信伤感网名听哭了 让对方看到心疼的伤感网名大全
2020微信伤感网名听哭了 让对方看到心疼的伤感网名大全 2019-12-26
背刺什么意思 网络词语背刺是什么梗
背刺什么意思 网络词语背刺是什么梗 2020-05-22
苹果12mini价格表官网报价 iPhone12mini全版本价格汇总
苹果12mini价格表官网报价 iPhone12mini全版本价格汇总 2020-11-13
2021德云社封箱演出完整版 2021年德云社封箱演出在线看
2021德云社封箱演出完整版 2021年德云社封箱演出在线看 2021-03-15
返回顶部