嵌入式linux網絡編程之——5年程序員給你深度講解socket套接字

<code>本文主要給大家分享網絡七層概念之網絡編程socket,前邊的章節已經給大家講述了鏈路層、物理層、網絡層、應用層、傳輸層等,歡迎學習嵌入式網絡編程的朋友關注、轉載和發表評論! 

(絕對的好文,建議先收藏和轉載!)/<code>

本文主要的講述內容為:

1 socket概述

2.1Socket 的歷史

2.2Socket 的功能

2.3套接字的三種類型

3套接字地址

3.1什麼是 Socket?

3.2Socket 描述符

3.3一個套接字是怎樣在網絡上傳輸數據的?

4套接字的一些基本知識

4.1基本結構

4.2基本轉換函數

5基本套接字調用

5.1socket()函數

5.2bind()函數

5.3connect()函數

5.4listen()函數

5.5accept()函數

5.6send()和 recv()函數

5.7sendto()和 recvfrom()函數

5.8close()和 shutdown()函數

5.9setsockopt()和 getsockopt()函數

5.10getpeername() 函數

5.11gethostname()函數


網絡程序設計全靠套接字socket接收和發送信息,儘管套接字這個詞好象顯得有些神秘,但其實這個概念 極易理解。

這章主要講述 Sockets API(Application Program Interface) ,以及一些其他的細節(比如Socket 的歷史、數據中的常用結構等), 通過這些介紹, 使讀者慢慢掌握 Linux 下的 Socket 編程。


在開始介紹有關編程的知識之前,首先讓我們來了解一些與 socket 有關的背景知識


8.2.1 Socket 的歷史

在 80 年代早期,遠景研究規劃局(Advanced Research Projects Agency, ARPA)資助了加利福尼亞大學伯克利分校的一個研究組,讓他們將 TCP/IP 軟件移植到 UNIX 操作系統中,並將結果提供給其他網點。作為項目的一部分,設計者們創建了一個接口,應用進程使用這個接口可以方便的進行通信。 他們決定,只要有可能就使用已有的系統調用,對那些不能方便的融入已有的函數集的情況,就再增加新的系統調用以支持 TCP/IP 功能。

這樣做的結果就出現了插口接口(Berkeley Socket),這個系統被稱為 Berkeley UNIX 或 BSD UNIX。由於許多計算機廠商,都採用了 Berkeley UNIX,所以許多機器上都可以使用 Socket 了。

這樣,Socket 接口就被廣泛使用,到現在已經成為事實上的標準。


嵌入式linux網絡編程之——5年程序員給你深度講解socket套接字

圖 8-1 SOCKET 接口示意圖

8.2.2 Socket 的功能

Socket 的英文原意就是"孔"或"插座" ,現在,作為 BSD UNIX 的進程通訊機制,取後一種意義。日常生活中常見的插座,有的是信號插座,有的是電源插座,有的可以接受信(或能量),有的可以發送信號(或能量)。假如電話線與電話機之間安放一個插座(相當於二者之間的接口,這一部分裝置物理上是存在的)則 Socket 非常相似於電話插座。

將電話系統與面向連接的 Socket 機制相比,有著驚人相似的地方。以一個國家級的電話網為例。電話的通話雙方相當於相互通信的兩個進程;通話雙方所在的地區(享有一個全局唯一的區號)相當於一個網絡,區號是它的網絡地址;區內的一個單位的交換機相當於一臺主機,主機分配給每個用戶的局內號碼相當於 Socket 號(下面將談到)。

任何用戶在通話之前,首先要佔有一部電話機,相當於申請一個 Socket 號;同時要知道對方的電話號碼,相當於對方有一個 Socket。然後向對方撥號呼叫,相當於發出連接請求(假如對方不在同一區內,還要撥對方區號,相當於給出網絡地址)。對方假如在場並空閒(相當於通信的另一主機開機且可以接受連接請求),拿起電話話筒,雙方就可以正式通話,相當於連接成功。雙方通話的過程,是向電話機發出信號和從電話機接受信號的過程,相當於向 Socket 發送數據和從 Socket 接受數據。通話結束後,一方掛起電話機,相當於關閉 Socket,撤消連接。

在電話系統中,一般用戶只能感受到本地電話機和對方電話號碼的存在,建立通話的過程、話音傳輸的過程以及整個電話系統的技術細節對它都是透明的,這也與 Socket 機制非常相似。Socket 利用網間網通信設施實現進程通信,但它對通信設施的細節毫不關心,只要通信設施能提供足夠的通信能力, 它就滿足了。

至此,我們對 Socket 進行了直觀的描述。抽象出來,Socket 實質上提供了進程通信的端點。進程通信之前,雙方首先必須各自創建一個端點,否則是沒有辦法建立聯繫並相互通信的。正如打電話之 前,雙方必須各自擁有一臺電話機一樣。

套接字是一個通信端的標識符,由 IP 地址和端口號確定,如<200.136.112.75:2520>。套接字與TCP/IP 應用程序接口(API)相關聯,每個 API 功能都需要一個套接字作為標識符。


兩個設備之間的數據交換可以描述為報文從某一個設備上的套接字發送到另一個設備上的套接字。 兩個套接字建立一個關聯,該關聯的組成元素如下:

{協議,本地地址,本地端口,遠程地址,遠程端口}


每一個 Socket 有一個本地的唯一 Socket 號,由操作系統分配

可以建立多個 Socket,以允許幾個應用共享同一個設備 IP 地址。另外,在一個設備上可以存在多個關聯,這意味著一個設備可以同時與其他設備有多個連接。

最重要的是,Socket 是面向客戶—服務器模型而設計的,針對客戶和服務器程序提供不同的

Socket 系統調用。客戶隨機申請一個 Socket 號(相當於一個想打電話的人可以在任何一臺入網的電話上撥叫呼叫);服務器擁有全局公認的 Socket,任何客戶都可以向它發出連接請求和信息請求(相當於一個被呼叫的電話擁有一個呼叫方知道的電話號碼)。

Socket 利用客戶— 服務器模式巧妙的解決了進程之間建立通信連接的問題。服務器 Socket 為全局所公認非常重要。兩個完全隨機的用戶進程之間,因為沒有任何一方的 Socket 是固定的,就像打電話卻不知道別人的電話號碼,要通話是不可能的。


