网络和流

Julia 提供了一个功能丰富的接口来处理流式 I/O 对象,如终端、管道和 TCP 套接字。此接口虽然在系统级是异步的,但是其以同步的方式展现给程序员,通常也不需要考虑底层的异步操作。这是通过大量使用 Julia 协作线程(协程)功能实现的。

基础流 I/O

所有 Julia stream 都暴露了 readwrite 方法,将 stream 作为它们的第一个参数,如:

julia> write(stdout, "Hello World");  # suppress return value 11 with ;
Hello World
julia> read(stdin, Char)

'\n': ASCII/Unicode U+000a (category Cc: Other, control)

注意,write 返回 11,字节数("Hello World")写入 stdout,但是返回值使用 ; 抑制。

这里按了两次回车,以便 Julia 能够读取到换行符。正如你在这个例子中所看到的,write 以待写入的数据作为其第二个参数,而 read 以待读取的数据的类型作为其第二个参数。

例如,为了读取一个简单的字节数组,我们可以这样做:

julia> x = zeros(UInt8, 4)
4-element Array{UInt8,1}:
 0x00
 0x00
 0x00
 0x00

julia> read!(stdin, x)
abcd
4-element Array{UInt8,1}:
 0x61
 0x62
 0x63
 0x64

但是,因为这有些繁琐,所以提供了几个方便的方法。例如,我们可以把上面的代码编写为:

julia> read(stdin, 4)
abcd
4-element Array{UInt8,1}:
 0x61
 0x62
 0x63
 0x64

或者如果我们想要读取一整行:

julia> readline(stdin)
abcd
"abcd"

请注意,根据你的终端设置,你的 TTY 可能是行缓冲的,因此在数据发送给 Julia 前可能需要额外的回车。

若要读取 stdin 的每一行,可以使用 eachline

for line in eachline(stdin)
    print("Found $line")
end

或者如果你想要按字符读取的话,使用 read

while !eof(stdin)
    x = read(stdin, Char)
    println("Found: $x")
end

文本 I/O

请注意,上面提到的 write 方法对二进制流进行操作。具体来说,值不会转换为任何规范的文本表示形式,而是按原样输出:

julia> write(stdout, 0x61);  # suppress return value 1 with ;
a

请注意,awrite 函数写入到 stdout 并且返回值为 1(因为 0x61 为一个字节)。

对于文本 I/O,请根据需要使用 printshow 方法(有关这两个方法之间的差异的详细讨论,请参阅它们的文档):

julia> print(stdout, 0x61)
97

有关如何实现自定义类型的显示方法的更多信息,请参阅 自定义 pretty-printing

IO 输出的上下文信息

有时,IO 输出可受益于将上下文信息传递到 show 方法的能力。IOContext 对象提供了将任意元数据与 IO 对象相关联的框架。例如,:compact => true 向 IO 对象添加一个参数来提示调用的 show 方法应该打印一个较短的输出(如果适用)。有关常用属性的列表,请参阅 IOContext 文档。

使用文件

和其他环境一样,Julia 有 open 函数,它接收文件名并返回一个 IOStream 对象,你可以用该对象来对文件进行读取和写入。例如,如果我们有文件 hello.txt,其内容为 Hello, World!

julia> f = open("hello.txt")
IOStream(<file hello.txt>)

julia> readlines(f)
1-element Array{String,1}:
 "Hello, World!"

若要写入文件,则可以带着 write("w")标志来打开它:

julia> f = open("hello.txt","w")
IOStream(<file hello.txt>)

julia> write(f,"Hello again.")
12

你如果在此刻检查 hello.txt 的内容,会注意到它是空的;改动实际上还没有写入到磁盘中。这是因为 IOStream 必须在写入实际刷新到磁盘前关闭:

julia> close(f)

再次检查 hello.txt 将显示其内容已被更改。

打开文件,对其内容执行一些操作,并再次关闭它是一种非常常见的模式。为了使这更容易,open 还有另一种调用方式,它以一个函数作为其第一个参数,以文件名作为其第二个参数,以该文件为参数调用该函数,然后再次关闭它。例如,给定函数:

function read_and_capitalize(f::IOStream)
    return uppercase(read(f, String))
end

可以调用:

julia> open(read_and_capitalize, "hello.txt")
"HELLO AGAIN."

来打开 hello.txt,对它调用 read_and_capitalize,关闭 hello.txt 并返回大写的内容。

为了避免被迫定义一个命名函数,你可以使用 do 语法,它可以动态地创建匿名函数:

julia> open("hello.txt") do f
           uppercase(read(f, String))
       end
"HELLO AGAIN."

一个简单的 TCP 示例

让我们直接进入一个 TCP 套接字相关的简单示例。此功能位于名为 Sockets 的标准库中。让我们先创建一个简单的服务器:

julia> using Sockets

julia> errormonitor(@async begin
           server = listen(2000)
           while true
               sock = accept(server)
               println("Hello World\n")
           end
       end)
