赞
踩
这篇主要记录录屏和SFTP的实现。
对于录屏及录屏的播放,因为我们的项目中需要把guacd和java后端分开两台服务器部署,而guacamole的录屏是通过guacd程序录制的。我的要求是在Java后端直接把录好的视频文件通过http前端播放,因此需要把录屏放在Java端的服务器上。
首先稍微修改一下guacamole-common的源码,添加几个可重载的函数,分别是向前端下发ws消息,向guacd上传前端消息以及ws连接关闭的地方。
GuacamoleWebSocketTunnelEndpoint类的onMessage函数中,添加receiveData(message);
- try {
- // Write received message
- writer.write(message.toCharArray());
- receiveData(message);
- }
- catch (GuacamoleConnectionClosedException e) {
- logger.debug("Connection to guacd closed.", e);
- }
- catch (GuacamoleException e) {
- logger.debug("WebSocket tunnel write failed.", e);
- }
-
- tunnel.releaseWriter();
onClose函数中添加closeConnect函数调用。
- public void onClose(Session session, CloseReason closeReason) {
- try {
- if (tunnel != null)
- tunnel.close();
- closeConnect();
- }
- catch (GuacamoleException e) {
- logger.debug("Unable to close WebSocket tunnel.", e);
- }
- }
定义两个可重载的函数
- protected void receiveData(String message) {
- //logger.info("GuacamoleWebSocketTunnelEndpoint-receiveData");
- }
-
- protected void closeConnect() {
- //logger.info("GuacamoleWebSocketTunnelEndpoint-receiveData");
- }
在Java工程的WebSocketTunnel类中重载函数
receiveData函数用于记录鼠标键盘事件
- @Override
- protected void receiveData(String message) {
- //logger.info("WebSocketTunnel-receiveData : " + message);
- // try {
- // userConnectLogEntity.getBufferedWriter2().write(message);
- // userConnectLogEntity.getBufferedWriter2().newLine();
- // userConnectLogEntity.getBufferedWriter2().flush();
- // } catch (IOException e) {
- // throw new RuntimeException(e);
- // }
- }
sendInstruction函数,对将要发送给前端的报文进行拦截处理,重点是最后的几行,把报文记录在一个文件中。
- Override
- protected void sendInstruction(String instruction) throws IOException {
- if(instruction.startsWith("0.,36.")) {
- uuid = instruction.substring(6, instruction.length()-1);
- System.out.println("uuid: "+uuid);
-
- TunnelStream tunnelStream = new TunnelStream();
- tunnelStream.setWebSocketTunnel(this);
- tunnelStream.setEnd(false);
- tunnelStream.setBuffer(null);
- streamMap.tunnelStreamMap.put(uuid, tunnelStream);
- streamMap.tunnelStreamMap.get(uuid).setOk(false);
- }
- else if(instruction.contains("application/octet-stream")) {
- fileTranfer = true;
-
- GuacamoleParser parser = new GuacamoleParser();
-
- int parsed;
- int offset = 0;
- int length = instruction.toCharArray().length;
- while (true) {
- try {
- if (!((parsed = parser.append(instruction.toCharArray(), offset, length)) != 0))
- break;
- }
- catch (GuacamoleException e) {
- throw new RuntimeException(e);
- }
-
- offset += parsed;
- length -= parsed;
- }
-
- GuacamoleInstruction ins = parser.next();
- synchronized (bufferInstructions) {
- bufferInstructions.put(ins.getArgs().get(0), ins);
- }
- }
- else if(instruction.contains("17.SFTP: File opened")) {
- streamMap.tunnelStreamMap.get(uuid).setOk(true);
- }
- else if(instruction.contains("8.SFTP: OK")) {
- streamMap.tunnelStreamMap.get(uuid).setOk(true);
- }
- else {
- if(fileTranfer) {
- if(instruction.startsWith("4.blob")) {
- int num1 = instruction.indexOf(",");
- int num2 = instruction.indexOf(",", num1+1);
- int num3 = instruction.indexOf(".", num1+1);
-
- int id = Integer.parseInt(instruction.substring(num3+1, num2));
-
- int num4 = instruction.indexOf(".", num2+1);
- String str = instruction.substring(num4+1, instruction.length()-1);
-
- TunnelStream tunnelStream = streamMap.tunnelStreamMap.get(uuid);
-
- if(tunnelStream != null) {
- synchronized(streamMap) {
- streamMap.tunnelStreamMap.get(uuid).setBuffer(str);
- }
-
- instruction = instruction.substring(0, num2+1) + "0.;";
- }
-
- }
- else if(instruction.startsWith("3.end")) {
- System.out.println("3.end");
- fileTranfer = false;
-
- TunnelStream tunnelStream = streamMap.tunnelStreamMap.get(uuid);
-
- synchronized(streamMap) {
- streamMap.tunnelStreamMap.get(uuid).setEnd(true);
- }
- }
- }
- }
-
- super.sendInstruction(instruction);
- if(!instruction.startsWith("0.")) {
- userConnectLogEntity.getBufferedWriter().write(instruction);
- }
- }

