网络编程11

实现自己的通信框架

  • 通信协议
    • 通信协议从广义上区分,可以分为公有协议和私有协议。由于私有协议的灵活性,它往往会在某个公司或者组织内部使用,按需定制,也因为如此,升级起来会非常方便,灵活性好。绝大多数的私有协议传输层都基于TCP/IP,所以利用Netty的NIO TCP协议栈可以非常方便地进行私有协议的定制和开发。
  • 私有协议
    • 私有协议本质上是厂商内部发展和采用的标准,除非授权,其他厂商一般无权使用该协议。私有协议也称非标准协议,就是未经国际或国家标准化组织采纳或批准,由某个企业自己制订,协议实现细节不愿公开,只在企业自己生产的设备之间使用的协议。私有协议具有封闭性、垄断性、排他性等特点。
  • 已有的框架
    • jdk自带的rmi
    • facebook的thrift
    • apache的avro
    • http+xml

协议栈功能设计

  • 功能描述

    • 基于Netty的NIO通信框架,提供高性能的异步通信能力;

    • 提供消息的编解码框架,可以实现POJO的序列化和反序列化;

    • 提供基于IP地址的白名单接入认证机制,持久化可以写到mysql、zookeeper,现在就是写到本地缓存里面

    • 链路的有效性校验机制;

      客户端和服务器建立了tcp连接,是不是保活的?

      1.tcp本身有keep-alive的保活机制,但是是2个小时,时间太长了,所以一般是在应用层实现心跳机制。

      2.tcp只能对端指定端口的应用程序可以接受报文,但是这个应用程序能不能正确处理业务是不能保证的

    • 链路的断连重连机制。

  •     ------握手请求------->		
      			  <------握手应答--------
      			   ------业务数据------->
    客户端                                               服务端
                   ------心跳---------->
                  <------心跳-----------
                  <------业务数据--------
    

消息定义

  • Netty协议栈消息定义包含两部分:消息头、消息体

消息结构

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

消息头结构

  • 名称 类型 长度 描述
    crcCode Int 32 Netty消息校验码
    Length Int 32 整个消息长度
    sessionID Long 64 会话ID
    Type Byte 8 0:业务请求消息1:业务响应消息2:业务one way消息3握手请求消息4握手应答消息5:心跳请求消息6:心跳应答消息
    Priority Byte 8 消息优先级:0~255
    Attachment Map<String,Object> 变长 可选字段,由于推展消息头

其他

链路的建立和关闭

  • 客户端发送握手请求,服务器端验证ip地址是否是可以建立连接的地址

可靠性

  • 心跳机制
    • 定期发送心跳信息
  • 重连
    • 如果没有收到心跳信息,会发起重连
  • 重新登录保护
    • 对同一个ip地址只允许登陆一次,第二次登陆失效第一次,重新登陆

实现

MessageType

  • package cn.enjoyedu.nettyadv.vo;
    
    /**
     * @author Mark老师   
     * 类说明:消息的类型定义
     */
    public enum MessageType {
          
          
    
        SERVICE_REQ((byte) 0),/*业务请求消息*/
        SERVICE_RESP((byte) 1), /*业务应答消息*/
        ONE_WAY((byte) 2), /*无需应答的业务请求消息*/
        LOGIN_REQ((byte) 3), /*登录请求消息*/
        LOGIN_RESP((byte) 4), /*登录响应消息*/
        HEARTBEAT_REQ((byte) 5), /*心跳请求消息*/
        HEARTBEAT_RESP((byte) 6);/*心跳应答消息*/
    
        private byte value;
    
        private MessageType(byte value) {
          
          
    	this.value = value;
        }
    
        public byte value() {
          
          
    	return this.value;
        }
    }
    
    

MyHeader

  • package cn.enjoyedu.nettyadv.vo;
    
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * @author Mark老师   
     * 类说明:消息头
     */
    public final class MyHeader {
          
          
    
        /*CRC校验*/
        private int crcCode = 0xabef0101;
    
        /*消息长度*/
        private int length;
    
        /*会话ID*/
        private long sessionID;
    
        /*消息类型*/
        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 "MyHeader [crcCode=" + crcCode + ", length=" + length
                + ", sessionID=" + sessionID + ", type=" + type + ", priority="
                + priority + ", attachment=" + attachment + "]";
        }
    
    }
    
    

MyMessage

  • package cn.enjoyedu.nettyadv.vo;
    
    /**
     * @author Mark老师   
     * 类说明:消息实体类
     */
    public final class MyMessage {
          
          
    
        // 消息头
        private MyHeader myHeader;
    
        // 消息体
        // 发送的消息可能是很多类型,同一定义为object
        private Object body;
    
        public final MyHeader getMyHeader() {
          
          
        	return myHeader;
        }
    
        public final void setMyHeader(MyHeader myHeader) {
          
          
        	this.myHeader = myHeader;
        }
    
        public final Object getBody() {
          
          
        	return body;
        }
    
        public final void setBody(Object body) {
          
          
        	this.body = body;
        }
    
        @Override
        public String toString() {
          
          
        	return "MyMessage [myHeader=" + myHeader + "][body="+body+"]";
        }
    }
    
    

