5. System call 或 Bust

我們在本章開始討論如何讓你存取 UNIX 系統或 BSD、Windows、Linux、Mac 等系統的 system call(系統呼叫)、socket API 及其它 function calls(函式呼叫)等網路功能。當你呼叫其中一個函式時,kernel 會接管,並且自動幫你處理全部的工作。

多數人會卡在這裡是因為不知道要用什麼樣的順序來呼叫這些函式,而你在找 man 使用手冊時會覺得手冊很難用。好的,為了要幫忙解決這可怕的困境,我已經試著在下列的章節精確地勾勒出(layout) system call,你在寫程式時只要照著一樣的順序呼叫就可以了。

為了要連結一些程式碼,需要一些牛奶跟餅乾[這恐怕你要自行準備],以及一些決心與勇氣,而你就能將資料發送到網際網路上,彷彿是 Jon Postel 之子般。

[請注意,為了簡潔,下列許多程式碼片段並沒有包含錯誤檢查的程式碼。而且它們很愛假設呼叫 getaddrinfo() 的結果都會成功,並會傳回鏈結串列(link-list)中的一個有效資料。這兩種情況在單獨執行的程式都有嚴謹的定位,所以,還是將它們當作模型來使用吧。]

5.1. getaddrinfo()-準備開始!

這是個有很多選項的工作馬(workhorse)函式,但是卻相當容易上手。它幫你設定之後需要的 struct。

談點歷史:它前身是你用來做 DNS 查詢的 gethostbyname()。而當時你需要手動將資訊載入 struct sockaddr_in,並在你的呼叫中使用。

感謝老天,現在已經不用了。[如果你想要設計能通用於 IPv4 與 IPv6 的程式也不用!]在現代,你有 getaddrinfo() 函式,可以幫你做許多事情,包含 DNS 與 service name 查詢,並填好你所需的 structs。

讓我們來看看!
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

int getaddrinfo(const char *node, // 例如: "www.example.com" 或 IP
                const char *service, // 例如: "http" 或 port number
                const struct addrinfo *hints,
                struct addrinfo **res);
你給這個函式三個輸入參數,結果它會回傳給你一個指向鏈結串列的指標 - res。

node 參數是要連線的主機名稱,或者一個 IP address(位址)。

下一個參數是 service,這可以是 port number,像是 "80",或者特定服務的名稱[可以在你 UNIX 系統上的 IANA Port List [17] 或 /etc/services 檔案中找到],像是 "http" 或 "ftp" 或 "telnet" 或 "smtp" 諸如此類的。

最後,hints 參數指向一個你已經填好相關資訊的 struct addrinfo。

這裡是一個呼叫範例,如果你是一部 server(伺服器),想要在你主機上的 IP address 及 port 3490 執行 listen。要注意的是,這邊實際上沒有做任何的 listening 或網路設定;它只有設定我們之後要用的 structures 而已。
int status;
struct addrinfo hints;
struct addrinfo *servinfo; // 將指向結果

memset(&hints, 0, sizeof hints); // 確保 struct 為空
hints.ai_family = AF_UNSPEC; // 不用管是 IPv4 或 IPv6
hints.ai_socktype = SOCK_STREAM; // TCP stream sockets
hints.ai_flags = AI_PASSIVE; // 幫我填好我的 IP 

if ((status = getaddrinfo(NULL, "3490", &hints, &servinfo)) != 0) {
  fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
  exit(1);
}

// servinfo 目前指向一個或多個 struct addrinfos 的鏈結串列

// ... 做每件事情,一直到你不再需要 servinfo  ....

freeaddrinfo(servinfo); // 釋放這個鏈結串列


注意一下,我將 ai_family 設定為 AF_UNSPEC,這樣代表我不用管我們用的是 IPv4 或 IPv6 address。如果你想要指定的話,你可以將它設定為 AF_INET 或 AF_INET6。

還有,你會在這裡看到 AI_PASSIVE 旗標;這個會告訴 getaddrinfo() 要將我本機的位址(address of local host)指定給 socket structure。這樣很棒,因為你就不用把位址寫死了[或者你可以將特定的位址放在 getaddrinfo() 的第一個參數中,我現在寫 NULL 的那個參數]。

