Spec-Zone.ru › Julia 1.3

Сети и потоки

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

Для тех, кто знаком с 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, поэтому, предположив, что среда (т. е. хост, 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–2020 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.3.1/manual/networking-and-streams/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API