跳到主要内容

使用 MoonBit 开发一个 HTTP 文件服务器

· 阅读需 17 分钟

在这篇文章中,我将会介绍如何使用 MoonBit 的异步编程功能和 moonbitlang/async 库,编写一个简单的 HTTP 文件服务器。如果你之前接触过 Python 语言,那么你可能知道,Python 有一个非常方便的内建 HTTP 服务器模块。只需要运行 python -m http.server,就能在当期文件夹启动一个文件服务器,用于局域网文件共享等用途。 在这篇文章中,我们将用 MoonBit 实现一个类似功能的程序,并借此了解 MoonBit 的异步编程支持。我们还将额外支持一个 python -m http.server 没有的实用功能:把整个文件夹打包成 zip 文件下载。

异步编程简史

异步编程,能让程序具有同时处理多项任务的能力。例如,对于一个文件服务器来说,可能会有多个用户同时访问这个服务器,而服务器需要同时服务所有用户,让它们的体验尽可能流畅、低延时。在典型的异步程序,例如服务器中,每项任务的大部分时间都花在等待 IO 上,实际的计算时间占比较低。因此,我们并不需要很多的计算资源,也能同时处理大量任务。而这其中的诀窍,就是频繁地在多个任务之间切换: 如果某项任务开始等待 IO,那么就不要继续处理它,而是马上切换到不需要等待的任务上。

过去,异步程序往往是通过多线程的方式实现的:每项任务对应一个操作系统的线程。 然而,操作系统线程需要占用较多资源,而且在线程之间切换开销较大。 因此,进入 21 世纪后,实现异步程序的主要方式变成了事件循环。 整个异步程序的形态是一个巨大的循环,每次循环中, 程序检查哪些 IO 操作已经完成,然后运行那些等待着这些已完成的 IO 操作的任务, 直到它们发起下一次 IO 请求,重新进入等待状态。 在这种编程范式中,任务间的切换发生在同一个用户态的线程里,因此开销极低。

然而,手写事件循环是一件非常痛苦的事情。 因为同一个任务的代码会被拆散到多次不同的循环中执行,程序的逻辑变得不连贯了。 因此,基于事件循环的程序非常难编写和调试。 幸运的是,就像大部分其他现代编程语言一样,MoonBit 提供了原生的异步编程支持。 用户可以像写同步程序一样写异步代码,MoonBit 会自动把异步代码切分成不同的部分。 而 moonbitlang/async 库则提供了事件循环和各种 IO 原语的实现,负责把异步代码运行起来。

MoonBit 中的异步编程

在 MoonBit 中,可以用 async fn 语法来声明一个异步函数。 异步函数看上去和同步函数完全一样,只不过它们在运行时可能在中途被打断, 一段时间后才继续恢复运行,从而实现多个任务间的切换。 在异步函数中可以正常使用循环等控制流构造,MoonBit 编译器会自动将它们变成异步的样子。

和许多其他语言不同,在调用异步函数时,MoonBit 不需要用 await 之类的特殊语法标记, 编译器会自动推断出哪些函数调用是异步的。 不过,如果你使用带有 MoonBit 支持的 IDE 或文本编辑器查看代码, 就会看到异步函数调用被渲染成了斜体、可能抛出错误的函数调用带有下划线。 因此,阅读代码时,依然可以一眼就找到所有异步的函数调用。

对于异步程序来说,另一个必不可少的组件是事件循环、任务调度和各种 IO 原语的实现。 这一点在 MoonBit 中是通过 moonbitlang/async 库实现的。 moonbitlang/async 库中提供了网络IO、文件IO、进程创建等异步操作的支持, 以及一系列管理异步编程任务的 API。 接下来,我们将会在编写 HTTP 文件服务器的途中介绍 moonbitlang/async 的各种功能。

HTTP 服务器的骨架

典型的 HTTP 服务器的结构是:

  • 服务器监听一个 TCP 端口,等待来自用户的连接请求
  • 接受来自用户的 TCP 连接后,服务器从 TCP 连接中读取用户的请求,处理用户的请求并将结果发回给用户

这里的每一项任务,都应该异步地进行: 在处理第一个用户的请求时,服务器仍应不断等待新的连接,并第一时间响应下一个用户的连接请求。 如果有多个用户同时连接到服务器,服务器应该同时处理所有用户的请求。 在这个过程中,所有可能耗费较多时间的操作,例如网络 IO 和文件 IO,都应该是异步的, 它们不应该阻塞程序、影响其他任务的处理。

