XMPP: Openfire + Spark 实现即时通讯

资源下载: https://pan.baidu.com/s/1ge3cajh 密码: 6ytk

安装XMPP集成环境

  • 将下载好的xampp-osx根据提示进行安装并打开。
    F4648516-D77F-4DF4-9031-F7BED0F8C13F.png
  • 点击Manage Servers,启动所有服务,如果MySQL Database无法启动,可打开终端执行如下命令: sudo /Applications/XAMPP/xamppfiles/bin/mysql.server start
    1B424F11-55A2-4F2C-B188-89059822D4CD.png
  • 点击Go To Application,配置数据库
    A3FE18F4-5CE0-4E8A-8BB6-A7AF97F216D2.png
  • 数据库新建成功后,需要导入数据库格式。可使用下载好的openfire_mysql.sq文件,也可用另外途径:前往/usr/local,找到文件夹openfire,但此时的openfire文件夹无法打开。选中后右键查看简介,将下面属性修改为读与写。这时可以打开openfire文件夹,查找路径/usr/local/openfire/resources/database下,找到openfire_mysql.sq文件,将其拷贝到桌面,然后打开数据库配置页面。
    C51E4113-4627-4076-A8AB-0C5A4A092378.png

Openfire服务器搭建

可以使用它轻易的构建高效率的技师通讯服务器,Openfire的安装和使用也是非常的简单,并利用Web进行后台管理。单台服务器可支持上万并发用户。由于是采用开放的XMPP协议,我们可以使用各种支持XMPP协议的IM客户端软件登陆服务(在这里我就使用了Spark)。

  • 下载Openfire,并根据提示进行安装。
  • 安装完成后,打开系统设置。
    2337F0EA-21E2-4864-8322-976E3D084138.png
    3C705089-8D87-44E8-BDCA-AB87D0F18484.png
  • 点击Open Admin Console,到网站中配置Openfire。
    7FDF4D15-E4E8-4AA9-AC5C-2D448EACB2AB.png
    6477BDE5-63A9-483A-A333-AFED5A64C13C.png
    D31A34D9-945C-40C6-BF02-0AE5ACCBC123.png
    833FB7BA-32F5-403C-9F78-B21389322536.png
    0602DDA6-5F09-4254-82CF-9E8E3404D753.png
    7B7802D1-FFB2-4D77-A5ED-6462D71E1577.png
  • 到这一步,基本上openfire就已经配置完毕了。

安装Spark客户端

  • 按照提示安装客户端,完成后打开


    E24D0A1D-BC30-4901-8845-D84B40F25EBA.png
  • 高级配置
    5D1E4204-7E6A-47EB-9219-65DFDA664C7A.png
  • 然后以管理员身份登录,登录成功后刷新openfire管理界面,可以看到头像变亮了,说明环境配置成功。接下来就可以撸码了。

代码实现

  • 主要实现这些功能:
    • 注册、登录、退出登录;
    • 添加好友、好友请求
    • 发送消息、接收消息
    • 消息记录
  • 新建一个管理类XMPPManager,创建以下对象:
import UIKit
import XMPPFramework

// 枚举:连接服务器的目的
enum ConnectServerPurpose : Int{
  case connectServerToLogin     // 登录
  case connectServerToRegister  // 注册
}

class XMPPManager: NSObject {
  
  deinit {
      NotificationCenter.default.removeObserver(self)
  }
  
  fileprivate var password : String?
  fileprivate var userName : String?
  fileprivate var connectServerPurpose : ConnectServerPurpose = .connectServerToLogin
  
  // 通信通道对象
  var xmppStream : XMPPStream?
  // JID
  var xmppJID : XMPPJID?
  // 好友花名册管理对象
  var xmppRoster : XMPPRoster?
  // 花名册数据存储对象
  var xmppRosterCoreDataStorage : XMPPRosterCoreDataStorage?
  // 信息归档对象
  var xmppMessageArchiving : XMPPMessageArchiving?
  // 信息存储对象
  var xmppMessageArchivingCoreDataStorage : XMPPMessageArchivingCoreDataStorage?
  
  var friendsListResultController : NSFetchedResultsController<NSFetchRequestResult>?
  var chatRecordsResultController : NSFetchedResultsController<NSFetchRequestResult>?
  // 好友请求
  var xmppPresence : XMPPPresence?
  
