主要是自己記錄一下,剛開始學習這方面知識。對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;
}
}
這基本解決了服務器與設備之間的通信,比較粗糙,望指點,下一篇把用到的一些工具轉換函數補上。