Netty权威指南读书笔记-第二章

来源:互联网 发布:淘宝怎么投诉客服人员 编辑:程序博客网 时间:2024/05/17 02:38

1.传统的BIO编程

网络编程的基本模型是Cient/Server模型,也就是两个进程之间相互通信,其中服务器提供网络位置信息(包括IP地址和监听端口号),客户端通过连接操作向服务器监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Soket)进行通信。

如下所示,BIO的服务器端通信模型:

采用BIO通信模型的服务器端通常由一个独立的Acceptor线程负责接收监听客户端的连接,接收到客户端的连接请求后为每一个客户端创建一个新的线程进行链路处理,处理完成后通过输出流应答给客户端,线程销毁,典型的一请求一应答通信模型。


服务器端代码实现:

package com.yy.test.socket.test;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.io.PrintWriter;import java.net.ServerSocket;import java.net.Socket;import java.util.Date;import jxl.common.Logger;public class TimeServer {private static Logger logger = Logger.getLogger(TimeServer.class);private static int PORT = 8085;public static void main(String[] args) throws IOException {ServerSocket srvSocket = null;Socket socket = null;try{srvSocket = new ServerSocket(PORT);System.out.println("server is started at port "+PORT);while(true){socket = srvSocket.accept();new Thread(new TimerServerHandler(socket)).start();}}catch(Exception e){logger.error("TimeServer.main error:",e);}finally{if(null != srvSocket){srvSocket.close();}}}}class TimerServerHandler implements Runnable{private Socket socket;private static Logger logger = Logger.getLogger(TimerServerHandler.class);public TimerServerHandler(Socket socket){this.socket = socket;}public void run() {PrintWriter out = null;BufferedReader in = null;try {out = new PrintWriter(socket.getOutputStream(),true);in =  new BufferedReader(new InputStreamReader(socket.getInputStream()));String line = null;while(true){line = in.readLine();if(null != line){System.out.println("server recved cmd:"+line);String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(line) ? new Date(System.currentTimeMillis()).toString():"BAD QUERY";out.println(currentTime);}}} catch (IOException e) {logger.error("TimerServerHandler.run error:",e);}finally{closeSocket(socket);closeBufferedReader(in);if(null != out){out.close();}}}public void closeBufferedReader(BufferedReader in){if(null != in){try {in.close();} catch (IOException e) {logger.error("TimerServerHandler.closeBufferedReader error:",e);}}}public void closeSocket(Socket socket){if(null != socket){try {socket.close();} catch (IOException e) {logger.error("TimerServerHandler.closeSocket error:",e);}}}}

客户端代码实现:

package com.yy.test.socket.test;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.io.PrintWriter;import java.net.Socket;import org.apache.log4j.Logger;public class TimeClient {public static Logger logger = Logger.getLogger(TimeClient.class);public static void main(String[] args) {Socket socket = null;PrintWriter out = null;BufferedReader in = null;int PORT = 8085;try{socket = new Socket("127.0.0.1",PORT);out = new PrintWriter(socket.getOutputStream(),true);in =  new BufferedReader(new InputStreamReader(socket.getInputStream()));out.println("QUERY TIME ORDER");System.out.println("send query cmd 2 server success!");String response = in.readLine();System.out.println("now time is "+response);}catch(Exception e){logger.error("TimeClient.main error:",e);}finally{closeSocket(socket);closeBufferedReader(in);if(null != out){out.close();}}}public static void closeBufferedReader(BufferedReader in){if(null != in){try {in.close();} catch (IOException e) {logger.error("TimeClient.closeBufferedReader error:",e);}}}public static void closeSocket(Socket socket){if(null != socket){try {socket.close();} catch (IOException e) {logger.error("TimeClient.closeSocket error:",e);}}}}

运行结果:



缺点:缺乏弹性伸缩能力,当客户端并发访问量增大时,服务器端的线程数同客户端并发访问数呈1:1的正比关系,由于线程是java虚拟机宝贵的系统资源,当线程膨胀之后,系统的性能将急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或僵死,不能对外提供服务。

2.伪异步IO编程

采用线程池和任务队列可以实现一种叫做伪异步的IO通信框架,模型如下图。

当有新的客户端接入的时候,将客户端的Socket封装成一个Task(实现了Runnable接口)任务投递到后台的线程池中进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此它的资源占用是可控的,无论多少客户端并发访问,都不会导致资源的耗尽和宕机。

服务器端代码实现:

