Dubbo Learning Record (19) - Appel de service [5] - Analyse du processus d'appel du consommateur de service ;

Analyse du processus d'appel de service

Code d'appel de service commun

@Component("demoServiceComponent")
public class DemoServiceComponent implements DemoService {
    
    

    @Reference
    private DemoService demoService;   //

    @Override
    public String sayHello(String name) {
    
    
        return demoService.sayHello(name);  // Invoker
    }
}

Ici, l'instance d'objet proxy est finalement introduite via @Reference. Dubbo utilise le framework Javassist pour générer une classe proxy dynamique pour l'interface de service par défaut, nous devons donc d'abord décompiler la classe proxy pour voir le code source, et enfin appeler le méthode sayHello, call handler#invoke, et La classe d'implémentation du gestionnaire est le type InvokerInvocationhandler, vous devez donc regarder la méthode InvokerInvocationHandler#invoke

public class proxy0 implements DC, HelloService, EchoService {
    
    
 
    // 方法数组
    public static Method[] methods;
    private InvocationHandler handler;
 
    public proxy0(InvocationHandler var1) {
    
    
        this.handler = var1;
    }
    public String sayHello(String var1) {
    
    
         // 将参数存储到 Object 数组中
        Object[] var2 = new Object[]{
    
    var1};
 
        // 调用 InvocationHandler 实现类的 invoke 方法得到调用结果
        Object var3 = this.handler.invoke(this, methods[0], var2);
 
        // 返回调用结果
        return (String)var3;
    }
    //....
    }

Processus d'appel du consommateur de service

1. InvokerInvocationHandler#invoke

  • Méthodes d'interception définies dans la classe Object (non remplacées par les sous-classes), telles que wait/notify
  • Pour déterminer si le nom de la méthode est toString, il s'agit d'appeler directement la méthode toString, sans appel à distance
  • Pour juger si le nom de la méthode est hashCode, il s'agit d'appeler directement la méthode hashCode sans appel à distance
  • Pour juger si le nom de la méthode est equals, il faut appeler directement la méthode equals ; aucun appel à distance n'est requis
  • Ni l'un ni l'autre, invocator.invoke(new RpcInvocation(method, args)) ne lance un appel à distance ;
public class InvokerInvocationHandler implements InvocationHandler {
    
    
    private static final Logger logger = LoggerFactory.getLogger(InvokerInvocationHandler.class);
    private final Invoker<?> invoker;

    public InvokerInvocationHandler(Invoker<?> handler) {
    
    
        this.invoker = handler;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    
        String methodName = method.getName();
        Class<?>[] parameterTypes = method.getParameterTypes();
        if (method.getDeclaringClass() == Object.class) {
    
    
            return method.invoke(invoker, args);
        }
        if ("toString".equals(methodName) && parameterTypes.length == 0) {
    
    
            return invoker.toString();
        }
        if ("hashCode".equals(methodName) && parameterTypes.length == 0) {
    
    
            return invoker.hashCode();
        }
        if ("equals".equals(methodName) && parameterTypes.length == 1) {
    
    
            return invoker.equals(args[0]);
        }

        // 这里的recreate方法很重要,他会调用AppResponse的recreate方法,
        // 如果AppResponse对象中存在exception信息,则此方法中会throw这个异常
        return invoker.invoke(new RpcInvocation(method, args)).recreate();
    }
}

RpcInvocation

Les propriétés de cette classe contiennent les paramètres de l'appel de service ;

  • returnType définit le type de valeur de retour
  • methodName : le nom de la méthode à appeler ;
  • parametersTypes : types de paramètres de méthode ;
  • arguments : valeurs des paramètres
  • invocateur : vide, rien n'est passé ;
  • pièces jointes : certaines informations connexes sur la configuration du service, le délai d'attente, etc. ;
public class RpcInvocation implements Invocation, Serializable {
    
    
    public RpcInvocation(Method method, Object[] arguments, Map<String, String> attachment) {
    
    
        this(method.getName(), method.getParameterTypes(), arguments, attachment, null);
        //设置返回值的类型;
        this.returnType = method.getReturnType();
    }
	
