1. 程式人生 > 程式設計 >nodejs處理tcp連線的核心流程

nodejs處理tcp連線的核心流程

前幾天和一個小夥伴交流了一下nodejs中epoll和處理請求的一些知識,今天簡單來聊一下nodejs處理請求的邏輯。我們從listen函式開始。

int uv_tcp_listen(uv_tcp_t* tcp,int backlog,uv_connection_cb cb) {
 // 設定處理的請求的策略,見下面的分析
 if (single_accept == -1) {
  const char* val = getenv("UV_TCP_SINGLE_ACCEPT");
  single_accept = (val != NULL && atoi(val) != 0); /* Off by default. */
 }
 if (single_accept)
  tcp->flags |= UV_HANDLE_TCP_SINGLE_ACCEPT;
 // 執行bind或設定標記
 err = maybe_new_socket(tcp,AF_INET,flags);
 // 開始監聽
 if (listen(tcp->io_watcher.fd,backlog))
  return UV__ERR(errno);
 // 設定回撥
 tcp->connection_cb = cb;
 tcp->flags |= UV_HANDLE_BOUND;
 // 設定io觀察者的回撥,由epoll監聽到連線到來時執行
 tcp->io_watcher.cb = uv__server_io;
 // 插入觀察者佇列,這時候還沒有增加到epoll,poll io階段再遍歷觀察者佇列進行處理(epoll_ctl)
 uv__io_start(tcp->loop,&tcp->io_watcher,POLLIN);

 return 0;
}

我們看到,當我們createServer的時候,到Libuv層就是傳統的網路程式設計的邏輯。這時候我們的服務就啟動了。在polhttp://www.cppcns.coml io階段,我們的監聽型的檔案描述符和上下文(感興趣的事件、回撥等)就會註冊到epoll中。正常來說就阻塞在epoll。那麼這時候有一個tcp連線到來,會怎樣呢?epoll首先遍歷觸發了事件的fd,然後執行fd上下文中的回撥,即uvserver_io。我們看看uvserver_io。

void uv__server_io(uv_loop_t* loop,uv__io_t* w,unsigned int events) {
 // 迴圈處理,uv__stream_fd(stream)為伺服器對應的fd
 while (uv__stream_fd(stream) != -1) {
  // 通過accept拿到和客戶端通訊的fd,我們看到這個fd和伺服器的fd是不一樣的
  err = uv__accept(uv__stream_fd(stream));
  // uv__stream_fd(stream)對應的fd是非阻塞的,返回這個錯說明沒有連線可用accept了,直接返回
  if (err < 0) {
   if (err == UV_EAGAIN || err == UV__ERR(EWOULDBLOCK))
    return;
  }
  // 記錄下來
  stream->accepted_fd = err;
  // 執行回撥
  stream->connection_cb(stream,0);
  /*
   stream->accepted_fd為-1說明在回撥connection_cb裡已經消費了accepted_fd,
   否則先登出vBBYPaju
伺服器在epoll中的fd的讀事件,等待消費後再註冊,即不再處理請求了 */ if (stream->accepted_fd != -1) { uv__io_stop(loop,&stream->io_watcher,POLLIN); return; } /* ok,accepted_fd已經被消費了,我們是否還要繼續accept新的fd, 如果設定了UV_HANDLE_TCP_SINGLE_ACCEPT,表示每次只處理一個連線,然後 睡眠一會,給機會給其他程序accept(多程序架構時)。如果不是多程序架構,又設定這個, 就會導致處理連線被延遲了一下 */ if (stream->type == UV_TCP && (stream->flags & UV_HANDLE_TCP_SINGLE_ACCEPT)) { struct timespec timeout = { 0,1 }; nanosleep(&timeout,NULL); } } }

從uv__server_io,我們知道Libuv在一個迴圈中不斷accept新的fd,然後執行回撥,正常來說,回撥會消費fd,如此迴圈,直到沒有連線可處理了。接下來,我們重點看看回調裡是如何消費fd的,大量的迴圈會不會消耗過多時間導致Libuv的事件迴圈被阻塞一會。tcp的回撥是c++層的OnConnection。

// 有連線時觸發的回撥
template <typename WrapType,typename UVType>
void ConnectionWrap<WrapType,UVType>::OnConnection(uv_stream_t* handle,int status) {
 // 拿到Libuv結構體對應的c++層物件                          
 WrapType* wrap_data = static_cast<WrapType*>(handle->data);
 CHECK_EQ(&wrap_data->handle_,reinterpret_cast<UVType*>(handle));

 Environment* env = wrap_data->env();
 HandleScope handle_scope(env->isolate());
 Context::Scope context_scope(env->context());

 // 和客戶端通訊的物件
 Local<Value> client_handle;

 if (stathttp://www.cppcns.comus == 0) {
  // Instantiate the client javascript object and handle.
  // 新建一個js層使用物件
  Local<Object> client_obj;
  if (!WrapType::Instantiate(env,wrap_data,WrapType::SOCKET)
       .ToLocal(&client_obj))
   return;

  // Unwrap the client javascript object.
  WrapType* wrawww.cppcns.comp;
  // 把js層使用的物件client_ohttp://www.cppcns.combj所對應的c++層物件存到wrap中
  ASSIGN_OR_RETURN_UNWRAP(&wrap,client_obj);
  // 拿到對應的handle
  uv_stream_t* client = reinterpret_cast<uv_stream_t*>(&wrap->handle_);
  // 從handleaccpet到的fd中拿一個儲存到client,client就可以和客戶端通訊了
  if (uv_accept(handle,client))
   return;
  client_handle = client_obj;
 } else {
  client_handle = Undefined(env->isolate());
 }
 // 回撥js,client_handle相當於在js層執行new TCP
 Local<Value> argv[] = { Integer::New(env->isolate(),status),client_handle };
 wrap_data->MakeCallback(env->onconnection_string(),arraysize(argv),argv);
}

程式碼看起來很複雜,我們只需要關注uv_accept。uv_accept的引數,第一個是伺服器對應的handle,第二個是表示和客戶端通訊的物件。

int uv_accept(uv_stream_t* server,uv_stream_t* client) {
 int err;

 switch (client->type) {
  case UV_NAMED_PIPE:
  case UV_TCP:
   // 把fd設定到client中
   err = uv__stream_open(client,server->accepted_fd,UV_HANDLE_READABLE | UV_HANDLE_WRITABLE);
   break;
 // ...
 }

 client->flags |= UV_HANDLE_BOUND;
 // 標記已經消費了fd
 server->accepted_fd = -1;
 return err;
}

uv_accept主要就是兩個邏輯,把和客戶端通訊的fd設定到client中,並標記已經消費,從而驅動剛才講的while迴圈繼續執行。對於上層來說,就是拿到了一個和客戶端的物件,在Libuv層是結構體,在c++層是一個c++物件,在js層是一個js物件,他們三個是一層層封裝且關聯起來的,最核心的是Libuv的client結構體中的fd,這是和客戶端通訊的底層門票。最後回撥js層,那就是執行net.js的onconnection。onconnection又封裝了一個Socket物件用於表示和客戶端通訊,他持有c++層的物件,c++層物件又持有Libuv的結構體,Libuv結構體又持有fd。

const socket = new Socket({
  handle: clientHandle,allowHalfOpen: self.allowHalfOpen,pauseOnCreate: self.pauseOnConnect,readable: true,writable: true
 });

到此這篇關於nodejs處理tcp連線的核心流程的文章就介紹到這了,更多相關nodejs處理tcp連線內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!