package com.yy.test.threadpool;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.io.PrintWriter;import java.net.ServerSocket;import java.net.Socket;import java.util.Date;import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.ExecutorService;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;import jxl.common.Logger;public class TimeServer {private static Logger logger = Logger.getLogger(TimeServer.class);private static int PORT = 8085;public static void main(String[] args) throws IOException {ServerSocket srvSocket = null;Socket socket = null;try{srvSocket = new ServerSocket(PORT);System.out.println("server is started at port "+PORT);TimerServerHandlerExcutePool pool = new TimerServerHandlerExcutePool(100,5);while(true){socket = srvSocket.accept();pool.execute(new TimerServerHandler(socket));}}catch(Exception e){logger.error("TimeServer.main error:",e);}finally{if(null != srvSocket){srvSocket.close();}}}}class TimerServerHandlerExcutePool{private ExecutorService pool ;public TimerServerHandlerExcutePool(int queueSize,int nThreads){pool = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), nThreads, 120l, TimeUnit.SECONDS, new ArrayBlockingQueue<runnable>(queueSize));}public void execute(Runnable task) {pool.execute(task);}}class TimerServerHandler implements Runnable{private Socket socket;private static Logger logger = Logger.getLogger(TimerServerHandler.class);public TimerServerHandler(Socket socket){this.socket = socket;}public void run() {PrintWriter out = null;BufferedReader in = null;try {out = new PrintWriter(socket.getOutputStream(),true);in =  new BufferedReader(new InputStreamReader(socket.getInputStream()));String line = null;while(true){line = in.readLine();if(null != line){System.out.println("server recved cmd:"+line);String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(line) ? new Date(System.currentTimeMillis()).toString():"BAD QUERY";out.println(currentTime);}}} catch (IOException e) {logger.error("TimerServerHandler.run error:",e);}finally{closeSocket(socket);closeBufferedReader(in);if(null != out){out.close();}}}public void closeBufferedReader(BufferedReader in){if(null != in){try {in.close();} catch (IOException e) {logger.error("TimerServerHandler.closeBufferedReader error:",e);}}}public void closeSocket(Socket socket){if(null != socket){try {socket.close();} catch (IOException e) {logger.error("TimerServerHandler.closeSocket error:",e);}}}}</runnable>

客户端代码实现:

package com.yy.test.threadpool;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.io.PrintWriter;import java.net.Socket;import org.apache.log4j.Logger;public class TimeClient {public static Logger logger = Logger.getLogger(TimeClient.class);public static void main(String[] args) {Socket socket = null;PrintWriter out = null;BufferedReader in = null;int PORT = 8085;try{socket = new Socket("127.0.0.1",PORT);out = new PrintWriter(socket.getOutputStream(),true);in =  new BufferedReader(new InputStreamReader(socket.getInputStream()));out.println("QUERY TIME ORDER");System.out.println("send query cmd 2 server success!");String response = in.readLine();System.out.println("now time is "+response);}catch(Exception e){logger.error("TimeClient.main error:",e);}finally{closeSocket(socket);closeBufferedReader(in);if(null != out){out.close();}}}public static void closeBufferedReader(BufferedReader in){if(null != in){try {in.close();} catch (IOException e) {logger.error("TimeClient.closeBufferedReader error:",e);}}}public static void closeSocket(Socket socket){if(null != socket){try {socket.close();} catch (IOException e) {logger.error("TimeClient.closeSocket error:",e);}}}}

运行结果:


伪异步IO服务器的主线程首先创建了一个线程池,当接到新的客户端连接时,将客户端的Socket封装成一个Task,然后调用线程池的execute方法执行,避免了每个请求接入都创建一个新线程。

由于线程池和消息队列都是有界的,因此无论客户端并发连接数多大,都不会导致线程个数过于膨胀或者内存溢出,相比于传统的一连接一线程模型,是一种改良。但是由于底层仍然采用同步阻塞模型,因此无法从根本上解决问题。

可以从源码中看出,当对Socket的输入流进行读取操作的时候,它会一直阻塞下去,直到发生下面几种情况:

有数据可读

可用数据已读完

异常发生


这意味着如果对方发送请求或者应答消息比较缓慢,或者网络传输较慢时,读取输入流一方的通信线程将长时间阻塞,在此期间其他接入消息只能在消息队列中排队。

接下来看一下输出流,不知为什么JDK1.7没有找到对OutputStream会阻塞的描述,只好截原书的图了。


当调用OutputStream的write方法的时候,它将会被阻塞,直到所有要发送的字节全部写入完毕,或者发生异常。学习过TCP/IP的人都知道,当消息的接收方处理缓慢的时候,将不能及时的从TCP缓冲区读取数据,这将会导致发送方的窗口大小不断减小,直到为0,双方处于Keep-alive状态,消息发送方将不能再向TCP缓冲区写消息,这时如果采用同步阻塞IO,操作将会被无限期阻塞,直到TCP窗口大小大于为0或者发生IO异常。

