如何在两个Web服务间维持持久连接并建立跨防火墙的双工通信
我需要从防火墙后向外发起SSL连接对接外部云Web服务,且保持该连接开启以实现双工通信,为便于理解,我将该需求拆分为两个阶段如下:
阶段1:实现客户端向云服务的出站连接复用
我有一个安装运行在客户防火墙后的agent,它会通过443端口向外部云Web服务发起连接。
我尝试了两种实现方式:
- SSL socket客户端
public class SSLSocketClient { public static void main(String[] args) throws Exception { try { SSLSocketFactory factory = (SSLSocketFactory)SSLSocketFactory.getDefault(); SSLSocket socket = (SSLSocket)factory.createSocket("mycloud.com", 443); socket.setKeepAlive(true); socket.startHandshake(); boolean isLive = true; PrintWriter out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( socket.getOutputStream()))); out.println("GET /some/orgs/myorg HTTP/1.1"); out.println("Host: mycloud.com"); out.println("Accept: application/json"); out.println("X-Api-Key: knwysf24tzeatwk5dwnqa6xh"); out.println(); out.flush(); /* * Make sure there were no surprises */ if (out.checkError()) System.out.println( "SSLSocketClient: java.io.PrintWriter error"); /* read response */ BufferedReader in = new BufferedReader( new InputStreamReader( socket.getInputStream())); String inputLine; while ((inputLine = in.readLine()) != null) System.out.println(inputLine); in.close(); out.close(); socket.close(); } catch (Exception e) { e.printStackTrace(); } } }
该方案中,每次请求响应周期结束后连接就会关闭,每次发起新请求都需要重新建立连接。
为避免重复建连问题,我尝试使用apache commons-httpclient库:
- 使用apache commons-httpclient库创建HTTP连接
public class HttpClientTest { private static int connectionTimeout = 1000; private static int socketTimeout = 10000; private static String host = "mycloud.com"; private static int port = 443; static boolean useExpectContinue = true; @SuppressWarnings("deprecation") public static DefaultHttpClient configureClient() { HttpParams params = new BasicHttpParams(); HttpProtocolParams.setUseExpectContinue(params, useExpectContinue); HttpConnectionParams .setConnectionTimeout(params, connectionTimeout); HttpConnectionParams.setSoTimeout(params, socketTimeout); HttpConnectionParams.setTcpNoDelay(params, Boolean.TRUE); String protocol = "https"; params.setParameter(ClientPNames.DEFAULT_HOST, new HttpHost(host, port, protocol)); DefaultHttpClient client = new DefaultHttpClient(params); return client; } @SuppressWarnings("deprecation") public static void main(String args []) throws IOException { String uri = "/idbridge/orgs/cableco_rt"; HttpGet httpGet = new HttpGet(uri); httpGet.setHeader("Host",host); httpGet.setHeader("Accept","application/json"); httpGet.setHeader("X-Api-Key","knwysf24tzeatwk5dwnqa6xh"); String uriOrg = "/idbridge/orgs"; HttpGet httpGetOrg = new HttpGet(uriOrg); httpGetOrg.setHeader("Host",host); httpGetOrg.setHeader("Accept","application/json"); httpGetOrg.setHeader("X-Api-Key","knwysf24tzeatwk5dwnqa6xh"); @SuppressWarnings("deprecation") DefaultHttpClient client = configureClient(); HttpParams params = client.getParams(); System.out.println("Client connected to -> " + params.getParameter("http.default-host") +"\n ______________________________"); System.out.println("Request -> " + httpGet.getURI()+"\n"); org.apache.http.HttpResponse rsp = client.execute(httpGet); HttpEntity entity = rsp.getEntity(); Header[] allHeaders = rsp.getAllHeaders(); System.out.println("Response" + " : "+EntityUtils.toString(entity,"UTF-8") +"\n______________________________"); org.apache.http.HttpResponse rspOrg = client.execute(httpGetOrg); System.out.println("Request -> " + httpGetOrg.getURI()+"\n"); HttpEntity entityOrg = rspOrg.getEntity(); Header[] allHeadersOrg = rspOrg.getAllHeaders(); System.out.println("Response" + " : "+EntityUtils.toString(entityOrg,"UTF-8")); } }
该方案解决了每次请求都需要重新建连的问题,只需创建一次DefaultHttpClient就可以多次复用。
阶段2:实现连接双工通信,支持云服务反向调用
我第二阶段的问题是要维持该连接,即我的源Web服务按上述方法创建的连接,后续目标Web服务需要发起请求时也可以复用该连接,需要创建某种hook或callback来开启双向通信,也就是双工通信。
为解决该问题我尝试使用了AsyncClientHttpExchange:
public class AsyncClientHttpExchange { public static void main(final String[] args) throws Exception { CloseableHttpAsyncClient httpclient = HttpAsyncClients.createDefault(); try { httpclient.start(); HttpGet request = new HttpGet("http://localhost:9090/connect"); Future<HttpResponse> future = httpclient.execute(request, null); HttpResponse response = future.get(); System.out.println(EntityUtils.toString(response.getEntity(),"UTF-8") ); System.out.println("Response: " + response.getStatusLine()); System.out.println("Shutting down"); } finally { httpclient.close(); } System.out.println("Done"); } }
但该方案仍然无法解决我callback/hook的需求,我需要的正是以下描述的行为:
回调:服务端在完成任务后主动发起调用,切换为客户端角色向原始客户端(此时客户端充当服务端)发起HTTP请求。两端需要共用一套协议约定(比如特定格式的PUT或POST请求)。
希望获得实现该功能的代码参考。
补充用例说明
服务S1和云服务CS2,S1部署在本地机房对接用户源,S1可以从用户源拉取数据发送到云服务CS2(S1可通过443端口向CS2发起出站SSL连接)。后续如果有用户在CS2发起认证请求,CS2需要调用本地S1服务对接用户源完成认证。注意:S1和用户源都处于防火墙后,仅支持S1主动向CS2发起443端口出站SSL连接,请问如何实现CS2按需向防火墙后的S1发起请求?
内容的提问来源于stack exchange,提问作者Afgan

