项目中很少会使用阻塞接收,一般都是select+read监听模式来实现非阻塞接收。
使用selece时,需要处理一些异常情况的返回,比如:系统中断产生EINTR错误;超时错误ETIMEDOUT。
使用read时,需要处理读取时可能出现的错误,比如:对方关闭连接(ECONNRESET),连接被拒绝(ECONNREFUSED),文件描述符错误(EBADF)等
下面以libmodbus中receive_msg函数为例,总结select+read的处理步骤,其中有对modbus协议处理的步骤(2、7、8、9),如果不关心modbu协议可忽略这些步骤。
【0】函数返回值说明:
等待服务器的响应或客户端的请求。如果没有应答这个函数将会阻塞(超时);
如果接收uint8_t的消息数组成功,将会返回接收字符的个数,否则将会返回-1,并且errno将会被置为如下值:
ECONNRESET:对方复位连接
EMBBADDATA:无效数据
ETIMEDOUT:超时
read()和recv()函数的错误码
static int receive_msg(modbus_t *ctx, uint8_t *msg, msg_type_t msg_type)
{
int rc;
fd_set rfds;
struct timeval tv;
struct timeval *p_tv;
int length_to_read;
int msg_length = 0;
_step_t step;
【1】设置fd_set
FD_ZERO(&rfds);
FD_SET(ctx->s, &rfds);
【2】分步接收并解析modbus的消息,首先接收功能码,并解析
step = _STEP_FUNCTION;
length_to_read = ctx->backend->header_length + 1;
【3】设置超时时间,根据服务器和客户端所传参数中的msg_type不同,做不同设置
---------- Request Indication ----------
| Client | ---------------------->| Server |
---------- Confirmation Response ----------
MSG_INDICATION:用在服务器端,一直等待,直到有客户端发起请求
MSG_CONFIRMATION:用在客户端,有等待超时
if (msg_type == MSG_INDICATION) {
/* 一直等待客户端的请求 */
p_tv = NULL;
} else {
tv.tv_sec = ctx->response_timeout.tv_sec;
tv.tv_usec = ctx->response_timeout.tv_usec;
p_tv = &tv;
}
【4】循环接收读取消息
while (length_to_read != 0) {
【5】select等待接收
while ((rc = select(ctx->s+1, rfds, NULL, NULL, p_tv)) == -1) {
if (errno == EINTR) {
//捕捉到一个非阻塞信号,c
FD_ZERO(rfds);
FD_SET(ctx->s, rfds);
}
}
if (rc == 0) {
//超时
errno = ETIMEDOUT;
rc = -1;
}
if (rc == -1) {
_error_print(ctx, "select");
if (ctx->error_recovery & MODBUS_ERROR_RECOVERY_LINK) {
int saved_errno = errno;
if (errno == ETIMEDOUT) {
_sleep_and_flush(ctx);
} else if (errno == EBADF) {
modbus_close(ctx);
modbus_connect(ctx);
}
errno = saved_errno;
}
return -1;
}
【6】接收消息
rc = read(ctx->s, msg + msg_length, length_to_read);
if (rc == 0) {
errno = ECONNRESET;
rc = -1;
}
if (rc == -1) {
//read错误
if ((ctx->error_recovery & MODBUS_ERROR_RECOVERY_LINK) &&
(errno == ECONNRESET || errno == ECONNREFUSED ||
errno == EBADF)) {
int saved_errno = errno;
modbus_close(ctx);
modbus_connect(ctx);
/* Could be removed by previous calls */
errno = saved_errno;
}
return -1;
}
//收到的总字节数
msg_length += rc;
//计算剩余的字节
length_to_read -= rc;
【7】分步接收modbus消息,判断当前步骤是否接收完,并进入下一步
if (length_to_read == 0) {
switch (step) {
【7.1】功能码接收完毕,根据功能码计算下一步的长度。
下一步标记为:meta,意为元或可变之意,因为该步骤的信息不固定
case _STEP_FUNCTION:
length_to_read = compute_meta_length_after_function(
msg[ctx->backend->header_length],
msg_type);
if (length_to_read != 0) {
step = _STEP_META;
break;
} //else:如果是其他的值,不break,直接转到下一步
【7.2】meta接收完毕,根据功能码和meta信息,计算下一步数据长度
case _STEP_META:
length_to_read = compute_data_length_after_meta(
ctx, msg, msg_type);
if ((msg_length + length_to_read) > ctx->backend->max_adu_length) {
errno = EMBBADDATA;
//接收数据太多,异常
return -1;
}
step = _STEP_DATA;
break;
default:
break;
}
}
【8】如果数据没有接收完毕,并且设置了字符间隔,则使用字符间隔作为超时来继续等待接收
if (length_to_read > 0 && ctx->byte_timeout.tv_sec != -1) {
tv.tv_sec = ctx->byte_timeout.tv_sec;
tv.tv_usec = ctx->byte_timeout.tv_usec;
p_tv = &tv;
}
}
【9】检查完整性
return ctx->backend->check_integrity(ctx, msg, msg_length);
}