linux服務器開發三(網絡編程)


網絡基礎

協議的概念

什么是協議

  • 從應用的角度出發,協議可理解為“規則”,是數據傳輸和數據的解釋的規則。
  • 假設,A、B雙方欲傳輸文件。規定:
  • 第一次,傳輸文件名,接收方接收到文件名,應答OK給傳輸方;
  • 第二次,發送文件的尺寸,接收方接收到該數據再次應答一個OK;
  • 第三次,傳輸文件內容。同樣,接收方接收數據完成后應答OK表示文件內容接收成功。
  • 由此,無論A、B之間傳遞何種文件,都是通過三次數據傳輸來完成。A、B之間形成了一個最簡單的數據傳輸規則。雙方都按此規則發送、接收數據。A、B之間達成的這個相互遵守的規則即為協議。
  • 這種僅在A、B之間被遵守的協議稱之為原始協議。當此協議被更多的人采用,不斷的增加、改進、維護、完善。最終形成一個穩定的、完整的文件傳輸協議,被廣泛應用於各種文件傳輸過程中。該協議就成為一個標准協議。最早的ftp協議就是由此衍生而來。
  • TCP協議注重數據的傳輸。http協議着重於數據的解釋。

典型協議

  • 傳輸層 常見協議有TCP/UDP協議。
  • 應用層 常見的協議有HTTP協議,FTP協議。
  • 網絡層 常見協議有IP協議、ICMP協議、IGMP協議。
  • 網絡接口層 常見協議有ARP協議、RARP協議。
  • TCP傳輸控制協議(Transmission Control Protocol)是一種面向連接的、可靠的、基於字節流的傳輸層通信協議。
  • UDP用戶數據報協議(User Datagram Protocol)是OSI參考模型中一種無連接的傳輸層協議,提供面向事務的簡單不可靠信息傳送服務。
  • HTTP超文本傳輸協議(Hyper Text Transfer Protocol)是互聯網上應用最為廣泛的一種網絡協議。
  • FTP文件傳輸協議(File Transfer Protocol)
  • IP協議是因特網互聯協議(Internet Protocol)
  • ICMP協議是Internet控制報文協議(Internet Control Message Protocol)它是TCP/IP協議族的一個子協議,用於在IP主機、路由器之間傳遞控制消息。
  • IGMP協議是 Internet 組管理協議(Internet Group Management Protocol),是因特網協議家族中的一個組播協議。該協議運行在主機和組播路由器之間。
  • ARP協議是正向地址解析協議(Address Resolution Protocol),通過已知的IP,尋找對應主機的MAC地址。
  • RARP協議是反向地址轉換協議,通過MAC地址確定IP地址。

網絡應用程序設計模式

C/S模式

  • 傳統的網絡應用設計模式,客戶機(client)/服務器(server)模式。需要在通訊兩端各自部署客戶機和服務器來完成數據通信。

B/S模式

  • 瀏覽器(Browser)/服務器(server)模式。只需在一端部署服務器,而另外一端使用每台PC都默認配置的瀏覽器即可完成數據的傳輸。

優缺點

  • 對於C/S模式來說,其優點明顯。客戶端位於目標主機上可以保證性能,將數據緩存至客戶端本地,從而提高數據傳輸效率。且,一般來說客戶端和服務器程序由一個開發團隊創作,所以他們之間所采用的協議相對靈活。可以在標准協議的基礎上根據需求裁剪及定制。例如,騰訊公司所采用的通信協議,即為ftp協議的修改剪裁版。
  • 因此,傳統的網絡應用程序及較大型的網絡應用程序都首選C/S模式進行開發。如,知名的網絡游戲魔獸世界。3D畫面,數據量龐大,使用C/S模式可以提前在本地進行大量數據的緩存處理,從而提高觀感。
  • C/S模式的缺點也較突出。由於客戶端和服務器都需要有一個開發團隊來完成開發。工作量將成倍提升,開發周期較長。另外,從用戶角度出發,需要將客戶端安插至用戶主機上,對用戶主機的安全性構成威脅。這也是很多用戶不願使用C/S模式應用程序的重要原因。
  • B/S模式相比C/S模式而言,由於它沒有獨立的客戶端,使用標准瀏覽器作為客戶端,其工作開發量較小。只需開發服務器端即可。另外由於其采用瀏覽器顯示數據,因此移植性非常好,不受平台限制。如早期的偷菜游戲,在各個平台上都可以完美運行。
  • B/S模式的缺點也較明顯。由於使用第三方瀏覽器,因此網絡應用支持受限。另外,沒有客戶端放到對方主機上,緩存數據不盡如人意,從而傳輸數據量受到限制。應用的觀感大打折扣。第三,必須與瀏覽器一樣,采用標准http協議進行通信,協議選擇不靈活
  • 因此在開發過程中,模式的選擇由上述各自的特點決定。根據實際需求選擇應用程序設計模式。

分層模型

OSI七層模型

OSI模型

  • 1.物理層:主要定義物理設備標准,如網線的接口類型、光纖的接口類型、各種傳輸介質的傳輸速率等。它的主要作用是傳輸比特流(就是由1、0轉化為電流強弱來進行傳輸,到達目的地后再轉化為1、0,也就是我們常說的數模轉換與模數轉換)。這一層的數據叫做比特。
  • 2.數據鏈路層:定義了如何讓格式化數據以幀為單位進行傳輸,以及如何讓控制對物理介質的訪問。這一層通常還提供錯誤檢測和糾正,以確保數據的可靠傳輸。如:串口通信中使用到的115200、8、N、1
  • 3.網絡層:在位於不同地理位置的網絡中的兩個主機系統之間提供連接和路徑選擇。Internet的發展使得從世界各站點訪問信息的用戶數大大增加,而網絡層正是管理這種連接的層。
  • 4.傳輸層:定義了一些傳輸數據的協議和端口號(WWW端口80等),如:TCP(傳輸控制協議,傳輸效率低,可靠性強,用於傳輸可靠性要求高,數據量大的數據),UDP(用戶數據報協議,與TCP特性恰恰相反,用於傳輸可靠性要求不高,數據量小的數據,如QQ聊天數據就是通過這種方式傳輸的)。 主要是將從下層接收的數據進行分段和傳輸,到達目的地址后再進行重組。常常把這一層數據叫做段。
  • 5.會話層:通過傳輸層(端口號:傳輸端口與接收端口)建立數據傳輸的通路。主要在你的系統之間發起會話或者接受會話請求(設備之間需要互相認識可以是IP也可以是MAC或者是主機名)。
  • 6.表示層:可確保一個系統的應用層所發送的信息可以被另一個系統的應用層讀取。例如,PC程序與另一台計算機進行通信,其中一台計算機使用擴展二一十進制交換碼(EBCDIC),而另一台則使用美國信息交換標准碼(ASCII)來表示相同的字符。如有必要,表示層會通過使用一種通格式來實現多種數據格式之間的轉換。
  • 7.應用層:是最靠近用戶的OSI層。這一層為用戶的應用程序(例如電子郵件、文件傳輸和終端仿真)提供網絡服務。

TCP/IP四層模型

  • TCP/IP網絡協議棧分為應用層(Application)、傳輸層(Transport)、網絡層(Network)和鏈路層(Link)四層。如下圖所示:

TCP/IP模型

  • 一般在應用開發過程中,討論最多的是TCP/IP模型。

通信過程

  • 兩台計算機通過TCP/IP協議通訊的過程如下所示:

TCP/IP通信過程

  • 上圖對應兩台計算機在同一網段中的情況,如果兩台計算機在不同的網段中,那么數據從一台計算機到另一台計算機傳輸過程中要經過一個或多個路由器,如下圖所示:

跨路由通信

  • 鏈路層有以太網、令牌環網等標准,鏈路層負責網卡設備的驅動、幀同步(即從網線上檢測到什么信號算作新幀的開始)、沖突檢測(如果檢測到沖突就自動重發)、數據差錯校驗等工作。交換機是工作在鏈路層的網絡設備,可以在不同的鏈路層網絡之間轉發數據幀(比如十兆以太網和百兆以太網之間、以太網和令牌環網之間),由於不同鏈路層的幀格式不同,交換機要將進來的數據包拆掉鏈路層首部重新封裝之后再轉發。
  • 網絡層的IP協議是構成Internet的基礎。Internet上的主機通過IP地址來標識,Inter-net上有大量路由器負責根據IP地址選擇合適的路徑轉發數據包,數據包從Internet上的源主機到目的主機往往要經過十多個路由器。路由器是工作在第三層的網絡設備,同時兼有交換機的功能,可以在不同的鏈路層接口之間轉發數據包,因此路由器需要將進來的數據包拆掉網絡層和鏈路層兩層首部並重新封裝。IP協議不保證傳輸的可靠性,數據包在傳輸過程中可能丟失,可靠性可以在上層協議或應用程序中提供支持。
  • 網絡層負責點到點(ptop,point-to-point)的傳輸(這里的“點”指主機或路由器),而傳輸層負責端到端(etoe,end-to-end)的傳輸(這里的“端”指源主機和目的主機)。傳輸層可選擇TCP或UDP協議。
  • TCP是一種面向連接的、可靠的協議,有點像打電話,雙方拿起電話互通身份之后就建立了連接,然后說話就行了,這邊說的話那邊保證聽得到,並且是按說話的順序聽到的,說完話掛機斷開連接。也就是說TCP傳輸的雙方需要首先建立連接,之后由TCP協議保證數據收發的可靠性,丟失的數據包自動重發,上層應用程序收到的總是可靠的數據流,通訊之后關閉連接。
  • UDP是無連接的傳輸協議,不保證可靠性,有點像寄信,信寫好放到郵筒里,既不能保證信件在郵遞過程中不會丟失,也不能保證信件寄送順序。使用UDP協議的應用程序需要自己完成丟包重發、消息排序等工作。
  • 目的主機收到數據包后,如何經過各層協議棧最后到達應用程序呢?其過程如下圖所示:

數據包網絡傳輸過程

  • 以太網驅動程序首先根據以太網首部中的“上層協議”字段確定該數據幀的有效載荷(payload,指除去協議首部之外實際傳輸的數據)是IP、ARP還是RARP協議的數據報,然后交給相應的協議處理。假如是IP數據報,IP協議再根據IP首部中的“上層協議”字段確定該數據報的有效載荷是TCP、UDP、ICMP還是IGMP,然后交給相應的協議處理。假如是TCP段或UDP段,TCP或UDP協議再根據TCP首部或UDP首部的“端口號”字段確定應該將應用層數據交給哪個用戶進程。IP地址是標識網絡中不同主機的地址,而端口號就是同一台主機上標識不同進程的地址,IP地址和端口號合起來標識網絡中唯一的進程。
  • 雖然IP、ARP和RARP數據報都需要以太網驅動程序來封裝成幀,但是從功能上划分,ARP和RARP屬於鏈路層,IP屬於網絡層。雖然ICMP、IGMP、TCP、UDP的數據都需要IP協議來封裝成數據報,但是從功能上划分,ICMP、IGMP與IP同屬於網絡層,TCP和UDP屬於傳輸層。

協議格式

數據包封裝

  • 傳輸層及其以下的機制由內核提供,應用層由用戶進程提供(后面將介紹如何使用socket API編寫應用程序),應用程序對通訊數據的含義進行解釋,而傳輸層及其以下處理通訊的細節,將數據從一台計算機通過一定的路徑發送到另一台計算機。應用層數據通過協議棧發到網絡上時,每層協議都要加上一個數據首部(header),稱為封裝(Encapsulation),如下圖所示:

TCP/IP數據包封裝

  • 不同的協議層對數據包有不同的稱謂,在傳輸層叫做段(segment),在網絡層叫做數據報(datagram),在鏈路層叫做幀(frame)。數據封裝成幀后發到傳輸介質上,到達目的主機后每層協議再剝掉相應的首部,最后將應用層數據交給應用程序處理。

以太網幀格式

  • 以太網的幀格式如下所示:

以太網幀格式

  • 其中的源地址和目的地址是指網卡的硬件地址(也叫MAC地址),長度是48位,是在網卡出廠時固化的。可在shell中使用ifconfig命令查看,“HWaddr 00:15:F2:14:9E:3F”部分就是硬件地址。協議字段有三種值,分別對應IP、ARP、RARP。幀尾是CRC校驗碼。
  • 以太網幀中的數據長度規定最小46字節,最大1500字節,ARP和RARP數據包的長度不夠46字節,要在后面補填充位。最大值1500稱為以太網的最大傳輸單元(MTU),不同的網絡類型有不同的MTU,如果一個數據包從以太網路由到撥號鏈路上,數據包長度大於撥號鏈路的MTU,則需要對數據包進行分片(fragmentation)。ifconfig命令輸出中也有“MTU:1500”。注意,MTU這個概念指數據幀中有效載荷的最大長度,不包括幀頭長度。

ARP數據報格式

  • 在網絡通訊時,源主機的應用程序知道目的主機的IP地址和端口號,卻不知道目的主機的硬件地址,而數據包首先是被網卡接收到再去處理上層協議的,如果接收到的數據包的硬件地址與本機不符,則直接丟棄。因此在通訊前必須獲得目的主機的硬件地址。ARP協議就起到這個作用。源主機發出ARP請求,詢問“IP地址是192.168.0.1的主機的硬件地址是多少”,並將這個請求廣播到本地網段(以太網幀首部的硬件地址填FF:FF:FF:FF:FF:FF表示廣播),目的主機接收到廣播的ARP請求,發現其中的IP地址與本機相符,則發送一個ARP應答數據包給源主機,將自己的硬件地址填寫在應答包中。

  • 每台主機都維護一個ARP緩存表,可以用arp -a命令查看。緩存表中的表項有過期時間(一般為20分鍾),如果20分鍾內沒有再次使用某個表項,則該表項失效,下次還要發ARP請求來獲得目的主機的硬件地址。想一想,為什么表項要有過期時間而不是一直有效?

  • ARP數據報的格式如下所示:

ARP數據報格式

  • 源MAC地址、目的MAC地址在以太網首部和ARP請求中各出現一次,對於鏈路層為以太網的情況是多余的,但如果鏈路層是其它類型的網絡則有可能是必要的。硬件類型指鏈路層網絡類型,1為以太網,協議類型指要轉換的地址類型,0x0800為IP地址,后面兩個地址長度對於以太網地址和IP地址分別為6和4(字節),op字段為1表示ARP請求,op字段為2表示ARP應答。

  • 看一個具體的例子。

  • 請求幀如下(為了清晰在每行的前面加了字節計數,每行16個字節):

      以太網首部(14字節)
      0000: ff ff ff ff ff ff 00 05 5d 61 58 a8 08 06
      ARP幀(28字節)
      0000: 00 01
      0010: 08 00 06 04 00 01 00 05 5d 61 58 a8 c0 a8 00 37
      0020: 00 00 00 00 00 00 c0 a8 00 02
      填充位(18字節)
      0020: 00 77 31 d2 50 10
      0030: fd 78 41 d3 00 00 00 00 00 00 00 00
    
  • 以太網首部:目的主機采用廣播地址,源主機的MAC地址是00:05:5d:61:58:a8,上層協議類型0x0806表示ARP。

  • ARP幀:硬件類型0x0001表示以太網,協議類型0x0800表示IP協議,硬件地址(MAC地址)長度為6,協議地址(IP地址)長度為4,op為0x0001表示請求目的主機的MAC地址,源主機MAC地址為00:05:5d:61:58:a8,源主機IP地址為c0 a8 00 37(192.168.0.55),目的主機MAC地址全0待填寫,目的主機IP地址為c0 a8 00 02(192.168.0.2)。

  • 由於以太網規定最小數據長度為46字節,ARP幀長度只有28字節,因此有18字節填充位,填充位的內容沒有定義,與具體實現相關。

  • 應答幀如下:

      以太網首部
      0000: 00 05 5d 61 58 a8 00 05 5d a1 b8 40 08 06
      ARP幀
      0000: 00 01
      0010: 08 00 06 04 00 02 00 05 5d a1 b8 40 c0 a8 00 02
      0020: 00 05 5d 61 58 a8 c0 a8 00 37
      填充位
      0020: 00 77 31 d2 50 10
      0030: fd 78 41 d3 00 00 00 00 00 00 00 00
    
  • 以太網首部:目的主機的MAC地址是00:05:5d:61:58:a8,源主機的MAC地址是00:05:5d:a1:b8:40,上層協議類型0x0806表示ARP。

  • ARP幀:硬件類型0x0001表示以太網,協議類型0x0800表示IP協議,硬件地址(MAC地址)長度為6,協議地址(IP地址)長度為4,op為0x0002表示應答,源主機MAC地址為00:05:5d:a1:b8:40,源主機IP地址為c0 a8 00 02(192.168.0.2),目的主機MAC地址為00:05:5d:61:58:a8,目的主機IP地址為c0 a8 00 37(192.168.0.55)。

  • 思考題:如果源主機和目的主機不在同一網段,ARP請求的廣播幀無法穿過路由器,源主機如何與目的主機通信?

IP段格式