Task (runnable) @0x00007fd31dc11ae0

对于那些熟悉 Unix 套接字 API 的人,这些方法名称会让人感觉很熟悉,可是它们的用法比原始的 Unix 套接字 API 要简单些。在本例中,首次调用 listen 会创建一个服务器,等待传入指定端口(2000)的连接。

julia> listen(2000) # 监听(IPv4 下的)localhost:2000
Sockets.TCPServer(active)

julia> listen(ip"127.0.0.1",2000) # 等价于第一个
Sockets.TCPServer(active)

julia> listen(ip"::1",2000) # 监听(IPv6 下的)localhost:2000
Sockets.TCPServer(active)

julia> listen(IPv4(0),2001) # 监听所有 IPv4 接口的端口 2001
Sockets.TCPServer(active)

julia> listen(IPv6(0),2001) # 监听所有 IPv6 接口的端口 2001
Sockets.TCPServer(active)

julia> listen("testsocket") # 监听 UNIX 域套接字
Sockets.PipeServer(active)

julia> listen("\\\\.\\pipe\\testsocket") # 监听 Windows 命名管道
Sockets.PipeServer(active)

请注意,最后一次调用返回的类型是不同的。这是因为此服务器不监听 TCP,而是监听命名管道(Windows)或 UNIX 域套接字。还请注意 Windows 命名管道格式必须具有特定的模式,即名称前缀(\\.\pipe\),以便唯一标识文件类型。TCP 和命名管道或 UNIX 域套接字之间的区别是微妙的,这与 acceptconnect 方法有关。accept 方法检索到连接到我们刚创建的服务器的客户端的连接,而 connect 函数使用指定的方法连接到服务器。connect 函数接收与 listen 相同的参数,因此,假设环境(即 host、cwd 等)相同,你应该能够将相同的参数传递给 connect,就像你在监听建立连接时所做的那样。那么让我们尝试一下(在创建上面的服务器之后):

julia> connect(2000)
TCPSocket(open, 0 bytes waiting)

julia> Hello World

不出所料,我们看到「Hello World」被打印出来。那么,让我们分析一下幕后发生的事情。在我们调用 connect 时,我们连接到刚刚创建的服务器。与此同时,accept 函数返回到新创建的套接字的服务器端连接,并打印「Hello World」来表明连接成功。

Julia 的强大优势在于,即使 I/O 实际上是异步发生的,API 也以同步方式暴露,我们不必担心回调,甚至不必确保服务器能够运行。在我们调用 connect 时,当前任务等待建立连接,并在这之后才继续执行。在此暂停中,服务器任务恢复执行(因为现在有一个连接请求是可用的),接受该连接,打印信息并等待下一个客户端。读取和写入以同样的方式运行。为了理解这一点,请考虑以下简单的 echo 服务器:

julia> errormonitor(@async begin
           server = listen(2001)
           while true
               sock = accept(server)
               @async while isopen(sock)
                   write(sock, readline(sock, keep=true))
               end
           end
       end)
Task (runnable) @0x00007fd31dc12e60

julia> clientside = connect(2001)
TCPSocket(RawFD(28) open, 0 bytes waiting)

julia> errormonitor(@async while isopen(clientside)
           write(stdout, readline(clientside, keep=true))
       end)
Task (runnable) @0x00007fd31dc11870

julia> println(clientside,"Hello World from the Echo Server")
Hello World from the Echo Server

与其他流一样,使用 close 即可断开该套接字:

julia> close(clientside)

解析 IP 地址

listen 方法不一致的 connect 方法之一是 connect(host::String,port),它将尝试连接到由 host 参数给定的主机上的由 port 参数给定的端口。它允许你执行以下操作:

julia> connect("google.com", 80)
TCPSocket(RawFD(30) open, 0 bytes waiting)

此功能的基础是 getaddrinfo,它将执行适当的地址解析:

julia> getaddrinfo("google.com")
ip"74.125.226.225"

异步 I/O

Base.readBase.write 的所有 I/O 操作都可以通过使用 coroutines 异步执行。 你可以使用 @async 宏创建一个新的协程来读取或写入流:

julia> task = @async open("foo.txt", "w") do io
           write(io, "Hello, World!")
       end;

julia> wait(task)

julia> readlines("foo.txt")
1-element Array{String,1}:
 "Hello, World!"

通常会遇到您想要同时执行多个异步操作并等待它们全部完成的情况。你可以使用@ sync宏,这会阻塞你的程序直到它所包裹的所有协程运行完毕。

julia> using Sockets

julia> @sync for hostname in ("google.com", "github.com", "julialang.org")
           @async begin
               conn = connect(hostname, 80)
               write(conn, "GET / HTTP/1.1\r\nHost:$(hostname)\r\n\r\n")
               readline(conn, keep=true)
               println("Finished connection to $(hostname)")
           end
       end
Finished connection to google.com
Finished connection to julialang.org
Finished connection to github.com