網路和串流
Julia 提供豐富的介面來處理串流 I/O 物件,例如終端機、管線和 TCP socket。此介面雖然在系統層級上是非同步的,但以同步的方式呈現給程式設計人員,而且通常不需要考慮底層的非同步操作。這是透過大量使用 Julia 合作式執行緒 (coroutine) 功能來實現的。
基本串流輸入/輸出
所有 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 可能會緩衝換行符號,因此在將資料傳送至 Julia 之前可能需要額外輸入一次。
若要從 stdin
讀取每一行,您可以使用 eachline
for line in eachline(stdin)
print("Found $line")
end
或者 read
,如果您想要逐字元讀取
while !eof(stdin)
x = read(stdin, Char)
println("Found: $x")
end
文字輸入/輸出
請注意,上面提到的 write
方法會作用於二進位串流。特別是,值不會轉換成任何規範的文字表示,而是原樣寫出
julia> write(stdout, 0x61); # suppress return value 1 with ;
a
請注意,a
是透過 stdout
由 write
函數寫入,且傳回值為 1
(因為 0x61
為一個位元組)。
對於文字 I/O,請使用 print
或 show
方法,視您的需求而定(請參閱這兩個方法的文件,以詳細了解它們之間的差異)
julia> print(stdout, 0x61)
97
請參閱 自訂美化列印,以取得更多有關如何實作自訂類型顯示方法的資訊。
IO 輸出內容相關屬性
有時 IO 輸出可以受益於將內容相關資訊傳遞至顯示方法的能力。 IOContext
物件提供此架構,用於將任意元資料與 IO 物件關聯。例如,:compact => true
會新增一個提示參數至 IO 物件,指示呼叫的顯示方法應列印較短的輸出(如果適用)。請參閱 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 socket 的簡單範例。此功能在一個稱為 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 socket API 的人來說,方法名稱會很熟悉,儘管它們的使用方式比原始 Unix socket 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 的一大優點是,儘管 I/O 實際上是異步發生的,但由於 API 是同步公開的,因此我們不必擔心回呼,甚至不必確保伺服器可以執行。當我們呼叫 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
來斷開 socket
julia> close(clientside)
解析 IP 位址
其中一個 connect
方法不遵循 listen
方法的是 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 作業都可以透過使用 coroutine 非同步執行。您可以使用 @async
巨集建立新的 coroutine,以從串流讀取或寫入串流
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
巨集,讓您的程式在它封裝的所有 coroutine 都退出之前封鎖
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 多播傳輸資料,只需在 socket 上執行 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 多播傳輸資料,只需對 socket 執行 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)