NettyConstant

  • package cn.enjoyedu.nettyadv.vo;
    
    /**
     * @author Mark老师  
     * 类说明:常量说明
     */
    public final class NettyConstant {
          
          
        public static final String SERVER_IP = "127.0.0.1";
        public static final int SERVER_PORT = 8989;
    }
    
    

序列化

  • messagepack
    • 在每一个类上都要加上@Message注解
  • probuf
  • kyro
    • 只能在java上用
    • 不需要加注解
  • fastjson

KryoDecoder

  • package cn.enjoyedu.nettyadv.kryocodec;
    
    import io.netty.buffer.ByteBuf;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.handler.codec.ByteToMessageDecoder;
    
    import java.util.List;
    
    /**
     * @author Mark老师   
     * 类说明:反序列化的Handler
     */
    public class KryoDecoder extends ByteToMessageDecoder {
          
          
    
        @Override
        protected void decode(ChannelHandlerContext ctx, ByteBuf in,
                              List<Object> out) throws Exception {
          
          
            Object obj = KryoSerializer.deserialize(in);
            out.add(obj);
        }
    }
    
    

KryoEncoder

  • package cn.enjoyedu.nettyadv.kryocodec;
    
    import cn.enjoyedu.nettyadv.vo.MyMessage;
    import io.netty.buffer.ByteBuf;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.handler.codec.MessageToByteEncoder;
    
    /**
     * @author Mark老师  
     * 类说明:序列化的Handler
     */
    public class KryoEncoder  extends MessageToByteEncoder<MyMessage> {
          
          
    
        @Override
        protected void encode(ChannelHandlerContext ctx, MyMessage message,
                              ByteBuf out) throws Exception {
          
          
            KryoSerializer.serialize(message, out);
            ctx.flush();
        }
    }
    
    

KryoFactory

  • package cn.enjoyedu.nettyadv.kryocodec;
    
    import com.esotericsoftware.kryo.Kryo;
    import com.esotericsoftware.kryo.serializers.DefaultSerializers;
    import de.javakaffee.kryoserializers.*;
    
    import java.lang.reflect.InvocationHandler;
    import java.math.BigDecimal;
    import java.math.BigInteger;
    import java.net.URI;
    import java.text.SimpleDateFormat;
    import java.util.*;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.regex.Pattern;
    
    /**
     * @author Mark老师   
     * 类说明:Kryo的工厂,拿到Kryo的实例
     */
    public class KryoFactory {
          
          
    
        public static Kryo createKryo() {
          
          
    
            Kryo kryo = new Kryo();
            kryo.setRegistrationRequired(false);
            kryo.register(Arrays.asList("").getClass(), new ArraysAsListSerializer());
            kryo.register(GregorianCalendar.class, new GregorianCalendarSerializer());
            kryo.register(InvocationHandler.class, new JdkProxySerializer());
            kryo.register(BigDecimal.class, new DefaultSerializers.BigDecimalSerializer());
            kryo.register(BigInteger.class, new DefaultSerializers.BigIntegerSerializer());
            kryo.register(Pattern.class, new RegexSerializer());
            kryo.register(BitSet.class, new BitSetSerializer());
            kryo.register(URI.class, new URISerializer());
            kryo.register(UUID.class, new UUIDSerializer());
            UnmodifiableCollectionsSerializer.registerSerializers(kryo);
            SynchronizedCollectionsSerializer.registerSerializers(kryo);
    
            kryo.register(HashMap.class);
            kryo.register(ArrayList.class);
            kryo.register(LinkedList.class);
            kryo.register(HashSet.class);
            kryo.register(TreeSet.class);
            kryo.register(Hashtable.class);
            kryo.register(Date.class);
            kryo.register(Calendar.class);
            kryo.register(ConcurrentHashMap.class);
            kryo.register(SimpleDateFormat.class);
            kryo.register(GregorianCalendar.class);
            kryo.register(Vector.class);
            kryo.register(BitSet.class);
            kryo.register(StringBuffer.class);
            kryo.register(StringBuilder.class);
            kryo.register(Object.class);
            kryo.register(Object[].class);
            kryo.register(String[].class);
            kryo.register(byte[].class);
            kryo.register(char[].class);
            kryo.register(int[].class);
            kryo.register(float[].class);
            kryo.register(double[].class);
    
            return kryo;
        }
    }
    
    

KryoSerializer

  • package cn.enjoyedu.nettyadv.kryocodec;
    
    import com.esotericsoftware.kryo.Kryo;
    import com.esotericsoftware.kryo.io.Input;
    import com.esotericsoftware.kryo.io.Output;
    import io.netty.buffer.ByteBuf;
    import io.netty.buffer.ByteBufInputStream;
    
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    
    /**
     * @author Mark老师   
     * 类说明:Kryo的序列化器,负责序列化和反序列化
     */
    public class KryoSerializer {
          
          
        private static Kryo kryo = KryoFactory.createKryo();
    
        /*序列化*/
        public static void serialize(Object object, ByteBuf out) {
          
          
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            Output output = new Output(baos);
            kryo.writeClassAndObject(output, object);
            output.flush();
            output.close();
    
            byte[] b = baos.toByteArray();
            try {
          
          
                baos.flush();
                baos.close();
            } catch (IOException e) {
          
          
                e.printStackTrace();
            }
            out.writeBytes(b);
        }
    
        /*反序列化*/
        public static Object deserialize(ByteBuf out) {
          
          
            if (out == null) {
          
          
                return null;
            }
            Input input = new Input(new ByteBufInputStream(out));
            return kryo.readClassAndObject(input);
        }
    }
    
    