moonbitlang/async 中,有一个辅助函数 @http.run_server, 能够绑我们自动完成上述工作,搭建一个 HTTP 服务器并运行它:

async fn 
async (path~ : String, port~ : Int) -> Unit
server_main
(
String
path
~ :
String
String
,
Int
port
~ :
Int
Int
) ->
Unit
Unit
{
(Unit, (?, Unit) -> Unit) -> Unit
@http.run_server
(
(String) -> Unit
@socket.Addr::parse
("[::]:\{
Int
port
}"), fn (
?
conn
,
Unit
addr
) {
Unit
@pipe.stderr
.
(String) -> Unit
write
("received new connection from \{
Unit
addr
}\n")
async (base_path : String, conn : ?) -> Unit
handle_connection
(
String
path
,
?
conn
)
}) }

server_main 接受两个参数,其中, path 是文件服务器工作的路径,port 是服务器监听的端口。 在 moonbitlang/async 中,一切异步代码都是可以取消的, 而异步代码被取消时会抛出错误,所以所有异步函数都会抛出错误。 因此,在 MoonBit 中,async fn 默认就会抛出错误,无需再显式标注 raise

server_main 中,我们使用 @http.run_server 创建了一个 HTTP 服务器并运行它。 @httpmoonbitlang/async 中提供 HTTP 解析等支持的包 moonbitlang/async/http 的别名, @http.run_server 的第一个参数是服务器要监听的地址。 这里我们提供的地址是 [::]:port, 这表示监听端口 port、接受来自任何网络接口的连接请求。 moonbitlang/async 有原生的 IPv4/IPv6 双栈支持,因此这里的服务器可以同时接受 IPv4 连接和 IPv6 连接。 @http.run_server 的第二个参数是一个回调函数,用于处理来自用户的连接。 回调函数会接受两个参数,第一个是来自用户的连接, 类型是 @http.ServerConnection,由 @http.run_server 自动获取并创建。 第二个参数是用户的网络地址。 这里,我们使用 handle_connection 函数来处理用户的请求,这个函数的实现将在稍后给出。 @http.run_server 会自动创建一个并行的任务,并在其中运行 handle_connection。 因此,服务器可以同时运行多份 handle_connection、处理多个连接。

处理用户来自用户的请求

接下来,我们开始实现实际处理用户请求的 handle_connection 函数。 handle_connection 接受两个参数,base_path 是文件服务器处理的路径, 而 conn 是来自用户的连接。

async fn 
async (base_path : String, conn : ?) -> Unit
handle_connection
(
String
base_path
:
String
String
,
?
conn
: @http.ServerConnection,
) ->
Unit
Unit
{
for { let
Unit
request
=
?
conn
.
() -> Unit
read_request
()
?
conn
.
() -> Unit
skip_request_body
()
guard
Unit
request
.
Unit
meth
is
Unit
Get
else {
?
conn
..
(Int, String) -> Unit
send_response
(501, "Not Implemented")
..
(String) -> Unit
write
("This request is not implemented")
..
() -> Unit
end_response
()
} let (
String
path
,
Bool
download_zip
) = match
Unit
request
.
String
path
{
String
[ ..path, .."?download_zip" ]
=> (
StringView
path
.
(self : StringView) -> String

Returns a new String containing a copy of the characters in this view.

Examples

  let str = "Hello World"
  let view = str.view(start_offset = str.offset_of_nth_char(0).unwrap(),end_offset = str.offset_of_nth_char(5).unwrap()) // "Hello"
  inspect(view.to_string(), content="Hello")
to_string
(), true)
String
path
=> (
String
path
, false)
} if
Bool
download_zip
{
async (conn : ?, path : String) -> Unit
serve_zip
(
?
conn
,
String
base_path
(self : String, other : String) -> String

Concatenates two strings, creating a new string that contains all characters from the first string followed by all characters from the second string.

Parameters:

  • self : The first string to concatenate.
  • other : The second string to concatenate.

Returns a new string containing the concatenation of both input strings.

Example:

  let hello = "Hello"
  let world = " World!"
  inspect(hello + world, content="Hello World!")
  inspect("" + "abc", content="abc") // concatenating with empty string
+
String
path
)
} else { let
?
file
=
(String, Unit) -> ?
@fs.open
(
String
base_path
(self : String, other : String) -> String

Concatenates two strings, creating a new string that contains all characters from the first string followed by all characters from the second string.

Parameters:

  • self : The first string to concatenate.
  • other : The second string to concatenate.

Returns a new string containing the concatenation of both input strings.

Example:

  let hello = "Hello"
  let world = " World!"
  inspect(hello + world, content="Hello World!")
  inspect("" + "abc", content="abc") // concatenating with empty string
+
String
path
,
Unit
mode
=
Unit
ReadOnly
) catch {
_ => {
?
conn
..
(Int, String) -> Unit
send_response
(404, "NotFound")
..
(String) -> Unit
write
("File not found")
..
() -> Unit
end_response
()
continue } } defer
?
file
.
() -> Unit
close
()
if
?
file
.
() -> Unit
kind
() is
Unit
Directory
{
if
Bool
download_zip
{
} else {
async (conn : ?, dir : ?, path~ : String) -> Unit
serve_directory
(
?
conn
,
?
file
.
() -> ?
as_dir
(),
String
path
~)
} } else {
async (conn : ?, file : ?, path~ : String) -> Unit
server_file
(
?
conn
,
?
file
,
String
path
~)
} } } }

