Linux :套接字

OneCode2World發表於2015-08-06

Linux系統學習筆記:套接字

Yeolar   2012-05-18 14:22   

Linux系統學習筆記 Linux系統學習筆記:程式間通訊

上一篇總結了Linux中的一些經典的程式間通訊的機制,本篇總結使用套接字的程式間通訊的方法。套接字的優勢在於它採用同樣的介面來處理計算機內和不同計算機間的通訊,通常它用於網路程式間通訊,在計算機內,UNIX域套接字可以作為全雙工管道的實現。

套接字介面

套接字介面是一組用來結合UNIX I/O函式進行程式間通訊的函式,大多數系統上都實現了它,包括各種UNIX變種、Windows和Mac系統。

/media/note/2012/05/18/linux-socket/fig1.png

套接字介面

套接字描述符

套接字是通訊端點的抽象,使用套接字描述符訪問套接字,Linux用檔案描述符實現套接字描述符,很多處理檔案描述符的函式也可以用於套接字描述符。

使用 socket 函式建立套接字。

1 #include <sys/socket.h>
2 
3 /* 建立套接字
4  * @return      成功返回檔案描述符,出錯返回-1 */
5 int socket(int domain, int type, int protocol);

引數說明:

domain

確定通訊的特性,包括地址格式,每個域都有自己的地址格式。POSIX.1指定的域包括:

  • AF_INET :IPv4地址域。
  • AF_INET6 :IPv6地址域。
  • AF_UNIX :UNIX域。
  • AF_UNSPEC :未指定,可以代表任何域。
type

確定套接字的型別。POSIX.1定義的套接字型別有:

  • SOCK_SEQPACKET :長度固定、有序、可靠的面向連線報文傳遞。
  • SOCK_STREAM :有序、可靠、雙向的面向連線位元組流。
  • SOCK_DGRAM :長度固定、不可靠的無連線報文傳遞。
  • SOCK_RAW :IP協議的資料包介面。
protocol
通常為0,表示按給定的域和套接字型別選擇預設協議。在 domain 和 type 給定的情況下如果有多個協議,可以用protocol 指定協議。

在 AF_INET 通訊域, SOCK_SEQPACKET 套接字型別的預設協議是SCTP, SOCK_STREAM 套接字型別的預設協議是TCP,SOCK_DGRAM 套接字型別的預設協議是UDP。

面向連線的協議通訊可比作打電話。在交換資料前,要求在本地套接字和遠端套接字之間建立一個邏輯連線,即連線是端到端的通訊通道。會話中不包含地址資訊,它隱含在連線中。

SOCK_STREAM 套接字提供位元組流服務,從套接字讀取資料時可能需要多次函式呼叫。 SOCK_SEQPACKET 套接字提供報文服務,從套接字接收的資料量和對方傳送的一致。

資料包提供了無連線服務,它是一種自含報文,傳送資料包可比作寄郵件。可以傳送很多資料包,但不保證它們的順序,而且可能會丟失,資料包包含接收地址。

SOCK_RAW 套接字提供了一個資料包介面用於直接訪問網路層(IP層),使用它時需要自己構造協議首部。建立SOCK_RAW 套接字需要超級使用者特權。

儘管套接字描述符是檔案描述符,擔不是所有使用檔案描述符的函式都能處理套接字描述符,下面是有關函式的支援情況:

close 釋放套接字
dup dup2 正常複製
fcntl 支援一些命令,如 F_DUPFD 、 F_GETFD 、 F_GETFL 、 F_GETOWN 、 F_SETFD 、 F_SETFL 、F_SETOWN
fstat 支援一些 stat 結構成員,由實現定義
ioctl 支援部分命令,依賴於底層裝置驅動
poll 正常使用
read readv 等價於無標誌位的 recv
select 正常使用
write writev 等價於無標誌位的 send

close 直到套接字的最後一個描述符關閉時才釋放網路端點。

可以用 shutdown 函式來禁止套接字上的輸入/輸出。

1 #include <sys/socket.h>
2 
3 /* 關閉套接字上的輸入/輸出
4  * @return      成功返回0,出錯返回-1 */
5 int shutdown(int sockfd, int how);

how 可以取:

  • SHUT_RD ,關閉讀端,即無法從套接字讀取資料。
  • SHUT_WR ,關閉寫端,即無法用套接字傳送資料。
  • SHUT_RDWR ,關閉讀寫,同時無法讀取和傳送資料。

定址

程式標識確定目標通訊程式,它有兩部分:計算機的網路地址確定計算機,服務確定計算機上的特定程式。

位元組序

CPU有大端位元組序和小端位元組序兩種位元組表示順序。為使不同的計算機可以正常交換資訊,網路協議指定了位元組序。

TCP/IP協議棧採用大端位元組序。可以用下面的函式處理主機位元組序和網路位元組序之間的轉換。

 1 #include <arpa/inet.h>
 2 
 3 /* 將主機位元組序的32位整型數轉換為網路位元組序 */
 4 uint32_t htonl(uint32_t hostlong);
 5 /* 將主機位元組序的16位整型數轉換為網路位元組序 */
 6 uint16_t htons(uint16_t hostshort);
 7 /* 將網路位元組序的32位整型數轉換為主機位元組序 */
 8 uint32_t ntohl(uint32_t netlong);
 9 /* 將網路位元組序的16位整型數轉換為主機位元組序 */
