Java NIO(4): 阻塞与非阻塞

本节课是小密圈《进击的Java新人》第十六周第四节课。这一节课,我们通过 linux 上的 socket 编程,来说明一下IO模型。

我们通过继续第八周的课程:Java网络编程(二):套接字 ,来看一下正式的网络编程的例子。

阻塞式IO

我们先看一个阻塞式的例子:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>

#define MAXLEN 4096

int main(int argc, char** argv)
{
    int    listenfd, sock_fd;
    struct sockaddr_in     servaddr;
    char    buff[MAXLEN];
    int     n;  

    if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){
        printf("create socket error: %s(errno: %d)/n",strerror(errno),errno);
        exit(0);
    }   

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(8001);

    if( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
        printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
        exit(0);
    }

    if( listen(listenfd, 10) == -1){
        printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
        exit(0);
    }

    printf("waiting for client to connect\n");

    while(1){
        if( (sock_fd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1){
            printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
            continue;
        }
        n = recv(sock_fd, buff, MAXLEN, 0);
        buff[n] = '\0';
        printf("recv msg from client: %s\n", buff);
        close(sock_fd);
        break;
    }
    close(listenfd);
}

然后使用gcc编译一下:

root@ecs-2f21:~/hinusDocs/cpp# gcc -o io io.c

使用这段Java客户端代码去连接服务端,可以看到服务端会打印出"hello world"


public class NetClient {
    public static void main(String[] args) {
        try {
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.connect(new InetSocketAddress("127.0.0.1", 8001));

            socketChannel.configureBlocking(true);

            ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
            writeBuffer.put("hello world!".getBytes());
            writeBuffer.flip();

            socketChannel.write(writeBuffer);
            socketChannel.close();
        } catch (IOException e) {
        }
    }
}

非阻塞式IO

我们上节课讲了非阻塞式IO,在 linux 上,要做到这一点,就要使用 fcnt 这个函数。具体地说,就是把原来的recv变成这样:

        int flags = fcntl(sock_fd, F_GETFL, 0); 
        fcntl(sock_fd, F_SETFL,flags | O_NONBLOCK);

        int total = 0;

        while (total < 12) {
            n = recv(sock_fd, buff, MAXLINE, 0); 
            if (n >= 0) {
              printf("I can do something else %d\n", n); 
              total += n;
            }   
        }

通过设置sockfd,把它改成非阻塞的。这样一来,recv方法不管是否读到数据,都会立即返回,所以我们会看到,当n等于0的时候,就会有大量的打印。也就是说,非阻塞的socket给我们提供了一种可能:检查一下socket上有没有数据,如果有数据,就可以取到这个数据,执行后面的逻辑,如果没有数据,那么recv函数也会立即返回,可以做点其他的事情。

这种IO模型就是非阻塞的。虽然recv函数没有直接让线程休眠,但本质上,我们使用了一个自旋在这里不断地检查数据,直到数据到达以后,才会跳出这个循环。所以,这种写法仍然是服务端在等待客户端,仍然是一种同步模型。

同步就一定阻塞吗?

通过上面的例子,我们也看到了,同步和阻塞并不是等价的。同步的意义只是说客户端发过来的数据到达之前,我干不了其他的事情。而阻塞强调的是调用一个函数,线程会不会休眠。在上面的非阻塞模型里,虽然调用recv不会再使线程休眠了,但程序并没有去执行什么有效的逻辑,所以这本质上仍然是一个同步模型。

Java中的非阻塞

在Java中要使用非阻塞非常简单,只需要在socketChannel上调用:

socketChannel.configureBlocking(false);

我们来看一下,它的具体实现:

在IDE里通过查看JDK源码可以找到:

    protected void implConfigureBlocking(boolean var1) throws IOException {
        IOUtil.configureBlocking(this.fd, var1);
    } // in SocketChannelImpl

然后在IOUtil里看到这是一个 static native 方法:

public static native void configureBlocking(FileDescriptor var0, boolean var1) throws IOException;

这个方法的具体实现位于 jdk/src/solaris/native/sun/nio/ch/IOUtil.c 中:

static int 
configureBlocking(int fd, jboolean blocking)
{
    int flags = fcntl(fd, F_GETFL);
    int newflags = blocking ? (flags & ~O_NONBLOCK) : (flags | O_NONBLOCK);

    return (flags == newflags) ? 0 : fcntl(fd, F_SETFL, newflags);
}

JNIEXPORT void JNICALL
Java_sun_nio_ch_IOUtil_configureBlocking(JNIEnv *env, jclass clazz,
                                         jobject fdo, jboolean blocking)
{
    if (configureBlocking(fdval(env, fdo), blocking) < 0)
        JNU_ThrowIOExceptionWithLastError(env, "Configure blocking failed");
}

哈哈,下面是configureBlocking的JNI定义,上面的那个是真正的实现。原来,JDK中也不过是使用 fcntl 来实现设置 socket 是否阻塞这个功能的。可见,要想真正地理解Java 在服务器上做了什么事情,必须有很好的服务端编程能力。而我们的课程就都集中在这一点上,这也是通过其他渠道学Java往往很难能学到的地方。

好了,今天就到这里。

作业:

1. 使用Java写一版非阻塞的服务端程序,和本节课中的C代码对应的。

2. 上节课我们讲了5种模型,今天给出了前两种模型的例子。对照本节课的例子,再去理解上节课的理论。

上一节课:nio(3): IO模型

下一节课:Java NIO(5): IO多路复用

目录:课程目录

编辑于 2017-06-16

文章被以下专栏收录