Сети и потоки
Julia предоставляет богатый интерфейс для работы с потоковыми объектами ввода-вывода, такими как терминалы, каналы и сокеты TCP. Хотя этот интерфейс асинхронный на системном уровне, для программиста он представлен синхронным образом, и обычно нет необходимости задумываться об underlying асинхронной операции. Это достигается за счёт интенсивного использования кооперативного многопоточия Julia (корутина).
Основные потоковые операции ввода-вывода
Все потоки 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, чтобы 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
или, если бы мы хотели прочитать всю строку вместо этого:
julia> readline(stdin) abcd "abcd"
Обратите внимание, что в зависимости от настроек вашего терминала, ваш TTY может быть буферизован по строкам и, следовательно, может потребовать дополнительной нажатия Enter перед отправкой данных в Julia.
Чтобы прочитать каждую строку из stdin, вы можете использовать eachline:
for line in eachline(stdin)
print("Found $line")
end
или read, если вы хотели читать по символам вместо этого:
while !eof(stdin)
x = read(stdin, Char)
println("Found: $x")
end
Ввод-вывод текста
Обратите внимание, что метод write, упомянутый выше, работает с бинарными потоками. В частности, значения не преобразуются в какое-либо каноническое текстовое представление, а записываются как есть:
julia> write(stdout, 0x61); # suppress return value 1 with ; a
Обратите внимание, что a записывается в stdout функцией write и что возвращаемое значение равно 1 (поскольку 0x61 — один байт).
Для ввода-вывода текста используйте методы print или show, в зависимости от ваших потребностей (см. документацию по этим двум методам для подробного обсуждения различий между ними):
julia> print(stdout, 0x61) 97
См. Пользовательское красивое форматирование для получения дополнительной информации о реализации методов отображения для пользовательских типов.
Контекстные свойства вывода IO
Иногда вывод 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. Эта функциональность находится в стандартной библиотеке пакетов под названием Sockets. Давайте сначала создадим простой сервер:
julia> using Sockets
julia> @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\) уникально идентифицирует тип файла . Различие между 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, текущая задача ждала установления соединения и продолжила выполнение только после завершения этого процесса. В этой паузе задача сервера возобновила выполнение (поскольку запрос на подключение стал доступен), приняла подключение, вывела сообщение и ожидала следующего клиента. Чтение и запись работают аналогичным образом. Чтобы увидеть это, рассмотрите следующий простой эхо-сервер:
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-адресов
Один из методов 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"
Асинхронный ввод-вывод
Все операции ввода-вывода, доступные через Base.read и Base.write, могут выполняться асинхронно с помощью корутин . Вы можете создать новую корутину для чтения или записи в поток с помощью макроса @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
© 2009–2021 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.6.0/manual/networking-and-streams/