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