Netty的介绍
- Netty是一个异步的、基于事件驱动的网络应用框架,用于快速开发高性能、高可用的网络IO程序
- Netty主要针对在TCP协议下,面向客户端的高并发应用,或者P2P场景下,大量数据持续传输的应用
- 基于NIO
I/O模型
- BIO 传统阻塞IO
- 每个请求都需要创建独立的线程,与对应的客户端进行数据读写处理。
- 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
- 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在Read操作上,造成线程资源浪费
/**
* description:BIO阻塞点:
* 1 serverSocket.accept() 没有连接时,会一直阻塞
* 2 inputStream.read(bytes) 没有数据时会一直阻塞,等待数据
*
*
* @author : huaneng
* @date : 2020/9/8 20:29
*/
public class BioServer {
public static void main(String[] args) throws IOException {
/*
* 思路:
* 1 创建线程池
* 2 来一个请求,则分配一个线程与之通信
* */
ExecutorService pool = Executors.newCachedThreadPool();
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器启动。。。");
while (true) {
// 监听,等待客户端连接 如果没有会一值阻塞在这里,不会执行后面的语句
final Socket socket = serverSocket.accept();
System.out.println("有客户端连接");
pool.execute(()->{
// 与客户的通信
handle(socket);
});
}
}
/**
* description: 与客户端通信的方法
* @author : huaneng 2020/9/8 20:38
*/
public static void handle(Socket socket) {
byte[] bytes = new byte[1024];
// 通过socket获取输入流
try {
InputStream inputStream = socket.getInputStream();
// 循环读取客户端数据
while (true) {
int read = inputStream.read(bytes);
if (read != -1) {
// 将读取的内容输出
System.out.println(new String(bytes,0,read));
}else {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
System.out.println("关闭连接");
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
- NIO 同步非阻塞IO
三大核心模块channel、 buffer 、selector
NIO是面向缓冲区编程。数据读取到它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络
】
- AIO 异步非阻塞IO
NIO和BIO的比较
- BIO以流的方式处理数据,而NIO以块的方式处理数据。块I/O的效率比流I/O高很多
- BIO是阻塞的,NIO则是非阻塞的
- BIO基于字节流和字符流进行操作,而NIO基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
NIO Selector Channel Buffer 的关系
- 每个channel都会对应一个buffer
- Selector对应一个线程,一个线程对应多个Channel
- 程序切换到哪个Channel是由事件决定的
- selector 会根据不同的事件,在各个通道上切换
- buffer就是一个内存块,底层有一个数组
- 数据的读取/写入是通过buffer,BIO中要么是输入流,要么是输出流,但是NIO可以写也可以读,使用flip切换
- channel也是双向的,可以返回底层操作系统的情况
NIO
buffer
- capacity :可以容纳的最大数量;在缓存区创建时设置,不能改变
- limit :表示缓冲区当前的终点,不能对超过缓冲区limit的位置进行读写
- position :当前位置,一个要被读或者写的元素的索引,每次读写都会改变
- mark : 标记
- MappedByteBuffer : 可以对数据直接在堆外内存进行操作,性能较高
- Scattering: 将数据写入buffer时,可以采用buffer[],依次写入
- Gatherind:将buffer读取数据时,采用buffer[],依次读取
public class ScatteringAndGatheringTest {
public static void main(String[] args) throws IOException {
// 使用ServerSocketChannel 和ServerChannel 来测试
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
// 绑定端口到socket 并启动
serverSocketChannel.socket().bind(inetSocketAddress);
// 创建 Buffer 数组
ByteBuffer[] buffers = new ByteBuffer[2];
buffers[0] = ByteBuffer.allocate(5);
buffers[1] = ByteBuffer.allocate(5);
int length = 10 ; // 假定从客户端读取10个字节
SocketChannel socketChannel = serverSocketChannel.accept();
// 循环读取
while (true) {
int byteRead = 0;
while (byteRead < length) {
long read = socketChannel.read(buffers);
// 累计字节数
byteRead += read;
System.out.println(" byteRead = " + byteRead);
// 使用流打印出每个buffers 的状态
Arrays.asList(buffers).stream().map(byteBuffer -> "position = " + byteBuffer.position() +
" limit = " +byteBuffer.limit()).forEach(System.out::println);
}
// buffer反转flip
Arrays.asList(buffers).forEach(Buffer::flip);
// 输出数据到客户端
long writeByte = 0;
while (writeByte < length) {
long write = socketChannel.write(buffers);
writeByte += write;
}
// 将所有的Buffer clear
Arrays.asList(buffers).forEach(Buffer::clear);
}
}
}
channel
- 双向的,可以同时读写
- 可以实现异步读写数据
- 常用的channel有FeilChannel、SocketServerChannel、SockerChannel
public class NioFileChannel01 {
public static void main(String[] args) throws Exception {
copyFile2();
}
private static void copyFile2() throws IOException {
// 读取文件test01
FileInputStream fileInputStream = new FileInputStream("D:\\test01.txt");
FileChannel readChannel = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("D:\\test02.txt");
FileChannel writeChannel = fileOutputStream.getChannel();
// copy
writeChannel.transferFrom(readChannel, 0, readChannel.size());
fileInputStream.close();
fileOutputStream.close();
}
private static void copyFile() throws IOException {
// 读取文件test01
FileInputStream fileInputStream = new FileInputStream("D:\\test01.txt");
FileChannel readChannel = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("D:\\test02.txt");
FileChannel writeChannel = fileOutputStream.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(5);
while (true) {
// 这里一定要复位,否则读到最后position = limit read就会返回0,永远不能为-1
buffer.clear();
int read = readChannel.read(buffer);
System.out.println(read);
if (read == -1) {
break;
}
buffer.flip();
// 写入文件test02
writeChannel.write(buffer);
}
fileInputStream.close();
fileOutputStream.close();
}
private static void readFromFile() throws IOException {
FileInputStream fileInputStream = new FileInputStream("D:\\test01.txt");
FileChannel fileChannel = fileInputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 从fileChannel读到byteBuffer
fileChannel.read(byteBuffer);
System.out.println(new String(byteBuffer.array()));
fileInputStream.close();
}
private static void writeToFile() throws IOException {
String s = "hello world 天天向上";
// 创建输出流 获取channel
FileOutputStream fileOutputStream = new FileOutputStream("D:\\test01.txt");
FileChannel fileChannel = fileOutputStream.getChannel();
// 创建一个Buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 将字符串放入buffer
byteBuffer.put(s.getBytes());
// 读写切换
byteBuffer.flip();
// 将byteBuffer写入channel
fileChannel.write(byteBuffer);
fileOutputStream.close();
}
}
Selector
Selector能够检查多个注册到选择器的通道是否有事件发生。如果有事件发生,便获取事件然后对每个事件进行相应的处理,这样就可以实现一个线程管理多个通道。
- Netty 的IO线程 NioEventLoop聚合了 Selector(选择器也叫多路复用器),可以同时并发处理成百上千个客户端连接。
- 当线程从某客户端 Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
- 线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道
- 由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致的线程挂起
- 一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
实例1:
服务端
public class NioServer {
public static void main(String[] args) throws IOException {
// 创建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 获取Selector
Selector selector = Selector.open();
// 绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
// 设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 首先将服务端注册到Selector,并且关心的事件为accpt
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 轮询等待客户端连接
while (true) {
// 每隔1s检测一次,看看是否由事件发生
int channelCount = selector.select(1000);
if (channelCount == 0) {
continue;
}
// 有客户端
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 通过key获取channel
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 根据key中channel的状态处理对应的业务
// 发生了连接事件
doAccept(serverSocketChannel, selector, key);
// 发生了读事件
doRead(key);
// 结束后删除 key,防止多线程下的重复操作
iterator.remove();
}
}
}
private static void doRead(SelectionKey key) throws IOException {
if(key.isReadable()){
// 根据key 获取到 SocketChannel
SocketChannel socketChannel =(SocketChannel) key.channel();
// 在根据key 得到socketChannel 绑定的 buffer
ByteBuffer buffer =(ByteBuffer) key.attachment();
socketChannel.read(buffer);
System.out.println("来自客户端的消息:" + new String(buffer.array()));
}
}
private static void doAccept(ServerSocketChannel serverSocketChannel, Selector selector, SelectionKey key) throws IOException {
if (key.isAcceptable()) {
System.out.println("有新的客户端连接,应该为客户端创建channel");
SocketChannel socketChannel = serverSocketChannel.accept();
// 将channel注册到selector ,并绑定一个buffer
socketChannel.register(selector, SelectionKey.OP_WRITE, ByteBuffer.allocate(1024));
}
}
}
实例2:简单的群聊
服务端
public class GroupChatServer {
private Selector selector;
private ServerSocketChannel listenChannel;
private static final int port = 6667;
public GroupChatServer() {
try {
selector = Selector.open();
listenChannel = ServerSocketChannel.open();
listenChannel.socket().bind(new InetSocketAddress(port));
listenChannel.configureBlocking(false);
listenChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
// 监听
public void listen() {
try {
while (true) {
int count = selector.select(2000);
if (count == 0) {
continue;
}
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
SocketChannel channel = listenChannel.accept();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println(channel.getRemoteAddress() + " 上线");
}
if (key.isReadable()) {
// 读取客户端消息
read(key);
}
keyIterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/***
* description: 读取
* @author : huaneng 2020/9/15 20:02
*/
private void read(SelectionKey key) {
SocketChannel channel = (SocketChannel) key.channel();
try {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
if (read > 0) {
String msg = new String(buffer.array());
System.out.println("收到客户端数据:" + msg);
// 发送给其他客户端
sendMsg(channel, msg);
}
} catch (IOException e) {
try {
System.out.println(channel.getRemoteAddress() + " 下线");
// 取消注册
key.cancel();
// 关闭通道
channel.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
/**
* description: 发送给其他客户端
*
* @author : huaneng 2020/9/15 19:56
*/
private void sendMsg(SocketChannel channel, String msg) throws IOException {
// 遍历所有注册的 selector
Set<SelectionKey> keys = selector.keys();
for (SelectionKey otherKey : keys) {
if(otherKey.channel() instanceof SocketChannel){
SocketChannel targetChannel = (SocketChannel) otherKey.channel();
if (targetChannel.equals(channel)) {
continue;
}
// 将buffer中的数据写的channel
targetChannel.write(ByteBuffer.wrap(msg.getBytes()));
}
}
}
public static void main(String[] args) {
GroupChatServer server = new GroupChatServer();
server.listen();
}
}
客户端
public class GroupChatClient {
private final String HOST = "127.0.0.1";
private final int PORT = 6667;
private Selector selector;
private SocketChannel socketChannel;
private String name;
public GroupChatClient() throws IOException {
selector = Selector.open();
socketChannel= SocketChannel.open(new InetSocketAddress(HOST,PORT));
socketChannel.configureBlocking(false);
// 注册
socketChannel.register(selector, SelectionKey.OP_READ);
// 得到 name
name = socketChannel.getLocalAddress().toString();
System.out.println( name +" 初始化完成");
}
public void sendInfo(String info) {
info = name + ":" + info;
try {
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
private void readInfo() throws IOException {
int count = selector.select();
if (count == 0) {
// System.out.println("没有消息");
return;
}
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isReadable()) {
SocketChannel channel =(SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
// 把读到的数据转为字符串
System.out.println(new String(buffer.array()));
}
keyIterator.remove();
}
}
public static void main(String[] args) throws IOException {
final GroupChatClient client = new GroupChatClient();
// 读取数据
new Thread(()->{
while (true) {
try {
client.readInfo();
Thread.sleep(2000);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String s = scanner.nextLine();
client.sendInfo(s);
}
}
}