IP數據報格式

  • IP數據報的首部長度和數據長度都是可變長的,但總是4字節的整數倍。對於IPv4,4位版本字段是4。4位首部長度的數值是以4字節為單位的,最小值為5,也就是說首部長度最小是4x5=20字節,也就是不帶任何選項的IP首部,4位能表示的最大值是15,也就是說首部長度最大是60字節。8位TOS字段有3個位用來指定IP數據報的優先級(目前已經廢棄不用),還有4個位表示可選的服務類型(最小延遲、最大?吐量、最大可靠性、最小成本),還有一個位總是0。總長度是整個數據報(包括IP首部和IP層payload)的字節數。每傳一個IP數據報,16位的標識加1,可用於分片和重新組裝數據報。3位標志和13位片偏移用於分片。TTL(Time to live)是這樣用的:源主機為數據包設定一個生存時間,比如64,每過一個路由器就把該值減1,如果減到0就表示路由已經太長了仍然找不到目的主機的網絡,就丟棄該包,因此這個生存時間的單位不是秒,而是跳(hop)。協議字段指示上層協議是TCP、UDP、ICMP還是IGMP。然后是校驗和,只校驗IP首部,數據的校驗由更高層協議負責。IPv4的IP地址長度為32位。

  • 想一想,前面講了以太網幀中的最小數據長度為46字節,不足46字節的要用填充字節補上,那么如何界定這46字節里前多少個字節是IP、ARP或RARP數據報而后面是填充字節?

UDP數據報格式

UDP數據段

  • 下面分析一幀基於UDP的TFTP協議幀。

      以太網首部
      0000: 00 05 5d 67 d0 b1 00 05 5d 61 58 a8 08 00
      IP首部
      0000: 45 00
      0010: 00 53 93 25 00 00 80 11 25 ec c0 a8 00 37 c0 a8
      0020: 00 01
      UDP首部
      0020: 05 d4 00 45 00 3f ac 40
      TFTP協議
      0020: 00 01 'c'':''\''q'
      0030: 'w''e''r''q''.''q''w''e'00 'n''e''t''a''s''c''i'
      0040: 'i'00 'b''l''k''s''i''z''e'00 '5''1''2'00 't''i'
      0050: 'm''e''o''u''t'00 '1''0'00 't''s''i''z''e'00 '0'
      0060: 00以太網首部:源MAC地址是00:05:5d:61:58:a8,目的MAC地址是00:05:5d:67:d0:b1,上層協議類型0x0800表示IP。
    
  • IP首部:每一個字節0x45包含4位版本號和4位首部長度,版本號為4,即IPv4,首部長度為5,說明IP首部不帶有選項字段。服務類型為0,沒有使用服務。16位總長度字段(包括IP首部和IP層payload的長度)為0x0053,即83字節,加上以太網首部14字節可知整個幀長度是97字節。IP報標識是0x9325,標志字段和片偏移字段設置為0x0000,就是DF=0允許分片,MF=0此數據報沒有更多分片,沒有分片偏移。TTL是0x80,也就是128。上層協議0x11表示UDP協議。IP首部校驗和為0x25ec,源主機IP是c0 a8 00 37(192.168.0.55),目的主機IP是c0 a8 00 01(192.168.0.1)。

  • UDP首部:源端口號0x05d4(1492)是客戶端的端口號,目的端口號0x0045(69)是TFTP服務的well-known端口號。UDP報長度為0x003f,即63字節,包括UDP首部和UDP層pay-load的長度。UDP首部和UDP層payload的校驗和為0xac40。

  • TFTP是基於文本的協議,各字段之間用字節0分隔,開頭的00 01表示請求讀取一個文件,接下來的各字段是:

      c:\qwerq.qwe
      netascii
      blksize 512
      timeout 10
      tsize 0
    
  • 一般的網絡通信都是像TFTP協議這樣,通信的雙方分別是客戶端和服務器,客戶端主動發起請求(上面的例子就是客戶端發起的請求幀),而服務器被動地等待、接收和應答請求。客戶端的IP地址和端口號唯一標識了該主機上的TFTP客戶端進程,服務器的IP地址和端口號唯一標識了該主機上的TFTP服務進程,由於客戶端是主動發起請求的一方,它必須知道服務器的IP地址和TFTP服務進程的端口號,所以,一些常見的網絡協議有默認的服務器端口,例如HTTP服務默認TCP協議的80端口,FTP服務默認TCP協議的21端口,TFTP服務默認UDP協議的69端口(如上例所示)。在使用客戶端程序時,必須指定服務器的主機名或IP地址,如果不明確指定端口號則采用默認端口,請讀者查閱ftp、tftp等程序的man page了解如何指定端口號。/etc/services中列出了所有well-known的服務端口和對應的傳輸層協議,這是由IANA(Internet Assigned Numbers Authority)規定的,其中有些服務既可以用TCP也可以用UDP,為了清晰,IANA規定這樣的服務采用相同的TCP或UDP默認端口號,而另外一些TCP和UDP的相同端口號卻對應不同的服務。

  • 很多服務有well-known的端口號,然而客戶端程序的端口號卻不必是well-known的,往往是每次運行客戶端程序時由系統自動分配一個空閑的端口號,用完就釋放掉,稱為ephemeral的端口號,想想這是為什么?

  • 前面提過,UDP協議不面向連接,也不保證傳輸的可靠性,例如:

  • 發送端的UDP協議層只管把應用層傳來的數據封裝成段交給IP協議層就算完成任務了,如果因為網絡故障該段無法發到對方,UDP協議層也不會給應用層返回任何錯誤信息。

  • 接收端的UDP協議層只管把收到的數據根據端口號交給相應的應用程序就算完成任務了,如果發送端發來多個數據包並且在網絡上經過不同的路由,到達接收端時順序已經錯亂了,UDP協議層也不保證按發送時的順序交給應用層。

  • 通常接收端的UDP協議層將收到的數據放在一個固定大小的緩沖區中等待應用程序來提取和處理,如果應用程序提取和處理的速度很慢,而發送端發送的速度很快,就會丟失數據包,UDP協議層並不報告這種錯誤。

  • 因此,使用UDP協議的應用程序必須考慮到這些可能的問題並實現適當的解決方案,例如等待應答、超時重發、為數據包編號、流量控制等。一般使用UDP協議的應用程序實現都比較簡單,只是發送一些對可靠性要求不高的消息,而不發送大量的數據。例如,基於UDP的TFTP協議一般只用於傳送小文件(所以才叫trivial的ftp),而基於TCP的FTP協議適用於 各種文件的傳輸。TCP協議又是如何用面向連接的服務來代替應用程序解決傳輸的可靠性問題呢。

TCP數據報格式

TCP數據段

  • 與UDP協議一樣也有源端口號和目的端口號,通訊的雙方由IP地址和端口號標識。32位序號、32位確認序號、窗口大小稍后詳細解釋。4位首部長度和IP協議頭類似,表示TCP協議頭的長度,以4字節為單位,因此TCP協議頭最長可以是4x15=60字節,如果沒有選項字段,TCP協議頭最短20字節。URG、ACK、PSH、RST、SYN、FIN是六個控制位,本節稍后將解釋SYN、ACK、FIN、RST四個位,其它位的解釋從略。16位檢驗和將TCP協議頭和數據都計算在內。緊急指針和各種選項的解釋從略。

TCP協議

TCP通信時序

  • 下圖是一次TCP通訊的時序圖。TCP連接建立斷開。包含大家熟知的三次握手和四次握手。

TCP通訊時序

  • 在這個例子中,首先客戶端主動發起連接、發送請求,然后服務器端響應請求,然后客戶端主動關閉連接。兩條豎線表示通訊的兩端,從上到下表示時間的先后順序,注意,數據從一端傳到網絡的另一端也需要時間,所以圖中的箭頭都是斜的。雙方發送的段按時間順序編號為1-10,各段中的主要信息在箭頭上標出,例如段2的箭頭上標着SYN, 8000(0), ACK1001, ,表示該段中的SYN位置1,32位序號是8000,該段不攜帶有效載荷(數據字節數為0),ACK位置1,32位確認序號是1001,帶有一個mss(Maximum Segment Size,最大報文長度)選項值為1024。

  • 建立連接(三次握手)的過程:

    • 1、客戶端發送一個帶SYN標志的TCP報文到服務器。這是三次握手過程中的段1。
      客戶端發出段1,SYN位表示連接請求。序號是1000,這個序號在網絡通訊中用作臨時的地址,每發一個數據字節,這個序號要加1,這樣在接收端可以根據序號排出數據包的正確順序,也可以發現丟包的情況,另外,規定SYN位和FIN位也要占一個序號,這次雖然沒發數據,但是由於發了SYN位,因此下次再發送應該用序號1001。mss表示最大段尺寸,如果一個段太大,封裝成幀后超過了鏈路層的最大幀長度,就必須在IP層分片,為了避免這種情況,客戶端聲明自己的最大段尺寸,建議服務器端發來的段不要超過這個長度。

    • 2、服務器端回應客戶端,是三次握手中的第2個報文段,同時帶ACK標志和SYN標志。它表示對剛才客戶端SYN的回應;同時又發送SYN給客戶端,詢問客戶端是否准備好進行數據通訊。
      服務器發出段2,也帶有SYN位,同時置ACK位表示確認,確認序號是1001,表示“我接收到序號1000及其以前所有的段,請你下次發送序號為1001的段”,也就是應答了客戶端的連接請求,同時也給客戶端發出一個連接請求,同時聲明最大尺寸為1024。

    • 3、客戶必須再次回應服務器端一個ACK報文,這是報文段3。
      客戶端發出段3,對服務器的連接請求進行應答,確認序號是8001。在這個過程中,客戶端和服務器分別給對方發了連接請求,也應答了對方的連接請求,其中服務器的請求和應答在一個段中發出,因此一共有三個段用於建立連接,稱為“三方握手(three-way-handshake)”。在建立連接的同時,雙方協商了一些信息,例如雙方發送序號的初始值、最大段尺寸等。

    • 在TCP通訊中,如果一方收到另一方發來的段,讀出其中的目的端口號,發現本機並沒有任何進程使用這個端口,就會應答一個包含RST位的段給另一方。例如,服務器並沒有任何進程使用8080端口,我們卻用telnet客戶端去連接它,服務器收到客戶端發來的SYN段就會應答一個RST段,客戶端的telnet程序收到RST段后報告錯誤Connection refused:

        $ telnet 192.168.0.200 8080
        Trying 192.168.0.200...
        telnet: Unable to connect to remote host: Connection refused
      
  • 數據傳輸的過程:

    • 1、客戶端發出段4,包含從序號1001開始的20個字節數據。
    • 2、服務器發出段5,確認序號為1021,對序號為1001-1020的數據表示確認收到,同時請求發送序號1021開始的數據,服務器在應答的同時也向客戶端發送從序號8001開始的10個字節數據,這稱為piggyback。
    • 3、客戶端發出段6,對服務器發來的序號為8001-8010的數據表示確認收到,請求發送序號8011開始的數據。
    • 在數據傳輸過程中,ACK和確認序號是非常重要的,應用程序交給TCP協議發送的數據會暫存在TCP層的發送緩沖區中,發出數據包給對方之后,只有收到對方應答的ACK段才知道該數據包確實發到了對方,可以從發送緩沖區中釋放掉了,如果因為網絡故障丟失了數據包或者丟失了對方發回的ACK段,經過等待超時后TCP協議自動將發送緩沖區中的數據包重發。
  • 關閉連接(四次握手)的過程:

    • 由於TCP連接是全雙工的,因此每個方向都必須單獨進行關閉。這原則是當一方完成它的數據發送任務后就能發送一個FIN來終止這個方向的連接。收到一個 FIN只意味着這一方向上沒有數據流動,一個TCP連接在收到一個FIN后仍能發送數據。首先進行關閉的一方將執行主動關閉,而另一方執行被動關閉。
    • 1、客戶端發出段7,FIN位表示關閉連接的請求。
    • 2、服務器發出段8,應答客戶端的關閉連接請求。
    • 3、服務器發出段9,其中也包含FIN位,向客戶端發送關閉連接請求。
    • 4、客戶端發出段10,應答服務器的關閉連接請求。
    • 建立連接的過程是三方握手,而關閉連接通常需要4個段,服務器的應答和關閉連接請求通常不合並在一個段中,因為有連接半關閉的情況,這種情況下客戶端關閉連接之后就不能再發送數據給服務器了,但是服務器還可以發送數據給客戶端,直到服務器也關閉連接為止。

滑動窗口 (TCP流量控制)

  • 介紹UDP時我們描述了這樣的問題:如果發送端發送的速度較快,接收端接收到數據后處理的速度較慢,而接收緩沖區的大小是固定的,就會丟失數據。TCP協議通過“滑動窗口(Sliding Window)”機制解決這一問題。看下圖的通訊過程:

滑動窗口

  • 1、發送端發起連接,聲明最大段尺寸是1460,初始序號是0,窗口大小是4K,表示“我的接收緩沖區還有4K字節空閑,你發的數據不要超過4K”。接收端應答連接請求,聲明最大段尺寸是1024,初始序號是8000,窗口大小是6K。發送端應答,三方握手結束。

  • 2、發送端發出段4-9,每個段帶1K的數據,發送端根據窗口大小知道接收端的緩沖區滿了,因此停止發送數據。

  • 3、接收端的應用程序提走2K數據,接收緩沖區又有了2K空閑,接收端發出段10,在應答已收到6K數據的同時聲明窗口大小為2K。

  • 4、接收端的應用程序又提走2K數據,接收緩沖區有4K空閑,接收端發出段11,重新聲明窗口大小為4K。

  • 5、發送端發出段12-13,每個段帶2K數據,段13同時還包含FIN位。

  • 6、接收端應答接收到的2K數據(6145-8192),再加上FIN位占一個序號8193,因此應答序號是8194,連接處於半關閉狀態,接收端同時聲明窗口大小為2K。

  • 7、接收端的應用程序提走2K數據,接收端重新聲明窗口大小為4K。

  • 8、接收端的應用程序提走剩下的2K數據,接收緩沖區全空,接收端重新聲明窗口大小為6K。

  • 9、接收端的應用程序在提走全部數據后,決定關閉連接,發出段17包含FIN位,發送端應答,連接完全關閉。

  • 上圖在接收端用小方塊表示1K數據,實心的小方塊表示已接收到的數據,虛線框表示接收緩沖區,因此套在虛線框中的空心小方塊表示窗口大小,從圖中可以看出,隨着應用程序提走數據,虛線框是向右滑動的,因此稱為滑動窗口。

  • 從這個例子還可以看出,發送端是一K一K地發送數據,而接收端的應用程序可以兩K兩K地提走數據,當然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據。也就是說,應用程序所看到的數據是一個整體,或說是一個流(stream),在底層通訊中這些數據可能被拆成很多數據包來發送,但是一個數據包有多少字節對應用程序是不可見的,因此TCP協議是面向流的協議。而UDP是面向消息的協議,每個UDP段都是一條消息,應用程序必須以消息為單位提取數據,不能一次提取任意字節的數據,這一點和TCP是很不同的。

TCP狀態轉換

  • 這個圖N多人都知道,它排除和定位網絡或系統故障時大有幫助,但是怎樣牢牢地將這張圖刻在腦中呢?那么你就一定要對這張圖的每一個狀態,及轉換的過程有深刻的認識,不能只停留在一知半解之中。下面對這張圖的11種狀態詳細解析一下,以便加強記憶!不過在這之前,先回顧一下TCP建立連接的三次握手過程,以及 關閉連接的四次握手過程。

TCP狀態轉換圖

  • CLOSED:表示初始狀態。

  • LISTEN:該狀態表示服務器端的某個SOCKET處於監聽狀態,可以接受連接。

  • SYN_SENT:這個狀態與SYN_RCVD遙相呼應,當客戶端SOCKET執行CONNECT連接時,它首先發送SYN報文,隨即進入到了SYN_SENT狀態,並等待服務端的發送三次握手中的第2個報文。SYN_SENT狀態表示客戶端已發送SYN報文。

  • SYN_RCVD: 該狀態表示接收到SYN報文,在正常情況下,這個狀態是服務器端的SOCKET在建立TCP連接時的三次握手會話過程中的一個中間狀態,很短暫。此種狀態時,當收到客戶端的ACK報文后,會進入到ESTABLISHED狀態。

  • ESTABLISHED:表示連接已經建立。

  • FIN_WAIT_1: FIN_WAIT_1和FIN_WAIT_2狀態的真正含義都是表示等待對方的FIN報文。區別是:

    • FIN_WAIT_1狀態是當socket在ESTABLISHED狀態時,想主動關閉連接,向對方發送了FIN報文,此時該socket進入到FIN_WAIT_1狀態。
    • FIN_WAIT_2狀態是當對方回應ACK后,該socket進入到FIN_WAIT_2狀態,正常情況下,對方應馬上回應ACK報文,所以FIN_WAIT_1狀態一般較難見到,而FIN_WAIT_2狀態可用netstat看到。
  • FIN_WAIT_2:主動關閉鏈接的一方,發出FIN收到ACK以后進入該狀態。稱之為半連接或半關閉狀態。該狀態下的socket只能接收數據,不能發。

  • TIME_WAIT: 表示收到了對方的FIN報文,並發送出了ACK報文,等2MSL后即可回到CLOSED可用狀態。如果FIN_WAIT_1狀態下,收到對方同時帶 FIN標志和ACK標志的報文時,可以直接進入到TIME_WAIT狀態,而無須經過FIN_WAIT_2狀態。

  • CLOSING: 這種狀態較特殊,屬於一種較罕見的狀態。正常情況下,當你發送FIN報文后,按理來說是應該先收到(或同時收到)對方的 ACK報文,再收到對方的FIN報文。但是CLOSING狀態表示你發送FIN報文后,並沒有收到對方的ACK報文,反而卻也收到了對方的FIN報文。什么情況下會出現此種情況呢?如果雙方幾乎在同時close一個SOCKET的話,那么就出現了雙方同時發送FIN報文的情況,也即會出現CLOSING狀態,表示雙方都正在關閉SOCKET連接。

  • CLOSE_WAIT: 此種狀態表示在等待關閉。當對方關閉一個SOCKET后發送FIN報文給自己,系統會回應一個ACK報文給對方,此時則進入到CLOSE_WAIT狀態。接下來呢,察看是否還有數據發送給對方,如果沒有可以 close這個SOCKET,發送FIN報文給對方,即關閉連接。所以在CLOSE_WAIT狀態下,需要關閉連接。

  • LAST_ACK: 該狀態是被動關閉一方在發送FIN報文后,最后等待對方的ACK報文。當收到ACK報文后,即可以進入到CLOSED可用狀態。

