Spec-Zone.ru › Julia 1.0

Сеть и потоки

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

Для тех, кто знаком с Unix-сокет API, имена методов будут знакомы, хотя их использование немного проще, чем в сыром Unix-сокет API. Первый вызов 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.0.4/manual/networking-and-streams/

Spec-Zone.ru

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