java 与打卡器通过udp协议交互

上篇文章使用串口与打卡器交互,这种方式的弊端就是需要很多串口接口才能使得一台服务器与多个打卡器交互,而使用udp协议只需要搭建一个局域网络就可以了。这里使用多线程通信机制也增加了程序的运行效率。(这里说明下,打卡器是不停的向外发送数据包,所以使用udp无连接协议乃是最适合的)。首先给出主服务类的代码:

package com.as.util;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.TimerTask;
import java.util.concurrent.CountDownLatch;

import timetrigger.JDBC1;
import com.as.bean.Schedule;
import com.as.bean.SearchCardRecordBean;

/**
 * 用来监听读卡器的主类
 * 
 * @author wangqing
 * 
 */
public class OpenCrMain extends TimerTask {
    private static InetSocketAddress socketAddress = null; // 服务监听地址
    private static DatagramSocket datagramSocket = null; // 连接对象
    public static String ServiceIp = "192.168.2.101";
    public static int ServicePort = 32500;
    public static List<String> sendIpList = new ArrayList<String>();
    private Schedule schedule;
    static List<SearchCardRecordBean> scrb = new ArrayList<SearchCardRecordBean>();// 被怀疑迟到的人

    public OpenCrMain(Schedule schedule) {
        this.schedule = schedule;
    }

