Java Socket连续传输多张图片(不断开链接)

Java 专栏收录该内容
10 篇文章 0 订阅

一、问题描述
         我们在做图片传输时,通常都是打开一个http请求,然后去下载一张图片,下载完成后,这个链接就断开了,在这样的情况下,我们最多允许传送一张图片,那么,像刚才那样的做法就不行了。而我的需求是,比如我有100张图片,我想传输完一百张图片在断开链接,或者更夸张点,我做了一个屏幕广播程序,需要把截图来的图片一张张的发给客户端,不想断开,也不能断开,然后我想什么时候断开就断开,不传图片我也不断开。这样的需求,用http请求就不太划算了,因为不断的请求链接然后在断开,是会降低传输效率的。那么有什么办法可以实现像刚才那样的需求呢,办法肯定是有的,接下来,我将我所积累的方法告诉大家。

二、方法
         说到方法,那么我们就要用最原始的通信工具了,那就是Socket,要实现在一个链接上不停的传输图片(比如屏幕广播程序),就需要自定义一个应用层协议,才能实现样的需求,废话不多说,请看我自己定义的协议:

图片数据包头(8byte) | 图片编号(4byte) | 图片名称长度(4byte) | 图片名称 | 图片字节数据长度(8byte) | 图片数据 | 奇偶校验(4byte)

协议解释:
         图片数据包头:定义这个包头就是表示我这是一张图片数据的开始,当我找到这个头之后,才可以去读取图片数据信息,不是随便找一个字节就开始读,那样的话,你永远也得不到一张图片。包头是你自己随意定义的一段特殊的数据,这样才可以和图片数据分开。

         图片编号:为啥是4byte呢,其实它就是一个int型的值,协议中我写了这些单位的,都是Java的基本数据类型(除包头和奇偶校验外),相应的,图片数据 长度就是long型。

         别的协议段不再解释,相信都看得懂,如果你的图片编号超过了int能承受的值,可以使用8byte,也就是long类型。有了这个协议,那么我们就可以传输图片了。

三、实现代码

1、先定义好协议包头,这个是随机的,想怎么定义就怎么定义,越特殊越好。

//协议包头和奇偶校验
 public final static byte[] PICTURE_PACKAGE_HEAD = {(byte) 0xFF, (byte) 0xCF,
            (byte) 0xFA, (byte) 0xBF, (byte) 0xF6, (byte) 0xAF, (byte) 0xFE,
            (byte) 0xFF};
 public final static byte[] PICTURE_PACKAGE_END = {(byte) 0xEF, (byte) 0xDA,
            (byte) 0xFF, (byte) 0xFD};

2、打包协议,并通过socket发送出去。这里我们使用Java的工具类DataOutputStream,它可以发送基本数据类型数据,这样用起来方便,效率也高。

/**
 * 假如图片数据为以下内容
 * int pngNumber = 1001;
 * String pngName = "Image01.png";
 * byte[] pictures; //图片数据,如果不是byte数组,可用工具类进行转化
 **/
DataOutputStream outputData = new DataOutputStream(pictureSocket.getOutputStream());
//发送图片头
outputData.write(PICTURE_PACKAGE_HEAD);
//发送图片编号
outputData.writeInt(pngNumber);
//发送图片名称长度,注意这个长度不是字符个数的长度,而是转化为byte之后的长度,不能弄错了
outputData.writeInt(pngName.getBytes().length);
//发送图片名称
outputData.write(pngName.getBytes());
//发送图片数据长度
outputData.writeLong(pictures.length);
//发送图片
outputData.write(pictures, 0, pictures.length);
//发送包尾
outputData.write(Command.PICTURE_PACKAGE_END);

当你建立好TCP链接后,就可以使用刚才那段代码进行发送图片数据。

3、客户端接收图片数据,并进行解析。

/**
 * 解析图片数据
 **/
public void receivePicture() {
    //判断输入流是否为null
        if (pictureSocket == null)
            return;
        try {
        //标志头状态
            boolean isHead = true;
            //循环读取PICTURE_PACKAGE_HEAD.length个字节,并判断是否和我们定义的头相同
            for (int i = 0; i < PICTURE_PACKAGE_HEAD.length; ++i) {
                byte head = (byte) inputStream.read();
                //如果不相同,那么结束循环,并丢弃这个字节
                if (head != Command.PICTURE_PACKAGE_HEAD[i]) {
                    isHead = false;
                    break;
                }
            }
            //如果找到头了,那么下一个字节就是图片数据的开始
            if (isHead) {
                DataInputStream inputData = new DataInputStream(inputStream);
                //读取图片编号
                int picNumber = inputData.readInt();
                //读取图片名称长度
                int picNameLen = inputData.readInt();
                //读取图片名称
                byte[] data = new byte[picNameLen];
                inputData.readFully(data);
                //将字节转化为字符
                String picName = new String(data,0,data.length);
                //释放data
                data = null;
                //读取图片数据长度
                long picLeng = inputData.writeLong();
                //new 一个字节数据输出流
                ByteArrayOutputStream fos = new ByteArrayOutputStream();
                //每一次读CACHE_SIZE个字节,CACHE_SIZE可以自定义,我的是1024 * 2
                byte[] buffer = new byte[CACHE_SIZE];
                int len = -1;
                //循环读取
                while (picLeng > 0
                        && (len = inputData.read(buffer, 0,
                        picLeng < buffer.length ? (int) picLeng
                                : buffer.length)) != -1) {
                    fos.write(buffer, 0, len);
                    fos.flush();
                    //每读取后,picLeng的值要减去len个,直到picLeng = 0
                    picLeng -= len;
                }
                fos.flush();
                buffer = null;

                // 如果发现picLeng 不为0,说明图片接收不完整,那么就放弃这段数据
                if (picLeng > 0) {
                    return;
                }
                //如果以上操作都成功,那么就取出图片数据
                byte[] pictures = fos.toByteArray();
                //然后使用相应的工具类就可以把pictures转化为图片了

                fos.close();
                fos = null;
            }
            //最后做一下奇偶校验,这里不在写,因为它可有可无,是为了防止包头数据和图片数据冲突,建立的尾部缓冲
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

         通过以上两个方法,我们可以实现在同一个链接里循环的发送图片,而且效率也很高。在这里,我使用的时DataInputStream和DataOutputStream这个两个类,有的项目不允许使用这两个类,那么你也可以只读取相应的字节数,然后在转化为int,long,string等类型的数据。
         如果想要更高的效率,可以使用两个子线程去处理图片数据,一个线程负责接收数据,因为read方法是阻塞的,这样就需要建立一个数据队列,另一个线程不断的到这个队列里去取数据,我试过这样的做法,对技术的要求比较高,特别的对于内存的控制更严格。在嵌入式设备上更是一种挑战。有兴趣的可以去试一试。
         由于代码量大,不便再此贴出,只贴出核心函数,各位理解这个思想,便可以自己去写,如果实在没有看明白的,可以与我讨论。
         我在做的过程中还遇到一个问题,就是我客户端和服务端不仅要发送图片数据,我还要发送一些消息,也就是说不仅仅是图片数据,还有别的,那么这种情况下,在一个通道里传两种数据,肯定是会出事的,所以最好的办法就是:建立两个tcp链接通道,一个专门发送图片数据,一个专门用于发送消息数据。这样便可以解决刚才的问题。

注意:我在网上看到有的网友直接用DataInputStream去图片数据,而不定义数据包头,如果它读取的数据刚好不是你所写的数据,那么这个数据读出来肯定是错误的,所以一定要找到头之后,才能取数据。

         由于小编技术能力有限,文中难免存在错误,还望指正,谢谢合作。

  • 8
    点赞
  • 24
    评论
  • 9
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值