tcp字节传输(java)-自定义包头和数据识别

1、背景

tcp传输的时候会自动拆包,因此服务端接收的数据段可能跟客户端发送过来的数据段长度不一致,比如客户端一次发送10000个字节。但是服务端接收了两次才接收完整(例如第一次接收6000字节,第二次接收4000字节)。但是服务端每次必须要接收完所有的字节才能进行处理,而且客户端每次发的数据长度都不一致。
于是经过协商,客户端每次发送数据段时,在数据段前加10个字节(后面统一称数据包头),前6个字节为数据包起始标识符,后4个字节为此次发送数据段的长度。

2、难点

因为tcp会拆包,所以数据段前的10个字节可能会出现在任何位置,也可能会出现在两次tcp传输过程中。另外如果包头前6个字节不是指定的标识,要向后顺延,直到找出包头。

3、思路

1)使用两个ByteBuffer对象,一个记录数据段前的10个字节,该对象仅创建一次。另一个ByteBuffer对象存储去除包头后的完整的数据段信息,该对象在每次接收新的包头时,都会根据包头的后4个字节重新创建(因为jvm的自动垃圾回收,所以这里不用担心内存溢出问题)。

2)接收完整的数据段后,如果还有多余数据则使用迭代方式处理。

4、java代码实现

1、这里只列出了核心代码,相关逻辑需要自己补全

2、创建tcp服务端代码
try (ServerSocket ss = new ServerSocket(port)) {
	while (true) {
		Socket socket = ss.accept();
		new SocketHandler(socket, eqpmtId, port, save).start();
	}
} catch (Exception e) {
	log.error("TCP服务端创建异常,端口为{},异常为\n", this.port, e);
}

3、tcp服务端详细处理代码
@Slf4j
class SocketHandler extends Thread {

    private Socket socket;

    private String eqpmtId;

    private Integer port;

    private boolean save;

    public SocketHandler(Socket socket, String eqpmtId, Integer port, boolean save) {
        this.socket = socket;
        this.eqpmtId = eqpmtId;
        this.port = port;
        this.save = save;
    }


    @Override
    public void run() {
        log.info("与{},{}建立消息socket通信", eqpmtId, port);
        try (InputStream inputStream = socket.getInputStream();
             FileOutputStream os = new FileOutputStream(new File("D:\\tmp-data\\" + System.currentTimeMillis() + ".h264"));) {
            byte[] buffer = new byte[64 * 1024];
            int len = 0;
            ByteBuffer dataBuffer = null;
            ByteBuffer headBuffer = ByteBuffer.allocate(10);
            while (socket.isConnected() && !socket.isClosed()) {
                if ((len = inputStream.read(buffer)) != -1) {
                    log.info("收到数据包len={}", len);
                    try {
                        dataBuffer = getDataBuffer(buffer, 0, len, headBuffer, dataBuffer);
                    } catch (Exception e) {
                        log.error("接收数据异常,重新开始接收...\n",e);
                        headBuffer.clear();
                        dataBuffer.clear();
                    }
                } else {
                    log.info("没有数据,休眠1秒,否则cpu会飙升");
                    Thread.sleep(1000);
                }
            }
        } catch (Exception e) {
            log.error("socket传输异常,异常为\n", this.port, e);
        }
        log.info("关闭与},{}消息socket通信", eqpmtId, port);
    }


