跳轉到內容

Ruby 程式設計/參考/物件/Socket

來自華夏公益教科書

Socket 類族是 Ruby 標準庫預設使用的網路通訊方式。

典型的使用流程是建立一個 Socket

require 'socket'
a = TCPSocket.new 'some host', 80 # port 80

然後從 a 中讀取和寫入。

現在,如果您嘗試從 Socket 中讀取,它通常會阻塞,直到一些資料傳入。

incoming_string = a.recv(1024) # blocks until data becomes available, or the socket is closed.

您可以透過首先檢查是否有即將到來的資料來避免這種阻塞呼叫,為此,您使用 select 呼叫。

readable,writable,error = IO.select([a], [a], nil, 3) # blocks until the timeout occurs (3 seconds) or until a becomes readable.

此時,如果 a 有任何傳入資料,readable 將是一個類似於 [a] 的陣列,否則它將是 nil。 如果 a 可寫,則 writable 將是一個類似於 [a] 的陣列

error 通常不會使用,儘管它在某些情況下可能有用。

select 方法實際上只是對您所使用的任何作業系統的底層 select c 呼叫的封裝,儘管總體上跨平臺的語義應該相同。

另一種不阻塞讀取的方法是呼叫 a.recv_nonblock(1024),如果不可用,它將引發異常。

這是一個例子

 require 'socket'
 a = TCPSocket.new 'google.com', 80
 a.write "GET / HTTP/1.0\r\n\r\n"
 begin
   r,w,e = select([a], [w], nil) # r will contain those that are ready to be read from [if they read "" that means the socket was closed], 
                                 # w those ready to write to, e is hardly ever used--not actually sure when it is EVER used.
 rescue SystemCallError => e # this will rescue a multitude of network related errors, like Errno::ENETDOWN, etc.
 end

回顯客戶端/伺服器

[編輯 | 編輯原始碼]

server.rb(先執行此程式)

require 'socket'
a = TCPServer.new('', 3333) # '' means to bind to "all interfaces", same as nil or '0.0.0.0'
loop {
  connection = a.accept
  puts "received:" + connection.recv(1024)
  connection.write 'got something--closing now--here is your response message from the server'
  connection.close
}

client.rb

require 'socket'
a = TCPSocket.new('127.0.0.1', 3333) # could replace 127.0.0.1 with your "real" IP if desired.
a.write "hi server!"
puts "got back:" + a.recv(1024)
a.close

輸出伺服器

C:\dev>ruby server.rb
received:hi server!

輸出客戶端

C:\dev>ruby client.rb
got back:got something--closing now--here is your response message from the server

如何判斷 Socket 何時關閉

[編輯 | 編輯原始碼]

如果 BasicSocket#recv(對所有 Socket 都可用)返回 "",則表示 Socket 已從另一端關閉。 recv 呼叫引發類似“ECONNRESET”的東西意味著 Socket 已關閉,但從另一端不正常地關閉。

在沒有資料傳輸的情況下保持連線一段時間

[編輯 | 編輯原始碼]

眾所周知,TCP Socket 會一直保持活動狀態,直到它們被一方或另一方關閉。 但是,當它們處於開啟但沒有傳輸任何資料的狀態時,連線可能會變為無效。 例如,如果另一端(遠端端)已消失或已重新啟動等等。 還有可能中間的 NAT 會在一段時間不使用後超時您的連線,從而也使連線無效。 這個問題的真正問題是,如果您不傳送任何資料,並且連線無效,您只有在 *之後* 傳送資料時才會發現這一事實。 因此,通知可能會延遲。

解決此問題的方法是,定期傳送 ping 訊息或您自己的訊息,或者設定 TCP_SOCKET keepalive 選項,例如

socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)

請注意,此選項適用於已經開啟的 Socket。 另請注意,從 這裡 看來它似乎每隔兩個小時左右才會傳送一次 ping。

如果您在讀取資料時有超時(例如,您要求他們在 x 秒內向您傳送一些東西,否則您將認為他們已死),那麼您可以使用 Time.now 跟蹤最後一次輸入,然後使用 select 命令(使用合理的超時)。 然後,在每次 select 之後,處理結果,然後遍歷您的連線,檢視舊連線是否已失效,因為它們長時間沒有向您傳送資料。

設定 Socket 選項

[編輯 | 編輯原始碼]

另請注意,許多選項必須在 開啟 Socket 之前設定。

這裡 有一些程式碼演示如何設定一個。

重新整理

[編輯 | 編輯原始碼]

預設情況下,Socket 通常使用 NAGL 最佳化選項建立,這意味著在寫入後傳送資料之前會有很小的延遲,這樣如果在資料包離開之前傳送了更多的小資料,它可以將它們合併成一個數據包。

這可能會導致一組資料包中的最後一個數據包產生額外的延遲。 但透過避免多個數據包的開銷,可以帶來少許加速。

為了解決最後一個數據包的延遲問題,您要麼想在寫入後立即對您的 Socket 呼叫 #flush(最好保留 NAGL,寫入大量資料,然後呼叫 #flush),要麼設定 Socket 選項以停用 NAGL。 有關更多資訊,請參見 Nagle 演算法

UDP Socket 示例

[編輯 | 編輯原始碼]

伺服器(先執行此程式)

require 'socket'
BasicSocket.do_not_reverse_lookup = true
# Create socket and bind to address
client = UDPSocket.new
client.bind('0.0.0.0', 33333)
data, addr = client.recvfrom(1024) # if this number is too low it will drop the larger packets and never give them to you
puts "From addr: '%s', msg: '%s'" % [addr.join(','), data]
client.close

客戶端

require 'socket'
sock = UDPSocket.new
data = 'I sent this'
sock.send(data, 0, '127.0.0.1', 33333)
sock.close

UDP 廣播示例

[編輯 | 編輯原始碼]

[1]

組播示例

[編輯 | 編輯原始碼]

[2]

[3]

另請注意,來自 http://www.ruby-forum.com/topic/133208 的“在 Windows 中,setsockopt 呼叫必須在 bind 呼叫之後進行”。

替代方案

[編輯 | 編輯原始碼]

其他方法,例如使用 eventmachine gem,也提供了 Socket 程式設計,具有一些優勢,例如對許多 Socket 的更好的功能(以及一些缺點)。 Rev gem 類似,但主要用 Ruby 編寫,而不是用 C 編寫。

您可以使用套接字與纖維(即單執行緒,但多連線)一起使用,方法是使用 1.9 + revactor 或 neverblock gem。


[編輯 | 編輯原始碼]
  1. http://betterlogic.com/roger/?p=1646
  2. http://onestepback.org/index.cgi/Tech/Ruby/MulticastingInRuby.red
  3. http://www.ruby-forum.com/topic/200353
華夏公益教科書