NIO介绍与Netty通信简单入门

1 篇文章 0 订阅

NIO同步阻塞与同步非阻塞

BIO与NIO

IO(BIO)和NIO区别:其本质就是阻塞和非阻塞的区别

阻塞概念:应用程序在获取网络数据的时候,如果网络传输数据很慢,就会一直等待,直到传输完毕为止。

非阻塞概念:应用程序直接可以获取已经准备就绪好的数据,无需等待。

IO为同步阻塞形式,NIO为同步非阻塞形式,NIO并没有实现异步,在JDK1.7后升级NIO库包,支持异步非阻塞

同学模型NIO2.0(AIO)

BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。 
NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。 
AIO(NIO.2):异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。 

 

同步时,应用程序会直接参与IO读写操作,并且我们的应用程序会直接阻塞到某一个方法上,直到数据准备就绪:

或者采用轮训的策略实时检查数据的就绪状态,如果就绪则获取数据.

异步时,则所有的IO读写操作交给操作系统,与我们的应用程序没有直接关系,我们程序不需要关系IO读写,当操作

系统完成了IO读写操作时,会给我们应用程序发送通知,我们的应用程序直接拿走数据极即可。

伪异步

由于BIO一个客户端需要一个线程去处理,因此我们进行优化,后端使用线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大的线程数N的比例关系,其中M可以远远大于N,通过线程池可以灵活的调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。

原理:

当有新的客户端接入时,将客户端的Socket封装成一个Task(该Task任务实现了java的Runnable接口)投递到后端的线程池中进行处理,由于线程池可以设置消息队列的大小以及线程池的最大值,因此,它的资源占用是可控的,无论多少个客户端的并发访问,都不会导致资源的耗尽或宕机。

  

使用多线程支持多个请求

服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善

//tcp服务器端...

class TcpServer {



      public static void main(String[] args) throws IOException {

           System.out.println("socket tcp服务器端启动....");

           ServerSocket serverSocket = new ServerSocket(8080);

           // 等待客户端请求

           try {

                 while (true) {

                      Socket accept = serverSocket.accept();

                      new Thread(new Runnable() {



                            @Override

                            public void run() {

                                  try {

                                       InputStream inputStream = accept.getInputStream();

                                       // 转换成string类型

                                       byte[] buf = new byte[1024];

                                       int len = inputStream.read(buf);

                                       String str = new String(buf, 0, len);

                                       System.out.println("服务器接受客户端内容:" + str);

                                  } catch (Exception e) {

                                       // TODO: handle exception

                                  }



                            }

                      }).start();



                 }

           } catch (Exception e) {

                 e.printStackTrace();

           } finally {

                 serverSocket.close();

           }



      }



}



public class TcpClient {

      public static void main(String[] args) throws UnknownHostException, IOException {

           System.out.println("socket tcp 客户端启动....");

           Socket socket = new Socket("127.0.0.1", 8080);

           OutputStream outputStream = socket.getOutputStream();

           outputStream.write("我是蚂蚁课堂".getBytes());

           socket.close();

      }

}

 

使用线程池管理线程

//tcp服务器端...

class TcpServer {

    

      public static void main(String[] args) throws IOException {

           ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

           System.out.println("socket tcp服务器端启动....");

           ServerSocket serverSocket = new ServerSocket(8080);

           // 等待客户端请求

           try {

                 while (true) {

                      Socket accept = serverSocket.accept();

                      //使用线程

                      newCachedThreadPool.execute(new Runnable() {



                            @Override

                            public void run() {

                                  try {

                                       InputStream inputStream = accept.getInputStream();

                                       // 转换成string类型

                                       byte[] buf = new byte[1024];

                                       int len = inputStream.read(buf);

                                       String str = new String(buf, 0, len);

                                       System.out.println("服务器接受客户端内容:" + str);

                                  } catch (Exception e) {

                                       // TODO: handle exception

                                  }



                            }

                      });

                     



                 }

           } catch (Exception e) {

                 e.printStackTrace();

           } finally {

                 serverSocket.close();

           }



      }



}



public class TcpClient {