	public RpcInvocation(String methodName, Class<?>[] parameterTypes, Object[] arguments, Map<String, String> attachments, Invoker<?> invoker) {
    
    
        this.methodName = methodName;
        this.parameterTypes = parameterTypes == null ? new Class<?>[0] : parameterTypes;
        this.arguments = arguments == null ? new Object[0] : arguments;
        this.attachments = attachments == null ? new HashMap<String, String>() : attachments;
        this.invoker = invoker;
    }

}

Ainsi, lorsque le serveur exécute le service, l'instance RpcInvocation contient les données nécessaires à l'exécution du service ;

2. MockClusterInvoker#invoke

Dans InvokerInvocationHandler#invoke, le type d'attribut de l'invocateur est une instance MockClusterInvoker ; exécute une logique Mock ;

  1. Déterminez si la configuration fictive est vide, si elle est vide, la logique fictive ne sera pas exécutée et la méthode d'appel du prochain Invocateur sera appelée ;
  2. N'est pas vide et commence par force, appelle la méthode doMockInvoke de la logique simulée et termine l'appel distant ;
public class MockClusterInvoker<T> implements Invoker<T> {
    
    
    private final Directory<T> directory;
    private final Invoker<T> invoker;
    @Override
    public Result invoke(Invocation invocation) throws RpcException {
    
    
        Result result = null;

        String value = directory.getUrl().getMethodParameter(invocation.getMethodName(), MOCK_KEY, Boolean.FALSE.toString()).trim();
        if (value.length() == 0 || "false".equalsIgnoreCase(value)) {
    
    
            //no mock
            result = this.invoker.invoke(invocation);
        } else if (value.startsWith("force")) {
    
    
            //force:direct mock  强制调用Mock逻辑, 直接放回数据;
            result = doMockInvoke(invocation, null);
        } else {
    
    
     		//省略部分代码
        }
        return result;
    }
}

3.1 AbstractClusterInvoker#invoke

Vient ensuite l'Invocateur de la fonction de tolérance aux pannes du cluster. Le FailoverClusterInvoker par défaut ne définit pas la méthode d'appel à l'intérieur. Il hérite de la méthode d'appel définie dans la classe AbstractClusterInvoker, il appelle donc la méthode d'appel de la classe parent ; trois choses sont faites
 :

  1. Obtenez les pièces jointes dans RpcContext et définissez-les sur l'objet RpcInvocation ;
  2. Appelez la méthode list et appelez la chaîne de routage pour obtenir les invocateurs d'instance Invoker appropriés à partir du répertoire de service ;
  3. Initialiser et obtenir la politique d'équilibrage de charge loadbalance ;
  4. Appelez la méthode doIoke implémentée par la sous-classe FailoverClusterInvoker
    @Override
    public Result invoke(final Invocation invocation) throws RpcException {
    
    
        checkWhetherDestroyed();

        // binding attachments into invocation.
        Map<String, String> contextAttachments = RpcContext.getContext().getAttachments();
        if (contextAttachments != null && contextAttachments.size() != 0) {
    
    
            ((RpcInvocation) invocation).addAttachments(contextAttachments);
        }

        List<Invoker<T>> invokers = list(invocation);
        LoadBalance loadbalance = initLoadBalance(invokers, invocation);
        RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
        return doInvoke(invocation, invokers, loadbalance);
    }

3.2 FailoverClusterInvoker#doInvoke

  • Selon la stratégie d'équilibrage de charge, un invocateur est sélectionné pour exécution ;
  • Le premier appel normal, l'appel échoue, sera réessayé deux fois, un total de 3 appels ;
    flux de travail :
  1. Get methodName ;
  2. Obtenir le nombre de tentatives, la valeur par défaut est 2 ; l'opération +1 sera effectuée, donc le premier appel doit être compté ;
  3. Appelez la méthode select pour équilibrer la charge et sélectionner un service ;
  4. Appelez la prochaine méthode invocateur#invoke ;
