http断点续传 ,支持视频播放支持进度条快进后退

前端封装的视频组件调用视频接口时,不支持进度条快进后退,每次都是从头开始播放,视频播放接口如下:

**
     * @author wb
     * @create_time 2020-03-09
     * @description 视频播放测试接口
     * @param response
     * @throws IOException
     */
    @ApiOperation(httpMethod = "GET",value = "视频播放测试接口" ,notes = "视频播放测试接口")
    @RequestMapping(value = "getVideo", method = RequestMethod.GET)
    public void getVideo(HttpServletResponse response) throws IOException {
        String path = ClassUtils.getDefaultClassLoader().getResource("static/video").getPath();
        File file = new File(path+"/three.wmv" );
        response.setContentType("video/mp4");
        response.setHeader("Accept-Ranges", "bytes");
        try (InputStream in = new FileInputStream(file); ServletOutputStream out = response.getOutputStream();) {
            int length;
            byte[] buffer = new byte[4 * 1024];
            // 向前台输出视频流
            while ((length = in.read(buffer)) > 0) {
                out.write(buffer, 0, length);
            }
        } catch (FileNotFoundException e) {
            System.out.println("文件读取失败, 文件不存在");
        }
    }

后来研究发现视频播放接口使用断点续传,可实现进度条快进后退播放,代码如下:

