基于 UNIX Socket 实现 RestTemplate

# 方式一

实现ClientHttpRequestFactory

# 依赖

利用了junixsocket这个 native 库以及其对 socket 的封装

        <dependency>
            <groupId>com.kohlschutter.junixsocket</groupId>
            <artifactId>junixsocket-core</artifactId>
            <version>2.1.2</version>
        </dependency>
        <dependency>
            <groupId>com.kohlschutter.junixsocket</groupId>
            <artifactId>junixsocket-rmi</artifactId>
            <version>2.1.2</version>
        </dependency>

# 实现

# org.springframework.http.client

新建包org.springframework.http.client,这是因为使用了具有默认访问权限的一些Abstract

# JnrClientHttpRequestFactory

  • 在包内新建JnrClientHttpRequestFactory类,实现ClientHttpRequestFactory接口

  • 新建了具有制定一个sock文件的构造器,并转换成AFUNIXSocketAddress类,该类为InetSocketAddress的子类

  • 实现createRequest方法,在该方法创建一个socket 连接,并返回JnrBufferingClientHttpRequest(下一步将创建)

public class JnrClientHttpRequestFactory implements ClientHttpRequestFactory {

    private AFUNIXSocketAddress address;

    public JnrClientHttpRequestFactory(File socketFile) throws IOException {
        this.address = new AFUNIXSocketAddress(socketFile);
    }

    @Override
    public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
        AFUNIXSocket sock = AFUNIXSocket.connectTo(address);
        return new JnrBufferingClientHttpRequest(sock, uri, httpMethod);
    }
}

# JnrBufferingClientHttpRequest

  • 同样在包内新建JnrBufferingClientHttpRequest类,继承AbstractBufferingClientHttpRequest

  • 这里创建了三个变量储存构造器传来的参数。分别是AFUNIXSocketURIHttpMethod

  • 实现executeInternal方法,在这里打印请求

  • 实现getMethodValuegetURI

    private AFUNIXSocket afunixSocket;
    private URI uri;
    private HttpMethod httpMethod;

    public JnrBufferingClientHttpRequest(AFUNIXSocket afunixSocket, URI uri, HttpMethod httpMethod) {
        this.afunixSocket = afunixSocket;
        this.uri = uri;
        this.httpMethod = httpMethod;
    }

    @Override
    protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
        OutputStream outputStream = afunixSocket.getOutputStream();
        PrintWriter writer = new PrintWriter(outputStream);

        writer.println(String.format("%s %s HTTP/1.1", httpMethod, uri));

        headers
                .forEach((key, values) -> {
                    if (values.isEmpty()) return;

                    writer.append(key).append(": ");
                    if (values.size() == 1)
                        writer.append(values.get(0));
                    else
                        values.forEach(value -> writer.append(value).append("; "));
                    writer.append("\\r");
                });

        writer.println("Host: http");
        writer.println("");
        outputStream.write(bufferedOutput);
        writer.flush();
        writer.close();
        return new JnrClientHttpResponse(afunixSocket);
    }

    @Override
    public String getMethodValue() {
        return httpMethod.name();
    }

    @Override
    public URI getURI() {
        return uri;
    }

# JnrClientHttpResponse

  • 包内创建JnrClientHttpResponse类,继承AbstractClientHttpResponse
  • 创建InputStreamHttpHeadersstatusCodestatusTest等 response 字段
  • 在构造器阶段读取响应
    • 首先读取请求行
    • 然后是解析请求头
    • 此时Inputstream将标记到剩下的请求体部分
  • 其中readLine为读取一行数据,readResponseHead为读取请求行
  • 实现getRawStatusCodegetStatusTextgetHeaders方法
  • close方法参考原生SimpleClientHttpResponse
  • getBody方法返回已经标记到请求体的inputStream
    private InputStream inputStream;
    private HttpHeaders httpHeaders;
    private int statusCode = -1;
    private String statusTest = "UNKNOWN";

    JnrClientHttpResponse(AFUNIXSocket afunixSocket) throws IOException {
        this.inputStream = afunixSocket.getInputStream();

        readResponseHead(readLine(inputStream));

        httpHeaders = new HttpHeaders();
        String line;
        while (!(line = readLine(inputStream)).isEmpty()) {
            String[] strings = line.split(" ", 2);
            httpHeaders.add(strings[0], strings[1]);
        }
    }

    @Override
    public int getRawStatusCode() throws IOException {
        return statusCode;
    }

    @Override
    public String getStatusText() throws IOException {
        return statusTest;
    }

    @Override
    public void close() {
        try {
            StreamUtils.drain(this.inputStream);
            this.inputStream.close();
        } catch (Exception ex) {
            // ignore
        }
    }

    @Override
    public InputStream getBody() throws IOException {
        return inputStream;
    }

    @Override
    public HttpHeaders getHeaders() {
        return httpHeaders;
    }

    private void readResponseHead(String line) {
        String[] strings = line.split(" ", 3);

        statusCode = Integer.valueOf(strings[1]);
        statusTest = strings[2];
    }

    private String readLine(InputStream inputStream) throws IOException {
        StringBuilder stringBuilder = new StringBuilder();

        int ch;
        while ((ch = inputStream.read()) != -1) {
            if (ch == '\\r') {
                //noinspection ResultOfMethodCallIgnored
                inputStream.read();
                return stringBuilder.toString();
            }

            stringBuilder.append((char) ch);
        }

        return stringBuilder.toString();
    }