然後我們執行呼叫,若有錯誤發生時[getaddrinfo 會傳回非零的值],如你所見,我們可以使用 gai_strerror() 函式將錯誤印出來。若每件事情都正常運作,那麼 serinfo 就會指向一個 struct addrinfos 的鏈結串列,串列中的每個成員都會包含一個我們之後會用到的某種 struct sockaddr。

最後,當我們終於使用 getaddrinfo() 配置的鏈結串列完成工作後,我們可以[也應該]要呼叫 freeaddrinfo() 將鏈結串列全部釋放。

這邊有一個呼叫範例,如果你是一個想要連線到特定 server 的 client(客戶端),比如是:"www.example.net"的 port 3490。再次強調,這裡並沒有真的進行連線,它只是設定我們之後要用的 structure。
int status;
struct addrinfo hints;
struct addrinfo *servinfo; // 將指向結果

memset(&hints, 0, sizeof hints); // 確保 struct 為空
hints.ai_family = AF_UNSPEC; // 不用管是 IPv4 或 IPv6
hints.ai_socktype = SOCK_STREAM; // TCP stream sockets

// 準備好連線
status = getaddrinfo("www.example.net", "3490", &hints, &servinfo);

// servinfo 現在指向有一個或多個 struct addrinfos 的鏈結串列


我一直說 serinfo 是一個鏈結串列,它有各種的位址資訊。讓我們寫一個能快速 demo 的程式,來呈現這個資訊。這個小程式 [18] 會印出你在命令列中所指定的主機之 IP address:
/*
** showip.c -- 顯示命令列中所給的主機 IP address
*/
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main(int argc, char *argv[])
{
  struct addrinfo hints, *res, *p;
  int status;
  char ipstr[INET6_ADDRSTRLEN];

  if (argc != 2) {
    fprintf(stderr,"usage: showip hostname\n");
    return 1;
  }

  memset(&hints, 0, sizeof hints);
  hints.ai_family = AF_UNSPEC; // AF_INET 或 AF_INET6 可以指定版本
  hints.ai_socktype = SOCK_STREAM;

  if ((status = getaddrinfo(argv[1], NULL, &hints, &res)) != 0) {
    fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
    return 2;
  }

  printf("IP addresses for %s:\n\n", argv[1]);

  for(p = res;p != NULL; p = p->ai_next) {
    void *addr;
    char *ipver;

    // 取得本身位址的指標,
    // 在 IPv4 與 IPv6 中的欄位不同:
    if (p->ai_family == AF_INET) { // IPv4
      struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;
      addr = &(ipv4->sin_addr);
      ipver = "IPv4";
    } else { // IPv6
      struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;
      addr = &(ipv6->sin6_addr);
      ipver = "IPv6";
    }

    // convert the IP to a string and print it:
    inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr);
    printf(" %s: %s\n", ipver, ipstr);
  }

  freeaddrinfo(res); // 釋放鏈結串列

  return 0;
}
如你所見,程式碼使用你在命令列輸入的參數呼叫 getaddrinfo(),它填好 res 所指的鏈結串列,並接著我們就能重複那行並印出東西或做點類似的事。

[有點不好意思!我們在討論 struct sockaddrs 它的型別差異是因 IP 版本而異之處有點鄙俗。我不確定是否有較優雅的方法。]

在下面執行範例!來看看大家喜歡看的執行畫面:
$ showip www.example.net
IP addresses for www.example.net:

  IPv4: 192.0.2.88

$ showip ipv6.example.com
IP addresses for ipv6.example.com:

  IPv4: 192.0.2.101
  IPv6: 2001:db8:8c00:22::171
現在已經在我們的掌控之下,我們會將 getaddrinfo() 傳回的結果送給其它的 socket 函式,而且終於可以建立我們的網路連線了!

讓我們繼續看下去!

5.2. socket()-取得 File Descriptor!

我想可以不用再將 socket() 晾在旁邊了,我一定要講一下 socket() system call,這邊是程式片段:
#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
可是這些參數是什麼?

它們可以讓你設定想要的 socket 類型[IPv4 或 IPv6,stream 或 datagram 以及 TCP 或 UDP]。

