RocketMQ中PullConsumer的啟動源碼分析

来源:https://www.cnblogs.com/a526583280/archive/2019/08/13/11343410.html
-Advertisement-
Play Games

通過DefaultMQPullConsumer作為預設實現,這裡的啟動過程和Producer很相似,但相比複雜一些 【RocketMQ中Producer的啟動源碼分析】 DefaultMQPullConsumer的構造方法: 這裡會封裝一個DefaultMQPullConsumerImpl,類似於P ...


通過DefaultMQPullConsumer作為預設實現,這裡的啟動過程和Producer很相似,但相比複雜一些

【RocketMQ中Producer的啟動源碼分析】

 

DefaultMQPullConsumer的構造方法:

1 public DefaultMQPullConsumer(final String consumerGroup, RPCHook rpcHook) {
2     this.consumerGroup = consumerGroup;
3     defaultMQPullConsumerImpl = new DefaultMQPullConsumerImpl(this, rpcHook);
4 }

這裡會封裝一個DefaultMQPullConsumerImpl,類似於Producer中DefaultMQProducerImpl


DefaultMQPullConsumerImpl:

 1 public class DefaultMQPullConsumerImpl implements MQConsumerInner {
 2     private final InternalLogger log = ClientLogger.getLog();
 3     private final DefaultMQPullConsumer defaultMQPullConsumer;
 4     private final long consumerStartTimestamp = System.currentTimeMillis();
 5     private final RPCHook rpcHook;
 6     private final ArrayList<ConsumeMessageHook> consumeMessageHookList = new ArrayList<ConsumeMessageHook>();
 7     private final ArrayList<FilterMessageHook> filterMessageHookList = new ArrayList<FilterMessageHook>();
 8     private volatile ServiceState serviceState = ServiceState.CREATE_JUST;
 9     private MQClientInstance mQClientFactory;
10     private PullAPIWrapper pullAPIWrapper;
11     private OffsetStore offsetStore;
12     private RebalanceImpl rebalanceImpl = new RebalancePullImpl(this);
13 
14     public DefaultMQPullConsumerImpl(final DefaultMQPullConsumer defaultMQPullConsumer, final RPCHook rpcHook) {
15         this.defaultMQPullConsumer = defaultMQPullConsumer;
16         this.rpcHook = rpcHook;
17     }
18     ......
19 }

如上會封裝這些東西,在後面遇到了再詳細介紹

 

而DefaultMQPullConsumer的start方法,其實際上調用的是DefaultMQPullConsumerImpl的start方法

DefaultMQPullConsumerImpl的start方法:

 1 public synchronized void start() throws MQClientException {
 2     switch (this.serviceState) {
 3         case CREATE_JUST:
 4             this.serviceState = ServiceState.START_FAILED;
 5 
 6             this.checkConfig();
 7 
 8             this.copySubscription();
 9 
10             if (this.defaultMQPullConsumer.getMessageModel() == MessageModel.CLUSTERING) {
11                 this.defaultMQPullConsumer.changeInstanceNameToPID();
12             }
13 
14             this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQPullConsumer, this.rpcHook);
15 
16             this.rebalanceImpl.setConsumerGroup(this.defaultMQPullConsumer.getConsumerGroup());
17             this.rebalanceImpl.setMessageModel(this.defaultMQPullConsumer.getMessageModel());
18             this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPullConsumer.getAllocateMessageQueueStrategy());
19             this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);
20 
21             this.pullAPIWrapper = new PullAPIWrapper(
22                 mQClientFactory,
23                 this.defaultMQPullConsumer.getConsumerGroup(), isUnitMode());
24             this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);
25 
26             if (this.defaultMQPullConsumer.getOffsetStore() != null) {
27                 this.offsetStore = this.defaultMQPullConsumer.getOffsetStore();
28             } else {
29                 switch (this.defaultMQPullConsumer.getMessageModel()) {
30                     case BROADCASTING:
31                         this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPullConsumer.getConsumerGroup());
32                         break;
33                     case CLUSTERING:
34                         this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPullConsumer.getConsumerGroup());
35                         break;
36                     default:
37                         break;
38                 }
39                 this.defaultMQPullConsumer.setOffsetStore(this.offsetStore);
40             }
41 
42             this.offsetStore.load();
43 
44             boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPullConsumer.getConsumerGroup(), this);
45             if (!registerOK) {
46                 this.serviceState = ServiceState.CREATE_JUST;
47 
48                 throw new MQClientException("The consumer group[" + this.defaultMQPullConsumer.getConsumerGroup()
49                     + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
50                     null);
51             }
52 
53             mQClientFactory.start();
54             log.info("the consumer [{}] start OK", this.defaultMQPullConsumer.getConsumerGroup());
55             this.serviceState = ServiceState.RUNNING;
56             break;
57         case RUNNING:
58         case START_FAILED:
59         case SHUTDOWN_ALREADY:
60             throw new MQClientException("The PullConsumer service state not OK, maybe started once, "
61                 + this.serviceState
62                 + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
63                 null);
64         default:
65             break;
66     }
67 
68 }

