La empresa tiene un proyecto dedicado a acoplar estacionamiento de terceros, crm, erp y otros sistemas comerciales. Soy responsable del proyecto. La docena o más de lugares están todos implementados en base a Http. De repente, un día, la implementación me dijo que había un sistema de aparcamiento basado en el protocolo TCP para acceder. Hablando con franqueza, lo rechacé al principio, porque me resultaba difícil aceptar la introducción de un dispositivo que era tan difícil de mantener y que destruiría en gran medida los límites del sistema original. Si no puedes soportarlo, es fuerte. Simplemente no cambio la fábrica de automóviles. Te encanta. Sin respuesta, de ninguna manera, solo muerde la bala y piensa en una manera.
1. La necesidad de un tonto
El estacionamiento no es lo mismo que crm
, erp
por lo general, este tipo de sistema de terceros lo iniciamos nosotros. El negocio principal del estacionamiento no es solo la verificación y el pago de tarifas que necesitamos para llamar activamente, sino también el empuje de entrada y salida del vehículo que requiere que un tercero inicie la llamada . Bueno, si se trata de hacer un sistema de estacionamiento basado en esto y proporcionar estos métodos tcp
al lograrlos, es un protocolo full-duplex. El problema es que somos un tercero para llamar, y todo nuestro negocio se Http
implementa en base a las respuestas de las solicitudes. Sin embargo, cambiar tcp
a una conexión larga traerá muchos problemas, por ejemplo, he enviado una solicitud al mismo canal, ¿cómo puedo hacer que los datos devueltos correspondan a mi solicitud? Podría decir, simple, ¿simplemente agregue una ID de solicitud? native!
Como dije antes, la gente es fuerte, no cambiaré nada, te guste o no. E incluso si se resuelve este problema, el límite del sistema no se puede controlar y será difícil que otros colegas de mantenimiento lo reciban.
2. Siempre hay más caminos que dificultades
Habiendo dicho todo eso, todavía queda trabajo por hacer. Mi idea es muy simple. Dado que el sistema original tcp
es difícil de mantener mediante la introducción de protocolos, simplemente podemos construir un nuevo sistema, empaquetarlo en un http
protocolo y proporcionarlo a nuestro sistema. Si no hay una ID de solicitud, generaremos la solicitud. según datos del protocolo canto de identificación.
Al final, la solución que di es construir un servicio intermedio basado en netty
y springboot
para aceptar los datos de entrada y salida de la fábrica de automóviles de destino y convertirlos en una http
llamada a nuestra interfaz de entrada y salida de estacionamiento estándar, y al mismo tiempo proporcionar una consulta de tarifas estándar y una interfaz de pago para nuestro sistema.
3. Realización definitiva
También es la primera vez que netty
lo uso. He leído un netty
libro real antes, y no hay otra persona en la empresa que lo haya usado, así que muchas cosas solo puedo explorarlas yo mismo. Estas son solo algunas implementaciones clave
1. Decodificador
La mayoría de los tcp
protocolos personalizados tienen varios elementos esenciales, uno es el indicador de inicio y el otro es la longitud del paquete de datos. Según esto tcp
, los datos que desea se pueden analizar del flujo de datos en un canal. Consulte lo siguiente para detalles. El código del código, primero juzgue la longitud del paquete cuando se encuentre el bit de bandera. Si la longitud no es suficiente, escríbalo en el búfer y espere hasta la próxima lectura. Si la longitud es suficiente, analice los datos poco a poco y según el protocolo.
@Component
public class ProtocolDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) {
Protocol protocol = new Protocol();
int a = byteBuf.readableBytes();
if(byteBuf.readableBytes() >= Constant.headLength) {
int beginReader;
while (true) {
// 获取包头开始的index
beginReader = byteBuf.readerIndex();
// 标记包头开始的index
byteBuf.markReaderIndex();
if(messageStart(byteBuf)){
break;
}
if (byteBuf.readableBytes() < Constant.headLength) {
return;
}
}
byte method = byteBuf.readByte();
//设置方法
protocol.setMethod(MethodEnum.getByRequestCode(method));
int bagLength = byteBuf.readInt();
if (byteBuf.readableBytes() < bagLength - 7) {
// 还原读指针
byteBuf.readerIndex(beginReader);
return;
}
byteBuf.skipBytes(32);
//读取数据
int dataLength = byteBuf.readInt();
byte[] dataByte = new byte[dataLength];
byteBuf.readBytes(dataByte);
String dataString = new String(dataByte, StandardCharsets.UTF_8);
JSONObject data = JSONObject.parseObject(dataString);
if(method == MethodEnum.QUERY_FEE.getRequestCode() || method == MethodEnum.NOTIFY_PAY.getRequestCode()){
JSONArray jsonArray = data.getJSONArray("data");
JSONObject json = jsonArray.getJSONObject(0);
String flowNo = json.getString("inserialno");
protocol.setFlowNo(flowNo);
}
//设置数据
protocol.setData(data);
list.add(protocol);
}
}
private boolean messageStart(ByteBuf byteBuffer){
if(byteBuffer.readByte() == 0x24){
byteBuffer.markReaderIndex();
return byteBuffer.readByte() == 0x24;
}
return false;
}
}
2. Codificador
El codificador no tiene nada que decir, solo escribe los datos del protocolo poco a poco
public class ProtocolEncoder extends MessageToByteEncoder<Protocol> {
ParkingConfig parkingConfig;
public ProtocolEncoder(ParkingConfig config){
this.parkingConfig = config;
}
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Protocol protocol, ByteBuf out) throws Exception {
String data = protocol.getData().toJSONString();
int textLength = data.getBytes().length;
//包头标识
out.writeBytes(Constant.beginBytes);
//协议指令码
if(protocol.isUp()) {
out.writeByte(protocol.getMethod().getRequestCode());
}else{
out.writeByte(protocol.getMethod().getResponseCode());
}
//包长
out.writeInt(textLength + Constant.headLength);
//授权码
out.writeCharSequence(parkingConfig.getTcpKey(), StandardCharsets.UTF_8);
//文本长度
out.writeInt(textLength);
//文本内容
out.writeCharSequence(data, StandardCharsets.UTF_8);
//校验码
byte[] req = new byte[out.readableBytes()];
out.writeBytes(CrcUtils.setParamCRC(req));
//包尾标识
out.writeByte(0X0D);
out.writeByte(0X0D);
}
}
3.TCP-HTTP
El mayor problema es cómo hacer coincidir la solicitud y la respuesta y envolverlo como un HTTP
protocolo. Sabemos que HTTP
el protocolo se implementa en base al modelo de solicitud-respuesta. Para TCP
lograr esto en una conexión larga, los datos de respuesta deben tomarse del canal Salga y regrese al hilo solicitante, y debe poder corresponder a la solicitud. Como se mencionó anteriormente, el sistema tripartito no proporciona una ID de solicitud, pero podemos generar manualmente una ID de solicitud basada en los datos del protocolo existente. En primer lugar, existe una correspondencia uno a uno entre los códigos de protocolo de enlace ascendente y descendente del protocolo de terceros. En términos simples, el código de protocolo de enlace ascendente de la tarifa de verificación es 0x00
, luego el código de protocolo de enlace descendente es 0x01
, esta correspondencia no cambiará, definimos una enumeración de protocolo, en esta enumeración Agregue los códigos de protocolo de enlace ascendente y descendente en y proporcione el método para obtener el protocolo de acuerdo con el código de protocolo. Al mismo tiempo, el elemento de placa de matrícula es indispensable en el negocio del estacionamiento. Después de establecer el límite de que la misma placa de matrícula solo se puede estacionar en un depósito, podemos usar el código de protocolo + placa de matrícula para identificar la solicitud y hacer coincidir la datos de respuesta a los datos de solicitud. el código se muestra a continuación
public class RequestBlockUtils {
private static ConcurrentHashMap<String, Protocol> responseMap = new ConcurrentHashMap<>();
private static ConcurrentHashMap<String, String> requestMap = new ConcurrentHashMap<>();
private static Lock lock = new ReentrantLock();
public static int queueRemain(){
return requestMap.size();
}
public static void putRequest(Protocol requestProtocol){
String requestUid = getRequestIdByRequest(requestProtocol);
String responseUid = getResponseIdByRequest(requestProtocol);
requestMap.put(requestUid, responseUid);
}
public static void putResponse(Protocol responseProtocol){
String requestId = getRequestIdByResponse(responseProtocol);
lock.lock();
if(requestMap.containsKey(requestId)) {
String responseId = getResponseIdByResponse(responseProtocol);
responseMap.put(responseId, responseProtocol);
}
lock.unlock();
}
public static Protocol pullResponseWithTimeOut(Protocol requestProtocol, int timeOut){
String requestUid = getRequestIdByRequest(requestProtocol);
if(!requestMap.containsKey(requestUid)){
return null;
}
String responseUid = requestMap.get(requestUid);
long startTime = new Date().getTime();
while(true){
if(new Date().getTime() - startTime >= timeOut * 1000 || responseMap.containsKey(responseUid)){
break;
}
}
lock.lock();
if(responseMap.containsKey(responseUid)){
return responseMap.remove(responseUid);
}
requestMap.remove(requestUid);
lock.unlock();
throw new RuntimeException("获取响应结果超时param:" + requestProtocol.getData().toJSONString());
}
private static String getRequestIdByRequest(Protocol requestProtocol){
return requestProtocol.getFlowNo() + "$Method$" + requestProtocol.getMethod().getRequestCode();
}
private static String getResponseIdByRequest(Protocol requestProtocol){
return requestProtocol.getFlowNo() + "$Method$" + requestProtocol.getMethod().getResponseCode();
}
private static String getRequestIdByResponse(Protocol responseProtocol){
return responseProtocol.getFlowNo() + "$Method$" + responseProtocol.getMethod().getRequestCode();
}
private static String getResponseIdByResponse(Protocol responseProtocol){
return responseProtocol.getFlowNo() + "$Method$" + responseProtocol.getMethod().getResponseCode();
}
}
Protocol.flowNo
La matrícula se coloca aquí y usamos dos Map
para guardar el mapeo de la relación de solicitud y respuesta. Al iniciar una solicitud, primero genere un mapa de identificación de solicitud-respuesta de acuerdo con el código de protocolo de enlace ascendente y descendente del protocolo + placa y colóquelo en el Mapa y llámelo y pullResponseWithTimeOut
bloquéelo por un período de tiempo para intentar obtener el correspondiente. hay datos de respuesta en el canal, la ID correspondiente se generará de acuerdo con los datos de respuesta y se enviará a Si el resultado se coloca map
en el pullResponseWithTimeOut
método, devolverá datos si no se ha agotado el tiempo. Si se ha agotado el tiempo, una excepción se lanzará directamente Tenga en cuenta que se requiere un bloqueo aquí, de lo contrario, puede haber un problema de que los datos de respuesta agotados no se borran.
4. Agregue una función de reintento
Dichos proyectos de terceros generalmente se implementan en la sala de computadoras. Las fluctuaciones de la red son normales. Es imposible reiniciar el servicio cada vez. Por lo tanto, haremos una función simple de reconexión automática a corto plazo aquí.
@Component
public class NettyClient {
private static final Logger logger = LoggerFactory.getLogger(NettyClient.class);
@Autowired
private ParkingConfig parkingConfig;
@Autowired
private WjlService wjlService;
@Autowired
private Store store;
private Channel channel;
Bootstrap b;
private boolean start = false;
public void start() throws Exception {
if(start){
throw new RuntimeException("cannot start client that has started");
}
start = true;
logger.info("开始初始化客户端");
NioEventLoopGroup group = new NioEventLoopGroup();
b = new Bootstrap();
b.group(group) // 注册线程池
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(parkingConfig.getTcpIp(), parkingConfig.getTcpPort()))
.handler(new LoggingHandler(LogLevel.INFO))
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(120, 0, 0, TimeUnit.SECONDS));
pipeline.addLast("decoder", new ProtocolDecoder());
pipeline.addLast("encoder", new ProtocolEncoder(parkingConfig));
pipeline.addLast(new MethodHandler(parkingConfig, wjlService, store));
pipeline.addLast(new ExceptionHandler());
pipeline.addLast(new HeartHandler());
}
});
logger.info("客户端初始化初始化完成");
connect();
}
public Protocol request(Protocol requestProtocol){
logger.info("接收到第三方请求数据:{},目前待处理请求数量:{}", requestProtocol.getData(), RequestBlockUtils.queueRemain());
RequestBlockUtils.putRequest(requestProtocol);
channel.writeAndFlush(requestProtocol);
return RequestBlockUtils.pullResponseWithTimeOut(requestProtocol, 5);
}
private void connect() throws InterruptedException {
if(channel != null && channel.isActive()){
return;
}
ChannelFuture cf;
while(true) {
try {
logger.info("开始连接服务器....");
cf = b.connect().sync();
if(cf.isSuccess()){
channel = cf.channel();
break;
}
}catch (Exception e){
logger.info("服务器连接失败,reason:{},将再十秒后开始重连接", e.getMessage());
Thread.sleep(10000);
}
}
logger.info("服务器连接成功.......");
cf.channel().closeFuture().sync();
}
class HeartHandler extends ChannelInboundHandlerAdapter{
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
System.out.println("120秒未收到服务端心跳--------------");
if (evt instanceof IdleStateEvent){
channel.close().sync();
}else {
super.userEventTriggered(ctx,evt);
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
ctx.channel().close().sync();
NettyClient.this.connect();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.error(cause.getMessage());
}
}
}