socket套接字是什麼

什麼是 socket?

socket 的原意是「插座」,在計算機通訊領域,socket 被翻譯爲「套接字」,它是計算機之間進行通訊的一種約定或一種方式。經過 socket 這種約定,一臺計算機能夠接收其餘計算機的數據,也能夠向其餘計算機發送數據。

咱們把插頭插到插座上就能從電網得到電力供應,一樣,爲了與遠程計算機進行數據傳輸,須要鏈接到因特網,而 socket 就是用來鏈接到因特網的工具。
 html

socket是什麼?


socket 的典型應用就是 Web 服務器和瀏覽器:瀏覽器獲取用戶輸入的 URL,向服務器發起請求,服務器分析接收到的 URL,將對應的網頁內容返回給瀏覽器,瀏覽器再通過解析和渲染,就將文字、圖片、視頻等元素呈現給用戶。

學習 socket,也就是學習計算機之間如何通訊,並編寫出實用的程序。linux

UNIX/Linux 中的 socket 是什麼?

在 UNIX/Linux 系統中,爲了統一對各類硬件的操做,簡化接口,不一樣的硬件設備也都被當作一個文件。對這些文件的操做,等同於對磁盤上普通文件的操做。

你也許聽不少高手說過,UNIX/Linux 中的一切都是文件!那個傢伙說的沒錯。

爲了表示和區分已經打開的文件,UNIX/Linux 會給每一個文件分配一個 ID,這個 ID 就是一個整數,被稱爲文件描述符(File Descriptor)。例如:算法

  • 一般用 0 來表示標準輸入文件(stdin),它對應的硬件設備就是鍵盤;
  • 一般用 1 來表示標準輸出文件(stdout),它對應的硬件設備就是顯示器。


UNIX/Linux 程序在執行任何形式的 I/O 操做時,都是在讀取或者寫入一個文件描述符。一個文件描述符只是一個和打開的文件相關聯的整數,它的背後多是一個硬盤上的普通文件、FIFO、管道、終端、鍵盤、顯示器,甚至是一個網絡鏈接。

請注意,網絡鏈接也是一個文件,它也有文件描述符!你必須理解這句話。

咱們能夠經過 socket() 函數來建立一個網絡鏈接,或者說打開一個網絡文件,socket() 的返回值就是文件描述符。有了文件描述符,咱們就可使用普通的文件操做函數來傳輸數據了,例如:編程

  • 用 read() 讀取從遠程計算機傳來的數據;
  • 用 write() 向遠程計算機寫入數據。


你看,只要用 socket() 建立了鏈接,剩下的就是文件操做了,網絡編程原來就是如此簡單!數組

WIndow 系統中的 socket 是什麼?

Windows 也有相似「文件描述符」的概念,但一般被稱爲「文件句柄」。所以,本教程若是涉及 Windows 平臺將使用「句柄」,若是涉及 Linux 平臺則使用「描述符」。

與 UNIX/Linux 不一樣的是,Windows 會區分 socket 和文件,Windows 就把 socket 當作一個網絡鏈接來對待,所以須要調用專門針對 socket 而設計的數據傳輸函數,針對普通文件的輸入輸出函數就無效了。瀏覽器

 

這個世界上有不少種套接字(socket),好比 DARPA Internet 地址(Internet 套接字)、本地節點的路徑名(Unix套接字)、CCITT X.25地址(X.25 套接字)等。但本教程只講第一種套接字——Internet 套接字,它是最具表明性的,也是最經典最經常使用的。之後咱們說起套接字,指的都是 Internet 套接字。

根據數據的傳輸方式,能夠將 Internet 套接字分紅兩種類型。經過 socket() 函數建立鏈接時,必須告訴它使用哪一種數據傳輸方式。服務器

Internet 套接字其實還有不少其它數據傳輸方式,可是我可不想嚇到你,本教程只講經常使用的兩種。

流格式套接字(SOCK_STREAM)

流格式套接字(Stream Sockets)也叫「面向鏈接的套接字」,在代碼中使用 SOCK_STREAM 表示。

SOCK_STREAM 是一種可靠的、雙向的通訊數據流,數據能夠準確無誤地到達另外一臺計算機,若是損壞或丟失,能夠從新發送。網絡

流格式套接字有本身的糾錯機制,在此咱們就不討論了。

