Linux 的套接字編程 (一)

 

一、需要的頭文件

數據類型:#include <sys/types.h>

函數定義:#include <sys/socket.h>

 

 

TCP/IP協議族:PF_INET

TCP/IP的地址族:AF_INET

 

 

二、socke函數

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

 

這一個函數在客戶端和服務器都要使用。 它是這樣被聲明的:

  返回值的類型與open的相同,一個整數。 FreeBSD從和文件句柄相同的池中分配它的值。 這就是允許套接字被以對文件相同的方式處理的原因。

  (1)參數domain告訴系統你需要使用什麼 協議族。有許多種協議族存在,有些是某些廠商專有的, 其它的都非常通用。協議族的聲明在sys/socket.h

  使用PF_INET是對於 UDPTCP 和其它 網間協議(IPv4)的情況。

  (2)對於參數type有五個定義好的值,也在 sys/socket.h中。這些值都以 “SOCK_”開頭。 其中最通用的是SOCK_STREAM, 它告訴系統你正需要一個可靠的流傳送服務 (和PF_INET一起使用時是指 TCP)提供了一個面向連接、可靠的數據傳輸服務,數據無差錯、無重複的發送且按發送順序接收。內設置流量控制,避免數據流淹沒慢的接收方。數據被看作是字節流,無長度限制。

  如果指定SOCK_DGRAM, 你是在請求無連接報文傳送服務 (在我們的情形中是UDP)數據包以獨立數據包的形式被髮送,不提供無差錯保證,數據可能丟失或重複,順序發送,可能亂序接收。。

  如何你需要處理基層協議 (例如IP),對較低層次協議,如IP、ICMP直接訪問或者甚至是網絡接口 (例如,以太網),你就需要指定 SOCK_RAW。 

  (3)參數protocol 取決於前兩個參數, 並非總是有意義。在以上情形中,使用取值0

 

三、Sockaddr 地址結構解析

  各種各樣的套接字函數需要指定地址,那是一小塊內存空間 (用C語言術語是指向一小塊內存空間的指針)。在 sys/socket.h中有各種各樣如struct sockaddr的聲明。 這個結構是這樣被聲明的:

/*
 * 內核用來存儲大多數種類地址的結構
 */
struct sockaddr {
    u_char   sa_len;     /* 總長度 */
    u_short sa_family;  /* 地址族 */
    char        sa_data[14];    /* 地址值,實際可能更長 */
};
#define SOCK_MAXADDRLEN 255     /* 可能的最長的地址長度 */

  sys/socket.h提到的各種類型的協議 將被按照地址族對待,並把它們就列在 sockaddr定義的前面:

 

 

用於指定IP的是 AF_INET。這個符號對應着常量 2

  在sockaddr中的域 sa_family指定地址族, 從而決定預先只確定下大致字節數的 sa_data的實際大小。

  特別是當地址族 是AF_INET時,我們可以使用 struct sockaddr_in,這可在 netinet/in.h中找到,任何需要 sockaddr的地方都以此作爲實際替代。

 

 

三個重要的域是: sin_family,結構體的字節1 1B; sin_port,16位值,在字節2和3 2B; sin_addr,一個32位整數,表示 IP地址,存儲在字節4-7 4B。

sin_addr被聲明爲類型 struct in_addr,這個類型定義在 netinet/in.h之中:

 

 

in_addr_t是一個32位整數。

  假設地址192.43.244.18,這是爲了表示32位整數的方便寫法,按每個八位二進制字節列出, 以最高位的字節開始。

傳入參數:

 

在不同計算機上會產生不同的效果(所謂的Big Endian和Little Endian)

 

Big Endian - PowerPC,Sparc64,etc

Little Endian - X86

 

所有網絡協議都是採用Big Endian的方式來傳輸數據的,而Intel X86主機採用的是Little Endian,所以我們需要注意這一點

需要使用對應的轉換函數

 

 

IP地址轉換函數

inet_addr() 點分十進制數表示的IP地址轉換爲網絡字節序的IP地址