# 测试

编写测试代码

    @Test
    public void contextLoads() throws IOException {
        RestTemplate restTemplate = new RestTemplate(new JnrClientHttpRequestFactory(Paths.get("/var/run/docker.sock").toFile()));
        HttpEntity<String> object = restTemplate.getForEntity("/v1.24/images/json", String.class);
        log.info(objectMapper.writeValueAsString(object));
    }

得到响应

2019-02-05 22:35:57.730  INFO 34943 --- [           main] p.a.t.jnrtest.JnrTestApplicationTests    : {"headers":{"Date:":["Tue, 05 Feb 2019 14:35:57 GMT"],"Docker-Experimental:":["false"],"Server:":["Docker/18.09.1 (linux)"],"Content-Length:":["327"],"Ostype:":["linux"],"Api-Version:":["1.39"],"Content-Type:":["application/json"]},"body":"[{\\\\"Containers\\\\":-1,\\\\"Created\\\\":1548886792,\\\\"Id\\\\":\\\\"sha256:caf27325b298a6730837023a8a342699c8b7b388b8d878966b064a1320043019\\\\",\\\\"Labels\\\\":null,\\\\"ParentId\\\\":\\\\"\\\\",\\\\"RepoDigests\\\\":[\\\\"alpine@sha256:b3dbf31b77fd99d9c08f780ce6f5282aba076d70a513a8be859d8d3a4d0c92b8\\\\"],\\\\"RepoTags\\\\":[\\\\"alpine:latest\\\\"],\\\\"SharedSize\\\\":-1,\\\\"Size\\\\":5529164,\\\\"VirtualSize\\\\":5529164}]\\","statusCodeValue":200,"statusCode":"OK"}

# 方式二

# 依赖

OkHttp

        <dependency>
            <groupId>com.kohlschutter.junixsocket</groupId>
            <artifactId>junixsocket-core</artifactId>
            <version>2.1.2</version>
        </dependency>
        <dependency>
            <groupId>com.kohlschutter.junixsocket</groupId>
            <artifactId>junixsocket-rmi</artifactId>
            <version>2.1.2</version>
        </dependency>
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>3.12.1</version>
        </dependency>

# 实现

# JnrAFUNIXSocketFactory

  • 新建该类,继承AFUNIXSocketFactory
  • 创建构造器,如方式一中提供了一个AFUNIXSocketAddress变量
  • 实现createSocket此处需要注意一定要使用super.createSocket()以绑定自身factory不然解析URL会出错
  • 然后connect到 UNIX Socket 中获得fd文件描述符,并将其返回
  • 实现addressFromHost直接返回 socket 地址,因为没有hostport属性,参数可以直接忽略
private AFUNIXSocketAddress address;

public JnrAFUNIXSocketFactory(File socketFile) throws IOException {
    address = new AFUNIXSocketAddress(socketFile);
}

@Override
public Socket createSocket() throws IOException {
    Socket socket = super.createSocket();
    socket.connect(address);
    return socket;
}

@Override
protected AFUNIXSocketAddress addressFromHost(String s, int i) throws IOException {
    return address;
}

# 测试

这里使用了OkHttpClientBuilder以便使用自定义的socketFactory

然后在RestTemplate中指定ClientHttpRequestFactory

注意这一次 URI 需要加上schemahttphost也需要指定例如localhost,以通过 URL 的校验,否则会报错

不过使用中会被忽略,因为会直接使用指定的socketFile

    @Test
    public void okHttp() throws IOException {
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        OkHttpClient okHttpClient = builder
                .socketFactory(new JnrAFUNIXSocketFactory(Paths.get("/var/run/docker.sock").toFile()))
                .build();

        OkHttp3ClientHttpRequestFactory okHttp3ClientHttpRequestFactory =
                new OkHttp3ClientHttpRequestFactory(okHttpClient);


        RestTemplate restTemplate = new RestTemplate(okHttp3ClientHttpRequestFactory);
        HttpEntity<String> object = restTemplate.getForEntity("http://localhost/v1.24/images/json", String.class);
        log.info(objectMapper.writeValueAsString(object));
    }

得到结果

2019-02-05 22:58:01.957  INFO 35177 --- [           main] p.a.t.jnrtest.JnrTestApplicationTests    : {"headers":{"Api-Version":["1.39"],"Ostype":["linux"],"Content-Type":["application/json"],"Docker-Experimental":["false"],"Content-Length":["327"],"Date":["Tue, 05 Feb 2019 14:58:01 GMT"],"Server":["Docker/18.09.1 (linux)"]},"body":"[{\\\\"Containers\\\\":-1,\\\\"Created\\\\":1548886792,\\\\"Id\\\\":\\\\"sha256:caf27325b298a6730837023a8a342699c8b7b388b8d878966b064a1320043019\\\\",\\\\"Labels\\\\":null,\\\\"ParentId\\\\":\\\\"\\\\",\\\\"RepoDigests\\\\":[\\\\"alpine@sha256:b3dbf31b77fd99d9c08f780ce6f5282aba076d70a513a8be859d8d3a4d0c92b8\\\\"],\\\\"RepoTags\\\\":[\\\\"alpine:latest\\\\"],\\\\"SharedSize\\\\":-1,\\\\"Size\\\\":5529164,\\\\"VirtualSize\\\\":5529164}]\\","statusCodeValue":200,"statusCode":"OK"}