MessagePack是编解码工具,稍后介绍
<dependency>
<groupId>io.nettygroupId>
<artifactId>netty-allartifactId>
dependency>
<dependency>
<groupId>org.msgpackgroupId>
<artifactId>msgpackartifactId>
<version>0.6.12version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
创建两个EventLoopGroup
实例,实际是两个Reactor线程组,一个用于服务端接收客户端的连接,一个用于进行socketChannel的网络读写
ServerBootstrap
对象是netty用于启动NIO服务端的辅助启动类,目的是降低服务端的开发复杂度
group
方法将两个NIO线程组当做入参传递到ServerBootstrap
中
接着设置创建的Channel为NioServerSocketChannel
然后配置TCP参数,此处将他的backlog设置为128
然后绑定I/O事件的处理类ServerChannelInitializer
,这个稍后看实现,主要用于处理网络I/O事件,例如对消息进行编解码、记录日志、处理业务等
可以通过childOption
针对客户端进行一些配置,例如检测心跳状态、设置是否一次发送等
private final EventLoopGroup bossGroup = new NioEventLoopGroup();
private final EventLoopGroup workerGroup = new NioEventLoopGroup();
private Channel channel;
@Autowired
private ChannelCache channelCachel;
public ChannelFuture run(InetSocketAddress address) {
ChannelFuture f = null;
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 128)
.childHandler(new ServerChannelInitializer()).childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true);
f = b.bind(address).sync();
channel = f.channel();
} catch (Exception e) {
log.error("Netty start error:", e);
} finally {
if (f != null && f.isSuccess()) {
log.info("Netty server listening " + address.getHostName() + " on port " + address.getPort()
+ " and ready for connections...");
} else {
log.error("Netty server start up Error!");
}
}
return f;
}
public void destroy() {
log.info("Shutdown Netty Server...");
channelCachel.flushDb();
if (channel != null) {
channel.close();
}
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
log.info("Shutdown Netty Server Success!");
}
其中channelCachel
是自定义的一个保存通道信息的工具类,稍后介绍
主要包括对消息的编解码、设置心跳超时以及设置业务处理类
本例中,服务端只检测读空闲时间
编解码器和业务处理类稍后展示
public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 解码编码
// socketChannel.pipeline().addLast(new
// LengthFieldBasedFrameDecoder(1024, 0, 2, 0, 2));
socketChannel.pipeline().addLast(new MsgDecoder());
// socketChannel.pipeline().addLast(new LengthFieldPrepender(2));
socketChannel.pipeline().addLast(new MsgEncoder());
socketChannel.pipeline().addLast(new IdleStateHandler(Const.READER_IDLE_TIME_SECONDS, 0, 0));
socketChannel.pipeline().addLast(new ServerHandler());
}
}
特别要注意,编解码器的顺序不要写错,不然会造成无法解码的情况,导致业务处理类无法处理,之前就因为这个问题折腾了挺久,这个以后单独测试
自己实现的一个管理channel信息的工具类,同时将channel的信息存入redis中
因为channel.id()
得到的channel标识无法被序列化,因此无法存入redis中,这也是实际出现过的问题,以后可以单独测试,因此采用channelId.asLongText()
当做redis的value,类型是String,这是channelId的一个全局唯一标识
目前由于担心出现一个设备通过多个channel连接过来的情况,所以在redis里,采用set来保存用户和channel的关系,key是用户的id,value可以是多个不同的channel标识
@Configuration
public class ChannelCache {
@Autowired
private MyRedisService myRedisService;
// 存储所有Channel
private ChannelGroup channelGroup = new DefaultChannelGroup("channelGroups", GlobalEventExecutor.INSTANCE);
// 存储Channel.id().asLongText()和用户id对应关系
private ConcurrentHashMap<String, Integer> channelIdUid = new ConcurrentHashMap<String, Integer>();
public ChannelGroup getChannelGroup() {
return channelGroup;
}
public ConcurrentHashMap<String, Integer> getChannelIdUid() {
return channelIdUid;
}
/**
* 退出时删除redis数据库中缓存
*
*/
public void flushDb() {
myRedisService.flushDb();
}
/**
* 获取Channel
* @return
*/
public Channel getChannel(Channel channel) {
Channel channel_ = channelGroup.find(channel.id());
if (channel_ != null) {
return channel_;
}
return null;
}
/**
* 添加Channel到ChannelGroup
* @param uid
* @param channel
*/
public void addChannel(Channel channel, int uid) {
Channel channel_ = channelGroup.find(channel.id());
if (channel_ == null) {
channelGroup.add(channel);
}
// redis添加对应用户和channelId之前的关系
Integer userId = channelIdUid.get(channel.id().asLongText());
if (userId != null && userId.intValue() != uid) {
// 和本次用户数据对不上,直接删除对应channel的老数据
redisDelete(userId, channel);
}
channelIdUid.put(channel.id().asLongText(), userId);
// redis添加对应channelId
redisAdd(uid, channel);
}
/**
* 删除Channel
* @param channel
*/
public void removeChannel(Channel channel) {
Channel channel_ = channelGroup.find(channel.id());
if (channel_ != null) {
channelGroup.remove(channel_);
}
Integer userId = channelIdUid.get(channel.id().asLongText());
if (userId != null) {
channelIdUid.remove(channel.id().asLongText());
redisDelete(userId, channel);
}
}
private void redisDelete(int uid, Channel channel) {
redisDelete(uid, channel.id());
}
private void redisDelete(int uid, ChannelId channelId) {
myRedisService.setRemove(myRedisService.getUserKeyPrefix() + uid, channelId.asLongText());
}
private void redisAdd(int uid, Channel channel) {
redisAdd(uid, channel.id());
}
private void redisAdd(int uid, ChannelId channelId) {
myRedisService.sSetAndTime(myRedisService.getUserKeyPrefix() + uid, myRedisService.getExpireSeconds(),
channelId.asLongText());
}
}
客户端连接服务端时,需要通过一个标识来验证连接用的账号是否在系统内,以APP连接netty为例
一般APP登录,都会生成一个token,以便每次访问后台接口进行验证,也方便有其他设备登录账号后,主动使上一个登录设备的token失效。一般情况下,token都会保存在redis当中。本例采用的例子是,APP连接netty服务时,会带上token参数,此时需要将此token与保存在redis数据库中的数据进行比对。
由于保存用户和通道之间也需要一个redis数据库,所以需要配置两个RedisTemplate
对象
@Configuration
public class RedisConfig {
/**
* 配置自定义redisTemplate. 方法名一定要叫redisTemplate 因为@Bean注解是根据方法名配置这个bean的name
*
* @return
*/
@Bean
public RedisTemplate<String, Object> myRedisTemplate(
@Value("${redis.my.host}") String host,
@Value("${redis.my.port}") int port,
@Value("${redis.my.password}") String password,
@Value("${redis.my.database}") int database) {
RedisStandaloneConfiguration config=new RedisStandaloneConfiguration();
config.setHostName(host);
config.setDatabase(database);
config.setPassword(RedisPassword.of(password));
config.setPort(port);
LettuceConnectionFactory factory=new LettuceConnectionFactory(config);
factory.afterPropertiesSet();
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setValueSerializer(new StringRedisSerializer());
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
@Bean
public RedisTemplate<String,Object> loginRedisTemplate(
@Value("${redis.login.host}") String host,
@Value("${redis.login.port}") int port,
@Value("${redis.login.password}") String password,
@Value("${redis.login.database}") int database){
RedisStandaloneConfiguration config=new RedisStandaloneConfiguration();
config.setHostName(host);
config.setDatabase(database);
config.setPassword(RedisPassword.of(password));
config.setPort(port);
LettuceConnectionFactory factory=new LettuceConnectionFactory(config);
factory.afterPropertiesSet();
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setValueSerializer(new StringRedisSerializer());
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
底层的传输与交互都是采用二进制的方式。
如何判断发送的消息已经结束,就需要通过协议来规定,比如收到换行符等标识时,判断为结束等。
根据协议,把二进制数据转换成Java对象称为解码(也叫做拆包),把Java对象转换为二进制数据称为编码(也叫做打包)。
常用的协议制定方法:
定长消息法:这种方式是使用长度固定的数据发送,一般适用于指令发送。譬如:数据发送端规定发送的数据都是双字节,AA 表示启动、BB 表示关闭等等
字符定界法:这种方式是使用特殊字符作为数据的结束符,一般适用于简单数据的发送。譬如:在消息的结尾自动加上文本换行符(Windows使用\r\n,Linux使用\n),接收方见到文本换行符就认为是一个完整的消息,结束接收数据开始解析。注意:这个标识结束的特殊字符一定要简单,常常使用ASCII码中的特殊字符来标识(会出现粘包、半包情况)。
定长报文头法:使用定长报文头,在报文头的某个域指明报文长度。该方法最灵活,使用最广。譬如:协议为– 协议编号(1字节)+数据长度(4个字节)+真实数据。请求到达后,解析协议编号和数据长度,根据数据长度来判断后面的真实数据是否接收完整。HTTP 协议的消息报头中的Content-Length 也是表示消息正文的长度,这样数据的接收端就知道到底读到多长的字节数就不用再读取数据了。
实际应用中,采用最多的还是定长报文头法。
本例采用的是定长报文头法,协议组成: 数据长度(4个字节) + 数据。
MessagePack是一个高效的二进制序列化框架。
特点:
支持多种语言,java为例,使用很简单,如果是自定义的类,需要加上@Messgae注解
序列化只需两行:
MessagePack messagePack = new MessagePack();
byte[] write = messagePack.write(msg);
反序列化:
// 自定义的类
Message message = new Message();
MessagePack msgpack = new MessagePack();
message = msgpack.read(b, Message.class);
服务端接收类
@org.msgpack.annotation.Message
public class Message {
// 用户id
private int uid;
// 模块id: 0-心跳包
private int module;
// json格式数据
private String data;
public Message() {
super();
}
public Message(int uid, int module, String data) {
this.uid = uid;
this.module = module;
this.data = data;
}
public int getUid() {
return uid;
}
public void setUid(int uid) {
this.uid = uid;
}
public int getModule() {
return module;
}
public void setModule(int module) {
this.module = module;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
@Override
public String toString() {
return "uid:" + uid + " module:" + module + " data:" + data;
}
}
客户端接收类
@Message
public class Result {
private int resultCode;
private String resultMsg;
private String data;
public Result() {
this(1, "success");
}
public Result(int resultCode, String resultMsg) {
this(resultCode, resultMsg, null);
}
public Result(int resultCode, String resultMsg, String data) {
this.resultCode = resultCode;
this.resultMsg = resultMsg;
this.data = data;
}
public int getResultCode() {
return resultCode;
}
public void setResultCode(int resultCode) {
this.resultCode = resultCode;
}
public String getResultMsg() {
return resultMsg;
}
public void setResultMsg(String resultMsg) {
this.resultMsg = resultMsg;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
@Override
public String toString() {
return "code:" + resultCode + " msg:" + resultMsg + " data:" + data;
}
}
public class MsgEncoder extends MessageToByteEncoder<Result> {
@Override
protected void encode(ChannelHandlerContext ctx, Result msg, ByteBuf out) throws Exception {
MessagePack messagePack = new MessagePack();
byte[] write = messagePack.write(msg);
out.writeInt(write.length);
out.writeBytes(write);
}
}
类似mina中的CumulativeProtocolDecoder
类,ByteToMessageDecoder
同样可以将未处理的ByteBuf
保存起来,下次一起处理,具体的原理以后再单独研究。
public class MsgDecoder extends ByteToMessageDecoder {
private static final Logger log = LoggerFactory.getLogger(MsgDecoder.class);
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// log.info("thread name: " + Thread.currentThread().getName());
long start = System.currentTimeMillis();
if (in.readableBytes() < 4) {
return;
}
in.markReaderIndex();
int length = in.readInt();
if (length <= 0) {
log.info("length: " + length);
ctx.close();
return;
}
if (in.readableBytes() < length) {
log.info("return");
in.resetReaderIndex();
return;
}
byte[] b = new byte[length];
in.readBytes(b);
Message message = new Message();
MessagePack msgpack = new MessagePack();
try {
message = msgpack.read(b, Message.class);
out.add(message);
} catch (Exception e) {
log.error("MessagePack read error");
ctx.close();
}
log.info(" ====== decode succeed: " + message.toString());
long time = System.currentTimeMillis() - start;
log.info("decode time: " + time + " ms");
}
}