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)