SOCK_STREAM 有如下幾個特徵:併發

  • 數據在傳輸過程當中不會消失;
  • 數據是按照順序傳輸的;
  • 數據的發送和接收不是同步的(有的教程也稱「不存在數據邊界」)。


能夠將 SOCK_STREAM 比喻成一條傳送帶,只要傳送帶自己沒有問題(不會斷網),就能保證數據不丟失;同時,較晚傳送的數據不會先到達,較早傳送的數據不會晚到達,這就保證了數據是按照順序傳遞的。
 socket

將面向鏈接的套接字比喻成傳送帶


爲何流格式套接字能夠達到高質量的數據傳輸呢?這是由於它使用了 TCP 協議(The Transmission Control Protocol,傳輸控制協議),TCP 協議會控制你的數據按照順序到達而且沒有錯誤。

你也許見過 TCP,是由於你常常據說「TCP/IP」。TCP 用來確保數據的正確性,IP(Internet Protocol,網絡協議)用來控制數據如何從源頭到達目的地,也就是常說的「路由」。

那麼,「數據的發送和接收不一樣步」該如何理解呢?

假設傳送帶傳送的是水果,接收者須要湊齊 100 個後才能裝袋,可是傳送帶可能把這 100 個水果分批傳送,好比第一批傳送 20 個,第二批傳送 50 個,第三批傳送 30 個。接收者不須要和傳送帶保持同步,只要根據本身的節奏來裝袋便可,不用管傳送帶傳送了幾批,也不用每到一批就裝袋一次,能夠等到湊夠了 100 個水果再裝袋。

流格式套接字的內部有一個緩衝區(也就是字符數組),經過 socket 傳輸的數據將保存到這個緩衝區。接收端在收到數據後並不必定當即讀取,只要數據不超過緩衝區的容量,接收端有可能在緩衝區被填滿之後一次性地讀取,也可能分紅好幾回讀取。

也就是說,無論數據分幾回傳送過來,接收端只須要根據本身的要求讀取,不用非得在數據到達時當即讀取。傳送端有本身的節奏,接收端也有本身的節奏,它們是不一致的。

流格式套接字有什麼實際的應用場景嗎?瀏覽器所使用的 http 協議就基於面向鏈接的套接字,由於必需要確保數據準確無誤,不然加載的 HTML 將沒法解析。

數據報格式套接字(SOCK_DGRAM)

數據報格式套接字(Datagram Sockets)也叫「無鏈接的套接字」,在代碼中使用 SOCK_DGRAM 表示。

計算機只管傳輸數據,不做數據校驗,若是數據在傳輸中損壞,或者沒有到達另外一臺計算機,是沒有辦法補救的。也就是說,數據錯了就錯了,沒法重傳。

由於數據報套接字所作的校驗工做少,因此在傳輸效率方面比流格式套接字要高。

能夠將 SOCK_DGRAM 比喻成高速移動的摩托車快遞,它有如下特徵:

  • 強調快速傳輸而非傳輸順序;
  • 傳輸的數據可能丟失也可能損毀;
  • 限制每次傳輸的數據大小;
  • 數據的發送和接收是同步的(有的教程也稱「存在數據邊界」)。


衆所周知,速度是快遞行業的生命。用摩托車發往同一地點的兩件包裹無需保證順序,只要以最快的速度交給客戶就行。這種方式存在損壞或丟失的風險,並且包裹大小有必定限制。所以,想要傳遞大量包裹,就得分配發送。
 

將無鏈接套接字比喻成摩托車快遞


另外,用兩輛摩托車分別發送兩件包裹,那麼接收者也須要分兩次接收,因此「數據的發送和接收是同步的」;換句話說,接收次數應該和發送次數相同。

總之,數據報套接字是一種不可靠的、不按順序傳遞的、以追求速度爲目的的套接字。

數據報套接字也使用 IP 協議做路由,可是它不使用 TCP 協議,而是使用 UDP 協議(User Datagram Protocol,用戶數據報協議)。

QQ 視頻聊天和語音聊天就使用 SOCK_DGRAM 來傳輸數據,由於首先要保證通訊的效率,儘可能減少延遲,而數據的正確性是次要的,即便丟失很小的一部分數據,視頻和音頻也能夠正常解析,最多出現噪點或雜音,不會對通訊質量有實質的影響。

注意:SOCK_DGRAM 沒有想象中的糟糕,不會頻繁的丟失數據,數據錯誤只是小几率事件。

 

 