半關閉

  • 當TCP鏈接中A發送FIN請求關閉,B端回應ACK后(A端進入FIN_WAIT_2狀態),B沒有立即發送FIN給A時,A方處在半鏈接狀態,此時A可以接收B發送的數據,但是A已不能再向B發送數據。

  • 從程序的角度,可以使用API來控制實現半連接狀態。

      #include <sys/socket.h>
      int shutdown(int sockfd, int how);
      sockfd: 需要關閉的socket的描述符
      how:	允許為shutdown操作選擇以下幾種方式:
      	SHUT_RD(0):	關閉sockfd上的讀功能,此選項將不允許sockfd進行讀操作。
      					該套接字不再接受數據,任何當前在套接字接受緩沖區的數據將被無聲的丟棄掉。
      	SHUT_WR(1):		關閉sockfd的寫功能,此選項將不允許sockfd進行寫操作。進程不能在對此套接字發出寫操作。
      	SHUT_RDWR(2):	關閉sockfd的讀寫功能。相當於調用shutdown兩次:首先是以SHUT_RD,然后以SHUT_WR。
    
  • 使用close中止一個連接,但它只是減少描述符的引用計數,並不直接關閉連接,只有當描述符的引用計數為0時才關閉連接。

  • shutdown不考慮描述符的引用計數,直接關閉描述符。也可選擇中止一個方向的連接,只中止讀或只中止寫。

  • 注意:

    • 1、如果有多個進程共享一個套接字,close每被調用一次,計數減1,直到計數為0時,也就是所用進程都調用了close,套接字將被釋放。
    • 2、在多進程中如果一個進程調用了shutdown(sfd, SHUT_RDWR)后,其它的進程將無法進行通信。但,如果一個進程close(sfd)將不會影響到其它進程。

2MSL

  • 2MSL (Maximum Segment Lifetime) TIME_WAIT狀態的存在有兩個理由:

    • (1)讓4次握手關閉流程更加可靠;4次握手的最后一個ACK是是由主動關閉方發送出去的,若這個ACK丟失,被動關閉方會再次發一個FIN過來。若主動關閉方能夠保持一個2MSL的TIME_WAIT狀態,則有更大的機會讓丟失的ACK被再次發送出去。
    • (2)防止lost duplicate對后續新建正常鏈接的傳輸造成破壞。lost uplicate在實際的網絡中非常常見,經常是由於路由器產生故障,路徑無法收斂,導致一個packet在路由器A,B,C之間做類似死循環的跳轉。IP頭部有個TTL,限制了一個包在網絡中的最大跳數,因此這個包有兩種命運,要么最后TTL變為0,在網絡中消失;要么TTL在變為0之前路由器路徑收斂,它憑借剩余的TTL跳數終於到達目的地。但非常可惜的是TCP通過超時重傳機制在早些時候發送了一個跟它一模一樣的包,並先於它達到了目的地,因此它的命運也就注定被TCP協議棧拋棄。
  • 另外一個概念叫做incarnation connection,指跟上次的socket pair一摸一樣的新連接,叫做incarnation of previous connection。lost uplicate加上incarnation connection,則會對我們的傳輸造成致命的錯誤。

  • TCP是流式的,所有包到達的順序是不一致的,依靠序列號由TCP協議棧做順序的拼接;假設一個incarnation connection這時收到的seq=1000, 來了一個lost duplicate為seq=1000,len=1000, 則TCP認為這個lost duplicate合法,並存放入了receive buffer,導致傳輸出現錯誤。通過一個2MSL TIME_WAIT狀態,確保所有的lost duplicate都會消失掉,避免對新連接造成錯誤。

  • 該狀態為什么設計在主動關閉這一方

    • (1)發最后ACK的是主動關閉一方。
    • (2)只要有一方保持TIME_WAIT狀態,就能起到避免incarnation connection在2MSL內的重新建立,不需要兩方都有。
  • 如何正確對待2MSL TIME_WAIT?

  • RFC要求socket pair在處於TIME_WAIT時,不能再起一個incarnation connection。但絕大部分TCP實現,強加了更為嚴格的限制。在2MSL等待期間,socket中使用的本地端口在默認情況下不能再被使用。

  • 若A 10.234.5.5 : 1234和B 10.55.55.60 : 6666建立了連接,A主動關閉,那么在A端只要port為1234,無論對方的port和ip是什么,都不允許再起服務。這甚至比RFC限制更為嚴格,RFC僅僅是要求socket pair不一致,而實現當中只要這個port處於TIME_WAIT,就不允許起連接。這個限制對主動打開方來說是無所謂的,因為一般用的是臨時端口;但對於被動打開方,一般是server,就悲劇了,因為server一般是熟知端口。比如http,一般端口是80,不可能允許這個服務在2MSL內不能起來。

  • 解決方案是給服務器的socket設置SO_REUSEADDR選項,這樣的話就算熟知端口處於TIME_WAIT狀態,在這個端口上依舊可以將服務啟動。當然,雖然有了SO_REUSEADDR選項,但sockt pair這個限制依舊存在。比如上面的例子,A通過SO_REUSEADDR選項依舊在1234端口上起了監聽,但這時我們若是從B通過6666端口去連它,TCP協議會告訴我們連接失敗,原因為Address already in use.

  • RFC 793中規定MSL為2分鍾,實際應用中常用的是30秒,1分鍾和2分鍾等。

  • RFC (Request For Comments),是一系列以編號排定的文件。收集了有關因特網相關資訊,以及UNIX和因特網社群的軟件文件。

程序設計中的問題

  • 做一個測試,首先啟動server,然后啟動client,用Ctrl-C終止server,馬上再運行server,運行結果:

      itcast$ ./server
      bind error: Address already in use 
    
  • 這是因為,雖然server的應用程序終止了,但TCP協議層的連接並沒有完全斷開,因此不能再次監聽同樣的server端口。我們用netstat命令查看一下:

itcast$ netstat -apn |grep 6666
tcp 1 0 192.168.1.11:38103 192.168.1.11:6666 CLOSE_WAIT 3525/client
tcp 0 0 192.168.1.11:6666 192.168.1.11:38103 FIN_WAIT2 -

  • server終止時,socket描述符會自動關閉並發FIN段給client,client收到FIN后處於CLOSE_WAIT狀態,但是client並沒有終止,也沒有關閉socket描述符,因此不會發FIN給server,因此server的TCP連接處於FIN_WAIT2狀態。

  • 現在用Ctrl-C把client也終止掉,再觀察現象:
    itcast$ netstat -apn |grep 6666
    tcp 0 0 192.168.1.11:6666 192.168.1.11:38104 TIME_WAIT -
    itcast$ ./server
    bind error: Address already in use

  • client終止時自動關閉socket描述符,server的TCP連接收到client發的FIN段后處於TIME_WAIT狀態。TCP協議規定,主動關閉連接的一方要處於TIME_WAIT狀態,等待兩個MSL(maximum segment lifetime)的時間后才能回到CLOSED狀態,因為我們先Ctrl-C終止了server,所以server是主動關閉連接的一方,在TIME_WAIT期間仍然不能再次監聽同樣的server端口。

  • MSL在RFC 1122中規定為兩分鍾,但是各操作系統的實現不同,在Linux上一般經過半分鍾后就可以再次啟動server了。至於為什么要規定TIME_WAIT的時間,可參考UNP 2.7節。

端口復用

  • 在server的TCP連接沒有完全斷開之前不允許重新監聽是不合理的。因為,TCP連接沒有完全斷開指的是connfd(127.0.0.1:6666)沒有完全斷開,而我們重新監聽的是lis-tenfd(0.0.0.0:6666),雖然是占用同一個端口,但IP地址不同,connfd對應的是與某個客戶端通訊的一個具體的IP地址,而listenfd對應的是wildcard address。解決這個問題的方法是使用setsockopt()設置socket描述符的選項SO_REUSEADDR為1,表示允許創建端口號相同但IP地址不同的多個socket描述符。

  • 在server代碼的socket()和bind()調用之間插入如下代碼:
    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

  • 有關setsockopt可以設置的其它選項請參考UNP第7章。

TCP異常斷開

心跳檢測機制

  • 在TCP網絡通信中,經常會出現客戶端和服務器之間的非正常斷開,需要實時檢測查詢鏈接狀態。常用的解決方法就是在程序中加入心跳機制。

  • Heart-Beat線程

  • 這個是最常用的簡單方法。在接收和發送數據時個人設計一個守護進程(線程),定時發送Heart-Beat包,客戶端/服務器收到該小包后,立刻返回相應的包即可檢測對方是否實時在線。

  • 該方法的好處是通用,但缺點就是會改變現有的通訊協議!大家一般都是使用業務層心跳來處理,主要是靈活可控。

  • UNIX網絡編程不推薦使用SO_KEEPALIVE來做心跳檢測,還是在業務層以心跳包做檢測比較好,也方便控制。

設置TCP屬性

  • SO_KEEPALIVE 保持連接檢測對方主機是否崩潰,避免(服務器)永遠阻塞於TCP連接的輸入。設置該選項后,如果2小時內在此套接口的任一方向都沒有數據交換,TCP就自動給對方發一個保持存活探測分節(keepalive probe)。這是一個對方必須響應的TCP分節.它會導致以下三種情況:對方接收一切正常:以期望的ACK響應。2小時后,TCP將發出另一個探測分節。對方已崩潰且已重新啟動:以RST響應。套接口的待處理錯誤被置為ECONNRESET,套接 口本身則被關閉。對方無任何響應:源自berkeley的TCP發送另外8個探測分節,相隔75秒一個,試圖得到一個響應。在發出第一個探測分節11分鍾 15秒后若仍無響應就放棄。套接口的待處理錯誤被置為ETIMEOUT,套接口本身則被關閉。如ICMP錯誤是“host unreachable(主機不可達)”,說明對方主機並沒有崩潰,但是不可達,這種情況下待處理錯誤被置為EHOSTUNREACH。

  • 根據上面的介紹我們可以知道對端以一種非優雅的方式斷開連接的時候,我們可以設置SO_KEEPALIVE屬性使得我們在2小時以后發現對方的TCP連接是否依然存在。
    keepAlive = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_KEEPALIVE, (void*)&keepAlive, sizeof(keepAlive));

  • 如果我們不能接受如此之長的等待時間,從TCP-Keepalive-HOWTO上可以知道一共有兩種方式可以設置,一種是修改內核關於網絡方面的 配置參數,另外一種就是SOL_TCP字段的TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT三個選項。

  • 1.The tcp_keepidle parameter specifies the interval of inactivity that causes TCP to generate a KEEPALIVE transmission for an application that requests them. tcp_keepidle defaults to 14400 (two hours).

      /*開始首次KeepAlive探測前的TCP空閉時間 */
    
  • 2.The tcp_keepintvl parameter specifies the interval between the nine retriesthat are attempted if a KEEPALIVE transmission is not acknowledged. tcp_keep ntvldefaults to 150 (75 seconds).

      /* 兩次KeepAlive探測間的時間間隔 */
    
  • 3.The tcp_keepcnt option specifies the maximum number of keepalive probes tobe sent. The value of TCP_KEEPCNT is an integer value between 1 and n, where n s the value of the systemwide tcp_keepcnt parameter.

      /* 判定斷開前的KeepAlive探測次數*/
    
      int keepIdle = 1000;
      int keepInterval = 10;
      int keepCount = 10;
      
      Setsockopt(listenfd, SOL_TCP, TCP_KEEPIDLE, (void *)&keepIdle, sizeof(keepIdle));
      Setsockopt(listenfd, SOL_TCP,TCP_KEEPINTVL, (void *)&keepInterval, sizeof(keepInterval));
      Setsockopt(listenfd,SOL_TCP, TCP_KEEPCNT, (void *)&keepCount, sizeof(keepCount));
    
  • SO_KEEPALIVE設置空閑2小時才發送一個“保持存活探測分節”,不能保證實時檢測。對於判斷網絡斷開時間太長,對於需要及時響應的程序不太適應。

  • 當然也可以修改時間間隔參數,但是會影響到所有打開此選項的套接口!關聯了完成端口的socket可能會忽略掉該套接字選項。

網絡名詞術語解析

路由(route)

  • 路由(名詞)
    • 數據包從源地址到目的地址所經過的路徑,由一系列路由節點組成。
  • 路由(動詞)
    • 某個路由節點為數據包選擇投遞方向的選路過程。

路由器工作原理

  • 路由器(Router)是連接因特網中各局域網、廣域網的設備,它會根據信道的情況自動選擇和設定路由,以最佳路徑,按前后順序發送信號的設備。

  • 傳統地,路由器工作於OSI七層協議中的第三層,其主要任務是接收來自一個網絡接口的數據包,根據其中所含的目的地址,決定轉發到下一個目的地址。因此,路由器首先得在轉發路由表中查找它的目的地址,若找到了目的地址,就在數據包的幀格前添加下一個MAC地址,同時IP數據包頭的TTL(Time To Live)域也開始減數, 並重新計算校驗和。當數據包被送到輸出端口時,它需要按順序等待,以便被傳送到輸出鏈路上。

  • 路由器在工作時能夠按照某種路由通信協議查找設備中的路由表。如果到某一特定節點有一條以上的路徑,則基本預先確定的路由准則是選擇最優(或最經濟)的傳輸路徑。由於各種網絡段和其相互連接情況可能會因環境變化而變化,因此路由情況的信息一般也按所使用的路由信息協議的規定而定時更新。

  • 網絡中,每個路由器的基本功能都是按照一定的規則來動態地更新它所保持的路由表,以便保持路由信息的有效性。為了便於在網絡間傳送報文,路由器總是先按照預定的規則把較大的數據分解成適當大小的數據包,再將這些數據包分別通過相同或不同路徑發送出去。當這些數據包按先后秩序到達目的地后,再把分解的數據包按照一定順序包裝成原有的報文形式。路由器的分層尋址功能是路由器的重要功能之一,該功能可以幫助具有很多節點站的網絡來存儲尋址信息,同時還能在網絡間截獲發送到遠地網段的報文,起轉發作用;選擇最合理的路由,引導通信也是路由器基本功能;多協議路由器還可以連接使用不同通信協議的網絡段,成為不同通信協議網絡段之間的通信平台。

  • 路由和交換之間的主要區別就是交換發生在OSI參考模型第二層(數據鏈路層),而路由發生在第三層,即網絡層。這一區別決定了路由和交換在移動信息的過程 中需使用不同的控制信息,所以兩者實現各自功能的方式是不同的。

路由表(Routing Table)

  • 在計算機網絡中,路由表或稱路由擇域信息庫(RIB)是一個存儲在路由器或者聯網計算機中的電子表格(文件)或類數據庫。路由表存儲着指向特定網絡地址的路徑。

路由條目

  • 路由表中的一行,每個條目主要由目的網絡地址、子網掩碼、下一跳地址、發送接口四部分組成,如果要發送的數據包的目的網絡地址匹配路由表中的某一行,就按規定的接口發送到下一跳地址。

缺省路由條目

  • 路由表中的最后一行,主要由下一跳地址和發送接口兩部分組成,當目的地址與路由表中其它行都不匹配時,就按缺省路由條目規定的接口發送到下一跳地址。

路由節點

  • 一個具有路由能力的主機或路由器,它維護一張路由表,通過查詢路由表來決定向哪個接口發送數據包。

以太網交換機工作原理

  • 以太網交換機是基於以太網傳輸數據的交換機,以太網采用共享總線型傳輸媒體方式的局域網。以太網交換機的結構是每個端口都直接與主機相連,並且一般都工作在全雙工方式。交換機能同時連通許多對端口,使每一對相互通信的主機都能像獨占通信媒體那樣,進行無沖突地傳輸數據。

  • 以太網交換機工作於OSI網絡參考模型的第二層(即數據鏈路層),是一種基於MAC(Media Access Control,介質訪問控制)地址識別、完成以太網數據幀轉發的網絡設備。

hub工作原理

  • 集線器實際上就是中繼器的一種,其區別僅在於集線器能夠提供更多的端口服務,所以集線器又叫多口中繼器。

  • 集線器功能是隨機選出某一端口的設備,並讓它獨占全部帶寬,與集線器的上聯設備(交換機、路由器或服務器等)進行通信。從Hub的工作方式可以看出,它在網絡中只起到信號放大和重發作用,其目的是擴大網絡的傳輸范圍,而不具備信號的定向傳送能力,是—個標准的共享式設備。其次是Hub只與它的上聯設備(如上層Hub、交換機或服務器)進行通信,同層的各端口之間不會直接進行通信,而是通過上聯設備再將信息廣播到所有端口上。 由此可見,即使是在同一Hub的不同兩個端口之間進行通信,都必須要經過兩步操作:

  • 第一步是將信息上傳到上聯設備;

  • 第二步是上聯設備再將該信息廣播到所有端口上。

