项目需要对接视频接口,某康提供的是tcp接口,负责人想着把数据保存在本地落库,方便以后的统计。这事就落到了我头上,我选netty。
开始东抄西抄,终于拼了一个不成样子的东西。很多地方返回不对。
开始找支持,同事A总共帮着解决了3个大问题。
1、登录后,发送心跳返回结果不对
ByteBuf 写完就清空了,导致心跳不对。
2、心跳正常后,发现netty阻塞了restful请求。
开启子线程,解决了线程阻塞
3、tcp返回的包,分多次返回,网上说的粘包和拆包
使用分隔符的Handle解决。
4、接口实时restful返回。
这3个问题。自己见解有限,一个都没弄好,多亏同事。完整代码如下:
netty客户端:
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoop;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class BootNettyClient {
@Value("${ysk.host:120.24.27.47}")
private String host;
@Value("${ysk.port:10167}")
private Integer port;
private boolean init=false;
// @Resource
// private YskSender yskSender;
private NettyClientChannelInitializer clientInitializer;
private SocketChannel socketChannel;
private CountDownLatch lathc;
public BootNettyClient(){
}
@PostConstruct
public void run(){
new Thread(()-> {
try {
log.info("优视康的接口地址为:"+host+":"+port);
start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
public void start() throws InterruptedException {
/**
* Bootstrap 是一个启动NIO服务的辅助启动类 客户端的
*/
lathc = new CountDownLatch(0);
clientInitializer=new NettyClientChannelInitializer<SocketChannel>(lathc);
Bootstrap bootstrap = new Bootstrap();
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
/**
* 设置group
*/
if (bootstrap != null) {
bootstrap = bootstrap.group(eventLoopGroup)
/**
* 关联客户端通道
*/
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
//4 设置通道的参数
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
/**
* 设置 I/O处理类,主要用于网络I/O事件,记录日志,编码、解码消息
*/
.handler(clientInitializer)
;
}
/**
* 连接服务端
*/
ChannelFuture future = bootstrap.connect(host, port).sync()
.addListener((ChannelFuture futureListener) -> {
final EventLoop eventLoop = futureListener.channel().eventLoop();
if (!futureListener.isSuccess()) {
log.info("与服务端断开连接!在10s之后准备尝试重连!");
eventLoop.schedule(() -> {
try {
start();
} catch (Exception e) {
e.printStackTrace();
}
}, 10, TimeUnit.SECONDS);
}
});
if(future.isSuccess()){
socketChannel = (SocketChannel)future.channel();
init=true;
log.info("netty server链接成功");
}
future.channel().closeFuture().sync();
} finally {
/**
* 退出,释放资源
*/
eventLoopGroup.shutdownGracefully();
}
}
public String setMessage(String msg) throws InterruptedException{
//单例模式获取ChannelFuture对象
if(init){
socketChannel.writeAndFlush(msg);
//发送数据控制门闩加一
lathc = new CountDownLatch(1);
clientInitializer.resetLathc(lathc);
lathc.await();//开始等待结果返回后执行下面的代码
return clientInitializer.getServerResult();
}
return "";
}
}
public class NettyClientChannelInitializer<SocketChannel> extends ChannelInitializer<Channel> {
private static final String DELIMITER_STR = "**";
private CountDownLatch lathc;
private NettyClientHandlerAdapter handler;
public NettyClientChannelInitializer(CountDownLatch lathc) {
this.lathc = lathc;
}
@Override
protected void initChannel(Channel ch) throws Exception {
handler = new NettyClientHandlerAdapter(lathc);
ByteBuf delimiter = Unpooled.copiedBuffer(DELIMITER_STR.getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(Integer.MAX_VALUE,delimiter));
ch.pipeline().addLast(new IdleStateHandler(5,10,10, TimeUnit.SECONDS));
// ChannelOutboundHandler,依照逆序执行(从下往上)
ch.pipeline().addLast("encoder", new StringEncoder());
// 属于ChannelInboundHandler,依照顺序执行(从上往下)
ch.pipeline().addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
/**
* 自定义ChannelInboundHandlerAdapter
*/
ch.pipeline().addLast(handler);
}
public void resetLathc(CountDownLatch lathc) {
handler.resetLatch(lathc);
}
public String getServerResult() {
return handler.getResult();
}
}
处理的hander:
import com.alibaba.fastjson.JSON;
import com.etcc.youshikang.entity.CmnHeartBeat;
import com.etcc.youshikang.entity.CmuLogin;
import com.etcc.youshikang.entity.YskResEntity;
import com.etcc.youshikang.enums.ActionEnum;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.ReferenceCountUtil;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.concurrent.CountDownLatch;
import static com.etcc.youshikang.enums.MidEnum.MID_AAA;
/**
* I/O数据读写处理类
*/
@ChannelHandler.Sharable
@Slf4j
public class NettyClientHandlerAdapter extends ChannelInboundHandlerAdapter {
private CountDownLatch lathc;
private String result;
// private YskSender yskSender;
//
// public NettyClientHandlerAdapter(YskSender yskSender) {
// this.yskSender = yskSender;
// }
public NettyClientHandlerAdapter(CountDownLatch lathc) {
this.lathc = lathc;
}
public void resetLatch(CountDownLatch lathc) {
this.lathc = lathc;
}
public String getResult() {
return result;
}
/**
* 从服务端收到新的数据时,这个方法会在收到消息时被调用
*
* @param ctx
* @param msg
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception, IOException {
try {
String msgStr = msg.toString();
//回应服务端
YskResEntity entity = JSON.parseObject(msgStr, YskResEntity.class);
String action = entity.getAction();
if (action.equals(ActionEnum.cmuLogin.getType())) {
log.debug(ActionEnum.cmuLogin.getName());
} else if (action.equals(ActionEnum.cmuHeartBeat.getType())) {
log.debug(ActionEnum.cmuHeartBeat.getName());
} else{
if (action.equals(ActionEnum.cmuInitResData.getType())) {
log.debug(ActionEnum.cmuInitResData.getName());
// yskSender.sendTaskMsg(ActionEnum.cmuInitResData.getType(), msgStr);
} else if (action.equals(ActionEnum.cmuQueryResData.getType())) {
log.debug(ActionEnum.cmuQueryResData.getName());
// yskSender.sendTaskMsg(ActionEnum.cmuQueryResData.getType(), msgStr);
}
result=msgStr;
lathc.countDown();
}
} finally {
ReferenceCountUtil.release(msg);
}
}
/**
* 从服务端收到新的数据、读取完成时调用
*
* @param ctx
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws IOException {
log.debug("channelReadComplete");
}
/**
* 当出现 Throwable 对象才会被调用,即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时
*
* @param ctx
* @param cause
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws IOException {
log.debug("exceptionCaught");
cause.printStackTrace();
ctx.close();//抛出异常,断开与客户端的连接
}
/**
* 客户端与服务端第一次建立连接时 执行
*
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception, IOException {
super.channelActive(ctx);
InetSocketAddress inSocket = (InetSocketAddress) ctx.channel().remoteAddress();
String clientIp = inSocket.getAddress().getHostAddress();
log.debug("channelActive:" + clientIp + ctx.name());
//上面都是一些排查数据信息
CmuLogin cmuLogin = new CmuLogin();
cmuLogin.setAction(ActionEnum.cmuLogin.getType());
cmuLogin.setCuid(0);
cmuLogin.setMid(MID_AAA.getValue());
cmuLogin.setPassword("123456789@Usc?.");
cmuLogin.setUsername("admin");
String concat = JSON.toJSONString(cmuLogin).concat("**");
ctx.writeAndFlush(concat);
}
/**
* 客户端与服务端 断连时 执行
*
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception, IOException {
super.channelInactive(ctx);
InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();
String clientIp = insocket.getAddress().getHostAddress();
ctx.close(); //断开连接时,必须关闭,否则造成资源浪费
System.out.println("channelInactive:" + clientIp);
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent e = (IdleStateEvent) evt;
switch (e.state()) {
case WRITER_IDLE:
CmnHeartBeat cmnHeartBeat = new CmnHeartBeat();
String heatMsg = JSON.toJSONString(cmnHeartBeat).concat("**");
ctx.writeAndFlush(heatMsg);
break;
default:
break;
}
}
}
}
@Service
@Slf4j
public class YskServiceImpl implements IYskService {
@Resource
BootNettyClient bootNettyClient;
@Autowired
private IMonitorResourceService monitorResource;
@Autowired
private YskSender yskSender;
@Override
public CmuInitRes cmuInitResData(CmuInit cmuInit) throws Exception {
cmuInit.setMid(MID_RESOURCETREE.getValue());
cmuInit.setAction(ActionEnum.cmuInitResData.getType());
String content = JSON.toJSONString(cmuInit).concat("**");
String s = bootNettyClient.setMessage(content);
return JSON.parseObject(s,CmuInitRes.class);
}
@Override
public CmuQueryRes cmuQueryResData(CmuQuery cmuQuery) throws Exception {
cmuQuery.setMid(MID_RESOURCETREE.getValue());
cmuQuery.setAction(ActionEnum.cmuQueryResData.getType());
String content = JSON.toJSONString(cmuQuery).concat("**");
String s = bootNettyClient.setMessage(content);
return JSON.parseObject(s,CmuQueryRes.class);
}
@Override
public void cmu(CmuInit cmuInit) throws Exception {
CmuInitRes res = cmuInitResData(cmuInit);
log.info("资源初始化:"+JSON.toJSONString(res));
if(res.getOpresult()==1){
CmuNode node = res.getNode();
buildTree(node);
//构建列表,转备入库mysql
// new Thread(()->{
process(node);
// }).start();
}
}
private void buildTree(CmuNode node) {
List<CmuNode> children = node.getChildren();
children.stream().forEach(item->{
if("device".equals(item.getDevicetypename())){
buildTree(item);
}
else if("group".equals(item.getDevicetypename())){
CmuQuery cmuQuery=new CmuQuery();
cmuQuery.setResourceId(String.valueOf(item.getResourceid()));
try {
CmuQueryRes cmuQueryRes = cmuQueryResData(cmuQuery);
final List<CmuNode> subChild = cmuQueryRes.getNode().getChildren();
item.setChildren(subChild);
buildTree(item);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
/**
* 处理分几步:
* 1、转换
* 2、入mysql
* 3、发es
* 4、入redis
* 5、调用组织结构的接口
* @param node
*/
private void process(CmuNode node) {
//转换为平级的列表
List<CmuNode> nodeList = node.toList();
List<MonitorResource> list = nodeList.stream().map(item -> {
MonitorResource resource = new MonitorResource();
resource.setId(String.valueOf(item.getResourceid()));
resource.setResourceName(item.getName());
resource.setResourceType(String.valueOf(item.getResourcetypeid()));
resource.setDeviceTypeName(item.getDevicetypename());
resource.setBoundpuid(item.getBoundpuid());
resource.setMainCodeStream(item.getMainCodeStream());
resource.setSubCodeStream(item.getSubCodeStream());
resource.setThirdCodeStream(item.getThirdCodeStream());
resource.setFourCodeStream(item.getFourCodeStream());
resource.setLongitude(String.valueOf(item.getLongitude()));
resource.setLatitude(String.valueOf(item.getLatitude()));
resource.setParentId(item.getParentId());
resource.setGbId(item.getGbid());
resource.setStatue(item.getStatue());
return resource;
}).collect(Collectors.toList());
//mysql保存
mysql(list);
//发送到es
es(list);
//es构建基本树
reids(list);
}
/**
* redis处理
* @param list
*/
private void reids(List<MonitorResource> list) {
//先清空
RedisUtil.del("cmuInitResData");
//处理list,构建 tree 添加level
List<MonitorResourceVo> monitorResourceVos = MonitorResourceAdapter.adaptMonitorResourceVos(list);
MonitorResourceVo root = monitorResourceVos.stream().filter(temp -> StringUtils.isEmpty(temp.getParentId())).findFirst().get();
monitorResourceVos.remove(root);
Map<String, List<MonitorResourceVo>> map = monitorResourceVos.stream().collect(Collectors.groupingBy(MonitorResourceVo::getParentId));
monitorResourceVos.forEach(temp->{
if(!"channel".equals(temp.getDeviceTypeName())){
temp.setChild(map.get(temp.getId()));
}
});
root.setChild(map.get(root.getId()));
//保存
RedisUtil.set("cmuInitResData",root);
}
/**
* 调用es接口
* @param list
*/
private void es(List<MonitorResource> list) {
list = list.stream().filter(temp -> "channel".equals(temp.getDeviceTypeName())).collect(Collectors.toList());
//发送到es的mq进行消费
yskSender.sendTaskMsg(RiskConstant.RISK_ROUTING_KEY,list);
}
/**
* 保存到es接口
* @param list
*/
private void mysql(List<MonitorResource> list) {
if(list.size()>0){
log.info("清空优视康视频数据");
LambdaUpdateWrapper<MonitorResource> uw=new LambdaUpdateWrapper<>();
uw.eq(MonitorResource::getDeleted,0);
monitorResource.remove(uw);
log.info("重新保存优视康视频数据");
monitorResource.saveBatch(list);
}
}
}