10 uint16_t ntohs(uint16_t netshort);

地址格式

地址標識特定通訊域中的套接字端點,地址格式和特定通訊域相關。為相容不同格式的地址,地址被強制轉換為通用的地址結構 sockaddr ,Linux系統中該結構定義如下:

1 struct sockaddr {
2     sa_family_t sa_family;      /* 地址型別 */
3     char        sa_data[14];    /* 變長地址 */
4 };

因特網地址定義在 <netinet/in.h> 中。

AF_INET 域中,套接字地址結構如下:

 1 struct in_addr {
 2     in_addr_t       s_addr;         /* IPv4地址 */
 3 };
 4 
 5 struct sockaddr_in {
 6     sa_family_t     sin_family;     /* 地址型別 */
 7     in_port_t       sin_port;       /* 埠號 */
 8     struct in_addr  sin_addr;       /* IPv4地址 */
 9     unsigned char   sin_zero[8];    /* 0填充 */
10 };

AF_INET6 域中,套接字地址結構如下:

 1 struct in6_addr {
 2     uint8_t         s6_addr[16];    /* IPv6地址 */
 3 };
 4 
 5 struct sockaddr_in6 {
 6     sa_family_t     sin6_family;    /* 地址型別 */
 7     in_port_t       sin6_port;      /* 埠號 */
 8     uint32_t        sin6_flowinfo;  /* 傳輸類和流資訊 */
 9     struct in6_addr sin6_addr;      /* IPv6地址 */
10     uint32_t        sin6_scope_id;  /* 作用域的介面集 */
11 };

in_port_t 定義為 uint16_t , in_addr_t 定義為 uint32_t 。

可以用 inet_ntop 和 inet_pton 函式對IPv4和IPv6地址作二進位制和點分十進位制字串之間的轉換。類似的還有inet_addr 和 inet_ntoa 等函式,但它們只能用於IPv4地址。

1 #include <arpa/inet.h>
2 
3 /* 將網路位元組序的二進位制地址轉換為字串格式
4  * @return      成功返回地址字串指標,出錯返回NULL */
5 const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
6 /* 將字串格式轉換為網路位元組序的二進位制地址
7  * @return      成功返回1,格式無效返回0,出錯返回-1 */
8 int inet_pton(int af, const char *src, void *dst);

af 只支援 AF_INET 和 AF_INET6 。

size 指定字串緩衝區 dst 的大小,可用 INET_ADDRSTRLEN 和 INET6_ADDRSTRLEN 來為IPv4和IPv6地址的字串設定足夠大的空間。

地址查詢

使用 gethostent 函式可以找到給定計算機的主機資訊。

 1 #include <netdb.h>
 2 
 3 /* 獲取檔案的下一個hostent結構
 4  * @return      成功返回指向hostent結構的指標,出錯返回NULL */
 5 struct hostent *gethostent(void);
 6 /* gethostent的可重入版本,自設緩衝 */
 7 int gethostent_r(struct hostent *ret, char *buf, size_t buflen, struct hostent **result, int *h_errnop);
 8 /* 回到檔案開頭 */
 9 void sethostent(int stayopen);
10 /* 關閉檔案 */
11 void endhostent(void);
12 
13 /* 通過主機名或地址查詢hostent結構
14  * @return      成功返回指向hostent結構的指標,出錯返回NULL */
15 struct hostent *gethostbyname(const char *name);
16 #include <sys/socket.h>       /* for AF_INET */
17 struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);

結構 hostent 的定義如下:

1 struct hostent {
2     char  *h_name;            /* 主機名 */
3     char **h_aliases;         /* 主機別名列表 */
4     int    h_addrtype;        /* 主機地址型別 */
5     int    h_length;          /* 地址長度 */
6     char **h_addr_list;       /* 地址列表 */
7 };

其中的地址採用網路位元組序。

gethostbyname 和 gethostbyaddr 函式已經過時。

使用 getnetent 函式可以獲取網路名和網路號。

1 #include <netdb.h>
2 
3 struct netent *getnetent(void);
4 void setnetent(int stayopen);
5 void endnetent(void);
6 
7 struct netent *getnetbyname(const char *name);
8 struct netent *getnetbyaddr(uint32_t net, int type);

結構 netent 的定義如下:

1 struct netent {
2     char      *n_name;     /* 網路名 */
3     char     **n_aliases;  /* 網路別名列表 */
4     int        n_addrtype; /* 網路地址型別 */
5     uint32_t   n_net;      /* 網路號 */
6 };

網路號按網路位元組序返回。地址型別為 AF_XX 的地址族常量。

使用 getprotoent 可以獲取協議名字和對應協議號。

1 #include <netdb.h>
2 
3 struct protoent *getprotoent(void);
4 void setprotoent(int stayopen);
5 void endprotoent(void);
6 
7 struct protoent *getprotobyname(const char *name);
8 struct protoent *getprotobynumber(int proto);

