pb怎么设置 allow editing_Deno TCP Echo Server 是怎么运行的?
創建了一個 “重學TypeScript” 的微信群,想加群的小伙伴,加我微信?"semlinker",備注重學TS。
在 “了不起的 Deno 入門教程”這篇文章中,我們介紹了如何使用 Deno 搭建一個簡單的 TCP echo server,本文將使用該示例來探究 TCP echo server 是怎么運行的?前方高能,請小伙伴們深吸一口氣做好準備。?了不起的 Deno 入門教程本來計劃重寫 18 年寫的 “深入學習 Node.js” 系列,然而 Deno 它來了,那就從 Deno 1.0.0 開始吧。
“深入學習 Node.js” 倉庫地址:https://github.com/semlinker/node-deep,有興趣的小伙伴可以了解一下。
一、搭建 TCP echo server
好了,廢話不多說,我們進入正題。首先我們先來回顧一下之前所寫的 TCP echo server,具體代碼如下:
echo_server.ts
const?listener?=?Deno.listen({?port:?8080?});console.log("listening?on?0.0.0.0:8080");
for?await?(const?conn?of?listener)?{
??Deno.copy(conn,?conn);
}
for await...of 語句會在異步或者同步可迭代對象上創建一個迭代循環,包括 String,Array,Array-like 對象(比如 arguments 或者 NodeList),TypedArray,Map, Set 和自定義的異步或者同步可迭代對象。
for await...of 的語法如下:
for await (variable of iterable) {statement
}
接著我們使用以下命令來啟動該 TCP echo server:
$?deno?run?--allow-net?./echo_server.ts這里需要注意的是,在運行 ./echo_server.ts 時,我們需要設置 --allow-net 標志,以允許網絡訪問。不然會出現以下錯誤信息:
error:?Uncaught?PermissionDenied:?network?access?to?"0.0.0.0:8080",???run?again?with?the?--allow-net?flag
為什么會這樣呢?這是因為 Deno 是一個 JavaScript/TypeScript 的運行時,默認使用安全環境執行代碼。當服務器成功運行之后,我們使用 nc 命令來測試一下服務器的功能:
$?nc?localhost?8080hell?semlinker
hell?semlinker
nc 是 netcat 的簡寫,有著網絡界的瑞士軍刀美譽。因為它短小精悍、功能實用,被設計為一個簡單、可靠的網絡工具。
nc 的作用:
1.實現任意 TCP/UDP 端口的偵聽,nc 可以作為 server 以 TCP 或 UDP 方式偵聽指定端口;
2.端口的掃描,nc 可以作為 Client 端發起 TCP 或 UDP 連接;
3.機器之間傳輸文件或機器之間網絡測速。
下面我們來分析一下從啟動 TCP echo server 服務器開始,到使用 nc?命令連接該服務器這期間發生了什么?
二、TCP echo server 運行流程分析
2.1 啟動 TCP echo server
在命令行運行 deno run --allow-net ./echo_server.ts 命令后,當前命令行會輸出以下信息:
listening on 0.0.0.0:8080表示我們的 TCP echo server 已經開始監聽本機的 8080 端口,這里我們可以使用 netstat 命令,來打印 Linux 中網絡系統的狀態信息:
[root@izuf6ghot555xyn666xm888?23178]#?netstat?-natpActive?Internet?connections?(servers?and?established)
Proto?Recv-Q?Send-Q?Local?Address????Foreign?Address????State???????PID/Program?name????
tcp????????0??????0?0.0.0.0:8080?????0.0.0.0:*??????????LISTEN??????23178/deno
通過觀察以上輸出的網絡信息,我們發現當前 TCP echo server 處于 LISTEN 監聽狀態,且當前進程的 PID 是 23178。
在 Linux 中,一切都是文件。在 Linux 的根目錄下存在一個 /proc 目錄,/proc 文件系統是一種虛擬文件系統,以文件系統目錄和文件形式,提供一個指向內核數據結構的接口,通過它能夠查看和改變各種系統屬性。
下面我們進入 23178 進程目錄并使用 ls -l | grep '^d' 命令查看當前目錄下的子目錄信息:
[root@izuf6ghot555xyn666xm888]#?cd?/proc/23178[root@izuf6ghot555xyn666xm888?23178]#?ls?-l?|?grep?'^d'
dr-xr-xr-x?2?root?root?0?May?17?13:17?attr
dr-x------?2?root?root?0?May?17?13:16?fd
dr-x------?2?root?root?0?May?17?13:29?fdinfo
dr-x------?2?root?root?0?May?17?13:29?map_files
dr-xr-xr-x?5?root?root?0?May?17?13:29?net
dr-x--x--x?2?root?root?0?May?17?13:16?ns
dr-xr-xr-x?4?root?root?0?May?17?13:16?task
下面我們主要分析 /proc/pid/task 和 /proc/pid/fd 這兩個目錄:
2.1.1. ?/proc/pid/task 目錄
該目錄包含的是進程中的每一個線程。每一個目錄的名字是以線程 ID 命名的(tid)。在每一個 tid 下面的目錄結構與 /proc/pid 下面的目錄結構相同。對于所有線程共享的屬性,task/tid 子目錄中的每個文件內容與 /proc/pid 目錄中的相應文件內容相同。 比如所有線程中的 task/tid/cwd 文件和父目錄中的 /proc/pid/cwd 文件內容相同,因為所有的線程共享一個工作目錄。對于每個線程的不同屬性,task/tid 下相應文件的值也不相同。
對于我們的 Deno 進程( 23178 ),我們使用 ls -al 命令查看 ?/proc/23178/task 目錄的信息:
[root@izuf6ghot555xyn666xm888?task]#?ls?-altotal?0
dr-xr-xr-x?4?root?root?0?May?17?13:16?.
dr-xr-xr-x?9?root?root?0?May?17?13:15?..
dr-xr-xr-x?6?root?root?0?May?17?13:16?23178
dr-xr-xr-x?6?root?root?0?May?17?13:16?23179
接下來我們進入?/proc/23178/task 目錄,來開始分析?/proc/pid/fd 目錄。
2.1.2 ?/proc/pid/fd 目錄
該目錄包含了當前進程打開的每一個文件。每一個條目都是一個文件描述符,是一個符號鏈接,指向的是實際打開的地址。其中 0 表示標準輸入,1 表示標準輸出,2 表示標準錯誤。在多線程程序中,如果主程序退出了,那么這個文件夾將不能被訪問。
文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核為每一個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者創建一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫往往會圍繞著文件描述符展開。但是文件描述符這一概念往往只適用于 UNIX、Linux 這樣的操作系統。
每個 Unix 進程(除了可能的守護進程)應均有三個標準的 POSIX 文件描述符,對應于三個標準流:
| 0 | Standard input | STDIN_FILENO | stdin |
| 1 | Standard output | STDOUT_FILENO | stdout |
| 2 | Standard error | STDERR_FILENO | stderr |
對于我們的 Deno 進程( 23178 ),我們使用 ls -al 命令查看 ?/proc/23178/fd 目錄的信息:
[root@izuf6ghot555xyn666xm888?fd]#?ls?-altotal?0
dr-x------?2?root?root??0?May?17?13:16?.
dr-xr-xr-x?9?root?root??0?May?17?13:15?..
lrwx------?1?root?root?64?May?17?13:16?0?->?/dev/pts/0
lrwx------?1?root?root?64?May?17?13:16?1?->?/dev/pts/0
lrwx------?1?root?root?64?May?17?13:16?2?->?/dev/pts/0
lrwx------?1?root?root?64?May?17?13:16?3?->?anon_inode:[eventpoll]
lr-x------?1?root?root?64?May?17?13:16?4?->?pipe:[30180039]
l-wx------?1?root?root?64?May?17?13:16?5?->?pipe:[30180039]
lrwx------?1?root?root?64?May?17?13:16?6?->?/dev/pts/0
lrwx------?1?root?root?64?May?17?13:16?7?->?/dev/pts/0
lrwx------?1?root?root?64?May?17?13:16?8?->?socket:[30180040]
觀察以上輸出結果,我們發現除了 0-2 文件描述符之外,我們的 Deno 進程( 23178 )還包含了其他的文件描述符。
這里我們重點關注文件描述符 8,根據輸出結果可知,它表示一個 Socket。那么這個 Socket 是什么時候創建的呢?這個問題我們先記著,后面我們會一起探究內部的創建過程。
接下來我們來分析下一個流程,即使用?nc 命令來連接我們的 TCP echo server。
2.2 連接 TCP echo server
接下來我們使用前面介紹的 nc 命令,來連接我們的 TCP echo server:
[root@izuf6ghot555xyn666xm888?~]#?nc?localhost?8080接著在鍵盤中輸入 hello semlinker,此時在當前命令行會自動回顯 hello semlinker。這時,我們先來使用 netstat 命令來查看當前的網絡狀態,具體命令如下:
[root@izuf6ghot555xyn666xm888?fd]#?netstat?-natp?|?grep?8080tcp????????0??????0?0.0.0.0:8080????????????0.0.0.0:*???????????????LISTEN??????23178/deno??????????
tcp????????0??????0?127.0.0.1:55700?????????127.0.0.1:8080??????????ESTABLISHED?23274/nc????????????
tcp????????0??????0?127.0.0.1:8080??????????127.0.0.1:55700?????????ESTABLISHED?23178/deno??
相信眼尖的小伙伴,已經注意到 23274/nc 這一行,通過這一行,我們可以發現 nc 使用本機的 55700 端口與我們的 TCP echo server 建立了 TCP 連接,因為當前的連接狀態為 ESTABLISHED。這時,讓我們再次使用 ls -al 命令來查看 /proc/23178/fd 目錄的信息,該命令的執行結果如下:
[root@izuf6ghot555xyn666xm888?fd]#?ls?-altotal?0
dr-x------?2?root?root??0?May?17?13:16?.
dr-xr-xr-x?9?root?root??0?May?17?13:15?..
lrwx------?1?root?root?64?May?17?13:16?0?->?/dev/pts/0
lrwx------?1?root?root?64?May?17?13:16?1?->?/dev/pts/0
lrwx------?1?root?root?64?May?17?13:16?2?->?/dev/pts/0
lrwx------?1?root?root?64?May?17?13:16?3?->?anon_inode:[eventpoll]
lr-x------?1?root?root?64?May?17?13:16?4?->?pipe:[30180039]
l-wx------?1?root?root?64?May?17?13:16?5?->?pipe:[30180039]
lrwx------?1?root?root?64?May?17?13:16?6?->?/dev/pts/0
lrwx------?1?root?root?64?May?17?13:16?7?->?/dev/pts/0
lrwx------?1?root?root?64?May?17?13:16?8?->?socket:[30180040]
lrwx------?1?root?root?64?May?17?13:46?9?->?socket:[30181765]
對比前面的輸出結果,當使用 nc 命令與 TCP echo server 建立連接后, /proc/23178/fd 目錄下增加了一個新的文件描述符,即 9 -> socket:[30181765],它也是用于表示一個 Socket。
好了,現在我們已經看到了現象,那具體的內部流程是怎么樣的呢?為了分析內部的執行流程,這時我們需要使用 Linux 提供的 strace 命令,該命令常用來跟蹤進程執行時的系統調用和所接收的信號。
三、使用 strace 跟蹤進程中的系統調用
為了能夠更好地理解后續的內容,我們需要先介紹一些前置知識,比如 Socket、Socket API、用戶態和內核態等相關知識。
3.1 文件描述符
Linux 系統中,把一切都看做是文件,文件又可分為:普通文件、目錄文件、鏈接文件和設備文件。
當進程打開現有文件或創建新文件時,內核向進程返回一個文件描述符,文件描述符就是內核為了高效管理已被打開的文件所創建的索引,用來指向被打開的文件,所有執行 I/O 操作的系統調用都會通過文件描述符。
每一個文件描述符會與一個打開文件相對應,同時,不同的文件描述符也會指向同一個文件。相同的文件可以被不同的進程打開也可以在同一個進程中被多次打開。
系統為每一個進程維護了一個文件描述符表,該表的值都是從 0 開始的,所以在不同的進程中你會看到相同的文件描述符,這種情況下相同文件描述符有可能指向同一個文件,也有可能指向不同的文件。
要理解文件描述符,我們需要了解由內核維護的 3 個數據結構。
- 進程級的文件描述符表;
- 系統級的打開文件描述符表;
- 文件系統的 i-node 表。
下圖展示了文件描述符、打開的文件句柄以及 i-node 之間的關系:
(圖片來源于網絡)
圖中兩個進程擁有諸多打開的文件描述符。
3.2 Socket
網絡上的兩個程序通過一個雙向的通信連接實現數據的交換,這個連接的一端稱為一個 socket(套接字),因此建立網絡通信連接至少要一對端口號。
socket 本質是對 TCP/IP 協議棧的封裝,它提供了一個針對 TCP 或者 UDP 編程的接口,并不是另一種協議。通過 socket,你可以使用 TCP/IP 協議。
Socket 的英文原義是“孔”或“插座”。作為 BSD UNIX 的進程通信機制,取后一種意思。通常也稱作"套接字",用于描述IP地址和端口,是一個通信鏈的句柄,可以用來實現不同虛擬機或不同計算機之間的通信。
在Internet 上的主機一般運行了多個服務軟件,同時提供幾種服務。每種服務都打開一個Socket,并綁定到一個端口上,不同的端口對應于不同的服務。
Socket 正如其英文原義那樣,像一個多孔插座。一臺主機猶如布滿各種插座的房間,每個插座有一個編號,有的插座提供 220 伏交流電, 有的提供 110 伏交流電,有的則提供有線電視節目。客戶軟件將插頭插到不同編號的插座,就可以得到不同的服務。—— 百度百科
關于 Socket,可以總結以下幾點:
- 它可以實現底層通信,幾乎所有的應用層都是通過 socket 進行通信的。
- 對 TCP/IP 協議進行封裝,便于應用層協議調用,屬于二者之間的中間抽象層。
- TCP/IP 協議族中,傳輸層存在兩種通用協議: TCP、UDP,兩種協議不同,因為不同參數的 socket 實現過程也不一樣。
下圖說明了面向連接的協議的套接字 API 的客戶端/服務器關系。
3.3 Socket API
(1)socket() 函數:用于創建套接字并配置套接字的各種屬性,返回描述符。
int?socket(int?af,?int?type,?int?protocol);- af 為地址族(Address Family),也就是 IP 地址類型,常用的有 AF_INET 和 AF_INET6。AF 是 “Address Family” 的簡寫,INET 是 “Inetnet” 的簡寫。AF_INET 表示 IPv4 地址,AF_INET6 表示 IPv6 地址。
- type 為數據傳輸方式/套接字類型,常用的有 SOCK_STREAM(流格式套接字) 和 SOCK_DGRAM(數據報套接字)。
- protocol 表示傳輸協議,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分別表示 TCP 傳輸協議和 UDP 傳輸協議。
使用方式:
int?tcp_socket?=?socket(AF_INET,?SOCK_STREAM,?0);??//創建TCP套接字int?udp_socket?=?socket(AF_INET,?SOCK_DGRAM,?0);??//創建UDP套接字
(2)bind() 函數:用于將套接字與特定的 IP 地址和端口綁定起來,只有這樣,流經該 IP 地址和端口的數據才能交給套接字處理。
int?bind(int?sock,?struct?sockaddr?*addr,?socklen_t?addrlen);?sock 為 socket 文件描述符,addr 為 sockaddr 結構體變量的指針,addrlen 為 addr 變量的大小,可由 sizeof() 計算得出。
使用方式:
//創建套接字int?listenfd?=?socket(AF_INET,?SOCK_STREAM,?0);
//創建sockaddr_in結構體變量
struct?sockaddr_in?serv_addr;
memset(&serv_addr,?0,?sizeof(serv_addr));??//每個字節都用0填充
serv_addr.sin_family?=?AF_INET;??//使用IPv4地址
serv_addr.sin_addr.s_addr?=?inet_addr("127.0.0.1");??//具體的IP地址
serv_addr.sin_port?=?htons(8080);??//端口
//將套接字和IP、端口綁定
bind(listenfd,?(struct?sockaddr*)&serv_addr,?sizeof(serv_addr));
以上代碼,將創建的套接字與 IP 地址 127.0.0.1、端口 8080 進行綁定。
(3)listen() 函數:用于讓套接字進入被動監聽狀態。所謂被動監聽,是指當沒有客戶端請求時,套接字處于 “睡眠” 狀態,只有當接收到客戶端請求時,套接字才會被 “喚醒” 來響應請求。
int?listen(int?sock,?int?backlog);sock 為需要進入監聽狀態的套接字,backlog 為請求隊列的最大長度。當套接字正在處理客戶端請求時,如果有新的請求進來,套接字是沒法處理的,只能把它放進緩沖區,待當前請求處理完畢后,再從緩沖區中讀取出來處理。如果不斷有新的請求進來,它們就按照先后順序在緩沖區中排隊,直到緩沖區滿。 這個緩沖區,就稱為請求隊列(Request Queue)。
當請求隊列滿時,就不再接收新的請求,對于 Linux,客戶端會收到 ECONNREFUSED 錯誤,對于 Windows,客戶端會收到 WSAECONNREFUSED 錯誤。需要注意的是,listen() 函數只是讓套接字處于監聽狀態,并沒有接收請求。接收請求需要使用 accept() 函數。
(4)accept() 函數:當套接字處于監聽狀態時,可以通過 accept() 函數來接收客戶端請求。
int?accept(int?sock,?struct?sockaddr?*addr,?socklen_t?*addrlen);??它的參數與 listen() 函數是一樣的:sock 為服務器端套接字,addr 為 sockaddr_in 結構體變量,addrlen 為參數 addr 的長度,可由 sizeof() 求得。
accept() 函數會返回一個新的套接字來和客戶端通信,addr 保存了客戶端的 IP 地址和端口號,而 sock 是服務器端的套接字,大家注意區分。
需要注意的是,listen() 函數只是讓套接字進入監聽狀態,并沒有真正接收客戶端請求,listen() 后面的代碼會繼續執行,直到遇到 accept()。accept() 會阻塞程序執行,直到有新的請求到來。 介紹完這幾個核心的 Socket API,我們來舉一個 Server Socket 的示例,從而讓大家更好的理解這些函數具體是如何使用。
simple_tcp_demo.c
#include??#include??
#include??
#include??
#include??
#include??
#define?PORT?8080?
int?main(int?argc,?char?const?*argv[])?{?
????int?server_fd,?new_socket,?valread;?
????struct?sockaddr_in?address;?
????int?opt?=?1;?
????int?addrlen?=?sizeof(address);?
????char?buffer[1024]?=?{0};?
????char?*hello?=?"Hello?from?server";?
???????
?????/*?①?創建監聽套接字,使用IPV4地址?*/?
????if?((server_fd?=?socket(AF_INET,?SOCK_STREAM,?0))?==?0)?
????{?
????????perror("socket?failed");?
????????exit(EXIT_FAILURE);?
????}?
???????
????/*?②?設置socket相關配置?*/
????if?(setsockopt(server_fd,?SOL_SOCKET,?SO_REUSEADDR,?
??????????&opt,?sizeof(opt)))?
????{?
????????perror("setsockopt");?
????????exit(EXIT_FAILURE);?
????}?
????
????/* AF_INET:因特網使用的 IPv4 地址,AF_INET6:因特網使用功能的 IPv6 地址?*/
????address.sin_family?=?AF_INET;?
????/*?INADDR_ANY就是指定地址為0.0.0.0的地址,這個地址事實上表示不確定地址,
???????或“所有地址”、“任意地址”。*/
????address.sin_addr.s_addr?=?INADDR_ANY;?
????/*?網絡端總是用Big?endian,而本機端卻要視處理器體系而定,比如x86就跟網絡端的看法不同,
???????使用的是Little endian。
?????? htons:Host To Network Short,它將本機端的字節序(endian)轉換成了
???????網絡端的字節序?*/
????address.sin_port?=?htons(?PORT?);?
???????
????/*?③?綁定到本機地址,端口為8080??*/?
????if?(bind(server_fd,?(struct?sockaddr?*)&address,??
?????????????????????????????????sizeof(address))<0)?
????{?
????????perror("bind?failed");?
????????exit(EXIT_FAILURE);?
????}?
????/*?④?為了更好的理解 backlog 參數,我們必須認識到內核為任何一個給定的監聽套接口維護兩個隊列:
???????-?未完成連接隊列(incomplete connection queue),每個這樣的 SYN 分節對應其中一項:
?????????已由某個客戶發出并到達服務器,而服務器正在等待完成相應的 TCP 三次握手過程。這些套接口
?????????處于 SYN_RCVD 狀態。
???????-?已完成連接隊列(completed?connection?queue),每個已完成?TCP?三次握手過程的客戶
?????????對應其中一項。這些套接口處于 ESTABLISHED 狀態。*/
????if?(listen(server_fd,?3)?0)?
????{?
????????perror("listen");?
????????exit(EXIT_FAILURE);?
????}?
????/*?⑤?accept()函數功能是,從處于?established?狀態的連接隊列頭部取出一個已經完成的連接,
???????如果這個隊列沒有已經完成的連接,accept()函數就會阻塞,直到取出隊列中已完成的用戶連接為止。*/
????/*?在實際開發過程中,此處會使用?while(true)?或?for?(;;)?循環處理用戶請求*/
????if?((new_socket?=?accept(server_fd,?(struct?sockaddr?*)&address,??
??????(socklen_t*)&addrlen))<0)?
????{?
????????perror("accept");?
????????exit(EXIT_FAILURE);?
????}
????/*?讀取客戶端發送過來的數據?*/
????valread?=?read(?new_socket?,?buffer,?1024);?
????printf("%s\n",buffer?);?
????/*?返回數據給客戶端?*/
????send(new_socket?,?hello?,?strlen(hello)?,?0?);?
????printf("Hello?message?sent\n");?
????return?0;?
}?
對于上述 simple_tcp_demo.c 代碼,可以通過 gcc 進行編譯并運行:
$?gcc?simple_tcp_demo.c?-o?simple_tcp_demo && ./simple_tcp_demo然后我們繼續使用 nc 命令來連接該服務器:
$?nc?localhost?8080hello?deno
Hello?from?server%??
如果一切正常的話,在命令行終端可以看到以下輸出結果:
$?tcp-server?gcc?simple_tcp_demo.c?-o?simple_tcp_demo?&&?./simple_tcp_demohello?deno
Hello?message?sent
3.4 用戶態和內核態
Linux 操作系統的體系架構分為用戶態和內核態(或者用戶空間和內核空間)。內核從本質上看是一種軟件 —— 控制計算機的硬件資源,并提供上層應用程序運行的環境。 用戶態即上層應用程序的活動空間,應用程序的執行必須依托于內核提供的資源,包括 CPU 資源、存儲資源、I/O 資源等。
為了使上層應用能夠訪問到這些資源,內核必須為上層應用提供訪問的接口:即系統調用。
系統調用時操作系統的最小功能單位。根據不同的應用場景,不同的 Linux 發行版本提供的系統調用數量也不盡相同,大致在 240-350 之間。
這些系統調用組成了用戶態跟內核態交互的基本接口。在實際的操作系統中,為了屏蔽這些復雜的底層實現細節,減輕開發者的負擔,操作系統為我們提供了庫函數。它實現對系統調用的封裝,將簡單的業務邏輯接口呈現給用戶,方便開發者調用。
這里我們以 write() 函數為例來演示一下系統調用的過程:
(圖片來源:https://www.linuxbnb.net/home/adding-a-system-call-to-linux-arm-architecture/)
除了系統調用外,我們來簡單介紹一下 Shell,相信有的讀者已經有寫過 Shell 腳本。Shell 是一個特殊的應用程序,俗稱命令行,本質上是一個命令解釋器,它下通系統調用,上通各種應用,通常充當著一種 “膠水” 的角色,來連接各個小功能程序,讓不同程序能夠以一個清晰的接口協同工作,從而增強各個程序的功能。
為了方便用戶和系統交互,一般情況下,一個 Shell 對應一個終端,終端是一個硬件設備,呈現給用戶的是一個圖形化窗口。當然前面我們也提到過 Shell 是可編程的,它擁有標準的 Shell 語法,符合其語法的文本,我們一般稱它為 Shell 腳本。
那么現在問題來了,如何從用戶態切換到內核態呢?要實現狀態切換,可以通過以下三種方式:
- 系統調用:其實系統調用本身就是中斷,但是軟中斷,跟硬中斷不同。
- 異常:如果當前進程運行在用戶態,如果這個時候發生了異常事件,就會觸發切換。
- 外設中斷:當外設完成用戶的請求時,會向 CPU 發送中斷信號。
3.5 strace 命令
strace 命令常用來跟蹤進程執行時的系統調用和所接收的信號。在 Linux 世界,進程不能直接訪問硬件設備,當進程需要訪問硬件設備(比如讀取磁盤文件,接收網絡數據等等)時,必須由用戶態模式切換至內核態模式,通過系統調用訪問硬件設備。strace 可以跟蹤到一個進程產生的系統調用,包括參數、返回值和執行消耗的時間。
接下來我們將使用 strace 命令,來跟蹤 Deno ?TCP echo server 進程的系統調用流程。首先在命令行中輸入以下命令:
[root@izuf6ghot555xyn666xm888?deno]#?strace?-ff?-o?./echo_server?deno?run?-A?./echo_server.ts-ff:如果提供 -o filename,則所有進程的跟蹤結果輸出到相應的 filename.pid 中,pid 是各進程的進程號。
-o filename:將 strace 的輸出寫入文件 filename。
當該命令成功運行之后,在 /home/deno 當前目錄下會生成以下兩個文件:
-rw-r--r--??1?root?root?14173?May?17?13:16?echo_server.23178-rw-r--r--??1?root?root???137?May?17?13:15?echo_server.23179
為了更直觀的了解 23178 和 23179 這兩個進程,這里我們再通過 pstree -ap | grep deno 命令將 deno 相關的進程以樹狀圖的形式展示出來:
[root@izuf6ghot555xyn666xm888?deno]#?pstree?-ap?|?grep?deno??|???|???????`-strace,23176?-ff?-o?./echo_server?deno?run?-A?./echo_server.ts
??|???|???????????`-deno,23178?run?-A?./echo_server.ts
??|???|???????????????`-{deno},23179
??|???????????|-grep,23285?--color=auto?deno
通過觀察上述的進程樹,我們可以知道我們的 TCP echo server 進程對應的進程 ID 是 23178,我們可以通過查看當前的網絡狀態來驗證我們的猜測:
[root@izuf6ghot555xyn666xm888?deno]#?netstat?-natp?|?grep?denotcp????????0??????0?0.0.0.0:8080????????????0.0.0.0:*???????????????LISTEN??????23178/deno
下面我們來打開 /home/deno/echo_server.23178 這個文件,這個文件內容較多,下面我們截取重要的部分:
echo-server-23178-listen從圖中可知,在 TCP echo server 啟動的時候,會調用 socket() 函數,創建監聽套接字,之后會將該套接字與本機 0.0.0.0 地址和 8080 端口綁定起來,只有這樣,流經該 IP 地址和端口的數據才能交給套接字處理。接著會繼續調用 listen() 函數,如 listen(8, 128) ,讓套接字進入被動監聽狀態。
這時我們進入 /proc/23178/fd 目錄,使用 ls -al 查看當前目錄的狀態,這里我們看到了預想的文件描述 —— 8 -> socket:[30180040]。
[root@izuf6ghot555xyn666xm888?fd]#?ls?-altotal?0
dr-x------?2?root?root??0?May?17?13:16?.
dr-xr-xr-x?9?root?root??0?May?17?13:15?..
lrwx------?1?root?root?64?May?17?13:16?0?->?/dev/pts/0
lrwx------?1?root?root?64?May?17?13:16?1?->?/dev/pts/0
lrwx------?1?root?root?64?May?17?13:16?2?->?/dev/pts/0
lrwx------?1?root?root?64?May?17?13:16?3?->?anon_inode:[eventpoll]
lr-x------?1?root?root?64?May?17?13:16?4?->?pipe:[30180039]
l-wx------?1?root?root?64?May?17?13:16?5?->?pipe:[30180039]
lrwx------?1?root?root?64?May?17?13:16?6?->?/dev/pts/0
lrwx------?1?root?root?64?May?17?13:16?7?->?/dev/pts/0
lrwx------?1?root?root?64?May?17?13:16?8?->?socket:[30180040]
接下來我們使用 nc 命令,來連接我們的 TCP echo server:
[root@izuf6ghot555xyn666xm888?deno]#?nc?localhost?8080前面我們已經知道,當成功創建連接后,/proc/23178/fd 目錄下會增加一個新的文件描述符:
lrwx------?1?root?root?64?May?17?13:46?9?->?socket:[30181765]前面我們已經介紹過了,當套接字處于監聽狀態時,可以通過 accept() 函數來接收客戶端請求。此外,accept() 函數會返回一個新的套接字來與客戶端通信。下面我繼續打開 /home/deno/echo_server.23178 這個文件,這里我們找了與 accept 相關的內容:
echo-server-23178-accept由圖可知文件描述符 9 所對應的 socket 套接字,是在調用 nc 命令之后產生了,當客戶端與服務端建立連接后會返回一個新的套接字來與客戶端通信。相信有的讀者也有注意到,圖中除了 accept4 之外,還出現了與 IO 多路復用相關的 epoll_ctl 和 epoll_wait 函數。
epoll 是 Linux 內核的可擴展 I/O 事件通知機制。于 Linux 2.5.44 首度登場,它設計目的旨在取代既有 POSIX select 與 poll 系統函數,讓需要大量操作文件描述符的程序得以發揮更優異的性能。epoll 實現的功能與 poll 類似,都是監聽多個文件描述符上的事件。
epoll 與 FreeBSD 的 kqueue 類似,底層都是由可配置的操作系統內核對象建構而成,并以文件描述符(file descriptor)的形式呈現于用戶空間。epoll 通過使用紅黑樹(RB-tree)搜索被監視的文件描述符(file descriptor)。
關于 IO 多路復用與 epoll 相關的內容,我們這里就不繼續展開了,后續有時間的話,會專門寫一下 IO 多路復用的文章,介紹一下 select、poll 和 epoll 這些多路復用器的區別。這篇內容相對會比較難理解,請小伙伴們多多包涵,后續會來篇輕松一點的,分析一下 Deno 標準庫的相關實現。
四、參考資源
- socket()函數用法詳解
- Linux下/proc目錄簡介
- strace 跟蹤進程中的系統調用
- 怎樣去理解Linux用戶態和內核態?
- Linux中的文件描述符與打開文件之間的關系
在 TS 中如何減少重復代碼
?一文讀懂 TS 中 Object, object, {} 類型之間的區別一文讀懂 TS 中 Object, object, {} 類型之間的區別
?遇到這些 TS 問題你會頭暈么?遇到這些 TS 問題你會頭暈么?
聚焦全棧,專注分享 Angular、TypeScript、Node.js 、Spring 技術棧等全棧干貨。
回復?0?進入重學TypeScript學習群
回復?1?獲取全棧修仙之路博客地址
總結
以上是生活随笔為你收集整理的pb怎么设置 allow editing_Deno TCP Echo Server 是怎么运行的?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 三位数除以两位数怎么算竖式_青岛版三年级
- 下一篇: floquet端口x极化入射波_请问CS