TCP/IP 協定與 Internet 網路:第八章 TCP Socket 程式介面  上一頁           下一頁

 

翻轉工作室:粘添壽

 

8-5 Socket 庫存函數

        Socket 提供一系列的程式庫讓使用者來編寫網路應用程式,本節將依照 RedHat Linux(BSD 標準)來介紹『Socket 庫存函數』(Socket Library)與相關程式的呼叫方式,至於其它作業系統也幾乎都是一樣。

8-5-1 Socket 庫存函數彙集

        我們可以透過 Socket 庫存函數來處理兩個傳輸提供者之間的交談,它所提供的功能呼叫彙集如表 8-1。

表 8-1 Socket 庫存函數

開啟 Socket(Create a Socket)

socket()

開啟一個 Socket,並回應一個檔案描述子

socketpair()

開啟兩個 Socket 且連結它們,並回應兩個檔案描述子

定名 Socket(Names a Socket)

bind()

將 Socket 定名,表示連結到傳輸提供者之位址上。

連結 Socket(Connects to a Socket)

listen()

將 Socket 設定進入『聆聽』狀態。

accept()

Socket 等待遠端連接訊號到達,並接受連線。

傳輸資料(Transfer Data)

send()/write()

透過連線傳送訊息。

sendto()

傳送分連接訊息(Datagram),必須描述對方 Socket 位址。

sendmsg()

傳送交談式訊息,必須描述對方 Socket 位址。

recv()/read()

透過連線接收訊息。

recvfrom()

接收非連接訊息(Datagram),包含傳送端 Socket 位址。

recvmsg()

接收交談式訊息,包含傳送端 Socket 位址。

停止 Socket(Shuts Down a Socket)

shutdown()

停止 Socket 工作,並禁止所有 Socket 之連線及傳輸動作。

Close()

同上

管理 Socket 程式集

getsockname()

取得本身 Socket 的名稱。

getpeername()

取得對方 Socket 的名稱。

setsockopt()

設定 Socket 某些參數。

getsockopt()

取得 Socket 參數值。

以下針對較常使用的庫存函數加以介紹。

8-5-2 開啟 Socket

        socket() 功能呼叫是用來產生一個通訊端點,也就是向系統註冊,通知系統要建立一個通訊端點,其程式格式如下:

#include <sys/types.h>

#include <sys/socket.h>

 

int domain;

int type;

int protocol;

int fd;

 

fd = socket(domain, type, protocol);

socket() 如同一般檔案系統的 open() 系統呼叫一樣。當 socket() 執行正常時,會回應一個整數(fd),一般稱之為『檔案描述子』 (File Description, fd),表示該 Socket 的識別值。如果檔案描述子大於 0 則表示開啟正常,並針對該檔案描述子作處理動作(connect()、read() 等)。在一個應用程式裡,可經過多個 socket() 呼叫,來產生多個通訊端點,並建立多個通訊通道,這些通訊端點就以不同的檔案描述值來區分。

當呼叫 socket() 系統程式時,必須給予相對應之參數,說明如下:

(A) domain 參數

參數 domain 或稱為 family,是用來選擇使用哪一種通訊協定的家族系列,domain 可選擇下列之一:

● AF_UNIX:Unix Internet Protocol。此通訊家族並不是真正的網路通訊協定,而是用來作 Unix 作業系統中,各程序(Process)之間的通訊使用。一般使用在回授(Loopback)傳輸提供者,而其應用在主機內程序之間的通訊。

● AF_INET:Internet Protocol。此為 TCP/IP 的 Internet 通訊協定,傳輸提供者可能是 TCP 或 UDP,也是本章討論的重點。

● AF_NS:Xerox NS Protocol。此為 Xerox 公司發展的通訊協定。

● AF_IMPLINK:Interface Message Protocol。此為一種智慧型的分封交換節點協定,這些節點都是使用點對點的連接方式,一般使用在租用電話線路來作資料傳輸使用。(不在本章討論範圍)