結構 protoent 的定義如下:

1 struct protoent {
2     char  *p_name;       /* 協議名 */
3     char **p_aliases;    /* 協議別名列表 */
4     int    p_proto;      /* 協議號 */
5 };

服務由地址的埠號部分表示,每種服務由一個唯一和熟知的埠號提供。使用 getservent 可以獲取服務名字和對應埠號。

1 #include <netdb.h>
2 
3 struct servent *getservent(void);
4 void setservent(int stayopen);
5 void endservent(void);
6 
7 struct servent *getservbyname(const char *name, const char *proto);
8 struct servent *getservbyport(int port, const char *proto);

結構 servent 的定義如下:

1 struct servent {
2     char  *s_name;       /* 服務名 */
3     char **s_aliases;    /* 服務別名列表 */
4     int    s_port;       /* 埠號 */
5     char  *s_proto;      /* 使用的協議 */
6 };

getaddrinfo 函式可以將一個主機名字和服務名字對映到一個地址, getnameinfo 的作用相反,用它們替換舊函式gethostbyname 和 gethostbyaddr 。 freeaddrinfo 用來釋放 addrinfo 結構連結串列。

 1 #include <sys/types.h>
 2 #include <sys/socket.h>
 3 #include <netdb.h>
 4 
 5 /* 獲取地址資訊
 6  * @return      成功返回0,出錯返回非0錯誤碼 */
 7 int getaddrinfo(const char *node, const char *service,
 8                 const struct addrinfo *hints, struct addrinfo **res);
 9 /* 釋放一個或多個addrinfo結構的連結串列 */
10 void freeaddrinfo(struct addrinfo *res);
11 /* 將錯誤碼轉換為錯誤資訊 */
12 const char *gai_strerror(int errcode);
13 
14 /* 獲取主機名或/和服務名
15  * @return      成功返回0,出錯返回非0值 */
16 int getnameinfo(const struct sockaddr *sa, socklen_t salen,
17                 char *host, size_t hostlen,
18                 char *serv, size_t servlen, int flags);

需要提供主機名或/和服務名,主機名可以是節點名或點分十進位制字串表示的主機地址。

結構 addrinfo 的定義如下:

 1 struct addrinfo {
 2     int              ai_flags;      /* 自定義行為 */
 3     int              ai_family;     /* 地址型別 */
 4     int              ai_socktype;   /* 套接字型別 */
 5     int              ai_protocol;   /* 協議 */
 6     size_t           ai_addrlen;    /* 地址長度 */
 7     struct sockaddr *ai_addr;       /* 地址 */
 8     char            *ai_canonname;  /* 主機規範名 */
 9     struct addrinfo *ai_next;       /* 連結串列中的下一個結構 */
10 };

可以用 hints 來過濾得到的結構,它只使用 ai_flags 、 ai_family 、 ai_socktype 、 ai_protocol 欄位,其他欄位必須設為0或 NULL 。

ai_flags 用來指定如何處理地址和名字,可用標誌有:

  • AI_ADDRCONFIG :查詢配置的地址型別(IPv4或IPv6)。
  • AI_ALL :查詢IPv4和IPv6地址,僅用於 AI_V4MAPPED 。
  • AI_CANONNAME :需要一個規範名而不是別名。
  • AI_NUMERICHOST :以數字格式返回主機地址。
  • AI_NUMERICSERV :以埠號返回服務。
  • AI_PASSIVE :套接字地址用於監聽繫結。
  • AI_V4MAPPED :如果沒找到IPv6地址,返回對映到IPv6格式的IPv4地址。

flags 引數指定一些轉換的控制方式,有:

  • NI_DGRAM :服務基於資料包而非基於流。
  • NI_NAMEREQD :如果找不到主機名,將其視作錯誤。
  • NI_NOFQDN :對於本地主機,僅返回完全限定域名的節點名部分。
  • NI_NUMERICHOST :以數字形式返回主機地址。
  • NI_NUMERICSERV :以埠號返回服務地址。

繫結地址

對於伺服器,需要給接收客戶端請求的套接字繫結一個眾所周知的地址。客戶端則讓系統選擇一個預設地址即可。

可以用 bind 函式將地址繫結到一個套接字。

1 #include <sys/socket.h>
2 
3 /* 將地址繫結到套接字
4  * @return      成功返回0,出錯返回-1 */
5 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

對於地址,要求:

  • 地址必須有效,不能指定其他機器的地址。
  • 地址必須和套接字的地址族所支援的格式匹配。
  • 如果不是超級使用者,埠號不能小於1024。
  • 一般只有套接字端點能夠和地址繫結。

對於因特網域,如果指定IP地址為 INADDR_ANY ,套接字端點可以被繫結到所有的系統網路介面,這樣它可以收到這個系統的所有網路卡的資料包。

getsockname 函式可以獲取繫結到套接字的地址。

1 #include <sys/socket.h>
2 
3 /* 獲取繫結到套接字的地址
4  * @return      成功返回0,出錯返回-1 */
5 int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

