主要是自己记录一下,刚开始学习这方面知识。对TCP通信理解的并不是特别透彻,只能通过代码一步一步深入:

本文主要功能是,传感器设备(包括可控制类电机)采集信息,以及发送指令,包括回传等功能。

废话不多说,老规矩,直接上代码:

package me.control;

import com.google.gson.JsonSyntaxException;
import me.control.bean.ChannelBean;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.*;

@Component
public class ControlServer implements ApplicationRunner {
    public static ControlServer controlServer;


//单例
    public static ControlServer getInstence() {
        if (controlServer == null) {
            controlServer = new ControlServer();
        }
        return controlServer;
    }

    private Selector selector = null;
    static final int port = 8888;
    private Charset charset = Charset.forName("UTF-8");
    private int bufferSize = 4096; //注意区块的大小

    //记录连接对象的容器(里面包含了该连接的全部信息)
    private List<ChannelBean> list = new ArrayList<>();


    public void init() throws IOException {
        selector = Selector.open();
        ServerSocketChannel server = ServerSocketChannel.open();
        server.bind(new InetSocketAddress(port));
        //非阻塞的方式
        server.configureBlocking(false);
        //注册到选择器上,设置为监听状态
        server.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("等待连接。。。");

        while (true) {
            int readyChannels = selector.select();
            if (readyChannels == 0) continue;
            Set selectedKeys = selector.selectedKeys();  //获取所有链接
            Iterator keyIterator = selectedKeys.iterator();

            while (keyIterator.hasNext()) {
                SelectionKey sk = (SelectionKey) keyIterator.next();
                keyIterator.remove();

                dealWithSelectionKey(server, sk);//处理一个连接

            }
        }
    }

   
    public boolean dealWithSelectionKey(ServerSocketChannel server, SelectionKey sk) throws IOException {

        if (sk.isAcceptable()) {
            SocketChannel sc = server.accept();
            //非阻塞模式
            sc.configureBlocking(false);
            //注册选择器,并设置为读取模式,收到一个连接请求,然后起一个SocketChannel,并注册到selector上,之后这个连接的数据,就由这个SocketChannel处理
            if (sc == null) {
                return false;
            }
            sc.register(selector, SelectionKey.OP_READ);
            //将此对应的channel设置为准备接受其他客户端请求,加入一个新的客户端
            sk.interestOps(SelectionKey.OP_ACCEPT);
            System.out.println("time:" + formatTime.format(new Date()) + ", Server is accepted from a new client :" + sc.getRemoteAddress().toString().substring(1));

        }

        //处理来自客户端的数据读取请求
        if (sk.isReadable()) {
            //返回该SelectionKey对应的 Channel,其中有数据需要读取,则读取它送过来的数据
            SocketChannel sc = (SocketChannel) sk.channel();
            //获取数据
            ByteBuffer buff = ByteBuffer.allocate(bufferSize);
            String content = null;
            String s16 = null;
            try {

                while (sc.read(buff) > 0) {
                    buff.flip();
//客户端发送过来的数据有可能是指令,也有可能是回传,但统一都是16进制数据
//此处是把收到的信息 buff--》string
                    content = DataUtil.decodeKey(buff);
//此处是把收到的信息 buff--》byte[](类似于 “01 02 03 04 05 06”)--》16进制数据
                    s16 = DataUtil.BinaryToHexString(DataUtil.decodeValue(buff)).trim();
                }
               
                if (sc.read(buff) == -1) {
                    System.out.println(sc.socket().getRemoteSocketAddress() + "断开连接");
                    sc.close();
                    return false;
                }
                sk.interestOps(SelectionKey.OP_READ);//改为接受数据状态
            } catch (IOException io) {
                sk.cancel();
                System.out.println("read or write error " + io);
                if (sk.channel() != null) {
                    sk.channel().close();
                    //下线通知,更新这里,并更新数据库
                    this.clientDisconnect(sk);
                    return false;
                }
            }
            
            System.out.println("接收到数据:" + content + "  " + formatTime.format(new Date()) + "长度:" + content.length());
            System.out.println("接收到16进制数据为:" + s16 + "长度:" + s16.length());
            if (s16.length() == 5) {//注册(包括心跳包也是这个编号),这里因为我的所有设备都设置编号为长度5
                boolean isContant = false;
                for (ChannelBean channelBean : list) {
                    if (channelBean.getId().equals(s16)) {
                        System.out.println("已查到库中包含该设备,改变连接状态为true");
                        channelBean.setConnect(true);
                        channelBean.setSocketChannel(sc);
                        isContant = true;
                    }
                }
                if (!isContant) {
                    System.out.println("已查到库中不包含该设备,添加设备到库中并设置连接状态为true");
                    ChannelBean channelBean = new ChannelBean();

                    channelBean.setId(s16);
                    channelBean.setName("dtu");
                    channelBean.setConnect(true);
                    channelBean.setSocketChannel(sc);
                    list.add(channelBean);
                    System.out.println("库中包含" + list.size() + "个设备");
                }
            }else {//非注册信息(包括发送与回传)发送信息不在此处处理,单独处理;这里只负责回传(16进制很方便,因为数据的长度是固定的)
//此处根据通道的id来判断是哪个设备回传的信息
//根据信息长度,过滤错误信息 ,然后把得到的信息转为String并解析保存到该通道的容器中
                for (ChannelBean channelBean:list){
                    if (channelBean.getSocketChannel().equals(sc)){
                        if (channelBean.getId().equals("00 01")){//泵站

                            if (s16.length()==62){
                                System.out.println(channelBean.getId()+"收到查询信息:"+ s16);
                                channelBean.setMsg(HexToBeanUtil.getBZInfo(s16));
                            }
                        }else if(channelBean.getId().equals("00 55")){//泵站水位计
                            if (s16.length()==134){
                                System.out.println(channelBean.getId()+"收到查询信息:"+ s16);
                                channelBean.setMsg(transformBZFluviograph(s16)+"cm");
                            }

                        }else if (channelBean.getId().equals("00 54")){//田间水位计
                            if (s16.length()==23){
                                System.out.println(channelBean.getId()+"收到查询信息:"+ s16);
                                channelBean.setMsg(transformTJFluviograph(s16)+"mm");
                            }

                        }else  {//闸门开度
                            if (s16.length()==14){
                                System.out.println(channelBean.getId()+"收到查询信息:"+ s16);
                                channelBean.setMsg(HexToBeanUtil.getZMOpen(s16));
                            }
                        }

                    }
                }
            }
        }
        return true;
    }
/**
     * 泵站水位计
     * @param hex
     * @return
     */
    public String transformBZFluviograph(String hex){
        int str = Integer.parseInt(hex.substring(9,11)+hex.substring(6,8),16);
        return str+"";
    }