  // 单例
  static let manager : XMPPManager = {
      let manager = XMPPManager.init()
      // 创建通信通道对象
      manager.xmppStream = XMPPStream.init()
      // 设置服务器IP地址
      manager.xmppStream?.hostName = kHostName
      // 设置服务器端口
      manager.xmppStream?.hostPort = kHostPort
      // 添加代理
      manager.xmppStream?.addDelegate(manager, delegateQueue: DispatchQueue.main)
      
      // 花名册数据存储对象
      manager.xmppRosterCoreDataStorage = XMPPRosterCoreDataStorage.sharedInstance()
      manager.xmppRoster = XMPPRoster.init(rosterStorage: manager.xmppRosterCoreDataStorage)
      manager.xmppRoster?.activate(manager.xmppStream)
      manager.xmppRoster?.addDelegate(manager, delegateQueue: DispatchQueue.main)
      
      // 信息存储对象
      manager.xmppMessageArchivingCoreDataStorage = XMPPMessageArchivingCoreDataStorage.sharedInstance()
      manager.xmppMessageArchiving = XMPPMessageArchiving.init(messageArchivingStorage: manager.xmppMessageArchivingCoreDataStorage, dispatchQueue: DispatchQueue.main)
      // 激活通信通道对象
      manager.xmppMessageArchiving?.activate(manager.xmppStream)

      return manager
  }()
  
  // 连接服务器
  func connectToServer(withUserName userName: String) {
      // 创建XMPPJID对象
      self.xmppJID = XMPPJID.init(user: userName, domain: kDomin, resource: kResource)
      // 设置通信通道对象的JID
      self.xmppStream?.myJID = self.xmppJID
      // 发送请求
      if self.xmppStream?.isConnected() == true || self.xmppStream?.isConnecting() == true {
          // 先退出登录状态
         self.exitLogin()
      }
      // 连接服务器
      do {
          try self.xmppStream?.connect(withTimeout: -1)
      }catch let error as NSError{
          print("连接服务器失败: " + error.description)
      }
  }
}

// MARK:- 登录方法
extension XMPPManager {
  // 登录方法
  func login(widthUserName userName: String?, andPassword password: String?) {
      guard let userName = userName, let password = password else {
          print("用户名和密码不能为空")
          return
      }
      // 记录连接服务器的目的是登录
      connectServerPurpose = .connectServerToLogin
      // 记录登录信息
      self.password = password
      self.userName = userName
      connectToServer(withUserName: userName)
  }
  
  // 退出登录
  func exitLogin() {
      // 先发送下线状态
      let presence = XMPPPresence.init(type: "unavailable")
      self.xmppStream?.send(presence)
      // 断开连接
      self.xmppStream?.disconnect()
  }
}

// MARK:- 注册方法
extension XMPPManager {
  func register(widthUserName userName: String?, andPassword password: String?) {
      guard let userName = userName, let password = password else {
          print("用户名和密码不能为空")
          return
      }
      // 记录连接服务器的目的是注册
      connectServerPurpose = .connectServerToRegister
      // 记录密码
      self.password = password
      connectToServer(withUserName: userName)
  }
}

// MARK:- XMPPStreamDelegate
extension XMPPManager : XMPPStreamDelegate {
  // 连接成功
  func xmppStreamDidConnect(_ sender: XMPPStream!) {
      print("连接成功")
      switch connectServerPurpose {
          case .connectServerToLogin:
              // 验证密码
              do {
                  try self.xmppStream?.authenticate(withPassword: self.password)
              } catch let error as NSError {
                  print("登录时验证密码失败: " + error.description)
              }
              break
          case .connectServerToRegister:
              do {
                  try self.xmppStream?.register(withPassword: self.password)
              } catch let error as NSError {
                  print("注册时验证密码失败: " + error.description)
              }
              break
      }
  }
  
  // 连接超时
  func xmppStreamConnectDidTimeout(_ sender: XMPPStream!) {
      print("连接超时")
  }
  
  // 登录成功
  func xmppStreamDidAuthenticate(_ sender: XMPPStream!) {
      print("登录成功  ", #line, #function)
      // 发送上线状态
      let presence = XMPPPresence.init(type: "available")
      XMPPManager.manager.xmppStream?.send(presence)
  }
  
  // 已经断开连接
  func xmppStreamDidDisconnect(_ sender: XMPPStream!, withError error: Error!) {
      print("++++++++++")
  }
}

// MARK:- 好友
extension XMPPManager {
  // 添加好友
  func addNewFriends(userName: String?) {
      guard let name = userName else { return }
      let friendJID = XMPPJID.init(string: "\(name)@\(kDomin)")
      self.xmppRoster?.subscribePresence(toUser: friendJID)
  }
  
