Node.js技術原理分析系列——Node.js調試能力分析

本文由體驗技術團隊屈金雄原創。

Node.js 是一個開源的、跨平臺的 JavaScript 運行時環境,它允許開發者在服務器端運行 JavaScript 代碼。Node.js 是基于 Chrome V8引擎構建的,專為高性能、高并發的網絡應用而設計,廣泛應用于構建服務器端應用程序、網絡應用、命令行工具等。

本系列將分為9篇文章為大家介紹 Node.js 技術原理:從調試能力分析到內置模塊新增,從性能分析工具 perf_hooks 的用法到 Chrome DevTools 的性能問題剖析,再到 ABI 穩定的理解、基于 V8 封裝 JavaScript 運行時、模塊加載方式探究、內置模塊外置以及 Node.js addon 的全面解讀等主題,每一篇都干貨滿滿。

本文內容為本系列第1篇,以下為正文內容。

inspector 是什么

直接取官方文檔中,對 inspector 的定義:

The node:inspector module provides an API for interacting with the V8 inspector.

翻譯過來就是,inspector 模塊提供了一組用于和 V8 inspector 交互的 API 。

解讀:

  • node inspector 是 Node.js 內置模塊
  • node inspector 僅提供與 V8 inspector 交互的能力,其本身并沒有調試能力
  • Node.js 調試能力來自 V8 inspector

Node.js 調試原理

調試的目的是通過觀察運行時數據來定位問題。Node.js 的運行時數據由 V8 引擎管理,為了實現調試功能,V8封裝了一套 api 供外部查看運行時數據,這套 api 名字就是 V8 inspector(運行時是一個 websocket 服務)。V8 inspector 由于調試協議不同,不能直接與 Chrome DevTools 交互,于是 Node.js 提供了 inspector 模塊,運行時也會啟動一個 websocket 服務,用于適配。

在這里插入圖片描述

如圖所示,進入 Node.js 調試模式前,主線程需要創建一個 debugger server( websocket 服務,即時通訊服務,也即 node inspector ),用來實現 debugger client(例如 vscode 調試器或 Chrome DevTools ) 與 V8 inspector 通信,V8 inspector 再獲取 Node.js 服務的數據,最終實現單步調試等功能。

經過封裝與簡化后,launch 模式啟動調試時我們甚至感知不到 debugger server 了,但是它一定是存在的。

深入分析 – inspect 參數

分析過程中,我對相關源碼做了粗讀,除了源碼本身,還參考了這篇文章:https://theanarkh.github.io/understand-nodejs/chapter24-Inspector/#11

本文使用的 Node.js 源碼是 18.20.2

在這里插入圖片描述

如上圖所示,表示的是 Node.js 調試模式啟動過程,大部分節點都是中文表述+函數名。

當我們用 node --inspect test.js 啟動一個 js 腳本時,程序會啟動 debugger server(一個 websocket 服務)。如上圖所示,相關邏輯都在初始化 inspector 部分(藍色節點),接下來細看一下這部分代碼。

下圖的起始節點 server.Start() 函數就是上圖的末端節點 server.Start()。
在這里插入圖片描述

圖中每一個節點都對應一個函數。無需理解所有節點,我們重點關注著色的幾個節點。

1.第一個藍色節點

當我們運行 node --inspect test.js 命令,可以看到如下打印,打印的內容 Node.js 開發者一定都很熟悉。

Debugger listening on ws://127.0.0.1:9229/43b86c7c-e538-4d5c-98ba-3f5196d8e986
For help, see: https://nodejs.org/en/docs/inspector// 這一行是Node開發者寫的業務代碼的打印
Server running at http://127.0.0.1:3000/

這個藍色節點已經是啟動代碼執行的最后一步了,第一個橙色節點之后的部分在處理連接請求,也就是說,當代碼走到第一個藍色節點時,已經成功啟動了一個 websocket 服務。

通過前面的代碼還能看出,這個 websocket 服務在新起的子線程上運行,正因如此,調試程序才可以在主線程出現異常而崩潰的情況下,記錄發送異常信息數據。

2.第一個橙色節點