服务端

NettyServer

  • package cn.enjoyedu.nettyadv;
    
    import cn.enjoyedu.nettyadv.server.ServerInit;
    import cn.enjoyedu.nettyadv.vo.NettyConstant;
    import io.netty.bootstrap.ServerBootstrap;
    import io.netty.channel.ChannelOption;
    import io.netty.channel.EventLoopGroup;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.nio.NioServerSocketChannel;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    
    /**
     * @author Mark老师   
     * 类说明:服务端的主入口
     */
    public class NettyServer {
          
          
    
    	private static final Log LOG = LogFactory.getLog(NettyServer.class);
    
        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, 1024)
                .childHandler(new ServerInit());
    
            // 绑定端口,同步等待成功
            b.bind(NettyConstant.SERVER_PORT).sync();
                LOG.info("Netty server start : "
                    + (NettyConstant.SERVER_IP + " : "
                        + NettyConstant.SERVER_PORT));
        }
    
        public static void main(String[] args) throws Exception {
          
          
    	    new NettyServer().bind();
        }
    }
    
    

ServerInit

  • package cn.enjoyedu.nettyadv.server;
    
    import cn.enjoyedu.nettyadv.kryocodec.KryoDecoder;
    import cn.enjoyedu.nettyadv.kryocodec.KryoEncoder;
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.socket.SocketChannel;
    import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
    import io.netty.handler.codec.LengthFieldPrepender;
    import io.netty.handler.timeout.ReadTimeoutHandler;
    
    /**
     * @author Mark老师   
     * 类说明:
     */
    public class ServerInit extends ChannelInitializer<SocketChannel> {
          
          
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
          
          
    
            /*粘包半包问题*/
            // 用第三种消息头消息体的方法解决粘包半包
            // 消息最长65535
            // 0,2,0,2是用来标识消息头消息体的长度和格式的
            ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(65535,
                    0,2,0,
                    2));
            // 给客户端做应答报文
            // 给发出去的报文加个消息头
            ch.pipeline().addLast(new LengthFieldPrepender(2));
    
            /*序列化相关*/
            // kryo
            ch.pipeline().addLast(new KryoDecoder());
            ch.pipeline().addLast(new KryoEncoder());
    
            /*处理心跳超时*/
            // 超过15秒对端没有发送过来心跳报文,则会抛出异常
            ch.pipeline().addLast(new ReadTimeoutHandler(15));
    
            // 这三个handler是必须需要的
            // 是消息协议栈本身需要的
    		// 请求应答handler
            ch.pipeline().addLast(new LoginAuthRespHandler());
            // 心跳相关handler
            ch.pipeline().addLast(new HeartBeatRespHandler());
            // 业务handler
            ch.pipeline().addLast(new ServerBusiHandler());
    
        }
    }
    
    

LoginAuthRespHandler

  • package cn.enjoyedu.nettyadv.server;
    
    import cn.enjoyedu.nettyadv.vo.MessageType;
    import cn.enjoyedu.nettyadv.vo.MyHeader;
    import cn.enjoyedu.nettyadv.vo.MyMessage;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.ChannelInboundHandlerAdapter;
    import io.netty.util.ReferenceCountUtil;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    
    import java.net.InetSocketAddress;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * @author Mark老师   
     * 类说明:登录检查
     */
    public class LoginAuthRespHandler extends ChannelInboundHandlerAdapter {
          
          
    
    	private final static Log LOG
                = LogFactory.getLog(LoginAuthRespHandler.class);
    
    	//用以检查用户是否重复登录的缓存
        private static Map<String, Boolean> nodeCheck = new ConcurrentHashMap<String, Boolean>();
        //用户登录的白名单
        private String[] whitekList = {
          
           "127.0.0.1"};
    
        public void channelRead(ChannelHandlerContext ctx, Object msg)
    	    throws Exception {
          
          
    		MyMessage message = (MyMessage) msg;
    		/*是不是握手认证请求*/
            // 消息头非空
            // 消息头的类型是不是自己定义的业务请求类型
    		if(message.getMyHeader()!=null
    			&&message.getMyHeader().getType()==MessageType.LOGIN_REQ.value()){
          
          
    			String nodeIndex = ctx.channel().remoteAddress().toString();
    			MyMessage 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);
    			}
    			LOG.info("The login response is : " + loginResp
    					+ " body [" + loginResp.getBody() + "]");
                // 认证结果发送给对端
    			ctx.writeAndFlush(loginResp);
                // 释放buffer,防止内存泄漏
    			ReferenceCountUtil.release(msg);
    		}else{
          
          
                // 继续往后传
    			ctx.fireChannelRead(msg);
    		}
        }
    
        private MyMessage buildResponse(byte result) {
          
          
    		MyMessage message = new MyMessage();
    		MyHeader myHeader = new MyHeader();
    		myHeader.setType(MessageType.LOGIN_RESP.value());
    		message.setMyHeader(myHeader);
    		message.setBody(result);
    		return message;
        }
    
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
    	    throws Exception {
          
          
    		cause.printStackTrace();
            // 删除缓存
    		nodeCheck.remove(ctx.channel().remoteAddress().toString());
    		ctx.close();
    		ctx.fireExceptionCaught(cause);
        }
    }
    
    

