1 /* 2 * Copyright 2016 The gRPC Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package io.grpc.cronet; 18 19 import static com.google.common.base.Preconditions.checkArgument; 20 import static com.google.common.base.Preconditions.checkNotNull; 21 import static io.grpc.internal.GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; 22 23 import android.util.Log; 24 import com.google.common.annotations.VisibleForTesting; 25 import com.google.common.base.Preconditions; 26 import com.google.common.util.concurrent.MoreExecutors; 27 import com.google.errorprone.annotations.DoNotCall; 28 import io.grpc.ChannelCredentials; 29 import io.grpc.ChannelLogger; 30 import io.grpc.ExperimentalApi; 31 import io.grpc.Internal; 32 import io.grpc.ManagedChannelBuilder; 33 import io.grpc.internal.AbstractManagedChannelImplBuilder; 34 import io.grpc.internal.ClientTransportFactory; 35 import io.grpc.internal.ConnectionClientTransport; 36 import io.grpc.internal.GrpcUtil; 37 import io.grpc.internal.ManagedChannelImplBuilder; 38 import io.grpc.internal.ManagedChannelImplBuilder.ClientTransportFactoryBuilder; 39 import io.grpc.internal.SharedResourceHolder; 40 import io.grpc.internal.TransportTracer; 41 import java.lang.reflect.InvocationTargetException; 42 import java.lang.reflect.Method; 43 import java.net.InetSocketAddress; 44 import java.net.SocketAddress; 45 import java.util.concurrent.Executor; 46 import java.util.concurrent.ScheduledExecutorService; 47 import javax.annotation.Nullable; 48 import org.chromium.net.BidirectionalStream; 49 import org.chromium.net.CronetEngine; 50 import org.chromium.net.ExperimentalBidirectionalStream; 51 import org.chromium.net.ExperimentalCronetEngine; 52 53 /** Convenience class for building channels with the cronet transport. */ 54 @ExperimentalApi("There is no plan to make this API stable, given transport API instability") 55 public final class CronetChannelBuilder 56 extends AbstractManagedChannelImplBuilder<CronetChannelBuilder> { 57 58 private static final String LOG_TAG = "CronetChannelBuilder"; 59 60 /** BidirectionalStream.Builder factory used for getting the gRPC BidirectionalStream. */ 61 public static abstract class StreamBuilderFactory { newBidirectionalStreamBuilder( String url, BidirectionalStream.Callback callback, Executor executor)62 public abstract BidirectionalStream.Builder newBidirectionalStreamBuilder( 63 String url, BidirectionalStream.Callback callback, Executor executor); 64 } 65 66 /** Creates a new builder for the given server host, port and CronetEngine. */ forAddress(String host, int port, CronetEngine cronetEngine)67 public static CronetChannelBuilder forAddress(String host, int port, CronetEngine cronetEngine) { 68 Preconditions.checkNotNull(cronetEngine, "cronetEngine"); 69 return new CronetChannelBuilder(host, port, cronetEngine); 70 } 71 72 /** 73 * Always fails. Call {@link #forAddress(String, int, CronetEngine)} instead. 74 */ 75 @DoNotCall("Unsupported. Use forAddress(String, int, CronetEngine) instead") forTarget(String target)76 public static CronetChannelBuilder forTarget(String target) { 77 throw new UnsupportedOperationException("call forAddress() instead"); 78 } 79 80 /** 81 * Always fails. Call {@link #forAddress(String, int, CronetEngine)} instead. 82 */ 83 @DoNotCall("Unsupported. Use forAddress(String, int, CronetEngine) instead") forAddress(String name, int port)84 public static CronetChannelBuilder forAddress(String name, int port) { 85 throw new UnsupportedOperationException("call forAddress(String, int, CronetEngine) instead"); 86 } 87 88 @Nullable 89 private ScheduledExecutorService scheduledExecutorService; 90 91 private final CronetEngine cronetEngine; 92 private final ManagedChannelImplBuilder managedChannelImplBuilder; 93 private TransportTracer.Factory transportTracerFactory = TransportTracer.getDefaultFactory(); 94 95 private boolean alwaysUsePut = false; 96 97 private int maxMessageSize = DEFAULT_MAX_MESSAGE_SIZE; 98 99 /** 100 * If true, indicates that the transport may use the GET method for RPCs, and may include the 101 * request body in the query params. 102 */ 103 private final boolean useGetForSafeMethods = false; 104 105 /** 106 * If true, indicates that the transport may use the PUT method for RPCs. 107 */ 108 private final boolean usePutForIdempotentMethods = false; 109 110 private boolean trafficStatsTagSet; 111 private int trafficStatsTag; 112 private boolean trafficStatsUidSet; 113 private int trafficStatsUid; 114 CronetChannelBuilder(String host, int port, CronetEngine cronetEngine)115 private CronetChannelBuilder(String host, int port, CronetEngine cronetEngine) { 116 final class CronetChannelTransportFactoryBuilder implements ClientTransportFactoryBuilder { 117 @Override 118 public ClientTransportFactory buildClientTransportFactory() { 119 return buildTransportFactory(); 120 } 121 } 122 123 managedChannelImplBuilder = new ManagedChannelImplBuilder( 124 InetSocketAddress.createUnresolved(host, port), 125 GrpcUtil.authorityFromHostAndPort(host, port), 126 new CronetChannelTransportFactoryBuilder(), 127 null); 128 this.cronetEngine = Preconditions.checkNotNull(cronetEngine, "cronetEngine"); 129 } 130 131 @Internal 132 @Override delegate()133 protected ManagedChannelBuilder<?> delegate() { 134 return managedChannelImplBuilder; 135 } 136 137 /** 138 * Sets the maximum message size allowed to be received on the channel. If not called, 139 * defaults to {@link io.grpc.internal.GrpcUtil#DEFAULT_MAX_MESSAGE_SIZE}. 140 */ maxMessageSize(int maxMessageSize)141 public final CronetChannelBuilder maxMessageSize(int maxMessageSize) { 142 checkArgument(maxMessageSize >= 0, "maxMessageSize must be >= 0"); 143 this.maxMessageSize = maxMessageSize; 144 return this; 145 } 146 147 /** 148 * Sets the Cronet channel to always use PUT instead of POST. Defaults to false. 149 */ alwaysUsePut(boolean enable)150 public final CronetChannelBuilder alwaysUsePut(boolean enable) { 151 this.alwaysUsePut = enable; 152 return this; 153 } 154 155 /** 156 * Sets {@link android.net.TrafficStats} tag to use when accounting socket traffic caused by this 157 * channel. See {@link android.net.TrafficStats} for more information. If no tag is set (e.g. this 158 * method isn't called), then Android accounts for the socket traffic caused by this channel as if 159 * the tag value were set to 0. 160 * 161 * <p><b>NOTE:</b>Setting a tag disallows sharing of sockets with channels with other tags, which 162 * may adversely effect performance by prohibiting connection sharing. In other words use of 163 * multiplexed sockets (e.g. HTTP/2 and QUIC) will only be allowed if all channels have the same 164 * socket tag. 165 * 166 * @param tag the tag value used to when accounting for socket traffic caused by this channel. 167 * Tags between 0xFFFFFF00 and 0xFFFFFFFF are reserved and used internally by system services 168 * like {@link android.app.DownloadManager} when performing traffic on behalf of an 169 * application. 170 * @return the builder to facilitate chaining. 171 */ setTrafficStatsTag(int tag)172 final CronetChannelBuilder setTrafficStatsTag(int tag) { 173 trafficStatsTagSet = true; 174 trafficStatsTag = tag; 175 return this; 176 } 177 178 /** 179 * Sets specific UID to use when accounting socket traffic caused by this channel. See {@link 180 * android.net.TrafficStats} for more information. Designed for use when performing an operation 181 * on behalf of another application. Caller must hold {@link 182 * android.Manifest.permission#MODIFY_NETWORK_ACCOUNTING} permission. By default traffic is 183 * attributed to UID of caller. 184 * 185 * <p><b>NOTE:</b>Setting a UID disallows sharing of sockets with channels with other UIDs, which 186 * may adversely effect performance by prohibiting connection sharing. In other words use of 187 * multiplexed sockets (e.g. HTTP/2 and QUIC) will only be allowed if all channels have the same 188 * UID set. 189 * 190 * @param uid the UID to attribute socket traffic caused by this channel. 191 * @return the builder to facilitate chaining. 192 */ setTrafficStatsUid(int uid)193 final CronetChannelBuilder setTrafficStatsUid(int uid) { 194 trafficStatsUidSet = true; 195 trafficStatsUid = uid; 196 return this; 197 } 198 199 /** 200 * Provides a custom scheduled executor service. 201 * 202 * <p>It's an optional parameter. If the user has not provided a scheduled executor service when 203 * the channel is built, the builder will use a static cached thread pool. 204 * 205 * @return this 206 * 207 * @since 1.12.0 208 */ scheduledExecutorService( ScheduledExecutorService scheduledExecutorService)209 public final CronetChannelBuilder scheduledExecutorService( 210 ScheduledExecutorService scheduledExecutorService) { 211 this.scheduledExecutorService = 212 checkNotNull(scheduledExecutorService, "scheduledExecutorService"); 213 return this; 214 } 215 buildTransportFactory()216 ClientTransportFactory buildTransportFactory() { 217 return new CronetTransportFactory( 218 new TaggingStreamFactory( 219 cronetEngine, trafficStatsTagSet, trafficStatsTag, trafficStatsUidSet, trafficStatsUid), 220 MoreExecutors.directExecutor(), 221 scheduledExecutorService, 222 maxMessageSize, 223 alwaysUsePut, 224 transportTracerFactory.create(), 225 useGetForSafeMethods, 226 usePutForIdempotentMethods); 227 } 228 229 @VisibleForTesting 230 static class CronetTransportFactory implements ClientTransportFactory { 231 private final ScheduledExecutorService timeoutService; 232 private final Executor executor; 233 private final int maxMessageSize; 234 private final boolean alwaysUsePut; 235 private final StreamBuilderFactory streamFactory; 236 private final TransportTracer transportTracer; 237 private final boolean usingSharedScheduler; 238 private final boolean useGetForSafeMethods; 239 private final boolean usePutForIdempotentMethods; 240 CronetTransportFactory( StreamBuilderFactory streamFactory, Executor executor, @Nullable ScheduledExecutorService timeoutService, int maxMessageSize, boolean alwaysUsePut, TransportTracer transportTracer, boolean useGetForSafeMethods, boolean usePutForIdempotentMethods)241 private CronetTransportFactory( 242 StreamBuilderFactory streamFactory, 243 Executor executor, 244 @Nullable ScheduledExecutorService timeoutService, 245 int maxMessageSize, 246 boolean alwaysUsePut, 247 TransportTracer transportTracer, 248 boolean useGetForSafeMethods, 249 boolean usePutForIdempotentMethods) { 250 usingSharedScheduler = timeoutService == null; 251 this.timeoutService = usingSharedScheduler 252 ? SharedResourceHolder.get(GrpcUtil.TIMER_SERVICE) : timeoutService; 253 this.maxMessageSize = maxMessageSize; 254 this.alwaysUsePut = alwaysUsePut; 255 this.streamFactory = streamFactory; 256 this.executor = Preconditions.checkNotNull(executor, "executor"); 257 this.transportTracer = Preconditions.checkNotNull(transportTracer, "transportTracer"); 258 this.useGetForSafeMethods = useGetForSafeMethods; 259 this.usePutForIdempotentMethods = usePutForIdempotentMethods; 260 } 261 262 @Override newClientTransport( SocketAddress addr, ClientTransportOptions options, ChannelLogger channelLogger)263 public ConnectionClientTransport newClientTransport( 264 SocketAddress addr, ClientTransportOptions options, ChannelLogger channelLogger) { 265 InetSocketAddress inetSocketAddr = (InetSocketAddress) addr; 266 return new CronetClientTransport(streamFactory, inetSocketAddr, options.getAuthority(), 267 options.getUserAgent(), options.getEagAttributes(), executor, maxMessageSize, 268 alwaysUsePut, transportTracer, useGetForSafeMethods, usePutForIdempotentMethods); 269 } 270 271 @Override getScheduledExecutorService()272 public ScheduledExecutorService getScheduledExecutorService() { 273 return timeoutService; 274 } 275 276 @Override swapChannelCredentials(ChannelCredentials channelCreds)277 public SwapChannelCredentialsResult swapChannelCredentials(ChannelCredentials channelCreds) { 278 return null; 279 } 280 281 @Override close()282 public void close() { 283 if (usingSharedScheduler) { 284 SharedResourceHolder.release(GrpcUtil.TIMER_SERVICE, timeoutService); 285 } 286 } 287 } 288 289 /** 290 * StreamBuilderFactory impl that applies TrafficStats tags to stream builders that are produced. 291 */ 292 private static class TaggingStreamFactory extends StreamBuilderFactory { 293 private static volatile boolean loadSetTrafficStatsTagAttempted; 294 private static volatile boolean loadSetTrafficStatsUidAttempted; 295 private static volatile Method setTrafficStatsTagMethod; 296 private static volatile Method setTrafficStatsUidMethod; 297 298 private final CronetEngine cronetEngine; 299 private final boolean trafficStatsTagSet; 300 private final int trafficStatsTag; 301 private final boolean trafficStatsUidSet; 302 private final int trafficStatsUid; 303 TaggingStreamFactory( CronetEngine cronetEngine, boolean trafficStatsTagSet, int trafficStatsTag, boolean trafficStatsUidSet, int trafficStatsUid)304 TaggingStreamFactory( 305 CronetEngine cronetEngine, 306 boolean trafficStatsTagSet, 307 int trafficStatsTag, 308 boolean trafficStatsUidSet, 309 int trafficStatsUid) { 310 this.cronetEngine = cronetEngine; 311 this.trafficStatsTagSet = trafficStatsTagSet; 312 this.trafficStatsTag = trafficStatsTag; 313 this.trafficStatsUidSet = trafficStatsUidSet; 314 this.trafficStatsUid = trafficStatsUid; 315 } 316 317 @Override newBidirectionalStreamBuilder( String url, BidirectionalStream.Callback callback, Executor executor)318 public BidirectionalStream.Builder newBidirectionalStreamBuilder( 319 String url, BidirectionalStream.Callback callback, Executor executor) { 320 ExperimentalBidirectionalStream.Builder builder = 321 ((ExperimentalCronetEngine) cronetEngine) 322 .newBidirectionalStreamBuilder(url, callback, executor); 323 if (trafficStatsTagSet) { 324 setTrafficStatsTag(builder, trafficStatsTag); 325 } 326 if (trafficStatsUidSet) { 327 setTrafficStatsUid(builder, trafficStatsUid); 328 } 329 return builder; 330 } 331 setTrafficStatsTag(ExperimentalBidirectionalStream.Builder builder, int tag)332 private static void setTrafficStatsTag(ExperimentalBidirectionalStream.Builder builder, 333 int tag) { 334 if (!loadSetTrafficStatsTagAttempted) { 335 synchronized (TaggingStreamFactory.class) { 336 if (!loadSetTrafficStatsTagAttempted) { 337 try { 338 setTrafficStatsTagMethod = ExperimentalBidirectionalStream.Builder.class 339 .getMethod("setTrafficStatsTag", int.class); 340 } catch (NoSuchMethodException e) { 341 Log.w(LOG_TAG, 342 "Failed to load method ExperimentalBidirectionalStream.Builder.setTrafficStatsTag", 343 e); 344 } finally { 345 loadSetTrafficStatsTagAttempted = true; 346 } 347 } 348 } 349 } 350 if (setTrafficStatsTagMethod != null) { 351 try { 352 setTrafficStatsTagMethod.invoke(builder, tag); 353 } catch (InvocationTargetException e) { 354 throw new RuntimeException(e.getCause() == null ? e.getTargetException() : e.getCause()); 355 } catch (IllegalAccessException e) { 356 Log.w(LOG_TAG, "Failed to set traffic stats tag: " + tag, e); 357 } 358 } 359 } 360 setTrafficStatsUid(ExperimentalBidirectionalStream.Builder builder, int uid)361 private static void setTrafficStatsUid(ExperimentalBidirectionalStream.Builder builder, 362 int uid) { 363 if (!loadSetTrafficStatsUidAttempted) { 364 synchronized (TaggingStreamFactory.class) { 365 if (!loadSetTrafficStatsUidAttempted) { 366 try { 367 setTrafficStatsUidMethod = ExperimentalBidirectionalStream.Builder.class 368 .getMethod("setTrafficStatsUid", int.class); 369 } catch (NoSuchMethodException e) { 370 Log.w(LOG_TAG, 371 "Failed to load method ExperimentalBidirectionalStream.Builder.setTrafficStatsUid", 372 e); 373 } finally { 374 loadSetTrafficStatsUidAttempted = true; 375 } 376 } 377 } 378 } 379 if (setTrafficStatsUidMethod != null) { 380 try { 381 setTrafficStatsUidMethod.invoke(builder, uid); 382 } catch (InvocationTargetException e) { 383 throw new RuntimeException(e.getCause() == null ? e.getTargetException() : e.getCause()); 384 } catch (IllegalAccessException e) { 385 Log.w(LOG_TAG, "Failed to set traffic stats uid: " + uid, e); 386 } 387 } 388 } 389 } 390 } 391