首先checkConfig方法會對配置做檢查


接著copySubscription方法:

 1 private void copySubscription() throws MQClientException {
 2     try {
 3         Set<String> registerTopics = this.defaultMQPullConsumer.getRegisterTopics();
 4         if (registerTopics != null) {
 5             for (final String topic : registerTopics) {
 6                 SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(this.defaultMQPullConsumer.getConsumerGroup(),
 7                     topic, SubscriptionData.SUB_ALL);
 8                 this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData);
 9             }
10         }
11     } catch (Exception e) {
12         throw new MQClientException("subscription exception", e);
13     }
14 }

這裡的registerTopics是由用戶調用setRegisterTopics方法註冊進來的Topic集合
在這裡會將集合中的Topic包裝成SubscriptionData保存在rebalanceImpl中

SubscriptionData:

 1 public class SubscriptionData implements Comparable<SubscriptionData> {
 2     public final static String SUB_ALL = "*";
 3     private boolean classFilterMode = false;
 4     private String topic;
 5     private String subString;
 6     private Set<String> tagsSet = new HashSet<String>();
 7     private Set<Integer> codeSet = new HashSet<Integer>();
 8     private long subVersion = System.currentTimeMillis();
 9     private String expressionType = ExpressionType.TAG;
10     ......
11 }


RebalanceImpl:

 1 public abstract class RebalanceImpl {
 2     protected final ConcurrentMap<MessageQueue, ProcessQueue> processQueueTable = new ConcurrentHashMap<MessageQueue, ProcessQueue>(64);
 3     protected final ConcurrentMap<String/* topic */, Set<MessageQueue>> topicSubscribeInfoTable =
 4         new ConcurrentHashMap<String, Set<MessageQueue>>();
 5     protected final ConcurrentMap<String /* topic */, SubscriptionData> subscriptionInner =
 6         new ConcurrentHashMap<String, SubscriptionData>();
 7     protected String consumerGroup;
 8     protected MessageModel messageModel;
 9     protected AllocateMessageQueueStrategy allocateMessageQueueStrategy;
10     protected MQClientInstance mQClientFactory;
11     ......
12 }

 

回到start方法,接著和Producer中一樣通過MQClientManager獲取一個MQClientInstance
然後會完成對rebalanceImpl屬性的填充

接著會實例化一個PullAPIWrapper,同時向其註冊過濾器的鉤子,這個對象在之後分析消息拉取時詳細介紹


接下來會根據消息的模式,決定使用不同方式的OffsetStore

 1 public enum MessageModel {
 2     /**
 3      * broadcast
 4      */
 5     BROADCASTING("BROADCASTING"),
 6     /**
 7      * clustering
 8      */
 9     CLUSTERING("CLUSTERING");
10     ......
11 }

分別是廣播模式和集群模式
廣播模式(BROADCASTING):同一個ConsumerGroup里的每個Consumer都能消費到所訂閱Topic的全部消息,也就是一個消息會被多次分發,被多個Consumer消費
集群模式(CLUSTERING):同一個ConsumerGroup里的每個Consumer只消費所訂閱消息的一部分內容,同一個ConsumerGroup里所有的Consumer消費的內容合起來才是所訂閱Topic內容的整體