closeConnect函数,用于ws连接断开时,记录日志,启动线程进行录屏文件的转换。sendInstruction函数中记录了下发的报文,通过调用guacenc程序把日志转换成m4v格式的视频文件。
- @Override
- protected void closeConnect() {
- try {
- streamMap.tunnelStreamMap.remove(uuid);
-
- userConnectLogEntity.getBufferedWriter().flush();
- userConnectLogEntity.getBufferedWriter().close();
-
- userConnectLogEntity.setEtime(new Date(System.currentTimeMillis()));
- userConnectLogEntity.setPeriod((int)(userConnectLogEntity.getEtime().getTime()-userConnectLogEntity.getStime().getTime()) / 1000);
-
- Thread thread = new MyThread(userConnectLogEntity, userConnectLogService);
- thread.start();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }

视频转换线程
- public class MyThread extends Thread {
- private UserConnectLogEntity userConnectLogEntity;
- private IUserConnectLogService userConnectLogService;
-
- public MyThread(UserConnectLogEntity userConnectLogEntity, IUserConnectLogService userConnectLogService) {
- this.userConnectLogEntity = userConnectLogEntity;
- this.userConnectLogService = userConnectLogService;
- }
-
- public void run() {
- try {
- String fileName = userConnectLogEntity.getVideo().substring(0, userConnectLogEntity.getVideo().length()-4);
- String str = "guacenc -s 1024x768 -r 300000 -f " + fileName;
- Process process = Runtime.getRuntime().exec(str);
- process.waitFor();
-
- logger.info("转换视频完成: " + fileName);
- }
- catch (Exception e) {
- logger.error(e.getMessage(), e);
- }
-
- String str = userConnectLogEntity.getVideo();
- int num1 = str.lastIndexOf(File.separator);
- int num2 = str.lastIndexOf(File.separator, num1-1);
- userConnectLogEntity.setVideo("/video"+str.substring(num2));
- userConnectLogService.updateById(userConnectLogEntity);
- }
- }

把视频文件暴露给web端
- @Configuration
- public class WebAppConfig extends WebMvcConfigurerAdapter {
-
- @Value("${fileserver.videofolder}")
- private String videoFolder;
-
- @Override
- public void addResourceHandlers(ResourceHandlerRegistry registry) {
- registry.addResourceHandler("/video/**").addResourceLocations("file:"+videoFolder);
- super.addResourceHandlers(registry);
- }
- }
这样视频文件直接通过web链接就可以在浏览器中播放。
另外要说明一点的是,默认的guacenc程序转换出来的视频文件在浏览器中是无法播放的,视频的内部格式不对,需要修改一下guacamole-server的源码重新编译一下。
guacamole-server-1.5.1\src\guacenc\guacenc.c文件,121行左右,修改一下视频格式重新编译。
- //if (guacenc_encode(path, out_path, "mpeg4", width, height, bitrate, force))
- // 修改为
- if (guacenc_encode(path, out_path, "libx264", width, height, bitrate, force))
SFTP的实现较为复杂,需要对SFTP上传下载的流程及guacamole封装的协议有较好的了解,才能实现。
文件列表相对简单些,通过查看guacamole的前端代码,基本可以了解其流程,自己再按照流程重新写一下前端就行。
实现Guacamole.Client的onfilesystem的响应
- guac.onfilesystem = function(object, name) {
- filesystemObject = object;
- currentPath = name;
- listDirectory(currentPath);
- };
获取文件列表的函数 ,主要是调用filesystemObject.requestInputStream、sendAck
- listDirectory(path) {
- filesystemObject.requestInputStream(path, function handleStream(stream, mimetype) {
- // Ignore stream if mimetype is wrong
- if (mimetype !== Guacamole.Object.STREAM_INDEX_MIMETYPE) {
- stream.sendAck('Unexpected mimetype', Guacamole.Status.Code.UNSUPPORTED);
- return;
- }
-
- currentPath = path;
- let exchangePath = path.replace(/^\//,'')
- folders = exchangePath.length ? exchangePath.split('/') : []
- paths = []
- folders.reduce((tmp, item, index) => {
- let path = tmp+"/"+item
- let obj = {
- path: path,
- folder: item
- }
- paths.push(obj)
- return path }, "")
- // Signal server that data is ready to be received
- stream.sendAck('Ready', Guacamole.Status.Code.SUCCESS);
-
- // Read stream as JSON
- let reader = new Guacamole.JSONReader(stream);
-
- // Acknowledge received JSON blobs
- reader.onprogress = function onprogress() {
- stream.sendAck("Received", Guacamole.Status.Code.SUCCESS);
- };
-
- // Reset contents of directory
- reader.onend = function jsonReady() {
- fileList = []
- // For each received stream name
- var mimetypes = reader.getJSON();
-
- for (var name in mimetypes) {
- if (name.substring(0, path.length) !== path){
- continue;
- }
-
- var filename = name.substring(path.length);
- if(path.substring(path.length-1) != '/'){
- filename = name.substring(path.length+1);
- }
-
- let one = {}
- one.path = filename
- if (mimetypes[name] === Guacamole.Object.STREAM_INDEX_MIMETYPE) {
- one.type="folder"
- }
- else {
- one.type="file"
- }
- one.fullpath = name
- fileList.push(one)
- }
- };
- });
- },

上传下载,首先得搞清楚整体得流程,
通过wireshark抓包,可以查看guacd与java后端的通信报文,
通过浏览器自带的调试工具,可以查看前端和Java后端之间的websocket通信报文,
通过上面两个工具的抓包分析,分析出上传下载的流程。
文件下载流程:
文件上传流程:
下面是代码实现的大致流程:
前端下载代码,先通过filesystemObject.requestInputStream发送下载请求,再通过iframe挂一个http get请求开始下载文件,中间通过stream.onblob事件回复Ack消息,通过stream.onend事件结束下载流程
- downloadfile(path){
- filesystemObject.requestInputStream(path, function downloadStreamReceived(stream, mimetype) {
- // Parse filename from string
- var filename = path.match(/(.*[\\/])?(.*)/)[2];
- var url = '/tunnels/' + uuid + '/sessions/' + stream.index + '/files/' + filename;
-
- // Create temporary hidden iframe to facilitate download
- var iframe = document.createElement('iframe');
- iframe.style.display = 'none';
-
- // The iframe MUST be part of the DOM for the download to occur
- document.body.appendChild(iframe);
-
- iframe.onload = function downloadComplete() {
- document.body.removeChild(iframe);
- };
-
- // Acknowledge (and ignore) any received blobs
- stream.onblob = function acknowledgeData() {
- stream.sendAck('OK', Guacamole.Status.Code.SUCCESS);
- };
-
- // Automatically remove iframe from DOM a few seconds after the stream
- // ends, in the browser does NOT fire the "load" event for downloads
- stream.onend = function downloadComplete() {
- window.setTimeout(function cleanupIframe() {
- if (iframe.parentElement) {
- document.body.removeChild(iframe);
- }
- }, 5000);
- };
-
- // Begin download
- iframe.src = url;
- });
- }

前端上传文件代码,file类型input的change事件响应函数。通过filesystemObject.createOutputStream发送文件上传请求,通过XMLHttpRequest post 发送文件给Java端,
- changFile(event){
- let file1 = event.target.files[0];
- var stream = filesystemObject.createOutputStream(file1.type, currentPath+'/'+file1.name);
-
- stream.onack = function beginUpload(status) {
- if (status.isError()) {
- return;
- }
- }
-
- var fd = new FormData();
- fd.append('file', file1);
-
- var url = '/tunnels/' + uuid + '/sessions/' + stream.index;
- const xhr = new XMLHttpRequest();
- xhr.open('POST', url);
- xhr.send(fd);
-
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4 && xhr.status === 200) {
- console.log('上传成功');
- updateDirectory(currentPath);
- }
- }
- },

