网络和流

网络和流

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> @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> @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> @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"