ネットワークとストリーム

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)

write11 (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"