一、简介和应用场景
1.1、简介
- netty是异步的,基于事件驱动的网络应用框架,用于快速开发高性能、高可用的网络IO程序
- netty主要针对在TCP/IP协议下,面向clents短的高并发应用,或者是PeerToPeer场景下,大量数据持续持续输出应用。(netty本质是对JDK的IO模型进行重写和优化)
- Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景
1.2、应用场景
分布式系统中的,远程服务调用,高性能RPC服务框架是必不可少的,Netty作为高性能的异步通信框架作为基础组件会被这RPC框架,例如阿里巴巴的dubbo底层使用的RPC,底层使用的就是netty
游戏服务器使用
二、IO
IO可以简单的理解为用什么的通道进行进行数据的发送和接收,这决定了程序通信的性能
Java支持三种网络编程模型(IO模式):BIO,NIO,AIO
JavaBIO模型:(同步并阻塞/传统阻塞型)JDK1.4之前
JavaBIO(bocking io)它是java的原生IO,服务器实现模式是一个连接一个线程,即客户端有连接请求时,服务器端就需要启动一个线程进行处理,如果连接不做任何事,就会造成不必要的性能开销 (BIO)
每有一个客户端请求,服务端都要开启一个线程,当并发过大时,服务端压力也会增大,而且在进行读写时,如果没有获取到数据,会一直等着,知道读写完成才返回
JavaNIO模型:(同步非阻塞)JDK1.4之后出现
服务器实现模式为一个线程处理多个请求(连接),即客户端发送的请求都会被注册到多路复用器(选择器Selector)上,多路复用器会一直轮询,多路复用器轮询到连接有IO请求就进行处理
JavaAIO模型:(异步非阻塞NIO.2)JDK1.7出现
这个模型并没有得到广泛的应用,AIOO引入异步通道的概念,采用了Proactor模式,简化了程序的编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端启动线程去处理,一般是用于连接数多且连接时间长的应用
三、IO模型选择
- BIO方式适用于连接数目比较小且固定的架构,这样方式对服务器资源要求比较高,并发局限于应用中,JDK1.4之前唯一的选择,但程序易理解
- NIO方式适用于连接数目多且连接比较短(轻操作)的架构,Netty是基于NIO的对其功能进行增强,可以支持长链接,比如聊天服务器,弹幕系统,服务间通信等,编程比较复杂,JDK1.4开始支持
AIO方式使用连接数目多且连接时间比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK1.7开始支持
四、JavaBIO(Java网络编程)
JavaBIO(bocking io):同步阻塞IO,他是传统的javaIO编程,其功能类和接口在java.io包下,处理模式是一个连接一个线程,即客户端有连接请求时,服务端就需要开启一个线程去处理,通过线程池可以实现多个客户连接服务器
4.1、网络编程三要素:
IP:在网络上用来唯一标识一台计算机
端口:用来标识计算机上运行的那个程序(应用程序的唯一标识),它的取值是0-65536,其中0-1023被一些知名网络和应用程序使用,
协议:通信的规则4.2、UDP协议
UDP协议:他是面向无连接的协议,即在数据传输时发送端不会和接收端建立连接,发送端也不会确认接收端是否存在,就会发出数据,接收端接收到数据也不会向发送端响应反馈,优点:消耗资源少,通信效率高,通常用于视频音频普通数据的传输,使用UDP协议由于是面向无连接的,传输过程中偶尔会丢一些数据包,所以UDP协议适用于丢一些数据也不会对结果有较大影响的场景(即适用于要求数据传入快,偶尔掉包无所谓的场景)例如直播,视频会议等
4.3、TCP协议
它是面向有连接的协议,它提供了两台计算机可靠无差别的数据传输,TPC协议中要明确客户端和服务器端,客户端向服务器端发送请求,每次连接的创建都需要3次握手,断开连接需要四次挥手
三次握手:第一次:客户端向服务器端发送请求申请连接
- 第二次:服务器端接收到请求,向客户端给予响应,接受连接
- 第三次:客户端向服务器端再次发送确认信息,确保信息无误(第三次连接防止客户端已经挂了)
4.4、UDP通信实现
原理:UDP协议是一种不可靠的通信协议,他在通信的两端各建立一个Socket,但是这两个Socket只是发送和接收的对象,没有客户端和服务器的概念,Java通过DatagramSocket类来实现UDP协议的Socket
发送端: ```java package com.study.udp;
import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress;
//使用UDP协议发送数据报包 public class SendUDP { public static void main(String[] args) throws Exception { //创建DatagramSocket发送UDP协议的数据报包 DatagramSocket ds = new DatagramSocket(); byte[] bytes = “testUDP”.getBytes(); InetAddress inetAddress = InetAddress.getByName(“10.10.4.160”); //创建数据报包 DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length, inetAddress, 10086); //调用send发送数据 ds.send(datagramPacket);
//关闭发送端
ds.close();
}
}
**接收端:**
```java
package com.study.udp;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
//接收UDP数据
public class ReceiveUDP {
public static void main(String[] args) throws Exception {
//创建DatagramSocket发送UDP协议的数据报包
DatagramSocket ds = new DatagramSocket(10086);
byte[] bytes = new byte[10000];
//创建数据报包
DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length);
ds.receive(datagramPacket);
System.out.println(new String(datagramPacket.getData()));
//关闭接收端
ds.close();
}
}
4.5、TCP协议通信实现
TCP是一种可靠的通信协议,它在通信的两端各建立一个Socket对象,从而在通信的两端形成网络虚拟链路,Java对TCP协议进行良好的封装,使用Socket代表两端的通信端口,并通过Socket产生IO流来进行网络通信,Java为客户端提供了Socket类,为服务端提供了ServerSocket类
接收端:
package com.study.tcp;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class TcpServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(10068, 20, InetAddress.getByName("10.10.4.160"));
//监听客户端连接,一旦有一个连接就会生成一个socket对象
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
byte[] bytes = new byte[1000];
inputStream.read(bytes);
System.out.println(new String(bytes));
socket.close();
}
}
发送端:
package com.study.tcp;
import java.io.OutputStream;
import java.net.Socket;
public class TcpClient {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("10.10.4.160", 10068);
OutputStream outputStream = socket.getOutputStream();
String str = "testTCP";
outputStream.write(str.getBytes());
outputStream.close();
socket.close();
}
}
通过原生的TCP客户端服务代码实现可以发现,我们使用java服务端只能监听一个端口,当这个端口请求过来时,才会建立一个连接,这样是能实现一个服务端处理一个请求,这是不现实的,可以通过线程池,来优化,每次请求过来,从线程池中获取一个线程连接去处理,这样就可以实现处理多个请求,这样如果连接过来什么都没做,就会出现资源浪费情况,
在通过ServerSocketS调用accept()方法时它会堵塞,在没有接收到连接时会一直阻塞在哪里等待连接,通过socket获取输入流read的时候也会阻塞,连接过后如果客户端没有输入数据,那么read方法也会阻塞,在哪里等着读取,这是客户端发送数据,read会读到数据,读到以后还是会继续阻塞在哪里
线程池优化:
package com.study.bio;
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 Exception {
//创建一个线程池
ExecutorService executorService = Executors.newCachedThreadPool();
//创建TCP服务器
ServerSocket serverSocket = new ServerSocket(10086);
//一直监听客户端连接
while (true) {
Socket socket = serverSocket.accept();
//开启一个线程
executorService.execute(() -> {
handler(socket);
});
}
}
private static void handler(Socket socket) {
try {
InputStream inputStream = socket.getInputStream();
while (true){
byte[] bytes = new byte[10000];
inputStream.read(bytes);
System.out.println(new String(bytes));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
五、JavaNIO(non -blocking IO)
5.1、简介
JavaNIO是指java 提供新的API,从JDK1.4开始Java提供一系列改进输入输出的新特性,被统称为NIO(new IO),是同步非阻塞的,他们放在Java.nio包下,并对原有的java.io包下的类进行改写
JavaNIO有三大核心:Channel(通道),Buffer(缓冲区),Selector(选择器)
NIO是面向缓冲区,或者面向块编程的
5.2、NIO核心一:块(buffer)
在NIO中Buffer是一个顶层父类,提供了除boolean的其他七种数据类型的Buffer,用来存放对应类型的数据
package com.study.buffer;
import java.nio.IntBuffer;
public class BufferTest {
public static void main(String[] args) {
//创建一个buffer,分配一个长度为5的Buffer,即可以存放五个int类型的数据
IntBuffer intBuffer = IntBuffer.allocate(5);
//存数据intBuffer.capacity()
for (int i = 0; i < intBuffer.capacity(); i++) {
intBuffer.put(0);
}
//读数据:记住Buffer读数据需要先进行读写反转
intBuffer.flip();
//开始取数据
while (intBuffer.hasRemaining()){//判断指针下一位是否有数据
int i = intBuffer.get();//获取数据并将指针移到下一位
System.out.println(i);
}
}
}
5.3、NIO三大核心原理关系
- 每一个channel都会对应一个Buffer
- Selector对应一个线程,一个Selector对应多个channel,所以一个线程可以对应对个channel(这里的channel就是连接),所以一个线程可以处理多个连接
- 连线需要注册到Selector程序
- Selector内部会一直轮询所有注册过来的channel(连接),监听channel的Event(时间),根据不同的事件来切换处理那个channel
- 数据的读取和写入是通过Buffer,也就是程序获取数据需要通过Buffer,这和BIO有本质的区别,BIO获取和写入数据都是通过流通道,且通道是单向的,那么只能读取,要么只能写入,而NIO的Buffer是双向的,既可以读取,也可以写入,但是需要调用filp()方法,进行读写转换
- Buffer就是一个内存块,它的底层是一个数组
- channel是双向的,程序读取数据必须经过Buffer,不能直接从channel读取
总结:Buffer的本质是一个可以读写的内存块,可以理解为一个容器对象(包含数组),该对象提供一组方法,可以轻松的使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供了从文件、网络中读取数据的渠道,但是读取数据必须经过Buffer
5.4、Buffer类介绍
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;//标记
private int position = 0;//位置
表示下一个要读取元素的索引,每次读写缓冲区都会改变该值,为下次读写做准备
初始position为0,表示下次要读读取第一个元素,我们可以手动设置这个值来实现读取指定索引的元素
intBuffer.position(2);表示下一次开始读第三个元素
private int limit;//当前缓冲区的终点
他规定了缓冲区的读写范围(读取元素个数限制),不能对超过终点范围的缓冲区进行读写,这个极限值可以修改
intBuffer.limit(4);表示只能读到第三个元素,第四个元素不会被读取到
private int capacity;//缓冲区容量
表示缓冲区大小,在创建缓冲区时确定,在存数据时不能超过缓冲区大小(因为底层是数组存储数据)
Buffer的常用方法
ByteBuffer的常用方法
5.5、NIO核心二:通道(channel)
BIO中的流是单向,例如我们创建一个FileInputStream流,他就是输入流,只能用来读取数据,而NIO中的channel是双向的既可以用来读取也可以用来写数据。
Channel在NIO中是一个接口,常用的实现类有FileChannel,DatagarmChannel,ServerSocketChannel,SocketChannel(注意:ServerSocketChannel类似于BIO中的ServerScoket,SocketChannel类似于BIO的Socket)
FileChannel:用于文件的数据读写
DatagarmChannel:用于UDP的数据读写
ServerSocketChannel,SocketChannel:用于TCP的数据读写,他们是抽象类,他们的实现是(ServerSocketChannelImpl,SocketChannelImpl)
FileChannel的使用
下面两个复制操作效率很高,底层使用了零拷贝
5.6、FileChannel的使用
写文件到磁盘
package com.study.channel;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
//使用FileChannel来实现写文件
public class FileChannel {
public static void main(String[] args) throws Exception{
//原生的IO对象被增强,他内部内置了channel,IO对象中包含了channel
String test="111212";
FileOutputStream fileOutputStream = new FileOutputStream("d:\\a.txt");
java.nio.channels.FileChannel channel = fileOutputStream.getChannel();
//创建一个缓存区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将需要写出的数据放入缓冲区
byteBuffer.put(test.getBytes());
//将缓冲区数据写入通道,需要进行读写转换
byteBuffer.flip();
//通道的读写需要站在通道的角度,往通道里写数据使用write,读通道的数据使用read
channel.write(byteBuffer);
//关闭资源
fileOutputStream.close();
}
}
读取文件到内存:
package com.study.channel;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
//通过channel来读取文件
public class ReadFileChannel {
public static void main(String[] args) throws Exception {
FileInputStream fileInputStream = new FileInputStream("d:\\a.txt");
FileChannel channel = fileInputStream.getChannel();
//创建一个缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//往缓冲区写数据,站在channel角度就是从channel中读数据到缓冲区
channel.read(byteBuffer);
byte[] array = byteBuffer.array();
String string = new String(array);
fileInputStream.close();
System.out.println(string);
}
}
总结:使用NIO进行文件的读写也是需要使用IO,我们通过原生的IO来获取通道,然后创建缓冲区,将缓冲区的数据写入或者读取到channel中,实现文件传输,原因是IO流中包含了channel,所以我们可以通过流对象获取channel,通过我们将数据写入到channel,流对象也就能获取数据
文件复制:使用同一个Buffer进行循环读写,每次循环需要重置buffer
package com.study.channel;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
//通过Buffer进行文件拷贝
public class CopyFileByChannel {
public static void main(String[] args) throws Exception {
//读取一个文件到缓冲区
FileInputStream fileInputStream = new FileInputStream("d:\\a.txt");
FileChannel inputChannel = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("d:\\b.txt");
FileChannel outputChannel = fileOutputStream.getChannel();
//创建一个Buffer, #1.此时position为0,limit和capacity都是10
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
//循环读写
while (true){
//#5.进行下一次操作时,由于上次使用flip操作和write,所以limit和position,导致读不到数据,所以要调用重置方法
//从通道读数据到缓冲区,返回-1表示已经读完
//重置ByteBuffer(以为读写复用了同一个buffer,所以需要重置,否者无法读取数据)
byteBuffer.clear();
//#2.使用read的时候,会一直读取,使得position改变成读取数据的长度(有可能等于limit)
int num = inputChannel.read(byteBuffer);
if(num==-1){
break;
}
//读写转换#3.此操作会使limit等于position,使position等于0,因为我们读了多少数据,就写多少数据,limit就是标致读了多少数据
byteBuffer.flip();
//将缓冲区数据写入通道#4.使得position等于limit
outputChannel.write(byteBuffer);
}
}
}
对上面进行优化:使用transferForm完成拷贝tagChannel.tarnsferForm(sourceChannel ,size)
package com.study.channel;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
//优化文件拷贝
public class CopyFileByChannelBetter {
public static void main(String[] args) throws Exception {
FileInputStream fileInputStream = new FileInputStream("d:\\a.txt");
FileChannel inputChannel = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("d:\\b.txt");
FileChannel outputChannel = fileOutputStream.getChannel();
//将源通道数据拷贝到目标通道
outputChannel.transferFrom(inputChannel, 0, inputChannel.size());
fileInputStream.close();
outputChannel.close();
}
}
5.7、Buffer和channel的注意事项
- Buffer支持类型化put和get,也就是我们放入什么类型,去的时候也是用什么类型去取否者可能会报错,例如:putInt/getInt
- 可以将普通的可读可写的Buffer变成只读Buffer
ByteBuffer onlyReadBuffer = byteBuffer.asReadOnlyBuffer();调用这个方法返回的就是一个只读的Buffer,如果这时在往里写会包ReadOnlyException异常
5.8、MappedByteBuffer
MappedByteBuffer可以让文件直接在内存中(堆外内存)进行修改,即操作系统不需要在拷贝一次,而如何同步文件由NIO完成、
5.9、channel分散和聚合 Buffer
如果数据比较多,我们可以使用多个Buffer数组完成读写操作,即Scattering和Gattering
Scattering(分散):将数据写入到Buffer时,可以写到Buffer数组中,即第一个满了,写第二个以此类推
Gattering(聚合):从Buffer中读数据时,可以读到一个Buffer数组中
channel提供了read()和write数组的方法,用来实现分散和聚合操作
5.10、NIO核心三:选择器(selector)
Selector是一个抽象类,常用方法:
public static Selector open();//创建一个选择器对象
public int select(Long timeout);//监控所有注册到这个选择器的通道,当通道中有IO操作进行时,将对应的SelectionKey加入到内部集合并返回,参数用来设置超时时间,这个方法体现了NIO的非阻塞,传入参数是等待时间,比如传入2000,即2000毫秒 后没有监听到时间就返回
select()是一个重载的方法,没有参数的select()是一个阻塞的方法,它会一直阻塞直到监听到注册过来的通道有事件发生才返回
selectNow();这个方法也是非阻塞的,他是如果通道没有事件发生,他就立即返回
public Set
selector.wakeup();唤醒selector,当selector在阻塞时,可以调用这个方法唤醒selector
注意:选择器监听通道,当通道有读写时间发生,就可以获取到对应的SelectionKey,通过select方法可以将得到额SelectionKey放入选择器的内部集合中,通过这个SelectionKey可以获得对应的channel
5.11、Selector/SelectionKey/SocketChannel/ServerSocketChannel之间的关系
- 当有客户端连接时,ServerSocketChannel会生成一个SocketChannel,把它注册到Selector中(通过SocketChannel的register方法注册),一个Selector可以注册多个SocketChannel,注册成功后会返回一个SelectionKey,放入Selector的Set集合中(即和Selector关联)
- Selector调用Select()方法对通道进行监听,这个方法的返回值是int,返回有事件发生的通道个数,进而得到有事件发生的SelectorKey,通过SelectKey中的channel方法,获取注册的channel
补: