上一节讲到服务引入的配置和参数订阅,经过参数订阅,消费端获取到了对应接口的分类下的提供者url,配置器url,路由url,那么接下来肯定要更新自己的提供者目录
并创建连接到提供者的客户端了。
除了服务引入启动的时候会进行提供者列表的刷新之外,在注册中心发生注册和注销事件的时候,dubbo也会进行提供者列表的刷新。
public void com.alibaba.dubbo.registry.redis.RedisRegistry.Notifier#run() {
while (running) {
try {
//core次数:10,最大次数:0-10随机加上10,超过最大次数,又从core次数10开始,如此往复
if (!isSkip()) {
try {
//循环没台redis服务连接池
for (Map.Entry<String, JedisPool> entry : jedisPools.entrySet()) {
JedisPool jedisPool = entry.getValue();
try {
jedis = jedisPool.getResource();
try {
//检查服务名是否以*结尾
if (service.endsWith(Constants.ANY_VALUE)) {
//是不是第一次
if (!first) {
//是的,那第一次我要了
first = false;
//获取与当前服务名匹配的key
Set<String> keys = jedis.keys(service);
if (keys != null && !keys.isEmpty()) {
for (String s : keys) {
//通知刷新目录,com.alibaba.dubbo.registry.redis.RedisRegistry#doNotify方法的逻辑在前一节分析过
doNotify(jedis, s);
}
}
//重置连接计数
resetSkip();
}
//订阅(阻塞)
jedis.psubscribe(new NotifySub(jedisPool), service); // blocking
} else {
//是不是第一次
if (!first) {
first = false;
//通知刷新目录,com.alibaba.dubbo.registry.redis.RedisRegistry#doNotify方法的逻辑在前一节分析过
doNotify(jedis, service);
//重置连接计数
resetSkip();
}
//订阅(阻塞)
jedis.psubscribe(new NotifySub(jedisPool), service + Constants.PATH_SEPARATOR + Constants.ANY_VALUE); // blocking
}
break;
} finally {
jedis.close();
}
} catch (Throwable t) { // Retry another server
logger.warn("Failed to subscribe service from redis registry. registry: " + entry.getKey() + ", cause: " + t.getMessage(), t);
// If you only have a single redis, you need to take a rest to avoid overtaking a lot of CPU resources
sleep(reconnectPeriod);
}
}
} catch (Throwable t) {
logger.error(t.getMessage(), t);
sleep(reconnectPeriod);
}
}
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
}
}
订阅通知 NotifySub,我们关注它的onMessage方法
public void NotifySub#onMessage(String key, String msg) {
if (logger.isInfoEnabled()) {
logger.info("redis event: " + key + " = " + msg);
}
//对提供者注册和注销感兴趣
if (msg.equals(Constants.REGISTER)
|| msg.equals(Constants.UNREGISTER)) {
try {
Jedis jedis = jedisPool.getResource();
try {
//通知,com.alibaba.dubbo.registry.redis.RedisRegistry#doNotify方法的逻辑在前一节分析过
doNotify(jedis, key);
} finally {
jedis.close();
}
} catch (Throwable t) { // TODO Notification failure does not restore mechanism guarantee
logger.error(t.getMessage(), t);
}
}
}
好了,我们大致了解一下通知器的工作原理即可,下面我们来看到是如何刷新提供者目录的
public synchronized void com.alibaba.dubbo.registry.integration.RegistryDirectory#notify(List<URL> urls) {
//提供者url
List<URL> invokerUrls = new ArrayList<URL>();
//路由url
List<URL> routerUrls = new ArrayList<URL>();
//配置器url
List<URL> configuratorUrls = new ArrayList<URL>();
for (URL url : urls) {
//获取协议
String protocol = url.getProtocol();
//获取分类,默认分类是providers
String category = url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
//是路由url吗?
if (Constants.ROUTERS_CATEGORY.equals(category)
|| Constants.ROUTE_PROTOCOL.equals(protocol)) {
routerUrls.add(url);
//是配置器url吗
} else if (Constants.CONFIGURATORS_CATEGORY.equals(category)
|| Constants.OVERRIDE_PROTOCOL.equals(protocol)) {
configuratorUrls.add(url);
//是提供者url吗?
} else if (Constants.PROVIDERS_CATEGORY.equals(category)) {
invokerUrls.add(url);
} else {
//提示当前url分类不支持
logger.warn("Unsupported category " + category + " in notified url: " + url + " from registry " + getUrl().getAddress() + " to consumer " + NetUtils.getLocalHost());
}
}
// configurators
//将url作为配置url构建配置器,主要用于修改原始的url,可以添加一些默认参数
if (configuratorUrls != null && !configuratorUrls.isEmpty()) {
this.configurators = toConfigurators(configuratorUrls);
}
// routers
//创建路由规则,通过url中的router参数去构建具体的路由规则类型
//比如ConditionRouterFactory,会根据条件进行匹配提供者
//197.0.0.1 => 192.0.0.2 =>左边为when条件,右边是then,感兴趣的可以自定去阅读对应的源码
if (routerUrls != null && !routerUrls.isEmpty()) {
List<Router> routers = toRouters(routerUrls);
if (routers != null) { // null - do nothing
setRouters(routers);
}
}
//获取刚才解析的配置器
List<Configurator> localConfigurators = this.configurators; // local reference
// merge override parameters
//这个是注册中心地址
this.overrideDirectoryUrl = directoryUrl;
//调用配置器去配置注册中心地址
if (localConfigurators != null && !localConfigurators.isEmpty()) {
for (Configurator configurator : localConfigurators) {
this.overrideDirectoryUrl = configurator.configure(overrideDirectoryUrl);
}
}
// providers
//刷新提供者
refreshInvoker(invokerUrls);
}
上面一段代码主要是对url进行分类,分为提供者url,配置器url,路由url,如果存在配置url,那么会构建对应协议的配置器,用于配置我们注册中心地址,最后开始刷新提供者目录
private void com.alibaba.dubbo.registry.integration.RegistryDirectory#refreshInvoker(List<URL> invokerUrls) {
//如果只有一个提供者,并且这个提供者的协议是empty,也就是没有获取到提供者
if (invokerUrls != null && invokerUrls.size() == 1 && invokerUrls.get(0) != null
&& Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) {
//标记为禁止访问
this.forbidden = true; // Forbid to access
//没有调用者,methodName -> List<invoker>
this.methodInvokerMap = null; // Set the method invoker map to null
//关闭(关闭通道)所有已经连接的invoker,清空urlInvokerMap(url -》 invoker)集合
destroyAllInvokers(); // Close all invokers
} else {
//允许访问
this.forbidden = false; // Allow to access
//记录旧数据
Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap; // local reference
if (invokerUrls.isEmpty() && this.cachedInvokerUrls != null) {
//如果invokerUrls没有url,也就是没有提供者地址,那么从缓存中获取
invokerUrls.addAll(this.cachedInvokerUrls);
} else {
this.cachedInvokerUrls = new HashSet<URL>();
//缓存
this.cachedInvokerUrls.addAll(invokerUrls);//Cached invoker urls, convenient for comparison
}
if (invokerUrls.isEmpty()) {
return;
}
//为每个url构建一个Invoker
//url -> invoker(InvokerDelegate的实例)
Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls);// Translate url list to Invoker map
//转换成方法名 -》 List<Invoker>
Map<String, List<Invoker<T>>> newMethodInvokerMap = toMethodInvokers(newUrlInvokerMap); // Change method name to map Invoker Map
// state change
// If the calculation is wrong, it is not processed.
if (newUrlInvokerMap == null || newUrlInvokerMap.size() == 0) {
logger.error(new IllegalStateException("urls to invokers error .invokerUrls.size :" + invokerUrls.size() + ", invoker.size :0. urls :" + invokerUrls.toString()));
return;
}
//合并多个分组的invoker
this.methodInvokerMap = multiGroup ? toMergeMethodInvokerMap(newMethodInvokerMap) : newMethodInvokerMap;
this.urlInvokerMap = newUrlInvokerMap;
try {
//消费无效的提供者url,循环oldUrlInvokerMap集合,把不存在与newUrlInvokerMap集合中的invoker进行销毁
destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap); // Close the unused Invoker
} catch (Exception e) {
logger.warn("destroyUnusedInvokers error. ", e);
}
}
}
上面一段代码主要做了这么几件事情
- 将提供者url构建成InvokerDelegate,在构建InvokerDelegate时会连接提供者
- 从url参数中获取接口的方法,以方法名为key,invoker为值,构建methodName-》Listmap
- 检查是否是多group的,如果是的话,以group为分组,通过cluster(如果是多个分组的,这个实例最终对象是MergeableCluster,当然应为装饰类的存在,这个cluster实际上是MockClusterWrapper)进行合并
- 消灭无效的提供者
下面我们来看下构建invoker的过程
private Map<String, Invoker<T>> com.alibaba.dubbo.registry.integration.RegistryDirectory#toInvokers(List<URL> urls) {
Map<String, Invoker<T>> newUrlInvokerMap = new HashMap<String, Invoker<T>>();
if (urls == null || urls.isEmpty()) {
return newUrlInvokerMap;
}
Set<String> keys = new HashSet<String>();
//消费者是否指定了协议
String queryProtocols = this.queryMap.get(Constants.PROTOCOL_KEY);
for (URL providerUrl : urls) {
// If protocol is configured at the reference side, only the matching protocol is selected
//筛选服务指定协议的url
if (queryProtocols != null && queryProtocols.length() > 0) {
boolean accept = false;
String[] acceptProtocols = queryProtocols.split(",");
for (String acceptProtocol : acceptProtocols) {
if (providerUrl.getProtocol().equals(acceptProtocol)) {
accept = true;
break;
}
}
if (!accept) {
continue;
}
}
if (Constants.EMPTY_PROTOCOL.equals(providerUrl.getProtocol())) {
continue;
}
//检查对应的协议是否存在对应的协议实现
if (!ExtensionLoader.getExtensionLoader(Protocol.class).hasExtension(providerUrl.getProtocol())) {
logger.error(new IllegalStateException("Unsupported protocol " + providerUrl.getProtocol() + " in notified url: " + providerUrl + " from registry " + getUrl().getAddress() + " to consumer " + NetUtils.getLocalHost()
+ ", supported protocol: " + ExtensionLoader.getExtensionLoader(Protocol.class).getSupportedExtensions()));
continue;
}
//合并参数,将提供者url的一些参数进行合并,当时不会合并提供者的一些线程名个数等,
//对于相同的参数,消费者自己配置的优先级最高
URL url = mergeUrl(providerUrl);
String key = url.toFullString(); // The parameter urls are sorted
//重复的url,跳过
if (keys.contains(key)) { // Repeated url
continue;
}
keys.add(key);
// Cache key is url that does not merge with consumer side parameters, regardless of how the consumer combines parameters, if the server url changes, then refer again
Map<String, Invoker<T>> localUrlInvokerMap = this.urlInvokerMap; // local reference
Invoker<T> invoker = localUrlInvokerMap == null ? null : localUrlInvokerMap.get(key);
if (invoker == null) { // Not in the cache, refer again
try {
boolean enabled = true;
if (url.hasParameter(Constants.DISABLED_KEY)) {
enabled = !url.getParameter(Constants.DISABLED_KEY, false);
} else {
enabled = url.getParameter(Constants.ENABLED_KEY, true);
}
if (enabled) {
//可用的,才进行构建invoker
invoker = new InvokerDelegate<T>(protocol.refer(serviceType, url), url, providerUrl);
}
} catch (Throwable t) {
logger.error("Failed to refer invoker for interface:" + serviceType + ",url:(" + url + ")" + t.getMessage(), t);
}
if (invoker != null) { // Put new invoker in cache
newUrlInvokerMap.put(key, invoker);
}
} else {
newUrlInvokerMap.put(key, invoker);
}
}
keys.clear();
return newUrlInvokerMap;
}
以方法名分组invoker
private Map<String, List<Invoker<T>>> com.alibaba.dubbo.registry.integration.RegistryDirectory#toMethodInvokers(Map<String, Invoker<T>> invokersMap) {
Map<String, List<Invoker<T>>> newMethodInvokerMap = new HashMap<String, List<Invoker<T>>>();
// According to the methods classification declared by the provider URL, the methods is compatible with the registry to execute the filtered methods
List<Invoker<T>> invokersList = new ArrayList<Invoker<T>>();
if (invokersMap != null && invokersMap.size() > 0) {
for (Invoker<T> invoker : invokersMap.values()) {
//从url参数获取接口所有的public方法名,方法名以逗号分割
String parameter = invoker.getUrl().getParameter(Constants.METHODS_KEY);
if (parameter != null && parameter.length() > 0) {
//逗号分割方法
String[] methods = Constants.COMMA_SPLIT_PATTERN.split(parameter);
if (methods != null && methods.length > 0) {
for (String method : methods) {
if (method != null && method.length() > 0
&& !Constants.ANY_VALUE.equals(method)) {
//以方法名进行分组
List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method);
if (methodInvokers == null) {
methodInvokers = new ArrayList<Invoker<T>>();
newMethodInvokerMap.put(method, methodInvokers);
}
methodInvokers.add(invoker);
}
}
}
}
invokersList.add(invoker);
}
}
//根据路由筛选invoker,假设使用的是条件路由是ConditionRouter,配置rule规则,规则值为host=192.0.0.1 => host=192.0.0.2
//它的意思是ip为192.0.0.1的消费者只能使用ip为192.0.0.2的提供者的服务,所以当当前消费者的ip为192.0.0.1时,它只会筛选出ip为192.0.0.2的invoker
List<Invoker<T>> newInvokersList = route(invokersList, null);
//* -》 List<Invoker>
newMethodInvokerMap.put(Constants.ANY_VALUE, newInvokersList);
//服务引用接口的所有public方法名
if (serviceMethods != null && serviceMethods.length > 0) {
for (String method : serviceMethods) {
List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method);
if (methodInvokers == null || methodInvokers.isEmpty()) {
methodInvokers = newInvokersList;
}
//根据方法进一步筛选
newMethodInvokerMap.put(method, route(methodInvokers, method));
}
}
// sort and unmodifiable
for (String method : new HashSet<String>(newMethodInvokerMap.keySet())) {
//进行排序
List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method);
Collections.sort(methodInvokers, InvokerComparator.getComparator());
newMethodInvokerMap.put(method, Collections.unmodifiableList(methodInvokers));
}
return Collections.unmodifiableMap(newMethodInvokerMap);
}
以group分组并进行合并
private Map<String, List<Invoker<T>>> com.alibaba.dubbo.registry.integration.RegistryDirectory#toMergeMethodInvokerMap(Map<String, List<Invoker<T>>> methodMap) {
Map<String, List<Invoker<T>>> result = new HashMap<String, List<Invoker<T>>>();
for (Map.Entry<String, List<Invoker<T>>> entry : methodMap.entrySet()) {
//方法名
String method = entry.getKey();
List<Invoker<T>> invokers = entry.getValue();
//group -> List<Invoker<T>>
Map<String, List<Invoker<T>>> groupMap = new HashMap<String, List<Invoker<T>>>();
for (Invoker<T> invoker : invokers) {
//获取group
String group = invoker.getUrl().getParameter(Constants.GROUP_KEY, "");
//分组
List<Invoker<T>> groupInvokers = groupMap.get(group);
if (groupInvokers == null) {
groupInvokers = new ArrayList<Invoker<T>>();
groupMap.put(group, groupInvokers);
}
groupInvokers.add(invoker);
}
if (groupMap.size() == 1) {
//如果只有一个就无需进行合并了
result.put(method, groupMap.values().iterator().next());
} else if (groupMap.size() > 1) {
//如果超过一个分组,把每组进行合并成一个
List<Invoker<T>> groupInvokers = new ArrayList<Invoker<T>>();
for (List<Invoker<T>> groupList : groupMap.values()) {
groupInvokers.add(cluster.join(new StaticDirectory<T>(groupList)));
}
result.put(method, groupInvokers);
} else {
result.put(method, invokers);
}
}
return result;
}
下面我继续分析客户端连接提供者的逻辑,这段逻辑是在构建InvokerDelegate时进行的
invoker = new InvokerDelegate<T>(protocol.refer(serviceType, url), url, providerUrl);
这个protocol是一个用javassist生成的类,它最终会调用DubboProtocol的refer方法,DubboProtocol被两个包装类进行了装饰,这两个类在我们分析服务的导出的时候分析过
//type:接口Class对象, url:提供者地址
public <T> Invoker<T> com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper#refer(Class<T> type, URL url) throws RpcException {
if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
return protocol.refer(type, url);
}
//构建拦截器链,这次与服务导出不同,这次需要的过滤器的分组是consumers,具体逻辑就不再赘述了
return buildInvokerChain(protocol.refer(type, url), Constants.REFERENCE_FILTER_KEY, Constants.CONSUMER);
}
|
V
public <T> Invoker<T> com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper#refer(Class<T> type, URL url) throws RpcException {
if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
return protocol.refer(type, url);
}
//获取监听器,和服务导出是一样的逻辑,不再赘述
return new ListenerInvokerWrapper<T>(protocol.refer(type, url),
Collections.unmodifiableList(
ExtensionLoader.getExtensionLoader(InvokerListener.class)
.getActivateExtension(url, Constants.INVOKER_LISTENER_KEY)));
}
|
|
V
public <T> Invoker<T> com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol#refer(Class<T> serviceType, URL url) throws RpcException {
//url获取optimizer参数,这个参数可以指定优化的序列化类名
optimizeSerialization(url);
// create rpc invoker.
//创建rpc invoker
DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
invokers.add(invoker);
return invoker;
}
初始化client
private ExchangeClient[] com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol#getClients(URL url) {
// whether to share connection
boolean service_share_connect = false;
//连接数
int connections = url.getParameter(Constants.CONNECTIONS_KEY, 0);
// if not configured, connection is shared, otherwise, one connection for one service
//如果没有设置连接,那么设置共享客户端
if (connections == 0) {
service_share_connect = true;
connections = 1;
}
ExchangeClient[] clients = new ExchangeClient[connections];
for (int i = 0; i < clients.length; i++) {
if (service_share_connect) {
//通常创建的就是共享客户端,内部也调用了initClient方法
clients[i] = getSharedClient(url);
} else {
clients[i] = initClient(url);
}
}
return clients;
}
获取共享客户端
private ExchangeClient com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol#getSharedClient(URL url) {
//获取地址
String key = url.getAddress();
//从缓存中获取
ReferenceCountExchangeClient client = referenceClientMap.get(key);
if (client != null) {
if (!client.isClosed()) {
//计数
client.incrementAndGetCount();
return client;
} else {
//如果连接已经关闭,那么移除
referenceClientMap.remove(key);
}
}
locks.putIfAbsent(key, new Object());
synchronized (locks.get(key)) {
//双重锁定检查
if (referenceClientMap.containsKey(key)) {
return referenceClientMap.get(key);
}
//初始化客户端
ExchangeClient exchangeClient = initClient(url);
//创建引用计算客户端,因为是共享客户端,所以这个类可以记录创建了多少个服务引入
client = new ReferenceCountExchangeClient(exchangeClient, ghostClientMap);
//记录到缓存
referenceClientMap.put(key, client);
ghostClientMap.remove(key);
locks.remove(key);
return client;
}
}
初始化客户端
private ExchangeClient com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol#initClient(URL url) {
// client type setting.
//从url中获取client参数值,确定使用什么样的客户端,可以指定netty或者mima
String str = url.getParameter(Constants.CLIENT_KEY, url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_CLIENT));
//添加编码参数,默认是 codec -> dubbo
url = url.addParameter(Constants.CODEC_KEY, DubboCodec.NAME);
// enable heartbeat by default
//添加心跳周期,默认是60s
url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT));
// BIO is not allowed since it has severe performance issue.
//检查指定的客户端是否存在
if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str)) {
throw new RpcException("Unsupported client type: " + str + "," +
" supported client type is " + StringUtils.join(ExtensionLoader.getExtensionLoader(Transporter.class).getSupportedExtensions(), " "));
}
ExchangeClient client;
try {
// connection should be lazy
//如果需要懒加载,那么创建懒加载客户端
if (url.getParameter(Constants.LAZY_CONNECT_KEY, false)) {
client = new LazyConnectExchangeClient(url, requestHandler);
} else {
//创建连接到提供者的客户端
client = Exchangers.connect(url, requestHandler);
}
} catch (RemotingException e) {
throw new RpcException("Fail to create remoting client for service(" + url + "): " + e.getMessage(), e);
}
return client;
}
dubbo创建了NettyClient客户端,连接提供者的逻辑就在这个类的构造器中
protected void com.alibaba.dubbo.remoting.transport.netty4.NettyClient#doOpen() throws Throwable {
final NettyClientHandler nettyClientHandler = new NettyClientHandler(getUrl(), this);
bootstrap = new Bootstrap();
bootstrap.group(nioEventLoopGroup)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
//.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getTimeout())
.channel(NioSocketChannel.class);
if (getConnectTimeout() < 3000) {
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000);
} else {
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getConnectTimeout());
}
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(Channel ch) throws Exception {
NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyClient.this);
ch.pipeline()//.addLast("logging",new LoggingHandler(LogLevel.INFO))//for debug
.addLast("decoder", adapter.getDecoder())
.addLast("encoder", adapter.getEncoder())
.addLast("handler", nettyClientHandler);
}
});
}
可以看到,其实dubbo创建客户端和创建服务端是差不多的,这里就不过多的说明了。最后总结一下invoker都被哪些类给包装了
MockClusterInvoker
invoker -> FailoverClusterInvoker(如果消费者设置了group,那么这个会变成MergeableClusterInvoker)
directory -> RegistryDirectory(具有动态更新提供者目录的功能)
urlInvokerMap -> 这是一个Map,key为提供者url,值为InvokeDelegate
InvokeDelegate
invoker -> ProtocolFilterWrapper
invoker -> ListenerInvokerWrapper
invoker -> DubboInvoker
clients -> 这是一个数组,它的元素个数取决于配置服务引入时设置了允许多少连接数,其实例为ReferenceCountExchangeClient
ReferenceCountExchangeClient
client -> HeaderExchangeClient
client -> NettyClient
channel -> NioSocketChannel
handler -> MultiMessageHandler
handler -> HeartbeatHandler
handler -> AllChannelHandler
handler -> DecodeHandler
handler -> HeaderExchangeChannel
handler -> DubboProtocol#requestHandler(内部类)
从ChannelHandler包装层次来看,参与消息编码与解码的通道处理器与服务端创建的通道处理是一样的,所以通道的处理逻辑去看请求or响应数据处理的文章就可以了,此处不再过多阐述。
从Invoker的包装层次来看,除了ProtocolFilterWrapper与ListenerInvokerWrapper与服务端一样(代码逻辑差不多,只是监听器和过滤器所在分组不同)之外,其他的完全不一样,所以我们需要研究一下它的不同点,他们的不同点我们到下一节去分析。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2021-01-15 7、mvc配置源码分析