addrlen 指定 addr 的緩衝區大小,返回時會被設定為返回地址的大小,如果地址過大,則會被截斷而不報錯。

套接字已經和對方連線時,可以用 getpeername 函式獲取對方的地址。

1 #include <sys/socket.h>
2 
3 /* 獲取繫結到套接字的地址
4  * @return      成功返回0,出錯返回-1 */
5 int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

建立連線

面向連線的網路服務在交換資料前需要首先在請求服務的套接字(客戶端)和提供服務的套接字(伺服器)之間建立連線。可以用 connect 函式建立連線。

1 #include <sys/socket.h>
2 
3 /* 建立連線
4  * @return      成功返回0,出錯返回-1 */
5 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

addr 為伺服器的地址。如果 sockfd 還沒有繫結地址,函式會給它繫結一個預設地址。

由於伺服器的負載變化等問題, connect 可能會返回錯誤,通常會選擇重連,如下面的指數補償演算法。

 1 #include <unistd.h>
 2 #include <sys/socket.h>
 3 
 4 #define MAXSLEEP 128
 5 
 6 int connect_retry(int sockfd, const struct sockaddr *addr, socklen_t alen)
 7 {
 8     int nsec;
 9 
10     /* Try to connect with exponential backoff. */
11     for (nsec = 1; nsec <= MAXSLEEP; nsec <<= 1) {
12         if (connect(sockfd, addr, alen) == 0) {
13             /* Connection accepted. */
14             return(0);
15         }
16         /* Delay before trying again. */
17         if (nsec <= MAXSLEEP/2)
18             sleep(nsec);
19     }
20     return(-1);
21 }

connect 也可用於無連線網路服務。所有傳送報文的目標地址被設為 addr ,並且只能接收來自 addr 的報文。

伺服器使用 listen 函式來說明可以接受連線請求。

1 #include <sys/socket.h>
2 
3 /* 說明可以接受連線
4  * @return      成功返回0,出錯返回-1 */
5 int listen(int sockfd, int backlog);

backlog 建議連線請求的佇列大小,實際值由系統決定,上限為 SOMAXCONN ,該值為128。佇列滿後,系統會拒絕多餘的連線請求。

之後用 accept 函式獲得連線請求並建立連線。

1 #include <sys/socket.h>
2 
3 /* 獲得連線請求,建立連線
4  * @return      成功返回檔案描述符,出錯返回-1 */
5 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

accept 會將客戶端地址設定到 addr 指向的緩衝區並更新 addrlen 指向的值,若不關心客戶端可將它們設為 NULL 。

函式返回連線到呼叫 connect 的客戶端的套接字描述符,它和 sockfd 有相同的套接字型別和地址族, sockfd 會被保持可用狀態接受其他連線請求。

如果沒有連線請求, accept 會阻塞直到有請求到來,如果 sockfd 為非阻塞模式,則返回-1,並設 errno 為 EAGAIN或 EWOULDBLOCK (兩者等價)。伺服器也可以用 poll 或 select 來等待請求,請求套接字會被作為可讀的。

資料傳輸

首先,因為套接字端點用檔案描述符表示,所以可以用 read 和 write 函式來交換資料,這會帶來相容性上的好處。但如果想獲得更多的功能,需要使用 send 和 recv 函式族。

send 函式族用於傳送資料。

 1 #include <sys/types.h>
 2 #include <sys/socket.h>
 3 
 4 /* 傳送資料
 5  * @return      成功返回傳送的位元組數,出錯返回-1 */
 6 ssize_t send(int sockfd, const void *buf, size_t len, int flags);
 7 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
 8         const struct sockaddr *dest_addr, socklen_t addrlen);
 9 ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

send 函式類似 write ,但它支援額外的引數 flags ,可用標誌有:

  • MSG_DONTROUTE :勿將資料路由出本地網路。
  • MSG_DONTWAIT :允許非阻塞操作,等價於用 O_NONBLOCK 。
  • MSG_EOR :如果協議支援,此為記錄結束。
  • MSG_OOB :如果協議支援,傳送帶外資料。

send 成功返回只代表資料已經正確傳送到網路上,不表示連線另一端的程式接收資料。對於支援為報文設限的協議,如果報文大小超過協議支援的最大值, send 失敗並設 errno 為 EMSGSIZE 。對於位元組流協議, send 會阻塞直到整個資料被傳輸。

sendto 和 send 的區別是它支援在無連線的套接字上指定目標地址。無連線的套接字如果在呼叫 connect 時沒有設定目標地址,則不能用 send 。

sendmsg 類似於 writev ,可以指定多重緩衝區來傳輸資料。結構 msghdr 的定義如下:

 1 struct msghdr {
 2     void         *msg_name;       /* 可選地址 */
 3     socklen_t     msg_namelen;    /* 地址大小 */
 4     struct iovec *msg_iov;        /* I/O緩衝區陣列 */
 5     size_t        msg_iovlen;     /* 陣列中元素數 */
 6     void         *msg_control;    /* 輔助資料 */
 7     size_t        msg_controllen; /* 輔助資料大小 */
 8     int           msg_flags;      /* 接收到的資料的標誌 */
 9 };

