阻塞socket 和非阻塞socket的区别(浅显易懂版)

什么是阻塞socket,什么是非阻塞socket。

对于这个问题,我们要先弄清什么是阻塞/非阻塞。

阻塞与非阻塞是对一个文件描述符指定的文件或设备的两种工作方式。 阻塞的意思是指,当试图对该文件描述符进行读写时,如果当时没有东西可读或者暂时不可写,程序就进入等待状态,直到有东西可读或者可写为止。 非阻塞的意思是,当没有东西可读或者不可写时,读写函数就马上返回,而不会等待。


现在来理解什么是阻塞socket,什么是非阻塞socket。每个通过socket()函数创建的socket,本质就是一个文件描述符,所以对该文件描述符的IO操作方式不同,就有了阻塞socket和非阻塞socket。 那是不是说阻塞socket下的所有socket api函数都是阻塞的呢,如果你还不能正确的回答这个问题,说明上面简短的说明并没有让你真正的明白什么是阻塞socket和非阻塞socket。这个问题的答案是否定的,为什么是否定的,因为并不是每个socket的api都会涉及到对文件描述符的IO操作。

这里我列举了,哪些socket api会阻塞:

accept,connect,recv(recvfrom),send(sendto),closesocket,select(poll或epoll)

1)accept在阻塞模式下,没有新连接时,线程会进入睡眠状态;非阻塞模式下,没有新连接时,立即返回WOULDBLOCK错误。

2)connect在阻塞模式下,仅TCP连接建立成功或出错时才返回,分几种具体的情况,这里不再叙述;非阻塞模式下,该函数会立即返回INPROCESS错误(需用select检测该连接是否建立成功)

3)recv/recvfrom/send/sendto很好理解,因为这两类函数读写socket文件描述符的接收/发送缓冲区。 

4) select/poll/epoll并不是真正意义上的阻塞,它们的阻塞是由于它们最后一个timeout参数决定的,timeout大于0时,它们会一直等待直到超时才退出(相等于阻塞了吧,^_^),而timeout=-1即永远等待。

sendrecv函数在阻塞和非阻塞模式下的表现

sendrecv函数并不是直接向网络上发送数据和接收数据

send函数是将应用层发送缓冲区的数据拷贝到内核缓冲区中

recv函数是将内核缓冲区的数据拷贝到应用缓冲区

可以用下面这张图来描述:

通过上图我们可以知道,不同的程序进行网络通信时,发送的一方会将内核缓冲区的数据通过网络传输给接收方的内核缓冲区。

在应用程序A与应用程序B建立TCP连接后,假设A不断调用send函数,会将数据不断拷贝到对应的内核缓冲区,如果应用程序不调用recv函数,那么在应用程序B的内核缓冲区被填满后,A的缓冲区也随后被填满,此时如果A继续调用send函数会有什么后果呢?

当socket处于阻塞模式时,继续调用send/recv函数,程序会阻塞在send/recv调用处
当socket处于非阻塞模式时,继续调用send/recv函数,会返回错误码

1.socket阻塞模式下send函数的表现

代码来自《C++服务器开发精髓》

服务端代码:

#include <sys/types.h> 
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>

int main(int argc, char* argv[])
{
    //1.创建一个侦听socket
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == -1)
    {
        std::cout << "create listen socket error." << std::endl;
        return -1;
    }

    //2.初始化服务器地址
    struct sockaddr_in bindaddr;
    bindaddr.sin_family = AF_INET;
    bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    bindaddr.sin_port = htons(3000);
    if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1)
    {
        std::cout << "bind listen socket error." << std::endl;
		close(listenfd);
        return -1;
    }

	//3.启动侦听
    if (listen(listenfd, SOMAXCONN) == -1)
    {
        std::cout << "listen error." << std::endl;
		close(listenfd);
        return -1;
    }

    while (true)
    {
        struct sockaddr_in clientaddr;
        socklen_t clientaddrlen = sizeof(clientaddr);
		//4. 接受客户端连接
        int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen);
        if (clientfd != -1)
        {         	
			//只接受连接,不调用recv收取任何数据
			std:: cout << "accept a client connection." << std::endl;
        }
    }
	
	//7.关闭侦听socket
	close(listenfd);

    return 0;
}

客户端代码:

/**
 * 验证阻塞模式下send函数的行为,client端
 * zhangyl 2018.12.17
 */
#include <sys/types.h> 
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>

#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT     3000
#define SEND_DATA       "helloworld"

int main(int argc, char* argv[])
{
    //1.创建一个socket
    int clientfd = socket(AF_INET, SOCK_STREAM, 0);
    if (clientfd == -1)
    {
        std::cout << "create client socket error." << std::endl;
        return -1;
    }

    //2.连接服务器
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
    serveraddr.sin_port = htons(SERVER_PORT);
    if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1)
    {
        std::cout << "connect socket error." << std::endl;
        close(clientfd);
        return -1;
    }

    //3. 不断向服务器发送数据,或者出错退出
    int count = 0;
    while (true)
    {
        int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0);
        if (ret != strlen(SEND_DATA))
        {
            std::cout << "send data error." << std::endl;
            break;
        } 
        else
        {
            count ++;
            std::cout << "send data successfully, count = " << count << std::endl;
        }
    }

    //5. 关闭socket
    close(clientfd);

    return 0;
}

 先启动server在启动client,客户端会不断向服务端发送helloworld,每次发送成功后会打印计数器,运行一顿时间后,停止打印,计数器不再增加

 当程序不再有输出,说明阻塞在某个函数gdb看一看

(gdb) bt
#0  0x00007ffff7d03690 in __libc_send (fd=3, buf=0x555555556045, len=10, 
    flags=0) at ../sysdeps/unix/sysv/linux/send.c:28
#1  0x00005555555553bb in main (argc=1, argv=0x7fffffffdf28) at client.cpp:42
(gdb) 
 

果然是send函数

上面这个例子证明了如果一端一直发送数据,另一端不接收数据,内核缓冲区很快就会被填满,发生阻塞. 其实这里所说的内核缓冲区就是TCP窗口

我们现在利用tcpdump工具查看一下这种情况下TCP窗口的大小

22:01:57.543364 IP 127.0.0.1.53382 > 127.0.0.1.3000: Flags [S], seq 1832090129, win 65495, options [mss 65495,sackOK,TS val 451488646 ecr 0,nop,wscale 7], length 0
22:01:57.543379 IP 127.0.0.1.3000 > 127.0.0.1.53382: Flags [S.], seq 1797517498, ack 1832090130, win 65483, options [mss 65495,sackOK,TS val 451488646 ecr 451488646,nop,wscale 7], length 0
22:01:57.543386 IP 127.0.0.1.53382 > 127.0.0.1.3000: Flags [.], ack 1797517499, win 512, options [nop,nop,TS val 451488646 ecr 451488646], length 0
...
22:02:11.342670 IP 127.0.0.1.3000 > 127.0.0.1.53382: Flags [.], ack 1832177322, win 0, options [nop,nop,TS val 451502445 ecr 451488936], length 0

 win就是TCP窗口的大小可以看出,逐渐减小最后变为零

2.socket非阻塞模式下send函数的表现

就是返回一个错误码,不阻塞了,注意,程序并不会结束。可以处理错误并继续运行。

3.socket阻塞模式下recv函数的表现

recv函数会挂起(阻塞)调用它的线程,直到以下情况之一发生:

  1. 数据到达:如果远程对等体发送了数据,并且数据已经被接收到socket的接收缓冲区中,recv函数会从缓冲区中取出数据,填充到用户提供的缓冲区中,并返回接收到的字节数。

  2. 连接终止:如果远程对等体关闭了连接,recv函数会返回0,表示对端已经关闭了连接,不会再有数据发送过来。

  3. 发生错误:如果在接收数据的过程中发生错误,recv函数会返回-1,并设置一个相应的错误码来指示发生了什么类型的错误。

  4. 超时:如果在套接字上设置了超时时间,并且在这个时间内没有数据到达,recv函数会因为超时而返回-1,并设置错误码为EWOULDBLOCKEAGAIN(不同的系统可能返回不同的错误码)。

4.socket非阻塞模式下recv函数的表现

recv在没有数据可读的情况下,会立即返回,返回值为-1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值