  // 获取好友列表
  func getFriendsList() -> Array<Any>? {
      guard let context = self.xmppRosterCoreDataStorage?.mainThreadManagedObjectContext else {
          return nil
      }
      let request = NSFetchRequest<NSFetchRequestResult>.init(entityName: "XMPPUserCoreDataStorageObject")
      let userInfo = String.init(format: "%@@%@", self.userName ?? "", kDomin)
      // 谓词
      let predicate = NSPredicate.init(format: "streamBareJidStr = %@", userInfo)
      request.predicate = predicate
      
      // 排序
      let sort = NSSortDescriptor.init(key: "displayName", ascending: true)
      request.sortDescriptors = [sort]
      
      friendsListResultController = NSFetchedResultsController.init(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
      friendsListResultController?.delegate = self
      do {
          try friendsListResultController?.performFetch()
      } catch let error as NSError {
          print("获取好友聊天记录失败: " + error.description)
      }
      return friendsListResultController?.fetchedObjects
  }
  
  // 获取与某个好友的聊天记录
  func getChatRecords(withFriendName friendName: String) ->Array<Any>?{
      guard let context = self.xmppMessageArchivingCoreDataStorage?.mainThreadManagedObjectContext else { return nil}
      let request = NSFetchRequest<NSFetchRequestResult>.init(entityName: "XMPPMessageArchiving_Message_CoreDataObject")
      let userInfo = String.init(format: "%@@%@", self.userName ?? "", kDomin)
      let friendsInfo = String.init(format: "%@@%@", friendName, kDomin)
      // 谓词
      let predicate = NSPredicate.init(format: "streamBareJidStr=%@ AND bareJidStr=%@", userInfo, friendsInfo)
      request.predicate = predicate
      
      // 排序
      let sort = NSSortDescriptor.init(key: "timestamp", ascending: true)
      request.sortDescriptors = [sort]
      
      chatRecordsResultController = NSFetchedResultsController.init(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
      chatRecordsResultController?.delegate = self
      do {
          try chatRecordsResultController?.performFetch()
      } catch let error as NSError {
          print("获取好友聊天记录失败: " + error.description)
      }
      guard let fetchedObjects = chatRecordsResultController?.fetchedObjects else { return nil }
      var messageArray = Array<Any>()
      for obj in fetchedObjects {
          if let _ = (obj as? XMPPMessageArchiving_Message_CoreDataObject)?.body {
              messageArray.append(obj)
          }
      }
      return messageArray
  }
}

// MARK:- XMPPRosterDelegate
extension XMPPManager : XMPPRosterDelegate {
  func xmppRoster(_ sender: XMPPRoster!,
                  didReceiveRosterItem item: DDXMLElement!) {
      print(#line, #function)
  }
  func xmppRosterDidBeginPopulating(_ sender: XMPPRoster!,
                                    withVersion version: String!) {
      print(#line, #function)
  }
  func xmppRosterDidEndPopulating(_ sender: XMPPRoster!) {
      print(#line, #function)
  }
  
  // 收到好友请求
  func xmppRoster(_ sender: XMPPRoster!,
                  didReceivePresenceSubscriptionRequest presence: XMPPPresence!) {
      self.xmppPresence = presence
      // 弹框
      let alert = UIAlertView.init(title: "好友请求", message: "\(presence.from().user)请求添加你为好友", delegate: self, cancelButtonTitle:
          "拒绝", otherButtonTitles: "同意")
      alert.show()
      
  }
}

// MARK:- 好友请求弹框
extension XMPPManager : UIAlertViewDelegate {
  func alertView(_ alertView: UIAlertView,
                 clickedButtonAt buttonIndex: Int) {
      switch buttonIndex {
      case 0:
          // 拒绝
          let jid = XMPPJID.init(string: self.xmppPresence?.from().user)
          self.xmppRoster?.rejectPresenceSubscriptionRequest(from: jid)
          break
      case 1:
          // 同意
          let jid = XMPPJID.init(string: self.xmppPresence?.from().user)
          self.xmppRoster?.acceptPresenceSubscriptionRequest(from: jid, andAddToRoster: true)
          break
      default: break
      }
  }
}

// MARK:- NSFetchedResultsControllerDelegate
extension XMPPManager : NSFetchedResultsControllerDelegate {
  func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
                  didChange anObject: Any,
                  at indexPath: IndexPath?,
                  for type: NSFetchedResultsChangeType,
                  newIndexPath: IndexPath?) {
      if anObject is XMPPUserCoreDataStorageObject {
          // 好友列表数据库发生变化
          NotificationCenter.default.post(name: NSNotification.Name(rawValue: notificationName_friendsListDidChange), object: nil)
      }else if anObject is XMPPMessageArchiving_Message_CoreDataObject{
          // 聊天记录数据库发生变化
          NotificationCenter.default.post(name: NSNotification.Name(rawValue: notificationName_chatRecordsDidChange), object: nil)
      }
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容