Socket通讯(阻塞式、多线程IO服务端 TCP长链接 ) 实现

需求

客户的网络接收机可以自动通过tcp长连接方式套接到服务器,发送报文。需要二次开发实现Socket服务器接受报文并根据通讯协议进行分析,需要定时广播心跳报文,维持长连接。

目标

1、实现一个Socket服务器,可维持多个长连接。
2、定时发送心跳报文。
3、读取并分析报文内容(基于字节)。
4、较为完善的异常处理。
5、测试客户端。

实现

1、SocketServer:基于Socket实现一个Socket服务器,阻塞判断是否有连接。由于给定的开发+测试时间只有一周,而且需求不是很复杂,因此没有选择java.nio[非阻塞的通信机制主要由 java.nio 包(新I/O包) 中的类实现, 主要的类包括 ServerSocketChannel, SocketChannel, Selector,SelectionKey 和ByteBuffer 等 ]
2、HandlerThread:多线程维持长链接,基于java.io读取字节流的报文,阻塞读取报文,用一个变量记录上次广播心跳报文时间,阻塞读取的同时判断是否需要广播心跳报文。
3、SocketPacket:使用builder建造者模式实现,建造者模式很适用于这种多个参数可选的情况、安全性和可读性都很高,可以在新建对象的时候校验参数是否满足要求。
包含字节数组转16进制字符串、校验和、转义等解析报文、拼装回复报文的方法。
4、SocketClient:测试Socket客户端,用于连接Socket服务器,测试心跳报文等。


/**
 * 阻塞式 多线程IO
 * Created by Zoey on 2019/01/15
 */
public class SocketServer {
    private static Logger log = LoggerFactory.getLogger(SocketServer.class);
    private static ServerSocket serverSocket = null;
    protected static int SOCKET_READ_TIMEOUT = 5000;

    public SocketServer(int port, int heartBeat) {
        startServer(port, heartBeat;
    }

    private void startServer(int port, int heartBeat) {
        //启动socket服务器
        try {
            //创建一个ServerSocket监听客户请求
            serverSocket = new ServerSocket(port);
            //注意:不能误用以下的timeout这个timeout设置的其实是阻塞时间
            //测试时可以增加timeout使server在指定时间内没有client连接的话关闭
//            serverSocket.setSoTimeout(30000);
        } catch (IOException e) {
            e.printStackTrace();
            log.error("Socket server start error [{}]", e.getMessage());
            return;
        }
        Socket client;
        try {
            log.info("Socket server start waiting.............");
            while (true) {
                client = serverSocket.accept();
                //设置read的阻塞时间
                client.setSoTimeout(SOCKET_READ_TIMEOUT);
                //新建一个线程来读取并处理报文
                log.info("Socket server get connection............");
                new HandlerThread(client, heartBeat);
            }

        } catch (Exception e) {
            //当socket主线程异常
            e.printStackTrace();
            log.error("Socket server runtime error [{}].", e.getMessage());
        } finally {
            stopServer();
        }
    }

    public static void stopServer() {
        try {
            if (serverSocket != null) {
                serverSocket.close();
                serverSocket = null;
            }
        } catch (IOException e) {
            log.error("SocketServer close error [{]].", e.getMessage());
            e.printStackTrace();
        }
    }

    public static boolean isRunning() {
        if (serverSocket != null) {
            return true;
        }
        return false;
    }
}

/**
 * 处理线程
 * Created by Zoey on 2019/01/15
 */
public class HandlerThread implements Runnable {
    private static Logger log = LoggerFactory.getLogger(HandlerThread.class);
    private static boolean isRunning = true;
    private static int NEXT_HEARTBEAT_MS;

    private Date LAST_HEARTBEAT_TIME;
    private int HEART_BEAT;

    private Socket socket;
    private static InputStream input = null;
    private OutputStream out = null;


    public HandlerThread(Socket client, int heartBeat) {
        socket = client;
        this.HEART_BEAT = heartBeat;
        new Thread(this).start();
    }