相應地, recv 函式族用於接收資料。

 1 #include <sys/types.h>
 2 #include <sys/socket.h>
 3 
 4 /* 接收資料
 5  * @return      成功返回接收的訊息位元組數,無可用訊息或對方已按序結束返回0,出錯返回-1 */
 6 ssize_t recv(int sockfd, void *buf, size_t len, int flags);
 7 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
 8         struct sockaddr *src_addr, socklen_t *addrlen);
 9 ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

recv 函式類似 read ,但同樣它支援額外的引數 flags ,可用標誌有:

  • MSG_WAITALL :等待直到所有資料可用,只對 SOCK_STREAM 有效。
  • MSG_TRUNC :返回報文的實際長度,即使被截斷。
  • MSG_PEEK :返回報文內容但不取走報文。
  • MSG_OOB :如果協議支援,接收帶外資料。

SOCK_DGRAM 和 SOCK_SEQPACKET 套接字型別一次讀取就返回整個報文,所以 MSG_WAITALL 對它們沒有作用。

如果傳送者用 shutdown 結束傳輸,或網路協議支援順序關閉且傳送端已關閉,則資料接收完之後 recv 返回0。

recvfrom 可以得到傳送者的源地址,通常用於無連線套接字。

recvmsg 類似於 readv ,可以將接收到的資料放入多個緩衝區,也可用於想接收輔助資料的情況。 msghdr 結構前面已經說明,從 recvmsg 返回時會設定 msg_flags 欄位來表示接收的資料的特性,可能的值有:

  • MSG_DONTWAIT : recvmsg 處於非阻塞模式。
  • MSG_TRUNC :一般資料被截斷。
  • MSG_CTRUNC :控制資料被截斷。
  • MSG_EOR :接收到記錄結束符。
  • MSG_OOB :接收到帶外資料。

套接字選項

有三種型別的套接字選項:

  1. 通用選項,適用於所有套接字型別。
  2. 特定套接字的選項,會依賴於下層協議。
  3. 特定協議的選項。

用 getsockopt 和 setsockopt 函式獲取和設定套接字選項。

1 #include <sys/socket.h>
2 
3 /* 獲取和設定套接字選項
4  * @return      成功返回0,出錯返回-1 */
5 int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
6 int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

引數說明:

level
指定選項應用的協議。如果選項是通用的套接字選項,設為 SOL_SOCKET ,否則設為控制該選項的協議號。對TCP選項設為 IPPROTO_TCP ,對UDP選項設為 IPPROTO_UDP ,對IP選項設為 IPPROTO_IP 。
optname

指定套接字選項。通用套接字選項包括:

選項 引數 optval 型別 說明
SO_ACCEPTCONN int 返回資訊指示該套接字是否能監聽,僅 getsockopt
SO_BROADCAST int 如果 *optval 非0,廣播資料包
SO_DEBUG int 如果 *optval 非0,啟動網路驅動除錯功能
SO_DONTROUTE int 如果 *optval 非0,繞過通常路由
SO_ERROR int 返回掛起的套接字錯誤並清除,僅 getsockopt
SO_KEEPALIVE int 如果 *optval 非0,啟動週期性keep-alive訊息
SO_LINGER struct linger 當有未傳送訊息並且套接字關閉時,延遲時間
SO_OOBINLINE int 如果 *optval 非0,將帶外資料放到普通資料中
SO_RCVBUF int 接收緩衝區的位元組大小
SO_RCVLOWAT int 接收呼叫中返回的資料最小位元組數
SO_RCVTIMEO struct timeval 套接字接收呼叫的超時值
SO_REUSEADDR int 如果 *optval 非0,重用 bind 中的地址
SO_SNDBUF int 傳送緩衝區的位元組大小
SO_SNDLOWAT int 傳送呼叫中傳送的資料最小位元組數
SO_SNDTIMEO struct timeval 套接字傳送呼叫的超時值
SO_TYPE int 標識套接字型別,僅 getsockopt
optval
根據選項的不同指向一個資料結構或整數。

通常TCP不允許繫結同一個地址,可以用 SO_REUSEADDR 來越過這個限制。

 1 #include <errno.h>
 2 #include <sys/socket.h>
 3 
 4 int initserver(int type, const struct sockaddr *addr, socklen_t alen, int qlen)
 5 {
 6     int fd, err;
 7     int reuse = 1;
 8 
 9     if ((fd = socket(addr->sa_family, type, 0)) < 0)
10         return(-1);
11     if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int)) < 0) {
12         err = errno;
13         goto errout;
14     }
15     if (bind(fd, addr, alen) < 0) {
16         err = errno;
17         goto errout;
18     }
19     if (type == SOCK_STREAM || type == SOCK_SEQPACKET) {
20         if (listen(fd, qlen) < 0) {
21             err = errno;
22             goto errout;
23         }
24     }
25     return(fd);
26 errout:
27     close(fd);
28     errno = err;
29     return(-1);
30 }

帶外資料