流格式套接字(Stream Sockets)就是「面向鏈接的套接字」,它基於 TCP 協議;數據報格式套接字(Datagram Sockets)就是「無鏈接的套接字」,它基於 UDP 協議。

這給你們形成一種印象,面向鏈接就是可靠的通訊,無鏈接就是不可靠的通訊,實際狀況是這樣嗎?

另外,無論是哪一種數據傳輸方式,都得經過整個 Internet 網絡的物理線路將數據傳輸過去,從這個層面理解,全部的 socket 都是有物理鏈接的呀,爲何還有無鏈接的 socket 呢?

本節就來給你們解開種種謎團,加深你們對數據傳輸方式的認識。

從字面上理解,面向鏈接好像有一條管道,它鏈接發送端和接收端,數據包都經過這條管道來傳輸。固然,兩臺計算機在通訊以前必須先搭建好管道。

無鏈接好像沒頭蒼蠅亂撞,數據包從發送端到接收端並無固定的線路,愛怎麼走就怎麼走,只要能到達就行。每一個數據包都比較自私,不和別人分享本身的線路,可是,你們最終都能異曲同工,到達接收端。

這樣理解沒錯,可是我相信這還不夠深刻,你們仍是感受雲裏霧裏,沒有看到本質。好,接下來就是見證奇蹟的時刻,我會用實例給你們演示!
 

一個簡化的互聯網模型


上圖是一個簡化的互聯網模型,H1 ~ H6 表示計算機,A~E 表示路由器,發送端發送的數據必須通過路由器的轉發才能到達接收端。

假設 H1 要發送若干個數據包給 H6,那麼有多條路徑能夠選擇,好比:

  • 路徑①:H1 --> A --> C --> E --> H6
  • 路徑②:H1 --> A --> B --> E --> H6
  • 路徑③:H1 --> A --> B --> D --> E --> H6
  • 路徑④:H1 --> A --> B --> C --> E --> H6
  • 路徑⑤:H1 --> A --> C --> B --> D --> E --> H6
數據包的傳輸路徑是路由器根據算法來計算出來的,算法會考慮不少因素,好比網絡的擁堵情況、下一個路由器是否忙碌等。

無鏈接的套接字

對於無鏈接的套接字,每一個數據包能夠選擇不一樣的路徑,好比第一個數據包選擇路徑④,第二個數據包選擇路徑①,第三個數據包選擇路徑②……固然,它們也能夠選擇相同的路徑,那也只不過是巧合而已。

每一個數據包之間都是獨立的,各走各的路,誰也不影響誰,除了迷路的或者發生意外的數據包,最後都能到達 H6。可是,到達的順序是不肯定的,好比:

  • 第一個數據包選擇了一條比較長的路徑(好比路徑⑤),第三個數據包選擇了一條比較短的路徑(好比路徑①),雖然第一個數據包很早就出發了,可是走的路比較遠,最終仍是晚於第三個數據包達到。
  • 第一個數據包選擇了一條比較短的路徑(好比路徑①),第三個數據包選擇了一條比較長的路徑(好比路徑⑤),按理說第一個數據包應該先到達,可是很是不幸,第一個數據包走的路比較擁堵,這條路上的數據量很是大,路由器處理得很慢,因此它仍是晚於第三個數據包達到了。


還有一些意外狀況會發生,好比:

  • 第一個數據包選擇了路徑①,可是路由器C忽然斷電了,那它就到不了 H6 了。
  • 第三個數據包選擇了路徑②,雖然路不遠,可是太擁堵,以致於它等待的時間太長,路由器把它丟棄了。


總之,對於無鏈接的套接字,數據包在傳輸過程當中會發生各類不測,也會發生各類奇蹟。H1 只負責把數據包發出,至於它何時到達,先到達仍是後到達,有沒有成功到達,H1 都無論了;H6 也沒有選擇的權利,只能被動接收,收到什麼算什麼,愛用不用。

無鏈接套接字遵循的是「盡最大努力交付」的原則,就是盡力而爲,實在作不到了也沒辦法。無鏈接套接字提供的沒有質量保證的服務。

面向鏈接的套接字

面向鏈接的套接字在正式通訊以前要先肯定一條路徑,沒有特殊狀況的話,之後就固定地使用這條路徑來傳遞數據包了。固然,路徑被破壞的話,好比某個路由器斷電了,那麼會從新創建路徑。
 

