Linux 系統應用程式設計——網路程式設計(高階篇)
一、網路超時檢測
在網路通訊過程中,經常會出現不可預知的各種情況。例如網路線路突發故障、通訊一方異常結束等。一旦出現上述情況,很可能長時間都不會收到資料,而且無法判斷是沒有資料還是資料無法到達。如果使用的是TCP協議,可以檢測出來;但如果使用UDP協議的話,需要在程式中進行相關檢測。所以,為避免程序在沒有資料時無限制的阻塞,使用網路超時檢測很有必要。
1、套接字接收超時檢測
這裡先介紹設定套接字選項的函式 setsockopt() 函式:
所需標頭檔案 | #include <sys/types.h> #include <sys/socket.h> |
函式原型 | int setsockopt (int sockfd, int level, int optname, const void *optval, socklen_t optlen ); |
函式引數 | sockfd:套接字描述符 level:選項所屬協議層 optval:儲存選項值的緩衝區 optlen:選項值的長度 |
函式返回值 | 成功:0 出錯:-1,並設定 errno |
下面是套接字常用選項及其說明:
LEVEL:SOL_SOCKET
選項名稱 | 說明 | 資料型別 |
SO_BROADCAST | 允許傳送廣播資料 | int |
SO_DEBUG | 允許除錯 | int |
SO_DONTRUOTE | 不查詢路由 | int |
SO_ERROR | 獲得套接字錯誤 | int |
SO_KEEPALIVE | 保持連線 | int |
SO_LINGER | 延遲關閉連線 | struct linger |
SO_OOBINLINE | 帶外資料放入正常資料流 | int |
SO_RCVBUF | 接收緩衝區大小 | int |
SO_SNDBUF | 傳送緩衝區大小 | int |
SO_RCVTIMEO | 接收超時 | struct timeval |
SO_SNDTIMEO | 傳送超時 | struct timeval |
SO_REUSERADDR | 允許重用本地地址和埠 | int |
SO_TYPE | 獲得套接字型別 | int |
下面利用SO_RCVTIMEO的選項實現套接字的接收超時檢測:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define N 64 #define PORT 8888 int main() { int sockfd; char buf[N]; struct sockaddr_in seraddr; struct timeval t = {6, 0}; if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket error"); exit(-1); } else { printf("socket successfully!\n"); printf("sockfd:%d\n",sockfd); } memset(&seraddr, 0, sizeof(seraddr)); seraddr.sin_family = AF_INET; seraddr.sin_port = htons(PORT); seraddr.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr)) == -1) { perror("bind error"); exit(-1); } else { printf("bind successfully!\n"); printf("PORT:%d\n",PORT); } if(setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &t, sizeof(t)) < 0) { perror("setsockopt error"); exit(-1); } if(recvfrom(sockfd, buf, N, 0, NULL, NULL) < 0) { perror("fail to recvfrom"); exit(-1); } else { printf("recv data: %s\n",buf); } return 0; }
執行結果如下:
[email protected]untu:~/qiang/socket/time$ ./setsockopt
socket successfully!
sockfd:3
bind successfully!
PORT:8888
fail to recvfrom: Resource temporarily unavailable
[email protected]:~/qiang/socket/time$
可以看到,6s之內沒有資料包到來,程式會從 recvfrom 函式返回,進行相應的錯誤處理。
注意:套接字一旦設定了超時之後,每一次傳送或接收時都會檢測,如果要取消超時檢測,重新用setsockopt函式設定即可(把時間值指定為 0)。
2、定時器超時檢測
這裡利用定時器訊號SIGALARM,可以在程式中建立一個鬧鐘。當到達目標時間後,指定的訊號處理函式被執行。這樣同樣可以利用SIGALARM訊號實現檢測,下面分別介紹相關資料型別和函式。
struct sigaction 是 Linux 中用來描述訊號行為的結構體型別,其定義如下:
struct sigaction
{
void (*sa_handler) (int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer) (void);
}
① sa_handler:此引數和signal()的引數handler相同,此引數主要用來對訊號舊的安裝函式signal()處理形式的支援;
② sa_sigaction:新的訊號安裝機制,處理函式被呼叫的時候,不但可以得到訊號編號,而且可以獲悉被呼叫的原因以及產生問題的上下文的相關資訊。
③ sa_mask:用來設定在處理該訊號時暫時將sa_mask指定的訊號擱置;
④ sa_restorer: 此引數沒有使用;
⑤ sa_flags:用來設定訊號處理的其他相關操作,下列的數值可用。可用OR 運算(|)組合:
ŸA_NOCLDSTOP:如果引數signum為SIGCHLD,則當子程序暫停時並不會通知父程序
SA_ONESHOT/SA_RESETHAND:當呼叫新的訊號處理函式前,將此訊號處理方式改為系統預設的方式
SA_RESTART:被訊號中斷的系統呼叫會自行重啟
SA_NOMASK/SA_NODEFER:在處理此訊號未結束前不理會此訊號的再次到來
SA_SIGINFO:訊號處理函式是帶有三個引數的sa_sigaction。
所需標頭檔案 | #include <signal.h> |
函式原型 | int sigaction(int signum, const struct sigaction *act , struct sigaction *oldact ); |
函式傳入值 | signum:可以指定SIGKILL和SIGSTOP以外的所有訊號 act :act 是一個結構體,裡面包含訊號處理函式的地址、 處理方式等資訊; oldact :引數oldact 是一個傳出引數,sigaction 函式呼叫成功後, oldact 裡面包含以前對 signum 訊號的處理方式的資訊; |
函式返回值 | 成功:0 出錯:-1 |
使用定時器訊號檢測超時的示例程式碼如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define N 64
#define PORT 8888
void handler(int signo)
{
printf("interrupted by SIGALRM\n");
}
int main()
{
int sockfd;
char buf[N];
struct sockaddr_in seraddr;
struct sigaction act;
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
perror("socket error");
exit(-1);
}
else
{
printf("socket successfully!\n");
printf("sockfd:%d\n",sockfd);
}
memset(&seraddr, 0, sizeof(seraddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(PORT);
seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr)) == -1)
{
perror("bind error");
exit(-1);
}
else
{
printf("bind successfully!\n");
printf("PORT:%d\n",PORT);
}
sigaction(SIGALRM, NULL, &act);
act.sa_handler = handler;
act.sa_flags &= ~SA_RESTART;
sigaction(SIGALRM, &act, NULL);
alarm(6);
if(recvfrom(sockfd, buf, N, 0, NULL, NULL) < 0)
{
perror("fail to recvfrom");
exit(-1);
}
printf("recv data: %s\n",buf);
alarm(0);
return 0;
}
執行結果如下:
[email protected]:~/qiang/socket/time$ ./alarm
socket successfully!
sockfd:3
bind successfully!
PORT:8888
interrupted by SIGALRM
fail to recvfrom: Interrupted system call
[email protected]:~/qiang/socket/time$
二、廣播
前面的網路通訊中,採用的都是單播(唯一的傳送方和接收方)的方式。很多時候,需要把資料同時傳送給區域網中的所有主機。例如,通過廣播ARP包獲取目標主機的MAC地址。
1、廣播地址
IP地址用來標識網路中的一臺主機。IPv4 協議用一個 32 位的無符號數表示網路地址,包括網路號和主機號。子網掩碼錶示 IP 地址中網路和佔幾個位元組。對於一個 C類地址來說,子網掩碼為 255.255.255.0。
每個網段都有其對應的廣播地址。以 C 類地址網段 192.168.1.x為例,其中最小的地址 192.168.1.0 代表該網段;而最大的地址192.168.1.255 則是該網段中的廣播地址。當我們向這個地址傳送資料包時,該網段中所以的主機都會接收並處理。
注意:傳送廣播包時,目標IP 為廣播地址而目標 MAC 是 ff:ff:ff:ff:ff。
2、廣播包的傳送和接收
廣播包的傳送和接收通過UDP套接字實現。
1)廣播包傳送流程如下:
建立udp 套接字
指定目標地址和埠
設定套接字選項允許傳送廣播包
傳送資料包
傳送廣播包的示例如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define N 64
#define PORT 8888
int main()
{
int sockfd;
int on = 1;
char buf[N] = "This is a broadcast package!";
struct sockaddr_in dstaddr;
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
perror("socket error");
exit(-1);
}
else
{
printf("socket successfully!\n");
printf("sockfd:%d\n",sockfd);
}
memset(&dstaddr, 0, sizeof(dstaddr));
dstaddr.sin_family = AF_INET;
dstaddr.sin_port = htons(PORT);
dstaddr.sin_addr.s_addr = inet_addr("192.168.1.255"); // 192.168.1.x 網段的廣播地址
if(setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)) < 0) //套接字預設不允許傳送廣播包,通過修改 SO_BROADCAST 選項使能
{
perror("setsockopt error");
exit(-1);
}
while(1)
{
sendto(sockfd, buf, N, 0,(struct sockaddr *)&dstaddr, sizeof(dstaddr));
sleep(1);
}
return 0;
}
2)、廣播包接收流程
廣播包接收流程如下:
建立UDP套接字
繫結地址
接收資料包
接收包示例如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define N 64
#define PORT 8888
int main()
{
int sockfd;
char buf[N];
struct sockaddr_in seraddr;
socklen_t peerlen = sizeof(seraddr);
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
perror("socket error");
exit(-1);
}
else
{
printf("socket successfully!\n");
printf("sockfd:%d\n",sockfd);
}
memset(&seraddr, 0, sizeof(seraddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(PORT);
seraddr.sin_addr.s_addr = inet_addr("192.168.1.255"); //接收方繫結廣播地址
if(bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr)) == -1)
{
perror("bind error");
exit(-1);
}
else
{
printf("bind successfully!\n");
printf("PORT:%d\n",PORT);
}
while(1)
{
if(recvfrom(sockfd, buf, N, 0, (struct sockaddr *)&seraddr, &peerlen) < 0)
{
perror("fail to recvfrom");
exit(-1);
}
else
{
printf("[%s:%d]",inet_ntoa(seraddr.sin_addr),ntohs(seraddr.sin_port));
printf("%s\n",buf);
}
}
return 0;
}
執行結果如下
[email protected]:~/qiang/socket/guangbo$ ./guangbore
socket successfully!
sockfd:3
bind successfully!
PORT:8888
[127.0.0.1:56195]This is a broadcast package!
[127.0.0.1:56195]This is a broadcast package!
[127.0.0.1:56195]This is a broadcast package!
[127.0.0.1:56195]This is a broadcast package!
[127.0.0.1:56195]This is a broadcast package!
[127.0.0.1:56195]This is a broadcast package!
[127.0.0.1:56195]This is a broadcast package!
[127.0.0.1:56195]This is a broadcast package!
[127.0.0.1:56195]This is a broadcast package!
[127.0.0.1:56195]This is a broadcast package!
[127.0.0.1:56195]This is a broadcast package!
...
3、組播
通過廣播可以很方便地實現傳送資料包給區域網中的所有主機。但廣播同樣存在一些問題,例如,頻繁地傳送廣播包造成所以主機資料鏈路層都會接收並交給上層 協議處理,也容易引起區域網的網路風暴。
下面介紹一種資料包傳送方式成為組播或多播。組播可以看成是單播和廣播的這種。當傳送組播資料包時,至於加入指定多播組的主機資料鏈路層才會處理,其他主機在資料鏈路層會直接丟掉收到的資料包。換句話說,我們可以通過組播的方式和指定的若干主機通訊。
1、組播地址
IPv4 地址分為以下5類。
A類地址:最高位為0,主機號佔24位,地址範圍從 1.0.0.1到 126.255.255.254。
B類地址:最高兩位為10,主機號佔16位,地址範圍從 128.0.0.1 到 191.254.255.254。
C類地址:最高3位為110,主機號佔8位,地址範圍從 192.0.1.1 到 223.255.254.254。
D類地址:最高4位為1110,地址範圍從192.0.1.1到 223.255.254.254。
E類地址保留。
其中D類地址唄成為組播地址。每一個組播地址代表一個多播組。
2、組播包的傳送和接收
組播包的傳送和接收也通過UDP套接字實現。
1))組播發送流程如下:
建立UDP套接字
指定目標地址和埠
傳送資料包
程式中,緊接著bind有一個setsockopt操作,它的作用是將socket加入一個組播組,因為socket要接收組播地址224.0.0.1的資料,它就必須加入該組播組。
結構體struct ip_mreq mreq是該操作的引數,下面是其定義:
struct ip_mreq
{
struct in_addr imr_multiaddr; // 組播組的IP地址。
struct in_addr imr_interface; // 本地某一網路裝置介面的IP地址。
};
一臺主機上可能有多塊網絡卡,接入多個不同的子網,imr_interface引數就是指定一個特定的裝置介面,告訴協議棧只想在這個裝置所在的子網中加入某個組播組。有了這兩個引數,協議棧就能知道:在哪個網路裝置介面上加入哪個組播組。傳送組播包的示例程式碼如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define N 64
#define PORT 8888
int main()
{
int sockfd;
char buf[N] = "This is a multicast package!";
struct sockaddr_in dstaddr;
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
perror("socket error");
exit(-1);
}
else
{
printf("socket successfully!\n");
printf("sockfd:%d\n",sockfd);
}
memset(&dstaddr, 0, sizeof(dstaddr));
dstaddr.sin_family = AF_INET;
dstaddr.sin_port = htons(PORT);
dstaddr.sin_addr.s_addr = inet_addr("224.10.10.1"); //繫結組播地址
while(1)
{
sendto(sockfd, buf, N, 0,(struct sockaddr *)&dstaddr, sizeof(dstaddr));
sleep(1);
}
return 0;
}
2)組播包接收流程
組播包接收流程如下
建立UDP套接字
加入多播組
繫結地址和埠
接收資料包
組播包接收流程如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define N 64
#define PORT 8888
int main()
{
int sockfd;
char buf[N];
struct ip_mreq mreq;
struct sockaddr_in seraddr,myaddr;
socklen_t peerlen = sizeof(seraddr);
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
perror("socket error");
exit(-1);
}
else
{
printf("socket successfully!\n");
printf("sockfd:%d\n",sockfd);
}
memset(&mreq, 0, sizeof(mreq));
mreq.imr_multiaddr.s_addr = inet_addr("224.10.10.1"); //加入多播組,允許資料鏈路層處理指定組播包
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
if(setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0)
{
perror("fail to setsockopt");
exit(-1);
}
memset(&seraddr, 0, sizeof(myaddr));//為套接字繫結組播地址和埠
myaddr.sin_family = AF_INET;
myaddr.sin_port = htons(PORT);
myaddr.sin_addr.s_addr = inet_addr("224.10.10.1");
if(bind(sockfd, (struct sockaddr *)&myaddr, sizeof(myaddr)) == -1)
{
perror("bind error");
exit(-1);
}
else
{
printf("bind successfully!\n");
printf("PORT:%d\n",PORT);
}
while(1)
{
if(recvfrom(sockfd, buf, N, 0, (struct sockaddr *)&seraddr, &peerlen) < 0)
{
perror("fail to recvfrom");
exit(-1);
}
else
{
printf("[%s:%d]",inet_ntoa(seraddr.sin_addr),ntohs(seraddr.sin_port));
printf("%s\n",buf);
}
}
return 0;
}
執行結果如下:
[email protected]:~/qiang/socket/zubo$ ./zubore
socket successfully!
sockfd:3
bind successfully!
PORT:8888
[192.168.1.2:53259]This is a multicast package!
[192.168.1.2:53259]This is a multicast package!
[192.168.1.2:53259]This is a multicast package!
[192.168.1.2:53259]This is a multicast package!
[192.168.1.2:53259]This is a multicast package!
[192.168.1.2:53259]This is a multicast package!
[192.168.1.2:53259]This is a multicast package!
[192.168.1.2:53259]This is a multicast package!
[192.168.1.2:53259]This is a multicast package!
....