HeartBeatRespHandler

  • package cn.enjoyedu.nettyadv.server;
    
    
    import cn.enjoyedu.nettyadv.vo.MessageType;
    import cn.enjoyedu.nettyadv.vo.MyHeader;
    import cn.enjoyedu.nettyadv.vo.MyMessage;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.ChannelInboundHandlerAdapter;
    import io.netty.util.ReferenceCountUtil;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    
    /**
     * @author Mark老师   
     * 类说明:心跳处理完成
     */
    public class HeartBeatRespHandler extends ChannelInboundHandlerAdapter {
          
          
    
    	private static final Log LOG
    			= LogFactory.getLog(HeartBeatRespHandler.class);
    
        public void channelRead(ChannelHandlerContext ctx, Object msg)
    	    throws Exception {
          
          
    		MyMessage message = (MyMessage) msg;
    		/*是不是心跳请求*/
    		if(message.getMyHeader()!=null
    				&&message.getMyHeader().getType()==MessageType.HEARTBEAT_REQ.value()){
          
          
    			/*心跳应答报文*/
    			MyMessage heartBeatResp = buildHeatBeat();
    			ctx.writeAndFlush(heartBeatResp);
    			ReferenceCountUtil.release(msg);
    		}else{
          
          
    			ctx.fireChannelRead(msg);
    		}
        }
    
        private MyMessage buildHeatBeat() {
          
          
    		MyMessage message = new MyMessage();
    		MyHeader myHeader = new MyHeader();
    		myHeader.setType(MessageType.HEARTBEAT_RESP.value());
    		message.setMyHeader(myHeader);
    		return message;
        }
    
    }
    
    

ServerBusiHandler

  • package cn.enjoyedu.nettyadv.server;
    
    import cn.enjoyedu.nettyadv.vo.MyMessage;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.SimpleChannelInboundHandler;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    
    /**
     * @author Mark老师   
     * 类说明:业务处理类
     */
    public class ServerBusiHandler
            extends SimpleChannelInboundHandler<MyMessage> {
          
          
        private static final Log LOG
                = LogFactory.getLog(ServerBusiHandler.class);
    
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, MyMessage msg)
                throws Exception {
          
          
            LOG.info(msg);
        }
    
        @Override
        public void channelInactive(ChannelHandlerContext ctx)
                throws Exception {
          
          
            LOG.info(ctx.channel().remoteAddress()+" 主动断开了连接!");
        }
    
    }
    
    

客户端