選好的路徑


這條路徑是由路由器維護的,路徑上的全部路由器都要存儲該路徑的信息(實際上只須要存儲上游和下游的兩個路由器的位置就行),因此路由器是有開銷的。

H1 和 H6 通訊完畢後,要斷開鏈接,銷燬路徑,這個時候路由器也會把以前存儲的路徑信息擦除。

在不少網絡通訊教程中,這條預先創建好的路徑被稱爲「虛電路」,就是一條虛擬的通訊電路。

爲了保證數據包準確、順序地到達,發送端在發送數據包之後,必須獲得接收端的確認才發送下一個數據包;若是數據包發出去了,一段時間之後仍然沒有獲得接收端的迴應,那麼發送端會從新再發送一次,直到獲得接收端的迴應。這樣一來,發送端發送的全部數據包都能到達接收端,而且是按照順序到達的。

發送端發送一個數據包,如何獲得接收端的確認呢?很簡單,爲每個數據包分配一個 ID,接收端接收到數據包之後,再給發送端返回一個數據包,告訴發送端我接收到了 ID 爲 xxx 的數據包。

面向鏈接的套接字會比無鏈接的套接字多出不少數據包,由於發送端每發送一個數據包,接收端就會返回一個數據包。此外,創建鏈接和斷開鏈接的過程也會傳遞不少數據包。

不可是數量多了,每一個數據包也變大了:除了源端口和目的端口,面向鏈接的套接字還包括序號、確認信號、數據偏移、控制標誌(一般說的 URG、ACK、PSH、RST、SYN、FIN)、窗口、校驗和、緊急指針、選項等信息;而無鏈接的套接字則只包含長度和校驗和信息。

有鏈接的數據包比無鏈接大不少,這意味着更大的負載和更大的帶寬。許多即時聊天軟件採用 UDP 協議(無鏈接套接字),與此有莫大的關係。

總結

兩種套接字各有優缺點:

  • 無鏈接套接字傳輸效率高,可是不可靠,有丟失數據包、搗亂數據的風險;
  • 有鏈接套接字很是可靠,萬無一失,可是傳輸效率低,耗費資源多。



兩種套接字的特色決定了它們的應用場景,有些服務對可靠性要求比較高,必須數據包可以完整無誤地送達,那就得選擇有鏈接的套接字(TCP 服務),好比 HTTP、FTP 等;而另外一些服務,並不須要那麼高的可靠性,效率和實時纔是它們所關心的,那就能夠選擇無鏈接的套接字(UDP 服務),好比 DNS、即時聊天工具等。

 

 

 

無論是 Windows 仍是 Linux,都使用 socket() 函數來建立套接字。socket() 在兩個平臺下的參數是相同的,不一樣的是返回值。

在《socket是什麼》一節中咱們講到了 Windows 和 Linux 在對待 socket 方面的區別。

Linux 中的一切都是文件,每一個文件都有一個整數類型的文件描述符;socket 也是一個文件,也有文件描述符。使用 socket() 函數建立套接字之後,返回值就是一個 int 類型的文件描述符。

Windows 會區分 socket 和普通文件,它把 socket 當作一個網絡鏈接來對待,調用 socket() 之後,返回值是 SOCKET 類型,用來表示一個套接字。

Linux 下的 socket() 函數

在 Linux 下使用 <sys/socket.h> 頭文件中 socket() 函數來建立套接字,原型爲:

int socket(int af, int type, int protocol);

1) af 爲地址族(Address Family),也就是 IP 地址類型,經常使用的有 AF_INET 和 AF_INET6。AF 是「Address Family」的簡寫,INET是「Inetnet」的簡寫。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。

你們須要記住127.0.0.1,它是一個特殊IP地址,表示本機地址,後面的教程會常常用到。

你也可使用 PF 前綴,PF 是「Protocol Family」的簡寫,它和 AF 是同樣的。例如,PF_INET 等價於 AF_INET,PF_INET6 等價於 AF_INET6。

2) type 爲數據傳輸方式/套接字類型,經常使用的有 SOCK_STREAM(流格式套接字/面向鏈接的套接字) 和 SOCK_DGRAM(數據報套接字/無鏈接的套接字),咱們已經在《套接字有哪些類型》一節中進行了介紹。