handle_connection 中,程序通过一个大循环来不断从连接中读取用户请求并处理。 每次循环中,我们首先通过 conn.read_request() 读取一个来自用户的请求。 conn.read_request() 只会读取 HTTP 请求的头部,这是为了允许用户流式地读取较大的 body。 由于我们的文件服务器只处理 Get 请求,我们不需要请求的 body 中包含任何信息。 因此,我们通过 conn.skip_body() 跳过用户请求的 body,以保证下一个请求的内容可以被正确读取。

接下来,如果遇到不是 Get 的请求,guard 语句的 else 块会被执行, 此时,guard 语句后面的代码会被跳过,我们可以进入下一次循环、处理下一个请求。 在 else 块中,通过 conn.send_response(..) 向用户发送一个 “不支持该请求” 的回复。 conn.send_response(..) 会发送回复的头部,这之后,我们用 conn.write(..) 向连接写入回复的主体内容。 在写完所有内容后,我们需要用 conn.end_response() 来表明已经写完了回复的所有内容。

这里,我们希望实现一个 python -m http.server 中没有的实用功能: 以 zip 的形式下载整个文件夹。 如果用户请求的 URL 的形式是 /path/to/directory?download_zip, 我们就把 /path/to/directory 打包成 .zip 文件发送给用户。 这一功能是通过 serve_zip 函数来实现的。

由于我们实现的是一个文件服务器, 用户的 GET 请求中指定的路径会直接映射到 base_path 下对应的路径。 @fsmoonbitlang/async 中提供文件 IO 支持的包 moonbitlang/async/fs 的别名。 这里我们使用 @fs.open 打开对应的文件。 如果打开文件失败了,我们向用户发送一个 404 回复,告诉用户这个文件不存在。

如果用户请求的文件是存在的,那么我们需要把文件发送给用户。 当然,在此之前,别忘了用 defer file.close() 保证 file 占用的资源被及时释放。 通过 file.kind(),我们可以获得文件的种类。 在文件服务器中,如果用户请求的路径是一个文件夹,我们需要进行特殊的处理。 因为文件夹不能直接被发送给用户,我们需要根据文件夹的内容, 向用户返回一个 HTML 页面,让用户可以从页面看到文件夹里有哪些文件,并通过点击跳转到对应的页面。 这部分功能通过函数 serve_directory 提供。 如果用户请求的是一个普通文件,那么直接将文件的内容传输给用户即可。 这部分功能通过函数 serve_file 来实现。

向用户发送一个普通文件的代码如下:

async fn 
async (conn : ?, file : ?, path~ : String) -> Unit
server_file
(
?
conn
: @http.ServerConnection,
?
file
: @fs.File,
String
path
~ :
String
String
,
) ->
Unit
Unit
{
let
String
content_type
= match
String
path
{
[.., .. ".png"] => "image/png" [.., .. ".jpg"] | "jpeg" => "image/jpeg" [.., .. ".html"] => "text/html" [.., .. ".css"] => "text/css" [.., .. ".js"] => "text/javascript" [.., .. ".mp4"] => "video/mp4" [.., .. ".mpv"] => "video/mpv" [.., .. ".mpeg"] => "video/mpeg" [.., .. ".mkv"] => "video/x-matroska" _ => "appliaction/octet-stream" }
?
conn
..
(Int, String, Map[String, String]) -> Unit
send_response
(200, "OK",
Map[String, String]
extra_headers
={ "Content-Type":
String
content_type
})
..
(?) -> Unit
write_reader
(
?
file
)
..
() -> Unit
end_response
()
}