NettyClient

  • package cn.enjoyedu.nettyadv;
    
    import cn.enjoyedu.nettyadv.client.ClientInit;
    import cn.enjoyedu.nettyadv.vo.MessageType;
    import cn.enjoyedu.nettyadv.vo.MyHeader;
    import cn.enjoyedu.nettyadv.vo.MyMessage;
    import cn.enjoyedu.nettyadv.vo.NettyConstant;
    import io.netty.bootstrap.Bootstrap;
    import io.netty.channel.Channel;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.ChannelOption;
    import io.netty.channel.EventLoopGroup;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.nio.NioSocketChannel;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    
    import java.net.InetSocketAddress;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author Mark老师   
     * 类说明:Netty客户端的主入口
     */
    public class NettyClient implements Runnable{
          
          
    
        private static final Log LOG = LogFactory.getLog(NettyClient.class);
    
        /*负责重连的线程池*/
        private ScheduledExecutorService executor = Executors
                .newScheduledThreadPool(1);
        private Channel channel;
        private EventLoopGroup group = new NioEventLoopGroup();
    
        /*是否用户主动关闭连接的标志值*/
        private volatile boolean userClose = false;
        /*连接是否成功关闭的标志值*/
        private volatile boolean connected = false;
    
    
        public boolean isConnected() {
          
          
            return connected;
        }
    
        /*连接服务器*/
        public void connect(int port,String host) throws InterruptedException {
          
          
            try {
          
          
                /*客户端启动必备*/
                Bootstrap b = new Bootstrap();
                b.group(group)
                        .channel(NioSocketChannel.class)/*指定使用NIO的通信模式*/
                .handler(new ClientInit());
                ChannelFuture future = b.connect(new InetSocketAddress(host,port)).sync();
                // 通过future拿到channel
                channel = future.sync().channel();
                synchronized (this){
          
          
                    this.connected = true;
                    this.notifyAll();
                }
                // 关闭事件上进行阻塞
                // 防止主线程结束了
                future.channel().closeFuture().sync();
            } finally {
          
          
                // 如果不是用户手动关闭
                if(!userClose){
          
          
                    /*非正常关闭,有可能发生了网络问题,进行重连*/
                    // 实现断线重连
                    System.out.println("需要进行重连");
                    // 专门负责重连的线程池
                    executor.execute(new Runnable() {
          
          
                        @Override
                        public void run() {
          
          
                            try {
          
          
                                /*给操作系统足够的时间,去释放相关的资源*/
                                // 由于操作系统释放端口是需要一定时间的
                                // 所以重连前等待操作系统释放端口
                                TimeUnit.SECONDS.sleep(1);
                                connect(NettyConstant.SERVER_PORT,
                                        NettyConstant.SERVER_IP);
                            } catch (InterruptedException e) {
          
          
                                e.printStackTrace();
                            }
                        }
                    });
    
                }else{
          
          
                    /*正常关闭*/
                    channel = null;
                    // 进行group的优雅关闭
                    group.shutdownGracefully().sync();
                    synchronized (this){
          
          
                        this.connected = false;
                        this.notifyAll();
                    }
                }
            }
        }
    
        @Override
        public void run() {
          
          
            try {
          
          
                connect(NettyConstant.SERVER_PORT,NettyConstant.SERVER_IP);
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
        }
    
        /*------------测试NettyClient--------------------------*/
        public static void main(String[] args) throws Exception {
          
          
    //        NettyClient nettyClient = new NettyClient();
    //        nettyClient.connect(NettyConstant.REMOTE_PORT
    //                , NettyConstant.REMOTE_IP);
        }
    
        /*------------以下方法供业务方使用--------------------------*/
        public void send(Object message) {
          
          
            if(channel==null||!channel.isActive()){
          
          
                throw new IllegalStateException("和服务器还未未建立起有效连接!" +
                        "请稍后再试!!");
            }
            MyMessage msg = new MyMessage();
            MyHeader myHeader = new MyHeader();
            myHeader.setType(MessageType.SERVICE_REQ.value());
            msg.setMyHeader(myHeader);
            msg.setBody(message);
            channel.writeAndFlush(msg);
        }
    
        public void close() {
          
          
            userClose = true;
            channel.close();
        }
    
    
    }
    
    
  • 客户端除了发送正常的消息外,还要做两件事

    • 1.发送连接请求
    • 2.发送心跳包
  • 断线重连实现的比较粗糙,没有判断异常,比较合适是应该判断异常类型,根据异常类型做处理

ClientInit

  • package cn.enjoyedu.nettyadv.client;
    
    import cn.enjoyedu.nettyadv.kryocodec.KryoDecoder;
    import cn.enjoyedu.nettyadv.kryocodec.KryoEncoder;
    import cn.enjoyedu.nettyadv.server.HeartBeatRespHandler;
    import cn.enjoyedu.nettyadv.server.LoginAuthRespHandler;
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.socket.SocketChannel;
    import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
    import io.netty.handler.codec.LengthFieldPrepender;
    import io.netty.handler.logging.LogLevel;
    import io.netty.handler.logging.LoggingHandler;
    import io.netty.handler.timeout.ReadTimeoutHandler;
    
    /**
     * @author Mark老师   
     * 类说明:客户端Handler的初始化
     */
    public class ClientInit extends ChannelInitializer<SocketChannel> {
          
          
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
          
          
            // 把客户端收到的报文打印出来
            ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
            /*粘包半包问题*/
            ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(65535,
                    0,2,0,
                    2));
            ch.pipeline().addLast(new LengthFieldPrepender(2));
    
            /*序列化相关*/
            ch.pipeline().addLast(new KryoDecoder());
            ch.pipeline().addLast(new KryoEncoder());
    
            /*处理心跳超时*/
            ch.pipeline().addLast(new ReadTimeoutHandler(15));
    		// 连接请求handler
            ch.pipeline().addLast(new LoginAuthReqHandler());
            // 心跳请求handler
            ch.pipeline().addLast(new HeartBeatReqHandler());
        }
    }
    
    

BusiClient

  • package cn.enjoyedu.nettyadv;
    
    import cn.enjoyedu.nettyadv.busivo.User;
    import cn.enjoyedu.nettyadv.busivo.UserContact;
    
    import java.util.Scanner;
    
    /**
     * @author Mark老师   
     * 往期课程和VIP课程咨询 依娜老师  QQ:2133576719
     * 类说明:业务方如何调用Netty客户端演示
     */
    public class BusiClient {
          
          
        public static void main(String[] args) throws Exception {
          
          
            NettyClient nettyClient = new NettyClient();
            new Thread(nettyClient).start();
            // 因为是上面启动的线程来发起连接操作的
            // 需要netty客户端通知
            while(!nettyClient.isConnected()){
          
          
                synchronized (nettyClient){
          
          
                    nettyClient.wait();
                }
            }
            System.out.println("网络通信已准备好,可以进行业务操作了........");
            Scanner scanner = new Scanner(System.in);
            while (true) {
          
          
                String msg = scanner.next();
                if (msg == null) {
          
          
                    break;
                } else if ("q".equals(msg.toLowerCase())) {
          
          
                    nettyClient.close();
                    while(nettyClient.isConnected()){
          
          
                        synchronized (nettyClient){
          
          
                            nettyClient.wait();
                        }
                    }
                    scanner.close();
                    System.exit(1);
                } else if("v".equals(msg.toLowerCase())){
          
          
                    User user = new User();
                    user.setAge(19);
                    String userName = "ABCDEFG --->1";
                    user.setUserName(userName);
                    user.setId("No:1");
                    user.setUserContact(
                            new UserContact(userName+"@xiangxue.com",
                                    "133"));
                    nettyClient.send(user);
                }
                else {
          
          
                    nettyClient.send(msg);
                }
            }
        }
    }
    
    
  • 发送请求时,通过BusiClient new出一个NettyClient,并启动一个线程在执行nettyClient来发送消息

