Netty之私有协议栈开发(五)

广义上区分,通信协议可以分为公有协议和私有协议。由于私有协议的灵活性,它往往会在某个公司或者组织内部使用,按需定制,也因为如此,升级起来会非常方便,灵活性好。绝大多数的私有协议传输层都基于TCP/IP,所以利用Netty的NIO/TCP协议栈可以非常方便地进行私有协议的定制和开发。


私有协议本质上是厂商内部发展和采用的标准,除非授权,其他厂商一般无权使用该协议。私有协议也称非标准协议,就是未经国际或国家标准化组织采纳或批准,由某个企业自己制订,协议实现细节不愿公开,只在企业自己生产的设备之间使用的协议。由于现代软件系统的复杂性,一个大型软件系统往往会被人为地拆分成多个模块,另外随着移动互联网的兴起,网站的规模也越来越大,业务的功能越来越多,为了能够支撑业务的发展,往往需要集群和分布式部署,这样,各个模块之间就要进行跨节点通信。

在传统的Java应用中,通常使用以下4种方式进行跨节点通信。
(1)通过RMI进行远程服务调用;
(2)通过Java的Socket+Java序列化的方式进行跨节点调用;
(3)利用一些开源的RPC框架进行远程服务调用,例如Facebook的Thrift、Apache的Avro等;
(4)利用标准的公有协议进行跨节点服务调用,例如HTTP+XML、RESTfuI+JSON或者WebService;
跨节点的远程服务调用,除了链路层的物理连接外,还需要对请求和响应消息进行编解码。在请求和应答消息本身以外,也需要携带一些其他控制和管理类指令,例如链路建立的握手请求和响应消息、链路检测的心跳消息等。当这些功能组合到一起之后,就会形成私有协议。


协议栈功能描述

Netty协议栈承载了业务内部各模块之间的消息交互和服务调用,它的主要功能如下。
1.基于Netty的NIO通信框架,提供高性能的异步通信能力.
2.提供消息的编解码框架,可以实现POJO的序列化和反序列化.
3.提供基于IP地址的白名单接入认证机制.
4.链路的有效性校验机制.
5.链路的断连重连机制·

通信模型如图:



具体步骤如下:

1.Netty协议栈客户端发送握手请求消息,携带节点ID等有效身份认证信息.
2.Netty协议栈服务端对握手请求消息进行合法性校验,包括节点ID有效性校验、节点重复登录校验和地址合法性校验,校验通过后,返回登录成功的握手应答消息.
3.链路建立成功之后,客户端发送业务消息.
4.链路成功之后,服务端发送心跳消息.
5.链路建立成功之后,客户端发送心跳消息.
6.镒路建立成功之后,服务端发送业务消息,私有协议栈开发.
7.服务端退出时,服务端关闭连接,客户端感知对方关闭连接后,被动关闭客户端连接。


消息定义

NettyMessage

名称 类型 长度 描述
header Header 变长 消息头定义
body Object 变长

请求消息:参数

响应消息:返回值

package com.alen.netty.protocol.model;

/**
 * 请求对象
 * @author alen
 * @create 2018-01-31 16:10
 **/
public final class NettyMessage {

    private Header header;

    private Object body;
    
    public final Header getHeader() {
        return header;
    }

    public final void setHeader(Header header) {
        this.header = header;
    }
    
    public final Object getBody() {
        return body;
    }
    
    public final void setBody(Object body) {
        this.body = body;
    }
    
    @Override
    public String toString() {
        return "NettyMessage{" +
                "header=" + header +
                ", body=" + body +
                '}';
    }
}


Header

名称 类型 长度 描述
crcCode int 32

Netty消息校验码(三部分)

1、0xABEF:固定值,表明消息是Netty协议消息,2字节

2、主版本号:1~255,1字节

3、次版本号:1~255,1字节

crcCode=0xABEF+主版本号+次版本号

