格物致知-記一次Nodejs源碼分析的經歷

2020-12-09 Nodejs開發

作者: theanarkh 來源:編程雜技

昨天分析http模塊相關的代碼時,遇到了一個晦澀的邏輯,看了想,想了看還是沒看懂。百度、谷歌了很多帖子也沒看到合適的答案。突然看到一個題目有點相似的搜索結果,點進去是Stack Overflow上的帖子,但是已經404,最後還是通過快照功能成功看到內容。這個帖子[1]和我的疑惑不相關,但是突然給了我一些靈感。沿著這個靈感去看了代碼,最後下載nodejs源碼,加了一些log,編譯了一夜(太久了,等不及編譯完成,得睡覺了)。上午起來驗證,終於解開了疑惑。這個問題源於下面這段代碼。

function connectionListenerInternal(server, socket) { socket.server = server; // 分配一個http解析器 const parser = parsers.alloc(); // 解析請求報文 parser.initialize( HTTPParser.REQUEST, new HTTPServerAsyncResource('HTTPINCOMINGMESSAGE', socket), server.maxHeaderSize || 0, server.insecureHTTPParser === undefined ? isLenient() : server.insecureHTTPParser, ); parser.socket = socket; // 開始解析頭部的開始時間 parser.parsingHeadersStart = nowDate(); socket.parser = parser; const state = { onData: null, onEnd: null, onClose: null, onDrain: null, // 同一tcp連接上,請求和響應的的隊列 outgoing: [], incoming: [], outgoingData: 0, keepAliveTimeoutSet: false }; state.onData = socketOnData.bind(undefined, server, socket, parser, state); socket.on('data', state.onData); if (socket._handle && socket._handle.isStreamBase && !socket._handle._consumed) { parser._consumed = true; socket._handle._consumed = true; parser.consume(socket._handle); } parser[kOnExecute] = onParserExecute.bind(undefined, server, socket, parser, state); socket._paused = false; }

這段代碼看起來很多,這是啟動http伺服器後,有新的tcp連接建立時執行的回調。問題在於tcp上有數據到來時,是怎麼處理的,上面代碼中nodejs監聽了socket的data事件,同時註冊了鉤子kOnExecute。data事件我們都知道是流上有數據到來時觸發的事件。我們看一下socketOnData做了什麼事情。

function socketOnData(server, socket, parser, state, d) { // 交給http解析器處理,返回已經解析的字節數 const ret = parser.execute(d); onParserExecuteCommon(server, socket, parser, state, ret, d); }

這看起來沒有問題,socket上有數據,然後交給http解析器處理。幾乎所有http模塊源碼解析的文章也是這樣分析的,我第一反應也覺得這個沒問題,那kOnExecute是做什麼的呢?kOnExecute鉤子函數的值是onParserExecute,這個看起來也是解析tcp上的數據的,看起來和onSocketData是一樣的作用,難道tcp上的數據有兩個消費者?我們看一下kOnExecute什麼時候被回調的。

void OnStreamRead(ssize_t nread, const uv_buf_t& buf) override { Local<Value> ret = Execute(buf.base, nread); Local<Value> cb = object()->Get(env()->context(), kOnExecute).ToLocalChecked(); MakeCallback(cb.As<Function>(), 1, &ret); }

在node_http_parser.cc中的OnStreamRead中被回調,那麼OnStreamRead又是什麼時候被回調的呢?OnStreamRead是nodejs中c++層流操作的通用函數,當流有數據的時候就會執行該回調。而且OnStreamRead中也會把數據交給http解析器解析。這看起來真的有兩個消費者?這就很奇怪,為什麼一份數據會交給http解析器處理兩次?這時候我的想法就是這兩個地方肯定是互斥的。但是我一直沒有找到是哪裡做了處理。最後在connectionListenerInternal的一段代碼中找到了答案。

if (socket._handle && socket._handle.isStreamBase && !socket._handle._consumed) { parser._consumed = true; socket._handle._consumed = true; parser.consume(socket._handle); }

因為tcp流是繼承StreamBase類的,所以if成立(後面會具體分析)。我們看一下consume的實現。

static void Consume(const FunctionCallbackInfo<Value>& args) { Parser* parser; ASSIGN_OR_RETURN_UNWRAP(&parser, args.Holder()); CHECK(args[0]->IsObject()); StreamBase* stream = StreamBase::FromObjject(args[0].As<Object>()); CHECK_NOT_NULL(stream); stream->PushStreamListener(parser); }

http解析器把自己註冊為tcp stream的一個listener。這裡涉及到了c++層對流的設計。我們從頭開始。看一下PushStreamListener做了什麼事情。c++層中,流的操作由類StreamResource進行了封裝。

class StreamResource { public: virtual ~StreamResource(); virtual int ReadStart() = 0; virtual int ReadStop() = 0; virtual int DoShutdown(ShutdownWrap* req_wrap) = 0; virtual int DoTryWrite(uv_buf_t** bufs, size_t* count); virtual int DoWrite(WriteWrap* w, uv_buf_t* bufs, size_t count, uv_stream_t* send_handle) = 0; void PushStreamListener(StreamListener* listener); void RemoveStreamListener(StreamListener* listener); protected: uv_buf_t EmitAlloc(size_t suggested_size); void EmitRead(ssize_t nread, const uv_buf_t& buf = uv_buf_init(nullptr, 0)); StreamListener* listener_ = nullptr; uint64_t bytes_read_ = 0; uint64_t bytes_written_ = 0; friend class StreamListener; };

我們看到StreamResource是一個基類,定義了操作流的公共方法。其中有一個成員是StreamListener類的實例。我們看看StreamListener的實現。

class StreamListener { public: virtual ~StreamListener(); virtual uv_buf_t OnStreamAlloc(size_t suggested_size) = 0; virtual void OnStreamRead(ssize_t nread, const uv_buf_t& buf) = 0; virtual void OnStreamDestroy() {} inline StreamResource* stream() { return stream_; } protected: void PassReadErrorToPreviousListener(ssize_t nread); StreamResource* stream_ = nullptr; StreamListener* previous_listener_ = nullptr; friend class StreamResource; };

StreamListener是一個負責消費流數據的類。StreamListener 和StreamResource類的關係如下。

null我們看到一個流可以註冊多個listener,多個listener形成一個鍊表。接著我們看一下創建一個c++層的tcp對象是怎樣的。下面是TCPWrap的繼承關係。

class TCPWrap : public ConnectionWrap<TCPWrap, uv_tcp_t>{} class ConnectionWrap : public LibuvStreamWrap{} class LibuvStreamWrap : public HandleWrap, public StreamBase{} class StreamBase : public StreamResource {}

我們看到tcp流是繼承於StreamResource的。新建一個tcp的c++的對象時(tcp_wrap.cc),會不斷往上調用父類的構造函數,其中在StreamBase中有一個關鍵的操作。

inline StreamBase::StreamBase(Environment* env) : env_(env) { PushStreamListener(&default_listener_); } EmitToJSStreamListener default_listener_;

StreamBase會默認給流註冊一個listener。我們看下EmitToJSStreamListener 具體的定義。

class ReportWritesToJSStreamListener : public StreamListener { public: void OnStreamAfterWrite(WriteWrap* w, int status) override; void OnStreamAfterShutdown(ShutdownWrap* w, int status) override; private: void OnStreamAfterReqFinished(StreamReq* req_wrap, int status); }; class EmitToJSStreamListener : public ReportWritesToJSStreamListener { public: uv_buf_t OnStreamAlloc(size_t suggested_size) override; void OnStreamRead(ssize_t nread, const uv_buf_t& buf) override; };

EmitToJSStreamListener繼承StreamListener ,定義了分配內存和讀取接收數據的函數。接著我們看一下PushStreamListener做了什麼事情。

inline void StreamResource::PushStreamListener(StreamListener* listener) { // 頭插法 listener->previous_listener_ = listener_; listener->stream_ = this; listener_ = listener; }

PushStreamListener就是構造出上圖的結構。對應到創建一個c++層的tcp對象中,如下圖。

然後我們看一下對於流來說,讀取數據的整個鏈路。首先是js層調用readStart

function tryReadStart(socket) { socket._handle.reading = true; const err = socket._handle.readStart(); if (err) socket.destroy(errnoException(err, 'read')); } // 註冊等待讀事件 Socket.prototype._read = function(n) { tryReadStart(this); };

我們看看readStart

int LibuvStreamWrap::ReadStart() { return uv_read_start(stream(), [](uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { static_cast<LibuvStreamWrap*>(handle->data)->OnUvAlloc(suggested_size, buf); }, [](uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) { static_cast<LibuvStreamWrap*>(stream->data)->OnUvRead(nread, buf); }); }

ReadStart調用libuv的uv_read_start註冊等待可讀事件,並且註冊了兩個回調函數OnUvAlloc和OnUvRead。

void LibuvStreamWrap::OnUvRead(ssize_t nread, const uv_buf_t* buf) { EmitRead(nread, *buf); } inline void StreamResource::EmitRead(ssize_t nread, const uv_buf_t& buf) { // bytes_read_表示已讀的字節數 if (nread > 0) bytes_read_ += static_cast<uint64_t>(nread); listener_->OnStreamRead(nread, buf); }

通過層層調用最後會調用listener_的OnStreamRead。我們看看tcp的OnStreamRead

void EmitToJSStreamListener::OnStreamRead(ssize_t nread, const uv_buf_t& buf_) { StreamBase* stream = static_cast<StreamBase*>(stream_); Environment* env = stream->stream_env(); HandleScope handle_scope(env->isolate()); Context::Scope context_scope(env->context()); AllocatedBuffer buf(env, buf_); stream->CallJSOnreadMethod(nread, buf.ToArrayBuffer()); }

繼續回調CallJSOnreadMethod

MaybeLocal<Value> StreamBase::CallJSOnreadMethod(ssize_t nread, Local<ArrayBuffer> ab, size_t offset, StreamBaseJSChecks checks) { Environment* env = env_; // ... AsyncWrap* wrap = GetAsyncWrap(); CHECK_NOT_NULL(wrap); Local<Value> onread = wrap->object()->GetInternalField(kOnReadFunctionField); CHECK(onread->IsFunction()); return wrap->MakeCallback(onread.As<Function>(), arraysize(argv), argv); }

CallJSOnreadMethod會回調js層的onread回調函數。onread會把數據push到流中,然後觸發data事件。這是tcp裡默認的數據讀取過程。而文章開頭講到的parser.consume打破了這個默認行為。stream->PushStreamListener(parser);修改了tcp流的listener鏈,http parser把自己作為數據的接收者。所以這時候tcp流上的數據是直接由node_http_parser.cc的OnStreamRead消費的。而不是觸發socket的data事件,最後通過在nodejs源碼中加log,重新編譯驗證的確如文中所述。最後提一個這個過程中還有一個關鍵的地方是調用consume函數的前提是socket._handle.isStreamBase為true。isStreamBase是在StreamBase::AddMethods中定義為true的,而tcp對象創建的過程中,調用了這個方法,所以tcp的isStreamBase是true,才會執行consume,才會執行kOnExecute回調。

相關焦點

  • 銀岡書院:致知格物,啟迪後賢
    「致知格物」盡顯鮮明特色的辦學方式作為清代的一個教育場所,在遵從統一教規基礎上,銀岡書院以「致知格物」為教育宗旨。「致知格物」原是書院創始人郝浴自身的修養準則和信條。創辦銀岡書院後,郝浴將自己原來的書室命名為「致知格物之堂」,並親筆提書與堂前。
  • 國學講堂(521):如何「格物」以「致知」?
  • 《大學》格物、致知、誠意、正心、修身、齊家、治國、平天下
    至善是指心靈獲得最大程度的自由,達到自然與事物發展相統一的境界。「明明德」和「親民」的一切方向是「止於至善」。以「止於至善」為方向或目標,等於是永無止境的期許。 八條目格物、致知、誠意、正心、修身、齊家、治國、平天下。
  • 仙家,出馬與出馬仙之九:出馬仙修身之格物致知
    格物、致知、正心、誠意然後可修身成。03今天,主要說一下格物致知。格物致知即通過學習和研究,正確地認知和了解出馬仙。我們做到了格物、致知,然後就需要我們正心、誠意地對待出馬這條路。
  • 格物、致知、誠意、正心、修身、齊家、治國、平天下!
    (《大學》)這段話後來被朱熹簡約為「八條目」:格物、致知、誠意、正心、修身、齊家、治國、平天下,特別是後四條人們更是耳熟能詳。這段話述說了認識的過程和認識的功效,而整個認識過程的基礎是「格物」。朱熹對「物格而後知至」有這樣的解釋:蓋人心之靈莫不有知,而天下之物莫不有理,惟於理有未窮,故其知有不盡也。
  • 學習知行合一,把格物致知的領悟,推向更深層次,提升認知
    其中有一段是陽明先生,告訴門生們一個道理,一個關于格物的道理。在這之前,我們先說說我們對格物的認知,我們多數人會覺得格物——就是把事物的特性,細節,特質等等了解的清清楚楚。所以這個是我們現在認為的「格物」。接著說陽明先生說他年輕的時候也是這樣的認為格物的。他越朋友討論格物致知,要格盡天下萬物的道理,要窮盡萬物的變化。所以他自己去格物——一處亭子前面的竹子,他堅持格這片竹子七天,最終思緒枯竭,道理不明,傷到精神,得了場病。
  • 從認知心理學問題解決的角度談「格物」
    認知心理學和格物有什麼聯繫呢?首先我們來看看什麼是認知心理學的問題解決。八目是:格物、致知、誠意、正心、修身、齊家、治國、平天下。其中的格物是基石。但是如何格物呢?古人沒有交代,也有的說,格物是古人研究物之理的方法,這個方法已經失傳。幸運的是,在《大學章句集注》中有這麼一段文字,為格物提供了一個大致的方法。
  • 「格物」是什麼意思?有三個人說透了此事
    在對待意識和物質關係問題上,孔子與他的門徒用一句話說明了問題:致知在格物,物格而後知至。你如今再怎麼翻譯,也只能是粗略體會,如果想找原汁原味的格物,只能到春秋諸子百家典籍中找。在《禮記》中,格物、致知、誠意、正心是重要的四目,另外的四目就是修身、齊家、治國、平天下。顯然「格物」是認知的重要方式,可後來人卻格來格去,總想搞出新的意境來。
  • 「格物」有三重境界,掌握這3個訣竅,才是真正的格物致知
    格物致知,是中華文化的一個重要傳承,最早出自《禮記大學》:「古之欲明明德於天下者,先治其國;欲治其國者,先齊其家;欲齊其家者,先修其身;欲修其身者,先正其心;欲正其心者,先誠其意;欲誠其意者,先致其知;致知在格物。
  • 王陽明:格物只在心上做,真理何必外物尋?
    來源:正心正舉公眾號文/月月 正心正舉特約作者「格物」,是中國古代認識論的重要命題,最早見於儒家經典《大學》裡的「欲誠其意者,先致其知,致知在格物」。唐代孔穎達認為「格物」是「事物之來發生,隨人所知習性喜好」,宋司馬光認為「格物」是「抵禦外物誘惑,而後知曉德行至道」,然而觀點最具代表性的,對後世影響最深的,當屬朱熹和王陽明。1.格物,到底要向外還是向內?
  • 解字、深意、案例分析:王陽明的「格物致知」到底是什麼意思?
    首先,「格物致知」來自於《禮記 大學》:「欲正其心者,先誠其意;欲誠其意者,先致其知;致知在格物。」其次,王陽明認為自己只是把《禮記 大學》關于格物致知的本來意思表達出來。下面詳細解釋王陽明的「格物致知」是什麼意思。
  • 一切都是心,致知、誠意、正心指的都是心而已(心想生)
    知就是致知,意就是誠意,心就是正心。格物,致知,誠意,正心,先把格物拿掉,致知講的是心,你不管是知識還是說良知,它都是心對吧;誠意指的是什麼?心,你的意識狀態你的本體;正心更不用講了,它指的還是心。它講的是一個,都是一個東西的狀態理解嗎?它並不是說好像是個外在的東西把它分為二。
  • nodejs的調試debug
    nodejs也不例外。今天我們來詳細介紹一下如何調試nodejs程序。開啟nodejs的調試還記得之前我們講到的koa程序嗎?本文將會以一個簡單的koa服務端程序為例,來展開nodejs的調試。我們需要加上 –inspect 參數:node --inspect app.js上面的代碼將會開啟nodejs的調試功能。
  • 知錯能改的nodejs之父
    而隨之帶來的就是在2009年,伺服器端的node誕生了,而這個語言的創造者就是今天我們要介紹的主人公——nodejs之父-Ryan Dahl。Ryan Dahl出生在美國加利福尼亞洲的聖地牙哥。他的家庭環境很好,小的時候,他的父母就給他買了一臺Apple IIc電腦。
  • 陽明心學語錄 | 良知箴 (十二):無善無噁心之體,有善有惡意之動;知善知惡是良知,為善去惡是格物
    答案很簡單:第一,這「四句教」的第四句話「為善去惡是格物」,是陽明心學與程朱理學的分水嶺;第二,在程朱理學為天下人修身成聖指出的舊的路徑外,陽明心學開闢了一條新的道路。我們先來分析第一個答案。《大學》中列出「格物、致知、誠意、正心、修身、齊家、治國、平天下」的儒家修身八條目,這是陽明心學和程朱理學都認可的。兩派的根本區別在於,對「格物」的理解不同。
  • 兩個步驟讓你獲取任何微信小程序源碼!
    在這裡把我重新簡化好的,快速地獲取一個微信小程序源碼的方式記錄下來。先來想想一個很簡單的問題,小程序的源文件存放在哪?但是在微信伺服器上,普通用戶想要獲取到,肯定是十分困難的,有沒有別的辦法呢?node.js運行環境如果沒有安裝nodejs,請先安裝一下下載地址:nodejs.org/en/反編譯的腳本安卓模擬器(要求自帶root權限)我使用的是「夜神模擬器」,用來獲取小程序源文件下載地址:www.yeshen.com不用越獄,不用root,使用電腦端的安卓模擬器
  • 自宋以來,《大學》中提出「格物」或「格物致知」被作為為學之要
    本文由作者不設伏筆只為等你獨家原創,未經許可禁止轉載自宋以來,《大學》中提出的所謂「格物」或「格物致知」被作為為學之要旨而受到重視,陽明當然也不例外,但他與宋儒有所不同。因此,如果舍誠意而專以格物為事,那就如同培養灌溉而不植根一樣,徒耗精力而一事無成(參見《王文成公全書》卷8,《書王天宇卷》)。如此看來,陽明以誠意為《大學》之要並非沒有道理。陽明認為,誠意為《大學》之本,所以他反對以格致為先、誠意為後的朱子《大學》說,而改立《大學古本》。
  • 儒家《大學》格物的「物」到底指什麼?
    這一找不要緊,居然發現我們之所以沒有發展出自己的科技,跟《大學》裡」致知在格物。物格而後知至「有很大關係。因為格物致知的實踐在儒家的學子們那裡是天經地義的事,但是究竟如何」格「、格的什麼「物」,似乎並未說清楚。
  • 【愛國報國】「博學篤志,格物明德」校訓的由來
    當得知研究生院關於校訓討論的各種建議時,路院長做了深入思考並做了修改,提筆寫下了「博學篤志,格物明德」八個字。後來他見到我時曾說:至大盡微體現了追求科學的無窮盡的精神,這在「格物」中可以體現。「明德」強調了對高尚品德的要求,這也是教育的根本。路院長對「至大盡微」四個字的修改非常深刻,給我留下了難忘的印象。據我的淺見,「博學」源自《中庸》中「博學之,審問之,慎思之,明辨之,篤行之。」