注意這個節點代表一個回調函數,這個函數在服務啟動時并沒有執行。

它的執行是由 debugger client(例如 vscode 調試器或 Chrome DevTools )發起的 http 請求觸發的,這次是client發起的第一次請求。

這次請求,對 vscode 調試器來說,就是它的 attach 模式( launch模式是把啟動和連接操作合并了);對Chrome DevTools 來說,感覺上應該是通過輪詢連接的,這個點暫時就不再深入研究了。

3.第二個橙色節點

client 緊接著會發第二次請求(未確認),請求頭會攜帶 upgrade websocket 信息。這時會觸發第二個橙色節點處的回調函數,當識別到是升級請求時,debugger server 才真正升級為 websocket 服務。

4.第二個藍色節點

升級完成后,控制臺會打印“Debugger attached.”,這也是我們調試時常見的控制臺打印信息。

接下來,debugger server 就可以正常處理業務請求了。

5.特別關注一下紅色節點

這里的代碼就可以看出,debugger client 與 debugger server 建立連接的過程中,debugger server 與 V8 inspector 建立了連接。

其實整個初始化 inspector(啟動 debugger server )的過程,是一套完整 websocket 實現,可以作為一個整體來看待。早期 Socket.io 模塊是內置在 Node.js 中的。

– inspect-brk 參數

–inspect-brk 命令,可以在用戶代碼啟動前中斷,相當于在用戶代碼的第一行打了個斷點。

如下圖所示,我們用 node --inspect-brk test.js 命令啟動服務。可以看到,只有 debugger server 啟動成功的提示,沒有 node 服務啟動成功的提示。這是因為在執行用戶代碼前停住了。

這個命令在我們想要研究或調試 node 代碼啟動,又不知道研究對象啟動入口位置時,比較有用

在這里插入圖片描述

啟動 debugger server 后,我們用連接上,這時可以看到執行停在了業務代碼的第一行,而這一行我們并沒有設置斷點,如下圖所示。
在這里插入圖片描述

– inspect-wait 參數

這是 node 20 版本新增的啟動參數,用于等待調試器連接后再執行代碼。這樣就可以從執行一開始就開始調試。

Node.js 三種常見的調試方式

本節的介紹,沒有像其他網絡教程那樣手把手,step by step 地寫清楚操作步驟,是因為有講調試原理。

初學者理解本節的前提是先看懂調試原理。

一、vscode 調試

vscode 調試是 Node.js 開發者最推薦的調試方式,因為可以一鍵啟動調試模式,可以不用像 Chrome DevTools 調試那樣起額外的窗口。

vscode 調試 Node.js 分為 launch 和 attach 兩種模式,這里先介紹一下 launch 模式。

launch 模式調試 Node.js

1.創建一個 Node.js 服務,就是 test.js 文件,內容如下:

const { createServer } = require('node:http');const hostname = '127.0.0.1';
const port = 3000;const server = createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');res.end('Hello World');
});server.listen(port, hostname, async () => {console.log(`Server running at http://${hostname}:${port}/`);
});

2.在 Node.js 服務入口文件所在的目錄,點擊 create a launch.json file 按鈕,然后選擇 Node.js 選項(這是選擇調試器),vscode 會自動創建一個 launch.json 文件。

在這里插入圖片描述

launch.json 文件內容如下,本場景就直接用自動生成的不需要作任何改動。

configurations 下的 request 字段表示的就是我們前面提到的調試模式,默認使用 launch 模式;name 表示這一套配置的名稱,默認名稱是 Launch Program,下一步會用到該名稱;program 表示項目啟動的入口文件。更多調試配置參考 vscode官網

{// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [{"type": "node","request": "launch","name": "Launch Program","skipFiles": [// "<node_internals>/**"],"program": "${workspaceFolder}\test.js"}]
}

如果用如上文件,屏蔽12行,設置了不跳過內部代碼,還可以調試到Node.js的JS源碼。

  1. 如下圖所示,打斷點,并選擇 Launch Program,然后點擊綠色三角,啟動調試。

在這里插入圖片描述

attach 模式調試 Node.js

