Netty-快速入门

----------------------------------------------------

netty是什么?

Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.
netty是一个异步的事件驱动(不同的阶段,对应不同的回调方法)的网络框架维护着高性能协议的服务器端和客户端的快速开发。

Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.
Netty是一个非阻塞的io客户端服务端的框架可以快速并且简单的开发网络应用比如说客户端和服务端的协议。它极大的简化了网络编程流程比如说tcp或者udp socket服务器。

'Quick and easy' doesn't mean that a resulting application will suffer from a maintainability or a performance issue. Netty has been designed carefully with the experiences earned from the implementation of a lot of protocols such as FTP, SMTP, HTTP, and various binary and text-based legacy protocols. As a result, Netty has succeeded to find a way to achieve ease of development, performance, stability, and flexibility without a compromise.
快速和简单并不意味着由此产生的应用程序将要遭受到可维护性或者性能问题的困扰。Netty精简的设计从一些的协议比如说FTP,STMP,HTTP和一些基于二进制的传统协议获取的经验。因此,Netty成功的发现一种方式去实现轻松的开发,性能,稳定和灵妥协。活性而不需要任何的.

特征(Features)

设计

Unified API for various transport types - blocking and non-blocking socket
统一的api基于不同的传输类型-阻塞和非阻塞的socket.
Based on a flexible and extensible event model which allows clear separation of concerns.
基于灵活的可扩展的时间模型,允许明确的关注分离.
Highly customizable thread model - single thread, one or more thread pools such as SEDA
高度可定制的线程模型-单线程,一个或多个线程池比如说SEDA.
SEDA(Staged Event-Driven Architecture)的核心思想是把一个请求处理过程分成几个Stage,不同资源消耗的Stage使用不同数量的线程来处理,Stage间使用事件驱动的异步通信模式。

True connectionless datagram socket support (since 3.1).
真正的无连接的数据报socket支持(基于3.1版本).

使用简单

  1. Well-documented Javadoc, user guide and examples
    详细的用户java文档,用户指南和demo
  2. No additional dependencies, JDK 5 (Netty 3.x) or 6 (Netty 4.x) is enough
    不需要额外的依赖,JDK 5 (Netty 3.x版本) 或者 6 (Netty 4.x版本)就足够了
  3. Note: Some components such as HTTP/2 might have more requirements. Please refer to the Requirements page for more information.
    注意:一些组件比如说HTTP/2可能需要一些额外的依赖。

性能

  1. Better throughput, lower latency
    更好的吞吐量,低延迟
  2. Less resource consumption
    资源消耗减少
  3. Minimized unnecessary memory copy.
    不必要的内存拷贝(零拷贝).

安全

Complete SSL/TLS and StartTLS support。
完全的SSl/tls 和 StartTLS的支持。

 
netty架构图
  1. core(核心模块):Extensible Event Model(可扩展的事件模型),Universal Communication API(通用的通讯API),Zero-Copy-Capable Rich Byte Buffer(零拷贝的字节缓冲区)
  2. Transport Services(传输服务): Socket & Datagram,HTTp Tunnel,In-Vm Pipe
  3. Protocol Support(协议支持): HTTP & WebSocket,SSl.StartTLS,Google Protobuf,zlib/gzip Compression,Large File Transfer,RTSP(和流媒体有关),Legacy Text.Binary Protocols with Unit Testability

----------------------------------------------------

Netty笔记之二:使用Netty构建http服务

直接看代码:

服务端:

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;

public class TestServer {
    public static void main(String[] args) throws Exception{
        //bossGroup是获取连接的,workerGroup是用来处理连接的,这二个线程组都是死循环
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try{
            //简化服务端启动的一个类
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            //group有二个重载方法,一个是接收一个EventLoopGroup类型参数的方法,一个是接收二个EventLoopGroup类型的参数的方法
            serverBootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).
                    childHandler(new TestServerInitializer());

            ChannelFuture  channelFuture = serverBootstrap.bind(8899).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }
}

服务端HttpServerInitializer(初始化连接的时候执行的回调),处理器Handler构成了一个链路

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;


public class TestServerInitializer extends ChannelInitializer<SocketChannel>{

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

        //http协议的编解码使用的,是HttpRequestDecoder和HttpResponseEncoder处理器组合
        //HttpRequestDecoder http请求的解码
        //HttpResponseEncoder http请求的编码
        pipeline.addLast("httpServerCodec",new HttpServerCodec());
        pipeline.addLast("testHttpServerHandler",new TestHttpServerHandler());
    }
}

服务端Handler,对请求进行路由

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;

import java.net.URI;

//浏览器的特性会发送一个/favicon.ico请求,获取网站的图标
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject>{

    //channelRead0是读取客户端的请求并且向客户端返回响应的一个方法
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
        
        if(msg instanceof HttpRequest){
            HttpRequest httpRequest = (HttpRequest)msg;
            System.out.println("请求方法名:"+httpRequest.method().name()); //get方法

            URI uri = new URI(httpRequest.uri());
            //使用浏览器访问localhost:8899会发送二次请求,其中有一次是localhost:8899/favicon.ico,这个url请求访问网站的图标
            if("/favicon.ico".equals(uri.getPath())){
                System.out.println("请求favicon.ico");
                return;
            }
            //向客户端返回的内容
            ByteBuf content = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
                    HttpResponseStatus.OK,content);

            response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/plain");
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH,content.readableBytes());

            ctx.writeAndFlush(response);

            //其实更合理的close连接应该判断是http1.O还是1.1来进行判断请求超时时间来断开channel连接。
            ctx.channel().close();
        }
    }
}

使用curl命令访问该服务:

➜ curl 'localhost:8899'
hello world%

控制台打印:

请求方法名:GET

相应的使用post,put,delete请求服务
post请求

curl -X post 'localhost:8899'

put请求

curl -X put 'localhost:8899'

delete请求

curl -X delete 'localhost:8899'

我们使用谷歌浏览器访问localhost:8899,打开开发者工具的时候发现还会请求一次/favicon.ico(请求当前网站的图标)。此时控制台打印

请求方法名:GET
请求方法名:GET
请求favicon.ico

扩展

我们改造一下TestHttpServerHandler代码,因为netty是事件驱动的网络框架,我们定义的TestHttpServerHandler继承SimpleChannelInboundHandler类,SimpleChannelInboundHandler类又继承ChannelInboundHandlerAdapter类,发现ChannelInboundHandlerAdapter中定义了好多事件回调方法,在不同的事件触发时会执行相应的方法,我们在TestHttpServerHandler重写这些方法来梳理一下netty的事件回调流程。

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;

import java.net.URI;

//浏览器的特性会发送一个/favicon.ico请求,获取网站的图标
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject>{

    //channelRead0是读取客户端的请求并且向客户端返回响应的一个方法
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
        //io.netty.handler.codec.http.DefaultHttpRequest,io.netty.handler.codec.http.LastHttpContent$1
        System.out.println(msg.getClass());

        //打印出远程的地址,/0:0:0:0:0:0:0:1: 49734,本地线程的49734端口的线程和netty进行通信
        System.out.println(ctx.channel().remoteAddress());
        TimeUnit.SECONDS.sleep(8);

        if(msg instanceof HttpRequest){
            HttpRequest httpRequest = (HttpRequest)msg;
            System.out.println("请求方法名:"+httpRequest.method().name()); //get方法

            URI uri = new URI(httpRequest.uri());
            //使用浏览器访问localhost:8899会发送二次请求,其中有一次是localhost:8899/favicon.ico,这个url请求访问网站的图标
            if("/favicon.ico".equals(uri.getPath())){
                System.out.println("请求favicon.ico");
                return;
            }

            //向客户端返回的内容
            ByteBuf content = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
                    HttpResponseStatus.OK,content);

            response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/plain");
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH,content.readableBytes());

            ctx.writeAndFlush(response);

            //其实更合理的close连接应该判断是http1.O还是1.1来进行判断请求超时时间来断开channel连接。
            ctx.channel().close();
        }
    }


    //管道活跃
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channel active");
        super.channelActive(ctx);
    }

    //管道注册
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channel registered");
        super.channelRegistered(ctx);
    }

    //通道被添加
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        System.out.println("handler added");
        super.handlerAdded(ctx);
    }

    //管道不活跃了
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channel inactive");
        super.channelInactive(ctx);
    }

    //取消注册
    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channel unregistered");
        super.channelUnregistered(ctx);
    }
}

使用curl命令访问该服务:

➜ curl 'localhost:8899'
hello world%

控制台显示,我们发现handlerAdded(通道被添加),channelRegistered(通道被注册),channelActive(管道活跃)然后通道关闭之后依次执行channelInactive(管道不活跃了),channelUnregistered(通道取消注册)

