电信的天翼物联网平台CTWing(AIOT)原先是我们俗称的aep,主要用于接入nb-iot设备,当然也可以接入其他的设备,在熟悉AIOT平台后,做后端的我有时候急需终端样品(智能门禁,支付识别终端,CPE终端,考勤机等)做北向订阅的应用开发,可南向设备的开发需要一段时间,因此可以使用其他办法,工具啥的模拟终端设备进行数据交互的开发。

对于实时性要求高的设备,比如智能门禁机,当触发需要开门的请求后,需要立即给设备发送开门指令(信号),那么常用的tcp协议成为最好最简单快捷的一种选择方式。

对于天翼物联网平台的基础使用(注册,登录,创建产品)就不记录了,直接创建产品

要求:设备直连  TCP协议  明文  特征字符串 一型一密 透传 分类可选择智慧社区的配件

 

在这里我们首先获取到产品ID和Master-APIkey,在点击详情里面

产品ID:15506850

Master-APIkey:d894a1c38274440986dd4f4cc3a7799a

特征串:IicRLZ58eW_4LYp5EUIKdJcqyL5DU7XuepoQaV4U-SY

一般设备最长使用imei作为唯一的标识来进行通讯的,这里我模拟一个imei号码注册,拿到该设备的认证信息

869401041201815

 

这里有一个点,设备ID是产品ID+设备编号(imei)拼接而成

deviceId: 15506850869401041201815

 这些参数在终端程序中是需要使用到的,因此先行拿出来放着

在看AIOT平台对于TCP的协议,透传模式和非透传模式

tcp数据协议的地址(官网可找)接口介绍-中国电信天翼物联网CTWing门户网站

上图是协议的关键,至于协议的业务交互流程,AIOT有文档里已经给出了,还有示例,因此直接上使用Springboot+Netty模拟此协议的代码

新建Springboot的maven项目,pom.xml文件导入依赖包(用到了swagger来测试终端上报数据)

	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.5.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<groupId>boot.ctwing.tcp.terminal</groupId>
	<artifactId>boot-example-ctwing-tcp-terminal-2.0.5</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>boot-example-ctwing-tcp-terminal-2.0.5</name>
	<url>http://maven.apache.org</url>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>io.netty</groupId>
			<artifactId>netty-all</artifactId>
			<version>4.1.29.Final</version>
		</dependency>

		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger2</artifactId>
			<version>2.9.2</version>
		</dependency>
		<dependency>
			<groupId>com.github.xiaoymin</groupId>
			<artifactId>swagger-bootstrap-ui</artifactId>
			<version>1.9.2</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<!-- 打包成一个可执行jar -->
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<executions>
					<execution>
						<goals>
							<goal>repackage</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>

Springboot启动类,Netty启动

package boot.ctwing.tcp.terminal;

import boot.ctwing.tcp.terminal.config.CtWingConstant;
import boot.ctwing.tcp.terminal.netty.TcpClient;
import boot.ctwing.tcp.terminal.utils.CtWingUtils;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

/**
 *  蚂蚁舞
 */
@SpringBootApplication
@EnableScheduling
public class BootCtWingTcpTerminal implements CommandLineRunner {
    public static void main( String[] args ) throws Exception {
    	SpringApplication.run(BootCtWingTcpTerminal.class, args);
        System.out.println( "Hello World!" );
    }

    @Override
    public void run(String... args) throws Exception {

        byte[] bytes = CtWingUtils.tcp_01_auth();
        new TcpClient().startup(CtWingConstant.port, CtWingConstant.address, bytes);

//        int port = 8996;
//        new IotTcpClient().connect(port, "127.0.0.1");
    }


}
SwaggerConfig配置
package boot.ctwing.tcp.terminal.config;

import com.google.common.base.Predicates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 *  蚂蚁舞
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createRestApi(){
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
                .apis(RequestHandlerSelectors.any()).paths(PathSelectors.any())
                .paths(Predicates.not(PathSelectors.regex("/error.*")))
                .paths(PathSelectors.regex("/.*"))
                .build().apiInfo(apiInfo());
    }

    private ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("天翼物联网CtWing终端模拟mock")
                .description("终端模拟需要的测试接口")
                .version("0.01")
                .build();
    }

    /**
     * http://localhost:8177/doc.html  地址和端口根据实际项目查看
     */


}

