شبکه و مسیرها¶
جولیا یک رابط قدرتمند برای کار کردن با اشیا مسیری ورودی/خروجی مثل ترمینالها، پایپها و سوکتهای TCP فراهم میکند. این رابط، اگر چه در سطح دستگاه غیرهمزمان است، اما به صورت همزمان به برنامهنویس ارائه میشود و معمولا هم توجه به عملیاتهای غیرهمزمان لایههای زیرین ضروری نیست. این ویژگی به وسیلهی استفادهی مکرر از رشتههای همکارانهی جولیا ([coroutine](@ref man-tasks)) حاصل میشود.
مسیر پایهای ورودی/خروجی¶
همهی مسیرهای جولیا حداقل به شکل یکی از متدهای 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
۱۱ را خروجی میدهد که برابر است با تعداد بایتهای Hello World
که در مسیر stdout
نوشته شده است ولی این مقدار خروجی به وسیلهی ;
کنترل میشود.
در اینجا انتر دوباره فشار داده شد تا جولیا خط بعدی را بخواند. حالا، همانطور که در این مثال میبینید، 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
میتوانید از 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
به وسیلهی تابع write
در stdout
نوشته شده است و مقدار خروجی برابر با 1
است (زیرا 0x61
یک بایت است).
برای ورودی/خروجی متنی بسته به نیازهایتان از متدهای print
یا show
استفاده کنید (برای دیدن این دو متد و جزئیات تفاوت بین آنها مستندات را ببینید):
julia> print(stdout, 0x61)
97
برای اطلاعات بیشتر در مورد چگونگی پیادهسازی متدهای نمایش به شکل دلخواه [نمایش دلخواه](@ref man-custom-pretty-printing) را ببینید.
ویژگیهای متنی خروجی IO¶
گاهی اوقات خروجی IO می تواند از امکان انتقال اطلاعات متنی به متدهای نمایش بهرهمند شود. شی IOContext
این بستر را برای به اشتراک گذاشتن فرادادهی دلخواه با یک شی IO فراهم میکند. برای مثال، :compact => true
یک پارامتر راهنما به شی IO اضافه میکند که روش نمایش مورد استفاده باید خروجی کوتاهتری چاپ کند (اگر چنین کاری انجامشدنی باشد). برای دیدن لیست ویژگیهای متعارف مستندات را ببینید)
کار با فایل¶
مثل بسیاری از محیطهای دیگر، جولیا دارای یک تابع 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
برای کسانی که با API سوکت Unix آشنایی دارند، نام متد آشنا خواهد بود، اگر چه استفادهی آنها قدری سادهتر از API خام سوکت Unix است. فراخوانی اول listen
سروری خواهد ساخت که آمادهی دریافت کانکشن ورودی روی یک پورت مشخص، (۲۰۰۰) در این مثال،
است. همچنین ممکن است همین تابع برای ساختن انواع مختلف سرور استفاده شود:
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 گوش نمیدهد. بلکه به یک سوکت تحت عنوان پایپ (ویندوز) یا UNIX گوش میدهد. همچنین توجه کنید که ویندوزی که تحت عنوان پایپ است یک الگوی مشخص دارد مثل اسم پیشوند (\\.\pipe\
) که به طور یکتا
نوع فایل.
را مشخص میکند.
The difference between TCP and named pipes or
UNIX domain sockets is subtle and has to do with the accept
and connect
methods. The accept
method retrieves a connection to the client that is connecting on
the server we just created, while the connect
function connects to a server using the
specified method. The connect
function takes the same arguments as listen
,
so, assuming the environment (i.e. host, cwd, etc.) is the same you should be able to pass the same
arguments to connect
as you did to listen to establish the connection. So let's try that
out (after having created the server above):
julia> connect(2000)
TCPSocket(open, 0 bytes waiting)
julia> Hello World
As expected we saw "Hello World" printed. So, let's actually analyze what happened behind the
scenes. When we called connect
, we connect to the server we had just created. Meanwhile,
the accept function returns a server-side connection to the newly created socket and prints "Hello
World" to indicate that the connection was successful.
A great strength of Julia is that since the API is exposed synchronously even though the I/O is
actually happening asynchronously, we didn't have to worry about callbacks or even making sure that
the server gets to run. When we called connect
the current task waited for the connection
to be established and only continued executing after that was done. In this pause, the server
task resumed execution (because a connection request was now available), accepted the connection,
printed the message and waited for the next client. Reading and writing works in the same way.
To see this, consider the following simple echo server:
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
As with other streams, use close
to disconnect the socket:
julia> close(clientside)
Resolving IP Addresses¶
One of the connect
methods that does not follow the listen
methods is
connect(host::String,port)
, which will attempt to connect to the host given by the host
parameter
on the port given by the port
parameter. It allows you to do things like:
julia> connect("google.com", 80)
TCPSocket(RawFD(30) open, 0 bytes waiting)
At the base of this functionality is getaddrinfo
, which will do the appropriate address
resolution:
julia> getaddrinfo("google.com")
ip"74.125.226.225"
Asynchronous I/O¶
All I/O operations exposed by Base.read
and Base.write
can be performed
asynchronously through the use of [coroutines](@ref man-tasks). You can create a new coroutine to
read from or write to a stream using the @async
macro:
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!"
It's common to run into situations where you want to perform multiple asynchronous operations
concurrently and wait until they've all completed. You can use the @sync
macro to cause
your program to block until all of the coroutines it wraps around have exited:
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