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)模式。須要在通信兩端各自部署客戶機和服務器來完成數據通訊。linux

    B/S模式

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

    優缺點

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

分層模型

OSI七層模型

OSI模型

  • 1.物理層:主要定義物理設備標準,如網線的接口類型、光纖的接口類型、各類傳輸介質的傳輸速率等。它的主要做用是傳輸比特流(就是由一、0轉化爲電流強弱來進行傳輸,到達目的地後再轉化爲一、0,也就是咱們常說的數模轉換與模數轉換)。這一層的數據叫作比特。
  • 2.數據鏈路層:定義瞭如何讓格式化數據以幀爲單位進行傳輸,以及如何讓控制對物理介質的訪問。這一層一般還提供錯誤檢測和糾正,以確保數據的可靠傳輸。如:串口通訊中使用到的115200、八、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應答。api

  • 看一個具體的例子。
  • 請求幀以下(爲了清晰在每行的前面加了字節計數,每行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。

  • 創建鏈接(三次握手)的過程:

    • 一、客戶端發送一個帶SYN標誌的TCP報文到服務器。這是三次握手過程當中的段1。
      客戶端發出段1,SYN位表示鏈接請求。序號是1000,這個序號在網絡通信中用做臨時的地址,每發一個數據字節,這個序號要加1,這樣在接收端能夠根據序號排出數據包的正確順序,也能夠發現丟包的狀況,另外,規定SYN位和FIN位也要佔一個序號,此次雖然沒發數據,可是因爲發了SYN位,所以下次再發送應該用序號1001。mss表示最大段尺寸,若是一個段太大,封裝成幀後超過了鏈路層的最大幀長度,就必須在IP層分片,爲了不這種狀況,客戶端聲明本身的最大段尺寸,建議服務器端發來的段不要超過這個長度。

    • 二、服務器端迴應客戶端,是三次握手中的第2個報文段,同時帶ACK標誌和SYN標誌。它表示對剛纔客戶端SYN的迴應;同時又發送SYN給客戶端,詢問客戶端是否準備好進行數據通信。
      服務器發出段2,也帶有SYN位,同時置ACK位表示確認,確認序號是1001,表示「我接收到序號1000及其之前全部的段,請你下次發送序號爲1001的段」,也就是應答了客戶端的鏈接請求,同時也給客戶端發出一個鏈接請求,同時聲明最大尺寸爲1024。

    • 三、客戶必須再次迴應服務器端一個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
  • 數據傳輸的過程:
    • 一、客戶端發出段4,包含從序號1001開始的20個字節數據。
    • 二、服務器發出段5,確認序號爲1021,對序號爲1001-1020的數據表示確認收到,同時請求發送序號1021開始的數據,服務器在應答的同時也向客戶端發送從序號8001開始的10個字節數據,這稱爲piggyback。
    • 三、客戶端發出段6,對服務器發來的序號爲8001-8010的數據表示確認收到,請求發送序號8011開始的數據。
    • 在數據傳輸過程當中,ACK和確認序號是很是重要的,應用程序交給TCP協議發送的數據會暫存在TCP層的發送緩衝區中,發出數據包給對方以後,只有收到對方應答的ACK段才知道該數據包確實發到了對方,能夠從發送緩衝區中釋放掉了,若是由於網絡故障丟失了數據包或者丟失了對方發回的ACK段,通過等待超時後TCP協議自動將發送緩衝區中的數據包重發。
  • 關閉鏈接(四次握手)的過程:
    • 因爲TCP鏈接是全雙工的,所以每一個方向都必須單獨進行關閉。這原則是當一方完成它的數據發送任務後就能發送一個FIN來終止這個方向的鏈接。收到一個 FIN只意味着這一方向上沒有數據流動,一個TCP鏈接在收到一個FIN後仍能發送數據。首先進行關閉的一方將執行主動關閉,而另外一方執行被動關閉。
    • 一、客戶端發出段7,FIN位表示關閉鏈接的請求。
    • 二、服務器發出段8,應答客戶端的關閉鏈接請求。
    • 三、服務器發出段9,其中也包含FIN位,向客戶端發送關閉鏈接請求。
    • 四、客戶端發出段10,應答服務器的關閉鏈接請求。
    • 創建鏈接的過程是三方握手,而關閉鏈接一般須要4個段,服務器的應答和關閉鏈接請求一般不合並在一個段中,由於有鏈接半關閉的狀況,這種狀況下客戶端關閉鏈接以後就不能再發送數據給服務器了,可是服務器還能夠發送數據給客戶端,直到服務器也關閉鏈接爲止。

滑動窗口 (TCP流量控制)

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

滑動窗口

  • 一、發送端發起鏈接,聲明最大段尺寸是1460,初始序號是0,窗口大小是4K,表示「個人接收緩衝區還有4K字節空閒,你發的數據不要超過4K」。接收端應答鏈接請求,聲明最大段尺寸是1024,初始序號是8000,窗口大小是6K。發送端應答,三方握手結束。
  • 二、發送端發出段4-9,每一個段帶1K的數據,發送端根據窗口大小知道接收端的緩衝區滿了,所以中止發送數據。
  • 三、接收端的應用程序提走2K數據,接收緩衝區又有了2K空閒,接收端發出段10,在應答已收到6K數據的同時聲明窗口大小爲2K。
  • 四、接收端的應用程序又提走2K數據,接收緩衝區有4K空閒,接收端發出段11,從新聲明窗口大小爲4K。
  • 五、發送端發出段12-13,每一個段帶2K數據,段13同時還包含FIN位。
  • 六、接收端應答接收到的2K數據(6145-8192),再加上FIN位佔一個序號8193,所以應答序號是8194,鏈接處於半關閉狀態,接收端同時聲明窗口大小爲2K。
  • 七、接收端的應用程序提走2K數據,接收端從新聲明窗口大小爲4K。
  • 八、接收端的應用程序提走剩下的2K數據,接收緩衝區全空,接收端從新聲明窗口大小爲6K。
  • 九、接收端的應用程序在提走所有數據後,決定關閉鏈接,發出段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不考慮描述符的引用計數,直接關閉描述符。也可選擇停止一個方向的鏈接,只停止讀或只停止寫。

  • 注意:
    • 一、若是有多個進程共享一個套接字,close每被調用一次,計數減1,直到計數爲0時,也就是所用進程都調用了close,套接字將被釋放。
    • 二、在多進程中若是一個進程調用了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,一種覆蓋一座或幾座大樓、一個校園或者一個廠區等地理區域的小範圍的計算機網。
  • 一、覆蓋的地理範圍較小,只在一個相對獨立的局部範圍內聯,如一座或集中的建築羣內。
  • 二、使用專門鋪設的傳輸介質進行聯網,數據傳輸速率高(10Mb/s~10Gb/s)
  • 三、通訊延遲時間短,可靠性較高
  • 四、局域網能夠支持多種傳輸介質

廣域網(WAN)

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

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

  • 廣域網的通訊子網能夠利用公用分組交換網、衛星通訊網和無線分組交換網,它將分佈在不一樣地區的局域網或計算機系統互連起來,達到資源共享的目的。如互聯網是世界範圍內最大的廣域網。
  • 一、適應大容量與突發性通訊的要求;
  • 二、適應綜合業務服務的要求;
  • 三、開放的設備接口與規範化的協議;
  • 四、完善的通訊服務與網絡管理。

端口

  • 邏輯意義上的端口,通常是指TCP/IP協議中的端口,端口號的範圍從0到65535,好比用於瀏覽網頁服務的80端口,用於FTP服務的21端口等等。
  • 一、端口號小於256的定義爲經常使用端口,服務器通常都是經過經常使用端口號來識別的。
  • 二、客戶端只需保證該端口號在本機上是唯一的就能夠了。客戶端口號因存在時間很短暫又稱臨時端口號;
  • 三、大多數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字節

常見網絡知識面試題

  • 一、TCP如何創建連接
  • 二、TCP如何通訊
  • 三、TCP如何關閉連接
  • 四、什麼是滑動窗口
  • 五、什麼是半關閉
  • 六、局域網內兩臺機器如何利用TCP/IP通訊
  • 七、internet上兩臺主機如何進行通訊
  • 八、如何在internet上識別惟一一個進程
    • 答:經過「IP地址+端口號」來區分不一樣的服務
  • 九、爲何說TCP是可靠的連接,UDP不可靠
  • 十、路由器和交換機的區別
  • 十一、點到點,端到端

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位表示地址類型。IPv四、IPv6和Unix Domain Socket的地址類型分別定義爲常數AF_INET、AF_INET六、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

高併發服務器

高併發服務器

多進程併發服務器

  • 使用多進程併發服務器時要考慮如下幾點:
    • 一、父進程最大文件描述個數(父進程中須要close關閉accept返回的新文件描述符)
    • 二、系統內建立進程個數(與內存大小相關)
    • 三、進程建立過可能是否下降總體服務性能(進程調度)
  • 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;
      }

多線程併發服務器

  • 在使用線程模型開發服務器時需考慮如下問題:
    • 一、調整進程內最大文件描述符上限
    • 二、線程若有共享數據,考慮線程同步
    • 三、服務於客戶端線程退出時,退出處理。(退出值,分離態)
    • 四、系統負載,隨着連接客戶端增長,致使其它線程不能及時獲得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
    • 一、select能監聽的文件描述符個數受限於FD_SETSIZE,通常爲1024,單純改變進程打開的文件描述符個數並不能改變select監聽文件個數
    • 二、解決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

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

    #include <sys/epoll.h>
      int epoll_create(int size)      size:監聽數目
  • 二、控制某個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隊列裏
  • 三、等待所監控文件描述符上有事件的產生,相似於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後能夠獲取客戶端信息。
相關文章
相關標籤/搜索