Сети и потоки
Julia предоставляет богатый интерфейс для работы с потоковыми объектами ввода-вывода, такими как терминалы, конвейеры и TCP-сокеты. Этот интерфейс, хотя и асинхронный на уровне системы, предоставляется программисту в синхронном режиме, и обычно нет необходимости задумываться об асинхронной работе. Это достигается путем активного использования кооперативного многопоточия 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
Для тех, кто знаком с Unix-API сокетов, имена методов будут знакомы, хотя их использование несколько проще, чем использование 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, поэтому, если среда (т.е. хост, cwd и т.д.) одинакова, вы должны иметь возможность передавать те же аргументы в connect, что и для прослушивания для установления соединения. Давайте попробуем это (после того, как создали сервер выше):
julia> connect(2000) TCPSocket(open, 0 bytes waiting) julia> Hello World
Как ожидалось, мы увидели "Hello World". Итак, давайте проанализируем, что произошло за кулисами. Когда мы вызвали connect, мы подключились к серверу, который только что создали. Между тем, функция accept возвращает соединение со стороны сервера к недавно созданному сокету и выводит "Hello World", чтобы указать, что соединение было успешно установлено.
Великая сила Julia заключается в том, что, поскольку 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"
© 2009–2019 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.2.0/manual/networking-and-streams/