CtWingConstant静态类,里面包括产品Id,特征字符串和设备imei

package boot.ctwing.tcp.terminal.config;

/**
 *  蚂蚁舞
 */
public class CtWingConstant {

    //  产品ID
    public static final String productId = "15506850";

    //  设备imei
    public static final String imei = "869401041201815";

    //  特征字符串
    public static final String password = "IicRLZ58eW_4LYp5EUIKdJcqyL5DU7XuepoQaV4U-SY";

    public static final String version = "1.0";

    public static final String address = "tcp.ctwing.cn";

    public static final int port = 8996;

    //  登录认证
    public static final String tcp_hex_01 = "01";

    //  上行数据报文
    public static final String tcp_hex_02 = "02";

    //  下行数据报文
    public static final String tcp_hex_03 = "03";

    //  上行心跳
    public static final String tcp_hex_04 = "04";

    //  登录响应
    public static final String tcp_hex_05 = "05";

    //  心跳响应
    public static final String tcp_hex_06 = "06";


}
TcpClient
package boot.ctwing.tcp.terminal.netty;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *  蚂蚁舞
 */
public class TcpClient {

    private final Logger log =  LoggerFactory.getLogger(this.getClass());

    public static SocketChannel socketChannel = null;

    private static final EventLoopGroup group = new NioEventLoopGroup();

    public void startup(int port, String host, byte[] bytes) throws Exception{

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group);
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.option(ChannelOption.TCP_NODELAY, true);
            bootstrap.handler(new TcpChannelInitializer<SocketChannel>());
            ChannelFuture f = bootstrap.connect(host, port).sync();
            if (f.isSuccess()) {
                socketChannel = (SocketChannel) f.channel();
                log.info("connect server success");
                f.channel().writeAndFlush(Unpooled.buffer().writeBytes(bytes));
                log.info("send success");
                f.channel().closeFuture().sync();
            }
        } catch (Exception e) {
            System.out.println(e.toString());
        } finally {
            group.shutdownGracefully().sync();
        }
    }


}
TcpChannelInitializer<SocketChannel>
package boot.ctwing.tcp.terminal.netty;


import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;

/**
 *  蚂蚁舞
 * @param <SocketChannel>
 */
public class TcpChannelInitializer<SocketChannel> extends ChannelInitializer<Channel>{


    @Override
    protected void initChannel(Channel ch) throws Exception {
        //  二者选择一个就可以
        //  使用netty自带的
//        ch.pipeline().addLast("decoder", new ByteArrayDecoder());
//        ch.pipeline().addLast("encoder", new ByteArrayEncoder());

        // 使用自定义的
        ch.pipeline().addLast(new TcpMessageCodec());


        ch.pipeline().addLast(new TcpChannelInboundHandlerAdapter());

    }

}
TcpChannelInboundHandlerAdapter
package boot.ctwing.tcp.terminal.netty;

import java.io.IOException;
import java.net.InetSocketAddress;

import boot.ctwing.tcp.terminal.config.CtWingConstant;
import boot.ctwing.tcp.terminal.utils.CtWingUtils;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *  蚂蚁舞
 */
public class TcpChannelInboundHandlerAdapter extends ChannelInboundHandlerAdapter{

    private final Logger log =  LoggerFactory.getLogger(this.getClass());