顧名思義,這個模式不會觸發 Node.js 和 debugger server 的啟動,只會作為 debugger client 附加到已經啟動的 Node.js 服務和debugger server 上。如果能理解前面講的調試原理,這里就很好理解了。

1.首先我們要運行 node --inspect test.js,這是在啟動 debugger server。

同時啟動的還有 node 服務,也就是圖中的 http://127.0.0.1:3000。

在這里插入圖片描述

2.在 launch.json 文件中添加 attach 模式配置

port 字段表示要連接到的 debugger server 的端口號,也就是我們上一步的 9229。

address 字段表示 debugger server 的地址,也就是上一步的 ws://127.0.0.1;debugger server 在本地的話,可以省略該配置。

{// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [{"type": "node","request": "launch","name": "Launch Program","skipFiles": ["<node_internals>/**"],"program": "${workspaceFolder}\test.js"},{"type": "node","request": "attach","name": "Attach Program","port": 9229,// "address": "localhost",},]
}

3.上一步添加完 Attach Program 配置后,多了一個啟動選項,如下圖所示。選擇 Attach Program,然后點擊綠色三角按鈕,啟動調試,即可連接上第一步啟動的 debugger server。

在這里插入圖片描述

遠程調試

attach模式本地調試不常用,通常用于遠程調試。目前vscode官方提供了兩種遠程調試方案:

  • 使用遠程開發擴展包(官方推薦)

這個方案不僅能遠程調試,還能用于遠程開發,比如我們有時可能需要在本地Windows機器上編輯遠程的Linux服務器上的項目。但是要安裝一系列插件,有些復雜

  • attach模式連接遠程 debugger server

在前文的attach模式配置中,加一個address字段,填上遠程debugger server的IP地址即可。

二、Chrome DevTools 調試

當我們理解了 Node.js 調試原理,Chrome DevTools 調試就變得手到擒來。

1.首先我們還是要運行 node --inspect test.js,這是在啟動 debugger server。

同時啟動的還有 node 服務,也就是圖中的 http://127.0.0.1:3000

在這里插入圖片描述

2.接下來再打開 chrome 控制臺,就能看見 Node.js 的圖標。注意沒有啟動 debugger server 的時候,圖標是不會出現的。點擊它,就可以打開 Chrome DevTools。

在這里插入圖片描述

3.首次打開 DevTools 時,需要在 Connection tab 頁配置準備連接的 debugger server。

可以看到 DevTools 自帶了兩個地址,如果是 debugger server 是在本地啟動,并且使用的是默認的 9229 端口,就不需要添加連接地址了。

在這里插入圖片描述

4.DevTools 連接 debugger server

之前有過介紹,vscode 連接 debugger server 需要用 attach 模式啟動調試。

DevTools 這邊在完成配置后,不需要任何操作就能自動連上 debugger server。(大概)是因為 DevTools 會輪詢所配置的地址。

連接成功后,會觸發node服務打印“Debugger attached.”提示語。

在這里插入圖片描述

5.開始調試

我們切到 Sources tab 頁,發現已經有了要調試的代碼。在需要的位置打斷點,再觸發斷點,即可調試。

例如下圖中,斷點位置的代碼是請求處理代碼,我們只需要訪問一下 http://127.0.0.1:3000 這個地址,即可觸發斷點。

在這里插入圖片描述

三、命令行調試

命令行調試是沒有客戶端之前的方式,現在一般不用,但是如果需要在不方便使用前面介紹的兩種方式的情況下,例如需要在服務器本地調試,可以用命令行調試。

我們把命令的雙橫杠去掉,也就是運行 node inspect test.js 命令,結果如下圖所示:

我們進入到了Node.js的命令行調試模式,具體的用法參考官方文檔的 Debug部分。

在這里插入圖片描述

接下來貼一些常用命令用法:

步進#
  • cont, c:繼續執行
  • next, n:下一步
  • step,s: 進入方法內部
  • out, o:退出當前方法
  • pause:暫停正在運行的代碼(類似開發者工具中的暫停按鈕)
