Netty学习–基本介绍和IO模型
文章目录
介绍和应用场景
介绍:
- netty是由JBOSS提供的一个Java开源框架,现为github上的独立项目。
- netty是一个异步的,基于事件驱动的网络应用框架,用于快速开发高性能,高可靠的网络IO程序
- netty主要针对TCP协议下,面向Client端的高并发应用,或者peer-to-peer场景下的大量数据持续传输的应用
- netty本质是一个NIO框架,使用于服务器通讯相关的多种应用场景
应用场景:
- 互联网行业,在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,netty作为异步高性能的通信框架,往往作为基础通信组件被这些RPC框架使用
- 游戏行业
- 大数据领域
I/O模型
- Java BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
- 适用场景:连接数小且固定的架构,这种方式对服务器资源的要求较高,并发局限于应用中
- Java NIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理
- 适用场景:连接数多且连接比较短的架构,比如聊天服务器,弹服务器
- Java AIO:异步非阻塞,AIO引进异步通道的概念,采用proactor模式,简化了程序编写,有效的请求才会启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
- 适用场景:连接数多且连接比较长的架构,比如相册服务器
BIO模型示例
package com.fish;
import java.io.IOException;
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 IOException {
//创建线程池
ExecutorService threadPool = Executors.newCachedThreadPool();
//创建服务端
ServerSocket serverSocket = new ServerSocket(8888);
//启动服务端
while (true) {
//等待客户端连接
System.out.println("正在等待客户端连接...");
final Socket socket = serverSocket.accept();
System.out.println("客户端已经连接...");
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("|启动的线程id==>" + Thread.currentThread().getId());
try {
socketHandler(socket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
public static void socketHandler(Socket socket) throws IOException {
try {
byte[] bytes = new byte[1024];
InputStream inputStream = socket.getInputStream();
while (true){
int read = inputStream.read(bytes);
if (read!=-1){
System.out.println(new String(bytes, 0, read));
}
}
}catch (Exception e){
e.printStackTrace();
}finally {
socket.close();
}
}
}
测试:
命令行输入telnet ip port
,然后ctrl+]
发送数据send data
NIO模型
NIO是面向缓冲区或者块编程的,数据读取到一个它稍后处理的缓存区,需要时可在缓冲区中前后移动,这就是增加了处理过程的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
Java NIO的非阻塞模式,使得一个线程从某通道发送请求或者读取数据,但是他仅能得到目前可用的数据,如果目前没有可用的数据,就什么都不会做,而不是保持线程阻塞,所以直至数据变得可以被读取到之前,该线程可以继续做其他的事情
核心部件:
- 通道(channel)
- 缓冲区(buffer)
- 选择器(selector)
NIO和BIO的区别
- BIO以流的方式处理数据,而NIO以块的方式处理数据,块的效率比流的效率高很多
- BIO是阻塞的,NIO是非阻塞的
- BIO基于字节流和字符流进行操作,而NIO基于channel和buffer进行操作,数据总是从通道读取到缓冲区,或者从缓冲区中写入到通道中。selector用于监听多个通道的事件(比如,连接请求,数据到达等),因此使用单个线程就能监听到多个客户端通道
Buffer的使用
package com.fish.NIO;
import java.nio.IntBuffer;
public class BufferTest {
public static void main(String[] args) {
//创建buffer,容量为5,可以存放5个int类型的数据
IntBuffer intBuffer=IntBuffer.allocate(5);
//向buffer中存储数据
for (int i=0;i<intBuffer.capacity();i++){
intBuffer.put(i+1);
}
//切换读写状态
intBuffer.flip();
for (int i=0;i<intBuffer.capacity();i++){
System.out.println(intBuffer.get());
}
}
}
三大核心的关系
- 每个channel都会对应一个buffer
- 一个selector对应一个线程,一个线程对应多个channel(连接)
- 上图反应了三个channel注册到了selector
- 程序切换到哪个channel是由事件决定的
- selector会根据不同的事件,在各个通道上切换
- buffer就是一个内存块,底层是有一个数组的
- 数据的读取写入是通过buffer,是可读可写的,但是需要切换
- channel是双向的,可以返回底层操作系统的情况。
缓冲区(Buffer)
缓冲区本质上是一个可以读写数据的内存块,可以理解为一个容器对象(含数组),该对象提供了一组方法,可以更加轻松的使用内存块。缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。channel提供从文件,网络读取数据的渠道,但是读取或写入的数据都必须经由buffer
buffer的四个属性
属性 | 描述 |
---|---|
capacity | 容量,即可以容纳的最大数量,在缓冲区创建时被设定,不可改变 |
limit | 表示缓冲区当前的终点,不能对缓冲区超过极限的位置进行读写操作,且极限是可以修改的 |
position | 位置,下一个要被读或写的元素的索引,每次读写缓冲区时数据都会改变值,为下次读写做准备 |
mark | 标记 |
注意事项:
-
ByteBuffer支持类型化的put和get,put放入的什么类型,get就应该使用相应的数据类型来取出,否则可能有异常
-
可以将一个普通buffer转化为只读buffer
- 使用asReadOnlyBuffe方法转化
-
MapperByteBuffer:可以让文件直接在内存中修改,操作系统不需要再拷贝一次
-
public class MapperBufferTest { public static void main(String[] args) throws IOException { //创建随机访问文件流,以读取和(可选)写入具有指定名称的文件。 //将创建一个新的FileDescriptor对象来表示与该文件的连接。 RandomAccessFile randomAccessFile = new RandomAccessFile("d://1.tx5t","rw"); FileChannel channel = randomAccessFile.getChannel(); /* *参数1:使用的模式 *参数2:可以直接修改的起始位置 *参数3:映射到内存的大小,就是可以直接修改的范围是0~5 */ MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5); map.put(0,(byte)'H'); randomAccessFile.close(); } }
-
-
NIO还支持通过多个Buffer(即buffer数组)完成读写操作,即Scattering和Gathering
-
Scattering:将数据写入到buffer时,可以采用buffer数组,依次写入(分散)
-
Gathering:从buffer读取数据时,可以采用buffer数组以此读取(聚集)
-
public class ScatteringAndGatheringTest { public static void main(String[] args) throws IOException { //创建一个serverSocketChannel ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //创建端口 InetSocketAddress inetSocketAddress = new InetSocketAddress(8888); //绑定端口到serverSocketChannel serverSocketChannel.bind(inetSocketAddress); //创建buffer数组 ByteBuffer[] byteBuffers=new ByteBuffer[2]; byteBuffers[0]=ByteBuffer.allocate(5); byteBuffers[1]=ByteBuffer.allocate(3); //等待客户端连接 SocketChannel socketChannel = serverSocketChannel.accept(); //最大数据读取长度 int messageLength=8; //循环读取数据 while (true){ int read=0; while (read<messageLength){ //将数据从通道读取到缓冲区中 long l = socketChannel.read(byteBuffers); read+=l; //打印当前已经读取数据的长度 System.out.println("read already==>" + read); //查看当前buffer的position和limit Arrays.asList(byteBuffers).stream().map(buffer -> "position==>"+buffer.position()+",limit==>"+buffer.limit()).forEach(System.out::println); } //将bytebuffer数组中的buffer状态进行读写切换 Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip()); //从缓冲区读取数据 int write=0; while (write<messageLength){ long l = socketChannel.write(byteBuffers); write+=l; } //清理buffer Arrays.asList(byteBuffers).forEach(buffer ->{buffer.clear();}); System.out.println("read==>" + read + "====write==>" + write); } } }
-
通道(Channel):
NIO的通道类似流,但有些区别
- 通道可以同时读写,而流只能读或者写
- 通道可以实现异步读写数据
- 通道可以从缓冲读数据,也可以写数据到缓冲
常用的channel类有:
- FileChannel:用于文件的读写
- DatagramChannel:用于UDP的数据读写
- ServerSocketChannel
- SocketChannel
常用方法:
- read(ByteBuffer dst):从通道读取数据到缓冲区
- writer(ByteBuffer src):从缓冲区读取数据到通道
示例:
public class NIOFileChannel {
public static void main(String[] args) throws IOException {
String str="hello,fish";
//创建输出流,
FileOutputStream outputStream = new FileOutputStream("d://1.txt");
//通过输出流获取fileChannel
FileChannel channel = outputStream.getChannel();
//创建一个byteBuffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
//将数据写入到buffer中
buffer.put(str.getBytes());
//切换buffer读写状态
buffer.flip();
//通道从buffer中读取数据
channel.write(buffer);
//关闭输出流
outputStream.close();
}
}
示例图:
**示例2:**拷贝文件
public class NIOFileChannel02 {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("d://1.txt");
FileOutputStream fileOutputStream = new FileOutputStream("d://2.txt");
//创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(512);
FileChannel inputStreamChannel = fileInputStream.getChannel();
FileChannel outputStreamChannel = fileOutputStream.getChannel();
while (true){
//clear是必须的,清空buffer
buffer.clear();
int read = inputStreamChannel.read(buffer);
if (read==-1){
break;
}
//读写反转
buffer.flip();
outputStreamChannel.write(buffer);
}
fileOutputStream.close();
fileInputStream.close();
}
}
选择器(Selector):
selector能够检测多个注册的通道上是否有事件发生(注意,多个channel以事件的方式可以注册到同一个selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
只有在连接真正有读写事件发生时,才会进行读写,就大大的减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
避免了多线程之间的上下文切换导致的开销。
示例:
服务端:
public class NIOServer {
public static void main(String[] args) throws IOException {
//创建serverSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//创建selector
Selector selector = Selector.open();
//绑定端口
serverSocketChannel.bind(new InetSocketAddress(8888));
//设置非阻塞模式
serverSocketChannel.configureBlocking(false);
//serverSocketChannel注册到selector,并设置关注事件为 连接事件(SelectionKey.OP_ACCEPT)
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环等待客户端连接
while (true){
//等待一秒,如果没有事件发生
if (selector.select(1000)==0){
System.out.println("没有客户端连接到服务器");
}
//如果有事件对其进行响应,获取到相关的selectionKey集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
//获取到selectorKey
SelectionKey selectionKey = iterator.next();
//根据key找到对应的通道进行处理
if (selectionKey.isAcceptable()){
//发生的事件为连接事件
SocketChannel socketChannel = serverSocketChannel.accept();
//将连接到服务器端的客户端设置为非阻塞
socketChannel.configureBlocking(false);
//打印
System.out.println("已经有一个客户端连接到服务端" + selectionKey.hashCode());
//将socketChannel注册到selector
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (selectionKey.isReadable()){
//发生的事件为读取事件
SocketChannel channel = (SocketChannel) selectionKey.channel();
//获取到和SocketChannel关联的buffer
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
//从客户端读取数据到缓冲
channel.read(buffer);
//打印数据
System.out.println(new String(buffer.array()));
}
//手动从集合中移除当前的selectionKey,防止重复操作
iterator.remove();
}
}
}
}
客户端:
public class NIOClient {
public static void main(String[] args) throws IOException {
//得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
//设置非阻塞
socketChannel.configureBlocking(false);
//提供服务端ip和端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8888);
//连接服务器
if (!socketChannel.connect(inetSocketAddress)){
//如果没有成功连接到服务器,客户端不会阻塞,可以进行其他的工作
while (!socketChannel.finishConnect()){
System.out.println("客户端未阻塞");
}
}
//成功连接到服务器后
String str="hello";
//wrap方法,将字符串包装到buffer
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
//将数据传输到服务去端
socketChannel.write(buffer);
//将客户端保持运行
System.in.read();
}
}
SelectorKey,表示selector和网络通道注册的关系
-
OP_ACCEPT = 1 << 4 //新的网络连接可以accept OP_CONNECT = 1 << 3 //连接已经建立 OP_WRITE = 1 << 2 //写操作 OP_READ = 1 << 0 //读操作
-
channel():获取与之关联的通道
-
attachmen():获取与之关联的共享数据
-
seletor():获取与之关联的选择器
Selector API中keys()和selectedKey()的区别
- keys:代表当前注册到seletor中的通道
- selectedKeys:代表当前响应事件的通道
群聊示例:
服务端:
public class GroupChatServer {
private Selector selector;
private ServerSocketChannel serverSocketChannel;
private static final int PORT=8888;
public GroupChatServer(){
try {
selector=Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void listen(){
//循环监听事件
try {
while (true){
int count = selector.select(200);
if (count>0){
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()){
SelectionKey key = keyIterator.next();
if (key.isAcceptable()){
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
//提示上线
System.out.println(socketChannel.getRemoteAddress() + ": already online");
}
if (key.isReadable()){
readData(key);
}
keyIterator.remove();
}
}
}
}catch (Exception e){
e.printStackTrace();
}
}
public void readData(SelectionKey selectionKey){
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
int read = socketChannel.read(buffer);
if (read>0){
String msg = new String(buffer.array(), 0, read);
System.out.println("读取到的数据为: "+msg);
sendInfoToOthers(msg,socketChannel);
}
} catch (IOException e) {
try {
System.out.println(socketChannel.getRemoteAddress() + " already outline");
selectionKey.cancel();
socketChannel.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
public void sendInfoToOthers(String msg,SocketChannel self) throws IOException {
for (SelectionKey key:selector.keys()){
Channel channel=key.channel();
if (channel instanceof SocketChannel && channel!=self){
SocketChannel dest=(SocketChannel) channel;
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
dest.write(buffer);
}
}
}
public static void main(String[] args) {
GroupChatServer groupChatServer = new GroupChatServer();
groupChatServer.listen();
}
}
客户端:
public class GroupChatClient {
private static final String HOST="127.0.0.1";
private static final int PORT=8888;
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);
socketChannel.register(selector, SelectionKey.OP_READ);
userName=socketChannel.getLocalAddress().toString().substring(1);
System.out.println(userName + " is ok ...");
}
public void sendInfo(String info){
info=userName+" say :" +info;
try {
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
public void readInfo(){
try {
int select = selector.select();
if (select>0){
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isReadable()){
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer);
String msg = new String(buffer.array());
System.out.println(msg);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
GroupChatClient groupChatClient = new GroupChatClient();
new Thread(new Runnable() {
@Override
public void run() {
while (true){
groupChatClient.readInfo();
}
}
}).start();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()){
String s = scanner.nextLine();
groupChatClient.sendInfo(s);
}
}
}
开始的疑惑和解答:
服务端和客户端都含有selector,两者是否有关系?
- 没有关系,两者之间的关系类似于:
零拷贝原理
DMA:direct memory access ,直接内存拷贝,不使用CPU
mmap优化:
通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数
适合小数据量读写
sendFile优化:
linux2.1版本提供了sendFile函数,其基本原理为,数据根本不经过用户态,直接从内存和缓冲区进入到SocketBuffer,同时,优于和用户态完全不玩,就减少了一次上下文切换。
linux2.4版本做了一些修改,避免了从内核缓冲区拷贝到Socket buffer的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。
适合大文件传输
示例:
server:
public class IOServer {
public static void main(String[] args) throws IOException {
InetSocketAddress address = new InetSocketAddress(8888);
ServerSocketChannel socketChannel = ServerSocketChannel.open();
socketChannel.socket().bind(address);
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true){
SocketChannel accept = socketChannel.accept();
int read=0;
if (read!=-1){
read= accept.read(buffer);
}
buffer.rewind();//position置为0,mark作废
}
}
}
client:
public class IOClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1",8888));
String filename="d:/1.txt";
FileChannel fileChannel = new FileInputStream(filename).getChannel();
/*
在linux下一个transferTo方法就可以完成传输
在window下一次调用transferTo只能传输8M文件,就需要分段传输文件
* */
long start = System.currentTimeMillis();
fileChannel.transferTo(0,fileChannel.size(),socketChannel);
System.out.println("传输文件大小: " + fileChannel.size() + "使用时间: " + (System.currentTimeMillis() - start));
fileChannel.close();
}
}