帶外資料是一些通訊協議支援的可選特性,允許更高優先順序的資料比普通資料優先傳輸。TCP支援帶外資料,UDP不支援。

在TCP中帶外資料稱為緊急資料,TCP只支援1位元組的緊急資料,允許緊急資料在普通資料資料流之外傳輸。在 send函式族中指定 MSG_OOB 標誌,就會產生緊急資料,取最後1個位元組。

用 SO_OOBINLINE 套接字選項可以在普通資料中接收緊急資料,可以在TCP的普通資料流中檢視緊急資料的位置,即緊急標記。

1 #include <sys/socket.h>
2 
3 /* 下一個要讀的位元組在標誌處返回1,沒在標誌處返回0,出錯返回-1 */
4 int sockatmark(int sockfd);

在讀取佇列出現帶外資料時, select 函式返回檔案描述符並異常掛起。可在普通資料流或用加 MSG_OOB 標誌的 recv函式族接收緊急資料,後面的緊急資料會覆蓋前面的。

UNIX域套接字

UNIX域套接字用於同一臺機器上的程式間通訊。因特網域套接字也可以實現但UNIX域套接字效率更高,後者只複製資料而不處理協議和其他和網路相關的工作。

UNIX域套接字有流和資料包兩種介面,它的資料包服務是可靠的。

可以用 socketpair 函式建立一對非命名的、相互連線的UNIX域套接字。

1 #include <sys/socket.h>
2 
3 /* 建立一對無名的相互連線的UNIX域套接字
4  * @return      成功返回0,出錯返回-1 */
5 int socketpair(int domain, int type, int protocol, int sv[2]);

例:

1 #include <sys/socket.h>
2 
3 /* Returns a full-duplex "stream" pipe (a UNIX domain socket) with the two file descriptors returned in fd[0] and fd[1]. */
4 int s_pipe(int fd[2])
5 {
6     return(socketpair(AF_UNIX, SOCK_STREAM, 0, fd));
7 }

也可以用 socket 建立命名的UNIX域套接字,用標準的 bind 、 listen 、 accept 、 connect 進行處理。

UNIX域套接字的地址定義在 <sys/un.h> 中,由 sockaddr_un 結構表示:

1 struct sockaddr_un {
2     sa_family_t sun_family;     /* AF_UNIX */
3     char        sun_path[108];  /* 路徑名 */
4 };

可以用 offsetof 函式獲取 sun_path 的偏移量再加上 sun_path 的長度得到繫結地址的長度。

將地址繫結到UNIX域套接字時,系統會用該路徑名建立一個 S_IFSOCK 型別的檔案。它只用於向客戶程式告訴套接字名字,不能開啟,也不能由應用程式用於通訊。如果檔案已經存在,則 bind 會失敗。關閉套接字時,不會自動刪除檔案,因此還需要在程式中手動刪除。

使用套接字的示例

面向連線的ruptime

客戶端:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <unistd.h>
 4 #include <netdb.h>
 5 #include <errno.h>
 6 #include <sys/socket.h>
 7 #include "error.h"
 8 
 9 #define MAXADDRLEN  256
10 #define BUFLEN      128
11 
12 extern int connect_retry(int, const struct sockaddr *, socklen_t);
13 
14 void print_uptime(int sockfd)
15 {
16     int     n;
17     char    buf[BUFLEN];
18 
19     while ((n = recv(sockfd, buf, BUFLEN, 0)) > 0)
20         write(STDOUT_FILENO, buf, n);
21     if (n < 0)
22         err_sys("recv error");
23 }
24 
25 int main(int argc, char *argv[])
26 {
27     struct addrinfo *ailist, *aip;
28     struct addrinfo hint;
29     int             sockfd, err;
30 
31     if (argc != 2)
32         err_quit("usage: ruptime hostname");
33     hint.ai_flags = 0;
34     hint.ai_family = 0;
35     hint.ai_socktype = SOCK_STREAM;
36     hint.ai_protocol = 0;
37     hint.ai_addrlen = 0;
38     hint.ai_canonname = NULL;
39     hint.ai_addr = NULL;
40     hint.ai_next = NULL;
41     if ((err = getaddrinfo(argv[1], "ruptime", &hint, &ailist)) != 0)
42         err_quit("getaddrinfo error: %s", gai_strerror(err));
43     for (aip = ailist; aip != NULL; aip = aip->ai_next) {
44         if ((sockfd = socket(aip->ai_family, SOCK_STREAM, 0)) < 0)
45             err = errno;
46         if (connect_retry(sockfd, aip->ai_addr, aip->ai_addrlen) < 0) {
47             err = errno;
48         } else {
49             print_uptime(sockfd);
50             exit(0);
51         }
52     }
53     fprintf(stderr, "can't connect to %s: %s\n", argv[1], strerror(err));
54     exit(1);
55 }

伺服器( popen 版本):

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <netdb.h>
 4 #include <errno.h>
 5 #include <syslog.h>
 6 #include <sys/socket.h>
 7 #include "error.h"
 8 
 9 #define BUFLEN  128