以前的人得寫死這些值,而你也可以這樣做。[domain 是 PF_INET 或 PF_INET6,type 是 SOCK_STREAM 或 SOCK_DGRAM,而 protocol 可以設定為 0,用來幫給予的 type 選擇適當的協定。或者你可以呼叫 getprotobyname() 來查詢你想要的協定,"tcp"或"udp"]。

[PF_INET 就是你在初始化 struct sockaddr_in 的 sin_family 欄位會用到的,它是 AF_INET 的親戚。實際上,它們的關係很近,所以其實它們的值也都一樣,而許多程式設計師會呼叫 socket(),並以 AF_INET 取代 PF_INET 來做為第一個參數傳遞。

現在,你可以去拿點牛奶跟餅乾,因為又是說故事時間了。

在很久很久以前,人們認為它應該是位址家族(address family),就是 "AF_INET"中的"AF"所代表的意思;而位址家族也要支援協定家族(protocol family)的幾個協定,這件事並沒有發生,而之後它們都過著幸福快樂的日子,結束。

所以最該做的事情就是在你的 struct sockaddr_in 中使用 AF_INET,而在呼叫 socket() 時使用 PF_INET。]

總之,這樣就夠了。你真的該做的只是使用呼叫 getaddrinfo() 得到的值,並將這個值直接餵給 socket(),像這樣:
int s;
struct addrinfo hints, *res;

// 執行查詢
// [假裝我們已經填好 "hints" struct]
getaddrinfo("www.example.com", "http", &hints, &res);

// [再來,你應該要對 getaddrinfo() 進行錯誤檢查, 並走到 "res" 鏈結串列查詢能用的資料,
// 而不是假設第一筆資料就是好的[像這些範例一樣]
// 實際的範例請參考 client/server 章節。
s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
socket() 單純傳回給你一個之後 system call 要用的 socket descriptor,錯誤時會回傳 -1。errno 全域變數會設定為該錯誤的值[細節請見 errno 的 man 使用手冊,而且你需要繼續閱讀並執行更多與它相關的 system call,這樣會比較有感覺。]

5.3. bind()-What port am I on?

一旦你有了一個 socket,你會想要將這個 socket 與你本機上的 port 進行關聯[如果你正想要 listen() 特定 port 進入的連線,通常都會這樣做,比如:多人網路連線遊戲在它們告訴你"連線到 192.168.5.10 port 3490"時這麼做]。port number 是用來讓 kernel 可以比對出進入的封包是屬於哪個 process 的 socket descriptor。如果你只是正在進行 connect()[因為你是 client,而不是 server],這可能就不用。不過還是可以讀讀,好玩嘛。
#include <sys/types.h>
#include <sys/socket.h>

int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
sockfd 是 socket() 傳回的 socket file descriptor。my_addr 是指向包含你的位址資訊、名稱及 IP address 的 struct sockaddr 之指標。addrlen 是以 byte 為單位的位址長度。

呼!有點比較好玩了。我們來看一個範例,它將 socket bind(綁定)到執行程式的主機上,port 是 3490:
struct addrinfo hints, *res;
int sockfd;

// 首先,用 getaddrinfo() 載入位址結構:

memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // fill in my IP for me

getaddrinfo(NULL, "3490", &hints, &res);

// 建立一個 socket:

sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

// 將 socket bind 到我們傳遞給 getaddrinfo() 的 port:

bind(sockfd, res->ai_addr, res->ai_addrlen);
使用 AI_PASSIVE 旗標,我可以跟程式說要 bind 它所在主機的 IP。如果你想要 bind 到指定的本地 IP address,捨棄 AI_PASSIVE,並改放一個位址到 getaddrinfo() 的第一個參數。

bind() 在錯誤時也會傳回 -1,並將 errno 設定為該錯誤的值。

許多舊程式都在呼叫 bind() 以前手動封裝 struct sockaddr_in。很顯然地,這是 IPv4 才有的,可是真的沒有辦法阻止你在 IPv6 做一樣的事情,一般來說,使用 getaddrinfo() 會比較簡單。總之,舊版的程式看起來會像這樣:
// !!! 這 是 老 方 法 !!!

int sockfd;
struct sockaddr_in my_addr;

sockfd = socket(PF_INET, SOCK_STREAM, 0);

