【netty in action】学习笔记-第一章 了解java NIO(1)
学习netty,java nio是基础,因为前者是对后者的封装,当然又不只是封装。随着学习的深入你会理解这句话的含义。
下图是netty的架构图,让你对netty涉及的模型,传输,协议有个基本印象。
netty的特性可以总结为一下几点:
- 统计的API操作阻塞和非阻塞的socket
- 接口易用
- 线程模型简单而强大
- 链式调用逻辑,复用性搞高
- 文档和示例丰富
- 除了JDK之外不依赖别的组件
- 相比于java api有更好的吞吐量以及低延迟
- 减少不必要的内存占用,不会再因为快速的,慢速的或者超负荷的连接导致OOM
- 支持SSL/TLS
- 社区活跃
两种实现异步API常用的设计
基于callbacks
FetchCalback.java
public interface FetchCalback {
void onData(Data data);
void onError(Throwable cause);
}
Fetcher.java
public interface Fetcher {
void fetchData(FetchCalback fetchCalback);
}
MyFetcher.java
public class MyFetcher implements Fetcher{
@Override
public void fetchData(FetchCalback fetchCalback) {
try {
//模拟获取数据
Thread.sleep(1000);
Data data = new Data(1, 2);
fetchCalback.onData(data);
} catch (Exception e) {
fetchCalback.onError(e);
}
}
}
Worker.java
public class Worker {
public void doWorker() {
Fetcher fetcher = new MyFetcher();
fetcher.fetchData(new FetchCalback() {
@Override
public void onData(Data data) {
System.out.println("获取到数据:" + data);
}
@Override
public void onError(Throwable cause) {
System.err.println(cause.getMessage());
}
});
}
public static void main(String[] args) {
Worker worker = new Worker();
worker.doWorker();
}
}
Data.java
@Getter
@Setter
@AllArgsConstructor
@ToString
public class Data {
int a;
int b;
}
代码比较简单,调用fetchdata获取数据,拿到数据之后通过FetchCalback
回调过来,调用方不需要等着获取结果做下一步的处理。
这里可能有人疑问:上面的fetchData
方法也会阻塞啊?是的,不过那是因为这只是个示例,实际的项目中获取数据完全可以放在线程池里异步处理。
基于回调的异步设计方案在很多其它语言也很流行,比较典型的就是javascript,里面大量的回调,给你一段代码感受下:
let p = new Promise((resolve, reject) => {
setTimeout(resolve, 1000, 'success');
});
p.then(
res => {
console.log(res);
return `${res} again`;
}
)
.then(
res => console.log(res)
);
基于futures的异步设计
Futures是一个抽象的概念,它表示一个值可能在某一点变得可用。一个Future要么获得
计算完的结果,要么获得计算失败后的异常。Java在java.util.concurrent包中附带了Future接口。
还是上面的例子,用future
的方式改造下。下面的代码只贴出来不一样的地方。
public interface Fetcher {
Future<Data> fetchData();
}
public class MyFetcher implements Fetcher{
ExecutorService executor = Executors.newFixedThreadPool(1);
@Override
public Future<Data> fetchData() {
return executor.submit(task);
}
Callable<Data> task = new Callable<Data>() {
@Override
public Data call() throws Exception {
//模拟获取数据
Thread.sleep(1000);
Data data = new Data(1, 2);
return data;
}
};
}
看一个基于阻塞方式实现的服务端示例,
//基于阻塞模式的EchoServer
public class PlainEchoServer {
public void server(int port) throws IOException {
final ServerSocket serverSocket = new ServerSocket(port);
try {
final Socket clientSocket = serverSocket.accept();
System.out.println("新的连接:" + clientSocket);
new Thread(new Runnable() {
@Override
public void run() {
//读取数据,业务处理
}
}).start();
}catch (Exception e) {
e.printStackTrace();
}
}
}
这段代码,给每个新连接的客户端创建一个处理线程。这种方案的问题在于客户端的连接数受限于系统所能支持的线程数。
下面这个示例是基于java nio异步方式实现的服务端示例。
public class PlainNIOEchoServer {
public void server(int port) throws IOException {
System.out.println("listening for connection on port " + port);
ServerSocketChannel serverChannel = ServerSocketChannel.open();
ServerSocket ss = serverChannel.socket();
InetSocketAddress address = new InetSocketAddress(port);
ss.bind(address);
serverChannel.configureBlocking(false);
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT); //监听接受连接的事件
while (true) {
selector.select();
Set readyKeys = selector.selectedKeys();
Iterator iterator = readyKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = (SelectionKey)iterator.next();
iterator.remove();
try {
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel)key.channel();
SocketChannel client = server.accept();
System.out.println("accept:" + client);
client.configureBlocking(false); //配置非阻塞
client.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE, ByteBuffer.allocate(100));
}
if (key.isReadable()) {
SocketChannel client = (SocketChannel)key.channel();
ByteBuffer out = (ByteBuffer)key.attachment();
client.read(out);
}
if (key.isWritable()) {
SocketChannel client = (SocketChannel)key.channel();
ByteBuffer out = (ByteBuffer)key.attachment();
out.flip();
client.write(out);
out.compact();
}
}catch (Exception e) {
key.cancel();
key.channel().close();
}
}
}
}
}
这段代码有几个点需要解释下。
ByteBuffer
是java nio里的一个核心概念,可以认为它是一个字节容器。我们可以用它来分配堆内的空间,可以分配对外的空间。后者效率更高,但是需要特别注意OOM的问题。
另外,需要注意到ByteBuffer
需要利用flip来切换读写模式,netty是不需要我们关心这个的。
另外一个需要关注的概念是selector
,java nio里使用selector
来判断一个或者多个channel是否可以读或者写。一个selector
可以监听多个connection,这种前面的一个线程处理一个连接的方式要好的多。
使用selector的步骤如下:
- 创建一个或者多个selector,并注册channel到上面
- 注册的时候指定感兴趣的事件监听,包括OP_CONNECT,OP_ACCEPT,OP_READ,OP_WRITE
- 轮询
selector.select()
检查是否有事件发生 - 如果上一步阻塞,说明没有事件发生,否则就拿到selectkey判断事件的类型进一步处理。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。