Networking and Streams

Julia предоставляет богатый интерфейс для работы с потоковыми объектами ввода-вывода, такими как терминалы, каналы и TCP-сокеты. Эти объекты позволяют отправлять и получать данные в потоковом режиме, что означает, что данные обрабатываются последовательно по мере их поступления. Этот интерфейс, хотя и асинхронный на уровне системы, представлен программисту в синхронном виде. Это достигается за счет активного использования функциональности совместного потокового выполнения 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 возвращает 11, количество байтов (в "Hello World"), записанных в stdout, но это возвращаемое значение подавляется с помощью ;.

Здесь снова была нажата клавиша Enter, чтобы Джулия прочитала новую строку. Теперь, как вы можете видеть из этого примера, 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 ("телетайпный терминал") может быть буферизован по строкам и, следовательно, может потребовать дополнительного нажатия Enter перед отправкой данных stdin в Julia. При запуске Julia из командной строки в TTY вывод по умолчанию отправляется в консоль, а стандартный ввод считывается с клавиатуры.

Чтобы прочитать каждую строку из 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 занимает один байт).

Для ввода/вывода текста используйте методы print или show, в зависимости от ваших потребностей (см. документацию по этим двум методам для подробного обсуждения различий между ними):

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 покажет, что его содержимое изменилось.

Открытие файла, выполнение действий с его содержимым и последующее закрытие - это очень распространенный шаблон. Чтобы упростить это, существует другой вызов open, который принимает функцию в качестве первого аргумента и имя файла в качестве второго, открывает файл, вызывает функцию с файлом в качестве аргумента, а затем закрывает его снова. Например, given a function:

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."

Если вы хотите перенаправить stdout в файл

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)

Перенаправление stdout в файл может помочь вам сохранить и проанализировать вывод программы, автоматизировать процессы и соответствовать требованиям соблюдения.

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

Тем, кто знаком с API сокетов Unix, имена методов будут знакомы, хотя их использование несколько проще, чем в сыром API сокетов Unix. Первый вызов 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, поэтому, предполагая, что окружение (т.е. хост, cwd и т.д.) одинаковое, вы должны иметь возможность передать те же аргументы в 4d61726b646f776e2e436f64652822222c2022636f6e6e6563742229_40726566, как вы это сделали для прослушивания, чтобы установить соединение. Итак, давайте попробуем это (после создания сервера выше):

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

julia> Hello World

Как и ожидалось, мы увидели "Hello World" на экране. Итак, давайте на самом деле проанализируем, что произошло за кулисами. Когда мы вызвали connect, мы подключились к серверу, который только что создали. Тем временем функция accept возвращает серверное соединение с только что созданным сокетом и выводит "Hello World", чтобы указать, что соединение было успешным.

Одним из больших преимуществ Julia является то, что, поскольку 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

Все операции ввода-вывода, предоставляемые 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 поддерживает multicast через IPv4 и IPv6, используя Протокол пользовательских датаграмм (UDP) в качестве транспорта.

В отличие от Протокола управления передачей (TCP), UDP почти не делает предположений о потребностях приложения. TCP обеспечивает управление потоком (он ускоряется и замедляется для максимизации пропускной способности), надежность (потерянные или поврежденные пакеты автоматически retransmit), последовательность (пакеты упорядочиваются операционной системой перед тем, как они передаются приложению), размер сегмента и настройку и завершение сеанса. UDP не предоставляет таких функций.

Распространенное использование UDP - это многокастовые приложения. TCP - это протокол с состоянием для связи между точно двумя устройствами. 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)