在iOS上聊天:使用套接字


图片由rawpixel.com创建

在本出版物中,我们将深入到TCP层,以开发聊天应用程序为例学习Core Foundation的套接字和工具。

预计阅读时间:25分钟。

为什么要使用插座?


您可能想知道:“为什么我要比URLSession一级 ?” 如果您足够聪明,不问这个问题,请直接转到下一部分。

不那么聪明的答案
好问题! 事实是,URLSession的使用基于HTTP协议 ,也就是说,通信以request-response的方式发生,大致如下:

  • 从服务器请求JSON格式的一些数据
  • 获取此数据,过程,显示等

但是,如果我们需要服务器主动将数据传输到您的应用程序怎么办? HTTP无法使用。

当然,我们可以不断拉动服务器,看看是否有适合我们的数据(又名polling )。 或者,我们可以变得更复杂,并使用长轮询 。 但是在这种情况下,所有这些拐杖都不适合。

毕竟,如果它适合我​​们的任务,那么为什么不让自己局限于请求-响应范式呢?

在本指南中,您将学习如何深入到较低的抽象级别并在聊天应用程序中直接使用SOCKETS

我们的应用程序将使用在聊天会话期间保持打开状态的流,而不是检查服务器是否有新消息。

开始使用


下载源资料 。 有一个模拟客户端应用程序和一个用Go编写的简单服务器。

您不必用Go语言编写,但是您将需要运行服务器应用程序,以便客户端应用程序可以连接到它。

启动服务器应用程序


原始资料既有已编译的应用程序又有原始资料。 如果您有健康的偏执狂,并且不信任别人的编译代码,则可以自己编译源代码。

如果您很勇敢,请打开Terminal ,然后转到包含已下载材料的目录,然后运行以下命令:

sudo ./server

出现提示时,输入密码。 之后,您应该会看到一条消息

聆听127.0.0.1:80。

注意:服务器应用程序以特权模式(“ sudo”命令)启动,因为它侦听端口80。所有编号小于1024的端口都需要特殊访问。

您的聊天服务器已准备就绪! 您可以转到下一部分。

如果您想自己编译服务器源代码,
那么在这种情况下,您需要使用Homebrew安装Go

如果没有Homebrew,则需要先安装它。 打开终端,并在其中粘贴以下行:

/usr/bin/ruby -e \
"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"


然后使用此命令安装Go:

brew install go

最后,转到包含已下载源材料的目录,并编译服务器应用程序的源代码:

go build server.go

最后,您可以使用本节开头的命令来启动服务器。

我们看客户所拥有的


现在打开DogeChat项目,对其进行编译,然后查看其中的内容。



如您所见,DogeChat现在允许您输入用户名并转到聊天部分。

该项目的开发人员似乎不知道如何进行聊天。 因此,我们所拥有的只是基本的UI和导航。 我们将编写一个网络层。 万岁!

创建聊天室


要直接进行开发,请转到ChatRoomViewController.swift 。 这是一个视图控制器,可以接收用户输入的文本并在表格视图中显示接收到的消息。

由于我们有一个ChatRoomViewController ,因此开发一个可以完成所有粗略工作的ChatRoom类是有意义的。

让我们考虑一下新类将提供的内容:

  • 打开与服务器应用程序的连接;
  • 将用户使用其指定的名称连接到聊天;
  • 发送和接收消息;
  • 最后关闭连接。

现在我们知道了从这个类中想要的东西,按Command-N ,选择Swift File并将其命名为ChatRoom

创建I / O流


用以下内容替换ChatRoom.swift的内容:

 import UIKit class ChatRoom: NSObject { //1 var inputStream: InputStream! var outputStream: OutputStream! //2 var username = "" //3 let maxReadLength = 4096 } 

在这里,我们定义ChatRoom类并声明所需的属性。

  1. 首先我们定义输入/输出流。 将它们成对使用将使我们能够在应用程序和聊天服务器之间创建套接字连接。 当然,我们将使用输出流发送消息,并使用输入流接收消息。
  2. 接下来,我们定义用户名。
  3. 最后,我们定义变量maxReadLength,该变量限制单个消息的最大长度。

现在转到ChatRoomViewController.swift文件并将此行添加到其属性列表中:

 let chatRoom = ChatRoom() 

现在我们已经创建了类的基本结构,是时候执行第一个计划的任务了:打开应用程序和服务器之间的连接。

打开连接


我们返回到ChatRoom.swift并为属性定义添加此方法:

 func setupNetworkCommunication() { // 1 var readStream: Unmanaged<CFReadStream>? var writeStream: Unmanaged<CFWriteStream>? // 2 CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, "localhost" as CFString, 80, &readStream, &writeStream) } 

这是我们在这里所做的:

  1. 首先,我们为套接字流定义两个变量,而无需使用自动内存管理
  2. 然后,我们使用这些相同的变量直接创建绑定到主机和端口号的流。

该函数有四个参数。 第一种是初始化线程时将使用的内存分配器的类型。 您应该使用kCFAllocatorDefault ,但如果要更改线程的行为,还有其他可能的选项。

译者注
CFStreamCreatePairWithSocketToHost函数的文档说:使用NULLkCFAllocatorDefault 。 并且kCFAllocatorDefault的描述说它是NULL的同义词。 圆是封闭的!

然后我们设置主机名。 在本例中,我们正在连接到本地服务器。 如果您的服务器位于其他位置,则可以设置其IP地址。

然后是服务器正在侦听的端口号。

最后,我们将指针传递给I / O流,以便函数可以初始化它们并将它们连接到它创建的流。

现在已经有了初始化的流,我们可以通过在setupNetworkCommunication()方法的末尾添加以下行来保存到它们的链接:

 inputStream = readStream!.takeRetainedValue() outputStream = writeStream!.takeRetainedValue() 

通过将takeRetainedValue()应用于非托管对象,我们可以维护对该对象的引用,同时避免将来发生内存泄漏。 现在,我们可以在任何需要的地方使用线程。

现在,我们需要将这些线程添加到运行循环中,以便我们的应用程序正确处理网络事件。 为此,请将这两行添加到setupNetworkCommunication()的末尾:

 inputStream.schedule(in: .current, forMode: .common) outputStream.schedule(in: .current, forMode: .common) 

终于该出发了! 首先,请在setupNetworkCommunication()方法的最后添加此代码

 inputStream.open() outputStream.open() 

现在,我们在客户端和服务器应用程序之间建立了开放连接。

我们可以编译并运行我们的应用程序,但是您不会看到任何更改,因为尽管我们不使用客户端-服务器连接进行任何操作。

连接聊天


现在我们已经与服务器建立了连接,是时候开始对其做一些事情了! 在聊天的情况下,您需要先自我介绍,然后才能向对话者发送消息。

这导致我们得出一个重要的结论:由于我们有两种类型的消息,因此需要以某种方式区分它们。

聊天协议


使用TCP层的优点之一是我们可以定义自己的“协议”进行通信。

如果我们使用HTTP,则需要使用这些不同的单词GETPUTPATCH 。 我们将需要形成URL并使用正确的标题和所有其他内容。

我们只有两种类型的消息。 我们将发送

iam:Luke

进入聊天并自我介绍。

我们将发送

msg:Hey, how goes it, man?

向所有受访者发送聊天消息。

它非常简单,但是绝对没有原则,因此不要在关键项目中使用此方法。

现在我们知道了服务器的期望,我们可以在ChatRoom类中编写一个方法,该方法将允许用户连接到聊天室。 唯一的参数是用户的昵称。

ChatRoom.swift中添加以下方法:

 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) } } 

  1. 首先,我们使用自己的“协议”来形成信息
  2. 保存名称以备将来参考。
  3. withUnsafeBytes(_ :)提供了一种方便的方法来处理闭包内部的不安全指针。
  4. 最后,我们将消息发送到输出流。 这看起来可能比您预期的要复杂,但是写(_:maxLength :)使用上一步中创建的不安全指针。

现在我们的方法已经准备好,打开ChatRoomViewController.swift并在viewWillAppear(_ :)的末尾添加对该方法的调用。

 chatRoom.joinChat(username: username) 

现在编译并运行该应用程序。 输入您的昵称,然后点击返回以查看...



... 再次没有改变!

等等,没关系! 进入终端窗口。 在这里,您将看到消息Vasya已加入,或者如果您的名字不是Vasya,则类似的消息。

很好,但最好在手机屏幕上显示成功的连接。

回应传入的消息


服务器将客户端加入消息发送给聊天中的每个人,包括您。 幸运的是,我们的应用程序已经拥有了一切,可以在ChatRoomViewController的消息表中以单元格的形式显示任何传入的消息。

您要做的就是使用inputStream来“捕获”这些消息,将它们转换为Message类的实例,然后将它们传递给表以进行显示。

为了能够响应传入的消息,您需要ChatRoom符合 StreamDelegate协议。

为此,请在ChatRoom.swift文件的底部添加此扩展名:

 extension ChatRoom: StreamDelegate { } 

现在声明谁将成为inputStream的委托。

将此行添加到setupNetworkCommunication()方法中,紧接调度调用之前(在:forMode:中):

 inputStream.delegate = self 

现在将流(_:handle :)方法实现添加到扩展中:

 func stream(_ aStream: Stream, handle eventCode: Stream.Event) { switch eventCode { case .hasBytesAvailable: print("new message received") case .endEncountered: print("The end of the stream has been reached.") case .errorOccurred: print("error occurred") case .hasSpaceAvailable: print("has space available") default: print("some other event...") } } 

我们处理传入的消息


因此,我们准备开始处理传入的消息。 我们感兴趣的事件是.hasBytesAvailable ,指示传入的消息已到达。

我们将编写一种处理这些消息的方法。 在新添加的方法下面,我们编写以下代码:

 private func readAvailableBytes(stream: InputStream) { //1 let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: maxReadLength) //2 while stream.hasBytesAvailable { //3 let numberOfBytesRead = inputStream.read(buffer, maxLength: maxReadLength) //4 if numberOfBytesRead < 0, let error = stream.streamError { print(error) break } // Construct the Message object } } 

  1. 我们设置将在其中读取传入字节的缓冲区。
  2. 我们循环旋转,而在输入流中有一些东西要阅读。
  3. 我们称为read(_:maxLength :),它从流中读取字节并将其放入缓冲区。
  4. 如果调用返回负值,则返回错误并退出循环。

我们需要在传入流中有数据后立即调用此方法,因此请转到流(_:handle :)方法内部的switch语句 ,找到.hasBytesAvailable开关,并在print语句之后立即调用此方法:

 readAvailableBytes(stream: aStream as! InputStream) 

在这个地方,我们准备好接收数据的缓冲区!

但是我们仍然需要将此缓冲区转换为消息表的内容。

将此方法放在readAvailableBytes(stream :)上

 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) } 

首先,我们使用传递给此方法的缓冲区和大小来初始化String。

文本将使用UTF-8,最后我们将释放缓冲区,并将消息除以':'符号以分隔发件人名称和消息本身。

现在,我们正在分析此消息是否来自其他参与者。 在产品上,您可以创建诸如唯一令牌之类的东西,这足以进行演示。

最后,在所有这种经济情况下,我们形成Message的一个实例并将其返回。

要使用此方法,请在最后一条注释后紧接在readAvailableBytes(stream :)方法的while循环末尾添加以下if-let

 if let message = processedMessageString(buffer: buffer, length: numberOfBytesRead) { // Notify interested parties } 

现在一切准备就绪,可以传递给某人消息 ……但向谁传递呢?

创建ChatRoomDelegate协议


因此,我们需要将新消息通知给ChatRoomViewController.swift ,但我们没有链接。 由于它包含一个强大的ChatRoom链接,因此我们可以陷入一个强大的链接周期的陷阱。

这是创建委托协议的理想场所。 ChatRoom不在乎谁需要了解新帖子。

ChatRoom.swift的顶部添加一个新的协议定义:

 protocol ChatRoomDelegate: class { func received(message: Message) } 

现在在ChatRoom类中添加一个弱链接以存储谁将成为代理:

 weak var delegate: ChatRoomDelegate? 

现在,我们添加readAvailableBytes(stream :)方法,在if-let构造内的最后一行注释下添加以下行:

 delegate?.received(message: message) 

返回到ChatRoomViewController.swift并添加以下类扩展,以确保在MessageInputDelegate之后立即遵守ChatRoomDelegate协议:

 extension ChatRoomViewController: ChatRoomDelegate { func received(message: Message) { insertNewMessageCell(message) } } 

原始项目已经包含必需的项目,因此insertNewMessageCell(_ :)将接受您的消息并在表格视图中显示正确的单元格。

现在,通过在调用super.viewWillAppear()之后立即将其添加到viewWillAppear(_ :)中 ,将视图控制器分配为委托。

 chatRoom.delegate = self 

现在编译并运行该应用程序。 输入名称,然后点击回车。



您将看到一个有关您与聊天连接的单元格。 万岁,您已成功向服务器发送了一条消息并收到了响应!

发布讯息


现在,ChatRoom可以发送和接收消息,是时候为用户提供发送自己的短语的能力了。

ChatRoom.swift中,在类定义的末尾添加以下方法:

 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) } } 

此方法类似于我们之前编写的joinChat(username :) ,不同之处在于该方法在文本前面带有msg前缀(表明这是真实的聊天消息)。

由于我们要通过“ 发送”按钮发送消息,因此我们返回到ChatRoomViewController.swift并在此处找到MessageInputDelegate

在这里,我们看到了空的sendWasTapped(message :)方法。 要发送消息,请将其发送到chatRoom:

 chatRoom.send(message: message) 

实际上,仅此而已! 由于服务器将接收该消息并将其转发给所有人,因此将以与加入聊天时相同的方式将新消息通知给ChatRoom。

编译并运行该应用程序。



如果您现在没有人可以聊天,请启动新的终端窗口,然后输入:

nc localhost 80

这将把您连接到服务器。 现在,您可以使用相同的“协议”连接到聊天室:

iam:gregg

如此-发送一条消息:

msg:Ay mang, wut's good?



恭喜,您写了一个聊天客户端!

我们打扫自己


如果您曾经开发过主动读取/写入文件的应用程序,那么您应该知道好的开发人员在完成使用文件后会关闭文件。 事实是,通过套接字的连接由文件描述符提供。 这意味着完成工作后,您需要像其他文件一样将其关闭。

为此,在定义send(message :)之后,将以下方法添加到ChatRoom.swift中

 func stopChatSession() { inputStream.close() outputStream.close() } 

您可能已经猜到了,此方法关闭线程,因此您不再可以接收和发送消息。 另外,线程从我们之前放置线程的运行循环中删除。

内部的switch语句.endEncountered部分中添加对此方法的调用(_:handle :)

 stopChatSession() 

然后返回ChatRoomViewController.swift并在viewWillDisappear(_ :)中执行相同的操作:

 chatRoom.stopChatSession() 

仅此而已! 现在确定!

结论


既然您已经掌握了套接字网络的基础知识,那么您就可以加深了解。

UDP套接字


此应用程序是使用TCP进行网络通信的一个示例,它可以保证将数据包传递到目的地。

但是,您可以使用UDP套接字。 这种类型的连接不能保证将软件包交付到预期的目的,但是速度要快得多。

这在游戏中特别有用。 经历过滞后吗? 这意味着您的连接不良,许多UDP数据包丢失。

网络套接字


应用程序中HTTP的另一种替代方法是称为Web套接字的技术。

与常规TCP套接字不同,Web套接字使用HTTP建立通信。 有了他们的帮助,您可以实现与普通套接字相同的功能,但是却具有舒适性和安全性,就像在浏览器中一样。

Source: https://habr.com/ru/post/zh-CN467909/


All Articles