《Linux操作系统 - 高级编程》第三部分 网络编程 (第6章 网络编程(高级篇))

6.1网络超时检测

在网络通信过程中,经常会出现不可预知的各种情况。例如网络线路突发故障、通信一方异常结束等。一旦出现上述情况,很可能长时间都不会收到数据,而且无法判断是没有数据还是数据无法到达。如果使用的是TCP协议,可以检测出来;但如果使用UDP协议的话,需要在程序中进行相关检测。所以,为避免进程在没有数据时无限制的阻塞,使用网络超时检测很有必要。

6.1.1套接字接收超时检测

这里先介绍设置套接字选项的函数 setsockopt() 函数:

表1 setsockopt() 函数

这里写图片描述

下面是套接字常用选项及其说明:

表2 LEVEL:SOL_SOCKET

这里写图片描述

下面利用SO_RCVTIMEO的选项实现套接字的接收超时检测。
【参见附件/setsockopt.c】

#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;  
}  

执行结果如下:
这里写图片描述
可以看到,6s之内没有数据包到来,程序会从 recvfrom 函数返回,进行相应的错误处理。

【注意】套接字一旦设置了超时之后,每一次发送或接收时都会检测,如果要取消超时检测,重新用setsockopt函数设置即可(把时间值指定为 0)。

6.1.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。

表3 sigaction()函数

这里写图片描述

使用定时器信号检测超时的示例代码如下:
【参见附件/alarm.c】

#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;  
}  

执行结果如下:
这里写图片描述

6.2广播

前面的网络通信中,采用的都是单播(唯一的发送方和接收方)的方式。很多时候,需要把数据同时发送给局域网中的所有主机。例如,通过广播ARP包获取目标主机的MAC地址。

6.2.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。

6.2.2广播包的发送和接收

广播包的发送和接收通过UDP套接字实现。
1)广播包发送流程如下:
 创建udp 套接字
 指定目标地址和端口
 设置套接字选项允许发送广播包
 发送数据包
发送广播包的示例如下:
【参见附件/broadcast/send.c】

#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.0.110"); // 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套接字
 绑定地址
 接收数据包
接收包示例如下:
【参见附件/broadcast/client.c】

#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.0.110"); //接收方绑定广播地址  
    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;  
}  

执行结果如下:
服务器:
这里写图片描述
客户端:
这里写图片描述

6.3组播

通过广播可以很方便地实现发送数据包给局域网中的所有主机。但广播同样存在一些问题,例如,频繁地发送广播包造成所以主机数据链路层都会接收并交给上层 协议处理,也容易引起局域网的网络风暴。

下面介绍一种数据包发送方式成为组播或多播。组播可以看成是单播和广播的这种。当发送组播数据包时,至于加入指定多播组的主机数据链路层才会处理,其他主机在数据链路层会直接丢掉收到的数据包。换句话说,我们可以通过组播的方式和指定的若干主机通信。

6.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类地址呗成为组播地址。每一个组播地址代表一个多播组。

6.3.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参数就是指定一个特定的设备接口,告诉协议栈只想在这个设备所在的子网中加入某个组播组。有了这两个参数,协议栈就能知道:在哪个网络设备接口上加入哪个组播组。
发送组播包的示例代码如下:
【参见附件/multicast/send.c】

#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套接字
 加入多播组
 绑定地址和端口
 接收数据包
组播包接收流程如下:
【参见附件/multicast/client.c】

#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;  
}  

执行结果如下:
服务器:
这里写图片描述
客户端:
这里写图片描述

小贴士:

常用命令解析
1、telnet
Telnet协议是TCP/IP协议族中的一员,是Internet远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力。在终端使用者的电脑上使用telnet程序,用它连接到服务器。终端使用者可以在telnet程序中输入命令,这些命令会在服务器上运行,就像直接在服务器的控制台上输入一样。可以在本地就能控制服务器。
当我们使用Telnet登录进入远程计算机系统时,事实上启动了两个程序:一个是Telnet客户程序,运行在本地主机上;另一个是Telnet服务器程序,它运行在要登录的远程计算机上。
本地主机上的Telnet客户程序主要完成以下功能:
●建立与远程服务器的TCP联接。
●从键盘上接收本地输入的字符。
●将输入的字符串变成标准格式并传送给远程服务器。
●从远程服务器接收输出的信息。
●将该信息显示在本地主机屏幕上。
远程主机的“服务”程序通常被昵称为“精灵”,它平时不声不响地守候在远程主机上,一接到本地主机的请求,就会立马活跃起来,并完成以下功能:
●通知本地主机,远程主机已经准备好了。
●等候本地主机输入命令。
●对本地主机的命令作出反应(如显示目录内容,或执行某个程序等)。
●把执行命令的结果送回本地计算机显示。
●重新等候本地主机的命令。
下面我们利用Telnet 测试我们自己所写的TCP服务器,下面是个TCP服务器的demo,实现"echo” 功能。
这里写图片描述
我们可以看到主机IP地址为192.168.3.51,端口号PORT为8888,正在监听中,这里我们执行telnet命令。
这里写图片描述
可以看到连接成功,下面我们可以向服务器端发送数据。
这里写图片描述
这里TCP服务器实现的功能是 echo ,就是在客户端输入数据,会显示在下方,图中可以看到效果,下面是服务器端显示。
这里写图片描述
2、lsof
lsof最基本的功能:
lsof -i:(端口)查看这个端口有那些进程在访问,比如我们上面正在监听的8888端口。
这里写图片描述
这里可以看到我们的TCP服务器和Telnet 都在访问这个端口号。

3、netstat
Netstat 命令用于显示各种网络相关信息,如网络连接,路由表,接口状态 (Interface Statistics),masquerade 连接,多播成员 (Multicast Memberships) 等等。
常见参数
-a (all)显示所有选项,默认不显示LISTEN相关
-t (tcp)仅显示tcp相关选项
-u (udp)仅显示udp相关选项
-n 拒绝显示别名,能显示数字的全部转化成数字。
-l 仅列出有在 Listen (监听) 的服務状态
-p 显示建立相关链接的程序名
-r 显示路由信息,路由表
-e 显示扩展信息,例如uid等
-s 按各个协议进行统计
-c 每隔一个固定时间,执行该netstat命令。
提示:LISTEN和LISTENING的状态只有用-a或者-l 才能看到

  1. 列出所有端口 (包括监听和未监听的)
    列出所有端口 netstat -a
    这里写图片描述
  2. 列出所有处于监听状态的 Sockets
    只显示监听端口 netstat -l
    这里写图片描述
    可以看到端口8888处于监听状态。
本章参考代码

点击进入

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bruceoxl

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值