Level 1
netty基本介绍
- netty是一个异步、基于事件驱动的网络应用型框架,常用于开发高性能、可靠性高的网络IO程序
- netty主要针对在TCP协议下,面向Client端的高并发应用,或者点对点场景下数据持续传输的相关应用。
- netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景。
java BIO编程
I/O模型
- I/O模型简单理解;就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通讯的相关性能。
- java一共支持三种网络编程模式NIO BIO AIO
- java BIO同步并阻塞,服务器实现模式为一个链接一个线程,即客户端有链接请求服务器端就需要启动一个线程处理,如果该线程不做任何事情,就会造成不必要的开销。
BIO NIO AIO使用场景分析
- BIO方式适用于链接数目比较小而且固定的架构,这种方式对服务器资源的要求比较高。
- NIO方式适用于链接数目多,且链接比较短的架构,比如聊天服务器,弹幕系统,服务器间通讯
- AIO方式适用于链接数目多且链接比较长的架构,比如相册服务器。
java BIO
Java BIO就是传统的Java I/O编程,其相关的类和接口在java.io。BIO(BlockingI/O):同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)。【后有应用实例】BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,程序简单易理解。
BIO工作机制
对
BIO编程流程解释
- 服务端启用一个
ServerSocket - 客户端启动
Socket对服务器进行通信,默认情况下,服务器端需要对每个客户建立一个线程与之通讯。 - 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝。
- 如果有响应,客户端线程会等待请求结束后,再继续执行。
java BIO应用实例
- 使用
BIO模型编写一个服务器端,监听6666端口,当有客户端连接时,就启动一个线程与之通讯。 - 要求使用线程池机制改善,可以连接多个客户端
- 服务器端可以接收客户端发送的数据
package com.atguigu.bio;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BIOServer {
public static void main(String[] args) throws Exception {
//线程池机制
//思路
//1. 创建一个线程池
//2. 如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
//创建ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器启动了");
while (true) {
System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
//监听,等待客户端连接
System.out.println("等待连接....");
//会阻塞在accept()
final Socket socket = serverSocket.accept();
System.out.println("连接到一个客户端");
//就创建一个线程,与之通讯(单独写一个方法)
newCachedThreadPool.execute(new Runnable() {
public void run() {//我们重写
//可以和客户端通讯
handler(socket);
}
});
}
}
//编写一个handler方法,和客户端通讯
public static void handler(Socket socket) {
try {
System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
byte[] bytes = new byte[1024];
//通过socket获取输入流
InputStream inputStream = socket.getInputStream();
//循环的读取客户端发送的数据
while (true) {
System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
System.out.println("read....");
int read = inputStream.read(bytes);
if (read != -1) {
System.out.println(new String(bytes, 0, read));//输出客户端发送的数据
} else {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("关闭和client的连接");
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
问题分析
- 每个请求都需要创建独立的线程,与对应的客户端进行数据
read,业务处理,数据write - 当并发数较大的时候,需要创建大量线程来处理连接,系统资源占用较大
- 链接建立之后,如果当前线程暂时没有数据可读,则线程就阻塞在
read操作上,造成线程资源浪费。
java NIO编程
基本介绍
java NIO是同步非阻塞的NIO有三大核心部分,ChanelBufferSelectorNIO是面向缓冲区,或者面向块编程,数据读取到一个它稍后处理的缓冲区,需要是可在缓冲区当中前后移动,这就增加了处理过程当中的灵活性。NIO是可以做到一个线程来处理多个操作的
package com.atguigu.nio;
import java.nio.IntBuffer;
public class BasicBuffer {
public static void main(String[] args) {
//举例说明 Buffer 的使用(简单说明)
//创建一个 Buffer,大小为 5,即可以存放 5 个 int
IntBuffer intBuffer = IntBuffer.allocate(5);
//向buffer存放数据
//intBuffer.put(10);
//intBuffer.put(11);
//intBuffer.put(12);
//intBuffer.put(13);
//intBuffer.put(14);
for (int i = 0; i < intBuffer.capacity(); i++) {
intBuffer.put(i * 2);
}
//如何从 buffer 读取数据
//将 buffer 转换,读写切换(!!!)
intBuffer.flip();
while (intBuffer.hasRemaining()) {
System.out.println(intBuffer.get());
}
}
}
NIO和BIO的比较
BIO以流的方式处理数据,而NIO以块的方式处理数据,效率要高很多BIO是阻塞的,NIO是非阻塞的BIO是基于字节流和字符流进行操作,而NIO是基于Channel,和Buffer进行操作,数据总是从通道读取到缓冲区,或者从缓冲区写入到通道当中,selector用于监听多个通道的事件。- Buffer与Channel之间的数据流向是双向的
NIO三大核心原理示意图
关系图
- 每个
Channel都会对应一个Buffer Selector对应一个线程,一个线程对于多个Channel- 程序切换到哪个
Channel是由事件决定的,Event是一个重要的概念。 Selector会根据不同的时间,在各个通道上面切换Buffer就是一个内存块,底层是有一个数组- 数据的读取写入是通过
Buffer,这个和BIO是不同的,BIO中要么是输入流,或者是输出流,不能双向,但是IO和Buffer是可以读可以写,需要flip方法切换Channnel是双向的,可以返回底层操作系统的情况。
缓冲区(Buffer)
缓冲区Buffer本质上是一个可以读写的内存模块,可以理解成一个容器对象(包含数组),该对象提供了一组方法,可以更加轻松的使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer
通道(Channel)
基本介绍
NIO通道类似于流,但是有一下区别,通道可以进行读写,而流只能进行其中一个,通道可以实现异步读写数据,通道可以从缓冲读数据,也可以写数据到缓冲。BIO中的Stream是单向的而后者是双向的
本地文件写数据
package com.atguigu.nio;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel01 {
public static void main(String[] args) throws Exception {
String str = "hello,尚硅谷";
//创建一个输出流 -> channel
FileOutputStream fileOutputStream = new FileOutputStream("d:\file01.txt");
//通过 fileOutputStream 获取对应的 FileChannel
//这个 fileChannel 真实类型是 FileChannelImpl
FileChannel fileChannel = fileOutputStream.getChannel();
//创建一个缓冲区 ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将 str 放入 byteBuffer
byteBuffer.put(str.getBytes());
//对 byteBuffer 进行 flip
byteBuffer.flip();
//将 byteBuffer 数据写入到 fileChannel
fileChannel.write(byteBuffer);
fileOutputStream.close();
}
}
本地文件读数据
package com.atguigu.nio;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel02 {
public static void main(String[] args) throws Exception {
//创建文件的输入流
File file = new File("d:\file01.txt");
FileInputStream fileInputStream = new FileInputStream(file);
//通过 fileInputStream 获取对应的 FileChannel -> 实际类型 FileChannelImpl
FileChannel fileChannel = fileInputStream.getChannel();
//创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());
//将通道的数据读入到 Buffer
fileChannel.read(byteBuffer);
//将 byteBuffer 的字节数据转成 String
System.out.println(new String(byteBuffer.array()));
fileInputStream.close();
}
}
使用一个Buffer完成文件读取、写入
package com.atguigu.nio;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel03 {
public static void main(String[] args) throws Exception {
FileInputStream fileInputStream = new FileInputStream("1.txt");
FileChannel fileChannel01 = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
FileChannel fileChannel02 = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
while (true) { //循环读取
//这里有一个重要的操作,一定不要忘了
/*
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
*/
byteBuffer.clear(); //清空 buffer
int read = fileChannel01.read(byteBuffer);
System.out.println("read = " + read);
if (read == -1) { //表示读完
break;
}
//将 buffer 中的数据写入到 fileChannel02--2.txt
byteBuffer.flip();
fileChannel02.write(byteBuffer);
}
//关闭相关的流
fileInputStream.close();
fileOutputStream.close();
}
}
拷贝文件的transferFrom方法
package com.atguigu.nio;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
public class NIOFileChannel04 {
public static void main(String[] args) throws Exception {
//创建相关流
FileInputStream fileInputStream = new FileInputStream("d:\a.jpg");
FileOutputStream fileOutputStream = new FileOutputStream("d:\a2.jpg");
//获取各个流对应的 FileChannel
FileChannel sourceCh = fileInputStream.getChannel();
FileChannel destCh = fileOutputStream.getChannel();
//使用 transferForm 完成拷贝
destCh.transferFrom(sourceCh, 0, sourceCh.size());
//关闭相关通道和流
sourceCh.close();
destCh.close();
fileInputStream.close();
fileOutputStream.close();
}
}
关于Buffer和Channel注意事项和细节
1 ByteBuffer支持类型化的put和get,put放入的是什么数据类型,get就应该使用相同的数据类型来取出,否则可能出现异常。
2 可以将一个普通Buffer转换成只读Buffer
3 NIO还提供了MappedByBuffer,可以让文件直接在内存中就行修改,而如何同步到文件由NIO完成。
4 NIO支持通过多个Buffer,完成读写操作,即Scattering和Gathering
package NIO;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
public class ScatteringAndGatheringTest {
public static void main(String[] args) throws IOException {
//使用 ServerSocketChannel 和 SocketChannel 网络
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
InetSocketAddress inetSocketAddress=new InetSocketAddress(7000);
//绑定端口到 socket,并启动
serverSocketChannel.socket().bind(inetSocketAddress);
//创建buffer数组
ByteBuffer[] byteBuffers=new ByteBuffer[2];
byteBuffers[0]=ByteBuffer.allocate(5);
byteBuffers[1]=ByteBuffer.allocate(3);
//等待客户端链接
SocketChannel socketChannel=serverSocketChannel.accept();
int messageLength=0;
//循环读取
while (true){
int byteRead=0;
while (byteRead<messageLength){
long l=socketChannel.read(byteBuffers);
byteRead+=l;
System.out.println("byteRead="+byteRead);
//使用流打印
Arrays.asList(byteBuffers).stream().map(buffer->"postion="+buffer.position()
+",limit"+buffer.limit()).forEach(System.out::println);
}
//将所有的buffer进行反转
Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.flip());
//将数据读出显示到客户端
long bytewirte=0;
while (bytewirte<messageLength){
long l=socketChannel.write(byteBuffers);
bytewirte+=l;
}
//将所有到buffer clean
Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.clear());
}
}
}
Selector(选择器)
- java的NIO,用非阻塞的io方式,可以用一个线程来处理多个客户端链接,就会使用
Selector - selector能够检测多个注册通道上是否有事件发生。
- 只有在链接真正有读写事件发生时,才会进行读写,减少系统开销,不用维护多个线程
- 避免了多线之间的上下文切换导致的开销。
Selector示意图和特点说明
netty的io线程NioEventLoop聚合了Selector,可以同时处理成百上千个客户端链接- 当线程从某客户端
Socket通道进行数据读写时,若没有数据可用,该线程可以进行其他任务 - 线程通常将非阻塞
IO的空闲时间用于其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道 - 由于读写操作都是非阻塞的,这就可以充分提升
IO线程的运行效率,避免由于频繁I/O阻塞导致的线程刮起。 - 一个
I/O线程可以并发处理N个客户端链接和读写操作,这从根本上解决了传统同步阻塞I/O一链接一线程模型,架构的性能,弹性伸缩能力和可靠性都得到了极大的提升。
注意事项
NIO中的ServerSocketChannel功能类似ServerSocket、SocketChannel功能类似SocketSelector相关方法说明
NIO非阻塞网络编程原理分析图
NIO非阻塞网络编程相关(Selector Selectionkey ServerScoketChannel和SocketChannel)关系梳理图。
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,完成业务处理。
NIO 非阻塞网络编程快速入门
package com.atguigu.nio;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
public static void main(String[] args) throws Exception{
//创建ServerSocketChannel -> ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//得到一个Selecor对象
Selector selector = Selector.open();
//绑定一个端口6666, 在服务器端监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//把 serverSocketChannel 注册到 selector 关心 事件为 OP_ACCEPT pos_1
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("注册后的selectionkey 数量=" + selector.keys().size()); // 1
//循环等待客户端连接
while (true) {
//这里我们等待1秒,如果没有事件发生, 返回
if(selector.select(1000) == 0) { //没有事件发生
System.out.println("服务器等待了1秒,无连接");
continue;
}
//如果返回的>0, 就获取到相关的 selectionKey集合
//1.如果返回的>0, 表示已经获取到关注的事件
//2. selector.selectedKeys() 返回关注事件的集合
// 通过 selectionKeys 反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
System.out.println("selectionKeys 数量 = " + selectionKeys.size());
//遍历 Set<SelectionKey>, 使用迭代器遍历
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()) {
//获取到SelectionKey
SelectionKey key = keyIterator.next();
//根据key 对应的通道发生的事件做相应处理
if(key.isAcceptable()) { //如果是 OP_ACCEPT, 有新的客户端连接
//该该客户端生成一个 SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端连接成功 生成了一个 socketChannel " + socketChannel.hashCode());
//将 SocketChannel 设置为非阻塞
socketChannel.configureBlocking(false);
//将socketChannel 注册到selector, 关注事件为 OP_READ, 同时给socketChannel
//关联一个Buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("客户端连接后 ,注册的selectionkey 数量=" + selector.keys().size()); //2,3,4..
}
if(key.isReadable()) { //发生 OP_READ
//通过key 反向获取到对应channel
SocketChannel channel = (SocketChannel)key.channel();
//获取到该channel关联的buffer
ByteBuffer buffer = (ByteBuffer)key.attachment();
channel.read(buffer);
System.out.println("form 客户端 " + new String(buffer.array()));
}
//手动从集合中移动当前的selectionKey, 防止重复操作
keyIterator.remove();
}
}
}
}
package com.atguigu.nio;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
public static void main(String[] args) throws Exception{
//创建ServerSocketChannel -> ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//得到一个Selecor对象
Selector selector = Selector.open();
//绑定一个端口6666, 在服务器端监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//把 serverSocketChannel 注册到 selector 关心 事件为 OP_ACCEPT pos_1
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("注册后的selectionkey 数量=" + selector.keys().size()); // 1
//循环等待客户端连接
while (true) {
//这里我们等待1秒,如果没有事件发生, 返回
if(selector.select(1000) == 0) { //没有事件发生
System.out.println("服务器等待了1秒,无连接");
continue;
}
//如果返回的>0, 就获取到相关的 selectionKey集合
//1.如果返回的>0, 表示已经获取到关注的事件
//2. selector.selectedKeys() 返回关注事件的集合
// 通过 selectionKeys 反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
System.out.println("selectionKeys 数量 = " + selectionKeys.size());
//遍历 Set<SelectionKey>, 使用迭代器遍历
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()) {
//获取到SelectionKey
SelectionKey key = keyIterator.next();
//根据key 对应的通道发生的事件做相应处理
if(key.isAcceptable()) { //如果是 OP_ACCEPT, 有新的客户端连接
//该该客户端生成一个 SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端连接成功 生成了一个 socketChannel " + socketChannel.hashCode());
//将 SocketChannel 设置为非阻塞
socketChannel.configureBlocking(false);
//将socketChannel 注册到selector, 关注事件为 OP_READ, 同时给socketChannel
//关联一个Buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("客户端连接后 ,注册的selectionkey 数量=" + selector.keys().size()); //2,3,4..
}
if(key.isReadable()) { //发生 OP_READ
//通过key 反向获取到对应channel
SocketChannel channel = (SocketChannel)key.channel();
//获取到该channel关联的buffer
ByteBuffer buffer = (ByteBuffer)key.attachment();
channel.read(buffer);
System.out.println("form 客户端 " + new String(buffer.array()));
}
//手动从集合中移动当前的selectionKey, 防止重复操作
keyIterator.remove();
}
}
}
}
NIO网络编程应用实例 - 群聊系统
- 编写一个
NIO群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞) - 实现多人的群聊
- 服务器端:可以监测用户上线,离线,并实现消息转发功能
- 客户端 通过
Channel可以无阻塞发送消息给其他用户,同时可以接受其他用户发送的相关消息 - 进一步理解
NIO非阻塞网络编程机制
// 服务端:
package com.atguigu.nio.groupchat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class GroupChatServer {
//定义属性
private Selector selector;
private ServerSocketChannel listenChannel;
private static final int PORT = 6667;
//构造器
//初始化工作
public GroupChatServer() {
try {
//得到选择器
selector = Selector.open();
//ServerSocketChannel
listenChannel = ServerSocketChannel.open();
//绑定端口
listenChannel.socket().bind(new InetSocketAddress(PORT));
//设置非阻塞模式
listenChannel.configureBlocking(false);
//将该 listenChannel 注册到 selector
listenChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void listen() {
try {
//循环处理
while (true) {
int count = selector.select();
if (count > 0) { //有事件处理
// 遍历得到 selectionKey 集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
//取出 selectionkey
SelectionKey key = iterator.next();
//监听到 accept
if (key.isAcceptable()) {
SocketChannel sc = listenChannel.accept();
sc.configureBlocking(false);
//将该 sc 注册到 seletor
sc.register(selector, SelectionKey.OP_READ);
//提示
System.out.println(sc.getRemoteAddress() + " 上线 ");
}
if (key.isReadable()) {//通道发送read事件,即通道是可读的状态
// 处理读(专门写方法..)
readData(key);
}
//当前的 key 删除,防止重复处理
iterator.remove();
}
} else {
System.out.println("等待....");
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//发生异常处理....
}
}
//读取客户端消息
public void readData(SelectionKey key) {
SocketChannel channel = null;
try {
//得到 channel
channel = (SocketChannel) key.channel();
//创建 buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = channel.read(buffer);
//根据 count 的值做处理
if (count > 0) {
//把缓存区的数据转成字符串
String msg = new String(buffer.array());
//输出该消息
System.out.println("form客户端:" + msg);
//向其它的客户端转发消息(去掉自己),专门写一个方法来处理
sendInfoToOtherClients(msg, channel);
}
} catch (IOException e) {
try {
System.out.println(channel.getRemoteAddress() + "离线了..");
//取消注册
key.cancel();
//关闭通道
channel.close();
} catch (IOException e2) {
e2.printStackTrace();
}
}
}
//转发消息给其它客户(通道)
private void sendInfoToOtherClients(String msg, SocketChannel self) throws IOException {
System.out.println("服务器转发消息中...");
//遍历所有注册到 selector 上的 SocketChannel,并排除 self
for (SelectionKey key : selector.keys()) {
//通过 key 取出对应的 SocketChannel
Channel targetChannel = key.channel();
//排除自己
if (targetChannel instanceof SocketChannel && targetChannel != self) {
//转型
SocketChannel dest = (SocketChannel) targetChannel;
//将 msg 存储到 buffer
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
//将 buffer 的数据写入通道
dest.write(buffer);
}
}
}
public static void main(String[] args) {
//创建服务器对象
GroupChatServer groupChatServer = new GroupChatServer();
groupChatServer.listen();
}
}
// 客户端:
package com.atguigu.nio.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;
public class GroupChatClient {
//定义相关的属性
private final String HOST = "127.0.0.1";//服务器的ip
private final int PORT = 6667;//服务器端口
private Selector selector;
private SocketChannel socketChannel;
private String username;
//构造器,完成初始化工作
public GroupChatClient() throws IOException {
selector = Selector.open();
//连接服务器
socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
//设置非阻塞
socketChannel.configureBlocking(false);
//将 channel 注册到selector
socketChannel.register(selector, SelectionKey.OP_READ);
//得到 username
username = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(username + " is ok...");
}
//向服务器发送消息
public void sendInfo(String info) {
info = username + " 说:" + info;
try {
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
//读取从服务器端回复的消息
public void readInfo() {
try {
int readChannels = selector.select();
if (readChannels > 0) {//有可以用的通道
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isReadable()) {
//得到相关的通道
SocketChannel sc = (SocketChannel) key.channel();
//得到一个 Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取
sc.read(buffer);
//把读到的缓冲区的数据转成字符串
String msg = new String(buffer.array());
System.out.println(msg.trim());
}
}
iterator.remove(); //删除当前的 selectionKey,防止重复操作
} else {
//System.out.println("没有可以用的通道...");
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
//启动我们客户端
GroupChatClient chatClient = new GroupChatClient();
//启动一个线程,每个 3 秒,读取从服务器发送数据
new Thread() {
public void run() {
while (true) {
chatClient.readInfo();
try {
Thread.currentThread().sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
//发送数据给服务器端
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String s = scanner.nextLine();
chatClient.sendInfo(s);
}
}
}
NIO与零拷贝
零拷贝基本介绍
- 零拷贝是网络编程的关键,很多性能优化都离不开
- 在
java程序当中,常用的零拷贝有mmap(内存映射)和sendFile,那么他们在os里面到底要如何设计。 - 看一下
NIO当中如何使用零拷贝
传统io数据读写
File file = new File("test.txt");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
byte[] arr = new byte[(int) file.length()];
raf.read(arr);
Socket socket = new ServerSocket(8080).accept();
socket.getOutputStream().write(arr);
mmap优化与sendFile优化
mmap通过内存映射,将文件映射到内存缓冲区,同时用户空间可以共享内核空间的数据,这样,在进行网络传输的时候就可以减少内核空间到用户空间的拷贝次数。Linux2.1版本提供了sendFile函数,数据不需要经过用户态,直接从内存缓冲区进入到SocketBuffer,由于和用户态完全无关,就减少了一次上下文切换。
零拷贝再次理解
- 我们说零拷贝,是从操作系统的角度来说。因为内核缓冲区之间,没有数据是重复的。
- 零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少
cpu缓存 伪共享以及无cpu校验和计算。
mmap和sendFile的区别
mmap适合小数据量读写,sendFile适合大文件传输mmap需要进行4次上下文切换,3次数据拷贝,sendFile需要3次上下文切换,最少2次数据拷贝sendFile可以使用DMA方式,减少CPU的拷贝,mmap则不能(必须从内核拷贝到Socket缓冲区)
NIO零拷贝案例
- 使用传统
IO方法传递一个大文件 - 使用
NIO零拷贝方式传递一个大文件
NewIOServer.java
package com.atguigu.nio.zerocopy;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
//服务器
public class NewIOServer {
public static void main(String[] args) throws Exception {
InetSocketAddress address = new InetSocketAddress(7001);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(address);
//创建buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(4096);
while (true) {
SocketChannel socketChannel = serverSocketChannel.accept();
int readcount = 0;
while (-1 != readcount) {
try {
readcount = socketChannel.read(byteBuffer);
} catch (Exception ex) {
// ex.printStackTrace();
break;
}
//
byteBuffer.rewind(); //倒带 position = 0 mark 作废
}
}
}
}
NewIOClient.java
package com.atguigu.nio.zerocopy;
import java.io.FileInputStream;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
public class NewIOClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 7001));
String filename = "protoc-3.6.1-win32.zip";
//得到一个文件channel
FileChannel fileChannel = new FileInputStream(filename).getChannel();
//准备发送
long startTime = System.currentTimeMillis();
//在 linux 下一个 transferTo 方法就可以完成传输
//在 windows 下一次调用 transferTo 只能发送 8m, 就需要分段传输文件,而且要主要
//传输时的位置=》课后思考...
//transferTo 底层使用到零拷贝
long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println("发送的总的字节数 = " + transferCount + " 耗时: " + (System.currentTimeMillis() - startTime));
//关闭
fileChannel.close();
}
}
Java AIO 基本介绍
JDK7引入了AsynchronousI/O,即AIO。在进行I/O编程中,常用到两种模式:Reactor和Proactor。Java的NIO就是Reactor,当有事件触发时,服务器端得到通知,进行相应的处理AIO即NIO2.0,叫做异步不阻塞的IO。AIO引入异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用- 目前
AIO还没有广泛应用,Netty也是基于NIO,而不是AIO,因此我们就不详解AIO了,有兴趣的同学可以参考《Java新一代网络编程模型AIO原理及Linux系统AIO介绍》
BIO、NIO、AIO 对比表
| BIO | NIO | AIO | |
|---|---|---|---|
| IO模型 | 同步阻塞 | 同步非阻塞(多路复用) | 异步非阻塞 |
| 编程难度 | 简单 | 复杂 | 复杂 |
| 可靠性 | 差 | 好 | 好 |
| 吞吐量 | 低 | 高 | 高 |
举例说明
- 同步阻塞:到理发店理发,就一直等理发师,直到轮到自己理发。
- 同步非阻塞:到理发店理发,发现前面有其它人理发,给理发师说下,先干其他事情,一会过来看是否轮到自己.
- 异步非阻塞:给理发师打电话,让理发师上门服务,自己干其它事情,理发师自己来家给你理发