採用廣播模式,消費者的消費進度offset會被保存在本地;而採用集群模式,消費者的消費進度offset會被保存在遠端(broker)上
故廣播模式使用LocalFileOffsetStore,集群模式使用RemoteBrokerOffsetStore


在採用廣播模式,即LocalFileOffsetStore,調用load方法會對其配置文件offsets.json進行載入,而RemoteBrokerOffsetStore時沒意義的非同步操作
LocalFileOffsetStore的load方法:

 1 public void load() throws MQClientException {
 2     OffsetSerializeWrapper offsetSerializeWrapper = this.readLocalOffset();
 3     if (offsetSerializeWrapper != null && offsetSerializeWrapper.getOffsetTable() != null) {
 4         offsetTable.putAll(offsetSerializeWrapper.getOffsetTable());
 5 
 6         for (MessageQueue mq : offsetSerializeWrapper.getOffsetTable().keySet()) {
 7             AtomicLong offset = offsetSerializeWrapper.getOffsetTable().get(mq);
 8             log.info("load consumer's offset, {} {} {}",
 9                 this.groupName,
10                 mq,
11                 offset.get());
12         }
13     }
14 }

readLocalOffset方法會將offsets.json文件中的json字元串轉換成OffsetSerializeWrapper對象封裝

 1 public class OffsetSerializeWrapper extends RemotingSerializable {
 2     private ConcurrentMap<MessageQueue, AtomicLong> offsetTable =
 3         new ConcurrentHashMap<MessageQueue, AtomicLong>();
 4 
 5     public ConcurrentMap<MessageQueue, AtomicLong> getOffsetTable() {
 6         return offsetTable;
 7     }
 8 
 9     public void setOffsetTable(ConcurrentMap<MessageQueue, AtomicLong> offsetTable) {
10         this.offsetTable = offsetTable;
11     }
12 }

從這裡就可里大致理解json文件中的內容,其中AtomicLong就對應MessageQueue下具體的Offset
之後在load方法中,會將該map保存在LocalFileOffsetStore中的offsetTable中


接著會調用mQClientFactory的start方法,這個方法在【RocketMQ中Producer的啟動源碼分析】中進行過分析

 

 1 public void start() throws MQClientException {
 2     synchronized (this) {
 3         switch (this.serviceState) {
 4             case CREATE_JUST:
 5                 this.serviceState = ServiceState.START_FAILED;
 6                 // If not specified,looking address from name server
 7                 if (null == this.clientConfig.getNamesrvAddr()) {
 8                     this.mQClientAPIImpl.fetchNameServerAddr();
 9                 }
10                 // Start request-response channel
11                 this.mQClientAPIImpl.start();
12                 // Start various schedule tasks
13                 this.startScheduledTask();
14                 // Start pull service
15                 this.pullMessageService.start();
16                 // Start rebalance service
17                 this.rebalanceService.start();
18                 // Start push service
19                 this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
20                 log.info("the client factory [{}] start OK", this.clientId);
21                 this.serviceState = ServiceState.RUNNING;
22                 break;
23             case RUNNING:
24                 break;
25             case SHUTDOWN_ALREADY:
26                 break;
27             case START_FAILED:
28                 throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
29             default:
30                 break;
31         }
32     }
33 }

首先若是沒有設置NameServer的地址,會調用fetchNameServerAddr方法進行自動定址,詳見Producer的啟動

之後mQClientAPIImpl的start方法會完成對Netty客戶端的綁定操作,詳見Producer的啟動

startScheduledTask方法則會設置五個定時任務:
①若是名稱服務地址namesrvAddr不存在,則調用前面的fetchNameServerAddr方法,定時更新名稱服務
②定時更新Topic所對應的路由信息
③定時清除離線的Broker,以及向當前線上的Broker發送心跳包
(以上詳見Producer的啟動)

④定時持久化消費者隊列的消費進度
DefaultMQPullConsumerImpl中的實現:

 1 public void persistConsumerOffset() {
 2     try {
 3         this.makeSureStateOK();
 4         Set<MessageQueue> mqs = new HashSet<MessageQueue>();
 5         Set<MessageQueue> allocateMq = this.rebalanceImpl.getProcessQueueTable().keySet();
 6         mqs.addAll(allocateMq);
 7         this.offsetStore.persistAll(mqs);
 8     } catch (Exception e) {
 9         log.error("group: " + this.defaultMQPullConsumer.getConsumerGroup() + " persistConsumerOffset exception", e);
10     }
11 }

首先從rebalanceImpl中取出所有處理的消費隊列MessageQueue集合
然後調用offsetStore的persistAll方法進一步處理該集合

由於廣播模式和集群模式,所以這裡有兩種實現:
廣播模式LocalFileOffsetStore的persistAll方法:

 1 public void persistAll(Set<MessageQueue> mqs) {
 2     if (null == mqs || mqs.isEmpty())
 3         return;
 4 
 5     OffsetSerializeWrapper offsetSerializeWrapper = new OffsetSerializeWrapper();
 6     for (Map.Entry<MessageQueue, AtomicLong> entry : this.offsetTable.entrySet()) {
 7         if (mqs.contains(entry.getKey())) {
 8             AtomicLong offset = entry.getValue();
 9             offsetSerializeWrapper.getOffsetTable().put(entry.getKey(), offset);
10         }
11     }
12 
13     String jsonString = offsetSerializeWrapper.toJson(true);
14     if (jsonString != null) {
15         try {
16             MixAll.string2File(jsonString, this.storePath);
17         } catch (IOException e) {
18             log.error("persistAll consumer offset Exception, " + this.storePath, e);
19         }
20     }
21 }

這裡和之前的load方法相反,會將MessageQueue對應的offset信息替換掉原來的json文件中的內容
這樣就完成了廣播模式下定時持久化消費者隊列的消費進度

集群模式RemoteBrokerOffsetStore的persistAll方法的實現:

 1 public void persistAll(Set<MessageQueue> mqs) {
 2     if (null == mqs || mqs.isEmpty())
 3         return;
 4 
 5     final HashSet<MessageQueue> unusedMQ = new HashSet<MessageQueue>();
 6     if (!mqs.isEmpty()) {
 7         for (Map.Entry<MessageQueue, AtomicLong> entry : this.offsetTable.entrySet()) {
 8             MessageQueue mq = entry.getKey();
 9             AtomicLong offset = entry.getValue();
10             if (offset != null) {
11                 if (mqs.contains(mq)) {
12                     try {
13                         this.updateConsumeOffsetToBroker(mq, offset.get());
14                         log.info("[persistAll] Group: {} ClientId: {} updateConsumeOffsetToBroker {} {}",
15                             this.groupName,
16                             this.mQClientFactory.getClientId(),
17                             mq,
18                             offset.get());
19                     } catch (Exception e) {
20                         log.error("updateConsumeOffsetToBroker exception, " + mq.toString(), e);
21                     }
22                 } else {
23                     unusedMQ.add(mq);
24                 }
25             }
26         }
27     }
28 
29     if (!unusedMQ.isEmpty()) {
30         for (MessageQueue mq : unusedMQ) {
31             this.offsetTable.remove(mq);
32             log.info("remove unused mq, {}, {}", mq, this.groupName);
33         }
34     }
35 }