这里,在 HTTP 回复中,我们根据文件的后缀名填入了不同的 Content-Type 字段。 这样一来,用户在浏览器中打开图片/视频/HTML 文件时,就可以直接预览文件的内容, 而不需要先下载文件再在本地打开。 对于其他文件,Content-Type 字段的值会是 application/octet-stream, 这会让浏览器自动将文件下载到本地。

我们依然使用 conn.send_response 来用户发送回复。 通过 extra_headers 字段我们可以在回复中加入额外的 HTTP header。 回复的主体则是文件的内容。 这里,conn.write_reader 会自动流式地把 file 的内容发送给用户。 假设用户请求了一个视频文件并在浏览器中播放, 如果我们先把整个视频文件读到内存中再发送给用户, 那么用户需要等服务器读入整个视频文件之后才能收到回复,服务器的响应速度会变慢。 而且,读入整个视频文件会浪费大量的内存。 而通过使用 write_reader@http.ServerConnection 会自动把文件内容切成小块分段发送, 用户马上就能看到视频开始播放,占用的内存也会大大减少。

接下来,让我们实现显示文件夹的函数 serve_directory

async fn 
async (conn : ?, dir : ?, path~ : String) -> Unit
serve_directory
(
?
conn
: @http.ServerConnection,
?
dir
: @fs.Directory,
String
path
~ :
String
String
,
) ->
Unit
Unit
{
let
Unit
files
=
?
dir
.
() -> Unit
read_all
()
Unit
files
.
() -> Unit
sort
()
?
conn
..
(Int, String, Map[String, String]) -> Unit
send_response
(200, "OK",
Map[String, String]
extra_headers
={ "Content-Type": "text/html" })
..
(String) -> Unit
write
("<!DOCTYPE html><html><head></head><body>")
..
(String) -> Unit
write
("<h1>\{
String
path
}</h1>\n")
..
(String) -> Unit
write
("<div style=\"margin: 1em; font-size: 15pt\">\n")
..
(String) -> Unit
write
("<a href=\"\{
String
path
}?download_zip\">download as zip</a><br/><br/>\n")
if
String
path
[:-1].
(self : StringView, str : StringView) -> Int?

Returns the offset of the last occurrence of the given substring. If the substring is not found, it returns None.

rev_find
("/") is
(Int) -> Int?
Some
(
Int
index
) {
let
String
parent
= if
Int
index
(self : Int, other : Int) -> Bool

Compares two integers for equality.

Parameters:

  • self : The first integer to compare.
  • other : The second integer to compare.

Returns true if both integers have the same value, false otherwise.

Example:

  inspect(42 == 42, content="true")
  inspect(42 == -42, content="false")
==
0 { "/" } else {
String
path
[:
Int
index
].
(self : StringView) -> String

Returns a new String containing a copy of the characters in this view.

Examples

  let str = "Hello World"
  let view = str.view(start_offset = str.offset_of_nth_char(0).unwrap(),end_offset = str.offset_of_nth_char(5).unwrap()) // "Hello"
  inspect(view.to_string(), content="Hello")
to_string
() }
?
conn
.
(String) -> Unit
write
("<a href=\"\{
String
parent
}\">..</a><br/><br/>\n")
} for
Unit
file
in
Unit
files
{
let
String
file_url
= if
String
path
[
String
path
.
(self : String) -> Int

Returns the number of UTF-16 code units in the string. Note that this is not necessarily equal to the number of Unicode characters (code points) in the string, as some characters may be represented by multiple UTF-16 code units.

Parameters:

  • string : The string whose length is to be determined.

Returns the number of UTF-16 code units in the string.

Example:

  inspect("hello".length(), content="5")
  inspect("🤣".length(), content="2") // Emoji uses two UTF-16 code units
  inspect("".length(), content="0") // Empty string
length
()
(self : Int, other : Int) -> Int

Performs subtraction between two 32-bit integers, following standard two's complement arithmetic rules. When the result overflows or underflows, it wraps around within the 32-bit integer range.

Parameters:

  • self : The minuend (the number being subtracted from).
  • other : The subtrahend (the number to subtract).

Returns the difference between self and other.

Example:

  let a = 42
  let b = 10
  inspect(a - b, content="32")
  let max = 2147483647 // Int maximum value
  inspect(max - -1, content="-2147483648") // Overflow case
-
1]
(x : Int, y : Int) -> Bool
!=
'/' {
"\{
String
path
}/\{
Unit
file
}"
} else { "\{
String
path
}\{
Unit
file
}"
}
?
conn
.
(String) -> Unit
write
("<a href=\"\{
String
file_url
}\">\{
Unit
file
}</a><br/>\n")
}
?
conn
..
(String) -> Unit
write
("</div></body></html>")
..
() -> Unit
end_response
()
}