其中『AF_』代表 Address Family,有些系統使用『PF_』(Protocol Family),兩者是相通的。domain 參數也如同 sockadd_in 資料結構中的 sin_family 參數一樣。

(B)  type 參數

參數 type 是設定該 Socket 的類型,可選擇下列類型之一:

● SOCK_STREAM:Stream Socket。傳輸提供者提供一個虛擬電路服務(TCP)。

● SOCK_DGRAM:Datagram Socket。提供電報傳輸服務(UDP)。

● SOCK_RAW:Raw Socket。通訊協定型態在傳輸層之下,譬如,在 AF_INET(傳輸層為 TCP 或 UDP)模式,SOCK_RAW 的通訊協定可以是 IP(Internet Protocol)或 ICMP(Internet Control Message Protocol)。

● SOCK_SEQPACKET:Sequenced Packet Socket。提供虛擬電路(TCP)並附有維護訊息的功能。

(C) protocol 參數

參數 protocol 是在某一個 domain 之下,選擇所要哪一種協定。例如選定 AF_INET domain 系列時,所使用的協定可以是 TCP、UDP 或 IP中的一種。但當設定 domain 和 type 值時,對於所使用的協定大多已經指定完成,因此 protocol 的值一般都設定為0。但有一特殊情況,如果 Socket 的型態是 SOCK_RAW 時,必須在參數中指定它的上層協定為 TCP、UDP、IP 或 ICMP。

另一個系統呼叫 socketpair() 是用來開啟兩個 Socket,並建立它們之間的連線。經過 socketpair() 呼叫所產生的兩個 Socket 必定是在同一系統上,好像一般 Unix 上的管道(Pipe)一樣,因此,domain 只能設定為 AF_UNIX,否則會發生錯誤。socketpair() 的語法如下:

#include <sys/types.h>

#include <sys/socket.h>

 

int domain, type, protocol, status, fd_array[2];

 

status = socketpair(domain, type, protocol, fd_array);

如果系統執行正常,status 會回應一個大於 0 的數值,而 fd_array 回應兩個檔案描述子來代表 Socket。

8-5-3 定址 Socket

產生一個 Socket 之後,必須針對該 Socket 定址,才會產生作用。定址的目的是將 Socket 連結到傳輸埠口上,Socket 才會有適當的通訊位址,因此稱之為 『定址』(Addressing)。定址 Socket 的功能呼叫是 bind(),它的基本做法是將已開啟的 Socket 描述子,再加上欲連接的 IP 位址和傳輸埠口,建構一個資料結構,再呼叫系統核心連接傳輸埠口。所建立的資料結構依照通訊協定方式有所不同:

● AF_UNIX:由 <sys/socket.h> 包含檔中定義 sockaddr 資料結構,其型態如 8-4-1 節所示。

● AF_INET:則在 <netinet/in.h> 包含檔中定義兩個資料結構:socketaddr_in 和 in_addr,其格式如 8-4-2 節所示。

● AF_NS:則在 <nstns/ns.h> 包含檔中描述 ns_addr 與 sockaddr_ns。AF_NS 不在本書討論範圍,因而不再另述。

我們以 AF_INET 為範例,bind() 呼叫程序如下:

#include <sys/types.h>

#include <netinet/in.h>

int fd;

struct sockaddr_in  *addressp;

int addrlen;

int status;

 

status = bind(fd, addressp, addrlen);

在 bind() 程式中,fd 為所欲連結之 Socket 的描述子,addressp 包含有關 Socket 欲連接的環境參數,如 IP 位址及 TCP 埠口等;addrlen 表示 addressp 資料的長度,以位元組計算。如果呼叫成功,則 status 會回應一個大於 0 的整數,其中 sockadd_in 資料結構內 IP 位址和埠口號碼的位元次序,與網路位元組的次序相反,一般必須經過位元組次序轉換(ntonl() 等函數)。至於如何建構 sockaddr_in 的內容,請參考 8-6 範例說明。