和上面類似,遍歷offsetTable中的內容,只不過不是保存在了本地,而是通過updateConsumeOffsetToBroker向Broker發送
updateConsumeOffsetToBroker方法:

 1 private void updateConsumeOffsetToBroker(MessageQueue mq, long offset) throws RemotingException,
 2     MQBrokerException, InterruptedException, MQClientException {
 3     updateConsumeOffsetToBroker(mq, offset, true);
 4 }
 5 
 6 public void updateConsumeOffsetToBroker(MessageQueue mq, long offset, boolean isOneway) throws RemotingException,
 7     MQBrokerException, InterruptedException, MQClientException {
 8     FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInAdmin(mq.getBrokerName());
 9     if (null == findBrokerResult) {
10 
11         this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic());
12         findBrokerResult = this.mQClientFactory.findBrokerAddressInAdmin(mq.getBrokerName());
13     }
14 
15     if (findBrokerResult != null) {
16         UpdateConsumerOffsetRequestHeader requestHeader = new UpdateConsumerOffsetRequestHeader();
17         requestHeader.setTopic(mq.getTopic());
18         requestHeader.setConsumerGroup(this.groupName);
19         requestHeader.setQueueId(mq.getQueueId());
20         requestHeader.setCommitOffset(offset);
21 
22         if (isOneway) {
23             this.mQClientFactory.getMQClientAPIImpl().updateConsumerOffsetOneway(
24                 findBrokerResult.getBrokerAddr(), requestHeader, 1000 * 5);
25         } else {
26             this.mQClientFactory.getMQClientAPIImpl().updateConsumerOffset(
27                 findBrokerResult.getBrokerAddr(), requestHeader, 1000 * 5);
28         }
29     } else {
30         throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null);
31     }
32 }

首先根據BrokerName查找Broker的路由信息:

 1 public FindBrokerResult findBrokerAddressInAdmin(final String brokerName) {
 2     String brokerAddr = null;
 3     boolean slave = false;
 4     boolean found = false;
 5 
 6     HashMap<Long/* brokerId */, String/* address */> map = this.brokerAddrTable.get(brokerName);
 7     if (map != null && !map.isEmpty()) {
 8         for (Map.Entry<Long, String> entry : map.entrySet()) {
 9             Long id = entry.getKey();
10             brokerAddr = entry.getValue();
11             if (brokerAddr != null) {
12                 found = true;
13                 if (MixAll.MASTER_ID == id) {
14                     slave = false;
15                 } else {
16                     slave = true;
17                 }
18                 break;
19 
20             }
21         } // end of for
22     }
23 
24     if (found) {
25         return new FindBrokerResult(brokerAddr, slave, findBrokerVersion(brokerName, brokerAddr));
26     }
27 
28     return null;
29 }

brokerAddrTable中的borker的路由信息會由 ②定時更新Topic所對應的路由信息 ,來完成更新,在brokerAddrTable中只要找的一個Broker的信息後,將其封裝為FindBrokerResult返回

若是沒有找到會執行updateTopicRouteInfoFromNameServer方法,也就是執行了一次定時任務中的方法,立即更新一次,再通過findBrokerAddressInAdmin方法,重新查找

找到之後,實例化一個請求頭 UpdateConsumerOffsetRequestHeader,將相應信息封裝,由於使用的是Oneway模式,所以這裡採用updateConsumerOffsetOneway方法,通過Netty向Broker發送

 1 public void updateConsumerOffsetOneway(
 2     final String addr,
 3     final UpdateConsumerOffsetRequestHeader requestHeader,
 4     final long timeoutMillis
 5 ) throws RemotingConnectException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException,
 6     InterruptedException {
 7     RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_CONSUMER_OFFSET, requestHeader);
 8 
 9     this.remotingClient.invokeOneway(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), request, timeoutMillis);
10 }

其實這裡就非常簡單地調用了invokeOneway方法,完成向Broker的消息單向發送

【RocketMQ中Producer消息的發送源碼分析】

非OneWay則採用同步發送
這樣,在集群模式下,消費進度也就交給了Broker管理,之後的負載均衡以此為基礎


⑤定時調整消費者端的線程池的大小
這裡針對的是PushConsumer,後續博客再介紹


對於PullConsumer來說rebalanceService服務的開啟才是最重要的

RebalanceService:

 1 public void run() {
 2     log.info(this.getServiceName() + " service started");
 3 
 4     while (!this.isStopped()) {
 5         this.waitForRunning(waitInterval);
 6         this.mqClientFactory.doRebalance();
 7     }
 8 
 9     log.info(this.getServiceName() + " service end");
10 }

這裡的waitForRunning和Broker的刷盤以及主從複製類似,會進行超時阻塞(預設20s),也可以通過Broker發送的NOTIFY_CONSUMER_IDS_CHANGED請求將其喚醒,之後會調用doRebalance方法