斷點#
  • setBreakpoint(), sb():在當前行設置斷點
  • setBreakpoint(line), sb(line):在特定行設置斷點
  • setBreakpoint(‘fn()’), sb(…):在函數主體的第一個語句上設置斷點
  • setBreakpoint(‘script.js’, 1), sb(…):在第一行設置斷點 script.js
  • setBreakpoint(‘script.js’, 1, ‘num < 4’), sb(…):在第一行設置條件斷點script.js,僅當num < 4 計算結果為true
  • clearBreakpoint(‘script.js’, 1), cb(…):清除script.js 第一行的斷點
信息#
  • backtrace, bt:打印當前調用棧

  • list(5):列出腳本源代碼以及 5 行上下文(前后 5 行)

  • watch(expr):將表達式添加到觀察列表,注意表達式需用引號括起來,如watch(“test2”)

  • unwatch(expr):從觀察列表中刪除表達式

  • unwatch(index):從觀察列表中刪除特定索引處的表達式

  • watchers:列出所有觀察者及其值(每個斷點上自動列出)

  • repl:打開調試器的 repl,在調試腳本的上下文中查看數據或表達式

  • exec expr, p expr:在調試腳本的上下文中執行表達式并打印其值

  • profile:啟動 CPU profiling session

  • profileEnd:停止當前 CPU profiling session

  • profiles:列出所有已完成的 CPU profiling session

  • profiles[n].save(filepath = ‘node.cpuprofile’):將 CPU profiling session以 JSON 格式保存到磁盤

  • takeHeapSnapshot(filepath = ‘node.heapsnapshot’):獲取堆快照并以 JSON 格式保存到磁盤

注意一下,repl命令可以進入斷點所在上下文,方便地查看數據或表達式:

在這里插入圖片描述

inspector 模塊的 API

Node.js 的 inspector 模塊提供了一組 API,用于在運行時與 V8 引擎進行交互,調試和分析 Node.js 應用程序。

這些 API 使開發者可以通過編程方式啟動調試會話、設置斷點、執行調試命令、收集性能數據等。

在這里插入圖片描述

這里僅詳細介紹兩個方法:

1.inspector.Session 類的 post 方法可以向 v8 inspector 發送消息(指令),獲取各種信息。v8 inspector 能識別的指令可以在 Chrome DevTools Protocol 中查看

貼一份官網的示例,對該類的能力可見一斑。

import { Session } from'node:inspector/promises';
import fs from'node:fs';
const session = newSession();
session.connect();await session.post('Profiler.enable');
await session.post('Profiler.start');
// Invoke business logic under measurement here...// some time later...
const { profile } = await session.post('Profiler.stop');// Write profile to disk, upload, etc.
fs.writeFileSync('./profile.cpuprofile', JSON.stringify(profile));

2.inspector.open方法可以在節點啟動后,以編程方式啟動debugger server。

vscode有個通過進程id,attach到沒有用–inspect模式啟動的node服務的能力,大概就是通過該接口實現的,暫未確認。

下一節,我們將講解如何在Node.js中新增一個內置模塊,請大家持續關注本系列內容~學習完本系列,你將獲得:

  • 提升調試與性能優化能力
  • 深入理解模塊化與擴展機制
  • 探索底層技術與定制化能力

關于OpenTiny

歡迎加入 OpenTiny 開源社區。添加微信小助手:opentiny-official 一起參與交流前端技術~
OpenTiny 官網:https://opentiny.design/
OpenTiny 代碼倉庫:https://github.com/opentiny/
TinyVue 源碼:https://github.com/opentiny/tiny-vue
TinyEngine 源碼:?https://github.com/opentiny/tiny-engine
歡迎進入代碼倉庫 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI~ 如果你也想要共建,可以進入代碼倉庫,找到 good first issue標簽,一起參與開源貢獻~

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/70096.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/70096.shtml
英文地址,請注明出處:http://en.pswp.cn/web/70096.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

輕松搭建本地大語言模型(二)Open-WebUI安裝與使用

文章目錄 前置條件目標一、安裝 Open-WebUI使用 Docker 部署 二、使用 Open-WebUI&#xff08;一&#xff09;訪問Open-WebUI&#xff08;二&#xff09;注冊賬號&#xff08;三&#xff09;模型選擇&#xff08;四&#xff09;交互 四、常見問題&#xff08;一&#xff09;容器…