length int 32 消息长度,包括消息头,消息体
sessionID long 64 集群节点全局唯一,由会话生成器生成
type Byte 8

0:业务请求消息

1:业务响应消息

2:业务ONE-WAY消息(既是请求又是响应)

3:握手请求消息

4:握手应答消息

5:心跳请求消息

6:心跳应答消息

priority Byte 8 消息优先级:0~255
attachment Mep<String,Object>  变长  可选,用于扩展消息头
package com.alen.netty.protocol.model;

import java.util.HashMap;
import java.util.Map;

/**
 * 报文头
 *
 * @author alen
 * @create 2018-01-31 16:08
 **/
public final class Header {

    private int crcCode = 0xabef0101;

    private int length;// 消息长度

    private long sessionID;// 会话ID

    private byte type;// 消息类型

    private byte priority;// 消息优先级

    private Map<String, Object> attachment = new HashMap<String, Object>(); // 附件


    public final int getCrcCode() {
        return crcCode;
    }

    public final void setCrcCode(int crcCode) {
        this.crcCode = crcCode;
    }

    public final int getLength() {
        return length;
    }

    public final void setLength(int length) {
        this.length = length;
    }

    public final long getSessionID() {
        return sessionID;
    }


    public final void setSessionID(long sessionID) {
        this.sessionID = sessionID;
    }

    public final byte getType() {
        return type;
    }


    public final void setType(byte type) {
        this.type = type;
    }


    public final byte getPriority() {
        return priority;
    }


    public final void setPriority(byte priority) {
        this.priority = priority;
    }

    public final Map<String, Object> getAttachment() {
        return attachment;
    }


    public final void setAttachment(Map<String, Object> attachment) {
        this.attachment = attachment;
    }

    @Override
    public String toString() {
        return "Header{" +
                "crcCode=" + crcCode +
                ", length=" + length +
                ", sessionID=" + sessionID +
                ", type=" + type +
                ", priority=" + priority +
                ", attachment=" + attachment +
                '}';
    }
}



设计


链路的关闭

由于采用长连接通信,在正常的业务运行期间,双方通过心跳和业务消息维持链路,任何一方都不需要主动关闭连接。

但是,在以下情况下·客户端和服务端需要关闭连接。

(1)当对方宕机或者重启时,会主动关闭链路,另一方读取到操作系统的通知信号,得知对方REST链路,需要关闭连接,释放自身的句柄等资源。由于采用TCP全双工通信,通信双方都需要关团连接,释放资源;

(2)消息读写过程中,发生了I/O异常,需要主动关闭连接;

(3)心跳消息读写过程中发生了l/O异常,需要主动关闭连接;

(4)心跳超时·需要主动关闭连接;

(5)发生编码异常等不可恢复错误时,需要主动关闭连接。


可靠性设计

Netty协议栈可能会运行在非常恶劣的网络环境中.网络超时、闪断、对方进程僵死或者处缓慢等情况都有可能发生.为了保证在这些极端异常场景下Netty协议栈仍能够正常工作或者自动恢复,需要对它的可靠性进行统一规划和设计。



心跳机制

在凌晨等业务低谷期时段,如果发生网络闪断、连接被Hang住等网络问题时,由于没有业务消息,应用进程很难发现到了白天业务高峰期时,会发生大量的网络通信失畋,严重的会导致一段时间进程内无法处理业务消息.为了解决这个问题在网络窄闲时采用心跳机制来检测链路的互通性,一旦发现网络故障,立即关闭链路,主动重连。

貝体的设计思路如下·

(1)当网络处于空困状态持续时间达到(连续周期T没有读写消息)时,客户端主动发送Ping心跳消息给服务端的

(2)如果在下一个周期T到来时客户端没有收到对方发送的Pong跳应答消息或者读取到服务端发送的其他业务消息,則心跳失败计数器加1

(3)每当客户端接收到服务的业务消息或者Pong应答消息时,将心跳失敗计数器清零;连续N次没有接收到服务端的Pong消息或者业务消息,則关闭路,间隔INTERVAL时间后发起重连操作。

