传统网络服务
首先得说一说经典的网络服务的设计
伪代码:
class Server implements Runnable {
public void run() {
try {
//创建socket server套接字
ServerSocket ss = new ServerSocket(PORT);
while (!Thread.interrupted())
//启动额外线程处理socket客户端连接后的业务处理
new Thread(new Handler(ss.accept())).start();
// or, single-threaded, or a thread pool
} catch (IOException ex) { /* ... */ }
}
//业务处理
static class Handler implements Runnable {
final Socket socket;
Handler(Socket s) { socket = s; }
public void run() {
try {
byte[] input = new byte[MAX_INPUT];
socket.getInputStream().read(input);
byte[] output = process(input);
socket.getOutputStream().write(output);
} catch (IOException ex) { /* ... */ }
}
private byte[] process(byte[] cmd) { /* ... */ }
}
}
Reactor模式(观察者设计模式)
思想:分而治之+事件驱动
简单来说reactor模式用于同时处理一个或多个传递给服务端的请求的事件的处理模式。 然后,服务端处理程序解析输入别的请求,并将它们同步分派给与之关联的请求异步处理程序。不恰当可类比web页面事件,当点击某个按钮时,浏览器收到这个信号(监听),分派给相关的js处理程序处理(handler)。
注意两点:
1、同步分派 ---由Dispatch承担
2、异步处理程序---由handler处理。
一、NIO的简单介绍
Java NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始),Java NIO提供了与标准IO不同的IO工作方式。 NIO中的核心内容有Channel、Buffer、Selector,其他组件如Pipe和FileLock只不过是三个组件共同使用的工具类。
1、Channle
Channle的实现主要有FileChannel、DatagramChannel、SocketChannle、ServerSocketChannel。这些通道覆盖了文件IO、UDP和TCP网络IO
2、Buffer
Buffer的实现由ByteBuf、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer以及MappedByteBuffer(用于表示内存映射文件)。以上Buffer涵盖了能够通过IO发送的基本数据类型。
3、Selector
Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。
二、NIO在网络服务端的应用。
2.1、传统ServerSocket
当我们刚开始学习想要做一个聊天服务器的时候,最简单的做法就是使用ServerSocket来作为服务端,接收Socket客户端发送来的数据。下面的代码就是一个简单的 例子,当我们从服务端接收到数据时,新建一个线程来处理我们的数据,这种模式是一种阻塞式IO,在高并发的情况下,我们需要新建大量线程,然而程序的效率在线程数不多的情况下是随着线程的增加而增加,但是到达一定数量后是随着线程的增加而减少。所以传统阻塞式IO的瓶颈在于不能处理过多的连接。
2.2、NIO非阻塞式服务。
javaNIO中的ServerSocketChannel是一个可以监听新进来的TCP连接的通道,就像标准的ServerSocket一样。我们在这里使用NIO来搭建一个时间反馈服务。
服务器端:
Reactor不仅监控所有的连接请求,还监控其他读请求。我们将多个channle注册到selector中,其中serverSocketChannle只注册了OP_ACCEPT事件,而socketChannel只注册了OP_READ事件。
Select调用select()方法后,检测所有注册的通道,返回给Reactor(反应器)我们感兴趣的事件已经准备就绪的那些通道,然后Reactor分发给不同的handler处理(handlerAccept、handlerReader)。
package reactor;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Date;
import java.util.Iterator;
public class NIOServer {
public static void main(String[] args)throws IOException{
//创建一个NIOsocket服务
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//将ServerSocketChannel设置为非阻塞
serverSocketChannel.configureBlocking(false);
//serverSocketChannel.bind(new InetSocketAddress(8989));
serverSocketChannel.socket().bind(new InetSocketAddress(8989));
//调用 Selector.open()方法创建一个selector
Selector selector = Selector.open();
//将serverSocketChannel注册到selector中,监听TCP连接
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true){
//这条语句会阻塞
int nkey = selector.select();
//也可以设置超时时间,防止进程阻塞
//int nkey = selector.select(long timeout);
System.out.println("nkey"+nkey);
if(nkey>0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
int i = 0;
while (iterator.hasNext()) {
i++;
System.out.println("iteratorLength" + i);
SelectionKey key = iterator.next();
iterator.remove();
reactor(key, selector);
}
}
}
}
public static void reactor(SelectionKey key,Selector selector) throws IOException {
if (key.isAcceptable()) {
//接收就绪状态
handlerAccept(key,selector);
} else if (key.isReadable()) {
//读就绪状态
handlerReader(key);
}
}
public static void handlerAccept(SelectionKey key,Selector selector) throws IOException {
ServerSocketChannel sever = (ServerSocketChannel) key.channel();
SocketChannel channel = sever.accept();
channel.configureBlocking(false);
System.out.println("客户端连接" + channel.toString());
//将通道注册到selector中
//此时Selector监听channel并对read感兴趣
channel.register(selector, SelectionKey.OP_READ);
}
public static void handlerReader(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel();
//读取部分消息,不确定数据长度只用了1024个字节长度接收,当数据大于1024时,会丢失数据,当数据只有几十个字节时会浪费内存空间,当并发数据量大时会造成不必要的浪费
// 文章 http://ifeve.com/non-blocking-server/
// http://tutorials.jenkov.com/java-performance/resizable-array.html
// 中介绍了如何实现这样支持可调整大小的数组的内存缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//不会阻塞
int n =0;
//此处需要捕获异常,否则若客户端强制关闭,服务器会报“Java.io.IOException: 远程主机强迫关闭了一个现有的连接。”,
// 并且服务器会在报错后停止运行,错误的意思就是客户端关闭了,但是服务器还在从这个套接字通道读取数据,便抛出IOException,
// 导致这种情况出现的原因就是,客户端异常关闭后,服务器的选择器会获取到与客户端套接字对应的套接字通道SelectionKey,
// 并且这个key的兴趣是OP_READ,执行从这个通道读取数据时,客户端已套接字已关闭,所以会出现“java.io.IOException: 远程主机强迫关闭了一个现有的连接”的错误。
// 所以在服务器在读取数据时,若发生异常,则取消当前key并关闭通道,如下代码:
try {
n = socketChannel.read(buffer);
}catch (Exception e){
e.printStackTrace();
key.cancel();
socketChannel.socket().close();
socketChannel.close();
return;
}
System.out.println("读取字节数"+n);
if (n > 0) {
byte[] data = buffer.array();
ByteBuffer rspbuffer= ByteBuffer.allocate(1024);
String op = new String(data, 0, n);
System.out.println("服务端收到信息:" + op);
if(op.equals("time")) {
String currentTime = "time".equalsIgnoreCase(op)?new Date(System.currentTimeMillis()).toString():"Bad Order";
rspbuffer.put(currentTime.getBytes());
}
rspbuffer.flip();
socketChannel.write(rspbuffer);
} else {
System.out.println("clinet is close");
key.cancel();
}
}
}
客户端:
package reactor;
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 ClientHandler implements Runnable {
String host;
int port;
Selector selector;
SocketChannel socketChannel;
volatile boolean stop;
public ClientHandler(String host,int port) {
this.host = host;
this.port = port;
try {
//
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
selector = Selector.open();
} catch (IOException e) {
e.printStackTrace();
}
}
public void run() {
try {
doConnect();
}catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
while(!stop) {
try {
//最长会阻塞毫秒(参数)
selector.select(1000);
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
SelectionKey key = null;
while(iterator.hasNext()) {
key = iterator.next();
iterator.remove();
handlerInput(key);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handlerInput(SelectionKey key) throws IOException {
if(key.isValid()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
if(key.isConnectable()) {
//finishConnect方法表示完成完成连接套接字通道的过程,如果成功返回true
if(socketChannel.finishConnect()) {
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
}
}
if(key.isReadable()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int num = socketChannel.read(buffer);
if(num>0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String string = new String(bytes, "utf-8");
System.out.println("Now is:"+string);
this.stop = true;
}
}
}
}
private void doWrite(SocketChannel socketChannel) throws IOException {
byte[] requst = "time".getBytes();
ByteBuffer buffer = ByteBuffer.allocate(requst.length);
buffer.put(requst);
buffer.flip();
socketChannel.write(buffer);
}
private void doConnect() throws IOException {
//如果连接成功,则将SocketChannel注册到复用器上,监听read事件,并且发送请求数据
if(socketChannel.connect(new InetSocketAddress(this.host, this.port))) {
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
//如果没有连接成功,则说明服务端没有返回tcp握手信息,但并不代表连接失败,只需要将SocketChannel注册到
//Selector上,监听connect事件,当服务端返回syn-acy消息后,Selector就能轮询到这个SocketChannel出于连接就绪了
}else {
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
}
}