半雙工/全雙工

  • Full-duplex(全雙工)全雙工是在通道中同時雙向數據傳輸的能力。
  • Half-duplex(半雙工)在通道中同時只能沿着一個方向傳輸數據。

DNS服務器

  • DNS 是域名系統 (Domain Name System) 的縮寫,是因特網的一項核心服務,它作為可以將域名和IP地址相互映射的一個分布式數據庫,能夠使人更方便的訪問互聯網,而不用去記住能夠被機器直接讀取的IP地址串。

  • 它是由解析器以及域名服務器組成的。域名服務器是指保存有該網絡中所有主機的域名和對應IP地址,並具有將域名轉換為IP地址功能的服務器。

局域網(LAN)

  • local area network,一種覆蓋一座或幾座大樓、一個校園或者一個廠區等地理區域的小范圍的計算機網。
  • 1、覆蓋的地理范圍較小,只在一個相對獨立的局部范圍內聯,如一座或集中的建築群內。
  • 2、使用專門鋪設的傳輸介質進行聯網,數據傳輸速率高(10Mb/s~10Gb/s)
  • 3、通信延遲時間短,可靠性較高
  • 4、局域網可以支持多種傳輸介質

廣域網(WAN)

  • wide area network,一種用來實現不同地區的局域網或城域網的互連,可提供不同地區、城市和國家之間的計算機通信的遠程計算機網。

  • 覆蓋的范圍比局域網(LAN)和城域網(MAN)都廣。廣域網的通信子網主要使用分組交換技術。

  • 廣域網的通信子網可以利用公用分組交換網、衛星通信網和無線分組交換網,它將分布在不同地區的局域網或計算機系統互連起來,達到資源共享的目的。如互聯網是世界范圍內最大的廣域網。

  • 1、適應大容量與突發性通信的要求;

  • 2、適應綜合業務服務的要求;

  • 3、開放的設備接口與規范化的協議;

  • 4、完善的通信服務與網絡管理。

端口

  • 邏輯意義上的端口,一般是指TCP/IP協議中的端口,端口號的范圍從0到65535,比如用於瀏覽網頁服務的80端口,用於FTP服務的21端口等等。
  • 1、端口號小於256的定義為常用端口,服務器一般都是通過常用端口號來識別的。
  • 2、客戶端只需保證該端口號在本機上是惟一的就可以了。客戶端口號因存在時間很短暫又稱臨時端口號;
  • 3、大多數TCP/IP實現給臨時端口號分配1024—5000之間的端口號。大於5000的端口號是為其他服務器預留的。
  • 我們應該在自定義端口時,避免使用well-known的端口。如:80、21等等。

MTU

  • MTU:通信術語 最大傳輸單元(Maximum Transmission Unit,MTU)

  • 是指一種通信協議的某一層上面所能通過的最大數據包大小(以字節為單位)。最大傳輸單元這個參數通常與通信接口有關(網絡接口卡、串口等)。

  • 以下是一些協議的MTU:

      FDDI協議:4352字節
      以太網(Ethernet)協議:1500字節
      PPPoE(ADSL)協議:1492字節
      X.25協議(Dial Up/Modem):576字節
      Point-to-Point:4470字節
    

常見網絡知識面試題

  • 1、TCP如何建立鏈接
  • 2、TCP如何通信
  • 3、TCP如何關閉鏈接
  • 4、什么是滑動窗口
  • 5、什么是半關閉
  • 6、局域網內兩台機器如何利用TCP/IP通信
  • 7、internet上兩台主機如何進行通信
  • 8、如何在internet上識別唯一一個進程
    • 答:通過“IP地址+端口號”來區分不同的服務
  • 9、為什么說TCP是可靠的鏈接,UDP不可靠
  • 10、路由器和交換機的區別
  • 11、點到點,端到端

Socket編程

套接字概念

  • Socket本身有“插座”的意思,在Linux環境下,用於表示進程間網絡通信的特殊文件類型。本質為內核借助緩沖區形成的偽文件。

  • 既然是文件,那么理所當然的,我們可以使用文件描述符引用套接字。與管道類似的,Linux系統將其封裝成文件的目的是為了統一接口,使得讀寫套接字和讀寫文件的操作一致。區別是管道主要應用於本地進程間通信,而套接字多應用於網絡進程間數據的傳遞。

  • 套接字的內核實現較為復雜,不宜在學習初期深入學習。

  • 在TCP/IP協議中,“IP地址+TCP或UDP端口號”唯一標識網絡通訊中的一個進程。“IP地址+端口號”就對應一個socket。欲建立連接的兩個進程各自有一個socket來標識,那么這兩個socket組成的socket pair就唯一標識一個連接。因此可以用Socket來描述網絡連接的一對一關系。

  • 套接字通信原理如下圖所示:

套接字通訊原理示意

  • 在網絡通信中,套接字一定是成對出現的。一端的發送緩沖區對應對端的接收緩沖區。我們使用同一個文件描述符索發送緩沖區和接收緩沖區。

  • TCP/IP協議最早在BSD UNIX上實現,為TCP/IP協議設計的應用層編程接口稱為socket API。本章的主要內容是socket API,主要介紹TCP協議的函數接口,最后介紹UDP協議和UNIX Domain Socket的函數接口。

網絡編程接口

預備知識

網絡字節序

  • 我們已經知道,內存中的多字節數據相對於內存地址有大端和小端之分,磁盤文件中的多字節數據相對於文件中的偏移地址也有大端小端之分。網絡數據流同樣有大端小端之分,那么如何定義網絡數據流的地址呢?發送主機通常將發送緩沖區中的數據按內存地址從低到高的順序發出,接收主機把從網絡上接到的字節依次保存在接收緩沖區中,也是按內存地址從低到高的順序保存,因此,網絡數據流的地址應這樣規定:先發出的數據是低地址,后發出的數據是高地址。

  • TCP/IP協議規定,網絡數據流應采用大端字節序,即低地址高字節。例如上一節的UDP段格式,地址0-1是16位的源端口號,如果這個端口號是1000(0x3e8),則地址0是0x03,地址1是0xe8,也就是先發0x03,再發0xe8,這16位在發送主機的緩沖區中也應該是低地址存0x03,高地址存0xe8。但是,如果發送主機是小端字節序的,這16位被解釋成0xe803,而不是1000。因此,發送主機把1000填到發送緩沖區之前需要做字節序的轉換。同樣地,接收主機如果是小端字節序的,接到16位的源端口號也要做字節序的轉換。如果主機是大端字節序的,發送和接收都不需要做轉換。同理,32位的IP地址也要考慮網絡字節序和主機字節序的問題。

  • 為使網絡程序具有可移植性,使同樣的C代碼在大端和小端計算機上編譯后都能正常運行,可以調用以下庫函數做網絡字節序和主機字節序的轉換

      #include <arpa/inet.h>
      
      uint32_t htonl(uint32_t hostlong);
      uint16_t htons(uint16_t hostshort);
      uint32_t ntohl(uint32_t netlong);
      uint16_t ntohs(uint16_t netshort);
    
  • h表示host,n表示network,l表示32位長整數,s表示16位短整數。

  • 如果主機是小端字節序,這些函數將參數做相應的大小端轉換然后返回,如果主機是大端字節序,這些函數不做轉換,將參數原封不動地返回。

IP地址轉換函數

  • 早期:

      #include <sys/socket.h>
      #include <netinet/in.h>
      #include <arpa/inet.h>
      int inet_aton(const char *cp, struct in_addr *inp);
      in_addr_t inet_addr(const char *cp);
      char *inet_ntoa(struct in_addr in);
      只能處理IPv4的ip地址
      不可重入函數
      注意參數是struct in_addr
    
  • 現在:

      #include <arpa/inet.h>
      int inet_pton(int af, const char *src, void *dst);
      const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
    
      支持IPv4和IPv6
      可重入函數
      其中inet_pton和inet_ntop不僅可以轉換IPv4的in_addr,還可以轉換IPv6的in6_addr。
      因此函數接口是void *addrptr。
    

sockaddr數據結構

  • strcut sockaddr 很多網絡編程函數誕生早於IPv4協議,那時候都使用的是sockaddr結構體,為了向前兼容,現在sockaddr退化成了(void *)的作用,傳遞一個地址給函數,至於這個函數是sockaddr_in還是sockaddr_in6,由地址族確定,然后函數內部再強制類型轉化為所需的地址類型。

sockaddr數據結構

  • sockaddr數據結構

      struct sockaddr {
      	sa_family_t sa_family; 		/* address family, AF_xxx */
      	char sa_data[14];			/* 14 bytes of protocol address */
      };
    
  • 使用 sudo grep -r "struct sockaddr_in {" /usr 命令可查看到struct sockaddr_in結構體的定義。一般其默認的存儲位置:/usr/include/linux/in.h 文件中。

      struct sockaddr_in {
      	__kernel_sa_family_t sin_family; 			/* Address family */  	地址結構類型
      	__be16 sin_port;					 		/* Port number */		端口號
      	struct in_addr sin_addr;					/* Internet address */	IP地址
      	/* Pad to size of `struct sockaddr'. */
      	unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
      	sizeof(unsigned short int) - sizeof(struct in_addr)];
      };
      
      struct in_addr {						/* Internet address. */
      	__be32 s_addr;
      };
      
      struct sockaddr_in6 {
      	unsigned short int sin6_family; 		/* AF_INET6 */
      	__be16 sin6_port; 					/* Transport layer port # */
      	__be32 sin6_flowinfo; 				/* IPv6 flow information */
      	struct in6_addr sin6_addr;			/* IPv6 address */
      	__u32 sin6_scope_id; 				/* scope id (new in RFC2553) */
      };
      
      struct in6_addr {
      	union {
      		__u8 u6_addr8[16];
      		__be16 u6_addr16[8];
      		__be32 u6_addr32[4];
      	} in6_u;
      	#define s6_addr 		in6_u.u6_addr8
      	#define s6_addr16 	in6_u.u6_addr16
      	#define s6_addr32	 	in6_u.u6_addr32
      };
      
      #define UNIX_PATH_MAX 108
      	struct sockaddr_un {
      	__kernel_sa_family_t sun_family; 	/* AF_UNIX */
      	char sun_path[UNIX_PATH_MAX]; 	/* pathname */
      };
    
  • Pv4和IPv6的地址格式定義在netinet/in.h中,IPv4地址用sockaddr_in結構體表示,包括16位端口號和32位IP地址,IPv6地址用sockaddr_in6結構體表示,包括16位端口號、128位IP地址和一些控制字段。UNIX Domain Socket的地址格式定義在sys/un.h中,用sock-addr_un結構體表示。各種socket地址結構體的開頭都是相同的,前16位表示整個結構體的長度(並不是所有UNIX的實現都有長度字段,如Linux就沒有),后16位表示地址類型。IPv4、IPv6和Unix Domain Socket的地址類型分別定義為常數AF_INET、AF_INET6、AF_UNIX。這樣,只要取得某種sockaddr結構體的首地址,不需要知道具體是哪種類型的sockaddr結構體,就可以根據地址類型字段確定結構體中的內容。因此,socket API可以接受各種類型的sockaddr結構體指針做參數,例如bind、accept、connect等函數,這些函數的參數應該設計成void *類型以便接受各種類型的指針,但是sock API的實現早於ANSI C標准化,那時還沒有void *類型,因此這些函數的參數都用struct sockaddr *類型表示,在傳遞參數之前要強制類型轉換一下,例如:

      struct sockaddr_in servaddr;
      bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));		/* initialize servaddr */
    

網絡套接字函數

  • socket模型創建流程圖