LoginAuthReqHandler

  • package cn.enjoyedu.nettyadv.client;
    
    import cn.enjoyedu.nettyadv.vo.MessageType;
    import cn.enjoyedu.nettyadv.vo.MyHeader;
    import cn.enjoyedu.nettyadv.vo.MyMessage;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.ChannelInboundHandlerAdapter;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    
    /**
     * @author Mark老师   
     * 类说明:发起登录请求
     */
    public class LoginAuthReqHandler extends ChannelInboundHandlerAdapter {
          
          
    
        private static final Log LOG = LogFactory.getLog(LoginAuthReqHandler.class);
    
        // 发送向服务器的认证请求
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
          
          
            /*发出认证请求*/
            ctx.writeAndFlush(buildLoginReq());
        }
    
        // 接受服务器的认证响应
        public void channelRead(ChannelHandlerContext ctx, Object msg)
                throws Exception {
          
          
            MyMessage message = (MyMessage) msg;
            /*是不是握手成功的应答*/
            if(message.getMyHeader()!=null
                    &&message.getMyHeader().getType()==MessageType.LOGIN_RESP.value()){
          
          
                byte loginResult = (byte) message.getBody();
                if (loginResult != (byte) 0) {
          
          
                    // 握手失败,关闭连接
                    ctx.close();
                } else {
          
          
                    LOG.info("Login is ok : " + message);
                    ctx.fireChannelRead(msg);
                }
            }else{
          
          
                ctx.fireChannelRead(msg);
            }
        }
    
        // 组装认证请求信息
        private MyMessage buildLoginReq() {
          
          
            MyMessage message = new MyMessage();
            MyHeader myHeader = new MyHeader();
            myHeader.setType(MessageType.LOGIN_REQ.value());
            message.setMyHeader(myHeader);
            return message;
        }
    
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
                throws Exception {
          
          
            ctx.fireExceptionCaught(cause);
        }
    }
    
    

HeartBeatReqHandler

  • package cn.enjoyedu.nettyadv.client;
    
    import cn.enjoyedu.nettyadv.vo.MessageType;
    import cn.enjoyedu.nettyadv.vo.MyHeader;
    import cn.enjoyedu.nettyadv.vo.MyMessage;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.ChannelInboundHandlerAdapter;
    import io.netty.util.ReferenceCountUtil;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    
    import java.util.concurrent.ScheduledFuture;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * @author Mark老师  
     * 类说明:心跳请求处理
     */
    public class HeartBeatReqHandler extends ChannelInboundHandlerAdapter {
          
          
    
        private static final Log LOG = LogFactory.getLog(HeartBeatReqHandler.class);
        private volatile  ScheduledFuture<?> heartBeat;
    
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
          
          
            MyMessage message = (MyMessage) msg;
            /*是不是握手认证成功的应答*/
            // 是握手成功的应答才发起心跳请求
            if(message.getMyHeader()!=null
                    &&message.getMyHeader().getType()==MessageType.LOGIN_RESP.value()){
          
          
                /*Netty已经提供了定时机制,定时发出心跳请求*/
                // 每5秒发送一个心跳请求
                heartBeat = ctx.executor().scheduleAtFixedRate(
                        new  HeartBeatReqHandler.HeartBeatTask(ctx),0,
                        5000,TimeUnit.MILLISECONDS);
                // 把认证请求报文释放掉
                ReferenceCountUtil.release(msg);
                /*是不是心跳的应答*/
            }else if(message.getMyHeader()!=null
                    &&message.getMyHeader().getType()==MessageType.HEARTBEAT_RESP.value()){
          
          
                //LOG.info("收到服务器心跳应答");
                // 心跳报文释放
                ReferenceCountUtil.release(msg);
            }else{
          
          
                ctx.fireChannelRead(msg);
            }
        }
    
        /*心跳请求的任务类*/
        private class HeartBeatTask implements Runnable {
          
          
            private final ChannelHandlerContext ctx;
    
            public HeartBeatTask(ChannelHandlerContext ctx) {
          
          
                this.ctx = ctx;
            }
    
            @Override
            public void run() {
          
          
                MyMessage heartBeat = buildHeatBeat();
                ctx.writeAndFlush(heartBeat);
            }
    
            // 组装心跳报文
            private MyMessage buildHeatBeat() {
          
          
                MyMessage message = new MyMessage();
                MyHeader myHeader = new MyHeader();
                myHeader.setType(MessageType.HEARTBEAT_REQ.value());
                message.setMyHeader(myHeader);
                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);
        }
    }
    
    

用netty框架改写之前的rpc

  • 之前的rpc就是通过短连接下的输入输出流实现的

