Spec-Zone.ru › Julia 1.5

Сети и потоки

Julia предоставляет богатый интерфейс для работы с потоковыми объектами ввода-вывода, такими как терминалы, конвейеры и сокеты TCP. Этот интерфейс, хотя и асинхронный на системном уровне, представлен программисту в синхронном стиле, и обычно нет необходимости думать об underlying асинхронных операциях. Это достигается за счёт активного использования кооперативного многопоточия 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 raw 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.5.3/manual/networking-and-streams/

Spec-Zone.ru

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