Spec-Zone.ru › Julia 1.7

Сети и потоки

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> errormonitor(@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 представлен синхронно, хотя I/O происходит асинхронно, нам не нужно было беспокоиться о обратных вызовах или даже о том, чтобы убедиться, что сервер выполняется. При вызове connect текущая задача ожидала установления соединения и продолжала выполнение только после того, как это было сделано. В этой паузе задача сервера возобновляла выполнение (поскольку запрос на подключение теперь был доступен), принимала подключение, выводила сообщение и ожидала следующего клиента. Чтение и запись работают аналогичным образом. Чтобы увидеть это, рассмотрите следующий простой эхо-сервер:

julia> errormonitor(@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> errormonitor(@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"

Асинхронный ввод-вывод

Все операции ввода-вывода, предоставляемые Base.read и Base.write, могут выполняться асинхронно с помощью корутин корутины. Вы можете создать новую корутину для чтения или записи в поток с помощью макроса @async:

julia> task = @async open("foo.txt", "w") do io
           write(io, "Hello, World!")
       end;

julia> wait(task)

julia> readlines("foo.txt")
1-element Array{String,1}:
 "Hello, World!"

Часто встречаются ситуации, когда требуется выполнение нескольких асинхронных операций параллельно и ожидание их завершения. Вы можете использовать макрос @sync, чтобы заставить программу заблокироваться до тех пор, пока не завершится выполнение всех корутин, которые он обертывает:

julia> using Sockets

julia> @sync for hostname in ("google.com", "github.com", "julialang.org")
           @async begin
               conn = connect(hostname, 80)
               write(conn, "GET / HTTP/1.1\r\nHost:$(hostname)\r\n\r\n")
               readline(conn, keep=true)
               println("Finished connection to $(hostname)")
           end
       end
Finished connection to google.com
Finished connection to julialang.org
Finished connection to github.com

© 2009–2021 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.7.0/manual/networking-and-streams/

Spec-Zone.ru

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