1 概述
由于工作的项目上的需求,需要在浏览器上不间断的播放图片,即像播放视频一样播放图片。
后端支持采用Java实现,需要用Java编写一个Http服务器,并提供WebSocket服务。前后端通过Http链接或WebSocket提供图片浏览服务,前端采用JS轮询或WebSocket推送的方式获取图片,浏览器显示图片有两种方式:一种是采用连续切换图片源,实现播放效果;另一种采用将图片画在canvas上面,实现播放。
要完成这个功能涉及到以下技术:
- Http服务器的实现
- 高速的读文件
- WebSocket原理及实现
- 基于浏览器pull方式的http资源获取
- 基于服务器端push方式的http资源获取
- JS播放图片帧的性能
2 技术分析
2.1 Http服务器的实现
实现HTTP服务器比较容易,实现方式也有如下多种:
- 基于jdk中com.sun包下面的HttpServer来实现。(不推荐,com.sun不在java规范内,jdk升级可能会不兼容)
- 基于jetty或tomcat的嵌入式包来实现。此方式基于Servlet规范来实现的,较简单且易于理解。
- 基于netty的方式实现。性能好,需要对NIO有了解,编程难度相对大一些。
- 基于vert.x的实现。这种方式底层还是采用netty实现,较简单,但是也需要熟悉vert.x的编程模型。
我们采用jetty的方式实现,基于servlet3.0规范,可以支持异步请求方式。代码如下:
public static void main(String[] args){ Server server = new Server(); ServerConnector connector = new ServerConnector(server); connector.setPort(8080); server.addConnector(connector); ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); context.setContextPath("/"); context.addServlet(new ServletHolder(new HelloServlet()), "/hello"); server.setHandler(context); try { // Initialize javax.websocket layer ServerContainer wscontainer = WebSocketServerContainerInitializer.configureContext(context); // Add WebSocket endpoint to javax.websocket layer wscontainer.addEndpoint(EventSocket.class); server.start(); server.dump(System.err); server.join(); } catch (Throwable t){ t.printStackTrace(System.err); }}
2.2 高速的读文件
关于Java高速读取文件可参考这篇文章:
这篇文章得出四个结论:- 为了减少I/O操作,每次应该读一个byte数组,而不是一个byte字节,8K的byte数组就是一个好的选择。
- 为了减少方法调用的开销,每次应该获取一个byte数组的数据,而不是一个byte字节。
- 为了减少线程同步锁的开销,要么减少线程同步方法的调用,要么采用非线程安全的类,如:FileChannel 和MappedByteBuffer。
- 为了减少在JVM/OS、internal buffers和应用程序数组之间的数据拷贝,要么使用带有内存映射的FileChannel类,要么使用a direct or wrapped array ByteBuffer.
下面提高两种高速读取文件方法:
for (String filePath : fileList){ try(FileChannel ch = new RandomAccessFile(filePath, "r").getChannel()){ int size = (int) ch.size(); MappedByteBuffer buf = ch.map(MapMode.READ_ONLY, 0, size); // 处理buf.... } catch (IOException e) { e.printStackTrace(); }}
for (String filePath : fileList){ try (SeekableByteChannel sbc = Files.newByteChannel(Paths.get(filePath), StandardOpenOption.READ)) { ByteBuffer buf = ByteBuffer.allocate(10); // Read the bytes with the proper encoding for this platform. If // you skip this step, you might see something that looks like // Chinese characters when you expect Latin-style characters. //String encoding = System.getProperty("file.encoding"); while (sbc.read(buf) > 0) { buf.rewind(); // 处理buf... //System.out.print(Charset.forName(encoding).decode(buf)); buf.flip(); } } catch (IOException x) { System.out.println("caught exception: " + x); }}
2.3 WebSocket原理及实现
WebSocket的原理以及与Http区别可以参考:,总体来说,原理及区别如下:
- WebSocket和Http协议没有太大的关系,WS只是借助Http实现了第一次握手,之后从http协议upgrade为ws://协议。
- WS是持久性连接(类似socket),而HTTP的短连接、长连接都不是持久的。
- WS协议是支持全双工的,可以pull,亦可以push。
用Java实现WebSocket服务端:
@ClientEndpoint@ServerEndpoint(value="/events/")public class EventSocket{ private static int DEFAULT_BUFFER_SIZE = 128 * 1024;// 8192 private byte[] bytes; @OnOpen public void onWebSocketConnect(Session session, EndpointConfig config) { session.setMaxBinaryMessageBufferSize(DEFAULT_BUFFER_SIZE); } @OnMessage public void onWebSocketText(Session session, String message) throws Exception{ System.out.println("Received TEXT message: " + message); bytes = ... // 读取图片文件字节 // 发送图片文件 session.getAsyncRemote().sendBinary( ByteBuffer.wrap( this.bytes ) ); } @OnClose public void onWebSocketClose(Session session, CloseReason reason){ System.out.println("Socket Closed: " + reason); } @OnError public void onWebSocketError(Session session, Throwable cause){ cause.printStackTrace(System.err); }}
2.4 浏览器并发请求与长连接
浏览器请求一般都是拉取服务器的资源,而请求方式分为短连接和长连接两种,这篇文章介绍很清楚:
浏览器对后端资源的请求都是并发的执行的,不同的浏览器并发连接数不同。现在大多数浏览器都支持http1.1协议,默认都会开启keep-alive,支持长连接。在浏览器对后端的资源发出请求,在开启keep-alive情况下,都会复用连接通道。如果是不间断的下载图片,应该使用的是长连接通道复用功能。
2.5 Web服务器Push技术
实现服务器端Push有以下几种方式:
- Ajax轮询。采用setInterval方法不停的调用
- Ajax长轮询。俗称Comet方式,不需要重复建立连接,没有响应就一直等,等到才关闭连接。
- WebSocket
- server-sent-events
Ajax轮询原理还是pull的方式,不算真正的push,但是对一些老版本的浏览器是适用的。WebSocket优点是支持全双工、可跨域。server-sent-server实现简单,但只支持server到client单向传输,且IE系列都不支持。详细内容参考:
除了上面一些方法外,还有一些其他方式,如:Flash XML Socket, Java Applet等非主流。
而在本案例中,如果采用WebSocket传送图片,可实现真正的服务器端不间断的推送图片数据,但是如果要实现并发传送,必须自己在浏览器端来实现,否则,仅仅单连接的情况下不一定比浏览器的并发连接快。
2.6 JS播放图片帧的性能
JS播放图片有多种方式,如:
- 采用标签,不停改变img的src属性,实现播放。
- 采用Html5的Canvas,将Image对象画在Canvas上,实现播放。
- 将图片设置为Div的背景,不停的更换背景,实现播放。
采用标签方式实现如下:
(function() { var i = 0; var pics = [ "andy_white.jpg", "andy_black.jpg" ]; var el = document.getElementById('img_to_flip'); // el doesn't change function toggle() { el.src = pics[i]; // set the image i = (i + 1) % pics.length; // update the counter } setInterval(toggle, 2000);})();
这种方式下浏览器CPU占用率非常高,在IE11和Chrome下,i3的CPU(T440P)占用都在60%左右,内存占用较少,大约在100M左右。CPU的消耗主要在浏览器对图片的渲染上。
用Canvas替代标签,CPU占用方面,IE11仍然占用那么高,Chrome能降一半。更换背景的方式没有实验。基于以上,采用Canvas的方式是一种比较好的选择。aaaa
bbbb