Linux网络编程拓展-域名解析 网络属性设置 超时优化

域名解析

网络信息检索函数:

gethostname() 获得主机名
getpeername() 获得与套接口相连的远程协议地址
getsockname() 获得本地套接口协议地址
gethostbyname() 根据主机名取得主机信息 
  endhostent()

gethostbyaddr() 根据主机地址取得主机信息
getprotobyname() 根据协议名取得主机协议信息
getprotobynumber() 根据协议号取得主机协议信息
getservbyname() 根据服务名取得相关服务信息
getservbyport() 根据端口号取得相关服务信息

gethostbyname() 函数详解

#include <netdb.h>
       extern int h_errno;

       struct hostent *gethostbyname(const char *name);

返回值
成功返回 hostent结构体 指针,失败返回空指针。
若失败可调用
void herror(const char *s);

const char *hstrerror(int err); 来获取错误信息。
信息值一般如下
HOST_NOT_FOUND:找不到主机;
TRY_AGAIN:重试;
NO_RECOVERY:不可修复性错误;
NO_DATA:指定的名字有效,但是没有记录;

使用条件:
IPv4中使用gethostbyname()函数完成主机名到地址解析,这个函数仅仅支持IPv4,且不允许调用
关于 hostent结构体 :

struct hostent {
                char *h_name;         //正式主机名  
                char **h_aliases;     //主机别名  
                int h_addrtype;       //主机IP地址类型:IPV4-AF_INET  
                int h_length;         //主机IP地址字节长度,对于IPv4是四字节,即32位  
                char **h_addr_list;   //主机的IP地址列表 
           }
           #define h_addr h_addr_list[0] /* 向后兼容 */

其中 h_addr_list

示例代码:

#include <stdio.h>  
#include <netdb.h>  
#include <sys/socket.h>  
  
int main(int argc, char *argv[])  
{  
    char *ptr, **pptr;  
    struct hostent *hptr;  
    char str[32] = {0};  
    int i;  
    ptr = argv[1];  
      
    if((hptr = gethostbyname(ptr)) == NULL)  
    {  
        printf("gethostbyname error: %s\n", ptr);  
        return 0;  
    }  
      
    printf("official hostname:%s\n", hptr->h_name);   //主机规范名  
      
    for(pptr = hptr->h_aliases; *pptr != NULL; pptr++)   //将主机别名打印出来  
        printf("alias: %s\n", *pptr);  
      
	printf("type of hostname:%d\n",hptr->h_addrtype);    //打印主机ip类型
    
	if(hptr->h_addrtype != AF_INET)
		return 0;
	printf("length of hostname: %d\n",hptr->h_length);  //主机IP地址字节长度

	//for(;*pptr!=NULL;pptr++)
    //printf("  address:%s\n", inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));
	for(i=0;*(hptr->h_addr+i);i++){
		if(NULL==inet_ntop(AF_INET,hptr->h_addr+i,str,32))
			break;	
		printf("hostname IP is: %s\n",str);	
	}
	
    return 0;  
}  

测试结果:

linux@ubuntu:~/Level6/day7$ ./dnstest www.baidu.com
official hostname:www.a.shifen.com
alias: www.baidu.com
type of hostname:2
length of hostname: 4
hostname IP is: 61.135.169.121
hostname IP is: 135.169.121.61
hostname IP is: 169.121.61.135
hostname IP is: 121.61.135.185
hostname IP is: 61.135.185.32
hostname IP is: 135.185.32.97
hostname IP is: 185.32.97.46
hostname IP is: 32.97.46.115
hostname IP is: 97.46.115.104
hostname IP is: 46.115.104.105
hostname IP is: 115.104.105.102
hostname IP is: 104.105.102.101
hostname IP is: 105.102.101.110
hostname IP is: 102.101.110.46
hostname IP is: 101.110.46.99
hostname IP is: 110.46.99.111
hostname IP is: 46.99.111.109
hostname IP is: 99.111.109.0
hostname IP is: 111.109.0.101
hostname IP is: 109.0.101.114

