温馨提示:内容局限于本人的理解,如果有错误,请指正,谢谢!
学习目标:
(1)熟悉Netty的客户端和服务器通信
(2)通过阻塞I/O、伪异步I/O的demo 了解它们是通信的基本流程。
上一篇文档,介绍了阻塞和伪异步I/O的案例,虽然伪异步I/O有一定的提升,但还是阻塞的。并未达到真正的非阻塞和高性能,本篇文章将接着学习下非阻塞I/O是如何做的,如何实现高性能?带着疑问,我们开始来学习。
Server.java 服务端启动类
import java.io.IOException;
/**
* @Auther: chen
* @Date:
* @Description:
*/
public class Server {
public static void main(String[] args){
ServerHandle serverHandle = null;
try {
serverHandle = new ServerHandle(9092);
} catch (IOException e) {
e.printStackTrace();
}
//启动多路复用器ServerHandle来处理请求
new Thread(serverHandle,"SERVER-CHEN-THREAD").start();
}
}
ServerHandle.java 多路复用器
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.Set;
/**
* @Auther: chen
* @Date:
* @Description:
*/
public class ServerHandle implements Runnable {
private int port;
private volatile boolean stop;
private Selector selector;
private ServerSocketChannel serverSocketChannel;
/**
* 初始化多路复用器
*
* @param port
* @throws IOException
*/
public ServerHandle(int port) throws IOException {
this.port = port;
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
//设置非阻塞模式
serverSocketChannel.configureBlocking(false);
//监听端口
serverSocketChannel.bind(new InetSocketAddress(port));
//注册ACCEPT 事件,表示关注客户端的注册事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器初始化完毕");
}
public void stop() {
this.stop = true;
}
@Override
public void run() {
while (!stop) {
try {
//每隔1s检查一次
selector.select(1000);
//获取到已经准备的keys,就是已经准备好的事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
//获取单个事件
SelectionKey key = iterator.next();
//处理key
try {
handleKey(key);
} catch (Exception ex) {
//出异常了,取消key
if (null != key) {
key.cancel();
if (key.channel() != null) {
try {
key.channel().close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}finally {
//处理后,把key移除掉
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != selector) {
try {
//多路复用器关闭后,所有注册在复用器上面的channel和pipe都会自动关闭
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 处理key
* @param key
* @throws IOException
*/
private void handleKey(SelectionKey key) throws IOException {
//判断key是否有效
if (key.isValid()) {
//key 连接状态
if (key.isAcceptable()) {
//客户端的连接请求
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//连接上了,注册OP_READ 事件
sc.register(selector, SelectionKey.OP_READ);
System.out.println("有客户端来连接了,帮他注册读事件");
}
//key 是否可读状态
if (key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取数据
int readData = sc.read(buffer);
// >0 读取到了数据
if (readData > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("服务器: 接收到客户端的数据,内容:" + body);
//发送数据给客户端
doWrite(sc, "现在是北京时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
//将key标记为可读
key.interestOps(SelectionKey.OP_READ);
} else if (readData < 0) {
//链路关闭了,则取消key,关闭sc
key.cancel();
sc.close();
} else {
// readData==0
// 没有读取到数据
}
}
}
}
/**
* 发送数据
* @param socketChannel
* @param hello
* @throws IOException
*/
private void doWrite(SocketChannel socketChannel, String hello) throws IOException {
if (hello != null) {
byte[] bytes = hello.getBytes();
ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
buffer.put(bytes);
//当前缓冲区是读状态,通过flip改变成写状态
buffer.flip();
socketChannel.write(buffer);
}
}
}
Client.java 客户端启动类
import java.io.IOException;
/**
* @Auther: chen
* @Date:
* @Description:
*/
public class Client {
public static void main(String[] args) {
ClientHandle clientHandle = null;
try {
clientHandle = new ClientHandle("127.0.0.1",9092);
new Thread(clientHandle,"CLIENT-CHEN-THREAD").start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
ClientHandle.java 客户端handle
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
/**
* @Auther: chen
* @Date:
* @Description:
*/
public class ClientHandle implements Runnable {
private int port;
private volatile boolean stop;
private Selector selector;
private String ip;
private SocketChannel socketChannel;
public ClientHandle(String ip, int port) throws IOException {
this.ip = ip;
this.port = port;
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
}
@Override
public void run() {
try {
//建立连接
doConnect();
} catch (IOException e) {
e.printStackTrace();
}
while (!stop) {
try {
selector.select(1000);
} catch (IOException e) {
e.printStackTrace();
}
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
try {
handleKey(key);
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 处理key
* @param key
* @throws IOException
*/
private void handleKey(SelectionKey key) throws IOException {
/**
* 验证key是否有效
*/
if (key.isValid()) {
//检查连接是否成功
SocketChannel sc = (SocketChannel) key.channel();
//key 连接状态
if (key.isConnectable()) {
//检查连接完成
if (sc.finishConnect()) {
//完成,注册读事件
sc.register(selector, SelectionKey.OP_READ);
doWrite(sc);
} else {
System.exit(1);
}
}
//key 读取状态
if (key.isReadable()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = sc.read(buffer);
if (read > 0) {
//buffer长度是1024 ,假如但是实际存储的位置10,那我要把0-10的数据读取出来,上线limit设置为10
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("客户端:接收到服务数据,内容 " + body);
this.stop = true;
} else if (read < 0) {
key.cancel();
sc.close();
} else {
}
}
}
}
/**
* 建立连接
* @throws IOException
*/
private void doConnect() throws IOException {
//建立连接成功,则注册读取事件
if (socketChannel.connect(new InetSocketAddress(ip, port))) {
socketChannel.register(selector, SelectionKey.OP_READ);
//成功了,发送数据给服务器
doWrite(socketChannel);
} else {
//注册失败,继续注册连接事件
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
}
/**
* 发送数据
* @param socketChannel
* @throws IOException
*/
private void doWrite(SocketChannel socketChannel) throws IOException {
byte[] bytes = "我是客户端".getBytes();
ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
buffer.put(bytes);
buffer.flip();
socketChannel.write(buffer);
if (!buffer.hasRemaining()) {
System.out.println("客户端发送完毕!");
}
}
}
运行效果:
服务端:
客户端:
上面实现了客户端发送数据到服务端,服务端返回当前的时间给客户端的功能。客户端是channel,在selector上注册所关注的事件,然后selector的轮询来处理请求,都是通过selector来处理,一个selector就可以同时处理多个客户的请求。客户端发送请求后,并未阻塞,等自己关注的数据准备好了,再进行相应的操作。通过简单的案例,对nio有个大概的了解。
调用的NIO时序图如下:
综合几种方式的源码来看,可以看出nio的编程的复杂度是最高的,为什么这么复杂的编码,应用却越来越广泛,主要有以下几个优点:
1、客户端发送请求都是异步的,通过多路复用器注册事件、等待内核准备好读写,再进行处理。
2、ScoketChannel 的读写操作也都是异步的,如果没有可读写的数据不会同步等待,直接返回。
3、epoll 没有句柄的限制,意味着selector可以同时处理成千上万个客户端的请求,并且性能不会随着客户端的增加而线性下降,适合做高性能、高负载的网络服务器。
虽然优点很多,但如果要用原生的NIO来编码,那相当的难受,并且效率很低,于是,有了netty,百度百科对于netty的介绍来看下:
Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
也就是说,Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。
netty 虽然简化了,但性能并没有降低。如果用netty来实现我们之前的客户端与服务器通信,那如何实现呢?
Server.java 服务器的main函数
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/**
* @Auther: chen
* @Date:
* @Description:
*/
public class Server {
public static void main(String[] args){
int port = 9092;
new Server().bind(port) ;
}
private void bind(int port) {
EventLoopGroup bossGroup = new NioEventLoopGroup();//@1
EventLoopGroup workGroup = new NioEventLoopGroup();//@2
ServerBootstrap bootstrap = new ServerBootstrap();
try {
ChannelFuture channelFuture = bootstrap.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ServerChannelHandle())
.bind(port).sync();//@3
channelFuture.channel().closeFuture().sync();@4
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
bootstrap.clone();
}
}
}
@1 是新建一个组来接收客户端请求,@2 是新建一个组来处理I/O请求
@3 就是绑定组合、监听端口channel、设置处理的handle和设置参数
@4 同步等待关闭
这里可以看出处理请求和I/O的组是不同的,这2个组就是不同的线程池。这里接收请求和实际处理的I/O分开,有什么好处呢?其实就是如果我一个线程池,那就和伪异步I/O一样,并没有实现真正的异步。
ServerChannelHandle.java 服务器端绑定处理请求的handle,在initChannel中可以自己实现编码器和解码器一些自动义操作
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
/**
* @Auther: chen
* @Date:
* @Description:
*/
public class ServerChannelHandle extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new ServerHandle());//@1
}
}
@1 这里通过handle的initChannel,设置了处理自己业务逻辑的类,这里其实还可以增加消息的编码和解码等操作、自定义协议等。
ServerHandle.java 处理具体业务的类
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @Auther: chen
* @Date:
* @Description:
*/
public class ServerHandle extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buffer = (ByteBuf) msg;
byte [] bytes = new byte[buffer.readableBytes()];
buffer.readBytes(bytes);
String body = new String(bytes,"UTF-8");
System.out.println("客户端发送的数据->"+ body);
String res = "现在是北京时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
ByteBuf byteBuf = Unpooled.copiedBuffer(res.getBytes());
ctx.writeAndFlush(byteBuf);
}
//读取完成后处理方法
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
System.out.println("EchoServerHandler.channelReadComplete");
//ctx.flush();
}
//异常捕获处理方法
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}
这里处理的逻辑很简单,就是把发送来的数据,读取打印出来。
接下来,编写客户端代码
Client.java
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
/**
* @Auther: chen
* @Date:
* @Description:
*/
public class Client {
public static void main(String[] args){
int port = 9092;
String host = "127.0.0.1";
new Client().connection(host, port) ;
}
private void connection(String host, int port) {
EventLoopGroup group = new NioEventLoopGroup();//@1
Bootstrap bootstrap = new Bootstrap();
try {
Bootstrap b = bootstrap.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChildChannelHandle());//@2
ChannelFuture f = b.connect(host, port).sync();//@3
System.out.println("客户端启动完成");
f.channel().closeFuture().sync(); //@4
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
group.shutdownGracefully();
}
}
}
@1 定义了个组来处理客户端的逻辑
@2 绑定组、设置参数、绑定hanele和channel
@3 同步连接服务器
@4 同步等关闭
这里和服务器很相似,只是这里只需要指定一个组,然后指定ip、port建立连接。
ChildChannelHandle 客户端处理请求的handle
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
/**
* @Auther: chen
* @Date:
* @Description:
*/
public class ChildChannelHandle extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new ClientHandle());
}
}
处理具体业务类
ClientHandle.java
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* @Auther: chen
* @Date:
* @Description:
*/
public class ClientHandle extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buffer = (ByteBuf) msg;
byte [] bytes = new byte[buffer.readableBytes()];
buffer.readBytes(bytes);
String body = new String(bytes,"UTF-8");
System.out.println("服务器返回->"+ body);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buffer = Unpooled.copiedBuffer("我是客户端".getBytes());
ctx.writeAndFlush(buffer);
}
}
分析:
这比上面的NIO方便太多了吧,很简洁
通过上面的案例,了解了NIO和netty 客户端与服务器连接的大致实现,发现代码简洁太多了。这也是运用netty的优势。
代码的地址:https://gitlab.com/157538651/netty-example