Springboot实现VNC的反向代理

// 设置Host

if (servletRequest.getAttribute(ATTR_TARGET_HOST) == null) {

URL trueUrl = URLUtil.url(targetUri);

servletRequest.setAttribute(ATTR_TARGET_HOST, new HttpHost(trueUrl.getHost(), trueUrl.getPort(), trueUrl.getProtocol()));

}

// 下面大部分都是父类的源码 没有需要特别修改的地方

String method = servletRequest.getMethod();

// 替换多余路径

String proxyRequestUri = this.rewriteUrlFromRequest(servletRequest);

HttpRequest proxyRequest;

if (servletRequest.getHeader(HttpHeaders.CONTENT_LENGTH) != null ||

servletRequest.getHeader(HttpHeaders.TRANSFER_ENCODING) != null) {

proxyRequest = new BasicHttpRequest(method, proxyRequestUri);

} else {

proxyRequest = this.newProxyRequestWithEntity(method, proxyRequestUri, servletRequest);

}

this.copyRequestHeaders(servletRequest, proxyRequest);

setXForwardedForHeader(servletRequest, proxyRequest);

HttpResponse proxyResponse = null;

try {

// Execute the request

proxyResponse = this.doExecute(servletRequest, servletResponse, proxyRequest);

// Process the response

int statusCode = proxyResponse.getStatusLine().getStatusCode();

// “reason phrase” is deprecated but it’s the only way to pass

servletResponse.setStatus(statusCode, proxyResponse.getStatusLine().getReasonPhrase());

// copying response headers to make sure SESSIONID or other Cookie which comes from remote server

// will be saved in client when the proxied url was redirected to another one.

copyResponseHeaders(proxyResponse, servletRequest, servletResponse);

if (statusCode == HttpServletResponse.SC_NOT_MODIFIED) {

servletResponse.setIntHeader(HttpHeaders.CONTENT_LENGTH, 0);

} else {

copyResponseEntity(proxyResponse, servletResponse, proxyRequest, servletRequest);

}

} catch (Exception e) {

handleRequestException(proxyRequest, proxyResponse, e);

} finally {

if (proxyResponse != null) {

EntityUtils.consumeQuietly(proxyResponse.getEntity());

}

}

}

​ 这里主要列出关键部分,因为方案还没有完结。

  • 问题

​ 本以为这样就成功了,但是测试之后发现页面和静态资源都代理过去了,但是有一个websocket请求失败了。像noVNC这种网页版的黑窗口,早就该想到肯定是用websocket这种长链接的请求进行交互的。后来去搜了一下这个叫websockify的请求,就是最开始介绍noVNC博客中介绍的:

​ 浏览器不支持VNC,所以不能直接连接VNC,但是可以使用代理,使用noVNC通过WebSocket建立连接,而VNC Server不支持WebSocket,所以需要开启Websockify代理来做WebSocket和TCP Socket之间的转换,这个代理在noVNC的目录里,叫做websockify。

​ 此时项目是能够拦截到websockify这个请求的,但是由于servlet把这个请求当成普通的请求去代理到目标服务器,这样是无法成功的,所以要做的就是类似实现一个websocket的反向代理,搜了一下的话发现例子不是很多,大多都是在前端做的,前端作为客户端与服务端建立websocket连接,但目前的状况很明显是需要这个代理模块既做websocket服务端与web端建立连接,再作为websocket客户端与VNC 服务端建立连接,然后进行交互传递通信。

​ 后面也找到了这篇博客通过noVNC和websockify连接到QEMU/KVM,然后总结一下从用户发出请求到得到响应的流程:

PC Chrome(客户端) => noVNC Server(noVNC端) => websockify(websocket转TCP Socket) => VNC Server(VNC服务端) => websockify(TCP Socket转websocket) => noVNC Server(noVNC端)=> PC Chrome(客户端)

用户使用PC Chrome浏览器请求 noVNC端(因为无法直接访问VNC Server端,VNC Server是不支持Websocket连接),经由websockify将websocket转为TCP Socket请求到VNC服务端,返回TCP响应,经由websockify转换为websocket返回给客户端浏览器,这样来进行交互。整个过程 websockify 代理器是关键,noVNC 可以被放在浏览器端。

  1. noVNC网页端与代理模块建立websocket通信

@Configuration

public class WebSocketConfig {

@Bean

public ServerEndpointExporter serverEndpointExporter() {

return new ServerEndpointExporter();

}

}

@ServerEndpoint(“/websockify”)

@Component

