网络和流
Julia 提供了一个丰富的接口来处理流式 I/O 对象,例如终端、管道和 TCP 套接字。虽然该接口在系统级别是异步的,但它以同步的方式呈现给程序员,通常不需要考虑底层的异步操作。这是通过大量使用 Julia 协作线程(协程)功能实现的。
基本流 I/O
所有 Julia 流至少公开了一个 read
方法和一个 write
方法,它们都将流作为其第一个参数,例如:
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,即写入 stdout
的字节数(在 "Hello World"
中),但此返回值使用 ;
进行了抑制。
这里再次按下了 Enter 键,以便 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 可能是行缓冲的,因此可能需要额外按一次 Enter 键才能将数据发送到 Julia。
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
有关如何为自定义类型实现显示方法的更多信息,请参阅 自定义漂亮打印。
IO 输出上下文属性
有时,IO 输出可以从能够将上下文信息传递到 show 方法中获益。 IOContext
对象为此框架提供了基础,用于将任意元数据与 IO 对象关联。例如,:compact => true
向 IO 对象添加了一个提示参数,指示调用的 show 方法应打印较短的输出(如果适用)。有关常见属性的列表,请参阅 IOContext
文档。
使用文件
您可以使用 write(filename::String, content)
方法将内容写入文件。
julia> write("hello.txt", "Hello, World!")
13
(13
是写入的字节数。)
您可以使用 read(filename::String)
方法读取文件的内容,或者使用 read(filename::String, String)
将内容作为字符串读取。
julia> read("hello.txt", String)
"Hello, World!"
高级:流式文件
上面的 read
和 write
方法允许您读取和写入文件内容。与许多其他环境一样,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!"
如果要写入文件,可以使用写入 ("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) # Listens on localhost:2000 (IPv4)
Sockets.TCPServer(active)
julia> listen(ip"127.0.0.1",2000) # Equivalent to the first
Sockets.TCPServer(active)
julia> listen(ip"::1",2000) # Listens on localhost:2000 (IPv6)
Sockets.TCPServer(active)
julia> listen(IPv4(0),2001) # Listens on port 2001 on all IPv4 interfaces
Sockets.TCPServer(active)
julia> listen(IPv6(0),2001) # Listens on port 2001 on all IPv6 interfaces
Sockets.TCPServer(active)
julia> listen("testsocket") # Listens on a UNIX domain socket
Sockets.PipeServer(active)
julia> listen("\\\\.\\pipe\\testsocket") # Listens on a Windows named pipe
Sockets.PipeServer(active)
请注意,最后一次调用的返回值类型有所不同。这是因为该服务器不是监听 TCP,而是监听命名管道(Windows)或 UNIX 域套接字。另请注意,Windows 命名管道格式必须遵循特定的模式,以便名称前缀 (\\.\pipe\
) 唯一标识 文件类型。TCP 与命名管道或 UNIX 域套接字之间的区别很细微,并且与 accept
和 connect
方法有关。 accept
方法检索到正在连接到我们刚刚创建的服务器的客户端的连接,而 connect
函数使用指定的方法连接到服务器。 connect
函数采用与 listen
相同的参数,因此,假设环境(即主机、cwd 等)相同,您应该能够将与 connect
相同的参数传递给 listen
以建立连接。所以让我们尝试一下(在创建上面的服务器之后)
julia> connect(2000)
TCPSocket(open, 0 bytes waiting)
julia> Hello World
正如预期的那样,我们看到了打印的“Hello World”。因此,让我们实际分析一下幕后发生了什么。当我们调用 connect
时,我们连接到刚刚创建的服务器。同时,accept 函数返回到新创建的套接字的服务器端连接,并打印“Hello World”以指示连接成功。
Julia 的一个巨大优势在于,由于 API 是同步公开的,即使 I/O 实际上是异步发生的,我们也不必担心回调,甚至不必确保服务器能够运行。当我们调用 connect
时,当前任务等待连接建立,并且仅在连接建立后继续执行。在此暂停期间,服务器任务恢复执行(因为现在有连接请求可用),接受连接,打印消息并等待下一个客户端。读取和写入的工作方式相同。要查看这一点,请考虑以下简单的回显服务器
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.read
和 Base.write
公开的全部 I/O 操作。您可以使用 @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
组播
Julia 使用用户数据报协议 (UDP) 作为传输协议,支持通过 IPv4 和 IPv6 进行 组播。
与传输控制协议 (TCP) 不同,UDP 对应用程序的需求几乎不做任何假设。TCP 提供流量控制(它加速和减速以最大化吞吐量)、可靠性(丢失或损坏的数据包会自动重新传输)、排序(数据包在操作系统将其提供给应用程序之前按顺序排列)、段大小以及会话设置和拆除。UDP 不提供此类功能。
UDP 的常见用途是在组播应用程序中。TCP 是一种用于两个设备之间通信的有状态协议。UDP 可以使用特殊的组播地址来允许许多设备之间的同时通信。
接收 IP 组播数据包
要通过 UDP 组播传输数据,只需在套接字上 recv
,并且将返回收到的第一个数据包。但是请注意,它可能不是您发送的第一个数据包!
using Sockets
group = ip"228.5.6.7"
socket = Sockets.UDPSocket()
bind(socket, ip"0.0.0.0", 6789)
join_multicast_group(socket, group)
println(String(recv(socket)))
leave_multicast_group(socket, group)
close(socket)
发送 IP 组播数据包
要通过 UDP 组播传输数据,只需 send
到套接字。请注意,发送方无需加入组播组。
using Sockets
group = ip"228.5.6.7"
socket = Sockets.UDPSocket()
send(socket, group, 6789, "Hello over IPv4")
close(socket)
IPv6 示例
此示例提供了与先前程序相同的功能,但使用 IPv6 作为网络层协议。
监听器
using Sockets
group = Sockets.IPv6("ff05::5:6:7")
socket = Sockets.UDPSocket()
bind(socket, Sockets.IPv6("::"), 6789)
join_multicast_group(socket, group)
println(String(recv(socket)))
leave_multicast_group(socket, group)
close(socket)
发送器
using Sockets
group = Sockets.IPv6("ff05::5:6:7")
socket = Sockets.UDPSocket()
send(socket, group, 6789, "Hello over IPv6")
close(socket)