一、網絡超時檢測 在網絡通信過程中,經常會出現不可預知的各種情況。例如網絡線路突發故障、通信一方異常結束等。一旦出現上述情況,很可能長時間都不會收到數據,而且無法判斷是沒有數據還是數據無法到達。如果使用的是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允許發送廣播數據intSO_DEBUG允許調試intSO_DONTRUOTE不查找路由intSO_ERROR獲得套接字錯誤intSO_KEEPALIVE保持連接intSO_LINGER延遲關閉連接struct lingerSO_OOBINLINE帶外數據放入正常數據流intSO_RCVBUF接收緩沖區大小intSO_SNDBUF發送緩沖區大小int
SO_RCVTIMEO接收超時struct timevalSO_SNDTIMEO發送超時struct timevalSO_REUSERADDR允許重用本地地址和端口intSO_TYPE獲得套接字類型int下面利用SO_RCVTIMEO的選項實現套接字的接收超時檢測:
[cpp] view
plain copy
#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
;
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;
}
執行結果如下:
[cpp] view
plain copy
fs@ubuntu:~/qiang/socket/time$ ./setsockopt
socket successfully!
sockfd:3
bind successfully!
PORT:8888
fail to recvfrom: Resource temporarily unavailable
fs@ubuntu:~/qiang/socket/time$
可以看到,6s之內沒有數據包到來,程序會從 recvfrom 函數返回,進行相應的錯誤處理。
注意:套接字一旦設置了超時之後,每一次發送或接收時都會檢測,如果要取消超時檢測,重新用setsockopt函數設置即可(把時間值指定為 0)。
2、定時器超時檢測 這裡利用定時器信號SIGALARM,可以在程序中創建一個鬧鐘。當到達目標時間後,指定的信號處理函數被執行。這樣同樣可以利用SIGALARM信號實現檢測,下面分別介紹相關數據類型和函數。
struct sigaction 是 Linux 中用來描述信號行為的結構體類型,其定義如下:
[cpp] view
plain copy
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使用定時器信號檢測超時的示例代碼如下:
[cpp] view
plain copy
#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
;
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;
}
執行結果如下:
[cpp] view
plain copy
fs@ubuntu:~/qiang/socket/time$ ./alarm
socket successfully!
sockfd:3
bind successfully!
PORT:8888
interrupted by SIGALRM
fail to recvfrom: Interrupted system call
fs@ubuntu:~/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 套接字
指定目標地址和端口
設置套接字選項允許發送廣播包
發送數據包
發送廣播包的示例如下:
[cpp] view
plain copy
#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
= "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套接字
綁定地址
接收數據包
接收包示例如下:
[cpp] view
plain copy
#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
;
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;
}
執行結果如下
[cpp] view
plain copy
fs@ubuntu:~/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是該操作的參數,下面是其定義:
[cpp] view
plain copy
struct ip_mreq
{
struct in_addr imr_multiaddr; // 組播組的IP地址。
struct in_addr imr_interface; // 本地某一網絡設備接口的IP地址。
};
一台主機上可能有多塊網卡,接入多個不同的子網,imr_interface參數就是指定一個特定的設備接口,告訴協議棧只想在這個設備所在的子網中加入某個組播組。有了這兩個參數,協議棧就能知道:在哪個網絡設備接口上加入哪個組播組。
發送組播包的示例代碼如下:
[cpp] view
plain copy
#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
= "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套接字
加入多播組
綁定地址和端口
接收數據包
組播包接收流程如下:
[cpp] view
plain copy
#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
;
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;
}
執行結果如下:
[cpp] view
plain copy
fs@ubuntu:~/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!
....