草庐IT

netty对多协议进行编解码

瞿亮 2023-04-19 原文
1、netty如何解析多协议

前提:

项目地址:https://gitee.com/q529075990qqcom/NB-IOT.git

我们需要一个创建mavne项目,这个项目是我已经写好的项目,项目结构图如下:

 

 

 

创建公共模块

创建子模块,准备好依赖Netty4.1版本

<dependencies>
    <dependency>
      <groupId>io.netty</groupId>
      <artifactId>netty-all</artifactId>
      <version>4.1.72.Final</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.28</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-simple</artifactId>
      <version>1.7.28</version>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>com.esotericsoftware</groupId>
      <artifactId>kryo</artifactId>
      <version>5.3.0</version>
    </dependency>
  </dependencies>
maven依赖

 

序列化的定义是:将一个对象编码成一个字节流(I/O);而与之相反的操作被称为反序列化。

package serializer;

/**
 * @description:
 * @author: quliang
 * @create: 2022-10-20 15:16
 **/
public interface Serializer {
    /**
     * 序列化
     *
     * @param obj
     * @return
     * @throws Exception
     */
    byte[] serialize(Object obj) throws Exception;

    /**
     * 反序列化
     *
     * @param bytes
     * @param clazz
     * @param <T>
     * @return
     * @throws Exception
     */
    <T> T deserialize(byte[] bytes, Class<T> clazz) throws Exception;

}
自定义序列化接口
package serializer;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy;
import org.objenesis.strategy.StdInstantiatorStrategy;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
 * @description:
 * @author: quliang
 * @create: 2022-10-20 15:18
 **/

public class KryoSerializer implements Serializer {
    private static final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {
        Kryo kryo = new Kryo();
        kryo.setReferences(true);
        kryo.setRegistrationRequired(false);
        ((DefaultInstantiatorStrategy) kryo.getInstantiatorStrategy())
                .setFallbackInstantiatorStrategy(new StdInstantiatorStrategy());
        return kryo;
    });

    @Override
    public byte[] serialize(Object obj) throws Exception {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            Output output = new Output(baos);
            Kryo kryo = kryoThreadLocal.get();
            kryo.writeObject(output, obj);
            kryoThreadLocal.remove();
            return output.toBytes();
        } catch (IOException e) {
            throw new Exception("序列化失败", e);
        }
    }

    @Override
    public <T> T deserialize(byte[] bytes, Class<T> clazz) throws Exception {
        try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes)) {
            Input input = new Input(bais);
            Kryo kryo = kryoThreadLocal.get();
            Object obj = kryo.readObject(input, clazz);
            kryoThreadLocal.remove();
            return clazz.cast(obj);
        } catch (IOException e) {
            throw new Exception("反序化失败");
        }
    }
}
Kryo实现序列化接口

 

我们需要解析两种协议,那我们就要提前定义好两种协议,分别是消息协议、登录协议

 

消息协议相关

package protocol.msg;

import lombok.Data;
import lombok.Getter;

/**
 * @description: 消息协议: |magic|version|data|
 * @author: quliang
 * @create: 2022-12-10 20:46
 **/
@Data
public class MsgProtocol {
    @Getter
    private byte magic=0;
    @Getter
    private byte version=1;
}
消息协议基类
package protocol.msg.request;

import lombok.Data;
import protocol.msg.MsgProtocol;

/**
 * @description:
 * @author: quliang
 * @create: 2022-12-10 20:58
 **/
@Data
public class MsgRequest extends MsgProtocol {
    private String msg;
}
消息请求子类
package protocol.msg.response;

import lombok.Data;
import protocol.msg.MsgProtocol;

/**
 * @description:
 * @author: quliang
 * @create: 2022-12-10 20:41
 **/
@Data
public class MsgResponse extends MsgProtocol {
    private int statCode;
}
消息响应子类
package encoder;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import protocol.msg.MsgProtocol;
import serializer.KryoSerializer;

/**
 * @description:
 * @author: quliang
 * @create: 2022-12-10 20:53
 **/

public class MsgEncoder extends MessageToByteEncoder<MsgProtocol> {

    @Override
    protected void encode(ChannelHandlerContext ctx, MsgProtocol msgProtocol, ByteBuf in) throws Exception {
        in.writeByte(msgProtocol.getMagic());
//        in.writeByte(msgProtocol.code());
        in.writeByte(msgProtocol.getVersion());
        byte[] data = new KryoSerializer().serialize(msgProtocol);
        in.writeShort(data.length);
        in.writeBytes(data);
    }
}
消息协议编码
package decoder;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import lombok.extern.slf4j.Slf4j;
import protocol.msg.MsgProtocol;
import serializer.KryoSerializer;