    private ByteBuffer getDataBuffer(byte[] buffer, int start, int end, ByteBuffer headBuffer, ByteBuffer dataBuffer) {
        int offset = start;
        int tmpLen = 0;
        //先找到包头
        if (headBuffer.position() < headBuffer.capacity()) {
            //当前数组长小于包头长度有,整个数组放入头缓存后返回
            int len = end - offset;
            if (len < headBuffer.capacity() - headBuffer.position()) {
                headBuffer.put(buffer, offset, len);
                return dataBuffer;
            }

            tmpLen = headBuffer.capacity() - headBuffer.position();
            headBuffer.put(buffer, offset, headBuffer.capacity() - headBuffer.position());
            offset = offset + tmpLen;
            //包头缓存填充满了,判断包头是否正确
            if (!isHead(headBuffer.array())) {
                //包头不正确,则不断向后移位直到找到包头
                log.info("包头有问题,向后移动一位继续校验");
                int headLastIndex = headBuffer.capacity() - 1;
                for (; offset < end; offset++) {
                    for (int i = 0; i < headLastIndex; i++) headBuffer.put(i, headBuffer.get(i + 1));
                    headBuffer.put(headLastIndex, buffer[offset]);
                    if (isHead(headBuffer.array())) break;
                }
                //移位结束确认是找到了包头还是当前数组已经遍历完
                if (!isHead(headBuffer.array())) {
                    headBuffer.position(headLastIndex);
                    return dataBuffer;
                }
            }
            //包头正确后,解析获取数据包有多长,并创建对应的缓存对象
            int dataLen = dataLen(headBuffer.array());
            log.info("包头设定长度为{}", dataLen);
            dataBuffer = ByteBuffer.allocate(dataLen);
        }

        if (offset == end) return dataBuffer;

        //如果可以填充满数据缓存对象,则发送数据包,并清理缓存
        if (end - offset >= dataBuffer.capacity() - dataBuffer.position()) {
            tmpLen = dataBuffer.capacity() - dataBuffer.position();
            dataBuffer.put(buffer, offset, dataBuffer.capacity() - dataBuffer.position());
            offset = offset + tmpLen;
			
			/** 收到完整数据包,进行处理,注意这里的函数要替换成自己的处理逻辑 **/
            sendData(dataBuffer, null);
			
            dataBuffer.clear();
            headBuffer.clear();
            if (offset == end) return dataBuffer;
            //迭代处理剩下的数据
            return getDataBuffer(buffer, offset, end, headBuffer, dataBuffer);
        }
        //如果不能填充慢数据缓存对象,则整个数据放入后返回
        dataBuffer.put(buffer, offset, end - offset);
        return dataBuffer;
    }


    //判断是否为包头
    public boolean isHead(byte[] buffer) {
        if (buffer == null || buffer.length < 10) return false;
        int b1 = buffer[0];
        int b2 = buffer[1];
        int b3 = buffer[2];
        int b4 = buffer[3];
        int b5 = buffer[4];
        int b6 = buffer[5];
        String s = "" + b1 + b2 + b3 + b4 + b5 + b6;
        if ("001001".equals(s)) return true;
        return false;
    }

	//判断数据包的长度(ByteUtil用的hutool工具包里的类,也可以自己实现)
    public int dataLen(byte[] buffer) {
        return ByteUtil.bytesToInt(new byte[]{buffer[6], buffer[7], buffer[8], buffer[9]});
    }

}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
QT是一种跨平台的C++开发框架,可以方便地实现TCP字节的发送和接收。为了实现自定义包头和内容的发送和接收,我们可以借助QT提供的QIODevice类和QDataStream类。 在发送端,首先我们需要创建一个QTcpSocket对象,并连接到服务器。然后,我们创建一个QByteArray对象,用于存储包头和内容。接下来,我们使用QDataStream对象将数据写入到QByteArray中,可以使用writeXXX方法,比如writeInt、writeString等。写入的顺序应该与接收端相对应。最后,我们将QByteArray通过QTcpSocket的write方法发送到服务器。 在接收端,首先我们需要创建一个QTcpServer对象,并监听指定的端口。当有新的连接到来时,我们使用QTcpServer的nextPendingConnection方法获得与客户端连接的QTcpSocket对象。接下来,我们通过QTcpSocket的read方法读取到来自客户端的数据,并将其写入到QByteArray中。然后,我们使用QDataStream对象从QByteArray中依次读取数据,可以使用readXXX方法来读取。读取的顺序应该与发送端相对应。最后,我们处理读取到的数据,进行进一步的操作。 在发送端和接收端,我们都需要约定好包头的格式和内容的顺序,以确保数据的正确传输和解析。可以使用固定长度的包头,比如4个字节表示包头长度,或者使用特殊字符来标识包头的开始与结束等等。根据具体的需求和情况,我们可以自定义自己的包头和内容的格式。 总之,通过QT提供的QTcpSocket、QByteArray和QDataStream等类,我们可以方便地实现TCP字节的发送和接收,并且可以自定义包头和内容的格式和顺序。这样可以帮助我们更好地满足具体的通信需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值