8-5-4 接受連線請求

一般網路應用環境都以 Client/Server 架構為大宗,Server 端的 Socket 經過定址後,必須進入聆聽(Listen)狀態,準備聆聽來自 Client 端的連線要求,但並未開始接受連線請求。Server 端再進入接收狀態(Accept),隨時接收 Client 端的連線訊息。當 Server 端執行 accept() 系統呼叫時,會進入等待狀況(Wait State),一直到接收到連線請求為止,因此接受對方連線有兩個功能呼叫為:listen() 和 accept()。

(A) listen():設定 Socket 進入 Listen 狀態

int fd;

int qlen;

int status;

 

status = listen(fd, qlen);

        listen() 設定 Socket fd 進入聆聽狀態,其中 sqlen 表示 Server 在執行 accept() 功能呼叫之前,系統所能佇列(Queue)Client 端要求的連線數目,如果執行正常,則 status 回應 0,否則回應 -1。listen() 是屬於 Server 端的功能呼叫,而且只適用於 SOCK_STREM 與 SOCK_SEQPACKET 類別(連接導向服務)的 Socket 上。

listen() 通常在 socket()、bind() 功能呼叫之後執行,其後緊接著 accept() 功能呼叫。雖然 listen() 只設定 Socket 成為聆聽狀態,但是系統執行此一功能呼叫時,已將下層的 TCP 狀態設定為接收狀態,當有連線要求進來時,下層的 TCP/IP 程式依然會接收連線,而將進來的連線要求排入佇列內,等待 accept() 系統呼叫來索取訊息。另一方面,上層的應用程式也必須執行 accept() 後,才會知道是否有連線進來。

(B) accept():接受對方連線要求

#include <sys/types.h>

#include <sys/socket.h>

 

int fd;

struct sockaddr *peeradr;

int peeradrlen;

int newfd;

 

newfd = accept(fd, peeradr, &peeradrlen);

        通常 accept() 是在 listen() 之後執行的,以便接收來自 Client 端的連線請求,fd 的值和 listen() 中所使用的 fd 值相同。

如果在同步傳輸模式(Synchronous),則 accept() 會等待到連線到達。當連線到達時,accept() 會再開啟一個新的 Socket(newfd),新的 Socket 繼承原來 Socket(fd)的所有特性(也就是 newfd 可當作 fd 的 Child Socket),爾後所有連線的處理動作都由新 Socket(newfd)負責,而原來 Socket(fd)則回到聆聽狀態,準備接受下一個連線請求。

當 accept() 執行成功時,會回傳 Client 端的 Socket 位址,以標明哪一個 Socket 要求連線。Server 端必須提供一個空的 Socket 資料結構(peeradr)來承接該位址,而其依照不同通訊型態有各自的資料結構,例如 AF_INET 為 sockaddr_in、AF_UNIX 是 sockaddr。參數 peeradrlen 是整數指標,在呼叫 accept() 時傳入 peeradr 的資料長度。Accept() 執行成功後,系統會回應對方 Socket 位址的實際長度,對 AF_INET 型態而言,該傳回值固定為 sizeof(sockaddr_in),但對 AF_UNIX 而言,該傳回值並不固定(一般在 110 位元組以內)。

如果在非同步傳輸模式(Asynchronous),執行 accept() 之前沒有 Client 要求連線時,系統會回應 -1(newfd = -1),並告知錯誤訊息(errno)。在同步傳輸模式中,也會有其它因素造成執行錯誤,而回應 -1(newfd = -1),但可由錯誤訊息中瞭解執行失敗的原因。

8-5-5 利用 TCP 協定傳輸資料

利用 TCP 協定來傳輸資料,首先應使用連線程式(listen() 與 accept() 功能呼叫),待連線建立後,資料即固定在這條連線上流動,使用者就可以利用 read()/recv() 與 write()/send() 來傳輸資料。事實上,因為雙方連線已經建立,傳輸資料只須針對傳輸緩衝器作讀寫的動作,因此 write() 可以取代 send() 的動作,另外 read() 的動作也和 recv() 一樣。

