文章目录
一、阻塞非阻塞概念
- 阻塞,就是调用我(函数),我(函数)没有接收完数据或者没有得到结果之前,我不会返回。
- 非阻塞,就是调用我(函数),我(函数)立即返回,通过select通知调用者。
- 同步,就是我调用一个功能,该功能没有结束前,我死等结果。
- 异步,就是我调用一个功能,不需要知道该功能结果,该功能有结果后通知我(回调通知)。
阻塞IO和非阻塞IO的区别就在于:应用程序的调用是否立即返回
同步IO和异步IO的区别就在于: 数据拷贝的时候进程是否阻塞
二、socket阻塞和非阻塞有哪些影响
1.建立连接 connect
阻塞方式下,connect首先发送SYN请求到服务器,当客户端收到服务器返回的SYN的确认时,则connect返回,否则的话一直阻塞。
非阻塞方式,connect将启用TCP协议的三次握手,但是connect函数并不等待连接建立好才返回,而是立即返回,返回的错误码为EINPROGRESS,表示正在进行某种过程。
2.接收连接 accept
阻塞模式下调用accept()函数,没有新连接时,进程会进入睡眠状态,直到有可用的连接,才返回。
非阻塞模式下调用accept()函数立即返回,有连接返回客户端套接字描述符。没有新连接时,将返回EWOULDBLOCK错误码,表示本来应该阻塞。
3.读操作 recv/read
阻塞与非阻塞recv返回值没有区分,都是 <0:出错,=0:连接关闭,>0接收到数据大小。
阻塞模式下调用read(),recv()等读套接字函数会一直阻塞住,直到有数据到来才返回。
当socket缓冲区中的数据量小于期望读取的数据量时,返回实际读取的字节数。当sockt的接收缓冲区中的数据大于期望读取的字节数时,读取期望读取的字节数,返回实际读取的长度。
非阻塞模式下,不管接收缓冲区中有没有数据,函数调用都离开返回。
- 接收缓冲区中有数据时,与阻塞socket有数据的情况是一样。
- 接收缓冲区中没有数据,则返回-1,errno被置为EWOULDBLOCK,表示该操作本来应该阻塞的,但是由于本socket为非阻塞的socket,因此立刻返回。
非阻塞模式下返回值 < 0 时并且(errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情况下认为连接是正常的,因此需要循环读取数据。
因此,非阻塞的read调用一般这样写:
if ((nread = read(sock_fd, buffer, len)) < 0)
{
if (errno == EWOULDBLOCK)
{
return 0; //表示没有读到数据
}
else return -1; //表示读取失败
}
else return nread; //读到数据长度
读取指定字节数据可封装readn()函数为:
ssize_t readn(int fd, void *vptr, size_t n)
{
int32 nleft = 0;
int32 nread = 0;
int8 *pread_buf = NULL;
pread_buf = (int8 *)vptr;
nleft = n;
while (nleft > 0)
{
nread = recv(fd, (char *)pread_buf, nleft, 0);
if (nread < 0)
{
if (EINTR == errno || EWOULDBLOCK == errno || EAGAIN == errno)
{
nread = 0;
}
else
{
return -1;
}
}
else if (nread == 0)
{
break;
}
else
{
nleft -= nread;
pread_buf += nread;
}
}
return (ssize_t)(n - nleft);
}
4.写操作 write/send
阻塞与非阻塞write/sned返回值没有区分,都是 <0:出错,=0:连接关闭,>0发送数据大小。
阻塞模式,如果发送缓冲区没有空间或者空间不足的话,write操作会直接阻塞住,如果有足够空间,则拷贝所有数据到发送缓冲区,然后返回。
非阻塞模式下,不管发送缓冲区中有没有空间,函数调用都离开返回。
- 发送缓冲区中有足够空间或者是不足以拷贝所有待发送数据的空间的话,则拷贝前面N个能够容纳的数据,返回实际拷贝的字节数。
- 发送缓冲区没有空间时直接返回-1,errno被置为EWOULDBLOCK表示没有空间可写数据,如果错误号是别的值,则表明发送失败。
非阻塞模式下返回值 < 0时并且 (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情况下认为连接是正常的, 因此需要循环发送。
非阻塞的write操作一般写法是:
ssize_t writen(int connfd, const void *pbuf, size_t nums)
{
int32 nleft = 0;
int32 nwritten = 0;
char *pwrite_buf = NULL;
if ((connfd <= 0) || (NULL == pbuf) || (nums < 0))
{
return -1;
}
pwrite_buf = (char *)pbuf;
nleft = nums;
while(nleft>0)
{
if (-1 == (nwritten = send(connfd, pwrite_buf, nleft, MSG_NOSIGNAL)))
{
if (EINTR == errno || EWOULDBLOCK == errno || EAGAIN == errno)
{
nwritten = 0;
}
else
{
errorf("%s,%d, Send() -1, 0x%x\n", __FILE__, __LINE__, errno);
return -1;
}
}
nleft -= nwritten;
pwrite_buf += nwritten;
}
return(nums);
}
5.简单小结
阻塞socket容易存在的问题:
- connect/accept/write导致线程阻塞。(服务关闭/重启线程无法退出)
- recv读数据长度不确定。(不能保证读取完整数据,导致无法解析)
非阻塞的socket
- 在建立连接时要兼容处理返回EINPROGRESS情况
- 在接收连接、读操作、写操作时要兼容处理返回EWOULDBLOCK错误码的情况
以上情况并非连接异常,而是网络延时或者套接字缓冲区满造成的,一般不宜做异常处理(如返回异常关闭套接字)
使用建议:
- 使用阻塞socket,通过select等IO复用模型可实现socket阻塞的非阻塞调用。(解决线程阻塞问题)
- 读写接口:套用封装好的readn/writen函数。(指定时间读不到数据/读不到指定数据算作异常)
三、设置套接字阻塞或非阻塞
1.生成socket时设置
socket函数创建socket默认是阻塞的,也可以增加选项将socket设置为非阻塞的:
int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);
2.使用fcntl设置
//设置套接字非阻塞
if (fcntl(sock_v4, F_SETFL, fcntl(sock_v4, F_GETFL) | O_NONBLOCK) == -1)
{
printf( "set sockfd nonblock -1, errno=%d\n", errno); //fcntl返回-1表示失败
}
2.使用ioctl设置
int b_on = 1;
ioctl (fd, FIONBIO, &b_on);
针对fcntl函数可做如下封装:
/** @fn set_nonblock(int32 sock_fd, bool b_set)
* @brief 设置socket fd为阻塞模式或者非阻塞模式
* @param[in] sock_fd 已经连接成功的连接fd。范围:大于0
* @param[in] b_set 是否设置为非阻塞模式。0-否,非0-是;
* @param[out] 无
* @return 成功返回0;失败返回-1
*/
int set_nonblock(int32 sock_fd, bool b_set)
{
int32 flags = 0;
flags = fcntl(sock_fd, F_GETFL, 0);
if (-1 == flags)
{
printf("get sockfd flag -1, errno=%d\n", errno);
return -1;
}
if (b_set)
{
if (fcntl(sock_fd, F_SETFL, flags | O_NONBLOCK) == -1)
{
printf("set sockfd nonblock -1, errno=%d\n", errno);
return -1;
}
}
else
{
if (fcntl(sock_fd, F_SETFL, flags & (~O_NONBLOCK)) == -1)
{
printf("set sockfd nonblock -1, errno=%d\n", errno);
return -1;
}
}
return 0;
}
四、Linux fcntl函数介绍
功能描述:根据文件描述词来操作文件的特性。
fcntl系统调用可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性。
头文件:
#include <unistd.h>
#include <fcntl.h>
函数原型:
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
参数:
fd:文件描述词。
cmd:操作命令。
arg:供命令使用的参数。
lock:同上。
描述:
fcntl()针对(文件)描述符提供控制。参数fd是被参数cmd操作(如下面的描述)的描述符。针对cmd的值,fcntl能够接受第三个参数arg。
返回值: 与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其他值。
下列三个命令有特定返回值:F_DUPFD,F_GETFD,F_GETFL以及F_GETOWN。
F_DUPFD 返回新的文件描述符
F_GETFD 返回相应标志
F_GETFL , F_GETOWN 返回一个正的进程ID或负的进程组ID
fcntl函数有5种功能:
- 复制一个现有的描述符(cmd=F_DUPFD).
- 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
- 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
- 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
- 获得/设置记录锁(cmd=F_GETLK , F_SETLK或F_SETLKW).
cmd 选项:
F_DUPFD:返回一个如下描述的(文件)描述符:
(1)最小的大于或等于arg的一个可用的描述符
(2)与原始操作符一样的某对象的引用
(3)如果对象是文件(file)的话,返回一个新的描述符,这个描述符与arg共享相同的偏移量(offset)
(4)相同的访问模式(读,写或读/写)
(5)相同的文件状态标志(如:两个文件描述符共享相同的状态标志)
(6)与新的文件描述符结合在一起的close-on-exec标志被设置成交叉式访问execve(2)的系统调用
F_GETFD:取得与文件描述符fd联合close-on-exec标志,类似FD_CLOEXEC.如果返回值和FD_CLOEXEC进行与运算结果是0的话,文件保持交叉式访问exec(), 否则如果通过exec运行的话,文件将被关闭(arg被忽略)
F_SETFD:设置close-on-exec旗标。该旗标以参数arg的FD_CLOEXEC位决定。
F_GETFL:取得fd的文件状态标志,如同下面的描述一样(arg被忽略)
F_SETFL:设置给arg描述符状态标志,可以更改的几个标志是:O_APPEND, O_NONBLOCK,O_SYNC和O_ASYNC。
F_GETOWN:取得当前正在接收SIGIO或者SIGURG信号的进程id或进程组id,进程组id返回成负值(arg被忽略)
F_SETOWN:设置将接收SIGIO和SIGURG信号的进程id或进程组id,进程组id通过提供负值的arg来说明,否则,arg将被认为是进程id
F_GETFL和F_SETFL的标志如下面的描述:
O_NONBLOCK:非阻塞I/O;如果read(2)调用没有可读取的数据,或者如果write(2)操作将阻塞,read或write调用返回-1和EAGAIN错误 O_APPEND :强制每次写(write)操作都添加在文件大的末尾,相当于open(2)的O_APPEND标志
O_DIRECT :最小化或去掉reading和writing的缓存影响.系统将企图避免缓存你的读或写的数据.
如果不能够避免缓存,那么它将最小化已经被缓存了的数 据造成的影响.如果这个标志用的不够好,将大大的降低性能
O_ASYNC :当I/O可用的时候,允许SIGIO信号发送到进程组,例如:当有数据可以读的时候
注意: 在修改文件描述符标志或文件状态标志时必须谨慎,先要取得现在的标志值,然后按照希望修改它,最后设置新标志值。不能只是执行F_SETFD或F_SETFL命令,这样会关闭以前设置的标志位。
五、参考资料
关于socket阻塞与非阻塞情况下的recv、send、read、write返回值
https://www.cnblogs.com/longingforlife/p/3289976.html