socket API

  • socket函數

      #include <sys/types.h> /* See NOTES */
      #include <sys/socket.h>
      int socket(int domain, int type, int protocol);
      domain:
      	AF_INET 這是大多數用來產生socket的協議,使用TCP或UDP來傳輸,用IPv4的地址
      	AF_INET6 與上面類似,不過是來用IPv6的地址
      	AF_UNIX 本地協議,使用在Unix和Linux系統上,一般都是當客戶端和服務器在同一台及其上的時候使用
      type:
      	SOCK_STREAM 這個協議是按照順序的、可靠的、數據完整的基於字節流的連接。這是一個使用最多的socket類型,這個socket是使用TCP來進行傳輸。
      	SOCK_DGRAM 這個協議是無連接的、固定長度的傳輸調用。該協議是不可靠的,使用UDP來進行它的連接。
      	SOCK_SEQPACKET該協議是雙線路的、可靠的連接,發送固定長度的數據包進行傳輸。必須把這個包完整的接受才能進行讀取。
      	SOCK_RAW socket類型提供單一的網絡訪問,這個socket類型使用ICMP公共協議。(ping、traceroute使用該協議)
      	SOCK_RDM 這個類型是很少使用的,在大部分的操作系統上沒有實現,它是提供給數據鏈路層使用,不保證數據包的順序
      protocol:
      	傳0 表示使用默認協議。
      返回值:
      	成功:返回指向新創建的socket的文件描述符,失敗:返回-1,設置errno
    
    • socket()打開一個網絡通訊端口,如果成功的話,就像open()一樣返回一個文件描述符,應用程序可以像讀寫文件一樣用read/write在網絡上收發數據,如果socket()調用出錯則返回-1。對於IPv4,domain參數指定為AF_INET。對於TCP協議,type參數指定為SOCK_STREAM,表示面向流的傳輸協議。如果是UDP協議,則type參數指定為SOCK_DGRAM,表示面向數據報的傳輸協議。protocol參數的介紹從略,指定為0即可。
  • bind函數

      #include <sys/types.h> /* See NOTES */
      #include <sys/socket.h>
      int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
      sockfd:
      	socket文件描述符
      addr:
      	構造出IP地址加端口號
      addrlen:
      	sizeof(addr)長度
      返回值:
      	成功返回0,失敗返回-1, 設置errno
    
    • 服務器程序所監聽的網絡地址和端口號通常是固定不變的,客戶端程序得知服務器程序的地址和端口號后就可以向服務器發起連接,因此服務器需要調用bind綁定一個固定的網絡地址和端口號。

    • bind()的作用是將參數sockfd和addr綁定在一起,使sockfd這個用於網絡通訊的文件描述符監聽addr所描述的地址和端口號。前面講過,struct sockaddr *是一個通用指針類型,addr參數實際上可以接受多種協議的sockaddr結構體,而它們的長度各不相同,所以需要第三個參數addrlen指定結構體的長度。如:

        struct sockaddr_in servaddr;
        bzero(&servaddr, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        servaddr.sin_port = htons(6666);
      
    • 首先將整個結構體清零,然后設置地址類型為AF_INET,網絡地址為INADDR_ANY,這個宏表示本地的任意IP地址,因為服務器可能有多個網卡,每個網卡也可能綁定多個IP地址,這樣設置可以在所有的IP地址上監聽,直到與某個客戶端建立了連接時才確定下來到底用哪個IP地址,端口號為6666。

  • listen函數

      #include <sys/types.h> /* See NOTES */
      #include <sys/socket.h>
      int listen(int sockfd, int backlog);
      sockfd:
      	socket文件描述符
      backlog:
      	排隊建立3次握手隊列和剛剛建立3次握手隊列的鏈接數和
    
    • 查看系統默認backlog

        cat /proc/sys/net/ipv4/tcp_max_syn_backlog
      
    • 典型的服務器程序可以同時服務於多個客戶端,當有客戶端發起連接時,服務器調用的accept()返回並接受這個連接,如果有大量的客戶端發起連接而服務器來不及處理,尚未accept的客戶端就處於連接等待狀態,listen()聲明sockfd處於監聽狀態,並且最多允許有backlog個客戶端處於連接待狀態,如果接收到更多的連接請求就忽略。listen()成功返回0,失敗返回-1。

  • accept函數

      #include <sys/types.h> 		/* See NOTES */
      #include <sys/socket.h>
      int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
      sockdf:
      	socket文件描述符
      addr:
      	傳出參數,返回鏈接客戶端地址信息,含IP地址和端口號
      addrlen:
      	傳入傳出參數(值-結果),傳入sizeof(addr)大小,函數返回時返回真正接收到地址結構體的大小
      返回值:
      	成功返回一個新的socket文件描述符,用於和客戶端通信,失敗返回-1,設置errno
    
    • 三方握手完成后,服務器調用accept()接受連接,如果服務器調用accept()時還沒有客戶端的連接請求,就阻塞等待直到有客戶端連接上來。addr是一個傳出參數,accept()返回時傳出客戶端的地址和端口號。addrlen參數是一個傳入傳出參數(value-result argument),傳入的是調用者提供的緩沖區addr的長度以避免緩沖區溢出問題,傳出的是客戶端地址結構體的實際長度(有可能沒有占滿調用者提供的緩沖區)。如果給addr參數傳NULL,表示不關心客戶端的地址。

    • 我們的服務器程序結構是這樣的:

        while (1) {
        	cliaddr_len = sizeof(cliaddr);
        	connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
        	n = read(connfd, buf, MAXLINE);
        	......
        	close(connfd);
        }
      
    • 整個是一個while死循環,每次循環處理一個客戶端連接。由於cliaddr_len是傳入傳出參數,每次調用accept()之前應該重新賦初值。accept()的參數listenfd是先前的監聽文件描述符,而accept()的返回值是另外一個文件描述符connfd,之后與客戶端之間就通過這個connfd通訊,最后關閉connfd斷開連接,而不關閉listenfd,再次回到循環開頭listenfd仍然用作accept的參數。accept()成功返回一個文件描述符,出錯返回-1。

  • connect函數

      #include <sys/types.h> 					/* See NOTES */
      #include <sys/socket.h>
      int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
      sockdf:
      	socket文件描述符
      addr:
      	傳入參數,指定服務器端地址信息,含IP地址和端口號
      addrlen:
      	傳入參數,傳入sizeof(addr)大小
      返回值:
      	成功返回0,失敗返回-1,設置errno
    
    • 客戶端需要調用connect()連接服務器,connect和bind的參數形式一致,區別在於bind的參數是自己的地址,而connect的參數是對方的地址。connect()成功返回0,出錯返回-1。

C/S模型-TCP

  • 下圖是基於TCP協議的客戶端/服務器程序的一般流程:

TCP協議通訊流程

  • 服務器調用socket()、bind()、listen()完成初始化后,調用accept()阻塞等待,處於監聽端口的狀態,客戶端調用socket()初始化后,調用connect()發出SYN段並阻塞等待服務器應答,服務器應答一個SYN-ACK段,客戶端收到后從connect()返回,同時應答一個ACK段,服務器收到后從accept()返回。

  • 數據傳輸的過程:

    • 建立連接后,TCP協議提供全雙工的通信服務,但是一般的客戶端/服務器程序的流程是由客戶端主動發起請求,服務器被動處理請求,一問一答的方式。因此,服務器從accept()返回后立刻調用read(),讀socket就像讀管道一樣,如果沒有數據到達就阻塞等待,這時客戶端調用write()發送請求給服務器,服務器收到后從read()返回,對客戶端的請求進行處理,在此期間客戶端調用read()阻塞等待服務器的應答,服務器調用write()將處理結果發回給客戶端,再次調用read()阻塞等待下一條請求,客戶端收到后從read()返回,發送下一條請求,如此循環下去。
    • 如果客戶端沒有更多的請求了,就調用close()關閉連接,就像寫端關閉的管道一樣,服務器的read()返回0,這樣服務器就知道客戶端關閉了連接,也調用close()關閉連接。注意,任何一方調用close()后,連接的兩個傳輸方向都關閉,不能再發送數據了。如果一方調用shutdown()則連接處於半關閉狀態,仍可接收對方發來的數據。
  • 在學習socket API時要注意應用程序和TCP協議層是如何交互的: 應用程序調用某個socket函數時TCP協議層完成什么動作,比如調用connect()會發出SYN段 應用程序如何知道TCP協議層的狀態變化,比如從某個阻塞的socket函數返回就表明TCP協議收到了某些段,再比如read()返回0就表明收到了FIN段

server

  • 下面通過最簡單的客戶端/服務器程序的實例來學習socket API。

  • server.c的作用是從客戶端讀字符,然后將每個字符轉換為大寫並回送給客戶端。

      #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>
      #include <unistd.h>
      #include <sys/socket.h>
      #include <netinet/in.h>
      #include <arpa/inet.h>
      
      #define MAXLINE 80
      #define SERV_PORT 6666
      
      int main(void)
      {
      	struct sockaddr_in servaddr, cliaddr;
      	socklen_t cliaddr_len;
      	int listenfd, connfd;
      	char buf[MAXLINE];
      	char str[INET_ADDRSTRLEN];
      	int i, n;
      
      	listenfd = socket(AF_INET, SOCK_STREAM, 0);
      
      	bzero(&servaddr, sizeof(servaddr));
      	servaddr.sin_family = AF_INET;
      	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
      	servaddr.sin_port = htons(SERV_PORT);
      
      	bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
      	listen(listenfd, 20);
      
      	printf("Accepting connections ...\n");
      	while (1) {
      		cliaddr_len = sizeof(cliaddr);
      		connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
      		n = read(connfd, buf, MAXLINE);
      		printf("received from %s at PORT %d\n",
      		inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
      		ntohs(cliaddr.sin_port));
      		for (i = 0; i < n; i++)
      			buf[i] = toupper(buf[i]);
      		write(connfd, buf, n);
      		close(connfd);
      	}
      	return 0;
      }
    

client

  • client.c的作用是從命令行參數中獲得一個字符串發給服務器,然后接收服務器返回的字符串並打印。

      #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>
      #include <unistd.h>
      #include <sys/socket.h>
      #include <netinet/in.h>
      
      #define MAXLINE 80
      #define SERV_PORT 6666
      
      int main(int argc, char *argv[])
      {
      	struct sockaddr_in servaddr;
      	char buf[MAXLINE];
      	int sockfd, n;
      char *str;
      
      	if (argc != 2) {
      		fputs("usage: ./client message\n", stderr);
      		exit(1);
      	}
      str = argv[1];
      
      	sockfd = socket(AF_INET, SOCK_STREAM, 0);
      
      	bzero(&servaddr, sizeof(servaddr));
      	servaddr.sin_family = AF_INET;
      	inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
      	servaddr.sin_port = htons(SERV_PORT);
      
      	connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
      
      	write(sockfd, str, strlen(str));
      
      	n = read(sockfd, buf, MAXLINE);
      	printf("Response from server:\n");
      	write(STDOUT_FILENO, buf, n);
      	close(sockfd);
      
      	return 0;
      }
    
  • 由於客戶端不需要固定的端口號,因此不必調用bind(),客戶端的端口號由內核自動分配。注意,客戶端不是不允許調用bind(),只是沒有必要調用bind()固定一個端口號,服務器也不是必須調用bind(),但如果服務器不調用bind(),內核會自動給服務器分配監聽端口,每次啟動服務器時端口號都不一樣,客戶端要連接服務器就會遇到麻煩。

  • 客戶端和服務器啟動后可以使用netstat命令查看鏈接情況:

      netstat -apn|grep 6666
    

出錯處理封裝函數

  • 上面的例子不僅功能簡單,而且簡單到幾乎沒有什么錯誤處理,我們知道,系統調用不能保證每次都成功,必須進行出錯處理,這樣一方面可以保證程序邏輯正常,另一方面可以迅速得到故障信息。

  • 為使錯誤處理的代碼不影響主程序的可讀性,我們把與socket相關的一些系統函數加上錯誤處理代碼包裝成新的函數,做成一個模塊wrap.c:

  • wrap.c

      #include <stdlib.h>
      #include <errno.h>
      #include <sys/socket.h>
      void perr_exit(const char *s)
      {
      	perror(s);
      	exit(1);
      }
      int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
      {
      	int n;
      	again:
      	if ( (n = accept(fd, sa, salenptr)) < 0) {
      		if ((errno == ECONNABORTED) || (errno == EINTR))
      			goto again;
      		else
      			perr_exit("accept error");
      	}
      	return n;
      }
      int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
      {
      	int n;
      	if ((n = bind(fd, sa, salen)) < 0)
      		perr_exit("bind error");
      	return n;
      }
      int Connect(int fd, const struct sockaddr *sa, socklen_t salen)
      {
      	int n;
      	if ((n = connect(fd, sa, salen)) < 0)
      		perr_exit("connect error");
      	return n;
      }
      int Listen(int fd, int backlog)
      {
      	int n;
      	if ((n = listen(fd, backlog)) < 0)
      		perr_exit("listen error");
      	return n;
      }
      int Socket(int family, int type, int protocol)
      {
      	int n;
      	if ( (n = socket(family, type, protocol)) < 0)
      		perr_exit("socket error");
      	return n;
      }
      ssize_t Read(int fd, void *ptr, size_t nbytes)
      {
      	ssize_t n;
      again:
      	if ( (n = read(fd, ptr, nbytes)) == -1) {
      		if (errno == EINTR)
      			goto again;
      		else
      			return -1;
      	}
      	return n;
      }
      ssize_t Write(int fd, const void *ptr, size_t nbytes)
      {
      	ssize_t n;
      again:
      	if ( (n = write(fd, ptr, nbytes)) == -1) {
      		if (errno == EINTR)
      			goto again;
      		else
      			return -1;
      	}
      	return n;
      }
      int Close(int fd)
      {
      	int n;
      	if ((n = close(fd)) == -1)
      		perr_exit("close error");
      	return n;
      }
      ssize_t Readn(int fd, void *vptr, size_t n)
      {
      	size_t nleft;
      	ssize_t nread;
      	char *ptr;
      
      	ptr = vptr;
      	nleft = n;
      
      	while (nleft > 0) {
      		if ( (nread = read(fd, ptr, nleft)) < 0) {
      			if (errno == EINTR)
      				nread = 0;
      			else
      				return -1;
      		} else if (nread == 0)
      			break;
      		nleft -= nread;
      		ptr += nread;
      	}
      	return n - nleft;
      }
      
      ssize_t Writen(int fd, const void *vptr, size_t n)
      {
      	size_t nleft;
      	ssize_t nwritten;
      	const char *ptr;
      
      	ptr = vptr;
      	nleft = n;
      
      	while (nleft > 0) {
      		if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
      			if (nwritten < 0 && errno == EINTR)
      				nwritten = 0;
      			else
      				return -1;
      		}
      		nleft -= nwritten;
      		ptr += nwritten;
      	}
      	return n;
      }
      
      static ssize_t my_read(int fd, char *ptr)
      {
      	static int read_cnt;
      	static char *read_ptr;
      	static char read_buf[100];
      
      	if (read_cnt <= 0) {
      again:
      		if ((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
      			if (errno == EINTR)
      				goto again;
      			return -1;	
      		} else if (read_cnt == 0)
      			return 0;
      		read_ptr = read_buf;
      	}
      	read_cnt--;
      	*ptr = *read_ptr++;
      	return 1;
      }
      
      ssize_t Readline(int fd, void *vptr, size_t maxlen)
      {
      	ssize_t n, rc;
      	char c, *ptr;
      	ptr = vptr;
      
      	for (n = 1; n < maxlen; n++) {
      		if ( (rc = my_read(fd, &c)) == 1) {
      			*ptr++ = c;
      			if (c == '\n')
      				break;
      		} else if (rc == 0) {
      			*ptr = 0;
      			return n - 1;
      		} else
      			return -1;
      	}
      	*ptr = 0;
      	return n;
      }
    
  • wrap.h

      #ifndef __WRAP_H_
      #define __WRAP_H_
      void perr_exit(const char *s);
      int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
      int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
      int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
      int Listen(int fd, int backlog);
      int Socket(int family, int type, int protocol);
      ssize_t Read(int fd, void *ptr, size_t nbytes);
      ssize_t Write(int fd, const void *ptr, size_t nbytes);
      int Close(int fd);
      ssize_t Readn(int fd, void *vptr, size_t n);
      ssize_t Writen(int fd, const void *vptr, size_t n);
      ssize_t my_read(int fd, char *ptr);
      ssize_t Readline(int fd, void *vptr, size_t maxlen);
      #endif
    

高並發服務器

高並發服務器

多進程並發服務器

  • 使用多進程並發服務器時要考慮以下幾點:

    • 1、父進程最大文件描述個數(父進程中需要close關閉accept返回的新文件描述符)
    • 2、系統內創建進程個數(與內存大小相關)
    • 3、進程創建過多是否降低整體服務性能(進程調度)
  • server

      /* server.c */
      #include <stdio.h>
      #include <string.h>
      #include <netinet/in.h>
      #include <arpa/inet.h>
      #include <signal.h>
      #include <sys/wait.h>
      #include <sys/types.h>
      #include "wrap.h"
      
      #define MAXLINE 80
      #define SERV_PORT 800
      
      void do_sigchild(int num)
      {
      	while (waitpid(0, NULL, WNOHANG) > 0)
      		;
      }
      int main(void)
      {
      	struct sockaddr_in servaddr, cliaddr;
      	socklen_t cliaddr_len;
      	int listenfd, connfd;
      	char buf[MAXLINE];
      	char str[INET_ADDRSTRLEN];
      	int i, n;
      	pid_t pid;
      
      	struct sigaction newact;
      	newact.sa_handler = do_sigchild;
      	sigemptyset(&newact.sa_mask);
      	newact.sa_flags = 0;
      	sigaction(SIGCHLD, &newact, NULL);
      
      	listenfd = Socket(AF_INET, SOCK_STREAM, 0);
      
      	bzero(&servaddr, sizeof(servaddr));
      	servaddr.sin_family = AF_INET;
      	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
      	servaddr.sin_port = htons(SERV_PORT);
      
      	Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
      
      	Listen(listenfd, 20);
      
      	printf("Accepting connections ...\n");
      	while (1) {
      		cliaddr_len = sizeof(cliaddr);
      		connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
      
      		pid = fork();
      		if (pid == 0) {
      			Close(listenfd);
      			while (1) {
      				n = Read(connfd, buf, MAXLINE);
      				if (n == 0) {
      					printf("the other side has been closed.\n");
      					break;
      				}
      				printf("received from %s at PORT %d\n",
      						inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
      						ntohs(cliaddr.sin_port));
      				for (i = 0; i < n; i++)
      					buf[i] = toupper(buf[i]);
      				Write(connfd, buf, n);
      			}
      			Close(connfd);
      			return 0;
      		} else if (pid > 0) {
      			Close(connfd);
      		} else
      			perr_exit("fork");
      	}
      	Close(listenfd);
      	return 0;
      }
    
  • client

      /* client.c */
      #include <stdio.h>
      #include <string.h>
      #include <unistd.h>
      #include <netinet/in.h>
      #include "wrap.h"
      
      #define MAXLINE 80
      #define SERV_PORT 6666
      
      int main(int argc, char *argv[])
      {
      	struct sockaddr_in servaddr;
      	char buf[MAXLINE];
      	int sockfd, n;
      
      	sockfd = Socket(AF_INET, SOCK_STREAM, 0);
      
      	bzero(&servaddr, sizeof(servaddr));
      	servaddr.sin_family = AF_INET;
      	inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
      	servaddr.sin_port = htons(SERV_PORT);
      
      	Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
      
      	while (fgets(buf, MAXLINE, stdin) != NULL) {
      		Write(sockfd, buf, strlen(buf));
      		n = Read(sockfd, buf, MAXLINE);
      		if (n == 0) {
      			printf("the other side has been closed.\n");
      			break;
      		} else
      			Write(STDOUT_FILENO, buf, n);
      	}
      	Close(sockfd);
      	return 0;
      }
    

多線程並發服務器

  • 在使用線程模型開發服務器時需考慮以下問題:

    • 1、調整進程內最大文件描述符上限
    • 2、線程如有共享數據,考慮線程同步
    • 3、服務於客戶端線程退出時,退出處理。(退出值,分離態)
    • 4、系統負載,隨着鏈接客戶端增加,導致其它線程不能及時得到CPU
  • server

      /* server.c */
      #include <stdio.h>
      #include <string.h>
      #include <netinet/in.h>
      #include <arpa/inet.h>
      #include <pthread.h>
      
      #include "wrap.h"
      #define MAXLINE 80
      #define SERV_PORT 6666
      
      struct s_info {
      	struct sockaddr_in cliaddr;
      	int connfd;
      };
      void *do_work(void *arg)
      {
      	int n,i;
      	struct s_info *ts = (struct s_info*)arg;
      	char buf[MAXLINE];
      	char str[INET_ADDRSTRLEN];
      	/* 可以在創建線程前設置線程創建屬性,設為分離態,哪種效率高內? */
      	pthread_detach(pthread_self());
      	while (1) {
      		n = Read(ts->connfd, buf, MAXLINE);
      		if (n == 0) {
      			printf("the other side has been closed.\n");
      			break;
      		}
      		printf("received from %s at PORT %d\n",
      				inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)),
      				ntohs((*ts).cliaddr.sin_port));
      		for (i = 0; i < n; i++)
      			buf[i] = toupper(buf[i]);
      		Write(ts->connfd, buf, n);
      	}
      	Close(ts->connfd);
      }
      
      int main(void)
      {
      	struct sockaddr_in servaddr, cliaddr;
      	socklen_t cliaddr_len;
      	int listenfd, connfd;
      	int i = 0;
      	pthread_t tid;
      	struct s_info ts[256];
      
      	listenfd = Socket(AF_INET, SOCK_STREAM, 0);
      
      	bzero(&servaddr, sizeof(servaddr));
      	servaddr.sin_family = AF_INET;
      	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
      	servaddr.sin_port = htons(SERV_PORT);
      
      	Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
      	Listen(listenfd, 20);
      
      	printf("Accepting connections ...\n");
      	while (1) {
      		cliaddr_len = sizeof(cliaddr);
      		connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
      		ts[i].cliaddr = cliaddr;
      		ts[i].connfd = connfd;
      		/* 達到線程最大數時,pthread_create出錯處理, 增加服務器穩定性 */
      		pthread_create(&tid, NULL, do_work, (void*)&ts[i]);
      		i++;
      	}
      	return 0;
      }
    
  • client

      /* client.c */
      #include <stdio.h>
      #include <string.h>
      #include <unistd.h>
      #include <netinet/in.h>
      #include "wrap.h"
      #define MAXLINE 80
      #define SERV_PORT 6666
      int main(int argc, char *argv[])
      {
      	struct sockaddr_in servaddr;
      	char buf[MAXLINE];
      	int sockfd, n;
      
      	sockfd = Socket(AF_INET, SOCK_STREAM, 0);
      
      	bzero(&servaddr, sizeof(servaddr));
      	servaddr.sin_family = AF_INET;
      	inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
      	servaddr.sin_port = htons(SERV_PORT);
      
      	Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
      
      	while (fgets(buf, MAXLINE, stdin) != NULL) {
      		Write(sockfd, buf, strlen(buf));
      		n = Read(sockfd, buf, MAXLINE);
      		if (n == 0)
      			printf("the other side has been closed.\n");
      		else
      			Write(STDOUT_FILENO, buf, n);
      	}
      	Close(sockfd);
      	return 0;
      }
    

