ネットワークとストリーム
Julia は端末・パイプ・TCP ソケットといったストリーム用 IO オブジェクトを扱うための豊富なインターフェースを提供します。このインターフェースはシステムレベルでは非同期的ですが、プログラマーから見ると同期的であり、内部の非同期的な操作をプログラマーが意識する必要は通常ありません。ストリームは Julia の協調的スレッディング (コルーチン) の機能を活用して実現されています。
ストリーム IO
全ての Julia のストリームは最低でも read
メソッドと write
メソッドを提供します。これらのメソッドの第一引数はストリームです:
julia> write(stdout, "Hello World"); # 返り値 11 を ; で出力しないようにする
Hello World
julia> read(stdin, Char)
'\n': ASCII/Unicode U+000a (category Cc: Other, control)
write
は 11
(stdout
に書き込まれたバイト列 "Hello World"
の長さ) を返しますが、;
があるので出力されていません。また read
の実行ではエンターキーを押すことで 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
あるいは入力された行全体が必要なら、readline
が使えます:
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
テキスト IO
上述した write
はデータをバイナリストリームとして処理することに注意してください。つまり書き込まれるデータは正準テキスト表現に変換されず、そのまま書き込まれます:
julia> write(stdout, 0x61); # 返り値 1 を ; で出力しないようにする
a
write
関数は 'a'
(0x61
) を stdout
に書き込み、0x61
の長さ 1
を返しています。
テキストを使った IO を行うには print
または show
使ってください。この二つの違いについては関数のドキュメントで説明されています。
julia> print(stdout, 0x61)
97
独自型に対して表示メソッドを実装する方法について詳しくは独自型の出力の整形の節を参照してください。
文脈プロパティ
IO 出力では表示メソッドに文脈情報を渡せると役立つ場合があります。IOContext
オブジェクトが IO オブジェクトに任意のメタデータを関連付けるフレームワークを提供します。例えば :compact => true
とすると、呼び出す出力メソッドで (可能なら) 短い出力を行うよう伝えるパラメータを IO オブジェクトに追加できます。よく使われるプロパティについては 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!"
ファイルに書き込みたいなら、書き込みフラグ ("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 ソケットを使う簡単な例を作ってみましょう。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 を知っているなら、メソッドの名前に見覚えがあるはずです。ただ Julia の Sokects
は Unix のソケット API よりもいくらか単純になっています。最初の listen
の呼び出しは指定したポート (2000
) に対する内向き接続を待機するサーバーを作成します。listen
関数は様々な種類のサーバーを作成できます:
julia> listen(2000) # localhost:2000 (IPv4) にリッスン
Sockets.TCPServer(active)
julia> listen(ip"127.0.0.1",2000) # 一つ前の呼び出しと同じ
Sockets.TCPServer(active)
julia> listen(ip"::1",2000) # localhost:2000 (IPv6) にリッスン
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\
というファイルタイプを識別するための文字列が付いています。名前付きパイプおよび UNIX ドメインソケットと TCP には accept
メソッドと connect
メソッドに関して細かな違いがあります。
こうして作成したサーバーへ接続しているクライアントを受け入れるには accept
メソッドを使い、接続方法を指定してサーバーへの接続を行うには connect
を使います。connect
関数は listen
と同じ引数を受け取るので、環境 (ホストやディレクトリ) が同じであれば listen
に渡したのと同じ引数を connect
に渡すことで接続を作成できます。上で作ったサーバーに対して connect
を試してみましょう:
julia> connect(2000)
TCPSocket(open, 0 bytes waiting)
julia> Hello World
期待通り "Hello World"
が出力されました。この裏側で何が起きているかを詳しく見ていきましょう。connect
を呼び出すと、先ほど作成したサーバーへ接続することになります。このとき accept
が新しく作成したソケットへのサーバーサイドの接続を返し、接続が成功したことを表す "Hello World" をサーバーが出力します。
Julia の大きな強みが、実際には非同期的な IO の API が同期的なものとして公開されるために、コールバックが必要とならないことです。さらにサーバーの実行が開始されていることを確認する必要さえありません。connect
を呼び出した現在のタスクは接続の確立を待機し、接続が確立してから実行を続けます。このときサーバータスクは (接続リクエストが利用可能になったので) 実行を再開し、接続を受け付けてメッセージを出力し、次のクライアントからの接続を待ちます。
読み書きは他のストリームと同様に行います。例えばエコーサーバーは次のようになります:
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"