8.2.3 套接字的三種類型

套接字有三種類型:流式套接字(SOCK_STREAM),數據報套接字(SOCK_DGRAM)及原始套接字


1.流式套接字(SOCK_STREAM)

流式的套接字可以提供可靠的、面向連接的通訊流。如果你通過流式套接字發送了順序的數據:

"1"、"2" 。那麼數據到達遠程時候的順序也是"1"、"2"。


流式套接字可以做什麼呢?你聽說過 Telnet 應用程序嗎?聽過?哦,最常用的 BBS 服務,以及系統的遠程登陸都是通過 Telnet 協議連接的。Telnet 就是一個流式連接。你是否希望你在 Telnet 應用程序上輸入的字符(或漢字)在到達遠程應用程序的時候是以你輸入的順序到達的?答案應該是肯定 的吧。還有 WWW 瀏覽器,它使用的 HTTP 協議也是通過流式套接字來獲取網頁的。事實上,如果你

Telnet 到一個 Web Site 的 80 端口上,然後輸入 "GET 網頁路徑名"然後按兩下回車(或者是兩下

Ctrl+回車)然後你就得到了"網頁路徑名"所代表的網頁!


流式套接字是怎樣保證這種應用層次上的數據傳輸質量呢?它使用了 TCP(The Transmission Control Protocol)協議(可以參考 RFC-793 來得到 TCP 的細節)。TCP 保證了你的數據傳輸是正確的,並且是順序的。TCP 是經常出現的 TCP/IP 中的前半部分。IP 代表 Internet Protocol(因特網協議,參考 RFC-791),IP 只處理網絡路由。

面向連接服務器處理的請求往往比較複雜,不是一來一去的請求應答所能解決的,而且往往是併發 服務器。使用面向連接的套接字編程,可以通過圖 2-2 來表示。

套接字工作過程如下:服務器首先啟動,通過調用 socket() 建立一個套接字,然後調用 bind() 將該套接字和本地網絡地址聯繫在一起,再調用 listen() 使套接字做好偵聽的準備,並規定它的請求隊列的長度,之後就調用 accept() 來接收連接。客戶在建立套接字後就可調用 connect() 和服務器建立連接。連接一旦建立,客戶機和服務器之間就可以通過調用 send() 和 recv() 來發送和接收數據。最後,待數據傳送結束後,雙方調用 close() 關閉套接字。


嵌入式linux網絡編程之——5年程序員給你深度講解socket套接字

圖 8-2 面向連接的 socket 的工作流程


2.數據報套接字(SOCK_DGRAM)

數據報套接字(Datagram Sockets)定義了一種無連接的服務,數據通過相互獨立的報文進行傳輸, 是無序的,並且不保證可靠,無差錯。


為什麼它叫做"無連接"?應該怎樣處理它們呢?為什麼它們是不可靠的?好的,這裡有一些事實:

· 如果你發送了一個數據報,它可能不會到達。

· 它可能會以不同的順序到達。

· 如果它到達了,它包含的數據中可能存在錯誤


數據報套接字也使用 IP,但是它不使用 TCP,它使用用戶數據報協議 UDP(User Datagram

Protocol 可以參考 RFC 768)。


為什麼說它們是"無連接"的呢?因為它(UDP)不像流式套接字那樣維護一個打開的連接,你只需要把數據打成一個包,把遠程的 IP 貼上去,然後把這個包發送出去。這個過程是不需要建立連接的。

UDP 的應用例子有:tftp, bootp 等。


那麼,數據包既然會丟失,怎樣能保證程序能夠正常工作呢?事實上,每個使用 UDP 的程序都要有自己的對數據進行確認的協議。比如, TFTP 協議定義了對於每一個發送出去的數據包,遠程在接受到之後都要回送一個數據包告訴本地程序: "我已經拿到了!" (一個 "ACK" 包)。如果數據包的發送者在 5 秒內沒有的得到回應,它就會重新發送這個數據包直到數據包接受者回送了 "ACK" 信號。這些知識對編寫一個使用 UDP 協議的程序員來說是非常必要的。


無連接服務器一般都是面向事務處理的,一個請求一個應答就完成了客戶程序與服務程序之間的相 互作用。若使用無連接的套接字編程,程序的流程可以用圖 2-3 表示。

嵌入式linux網絡編程之——5年程序員給你深度講解socket套接字

圖 8-3無連接的 socket 工作流程

3.原始套接字

原始套接字是一種對原始網絡報文進行處理的套接字,主要用於一些協議的開發,可以進行比較底 層的操作,如允許對底層協議如 IP 或 ICMP 直接訪問,主要用於新的網絡協議實現的測試等。它功能強大,但是沒有上面介紹的兩種套接字使用方便。

流式套接字(SOCK_STREAM)和數據報套接字(SOCK_DGRAM)涵蓋了一般應用層次的 TCP/IP 應用。應用層位於 TCP/UDP 層之上,所以這兩類套接字幾乎涵蓋了所有的應用層需求,也就是說幾乎所有的應用程序都可以使用這兩類來實現。

通常情況下程序設計人員接觸的網絡應用開發也涉及不到原始套接字。但是,如果深入地考慮一些 問題時,就會不知道該如何入手了,例如:

· 發送一個自定義的 IP 包。

· 發送一個 ICMP 數據報。

· 網卡的偵聽模式,監聽網絡上的數據包。

· 自定義協議的實現。

要解決這些問題,需要了解另一類套接字,這就是原始套接字。原始套接字主要應用在底層網絡編 程上,同時也是網絡黑客的必備手段。例如 sniffer、拒絕服務(Dos)、IP 地址欺騙等都需要在原始套接字的基礎上實現。

與原始套接字對應,之前的 TCP/UDP 的套接字稱為標準套接字。標準套接字與網絡協議棧的 TCP、

UDP 打交道,而原始套接字則與 IP 層級網絡協議棧核心打交道。原始套接字提供以下 3 種標準套接字不具備的功能:

· 使用原始套接字可以讀/寫 ICMP、IGMP 分組。例如 ping 程序,就使用了原始套接字發送 ICMP

