XMPP即时通讯(代码实现)
1.配置XMPP(XMPPConfig.m)
2.配置XMPPFramework框架
3.创建单例类(XMPPManager.h/XMPPManager.m)管理器
XMPPManager.m:
#import "XMPPManager.h"
#import "AppDelegate.h"
//连接服务器的目的
typedef NS_ENUM(NSInteger, ConnectToServerPopurpose)
{
ConnectToServerPopurposeLogin, //登录
ConnectToServerPopurposeRegist //注册
};
@interface XMPPManager ()<XMPPStreamDelegate,XMPPRosterDelegate>
@property (nonatomic, assign) ConnectToServerPopurpose serverPurpose; //连接服务器的目的
@property (nonatomic, copy) NSString *loginPassword; //登录密码
@property (nonatomic, copy) NSString *registerPassword; //注册密码
- (void)connectServer; //连接服务器
- (void)disConnectWithServer; //断开服务器
@end
@implementation XMPPManager
static XMPPManager *manager = nil;
+ (XMPPManager *)defaultXMPPManager {
//gcd once 程序执行期间只执行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[XMPPManager alloc] init];
});
return manager;
}
//重写init方法
- (instancetype)init
{
self = [super init];
if (self) {
//创建通信通道用于和服务器进行连接和沟通
self.stream = [[XMPPStream alloc] init];
//设置服务器
self.stream.hostName = kHostName;
//设置端口号
self.stream.hostPort = kHostPort;
//添加代理
[self.stream addDelegate:self delegateQueue:dispatch_get_main_queue()];
//创建好友列表仓库
XMPPRosterCoreDataStorage *rosterCorDataStorage = [XMPPRosterCoreDataStorage sharedInstance];
//创建花名册对象
self.roster = [[XMPPRoster alloc] initWithRosterStorage:rosterCorDataStorage dispatchQueue:dispatch_get_main_queue()];
//将花名册对象添加到stream活动
[self.roster activate:self.stream];
//添加代理
[self.roster addDelegate:self delegateQueue:dispatch_get_main_queue()];
//创建信息归档对象
XMPPMessageArchivingCoreDataStorage *messageArchingCoreStorage = [XMPPMessageArchivingCoreDataStorage sharedInstance];
//创建信息归档对象
self.messageArching = [[XMPPMessageArchiving alloc] initWithMessageArchivingStorage:messageArchingCoreStoragedispatchQueue:dispatch_get_main_queue()];
//添加活动到通信管道
[self.messageArching activate:self.stream];
//获取数据管理器
self.managerContext = messageArchingCoreStorage.mainThreadManagedObjectContext;
}
return self;
}
#pragma mark - XMPPRosterDelegate
- (void)xmppRoster:(XMPPRoster *)sender didReceivePresenceSubscriptionRequest:(XMPPPresence *)presence {
//获取请求对象的JID
XMPPJID *requestJID = presence.from;
//创建提示框
UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:@"添加好友提醒" message:requestJID.userpreferredStyle:UIAlertControllerStyleAlert];
//创建事件
UIAlertAction *addAction = [UIAlertAction actionWithTitle:@"添加" style:(UIAlertActionStyleDefault) handler:^(UIAlertAction *action) {
//接受好友请求
[self.roster acceptPresenceSubscriptionRequestFrom:requestJID andAddToRoster:YES];
}];
UIAlertAction *rejectAction = [UIAlertAction actionWithTitle:@"拒绝" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
//拒绝好友请求
[self.roster rejectPresenceSubscriptionRequestFrom:requestJID];
}];
//添加事件
[alertVC addAction:addAction];
[alertVC addAction:rejectAction];
//弹出提示框
//获取AppDelegate
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
//获取根视图控制器
UIViewController *rootVC = appDelegate.window.rootViewController;
[rootVC presentViewController:alertVC animated:YES completion:nil];
}
//连接服务器
- (void)connectServer {
if ([self.stream isConnected] || [self.stream isConnecting]) {
//断开连接
[self disConnectWithServer];
}
//建立新的连接(30秒超时)
NSError *error = nil;
[self.stream connectWithTimeout:30 error:&error];
if (error) {
NSLog(@"connect fail");
}
}
//登录
- (void)loginWithUserName:(NSString *)username password:(NSString *)pw {
self.loginPassword = pw; //记录登录密码
self.serverPurpose = ConnectToServerPopurposeLogin; //登录标识
//获取jid 唯一标识
XMPPJID *myJID = [XMPPJID jidWithUser:username domain:kDomin resource:kResource];
self.stream.myJID = myJID; //设置JID
//连接服务器
[self connectServer];
}
//注册
- (void)registWithUserName:(NSString *)username passeord:(NSString *)pw {
self.registerPassword = pw; //记录注册密码
self.serverPurpose = ConnectToServerPopurposeRegist; //注册标识
//获取JID唯一标识
XMPPJID *myJID = [XMPPJID jidWithUser:username domain:kDomin resource:kResource];
self.stream.myJID = myJID; //设置JID
//连接服务器
[self connectServer];
}
//添加好友
- (void)addFriend:(NSString *)name {
//创建JID
XMPPJID *myJID = [XMPPJID jidWithString:[NSString stringWithFormat:@"%@@%@",name,kHostName]];
//添加好友
[self.roster subscribePresenceToUser:myJID];
}
//删除好友
- (void)deleteFriend:(NSString *)name {
//获取JID
XMPPJID *myJID = [XMPPJID jidWithString:[NSString stringWithFormat:@"%@@%@",name,kHostName]];
[self.roster removeUser:myJID];
}
//断开服务器连接
- (void)disConnectWithServer {
//断开服务器
[self.stream disconnect];
}
#pragma mark - XMPPStreamDelegate
//成功连接服务器
- (void)xmppStreamDidConnect:(XMPPStream *)sender {
NSLog(@"connect success");
switch (self.serverPurpose) {
case ConnectToServerPopurposeLogin:
//登录
{
[self.stream authenticateWithPassword:self.loginPassword error:nil];
}
break;
case ConnectToServerPopurposeRegist:
//注册
{
[self.stream registerWithPassword:self.registerPassword error:nil];
}
break;
default:
break;
}
}
//连接超时
- (void)xmppStreamConnectDidTimeout:(XMPPStream *)sender {
NSLog(@"connect time out");
}
@end
用户登录:
#import "LoginViewController.h"
#import "XMPPManager.h"
#import "RosterTableViewController.h"
BOOL isClickButton = YES;
@interface LoginViewController ()<XMPPStreamDelegate>
@property (weak, nonatomic) IBOutlet UITextField *userNameTF;
@property (weak, nonatomic) IBOutlet UITextField *passwordTF;
@end
@implementation LoginViewController
- (void)viewDidLoad {
[super viewDidLoad];
//添加代理
[[XMPPManager defaultXMPPManager].stream addDelegate:self delegateQueue:dispatch_get_main_queue()];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
/*
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
#pragma mark - handleAction
- (IBAction)handleLogin:(UIButton *)sender {
isClickButton = YES; //标识点击了登录button
[[XMPPManager defaultXMPPManager] loginWithUserName:self.userNameTF.text password:self.passwordTF.text];
}
- (IBAction)handleRegister:(UIButton *)sender {
}
#pragma mark - XMPPStreamDelegate
//登录成功
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender {
//上线--更改状态
XMPPPresence *presence = [XMPPPresence presenceWithType:@"available"];
[[XMPPManager defaultXMPPManager].stream sendElement:presence];
if (isClickButton) {
//提示
UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:@"温馨提示" message:@"欢迎回来" preferredStyle:(UIAlertControllerStyleAlert)];
//添加事件
UIAlertAction *action = [UIAlertAction actionWithTitle:@"好" style:(UIAlertActionStyleDefault) handler:^(UIAlertAction *action) {
//获取从storyBoard中获取联系人列表界面
RosterTableViewController *rosterVC = [self.storyboard instantiateViewControllerWithIdentifier:@"contact"];
//传值
rosterVC.userName = self.userNameTF.text;
rosterVC.paassWord = self.passwordTF.text;
//push
[self.navigationController pushViewController:rosterVC animated:YES];
}];
[alertVC addAction:action];
//弹出提示
[self presentViewController:alertVC animated:YES completion:nil];
//更改BOOL
isClickButton = NO;
}
}
//登录失败
- (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(NSXMLElement *)error {
//提示
UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:@"温馨提示" message:@"账号或密码错误请核对" preferredStyle:(UIAlertControllerStyleAlert)];
//添加事件
UIAlertAction *action = [UIAlertAction actionWithTitle:@"好" style:(UIAlertActionStyleDefault) handler:^(UIAlertAction *action) {
}];
[alertVC addAction:action];
//弹出提示
[self presentViewController:alertVC animated:YES completion:nil];
}
@end
用户注册:
#import "RegisterViewController.h"
#import "XMPPManager.h"
@interface RegisterViewController ()<XMPPStreamDelegate>
@property (weak, nonatomic) IBOutlet UITextField *userNameTF;
@property (weak, nonatomic) IBOutlet UITextField *passwordTF;
@property (weak, nonatomic) IBOutlet UITextField *rePasswordTF;
@end
@implementation RegisterViewController
- (void)viewDidLoad {
[super viewDidLoad];
//添加代理
[[XMPPManager defaultXMPPManager].stream addDelegate:self delegateQueue:dispatch_get_main_queue()];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
/*
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
#pragma mark - handleAction
- (IBAction)handleSubmit:(UIButton *)sender {
//注册
[[XMPPManager defaultXMPPManager] registWithUserName:self.userNameTF.text passeord:self.passwordTF.text];
}
- (IBAction)handleCleare:(UIButton *)sender {
}
#pragma mark - XMPPStreamDelegate
//注册成功
- (void)xmppStreamDidRegister:(XMPPStream *)sender {
NSLog(@"register success");
}
//注册失败
- (void)xmppStream:(XMPPStream *)sender didNotRegister:(NSXMLElement *)error {
NSLog(@"register fail");
}
@end
联系人列表
#import "RosterTableViewController.h"
#import "RosterCell.h"
#import "ChatTableViewController.h"
#import "XMPPManager.h"
@interface RosterTableViewController ()<XMPPRosterDelegate>
@property (nonatomic, strong) NSMutableArray *contacts; //联系人数组
@end
@implementation RosterTableViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
self.contacts = [NSMutableArray array]; //创建数组
//添加代理
[[XMPPManager defaultXMPPManager].roster addDelegate:self delegateQueue:dispatch_get_main_queue()];
//再次登录
[[XMPPManager defaultXMPPManager] loginWithUserName:self.userName password:self.paassWord];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.contacts.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
RosterCell *cell = [tableView dequeueReusableCellWithIdentifier:@"roster" forIndexPath:indexPath];
//获取对应的联系人JID
XMPPJID *jid = self.contacts[indexPath.row];
cell.textLabel.text = jid.user;
return cell;
}
#pragma mark - XMPPRosterDelegate
//开始检索好友
- (void)xmppRosterDidBeginPopulating:(XMPPRoster *)sender {
NSLog(@"begin search friends");
}
//检索好友,每执行一次获取一个好友信息
- (void)xmppRoster:(XMPPRoster *)sender didRecieveRosterItem:(NSXMLElement *)item {
NSLog(@"%@",[[item attributeForName:@"jid"] stringValue]);
//获取JIDStr
NSString *jidStr = [[item attributeForName:@"jid"] stringValue];
//获取JID
XMPPJID *myJID = [XMPPJID jidWithString:jidStr resource:kResource];
//防止重复添加好友
for (XMPPJID *JID in self.contacts) {
if ([JID.user isEqualToString:myJID.user]) {
return;
}
}
//放入数组
[self.contacts addObject:myJID];
//刷新界面
[self.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:self.contacts.count - 1 inSection:0]]withRowAnimation:UITableViewRowAnimationLeft];
}
//结束检索好友
- (void)xmppRosterDidEndPopulating:(XMPPRoster *)sender {
NSLog(@"end search friends");
}
#pragma mark - Navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
//界面传值
//获取下一个试图控制器
ChatTableViewController *chatVC = segue.destinationViewController;
//获取cell
UITableViewCell *cell = sender;
//获取下标
NSInteger index = [self.tableView indexPathForCell:cell].row;
//获取对应的对象
XMPPJID *JID = self.contacts[index];
chatVC.friendJID = JID;
}
@end
聊天控制器
#import "ChatTableViewController.h"
#import "ChatCell.h"
@interface ChatTableViewController ()<XMPPStreamDelegate>
@property (nonatomic, strong) NSMutableArray *messageArray; //用来存储信息
@end
@implementation ChatTableViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"发送" style:(UIBarButtonItemStyleDone) target:selfaction:@selector(sendMessage)];
//添加代理
[[XMPPManager defaultXMPPManager].stream addDelegate:self delegateQueue:dispatch_get_main_queue()];
//创建数组
self.messageArray = [NSMutableArray array];
//获取本地聊天信息
[self reloadMessage];
}
//发送新的消息
- (void)sendMessage
{
//创建新的信息
XMPPMessage *message = [XMPPMessage messageWithType:@"chat" to:self.friendJID];
//添加信息体
[message addBody:@"比如说"];
[[XMPPManager defaultXMPPManager].stream sendElement:message];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.messageArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
ChatCell *cell = [tableView dequeueReusableCellWithIdentifier:@"chat" forIndexPath:indexPath];
//获取对应数组元素
XMPPMessage *message = self.messageArray[indexPath.row];
if ([message isKindOfClass:[XMPPMessage class]]) {
//将收到的信息放到左边,发送的放到右边
if ([message.from.user isEqualToString:self.friendJID.user]) {
cell.textLabel.text = message.body;
cell.detailTextLabel.text = @"";
}else {
cell.textLabel.text = @"";
cell.detailTextLabel.text = message.body;
}
}else {
//数组中的对象时XMPPMessageArchiving_Mesage_CoreDataObject类型
XMPPMessageArchiving_Message_CoreDataObject *mes = (XMPPMessageArchiving_Message_CoreDataObject *)message;
//Outgoing发送,用来判断消息是接受的 还是发送的
if (![mes isOutgoing]) {
cell.textLabel.text = mes.message.body;
cell.detailTextLabel.text = @"";
}else {
cell.textLabel.text = @"";
cell.detailTextLabel.text = mes.message.body;
}
}
return cell;
}
//读取本地信息
- (void)reloadMessage
{
//获取数据管理器
NSManagedObjectContext *managerContext = [XMPPManager defaultXMPPManager].managerContext;
//请求对象
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
//实体描述对象
//XMPPMessageArchiving_Message_CoreDataObject是持久化信息对应的实体类
NSEntityDescription *entity = [NSEntityDescription entityForName:@"XMPPMessageArchiving_Message_CoreDataObject"inManagedObjectContext:managerContext];
[fetchRequest setEntity:entity];
// 查询条件
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"bareJidStr == %@ AND streamBareJidStr == %@", self.friendJID.bare,[XMPPManagerdefaultXMPPManager].stream.myJID.bare];
[fetchRequest setPredicate:predicate];
// 排序
//按照时间排序
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"timestamp"
ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObjects:sortDescriptor, nil]];
NSError *error = nil;
//执行查询,获取符合条件的对象
NSArray *fetchedObjects = [managerContext executeFetchRequest:fetchRequest error:&error];
if (fetchedObjects == nil) {
NSLog(@"your content is null for search");
}
//将查询到的本地聊天信息存放到数组中
[self.messageArray addObjectsFromArray:fetchedObjects];
//刷新数据
[self.tableView reloadData];
}
//展示信息
- (void)showMessageWithMessage:(XMPPMessage *)message {
//将信息放入数组
[self.messageArray addObject:message];
//刷新数据
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.messageArray.count - 1 inSection:0];
[self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationRight];
//滑动tableView到对应的cell
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:(UITableViewScrollPositionBottom) animated:YES];
}
#pragma mark - XMPPSteamDelegate
//接收信息
- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message {
NSLog(@"%@",message.body);
//只获取当前好友的聊天信息
if ([message.from.user isEqualToString:self.friendJID.user]) {
//展示信息
[self showMessageWithMessage:message];
}
}
//发送信息
- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message {
[self showMessageWithMessage:message];
}
/*
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
@end
XMPP即时通讯(代码实现)的更多相关文章
- XMPP即时通讯资料记录
几天开始研究XMPP即时通讯的技术,来实现移动应用的计时聊天功能.记录下参考的博客地址,还挺详细的. http://blog.csdn.net/fhbystudy/article/details/16 ...
- XMPP即时通讯
XMPP:XMPP是基于XML的点对点通讯协议,The Extensible Messaging and Presence Protocol(可扩展通讯和表示协议). XMPP可用于服务类实时通讯,表 ...
- iOS开发之XMPP即时通讯简单实现
首先搭载服务器和数据库 搭载服务器我用的是openfire,数据库用的是mysql 这里推荐两个链接 配置mysql,用的是mysql workbench http://justsee.iteye.c ...
- XMPP即时通讯基础知识
XMPP参考 一.定义 XMPP 是一种很类似于http协议的一种数据传输协议,它的过程就如同“解包装--〉包装”的过程,用户只需要明白它接受的类型,并理解它返回的类型,就可以很好的利用xmpp来进行 ...
- xmpp即时通讯的笔记(摘抄)
xmpp的使用: 即时通讯 instant messaging(IM) : -->实时收发信息! 即时通讯相关软件: **QQ,MSN,GoogleTalk,AIM,Jabber(XMPP别名 ...
- iOS中 XMPP即时通讯实现的主要步骤
这里只是列出实现的只要步骤,不是全部代码. 首先导入XMPPFramework,及相关配置,完成后开始. 创建一个XMPPHelper 类来管理要进行的操作. XMPPHelper.h文件如下 ty ...
- XMPP即时通讯协议使用(前传)——协议详解
XMPP详解 XMPP(eXtensible Messaging and Presence Protocol,可扩展消息处理和现场协议)是一种在两个地点间传递小型结构化数据的协议.在此基础上,XMPP ...
- XMPP即时通讯协议使用(七)——利用Strophe实现WebIM及strophe.plugins插件使用
Strophe简介与Openfire配置 Strophe.js是为XMPP写的一个js类库.因为http协议本身不能实现持久连接,所以strophe利用BOSH模拟实现持久连接. 官方文档: http ...
- XMPP即时通讯协议使用(六)——开发Openfire聊天记录插件
转载地址:http://www.cnblogs.com/hoojo/archive/2013/03/29/openfire_plugin_chatlogs_plugin_.html 开发环境: Sys ...
随机推荐
- Swift对面向对象提供了良好的支持,下面介绍几个其独有的特性。
Swift对面向对象提供了良好的支持,下面介绍几个其独有的特性. 懒加载属性 Swift在语言层面上提供了类中懒加载属性的支持,使用lazy作为关键字: class Renderer { lazy v ...
- iOS 详解NSXMLParser方法解析XML数据方法
前一篇文章已经介绍了如何通过URL从网络上获取xml数据.下面介绍如何将获取到的数据进行解析. 下面先看看xml的数据格式吧! <?xml version="1.0" enc ...
- 转载 C#中敏捷开发规范
转载原地址 http://www.cnblogs.com/weixing/archive/2012/03/05/2380492.html 1.命名规则和风格 Naming Conventions an ...
- Oracle管道函数示例
Oracle的管道函数需要定义下面的三样: Record/Object Type:定义一个Record或Object类型的变量,这个变量用于表示返回结果集的一行数据,有点像C#中的DataRow类. ...
- IDF实验室-简单编程-特殊的日子 writeup
题目:http://ctf.idf.cn/index.php?g=game&m=article&a=index&id=50 题目提示要爆破,代表加密应该是不可逆的. 密文:4D ...
- cocos2d-x Animation
转自:http://codingnow.cn/cocos2d-x/810.html 这一篇来学习怎么使用cocos2d-x引擎播放帧动画,就是把一帧一帧的图片像电影那样显示出来.1. 首先来了解一下相 ...
- 关于java.util.Properties读取中文乱码的正确解决方案(不要再用native2ascii.exe了)
从Spring框架流行后,几乎根本不用自己写解析配置文件的代码了,但近日一个基础项目(实在是太基础,不能用硕大繁琐的Spring), 碰到了用java.util.Properties读取中文内容(UT ...
- SQLServer2005日志传送常见的几个问题
1.STANDBY 只读方式还原数据库:[备份数据库服务器]将完全备份文件复制到备份数据库服务器上,并以STANDBY的方式进行恢复 . SQL语句: RESTORE DATABASE [CNBlog ...
- 并行编程之多线程共享非volatile变量,会不会可能导致线程while死循环
背景 大家都知道线程之间共享变量要用volatilekeyword.可是,假设不用volatile来标识,会不会导致线程死循环?比方以下的伪代码: static int flag = -1; void ...
- [RxJS] Subject basic
A Subject is a type that implements both Observer and Observable types. As an Observer, it can subsc ...