non-blocking io,jdk1.4后新增
读写数据的双向通道,可以用channel将数据从buffer中读出,也可以将buffer的数据写入channel,而java的Stream流要么输入要么输出,常用的Channel有SocketChannel、ServerSocketChannel、FileChannel
Buffer用于缓冲与读写数据,常用的是ByteBuffer
为了限制多线程设计下线程的最大数量,可以在服务器端利用线程池处理客户端请求
缺点:
thread不直接与客户端请求连接,中间引入Selector,Selector用于监视所管理的所有Channel,当监控到Channel中的事件处于就绪,由Selector通知thread去处理对应的客户端请求,提高服务器端线程利用率
@Slf4j
public class TestByteBuffer {
public static void main(String[] args) {
//FileChannel的获取方式
//1. 通过输入输出流获取(下面使用这种方式)
//2. 通过RandomAccessFile获取
try (FileChannel channel = new FileInputStream("data.txt").getChannel()) {
//准备10个字节的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
while (true) { //每次while循环最多读取10个字节数据
//从channel读取数据,利用channel操作buffer,并写入buffer
int len = channel.read(buffer); //read方法返回值为-1时表示channel中数据已经读取完毕
log.debug("读取到的字节数{}", len);
if (len == -1) break; //当返回-1时退出读取循环
//打印buffer内容
buffer.flip(); //切换读模式
while (buffer.hasRemaining()) { //buffer中是否还有未读数据
byte b = buffer.get(); //读一个字节
log.debug("读取到的实际字节{}", (char)b);
}
//切换为写模式
buffer.clear();
}
} catch (IOException e) {
}
}
}
通过以上例子可以看出,ByteBuffer的使用步骤如下
Buffer中三个重要属性
Buffer刚初始化时,在写模式下的状态,position为下一个字节写入到buffer的位置。limit=capacity,写入限制=最大容量
当写入4个字节后的状态:
当调用buffer.flip()切换为读模式后,position切换为读取位置,limit表示读取限制
当buffer中所有数据都读取完成后
当读取完成后,调用buffer.clear()切换为写模式
如果buffer中的数据还没读取完,就想要切换为写模式,应该调用buffer.compact方法,compact可以把未读取的部分向前压缩,接着在未读取的数据后面写入新数据
/** buffer1是HeapByteBuffer类型
* 即java堆内存,读写效率较低,会受到垃圾回收影响
* 但堆内存分配时效率较高
*/
ByteBuffer buffer1 = ByteBuffer.allocate(16);
/**buffer2是DirectByteBuffer类型
* 即系统直接内存,读写效率较高(相比HeapByteBuffer少一次数据拷贝)
* 且不会被垃圾回收影响
* 但直接内存分配时效率较低
*/
ByteBuffer buffer2 = ByteBuffer.allocateDirect(16);
ByteBuffer读取数据的API演示
//初始buffer
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put(new byte[]{'a', 'b', 'c', 'd'});
buffer.flip();
rewind重置position
//读取buffer中所有数据
buffer.get(new byte[4]);
debugAll(buffer);
//重置读模式下position指针
buffer.rewind();
debugAll(buffer);
//mark & reset
//mark做一个标记,记录position位置
//reset是将position重置到mark的位置
buffer.get();
buffer.get(); //先读取两次,postion此时指向索引为2的数据
buffer.mark(); //在position当前位置,即索引为2的位置添加mark标记
buffer.get();
buffer.get();
buffer.reset(); //将position重置到索引2的位置
debugAll(buffer);
get(i)用于直接获取索引为i除的数据,不会影响position的位置
//get(i)
buffer.get(3);
debugAll(buffer);
//1. 字符串转为ByteBuffer,将字符串转为字节数组写入buffer
ByteBuffer buffer1 = ByteBuffer.allocate(16);
buffer1.put("hello".getBytes()); //执行完成后buffer1还是在写模式
debugAll(buffer1);
//2. Charset
ByteBuffer buffer2 = StandardCharsets.UTF_8.encode("hello"); //执行完成后buffer2被切换为读模式
debugAll(buffer2);
//3. wrap
ByteBuffer buffer3 = ByteBuffer.wrap("hello".getBytes()); //执行完成后buffer2被切换为读模式
debugAll(buffer3);
//buffer转为字符串,使用decode方法,注意这里传入的buffer必须要是读模式,比如传入buffer1就会出错
String str1 = StandardCharsets.UTF_8.decode(buffer2).toString();
System.out.println(str1);
如把下面的文件words.txt中的one、two、three分别读取到三个buffer中
//通过RandomAccessFile获取channel,参数1为读取的文件名,参数2为指定channel的模式为只读
try (FileChannel channel = new RandomAccessFile("words.txt", "r").getChannel()) {
ByteBuffer b1 = ByteBuffer.allocate(3);
ByteBuffer b2 = ByteBuffer.allocate(3);
ByteBuffer b3 = ByteBuffer.allocate(5);
//使用scattering read,将三个ByteBuffer作为数组传给read方法,可以分散将结果读取到三个buffer中
channel.read(new ByteBuffer[]{b1, b2, b3});
debugAll(b1);
debugAll(b2);
debugAll(b3);
} catch (IOException e) {
};
ByteBuffer b1 = StandardCharsets.UTF_8.encode("hello");
ByteBuffer b2 = StandardCharsets.UTF_8.encode("world");
//一个汉字3个字节
ByteBuffer b3 = StandardCharsets.UTF_8.encode("你好");
//将channel中的数据写入到words2文件中,设置channel模式为读写模式
try (FileChannel channel = new RandomAccessFile("words2.txt", "rw").getChannel()) {
channel.write(new ByteBuffer[]{b1, b2, b3});
} catch (IOException e) {
}
在网络上发送多条数据给服务器,约定数据之间用\n作为分割,比如发送三条原始数据给服务器:
Hello world\n
Im ikun\n
How are you\n
但是在网络传输后,服务器收到的数据变成了下面两条
Hello world\nIm ikun\nHo
w are you\n
其中原本第一条和第二条数据合并成了一条数据,这就是粘包,而原本第三条数据被分割成了两条数据,这就是半包,由于客户端发送数据给服务器时是将所有三条数据一起发送的,所以服务器接收到的数据就会出现粘包,而半包是由于服务器的缓冲区大小限制,一次不能完全读取完客户端发送的所有数据,所以数据会被截断
public static void main(String[] args) {
//假设source就是服务器端用于接收客户端数据的buffer
ByteBuffer source = ByteBuffer.allocate(32);
//模拟粘包和半包现象,向source中放入粘包和半包数据
source.put("Hello world\nIm ikun\nHo".getBytes());
//调用方法解析数据
split(source);
source.put("w are you\n".getBytes());
split(source);
}
private static void split(ByteBuffer source){
//切换读方法
source.flip();
for(int i = 0;i < source.limit();i++){
//使用get(i)找到完整消息结尾的索引,get(i)不会移动source的position
if(source.get(i) == '\n'){
//计算完整消息长度,长度等于当前找到/n的索引+1-position的位置
int length = i+1-source.position();
//把完整消息存入新的target
ByteBuffer target = ByteBuffer.allocate(length);
//从source读取,读取后source的position向后移动,再向target写
for(int j = 0;j < length;j++){
target.put(source.get());
}
//输出target中的完整消息
debugAll(target);
}
}
//因为可能存在半包现象,所以用compact
source.compact();
}
FileChannel只能工作在阻塞模式下
只能通过FileInputStream、FileOutputStream或者RandomAccessFile来获取FileChannel,并且前两者获取的channel只能读或写,最后一个可以指定既可读也可写
int readBytes = channel.read(buffer)
从channel中读取数据填充到buffer中,readBytes表示读取到了多少字节,-1表示读取到了末尾
ByteBuffer buffer = ....;
buffer.put(...); //存入数据
buffer.flip(); //切换buffer到读模式
while(buffer.hasRemaining()){ //因为调用一次write不能保证buffer中所有数据都被写入channel,所以通过while条件循环判断
channel.write(buffer);
}
使用完毕后,需要关闭channel
public class TestFileChannelTransferTo {
public static void main(String[] args) {
try (
//从from中读取数据并写入到to中
FileChannel from = new FileInputStream("data.txt").getChannel();
FileChannel to = new FileOutputStream("to.txt").getChannel()
) {
long size = from.size();
//用left代表剩下需要传输的数据,当left>0时说明需要继续传输
for(long left = size;left > 0;) {
//transferTo底层会运用零拷贝进行优化,参数1为从from的哪个位置开始拷贝,参数2为拷贝的数据量,参数3为目标channel
//返回值为实际传输的数据量,用left-,transferTo方法一次最多传输2g,所以可能需要多次传输
left -= from.transferTo(size-left, left, to);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static void countFilesNum() throws IOException {
//子路径数计数
AtomicInteger dirCount = new AtomicInteger();
//子文件数技术
AtomicInteger fileCount = new AtomicInteger();
//访问D:\IdeaStudy\Netty目录下所有子目录和子文件
Files.walkFileTree(Paths.get("D:\\IdeaStudy\\Netty"), 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 {
System.out.println(file);
fileCount.incrementAndGet();
return super.visitFile(file, attrs);
}
});
//输出子目录和文件数
System.out.println("dirCount:" + dirCount);
System.out.println("fileCount:" + fileCount);
}
private static void deleteDir() throws IOException {
Files.walkFileTree(Paths.get("D:\\IdeaStudy\\Netty"), new SimpleFileVisitor<Path>(){
//进入目录中,先删除目录中每个文件
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return super.visitFile(file, attrs);
}
//退出目录时目录中文件已经被删完了,所以可以删除目录本身
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return super.postVisitDirectory(dir, exc);
}
});
}
private static void copyDir() throws IOException {
//原路径
String source = "D:\\IdeaStudy\\Netty";
//目标路径
String target = "D:\\IdeaStudy\\Netty\\testCopy";
//path代表每次遍历到的目录或文件
Files.walk(Paths.get(source)).forEach(path->{
try {
//先把源文件中的源路径替换为目标路径
String targetName = path.toString().replace(source, target);
//如果path是目录就新建目录,如果是文件就拷贝文件
if (Files.isDirectory(path)) { //目录
Files.createDirectories(Paths.get(targetName));
} else if (Files.isRegularFile(path)) { //文件
Files.copy(path, Paths.get(targetName));
}
}catch (IOException e){
e.printStackTrace();
}
});
}
服务器代码:
@Slf4j
public class Server {
//单线程模式下的阻塞模式
public static void main(String[] args) throws IOException {
//buffer用于与channel配合读取数据
ByteBuffer buffer = ByteBuffer.allocate(16);
//创建服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
//绑定服务器端口
ssc.bind(new InetSocketAddress(8080));
//用于存放与客户端建立连接的集合
List<SocketChannel> channels = new ArrayList<>();
//服务器建立连接
while (true){
//sc用于和客户端通信
log.debug("connecting...");
//accept是阻塞方法,当运行到这行代码,那么在一个客户端建立新连接之前,执行到此处的线程会暂停运行
SocketChannel sc = ssc.accept();
log.debug("connected {}...",sc);
//将刚刚通过accept建立连接的channel放入list保存
channels.add(sc);
//循环读取list中所有不同客户端发送的保存在channel中数据
for (SocketChannel channel: channels){
log.debug("before read {}...", channel);
//从channel读,向buffer写,read也是阻塞方法,如果当前这次循环到的客户端连接对应的channel中没有数据,则线程暂停运行
channel.read(buffer);
buffer.flip();
debugRead(buffer);
buffer.clear();
log.debug("after read {}...", channel);
}
}
}
}
客户端代码:
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost", 8080));
sc.write(Charset.defaultCharset().encode("hello"));
}
}
SocketChannel sc = ssc.accept();
处阻塞channel.read(buffer);
处阻塞SocketChannel sc = ssc.accept();
继续等待下一个客户端建立连接改造服务器端代码为非阻塞模式
@Slf4j
public class Server {
//nio非阻塞模式单线程
public static void main(String[] args) throws IOException {
//buffer用于与channel配合读取数据
ByteBuffer buffer = ByteBuffer.allocate(16);
//创建服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
//将服务器切换为非阻塞模式,影响accept方法,默认是阻塞模式
ssc.configureBlocking(false);
//绑定服务器端口
ssc.bind(new InetSocketAddress(8080));
//用于存放与客户端建立连接的集合
List<SocketChannel> channels = new ArrayList<>();
//服务器建立连接
while (true){
//sc用于和客户端通信
//accept变成非阻塞方法,即使没有客户端建立连接,线程可以继续运行,返回null
SocketChannel sc = ssc.accept();
//当accept接收到客户端连接返回不为null后,再将其放入list保存
if(sc != null){
log.debug("connected {}...",sc);
//将socketChannel设置为非阻塞模式,影响read方法
sc.configureBlocking(false);
//将刚刚通过accept建立连接的channel放入list保存
channels.add(sc);
}
//循环读取list中所有不同客户端发送的保存在channel中数据
for (SocketChannel channel: channels){
//非阻塞read,如果没有读取到客户端写入channel中的数据,read方法返回0
int read = channel.read(buffer);
//当能够从channel中读取数据时
if(read > 0){
buffer.flip();
debugRead(buffer);
buffer.clear();
log.debug("after read {}...", channel);
}
}
}
}
}
该模式下服务器线程会一直运行,即使accept和read方法没有得到结果,虽然相比阻塞模式能提高线程利用率,但是因为即使客户端没有连接或写入,服务器线程也会不停监测,导致cpu资源会被浪费
将channel邦迪到一个selector上后,selector会自动监控这个channel中的事件,当有事件发生时,selector.select()
才能继续运行;绑定channel后,selector中会创建一个和这个channel对应的selectedKey,当channel发生事件时,能够通过这个对应的selectedKey拿到事件类型和发生事件的channel的信息,可以指明每个selectedKey具体关注哪个事件,selectorKey的事件有以下几种:
改造服务端代码:
@Slf4j
public class Server {
//nio非阻塞模式单线程
public static void main(String[] args) throws IOException {
//创建selector,管理多个channel
Selector selector = Selector.open();
//创建服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
//将服务器切换为非阻塞模式,影响accept方法,默认是阻塞模式
ssc.configureBlocking(false);
//注册channel到selector上,当这个channel有事件发生时,selector就能监测到,同时会绑定一个SelectionKey到selector上,这个key专门用于管理这个channel,同时会返回这个SelectionKey,可以通过SelectionKey返回值得到发生的是什么事件,和发生事件的channel
SelectionKey sscKey = ssc.register(selector, 0, null);
//指明sscKey只关注accept事件
sscKey.interestOps(SelectionKey.OP_ACCEPT);
log.debug("注册ServerSocketChannel:{},等待客户端连接...", sscKey);
//绑定服务器端口
ssc.bind(new InetSocketAddress(8080));
while (true){
//select方法,没有事件发生时,运行到此处的线程阻塞,有事件发生,线程恢复运行
selector.select();
//处理事件,selectedKeys返回selector上selectedKeys集合的迭代器,selectedKeys保存有事件发生的channel对应的selectionKey
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
log.debug("检测到selector上绑定的channel发生了事件,开始遍历所有selectedKeys");
while (iterator.hasNext()){
//获取当前遍历到的selectedKey的迭代器
SelectionKey key = iterator.next();
//从selector的selectedKeys集合中移除当前遍历到的selectedKey,如果不做这一步,下面可能会出现空指针异常
iterator.remove();
//获得事件的channel,并区分事件的类型
if(key.isAcceptable()){ //如果是客户端连接事件
log.debug("本次遍历获取到ServerSocketChannel的连接事件: {}", key);
获取触发连接事件的ServerSocketChannel
ServerSocketChannel channel = (ServerSocketChannel)key.channel();
//处理连接事件
SocketChannel sc = channel.accept();
//修改sc为非阻塞
sc.configureBlocking(false);
//将sc注册到selector上,sc这个channel之后就通过selector中的scKey进行管理
SelectionKey scKey = sc.register(selector, 0, null);
//指定scKey只关注read事件
scKey.interestOps(SelectionKey.OP_READ);
log.debug("建立新的SocketChannel:{}",sc);
}else if(key.isReadable()){ //如果是读取事件
try {
log.debug("本次遍历获取到SocketChannel的读取事件: {}", key);
//获取触发读取事件的SocketChannel
SocketChannel channel = (SocketChannel)key.channel();
//创建16字节的ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(16);
//read再读取时返回读取的字节数,若客户端正常断开,read方法返回-1
int read = channel.read(buffer);
if(read == -1)
//当客户端建立连接后,客户端正常关闭时会触发read事件,但此时若再从channel中读取数据,读取不到任何数据
//且对应的selectedKey的read事件也没有被处理,在下次循环时,还会从selectionKeys中放到selectedKeys集合中
//所以这里要用cancel方法将对应channel的key进行反注册,将这个key对应的channel从之前注册到的selector上移除
key.cancel();
else {
//切换buffer为读模式
buffer.flip();
//读取buffer中内容
debugRead(buffer);
}
} catch (IOException e) {
e.printStackTrace();
//当客户端建立连接后,客户端强制关闭时会触发read事件,但此时若再从channel中读取数据,则会抛出io异常
//且对应的selectedKey的read事件也没有被处理,在下次循环时,还会从selectionKeys中放到selectedKeys集合中
//所以这里要用cancel方法将对应channel的key进行反注册,将这个key对应的channel从之前注册到的selector上移除
key.cancel();
}
}
}
log.debug("遍历selectedKeys结束");
}
}
}
selector.select()
的工作原理是selector中所有绑定的channel有未处理的事件时,就不会阻塞。所以获取到selector中的未处理事件后要通过accept、read或cancel方法处理网络通信可能出现的情况
在上图第一种情况下,如果服务器用于从channel中读取客户端发送数据的buffer长度小于客户端发送的一条完整数据的长度,则这个channel对应的selector在下次执行selector.select();时不会被阻塞,直到channel中数据被读取完
在之前的代码中如果出现这种情况,那么第一次使用channel.read(buffer)读取到保存在buffer中的内容会被丢失,即下图的代码会执行多次
针对这个问题,接收客户端数据的buffer需要可以扩容,并且不能是局部变量
处理方法:
@Slf4j
public class Server {
//用于根据\n分割完整消息并输出
private static void split(ByteBuffer source){
source.flip();
for(int i = 0;i < source.limit();i++){
//找到一条完整消息
if(source.get(i) == '\n'){
//计算完整消息长度,长度等于当前找到/n的索引+1-position的位置
int length = i+1-source.position();
//把完整消息存入新的byteBuffer
ByteBuffer target = ByteBuffer.allocate(length);
//从source读取,向target写
for(int j = 0;j < length;j++){
//使用get从source中读取一次,把source的position下标移动一次
target.put(source.get());
}
debugAll(target);
}
}
source.compact();
}
//nio非阻塞模式单线程
public static void main(String[] args) throws IOException {
//创建selector,管理多个channel
Selector selector = Selector.open();
//创建服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
//将服务器切换为非阻塞模式,影响accept方法,默认是阻塞模式
ssc.configureBlocking(false);
//注册channel到selector上,当这个channel有事件发生时,selector就能监测到,同时会绑定一个SelectionKey到selector上,这个key专门用于管理这个channel,同时会返回这个SelectionKey,可以通过SelectionKey返回值得到发生的是什么事件,和发生事件的channel
SelectionKey sscKey = ssc.register(selector, 0, null);
//指明sscKey只关注accept事件
sscKey.interestOps(SelectionKey.OP_ACCEPT);
log.debug("注册ServerSocketChannel:{},等待客户端连接...", sscKey);
//绑定服务器端口
ssc.bind(new InetSocketAddress(8080));
while (true){
//select方法,没有事件发生时,运行到此处的线程阻塞,有事件发生,线程恢复运行
selector.select();
//处理事件,selectedKeys返回selector上selectedKeys集合的迭代器,selectedKeys保存有事件发生的channel对应的selectionKey
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
log.debug("检测到selector上绑定的channel发生了事件,开始遍历所有selectedKeys");
while (iterator.hasNext()){
//获取当前遍历到的selectedKey的迭代器
SelectionKey key = iterator.next();
//从selector的selectedKeys集合中移除当前遍历到的selectedKey,如果不做这一步,下面可能会出现空指针异常
iterator.remove();
//获得事件的channel,并区分事件的类型
if(key.isAcceptable()){ //如果是客户端连接事件
log.debug("本次遍历获取到ServerSocketChannel的连接事件: {}", key);
获取触发连接事件的ServerSocketChannel
ServerSocketChannel channel = (ServerSocketChannel)key.channel();
//处理连接事件
SocketChannel sc = channel.accept();
//修改sc为非阻塞
sc.configureBlocking(false);
//创建4字节的ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(4);
//将sc注册到selector上,sc这个channel之后就通过selector中的scKey进行管理
//同时将buffer作为附件绑定到这个channel上,这个buffer只用于接收该channel中的数据,避免多个channel同时有事件时公用同一个buffer读取
//buffer作为附件绑定到channel上后,如果从channel中读取数据时发生半包、粘包,下次也能直接拿到channel对应的buffer,在其中position的位置接着写入
SelectionKey scKey = sc.register(selector, 0, buffer);
//指定scKey只关注read事件
scKey.interestOps(SelectionKey.OP_READ);
log.debug("建立新的SocketChannel:{}",sc);
}else if(key.isReadable()){ //如果是读取事件
try {
log.debug("本次遍历获取到SocketChannel的读取事件: {}", key);
//获取触发读取事件的SocketChannel
SocketChannel channel = (SocketChannel)key.channel();
//获取注册channel到selector时所关联的附件,这里就是拿到从该channel中读取数据的buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
//read在读取时返回读取的字节数,若客户端正常断开,read方法返回-1
//从channel中读取内容到buffer中,如果buffer一次存不下channel中所有数据,这个channel对应的selector下次在执行selector.select();时不会阻塞,直到channel中数据被读取完
int read = channel.read(buffer);
if(read == -1)
//当客户端建立连接后,客户端正常关闭时会触发read事件,但此时若再从channel中读取数据,读取不到任何数据
//且对应的selectedKey的read事件也没有被处理,在下次循环时,还会从selectionKeys中放到selectedKeys集合中
//所以这里要用cancel方法将对应channel的key进行反注册,将这个key对应的channel从之前注册到的selector上移除
key.cancel();
else {
//尝试从buffer中根据\n分割一条完整消息
split(buffer);
//split中最后会把buffer切换回写模式,如果写模式下position=limit说明整条消息长度超过了buffer容量,需要进行扩容
if(buffer.position() == buffer.limit()){
ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
buffer.flip();
newBuffer.put(buffer);
//将从channel中读取数据的buffer替换为新的buffer
key.attach(newBuffer);
}
}
} catch (IOException e) {
e.printStackTrace();
//当客户端建立连接后,客户端强制关闭时会触发read事件,但此时若再从channel中读取数据,则会抛出io异常
//且对应的selectedKey的read事件也没有被处理,在下次循环时,还会从selectionKeys中放到selectedKeys集合中
//所以这里要用cancel方法将对应channel的key进行反注册,将这个key对应的channel从之前注册到的selector上移除
key.cancel();
}
}
}
log.debug("遍历selectedKeys结束");
}
}
}
当服务器发送大量数据给客户端时,可能服务器需要分成多次发送,在多次发送时,需要进行一定优化,避免当服务器缓冲已满时服务器还在尝试写入数据的情况
服务器端代码
public class WriteServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
ssc.bind(new InetSocketAddress(8080));
while(true){
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if(key.isAcceptable()){
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//向客户端发送大量数据
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 30000000; i++) {
stringBuilder.append("a");
}
ByteBuffer buffer = Charset.defaultCharset().encode(stringBuilder.toString());
//返回值代表实际写入字节数
while (buffer.hasRemaining()){
int write = sc.write(buffer);
//打印本次向客户端发送的数据字节数
System.out.println(write);
}
}
}
}
}
}
客户端代码
public class WriteClient {
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost",8080));
//接收数据,count保存一共从服务器获取的字节数
int count = 0;
while (true){
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
count += sc.read(buffer);
System.out.println(count);
buffer.clear();
}
}
}
运行结果:
可以看到服务器发送数据时,会发送多次,当服务器缓冲区写满后,如果客户端没有及时读取就会出现写入0字节的情况,由于下面的while循环会一直不停尝试写入数据,就算缓冲区已满
这种方式会导致服务器资源被浪费,因为当缓冲区满时就不应该尝试再进行写入,服务器改造后如下
public class WriteServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
ssc.bind(new InetSocketAddress(8080));
while(true){
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if(key.isAcceptable()){
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
SelectionKey scKey = sc.register(selector, 0, null);
//建立连接后直接向客户端发送大量数据
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 30000000; i++) {
stringBuilder.append("a");
}
ByteBuffer buffer = Charset.defaultCharset().encode(stringBuilder.toString());
//向客户端发送数据,返回值代表实际写入字节数,当数据量大时,会分多次写入,buffer数据在完全写入sc之前,sc会有一个写入事件,不会被selector.select()阻塞
int write = sc.write(buffer);
//打印本次向客户端发送的数据字节数
System.out.println(write);
//当buffer中还有数据时,说明数据一次不能写完到客户端,需要多次写入
if (buffer.hasRemaining()){
//让channel关注可写事件
scKey.interestOps(scKey.interestOps() + SelectionKey.OP_WRITE);
//将未写完的数据关联到scKey上
scKey.attach(buffer);
}
}else if(key.isWritable()){ //当服务器缓冲区有空余位置,且还有写入事件时
//取得与key关联的,存放未写完数据的buffer
ByteBuffer buffer = (ByteBuffer)key.attachment();
//取得与key关联的channel
SocketChannel sc = (SocketChannel) key.channel();
int write = sc.write(buffer);
System.out.println(write);
//当数据完全写完后,清理
if(!buffer.hasRemaining()){
key.attach(null); //清除关联的buffer
key.interestOps(key.interestOps() - SelectionKey.OP_WRITE); //key不再关注写事件
}
}
}
}
}
经过改造后,运行结果如下:
服务器端在不能写入时,不会再不停尝试写入
之前的模式都是使用的单线程,一个selector管理客户端连接、读写事件,当一个事件的执行时间较长,会影响整个系统的执行效率,所以可以采用上图所示的模式,将所有连接事件交给一个单独的selector管理,再将不同客户端的读写事件分给多个不同的selector管理,提高效率
优化和客户端代码
public class MultiThreadServer {
public static void main(String[] args) throws IOException {
//主线程名更名为boss,只负责管理客户端的连接事件
Thread.currentThread().setName("boss");
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
//管理客户端连接事件
Selector boss = Selector.open();
SelectionKey bossKey = ssc.register(boss, 0, null);
bossKey.interestOps(SelectionKey.OP_ACCEPT);
ssc.bind(new InetSocketAddress(9090));
Worker[] workers = new Worker[2];
for(int i = 0;i < workers.length;i++){
workers[i] = new Worker("worker-"+i);
}
AtomicInteger index = new AtomicInteger();
//创建固定数量的worker
Worker worker = new Worker("selector-0");
while (true){
boss.select();
Iterator<SelectionKey> iter = boss.selectedKeys().iterator();
while (iter.hasNext()){
SelectionKey key = iter.next();
iter.remove();
if(key.isAcceptable()){
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
log.debug("服务器与客户端:({})建立连接", sc.getRemoteAddress());
//sc关联worker的selector
log.debug("worker初始化前:({})", sc.getRemoteAddress());
//轮询负载均衡给workers中的worker分配socketChannel事件
workers[index.getAndIncrement() % workers.length].register(sc); //这行代码被boss线程调用
log.debug("worker初始化后:({})建立连接", sc.getRemoteAddress());
}
}
}
}
static class Worker implements Runnable{
private Thread thread;
private Selector selector;
private String name;
private volatile boolean start = false; //线程和selector都为初始化
private ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>();
public Worker(String name) {
this.name = name;
}
//worker的初始化方法
public void register(SocketChannel sc) throws IOException {
if(!start){
thread = new Thread(this, name);
selector = Selector.open();
thread.start();
start = true;
}
//由boss线程向队列添加任务,但任务不会立即执行;
queue.add(()->{
//如果worker线程run方法中selector.select()在下面这行代码之前执行,并且worker线程执行selector.select()被阻塞,则当前boss线程中的这行代码也会被阻塞
try {
sc.register(selector,SelectionKey.OP_READ);
} catch (ClosedChannelException e) {
e.printStackTrace();
}
});
selector.wakeup(); //避免在worker线程中被selector.select()阻塞的情况,让其可以继续运行后面的注册register
}
@Override
public void run() {
while (true){
try {
//当worker线程执行selector.select()被阻塞时,boss线程调用worker中register方法执行sc.register(selector,SelectionKey.OP_READ)时也会被阻塞
selector.select();
Runnable task = queue.poll();
if(task != null){
task.run(); //在这worker线程取出之前boss线程通过queue.add放入queue中的sc.register(selector,SelectionKey.OP_READ)并执行,将新建立连接的客户端的sc关联到worker的selector上
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if(key.isReadable()){
ByteBuffer buffer = ByteBuffer.allocate(16);
SocketChannel channel = (SocketChannel) key.channel();
log.debug("worker读取到了:{}", channel.getRemoteAddress());
channel.read(buffer);
buffer.flip();
debugAll(buffer);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
客户端代码
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
//与服务器端建立连接
sc.connect(new InetSocketAddress("localhost", 9090));
//向服务器发送hello
int write = sc.write(Charset.defaultCharset().encode("123456789123456\n"));
System.in.read();
}
}
用户调用read、write等方法进行读写操作时,都需要切换到内核空间,由操作系统完成具体的读写工作,在这个基础上有多种不同的io模型
同步:整个工作从头到尾由一个线程完成,包括发起请求到获取到最后的执行结果
异步:当需要进行某个工作时,由A线程将需要进行的工作内容放在一个通知中,并发送给另一个B线程,在通知中传递一个回调方法,之后A线程继续进行后续工作流程,当B线程执行完通知中的工作后,再通过回调方法将执行结果返回给A线程,整个异步过程至少需要两个线程
多路复用工作方式对比阻塞IO
阻塞IO:在阻塞模式下,当用户线程由于在等待某个特定事件而进入阻塞后,即使之后有其他事件发生,也不能去处理其他已经发生的事件,必须要等待之前等待的特定事件的发生后才能处理其他事件,比如用户先执行channel1的read,获取到数据后又执行accept等待连接事件,若此时channel1中又有数据,可以直接read事件时,必须要等待一个accept事件的发生才能执行到channel1的新read事件
同步多路复用:在多路复用中,select方法不关心发生的具体是什么事件,只要selector管理的channel中有事件发生,就会直接返回用户空间。用户空间再通过channel获取到发生的事件的类型,根据不同类型再切换到内核空间执行内核事件,不同事件之间不会相互影响
需求:从磁盘上读取文件内容,再发送到一个客户端,实现代码如下
整个工作流程中,进行了4次数据拷贝:
整个工作流程中,进行了3次用户态与内核态之间的切换
利用NIO进行零拷贝优化:NIO中的transferTo/ttransferFrom对应linux中的sendFile方法,在底层可以实现零拷贝:
调用transferTo方法时,会进行一次用户态到内核态的切换,可以直接将数据从磁盘读取到内核缓冲区,再把数据直接发送到网卡进行传输,只会讲一些offset、length写入socket缓冲区,几乎没有性能消耗
零拷贝的优势:
即异步IO,下面以AIO读取文件举例:从利用AIO的channel从data.txt中读取数据并输出
@Slf4j
public class AioFIleChannel {
public static void main(String[] args){
//AsynchronousFileChannel为AIO中的channel
//参数1:操作的文件
//参数2:需要进行什么操作
try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(Paths.get("data.txt"), StandardOpenOption.READ)) {
//参数1:读取到的ByteBuffer
//参数2:读取起始位置
//参数3:附件,当一次读取不完时,用另一个buffer继续读取
//参数4:回调对象,包含回调方法
ByteBuffer buffer = ByteBuffer.allocate(16);
log.debug("read mission begin...");
//channel的read实际交给另一个线程进行,完成后结果返回主线程
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override //成功回调,参数1为读取到的数据长度,参数2为channel.read第三个参数附件,因为绑定的附件就是参数1,所以方法中可以通过attachment获取读取到的内容
public void completed(Integer result, ByteBuffer attachment) {
attachment.flip();
debugAll(attachment);
log.debug("read completed...");
}
@Override //失败回调
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
log.debug("read mission return...");
//channel.read的执行线程是守护线程,为了防止主线程执行完成守护线程被销毁,用输入阻塞主线程
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端向服务器发送Hello world,服务器接收后输出客户端发送的数据,服务器端代码:
public class HelloServer {
public static void main(String[] args) {
//服务器启动器,负责组装netty组件
new ServerBootstrap()
//NioEventLoopGroup中包含多个EventLoop,每个EventLoop包含一个线程和选择器,可以循环检测事件的发生
.group(new NioEventLoopGroup()) //NioEventLoopGroup中包含监测accept事件和监测read事件的EventLoop
//选择服务器的ServerSocketChannel的实现类
.channel(NioServerSocketChannel.class)
//child负责处理读写事件,这里childHandler就是指定处理读写事件的具体内容,决定了child能执行哪些操作(handler)
.childHandler(
//ChannelInitializer负责给child添加handler
new ChannelInitializer<NioSocketChannel>() {
//添加handler的方法,客户端连接后由netty内部的连接处理器调用
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
//添加把ByteBuf转为字符串的handler
channel.pipeline().addLast(new StringDecoder());
//添加自定义handler
channel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
//自定义handler处理读事件
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//打印上一步转换完成的字符串
System.out.println(msg);
}
});
}
}
)
//绑定服务器ServerSocketChannel的监听端口
.bind(9090);
}
}
客户端代码:
public class HelloClient {
public static void main(String[] args) throws InterruptedException {
//客户端启动器
new Bootstrap()
//创建客户端EventLoopGroup
.group(new NioEventLoopGroup())
//选择客户端SocketChannel实现类
.channel(NioSocketChannel.class)
//连接建立后调用ChannelInitializer中的initChannel方法进行初始化
.handler(new ChannelInitializer<NioSocketChannel>() {
//连接建立后被netty内部的连接处理器调用
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
channel.pipeline().addLast(new StringEncoder());
}
})
//连接服务器
.connect(new InetSocketAddress("localhost", 9090))
//阻塞方法,直到连接建立才继续执行
.sync()
//返回客户端和服务器端连接的socketChannel
.channel()
//调用channel的方法向服务器发送数据
.writeAndFlush("Hello world");
}
}
channel.pipeline.addLast(Handler)
中的参数,每个handler可以设置关注多个不同的事件,当对应的channel发送该事件时,就能执行其中的代码;handler可以分为inbound入站处理器和outbound出站处理器,inbound处理写入事件,outbound处理读取事件@Slf4j
public class TestEventLoop {
public static void main(String[] args) {
//创建事件循环组,当不传参数时,ctrl点进构造方法,发现默认参数是0,再继续跟,会发现当参数是0时,会取(1、io.netty.eventLoopThreads变量的值、cpu线程数*2)中的最大值作为该NioEventLoopGroup中拥有的EventLoop对象数量
EventLoopGroup group = new NioEventLoopGroup(2); //NioEventLoopGroup可以处理io事件、普通任务、定时任务
//EventLoopGroup eventExecutors = new DefaultEventLoop(); //DefaultEventLoop可以处理普通任务和定时任务
//获取下一个事件循环对象,当执行次数超过其中的EventLoop数量时,会从头开始继续读取
EventLoop eventLoop = group.next();
//将某个普通任务交给eventLoop对象异步执行
eventLoop.submit(new Runnable() {
@Override
public void run() {
log.debug("eventLoop thread");
}
});
//将某个定时任务交给eventLoop对象异步执行,参数1是任务,参数2是延迟时间2,参数3是循环间隔时间,参数4是时间单位
eventLoop.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
log.debug("async eventloop thread");
}
},0,1, TimeUnit.SECONDS);
log.debug("main thread");
}
}
当channel交给EventLoopGroup后,就会与EventLoopGroup中的某个EventLoop进行绑定,后续这个channel再触发任何事件,都由同样的EventLoop进行处理
服务器端
@Slf4j
public class TestEventLoopServer {
public static void main(String[] args) {
//group专门处理耗时长的channel的事件,DefaultEventLoopGroup类只能处理普通任务和定时任务,不能处理io任务
EventLoopGroup group = new DefaultEventLoopGroup(2);
new ServerBootstrap()
//group可以接收两个参数
//第一个参数为boss group,只负责处理ServerSocketChannel的accept事件,并将处理后的SocketChannel交给第二个参数的worker,将其绑定到worker中某个EventLoop中,这里的boss group中只会有一个线程工作,因为服务器只会有一个ServerSocketChannel
//第二个参数为worker group,负责处理SocketChannel的事件,指定worker EventLoopGroup中有2个EventLoop,若不指定参数,无参构造会以cpu线程数*2作为参数
//当客户端建立连接时,netty默认以轮询方式将ServerSocketChannel通过处理accept事件而新建立的channel绑定到worker EventLoopGroup中每个EventLoop上
.group(new NioEventLoopGroup(), new NioEventLoopGroup(2))
//选择服务器端的ServerSocketChannel的实现类
.channel(NioServerSocketChannel.class)
//有客户端建立连接时,创建一个NioSocketChannel
.childHandler(new ChannelInitializer<NioSocketChannel>() {
//初始化建立连接时创建的NioSocketChannel
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
//向该channel的pipeline中添加一个handler,addLast方法的参数就是添加的handler,ChannelInboundHandlerAdapter这个handler用于处理入站事件,即从远程端到本地端的数据流动,例如接收到的数据、连接建立等事件
//绑定后,该channel触发任何事件时,就会根据触发的事件类型,依次匹配pipeline中各handler中的方法
//addLast如果没有参数1指定EventLoopGroup,则默认就用服务器中的worker group执行任务,这个channel就会与group中某个EventLoop绑定,以后这个channel再触发事件,都是由同一个EventLoop执行
//参数2为handler名
//参数3为handler需要执行的任务
channel.pipeline().addLast("h1", new ChannelInboundHandlerAdapter(){
//channel在接收到客户端发送的新的数据时被调用
@Override //msg是ByteBuf类型
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
//将byteBuf转为String输出
log.debug(byteBuf.toString(Charset.defaultCharset()));
//将任务交割pipeline中下一个handler继续处理
ctx.fireChannelRead(msg);
}
//addLast参数1如果指定某个EventLoopGroup,则这个handler需要完成的工作交给指定的group执行
}).addLast(group, "h2", new ChannelInboundHandlerAdapter(){
//channel在接收到客户端发送的新的数据时被调用
@Override //msg是ByteBuf类型
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
//将byteBuf转为String输出
log.debug(byteBuf.toString(Charset.defaultCharset()));
}
});
}
})
.bind(9090);
}
}
客户端
public class TestEventLoopClient {
public static void main(String[] args) throws InterruptedException {
//客户端启动器
Channel channel = new Bootstrap()
//创建客户端的EventLoopGroup
.group(new NioEventLoopGroup())
//选择客户端SocketChannel实现类
.channel(NioSocketChannel.class)
//连接建立后创建一个NioSocketChannel和服务器通信
//并调用ChannelInitializer中的initChannel方法进行初始化
.handler(new ChannelInitializer<NioSocketChannel>() {
//连接建立后被netty内部的连接处理器调用
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
channel.pipeline().addLast(new StringEncoder());
}
})
//连接服务器
.connect(new InetSocketAddress("localhost", 9090))
//阻塞方法,直到连接建立才继续执行
.sync()
//返回客户端和服务器端连接的socketChannel
.channel();
for(int i = 0;i < 10;i++){
Thread.sleep(1000);
channel.writeAndFlush("Hello world:" + i);
}
}
}
假设有三个客户端建立连接,则对应三个SocketChannel,这三个channel都有h1和h2两个handler需要执行,下图就表名了每个channel触发事件时,每个channel中需要执行的这两个handler由服务器端哪个EventLoopGroup中哪个EventLoop来执行的关系,比如channel1的h1由NioEventLoopGroup中的EventLoop1执行,h2由DefaultEventLoopGroup中的EventLoop1执行
io.netty.channel.AbstractChannelHandlerContext#invokeChannelRead
//调用Handler的channelRead方法传入消息
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
//pipeline的touch方法触摸消息,并记录已经被处理
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
//获取到channel对应的pipeline中的下一个handler的EventLoop
EventExecutor executor = next.executor();
//判断当前handler的eventLoop是否和下个handler的EventLoop一致,如果一致说明下个handler的任务也是当前eventLoop中的线程进行处理,所以直接调用下个handler的channelRead方法
if (executor.inEventLoop()) {
next.invokeChannelRead(m);
//如果不一致说明下个handler需要切换到其他EventLoopGroup中的EventLoop执行,所以线程也需要切换,所以将调用channelRead的代码封装到Runnable对象中,将其交给下个handler对应的EventLoop中的线程进行执行
} else {
executor.execute(new Runnable() {
public void run() {
next.invokeChannelRead(m);
}
});
}
}
主要方法:
客户端代码
public class EventLoopClient {
public static void main(String[] args) throws InterruptedException {
//Future、Promise类型都是和异步方法配套使用
ChannelFuture channelFuture = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
channel.pipeline().addLast(new StringEncoder());
}
})
//异步非阻塞,main发起调用,将connect的执行交给上面在group方法中创建的NioEventLoopGroup中的一个线程执行
.connect(new InetSocketAddress("localhost", 9090));
//处理方法一:connect的连接工作交给其他线程,连接后剩余工作还是main线程执行
//sync阻塞main线程,使用sync同步执行connect的线程,直到执行connect的线程完全建立连接后,通知main才能继续执行
channelFuture.sync();
//如果没有上面的sync,main就直接在执行channelFuture.channel(),而此时执行connect的线程可能还没有完全建立与服务器端的通信并创建SocketChannel,所以如果没有sync,服务器可能就不能正常接收到客户端发送的消息
Channel channel = channelFuture.channel();
channel.writeAndFlush("hello world");
//处理方法二:connect的连接工作和后续剩余工作全交给其他线程,main线程通过addListener给channelFuture添加一个回调对象,其中的方法在连接建立后由建立连接的线程继续执行
channelFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
Channel channel = channelFuture.channel();
channel.writeAndFlush("hello world");
}
});
}
}
需求:当客户端断开与服务器连接时,需要做一些关闭连接的操作,如释放资源,客户端新开一个input线程循环接收用户输入,若输入不是q则发送给服务器,否则退出并释放资源
客户端代码:
@Slf4j
public class EventLoopClient {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
//Future、Promise类型都是和异步方法配套使用
ChannelFuture channelFuture = new Bootstrap()
.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
channel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
channel.pipeline().addLast(new StringEncoder());
}
})
//异步非阻塞,main发起调用,将connect的执行交给上面在group方法中创建的NioEventLoopGroup中的一个线程执行
.connect(new InetSocketAddress("localhost", 9090));
Channel channel = channelFuture.sync().channel();
//开一个新线程名叫input,作用是检测控制台输入,当输入不为q时,将输入发送给服务器端,否则退出,并且在退出时需要释放资源
new Thread(()->{
Scanner scanner = new Scanner(System.in);
while (true){
String line = scanner.nextLine();
if("q".equals(line)){
//close是异步方法,由创建channel时绑定的NioEventLoopGroup中的EventLoop进行关闭
channel.close();
break;
}
log.debug("input线程收到输入:{}", line);
channel.writeAndFlush(line);
}
}, "input").start();
//获取closeFuture,同理和connect类似,有两种方法处理关闭时的善后操作
ChannelFuture closeFuture = channel.closeFuture();
//1)同步方式做关闭时处理,处理关闭的代码由main线程执行
closeFuture.sync();
log.debug("channel关闭时释放资源。。。");
//关闭客户端的NioEventLoopGroup,不再接收新数据,并且将缓冲中的数据都发送出去后再完全关闭
group.shutdownGracefully();
//2)异步方式做关闭时处理,处理关闭的代码由main线程交给执行channel.close的线程执行
closeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
log.debug("channel关闭时释放资源。。。");
group.shutdownGracefully();
}
});
}
}
当客户端向服务器正常发送消息时:
使用方式一退出时:
使用方式二退出时:
@Slf4j
public class JdkFuture {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(2);
//提交任务给线程池执行
Future<Integer> future = service.submit(() -> {
log.debug("执行计算");
Thread.sleep(1000);
return 50;
});
log.debug("等待结果");
//主线程同步阻塞获取future的结果
log.debug("运行结果:{}",future.get());
}
}
运行结果
2. netty future:可以同步等待任务结束得到结果(sync)或异步等待结果(addListener),但都要等待任务结束才能得到结果
@Slf4j
public class NettyFuture {
public static void main(String[] args) throws ExecutionException, InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
EventLoop eventLoop = group.next();
//提交任务给EventLoop执行
Future<Integer> future = eventLoop.submit(() -> {
log.debug("执行计算");
Thread.sleep(1000);
return 70;
});
log.debug("等待结果");
//同步阻塞获取EventLoop结果,由主线程获取
log.debug("获取结果:{}", future.get());
//异步获取结果,由执行future的EvnetLoop线程获取
future.addListener(future1 -> log.debug("获取结果:{}", future1.getNow()));
}
}
3. netty promise:脱离任务单独存在,只作为容器在两个线程间传递结果
@Slf4j
public class NettyPromise {
public static void main(String[] args) throws ExecutionException, InterruptedException {
EventLoop eventLoop = new NioEventLoopGroup().next();
//主线程主动创建promise对象,就是线程间放结果的容器,而future是被动由子线程返回的结果
DefaultPromise<Integer> promise = new DefaultPromise<>(eventLoop);
//创建线程
new Thread(()->{
log.debug("执行计算");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//任务执行完成,向promise中放入结果
promise.setSuccess(80);
}).start();
log.debug("等待结果");
//主线程利用promises同步阻塞接收结果
log.debug("结果是:{}", promise.get());
}
}
重要接口
功能/名称 | jdk Future | netty Future | Promise |
---|---|---|---|
cancel | 取消任务 | - | - |
isCanceled | 任务是否取消 | - | - |
isDone | 任务是否完成,不能区分成功失败 | - | - |
get | 获取任务结果,阻塞等待,若失败抛异常 | - | - |
getNow | - | 获取任务结果,非阻塞,还未产生结果时返回 null | - |
await | - | 同步阻塞等待任务结束,如果任务失败,不会抛异常,而是通过 isSuccess 判断 | - |
sync | - | 同步阻塞等待任务结束,但不获取任务结果,如果任务失败,抛出异常 | - |
isSuccess | - | 判断任务是否成功 | - |
cause | - | 获取失败信息,非阻塞,如果没有失败,返回null | - |
addLinstener | - | 添加回调,异步接收并处理结果 | - |
setSuccess | - | - | 设置成功结果 |
setFailure | - | - | 设置失败结果 |
Handler用于处理channel中各种事件,分为入站和出站,所有同一channel的Handler连在一起就是pipeline
Channel相当于加工车间,Pipeline是车间的流水线,Handler是流水线中的各种加工工序,ByteBuf是加工原材料,先经过一道道入站工序,再经过一道道出站工序最后变成产品
Pipeline流水线的示例代码和入站出站时的执行顺序
@Slf4j
public class NettyPipeline {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
//通过channel获取到pipeline
ChannelPipeline pipeline = channel.pipeline();
//添加Handler,netty会给每个pipeline默认添加一个head和一个tail处理器
//即有客户端消息入站时处理器链为:head->h1->h2->h3->tail
//服务器端向channel中写入消息并出站时的处理器链为:tail->h6->h5->h4->head
pipeline.addLast("handler1", new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
String name = buf.toString(Charset.defaultCharset());
log.debug("inbound handler1 1 将接收到的数据转换为String:{}", name);
//将处理完的数据交给下个handler处理
super.channelRead(ctx, name);
}
});
pipeline.addLast("handler12", new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object name) throws Exception {
Student student = new Student(name.toString());
log.debug("inbound handler1 2 将String转换为Student对象:{}", student);
super.channelRead(ctx, student);
}
});
pipeline.addLast("handler13", new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("inbound handler1 3 获取到的结果是:{},结果的类型是:{}",msg,msg.getClass());
//向channel中写入消息,触发outbound handler
channel.writeAndFlush(ctx.alloc().buffer().writeBytes("server...".getBytes(StandardCharsets.UTF_8)));
}
});
pipeline.addLast("handler14", new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("outbound handler1 4");
super.write(ctx, msg, promise);
}
});
pipeline.addLast("handler15", new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("outbound handler1 5");
super.write(ctx, msg, promise);
}
});
pipeline.addLast("handler16", new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("outbound handler1 6");
super.write(ctx, msg, promise);
}
});
}
})
.bind(9090);
}
@Data
@AllArgsConstructor
static class Student{
String name;
}
}
Pipeline中入站和出站时不同方法下的handler调用顺序
super.channelRead(ctx, student);
内部就是ctx.fireChannelRead(msg);
,ctx.fireChannelRead(msg);
作用是调用pipeline中从当前inboundHandler出发向tail方向的下一个inboundHandler的channelRead方法channel.writeAndFlush
作用是调用从pipeline的tail往head方向找第一个outboundHandler的write方法super.write(ctx, msg, promise);
内部就是ctx.write(msg, promise);
,作用是调用从当前outboundHandler出发向head方向的下一个outboundHandler的write方法当需要测试一些写好的handler是否正确时,可以使用EmbeddedChannel,无需启动服务器和客户端
@Slf4j
public class NettyEmbeddedChannel {
public static void main(String[] args) {
ChannelInboundHandlerAdapter handler1 = new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("handler1被调用");
super.channelRead(ctx, msg);
}
};
ChannelInboundHandlerAdapter handler2 = new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("handler2被调用");
super.channelRead(ctx, msg);
}
};
ChannelOutboundHandlerAdapter handler3 = new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("handler3被调用");
super.write(ctx, msg, promise);
}
};
ChannelOutboundHandlerAdapter handler4 = new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("handler4被调用");
super.write(ctx, msg, promise);
}
};
//EmbeddedChannel可以用来测试各种写好的Handler是否正确,无需启动服务器和客户端
EmbeddedChannel channel = new EmbeddedChannel(handler1, handler2, handler3, handler4);
//模拟入站
channel.writeInbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("hello".getBytes(StandardCharsets.UTF_8)));
//模拟出站
channel.writeOutbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("hello".getBytes(StandardCharsets.UTF_8)));
}
}
ByteBuf是netty对ByteBuffer的增强
public class TestByteBuf {
public static void main(String[] args) {
//buf相比buffer可以自动扩容,而buffer超过容量后会异常;参数不指定时默认大小为256字节
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
System.out.println("buf的初始容量为:"+buf.capacity());
for(int i = 0;i < 300;i++){
//循环写入300个a,占用300字节
buf.writeBytes("a".getBytes(StandardCharsets.UTF_8));
}
System.out.println("buf写入300字节后的容量为:"+buf.capacity());
}
}