回顯請求,並接受 ICMP 回顯應答。原始套接字允許使用 ICMP 或 IGMP 構造的應用程序完全作為用戶進程處理,而不必再增加過多的內核編碼。

· 使用原始套接字可以讀寫特殊的 IP 數據報,內核不處理這些數據報的協議字段。大多數內核只處理 1(ICMP)、2(IGMP)、3(TCP)和 17(UDP)的數據報。但協議字段還可能為其他值。例如,OSPF 路由協議就不適用 TCP 或者 UDP,而直接使用 IP,將 IP 數據報的協議字段設為 89。因此,由於這些數據報包含內核完全不知道的協議字段,實現 OSPF 協議的 gated 程序必須使用原始套接字來讀寫它們。

· 使用原始套接字,利用函數 setsockopt()設置套接字選項

,使用 IP_HDRINGCL 可以對 IP 頭部進行操作,因此可以修改 IP 數據和 IP 層之上的各層數據,構造自己的特定類型的 TCP 或者 UDP 的分組。


8.3 套接字地址

好了,關於 socket 的背景知識我們已經講得夠多了,下面,就讓我們正式開始揭開 socket 的神秘面紗吧!


8.3.1 什麼是 Socket?

大家經常談論 "Socket"(套接字),那麼一個套接字究竟是什麼呢?

一個套接字可以這樣來解釋:它是通過標準的 UNIX 文件描述符和其他的程序通訊的一個方法。


8.3.2 Socket 描述符

使用 UNIX 的黑客高手有這麼一句話:"恩,在 UNIX 系統中,任何東西都是一個文件。 "這句話描述了這樣一個事實:在 UNIX 系統中,任何對 I/O 的操作,都是通過讀或寫一個文件描述符來實現的。

一個文件描述符只是一個簡單的整形數值,代表一個被打開的文件(這裡的文件是廣義的文件,並不只代表不同的磁盤文件,它可以代表一個網絡上的連接,一個先進先出隊列,一個終端顯示屏幕,以及其他的一切)。在 UNIX 系統中任何東西都是一個文件!所以如果你想通過 Internet 和另外一個程序通訊的話,你將會是通過一個文件來描述符實現的。

好的,你已經相信 Socket 是一個文件描述符了,那麼我們應該怎樣才能得到這個代表網絡連接的文件描述符呢?你現在一定非常在意這個問題。是這樣的:你首先調用系統函數 socket(),它返回一個套接字(Socket)描述符,然後你就可以通過對這個套接字描述符來進行一些操作:系統函數 send() 和 recv()(你可以使用 "man"命令來查找系統幫助:man send, man recv)。

你會想:"套接字描述符是一個文件描述符,為什麼不能用對文件操作的 write() 和 read() 來進行套接字通訊呢?"事實上,write() 和 read() 是可以對套接字描述符進行操作的,但是,通過使用 send() 和 recv() 函數,你可以對網絡數據的傳輸進行更好的控制!


8.3.3 一個套接字是怎樣在網絡上傳輸數據的?

我們已經談過了網絡協議層,那麼我們還應該繼續多瞭解一些東西:物理網絡上的數據是怎樣傳送

的。


我們可以認為是這樣的:

數據被分成一個一個的包(Packet),包的數據頭(或數據尾)被第一層協議 (比如 TFTP 協議)加上第一層協議數據;然後整個包(包括內部加入的 TFTP 信息頭)被下層協議再次包裝(比如 UDP),在這之後數據包會再次被下層協議包裝(比如 IP 協議),最後是被最底層的硬件層(物理層)包裝上最後一層信息(Ethernet 信息頭)。

當接收端的計算機接收到這個包後,硬件首先剝去數據包中的 Ethernet 信息頭,然後內核在剝去

IP 和 UDP 信息頭,最後把數據包提交給 TFTP 應用程序,由 TFTP 剝去 TFTP 信息頭,最後得到了原始數據。

對流式套接字你所需要做的只是調用 send() 函數來發送數據。而對於數據報套接字,你需要自己加個信息頭,然後調用 sendto() 函數把數據發送出去。Linux 系統內核中已經建立了 Transport

Layer 和 Internet Layer。硬件負責 NetworkAccess Layer。簡單而有效,不是嗎?


8.4 套接字的一些基本知識

好的,從現在開始,我們應該談些和程序有關的事情了。


8.4.1 基本結構

首先,介紹一些使用套接字編程中常見的網絡數據結構,這些數據結構對大家會很有幫助。

1.struct sockaddr

這個結構用來存儲套接字地址。數據定義:

struct sockaddr

{

unsigned short sa_family; /* address 族, AF_xxx */ char sa_data[14]; /* 14 bytes 的協議地址 */

};

sa_family 一般來說,都是 "AFINET"。


sa_data 包含了一些遠程電腦的地址、端口和套接字的數目,它裡面的數據是雜溶在一切的。


為了處理 struct sockaddr,程序員建立了另外一個相似的結構 struct sockaddr_in:struct sockaddr_in ("in" 代表 "Internet")

struct sockaddr_in

{

short int sin_family; /* Internet 地址族 */ unsigned short int sin_port; /* 端口號 */ struct in_addr sin_addr; /* Internet 地址 */

unsigned char sin_zero[8]; /*添 0(和 struct sockaddr 一樣大小)*/

};


這個結構提供了方便的手段來訪問 socket address(struct sockaddr)結構中的每一個元素。注


意 sin_zero[8] 是為了是兩個結構在內存中具有相同的尺寸, 使用 sockaddr_in 的時候要把

sin_zero 全部設成零值(使用 bzero()或 memset()函數)。而且,有一點很重要,就是一個指向 struct

sockaddr_in 的指針可以聲明指向一個 sturct sockaddr 的結構。所以雖然 socket() 函數需要一個structaddr * ,你也可以給他一個 sockaddr_in * 。注意在 struct sockaddr_in 中,sin_family 相當於在 struct sockaddr 中的 sa_family, 需要設成"AF_INET"。

最後一定要保證 sin_port 和 sin_addr 必須是網絡字節順序(見下節)!


2.struct in_addr

其定義如下:

/* 因特網地址 (a structure for historical reasons) */ struct in_addr

{

unsigned long s_addr;

};

