网络编程中的select实现超时检测和通用API

关于select的基础知识在上一篇文章已经初步总结了,他的应用主要分为三个方向:

  1. 用于完成超时检测(connect、accept、read以及write)
  2. 用于处理客户端的普通文件描述符和套接字文件描述符,监控这些描述符可以避免在服务器死掉以后,客户端阻塞在等待标准输入或者其他文件的读取。
  3. 用于服务器构造多并发,使用select可以实现单进程实现多并发!只不过是自己管理所有建立的用于通信的链接和用于监听的套接字,有别于poll和epoll(底层自己管理)

实现connect超时检测

首先要明确以下知识点:

  • 在建立套接字(fd)以后默认是阻塞的
  • 如果客户端连接服务器发生异常,则默认阻塞的情况下,返回时间是1.5RTT,大约100秒!–软件质量低下
  • 先把套接字通过fcntl变为非阻塞模型,再调用connect函数
    • 如果网络顺利,直接建立链接
    • 如果网络不好,则根据返回值判断,connect的返回值如果是-1而且errno==EINPROGRESS表示客户端和服务器正在建立连接,需要等待一段时间才能建立链接
    • 等待的时间我们可以自己设定
    • 利用select监控该套接字—提高软件质量
  • 尽管select返回了套接字的可写状态(套接字默认是可写入状态,正常建立连接以后也是表返回可写入状态)但不一定表示就是正确建立链接
    • select返回大于零的值
    • 真正建立链接
    • 建立套接字产生错误,会返回写失败信息,造成可写入的状态。 此时错误信息不会保存至errno变量中,因此,需要调用getsockopt来获取。

/**
 * activate_noblock - 设置I/O为非阻塞模式
 * @fd: 文件描符符
 */
int activate_nonblock(int fd)
{
  int ret = 0;

  int flags = fcntl(fd,F_GETFL);
  if(-1 == flags)
  {
    ret =flags;
    perror("fcntl");
    return ret;
  }

  flags |= O_NONBLOCK;

  ret = fcntl(fd,F_SETFL,flags);
  if(ret == -1)
  {
    perror("fcntl(fd,F_SETFL,flags)");
    return ret;
  }

  return ret;
}

/**
 * deactivate_nonblock - 设置I/O为阻塞模式
 * @fd: 文件描符符
 */
int deactivate_nonblock(int fd)
{
  int ret = 0;

  int flags = fcntl(fd,F_GETFL);
  if(-1 == flags)
  {
    ret =flags;
    perror("fcntl");
    return ret;
  }

  flags &= ~O_NONBLOCK;

  ret = fcntl(fd,F_SETFL,flags);
  if(ret == -1)
  {
    perror("fcntl(fd,F_SETFL,flags)");
    return ret;
  }

  return ret;
}

/**
 * connect_timeout - connect
 * @fd: 套接字
 * @addr: 要连接的对方地址
 * @wait_seconds: 等待超时秒数,如果为0表示正常模式
 * 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT
 */
static int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
{
  int ret = 0;
  socklen_t addrlen = sizeof(struct sockaddr_in);
  fd_set connect_fdset;
  struct timeval timeout;

  int err;
    socklen_t socklen = sizeof(err);
    int sockoptret; 

  if(wait_seconds > 0)//设置成非阻塞--因为文件描述符默认是阻塞的
  {
    activate_nonblock(fd);
  }


  /*非阻塞
  --成功:立马建立连接
  --失败:ret < 0 && errno == EINPROGRESS,表示没有获取到链接
  */
  ret = connect(fd,(struct sockaddr*)addr,addrlen);
  if(ret < 0 && errno == EINPROGRESS)
  {
    FD_ZERO(&connect_fdset);
    FD_SET(fd,&connect_fdset);

    timeout.tv_sec = wait_seconds;
    timeout.tv_usec = 0;

    do
    {
      // 一但连接建立,则套接字就可写  所以connect_fdset放在了写集合中
      ret = select(fd + 1,NULL,&connect_fdset,NULL,&timeout);//在规定时间内监控链接
    }while(ret < 0 && errno == EINTR);

    if(ret == 0)//超时
    {
      ret = -1;
      errno = ETIMEDOUT;
    }
    else if (ret < 0)//select出错
    {
      return -1;
    }
    else if( ret == 1)//有两种情况会导致文件描述符变为可写入的状态/准备好的状态
    {
      /* ret返回为1(表示套接字可写),可能有两种情况,一种是连接建立成功,一种是套接字产生错误,*/
            /* 此时错误信息不会保存至errno变量中,因此,需要调用getsockopt来获取。 */
      sockoptret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &socklen);//获取套接字的状态
      if(sockoptret == -1)//getsockopt(可以修改十一种状态,诸如TIME_WAIT的时间等)调用失败
      {
        return -1;
      }
      if(err == 0)//真正可写入/准备好
        ret = 0;
      else{//-1表示套接字产生错误
        errno = err;
        ret = -1;
      }
    }
  }

  if (wait_seconds > 0)
    {
        deactivate_nonblock(fd);
    }

  return ret;
}

实现accept超时检测

/**
 * accept_timeout - 带超时的accept
 * @fd: 套接字
 * @addr: 输出参数,返回对方地址
 * @wait_seconds: 等待超时秒数,如果为0表示正常模式
 * 成功(未超时)返回已连接套接字,超时返回-1并且errno = ETIMEDOUT
 */
int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
{
    int ret;
    socklen_t addrlen = sizeof(struct sockaddr_in);

    if (wait_seconds > 0)
    {
        fd_set accept_fdset;
        struct timeval timeout;
        FD_ZERO(&accept_fdset);
        FD_SET(fd, &accept_fdset);
        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;
        do
        {
            ret = select(fd + 1, &accept_fdset, NULL, NULL, &timeout);
        } while (ret < 0 && errno == EINTR);
        if (ret == -1)
            return -1;
        else if (ret == 0)
        {
            errno = ETIMEDOUT;
            return -1;
        }
    }

实现read超时检测

/**
 * read_timeout - 读超时检测函数,不含读操作
 * @fd: 文件描述符
 * @wait_seconds: 等待超时秒数,如果为0表示不检测超时
 * 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT
 */
int read_timeout(int fd, unsigned int wait_seconds)
{
    int ret = 0;
    if (wait_seconds > 0)
    {
        fd_set read_fdset;
        struct timeval timeout;

        FD_ZERO(&read_fdset);
        FD_SET(fd, &read_fdset);

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;

        //select返回值三态
        //1 若timeout时间到(超时),没有检测到读事件 ret返回=0
        //2 若ret返回<0 &&  errno == EINTR 说明select的过程中被别的信号中断(可中断睡眠原理)
        //2-1 若返回-1,select出错
        //3 若ret返回值>0 表示有read事件发生,返回事件发生的个数

        do
        {
            ret = select(fd + 1, &read_fdset, NULL, NULL, &timeout);
        } while (ret < 0 && errno == EINTR); 

        if (ret == 0)
        {
            ret = -1;
            errno = ETIMEDOUT;
        }
        else if (ret == 1)
            ret = 0;
    }

    return ret;
}

实现write超时检测

/**
 * write_timeout - 写超时检测函数,不含写操作
 * @fd: 文件描述符
 * @wait_seconds: 等待超时秒数,如果为0表示不检测超时
 * 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT
 */
int write_timeout(int fd, unsigned int wait_seconds)
{
    int ret = 0;
    if (wait_seconds > 0)
    {
        fd_set write_fdset;
        struct timeval timeout;

        FD_ZERO(&write_fdset);
        FD_SET(fd, &write_fdset);

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;
        do
        {
            ret = select(fd + 1, NULL, &write_fdset, NULL, &timeout);
        } while (ret < 0 && errno == EINTR);

        if (ret == 0)
        {
            ret = -1;
            errno = ETIMEDOUT;
        }
        else if (ret == 1)
            ret = 0;
    }

    return ret;
}

利用select实现的通用套接字编程库

  • 头文件
#ifndef __COMSOCKET_H__
#define __COMSOCKET_H__

#ifdef __cplusplus

extern "C"
{
#endif


#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <fcntl.h>

//错误码定义  

#define Sck_Ok              0
#define Sck_BaseErr         3000

#define Sck_ErrParam                    (Sck_BaseErr+1)
#define Sck_ErrTimeOut                  (Sck_BaseErr+2)
#define Sck_ErrPeerClosed               (Sck_BaseErr+3)
#define Sck_ErrMalloc                   (Sck_BaseErr+4)

//函数声明

//客户端环境低级初始化
int sckClient_init01(void **handle,char * ip,int port,int con_time,int send_time,int recv_time);


//客户端环境高级初始化
int sckClient_init(void **handle,int con_time,int send_time,int recv_time,int cnt);

//客户端获取链接
int sckClient_getConn(void *handle,char * ip,int port,int * con_fd);


//客户端关闭连接
int sckClient_closeConn(int  con_fd);

//客户端发送报文
int sckClient_send(void *handle, int con_fd,unsigned char *data, int datalen);

//客户端端接受报文
int sckClient_rcv(void *handle,int con_fd, unsigned char *out, int *outlen); //1


// 客户端环境释放 
void sckClient_destroy(void **handle);
/*=========================================================================================*/

//函数声明
//服务器端初始化
int sckServer_init(int port, int *listenfd);

int sckServer_accept(int listenfd, int *con_fd,  int timeout);
//服务器端发送报文
int sckServer_send(int con_fd,  unsigned char *data, int datalen, int timeout);
//服务器端端接受报文
int sckServer_rcv(int  con_fd, unsigned char *out, int *outlen,  int timeout); 

//服务器端环境释放 
int sckServer_destroy(int con_fd);

#ifdef __cplusplus
}
#endif
#endif
  • 实现文件

#include "comsocket.h"

typedef struct _sckHandle
{
  int sock_fd;
  int arrayNum;
  int con_time;
  int send_time;
  int recv_time;
}SckHandle;


/**
 * readn - 读取固定字节数
 * @fd: 文件描述符
 * @buf: 接收缓冲区
 * @count: 要读取的字节数
 * 成功返回count,失败返回-1,读到EOF返回<count
 */
ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;//剩余字节数
    ssize_t nread;//已经读取的字节数
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nread = read(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nread == 0)
            return count - nleft;

        bufp += nread;
        nleft -= nread;
    }

    return count;
}