handler added
channel registered
channel active
class io.netty.handler.codec.http.DefaultHttpRequest
/0:0:0:0:0:0:0:1:49734
请求方法名:GET
class io.netty.handler.codec.http.LastHttpContent$1
/0:0:0:0:0:0:0:1:49734
channel inactive
channel unregistered

我们发现49734发起了调用8899端口的服务,可以使用命令来查看

➜ lsof -i:8899
COMMAND   PID          USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
java    56283 naeshihiroshi  106u  IPv6 0x72f1692aa9c77fbf      0t0  TCP *:8899 (LISTEN)
java    56283 naeshihiroshi  109u  IPv6 0x72f1692aaca7cfbf      0t0  TCP localhost:8899->localhost: 49734 (ESTABLISHED)
curl    56649 naeshihiroshi    5u  IPv6 0x72f1692aabcfa53f      0t0  TCP localhost: 49734->localhost:8899 (ESTABLISHED)

----------------------------------------------------

Netty笔记之三:Netty实现Socket编程

 

加入依赖:

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

我是使用gradle构建项目的,所以build.gradle如下:

compile (
    'io.netty:netty-all:4.1.10.Final'
)

服务器代码

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;
import io.netty.handler.logging.LoggingHandler;

public class MyServer {
    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup wokerGroup = new NioEventLoopGroup();

        try{
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            //在服务器端的handler()方法表示对bossGroup起作用,而childHandler表示对wokerGroup起作用
            serverBootstrap.group(bossGroup,wokerGroup).channel(NioServerSocketChannel.class)
                    .childHandler(new MyServerInitializer());

            ChannelFuture channelFuture = serverBootstrap.bind(8899).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            wokerGroup.shutdownGracefully();
        }
    }
}

服务器端初始化Initializer

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;


public class MyServerInitializer extends ChannelInitializer<SocketChannel>{
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4));
        pipeline.addLast(new LengthFieldPrepender(4));
        //字符串解码
        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
        //字符串编码
        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
        //自己定义的处理器
        pipeline.addLast(new MyServerHandler());
    }
}

服务器端自定义Handler:

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

import java.util.UUID;

public class MyServerHandler extends SimpleChannelInboundHandler<String>{

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        //打印出客户端地址
        System.out.println(ctx.channel().remoteAddress()+", "+msg);
        ctx.channel().writeAndFlush("form server: "+ UUID.randomUUID());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

客户端:

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

public class MyClient {
    public static void main(String[] args) throws Exception{
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();

        try{
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
                    .handler(new MyClientInitializer());

            ChannelFuture channelFuture = bootstrap.connect("localhost",8899).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            eventLoopGroup.shutdownGracefully();
        }
    }
}

客户端初始化Initializer

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;

public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4));
        pipeline.addLast(new LengthFieldPrepender(4));
        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
        pipeline.addLast(new MyClientHandler());
    }
}

客户端自定义Handler:

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

import java.time.LocalDateTime;


public class MyClientHandler extends SimpleChannelInboundHandler<String>{

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        //服务端的远程地址
        System.out.println(ctx.channel().remoteAddress());
        System.out.println("client output: "+msg);
        ctx.writeAndFlush("from client: "+ LocalDateTime.now());
    }

    /**
     * 当服务器端与客户端进行建立连接的时候会触发,如果没有触发读写操作,则客户端和客户端之间不会进行数据通信,也就是channelRead0不会执行,
     * 当通道连接的时候,触发channelActive方法向服务端发送数据触发服务器端的handler的channelRead0回调,然后
     * 服务端向客户端发送数据触发客户端的channelRead0,依次触发。
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush("来自与客户端的问题!");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

启动客户端和服务端,服务端控制台:

/127.0.0.1:49645, 来自与客户端的问题!
/127.0.0.1:49645, from client: 2017-10-09T23:44:42.493
/127.0.0.1:49645, from client: 2017-10-09T23:44:42.495
/127.0.0.1:49645, from client: 2017-10-09T23:44:42.496
...

客户端控制台:

localhost/127.0.0.1:8899
client output: form server: ca79614c-351c-4aed-9906-532fb19f8286
localhost/127.0.0.1:8899
client output: form server: c66482ff-503b-41e8-a2b1-0de88e856433
localhost/127.0.0.1:8899
...

流程分析
当客户端和服务端通道建立连接的时候(触发channelActive方法),客户端向服务发送来“自与客户端的问题!”数据,服务器端channelRead0被触发,进行回写数据到客户端触发客户端的channelRead0回调方法被调用,依次往复。

----------------------------------------------------

Netty笔记之四:使用Netty实现多客户端连接并且互相通信

 
 

使用netty实现多客户端连接并且互相通信的需求:

1.服务器启动,n多个客户端与服务器进行连接,一个客户端上线之后,服务器端控制台会打印xx上线了,其他的客户端控制台打印xx上线了。如果一个客户端下线了,服务器端的控制台上打印,xx下线了,其他的客户端控制台打印xx下线了。

2.多个客户端都上线之后,一个客户端(比如说A)给服务端发送消息,那么客户端(比如说A,B,C,包括自己本身)都会收到消息,对于A来说,会标志此消息是自己发送自己的,其他的客户端则会收到具体的消息。

服务器代码:

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;

public class MyChatServer {
    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup wokerGroup = new NioEventLoopGroup();

        try{
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup,wokerGroup).channel(NioServerSocketChannel.class)
                    .childHandler(new MyChatServerInializer());

            ChannelFuture channelFuture = serverBootstrap.bind(8899).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            wokerGroup.shutdownGracefully();
        }
    }
}

服务器端初始化Initializer

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
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 io.netty.util.CharsetUtil;

public class MyChatServerInializer extends ChannelInitializer<SocketChannel> {

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

        //分割接收到的Bytebu,根据指定的分割符
        pipeline.addLast(new DelimiterBasedFrameDecoder(4096, Delimiters.lineDelimiter()));
        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
        pipeline.addLast(new MyChatServerHandler());
    }
}

自定义Handler:

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;

public class MyChatServerHandler extends SimpleChannelInboundHandler<String>{

    //保留所有与服务器建立连接的channel对象,这边的GlobalEventExecutor在写博客的时候解释一下,看其doc
    private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    /**
     * 服务器端收到任何一个客户端的消息都会触发这个方法
     * 连接的客户端向服务器端发送消息,那么其他客户端都收到此消息,自己收到【自己】+消息
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        Channel channel = ctx.channel();

        channelGroup.forEach(ch -> {
            if(channel !=ch){
                ch.writeAndFlush(channel.remoteAddress() +" 发送的消息:" +msg+" \n");
            }else{
                ch.writeAndFlush(" 【自己】"+msg +" \n");
            }
        });
    }


    //表示服务端与客户端连接建立
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();  //其实相当于一个connection

        /**
         * 调用channelGroup的writeAndFlush其实就相当于channelGroup中的每个channel都writeAndFlush
         *
         * 先去广播,再将自己加入到channelGroup中
         */
        channelGroup.writeAndFlush(" 【服务器】 -" +channel.remoteAddress() +" 加入\n");
        channelGroup.add(channel);
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        channelGroup.writeAndFlush(" 【服务器】 -" +channel.remoteAddress() +" 离开\n");

        //验证一下每次客户端断开连接,连接自动地从channelGroup中删除调。
        System.out.println(channelGroup.size());
        //当客户端和服务端断开连接的时候,下面的那段代码netty会自动调用,所以不需要人为的去调用它
        //channelGroup.remove(channel);
    }

    //连接处于活动状态
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        System.out.println(channel.remoteAddress() +" 上线了");
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        System.out.println(channel.remoteAddress() +" 下线了");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

客户端代码:

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.InputStreamReader;

public class MyChatClient {
    public static void main(String[] args) throws Exception{
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();

        try{
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
                    .handler(new MyChatClientInitializer());

            Channel channel = bootstrap.connect("localhost",8899).sync().channel();

            //标准输入
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));

            //利用死循环,不断读取客户端在控制台上的输入内容
            for (;;){
                channel.writeAndFlush(bufferedReader.readLine() +"\r\n");
            }

        }finally {
            eventLoopGroup.shutdownGracefully();
        }
    }
}

客户端初始化Initializer

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
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 io.netty.util.CharsetUtil;


public class MyChatClientInitializer extends ChannelInitializer<SocketChannel>{

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

        pipeline.addLast(new DelimiterBasedFrameDecoder(4096, Delimiters.lineDelimiter()));
        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
        pipeline.addLast(new MyChatClientHandler());
    }
}