如 果 你 聲 明 了 一 個 " ina " 作 為 一 個 struct sockaddr_in 的 結 構 , 那 麼

"ina.sin_addr.s_addr"就是 4 個字節的 IP 地址(按網絡字節順序排放)。需要注意的是,即使你的系統仍然使用聯合而不是結構來表示 struct in_addr,你仍然可以用上面的方法得到 4 個字節的

IP 地址(一些 #define 幫了你的忙)。


8.4.2 基本轉換函數

在前面提到了網絡字節順序。那麼什麼是網絡字節順序,它有什麼特殊性,又如何將我們通常使用 的數據轉換成這種格式呢?

1.網絡字節順序

因為每一個機器內部對變量的字節存儲順序不同(有的系統是高位在前,底位在後,而有的系統是底位在前,高位在後),而網絡傳輸的數據大家是一定要統一順序的。所以對與內部字節表示順序和網絡字節順序不同的機器,就一定要對數據進行轉換(比如 IP 地址的表示,端口號的表示)。但是內部字節順序和網絡字節順序相同的機器該怎麼辦呢?

是這樣的:它們也要調用轉換函數,但是真正轉換還是不轉換是由系統函數自己來決定的。


2.有關的轉化函數

我們通常使用的有兩種數據類型:短型(兩個字節)和長型(四個字節)。下面介紹的這些轉換函數對於這兩類的無符號整型變量都可以進行正確的轉換。

如果你想將一個短型數據從主機字節順序轉換到網絡字節順序的話,有這樣一個函數:它是以"h" 開頭的(代表"主機") ;緊跟著它的是"to" ,代表"轉換到" ;然後是"n"代表"網絡" ; 最後是"s" ,代表"短型數據"。H-to-n-s,就是 htons() 函數(可以使用 Host to Network Short 來助記)

很簡單吧……我沒有理解的時候覺得這個函數不好記呢……

你可以使用 "n", "h", "to", "s", "l"的任意組合……當然,你要在可能的情況下進行組合。比如,系統是沒有 stolh() 函數的(Short to Long Host?)。

下面給出套接字字節轉換程序的列表:

· htons()——"Host to Network Short"

注意:現在你可能認為自己已經精通於這幾個函數的用處了……你可能會想: "恩……在我的

68000 機器內部,字節的表示順序已經是網絡字節順序了,那麼我的程序裡就不必調用 htonl() 來轉換我的 IP 地址了" 。是的,你可能是對的。但是假如你把你的程序移植到一個內部字節順序和網絡字節順序相反的機器上,你的程序就會運行不正常!所以,一定要記住:在你把數據發送到 Internet 之前,一定要把它的字節順序從主機字節順序轉換到網絡字節順序!

在 struct sockaddr_in 中的 sin_addr 和 sin_port 他們的字節順序都是網絡字節順序,而

sin_family 卻不是網絡字節順序的。為什麼呢?


這個是因為 sin_addr 和 sin_port 是從 IP 和 UDP 協議層取出來的數據,而在 IP 和 UDP 協議層, 是直接和網絡相關的,所以,它們必須使用網絡字節順序。然而,sin_family 域只是內核用來判斷struct sockaddr_in 是存儲的什麼類型的數據,並且,sin_family 永遠也不會被髮送到網絡上,所以可以使用主機字節順序來存儲。


3.IP 地址轉換

很幸運, Linux 系統提供和很多用於轉換 IP 地址的函數,使你不必自己再寫出一段費力不討好的子程序來吃力的變換 IP。

首先,讓我假設你有一個 struct sockaddr_in ina,並且你的 IP 是 166.111.69.52 ,你想把你的 IP 存儲到 ina 中。你可以使用的函數:inet_addr() ,它能夠把一個用數字和點表示 IP 地址的字符串轉換成一個無符號長整型。你可以像下面這樣使用它:

ina.sin_addr.s_addr = inet_addr(" 166.111.69.52" ); 注意:

· inet_addr()返回的地址已經是網絡字節順序了,你沒有必要再去調用 htonl() 函數,是不是很方便呢?

· 上面的用法並不是一個很好的習慣,因為上面的代碼沒有進行錯誤檢查。如果 inet_addr() 函數執行錯誤, 它將會返回 – 1…… 等等! 二進制的無符號整數值– 1 相當於什麼? 相當於255.255.255.255 !! 一個廣播用的 IP 地址!沒有辦法,你只能在你自己的程序裡進行對症下藥的錯誤檢查了。

好,現在我們已經可以把字符串的 IP 地址轉換成長整型了。那麼還有沒有其他的方法呢?如果你有一個 struct in_addr 並且你想把它代表的 IP 地址打印出來(按照 數字.數字.數字.數字的格式)……

這裡,你可以使用函數 inet_ntoa()( "ntoa"代表"Network to ASCII"): printf(" %s" , inet_ntoa(ina.sin_addr));


這段代碼將會把 struct in_addr 裡面存儲的網絡地址以 數字.數字.數字.數字 的格式顯示出來。


注意:

· inet_ntoa() 使用 struct in_addr 作為一個參數,不是一個長整型值。

· inet_ntoa() 返回一個字符指針,它指向一個定義在函數 inet_ntoa() 中的 static 類型字符串。所以每次你調用 inet_ntoa(),都會改變最後一次調用 inet_ntoa() 函數時得到的結果。

比如:

char *a1, a2;

a1 = inet_ntoa(ina1.sin_addr); /* this is 166.111.69.52 */ a2 = inet_ntoa(ina2.sin_addr); /* this is 166.111.69.53 */ printf("address 1: %s\\n" ,a1);

printf("address 2: %s\\n" ,a2); 將會顯示出:

address 1: 166.111.69.53

address 2: 166.111.69.53

如果你想把結果保存下來,那麼你可以在每次調用 inet_ntoa() 後調用 strcpy() 將結果存到另 外一個你自己的字符串中。

在後面,將會介紹怎樣把域名轉換為 IP。


8.5 基本套接字調用

Linux 支持伯克利(BSD)風格的套接字編程.它同時支持面向連接和不連接類型的套接字。

在面向連接的通訊中服務器和客戶機在交換數據之前先要建立一個連接.再不連接通訊中數據被作 為信息的一部分被交換.無論那一種方式,服務器總是最先啟動,把自己綁定(Banding)在一個套接字上,然後偵聽信息.服務器究竟怎樣試圖去偵聽就得依靠你編程所設定的連接的類型了。

你需要了解的一些系統調用:

· socket()

· bind()

· connect()

· listen()

· accept()

· send()

· recv()

· sendto()

· recvfrom()

· close()

· shutdown()

· setsockopt()

· getsockopt()

· getpeername()

· getsockname()

· gethostbyname()

· gethostbyaddr()

· getprotobyname()

· fcntl()

我們將在以下詳細介紹這些系統調用。


8.5.1 socket()函數

取得套接字描述符!(記得我們以前說過的嗎?它其實就是一個文件描述符)

socket 函數的定義是下面這樣子的:

#include

#include

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


你是否對 int domain 和 int type、int protocol 有些疑惑呢?調用 socket()的參數是什麼呢? 首先,domain 需要被設置為 "AF_INET" ,就像上面的 struct sockaddr_in。然後,type 參數告訴內核這個 socket 是什麼類型,"SOCK_STREAM"或是"SOCK_DGRAM" 。最後,只需要把 protocol

設置為 0。


注意: 事實上, domain 參數可以取除了"AF_INET" 外的很多值,types 參數也可以取除了

"SOCK_STREAM"或"SOCK_DGRAM"的另外類型。具體可以參考 socket 的 man pages(幫助頁)。


套接字創建時沒有指定名字.客戶機用套接字的名字讀寫它。這就是下面的綁定函數所要做之事。


socket() 函數只是簡單的返回一個你以後可以使用的套接字描述符。如果發生錯誤,socket() 函數返回 – 1。全局變量 errno 將被設置為錯誤代碼。(可以參考 perror() 的 man pages)


8.5.2 bind()函數

bind() 函數可以幫助你指定一個套接字使用的端口。

當你使用 socket() 函數得到一個套接字描述符,你也許需要將 socket 綁定上一個你的機器上的端口。

· 當你需要進行端口監聽 listen()操作,等待接受一個連入請求的時候,一般都需要經過這一步。比如網絡泥巴(MUD),Telnet a.b.c.d 4000。

· 如果你只是想進行連接一臺服務器,也就是進行 connect() 操作的時候,這一步並不是必須的。


#include

#include

int bind (int sockfd , struct sockaddr *my_addr , int addrlen) ; 參數說明:

· sockfd 是由 socket()函數返回的套接字描述符。

· my_addr 是一個指向 struct sockaddr 的指針, 包含有關你的地址的信息:名稱、端口和 IP 地址。

· addrlen 可以設置為 sizeof(struct sockaddr)。


好,下面我們看一段程序:


#include <string.h>

#include

#include

#define MYPORT 4000 int main()

{

int sockfd ;

struct sockaddr_in my_addr ;

sockfd = socket(AF_INET, SOCK_STREAM, 0); /* 在你自己的程序中 */

/* 要進行錯誤檢查!! */

my_addr.sin_family = AF_INET ; /* 主機字節順序 */ my_addr.sin_port = htons(MYPORT); /* 網絡字節順序,短整型 */ my_addr.sin_addr.s_addr = inet_addr(" 166.111.69.52") ; bzero(&(my_addr.sin_zero), 8); /* 將整個結構剩餘*/

/* 部分數據設為 0 */

/* 不要忘記在你自己的程序中加入判斷 bind 錯誤的代碼*/ bind(sockfd,(struct sockaddr *)&my_addr,

sizeof(struct sockaddr));

……

……

}

這裡有一些值得注意的代碼段:


· my_addr.sin_port 是網絡字節順序。

· my_addr.sin_addr.s_addr 也是網絡字節順序。


· 代碼段包含的頭文件,在不同的系統中可能有一點小小的區別。(不過在 Linux 中是如此)如果並非如此,你可以查一查 man pages 來獲取幫助。

最後,bind() 可以在程序中自動獲取你自己的 IP 地址和端口。代碼如下:

my_addr.sin_port = 0 ; /* 隨機選擇一個端口 */

my_addr.sin_addr.s_addr = INADDR_ANY ; /* 使用自己的地址 */


如上,通過設置 my_addr.sin_port 為 0,bind()可以知道你要它幫你選擇合適的端口;通過設置

my_addr.sin_addr.s_addr 為 INADDR_ANY,bind() 知道你要它將 s_addr 填充為運行這個進程的機器的 IP。這一切都可以要求 bind() 來自動的幫助你完成。

如果你注意到了一些細節的話,你可能會發現我並沒有將 INADDR_ANY 轉換為網絡字節順序!是這樣的,INADDR_ANY 的值為 0,0 就是 0,無論用什麼順序排列位的順序,它都是不變的。

有讀者會想了,因為我用的 INADDR_ANY 是一個#define,那麼如果將我的程序移植到另外一個系統,假如那裡的 INADDR_ANY 是這樣定義的:#define INADDR_ANY 100,那麼我的程序不是就會不運行了嗎?那麼下面這段代碼就 OK 了。

my_addr.sin_port = htons(0); /* 隨機選擇一個未用的端口 */ my_addr.sin_addr.s_addr = htonl(INADDR_ANY); /*使用自己的 IP 地址 */

現在我們已經是這麼的嚴謹,對於任何數值的 INADDR_ANY 調用 bind 的時候就都不會有麻煩了。當 bind() 函數調用錯誤的時候,它也是返回– 1 作為錯誤發生的標誌。errn 的值為錯誤代碼。另外一件必須指出的事情是:當你調用 bind() 的時候,不要把端口數設置的過小!小於 1024 的

所有端口都是保留下來作為系統使用端口的,沒有 root 權利無法使用。你可以使用 1024 以上的任何

端口,一直到 65535 :你所可能使用的最大的端口號(當然,你還要保證你所希望使用的端口沒有被其他程序所使用)。

最後注意有關 bind() 的是:有時候你並不一定要調用 bind() 來建立網絡連接。比如你只是想連接到一個遠程主機上面進行通訊,你並不在乎你究竟是用的自己機器上的哪個端口進行通訊(比如

Telnet),那麼你可以簡單的直接調用 connect() 函數,connect() 將自動尋找出本地機器上的一個未使用的端口,然後調用 bind() 來將其 socket 綁定到那個端口上。


8.5.3 connect()函數


讓我們花一點時間來假設你是一個 Telnet 應用程序。你的使用者命令你建立一個套接字描述符。你遵從命令,調用了 socket()。然後,使用者告訴你連接到 "166.111.69.52"的 23 端口(標準的Telnet 端口)……你應該怎麼做呢?


你很幸運:Telnet 應用程序,你現在正在閱讀的就是套接字的進行網絡連接部分:connect()。


connect() 函數的定義是這樣的:

#include

#include

int connect (int sockfd, struct sockaddr *serv_addr, int addrlen);


connect()的三個參數意義如下:

· sockfd :套接字文件描述符,由 socket()函數返回的。

· serv_addr 是一個存儲遠程計算機的 IP 地址和端口信息的結構。

· addrlen 應該是 sizeof(struct sockaddr)。下面讓我們來看看下面的程序片段:

#include <string.h>

#include

#include

#define DEST_IP " 166.111.69.52"

#define DEST_PORT 23 int main()

{

int sockfd ;

/* 將用來存儲遠程信息 */ struct sockaddr_in dest_addr ;

/* 注意在你自己的程序中進行錯誤檢查*/ sockfd = socket(AF_INET, SOCK_STREAM, 0);

/* 主機字節順序 */ dest_addr.sin_family = AF_INET ;

/* 網絡字節順序,短整型 */

dest_addr.sin_port = htons(DEST_PORT); dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);

/* 將剩下的結構中的空間置 0 */ bzero(&(dest_addr.sin_zero), 8);

/* 不要忘記在你的代碼中對 connect()進行錯誤檢查*/ connect(sockfd, (struct sockaddr *)&dest_addr,

sizeof(struct sockaddr));

……

……

}