新RpcServerFrame

  • package cn.enjoyedu.rpcnetty.rpc.base;
    
    import cn.enjoyedu.rpcnetty.remote.SendSms;
    import cn.enjoyedu.rpcnetty.rpc.base.server.ServerInit;
    import cn.enjoyedu.rpcnetty.rpc.base.vo.NettyConstant;
    import cn.enjoyedu.rpcnetty.rpc.sms.SendSmsImpl;
    import io.netty.bootstrap.ServerBootstrap;
    import io.netty.channel.ChannelOption;
    import io.netty.channel.EventLoopGroup;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.nio.NioServerSocketChannel;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.PostConstruct;
    import javax.annotation.PreDestroy;
    
    /**
     * @author Mark老师   
     * 类说明:rpc框架的服务端部分,交给Spring 托管
     * 包括Netty组件的初始化,监听端口、实际服务的注册等等
     */
    @Service
    public class RpcServerFrame implements Runnable{
          
          
    
        @Autowired
        private RegisterService registerService;
        @Autowired
        private ServerInit serverInit;
    
    	private static final Log LOG = LogFactory.getLog(RpcServerFrame.class);
        private EventLoopGroup bossGroup = new NioEventLoopGroup();
        private EventLoopGroup workerGroup = new NioEventLoopGroup();
    
        public void bind() throws Exception {
          
          
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 1024)
                .childHandler(serverInit);
    
            // 绑定端口,同步等待成功
            b.bind(NettyConstant.REMOTE_PORT).sync();
            LOG.info("网络服务已准备好,可以进行业务操作了....... : "
                + (NettyConstant.REMOTE_IP + " : "
                    + NettyConstant.REMOTE_PORT));
        }
    
        // 当这个类启动时,启动netty的服务器端
        @PostConstruct
        public void startNet() throws Exception {
          
          
            registerService.regService(SendSms.class.getName(), SendSmsImpl.class);
            new Thread(this).start();
        }
    
        @PreDestroy
        public void stopNet() throws InterruptedException {
          
          
            bossGroup.shutdownGracefully().sync();
            workerGroup.shutdownGracefully().sync();
        }
    
        @Override
        public void run() {
          
          
            try {
          
          
                bind();
            } catch (Exception e) {
          
          
                e.printStackTrace();
            }
        }
    }
    
    
  • 要实现netty的绑定

新RpcClientFrame

  • package cn.enjoyedu.rpcnetty.rpc;
    
    import cn.enjoyedu.rpcnetty.rpc.base.vo.NettyConstant;
    import cn.enjoyedu.rpcnetty.rpc.client.ClientBusiHandler;
    import cn.enjoyedu.rpcnetty.rpc.client.ClientInit;
    import io.netty.bootstrap.Bootstrap;
    import io.netty.channel.Channel;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.ChannelOption;
    import io.netty.channel.EventLoopGroup;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.nio.NioSocketChannel;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.PostConstruct;
    import javax.annotation.PreDestroy;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.net.InetSocketAddress;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    /**
     *@author Mark老师  
     *类说明:rpc框架的客户端代理部分,交给Spring 托管
     * 1、动态代理的实现中,不再连接服务器,而是直接发送请求
     * 2、客户端网络部分的主体,包括Netty组件的初始化,连接服务器等
     */
    @Service
    public class RpcClientFrame implements Runnable{
          
          
    
        private static final Log LOG = LogFactory.getLog(RpcClientFrame.class);
    
        private ScheduledExecutorService executor = Executors
                .newScheduledThreadPool(1);
        private Channel channel;
        private EventLoopGroup group = new NioEventLoopGroup();
    
        /*是否用户主动关闭连接的标志值*/
        private volatile boolean userClose = false;
        /*连接是否成功关闭的标志值*/
        private volatile boolean connected = false;
    
        @Autowired
        private ClientInit clientInit;
        @Autowired
        private ClientBusiHandler clientBusiHandler;
    
        /*远程服务的代理对象,参数为客户端要调用的的服务*/
        public <T> T getRemoteProxyObject(final Class<?> serviceInterface) throws Exception {
          
          
    
            /*拿到一个代理对象,由这个代理对象通过网络进行实际的服务调用*/
            return (T)Proxy.newProxyInstance(serviceInterface.getClassLoader(),
                    new Class<?>[]{
          
          serviceInterface},
                    new DynProxy(serviceInterface,clientBusiHandler));
        }
    
        /*动态代理,实现对远程服务的访问*/
        private static class DynProxy implements InvocationHandler{
          
          
            private Class<?> serviceInterface;
            private ClientBusiHandler clientBusiHandler;
    
            public DynProxy(Class<?> serviceInterface, ClientBusiHandler clientBusiHandler) {
          
          
                this.serviceInterface = serviceInterface;
                this.clientBusiHandler = clientBusiHandler;
            }
    
            @Override
            public Object invoke(Object proxy, Method method, Object[] args)
                    throws Throwable {
          
          
                Map<String,Object> content = new HashMap<>();
                content.put("siName",serviceInterface.getName());
                content.put("methodName",method.getName());
                content.put("paraTypes",method.getParameterTypes());
                content.put("args",args);
                return clientBusiHandler.send(content);
            }
        }
    
        public boolean isConnected() {
          
          
            return connected;
        }
    
        /*连接服务器*/
        public void connect(int port, String host) throws Exception {
          
          
    
            try {
          
          
                Bootstrap b = new Bootstrap();
                b.group(group).channel(NioSocketChannel.class)
                        .option(ChannelOption.TCP_NODELAY, true)
                        .handler(clientInit);
                // 发起异步连接操作
                ChannelFuture future = b.connect(
                        new InetSocketAddress(host, port)).sync();
                channel = future.sync().channel();
                /*连接成功后通知等待线程,连接已经建立*/
                synchronized (this){
          
          
                    this.connected = true;
                    this.notifyAll();
                }
                future.channel().closeFuture().sync();
            } finally {
          
          
                if(!userClose){
          
          /*非用户主动关闭,说明发生了网络问题,需要进行重连操作*/
                    System.out.println("发现异常,可能发生了服务器异常或网络问题," +
                            "准备进行重连.....");
                    //再次发起重连操作
                    executor.execute(new Runnable() {
          
          
                        @Override
                        public void run() {
          
          
                            try {
          
          
                                TimeUnit.SECONDS.sleep(1);
                                try {
          
          
                                    // 发起重连操作
                                    connect(NettyConstant.REMOTE_PORT,
                                            NettyConstant.REMOTE_IP);
                                } catch (Exception e) {
          
          
                                    e.printStackTrace();
                                }
                            } catch (InterruptedException e) {
          
          
                                e.printStackTrace();
                            }
                        }
                    });
                }else{
          
          /*用户主动关闭,释放资源*/
                    channel = null;
                    group.shutdownGracefully().sync();
                    connected = false;
    //                synchronized (this){
          
          
    //                    this.connected = false;
    //                    this.notifyAll();
    //                }
                }
            }
        }
    
        @Override
        public void run() {
          
          
            try {
          
          
                connect(NettyConstant.REMOTE_PORT, NettyConstant.REMOTE_IP);
            } catch (Exception e) {
          
          
                e.printStackTrace();
            }
        }
    
        public void close() {
          
          
            userClose = true;
            channel.close();
        }
    
        @PostConstruct
        public void startNet() throws InterruptedException {
          
          
            new Thread(this).start();
            while(!this.isConnected()){
          
          
                synchronized (this){
          
          
                    this.wait();
                }
            }
            LOG.info("网络通信已准备好,可以进行业务操作了........");
        }
    
        @PreDestroy
        public void stopNet(){
          
          
            close();
        }
    
    }
    
    