my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(MYPORT); // short, network byte order
my_addr.sin_addr.s_addr = inet_addr("10.12.110.57");
memset(my_addr.sin_zero, '\0', sizeof my_addr.sin_zero);

bind(sockfd, (struct sockaddr *)&my_addr, sizeof my_addr);
在上列的程式碼中,如果你想要 bind 到你本機的 IP address[就像上面的 AI_PASSIVE 旗標],你也可以將 INADDR_ANY 指定給 s_addr 欄位。INADDR_ANY 的 IPv6 版本是一個 in6addr_any 全域變數,它會被指定給你的 struct sockaddr_in6 的 sin6_addr 欄位。

[也有一個你能用於變數初始器(variable initializer)的 IN6ADDR_ANY_INIT macro(巨集)]

另一件呼叫 bind() 時要小心的事情是:不要用太小的 port number。全部 1024 以下的 ports 都是保留的[除非你是系統管理員]!你可以使用任何 1024 以上的 port number,最高到 65535[提供尚未被其它程式使用的]。

你可能有注意到,有時候你試著重新執行 server,而 bind() 卻失敗了,它聲稱"Address already in use."(位址使用中)。這是什麼意思呢?很好,有些連接到 socket 的連線還懸在 kernel 裡面,而它佔據了這個 port。你可以等待它自行清除[一分鐘之類],或者在你的程式中新增程式碼,讓它重新使用這個 port,類似這樣:
int yes=1;
//char yes='1'; // Solaris 的使用者用這個

// 可以跳過 "Address already in use" 錯誤訊息

if (setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) {
  perror("setsockopt");
  exit(1);
}
最後一個對 bind() 的額外小提醒:在你不願意呼叫 bind() 時。若你正使用 connect() 連線到遠端的機器,你可以不用管 local port 是多少(以 telnet 為例,你只管遠端的 port 就好),你可以單純地呼叫 connect(),它會檢查 socket 是否尚未綁定(unbound),並在有需要的時候自動將 socket bind() 到一個尚未使用的 local port。

5.4. connect(),嘿!你好。

咱們用幾分鐘的時間假裝你是個 telnet 應用程式,你的使用者命令你[就像 TRON 電影裡那樣]取得一個 socket file descriptor。你執行並呼叫 socket()。接著使用者告訴你連線到 "10.12.110.57" 的 port 23[標準 telnet port]。喲!你現在該做什麼呢?

你是很幸運的程式,你現在可以細讀 connect() 的章節,如何連線到遠端主機。所以努力往前讀吧!刻不容緩!

connect() call 如下:
#include <sys/types.h>
#include <sys/socket.h>

int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
sockfd 是我們的好鄰居 socket file descriptor,如同 socket() 呼叫所傳回的,serv_addr 是一個 struct sockaddr,包含了目的 port 及 IP 位址,而 addrlen 是以 byte 為單位的 server 位址結構之長度。

全部的資訊都可以從 getaddrinfo() 呼叫中取得,它很棒。

這樣有開始比較有感覺了嗎?我在這裡沒辦法知道,所以我只能希望是這樣沒錯。

我們有個範例,這邊我們用 socket 連線到 "www.example.com" 的 port 3490:
struct addrinfo hints, *res;
int sockfd;

// 首先,用 getaddrinfo() 載入 address structs:

memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;

getaddrinfo("www.example.com", "3490", &hints, &res);

// 建立一個 socket:

sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

// connect!
connect(sockfd, res->ai_addr, res->ai_addrlen);
老學校的程式再次填滿了它們自己的 struct sockaddr_ins 並傳給 connect()。如果你願意的話,你可以這樣做。請見上面 bind() 章節中類似的提點。

要確定有檢查 connect() 的傳回值,它在錯誤時會傳回 -1,並設定 errno 變數。

還要注意的是,我們不會呼叫 bind()。基本上,我們不用管我們的 local port number;我們只在意我們的目地[遠端 port]。Kernel 會幫我們選擇一個 local port,而我們要連線的站台會自動從我們這裡取得資訊,不用擔心。

5.5. listen()-有人會呼叫我嗎?

OK,是該改變步調的時候了。如果你不想要連線到一個遠端主機要怎麼做。

我說過,好玩就好,你想要等待進入的連線,並以某種方式處理它們。