public class FailoverClusterInvoker<T> extends AbstractClusterInvoker<T> {
    
    
    public FailoverClusterInvoker(Directory<T> directory) {
    
    
        super(directory);
    }
    @Override
    @SuppressWarnings({
    
    "unchecked", "rawtypes"})
    public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
    
    
        List<Invoker<T>> copyInvokers = invokers;
        String methodName = RpcUtils.getMethodName(invocation);
        int len = getUrl().getMethodParameter(methodName, RETRIES_KEY, DEFAULT_RETRIES) + 1;
        if (len <= 0) {
    
    
            len = 1;
        }
        // retry loop.
        RpcException le = null; // last exception.
        List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyInvokers.size()); // invoked invokers.
        Set<String> providers = new HashSet<String>(len);
        for (int i = 0; i < len; i++) {
    
    
            if (i > 0) {
    
    
                checkWhetherDestroyed();
                copyInvokers = list(invocation);
                // check again
                checkInvokers(copyInvokers, invocation);
            }
            Invoker<T> invoker = select(loadbalance, invocation, copyInvokers, invoked);
            invoked.add(invoker);
            RpcContext.getContext().setInvokers((List) invoked);
            try {
    
    
                Result result = invoker.invoke(invocation);
				//省略部分代码
                return result;
            } catch (RpcException e) {
    
    
            } catch (Throwable e) {
    
    
                le = new RpcException(e.getMessage(), e);
            } finally {
    
    
                providers.add(invoker.getUrl().getAddress());
            }
        }
		//异常处理
    }

}

Le type du prochain Invoker est la classe RegistryDirectory$InvokerDelegate, appelez la méthode InvokerDelegate#invoke

4 InvocateurDélégué#invoquer

La classe InvokerDelegate ne définit pas la méthode d'invocation, qui hérite de l'InvokerWrapper, qui définit la méthode d'invocation,
l'invocation de l'objet proxy ne fait rien ;

private static class InvokerDelegate<T> extends InvokerWrapper<T> {
    
    
        private URL providerUrl;

        public InvokerDelegate(Invoker<T> invoker, URL url, URL providerUrl) {
    
    
            super(invoker, url);
            this.providerUrl = providerUrl;
        }

        public URL getProviderUrl() {
    
    
            return providerUrl;
        }
}

public class InvokerWrapper<T> implements Invoker<T> {
    
    

    private final Invoker<T> invoker;
    @Override
    public Result invoke(Invocation invocation) throws RpcException {
    
    
        return invoker.invoke(invocation);
    }
}

5 CallbackRegistrationInvoker#invoke

  • Invocateur généré par ProtocolFilterWrapper ;
  • La chaîne de filtrage d'exécution sera appelée, et le résultat sera obtenu après exécution ;
  • Après avoir obtenu le résultat, il obtiendra l'auditeur dans le ListenableFilter et exécutera la méthode onResponse de l'auditeur
    static class CallbackRegistrationInvoker<T> implements Invoker<T> {
    
    

        private final Invoker<T> filterInvoker;
        private final List<Filter> filters;

        @Override
        public Result invoke(Invocation invocation) throws RpcException {
    
    
            // 执行过滤器链
            Result asyncResult = filterInvoker.invoke(invocation);

            // 过滤器都执行完了之后,回调每个过滤器的onResponse或onError方法
            asyncResult = asyncResult.whenCompleteWithContext((r, t) -> {
    
    
                for (int i = filters.size() - 1; i >= 0; i--) {
    
    
                    Filter filter = filters.get(i);
                    // onResponse callback
                    if (filter instanceof ListenableFilter) {
    
    
                        Filter.Listener listener = ((ListenableFilter) filter).listener();
                        if (listener != null) {
    
    
                            if (t == null) {
    
    
                                listener.onResponse(r, filterInvoker, invocation);
                            } else {
    
    
                                listener.onError(t, filterInvoker, invocation);
                            }
                        }
                    } else {
    
    
                        filter.onResponse(r, filterInvoker, invocation);
                    }
                }
            });
            return asyncResult;
        }
}

5 ConsumerContextFilter#invoke

Définir les paramètres de contexte ;

  • adresse locale localAddress ;
  • Adresse d'appel distant remoteAddress
  • nom de l'application distante remoteApplicationName ;
  • Pièces jointes aux paramètres de service ;
