Netty简单认识:
1) Netty 是由JBOSS 提供的一个Java 开源框架。
2) Netty 是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络I0 程序。
3) Netty 主要针对在TCP协议下的使用
4)Netty本质是- 个NIO框架,适用于服务器通讯相关的多种应用场景
Netty应用:
https://netty.io/wiki/related-projects.html这里面是和netty有关的框架
Netty应用于网络间的通信,如阿里的dubbo框架,应用于服务之间的调用;谷歌的grpc框架;
I/O模型:
netty是基于nio开发,所以我们需要了解java中的I/O模型:
- Bio:同步阻塞io,服务器实现模式为一 一个连接-一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理
如果这个连接不做任何事情会造成不必要的线程开销,适用于连接数小的和固定的,编程简单。
- Nio:同步非阻塞,JavaNIO :同步非阻塞, 服务器实现模式为- -个线程处理多个请求(连接),即客户端发送的连接请求
都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理,适用于连接数多的连接时间短的,编程复杂。(1.4以后版本)
- Aio:异步非阻塞,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,
一般适用于连接数较多且连接时间较长的应用,适用于连接数多的和连接时间长的,编程复杂,(1.7以后版本)
bio:
public class ServerClient { public static void main(String[] args) throws IOException { //创建自定义线程池 ExecutorService executorService = new ThreadPoolExecutor( 3, 7, 2000, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(3), new ThreadPoolExecutor.CallerRunsPolicy()); //启动服务端 ServerSocket serverSocket = new ServerSocket(8086); System.out.println("服务端启动,等待连接。。。。。。。"); while(true){ //等待连接 final Socket socket = serverSocket.accept(); System.out.println("连接上一个客户。。。。。。"+socket.getPort()); executorService.execute(()->{ handel(socket); }); } } //处理读取到的数据 public static void handel(Socket socket){ try{ byte[] bytes = new byte[1024]; InputStream inputStream = socket.getInputStream(); while (true){ //等待读取数据 int length = inputStream.read(bytes); if(length != -1){ System.out.println("读取到数据"+new String(bytes,0,length)); }else{ break; } } }catch (Exception e){ e.printStackTrace(); }finally { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
通过这个demo我们可以看到,每个客户端需要独立的线程,使用bio会在等待连接和读取数据两处进行阻塞,造成线程资源的浪费。
nio:
1》 NIO 有三大核心部分: Channel(通道), Buffer(缓冲区), Selector(选择器)
2》NIO是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后
移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络
3》Java NIO的非阻塞模式,使-一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,
如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可
以继续做其他的事情。非阻塞写 也是如此,-一个线程请求写入- -些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
通俗理解: NIO是可以做到用-一个线程来处理多个操作的。假设有10000 个请求过来,根据实际情况,可以分配50或者100个线程来处理。
不像之前的阻塞I0那样,非得分配10000个。
4》HTTP2.0采用多路复用技术
5》nio中buffer的demo:
public class NioBaise { public static void main(String[] args) { //创建容量为5的IntBuffer IntBuffer intBuffer = IntBuffer.allocate(5); for (int i = 0; i <5 ; i++) { intBuffer.put(i); } //切换,有写变读 intBuffer.flip(); //判断是否还有数据 while (intBuffer.hasRemaining()){ //获取数据,get()获取数据后,就会移动下标 System.out.println(intBuffer.get()); } } }
NIO 和BIO的比较:
1》BIO 是阻塞的,NIO则是非阻塞的
2》 BIO 以流的方式处理数据,而NIO以块的方式处理数据,块I/O 的效率比流I0高很多
3》 BIO 基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区 )进行操作,数据总是从通道
读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择 器)用于监听多个通道的事件(比如:连接请求,
数据到达等),因此使用单个线程就可以监听多个客户端通道
nio中的buffer:
有四个重要属性
Capacity:容量,即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变
Limit:表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的
Position:位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变改值,每次读写缓冲区数据时都会改变改值,
Mark:标记
Buffer中常用方法:
在Buffer中ByteBuffer是最常用的(二进制数据),主要方法如下:
nio中的channel:
1) NIO的通道类似于流,但有些区别如下:
●通道可以同时进行读写, 而流只能读或者只能写
●通道可以实现异步读写 数据
●通道可以从缓冲读数据, 也可以写数据到缓冲:
2) BIO 中的stream 是单向的,例如FilelnputStream 对象只能进行读取数据的操作,而NIO中的通道(Channel)是双向的,可以读操作,也可以写操作。
3) Channel 在NIO中是一个接口 public interface Channel extends Closeable{}
4)常用的Channel 类有: FileChannel、DatagramChannel、ServerSocketChannel 和SocketChannel。
5) FileChannel 用于文件的数据读写,DatagramChannel 用于UDP 的数据读写,ServerSocketChannel 和SocketChannel用于TCP的数据读写。
FileChannel常用方法:
FileChannel的demo1:向文件中写入字符串
public class FileChannelDemo { public static void main(String[] args) throws IOException { String str = "hello"; //创建文件流 FileOutputStream fileOutputStream = new FileOutputStream("e:\\hello.txt"); //将流转为通道 FileChannel fileChannel = fileOutputStream.getChannel(); //创建换缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); byteBuffer.put(str.getBytes()); //将byteBuffer进行反转 byteBuffer.flip(); //将缓冲区数据写入到fileChannel通道里 fileChannel.write(byteBuffer); //关闭管道 fileChannel.close(); } }
FileChannel的demo2:向文件中读取字符串
public class FileChannelDemo2 { public static void main(String[] args) throws IOException { //创建文件输入流 FileInputStream fileInputStream = new FileInputStream("e:\\hello.txt"); //将流转为通道 FileChannel fileChannel = fileInputStream.getChannel(); //创建换缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(fileInputStream.available()); //将管道里面的数据写入到byteBuffer中 fileChannel.read(byteBuffer); //将byteBuffer进行反转 byteBuffer.flip(); System.out.println(new String(byteBuffer.array())); //关闭管道 fileChannel.close(); } }
FileChannel的demo3:文件复制:
public class FileChannelDemo3 { public static void main(String[] args) throws IOException { //创建文件输入流 FileInputStream fileInputStream = new FileInputStream("e:\\hello.txt"); FileChannel fileChannel = fileInputStream.getChannel(); //创建文件输出流 FileOutputStream fileOutputStream = new FileOutputStream("e:\\hello2.txt"); FileChannel channel = fileOutputStream.getChannel(); //创建换缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); while (true){ //复位 byteBuffer.clear(); //将管道里面的数据写入到byteBuffer中 int read = fileChannel.read(byteBuffer); if(read == -1){ break; } //将byteBuffer进行反转 byteBuffer.flip(); //将byteBuffer里面的数据写入到通道中 channel.write(byteBuffer); } System.out.println("写入完成"); //关闭管道 fileChannel.close(); fileOutputStream.close(); fileInputStream.close(); } }
FileChannel的demo4:文件复制,直接使用transferFrom()方法;
public class FileChannelDemo4 { public static void main(String[] args) throws IOException { //创建文件输入流 FileInputStream fileInputStream = new FileInputStream("e:\\hello.txt"); FileChannel sourceChannel = fileInputStream.getChannel(); //创建文件输出流 FileOutputStream fileOutputStream = new FileOutputStream("e:\\hello2.txt"); FileChannel targetChannel = fileOutputStream.getChannel(); //复制 targetChannel.transferFrom(sourceChannel,0,sourceChannel.size()); sourceChannel.close(); targetChannel.close(); fileInputStream.close(); fileOutputStream.close(); } }
Buffer和channel的注意点:
1) ByteBuffer 支持类型化的put和get, put放入的是什么数据类型,get 就应该使用相应的数据类型来取出,否
则可能有BufferUnderflowException 异常。
2)可以将-一个普通Buffer转成只读Buffer,使用asReadOnlyBuffer()返回ByteBuffer
public abstract ByteBuffer asReadOnlyBuffer()
3) NIO 还提供了MappedByteBuffer, 可以让文件直接在内存 (堆外的内存)中进行修改,而 如何同步到文件由nio完成。
demo:
public class MapedByteBufferDemo { public static void main(String[] args) throws Exception{ RandomAccessFile randomAccessFile = new RandomAccessFile("e:\\hello.txt","rw"); FileChannel channel = randomAccessFile.getChannel(); /** * 参数一:读写模式 * 参数二:开始读取的位值 * 参数三:是映射到内存的大小,而不是索引的大小 */ MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5); map.put(0,(byte)'H'); map.put(2,(byte)'y'); //如果超过大小会发生异常:java.lang.IndexOutOfBoundsException //map.put(5,(byte)'0'); randomAccessFile.close(); System.out.println("修改成功"); } }
nio中的Selector:
示意图及特点:
常用方法:
public static Selector open() throws IOException:得到一个选择器对象public int select(long timeout);//监控所有注册的通道,当其中有I0操作可以进行时,将对应的SelectionKey加入到内部集合中并返回,参数用来设置超时时间public Set<SelectionKey> selectedKeys()//从内部集合中得到所有的SelectionKey方法说明:
过程:
1)当客户端连接时, 会通过ServerSocketChannel得到SocketChannel
2) Selector 进行监听select 方法,返回有事件发生的通道的个数.
3)将socketChannel注册到Selector上, register(Selector sel, int ops),一个selector上可以注册多个SocketChannel
4)注册后返回一个SelectionKey, 会和该Selector关联(集合)
5) 进-步得到各个SelectionKey (有事件发生)
6)在通过SelectionKey 反向 获取SocketChannel ,方法channel()
7)可以通过得到的 channel, 完成业务 处理
demo:NioServer(服务端)
public class NioServer { public static void main(String[] args) throws IOException { //创建ServerSocketChannel ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //绑定网络端口 serverSocketChannel.socket().bind(new InetSocketAddress(666)); //得到selector对象 Selector selector = Selector.open(); //设置为非阻塞的 serverSocketChannel.configureBlocking(false); //将serverSocketChannel注册到selector中,selector只关心OP_ACCEPT事件 SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //循环等待客户连接 while (true){ //没有事件就返回 if(selector.select(1000)==0){ System.out.println("服务端等待连接。。。。。。"); continue; } //获取到相关SelectionKey集合,表示获取到关注的事件 Set<SelectionKey> selectionKeys = selector.selectedKeys(); //使用迭代器遍历 Iterator<SelectionKey> keyIterator = selectionKeys.iterator(); if(keyIterator.hasNext()){ //获取到SelectionKey SelectionKey key = keyIterator.next(); //根据key对应的事件作相应的处理 if(key.isAcceptable()){//表示有新客户端连接 //每次连接一个客户端都会产生新的SocketChannel SocketChannel socketChannel = serverSocketChannel.accept(); System.out.println("每次连接一个客户端都会产生新的SocketChannel的hashcode"+socketChannel.hashCode()); //设置客户端为非阻塞的 socketChannel.configureBlocking(false); //将socketChannel注册到selector,关注事件为OP_READ,同时给socketChannel关联一个buffer socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024)); System.out.println("连接上一个客户端...."); } if(key.isReadable()){//发生OP_READ事件 //通过key反向获取到channel SocketChannel socketChannel = (SocketChannel)key.channel(); //获取到该channer关联的buffer ByteBuffer byteBuffer =(ByteBuffer) key.attachment(); socketChannel.read(byteBuffer); System.out.println("读取到数据。。。"+new String(byteBuffer.array())); } //移除seletorKey,防止重复操作。 keyIterator.remove(); } } } }
客户端:NioClient
public class NioClient { public static void main(String[] args) throws IOException { //连接服务端的ip和端口 InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8888); //得到一个网络通道 SocketChannel socketChannel = SocketChannel.open(); //设置为非阻塞的 socketChannel.configureBlocking(false); //连接服务器 if (!socketChannel.connect(inetSocketAddress)) { while (!socketChannel.finishConnect()) { System.out.println("因为连接需要时间,客户端不会阻塞,可以做其他工作"); } } String str = "hello"; //创建buffer ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes()); //将buffer数据写入到channel中 socketChannel.write(byteBuffer); System.in.read(); } }
SelectKey介绍:
1) SelectionKey,表示Selector 和网络通道的注册关系,共四种:
int OP_ ACCEPT:有新的网络连接可以accept,值为16
int OP_ CONNECT:代表连接已经建立,值为8
intOP_ READ:代表读操作,值为1
intOP_ WRITE:代表写操作,值为4
源码是采用位操作符:
public static final int OP_READ= 1 << 0; public static final int OP_WRITE= 1 << 2; public static final int OP_CONNECT= 1 << 3; public static final int OP_ACCEPT= 1 <<4;
相关方法:
public abstract Selector selecto/(;/得到与之关联的Selector对象) public abstract SelectableChannel chanel)://得到与之关联的通道 public final Object ttachment():/得到与之关联的共享数据 public abstract SelectionKey interestOps(int ops//设置或改变监听事件 public final boolean isAcceptable(;//是否可以accept public final boolean isReadablel(;//是否可以读 public final boolean isWritable(;//是否可以写
ServerSocketChannel:在服务端监听客户端的连接
public static ServerSocketChannel open(),得到-一个ServerSocketChannel通道 public final ServerSocketChannel bind(SocketAddress local),设置服务器端端口号 public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,取值false表示采用非阻塞模式 public SocketChannel accept),接受一个连接, 返回代表这个连接的通道对象 public final SelectionKey register(Selectorsel, intops),注册一个选择器并设置监听事件
SocketChannel:网络io操作,负责数据的读写操作。Nio把数据从缓存中写入通道或把通道里的数据读取到缓存中。
public static SocketChannel open/://得到一个SocketChannel通道 public final SelectableChannel configureBlocking(boolean blok;//设置阻塞或非阻塞模式,取值false表示采用非阻塞模式 public boolean connect(SocketAddress remote://连接服务器 public boolean finishConnet://如果上面的方法连接失败,接下来就要通过该方法完成连接操作 public int write(ByteBuffer sc);//通道里写数据 public int read(ByteBuffer dst);//从通道里读数据 public final SelectionKey register(Selector sel, int ops, Object a://注册个选择器并设置监听事件,最后一个参数可以设置共享数据 public final void close(;//关闭通道
SocketChannel和ServerSocketChannel区别:SocketChannel更注重于数据上的操作,ServerSocketChannel注重于连接上。
群聊demo:服务端
package com.netty.groupChat; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; import java.util.Set; public class GroupChatServer { private int port = 9999; private ServerSocketChannel listenChannel; private Selector selector; //初始化 public GroupChatServer(){ try{ //创建选择器 selector = Selector.open(); //创建ServerSocketChannel listenChannel = ServerSocketChannel.open(); //绑定端口 listenChannel.socket().bind(new InetSocketAddress(port)); //设置非阻塞模式 listenChannel.configureBlocking(false); //将ServerSocketChannel注册到Secletor中,接受连接事件 listenChannel.register(selector,SelectionKey.OP_ACCEPT); }catch (Exception e){ e.printStackTrace(); } } //监听连接 public void listenEvent() { try { while (true) { //有事件才处理 if(selector.select() > 0){ //遍历循环得到SelectionKey Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectionKeys.iterator(); while (keyIterator.hasNext()) { //得到SelectKey SelectionKey key = keyIterator.next(); //监听对应的事件,这里监听连接事件 if (key.isAcceptable()) { //创建SocketChannel SocketChannel sc = listenChannel.accept(); //设置非阻塞 sc.configureBlocking(false); //将SocketChannel注册到Selector中,处理读事件 sc.register(selector, SelectionKey.OP_READ); //提示上线 System.out.println(sc.getRemoteAddress() + "上线"); } //处理读事件 if (key.isReadable()) { readData(key); } //删除当前的key,避免重复处理 keyIterator.remove(); } }else{ System.out.println("等待。。。。"); } } } catch (Exception e) { e.printStackTrace(); } finally { } } //读取数据 public void readData(SelectionKey key){ //通过key获取绑定的SocketChannel SocketChannel socketChannel = (SocketChannel)key.channel(); //创建缓存区 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); try{ //从通道的数据读取到缓冲区中 int read = socketChannel.read(byteBuffer); //当有数据时 if(read>0){ //获取缓冲区数据 String msg = new String(byteBuffer.array()); //虎丘客户端信息 System.out.println("客户端信息。。。。。"+msg); //向其他用户分发信息 forwardMsg(msg,socketChannel); } }catch (Exception e){ try{ //取消注册 key.cancel(); //关闭通道 socketChannel.close(); System.out.println(socketChannel.getRemoteAddress()+"下线"); }catch (Exception ex){ ex.printStackTrace(); } } } //转发信息 public void forwardMsg(String msg,SocketChannel self) throws IOException { System.out.println("消息转发。。。。。"); //获取到所有注册到selector中的SocketChannel Set<SelectionKey> keys = selector.keys(); for(SelectionKey key : keys){ Channel targetChannel = key.channel(); //不向自己发消息 if(targetChannel instanceof SocketChannel && targetChannel != self){ SocketChannel dest = (SocketChannel) targetChannel; ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes()); //将数据从缓冲区中读到通道中 dest.write(byteBuffer); } } } public static void main(String[] args) { GroupChatServer groupChatServer = new GroupChatServer(); System.out.println("服务器启动。。。"); groupChatServer.listenEvent(); } }
客户端:
package com.netty.groupChat; 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.Scanner; import java.util.Set; public class GroupChatClient { private String host = "127.0.0.1"; private int port = 9999; private SocketChannel socketChannel; private Selector selector; private String user; public GroupChatClient(){ try { //创建选择器 selector = Selector.open(); //连接服务器 socketChannel = socketChannel.open(new InetSocketAddress(host,port)); //设置非阻塞 socketChannel.configureBlocking(false); //将socketChannel注册到selector,处理OP_READ事件 socketChannel.register(selector, SelectionKey.OP_READ); //得到用户 user = socketChannel.getRemoteAddress().toString(); System.out.println("ok ............"); } catch (IOException e) { e.printStackTrace(); } } //发送消息 public void sendInfo(String info) { try{ info = user+"说:"+info; ByteBuffer byteBuffer = ByteBuffer.wrap(info.getBytes()); socketChannel.write(byteBuffer); }catch (Exception e){ e.printStackTrace(); } } //读取数据 public void readInfo() { try{ int readChannel = selector.select(); //有事件 if(readChannel > 0){ //遍历循环,得到SelectionKey Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()){ SelectionKey key = iterator.next(); //处理读取事件 if(key.isReadable()){ //通过key获取绑定的SocketChannel SocketChannel socketChannel =(SocketChannel) key.channel(); //创建缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //将通道里的数据写出到byteBuffer socketChannel.read(byteBuffer); //获取信息 System.out.println(new String(byteBuffer.array())); } //删除当前的SelectionKey,防止重复 iterator.remove(); } }else{ } }catch (Exception e){ e.printStackTrace(); } } public static void main(String[] args) throws IOException { GroupChatClient groupChatClient = new GroupChatClient(); new Thread(() -> { while (true) { groupChatClient.readInfo(); try { Thread.currentThread().sleep(3000); } catch (Exception e) { e.printStackTrace(); } } }).start(); Scanner scanner = new Scanner(System.in); while (scanner.hasNextLine()) { groupChatClient.sendInfo(scanner.nextLine()); } }; }
NIO与零拷贝.
1)零拷贝是网络编程的关键,很多性能优化都离不开。
2)在Java 程序中,常用的零拷贝有mmap(内存映射)和sendFile. 那么,他们在OS里,到底是怎么样的一个
的设计?我们分析mmap和sendFile 这两个零拷贝
传统IO模型:
DMA: directmemory access直接内存拷则(不使用CPU)
mmap优化:
1) mmap通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据.这样,在进行网
络传输时,就可以减少内核空间到用户空间的拷贝次数。如下图
sendFile优化:
Linux2.1版本提供了sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到
Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换。
零拷贝理解:
1) 我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有kermel buffer 有
一份数据),
2)零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的CPU缓存伪共享
以及无CPU校验和计算。
mmap和sendFile的区别:
1》mmap适合小数据量读写,sendFile 适合大文件传输。
2》mmap需要4次上下文切换,3次数据拷贝: sendFile 需要3次上下文切换,最少2次数据拷贝。
3》sendFile可以利用DMA方式,减少CPU拷贝,mmap则不能(必须从内核拷贝到Socket 缓冲区)。
在nio中FileChannel中的transformTo()方法底层原理就是零拷贝。
demo:
public class NioCopyServer { public static void main(String[] args) throws IOException { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().bind(new InetSocketAddress(9998)); while (true){ SocketChannel socketChannel = serverSocketChannel.accept(); ByteBuffer byteBuffer = ByteBuffer.allocate(4096); int readCount = socketChannel.read(byteBuffer); while (readCount == -1){ break; } byteBuffer.rewind(); } } }
客户端
public interface NioCopyClient { public static void main(String[] args) throws IOException { SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9999)); FileInputStream fileInputStream = new FileInputStream(new File("e:\\hello.txt")); FileChannel fileChannel = fileInputStream.getChannel(); long startTimeMillis = System.currentTimeMillis(); long transferTo = fileChannel.transferTo(0, fileChannel.size(), socketChannel); long endTimeMillis = System.currentTimeMillis(); System.out.println("文件大小:"+transferTo+",耗时:"+(endTimeMillis-startTimeMillis)); } }
待更。。。