Networking and Streams
Julia bietet eine umfangreiche Schnittstelle zur Verarbeitung von Streaming-I/O-Objekten wie Terminals, Pipes und TCP-Sockets. Diese Objekte ermöglichen es, Daten in einem streamähnlichen Format zu senden und zu empfangen, was bedeutet, dass Daten sequenziell verarbeitet werden, sobald sie verfügbar sind. Diese Schnittstelle, obwohl auf Systemebene asynchron, wird dem Programmierer in einer synchronen Weise präsentiert. Dies wird erreicht, indem die Funktionalität des kooperativen Threadings von Julia (coroutine) intensiv genutzt wird.
Basic Stream I/O
Alle Julia-Streams bieten mindestens eine read
und eine write
Methode an, die den Stream als erstes Argument nimmt, z.B.:
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)
Beachten Sie, dass write
11 zurückgibt, die Anzahl der Bytes (in "Hello World"
), die in stdout
geschrieben wurden, aber dieser Rückgabewert wird mit dem ;
unterdrückt.
Hier wurde erneut die Eingabetaste gedrückt, damit Julia die neue Zeile liest. Wie Sie aus diesem Beispiel sehen können, nimmt write
die zu schreibenden Daten als zweiten Parameter, während read
den Typ der zu lesenden Daten als zweiten Parameter übernimmt.
Um beispielsweise ein einfaches Byte-Array zu lesen, könnten wir Folgendes tun:
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
Allerdings ist dies etwas umständlich, daher gibt es mehrere praktische Methoden. Zum Beispiel hätten wir das Obige so schreiben können:
julia> read(stdin, 4)
abcd
4-element Array{UInt8,1}:
0x61
0x62
0x63
0x64
oder wenn wir die gesamte Zeile lesen wollten:
julia> readline(stdin)
abcd
"abcd"
Beachten Sie, dass je nach Ihren Terminaleinstellungen Ihr TTY ("Teletype-Terminal") zeilenpuffert sein kann und daher möglicherweise ein zusätzliches Enter erforderlich ist, bevor die stdin
-Daten an Julia gesendet werden. Wenn Sie Julia von der Befehlszeile in einem TTY ausführen, wird die Ausgabe standardmäßig an die Konsole gesendet, und die Standardeingabe wird von der Tastatur gelesen.
Um jede Zeile von stdin
zu lesen, können Sie eachline
verwenden:
for line in eachline(stdin)
print("Found $line")
end
oder read
, wenn Sie es zeichenweise lesen möchten:
while !eof(stdin)
x = read(stdin, Char)
println("Found: $x")
end
Text I/O
Beachten Sie, dass die oben erwähnte write
Methode auf binären Streams arbeitet. Insbesondere werden Werte nicht in eine kanonische Textdarstellung umgewandelt, sondern so ausgegeben, wie sie sind:
julia> write(stdout, 0x61); # suppress return value 1 with ;
a
Beachten Sie, dass a
von der Funktion stdout
in write
geschrieben wird und dass der zurückgegebene Wert 1
ist (da 0x61
ein Byte ist).
Für die Text-Eingabe/Ausgabe verwenden Sie die print
oder show
Methoden, je nach Ihren Bedürfnissen (siehe die Dokumentation für diese beiden Methoden für eine detaillierte Diskussion über die Unterschiede zwischen ihnen):
julia> print(stdout, 0x61)
97
Siehe Custom pretty-printing für weitere Informationen zur Implementierung von Anzeige-Methoden für benutzerdefinierte Typen.
IO Output Contextual Properties
Manchmal kann die IO-Ausgabe von der Möglichkeit profitieren, kontextuelle Informationen in die Anzeigemethoden zu übergeben. Das IOContext
-Objekt bietet dieses Framework, um beliebige Metadaten mit einem IO-Objekt zu verknüpfen. Zum Beispiel fügt :compact => true
dem IO-Objekt einen Hinweisparameter hinzu, dass die aufgerufene Anzeigemethode eine kürzere Ausgabe drucken sollte (sofern zutreffend). Siehe die Dokumentation des 4d61726b646f776e2e436f64652822222c2022494f436f6e746578742229_40726566
für eine Liste gängiger Eigenschaften.
Working with Files
Sie können Inhalte mit der Methode write(filename::String, content)
in eine Datei schreiben:
julia> write("hello.txt", "Hello, World!")
13
(13
ist die Anzahl der geschriebenen Bytes.)
Sie können den Inhalt einer Datei mit der Methode read(filename::String)
lesen, oder read(filename::String, String)
, um den Inhalt als String zu lesen:
julia> read("hello.txt", String)
"Hello, World!"
Advanced: streaming files
Die read
- und write
-Methoden oben ermöglichen es Ihnen, den Inhalt von Dateien zu lesen und zu schreiben. Wie in vielen anderen Umgebungen hat Julia auch eine open
-Funktion, die einen Dateinamen entgegennimmt und ein IOStream
-Objekt zurückgibt, das Sie verwenden können, um Dinge aus der Datei zu lesen und zu schreiben. Zum Beispiel, wenn wir eine Datei hello.txt
haben, deren Inhalt Hello, World!
ist:
julia> f = open("hello.txt")
IOStream(<file hello.txt>)
julia> readlines(f)
1-element Array{String,1}:
"Hello, World!"
Wenn Sie in eine Datei schreiben möchten, können Sie sie mit dem Schreib- ("w"
) Flag öffnen:
julia> f = open("hello.txt","w")
IOStream(<file hello.txt>)
julia> write(f,"Hello again.")
12
Wenn Sie den Inhalt von hello.txt
zu diesem Zeitpunkt überprüfen, werden Sie feststellen, dass es leer ist; es wurde noch nichts auf die Festplatte geschrieben. Dies liegt daran, dass der IOStream
geschlossen werden muss, bevor der Schreibvorgang tatsächlich auf die Festplatte übertragen wird:
julia> close(f)
Das Überprüfen von hello.txt
erneut zeigt, dass sich der Inhalt geändert hat.
Das Öffnen einer Datei, das Bearbeiten ihres Inhalts und das anschließende Schließen ist ein sehr häufiges Muster. Um dies zu erleichtern, gibt es einen weiteren Aufruf von open
, der eine Funktion als erstes Argument und den Dateinamen als zweites Argument nimmt, die Datei öffnet, die Funktion mit der Datei als Argument aufruft und sie dann wieder schließt. Zum Beispiel, gegeben eine Funktion:
function read_and_capitalize(f::IOStream)
return uppercase(read(f, String))
end
Du kannst anrufen:
julia> open(read_and_capitalize, "hello.txt")
"HELLO AGAIN."
Um hello.txt
zu öffnen, rufen Sie read_and_capitalize
darauf auf, schließen Sie hello.txt
und geben Sie den kapitalisierten Inhalt zurück.
Um zu vermeiden, dass man überhaupt eine benannte Funktion definieren muss, kann man die do
-Syntax verwenden, die eine anonyme Funktion im Handumdrehen erstellt:
julia> open("hello.txt") do f
uppercase(read(f, String))
end
"HELLO AGAIN."
Wenn Sie stdout in eine Datei umleiten möchten
out_file = open("output.txt", "w")
# Redirect stdout to file
redirect_stdout(out_file) do
# Your code here
println("This output goes to `out_file` via the `stdout` variable.")
end
# Close file
close(out_file)
Die Umleitung von stdout in eine Datei kann Ihnen helfen, Programmausgaben zu speichern und zu analysieren, Prozesse zu automatisieren und Compliance-Anforderungen zu erfüllen.
A simple TCP example
Lass uns direkt mit einem einfachen Beispiel zu TCP-Sockets einsteigen. Diese Funktionalität befindet sich in einem Standardbibliothekspaket namens Sockets
. Lassen Sie uns zunächst einen einfachen Server erstellen:
julia> using Sockets
julia> errormonitor(@async begin
server = listen(2000)
while true
sock = accept(server)
println("Hello World\n")
end
end)
Task (runnable) @0x00007fd31dc11ae0
Für diejenigen, die mit der Unix-Socket-API vertraut sind, werden die Methodennamen vertraut erscheinen, obwohl ihre Verwendung etwas einfacher ist als bei der rohen Unix-Socket-API. Der erste Aufruf von listen
erstellt einen Server, der auf eingehende Verbindungen an dem angegebenen Port (2000) in diesem Fall wartet. Dieselbe Funktion kann auch verwendet werden, um verschiedene andere Arten von Servern zu erstellen:
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)
Beachten Sie, dass der Rückgabewert des letzten Aufrufs unterschiedlich ist. Dies liegt daran, dass dieser Server nicht über TCP hört, sondern über eine benannte Pipe (Windows) oder einen UNIX-Domänensocket. Beachten Sie auch, dass das Format der Windows-benannten Pipe einem bestimmten Muster folgen muss, sodass das Namenspräfix (\\.\pipe\
) die file type eindeutig identifiziert. Der Unterschied zwischen TCP und benannten Pipes oder UNIX-Domänensockets ist subtil und hat mit den Methoden accept
und connect
zu tun. Die Methode 4d61726b646f776e2e436f64652822222c20226163636570742229_40726566
ruft eine Verbindung zum Client ab, der sich mit dem Server verbindet, den wir gerade erstellt haben, während die Funktion 4d61726b646f776e2e436f64652822222c2022636f6e6e6563742229_40726566
eine Verbindung zu einem Server unter Verwendung der angegebenen Methode herstellt. Die Funktion 4d61726b646f776e2e436f64652822222c2022636f6e6e6563742229_40726566
nimmt die gleichen Argumente wie listen
entgegen, sodass Sie, vorausgesetzt die Umgebung (d.h. Host, cwd usw.) ist dieselbe, in der Lage sein sollten, die gleichen Argumente an 4d61726b646f776e2e436f64652822222c2022636f6e6e6563742229_40726566
zu übergeben, wie Sie es getan haben, um die Verbindung herzustellen. Lassen Sie uns das ausprobieren (nachdem wir den oben genannten Server erstellt haben):
julia> connect(2000)
TCPSocket(open, 0 bytes waiting)
julia> Hello World
Wie erwartet sahen wir "Hello World" ausgegeben. Lassen Sie uns also tatsächlich analysieren, was hinter den Kulissen passiert ist. Als wir connect
aufgerufen haben, haben wir eine Verbindung zu dem Server hergestellt, den wir gerade erstellt hatten. In der Zwischenzeit gibt die Accept-Funktion eine serverseitige Verbindung zu dem neu erstellten Socket zurück und druckt "Hello World", um anzuzeigen, dass die Verbindung erfolgreich war.
Eine große Stärke von Julia ist, dass die API synchron exponiert ist, obwohl die E/A tatsächlich asynchron erfolgt. Wir mussten uns also keine Gedanken über Rückrufe machen oder sicherstellen, dass der Server läuft. Als wir connect
aufgerufen haben, wartete die aktuelle Aufgabe darauf, dass die Verbindung hergestellt wurde, und setzte die Ausführung erst fort, nachdem dies geschehen war. In dieser Pause setzte die Serveraufgabe die Ausführung fort (da jetzt eine Verbindungsanfrage verfügbar war), akzeptierte die Verbindung, druckte die Nachricht aus und wartete auf den nächsten Client. Lesen und Schreiben funktioniert auf die gleiche Weise. Um dies zu sehen, betrachten Sie den folgenden einfachen Echo-Server:
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
Wie bei anderen Streams verwenden Sie close
, um die Verbindung zu trennen:
julia> close(clientside)
Resolving IP Addresses
Eine der connect
Methoden, die nicht den listen
Methoden folgt, ist connect(host::String,port)
, die versucht, eine Verbindung zum Host herzustellen, der durch den host
-Parameter angegeben ist, über den Port, der durch den port
-Parameter angegeben ist. Es ermöglicht Ihnen, Dinge zu tun wie:
julia> connect("google.com", 80)
TCPSocket(RawFD(30) open, 0 bytes waiting)
Am Fuße dieser Funktionalität steht getaddrinfo
, das die entsprechende Adressauflösung durchführt:
julia> getaddrinfo("google.com")
ip"74.125.226.225"
Asynchronous I/O
Alle I/O-Operationen, die von Base.read
und Base.write
bereitgestellt werden, können asynchron durch die Verwendung von coroutines durchgeführt werden. Sie können eine neue Coroutine erstellen, um von einem Stream zu lesen oder in einen Stream zu schreiben, indem Sie das @async
-Makro verwenden:
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!"
Es ist üblich, auf Situationen zu stoßen, in denen Sie mehrere asynchrone Operationen gleichzeitig ausführen und warten möchten, bis sie alle abgeschlossen sind. Sie können das @sync
-Makro verwenden, um Ihr Programm zu blockieren, bis alle Coroutinen, die es umschließt, beendet sind:
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
Multicast
Julia unterstützt multicast über IPv4 und IPv6 unter Verwendung des User Datagram Protocols (UDP) als Transport.
Im Gegensatz zum Transmission Control Protocol (TCP) trifft UDP fast keine Annahmen über die Bedürfnisse der Anwendung. TCP bietet Flusskontrolle (es beschleunigt und verlangsamt, um die Durchsatzrate zu maximieren), Zuverlässigkeit (verlorene oder beschädigte Pakete werden automatisch erneut gesendet), Sequenzierung (Pakete werden vom Betriebssystem in der Reihenfolge angeordnet, bevor sie der Anwendung übergeben werden), Segmentgröße sowie Sitzungsaufbau und -abbau. UDP bietet keine solchen Funktionen.
Ein häufiger Anwendungsfall für UDP sind Multicast-Anwendungen. TCP ist ein zustandsbehaftetes Protokoll für die Kommunikation zwischen genau zwei Geräten. UDP kann spezielle Multicast-Adressen verwenden, um eine gleichzeitige Kommunikation zwischen vielen Geräten zu ermöglichen.
Receiving IP Multicast Packets
Um Daten über UDP-Multicast zu übertragen, rufen Sie einfach recv
auf dem Socket auf, und das erste empfangene Paket wird zurückgegeben. Beachten Sie, dass es jedoch möglicherweise nicht das erste Paket ist, das Sie gesendet haben!
using Sockets
group = ip"228.5.6.7"
socket = Sockets.UDPSocket()
bind(socket, ip"0.0.0.0", 6789)
join_multicast_group(socket, group)
println(String(recv(socket)))
leave_multicast_group(socket, group)
close(socket)
Sending IP Multicast Packets
Um Daten über UDP-Multicast zu übertragen, senden Sie einfach an den Socket. Beachten Sie, dass es für einen Sender nicht erforderlich ist, der Multicast-Gruppe beizutreten.
using Sockets
group = ip"228.5.6.7"
socket = Sockets.UDPSocket()
send(socket, group, 6789, "Hello over IPv4")
close(socket)
IPv6 Example
Dieses Beispiel bietet die gleiche Funktionalität wie das vorherige Programm, verwendet jedoch IPv6 als Protokoll der Netzwerkschicht.
Zuhörer:
using Sockets
group = Sockets.IPv6("ff05::5:6:7")
socket = Sockets.UDPSocket()
bind(socket, Sockets.IPv6("::"), 6789)
join_multicast_group(socket, group)
println(String(recv(socket)))
leave_multicast_group(socket, group)
close(socket)
Absender:
using Sockets
group = Sockets.IPv6("ff05::5:6:7")
socket = Sockets.UDPSocket()
send(socket, group, 6789, "Hello over IPv6")
close(socket)