请求、响应与超时机制详解

请求、响应与超时机制详解

在C/S软件模型中,往往都遵循这请求和响应的机制,而且往往还伴随着超时控制。即:A发送请求,然后等待B的响应,同时开始超时计时,如果在超时时间内成功接收到响应,则结束等待和计时。如果到了超时时间还没有接收到响应,则结束等待同时此次通讯失败。

我们站在A端来设计。
A发送通讯请求,然后进入等待状态。此时常见的操作就是将当前线程挂起(suspend)。让挂起线程进入就绪状态有两个条件:超时或正确响应。当线程重新运行后,根据唤醒的方式不一样,执行不同的操作。如果是超时,则可能需要中断此次操作,然后上报错误信息。如果是正确响应,则结束超时计时,同时继续执行下一次传输。

这种模式我使用三种环境来说明:
- ucos ii(freeRTOS)
- linux
- qt5

ucos适用于低端嵌入式
linux适用于高端嵌入式和服务器
qt5适用于桌面上位机和客户端

ucos ii实现

ucos实现起来很简单,使用post/pend系列函数可以轻松实现,因为这类函数本身就带有超时控制。

INT8U OSMboxPost(OS_EVENT *pevent,void *pmsg);
INT8U OSMboxPend(OS_EVENT *pevent,INT32U timeout,INT8U *perr);

所以使用信号量或者邮箱能够很轻松的实现上面的要求。

linux实现

linux的线程同步可以使用条件变量,条件变量需要配合互斥锁使用。
互斥锁:
- pthread_mutex_init
- pthread_mutex_lock
- pthread_mutex_unlock

条件变量:
- pthread_cond_init
- pthread_cond_signal
- pthread_cond_wait
- pthread_cond_timewait

pthread_cond_wait会无限制的等待,直到触发为止,而pthread_cond_timewait则带有时间参数,正好满足我们的要求。
pthread_cond_timewait函数原型是:

int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);

在使用pthread_cond_timewait时,我们关心两点:
- struct timespec *restrict abstime参数
- int返回值

linux系统中通常有四种表示时间的方法:
- time_t
- struct timeval
- struct timespec
- struct tm

time_t就是一个长整型,表示当前时间距离1970-01-01 00:00:00 +0000 (UTC)的秒数。所以精度为秒
struct timeval是一个结构体,精度为微秒

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

struct timespec跟struct timeval一样,只不过精度更高,为纳秒

struct timespec
{
    __time_t tv_sec;        /* Seconds. */
    long int tv_nsec;       /* Nanoseconds. */
};

struct tm 直接用秒、分、小时、天、月、年等来表示时间。

struct tm
{
  int tm_sec;            /* Seconds.    [0-60] (1 leap second) */
  int tm_min;            /* Minutes.    [0-59] */
  int tm_hour;           /* Hours.    [0-23] */
  int tm_mday;           /* Day.        [1-31] */
  int tm_mon;            /* Month.    [0-11] */
  int tm_year;           /* Year    - 1900. */
  int tm_wday;           /* Day of week.    [0-6] */
  int tm_yday;           /* Days in year.[0-365]    */
  int tm_isdst;          /* DST.        [-1/0/1]*/

#ifdef    __USE_BSD
  long int tm_gmtoff;        /* Seconds east of UTC. */
  __const char *tm_zone;     /* Timezone abbreviation. */
#else
  long int __tm_gmtoff;       /* Seconds east of UTC. */
  __const char *__tm_zone;    /* Timezone abbreviation. */
#endif
};

相关的函数:

time_t time(time_t *t);
可以获取精确到秒的当前距离1970-01-01 00:00:00 +0000 (UTC)的秒数。
#include
int gettimeofday(struct timeval *tv, struct timezone *tz);
可以获取精确到微秒当前距离1970-01-01 00:00:00 +0000 (UTC)的微秒数。
#include
int clock_gettime(clockid_t clk_id, struct timespec *tp);
可以获取精确到纳秒当前距离1970-01-01 00:00:00 +0000 (UTC)的纳秒数。

