Netty
基本介绍
细节描述
1)Java NIO全称java non-blocking IO ,是指JDK提供的新的API,从JDK1.4开始,Java提供了一系列改进输入/输出的新特性,被统称为NIO,同步非阻塞
2)NIO相关的类都放在java.nio包及子包下,并且对原java.io包中很多类进行改写
3)NIO有三大核心部分: Channel(通道),Buffer(缓冲区),Selector(选择器)
4)NIO是面向缓冲区,或者面向块编程。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程的灵活性。
5)Java NIO的非阻塞模式,使一个线程从某通道中发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有可用数据,就什么也不会获取,而不是保持线程阻塞在那里,所以直至数据变得可以读取之前,该线程可以继续做其他得事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但是不需要等待它完全写入,这个线程同时可以做别的事情。
6)NIO可以做到一个线程来处理多个操作,假设有100个请求过来,根据实际情况,可以分配5或者10个线程来处理。如果是BIO的话,那么就需要对每个请求建立一个线程。
7)HTTP2.0使用lO多路复用技术,做到同一个连接并发处理多个请求,并且并发请求的数量比HTTP1.1大了好几个数量级。
BIO和NIO比较
1)BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O效率比流IO高很多
2)BIO是阻塞的,NIO是非阻塞的
3)BIO是基于字节流和字符流进行操作,而NIO基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区,或者从缓冲区读取到通道。Selector用于监听多个通道的事件(比如连接请求,数据到达等),因此可以用单个线程就能监听多个客户端通道。
NIO核心原理
Selector ,Channel和Buffer的关系图
1)每个Channel都会对应一个Buffer
2)Selector对应一个线程,一个线程对应多个Channel(连接)
3)该图反应you三个Channel注册到Selector
4)程序切换到那个Channel是由事件决定的
5)Selector会根据不同的事件在各个通道进行切换
6)Buffer就是一个内存块,底层是一个数组
7)数据的读取和写入都是通过Buffer,可以读也可以写,需要flip方法切换,BIO要么是输入流要么是输出流
8)Channel是双向的,可以返回底层操作系统的情况,比如linux
缓冲区
基本介绍
缓冲区(Buffer): 缓冲区本质就是一个可以读写数据的内存块,可以简单的理解为一个容器对象,该对象提供了一组方法,可以更轻松的使用内存块,缓冲区对象内置一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供了从文件,网络读取数据通道,但是读取和写入数据必须经过Buffer
常用的buffer子类
1)ByteBuffer 存储字节数据到缓冲区
2)ShortBuffer 存储短整型数据数据到缓冲区
3)CharBuffer 存储字符数据到缓冲区
4)IntBuffer 存储整数数据到缓冲区
5)LongBuffer 存储长整型数据到缓冲区
6)DoubleBuffer 存储小数数据到缓冲区
7)FloatBuffer 存储小数数据到缓冲区
通道
基本介绍
1)NIO通道类似流,但是区别如下
- 通道可以同时进行读写,而流只能读或者写
- 通道可以实现异步读写数据
- 通道可以从缓冲区读数据,也可以写数据到缓冲区
2)Channel在NIO是一个接口
public interface Channel extends Closeable {
3)常见的channel : FileChannel(用于文件读写),DatagramChannel(用于UDP数据读写),SeverSocketChannel和SocketChannel(用于TCP的数据读写)
关于buffer和channel注意细节
1)ByteBuffer支持类型化的put和get,put放入什么类型,get就获取什么类型,否则就可能有BufferUnderflowException
2)可以将普通的Buffer转换为只读Buffer
3)NIO还支持通过多个Buffer(Buffer数组来完成)即Scattering和Gathering
4)NIO提供了MappedByteBuffer可以让文件直接在内存中进行修改
Selector
基本介绍
1)java的NIO,用非阻塞IO的方式 .可以用一个线程,处理多个客户端连接,就会使用到Selector(选择器)
2)Selector能够检测多个注册通道是否有事件发送,如果有事件发生,便获取事件,然后对每个事件进行相应的处理,这样一个线程就可以去管理多个通道,也就是管理多个连接和请求
3)避免了多线程的上下问切换导致的开销
特点说明
1)Netty的IO线程NioEventLoop聚合了Selector(选择器也叫多路复用器),可以同时并发处理成百上千个客户端连接。
2)当线程从客户端Socket通道进行读写数据的时,若没有数据可以用,该线程可以进行其他的任务。
3)线程通常以非阻塞IO的空闲时间用于在其他通道的IO操作,所以单独线程可以管理多个输入和输出通道
4)由于读写操作是非阻塞的,充分提高了IO线程的运行效率,避免由于IO阻塞导致线程挂起
注意事项
1)NIO中的ServerSocketChannel功能类似ServerSocket,SocketChannel类似Socket
说明:
1,当客户端进行连接的时候,会通过ServerSocketChannel得到SocketChannel
2,Selector进行监听selector方法,返回有事件发生的通道个数
3,将socketChannel注册到Selector上,register(Selector sel,int ops),一个selector上可以注册多个SocketChannel.
4,注册返回一个SelectionKey,会和该Selector关联
5,进一步得到各个SelectionKey(有事件发生)
6,通过SelectionKey反向获取SocketChannel,方法Channel
7,可以通过得到的channel进行业务处理
8,业务处理
SelectionKey
SeverSockerChannel
SocketChannel
应用实例
应用实例1-本地文件写数据
实例要求:
1)使用前面学习后的ByteBuffer(缓冲) 和 FileChannel(通道), 将 “hello,world!” 写入到file.txt 中
2)文件不存在就创建
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileOper01 {
public static void main(String[] args) throws IOException {
String str = "hello,world!";
//创建输出流
FileOutputStream outputStream = new FileOutputStream("file.text");
//获取通道
FileChannel channel = outputStream.getChannel();
//获取缓冲区对象
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//加载数据到缓冲区内存中
byteBuffer.put(str.getBytes("UTF-8"));
//缓冲区翻转
byteBuffer.flip();
//把缓冲区的数据加载到通道中
channel.write(byteBuffer);
//关闭通道和流
channel.close();
outputStream.close();
}
}
应用实例2-本地文件读数据
实例要求:
- 使用前面学习后的ByteBuffer(缓冲) 和 FileChannel(通道), 将 file.txt 中的数据读入到程序,并显示在控制台屏幕
- 假定文件已经存在
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileOper02 {
public static void main(String[] args) throws IOException {
//创建输出流
FileInputStream fileInputStream = new FileInputStream("file.text");
//获取通道
FileChannel channel = fileInputStream.getChannel();
//获取缓冲区对象
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read = channel.read(byteBuffer);
System.out.println("读取的字节数" + read);
System.out.println(new String(byteBuffer.array()));
//关闭通道和流
channel.close();
fileInputStream.close();
}
}
应用实例3-使用一个Buffer完成文件读取
实例要求:
- 使用 FileChannel(通道) 和 方法 read , write,完成文件的拷贝
- 拷贝一个文本文件 1.txt , 放在项目下即可
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* 实例要求:
* 使用 FileChannel(通道) 和 方法 read , write,完成文件的拷贝
*
* 拷贝一个文本文件 1.txt , 放在项目下即可
* 代码演示
*/
public class NIOFileOper03 {
public static void main(String[] args) throws IOException {
//创建输出流
FileInputStream fileInputStream = new FileInputStream("file.text");
//获取通道
FileChannel channel = fileInputStream.getChannel();
//获取缓冲区对象
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
channel.read(byteBuffer);
System.out.println(new String(byteBuffer.array()));
//缓冲区翻转
byteBuffer.flip();
FileOutputStream fileOutputStream = new FileOutputStream("fileCopy.text");
FileChannel outputChanel = fileOutputStream.getChannel();
outputChanel.write(byteBuffer);
//关闭通道和流
channel.close();
outputChanel.close();
fileInputStream.close();
fileOutputStream.close();
}
}
应用实例4-拷贝文件transferFrom 方法
实例要求:
1)使用 FileChannel(通道) 和 方法 transferFrom ,完成文件的拷贝
2)拷贝一张图片
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
public class NIOFileOper04 {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("01.jpg");
FileOutputStream fos = new FileOutputStream("01cp.jpg");
FileChannel fosChannel = fos.getChannel();
FileChannel fisChannel = fis.getChannel();
System.out.println("此通道文件的大小"+fisChannel.size());
//从目标通道拷贝数据到当前通道
fosChannel.transferFrom(fisChannel, 0, fisChannel.size());
fos.close();
fis.close();
fisChannel.close();
fosChannel.close();
System.out.println("图片拷贝完成。。。");
}
}
应用实例5-NIO 非阻塞 网络编程快速入门
案例要求:
编写一个 NIO 入门案例,实现服务器端和客户端之间的数据简单通讯(非阻塞)
目的:理解NIO非阻塞网络编程机制
服务端
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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class NIOServer1 {
public static void main(String[] args) throws IOException {
//1 得到一个serverSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2 绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(8888));
//3 配置非阻塞
serverSocketChannel.configureBlocking(false);
//4 得到一个Selector对象
Selector selector = Selector.open();
//5 向Selector注册ServerSocketChannel
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//6 开始工作
while (true) {
//6.1 监听客户端
if (selector.select(1000) == 0) {
System.out.println("等待1s,无客户端连接");
continue;
}
//6.2 判断通道事件
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
//遍历通道
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
//客户端连接请求事件
//接受通道
SocketChannel socketChannel = serverSocketChannel.accept();
//配置非阻塞
socketChannel.configureBlocking(false);
//注册到selector
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("有client连接请求过来");
}
if (key.isReadable()) {
//读取客户端事件
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
socketChannel.read(buffer);
System.out.println("接受客户端数据" + new String(buffer.array()));
}
// 6.3 手动从集合中移除当前key,防止重复处理
keyIterator.remove();
}
}
}
}
客户端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOClient1 {
public static void main(String[] args) throws IOException {
//1 得到一个Socker连接
SocketChannel socketChannel = SocketChannel.open();
//2 配置非阻塞
socketChannel.configureBlocking(false);
//3 绑定端口ip
InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 8888);
//4连接
if (!socketChannel.connect(socketAddress)) {
while (!socketChannel.finishConnect()) {
//nio非阻塞式
System.out.println("客户端: 因为连接需要时间,客户端不会阻塞,可以做个计算工作...");
}
}
//写入数据
ByteBuffer byteBuffer = ByteBuffer.wrap("Hello Netty!!!".getBytes());
socketChannel.write(byteBuffer);
System.in.read();
System.out.println("客户端完成工作");
}
}
应用实例6 NIO 网络编程应用实例-群聊系统
实例要求:
1)编写一个 NIO 群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
2)实现多人群聊
3)服务器端:可以监测用户上线,离线,并实现消息转发功能
4)客户端:通过channel 可以无阻塞发送消息给其它所有用户,同时可以接受其它用户发送的消息(有服务器转发得到)
import com.atguigu.netty.qq.ChatClient;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;
public class GroupChatClient {
private final String HOST = "127.0.0.1"; //服务器地址
private int PORT = 6667; //服务器端口
private Selector selector;
private SocketChannel socketChannel;
private String userName;
public GroupChatClient() throws IOException {
//得到选择器
selector = Selector.open();
//连接远程服务器
socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", PORT));
//设置非阻塞
socketChannel.configureBlocking(false);
//注册选择器并设置为 read
socketChannel.register(selector, SelectionKey.OP_READ);
//得到客户端 IP 地址和端口信息,作为聊天用户名使用
userName = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(userName + " is ok ~");
}
//向服务器端发送数据
public void sendInfo(String info) throws Exception {
//如果控制台输入 exit 就关闭通道,结束聊天
if (info.equalsIgnoreCase("exit")) {
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
socketChannel.close();
socketChannel = null;
return;
}
info = userName + " 说: " + info;
try {
//往通道中写数据
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
//从服务器端接收数据
public void readInfo() {
try {
int readyChannels = selector.select();
if (readyChannels > 0) { //有可用通道
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey sk = (SelectionKey) keyIterator.next();
if (sk.isReadable()) {
//得到关联的通道
SocketChannel sc = (SocketChannel) sk.channel();
//得到一个缓冲区
ByteBuffer buff = ByteBuffer.allocate(1024);
//读取数据并存储到缓冲区
sc.read(buff);
//把缓冲区数据转换成字符串
String msg = new String(buff.array());
System.out.println(msg.trim());
}
keyIterator.remove(); //删除当前 SelectionKey,防止重复处理
}
} else {
//会检测到没有可用的channel ,可以退出
System.out.println("没有可用channel ...");
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
//创建一个聊天客户端对象
GroupChatClient chatClient = new GroupChatClient();
new Thread() { //单独开一个线程不断的接收服务器端广播的数据
public void run() {
while (true) {
chatClient.readInfo();
try { //间隔 3 秒
Thread.currentThread().sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
Scanner scanner = new Scanner(System.in);
//在控制台输入数据并发送到服务器端
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
chatClient.sendInfo(msg.trim());
}
}
}
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.text.SimpleDateFormat;
import java.util.Iterator;
public class GroupChatServer {
private Selector selector;
private ServerSocketChannel listenerChannel;
private static final int PORT = 6667; //服务器端口
public GroupChatServer() {
try {
// 得到选择器
selector = Selector.open();
// 打开监听通道
listenerChannel = ServerSocketChannel.open();
// 绑定端口
listenerChannel.socket().bind(new InetSocketAddress(PORT));
// 设置为非阻塞模式
listenerChannel.configureBlocking(false);
// 将选择器绑定到监听通道并监听 accept 事件
listenerChannel.register(selector, SelectionKey.OP_ACCEPT);
printInfo("服务器 ok.......");
} catch (IOException e) {
e.printStackTrace();
}
}
public void listen() {
try {
while (true) { //不停轮询
int count = selector.select();//获取就绪 channel
if (count > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 监听到 accept
if (key.isAcceptable()) {
SocketChannel sc = listenerChannel.accept();
//非阻塞模式
sc.configureBlocking(false);
//注册到选择器上并监听 read
sc.register(selector, SelectionKey.OP_READ);
//System.out.println(sc.getRemoteAddress().toString().substring(1) + "online ...");
System.out.println(sc.socket().getRemoteSocketAddress().toString().substring(1) + " 上线 ...");
//将此对应的 channel 设置为 accept,接着准备接受其他客户端请求
key.interestOps(SelectionKey.OP_ACCEPT);
}
//监听到 read
if (key.isReadable()) {
readData(key); //读取客户端发来的数据
}
//一定要把当前 key 删掉,防止重复处理
iterator.remove();
}
} else {
System.out.println("waitting ...");
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void readData(SelectionKey key) {
SocketChannel channel = null;
try {
// 得到关联的通道
channel = (SocketChannel) key.channel();
//设置 buffer 缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//从通道中读取数据并存储到缓冲区中
int count = channel.read(buffer);
//如果读取到了数据
if (count > 0) {
//把缓冲区数据转换为字符串
String msg = new String(buffer.array());
printInfo(msg);
//将关联的 channel 设置为 read,继续准备接受数据
key.interestOps(SelectionKey.OP_READ);
sendInfoToOtherClients(channel, msg); //向所有客户端广播数据
}
buffer.clear();
} catch (IOException e) {
try {
//当客户端关闭 channel 时,进行异常如理
//printInfo(channel.getRemoteAddress().toString().substring(1) + "offline...");
printInfo(channel.socket().getRemoteSocketAddress().toString().substring(1) + " 离线了 ...");
key.cancel(); //取消注册
channel.close(); //关闭通道
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
public void sendInfoToOtherClients(SocketChannel except, String msg) throws IOException {
System.out.println("服务器进行消息转发 ...");
//转发数据到所有的 SocketChannel 中
for (SelectionKey key : selector.keys()) {
Channel targetchannel = key.channel();
//排除自身
if (targetchannel instanceof SocketChannel && targetchannel != except) {
SocketChannel dest = (SocketChannel) targetchannel;
//把数据存储到缓冲区中
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
//往通道中写数据
dest.write(buffer);
}
}
}
private void printInfo(String str) { //显示消息
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("服务器接收到消息 时间: [" + sdf.format(new java.util.Date()) + "] -> " + str);
}
public static void main(String[] args) {
GroupChatServer server = new GroupChatServer();
server.listen();
}
}