再次強調,一定要檢測 connect() 的返回值:如果發生了錯誤(比如無法連接到遠程主機,或是

遠程主機的指定端口無法進行連接等)它將會返回錯誤值 – 1。全局變量 errno 將會存儲錯誤代碼。另外,注意我們沒有調用 bind() 函數。基本上,我們並不在乎我們本地用什麼端口來通訊,是不

是?我們在乎的是我們連到哪臺主機上的哪個端口上。Linux 內核自動為我們選擇了一個沒有被使用的

本地端口。


在面向連接的協議的程序中,服務器執行以下函數:

· 調用 socket() 函數創建一個套接字。

· 調用 bind() 函數把自己綁定在一個地址上。

· 調用 listen() 函數偵聽連接。

· 調用 accept() 函數接受所有引入的請求。

· 調用 recv() 函數獲取引入的信息然後調用 send() 回答。


8.5.4 listen()函數


listen() 函數是等待別人連接,進行系統偵聽請求的函數。當有人連接你的時候,你有兩步需要做:通過 listen() 函數等待連接請求,然後使用 accept() 函數來處理。(accept() 函數在下面介紹)。

listen() 函數調用是非常簡單的。函數聲明如下:


#include

int listen(int sockfd, int backlog);


listen() 函數的參數意義如下:

