闲来无聊。公司因为有事情需要借助mqtt等物联网平台,进行收发消息,用了一些java开源的mqtt软件,挺方便的,但是痴迷网络编程的我决定自己写一个类似mqtt一样的发布订阅功能,就看好了netty作为整个通信的骨架,整个项目也就写了三天,项目雏形已现,可以进行测试了,后期将要对其进行改造,当前版本为v1.0,单机版本,后期我会在陆续发布一款适用于分布式的发布订阅系统,楼主也是一个刚毕业的小白,水平有限,忘大家见谅。
1.项目v1.0架构

整个项目是一个代理Broker,和多个Client和Publisher共同组成,Broker负责客户端的订阅注册,负责监听所有连接的心跳,超过指定时限,会自动的剔除,broker还作为鉴权端,认证所有的连接者,认证成功的可以继续进行下一步操作,认证失败则断开连接。client是作为消息的监听者,负责主题的订阅和删除,publisher负责主题消息的发送。
2.项目组织结构
这是真个项目的源码结构图 ,下面将一一讲解。
2.1Broker原理实现
1. 核心模块,netty服务端构建 BrokeServer
public class BrokeServer implements Server {
private NioEventLoopGroup worker=new NioEventLoopGroup();
private NioEventLoopGroup boss=new NioEventLoopGroup();
private final Logger LOGGER=Logger.getLogger(BrokeServer.class);
public final static BrokeServer INSTANCE=new BrokeServer();
private BrokeServer() {
// TODO Auto-generated constructor stub
}
@Override
public void start() {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, worker).channel(NioServerSocketChannel.class)
.childHandler(new ChanelInitializerHandler())
.childOption(ChannelOption.SO_KEEPALIVE, true);
try {
ChannelFuture channelFuture = serverBootstrap.bind(9999).sync();
channelFuture.addListener(new GenericFutureListener<Future<? super Void>>() {
@Override
public void operationComplete(Future<? super Void> future) throws Exception {
if(future.isSuccess()||future.isDone()){
LOGGER.info("【broker已启动】ip->"+channelFuture.channel().remoteAddress()+" port->"+9999);
}else{
LOGGER.info("【broker】启动失败");
}
}
});
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
worker.shutdownGracefully();
boss.shutdownGracefully();
}
}
@Override
public void stop() {
if(worker!=null){
worker.shutdownGracefully();
}
if(boss!=null){
boss.shutdownGracefully();
}
LOGGER.info("【broker】关闭成功");
}
}
就是netty常用的模式,一个worker和boss,也就是Reactor模型,随后添加了监听器,处理器,处理器初始化是 ChanelInitializerHandler(),
public class ChanelInitializerHandler extends ChannelInitializer {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(0,0,15));
pipeline.addLast(new IdleStateTrigger());
//设计包的格式 1字节固定包头 1字节功能码 1字节(判断是否包含topic字段) 4字节固定长度字段 12字节固定topic(非必须) 剩余字节数据
pipeline.addLast(new LengthFieldBasedFrameDecoder(2048, 3, 4, 0, 0));
pipeline.addLast(new MessageToPoDecoder());
//添加认证的处理器
pipeline.addLast("auth",new AuthenticationHandler());
pipeline.addLast(new MessageEncoder());
}
}
2.发送协议讲解
该项目的通信协议非常简单,包体格式如下:
固定包头(1byte) 0xA8 | 功能码(1byte) | topic判断字段(1byte) | 数据长度字段(4byte) | 固定主题字段(12byte) |
数据字段 (可变长) |
协议解析:1.固定包头,根据固定包头来解析数据,防止传入非法的数据包头,很简单有一个字符0xA8
2.功能码,用来确定该数据报的作用,是否是心跳,发布信息,订阅信息,等等描述功能动作
3.topic判断字段,确定是否包含有topic字段,默认 0 没有 1含有topic字段
4.数据长度,4个字节,其数值=固定主题字段长度+数据字段长度
5.固定主题字段,保存topic的信息
6.数据字段,用来传输数据
3.编码器解析
1. IdleStateHandler是一个读写空闲状态的监听器,具体原理很简单,大家可以自行百度
2.IdleStateTrigger自定义类负责监听客户端连接是否有效
3.LengthFieldBasedFrameDecoder(2048, 3, 4, 0, 0)是一个基于固定长度字段的帧格式的协议解码器,netty自带,非常常用
4.MessageToPoDecoder自定义的将bytefuf解析为Message Po对象
5.AuthenticationHandler鉴权认证器
IdleStateTrigger
public class IdleStateTrigger extends ChannelInboundHandlerAdapter {
private final Logger LOGGER=Logger.getLogger(IdleStateTrigger.class);
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if(evt instanceof IdleStateEvent){
IdleStateEvent event= (IdleStateEvent) evt;
switch (event.state()){
case READER_IDLE:
//服务端触发事件,说明客户端已经掉线,关闭失效连接
LOGGER.error("【客户端已断开】"+ctx.channel().remoteAddress()+" "+DateUtils.getCurrentDateTime());
ctx.close();
break;
}
}
super.userEventTriggered(ctx, evt);
}
}
MessageToPoDecoder
public class MessageToPoDecoder extends ReplayingDecoder<Void> {
private final byte BODY_HEAD=(byte) 0xA8;
//协议格式 1字节固定包头 1字节功能码 1字节(判断是否包含topic字段) 4字节固定长度字段 12字节固定topic(非必须) 剩余字节数据
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
//System.out.println("服务器收到");
byte fixHead = in.readByte();//读取固定头部 0xA8
if(!checkHead(fixHead)){
System.out.println("服务器收到头部:"+fixHead);
ctx.channel().close();
throw new IllegalDataHeaderException(fixHead);
}
byte funCode = in.readByte();//读取功能码
byte ishaveTopic = in.readByte(); //读取判断字段
int bodyLength=in.readInt(); //读取固定数据长度字段
byte[] topic=null;
byte[] body=null;
if(ishaveTopic==1){
topic=new byte[12];
in.readBytes(topic);
bodyLength=bodyLength-12; //若有topic主题字段 则读取body体字段长度-12
}
body=new byte[bodyLength];
in.readBytes(body);
//System.out.println("添加MSG功能码:"+FuncodeEnum.getEumInstanceByType(funCode));
out.add(new Message(FuncodeEnum.getEumInstanceByType(funCode), ishaveTopic, topic,bodyLength, body));
}
/**
* 包头校验
* @param b
* @return
*/
private boolean checkHead(byte b){
if(b==BODY_HEAD)
return true;
else
return false;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// TODO Auto-generated method stub
super.exceptionCaught(ctx, cause);
cause.printStackTrace();
}
}
未完待续....................