接下来是java端的http接口,
下载文件接口,主要是通过ServletOutputStream向前端写文件流。文件流实际是在websocket处理函数中接收的,这儿guacamole通过消息过滤等方式实现了,比较复杂。我这儿简单粗暴的用了全局的公共变量实现,每个websocket实例接受到文件段后,保存到一个公共缓冲区中,再置一个标志位,http controller这儿,循环判断标准位,取出文件段,向前端写文件流。
- @GetMapping("/tunnels/{tnid}/sessions/{snid}/files/{filename}")
- public void download(@PathVariable("tnid")String tnid, @PathVariable("snid")String snid, @PathVariable("filename")String filename, HttpServletResponse response) {
- try {
- System.out.println("download controller: "+tnid);
- response.setCharacterEncoding("UTF-8");
- response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
- ServletOutputStream os = response.getOutputStream();
-
- if(streamMap.tunnelStreamMap.get(tnid) != null) {
- streamMap.tunnelStreamMap.get(tnid).getWebSocketTunnel().startSendFile(snid);
- streamMap.tunnelStreamMap.get(tnid).setEnd(false);
- streamMap.tunnelStreamMap.get(tnid).setBuffer(null);
-
- long start = System.currentTimeMillis();
- while(!streamMap.tunnelStreamMap.get(tnid).isEnd()){
- synchronized(streamMap) {
- String str = streamMap.tunnelStreamMap.get(tnid).getBuffer();
- if (str != null) {
- streamMap.tunnelStreamMap.get(tnid).setBuffer(null);
-
- os.write(decoder.decode(str.getBytes()));
- }
- }
- }
- }
-
- os.close();
- }
- catch (Exception e) {
- throw new RuntimeException(e);
- }
- }