/**
     * @author wb
     * @create_time 2020-03-10
     * @description 视频断点续传播放测试接口
     * @param request
     * @param response
     */
    @ApiOperation(httpMethod = "GET",value = "视频断点续传播放测试接口" ,notes = "视频断点续传播放测试接口")
    @RequestMapping(value = "/player", method = RequestMethod.GET)
    public void player2(HttpServletRequest request, HttpServletResponse response) {
        String path = ClassUtils.getDefaultClassLoader().getResource("static/video").getPath()+"/three.wmv" ;

        BufferedInputStream bis = null;
        try {
            File file = new File(path);
            if (file.exists()) {
                long p = 0L;
                long toLength = 0L;
                long contentLength = 0L;
                int rangeSwitch = 0; // 0,从头开始的全文下载;1,从某字节开始的(bytes=27000-);2,从某字节开始到某字节结束的(bytes=27000-39000)
                long fileLength;
                String rangBytes = "";
                fileLength = file.length();

                // get file content
                InputStream ins = new FileInputStream(file);
                bis = new BufferedInputStream(ins);

                // tell the client to allow accept-ranges
                response.reset();
                //支持断点续传
                response.setHeader("Accept-Ranges", "bytes");

                // client requests a file block download start byte
                String range = request.getHeader("Range");
                if (range != null && range.trim().length() > 0 && !"null".equals(range)) {
//                    200是OK(一切正常),206是Partial Content(服务器已经成功处理了部分内容),
//                    416 Requested Range Not Satisfiable(对方(客户端)发来的Range 请求头不合理)。
                    response.setStatus(javax.servlet.http.HttpServletResponse.SC_PARTIAL_CONTENT);
                    rangBytes = range.replaceAll("bytes=", "");
                    if (rangBytes.endsWith("-")) { // bytes=270000-
                        rangeSwitch = 1;
                        p = Long.parseLong(rangBytes.substring(0, rangBytes.indexOf("-")));
                        contentLength = fileLength - p; // 客户端请求的是270000之后的字节(包括bytes下标索引为270000的字节)
                    } else { // bytes=270000-320000
                        rangeSwitch = 2;
                        String temp1 = rangBytes.substring(0, rangBytes.indexOf("-"));
                        String temp2 = rangBytes.substring(rangBytes.indexOf("-") + 1, rangBytes.length());
                        p = Long.parseLong(temp1);
                        toLength = Long.parseLong(temp2);
                        contentLength = toLength - p + 1; // 客户端请求的是 270000-320000 之间的字节
                    }
                } else {
                    contentLength = fileLength;
                }

                // 如果设设置了Content-Length,则客户端会自动进行多线程下载。如果不希望支持多线程,则不要设置这个参数。
                // Content-Length: [文件的总大小] - [客户端请求的下载的文件块的开始字节]
                response.setHeader("Content-Length", new Long(contentLength).toString());

                // 断点开始
                // 响应的格式是:
                // Content-Range: bytes [文件块的开始字节]-[文件的总大小 - 1]/[文件的总大小]
                if (rangeSwitch == 1) {
                    String contentRange = new StringBuffer("bytes ").append(new Long(p).toString()).append("-")
                            .append(new Long(fileLength - 1).toString()).append("/")
                            .append(new Long(fileLength).toString()).toString();
                    response.setHeader("Content-Range", contentRange);
                    bis.skip(p);
                } else if (rangeSwitch == 2) {
                    String contentRange = range.replace("=", " ") + "/" + new Long(fileLength).toString();
                    response.setHeader("Content-Range", contentRange);
                    bis.skip(p);
                } else {
                    String contentRange = new StringBuffer("bytes ").append("0-").append(fileLength - 1).append("/")
                            .append(fileLength).toString();
                    response.setHeader("Content-Range", contentRange);
                }
                response.setContentType("video/mp4");
//                String fileName = file.getName();
//                response.setContentType("application/octet-stream");
//                response.addHeader("Content-Disposition", "attachment;filename=" + fileName);
                OutputStream out = response.getOutputStream();
                int n = 0;
                long readLength = 0;
                int bsize = 1024;
                byte[] bytes = new byte[bsize];
                if (rangeSwitch == 2) {
                    // 针对 bytes=27000-39000 的请求,从27000开始写数据
                    while (readLength <= contentLength - bsize) {
                        n = bis.read(bytes);
                        readLength += n;
                        out.write(bytes, 0, n);
                    }
                    if (readLength <= contentLength) {
                        n = bis.read(bytes, 0, (int) (contentLength - readLength));
                        out.write(bytes, 0, n);
                    }
                } else {
                    while ((n = bis.read(bytes)) != -1) {
                        out.write(bytes, 0, n);
                    }
                }
                out.flush();
                out.close();
                bis.close();
            }
        } catch (IOException ie) {
            // 忽略 ClientAbortException 之类的异常
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

 

什么是断点续传?

断点续传其实正如字面意思,就是在下载的断开点继续开始传输,不用再从头开始。所以理解断点续传的核心后,发现其实和很简单,关键就在于对传输中断点的把握。

 

在HTTP/1.1协议没出的时候,也就是HTTP/1.0协议,这种协议不可以使用长链接和断点续传和其他新特性;自从这个1.1被广大使用的现在,很多的下载器都被支持断点续传。

断点续传也就是从下载断开的哪里,重新接着下载,直到下载完整/可用。如果要使用这种断点续传,4个HTTP头不可少的,分别是Range头、Content-Range头、Accept-Ranges头、Content-Length头。这里我讲的是服务端,其中要用Range头是因为它是客户端发过来的信息。服务端是响应,而客户端(浏览器)是请求。

 

Range头必须要了解它,否则没法解析。请求中会带过来的断点信息,一般三种格式。

Range : bytes=50-        意思是从第50个字节开始到最后一个字节

Range : bytes=-70         意思是最后的70个字节

Range : bytes=50-100   意思是从第50字节到100字节 

读取客户端发来的Range头解析为:

假设文件总大小为130字节。

第一种Range   50-130

第二种Range   ( 130 - 70 )-130

第三种Range  50-100

还有一点要晓得的就是返回的HTTP状态码200、206、416这些意义。200是OK(一切正常),206是Partial Content(服务器已经成功处理了部分内容),416 Requested Range Not Satisfiable(对方(客户端)发来的Range 请求头不合理)。

一般处理单线程处理: 客户端发来请求 ——->  服务端返回200  ——> 客户端开始接受数据  ——> 用户手多多把下载停止了 ——> 客户端突然停止接受数据 ——> 然后客户端都没说再见就与服务端断开了 ——> 用户手的痒了又按回开始键 ——> 客户端再次与服务端连接上,并发送Range请求头给服务端 ——> 这时服务端返回是206 ——> 服务端从断开的数据那继续发送,并且会发送响应头:Content-Range给客户端 ——>客户端接收数据 ——>直到完成。

 

再服务端返回206的前面,客户端假如发送了些不合理的Range请求头,服务端就不是返回206而是416。就是结尾字节大于开始字节或者是结尾字节是0什么的,这必定是416的。

单线程通常就是这样,那么我们的客户端是多线程呢,那么我们必定也是多线程。客户端会一次性发来多个请求,来贪婪的快速地下载完成文件。链接别太多就行了。

 

GET /123.zip HTTP/1.1   客户端发来请求了。

那我们告诉它。

HTTP/1.1 200 OK 

Accept-Ranges : bytes   //告诉客户端,我们是支持断点传输的,你知道了吗?

Content-Length : 1900 //文件总大小 

Content-Type : image/jpeg //文件类型

二进制数据。

好了,就这样发送去了。发着发着,咦TM断掉了。我的七舅姥爷姑奶奶,为毛就断掉了呢,包租婆,怎么霎时间摸左水呐。

客户端又发来请求这回有点意思。

GET /123.zip HTTP/1.1 

Range:bytes=580-

大家看到没,会多了怎么一行,我们解析为从580字节开始到1900字节,是要部分内容耶,那么返回什么呢。没错206啊。

 

HTTP/1.1 206 Partial Content

Accept-Ranges : bytes

Content-Type : image/jpeg //文件类型

Content-Length : (1900 - 580) //长度则不是总长度了,而580到1900共有多少字节。

 Content-Range :bytes 580-(1900-1 ) / 1900  //这位同学,我想问问你,为什么结束字节要减1呢。这是因为发来的Range请求头文件下标是0开始,那么结尾数显示也要减1;但是实际上输出的字节是不减1的,完全是写法问题。

 

 

下面一段代码是在做笔记时从网上copy来的,未做验证,原文地址:https://blog.csdn.net/zhaowen25/article/details/41779221

服务端代码

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.StringWriter;
import java.net.ServerSocket;
import java.net.Socket;
 
// 断点续传服务端
public class FTPServer {
 
	// 文件发送线程
	class Sender extends Thread{
		// 网络输入流
		private InputStream in;
		// 网络输出流
		private OutputStream out;
		// 下载文件名
		private String filename;
 
		public Sender(String filename, Socket socket){
			try {
				this.out = socket.getOutputStream();
				this.in = socket.getInputStream();
				this.filename = filename;
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		@Override
		public void run() {
			try {
				System.out.println("start to download file!");
				int temp = 0;
				StringWriter sw = new StringWriter();
				while((temp = in.read()) != 0){
					sw.write(temp);
					//sw.flush();
				}
				// 获取命令
				String cmds = sw.toString();
				System.out.println("cmd : " + cmds);
				if("get".equals(cmds)){
					// 初始化文件
					File file = new File(this.filename);
					RandomAccessFile access = new RandomAccessFile(file,"r");
					//
					StringWriter sw1 = new StringWriter();
					while((temp = in.read()) != 0){
						sw1.write(temp);
						sw1.flush();
					}
					System.out.println(sw1.toString());
					// 获取断点位置
					int startIndex = 0;
					if(!sw1.toString().isEmpty()){
						startIndex = Integer.parseInt(sw1.toString());
					}
					long length = file.length();
					byte[] filelength = String.valueOf(length).getBytes();
					out.write(filelength);
					out.write(0);
					out.flush();
					// 计划要读的文件长度
					//int length = (int) file.length();//Integer.parseInt(sw2.toString());
					System.out.println("file length : " + length);
					// 缓冲区10KB
					byte[] buffer = new byte[1024*10];
					// 剩余要读取的长度
					int tatol = (int) length;
					System.out.println("startIndex : " + startIndex);
					access.skipBytes(startIndex);
					while (true) {
						// 如果剩余长度为0则结束
						if(tatol == 0){
							break;
						}
						// 本次要读取的长度假设为剩余长度
						int len = tatol - startIndex;
						// 如果本次要读取的长度大于缓冲区的容量
						if(len > buffer.length){
							// 修改本次要读取的长度为缓冲区的容量
							len = buffer.length;
						}
						// 读取文件,返回真正读取的长度
						int rlength = access.read(buffer,0,len);
						// 将剩余要读取的长度减去本次已经读取的
						tatol -= rlength;
						// 如果本次读取个数不为0则写入输出流,否则结束
						if(rlength > 0){
							// 将本次读取的写入输出流中
							out.write(buffer,0,rlength);
							out.flush();
						} else {
							break;
						}
						// 输出读取进度
						//System.out.println("finish : " + ((float)(length -tatol) / length) *100 + " %");
					}
					//System.out.println("receive file finished!");
					// 关闭流
					out.close();
					in.close();
					access.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
			super.run();
		}
	}
	
	public void run(String filename, Socket socket){
		// 启动接收文件线程 
		new Sender(filename,socket).start();
	}
	
	public static void main(String[] args) throws Exception {
		// 创建服务器监听
		ServerSocket server = new ServerSocket(8888);
		// 接收文件的保存路径
		String filename = "E:\\ceshi\\mm.pdf";
		for(;;){
			Socket socket = server.accept();
			new FTPServer().run(filename, socket);
		}
	}
 
}

客户端代码

import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.StringWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
 
// 断点续传客户端
public class FTPClient {
 
	/**
	 *  request:get0startIndex0
	 *  response:fileLength0fileBinaryStream
	 *  
	 * @param filepath
	 * @throws Exception
	 */
	public void Get(String filepath) throws Exception {
		Socket socket = new Socket();
		// 建立连接
		socket.connect(new InetSocketAddress("127.0.0.1", 8888));
		// 获取网络流
		OutputStream out = socket.getOutputStream();
		InputStream in = socket.getInputStream();
		// 文件传输协定命令
		byte[] cmd = "get".getBytes();
		out.write(cmd);
		out.write(0);// 分隔符
		int startIndex = 0;
		// 要发送的文件
		File file = new File(filepath);
		if(file.exists()){
			startIndex = (int) file.length();
		}
		System.out.println("Client startIndex : " + startIndex);
		// 文件写出流
		RandomAccessFile access = new RandomAccessFile(file,"rw");
		// 断点
		out.write(String.valueOf(startIndex).getBytes());
		out.write(0);
		out.flush();
		// 文件长度
		int temp = 0;
		StringWriter sw = new StringWriter();
		while((temp = in.read()) != 0){
			sw.write(temp);
			sw.flush();
		}
		int length = Integer.parseInt(sw.toString());
		System.out.println("Client fileLength : " + length);
		// 二进制文件缓冲区
		byte[] buffer = new byte[1024*10];
		// 剩余要读取的长度
		int tatol = length - startIndex;
		//
		access.skipBytes(startIndex);
		while (true) {
			// 如果剩余长度为0则结束
			if (tatol == 0) {
				break;
			}
			// 本次要读取的长度假设为剩余长度
			int len = tatol;
			// 如果本次要读取的长度大于缓冲区的容量
			if (len > buffer.length) {
				// 修改本次要读取的长度为缓冲区的容量
				len = buffer.length;
			}
			// 读取文件,返回真正读取的长度
			int rlength = in.read(buffer, 0, len);
			// 将剩余要读取的长度减去本次已经读取的
			tatol -= rlength;
			// 如果本次读取个数不为0则写入输出流,否则结束
			if (rlength > 0) {
				// 将本次读取的写入输出流中
				access.write(buffer, 0, rlength);
			} else {
				break;
			}
			System.out.println("finish : " + ((float)(length -tatol) / length) *100 + " %");
		}
		System.out.println("finished!");
		// 关闭流
		access.close();
		out.close();
		in.close();
	}
 
	public static void main(String[] args) {
		FTPClient client = new FTPClient();
		try {
			client.Get("E:\\ceshi\\test\\mm.pdf");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章