客户端Handler:

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

public class MyChatClientHandler extends SimpleChannelInboundHandler<String> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        System.out.println(msg);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

启动服务器,启动第一个客户端的时候,服务器端控制台打印

/127.0.0.1:54771 上线了

第二个客户端启动,则服务器控制台打印,

/127.0.0.1:54771 上线了
/127.0.0.1:54792 上线了

此时第一个客户端打印,
【服务器】 -/127.0.0.1:54792 加入

启动第三个客户端依次可以自己验证,当我在客户端1控制台输入信息的时候,客户端1控制台打印了【自己】hi,server

hi,server
 【自己】hi,server 

其他的二个客户端控制台打印

/127.0.0.1:54771 发送的消息:hi,server 

如果其他的客户端下线之后,比如客户端1下线,服务器端控制台打印

/127.0.0.1:54771 下线了
2

其他客户端控制台打印:

【服务器】 -/127.0.0.1:54771 离开

使用命令去验证端口通信

 
 
 
 

----------------------------------------------------

Netty笔记之五:Netty实现心跳检测

集群之间的主节点与从节点之间实现数据的最终一致性。节点与节点之间实现数据的异步同步。节点与节点之间怎样才能感知对应节点状态。这就要求节点每隔一段时间定时的发送心跳包去感知对方的服务健康状态。一般在设置几个心跳包之后我们就可以认为对方节点已经挂了,我们就可以将该节点从集群中踢出去。

我们有个疑问,比如说之前的多客户端通信demo,当客户端断开与服务器连接的时候会触发handlerRemoved方法,那么我们就知道该服务的状态了。为什么还需要心跳包去感知呢?

真实情况远比我们想象中的复杂,比如我们的客户端是移动手机并且已经建立好了连接,当打开飞行模式(或者强制关机)的时候我们就无法感知当前连接已经断开了(handlerRemoved不会触发的),

当我们客户端和服务器端进行通信的时候,关闭网络或者打开飞行模式,此时通过handlerAdded方法和handlerRemoved是无法判断服务是否已经宕掉的。那么就引出了本文的内容。

什么是心跳检测?

判断对方(设备,进程或其它网元)是否正常动行,一般采用定时发送简单的通讯包,如果在指定时间段内未收到对方响应,则判断对方已经宕掉。用于检测TCP的异常断开。
基本原因是服务器端不能有效的判断客户端是否在线,也就是说服务器无法区分客户端是长时间在空闲,还是已经掉线的情况。所谓的心跳包就是客户端定时发送简单的信息给服务器端告诉它我还在而已。
代码就是每隔几分钟发送一个固定信息给服务端,服务端收到后回复一个固定信息。如果服务端几分钟内没有收到客户端信息则视客户端断开。比如有些通信软件长时间不使用,要想知道它的状态是在线还是离线就需要心跳包,定时发包收包。
发包方可以是客户也可以是服务端,看哪边实现方便合理。一般是客户端。服务器也可以定时轮询发心跳下去。
一般来说,出于效率的考虑,是由客户端主动向服务器端发包,而不是相反。
在分布式集群部署环境中也经常使用到心跳检测,比如主从服务之间的心跳检查,各master之间的互相检测等等,所以还是非常有实践意义的。

看一个实际开发中的心跳检测最简单的实践:

服务器端

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;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;


public class MyServer {
    public static void main(String[] args) throws Exception{
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup wokerGroup = new NioEventLoopGroup();

        try{
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup,wokerGroup).channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new MyServerInitializer());

            ChannelFuture channelFuture = serverBootstrap.bind(8899).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            wokerGroup.shutdownGracefully();
        }
    }
}

服务器端Initializer

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.timeout.IdleStateHandler;

import java.util.concurrent.TimeUnit;

public class MyServerInitializer extends ChannelInitializer<SocketChannel>{

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

        //处理空闲状态事件的处理器
        pipeline.addLast(new IdleStateHandler(5,7,10, TimeUnit.SECONDS));
        //对空闲检测进一步处理的Handler
        pipeline.addLast(new MyServerHandler());
    }
}
  • 定义的服务器端读事件的时间,当客户端5s时间没有往服务器写数据(服务器端就是读操作)则触发IdleStateEvent事件。
  • 服务器端写事件的时间,当服务器端7s的时间没有向客户端写数据,则触发IdleStateEvent事件。
  • 当客户端没有往服务器端写数据和服务器端没有往客户端写数据10s的时间,则触发IdleStateEvent事件。

自定义处理器服务器Handler

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleStateEvent;

/**
 * 定义的MyServerHandler没有去继承 SimpleChannelInboundHandler,而是继承
 * SimpleChannelInboundHandler的父类ChannelInboundHandlerAdapter
 */
public class MyServerHandler extends ChannelInboundHandlerAdapter{

    //管道中上一个Handler触发的事件
    

客户端

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.InputStreamReader;

public class MyChatClient {
    public static void main(String[] args) throws Exception{
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();

        try{
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
                    .handler(new MyChatClientInitializer());

            Channel channel = bootstrap.connect("localhost",8899).sync().channel();

            //标准输入
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));

            //利用死循环,不断读取客户端在控制台上的输入内容
            for (;;){
                channel.writeAndFlush(bufferedReader.readLine() +"\r\n");
            }

        }finally {
            eventLoopGroup.shutdownGracefully();
        }
    }
}

测试

启动服务器和客户端,当5s后客户端没有往服务端写数据,造成了服务器端读空闲,服务器的控制台上打印:

十月 13, 2017 12:41:55 上午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0x38081a23, L:/0:0:0:0:0:0:0:0:8899] READ: [id: 0x6d059808, L:/127.0.0.1:8899 - R:/127.0.0.1:57561]
十月 13, 2017 12:41:55 上午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0x38081a23, L:/0:0:0:0:0:0:0:0:8899] READ COMPLETE
/127.0.0.1:57561超时事件:读空闲

修改服务器端Initializer中的代码:

pipeline.addLast(new IdleStateHandler(5,3,10, TimeUnit.SECONDS));

重启服务器和客户端,当服务器端3s没有往客户端写数据,则造成服务器的写空闲。服务器端控制台打印:

十月 13, 2017 12:44:43 上午 io.netty.handler.logging.LoggingHandler bind
信息: [id: 0x12eb2eca] BIND: 0.0.0.0/0.0.0.0:8899
十月 13, 2017 12:44:43 上午 io.netty.handler.logging.LoggingHandler channelActive
信息: [id: 0x12eb2eca, L:/0:0:0:0:0:0:0:0:8899] ACTIVE
十月 13, 2017 12:44:49 上午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0x12eb2eca, L:/0:0:0:0:0:0:0:0:8899] READ: [id: 0x8547406c, L:/127.0.0.1:8899 - R:/127.0.0.1:57592]
十月 13, 2017 12:44:49 上午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0x12eb2eca, L:/0:0:0:0:0:0:0:0:8899] READ COMPLETE
/127.0.0.1:57592超时事件:写空闲

修改服务器端Initializer中的代码:

 pipeline.addLast(new IdleStateHandler(5,7,4, TimeUnit.SECONDS));

当客户端没有往服务器写数据(造成服务器读事件)和服务端没有往客户端写数据(造成服务器端写事件)的时间达到4s则触发服务端读写空闲。
重启客户端和服务器服务,4s后服务器端控制台打印:

十月 13, 2017 12:47:52 上午 io.netty.handler.logging.LoggingHandler bind
信息: [id: 0x8bb2c1a5] BIND: 0.0.0.0/0.0.0.0:8899
十月 13, 2017 12:47:52 上午 io.netty.handler.logging.LoggingHandler channelActive
信息: [id: 0x8bb2c1a5, L:/0:0:0:0:0:0:0:0:8899] ACTIVE
十月 13, 2017 12:47:57 上午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0x8bb2c1a5, L:/0:0:0:0:0:0:0:0:8899] READ: [id: 0x49a9006e, L:/127.0.0.1:8899 - R:/127.0.0.1:57622]
十月 13, 2017 12:47:57 上午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0x8bb2c1a5, L:/0:0:0:0:0:0:0:0:8899] READ COMPLETE
/127.0.0.1:57622超时事件:读写空闲

----------------------------------------------------

Netty笔记之六:Netty对websocket的支持

 

WebSocket是一种规范,是Html5规范的一部分,websocket解决什么问题呢?解决http协议的一些不足。我们知道,http协议是一种无状态的,基于请求响应模式的协议。