缺点

新增ClientBusiHandler

  • package cn.enjoyedu.rpcnetty.rpc.client;
    
    import cn.enjoyedu.rpcnetty.rpc.base.vo.MessageType;
    import cn.enjoyedu.rpcnetty.rpc.base.vo.MyHeader;
    import cn.enjoyedu.rpcnetty.rpc.base.vo.MyMessage;
    import io.netty.channel.ChannelHandler;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.SimpleChannelInboundHandler;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.stereotype.Service;
    
    import java.util.Random;
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.BlockingQueue;
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * 新增的客户端业务处理Handler类
     * 并提供了发送数据到服务器的send方法
     * 采用阻塞队列,将异步调用转同步调用
     * 交给Spring 托管
     */
    @Service
    @ChannelHandler.Sharable
    public class ClientBusiHandler extends SimpleChannelInboundHandler<MyMessage> {
          
          
    
        private static final Log LOG = LogFactory.getLog(ClientBusiHandler.class);
        private ChannelHandlerContext ctx;
        private final ConcurrentHashMap<Long, BlockingQueue<Object>> responseMap
                = new ConcurrentHashMap<Long, BlockingQueue<Object>>();
    
        @Override
        public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
          
          
            super.handlerAdded(ctx);
            this.ctx = ctx;
        }
    
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, MyMessage msg) throws Exception {
          
          
            if (msg.getMyHeader() != null
                    && msg.getMyHeader().getType() == MessageType.SERVICE_RESP
                    .value()) {
          
          
                long sessionId = msg.getMyHeader().getSessionID();
                boolean result =  (boolean)msg.getBody();
                BlockingQueue<Object> msgQueue = responseMap.get(sessionId);
                msgQueue.put(result);
            }
        }
    
        public Object send(Object message) throws InterruptedException {
          
          
            if(ctx.channel()==null||!ctx.channel().isActive()){
          
          
                throw new IllegalStateException("和服务器还未未建立起有效连接!" +
                        "请稍后再试!!");
            }
            MyMessage msg = new MyMessage();
            MyHeader myHeader = new MyHeader();
            Random r = new Random();
            long sessionId = r.nextLong()+1;
            myHeader.setSessionID(sessionId);
            myHeader.setType(MessageType.SERVICE_REQ.value());
            msg.setMyHeader(myHeader);
            msg.setBody(message);
            BlockingQueue<Object> msgQueue = new ArrayBlockingQueue<>(1);
            responseMap.put(sessionId,msgQueue);
            ctx.writeAndFlush(msg);
            // 异步转同步
            Object result =  msgQueue.take();
            LOG.info("获取到服务端的处理结果"+result);
            return result;
        }
    }
    
    
  • 实际工作中很少让netty处理业务工作,上面类中的channelRead0方法,应该再分发一次diapatch,交给其他的业务线程池处理

猜你喜欢

转载自blog.csdn.net/Markland_l/article/details/114234661