阿里云百煉通義大模型

阿里云百煉通義大模型 Part one&#xff08;阿里云百煉大模型&#xff09;一、什么是百煉&#xff08;一&#xff09;調用大模型 二、支持的大模型三、模型總覽四、為什么選擇百煉&#xff1f;五、開始使用百煉Part two一、開發參考二、模型調用&#xff08;一&#xff09;通義…

Golang學習筆記_33——橋接模式

Golang學習筆記_30——建造者模式 Golang學習筆記_31——原型模式 Golang學習筆記_32——適配器模式 文章目錄 橋接模式詳解一、橋接模式核心概念1. 定義2. 解決的問題3. 核心角色4. 類圖 二、橋接模式的特點三、適用場景1. 多維度變化2. 跨平臺開發3. 動態切換實現 四、與其他…

低代碼(Low Code)全解析:從概念到應用,從選擇到價值

?在數字化浪潮席卷全球的當下&#xff0c;企業對軟件開發的效率與靈活性愈發重視&#xff0c;低代碼平臺應運而生并迅速掀起技術熱潮。 本文基于筆者 6 年的低代碼實踐經驗&#xff0c;深入剖析低代碼的諸多方面&#xff0c;涵蓋其定義、發展歷程、國內平臺對比、開發流程、與…

函數重載講解

雖然在初識C-CSDN博客中介紹過&#xff0c;但還是感覺要單發出來大概講解下 什么是函數重載&#xff1f; 函數重載是指在同一個作用域內&#xff0c;函數名相同&#xff0c;但它們的 參數列表 不同。C 允許你根據函數的參數個數、類型或者順序的不同來定義多個同名函數。編譯…

14-H指數

給你一個整數數組 citations &#xff0c;其中 citations[i] 表示研究者的第 i 篇論文被引用的次數。計算并返回該研究者的 h 指數。 根據維基百科上 h 指數的定義&#xff1a;h 代表“高引用次數” &#xff0c;一名科研人員的 h 指數 是指他&#xff08;她&#xff09;至少發…

關于es6-module的語法

ES6&#xff08;ECMAScript 2015&#xff09;引入了模塊化的概念&#xff0c;旨在使 JavaScript 更加模塊化、可維護和可重用。ES6 模塊允許我們在不同的文件中組織和管理代碼&#xff0c;使得不同模塊之間的依賴關系更加清晰。 1. 導出&#xff08;Export&#xff09; 1.1 命…

Chrome多開終極形態解鎖!「窗口管理工具+IP隔離插件

Web3項目多開&#xff0c;繼ads指紋瀏覽器錢包被盜后&#xff0c;更多人采用原生chrome瀏覽器&#xff0c;當然對于新手&#xff0c;指紋瀏覽器每月成本也是一筆不小開支&#xff0c;今天逛Github發現了這樣一個解決方案&#xff0c;作者開發了窗口管理工具IP隔離插件&#xff…

DeepSeek核心算法解析:如何打造比肩ChatGPT的國產大模型

注&#xff1a;此文章內容均節選自充電了么創始人&#xff0c;CEO兼CTO陳敬雷老師的新書《自然語言處理原理與實戰》&#xff08;人工智能科學與技術叢書&#xff09;【陳敬雷編著】【清華大學出版社】 文章目錄 DeepSeek大模型技術系列一DeepSeek核心算法解析&#xff1a;如何…

arm 入坑筆記

1.開發環境&#xff08;IDE&#xff09;使用keil_5 (keil_mdk) 2.兩個手冊需要關注&#xff1a;用戶手冊&#xff08;編程需要&#xff09;&#xff0c;數據手冊&#xff08;硬件&#xff09; 3.32bit地址空間&#xff1a;0~2^324GB尋址空間及&#xff08;0-FFFF_FFFF&#x…

弱監督語義分割學習計劃(0)-計劃制定

