今天记录一下这一块的演变历史~
首先是我们熟悉的bio,利用原生socket进行操作。
BIO
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
终于说到netty了,为什么有netty,首要原因就是:
nio使用起来太麻烦
netty保留的nio的特性,进行了封装优化
直接上nettydemo
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线程来处理客户端请求,线程池的话有什么用我还不知道(可以监听多个端口,在多个服务共用netty时可以起用处?)。。。设置多个boss会不会还是资源浪费?workerGroup中,一个客户端过来的channel就绑定在一个NioEventLoop上,单线程,按顺序执行handler链,channel还绑定了一个selector来处理channel的各种key。