3) protocol 表示傳輸協議,經常使用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分別表示 TCP 傳輸協議和 UDP 傳輸協議。

有了地址類型和數據傳輸方式,還不足以決定採用哪一種協議嗎?爲何還須要第三個參數呢?

正如你們所想,通常狀況下有了 af 和 type 兩個參數就能夠建立套接字了,操做系統會自動推演出協議類型,除非遇到這樣的狀況:有兩種不一樣的協議支持同一種地址類型和數據傳輸類型。若是咱們不指明使用哪一種協議,操做系統是沒辦法自動推演的。

本教程使用 IPv4 地址,參數 af 的值爲 PF_INET。若是使用 SOCK_STREAM 傳輸數據,那麼知足這兩個條件的協議只有 TCP,所以能夠這樣來調用 socket() 函數:

int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  //IPPROTO_TCP表示TCP協議

這種套接字稱爲 TCP 套接字。

若是使用 SOCK_DGRAM 傳輸方式,那麼知足這兩個條件的協議只有 UDP,所以能夠這樣來調用 socket() 函數:

int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);  //IPPROTO_UDP表示UDP協議

這種套接字稱爲 UDP 套接字。

上面兩種狀況都只有一種協議知足條件,能夠將 protocol 的值設爲 0,系統會自動推演出應該使用什麼協議,以下所示:

int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);  //建立TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);  //建立UDP套接字

後面的教程中多采用這種簡化寫法。

在Windows下建立socket

Windows 下也使用 socket() 函數來建立套接字,原型爲:

SOCKET socket(int af, int type, int protocol);

除了返回值類型不一樣,其餘都是相同的。Windows 不把套接字做爲普通文件對待,而是返回 SOCKET 類型的句柄。請看下面的例子:

SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);  //建立TCP套接字

 

socket() 函數用來建立套接字,肯定套接字的各類屬性,而後服務器端要用 bind() 函數將套接字與特定的 IP 地址和端口綁定起來,只有這樣,流經該 IP 地址和端口的數據才能交給套接字處理。相似地,客戶端也要用 connect() 函數創建鏈接。

bind() 函數

bind() 函數的原型爲:

int bind(int sock, struct sockaddr *addr, socklen_t addrlen);  //Linux
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);  //Windows
下面以 Linux 爲例進行講解,Windows 與此相似。

sock 爲 socket 文件描述符,addr 爲 sockaddr 結構體變量的指針,addrlen 爲 addr 變量的大小,可由 sizeof() 計算得出。

下面的代碼,將建立的套接字與IP地址 127.0.0.一、端口 1234 綁定:

 
  1. //建立套接字
  2. int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  3.  
  4. //建立sockaddr_in結構體變量
  5. struct sockaddr_in serv_addr;
  6. memset(&serv_addr, 0, sizeof(serv_addr)); //每一個字節都用0填充
  7. serv_addr.sin_family = AF_INET; //使用IPv4地址
  8. serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具體的IP地址
  9. serv_addr.sin_port = htons(1234); //端口
  10.  
  11. //將套接字和IP、端口綁定
  12. bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

這裏咱們使用 sockaddr_in 結構體,而後再強制轉換爲 sockaddr 類型,後邊會講解爲何這樣作。

sockaddr_in 結構體

接下來不妨先看一下 sockaddr_in 結構體,它的成員變量以下:

 
  1. struct sockaddr_in{
  2. sa_family_t sin_family; //地址族(Address Family),也就是地址類型
  3. uint16_t sin_port; //16位的端口號
  4. struct in_addr sin_addr; //32位IP地址
  5. char sin_zero[8]; //不使用,通常用0填充
  6. };

1) sin_family 和 socket() 的第一個參數的含義相同,取值也要保持一致。

2) sin_prot 爲端口號。uint16_t 的長度爲兩個字節,理論上端口號的取值範圍爲 0~65536,但 0~1023 的端口通常由系統分配給特定的服務程序,例如 Web 服務的端口號爲 80,FTP 服務的端口號爲 21,因此咱們的程序要儘可能在 1024~65536 之間分配端口號。

端口號須要用 htons() 函數轉換,後面會講解爲何。

3) sin_addr 是 struct in_addr 結構體類型的變量,下面會詳細講解。

4) sin_zero[8] 是多餘的8個字節,沒有用,通常使用 memset() 函數填充爲 0。上面的代碼中,先用 memset() 將結構體的所有字節填充爲 0,再給前3個成員賦值,剩下的 sin_zero 天然就是 0 了。

in_addr 結構體

sockaddr_in 的第3個成員是 in_addr 類型的結構體,該結構體只包含一個成員,以下所示:

 
  1. struct in_addr{
  2. in_addr_t s_addr; //32位的IP地址
  3. };

in_addr_t 在頭文件 <netinet/in.h> 中定義,等價於 unsigned long,長度爲4個字節。也就是說,s_addr 是一個整數,而IP地址是一個字符串,因此須要 inet_addr() 函數進行轉換,例如:

 
  1. unsigned long ip = inet_addr("127.0.0.1");
  2. printf("%ld\n", ip);

運行結果:
16777343


圖解 sockaddr_in 結構體


爲何要搞這麼複雜,結構體中嵌套結構體,而不用 sockaddr_in 的一個成員變量來指明IP地址呢?socket() 函數的第一個參數已經指明瞭地址類型,爲何在 sockaddr_in 結構體中還要再說明一次呢,這不是囉嗦嗎?

這些繁瑣的細節確實給初學者帶來了必定的障礙,我想,這或許是歷史緣由吧,後面的接口總要兼容前面的代碼。各位讀者必定要有耐心,暫時不理解沒有關係,根據教程中的代碼「照貓畫虎」便可,時間久了天然會接受。

爲何使用 sockaddr_in 而不使用 sockaddr

bind() 第二個參數的類型爲 sockaddr,而代碼中卻使用 sockaddr_in,而後再強制轉換爲 sockaddr,這是爲何呢?

sockaddr 結構體的定義以下:

 
  1. struct sockaddr{
  2. sa_family_t sin_family; //地址族(Address Family),也就是地址類型
  3. char sa_data[14]; //IP地址和端口號
  4. };

下圖是 sockaddr 與 sockaddr_in 的對比(括號中的數字表示所佔用的字節數):


sockaddr 和 sockaddr_in 的長度相同,都是16字節,只是將IP地址和端口號合併到一塊兒,用一個成員 sa_data 表示。要想給 sa_data 賦值,必須同時指明IP地址和端口號,例如」127.0.0.1:80「,遺憾的是,沒有相關函數將這個字符串轉換成須要的形式,也就很難給 sockaddr 類型的變量賦值,因此使用 sockaddr_in 來代替。這兩個結構體的長度相同,強制轉換類型時不會丟失字節,也沒有多餘的字節。

能夠認爲,sockaddr 是一種通用的結構體,能夠用來保存多種類型的IP地址和端口號,而 sockaddr_in 是專門用來保存 IPv4 地址的結構體。另外還有 sockaddr_in6,用來保存 IPv6 地址,它的定義以下:

純文本複製
 
  1. struct sockaddr_in6 {
  2. sa_family_t sin6_family; //(2)地址類型,取值爲AF_INET6
  3. in_port_t sin6_port; //(2)16位端口號
  4. uint32_t sin6_flowinfo; //(4)IPv6流信息
  5. struct in6_addr sin6_addr; //(4)具體的IPv6地址
  6. uint32_t sin6_scope_id; //(4)接口範圍ID
  7. };

正是因爲通用結構體 sockaddr 使用不便,才針對不一樣的地址類型定義了不一樣的結構體。

connect() 函數

connect() 函數用來創建鏈接,它的原型爲:

int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);  //Linux
int connect(SOCKET sock, const struct sockaddr *serv_addr, int addrlen);  //Windows

各個參數的說明和 bind() 相同,再也不贅述。

 

對於服務器端程序,使用 bind() 綁定套接字後,還須要使用 listen() 函數讓套接字進入被動監聽狀態,再調用 accept() 函數,就能夠隨時響應客戶端的請求了。

listen() 函數

經過 listen() 函數可讓套接字進入被動監聽狀態,它的原型爲:

 
  1. int listen(int sock, int backlog); //Linux
  2. int listen(SOCKET sock, int backlog); //Windows

sock 爲須要進入監聽狀態的套接字,backlog 爲請求隊列的最大長度。

所謂被動監聽,是指當沒有客戶端請求時,套接字處於「睡眠」狀態,只有當接收到客戶端請求時,套接字纔會被「喚醒」來響應請求。

請求隊列