@Activate(group = CONSUMER, order = -10000)
public class ConsumerContextFilter extends ListenableFilter {
    
    
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
    
    
        // 设置RpcContext参数
        RpcContext.getContext()
                .setInvoker(invoker)
                .setInvocation(invocation)
                .setLocalAddress(NetUtils.getLocalHost(), 0)
                .setRemoteAddress(invoker.getUrl().getHost(), invoker.getUrl().getPort())
                .setRemoteApplicationName(invoker.getUrl().getParameter(REMOTE_APPLICATION_KEY))
                .setAttachment(REMOTE_APPLICATION_KEY, invoker.getUrl().getParameter(APPLICATION_KEY));
        if (invocation instanceof RpcInvocation) {
    
    
            ((RpcInvocation) invocation).setInvoker(invoker);
        }
        try {
    
    
            RpcContext.removeServerContext();
            return invoker.invoke(invocation);
        } finally {
    
    
            RpcContext.removeContext();
        }
    }

    static class ConsumerContextListener implements Listener {
    
    
        @Override
        public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
    
    
            RpcContext.getServerContext().setAttachments(appResponse.getAttachments());
        }
		//....
    }
}    

6. FutureFilter#invoke

  1. Appelez fireInvokeCallback pour déterminer s'il existe une méthode de rappel et, le cas échéant, déclenchez la méthode de rappel
  2. Appelez le prochain traitement Invoke#invoke ;
@Activate(group = CommonConstants.CONSUMER)
public class FutureFilter extends ListenableFilter {
    
    
    @Override
    public Result invoke(final Invoker<?> invoker, final Invocation invocation) throws RpcException {
    
    
        fireInvokeCallback(invoker, invocation);
        // need to configure if there's return value before the invocation in order to help invoker to judge if it's
        // necessary to return future.
        return invoker.invoke(invocation);
    }
}

7. ListenerInvokerWrapper#invoke

Rien à faire, appelez la prochaine méthode Invoker#invoke ;

public class ListenerInvokerWrapper<T> implements Invoker<T> {
    
    
    private final Invoker<T> invoker;
    private final List<InvokerListener> listeners;
    @Override
    public Result invoke(Invocation invocation) throws RpcException {
    
    
        // AsyncToSyncInvoker
        return invoker.invoke(invocation);
    }

}

8. AsyncToSyncInvoker#invoke

public class AsyncToSyncInvoker<T> implements Invoker<T> {
    
    

    private Invoker<T> invoker;

    public AsyncToSyncInvoker(Invoker<T> invoker) {
    
    
        this.invoker = invoker;
    }

    @Override
    public Class<T> getInterface() {
    
    
        return invoker.getInterface();
    }

    @Override
    public Result invoke(Invocation invocation) throws RpcException {
    
    
        // 异步转同步

        Result asyncResult = invoker.invoke(invocation);  // AsyncRpcResult--->CompletableFuture

        try {
    
    
            // 如果invocation指定是同步的,则阻塞等待结果
            if (InvokeMode.SYNC == ((RpcInvocation) invocation).getInvokeMode()) {
    
    
                asyncResult.get(Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
            }
        } catch (InterruptedException e) {
    
    
  		//..异常处理
        } catch (Throwable e) {
    
    
            throw new RpcException(e.getMessage(), e);
        }
        return asyncResult;
    }
}

9. AbstractInvoker.invoke(invocation)

Appelez principalement la méthode doInvoke de DubboInvoker. S'il y a une exception dans la méthode doInvoker, elle sera empaquetée et empaquetée dans AsyncRpcResult

    @Override
    public Result invoke(Invocation inv) throws RpcException {
    
    
        RpcInvocation invocation = (RpcInvocation) inv;
        invocation.setInvoker(this);

        invocation.setInvokeMode(RpcUtils.getInvokeMode(url, invocation));
        RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);