· sockfd 是一個套接字描述符,由 socket()系統調用獲得。

· backlog 是未經過處理的連接請求隊列可以容納的最大數目。

backlog 具體一些是什麼意思呢?每一個連入請求都要進入一個連入請求隊列,等待 listen 的程序調用 accept()(accept() 函數下面有介紹)函數來接受這個連接。當系統還沒有調用 accept()函數的時候,如果有很多連接,那麼本地能夠等待的最大數目就是 backlog 的數值。你可以將其設成 5 到

10 之間的數值(推薦)。


像上面的所有函數一樣, listen() 如果返回 – 1 ,那麼說明在 listen() 的執行過程中發生了錯誤。全局變量 errno 中存儲了錯誤代碼。

那麼我們需要指定本地端口了,因為我們是等待別人的連接。所以,在 listen() 函數調用之前, 我們需要使用 bind() 函數來指定使用本地的哪一個端口數值。

如果你想在一個端口上接受外來的連接請求的話,那麼函數的調用順序為:

socket() ;

bind() ;

listen() ;

/* 在這裡調用 accept()函數 */


……


下面將不給出例程,因為 listen() 是非常容易理解的。下面的 accept() 函數說明中的例程中, 有 listen() 的使用。


8.5.5 accept()函數


函數 accept()有一些難懂。當調用它的時候,大致過程是下面這樣的:


· 有人從很遠很遠的地方嘗試調用 connect() 來連接你的機器上的某個端口

(當然是你已經在 listen() 的)。


· 他的連接將被 listen 加入等待隊列等待 accept() 函數的調用(加入等待隊列的最多數目由調用 listen() 函數的第二個參數 backlog 來決定)。

· 你調用 accept() 函數,告訴他你準備連接。

· accept() 函數將回返回一個新的套接字描述符,這個描述符就代表了這個連接!

好,這時候你有了兩個套接字描述符,返回給你的那個就是和遠程計算機的連接,而第一個套接字 描述符仍然在你的機器上原來的那個端口上 listen()。

這時候你所得到的那個新的套接字描述符就可以進行 send() 操作和 recv()操作了。下面是 accept() 函數的聲明:

#include

int accept(int sockfd, void *addr, int *addrlen);

accept() 函數的參數意義如下:


· sockfd 是正在 listen() 的一個套接字描述符。

· addr 一般是一個指向 struct sockaddr_in 結構的指針;裡面存儲著遠程連接過來的計算機的信息(比如遠程計算機的 IP 地址和端口)。

· addrlen 是一個本地的整型數值,在它的地址傳給 accept() 前它的值應該是 sizeof(struct

sockaddr_in);accept() 不會在 addr 中存儲多餘 addrlen bytes 大小的數據。如果 accept() 函數在 addr 中存儲的數據量不足 addrlen,則 accept() 函數會改變 addrlen 的值來反應這個情況。

讀者現在應該想到:如果調用 accept() 失敗的話,accept() 函數會返回 – 1 來表明調用失敗, 同時全局變量 errno 將會存儲錯誤代碼。

下面我們來看一段程序片段:


