前言 19年那会我还是用java去写的tcp,当然是另一个项目,也没有做沾包处理,今天重新把原来的项目给加上了粘包处理,当然还是选择用java语言.直接在原先的类上直接修改的.
这是我用kotlin写的一个TCP粘包处理,思路也是同样的,只是写法不同,有兴趣可以去看下.Android Kotlin语言实现+TCP客户端开发+粘包处理
class TcpService implements Runnable
创建一个TCP连接类,该类实现Runnable,连接的过程中需要在子线程中去处理,所以我直接让该类实现接口.
public TcpService(OnListenerIndustrialObject objectCallBack) {
this.objectCallBack = objectCallBack;
isconnect = true;
// 放两个线程 一个发送的线程 一个读取接收消息的线程
executorService = Executors.newFixedThreadPool(2);
}
创建一个构造方法,初始化一些该类所需要的参数.我这方法里使用接口回调,回调一些当前的状态信息等,变量isconnect改变当前的是否可以连接TCP的状态 这里创建了一个只有两个线程的线程池.一个是发送消息的.一个是处理粘包的.
/**
* 设置ip地址
*/
public TcpService setIp(String ip) {
this.ip = ip;
return this;
}
public TcpService setProt(int prot) {
this.prot = prot;
return this;
}
开始之前需要传输对应的IP端口号,返回本类该对象.也可以链式调用.
@Override
public void run() {
readQueueData();
Log.i(TAG, "tcp run");
while (isconnect) {
try {
socket = new Socket();
SocketAddress socketAddress = new InetSocketAddress(ip, prot);
Log.i(TAG, "" + ip + "," + prot);
socket.connect(socketAddress, 30000);
if (socket.isConnected()) {
createIo();
}
//连接成功发送注册命令
sendCommand("00030000020000000a01000000000000000000");
//tcp收到命令之后返回来的数据
readTCPData();
} catch (Exception e) {
e.printStackTrace();
Log.i(TAG, "tcp connect exception");
Log.i(TAG, "TCP reconnects in 5 seconds");
objectCallBack.isNetWorkNormally(false);
closeTcp();
try {
Thread.sleep(5000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
Log.i(TAG, "tcp end");
}
同样把这些写在循环当中,以防止因某些情况断开连接导致无法重新连接 readQueueData()这个方法就是处理粘包的问题,这个放在最后讲
private void createIo() throws Exception {
in = new DataInputStream(socket.getInputStream());
out = new DataOutputStream(socket.getOutputStream());
}
连接成功之后创建SocketIO流,接收发送消息用
/**
* 发送命令
*
* @param registerCommand 命令
*/
synchronized void sendCommand(final String registerCommand) {
executorService.execute(new Runnable() {
@Override
public void run() {
try {
if (out != null) {
out.write(CodeUtil.hex2byte(registerCommand));
out.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
开启发送线程,这个命令权限公开出去,在外部可以直接调用
private void readTCPData() {
try {
byte[] bytes = new byte[1024];
int len = 0;
if (in != null) {
objectCallBack.isNetWorkNormally(true);
while ((len = in.read(bytes)) != -1) {
for (int i = 0; i < len; i++) {
queueData.offer(bytes[i]);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
开启接收命令的方法. 可以看到在接收命令中我们把采集的数据都放在一个队列当中.我们之前开重写的run方法中第一个的方法readQueueData()就是处理这个字节队列的
private ConcurrentLinkedQueue<Byte> queueData = new ConcurrentLinkedQueue<>();
我们看一下处理粘包的过程,我直接把思路写在注释里.这样配合代码看起来也不用上下翻. 先看下命令的消息体格式是什么样子
//消息头 他为什么变绿了? 8个字节
18 02 00 00 03 00 00 00
//消息长度 一个字节,此长度包含消息长度本身和消息体和消息结尾,不包含消息头.
2C //转换出来就是44
//消息体 41个字节
FF00FFFE0100000000000000000000000000000000000000000000000000000000000000000000000000
//消息尾 消息尾一般都是两个字节0D0A
0D0A
// 这个和kotlin那篇文章的消息体格式是不相同的 不要弄混了
private void readQueueData() {
executorService.execute(() -> {
int outCount = 0;
// 创建只有一个消息头的字节数组+消息长度的字节 也就是9个字节
byte[] b = new byte[9];
while (true) {
// 持续遍历到第9个字节我们就需要拿它的消息体
if (outCount == 9) {
// 分析当前的消息体的长度
int length = CodeUtil.byteToInt(b[8]);
int inCount = 0;
// 当前的数据长度加上之前的消息头,也就是 44+9
byte[] bytes = new byte[length + 9];
// 长度到达9的时候创建一个新的字节数组,把之后的数据添加到新的字节数组中
while (true) {
Byte inPoll = queueData.poll();
if (inPoll == null) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
bytes[inCount + 9] = inPoll;
if (inCount == length - 1) {
// 消息体拿的数量 和消息体的长度的一致则解析数据,并且跳出内循环
// 继续解析下次的消息体
outCount = 0;
// 把当前的消息头的数组的添加进循环到9的时候创建的数组中
System.arraycopy(b, 0, bytes, 0, 9);
// 解析数据
analysisData(bytes);
break;
}
inCount++;
}
}
Byte poll = queueData.poll();
if (poll == null) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
// 根据当前的遍历的下标去赋值
b[outCount] = poll;
outCount++;
}
});
}
这就是所有的东西了,