主要是自己记录一下,刚开始学习这方面知识。对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 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+"";
}
/**
* 下线处理的过程
*
* 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;
}
}
这基本解决了服务器与设备之间的通信,比较粗糙,望指点,下一篇把用到的一些工具转换函数补上。