Networking and Streams

تقدم جوليا واجهة غنية للتعامل مع كائنات الإدخال/الإخراج المتدفقة مثل المحطات، والأنابيب، ومآخذ TCP. تتيح هذه الكائنات إرسال البيانات واستقبالها بطريقة تشبه التدفق، مما يعني أنه يتم معالجة البيانات بشكل متسلسل عند توفرها. على الرغم من أن هذه الواجهة غير متزامنة على مستوى النظام، إلا أنها تُقدم بطريقة متزامنة للمبرمج. يتم تحقيق ذلك من خلال الاستخدام المكثف لوظائف خيوط التعاون في جوليا (coroutine).

Basic Stream I/O

جميع تدفقات جوليا تعرض على الأقل طريقة 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 مرة أخرى حتى تقرأ جوليا السطر الجديد. الآن، كما يمكنك أن ترى من هذا المثال، 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 ("طرفية التلكس") مخزنًا للخطوط وقد يتطلب بذلك إدخالًا إضافيًا قبل إرسال بيانات stdin إلى جوليا. عند تشغيل جوليا من سطر الأوامر في TTY، يتم إرسال المخرجات إلى وحدة التحكم بشكل افتراضي، ويتم قراءة الإدخال القياسي من لوحة المفاتيح.

لقراءة كل سطر من stdin يمكنك استخدام eachline:

for line in eachline(stdin)
    print("Found $line")
end

أو read إذا كنت تريد القراءة حسب الحرف بدلاً من ذلك:

while !eof(stdin)
    x = read(stdin, Char)
    println("Found: $x")
end

Text I/O

لاحظ أن طريقة write المذكورة أعلاه تعمل على تدفقات ثنائية. على وجه الخصوص، لا يتم تحويل القيم إلى أي تمثيل نصي قياسي ولكن يتم كتابتها كما هي:

julia> write(stdout, 0x61);  # suppress return value 1 with ;
a

لاحظ أن a يتم كتابته إلى stdout بواسطة الدالة write وأن القيمة المعادة هي 1 (حيث أن 0x61 هو بايت واحد).

لاستخدام الإدخال/الإخراج النصي، استخدم طرق print أو show، اعتمادًا على احتياجاتك (انظر الوثائق الخاصة بهاتين الطريقتين لمناقشة مفصلة حول الفرق بينهما):

julia> print(stdout, 0x61)
97

انظر Custom pretty-printing لمزيد من المعلومات حول كيفية تنفيذ طرق العرض لأنواع مخصصة.

IO Output Contextual Properties

أحيانًا يمكن أن تستفيد مخرجات IO من القدرة على تمرير معلومات سياقية إلى طرق العرض. يوفر كائن IOContext هذا الإطار لربط بيانات وصفية عشوائية بكائن IO. على سبيل المثال، :compact => true يضيف معلمة تلميحية إلى كائن IO تشير إلى أن طريقة العرض المستدعاة يجب أن تطبع مخرجات أقصر (إذا كان ذلك ممكنًا). راجع وثائق 4d61726b646f776e2e436f64652822222c2022494f436f6e746578742229_40726566 للحصول على قائمة بالخصائص الشائعة.

Working with Files

يمكنك كتابة المحتوى إلى ملف باستخدام طريقة write(filename::String, content):

julia> write("hello.txt", "Hello, World!")
13

(13 هو عدد البايتات المكتوبة.)

يمكنك قراءة محتويات ملف باستخدام طريقة read(filename::String)، أو read(filename::String, String) لقراءة المحتويات كسلسلة نصية:

julia> read("hello.txt", String)
"Hello, World!"

Advanced: streaming files

تسمح لك طرق read و write أعلاه بقراءة وكتابة محتويات الملفات. مثل العديد من البيئات الأخرى، تحتوي جوليا أيضًا على دالة 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."

إذا كنت تريد إعادة توجيه stdout إلى ملف

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)

إعادة توجيه stdout إلى ملف يمكن أن يساعدك في حفظ وتحليل مخرجات البرنامج، وأتمتة العمليات، وتلبية متطلبات الامتثال.

A simple TCP example

لنبدأ مباشرة بمثال بسيط يتضمن مآخذ 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

