此文档已在语雀中分享,点击查看。
https://netty.io/
Netty是一个异步事件驱动的网络应用框架。
用于快速开发可维护的高性能协议服务器和客户端。
Netty是jboss提供的一个java开源框架,Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可用性的网络服务器和客户端程序。也就是说Netty是一个基于NO的编程框架,使用Netty可以快速的开发出一个网络应用。
由于Java 自带的NIO api使用起来非常复杂,并且还可能出现 Epoll Bug,这使得我们使用原生的NIO来进行网络编程存在很大的难度且非常耗时。但是Netty良好的设计可以使开发人员快速高效的进行网络应用开发。
1. Netty已经是行业内网络通信编程的标准,广泛应用于通信领域和很多其他中间件技术的底层。
基本上都是使用Netty作为网络通信的底层框架
2. 应用非常广泛
1. 游戏行业
2. 很多框架的通信底层,解决进程间通信。
Spring WebFlux、rocketMQ、dubbo、HBase、Gateway等,是分布式系统,通信的核心。
面试“绝招”
除了提升技术水平之外,另一个大家比较重视的就是面试了。如果有时间,一定要系统性地学习Netty。如果没有掌握Netty的核心原理,那么永远都是Java的初学者。
- Netty的粘包/拆包是怎么处理的,有哪些实现?
- 同步与异步、阻塞与非阻塞的区别?
- BIO、NIO、AIO分别是什么?
- select、poll、epoll的机制及其区别?
所以,深入学习Netty,也是跳槽面试、升职加薪的必备“绝招”。
1. NIO全称成为None Blocking IO (非阻塞IO)。【JDK1.4】
2. 非阻塞 主要应用在网络通信中,能够合理利用资源,提高系统的并发效率。支持高并发的系统访问。
线程也是要占用内存的,一个线程占用1M,内存吃不消。cpu要轮转执行线程,线程有线程上下文记录当前线程执行的状态。传统的web开发,线程的创建是不可控的,怎么控制线程数呢?这个时候就要引入池化思想。提前创建好线程池。
没有线程可用,就会到队列中进行等待。TreadPoolExecutor中的参数说明:第一个参数core pool size,最小的线程数,cup的核数,jdk8之后支持,命令行参数指定cpu核数。第二个参数:最多的线程数。第三个参数:最大等待时长。第五个参数:等待队列,队列中最大多少个线程。
这个时候如果客户端发生阻塞,那么线程也会发生阻塞,等待客户端解除阻塞。比如键盘输入、IO操作。这种模式造成线程无法复用造成资源浪费。
传统的网络通信都是通过InputStream、OutputStream流进行操作,但是在NIO的网络通信使用管道的方式进行通信。
NIO中引入了Seletor,区别于传统的网络通信,Seletor去监控管道正常的通信,正常的读写,没有阻塞的话,就会为其分配线程。
在运行过程中,客户端等待输入,发生阻塞,Seletor是可以监控到的。监控到之后,就会把分配的线程解放出来,让其他的客户端使用。
1. IO通信的通道,类似于InputStream、OutputStream
2. Channel没有方向性
1. 文件操作
FileChannel,读写文件中的数据。
2. 网络操作
SocketChannel,通过TCP读写网络中的数据。
ServerSockectChannel,监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
DatagramChannel,通过UDP读写网络中的数据。
1. FileInputStreanm/FileOutputStream
2. RandomAccessFile
3. Socket
4. ServerSocket
5. DatagramSocket
管道中数据最终是要流向Buffer中的,既然Channel没有方向,那么Buffer就要有方向性,用于区分是读还是写。Buffer作为缓存。多个字节缓存后与程序交互,提高IO的利用率。
1. Channel读取或者写入的数据,都要写到Buffer中,才可以被程序操作。
2. 因为Channel没有方向性,所以Buffer为了区分读写,引入了读模式、写模式进行区分。
1. ByteBuffer
2. CharBuffer
3. DoubleBuffer
4. FloatBuffer
5. IntBuffer
6. LongBuffer
7. ShortBuffer
8. MappedByteBuffer..
1. ByteBuffer.allocate(10);
2. encode()
public class TestNIO1 {
public static void main(String[] args) throws IOException {
//1 创建Channel通道 FileChannel
FileChannel channel = new FileInputStream("/Users/sunshuai/Develop/code/java/idea/netty-proj-lession/netty-basic-01/data.txt").getChannel();
//2 创建缓冲区
//1234567890
ByteBuffer buffer = ByteBuffer.allocate(10);
while (true) {
//3把通道内获取的文件数据,写入缓冲区
int read = channel.read(buffer);
if (read == -1) break;
//4.程序读取buffer内容,后续的操作。设置buffer为读模式。
buffer.flip();
//5.循环读取缓冲区中的数据
while (buffer.hasRemaining()) {
byte b = buffer.get();
System.out.println("(char)b = " + (char) b);
}
//6. 设置buffer的写模式
buffer.clear();
}
}
}
public class TestNIO2 {
public static void main(String[] args) {
//RadomAccessFile 异常处理
FileChannel channel = null;
try {
channel = new RandomAccessFile("/Users/sunshuai/Develop/code/java/idea/netty-proj-lession/netty-basic-01/data.txt", "rw").getChannel();
ByteBuffer buffer = ByteBuffer.allocate(10);
while (true) {
int read = channel.read(buffer);
if (read == -1) break;
buffer.flip();
while (buffer.hasRemaining()) {
byte b = buffer.get();
System.out.println("(char) b = " + (char) b);
}
buffer.clear();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (channel != null) {
try {
channel.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
public class TestNIO3 {
public static void main(String[] args) {
try (FileChannel channel = FileChannel.open(Paths.get("/Users/sunshuai/Develop/code/java/idea/netty-proj-lession/netty-basic-01/data.txt"), StandardOpenOption.READ);) {
ByteBuffer buffer = ByteBuffer.allocate(10);
while (true) {
int read = channel.read(buffer);
if (read == -1) break;
buffer.flip();
while (buffer.hasRemaining()) {
byte b = buffer.get();
System.out.println("(char)b = " + (char) b);
}
buffer.clear();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
1. 获取Channel FileChannel SocketChannel ServerSocketChannel
2. 创建Buffer ByteBuffer
3. 循环的从Channel中获取数据,读入到Buffer中。进行操作.
channel.read(buffer);
buffer.flip();//设置读模式
循环从buffer中获取数据。
buffer.get();
buffer.clear();//设置写模式
buffer.compact();//写模式
思考一个问题,什么时候设计成接口,什么时候设计为抽象类?
名词设计为抽象类,如汽车、形状。
动词设计为接口。DAO、Service。
特例:InputStrem、OutPutStream,除此之外没用例外。
1. HeapByteBuffer 堆ByteBuffer JVM内的堆内存 ---> 读写操作 效率低 会收到GC影响
2. MappedByteBuffer(DirectByteBuffer) OS内存 ---> 读写操作 效率高 不会收到GC影响 。 不主动析构,会造成内存的泄露
内存泄露:本来有100M,但实际上只能用80M
内容溢出:运行程序要100M,而你只有80M
内存泄露的主要原因:
1. ByteBuffer.allocate(10);//一旦分配空间,不可以动态调整
2. encode()
ByteBuffer是一个类似数组的结构,整个结构中包含三个主要的状态
1. Capacity
buffer的容量,类似于数组的size
2. Position
buffer当前缓存的下标,在读取操作时记录读到了那个位置,在写操作时记录写到了那个位置。从0开始,每读取一次,下标+1
3. Limit
读写限制,在读操作时,设置了你能读多少字节的数据,在写操作时,设置了你还能写多少字节的数据
所谓的读写模式,本质上就是这几个状态的变化。主要有Position和Limit联合决定了Buffer的读写数据区域。
最后总结一下
写入Buffer数据之前要设置写模式
1. 写模式
a. 新创建的Buffer自动是写模式
b. 调用了clear,compact方法
读取Buffer数据之前要设置读模式
2. 读模式
1. 调用flip方法
1. channel的read方法
channel.read(buffer)
2. buffer的put方法
buffer.put(byte) buffer.put((byte)'a')..
buffer.put(byte[])
1. channel的write方法
2. buffer的get方法 //每调用一次get方法会影响,position的位置。
3. rewind方法(手风琴),可以将postion重置成0 ,用于复读数据。
4. mark&reset方法,通过mark方法进行标记(position),通过reset方法跳回标记,从新执行.
5. get(i) 方法,获取特定position上的数据,但是不会对position的位置产生影响。
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("sunshuai".getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
System.out.println("buffer.get() = " + (char)buffer.get());
}
buffer.clear();
ByteBuffer buffer = Charset.forName("UTF-8").encode("sunshuai");
1、encode方法自动 把字符串按照字符集编码后,存储在ByteBuffer.
2、自动把ByteBuffer设置成读模式,且不能手工调用flip方法。
ByteBuffer buffer = StandardCharsets.UTF_8.encode("sunshuai");
while (buffer.hasRemaining()) {
System.out.println("buffer.get() = " + (char) buffer.get());
}
buffer.clear();
1、encode方法自动 把字符串按照字符集编码后,存储在ByteBuffer.
2、自动把ByteBuffer设置成读模式,且不能手工调用flip方法。
ByteBuffer buffer = ByteBuffer.wrap("sunshuai".getBytes());
while (buffer.hasRemaining()) {
System.out.println("buffer.get() = " + (char) buffer.get());
}
buffer.clear();
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("孙".getBytes());
buffer.flip();
CharBuffer result = StandardCharsets.UTF_8.decode(buffer);
System.out.println("result.toString() = " + result.toString());
- \n 作为分割符,进行行的区分。
- compact进行处理,把第一次没有读取完的数据,向前移动和后面的内容进行整合。
//1. 半包 粘包
public class TestNIO10 {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(50);
buffer.put("Hi sunshuai\nl love y".getBytes());
doLineSplit(buffer);
buffer.put("ou\nDo you like me?\n".getBytes());
doLineSplit(buffer);
}
// ByteBuffer接受的数据 \n
private static void doLineSplit(ByteBuffer buffer) {
buffer.flip();
for (int i = 0; i < buffer.limit(); i++) {
if (buffer.get(i) == '\n') {
//为了处理一行中有多个换行符,浪费空间的问题
int length = i + 1 - buffer.position();
ByteBuffer target = ByteBuffer.allocate(length);
for (int j = 0; j < length; j++) {
target.put(buffer.get());
}
//截取工作完成
target.flip();
System.out.println("StandardCharsets.UTF_8.decode(target).toString() = " + StandardCharsets.UTF_8.decode(target).toString());
}
}
buffer.compact();
}
}
1. 第一个程序 读文件的内容,读Buffer---> String ---->程序中使用了
public class TestNIO1 {
public static void main(String[] args) throws IOException {
//1 创建Channel通道 FileChannel
FileChannel channel = new FileInputStream("/Users/sunshuai/Develop/code/java/idea/netty-proj-lession/netty-basic-01/data.txt").getChannel();
//2 创建缓冲区
//1234567890
ByteBuffer buffer = ByteBuffer.allocate(10);
while (true) {
//3把通道内获取的文件数据,写入缓冲区
int read = channel.read(buffer);
if (read == -1) break;
//4.程序读取buffer内容,后续的操作。设置buffer为读模式。
buffer.flip();
//5.循环读取缓冲区中的数据
while (buffer.hasRemaining()) {
byte b = buffer.get();
System.out.println("(char)b = " + (char) b);
}
//Charset.forName("UTF-8").decode(buffer).toString ---> String
//6. 设置buffer的写模式
buffer.clear();
}
}
}
public class TestNIO11 {
public static void main(String[] args) throws IOException {
//1 获得Channel FileOutputStream, RandomAccessFile
FileChannel channel = new FileOutputStream("data1").getChannel();
//2 获得Buffer
ByteBuffer buffer = Charset.forName("UTF-8").encode("sunshuai");
//3write
channel.write(buffer);
}
public class TestNIO12 {
public static void main(String[] args) throws IOException {
//data---data2
/* FileInputStream inputStream = new FileInputStream("/Users/sunshuai/Develop/code/java/idea/netty-proj-lession/netty-basic-01/data.txt");
FileOutputStream fileOutputStream = new FileOutputStream("/Users/sunshuai/Develop/code/java/idea/netty-proj-lession/netty-basic-01/data2.txt");
byte[] buffer = new byte[1024];
while (true) {
int read = inputStream.read(buffer);
if (read == -1) break;
fileOutputStream.write(buffer, 0, read);
}*/
/* FileInputStream inputStream = new FileInputStream("/Users/sunshuai/Develop/code/java/idea/netty-proj-lession/netty-basic-01/data.txt");
FileOutputStream fileOutputStream = new FileOutputStream("/Users/sunshuai/Develop/code/java/idea/netty-proj-lession/netty-basic-01/data2.txt");
IOUtils.copy(inputStream,fileOutputStream);*/
FileChannel from = new FileInputStream("/Users/sunshuai/Develop/code/java/idea/netty-proj-lession/netty-basic-01/data.txt").getChannel();
FileChannel to = new FileOutputStream("/Users/sunshuai/Develop/code/java/idea/netty-proj-lession/netty-basic-01/data2.txt").getChannel();
//传输数据上线的 2G-1
// 若果实际文件大小就是超过2G 如何进行文件的copy
//from.transferTo(0, from.size(), to);
long left = from.size();
while (left > 0) {
left = left - from.transferTo(from.size()-left, left, to);
}
}
}
NIO的拷贝效率更高,因为NIO涉及到零拷贝,后面我们会讲到什么是零拷贝。
NIO的数据拷贝是有传输上限的,上限大小是2G-1
服务器用于接收请求,建立连接ServerSocketChannel
客户端用于发送请求
1. 服务端 接受请求、并发、如何接入 ---》 ServerScoketChannel
2. 进行实际通信 ScoketChannel
Tip:端口号是和协议挂钩的,不同的协议端口号可以相同,不会冲突。
通过这版代码 证明了 服务器端 存在2中阻塞
1. 连接阻塞 ----> accept方法存在阻塞---> ServerSocketChannel阻塞。
2. IO阻塞 ----> channel的read方法存在阻塞---> SocketChannel阻塞。
上述分析 对应着的2个问题。
public class MyServer {
public static void main(String[] args) throws IOException {
//1. 创建ServerScoketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2. 设置服务端的监听端口:---》client通过网络进行访问 ip:port http://localhost:8989
serverSocketChannel.bind(new InetSocketAddress(8000));
List<SocketChannel> channelList = new ArrayList<>();
ByteBuffer buffer = ByteBuffer.allocate(20);
//3. 接受client的连接
while (true) {
//4. ScoketChannle 代表 服务端与Client链接的一个通道
System.out.println("等待连接服务器...");
SocketChannel socketChannel = serverSocketChannel.accept();//阻塞 程序等待client
System.out.println("服务器已经连接..."+socketChannel);
channelList.add(socketChannel);
//5. client与服务端 通信过程 NIO代码
for (SocketChannel channel : channelList) {
System.out.println("开始实际的数据通信....");
channel.read(buffer);//阻塞 对应的IO通信的阻塞
buffer.flip();
CharBuffer decode = Charset.forName("UTF-8").decode(buffer);
System.out.println("decode.toString() = " + decode.toString());
buffer.clear();
System.out.println("通信已经结束....");
}
}
}
}
存在问题:会有两种阻塞,一是服务端等待客户端连接阻塞,而是客户端等待输入阻塞。
public static void main(String[] args) throws IOException {
//1.创建ServerScoketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//ip不需要设置,哪台机器运行,Ip就是哪台机器
serverSocketChannel.bind(new InetSocketAddress(8000));
serverSocketChannel.configureBlocking(false);
//2.接收客户端请求 因为要一直接收,所以要用死循环
List<SocketChannel> channelList = new ArrayList<>();
ByteBuffer buffer = ByteBuffer.allocate(20);
while (true) {
//3.返回值就是接收到的客户端请求
SocketChannel channel = serverSocketChannel.accept();//不再阻塞
if (channel != null) {
channel.configureBlocking(false);
channelList.add(channel);
}
//4.将客户端请求存放到集合中
for (SocketChannel socketChannel : channelList) {
int read = socketChannel.read(buffer);//发生阻塞,IO阻塞,等待客户端传输数据
if (read > 0) {
System.out.println("开始实际数据通信....");
buffer.flip();
CharBuffer decode = Charset.forName("UTF-8").decode(buffer);
System.out.println("decode.toString() = " + decode.toString());
System.out.println("数据通信结束....");
buffer.clear();
}
}
}
}
存在问题:第二版代码,虽然解决了阻塞问题,但还有一个问题没有解决,就是CPU一直轮转的问题,无限的循环,我们需要一个组件帮我们监控什么时候有客户端连接进来,什么时候客户端进行读写操作,而不是一直循环。
监控是什么?连接的创建,IO的读写,引入NIO中的Selector。
监控的是什么对象?ServerSocketChannel、SocketChannel。
作为Seletor,不会一直监控这些对象,只有这些对象有特殊状态时候才会监管。
Seletor怎么来监管?
Seletor中有两个属性,一个是keys,一旦被注册,都会被放置在keys属性当中,另一个属性是SelectionKeys,当selector方法真正监控到之后,把实际存在的Accept SSC和READ、WRITE的SC放到SelectionKeys中。
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8000));
serverSocketChannel.configureBlocking(false);//只有在非阻塞的情况下,才可以用Seletor
Selector selector = Selector.open();
//将ServerSocketChannel注册到Seletor
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);
//设置感兴趣的事件,监控的事件
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
System.out.println("MyServer2.main");
//监控
while (true){
selector.select();//等待
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();//拿到监控的对象,SSC、SC
System.out.println("------------------");
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
SelectionKey sck = sc.register(selector, 0, null);
sck.interestOps(SelectionKey.OP_READ);
}else if (key.isReadable()){
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(6);
sc.read(buffer);
buffer.flip();
System.out.println("Charset.defaultCharset().decode(buffer).toString() = " + Charset.defaultCharset().decode(buffer).toString());
}
}
}
}
这里为什么要执行terator.remove()呢?因为当第二次执行的时候会存在空指针。当再次需要用到的时候,会从keys中再次放到Selectkey中。
这版代码解决了ServerSocketChannel的等待客户端连接阻塞,已经SocketChannel的读写阻塞。但是还存在一个问题,就是当客户端发的数据足够多,已经我们在读操作中设定的Buffer空间不够大时,会出现无法一次性读取客户端所有数据的情况。这种情况未读完的数据会被Seletor再次监控到,客户端只是进行了一次写入。
结论:当client连接服务器发起操作后,服务器必须全部处理完成,整个交互才算完成,如果没有全部处理完成,select方法会被一直调用。
在某些特殊清空下,客户端无法处理,select就会频繁调用。如:当客户端关闭的时候,会向服务器写入一个-1表示状态,并不是真正的数据,服务器接收到之后,并不会处理,所以这个状态一直存在,会导致select方法一直被调用。我们应该如何解决呢?如果服务器要解决一些没有用的,且必须要处理的,可以调用cacle方法。
网络中传输数据是按照包传递的,一个包的大小1460B
Seletor循环监听事件的方式 解决死循环空转的问题。
public class MyServer2 {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8000));
serverSocketChannel.configureBlocking(false);//Selector 只有在非阻塞的情况下 才可以使用。
//引入监管者
Selector selector = Selector.open();//1. 工厂,2. 单例
//监管者 管理谁? selector.xxxx(ssc); //管理者 ssc ---> Accept
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);
// selector监控 SSC ACCEPT
// selector
// keys --> HashSet
// register注册 ssc
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
System.out.println("MyServler2.main");
//监控
while (true) {
selector.select();//等待.只有监控到了 有实际的连接 或者 读写操作 ,才会处理。
//对应的 有ACCEPT状态的SSC 和 READ WRITE状态的 SC 存起来
// SelectionsKeys HashSet
System.out.println("-------------------------");
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {//ServerSocketChannel ScoketChannel
SelectionKey key = iterator.next();
//用完之后 就要把他从SelectedKeys集合中删除掉。问题? ServerScoketChannel---SelectedKeys删除 ,后续 SSC建立新的连接?
iterator.remove();
if (key.isAcceptable()) {
//serverSocketChannel.accept();
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
//监控sc状态 ---> keys
SelectionKey sckey = sc.register(selector, 0, null);
sckey.interestOps(SelectionKey.OP_READ);
} else if (key.isReadable()) {
try {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(5);
int read = sc.read(buffer);
if (read == -1) {
key.cancel();
} else {
buffer.flip();
System.out.println("Charset.defaultCharset().decode(buffer).toString() = " + Charset.defaultCharset().decode(buffer).toString());
}
} catch (IOException e) {
//发生异常后的处理。
e.printStackTrace();
key.cancel();
}
}
}
}
}
}
存在问题:当出现异常时,为了不影响其他线程,我们要手动处理异常。这版代码存在半包和粘包问题,我们可以用以前写的方法加入处理半包粘包问题,随之而来,又出现一个问题,会导致数据缺失,当出现粘包问题时,buffer.compact(),会保存未处理完的结果,但是这个buffer并没有被再次使用,我们每次都会新创建一个buffer。下面就要着重解决一个问题:如何保证多个操作ByteBuffer是同一个?
我们知道ByteBuffer是和Channel相关的,如果将Channel和ByteBuffer绑定在一起就行了。
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8000));
serverSocketChannel.configureBlocking(false);//只有在非阻塞的情况下,才可以用Seletor
Selector selector = Selector.open();
//将ServerSocketChannel注册到Seletor
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);
//设置感兴趣的事件,监控的事件
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
System.out.println("MyServer3.main");
//监控
while (true) {
selector.select();//等待
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();//拿到监控的对象,SSC、SC
System.out.println("------------------");
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(7);
SelectionKey sck = sc.register(selector, 0, buffer);
sck.interestOps(SelectionKey.OP_READ);
} else if (key.isReadable()) {
try {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
// ByteBuffer buffer = ByteBuffer.allocate(20);
int read = sc.read(buffer);
if (read == -1) {
key.cancel();
}else {
/*buffer.flip();
System.out.println("Charset.defaultCharset().decode(buffer).toString() = " + Charset.defaultCharset().decode(buffer).toString());*/
doLineSplit(buffer);
}
} catch (IOException e) {
e.printStackTrace();
key.cancel();
}
}
}
}
}
// ByteBuffer接受的数据 \n
private static void doLineSplit(ByteBuffer buffer) {
buffer.flip();
for (int i = 0; i < buffer.limit(); i++) {
if (buffer.get(i) == '\n') {
//为了处理一行中有多个换行符,浪费空间的问题
int length = i + 1 - buffer.position();
ByteBuffer target = ByteBuffer.allocate(length);
for (int j = 0; j < length; j++) {
target.put(buffer.get());
}
//截取工作完成
target.flip();
System.out.println("StandardCharsets.UTF_8.decode(target).toString() = " + StandardCharsets.UTF_8.decode(target).toString());
}
}
buffer.compact();
}
问题:上面的代码虽然解决了数据缺失问题,但是当客户端发送的数据大,或者我们的缓冲区太小,一句话中没有’\n’,这个时候需要扩容。但是扩容之后空间会变大(下一版代码),客户端发送的消息变小了,我们也要考虑缩容的问题(Netty)。ByteBuffer的拷贝,效率问题。
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8000));
serverSocketChannel.configureBlocking(false);//只有在非阻塞的情况下,才可以用Seletor
Selector selector = Selector.open();
//将ServerSocketChannel注册到Seletor
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);
//设置感兴趣的事件,监控的事件
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
System.out.println("MyServer3.main");
//监控
while (true) {
selector.select();//等待
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();//拿到监控的对象,SSC、SC
System.out.println("------------------");
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(7);
SelectionKey sck = sc.register(selector, 0, buffer);
sck.interestOps(SelectionKey.OP_READ);
} else if (key.isReadable()) {
try {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
// ByteBuffer buffer = ByteBuffer.allocate(20);
int read = sc.read(buffer);
if (read == -1) {
key.cancel();
}else {
/*buffer.flip();
System.out.println("Charset.defaultCharset().decode(buffer).toString() = " + Charset.defaultCharset().decode(buffer).toString());*/
doLineSplit(buffer);
//缓冲区空间不够 满了 需要扩容
if (buffer.position()==buffer.limit()){
ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
//将原先的缓冲区数据写到新的
buffer.flip();
newBuffer.put(buffer);
//新的buffer和channel绑定 替换
key.attach(newBuffer);
}
}
} catch (IOException e) {
e.printStackTrace();
key.cancel();
}
}
}
}
}
上面的代码使用‘\n’区分不同信息,实际上还有其他方法解决半包粘包问题:头体分离
Channel和buffer绑定到一起,可以解决粘包问题
SelectionKey sckey = sc.register(selector,0,buffer);
iterator.remove()
把用过的SelectionKey从SeletionKeys集合中剔除
selectionKey.cancle();
把一些无法实际解决的内容,通过cancle()来取消。避免每一次都被select()获取到从新进行循环过程。
读取数据
1. 通过附件的形式,把byteBuffer和channel进行了绑定,从而可以多次处理数据。
2. ByteBuffer的扩容。
数据的写出
1. 第一个问题 写一次数据,当发现数据没有写完,设置WRITE监听状态。
2. 每一次发生Write的状态,都把剩下的数据写出去。
先来看一些图示,这就是Reactor单线程版本。
服务端由一个线程去处理服务端的连接,以及客户端的读写。通过select()监听到不同的状态,以及Channel和Buffer绑定,完成客户端的读写操作,这就是Reactor单线程版本。
Reactor单线程模式有什么问题?由一个线程处理,效率肯定是很低的。下面是一些单线程版代码。
服务端
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8000));
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey sscKeys = iterator.next();
iterator.remove();
if (sscKeys.isAcceptable()){
SocketChannel sc = serverSocketChannel.accept();
sc.configureBlocking(false);
sc.register(selector,SelectionKey.OP_READ);
//准备数据
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 2000000; i++) {
sb.append("l");
}
//NIO buffer存储数据 channel写
ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
while (buffer.hasRemaining()){
int write = sc.write(buffer);
System.out.println("write = " + write);
}
}else if (sscKeys.isReadable()){
}
}
}
}
客户端
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress(8000));
int read = 0;
while (true) {
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
read += socketChannel.read(buffer);
System.out.println("read = " + read);
buffer.clear();
}
}
通过运行代码,发现以下结果:服务器向客户端发送的数据有很多空包,这就是在TCP传输过程中流量控制,服务端发送数据到客户端,发现客户端接收不过来,服务端就会发送一些空包,减轻客户端压力。
站在服务端的角度,这种流量控制有问题吗?对于发送空数据操作,没有意义,但是也在发这种数据,导致有新的客户端连接进来,只能处于等待。
如何处理这种情况?就需要监控Writable状态,允许写的时候再写。
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8000));
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();//selector注册 集合 SocketChannel
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey sscKey = iterator.next();
iterator.remove();
if (sscKey.isAcceptable()) {
SocketChannel sc = serverSocketChannel.accept();
sc.configureBlocking(false);
SelectionKey sckey = sc.register(selector, SelectionKey.OP_READ);
//准备数据
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 20000000; i++) {
sb.append("s");
}
//NIO Buffer存储数据 channel写
ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
int write = sc.write(buffer);
System.out.println("write = " + write);
if (buffer.hasRemaining()) {
//为当前的SoketChannel增加 Write的监听
//READ + Write
sckey.interestOps(sckey.interestOps() + SelectionKey.OP_WRITE);
//把剩余数据存储的buffer传递过去
sckey.attach(buffer);
}
} else if (sscKey.isWritable()) {
//循环含义的
//channel
SocketChannel sc = (SocketChannel) sscKey.channel();
//buffer
ByteBuffer buffer = (ByteBuffer) sscKey.attachment();
//写操作
int write = sc.write(buffer);
System.out.println("write = " + write);
if (!buffer.hasRemaining()) {
sscKey.attach(null);
sscKey.interestOps(sscKey.interestOps() - SelectionKey.OP_WRITE);
}
}
}
}
}
这个时候发现服务端不会再发空包给客户端了,减少了服务端的压力,从而可以腾出手来处理其他的线程请求。
主从式架构,主干活从也干活,但是干的是不一样的活。 和主备式架构的区别就是,主备式架构有备用的主,当主挂掉之后,备用的替上。Redis中的哨兵机制就是典型的主备架构。
主从reactor模式
public class ReactorBossServer {
private static final Logger log = LoggerFactory.getLogger(ReactorBossServer.class);
public static void main(String[] args) throws IOException, InterruptedException {
log.debug("boss thread start ....");
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8000));
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
//模拟多线程的环境,在实际开发中,还是要使用线程池
/*
Worker worker = new Worker("worker1");
*/
Worker[] workers = new Worker[2];
for (int i = 0; i < workers.length; i++) {
workers[i] = new Worker("worker - " + i);//worker-0 worker-1
}
AtomicInteger index = new AtomicInteger();
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey sscSelectionKey = iterator.next();
iterator.remove();
if (sscSelectionKey.isAcceptable()) {
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//sc.register(selector, SelectionKey.OP_READ);
log.debug("boss invoke worker register ...");
//worker-0 worker-1 worker-0 worker-1
//hash取摸 x%2= 0 1 [0,1,0,1]
workers[index.getAndIncrement()% workers.length].register(sc);
log.debug("boss invoked worker register");
}
}
}
}
}
public class Worker implements Runnable {
private static final Logger log = LoggerFactory.getLogger(Worker.class);
private Selector selector;
private Thread thread;
private String name;
private volatile boolean isCreated;//false
private ConcurrentLinkedQueue<Runnable> runnables = new ConcurrentLinkedQueue<>();
//构造方法
//为什么不好?
//Select Thread
public Worker(String name) throws IOException {
this.name = name;
/* thread = new Thread(this, name);
thread.start();
selector = Selector.open();*/
}
//线程的任务
public void register(SocketChannel sc) throws IOException, InterruptedException {
log.debug("worker register invoke....");
if (!isCreated) {
thread = new Thread(this, name);
thread.start();
selector = Selector.open();
isCreated = true;
}
runnables.add(() -> {
try {
sc.register(selector, SelectionKey.OP_READ);//reigster select方法之前运行 。。
} catch (ClosedChannelException e) {
throw new RuntimeException(e);
}
});
selector.wakeup();//select
}
@Override
public void run() {
while (true) {
log.debug("worker run method invoke....");
try {
selector.select();
Runnable poll = runnables.poll();
if (poll != null) {
poll.run();
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey sckey = iterator.next();
iterator.remove();
if (sckey.isReadable()) {
SocketChannel sc = (SocketChannel) sckey.channel();
ByteBuffer buffer = ByteBuffer.allocate(30);
sc.read(buffer);
buffer.flip();
String result = Charset.defaultCharset().decode(buffer).toString();
System.out.println("result = " + result);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
如果selector.select()运行在 sc.register(selector, SelectionKey.OP_READ)之前,那么线程将阻塞,就算后面注册了感兴趣的事件,select()也不会被唤醒。所以必须保证这两个代码在一个线程中运行,使用ConcurrentLinkedQueue队列,将代码传递过去。selector.wakeup()特点就是在之前阻塞可以唤醒,在之后阻塞也可以唤醒。
Java JVM进程不能直接操作硬件,必须要借助操作系统来完成,另外操作硬件需要驱动,没有驱动无法操作硬件。这就注定Java借助操作系统(Linux、Windows、Mac)来操作硬件。
应用程序所占用的内存属于用户内存空间,而操作系统运行在内核地址空间,这两种空间是相互独立,互不侵犯的,可以借助操作系统提供的API通信,所以与操作系统有关的资源,用完就关闭。
JVM想读取文件到应用缓存,不能直接读取硬盘上的文件,实际上是由操作系统读取硬盘上的文件,数据存储在操作系统的内核空间(高速页缓存),随后操作系统的数据再传递到Java应用缓存。所以整个流程上,数据经过了两次数据拷贝。
当JVM想传输数据时,也不能直接写到网卡,因为中间隔着操作系统,只能先写到操作系统,然后由操作系统在写到网卡。
显而易见,这种操作效率很低,在NIO当中提供的一种方法内存映射来减少数据的拷贝。通过在操作系统开辟共享的内存空间,JVM可以直接操作操作系统中的内存,这就减少了一次数据拷贝。坏处就是我们需要用完内存之后手动析构,Java中GC不会对这块内存进行管理。不析构会带来内存泄露。
Linux2.1提供了一个sendFile方法,可以直接将告诉页缓存中的文件拷贝到socket缓存,不再需要从JVM中拷贝到socket缓存,又减少了一次拷贝,这就是零拷贝,没有虚拟机参与的拷贝。
在Linux2.4版本之后,又对拷贝做个改进,可以直接通过高速页缓存拷贝到网卡,不再拷贝到socket,又减少了一次拷贝。