版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進行舉報或認(rèn)領(lǐng)
文檔簡介
前 前 言|TCPSockets編程手冊指南目 錄|目目 錄|第1章 建立套接字 1Ruby的套接字庫 1創(chuàng)建首個套接字 1什么是端點 2環(huán)回地址 3IPv6 3端口 4創(chuàng)建第二個套接字 5文檔 6本章涉及的系統(tǒng)調(diào)用 7第2章 建立連接 8第3章 服務(wù)器生命周期 9服務(wù)器綁定 9該綁定到哪個端口 10該綁定到哪個地址 11服務(wù)器偵聽 12偵聽隊列 132|目 錄偵聽隊列的長度 13接受連接 14以阻塞方式接受連接 15accept調(diào)用返回一個數(shù)組 15連接類 17文件描述符 17連接地址 18accept循環(huán) 18關(guān)閉服務(wù)器 19退出時關(guān)閉 19不同的關(guān)閉方式 20Ruby包裝器 22服務(wù)器創(chuàng)建 22連接處理 24合而為一 25本章涉及的系統(tǒng)調(diào)用 25第4章 客戶端生命周期 27客戶端綁定 28客戶端連接 28Ruby包裝器 30本章涉及的系統(tǒng)調(diào)用 32第5章 交換數(shù)據(jù) 33第6章 套接字讀操作 36簡單的讀操作 36沒那么簡單 37讀取長度 38阻塞的本質(zhì) 39目 錄|3EOF事件 39部分讀取 41本章涉及的系統(tǒng)調(diào)用 43第7章 套接字寫操作 44第8章 緩沖 45寫緩沖 45該寫入多少數(shù)據(jù) 46讀緩沖 47該讀取多少數(shù)據(jù) 47第9章 第一個客戶端/服務(wù)器 49服務(wù)器 49客戶端 51投入運行 529.3分析 52第10章 套接字選項 54SO_TYPE 54SO_REUSE_ADDR 55本章涉及的系統(tǒng)調(diào)用 56第11章 非阻塞式IO 57非阻塞式讀操作 57非阻塞式寫操作 60非擁塞式接收 62非擁塞式連接 63第12章 連接復(fù)用 65select(2) 664|目 錄讀/寫之外的事件 68EOF 69accept 69connect 69高性能復(fù)用 72第13章 Nagle算法 74第14章 消息劃分 76使用新行 77使用內(nèi)容長度 79第15章 超時 81不可用的選項 81IO.select 82接受超時 83連接超時 83第16章 DNS查詢 85第17章 SSL套接字 87第18章 緊急數(shù)據(jù) 92發(fā)送緊急數(shù)據(jù) 93接受緊急數(shù)據(jù) 9318.3局限 94緊急數(shù)據(jù)和IO.select 95SO_OOBINLINE選項 96第19章 網(wǎng)絡(luò)架構(gòu)模式 97目 錄|5第20章 串行化 10120.1講解 10120.2實現(xiàn) 10120.3思考 105第21章 單連接進程 10721.1講解 10721.2實現(xiàn) 10821.3思考 21.4案例 第22章 單連接線程 22.1講解 22.2實現(xiàn) 22.3思考 22.4案例 第23章 Preforking 23.1講解 23.2實現(xiàn) 23.3思考 12323.4案例 124第24章 線程池 12524.1講解 12524.2實現(xiàn) 12524.3思考 12924.4案例 130第25章 事件驅(qū)動 1316|目 錄25.1講解 13125.2實現(xiàn) 13325.3思考 14025.4案例 142第26章 混合模式 143nginx 143Puma 144EventMachine 145第27章 結(jié)語 1471.2創(chuàng)建首個套接字|第1章1.2創(chuàng)建首個套接字|建立套接字讓我們結(jié)合例子開始套接字的學(xué)習(xí)之旅吧。Ruby的套接字庫Ruby的套接字類在默認(rèn)情況下并不會被載入,它需要使用require'socket'導(dǎo)入。其中包括了各種用于TCP套接字、UDP套接字的類,以及必要的基本類型。在本書中你會看到其中的部分內(nèi)容。socketsocket庫是Rubyopensslzlib及curses這些庫類似,socket庫與其所依賴的C語言庫之間是thinbinding關(guān)系,socket庫在多個Ruby發(fā)布版中一直都很穩(wěn)定。在創(chuàng)建套接字之前不要忘記使用require'socket'。創(chuàng)建首個套接字記住我之前的提醒,接下來讓我們著手創(chuàng)建一個套接字。2|第1章建立套接字##./code/snippets/create_socket.rbrequire'socket'socket=Socket.new(Socket::AF_INET,Socket::SOCK_STREAM)上面的代碼在INET域創(chuàng)建了一個類型為STREAM的套接字。INET是internet的縮寫,特別用于指代IPv4版本的套接字。STREAMTCP的是DGRA(datgra的類型用來告訴內(nèi)核需要創(chuàng)建什么樣的套接字。什么是端點在談到IPv4的時候,我提到了幾個新名詞。繼續(xù)新的內(nèi)容之前,讓我們先學(xué)習(xí)一下IPv4和尋址。在兩個套接字之間進行通信,就需要知道如何找到對方。這很像是打電話:如果你想和某人進行電話交流,必須知道對方的電話號碼。套接字使用IP地址將消息指向特定的主機。主機由唯一的IP地址來標(biāo)識,IP地址就是主機的“電話號碼”。上面我特別提到了IPv4地址。IPv4地址通??雌饋硐襁@樣:4個小于等于255能做什么?配置了IP地址的主機可以向另一臺同樣配置了IP地址的主機發(fā)送數(shù)據(jù)。1.5IPv6|3你知道想要與之對話的主機的地址后,就很容易想象套接字通信了,但是怎樣才能獲取到那個地址呢?需要把它背下來嗎,還是寫在紙上?謝天謝地,都不需要。你知道想要與之對話的主機的地址后,就很容易想象套接字通信了,但是怎樣才能獲取到那個地址呢?需要把它背下來嗎,還是寫在紙上?謝天謝地,都不需要。你之前可能聽說過DNS。它是一個用來將主機名映射到IP地址的隨后可以讓DNS將主機名解析成地址。即便是地址發(fā)生了變動,主機名總是能夠?qū)⒛阋蛘_的位置。真棒!IP地址電話簿環(huán)回地址IP地址未必總是指向遠(yuǎn)端主機。尤其是在研發(fā)階段,你通常需要連接自己本地主機上的套接字。多數(shù)系統(tǒng)都定義了環(huán)回接口(opckneac。和網(wǎng)卡接口不同,環(huán)回接口對應(yīng)的主機名是loclhost,對應(yīng)的P地址通常是。這些都定義在系統(tǒng)的hosts文件中。IPv6我說起過幾次IPv4,但是并沒有提過IPv6。IPv6是另一種IP地址尋址方案。4|第1章建立套接字干嘛要有兩種尋址方案?因為IPv4①IPv4由4組成,各自的范圍在0~255。每一組數(shù)字可以用8位二進制數(shù)字來表32232或43驚訝于IP地址的枯竭了。今天IPv6地址空間看起來非常大,不過隨著IPv4IPv6級別的獨立IP地址。不過大部分時間你無需手動輸入這些地址,無論使用哪種尋址方案,結(jié)果都一樣。端口對于端點而言,還有另外一個重要的方面——端口號。繼續(xù)我們那個電話的例子:如果你要和辦公樓中的某人進行通話,就得撥通他們的電話號碼,然后再撥分機號。端口號就是套接字端點的“分機號”。對于每個套接字而言,IP地址和端口號的組合必須是唯一的。所以在同一個偵聽端口上可以有兩個套接字,一個使用IPv4地址,另一個使用IPv6地址,但是這兩個套接字不能都使用同一個IPv4地址?!獆5若沒有端口號,一臺主機一次只能夠支持一個套接字。將每個活動套接字與特定的端口號結(jié)合起來,主機便可以同時支持上千個套接字。DNSDNS沒法解決這個問題,不過我們可以借助已明確定義的端口號列表。例如,HTTP默認(rèn)在端口80上進行通信,F(xiàn)TP的端口是21。實際上有一個組織負(fù)責(zé)維護這個列表。①在下一章會用到更多的端口號。我該使用哪個端口號?創(chuàng)建第二個套接字現(xiàn)在讓我們來嘗點ub提供的語法糖(snacicsuga。Ruby(而非常量:INET和:STREAM分別描述Socket::AF_INET以及Socket::SOCK_STREAM##./code/snippets/create_socket_memoized.rbrequire'socket'socket=Socket.new(:INET6,:STREAM)這段代碼創(chuàng)建了一個套接字,不過還不能同其他套接字交換數(shù)據(jù)。下一章我們會看到如何使用類似的套接字完成實際的工作。6|第1章建立套接字文檔(2)ri。下面是簡單說明。Unix手冊頁(C語言代碼RubyRubySocket.new函數(shù)命令來查看它的用法:$$man2socket注意到2沒?這告訴man程序查看手冊頁的第2節(jié)。手冊頁被劃分成若干節(jié)。節(jié)1:一般命令(shell程序。節(jié)2:系統(tǒng)調(diào)用。節(jié)3:C庫函數(shù)。4:特殊文件。5:文件格式。節(jié)7:提供了各種話題的綜述。tcp(7)就很有意思。|7我會使用這樣的語法引用手冊頁:socket(2)。它引用的是socket手冊頁的第2于多個節(jié)之中,例如stat(1)和stat(2)。如果你注意到socket(2)SEEri是Ruby命令行文檔工具。Ruby安裝程序會將安裝核心庫文檔作為整個安裝過程的一部分。Ruby的文檔相當(dāng)全面。我們可以使用下面的命令來看看Socket.new的ri文檔:$riSocket.new$riSocket.newri非常有用而且不需要連接互聯(lián)網(wǎng)。如果你需要指南或者示例,它是不錯的。1.9 本章涉及的系統(tǒng)調(diào)用每一章都會列出新介紹的系統(tǒng)調(diào)用,告訴你如何使用ri或手冊頁來獲得其更多的信息。Socket.new→socket(2)8|第5章進程皆有文件描述符第8|第5章進程皆有文件描述符建立連接TCP在兩個端點之間建立連接。端點可能處于同一臺主機,也可能位于不同的主機中。不管是哪一種情況,背后的原理都是一樣的。當(dāng)你創(chuàng)建套接字時,這個套接字必須擔(dān)任以下角色之一:(1)發(fā)起者(intitor;2)偵聽者(istener。兩種角色必不可少。少了偵聽套在網(wǎng)絡(luò)編程中,通常將從事偵聽的套接字稱作“服務(wù)器”,將發(fā)起連接的套接字稱作“客戶端”。下一章將觀察它們各自的生命周期。3.1服務(wù)器綁定|第3.1服務(wù)器綁定|服務(wù)器生命周期服務(wù)器套接字用于偵聽連接而非發(fā)起連接,其典型的生命周期如下:創(chuàng)建;綁定;偵聽;接受;關(guān)閉。我們已經(jīng)講過了“創(chuàng)建”。接下來繼續(xù)講解余下的部分。服務(wù)器綁定服務(wù)器生命周期中的第二步是綁定到監(jiān)聽連接的端口上。#./code/snippets/bind.rb#./code/snippets/bind.rbrequire'socket'#首先創(chuàng)建一個新的TCP套接字。10|第3章服務(wù)器生命周期socket=Socket.new(:INET,:STREAM)socket=Socket.new(:INET,:STREAM)#創(chuàng)建一個C結(jié)構(gòu)體來保存用于偵聽的地址。addr=Socket.pack_sockaddr_in(4481,'')#執(zhí)行綁定。socket.bind(addr)這是一個低層次實現(xiàn),演示了如何將TCP套接字綁定到本地端口上。實際上,它和用于實現(xiàn)同樣功能的C代碼幾乎一模一樣。這個套接字現(xiàn)在被綁定到本地主機的端口4481再使用此端口,否則會產(chǎn)生異常Errno::EADDRINUSE??蛻舳颂捉幼挚梢允褂迷摱丝谔栠B接服務(wù)器套接字,并建立連接??蛻舳颂捉幼蛛S后會連接到該端口。當(dāng)然了,提供了語法上的便利,你無需直接使用Socketpack_sockaddr_in或Socketbind不過在學(xué)習(xí)這些語法糖之前我們有必要避易就難地看看這一切是如何實現(xiàn)的。該綁定到哪個端口該選擇隨機端口嗎?該如何知道是否已經(jīng)有其他的程序?qū)⒛硞€端口宣為己有?|11任何在0~65535之間的端口都可以使用,但是在選用之前別忘了一些重要的約定。規(guī)則1不要使用0~1024之間的端口。這些端口是作為熟知HTTP默認(rèn)使用端口80,SMTP默認(rèn)使用端口25,rsync默認(rèn)使用端口873。綁定到這些端口通常需要root權(quán)限。規(guī)則2不要使用49000~65535之間的端口。這些都是臨時(ephemeral(connection除此之外,1025~48999之間端口的使用是一視同仁的該綁定到哪個地址.0.0.0或,又會有什么不同呢?答案和你所使用的接口有關(guān)。之前我提到過系統(tǒng)中有一個IP地址為會有另一個物理的、基于硬件的接口,使用不同的IP地址(假設(shè)是。當(dāng)你綁定到某個由P地址所描述的特定接口時,套接字就只會在該接口上進行偵聽,而忽略其他接口。——————————12|第3章服務(wù)器生命周期如果綁定到那么你的套接字就只會偵聽環(huán)回接口在種情況下只有到locahost或1270.0.1的連接才會被服務(wù)器套字接受。環(huán)回接口僅限于本地連接使用,無法用于外部連接。如果綁定到那么套接字只偵聽此接口任何尋址到個接口的客戶端都在偵聽范圍中是其他建立在localhot上的接不會被該服務(wù)器套接字接受。如果你希望偵聽每一個接口,那么可以使用。這樣會綁定到所有可用的接口、環(huán)回接口等。大多數(shù)時候,這正是你所需要的。##./code/snippets/loopback_binding.rbrequire'socket'#該套接字將會綁定在環(huán)回接口,只偵聽來自本地主機的客戶端。local_socket=Socket.new(:INET,:STREAM)local_addr=Socket.pack_sockaddr_in(4481,'')local_socket.bind(local_addr)#該套接字將會綁定在所有已知的接口,偵聽所有向其發(fā)送信息的客戶端。any_socket=Socket.new(:INET,:STREAM)any_addr=Socket.pack_sockaddr_in(4481,'')any_socket.bind(any_addr)#該套接字試圖綁定到一個未知的接口,結(jié)果導(dǎo)致Errno::EADDRNOTAVAIL。error_socket=Socket.new(:INET,:STREAM)error_addr=Socket.pack_sockaddr_in(4481,'')error_socket.bind(error_addr)服務(wù)器偵聽創(chuàng)建套接字并綁定到特定端口之后,需要告訴套接字對接入的連接進|13行偵聽。#./code/snippets/listen.rb#./code/snippets/listen.rbrequire'socket'#創(chuàng)建套接字并綁定到端口4481。socket=Socket.new(:INET,:STREAM)addr=Socket.pack_sockaddr_in(4481,'')socket.bind(addr)#告訴套接字偵聽接入的連接。socket.listen(5)和前一章代碼唯一的不同之處就是在套接字上多了一個listen調(diào)用。解釋下listen。偵聽隊列你可能注意到我們給listen方法傳遞了一個整數(shù)類型的參數(shù)。這個數(shù)字表示服務(wù)器套接字能夠容納的待處理(pending)的最大連接數(shù)。待處理的連接列表被稱作偵聽隊列。已滿,那么客戶端將會產(chǎn)生Errno::ECONNREFUSED。偵聽隊列的長度偵聽隊列的長度聽起來似乎是一個神奇的數(shù)字。為什么不把它設(shè)成1014|第3章服務(wù)器生命周期000呢?為什么還要想著拒絕某個連接呢?這些問題提得很好。我們首先來討論一下偵聽隊列長度的限制。通過在運行時查看Socket::SOMAXCONN可以獲知當(dāng)前所允許的最大的偵聽隊列長度。在我的Mac上這個數(shù)字是128root用戶可以在有需要的服務(wù)器上增加這個系統(tǒng)級別的限制。假如你運行的服務(wù)器收到了錯誤信息Error::ECONNREFUSED,那增一般來說你肯定不希望拒絕連接,可以使用server.listen(Socket::SOMAXCONN)將偵聽隊列長度設(shè)置為允許的最大值。接受連接我們終于來到了服務(wù)器實際處理接入連接的環(huán)節(jié)。這是通過accept方法實現(xiàn)的下面的代碼演示了如何創(chuàng)建偵聽套接字接受首個連接##./code/snippets/accept.rbrequire'socket'#創(chuàng)建服務(wù)器套接字。server=Socket.new(:INET,:STREAM)addr=Socket.pack_sockaddr_in(4481,'')server.bind(addr)server.listen(128)#接受連接。connection,_=server.accept3.3接受連接|15:$$echoohai|nclocalhost4481運行結(jié)果是nc(1)且Ruby程序都順利退出。最精彩的并不在于此,而在于連接已經(jīng)建立,一切工作正常。慶祝下吧!以阻塞方式接受連接accept調(diào)用是阻塞式的。在它接收到一個新的連接之前,它會一直阻塞當(dāng)前線程。還記得上一章討論過的偵聽隊列嗎還記得上一章討論過的偵聽隊列嗎?accept只不過就是將還處理的連接從隊列中彈(p而已如果隊列為空那么它就一直等,直到有連接被加入隊列為止。accept調(diào)用返回一個數(shù)組在上面的例子中,我從ccept調(diào)用中獲得了兩個返回值。acept方法實際上返回的是一個數(shù)組這個數(shù)組包含兩個元素第一個元素建立好的連接,第二個元素是一個ddrinfo對象。該對象描述了客戶端連接的遠(yuǎn)程地址。AddrinfoAddrinfo是一個RubySocket的一部分出現(xiàn)。Addrinfo16|第3章服務(wù)器生命周期可以使用可以使用Adrinfo.tcp('localhost',4481)構(gòu)建這些信息。一些有用的方法包括#ip_addess和#ip_ort。查看$riAddrinfo了解更多信息。接下來仔細(xì)查看一下#acept返回的連接和地址。##./code/snippets/accept_connection_class.rbrequire'socket'#創(chuàng)建服務(wù)器套接字。server=Socket.new(:INET,:STREAM)addr=Socket.pack_sockaddr_in(4481,'')server.bind(addr)server.listen(128)#接受一個新連接。connection,_=server.acceptprint'Connectionclass:pconnection.classprint'Serverfileno:'pserver.filenoprint'Connectionfileno:'pconnection.filenoprint'Localaddress:'pconnection.local_addressprint'Remoteaddress:'pconnection.remote_address當(dāng)服務(wù)器獲得一個連接(使用之前用過的neta命令|17ConnectionConnectionclass:Serverfileno:5Connectionfileno:8Localaddress:#<Addrinfo::4481TCP>Remoteaddress:#<Addrinfo::58164TCP>代碼輸出告訴了我們一系列TCP連接相關(guān)的處理信息。下面逐一進行分析。連接類盡連接(onnecioncas一個連接實際上就是ocket的一個實例。文件描述符我們知道acept返回一個Socket的實例,不過這個連接的文件描述符編號和服務(wù)器套接字不一樣文描述符編號是內(nèi)核用于跟蹤當(dāng)進程所打開文件的一種方法。Unix這包括文件系統(tǒng)中的文件以及管道、套接字和打印機,等等。套接字是文件嗎?這表明accet返回了一個不同于服務(wù)器套接字的全新Socket。這個Socket實例描述了特定的連接。這一點很重要。每個連接都由一個全新的Sockt對象描述,這樣服務(wù)器套接字就可以保持不變,不停地接受新的連接?!?8|第3章服務(wù)器生命周期連接地址連接對象知道兩個地址:本地地址和遠(yuǎn)程地址。其中的遠(yuǎn)程地址是accept的第二個返回值不過也可以從連接中的emote_address訪問到。連的loca_address指的是本地主機的端點,emote_address指的是另一端的端點而這個端點可能位于另一臺主機也可能存在同一臺主機(本例便是如此。每一個TCP這組唯一的組合所定義的。對于所有TCP連接而言,這4個屬性的組合必須是唯一的。遠(yuǎn)程主機的兩個連接了。accept循環(huán)#./code/snippets/naive_accept_loop.rbrequire'socket'#創(chuàng)建服務(wù)器套接字。server=Socket.new(:INET,:STREAM)accept會返回一個連接。在前面的代碼中,服務(wù)器接受了一個連接后退出在編寫真正的服務(wù)器代碼時#./code/snippets/naive_accept_loop.rbrequire'socket'#創(chuàng)建服務(wù)器套接字。server=Socket.new(:INET,:STREAM)|19addraddr=Socket.pack_sockaddr_in(4481,'')server.bind(addr)server.listen(128)#進入無限循環(huán),接受并處理連接。loopdoconnection,_=#處理連接。connection.closeend這是使用Ruby會看到一些經(jīng)由Ruby包裝過的方法。關(guān)閉服務(wù)器一旦服務(wù)器接受了某個連接并處理完畢,那么最后一件事就是關(guān)閉該連接。這就算是完成了一個連接的“創(chuàng)建處理關(guān)閉”的生命周期。我就不再貼上另一段代碼了,繼續(xù)參考上面的代碼段。在接受新的連接之前,先在之前的連接上調(diào)用close即可。退出時關(guān)閉為什么需要close描述符(包括套接字。那為什么還要自己動手去關(guān)閉呢?這有以下兩個很好的理由。Ruby20|第3章服務(wù)器生命周期器可是你的好伙伴,幫你清理用不著的連接,不過保持自己所用資源的完全控制權(quán),丟掉不再需要的東西總是一個不錯的想法。要注意的是,垃圾收集器會將它收集到的所有一切全部關(guān)閉。會出問題。要獲知當(dāng)前進程所允許打開文件的數(shù)量,你可以使用要獲知當(dāng)前進程所允許打開文件的數(shù)量,你可以使用Process.getrlimit(:NOFILE)。返回值是一個數(shù)組,包含了軟限制(戶配置的設(shè)置)和硬限制(系統(tǒng)限制。如果想將限制設(shè)置到最大值,可以使用Process.setrlimit(Process.getrlimit(:NOFILE)[1])。不同的關(guān)閉方式考慮到套接字允許雙向通信(讀/寫,實際上可以只關(guān)閉其中一個通道。#./code/snippets/close_write.rb#./code/snippets/close_write.rbrequire'socket'#創(chuàng)建服務(wù)器套接字。server=Socket.new(:INET,:STREAM)addr=Socket.pack_sockaddr_in(4481,'')server.bind(addr)server.listen(128)connection,_=|21##該連接隨后也許不再需要寫入數(shù)據(jù),但是可能仍需要進行讀取。connection.close_write#該連接不再需要進行任何數(shù)據(jù)讀寫操作。connection.close_read(write會發(fā)送一個EOF(我們很快就會講到EOF)close_write和close_ead方法在底層都利用了hton)。同close2)明顯不同的是:即便是存在著連接的副本,hton2)也可以完全關(guān)閉該連接的某一部分。可以使用可以使用Socket#dup系統(tǒng)層面上利用dup(2)復(fù)制了底層的文件描述符。不過這種情況極為罕見,你不大可能會碰上。方和當(dāng)前進程一模一樣。除了擁有當(dāng)前進程在內(nèi)存中的所有內(nèi)容之連接副本是怎么回事?和close不同,shutdown會完全關(guān)閉在當(dāng)前套接字及其副本上的通信。但是它并不會回收套接字所使用過的資源。每個套接字實例仍必22|第3章服務(wù)器生命周期須使用close結(jié)束它的生命周期。#./code/snippets/shutdown.rb#./code/snippets/shutdown.rbrequire'socket'#創(chuàng)建服務(wù)器套接字。server=Socket.new(:INET,:STREAM)addr=Socket.pack_sockaddr_in(4481,'')server.bind(addr)server.listen(128)connection,_=#創(chuàng)建連接副本。copy=connection.dup#關(guān)閉所有連接副本上的通信。connection.shutdown#關(guān)閉原始連接。副本會在垃圾收集器進行收集時關(guān)閉。connection.closeRuby包裝器我們都熟知并熱愛Ruby所提供的優(yōu)雅語法,它用來創(chuàng)建及使用服務(wù)器套接字的擴展也會讓你愛不釋手。這些便捷的方法將樣本代碼(boilerplatecode)包裝在定制的類中并盡可能地利用Ruby的語句塊。下面我們來看一下它是如何實現(xiàn)的。服務(wù)器創(chuàng)建首先是TCPServer類。它將進程中“服務(wù)器創(chuàng)建”這部分進行了非常簡潔的抽象。Ruby|23##./code/snippets/server_easy_way.rbrequire'socket'server=TCPServer.new(4481)瞧,現(xiàn)在看起來更有Ruby味兒了。這段代碼實際上是如下代碼的替換:##./code/snippets/server_hard_way.rbrequire'socket'server=Socket.new(:INET,:STREAM)addr=Socket.pack_sockaddr_in(4481,'')server.bind(addr)server.listen(5)我很清楚自己該選哪一種!創(chuàng)建一創(chuàng)建一個TCServer實例返回的實際上并不是Soket實例而是TCPServer實例。兩者的接口幾乎一樣,但還是存在一些重要的差異。其中最明顯的就是TCPServr#accept只返回連接,而不返回remoteaddress。有沒有注意到我們并沒有為這些構(gòu)造函數(shù)指定偵聽隊列的長度?因為用不著使用默認(rèn)將偵聽隊列長度。隨著IPv6IPv4和IPv6Ruby包裝器會返回兩個TCPIPv6行偵聽。24|第3章服務(wù)器生命周期#./code/snippets/server_sockets.rb#./code/snippets/server_sockets.rbrequire'socket'servers=Socket.tcp_server_sockets(4481)連接處理除了創(chuàng)建服務(wù)器,Ruby也為連接處理提供了優(yōu)美的抽象。還記得使用loop處理多個連接嗎?聰明人才不會用loop呢。應(yīng)該像下面這樣做:##./code/snippets/accept_loop.rbrequire'socket'#創(chuàng)建偵聽套接字。server=TCPServer.new(4481)#進入無限循環(huán)接受并處理連接。Socket.accept_loop(server)do|connection|#處理連接。connection.closeend要注意連接并不會在每個代碼塊結(jié)尾處自動關(guān)閉傳遞給碼塊的數(shù)和accep調(diào)用的返回值一模一樣。Socket.accept_loop還有另外一個好處你以向它傳遞多個偵套接字它可以接受在這些套接字上的全部連接這和server_sockets可謂是相得益彰:##./code/snippets/accept_server_sockets.rbrequire'socket'|25##創(chuàng)建偵聽套接字。servers=Socket.tcp_server_sockets(4481)#進入無限循環(huán),接受并處理連接。Socket.accept_loop(servers)do|connection|#處理連接。connection.closeend理。合而為一這些Ruby包裝器的集大成者是Socket.tcp_server_loop,它將之前的所有步驟合而為一:#./code/snippets/tcp_server_loop.rb#./code/snippets/tcp_server_loop.rbrequire'socket'Socket.tcp_server_loop(4481)do|connection|#處理連接。connection.closeendp的一個包裝器而已,但再也沒有比它更簡潔的寫法了。3.6 本章涉及的系統(tǒng)調(diào)用Socket#bind→bind(2)Socket#listen→listen(2)Socket#accept→accept(2)26|第3章服務(wù)器生命周期Socket#local_address→getsockname(2)Socket#remote_address→getpeername(2)Socket#close→close(2)Socket#close_write→shutdown(2)Socket#shutdown→shutdown(2)4.1客戶端綁定|第4.1客戶端綁定|客戶端生命周期道特定服務(wù)器的位置并創(chuàng)建指向外部服務(wù)器的連接。很顯然,沒有客戶端的服務(wù)器是不完整的??蛻舳说纳芷谝确?wù)器短一些。它包括以下幾個階段:創(chuàng)建;綁定;連接;關(guān)閉。第一個階段對于客戶端和服務(wù)器來說都是一樣的,所以就客戶端而言,我們從第二個階段“綁定”開始講起。28|第4章客戶端生命周期客戶端綁定客戶端套接字和服務(wù)器套接字一樣,都是以bind器部分,我們使用特定的地址和端口來調(diào)用bind不去調(diào)用bind(或者服務(wù)器套接字一個隨機端口號??蛻舳酥圆恍枰{(diào)用客戶端之所以不需要調(diào)用口訪問。而服務(wù)器要綁定到特定端口的原因是,客戶端需要通過特定的端口訪問到服務(wù)器。以FTP21FTP服務(wù)器應(yīng)該綁定到該端口,這樣客戶端就知道從哪里獲取FTP服務(wù)了??蛻舳丝梢詮娜魏味丝诎l(fā)起連接,客戶端選擇的端口號不會影響到服務(wù)器??蛻舳瞬恍枰{(diào)用bind,因為沒有人需要知道它們的端口號。為什么不調(diào)用bind?這一節(jié)并沒有展示什么代碼,因為我的建議是:不要給客戶端綁定端口!客戶端連接客戶端和服務(wù)器真正的區(qū)別就在于connect調(diào)用。該調(diào)用發(fā)起到遠(yuǎn)程套接字的連接。|29#./code/snippets/connect.rb#./code/snippets/connect.rbrequire'socket'socket=Socket.new(:INET,:STREAM)#發(fā)起到端口80的連接。remote_addr=Socket.pack_sockaddr_in(80,'')socket.connect(remote_addr)C語言結(jié)構(gòu)體的描述形式。該代碼片段從本地的臨時端口向在的端口80上進行偵聽的套接字發(fā)起TCP連接。注意我們并沒有調(diào)用bind。連接故障種情況實際上是殊途同歸。因為TCP可能等待遠(yuǎn)程主機的回應(yīng)。下面試試連接一個不可用的端點:#./code/snippets/connect_non_existent.rb#./code/snippets/connect_non_existent.rbrequire'socket'socket=Socket.new(:INET,:STREAM)。remote_addr=Socket.pack_sockaddr_in(70,'')——————————30|第4章客戶端生命周期socket.connect(remote_addr)socket.connect(remote_addr)如果你運行這段代碼,它花費很長時間才能從connect調(diào)用返回。conncet調(diào)用默認(rèn)有一段較長時間的超時。于同遠(yuǎn)端快速建立連接。但如果出現(xiàn)超時,最終會產(chǎn)生一個Errno::ETIMEOUT了。如果你對調(diào)校套接字超時感興趣,請參看第15章。當(dāng)客戶端連接到一個已經(jīng)調(diào)用過bid和liste但尚未調(diào)用ccept的服務(wù)器時,也會出現(xiàn)同樣的情況。只有遠(yuǎn)程服務(wù)器接受了連接,connect調(diào)用才會成功返回。Ruby包裝器創(chuàng)建客戶端套接字的代碼幾乎和創(chuàng)建服務(wù)器套接字一樣繁瑣、低級。如我們所愿,Ruby也對其進行了包裝,使它們更易于使用。客戶端創(chuàng)建在向你展現(xiàn)優(yōu)美的Ruby風(fēng)格的代碼之前,我要先讓你看看那些低級繁瑣的代碼,這樣才能夠有所比較:Ruby|31#./code/snippets/connect.rb#./code/snippets/connect.rbrequire'socket'socket=Socket.new(:INET,:STREAM)#發(fā)起到端口80的連接。remote_addr=Socket.pack_sockaddr_in(80,'')socket.connect(remote_addr)用了語法糖之后:##./code/snippets/client_easy_way.rbrequire'socket'socket=TCPSocket.new('',80)這下子感覺好多了。之前的三行代碼、兩個構(gòu)造函數(shù)以及大量的上下文被精簡為一個構(gòu)造函數(shù)。還有一個使用Socket.tcp碼塊的形式:#./code/snippets/client_block_form.rb#./code/snippets/client_block_form.rbrequire'socket'Socket.tcp('',80)do|connection|connection.write"GET/HTTP/1.1\r\n"connection.closeend#如果省略代碼塊參數(shù),則行為方式同TCPSocket.new()一樣。client=Socket.tcp('',80)32|第4章客戶端生命周期本章涉及的系統(tǒng)調(diào)用Socket#bind→bind(2)Socket#connect→connect(2)流|第5流|交換數(shù)據(jù)和客戶端的連接,還能夠讓它們進行數(shù)據(jù)交換。在深入學(xué)習(xí)之前,我想強調(diào),你可以將TCPTCPBerkeley套接字API決各類問題。在實際中,所有的數(shù)據(jù)都被編碼為TCP/IP界的簡單想象。34|第5章交換數(shù)據(jù)流還有一件事我需要說清楚,即TCP所具有的基于流的性質(zhì),這一點我們還沒有講過?;氐奖緯潦?,當(dāng)時我們創(chuàng)建了第一個套接字并傳入了一個叫做:STREAM的選項,該選項表明我們希望使用一個流套接字。TCP:STREAM選項,那就無法創(chuàng)建TCP套接字。那么這究竟意味著什么?這對于我們的代碼會產(chǎn)生什么影響呢?而言,TCP在網(wǎng)絡(luò)上發(fā)送的是分組。提供了一個不間斷的、有序的通信流。只有流,別無其他。讓我們用一些偽代碼進行演示。##下面的代碼會在網(wǎng)絡(luò)上發(fā)送3份數(shù)據(jù),一次一份。data=['a','b','c']forpieceindatawrite_to_connection(piece)end#下面的代碼在一次操作中讀取全部數(shù)據(jù)。result=read_from_connection#=>['a','b','c']流|35流并沒有消息邊界3它并不知道客戶端是分批發(fā)送的數(shù)據(jù)。留的。36|第6章套接字讀操作第36|第6章套接字讀操作套接字讀操作提供了一些優(yōu)雅便捷的包裝器。本章將深入學(xué)習(xí)各種讀取數(shù)據(jù)的方法以及各自的適用場景。簡單的讀操作從套接字讀取數(shù)據(jù)最簡單的方法是使用rea:##./code/snippets/read.rbrequire'socket'Socket.tcp_server_loop(4481)do|connection|#從連接中讀取數(shù)據(jù)最簡單的方法。putsconnection.read#完成讀取之后關(guān)閉連接。讓客戶端知道不用再等待數(shù)據(jù)返回。connection.closeend|37如果你在終端上運行這個例子,在另一個終端上運行下面的netcat命令,將會在ub服務(wù)器中看到輸出結(jié)果: $echogekko|nclocalhost4481 如果如果你使用過Ruby的FleI,那么這段代碼可能看起來挺眼熟。ub的各種套接字類以及Fil在I中都有一個共同的父類Rub中所有的對套接字管道文件……都有一套通用的接口,支持rea、wrte、flus等方法。等系統(tǒng)調(diào)用都可以作用于文件、套接字、管道等之上。這種抽象源自于操作系統(tǒng)核心本身。記住,一切皆為文件。沒那么簡單讀取數(shù)據(jù)的方法很簡單,但是容易出錯。如果你運行下面的netcat命令然后撒手不管服務(wù)器將永遠(yuǎn)不會停止讀取數(shù)據(jù)也永遠(yuǎn)不退出: $tail-f/var/log/system.log|nclocalhost4481 造成這種情況的原因是(end-of-fil此刻我們暫且不理會EOF,來看一種不太成熟的解決方法。這個問題的關(guān)鍵在于tal-根本就不會停止發(fā)送數(shù)據(jù)。如果tai沒有數(shù)據(jù)可以發(fā)送,它會一直等到有為止。這使得連接netcat的管道一直處于打開狀態(tài)因此netcat也永遠(yuǎn)都不會停止向服務(wù)器發(fā)送數(shù)據(jù)。38|第6章套接字讀操作服務(wù)器的red調(diào)用就一直被阻塞著直到客戶端發(fā)送完數(shù)據(jù)為止在我們的這個例子中,服務(wù)器就這樣等待……等待……一直等待在這期間它會將接收到的數(shù)據(jù)緩沖起來,不返回給應(yīng)用程序。讀取長度解決以上問題的一個方法是指定最小的讀取長度這樣就用等到戶端結(jié)束發(fā)送才停止讀取操作而告訴服務(wù)器讀rea定數(shù)據(jù)量,然后返回。##./code/snippets/read_with_length.rbrequire'socket'one_kb=1024#字節(jié)數(shù)Socket.tcp_server_loop(4481)do|connection|#以為1KB為單位進行讀取。whiledata=connection.read(one_kb)doputsdataendconnection.closeend和上一節(jié)一樣運行以下命令: $tail-f/var/log/system.log|nclocalhost4481 上面的代碼會使服務(wù)器在netca命令運行的同時以1為單位打出數(shù)據(jù)。這個例子的不同之處在于我們給red傳遞了一個整數(shù)。它告訴read6.5EOF|39在讀取了一定數(shù)量的數(shù)據(jù)后就停止讀取并返回由于希望得到所有用的數(shù)據(jù)我們在調(diào)用red方法時使用了循環(huán)直到它不再返回數(shù)為止。阻塞的本質(zhì)read調(diào)用會一直阻塞,直到獲取了完整長度(ulllngh)的數(shù)據(jù)為止在上面的例子中每次讀1B運行幾次之后應(yīng)該會清楚地發(fā)現(xiàn):如果讀取了一部分?jǐn)?shù)據(jù),但是不1B,那么rea會一直阻塞直至獲得完整1B數(shù)據(jù)為止。取1KB500B就會一直傻等著那沒發(fā)的500B!服務(wù)器采用部分讀?。╬artialread)的方式。EOF事件當(dāng)在連接上調(diào)用read并接收到F事件時,就可以確定不會再有數(shù)據(jù),可以停止讀取了。這個概念對于理解操作至關(guān)重要。先插點歷史典故:代表“ndoffil(文件結(jié)束40|第6章套接字讀操作EOF(sateevn。shutdown或close來表明EOF事件被發(fā)送給在另一端進行讀操作的進程,這樣它就知道不會再有數(shù)據(jù)到達了。讓我們從頭再來審視并解決上一節(jié)的那個問題:如果服務(wù)器希望接受1KB數(shù)據(jù),而客戶端卻只發(fā)送了500B。一種改進方法是客戶端發(fā)送EOF1KBEOF再有數(shù)據(jù)到達了。下面是正確的數(shù)據(jù)讀取代碼:##./code/snippets/read_with_length.rbrequire'socket'one_kb1024Socket.tcp_server_loop(4481)do|connection|一次讀取1KB的數(shù)據(jù)。whiledata=connection.read(one_kb)doputsdataendconnection.closeend客戶端連接:#./code/snippets/write_with_eof.rb#./code/snippets/write_with_eof.rbrequire'socket'client=TCPSocket.new('localhost',4481)|41client.write('gekko')client.closeclient.write('gekko')client.close客戶端發(fā)送EOF最簡單的方式就是關(guān)閉自己的套接字。如果套接字已經(jīng)關(guān)閉,肯定不會再發(fā)送數(shù)據(jù)了!要要說起F,它的名字還是挺恰如其分的。當(dāng)你調(diào)用Fileread時(同Sockt#read的行為方式類似,它會一直進行數(shù)據(jù)讀取直到?jīng)]有數(shù)據(jù)為止。一旦讀完整個文件,它會接收到一個F事件并返回已讀取到的數(shù)據(jù)。部分讀取是。readpartial并不會阻塞,而是立刻返回可用的數(shù)據(jù)。調(diào)用readpartial時你必傳遞一個整數(shù)作為參數(shù)來指定最大的長度readpartial最多讀取到指定長度如果你指明讀取1B數(shù)據(jù)但是客戶端只發(fā)送了50eadpartial并不會阻塞它會立刻將已讀取到的數(shù)據(jù)返回。在服務(wù)器端運行:42|第6章套接字讀操作##./code/snippets/readpartial_with_length.rbrequire'socket'one_hundred_kb=1024*100Socket.tcp_server_loop(4481)do|connection|begin#每次讀取100KB或更少。whiledata=connection.readpartial(one_hundred_kb)doputsdataendrescueEOFErrorendconnection.closeend結(jié)合以下客戶端命令:$tail-f/var/log/system.log|nclocalhost4481 從中可以看到服務(wù)器會持續(xù)讀取一切可用的數(shù)據(jù),而不是非要等足10只要有數(shù)據(jù)redpartial就會將其返回即便是小于最大長度。就而言readpartial的工作方式不同于rea當(dāng)接收到E時,read僅僅是返回,而redpartial則會產(chǎn)生一個OFError異常,提醒我們要留心。再總結(jié)一下:rea很懶惰,只會傻等著,以求返回盡可能多的數(shù)據(jù)相反,readartial更勤快,只要有可用的數(shù)據(jù)就立刻將其返回。在學(xué)習(xí)過write之后,我們會轉(zhuǎn)向緩沖區(qū)。到那個時候,我們就可以回答一些有意思的問題了,例如:我應(yīng)該一次性讀取多少數(shù)據(jù)?小讀|43取量的多次讀操作和大讀取量的單次讀操作究竟哪一種更好?6.7 本章涉及的系統(tǒng)調(diào)用Socket#read→read(2),行為類似fread(3)Socket#readpartial→read(2)44|第5章進程皆有文件描述符第44|第5章進程皆有文件描述符套接字寫操作我知道一些讀者已經(jīng)想到了:一個套接字要想讀取數(shù)據(jù),另一個套接字就必須寫入數(shù)據(jù)!恭喜你,答對了!#./code/snippets/write.rbrequire'socket'Socket.tcp_server_loop(4481)do|connection|#向連接中寫入數(shù)據(jù)的最簡單的方法。#./code/snippets/write.rbrequire'socket'Socket.tcp_server_loop(4481)do|connection|#向連接中寫入數(shù)據(jù)的最簡單的方法。connection.write('Welcome!')connection.closeend除了write調(diào)用之外就沒什么好說的了。在下一章學(xué)習(xí)緩沖區(qū)的時候,我們將會解答一些有趣的問題。本章涉及的系統(tǒng)調(diào)用Socket#write→write(2)8.1寫緩沖|第8.1寫緩沖|緩 沖在本章中,我們會解答幾個重要的問題:在一次調(diào)用中應(yīng)該讀/寫多少數(shù)據(jù)?如果write成功返回,是否意味著連接的另一端已經(jīng)接收到了數(shù)據(jù)?是否應(yīng)該將一個大數(shù)據(jù)量的write分割成多個小數(shù)據(jù)量進行多次寫入?這樣會造成怎樣的影響?寫緩沖我們先來討論在TCP連接上調(diào)用write進行寫入的時候究竟發(fā)生了什么?當(dāng)你調(diào)用write并返回時,就算是沒有引發(fā)異常,也并不代表數(shù)據(jù)已經(jīng)通過網(wǎng)絡(luò)順利發(fā)送并被客戶端套接字接收到。write返回時,它只是表明你已經(jīng)將數(shù)據(jù)提交給了Ruby的IO系統(tǒng)和底層的操作系統(tǒng)內(nèi)核。在應(yīng)用程序代碼和實際的網(wǎng)絡(luò)硬件之間至少還存在一個緩沖層。讓我們先來指明它們的具體所在,然后再來看看如何同它們打交道。46|第8章緩沖如果write成功返回,這僅能保證你的數(shù)據(jù)已經(jīng)交到了操作系統(tǒng)內(nèi)核的手中。它可以立刻發(fā)送數(shù)據(jù),也可以出于效率上的考慮暫不發(fā)送,將其同別的數(shù)據(jù)進行合并。TCP套接字默認(rèn)將sync設(shè)置為true。這就跳過了Ruby的內(nèi)部緩沖①,否則就又要多出一個緩沖層了。所有的所有的IO通過網(wǎng)絡(luò)發(fā)送數(shù)據(jù)的速度很慢write以立刻返回。然后在幕后由內(nèi)核將所有還未執(zhí)行的寫操作匯總到一起,在發(fā)送時進行分組及優(yōu)化,在實現(xiàn)最佳性能的同時避免網(wǎng)絡(luò)過載。在網(wǎng)絡(luò)層面上,發(fā)送大量的小分組會引發(fā)可觀的開銷,因此內(nèi)核會將多個小數(shù)據(jù)量的寫操作合并成較大數(shù)據(jù)量的寫操作。為何需要緩沖區(qū)?該寫入多少數(shù)據(jù)鑒于目前對于緩沖的了解,我們再次擺出這個問題:是應(yīng)該采用多個小數(shù)據(jù)量的write調(diào)用還是單個大數(shù)據(jù)量的write調(diào)用?——————————①\h/2012/09/25/ruby-io-buffers.html.②/2841832.8.4該讀取多少數(shù)據(jù)|47量的避免全部載入內(nèi)存中。一口氣寫入所有的數(shù)據(jù)讀緩沖不止是寫操作,讀操作同樣會被緩沖。如果你調(diào)用ead從TCP連接中讀取數(shù)據(jù)并給它傳遞一個最大的讀取長度,ub實際上可能會接收大于你指定長度的數(shù)據(jù)。在這種情況下“多出的”數(shù)據(jù)會被存儲在Rub內(nèi)部的讀緩沖區(qū)中在下次調(diào)用ead時,uy會先查看自己的內(nèi)部緩沖區(qū)中有沒有未讀取的數(shù)據(jù),然后再通過操作系統(tǒng)內(nèi)核請求更多的數(shù)據(jù)。該讀取多少數(shù)據(jù)這個問題的答案可不像寫緩沖那樣直觀,我們來看看涉及的問題以及最佳實踐。因為TCP提供的是數(shù)據(jù)流,我們無法得知發(fā)送方到底發(fā)送了多少數(shù)據(jù)。這就意味著在決定讀取長度的時候,我們只能靠猜測。為什么不指定一個很大的讀取長度來確??偸强梢缘玫剿锌捎玫?8|第8章緩沖如果我們指定一個較小的讀取長度,它需要多次才能夠讀取完全部的數(shù)據(jù)。這會導(dǎo)致每次系統(tǒng)調(diào)用所引發(fā)的大量開銷問題。怎么辦?那你可能得指定一個較大的讀取長度。這個問題并沒有萬能解藥,不過我偷了點懶,直接去研究了一下使用了套接字的各類Ruby項目,看看它們是如何在該問題上達成共識的。我看過Mngel、on、Pua、Passenger以及Nt::HTTP,它們無一例外地采用了readpatial(1024*16)。有些e項目都是16作為各自的讀取長度。然而,redis-rb使用1KB作為讀取長度。你總是可以通過調(diào)優(yōu)服務(wù)器來適應(yīng)當(dāng)下的數(shù)據(jù)量以獲得最佳的性能,在猶豫不定時,16KB是一個公認(rèn)比較合適的讀取長度。9.1服務(wù)器|第9章9.1服務(wù)器|/服務(wù)器下來該運用學(xué)到的知識編寫一個網(wǎng)絡(luò)服務(wù)器和客戶端了。服務(wù)器就這個服務(wù)器而言我們打算編寫一種全新的oL解決方案它作為ub散列表之上的一個網(wǎng)絡(luò)層。我們稱其為CloudHah。下面是這個簡單的ClouHash服務(wù)器的完整實現(xiàn):##./code/cloud_hash/server.rbrequire'socket'moduleclassServerdefinitialize(port)#創(chuàng)建底層的服務(wù)器套接字。@server=TCPServer.new(port)psgnt"@storage={}50|第9章第一個客戶端/服務(wù)器endenddefstart#accept循。Socket.accept_loop(@server)do|connection|handle(connection)connection.closeendenddefhandle(connection)#從連接中進行讀取,直到出現(xiàn)EOF。request=connection.read#將hah操作的結(jié)果寫回。connection.writeprocess(request)end#所支持的命令:#SETkeyvalue#GETkeydefprocess(request)command,key,value=request.splitcasecommand.upcasewhen'GET'@storage[key]when'SET'@storage[key]=endendendendserver=server.start|51客戶端下面是客戶端的完整實現(xiàn):##./code/cloud_hash/client.rbrequire'socket'moduleclassClientclass<<selfattr_accessor:host,enddefself.get(key)request"GET#{key}"enddefself.set(key,value)request"SET#{key}enddefself.request(string)#為每一個請求操作創(chuàng)建一個新連接。@client=TCPSocket.new(host,port)@client.write(string)#完成請求之后發(fā)送EOF。@client.close_write#一直讀取到EOF來獲取響應(yīng)信息。@client.readendendendCloudHash::Client.host=CloudHash::Client.port=448152|第9章第一個客戶端/服務(wù)器putsputsCloudHash::Client.set'prez','obama'putsCloudHash::Client.get'prez'putsCloudHash::Client.get'vp'投入運行接下來把它們組裝起來投入運行吧!啟動服務(wù)器:$$rubycode/cloud_hash/server.rb別忘了其中的數(shù)據(jù)結(jié)構(gòu)就是一個散列表。運行客戶端將會執(zhí)行以下操作:$$tail-4code/cloud_hash/client.rbputsCloudHash::Client.set'prez','obama'putsCloudHash::Client.get'prez'putsCloudHash::Client.get'vp'$rubycode/cloud_hash/client.rb9.3 分析我們前面都做了些什么?我們使用網(wǎng)絡(luò)I將uby散列表進行了包裝不過并沒有包裝全部的Hash而僅僅是讀寫(geter/ette)而已代碼中的不少地方都是些網(wǎng)絡(luò)編程的樣板代碼所以應(yīng)該很容易就能看出該如何擴展這個例子,以便涵蓋更多的Hash。我對代碼作了注釋,便于你搞明白程序的來龍去脈,此外我還堅持貫9.3分析|53徹了我們已經(jīng)學(xué)習(xí)過的那些概念,比如建立連接、EOF等。連接?如果你想連續(xù)發(fā)送一批請求,那么每個請求都會占用一個連并非一定要采用這種處理方式建立連接會引發(fā)開銷CludHash全可以在同一個連接上處理多個請求。/服務(wù)器可以使用一種不需要發(fā)送EOF行交互了。我們可以在服務(wù)器中加入某種形式的并發(fā)來解決這個問題本書余下的部分將以目前你學(xué)到的內(nèi)容作為基礎(chǔ),致力于幫助你編寫出高效易于理解功能完善的網(wǎng)絡(luò)程序就CloudHah本身而言并沒有很好地展示如何進行套接字編程。54|第10章套接字選項第10章54|第10章套接字選項套接字選項我們以套接字選項作為這一系列有關(guān)套接字高級技術(shù)的章節(jié)的開始。套接字選項是一種配置特定系統(tǒng)下套接字行為的低層手法。因為涉及低層設(shè)置,所以Ruby并沒有為這方面的系統(tǒng)調(diào)用提供便捷的包裝器。SO_TYPE我們先來看看如何獲得套接字類型這一套接字選項。#./code/snippets/getsockopt.rb#./code/snippets/getsockopt.rbrequire'socket'socket=TCPSocket.new('',80)獲得一個描述套接字類型的Socket::Optionopt=socket.getsockopt(Socket::SOL_SOCKET,Socket::SO_TYPE)#將描述該選項的整數(shù)值同存儲在Socket::SOCK_STREAM中的整數(shù)值進行比較。==Socket::SOCK_STREAM#=>==Socket::SOCK_DGRAM#=>getsockopt返回一個Socket::Option郵電SO_REUSE_ADDR|55郵電與返回值相關(guān)聯(lián)的底層的整數(shù)值。(了這個類型,所以將int值同各種Soket類型常量進行比較,結(jié)果發(fā)現(xiàn)這是一個STREAM套接字。記住Ruby可以寫成這樣:##./code/snippets/getsockopt_wrapper.rbrequire'socket'socket=TCPSocket.new('',80)#使用符號名而不是常量。opt=socket.getsockopt(:SOCKET,:TYPE)SO_REUSE_ADDR這是每個服務(wù)器都應(yīng)該設(shè)置的一個常見選項。SO_REUSE_ADDR選項告訴內(nèi)核:如果服務(wù)器當(dāng)前處于TCP的TIME_WAIT使用的本地地址也無妨。當(dāng)你關(guān)閉(當(dāng)你關(guān)閉(close)了某個緩沖區(qū),但其中仍有未處理數(shù)據(jù)的套接字之時就會出現(xiàn)TIME_WAIT狀態(tài)。前面曾說過,調(diào)用write只是保證數(shù)據(jù)已經(jīng)進入了緩沖層。當(dāng)你關(guān)閉一個套接字時,它未處理的數(shù)據(jù)并不會被丟棄。TIME_WAIT狀態(tài)56|第10章套接字選項在幕后,內(nèi)核使連接保持足夠長的打開時間,以便將未處理的數(shù)據(jù)發(fā)送完畢。這就意味著它必須發(fā)送數(shù)據(jù),然后等待接收方的確認(rèn),以免數(shù)據(jù)需要重傳。在幕后,內(nèi)核使連接保持足夠長的打開時間,以便將未處理的數(shù)據(jù)發(fā)送完畢。這就意味著它必須發(fā)送數(shù)據(jù),然后等待接收方的確認(rèn),以免數(shù)據(jù)需要重傳。如果關(guān)閉一個尚有數(shù)據(jù)未處理的服務(wù)器并立刻將同一個地址綁定到另一個套接字上(比如重啟服務(wù)器,則會引發(fā)一個Errno::EADDRINUSE,除非未處理的數(shù)據(jù)被丟棄掉。設(shè)置SO_REUSE_ADDR可以繞過這個問題,使你可以綁定到一個處于TIME_WAIT狀態(tài)的套接字所使用的地址上。下面是打開該選項的方法:##./code/snippets/reuseaddr.rbrequire'socket'server=TCPServer.new('localhost',4481)server.setsockopt(:SOCKET,:REUSEADDR,true)server.getsockopt(:SOCKET,:REUSEADDR)#=>true注意,TCPServer.new、Socket.tcp_server_loop及其類似的方法默認(rèn)都打開了此選項??梢酝ㄟ^setsockopt(2)查看系統(tǒng)上可用的套接字選項的完整列表。本章涉及的系統(tǒng)調(diào)用Socket#setsockopt→setsockopt(2)Socket#getsockopt→getsockopt(2)11.1非阻塞式讀操作|57第章11.1非阻塞式讀操作|57IO本章是關(guān)于非阻塞式IOIO變得清晰。非阻塞式IO同下一章要介紹的連接復(fù)用之間的關(guān)系非常緊密,不過我打算先講述前者,因為非阻塞式IO本身就已經(jīng)能獨當(dāng)一面了。11.1 非阻塞式讀操作還記得我們之前學(xué)過的ead嗎?我提到過read會一直保持阻塞,直到接收到E或是獲得指定的最小字節(jié)數(shù)為止如果客戶端沒有發(fā)送就可能會導(dǎo)致阻塞這種狀況可以通過readartial暫時解決readpartial會立即返回所有的可用數(shù)據(jù)但如果沒有數(shù)據(jù)可用那么readparial仍舊會陷入擁塞。如果需要一種不會阻塞的讀操作可以使用red_nonblock。和readparial非常類似read_nonlock需要一個整數(shù)的參數(shù)定需要讀取的最大字節(jié)數(shù)。記住red_nonblock和readparial58|第11章非阻塞式IO示如下:##./code/snippets/read_nonblock.rbrequire'socket'Socket.tcp_server_loop(4481)do|connection|loopdobeginputsconnection.read_nonblock(4096)rescueErrno::EAGAINretryrescuebreakendendconnection.closeend啟動之前用過的那個客戶端,一直保持連接打開:$$tail-f/var/log/system.log|nclocalhost4481即便沒有向服務(wù)器發(fā)送數(shù)據(jù),readnonblock調(diào)用仍然會立即返回事實上,它產(chǎn)生了一個rrno::EAGAIN異常。下面是手冊頁中關(guān)于EAGAIN的描述:文件被標(biāo)記用于非阻塞式IO,無數(shù)據(jù)可讀。原來是這么回事。這不同于readpartial,后者在這種情況下會阻塞。如果你碰上了這種錯誤該怎么辦?套接字是否會阻塞?在本例中,我|59(etr而并非正確的做法。對被阻塞的讀操作進行重試的正確做法是使用IO.select:beginbeginconnection.read_nonblock(4096)rescueErrno::EAGAINIO.select([connection])retryend上面的代碼可以實現(xiàn)的功能同之前那種堆砌了大量與retr的代碼一樣但卻去掉了不必要的循環(huán)使用套接字?jǐn)?shù)組為IO.selet調(diào)用的第一個參數(shù)將會造成阻塞到其中的某個套字變得可讀為止。所以應(yīng)該僅當(dāng)套接字有數(shù)據(jù)可讀時才調(diào)用retr在下一章我們會更細(xì)致地講解IO.slect。在本例中,我們使用非阻塞方法重新實現(xiàn)了阻塞式的read方法。這本身并沒有什么用處但是IO.select提供了一種靈活性可以在進行其他工作的同時監(jiān)控多個套接字或是定期檢查它們的可讀性。read_nonblocread_nonblock方法首先檢查Ruby的內(nèi)部緩沖區(qū)中是否還有未處理的數(shù)據(jù)。如果有,則立即返回。然后,rea_nonblock會詢問內(nèi)核是否有其他可用的數(shù)據(jù)可供select(2讀取如果答案是肯定的不管這些數(shù)據(jù)是在內(nèi)核緩沖區(qū)還是網(wǎng)絡(luò)中,它們都會被讀取并返回。其他情況都會使read(2)阻塞并在readnonblock中引發(fā)異常。什么時候讀操作會阻塞?60|第11章非阻塞式IO非阻塞式寫操作非阻塞式寫操作同我們之前看到的write調(diào)用有多處重要的不同。最明顯的一處是:write_nonblock可能會返回部分寫入的結(jié)果,而write調(diào)用總是將你發(fā)送給它的數(shù)據(jù)全部寫入。下面使用netcat啟動一個臨時服務(wù)器來演示這種行為:$$nc-llocalhost4481然后再啟動一個采用了write_nonblock的客戶端:#./code/snippets/write_nonblock.rb#./code/snippets/write_nonblock.rbrequire'socket'client=TCPSocket.new('localhost',4481)payload='Loremipsum'*10_000written=client.write_nonblock(payload)written<payload.size#=>true當(dāng)方法之所以返回是因為碰上了某種使它出現(xiàn)阻塞的情況因此也沒法進行寫入所以返回了整數(shù)值告訴我們寫入了多少數(shù)據(jù)接來我們要負(fù)責(zé)將還未發(fā)送的數(shù)據(jù)繼續(xù)寫入。write_nonblock的行為和系統(tǒng)調(diào)用write(2)Ruby的write能會多次調(diào)用write(2)寫入所有請求的數(shù)據(jù)。|61繼續(xù)寫入沒完成的部分。但別急著立刻下手。如果底層的write(2)仍處于阻塞,那你會得到一個Errno::EAGAIN異常。最終還是得靠阻塞地進行寫入。##./code/snippets/retry_partial_write.rbrequire'socket'client=TCPSocket.new('localhost',4481)payload='Loremipsum'*10_000beginloopdobytes=client.write_nonblock(payload)breakifbytes>=payload.slice!(0,bytes)IO.select(nil,[client])endrescueErrno::EAGAINIO.select(nil,[client])retryend這里我們將一個套接字?jǐn)?shù)組作為IO.select的第二個參數(shù),這樣IO.select會一直阻塞,直到其中的某個套接字可以寫入。例子中的循環(huán)語句正確地處理了部分寫操作。當(dāng)write_nonblock返后等套接字再次可寫時,重新執(zhí)行循環(huán)。62|第11章非阻塞式IO底層的底層的write(2)在下述兩種情況下會阻塞。TCPTCP使用擁塞控制算法確保網(wǎng)TCP連以免網(wǎng)絡(luò)過載。①TCP戶端可以接收更多的數(shù)據(jù)為止。什么時候?qū)懖僮鲿枞??非擁塞式接收盡管非阻塞式的rea和rite用得最多但除了它們之外別的方也有非阻塞形式。accept_nonblock和普通的accet幾乎一樣。還記不記得我當(dāng)時說過accept只是從偵聽隊列中彈出一個連接?如果偵聽隊列為空,那accept就得阻塞了。而ccept_nonblock就不會阻塞,只是產(chǎn)生一個Errno::AGAIN。①如果發(fā)送時間過長,可能是因為網(wǎng)絡(luò)出現(xiàn)了擁塞,那么這時就應(yīng)該減少數(shù)據(jù)發(fā)送量,避免加劇擁塞。——譯者注|63下面是一個例子:##./code/snippets/accept_nonblock.rbrequire'socket'server=TCPServer.new(4481)loopdobeginconnection=rescueErrno::EAGAIN#執(zhí)行其他重要的工作。retryendend11.4 非擁塞式連接看看你現(xiàn)在能不能猜出connect_nonblock有點出乎你的意料!connect_nonblock的行為和其他的非阻塞式方法有些不同。其他方法要么是完成操作,要么是產(chǎn)生一個對應(yīng)的異常,而conncet_nonblock則是保持操作繼續(xù)運行,并產(chǎn)生一個異常。如果connect_nonblockErrno::EINPROGRESS的例子:64|第11章非阻塞式IO#./code/snippets/connect_nonblock.rb#./code/snippets/connect_nonblock.rbrequire'socket'socket=Socket.new(:INET,:STREAM)remote_addr=Socket.pack_sockaddr_in(80,'')begin#在端口80向發(fā)起一個非阻塞式連接。socket.connect_nonblock(remote_addr)rescueErrno::EINPROGRESS#操作在進行中。rescueErrno::EALREADY#之前的非阻塞式連接已經(jīng)在進行當(dāng)中。rescueErrno::ECONNREFUSED#遠(yuǎn)程主機拒絕連接。end12.1select(2)|第12.1select(
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 2024牛肉供應(yīng)鏈優(yōu)化與物流配送合同
- 二零二五年鮑魚海鮮產(chǎn)品進出口合同2篇
- 2025年度中小企業(yè)財務(wù)輔導(dǎo)與融資對接服務(wù)合同3篇
- 2025年工藝品FOB出口合同標(biāo)準(zhǔn)范本2篇
- 2024年相機設(shè)備采購正式協(xié)議樣本
- 2024特定事項補充協(xié)議范本版B版
- 2025年度淋浴房安全檢測與安裝服務(wù)合同4篇
- 2025年環(huán)保型小區(qū)車棚租賃與充電樁建設(shè)合同3篇
- 2025年度綠色生態(tài)園林景觀項目苗木采購合同樣本3篇
- 2025年度消防設(shè)施設(shè)備安全性能評估合同3篇
- 軟件項目應(yīng)急措施及方案
- 2025河北邯鄲經(jīng)開國控資產(chǎn)運營管理限公司招聘專業(yè)技術(shù)人才5名高頻重點提升(共500題)附帶答案詳解
- 2024年民法典知識競賽考試題庫及答案(共50題)
- 2025老年公寓合同管理制度
- 2024-2025學(xué)年人教版數(shù)學(xué)六年級上冊 期末綜合卷(含答案)
- 2024中國汽車后市場年度發(fā)展報告
- 鈑金設(shè)備操作培訓(xùn)
- 感染性腹瀉的護理查房
- 天津市部分區(qū)2023-2024學(xué)年高二上學(xué)期期末考試 物理 含解析
- 水利工程招標(biāo)文件樣本
- 中考英語688高頻詞大綱詞頻表
評論
0/150
提交評論