最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Springboot實(shí)現(xiàn)VNC的反向代理功能

 更新時(shí)間:2021年09月29日 16:11:40   作者:茈情苛侍  
這篇文章主要介紹了Springboot實(shí)現(xiàn)VNC的反向代理,搭建過(guò)程也很簡(jiǎn)單,通過(guò)注冊(cè)bean攔截指定URL路徑進(jìn)行自定義操作,具體實(shí)例代碼跟隨小編一起看看需要的朋友可以參考下

背景

​ 用戶(hù)需要通過(guò)前端HTML頁(yè)面的noVNC(noVNC是什么?)客戶(hù)端連接底層VNC Server服務(wù)端,為了防止VNC Server的IP暴露,因此需要做一層代理。正常情況下使用Nginx、Apache等都可以搞定,但是由于項(xiàng)目架構(gòu)的一些問(wèn)題,暫時(shí)不能再加一臺(tái)反向代理服務(wù)器,所以決定寫(xiě)一個(gè)單獨(dú)的模塊實(shí)現(xiàn)反向代理的功能。

​ 在網(wǎng)上和Github上找了一下,使用了HTTP-Proxy-Servlet,引入該依賴(lài)搭建一個(gè)Spring Boot項(xiàng)目。

搭建

引入代理的依賴(lài)

<dependency>
    <groupId>org.mitre.dsmiley.httpproxy</groupId>
    <artifactId>smiley-http-proxy-servlet</artifactId>
    <version>1.12</version>
</dependency>

通過(guò)注冊(cè)bean攔截指定URL路徑進(jìn)行自定義操作

@Configuration
public class ProxyServletConfiguration {

    // 攔截所有請(qǐng)求交給下面的VNCProxyServlet去處理
    private final static String SERVLET_URL = "/*";

    @Bean
    public ServletRegistrationBean<VNCProxyServlet> servletServletRegistrationBean() {
        ServletRegistrationBean<VNCProxyServlet> servletRegistrationBean = new ServletRegistrationBean<>(new VNCProxyServlet(), SERVLET_URL);
        //設(shè)置網(wǎng)址以及參數(shù)
        Map<String, String> params = ImmutableMap.of(
                "targetUri", "null", //這里寫(xiě)null是因?yàn)閠argetUri是在自定義的VNCProxyServlet類(lèi)中動(dòng)態(tài)傳入的,而且這里必須要有值
                ProxyServlet.P_LOG, "true",
                ProxyServlet.P_PRESERVEHOST,"true",
                ProxyServlet.P_PRESERVECOOKIES,"true"
        );
        servletRegistrationBean.setInitParameters(params);
        return servletRegistrationBean;
    }

}

這里遇到的坑:

​ 剛開(kāi)始其實(shí)是準(zhǔn)備在已有的一個(gè)模塊中加上這個(gè)代理功能,因?yàn)榭梢灾付〝r截的路徑,比如只攔截請(qǐng)求路徑為/proxy/*的,然后交給自定義的Servlet去代理,后來(lái)寫(xiě)好測(cè)試時(shí),發(fā)現(xiàn)代理過(guò)去后代理目標(biāo)主頁(yè)一片空白,看了控制臺(tái)的Network后,主頁(yè)確實(shí)是返回200且加載正常,但是由主頁(yè)發(fā)起的js、css和img等靜態(tài)資源狀態(tài)碼都為404。

​ 當(dāng)時(shí)以為是代碼的問(wèn)題,后來(lái)發(fā)現(xiàn)靜態(tài)資源都是相對(duì)路徑的有問(wèn)題,如果前端的靜態(tài)資源是引入第三方的,比如從CDN中引入Vue.js則不會(huì)出現(xiàn)問(wèn)題,都可以正常的被代理。既然狀態(tài)碼是404,那肯定是找不到這個(gè)資源,看了一下發(fā)現(xiàn)如果在靜態(tài)資源的路徑前加上指定攔截的路徑/proxy/就可以被正常代理。此時(shí)才明白,因?yàn)樵L問(wèn)首頁(yè)的路徑中帶/proxy/是在地址欄主動(dòng)輸入的,所以請(qǐng)求到后臺(tái),后臺(tái)Servlet攔截發(fā)現(xiàn)路徑中帶/proxy/,把該請(qǐng)求交給自定義的代理Servlet去處理然后返回。而主頁(yè)上的js、css等靜態(tài)資源發(fā)起請(qǐng)求的路徑是不會(huì)帶/proxy/*的,因此不會(huì)走到代理Servlet,并且代理模塊中也沒(méi)有相應(yīng)資源路徑,所以就理所應(yīng)當(dāng)?shù)姆祷亓?04。

​ 為此還專(zhuān)門(mén)在GitHub上問(wèn)了一下作者,作者也是回復(fù)說(shuō)這并不是這個(gè)代理模塊該做的事,最好是前端處理,或者讓前端使用絕對(duì)路徑。附上地址(Discussions

​ 最后就是決定單獨(dú)拉出來(lái)寫(xiě)一個(gè)Spring Boot項(xiàng)目做這個(gè)代理功能模塊,直接代理/*,這樣所有請(qǐng)求到這個(gè)模塊的都會(huì)被代理。

自定義Servlet實(shí)現(xiàn)動(dòng)態(tài)代理目標(biāo)地址

// VNCProxyServlet繼承了ProxyServlet 重寫(xiě)了service方法 在方法中添加自定義操作 從請(qǐng)求地址中動(dòng)態(tài)獲取
@Override
    protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws ServletException, IOException {
        // 獲取請(qǐng)求地址
        String targetUri = servletRequest.getRequestURL().toString();
		
        // 正則取請(qǐng)求地址中的參數(shù) 參數(shù)是放在域名中的
        Matcher matcher = DOMAIN_REX.matcher(targetUri);
        if(!matcher.find()){
            // 自定義的異常
            throw new GenericException("從域名中獲取vmId異常!");
        }
        // 取域名中的第一個(gè) eg: http://vmId.xxx.cn得 [vmId,xxx,cn] 得 vmId
        Long vmId = Long.valueOf(matcher.group().split("\\.")[0]);
        
        // eg:業(yè)務(wù)邏輯根據(jù)vmId去拿 targetUri
    	targetUri = vmService.getTargetUrl(vmId);
	
        if (StringUtils.isEmpty(targetUri)) {
            throw new GenericException("代理路徑不正確,請(qǐng)確認(rèn)路徑");
        }

        // 設(shè)置Url
        if (servletRequest.getAttribute(ATTR_TARGET_URI) == null) {
            servletRequest.setAttribute(ATTR_TARGET_URI, targetUri);
        }

        // 設(shè)置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()));
        }
		// 下面大部分都是父類(lèi)的源碼 沒(méi)有需要特別修改的地方
        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());
            }
        }
    }

​ 這里主要列出關(guān)鍵部分,因?yàn)榉桨高€沒(méi)有完結(jié)。

問(wèn)題

​ 本以為這樣就成功了,但是測(cè)試之后發(fā)現(xiàn)頁(yè)面和靜態(tài)資源都代理過(guò)去了,但是有一個(gè)websocket請(qǐng)求失敗了。像noVNC這種網(wǎng)頁(yè)版的黑窗口,早就該想到肯定是用websocket這種長(zhǎng)鏈接的請(qǐng)求進(jìn)行交互的。后來(lái)去搜了一下這個(gè)叫websockify的請(qǐng)求,就是最開(kāi)始介紹noVNC博客中介紹的:

​ 瀏覽器不支持VNC,所以不能直接連接VNC,但是可以使用代理,使用noVNC通過(guò)WebSocket建立連接,而VNC Server不支持WebSocket,所以需要開(kāi)啟Websockify代理來(lái)做WebSocket和TCP Socket之間的轉(zhuǎn)換,這個(gè)代理在noVNC的目錄里,叫做websockify。

​ 此時(shí)項(xiàng)目是能夠攔截到websockify這個(gè)請(qǐng)求的,但是由于servlet把這個(gè)請(qǐng)求當(dāng)成普通的請(qǐng)求去代理到目標(biāo)服務(wù)器,這樣是無(wú)法成功的,所以要做的就是類(lèi)似實(shí)現(xiàn)一個(gè)websocket的反向代理,搜了一下的話發(fā)現(xiàn)例子不是很多,大多都是在前端做的,前端作為客戶(hù)端與服務(wù)端建立websocket連接,但目前的狀況很明顯是需要這個(gè)代理模塊既做websocket服務(wù)端與web端建立連接,再作為websocket客戶(hù)端與VNC 服務(wù)端建立連接,然后進(jìn)行交互傳遞通信。

​ 后面也找到了這篇通過(guò)noVNC和websockify連接到QEMU/KVM,然后總結(jié)一下從用戶(hù)發(fā)出請(qǐng)求到得到響應(yīng)的流程:

PC Chrome(客戶(hù)端) => noVNC Server(noVNC端) => websockify(websocket轉(zhuǎn)TCP Socket) => VNC Server(VNC服務(wù)端) => websockify(TCP Socket轉(zhuǎn)websocket) => noVNC Server(noVNC端)=> PC Chrome(客戶(hù)端)

用戶(hù)使用PC Chrome瀏覽器請(qǐng)求 noVNC端(因?yàn)闊o(wú)法直接訪問(wèn)VNC Server端,VNC Server是不支持Websocket連接),經(jīng)由websockify將websocket轉(zhuǎn)為T(mén)CP Socket請(qǐng)求到VNC服務(wù)端,返回TCP響應(yīng),經(jīng)由websockify轉(zhuǎn)換為websocket返回給客戶(hù)端瀏覽器,這樣來(lái)進(jìn)行交互。整個(gè)過(guò)程 websockify 代理器是關(guān)鍵,noVNC 可以被放在瀏覽器端。

1.noVNC網(wǎng)頁(yè)端與代理模塊建立websocket通信

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
@ServerEndpoint("/websockify")
@Component
public class WebSocketServer {

    /**
     * 連接建立成功調(diào)用的方法
     */
    @OnOpen
    public void onOpen(Session session) {
        logger.info("open...");
    }

    /**
     * 連接關(guān)閉調(diào)用的方法
     */
    @OnClose
    public void onClose() {
        logger.info("close...");
    }

    /**
     * 收到客戶(hù)端消息后調(diào)用的方法
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        logger.info(message);
    }

    /**
     * 發(fā)生錯(cuò)誤調(diào)用的方法
     */
    @OnError
    public void onError(Session session, Throwable error) {
        logger.error("用戶(hù)錯(cuò)誤原因:"+error.getMessage());
        error.printStackTrace();
    }

}