        try {
    
    
            return doInvoke(invocation);
        } catch (InvocationTargetException e) {
    
     // biz exception
            Throwable te = e.getTargetException();
            if (te == null) {
    
    
                return AsyncRpcResult.newDefaultAsyncResult(null, e, invocation);
            } else {
    
    
                if (te instanceof RpcException) {
    
    
                    ((RpcException) te).setCode(RpcException.BIZ_EXCEPTION);
                }
                return AsyncRpcResult.newDefaultAsyncResult(null, te, invocation);
            }
        } catch (RpcException e) {
    
    
            if (e.isBiz()) {
    
    
                return AsyncRpcResult.newDefaultAsyncResult(null, e, invocation);
            } else {
    
    
                throw e;
            }
        } catch (Throwable e) {
    
    
            return AsyncRpcResult.newDefaultAsyncResult(null, e, invocation);
        }
    }

10. DubboInvoker#doInvoke

  • Interroger un client à partir de clients pour envoyer des données, s'il est configuré pour ne pas se soucier du résultat, appelez la méthode d'envoi de ReferenceCountExchangeClient, sinon appelez la méthode de demande de ReferenceCountExchangeClient ;
  • asyncRpcResult.subscribeTo(responseFuture) : Le responseFuture est lié au résultat RPC asynchrone. Lorsque le responseFuture obtient la valeur de retour, il définit le contenu du résultat renvoyé sur le résultat RPC asynchrone asyncRpcResult
