说到Netty,不得不说到JDK的IO/NIO,最近抽空系统看了下IO与NIO的相关知识,也对JDK的IO以及NIO的设计有了新的理解,在开始介绍之前,需要先了解清楚几个概念,分别是同步/异步,阻塞/非阻塞。
同步/异步:对应操作系统内核层面,即具体的动作的实施者,我们所有的IO/NIO操作最终都是通过jvm调用native接口(封装的操作系统IO/NIO相关操作接口),底层的实现其实都是同步的。异步的实现基于AIO,不在讨论范围内
阻塞/非阻塞:由于操作系统层面,线程/进程都是相对昂贵的资源,如果由于底层操作系统的同步等待数据导致应用程序的线程/进程一直处于等待数据的阻塞状态,这时的线程资源其实是属于闲置浪费的。
与之对应的组合可以分为如下几种: 同步阻塞,同步非阻塞,异步阻塞,异步非阻塞,考虑到异步与AIO有关,这里我们先不做探讨,只说明前两种。
同步阻塞:比如去餐馆点餐,在餐馆前台点了一份宫保鸡丁盖浇饭,这时候服务员告诉厨师来一份宫保鸡丁盖浇饭,厨师(操作系统)开始同步的准备,这时候作为点餐的你,啥也不做,站在那等着你的宫保鸡丁盖浇饭准备好。
同步非阻塞:上述一样的场景,只是你在前台点好餐以后,找个合适的空位,打开你的手机刷刷新闻,将原先阻塞等待的动作变成的刷手机新闻,即将空闲的线程资源得到充分利用,玩一段时间抬头问问前台好了没就可以了,而没必要一直傻等着。
对应到JDK里面,同步阻塞的实现IO以及NIO都支持,而同步非阻塞只有NIO支持,以同步阻塞的IO为例:
客户端
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 8080);
BufferedReader reader = null;
BufferedWriter writer = null;
String input;
try {
reader = new BufferedReader(new InputStreamReader(System.in));
OutputStream out;
writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
while (true){
writer.write(reader.readLine());
writer.write("\n");
writer.flush();
}
}finally {
if(reader != null){
reader.close();
}
if(writer != null){
writer.close();
}
if(socket != null){
socket.shutdownOutput();
}
}
}
服务端
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(18080);
String input;
BufferedReader reader = null;
BufferedWriter writer = null;
Socket socket = null;
try {
while (true){
System.out.println("等待客户端连接");
socket = serverSocket.accept();
System.out.println("接收到客户端连接,准备接收客户端信息");
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("测试使用");
while ((input = reader.readLine()) != null){
System.out.println("接受到客户端的请求:" + input);
}
OutputStream out;
writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
writer.write("服务端返回当前时间:" + LocalDateTime.now());
writer.flush();
}
}finally {
if(reader != null){
reader.close();
}
if(writer != null){
writer.close();
}
if(socket != null){
socket.close();
}
}
}
上述就是同步阻塞的IO的例子(代码写的比较糙,作为一个例子说明意图),同步的意思上述说明已经很明显,这边说一下上述Server端代码中阻塞相关的内容。
- socket = serverSocket.accept(),切到对应的源码中查看相关信息,注释比较明确,server端启动的时候,就会阻塞,直到有客户端的连接进来
/**
* Listens for a connection to be made to this socket and accepts
* it. The method blocks until a connection is made.
*
* <p>A new Socket {@code s} is created and, if there
* is a security manager,
* the security manager's {@code checkAccept} method is called
* with {@code s.getInetAddress().getHostAddress()} and
* {@code s.getPort()}
* as its arguments to ensure the operation is allowed.
* This could result in a SecurityException.
*
* @exception IOException if an I/O error occurs when waiting for a
* connection.
* @exception SecurityException if a security manager exists and its
* {@code checkAccept} method doesn't allow the operation.
* @exception SocketTimeoutException if a timeout was previously set with setSoTimeout and
* the timeout has been reached.
* @exception java.nio.channels.IllegalBlockingModeException
* if this socket has an associated channel, the channel is in
* non-blocking mode, and there is no connection ready to be
* accepted
*
* @return the new Socket
* @see SecurityManager#checkAccept
* @revised 1.4
* @spec JSR-51
*/
public Socket accept() throws IOException {
if (isClosed())
throw new SocketException("Socket is closed");
if (!isBound())
throw new SocketException("Socket is not bound yet");
Socket s = new Socket((SocketImpl) null);
implAccept(s);
return s;
}
2.input = reader.readLine(),在读取客户端输入请求的时候,相应的readLine操作也是阻塞等待客户端的输入的,对应的源码在java.io.Reader定义的抽象接口中有说明,方法会一直阻塞知道接收到客户端输入可用。
/**
* Reads characters into a portion of an array. This method will block
* until some input is available, an I/O error occurs, or the end of the
* stream is reached.
*
* @param cbuf Destination buffer
* @param off Offset at which to start storing characters
* @param len Maximum number of characters to read
*
* @return The number of characters read, or -1 if the end of the
* stream has been reached
*
* @exception IOException If an I/O error occurs
* @exception IndexOutOfBoundsException
* If {@code off} is negative, or {@code len} is negative,
* or {@code len} is greater than {@code cbuf.length - off}
*/
public abstract int read(char cbuf[], int off, int len) throws IOException;
3.writer.write("服务端返回当前时间:" + LocalDateTime.now()),往客户端发送响应信息,这个比较好理解服务端在写的同时,客户端对应的读取操作跟服务端其实类似,肯定是需要等服务端写完才能读取,所以也是一个阻塞等待可用输入的过程。
上述的server端,只允许同时有一个client端接入,只有等对应处理的client结束之后,才允许后续的client接入,正常的一个优化思路是,我将accept接收到的socket交由我自定义的线程池去处理,那这样能满足多客户端的并发接入的要求了,这样是不是没有问题了?其实在接入的客户端数量少的时候,是没有问题的,但是当客户端数量上来,类似QQ/微信这样的好多客户端需要发送接收消息的系统或者一个有巨量连接的网络服务器,单纯的这么处理,线程池的线程数量以及线程上下文的切换所消耗的资源是很巨大的。而且线程池中并不是所有的客户端都在实时进行读写操作的,大部分应该处于连接状态,不做任何的任务处理。
所以这时候的关注点就要转移到刚才所说的三个阻塞的操作上去
- serverSocket.accept(),ServerSocket接口提供了一个setSoTimeout方法,可以帮助我们实现非阻塞的功能,设置一个超时时间,在到达后,捕获一个SocketTimeoutException异常,按照自定义的业务处理完继续轮询serverSocket.accept()方法,但这也是个取巧的方式,超时时间的取值不好设定。
2.read/write 相关操作在jvm提供的IO接口层面都是阻塞类型的接口,所以阻塞的线程资源无法进行释放。
所以这时候JDK推出了NIO的接口规范,也称为New IO,这个我们在下一章节继续进行分析。
上述内容为自己的相关理解,可能还有不足之处,如有问题或者更好的建议,希望不吝指出,多谢。
网友评论