网络属性设置

YNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int getsockopt(int sockfd, int level, int optname,
                      void *optval, socklen_t *optlen);
       int setsockopt(int sockfd, int level, int optname,
                      const void *optval, socklen_t optlen);

level指定控制套接字的层次.可以取三种值:
1) SOL_SOCKET:通用套接字选项. (应用层)
2) IPPROTO_TCP:TCP选项. (传输层)
3) IPPROTO_IP:IP选项. (网络层)

下面详细解释 optname 指定控制的方式(选项的名称) 

其中 timeval 的定义如下:

struct timeval{
    __time_t tv_sec;        /* Seconds.秒 */
    __suseconds_t tv_usec;  /* Microseconds. 微秒*/
};

optval 指定选项值的地址

optlen 指定选项值的长度

示例代码:

/*允许广播*/
int b_br = 1;
setsockopt(fd,SOL_SOCKET,SO_BROADCAST,&b_br,sizeof(int));


/*设置接收超时*/
struct timeval tv;
tv.tv_sec = 5;
tv.tv_usec = 0;

setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(struct timeval));

/*允许绑定地址快速重用*/
nt b_br = 1;
setsockopt(fd,SOL_SOCKET,SO_REUSERADDR,&b_br,sizeof(1));

网络超时优化

方法1:设置 socket的属性 SO_RCVTIMEO

参考代码:

struct timeval tv;
tv.tv_sec = 5;
tv.tv_usec = 0;

setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(struct timeval));

方法2: 用select检测socket是否准备就绪
参考代码:

struct fd_set rdfs;
while(1){
    struct timewal tv = {5,0};

    FD_ZERO(&rdfs);
    FD_SET(sockfd,&rdfs);
    if(select(sockfd+1,&rdfs,NULL,NULL,&tv)>0){
        recv()/recvfrom()   //从socket读数据
    }
}

方法3: 设置定时器(timer), 捕捉SIGALRM信号

参考代码如下

void  handler(int signo)     {   return;  }

    struct sigaction  act;
    sigaction(SIGALRM, NULL, &act);
    act.sa_handler = handler;
    act.sa_flags &= ~SA_RESTART;    //SA_RESTART位清零
    sigaction(SIGALRM, &act, NULL);
    alarm(5);
    if (recv(,,,) < 0)...

心跳机制
应用场景:
在长连接下,有可能很长一段时间都没有数据往来。理论上说,这个连接是一直保持连接的,但是实际情况中,如果中间节点出现什么故障是难以知道的。更要命的是,有的节点(防火墙)会自动把一定时间之内没有数据交互的连接给断掉。在这个时候,就需要我们的心跳包了,用于维持长连接,保活

什么是心跳机制?
就是每隔几分钟发送一个固定信息给服务端,服务端收到后回复一个固定信息如果服务端几分钟内没有收到客户端信息则视客户端断开。

发包方:可以是客户也可以是服务端,看哪边实现方便合理。
心跳包之所以叫心跳包是因为:它像心跳一样每隔固定时间发一次,以此来告诉服务器,这个客户端还活着。事实上这是为了保持长连接,至于这个包的内容,是没有什么特别规定的,不过一般都是很小的包,或者只包含包头的一个空包。心跳包主要也就是用于长连接的保活和断线处理。一般的应用下,判定时间在30-40秒比较不错。如果实在要求高,那就在6-9秒。

心跳包的发送,通常有两种技术:
1.应用层自己实现的心跳包
由应用程序自己发送心跳包来检测连接是否正常,服务器每隔一定时间向客户端发送一个短小的数据包,然后启动一个线程,在线程中不断检测客户端的回应, 如果在一定时间内没有收到客户端的回应,即认为客户端已经掉线;同样,如果客户端在一定时间内没有收到服务器的心跳包,则认为连接不可用。

