物聯網Java服務端TCP通訊

主要是自己記錄一下,剛開始學習這方面知識。對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;
    }
}

這基本解決了服務器與設備之間的通信,比較粗糙,望指點,下一篇把用到的一些工具轉換函數補上。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章