لأولئك الذين يعرفون واجهة برمجة تطبيقات مقبس Unix، ستبدو أسماء الطرق مألوفة، على الرغم من أن استخدامها أبسط قليلاً من واجهة برمجة تطبيقات مقبس 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\) بشكل فريد file type. الفرق بين TCP والأنابيب المسماة أو مقابس نطاق UNIX دقيق ويتعلق بـ accept و connect الطرق. طريقة 4d61726b646f776e2e436f64652822222c20226163636570742229_40726566 تسترجع اتصالاً بالعميل الذي يتصل بالخادم الذي أنشأناه للتو، بينما وظيفة 4d61726b646f776e2e436f64652822222c2022636f6e6e6563742229_40726566 تتصل بخادم باستخدام الطريقة المحددة. تأخذ وظيفة 4d61726b646f776e2e436f64652822222c2022636f6e6e6563742229_40726566 نفس المعاملات مثل listen، لذا، بافتراض أن البيئة (أي المضيف، cwd، إلخ) هي نفسها، يجب أن تكون قادرًا على تمرير نفس المعاملات إلى 4d61726b646f776e2e436f64652822222c2022636f6e6e6563742229_40726566 كما فعلت للاستماع لإنشاء الاتصال. لذا دعنا نجرب ذلك (بعد إنشاء الخادم أعلاه):

julia> connect(2000)
TCPSocket(open, 0 bytes waiting)

julia> Hello World

كما هو متوقع، رأينا "Hello World" مطبوعة. لذا، دعنا نحلل ما حدث خلف الكواليس. عندما استدعينا connect، نتصل بالخادم الذي أنشأناه للتو. في هذه الأثناء، تعيد دالة accept اتصالاً من جانب الخادم إلى المقبس الذي تم إنشاؤه حديثًا وتطبع "Hello World" للإشارة إلى أن الاتصال كان ناجحًا.

قوة كبيرة في جوليا هي أنه نظرًا لأن واجهة برمجة التطبيقات مكشوفة بشكل متزامن على الرغم من أن الإدخال/الإخراج يحدث فعليًا بشكل غير متزامن، لم نضطر للقلق بشأن ردود النداء أو حتى التأكد من أن الخادم يعمل. عندما استدعينا 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)

Resolving IP Addresses

واحدة من طرق 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"

Asynchronous I/O

جميع عمليات الإدخال / الإخراج المعروضة بواسطة Base.read و Base.write يمكن تنفيذها بشكل غير متزامن من خلال استخدام coroutines. يمكنك إنشاء Coroutine جديدة للقراءة من أو الكتابة إلى دفق باستخدام الماكرو @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

Multicast

تدعم جوليا multicast عبر IPv4 و IPv6 باستخدام بروتوكول بيانات المستخدم (UDP) كوسيلة نقل.

على عكس بروتوكول التحكم في الإرسال (TCP)، فإن بروتوكول UDP لا يقوم تقريبًا بأي افتراضات حول احتياجات التطبيق. يوفر TCP التحكم في التدفق (حيث يسرع ويبطئ لتعظيم الإنتاجية)، والموثوقية (حيث يتم إعادة إرسال الحزم المفقودة أو التالفة تلقائيًا)، والتسلسل (حيث يتم ترتيب الحزم بواسطة نظام التشغيل قبل أن تُعطى للتطبيق)، وحجم القطع، وإعداد الجلسة وإغلاقها. لا يوفر UDP أي من هذه الميزات.

استخدام شائع لـ UDP هو في تطبيقات البث المتعدد. TCP هو بروتوكول ذو حالة للتواصل بين جهازين بالضبط. يمكن لـ UDP استخدام عناوين البث المتعدد الخاصة للسماح بالتواصل المتزامن بين العديد من الأجهزة.

Receiving IP Multicast Packets

لنقل البيانات عبر البث المتعدد UDP، ببساطة recv على المقبس، وسيتم إرجاع أول حزمة تم استلامها. لاحظ أنه قد لا تكون الحزمة الأولى التي أرسلتها!

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

لإرسال البيانات عبر البث المتعدد UDP، ببساطة send إلى المقبس. لاحظ أنه ليس من الضروري أن ينضم المرسل إلى مجموعة البث المتعدد.

using Sockets
group = ip"228.5.6.7"
socket = Sockets.UDPSocket()
send(socket, group, 6789, "Hello over IPv4")
close(socket)

IPv6 Example

هذا المثال يوفر نفس الوظائف مثل البرنامج السابق، ولكنه يستخدم IPv6 كبروتوكول طبقة الشبكة.

المستمع:

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)

مرسل:

using Sockets
group = Sockets.IPv6("ff05::5:6:7")
socket = Sockets.UDPSocket()
send(socket, group, 6789, "Hello over IPv6")
close(socket)