All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cn.ipokerface.aps.handler.ApnsClientChannelHandler Maven / Gradle / Ivy

The newest version!
package cn.ipokerface.aps.handler;

import cn.ipokerface.aps.Constant;
import cn.ipokerface.aps.notification.Notification;
import cn.ipokerface.aps.notification.NotificationFuture;
import cn.ipokerface.aps.response.NotificationError;
import cn.ipokerface.aps.response.NotificationResponse;
import cn.ipokerface.aps.response.ResponseCode;
import com.alibaba.fastjson.JSON;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpScheme;
import io.netty.handler.codec.http2.*;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.collection.IntObjectHashMap;
import io.netty.util.concurrent.GenericFutureListener;
import io.netty.util.concurrent.Promise;
import io.netty.util.concurrent.PromiseCombiner;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

/**
 * Created by       PokerFace
 * Create Date      2020-08-24.
 * Email:           [email protected]
 * Version          1.0.0
 * 

* Description: */ public class ApnsClientChannelHandler extends Http2ConnectionHandler implements Http2FrameListener, Http2Connection.Listener { private static final Logger logger = LoggerFactory.getLogger(ApnsClientChannelHandler.class); private static final IOException STREAMS_EXHAUSTED_EXCEPTION = new IOException("HTTP/2 streams exhausted; closing connection."); private static final IOException STREAM_CLOSED_BEFORE_REPLY_EXCEPTION = new IOException("Stream closed before a reply was received"); private Throwable connectionErrorCause; private final Map> promiseStreamId = new IntObjectHashMap<>(); private String authority; private Duration idleInterval; private ScheduledFuture idleFuture; private String defaultTopic; private Http2Connection.PropertyKey responseHeadersPropertyKey; private Http2Connection.PropertyKey responsePromisePropertyKey; private Http2Connection.PropertyKey streamErrorCausePropertyKey; public ApnsClientChannelHandler(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, Http2Settings initialSettings, String authority, String defaultTopic, Duration idleInterval) { super(decoder, encoder, initialSettings); this.authority = authority; if (idleInterval == null) { this.idleInterval = Duration.ofSeconds(30); } else{ this.idleInterval = idleInterval.dividedBy(2); } this.defaultTopic = defaultTopic; this.connection().addListener(this); this.responseHeadersPropertyKey = this.connection().newKey(); this.responsePromisePropertyKey = this.connection().newKey(); this.streamErrorCausePropertyKey = this.connection().newKey(); } @Override public void channelInactive(final ChannelHandlerContext context) throws Exception { super.channelInactive(context); for (final NotificationFuture future : this.promiseStreamId.values()) { future.completeExceptionally(STREAM_CLOSED_BEFORE_REPLY_EXCEPTION); } this.promiseStreamId.clear(); } @Override public void userEventTriggered(final ChannelHandlerContext context, final Object event) throws Exception { if (event instanceof IdleStateEvent) { logger.trace("Sending ping due to inactivity."); this.encoder().writePing(context, false, System.currentTimeMillis(), context.newPromise()).addListener( (GenericFutureListener) future -> { if (!future.isSuccess()) { logger.debug("Failed to write PING frame.", future.cause()); future.channel().close(); } }); this.idleFuture = context.channel().eventLoop().schedule(() -> { logger.debug("Closing channel due to ping timeout."); context.channel().close(); }, idleInterval.toMillis(), TimeUnit.MILLISECONDS); this.flush(context); } super.userEventTriggered(context, event); } @Override public void exceptionCaught(final ChannelHandlerContext context, final Throwable cause) { // Always try to fail the "channel ready" promise if we catch an exception; in some cases, these may happen // after a connection has already become ready, in which case the failure attempt will have no effect. getChannelReadyPromise(context.channel()).tryFailure(cause); } @Override public void write(ChannelHandlerContext context, Object message, ChannelPromise promise) throws Exception { // super.write(ctx, message, promise); if (message instanceof NotificationFuture) { final NotificationFuture pushNotificationFuture = (NotificationFuture) message; promise.addListener(future -> { if (!future.isSuccess()) { logger.trace("Failed to write push notification.", future.cause()); pushNotificationFuture.completeExceptionally(future.cause()); } }); this.writeNotification(context, pushNotificationFuture, promise); } else { // This should never happen, but in case some foreign debris winds up in the pipeline, just pass it through. logger.error("Unexpected object in pipeline: {}", message); context.write(message, promise); } } /** * * @param context * @param streamId */ private void retryNotificationFromStream(final ChannelHandlerContext context, final int streamId) { final Http2Stream stream = this.connection().stream(streamId); final NotificationFuture responseFuture = stream.removeProperty(this.responsePromisePropertyKey); final ChannelPromise writePromise = context.channel().newPromise(); this.writeNotification(context, responseFuture, writePromise); } /** * * @param context * @param responsePromise * @param writePromise */ private void writeNotification(final ChannelHandlerContext context, final NotificationFuture responsePromise, final ChannelPromise writePromise) { if (context.channel().isActive()) { final int streamId = this.connection().local().incrementAndGetNextStreamId(); if (streamId > 0) { // We'll attach the push notification and response promise to the stream as soon as the stream is created. // Because we're using a StreamBufferingEncoder under the hood, there's no guarantee as to when the stream // will actually be created, and so we attach these in the onStreamAdded listener to make sure everything // is happening in a predictable order. this.promiseStreamId.put(streamId, responsePromise); final Notification notification = responsePromise.getNotification(); final Http2Headers headers = buildHeader(notification, context, streamId); final ChannelPromise headersPromise = context.newPromise(); this.encoder().writeHeaders(context, streamId, headers, 0, false, headersPromise); logger.trace("Wrote headers on stream {}: {}", streamId, headers); String payload = notification.payloadJson(); final ByteBuf payloadBuffer = Unpooled.wrappedBuffer(payload.getBytes(StandardCharsets.UTF_8)); final ChannelPromise dataPromise = context.newPromise(); this.encoder().writeData(context, streamId, payloadBuffer, 0, true, dataPromise); logger.trace("Wrote payload on stream {}: {}", streamId, notification.payloadJson()); final PromiseCombiner promiseCombiner = new PromiseCombiner(context.executor()); promiseCombiner.addAll((ChannelFuture) headersPromise, dataPromise); promiseCombiner.finish(writePromise); } else { // This is very unlikely, but in the event that we run out of stream IDs, we need to open a new // connection. Just closing the context should be enough; automatic reconnection should take things // from there. writePromise.tryFailure(STREAMS_EXHAUSTED_EXCEPTION); context.channel().close(); } } else { writePromise.tryFailure(STREAM_CLOSED_BEFORE_REPLY_EXCEPTION); } } protected Http2Headers buildHeader(Notification notification,ChannelHandlerContext context,int streamId) { final Http2Headers headers = new DefaultHttp2Headers() .method(HttpMethod.POST.asciiName()) .authority(this.authority) .path(Constant.header_path_format.concat(notification.getDeviceToken())) .scheme(HttpScheme.HTTPS.name()) .addInt(Constant.header_apns_expiration, (int)notification.getExpiration()); if (notification.getApnsCollapseId() != null) { headers.add(Constant.header_apns_collapse_id, notification.getApnsCollapseId()); } if (notification.getPriority() != null) { headers.addInt(Constant.header_apns_priority, notification.getPriority().getCode()); } if (notification.getType() != null) { headers.add(Constant.header_apns_push_type , notification.getType().name()); } if (!StringUtils.isEmpty(notification.getTopic())) { headers.add(Constant.header_apns_topic , notification.getTopic()); } else{ headers.add(Constant.header_apns_topic , this.defaultTopic); } headers.add(Constant.header_apns_id, notification.getApnsId()); return headers; } private void handleEndOfStream(final ChannelHandlerContext context, final Http2Stream stream, final Http2Headers headers, final ByteBuf data) { final NotificationFuture> responseFuture = stream.getProperty(this.responsePromisePropertyKey); final Notification notification = responseFuture.getNotification(); final HttpResponseStatus status = HttpResponseStatus.parseLine(headers.status()); String apnsId = headers.get(Constant.header_apns_id).toString(); ResponseCode code = ResponseCode.of(status.code()); if (HttpResponseStatus.OK.equals(status)) { responseFuture.complete(new NotificationResponse(code, apnsId , notification, null)); } else { if (data != null) { NotificationError errorResponse = JSON.parseObject(data.toString(StandardCharsets.UTF_8), NotificationError.class); responseFuture.complete(new NotificationResponse(code, apnsId , notification, errorResponse)); } else { logger.warn("Gateway sent an end-of-stream HEADERS frame for an unsuccessful notification."); } } } private Promise getChannelReadyPromise(final Channel channel) { return channel.attr(Constant.channel_ready_promise_attribute_key).get(); } // -------------------------- connection listener --------------------------------- @Override public void onStreamAdded(Http2Stream stream) { stream.setProperty(responsePromisePropertyKey, promiseStreamId.remove(stream.id())); } @Override public void onStreamActive(Http2Stream stream) { } @Override public void onStreamHalfClosed(Http2Stream stream) { } @Override public void onStreamClosed(Http2Stream stream) { // Always try to fail promises associated with closed streams; most of the time, this should fail silently, but // in cases of unexpected closure, it will make sure that nothing gets left hanging. final CompletableFuture> responsePromise = stream.getProperty(this.responsePromisePropertyKey); if (responsePromise != null) { final Throwable cause; if (stream.getProperty(this.streamErrorCausePropertyKey) != null) { cause = stream.getProperty(this.streamErrorCausePropertyKey); }else if (this.connectionErrorCause != null) { cause = this.connectionErrorCause; } else { cause = STREAM_CLOSED_BEFORE_REPLY_EXCEPTION; } responsePromise.completeExceptionally(cause); } } @Override public void onStreamRemoved(Http2Stream stream) { stream.removeProperty(this.responseHeadersPropertyKey); stream.removeProperty(this.responsePromisePropertyKey); } @Override public void onGoAwaySent(int lastStreamId, long errorCode, ByteBuf debugData) { } @Override public void onGoAwayReceived(int lastStreamId, long errorCode, ByteBuf debugData) { } @Override protected void onStreamError(ChannelHandlerContext ctx, boolean outbound, Throwable cause, Http2Exception.StreamException http2Ex) { final Http2Stream stream = this.connection().stream(http2Ex.streamId()); // The affected stream may already be closed (or was never open in the first place) if (stream != null) { stream.setProperty(this.streamErrorCausePropertyKey, http2Ex); } super.onStreamError(ctx, outbound, cause, http2Ex); } @Override protected void onConnectionError(ChannelHandlerContext ctx, boolean outbound, Throwable cause, Http2Exception http2Ex) { this.connectionErrorCause = http2Ex != null ? http2Ex : cause; super.onConnectionError(ctx, outbound, cause, http2Ex); } // -------------------------- connection listener --------------------------------- // -------------------------- frame listener --------------------------------- @Override public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) throws Http2Exception { final int bytesProcessed = data.readableBytes() + padding; if (endOfStream) { final Http2Stream stream = this.connection().stream(streamId); this.handleEndOfStream(ctx, stream, stream.getProperty(this.responseHeadersPropertyKey), data); } else { logger.error("Gateway sent a DATA frame that was not the end of a stream."); } return bytesProcessed; } @Override public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding, boolean endOfStream) throws Http2Exception { final Http2Stream stream = this.connection().stream(streamId); if (endOfStream) { this.handleEndOfStream(ctx, stream, headers, null); } else { stream.setProperty(this.responseHeadersPropertyKey, headers); } } @Override public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endOfStream) throws Http2Exception { this.onHeadersRead(ctx, streamId, headers, padding, endOfStream); } @Override public void onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency, short weight, boolean exclusive) throws Http2Exception { } @Override public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception { if (errorCode == Http2Error.REFUSED_STREAM.code()) { // This can happen if the server reduces MAX_CONCURRENT_STREAMS while we already have notifications in // flight. We may get multiple RST_STREAM frames per stream since we send multiple frames (HEADERS and // DATA) for each push notification, but we should only get one REFUSED_STREAM error; the rest should all be // STREAM_CLOSED. retryNotificationFromStream(ctx, streamId); } } @Override public void onSettingsAckRead(ChannelHandlerContext ctx) throws Http2Exception { } @Override public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) throws Http2Exception { logger.debug("Received settings from APNs gateway: {}", settings); // Always try to mark the "channel ready" promise as a success after we receive a SETTINGS frame. If it's the // first SETTINGS frame, we know all handshaking and connection setup is done and the channel is ready to use. // If it's a subsequent SETTINGS frame, this will have no effect. getChannelReadyPromise(ctx.channel()).trySuccess(ctx.channel()); } @Override public void onPingRead(ChannelHandlerContext ctx, long data) throws Http2Exception { } @Override public void onPingAckRead(ChannelHandlerContext ctx, long data) throws Http2Exception { if (this.idleFuture != null) { this.idleFuture.cancel(false); } else { logger.error("Received PING ACK, but no corresponding outbound PING found."); } } @Override public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId, Http2Headers headers, int padding) throws Http2Exception { } @Override public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData) throws Http2Exception { logger.info("Received GOAWAY from APNs server: {}", debugData.toString(StandardCharsets.UTF_8)); } @Override public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement) throws Http2Exception { } @Override public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags, ByteBuf payload) throws Http2Exception { } // -------------------------- frame listener --------------------------------- public static class ApnsClientChannelHandlerBuilder extends AbstractHttp2ConnectionHandlerBuilder { protected String authority; protected Duration idleInterval; protected String defaultTopic; public ApnsClientChannelHandlerBuilder authority(final String authority) { this.authority = authority; return this; } public ApnsClientChannelHandlerBuilder defaultTopic(String topic) { this.defaultTopic = topic; return this; } public ApnsClientChannelHandlerBuilder idleInterval(final Duration idleIntervalMillis) { this.idleInterval = idleIntervalMillis; return this; } @Override public ApnsClientChannelHandlerBuilder frameLogger(final Http2FrameLogger frameLogger) { return super.frameLogger(frameLogger); } @Override public Http2FrameLogger frameLogger() { return super.frameLogger(); } @Override protected final boolean isServer() { return false; } @Override protected boolean encoderEnforceMaxConcurrentStreams() { return true; } @Override public ApnsClientChannelHandler build(final Http2ConnectionDecoder decoder, final Http2ConnectionEncoder encoder, final Http2Settings initialSettings) { Objects.requireNonNull(this.authority, "Authority must be set before building an ApnsClientHandler."); final ApnsClientChannelHandler handler = new ApnsClientChannelHandler(decoder, encoder, initialSettings, this.authority,this.defaultTopic, this.idleInterval); this.frameListener(handler); return handler; } @Override public ApnsClientChannelHandler build() { return super.build(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy