0


Internet Socket (非)阻塞write/read n个字节

以下均为在Internet Socket下的write和read

一、阻塞/非阻塞的write和read简单介绍

(1)write

#include <unistd.h>
 
ssize_t write(int fd, const void *buf, size_t nbytes);

1.用途描述:期望将用户空间的buf内nbytes字节的数据拷贝到文件描述符fd指定的内核缓冲区中。

    注:并不是发送到对端socket,这是内核通过网络干的事情,write写入内核缓冲区,内核通过网络在某个时间把其中的数据发送到对端sockert文件描述符指定的对端内核缓冲区中,send也一样,不是发送。

2.返回值:

-1:出错并设置errno。

=0:连接关闭。

0:写入数据大小。

    缓冲区足够写入全部数据时,都是全部写入并返回大小。

    缓冲区不够全部写入时,先写入部分,阻塞write会阻塞直到全部写入,非阻塞会返回写入的数据大小。非阻塞下返回-1并且errno == EINTR(信号中断) || errno == EWOULDBLOCK(将要阻塞) || errno == EAGAIN(无空间可写)时,认为连接是正常的。

EAGAIN,提示你的应用程序现在没有数据可读或者没有空间可写,请稍后再试。在VxWorks和Windows上,EAGAIN的名字叫做EWOULDBLOCK。

(2)read

    #include <uinstd.h>
     
    ssize_t read(int fd, void *buf, size_t nbytes);

用途描述:期望将内核中缓冲区的nbytes字节的数据拷贝到用户进程空间的buf中。

    注:并不是从对端socket接收数据,这是内核通过网络干的事情,内核在某个时间接收到了对端socket发送过来的数据,read从内核缓冲区中读取这些数据。recv也一样,不是接收。

2.返回值:

-1,出错并设置errno。

=0,连接关闭。

0,读到的数据大小。

    如果缓冲区有数据,都是读走并返回大小。

    如果缓冲区没有数据,阻塞模式下read会阻塞等待数据,非阻塞下,返回-1,当errno == EINTR(信号中断) || errno == EWOULDBLOCK(将要阻塞) || errno == EAGAIN(无数据可读)时,认为连接是正常的。

二、阻塞/非阻塞的write和read读到n个字节的写法

阻塞write写入n个字节:

ssize_t writen(int fd, const void* buf,size_t n)
{
  ssize_t numwriten;
  size_t totwriten;
  const char *p;

  p = buf;
  for(totwriten = 0; totwriten < n;)
  {
    numwriten = write(fd, p, n - totwriten);

    if(numwriten <= 0)
    {
      if(numwriten == -1 && errno == EINTR)
        continue;
      else
        return -1;
    }
    totwriten += numwriten;
    p += numwriten;

  }
  return totwriten;
}

阻塞read读取n个字节:

ssize_t readn(int fd, void*buf,size_t n)
{
  ssize_t numread;
  size_t totread;
  char *p;

  p = buf;
  for(totread = 0; totread < n;)
  {
    numread= read(fd, p, n - totread);

    if(numread == 0)
      return totread;
    if(numread == -1)
    {
      if(errno == EINTR)
        continue;
      else
        return -1;
    }
    totread += numread;
    p += numread;
  }
  return totread;
}

非阻塞write写n个字节:

int writen(int connfd, const void *buf, int nums) {
  int left = 0;
  int had = 0;
  char *ptr = NULL;

  if ((connfd <= 0) || (buf == NULL) || (nums < 0)) {
    return -1;
  }

  ptr = (char *)buf;
  left = nums;
  int i = 0;

  while (left > 0) {
    had = write(connfd, ptr, left);
    if (had == -1) {
      if (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN) {
        had = 0;
          usleep(5);
          if(i == 500)
              return -1;    //返回-1且errno为:EINTR EWOULDBLOCK EAGAIN的话是超时
          else
            i++;
      } else {
        return -1;
      }
    } else if (had == 0) {
      return 0;
    } else {
      left -= had;
      ptr += had;
    }
  }

  return nums;
}

非阻塞read读取n个字节:

int readn(int fd, void *buf, int n) {
  int left = 0;
  int had = 0;
  int *ptr = NULL;

  ptr = (int *)buf;
  left = n;
  int i = 0;

  while (left > 0) {
    had = read(fd, (char *)ptr, left);
    if (had == -1) {
      if (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN) {
        had = 0;
        usleep(10);
        if (i == 100)
          return -1;
        else
          i++;
      } else {
        return -1;
      }
    } else if (had == 0) {
      return 0;
    } else {
      left -= had;
      ptr += had;
    }
  }
  return n - left;
}

三、send和recv

#include <sys/socket.h>

ssize_t recv(int sockfd, void *buffer, size_t length, int flags);

ssize_t send(int sockfd, const void *buffer, size_t length, int flags);

(详细描述在Linux/Unix系统编程手册p1033)

前三个参数和write、read一样,第四个参数修改I/O操作的行为。

recv()可选:MSG_DONTWAIT,MSG_OOB,MSG_PEEK,MSG_WAITALL。

MSG_WAITALL:

    通常,recv()调用返回的字节数比请求的字节数要少,而那些字节实际上还在套接字中。指定了 MSG_WAITALL 标记后将导致系统调用阻塞,直到成功接收到 length个字节。但是,就算指定了这个标记,当出现如下情况时,该调用返回的字节数可能还是会少于请求的字节。这些情况是:

(a)捕获到一个信号;

(b)流式套接字的对端终止了连接;

(c)遇到了带外数据字节;

(d)从数据报套接字接收到的消息长度小于 length 个字节;

(e)套接字上出现了错误。

(MSG_WAITALL 标记可以取代上面地阻塞readn()函数,区别在于我们实现的 readn()函数在被

信号处理例程中断后会重新得到调用。)

send()可选:MSG_DONTWAIT,MSG_MORE,MSG_NOSIGNAL,MSG_OOB。

四、阻塞wirte和read+超时

1.setsocketopt:

#include <sys/socket.h>

int setsockopt( int socket, int level, int option_name,
                const void *option_value, size_t ,ption_len);

参数option_name:

SO_RCVTIMEO,设置读取超时时间。该选项最终将接收超时时间赋给sock->sk->sk_rcvtimeo。

SO_SNDTIMEO,设置写入超时时间。该选项最终将发送超时时间赋sock->sk->sk_sndtimeo。

这两个选项设置后,若超时, 返回-1,并设置errno为EAGAIN或EWOULDBLOCK.

获取超时值

 socklen_t optlen = sizeof(struct timeval);
 struct timeval tv;
 tv.tv_sec = 10;
 tv.tv_usec = 0;
 getsockopt(socketfd, SOL_SOCKET,SO_SNDTIMEO, &tv, &optlen);

设置超时值

 socklen_t optlen = sizeof(struct timeval);
 struct timeval tv;
 tv.tv_sec = 10;
 tv.tv_usec = 0;
 setsockopt(socketfd, SOL_SOCKET, SO_RCVTIMEO, &tv, optlen);

2.select/poll/epoll:

select()阻塞write+超时:

int write_timeout(int fd, void *buf, size_t n, u_int32_t time) {
    //设置超时时间
  
    fd_set wSet;
    FD_ZERO(&wSet);
    FD_SET(fd, &wSet);

    struct timeval timeout;
    memset(&timeout, 0, sizeof(timeout));
    timeout.tv_sec = time;
    timeout.tv_usec = 0;
    // select加超时,阻塞并等待写就绪
    int r;
    while (1) {
      r = select(fd + 1, NULL, &wSet, NULL, &timeout);
      if (r < 0) {
        if (errno == EINTR)
          continue;
        return r;
      } else if (r == 0) {
        errno = ETIMEDOUT; //设置errno为超时
        return -1;
      } else {
        break;
      }
    }
  
    //开写
    int writeNum;
    writeNum = write(fd, buf, n);
    return writeNum;
}

select阻塞read+超时:

int read_timeout(int fd, void *buf, size_t n, u_int32_t time) {
    //设置超时时间
  
    fd_set rSet;
    FD_ZERO(&rSet);
    FD_SET(fd, &rSet);

    struct timeval timeout;
    memset(&timeout, 0, sizeof(timeout));
    timeout.tv_sec = time;
    timeout.tv_usec = 0;
    // select加上超时,并阻塞等待读就绪
    int r;
    while (1) {
      r = select(fd + 1, &rSet, NULL, NULL, &timeout);
      if (r < 0) {
        if (errno == EINTR)
          continue;
        return r;
      } else if (r == 0) {
        errno = ETIMEDOUT;
        return -1;
      } else {
        break;
      }
    }
  
    //开读
    int readnum;
    readnum = read(fd, buf, n);
    return readnum;
}

select/poll/epoll是线程安全的,他为每个线程都提供一个副本,都有一个超时时间

3.信号中断阻塞读/写

alarm():

    write/read是一个低速系统调用,在阻塞期间,如果收到信号,write/read就会被中断不再继续执行,返回-1,并将errono设置为EINTR。但是,如果是read()等系统调用,他们被信号中断有时会影响整个程序的正确性,所以有些系统使这类系统调用自动重启动。就是一旦被某信号中断, 立即再启动,所以重启后该阻塞还是会阻塞。但通过设置SA_INTERRUPT可以让read被信号中断后不重启,立即返回。

    alarm()也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间到时,它向调用它的进程发送SIGALRM信号。其默认响应方式方式是终止调用该alarm函数的进程。要注意的是,一个进程只能有一个闹钟时间,多线程场景很难处理。如果在调用alarm之前已设置过闹钟时间,则任何以前的闹钟时间都被新值所代替。调用alarm(0)来取消此闹钟,并返回剩余时间。

alarm()精确到秒,setitimer()精确到微秒。

alarm()阻塞write超时

void handler(int s) {
    printf("SIGALRM信号到达。\n");
    return;
}
int write_timeout_alarm(int fd, void *buf, size_t n, u_int32_t time) {
    struct sigaction act;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    act.sa_flags |= SA_INTERRUPT; //设置该选项后,中断的系统调用不会自动重启
    if (sigaction(SIGALRM, &act, NULL) == -1) {
    perror("sigaction");
    exit(-1);
    }

    alarm(time);
    int readnum = write(0, buf, sizeof(buf)); //从标准输入读取字符
    alarm(0);

    return readnum;
}

alarm()阻塞read超时

void handler(int s) {
    printf("SIGALRM信号到达。\n");
    return;
}

int read_timeout_alarm(int fd, void *buf, size_t n, u_int32_t time) {
    struct sigaction act;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    act.sa_flags |= SA_INTERRUPT; //设置该选项后,中断的系统调用不会自动重启
    if (sigaction(SIGALRM, &act, NULL) == -1) {
      perror("sigaction");
      exit(-1);
    }

    alarm(time);
    int writenum = write(1, buf, sizeof(buf)); //从标准输入读取字符
    alarm(0);
 
    return writenum;
}

总结:阻塞加超时用select()或者setsocketopt()就完了

五、timerfd系列函数简介:

    timerfd是Linux为用户程序提供的一个定时器接口。这个接口基于文件描述符,通过文件描述符的可读事件进行超时通知,可以用read获取自从上次获取后的超时次数。对于多定时器,可以配合select/poll/epoll使用。
#include <sys/timerfd.h>

int timerfd_create(int clockid, int flags);
//创建一个定时器对象,返回指是一个与之关联的文件描述符

int timerfd_settime(int fd, int flags, const struct itimerspec *new_value,
                    struct itimerspec *old_value);
//设置新的超时时间,并开始计时,能够启动和停止定时器;

int timerfd_gettime(int fd, struct itimerspec *curr_value);
//获取距离下次超时剩余的时间

详细介绍参见:

Linux下定时函数timerfd_xxx()的使用_爱就是恒久忍耐的博客-CSDN博客_timerfd_settime

标签: linux 服务器

本文转载自: https://blog.csdn.net/weixin_63733944/article/details/123440088
版权归原作者 Origin-yy 所有, 如有侵权,请联系我们删除。

“Internet Socket (非)阻塞write/read n个字节”的评论:

还没有评论