/**
 * writen - 发送固定字节数
 * @fd: 文件描述符
 * @buf: 发送缓冲区
 * @count: 要读取的字节数
 * 成功返回count,失败返回-1
 */
ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nwritten == 0)
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }

    return count;
}

/**
 * recv_peek - 仅仅查看套接字缓冲区数据,但不移除数据
 * @sockfd: 套接字
 * @buf: 接收缓冲区
 * @len: 长度
 * 成功返回>=0,失败返回-1
 */
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
    while (1)
    {
        int ret = recv(sockfd, buf, len, MSG_PEEK);
        if (ret == -1 && errno == EINTR)
            continue;
        return ret;
    }
}

/**
 * activate_noblock - 设置I/O为非阻塞模式
 * @fd: 文件描符符
 */
int activate_nonblock(int fd)
{
  int ret = 0;

  int flags = fcntl(fd,F_GETFL);
  if(-1 == flags)
  {
    ret =flags;
    perror("fcntl");
    return ret;
  }

  flags |= O_NONBLOCK;

  ret = fcntl(fd,F_SETFL,flags);
  if(ret == -1)
  {
    perror("fcntl(fd,F_SETFL,flags)");
    return ret;
  }

  return ret;
}

/**
 * deactivate_nonblock - 设置I/O为阻塞模式
 * @fd: 文件描符符
 */
int deactivate_nonblock(int fd)
{
  int ret = 0;

  int flags = fcntl(fd,F_GETFL);
  if(-1 == flags)
  {
    ret =flags;
    perror("fcntl");
    return ret;
  }

  flags &= ~O_NONBLOCK;

  ret = fcntl(fd,F_SETFL,flags);
  if(ret == -1)
  {
    perror("fcntl(fd,F_SETFL,flags)");
    return ret;
  }

  return ret;
}



/**
 * connect_timeout - connect
 * @fd: 套接字
 * @addr: 要连接的对方地址
 * @wait_seconds: 等待超时秒数,如果为0表示正常模式
 * 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT
 */
static int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
{
  int ret = 0;
  socklen_t addrlen = sizeof(struct sockaddr_in);
  fd_set connect_fdset;
  struct timeval timeout;

  int err;
    socklen_t socklen = sizeof(err);
    int sockoptret; 

  if(wait_seconds > 0)//设置成非阻塞--因为文件描述符默认是阻塞的
  {
    activate_nonblock(fd);
  }


  /*非阻塞
  --成功:立马建立连接
  --失败:ret < 0 && errno == EINPROGRESS,表示没有获取到链接
  */
  ret = connect(fd,(struct sockaddr*)addr,addrlen);
  if(ret < 0 && errno == EINPROGRESS)
  {
    FD_ZERO(&connect_fdset);
    FD_SET(fd,&connect_fdset);

    timeout.tv_sec = wait_seconds;
    timeout.tv_usec = 0;

    do
    {
      // 一但连接建立,则套接字就可写  所以connect_fdset放在了写集合中
      ret = select(fd + 1,NULL,&connect_fdset,NULL,&timeout);//在规定时间内监控链接
    }while(ret < 0 && errno == EINTR);

    if(ret == 0)//超时
    {
      ret = -1;
      errno = ETIMEDOUT;
    }
    else if (ret < 0)//select出错
    {
      return -1;
    }
    else if( ret == 1)//有两种情况会导致文件描述符变为可写入的状态/准备好的状态
    {
      /* ret返回为1(表示套接字可写),可能有两种情况,一种是连接建立成功,一种是套接字产生错误,*/
            /* 此时错误信息不会保存至errno变量中,因此,需要调用getsockopt来获取。 */
      sockoptret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &socklen);//获取套接字的状态
      if(sockoptret == -1)//getsockopt(可以修改十一种状态,诸如TIME_WAIT的时间等)调用失败
      {
        return -1;
      }
      if(err == 0)//真正可写入/准备好
        ret = 0;
      else{//-1表示套接字产生错误
        errno = err;
        ret = -1;
      }
    }
  }

  if (wait_seconds > 0)
    {
        deactivate_nonblock(fd);
    }

  return ret;
}


