澳门凯旋门官方网站

因为澳门凯旋门官网充满了多种多样的游戏,所以澳门凯旋门官网被评为世界高科技高成长的娱乐平台,享受你一定会有很大的收获的,因为从来都不掉线。

快速入门,不再需要电脑软件来写入GPS坐标

以前我们想要在地理照片上写入经纬度通常需要借助于电脑软件来完成数据的定位,但是有了ATP公司的迷你PhotoFinder我们就能直接进行处理了,把地理数据直接写入你的存储卡。和其他几款类似产品一样,PhotoFinder会通过SiRF Star III芯片记录下GPS时间戳数据,当你将完成的图片存好后,图片会带有一个EXIF时间戳标签。看起来这只是一个依赖于第三方软件来实现图片运用处理的简单系统,同时能当个读卡器用。我们觉得应该再多一些功能。目前价格还不清楚,如果价格合理应该还是有一定诱惑力的。[Via Photography Blog, thanks Mark]

文章版权由作者李晓晖和博客园共有,若转载请于明显处标明出处:

文章版权由作者李晓晖和博客园共有,若转载请于明显处标明出处:

  • 案例代码下载
  • 初学者的话推荐直接套用 all-in-one 的 jar 包,若熟悉 Netty 可以根据需求添加不同的 jar 包

1. 前言

GIS代码进行更新后,由于用户前端已有缓存,导致更新的功能不能被及时同步。为避免前端请求读取缓存,常见方法是在每一个请求后面加上一个随机生成的变量参数,这样可以保证每个请求都不会跟历史请求重复。但是,这样处理是不合理的,我们虽然避免了读取缓存,但是却会导致系统效率降低。

所以,我们要解决的问题应该是:只有当代码更新后,客户前端第一次触发的所有请求都应该不走缓存,而之后,相同请求缓存继续有效。

1. 前言

GIS代码进行更新后,由于用户前端已有缓存,导致更新的功能不能被及时同步。为避免前端请求读取缓存,常见方法是在每一个请求后面加上一个随机生成的变量参数,这样可以保证每个请求都不会跟历史请求重复。但是,这样处理是不合理的,我们虽然避免了读取缓存,但是却会导致系统效率降低。

所以,我们要解决的问题应该是:只有当代码更新后,客户前端第一次触发的所有请求都应该不走缓存,而之后,相同请求缓存继续有效。

2.解决思路

核心思想为,在GIS的每次请求后面带上一个version参数,每次更新后version参数的值均发生变化,于是该version对应的任何请求,第一次均会重新从服务端读取最新数据,但是之后的请求由于version不再变化,缓存继续有效。

所以这里我们实际需要解决的问题变为了,如何能够自动化生成更新version。

2.解决思路

核心思想为,在GIS的每次请求后面带上一个version参数,每次更新后version参数的值均发生变化,于是该version对应的任何请求,第一次均会重新从服务端读取最新数据,但是之后的请求由于version不再变化,缓存继续有效。

所以这里我们实际需要解决的问题变为了,如何能够自动化生成更新version。

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.22.Final</version>
</dependency>

3.实现方法

此方案主要针对前端version,所以我们要解决如何能够让该version自动赋值到前端JS代码中,而不是每次我们自己手动给一个version值。由于每次前端更新后,均需要使用ant将代码进行再次编译,所以我们的实现方法为:

a.在进行ant编译时生成时间戳变量,再将该变量直接写入到待打包的JS代码中。

图片 1

b.前端所有JS代码获取时加上version变量参数。

3.实现方法

此方案主要针对前端version,所以我们要解决如何能够让该version自动赋值到前端JS代码中,而不是每次我们自己手动给一个version值。由于每次前端更新后,均需要使用ant将代码进行再次编译,所以我们的实现方法为:

a.在进行ant编译时生成时间戳变量,再将该变量直接写入到待打包的JS代码中。

图片 2

b.前端所有JS代码获取时加上version变量参数。

TIME 协议(服务器)