2.使用SO_KEEPALIVE套接字选项
在TCP的机制里面,本身是存在有心跳包的机制的,也就是TCP的选项. 不论是服务端还是客户端,一方开启KeepAlive功能后,就会自动在规定时间内向对方发送心跳包, 而另一方在收到心跳包后就会自动回复,以告诉对方我仍然在线。因为开启KeepAlive功能需要消耗额外的宽带和流量,所以TCP协议层默认并不开启默认的KeepAlive超时需要7,200,000 MilliSeconds, 即2小时,探测次数为5次。对于很多服务端应用程序来说,2小时的空闲时间太长。因此,我们需要手工开启KeepAlive功能并设置合理的KeepAlive参数

开启KeepAlive选项后会导致的三种情况:
1、对方接收一切正常:以期望的ACK响应,2小时后,TCP将发出另一个探测分节
2、对方已崩溃且已重新启动:以RST响应。套接口的待处理错误被置为ECONNRESET,套接口本身则被关闭。
3、对方无任何响应:套接口的待处理错误被置为ETIMEOUT,套接口本身则被关闭.

有关SO_KEEPALIVE的三个参数:
1.tcp_keepalive_intvl,保活探测消息的发送频率。默认值为75s。
发送频率tcp_keepalive_intvl乘以发送次数tcp_keepalive_probes,就得到了从开始探测直到放弃探测确定连接断开的时间,大约为11min。
2.tcp_keepalive_probes,TCP发送保活探测消息以确定连接是否已断开的次数。默认值为9(次)。
3.tcp_keepalive_time,在TCP保活打开的情况下,最后一次数据交换到TCP发送第一个保活探测消息的时间,即允许的持续空闲时间。默认值为7200s(2h)。

总结:
一个服务器通常会连接多个客户端,因此由用户在应用层自己实现心跳包,代码较多 且稍显复杂。用TCP/IP协议层为内置的KeepAlive功能来实现心跳功能则简单得多。心跳包在按流量计费的环境下增加了费用.但TCP得在连接闲置2小时后才发送一个保持存活探测段,所以通常的方法是将保持存活参数改小,但这些参数按照内核去维护,而不是按照每个套接字维护,因此改动它们会影响所有开启该选项的套接字。

下面我们通过一个实例来展示心跳机制。

结构,一个客户程序,和一个服务程序。

步骤:
服务器:
1.经过socket、bind、listen、后用accept获取一个客户的连接请求,为了简单直观,这里服务器程序只接收一个connect请求,我们用clifd来获取唯一的一个连接。
2.为clifd修改KeepAlive的相关参数,并开启KeepAlive套接字选项,这里我们把间隔时间设为了5秒,闲置时间设置了5秒,探测次数设置为5次。
3.将clifd加入select监听的描述符号集

客户:很简单,只是连接上去,并停留在while死循环。

方式:
服务程序放到阿里云服务器上,我们执行服务程序并将输出结果重定向到一个日志文件,目的是为了将我们本地网络连接断开后,超过了keepalive闲置时间+重复发包探测的时间后,重新打开本地的网络连接,并登录服务器,通过该日志文件的内容来查看程序的打印结果。

client 代码 cpp

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <strings.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
using namespace std;
 