多路I/O轉接服務器

  • 多路IO轉接服務器也叫做多任務IO服務器。該類服務器實現的主旨思想是,不再由應用程序自己監視客戶端連接,取而代之由內核替應用程序監視文件。

  • 主要使用的方法有三種

  • select

    • 1、select能監聽的文件描述符個數受限於FD_SETSIZE,一般為1024,單純改變進程打開的文件描述符個數並不能改變select監聽文件個數

    • 2、解決1024以下客戶端時使用select是很合適的,但如果鏈接客戶端過多,select采用的是輪詢模型,會大大降低服務器響應效率,不應在select上投入更多精力

        #include <sys/select.h>
        /* According to earlier standards */
        #include <sys/time.h>
        #include <sys/types.h>
        #include <unistd.h>
        int select(int nfds, fd_set *readfds, fd_set *writefds,
        			fd_set *exceptfds, struct timeval *timeout);
        
        	nfds: 		監控的文件描述符集里最大文件描述符加1,因為此參數會告訴內核檢測前多少個文件描述符的狀態
        	readfds:	監控有讀數據到達文件描述符集合,傳入傳出參數
        	writefds:	監控寫數據到達文件描述符集合,傳入傳出參數
        	exceptfds:	監控異常發生達文件描述符集合,如帶外數據到達異常,傳入傳出參數
        	timeout:	定時阻塞監控時間,3種情況
        				1.NULL,永遠等下去
        				2.設置timeval,等待固定時間
        				3.設置timeval里時間均為0,檢查描述字后立即返回,輪詢
        	struct timeval {
        		long tv_sec; /* seconds */
        		long tv_usec; /* microseconds */
        	};
        	void FD_CLR(int fd, fd_set *set); 	//把文件描述符集合里fd清0
        	int FD_ISSET(int fd, fd_set *set); 	//測試文件描述符集合里fd是否置1
        	void FD_SET(int fd, fd_set *set); 	//把文件描述符集合里fd位置1
        	void FD_ZERO(fd_set *set); 			//把文件描述符集合里所有位清0
      
  • server

      /* server.c */
      #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>
      #include <netinet/in.h>
      #include <arpa/inet.h>
      #include "wrap.h"
      
      #define MAXLINE 80
      #define SERV_PORT 6666
      
      int main(int argc, char *argv[])
      {
      	int i, maxi, maxfd, listenfd, connfd, sockfd;
      	int nready, client[FD_SETSIZE]; 	/* FD_SETSIZE 默認為 1024 */
      	ssize_t n;
      	fd_set rset, allset;
      	char buf[MAXLINE];
      	char str[INET_ADDRSTRLEN]; 			/* #define INET_ADDRSTRLEN 16 */
      	socklen_t cliaddr_len;
      	struct sockaddr_in cliaddr, servaddr;
      
      	listenfd = Socket(AF_INET, SOCK_STREAM, 0);
      
      bzero(&servaddr, sizeof(servaddr));
      servaddr.sin_family = AF_INET;
      servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
      servaddr.sin_port = htons(SERV_PORT);
      
      Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
      
      Listen(listenfd, 20); 		/* 默認最大128 */
      
      maxfd = listenfd; 			/* 初始化 */
      maxi = -1;					/* client[]的下標 */
      
      for (i = 0; i < FD_SETSIZE; i++)
      	client[i] = -1; 		/* 用-1初始化client[] */
      
      FD_ZERO(&allset);
      FD_SET(listenfd, &allset); /* 構造select監控文件描述符集 */
      
      for ( ; ; ) {
      	rset = allset; 			/* 每次循環時都從新設置select監控信號集 */
      	nready = select(maxfd+1, &rset, NULL, NULL, NULL);
      
      	if (nready < 0)
      		perr_exit("select error");
      	if (FD_ISSET(listenfd, &rset)) { /* new client connection */
      		cliaddr_len = sizeof(cliaddr);
      		connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
      		printf("received from %s at PORT %d\n",
      				inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
      				ntohs(cliaddr.sin_port));
      		for (i = 0; i < FD_SETSIZE; i++) {
      			if (client[i] < 0) {
      				client[i] = connfd; /* 保存accept返回的文件描述符到client[]里 */
      				break;
      			}
      		}
      		/* 達到select能監控的文件個數上限 1024 */
      		if (i == FD_SETSIZE) {
      			fputs("too many clients\n", stderr);
      			exit(1);
      		}
      
      		FD_SET(connfd, &allset); 	/* 添加一個新的文件描述符到監控信號集里 */
      		if (connfd > maxfd)
      			maxfd = connfd; 		/* select第一個參數需要 */
      		if (i > maxi)
      			maxi = i; 				/* 更新client[]最大下標值 */
      
      		if (--nready == 0)
      			continue; 				/* 如果沒有更多的就緒文件描述符繼續回到上面select阻塞監聽,
      										負責處理未處理完的就緒文件描述符 */
      		}
      		for (i = 0; i <= maxi; i++) { 	/* 檢測哪個clients 有數據就緒 */
      			if ( (sockfd = client[i]) < 0)
      				continue;
      			if (FD_ISSET(sockfd, &rset)) {
      				if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
      					Close(sockfd);		/* 當client關閉鏈接時,服務器端也關閉對應鏈接 */
      					FD_CLR(sockfd, &allset); /* 解除select監控此文件描述符 */
      					client[i] = -1;
      				} else {
      					int j;
      					for (j = 0; j < n; j++)
      						buf[j] = toupper(buf[j]);
      					Write(sockfd, buf, n);
      				}
      				if (--nready == 0)
      					break;
      			}
      		}
      	}
      	close(listenfd);
      	return 0;
      }
    
  • client

      /* client.c */
      #include <stdio.h>
      #include <string.h>
      #include <unistd.h>
      #include <netinet/in.h>
      #include "wrap.h"
      
      #define MAXLINE 80
      #define SERV_PORT 6666
      
      int main(int argc, char *argv[])
      {
      	struct sockaddr_in servaddr;
      	char buf[MAXLINE];
      	int sockfd, n;
      
      	sockfd = Socket(AF_INET, SOCK_STREAM, 0);
      
      	bzero(&servaddr, sizeof(servaddr));
      	servaddr.sin_family = AF_INET;
      	inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
      	servaddr.sin_port = htons(SERV_PORT);
      
      	Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
      
      	while (fgets(buf, MAXLINE, stdin) != NULL) {
      		Write(sockfd, buf, strlen(buf));
      		n = Read(sockfd, buf, MAXLINE);
      		if (n == 0)
      			printf("the other side has been closed.\n");
      		else
      			Write(STDOUT_FILENO, buf, n);
      	}
      	Close(sockfd);
      	return 0;
      }
    
  • pselect

    • pselect原型如下。此模型應用較少,有需要的同學可參考select模型自行編寫C/S

        #include <sys/select.h>
        int pselect(int nfds, fd_set *readfds, fd_set *writefds,
        			fd_set *exceptfds, const struct timespec *timeout,
        			const sigset_t *sigmask);
        	struct timespec {
        		long tv_sec; /* seconds */
        		long tv_nsec; /* nanoseconds */
        	};
        	用sigmask替代當前進程的阻塞信號集,調用返回后還原原有阻塞信號集
      
  • poll

      #include <poll.h>
      int poll(struct pollfd *fds, nfds_t nfds, int timeout);
      struct pollfd {
      	int fd; /* 文件描述符 */
      	short events; /* 監控的事件 */
      	short revents; /* 監控事件中滿足條件返回的事件 */
      };
      POLLIN			普通或帶外優先數據可讀,即POLLRDNORM | POLLRDBAND
      POLLRDNORM		數據可讀
      POLLRDBAND		優先級帶數據可讀
      POLLPRI 		高優先級可讀數據
      POLLOUT		普通或帶外數據可寫
      POLLWRNORM		數據可寫
      POLLWRBAND		優先級帶數據可寫
      POLLERR 		發生錯誤
      POLLHUP 		發生掛起
      POLLNVAL 		描述字不是一個打開的文件
    
      nfds 			監控數組中有多少文件描述符需要被監控
    
      timeout 		毫秒級等待
      	-1:阻塞等,#define INFTIM -1 				Linux中沒有定義此宏
      	0:立即返回,不阻塞進程
      	>0:等待指定毫秒數,如當前系統時間精度不夠毫秒,向上取值
    
    • 如果不再監控某個文件描述符時,可以把pollfd中,fd設置為-1,poll不再監控此pollfd,下次返回時,把revents設置為0。
  • server

      /* server.c */
      #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>
      #include <netinet/in.h>
      #include <arpa/inet.h>
      #include <poll.h>
      #include <errno.h>
      #include "wrap.h"
      
      #define MAXLINE 80
      #define SERV_PORT 6666
      #define OPEN_MAX 1024
      
      int main(int argc, char *argv[])
      {
      	int i, j, maxi, listenfd, connfd, sockfd;
      	int nready;
      	ssize_t n;
      	char buf[MAXLINE], str[INET_ADDRSTRLEN];
      	socklen_t clilen;
      	struct pollfd client[OPEN_MAX];
      	struct sockaddr_in cliaddr, servaddr;
      
      	listenfd = Socket(AF_INET, SOCK_STREAM, 0);
      
      	bzero(&servaddr, sizeof(servaddr));
      	servaddr.sin_family = AF_INET;
      	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
      	servaddr.sin_port = htons(SERV_PORT);
      
      	Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
      
      	Listen(listenfd, 20);
      
      	client[0].fd = listenfd;
      	client[0].events = POLLRDNORM; 					/* listenfd監聽普通讀事件 */
      
      	for (i = 1; i < OPEN_MAX; i++)
      		client[i].fd = -1; 							/* 用-1初始化client[]里剩下元素 */
      	maxi = 0; 										/* client[]數組有效元素中最大元素下標 */
      
      	for ( ; ; ) {
      		nready = poll(client, maxi+1, -1); 			/* 阻塞 */
      		if (client[0].revents & POLLRDNORM) { 		/* 有客戶端鏈接請求 */
      			clilen = sizeof(cliaddr);
      			connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
      			printf("received from %s at PORT %d\n",
      					inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
      					ntohs(cliaddr.sin_port));
      			for (i = 1; i < OPEN_MAX; i++) {
      				if (client[i].fd < 0) {
      					client[i].fd = connfd; 	/* 找到client[]中空閑的位置,存放accept返回的connfd */
      					break;
      				}
      			}
      
      			if (i == OPEN_MAX)
      				perr_exit("too many clients");
      
      			client[i].events = POLLRDNORM; 		/* 設置剛剛返回的connfd,監控讀事件 */
      			if (i > maxi)
      				maxi = i; 						/* 更新client[]中最大元素下標 */
      			if (--nready <= 0)
      				continue; 						/* 沒有更多就緒事件時,繼續回到poll阻塞 */
      		}
      		for (i = 1; i <= maxi; i++) { 			/* 檢測client[] */
      			if ((sockfd = client[i].fd) < 0)
      				continue;
      			if (client[i].revents & (POLLRDNORM | POLLERR)) {
      				if ((n = Read(sockfd, buf, MAXLINE)) < 0) {
      					if (errno == ECONNRESET) { /* 當收到 RST標志時 */
      						/* connection reset by client */
      						printf("client[%d] aborted connection\n", i);
      						Close(sockfd);
      						client[i].fd = -1;
      					} else {
      						perr_exit("read error");
      					}
      				} else if (n == 0) {
      					/* connection closed by client */
      					printf("client[%d] closed connection\n", i);
      					Close(sockfd);
      					client[i].fd = -1;
      				} else {
      					for (j = 0; j < n; j++)
      						buf[j] = toupper(buf[j]);
      						Writen(sockfd, buf, n);
      				}
      				if (--nready <= 0)
      					break; 				/* no more readable descriptors */
      			}
      		}
      	}
      	return 0;
      }
    
  • client

      /* client.c */
      #include <stdio.h>
      #include <string.h>
      #include <unistd.h>
      #include <netinet/in.h>
      #include "wrap.h"
      
      #define MAXLINE 80
      #define SERV_PORT 6666
      
      int main(int argc, char *argv[])
      {
      	struct sockaddr_in servaddr;
      	char buf[MAXLINE];
      	int sockfd, n;
      
      	sockfd = Socket(AF_INET, SOCK_STREAM, 0);
      
      	bzero(&servaddr, sizeof(servaddr));
      	servaddr.sin_family = AF_INET;
      	inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
      	servaddr.sin_port = htons(SERV_PORT);
      
      	Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
      
      	while (fgets(buf, MAXLINE, stdin) != NULL) {
      		Write(sockfd, buf, strlen(buf));
      		n = Read(sockfd, buf, MAXLINE);
      		if (n == 0)
      			printf("the other side has been closed.\n");
      		else
      			Write(STDOUT_FILENO, buf, n);
      	}
      	Close(sockfd);
      	return 0;
      }
    
  • ppoll

    • GNU定義了ppoll(非POSIX標准),可以支持設置信號屏蔽字,大家可參考poll模型自行實現C/S。

        #define _GNU_SOURCE /* See feature_test_macros(7) */
        #include <poll.h>
        int ppoll(struct pollfd *fds, nfds_t nfds,
        		   const struct timespec *timeout_ts, const sigset_t *sigmask);
      
  • epoll

    • epoll是Linux下多路復用IO接口select/poll的增強版本,它能顯著提高程序在大量並發連接中只有少量活躍的情況下的系統CPU利用率,因為它會復用文件描述符集合來傳遞結果而不用迫使開發者每次等待事件之前都必須重新准備要被偵聽的文件描述符集合,另一點原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內核IO事件異步喚醒而加入Ready隊列的描述符集合就行了。

    • 目前epell是linux大規模並發網絡程序中的熱門首選模型。

    • epoll除了提供select/poll那種IO事件的電平觸發(Level Triggered)外,還提供了邊沿觸發(Edge Triggered),這就使得用戶空間程序有可能緩存IO狀態,減少epoll_wait/epoll_pwait的調用,提高應用程序效率。

    • 可以使用cat命令查看一個進程可以打開的socket描述符上限。

        cat /proc/sys/fs/file-max
      
    • 如有需要,可以通過修改配置文件的方式修改該上限值。

        sudo vi /etc/security/limits.conf
        在文件尾部寫入以下配置,soft軟限制,hard硬限制。如下圖所示。
        * soft nofile 65536
        * hard nofile 100000
      

/etc/security/limits.conf