import java.util.List;

/**
 * @description:
 * @author: quliang
 * @create: 2022-12-10 20:52
 **/
@Slf4j
public class MsgDecoder extends ByteToMessageDecoder {

    private Class<MsgProtocol> msgClass;

    public MsgDecoder(Class clazz) {
        this.msgClass = clazz;
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        try {
            byte magic = in.readByte();
            byte version = in.readByte();

            short dataSize = in.readShort();
            byte[] data = new byte[dataSize];
            in.readBytes(data);

            MsgProtocol baseProtocol = new KryoSerializer().deserialize(data, msgClass);
            out.add(baseProtocol);
        } catch (Exception e) {
            //如果解码错误,将数据传递到下一个解码器中
            log.error("msg decoder {}",e.getMessage());
            // 重置读取字节索引,因为上边已经读了(readBytes),不加这个会导致数据为空
            in.resetReaderIndex();
            // 这里是复制流,复制一份,防止skipBytes跳过,导致传递的消息变成空;
            ByteBuf buff = in.retainedDuplicate();
            //原因是netty不允许有字节内容不读的情况发生,所以采用下边的方法解决。
            in.skipBytes(in.readableBytes());
            //继续传递到下一个解码器中
            out.add(buff);
        }
    }
}
消息协议解码

 

登录协议相关

package protocol.system;



import lombok.Getter;

/**
 * @description: 登录协议: |magic|version|code|data|
 * @author: quliang
 * @create: 2022-12-09 18:10
 **/

public class LoginProtocol {
    @Getter
    private byte magic=0;
    @Getter
    private byte version=1;
    @Getter
    public byte code;
}
登录协议基类
package protocol.system.request;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import protocol.system.LoginProtocol;

/**
 * @description:
 * @author: quliang
 * @create: 2022-12-06 18:17
 **/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginRequest extends LoginProtocol {

    private String userId;

    private String userName;
}
登录请求子类
package protocol.system.response;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import protocol.system.LoginProtocol;

/**
 * @description:
 * @author: quliang
 * @create: 2022-12-06 18:22
 **/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginResponse extends LoginProtocol {

    private String msg;

    private String data;

}
登录响应子类
package encoder;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import protocol.system.LoginProtocol;
import serializer.KryoSerializer;

/**
 * @description:
 * @author: quliang
 * @create: 2022-12-06 22:11
 **/

public class LoginEncoder extends MessageToByteEncoder<LoginProtocol> {

    @Override
    protected void encode(ChannelHandlerContext ctx, LoginProtocol baseProtocol, ByteBuf in) throws Exception {
        in.writeByte(baseProtocol.getMagic());
        in.writeByte(baseProtocol.getCode());
        in.writeByte(baseProtocol.getVersion());
        byte[] data = new KryoSerializer().serialize(baseProtocol);
        in.writeShort(data.length);
        in.writeBytes(data);
    }
}
登录协议编码
package decoder;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import lombok.extern.slf4j.Slf4j;
import protocol.system.LoginProtocol;
import serializer.KryoSerializer;
import java.util.List;

/**
 * @description:
 * @author: quliang
 * @create: 2022-12-06 17:59
 **/
@Slf4j
public class LoginDecoder extends ByteToMessageDecoder {

    private Class<LoginProtocol> clazz;

    public LoginDecoder(Class clazz) {
        this.clazz = clazz;
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        try {
            byte magic = in.readByte();
            byte code = in.readByte();
            byte version = in.readByte();

            short dataSize = in.readShort();
            byte[] data = new byte[dataSize];
            in.readBytes(data);

            LoginProtocol baseProtocol = new KryoSerializer().deserialize(data, clazz);
            out.add(baseProtocol);
        } catch (Exception e) {
            //如果解码错误,将数据传递到下一个解码器中
            log.error("login decoder {}", e.getMessage());
            // 重置读取字节索引,因为上边已经读了(readBytes),不加这个会导致数据为空
            in.resetReaderIndex();
            // 这里是复制流,复制一份,防止skipBytes跳过,导致传递的消息变成空;
            ByteBuf buff = in.retainedDuplicate();
            //原因是netty不允许有字节内容不读的情况发生,所以采用下边的方法解决。
            in.skipBytes(in.readableBytes());
            //继续传递到下一个解码器中
            out.add(buff);
        }
    }
}
登录协议解码

 这样公共模块就创建完成了