    /**
     * 从服务端收到新的数据时,这个方法会在收到消息时被调用
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception, IOException {
        byte[] req = (byte[]) msg;
        if(req.length > 0){
            String hex = CtWingUtils.bytesToHexStr(req);
            log.info("data--"+hex);
            String soh = hex.substring(0,2);
            switch (soh) {
                case CtWingConstant.tcp_hex_06:
                    // 平台回复终端心跳的响应 保活用
                    break;
                case CtWingConstant.tcp_hex_05:
                    // 认证消息返回 05 00 00  登录结果: 0 成功 1 未知错误 2 设备未注册 3 设备认证失败 4 设备已登录
                    // to do
                    break;
                case CtWingConstant.tcp_hex_03:
                    // 0x03 +数据长度(2字节) +业务数据 下行数据,处理下行逻辑
                    String dataHex = hex.substring(6);
                    log.info("hexStr--"+dataHex);
                    // 如果是字符串 16进制字符串转字符串
                    log.info("str--"+CtWingUtils.hexStrToStr(dataHex));
                default:
                    break;
            }
        }
    }

    /**
     * 从服务端收到新的数据、读取完成时调用
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws IOException {
        System.out.println("channelReadComplete");
    }

    /**
     * 当出现 Throwable 对象才会被调用,即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws IOException {
        System.out.println("exceptionCaught");
        cause.printStackTrace();
        ctx.close();//抛出异常,断开与客户端的连接
    }

    /**
     * 客户端与服务端第一次建立连接时 执行
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception, IOException {
        System.out.println("channelActive");
        super.channelActive(ctx);

    }

    /**
     * 客户端与服务端 断连时 执行
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception, IOException {
        super.channelInactive(ctx);
        InetSocketAddress inSocket = (InetSocketAddress) ctx.channel().remoteAddress();
        String clientIp = inSocket.getAddress().getHostAddress();
        ctx.close(); //断开连接时,必须关闭,否则造成资源浪费
        System.out.println("channelInactive:"+clientIp);
    }

}

TcpMessageCodec

package boot.ctwing.tcp.terminal.netty;


import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageCodec;

import java.util.List;

/**
 *  蚂蚁舞
 */
@ChannelHandler.Sharable
public class TcpMessageCodec extends MessageToMessageCodec<ByteBuf, ByteBuf> {


    @Override
    protected void encode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
        byte[] array = new byte[msg.readableBytes()];
        msg.getBytes(0, array);
        out.add(Unpooled.wrappedBuffer(array));
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
        byte[] array = new byte[msg.readableBytes()];
        msg.getBytes(0, array);
        out.add(array);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
        System.out.println("OutIn异常!"+cause);
    }

}

TcpHeartTimer
package boot.ctwing.tcp.terminal.netty;


import boot.ctwing.tcp.terminal.config.CtWingConstant;
import boot.ctwing.tcp.terminal.utils.CtWingUtils;
import io.netty.buffer.Unpooled;
import io.netty.channel.socket.SocketChannel;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

/**
 *  蚂蚁舞
 */
@Service
public class TcpHeartTimer {

    //  使用定时器发送心跳
    @Scheduled(cron = "0 0/3 * * * ?")
    public void tcp_ct_wing_heart_timer() {
        String back = CtWingConstant.tcp_hex_04;
        byte[] data = CtWingUtils.hexStrToBytes(back);
        SocketChannel socketChannel = TcpClient.socketChannel;
        if( socketChannel != null && socketChannel.isOpen()) {
            socketChannel.writeAndFlush(Unpooled.buffer().writeBytes(data));
        }
    }

}
CtWingUtils
package boot.ctwing.tcp.terminal.utils;


import boot.ctwing.tcp.terminal.config.CtWingConstant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;

/**
 *  蚂蚁舞
 */
public class CtWingUtils {

    private static final Logger log =  LoggerFactory.getLogger(CtWingUtils.class);

    private static final char[] HEXES = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