基礎API

  • 1、創建一個epoll句柄,參數size用來告訴內核監聽的文件描述符的個數,跟內存大小有關。

      #include <sys/epoll.h>
      int epoll_create(int size)		size:監聽數目
    
  • 2、控制某個epoll監控的文件描述符上的事件:注冊、修改、刪除。

      #include <sys/epoll.h>
      int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
      	epfd:	為epoll_creat的句柄
      	op:		表示動作,用3個宏來表示:
      		EPOLL_CTL_ADD (注冊新的fd到epfd),
      		EPOLL_CTL_MOD (修改已經注冊的fd的監聽事件),
      		EPOLL_CTL_DEL (從epfd刪除一個fd);
      	event:	告訴內核需要監聽的事件
    
      	struct epoll_event {
      		__uint32_t events; /* Epoll events */
      		epoll_data_t data; /* User data variable */
      	};
      	typedef union epoll_data {
      		void *ptr;
      		int fd;
      		uint32_t u32;
      		uint64_t u64;
      	} epoll_data_t;
    
      	EPOLLIN :	表示對應的文件描述符可以讀(包括對端SOCKET正常關閉)
      	EPOLLOUT:	表示對應的文件描述符可以寫
      	EPOLLPRI:	表示對應的文件描述符有緊急的數據可讀(這里應該表示有帶外數據到來)
      	EPOLLERR:	表示對應的文件描述符發生錯誤
      	EPOLLHUP:	表示對應的文件描述符被掛斷;
      	EPOLLET: 	將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)而言的
      	EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里
    
  • 3、等待所監控文件描述符上有事件的產生,類似於select()調用。

      #include <sys/epoll.h>
      int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
      	events:		用來存內核得到事件的集合,
      	maxevents:	告之內核這個events有多大,這個maxevents的值不能大於創建epoll_create()時的size,
      	timeout:	是超時時間
      		-1:	阻塞
      		0:	立即返回,非阻塞
      		>0:	指定毫秒
      	返回值:	成功返回有多少文件描述符就緒,時間到時返回0,出錯返回-1
    
  • server

      #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>
      #include <netinet/in.h>
      #include <arpa/inet.h>
      #include <sys/epoll.h>
      #include <errno.h>
      #include "wrap.h"
      
      #define MAXLINE 80
      #define SERV_PORT 6666
      #define OPEN_MAX 1024
      
      int main(int argc, char *argv[])
      {
      	int i, j, maxi, listenfd, connfd, sockfd;
      	int nready, efd, res;
      	ssize_t n;
      	char buf[MAXLINE], str[INET_ADDRSTRLEN];
      	socklen_t clilen;
      	int client[OPEN_MAX];
      	struct sockaddr_in cliaddr, servaddr;
      	struct epoll_event tep, ep[OPEN_MAX];
      
      	listenfd = Socket(AF_INET, SOCK_STREAM, 0);
      
      	bzero(&servaddr, sizeof(servaddr));
      	servaddr.sin_family = AF_INET;
      	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
      	servaddr.sin_port = htons(SERV_PORT);
      
      	Bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
      
      	Listen(listenfd, 20);
      
      	for (i = 0; i < OPEN_MAX; i++)
      		client[i] = -1;
      	maxi = -1;
      
      	efd = epoll_create(OPEN_MAX);
      	if (efd == -1)
      		perr_exit("epoll_create");
      
      	tep.events = EPOLLIN; tep.data.fd = listenfd;
      
      	res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep);
      	if (res == -1)
      		perr_exit("epoll_ctl");
      
      	while (1) {
      		nready = epoll_wait(efd, ep, OPEN_MAX, -1); /* 阻塞監聽 */
      		if (nready == -1)
      			perr_exit("epoll_wait");
      
      		for (i = 0; i < nready; i++) {
      			if (!(ep[i].events & EPOLLIN))
      				continue;
      			if (ep[i].data.fd == listenfd) {
      				clilen = sizeof(cliaddr);
      				connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
      				printf("received from %s at PORT %d\n", 
      						inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), 
      						ntohs(cliaddr.sin_port));
      				for (j = 0; j < OPEN_MAX; j++) {
      					if (client[j] < 0) {
      						client[j] = connfd; /* save descriptor */
      						break;
      					}
      				}
      
      				if (j == OPEN_MAX)
      					perr_exit("too many clients");
      				if (j > maxi)
      					maxi = j; 		/* max index in client[] array */
      
      				tep.events = EPOLLIN; 
      				tep.data.fd = connfd;
      				res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep);
      				if (res == -1)
      					perr_exit("epoll_ctl");
      			} else {
      				sockfd = ep[i].data.fd;
      				n = Read(sockfd, buf, MAXLINE);
      				if (n == 0) {
      					for (j = 0; j <= maxi; j++) {
      						if (client[j] == sockfd) {
      							client[j] = -1;
      							break;
      						}
      					}
      					res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
      					if (res == -1)
      						perr_exit("epoll_ctl");
      
      					Close(sockfd);
      					printf("client[%d] closed connection\n", j);
      				} else {
      					for (j = 0; j < n; j++)
      						buf[j] = toupper(buf[j]);
      					Writen(sockfd, buf, n);
      				}
      			}
      		}
      	}
      	close(listenfd);
      	close(efd);
      	return 0;
      }
    
  • client

      /* client.c */
      #include <stdio.h>
      #include <string.h>
      #include <unistd.h>
      #include <netinet/in.h>
      #include "wrap.h"
      
      #define MAXLINE 80
      #define SERV_PORT 6666
      
      int main(int argc, char *argv[])
      {
      	struct sockaddr_in servaddr;
      	char buf[MAXLINE];
      	int sockfd, n;
      
      	sockfd = Socket(AF_INET, SOCK_STREAM, 0);
      
      	bzero(&servaddr, sizeof(servaddr));
      	servaddr.sin_family = AF_INET;
      	inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
      	servaddr.sin_port = htons(SERV_PORT);
      
      	Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
      
      	while (fgets(buf, MAXLINE, stdin) != NULL) {
      		Write(sockfd, buf, strlen(buf));
      		n = Read(sockfd, buf, MAXLINE);
      		if (n == 0)
      			printf("the other side has been closed.\n");
      		else
      			Write(STDOUT_FILENO, buf, n);
      	}
      
      	Close(sockfd);
      	return 0;
      }
    

epoll進階

事件模型

  • EPOLL事件有兩種模型:

    • Edge Triggered (ET) 邊緣觸發只有數據到來才觸發,不管緩存區中是否還有數據。
    • Level Triggered (LT) 水平觸發只要有數據都會觸發。
  • 思考如下步驟:

    • 1.假定我們已經把一個用來從管道中讀取數據的文件描述符(RFD)添加到epoll描述符。
    • 2.管道的另一端寫入了2KB的數據
    • 3.調用epoll_wait,並且它會返回RFD,說明它已經准備好讀取操作
    • 4.讀取1KB的數據
    • 5.調用epoll_wait……
  • 在這個過程中,有兩種工作模式:

    • ET模式

      • ET模式即Edge Triggered工作模式。
      • 如果我們在第1步將RFD添加到epoll描述符的時候使用了EPOLLET標志,那么在第5步調用epoll_wait之后將有可能會掛起,因為剩余的數據還存在於文件的輸入緩沖區內,而且數據發出端還在等待一個針對已經發出數據的反饋信息。只有在監視的文件句柄上發生了某個事件的時候 ET 工作模式才會匯報事件。因此在第5步的時候,調用者可能會放棄等待仍在存在於文件輸入緩沖區內的剩余數據。epoll工作在ET模式的時候,必須使用非阻塞套接口,以避免由於一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死。最好以下面的方式調用ET模式的epoll接口,在后面會介紹避免可能的缺陷。
        • 1)基於非阻塞文件句柄
        • 2)只有當read或者write返回EAGAIN(非阻塞讀,暫時無數據)時才需要掛起、等待。但這並不是說每次read時都需要循環讀,直到讀到產生一個EAGAIN才認為此次事件處理完成,當read返回的讀到的數據長度小於請求的數據長度時,就可以確定此時緩沖中已沒有數據了,也就可以認為此事讀事件已處理完成。
    • LT模式

      • LT模式即Level Triggered工作模式。
      • 與ET模式不同的是,以LT方式調用epoll接口的時候,它就相當於一個速度比較快的poll,無論后面的數據是否被使用。
      • LT(level triggered):LT是缺省的工作方式,並且同時支持block和no-block socket。在這種做法中,內核告訴你一個文件描述符是否就緒了,然后你可以對這個就緒的fd進行IO操作。如果你不作任何操作,內核還是會繼續通知你的,所以,這種模式編程出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表。
      • ET(edge-triggered):ET是高速工作方式,只支持no-block socket。在這種模式下,當描述符從未就緒變為就緒時,內核通過epoll告訴你。然后它會假設你知道文件描述符已經就緒,並且不會再為那個文件描述符發送更多的就緒通知。請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once).

實例一:

  • 基於管道epoll ET觸發模式

      #include <stdio.h>
      #include <stdlib.h>
      #include <sys/epoll.h>
      #include <errno.h>
      #include <unistd.h>
      
      #define MAXLINE 10
      
      int main(int argc, char *argv[])
      {
      	int efd, i;
      	int pfd[2];
      	pid_t pid;
      	char buf[MAXLINE], ch = 'a';
      
      	pipe(pfd);
      	pid = fork();
      	if (pid == 0) {
      		close(pfd[0]);
      		while (1) {
      			for (i = 0; i < MAXLINE/2; i++)
      				buf[i] = ch;
      			buf[i-1] = '\n';
      			ch++;
      
      			for (; i < MAXLINE; i++)
      				buf[i] = ch;
      			buf[i-1] = '\n';
      			ch++;
      
      			write(pfd[1], buf, sizeof(buf));
      			sleep(2);
      		}
      		close(pfd[1]);
      	} else if (pid > 0) {
      		struct epoll_event event;
      		struct epoll_event resevent[10];
      		int res, len;
      		close(pfd[1]);
      
      		efd = epoll_create(10);
      		/* event.events = EPOLLIN; */
      		event.events = EPOLLIN | EPOLLET;		/* ET 邊沿觸發 ,默認是水平觸發 */
      		event.data.fd = pfd[0];
      	epoll_ctl(efd, EPOLL_CTL_ADD, pfd[0], &event);
      
      		while (1) {
      			res = epoll_wait(efd, resevent, 10, -1);
      			printf("res %d\n", res);
      			if (resevent[0].data.fd == pfd[0]) {
      				len = read(pfd[0], buf, MAXLINE/2);
      				write(STDOUT_FILENO, buf, len);
      			}
      		}
      		close(pfd[0]);
      		close(efd);
      	} else {
      		perror("fork");
      		exit(-1);
      	}
      	return 0;
      }
    

實例二:

  • 基於網絡C/S模型的epoll ET觸發模式

  • server

      /* server.c */
      #include <stdio.h>
      #include <string.h>
      #include <netinet/in.h>
      #include <arpa/inet.h>
      #include <signal.h>
      #include <sys/wait.h>
      #include <sys/types.h>
      #include <sys/epoll.h>
      #include <unistd.h>
      
      #define MAXLINE 10
      #define SERV_PORT 8080
      
      int main(void)
      {
      	struct sockaddr_in servaddr, cliaddr;
      	socklen_t cliaddr_len;
      	int listenfd, connfd;
      	char buf[MAXLINE];
      	char str[INET_ADDRSTRLEN];
      	int i, efd;
      
      	listenfd = socket(AF_INET, SOCK_STREAM, 0);
      
      	bzero(&servaddr, sizeof(servaddr));
      	servaddr.sin_family = AF_INET;
      	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
      	servaddr.sin_port = htons(SERV_PORT);
      
      	bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
      
      	listen(listenfd, 20);
      
      	struct epoll_event event;
      	struct epoll_event resevent[10];
      	int res, len;
      	efd = epoll_create(10);
      	event.events = EPOLLIN | EPOLLET;		/* ET 邊沿觸發 ,默認是水平觸發 */
      
      	printf("Accepting connections ...\n");
      	cliaddr_len = sizeof(cliaddr);
      	connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
      	printf("received from %s at PORT %d\n",
      			inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
      			ntohs(cliaddr.sin_port));
      
      	event.data.fd = connfd;
      	epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event);
      
      	while (1) {
      		res = epoll_wait(efd, resevent, 10, -1);
      		printf("res %d\n", res);
      		if (resevent[0].data.fd == connfd) {
      			len = read(connfd, buf, MAXLINE/2);
      			write(STDOUT_FILENO, buf, len);
      		}
      	}
      	return 0;
      }
    
  • client

      /* client.c */
      #include <stdio.h>
      #include <string.h>
      #include <unistd.h>
      #include <netinet/in.h>
      
      #define MAXLINE 10
      #define SERV_PORT 8080
      
      int main(int argc, char *argv[])
      {
      	struct sockaddr_in servaddr;
      	char buf[MAXLINE];
      	int sockfd, i;
      	char ch = 'a';
      
      	sockfd = socket(AF_INET, SOCK_STREAM, 0);
      
      	bzero(&servaddr, sizeof(servaddr));
      	servaddr.sin_family = AF_INET;
      	inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
      	servaddr.sin_port = htons(SERV_PORT);
      
      	connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
      
      	while (1) {
      		for (i = 0; i < MAXLINE/2; i++)
      			buf[i] = ch;
      		buf[i-1] = '\n';
      		ch++;
      
      		for (; i < MAXLINE; i++)
      			buf[i] = ch;
      		buf[i-1] = '\n';
      		ch++;
      
      		write(sockfd, buf, sizeof(buf));
      		sleep(10);
      	}
      	Close(sockfd);
      	return 0;
      }
    

實例三:

  • 基於網絡C/S非阻塞模型的epoll ET觸發模式

  • server

      /* server.c */
      #include <stdio.h>
      #include <string.h>
      #include <netinet/in.h>
      #include <arpa/inet.h>
      #include <sys/wait.h>
      #include <sys/types.h>
      #include <sys/epoll.h>
      #include <unistd.h>
      #include <fcntl.h>
      
      #define MAXLINE 10
      #define SERV_PORT 8080
      
      int main(void)
      {
      	struct sockaddr_in servaddr, cliaddr;
      	socklen_t cliaddr_len;
      	int listenfd, connfd;
      	char buf[MAXLINE];
      	char str[INET_ADDRSTRLEN];
      	int i, efd, flag;
      
      	listenfd = socket(AF_INET, SOCK_STREAM, 0);
      
      	bzero(&servaddr, sizeof(servaddr));
      	servaddr.sin_family = AF_INET;
      	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
      	servaddr.sin_port = htons(SERV_PORT);
      
      	bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
      
      	listen(listenfd, 20);
      
      	struct epoll_event event;
      	struct epoll_event resevent[10];
      	int res, len;
      	efd = epoll_create(10);
      	/* event.events = EPOLLIN; */
      	event.events = EPOLLIN | EPOLLET;		/* ET 邊沿觸發 ,默認是水平觸發 */
      
      	printf("Accepting connections ...\n");
      	cliaddr_len = sizeof(cliaddr);
      	connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
      	printf("received from %s at PORT %d\n",
      			inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
      			ntohs(cliaddr.sin_port));
      
      	flag = fcntl(connfd, F_GETFL);
      	flag |= O_NONBLOCK;
      	fcntl(connfd, F_SETFL, flag);
      	event.data.fd = connfd;
      	epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event);
      
      	while (1) {
      		printf("epoll_wait begin\n");
      		res = epoll_wait(efd, resevent, 10, -1);
      		printf("epoll_wait end res %d\n", res);
      
      		if (resevent[0].data.fd == connfd) {
      			while ((len = read(connfd, buf, MAXLINE/2)) > 0)
      				write(STDOUT_FILENO, buf, len);
      		}
      	}
      	return 0;
      }
    
  • client

      /* client.c */
      #include <stdio.h>
      #include <string.h>
      #include <unistd.h>
      #include <netinet/in.h>
      
      #define MAXLINE 10
      #define SERV_PORT 8080
      
      int main(int argc, char *argv[])
      {
      	struct sockaddr_in servaddr;
      	char buf[MAXLINE];
      	int sockfd, i;
      	char ch = 'a';
      
      	sockfd = socket(AF_INET, SOCK_STREAM, 0);
      
      	bzero(&servaddr, sizeof(servaddr));
      	servaddr.sin_family = AF_INET;
      	inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
      	servaddr.sin_port = htons(SERV_PORT);
      
      	connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
      
      	while (1) {
      		for (i = 0; i < MAXLINE/2; i++)
      			buf[i] = ch;
      		buf[i-1] = '\n';
      		ch++;
      
      		for (; i < MAXLINE; i++)
      			buf[i] = ch;
      		buf[i-1] = '\n';
      		ch++;
      
      		write(sockfd, buf, sizeof(buf));
      		sleep(10);
      	}
      	Close(sockfd);
      	return 0;
      }
    

線程池並發服務器

  • 1.預先創建阻塞於accept多線程,使用互斥鎖上鎖保護accept
  • 2.預先創建多線程,由主線程調用accept

UDP服務器

  • 傳輸層主要應用的協議模型有兩種,一種是TCP協議,另外一種則是UDP協議。TCP協議在網絡通信中占主導地位,絕大多數的網絡通信借助TCP協議完成數據傳輸。但UDP也是網絡通信中不可或缺的重要通信手段。

  • 相較於TCP而言,UDP通信的形式更像是發短信。不需要在數據傳輸之前建立、維護連接。只專心獲取數據就好。省去了三次握手的過程,通信速度可以大大提高,但與之伴隨的通信的穩定性和正確率便得不到保證。因此,我們稱UDP為“無連接的不可靠報文傳遞”。

  • 那么與我們熟知的TCP相比,UDP有哪些優點和不足呢?由於無需創建連接,所以UDP開銷較小,數據傳輸速度快,實時性較強。多用於對實時性要求較高的通信場合,如視頻會議、電話會議等。但隨之也伴隨着數據傳輸不可靠,傳輸數據的正確率、傳輸順序和流量都得不到控制和保證。所以,通常情況下,使用UDP協議進行數據傳輸,為保證數據的正確性,我們需要在應用層添加輔助校驗協議來彌補UDP的不足,以達到數據可靠傳輸的目的。

  • 與TCP類似的,UDP也有可能出現緩沖區被填滿后,再接收數據時丟包的現象。由於它沒有TCP滑動窗口的機制,通常采用如下兩種方法解決:

    • 1)服務器應用層設計流量控制,控制發送數據速度。

    • 2)借助setsockopt函數改變接收緩沖區大小。如:

        #include <sys/socket.h>
        int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
        	int n = 220x1024
        	setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));
      

C/S模型-UDP