创建服务端

package com.ql;

import com.ql.handler.MsgHandler;
import decoder.LoginDecoder;
import decoder.MsgDecoder;
import com.ql.handler.LoginHandler;
import encoder.LoginEncoder;
import encoder.MsgEncoder;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import protocol.system.request.LoginRequest;
import protocol.msg.request.MsgRequest;

/**
 * @author quliang
 * @description 服务端
 * @date 2022-12-06 17:39:14
 */
@Slf4j
public class IotServer {

    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap().group(
                    bossGroup, workGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel channel) throws Exception {
                            ChannelPipeline pipeline = channel.pipeline();
                            pipeline.addLast(new LoggingHandler(LogLevel.INFO));
                            /**
                             * 心跳机制
                             */
                            //pipeline.addLast(new IdleStateHandler(5, 10, 5, TimeUnit.SECONDS));

                            /**
                             * 消息、登录解码器
                             */
                            pipeline.addLast(new LoginDecoder(LoginRequest.class));
                            pipeline.addLast(new MsgDecoder(MsgRequest.class));

                            /**
                             * 消息、登录处理器
                             */
                            pipeline.addLast(new MsgHandler());
                            pipeline.addLast(new LoginHandler());

                            /**
                             * 消息、登录编码器
                             */
                            pipeline.addLast(new MsgEncoder());
                            pipeline.addLast(new LoginEncoder());

                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 1024);

            ChannelFuture cf = bootstrap.bind(8849).sync();
            log.info("socket服务端启动成功 {}", cf.channel().localAddress().toString());

            cf.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }

    }
}
服务端代码
package com.ql.handler;

import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import protocol.msg.request.MsgRequest;
import protocol.msg.response.MsgResponse;

/**
 * @description: 消息处理器
 * @author: quliang
 * @create: 2022-12-10 20:57
 **/
@Slf4j
@ChannelHandler.Sharable
public class MsgHandler extends SimpleChannelInboundHandler<MsgRequest> {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("上线{}", ctx.channel().remoteAddress().toString());
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MsgRequest request) throws Exception {
        log.info("服务端读取消息体数据为{}", request.toString());
        MsgResponse response = new MsgResponse();
        response.setStatCode(200);
        ctx.channel().writeAndFlush(response);
    }
}
服务端消息处理器
package com.ql.handler;

import io.netty.channel.*;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
import protocol.system.request.LoginRequest;
import protocol.system.response.LoginResponse;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @description: 登录处理器
 * @author: quliang
 * @create: 2022-12-06 18:14
 **/
@Slf4j
@ChannelHandler.Sharable
public class LoginHandler extends SimpleChannelInboundHandler<LoginRequest>{
    private static AtomicInteger READER_COUNT = new AtomicInteger(0);

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("服务端:{} 通道开启!", ctx.channel().localAddress().toString());
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        log.info("服务端: {} 通道关闭!", ctx.channel().localAddress().toString());
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, LoginRequest loginRequest) throws Exception {
        log.info("读取数据 {} ", loginRequest.toString());
        LoginResponse response= new LoginResponse("success", null);
        ctx.channel().writeAndFlush(response);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        log.info("...............数据接收-完毕...............");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
        log.error("...............业务处理异常...............{}", cause);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            Channel channel = ctx.channel();
            switch (event.state()) {
                case READER_IDLE:
                    log.info("读空闲");
                    READER_COUNT.addAndGet(1);
                    break;
                case WRITER_IDLE:
                    log.info("写空闲");
                    break;
                default:
                    break;
            }
            ctx.disconnect();
            if (READER_COUNT.get() > 3) {
                log.info("close this channel {}", channel.remoteAddress().toString());
            }
        }
    }

}
服务端登录处理器

 

服务端其实很多都是直接引用公共模块的,代码也并不复杂

创建消息客户端

package com.ql;

import com.ql.handler.ClientMsgHandler;
import decoder.MsgDecoder;
import encoder.MsgEncoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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 io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import protocol.msg.response.MsgResponse;

import java.net.InetSocketAddress;

/**
 * @author quliang
 * @description 客户端
 * @date 2022-12-06 17:37:56
 */
@Slf4j
public class IotClientMsg {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup clientGroup = new NioEventLoopGroup();
        try {
            Bootstrap bs = new Bootstrap();
            bs.group(clientGroup)
                    .channel(NioSocketChannel.class)
                    .remoteAddress(new InetSocketAddress("169.254.190.154", 8849))
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new LoggingHandler(LogLevel.INFO));