上传文件接口。同样通过公共的Bean和websocket线程同步消息
- @PostMapping("/tunnels/{tnid}/sessions/{snid}")
- public void upload(@RequestParam("file") MultipartFile uploadFile, @PathVariable("tnid")String tnid, @PathVariable("snid")String snid) {
- try {
- InputStream inputStream = uploadFile.getInputStream();
- byte[] buffer = new byte[8192];
- int bytesRead = 0;
- while ((bytesRead = inputStream.read(buffer, 0, buffer.length)) != -1) {
-
- long start = System.currentTimeMillis();
- while(!streamMap.tunnelStreamMap.get(tnid).isOk()) {
- // 等待上传完成消息
- }
-
- streamMap.tunnelStreamMap.get(tnid).setOk(false);
-
- System.out.println(bytesRead);
- byte[] bb = null;
- if(bytesRead < 8192) {
- bb = new byte[bytesRead];
- System.arraycopy(buffer, 0, bb, 0, bytesRead);
- }
- else {
- bb = buffer;
- }
-
- if(streamMap.tunnelStreamMap.get(tnid) != null) {
- streamMap.tunnelStreamMap.get(tnid).getWebSocketTunnel().sendBlob(snid, bb);
- }
- }
-
- if(streamMap.tunnelStreamMap.get(tnid) != null) {
- streamMap.tunnelStreamMap.get(tnid).getWebSocketTunnel().sendEnd(snid);
- }
-
- inputStream.close();
- }
- catch (Exception e) {
- throw new RuntimeException(e);
- }
- }