(A) write():將資料寫入 Socket 中,傳送給連線對方。

#include <sys/socket.h>

int sd;

int nbytes;

char *buf;

int ndata;

 

ndata = write(sd, buf, nbytes);

其實 write() 是將資料寫入傳送緩衝器上,再由下層 TCP/IP 通訊軟體負責傳送到對方,對方也是如此,下層 TCP/IP 通訊軟體負責將連線中的資料接收後,儲存於接收緩衝器,等待上層執行 read() 功能呼叫來讀取。write() 程式中的 sd 是執行 accept() 程式之後,所產生的 Socket 識別碼(newfd),參數 buf 是一個字元指標,指向欲寫入緩衝器的位址,nbyte 表示欲寫入多少個位元組資料,也就是傳送資料的長度。

當 write() 執行完後,會傳回確實所寫入的位元組數目 ndata,如果 ndata = -1 表示執行錯誤。這種現象表示呼叫 write() 時,所欲發送資料的數目並不一定一次可以發送完,也許必須經過多次的發送。這是因為執行 write() 時,系統只將資料佇列於傳送緩衝器上,再由底層負責傳送(TCP/IP 軟體),但如果寫入動作太快,而下層來不及傳送,便會發生緩衝器滿載的情形,此時系統無法接收新的資料。使用者必須檢查 ndata 所傳回的值,得知真正傳送出去的位元組。如果沒有全部傳送出去,可延遲一段時間後再傳送剩餘部分。

(B) read():由連線中的 Socket 讀取資料

#include <sys/socket.h>

int sd;

int nbytes;

char *buf;

int ndata;

 

ndata = read(sd, buf, nbytes);

read() 的功能是由連線中的 Socket 讀取資料,即接收讀取緩衝器上的資料。其中 sd 是經過建立連線後(connect() 功能呼叫)所產生新的 Socket 識別碼,也就是連線的 Socket ID。buf 是一個字元指標,表示所接收資料的存放位址。nbytes 為預留接收位置的長度,表示此次執行 read() 最高可接收多少資料。當 read() 執行後,會回傳一個整數(ndata)表示確實接收到多少個位元組資料,如果回應 -1(ndata = -1)表示功能呼叫錯誤。

事實上,當建立連線後(connect()),下層 TCP/IP 通訊軟體已開始接收資料,而將收到的資料儲存於接收緩衝器上,等待應用程式執行 read() 來讀取資料。但如果緩衝器已滿,而上層還未讀取,則下層(TCP/IP)會通知對方暫緩傳送。

8-5-6 利用 UDP 協定傳輸資料

        利用 UDP 協定傳輸資料時,不需要經過建立連線程式,只要雙方開啟 Socket 之後,便可利用 sendto() 和 recvfrom() 來傳送資料。資料在傳送當中必須註明對方 Socket 位址,接收時也必須由資料中判斷是由何地方傳送過來,因此在 sendto() 和 recvfrom() 兩個功能呼叫中的參數較為複雜。

(A) sendto():將資料傳送給遠端 Socket

#include <sys/types.h>

#include <sys/socket.h>

int sd;

char *buf;

int len;

int flages;

struct sockaddr *tosd;

int tosdlen;

int ndata;

 

ndata = sendto(sd, buf, len, flages, tosd, tosdlen);

sendto() 是用來將資料傳送給對方,sd 表示本身 Socket ID(型態為 SOCK_DGRAM),buf 為欲傳送資料的指標位置,len 表示所要傳送資料的長度(位元組),參數 flags 是用來告知系統傳送的特性。參數 tosd 表示所要傳送對方 Socket 的位址,而 tosdlen 為對方 Socket 位址的長度,如以 AF_INET 為例,位址長度為 sizeof(struct sockaddr_in)。