​ 都是很常用的websocket服務(wù)端的代碼,唯一要注意的是前端請(qǐng)求'/websockify'地址發(fā)起websocket連接時(shí),要注意用ip,尤其是本地,使用localhost會(huì)報(bào)錯(cuò),要使用127.0.0.1。最后測(cè)試連接成功,返回狀態(tài)碼101,并且消息可以正常接收。noVNC網(wǎng)頁(yè)端與代理模塊建立websocket通信完成。

2.代理模塊與VNC Server建立websocket通信

java后臺(tái)作為websocket客戶(hù)端很少,大多是用Netty去寫(xiě)的,但是不適合目前的情況,最后還是找到了一個(gè)感覺(jué)比較合適的

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("發(fā)生了錯(cuò)誤...");
                }
            };
            mWs.connect();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

// 調(diào)用后報(bào)錯(cuò)  直接關(guān)閉了連接  狀態(tài)碼為1002 
// close...
// 1002
// Invalid status code received: 400 Status line: HTTP/1.1 400 Client must support 'binary' or 'base64' protocol

​ 發(fā)生錯(cuò)誤后,發(fā)現(xiàn)關(guān)鍵地方,客戶(hù)端必須支持 binary或base64協(xié)議,一番搜索后再Stack Overflow找到了線索,并且是Kanaka(noVNC和websockify的開(kāi)發(fā)者)親自回答的,大概意思就是你需要在構(gòu)造函數(shù)中提供這些協(xié)議。

​ 然后我又在websockify.js的源碼中找到了這個(gè)構(gòu)造,確實(shí)需要傳遞一個(gè)protocols的數(shù)組參數(shù),可是這是前端,并不知道Java如何完成這個(gè)操作。

后續(xù)

​ 首先再次感謝開(kāi)源項(xiàng)目和各位博主大佬的分享,依舊在尋找解決方案......

