• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2014 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.netty;
18 
19 import static com.google.common.base.Preconditions.checkArgument;
20 import static com.google.common.base.Preconditions.checkState;
21 import static io.grpc.internal.GrpcUtil.DEFAULT_KEEPALIVE_TIMEOUT_NANOS;
22 import static io.grpc.internal.GrpcUtil.DEFAULT_KEEPALIVE_TIME_NANOS;
23 import static io.grpc.internal.GrpcUtil.KEEPALIVE_TIME_NANOS_DISABLED;
24 
25 import com.google.common.annotations.VisibleForTesting;
26 import com.google.common.base.Preconditions;
27 import com.google.errorprone.annotations.CanIgnoreReturnValue;
28 import io.grpc.Attributes;
29 import io.grpc.EquivalentAddressGroup;
30 import io.grpc.ExperimentalApi;
31 import io.grpc.Internal;
32 import io.grpc.NameResolver;
33 import io.grpc.internal.AbstractManagedChannelImplBuilder;
34 import io.grpc.internal.AtomicBackoff;
35 import io.grpc.internal.ClientTransportFactory;
36 import io.grpc.internal.ConnectionClientTransport;
37 import io.grpc.internal.GrpcUtil;
38 import io.grpc.internal.KeepAliveManager;
39 import io.grpc.internal.ProxyParameters;
40 import io.grpc.internal.SharedResourceHolder;
41 import io.grpc.internal.TransportTracer;
42 import io.netty.channel.Channel;
43 import io.netty.channel.ChannelOption;
44 import io.netty.channel.EventLoopGroup;
45 import io.netty.channel.socket.nio.NioSocketChannel;
46 import io.netty.handler.ssl.SslContext;
47 import java.net.InetSocketAddress;
48 import java.net.SocketAddress;
49 import java.util.HashMap;
50 import java.util.Map;
51 import java.util.concurrent.ScheduledExecutorService;
52 import java.util.concurrent.TimeUnit;
53 import javax.annotation.CheckReturnValue;
54 import javax.annotation.Nullable;
55 import javax.net.ssl.SSLException;
56 
57 /**
58  * A builder to help simplify construction of channels using the Netty transport.
59  */
60 @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1784")
61 @CanIgnoreReturnValue
62 public final class NettyChannelBuilder
63     extends AbstractManagedChannelImplBuilder<NettyChannelBuilder> {
64   public static final int DEFAULT_FLOW_CONTROL_WINDOW = 1048576; // 1MiB
65 
66   private static final long AS_LARGE_AS_INFINITE = TimeUnit.DAYS.toNanos(1000L);
67 
68   private final Map<ChannelOption<?>, Object> channelOptions =
69       new HashMap<ChannelOption<?>, Object>();
70 
71   private NegotiationType negotiationType = NegotiationType.TLS;
72   private OverrideAuthorityChecker authorityChecker;
73   private Class<? extends Channel> channelType = NioSocketChannel.class;
74 
75   @Nullable
76   private EventLoopGroup eventLoopGroup;
77   private SslContext sslContext;
78   private int flowControlWindow = DEFAULT_FLOW_CONTROL_WINDOW;
79   private int maxHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
80   private long keepAliveTimeNanos = KEEPALIVE_TIME_NANOS_DISABLED;
81   private long keepAliveTimeoutNanos = DEFAULT_KEEPALIVE_TIMEOUT_NANOS;
82   private boolean keepAliveWithoutCalls;
83   private ProtocolNegotiatorFactory protocolNegotiatorFactory;
84   private LocalSocketPicker localSocketPicker;
85 
86   /**
87    * Creates a new builder with the given server address. This factory method is primarily intended
88    * for using Netty Channel types other than SocketChannel. {@link #forAddress(String, int)} should
89    * generally be preferred over this method, since that API permits delaying DNS lookups and
90    * noticing changes to DNS.
91    */
92   @CheckReturnValue
forAddress(SocketAddress serverAddress)93   public static NettyChannelBuilder forAddress(SocketAddress serverAddress) {
94     return new NettyChannelBuilder(serverAddress);
95   }
96 
97   /**
98    * Creates a new builder with the given host and port.
99    */
100   @CheckReturnValue
forAddress(String host, int port)101   public static NettyChannelBuilder forAddress(String host, int port) {
102     return new NettyChannelBuilder(host, port);
103   }
104 
105   /**
106    * Creates a new builder with the given target string that will be resolved by
107    * {@link io.grpc.NameResolver}.
108    */
109   @CheckReturnValue
forTarget(String target)110   public static NettyChannelBuilder forTarget(String target) {
111     return new NettyChannelBuilder(target);
112   }
113 
114   @CheckReturnValue
NettyChannelBuilder(String host, int port)115   NettyChannelBuilder(String host, int port) {
116     this(GrpcUtil.authorityFromHostAndPort(host, port));
117   }
118 
119   @CheckReturnValue
NettyChannelBuilder(String target)120   NettyChannelBuilder(String target) {
121     super(target);
122   }
123 
124   @CheckReturnValue
NettyChannelBuilder(SocketAddress address)125   NettyChannelBuilder(SocketAddress address) {
126     super(address, getAuthorityFromAddress(address));
127   }
128 
129   @CheckReturnValue
getAuthorityFromAddress(SocketAddress address)130   private static String getAuthorityFromAddress(SocketAddress address) {
131     if (address instanceof InetSocketAddress) {
132       InetSocketAddress inetAddress = (InetSocketAddress) address;
133       return GrpcUtil.authorityFromHostAndPort(inetAddress.getHostString(), inetAddress.getPort());
134     } else {
135       return address.toString();
136     }
137   }
138 
139   /**
140    * Specifies the channel type to use, by default we use {@link NioSocketChannel}.
141    */
channelType(Class<? extends Channel> channelType)142   public NettyChannelBuilder channelType(Class<? extends Channel> channelType) {
143     this.channelType = Preconditions.checkNotNull(channelType, "channelType");
144     return this;
145   }
146 
147   /**
148    * Specifies a channel option. As the underlying channel as well as network implementation may
149    * ignore this value applications should consider it a hint.
150    */
withOption(ChannelOption<T> option, T value)151   public <T> NettyChannelBuilder withOption(ChannelOption<T> option, T value) {
152     channelOptions.put(option, value);
153     return this;
154   }
155 
156   /**
157    * Sets the negotiation type for the HTTP/2 connection.
158    *
159    * <p>Default: <code>TLS</code>
160    */
negotiationType(NegotiationType type)161   public NettyChannelBuilder negotiationType(NegotiationType type) {
162     negotiationType = type;
163     return this;
164   }
165 
166   /**
167    * Provides an EventGroupLoop to be used by the netty transport.
168    *
169    * <p>It's an optional parameter. If the user has not provided an EventGroupLoop when the channel
170    * is built, the builder will use the default one which is static.
171    *
172    * <p>The channel won't take ownership of the given EventLoopGroup. It's caller's responsibility
173    * to shut it down when it's desired.
174    */
eventLoopGroup(@ullable EventLoopGroup eventLoopGroup)175   public NettyChannelBuilder eventLoopGroup(@Nullable EventLoopGroup eventLoopGroup) {
176     this.eventLoopGroup = eventLoopGroup;
177     return this;
178   }
179 
180   /**
181    * SSL/TLS context to use instead of the system default. It must have been configured with {@link
182    * GrpcSslContexts}, but options could have been overridden.
183    */
sslContext(SslContext sslContext)184   public NettyChannelBuilder sslContext(SslContext sslContext) {
185     if (sslContext != null) {
186       checkArgument(sslContext.isClient(),
187           "Server SSL context can not be used for client channel");
188       GrpcSslContexts.ensureAlpnAndH2Enabled(sslContext.applicationProtocolNegotiator());
189     }
190     this.sslContext = sslContext;
191     return this;
192   }
193 
194   /**
195    * Sets the flow control window in bytes. If not called, the default value
196    * is {@link #DEFAULT_FLOW_CONTROL_WINDOW}).
197    */
flowControlWindow(int flowControlWindow)198   public NettyChannelBuilder flowControlWindow(int flowControlWindow) {
199     checkArgument(flowControlWindow > 0, "flowControlWindow must be positive");
200     this.flowControlWindow = flowControlWindow;
201     return this;
202   }
203 
204   /**
205    * Sets the max message size.
206    *
207    * @deprecated Use {@link #maxInboundMessageSize} instead
208    */
209   @Deprecated
maxMessageSize(int maxMessageSize)210   public NettyChannelBuilder maxMessageSize(int maxMessageSize) {
211     maxInboundMessageSize(maxMessageSize);
212     return this;
213   }
214 
215   /**
216    * Sets the maximum size of header list allowed to be received. This is cumulative size of the
217    * headers with some overhead, as defined for
218    * <a href="http://httpwg.org/specs/rfc7540.html#rfc.section.6.5.2">
219    * HTTP/2's SETTINGS_MAX_HEADER_LIST_SIZE</a>. The default is 8 KiB.
220    */
maxHeaderListSize(int maxHeaderListSize)221   public NettyChannelBuilder maxHeaderListSize(int maxHeaderListSize) {
222     checkArgument(maxHeaderListSize > 0, "maxHeaderListSize must be > 0");
223     this.maxHeaderListSize = maxHeaderListSize;
224     return this;
225   }
226 
227   /**
228    * Equivalent to using {@link #negotiationType(NegotiationType)} with {@code PLAINTEXT} or
229    * {@code PLAINTEXT_UPGRADE}.
230    *
231    * @deprecated use {@link #usePlaintext()} instead.
232    */
233   @Override
234   @Deprecated
usePlaintext(boolean skipNegotiation)235   public NettyChannelBuilder usePlaintext(boolean skipNegotiation) {
236     if (skipNegotiation) {
237       negotiationType(NegotiationType.PLAINTEXT);
238     } else {
239       negotiationType(NegotiationType.PLAINTEXT_UPGRADE);
240     }
241     return this;
242   }
243 
244   /**
245    * Equivalent to using {@link #negotiationType(NegotiationType)} with {@code PLAINTEXT}.
246    */
247   @Override
usePlaintext()248   public NettyChannelBuilder usePlaintext() {
249     negotiationType(NegotiationType.PLAINTEXT);
250     return this;
251   }
252 
253   /**
254    * Equivalent to using {@link #negotiationType(NegotiationType)} with {@code TLS}.
255    */
256   @Override
useTransportSecurity()257   public NettyChannelBuilder useTransportSecurity() {
258     negotiationType(NegotiationType.TLS);
259     return this;
260   }
261 
262   /**
263    * Enable keepalive with default delay and timeout.
264    *
265    * @deprecated Please use {@link #keepAliveTime} and {@link #keepAliveTimeout} instead
266    */
267   @Deprecated
enableKeepAlive(boolean enable)268   public final NettyChannelBuilder enableKeepAlive(boolean enable) {
269     if (enable) {
270       return keepAliveTime(DEFAULT_KEEPALIVE_TIME_NANOS, TimeUnit.NANOSECONDS);
271     }
272     return keepAliveTime(KEEPALIVE_TIME_NANOS_DISABLED, TimeUnit.NANOSECONDS);
273   }
274 
275   /**
276    * Enable keepalive with custom delay and timeout.
277    *
278    * @deprecated Please use {@link #keepAliveTime} and {@link #keepAliveTimeout} instead
279    */
280   @Deprecated
enableKeepAlive(boolean enable, long keepAliveTime, TimeUnit delayUnit, long keepAliveTimeout, TimeUnit timeoutUnit)281   public final NettyChannelBuilder enableKeepAlive(boolean enable, long keepAliveTime,
282       TimeUnit delayUnit, long keepAliveTimeout, TimeUnit timeoutUnit) {
283     if (enable) {
284       return keepAliveTime(keepAliveTime, delayUnit)
285           .keepAliveTimeout(keepAliveTimeout, timeoutUnit);
286     }
287     return keepAliveTime(KEEPALIVE_TIME_NANOS_DISABLED, TimeUnit.NANOSECONDS);
288   }
289 
290   /**
291    * {@inheritDoc}
292    *
293    * @since 1.3.0
294    */
295   @Override
keepAliveTime(long keepAliveTime, TimeUnit timeUnit)296   public NettyChannelBuilder keepAliveTime(long keepAliveTime, TimeUnit timeUnit) {
297     checkArgument(keepAliveTime > 0L, "keepalive time must be positive");
298     keepAliveTimeNanos = timeUnit.toNanos(keepAliveTime);
299     keepAliveTimeNanos = KeepAliveManager.clampKeepAliveTimeInNanos(keepAliveTimeNanos);
300     if (keepAliveTimeNanos >= AS_LARGE_AS_INFINITE) {
301       // Bump keepalive time to infinite. This disables keepalive.
302       keepAliveTimeNanos = KEEPALIVE_TIME_NANOS_DISABLED;
303     }
304     return this;
305   }
306 
307   /**
308    * {@inheritDoc}
309    *
310    * @since 1.3.0
311    */
312   @Override
keepAliveTimeout(long keepAliveTimeout, TimeUnit timeUnit)313   public NettyChannelBuilder keepAliveTimeout(long keepAliveTimeout, TimeUnit timeUnit) {
314     checkArgument(keepAliveTimeout > 0L, "keepalive timeout must be positive");
315     keepAliveTimeoutNanos = timeUnit.toNanos(keepAliveTimeout);
316     keepAliveTimeoutNanos = KeepAliveManager.clampKeepAliveTimeoutInNanos(keepAliveTimeoutNanos);
317     return this;
318   }
319 
320   /**
321    * {@inheritDoc}
322    *
323    * @since 1.3.0
324    */
325   @Override
keepAliveWithoutCalls(boolean enable)326   public NettyChannelBuilder keepAliveWithoutCalls(boolean enable) {
327     keepAliveWithoutCalls = enable;
328     return this;
329   }
330 
331 
332   /**
333    * If non-{@code null}, attempts to create connections bound to a local port.
334    */
localSocketPicker(@ullable LocalSocketPicker localSocketPicker)335   public NettyChannelBuilder localSocketPicker(@Nullable LocalSocketPicker localSocketPicker) {
336     this.localSocketPicker = localSocketPicker;
337     return this;
338   }
339 
340   /**
341    * This class is meant to be overriden with a custom implementation of
342    * {@link #createSocketAddress}.  The default implementation is a no-op.
343    *
344    * @since 1.16.0
345    */
346   @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4917")
347   public static class LocalSocketPicker {
348 
349     /**
350      * Called by gRPC to pick local socket to bind to.  This may be called multiple times.
351      * Subclasses are expected to override this method.
352      *
353      * @param remoteAddress the remote address to connect to.
354      * @param attrs the Attributes present on the {@link io.grpc.EquivalentAddressGroup} associated
355      *        with the address.
356      * @return a {@link SocketAddress} suitable for binding, or else {@code null}.
357      * @since 1.16.0
358      */
359     @Nullable
createSocketAddress( SocketAddress remoteAddress, @EquivalentAddressGroup.Attr Attributes attrs)360     public SocketAddress createSocketAddress(
361         SocketAddress remoteAddress, @EquivalentAddressGroup.Attr Attributes attrs) {
362       return null;
363     }
364   }
365 
366   @Override
367   @CheckReturnValue
368   @Internal
buildTransportFactory()369   protected ClientTransportFactory buildTransportFactory() {
370     ProtocolNegotiator negotiator;
371     if (protocolNegotiatorFactory != null) {
372       negotiator = protocolNegotiatorFactory.buildProtocolNegotiator();
373     } else {
374       SslContext localSslContext = sslContext;
375       if (negotiationType == NegotiationType.TLS && localSslContext == null) {
376         try {
377           localSslContext = GrpcSslContexts.forClient().build();
378         } catch (SSLException ex) {
379           throw new RuntimeException(ex);
380         }
381       }
382       negotiator = createProtocolNegotiatorByType(negotiationType, localSslContext);
383     }
384     return new NettyTransportFactory(
385         negotiator, channelType, channelOptions,
386         eventLoopGroup, flowControlWindow, maxInboundMessageSize(),
387         maxHeaderListSize, keepAliveTimeNanos, keepAliveTimeoutNanos, keepAliveWithoutCalls,
388         transportTracerFactory.create(), localSocketPicker);
389   }
390 
391   @Override
392   @CheckReturnValue
getNameResolverParams()393   protected Attributes getNameResolverParams() {
394     int defaultPort;
395     switch (negotiationType) {
396       case PLAINTEXT:
397       case PLAINTEXT_UPGRADE:
398         defaultPort = GrpcUtil.DEFAULT_PORT_PLAINTEXT;
399         break;
400       case TLS:
401         defaultPort = GrpcUtil.DEFAULT_PORT_SSL;
402         break;
403       default:
404         throw new AssertionError(negotiationType + " not handled");
405     }
406     return Attributes.newBuilder()
407         .set(NameResolver.Factory.PARAMS_DEFAULT_PORT, defaultPort).build();
408   }
409 
overrideAuthorityChecker(@ullable OverrideAuthorityChecker authorityChecker)410   void overrideAuthorityChecker(@Nullable OverrideAuthorityChecker authorityChecker) {
411     this.authorityChecker = authorityChecker;
412   }
413 
414   @VisibleForTesting
415   @CheckReturnValue
createProtocolNegotiatorByType( NegotiationType negotiationType, SslContext sslContext)416   static ProtocolNegotiator createProtocolNegotiatorByType(
417       NegotiationType negotiationType,
418       SslContext sslContext) {
419     switch (negotiationType) {
420       case PLAINTEXT:
421         return ProtocolNegotiators.plaintext();
422       case PLAINTEXT_UPGRADE:
423         return ProtocolNegotiators.plaintextUpgrade();
424       case TLS:
425         return ProtocolNegotiators.tls(sslContext);
426       default:
427         throw new IllegalArgumentException("Unsupported negotiationType: " + negotiationType);
428     }
429   }
430 
431   @CheckReturnValue
432   interface OverrideAuthorityChecker {
checkAuthority(String authority)433     String checkAuthority(String authority);
434   }
435 
436   @Override
437   @CheckReturnValue
438   @Internal
checkAuthority(String authority)439   protected String checkAuthority(String authority) {
440     if (authorityChecker != null) {
441       return authorityChecker.checkAuthority(authority);
442     }
443     return super.checkAuthority(authority);
444   }
445 
protocolNegotiatorFactory(ProtocolNegotiatorFactory protocolNegotiatorFactory)446   void protocolNegotiatorFactory(ProtocolNegotiatorFactory protocolNegotiatorFactory) {
447     this.protocolNegotiatorFactory
448         = Preconditions.checkNotNull(protocolNegotiatorFactory, "protocolNegotiatorFactory");
449   }
450 
451   @Override
setTracingEnabled(boolean value)452   protected void setTracingEnabled(boolean value) {
453     super.setTracingEnabled(value);
454   }
455 
456   @Override
setStatsEnabled(boolean value)457   protected void setStatsEnabled(boolean value) {
458     super.setStatsEnabled(value);
459   }
460 
461   @Override
setStatsRecordStartedRpcs(boolean value)462   protected void setStatsRecordStartedRpcs(boolean value) {
463     super.setStatsRecordStartedRpcs(value);
464   }
465 
466   @VisibleForTesting
setTransportTracerFactory(TransportTracer.Factory transportTracerFactory)467   NettyChannelBuilder setTransportTracerFactory(TransportTracer.Factory transportTracerFactory) {
468     this.transportTracerFactory = transportTracerFactory;
469     return this;
470   }
471 
472   interface ProtocolNegotiatorFactory {
473     /**
474      * Returns a ProtocolNegotatior instance configured for this Builder. This method is called
475      * during {@code ManagedChannelBuilder#build()}.
476      */
buildProtocolNegotiator()477     ProtocolNegotiator buildProtocolNegotiator();
478   }
479 
480   /**
481    * Creates Netty transports. Exposed for internal use, as it should be private.
482    */
483   @CheckReturnValue
484   private static final class NettyTransportFactory implements ClientTransportFactory {
485     private final ProtocolNegotiator protocolNegotiator;
486     private final Class<? extends Channel> channelType;
487     private final Map<ChannelOption<?>, ?> channelOptions;
488     private final EventLoopGroup group;
489     private final boolean usingSharedGroup;
490     private final int flowControlWindow;
491     private final int maxMessageSize;
492     private final int maxHeaderListSize;
493     private final AtomicBackoff keepAliveTimeNanos;
494     private final long keepAliveTimeoutNanos;
495     private final boolean keepAliveWithoutCalls;
496     private final TransportTracer transportTracer;
497     private final LocalSocketPicker localSocketPicker;
498 
499     private boolean closed;
500 
NettyTransportFactory(ProtocolNegotiator protocolNegotiator, Class<? extends Channel> channelType, Map<ChannelOption<?>, ?> channelOptions, EventLoopGroup group, int flowControlWindow, int maxMessageSize, int maxHeaderListSize, long keepAliveTimeNanos, long keepAliveTimeoutNanos, boolean keepAliveWithoutCalls, TransportTracer transportTracer, LocalSocketPicker localSocketPicker)501     NettyTransportFactory(ProtocolNegotiator protocolNegotiator,
502         Class<? extends Channel> channelType, Map<ChannelOption<?>, ?> channelOptions,
503         EventLoopGroup group, int flowControlWindow, int maxMessageSize, int maxHeaderListSize,
504         long keepAliveTimeNanos, long keepAliveTimeoutNanos, boolean keepAliveWithoutCalls,
505         TransportTracer transportTracer, LocalSocketPicker localSocketPicker) {
506       this.protocolNegotiator = protocolNegotiator;
507       this.channelType = channelType;
508       this.channelOptions = new HashMap<ChannelOption<?>, Object>(channelOptions);
509       this.flowControlWindow = flowControlWindow;
510       this.maxMessageSize = maxMessageSize;
511       this.maxHeaderListSize = maxHeaderListSize;
512       this.keepAliveTimeNanos = new AtomicBackoff("keepalive time nanos", keepAliveTimeNanos);
513       this.keepAliveTimeoutNanos = keepAliveTimeoutNanos;
514       this.keepAliveWithoutCalls = keepAliveWithoutCalls;
515       this.transportTracer = transportTracer;
516       this.localSocketPicker =
517           localSocketPicker != null ? localSocketPicker : new LocalSocketPicker();
518 
519       usingSharedGroup = group == null;
520       if (usingSharedGroup) {
521         // The group was unspecified, using the shared group.
522         this.group = SharedResourceHolder.get(Utils.DEFAULT_WORKER_EVENT_LOOP_GROUP);
523       } else {
524         this.group = group;
525       }
526     }
527 
528     @Override
newClientTransport( SocketAddress serverAddress, ClientTransportOptions options)529     public ConnectionClientTransport newClientTransport(
530         SocketAddress serverAddress, ClientTransportOptions options) {
531       checkState(!closed, "The transport factory is closed.");
532 
533       ProtocolNegotiator localNegotiator = protocolNegotiator;
534       ProxyParameters proxyParams = options.getProxyParameters();
535       if (proxyParams != null) {
536         localNegotiator = ProtocolNegotiators.httpProxy(
537             proxyParams.proxyAddress, proxyParams.username, proxyParams.password,
538             protocolNegotiator);
539       }
540 
541       final AtomicBackoff.State keepAliveTimeNanosState = keepAliveTimeNanos.getState();
542       Runnable tooManyPingsRunnable = new Runnable() {
543         @Override
544         public void run() {
545           keepAliveTimeNanosState.backoff();
546         }
547       };
548 
549       NettyClientTransport transport = new NettyClientTransport(
550           serverAddress, channelType, channelOptions, group,
551           localNegotiator, flowControlWindow,
552           maxMessageSize, maxHeaderListSize, keepAliveTimeNanosState.get(), keepAliveTimeoutNanos,
553           keepAliveWithoutCalls, options.getAuthority(), options.getUserAgent(),
554           tooManyPingsRunnable, transportTracer, options.getEagAttributes(),
555           localSocketPicker);
556       return transport;
557     }
558 
559     @Override
getScheduledExecutorService()560     public ScheduledExecutorService getScheduledExecutorService() {
561       return group;
562     }
563 
564     @Override
close()565     public void close() {
566       if (closed) {
567         return;
568       }
569       closed = true;
570 
571       protocolNegotiator.close();
572       if (usingSharedGroup) {
573         SharedResourceHolder.release(Utils.DEFAULT_WORKER_EVENT_LOOP_GROUP, group);
574       }
575     }
576   }
577 }
578