(4)服务景网络窄闲状态持续时间达到T后,服务端将心跳失败计数器加只要接收到客户端发送的Ping消息或者其他业务消息,计数器清零·

(5)服务端连缕N次没有接收到客户端的Ping消息或者其他业务消息.则关闭涟路,释放资源,等待客户端重连·

通过Ping-Pong双向心跳机制,可以保证无论通信哪一方出现网络故障,都能被及时地检思出来,为了防止由于对方时间内繁忙没有及时返回应答造成的误判,只有连续N次心跳检都失畋才认定链路己经损害,需要关闭涟路并重建链路·当读或者与心跳消息发生以)异常的时候,说明链路已经中断.此时需要立即关闭链路,如果是客户端,需要重新发起连接.如果是服务端,需要清窄缓存的半包信息,等待客户端重连。


重连机制
如果链路中断,等lNTERVAL时间后,中客户端发起重连操作,如果重连失败,间隔周期TERVAL后冉次发起重连,直到重连成功。为了保证服务端能够有充足的时间释放句柄资源,在首次断连时客户需要等待INTERVAL时间之后冉发起重连,而不是失败后就立即重连·
为了保证句柄资源能够及时释放,无论什么场景下的重连失败,客户端都必须保证自身的资源被及时释放,包括但不限于SocketChannel、Socket等“重连失畋后,需要打印异常堆栈信息,方便后续的向題定位.


重复登录保护
当客户端握手成功之后·在链路处于正常状态下,不允许客户端重复登录,以防止客户端在异常状态下反复重连导致句柄资源被耗尽的服务端接收到客户端的握手请求消息之后,首先对IP地址进行合法性检验,如果校验成功.在缓存的地斛表中查看客户端是否己登录。如果已经登录.则拒绝重复登录,返回错误码,同时关闭TCP链路,并在服务的日志中打印握手失效的原因·

客户端接收到握手失敗的应答消息之后,关闭客户端的TCP连接,等待INTERVAL时间之后,再次发起TCP连接,直到认证成功。
为了防止服务端和客户端对链接状态理解不一致导致的客户端无法握手成功的问题,当服务瑞连续N次心跳超时之后需要主动关閉链路,清空该客户端的地址缓存信息,以保证后续该客户端可以重连成动,防止被重复登录保护机制拒绝掉。

消息缓存重发
无论客户端还是服务端,当发生路中断之后,在链潞恢复之前,缓存在消息队列中,待发送的消息不能丢失,等路恢复之后,重新发送这些消息,保证链路中断期司消息不去失。考虑到内存溢出的风险,建议消息缓存队列设置上限,当达到上限之后,应该拒绝继续向该队列添加新的消息。

实现

1.编解码参考 Netty之编解码技术(四)
2.握手和安全认证

握手的发起是在客户端和服务端TCP链路建立成功通道激活时,握手消息的接入和安全认证在服务端处理。
首先开发一个握手认证的客户端ChannelHandler,用于在通道激活时发起握手请求,具体代码实现如下。

package com.alen.netty.protocol.client;


import com.alen.netty.protocol.model.Header;
import com.alen.netty.protocol.model.MessageType;
import com.alen.netty.protocol.model.NettyMessage;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
 * 客户端握手安全认证
 */