@Override
    protected Result doInvoke(final Invocation invocation) throws Throwable {
    
    
        RpcInvocation inv = (RpcInvocation) invocation;
        final String methodName = RpcUtils.getMethodName(invocation);
        inv.setAttachment(PATH_KEY, getUrl().getPath());
        inv.setAttachment(VERSION_KEY, version);

        // 一个DubboInvoker对象可能并发的同时去调用某个服务
        // 那么单独的一次调用都需要一个单独的client去发送请求
        // 所以这里会去选择使用本次调用该使用哪个client
        ExchangeClient currentClient;
        if (clients.length == 1) {
    
    
            currentClient = clients[0];
        } else {
    
    
            // 轮询使用clients
            currentClient = clients[index.getAndIncrement() % clients.length];
        }

        try {
    
    
            // isOneway为true,表示请求不需要拿结果
            boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
            // 拿当前方法的所配置的超时时间,默认为1000,1秒
            int timeout = getUrl().getMethodPositiveParameter(methodName, TIMEOUT_KEY, DEFAULT_TIMEOUT);
            if (isOneway) {
    
    
                boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
                currentClient.send(inv, isSent);
                // 生成一个默认的值的结果,value=null
                return AsyncRpcResult.newDefaultAsyncResult(invocation);
            } else {
    
    
                AsyncRpcResult asyncRpcResult = new AsyncRpcResult(inv);
                // 异步去请求,得到一个CompletableFuture
                CompletableFuture<Object> responseFuture = currentClient.request(inv, timeout);

                // responseFuture会完成后会调用asyncRpcResult中的方法,这里并不会阻塞,如果要达到阻塞的效果在外层使用asyncRpcResult去控制
                asyncRpcResult.subscribeTo(responseFuture);
                // save for 2.6.x compatibility, for example, TraceFilter in Zipkin uses com.alibaba.xxx.FutureAdapter
                FutureContext.getContext().setCompatibleFuture(responseFuture);
                return asyncRpcResult;
            }
        } catch (TimeoutException e) {
    
    
            throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
        } catch (RemotingException e) {
    
    
            throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }

AsyncRpcResultAsyncRpcResult

Objet de résultat d'appel asynchrone ;

  • Contient l'invocation du paramètre d'appel et l'attribut de contexte ;
  • Héritez de la classe abstraite AbstractResult, qui hérite de la classe CompletableFuture, représentant un objet de classe asynchrone ;
public abstract class AbstractResult extends CompletableFuture<Result> implements Result {
    
    
}

public class AsyncRpcResult extends AbstractResult {
    
    
    private RpcContext storedContext;
    private RpcContext storedServerContext;

    private Invocation invocation;

    public AsyncRpcResult(Invocation invocation) {
    
    
        this.invocation = invocation;
        this.storedContext = RpcContext.getContext();
        this.storedServerContext = RpcContext.getServerContext();
    }
}

11. ReferenceCountExchangeClient#request

Rien à faire, appelez le traitement client de la couche suivante ;

final class ReferenceCountExchangeClient implements ExchangeClient {
    
    
    private final Client client;
    @Override
    public CompletableFuture<Object> request(Object request, int timeout) throws RemotingException {
    
    
        return channel.request(request, timeout);
        }
}

12. En-têteExchangeClient#request

n'a rien fait

public class HeaderExchangeClient implements ExchangeClient {
    
    

    private final Client client;
    private final ExchangeChannel channel;

    @Override
    public CompletableFuture<Object> request(Object request, int timeout) throws RemotingException {
    
    
        return channel.request(request, timeout);
        }
}

13.HeaderExchangeChannel#demande

Créez une instance de demande req, définissez les informations de version de Dubbo, appelez de manière synchrone, demandez une demande de données (instance RpcInvocation)
pour créer une instance DefaultFuture, liez l'ID de demande en interne et placez l'instance DefaultFuture en tant que valeur, l'ID de demande en tant que clé et placez dans le cache FUTURES ; Une fois que HeaderExchangeHandler reçoit le résultat renvoyé, il retire l'instance DefaultFuture de Futures en fonction de l'ID de la demande, puis renvoie Response.

    @Override
    public CompletableFuture<Object> request(Object request, int timeout) throws RemotingException {
    
    
        // create request.
        Request req = new Request();
        req.setVersion(Version.getProtocolVersion());
        req.setTwoWay(true);
        req.setData(request);
        DefaultFuture future = DefaultFuture.newFuture(channel, req, timeout);
        try {
    
    
            channel.send(req);
        } catch (RemotingException e) {
    
    
            future.cancel();
            throw e;
        }
        return future;
    }

14. AbstractPeer.send (message d'objet)

Obtenez le paramètre d'envoi à partir de l'url, la valeur par défaut est false

public abstract class AbstractPeer implements Endpoint, ChannelHandler {
    
    
    private final ChannelHandler handler;
    private volatile URL url;
    @Override
    public void send(Object message) throws RemotingException {
    
    
        send(message, url.getParameter(Constants.SENT_KEY, false));
    }
}

15. AbstractClient#send(Message d'objet, booléen envoyé)

N'avoir rien fait;

16. NettyChannel#envoyer

  • Appelez NioSocketChannel#send pour envoyer des données, appelez writeAndFlush de NioSocketChannel pour envoyer des données
  • Ensuite, jugez si l'envoi est vrai, puis bloquez le délai d'expiration spécifié dans l'url,
  • Parce que si l'envoi est faux, le délai d'attente sera bloqué dans HeaderExchangeChannel
  • Déterminez s'il y a une exception dans le futur et lancez une exception si elle existe ;
final class NettyChannel extends AbstractChannel {
    
    
    private static final ConcurrentMap<Channel, NettyChannel> CHANNEL_MAP = new ConcurrentHashMap<Channel, NettyChannel>();
    private final Channel channel;

    @Override
    public void send(Object message, boolean sent) throws RemotingException {
    
    
        // whether the channel is closed
        super.send(message, sent);
        boolean success = true;
        int timeout = 0;
        try {
    
    
            ChannelFuture future = channel.writeAndFlush(message);
            if (sent) {
    
    
                // wait timeout ms
                timeout = getUrl().getPositiveParameter(TIMEOUT_KEY, DEFAULT_TIMEOUT);
				//阻塞一定时间
                success = future.await(timeout);
            }
            Throwable cause = future.cause();
            if (cause != null) {
    
    
                throw cause;
            }
        } catch (Throwable e) {
    
    
            throw new RemotingException(this, "Failed to send message " + message + " to " + getRemoteAddress() + ", cause: " + e.getMessage(), e);
        }
        if (!success) {
    
    
            throw new RemotingException(this, "Failed to send message " + message + " to " + getRemoteAddress()
                    + "in timeout(" + timeout + "ms) limit");
        }
    }
}

17. NioSocketChannel.writeAndFlush(Object msg)

Le Netty de niveau inférieur envoie des données de manière non bloquante ; il appartient au contenu de netty.

Je suppose que tu aimes

Origine blog.csdn.net/yaoyaochengxian/article/details/124569408
conseillé
Classement