    public static String bytesToHexStr(byte[] bytes) {
        if (bytes == null || bytes.length == 0) {
            return null;
        }
        StringBuilder hex = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            hex.append(HEXES[(b >> 4) & 0x0F]);
            hex.append(HEXES[b & 0x0F]);
        }
        return hex.toString().toUpperCase();
    }

    public static byte[] hexStrToBytes(String hex) {
        if (hex == null || hex.length() == 0) {
            return null;
        }
        char[] hexChars = hex.toCharArray();
        byte[] bytes = new byte[hexChars.length / 2];   // 如果 hex 中的字符不是偶数个, 则忽略最后一个
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = (byte) Integer.parseInt("" + hexChars[i * 2] + hexChars[i * 2 + 1], 16);
        }
        return bytes;
    }

    public static String strToHexStr(String str) {
        StringBuilder sb = new StringBuilder();
        byte[] bs = str.getBytes();
        int bit;
        for (int i = 0; i < bs.length; i++) {
            bit = (bs[i] & 0x0f0) >> 4;
            sb.append(HEXES[bit]);
            bit = bs[i] & 0x0f;
            sb.append(HEXES[bit]);
        }
        return sb.toString().trim();
    }

    public static String hexStrToStr(String hexStr) {
        //能被16整除,肯定可以被2整除
        byte[] array = new byte[hexStr.length() / 2];
        try {
            for (int i = 0; i < array.length; i++) {
                array[i] = (byte) (0xff & Integer.parseInt(hexStr.substring(i * 2, i * 2 + 2), 16));
            }
            hexStr = new String(array, StandardCharsets.UTF_8);
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
        return hexStr;
    }

    public static String hexLen4Calc(int fixed, int len){
       StringBuilder x = new StringBuilder(Integer.toHexString(len));
       int xC = fixed - x.length();
       for(int i = 0; i< xC; i++){
           x.insert(0, "0");
       }
       return x.toString();
    }

    public static byte[] tcp_01_auth(){
        String deviceId = CtWingConstant.productId+CtWingConstant.imei;
        String deviceIdLenHex = hexLen4Calc(4, deviceId.length());
        String deviceIdHex = strToHexStr(deviceId);
        String passwordLenHex = hexLen4Calc(4, CtWingConstant.password.length());
        String passwordHex = strToHexStr(CtWingConstant.password);
        String versionLenHex = hexLen4Calc(4, CtWingConstant.version.length());
        String versionHex = strToHexStr(CtWingConstant.version);

        String cmd = CtWingConstant.tcp_hex_01+deviceIdLenHex+deviceIdHex+passwordLenHex+passwordHex+versionLenHex+versionHex;
        log.info(cmd);
        return hexStrToBytes(cmd);
    }

    public static byte[] tcp_02_up_msg(String str){
        String upHexStr = strToHexStr(str);
        String upHexStrLenHex = hexLen4Calc(4, upHexStr.length()/2);
        String cmd = CtWingConstant.tcp_hex_02+upHexStrLenHex+upHexStr;
        log.info(cmd);
        return hexStrToBytes(cmd);
    }



}
TcpTerminalController
package boot.ctwing.tcp.terminal.controller;

import boot.ctwing.tcp.terminal.netty.TcpClient;
import boot.ctwing.tcp.terminal.utils.CtWingUtils;
import io.netty.buffer.Unpooled;
import io.netty.channel.socket.SocketChannel;
import org.springframework.web.bind.annotation.*;

@RestController
public class TcpTerminalController {

	@GetMapping(value = {"", "/"})
	public String index() {
		return "天翼物联网CtWing终端模拟mock";
	}

	@GetMapping("/reportData")
	public String reportData(@RequestParam(name="content",defaultValue="hello-myw-terminal") String content) {
		byte[] data = CtWingUtils.tcp_02_up_msg(content);
		SocketChannel socketChannel = TcpClient.socketChannel;
		if( socketChannel != null && socketChannel.isOpen()) {
			socketChannel.writeAndFlush(Unpooled.buffer().writeBytes(data));
		}
		return "success";
	}

}

代码目录结构

boot-example-ctwing-tcp-terminal-2.0.5
    │  pom.xml
    │  
    └─src
        ├─main
        │  ├─java
        │  │  └─boot
        │  │      └─ctwing
        │  │          └─tcp
        │  │              └─terminal
        │  │                  │  BootCtWingTcpTerminal.java
        │  │                  │  
        │  │                  ├─config
        │  │                  │      CtWingConstant.java
        │  │                  │      SwaggerConfig.java
        │  │                  │      
        │  │                  ├─controller
        │  │                  │      TcpTerminalController.java
        │  │                  │      
        │  │                  ├─netty
        │  │                  │      TcpChannelInboundHandlerAdapter.java
        │  │                  │      TcpChannelInitializer.java
        │  │                  │      TcpClient.java
        │  │                  │      TcpHeartTimer.java
        │  │                  │      TcpMessageCodec.java
        │  │                  │      
        │  │                  └─utils
        │  │                          CtWingUtils.java
        │  │                          
        │  └─resources
        │          application.properties
        │          logback-spring.xml
        │          
        └─test
            └─java
                └─boot
                    └─ctwing
                        └─tcp
                            └─terminal
                                    BootCtWingTcpTerminalTest.java
                                    