      public static void main(String[] args) throws UnknownHostException, IOException {

           System.out.println("socket tcp 客户端启动....");

           Socket socket = new Socket("127.0.0.1", 8080);

           OutputStream outputStream = socket.getOutputStream();

           outputStream.write("我是蚂蚁课堂".getBytes());

           socket.close();

      }

}

  

IO模型关系

什么是阻塞

阻塞概念:应用程序在获取网络数据的时候,如果网络传输很慢,那么程序就一直等着,直接到传输完毕。

什么是非阻塞

应用程序直接可以获取已经准备好的数据,无需等待.

IO为同步阻塞形式,NIO为同步非阻塞形式。NIO没有实现异步,在JDK1.7之后,升级了NIO库包

,支持异步费阻塞通讯模型NIO2.0(AIO)

 

Netty快速入门

什么是Netty

 Netty 是一个基于 JAVA NIO 类库的异步通信框架,它的架构特点是:异步非阻塞、基于事件驱动、高性能、高可靠性和高可定制性。

Netty应用场景

1.分布式开源框架中dubbo、Zookeeper,RocketMQ底层rpc通讯使用就是netty。

2.游戏开发中,底层使用netty通讯。

为什么选择netty

在本小节,我们总结下为什么不建议开发者直接使用JDK的NIO类库进行开发的原因:

1)      NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等;

2)      需要具备其它的额外技能做铺垫,例如熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序;

3)      可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大;

4)      JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题,但是直到JDK1.7版本该问题仍旧存在,只不过该bug发生概率降低了一些而已,它并没有被根本解决。

 

Netty通信实践

 

Netty服务器端​

package com.xiaofeng.netty.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
 * @author xiaofeng
 * @version V1.0
 * @title: NettyServer
 * @package: com.xiaofeng.netty.server
 * @description: netty server
 * @date 2019/12/17 11:24
 */
public class NettyServer {
    private static class SingletionNettyServer {
        static final NettyServer instance = new NettyServer();
    }

    public static NettyServer getInstance() {
        return SingletionNettyServer.instance;
    }

    private EventLoopGroup mainGroup;
    private EventLoopGroup subGroup;
    private ServerBootstrap server;
    private ChannelFuture future;

    public NettyServer() {
        //主线程组,用于接受客户端的连接,但是不做任何处理,跟老板一样,不做事
        mainGroup = new NioEventLoopGroup();
        //从线程组, 老板线程组会把任务丢给他,让手下线程组去做任务
        subGroup = new NioEventLoopGroup();
        //server启动类
        server = new ServerBootstrap();
        //建立联系
        server.group(mainGroup, subGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new NettyServerInitialzer());
    }

    public void start() {
        this.future = server.bind(9999);
        System.err.println("netty server 启动完毕...");
    }

    public static void main(String[] args) {
        NettyServer.getInstance().start();
    }
}
package com.xiaofeng.netty.server;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

/**
 * @author xiaofeng
 * @version V1.0
 * @title: NettyServerInitialzer
 * @package: com.xiaofeng.netty.server
 * @description: 服务端初始化器
 * @date 2019/12/17 11:30
 */
public class NettyServerInitialzer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        pipeline.addLast("stringD", new StringDecoder());
        pipeline.addLast("stringC", new StringEncoder());
        pipeline.addLast("http", new HttpClientCodec());

        // 自定义的handler
        pipeline.addLast(new ServerChatHandler());
    }

}
package com.xiaofeng.netty.server;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;

/**
 * @author xiaofeng
 * @version V1.0
 * @title: ServerChatHandler
 * @package: com.xiaofeng.netty.server
 * @description: 处理消息的handler
 * @date 2019/12/17 11:38
 */
public class ServerChatHandler extends SimpleChannelInboundHandler<String> {