和 read() 一樣,sendto() 並不保證所欲傳送的資料可以傳完,也許會有緩衝器滿載的現象,因此執行 sendto() 後,會傳回已成功傳送的位元組數目(ndata),如果傳回的值為 -1(ndata = -1)表示該功能呼叫失敗。

(B) recvfrom():接收遠端 Socket 傳來的資料

#include <sys/types.h>

#include <sys/socket.h>

 

int sd;

char *buff;

int len;

int flags;

struct sockaddr *fromsock;

int *fsocklen;

int ndata;

 

ndata = recvfrom(sd, buff, len, flags, fromsock, fsocklen);

recvfrom() 是用來收取遠端 Socket 傳給本機資料的功能呼叫。其中參數 sd 表示本地 Socket 位址(經由 socket() 與 bind() 呼叫而成);buff 為所接收資料存放位址的指標;len 為預留多少長度的緩衝器準備接收資料,以位元組計算;flags 是告知系統接收資料的工作模式。接收到對方傳送之資料時,也必須知道對方來自何處,呼叫者必須準備一個 Socket 資料結構的空間,來存放對方 Socket 的位址,此為 fromsock 參數,而對方位址的長度存放於 fsocklen 變數內,兩者皆為指標變數。

執行 recvfrom() 功能呼叫後,會傳回到底接收到幾個位元組資料(ndata),如果回應 -1(ndata = -1)表示執行錯誤。參數 flags 是用來告知系統傳送或接收資料的特性,其定義如下:

● MSG_OOB:接收或傳送 Out of Band 的資料,此一選項僅適用於 Stream Socket。

● MSG_PEEK:當設定此功能時,系統將資料傳送資料後,緩衝器還會保留原來資料。

8-5-7 管理 Socket 系統呼叫

        在 Unix/Linux 上提供一些有關 Socket 管理的功能呼叫,分別敘述如下:

(A) getpeername():獲取連線對方的 Socket 位址

#include <sys/socket.h>

int sd;

int *peeradrlen;

struct sockaddr *peeradr;

int status;

 

status = getpeername(sd, peeradr, peeradrlen);

getpeername() 是用來獲取連線對方 Socket 位址的功能呼叫。呼叫者必須提供一個空的 Socket 位址(peeradr)來接收該位址,而必須指定所承接緩衝器的長度(peeradrlen),兩者皆為位址指標。參數 sd 表示本身 Socket 的識別碼。如果執行正確會回應 0(status = 0);否則回應 -1(status = -1)。

(B) getsockname():取得本地 Socket 的位址

#include <sys/socket.h>

int sd;

int *myadrlen;

struct sockaddr *myadr;

int status;

 

status = getsockname(sd, myadr, myadrlen);

getsockname() 是用來讀取本地 Socket 位址的功能呼叫,其參數 myadr 及 myadrlen 如同 getpeername() 的定義相同。

(C) setsockopt():設定 Socket 特性選項

#include <sys/socket.h>

 

int sd;

int level;

int optname;

int *optlen;

char *optval;

int status

 

status = getsockopt(sd, level, optname, optval, optlen);

setsockopt() 的功能是設定 Socket 的一些特性,這些特性會影響到 Socket 的運作情形。參數 sd 為欲設定之 Socket,而執行正常會回應 0(status = 0);否則回應 -1。參數 level 表示欲設定選項值是位於 TCP/IP 通訊協定之中哪一個介面,一般有三個主要設定介面:(如圖 8-6 所示)

(1) SOL_SOCKET:設定選項值位於 Socket 層次(Level)。

(2) IPPROTO_TCP:針對 TCP 層次設定選項值,此設定方式只有對 TCP/IP 通訊模式有效。

(3) IPPROTO_IP:針對 IP 層次設定選項,也是僅對 TCP/IP 模式有效。

圖 8-6 Socket 設定選項層次