    /**
     * 田间水位计
     * @param hex
     * @return
     */
    public String transformTJFluviograph(String hex){
        int str = Integer.parseInt(hex.substring(15,17)+hex.substring(12,14),16);
        return str+"";
    }

    /**
     * 下线处理的过程
     * <p>
     * 1、socket断开,channel断开
     * 2、userlist表除名,如果可能,给互联用户下线通知 下线的逻辑为广播一下,然后让其他人做对比......
     * 3、数据库进行更新
     * 4、日志记录
     **/
    private void clientDisconnect(SelectionKey sk) {
        for (ChannelBean channelBean : list) {
            if (channelBean.getSocketChannel().equals(sk)) {
                channelBean.setConnect(false);
            }
        }
        //TODO 设计下线格式
        try {
            this.broadCastInfo(selector, (SocketChannel) sk.channel(), "下线");
            sk.channel().close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        //TODO 数据库更新
        //这里对数据库的操作以后再做......,即下线的影响,以后再处理......
        //TODO 日志记录
    }
//发送信息处理,外部调用 id是设备编号  info是信息内容

    public String sendToClient(String id, String info) throws IOException {
        for (ChannelBean channelBean : list) {
            if (channelBean.getId().equals(id)) {
                if (channelBean.isConnect()) {
                    System.out.println("发送信息:"+info);
//把数据转为16进制发送
                  channelBean.getSocketChannel().write(DataUtil.encodeValue(DataUtil.hexStrToBinaryStr(info)));
                    return "send success";
                } else {
                    return "this device has disconnect";
                }
            }
        }
        return "this device has no connect";
    }

//给所有设备发送信息
    public void broadCastInfo(Selector selector, SocketChannel selfChannel, String info) throws IOException {
        //广播数据到所有的SocketChannel中
        for (SelectionKey key : selector.keys()) {
            Channel targetchannel = key.channel();
            //如果except不为空,不回发给发送此内容的客户端
            if (targetchannel instanceof SocketChannel) {
                SocketChannel dest = (SocketChannel) targetchannel;
                dest.write(DataUtil.encodeValue(DataUtil.hexStrToBinaryStr(info)));
            }
        }
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {

        ControlServer controlServer = ControlServer.getInstence();
        controlServer.init();
    }
}
package me.control.bean;

import java.nio.channels.SocketChannel;

//通道集合
public class ChannelBean {
    private String id;//设备编号
    private String name;//设备名称
    private SocketChannel socketChannel;//通信通道
    private Object msg;//最近的返回数据
    private boolean isConnect;//是否连接

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public SocketChannel getSocketChannel() {
        return socketChannel;
    }

    public void setSocketChannel(SocketChannel socketChannel) {
        this.socketChannel = socketChannel;
    }

    public boolean isConnect() {
        return isConnect;
    }

    public void setConnect(boolean connect) {
        isConnect = connect;
    }

    public Object getMsg() {
        return msg;
    }

    public void setMsg(Object msg) {
        this.msg = msg;
    }
}

这基本解决了服务器与设备之间的通信,比较粗糙,望指点,下一篇把用到的一些工具转换函数补上。