网页聊天的程序(基于http协议的),浏览器客户端发送一个数据,服务器接收到这个浏览器数据之后,如何将数据推送给其他的浏览器客户端呢?
这就涉及到服务器的推技术。早年为了实现这种服务器也可以像浏览器客户端推送消息的长连接需求,有很多方案,比如说最常用的采用一种轮询技术,就是客户端每隔一段时间,比如说2s或者3s向服务器发送请求,去请求服务器端是否还有信息没有响应给客户端,有就响应给客户端,当然没有响应就只是一种无用的请求。

这种长轮询技术的缺点有:
1)响应数据不是实时的,在下一次轮询请求的时候才会得到这个响应信息,只能说是准实时,而不是严格意义的实时。
2)大多数轮询请求的空轮询,造成大量的资源带宽的浪费,每次http请求携带了大量无用的头信息,而服务器端其实大多数都不关注这些头信息,而实际大多数情况下这些头信息都远远大于body信息,造成了资源的消耗。

拓展
比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。

WebSocket是什么?

WebSocket一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并被RFC7936所补充规范。WebSocket API也被W3C定为标准。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

websocket的出现就是解决了客户端与服务端的这种长连接问题,这种长连接是真正意义上的长连接。客户端与服务器一旦连接建立双方就是对等的实体,不再区分严格意义的客户端和服务端。长连接只有在初次建立的时候,客户端才会向服务端发送一些请求,这些请求包括请求头和请求体,一旦建立好连接之后,客户端和服务器只会发送数据本身而不需要再去发送请求头信息,这样大量减少了
网络带宽。websocket协议本身是构建在http协议之上的升级协议,客户端首先向服务器端去建立连接,这个连接本身就是http协议只是在头信息中包含了一些websocket协议的相关信息,一旦http连接建立之后,服务器端读到这些websocket协议的相关信息就将此协议升级成websocket协议。websocket协议也可以应用在非浏览器应用,只需要引入相关的websocket库就可以了。

HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。Websocket使用ws或wss的统一资源标志符,类似于HTTPS,其中wss表示在TLS之上的Websocket。如:

ws://example.com/wsapi
wss://secure.example.com/

优点

  • 较少的控制开销:相对与http请求的头部信息,websocket信息明显减少。
  • 更强的实时性:由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
  • 保持连接状态。于HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
  • 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
  • 可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
  • 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。

netty对websocket协议的支持

demo

浏览器页面向服务器发送消息,服务器将当前消息发送时间反馈给浏览器页面。

服务器端

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;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

import java.net.InetSocketAddress;

//websocket长连接示例
public class MyServer {
    public static void main(String[] args) throws Exception{
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup wokerGroup = new NioEventLoopGroup();

        try{
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup,wokerGroup).channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new WebSocketChannelInitializer());

            ChannelFuture channelFuture = serverBootstrap.bind(new InetSocketAddress(8899)).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            wokerGroup.shutdownGracefully();
        }

    }
}

服务器端初始化连接

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;

public class WebSocketChannelInitializer extends ChannelInitializer<SocketChannel>{

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

        //websocket协议本身是基于http协议的,所以这边也要使用http解编码器
        pipeline.addLast(new HttpServerCodec());
        //以块的方式来写的处理器
        pipeline.addLast(new ChunkedWriteHandler());
        //netty是基于分段请求的,HttpObjectAggregator的作用是将请求分段再聚合,参数是聚合字节的最大长度
        pipeline.addLast(new HttpObjectAggregator(8192));

        //ws://server:port/context_path
        //ws://localhost:9999/ws
        //参数指的是contex_path
        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
        //websocket定义了传递数据的6中frame类型
        pipeline.addLast(new TextWebSocketFrameHandler());

    }
}

WebSocketServerProtocolHandler:参数是访问路径,这边指定的是ws,服务客户端访问服务器的时候指定的url是:ws://localhost:8899/ws
它负责websocket握手以及处理控制框架(Close,Ping(心跳检检测request),Pong(心跳检测响应))。 文本和二进制数据帧被传递到管道中的下一个处理程序进行处理。


WebSocket规范中定义了6种类型的桢,netty为其提供了具体的对应的POJO实现。
WebSocketFrame:所有桢的父类,所谓桢就是WebSocket服务在建立的时候,在通道中处理的数据类型。本列子中客户端和服务器之间处理的是文本信息。所以范型参数是TextWebSocketFrame。

 
WebSocketFrame继承类

 

自定义Handler

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;

import java.time.LocalDateTime;

//处理文本协议数据,处理TextWebSocketFrame类型的数据,websocket专门处理文本的frame就是TextWebSocketFrame
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame>{

    //读到客户端的内容并且向客户端去写内容
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        System.out.println("收到消息:"+msg.text());

        /**
         * writeAndFlush接收的参数类型是Object类型,但是一般我们都是要传入管道中传输数据的类型,比如我们当前的demo
         * 传输的就是TextWebSocketFrame类型的数据
         */
        ctx.channel().writeAndFlush(new TextWebSocketFrame("服务时间:"+ LocalDateTime.now()));
    }

    //每个channel都有一个唯一的id值
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        //打印出channel唯一值,asLongText方法是channel的id的全名
        System.out.println("handlerAdded:"+ctx.channel().id().asLongText());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        System.out.println("handlerRemoved:" + ctx.channel().id().asLongText());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("异常发生");
        ctx.close();
    }
}

页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebSocket客户端</title>
</head>
<body>
<script type="text/javascript">
    var socket;

    //如果浏览器支持WebSocket
    if(window.WebSocket){
        //参数就是与服务器连接的地址
        socket = new WebSocket("ws://localhost:8899/ws");

        //客户端收到服务器消息的时候就会执行这个回调方法
        socket.onmessage = function (event) {
            var ta = document.getElementById("responseText");
            ta.value = ta.value + "\n"+event.data;
        }

        //连接建立的回调函数
        socket.onopen = function(event){
            var ta = document.getElementById("responseText");
            ta.value = "连接开启";
        }

        //连接断掉的回调函数
        socket.onclose = function (event) {
            var ta = document.getElementById("responseText");
            ta.value = ta.value +"\n"+"连接关闭";
        }
    }else{
        alert("浏览器不支持WebSocket!");
    }

    //发送数据
    function send(message){
        if(!window.WebSocket){
            return;
        }

        //当websocket状态打开
        if(socket.readyState == WebSocket.OPEN){
            socket.send(message);
        }else{
            alert("连接没有开启");
        }
    }
</script>
<form onsubmit="return false">
    <textarea name = "message" style="width: 400px;height: 200px"></textarea>

    <input type ="button" value="发送数据" onclick="send(this.form.message.value);">

    <h3>服务器输出:</h3>

    <textarea id ="responseText" style="width: 400px;height: 300px;"></textarea>

    <input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空数据">
</form>
</body>
</html>

启动服务器,然后运行客户端页面,当客户端和服务器端连接建立的时候,服务器端执行handlerAdded回调方法,客户端执行onopen回调方法

服务器端控制台:

handlerAdded:acde48fffe001122-00005c11-00000001-4ce4764fffa940fe-df037eb5

页面:

 
客户端连接建立

客户端发送消息,服务器端进行响应,

 
浏览器客户端发送消息

服务端控制台打印:

收到消息:websocket程序

客户端也收到服务器端的响应:

 
浏览器客户端收到服务器端响应

打开开发者工具

 
页面发送websocket请求.png

 

在从标准的HTTP或者HTTPS协议切换到WebSocket时,将会使用一种升级握手的机制。因此,使用WebSocket的应用程序将始终以HTTP/S作为开始,然后再执行升级。这个升级动作发生的确定时刻特定与应用程序;它可能会发生在启动时候,也可能会发生在请求了某个特定的IURL之后。

 
查看桢信息

----------------------------------------------------

Netty笔记之七:Google Protobuf与Netty结合

学过java的都使用过RMI框架(remote method invocation),远程方法调用,比如A,B二个服务器,A调用B服务器上的方法就像调用本地方法一样,但是本质上是跨机器的调用了,A机器将调用的方法名,参数通过字节码的形式传输到B这台机器上,B这台机器将这些字节码转换成对B机器上具体方法的调用,并将相应的返回值序列化成二进制数据传输到A服务器上。

RPC(Remote Procedure Call)其实和rmi及其类似,RPC与RMI框架对比的优势就是好多RPC框架都是跨语言的。

RMI只针对java,A,B服务都使用java编写。几乎所有的RPC框架都存在代码生成,自动代码屏蔽了底层序列化通信等各种细节的处理,使得用户(开发者)可以像调用本地方法一样调用远程的方法。一般这种自动生成的代码在客户端我们称为stub,服务端我们称为skeleton。

