|
@@ -0,0 +1,367 @@
|
|
|
|
|
+package com.zksy.audio.utils;
|
|
|
|
|
+
|
|
|
|
|
+import cn.hutool.core.convert.Convert;
|
|
|
|
|
+import cn.hutool.core.date.DateField;
|
|
|
|
|
+import cn.hutool.core.date.DatePattern;
|
|
|
|
|
+import cn.hutool.core.date.DateUtil;
|
|
|
|
|
+import cn.hutool.core.lang.Validator;
|
|
|
|
|
+import cn.hutool.core.thread.ThreadUtil;
|
|
|
|
|
+import cn.hutool.core.util.StrUtil;
|
|
|
|
|
+import com.zksy.audio.domain.ChannelInfo;
|
|
|
|
|
+import com.zksy.audio.domain.NoiseInfo;
|
|
|
|
|
+import com.zksy.audio.service.NoiseInfoService;
|
|
|
|
|
+import com.zksy.audio.utils.CrtHelper;
|
|
|
|
|
+import io.netty.buffer.ByteBuf;
|
|
|
|
|
+import io.netty.buffer.Unpooled;
|
|
|
|
|
+import io.netty.channel.ChannelHandler;
|
|
|
|
|
+import io.netty.channel.ChannelHandlerContext;
|
|
|
|
|
+import io.netty.channel.ChannelId;
|
|
|
|
|
+import io.netty.channel.ChannelInboundHandlerAdapter;
|
|
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.apache.commons.io.FileUtils;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
|
|
+
|
|
|
|
|
+import java.io.File;
|
|
|
|
|
+import java.io.IOException;
|
|
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
|
|
+import java.util.Arrays;
|
|
|
|
|
+import java.util.concurrent.ConcurrentHashMap;
|
|
|
|
|
+import java.util.concurrent.ConcurrentMap;
|
|
|
|
|
+import java.util.regex.Matcher;
|
|
|
|
|
+import java.util.regex.Pattern;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 消息处理器
|
|
|
|
|
+ * 负责处理各种请求和响应
|
|
|
|
|
+ */
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Component
|
|
|
|
|
+@ChannelHandler.Sharable
|
|
|
|
|
+public class MessageHandler extends ChannelInboundHandlerAdapter {
|
|
|
|
|
+
|
|
|
|
|
+ public ConcurrentMap<ChannelId, ChannelInfo> channelGroup = new ConcurrentHashMap<>();
|
|
|
|
|
+
|
|
|
|
|
+ public ConcurrentMap<String, ChannelInfo> deviceGroup = new ConcurrentHashMap<>();
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${wlds.fileUploadBaseDir}")
|
|
|
|
|
+ private String fileUploadBaseDir;
|
|
|
|
|
+
|
|
|
|
|
+ private final static String head = "524946462440010057415645666D742010000000010001000020000000400000020010006461746100400100";
|
|
|
|
|
+ private final NoiseInfoService service;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ public MessageHandler(NoiseInfoService noiseInfoService) {
|
|
|
|
|
+ this.service = noiseInfoService;
|
|
|
|
|
+ }
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
|
|
|
|
+ //log.info("==收到客户端:{}|{}发送的消息:{}============:", ctx.channel().id().asLongText(), ctx.channel().remoteAddress(), msg);
|
|
|
|
|
+ try {
|
|
|
|
|
+ ByteBuf byteBuffer = (ByteBuf) msg;
|
|
|
|
|
+ if (byteBuffer == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ int readableLength = byteBuffer.readableBytes();
|
|
|
|
|
+ byte[] arrayWithOutHead = new byte[readableLength];//去掉包头标识的数据
|
|
|
|
|
+ byteBuffer.getBytes(byteBuffer.readerIndex(), arrayWithOutHead);
|
|
|
|
|
+
|
|
|
|
|
+ String hex = CrtHelper.byteToHexStr1(arrayWithOutHead);
|
|
|
|
|
+ log.info("收到消息:{}", hex);
|
|
|
|
|
+ double power = 0;
|
|
|
|
|
+ String deviceNo;
|
|
|
|
|
+ double gain = 0;
|
|
|
|
|
+
|
|
|
|
|
+ byte[] bytes;
|
|
|
|
|
+ String asciiString;
|
|
|
|
|
+ String path;
|
|
|
|
|
+
|
|
|
|
|
+ ChannelInfo info;
|
|
|
|
|
+
|
|
|
|
|
+ ChannelInfo channelInfo = new ChannelInfo("puqi", ctx.channel());
|
|
|
|
|
+
|
|
|
|
|
+ if (!channelGroup.containsKey(ctx.channel().id())) {
|
|
|
|
|
+ // 加入客户端
|
|
|
|
|
+ channelGroup.put(ctx.channel().id(), channelInfo);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ info = channelGroup.get(ctx.channel().id());
|
|
|
|
|
+
|
|
|
|
|
+ // 申报身份
|
|
|
|
|
+ if (hex.startsWith("242424")) {
|
|
|
|
|
+ String reativePath = "";
|
|
|
|
|
+ String[] hexArray = Arrays.stream(hex.split("0D0A")).filter(StrUtil::isNotEmpty).toArray(String[]::new);
|
|
|
|
|
+ log.info("hexArray.length:{}", hexArray.length);
|
|
|
|
|
+ if (hexArray.length >= 1) {
|
|
|
|
|
+ bytes = CrtHelper.strToToHexByte1(hexArray[0]);
|
|
|
|
|
+ asciiString = new String(bytes, StandardCharsets.US_ASCII);
|
|
|
|
|
+ deviceNo = asciiString.replaceAll("\\$\\$\\$", "").replaceAll("\r\n", "").trim();
|
|
|
|
|
+ path = getPathName(deviceNo, reativePath);
|
|
|
|
|
+
|
|
|
|
|
+ info.setDeviceNo(deviceNo);
|
|
|
|
|
+ info.setPath(path);
|
|
|
|
|
+ info.setFirst(true);
|
|
|
|
|
+ info.setFilePath(path);
|
|
|
|
|
+ info.setLength(0);
|
|
|
|
|
+
|
|
|
|
|
+ deviceGroup.put(deviceNo, info);
|
|
|
|
|
+ channelGroup.put(ctx.channel().id(), info);
|
|
|
|
|
+ info = channelGroup.get(ctx.channel().id());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (hexArray.length >= 2) {
|
|
|
|
|
+ bytes = CrtHelper.strToToHexByte1(hexArray[1]);
|
|
|
|
|
+ asciiString = new String(bytes, StandardCharsets.US_ASCII);
|
|
|
|
|
+ asciiString = asciiString.replaceAll("VBAT:", "").replaceAll("V", "").replaceAll("\r\n", "").trim();
|
|
|
|
|
+ if (Validator.isNumber(asciiString)) {
|
|
|
|
|
+ power = Convert.toDouble(asciiString);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (hexArray.length >= 3) {
|
|
|
|
|
+ String gainHex = hexArray[3];
|
|
|
|
|
+ bytes = CrtHelper.strToToHexByte1(gainHex);
|
|
|
|
|
+ asciiString = new String(bytes, StandardCharsets.US_ASCII);
|
|
|
|
|
+ asciiString = asciiString.replaceAll("Gain:", "").replaceAll("\r\n", "");
|
|
|
|
|
+ if (Validator.isNumber(asciiString)) {
|
|
|
|
|
+ gain = Convert.toDouble(asciiString);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ channelGroup.get(ctx.channel().id()).setPower(power);
|
|
|
|
|
+ channelGroup.get(ctx.channel().id()).setGain(gain);
|
|
|
|
|
+
|
|
|
|
|
+ if (hexArray.length >= 4) {
|
|
|
|
|
+ String videoHex = hexArray[3];
|
|
|
|
|
+ info = channelGroup.get(ctx.channel().id());
|
|
|
|
|
+ path = info.getPath();
|
|
|
|
|
+ info.setLength(info.getLength() + (videoHex.length() / 2));
|
|
|
|
|
+ String finalPath1 = path;
|
|
|
|
|
+ ThreadUtil.execAsync(() -> {
|
|
|
|
|
+ saveHexStringToFile(finalPath1, videoHex);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+ // 申报电量
|
|
|
|
|
+ else if (hex.startsWith("56424154")) {
|
|
|
|
|
+ if (!channelGroup.containsKey(ctx.channel().id())) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StrUtil.isEmpty(channelGroup.get(ctx.channel().id()).getDeviceNo())) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ String[] hexArray = Arrays.stream(hex.split("0D0A")).filter(StrUtil::isNotEmpty).toArray(String[]::new);
|
|
|
|
|
+ if (hexArray.length >= 1) {
|
|
|
|
|
+ bytes = CrtHelper.strToToHexByte1(hexArray[0]);
|
|
|
|
|
+ asciiString = new String(bytes, StandardCharsets.US_ASCII);
|
|
|
|
|
+ asciiString = asciiString.replaceAll("VBAT:", "").replaceAll("V", "").replaceAll("\r\n", "").trim();
|
|
|
|
|
+ if (Validator.isNumber(asciiString)) {
|
|
|
|
|
+ power = Double.parseDouble(asciiString);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (hexArray.length >= 2) {
|
|
|
|
|
+ String gainHex = hexArray[1];
|
|
|
|
|
+ bytes = CrtHelper.strToToHexByte1(gainHex);
|
|
|
|
|
+ asciiString = new String(bytes, StandardCharsets.US_ASCII);
|
|
|
|
|
+ asciiString = asciiString.replaceAll("Gain:", "").replaceAll("\r\n", "");
|
|
|
|
|
+ if (Validator.isNumber(asciiString)) {
|
|
|
|
|
+ gain = Double.parseDouble(asciiString);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ channelGroup.get(ctx.channel().id()).setGain(gain);
|
|
|
|
|
+ channelGroup.get(ctx.channel().id()).setPower(power);
|
|
|
|
|
+
|
|
|
|
|
+ if (hexArray.length >= 3) {
|
|
|
|
|
+ String videoHex = hexArray[2];
|
|
|
|
|
+ info = channelGroup.get(ctx.channel().id());
|
|
|
|
|
+ path = info.getPath();
|
|
|
|
|
+ info.setLength(info.getLength() + (videoHex.length() / 2));
|
|
|
|
|
+ String finalPath1 = path;
|
|
|
|
|
+ ThreadUtil.execAsync(() -> {
|
|
|
|
|
+ saveHexStringToFile(finalPath1, videoHex);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 增益值
|
|
|
|
|
+ else if (hex.startsWith("4761696E")) {
|
|
|
|
|
+ if (!channelGroup.containsKey(ctx.channel().id())) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StrUtil.isEmpty(channelGroup.get(ctx.channel().id()).getDeviceNo())) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ String[] hexArray = Arrays.stream(hex.split("0D0A")).filter(StrUtil::isNotEmpty).toArray(String[]::new);
|
|
|
|
|
+ if (hexArray.length >= 1) {
|
|
|
|
|
+ String gainHex = hexArray[0];
|
|
|
|
|
+ bytes = CrtHelper.strToToHexByte1(gainHex);
|
|
|
|
|
+ asciiString = new String(bytes, StandardCharsets.US_ASCII);
|
|
|
|
|
+ asciiString = asciiString.replaceAll("Gain:", "").replaceAll("\r\n", "");
|
|
|
|
|
+ if (Validator.isNumber(asciiString)) {
|
|
|
|
|
+ gain = Double.parseDouble(asciiString);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ channelGroup.get(ctx.channel().id()).setGain(gain);
|
|
|
|
|
+ if (hexArray.length >= 2) {
|
|
|
|
|
+ String videoHex = hexArray[1];
|
|
|
|
|
+ info = channelGroup.get(ctx.channel().id());
|
|
|
|
|
+ path = info.getPath();
|
|
|
|
|
+ info.setLength(info.getLength() + (videoHex.length() / 2));
|
|
|
|
|
+ String finalPath1 = path;
|
|
|
|
|
+ ThreadUtil.execAsync(() -> {
|
|
|
|
|
+ saveHexStringToFile(finalPath1, videoHex);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (hex.startsWith("4F4B")) {
|
|
|
|
|
+ bytes = CrtHelper.strToToHexByte1(hex);
|
|
|
|
|
+ asciiString = new String(bytes, StandardCharsets.US_ASCII);
|
|
|
|
|
+ log.info("==收到客户端:{},GPS信息:{}", ctx.channel().id().asLongText(), asciiString);
|
|
|
|
|
+ info = channelGroup.get(ctx.channel().id());
|
|
|
|
|
+ String pattern = ",(\\d+\\.\\d+),\\d+\\.\\d+";
|
|
|
|
|
+ Pattern regex = Pattern.compile(pattern);
|
|
|
|
|
+ Matcher match = regex.matcher(asciiString);
|
|
|
|
|
+ if (match.matches()) {
|
|
|
|
|
+ // 提取经纬度,注意第一个捕获组是纬度,第二个捕获组(如果有的话)是经度
|
|
|
|
|
+ // 但由于我们只关心第一个逗号后的经纬度,所以可以直接拆分
|
|
|
|
|
+ String[] coords = match.group(0).replaceFirst(",", "").split(",");
|
|
|
|
|
+ if (coords.length == 2) {
|
|
|
|
|
+ double lat = Double.parseDouble(coords[0]);
|
|
|
|
|
+ double lon = Double.parseDouble(coords[1]);
|
|
|
|
|
+ log.info("经纬度:{},{}", lat, lon);
|
|
|
|
|
+ //-----------
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.info("==收到客户端:{}的设备调试信息:{}", ctx.channel().id().asLongText(), asciiString);
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (!channelGroup.containsKey(ctx.channel().id())) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StrUtil.isEmpty(channelGroup.get(ctx.channel().id()).getDeviceNo())) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ info = channelGroup.get(ctx.channel().id());
|
|
|
|
|
+ path = channelGroup.get(ctx.channel().id()).getPath();
|
|
|
|
|
+ String finalPath1 = path;
|
|
|
|
|
+ String headHex;
|
|
|
|
|
+ byte[] length = new byte[4];
|
|
|
|
|
+ if (info.getFirst() && !hex.startsWith("52494646") ){
|
|
|
|
|
+ //用于没带头文件的音频数据
|
|
|
|
|
+ headHex = head + hex;
|
|
|
|
|
+ long videoMaxSize = 80 * 1024 + 44;
|
|
|
|
|
+ info.setVideoMaxSize(videoMaxSize);
|
|
|
|
|
+
|
|
|
|
|
+ } else {
|
|
|
|
|
+ headHex = hex;
|
|
|
|
|
+ if (hex.startsWith("52494646")){
|
|
|
|
|
+ //24001900 ====> 163840
|
|
|
|
|
+ length[0] = (byte) Integer.parseInt(hex.substring(14, 16), 16);
|
|
|
|
|
+ length[1] = (byte) Integer.parseInt(hex.substring(12, 14), 16);
|
|
|
|
|
+ length[2] = (byte) Integer.parseInt(hex.substring(10, 12), 16);
|
|
|
|
|
+ length[3] = (byte) Integer.parseInt(hex.substring(8, 10), 16);
|
|
|
|
|
+ long videoMaxSize = (Long.parseLong(CrtHelper.byteToHexStr1(length), 16) - 36) / 10 + 44;
|
|
|
|
|
+ info.setVideoMaxSize(videoMaxSize);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ info.setFirst(false);
|
|
|
|
|
+ info.setLength(info.getLength() + headHex.length() / 2);
|
|
|
|
|
+ ThreadUtil.execAsync(() -> {
|
|
|
|
|
+ saveHexStringToFile(finalPath1, headHex);
|
|
|
|
|
+ });
|
|
|
|
|
+ log.info("======收到客户端总字节{}-----videoMaxSize:{}",info.getLength(),info.getVideoMaxSize());
|
|
|
|
|
+ // 如果接收音频文件字节大于头文件标识长度;
|
|
|
|
|
+ if (info.getLength() >= info.getVideoMaxSize()) {
|
|
|
|
|
+ if (info.getGatheTime() == null) {
|
|
|
|
|
+ info.setGatheTime(DateUtil.date());
|
|
|
|
|
+ }
|
|
|
|
|
+ // TODO: 2023/9/5 音频文件信息入库
|
|
|
|
|
+ int rms = DataParser.calculateRMS(fileUploadBaseDir+path);
|
|
|
|
|
+ double centerFrequency = DataParser.calcCenterFrequency(fileUploadBaseDir+path);
|
|
|
|
|
+ NoiseInfo noise = new NoiseInfo();
|
|
|
|
|
+ noise.setEncode(info.getDeviceNo());
|
|
|
|
|
+ noise.setPower(info.getPower());
|
|
|
|
|
+ noise.setGain(info.getGain());
|
|
|
|
|
+ noise.setRms(rms);
|
|
|
|
|
+ noise.setCenterFrequency(centerFrequency);
|
|
|
|
|
+ noise.setFilepath(info.getFilePath());
|
|
|
|
|
+ noise.setTs(DateUtil.format(DateUtil.offset(info.getGatheTime(), DateField.HOUR, -8),
|
|
|
|
|
+ DatePattern.NORM_DATETIME_PATTERN));
|
|
|
|
|
+ //log.info(JsonUtils.toJsonString(noise));
|
|
|
|
|
+ service.save(noise);
|
|
|
|
|
+ //设备默认上报三次音频数据, 数据接收完成回复设备Ok,停止上报
|
|
|
|
|
+ byte[] cmdBytes = "OK\r\n".getBytes(StandardCharsets.US_ASCII);
|
|
|
|
|
+ ctx.channel().writeAndFlush(Unpooled.copiedBuffer(cmdBytes));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error(e.getMessage(), e);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String getPathName(String deviceNo, String tempPath) {
|
|
|
|
|
+ String ext = ".wav";
|
|
|
|
|
+ tempPath = "/devices/" + deviceNo + "/" + DateUtil.format(DateUtil.date(), DatePattern.PURE_DATE_FORMAT);
|
|
|
|
|
+ File desc = new File(fileUploadBaseDir, tempPath);
|
|
|
|
|
+ if (!desc.exists()) {
|
|
|
|
|
+ if (!desc.getParentFile().exists()) {
|
|
|
|
|
+ desc.getParentFile().mkdirs();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ String fileName = deviceNo + "_" + DateUtil.format(DateUtil.date(), DatePattern.PURE_DATETIME_FORMAT) + ext;
|
|
|
|
|
+ return tempPath + "/" + fileName;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void saveHexStringToFile(String filePath, String hexString) {
|
|
|
|
|
+ if (StrUtil.isEmpty(hexString)) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ log.info("音频字节:{}",hexString);
|
|
|
|
|
+ byte[] bytes = CrtHelper.strToToHexByte1(hexString);
|
|
|
|
|
+ try {
|
|
|
|
|
+ FileUtils.writeByteArrayToFile(new File(fileUploadBaseDir + filePath), bytes, true);
|
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void channelActive(ChannelHandlerContext ctx) throws Exception {
|
|
|
|
|
+ super.channelActive(ctx);
|
|
|
|
|
+ log.info("==客户端:{},连接成功", ctx.channel().remoteAddress());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
|
|
|
|
|
+ ctx.flush();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
|
|
|
|
+ log.error("TCP异常:{}", cause.getMessage());
|
|
|
|
|
+ super.exceptionCaught(ctx, cause);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
|
|
|
|
+ log.info("==客户端:{},掉线了", ctx.channel().remoteAddress());
|
|
|
|
|
+ ChannelInfo channelInfo = channelGroup.remove(ctx.channel().id());
|
|
|
|
|
+ if (channelInfo != null && StrUtil.isNotEmpty(channelInfo.getDeviceNo())) {
|
|
|
|
|
+ deviceGroup.remove(channelInfo.getDeviceNo());
|
|
|
|
|
+ }
|
|
|
|
|
+ super.channelInactive(ctx);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
|
|
|
|
|
+ log.info("==客户端:{},掉线了,ChannelUnregistered", ctx.channel().remoteAddress());
|
|
|
|
|
+ super.channelUnregistered(ctx);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
|
|
|
|
|
+ log.info("==服务端:{},下线了", ctx.toString());
|
|
|
|
|
+ super.handlerRemoved(ctx);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|