參數 optname、optval 與 opelen 表示設定選項的名稱,及其相對應之設定值和設定值的長度。有關選項值和設定層次有關,而且各家系統並不完全相同,使用時必須參考系統使用手冊才會正確,一般常用的設定選項有:

● SO_BROADCAST:設定通訊通道具有廣播的功能。

● SO_DEBUG:設定核心(Kernel)能結合通訊通道具有偵錯(Debug)資訊的功能。

● SO_DONTROUTE:設定資料能 Bypass 核心程式的路由選擇。

● SO_KEEPALIVE:設定虛擬電路能保持連線的功能。譬如在連線一端終止連接,而本通訊點還可保持連接狀態。

● SO_LINGER:設定如何處理當連線終止後,還保留在佇列器上等待傳送的資料。

● SO_RCVBUF:修改 Socket 的接收緩衝器空間大小。

● SO_SNDBUF:修改 Socket 的傳送緩衝器空間大小。

(D) getsockopt():讀取 Socket 上的選項設定值

#include <sys/socket.h>

 

int sd, level, optname, *optlen;

char *optval;

int status;

 

status = getsockopt(sd, level, optname, optval, optlen);

        getsockopt() 和 setsockopt() 的功能正好相反,getsockopt() 是用來取得目前 Socket 選項的設定值,參數表示方式也如同 setsockopt() 一樣。

(E)  shutdown() 或 close():關閉 Socket

int sd;

int status;

 

status = close(sd);

        close() 將 Socket sd 關閉,以結束通訊連線。但可能還有訊息佇列在緩衝器(包括資料、確認訊號等等)尚未傳送出去。對於一般 TCP 協定而言,雖然執行 close() 後回即時回應完成訊息,其實 TCP/IP 通訊軟體還是會繼續將訊息傳送完畢,才真正完成斷線的工作。對於非連接方式(UDP)的 Socket 而言,理論上下層並不需要作斷線的動作,也就是說可以不用下 close() 功能呼叫,但是為了讓系統系放所佔有的資源,使用者還是必須下 close() 功能,以完成釋放的動作。

8-5-8 其它功能呼叫程式

        以下介紹其它有關 Socket 程式發展的功能呼叫:

(A) 網路位元組次序和主機位元組次序轉換

unsigned long i, k;

k = htonl(i)  /* host to network byte order converter (long) */

 

unsigned short i, k;

k = htons(i)  /* host to network byte order converter (short) */

 

unsigned long i, k;

k = ntohl(i)  /* network to host byte order converter (long) */

 

unsigned short i, k;

k = ntohs(i)  /* network to host byte order converter (short) */

主機位元組表示的次序(Order)依照各種 CPU 型態有所不同,Motorola 和 Intel CPU 表示次序剛好相反,一般網路位址的表示次序和 Motorola CPU 相同,因此使用 Intel 系列的 CPU,其網路和主機位址的次序必須經過轉換才會相配合。htonl() 和 htons() 是將主機位元組次序轉換為網路位元組次序,一者為長整數(long);而另一者為短整數(short)。ntohl() 和 ntohs() 是將網路位元組次序轉換成主機位元組次序。例如,i = 0x1234,則 htons(i) = 0x3412;若 i = 0x12345678,則 htonl(i) = 0x78563412。

(B) 區塊資料拷貝、清除或比較

char *src, *dest;

int len;

void bcopy(src, dest, len)

 

char *src;

int len;

void(src, len);

 

chat *src, *dest;

int len, status;

status = bcmp(src, dest, len);

        在網路程式設計上常需要將資料由一個緩衝器複製到另一個緩衝器上(bcopy())、或者清除緩衝器上的資料(bzero())、或兩個緩衝器之間的內容做比較(bcmp())。以 bcmp() 為範例,參數 src 表示來源資料所存放位址的指標,dest 表示被比較(目的)資料的指標,len 為比較資料的位元組長度,回應值 status = 0 表示兩個緩衝器的內容相同;否則為 status = -1。

 

 

<GOTOP>