    public void run() {
        try {
            //由Socket对象得到字节输入流
            input = socket.getInputStream();
            //由Socket对象得到字节输出流
            out = socket.getOutputStream();

            //记录首次连接时间,作为之后是否发送心跳报文的判断
            LAST_HEARTBEAT_TIME = new Date();

            //todo 回复报文的标识符,根据通讯协议实际情况修改
            byte[] callResponseData = {SocketPacket.STATUS_CALL};

            //循环read并判断是否有内容上报,是否需要发送心跳。
            byte[] message;
            while (isRunning) {
                //如果下次心跳发送时间<0 直接发送心跳 更新上次心跳发送时间,将下次心跳发送时间更新为心跳间隔
                if (NEXT_HEARTBEAT_MS < 0) {
                    sendHeartBeat();
                    LAST_HEARTBEAT_TIME = new Date();
                    NEXT_HEARTBEAT_MS = HEART_BEAT;
                } else {
                    //如果下次心跳发送时间>0 更新下次心跳发送时间为 心跳间隙-(当前时间-上次心跳发送时间)
                    NEXT_HEARTBEAT_MS = (int) (HEART_BEAT - (new Date().getTime() - LAST_HEARTBEAT_TIME.getTime()));
                }
                try {
                    byte head = (byte) input.read();
                    //todo 我的通讯协议里,报文以0x7B开始,实际情况根据自己的通讯协议修改
                    if (head == (byte) 0x7B) {
                        message = handleRead();
                        sendCallResponse(message, callResponseData);
                    } else if (head == -1) {
                        isRunning = false;
                    }
                } catch (SocketTimeoutException e) {
                    log.info("Socket read timeout.......");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            //report一个事件 表示连接被关闭
            log.error("SocketServer error: [{}]", e.getMessage());
        } finally {
            //妥善的关闭输入和输出流,关闭socket服务器。
            if (socket != null) {
                try {
                    input.close();
                    out.close();
                    socket.close();
                    log.info("socket close success");
                } catch (Exception e) {
                    socket = null;
                    log.error("Socket close finally Error:[{}]", e.getMessage());
                }
            }
        }
    }

    /**
     * 单字节read输入流传入的内容,得到字节数组
     * 本项目中以通讯协议的开头和结尾的字节0x7B来判断,根据自己的需求更换读取方式
     * @return
     * @throws IOException
     */
    private byte[] handleRead() throws IOException {
        //初始化长度1024
        byte[] response = new byte[1024];
        //记录获取到的字节个数
        int i = 1;

        //把已经获取到的head填充到response中
        response[0] = 0x7B;

        //todo 0x7B结尾,当再次读取到0x7B的时候跳出循环 根据实际通讯协议更改
        for (byte oneByte = (byte) input.read(); oneByte != 0x7B; i++) {
            response[i] = oneByte;
            System.out.print(oneByte);
            oneByte = (byte) input.read();
        }
        //填充尾部
        response[i] = 0x7B;
        //截取需要的长度并给到一个新的字节数组中,返回
        byte[] result = new byte[i + 1];
        System.arraycopy(response, 0, result, 0, i + 1);
        return result;
    }

    /**
     * 发送心跳报文
     * @throws IOException
     */
    private void sendHeartBeat() throws IOException {
        //todo 具体实现
    }

    /**
     * 回复报文
     * @param message
     * @param callResponseData
     * @throws Exception
     */
    private void sendCallResponse(byte[] message, byte[] callResponseData) throws Exception {
        //分别显示收到的16进制 和处理后的16进制
        String hexString = SocketPacket.bytesToHexString(message).toUpperCase();
        log.info("Server Receive: [{}]", hexString);
        //todo 传入16进制字符串,交给SocketPacket去解析通讯协议 根据实际情况实现 SocketPacket的Builder
        SocketPacket receivePacket = new SocketPacket.Builder(message).build();
        log.info("Server Handle:  [{}]", receivePacket.getHexString());

        //todo 以下根据实际需求实现 根据标识符判断上报的报文类型,回复对应信息
        if (receivePacket.getCmd() == SocketPacket.STATUS_CALL) {
            SocketPacket sendPacket = new SocketPacket.Builder(SocketPacket.STATUS_RESPONSE).setData(callResponseData).build();
            log.info("Server Send Call Response: [{}]", sendPacket.getHexString());
            out.write(sendPacket.getPacket());
            out.flush();
        }
    }
}
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Random;

/**
 * Created by Zoey on 2019/01/16
 */
public class SocketPacket {
    private Logger log = LoggerFactory.getLogger(getClass());

    private final byte head;
    private final byte[] len;
    private final byte[] dstAddr;
    private final byte[] srcAddr;
    private final byte cmd;
    private final byte[] data;
    private final byte[] checkSum;
    private final byte end;

    //todo 根据项目实际情况取舍 命令字 服务端:01注册请求 02注册成功 04特征码接收成功
    protected final static byte REGISTER_REQUEST = (byte) 0x01;
    protected final static byte REGISTER_SUCCESS = (byte) 0x02;
    protected final static byte STATUS_CALL = (byte) 0x21;
    protected final static byte STATUS_RESPONSE = (byte) 0x04;
    protected final static byte HEAT_BEAT = (byte) 0x31;

    private SocketPacket(Builder builder) {
        this.head = builder.head;
        this.len = builder.len;
        this.dstAddr = builder.dstAddr;
        this.srcAddr = builder.srcAddr;
        this.cmd = builder.cmd;
        this.data = builder.data;
        this.checkSum = builder.checkSum;
        this.end = builder.end;
    }

    public static class Builder {
        //必须参数
        private byte[] len = new byte[2];
        private byte cmd;
        private byte[] checkSum = new byte[2];

        // 可选参数
        private byte[] data;
        private byte head = (byte) 0x7B;
        private byte end = (byte) 0x7B;
        private byte[] srcAddr = {(byte) 0x99, (byte) 0x99};
        //客户端地址 默认0000为群地址,任何设备都接收 回复设备注册验证时需要根据客户端发送的地址携带客户端地址
        private byte[] dstAddr = {(byte) 0x00, (byte) 0x00};
        
        public Builder(byte cmd) {
            this.cmd = cmd;
        }

        /**
         * 获得客户端发送的报文内容,转义并获取
         * 使用System.arraycopy()方法copy
         * @param response
         */
        public Builder(byte[] response) {
            //转义
            response = escapeFromResponse(response);
            //todo 根据通讯协议copy字节填充参数
        }

        /**
         * 使用填充好的参数值新建 SocketPacket 对象并校验
         * @return
         * @throws Exception
         */
        public SocketPacket build() throws Exception {
            //todo 根据实际通讯协议校验
            this.len = intToByteArray(this.data == null ? 7 : this.data.length + 7);//data的长度+2字节长度+2字节目标地址+2字节源地址+1字节命令字
            if (this.len.length != 2) {
                throw new Exception("Error len length: required length:2 exist length:" + this.len.length);
            }
            this.checkSum = checkSum(this);
            if (this.checkSum.length != 2) {
                throw new Exception("Error checkSum length: required length:2 exist length:" + this.checkSum.length);
            }
            return new SocketPacket(this);
        }

        // 每个 setter 方法都返回当前的对象,做到链式调用
        public byte getHead() {
            return head;
        }

        public Builder setHead(byte head) {
            this.head = head;
            return this;
        }

        public byte getEnd() {
            return end;
        }

        public Builder setEnd(byte end) {
            this.end = end;
            return this;
        }

        public Builder setSrcAddr(byte[] srcAddr) {
            this.srcAddr = srcAddr;
            return this;
        }

        public byte[] getDstAddr() {
            return dstAddr;
        }

        public Builder setDstAddr(byte[] dstAddr) {
            this.dstAddr = dstAddr;
            return this;
        }

        public byte[] getData() {
            return data;
        }

        public Builder setData(byte[] data) {
            this.data = data;
            return this;
        }
    }

    public byte getCmd() {
        return cmd;
    }

    public byte[] getSrcAddr() {
        return srcAddr;
    }


    /**
     * 获取需要发送的报文的转义后的报文内容
     * @return
     */
    protected byte[] getPacket() {
        byte[] checkData = new byte[this.data == null ? 11 : this.data.length + 11];
        checkData[0] = this.head;
        System.arraycopy(this.len, 0, checkData, 1, 2);
        System.arraycopy(this.dstAddr, 0, checkData, 3, 2);
        System.arraycopy(this.srcAddr, 0, checkData, 5, 2);
        checkData[7] = this.cmd;
        if (this.data != null) {
            System.arraycopy(this.data, 0, checkData, 8, this.data.length);
        }
        System.arraycopy(this.checkSum, 0, checkData, this.data == null ? 8 : this.data.length + 8, 2);
        checkData[checkData.length - 1] = this.end;
        return escapeFromRequest(checkData);
    }

    protected String getHexString() {
        return bytesToHexString(getPacket()).toUpperCase();
    }


    private static byte[] checkSum(Builder builder) {
        byte[] checkData = new byte[builder.data == null ? 7 : builder.data.length + 7];

        System.arraycopy(builder.len, 0, checkData, 0, 2);
        System.arraycopy(builder.dstAddr, 0, checkData, 2, 2);
        System.arraycopy(builder.srcAddr, 0, checkData, 4, 2);
        checkData[6] = builder.cmd;
        if (builder.data != null) {
            System.arraycopy(builder.data, 0, checkData, 7, builder.data.length);
        }

        int checksum = makeChecksum(bytesToHexString(checkData));
        System.out.println("checkSum[" + bytesToHexString(intToByteArray(checksum)) + "]");
        return intToByteArray(checksum);
    }

    /**
     * Convert byte[] to hex string.这里我们可以将byte转换成int,然后利用Integer.toHexString(int)来转换成16进制字符串。
     *
     * @param src byte[] data
     * @return hex string
     */
    public static String bytesToHexString(byte[] src) {
        StringBuilder stringBuilder = new StringBuilder("");
        if (src == null || src.length <= 0) {
            return null;
        }
        for (int i = 0; i < src.length; i++) {
            int v = src[i] & 0xFF;
            String hv = Integer.toHexString(v);
            if (hv.length() < 2) {
                stringBuilder.append(0);
            }
            stringBuilder.append(hv);
        }
        return stringBuilder.toString();
    }

    /**
     * Convert hex string to byte[]
     *
     * @param hexString the hex string
     * @return byte[]
     */
    private static byte[] hexStringToBytes(String hexString) {
        if (hexString == null || hexString.equals("")) {
            return null;
        }
        hexString = hexString.toUpperCase();
        int length = hexString.length() / 2;
        char[] hexChars = hexString.toCharArray();
        byte[] d = new byte[length];
        for (int i = 0; i < length; i++) {
            int pos = i * 2;
            d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
        }
        return d;
    }

    /**
     * Convert char to byte
     *
     * @param c char
     * @return byte
     */
    private static byte charToByte(char c) {
        return (byte) "0123456789ABCDEF".indexOf(c);
    }

    /**
     * 校验和
     * @param hexdata
     * @return
     */
    private static int makeChecksum(String hexdata) {
        if (hexdata == null || hexdata.equals("")) {
            return 0;
        }
        hexdata = hexdata.replaceAll(" ", "");
        int total = 0;
        int len = hexdata.length();
        if (len % 2 != 0) {
            return 0;
        }
        int num = 0;
        while (num < len) {
            String s = hexdata.substring(num, num + 2);
            total += Integer.parseInt(s, 16);
            num = num + 2;
        }
        return ~total;
    }

    /**
     * byte数组中取int数值,本方法适用于(低位在前,高位在后)的顺序,和intToBytes()配套使用
     *
     * @param src byte数组
     * @return int数值
     */
    protected static int bytesToInt(byte[] src) {
        int value;
        value = (int) ((src[0] & 0xFF)
                | ((src[1] & 0xFF) << 8));
        return value;
    }

    /**
     * 计算报文长度并返回一个低位在前高位在后的长度为2的byte数组
     * 将int 转化成低位在前高位在后的byte数组
     * @param n
     * @return
     */
    protected static byte[] intToByteArray(int n) {
        byte[] result = new byte[2];
        result[0] = (byte) (n & 0xff);
        result[1] = (byte) (n >> 8 & 0xff);
        return result;
    }

    /**
     * 生成一个四位随机数
     * @return
     */
    protected static byte[] randomByte() {
        Random random = new Random();
        byte[] result = new byte[4];
        result = ArrayUtils.addAll(intToByteArray(random.nextInt(256)), intToByteArray(random.nextInt(256)));
        return result;
    }

    /**
     * 对收到的报文进行提前转义
     * @param response
     * @return
     */
    protected static byte[] escapeFromResponse(byte[] response){
        //todo 根据实际通讯协议更改
        String hexString = bytesToHexString(response).toUpperCase();
        hexString = hexString.replace("7A02","7B").replace("7A01","7A");
        return hexStringToBytes(hexString);
    }

    /**
     * 对回复的报文进行转义
     * @param request
     * @return
     */
    protected static byte[] escapeFromRequest(byte[] request){
        //todo 根据实际通讯协议更改
        String hexString = bytesToHexString(request).toUpperCase();
        hexString = hexString.replaceAll("^7B","##").replaceAll("7B$","##")
                .replace("7A","7A01").replace("7B","7A02").replace("##","7B");
        return hexStringToBytes(hexString);
    }
}

你可能感兴趣的:(Socket通讯(阻塞式、多线程IO服务端 TCP长链接 ) 实现)