public class LoginAuthReqHandler extends ChannelInboundHandlerAdapter {


    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        /*当客户端跟服务端TCP三次握手成功之后,由客户端构造握手请求消 息发送给服务端,
        由于采用IP白名单认证机制,因此,不需要携带消息体,消息体为空,
		消息类型为“3:握手请求消息”。握手请求发送之后,按照协议规范,服务端需要返回握
		手应答消息。*/
        ctx.writeAndFlush(buildLoginReq());
    }

    /*对握手应答消息进行处理,首先判断消息是否是握手应答消息,如果不
    是,直接透传给后面的ChanneIHandIer进行处理:如果是握手应答消息,则对应答结果进
    行判断,如果非0,说明认证失败,关闭链路,重新发起连接·*/
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        NettyMessage message = (NettyMessage) msg;

        // 如果是握手应答消息,需要判断是否认证成功
        if (message.getHeader() != null && message.getHeader().getType() == MessageType.LOGIN_RESP.value()) {
            byte loginResult = (byte) message.getBody();
            if (loginResult != (byte) 0) {
                // 握手失败,关闭连接
                ctx.close();
            } else {
                System.out.println("Login is ok : " + message);
                ctx.fireChannelRead(msg);
            }
        } else
            ctx.fireChannelRead(msg);
    }

    private NettyMessage buildLoginReq() {
        NettyMessage message = new NettyMessage();
        Header header = new Header();
        header.setType(MessageType.LOGIN_REQ.value());
        message.setHeader(header);
        return message;
    }

    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        ctx.fireExceptionCaught(cause);
    }
}

服务端

package com.alen.netty.protocol.server;

import com.alen.netty.protocol.model.Header;
import com.alen.netty.protocol.model.MessageType;
import com.alen.netty.protocol.model.NettyMessage;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.net.InetSocketAddress;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


/**
 * 服务端握手安全认证
 */

public class LoginAuthRespHandler extends ChannelInboundHandlerAdapter {

    //分别定义了重复登录保护和认证白名单列表,主要用于提升握手的可靠性。
    private Map<String, Boolean> nodeCheck = new ConcurrentHashMap<String, Boolean>();
    private String[] whitekList = {"127.0.0.1", "192.168.1.104"};


    /*用于接入认证,首先根据客户端的源地址进行重复
    登录判断,如果客户端己经登录成功,拒绝重复登录,以防止由于客户端重复登录导致的句
    柄泄漏。后通过ChanneIHandIercontext的ChanneI接凵获取客户端的InetSocketAddress
    地址,从中取得发送方的源地址信息,通过源地址进行白名单校验,校验通过握手成功,
    否则握手失败.最后通过buildResponse构造握手应答消息返回给客户端。
    当发生异常关闭琏路的时候,需要将客户端的信息从登录注册表中去注册,以保证后
    续客户端可以重连成功。*/
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        NettyMessage message = (NettyMessage) msg;
        // 如果是握手请求消息,处理,其它消息透传
        if (message.getHeader() != null && message.getHeader().getType() == MessageType.LOGIN_REQ.value()) {
            String nodeIndex = ctx.channel().remoteAddress().toString();
            NettyMessage loginResp = null;
            // 重复登陆,拒绝
            if (nodeCheck.containsKey(nodeIndex)) {
                loginResp = buildResponse((byte) -1);
            } else {
                InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();
                String ip = address.getAddress().getHostAddress();
                boolean isOK = false;
                for (String WIP : whitekList) {
                    if (WIP.equals(ip)) {
                        isOK = true;
                        break;
                    }
                }
                loginResp = isOK ? buildResponse((byte) 0) : buildResponse((byte) -1);
                if (isOK) {
                    nodeCheck.put(nodeIndex, true);
                }
            }
            System.out.println("The login response is : " + loginResp + " body [" + loginResp.getBody() + "]");
            ctx.writeAndFlush(loginResp);
        } else {
            ctx.fireChannelRead(msg);
        }
    }

    private NettyMessage buildResponse(byte result) {
        NettyMessage message = new NettyMessage();
        Header header = new Header();
        header.setType(MessageType.LOGIN_RESP.value());
        message.setHeader(header);
        message.setBody(result);
        return message;
    }

    /**
     *
     服务端感知到断连事件之后,需要清空缓存的登录认证注册信息,以保证后续客户端
     能够正常重连。
     * @param ctx
     * @param cause
     * @throws Exception
     */
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        cause.printStackTrace();
        nodeCheck.remove(ctx.channel().remoteAddress().toString());// 删除缓存
        ctx.close();
        ctx.fireExceptionCaught(cause);
    }
}



