一、需要的頭文件
數據類型:#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
是對於 UDP, TCP 和其它 網間協議(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 不是個總有許多客戶請求的協議, 並且總可以立即處理每個請求。
最後,守護進程開始無休止循環,按照如下步驟:
-
調用
accept
。 在這裏等待直到一個客戶端與之聯繫。在這裏, 接收一個新套接字,c
, 用來與其特定的客戶通信。 -
使用 C 語言函數
fdopen
把套接字從一個 低級 文件描述符 轉變成一個 C語言風格的FILE
指針。 這使得後面可以使用fprintf
。 -
檢查時間,按 ISO 8601格式打印到 “文件”
client
。 然後使用fclose
關閉文件。 這會把套接字一同自動關閉。
我們可把這些步驟 概括 起來, 作爲模型用於許多其它服務器:
這個流程圖很好的描述了順序服務器, 那是在某一時刻只能服務一個客戶的服務器, 就像我們的daytime服務器能做的那樣。 這隻能存在於客戶端與服務器沒有真正的“對話”的時候: 服務器一檢測到一個與客戶的連接,就送出一些數據並關閉連接。 整個操作只花費若干納秒就完成了。
這張流程圖的好處是,除了在父進程 fork
之後和父進程退出前的短暫時間內, 一直只有一個進程活躍: 我們的服務器不佔用許多內存和其它系統資源。
注意我們已經將初始化守護進程 加入到我們的流程圖中。我們不需要初始化我們自己的守護進程 (譯者注:這裏僅指上面的示例程序。一般寫程序時都是需要的。), 但這是在程序流程中設置signal
處理程序、 打開我們可能需要的文件等操作的好地方。
幾乎流程圖中的所有部分都可以用於描述許多不同的服務器。 條目 serve 是個例外,我們考慮爲一個 “黑盒子”, 那是你要爲你自己的服務器專門設計的東西, 並且 “接到其餘部分上”。
並非所有協議都那麼簡單。許多協議收到一個來自客戶的請求, 回覆請求,然後接收下一個來自同一客戶的請求。 因此,那些協議不知道將要服務客戶多長時間。 這些服務器通常爲每個客戶啓動一個新進程 當新進程服務它的客戶時, 守護進程可以繼續監聽更多的連接。
這個流程圖很好的描述了順序服務器, 那是在某一時刻只能服務一個客戶的服務器, 就像我們的daytime服務器能做的那樣。 這隻能存在於客戶端與服務器沒有真正的“對話”的時候: 服務器一檢測到一個與客戶的連接,就送出一些數據並關閉連接。 整個操作只花費若干納秒就完成了。
這張流程圖的好處是,除了在父進程 fork
之後和父進程退出前的短暫時間內, 一直只有一個進程活躍: 我們的服務器不佔用許多內存和其它系統資源。
注意我們已經將初始化守護進程 加入到我們的流程圖中。我們不需要初始化我們自己的守護進程 (譯者注:這裏僅指上面的示例程序。一般寫程序時都是需要的。), 但這是在程序流程中設置signal
處理程序、 打開我們可能需要的文件等操作的好地方。
幾乎流程圖中的所有部分都可以用於描述許多不同的服務器。 條目 serve 是個例外,我們考慮爲一個 “黑盒子”, 那是你要爲你自己的服務器專門設計的東西, 並且 “接到其餘部分上”。
並非所有協議都那麼簡單。許多協議收到一個來自客戶的請求, 回覆請求,然後接收下一個來自同一客戶的請求。 因此,那些協議不知道將要服務客戶多長時間。 這些服務器通常爲每個客戶啓動一個新進程 當新進程服務它的客戶時, 守護進程可以繼續監聽更多的連接。