1、Netty介绍
Netty原是由JBOSS提供的一个Java开源框架,现为Github的独立项目。Netty是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络IO程序。主要针对在TCP协议下,面向Client端的高并发应用,或Peer-to-Peer场景下的大量数据持续传输应用。本质上Netty是一个NIO框架,深入学习Netty首先需要学习NIO。
2、I/O模型
2.1 I/O模型基本说明
1) 简单理解:就是用什么样的通道进行数据的发送和接收
2)Java共支持3种网络编程模型I/O:BIO、NIO、AIO
Java BIO:同步并阻塞(创痛阻塞型),服务器实现模式为一个连接一个线程,即客户端与服务器端有连接请求时就需要启动一个线程进行处理,如果这个连接不做任何事情就会导致不必要的线程开销
Java NIO:同步非阻塞,服务器实现模式为一个线程处理多个请求,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。
Java AIO(NIO.2):异步非阻塞,AIO引入异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
2.2 Java BIO
BIO(blocking I/O):同步阻塞,可通过线程池机制改善不必要的线程开销。适用于连接数目较小且固定的架构,这种方式对服务器资源要求较高,并发局限于应用中。
BIO编程简单流程:
1)服务器端启动一个ServerSocket
2)客户端启动Socket对服务器进行通信,默认情况下服务器端需要对每个连接(客户)建立一个线程与之通讯
3)客户端发送请求后,先咨询服务器是否有线程响应,如果没有则会等待,或直接被拒绝
4)如果响应,客户端线程会等待请求结束后,在继续执行
测试实例:
public class BIOServer {
public static void main(String[] args) throws IOException {
// 1、创建一个线程池
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
// 2、创建ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务启动");
while (true) {
// 监听,等待客户端连接
final Socket socket = serverSocket.accept();
System.out.println("连接到客户端");
// 创建线程,与之通讯
newCachedThreadPool.execute(new Runnable() {
public void run() {
handler(socket);
}
});
}
}
// 写一个handler方法,和客户端通讯
public static void handler(Socket socket) {
byte[] data = new byte[1024];
// 通过socke,获取输入流
InputStream inputStream;
try {
inputStream = socket.getInputStream();
int len = -1;
// 循环读取客户端发送过来的数据
while ((len = inputStream.read(data)) != -1) {
System.out.println(new String(data, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("关闭与client 的连接");
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
使用telnet 127.0.0.1 6666启动客户端测试:
服务端接收的数据:
BIO问题分析:
1)每个请求都需要创建一个独立线程,与对应的客户端进行数据Read、业务处理、数据Write
2)当并发较大时,就需要创建大量的线程去处理,系统资源使用较大
3)连接建立后,如果当前线程暂时没有数据Read,则线程就会阻塞在Read操作上,造成资源浪费
2.3 Java NIO
2.3.1 NIO基本介绍
全称java non-blocking IO,相关类都在java.nio包及子包下。
NIO有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)
NIO是面向缓冲区(或者面向块)编程的。将数据读取到一个要稍后处理的缓冲区,需要时可在缓存区中前后移动,这样增加了处理过程中的灵活性,使其可以提供非阻塞式的高伸缩性网络。
NIO,使一个线程从某通道发送请求或读取数据,但是它仅能得到目前可用的数据,如果目前没有数据就什么也不会获取,二不是保持线程阻塞,所以直至数据变得可以读取之前,该线程都可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不要等他完全写入,这个线程同时可以去做其他事情。
2.3.2 NIO的Buffer基本使用
public class BasicBuffer {
public static void main(String[] args) {
// 举例说明Buffer的使用(简单使用)
// 创建一个Buffer
// 创建一个Buffer,大小为5,可以存5个int
IntBuffer intBuffer = IntBuffer.allocate(5);
// 向buffer中存放数据
for (int i = 0; i < intBuffer.capacity(); i++) {
intBuffer.put(i * 2);
}
// 从buffer中读取数据
// 将buffer转换,读写切换
intBuffer.flip();
while (intBuffer.hasRemaining()) {
System.out.println(intBuffer.get());
}
}
}
2.3.3 NIO与BIO的比较
1)BIO以流的方式处理数据,NIO以块的方式,块I/O的效率比流I/O的高很多
2)BIO是阻塞的,NIO是非阻塞的
3)BIO基于字节流和字符流进行操作,而NIO是基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因而可使用单个线程就可以监听多个客户单通道。
2.3.4 Buffer的机制与子类
Buffer(缓冲区):本质上是一个可以读写数据的内存块,可以理解成是一个容器对象,该对象提供了一系列方法,可以更轻松的使用内存块,Buffer对象内置了一些机制,可以跟踪和记录缓冲区的状态变化情况。Channel提供从文件、网络读取数据的渠道,但是读取或写入数据都必须经由Buffer。
1)常用子类
在NIO中,Buffer是一个顶层父类,它的子类有:ByteBuffer、ShortBuffer、CharBuffer、IntBuffer、LongBuffer、DoubleBuffer、FloatBuffer。其中使用的最多的是ByteBuffer
Buffer类定义了所有缓冲区都具有的4个属性来提供关于其所包含的数据元素信息:
Capacity:容量,可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变。
Limit:表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的
Position:位置,下一个要被读写的元素的索引,每次读写缓冲区数据都会改变该值,为下一次读写操作做准备
Mark:标记
Invariants:mark <= position <= limit <= capacity
2)常用方法
2.3.4 Channel
(1)基本介绍
1)NIO的通道类似于流,但有一些区别如下:
- 通道可以同时进行读写,而流只能读或写
- 通道可以实现异步读写数据
- 通道可以从缓冲读取数据,也可以写数据到缓冲(Channel <==> Buffer)
2)BIO中的stream是单向的,如FileInputStream只能读取数据,而NIO中的通道是双向的
3)Channel在NIO中是一个接口:
- public interface CHannel extends Closeable {}
4)常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel(类似ServerSocket)和SocketChannel(类似Socket)
5)常用Channel类说明:
- FileChannel:用于文件的数据读写
- DatagramChannel:用于UDP的数据读写
- ServerSocketChannel、SocketChannel:用于TCP数据读写
(2)FIleChannel 常用方法说明
FIleChannel 主要用来对本地文件进行IO操作,常见方法有:
1)public int read(ByteBuffer dst),从通道读取数据并放到缓冲区
2)public int write(ByteBuffer src),把缓冲区的数据写到通道中
3)public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道
4)public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道
(3)应用实例
将数据通过Channel写入到本地文件:
public class NIOFileChannel01 {
public static void main(String[] args) throws Exception {
String str = "hello world!";
// 创建一个输出流
FileOutputStream fileOutputStream = new FileOutputStream("G:\\01.txt");
// 通过输出流获取对应的FileChannel
FileChannel fileChannel = fileOutputStream.getChannel();
// 创建一个缓冲区 ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 将str放到ByteBuffer
byteBuffer.put(str.getBytes());
// 对ByteBuffer进行flip
byteBuffer.flip();
// 将数据从缓冲区写入到通道
fileChannel.write(byteBuffer);
fileOutputStream.close();
}
将01.txt文件读入到程序:
public class NIOFileChannel02 {
public static void main(String[] args) throws Exception {
// 创建文件输入流
File file = new File("G:\\01.txt");
FileInputStream fileInputStream = new FileInputStream(file);
// 通过输入流获取Channel
FileChannel fileChannel = fileInputStream.getChannel();
// 创建一个缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
// 将数据从通道读取到缓冲区
fileChannel.read(byteBuffer);
// 将byteBuffer字节转换为字符串
System.out.println(new String(byteBuffer.array()));
fileInputStream.close();
}
}
使用一个Buffer完成文件读取与写入
public static void main(String[] args) throws Exception {
// 通过输入流获取Channel
FileInputStream fileInputStream = new FileInputStream("1.txt");
FileChannel inputStreamChannel = fileInputStream.getChannel();
// 通过输出流获取Channel
FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
FileChannel outputStreamChannel = fileOutputStream.getChannel();
// 创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
// 循环读取
while (true) {
// buffer复位
byteBuffer.clear();
int read = inputStreamChannel.read(byteBuffer);
// 读取完毕
if (read == -1) {
break;
}
// 将buffer中的数据写入到Channel
byteBuffer.flip();
int write = outputStreamChannel.write(byteBuffer);
}
// 关闭流
outputStreamChannel.close();
inputStreamChannel.close();
}
使用transferFrom完成文件拷贝:
public class NIOFileChannel04 {
public static void main(String[] args) throws Exception {
// 创建相关流
FileInputStream fileInputStream = new FileInputStream("0.jpg");
FileOutputStream fileOutputStream = new FileOutputStream("0_bak.jpg");
// 通过流获取相关Channel
FileChannel inputStreamChannel = fileInputStream.getChannel();
FileChannel outputStreamChannel = fileOutputStream.getChannel();
// 使用transferForm完成拷贝
outputStreamChannel.transferFrom(inputStreamChannel, 0, inputStreamChannel.size());
// 关闭
outputStreamChannel.close();
inputStreamChannel.close();
}
(4)关于Buffer和Channel的注意事项和细节
1)ByteBuffer支持类型化的put和get,put放入的是什么数据类型,get就应该使用响应的数据类型取出来,否则可能有BufferUnderflowException异常
2)可以将一个普通Buffer转换只读Buffer
buffer.asReadOnlyBuffer();
3)NIO还提供了MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件由NIO来完成。
3)上诉例子都是通过一个Buffer完成的,NIO还支持通过多个Buffer(Buffer数组)完成读写操作,即Scattering和Gatering
MappedByteBuffer说明:
/**
* MappedByteBuffer(堆外内存)修改,操作系统不需要拷贝一次
* 1、可让文件直接在内存修改
*/
public class MappedByteBufferTest {
public static void main(String[] args) throws Exception {
RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
// 获取对应通道
FileChannel channel = randomAccessFile.getChannel();
/**
* 参数1:FileChannel.MapMode.READ_WRITE 使用读写模式
* 参数2:0 可以直接修改的起始位置
* 参数3:5 是映射到内存的大小,即将1.txt的多少个字节映射到内存
* 可以直接修改的范围就是[0, 5)
*/
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
mappedByteBuffer.put(0, (byte) 'H');
mappedByteBuffer.put(3, (byte) '9');
randomAccessFile.close();
}
}
Buffer的分散与聚集:
/**
* Scattering:将数据写入到Buffer时,可以采用Buffer数组,依次写入
* Gathering:从Buffer读取数据时,可以采用Buffer数组,依次读取
*/
public class ScatteringAndGathering {
public static void main(String[] args) throws IOException {
// 使用ServerSocketChannel 和 SocketChannel 网络
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(7777);
// 绑定端口到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 msgLen = 8; // 假设从客户端接收8个字节
// 循环读取
while (true) {
int byteRead = 0;
while (byteRead < msgLen) {
long read = socketChannel.read(byteBuffers);
byteRead += read; // 累计读取的字节数
System.out.println("byteRead = " + byteRead);
// 使用流打印,查看当前buffer的position和limit
Arrays.asList(byteBuffers).stream().map(buffer -> "position = " + buffer.position() + "\tlimit = " + buffer.limit()).forEach(System.out::println);
}
// 将所有的buffer进行flip
Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());
// 将数据读出并显示到客户端
long byteWrite = 0;
while (byteWrite < msgLen) {
long l =socketChannel.write(byteBuffers);
byteWrite += l;
}
// 将所有的buffer进行clear
Arrays.asList(byteBuffers).forEach(buffer -> buffer.clear());
System.out.println("byteRead = " + byteRead + "\tbyteWrite = " + byteWrite + "\tmsgLen = " + msgLen);
}
}
}
2.3.5 Selector(选择器)
(1)基本介绍:
1)Java的NIO用非阻塞的IO方式。可用一个线程,处理多个的客户端连接,这时就会用到Selector
2)Selector能够检测多个注册的通道上是否有事件发生(多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以以一个单线程去管理多个通道,也就是管理多个连接和请求。
3)只有在连接真正有读写事件发生时,才会进行读写,降低了系统开销,且不必为每个连接都创建一个线程更不用去维护多个线程
4)避免了多线程之间的上下文切换导致的开销
(2)特点说明:
1)Netty的IO线程NioEventLoop聚合了Selector,可以同时并发处理多个客户端连接。
2)当线程从某个客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
3)线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。
4)由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致的线程挂起。
5)一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一请求一线程的模式,架构的性能、弹性伸缩能力、可靠性都得到了极大提升。
(3)Selector的API
Selector是一个抽象类,常用方法:
public abstract class Selector implements Closeable {
public static Selector open(); // 得到一个选择器对象
public int select(long timeout); // 监控所有注册的通道,当其中有IO操作可以进行时,将对应的SelectionKey加入到内部集合中并返回,参数用来设置超时时间
public Set<SelectionKey> selectedKeys(); // 从内部集合中得到所有的SelectionKey
}
(4)NIO非阻塞网络编程原理分析图
1)当客户端连接时,会通过ServerSocketChannel得到SocketChannel
2)将SocketChannel注册到Selector上【register(Selector sel, int ops)】,一个Selector上可注册多个SocketChannel
3)注册后返回一个SelectionKey,并与该Selector关联
4)Selector进行监听select方法,返回有事件发生的通道的个数
5)进一步得到各个SelectionKey(有事件发生的)
6)再通过SelectionKey反向获取SocketChannel
7)通过得到的Channel完成业务逻辑
(5)举例说明
实现服务端与客户端之间的数据简单通讯(非阻塞),客户端通过控制台输入数据,服务端打印数据。
服务端:
public class NIOServer {
public static void main(String[] args) throws Exception {
// 创建ServerSocketChannel,并通过创建ServerSocketChannel获取ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 得到一个Selector对象
Selector selector = Selector.open();
// 绑定一个端口,在服务端监听
serverSocketChannel.socket().bind(new InetSocketAddress(7777));
// 设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 把ServerSocketChannel注册到selector关心事件为OP_ACCEPT (连接事件)
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 循环等待客户端连接
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();
// 遍历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();
socketChannel.configureBlocking(false);
// 将socketChannel注册到selector,关注事件为OP_READ,同事给SocketChannel关联一个Buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
// 如果是OP_READ,
if (key.isReadable()) {
// 通过key反向获取对应的channel
SocketChannel channel = (SocketChannel) key.channel();
// 获取到该channel关联的buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear();
while (channel.read(buffer) > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
System.out.println("from client : " + new String(buffer.array()));
buffer.clear();
}
}
// 手动从集合中移除当前的SelectionKey,防止重复操作
keyIterator.remove();
}
}
}
}
客户端:
public class NIOClient {
public static void main(String[] args) throws Exception {
// 得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
// 设置非阻塞
socketChannel.configureBlocking(false);
// 提供服务端的ip和端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 7777);
// 连接服务器
if (!socketChannel.connect(inetSocketAddress)) {
while (!socketChannel.finishConnect()) {
System.out.println("连接需要时间,客户端不会注册,可以做其它工作");
}
}
// 连接成功,发送数据
// String str = "Hello, World!";
// ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
Scanner scanner = new Scanner(System.in);
while (true) {
String str = scanner.nextLine();
// 发送数据,将buffer中的数据写入到channel中
socketChannel.write(ByteBuffer.wrap(str.getBytes()));
//设置标识符退出客户端
if ("quit".equals(str)) {
System.out.println("退出聊天...");
break;
}
}
}
}
2.3.6 群聊系统
服务端:
步骤:
1)服务器启动并监听7777
2)服务器接收客户端信息,并实现转发(处理上线和离线)
public class GroupChatServer {
// 定义属性
private Selector selector;
private ServerSocketChannel listenChannel;
private static final int PORT = 7777;
// 构造器
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 socketChannel = listenChannel.accept();
// 设置socketChannel为非阻塞
socketChannel.configureBlocking(false);
// 将socketChannel注册到selector
socketChannel.register(selector, SelectionKey.OP_READ);
// 提示
System.out.println(socketChannel.getRemoteAddress() + " 上线");
}
// 通道发生read事件,即通道是可读状态
if (key.isReadable()) {
// TODO
readData(key);
}
// 删除当前的selectionkey
iterator.remove();
}
} else {
System.out.println("等待...");
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 读取客户端消息
private void readData(SelectionKey key) {
// 定义一个SocketChannel
SocketChannel channel = null;
try {
// 得到channel
channel = (SocketChannel) key.channel();
// 创建buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.clear();
int count = channel.read(buffer);
String msg = new String(buffer.array(), 0, count);
buffer.flip();
System.out.println("from " + channel.getRemoteAddress() + " : " + msg);
// 向其它客户端转发消息
sendINfoToOtherClients(msg, channel);
} catch (IOException e) {
try {
System.out.println(channel.getRemoteAddress() + " 离线了...");
// 取消注册
key.channel();
// 关闭通道
channel.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
// 转发消息给其它客户(通道)
private void sendINfoToOtherClients(String msg, SocketChannel self) throws IOException {
// System.out.println("服务器转发消息...");
// 遍历所有注册到selector上的SocketChannel,并排除自己(self)
for (SelectionKey key : selector.keys()) {
// 可能取出ServerSocketChannel,故获取类型为Channel,转发时排除掉ServerSocketChannel
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();
}
}
客户端:
步骤:
1)连接服务器
2)发送信息
3)接收服务器信息
public class GroupChatClient {
// 定义相关的属性
private final String HOST = "127.0.0.1"; // IP
private final int PORT = 7777;
private Selector selector;
private SocketChannel socketChannel;
private String username;
// 构造器
public GroupChatClient() {
try {
// 获取selector
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...");
} catch (IOException e) {
e.printStackTrace();
}
}
// 向服务器发送消息
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();
iterator.remove();
if (key.isReadable()) {
// 得到相关的通道
SocketChannel channel = (SocketChannel) key.channel();
// 得到一个Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.clear();
// 读取
int count = channel.read(buffer);
buffer.flip();
// 把读取从缓冲区读取的数据转成字符串
String msg = new String(buffer.array(),0, count);
System.out.println(msg);
}
}
}
} catch (IOException e) {
System.out.println("没有可用的通道");
}
}
public static void main(String[] args) {
// 启动客户端
GroupChatClient client = new GroupChatClient();
// 启动一个线程
new Thread() {
@Override
public void run() {
while (true) {
client.readInfo();
}
}
}.start();
// 发送数据给服务器端
Scanner scanner = new Scanner(System.in);
while (true) {
String msg = scanner.nextLine();
client.sendInfo(msg);
}
}
}
2.3.7 NIO与零拷贝
(1)基本介绍
1)零拷贝是网络编程的关键,很多性能优化都离不开
2)在Java中,常用的零拷贝有mmap(内存映射)和sendFile。
(2)mmap优化
mmap通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。
(3)sendFile优化
Linux2.1版本提供了sendFile函数,其基本原理:数据不经过用户态,直接从内核缓冲区进入到SocketBuffer,同时,由于和用户态完全无关,就减少了一次上下文切换。
(4) mmap和sendFile的区别
1)mmap适合小数据量读写,sendFile适合大文件传输
2)mmap需要4次上下文切换,3次数据拷贝;sendFile需要3次上下文切换,最少2两次数据拷贝
3)sendFile可以利用DMA(直接内存访问)方式,减少CPU拷贝,mmap则不能(必须从内核拷贝到Socket缓冲区)
(4)零拷贝传递文件
服务器端:
public class NewIOServer {
public static void main(String[] args) throws IOException {
InetSocketAddress address = new InetSocketAddress(7001);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ServerSocket serverSocket = serverSocketChannel.socket();
// 绑定端口
serverSocket.bind(address);
// 创建buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
SocketChannel socketChannel = serverSocketChannel.accept();
int readCount = 0;
while (-1 != readCount) {
readCount = socketChannel.read(buffer);
buffer.rewind();
}
}
}
}
服务器端:
public class NewIOClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 7001));
String filename = "G:\\学习\\jdk-8u162-linux-x64.tar.gz";
// 得到一个文件channel
FileChannel fileChannel = new FileInputStream(filename).getChannel();
// 准备发送文件
long startTime = System.currentTimeMillis();
// 在linux下transferTo可以完成传输
// 在windows下一次调用transferTO只能发送8m,需要分段传输文件,而且需要注意传输时的位置
int byteWritten = 0; // 已写入的大小
long byteCount = fileChannel.size();
while (byteWritten < byteCount) {
byteWritten += fileChannel.transferTo(byteWritten, byteCount - byteWritten, socketChannel);
}
System.out.println("发送的总的字节数:" + byteWritten + " \t耗时" + (System.currentTimeMillis() - startTime));
}
}