TCP Sockets編程手冊指南_第1頁
TCP Sockets編程手冊指南_第2頁
TCP Sockets編程手冊指南_第3頁
TCP Sockets編程手冊指南_第4頁
TCP Sockets編程手冊指南_第5頁
已閱讀5頁,還剩151頁未讀, 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡介

前 前 言|TCPSockets編程手冊指南目 錄|目目 錄|第1章 建立套接字 1Ruby的套接字庫 1創(chuàng)建首個(gè)套接字 1什么是端點(diǎn) 2環(huán)回地址 3IPv6 3端口 4創(chuàng)建第二個(gè)套接字 5文檔 6本章涉及的系統(tǒng)調(diào)用 7第2章 建立連接 8第3章 服務(wù)器生命周期 9服務(wù)器綁定 9該綁定到哪個(gè)端口 10該綁定到哪個(gè)地址 11服務(wù)器偵聽 12偵聽隊(duì)列 132|目 錄偵聽隊(duì)列的長度 13接受連接 14以阻塞方式接受連接 15accept調(diào)用返回一個(gè)數(shù)組 15連接類 17文件描述符 17連接地址 18accept循環(huán) 18關(guān)閉服務(wù)器 19退出時(shí)關(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章 第一個(gè)客戶端/服務(wù)器 49服務(wù)器 49客戶端 51投入運(yùn)行 529.3分析 52第10章 套接字選項(xiàng) 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章 超時(shí) 81不可用的選項(xiàng) 81IO.select 82接受超時(shí) 83連接超時(shí) 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選項(xiàng) 96第19章 網(wǎng)絡(luò)架構(gòu)模式 97目 錄|5第20章 串行化 10120.1講解 10120.2實(shí)現(xiàn) 10120.3思考 105第21章 單連接進(jìn)程 10721.1講解 10721.2實(shí)現(xiàn) 10821.3思考 21.4案例 第22章 單連接線程 22.1講解 22.2實(shí)現(xiàn) 22.3思考 22.4案例 第23章 Preforking 23.1講解 23.2實(shí)現(xiàn) 23.3思考 12323.4案例 124第24章 線程池 12524.1講解 12524.2實(shí)現(xiàn) 12524.3思考 12924.4案例 130第25章 事件驅(qū)動(dòng) 1316|目 錄25.1講解 13125.2實(shí)現(xiàn) 13325.3思考 14025.4案例 142第26章 混合模式 143nginx 143Puma 144EventMachine 145第27章 結(jié)語 1471.2創(chuàng)建首個(gè)套接字|第1章1.2創(chuàng)建首個(gè)套接字|建立套接字讓我們結(jié)合例子開始套接字的學(xué)習(xí)之旅吧。Ruby的套接字庫Ruby的套接字類在默認(rèn)情況下并不會被載入,它需要使用require'socket'導(dǎo)入。其中包括了各種用于TCP套接字、UDP套接字的類,以及必要的基本類型。在本書中你會看到其中的部分內(nèi)容。socketsocket庫是Rubyopensslzlib及curses這些庫類似,socket庫與其所依賴的C語言庫之間是thinbinding關(guān)系,socket庫在多個(gè)Ruby發(fā)布版中一直都很穩(wěn)定。在創(chuàng)建套接字之前不要忘記使用require'socket'。創(chuàng)建首個(gè)套接字記住我之前的提醒,接下來讓我們著手創(chuàng)建一個(gè)套接字。2|第1章建立套接字##./code/snippets/create_socket.rbrequire'socket'socket=Socket.new(Socket::AF_INET,Socket::SOCK_STREAM)上面的代碼在INET域創(chuàng)建了一個(gè)類型為STREAM的套接字。INET是internet的縮寫,特別用于指代IPv4版本的套接字。STREAMTCP的是DGRA(datgra的類型用來告訴內(nèi)核需要?jiǎng)?chuàng)建什么樣的套接字。什么是端點(diǎn)在談到IPv4的時(shí)候,我提到了幾個(gè)新名詞。繼續(xù)新的內(nèi)容之前,讓我們先學(xué)習(xí)一下IPv4和尋址。在兩個(gè)套接字之間進(jìn)行通信,就需要知道如何找到對方。這很像是打電話:如果你想和某人進(jìn)行電話交流,必須知道對方的電話號碼。套接字使用IP地址將消息指向特定的主機(jī)。主機(jī)由唯一的IP地址來標(biāo)識,IP地址就是主機(jī)的“電話號碼”。上面我特別提到了IPv4地址。IPv4地址通??雌饋硐襁@樣:4個(gè)小于等于255能做什么?配置了IP地址的主機(jī)可以向另一臺同樣配置了IP地址的主機(jī)發(fā)送數(shù)據(jù)。1.5IPv6|3你知道想要與之對話的主機(jī)的地址后,就很容易想象套接字通信了,但是怎樣才能獲取到那個(gè)地址呢?需要把它背下來嗎,還是寫在紙上?謝天謝地,都不需要。你知道想要與之對話的主機(jī)的地址后,就很容易想象套接字通信了,但是怎樣才能獲取到那個(gè)地址呢?需要把它背下來嗎,還是寫在紙上?謝天謝地,都不需要。你之前可能聽說過DNS。它是一個(gè)用來將主機(jī)名映射到IP地址的隨后可以讓DNS將主機(jī)名解析成地址。即便是地址發(fā)生了變動(dòng),主機(jī)名總是能夠?qū)⒛阋蛘_的位置。真棒!IP地址電話簿環(huán)回地址IP地址未必總是指向遠(yuǎn)端主機(jī)。尤其是在研發(fā)階段,你通常需要連接自己本地主機(jī)上的套接字。多數(shù)系統(tǒng)都定義了環(huán)回接口(opckneac。和網(wǎng)卡接口不同,環(huán)回接口對應(yīng)的主機(jī)名是loclhost,對應(yīng)的P地址通常是。這些都定義在系統(tǒng)的hosts文件中。IPv6我說起過幾次IPv4,但是并沒有提過IPv6。IPv6是另一種IP地址尋址方案。4|第1章建立套接字干嘛要有兩種尋址方案?因?yàn)镮Pv4①IPv4由4組成,各自的范圍在0~255。每一組數(shù)字可以用8位二進(jìn)制數(shù)字來表32232或43驚訝于IP地址的枯竭了。今天IPv6地址空間看起來非常大,不過隨著IPv4IPv6級別的獨(dú)立IP地址。不過大部分時(shí)間你無需手動(dòng)輸入這些地址,無論使用哪種尋址方案,結(jié)果都一樣。端口對于端點(diǎn)而言,還有另外一個(gè)重要的方面——端口號。繼續(xù)我們那個(gè)電話的例子:如果你要和辦公樓中的某人進(jìn)行通話,就得撥通他們的電話號碼,然后再撥分機(jī)號。端口號就是套接字端點(diǎn)的“分機(jī)號”。對于每個(gè)套接字而言,IP地址和端口號的組合必須是唯一的。所以在同一個(gè)偵聽端口上可以有兩個(gè)套接字,一個(gè)使用IPv4地址,另一個(gè)使用IPv6地址,但是這兩個(gè)套接字不能都使用同一個(gè)IPv4地址?!獆5若沒有端口號,一臺主機(jī)一次只能夠支持一個(gè)套接字。將每個(gè)活動(dòng)套接字與特定的端口號結(jié)合起來,主機(jī)便可以同時(shí)支持上千個(gè)套接字。DNSDNS沒法解決這個(gè)問題,不過我們可以借助已明確定義的端口號列表。例如,HTTP默認(rèn)在端口80上進(jìn)行通信,F(xiàn)TP的端口是21。實(shí)際上有一個(gè)組織負(fù)責(zé)維護(hù)這個(gè)列表。①在下一章會用到更多的端口號。我該使用哪個(gè)端口號?創(chuàng)建第二個(gè)套接字現(xiàn)在讓我們來嘗點(diǎ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)建了一個(gè)套接字,不過還不能同其他套接字交換數(shù)據(jù)。下一章我們會看到如何使用類似的套接字完成實(shí)際的工作。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于多個(gè)節(jié)之中,例如stat(1)和stat(2)。如果你注意到socket(2)SEEri是Ruby命令行文檔工具。Ruby安裝程序會將安裝核心庫文檔作為整個(gè)安裝過程的一部分。Ruby的文檔相當(dāng)全面。我們可以使用下面的命令來看看Socket.new的ri文檔:$riSocket.new$riSocket.newri非常有用而且不需要連接互聯(lián)網(wǎng)。如果你需要指南或者示例,它是不錯(cuò)的。1.9 本章涉及的系統(tǒng)調(diào)用每一章都會列出新介紹的系統(tǒng)調(diào)用,告訴你如何使用ri或手冊頁來獲得其更多的信息。Socket.new→socket(2)8|第5章進(jìn)程皆有文件描述符第8|第5章進(jìn)程皆有文件描述符建立連接TCP在兩個(gè)端點(diǎn)之間建立連接。端點(diǎn)可能處于同一臺主機(jī),也可能位于不同的主機(jī)中。不管是哪一種情況,背后的原理都是一樣的。當(dāng)你創(chuàng)建套接字時(shí),這個(gè)套接字必須擔(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)建一個(gè)新的TCP套接字。10|第3章服務(wù)器生命周期socket=Socket.new(:INET,:STREAM)socket=Socket.new(:INET,:STREAM)#創(chuàng)建一個(gè)C結(jié)構(gòu)體來保存用于偵聽的地址。addr=Socket.pack_sockaddr_in(4481,'')#執(zhí)行綁定。socket.bind(addr)這是一個(gè)低層次實(shí)現(xiàn),演示了如何將TCP套接字綁定到本地端口上。實(shí)際上,它和用于實(shí)現(xiàn)同樣功能的C代碼幾乎一模一樣。這個(gè)套接字現(xiàn)在被綁定到本地主機(jī)的端口4481再使用此端口,否則會產(chǎn)生異常Errno::EADDRINUSE??蛻舳颂捉幼挚梢允褂迷摱丝谔栠B接服務(wù)器套接字,并建立連接。客戶端套接字隨后會連接到該端口。當(dāng)然了,提供了語法上的便利,你無需直接使用Socketpack_sockaddr_in或Socketbind不過在學(xué)習(xí)這些語法糖之前我們有必要避易就難地看看這一切是如何實(shí)現(xiàn)的。該綁定到哪個(gè)端口該選擇隨機(jī)端口嗎?該如何知道是否已經(jīng)有其他的程序?qū)⒛硞€(gè)端口宣為己有?|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之間的端口。這些都是臨時(shí)(ephemeral(connection除此之外,1025~48999之間端口的使用是一視同仁的該綁定到哪個(gè)地址.0.0.0或,又會有什么不同呢?答案和你所使用的接口有關(guān)。之前我提到過系統(tǒng)中有一個(gè)IP地址為會有另一個(gè)物理的、基于硬件的接口,使用不同的IP地址(假設(shè)是。當(dāng)你綁定到某個(gè)由P地址所描述的特定接口時(shí),套接字就只會在該接口上進(jìn)行偵聽,而忽略其他接口。——————————12|第3章服務(wù)器生命周期如果綁定到那么你的套接字就只會偵聽環(huán)回接口在種情況下只有到locahost或1270.0.1的連接才會被服務(wù)器套字接受。環(huán)回接口僅限于本地連接使用,無法用于外部連接。如果綁定到那么套接字只偵聽此接口任何尋址到個(gè)接口的客戶端都在偵聽范圍中是其他建立在localhot上的接不會被該服務(wù)器套接字接受。如果你希望偵聽每一個(gè)接口,那么可以使用。這樣會綁定到所有可用的接口、環(huán)回接口等。大多數(shù)時(shí)候,這正是你所需要的。##./code/snippets/loopback_binding.rbrequire'socket'#該套接字將會綁定在環(huán)回接口,只偵聽來自本地主機(jī)的客戶端。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)#該套接字試圖綁定到一個(gè)未知的接口,結(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)建套接字并綁定到特定端口之后,需要告訴套接字對接入的連接進(jìn)|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)和前一章代碼唯一的不同之處就是在套接字上多了一個(gè)listen調(diào)用。解釋下listen。偵聽隊(duì)列你可能注意到我們給listen方法傳遞了一個(gè)整數(shù)類型的參數(shù)。這個(gè)數(shù)字表示服務(wù)器套接字能夠容納的待處理(pending)的最大連接數(shù)。待處理的連接列表被稱作偵聽隊(duì)列。已滿,那么客戶端將會產(chǎn)生Errno::ECONNREFUSED。偵聽隊(duì)列的長度偵聽隊(duì)列的長度聽起來似乎是一個(gè)神奇的數(shù)字。為什么不把它設(shè)成1014|第3章服務(wù)器生命周期000呢?為什么還要想著拒絕某個(gè)連接呢?這些問題提得很好。我們首先來討論一下偵聽隊(duì)列長度的限制。通過在運(yùn)行時(shí)查看Socket::SOMAXCONN可以獲知當(dāng)前所允許的最大的偵聽隊(duì)列長度。在我的Mac上這個(gè)數(shù)字是128root用戶可以在有需要的服務(wù)器上增加這個(gè)系統(tǒng)級別的限制。假如你運(yùn)行的服務(wù)器收到了錯(cuò)誤信息Error::ECONNREFUSED,那增一般來說你肯定不希望拒絕連接,可以使用server.listen(Socket::SOMAXCONN)將偵聽隊(duì)列長度設(shè)置為允許的最大值。接受連接我們終于來到了服務(wù)器實(shí)際處理接入連接的環(huán)節(jié)。這是通過accept方法實(shí)現(xiàn)的下面的代碼演示了如何創(chuàng)建偵聽套接字接受首個(gè)連接##./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運(yùn)行結(jié)果是nc(1)且Ruby程序都順利退出。最精彩的并不在于此,而在于連接已經(jīng)建立,一切工作正常。慶祝下吧!以阻塞方式接受連接accept調(diào)用是阻塞式的。在它接收到一個(gè)新的連接之前,它會一直阻塞當(dāng)前線程。還記得上一章討論過的偵聽隊(duì)列嗎還記得上一章討論過的偵聽隊(duì)列嗎?accept只不過就是將還處理的連接從隊(duì)列中彈(p而已如果隊(duì)列為空那么它就一直等,直到有連接被加入隊(duì)列為止。accept調(diào)用返回一個(gè)數(shù)組在上面的例子中,我從ccept調(diào)用中獲得了兩個(gè)返回值。acept方法實(shí)際上返回的是一個(gè)數(shù)組這個(gè)數(shù)組包含兩個(gè)元素第一個(gè)元素建立好的連接,第二個(gè)元素是一個(gè)ddrinfo對象。該對象描述了客戶端連接的遠(yuǎn)程地址。AddrinfoAddrinfo是一個(gè)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)#接受一個(gè)新連接。connection,_=server.acceptprint'Connectionclass:pconnection.classprint'Serverfileno:'pserver.filenoprint'Connectionfileno:'pconnection.filenoprint'Localaddress:'pconnection.local_addressprint'Remoteaddress:'pconnection.remote_address當(dāng)服務(wù)器獲得一個(gè)連接(使用之前用過的neta命令|17ConnectionConnectionclass:Serverfileno:5Connectionfileno:8Localaddress:#<Addrinfo::4481TCP>Remoteaddress:#<Addrinfo::58164TCP>代碼輸出告訴了我們一系列TCP連接相關(guān)的處理信息。下面逐一進(jìn)行分析。連接類盡連接(onnecioncas一個(gè)連接實(shí)際上就是ocket的一個(gè)實(shí)例。文件描述符我們知道acept返回一個(gè)Socket的實(shí)例,不過這個(gè)連接的文件描述符編號和服務(wù)器套接字不一樣文描述符編號是內(nèi)核用于跟蹤當(dāng)進(jìn)程所打開文件的一種方法。Unix這包括文件系統(tǒng)中的文件以及管道、套接字和打印機(jī),等等。套接字是文件嗎?這表明accet返回了一個(gè)不同于服務(wù)器套接字的全新Socket。這個(gè)Socket實(shí)例描述了特定的連接。這一點(diǎn)很重要。每個(gè)連接都由一個(gè)全新的Sockt對象描述,這樣服務(wù)器套接字就可以保持不變,不停地接受新的連接。——————————18|第3章服務(wù)器生命周期連接地址連接對象知道兩個(gè)地址:本地地址和遠(yuǎn)程地址。其中的遠(yuǎn)程地址是accept的第二個(gè)返回值不過也可以從連接中的emote_address訪問到。連的loca_address指的是本地主機(jī)的端點(diǎn),emote_address指的是另一端的端點(diǎn)而這個(gè)端點(diǎn)可能位于另一臺主機(jī)也可能存在同一臺主機(jī)(本例便是如此。每一個(gè)TCP這組唯一的組合所定義的。對于所有TCP連接而言,這4個(gè)屬性的組合必須是唯一的。遠(yuǎn)程主機(jī)的兩個(gè)連接了。accept循環(huán)#./code/snippets/naive_accept_loop.rbrequire'socket'#創(chuàng)建服務(wù)器套接字。server=Socket.new(:INET,:STREAM)accept會返回一個(gè)連接。在前面的代碼中,服務(wù)器接受了一個(gè)連接后退出在編寫真正的服務(wù)器代碼時(shí)#./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)#進(jìn)入無限循環(huán),接受并處理連接。loopdoconnection,_=#處理連接。connection.closeend這是使用Ruby會看到一些經(jīng)由Ruby包裝過的方法。關(guān)閉服務(wù)器一旦服務(wù)器接受了某個(gè)連接并處理完畢,那么最后一件事就是關(guān)閉該連接。這就算是完成了一個(gè)連接的“創(chuàng)建處理關(guān)閉”的生命周期。我就不再貼上另一段代碼了,繼續(xù)參考上面的代碼段。在接受新的連接之前,先在之前的連接上調(diào)用close即可。退出時(shí)關(guān)閉為什么需要close描述符(包括套接字。那為什么還要自己動(dòng)手去關(guān)閉呢?這有以下兩個(gè)很好的理由。Ruby20|第3章服務(wù)器生命周期器可是你的好伙伴,幫你清理用不著的連接,不過保持自己所用資源的完全控制權(quán),丟掉不再需要的東西總是一個(gè)不錯(cuò)的想法。要注意的是,垃圾收集器會將它收集到的所有一切全部關(guān)閉。會出問題。要獲知當(dāng)前進(jìn)程所允許打開文件的數(shù)量,你可以使用要獲知當(dāng)前進(jìn)程所允許打開文件的數(shù)量,你可以使用Process.getrlimit(:NOFILE)。返回值是一個(gè)數(shù)組,包含了軟限制(戶配置的設(shè)置)和硬限制(系統(tǒng)限制。如果想將限制設(shè)置到最大值,可以使用Process.setrlimit(Process.getrlimit(:NOFILE)[1])。不同的關(guān)閉方式考慮到套接字允許雙向通信(讀/寫,實(shí)際上可以只關(guān)閉其中一個(gè)通道。#./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ù),但是可能仍需要進(jìn)行讀取。connection.close_write#該連接不再需要進(jìn)行任何數(shù)據(jù)讀寫操作。connection.close_read(write會發(fā)送一個(gè)EOF(我們很快就會講到EOF)close_write和close_ead方法在底層都利用了hton)。同close2)明顯不同的是:即便是存在著連接的副本,hton2)也可以完全關(guān)閉該連接的某一部分。可以使用可以使用Socket#dup系統(tǒng)層面上利用dup(2)復(fù)制了底層的文件描述符。不過這種情況極為罕見,你不大可能會碰上。方和當(dāng)前進(jìn)程一模一樣。除了擁有當(dāng)前進(jìn)程在內(nèi)存中的所有內(nèi)容之連接副本是怎么回事?和close不同,shutdown會完全關(guān)閉在當(dāng)前套接字及其副本上的通信。但是它并不會回收套接字所使用過的資源。每個(gè)套接字實(shí)例仍必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)閉原始連接。副本會在垃圾收集器進(jìn)行收集時(shí)關(guān)閉。connection.closeRuby包裝器我們都熟知并熱愛Ruby所提供的優(yōu)雅語法,它用來創(chuàng)建及使用服務(wù)器套接字的擴(kuò)展也會讓你愛不釋手。這些便捷的方法將樣本代碼(boilerplatecode)包裝在定制的類中并盡可能地利用Ruby的語句塊。下面我們來看一下它是如何實(shí)現(xiàn)的。服務(wù)器創(chuàng)建首先是TCPServer類。它將進(jìn)程中“服務(wù)器創(chuàng)建”這部分進(jìn)行了非常簡潔的抽象。Ruby|23##./code/snippets/server_easy_way.rbrequire'socket'server=TCPServer.new(4481)瞧,現(xiàn)在看起來更有Ruby味兒了。這段代碼實(shí)際上是如下代碼的替換:##./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)建一個(gè)TCServer實(shí)例返回的實(shí)際上并不是Soket實(shí)例而是TCPServer實(shí)例。兩者的接口幾乎一樣,但還是存在一些重要的差異。其中最明顯的就是TCPServr#accept只返回連接,而不返回remoteaddress。有沒有注意到我們并沒有為這些構(gòu)造函數(shù)指定偵聽隊(duì)列的長度?因?yàn)橛貌恢褂媚J(rèn)將偵聽隊(duì)列長度。隨著IPv6IPv4和IPv6Ruby包裝器會返回兩個(gè)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處理多個(gè)連接嗎?聰明人才不會用loop呢。應(yīng)該像下面這樣做:##./code/snippets/accept_loop.rbrequire'socket'#創(chuàng)建偵聽套接字。server=TCPServer.new(4481)#進(jìn)入無限循環(huán)接受并處理連接。Socket.accept_loop(server)do|connection|#處理連接。connection.closeend要注意連接并不會在每個(gè)代碼塊結(jié)尾處自動(dòng)關(guān)閉傳遞給碼塊的數(shù)和accep調(diào)用的返回值一模一樣。Socket.accept_loop還有另外一個(gè)好處你以向它傳遞多個(gè)偵套接字它可以接受在這些套接字上的全部連接這和server_sockets可謂是相得益彰:##./code/snippets/accept_server_sockets.rbrequire'socket'|25##創(chuàng)建偵聽套接字。servers=Socket.tcp_server_sockets(4481)#進(jìn)入無限循環(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的一個(gè)包裝器而已,但再也沒有比它更簡潔的寫法了。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ù)器短一些。它包括以下幾個(gè)階段:創(chuàng)建;綁定;連接;關(guān)閉。第一個(gè)階段對于客戶端和服務(wù)器來說都是一樣的,所以就客戶端而言,我們從第二個(gè)階段“綁定”開始講起。28|第4章客戶端生命周期客戶端綁定客戶端套接字和服務(wù)器套接字一樣,都是以bind器部分,我們使用特定的地址和端口來調(diào)用bind不去調(diào)用bind(或者服務(wù)器套接字一個(gè)隨機(jī)端口號??蛻舳酥圆恍枰{(diào)用客戶端之所以不需要調(diào)用口訪問。而服務(wù)器要綁定到特定端口的原因是,客戶端需要通過特定的端口訪問到服務(wù)器。以FTP21FTP服務(wù)器應(yīng)該綁定到該端口,這樣客戶端就知道從哪里獲取FTP服務(wù)了??蛻舳丝梢詮娜魏味丝诎l(fā)起連接,客戶端選擇的端口號不會影響到服務(wù)器。客戶端不需要調(diào)用bind,因?yàn)闆]有人需要知道它們的端口號。為什么不調(diào)用bind?這一節(jié)并沒有展示什么代碼,因?yàn)槲业慕ㄗh是:不要給客戶端綁定端口!客戶端連接客戶端和服務(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)體的描述形式。該代碼片段從本地的臨時(shí)端口向在的端口80上進(jìn)行偵聽的套接字發(fā)起TCP連接。注意我們并沒有調(diào)用bind。連接故障種情況實(shí)際上是殊途同歸。因?yàn)門CP可能等待遠(yuǎn)程主機(jī)的回應(yīng)。下面試試連接一個(gè)不可用的端點(diǎn):#./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)如果你運(yùn)行這段代碼,它花費(fèi)很長時(shí)間才能從connect調(diào)用返回。conncet調(diào)用默認(rèn)有一段較長時(shí)間的超時(shí)。于同遠(yuǎn)端快速建立連接。但如果出現(xiàn)超時(shí),最終會產(chǎn)生一個(gè)Errno::ETIMEOUT了。如果你對調(diào)校套接字超時(shí)感興趣,請參看第15章。當(dāng)客戶端連接到一個(gè)已經(jīng)調(diào)用過bid和liste但尚未調(diào)用ccept的服務(wù)器時(shí),也會出現(xiàn)同樣的情況。只有遠(yuǎn)程服務(wù)器接受了連接,connect調(diào)用才會成功返回。Ruby包裝器創(chuàng)建客戶端套接字的代碼幾乎和創(chuàng)建服務(wù)器套接字一樣繁瑣、低級。如我們所愿,Ruby也對其進(jìn)行了包裝,使它們更易于使用??蛻舳藙?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è)構(gòu)造函數(shù)以及大量的上下文被精簡為一個(gè)構(gòu)造函數(shù)。還有一個(gè)使用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ù)和客戶端的連接,還能夠讓它們進(jìn)行數(shù)據(jù)交換。在深入學(xué)習(xí)之前,我想強(qiáng)調(diào),你可以將TCPTCPBerkeley套接字API決各類問題。在實(shí)際中,所有的數(shù)據(jù)都被編碼為TCP/IP界的簡單想象。34|第5章交換數(shù)據(jù)流還有一件事我需要說清楚,即TCP所具有的基于流的性質(zhì),這一點(diǎn)我們還沒有講過?;氐奖緯潦迹?dāng)時(shí)我們創(chuàng)建了第一個(gè)套接字并傳入了一個(gè)叫做:STREAM的選項(xiàng),該選項(xiàng)表明我們希望使用一個(gè)流套接字。TCP:STREAM選項(xiàng),那就無法創(chuàng)建TCP套接字。那么這究竟意味著什么?這對于我們的代碼會產(chǎn)生什么影響呢?而言,TCP在網(wǎng)絡(luò)上發(fā)送的是分組。提供了一個(gè)不間斷的、有序的通信流。只有流,別無其他。讓我們用一些偽代碼進(jìn)行演示。##下面的代碼會在網(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如果你在終端上運(yùn)行這個(gè)例子,在另一個(gè)終端上運(yùn)行下面的netcat命令,將會在ub服務(wù)器中看到輸出結(jié)果: $echogekko|nclocalhost4481 如果如果你使用過Ruby的FleI,那么這段代碼可能看起來挺眼熟。ub的各種套接字類以及Fil在I中都有一個(gè)共同的父類Rub中所有的對套接字管道文件……都有一套通用的接口,支持rea、wrte、flus等方法。等系統(tǒng)調(diào)用都可以作用于文件、套接字、管道等之上。這種抽象源自于操作系統(tǒng)核心本身。記住,一切皆為文件。沒那么簡單讀取數(shù)據(jù)的方法很簡單,但是容易出錯(cuò)。如果你運(yùn)行下面的netcat命令然后撒手不管服務(wù)器將永遠(yuǎn)不會停止讀取數(shù)據(jù)也永遠(yuǎn)不退出: $tail-f/var/log/system.log|nclocalhost4481 造成這種情況的原因是(end-of-fil此刻我們暫且不理會EOF,來看一種不太成熟的解決方法。這個(gè)問題的關(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ù)為止在我們的這個(gè)例子中,服務(wù)器就這樣等待……等待……一直等待在這期間它會將接收到的數(shù)據(jù)緩沖起來,不返回給應(yīng)用程序。讀取長度解決以上問題的一個(gè)方法是指定最小的讀取長度這樣就用等到戶端結(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為單位進(jìn)行讀取。whiledata=connection.read(one_kb)doputsdataendconnection.closeend和上一節(jié)一樣運(yùn)行以下命令: $tail-f/var/log/system.log|nclocalhost4481 上面的代碼會使服務(wù)器在netca命令運(yùn)行的同時(shí)以1為單位打出數(shù)據(jù)。這個(gè)例子的不同之處在于我們給red傳遞了一個(gè)整數(shù)。它告訴read6.5EOF|39在讀取了一定數(shù)量的數(shù)據(jù)后就停止讀取并返回由于希望得到所有用的數(shù)據(jù)我們在調(diào)用red方法時(shí)使用了循環(huán)直到它不再返回?cái)?shù)為止。阻塞的本質(zhì)read調(diào)用會一直阻塞,直到獲取了完整長度(ulllngh)的數(shù)據(jù)為止在上面的例子中每次讀1B運(yùn)行幾次之后應(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í),就可以確定不會再有數(shù)據(jù),可以停止讀取了。這個(gè)概念對于理解操作至關(guān)重要。先插點(diǎn)歷史典故:代表“ndoffil(文件結(jié)束40|第6章套接字讀操作EOF(sateevn。shutdown或close來表明EOF事件被發(fā)送給在另一端進(jìn)行讀操作的進(jìn)程,這樣它就知道不會再有數(shù)據(jù)到達(dá)了。讓我們從頭再來審視并解決上一節(jié)的那個(gè)問題:如果服務(wù)器希望接受1KB數(shù)據(jù),而客戶端卻只發(fā)送了500B。一種改進(jìn)方法是客戶端發(fā)送EOF1KBEOF再有數(shù)據(jù)到達(dá)了。下面是正確的數(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時(shí)(同Sockt#read的行為方式類似,它會一直進(jìn)行數(shù)據(jù)讀取直到?jīng)]有數(shù)據(jù)為止。一旦讀完整個(gè)文件,它會接收到一個(gè)F事件并返回已讀取到的數(shù)據(jù)。部分讀取是。readpartial并不會阻塞,而是立刻返回可用的數(shù)據(jù)。調(diào)用readpartial時(shí)你必傳遞一個(gè)整數(shù)作為參數(shù)來指定最大的長度readpartial最多讀取到指定長度如果你指明讀取1B數(shù)據(jù)但是客戶端只發(fā)送了50eadpartial并不會阻塞它會立刻將已讀取到的數(shù)據(jù)返回。在服務(wù)器端運(yùn)行: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時(shí),read僅僅是返回,而redpartial則會產(chǎn)生一個(gè)OFError異常,提醒我們要留心。再總結(jié)一下:rea很懶惰,只會傻等著,以求返回盡可能多的數(shù)據(jù)相反,readartial更勤快,只要有可用的數(shù)據(jù)就立刻將其返回。在學(xué)習(xí)過write之后,我們會轉(zhuǎn)向緩沖區(qū)。到那個(gè)時(shí)候,我們就可以回答一些有意思的問題了,例如:我應(yīng)該一次性讀取多少數(shù)據(jù)?小讀|43取量的多次讀操作和大讀取量的單次讀操作究竟哪一種更好?6.7 本章涉及的系統(tǒng)調(diào)用Socket#read→read(2),行為類似fread(3)Socket#readpartial→read(2)44|第5章進(jìn)程皆有文件描述符第44|第5章進(jìn)程皆有文件描述符套接字寫操作我知道一些讀者已經(jīng)想到了:一個(gè)套接字要想讀取數(shù)據(jù),另一個(gè)套接字就必須寫入數(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ū)的時(shí)候,我們將會解答一些有趣的問題。本章涉及的系統(tǒng)調(diào)用Socket#write→write(2)8.1寫緩沖|第8.1寫緩沖|緩 沖在本章中,我們會解答幾個(gè)重要的問題:在一次調(diào)用中應(yīng)該讀/寫多少數(shù)據(jù)?如果write成功返回,是否意味著連接的另一端已經(jīng)接收到了數(shù)據(jù)?是否應(yīng)該將一個(gè)大數(shù)據(jù)量的write分割成多個(gè)小數(shù)據(jù)量進(jìn)行多次寫入?這樣會造成怎樣的影響?寫緩沖我們先來討論在TCP連接上調(diào)用write進(jìn)行寫入的時(shí)候究竟發(fā)生了什么?當(dāng)你調(diào)用write并返回時(shí),就算是沒有引發(fā)異常,也并不代表數(shù)據(jù)已經(jīng)通過網(wǎng)絡(luò)順利發(fā)送并被客戶端套接字接收到。write返回時(shí),它只是表明你已經(jīng)將數(shù)據(jù)提交給了Ruby的IO系統(tǒng)和底層的操作系統(tǒng)內(nèi)核。在應(yīng)用程序代碼和實(shí)際的網(wǎng)絡(luò)硬件之間至少還存在一個(gè)緩沖層。讓我們先來指明它們的具體所在,然后再來看看如何同它們打交道。46|第8章緩沖如果write成功返回,這僅能保證你的數(shù)據(jù)已經(jīng)交到了操作系統(tǒng)內(nèi)核的手中。它可以立刻發(fā)送數(shù)據(jù),也可以出于效率上的考慮暫不發(fā)送,將其同別的數(shù)據(jù)進(jìn)行合并。TCP套接字默認(rèn)將sync設(shè)置為true。這就跳過了Ruby的內(nèi)部緩沖①,否則就又要多出一個(gè)緩沖層了。所有的所有的IO通過網(wǎng)絡(luò)發(fā)送數(shù)據(jù)的速度很慢write以立刻返回。然后在幕后由內(nèi)核將所有還未執(zhí)行的寫操作匯總到一起,在發(fā)送時(shí)進(jìn)行分組及優(yōu)化,在實(shí)現(xiàn)最佳性能的同時(shí)避免網(wǎng)絡(luò)過載。在網(wǎng)絡(luò)層面上,發(fā)送大量的小分組會引發(fā)可觀的開銷,因此內(nèi)核會將多個(gè)小數(shù)據(jù)量的寫操作合并成較大數(shù)據(jù)量的寫操作。為何需要緩沖區(qū)?該寫入多少數(shù)據(jù)鑒于目前對于緩沖的了解,我們再次擺出這個(gè)問題:是應(yīng)該采用多個(gè)小數(shù)據(jù)量的write調(diào)用還是單個(gè)大數(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ù)并給它傳遞一個(gè)最大的讀取長度,ub實(shí)際上可能會接收大于你指定長度的數(shù)據(jù)。在這種情況下“多出的”數(shù)據(jù)會被存儲在Rub內(nèi)部的讀緩沖區(qū)中在下次調(diào)用ead時(shí),uy會先查看自己的內(nèi)部緩沖區(qū)中有沒有未讀取的數(shù)據(jù),然后再通過操作系統(tǒng)內(nèi)核請求更多的數(shù)據(jù)。該讀取多少數(shù)據(jù)這個(gè)問題的答案可不像寫緩沖那樣直觀,我們來看看涉及的問題以及最佳實(shí)踐。因?yàn)門CP提供的是數(shù)據(jù)流,我們無法得知發(fā)送方到底發(fā)送了多少數(shù)據(jù)。這就意味著在決定讀取長度的時(shí)候,我們只能靠猜測。為什么不指定一個(gè)很大的讀取長度來確??偸强梢缘玫剿锌捎玫?8|第8章緩沖如果我們指定一個(gè)較小的讀取長度,它需要多次才能夠讀取完全部的數(shù)據(jù)。這會導(dǎo)致每次系統(tǒng)調(diào)用所引發(fā)的大量開銷問題。怎么辦?那你可能得指定一個(gè)較大的讀取長度。這個(gè)問題并沒有萬能解藥,不過我偷了點(diǎn)懶,直接去研究了一下使用了套接字的各類Ruby項(xiàng)目,看看它們是如何在該問題上達(dá)成共識的。我看過Mngel、on、Pua、Passenger以及Nt::HTTP,它們無一例外地采用了readpatial(1024*16)。有些e項(xiàng)目都是16作為各自的讀取長度。然而,redis-rb使用1KB作為讀取長度。你總是可以通過調(diào)優(yōu)服務(wù)器來適應(yīng)當(dāng)下的數(shù)據(jù)量以獲得最佳的性能,在猶豫不定時(shí),16KB是一個(gè)公認(rèn)比較合適的讀取長度。9.1服務(wù)器|第9章9.1服務(wù)器|/服務(wù)器下來該運(yùn)用學(xué)到的知識編寫一個(gè)網(wǎng)絡(luò)服務(wù)器和客戶端了。服務(wù)器就這個(gè)服務(wù)器而言我們打算編寫一種全新的oL解決方案它作為ub散列表之上的一個(gè)網(wǎng)絡(luò)層。我們稱其為CloudHah。下面是這個(gè)簡單的ClouHash服務(wù)器的完整實(shí)現(xiàn):##./code/cloud_hash/server.rbrequire'socket'moduleclassServerdefinitialize(port)#創(chuàng)建底層的服務(wù)器套接字。@server=TCPServer.new(port)psgnt"@storage={}50|第9章第一個(gè)客戶端/服務(wù)器endenddefstart#accept循。Socket.accept_loop(@server)do|connection|handle(connection)connection.closeendenddefhandle(connection)#從連接中進(jìn)行讀取,直到出現(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客戶端下面是客戶端的完整實(shí)現(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)#為每一個(gè)請求操作創(chuàng)建一個(gè)新連接。@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章第一個(gè)客戶端/服務(wù)器putsputsCloudHash::Client.set'prez','obama'putsCloudHash::Client.get'prez'putsCloudHash::Client.get'vp'投入運(yùn)行接下來把它們組裝起來投入運(yùn)行吧!啟動(dòng)服務(wù)器:$$rubycode/cloud_hash/server.rb別忘了其中的數(shù)據(jù)結(jié)構(gòu)就是一個(gè)散列表。運(yùn)行客戶端將會執(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散列表進(jìn)行了包裝不過并沒有包裝全部的Hash而僅僅是讀寫(geter/ette)而已代碼中的不少地方都是些網(wǎng)絡(luò)編程的樣板代碼所以應(yīng)該很容易就能看出該如何擴(kuò)展這個(gè)例子,以便涵蓋更多的Hash。我對代碼作了注釋,便于你搞明白程序的來龍去脈,此外我還堅(jiān)持貫9.3分析|53徹了我們已經(jīng)學(xué)習(xí)過的那些概念,比如建立連接、EOF等。連接?如果你想連續(xù)發(fā)送一批請求,那么每個(gè)請求都會占用一個(gè)連并非一定要采用這種處理方式建立連接會引發(fā)開銷CludHash全可以在同一個(gè)連接上處理多個(gè)請求。/服務(wù)器可以使用一種不需要發(fā)送EOF行交互了。我們可以在服務(wù)器中加入某種形式的并發(fā)來解決這個(gè)問題本書余下的部分將以目前你學(xué)到的內(nèi)容作為基礎(chǔ),致力于幫助你編寫出高效易于理解功能完善的網(wǎng)絡(luò)程序就CloudHah本身而言并沒有很好地展示如何進(jìn)行套接字編程。54|第10章套接字選項(xiàng)第10章54|第10章套接字選項(xiàng)套接字選項(xiàng)我們以套接字選項(xiàng)作為這一系列有關(guān)套接字高級技術(shù)的章節(jié)的開始。套接字選項(xiàng)是一種配置特定系統(tǒng)下套接字行為的低層手法。因?yàn)樯婕暗蛯釉O(shè)置,所以Ruby并沒有為這方面的系統(tǒng)調(diào)用提供便捷的包裝器。SO_TYPE我們先來看看如何獲得套接字類型這一套接字選項(xiàng)。#./code/snippets/getsockopt.rb#./code/snippets/getsockopt.rbrequire'socket'socket=TCPSocket.new('',80)獲得一個(gè)描述套接字類型的Socket::Optionopt=socket.getsockopt(Socket::SOL_SOCKET,Socket::SO_TYPE)#將描述該選項(xiàng)的整數(shù)值同存儲在Socket::SOCK_STREAM中的整數(shù)值進(jìn)行比較。==Socket::SOCK_STREAM#=>==Socket::SOCK_DGRAM#=>getsockopt返回一個(gè)Socket::Option郵電SO_REUSE_ADDR|55郵電與返回值相關(guān)聯(lián)的底層的整數(shù)值。(了這個(gè)類型,所以將int值同各種Soket類型常量進(jìn)行比較,結(jié)果發(fā)現(xiàn)這是一個(gè)STREAM套接字。記住Ruby可以寫成這樣:##./code/snippets/getsockopt_wrapper.rbrequire'socket'socket=TCPSocket.new('',80)#使用符號名而不是常量。opt=socket.getsockopt(:SOCKET,:TYPE)SO_REUSE_ADDR這是每個(gè)服務(wù)器都應(yīng)該設(shè)置的一個(gè)常見選項(xiàng)。SO_REUSE_ADDR選項(xiàng)告訴內(nèi)核:如果服務(wù)器當(dāng)前處于TCP的TIME_WAIT使用的本地地址也無妨。當(dāng)你關(guān)閉(當(dāng)你關(guān)閉(close)了某個(gè)緩沖區(qū),但其中仍有未處理數(shù)據(jù)的套接字之時(shí)就會出現(xiàn)TIME_WAIT狀態(tài)。前面曾說過,調(diào)用write只是保證數(shù)據(jù)已經(jīng)進(jìn)入了緩沖層。當(dāng)你關(guān)閉一個(gè)套接字時(shí),它未處理的數(shù)據(jù)并不會被丟棄。TIME_WAIT狀態(tài)56|第10章套接字選項(xiàng)在幕后,內(nèi)核使連接保持足夠長的打開時(shí)間,以便將未處理的數(shù)據(jù)發(fā)送完畢。這就意味著它必須發(fā)送數(shù)據(jù),然后等待接收方的確認(rèn),以免數(shù)據(jù)需要重傳。在幕后,內(nèi)核使連接保持足夠長的打開時(shí)間,以便將未處理的數(shù)據(jù)發(fā)送完畢。這就意味著它必須發(fā)送數(shù)據(jù),然后等待接收方的確認(rèn),以免數(shù)據(jù)需要重傳。如果關(guān)閉一個(gè)尚有數(shù)據(jù)未處理的服務(wù)器并立刻將同一個(gè)地址綁定到另一個(gè)套接字上(比如重啟服務(wù)器,則會引發(fā)一個(gè)Errno::EADDRINUSE,除非未處理的數(shù)據(jù)被丟棄掉。設(shè)置SO_REUSE_ADDR可以繞過這個(gè)問題,使你可以綁定到一個(gè)處于TIME_WAIT狀態(tài)的套接字所使用的地址上。下面是打開該選項(xiàng)的方法:##./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)都打開了此選項(xiàng)??梢酝ㄟ^setsockopt(2)查看系統(tǒng)上可用的套接字選項(xià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)系非常緊密,不過我打算先講述前者,因?yàn)榉亲枞絀O本身就已經(jīng)能獨(dú)當(dāng)一面了。11.1 非阻塞式讀操作還記得我們之前學(xué)過的ead嗎?我提到過read會一直保持阻塞,直到接收到E或是獲得指定的最小字節(jié)數(shù)為止如果客戶端沒有發(fā)送就可能會導(dǎo)致阻塞這種狀況可以通過readartial暫時(shí)解決readpartial會立即返回所有的可用數(shù)據(jù)但如果沒有數(shù)據(jù)可用那么readparial仍舊會陷入擁塞。如果需要一種不會阻塞的讀操作可以使用red_nonblock。和readparial非常類似read_nonlock需要一個(gè)整數(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啟動(dòng)之前用過的那個(gè)客戶端,一直保持連接打開:$$tail-f/var/log/system.log|nclocalhost4481即便沒有向服務(wù)器發(fā)送數(shù)據(jù),readnonblock調(diào)用仍然會立即返回事實(shí)上,它產(chǎn)生了一個(gè)rrno::EAGAIN異常。下面是手冊頁中關(guān)于EAGAIN的描述:文件被標(biāo)記用于非阻塞式IO,無數(shù)據(jù)可讀。原來是這么回事。這不同于readpartial,后者在這種情況下會阻塞。如果你碰上了這種錯(cuò)誤該怎么辦?套接字是否會阻塞?在本例中,我|59(etr而并非正確的做法。對被阻塞的讀操作進(jìn)行重試的正確做法是使用IO.select:beginbeginconnection.read_nonblock(4096)rescueErrno::EAGAINIO.select([connection])retryend上面的代碼可以實(shí)現(xiàn)的功能同之前那種堆砌了大量與retr的代碼一樣但卻去掉了不必要的循環(huán)使用套接字?jǐn)?shù)組為IO.selet調(diào)用的第一個(gè)參數(shù)將會造成阻塞到其中的某個(gè)套字變得可讀為止。所以應(yīng)該僅當(dāng)套接字有數(shù)據(jù)可讀時(shí)才調(diào)用retr在下一章我們會更細(xì)致地講解IO.slect。在本例中,我們使用非阻塞方法重新實(shí)現(xiàn)了阻塞式的read方法。這本身并沒有什么用處但是IO.select提供了一種靈活性可以在進(jìn)行其他工作的同時(shí)監(jiān)控多個(gè)套接字或是定期檢查它們的可讀性。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ā)異常。什么時(shí)候讀操作會阻塞?60|第11章非阻塞式IO非阻塞式寫操作非阻塞式寫操作同我們之前看到的write調(diào)用有多處重要的不同。最明顯的一處是:write_nonblock可能會返回部分寫入的結(jié)果,而write調(diào)用總是將你發(fā)送給它的數(shù)據(jù)全部寫入。下面使用netcat啟動(dòng)一個(gè)臨時(shí)服務(wù)器來演示這種行為:$$nc-llocalhost4481然后再啟動(dòng)一個(gè)采用了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)方法之所以返回是因?yàn)榕錾狭四撤N使它出現(xiàn)阻塞的情況因此也沒法進(jì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)仍處于阻塞,那你會得到一個(gè)Errno::EAGAIN異常。最終還是得靠阻塞地進(jìn)行寫入。##./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這里我們將一個(gè)套接字?jǐn)?shù)組作為IO.select的第二個(gè)參數(shù),這樣IO.select會一直阻塞,直到其中的某個(gè)套接字可以寫入。例子中的循環(huán)語句正確地處理了部分寫操作。當(dāng)write_nonblock返后等套接字再次可寫時(shí),重新執(zhí)行循環(huán)。62|第11章非阻塞式IO底層的底層的write(2)在下述兩種情況下會阻塞。TCPTCP使用擁塞控制算法確保網(wǎng)TCP連以免網(wǎng)絡(luò)過載。①TCP戶端可以接收更多的數(shù)據(jù)為止。什么時(shí)候?qū)懖僮鲿枞??非擁塞式接收盡管非阻塞式的rea和rite用得最多但除了它們之外別的方也有非阻塞形式。accept_nonblock和普通的accet幾乎一樣。還記不記得我當(dāng)時(shí)說過accept只是從偵聽隊(duì)列中彈出一個(gè)連接?如果偵聽隊(duì)列為空,那accept就得阻塞了。而ccept_nonblock就不會阻塞,只是產(chǎn)生一個(gè)Errno::AGAIN。①如果發(fā)送時(shí)間過長,可能是因?yàn)榫W(wǎng)絡(luò)出現(xiàn)了擁塞,那么這時(shí)就應(yīng)該減少數(shù)據(jù)發(fā)送量,避免加劇擁塞?!g者注|63下面是一個(gè)例子:##./code/snippets/accept_nonblock.rbrequire'socket'server=TCPServer.new(4481)loopdobeginconnection=rescueErrno::EAGAIN#執(zhí)行其他重要的工作。retryendend11.4 非擁塞式連接看看你現(xiàn)在能不能猜出connect_nonblock有點(diǎn)出乎你的意料!connect_nonblock的行為和其他的非阻塞式方法有些不同。其他方法要么是完成操作,要么是產(chǎn)生一個(gè)對應(yīng)的異常,而conncet_nonblock則是保持操作繼續(xù)運(yùn)行,并產(chǎn)生一個(gè)異常。如果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ā)起一個(gè)非阻塞式連接。socket.connect_nonblock(remote_addr)rescueErrno::EINPROGRESS#操作在進(jìn)行中。rescueErrno::EALREADY#之前的非阻塞式連接已經(jīng)在進(jìn)行當(dāng)中。rescueErrno::ECONNREFUSED#遠(yuǎn)程主機(jī)拒絕連接。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)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論