3.心跳机制实现

有两种方式一种用netty自带的,一种自己实现

Netty提供了对心跳机制的天然支持,Netty4.0提供了一个类,名为IdleStateHandler,这个类可以对三种类型的心跳检测

这个类的构造参数是这样的:

前三个的参数解释如下:

1)readerIdleTime:为读超时时间(即测试端一定时间内未接受到被测试端消息)

2)writerIdleTime:为写超时时间(即测试端一定时间内向被测试端发送消息)

3)allIdleTime:所有类型的超时时间


这个类主要也是一个ChannelHandler,也需要被载入到ChannelPipeline中,加入我们在服务器端的ChannelInitializer中加入如下的代码:


自己实现

客户端

package com.alen.netty.protocol.client;

import com.alen.netty.protocol.model.Header;
import com.alen.netty.protocol.model.MessageType;
import com.alen.netty.protocol.model.NettyMessage;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;


/**
 * 客户端心跳处理
 */
public class HeartBeatReqHandler extends ChannelInboundHandlerAdapter {

    private volatile ScheduledFuture<?> heartBeat;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        NettyMessage message = (NettyMessage) msg;
        // 握手成功,主动发送心跳消息
        if (message.getHeader() != null && message.getHeader().getType() == MessageType.LOGIN_RESP .value()) {
            heartBeat = ctx.executor().scheduleAtFixedRate(
                    new HeartBeatTask(ctx), 0, 5000,
                    TimeUnit.MILLISECONDS);
        } else if (message.getHeader() != null&& message.getHeader().getType() == MessageType.HEARTBEAT_RESP.value()) {
            System.out .println("Client receive server heart beat message : ---> "+ message);
        } else {
            ctx.fireChannelRead(msg);
        }
    }

    private class HeartBeatTask implements Runnable {
        private final ChannelHandlerContext ctx;

        public HeartBeatTask(final ChannelHandlerContext ctx) {
            this.ctx = ctx;
        }

        @Override
        public void run() {
            NettyMessage heatBeat = buildHeatBeat();
            System.out.println("Client send heart beat messsage to server : ---> "+ heatBeat);
            ctx.writeAndFlush(heatBeat);
        }

        private NettyMessage buildHeatBeat() {
            NettyMessage message = new NettyMessage();
            Header header = new Header();
            header.setType(MessageType.HEARTBEAT_REQ.value());
            message.setHeader(header);
            return message;
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        cause.printStackTrace();
        if (heartBeat != null) {
            heartBeat.cancel(true);
            heartBeat = null;
        }
        ctx.fireExceptionCaught(cause);
    }
}

服务端
package com.alen.netty.protocol.server;

import com.alen.netty.protocol.model.Header;
import com.alen.netty.protocol.model.MessageType;
import com.alen.netty.protocol.model.NettyMessage;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
 * 服务端心跳处理
 */
public class HeartBeatRespHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        NettyMessage message = (NettyMessage) msg;
        // 返回心跳应答消息
        if (message.getHeader() != null && message.getHeader().getType() == MessageType.HEARTBEAT_REQ.value()) {
            System.out.println("Receive client heart beat message : ---> " + message);
            NettyMessage heartBeat = buildHeatBeat();
            System.out.println("Send heart beat response message to client : ---> " + heartBeat);
            ctx.writeAndFlush(heartBeat);
        } else {
            ctx.fireChannelRead(msg);
        }
    }

    private NettyMessage buildHeatBeat() {
        NettyMessage message = new NettyMessage();
        Header header = new Header();
        header.setType(MessageType.HEARTBEAT_RESP.value());
        message.setHeader(header);
        return message;
    }

}

4.断连重试

