实现自己的通信框架
- 通信协议
- 通信协议从广义上区分,可以分为公有协议和私有协议。由于私有协议的灵活性,它往往会在某个公司或者组织内部使用,按需定制,也因为如此,升级起来会非常方便,灵活性好。绝大多数的私有协议传输层都基于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,交给其他的业务线程池处理