                            //消息解码器
                            pipeline.addLast(new MsgDecoder(MsgResponse.class));
                            //客户端消息处理器
                            pipeline.addLast(new ClientMsgHandler());
                            //消息编码器
                            pipeline.addLast(new MsgEncoder());
                        }
                    });

            ChannelFuture cf = bs.connect().sync();
            log.info("启动成功{}", cf.channel().localAddress().toString());

//            Scanner scanner = new Scanner(System.in);

            cf.channel().closeFuture().sync();
        } finally {
            clientGroup.shutdownGracefully().sync();
        }
    }
}
客户端代码
package com.ql.handler;

import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import protocol.msg.request.MsgRequest;
import protocol.msg.response.MsgResponse;

/**
 * @description:
 * @author: quliang
 * @create: 2022-12-10 20:40
 **/
@Slf4j
@ChannelHandler.Sharable
public class ClientMsgHandler extends SimpleChannelInboundHandler<MsgResponse> {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        MsgRequest request = new MsgRequest();
        request.setMsg("hello");
        ctx.channel().writeAndFlush(request);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MsgResponse response) throws Exception {
        int code = response.getStatCode();
        log.info("消息处理器读取响应对象数据为{}", code);
    }
}
客户端消息处理器

 

 消息客户端代码也并不复杂

创建登录客户端

package com.ql;

import com.ql.handler.ClientLoginHandler;
import decoder.LoginDecoder;
import encoder.LoginEncoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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 io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import protocol.system.response.LoginResponse;

import java.net.InetSocketAddress;

/**
 * @author quliang
 * @description 客户端
 * @date 2022-12-06 17:37:56
 */
@Slf4j
public class IotClientLogin {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup clientGroup = new NioEventLoopGroup();
        try {
            Bootstrap bs = new Bootstrap();
            bs.group(clientGroup)
                    .channel(NioSocketChannel.class)
                    .remoteAddress(new InetSocketAddress("169.254.190.154", 8849))
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new LoggingHandler(LogLevel.INFO));

                            pipeline.addLast(new LoginDecoder(LoginResponse.class));
                            //pipeline.addLast(new MsgDecoder(MsgResponse.class));

                            //pipeline.addLast(new ClientMsgHandler());
                            pipeline.addLast(new ClientLoginHandler());

                            //pipeline.addLast(new MsgEncoder());
                            pipeline.addLast(new LoginEncoder());
                        }
                    });

            ChannelFuture cf = bs.connect().sync();
            log.info("启动成功{}", cf.channel().localAddress().toString());

//            Scanner scanner = new Scanner(System.in);

            cf.channel().closeFuture().sync();
        } finally {
            clientGroup.shutdownGracefully().sync();
        }
    }
}
客户端代码
package com.ql.handler;

import io.netty.channel.*;
import lombok.extern.slf4j.Slf4j;
import protocol.system.request.LoginRequest;
import protocol.system.response.LoginResponse;

import java.util.Scanner;

/**
 * @description:
 * @author: quliang
 * @create: 2022-12-06 22:16
 **/
@Slf4j
@ChannelHandler.Sharable
public class ClientLoginHandler extends SimpleChannelInboundHandler<LoginResponse>  {

    private Scanner scanner = new Scanner(System.in);

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("客户端:{} 通道开启!", ctx.channel().localAddress().toString());
        login(ctx);
    }

    /**
     * 登录方法
     * @param ctx
     */
    private void login(ChannelHandlerContext ctx) {
        LoginRequest request = new LoginRequest("123", "123");
        ctx.channel().writeAndFlush(request);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        log.info("客户端: {} 读取数据 {}", ctx.channel().localAddress().toString(), msg.toString());
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, LoginResponse response) throws Exception {
        log.info("客户端: {} 读取数据 {}", ctx.channel().localAddress().toString(), response.toString());
        String msg = response.getMsg();
        log.info("========{}", msg);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        log.info("...............数据接收-完毕...............");
    }
}
客户端登录处理器

 

 

 

 

我们是怎么通过这个项目来实现不同协议编解码?

其实也不难,我们仔细看MsgDecoder、LoginDecoder两个类其中一个类的代码,其中有个巧妙的操作就是使用try-catch,

只要解码器无法解码发生异常,就重置读取字节索引传递到下一个解码器中,直到传递到正确解码器中。不过为了兼容多种协议,

解码异常也会让服务端性能有所下降的,取舍之间必有得失。

 

 

 