10 #define QLEN 10
11 
12 #ifndef HOST_NAME_MAX
13 #define HOST_NAME_MAX 256
14 #endif
15 
16 extern int initserver(int, struct sockaddr *, socklen_t, int);
17 
18 void serve(int sockfd)
19 {
20     int     clfd;
21     FILE    *fp;
22     char    buf[BUFLEN];
23 
24     for (;;) {
25         clfd = accept(sockfd, NULL, NULL);
26         if (clfd < 0) {
27             syslog(LOG_ERR, "ruptimed: accept error: %s", strerror(errno));
28             exit(1);
29         }
30         if ((fp = popen("/usr/bin/uptime", "r")) == NULL) {
31             sprintf(buf, "error: %s\n", strerror(errno));
32             send(clfd, buf, strlen(buf), 0);
33         } else {
34             while (fgets(buf, BUFLEN, fp) != NULL)
35                 send(clfd, buf, strlen(buf), 0);
36             pclose(fp);
37         }
38         close(clfd);
39     }
40 }
41 
42 int main(int argc, char *argv[])
43 {
44     struct addrinfo *ailist, *aip;
45     struct addrinfo hint;
46     int             sockfd, err, n;
47     char            *host;
48 
49     if (argc != 1)
50         err_quit("usage: ruptimed");
51 #ifdef _SC_HOST_NAME_MAX
52     n = sysconf(_SC_HOST_NAME_MAX);
53     if (n < 0)  /* best guess */
54 #endif
55         n = HOST_NAME_MAX;
56     host = malloc(n);
57     if (host == NULL)
58         err_sys("malloc error");
59     if (gethostname(host, n) < 0)
60         err_sys("gethostname error");
61     daemonize("ruptimed");
62     hint.ai_flags = AI_CANONNAME;
63     hint.ai_family = 0;
64     hint.ai_socktype = SOCK_STREAM;
65     hint.ai_protocol = 0;
66     hint.ai_addrlen = 0;
67     hint.ai_canonname = NULL;
68     hint.ai_addr = NULL;
69     hint.ai_next = NULL;
70     if ((err = getaddrinfo(host, "ruptime", &hint, &ailist)) != 0) {
71         syslog(LOG_ERR, "ruptimed: getaddrinfo error: %s", gai_strerror(err));
72         exit(1);
73     }
74     for (aip = ailist; aip != NULL; aip = aip->ai_next) {
75         if ((sockfd = initserver(SOCK_STREAM, aip->ai_addr,
76           aip->ai_addrlen, QLEN)) >= 0) {
77             serve(sockfd);
78             exit(0);
79         }
80     }
81     exit(1);
82 }

伺服器( fork 版本):

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <netdb.h>
 4 #include <errno.h>
 5 #include <syslog.h>
 6 #include <fcntl.h>
 7 #include <sys/socket.h>
 8 #include <sys/wait.h>
 9 #include "error.h"
10 
11 #define QLEN 10
12 
13 #ifndef HOST_NAME_MAX
14 #define HOST_NAME_MAX 256
15 #endif
16 
17 extern int initserver(int, struct sockaddr *, socklen_t, int);
18 
19 void serve(int sockfd)
20 {
21     int     clfd, status;
22     pid_t   pid;
23 
24     for (;;) {
25         clfd = accept(sockfd, NULL, NULL);
26         if (clfd < 0) {
27             syslog(LOG_ERR, "ruptimed: accept error: %s", strerror(errno));
28             exit(1);
29         }
30         if ((pid = fork()) < 0) {
31             syslog(LOG_ERR, "ruptimed: fork error: %s", strerror(errno));
32             exit(1);
33         } else if (pid == 0) {  /* child */
34             /*
35              * The parent called daemonize ({Prog daemoninit}), so
36              * STDIN_FILENO, STDOUT_FILENO, and STDERR_FILENO
37              * are already open to /dev/null.  Thus, the call to
38              * close doesn't need to be protected by checks that
39              * clfd isn't already equal to one of these values.
40              */
41             if (dup2(clfd, STDOUT_FILENO) != STDOUT_FILENO ||
42               dup2(clfd, STDERR_FILENO) != STDERR_FILENO) {
43                 syslog(LOG_ERR, "ruptimed: unexpected error");
44                 exit(1);
45             }
46             close(clfd);
47             execl("/usr/bin/uptime", "uptime", (char *)0);
48             syslog(LOG_ERR, "ruptimed: unexpected return from exec: %s",
49               strerror(errno));
50         } else {        /* parent */
51             close(clfd);
52             waitpid(pid, &status, 0);
53         }
54     }
55 }
56 
57 int main(int argc, char *argv[])
58 {
59     struct addrinfo *ailist, *aip;
60     struct addrinfo hint;
61     int             sockfd, err, n;
62     char            *host;
63 
64     if (argc != 1)
65         err_quit("usage: ruptimed");
66 #ifdef _SC_HOST_NAME_MAX
67     n = sysconf(_SC_HOST_NAME_MAX);
68     if (n < 0)  /* best guess */
69 #endif
70         n = HOST_NAME_MAX;
71     host = malloc(n);
72     if (host == NULL)
73         err_sys("malloc error");
74     if (gethostname(host, n) < 0)
75         err_sys("gethostname error");
76     daemonize("ruptimed");
77     hint.ai_flags = AI_CANONNAME;
78     hint.ai_family = 0;
79     hint.ai_socktype = SOCK_STREAM;
80     hint.ai_protocol = 0;
81     hint.ai_addrlen = 0;
82     hint.ai_canonname = NULL;
83     hint.ai_addr = NULL;
84     hint.ai_next = NULL;
85     if ((err = getaddrinfo(host, "ruptime", &hint, &ailist)) != 0) {
86         syslog(LOG_ERR, "ruptimed: getaddrinfo error: %s", gai_strerror(err));
87         exit(1);
88     }
89     for (aip = ailist; aip != NULL; aip = aip->ai_next) {
90         if ((sockfd = initserver(SOCK_STREAM, aip->ai_addr,
91           aip->ai_addrlen, QLEN)) >= 0) {
92             serve(sockfd);
93             exit(0);
94         }
95     }
96     exit(1);
97 }

