以下均为在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
版权归原作者 Origin-yy 所有, 如有侵权,请联系我们删除。