启动Springboot项目后可以看到最后的日志信息是这样的

20:31:55.987 spring-boot-logging [main] INFO  o.a.tomcat.util.net.NioSelectorPool - Using a shared selector for servlet write/read
20:31:56.014 spring-boot-logging [main] INFO  o.s.b.w.e.tomcat.TomcatWebServer - Tomcat started on port(s): 8177 (http) with context path ''
20:31:56.022 spring-boot-logging [main] INFO  b.c.t.terminal.BootCtWingTcpTerminal - Started BootCtWingTcpTerminal in 7.813 seconds (JVM running for 8.702)
20:31:56.027 spring-boot-logging [main] INFO  b.c.tcp.terminal.utils.CtWingUtils - 0100173135353036383530383639343031303431323031383135002b496963524c5a353865575f344c5970354555494b644a6371794c35445537587565706f51615634552d53590003312e30
channelActive
20:31:58.379 spring-boot-logging [main] INFO  b.c.tcp.terminal.netty.TcpClient - connect server success
20:31:58.413 spring-boot-logging [main] INFO  b.c.tcp.terminal.netty.TcpClient - send success
20:31:58.542 spring-boot-logging [nioEventLoopGroup-2-1] INFO  b.c.t.t.n.TcpChannelInboundHandlerAdapter - data--050000
channelReadComplete

启动就开始认证

0100173135353036383530383639343031303431323031383135002b496963524c5a353865575f344c5970354555494b644a6371794c35445537587565706f51615634552d53590003312e30

得到服务端的响应信息

050000

心跳返回是(定时器3分钟发一次(04),平台说的是5分钟内)

06

此时我们再看AIOT平台的设备管理详情里面

 显示已经激活了

认证成功  心跳也正常 那么开始发送数据,一般发送的数据在设备端是不含中文的,我这里把中文也带上,测试下发送中文是否可用可行,我的端口是8177 因此本地访问

http://localhost:8177/doc.html

 我发送了好几条数据(字符串形式),回到AIOT平台的数据里查看

如此模拟设备的数据成功上传到天翼物联网平台AIOT

6JqC6JqB6Iie


6JqC6JqB5Lmf5Lya6Lez6Iie


bXl3


bXl5aHR3MTIzNDU0NjU0Njc0ZXF3amRjcW93ZWljcW93aXhjbmRjeA==

 四条数据是经过base64的,因此需要解开,在转成我发送的字符串就可以

package boot.ctwing.tcp.app;

import boot.ctwing.tcp.app.utils.CtWingUtils;
import java.util.Base64;

public class Test {

    public static void main(String[] args) {
        byte[] decoded1 = Base64.getDecoder().decode("6JqC6JqB6Iie");
        String hex1 = CtWingUtils.bytesToHexStr(decoded1);
        System.out.println(CtWingUtils.hexStrToStr(hex1));

        byte[] decoded2 = Base64.getDecoder().decode("bXl3");
        String hex2 = CtWingUtils.bytesToHexStr(decoded2);
        System.out.println(CtWingUtils.hexStrToStr(hex2));

        byte[] decoded3 = Base64.getDecoder().decode("6JqC6JqB5Lmf5Lya6Lez6Iie");
        String hex3 = CtWingUtils.bytesToHexStr(decoded3);
        System.out.println(CtWingUtils.hexStrToStr(hex3));

        byte[] decoded4 = Base64.getDecoder().decode("bXl5aHR3MTIzNDU0NjU0Njc0ZXF3amRjcW93ZWljcW93aXhjbmRjeA==");
        String hex4 = CtWingUtils.bytesToHexStr(decoded4);
        System.out.println(CtWingUtils.hexStrToStr(hex4));
    }
}

最终得到的字符串数据

蚂蚁舞
myw
蚂蚁也会跳舞
myyhtw123454654674eqwjdcqoweicqowixcndcx

如此使用springboot+netty模拟天翼物联网CtWing的终端设备算是完成了。

更多推荐