目录
BIO
简单代码实现
首先是我们熟悉的bio,利用原生socket进行操作。
public class SocketServer {
public static void main(String[] args) {
try {
ServerSocket server = new ServerSocket(8888);
System.out.println("服务器已经启动!");
// 接收客户端发送的信息
Socket socket = server.accept();
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String info = null;
while ((info = br.readLine()) != null) {
System.out.println(info);
}
// 向客户端写入信息
OutputStream os = socket.getOutputStream();
String str = "欢迎登陆到server服务器!";
os.write(str.getBytes());
// 关闭文件流
os.close();
br.close();
is.close();
socket.close();
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
上面是一个最简单的socket单线程服务器实例,accept()方法阻塞等待请求,请求处理完之后结束程序。
代码优化实现
我们可以进一步优化代码,accept()接收请求之后,不在当前线程内部进行io处理,另外开辟新的线程进行io操作
public class Bio {
public static void main(String[] args) {
ServerSocket server = new ServerSocket(8888);
System.out.println("服务器已经启动!");
// 接收客户端发送的信息
while(true){
Socket socket = null;
try {
socket = server.accept();
} catch (IOException e) {
e.printStackTrace();
}
new Thread(() ->{
try {
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String info = null;
while ((info = br.readLine()) != null) {
System.out.println(info);
}
// 向客户端写入信息
OutputStream os = socket.getOutputStream();
String str = "欢迎登陆到server服务器!";
os.write(str.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
面这段代码有了部分优化,主线程进行循环accept(),负责请求接收,工作线程采用新线程或者线程池的方式处理。这种方式的好处是,将io工作委派给新的线程,可以较为及时接收新的请求。缺点也很明显,耗费大量的线程资源,即使使用线程池来进行管理,当大量请求到来,线程池的队列爆满,程序就会崩溃。
NIO
代码实现
NIO出现解决的bio的缺点问题,我们先看一下网上找滴一个nio的简单例子。
public class Nio {
// 本地字符集
private static final String LocalCharSetName = "UTF-8";
// 本地服务器监听的端口
private static final int Listenning_Port = 8888;
// 缓冲区大小
private static final int Buffer_Size = 1024;
// 超时时间,单位毫秒
private static final int TimeOut = 3000;
public static void main(String[] args) throws IOException {
// 创建一个在本地端口进行监听的服务Socket信道.并设置为非阻塞方式
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.socket().bind(new InetSocketAddress(Listenning_Port));
serverChannel.configureBlocking(false);
// 创建一个选择器并将serverChannel注册到它上面
Selector selector = Selector.open();
//设置为客户端请求连接时,默认客户端已经连接上
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 轮询监听key,select是阻塞的,accept()也是阻塞的
if (selector.select(TimeOut) == 0) {
System.out.println(".");
continue;
}
// 有客户端请求,被轮询监听到
Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();
while (keyIter.hasNext()) {
SelectionKey key = keyIter.next();
if (key.isAcceptable()) {
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
//意思是在通过Selector监听Channel时对读事件感兴趣
clientChannel.register(selector, SelectionKey.OP_READ,
ByteBuffer.allocate(Buffer_Size));
}
else if (key.isReadable()) {
SocketChannel clientChannel = (SocketChannel) key.channel();
// 接下来是java缓冲区io操作,避免io堵塞
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear();
long bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
// 没有读取到内容的情况
clientChannel.close();
} else {
// 将缓冲区准备为数据传出状态
buffer.flip();
// 将获得字节字符串(使用Charset进行解码)
String receivedString = Charset
.forName(LocalCharSetName).newDecoder().decode(buffer).toString();
System.out.println("接收到信息:" + receivedString);
String sendString = "你好,客户端. 已经收到你的信息" + receivedString;
buffer = ByteBuffer.wrap(sendString.getBytes(LocalCharSetName));
clientChannel.write(buffer);
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
}
keyIter.remove();
}
}
}
}
基本操作绑定监听端口,然后就是开启一个selector轮询监听客户端请求。注意,这里当客户端请求上来了,是不会马上和客户端建立连接的(此时客户端发送的请求已经告知selector并注册key,并且等待),用一个key来做记录,在轮询处理中,发现服务器通道有连接key,如果是可接受key事件,ServerSocketChannel.accept(),建立连接(这里就是基于事件通知机制,不再用阻塞accept()等待,而是基于事件通知,再进行accept()),建立连接通道后,通道注册到selector并且关注可读事件,客户端发送数据可以写入到接收端的TCP缓冲区。等到写完之后,客户端通道出现可读事件,isReadable()为true,进行读取TCP缓冲,写入到应用缓冲,使用的是java的缓冲io,程序读取信息到buffer和写返回结果到buffer都是在main线程,都是一个线程,并没有new 新的线程处理io。在把信息写入缓冲区后,才会新建线程进行io操作。
nio相对bio
是基于nio类似swing那种事件驱动机制来连接客户端的。也就是说,bio时,客户端来连接了就是直接连接怼到端口,然后阻塞后续请求,等待上一个连接处理完。nio呢,当有客户端来连接时,不好意思,服务器并不给你直接怼到端口创建连接,而是注册一个key到selector说我是可处理的,然后selector轮询发现,这里有个key是客户端要来的意思(类似于swing的事件源),于是处理客户端的这个请求,设置为读操作,然后建立连接进行io操作,io是利用缓冲区做桥梁,后续再进行new Thread io操作,不耽误selector下一次轮询操作,这种io方式效率很高,在这里不详细说。
这种方式客户端请求来一个记录一个,类似批量操作请求,不用为没一个请求启用新线程进行处理,在一个main里面统统搞定,同时采用非阻塞I/O的通信方式,不要求阻塞等待I/O 操作完成即可返回,从而减少了管理I/O 连接导致的系统开销,大幅度提高了系统性能。
总的来说就是事件驱动模型和非阻塞io的结合使用。
更新:
-
Nio是面向缓存区操作,在read或者wirte的时候可以不阻塞,例如上面的例子,建立了通道,分配了缓冲区,然后进行read。这个read数据到缓冲区的过程由操作系统控制,相对java程序是异步的。如果java程序read的时候客户端发送的数据还没有完全写入到缓冲区,那么read得到的只是部分数据,这个时候read不阻塞,线程可以处理别的事情,稍后继续读取缓冲区中的数据。
-
Nio同时还使用到虚拟内存技术,从网卡读取的数据由DMA处理写入到虚拟内存,java用户空间内存映射到虚拟内存地址,相当于可以直接操作读取虚拟内存的数据,这样的好处是在操作系统内存不足的情况也可以读取大文件、数据。同时java用户程序直接操作虚拟内存,也减少了一次数据拷贝【网卡->内核空间->用户空间】,直接操作虚拟内存,相当于零拷贝。如果给通道分配缓冲区时,使用直接内存,还可以更快的读取数据,java程序相当于直接操作内核空间内存,零拷贝且读取快
Netty
1. Netty简介
Netty是一个高性能、异步事件驱动的NIO框架,基于JAVA NIO提供的API实现。它提供了对TCP、UDP和文件传输的支持,作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。 作为当前最流行的NIO框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,一些业界著名的开源组件也基于Netty的NIO框架构建。
2. Netty线程模型
在JAVA NIO方面Selector给Reactor模式提供了基础,Netty结合Selector和Reactor模式设计了高效的线程模型。先来看下Reactor模式:首先Reactor模式首先是事件驱动的,有一个或者多个并发输入源,有一个Server Handler和多个Request Handlers,这个Service Handler会同步的将输入的请求多路复用的分发给相应的Request Handler。过程如下:
从结构上有点类似生产者和消费者模型,即一个或多个生产者将事件放入一个Queue中,而一个或者多个消费者主动的从这个队列中poll事件来处理;而Reactor模式则没有Queue来做缓冲,每当一个事件输入到Service Handler之后,该Service Handler会主动根据不同的Evnent类型将其分发给对应的Request Handler来处理。
3. Reator模式的实现
1.第一种实现模型
这是最简单的Reactor单线程模型,由于Reactor模式使用的是异步非阻塞IO,所有的IO操作都不会被阻塞,理论上一个线程可以独立处理所有的IO操作。这时Reactor线程是个多面手,负责多路分离套接字,Accept新连接,并分发请求到处理链中。
对于一些小容量应用场景,可以使用到单线程模型。但对于高负载,大并发的应用却不合适,主要原因如下:
- 当一个NIO线程同时处理成百上千的链路,性能上无法支撑,即使NIO线程的CPU负荷达到100%,也无法完全处理消息
- 当NIO线程负载过重后,处理速度会变慢,会导致大量客户端连接超时,超时之后往往会重发,更加重了NIO线程的负载。
- 可靠性低,一个线程意外死循环,会导致整个通信系统不可用
为了解决这些问题,出现了Reactor多线程模型。
2.Reactor多线程模型
相比上一种模式,该模型在处理链部分采用了多线程(线程池)。
在绝大多数场景下,该模型都能满足性能需求。但是,在一些特殊的应用场景下,如服务器会对客户端的握手消息进行安全认证。这类场景下,单独的一个Acceptor线程可能会存在性能不足的问题。为了解决这些问题,产生了第三种Reactor线程模型。
3.Reactor主从模型
该模型相比第二种模型,是将Reactor分成两部分,mainReactor负责监听server socket,accept新连接;并将建立的socket分派给subReactor。subReactor负责多路分离已连接的socket,读写网络数据,对业务处理功能,其扔给worker线程池完成。通常,subReactor个数上可与CPU个数等同。
4.Netty模型
Reactor的三种模型,那么Netty是哪一种呢?其实Netty的线程模型是Reactor模型的变种,那就是去掉线程池的第三种形式的变种,这也是Netty NIO的默认模式。Netty中Reactor模式的参与者主要有下面一些组件:
- Selector
- EventLoopGroup/EventLoop Reactor模式中的分发(Dispatcher)的角色
- ChannelPipeline 请求处理器
Netty采用了串行化设计理念,从消息的读取、编码以及后续Handler的执行,始终都由IO线程EventLoop负责,这就意外着整个流程不会进行线程上下文的切换,数据也不会面临被并发修改的风险。这也解释了为什么Netty线程模型去掉了Reactor主从模型中线程池。
4.Buffer
Netty提供的经过扩展的Buffer相对NIO中的有个许多优势,作为数据存取非常重要的一块,我们来看看Netty中的Buffer有什么特点。
1.ByteBuf读写指针
- 在ByteBuffer中,读写指针都是position,而在ByteBuf中,读写指针分别为readerIndex和writerIndex,直观看上去ByteBuffer仅用了一个指针就实现了两个指针的功能,节省了变量,但是当对于ByteBuffer的读写状态切换的时候必须要调用flip方法,而当下一次写之前,必须要将Buffe中的内容读完,再调用clear方法。每次读之前调用flip,写之前调用clear,这样无疑给开发带来了繁琐的步骤,而且内容没有读完是不能写的,这样非常不灵活。相比之下我们看看ByteBuf,读的时候仅仅依赖readerIndex指针,写的时候仅仅依赖writerIndex指针,不需每次读写之前调用对应的方法,而且没有必须一次读完的限制。
2.零拷贝
- Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
- Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。
- Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。
3.引用计数与池化技术
- 在Netty中,每个被申请的Buffer对于Netty来说都可能是很宝贵的资源,因此为了获得对于内存的申请与回收更多的控制权,Netty自己根据引用计数法去实现了内存的管理。Netty对于Buffer的使用都是基于直接内存(DirectBuffer)实现的,大大提高I/O操作的效率,然而DirectBuffer和HeapBuffer相比之下除了I/O操作效率高之外还有一个天生的缺点,即对于DirectBuffer的申请相比HeapBuffer效率更低,因此Netty结合引用计数实现了PolledBuffer,即池化的用法,当引用计数等于0的时候,Netty将Buffer回收致池中,在下一次申请Buffer的没某个时刻会被复用。
5.简单代码实现
为什么有netty,首要原因就是:nio使用起来太麻烦。netty保留的nio的特性,进行了封装优化
public class Netty {
public void start(int port) throws Exception
{
ServerBootstrap strap = new ServerBootstrap();
//主线程
EventLoopGroup bossGroup = new NioEventLoopGroup();
//从线程
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
strap.group(bossGroup, workerGroup).
//主线程监听通道
channel(NioServerSocketChannel.class).
option(ChannelOption.SO_BACKLOG, 1024).
//定义从线程的handler链,责任链模式
childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyServerHandler());
}
});
ChannelFuture future=strap.bind(port).sync();
future.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
System.out.println("start server");
new Netty().start(8000);
}
}
上面是一个netty的server,相比起nio的server简洁了非常多,netty使用Reactor模型,代码中的bossGroup 就是一个线程池,Reactor中扮演接收请求,并且分派任务到工作线程的角色。workerGroup 就是我们的苦力干活的。workerGroup 中的线程调用我们定义好的handler、链进行各种业务处理,bossGroup负责请求接收。处理过程大致如下
netty对nio进行了封装,使得我们使用起来极为方便,同时netty里面的selector采用了线程池的方式进行监听客户端请求,就是好多个NioEventLoop,原则上,一个端口就只是用一个NioEventLoop线程来处理客户端请求,workerGroup中,一个客户端过来的channel就绑定在一个NioEventLoop上,单线程,按顺序执行handler链,channel还绑定了一个selector来处理channel的各种key。
6.总结
Netty其实本质上就是Reactor模式的实现,Selector作为多路复用器,EventLoop作为转发器,Pipeline作为事件处理器。但是和一般的Reactor不同的是,Netty使用串行化实现,并在Pipeline中使用了责任链模式。
Netty中的buffer相对有NIO中的buffer又做了一些优化,大大提高了性能。