不懂的都感觉netty是很高深的技术 ,但是如果只是拿来用,不深刻理解底层原理,还是很简单的。
正题开始
核心配置:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.25.Final</version>
</dependency>
设置主从线程和通讯端口以及启动netty
下面用到了 @ComponentScan 这个注解 已经注释了 不懂的自行百度
mport io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import org.springframework.context.annotation.ComponentScan;
//设置自动加载
@ComponentScan
public class WsServer {
private static class SingletionWsServer {
static final WsServer instance = new WsServer();
}
public static WsServer getInstance() {
return SingletionWsServer.instance;
}
private EventLoopGroup mainGroup;
private EventLoopGroup workGroup;
private ServerBootstrap serverBootstrap;
private ChannelFuture channelFuture;
//设置两个线程的主从关系
public WsServer() {
//设置两个线程组 一个处理连接用户 一个处理连接之后的操作
mainGroup = new NioEventLoopGroup();
workGroup = new NioEventLoopGroup();
serverBootstrap = new ServerBootstrap();
//设置两个线程组
serverBootstrap.group(mainGroup, workGroup)//设置主从线程
.channel(NioServerSocketChannel.class)//设置NIO双向通讯
.childHandler(new WsServerInitializer());//子处理器 用于处理workeGroup线程
}
//启动
public void start() {
this.channelFuture = serverBootstrap.bind(8088);
System.err.println("netty websocket server 启动成功");
}
}
编写子处理器
下面把心跳的机制也写到里面了
channelPipeline.addLast(new IdleStateHandler(4,8,12));第一个4是读空闲4秒,第二个是写空闲8秒
第三个是最重要的读写空闲设置了12秒(一般设置60秒,这个是为了方便测试)。设置这个参数是控制关闭无用的channel 。当读写空闲超过12秒。我们自动把这个通道(channel)删除.提供服务器性能和节约资源空间。
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;
public class WsServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
//通过SocketChannel 获取对应的管道
ChannelPipeline channelPipeline= channel.pipeline();
//webSocket 是基于HTTP的 所以要有http解码器
channelPipeline.addLast(new HttpServerCodec());
//对大数据流的支持
channelPipeline.addLast(new ChunkedWriteHandler());
//对HttpMessage进行聚合
//几乎在netty编程中,都会使用到handler
channelPipeline.addLast(new HttpObjectAggregator(1024 *64));
/**
* 增加心跳支持
*针对客户端 读空闲或者写空闲不出来 只对读写空闲请求超过60秒 则主动断开
*/
channelPipeline.addLast(new IdleStateHandler(4,8,12));
//自定义空闲状态监测
channelPipeline.addLast(new HeartBeatHandler());
/**
* webSocket处理的协议,用于指定用户访问的路由:/ws
* 本handler会帮你处理一些繁琐的事
* 会帮你处理握手动作
*/
channelPipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
//添加自定义助手类
channelPipeline.addLast(new ChatHandler());
}
}
下面是自定义助手类的代码
基本的处理逻辑都在里面。
import com.im.immuxin.enums.MsgActionEnum;
import com.im.immuxin.server.UsersServer;
import com.im.immuxin.utils.JsonUtils;
import com.im.immuxin.utils.SpringUtil;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
//用于记录和管理所有的channel
public static ChannelGroup users = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
protected void channelRead0(ChannelHandlerContext cxt, TextWebSocketFrame msg) throws Exception {
//获取Channel
Channel currentChannel =cxt.channel();
//获取客户端传输过来的消息
String conter= msg.text();
//把传输过来的消息转换成我们定义的类型 使用jsonToPojo()方法
DataContert dataContert = JsonUtils.jsonToPojo(conter,DataContert.class);
//获取动作类型
Integer action = dataContert.getAction();
//判断消息类型,根据不同的消息类型来处理不同的业务
if(action== MsgActionEnum.CONNECT.type){
//当websocket 第一次open的时候,初始化channel,把用的channel和userid关联起来
//获取用户ID
String senderId = dataContert.getChatMsg().getSenderId();
//绑定关系
UserChannelRel.put(senderId,currentChannel);
//测试
for(Channel c :users){
System.out.println(c.id().asLongText());
}
UserChannelRel.output();
}else if(action==MsgActionEnum.CHAT.type){
//聊天类型的消息,把聊天记录保存到数据库,同时标记消息的签收状态[未签收]
//获取消息类
ChatMsg chatMsg =dataContert.getChatMsg();
//获取消息
String msgText =chatMsg.getMsg();
//接收者ID
String recaiverId =chatMsg.getRecaiverId();
//发送者ID
String senderId =chatMsg.getSenderId();
DataContert dataContertMsg =new DataContert();
dataContertMsg.setChatMsg(chatMsg);
//把消息保存到数据库 设置签收状态
//通过springUtil获取里面的bean
UsersServer usersServer=(UsersServer) SpringUtil.getBean("usersImpl");
String msgId =usersServer.saveMsg(chatMsg);
chatMsg.setMsgId(msgId);
//获取对应的Channel
Channel receiverChannel=UserChannelRel.get(recaiverId);
if (receiverChannel==null){
//用户离线
}else {
//当receiverChannel(Channer)不为空时 去ChannelGroup(users)里面去找对应的channel是否存在
Channel findChannel =users.find(receiverChannel.id());
if(findChannel !=null ){
//用户在线
receiverChannel.writeAndFlush(
new TextWebSocketFrame(
JsonUtils.objectToJson(dataContertMsg)));
}else {
//用户离线
}
}
}else if(action==MsgActionEnum.SIGNED.type){
//消息签收类型,针对具体的消息进行签收,修改数据库中对应消息的签收状态[已签收]
UsersServer usersServer=(UsersServer) SpringUtil.getBean("usersImpl");
//获取扩展字段 扩展字段在signed类型消息中,代表需要签收的消息的id,逗号隔开
String msgIdsStr = dataContert.getExtand();
String msgIds[] =msgIdsStr.split(",");
List<String> msgIdList = new ArrayList<>();
for (String mid :msgIds){
if (StringUtils.isNotBlank(mid)){
msgIdList.add(mid);
}
}
System.out.println(msgIdList.toString());
//批量处理消息 (以签收)
if (msgIdList !=null && msgIdList.size()>0){
usersServer.updateMsgSigned(msgIdList);
}
}else if(action==MsgActionEnum.KEEPALIVE.type){
//心跳类型的消息
System.out.println("收到来自channel为[" + currentChannel + "]的心跳包...");
}else if(action==MsgActionEnum.PULL_FRIEND.type){
//拉取好友的消息
}
// //发送消息
// for(Channel channel :users){
// channel.writeAndFlush(new TextWebSocketFrame("[服务器接收到消息]:"+new Date()+"消息为:"+conter));
// }
// users.writeAndFlush(new TextWebSocketFrame("[服务器接收到消息]:"+new Date()+"消息为:"+conter));
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// super.handlerAdded(ctx);
users.add(ctx.channel());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
//super.handlerRemoved(ctx);
users.remove(ctx.channel());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//super.exceptionCaught(ctx, cause);
//连接异常打印错误
cause.printStackTrace();
//发生异常之后关闭连接(关闭channel),随后从ChannelGroup 中移除
ctx.channel().close();
users.remove(ctx.channel());
}
}
上面的代码中 我自己定义了一些枚举。便于管理前端传送过来的消息。(注意:前端传送过来的消息体结构必须和我们后端定义的一致。便于我们解析。)
重点:
这个类继承了SimpleChannelInboundHandler这个netty的一个方法。
继承这个方法必须要实现channelRead0()这个方法。
通过 Channel currentChannel =cxt.channel(); 可以获取的第一次连接上的Channel。每次断开连接分配的Channel都是不一样的。netty就是根据每个Channel来发送消息的。那重点就来了。
怎么根据Channel给指定的朋友发送消息呢?
其实很简单。每次连接的时候。会自动分配一个Channel.我们只需要把我们的ID和这个Channel绑定。就可以实现给指定的人发送消息。Map
如下图:
通过一个Map即可绑定每个连接上netty的用户。这样我们只需通过指定的KEY即可找到当前在线的朋友。即可发送消息。
下面给出绑定Channel的代码
import io.netty.channel.Channel;
import java.util.HashMap;
/**
* 用户ID 和Channel的关联关系处理
*/
public class UserChannelRel {
private static HashMap<String , Channel> manager =new HashMap<>();
public static void put(String senderId,Channel channel){
manager.put(senderId,channel);
}
public static Channel get(String senderId){
return manager.get(senderId);
}
public static void output() {
for (HashMap.Entry<String, Channel> entry : manager.entrySet()) {
System.out.println("UserId: " + entry.getKey()
+ ", ChannelId: " + entry.getValue().id().asLongText());
}
}
}
只有理解了这步,基本就可以实现给指定的朋友发送消息了。当然群发也是一样。
最后给出心跳的处理代码
其实这段代码也可以写在我们自定义的助手类里面。但是为了好看。我提取出来了。
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;
public class HeartBeatHandler extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
//super.userEventTriggered(ctx, evt);
//判断 evt是否是 IdleStateEvent(用于触发用户事件 ,包含 读空闲/写空闲/读写空闲)
if (evt instanceof IdleStateEvent){
//对evt进行强转
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("进入读写空闲请求·····");
System.out.println("关闭Channel数量前:"+ChatHandler.users.size());
//读写空闲请求需要关闭无用的Channel
Channel channel= ctx.channel();
//关闭无用的channel 以防资源浪费
channel.close();
System.out.println("关闭Channel数量后:"+ChatHandler.users.size());
}
}
}
}
用到的枚举
代码如下
import java.io.Serializable;
public class DataContert implements Serializable {
private static final long serialVersionUID = -6726890501587734885L;
private Integer action; //动作类型
private ChatMsg chatMsg; //用于用户聊天的entiey
private String extand; //扩展字段
public Integer getAction() {
return action;
}
public void setAction(Integer action) {
this.action = action;
}
public ChatMsg getChatMsg() {
return chatMsg;
}
public void setChatMsg(ChatMsg chatMsg) {
this.chatMsg = chatMsg;
}
public String getExtand() {
return extand;
}
public void setExtand(String extand) {
this.extand = extand;
}
}
ChatMsg:
import java.io.Serializable;
public class ChatMsg implements Serializable {
private static final long serialVersionUID = -4641427694954507726L;
private String senderId; //发送者用户ID
private String recaiverId; //接受者用户ID
private String msg; //聊天内容
private String msgId; //用于消息的签收
public String getSenderId() {
return senderId;
}
public void setSenderId(String senderId) {
this.senderId = senderId;
}
public String getRecaiverId() {
return recaiverId;
}
public void setRecaiverId(String recaiverId) {
this.recaiverId = recaiverId;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public String getMsgId() {
return msgId;
}
public void setMsgId(String msgId) {
this.msgId = msgId;
}
}
下面是我写的解析前端传输过来的数据。转换成我们要的类型。(只适用这代码里面的数据格式)
这个方法代码如下:
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import java.util.Map;
/**
* 自定义转换类
*/
public class JsonUtils {
//定义jackson对象
private static final ObjectMapper MAPPER =new ObjectMapper();
/**
* 将对象转换成JSON字符串
*/
public static String objectToJson(Object data){
try {
String string =MAPPER.writeValueAsString(data);
return string;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
/**
* 将json结果集转化为对象
*/
public static <T> T jsonToPojo(String jsonDate,Class<T> beanType){
try {
T t= MAPPER.readValue(jsonDate,beanType);
return t;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
/**
* 将json数据装换成pojo对象list
*/
public static <T>List<T> jsonToList(String jsonDate,Class<T> beanType){
JavaType javaType=MAPPER.getTypeFactory().constructParametricType(List.class, beanType);
try {
List<T> list =MAPPER.readValue(jsonDate,javaType);
return list;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
以上就是基本代码了。
总结一下:
当我们定义好主从线程。设置好端口号后。最主要的就是那个我们写的自定义助手类(ChatHandler)
基本的处理逻辑都在里面。只有懂得了。每次连接成功后他都会生成一个唯一的Channel。我们需要通过这个来发送消息。只要我们把这个和ID绑定。就可以通过ID来找到这个Channel。找到了要发送的Channel。我们就可以通过
channel.writeAndFlush(
new TextWebSocketFrame(
JsonUtils.objectToJson(dataContertMsg)));//dataContertMsg是要发送的消息。
来直接发送消息。这样就可以达到基本通信。 消息推送也是一样。
前端代码就不写了。