websocket处理部分,注意和http controller的同步
- @Override
- protected void sendInstruction(String instruction) throws IOException {
- if(instruction.startsWith("0.,36.")) {
- uuid = instruction.substring(6, instruction.length()-1);
- System.out.println("uuid: "+uuid);
-
- TunnelStream tunnelStream = new TunnelStream();
- tunnelStream.setWebSocketTunnel(this);
- tunnelStream.setEnd(false);
- tunnelStream.setBuffer(null);
- streamMap.tunnelStreamMap.put(uuid, tunnelStream);
- streamMap.tunnelStreamMap.get(uuid).setOk(false);
- }
- else if(instruction.contains("application/octet-stream")) {
- fileTranfer = true;
-
- GuacamoleParser parser = new GuacamoleParser();
-
- int parsed;
- int offset = 0;
- int length = instruction.toCharArray().length;
- while (true) {
- try {
- if (!((parsed = parser.append(instruction.toCharArray(), offset, length)) != 0))
- break;
- }
- catch (GuacamoleException e) {
- throw new RuntimeException(e);
- }
-
- offset += parsed;
- length -= parsed;
- }
-
- GuacamoleInstruction ins = parser.next();
- synchronized (bufferInstructions) {
- bufferInstructions.put(ins.getArgs().get(0), ins);
- }
- }
- else if(instruction.contains("17.SFTP: File opened")) {
- streamMap.tunnelStreamMap.get(uuid).setOk(true);
- }
- else if(instruction.contains("8.SFTP: OK")) {
- streamMap.tunnelStreamMap.get(uuid).setOk(true);
- }
- else {
- if(fileTranfer) {
- if(instruction.startsWith("4.blob")) {
- int num1 = instruction.indexOf(",");
- int num2 = instruction.indexOf(",", num1+1);
- int num3 = instruction.indexOf(".", num1+1);
-
- int id = Integer.parseInt(instruction.substring(num3+1, num2));
-
- int num4 = instruction.indexOf(".", num2+1);
- String str = instruction.substring(num4+1, instruction.length()-1);
-
- TunnelStream tunnelStream = streamMap.tunnelStreamMap.get(uuid);
-
- if(tunnelStream != null) {
- synchronized(streamMap) {
- streamMap.tunnelStreamMap.get(uuid).setBuffer(str);
- }
-
- instruction = instruction.substring(0, num2+1) + "0.;";
- }
- }
- else if(instruction.startsWith("3.end")) {
- System.out.println("3.end");
- fileTranfer = false;
- //int num1 = instruction.indexOf(".", 3);
- //int id = Integer.parseInt(instruction.substring(num1+1, instruction.length()-1));
-
- TunnelStream tunnelStream = streamMap.tunnelStreamMap.get(uuid);
- synchronized(streamMap) {
- streamMap.tunnelStreamMap.get(uuid).setEnd(true);
- }
- }
- }
- }
-
- super.sendInstruction(instruction);
- if(!instruction.startsWith("0.")) {
- userConnectLogEntity.getBufferedWriter().write(instruction);
- }
- }
-
- public void startSendFile(String sid) {
- acknowledgeStream(sid);
- }
-
- @Override
- protected void receiveData(String message) {
- }
-
- public void sendBlob(String sid, byte[] bytes) {
- GuacamoleWriter writer = guacamoleTunnel.acquireWriter();
-
- GuacamoleInstruction ins = new GuacamoleInstruction("blob", sid, BaseEncoding.base64().encode(bytes));
- try {
- writer.writeInstruction(ins);
- }
- catch (GuacamoleException e) {
- logger.debug("Unable to send \"{}\" for intercepted stream.", ins.getOpcode(), e);
- }
-
- guacamoleTunnel.releaseWriter();
- }
-
- public void sendEnd(String sid) {
- GuacamoleWriter writer = guacamoleTunnel.acquireWriter();
- GuacamoleInstruction ins = new GuacamoleInstruction("end", sid);
- try {
- writer.writeInstruction(ins);
- }
- catch (GuacamoleException e) {
- logger.debug("Unable to send \"{}\" for intercepted stream.", ins.getOpcode(), e);
- }
-
- guacamoleTunnel.releaseWriter();
- }
-
- protected void acknowledgeStream(String sid) {
- GuacamoleInstruction ins = null;
- synchronized (bufferInstructions) {
- ins = bufferInstructions.remove(sid);
- }
-
- if(ins != null) {
- GuacamoleWriter writer = guacamoleTunnel.acquireWriter();
- try {
- writer.writeInstruction(
- new GuacamoleInstruction("ack", ins.getArgs().get(0), "OK",
- Integer.toString(GuacamoleStatus.SUCCESS.getGuacamoleStatusCode())));
- }
- catch (GuacamoleException e) {
- throw new RuntimeException(e);
- }
-
- guacamoleTunnel.releaseWriter();
- }
- }

Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。