当客户端感知断连事件之后,释放资源,重新发起连接,具体代码实现

} finally {
	    // 首先监听网络断连事件,如果ChanneI关闭,则执行后续的重连任务,通过Bootstrap
		//重新发起连接,客户端挂在cIoseFuture上监听链路关闭信号,一旦关闭,则创建重连定时
		//器,5s之后重新发起连接,直到重连成功。
		//服务端感知到断连事件之后,需要清空缓存的登录认证注册信息,以保证后续客户端
		//能够正常重连。
	    executor.execute(new Runnable() {
		@Override
		public void run() {
		    try {
			TimeUnit.SECONDS.sleep(1);
			try {
			    connect(NettyConstant.PORT, NettyConstant.REMOTEIP);// 发起重连操作
			} catch (Exception e) {
			    e.printStackTrace();
			}
		    } catch (InterruptedException e) {
			e.printStackTrace();
		    }
		}
	    });

5.服务端及客户端代码

服务端

package com.alen.netty.protocol.server;

import com.alen.netty.protocol.model.NettyConstant;
import com.alen.netty.protocol.model.NettyMessage;
import com.alen.netty.serial.ende.NettyMessageDecoder;
import com.alen.netty.serial.ende.NettyMessageEncoder;
import com.alen.netty.serial.protostuff.ProtostuffSerializer;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
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.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.ReadTimeoutHandler;

import java.io.IOException;


public class NettyServer {

    public void bind() throws Exception {
        // 配置服务端的NIO线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 100)
                .handler(new LoggingHandler(LogLevel.INFO))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch)
                            throws IOException {
                        ch.pipeline().addLast(new NettyMessageDecoder<NettyMessage, ProtostuffSerializer>(NettyMessage.class, ProtostuffSerializer.class, 1 << 20, 2, 4));
                        ch.pipeline().addLast(new NettyMessageEncoder<NettyMessage, ProtostuffSerializer>(NettyMessage.class, ProtostuffSerializer.class));
                        //超时处理
                        ch.pipeline().addLast("readTimeoutHandler", new ReadTimeoutHandler(50));
                        ch.pipeline().addLast(new LoginAuthRespHandler());
                        ch.pipeline().addLast("handler", new ServerHandler());
                        ch.pipeline().addLast("HeartBeatHandler", new HeartBeatRespHandler());

                    }
                });

        // 绑定端口,同步等待成功
        b.bind(NettyConstant.REMOTEIP, NettyConstant.PORT).sync();
        System.out.println("Netty server start ok : "
                + (NettyConstant.REMOTEIP + " : " + NettyConstant.PORT));
    }

    public static void main(String[] args) throws Exception {
        new NettyServer().bind();
    }
}