目标:编写一个 TIME 协议,服务器端在接收到客户端的连接时会向客户端发送一个 32 位的时间戳,并且一旦消息发送成功就会立即关闭

4.补充一点:如果是数据更新了怎么办?

首先,我们将数据分为两种,一种是我们自己GIS业务库中的配置数据,一种是地理服务器中的数据(包括第三方的地理服务器)。如果这两种数据均有更新,我们如何做到前端及时同步?

4.补充一点:如果是数据更新了怎么办?

首先,我们将数据分为两种,一种是我们自己GIS业务库中的配置数据,一种是地理服务器中的数据(包括第三方的地理服务器)。如果这两种数据均有更新,我们如何做到前端及时同步?

编写 Handler

  • 首先编写 Handler(处理器),继承 ChannelInboundHandlerAdapter 类(该类默认将事件自动传播到下一个入站处理器)并重写其两个事件方法:
    • channelActive():Channel激活,当有客户端连接时触发
    • exceptionCaught():捕获到异常时触发
public class TimeServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(final ChannelHandlerContext ctx) {
        final ByteBuf time = ctx.alloc().buffer(4);
        time.writeInt((int) (System.currentTimeMillis() / 1000L   2208988800L));
        final ChannelFuture f = ctx.writeAndFlush(time);
        f.addListener(new ChannelFutureListener() {
            public void operationComplete(ChannelFuture future) {
                assert f == future;
                ctx.close();
            }
        });
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}
  • 代码分析:

    1. 我们需要写入一个 32位 的时间戳,因此需要一个至少有 4 个字节的 ByteBuf,通过 ChannelHandlerContext.alloc() 得到一个当前的 ByteBufAllocator,然后分配一个新的缓冲
    2. 通过 ByteBuf 的 write() 方法写入时间戳
    3. 通过 ChannelHandlerContext 对象的 writeAndFlush() 方法将字节容器的数据写入缓冲区并刷新,它会返回一个 ChannelFuture 对象
    4. 因为 Netty 的操作都是异步的,例如下面代码中的消息在被发送之前可能会被先关闭连接
    Channel ch = ...;
    ch.writeAndFlush(message);
    ch.close();
    
    1. 因此 close() 方法需要在数据通过 write() 发送到客户端之后在调用,因此为 ChannelFuture 增加一个 ChannelFutureListener 来监听操作完成事件,并关闭 Channel
    2. 也可以使用简单的预定义监听代码
    f.addListener(ChannelFutureListener.CLOSE);
    
  • ctx.write(Object) 方法不会使消息写入到通道上,它被缓冲在了内部,你需要调用 ctx.flush() 方法来把缓冲区中数据强行输出。或者你可以用更简洁的 cxt.writeAndFlush(msg) 以达到同样的目的

4.1GIS业务配置库中的配置数据读取

先抛出解决方案:同样,所有数据类请求加上时间戳,让数据类请求均不走前端缓存。

但是,不走前端缓存并不代表不走后端缓存,而这里则是我们已经或者还需进一步优化的地方:业务库中的GIS基本配置项都会在业务服务器启动时读取到内存中,所以如果配置数据做了更新,传统方案上需要业务服务器重启才行,但是目前业务已经提供了数据重载的接口。

所以,当业务数据做了更新后,要么重启业务服务,要么在构建中点击数据重载(会加入到GIS构建中)。这样可以保证,所有的GIS业务配置类数据请求会进入到后台,但是后台中缓存的数据是最新数据,从而既保证数据最新又避免对数据库的压力。

4.1GIS业务配置库中的配置数据读取

先抛出解决方案:同样,所有数据类请求加上时间戳,让数据类请求均不走前端缓存。

但是,不走前端缓存并不代表不走后端缓存,而这里则是我们已经或者还需进一步优化的地方:业务库中的GIS基本配置项都会在业务服务器启动时读取到内存中,所以如果配置数据做了更新,传统方案上需要业务服务器重启才行,但是目前业务已经提供了数据重载的接口。