這個過程有兩個步驟:你要先呼叫 listen(),接著呼叫 accept()[參考下一節]。

listen 呼叫相當簡單,不過需要一點說明:
int listen(int sockfd, int backlog);

sockfd 是來自 socket() system call 的一般 socket file descriptor。backlog 是進入的佇列(incoming queue)中所允許的連線數目。這代表什麼意思呢?好的,進入的連線將會在這個佇列中排隊等待,直到你 accept() 它們(請見下節),而這限制了排隊的數量。多數的系統預設將這個數值限制為 20;你或許可以一開始就將它設定為 5 或 10。

再來,如同往常,listen() 會傳回 -1 並在錯誤時設定 errno。

好的,你可能會想像,我們需要在呼叫 listen() 以前呼叫 bind(),讓 server 可以在指定的 port 上執行。[你必須能告訴你的好朋友要連線到哪一個 port!]所以如果你正在 listen 進入的連線,你會執行的 system call 順序是:
getaddrinfo();
socket();
bind();
listen();
/* accept() 從這裡開始 */
我只是留下範例程式的位置,因為它相當顯而易見。[在下面 accept() 章節中的程式碼會比較完整]。這整件事情真正需要技巧的部分是呼叫 accept()。

5.6. accept()-"謝謝你呼叫 port 3490。"

準備好,accept() 呼叫是很奇妙的!會發生的事情就是:很遠的人會試著 connect() 到你的電腦正在 listen() 的 port。他們的連線會排隊等待被 accept()。你呼叫 accept(),並告訴它要取得擱置的(pending)連線。它會傳回給你專屬這個連線的一個新 socket file descriptor!那是對的,你突然有了兩個 socket file descriptor!原本的 socket file descriptor 仍然正在 listen 之後的連線,而新建立的 socket file descriptor 則是在最後要準備給 send() 與 recv() 用的。

呼叫如下:
#include <sys/types.h>
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd 是正在進行 listen() 的 socket descriptor。很簡單,addr 通常是一個指向 local struct sockaddr_storage 的指標,關於進來的連線將往哪裡去的資訊[而你可以用它來得知是哪一台主機從哪一個 port 呼叫你的]。addrlen 是一個 local 的整數變數,應該在將它的位址傳遞給 accept() 以前,將它設定為 sizeof(struct sockaddr_storage)。accept() 不會存放更多的 bytes(位元組)到 addr。若它存放了較少的 bytes 進去,它會改變 addrlen 的值來表示。

有想到嗎?accept() 在錯誤發生時傳回 -1 並設定 errno。不過 BetCha 不這麼認為。

跟以前一樣,用一段程式範例會比較好吸收,所以這裡有一段範例程供你細讀:
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MYPORT "3490" // 使用者將連線的 port
#define BACKLOG 10 // 在佇列中可以有多少個連線在等待

int main(void)
{
  struct sockaddr_storage their_addr;
  socklen_t addr_size;
  struct addrinfo hints, *res;
  int sockfd, new_fd;

  // !! 不要忘了幫這些呼叫做錯誤檢查 !!

  // 首先,使用 getaddrinfo() 載入 address struct:

  memset(&hints, 0, sizeof hints);
  hints.ai_family = AF_UNSPEC; // 使用 IPv4 或 IPv6,都可以
  hints.ai_socktype = SOCK_STREAM;
  hints.ai_flags = AI_PASSIVE; // 幫我填上我的 IP 

  getaddrinfo(NULL, MYPORT, &hints, &res);

  // 產生一個 socket,bind socket,並 listen socket:

  sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
  bind(sockfd, res->ai_addr, res->ai_addrlen);
  listen(sockfd, BACKLOG);

  // 現在接受一個進入的連線:

  addr_size = sizeof their_addr;
  new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &addr_size);

  // 準備好與 new_fd 這個 socket descriptor 進行溝通!
  .
  .
  .

一樣,我們會將 new_fd socket descriptor 用於 send() 與 recv() 呼叫。若你只是要取得一個連線,你可以用 close() 關閉正在 listen 的 sockfd,以避免有更多的連線進入同樣的 port,若你有這個需要的話。

5.7. send() 與 recv()- 寶貝,跟我說說話!

