Сети и потоки
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 может быть буферизован по строкам и, следовательно, может потребовать дополнительного ввода, прежде чем данные будут отправлены в 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!"
Если вы хотите записать в файл, вы можете открыть его с флагом write ("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 является то, что поскольку 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.1.1/manual/networking-and-streams/