1.NIO编程
1.1 什么是NIO编程
一种理解是New I/O ,原因是相较之前的I/O类库是新增的。更多的人喜欢称之为非阻塞I/O(Non-block I/O),由于非阻塞I/O更能体现NIO的特点,所以后续NIO都指的是非阻塞I/O
1.2NIO类库介绍
1.缓冲区Buffer
在面向流的I/O中,可以直接写入或者将数据直接读取到Stream对象中,在NIO库中,所有数据都是用缓冲区来处理的。在写入数据的时候,也是写入到缓冲区的。
缓冲区实质是一个数组,通常是一个字节数组,缓冲区提供了对数据的结构化访问以及维护读写limit等信息
2.通道channel
通道与流不同之处在于通道是双向的,流知识在一个方向上面移动(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读、写或者二者同时进行。
因为CHannel是双全工的,所以它比流更好的映射底层操作系统的API。特别是在UNIX网络编程模型中,底层的操作系统都是全双工的,同时支持读写操作。
Channel可以分为两大类:用于网络读写的SelectableChannel和用于文件操作的FileChannel。ServerSocket和SocketChannel都是SelectableChannel的子类。
3.多路复用器Selector
多路复用器提供选择已经就绪的任务的能力。简单的说,Selector会不断的轮询地注册在上面的Channel,如果某个 Channel上面发生读或者写事件,这个Channel就会处于就绪状态,会被Selector轮询出来
4.NIO client和NIO server举例
public class NioClient {
public static void client() throws IOException {
// 获取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
FileChannel inChannel = FileChannel.open(Paths.get("C:\\Users\\renyun\\Desktop\\图片\\logo2.png"), StandardOpenOption.READ);
// 分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
// 读取本地文件,并发送到服务端
while (inChannel.read(buf) != -1) {
// 切换到读数据模式
buf.flip();
// 将缓冲区的数据写入管道
sChannel.write(buf);
// 清空缓冲区
buf.clear();
}
//关闭通道
inChannel.close();
sChannel.close();
}
public static void main(String[] args) {
new Thread(() -> {
try {
server();
} catch (IOException e) {
e.printStackTrace();
}
}, "t1").start();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
try {
client();
} catch (IOException e) {
e.printStackTrace();
}
}, "t2").start();
}
}
public class NioServer {
/**
* 服务端
*/
public static void server() throws IOException {
// 获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
FileChannel fileChannel = FileChannel.open(Paths.get("C:\\Users\\renyun\\Desktop\\图片\\logo2.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
// 绑定端口号
ssChannel.bind(new InetSocketAddress(9898));
// 获取客户端连接的通道
SocketChannel socketChannel = ssChannel.accept();
// 分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
// 读取客户端的数据,并保存到本地
while(socketChannel.read(buf) != -1) {
// 切换成读模式
buf.flip();
// 写入
fileChannel.write(buf);
// 清空缓冲区
buf.clear();
}
// 关闭通道
ssChannel.close();
socketChannel.close();
fileChannel.close();
}
}
5为什么不选择原生NIO编程的原因
1.NIO的类库和API繁杂,使用麻烦
2.需要具备其他额外技能,例如熟悉多线程编程,NIO涉及到Reactor模式,必须对多线程和网络编程非常熟悉。
3.可高兴能力不起,工作量和难度都非常大
4.JDK NIO存在bug
2 Netty NIO入门指南
有Netty4和Netty两个版本,最好还是选择Netty4。第一步搭建工程,可以直接引用Maven的依赖包,也可以直接卸载jar包复制到lib目录里。
<!--netty依赖-->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.20.Final</version>
</dependency>
</dependencies>
2.1Netty入门服务端开发
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
try {
bootstrap.group(bossGroup, worker) //设置两个线程组
.channel(NioServerSocketChannel.class) //作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG,128) //设置线程队列等待连接的个数
.childOption(ChannelOption.SO_KEEPALIVE,true) //设置保持活动连接状态
.childHandler(new ChannelInitializer<SocketChannel>() { //创建一个通道初始化对象
//给pipline 设置处理器
@Override
protected void initChannel(SocketChannel channel) throws Exception {
channel.pipeline().addLast(new NettyServerHandler());
}
}); //
System.out.println("服务器已经准备好了");
//绑定一个端口并且同步处理,生成一个ChannelFuture对象
ChannelFuture cf = bootstrap.bind(6668).sync();
//对关闭通道进行监听 异步模型 channelFuture
cf.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
介绍重点类
NioEventLoopGroup 专门用于网络事件的处理,实际上就是Reator线程组,创建两个的原因一个用于服务端接受客户端的连接,另一个用于SocketChannel的网络读写。
ServerBootstrap是Netty用于启动服务端的辅助启动类,目的降低服务端的开发复杂程度,通过调用group方法吧两个线程组当做入参传递到ServerBootstrap里面
NioServerSocketChannel对应的JDK NIO类库里面的ServerSocketChannel类
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
//读取数据实际(读取客户端消息) ChannelHandlerContext上下文对象
// Object msg 客户端发送的消息 默认是object形式的
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("server ctx ="+ctx);
ByteBuf buf = (ByteBuf) msg;
System.out.println("客户端发送消息"+buf.toString(CharsetUtil.UTF_8));
System.out.println("客户端地址"+ctx.channel().remoteAddress());
// super.channelRead(ctx, msg);
//将msg转成bytebuffer
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("hello 客户端",CharsetUtil.UTF_8));
}
//处理异常,一般是关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
// super.exceptionCaught(ctx, cause);
}
}
NettyServerHandler 继承自ChannelInboundHandlerAdapter ,对网络事件进行读写操作;
重点关注channelRead和exceptionCaught方法。
ByteBuf buf = (ByteBuf) msg;将msg转为Netty 的ByteBuf对象。ByteBuf的方法可以获取缓冲区的字节数
通过上述的代码可以看出,相比较于传统的JDK NIO原生类库的服务端,代码量大大减少,开发量减少了很多。
2.2Netty客户端开发
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
//客户端需要仅一个事件循环组
EventLoopGroup eventExecutors = new NioEventLoopGroup();
try{
//创建客户端启动对象
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventExecutors)
.channel(NioSocketChannel.class) //设置客户端通道的处理
.handler(new ChannelInitializer<SocketChannel>() { //创建一个通道初始化对象
//给pipline 设置处理器
@Override
protected void initChannel(SocketChannel channel) throws Exception {
channel.pipeline().addLast(new NettyClientHandler());
}
});
System.out.println("客户端已经好了");
ChannelFuture cf = bootstrap.connect("127.0.0.1", 6668).sync();
cf.channel().closeFuture().sync();
}finally {
eventExecutors.shutdownGracefully();
}
}
}
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
//通道就绪就会触发该方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client" +ctx);
ctx.writeAndFlush(Unpooled.copiedBuffer("HELLO SERVER", CharsetUtil.UTF_8));
}
//当通道有读取事件时会触发
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println("服务器回复的消息"+buf.toString(CharsetUtil.UTF_8));
System.out.println("服务器端的地址"+ctx.channel().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
客户端的服务相较于服务端较为简单;
2.3运行和调试
需要指出的是,本例程没有考虑读半包的处理,对于功能演示或者测试没有问题,但是如果进行压力测试,就不能正常工作,就需要处理半包相关的知识点。利用一些方法来解决TCP粘包和拆包。