inet_ntoa() 網絡字節序的IP地址轉換爲點分十進制數表示的IP地址

 

字節排序函數

 

     #include <arpa/inet.h>
     or
     #include <netinet/in.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);

 

 

三、客戶端函數

(1)connect函數

需要頭文件

 

#include <sys/types.h>
#include <sys/socket.h> 

 

一旦一個客戶端已經建立了一個套接字, 就需要把它連接到一個遠方系統的一個端口上。

 

 

參數 s 是套接字, 那是由函數socket返回的值。 name 是一個指向 sockaddr的指針,這個結構體我們已經展開討論過了。 最後,namelen通知系統 在我們的sockaddr結構體中有多少字節。

  如果 connect 成功, 返回 0。否則返回 -1 並將錯誤碼存放於 errno之中。

 

connect函數是阻塞模式函數,除非接受到相關數據否則一直等待,類似的函數還有recvfrom和recv函數

 

四、一個簡單的客戶端程序, 一個從192.43.244.18獲取當前時間並打印到 stdout的程序

 

 

五、服務器函數

 

      典型的服務器不初始化連接。 相反,服務器等待客戶端呼叫並請求服務。 服務器不知道客戶端什麼時候會呼叫, 也不知道有多少客戶端會呼叫。服務器就是這樣靜坐在那兒, 耐心等待,一會兒,又一會兒, 它突然發覺自身被從許多客戶端來的請求圍困, 所有的呼叫都同時來到。

  套接字接口提供三個基本的函數處理這種情況,bind,listen,accpet。

(1)bind函數

 

 我們使用bind函數 告訴套接字我們要服務的端口。

 

Sockfd:套接字描述符,指明創建連接的套接字

my_addr:本地地址,IP地址和端口號

addrlen :地址長度

 

(2)Listen函數

繼續我們的辦公室電話類比, 在你告訴電話中心操作員你會在哪個分機後, 現在你走進你的辦公室,確認你自己的電話已插上並且振鈴已被打開。 還有,你確認呼叫等待功能開啓,這樣即使你正在與其它人通話, 也可聽見電話振鈴。

Sockfd:套接字描述符,指明創建連接的套接字

input_queue_size:該套接字使用的隊列長度,指定在請求隊列中允許的最大請求數 

 

(3)accept函數

 

在你聽見電話鈴響後,你應答呼叫接起電話。 現在你已經建立起一個與你的客戶的連接。 這個連接保持到你或你的客戶掛線。

  服務器通過使用函數accept函數接受連接。

 

 

 

 

注意,這次 addrlen 是一個指針。 這是必要的,因爲在此情形中套接字要 填上 addr,這是一個 sockaddr_in 結構體。

  返回值是一個整數。其實, accept 返回一個 新 套接字。你將使用這個新套接字與客戶通信。

  老套接字會發生什麼呢?它繼續監聽更多的請求 (想起我們傳給listen的變量 backlog了嗎?),直到我們 close(關閉) 它。

  現在,新套接字僅對通信有意義,是完全接通的。 我們不能再把它傳給 listen接受更多的連接。

 

Sockfd:套接字描述符,指明正在監聽的套接字

addr:提出連接請求的主機地址

addrlen:地址長度

 

 

六、一個簡單的服務器程序

 

 

我們開始於建立一個套接字。然後我們填好 sockaddr_in 類型的結構體 sa。注意, INADDR_ANY的特定使用方法:

 

 

 

 