好了,我们回到pthread_cond_timedwait上来,它需要一个struct timespec表示延迟时间,我们可以这样操作:

struct timespec ts;
(void) pthread_mutex_lock(&t.mn);
clock_gettime(CLOCK_REALTIME, &ts);                 //获取当前时间
ts.tv_sec += 5;                                     //在当前时间基础上增加5s,即延时5s
rc = pthread_cond_timedwait(&t.cond, &t.mn, &ts);   //线程等待
if (rc == 0){
    //成功响应
}
else if(rc==ETIMEDOUT){
    //超时
}
else{
    //其他错误
}
(void) pthread_mutex_unlock(&t.mn);

再来说说pthread_cond_timedwait的返回值:
- 0,响应成功
- ETIMEDOUT,超时
- EINVAL,说明cond, mutex,或者abstime的值是无效的
- EPERM,说明当前线程在调用pthread_cond_timedwait时不是mutex的拥有者。

qt5实现

通过上面的讲解我们可以看出ucos和linux的线程操作方式还是很相似的。但是qt就完全不同了,我们进行qt开发的时候,时刻要记住一点就是:qt是基于事件驱动的。因此我们一定要保证:当qt什么都不做的时候,一定是在进行事件轮询。要不然整个程序就死了。
下面直接进入主题
qt通讯以串口为例,串口接收通常以槽函数来实现

my_port=new QSerialPort();
...
connect(my_port,SIGNAL(readyRead()),this,SLOT(slt_spReceive()));

将串口接收绑定到slt_spReceive上。
发送数据的时候

pkg_send(sendBa);
stat=ackwait(timeouts);
if(stat==-1)
{
    //超时
}
else
{
    //正确响应
}

这段代码中pkg_send(sendBa)发送玩数据后,ackwait(timeouts)开始等待timeouts,这是一个伪阻塞函数,也就是说,对于当前函数而言,不会立即执行后面的if判断,而是还阻塞在此处。但是前面也说了,当qt什么都不做的时候,一定是在进行事件轮询,所以在阻塞的时候,一定要进行时间轮询式的阻塞,而且这个阻塞还要能够被其他时间打断,比如定时器超时,比如数据正确响应信号。那么我们就要想到一个东西:局部时间循环,即QEventLoop。
比如我们的ackwait函数可以这么写

int SendFile::ackwait(int to)     //响应等待
{
    QEventLoop loop;
    QTimer timeout_t;
    timeout_t.setSingleShot(true);
    connect(&timeout_t,SIGNAL(timeout()),&loop,SLOT(quit()));           //设置超时信号
    connect(this,SIGNAL(sgn_ackRight()),&loop,SLOT(quit()));            //设置响应正确信号
    timeout_t.start(to);                                                //启动超时定时器
    loop.exec();

    if(timeout_t.isActive())
    {
        qDebug()<<"ack ok";
        return 1;
    }
    else
    {
        qDebug()<<"timeout";
        return -1;
    }
}

首先定义了一个事件循环QEventLoop loop;
然后定义了一个定时器QTimer timeout_t作为超时计时
然后绑定两个信号来打断局部事件循环

connect(&timeout_t,SIGNAL(timeout()),&loop,SLOT(quit()));           //设置超时信号
connect(this,SIGNAL(sgn_ackRight()),&loop,SLOT(quit()));            //设置响应正确信号

然后开启定时器计时
再开启局部事件循环
然后等待事件循环被打断
然后判断是什么事件打断的,做出响应的处理。

当然在接收槽函数中,当接收到等待的数据时,需要发送响应信号。

void SendFile::slt_spReceive()
{
    ...
    //接收到等待的数据
    sgn_ackRight();
}

猜你喜欢

转载自blog.csdn.net/qiuzhiqian1990/article/details/62427956