客户端
/*
 * Copyright 2013-2018 Lilinfeng.
 *  
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *  
 *      http://www.apache.org/licenses/LICENSE-2.0
 *  
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.alen.netty.protocol.client;

import com.alen.netty.protocol.model.NettyConstant;
import com.alen.netty.protocol.model.NettyMessage;
import com.alen.netty.serial.ende.NettyMessageEncoder;
import com.alen.netty.serial.protostuff.ProtostuffSerializer;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.ReadTimeoutHandler;

import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;


public class NettyClient {

    private ScheduledExecutorService executor = Executors
            .newScheduledThreadPool(1);
    EventLoopGroup group = new NioEventLoopGroup();

    public void connect(int port, String host) throws Exception {
        // 配置客户端NIO线程组
        try {
            Bootstrap b = new Bootstrap();
            b.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch)
                                throws Exception {
                            ch.pipeline().addLast(new com.alen.netty.serial.ende.NettyMessageDecoder<NettyMessage, ProtostuffSerializer>(NettyMessage.class, ProtostuffSerializer.class, 1 << 20, 2, 4));
                            ch.pipeline().addLast(new NettyMessageEncoder<NettyMessage, ProtostuffSerializer>(NettyMessage.class, ProtostuffSerializer.class));
                            ch.pipeline().addLast("readTimeoutHandler", new ReadTimeoutHandler(50));
                            ch.pipeline().addLast("LoginAuthHandler", new LoginAuthReqHandler());
                            ch.pipeline().addLast("handler", new NettyClientHandler());
                            ch.pipeline().addLast("HeartBeatHandler", new HeartBeatReqHandler());

                        }
                    });
            // 发起异步连接操作
            ChannelFuture future = b.connect(
                    new InetSocketAddress(host, port),
                    new InetSocketAddress(NettyConstant.LOCALIP,
                            NettyConstant.LOCAL_PORT)).sync();
            future.channel().closeFuture().sync();
        } finally {
            // 首先监听网络断连事件,如果ChanneI关闭,则执行后续的重连任务,通过Bootstrap
            //重新发起连接,客户端挂在cIoseFuture上监听链路关闭信号,一旦关闭,则创建重连定时
            //器,5s之后重新发起连接,直到重连成功。
            //服务端感知到断连事件之后,需要清空缓存的登录认证注册信息,以保证后续客户端
            //能够正常重连。
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                        try {
                            connect(NettyConstant.PORT, NettyConstant.REMOTEIP);// 发起重连操作
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    /**
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        new NettyClient().connect(NettyConstant.PORT, NettyConstant.REMOTEIP);
    }

}


6.发起请求及请求处理

客户端

package com.alen.netty.protocol.client;

import com.alen.netty.protocol.model.Header;
import com.alen.netty.protocol.model.MessageType;
import com.alen.netty.protocol.model.NettyMessage;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;


public class NettyClientHandler extends ChannelInboundHandlerAdapter {


    private static int i = 0;//发五次测试请求

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        NettyMessage message = (NettyMessage) msg;
        if (i <= 5) {
            //测试请求
            Thread t = new Thread(new TestTask(ctx));
            t.start();
            i++;
        }
        // 如果是响应请求消息,处理,其它消息透传
        if (message.getHeader() != null
                && message.getHeader().getType() == MessageType.SERVICE_RESP
                .value()) {
            System.out.println("响应信息-------------" + message.toString());
        } else {
            ctx.fireChannelRead(msg);
        }
    }
    
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
    }

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


    private class TestTask implements Runnable {
        private final ChannelHandlerContext ctx;

        public TestTask(final ChannelHandlerContext ctx) {
            this.ctx = ctx;
        }

        @Override
        public void run() {
            NettyMessage heatBeat = buildClientRequest();
            System.out
                    .println("客户端发送请求 : ---> "
                            + heatBeat);
            ctx.writeAndFlush(heatBeat);
        }

        private NettyMessage buildClientRequest() {
            NettyMessage message = new NettyMessage();
            Header header = new Header();
            header.setType(MessageType.SERVICE_REQ.value());
            message.setHeader(header);
            message.setBody("测试请求");
            return message;
        }
    }

}

服务端
package com.alen.netty.protocol.server;


import com.alen.netty.protocol.model.Header;
import com.alen.netty.protocol.model.MessageType;
import com.alen.netty.protocol.model.NettyMessage;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;


public class ServerHandler extends ChannelInboundHandlerAdapter {


    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println(" server channel active... ");
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        NettyMessage message = (NettyMessage) msg;

        // 如果是握手请求消息,处理,其它消息透传
        if (message.getHeader() != null
                && message.getHeader().getType() == MessageType.SERVICE_REQ
                .value()) {
            System.out.println(" 处理请求... " + message.toString());
            NettyMessage loginResp = this.buildResponse("--------测试响应----------");
            ctx.writeAndFlush(loginResp);
        } else {
            ctx.fireChannelRead(msg);
        }
    }

    private NettyMessage buildResponse(String result) {
        NettyMessage message = new NettyMessage();
        Header header = new Header();
        header.setType(MessageType.SERVICE_RESP.value());
        message.setHeader(header);
        message.setBody(result);
        return message;
    }

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

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {

    }

}
参考netty权威指南

猜你喜欢

转载自blog.csdn.net/u014401141/article/details/79239860