序列化与反序列化技术,也称为编码与解码技术,比如我们本篇博客讨论的Google Protobuf,和marshalling等技术。

从广义上来讲,webservice也可以称为RPC框架,但是相比于其他的RPC框架来说,webservice的性能稍微差点,因为决定一个rpc性能的优秀与否在于其底层对象编解码性能。RPC一般都是基于socket协议传输的,而webservice基于http传输的,socket协议的性能也要高于http协议传输数据。所以,一般在公司内部各个微服务之间的服务调用都使用RPC框架多一点,因为在性能上的考虑,而我们总所周知的dubbo虽然也算是RPC框架,但其实并不支持多语言。

什么是protocol buffers?

Protocol buffers是谷歌的语言中立,平台中立的,可扩展机制的序列化数据结构框架-可以看作是xml,但是体积更小,传输速率更快,使用更加简单。一旦你定义了你的数据格式,你可以使用生成源代码去轻松地从各种数据流读和写你的结构化数据并且使用不同的语言。protobuf有2.0版本和3.0版本,3.0版本十grpc框架的基础

Protocol buffers目前支持Java, Python, Objective-C, 和C++生成代码。新的proto3语言版本,你可以使用Go, JavaNano, Ruby, 和 C#。

为什么使用Protocol buffers

使用一个简单的可以从一个文件中去读写人员联系信息"地址簿"程序。每个在地址簿的人有姓名,id,邮箱地址和一个联系人电话号码属性。

你如何序列化和检索这样的结构化数据? 有几种方法来解决这个问题:
使用java原生的序列化。这是一种默认的方式因为是内嵌于java语言的,但是有一大堆众所周知的问题(参考Effective Java这本书),并且你不能将数据分享于C++和Python应用(也就是不能跨语言)。

还可以将数据项编码为单个字符串的ad-hoc方式 - 例如将4个ints编码为“12:3:-23:67”。 这是一个简单而灵活的方法,尽管它需要编写一次性编码和解析代码,并且解析具有很小的运行时成本。 这最适合编码非常简单的数据。

将数据序列化为XML。 这种方法可能非常有吸引力,因为XML是(可能的)人类可读的,并且有很多语言的绑定库。 如果您想与其他应用程序/项目共享数据,这可能是一个很好的选择。 然而,XML浪费性能,编码/解码可能会对应用程序造成巨大的性能损失。 另外,检索XML DOM树比在一般类中简单的字段检索要复杂得多。

Protocol buffers是灵活,高效,自动化的解决方案来解决这个问题。 使用Protocol buffers,您可以编写一个.proto描述您希望存储的数据结构。 Protocol buffers编译器创建一个实现自动编码和解析协议缓冲区数据的类,并使用高效的二进制格式。 生成的类为组成Protocol buffers的字段提供getter和setter。

使用Protobuf编写一个编码解码的最简单程序

  • 在 .proto结尾的文件中定义消息格式。
  • 使用protocol buffers编译器将 .proto结尾的文件生成对应语言的源代码(本demo使用java编译器)。
  • 使用Java protocol buffer API 去读写消息。

定义一个Student.proto文件

syntax ="proto2";

package com.zhihao.miao.protobuf;

//optimize_for 加快解析的速度
option optimize_for = SPEED;
option java_package = "com.zhihao.miao.protobuf";
option java_outer_classname="DataInfo";

message Student{
    required string name = 1;
    optional int32 age = 2;
    optional string address = 3;
}

在Java项目中,除非你已经明确指定了java_package,否则package 用作Java的包名。即使您提供java_package,您仍然应该定义一个package,以避免在Protocol Buffers名称空间和非Java语言中的名称冲突。

在package的定义之后,我们可以看到两个定义的java选项:java_packagejava_outer_classnamejava_package指定您生成的类应该存放的Java包名称。 如果没有明确指定它,将会使用package定义的name作为包名,但这些名称通常不是适合的Java包名称(因为它们通常不以域名开头)。 java_outer_classname选项定义应该包含此文件中所有类的类名。 如果你不明确地给出一个java_outer_classname,它将通过将文件名转换为驼峰的方式来生成。 例如,默认情况下,“my_proto.proto”将使用“MyProto”作为外部类名称。

每个元素上的“= 1”,“= 2”标记标识字段在二进制编码中使用的唯一“标签”。你可以将经常使用或者重复的字段标注成1-15,因为在进行编码的时候因为少一个字节进行编码,所以效率更高。

required:必须提供该字段的值,否则被认为没有初始化。尝试构建一个未初始化的值被会抛出RuntimeException。解析一个为初始化的消息会抛出IOException。除此之外与optional一样。
optional:可以设置或不设置该字段。 如果未设置可选字段值,则使用默认值。
repeated:字段可能重复任意次数(包括零)。 重复值的顺序将保留在protocol buffer中。 将重复的字段视为动态大小的数组。(本列子中没有字段定义成repeated类型,定义成repeated类型其实就是java中List类型的字段。

慎重使用required类型,将required类型的字段更改为optional会有一些问题,而将optional类型的字段更改为required类型,则没有问题。

编译

使用protocol buffers编译器将对应的.proto文件编译成对应的类
关于编译器的安装,下载地址

 
下载页面图示

修改环境变量

➜  vim .bash_profile
export PATH=/Users/naeshihiroshi/software/work/protoc-3.3.0-osx-x86_64/bin
➜  source .bash_profile
➜  which protoc
/Users/naeshihiroshi/software/work/protoc-3.3.0-osx-x86_64/bin/protoc

进入项目目录,执行编译语句如下:

➜  netty_lecture git:(master) ✗ protoc --java_out=src/main/java  src/protobuf/Student.proto   

--java_out后面第一个参数指定代码的路径,具体的包名在.proto文件中的java_package指定了,第二个指定要编译的proto文件。

自动生成的类名是DataInfo(在java_outer_classname中指定了),自动生成的类太长,这边就不列出来了。

编写序列化反序列化测试类

package com.zhihao.miao.protobuf;

//实际使用protobuf序列化框架客户端将对象转译成字节数组,然后通过协议传输到服务器端,服务器端可以是其他的语言框架(比如说python)将
//字节对象反编译成java对象
public class ProtobuffTest {
    public static void main(String[] args) throws Exception{
        DataInfo.Student student = DataInfo.Student.newBuilder().
                setName("张三").setAge(20).setAddress("北京").build();

        //将对象转译成字节数组,序列化
        byte[] student2ByteArray = student.toByteArray();

        //将字节数组转译成对象,反序列化
        DataInfo.Student student2 = DataInfo.Student.parseFrom(student2ByteArray);

        System.out.println(student2.getName());
        System.out.println(student2.getAge());
        System.out.println(student2.getAddress());
    }
}

执行测试类,控制台打印:

张三
20
北京

Google Protobuf与netty结合

protobuf做为序列化的一种方式,序列化之后通过什么样的载体在网络中传输?

使用netty使得经过protobuf序列化的对象可以通过网络通信进行客户端和服务器的信息通信。客户端使用protobuf将对象序列化成字节码,而服务器端通过protobuf将对象反序列化成原本对象。

写一个使用Protobuf作为序列化框架,netty作为传输层的最简单的demo,需求描述:

  • 客户端传递一个User对象给服务端(User对象包括姓名,年龄,密码)
  • 客户端接收客户端的User对象并且将其相应的银行账户等信息反馈给客户端

定义的.proto文件如下:

syntax ="proto2";

package com.zhihao.miao.netty.sixthexample;

option optimize_for = SPEED;
option java_package = "com.zhihao.miao.test.day06";
option java_outer_classname="DataInfo";

message RequestUser{
    optional string user_name = 1;
    optional int32 age = 2;
    optional string password = 3;
}

message ResponseBank{
    optional string bank_no = 1;
    optional double money = 2;
    optional string bank_name=3;
}

使用Protobuf编译器进行编译,生成DataInfo对象,

服务器端代码:

package com.zhihao.miao.test.day06;


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;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

public class ProtoServer {
    public static void main(String[] args) throws Exception{
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup wokerGroup = new NioEventLoopGroup();

        try{
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup,wokerGroup).channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ProtoServerInitializer());

            ChannelFuture channelFuture = serverBootstrap.bind(8899).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            wokerGroup.shutdownGracefully();
        }
    }
}

服务端ProtoServerInitializer(初始化连接):

package com.zhihao.miao.test.day06;


import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;


public class ProtoServerInitializer extends ChannelInitializer<SocketChannel>{

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

