文章目录
学习链接
关于 video 播放的新探索
西瓜视频播放器(HTML5)(这个播放器可以支持mp4视频分段播放)
下面报错的原因找到了,参考:Java后端接口返回视频流,使用video组件播放视频,实现分段下载
目标
HTNL5的新对象—video对象
- 学习video标签,掌握video标签的常用属性
- 学习video对象,掌握video对象的常用属性、方法
- 学习video对象的事件,了解事件的触发时机,触发顺序
- 做一个视频播放器
video标签自带视频和制作的视频区别
- video标签自带的视频播放器,控件不灵活,不能根据需求自定义更改控件
- 各浏览器样式不统一
video标签的src属性
video标签是HTML5的新标签,video标签用于定义视频
,可以载入视频
,在页面上展示播放视频,并用播放
暂停
音量
等等控件来控制视频
-
src:接收要播放的视频的url地址,这个url可以是
本地ur
,也可以是远程服务器的资源地址
-
controls属性:给video标签加上了这个属性,就会给用户展示播放音量等控件
-
autoplay属性:video标签设嚣该属性,就会自动播放(但是,必须同时设置muted,否则不会自动播放)
-
muted属性: video上出现了该属性,视频会被静音
-
loop属性:video标签出现该属性,在视频播放结束后会循环播放(默认情况下,视频播放完了就会就停止)
-
poster属性:设置视频摇放前显示图像(相当于视频封面,默认情况下,在视频未播放前,会显示视频的第一帧)
-
width属性:设置视频的宽(可同时设置高度)
首先,我们先来看下 video 最基础的用法:
- 使用 src 属性
<video src="http://v2v.cc/~j/theora_testsuite/320x240.ogg" controls>
你的浏览器不支持 <code>video</code> 标签。
</video>
- 使用 source 标签
<video controls>
<source src="foo.ogg" type="video/ogg">
<source src="foo.mp4" type="video/mp4">
Your browser does not support the <code>video</code> element.
</video>
这是 MDN 关于 video 给出的基本用例。在这里我们简单介绍下两种方法的不同,src 只能赋予 video 一个播放地址,当浏览器不支持这种视频格式的解码时就会出现错误,导致视频播放失败。为了解决这个问题才有了 source 标签,利用多个 source 标签引入不同格式的视频,从上到下解析直到遇到看上述代码当浏览器不支持 ogg 格式时,浏览器会自动播放 foo.mp4。
本地视频文件
前端代码
播放效果
服务器视频文件
示例1
后端代码
@RestController
public class VideoController {
@Autowired
private HttpServletResponse response;
@GetMapping("getVideo")
public void getVideo(String videoName) throws Exception {
System.out.println("请求过来了...");
FileSystemResourceLoader fileResourceLoader = new FileSystemResourceLoader();
Resource resource = fileResourceLoader.getResource("D:\\Projects\\vue-springboot\\src\\main\\resources\\video\\" + videoName);
int available = resource.getInputStream().available();
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(available));
ServletOutputStream outputStream = response.getOutputStream();
StreamUtils.copy(resource.getInputStream(), outputStream);
outputStream.flush();
outputStream.close();
}
}
前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
margin: 0;
}
</style>
</head>
<body>
<video src="http://127.0.0.1:8085/getVideo?videoName=rocketmq.mp4" controls poster="./poster2.jpg" width="400"></video>
<span style="background-color: yellow;" >1</span>
</body>
</html>
播放效果
-
虽然视频能播放,但是不能拖动视频播放进度条(这是个硬伤)
-
一个视频发了好几个请求
-
并且后台,时不时就报下面的错误
org.apache.catalina.connector.ClientAbortException: java.io.IOException: 你的主机中的软件中止了一个已建立的连接。
at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:351)
at org.apache.catalina.connector.OutputBuffer.flushByteBuffer(OutputBuffer.java:776)
at org.apache.catalina.connector.OutputBuffer.append(OutputBuffer.java:681)
at org.apache.catalina.connector.OutputBuffer.writeBytes(OutputBuffer.java:386)
at org.apache.catalina.connector.OutputBuffer.write(OutputBuffer.java:364)
at org.apache.catalina.connector.CoyoteOutputStream.write(CoyoteOutputStream.java:96)
at org.springframework.util.StreamUtils.copy(StreamUtils.java:143)
at com.zzhua.video.VideoController.getVideo(VideoController.java:36)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:897)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:526)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:860)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1587)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.io.IOException: 你的主机中的软件中止了一个已建立的连接。
at sun.nio.ch.SocketDispatcher.write0(Native Method)
at sun.nio.ch.SocketDispatcher.write(SocketDispatcher.java:51)
at sun.nio.ch.IOUtil.writeFromNativeBuffer(IOUtil.java:93)
at sun.nio.ch.IOUtil.write(IOUtil.java:65)
at sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:471)
at org.apache.tomcat.util.net.NioChannel.write(NioChannel.java:140)
at org.apache.tomcat.util.net.NioBlockingSelector.write(NioBlockingSelector.java:101)
at org.apache.tomcat.util.net.NioSelectorPool.write(NioSelectorPool.java:152)
at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.doWrite(NioEndpoint.java:1261)
at org.apache.tomcat.util.net.SocketWrapperBase.doWrite(SocketWrapperBase.java:793)
at org.apache.tomcat.util.net.SocketWrapperBase.writeBlocking(SocketWrapperBase.java:563)
at org.apache.tomcat.util.net.SocketWrapperBase.write(SocketWrapperBase.java:501)
at org.apache.coyote.http11.Http11OutputBuffer$SocketOutputBuffer.doWrite(Http11OutputBuffer.java:538)
at org.apache.coyote.http11.filters.IdentityOutputFilter.doWrite(IdentityOutputFilter.java:73)
at org.apache.coyote.http11.Http11OutputBuffer.doWrite(Http11OutputBuffer.java:190)
at org.apache.coyote.Response.doWrite(Response.java:601)
at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:339)
... 61 common frames omitted
示例2
后端代码
@Component
public class NonStaticResourceHttpRequestHandler extends ResourceHttpRequestHandler {
// 定义视频路径
public String filepath = "filepath";
@Override
protected Resource getResource(HttpServletRequest request) {
// 获取视频路径对象
final Path filePath = (Path) request.getAttribute(filepath);
// 用 FileSystemResource 加载资源
return new FileSystemResource(filePath);
}
}
@RestController
@RequestMapping("video")
public class FileRestController {
@Autowired
private NonStaticResourceHttpRequestHandler nonStaticResourceHttpRequestHandler;
@GetMapping("/getVideo")
public void getVideo(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("======================>");
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
System.out.println(headerName + ":" + request.getHeader(headerName));
}
System.out.println("<======================");
//sourcePath 是获取编译后 resources 文件夹的绝对地址,获得的原始 sourcePath 以/开头,所以要用 substring(1) 去掉第一个字符/
//realPath 即视频所在的完整地址
String sourcePath = this.getClass().getClassLoader().getResource("").getPath().substring(1);
String realPath = sourcePath + "video/" + request.getParameter("videoName");
Path filePath = Paths.get(realPath);
if (Files.exists(filePath)) {
// 利用 Files.probeContentType 获取文件类型
String mimeType = Files.probeContentType(filePath);
if (!StringUtils.isEmpty(mimeType)) {
// 设置 response
response.setContentType(mimeType);
}
request.setAttribute(nonStaticResourceHttpRequestHandler.filepath, filePath);
// 利用 ResourceHttpRequestHandler.handlerRequest() 实现返回视频流
nonStaticResourceHttpRequestHandler.handleRequest(request, response);
} else {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
}
}
}
前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
margin: 0;
}
</style>
</head>
<body>
<video src="http://127.0.0.1:8085/video/getVideo?videoName=rocketmq.mp4" controls poster="./poster2.jpg" width="400"></video>
<span style="background-color: yellow;" >1</span>
</body>
</html>
播放效果
- 视频能够正常播放,并且能够拖动视频进度条,但是一拖动进度条,后台就报错(见下图)
- 前端的video标签默认会分段请求,会携带Range请求头(见下图)
示例3
上面的分段请求,我感觉是不是哪里写的有问题,接下来使用一些springboot自带的分段资源处理,它没有报错,后面需看看源码是为什么?
但是通过下面的示例可以看出来
- 前端video标签,它会自动分段请求
- 后台需要根据前端的Range请求头,将指定分段的数据返回给前端
- 后台没有示例1和示例2中的报错
- 观察发送的请求头和响应头,发现前端发送的请求头里:
Range: bytes=15269888-
,它总是没有后面的这个值(只有开始的值),然后后台响应的是:Content-Range: bytes 15269888-119755709/119755710
,也不知道他两是为啥这样交互?每次服务器都只有一个开始的位置,然后从这个开始的位置到文件的末尾,每次都这样写给浏览器的话,这不是有问题吗? - ResourceHttpRequestHandler 会交给 ResourceRegionHttpMessageConverter
- 哦,整明白了,拖动进度跳的时候,这个源码也会报错,只是,我把这个使用异常处理器抓住了,并且打印出来了,而异常处理器没有抓住源码中ResourceHttpRequestHandler的异常而已。
后端配置
就是作为静态资源暴露出去
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry
.addMapping("/**")
.maxAge(3600)
.allowCredentials(true)
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*")
.exposedHeaders("token","Authorization")
;
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/video/**")
.addResourceLocations("file:/D:\\Projects\\vue-springboot\\src\\main\\resources\\video\\");
}
}
前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
margin: 0;
}
</style>
</head>
<body>
<video src="http://127.0.0.1:8085/video/rocketmq.mp4" controls poster="./poster2.jpg" width="400"></video>
<span style="background-color: yellow;" >1</span>
</body>
</html>
播放效果
video对象
video对象创建和获取
video对象的创建:就是创建一个video元素
const oVideo = document.createDocument( "video")
video对象的获取:就是获取video元素
const oVideo - document.getElementById('video')
video的属性
- src属性:媒体文件的地址
- duration属性:返回视频的总时长︰单位为秒(注意:需要监听到loadeddata事件发生后,才能获取)
- currentTime属性:设置或返回视频的当前播放事件单位也是秒
- volume属性:设置或返回视频的音量,
0 ~ 1
0.1
0.5
1
最大值1 代表音量最大
最小0 代表静音
- muted属性:是设置或返回视频是否静音,值为 true false,与volume属性不相关
- ended属性:视频是否播放完
- paused属性:设置或返回视频是否暂停,值为 true false
- playbackRate属性:设置或返回播放速度,默认值为1,即正常速度播放。
video的方法
-
play():播放视频
-
pause():暂停播放
video的事件
- loadstart: 媒体数据开始加载时 触发
- loadeddata: 媒体数据加载完成
- canplay: 当浏览器判定视频可以播放了
- error: 视频加载发生错误
- play: 当视频启动播放时触发
- pause: 当视频被暂停时触发
- timeupdate:播放中,当前播放位置发生改变时触发(视频在播放中时,会一直触发)
- volumechange:当音量大小发生变化时触发
示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
margin: 0;
}
</style>
</head>
<body>
<video id="video" controls poster="./poster.jpg" width="400"></video>
<p>
<button id="play">播放</button>
<button id="pause">暂停</button>
</p>
</body>
<script>
let video = document.querySelector('#video')
let playBtn = document.querySelector('#play')
let pauseBtn = document.querySelector('#pause')
video.src = 'http://127.0.0.1:8085/video/rocketmq.mp4'
playBtn.onclick = () => {
video.muted = true
video.play()
}
pauseBtn.onclick = () => {
video.pause()
}
video.addEventListener('loadstart',function(){
console.log('开始加载...');
})
video.addEventListener('loadeddata',function(){
console.log('加载完成...');
})
video.addEventListener('canplay',function(){
console.log('视频可以播放了...');
})
video.addEventListener('play',function(){
console.log('视频启动播放...',video.paused);
})
video.addEventListener('pause',function(){
console.log('视频暂停播放...',video.paused);
})
video.addEventListener('timeupdate',function(){
console.log('视频播放中',video.currentTime);
})
video.addEventListener('volumechange',function(){
console.log('音量修改了',video.volume);
})
video.addEventListener('error',function(){
console.log('视频加载出错了',video.volume);
})
</script>
</html>
实现一个video播放器(空)
xgplayer分段播放mp4视频
安装依赖
npm i [email protected] --save
npm i [email protected] --save
App.vue
<template>
<div id="mse"></div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import Player from "xgplayer"
import Mp4Plugin from "xgplayer-mp4"
import "xgplayer/dist/index.min.css"
onMounted(() => {
const player = new Player({
url:'http://127.0.0.1:8085/video/getVideo?videoName=rocketmq.mp4',
id:'mse',
autoplay: true,
width: 800,
// height: window.innerHeight,
plugins: [Mp4Plugin],
mp4plugin: {
maxBufferLength: 30,
minBufferLength: 10,
reqOptions: {
mode: 'cors',
method: 'POST',
headers: { // 需要带的自定义请求头
'x-test-header': 'rrrr'
},
}
// ... 其他配置
}
})
window.player = player
})
</script>
后台代码
NonStaticResourceHttpRequestHandler
因为发现xgplayer在发送分段请求的时候,请求分段视频资源的请求方法为POST请求,因此,需要添加POST请求方法的支持(原来这里只支持head和get)。
@Component
public class NonStaticResourceHttpRequestHandler extends ResourceHttpRequestHandler implements SmartInitializingSingleton {
// 定义视频路径
public String filepath = "filepath";
@Override
protected Resource getResource(HttpServletRequest request) {
// 获取视频路径对象
final Path filePath = (Path) request.getAttribute(filepath);
// 用 FileSystemResource 加载资源
return new FileSystemResource(filePath);
}
@Override
public void afterSingletonsInstantiated() {
this.setSupportedMethods(HttpMethod.GET.name(),HttpMethod.POST.name(), HttpMethod.HEAD.name());
}
}
FileRestController
@RestController
@RequestMapping("video")
public class FileRestController {
@Autowired
private NonStaticResourceHttpRequestHandler nonStaticResourceHttpRequestHandler;
@RequestMapping("/getVideo")
public void getVideo(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("======================>");
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
System.out.println(headerName + ":" + request.getHeader(headerName));
}
System.out.println("<======================");
//sourcePath 是获取编译后 resources 文件夹的绝对地址,获得的原始 sourcePath 以/开头,所以要用 substring(1) 去掉第一个字符/
//realPath 即视频所在的完整地址
String sourcePath = this.getClass().getClassLoader().getResource("").getPath().substring(1);
String realPath = sourcePath + "video/" + request.getParameter("videoName");
Path filePath = Paths.get(realPath);
if (Files.exists(filePath)) {
// 利用 Files.probeContentType 获取文件类型
String mimeType = Files.probeContentType(filePath);
if (!StringUtils.isEmpty(mimeType)) {
// 设置 response
response.setContentType(mimeType);
}
request.setAttribute(nonStaticResourceHttpRequestHandler.filepath, filePath);
// 利用 ResourceHttpRequestHandler.handlerRequest() 实现返回视频流
nonStaticResourceHttpRequestHandler.handleRequest(request, response);
} else {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
}
}
}
WebConfig
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry
.addMapping("/**")
.maxAge(36000)
.allowCredentials(true)
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*")
.exposedHeaders("token","Authorization")
;
}
}
效果
前面,我们通过原始的video表情去请求后台,发现的问题:每次拖动请求头,它发出的请求的Range范围总是从指定的范围到文件末尾,每个请求都这样搞的话,后台就要不断的写。下面使用的xgplayer就可以请求指定部分的视频资源来播放。
- 可以在下图中看到,在拖动视频进度条的时候,可以看到会有分段请求发出,并且使用了
Range请求头
请求指定部分的视频资源,后台将会根据此Range请求头将对应部分的视频数据发送给前端 - 每次请求都携带了自定义请求头
- 还要注意一点就是:使用xgplayer后,它还是会发一个
http://127.0.0.1:8085/video/getVideo?videoName=rocketmq.mp4
的get请求,但是后面的请求,都是用的分段请求去请求指定分段的视频部分了。 - 还是会出现上面示例2的报错,但至少不是每次请求的Range都到文件末尾去,并且整个过程都能正常播放。
后面都是连续的请求
报错原因
报错的原因找到了,参考:Java后端接口返回视频流,使用video组件播放视频,实现分段下载
tomcat原话:写操作IO异常几乎总是由于客户端主动关闭连接导致,所以直接吃掉异常打日志,比如使用video播放视频时经常会发送Range为0- 的范围只是为了获取视频大小,之后就中断连接了
,那就是说这个报错是浏览器故意断开连接导致连接中断,而不是后台的原因。