    /**
     * 初始化连接
     * @throws SocketException
     */
    public static void init() {
        try {
            socketAddress = new InetSocketAddress(ServiceIp, ServicePort);
            datagramSocket = new DatagramSocket(socketAddress);
            datagramSocket.setSoTimeout(60 * 1000);
            System.out.println("服务端已经启动");
        } catch (Exception e) {
            datagramSocket = null;
            System.err.println("服务端启动失败");
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        openCrAndReadIt();
    }
    /**
     * 接收数据包,该方法会造成线程阻塞
     * @return
     * @throws Exception
     * @throws IOException
     */
    public synchronized static DatagramPacket receive(DatagramPacket packet)
            throws Exception {
        try {
                datagramSocket.receive(packet);
            return packet;
        } catch (Exception e) {
            System.out.println("抛出异常");
            throw e;
        }
    }
    /**
     * 将响应包发送给请求端
     * @param bt
     * @throws IOException
     */
    public synchronized static void response(DatagramPacket packet) {
        try {
            datagramSocket.send(packet);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 打开读卡器并分析数据
     */
    public synchronized void openCrAndReadIt() {
        try {
            scrb = new JDBC1().searchDoubtLate(schedule.getScheduleID(),
                    schedule.getBeginTime(), schedule.getEndTime());// 得出某个特定的班缺勤嫌疑人人员列表
            for (int i = 0; i < scrb.size(); i++) {
                scrb.get(i).setSearchResult("未找到");
            }
            init();
            byte[] buffer = new byte[1024 * 64]; // 缓冲区
            List<String> iplist=new ArrayList<String>();
            iplist.add("192.168.2.202");
            CountDownLatch threadSignal = new CountDownLatch(iplist.size());//初始化countDown 
            for (String string : iplist) {
                DatagramPacket packet = new DatagramPacket(buffer, buffer.length,InetAddress.getByName(string),32500);
                
                    //开启线程对数据进行分析
                    new AnalysisDataFromCr(threadSignal,packet).start();
                    
                    
            }
            threadSignal.await();//等待所有子线程执行完  
            datagramSocket.close();
            for(int i=0;i<scrb.size();i++){
                System.out.println(scrb.get(i).getSerial_Nr());
            }
            new JDBC1().add1(scrb);//匹配完后把结果添加到数据库中
            System.out.println("添加完成");
        } catch (Exception e) {
            System.out.println("抛出异常");
        }
    }


    
}

如上:本程序类是定时器的类所以需要继承TimerTask类,其中run方法为定时器启动时调用的方法,其中只有一个openCrAndReadIt(),用以完成处理读卡器数据的功能。首先init()函数开启监听服务,随时接收来自读卡器的数据,这里scrb的数据作为一个需要处理的全局变量,每个打卡器对应一个Ip地址(读卡器的Ip应当从数据库中查询出来,这里只提供了模拟数据的实现)

对读卡器的ip集合进行遍历, DatagramPacket packet = new DatagramPacket(buffer, buffer.length,InetAddress.getByName(string),32500);设置的是服务器与每一个打卡器交互的数据包, 这里的 threadSignal是一个线程计数器,目的是为了使主线程等待子线程运行完毕(只有所有打卡器数据处理完后才能对数据进行最终的写入数据库操作)。需要把这个线程计数器的对象传入线程内部,线程过程执行完后则使之减一,最终在主线程中统计该计数器就可以判断是否所有的子线程都执行完毕。接下来给出处理打卡器数据的子线程类:

package com.as.util;
import java.net.DatagramPacket;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
 * 处理读卡器数据的线程类
 * @author 
 *
 */
public class AnalysisDataFromCr extends Thread{
//线程计数器    
private CountDownLatch cr;
//数据流
private DatagramPacket packet;
public static String closeString = "FEF10f000000000000000fFF";
public static String openString = "FEF10e000000000000000eFF";
public AnalysisDataFromCr(CountDownLatch cr,DatagramPacket packet){
    this.cr=cr;
    this.packet=packet;
}
public void run(){
    long start = new Date().getTime();
    long end = start;
    openCr();
    while ((end - start) <= 10* 10) {
        try {
            OpenCrMain.receive(packet);
        } catch (Exception e) {
            e.printStackTrace();
        }
        byte[] bt = new byte[packet.getLength()];
        System.arraycopy(packet.getData(), 0, bt, 0, packet.getLength());
        StringBuffer dataStr = toHex(bt, 0, bt.length);
        analyseDataPacket(dataStr);
        end++;
    }
    closeCr();
    cr.countDown();
}
/**
 * 接收打卡器数据并比较怀疑人
 * @param bufferContent
 */
public void analyseDataPacket(StringBuffer bufferContent) {
    // 刚开机时硬件会随机产生一些垃圾代码,需要把他们去掉
    StringBuffer neededData = bufferContent;
    boolean stop = false;
    while (!stop) {
        boolean found = neededData.toString().regionMatches(0, "02", 0, 2);
        if (found == false) {
            if(neededData.length()>0)
            neededData = neededData.deleteCharAt(0);
            else{
                stop = true;
            }
        } else {
            stop = true;
        }
    }
    // 缓存里可能会存在很多个数据包,先找出总长度,然后一个数据包一个数据包的处理
    String activePacket = "";
    if (neededData.length() > 0) {
        int totalbytes = Integer.parseInt(neededData.substring(2, 6), 16);// 算出该数据包的总字节数
        if (totalbytes * 2 <= neededData.length()) {
            activePacket = neededData.substring(0, totalbytes * 2);
            neededData = neededData.delete(0, totalbytes * 2);
            int readerID = Integer.parseInt(activePacket.substring(6, 10),
                    16);// 设备ID的十进制
            int cardIDs = Integer.parseInt(activePacket.substring(14, 16),
                    16);// 算出包里包含的标签数
            String cardHex = "";
            if (cardIDs > 0) {
                for (int i = 0; i < cardIDs; i++) {
                    cardHex = activePacket
                            .substring(16 + 6 * i, 22 + 6 * i);
                    for (int j = 0; j < OpenCrMain.scrb.size(); j++) {
                        // 匹配到的就改成找到
                        if (cardHex.equals(OpenCrMain.scrb.get(j).getSerial_Nr())) {
                            OpenCrMain.scrb.get(j).setSearchResult("找到");
                        }
                    }
                }
            }
        }
    }

}

// 把data中从off开始的 length个字符转换成十六进制
public static final StringBuffer toHex(byte[] data, int off, int length) {
    // double size, two bytes (hex range) for one byte
    StringBuffer buf = new StringBuffer(data.length * 2);
    for (int i = off; i < length; i++) {
        if (((int) data[i] & 0xff) < 0x10) {
            buf.append("0");
        }
        buf.append(Long.toString((int) data[i] & 0xff, 16));
    }
    return buf;
}
//打开打卡器
public void openCr(){
    List<byte[]> bytes = new ArrayList<byte[]>();
    for (int i = 0; i < openString.length(); i += 2) {
        String hex = openString.substring(i, i + 2);
        byte[] bnew = CardReaderUtil.hexStringToBytes(hex);
        bytes.add(bnew);
    }
    byte[] newByte = CardReaderUtil.sysCopy(bytes);
    packet.setData(newByte);
    OpenCrMain.response(packet);
}
//关闭打卡器
public void closeCr(){
    List<byte[]> bytes = new ArrayList<byte[]>();
    for (int i = 0; i < closeString.length(); i += 2) {
        String hex = closeString.substring(i, i + 2);
        byte[] bnew = CardReaderUtil.hexStringToBytes(hex);
        bytes.add(bnew);
    }
    byte[] newByte = CardReaderUtil.sysCopy(bytes);
    packet.setData(newByte);
    OpenCrMain.response(packet);
}
}

可以看到这里的run函数里面为详细的执行流程,首先打开读卡器(向读卡器发送打开指令)openCr(),然后循环读取打卡器的数据,这个循环的长度限制为一个防重读时间内(确保在这个时间内读不到重复的数据),analyseDataPacket()方法中是对主线程全局变量scrb的处理,修改完毕后发送关闭的指令(closeCr())关闭这个读卡器。线程计数器减一。等所有子线程结束后主线程将处理后的全部数据插入数据库。多线程的好处就是能够对较大的数据量批次处理,但这里要注意几个互斥量读者可以自己从主线程类中查看带 synchronized的方法。