Java 1.4之前的早期版本,Java对I/O的支持并不完善,开发人员在开发高性能I/O程序的时候,会面临以下挑战和困难
Linux的内核将所有外部设备都看做一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令,返回一个file descriptor (fd, 文件描述符)。 而对一个socket的读写也会有相应的描述符,称为socketfd (socket 描述符),描述符就是一个数字,它指向内核中的一个结构体(文件路径,数据区等一些属性)。根据UNIX网络编程对I/O模型的分类,UNIX提供了5种I/O模型,分别如下:
阻塞I/O模型:最常用的I/O模型,缺省情形下,所有文件操作都是阻塞的。
以套接字接口为例来讲解此模型:在进程空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误时才会返回,在此期间会一直等待,进程在从调用recvfrom开始到它返回的整段时间内都是被阻塞的,因此被称为阻寨I/O模型,如下图:
非阻塞I/O模型: recvfrom 从应用层到内核的时候,如果该缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,一般都会对非阻塞I/O模型进行轮询检查这个状态,看内核是不是有数据到来。如下图:
I/O复用模型: Linux提供select/poll, 进程通过将一个或多个fd传递给select或poll系统调用,阻塞在select 操作上,这样select/poll可以帮我们侦测多个fd是否处于就绪状态。select/poll 是顺序扫描fd是否就绪,而且支持的fd数量有限,因此它的使用受到了一些制约。Linux还提供了一个epoll系统调用,epoll使用基于事件驱动方式代替顺序扫描,因此性能更高。当有fd就绪时,立即回调函数rollback,如下图:
信号驱动I/O模型:首先开启套接口信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,就为该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom,来读取数据,并通知主循环函数处理数据,如下图:
异步I/O:告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知我们。这种模型与信号驱动模型的主要区别是:信号驱动I/O由内核通知我们何时可以开始一个I/O操作;异步I/O 模型由内核通知我们I/O操作何时已经完成,如下图:
在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建和维护额外进程或者线程,降低了系统的工作量,节省系统资源,I/O多路复用的主要应用场景如下。
目前支持I/O多路复用的系统调用有select、 pselect、 poll、 epoll,在Linux网络编程过程中,很长一段时间都使用select做轮询和网络事件通知,然而select 的一些固有缺陷导致了它的应用受到了很大的限制,最终Linux 选择了epoll, epoll 与select 的原理比较类似,为了克服select的缺点,epoll和selet的主要区别如下:
1.epoll支持一个进程打开的socket描述符( FD )不受限制(仅受限于操作系统的最大文件句柄数)。
select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_ SETSIZE设置,默认值是1024。 对于那些需要支持上万个TCP连接的大型服务器来说显然太少了。可以选择修改这个宏然后重新编译内核,不过这会带来网络效率的下降。也可以通过选择多进程的方案(传统的Apache方案)解决这个问题,尽管在Linux上创建进程的代价比较小,但仍旧是不可忽视的。另外,进程间的数据交换非常麻烦,对于Java 来说,由于没有共享内存,需要通过Socket通信或者其他方式进行数据同步,这带来了额外的性能损耗,增加了程序复杂度,而epoll并没有这个限制,它所支持的FD上限是操作系统的最大文件句柄数,这个数字远远大于1024。例如,在1GB内存的机器上大约是10 万个句柄左右,具体的值可以通过cat /proc/sys/fs/file- max察看,通常情况下这个值跟系统的内存关系比较大。
2.epoll的I/O效率不会随着FD数目的增加而线性下降
传统select/poll 的另一个致命弱点,就是当拥有一个很大的socket集合时,由于网络延时或者链路空闲导致任一时刻只有少部分的socket是“活跃”的,但是select/poll 每次调用都会线性扫描全部的集合,导致效率呈现线性下降。而epoll只会对“活跃”的socket进行操作,这是因为在内核实现中,epoll是根据每个fd上面的callback函数实现的。那么,只有“活跃”的socket 才会去主动调用callback函数,其他idle状态的socket则不会。
3.epoll使用mmap加速内核与用户空间的消息传递
无论是select、poll还是epoll都需要内核把FD消息通知给用户空间,因此避免不必要的内存复制就显得非常重要,epoll是通过内核和用户空间mmap同一块内存来实现的。
4.epoll 的API更加简单
包括创建一个epoll描述符、添加监听事件、阻塞等待所监听的事件发生、关闭epoll描述符等。
网络编程的基本模型就是Client/Service模型,也就是两个进程之间的通信,其中服务端提供IP地址和端口,客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,连接建立成功,双方通过网络套接字Socket通信
BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,当收到客户端的连接请求后为每一个客户端创建一个新的线程进行链路处理,处理完成后通过输出流应答给客户端,线程销毁,这就是典型的一请求一应答模型,具体如下:
缺乏弹性伸缩能力,当客户端并发量增加后,服务端线程和客户端数成1:1正比,导致线程数急剧增加,系统性能下降,发生线程堆栈溢出,新建线程失败等问题,最终导致宕机或者死机
服务端代码实现
//服务端
public static void main(String[] args) throws Exception{
ServerSocket server = new ServerSocket(9999); //创建服务器
while(true) {
final Socket socket = server.accept(); //接受客户端的请求
new Thread() {
public void run() {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintStream ps = new PrintStream(socket.getOutputStream());
ps.println("收到请求");
System.out.println(br.readLine());
ps.println("收到请求");
System.out.println(br.readLine());
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
}
}
客户端代码实现:
public static void main(String[] args) throws Exception{
Socket socket = new Socket("127.0.0.1", 9999); //创建Socket指定ip地址和端口号
InputStream is = socket.getInputStream(); //获取输入流
OutputStream os = socket.getOutputStream(); //获取输出流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
PrintStream ps = new PrintStream(os);
System.out.println(br.readLine());
ps.println("发送请求");
System.out.println(br.readLine());
ps.println("发送请求");
socket.close();
}
NIO官方的解释意思是:New I/O,相对于JDK1.4之前的阻塞I/O,New I/O 类库的目标就是让java支持非阻塞I/O,所以也称之为非阻塞I/O(Non-block I/O),之所以称之为异步非阻塞I/O是因为相比于之前的同步阻塞读和写,他是异步的,但是按照严格的UNIX网络编程模型区分和JDK的实现来分,NIO不能称之为异步非阻塞I/O,在早期的JDK1.4版本之前,JDK的selector基于select/poll模型实现,它是基于I/O复用技术实现的非阻塞I/O,而不是异步I/O,在JDK1.5之后的版本中,优化使用epoll来替代select/poll,但是上层API没有发生变化,没有改变I/O模型,只是性能的优化,直到JDK1.7提供了NIO2.0新增了异步套接字通道,此时才真正实现异步I/O,在异步I/O操作的时候传递信号变量当操作完成后会回调相关方法,异步I/O也被称为AIO。
缓冲区Buffer
Buffer是一个对象,包含一些要写入或者读出的数据,在NIO类库中,所有数据都是用缓冲区处理的,读取数据时,它是直接读到缓冲区中,写入数据时,也是写入缓冲区,任何时候访问NIO中的数据都是通过缓冲区进行操作,缓冲区实质就是一个数组(通常是字节数组ByteBuffer),提供了对数据的结构化访问以及维护读写位置等信息
最常用的缓冲区就是ByteBuffer,一个ByteBuffer提供了一组功能用于操作byte数组。基本每一种java基本类型(除了Boolean)都对应一种缓冲区
通道Channel
Channel是一个通道,网络数据通过Channel读取和写入。通道与流的不同之处在于通道是双向的,流是单向的,通道用于读、写或者二者同时进行,通道的顶层是Channel接口,实际上Channel可以分为两大类:用于网络读写的SelectableChannel和用于文件操作的FileChannel,后面提到的ServerSockerChannel和SocketChannel都是SelectableChannel的子类
多路复用Selector
多路复用器提供了选择已经就绪的任务的能力,简单说:Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作,一个多路复用Selector可以同时轮询多个Channel,由于JDK使用了epoll()代替了传统的select实现,所以它没有最大连接句柄1024/2048的限制,这也就意味着只需要一个线程负责Selector的轮询
NIO序列图
NIO服务端开发
public class TimeServerHandle implements Runnable {
private Selector selector;
private ServerSocketChannel servChannel;
private volatile boolean stop;
/**
* 初始化多路复用器、绑定监听端口
*
* @param port
*/
public TimeServerHandle(int port) {
try {
selector = Selector.open();
servChannel = ServerSocketChannel.open();
servChannel.configureBlocking(false);
servChannel.socket().bind(new InetSocketAddress(port), 1024);
servChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("The time server is start in port : " + port);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
public void stop() {
this.stop = true;
}
/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
while (!stop) {
try {
selector.select(1000);
Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null)
key.channel().close();
}
}
}
} catch (Throwable t) {
t.printStackTrace();
}
}
// 多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
if (selector != null)
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
// 处理新接入的请求消息
if (key.isAcceptable()) {
// 接收新的连接
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
// Add the new connection to the selector
sc.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
// 读取数据
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("The time server receive order :"
+ body);
String currentTime = "QUERY TIME ORDER"
.equalsIgnoreCase(body) ? new java.util.Date(
System.currentTimeMillis()).toString()
: "BAD ORDER";
doWrite(sc, currentTime);
} else if (readBytes > 0) {
// 对端链路关闭
key.cancel();
sc.close();
} else
; // 读到0字节,忽略
}
}
}
private void doWrite(SocketChannel channel, String response)
throws IOException {
if (response != null && response.trim().length() > 0) {
byte[] bytes = response.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
channel.write(writeBuffer);
}
}
public static void main(String[] args) {
TimeServerHandle timeServerHandle = new TimeServerHandle(9999);
new Thread(timeServerHandle," NIO-SERVER").start();
}
}
NIO客户端开发
public class TimeClientHandle implements Runnable {
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean stop;
public TimeClientHandle(String host, int port) {
this.host = host == null ? "127.0.0.1" : host;
this.port = port;
try {
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
try {
doConnect();
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
while (!stop) {
try {
selector.select(1000);
Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null)
key.channel().close();
}
}
}
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
// 多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
if (selector != null)
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
// 判断是否连接成功
SocketChannel sc = (SocketChannel) key.channel();
if (key.isConnectable()) {
if (sc.finishConnect()) {
sc.register(selector, SelectionKey.OP_READ);
doWrite(sc);
} else
System.exit(1);// 连接失败,进程退出
}
if (key.isReadable()) {
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("Now is : " + body);
this.stop = true;
} else if (readBytes < 0) {
// 对端链路关闭
key.cancel();
sc.close();
} else
; // 读到0字节,忽略
}
}
}
private void doConnect() throws IOException {
// 如果直接连接成功,则注册到多路复用器上,发送请求消息,读应答
if (socketChannel.connect(new InetSocketAddress(host, port))) {
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
} else
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
private void doWrite(SocketChannel sc) throws IOException {
byte[] req = "QUERY TIME ORDER".getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
writeBuffer.put(req);
writeBuffer.flip();
sc.write(writeBuffer);
if (!writeBuffer.hasRemaining())
System.out.println("Send order 2 server succeed.");
}
public static void main(String[] args) {
TimeClientHandle timeClientHandle = new TimeClientHandle(null, 9999);
new Thread(timeClientHandle," NIO-CLIENT").start();
}
}
分别启动服务端和客户端,执行结果:
以上即是NIO编程的基本代码,但是它并没有考虑“半包读”和“半包写”。
NIO编程的优点
不同I/O模型的对比
同步阻塞I/O | 伪异步I/O | 非阻塞I/O | 异步I/O(AIO) | |
---|---|---|---|---|
客户端个数:I/O线程数 |
1:1 |
M:N(其中M可以大于N) |
M:1(1个I/O线程处理多个客户端连接) |
M:0(不需要启动额外的I/O线程,被动回调) |
I/O类型(阻塞) |
阻塞I/O |
阻塞I/O |
非阻塞I/O |
非阻塞I/O |
I/O类型(同步) |
同步I/O |
同步I/O |
同步I/O(I/O多路复用) |
异步I/O |
API使用难度 |
简单 |
简单 |
非常复杂 |
复杂 |
调试难度 |
调试难度 |
调试难度 |
调试难度 |
调试难度 |
可靠性 |
非常差 |
差 |
高 |
高 |
吞吐量 |
低 |
中 |
高 |
高 |
什么是Netty
Netty 是基于 Java NIO 的异步事件驱动的网络应用框架,它的健壮性、功能、可定制性和可扩展性在同类框架中首屈一指,得到成百上千商用项目的验证,Netty 提供了高层次的抽象来简化 TCP 和 UDP 服务器的编程,使用Netty 可以快速和简单的开发出一个网络应用,避免使用复杂繁琐的JDK NIO类库。
Netty开发入门
引入Netty开发的相关依赖:
io.netty
netty-all
5.0.0.Alpha2
Netty实现NIO服务端开发
public class NettyServer {
public void bind(int port){
//NioEventLoopGroup是一个线程组,包含一组NIO线程,用于网络事件处理,实际上就是Reactor线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();//用于服务端接受客户端的连接
EventLoopGroup workerGroup = new NioEventLoopGroup();//用于SocketChannel的网络读写
try{
//ServerBootstrap对象是Netty用于启动NIO服务端的辅助启动类
ServerBootstrap bs = new ServerBootstrap();
bs.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)//设置创建的channel
.option(ChannelOption.SO_BACKLOG, 1024)//设置NioServerSocketChannel的TCP参数
.childHandler(new ChildChannelHandler());//绑定I/O事件处理类ChildChannelHandler
ChannelFuture sync = bs.bind(port).sync();//绑定监听端口并调用同步阻塞方法等待绑定操作完成
sync.channel().closeFuture().sync();//等待服务器链路关闭之后main函数才退出
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放连接池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
//事件处理类ChildChannelHandler,类似Reactor中的Handler
class ChildChannelHandler extends ChannelInitializer{
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new TimeServerHandler());
}
}
class TimeServerHandler extends ChannelHandlerAdapter{
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf)msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "UTF-8");
System.out.println("Time Server receive order:" + body);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)? new Date(System.currentTimeMillis()).toString(): "BAD ORDER";
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//Netty把write方法并不直接将消息写入到SocketChannel中,调用write方法只是把待发送的消息放到缓冲数组中,
// 调用flush方法才将消息全部写道SocketChanel
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//释放相关句柄等资源
ctx.close();
}
}
public static void main(String[] args) {
new NettyServer().bind(9988);
}
}
Netty实现NIO客户端开发
public class NetttClient {
public void connect(String host, int port){
//配置客户端NIO线程组
EventLoopGroup group = new NioEventLoopGroup();
try{
//创建客户端辅助启动类Bootstrap
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,Boolean.TRUE)
.handler(new ChannelInitializer() {
//创建NioSocketChannel成功之后,进行初始化
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new TimeServerHandler());
}
});
//发起异步连接操作
ChannelFuture sync = bootstrap.connect(host, port).sync();
//等待客户端链路关闭
sync.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放NIO线程组资源
group.shutdownGracefully();
}
}
class TimeServerHandler extends ChannelHandlerAdapter {
private final ByteBuf firstMessage;
public TimeServerHandler() {
byte[] req = "QUERY TIME ORDER".getBytes();
firstMessage = Unpooled.buffer(req.length);
firstMessage.writeBytes(req);
}
//当客户端和服务端TCP链路建立成功之后,Netty的NI线程会调用channelActive方法,发送查询指定给服务端
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(firstMessage);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("now is :" + body);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.info("Unexpected exception frm downstream:" + cause.getMessage());
ctx.close();
}
}
public static void main(String[] args){
new NetttClient().connect("127.0.0.1",9988);
}
}
分别启动服务端和客户端执行结果如下:
Time Server receive order:QUERY TIME ORDER
now is :Sat May 30 18:30:18 CST 2020
可以看出相比于传统的NIO程序,使用Netty开发代码更加简洁,开发难度更低,扩展性也更好。在上面的Netty入门的代码中并没有考虑读半包等问题。当系统压力突增或者发送大报文之后,就会存在粘包/拆包的问题,可能导致解码错位甚至错误,导致程序不能正常工作。