视频库之断点续传

        近日公司有个项目要做一个视频库,故开始阶段性研究。首先先要研究断点续传。

        第一步:打算提高上传视频的时间,上传的时间分成两部分,读流和写入GFS。想做一个实验来验证到底时间花费在了哪里,故在程序中加入了打印语句。如下:

      

	@Override
	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		PrintWriter out = response.getWriter();
		
		Date date = new Date();
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		
		System.out.println("1="+sdf.format(new Date()));

		try {
			ServletFileUpload upload = new ServletFileUpload();
			upload.setFileSizeMax(fileSizeMax * 1024 * 1024);
			FileItemIterator iter = upload.getItemIterator(request);
			while(iter.hasNext()){
				FileItemStream item = iter.next();
				String fileName = item.getName();
				InputStream stream = item.openStream();
			
				String fn = item.getFieldName();
				System.out.println("2="+sdf.format(new Date()));
				if (!item.isFormField() && !StringUtil.isEmpty(fn)) {
					
					System.out.println("3(1)="+sdf.format(new Date()));
					String cloudFileName = videoCloudStorage.upload(fileName,stream);
					System.out.println("3(2)="+sdf.format(new Date()));
					out.write(ReturnMessageUtil.createOKMsg(cloudFileName));
				}
			}
		} catch (Exception e) {
			logger.error("上传文件失败!", e);
//			out.write(ReturnMessageUtil.createErrorMsg("upload failed!"));
			response.sendError(500, "上传文件失败!");
		}
         System.out.println("4="+sdf.format(new Date()));
	}

    打印结果为:

1=2015-07-29 17:50:13
2=2015-07-29 17:50:13
2=2015-07-29 17:50:13
33(1)=2015-07-29 17:50:13
33(2)=2015-07-29 17:50:19
2=2015-07-29 17:50:19
4=2015-07-29 17:50:19

 可以看出时间都花在了往云存储上传输。当时觉得客户端往weblogic传流是秒传的,weblogic是在得到了全部流后才往云存储上传输。然后又做实验,这次是不传云存储了,而是直接把流写文件到weblogic上,代码入下:

File file = new File("//data//weblogic//1.mp4");
					FileOutputStream fos = new FileOutputStream(file);
					BufferedOutputStream bos = new BufferedOutputStream(fos);

					BufferedInputStream bis = new BufferedInputStream(stream);
					try {
						byte[] buffer = new byte[2048];
						int len = -1;
						while ((len = bis.read(buffer)) != -1) {
							// 输出读入的数据
							bos.write(buffer, 0, len);
						}
						bos.close();

					} catch (IOException e) {
						e.printStackTrace();
					}

    这次再打印语句,发现传云存储和直接传服务器上生成文件时间基本一样,但是这个还是无法判断出来到底时间是花在了读取流上还是花在了写文件上,然后又做了一个实验,就是把获取的流不写入文件了,而是读取后什么都不做,直接注掉上面的bos.write()方法,再跑一遍,发现结果还是一样,所以只能得出的结论是,流并不是一次性就传到了weblogic上,而是只是打开了一个通道,当

 while ((len = bis.read(buffer)) != -1) {

    的时候才是去真正读取流,所以真正费时间还是在读取流上而非写入文件。但是FLASH端说只能单线程传流,所以看来优化上传还是要想别的办法。

  

      第二步:秒传

       用户点击上传按钮,FLASH先发送请求给后端,后端从数据库中用MD5判断验证是否已经上传过(且
上传完整)该视频,如果上传过,则实现秒传(即为直接返回服务器存储地址,不用再上传),如果上传过,但是没有上传全部,则给FLASH返回一个
已上传文件大小,FLASH重新分份后继续发送剩下的文件流给服务端。
       知识点:MD5生成散列码

       java.security.MessageDigest类用于为应用程序提供信息摘要算法的功能,如 MD5 或 SHA 算法。简单点说就是用于生成散列码信息摘要是安全的单向哈希函数,它接收任意大小的数据,输出固定长度的哈希值。

      用法如下:

     

public static void main(String[] args){
		  try {
			String str = "abcd";
			MessageDigest com=MessageDigest.getInstance("MD5"); //生成MessageDigest对象
			com.update(str.getBytes("UTF-8"));  //传入需要计算的字符串
			byte[] b = com.digest(); //计算消息摘要
			System.out.println(convertToHexString(b));
			
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	  }
	  
	  static String convertToHexString(byte data[]) {
		  StringBuffer strBuffer = new StringBuffer();
		  for (int i = 0; i < data.length; i++) {
		   strBuffer.append(Integer.toHexString(0xff & data[i]));
		  }
		  return strBuffer.toString();
      }

     输出结果为:e2fc714c4727ee9395f324cd2e7f331f  (固定长度的)

     如果是想获得文件的散列码,则代码为:

    

public class Auth{

	  public static byte[] create(String filename)   throws Exception{
	  InputStream fis = new FileInputStream(filename);
	  byte[] buf= new byte[1024];
	  MessageDigest com=MessageDigest.getInstance("MD5");
	  int num;
	  do{
	   num=fis.read(buf);
	   if(num>0){
	     com.update(buf,0,num);
	   }
	  }while(num!=-1);

	  fis.close();
	  return com.digest();
	  }

	  
	  public static void main(String[] args){
		  try {
			byte[] fileA =create("E:\\1.jpg");
			System.out.println(convertToHexString(fileA));		
			
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	  }
	  
	  static String convertToHexString(byte data[]) {
		  StringBuffer strBuffer = new StringBuffer();
		  for (int i = 0; i < data.length; i++) {
		   strBuffer.append(Integer.toHexString(0xff & data[i]));
		  }
		  return strBuffer.toString();
      }  
	}

     输出结果为:4bca3e9cec71f8ad3601ddd31d7ceb

     第三步:合并文件

     断点续传的核心就是分块上传,然后再进行合并,我们先来了解一下合并所需的类RandomAccessFile。

    

RandomAccessFile类是随机读取类,它是一个完全独立的类。

适用于由大小已知的记录组成的文件,所以我们可以使用seek()将记录从一处转移到另一处,然后读取或者修改记录。

文件中记录的大小不一定都相同,只要能够确定哪些记录有多大以及它们在文件中的位置即可。

RandomAccessFile类可以实现对文件内容的读写操作,但是比较复杂。所以 一般操作文件内容往往会使用字节流或字符流方式
(1)写入数据
当用 rw 方式声明RandomAccessFile对象时,如果要写入的文件不存在,系统将自行创建。 
r 为只读; w 为只写; rw 为读写。 
为了保证可以进行随机读取,所有写入的名字都是8个字节,写入的数字都是固定的4个字节。
 
import java.io.File;
import java.io.RandomAccessFile;
 
public  class RandomAccessFileDemo01 {
       //  所有的异常直接抛出,程序中不再进行处理
       public  static  void main(String args[])  throws Exception {
           File f =  new File("d:" + File.separator + "test.txt");  //  指定要操作的文件
           RandomAccessFile rdf =  null//  声明RandomAccessFile类的对象
           rdf =  new RandomAccessFile(f, "rw"); //  读写模式,如果文件不存在,会自动创建
           String name =  null;
            int age = 0;
           name = "zhangsan";  //  字符串长度为8
           age = 30;  //  数字的长度为4
           rdf.writeBytes(name);  //  将姓名写入文件之中
           rdf.writeInt(age);  //  将年龄写入文件之中
           name = "lisi    ";  //  字符串长度为8
           age = 31;  //  数字的长度为4
           rdf.writeBytes(name);  //  将姓名写入文件之中
           rdf.writeInt(age);  //  将年龄写入文件之中
           name = "wangwu  ";  //  字符串长度为8
           age = 32;  //  数字的长度为4
           rdf.writeBytes(name);  //  将姓名写入文件之中
           rdf.writeInt(age);  //  将年龄写入文件之中
           rdf.close();  //  关闭
      }
};
 

(2)读取数据

读取是直接使用 r 的模式即可,以只读的方式打开文件。
读取时所有的字符串只能按照byte数组方式读取出来,而且长度必须和写入时的固定大小相匹配。 
 
import java.io.File;
import java.io.RandomAccessFile;

public  class RandomAccessFileDemo02{
     //  所有的异常直接抛出,程序中不再进行处理
     public  static  void main(String args[])  throws Exception{
        File f =  new File("d:" + File.separator + "test.txt") ;     //  指定要操作的文件
        RandomAccessFile rdf =  null ;         //  声明RandomAccessFile类的对象
        rdf =  new RandomAccessFile(f,"r") ; //  以只读的方式打开文件
        String name =  null ;
         int age = 0 ;
         byte b[] =  new  byte[8] ;     //  开辟byte数组
        
//  读取第二个人的信息,意味着要空出第一个人的信息
        rdf.skipBytes(12) ;         //  跳过第一个人的信息
         for( int i=0;i<b.length;i++){
            b[i] = rdf.readByte() ;     //  读取一个字节
        }
        name =  new String(b) ;     //  将读取出来的byte数组变为字符串
        age = rdf.readInt() ;     //  读取数字
        System.out.println("第二个人的信息 --> 姓名:" + name + ";年龄:" + age) ;
         //  读取第一个人的信息
        rdf.seek(0) ;     //  指针回到文件的开头
         for( int i=0;i<b.length;i++){
            b[i] = rdf.readByte() ;     //  读取一个字节
        }
        name =  new String(b) ;     //  将读取出来的byte数组变为字符串
        age = rdf.readInt() ;     //  读取数字
        System.out.println("第一个人的信息 --> 姓名:" + name + ";年龄:" + age) ;
        rdf.skipBytes(12) ;     //  空出第二个人的信息
         for( int i=0;i<b.length;i++){
            b[i] = rdf.readByte() ;     //  读取一个字节
        }
        name =  new String(b) ;     //  将读取出来的byte数组变为字符串
        age = rdf.readInt() ;     //  读取数字
        System.out.println("第三个人的信息 --> 姓名:" + name + ";年龄:" + age) ;
        rdf.close() ;                 //  关闭
    }
};
 

运行结果

第二个人的信息 --> 姓名:lisi    ;年龄:31
第一个人的信息 --> 姓名:zhangsan;年龄:30
第三个人的信息 --> 姓名:wangwu  ;年龄:32
初步了解了RandomAccessFile类,下面我们演示下如何合并文件,代码如下:
    	  File file = new File("e:\\file.txt");  //文件内容为:今天挣了
    	  File file1 = new File("e:\\file1.txt");  //文件内容为:1000元
    	 
    	  RandomAccessFile file2 = new RandomAccessFile("e:\\file2.txt","rw");
    	
    	  BufferedReader is = new BufferedReader(new FileReader(file));
    	  BufferedReader is1 = new BufferedReader(new FileReader(file1));
    	  
    	  String line = "";
    	  
    	  while((line = is.readLine())!=null){
    		  file2.write(line.getBytes());
    	  }
    	  
    	  file2.seek(file2.getFilePointer());
    	  while((line = is1.readLine())!=null){
    		  file2.write(line.getBytes());
    	  }
    	  
    	  /**
    	   * 输出file2的内容
    	   */
    	  RandomAccessFile file3 = new RandomAccessFile("e:\\file2.txt","r");
    	  byte[] b = new byte[(int)file3.length()];
    	  byte[] c = new byte[(int)file3.length()];
    	  
    	  for(int i=0;i<b.length;i++){
    		  c[i] = file3.readByte();
    	  }
    	  
    	  String aa = new String(c);
    	  System.out.println(aa);
    	 
    	  file2.close();
    输出结果为:今天挣了1000元。
 
 第四步:验证Flash身份
       因为上传服务要提供web服务,所以需要每次验证下Flash的身份,这里我们采用MD5验签的方法,如果是java间也可以采用公钥私钥的方法。
      这里采用的方式是双方规定一种加密的格式,然后把Flash端Md5加密的结果和服务器端Md5加密的结果进行比较,如果相同则认为验证成功。
      代码如下:
     
		String clientSignMd5 = request.getParameter("sign");
		
		MD5 md5 = new MD5();		
		Date date = new Date();

		SimpleDateFormat sdf  = new SimpleDateFormat("yyyyMMdd");
		String serversign = AppType.SPACE + "-xxxxxxxxx-" + sdf.format(date);
		String serverSignMd5 = md5.encodeByMD5(serversign);
		
		if(!clientSignMd5.equals(serverSignMd5)){
			return ReturnMessageUtil.createErrorMsg("没有上传权限");
		}
 
第五步:ActiveMQ 发送转码请求。这里参考另一篇博客《ActiveMq实例》。
第六步:转码并截图。这里参考另一篇博客《Ffmpeg实例》。
第七步:合成图片。因为FLASH端放视频的时候,需要有视频图片的概览,就是每几分钟显示一张缩略图,但是如果每张图都要从FLASH端去读取的话,那么对前端的压力会很大,故把所有的缩略图合成一张大图,这样FLASH只需要请求一次这个大图就成了,然后FLASH端自己根据给定的尺寸切图显示。
至此就完成了一整套上传视频-转码抽图的功能了。
 
 
 

猜你喜欢

转载自zy116494718.iteye.com/blog/2231559