Netty combat: cómo conectar el protocolo tcp de terceros en un proyecto web

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, erppor 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 tcpal lograrlos, es un protocolo full-duplex. El problema es que somos un tercero para llamar, y todo nuestro negocio se Httpimplementa en base a las respuestas de las solicitudes. Sin embargo, cambiar tcpa 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 tcpes difícil de mantener mediante la introducción de protocolos, simplemente podemos construir un nuevo sistema, empaquetarlo en un httpprotocolo 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 nettyy springbootpara aceptar los datos de entrada y salida de la fábrica de automóviles de destino y convertirlos en una httpllamada 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 nettylo uso. He leído un nettylibro 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 tcpprotocolos 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 HTTPprotocolo. Sabemos que HTTPel protocolo se implementa en base al modelo de solicitud-respuesta. Para TCPlograr 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.flowNoLa matrícula se coloca aquí y usamos dos Mappara 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 pullResponseWithTimeOutbloqué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 mapen el pullResponseWithTimeOutmé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());
        }
    }
}

Supongo que te gusta

Origin blog.csdn.net/qq_35488769/article/details/105740057
Recomendado
Clasificación