/**
 * write_timeout - 写超时检测函数,不含写操作
 * @fd: 文件描述符
 * @wait_seconds: 等待超时秒数,如果为0表示不检测超时
 * 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT
 */
int write_timeout(int fd, unsigned int wait_seconds)
{
    int ret = 0;
    if (wait_seconds > 0)
    {
        fd_set write_fdset;
        struct timeval timeout;

        FD_ZERO(&write_fdset);
        FD_SET(fd, &write_fdset);

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;
        do
        {
            ret = select(fd + 1, NULL, &write_fdset, NULL, &timeout);
        } while (ret < 0 && errno == EINTR);

        if (ret == 0)
        {
            ret = -1;
            errno = ETIMEDOUT;
        }
        else if (ret == 1)
            ret = 0;
    }

    return ret;
}


/**
 * read_timeout - 读超时检测函数,不含读操作
 * @fd: 文件描述符
 * @wait_seconds: 等待超时秒数,如果为0表示不检测超时
 * 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT
 */
int read_timeout(int fd, unsigned int wait_seconds)
{
    int ret = 0;
    if (wait_seconds > 0)
    {
        fd_set read_fdset;
        struct timeval timeout;

        FD_ZERO(&read_fdset);
        FD_SET(fd, &read_fdset);

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;

        //select返回值三态
        //1 若timeout时间到(超时),没有检测到读事件 ret返回=0
        //2 若ret返回<0 &&  errno == EINTR 说明select的过程中被别的信号中断(可中断睡眠原理)
        //2-1 若返回-1,select出错
        //3 若ret返回值>0 表示有read事件发生,返回事件发生的个数

        do
        {
            ret = select(fd + 1, &read_fdset, NULL, NULL, &timeout);
        } while (ret < 0 && errno == EINTR); 

        if (ret == 0)
        {
            ret = -1;
            errno = ETIMEDOUT;
        }
        else if (ret == 1)
            ret = 0;
    }

    return ret;
}


//客户端环境低级初始化
int sckClient_init01(void **handle,char * ip,int port,int con_time,int send_time,int recv_time)
{
  int ret = 0;
  SckHandle * tmp = NULL;
  int sock;
  int con;
  struct sockaddr_in serv_addr;

  if(handle == NULL || con_time < 0 || send_time < 0 || recv_time < 0
    || ip == NULL|| port < 0||port > 65535)
  {
    ret = Sck_ErrParam;
    perror("sckClient_init:(handle == NULL || con_time < 0 || send_time < 0 || recv_time < 0)");
    return ret;
  }


  tmp = (SckHandle *)malloc(sizeof(SckHandle));
  if(tmp == NULL)
  {
    ret =  Sck_ErrMalloc;
    perror("malloc(sizeof(SckHandle)");
    return ret;
  }

  tmp->con_time = con_time;
  tmp->send_time = send_time;
  tmp->recv_time = recv_time;

  sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
  if(sock < 0)
  {
    ret = errno;
    perror("socket");
    return ret;
  }

  bzero(&serv_addr,sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_port = htons(port);
  serv_addr.sin_addr.s_addr = inet_addr(ip);

  if((con = connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))) < 0)
  {
    ret = errno;
    perror("connect");
    return ret;
  }
  //tmp->con_fd = con;
  tmp->sock_fd = sock;

  *handle = tmp;

  return ret;
}

//客户端环境高级初始化
//成功返回0,失败返回相应的错误码
//参数分别是SckHandle的结构体成员
int sckClient_init(void **handle,int con_time,int send_time,int recv_time,int cnt)
{
  int ret = 0;
  SckHandle * tmp = NULL;

  if(handle == NULL || con_time < 0 || send_time < 0 || recv_time < 0)//合法性检测
  {
    ret = Sck_ErrParam;
    printf("sckClient_init:(handle == NULL || con_time < 0 || send_time < 0 || recv_time < 0)");
    return ret;
  }

  /*分配堆空间内存--临时变量*/
  tmp = (SckHandle *)malloc(sizeof(SckHandle));
  if(tmp == NULL)
  {
    ret =  Sck_ErrMalloc;
    perror("malloc(sizeof(SckHandle)");
    return ret;
  }

  //进行初始化
  tmp->con_time = con_time;
  tmp->send_time = send_time;
  tmp->recv_time = recv_time;
  tmp->arrayNum = cnt;

  //将分配好的结果甩出去
  *handle = tmp;

  return ret;
}


