前言
前几天,随便画了草图来对netty进行一个简单的封装,从而完成我们消息系统的实现。我们的基本的流程图是这样的: 那么今天也是在写了几个接口之后,开始我们这个系统的搭建。 这块的话,将先搭建出一个架子出来,把架子和我们的业务代码进行分离。也就是说搞一个脚手架,方便后面别人,也包括 我自己进行新的业务增强。这样的话就可以实现比较高效的代码复用了。当然值得一提的是,我们这个系统是分前后端的,也就是说还有个客户端要搞,这个当前只是搞了服务端。给了一个服务端的架子。具体怎么把这个架子用起来,要么就等我慢慢写到那个业务,要么就自己慢慢看喽。当然只是简单封装,代码很简单。
核心
其实我们整个封装的核心,其实就两个部分。
交换机
这个交换机,其实就是我们的消息的发送者。我们这里其实主要有两个模式:
- 一对一单点模式(好友聊天)
- 群播模式(群聊)
之所以要使用这个交换机只不过是为了方便做管理罢了。因为在我的社区当中有个类似于聊天室的功能,而且这个聊天室有很多个。因此需要进行简单管理。再或者说有时候我们有多个不同类别的设备需要进行管理互通之类的,例如电脑给手机发送消息。但是不管怎么说,最基本的其实就是这两个,也就是:组内广播,一对一 点播(组内,组外)
此外,交换机的匹配规则是可以自定义的,有时候,可能需要匹配到以A开头的组别,等等,这个后面都好说。
说到这里的话,你应该很好奇,这个玩意该怎么实现。其实很简单,首先我们对所有的用户的连接给管理起来,把对应的userid-group和channel放到一个map当中管理起来。 然后,我们再建立一个分组索引即可。这个分组索引当然也是一个map来进行维护的。这里主要有两个:
java
/**
* 将连接的channel进行分组处理,方便后面实现广播等等机制
* */
public class GroupsPool {
/**
* Groups:{
* group:[userid-group,userid-group...]
* }
*
* RevGroups:{
* userid-group:[group,group...]
* }
* */
private static volatile ConcurrentHashMap<String, LinkedList<String>> Groups = null;
private static volatile ConcurrentHashMap<String, LinkedList<String>> RevGroups= null;
static {
Groups = new ConcurrentHashMap<>();
RevGroups = new ConcurrentHashMap<>();
}
public static ConcurrentHashMap<String, LinkedList<String>> getGroups() {
return Groups;
}
public static void setGroups(ConcurrentHashMap<String, LinkedList<String>> groups) {
Groups = groups;
}
public static ConcurrentHashMap<String, LinkedList<String>> getRevGroups() {
return RevGroups;
}
public static void setRevGroups(ConcurrentHashMap<String, LinkedList<String>> revGroups) {
RevGroups = revGroups;
}
}
这样的话,就非常的直接了。 那么我们的交换机说白了就是: 换着花样拿到这里的group,然后找到userid-group,因为这个userid-group 在我们的channekMap当中有对应的channel,我们通过这个channel来实现通讯
处理器
我们这里面定义的处理器,其实和netty里面定义的处理器是一样的,其目的都是为了方便实现对消息的处理。刚刚的交换机实现了,这个消息要发到那里去。那么现在的处理器决定了,这个消息要怎么处理才能发送。大白话就是交换机决定了你的消息能到谁的手上,处理器决定了你的消息长啥样
只是这里我们与netty不同的是,这个处理器是和我们的业务相关的。例如你的消息过来要入库,这个是你的业务相关。那么这块在专门抽离处理的目的一是为了方便拓展,二是为了方便和业务进行抽离。还是那句话,这个只是个架子,架子就不要和业务耦合太紧密。
那么这里有什么重要点嘛?其实就两个:
- 什么样的消息走什么样的处理器,这里的话我处理地很粗糙,直接根据不同的消息类型处理的。如果有什么额外的需求的话,只能自己在消息里面加入一个什么标志位,然后自己写一个handler处理了(这个提供实现接口,实现这个类,然后注入进handlers当中即可)
- 对同一个消息处理时,不同handler之间对消息的转递。
那么实现了这两个点,我们handler就基本上可以愉快玩耍了。
基本服务搭建
由于时间有限,这个是我花了三个小时构建的(其他的基本的CURD业务没啥好说的)所以的话,我这里目前就搭建了基本的架子,然后往里面填代码就好了。
当然在这里我们这里先讨论的是关于这个Netty本身,先把这个主体玩意搭建起来,后面的增强就很好处理了。
基本配置
其实使用netty就这几步:
- 配置基本参数,例如线程池大小,地址端口等等
- 编写handler,定制相关协议(对消息的接收处理)
- 编写启动类,启动端口监听
我们先来看到配置:
java
/**
* 这个主要是我们服务端的相关配置
* */
public class NettyProperties {
/**
* boss线程数量 默认为cpu线程数*2
*/
public static Integer boss=2;
/**
* worker线程数量 默认为cpu线程数*2
*/
public static Integer worker=2;
/**
* 连接超时时间 默认为30s
*/
public static Integer timeout = 30000;
/**
* 服务器主端口 默认9000
*/
public static Integer port = 9000;
public static String host = "127.0.0.1";
}
java
public class NettyConfig {
/**
* boss 线程池
* 负责客户端连接
* @return NioEventLoopGroup
*/
public NioEventLoopGroup boosGroup(){
return new NioEventLoopGroup(NettyProperties.boss);
}
/**
* worker线程池
* 负责业务处理
* @return NioEventLoopGroup
*/
public NioEventLoopGroup workerGroup(){
return new NioEventLoopGroup(NettyProperties.worker);
}
/**
* 服务器启动器
* @return ServerBootstrap
*/
public ServerBootstrap serverBootstrap(){
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(boosGroup(),workerGroup()) // 指定使用的线程组
.channel(NioServerSocketChannel.class) // 指定使用的通道
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,NettyProperties.timeout) // 指定连接超时时间
.childHandler(new ServerHandler()); // 指定worker处理器
return serverBootstrap;
}
}
相关处理的Handler
这里我们使用的是WS协议,所以这里要处理一下:
java
package com.huterox.messaging.core.supports.handler;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
/**
* 用于检测channel 的心跳handler
* 继承ChannelInboundHandlerAdapter,目的是不需要实现ChannelRead0 这个方法
*/
public class HeartBeatHandler extends ChannelInboundHandlerAdapter {
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if(evt instanceof IdleStateEvent){
IdleStateEvent event = (IdleStateEvent)evt;//强制类型转化
if(event.state()== IdleState.READER_IDLE){
// System.out.println("进入读空闲......");
}else if(event.state() == IdleState.WRITER_IDLE) {
// System.out.println("进入写空闲......");
}else if(event.state()== IdleState.ALL_IDLE){
// System.out.println("channel 关闭之前:users 的数量为:"+ UserConnectPool.getChannelGroup().size());
Channel channel = ctx.channel();
//资源释放
channel.close();
// System.out.println("channel 关闭之后:users 的数量为:"+UserConnectPool.getChannelGroup().size());
}
}
}
}
因为netty的话是比较底层的,直接基于socket实现的,在传输层,所以这里要指定响应的基本的应用层协议。
java
package com.huterox.messaging.core.supports.handler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;
/**
* 定义worker端的处理器
*/
public class ServerHandler extends ChannelInitializer<SocketChannel> {
/**
* 初始化通道以及配置对应管道的处理器
* @param channel
* @throws Exception
*/
@Override
protected void initChannel(SocketChannel channel) throws Exception{
//获取管道(pipeline)
ChannelPipeline pipeline = channel.pipeline();
//websocket 基于http协议,所需要的http 编解码器
pipeline.addLast(new HttpServerCodec());
//在http上有一些数据流产生,有大有小,我们对其进行处理,既然如此,
// 我们需要使用netty 对下数据流写 提供支持,这个类叫:ChunkedWriteHandler
pipeline.addLast(new ChunkedWriteHandler());
//对httpMessage 进行聚合处理,聚合成request或 response
pipeline.addLast(new HttpObjectAggregator(1024*64));
//===========================增加心跳支持==============================
/**
* 针对客户端,如果在1分钟时间内没有向服务端发送读写心跳(ALL),则主动断开连接
* 如果有读空闲和写空闲,则不做任何处理
*/
pipeline.addLast(new IdleStateHandler(8,10,12));
//自定义的空闲状态检测的handler
pipeline.addLast(new HeartBeatHandler());
/**
* 本handler 会帮你处理一些繁重复杂的事情
* 会帮你处理握手动作:handshaking(close、ping、pong) ping+pong = 心跳
* 对于websocket 来讲,都是以frams 进行传输的,不同的数据类型对应的frams 也不同
*/
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
//自定义的handler,这里面完成对我们整个netty信息的处理,增强
pipeline.addLast(new ServerListenerHandler());
}
}
之后就是我们这边最重要的一个块。我们的ServerListenerHandler() 它的作用不亚于MVC当中的dispatchHandler。
java
package com.huterox.messaging.core.supports.handler;
import com.huterox.messaging.ServerBoot;
import com.huterox.messaging.core.enums.MessageActionEnum;
import com.huterox.messaging.core.entities.DataContent;
import com.huterox.messaging.core.supports.pools.UserConnectPool;
import com.huterox.messaging.utils.JsonUtils;
import com.huterox.messaging.utils.R;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.AttributeKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Objects;
/**
* 这是我们最核心的一个处理,它的地位等于MVC当中的dispatch
* 负责我们整个消息服务器和用户端的一个数据的监听
* */
@ChannelHandler.Sharable
public class ServerListenerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private static final Logger log = LoggerFactory.getLogger(ServerBoot.class);
static {
//先初始化出来最大的容器
UserConnectPool.getChannelMap();
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
String content = msg.text();
/*获取客户端传过来的消息*/
DataContent dataContent = JsonUtils.jsonToPojo(content, DataContent.class);
assert dataContent != null;
Integer action = dataContent.getAction();
Channel channel = ctx.channel();
/*
* 根据消息类型对其进行处理,我们这里只做两个事情
* 1. 注册用户
* 2. 心跳在线
* 之后我们将维护所有的用户连接
* */
if(Objects.equals(action, MessageActionEnum.CONNECT.type)){
/*
* 2.1 当websocket 第一次 open 的时候,
* 初始化channel,把用的 channel 和 userid 关联起来
* */
String userid = dataContent.getUserid();
String group = dataContent.getGroup();
userid = userid+"-"+group;
//将userid加入对应的channel分组当中
AttributeKey<String> key = AttributeKey.valueOf("userId");
ctx.channel().attr(key).setIfAbsent(userid);
UserConnectPool.getChannelMap().put(userid,channel);
UserConnectPool.output();
} else if(Objects.equals(action, MessageActionEnum.KEEPALIVE.type)){
/*
* 心跳包的处理
* */
channel.writeAndFlush(
new TextWebSocketFrame(
JsonUtils.objectToJson(R.ok("返回心跳包").
put("type", MessageActionEnum.KEEPALIVE.type))
)
);
}
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
//接收到请求
log.info("有新的客户端链接:[{}]", ctx.channel().id().asLongText());
AttributeKey<String> key = AttributeKey.valueOf("userId");
ctx.channel().attr(key).setIfAbsent("temp");
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
String chanelId = ctx.channel().id().asShortText();
log.info("客户端被移除:channel id 为:"+chanelId);
removeUserId(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
//发生了异常后关闭连接,同时从channelgroup移除
ctx.channel().close();
removeUserId(ctx);
}
/**
* 删除用户与channel的对应关系
*/
private void removeUserId(ChannelHandlerContext ctx) {
AttributeKey<String> key = AttributeKey.valueOf("userId");
String userId = ctx.channel().attr(key).get();
UserConnectPool.getChannelMap().remove(userId);
}
}
在这块我们完成了所有的客户端的连接,这样我们就可以很方便地做各种增强了。
存储Channel
这里的话提到对于Channel的存储是个Map。实际上是在这里:
java
package com.huterox.messaging.core.supports.pools;
import io.netty.channel.Channel;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class UserConnectPool {
/**
* 存放请求ID与channel的对应关系
*/
private static volatile ConcurrentHashMap<String, Channel> channelMap = null;
private static final Object lock2 = new Object();
public static void output(){
for (Map.Entry<String,Channel> entry :channelMap.entrySet()) {
System.out.println("UserId:"+entry.getKey()
+",ChannelId:"+entry.getValue().id().asLongText()
);
}
}
public static ConcurrentHashMap<String, Channel> getChannelMap() {
if (null == channelMap) {
synchronized (lock2) {
if (null == channelMap) {
channelMap = new ConcurrentHashMap<>();
}
}
}
return channelMap;
}
public static Channel getChannelFromMap(String userId) {
if (null == channelMap) {
return getChannelMap().get(userId);
}
return channelMap.get(userId);
}
}
之后我们很多的操作就可以基于这个来实现,来happy了。
消息的转换
同时在这里你应该注意到了,我们在这里还需要对消息进行一个转化,这里首先是定义了一个基本的消息类:
java
package com.huterox.messaging.core.entities;
/**
* netty客户端返回数据格式
* */
public class DataContent {
//用户id
private String userid;
//用户组别
private String group;
//此次用户请求的行为
private Integer action;
//携带的消息(这里是一个object对象,做强制类型转换即可)
private Object message;
public DataContent(String userid, String group, Integer action) {
this.userid = userid;
this.group = group;
this.action = action;
}
public String getUserid() {
return userid;
}
public void setUserid(String userid) {
this.userid = userid;
}
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
public Integer getAction() {
return action;
}
public void setAction(Integer action) {
this.action = action;
}
@Override
public String toString() {
return "DataContent{" +
"userid='" + userid + '\'' +
", group='" + group + '\'' +
", action=" + action +
'}';
}
}
然后的话,我们有个JsonUtils可以帮助我们把这些东西转化未我们的Java对象。
java
package com.huterox.messaging.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
/**
* @Description: 自定义响应结构, 转换类
*/
public class JsonUtils {
// 定义jackson对象
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 将对象转换成json字符串。
* <p>Title: pojoToJson</p>
* <p>Description: </p>
* @param data
* @return
*/
public static String objectToJson(Object data) {
try {
String string = MAPPER.writeValueAsString(data);
return string;
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
/**
* 将json结果集转化为对象
*
* @param jsonData json数据
* @param beanType 对象类型
* @return
*/
public static <T> T jsonToPojo(String jsonData, Class<T> beanType) {
try {
T t = MAPPER.readValue(jsonData, beanType);
return t;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 将json数据转换成pojo对象list
* <p>Title: jsonToList</p>
* <p>Description: </p>
* @param jsonData
* @param beanType
* @return
*/
public static <T> List<T> jsonToList(String jsonData, Class<T> beanType) {
JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType);
try {
List<T> list = MAPPER.readValue(jsonData, javaType);
return list;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
启动类
当然别忘了我们还有个启动类。在接下来使用的时候,我们要整合进入Spring Boot。
java
package com.huterox.messaging;
import com.huterox.messaging.config.NettyProperties;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.nio.NioEventLoopGroup;
import javax.annotation.Resource;
/**
* 服务器启动类
*/
public class ServerBoot {
@Resource
ServerBootstrap serverBootstrap;
@Resource
NioEventLoopGroup boosGroup;
@Resource
NioEventLoopGroup workerGroup;
/**
* 开机启动
*/
public void start() throws InterruptedException {
// 绑定端口启动
serverBootstrap.bind(NettyProperties.port).sync();
}
/**
* 关闭线程池
*/
public void close() throws InterruptedException {
boosGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
后面我们在SpringBoot当中启动的时候,指定启动start()即可。这里面怎么具体操作的话,需要在后面具体使用的时候再进行说明了,届时会再给出前端的连接代码。
总结
以上就是全部内容,over~