这里,我们首先读入文件夹中的文件列表并对它们进行排序。 接下来,我们根据文件夹的内容,拼出一段 HTML 页面。 HTML 页面的主体内容是文件夹中的文件, 每个文件对应一个链接,上面显示着文件名,点击链接就能跳转到对应的文件。 这里,我们通过 HTML 的 <a> 元素来实现这一点。 如果文件夹不是根目录,那么我们在页面开头放上一个特殊的链接 ..,点击它会跳转到上一级目录。 此外,页面里还有一个 download as zip 的链接, 点击这个链接就能把当前文件夹打包成 zip 后下载。

实现将文件夹打包成 zip 的功能

接下来,我们实现将文件夹打包成 zip 提供给用户的功能。 这里,简单起见,我们使用系统的 zip 命令。 serve_zip 函数的实现如下:

async fn 
async (conn : ?, path : String) -> Unit
serve_zip
(
?
conn
: @http.ServerConnection,
String
path
:
String
String
,
) ->
Unit
Unit
{
let
Unit
full_path
=
(String) -> Unit
@fs.realpath
(
String
path
)
let
String
zip_name
= if
Unit
full_path
[:].
(String) -> Unit
rev_find
("/") is
(Int) -> Unit
Some
(
Int
i
) {
Unit
full_path
[
Int
i
+1:].
() -> String
to_string
()
} else {
String
path
}
((Unit) -> Unit) -> Unit
@async.with_task_group
(fn(
Unit
group
) {
let (
Unit
we_read_from_zip
,
Unit
zip_write_to_us
) =
() -> (Unit, Unit)
@process.read_from_process
()
defer
Unit
we_read_from_zip
.
() -> Unit
close
()
Unit
group
.
(() -> Unit) -> Unit
spawn_bg
(fn() {
let
Int
exit_code
=
(String, Array[String], Unit) -> Int
@process.run
(
"zip", [ "-q", "-r", "-",
String
path
],
Unit
stdout
=
Unit
zip_write_to_us
,
) if
Int
exit_code
(x : Int, y : Int) -> Bool
!=
0 {
(msg : String, loc~ : SourceLoc = _) -> Unit raise Failure

Raises a Failure error with a given message and source location.

Parameters:

  • message : A string containing the error message to be included in the failure.
  • location : The source code location where the failure occurred. Automatically provided by the compiler when not specified.

Returns a value of type T wrapped in a Failure error type.

Throws an error of type Failure with a message that includes both the source location and the provided error message.

fail
("zip failed with exit code \{
Int
exit_code
}")
} })
?
conn
..
(Int, String, Map[String, String]) -> Unit
send_response
(200, "OK",
Map[String, String]
extra_headers
={
"Content-Type": "application/octet-stream", "Content-Disposition": "filename=\{
String
zip_name
}.zip",
}) ..
(Unit) -> Unit
write_reader
(
Unit
we_read_from_zip
)
..
() -> Unit
end_response
()
}) }

serve_zip 函数的开头,我们首先计算了用户下载的 .zip 文件的文件名。 接下来,我们使用 @async.with_task_group 创建了一个新的任务组。 任务组是 moonbitlang/async 中用于管理异步任务的核心构造, 所有异步任务都必须在一个任务组中创建。 在介绍 with_task_group 之前,让我们先看看 serve_zip 剩下的内容。 首先,我们使用 @process.read_from_process() 创建了一个临时管道, 从管道的一端写入的数据可以从另一侧读出,因此它可以用于读取一个进程的输出。 这里我们把管道的写入端 zip_write_to_us 会被提供给 zip 命令,用于写入压缩的结果。 而我们将从管道的读入端 we_read_from_zip 读取 zip 命令的输出,并将其发送给用户。

接下来,我们在新的任务组中创建了一个单独的任务, 并在其中使用 @process.run 运行 zip 命令。 @processmoonbitlang/async/process 的别名, 是 moonbitlang/async 中提供调用外部进程功能的包。 我们向 zip 传递的参数的意义是:

  • -q:不要输出日志信息
  • -r:递归压缩整个文件夹
  • -:把结果写入到 stdout
  • path:要压缩的文件夹