int main()
{
	int skfd;
	if ((skfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
		perror("");
		exit(-1);
	}	
	
	struct sockaddr_in saddr;
	bzero(&saddr, sizeof(saddr));
	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(9999);
	saddr.sin_addr.s_addr = inet_addr("115.29.109.198");
	
	if (connect(skfd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0) {
		perror("");
		exit(-1);
	}
 
	cout << "连接成功" << endl;
	while(1);
	return 0;
}
 

server 代码 cpp

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <strings.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <sys/select.h>
#include <netinet/tcp.h>
using namespace std;
 
#define LISTENNUM 5
 
int main()
{
	int skfd;
    if ((skfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
    	perror("");
        exit(-1);
    }
        
    struct sockaddr_in saddr;
    bzero(&saddr, sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = inet_addr("115.29.109.198");
 
    if (bind(skfd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0) {
    	perror("");
        exit(-1);
    }
 
    if (listen(skfd, LISTENNUM) < 0) {
    	perror("");
        exit(-1);
    }
        
    int clifd;
    if ((clifd = accept(skfd, NULL, NULL)) < 0) {
    	perror("");
        exit(-1);
    }
    cout << "有新连接" << endl;
    
    //setsockopt
    int tcp_keepalive_intvl = 5;   //保活探测消息的发送频率。默认值为75s
    int tcp_keepalive_probes = 5;  //TCP发送保活探测消息以确定连接是否已断开的次数。默认值为9次
    int tcp_keepalive_time = 5;    //允许的持续空闲时间。默认值为7200s(2h)
    int tcp_keepalive_on = 1;
 
    if (setsockopt(clifd, SOL_TCP, TCP_KEEPINTVL,
        &tcp_keepalive_intvl, sizeof(tcp_keepalive_intvl)) < 0) {
        perror("");
        exit(-1);
    }
 
    if (setsockopt(clifd, SOL_TCP, TCP_KEEPCNT,
    	&tcp_keepalive_probes, sizeof(tcp_keepalive_probes)) < 0) {
        perror("");
        exit(-1);
    }
 
    if (setsockopt(clifd, SOL_TCP, TCP_KEEPIDLE,
        &tcp_keepalive_time, sizeof(tcp_keepalive_time)) < 0) {
    	perror("");
        exit(-1);
    }
 
	if (setsockopt(clifd, SOL_SOCKET, SO_KEEPALIVE,
        &tcp_keepalive_on, sizeof(tcp_keepalive_on))) {
        perror("");
        exit(-1);
    }
 
    char buf[1025];
    int r;
    int maxfd;
    fd_set rset;
    FD_ZERO(&rset);
    sleep(5);
    while (1) {
    	FD_SET(clifd, &rset);
        maxfd = clifd + 1;
        if (select(maxfd, &rset, NULL, NULL, NULL) < 0) {
        	perror("");
            exit(-1);
        }
                
        if (FD_ISSET(clifd, &rset)) {
        	r = read(clifd, buf, sizeof(buf));
            if (r == 0) {
            	cout << "接收到FIN" << endl;
                close(clifd);
                break;
            }
            else if (r == -1) {
            	if (errno == EINTR) {
                	cout << "errno: EINTR" << endl;
                    continue;
                }
 
               	if (errno == ECONNRESET) {
                	cout << "errno: ECONNRESET" << endl;
                    cout << "对端已崩溃且已重新启动" << endl;
                    close(clifd);
                    break;
                }
                                
                if (errno == ETIMEDOUT) {
                	cout << "errno: ETIMEDOUT" << endl;
                   	cout << "对端主机崩溃" << endl;
                    close(clifd);
                    break;
                }
 
                if (errno == EHOSTUNREACH) {
                	cout << "errno: EHOSTUNREACH" << endl;
                    cout << "对端主机不可达" << endl;
                   	close(clifd);
                    break;
                }
            }
        }
    }
 
    close(skfd);
	return 0;
}

执行服务程序,再执行客户程序,之后将网络连接断开一段时间后(大于KeepAlive空闲时间+重复探测时间),重新打开网络连接,发现打印了ETIMEDOUT,验证了在客户网络断开后,到达空闲时间时,服务器由于开启了KeepAlive选项,会向客户端发送探测包,几次还没收到客户端的回应,那么select将返回套接字可读的条件,并且read返回-1.设置相关错误,而与之相反的情况是如果不开启KeelAlive选项,那么即使客户端网络断开超过了整个的空闲和探测时间,服务端的select也不会返回可读的条件,即应用程序无法得到通知。

参考文档:
https://blog.csdn.net/aspnet_lyc/article/details/37318861?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.add_param_isCf&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.add_param_isCf