Socket弊端
前言
1、可能有大量的线程处于休眠,只是等待输入或者输出数据
2、每个线程的调用栈都分配内存很庞大
3、线程间上下文切换开销
可以理解为你要雇那么多工人还有给工人每个人分一块地盘,而且你还得不停地切换用哪个工人。
引入NIO
IO
NIO
class java.nio.channels.Selector是Java的非阻塞I/O实现的关键。它使用了事件通知API以确定在一组非阻塞套接字中有哪些已经就绪能够进行I/O相关的操作。
Netty介绍
设计
统一的API,支持多种传输类型,阻塞的和非阻塞的
简单而强大的线程模型
真正的无连接数据报套接字支持
链接逻辑组件以支持复用
易于使用
详实的Javadoc和大量的示例集
不需要超过JDK 1.6+[7]的依赖。(一些可选的特性可能需要Java 1.7+和/或额外的依赖)
性能
拥有比Java的核心API更高的吞吐量以及更低的延迟
得益于池化和复用,拥有更低的资源消耗
最少的内存复制
健壮性
不会因为慢速、快速或者超载的连接而导致OutOfMemoryError
消除在高速网络中NIO应用程序常见的不公平读/写比率
安全性
完整的SSL/TLS以及StartTLS支持
可用于受限环境下,如Applet和OSGI
社区驱动
发布快速而且频繁
Netty核心
Channel
它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的I/O操作的程序组件)的开放连接,如读操作和写操作
回调;
Future;
事件和ChannelHandler
项目中用到的netty
netty作为服务端,向客户端推送视差信息
package com.wg.server;
import com.wg.constant.Constants;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.util.concurrent.Future;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.PreDestroy;
/**
* @author lst
* @date 2023/6/6 16:58
* @return null
*/
@Slf4j
@Service
public class DirectionServer {
@Value("${netty.thread-number:1}")
private int threadNumber;
@Value("${direction.port:8096}")
private int port;
@Autowired
private DirectionServerHandler directionServerHandler;
private Channel channel;
private final EventLoopGroup GROUP = new NioEventLoopGroup(threadNumber);
public void start() throws Exception {
// 1、启动器,负责装配netty组件,启动服务器
ChannelFuture channelFuture = new ServerBootstrap()
// 2、创建 NioEventLoopGroup,可以简单理解为 线程池 + Selector
.group(GROUP)
// 3、选择服务器的 ServerSocketChannel 实现
.channel(NioServerSocketChannel.class)
// 4、child 负责处理读写,该方法决定了 child 执行哪些操作
// ChannelInitializer 处理器(仅执行一次)
// 它的作用是待客户端SocketChannel建立连接后,执行initChannel以便添加更多的处理器
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new FixedLengthFrameDecoder(Constants.MEG_LENGTH));
ch.pipeline().addLast(directionServerHandler);
}
}).bind(port).sync();
if (channelFuture != null && channelFuture.isSuccess()) {
log.warn("DirectionServer start success, port = {}", port);
// 获取通道
channel = channelFuture.channel();
channelFuture.channel().closeFuture().addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
log.warn(future.channel().toString() + " 链路关闭");
// 链路关闭时,再释放线程池和连接句柄
GROUP.shutdownGracefully();
}
});
} else {
log.error("DirectionServer start failed!");
}
}
@PreDestroy
public void destroy() {
try {
if (channel != null) {
ChannelFuture await = channel.close().await();
if (!await.isSuccess()) {
log.error("DirectionServer channel close fail, {}", await.cause());
}
}
Future<?> future = GROUP.shutdownGracefully().await();
if (!future.isSuccess()) {
log.error("DirectionServer group shutdown fail, {}", future.cause());
}
if (log.isInfoEnabled()) {
log.info("DirectionServer shutdown success");
}
} catch (InterruptedException e) {
log.error("DirectionServer shutdown fail, {}", e);
}
}
}
/**
* WebServerHandler.java
* Created at 2022-08-30
* Created by chenyuxiang
* Copyright (C) 2022 WEGO Group, All rights reserved.
*/
package com.wg.server;
import cn.hutool.core.util.HexUtil;
import com.wg.constant.Constants;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author lst
* @date 2023/6/6 16:57
* @return null
*/
@Slf4j
@Service
@ChannelHandler.Sharable
public class DirectionServerHandler extends ChannelInboundHandlerAdapter {
/**
* 接入的主程序入口服务
*/
public static final Map<String, ChannelHandlerContext> DIRECTION_MAP = new ConcurrentHashMap<>();
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
String uuid = ctx.channel().id().asLongText();
DIRECTION_MAP.put(uuid, ctx);
log.info("连接请求进入: {}, 地址: {}", uuid, ctx.channel().remoteAddress());
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
String uuid = ctx.channel().id().asLongText();
DIRECTION_MAP.remove(uuid);
ctx.channel().close();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
try {
byte[] bytes = new byte[Constants.MEG_LENGTH];
in.readBytes(bytes);
log.info("收到消息 --> {}", HexUtil.encodeHexStr(bytes));
} finally {
// 释放ByteBuf
ReferenceCountUtil.release(in);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
String uuid = ctx.channel().id().asLongText();
DIRECTION_MAP.remove(uuid);
log.error(cause.getMessage());
ctx.close();
}
}
package com.wg.job;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.HexUtil;
import com.wg.constant.Constants;
import com.wg.controller.RecordSnapShotController;
import com.wg.model.RecordStatusRes;
import com.wg.server.WebServerHandler;
import com.wg.util.MessageUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Map;
import static cn.hutool.core.convert.Convert.hexToBytes;
import static com.wg.common.RecordStatus.RECORDING;
import static com.wg.controller.UserController.*;
import static com.wg.server.DirectionServerHandler.DIRECTION_MAP;
/**
* @author lst
* @date 2023年06月08日 15:07
*/
@Component
@Slf4j
public class DirectionJob {
private static int msgId = -1;
@Value("${directionAndRecordStatus.interval:1000}")
private long interval;
@Scheduled(fixedDelayString = "${working.sendDirectionMessageToClient:30000}")
public void sendDirectionMessageToClient() throws InterruptedException {
if (System.getProperty("os.name") != null && System.getProperty("os.name").toLowerCase().startsWith("windows")) {
return;
}
if (!DIRECTION_MAP.isEmpty()) {
for (Map.Entry<String, ChannelHandlerContext> entry : DIRECTION_MAP.entrySet()) {
String msgId0xFF = generateMsgId();
String cmdName0xFF = "08";
String key = msgId0xFF + cmdName0xFF + fillZero(Integer.toHexString(horizontalVal & 0xFF)) + fillZero(Integer.toHexString(upAndDownVal & 0xFF));
byte[] bytes = hexToBytes(key);
log.info(key + "," + "此处发送上下左右数值指令:" + HexUtil.encodeHexStr(bytes));
entry.getValue().writeAndFlush(Unpooled.copiedBuffer(bytes));
Thread.sleep(interval);
msgId0xFF = generateMsgId();
cmdName0xFF = "09";
RecordStatusRes recordStatusRes = new RecordSnapShotController().getCplusplusRecordStatus();
String record0xFF = "00";
if (recordStatusRes != null && recordStatusRes.getStatus() != null && recordStatusRes.getStatus().equals(RECORDING)) {
//录制中
record0xFF = "01";
}
String recordKey = msgId0xFF + cmdName0xFF + record0xFF;
byte[] recordBytes = hexToBytes(recordKey);
log.info(recordKey + "," + "此处发送录像状态指令:" + HexUtil.encodeHexStr(recordBytes));
entry.getValue().writeAndFlush(Unpooled.copiedBuffer(recordBytes));
}
}
}
public static String generateMsgId() {
msgId++;
//取低8位就可以
String ret = Integer.toHexString(msgId & 0xFF);
ret = ret.length() == 1 ? "0" + ret : ret;
return ret;
}
private static String fillZero(String str) {
str = str.length() == 1 ? "0" + str : str;
return str;
}
}