#include <string.h>

#include

#include

/* 用戶連接的端口號 */

#define MYPORT 4000

/* 等待隊列中可以存儲多少個未經過 accept()處理的連接 */

#define BACKLOG 10 int main()

{

/* 用來監聽網絡連接的套接字 sock_fd,用戶連入的套接字使用 new_fd */ int sockfd, new_fd ;

/* 本地的地址信息 */ struct sockaddr_in my_addr ;

/* 連接者的地址信息 */

struct sockaddr_in their_addr ; int sin_size;

/* 記得在自己的程序中這部分要進行錯誤檢查! */ sockfd = socket(AF_INET, SOCK_STREAM, 0) ;

/* 主機字節順序 */ my_addr.sin_family = AF_INET ;

/* 網絡字節順序,短整型 */ my_addr.sin_port = htons(MYPORT) ;

/* 自動賦值為自己的 IP */ my_addr.sin_addr.s_addr = INADDR_ANY ;

/* 將結構中未使用部分全部清零 */ bzero(&(my_addr.sin_zero), 8) ;

/* 不要忘記在你自己的程序中下面的程序調用需要進行錯誤檢測*/ bind(sockfd, (struct sockaddr *)&my_addr,

sizeof(struct sockaddr)); listen(sockfd, BACKLOG);

sin_size = sizeof(struct sockaddr_in);

new_fd = accept(sockfd, &their_addr, &sin_size);

……

……

}

注意:我們使用了套接字描述符 new_fd 用來進行所有的 send() 和 recv() 調用。如果你只想獲

得一個單獨的連接,那麼你可以將原來的 sock_fd 關掉(調用 close()),這樣的話就可以阻止以後的連接了。

在面向連接的通信中客戶機要做如下一些事:

· 調用 socket() 函數創建一個套接字。

· 調用 connect() 函數試圖連接服務。

· 如果連接成功調用 write() 函數請求數據,調用 read() 函數接收引入的應答。


8.5.6 send()和 recv()函數


這兩個函數是最基本的,通過連接的套接字流進行通訊的函數。


如果你想使用無連接的用戶數據報的話,請參考下面的 sendto() 和 recvfrom() 函數。


#include

#include

int send(int sockfd, const void *msg, int len, int flags);


send 的參數含義如下:

· sockfd 是代表你與遠程程序連接的套接字描述符。

· msg 是一個指針,指向你想發送的信息的地址。

· len 是你想發送信息的長度。

· flags 發送標記。一般都設為 0(你可以查看 send 的 man pages 來獲得其他的參數值並且明白各個參數所代表的含義)。

下面看看有關 send() 函數的代碼片段: char *msg = " Hello! World!";

int len, bytes_sent;

……

……

len = strlen(msg);

bytes_sent = send(sockfd, msg, len, 0);

……

……

……

send()函數在調用後會返回它真正發送數據的長度。


注意:send() 所發送的數據可能少於你給它的參數所指定的長度!


因為如果你給 send() 的參數中包含的數據的長度遠遠大於 send() 所能一次發送的數據,則

send() 函數只發送它所能發送的最大數據長度,然後它相信你會把剩下的數據再次調用它來進行第二次發送。

所以,記住如果 send() 函數的返回值小於 len 的話,則你需要再次發送剩下的數據。幸運的是, 如果包足夠小(小於 1K),那麼 send() 一般都會一次發送光的。


像上面的函數一樣,send() 函數如果發生錯誤,則返回 – 1,錯誤代碼存儲在全局變量 errno中。


下面我們來看看 recv() 函數。


函數 recv() 調用在許多方面都和 send() 很相似,下面是 recv() 函數的聲明:

#include

#include

int recv(int sockfd, void *buf, int len, unsigned int flags);


recv()的參數含義如下:

· sockfd 是你要讀取數據的套接字描述符。

· buf 是一個指針,指向你能存儲數據的內存緩存區域。

· len 是緩存區的最大尺寸。

· flags 是 recv() 函數的一個標誌, 一般都為 0(具體的其他數值和含義請參考 recv() 的man pages)。

recv() 返回它所真正收到的數據的長度。(也就是存到 buf 中數據的長度)。如果返回 –1 則代表發生了錯誤(比如網絡以外中斷、對方關閉了套接字連接等),全局變量 errno 裡面存儲了錯誤代碼。

很簡單,不是嗎?現在你已經可以使用套接字連接進行網絡發送數據和接受數據了!


Ya! 你現在已經成為了一個 Linux 下的網絡程序員了!


8.5.7 sendto()和 recvfrom()函數

這兩個函數是進行無連接的 UDP 通訊時使用的。使用這兩個函數,則數據會在沒有建立過任何連接的網絡上傳輸。因為數據報套接字無法對遠程主機進行連接,想想我們在發送數據前需要知道些什麼 呢?

對了!是遠程主機的 IP 地址和端口!


下面是 sendto()函數和 recvfrom()函數的聲明:

#include

#include

int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);

和你所看到的一樣,這個函數和 send()函數基本一致。


· sockfd 是代表你與遠程程序連接的套接字描述符。

· msg 是一個指針,指向你想發送的信息的地址。

· len 是你想發送信息的長度。

· flags 發送標記。一般都設為 0。 (你可以查看 send 的 man pages 來獲得其他的參數值並且明白各個參數所代表的含義)

· to 是一個指向 struct sockaddr 結構的指針,裡面包含了遠程主機的

IP 地址和端口數據。


· tolen 只是指出了 struct sockaddr 在內存中的大小sizeof(struct sockaddr)。

和 send() 一樣,sendto() 返回它所真正發送的字節數(當然也和 send() 一樣,它所真正發送的字節數可能小於你所給它的數據的字節數)。 當它發生錯誤的時候,也是返回– 1,同時全局變量

errno 存儲了錯誤代碼。


同樣的,recvfrom() 函數和 recv() 函數也基本一致。


#include

#include

int recvfrom(int sockfd, void *buf, int len, unsigned int flags struct sockaddr *from, int *fromlen);

其參數含義如下:


· sockfd 是你要讀取數據的套接字描述符。

· buf 是一個指針,指向你能存儲數據的內存緩存區域。

· len 是緩存區的最大尺寸。