經過與deepseek的一番討論和交流&#xff0c;DeepSeek為我設計了一個30天高強度學習計劃&#xff0c;重點聚焦弱監督/無監督語義分割在野外場景的應用&#xff0c;結合理論與實踐&#xff0c;并最終導向可落地的開源項目。以下是詳細計劃&#xff1a; 總體策略 優先級排序&…

vscode遠程報錯:Remote host key has changed,...

重裝了Ubuntu系統之后&#xff0c;由20.04改為22.04&#xff0c;再用vscode遠程&#xff0c;就出現了以上報錯。 親測有效的辦法 gedit ~/.ssh/known_hosts 打開這個配置文件 刪掉與之匹配的那一行&#xff0c;不知道刪哪一行的話&#xff0c;就打開第一行這個 /.ssh/confi…

Python - 爬蟲利器 - BeautifulSoup4常用 API

文章目錄 前言BeautifulSoup4 簡介主要特點&#xff1a;安裝方式: 常用 API1. 創建 BeautifulSoup 對象2. 查找標簽find(): 返回匹配的第一個元素find_all(): 返回所有匹配的元素列表select_one() & select(): CSS 選擇器 3. 訪問標簽內容text 屬性: 獲取標簽內純文本get_t…

DeepSeek驅動下的數據倉庫范式轉移:技術解耦、認知重構與治理演進

DeepSeek驅動下的數據倉庫范式轉移&#xff1a;技術解耦、認知重構與治理演進 ——基于多場景實證的架構革命研究 一、技術解耦&#xff1a;自動化編程范式的演進 1.1 語義驅動的ETL生成機制 在金融風控場景中&#xff0c;DeepSeek通過動態語法樹解析&#xff08;Dynamic Syn…

代碼隨想錄算法訓練營day38(補0206)

如果求組合數就是外層for循環遍歷物品&#xff0c;內層for遍歷背包。 如果求排列數就是外層for遍歷背包&#xff0c;內層for循環遍歷物品。 1.零錢兌換 題目 322. 零錢兌換 給你一個整數數組 coins &#xff0c;表示不同面額的硬幣&#xff1b;以及一個整數 amount &#xff0c…

golang channel底層實現?

底層數據實現 type hchan struct { qcount uint // 當前隊列中的元素數量 dataqsiz uint // 環形隊列的大小 buf unsafe.Pointer // 指向環形隊列的指針 elemsize uint16 // 元素大小 closed uint32 // chan…

圖的最小生成樹算法: Prim算法和Kruskal算法(C++)

上一節我們學習了最短路徑算法, 這一節來學習最小生成樹. 最小生成樹(Minimum Spanning Tree, MST)算法是圖論中的一種重要算法, 主要用于在加權無向圖中找到一棵生成樹, 使得這棵樹包含圖中的所有頂點, 并且所有邊的權重之和最小. 這樣的樹被稱為最小生成樹. 最小生成樹廣泛應…

矩陣系統源碼搭建的數據管理開發功能解析,支持OEM

一、引言 在矩陣系統中&#xff0c;數據猶如血液&#xff0c;貫穿整個系統的運行。高效的數據管理開發功能是確保矩陣系統穩定、可靠運行的關鍵&#xff0c;它涵蓋了數據的存儲、處理、安全等多個方面。本文將深入探討矩陣系統源碼搭建過程中數據管理功能的開發要點。 二、數據…

DeepSeek 助力 Vue 開發:打造絲滑的日期選擇器(Date Picker),未使用第三方插件

前言&#xff1a;哈嘍&#xff0c;大家好&#xff0c;今天給大家分享一篇文章&#xff01;并提供具體代碼幫助大家深入理解&#xff0c;徹底掌握&#xff01;創作不易&#xff0c;如果能幫助到大家或者給大家一些靈感和啟發&#xff0c;歡迎收藏關注哦 &#x1f495; 目錄 Deep…

操作系統知識點2

1.P&#xff0c;V操作可以實現進程同步&#xff0c;進程互斥&#xff0c;進程的前驅關系 2.先來先服務調度算法是不可搶占的算法 3.UNIX操作系統中&#xff0c;對文件系統中空閑區的管理通常采用成組鏈接法 4.對于FAT32文件系統&#xff0c;它采用的是鏈接結構 5.不同的I/O…