參考:https://www.cnblogs.com/mc-74120/p/13622008.html
pom檔案
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
啟動類
@EnableFeignClients
@EnableDiscoveryClient
@EnableScheduling
@SpringBootApplication
@EnableAsync
public class ChimetaCoreApplication implements CommandLineRunner{
@Autowired
private NettyServerListener nettyServerListener;
public static void main(String[] args) {
SpringApplication.run(ChimetaCoreApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
nettyServerListener.start();
}
}
服務端程式碼的listener
package com.chimeta.netty;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.chimeta.netty.protobuf.ImProto;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.extern.slf4j.Slf4j;
/**
* 服務啟動監聽器
*
* @author mwan
*/
@Component
@Slf4j
public class NettyServerListener {
/**
* 建立bootstrap
*/
ServerBootstrap serverBootstrap = new ServerBootstrap();
/**
* BOSS
*/
EventLoopGroup boss = new NioEventLoopGroup();
/**
* Worker
*/
EventLoopGroup work = new NioEventLoopGroup();
/**
* 通道介面卡
*/
@Resource
private ServerChannelHandlerAdapter channelHandlerAdapter;
/**
* 從配置中心獲取NETTY伺服器配置
*/
@Value("${server.netty.port:10001}")
private int NETTY_PORT;
@Value("${server.netty.maxthreads:5000}")
private int MAX_THREADS;
/**
* 關閉伺服器方法
*/
@PreDestroy
public void close() {
log.info("關閉伺服器....");
//優雅退出
boss.shutdownGracefully();
work.shutdownGracefully();
}
/**
* 開啟及服務執行緒
*/
public void start() {
serverBootstrap.group(boss, work)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, MAX_THREADS) //最大客戶端連線數為1024
.handler(new LoggingHandler(LogLevel.INFO)).childOption(ChannelOption.SO_KEEPALIVE, true); ;
try {
//設定事件處理
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 下面的每一個addLast都有自己的含義,需要每個都過一下
ch.pipeline().addLast(new IdleStateHandler(18,0,0));
ch.pipeline().addLast(new ProtobufVarint32FrameDecoder());
//ch.pipeline().addLast(new CustomProtobufInt32FrameDecoder());
ch.pipeline().addLast(new ProtobufDecoder(ImProto.ImMsg.getDefaultInstance()));
ch.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender());
//ch.pipeline().addLast(new CustomProtobufInt32LengthFieldPrepender());
ch.pipeline().addLast(new ProtobufEncoder());
// 業務處理
ch.pipeline().addLast(channelHandlerAdapter);
}
});
log.info("netty伺服器在[{}]埠啟動監聽", NETTY_PORT);
ChannelFuture f = serverBootstrap.bind(NETTY_PORT).sync();
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.error("[出現異常] 釋放資源", e);
boss.shutdownGracefully();
work.shutdownGracefully();
log.info("服務已關閉!");
}
}
}
ServerChannelHandlerAdapter處理類
package com.chimeta.netty;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.chimeta.netty.model.SessionCloseReason;
import com.chimeta.netty.protobuf.ImProto.ImMsg;
import com.chimeta.netty.util.ChannelUtils;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
/**
* 通訊服務處理器
*/
@Component
@Sharable
@Slf4j
public class ServerChannelHandlerAdapter extends ChannelInboundHandlerAdapter {
/**
* 注入請求分排器
*/
@Autowired
private MessageDispatcher messageDispatcher;
@Autowired
private DeviceSessionManager sessionManager;
/** 用來記錄當前線上連線數。應該把它設計成執行緒安全的。 */
//private AtomicInteger sessionCount = new AtomicInteger(0);
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
super.handlerAdded(ctx);
if (!ChannelUtils.addChannelSession(ctx.channel(), new IoSession(ctx.channel()))) {
ctx.channel().close();
log.error("Duplicate session,IP=[{}]",ChannelUtils.getRemoteIp(ctx.channel()));
}
//String server_ip = NetworkUtils.getRealIp();//獲得本機IP
// 快取計數器加1
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
super.handlerRemoved(ctx);
// 快取計數器減1
//String server_ip = NetworkUtils.getRealIp();//獲得本機IP
log.info(ctx.channel().id()+"離開了");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ImMsg gameMessage = (ImMsg)msg;
final Channel channel = ctx.channel();
IoSession session = ChannelUtils.getSessionBy(channel);
if(session.isHeartbeated()) {
session.setHeartbeated(false);
}
String deviceCode="";
if(session.getDevice() != null && StringUtils.isNotBlank(session.getDevice().getDeviceCode())) {
deviceCode = session.getDevice().getDeviceCode();
}
// if(!MessagingConst.TYPE_UPOS_REQUEST.equals(gameMessage.getMsg().getTypeUrl())) {
try {
log.info("Inbound message is :" + JsonFormat.printer().usingTypeRegistry(DeviceSessionManager.typeRegistry).print(gameMessage.toBuilder())
+ ", from device " + deviceCode);
} catch (InvalidProtocolBufferException e) {
log.info("Inbound message is :" + gameMessage.toString());
}
// }
messageDispatcher.dispatch(gameMessage, session);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
log.error("通訊發生異常:", cause);
ctx.close();
}
/**
* 一段時間未進行讀寫操作 回撥
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
/*心跳處理*/
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.READER_IDLE) {
/*讀超時*/
log.info("READER_IDLE read overtime,close session");
final Channel channel = ctx.channel();
IoSession session = ChannelUtils.getSessionBy(channel);
/*
* if(messageDispatcher.sendHeartbeat(session) == false) { //如果心跳檢測失敗,則連線異常,主動斷開
* session.setSessionCloseReason(SessionCloseReason.OVER_TIME); ctx.close(); };
*/
session.setSessionCloseReason(SessionCloseReason.OVER_TIME);
ctx.close();
} else if (event.state() == IdleState.WRITER_IDLE) {
/*寫超時*/
log.info("WRITER_IDLE 寫超時");
} else if (event.state() == IdleState.ALL_IDLE) {
/*總超時*/
log.info("ALL_IDLE 總超時");
}
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
sessionManager.unregisterUserContext(ctx.channel());
log.info(ctx.channel().id() + "已掉線!");
// 這裡加入玩家的掉線處理
ctx.close();
}
}
MessageDispatcher分派各個處理器
package com.chimeta.netty;
import com.chimeta.netty.service.TerminalService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import com.chimeta.netty.constant.MessagingConst;
import com.chimeta.netty.model.SessionCloseReason;
import com.chimeta.netty.protobuf.ImProto.ImMsg;
import com.chimeta.netty.service.LoginService;
import com.chimeta.netty.util.MessageBuilder;
import com.google.protobuf.InvalidProtocolBufferException;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Resource;
/**
* 請求分排器
*/
@Component
@Slf4j
public class MessageDispatcher{
@Autowired
private LoginService loginService;
@Resource
private TerminalService terminalService;
/**
* 訊息分發處理
*
* @param gameMsg
* @throws InvalidProtocolBufferException
*/
@Async
public void dispatch(ImMsg imMsg, IoSession currSession) throws InvalidProtocolBufferException {
if(imMsg.getId() < 0) {
currSession.sendMessage(MessageBuilder.buildErrorResponse(imMsg, MessagingConst.RESPONSE_ERR_CODE_400, "Invalid message!"));
return;
}
//log.info("接收到的訊息TypeUrl是: "+imMsg.getMsg().getTypeUrl());
switch(imMsg.getMsg().getTypeUrl()) {
case MessagingConst.TYPE_ONLINE_REQUEST:
// 處理裝置上線請求
loginService.doLogin(imMsg, currSession);
break;
case MessagingConst.TYPE_USER_LOGON_REQUEST:
// 處理請求
loginService.doUserLogon(imMsg, currSession);
break;
case MessagingConst.TYPE_USER_LOGOFF_REQUEST:
// 處理請求
loginService.doUserLogoff(imMsg, currSession);
break;
case MessagingConst.TYPE_TERMINAL_STATE_REQUEST:
// 我寫的
terminalService.multiInsert(imMsg, currSession);
break;
default:
if(currSession != null) {
// 返回客戶端發來的心跳訊息
responseHeartbeat(imMsg, currSession);
}
break;
}
}
/**
* 傳送心跳包訊息
* @param gameMsg
* @param currSession
* @return
*/
public boolean sendHeartbeat(IoSession currSession) {
try {
if(currSession.isHeartbeated()) {
return false;
}
ImMsg.Builder imMsgBuilder = ImMsg.newBuilder();
currSession.sendMessage(imMsgBuilder.build());
currSession.setHeartbeated(true);
return true;
}catch(Exception e) {
log.error("主動傳送心跳包時發生異常:", e);
currSession.close(SessionCloseReason.EXCEPTION);
return false;
}
}
/**
* 返回客戶端發來的心跳包訊息
* @param imMsg
* @param currSession
*/
private void responseHeartbeat(ImMsg imMsg,IoSession currSession) {
ImMsg.Builder imMsgBuilder = ImMsg.newBuilder();
currSession.sendMessage(imMsgBuilder.build());
}
}
最後到service業務處理TerminalService
package com.chimeta.netty.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.chimeta.common.entity.terminal.TerminalStateMonitorDO;
import com.chimeta.netty.IoSession;
import com.chimeta.netty.constant.MessagingConst;
import com.chimeta.netty.model.DeviceInfo;
import com.chimeta.netty.protobuf.ImProto;
import com.chimeta.netty.util.MessageBuilder;
import com.chimeta.terminal.mapper.TerminalStateMonitorMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* 盒子裝置相關的實現類
*/
@Service
@Slf4j
public class TerminalService extends ServiceImpl<TerminalStateMonitorMapper, TerminalStateMonitorDO> {
@Transactional(rollbackFor = Exception.class)
public void multiInsert(ImProto.ImMsg imMsg, IoSession currSession){
DeviceInfo deviceInfo = currSession.getDevice();
if(deviceInfo == null) {
currSession.sendMessage(MessageBuilder.buildErrorResponse(imMsg, MessagingConst.RESPONSE_ERR_CODE_400, "device not online!"));
return;
}
try {
ImProto.TerminalStateList terminalStateList = imMsg.getMsg().unpack(ImProto.TerminalStateList.class);
log.info("TerminalService multiInsert TerminalStateList:{}", terminalStateList);
List<ImProto.TerminalState> requestTerminalStateList = terminalStateList.getTerminalStateList();
if (!CollectionUtils.isEmpty(requestTerminalStateList)){
List<TerminalStateMonitorDO> tmplist = new ArrayList<>();
for (ImProto.TerminalState requestTerminalState : requestTerminalStateList){
TerminalStateMonitorDO terminalStateMonitorDO = new TerminalStateMonitorDO();
terminalStateMonitorDO.setBatteryLevel(requestTerminalState.getBatteryLevel());
terminalStateMonitorDO.setChargingState(requestTerminalState.getChargingState());
terminalStateMonitorDO.setTemperature(BigDecimal.valueOf(requestTerminalState.getTemperature()));
terminalStateMonitorDO.setUniqueCode(deviceInfo.getDeviceCode());
terminalStateMonitorDO.setStateTime(requestTerminalState.getStateTime());
tmplist.add(terminalStateMonitorDO);
}
this.saveBatch(tmplist);
}
} catch (Exception e) {
log.error("TerminalService multiInsert error:{}", e);
}
}
}
至此,服務端的處理邏輯寫完,然後比較費時間的是自己寫client的請求,終於經過兩三天時間總結好了,寫了個test類,如下
package com.chimeta.core;
import com.chimeta.netty.protobuf.ImProto;
import com.google.protobuf.Any;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
@Slf4j
@RunWith(MockitoJUnitRunner.class)
class NettyTerminalTest {
@Test
public void tryTest() throws InterruptedException {
ImProto.TerminalStateList terminalstateList = ImProto.TerminalStateList.newBuilder().build();
for (int i = 0; i < 3; i++) {
ImProto.TerminalState build = ImProto.TerminalState.newBuilder()
.setBatteryLevel(i)
.setChargingState(i * 11)
.setTemperature(i * 11.1)
.setStateTime(i * 111)
.build();
terminalstateList = terminalstateList.toBuilder().addTerminalState(build).build();
}
ImProto.ImMsg imMsg = ImProto.ImMsg.newBuilder().setId(66).setMsg(Any.pack(terminalstateList)).build();
Channel channel = new Bootstrap()
.group(new NioEventLoopGroup(1))
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
System.out.println("初始化連線...");
ch.pipeline().addLast("encode", new ProtobufEncoder())
.addLast(new ProtobufVarint32FrameDecoder()).addLast(new ProtobufVarint32LengthFieldPrepender());
}
})
.channel(NioSocketChannel.class).connect("192.168.123.170", 10001)
.sync()
.channel();
// channel.pipeline().addLast(new StringEncoder()).writeAndFlush(ByteBufAllocator.DEFAULT.buffer().writeBytes(imMsg.toByteArray()));
channel.pipeline().writeAndFlush(Unpooled.copiedBuffer(imMsg.toByteArray()));
System.out.println("over!");
}
}
好了,記錄下,以後就不會忘記了