public class WebSocketServer {

/**

  • 连接建立成功调用的方法

*/

@OnOpen

public void onOpen(Session session) {

logger.info(“open…”);

}

/**

  • 连接关闭调用的方法

*/

@OnClose

public void onClose() {

logger.info(“close…”);

}

/**

  • 收到客户端消息后调用的方法

*/

@OnMessage

public void onMessage(String message, Session session) {

logger.info(message);

}

/**

  • 发生错误调用的方法

*/

@OnError

public void onError(Session session, Throwable error) {

logger.error(“用户错误原因:”+error.getMessage());

error.printStackTrace();

}

}

​ 都是很常用的websocket服务端的代码,唯一要注意的是前端请求**‘/websockify’**地址发起websocket连接时,要注意用ip,尤其是本地,使用localhost会报错,要使用127.0.0.1。最后测试连接成功,返回状态码101,并且消息可以正常接收。noVNC网页端与代理模块建立websocket通信完成。

  1. 代理模块与VNC Server建立websocket通信

java后台作为websocket客户端很少,大多是用Netty去写的,但是不适合目前的情况,最后还是找到了一个感觉比较合适的

public class MyWebSocketClient {

public static WebSocketClient mWs;

public static void main(String[] args) {

try {

//

String url = “ws://172.28.132.11:8888/websocketify”;

URI uri = new URI(url);

HashMap<String, String> httpHeadersMap = new HashMap<>();

httpHeadersMap.put(“Sec-WebSocket-Version”, “13”);

httpHeadersMap.put(“Sec-WebSocket-Key”, “YBhzbbwLI83U5EH8Tlutwg==”);

httpHeadersMap.put(“Connection”,“Upgrade”);

httpHeadersMap.put(“Upgrade”,“websocket”);

httpHeadersMap.put(“User-Agent”,“Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36”);

httpHeadersMap.put(“Cookie”,“token=8asda2das-84easdac-asdaqwe4-2asda-asdsadas”);

httpHeadersMap.put(“Sec-WebSocket-Extensions”,“permessage-deflate; client_max_window_bits”);

mWs = new WebSocketClient(uri,httpHeadersMap){

@Override

public void onOpen(ServerHandshake serverHandshake) {

System.out.println(“open…”);

System.out.println(serverHandshake.getHttpStatus());

mWs.send(“666”);

}

@Override

public void onMessage(String s) {

System.out.println(s);

}

@Override

public void onClose(int i, String s, boolean b) {

System.out.println(“close…”);

System.out.println(i);

System.out.println(s);

System.out.println(b);

}

@Override

public void onError(Exception e) {

System.out.println(“发生了错误…”);

}

};

mWs.connect();

} catch (Exception e) {

e.printStackTrace();

}

}

}

// 调用后报错 直接关闭了连接 状态码为1002

// close…

// 1002

// Invalid status code received: 400 Status line: HTTP/1.1 400 Client must support ‘binary’ or ‘base64’ protocol

​ 发生错误后,发现关键地方,客户端必须支持 binary或base64协议,一番搜索后再Stack Overflow找到了线索,并且是Kanaka(noVNC和websockify的开发者)亲自回答的,大概意思就是拟需要在构造函数中提供这些协议。

Stack Overflow回答

​ 然后我又在websockify.js的源码中找到了这个构造,确实需要传递一个protocols的数组参数,可是这是前端,并不知道Java如何完成这个操作。

websocket.js

  • 后续

​ 首先再次感谢开源项目和各位博主大佬的分享,依旧在寻找解决方案…

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

读者福利

由于篇幅过长,就不展示所有面试题了,感兴趣的小伙伴

35K成功入职:蚂蚁金服面试Java后端经历!「含面试题+答案」

35K成功入职:蚂蚁金服面试Java后端经历!「含面试题+答案」

35K成功入职:蚂蚁金服面试Java后端经历!「含面试题+答案」

更多笔记分享

35K成功入职:蚂蚁金服面试Java后端经历!「含面试题+答案」
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
e5c14a7895254671a72faed303032d36.jpg" alt=“img” style=“zoom: 33%;” />

读者福利

由于篇幅过长,就不展示所有面试题了,感兴趣的小伙伴

[外链图片转存中…(img-zzuxT868-1713407323853)]

[外链图片转存中…(img-kRCDs7NQ-1713407323854)]

[外链图片转存中…(img-PQqLvImM-1713407323854)]

更多笔记分享

[外链图片转存中…(img-0pXXU5dt-1713407323854)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值