普通阻塞式 socket (BIO)
- client
通过 java.net.Socket 来获取流
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
public class SocketClient {
public static void main(String[] args) throws IOException {
Socket sock = new Socket("localhost", 6666); // 连接指定服务器和端口
try (InputStream input = sock.getInputStream();
OutputStream output = sock.getOutputStream()) {
handle(input, output);
}
sock.close();
System.out.println("disconnected.");
}
private static void handle(InputStream input, OutputStream output) throws IOException {
BufferedWriter writer =
new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
BufferedReader reader =
new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
Scanner scanner = new Scanner(System.in);
System.out.println("[server] " + reader.readLine());
for (; ; ) {
System.out.print(">>> "); // 打印提示
String s = scanner.nextLine(); // 读取一行输入
writer.write(s);
writer.newLine();
writer.flush();
String resp = reader.readLine();
System.out.println("<<< " + resp);
if (resp.equals("bye")) {
break;
}
}
}
}
- server
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
@SuppressWarnings("all")
public class SocketServer {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(6666); // 监听指定端口
System.out.println("server is running...");
for (; ; ) {
Socket sock = ss.accept();
System.out.println("connected from " + sock.getRemoteSocketAddress());
Thread t = new Handler(sock);
t.start();
}
}
static class Handler extends Thread {
Socket sock;
public Handler(Socket sock) {
this.sock = sock;
}
@Override
public void run() {
try (InputStream input = this.sock.getInputStream()) {
try (OutputStream output = this.sock.getOutputStream()) {
handle(input, output);
}
} catch (Exception e) {
try {
this.sock.close();
} catch (IOException ignored) {
}
}
System.out.println("client disconnected.");
}
private void handle(InputStream input, OutputStream output) throws IOException {
BufferedWriter writer =
new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
BufferedReader reader =
new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
writer.write("hello\n");
writer.flush();
for (; ; ) {
String s = reader.readLine();
if (s.equals("bye")) {
writer.write("bye\n");
writer.flush();
break;
}
writer.write("ok: " + s + "\n");
writer.flush();
}
}
}
}
Buffer
@Slf4j
public class TestFileChannelTransferTo {
public static void main(String[] args) {
long start = System.currentTimeMillis();
try (final FileChannel from = new FileInputStream("from.txt").getChannel();
final FileChannel to = new FileOutputStream("to.txt").getChannel()) {
// 剩余 未传输的 字节数
long size = from.size();
// Question: 上限:最大一次传输 2G 数据,超出不会被传输
// from.transferTo(0, from.size(), to); // before: FileChannel#transferTo
for (long remainder = size; remainder > 0; ) { // after: use for when bigger 2 GB
long position = size - remainder; // 全部大小 - 剩余字节树
System.out.println("position: " + position + " remainder:" + remainder);
// 比 输入输出流 效率高,底层使用操作系统的 零拷贝 进行优化
remainder -= from.transferTo(position, remainder, to); // transferTo 返回实际传输的字节数
}
} catch (IOException e) {
e.printStackTrace();
}
log.info("---time cost: " + (System.currentTimeMillis() - start) + " 毫秒---");
}
@Test
public void traverse() throws IOException {
// 这里不能使用 局部变量 int, 因为 内部类使用局部变量 必须都是 final 的,java1.8后可以不写final
final AtomicInteger dirCount = new AtomicInteger();
final AtomicInteger fileCount = new AtomicInteger();
// SimpleFileVisitor uses the visitor pattern.
Files.walkFileTree(
Paths.get("E:\\aio1"),
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
System.out.println(">>> 进入 目录 " + dir);
dirCount.incrementAndGet();
return super.preVisitDirectory(dir, attrs);
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
// if (file.toString().endsWith(".jar")) // 加判断
System.out.println(file);
// Files.delete(file); // =================== 删除文件
fileCount.incrementAndGet();
return super.visitFile(file, attrs);
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
throws IOException {
// Files.delete(dir); // =================== 删除文件夹
System.out.println("<<< 退出 目录 " + dir);
return super.postVisitDirectory(dir, exc);
}
});
System.out.println("文件夹:" + dirCount);
System.out.println("文件数:" + fileCount);
}
@Test
public void testCopyDirAll() throws IOException {
String source = "E:\\aio1";
String target = "E:\\aio2";
// 同样也是遍历
try (Stream<Path> traverseStream = Files.walk(Paths.get(source))) {
traverseStream.forEach(
path -> {
String targetName = path.toString().replace(source, target);
try {
Path currentPath = Paths.get(targetName);
if (Files.isDirectory(path)) { // 如果是 目录 创建
Files.createDirectories(currentPath);
} else if (Files.isRegularFile(path)) { // 如果是 文件 复制
Files.copy(path, currentPath);
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
}
阻塞vs非阻塞
-
阻塞:
- 单线程下, 阻塞方法相互影响, 不能正常工作
- 多线程下, 线程数多了, cpu 占用高, 上下文切换成本高; 线程池虽然减少了线程数但只能用短连接. 都是治标不治本;
-
非阻塞:
- 在 ServerSocketChannel.accept 没有连接建立时返回 null , 继续运行
- SocketChannel.read 在没有数据可读时, 会返回 0, 会一直循环, 做的只是等待而已.
- 但线程不断运行的背后, 白白浪费了 cpu 资源.
- 数据复制的过程中, 线程实际上还是阻塞的
-
多路复用
- 单线程配合 Selector 完成对多个 Channel 可读写事件的监控.
- 在非阻塞的基础上引入了事件的概念. 表现在未分配时阻塞, 有事件就放行
- 多路复用仅针对网络IO、普通文件IO 没办法利用多路复用
- 单线程配合 Selector 完成对多个 Channel 可读写事件的监控.
select 何时不阻塞
- 事件发生时
- 客服端发起连接: accept
- client send data, normal close & exception close 都会触发: > read; when data > buffer缓冲区, 会触发多次读取事件
- channel 可写, 触发 write 事件
- in linux, nio bug 发生时也不阻塞
- 调用 selector.wakeup()
- 调用 selector.close()
- selector 所在线程 interrupt
-
阻塞


-
非阻塞

-
多路复用


-
信号驱动
以上都是同步: 线程自己去获取结果
- 异步非阻塞(异步没有阻塞的) 通过回调来通知后续操作, 推送结果 (至少两个线程)
Netty


1、什么是Netty
Netty is an asynchronous event-driven network application framework
for rapid development of maintainable high performance protocol servers & clients
Netty 是一个异步的、基于事件驱动的网络应用框架,用于快速开发可维护、高性能的网络服务器和客户端
注意:netty的异步还是基于多路复用的,并没有实现真正意义上的异步IO
2、Netty的优势
如果使用传统NIO,其工作量大,bug 多
- 需要自己构建协议
- 解决 TCP 传输问题,如粘包、半包
- 因为bug的存在,epoll 空轮询导致 CPU 100%
Netty 对 API 进行增强,使之更易用,如
- FastThreadLocal => ThreadLocal
- ByteBuf => ByteBuffer
二、案例
1、服务器端代码
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
/**
* Server
*
* @author howeres
* @version 2022/6/3
*/
@Slf4j
public class Server {
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup(2);
try {
ServerBootstrap server = new ServerBootstrap();
server.channel(NioServerSocketChannel.class);
server.group(boss, worker);
server.childHandler(new ChannelInitial());
ChannelFuture future = server.bind(8080).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
static class ChannelInitial extends ChannelInitializer<NioSocketChannel> {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler(LogLevel.DEBUG));
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(
new SimpleChannelInboundHandler<HttpRequest>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg)
throws Exception {
log.info("uri is {}", msg.uri());
DefaultFullHttpResponse response =
new DefaultFullHttpResponse(
msg.protocolVersion(), HttpResponseStatus.OK);
byte[] bytes = "<h1>Hello world/~</h1>".getBytes();
response.headers().setInt(CONTENT_LENGTH, bytes.length);
response.content().writeBytes(bytes);
ctx.writeAndFlush(response);
}
});
}
}
}
2、客户端代码
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
/**
* Client
*
* @author howeres
* @version 2022/6/3
*/
@Slf4j
public class Client {
public static void main(String[] args) {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap client = new Bootstrap();
client.channel(NioSocketChannel.class);
client.group(worker);
client.handler(new ChannelInitial());
ChannelFuture future = client.connect("localhost", 8080).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
worker.shutdownGracefully();
}
}
static class ChannelInitial extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
pipeline.addLast(
new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
byte[] LINE = {'\n', '\r'};
ByteBuf buffer = ctx.alloc().buffer(); // allocate 分配 调拨
buffer.writeBytes("*3".getBytes()).writeBytes(LINE);
buffer.writeBytes("$3".getBytes()).writeBytes(LINE);
buffer.writeBytes("set".getBytes()).writeBytes(LINE);
buffer.writeBytes("$4".getBytes()).writeBytes(LINE);
buffer.writeBytes("name".getBytes()).writeBytes(LINE);
buffer.writeBytes("$7".getBytes()).writeBytes(LINE);
buffer.writeBytes("howeres".getBytes()).writeBytes(LINE);
ctx.writeAndFlush(buffer);
super.channelActive(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
ByteBuf buf = (ByteBuf) msg;
log.info("msg: {}", buf.toString(StandardCharsets.UTF_8));
super.channelRead(ctx, msg);
}
});
}
}
}
源码
启动流程
Selector selector = Selector.open();
-
AbstractBootstrap#doBind步骤一:initAndRegister() #1并切换线程返回了 regFuture 处理- init main线程
- 创建 NioServerSocketChannel, 在构造器里初始化 mian线程
ServerSocketChannel ssc = ServerSocketChannel.open(); - 添加 NioServerSocketChannel 初始化 handler main线程
- 初始化 handler 等待调用 (nio-thread线程 调用)
向 nio ssc 加入了 acceptor handler 此处仅添加未调用 (在 accept 事件发生后建立连接)
- 初始化 handler 等待调用 (nio-thread线程 调用)
- 创建 NioServerSocketChannel, 在构造器里初始化 mian线程
- register (切换线程)
initAndRegister() #2//config().group().register(channel);- 启动 nio boss 线程 main线程
- 原生 java ssc 注册至 selector (未关注事件)
AbstractNioChannel#doRegisternio-thread线程
SelectionKey selectionKey = ssc.register(selector, 0, nettySsc); - 在
AbstractUnsafe#register0执行 NioServerSocketChannel 初始化 handler //pipeline.invokeHandlerAddedIfNeeded();nio-thread线程 -
safeSetSuccess(promise);塞回结果 promise == regFuture
- 得到结果后
AbstractBootstrap#doBind设置监听函数进行回调 (新开了一个Nio线程执行NioServerSocketChannel 的 channel#bind )
- init main线程
-
regFuture 的回调 doBind0 nio-thread线程
-
NioServerSocketChannel#doBind判断jdk版本是否大于7, 再让 原生ServerSocketChannel绑定端口 nio-thread线程
ssc.bind(new InetSocketAddress(8080, backlog));
- 触发 NioServerSocketChannel active 事件
pipeline.fireChannelActive();nio-thread线程
断点 HeadContext#channelActive 关注下一步, 主要目的是注册 ACCEPT 事件
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
经过初始化的 channel 处于激活状态, 包含的pipeline 为:
head -> acceptor -> tail
channel 不具备业务功能, 真正干活的都是流水线上的 handler;
accept 流程
- selector.select() 阻塞直到事件发生
- 遍历处理 selectedKeys
- 拿到一个 key, 判断事件类型是否为 accept
- 创建 SocketChannel, 设置非阻塞 // unsafe.read() ->
NioMessageUnsafe#read
NioServerSocketChannel#doReadMessages(List<Object> readBuf)将 accept 得到的SocketChannel初始化成new NioSocketChannel(this, ch)//并设置为非阻塞, 最后当成一个消息放进readBuf
然后使用父NioServerSocketChannel的pipeline.fireChannelRead(readBuf.get(i));挨个去调用;
ServerBootstrapAcceptor#channelRead方法, 为刚刚新建的NioSocketChannel去设置 option参数, 再执行到 group.register() // 让 Group 里面的 selector 对新创建的 channel 进行监听
判断当前线程是不是inEventLoop(是EventLoop线程就直接注册, 不是就新开一个) 保证是在新channel 的 loop里面操作 - 将 SocketChannel 注册至 selector
sc.register(eventLoop的选择器, 0 , NioSocketChannel)
调用 NioSocketChannel 上的初始化器 - 关注 selectionKey 的 read 事件
read 流程
- selector.select() 阻塞直到事件发生
- 遍历处理 selectedKeys
- 拿到一个 key, 判断事件类型是否为 read
NioEventLoop#697AbstractNioByteChannel#139 - 读取操作
AbstractNioByteChannel#148 #163
NioEventLoop
NioEventLoop 重要组成部分: selector、线程 Thread & Executor、**任务队列 ** Queue<Runnable> (在祖父类 SingleThreadEventExecutor 里面)、定时任务 PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue; (在曾祖父类 AbstractScheduledEventExecutor.class 里面)
NioEventLoop 即会处理 io 事件, 也会处理普通任务和定时任务
-
selector 何时创建 NioEventLoop 构造器里面 openSelector(); // 放在 unwrappedSelector
- 1.1 selector 为何有两个 selector 成员
// 替换 selectedKeys 的 Set 集合实现为数组(SelectedSelectionKeySet 继承抽象的 AbstractSet 再组合一个 SelectionKey[] keys), 为了在遍历 selectedKeys 时提高性能
- 1.1 selector 为何有两个 selector 成员
-
nio线程在何时启动
首次调用 execute 方法时
通过 状态位STATE_UPDATER 控制线程只启动一次 -
提交普通任务会结束selector阻塞 // 会wakeup(bool inEventLoop) 判断提交线程和 NIO线程是不是同一个. 不是才会下一步
- wakeup 方法中的代码如何理解
!inEventLoop// 只有其他线程提交才会wakeup - wakeUp变量的作用是什么 // 原子变量, 只能通过CAS的方式设置值
因为selector.wakeup()是一个重量级操作, 避免多个线程频繁调用(只要其中一个调用成功就行)
- wakeup 方法中的代码如何理解
-
每次循环, 什么时候会进入 SelectStrategy.SELECT 分支
NioEventLoop#run
当没有任务时, 才会进入 SelectStrategy.SELECT
当有任务时, selectSupplier.get() 会 selectNow() (没拿到任务会返回0), 拿到任务连同任务一起交给下面的逻辑运行- 何时会select阻塞, 阻塞多久?
NioEventLoop#select
timeoutMillis-超时时间, selectDeadLineNanos-截止时间=当前时间+1s + 0.5ms 再转化为毫秒 (除开定时任务)
- 何时会select阻塞, 阻塞多久?
-
nio空轮询bug在哪里体现, 如何解决 (bug 出在 jdk 在 linux 下的 selector)
重新创建了一个selectorselectRebuildSelector要把原来的selectionKey都复制回去 -
ioRatio控制什么, 设置为 100 有何作用?
ioRatio控制处理io事件所占用的时间比例, 默认 50%
ioRatio=80 ioTime=8s 运行时间=ioTime* (100-80)/80 = 2s
如果 ioRatio 设为100, 会特殊处理, 两者全部执行,
try { processSelectedKeys } finally { runAllTasks } -
selecedKeys 优化是怎么回事?
processSelectedKeys ->
processSelectedKeysOptimized else processSelectedKeysPlain;
XXOptimized 使用下标的方式获取 selectedKeys.keys[i] 并置为空
通过 attachment 拿到 channel -
在哪里区分不同事件类型
processSelectedKeysOptimized 调用的 processSelectedKey 方法
EventLoop 是一个程序结构,用于等待和发送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)
结构中有两个线程:
- 一个负责程序本身的运行,称为"主线程";
- 另一个负责主线程与其他进程(主要是各种I/O操作)的通信,被称为"Event Loop线程"(可以译为"消息线程")
任务队列
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)
- 主线程之外,存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 不断重复第三步
“任务队列” EventQueue是一个事件的队列, FIFO的数据结构, 只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。
回调函数"(callback),就是那些会被主线程挂起来的代码
只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)
- javascript 中的 EventLoop

执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行
- 以下是借用 node.js 的 EventLoop概念

优化
- MASK
@Skip 注解的使用, 利用注解的继承, 得知重写的注解不会附带父类的注解, 也就不会被跳过. 相反没有被重写的 handler 就会被快速跳过, 不会被 ChannelPipeline 对象调用.
- 数组
在 NioEventLoop 构造器中对 selector.selectedKeys() 进行了优化, 为了遍历把 Set 优化成 数组. 并在 processSelectedKeysOptimized 方法中, 使用下标的方式获取 selectedKeys.keys[i] 再置为空.
Reference
Essential Technologies for Java Developers: I/O and Netty domestic
Essential Technologies for Java Developers: I/O and Netty overseas
Imooc wiki

1万+

被折叠的 条评论
为什么被折叠?