        //解码器,通过Google Protocol Buffers序列化框架动态的切割接收到的ByteBuf
        pipeline.addLast(new ProtobufVarint32FrameDecoder());
        //服务器端接收的是客户端RequestUser对象,所以这边将接收对象进行解码生产实列
        pipeline.addLast(new ProtobufDecoder(DataInfo.RequestUser.getDefaultInstance()));
        //Google Protocol Buffers编码器
        pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
        //Google Protocol Buffers编码器
        pipeline.addLast(new ProtobufEncoder());

        pipeline.addLast(new ProtoServerHandler());
    }
}

自定义服务端的处理器:

package com.zhihao.miao.test.day06;


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

public class ProtoServerHandler extends SimpleChannelInboundHandler<DataInfo.RequestUser> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, DataInfo.RequestUser msg) throws Exception {
        System.out.println(msg.getUserName());
        System.out.println(msg.getAge());
        System.out.println(msg.getPassword());

        DataInfo.ResponseBank bank = DataInfo.ResponseBank.newBuilder().setBankName("中国工商银行")
                .setBankNo("6222222200000000000").setMoney(560000.23).build();

        ctx.channel().writeAndFlush(bank);
    }
}

客户端:

package com.zhihao.miao.test.day06;

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

public class ProtoClient {

    public static void main(String[] args) throws Exception{
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();

        try{
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
                    .handler(new ProtoClientInitializer());

            ChannelFuture channelFuture = bootstrap.connect("localhost",8899).sync();
            channelFuture.channel().closeFuture().sync();

        }finally {
            eventLoopGroup.shutdownGracefully();
        }
    }
}

客户端初始化连接(ProtoClientInitializer),

package com.zhihao.miao.test.day06;


import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;

public class ProtoClientInitializer extends ChannelInitializer<SocketChannel> {

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

        //解码器,通过Google Protocol Buffers序列化框架动态的切割接收到的ByteBuf
        pipeline.addLast(new ProtobufVarint32FrameDecoder());
        //将接收到的二进制文件解码成具体的实例,这边接收到的是服务端的ResponseBank对象实列
        pipeline.addLast(new ProtobufDecoder(DataInfo.ResponseBank.getDefaultInstance()));
        //Google Protocol Buffers编码器
        pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
        //Google Protocol Buffers编码器
        pipeline.addLast(new ProtobufEncoder());

        pipeline.addLast(new ProtoClientHandler());
    }
}

自定义客户端处理器:

package com.zhihao.miao.test.day06;


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

public class ProtoClientHandler extends SimpleChannelInboundHandler<DataInfo.ResponseBank> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, DataInfo.ResponseBank msg) throws Exception {
        System.out.println(msg.getBankNo());
        System.out.println(msg.getBankName());
        System.out.println(msg.getMoney());
    }


    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        DataInfo.RequestUser user = DataInfo.RequestUser.newBuilder()
                .setUserName("zhihao.miao").setAge(27).setPassword("123456").build();
        ctx.channel().writeAndFlush(user);
    }
}

运行服务器端和客户端,服务器控制台打印:

七月 03, 2017 11:12:03 下午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0xa1a63b58, L:/0:0:0:0:0:0:0:0:8899] READ: [id: 0x08c534f3, L:/127.0.0.1:8899 - R:/127.0.0.1:65448]
七月 03, 2017 11:12:03 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0xa1a63b58, L:/0:0:0:0:0:0:0:0:8899] READ COMPLETE
zhihao.miao
27
123456

客户端控制台打印:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
6222222200000000000
中国工商银行
560000.23

总结

本节我们使用Google Protobuf定义消息体格式,使用Netty作为网络传输层框架。其实大多数RPC框架底层实现都是使用序列化框架和NIO通信框架进行结合。下面还会学习基于Protobuf 3.0协议的Grpc框架(Google基于Protobuf 3.0协议的一个跨语言的rpc框架,更加深入的去了解rpc框架)。

----------------------------------------------------

Netty笔记之八:自定义通信协议

 

Netty中双方建立通信之后,对象数据会按照ByteBuf字节码的方式进行传输。

自定义一种通信协议,协议将传输数据定义了消息头和消息正文。

管道中传递LuckMessage对象,LuckMessage中定义了消息头LuckHeader和消息正文content。消息头header包括version,contentLength,sessionId。

消息定义:

// 消息的头部
public class LuckHeader {

    // 协议版本
    private int version;
    // 消息内容长度
    private int contentLength;
    // 服务名称
    private String sessionId;

    public LuckHeader(int version, int contentLength, String sessionId) {
        this.version = version;
        this.contentLength = contentLength;
        this.sessionId = sessionId;
    }

    public int getVersion() {
        return version;
    }

    public void setVersion(int version) {
        this.version = version;
    }

    public int getContentLength() {
        return contentLength;
    }

    public void setContentLength(int contentLength) {
        this.contentLength = contentLength;
    }

    public String getSessionId() {
        return sessionId;
    }

    public void setSessionId(String sessionId) {
        this.sessionId = sessionId;
    }
}

// 消息的主体
public class LuckMessage {

    private LuckHeader luckHeader;
    private String content;

    public LuckMessage(LuckHeader luckHeader, String content) {
        this.luckHeader = luckHeader;
        this.content = content;
    }

    public LuckHeader getLuckHeader() {
        return luckHeader;
    }

    public void setLuckHeader(LuckHeader luckHeader) {
        this.luckHeader = luckHeader;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    @Override
    public String toString() {
        return String.format("[version=%d,contentLength=%d,sessionId=%s,content=%s]",
                luckHeader.getVersion(),
                luckHeader.getContentLength(),
                luckHeader.getSessionId(),
                content);
    }
}

服务端代码:

public class LuckServer {

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

        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {

            ServerBootstrap serverBootstrap = new ServerBootstrap();
            // 指定socket的一些属性
            serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024);
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)  // 指定是一个NIO连接通道
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new LuckServerInitializer());

            // 绑定对应的端口号,并启动开始监听端口上的连接
            Channel ch = serverBootstrap.bind(8899).sync().channel();

            System.out.printf("luck协议启动地址:127.0.0.1:%d/\n", 8899);

            // 等待关闭,同步端口
            ch.closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

服务端初始化连接:

package com.zhihao.miao.test.day08;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;

public class LuckServerInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel channel) throws Exception {

        ChannelPipeline pipeline = channel.pipeline();

        pipeline.addLast(new LuckEncoder());
        pipeline.addLast(new LuckDecoder());
        // 添加逻辑控制层
        pipeline.addLast(new LuckServerHandler());

    }
}

编码Handler:

package com.zhihao.miao.test.day08;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

public class LuckEncoder extends MessageToByteEncoder<LuckMessage> {

    @Override
    protected void encode(ChannelHandlerContext ctx, LuckMessage message, ByteBuf out) throws Exception {

        // 将Message转换成二进制数据
        LuckHeader header = message.getLuckHeader();

        // 这里写入的顺序就是协议的顺序.

        // 写入Header信息
        out.writeInt(header.getVersion());
        out.writeInt(message.getContent().length());
        out.writeBytes(header.getSessionId().getBytes());

        // 写入消息主体信息
        out.writeBytes(message.getContent().getBytes());
    }
}

解码器Handler:

package com.zhihao.miao.test.day08;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

import java.util.List;

public class LuckDecoder extends ByteToMessageDecoder {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {

        // 获取协议的版本
        int version = in.readInt();
        // 获取消息长度
        int contentLength = in.readInt();
        // 获取SessionId
        byte[] sessionByte = new byte[36];
        in.readBytes(sessionByte);
        String sessionId = new String(sessionByte);

        // 组装协议头
        LuckHeader header = new LuckHeader(version, contentLength, sessionId);

        // 读取消息内容,这边demo中不对
        byte[] contentbys = new byte[in.readableBytes()];
        in.readBytes(contentbys);

        String content = new String(contentbys);

        LuckMessage message = new LuckMessage(header, content);

        out.add(message);
    }
}

自定义服务端handler处理器:

package com.zhihao.miao.test.day08;

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

public class LuckServerHandler extends SimpleChannelInboundHandler<LuckMessage> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, LuckMessage msg) throws Exception {
        // 简单地打印出server接收到的消息
        System.out.println(msg.toString());
    }


    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("service exception:"+cause.getMessage());
    }
}

客户端:

package com.zhihao.miao.test.day08;

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.util.UUID;

public class LuckClient {

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

        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group).channel(NioSocketChannel.class)
                    .handler(new LuckServerInitializer());

            // Start the connection attempt.
            Channel ch = b.connect("127.0.0.1", 8899).sync().channel();

            int version = 1;
            String sessionId = UUID.randomUUID().toString();
            String content = "I'm the luck protocol!";

            LuckHeader header = new LuckHeader(version, content.length(), sessionId);
            LuckMessage message = new LuckMessage(header, content);
            ch.writeAndFlush(message);

            ch.close();

        } finally {
            group.shutdownGracefully();
        }
    }
}

