需求
客户的网络接收机可以自动通过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);
}
}