無連線的ruptime

客戶端:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <netdb.h>
 4 #include <errno.h>
 5 #include <sys/socket.h>
 6 #include "error.h"
 7 
 8 #define BUFLEN      128
 9 #define TIMEOUT     20
10 
11 void sigalrm(int signo) {}
12 
13 void print_uptime(int sockfd, struct addrinfo *aip)
14 {
15     int     n;
16     char    buf[BUFLEN];
17 
18     buf[0] = 0;
19     if (sendto(sockfd, buf, 1, 0, aip->ai_addr, aip->ai_addrlen) < 0)
20         err_sys("sendto error");
21     alarm(TIMEOUT);
22     if ((n = recvfrom(sockfd, buf, BUFLEN, 0, NULL, NULL)) < 0) {
23         if (errno != EINTR)
24             alarm(0);
25         err_sys("recv error");
26     }
27     alarm(0);
28     write(STDOUT_FILENO, buf, n);
29 }
30 
31 int main(int argc, char *argv[])
32 {
33     struct addrinfo     *ailist, *aip;
34     struct addrinfo     hint;
35     int                 sockfd, err;
36     struct sigaction    sa;
37 
38     if (argc != 2)
39         err_quit("usage: ruptime hostname");
40     sa.sa_handler = sigalrm;
41     sa.sa_flags = 0;
42     sigemptyset(&sa.sa_mask);
43     if (sigaction(SIGALRM, &sa, NULL) < 0)
44         err_sys("sigaction error");
45     hint.ai_flags = 0;
46     hint.ai_family = 0;
47     hint.ai_socktype = SOCK_DGRAM;
48     hint.ai_protocol = 0;
49     hint.ai_addrlen = 0;
50     hint.ai_canonname = NULL;
51     hint.ai_addr = NULL;
52     hint.ai_next = NULL;
53     if ((err = getaddrinfo(argv[1], "ruptime", &hint, &ailist)) != 0)
54         err_quit("getaddrinfo error: %s", gai_strerror(err));
55     for (aip = ailist; aip != NULL; aip = aip->ai_next) {
56         if ((sockfd = socket(aip->ai_family, SOCK_DGRAM, 0)) < 0) {
57             err = errno;
58         } else {
59             print_uptime(sockfd, aip);
60             exit(0);
61         }
62     }
63     fprintf(stderr, "can't contact %s: %s\n", argv[1], strerror(err));
64     exit(1);
65 }

伺服器:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <netdb.h>
 4 #include <errno.h>
 5 #include <syslog.h>
 6 #include <sys/socket.h>
 7 #include "error.h"
 8 
 9 #define BUFLEN      128
10 #define MAXADDRLEN  256
11 
12 #ifndef HOST_NAME_MAX
13 #define HOST_NAME_MAX 256
14 #endif
15 
16 extern int initserver(int, struct sockaddr *, socklen_t, int);
17 
18 void serve(int sockfd)
19 {
20     int         n;
21     socklen_t   alen;
22     FILE        *fp;
23     char        buf[BUFLEN];
24     char        abuf[MAXADDRLEN];
25 
26     for (;;) {
27         alen = MAXADDRLEN;
28         if ((n = recvfrom(sockfd, buf, BUFLEN, 0,
29           (struct sockaddr *)abuf, &alen)) < 0) {
30             syslog(LOG_ERR, "ruptimed: recvfrom error: %s", strerror(errno));
31             exit(1);
32         }
33         if ((fp = popen("/usr/bin/uptime", "r")) == NULL) {
34             sprintf(buf, "error: %s\n", strerror(errno));
35             sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)abuf, alen);
36         } else {
37             if (fgets(buf, BUFLEN, fp) != NULL)
38                 sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)abuf, alen);
39             pclose(fp);
40         }
41     }
42 }
43 
44 int main(int argc, char *argv[])
45 {
46     struct addrinfo *ailist, *aip;
47     struct addrinfo hint;
48     int             sockfd, err, n;
49     char            *host;
50 
51     if (argc != 1)

相關文章