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")
endText 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.comMulticast
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)