客户端初始化连接:

package com.zhihao.miao.test.day08;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;

public class LuckClientInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel channel) throws Exception {

        ChannelPipeline pipeline = channel.pipeline();

        // 添加编解码器, 由于ByteToMessageDecoder的子类无法使用@Sharable注解,
        // 这里必须给每个Handler都添加一个独立的Decoder.
        pipeline.addLast(new LuckEncoder());
        pipeline.addLast(new LuckDecoder());

        pipeline.addLast(new LuckClientHandler());

    }
}

客户端自定义handler:

package com.zhihao.miao.test.day08;

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

public class LuckClientHandler extends SimpleChannelInboundHandler<LuckMessage> {

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, LuckMessage message) throws Exception {
        System.out.println(message);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("client exception:"+cause.getMessage());
    }
}

启动服务器和客户端,服务器端控制台打印:

七月 04, 2017 5:22:27 下午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0x9df966cc, L:/0:0:0:0:0:0:0:0:8899] READ: [id: 0x99c6480a, L:/127.0.0.1:8899 - R:/127.0.0.1:55722]
七月 04, 2017 5:22:27 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0x9df966cc, L:/0:0:0:0:0:0:0:0:8899] READ COMPLETE
[version=1,contentLength=22,sessionId=c9345f67-99b6-46d2-97ff-eef853c9d569,content=I'm the luck protocol!]
 

----------------------------------------------------

Netty笔记之九: Netty多种通讯协议支持

会构建一个Server,同时支持Cat,Dog和People通信协议。有二种实现方式:

  • 第一种方式利用了自定义协议,传递消息的时候,对消息的前几位(比如2位)进行自定义的位置(比如AB)解码器解析的时候前二位为AB表示一种协议类型,CD一种协议类型。这种方式没有利用protobuf,而是直接使用Netty自定义协议来解决的方案。
  • 第二种方式使用protobuf来实现,实际上是对消息的定义方式进行规定,因为netty本身,客户端和服务器端建立的是一条TCP连接,一方必须要判断对方发送过来的对象是什么类型。

Protocol Buffers实现netty的多种传输协议

我们知道使用Protocol Buffers首先定义一个.proto文件

定义一个最外层的消息,最外层的消息(MyMessage)包含了所有传递的消息类型,所有的消息类型嵌套在最外层的消息类型中,每次传递都将传递具体消息类型(以最外层消息类型的枚举类型传递)

syntax ="proto2";

package com.zhihao.miao.netty.sixthexample;

option optimize_for = SPEED;
option java_package = "com.zhihao.miao.netty.seventhexample";
option java_outer_classname="MyDataInfo";

message MyMessage {

    enum DataType{
        PeopleType = 1;
        DogType = 2;
        CatType = 3;
    }

    required DataType data_type = 1;

    //oneof的意思:如果有多个可选字段,在某一个时刻只能只有一个值被设置,可以节省内存空间
    oneof dataBody {
        People people = 2;
        Dog dog = 3;
        Cat cat = 4;
    }
}

message People{
    optional string name = 1;
    optional int32 age = 2;
    optional string address = 3;
}

message Dog{
    optional string name = 1;
    optional string age = 2;
}

message Cat{
    optional string name = 1;
    optional string city = 2;
}

使用编译器编译生成代码

protoc --java_out=src/main/java src/protobuf/People.proto

关于proto协议中的Oneof含义,如果有多个可选字段,在某一个时刻只能只有一个值被设置,官方链接,生成MyDataInfo类,类代码太多,这边不贴出了

服务端代码:

package com.zhihao.miao.netty.seventhexample;


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;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

public class TestServer {
    public static void main(String[] args) throws Exception{
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup wokerGroup = new NioEventLoopGroup();

        try{
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup,wokerGroup).channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new TestServerInitializer());

            ChannelFuture channelFuture = serverBootstrap.bind(8888).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            wokerGroup.shutdownGracefully();
        }
    }
}

服务端初始化链接:

package com.zhihao.miao.netty.seventhexample;


import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;


public class TestServerInitializer extends ChannelInitializer<SocketChannel>{

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

        pipeline.addLast(new ProtobufVarint32FrameDecoder());
        //使用最外层的消息实例
        pipeline.addLast(new ProtobufDecoder(MyDataInfo.MyMessage.getDefaultInstance()));
        pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
        pipeline.addLast(new ProtobufEncoder());

        pipeline.addLast(new TestServerHandler());
    }
}

其实实现的关键就在于此,使用MyDataInfo.MyMessage实列(MyDataInfo.MyMessage是枚举类型),而我们定义的三种对象刚好就是其枚举对象

自定义的服务端的Handler,根据通道中传递数据的不同dataType值来解析程具体的类型:

package com.zhihao.miao.netty.seventhexample;


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

public class TestServerHandler extends SimpleChannelInboundHandler<MyDataInfo.MyMessage> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MyDataInfo.MyMessage msg) throws Exception {
        MyDataInfo.MyMessage.DataType dataType = msg.getDataType();

        if(dataType == MyDataInfo.MyMessage.DataType.PeopleType){
            MyDataInfo.People people = msg.getPeople();

            System.out.println(people.getName());
            System.out.println(people.getAge());
            System.out.println(people.getAddress());
        }else if(dataType == MyDataInfo.MyMessage.DataType.DogType){
            MyDataInfo.Dog dog = msg.getDog();

            System.out.println(dog.getName());
            System.out.println(dog.getAge());
        }else if(dataType == MyDataInfo.MyMessage.DataType.CatType){
            MyDataInfo.Cat cat = msg.getCat();

            System.out.println(cat.getName());
            System.out.println(cat.getCity());
        }
    }
}

客户端代码:

package com.zhihao.miao.netty.seventhexample;


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

public class TestClient {

    public static void main(String[] args) throws Exception{
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();

        try{
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
                    .handler(new TestClientInitializer());

            ChannelFuture channelFuture = bootstrap.connect("localhost",8888).sync();
            channelFuture.channel().closeFuture().sync();

        }finally {
            eventLoopGroup.shutdownGracefully();
        }
    }
}

客户端的初始化链接:

package com.zhihao.miao.netty.seventhexample;


import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;

public class TestClientInitializer extends ChannelInitializer<SocketChannel> {

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

        pipeline.addLast(new ProtobufVarint32FrameDecoder());
        //使用最外层的消息实例
        pipeline.addLast(new ProtobufDecoder(MyDataInfo.MyMessage.getDefaultInstance()));
        pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
        pipeline.addLast(new ProtobufEncoder());

        pipeline.addLast(new TestClientHandler());
    }
}

自定义处理器端的handler,随机发送不同协议的数据:

package com.zhihao.miao.netty.seventhexample;


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

import java.util.Random;

public class TestClientHandler extends SimpleChannelInboundHandler<MyDataInfo.MyMessage> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MyDataInfo.MyMessage msg) throws Exception {

    }

    //客户端像服务器端发送数据
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        int randomInt = new Random().nextInt(3);

        MyDataInfo.MyMessage myMessage = null;

        if(0 == randomInt){
            myMessage = MyDataInfo.MyMessage.newBuilder().
                    setDataType(MyDataInfo.MyMessage.DataType.PeopleType).
                    setPeople(MyDataInfo.People.newBuilder().setName("张三").
                            setAddress("上海").setAge(26).build()).build();
        }else if(1 == randomInt){
            myMessage = MyDataInfo.MyMessage.newBuilder().
                    setDataType(MyDataInfo.MyMessage.DataType.DogType).
                    setDog(MyDataInfo.Dog.newBuilder().setName("旺财")
                            .setAge("2").build()).build();
        }else if(2 == randomInt){
            myMessage = MyDataInfo.MyMessage.newBuilder().
                    setDataType(MyDataInfo.MyMessage.DataType.CatType).
                    setCat(MyDataInfo.Cat.newBuilder().setName("汤姆")
                            .setCity("上海").build()).build();
        }

        ctx.channel().writeAndFlush(myMessage);
    }
}

启动服务器端,然后启动客户端多执行几次,服务器的控制台显示:

七月 05, 2017 10:10:37 下午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0xd5f957bd, L:/0:0:0:0:0:0:0:0:8888] READ: [id: 0x82a26e9f, L:/127.0.0.1:8888 - R:/127.0.0.1:51777]
七月 05, 2017 10:10:37 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0xd5f957bd, L:/0:0:0:0:0:0:0:0:8888] READ COMPLETE
汤姆
上海
七月 05, 2017 10:11:38 下午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0xd5f957bd, L:/0:0:0:0:0:0:0:0:8888] READ: [id: 0x128da3e7, L:/127.0.0.1:8888 - R:/127.0.0.1:52049]
七月 05, 2017 10:11:38 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0xd5f957bd, L:/0:0:0:0:0:0:0:0:8888] READ COMPLETE
张三
26
上海
七月 05, 2017 10:11:49 下午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0xd5f957bd, L:/0:0:0:0:0:0:0:0:8888] READ: [id: 0xa8220c73, L:/127.0.0.1:8888 - R:/127.0.0.1:52097]
七月 05, 2017 10:11:49 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0xd5f957bd, L:/0:0:0:0:0:0:0:0:8888] READ COMPLETE
汤姆
上海
七月 05, 2017 10:11:55 下午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0xd5f957bd, L:/0:0:0:0:0:0:0:0:8888] READ: [id: 0x9ac52ec1, L:/127.0.0.1:8888 - R:/127.0.0.1:52125]
七月 05, 2017 10:11:55 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0xd5f957bd, L:/0:0:0:0:0:0:0:0:8888] READ COMPLETE
张三
26
上海
七月 05, 2017 10:12:07 下午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0xd5f957bd, L:/0:0:0:0:0:0:0:0:8888] READ: [id: 0x797d03b6, L:/127.0.0.1:8888 - R:/127.0.0.1:52178]
七月 05, 2017 10:12:07 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0xd5f957bd, L:/0:0:0:0:0:0:0:0:8888] READ COMPLETE
旺财
2

使用netty实现多种传输协议

官网类似的demo,自己写了很长也参考了官网才写出这个demo,对netty的理解又加深了:

三种协议实体类:

Person协议

package com.zhihao.miao.test.day10;

public class Person {

    private String username;

    private int age;
    
    //get set方法

}

Dog协议

package com.zhihao.miao.test.day10;
public class Dog {

    private String name;

    private String age;

    //get set方法
}

Cat协议

package com.zhihao.miao.test.day10;

public class Cat {
    private String name;
    private String city;
  //get set方法
}

服务端:

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

public class MultiServer {

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

        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        // 指定socket的一些属性
        serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024);
        serverBootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)  // 指定是一个NIO连接通道
                .handler(new LoggingHandler(LogLevel.INFO))
                .childHandler(new ServerChannelInitializer());

        // 绑定对应的端口号,并启动开始监听端口上的连接
        Channel ch = serverBootstrap.bind(8899).sync().channel();


        // 等待关闭,同步端口
        ch.closeFuture().sync();

    }
}

服务器端初始化lInitializer

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;

public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //解析handler
        pipeline.addLast(new ServlerDecoder());
        pipeline.addLast(new TestServerHandler());
    }
}

服务端解码器Handler,如果解析的位置数据是0则按照 Person协议进行解码,如果传递的位置数据是1,则按照Dog协议进行解码,如果传递的位置数据是2,则按照Cat协议进行解码:

public class ServlerDecoder extends ByteToMessageDecoder {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {

        int flag = in.readInt();

        if(flag == 0){
            int usernamelength = in.readInt();

            byte[] usernamebys = new byte[usernamelength];
            in.readBytes(usernamebys);

            String username = new String(usernamebys);

            int age = in.readInt();

            Person pserson = new Person();
            pserson.setUsername(username);
            pserson.setAge(age);

            out.add(pserson);


        }
        if(flag ==1){
            int namelength =in.readInt();

            byte[] namebys = new byte[namelength];
            in.readBytes(namebys);

            String name = new String(namebys);

            byte[] agebys = new byte[in.readableBytes()];
            in.readBytes(agebys);

            String age = new String(agebys);

            Dog dog = new Dog();
            dog.setName(name);
            dog.setAge(age);

            out.add(dog);
        }
        if(flag ==2){
            int namelength = in.readInt();

            byte[] nameByte = new byte[namelength];
            in.readBytes(nameByte);

            String name = new String(nameByte);

            byte[] colorbys = new byte[in.readableBytes()];
            in.readBytes(colorbys);

            String color = new String(colorbys);

            Cat cat = new Cat();
            cat.setName(name);
            cat.setColor(color);

            out.add(cat);
        }
    }

自定义服务器端Handler:

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

public class TestServerHandler extends SimpleChannelInboundHandler<Object> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        if(msg instanceof Person){
            System.out.println(((Person) msg).getUsername());
            System.out.println(((Person) msg).getAge());
        }

        if(msg instanceof Dog){
            System.out.println(((Dog) msg).getName());
            System.out.println(((Dog) msg).getAge());
        }

        if(msg instanceof Cat){
            System.out.println(((Cat) msg).getName());
            System.out.println(((Cat) msg).getColor());
        }
    }
}

客户端:

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;

public class MultiClient {

    public static void main(String[] args) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        Bootstrap b = new Bootstrap();
        b.group(group).channel(NioSocketChannel.class).handler(new ClientChannelInitializer());

        // Start the connection attempt.
        Channel ch = b.connect("127.0.0.1", 8899).sync().channel();

        ch.flush();
    }
}

客户端初始化Initializer

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;

import java.util.Random;

public class ClientChannelInitializer extends ChannelInitializer<SocketChannel> {

    

三种自定义编码协议,与服务器端进行对应传输Person数据的时候,在Person数据之前加上标识位置数据0,在Dog数据之前加上标识位置数据1,在Cat数据之前加上标识位置数据2,然后将其与本身的数据一起编码成二进制进行传输。

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

public class PersonEncoder extends MessageToByteEncoder<Person> {

    @Override
    protected void encode(ChannelHandlerContext ctx, Person msg, ByteBuf out) throws Exception {
        String username = msg.getUsername();
        int usernamelength = username.length();
        int age = msg.getAge();

        out.writeInt(0); //标识位
        out.writeInt(usernamelength);
        out.writeBytes(username.getBytes());
        out.writeInt(age);
    }
}

Dog协议编码器

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

public class DogEncoder extends MessageToByteEncoder<Dog> {

    @Override
    protected void encode(ChannelHandlerContext ctx, Dog msg, ByteBuf out) throws Exception {

        String name = msg.getName();
        int namelength = name.length();
        String age = msg.getAge();

        out.writeInt(1); //标识位
        out.writeInt(namelength);
        out.writeBytes(name.getBytes());
        out.writeBytes(age.getBytes());
    }
}

Cat协议编码器:

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

public class CatEncoder extends MessageToByteEncoder<Cat> {

    @Override
    protected void encode(ChannelHandlerContext ctx, Cat msg, ByteBuf out) throws Exception {
        String name = msg.getName();
        int namelength = name.length();
        String color = msg.getColor();

        out.writeInt(2); //标识位
        out.writeInt(namelength);
        out.writeBytes(name.getBytes());
        out.writeBytes(color.getBytes());

    }
}

自定义客户端处理器:

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class TestClientHandler extends ChannelInboundHandlerAdapter {

    private Person person;

    private Cat cat;

    private Dog dog;

    public TestClientHandler(Person person){
        this.person = person;
    }

    public TestClientHandler(Dog dog){
        this.dog = dog;
    }

    public TestClientHandler(Cat cat){
        this.cat =cat;
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        if(person != null){
            ctx.channel().writeAndFlush(person);
        }

        if(dog != null){
            ctx.channel().writeAndFlush(dog);
        }

        if(cat != null){
            ctx.channel().writeAndFlush(cat);
        }
    }
}

启动服务端,再多次启动客户端,服务器控制台打印出不同协议传输的结果

maomi
yellow
十月 15, 2017 4:33:43 下午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0x930eab24, L:/0:0:0:0:0:0:0:0:8899] READ: [id: 0xf40f7b07, L:/127.0.0.1:8899 - R:/127.0.0.1:57879]
十月 15, 2017 4:33:43 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0x930eab24, L:/0:0:0:0:0:0:0:0:8899] READ COMPLETE
wangcai
2
十月 15, 2017 4:33:48 下午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0x930eab24, L:/0:0:0:0:0:0:0:0:8899] READ: [id: 0x3384f158, L:/127.0.0.1:8899 - R:/127.0.0.1:57914]
十月 15, 2017 4:33:48 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0x930eab24, L:/0:0:0:0:0:0:0:0:8899] READ COMPLETE
zhihao
27

----------------------------------------------------

----------------------------------------------------

----------------------------------------------------

----------------------------------------------------

----------------------------------------------------

----------------------------------------------------

posted @ 2022-12-06 11:14  hanease  阅读(96)  评论(0编辑  收藏  举报