初识Netty — 实现简单的C/S通信

写在前面:
Netty是Java的网络编程框架,既然是框架的学习,不免会碰到很多分支的知识和不熟悉的名词。这就需要不断的做“下潜”,耐心搜索,不求甚解,等到大致熟悉之后再去逐一深究。因此有些概念作者也不能做出详细解释,请参考贴出的相关文章或自行搜索以解决疑惑。

什么是Netty

网上很多文章都有作解释。以作者的使用体验来说,Netty是封装了 Java socket nio 来进行网络编程的工具。说到网络编程,大二软工的软件工程实训就有这个小课题,当时作者是用Java socket io来写,还没用到nio呢,就是参照网上的例子手动模拟通信过程,自己用最简单的 阻塞I/O 的模式写了一个Thread类来处理所有不同种类的请求,由于需求简单,尚能完成。 想要模拟效果更自然一点就要用 非阻塞I/O 模式,而nio就是用来写非阻塞I/O的api。但是nio的编写对java程序员是有比较高的要求的。Netty就可以简化这一系列操作。

预备知识

贴几个比较靠谱的博客,不求甚解,大致了解一下就好。
关于NIO:
https://www.jianshu.com/p/3cec590a122f (推荐,也包括I/O模型)
https://my.oschina.net/andylucc/blog/614295
关于I/O模型:
https://segmentfault.com/a/1190000003063859

开发环境

java JDK1.8 + IDEA + maven + Netty 4.1.6
maven依赖:

 <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.6.Final</version>
 </dependency>

实现功能

C/S通信:C是客户端,S是服务端。在IDEA控制台开启服务端接收客户端的信息String, 并返回一个“hi!”+String,客户端收到服务端的信息后在控制台上输出。

代码讲解

分为服务端和客户端两部分,各自又有一个处理连接逻辑的代码

服务端代码

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;


public class EchoServer {
    private final int port;  //1.设置服务端端口

    public EchoServer(int port) {
        this.port = port;
    }

    public void start() throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();  //2.创建 EventLoopGroup
        try{
            /*
             * 客户端的是Bootstrap,服务端的则是    ServerBootstrap。
             **/
            ServerBootstrap sbs = new ServerBootstrap();  //3.创建 ServerBootstrap
            sbs.group(group)
                    .channel(NioServerSocketChannel.class)  //4.指定使用 NIO 的传输 Channel
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline ph = ch.pipeline();
                            /*
                            * Netty中的编码/解码器,通过他你能完成字节与pojo、pojo与pojo的相互转换,
                            * 从而达到自定义协议的目的。
                            * 下面是以("\n")为结尾分割的 解码器
                            * */
                            ph.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()))
                                    .addLast("decoder", new StringDecoder())
                                    .addLast("encoder", new StringEncoder())  //解码和编码,应和客户端一致
                                    .addLast("handler", new EchoServerHandler());  //5.添加 EchoServerHandler 到 Channel 的 ChannelPipeline

                        }
                    });

            ChannelFuture cf = sbs.bind(this.port).sync();  //6.设置socket地址使用所选的端口 并且 绑定的服务器,sync 等待服务器关闭

            System.out.println("服务端启动成功...");

            cf.channel().closeFuture().sync();  //7.关闭 channel 和 块,直到它被关闭

        }finally {
            group.shutdownGracefully();  //8.关闭 EventLoopGroup,释放所有资源
        }
    }

    public static void main(String[] args) throws Exception{
        new EchoServer(65535).start();
    }
}

注意这一段代码:

这是关键所在,其余代码基本上是套路代码,按部就班写就可以。
关键在于此处出现了:

  • 用ChannelPipeline引用了SocketChannel的pipeline,原因在于ChannelPipeline是用于存放ChannelHandler的容器,而接下来的解码编码操作和自定义的逻辑处理类都要涉及到ChannelHandler的子类
    它们之间的关系可以用下图表示:

  • Encoder(编码器)和Decoder(解码器),属于Codec框架的内容,大致意思是:此处描述了服务端和客户端之间传输了什么类型的数据,这里要传输String就用到了StringDecoder/Encode 当然也可以传输其他类型的数据,详情参考这篇博客:https://www.jianshu.com/p/fd815bd437cd

  • 注释5.处的EchoServerHandler是自定义的类,可以看作是一种“规则”,规定了服务端以什么方式处理客户端发来的数据。

服务端处理连接的代码

import java.net.InetAddress;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class EchoServerHandler extends SimpleChannelInboundHandler<String> {
    /*
     * 收到消息时,返回信息
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg)
            throws Exception {
        System.out.println("服务端接受的消息 : " + msg);  // 收到消息直接打印输出
        if("quit".equals(msg)){  //服务端断开的条件
            ctx.close();
        }
        ctx.writeAndFlush("hi! "+msg+"\n");  // 返回客户端消息
    }
    /*
     * 建立连接时,返回消息
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("连接的客户端地址:" + ctx.channel().remoteAddress());
        ctx.writeAndFlush("客户端"+ InetAddress.getLocalHost().getHostName() + "成功与服务端建立连接! \n");
        super.channelActive(ctx);
    }
}

客户端代码

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Scanner;

public class EchoClient {
    private final String host;  //ip地址
    private final int port;     //端口

    public EchoClient() {
        this(0);
    }

    public EchoClient(int port) {
        this("localhost", port);
    }

    public EchoClient(String host, int port) {
         this.host = host;
         this.port = port;
    }

    public void start() throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();  //1.创建 EventLoopGroup
        try{
            /**
             * Netty创建全部都是实现自AbstractBootstrap。
             * 客户端的是Bootstrap,服务端的则是    ServerBootstrap。
             **/
            Bootstrap bs = new Bootstrap();  //2.创建 Bootstrap

            System.out.println("客户端成功启动...");

            bs.group(group)  //3.指定 NioEventLoopGroup 来处理客户端事件。
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {  //4.指定使用 NIO 的传输 Channel
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline ph = ch.pipeline();
                            ph.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()))
                                    .addLast("decoder", new StringDecoder())
                                    .addLast("encoder", new StringEncoder())  // 解码和编码,应和服务端一致
                                    .addLast("handler", new EchoClientHandler());  //5.当建立一个连接和一个新的通道时,创建添加到 EchoClientHandler 实例 到 channel pipeline
                        }
                    });
            Channel ch = bs.connect(this.host, this.port).sync().channel();  //6.设置服务器的ip和端口,并且连接到远程; 等待连接完成

            Scanner in=new Scanner(System.in);

            while(true){
                System.out.println("请输入要发送的信息:");
                String str=in.next();
                //连接后发送数据
                ch.writeAndFlush(str+ "\r\n");
                System.out.println("客户端发送数据:"+str);
                if (str.equals("quit"))break;
            }

            System.exit(0);

        }finally {
            group.shutdownGracefully();  //8.关闭线程池和释放所有资源
        }
    }

    public static void main(String[] args) throws Exception{
        new EchoClient("127.0.0.1", 65535).start();
    }
}

客户端和服务端代码样式基本一致,有几个关键点都已注释

此处以不断向客户端发送信息,输入“quit”终止连接。

运行效果

先运行服务端再运行客户端
服务端:

客户端1:

客户端2:

参考文章:

https://www.cnblogs.com/liuming1992/p/4758532.html
https://blog.csdn.net/qazwsxpcm/article/details/77750865
https://www.jianshu.com/p/b9f3f6a16911

发表评论

电子邮件地址不会被公开。 必填项已用*标注