在调用 @process.run 时,我们通过 stdout=zip_write_to_us, 把 zip 命令的 stdout 重定向到了 zip_write_to_us,以获取 zip 的输出。 相比创建一个临时文件,这么做有两个好处:

  • zip 间的数据传递完全在内存中进行,不需要进行低效的磁盘 IO
  • zip 一边压缩,我们可以一边像用户发送已经压缩好的部分,效率更高

@process.run 会等待 zip 结束运行,并返回 zip 命令的状态码。 如果 zip 的返回值不是 0,说明 zip 失败了,我们抛出一个错误。

在调用 zip 的同时,我们继续使用 conn.send_response(..) 向用户发送回复信息。 接下来,我们用 conn.write_reader(we_read_from_zip)zip 的输出发送给用户。 Content-Disposition 这一 HTTP header 能让我们指定用户下载的 zip 文件的名字。

到这里,一切看上去都很合理。 但为什么这里要创建一个新的任务组呢?为什么不能直接提供创建新任务的 API 呢? 在编写异步程序时,有一个现象: 写出在正确时行为正确的程序比较容易,但写出在出错时依然行为正确的程序很难。 比如,对于 serve_zip 这个例子:

  • 如果 zip 命令失败了我们应该怎么办?
  • 如果数据发送到一半发生了网络错误,或者用户关闭了连接,应该怎么办?

如果 zip 命令失败了,那么整个 serve_zip 函数也应该失败。 由于此时用户可能已经收到了一部分不完整的数据,我们很难再把连接恢复到正常状态, 只能关闭把整个连接。 如果数据发送到一半发生了网络错误,那么我们应该停止 zip 的运行。 因为此时 zip 的结果已经没有用了,让它继续运行只是在浪费资源。 而且在最坏的情况下,由于我们不再读取 zip 的输出,和 zip 通信用的管道可能会被填满, 此时,zip 可能会永远阻塞在向管道写入的操作上,变成一个僵尸进程。

在上面的代码中,我们没有显式地写任何错误处理逻辑, 但是,在出现上述错误时,我们的程序的行为却是符合预期的, 而魔法就在于 @async.with_task_group 的语义,及其背后的 结构化并发 范式。 @async.with_task_group(f) 的大致语义如下:

  • 它会创建一个新的任务组 group,并运行 f(group)
  • f 可以通过 group.spawn_bg(..) 等函数在 group 中创建新的任务
  • 只有当 group 中的所有任务都完成时,with_task_group 才会返回
  • 如果 group 中的任何一个任务失败了,那么 with_task_group 也会失败,group 中的其他任务会被自动取消

这里的最后一条,就是保证正确错误处理的行为的关键:

  • 如果调用 zip 的任务失败了,那么错误会传播到整个任务组。 向用户发送回复的主任务会自动被取消, 然后错误会通过 with_task_group 自动向上传播,关闭连接
  • 如果发送回复的主任务失败了,错误同样会传播到整个任务组。 此时 @process.run 会被取消,此时它会自动向 zip 发送终止信号,结束 zip 的运行

因此,在使用 moonbitlang/async 编写异步程序时, 只需要根据程序的结构在适当的位置插入任务组, 剩下的错误处理的所有细节,都会由 with_task_group 自动解决。 这正是 moonbitlang/async 使用的结构化并发范式的威力:通过编程范式的引导, 它能让我们写出结构更清晰的异步程序,并以一种润物细无声的方式, 让异步程序在出错时也能有正确的行为。

让服务器跑起来

至此,整个 HTTP 服务器的所有内容都已实现完毕,我们可以运行这个服务器了。 MoonBit 对异步代码有原生支持,可以直接用 async fn main 定义异步程序的入口, 或是用 async test 直接测试异步代码。 这里,我们让 HTTP 服务器运行在当前目录、向用户提供当前目录下的文件,并让它监听 8000 端口:

async test {
  
async (path~ : String, port~ : Int) -> Unit
server_main
(
String
path
=".",
Int
port
=8000)
}

通过 moon test moonbit_http_server.mbt.md 运行这份文档的源码, 并在浏览器中打开 http://127.0.0.1:8000,即可使用我们实现的文件服务器。

关于 moonbitlang/async 的更多功能,可以参考它的 API 文档GitHub repo