所以,当业务数据做了更新后,要么重启业务服务,要么在构建中点击数据重载(会加入到GIS构建中)。这样可以保证,所有的GIS业务配置类数据请求会进入到后台,但是后台中缓存的数据是最新数据,从而既保证数据最新又避免对数据库的压力。

编写服务器类

  • 服务端需要两个 NioEventLoopGroup,它本质是一个线程池:
    • bossGroup:处理客户端连接事件的线程池
    • workerGroup:处理连接后所有事件的线程池
  • ServerBootstrap 是服务端的辅助启动类,用于创建服务端
  • 指定连接该服务器的 Channel 类型为 NioServerSocketChannel
  • 通过 ChannelInitializer 辅助配置客户端连接生成的 Channel,指定需要执行的 Handler
  • 设置 EventLoopGroup 参数:
    • .option:用于设置 bossGroup 的相关参数
    • .childOption:用于设置workerGroup相关参数
  • 绑定端口,并调用 closeFuture() 方法返回此通道关闭时的 ChannelFuture,调用 ChannelFuture 的 sync() 阻塞方法直到服务端关闭链路之后才退出 main() 函数
public class Server {

    private int port;

    public Server(int port) {
        this.port = port;
    }

    public void run() {
        EventLoopGroup bossGroup = new NioEventLoopGroup();         // 处理客户端连接事件的线程池
        EventLoopGroup workerGroup = new NioEventLoopGroup();       // 处理连接后所有事件的线程池
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();      // NIO 服务的辅助启动类
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)          // 指定连接该服务器的 Channel 类型为 NioServerSocketChannel
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new TimeServerHandler());              // 指定需要执行的 Handler
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)          // 设置 bossGroup 的相关参数
                    .childOption(ChannelOption.SO_KEEPALIVE, true);         // 设置 workerGroup 相关参数

            ChannelFuture f = bootstrap.bind(port).sync();          // 绑定端口,调用 ChannelFuture 的 sync() 阻塞方法等待绑定完成
            // 调用 closeFuture() 方法返回此通道关闭时的 ChannelFuture
            // 调用 ChannelFuture 的 sync() 阻塞方法直到服务端关闭链路之后才退出 main() 函数
            f.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 优雅退出机制。。。退出线程池(该方法源码没读过,也不知怎么个优雅方式)
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }


    public static void main(String[] args) {
        int port = (args.length > 0) ? Integer.parseInt(args[0]) : 8080;
        new Server(port).run();
    }
}

4.2地理服务器中的数据更新

方案1:同样使用随机时间戳来确保每次请求均是最新的数据,此种方法比较简单通用。

方案2:将version概念引入,数据库中增加一个数据version配置,每次地理服务器有更新后对version进行修改,然后使用构建让业务服务器重读配置,前端请求GIS配置时获得数据version,在请求地理服务时带上该version。

建议先以方案1来进行,这样与4.1中的数据请求可以同步,代码上可以统一处理。如果要进行方案2,则需要工程知道地理服务器何时做了更新,然后再在配置中修改version,稍微增加了工程维护量。

4.2地理服务器中的数据更新

方案1:同样使用随机时间戳来确保每次请求均是最新的数据,此种方法比较简单通用。

方案2:将version概念引入,数据库中增加一个数据version配置,每次地理服务器有更新后对version进行修改,然后使用构建让业务服务器重读配置,前端请求GIS配置时获得数据version,在请求地理服务时带上该version。

建议先以方案1来进行,这样与4.1中的数据请求可以同步,代码上可以统一处理。如果要进行方案2,则需要工程知道地理服务器何时做了更新,然后再在配置中修改version,稍微增加了工程维护量。

TIME 协议(客户端)

目标:连接服务端并接收服务端发送的时间戳消息,输出到控制台

5.总结

5.总结

编写 Handler