UDP處理模型

  • 由於UDP不需要維護連接,程序邏輯簡單了很多,但是UDP協議是不可靠的,保證通訊可靠性的機制需要在應用層實現。

  • 編譯運行server,在兩個終端里各開一個client與server交互,看看server是否具有並發服務的能力。用Ctrl+C關閉server,然后再運行server,看此時client還能否和server聯系上。和前面TCP程序的運行結果相比較,體會無連接的含義。

  • server

      #include <string.h>
      #include <netinet/in.h>
      #include <stdio.h>
      #include <unistd.h>
      #include <strings.h>
      #include <arpa/inet.h>
      #include <ctype.h>
      
      #define MAXLINE 80
      #define SERV_PORT 6666
      
      int main(void)
      {
      	struct sockaddr_in servaddr, cliaddr;
      	socklen_t cliaddr_len;
      	int sockfd;
      	char buf[MAXLINE];
      	char str[INET_ADDRSTRLEN];
      	int i, n;
      
      	sockfd = socket(AF_INET, SOCK_DGRAM, 0);
      
      	bzero(&servaddr, sizeof(servaddr));
      	servaddr.sin_family = AF_INET;
      	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
      	servaddr.sin_port = htons(SERV_PORT);
      
      	bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
      	printf("Accepting connections ...\n");
      
      	while (1) {
      		cliaddr_len = sizeof(cliaddr);
      		n = recvfrom(sockfd, buf, MAXLINE,0, (struct sockaddr *)&cliaddr, &cliaddr_len);
      		if (n == -1)
      			perror("recvfrom error");
      		printf("received from %s at PORT %d\n", 
      				inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
      				ntohs(cliaddr.sin_port));
      		for (i = 0; i < n; i++)
      			buf[i] = toupper(buf[i]);
      
      		n = sendto(sockfd, buf, n, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
      		if (n == -1)
      			perror("sendto error");
      	}
      	close(sockfd);
      	return 0;
      }
    
  • client

      #include <stdio.h>
      #include <string.h>
      #include <unistd.h>
      #include <netinet/in.h>
      #include <arpa/inet.h>
      #include <strings.h>
      #include <ctype.h>
      
      #define MAXLINE 80
      #define SERV_PORT 6666
      
      int main(int argc, char *argv[])
      {
      	struct sockaddr_in servaddr;
      	int sockfd, n;
      	char buf[MAXLINE];
      
      	sockfd = socket(AF_INET, SOCK_DGRAM, 0);
      
      	bzero(&servaddr, sizeof(servaddr));
      	servaddr.sin_family = AF_INET;
      	inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
      	servaddr.sin_port = htons(SERV_PORT);
      
      	while (fgets(buf, MAXLINE, stdin) != NULL) {
      		n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
      		if (n == -1)
      			perror("sendto error");
      		n = recvfrom(sockfd, buf, MAXLINE, 0, NULL, 0);
      		if (n == -1)
      			perror("recvfrom error");
      		write(STDOUT_FILENO, buf, n);
      	}
      	close(sockfd);
      	return 0;
      }
    

多播(組播)

  • 組播組可以是永久的也可以是臨時的。組播組地址中,有一部分由官方分配的,稱為永久組播組。永久組播組保持不變的是它的ip地址,組中的成員構成可以發生變化。永久組播組中成員的數量都可以是任意的,甚至可以為零。那些沒有保留下來供永久組播組使用的ip組播地址,可以被臨時組播組利用。

      224.0.0.0~224.0.0.255		為預留的組播地址(永久組地址),地址224.0.0.0保留不做分配,其它地址供路由協議使用;
      224.0.1.0~224.0.1.255		是公用組播地址,可以用於Internet;欲使用需申請。
      224.0.2.0~238.255.255.255	為用戶可用的組播地址(臨時組地址),全網范圍內有效;
      239.0.0.0~239.255.255.255	為本地管理組播地址,僅在特定的本地范圍內有效。
    
  • 可使用ip ad命令查看網卡編號,如:

      itcast$ ip ad
      1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default 
          link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
          inet 127.0.0.1/8 scope host lo
             valid_lft forever preferred_lft forever
          inet6 ::1/128 scope host 
             valid_lft forever preferred_lft forever
      2: eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN group default qlen 1000
          link/ether 00:0c:29:0a:c4:f4 brd ff:ff:ff:ff:ff:ff
          inet6 fe80::20c:29ff:fe0a:c4f4/64 scope link 
             valid_lft forever preferred_lft forever
    
    • if_nametoindex 命令可以根據網卡名,獲取網卡序號。
  • server

      #include <stdio.h>
      #include <stdlib.h>
      #include <sys/types.h>
      #include <sys/socket.h>
      #include <string.h>
      #include <unistd.h>
      #include <arpa/inet.h>
      #include <net/if.h>
      
      #define SERVER_PORT 6666
      #define CLIENT_PORT 9000
      #define MAXLINE 1500
      #define GROUP "239.0.0.2"
      
      int main(void)
      {
      	int sockfd, i ;
      	struct sockaddr_in serveraddr, clientaddr;
      	char buf[MAXLINE] = "itcast\n";
      	char ipstr[INET_ADDRSTRLEN]; /* 16 Bytes */
      	socklen_t clientlen;
      	ssize_t len;
      	struct ip_mreqn group;
      
      	/* 構造用於UDP通信的套接字 */
      	sockfd = socket(AF_INET, SOCK_DGRAM, 0);
      
      	bzero(&serveraddr, sizeof(serveraddr));
      	serveraddr.sin_family = AF_INET; /* IPv4 */
      	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); /* 本地任意IP INADDR_ANY = 0 */
      	serveraddr.sin_port = htons(SERVER_PORT);
      
      	bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
      
      	/*設置組地址*/
      	inet_pton(AF_INET, GROUP, &group.imr_multiaddr);
      	/*本地任意IP*/
      	inet_pton(AF_INET, "0.0.0.0", &group.imr_address);
      	/* eth0 --> 編號 命令:ip ad */
      	group.imr_ifindex = if_nametoindex("eth0");
      	setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_IF, &group, sizeof(group));
      
      	/*構造 client 地址 IP+端口 */
      	bzero(&clientaddr, sizeof(clientaddr));
      	clientaddr.sin_family = AF_INET; /* IPv4 */
      	inet_pton(AF_INET, GROUP, &clientaddr.sin_addr.s_addr);
      	clientaddr.sin_port = htons(CLIENT_PORT);
      
      	while (1) {
      		//fgets(buf, sizeof(buf), stdin);
      		sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&clientaddr, sizeof(clientaddr));
      		sleep(1);
      	}
      	close(sockfd);
      	return 0;
      }
    
  • client

      #include <netinet/in.h>
      #include <stdio.h>
      #include <sys/types.h>
      #include <sys/socket.h>
      #include <arpa/inet.h>
      #include <string.h>
      #include <stdlib.h>
      #include <sys/stat.h>
      #include <unistd.h>
      #include <fcntl.h>
      #include <net/if.h>
      
      #define SERVER_PORT 6666
      #define MAXLINE 4096
      #define CLIENT_PORT 9000
      #define GROUP "239.0.0.2"
      
      int main(int argc, char *argv[])
      {
      	struct sockaddr_in serveraddr, localaddr;
      	int confd;
      	ssize_t len;
      	char buf[MAXLINE];
      
      	/* 定義組播結構體 */
      	struct ip_mreqn group;
      	confd = socket(AF_INET, SOCK_DGRAM, 0);
      
      	//初始化本地端地址
      	bzero(&localaddr, sizeof(localaddr));
      	localaddr.sin_family = AF_INET;
      	inet_pton(AF_INET, "0.0.0.0" , &localaddr.sin_addr.s_addr);
      	localaddr.sin_port = htons(CLIENT_PORT);
      
      	bind(confd, (struct sockaddr *)&localaddr, sizeof(localaddr));
      
      	/*設置組地址*/
      	inet_pton(AF_INET, GROUP, &group.imr_multiaddr);
      	/*本地任意IP*/
      	inet_pton(AF_INET, "0.0.0.0", &group.imr_address);
      	/* eth0 --> 編號 命令:ip ad */
      	group.imr_ifindex = if_nametoindex("eth0");
      	/*設置client 加入多播組 */
      	setsockopt(confd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof(group));
      
      	while (1) {
      		len = recvfrom(confd, buf, sizeof(buf), 0, NULL, 0);
      		write(STDOUT_FILENO, buf, len);
      	}
      	close(confd);
      	return 0;
      }
    

socket IPC(本地套接字domain)

  • socket API原本是為網絡通訊設計的,但后來在socket的框架上發展出一種IPC機制,就是UNIX Domain Socket。雖然網絡socket也可用於同一台主機的進程間通訊(通過loopback地址127.0.0.1),但是UNIX Domain Socket用於IPC更有效率:不需要經過網絡協議棧,不需要打包拆包、計算校驗和、維護序號和應答等,只是將應用層數據從一個進程拷貝到另一個進程。這是因為,IPC機制本質上是可靠的通訊,而網絡協議是為不可靠的通訊設計的。UNIX Domain Socket也提供面向流和面向數據包兩種API接口,類似於TCP和UDP,但是面向消息的UNIX Domain Socket也是可靠的,消息既不會丟失也不會順序錯亂。

  • UNIX Domain Socket是全雙工的,API接口語義豐富,相比其它IPC機制有明顯的優越性,目前已成為使用最廣泛的IPC機制,比如X Window服務器和GUI程序之間就是通過UNIXDomain Socket通訊的。

  • 使用UNIX Domain Socket的過程和網絡socket十分相似,也要先調用socket()創建一個socket文件描述符,address family指定為AF_UNIX,type可以選擇SOCK_DGRAM或SOCK_STREAM,protocol參數仍然指定為0即可。

  • UNIX Domain Socket與網絡socket編程最明顯的不同在於地址格式不同,用結構體sockaddr_un表示,網絡編程的socket地址是IP地址加端口號,而UNIX Domain Socket的地址是一個socket類型的文件在文件系統中的路徑,這個socket文件由bind()調用創建,如果調用bind()時該文件已存在,則bind()錯誤返回。

  • 對比網絡套接字地址結構和本地套接字地址結構:

      struct sockaddr_in {
      __kernel_sa_family_t sin_family; 			/* Address family */  	地址結構類型
      __be16 sin_port;					 	/* Port number */		端口號
      struct in_addr sin_addr;					/* Internet address */	IP地址
      };
      struct sockaddr_un {
      __kernel_sa_family_t sun_family; 		/* AF_UNIX */			地址結構類型
      char sun_path[UNIX_PATH_MAX]; 		/* pathname */		socket文件名(含路徑)
      };
    
  • 以下程序將UNIX Domain socket綁定到一個地址。

      size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
      #define offsetof(type, member) ((int)&((type *)0)->MEMBER)
    
  • server

      #include <stdlib.h>
      #include <stdio.h>
      #include <stddef.h>
      #include <sys/socket.h>
      #include <sys/un.h>
      #include <sys/types.h>
      #include <sys/stat.h>
      #include <unistd.h>
      #include <errno.h>
      
      #define QLEN 10
      /*
      * Create a server endpoint of a connection.
      * Returns fd if all OK, <0 on error.
      */
      int serv_listen(const char *name)
      {
      	int fd, len, err, rval;
      	struct sockaddr_un un;
      
      	/* create a UNIX domain stream socket */
      	if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
      		return(-1);
      	/* in case it already exists */
      	unlink(name); 			
      
      	/* fill in socket address structure */
      	memset(&un, 0, sizeof(un));
      	un.sun_family = AF_UNIX;
      	strcpy(un.sun_path, name);
      	len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
      
      	/* bind the name to the descriptor */
      	if (bind(fd, (struct sockaddr *)&un, len) < 0) {
      		rval = -2;
      		goto errout;
      	}
      	if (listen(fd, QLEN) < 0) { /* tell kernel we're a server */
      		rval = -3;
      		goto errout;
      	}
      	return(fd);
      
      errout:
      	err = errno;
      	close(fd);
      	errno = err;
      	return(rval);
      }
      int serv_accept(int listenfd, uid_t *uidptr)
      {
      	int clifd, len, err, rval;
      	time_t staletime;
      	struct sockaddr_un un;
      	struct stat statbuf;
      
      	len = sizeof(un);
      	if ((clifd = accept(listenfd, (struct sockaddr *)&un, &len)) < 0)
      		return(-1); /* often errno=EINTR, if signal caught */
      
      	/* obtain the client's uid from its calling address */
      	len -= offsetof(struct sockaddr_un, sun_path); /* len of pathname */
      	un.sun_path[len] = 0; /* null terminate */
      
      	if (stat(un.sun_path, &statbuf) < 0) {
      		rval = -2;
      		goto errout;
      	}
      	if (S_ISSOCK(statbuf.st_mode) == 0) {
      		rval = -3; /* not a socket */
      		goto errout;
      	}
      	if (uidptr != NULL)
      		*uidptr = statbuf.st_uid; /* return uid of caller */
      	/* we're done with pathname now */
      	unlink(un.sun_path); 
      	return(clifd);
      
      errout:
      	err = errno;
      	close(clifd);
      	errno = err;
      	return(rval);
      }
      int main(void)
      {
      	int lfd, cfd, n, i;
      	uid_t cuid;
      	char buf[1024];
      	lfd = serv_listen("foo.socket");
      
      	if (lfd < 0) {
      		switch (lfd) {
      			case -3:perror("listen"); break;
      			case -2:perror("bind"); break;
      			case -1:perror("socket"); break;
      		}
      		exit(-1);
      	}
      	cfd = serv_accept(lfd, &cuid);
      	if (cfd < 0) {
      		switch (cfd) {
      			case -3:perror("not a socket"); break;
      			case -2:perror("a bad filename"); break;
      			case -1:perror("accept"); break;
      		}
      		exit(-1);
      	}
      	while (1) {
      r_again:
      		n = read(cfd, buf, 1024);
      		if (n == -1) {
      		if (errno == EINTR)
      		goto r_again;
      	}
      	else if (n == 0) {
      		printf("the other side has been closed.\n");
      		break;
      	}
      	for (i = 0; i < n; i++)
      		buf[i] = toupper(buf[i]);
      		write(cfd, buf, n);
      	}
      	close(cfd);
      	close(lfd);
      	return 0;
      }
    
  • client

      #include <stdio.h>
      #include <stdlib.h>
      #include <stddef.h>
      #include <sys/stat.h>
      #include <fcntl.h>
      #include <unistd.h>
      #include <sys/socket.h>
      #include <sys/un.h>
      #include <errno.h>
      
      #define CLI_PATH "/var/tmp/" /* +5 for pid = 14 chars */
      /*
      * Create a client endpoint and connect to a server.
      * Returns fd if all OK, <0 on error.
      */
      int cli_conn(const char *name)
      {
      	int fd, len, err, rval;
      	struct sockaddr_un un;
      
      	/* create a UNIX domain stream socket */
      	if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
      		return(-1);
      
      	/* fill socket address structure with our address */
      	memset(&un, 0, sizeof(un));
      	un.sun_family = AF_UNIX;
      	sprintf(un.sun_path, "%s%05d", CLI_PATH, getpid());
      	len = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
      
      	/* in case it already exists */
      	unlink(un.sun_path); 
      	if (bind(fd, (struct sockaddr *)&un, len) < 0) {
      		rval = -2;
      		goto errout;
      	}
      
      	/* fill socket address structure with server's address */
      	memset(&un, 0, sizeof(un));
      	un.sun_family = AF_UNIX;
      	strcpy(un.sun_path, name);
      	len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
      	if (connect(fd, (struct sockaddr *)&un, len) < 0) {
      		rval = -4;
      		goto errout;
      	}
      return(fd);
      	errout:
      	err = errno;
      	close(fd);
      	errno = err;
      	return(rval);
      }
      int main(void)
      {
      	int fd, n;
      	char buf[1024];
      
      	fd = cli_conn("foo.socket");
      	if (fd < 0) {
      		switch (fd) {
      			case -4:perror("connect"); break;
      			case -3:perror("listen"); break;
      			case -2:perror("bind"); break;
      			case -1:perror("socket"); break;
      		}
      		exit(-1);
      	}
      	while (fgets(buf, sizeof(buf), stdin) != NULL) {
      		write(fd, buf, strlen(buf));
      		n = read(fd, buf, sizeof(buf));
      		write(STDOUT_FILENO, buf, n);
      	}
      	close(fd);
      	return 0;
      }
    

其它常用函數

名字與地址轉換

  • gethostbyname根據給定的主機名,獲取主機信息。

  • 過時,僅用於IPv4,且線程不安全。

      #include <stdio.h>
      #include <netdb.h>
      #include <arpa/inet.h>
      
      extern int h_errno;
      
      int main(int argc, char *argv[])
      {
      	struct hostent *host;
      	char str[128];
      	host = gethostbyname(argv[1]);
      	printf("%s\n", host->h_name);
      
      	while (*(host->h_aliases) != NULL)
      		printf("%s\n", *host->h_aliases++);
      
      	switch (host->h_addrtype) {
      		case AF_INET:
      			while (*(host->h_addr_list) != NULL)
      			printf("%s\n", inet_ntop(AF_INET, (*host->h_addr_list++), str, sizeof(str)));
      		break;
      		default:
      			printf("unknown address type\n");
      			break;
      	}
      	return 0;
      }
    
  • gethostbyaddr函數。

  • 此函數只能獲取域名解析服務器的url和/etc/hosts里登記的IP對應的域名。

      #include <stdio.h>
      #include <netdb.h>
      #include <arpa/inet.h>
      
      extern int h_errno;
      
      int main(int argc, char *argv[])
      {
      	struct hostent *host;
      	char str[128];
      	struct in_addr addr;
      
      	inet_pton(AF_INET, argv[1], &addr);
      	host = gethostbyaddr((char *)&addr, 4, AF_INET);
      	printf("%s\n", host->h_name);
      
      	while (*(host->h_aliases) != NULL)
      		printf("%s\n", *host->h_aliases++);
      	switch (host->h_addrtype) {
      		case AF_INET:
      			while (*(host->h_addr_list) != NULL)
      			printf("%s\n", inet_ntop(AF_INET, (*host->h_addr_list++), str, sizeof(str)));
      			break;
      		default:
      			printf("unknown address type\n");
      			break;
      	}
      	return 0;
      }
    
  • getservbyname

  • getservbyport

    • 根據服務程序名字或端口號獲取信息。使用頻率不高。
  • getaddrinfo

  • getnameinfo

  • freeaddrinfo

    • 可同時處理IPv4和IPv6,線程安全的。

套接口和地址關聯

  • getsockname
    • 根據accpet返回的sockfd,得到臨時端口號
  • getpeername
    • 根據accpet返回的sockfd,得到遠端鏈接的端口號,在exec后可以獲取客戶端信息。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM