版本记录
| 版本号 | 时间 |
|---|---|
| V1.0 | 2019.07.09 星期二 |
前言
实时通信在很多App上都有应用,包括我在上一家公司做的App也使用了实时通信技术,用于私聊和群聊,所以这里特意开一个专题一起学习一下实时通信技术。感兴趣的看下面几篇文章。
1. 实时通信技术研究(一) —— 基于Socket和TCP网络的实时聊天信息流的实现(一)
源码
1. Swift
首先看下工程组织结构

下面就是源码了
1. ChatRoomViewController.swift
import UIKit
class ChatRoomViewController: UIViewController {
let tableView = UITableView()
let messageInputBar = MessageInputView()
let chatRoom = ChatRoom()
var messages: [Message] = []
var username = ""
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
chatRoom.delegate = self
chatRoom.setupNetworkCommunication()
chatRoom.joinChat(username: username)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
chatRoom.stopChatSession()
}
}
//MARK - Message Input Bar
extension ChatRoomViewController: MessageInputDelegate {
func sendWasTapped(message: String) {
chatRoom.send(message: message)
}
}
extension ChatRoomViewController: ChatRoomDelegate {
func received(message: Message) {
insertNewMessageCell(message)
}
}
2. ChatRoomViewController+Table.swift
import UIKit
extension ChatRoomViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = MessageTableViewCell(style: .default, reuseIdentifier: "MessageCell")
cell.selectionStyle = .none
let message = messages[indexPath.row]
cell.apply(message: message)
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messages.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let height = MessageTableViewCell.height(for: messages[indexPath.row])
return height
}
func insertNewMessageCell(_ message: Message) {
messages.append(message)
let indexPath = IndexPath(row: messages.count - 1, section: 0)
tableView.beginUpdates()
tableView.insertRows(at: [indexPath], with: .bottom)
tableView.endUpdates()
tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}
3. JoinChatViewController.swift
import UIKit
class JoinChatViewController: UIViewController {
let logoImageView = UIImageView()
let shadowView = UIView()
let nameTextField = TextField()
}
extension JoinChatViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
let chatRoomVC = ChatRoomViewController()
if let username = nameTextField.text {
chatRoomVC.username = username
}
navigationController?.pushViewController(chatRoomVC, animated: true)
return true
}
}
class TextField: UITextField {
let padding = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 8)
override func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.inset(by: padding)
}
override func placeholderRect(forBounds bounds: CGRect) -> CGRect {
return bounds.inset(by: padding)
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return bounds.inset(by: padding)
}
}
4. Label.swift
import UIKit
class Label: UILabel {
override func drawText(in rect: CGRect) {
let insets = UIEdgeInsets.init(top: 8, left: 16, bottom: 8, right: 16)
super.drawText(in: rect.inset(by: insets))
}
}
5. Layouts.swift
import UIKit
extension ChatRoomViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChange(notification:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
loadViews()
}
@objc func keyboardWillChange(notification: NSNotification) {
if let userInfo = notification.userInfo {
let endFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)!.cgRectValue
let messageBarHeight = messageInputBar.bounds.size.height
let point = CGPoint(x: messageInputBar.center.x, y: endFrame.origin.y - messageBarHeight/2.0)
let inset = UIEdgeInsets(top: 0, left: 0, bottom: endFrame.size.height, right: 0)
UIView.animate(withDuration: 0.25) {
self.messageInputBar.center = point
self.tableView.contentInset = inset
}
}
}
func loadViews() {
navigationItem.title = "Let's Chat!"
navigationItem.backBarButtonItem?.title = "Run!"
view.backgroundColor = UIColor(red: 24 / 255, green: 180 / 255, blue: 128 / 255, alpha: 1.0)
tableView.dataSource = self
tableView.delegate = self
tableView.separatorStyle = .none
view.addSubview(tableView)
view.addSubview(messageInputBar)
messageInputBar.delegate = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let messageBarHeight:CGFloat = 60.0
let size = view.bounds.size
tableView.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height - messageBarHeight - view.safeAreaInsets.bottom)
messageInputBar.frame = CGRect(x: 0, y: size.height - messageBarHeight - view.safeAreaInsets.bottom, width: size.width, height: messageBarHeight)
}
}
extension JoinChatViewController {
override func viewDidLoad() {
super.viewDidLoad()
loadViews()
view.addSubview(shadowView)
view.addSubview(logoImageView)
view.addSubview(nameTextField)
}
func loadViews() {
view.backgroundColor = UIColor(red: 24/255, green: 180/255, blue: 128/255, alpha: 1.0)
navigationItem.title = "Doge Chat!"
logoImageView.image = UIImage(named: "doge")
logoImageView.layer.cornerRadius = 4
logoImageView.clipsToBounds = true
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowRadius = 5
shadowView.layer.shadowOffset = CGSize(width: 0.0, height: 5.0)
shadowView.layer.shadowOpacity = 0.5
shadowView.backgroundColor = UIColor(red: 24 / 255, green: 180 / 255, blue: 128 / 255, alpha: 1.0)
nameTextField.placeholder = "What's your username?"
nameTextField.backgroundColor = .white
nameTextField.layer.cornerRadius = 4
nameTextField.delegate = self
let backItem = UIBarButtonItem()
backItem.title = "Run!"
navigationItem.backBarButtonItem = backItem
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
logoImageView.bounds = CGRect(x: 0, y: 0, width: 150, height: 150)
logoImageView.center = CGPoint(x: view.bounds.size.width / 2.0, y: logoImageView.bounds.size.height / 2.0 + view.bounds.size.height/4)
shadowView.frame = logoImageView.frame
nameTextField.bounds = CGRect(x: 0, y: 0, width: view.bounds.size.width - 40, height: 44)
nameTextField.center = CGPoint(x: view.bounds.size.width / 2.0, y: logoImageView.center.y + logoImageView.bounds.size.height / 2.0 + 20 + 22)
}
}
extension MessageTableViewCell {
override func layoutSubviews() {
super.layoutSubviews()
if isJoinMessage() {
layoutForJoinMessage()
} else {
messageLabel.font = UIFont(name: "Helvetica", size: 17)
messageLabel.textColor = .white
let size = messageLabel.sizeThatFits(CGSize(width: 2 * (bounds.size.width / 3), height: .greatestFiniteMagnitude))
messageLabel.frame = CGRect(x: 0, y: 0, width: size.width + 32, height: size.height + 16)
if messageSender == .ourself {
nameLabel.isHidden = true
messageLabel.center = CGPoint(x: bounds.size.width - messageLabel.bounds.size.width/2.0 - 16, y: bounds.size.height/2.0)
messageLabel.backgroundColor = UIColor(red: 24 / 255, green: 180 / 255, blue: 128 / 255, alpha: 1.0)
} else {
nameLabel.isHidden = false
nameLabel.sizeToFit()
nameLabel.center = CGPoint(x: nameLabel.bounds.size.width / 2.0 + 16 + 4, y: nameLabel.bounds.size.height/2.0 + 4)
messageLabel.center = CGPoint(x: messageLabel.bounds.size.width / 2.0 + 16, y: messageLabel.bounds.size.height/2.0 + nameLabel.bounds.size.height + 8)
messageLabel.backgroundColor = .lightGray
}
}
messageLabel.layer.cornerRadius = min(messageLabel.bounds.size.height / 2.0, 20)
}
func layoutForJoinMessage() {
messageLabel.font = UIFont.systemFont(ofSize: 10)
messageLabel.textColor = .lightGray
messageLabel.backgroundColor = UIColor(red: 247 / 255, green: 247 / 255, blue: 247 / 255, alpha: 1.0)
let size = messageLabel.sizeThatFits(CGSize(width: 2 * (bounds.size.width / 3), height: .greatestFiniteMagnitude))
messageLabel.frame = CGRect(x: 0, y: 0, width: size.width + 32, height: size.height + 16)
messageLabel.center = CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2.0)
}
func isJoinMessage() -> Bool {
if let words = messageLabel.text?.components(separatedBy: " ") {
if words.count >= 2 && words[words.count - 2] == "has" && words[words.count - 1] == "joined" {
return true
}
}
return false
}
}
6. Message.swift
import Foundation
struct Message {
let message: String
let senderUsername: String
let messageSender: MessageSender
init(message: String, messageSender: MessageSender, username: String) {
self.message = message.withoutWhitespace()
self.messageSender = messageSender
self.senderUsername = username
}
}
7. MessageInputView.swift
import UIKit
protocol MessageInputDelegate {
func sendWasTapped(message: String)
}
class MessageInputView: UIView {
var delegate: MessageInputDelegate?
let textView = UITextView()
let sendButton = UIButton()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(red: 247 / 255, green: 247 / 255, blue: 247 / 255, alpha: 1.0)
textView.layer.cornerRadius = 4
textView.layer.borderColor = UIColor(red: 200 / 255, green: 200 / 255, blue: 200 / 255, alpha: 0.6).cgColor
textView.layer.borderWidth = 1
sendButton.backgroundColor = UIColor(red: 8 / 255, green: 183 / 255, blue: 231 / 255, alpha: 1.0)
sendButton.layer.cornerRadius = 4
sendButton.setTitle("Send", for: .normal)
sendButton.isEnabled = true
sendButton.addTarget(self, action: #selector(MessageInputView.sendTapped), for: .touchUpInside)
addSubview(textView)
addSubview(sendButton)
}
@objc func sendTapped() {
if let delegate = delegate, let message = textView.text {
delegate.sendWasTapped(message: message)
textView.text = ""
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
let size = bounds.size
textView.bounds = CGRect(x: 0, y: 0, width: size.width - 32 - 8 - 60, height: 40)
sendButton.bounds = CGRect(x: 0, y: 0, width: 60, height: 44)
textView.center = CGPoint(x: textView.bounds.size.width/2.0 + 16, y: bounds.size.height / 2.0)
sendButton.center = CGPoint(x: bounds.size.width - 30 - 16, y: bounds.size.height / 2.0)
}
}
extension MessageInputView: UITextViewDelegate {
func textViewShouldEndEditing(_ textView: UITextView) -> Bool {
return true
}
}
8. MessageTableViewCell.swift
import UIKit
enum MessageSender {
case ourself
case someoneElse
}
class MessageTableViewCell: UITableViewCell {
var messageSender: MessageSender = .ourself
let messageLabel = Label()
let nameLabel = UILabel()
func apply(message: Message) {
nameLabel.text = message.senderUsername
messageLabel.text = message.message
messageSender = message.messageSender
setNeedsLayout()
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
messageLabel.clipsToBounds = true
messageLabel.textColor = .white
messageLabel.numberOfLines = 0
nameLabel.textColor = .lightGray
nameLabel.font = UIFont(name: "Helvetica", size: 10)
clipsToBounds = true
addSubview(messageLabel)
addSubview(nameLabel)
}
class func height(for message: Message) -> CGFloat {
let maxSize = CGSize(width: 2*(UIScreen.main.bounds.size.width/3), height: CGFloat.greatestFiniteMagnitude)
let nameHeight = message.messageSender == .ourself ? 0 : (height(forText: message.senderUsername, fontSize: 10, maxSize: maxSize) + 4 )
let messageHeight = height(forText: message.message, fontSize: 17, maxSize: maxSize)
return nameHeight + messageHeight + 32 + 16
}
private class func height(forText text: String, fontSize: CGFloat, maxSize: CGSize) -> CGFloat {
let font = UIFont(name: "Helvetica", size: fontSize)!
let attrString = NSAttributedString(string: text, attributes:[NSAttributedString.Key.font: font,
NSAttributedString.Key.foregroundColor: UIColor.white])
let textHeight = attrString.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, context: nil).size.height
return textHeight
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
9. StringExtension.swift
import Foundation
extension String {
func withoutWhitespace() -> String {
return self.replacingOccurrences(of: "\n", with: "")
.replacingOccurrences(of: "\r", with: "")
.replacingOccurrences(of: "\0", with: "")
}
}
10. AppDelegate.swift
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
//UIApplication.shared.statusBarStyle = .default
window = UIWindow(frame: UIScreen.main.bounds)
window!.rootViewController = UINavigationController(rootViewController: JoinChatViewController())
window!.makeKeyAndVisible()
return true
}
}
11. ChatRoom.swift
import UIKit
protocol ChatRoomDelegate: class {
func received(message: Message)
}
class ChatRoom: NSObject {
//1
var inputStream: InputStream!
var outputStream: OutputStream!
weak var delegate: ChatRoomDelegate?
//2
var username = ""
//3
let maxReadLength = 4096
func setupNetworkCommunication() {
// 1
var readStream: Unmanaged<CFReadStream>?
var writeStream: Unmanaged<CFWriteStream>?
// 2
CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault,
"localhost" as CFString,
80,
&readStream,
&writeStream)
inputStream = readStream!.takeRetainedValue()
outputStream = writeStream!.takeRetainedValue()
inputStream.delegate = self
inputStream.schedule(in: .current, forMode: .common)
outputStream.schedule(in: .current, forMode: .common)
inputStream.open()
outputStream.open()
}
func joinChat(username: String) {
//1
let data = "iam:\(username)".data(using: .utf8)!
//2
self.username = username
//3
_ = data.withUnsafeBytes {
guard let pointer = $0.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
print("Error joining chat")
return
}
//4
outputStream.write(pointer, maxLength: data.count)
}
}
func send(message: String) {
let data = "msg:\(message)".data(using: .utf8)!
_ = data.withUnsafeBytes {
guard let pointer = $0.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
print("Error joining chat")
return
}
outputStream.write(pointer, maxLength: data.count)
}
}
func stopChatSession() {
inputStream.close()
outputStream.close()
}
}
extension ChatRoom: StreamDelegate {
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
switch eventCode {
case .hasBytesAvailable:
print("new message received")
readAvailableBytes(stream: aStream as! InputStream)
case .endEncountered:
print("new message received")
stopChatSession()
case .errorOccurred:
print("error occurred")
case .hasSpaceAvailable:
print("has space available")
default:
print("some other event...")
}
}
private func readAvailableBytes(stream: InputStream) {
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: maxReadLength)
while stream.hasBytesAvailable {
let numberOfBytesRead = inputStream.read(buffer, maxLength: maxReadLength)
if numberOfBytesRead < 0, let error = stream.streamError {
print(error)
break
}
// Construct the message object
if let message = processedMessageString(buffer: buffer, length: numberOfBytesRead) {
// Notify interested parties
delegate?.received(message: message)
}
}
}
private func processedMessageString(buffer: UnsafeMutablePointer<UInt8>,
length: Int) -> Message? {
//1
guard
let stringArray = String(
bytesNoCopy: buffer,
length: length,
encoding: .utf8,
freeWhenDone: true)?.components(separatedBy: ":"),
let name = stringArray.first,
let message = stringArray.last
else {
return nil
}
//2
let messageSender: MessageSender = (name == self.username) ? .ourself : .someoneElse
//3
return Message(message: message, messageSender: messageSender, username: name)
}
}
后记
本篇主要讲述了基于Socket和TCP网络的实时聊天信息流的实现,感兴趣的给个赞或者关注~~~