到此這篇關(guān)于Springboot實(shí)現(xiàn)VNC的反向代理功能的文章就介紹到這了,更多相關(guān)Springboot反向代理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Spark操作之a(chǎn)ggregate、aggregateByKey詳解

    Spark操作之a(chǎn)ggregate、aggregateByKey詳解

    這篇文章主要介紹了Spark操作之a(chǎn)ggregate、aggregateByKey詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-06-06
  • Java實(shí)現(xiàn)將PDF轉(zhuǎn)為圖片格式的方法詳解

    Java實(shí)現(xiàn)將PDF轉(zhuǎn)為圖片格式的方法詳解

    PDF文件和圖片文件,這是兩種完全不一樣的格式,可是有的時(shí)候這兩種格式卻是有相互轉(zhuǎn)換的需要,本文將介紹如何通過(guò)Java應(yīng)用程序快速高效地將PDF轉(zhuǎn)為圖片格式。一起來(lái)看看吧
    2023-03-03
  • Java計(jì)算字符串公式的方式解讀

    Java計(jì)算字符串公式的方式解讀

    這篇文章主要介紹了Java計(jì)算字符串公式的方式解讀,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-12-12
  • spring?security需求分析與基礎(chǔ)環(huán)境準(zhǔn)備教程

    spring?security需求分析與基礎(chǔ)環(huán)境準(zhǔn)備教程

    這篇文章主要為大家介紹了spring?security需求分析與基礎(chǔ)環(huán)境準(zhǔn)備教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步
    2022-03-03
  • Redis介紹和使用場(chǎng)景詳解

    Redis介紹和使用場(chǎng)景詳解

    這篇文章主要為大家詳細(xì)介紹了Redis介紹和使用場(chǎng)景,需要的朋友可以參考,具體內(nèi)容如下
    2018-04-04
  • Mybatis?Plus?新版lambda?表達(dá)式查詢(xún)異常的處理

    Mybatis?Plus?新版lambda?表達(dá)式查詢(xún)異常的處理

    這篇文章主要介紹了Mybatis?Plus?新版lambda?表達(dá)式查詢(xún)異常的處理方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-01-01
  • 淺析Java 反射機(jī)制的用途和缺點(diǎn)

    淺析Java 反射機(jī)制的用途和缺點(diǎn)

    這篇文章給大家分析了Java 反射機(jī)制的用途和缺點(diǎn)以及相關(guān)知識(shí)點(diǎn)內(nèi)容,有興趣的朋友可以參考學(xué)習(xí)下。
    2018-07-07
  • 如何巧用HashMap一行代碼統(tǒng)計(jì)單詞出現(xiàn)次數(shù)詳解

    如何巧用HashMap一行代碼統(tǒng)計(jì)單詞出現(xiàn)次數(shù)詳解

    這篇文章主要給大家介紹了關(guān)于如何巧用HashMap一行代碼統(tǒng)計(jì)單詞出現(xiàn)次數(shù)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-07-07
  • Java?zxing實(shí)現(xiàn)生成并解析二維碼與條形碼

    Java?zxing實(shí)現(xiàn)生成并解析二維碼與條形碼

    這篇文章主要為大家詳細(xì)介紹了Java如何通過(guò)zxing實(shí)現(xiàn)生成并解析二維碼與條形碼,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下
    2024-11-11
  • 詳解Spring整合mybatis--Spring中的事務(wù)管理(xml形式)

    詳解Spring整合mybatis--Spring中的事務(wù)管理(xml形式)

    這篇文章主要介紹了Spring整合mybatis--Spring中的事務(wù)管理(xml形式),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2023-11-11

最新評(píng)論

沅江市| 麻阳| 呼伦贝尔市| 拜泉县| 海丰县| 永登县| 兴化市| 宁陵县| 集安市| 景东| 英山县| 壶关县| 天全县| 澳门| 灵武市| 通江县| 微山县| 贡觉县| 徐水县| 衡阳县| 紫金县| 莒南县| 定日县| 曲松县| 全椒县| 繁昌县| 武宁县| 桐乡市| 威远县| 自治县| 琼结县| 建瓯市| 迁安市| 饶阳县| 界首市| 延川县| 商水县| 阳城县| 梅州市| 威海市| 资源县|