Networking and Streams
Juliaは、端末、パイプ、TCPソケットなどのストリーミングI/Oオブジェクトを扱うための豊富なインターフェースを提供します。これらのオブジェクトは、データをストリームのような形で送受信できるようにし、データが利用可能になると順次処理されます。このインターフェースは、システムレベルでは非同期ですが、プログラマーには同期的な方法で提示されます。これは、Juliaの協調スレッド機能(coroutine)を多用することで実現されています。
Basic Stream 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
は、stdout
に書き込まれたバイト数(「Hello World」のバイト数)である11を返しますが、この戻り値は;
で抑制されています。
ここで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("テレタイプ端末")が行バッファリングされている場合があり、そのためstdin
データがJuliaに送信される前に追加のエンターが必要になることがあります。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
Text I/O
write
メソッドは、バイナリストリームで動作することに注意してください。特に、値は任意の標準的なテキスト表現に変換されることはなく、そのまま書き出されます:
julia> write(stdout, 0x61); # suppress return value 1 with ;
a
a
はstdout
によってwrite
関数に書き込まれ、返される値は1
です(0x61
は1バイトだからです)。
テキストI/Oには、必要に応じてprint
またはshow
メソッドを使用してください(これら2つのメソッドの違いについての詳細な説明はドキュメントを参照してください):
julia> print(stdout, 0x61)
97
Custom pretty-printing の詳細については、カスタムタイプの表示メソッドを実装する方法を参照してください。
IO Output Contextual Properties
時には、IO出力は、コンテキスト情報を表示メソッドに渡す能力から恩恵を受けることがあります。IOContext
オブジェクトは、IOオブジェクトに任意のメタデータを関連付けるためのフレームワークを提供します。例えば、:compact => true
は、呼び出された表示メソッドが短い出力を印刷すべきであるというヒントパラメータをIOオブジェクトに追加します(該当する場合)。一般的なプロパティのリストについては、4d61726b646f776e2e436f64652822222c2022494f436f6e746578742229_40726566
のドキュメントを参照してください。
Working with Files
ファイルにコンテンツを書き込むには、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!"
Advanced: streaming files
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
を再度調べると、その内容が変更されていることがわかります。
ファイルを開き、その内容に対して何かを行い、再び閉じるというのは非常に一般的なパターンです。これを簡単にするために、最初の引数として関数を、2番目の引数としてファイル名を取る別の呼び出し 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."
標準出力をファイルにリダイレクトしたい場合
out_file = open("output.txt", "w")
# Redirect stdout to file
redirect_stdout(out_file) do
# Your code here
println("This output goes to `out_file` via the `stdout` variable.")
end
# Close file
close(out_file)
標準出力をファイルにリダイレクトすることで、プログラムの出力を保存して分析したり、プロセスを自動化したり、コンプライアンス要件を満たしたりするのに役立ちます。
A simple TCP example
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\
)がfile typeを一意に識別します。TCPと名前付きパイプまたはUNIXドメインソケットの違いは微妙で、accept
とconnect
メソッドに関係しています。4d61726b646f776e2e436f64652822222c20226163636570742229_40726566
メソッドは、私たちが作成したサーバーに接続しているクライアントへの接続を取得しますが、4d61726b646f776e2e436f64652822222c2022636f6e6e6563742229_40726566
関数は、指定されたメソッドを使用してサーバーに接続します。4d61726b646f776e2e436f64652822222c2022636f6e6e6563742229_40726566
関数は、listen
と同じ引数を取るため、環境(ホスト、カレントワーキングディレクトリなど)が同じであれば、接続を確立するためにリッスンしたときと同じ引数を4d61726b646f776e2e436f64652822222c2022636f6e6e6563742229_40726566
に渡すことができるはずです。それでは、上記のサーバーを作成した後にそれを試してみましょう。
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
を使用してください:
julia> close(clientside)
Resolving IP Addresses
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"
Asynchronous I/O
すべてのI/O操作は、Base.read
およびBase.write
によって非同期的に実行できます。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
Multicast
Juliaは、ユーザーデータグラムプロトコル(UDP)を使用して、IPv4およびIPv6でmulticastをサポートしています。
Transmission Control Protocol(TCP)とは異なり、UDPはアプリケーションのニーズについてほとんど仮定をしません。TCPはフロー制御(スループットを最大化するために加速および減速します)、信頼性(失われたまたは破損したパケットは自動的に再送信されます)、シーケンシング(パケットはアプリケーションに渡される前にオペレーティングシステムによって順序付けられます)、セグメントサイズ、およびセッションのセットアップとテアダウンを提供します。UDPはそのような機能を提供しません。
UDPの一般的な使用例はマルチキャストアプリケーションです。TCPは正確に2つのデバイス間の通信のための状態を持つプロトコルです。UDPは特別なマルチキャストアドレスを使用して、多くのデバイス間で同時に通信を行うことができます。
Receiving IP Multicast Packets
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)
Sending IP Multicast Packets
UDPマルチキャストでデータを送信するには、単にソケットにsend
します。送信者がマルチキャストグループに参加する必要はないことに注意してください。
using Sockets
group = ip"228.5.6.7"
socket = Sockets.UDPSocket()
send(socket, group, 6789, "Hello over IPv4")
close(socket)
IPv6 Example
この例は、前のプログラムと同じ機能を提供しますが、ネットワーク層プロトコルとして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)