當套接字正在處理客戶端請求時,若是有新的請求進來,套接字是無法處理的,只能把它放進緩衝區,待當前請求處理完畢後,再從緩衝區中讀取出來處理。若是不斷有新的請求進來,它們就按照前後順序在緩衝區中排隊,直到緩衝區滿。這個緩衝區,就稱爲請求隊列(Request Queue)。

緩衝區的長度(能存放多少個客戶端請求)能夠經過 listen() 函數的 backlog 參數指定,但究竟爲多少並無什麼標準,能夠根據你的需求來定,併發量小的話能夠是10或者20。

若是將 backlog 的值設置爲 SOMAXCONN,就由系統來決定請求隊列長度,這個值通常比較大,多是幾百,或者更多。

當請求隊列滿時,就再也不接收新的請求,對於 Linux,客戶端會收到 ECONNREFUSED 錯誤,對於 Windows,客戶端會收到 WSAECONNREFUSED 錯誤。

注意:listen() 只是讓套接字處於監聽狀態,並無接收請求。接收請求須要使用 accept() 函數。

accept() 函數

當套接字處於監聽狀態時,能夠經過 accept() 函數來接收客戶端請求。它的原型爲:

純文本複製
 
  1. int accept(int sock, struct sockaddr *addr, socklen_t *addrlen); //Linux
  2. SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen); //Windows

它的參數與 listen() 和 connect() 是相同的:sock 爲服務器端套接字,addr 爲 sockaddr_in 結構體變量,addrlen 爲參數 addr 的長度,可由 sizeof() 求得。

accept() 返回一個新的套接字來和客戶端通訊,addr 保存了客戶端的IP地址和端口號,而 sock 是服務器端的套接字,你們注意區分。後面和客戶端通訊時,要使用這個新生成的套接字,而不是原來服務器端的套接字。

最後須要說明的是:listen() 只是讓套接字進入監聽狀態,並無真正接收客戶端請求,listen() 後面的代碼會繼續執行,直到遇到 accept()。accept() 會阻塞程序執行(後面代碼不能被執行),直到有新的請求到來。

 

在 Linux 和 Windows 平臺下,使用不一樣的函數發送和接收 socket 數據,下面咱們分別講解。

Linux下數據的接收和發送

Linux 不區分套接字文件和普通文件,使用 write() 能夠向套接字中寫入數據,使用 read() 能夠從套接字中讀取數據。

前面咱們說過,兩臺計算機之間的通訊至關於兩個套接字之間的通訊,在服務器端用 write() 向套接字寫入數據,客戶端就能收到,而後再使用 read() 從套接字中讀取出來,就完成了一次通訊。

write() 的原型爲:

ssize_t write(int fd, const void *buf, size_t nbytes);

fd 爲要寫入的文件的描述符,buf 爲要寫入的數據的緩衝區地址,nbytes 爲要寫入的數據的字節數。

size_t 是經過 typedef 聲明的 unsigned int 類型;ssize_t 在 "size_t" 前面加了一個"s",表明 signed,即 ssize_t 是經過 typedef 聲明的 signed int 類型。

write() 函數會將緩衝區 buf 中的 nbytes 個字節寫入文件 fd,成功則返回寫入的字節數,失敗則返回 -1。

read() 的原型爲:

ssize_t read(int fd, void *buf, size_t nbytes);

fd 爲要讀取的文件的描述符,buf 爲要接收數據的緩衝區地址,nbytes 爲要讀取的數據的字節數。

read() 函數會從 fd 文件中讀取 nbytes 個字節並保存到緩衝區 buf,成功則返回讀取到的字節數(但遇到文件結尾則返回0),失敗則返回 -1。

Windows下數據的接收和發送

Windows 和 Linux 不一樣,Windows 區分普通文件和套接字,並定義了專門的接收和發送的函數。

從服務器端發送數據使用 send() 函數,它的原型爲:

int send(SOCKET sock, const char *buf, int len, int flags);

sock 爲要發送數據的套接字,buf 爲要發送的數據的緩衝區地址,len 爲要發送的數據的字節數,flags 爲發送數據時的選項。

返回值和前三個參數再也不贅述,最後的 flags 參數通常設置爲 0 或 NULL,初學者沒必要深究。

在客戶端接收數據使用 recv() 函數,它的原型爲:

int recv(SOCKET sock, char *buf, int len, int flags);
相關文章
相關標籤/搜索