這兩個用來通信的函式是透過 stream socket 或 connected datagram ssocket。若你想要使用常規的 unconnected datagram socket,你會需要參考底下的 sendto() 及 recvfrom() 的章節。

send() 呼叫:
int send(int sockfd, const void *msg, int len, int flags);
sockfd 是你想要送資料過去的 socket descriptor[不論它是不是 socket() 傳回的,或是你用 accept() 取得的]。msg 是一個指向你想要傳送資料之指標,而 len 是以 byte 為單位的資料長度。而 flags 設定為 0 就好。[更多相關的旗標資訊請見 send() man 使用手冊]。

一些範例程式如下:
char *msg = "Beej was here!";
int len, bytes_sent;
.
.
.
len = strlen(msg);
bytes_sent = send(sockfd, msg, len, 0);
.
.
.
send() 會傳回實際有送出的 byte 數目,這可能會少於你所要傳送的數目!有時候你告訴 send() 要送整筆的資料,而它就是無法處理這麼多資料。它只會盡量將資料送出,並認為你之後會再次送出剩下沒送出的部分。

要記住,如果 send() 傳回的值與 len 的值不符合的話,你就需要再送出字串剩下的部分。好消息是:如果封包很小[比 1K 還要小這類的],或許有機會一次就送出全部的東西。

一樣,錯誤時會傳回 -1,並將 errno 設定為錯誤碼(error number)。

recv() 呼叫在許多地方都是類似的:
int recv(int sockfd, void *buf, int len, int flags);
sockfd 是要讀取的 socket descriptor,buf 是要記錄讀到資訊的緩衝區(buffer),len 是緩衝區的最大長度,而 flags 可以再設定為 0。[關於旗標資訊的細節請參考 recv() 的 man 使用手冊]。

recv() 傳回實際讀到並寫入到緩衝區的 byte 數,而錯誤時傳回 -1[並設定相對的 errno]。

等等! recv() 會傳回 0,這只能表示一件事情:遠端那邊已經關閉了你的連線!recv() 傳回 0 的值是讓你知道這件事情。

這樣很簡單,不是嗎?你現在可以送回資料,並往 stream sockets 邁進!嘻嘻!你是 UNIX 網路程式設計師了。

5.8. sendto() 與 recvfrom()- 用 DGRAM 風格跟我說說話

我聽到你說,"這全部都是上等的好貨","可是我該如何使用 unconnected datagram socket 呢?"

沒問題,朋友。我們正要講這件事。

因為 datagram socket 沒有連線到到遠端主機,猜猜看,我們在送出封包以前會需要哪些資訊呢?

對!目的位址!在這裡搶先看:
sendto(int sockfd, const void *msg, int len, unsigned int flags,
       const struct sockaddr *to, socklen_t tolen);
如你所見,這個呼叫基本上與呼叫 send() 一樣,只是多了兩個額外的資訊。to 是一個指向 struct sockaddr[這或許是另一個你可以在最後轉型的 struct sockaddr_in 或 struct sockaddr_in6 或 struct sockaddr_storage]的指標,它包含了目的 IP address 與 port。tolen 是一個 int,可以單純地將它設定為 sizeof *to 或 sizeof(struct sockaddr_storage)。

為了能自動處理目的位址結構(destination address structure),你或許可以用底下的 getaddrinfo() 或 recvfrom(),或者你也可以手動填上。

如同 send(),sendto() 會傳回實際已傳送的資料數量(一樣,可能會少於你要傳送的資料量!)而錯誤時傳回 -1。

recv() 與 recvfrom() 也是差不多的。recvfrom() 的對照如下:
int recvfrom(int sockfd, void *buf, int len, unsigned int flags,
            struct sockaddr *from, int *fromlen);
一樣,它跟 recv() 很像,只是多了兩個欄位。from 是指向 local struct sockaddr_storage 的指標,這個資料結構包含了封包來源的 IP address 與 port。fromlen 是指向 local int 的指標,應該要初始化為 sizeof *from 或是 sizeof(struct sockaddr_storage)。當函式傳回時,fromlen 會包含實際上儲存於 from 中的位址長度。

recvfrom() 傳回接收的資料數目,或在發生錯誤時傳回 -1[並設定相對的 errno]。

所以這裡有個問題:為什麼我們要用 struct sockaddr_storage 做為 socket 的型別呢?為什麼不用 struct sockaddr_in 呢?

