网络和流
Julia 提供了一个功能丰富的接口来处理流式 I/O 对象,如终端、管道和 TCP 套接字。此接口虽然在系统级是异步的,但是其以同步的方式展现给程序员,通常也不需要考虑底层的异步操作。这是通过大量使用 Julia 协作线程(协程)功能实现的。
基础流 I/O
所有 Julia stream 都暴露了 read
和 write
方法,将 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
请注意,a
被 write
函数写入到 stdout
并且返回值为 1
(因为 0x61
为一个字节)。
对于文本 I/O,请根据需要使用 print
或 show
方法(有关这两个方法之间的差异的详细讨论,请参阅它们的文档):
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 域套接字之间的区别是微妙的,这与 accept
和 connect
方法有关。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"