使用JDK11自带的HttpClient发送苹果VOIP推送,P12证书
前言
至于voip推送是啥,就不多说了,看到这的人,应该都知道了,实在不知道就去苹果开发者文档
两个核心,HTTP/2,TLSv1.2
HTTP/2,是HTTP/1.1的升级版,有啥好处,自行了解
TLS是SSL的升级版,目的是为了安全,有啥好处,自行了解
英语我也看不懂,转换成白话如下
推送地址:
沙盒:https://api.sandbox.push.apple.com/3/device/{token}
生产:https://api.push.apple.com/3/device/{token}
请求参数如下
参数名 | 必选 | 类型 | 说明 |
---|---|---|---|
apns-id | N | Header | 通知id,一般用UUID,32位小写以 8-4-4-4-12 形式由连字符隔开的五组显示 |
apns-push-type | Y | Header | alert,background,voip,complication,fileprovider,mdm 六选一,我用到的是voip |
apns-expiration | Y | Header | 通知不再有效的日期。时间戳,如果传0则仅推送一次 |
apns-priority | Y | Header | 通知的优先级。如果省略此标题,APN 将通知优先级设置为 10。10 指定立即发送通知 5 根据用户设备上的电源考虑指定发送通知 |
apns-topic | Y | Header | 通知主题,一般是bundle ID/app ID加上后缀,比如voip则加上.voip |
无 | Y | body | 需要传输的json数据 |
JDK9+的代码
因为从JDK9开始才支持HTTP/2,所以使用这种方式JDK必须大于等于JDK9,我在JDK11下测试无误
package cn.mengxw;
import org.apache.hc.core5.ssl.SSLContextBuilder;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import java.io.File;
import java.io.FileInputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.KeyStore;
import java.time.Duration;
import java.util.UUID;
public class HttpCli {
public static void main(String[] args) throws Exception {
long time1 = System.currentTimeMillis();
System.setProperty("javax.net.debug", "all");
// 证书文件路径
String path = "";
// 证书文件路径
String password = "";
// 需要推送到的toekn
String token = "";
// topic
String topic = "".concat(".voip");
// 如要传输的json字符串
String jsonData = "";
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(new FileInputStream(new File(path)), password.toCharArray());
// 借用了SSLContextBuilder,自己构建也行,但是这个方便
SSLContext sslContext =
new SSLContextBuilder()
.loadKeyMaterial(keyStore, password.toCharArray())
.setKeyStoreType("PKCS12")
.build();
SSLParameters sslParameters = new SSLParameters();
sslParameters.setProtocols(new String[] {"TLSv1.2"});
HttpClient client =
HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2) // default
.followRedirects(
HttpClient.Redirect.ALWAYS) // /Always redirect, except from HTTPS URLs to HTTP URLs
.connectTimeout(Duration.ofMillis(5000))
.sslContext(sslContext)
.sslParameters(sslParameters)
// .proxy(ProxySelector.of(new InetSocketAddress("localhost", 8888)))
.build();
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create("https://api.sandbox.push.apple.com/3/device/" + token))
.header("apns-id", UUID.randomUUID().toString().toLowerCase())
.headers("apns-push-type", "voip")
.header("apns-expiration", "0")
.headers("apns-priority", "10")
.headers("apns-topic", topic)
.POST(HttpRequest.BodyPublishers.ofString(jsonData))
.timeout(Duration.ofMillis(5009))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
long time2 = System.currentTimeMillis();
System.out.println("耗时:" + (time2 - time1) / 1000);
System.out.println(response.statusCode());
System.out.println(response);
System.out.println(response.body());
}
}
JDK7+
前面说了,JDK9才开始支持HTTP/2,但是我们生产环境用的是JDK8,那这个就很操蛋,不知道还有多少的小伙伴们也是。那这样子前面的代码就不能够使用了,继续翻阅文档后发现,httpcomponents-client在5.0开始支持HTTP/2了,并且支持从JDK1.7+运行,整合我意,我贴出官方地址,有意可自行前往
https://hc.apache.org/httpcomponents-core-5.1.x/examples.html
这个是官方例子
https://github.com/apache/httpcomponents-core/blob/5.1.x/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2ConscriptRequestExecutionExample.java
截止目前,最新的maven坐标,官方为了和前一个版本区分,加了两个5
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents.client5/httpclient5 -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.1</version>
</dependency>
httpclient5中对HTTP/2的处理是异步的,主要是通过 FutureCallback<Message<HttpResponse, String>>来处理响应
废话不多说,具体的请求代码如下
import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.http.*;
import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester;
import org.apache.hc.core5.http.nio.AsyncClientEndpoint;
import org.apache.hc.core5.http.nio.AsyncRequestProducer;
import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer;
import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
import org.apache.hc.core5.http.nio.support.AsyncRequestBuilder;
import org.apache.hc.core5.http.nio.support.BasicResponseConsumer;
import org.apache.hc.core5.http.ssl.TLS;
import org.apache.hc.core5.http2.HttpVersionPolicy;
import org.apache.hc.core5.http2.config.H2Config;
import org.apache.hc.core5.http2.frame.RawFrame;
import org.apache.hc.core5.http2.impl.nio.H2StreamListener;
import org.apache.hc.core5.http2.impl.nio.bootstrap.H2RequesterBootstrap;
import org.apache.hc.core5.io.CloseMode;
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.hc.core5.util.Timeout;
/***
*
* @param url 请求地址
* @param certificate 证书文件
* @param keyStoreType 证书类型
* @param password 证书密码
* @param json 发送的数据
* @param headers 请求头
* @param callback 回调方法
* @throws KeyStoreException
* @throws IOException
* @throws UnrecoverableKeyException
* @throws NoSuchAlgorithmException
* @throws CertificateException
* @throws KeyManagementException
* @throws URISyntaxException
* @throws ExecutionException
* @throws InterruptedException
*/
public static void AsyncHttpsPostJson(
String url,
File certificate,
String keyStoreType,
String password,
String json,
Map<String, String> headers,
FutureCallback<Message<HttpResponse, String>> callback)
throws KeyStoreException, IOException, UnrecoverableKeyException, NoSuchAlgorithmException,
CertificateException, KeyManagementException, URISyntaxException, ExecutionException,
InterruptedException {
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(new FileInputStream(certificate), password.toCharArray());
SSLContext sslContext =
SSLContexts.custom()
.loadKeyMaterial(keyStore, password.toCharArray())
// .setKeyStoreType("PKCS12")
.build();
H2Config h2Config = H2Config.custom().setPushEnabled(false).setCompressionEnabled(true).build();
TlsStrategy tlsStrategy =
new ClientTlsStrategyBuilder().setTlsVersions(TLS.V_1_2).setSslContext(sslContext).build();
HttpAsyncRequester requester =
H2RequesterBootstrap.bootstrap()
.setH2Config(h2Config)
.setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_2)
.setTlsStrategy(tlsStrategy)
.create();
requester.start();
URI uri = new URI(url);
HttpHost target = new HttpHost("https", uri.getHost(), 443);
Future<AsyncClientEndpoint> future = requester.connect(target, Timeout.ofSeconds(60));
AsyncClientEndpoint clientEndpoint = future.get();
AsyncRequestBuilder asyncRequestBuilder =
AsyncRequestBuilder.post()
.setHttpHost(target)
.setPath(uri.getPath())
.setCharset(Charset.forName("utf-8"));
// 在请求超过1024字节的数据时,curl会先发送个请求头,询问服务器是否接受请求。甭搭理,如果加上这句,苹果服务器是不会响应的,会导致程序卡死
asyncRequestBuilder.addHeader("expect", "");
if (Objects.nonNull(headers)) {
headers.forEach(
(k, v) -> {
asyncRequestBuilder.addHeader(k, v);
});
}
if (StringUtils.isNotBlank(json)) {
asyncRequestBuilder.setEntity(json);
}
AsyncRequestProducer requestProducer = asyncRequestBuilder.build();
if (Objects.isNull(callback)) {
callback = logCallBack(url);
}
clientEndpoint.execute(
requestProducer, new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), callback);
// clientEndpoint.releaseAndReuse();
// requester.initiateShutdown();
// requester.close();
}
关于Expect:100-continue
当要POST的数据大于1024字节的时候, curl并不会直接就发起POST请求, 而是会分为俩步,
- 发送一个请求, 包含一个Expect:100-continue, 询问Server使用愿意接受数据
- 接收到Server返回的100-continue应答以后, 才把数据POST给Server
这段文字具体参照这儿