    // 用于记录和管理所有客户端的channle
    public static ChannelGroup users =
            new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String s)
            throws Exception {
        Channel channel = ctx.channel();
        for (Channel ch : users) {
            if (ch == channel) {
                System.out.println("收到来自客户端[" + channel.id().asShortText() + "]的消息:" + s);
                ctx.writeAndFlush("[你说]:" + s + "\n");
            } else {
                ctx.writeAndFlush("[" + channel.remoteAddress() + "]" + s + "\n");
            }
        }
    }

    /**
     * 当客户端连接服务端之后(打开连接)
     * 获取客户端的channle,并且放到ChannelGroup中去进行管理
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        String channelId = ctx.channel().id().asShortText();
        System.out.println("客户端加入,channelId为:" + channelId);
        users.add(ctx.channel());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        String channelId = ctx.channel().id().asShortText();
        System.out.println("客户端被移除,channelId为:" + channelId);

        // 当触发handlerRemoved,ChannelGroup会自动移除对应客户端的channel
        users.remove(ctx.channel());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        // 发生异常之后关闭连接(关闭channel),随后从ChannelGroup中移除
        ctx.channel().close();
        users.remove(ctx.channel());
    }
}

 

Netty客户端

 

package com.xiaofeng.netty.client;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

/**
 * @author xiaofeng
 * @version V1.0
 * @title: NettyClient
 * @package: com.xiaofeng.netty.client
 * @description: netty client
 * @date 2019/12/17 11:14
 */
public class NettyClient {
    private static class SingletionNettyClient {
        static final NettyClient instance = new NettyClient();
    }

    public static NettyClient getInstance() {
        return SingletionNettyClient.instance;
    }

    private EventLoopGroup eventGroup;
    private Bootstrap client;

    public NettyClient() {
        eventGroup = new NioEventLoopGroup();
        //创建客户端启动类
        client = new Bootstrap();
        client.group(eventGroup)
                .channel(NioSocketChannel.class)
                .handler(new NettyClientInitialzer());
    }

    public void start() throws InterruptedException, IOException {
        Channel channel = client.connect("127.0.0.1", 9999).sync().channel();
        while (true) {
            BufferedReader reader = new BufferedReader(
                    new InputStreamReader(System.in));
            String input = reader.readLine();
            if (input != null) {
                if ("quit".equals(input)) {
                    System.exit(1);
                }
                channel.writeAndFlush(input);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException, IOException {
        NettyClient.getInstance().start();

    }
}

 

package com.xiaofeng.netty.client;

import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

/**
 * @author xiaofeng
 * @version V1.0
 * @title: NettyClientInitialzer
 * @package: com.xiaofeng.netty.client
 * @description: 客户端初始化器
 * @date 2019/12/17 11:29
 */
public class NettyClientInitialzer extends ChannelInitializer<Channel> {

    @Override
    protected void initChannel(Channel channel) throws Exception {
        //获取管道
        ChannelPipeline pipeline = channel.pipeline();
        //添加编解码器
        pipeline.addLast("stringD", new StringDecoder());
        pipeline.addLast("stringC", new StringEncoder());
        pipeline.addLast("http", new HttpClientCodec());
        // 自定义的handler
        pipeline.addLast(new ClientChatHandler());
    }
}
package com.xiaofeng.netty.client;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

/**
 * @author xiaofeng
 * @version V1.0
 * @title: ClientChatHandler
 * @package: com.xiaofeng.netty.client
 * @description: 处理消息的handler
 * @date 2019/12/17 11:38
 */
public class ClientChatHandler extends SimpleChannelInboundHandler<String> {


    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String s)
            throws Exception {
        System.out.println(s);
    }

}

TCP粘包、拆包问题解决方案

什么是粘包/拆包

   一个完整的业务可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这个就是TCP的拆包和封包问题。

下面可以看一张图,是客户端向服务端发送包:

1. 第一种情况,Data1和Data2都分开发送到了Server端,没有产生粘包和拆包的情况。

2. 第二种情况,Data1和Data2数据粘在了一起,打成了一个大的包发送到Server端,这个情况就是粘包。

3. 第三种情况,Data2被分离成Data2_1和Data2_2,并且Data2_1在Data1之前到达了服务端,这种情况就产生了拆包。

由于网络的复杂性,可能数据会被分离成N多个复杂的拆包/粘包的情况,所以在做TCP服务器的时候就需要首先解决拆包/

解决办法

     1.消息定长,报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分。


sc.pipeline().addLast(new FixedLengthFrameDecoder(10));

2.包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分。

ByteBuf buf = Unpooled.copiedBuffer("_mayi".getBytes());

sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));

将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值