public class TimeClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf m = (ByteBuf) msg; 
        try {
            long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
            System.out.println(new Date(currentTimeMillis));
            ctx.close();
        } finally {
            m.release();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

5.1为什么前端JS和后台数据不用统一的version确保更新

a.如果用统一version,则该version需要使用库中配置(或配置文件),但是JS文件的加载往往是在数据请求之前,如此无法保证在version获得之前的JS文件为最新文件。

b.数据的更新并不代表系统需要重新编译,所以针对数据的version无法和JS版本的version同步。

5.1为什么前端JS和后台数据不用统一的version确保更新

a.如果用统一version,则该version需要使用库中配置(或配置文件),但是JS文件的加载往往是在数据请求之前,如此无法保证在version获得之前的JS文件为最新文件。

b.数据的更新并不代表系统需要重新编译,所以针对数据的version无法和JS版本的version同步。

编写客户端类

  • 与服务端唯一不同的是使用 BootStrap 和 Channel 的实现,并调用 connect() 方法连接服务端
public class Client {
    public static void main(String[] args) throws Exception {
        String host = (args.length == 1) ? args[0] : "localhost";
        int port = (args.length == 2) ? Integer.parseInt(args[1]) : 8080;

        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(workerGroup);
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
                }
            });

            ChannelFuture f = bootstrap.connect(host, port).sync();     // 连接服务端,调用 ChannelFuture 的 sync() 阻塞方法等待连接完成
            // 调用 closeFuture() 方法返回此通道关闭时的 ChannelFuture
            // 调用 ChannelFuture 的 sync() 阻塞方法直到客户端关闭链路之后才退出 main() 函数
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}

5.2方案总结

a.前端JS使用ANT编译自动生成版本号。

b.数据请求加上随机时间戳。

 

                           -----欢迎转载,但保留版权,请于明显处标明出处:

                                                                              如果您觉得本文确实帮助了您,可以微信扫一扫,进行小额的打赏和鼓励,谢谢 ^_^

                                             图片 3

5.2方案总结

a.前端JS使用ANT编译自动生成版本号。

b.数据请求加上随机时间戳。

 

                           -----欢迎转载,但保留版权,请于明显处标明出处:

                                                                              如果您觉得本文确实帮助了您,可以微信扫一扫,进行小额的打赏和鼓励,谢谢 ^_^

                                             图片 4

处理基于流的传输

回到 TIME 客户端例子,服务端发送的数据是一个 32位 的时间戳,如果服务端发送了 16位 的数据呢,那客户端读取的数据就不准确了

解决方法一

  • 构造一个内部的缓冲,只有直到 4 个字节全部接收到内部缓冲,才进行处理
public class TimeClientHandler extends ChannelInboundHandlerAdapter {

    private ByteBuf byteBuf;

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        byteBuf = ctx.alloc().buffer(4);
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        byteBuf.release();
        byteBuf = null;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf m = (ByteBuf) msg;
        byteBuf.writeBytes(m);
        m.release();

        if (byteBuf.readableBytes() >= 4){
            long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
            System.out.println(new Date(currentTimeMillis));
            ctx.close();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}
  • 代码分析:
    • ChannelHandler 有 2 个生命周期监听方法:handlerAdded()、handlerRemoved() ,你可以完成任意初始化任务,只要它不会阻塞很长时间
    • 分配一个 4 个字节的字节容器,将读取的数据写入该字节容器
    • 判断容器中是否有足够的数据(4个字节),如果有在进行业务处理

解决方法二

