引言html
隨着公司業務的須要,原來的推送,不能知足現有的要求,好比,有的公司須要局域網部署平臺服務,不能訪問外網,意味着,經過自帶的 APNS遠程推送服務,基本上就掛掉了;再好比,公司的某一個業務,須要實時刷新數據,如股票價格,報警實時監測數據等這種場景。這時候,就須要另闢蹊徑;固然,咱們有不少種選擇,好比,RabbitMQ-實現了高級消息隊列協議的開源消息代理軟件,爲啥要用這個呢,由於Android已經用這個作好了,因此,iOS 也沒有其餘選擇了;ios
在集成客戶端的時候,咱們有一些概念須要知道,如消息隊列,生產者,消費者,交換器,隊列,通道等;git
RabbitMQ 本質上是 MB(Message Broker) 消息代理,能夠這麼認爲,RabbitMQ就像一個 「郵局」,負責收發存儲郵包(消息),惟一與郵局不一樣的是,它不會處理消息內容github
AMQP,即Advanced Message Queuing Protocol,高級消息隊列協議,是應用層協議的一個開放標準;相似Http,https,STMP這種,咱們只須要知道,他是一個協議,實際開發中會遇到,底層實現咱們不用管;web
RabbitMQ 是一個由 Erlang 語言開發的 AMQP 的開源實現。面試
支持多種客戶端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP,Object-C, Swift等,支持AJAX。用於在分佈式系統中存儲轉發消息,在易用性、擴展性、高可用性等方面表現不俗。json
RabbitMQ 使用一些機制來保證可靠性,如持久化、傳輸確認、發佈確認。瀏覽器
在消息進入隊列以前,經過 Exchange 來路由消息的。對於典型的路由功能,RabbitMQ 已經提供了一些內置的 Exchange 來實現。針對更復雜的路由功能,能夠將多個 Exchange 綁定在一塊兒,也經過插件機制實現本身的 Exchange 。bash
多個 RabbitMQ 服務器能夠組成一個集羣,造成一個邏輯 Broker 。服務器
隊列能夠在集羣中的機器上進行鏡像,使得在部分節點出問題的狀況下隊列仍然可用。
RabbitMQ 支持多種消息隊列協議,好比 STOMP、MQTT 等等。
RabbitMQ 幾乎支持全部經常使用語言,好比 Java、.NET、Ruby, Object-C等等。
RabbitMQ 提供了一個易用的用戶界面,使得用戶能夠監控和管理消息 Broker 的許多方面。
若是消息異常,RabbitMQ 提供了消息跟蹤機制,使用者能夠找出發生了什麼。
RabbitMQ 提供了許多插件,來從多方面進行擴展,也能夠編寫本身的插件。
全部 MQ 產品從模型抽象上來講都是同樣的過程:
消費者(consumer)訂閱某個隊列。生產者(producer)建立消息,而後發佈到隊列(queue)中,最後將消息發送到監聽的消費者。
上面介紹RabbitMQ是AMQP的實現,因此其內部也是AMQP的概念
發送消息的程序或者代碼就是生產者,通常是 服務端
交換器,用來接收生產者發送的消息並將這些消息路由給服務器中的隊列.生產者把消息發佈到 Exchange 上,消息最終到達隊列並被消費者接收,而 Binding 決定交換器的消息應該發送到那個隊列。
生產者在將消息發送給Exchange的時候,通常會指定一個routing key,來指定這個消息的路由規則,而這個routing key須要與Exchange Type及binding key聯合使用才能最終生效。
在Exchange Type與binding key固定的狀況下(在正常使用時通常這些內容都是固定配置好的),咱們的生產者就能夠在發送消息給Exchange時,經過指定routing key來決定消息流向哪裏。
RabbitMQ爲routing key設定的長度限制爲255 bytes。
此外,這個 Key 值,通常是服務端跟客戶端約定好的,能夠是組織ID,也能夠用ID
綁定,用於消息隊列和交換器之間的關聯。一個綁定就是基於路由鍵將交換器和消息隊列鏈接起來的路由規則,因此能夠將交換器理解成一個由綁定構成的路由表。
消息隊列,用來保存消息直到發送給消費者。它是消息的容器,也是消息的終點。一個消息可投入一個或多個隊列。消息一直在隊列裏面,等待消費者鏈接到這個隊列將其取走。
網絡鏈接,好比一個TCP鏈接,RabbitMQ的OC CLient就是封裝了 開源的TCP實現CocoaAsyncSocket
信道,多路複用鏈接中的一條獨立的雙向數據流通道。信道是創建在真實的TCP鏈接內地虛擬鏈接,AMQP 命令都是經過信道發出去的,不論是發佈消息、訂閱隊列仍是接收消息,這些動做都是經過信道完成。由於對於操做系統來講創建和銷燬 TCP 都是很是昂貴的開銷,因此引入了信道的概念,以複用一條 TCP 鏈接。
訂閱某個隊列,消息的接受者
生產者把消息發佈到 Exchange 上,消息最終到達隊列並被消費者接收,而 Binding 決定交換器的消息應該發送到那個隊列。
在綁定(Binding)Exchange與Queue的同時,通常會指定一個binding key;生產者將消息發送給Exchange時,通常會指定一個routing key;當binding key與routing key相匹配時,消息將會被路由到對應的Queue中。
在綁定多個Queue到同一個Exchange的時候,這些Binding容許使用相同的binding key。
binding key 並非在全部狀況下都生效,它依賴於Exchange Type,好比fanout類型的Exchange就會無視binding key,而是將消息路由到全部綁定到該Exchange的Queue。
RabbitMQ經常使用的Exchange Type有==fanout==、==direct==、==topic==、==headers==這四種(AMQP規範裏還提到兩種Exchange Type,分別爲system與自定義,這裏不予以描述),headers 匹配 AMQP 消息的 header 而不是路由鍵,此外 headers 交換器和 direct 交換器徹底一致,但性能差不少,目前幾乎用不到了,因此直接看另外三種類型:
direct類型的Exchange路由規則也很簡單,它會把消息路由到那些binding key與routing key徹底匹配的Queue中。
相關代碼以下:
/** 建立信道 */
id<RMQChannel> ch = [_conn createChannel];
/** 建立交換器,direct方法,這個須要跟後臺保持一致,包括交換器的名稱,也要跟後臺約定好,還有配置選項,是否持久化等 */
RMQExchange *x = [ch direct:@"Mobile_Alarm" options:(RMQExchangeDeclareNoOptions)];
/** 建立隊列,若是不指定隊列名稱,MQ會默認自動建立一個隊列,前綴以 `rmq-objc-client.gen-` 開頭 */
RMQQueue *q = [ch queue:@"" options:RMQQueueDeclareExclusive | RMQQueueDeclareAutoDelete];
/** 隊列綁定交換器,並指定 rountKey,須要跟後臺指定相同的規則,能夠是用戶ID等 */
[q bind:x routingKey:@"5"];
複製代碼
fanout
這種類型的Exchange,就是羣發,此exchange的路由規則很簡單直接將消息路由到全部綁定的隊列中,無須對消息的routingkey進行匹配操做。
/** 須要注意:<RMQConnectionDelegate> 若是建立鏈接,指定代理是 當前class,那麼當前class須要遵照鏈接的代理協議,並實現相關代理方法*/
/** 建立鏈接 */
RMQConnection *conn = [[RMQConnection alloc] initWithUri:url5 delegate:self];
[conn start];
/** 建立信道 */
id<RMQChannel> ch = [conn createChannel];
/** 建立交換器 */
RMQExchange *x = [ch fanout:@"Alarm"];
RMQQueue *q = [ch queue:@"" options:RMQQueueDeclareExclusive];
/** 綁定交換器 */
[q bind:x];
/** 訂閱消息 */
[q subscribe:^(RMQMessage * _Nonnull message) {
NSLog(@"Received %@", [[NSString alloc] initWithData:message.body encoding:NSUTF8StringEncoding]);
}];
///socket 鏈接失敗回調,超時或者地址有誤
- (void)connection:(RMQConnection *)connection failedToConnectWithError:(NSError *)error;
/// 沒有鏈接成功回調
- (void)connection:(RMQConnection *)connection disconnectedWithError:(NSError *)error;
/// 自動恢復鏈接調用
- (void)willStartRecoveryWithConnection:(RMQConnection *)connection;
/// 正在開始恢復鏈接
- (void)startingRecoveryWithConnection:(RMQConnection *)connection;
/// 已經恢復鏈接的時候,調用
- (void)recoveredConnection:(RMQConnection *)connection;
/// 鏈接過程當中,信道異常調用
- (void)channel:(id<RMQChannel>)channel error:(NSError *)error;
複製代碼
前面講到direct類型的Exchange路由規則是徹底匹配binding key與routing key,但這種嚴格的匹配方式在不少狀況下不能知足實際業務需求。topic類型的Exchange在匹配規則上進行了擴展,它與direct類型的Exchage類似,也是將消息路由到binding key與routing key相匹配的Queue中,但這裏的匹配規則有些不一樣,它約定:
routing key爲一個句點號「. 」分隔的字符串(咱們將被句點號「. 」分隔開的每一段獨立的字符串稱爲一個單詞),如「device.userId」、「alarm.type」,
binding key與routing key同樣也是句點號「. 」分隔的字符串
binding key中能夠存在兩種特殊字符「」與「#」,用於作模糊匹配,其中「」用於匹配一個單詞,「#」用於匹配多個單詞(能夠是零個)
RMQConnection * connection = [[RMQConnection alloc] initWithUri:url delegate:[RMQConnectionDelegateLogger new]];
[connection start];
id<RMQChannel>channel = [connection createChannel];
RMQExchange * exchange = [channel topic:@"topic_logs" options:RMQExchangeDeclarePassive];
[exchange publish:finalData routingKey:[NSString stringWithFormat:@"device.%@",didStr]];
[connection close];
複製代碼
以上圖中的配置爲例,routingKey=」dev.alarm.device」的消息會同時路由到QA與QB,routingKey=」dev.alarm.type」的消息會路由到QA,routingKey=」lazy.brown.fox」的消息會路由到Q2,routingKey=」water.type.ID」的消息會路由到QB;routingKey=」device.user.water」、routingKey=」alarmtype」的消息將會被丟棄,由於它們沒有匹配任何bindingKey。
pod 'RMQClient'
複製代碼
注意:
RabbitMQ pod 內部有 CocoaAsyncSocket,若是項目中,已經集成了,須要刪除多餘的
考慮到客戶端架構的特殊性,須要將MQ消息訂閱封裝成一個工具,並配置爲 單例 模式。
導入頭文件 #import <RMQClient.h>
LXCustomRabbitMQManger.h
//開始訂閱消息
- (void)startScribeMessage;
//關閉鏈接
- (void)closeConnection;
複製代碼
LXCustomRabbitMQManger.m
@interface LXCustomRabbitMQManger()<RMQConnectionDelegate>
@property (nonatomic,strong) RMQConnection *conn;
@property (nonatomic,strong) LXCustomAlarmModel *alarmModel;
@end
- (void)startScribeMessage {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(activeNotification:) name:UIApplicationDidBecomeActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(backgroundNotification:) name:UIApplicationDidEnterBackgroundNotification object:nil];
}
- (void)closeConnection {
if (_conn) {
[self.conn close];
self.conn = nil;
}
}
//建立本地推送
- (void)registerNotification:(NSInteger )alerTime {
kweakSelf;
kStrongSelf;
// 使用 UNUserNotificationCenter 來管理通知
if (@available(iOS 10.0, *)) {
// 使用 UNUserNotificationCenter 來管理通知
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
//需建立一個包含待通知內容的 UNMutableNotificationContent 對象,注意不是 UNNotificationContent ,此對象爲不可變對象。
UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
content.title = [NSString localizedUserNotificationStringForKey:self.alarmModel.alarm_type arguments:nil];
content.body = [NSString localizedUserNotificationStringForKey:self.alarmModel.geography
arguments:nil];
content.sound = [UNNotificationSound defaultSound];
// 在 alertTime 後推送本地推送
UNTimeIntervalNotificationTrigger* trigger = [UNTimeIntervalNotificationTrigger
triggerWithTimeInterval:alerTime repeats:NO];
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:@"FiveSecond"
content:content trigger:trigger];
//添加推送成功後的處理!
[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
/* DQAlertView *reservationAlert = [[DQAlertView alloc] initWithTitle:self.alarmModel.alarm_type message:self.alarmModel.geography delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"肯定"]; reservationAlert.shouldDimBackgroundWhenShowInView = YES; reservationAlert.shouldDismissOnOutsideTapped = YES; [reservationAlert show]; [reservationAlert actionWithBlocksCancelButtonHandler:^{ } otherButtonHandler:^{ //跳轉 LXDeviceAlarmModel *alarmIDModel = [[LXDeviceAlarmModel alloc] init]; alarmIDModel.ID = strongSelf.alarmModel.ID; LXDeviceDetailController *deviceDetailVC = [[LXDeviceDetailController alloc] init]; deviceDetailVC.deviceDealType = KearlyNoDealType; deviceDetailVC.deviceAlarm = alarmIDModel; [[self getCurrentVC].navigationController pushViewController:deviceDetailVC animated:YES]; }]; */
UIAlertController *alert = [UIAlertController alertControllerWithTitle:self.alarmModel.alarm_type message:self.alarmModel.geography preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil];
UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"肯定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
LXDeviceAlarmModel *alarmIDModel = [[LXDeviceAlarmModel alloc] init];
alarmIDModel.ID = strongSelf.alarmModel.ID;
LXDeviceDetailController *deviceDetailVC = [[LXDeviceDetailController alloc] init];
deviceDetailVC.deviceDealType = KearlyNoDealType;
deviceDetailVC.deviceAlarm = alarmIDModel;
[[self getCurrentVC].navigationController pushViewController:deviceDetailVC animated:YES];
}];
[alert addAction:cancelAction];
[alert addAction:confirmAction];
[[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert animated:YES completion:nil];
}];
}
else {
UILocalNotification *notification = [[UILocalNotification alloc] init];
// 設置觸發通知的時間
NSDate *fireDate = [NSDate dateWithTimeIntervalSinceNow:1];
NSLog(@"fireDate=%@",fireDate);
notification.fireDate = fireDate;
// 時區
notification.timeZone = [NSTimeZone defaultTimeZone];
// 設置重複的間隔
notification.repeatInterval = kCFCalendarUnitSecond;
// 通知內容
notification.alertBody = self.alarmModel.alarm_type;
notification.applicationIconBadgeNumber = 1;
// 通知被觸發時播放的聲音
notification.soundName = UILocalNotificationDefaultSoundName;
// 通知參數
NSDictionary *userDict = [NSDictionary dictionaryWithObject:self.alarmModel.geography forKey:@"key"];
notification.userInfo = userDict;
// ios8後,須要添加這個註冊,才能獲得受權
if ([[UIApplication sharedApplication] respondsToSelector:@selector(registerUserNotificationSettings:)]) {
UIUserNotificationType type = UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound;
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:type
categories:nil];
[[UIApplication sharedApplication] registerUserNotificationSettings:settings];
// 通知重複提示的單位,能夠是天、周、月
notification.repeatInterval = NSCalendarUnitDay;
} else {
// 通知重複提示的單位,能夠是天、周、月
notification.repeatInterval = NSDayCalendarUnit;
}
// 執行通知註冊
[[UIApplication sharedApplication] scheduleLocalNotification:notification];
}
}
- (void)receiveRabbitServicerMessage {
// NSString *url5 = @"amqp://web_user:123@192.178.0.2:5672/device_log_2";
DebugLog(@"MQ服務器地址:%@",url5);
if (_conn == nil) {
_conn = [[RMQConnection alloc] initWithUri:url5 delegate:self];
}
[_conn start];
id<RMQChannel> ch = [_conn createChannel];
RMQExchange *x = [ch direct:@"Mobile_Electric_Alarm" options:(RMQExchangeDeclareNoOptions)];
RMQQueue *q = [ch queue:@"" options:RMQQueueDeclareExclusive | RMQQueueDeclareAutoDelete];
[q bind:x routingKey:kUserInfo.company_id];
kweakSelf;
[q subscribe:^(RMQMessage * _Nonnull message) {
id result = [[NSString alloc] initWithData:message.body encoding:NSUTF8StringEncoding];
NSDictionary *dict = [self dictionaryWithJsonString:result];
if ([dict isKindOfClass:[NSDictionary class]]) {
weakSelf.alarmModel = [LXCustomAlarmModel mj_objectWithKeyValues:dict];
[weakSelf registerNotification:1];
}
}];
}
- (void)emitLogDirect:(NSString *)msg severity:(NSString *)severity {
// NSString *url5 = @"amqp://web_user:123@192.178.0.2:5672/device_log_2";
DebugLog(@"MQ發送服務器地址:%@",url5);
RMQConnection *conn = [[RMQConnection alloc] initWithUri:url5 delegate:self];
self.conn = conn;
[conn start];
id<RMQChannel> ch = [conn createChannel];
RMQExchange *x = [ch direct:@"Mobile_Electric_Alarm" options:(RMQExchangeDeclareNoOptions)];
RMQQueue *q = [ch queue:@"" options:RMQQueueDeclareExclusive | RMQQueueDeclareAutoDelete];
[q bind:x routingKey:kUserInfo.company_id];
[x publish:[msg dataUsingEncoding:NSUTF8StringEncoding] routingKey:severity];
NSLog(@"Sent '%@'", msg);
[conn close];
}
#pragma mark - 系統的通知監聽
- (void)activeNotification:(NSNotification *)notification{
if (_conn == nil) {
//登陸成功
if ([[LXUserInfoManger shareLXUserInfoManger].currentUserInfo.is_cloud isEqualToString:@"0"]) {
//MQ推送
[self receiveRabbitServicerMessage];
}
}
}
- (void)backgroundNotification:(NSNotification *)notification{
[self closeConnection];
}
- (void)connection:(RMQConnection *)connection failedToConnectWithError:(NSError *)error {
if (error) {
NSLog(@"%@",error);
NSLog(@"鏈接超時");
[self closeConnection];
}else{
}
}
- (void)connection:(RMQConnection *)connection disconnectedWithError:(NSError *)error {
if (error) {
NSLog(@"%@",error);
}
else{
NSLog(@"鏈接成功");
}
}
- (void)willStartRecoveryWithConnection:(RMQConnection *)connection {
DebugLog(@"將要開始恢復連接");
}
- (void)startingRecoveryWithConnection:(RMQConnection *)connection {
DebugLog(@"開始恢復連接");
}
- (void)recoveredConnection:(RMQConnection *)connection {
DebugLog(@"恢復連接");
}
- (void)channel:(id<RMQChannel>)channel error:(NSError *)error {
if (error) {
NSLog(@"%@",error);
[self closeConnection];
}
}
//獲取當前屏幕顯示的viewcontroller,當接收到推送須要跳轉VC
- (UIViewController *)getCurrentVC {
UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
UIViewController *currentVC = [self getCurrentVCFrom:rootViewController];
return currentVC;
}
- (UIViewController *)getCurrentVCFrom:(UIViewController *)rootVC {
UIViewController *currentVC;
if ([rootVC presentedViewController]) {
// 視圖是被presented出來的
rootVC = [rootVC presentedViewController];
}
if ([rootVC isKindOfClass:[UITabBarController class]]) {
// 根視圖爲UITabBarController
currentVC = [self getCurrentVCFrom:[(UITabBarController *)rootVC selectedViewController]];
} else if ([rootVC isKindOfClass:[UINavigationController class]]){
// 根視圖爲UINavigationController
currentVC = [self getCurrentVCFrom:[(UINavigationController *)rootVC visibleViewController]];
} else {
// 根視圖爲非導航類
currentVC = rootVC;
}
return currentVC;
}
//接收到推送,json格式字符串轉字典:由於MQ推過來的消息是 String 類型的
- (NSDictionary *)dictionaryWithJsonString:(NSString *)jsonString {
if (jsonString == nil) {
return nil;
}
NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
NSError *err;
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:jsonData
options:NSJSONReadingMutableContainers
error:&err];
if(err) {
NSLog(@"json解析失敗:%@",err);
return nil;
}
return dic;
}
複製代碼
若是電腦安裝 brew 軟件包工具的話,執行並啓動
brew install rabbitmq
sudo ./rabbitmq-server
複製代碼
瀏覽器訪問:localhost:15672,默認帳號爲:guest 密碼: guest
Received connection: <RMQConnection: 0x103fa1fc0> disconnectedWithError: Error Domain=GCDAsyncSocketErrorDomain Code=7 "Socket closed by remote peer" UserInfo={NSLocalizedDescription=Socket closed by remote peer}
解決辦法:MQ的鑑權,O-C跟Java的格式不太同樣,Java是經過Name,Host,Ip等,iOS的是直接設置地址
說明:
Error Domain=com.rabbitmq.rabbitmq-objc-client Code=406 "PRECONDITION_FAILED - inequivalent arg 'durable' for queue 'Mobile_Electric_Alarm' in vhost 'device_log_2': received 'false' but current is 'true'" UserInfo={NSLocalizedDescription=PRECONDITION_FAILED - inequivalent arg 'durable' for queue 'Mobile_Electric_Alarm' in vhost 'device_log_2': received 'false' but current is 'true'}
看3.3解決辦法
Error Domain=com.rabbitmq.rabbitmq-objc-client Code=404 "NOT_FOUND - no queue 'rmq-objc-client.gen-3B6E0E14-06E6-4AFC-B6E1-42A7F8FCD218-46927-00002C8AE36AB350' in vhost 'device_log_2'" UserInfo={NSLocalizedDescription=NOT_FOUND - no queue 'rmq-objc-client.gen-3B6E0E14-06E6-4AFC-B6E1-42A7F8FCD218-46927-00002C8AE36AB350' in vhost 'device_log_2'}
解決辦法:
Exchange 交換器的配置參數,跟後臺約定的不一致,有時候後臺也不知道本身配置的啥,就須要嘗試了;
/** typedef NS_OPTIONS(NSUInteger, RMQExchangeDeclareOptions) { RMQExchangeDeclareNoOptions = 0, /// 被動聲明 RMQExchangeDeclarePassive = 1 << 0, ///交換器持久化 RMQExchangeDeclareDurable = 1 << 1, /// 自動銷燬交換器,當全部的消息隊列都被使用 RMQExchangeDeclareAutoDelete = 1 << 2, /// 配置的交換器內部構造對應用發佈者不可見 RMQExchangeDeclareInternal = 1 << 3, /// @brief RMQExchangeDeclareNoWait = 1 << 4, };*/
RMQExchange *x = [ch direct:@"Mobile_Electric_Alarm" options:(RMQExchangeDeclareNoOptions)];
/** * 隊列的配置可選參數 * typedef NS_OPTIONS(NSUInteger, RMQQueueDeclareOptions) { RMQQueueDeclareNoOptions = 0, /// 被動聲明 RMQQueueDeclarePassive = 1 << 0, /// 隊列持久化 RMQQueueDeclareDurable = 1 << 1, /// 只能被當前的鏈接受權訪問 RMQQueueDeclareExclusive = 1 << 2, /// 自動刪除 RMQQueueDeclareAutoDelete = 1 << 3, /// RMQQueueDeclareNoWait = 1 << 4, }; */
RMQQueue *q = [ch queue:@"" options:RMQQueueDeclareExclusive | RMQQueueDeclareAutoDelete];
複製代碼
若是鏈接成功後,那麼在MQ的管理臺,就能夠看到當前鏈接的消費者了
[圖片上傳失敗...(image-97aaad-1534667228188)]
[參考文章]
1> RabbbitMQ官網
RabbitMQ在iOS中的使用資料不多,若是有問題的話,歡迎留言探討。
另外,若是你以爲個人文章對你有必定的幫助,很是感謝可以點贊,謝謝!
做爲一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個個人iOS交流羣:810733363。微信交流羣能夠加小編微信(18173184023 )。無論你是小白仍是大牛歡迎入駐 ,分享BAT,阿里面試題、面試經驗,討論技術, 你們一塊兒交流學習成長!但願幫助開發者少走彎路。
來源:本文爲第三方轉載,若有侵權請聯繫小編刪除。