1. 程式人生 > >Linux 系統應用程式設計——網路程式設計(高階篇)

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!

....