[TOC]
BIO:同步阻塞模型
在传统的同步阻塞模型中,ServerSocket负责绑定IP地址,监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。 采用BIO通信模型的Server端,由一个Acceptor线程负责监听客户端的连接,它接收到客户端的连接后,为每一个客户端创建一个新的线程进行链路处理,完成后,通过输出流返回客户端,线程销毁。该模型最大的缺点就是缺乏伸缩能力,当客户端并发量增加的时候,服务端的线程和客户端的线程成1:1的正比关系,当线程数过多系统的性能将急剧下降,最终导致宕机,不能对外提供服务的严重情况。
Server:
package netty.bio;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author wang.xw
* @date 2018/3/13 13:44.
*/
public class TimeServer {
private static final int DEFAULT_SERVER_PORT = 8081;
public static void main(String[] args) {
ServerSocket server = null;
try {
server = new ServerSocket(DEFAULT_SERVER_PORT);
System.out.println("The time server is start in port :" + DEFAULT_SERVER_PORT);
while (true) {
System.out.println("...");
Socket socket = server.accept();
new Thread(new TimerServerHadler(socket)).start();
}
} catch (IOException e) {
} finally {
if (server != null) {
try {
server.close();
System.out.println("server close");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
package netty.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
/**
* @author wang.xw
* @date 2018/3/13 13:54.
*/
public class TimerServerHadler implements Runnable {
private Socket socket;
public TimerServerHadler(Socket socket) {
this.socket = socket;
}
public void run() {
BufferedReader in = null;
PrintWriter out = null;
String currentTime = null;
try {
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
out = new PrintWriter(this.socket.getOutputStream(), true);
String body = null;
while (true) {
body = in.readLine();
System.out.println("The TimeServer receive order : " + body);
currentTime = "query time".equalsIgnoreCase(body) ? System.currentTimeMillis() + "" : "bad order";
// 返回客户端
out.println(currentTime);
}
} catch (IOException e) {
if (in != null) {
try {
in.close();
} catch (IOException e1) {
e.printStackTrace();
}
}
if (out != null) {
out.close();
}
if (socket != null) {
try {
socket.close();
} catch (IOException e1) {
e.printStackTrace();
}
}
}
}
}
client:
package netty.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
/**
* @author wang.xw
* @date 2018/3/13 14:28.
*/
public class TimeClient {
private static final String DEFAULT_SERVER_IP = "localhost";
private static final int DEFAULT_SERVER_PORT = 8081;
public static void main(String[] args) {
BufferedReader in = null;
PrintWriter out = null;
Socket socket = null;
try {
socket = new Socket(DEFAULT_SERVER_IP, DEFAULT_SERVER_PORT);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
// 读取控制台消息
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
out.println(scanner.nextLine());
System.out.println("send to server success !");
String res = in.readLine();
System.out.println("server return : " + res);
}
} catch (IOException e) {
//不处理
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (out != null) {
out.close();
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
伪异步I/O模型:
伪异步I/O采用线程池管理管理这些线程,因为线程池和消息队列都是有界的,因此避免了由于客户端并发数量大,导致内存溢出或者宕机。而对Socket的输入流进行读取时,会一直阻塞,直到发生:
- 有数据可读
- 可用数据以及读取完毕
- 发生空指针或I/O异常
所以在读取数据较慢时(比如数据量大、网络传输慢等),大量并发的情况下,其他接入的消息,只能一直排入到消息队列等待。严重情况下会导致所有客户端都连接超时。
Server:
package netty.pesudo;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author wang.xw
* @date 2018/3/13 13:44.
*/
public class TimeServer {
private static final int DEFAULT_SERVER_PORT = 8081;
public static void main(String[] args) {
ServerSocket server = null;
try {
server = new ServerSocket(DEFAULT_SERVER_PORT);
System.out.println("The time server is start in port :" + DEFAULT_SERVER_PORT);
System.out.println("corePoolSize :" + Runtime.getRuntime().availableProcessors());
while (true) {
System.out.println("...");
Socket socket = server.accept();
TimerServerHadlerExecutePool executePool = new TimerServerHadlerExecutePool(50, 1000);
executePool.execute(new TimerServerHadler(socket));
}
} catch (IOException e) {
} finally {
if (server != null) {
try {
server.close();
System.out.println("server close");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
自定义线程池:
package netty.pesudo;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author wang.xw
* @date 2018/3/14 10:22.
*/
public class TimerServerHadlerExecutePool {
private ExecutorService executor;
public TimerServerHadlerExecutePool(int maxPoolSize, int queueSize) {
executor = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(),
maxPoolSize,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(queueSize)
);
}
public void execute(Runnable task) {
executor.execute(task);
}
}
NIO:同步非阻塞I/O模型
NIO是基于缓冲区编程的,先要了解几个重要的概念
1. 缓冲区Buffer
Buffer是一个对象,包含一些要写入或者读出的数据,在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,也是写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。提供了对数据结构化访问以及维护读写位置(limit)等信息。具体的缓存区有这些:ByteBuffe、CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。他们实现了相同的接口:Buffer。
2. 通道Channel
Channel是一个通道,它像自来水管一样,网络数据通过Channel读取和写入,通道和流的不同之处在于通道是双向的而流是单向的(一个流必须是InputStream或者OutputStream的子类),而通道可以同时用于读和写。因为Channel是全双工的所以它比流能更好的映射底层的操作系统的api.
3. 多路复用器Selector
Selector提供选择已经就绪的任务的能力:Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。 一个Selector可以同时轮询多个Channel,因为JDK使用了epoll()代替传统的select实现,所以没有最大连接句柄1024/2048的限制。所以,只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。
Server
public class TimeServer {
private static final int DEFAULT_SERVER_PORT = 8081;
public static void main(String[] args) {
TimeServerHandler timeServer = new TimeServerHandler(DEFAULT_SERVER_PORT);
new Thread(timeServer).start();
System.out.println("main thread id: " + Thread.currentThread().getId());
}
}
--
package netty.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.Iterator;
import java.util.Set;
/**
* @author wang.xw
* @date 2018/3/15 11:21.
*/
public class TimeServerHandler implements Runnable {
private Selector selector;
private ServerSocketChannel channel;
private volatile boolean start = false;
/**
* 初始化多路复用器,绑定监听端口号
*
* @param port
*/
public TimeServerHandler(int port) {
try {
selector = Selector.open();
// 1.打开ServerSocketChannel,监听客户端连接,是所有客户端连接的父管道
channel = ServerSocketChannel.open();
// 2.监听端口号,设置为非阻塞模式
channel.socket().bind(new InetSocketAddress(port), 1024);
channel.configureBlocking(false);
// 3.监听客户端连接请求
channel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("The time server is start in port :" + port);
System.out.println("The Selector Thread id :" + Thread.currentThread().getId());
} catch (IOException e) {
e.printStackTrace();
// 资源初始化失败系统退出
System.exit(1);
}
}
@Override
public void run() {
while (!start) {
if (!selector.isOpen()) {
System.out.println("selector is closed");
break;
}
try {
// 每隔一秒唤醒一次
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
//selector关闭后会自动释放里面管理的资源
if (selector != null)
try {
selector.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
if (key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//通过ServerSocketChannel的accept创建SocketChannel实例
//完成该操作意味着完成TCP三次握手,TCP物理链路正式建立
SocketChannel sc = ssc.accept();
//设置为非阻塞的
sc.configureBlocking(false);
//注册为读操作
sc.register(selector, SelectionKey.OP_READ);
}
// 读操作
if (key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
//创建ByteBuffer,并开辟一个1M的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取请求码流,返回读取到的字节数
int readBytes = sc.read(buffer);
if (readBytes > 0) {
//将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作
buffer.flip();
//根据缓冲区可读字节数创建字节数组
byte[] bytes = new byte[buffer.remaining()];
//将缓冲区可读字节数组复制到新建的数组中
buffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("The TimeServer receive order : " + body);
String currentTime = "query time".equalsIgnoreCase(body) ? System.currentTimeMillis() + "" : "bad order";
//发送应答消息
doWrite(sc, currentTime);
} else if (readBytes < 0) {
key.cancel();
sc.close();
}
}
}
}
// 写回客户端
private void doWrite(SocketChannel channel, String response) throws IOException {
if (response != null && response.trim().length() > 0) {
byte[] bytes = response.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
channel.write(writeBuffer);
}
}
}
Client
package netty.nio;
/**
* @author wang.xw
* @date 2018/3/15 14:32.
*/
public class TimeClient {
private static final String DEFAULT_SERVER_IP = "localhost";
private static final int DEFAULT_SERVER_PORT = 8081;
public static void main(String[] args) {
new Thread(new TimeClientHandler(DEFAULT_SERVER_IP, DEFAULT_SERVER_PORT)).start();
System.out.println("main thread id: " + Thread.currentThread().getId());
}
}
--
package netty.nio;
import java.io.IOException;
import java.io.PrintWriter;
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.Scanner;
import java.util.Set;
/**
* @author wang.xw
* @date 2018/3/15 14:34.
*/
public class TimeClientHandler implements Runnable {
private transient String host;
private transient int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean start = true;
public TimeClientHandler(String host, int port) {
this.host = host;
this.port = port;
try {
// 创建选择器
selector = Selector.open();
// 打开SocketChannel,绑定客户端本地地址
socketChannel = SocketChannel.open();
// 设置为非阻塞模式
socketChannel.configureBlocking(false);
System.out.println("The client Thread id :" + Thread.currentThread().getId());
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
@Override
public void run() {
try {
doConnect();
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
while (start) {
if (!selector.isOpen()) {
System.out.println("selector is closed");
break;
}
System.out.println("...");
try {
// 每隔一秒唤醒一次
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
SocketChannel sc = (SocketChannel) key.channel();
// 判断是否处于连接状态,服务端是否返回ack消息
if (key.isConnectable()) {
// 判断客户端是否连接成功
if (sc.finishConnect()) {
//注册为读操作
sc.register(selector, SelectionKey.OP_READ);
doWrite(sc);
} else {
System.exit(1);
}
}
if (key.isReadable()) {
//创建ByteBuffer,并开辟一个1M的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取请求码流,返回读取到的字节数
int readBytes = sc.read(buffer);
if (readBytes > 0) {
//将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作
buffer.flip();
//根据缓冲区可读字节数创建字节数组
byte[] bytes = new byte[buffer.remaining()];
//将缓冲区可读字节数组复制到新建的数组中
buffer.get(bytes);
String res = new String(bytes, "UTF-8");
System.out.println("server return : " + res);
this.start = false;
} else if (readBytes < 0) {
key.cancel();
sc.close();
}
}
}
}
private void doWrite(SocketChannel sc) throws IOException {
Scanner scanner = new Scanner(System.in);
byte[] req = scanner.next().getBytes();
ByteBuffer buffer = ByteBuffer.allocate(req.length);
buffer.put(req);
buffer.flip();
sc.write(buffer);
System.out.println("send to server success !");
}
private void doConnect() throws IOException {
// 如果直接连接成功,则直接注册到多路复用器上,进行读操作
if (socketChannel.connect(new InetSocketAddress(host, port))) {
//注册为读操作
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
} else {
// 等待服务器返回TCP syn-ack
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
}
}
JDK原生的NIO类库编程十分复杂,而且还有一个严重epollbug.所以不推荐被直接拿来使用。在NIO2.0的时候引入了AIO的概念,AIO真的实现了异步非阻塞I/O,他不需要通过多路复用器对注册的管道进行轮询操作就能够实现异步读写。只有了解了基本的I/O模型我们才能更深入的研究netty。