這個常量的值是0。由於我們已經使用 bzero於整個結構體, 再把成員設爲0將是冗餘。 但是如果我們把代碼移植到其它一些 INADDR_ANY可能不是0的系統上, 我們就需要把實際值指定給 sa.sin_addr.s_addr。多數現在C語言 編譯器已足夠智能,會注意到 INADDR_ANY是一個常量。由於它是0, 他們將會優化那段代碼外的整個條件語句。

  在我們成功調用bind後, 我們已經準備好成爲一個 守護進程:我們使用 fork建立一個子進程。 同在父進程和子進程裏,變量s都是套接字。 父進程不再需要它,於是調用了close, 然後返回0通知父進程的父進程成功終止。

  此時,子進程繼續在後臺工作。 它調用listen並設置 backlog 爲 4。這裏並不需要設置一個很大的值, 因爲 daytime 不是個總有許多客戶請求的協議, 並且總可以立即處理每個請求。

  最後,守護進程開始無休止循環,按照如下步驟:

  1. 調用accept。 在這裏等待直到一個客戶端與之聯繫。在這裏, 接收一個新套接字,c, 用來與其特定的客戶通信。

  2. 使用 C 語言函數 fdopen 把套接字從一個 低級 文件描述符 轉變成一個 C語言風格的 FILE 指針。 這使得後面可以使用 fprintf

  3. 檢查時間,按 ISO 8601格式打印到 “文件” client。 然後使用 fclose 關閉文件。 這會把套接字一同自動關閉。

  我們可把這些步驟 概括 起來, 作爲模型用於許多其它服務器:

 

 

 

 

 

這個流程圖很好的描述了順序服務器, 那是在某一時刻只能服務一個客戶的服務器, 就像我們的daytime服務器能做的那樣。 這隻能存在於客戶端與服務器沒有真正的“對話”的時候: 服務器一檢測到一個與客戶的連接,就送出一些數據並關閉連接。 整個操作只花費若干納秒就完成了。

  這張流程圖的好處是,除了在父進程 fork之後和父進程退出前的短暫時間內, 一直只有一個進程活躍: 我們的服務器不佔用許多內存和其它系統資源。

  注意我們已經將初始化守護進程 加入到我們的流程圖中。我們不需要初始化我們自己的守護進程 (譯者注:這裏僅指上面的示例程序。一般寫程序時都是需要的。), 但這是在程序流程中設置signal 處理程序、 打開我們可能需要的文件等操作的好地方。

  幾乎流程圖中的所有部分都可以用於描述許多不同的服務器。 條目 serve 是個例外,我們考慮爲一個 “黑盒子”, 那是你要爲你自己的服務器專門設計的東西, 並且 “接到其餘部分上”。

  並非所有協議都那麼簡單。許多協議收到一個來自客戶的請求, 回覆請求,然後接收下一個來自同一客戶的請求。 因此,那些協議不知道將要服務客戶多長時間。 這些服務器通常爲每個客戶啓動一個新進程 當新進程服務它的客戶時, 守護進程可以繼續監聽更多的連接。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

這個流程圖很好的描述了順序服務器, 那是在某一時刻只能服務一個客戶的服務器, 就像我們的daytime服務器能做的那樣。 這隻能存在於客戶端與服務器沒有真正的“對話”的時候: 服務器一檢測到一個與客戶的連接,就送出一些數據並關閉連接。 整個操作只花費若干納秒就完成了。

  這張流程圖的好處是,除了在父進程 fork之後和父進程退出前的短暫時間內, 一直只有一個進程活躍: 我們的服務器不佔用許多內存和其它系統資源。

  注意我們已經將初始化守護進程 加入到我們的流程圖中。我們不需要初始化我們自己的守護進程 (譯者注:這裏僅指上面的示例程序。一般寫程序時都是需要的。), 但這是在程序流程中設置signal 處理程序、 打開我們可能需要的文件等操作的好地方。

  幾乎流程圖中的所有部分都可以用於描述許多不同的服務器。 條目 serve 是個例外,我們考慮爲一個 “黑盒子”, 那是你要爲你自己的服務器專門設計的東西, 並且 “接到其餘部分上”。

  並非所有協議都那麼簡單。許多協議收到一個來自客戶的請求, 回覆請求,然後接收下一個來自同一客戶的請求。 因此,那些協議不知道將要服務客戶多長時間。 這些服務器通常爲每個客戶啓動一個新進程 當新進程服務它的客戶時, 守護進程可以繼續監聽更多的連接。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章