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

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

Julia は、端末、パイプ、TCP ソケットなどのストリーミング I/O オブジェクトを扱う豊富なインターフェイスを提供します。このインターフェイスは、システム レベルでは非同期ですが、プログラマに同期的に提示され、通常は背後にある非同期処理を考える必要はありません。これは、Julia 協調スレッド(coroutine)機能を多用することによって達成されます。

Basic Stream I/O

All Julia streams expose at least a read and a write method, taking the stream as their first argument, e.g.:

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)

Note that write returns 11, the number of bytes (in "Hello World") written to stdout, but this return value is suppressed with the ;.

Here Enter was pressed again so that Julia would read the newline. Now, as you can see from this example, write takes the data to write as its second argument, while read takes the type of the data to be read as the second argument.

For example, to read a simple byte array, we could do:

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

However, since this is slightly cumbersome, there are several convenience methods provided. For example, we could have written the above as:

julia> read(stdin, 4)
abcd
4-element Array{UInt8,1}:
 0x61
 0x62
 0x63
 0x64

or if we had wanted to read the entire line instead:

julia> readline(stdin)
abcd
"abcd"

Note that depending on your terminal settings, your TTY may be line buffered and might thus require an additional enter before the data is sent to Julia.

To read every line from stdin you can use eachline:

for line in eachline(stdin)
    print("Found $line")
end

or read if you wanted to read by character instead:

while !eof(stdin)
    x = read(stdin, Char)
    println("Found: $x")
end

Text I/O

Note that the write method mentioned above operates on binary streams. In particular, values do not get converted to any canonical text representation but are written out as is:

julia> write(stdout, 0x61);  # suppress return value 1 with ;
a

Note that a is written to stdout by the write function and that the returned value is 1 (since 0x61 is one byte).

For text I/O, use the print or show methods, depending on your needs (see the documentation for these two methods for a detailed discussion of the difference between them):

julia> print(stdout, 0x61)
97

See Custom pretty-printing for more information on how to implement display methods for custom types.

IO Output Contextual Properties

Sometimes IO output can benefit from the ability to pass contextual information into show methods. The IOContext object provides this framework for associating arbitrary metadata with an IO object. For example, :compact => true adds a hinting parameter to the IO object that the invoked show method should print a shorter output (if applicable). See the IOContext documentation for a list of common properties.

Working with Files

Like many other environments, Julia has an open function, which takes a filename and returns an IOStream object that you can use to read and write things from the file. For example, if we have a file, hello.txt, whose contents are Hello, World!:

julia> f = open("hello.txt")
IOStream(<file hello.txt>)

julia> readlines(f)
1-element Array{String,1}:
 "Hello, World!"

If you want to write to a file, you can open it with the write ("w") flag:

julia> f = open("hello.txt","w")
IOStream(<file hello.txt>)

julia> write(f,"Hello again.")
12

If you examine the contents of hello.txt at this point, you will notice that it is empty; nothing has actually been written to disk yet. This is because the IOStream must be closed before the write is actually flushed to disk:

julia> close(f)

Examining hello.txt again will show its contents have been changed.

Opening a file, doing something to its contents, and closing it again is a very common pattern. To make this easier, there exists another invocation of open which takes a function as its first argument and filename as its second, opens the file, calls the function with the file as an argument, and then closes it again. For example, given a function:

function read_and_capitalize(f::IOStream)
    return uppercase(read(f, String))
end

You can call:

julia> open(read_and_capitalize, "hello.txt")
"HELLO AGAIN."

to open hello.txt, call read_and_capitalize on it, close hello.txt and return the capitalized contents.

To avoid even having to define a named function, you can use the do syntax, which creates an anonymous function on the fly:

julia> open("hello.txt") do f
           uppercase(read(f, String))
       end
"HELLO AGAIN."

簡単な 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 に精通している人にとって親しみやすいメソッド名ですが、その使用法は生の 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と同じ引数を受け取るので、環境(ホスト、現在のカレントディレクトリなど)が同じであると仮定すると、接続を確立するために行ったのと同じ引数をconnectに渡すことができるはずです。(上記のサーバーを作成した後)それを試してみましょう:

julia> connect(2000)
TCPSocket(open, 0 bytes waiting)

julia> Hello World

予想通り"Hello World" が(サーバー側に)プリントされました。それでは、舞台裏で何が起こったのかを実際に分析してみましょう。connectを呼ぶと、作成したサーバーに接続します。一方、accept 関数は、新しく作成されたソケットへのサーバー側からの接続を返し、接続が成功したことを示す "Hello World" を出力します。

Julia の大きな強みは、I/O が実際に非同期的に発生しているにもかかわらず、API が同期的に公開されるので、コールバックを心配したり、サーバーが確実に実行されることを確認したりする必要がなかったことです。connectを呼び出すと、現在のタスクは接続が確立されるのを待ち、その後も実行を続行しました。この一時停止では、サーバー タスクが実行を再開し (接続要求が利用可能になったため)、接続を受け入れ、メッセージを印刷し、次のクライアントを待機しました。ReadとWriteは同じように機能します。 これを確認するために次の単純なエコー サーバーを考えてみましょう:

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"