  • 方法一虽然解决了问题但修改后的处理器并不简洁,可以把一整个 ChannelHandler 拆分成多个 ChannelHandler 以减少应用复杂度,多个 ChannelHandler 构成一个处理链
  • 因此可以将 TimeClientHandler 拆分成2个处理器:
    • TimeDecoder:解析数据
    • TimeClientHandler:处理业务,跟初始实现一样
  • Netty 提供了 ByteToMessageDecoder 可以帮助完成 TimeDecoder 的开发,ByteToMessageDecoder 是 ChannelInboundHandler 的一个实现类,它可以让数据解析变得更简单
public class TimeDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        if (byteBuf.readableBytes() < 4) {
            return;
        }
        list.add(byteBuf.readBytes(4));
    }
}
  • 代码分析:
    1. 每当有新数据接收时,ByteToMessageDecoder 都会调用 decode() 方法来处理字节容器 ByteBuf 对象
    2. 如果在 decode() 方法里增加一个对象到 list 对象里面,则意味着解码消息成功,ByteToMessageDecoder 将会丢弃在字节容器 ByteBuf 里已经被读过的数据
  • 修改 ChannelInitializer 将另外一个 ChannelHandler 加入到 ChannelPipeline
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ch.pipeline().addLast(new TimeDecoder(),new TimeClientHandler());
    }
});

用 POJO 代替 ByteBuf

之前例子使用 ByteBuf 作为协议消息的数据结构,目前读取的仅仅是一个 32 位 的数据,直接使用 ByteBuf 不是问题,然而在真实的协议中,数据量肯定不止如此,通过 ByteBuf 处理数据将变的复杂困难,因此下面介绍如何使用 POJO(普通 Java 对象) 代替 ByteBuf

  • 首先定义新类型 UnixTime
public class UnixTime {
    private final long value;

    public UnixTime() {
        this(System.currentTimeMillis() / 1000L   2208988800L);
    }

    public UnixTime(long value) {
        this.value = value;
    }

    public long value() {
        return value;
    }

    @Override
    public String toString() {
        return new Date((value() - 2208988800L) * 1000L).toString();
    }
}
  • 修改 TimeDecoder 类,返回一个 UnixTime 以代替 ByteBuf
protected void decode(ChannelHandlerContext channelHandlerContext,ByteBuf byteBuf, List<Object> list) throws Exception {
    if (byteBuf.readableBytes() < 4) {
        return;
    }
    list.add(new UnixTime(byteBuf.readInt()));
}
  • 修改 TimeClientHandler
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    UnixTime unixTime = (UnixTime) msg;
    ctx.close();
}
  • 修改 TimeServerHandler
@Override
public void channelActive(final ChannelHandlerContext ctx) {
    ChannelFuture f = ctx.writeAndFlush(new UnixTime());
    f.addListener(ChannelFutureListener.CLOSE);
}
  • 最后还需要实现一个编码器,通过实现 ChannelOutboundHandler 来将 UnixTime 对象转换为 ByteBuf,这里有两个点要注意:
    • 当编码后的数据被写到了通道上 Netty 可以通过 ChannelPromise 对象的标记确认成功或失败
    • 不需要调用 cxt.flush(),因为处理器已经单独分离出了一个方法 void flush(ChannelHandlerContext cxt),如果想自己实现 flush() 方法内容可以自行覆盖这个方法
public class TimeEncoder extends ChannelOutboundHandlerAdapter{
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        UnixTime m = (UnixTime) msg;
        ByteBuf encoded = ctx.alloc().buffer(4);
        encoded.writeInt((int)m.value());
        ctx.write(encoded, promise); 
    }
}
  • 你可以使用 MessageToByteEncode
public class TimeEncoder extends MessageToByteEncoder<UnixTime> {
    @Override
    protected void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) {
        out.writeInt((int)msg.value());
    }
}
  • 最后将 TimeEncoder 加入到 ChannelPipeline,并位于 TimeServerHandler 之前

关闭应用

  • 关闭一个 Netty 应用只需通过 shutdownGracefully() 方法来关闭所有的 EventLoopGroup
  • 当所有的 EventLoopGroup 被完全地终止,并且对应的所有 channel 都已经被关闭时,Netty 会返回一个 Future 对象来通知你

本文由澳门凯旋门官网发布于澳门凯旋门官网,转载请注明出处:快速入门,不再需要电脑软件来写入GPS坐标

TAG标签:
Ctrl+D 将本页面保存为书签,全面了解最新资讯,方便快捷。