· flags 是 recvfrom() 函數的一個標誌, 一般都為 0 (具體的其他數值和含義請參考 recvfrom() 的 man pages)。


· from 是一個本地指針,指向一個 struct sockaddr 的結構(裡面存有源 IP 地址和端口數)。


· fromlen 是一個指向一個 int 型數據的指針,它的大小應該是sizeof(struct sockaddr).當函數返回的時候,

formlen 指向的數據是 form 指向的 struct sockaddr 的實際大小。


recvfrom() 返回它接收到的字節數,如果發生了錯誤,它就返回 – 1,全局變量 errno 存儲了錯誤代碼。

如果一個信息大得緩衝區都放不下,那麼附加信息將被砍掉。該調用可以立即返回,也可以永久的 等待。這取決於你把 flags 設置成什麼類型。你甚至可以設置超時(timeout)值。

在說明書(man pages)中可以找到 recvfrom 的更多信息。


注意:如果你使用 cnnect() 連接到了一個數據報套接字的服務器程序上,那麼你就可以使用

send() 和 recv() 函數來傳輸你的數據。不要以為你在使用一個流式的套接字,你所使用的仍然是一個用戶數據報的套接字,只不過套接字界面在 send() 和 recv()的時候自動幫助你加上了目標地址, 目標端口的信息。


8.5.8 close()和 shutdown()函數

程序進行網絡傳輸完畢後,你需要關閉這個套接字描述符所表示的連接。實現這個非常簡單,只需 要使用標準的關閉文件的函數:close()。

使 用 方 法 : close(sockfd);

執行 close()之後,套接字將不會在允許進行讀操作和寫操作。任何有關對套接字描述符進行讀和寫的操作都會接收到一個錯誤。

如果你想對網絡套接字的關閉進行進一步的操作的話,你可以使用函數 shutdown()。它允許你進行單向的關閉操作,或是全部禁止掉。

#include

int shutdown(int sockfd, int how); 它的參數含義如下:

· sockfd 是一個你所想關閉的套接字描述符。

· how 可以取下面的值。0 表示不允許以後數據的接收操;1 表示不允許以後數據的發送操作;

2 表示和 close() 一樣,不允許以後的任何操作(包括接收,發送數據)。


shutdown() 如果執行成功將返回 0,如果在調用過程中發生了錯誤,它將返回–1,全局變量 errno中存儲了錯誤代碼。

如果你在一個未連接的數據報套接字上使用 shutdown() 函數(還記得可以對數據報套接字 UDP 進行 connect() 操作嗎?),它將什麼也不做。


8.5.9 setsockopt()和 getsockopt()函數

Linux 所提供的 socket 庫含有一個錯誤(bug)。此錯誤表現為你不能為一個套接字重新啟用同一個端口號,即使在你正常關閉該套接字以後。例如,比方說,你編寫一個服務器在一個套接字上等待的 程序。服務器打開套接字並在其上偵聽是沒有問題的。無論如何,總有一些原因(不管是正常還是非正 常的結束程序)使你的程序需要重新啟動。然而重啟動後你就不能把它綁定在原來那個端口上了。從

bind() 系統調用返回的錯誤代碼總是報告說你試圖連接的端口已經被別的進程所綁定。


問題就是 Linux 內核在一個綁定套接字的進程結束後從不把端口標記為未用。在大多數

Linux/UNIX 系統中,端口可以被一個進程重複使用,甚至可以被其它進程使用。


在 Linux 中繞開這個問題的辦法是,當套接字已經打開但尚未有連接的時候用 setsockopt() 系統調用在其上設定選項(options)。而 getsockopt() 可以從給定的套接字取得選項。

這裡是這些調用的語法:


#include

#include

int getsockopt(int sockfd, int level, int name,

char *value, int *optlen); int setsockopt(int sockfd, int level, int name,

char *value, int *optlen); 下面是兩個調用的參數說明:

· sockfd 必須是一個已打開的套接字。

· level 是函數所使用的協議標準(protocol level) (TCP/IP 協議使用

PPROTO_TCP,套接字標準的選項實用 SOL_SOCKET)。


· name 選項在套接字說明書中(man page)有詳細說明。

· value 指向為 getsockopt()函數所獲取的值,setsockopt()函數所設置的值的地址。

· optlen 指針指向一個整數,該整數包含參數以字節計算的長度。

現在我們再回到 Linux 的錯誤上來.當你打開一個套接字時必須同時用下面的代碼段來調用

setsockopt() 函數:


/* 設定參數數值 */


opt = 1; len = sizeof(opt);


/* 設置套接字屬性 */ setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,&len);

setsockopt()函數還有很多其他用法,請參考幫助頁(man pages)。


8.5.10 getpeername() 函數


這個函數可以取得一個已經連接上的套接字的遠程信息(比如 IP 地址和端口),告訴你在遠程和你連接的到底是誰。

#include


int getpeername(int sockfd, struct sockaddr *addr, int *addrlen); 下面是參數說明:

· sockfd 是你想取得遠程信息的那個套接字描述符。

· addr 是一個指向 struct sockaddr (或是 struct sockaddr_in)的指針。

· addrlen 是一個指向 int 的指針,應該賦於 sizeof(struct sockaddr)的大小。


如果在函數執行過程中出現了錯誤,函數將返回 –1,並且錯誤代碼儲存在全局變量 errno 中。當你擁有了遠程連接用戶的 IP 地址,你就可以使用 inet_ntoa() 或 gethostbyaddr() 來輸出

信息或是做進一步的處理。


8.5.11 gethostname()函數


gethostname() 函數可以取得本地主機的信息.它比 getpeername() 要容易使用一些。


它返回正在執行它的計算機的名字。返回的這個名字可以被 gethostbyname() 函數使用,由此可以得到本地主機的 IP 地址。

#include <unistd.h>

int gethostname(char *hostname, size_t size); 參數說明如下:

· hostname 是一個指向字符數組的指針,當函數返回的時候,它裡面的數據就是本地的主機的名字。

· size 是 hostname 指向的數組的長度。

函數如果成功執行,它返回 0,如果出現錯誤,則返回–1,全局變量 errno 中存儲著錯誤代碼。


分享到:


相關文章: