Сети и потоки
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 может выиграть от возможности передавать контекстную информацию в методы show. Объект IOContext предоставляет эту структуру для связывания произвольных метаданных с объектом IO. Например, :compact => true добавляет параметр подсказки в объект IO, что вызванный метод show должен напечатать более короткий вывод (если применимо). См. документацию 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)
Base.TCPServer(active)
julia> listen(ip"127.0.0.1",2000) # Equivalent to the first
Base.TCPServer(active)
julia> listen(ip"::1",2000) # Listens on localhost:2000 (IPv6)
Base.TCPServer(active)
julia> listen(IPv4(0),2001) # Listens on port 2001 on all IPv4 interfaces
Base.TCPServer(active)
julia> listen(IPv6(0),2001) # Listens on port 2001 on all IPv6 interfaces
Base.TCPServer(active)
julia> listen("testsocket") # Listens on a UNIX domain socket
Base.PipeServer(active)
julia> listen("\\\\.\\pipe\\testsocket") # Listens on a Windows named pipe
Base.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 является то, что, поскольку 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/v0.7.0/manual/networking-and-streams/