//客户端获取链接
//成功:返回0,失败返回错误代码
//参数是服务器地址以及已经初始化好的句柄和准备建立的套接字
int sckClient_getConn(void *handle,char * ip,int port,int * con_fd)
{
  int ret = 0;
  struct sockaddr_in serv_addr;
  SckHandle * tmp = (SckHandle *)handle;
  int sock;

  //合法性检测
  if(handle == NULL || ip == NULL || con_fd == NULL 
    || port < 0 || port  > 65535)
    {
      ret = Sck_ErrParam;
      printf("sckClient_getConn(handle == NULL || ip == NULL || con_fd == NULL port < 0 || port  > 65535)\n");
      return ret;
    }

  //建立套接字
  sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
  if(sock < 0)
  {
    ret = errno;
    perror("socket");
    return ret;
  }

  //将建立好的套接字赋值给临时变量
  tmp->sock_fd = sock;

  //设置要连接的服务器地址
  bzero(&serv_addr,sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_port = htons(port);
  serv_addr.sin_addr.s_addr = inet_addr(ip);

  /*if(connect(tmp->sock_fd,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) < 0)
  {
    ret = errno;
    perror("connect");
    return ret;
  }*/

  //连接服务器
  // 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT
  ret = connect_timeout(tmp->sock_fd, (struct sockaddr_in*) (&serv_addr), (unsigned int )tmp->con_time);
  if (ret < 0)//连接服务器失败
  {
    if (ret==-1 && errno == ETIMEDOUT)//连接超时
    {
        ret = Sck_ErrTimeOut;
        return ret;
    }
    else//其他情况的连接失败
    {
        printf("func connect_timeout() err:  %d\n", ret);
    }
  }
  *con_fd = tmp->sock_fd;  //将建立好的套接字甩出去
  return ret;
}


//客户端关闭连接
//参数:要关闭的套接字
int sckClient_closeConn(int  con_fd)
{
  if(con_fd > 0)
  {
    close(con_fd);
  }
  return 0;
}

//客户端发送报文
//成功:返回0,失败:返回错误代码(-1或者其他非零值)
//参数:句柄、已经建立的套接字、要发送数据缓冲区的地址、要发送的数据长度
int sckClient_send(void *handle, int con_fd,unsigned char *data, int datalen)
{
  int ret = 0;
  SckHandle * tmp = (SckHandle *)handle;
  unsigned char * pkt = NULL;

  int nwrite = 0;
  int pkt_len = 0;

  //合法性检测
  if(handle == NULL || data == NULL
    || con_fd < 0 || con_fd > 65535 || datalen <0)
    {
      ret = Sck_ErrParam;
      perror("Sck_ErrParam");
      return ret;
    }

  //写超时检测
  // 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT
  ret = write_timeout(con_fd,tmp->send_time);

  if(ret == 0)//没有超时
  {
    pkt = (unsigned char *)malloc(datalen + sizeof(int));//分配堆空间内存--临时数据包
    if(pkt == NULL)
    {
      ret = Sck_ErrMalloc;
      perror("malloc(datalen + sizeof(int))");
      return ret;
    }

    pkt_len = htonl(datalen);//包头(数据包长度)字节序转换
    memcpy(pkt,&pkt_len,sizeof(int));//复制转换后表示长度的数据到包头
    memcpy(pkt+sizeof(int),data,datalen);//复制有效数据到包体

    nwrite = writen(con_fd,pkt,datalen + sizeof(int));//向服务器发送数据
    if(nwrite < (datalen + sizeof(int)))//写入出错
    {
      if(pkt != NULL)//回收内存
      {
        free(pkt);
        pkt = NULL;
      }
      return nwrite;
    }

  }
  else if(ret < 0)//在检测超时的时候调用select失败
  {
    if(ret == -1 && errno == ETIMEDOUT)//确实超时了
    {
      ret = Sck_ErrTimeOut;
      printf("write_timeout(con_fd,tmp->send_time)");
      return ret;
    } 
    return ret;//其他失败的情况
  }

  //内存回收
  free(pkt);
  pkt = NULL;
  return ret;
}

/*typedef struct _pkt
{
  int len;
  unsigned char buf[1024];
}pkt_t;
*/
//客户端端接受报文
//成功:返回0,失败:返回错误代码或者实际读取到的字节数,但是该数字小于包头存储的数字
//参数:句柄、已经建立好的套接字,目的缓冲区地址、正确读取以后读取到的字节数
int sckClient_rcv(void *handle,int con_fd, unsigned char *out, int *outlen)
{
  int ret = 0;
  SckHandle * tmp = (SckHandle *)handle;

  int tmp_len ;

  //合法性检测
  if(handle == NULL || out == NULL
    || outlen == NULL||con_fd < 0 || con_fd > 65535)
    {
      ret = Sck_ErrParam;
      perror("Sck_ErrParam");
      return ret;
    }


  //超时检测  
  ret = read_timeout(con_fd, tmp->recv_time);
  if(ret == -1)//调用select失败
  {
    if(errno == ETIMEDOUT)//确实超时了
    {
      ret = Sck_ErrTimeOut;
      perror("read_timeout");
      return ret;
    }
    perror("read_timeout");//其他情况的失败
    return ret;
  }
  else if(ret == 0)//没有超时
  {
    //成功返回count,失败返回-1,读到EOF返回<count
    ret = readn(con_fd,&tmp_len,sizeof(tmp_len));//读取包头--表示有效数据长度
    if(ret == -1)//读取失败 
    {
      printf("func readn() err:%d\n",ret);
      return ret;
    }
    else if(ret < sizeof(tmp_len))//读取结束--没有数据可读--对方关闭连接
    {
      ret = Sck_ErrPeerClosed;
      printf("client close\n");
      return ret ;//返回读取到的字节数--没用的字节数--表示错误
    }

    tmp_len = ntohl(tmp_len);//字节序转换得到包体长度
    ret = readn(con_fd,out,tmp_len);//读取指定长度的有效数据
    if(ret == -1)//读取失败
    {
      printf("func readn() err:%d\n",ret);
      return ret;
    }
    else if(ret < tmp_len)//对方关闭连接
    {
      ret = Sck_ErrPeerClosed;
      printf("client close\n");
      return ret ;//返回读取到的字节数--没用的字节数--表示错误
    }

  }

  *outlen = tmp_len;

  return 0;//最后一定要返回0,因为ret在最后变成了readn函数的返回结果--读取到的字节数
}

// 客户端环境释放 
//参数:句柄

void sckClient_destroy(void **handle)
{
  if(handle != NULL)
  {
    free(*handle);
    *handle = NULL;
  }
}

/*===================================================================================*/

//服务器端初始化
//成功:返回0,失败:返回错误码
//参数:要绑定的端口号、准备建立的监听套接字
int sckServer_init(int port, int *listenfd)
{
  int   ret = 0;
    int mylistenfd;
    struct sockaddr_in servaddr;

  //初始化服务器地址
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(port);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    //建立监听用的套接字 
    mylistenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (mylistenfd < 0)
    {
        ret = errno ;
        printf("func socket() err:%d \n", ret);
        return ret;
    }


  //设置地址复用
    int on = 1;
    ret = setsockopt(mylistenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) );
    if (ret < 0)
    {
        ret = errno ;
        printf("func setsockopt() err:%d \n", ret);
        return ret;
    }


  //绑定地址/端口和监听用的套接字
    ret = bind(mylistenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
    if (ret < 0)
    {
        ret = errno ;
        printf("func bind() err:%d \n", ret);
        return ret;
    }

  //开始监听    
    ret = listen(mylistenfd, SOMAXCONN);
    if (ret < 0)
    {
        ret = errno ;
        printf("func listen() err:%d \n", ret);
        return ret;
    }

  //将建立好的监听套接字甩出去   
    *listenfd = mylistenfd;

    return ret;//成功初始化返回0,否则返回出错步骤返回的错误代码
}


/**
 * accept_timeout - 带超时的accept
 * @fd: 套接字
 * @addr: 输出参数,返回对方地址
 * @wait_seconds: 等待超时秒数,如果为0表示正常模式
 * 成功(未超时)返回已连接套接字,超时返回-1并且errno = ETIMEDOUT
 */
int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
{
    int ret;
    socklen_t addrlen = sizeof(struct sockaddr_in);

    if (wait_seconds > 0)
    {
        fd_set accept_fdset;
        struct timeval timeout;
        FD_ZERO(&accept_fdset);
        FD_SET(fd, &accept_fdset);
        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;
        do
        {
            ret = select(fd + 1, &accept_fdset, NULL, NULL, &timeout);
        } while (ret < 0 && errno == EINTR);
        if (ret == -1)
            return -1;
        else if (ret == 0)
        {
            errno = ETIMEDOUT;
            return -1;
        }
   return ret ;
    }

    //一但检测出 有select事件发生,表示对等方完成了三次握手,客户端有新连接建立
    //此时再调用accept将不会堵塞
    if (addr != NULL)
        ret = accept(fd, (struct sockaddr*)addr, &addrlen); //返回已连接套接字
    else
        ret = accept(fd, NULL, NULL);
        if (ret == -1)
        {
            ret = errno;
            printf("func accept() err:%d \n", ret);
            return ret;
        }


    return ret;
}

//服务器端接受客户端连接请求
//成功:返回0,失败:返回错误码
//参数:已经建立好的用于监听的套接字,准备建立的用于连接的套接字文件描述符--连接、超时时间值
int sckServer_accept(int listenfd, int *con_fd,  int timeout)
{
  int   ret = 0;

  //超时检测
  //成功(未超时)返回已连接套接字,超时返回-1并且errno = ETIMEDOUT
    ret = accept_timeout(listenfd, NULL, (unsigned int) timeout);//这里可以改进--这里没有保存客户端的地址
    if (ret < 0)
    {
        if (ret == -1 && errno == ETIMEDOUT)//确实超时
        {
            ret = Sck_ErrTimeOut;
            printf("func accept_timeout() timeout err:%d \n", ret);
            return ret;
        }
        else//其他select失败的情况
        {
            ret = errno;
            printf("func accept_timeout() err:%d \n", ret);
            return ret;
        }
    }

    *con_fd = ret;//将从完成三次握手的队列里面取出可用的连接甩出去
    return ret;//成功返回0,失败返回错误代码
}

//服务器端发送报文
//成功:返回0,失败:返回错误代码或者实际写入的字节数
//参数:已经建立好的连接,待发送数据、要发送长度、超时时间
int sckServer_send(int con_fd,  unsigned char *data, int datalen, int timeout)
{
  int   ret = 0;
  int writed = 0;
  unsigned char *netdata = NULL;
  //超时检测
  // 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT
    ret = write_timeout(con_fd, timeout);
    if (ret == 0)//没有超时
    {
        writed = 0;
        netdata = ( unsigned char *)malloc(datalen + 4);//分配空间
        if ( netdata == NULL)
        {
            ret = Sck_ErrMalloc;
            printf("func sckServer_send() mlloc Err:%d\n ", ret);
            return ret;
        }
    //数据打包
        int netlen = htonl(datalen);
        memcpy(netdata, &netlen, 4);
        memcpy(netdata+4, data, datalen);

    //数据发送
        writed = writen(con_fd, netdata, datalen + 4);
        if (writed < (datalen + 4) )//对方关闭
        {
            if (netdata != NULL) 
            {
                free(netdata);//内存回收
                netdata = NULL;
            }
            return writed;//返回实际写入的数据--非零都表示发送失败
        }

    }
    else if (ret < 0)
    {
        //失败返回-1,超时返回-1并且errno = ETIMEDOUT
        if (ret == -1 && errno == ETIMEDOUT)//确实超时
        {
            ret = Sck_ErrTimeOut;
            printf("func sckServer_send() mlloc Err:%d\n ", ret);
            return ret;
        }
        return ret;//其他情况的select失败。ret非零,但不一定是-1
    }


  free(netdata);//内存回收
    netdata = NULL;
    return ret;
}


//服务器端端接受报文
//成功:返回0,失败:返回错误代码或者实际读取到的字节数,但是该数字小于包头存储的数字
//参数:已经建立好的连接,目的缓冲区地址、正确读取以后读取到的字节数,超时时间
int sckServer_rcv(int  con_fd, unsigned char *out, int *outlen,  int timeout)
{
  int       ret = 0;

  //合法性检测
    if (out==NULL || outlen==NULL)
    {
        ret = Sck_ErrParam;
        printf("func sckClient_rev() timeout , err:%d \n", Sck_ErrTimeOut);
        return ret;
    }

    ret =  read_timeout(con_fd, timeout); //超时检测
    if (ret != 0)//select调用失败
    {
        if (ret==-1 || errno == ETIMEDOUT)//确实超时了
        {
            ret = Sck_ErrTimeOut;
            printf("func sckClient_rev() timeout , err:%d \n", Sck_ErrTimeOut);
            return ret;
        }
        else//其他情况的失败
        {
            printf("func sckClient_rev() timeout , err:%d \n", Sck_ErrTimeOut);
            return ret;
        }   
    }
    //没有超时
    int netdatalen = 0;
  ret = readn(con_fd, &netdatalen,  4); //读包头 4个字节
    if (ret == -1)//读取失败
    {
        printf("func readn() err:%d \n", ret);
        return ret;
    }
    else if (ret < 4)//对方关闭
    {
        ret = Sck_ErrPeerClosed;
        printf("func readn() err peer closed:%d \n", ret);
        return ret;
    }

    int n;
    n = ntohl(netdatalen);//字节序转换
    ret = readn(con_fd, out, n); //根据长度读数据
    if (ret == -1)//读取失败
    {
        printf("func readn() err:%d \n", ret);
        return ret;
    }
    else if (ret < n)//对方关闭
    {
        ret = Sck_ErrPeerClosed;
        printf("func readn() err peer closed:%d \n", ret);
        return ret;//返回读取到的字节数--没用的字节数--表示错误
    }


    *outlen = n;//将正确读取以后读取到的字节数甩出去

    return 0;//最后一定要返回0,因为ret在最后变成了readn函数的返回结果--读取到的实际字节数
}

//服务器端环境释放 
//成功:返回,失败:返回-1
//准备关闭的链接
int sckServer_destroy(int con_fd)
{
  if(close(con_fd) == 0)
    return 0;
  else 
    return -1;
}
  • 客户端测试代码


#include "comsocket.h"



int main()
{
  void * handle = NULL;
  int ret = 0;
  unsigned char *data = (unsigned char *)"asdfghjklaedas";
  unsigned char out[1024] = {0};
  int datalen = 10;
  int outlen = 1024;
  int con_fd;
  //客户端环境初始化
  ret = sckClient_init(&handle,5,5,5,1);

  ret = sckClient_getConn(handle,"192.168.1.110",8001,&con_fd);


  //客户端发送报文
  ret = sckClient_send(handle,con_fd, data,datalen);

  //客户端端接受报文
  ret = sckClient_rcv(handle,con_fd,out, &outlen); 
  out[outlen] = '\0';
    printf("data: %s \nlen: %d\n", out,outlen);

  // 客户端环境释放 
  sckClient_destroy(&handle);

  return ret;
}
  • 服务器端测试代码
#include "comsocket.h"

#define PORT 8001


void chld_handle(int sig)
{
  pid_t my_pid;
  //wait(NULL);//只能处理单进程的情况
  while((my_pid = waitpid(-1, NULL, WNOHANG)) > 0)
    printf("child is died,parent take care of it:%d\n", my_pid);

}
int main()
{
  int ret = 0;
  int listenfd;
  int con_fd;
  int datalen = 1024;
  int timeout = 5;
  //unsigned char *data = (unsigned char *)"lzjyrm";

  unsigned char recv[1024] = {0};


  pid_t pid;

  signal(SIGCHLD,chld_handle);
  signal(SIGPIPE,SIG_IGN);

  ret = sckServer_init(PORT, &listenfd);
  if(ret != 0)
  {
    printf("sckServer_init err:%d\n ",ret);
    return ret;
  }
  printf("Waiting for connect.....\n");
  while(1)
  {
    ret = sckServer_accept(listenfd, &con_fd,  timeout);
    if(ret == Sck_ErrTimeOut)
    {
      printf("sckServer_accept Sck_ErrTimeOut");
      continue;
    }

    pid = fork();

    if(pid == -1)
    {
      perror("fork");
      return errno;
    }
    else if(pid == 0)
    {
      close(listenfd);
      while(1)
      {
        memset(recv,0,sizeof(recv));
        ret = sckServer_rcv(con_fd, recv, &datalen,  timeout);  //服务器端端接受报文
        if(ret != 0)
        {
          printf("sckServer_rcv: %d\n",ret);
          break;
        }
        recv[datalen] = '\0';
        printf("rcv:%s\n==================\n",recv);
        //服务器端发送报文
        ret = sckServer_send(con_fd,recv, datalen, timeout);
        if(ret != 0)
        {
          printf("sckServer_send: %d\n",ret);
          break;
        }
      }
      close(con_fd);
      exit(ret);
    }
    else if(pid > 0)
    {
      close(con_fd);
    }
  }

  //服务器端环境释放 
  //ret = sckServer_destroy(void **handle);
  return 0;
}
在 Linux 实现 C 语言的超时检测有多种方法,这里介绍两种: 1. 基于 select 函数的超时检测 通过 select 函数可以将多个文件描述符和超时时间传入,函数会检测这些文件描述符是否准备好进行读或写,并设定一个超时时间。如果在超时时间内没有任何一个文件描述符准备好,则 select 函数返回 0。如果准备好的文件描述符数量大于 0,则 select 函数返回准备好的文件描述符数量。 使用 select 函数进行超时检测的一般流程如下: ```c fd_set fdset; struct timeval timeout; FD_ZERO(&fdset); /* 清空文件描述符集合 */ FD_SET(fd, &fdset); /* 设置要检测的文件描述符 */ timeout.tv_sec = seconds; /* 设置超时时间,单位为秒 */ timeout.tv_usec = 0; /* 调用 select 函数进行超时检测 */ if (select(fd + 1, &fdset, NULL, NULL, &timeout) == 0) { /* 超时未发生 */ } else { /* 超时发生或文件描述符准备就绪 */ } ``` 2. 基于 alarm 函数的超时检测 通过 alarm 函数可以定时向进程发送一个 SIGALRM 信号,可以设定一个定时器,在定时器时间到达时向进程发送 SIGALRM 信号。在程序可通过注册一个 SIGALRM 信号处理函数来处理此信号。通过 alarm 函数实现超时检测的流程如下: ```c void timeout_handler(int sig) { /* 超时处理逻辑 */ } signal(SIGALRM, timeout_handler); /* 注册 SIGALRM 信号处理函数 */ alarm(duration); /* 设定定时器,duration 为超时时间,单位为秒 */ /* 调用需要超时检测的函数 */ function_to_check_timeout(); alarm(0); /* 取消定时器,避免影响其他函数操作 */ ``` 需要注意的是,在使用 alarm 函数时,需要确保被调用的函数不会阻塞同步信号,否则在超时时间到达后可能会发生不可预测的结果。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值