1. 程式人生 > >非阻塞socket程式設計

非阻塞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