因為你知道的,我們不想要讓自己綁在 IPv4 或 IPv6,所以我們使用通用的泛型 struct sockaddr_storage,我們知道這樣有足夠的空間可以用在 IPv4 與 IPv6。

[所以 ... 這裡有另一個問題:為什麼不是 struct sockaddr 本身就可以容納任何位址呢?我們甚至可以將通用的 struct sockaddr_storage 轉型為通用的 struct sockaddr!似乎沒什麼關係又很累贅啊。答案是,它就是不夠大,我猜在這個時候更動它會有問題,所以他們就弄了一個新的。]

記住,如果你 connect() 到一個 datagram socket,你可以在你全部的交易中只使用 send() 與 recv()。socket 本身仍然是 datagram socket,而封包仍然使用 UDP,但是 socket interface 會自動幫你增加目的與來源資訊。

5.9. close() 與 shutdown()-從我面前消失吧!

呼!你已經整天都在 send() 與 recv()了。你正準備要關閉你 socket descriptor 的連線,這很簡單,你只要使用常規的 UNIX file descriptor close() 函式:
close(sockfd);
這會避免對 socket 做更多的讀寫。任何想要對這個遠端的 socket 進行讀寫的人都會收到錯誤。

如果你想要能多點控制 socket 如何關閉,可以使用 shutdown() 函式。它讓你可以切斷單向的通信,或者雙向[就像是 close() 所做的],這是函式原型:
int shutdown(int sockfd, int how);
sockfd 是你想要 shutdown 的 socket file descriptor,而 how 是下列其中一個值:
0 不允許再接收資料
1 不允許再傳送資料
2 不允許再傳送與接收資料[就像 close()]
shutdown() 成功時傳回 0,而錯誤時傳回 -1(設定相對的 errno)。

若你在 unconnected datagram socket 上使用 shutdown(),它只會單純的讓 socket 無法再進行 send() 與 recv() 呼叫[要記住你只能在有 connect() 到 datagram socket 的時候使用]。

重要的是 shutdown() 實際上沒有關閉 file descriptor,它只是改變了它的可用性。如果要釋放 socket descriptor,你還是需要使用 close()。

沒了。

[除了要記得的是,如果你用 Windows 與 Winsock,你應該要呼叫 closesocket() 而不是 close()。]

5.10 getpeername()-你是誰?

這個函式很簡單。

它太簡單了,我幾乎不想給它一個獨立的章節,雖然還是給了。

getpeername() 函式會告訴你另一端連線的 stream socket 是誰,函式原型如下:
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);
sockfd 是連線的 stream socket 之 descriptor,addr 是指向 struct sockaddr[或 struct sockaddr_in]的指標,這個資料結構儲存了連線另一端的資訊,而 addrlen 則是指向 int 的指標,應該將它初始化為 sizeof *addr 或 sizeof(struct sockaddr)。

函式在錯誤時傳回 -1,並設定相對的 errno。

一旦你取得了它們的位址,你就可以用 inet_ntop()、getnameinfo() 或 gethostbyaddr() 印出或取得更多的資訊。不過你無法取得它們的登入帳號。

[好好好,如果另一台電腦執行的是 ident daemon 就可以。]然而,這個已經超出本文的範圍,更多資訊請參考 RFC 1413 [19]。

5.11 gethostname()-我是誰?

比 getpeername() 更簡單的函式是 gethostname(),它會傳回你執行程式的電腦名稱,這個名稱之後可以用在 gethostbyname(),用來定義你本機電腦的 IP address。

有什麼更有趣的嗎?

我可以想到一些事情,不過這不適合 socket 程式設計,總之,下面是一段範例:
#include <unistd.h>
int gethostname(char *hostname, size_t size);
參數很簡單:hostname 是指向字元陣列(array of chars)的指標,它會儲存函式傳回的主機名稱(hostname),而 size 是以 byte 為單位的主機名稱長度。

函式在成功執行時傳回 0,在錯誤時傳回 -1,並一樣設定 errno。

[17] http://www.iana.org/assignments/port-numbers
[18] http://beej.us/guide/bgnet/examples/showip.c
[19] http://tools.ietf.org/html/rfc1413