有关netty对多协议进行编解码的更多相关文章

  1. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  2. ruby-on-rails - 按天对 Mongoid 对象进行分组 - 2

    在控制台中反复尝试之后,我想到了这种方法,可以按发生日期对类似activerecord的(Mongoid)对象进行分组。我不确定这是完成此任务的最佳方法,但它确实有效。有没有人有更好的建议,或者这是一个很好的方法?#eventsisanarrayofactiverecord-likeobjectsthatincludeatimeattributeevents.map{|event|#converteventsarrayintoanarrayofhasheswiththedayofthemonthandtheevent{:number=>event.time.day,:event=>ev

  3. ruby - 使用 C 扩展开发 ruby​​gem 时,如何使用 Rspec 在本地进行测试? - 2

    我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当

  4. ruby - 如何进行排列以有效地定制输出 - 2

    这是一道面试题,我没有答对,但还是很好奇怎么解。你有N个人的大家庭,分别是1,2,3,...,N岁。你想给你的大家庭拍张照片。所有的家庭成员都排成一排。“我是家里的friend,建议家庭成员安排如下:”1岁的家庭成员坐在这一排的最左边。每两个坐在一起的家庭成员的年龄相差不得超过2岁。输入:整数N,1≤N≤55。输出:摄影师可以拍摄的照片数量。示例->输入:4,输出:4符合条件的数组:[1,2,3,4][1,2,4,3][1,3,2,4][1,3,4,2]另一个例子:输入:5输出:6符合条件的数组:[1,2,3,4,5][1,2,3,5,4][1,2,4,3,5][1,2,4,5,3][

  5. ruby - 即使失败也继续进行多主机测试 - 2

    我已经构建了一些serverspec代码来在多个主机上运行一组测试。问题是当任何测试失败时,测试会在当前主机停止。即使测试失败,我也希望它继续在所有主机上运行。Rakefile:namespace:specdotask:all=>hosts.map{|h|'spec:'+h.split('.')[0]}hosts.eachdo|host|begindesc"Runserverspecto#{host}"RSpec::Core::RakeTask.new(host)do|t|ENV['TARGET_HOST']=hostt.pattern="spec/cfengine3/*_spec.r

  6. ruby - 是否可以覆盖 gemfile 进行本地开发? - 2

    我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI

  7. ruby - 在 Windows 机器上使用 Ruby 进行开发是否会适得其反? - 2

    这似乎非常适得其反,因为太多的gem会在window上破裂。我一直在处理很多mysql和ruby​​-mysqlgem问题(gem本身发生段错误,一个名为UnixSocket的类显然在Windows机器上不能正常工作,等等)。我只是在浪费时间吗?我应该转向不同的脚本语言吗? 最佳答案 我在Windows上使用Ruby的经验很少,但是当我开始使用Ruby时,我是在Windows上,我的总体印象是它不是Windows原生系统。因此,在主要使用Windows多年之后,开始使用Ruby促使我切换回原来的系统Unix,这次是Linux。Rub

  8. CAN协议的学习与理解 - 2

    最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总

  9. ruby - 捕获 Ruby Logger 输出以进行测试 - 2

    我有一个像这样的ruby​​类:require'logger'classTdefdo_somethinglog=Logger.new(STDERR)log.info("Hereisaninfomessage")endend测试脚本行如下:#!/usr/bin/envrubygem"minitest"require'minitest/autorun'require_relative't'classTestMailProcessorClasses当我运行这个测试时,out和err都是空字符串。我看到消息打印在stderr上(在终端上)。有没有办法让Logger和capture_io一起玩得

  10. ruby - 按数字(从大到大)然后按字母(字母顺序)对对象集合进行排序 - 2

    我正在构建一个小部件来显示奥运会的奖牌数。我有一个“国家”对象的集合,其中每个对象都有一个“名称”属性,以及奖牌计数的“金”、“银”、“铜”。列表应该排序:1.首先是奖牌总数2.如果奖牌相同,按类型分割(金>银>铜,即2金>1金+1银)3.如果奖牌和类型相同,则按字母顺序子排序我正在用ruby​​做这件事,但我想语言并不重要。我确实找到了一个解决方案,但如果感觉必须有更优雅的方法来实现它。这是我做的:使用加权奖牌总数创建一个虚拟属性。因此,如果他们有2个金牌和1个银牌,加权总数将为“3.020100”。1金1银1铜为“3.010101”由于我们希望将奖牌数排序为最高的,因此列表按降序排

随机推荐