通过对输入流和输出流的AP文档I进行分析,我们了解到读和写都是同步阻塞的。阻塞的时间取决于对方IO的处理速度和网络IO的传输速度。事实上,我们无法保证生产环境上网络情况和对端的应用程序能足够快,如果我们的应用依赖对方的处理速度,它的可靠性就非常差。也许在本地和测试环境一切OK,但是一旦上线运行,面对恶劣的网络环境和良莠不齐的第三方服务,问题就会像火山一样喷发。

伪异步IO仅仅是对传统的BIO一个简单的优化,并不能从根本上解决同步IO导致的通信线程阻塞的问题。如果通信双方应答时间过长将会引起级联故障:

服务器端处理缓慢,返回应答消息花费60s,平时只需要10ms。

采用伪异步IO的线程正在读取故障服务节点的响应,由于读取输入流是阻塞的,它将会被同步阻塞60s。

假如所有可用的线程都被服务器阻塞,那后续所有的IO消息都将在队列中排队。

由于线程池采用阻塞队列实现,如果队列满,后续如队列的操作将会被阻塞。

由于前端只有一个线程Acceptor接收客户端接入,它被阻塞在线程池的同步阻塞队列的入队操作上,新的客户端连接请求将会被拒绝,客户端将会发生大量的连接超时。

由于几乎所有的连接请求都超时,调用者会认为服务器已经崩溃,无法接收新的请求。

3.NIO编程

比起New I/O ,NIO(Non-block I/O)非阻塞I/O更能体现NIO的特点。Java NIO弥补了同步阻塞I/O的不足,提供了高速的、面向块的I/O。通过定义包含数据的类,以块的形式处理数据。

与ServerSocket和Socket类似,NIO也提供了ServerSocketChannel和SocketChannel两种套接字通道,并且都支持阻塞和非阻塞两种模式。阻塞模式使用非常简单,但是可靠性和性能都不好。非阻塞模式刚好相反,一般开发人员要根据自己的需求来选择合适的模式。一般来说,低负载、低并发的应用程序可以选择同步阻塞I/O来降低编程复杂度,但是对于高负载、高并发的网路应用,需要使用NIO的非阻塞模式开发。

3.1 NIO类库简介

3.1.1  缓冲区Buffer:Buffer是一个包含一些要写入的或者要读出的数据,在NIO类库中加入Buffer,体现了新库和原I/O的一个重要区别。在面向流的I/O中,可以将数据直接写入或者将数据直接读到Strean对象中。在NIO库中,所有的数据都是通过缓冲区处理的。在读数据时,它是直接读到缓冲区中的;在写数据时,写到缓存区去。任何时候访问NIO,都是通过缓冲区进行操作。缓冲区其实是一个数组,通常是一个字节数组(ByteBuffer),也可以是其他类型的数组,但是缓冲区不仅仅是一个数组,同时也提供了对数据的结构化访问以及维护读写位置等信息。最常用的缓冲区是ByteBuffer,提供了一组操作用于操作字节数组,其实每一种数据类型都对应一种缓冲区,比如CharBuffer,IntBuffer,FloatBuffer,BooleanBuffer,LongBuffer,DoubleBuffer等。每一个类都是Buffer接口的实现类,除ByteBuffer类,其他Buffer都具有完全一样的操作,只是处理的数据类型不一样。因为大多数IO操作都是使用ByteBuffer,所以ByteBuffer还提供一些其他的操作,方便网络读写。


3.1.2  通道 Channel:Channel是一个通道,可以通过它读写数据,它就像自来水管一样,网络数据通过它读取和写入。通道和流的不同之处是通道是双向的,流只能在一个方向上移动(一个流必须是InpuutStream或者OutputStream的子类),并且通道可以同时用于读、写或者同时读写。因为Channel是全双工的,所以可以比流更好的映射底层操作系统的API,特别是在Unix网路编程中,底层操作系统的通道都是全双工的,同时支持读写操作。实际上,Channel可以分为两大类,分别是用于网络读写的SelectableChannel和用于文件操作的FileChannel。


3.1.3  多路复用器 Selector:

多路复用器提供选择已经就绪的任务的能力,Selector会不断地轮询注册在其上的Channel,如果某个Channel上有新的TCP连接接入、读和写操作,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectorKey可以获取就绪Channel的集合,进行后续的I/O操作。一个Selector可以同时轮询多个Channel,因为JDK使用了epoll代替传统的select实现,所以它并没有最大句柄数1024/2048|的限制,意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端,这确实是一个巨大的进步。