RebalanceImpl的doRebalance方法:

 1 public void doRebalance(final boolean isOrder) {
 2    Map<String, SubscriptionData> subTable = this.getSubscriptionInner();
 3     if (subTable != null) {
 4         for (final Map.Entry<String, SubscriptionData> entry : subTable.entrySet()) {
 5             final String topic = entry.getKey();
 6             try {
 7                 this.rebalanceByTopic(topic, isOrder);
 8             } catch (Throwable e) {
 9                 if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
10                     log.warn("rebalanceByTopic Exception", e);
11                 }
12             }
13         }
14     }
15 
16     this.truncateMessageQueueNotMyTopic();
17 }

這裡就會取得copySubscription方法中說過的訂閱Topic集合,這個集合會在②中的定時任務會通過NameServer來進行更新

通過rebalanceByTopic方法,處理訂閱的Topic:

 1 private void rebalanceByTopic(final String topic, final boolean isOrder) {
 2     switch (messageModel) {
 3         case BROADCASTING: {
 4             Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
 5             if (mqSet != null) {
 6                 boolean changed = this.updateProcessQueueTableInRebalance(topic, mqSet, isOrder);
 7                 if (changed) {
 8                     this.messageQueueChanged(topic, mqSet, mqSet);
 9                     log.info("messageQueueChanged {} {} {} {}",
10                         consumerGroup,
11                         topic,
12                         mqSet,
13                         mqSet);
14                 }
15             } else {
16                 log.warn("doRebalance, {}, but the topic[{}] not exist.", consumerGroup, topic);
17             }
18             break;
19         }
20         case CLUSTERING: {
21             Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);

              
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 簡單來說,通過複製的方式創建對象。 【舉個慄子】:點外賣的收貨地址 ...
  • 移動電商平臺彈性架構案例雲服務彈性機房今天先到這兒,希望對技術領導力, 企業管理,系統架構設計與評估,團隊管理, 項目管理, 產品管理,團隊建設 有參考作用 , 您可能感興趣的文章: 領導人怎樣帶領好團隊構建創業公司突擊小團隊國際化環境下系統架構演化微服務架構設計視頻直播平臺的系統架構演化微服務與D... ...
  • 第一章 概述 1 spring 以 ioc 和 aop 為內核,提供了展現層 springMVC、持久層SpringJDBC及業務層事務管理等一站式企業級應用技術。 2spring的特性 方便解耦,簡化開發。通過IOC容器,用戶可以將對象之間的依賴關係交由spring進行控制,避免硬編碼所造成的的過 ...
  • JRebel 使用 JRebel 可以在修改代碼後,動態重新載入修改的代碼,免去了代碼工程全量重建、重啟的耗時流程,有效地提高開發者的效率。在 IDEA 的插件市場搜索 JRebel for IntelliJ 找到安裝即可。 JRebel for IntelliJ 版本:2019.1.4 1、啟用自 ...
  • Problem Description Problem Description Euler is a well-known matematician, and, among many other things, he discovered that the formulan^{2} + n + 41 ...
  • 一、多線程 1.我們的環境 (1)xubuntu 16.04(2)anaconda(3)pycharm(4)python 3.6 2.程式:一堆代碼以文本的形式存入一個文檔 3.進程:程式運行的一個狀態。 特點:(1)其中包含地址控制項、記憶體、數據棧等;(2)每個進程由自己完全獨立的運行環境,多進程共 ...
  • Windows下Beyond Compare 4 30天評估到期了的話,可以嘗試下麵兩種方式: 破解方式把Beyond Compare 4安裝文件夾下麵的BCUnrar.dll文件刪掉就行了,但是這種依然會提示在試用期 BC4註冊碼:可以用下麵這個註冊碼,有效期是到2019年12月 BEGIN LI ...
  • 使用dir()我們可以知道這個數據類型的內置函數有什麼方法: 1.迭代器 iterable:可迭代的 迭代就是將數據能夠一個一個按順序取出來 上面數據類型返回為真說明它是可以迭代的,反之是不可迭代的 可迭代協議: 就是內部要有一個__iter__()來滿足要求 當一個具有可迭代的數據執行__iter ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...