本节课是小密圈《进击的Java新人》第十六周第四节课。这一节课,我们通过 linux 上的 socket 编程,来说明一下IO模型。
我们通过继续第八周的课程:Java网络编程(二):套接字,来看一下正式的网络编程的例子。
阻塞式IO
我们先看一个阻塞式的例子:
#include#include#include#include#include#include#include
#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种模型,今天给出了前两种模型的例子。对照本节课的例子,再去理解上节课的理论。