嵌入式开发之网络连接管理
本文在这主要记录关于开发板无线网络连接状态的监管,由于项目需求,需要对产品的功能进行优化,要求有一个掉线检测的功能,下面让我们看看使用自定义的HeartBeat方式来检测客户端的连接情况。
1、心跳机制
心跳包由来 :像心跳一样每隔固定时间发一次,以此来告诉服务器,这个客户端还活着。事实上这是为了保持长连接,至于这个包的内容,是没有什么特别规定的,不过一般都是很小的包,或者只包含包头的一个空包。
在TCP的机制里面,本身是存在有心跳包的机制的,也就是TCP的选项:SO_KEEPALIVE。系统默认是设置的2小时的心跳频率。但是它检查不到机器断电、网线拔出、防火墙这些断线。而且逻辑层处理断线可能也不是那么好处理。一般,如果只是用于保活还是可以的。
心跳包一般来说都是在逻辑层发送空的echo包来实现的。下一个定时器,在一定时间间隔下发送一个空包给客户端,然后客户端反馈一个同样的空包回来,服务器如果在一定时间内收不到客户端发送过来的反馈包,那就只有认定说掉线了。
其实,要判定掉线,只需要send或者recv一下,如果结果为零,则为掉线。但是,在长连接下,有可能很长一段时间都没有数据往来。理论上说,这个连接是一直保持连接的,但是实际情况中,如果中间节点出现什么故障是难以知道的。更要命的是,有的节点(防火墙)会自动把一定时间之内没有数据交互的连接给断掉。在这个时候,就需要我们的心跳包了,用于维持长连接,保活。
在获知了断线之后,服务器逻辑可能需要做一些事情,比如断线后的数据清理呀,重新连接呀……当然,这个自然是要由逻辑层根据需求去做了。
总的来说,心跳包主要也就是用于长连接的保活和断线处理。一般的应用下,判定时间在30-40秒比较不错。如果实在要求高,那就在6-9秒
心跳检测步骤:
1 客户端每隔一个时间间隔发生一个探测包给服务器
2 客户端发包时启动一个超时定时器
3 服务器端接收到检测包,应该回应一个包
4 如果客户机收到服务器的应答包,则说明服务器正常,删除超时定时器
5 如果客户端的超时定时器超时,依然没有收到应答包,则说明服务器挂了
2、 socket
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现, socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭).
说白了Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
注意:其实socket也没有层的概念,它只是一个facade设计模式的应用,让编程变的更简单。是一个软件抽象层。在网络编程中,我们大量用的都是通过socket实现的。
服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
以上内容只是做一个socket简单的介绍,接下来主要讲解一下socket相关接口函数的使用情况。
2.1 socket()函数
int socket(int protofamily, int type, int protocol);//返回sockfd
其中:
sockfd—描述符,类似于文件描述符一样,用于创建与标识唯一的socket连接、若sockfd<0,代表创建socket失败;
protofamiliy—协议域,又称为协议族,决定socket的地址类型、一般设置为AF_INET(IPV4地址);
type—socket类型,一般设置为SOCK_STREAM(默认协议为TCP协议);
protocol—指定协议,有TCP、UDP、STCP、TIPC等,一般protocol设置为0,对应选择type类型的默认协议;
例如:
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) //判断是否成功创建socket连接
{
fprintf(stderr, "create socket failed\n");
exit(EXIT_FAILURE);
}
puts("create socket success");
printf("sockfd is %d\n", sockfd);
2.2 bind()函数
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
其中:
sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
上述结构体定义在
以下是必要的代码说明:
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
bzero(&server_addr, sizeof(struct sockaddr_in));
//设置addr的成员变量信息
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
//设置ip为本机IP
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(sockfd, (struct sockaddr*)(&server_addr), sizeof(struct sockaddr)) < 0)//判定绑定是否成功
{
fprintf(stderr, "bind failed \n");
exit(EXIT_FAILURE);
}
puts("bind success\n");
在上述代码中出现htonl()函数——网络字节转换函数,host to network long,将多字节整数类型的数据,从主机字节顺序转化为网络字节顺序(也可以这样理解,网络字节顺序相当于黄金,大家都认可,主机字节顺序则只是某个国家自己的货币,不一定能够被其他国家认可使用),网络字节转换函数还有htons(), ntohs(),ntohl()。
addrlen——对应地址长度;
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
2.3 listen()、connect()、accept()
int listen(int sockfd, int backlog);//backlog表示相应socket可以排队的最大连接个数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//客户端socket的描述符、服务器的socket地址、socket地址的长度
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回连接connect_fd
完成以上操作,就可以进行客户端与服务器之间的交互。
2.4 close()函数
在服务器与客户端建立连接之后,完成交互之后需要关闭相应的socket描述字(做事要有始有终)。
close(sockfd);
3.总结
1)网络层的“ip地址”可以唯一识别网络中的主机;
2)“协议+端口”可以唯一标识主机中的应用程序进程;
3)“ip地址,协议,端口”就唯一标识某一主机中的网络进程;
4)TCP协议的心跳机制基于服务器与客户端间是否建立局域网关系,可以分为跨网心跳(通过各自网络交互)与非跨网心跳(两者在同一局域网内,即使客户端未连接网络也可以跟服务器交互)。
跨网心跳与非跨网心跳是为了更好的理解,自主定义的词汇,上述内容都是自己查阅资料与实践结合的认知和感悟,如有不妥之处,请见谅!如有疑问,欢迎留言互相交流切磋!
基于TCP协议的socket定时连接交互的心跳包可以在本人博客上下载。