非阻塞socket编程

一. 阻塞、非阻塞、异步

阻塞:阻塞调用是指调用结果返回之前,当前线程会被挂起。该进程被标记为睡眠状态并被调度出去。函数只有在得到结果之后才会返回。当socket工作在阻塞模式的时候, 如果没有数据的情况下调用该函数,则当前线程就会被挂起,直到有数据为止。

非阻塞:非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。epoll工作在非阻塞模式时,才会发挥作用。

一般有三种操作IO的方式:

blocking IO: 发起IO操作后阻塞当前线程直到IO结束,标准的同步IO,如默认行为的posix readwrite

non-blocking IO: 发起IO操作后不阻塞,用户可阻塞等待多个IO操作同时结束。non-blocking也是一种同步IO:“批量的同步”。如linux下的poll,selectepoll,BSD下的kqueue

asynchronous IO: 发起IO操作后不阻塞,用户得递一个回调待IO结束后被调用。如windows下的OVERLAPPEDIOCP。linux的native AIO只对文件有效。

二. 非阻塞Socket

正常情况下,socket工作在阻塞模式下,在调用accept,connect,read,write等函数时,都是阻塞方式,直到读到数据才会返回。但是,如果将socket设置为非阻塞状态,那么这么些函数就会立即返回,不会阻塞当前线程。
设置非阻塞socket的方法是:

int SetNonBlock(int iSock)
{
    int iFlags;

    iFlags = fcntl(iSock, F_GETFL, 0);
    iFlags |= O_NONBLOCK;
    iFlags |= O_NDELAY;
    int ret = fcntl(iSock, F_SETFL, iFlags);
    return ret;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

三. 非阻塞accept

tcp的socket一旦通过listen()设置为server后,就只能通过accept()函数,被动地接受来自客户端的connect请求。进程对accept()的调用是阻塞的,就是说如果没有连接请求就会进入睡眠等待,直到有请求连接,接受了请求(或者超过了预定的等待时间)才会返回。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
返回值是一个新的套接字描述符,它代表的是和客户端的新的连接,可以把它理解成是一个客户端的socket,这个socket包含的是客户端的ip和port信息 。失败返回-1, 错误原因存于errno 中。
之后的read和write函数中的fd都是指这个 new_fd。

阻塞模式下调用accept()函数,而且没有新连接时,进程会进入睡眠状态
非阻塞模式下调用accept()函数,而且没有新连接时,将返回EWOULDBLOCK(11)错误

可以用以下代码来测试:

int SetNonBlock(int iSock)
{
    int iFlags;

    iFlags = fcntl(iSock, F_GETFL, 0);
    iFlags |= O_NONBLOCK;
    iFlags |= O_NDELAY;
    int ret = fcntl(iSock, F_SETFL, iFlags);
    return ret;
}

int main(int argc, char* argv[])
{
    int listenfd, connfd;
   
    struct sockaddr_in serveraddr;
    struct sockaddr_in clientaddr;
    socklen_t clilen;

    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    SetNonBlock(listenfd);

    //listenfd绑定ip地址
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    char local_addr[20]="127.0.0.1";
    inet_aton(local_addr,&(serveraddr.sin_addr));
    serveraddr.sin_port=htons(8000);
    
    //bind和listen不是阻塞函数
    bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
    listen(listenfd, 20);

    cout << "server listening ..."  << endl;

    int ret = -1;

    while(1)
    {
        connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);//以后读写都用这个返回的fd
        cout<<"connfd = "<<connfd<<", errno = "<<errno<<endl;
        sleep(1);
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

如果设置为非阻塞,accept会立即返回,打印出错误信息,errno=11,有新的连接时,连接会成功;
如果不设置非阻塞,进程就阻塞在accept那里,直到有新的连接到来。

非阻塞模式下,accept函数可以与epoll结合,实现等待。具体可以见另一篇博文:
http://blog.csdn.net/okiwilldoit/article/details/50469515

四. 非阻塞connect

在阻塞模式下,客户端调用connect()函数将激发TCP的三路握手过程,但仅在连接建立成功或出错时才返回。
非阻塞工作模式,调用connect()函数会立刻返回EINPROCESS错误,但TCP通信的三路握手过程正在进行,所以可以使用select函数来检查这个连接是否建立成功。
源自Berkeley的实现有两条与select函数和非阻塞相关的规则:
1>.当连接成功建立时,描述字变成可写。
2>.当连接建立出错时,描述字变成即可读又可写。getsockopt()函数的errno == 0表示只可写。

处理非阻塞 connect 的步骤:
(1) 创建socket,并利用fcntl将其设置为非阻塞
(2) 调用connect函数,如果返回0,则连接建立;如果返回-1,检查errno ,如果值为 EINPROGRESS,则连接正在建立。
(3) 为了控制连接建立时间,将该socket描述符加入到select的可写集合中,采用select函数设定超时。
(4) 如果规定时间内成功建立,则描述符变为可写;否则,采用getsockopt函数捕获错误信息。当errno == 0表示只可写。

实例:
Redis客户端CLI (command line interface),位于源代码的src/deps/hiredis下面。
实际上,不仅是Redis客户端,其他类似的client/server架构中,client均可采用非阻塞式connect实现。
https://github.com/redis/hiredis/blob/master/net.c
参考函数:_redisContextConnectTcp()

当然,也可以用poll或epoll来代替select。
非阻塞模式 connect() + select()代码:

int RouterNode::Connect()
{
	sockaddr_in servaddr = {0};
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, ip_.c_str(), &servaddr.sin_addr);
    servaddr.sin_port = htons(port_);

	int ret = ::connect(fd_, (struct sockaddr *)&servaddr, sizeof(servaddr));
	
	if(ret == 0)
	{
		is_connected_ = true;
		return 0;
	}
	
	int error = 0;
	socklen_t len = sizeof (error);
	
	if(errno != EINPROGRESS)
	{
		goto __fail;
	}
	
	fd_set wset;//写集合
	FD_ZERO(&wset);
	FD_SET(fd_, &wset);

	struct timeval tval;
	tval.tv_sec = 3;//3s
	tval.tv_usec = 0; 

	if (select(fd_ + 1, NULL, &wset, NULL, &tval) == -1) //出错、超时,连接失败
	{
		goto __fail; 
	}
	
	if(!FD_ISSET(fd_, &wset))//不可写
	{
		goto __fail;
	}

	if (getsockopt(fd_, SOL_SOCKET, SO_ERROR, &error, &len) == -1)
	{
		goto __fail;
	}

	if(error)
	{
		goto __fail;
	}
	
	is_connected_ = true;
	return 0;

__fail:
	close(fd_);
	return -1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

五. 非阻塞write

对于写操作write,非阻塞socket在发送缓冲区没有空间时会直接返回-1,错误号EWOULDBLOCK或EAGAIN,表示没有空间可写数据,如果错误号是别的值,则表明发送失败。
如果发送缓冲区中有足够空间或者是不足以拷贝所有待发送数据的空间的话,则拷贝前面N个能够容纳的数据,返回实际拷贝的字节数。
而对于阻塞Socket而言,如果发送缓冲区没有空间或者空间不足的话,write操作会直接阻塞住,如果有足够空间,则拷贝所有数据到发送缓冲区,然后返回。
实现代码:

/**
* 返回-1:失败
* 返回>0: 成功
*/
int WriteNonBlock(int fd, const char* send_buf, size_t send_len)
{
    int sentlen = 0;//已经发送的长度

    while(sentlen < send_len)
    {
        int ret = write(fd, send_buf+sentlen, send_len-sentlen);
        if(ret <= 0)
        {
            if(ret < 0 && errno == EINTR)
            {
                continue;
            }
            else//遇到EAGAIN直接退出
            {
                break;
            }
        }
        sentlen += ret;
    }
    return sentlen;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

六. 非阻塞read

对于阻塞的socket,当socket的接收缓冲区中没有数据时,read调用会一直阻塞住,直到有数据到来才返回。
当socket缓冲区中的数据量小于期望读取的数据量时,返回实际读取的字节数。
当sockt的接收缓冲区中的数据大于期望读取的字节数时,读取期望读取的字节数,返回实际读取的长度。

对于非阻塞socket而言,socket的接收缓冲区中有没有数据,read调用都会立刻返回。
接收缓冲区中有数据时,与阻塞socket有数据的情况是一样的,如果接收缓冲区中没有数据,则返回-1,
错误号为EWOULDBLOCK或EAGAIN,表示该操作本来应该阻塞的,但是由于本socket为非阻塞的socket,
因此立刻返回,遇到这样的情况,可以在下次接着去尝试读取。如果返回值是其它负值,则表明读取错误。
实现代码:

/**
* 返回-1:失败
* 返回>0: 成功
*/
int ReadNonBlock(int fd, char* recv_buf, size_t recv_len)
{
    int readlen = 0;//已经读到的长度
    while(readlen < recv_len)
    {
        int ret = read(fd, recv_buf+readlen, recv_len-readlen);
        if(ret == 0)//已到达文件末尾
        {
            return readlen;
        }
        else if(ret > 0)
        {
            readlen += ret;
        }       
        else if(errno == EINTR)
        {
            continue;
        }
        else//遇到EAGAIN直接退出
        {
            break;
        }
    }

    return readlen;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

recvfrom,sendto等函数也是同样类似的方法。

--------------------- 本文来自 okiwilldoit 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/okiwilldoit/article/details/51015444?utm_source=copy 

猜你喜欢

转载自blog.csdn.net/chenxinntu/article/details/82926867