3.2 NIO服务器端序列图



步骤一:打开ServerSocketChannel ,用于监听客户端的连接,它是所有Socket连接的父管道。

ServerSocketChannel acceptorSrv = ServerSocketChannel.open();

步骤二:绑定监听端口,设置连接为非阻塞模式。

acceptorSrv.bind(new InetSocketAddress(InetAddress.getByName("IP"),PORT));
acceptorSrv.configureBlocking(false);

步骤三:创建Reactor线程,创建多路复用器并启动线程。

Selector selector = Selector.open();
new Thread(new ReactorTask()).start();

步骤四:将ServerSocketChannel注册到Reactor线程的多路复用器Selector 上,监听ACCEPT事件。

SelectionKey key = acceptorSrv.register(selector, SelectionKey.OP_ACCEPT);

步骤五:多路复用器在线程run方法的无限循环体内轮询准备就绪的Key。

Set selectKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectKeys.iterator();
while(it.hasNext()){
SelectionKey currentKey = it.next();
//deal with I/O event
}

步骤六:多路复用器监听到有新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理链路。

SocketChannel socketChannel =  acceptorSrv.accept();

步骤七:设置客户端链路为非阻塞模式。

socketChannel.configureBlocking(false);
socketChannel.socket().setReuseAddress(true);

步骤八:将新接入的客户端连接注册到Reactor线程的多路复用器Selector 上,监听读操作,用于读取客户端发来的网络消息。

SelectionKey key = socketChannel.register(selector, SelectionKey.OP_READ);

步骤九:异步读取客户端请求消息到缓冲区。

ByteBuffer receivedBuffer = ByteBuffer.allocate(1024);

int readNumber= socketChannel.read(receivedBuffer);

步骤十:对ByteBuffer进行编解码,如果有半包消息指针reset,继续读取后续的报文,将解码成功的消息封装成Task,投递到业务线程池中,进行业务逻辑处理。

步骤十一:将POJO对象encode成ByteBuffer,调用SocketChannel的异步write方法,将消息异步发送给客户端。

socketChannel.write(buffer);

服务器端代码实现:

package com.yy.test.nio;import java.io.IOException;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.channels.SelectionKey;import java.nio.channels.Selector;import java.nio.channels.ServerSocketChannel;import java.nio.channels.SocketChannel;import java.util.Date;import java.util.Iterator;import java.util.Set;import jxl.common.Logger;public class TimeServer {private static Logger logger = Logger.getLogger(TimeServer.class);private static int PORT = 8085;public static void main(String[] args) throws IOException {try{new Thread(new MultiplexerTimeServer(PORT)).start();}catch(Exception e){logger.error("TimeServer.main error:",e);}}}class MultiplexerTimeServer implements Runnable{private Selector selector;private ServerSocketChannel srvChannel;private volatile boolean stop;public MultiplexerTimeServer(int port){try {selector = Selector.open();srvChannel = ServerSocketChannel.open();srvChannel.configureBlocking(false);srvChannel.bind(new InetSocketAddress(port),1024);srvChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("Server is started at port "+port);} catch (Exception e) {System.out.println("MultiplexerTimeServer.start eror:"+e);}}public void stop(){this.stop = true;}public void handleInput(SelectionKey selectionKey) throws IOException{if(selectionKey.isValid()){//新接入的请求if(selectionKey.isAcceptable()){ServerSocketChannel srvChannel = (ServerSocketChannel)selectionKey.channel();SocketChannel socketChannel = srvChannel.accept();socketChannel.configureBlocking(false);socketChannel.register(selector, SelectionKey.OP_READ);//将新的channel注册到selector}if(selectionKey.isReadable()){SocketChannel socketChannel = (SocketChannel) selectionKey.channel();ByteBuffer readBuffer = ByteBuffer.allocate(1024);int readLength = socketChannel.read(readBuffer);if(readLength > 0){readBuffer.flip();byte[] bytes = new byte[readBuffer.remaining()];readBuffer.get(bytes);//读到bytes中去String query = new String(bytes,"utf-8");System.out.println("server received : "+query);String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(query) ? new Date(System.currentTimeMillis()).toString():"BAD QUERY";doWrite(socketChannel, currentTime);}else if(readLength < 0){selectionKey.cancel();socketChannel.close();}}}}public void doWrite(SocketChannel socketChannel ,String content) throws IOException{byte[] bytes = content.getBytes();ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);writeBuffer.put(bytes);writeBuffer.flip();socketChannel.write(writeBuffer);System.out.println("server send:"+content);}public void run() {while(!this.stop){try{selector.select();Set<selectionkey> keys = selector.selectedKeys();for(Iterator<selectionkey> it = keys.iterator();it.hasNext();){SelectionKey selectionKey = it.next();it.remove();handleInput(selectionKey);}}catch(Exception e){System.out.println("MultiplexerTimeServer.run error:"+e);}}}}</selectionkey></selectionkey>

客户端代码实现:

package com.yy.test.nio;import org.apache.log4j.Logger;public class TimeClient {public static Logger logger = Logger.getLogger(TimeClient.class);public static void main(String[] args) {int PORT = 8085;try{new Thread(new TimeClientHandler("127.0.0.1",PORT)).start();}catch(Exception e){logger.error("TimeClient.main error:",e);}}}
package com.yy.test.nio;import java.io.IOException;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.channels.SelectionKey;import java.nio.channels.Selector;import java.nio.channels.SocketChannel;import java.util.Iterator;import java.util.Set;public class TimeClientHandler implements Runnable {private String host;private int port;private Selector selector;private SocketChannel socketChannel;private volatile boolean stop;public TimeClientHandler(String host,int port){this.host = host;this.port = port;try{selector = Selector.open();socketChannel = SocketChannel.open();socketChannel.configureBlocking(false);}catch(Exception e){System.out.println("TimeClientHandler config error:"+e);}}public void doWrite(SocketChannel socketChannel) throws IOException{byte[] bytes = "QUERY TIME ORDER".getBytes();ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);writeBuffer.put(bytes);writeBuffer.flip();socketChannel.write(writeBuffer);if(!writeBuffer.hasRemaining()){System.out.println("TimeClientHandler.doWrite success!");}}public void doConnect() throws IOException{if(socketChannel.connect(new InetSocketAddress(host, port))){socketChannel.register(selector, SelectionKey.OP_READ);System.out.println("socketChannel registe suucess!");doWrite(socketChannel);}else{socketChannel.register(selector, SelectionKey.OP_CONNECT);}}public void run() {try {doConnect();} catch (IOException e) {System.out.println("TimeClientHandler.run error:"+e);}while(!stop){try {int keyCount = selector.select();Set<selectionkey> keys = selector.selectedKeys();for(Iterator<selectionkey> it = keys.iterator();it.hasNext();){SelectionKey selectionKey = it.next();it.remove();try {handleInput(selectionKey);} catch (Exception e) {if(selectionKey != null){selectionKey.cancel();if(selectionKey.channel()!=null){selectionKey.channel().close();}}System.out.println("TimeClientHandler.handleInput error:"+e);}}} catch (IOException e) {System.out.println("TimeClientHandler.select error:"+e);}}if(null != selector){try {selector.close();} catch (IOException e) {System.out.println("selector.close error:"+e);}}}private void handleInput(SelectionKey selectionKey) throws Exception {if(selectionKey.isValid()){SocketChannel socketChannel = (SocketChannel) selectionKey.channel();if(selectionKey.isConnectable()){if(socketChannel.finishConnect()){socketChannel.register(selector, SelectionKey.OP_READ);doWrite(socketChannel);}else{System.out.println("Don't connected!");}}if(selectionKey.isReadable()){ByteBuffer readBuffer = ByteBuffer.allocate(1024);int readCount = socketChannel.read(readBuffer);if(readCount > 0){readBuffer.flip();byte[] bytes = new byte[readBuffer.remaining()];readBuffer.get(bytes);String content = new String(bytes,"UTF-8");System.out.println("now is "+content);}else if(readCount < 0){//对端链路关闭selectionKey.cancel();socketChannel.close();}}}}}</selectionkey></selectionkey>

运行结果:



我们发现NIO的编程复杂度确实比同步阻塞IO大很多,如果考虑“半包读”和“半包写”,程序将会更加复杂。

使用NIO编程优点:

客户端发起的连接操作是异步的,可以通过向多路复用器注册OP_CONNECT,不会像之前的客户端那样被同步阻塞。

SocketChanel的读写操作都是异步的,如果没有可读可写的数据不会同步等待,直接返回,这样I/O通信线程可以处理其他的链路,不需要同步等待这个链路可用。

线程模型的优化,JDK采用了epoll代替传统的select,它没有连接句柄数的限制,可以同时处理成千上万的客户端连接,而且性能不会随着客户端线程的增加而下降,非常适合做高性能、高并发的网络服务器。

4. AIO编程

未完待续...

0 0
原创粉丝点击