美文网首页
Socket + ProtoBuf

Socket + ProtoBuf

作者: 阿文灬 | 来源:发表于2017-09-19 05:56 被阅读0次

    TCP/UDP

    进行Socket编程, 常见使用的协议UDP/TCP
    TCP:传输控制协议 。是专门设计用于在不可靠的因特网上提供可靠的,端到端的字节流通信的协议。它是一种面向连接的协议。TCP连接是字节流而非报文流。
    UDP:用户数据报协议 。不需要建立连接,不可靠。

    TCP连接过程.png UDP连接过程.png

    长连接和短连接

    长连接,指在一个TCP连接上可以连续发送多个数据包,在TCP连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维持。
    短连接是指通信双方有数据交互时,就建立一个TCP连接,数据发送完成后,则断开此TCP连接。

    通常的短连接操作步骤是:
    连接→数据传输→关闭连接或在没有数据传输时直接关闭;
    而长连接通常就是:
    连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接;

    Socket框架

    • BSD Socket
      BSD Socket 是UNIX系统中通用的网络接口,它不仅支持各种不同的网络类型,而且也是一种内部进程之间的通信机制。在我们iOS中也可以使用,但是它所有的函数都是基于C语言的,所有在实际的项目开发中,我们都是使用封装好的。
    • CFSocket
      CFSocket是苹果官方提供给我们进行Socket编程,里面还是有很多C语言的东西,大家有时间可以研究一下,在实际项目中,通常我们不会自己直接使用。
    • AsyncSocket
      AsyncSocket是一个开源的库,用来进行iOS的Socket编程就非常方便, 但是目前只有OC版本, 并且长时间没有更新
    • ysocket
      目前使用Swift进行Socket编程时,常用的一个库

    消息数据类型

    • 直接传递C/C++语言中一字节对齐的结构体数据,只要结构体的声明为定长格式,那么该方式对于C/C++程序而言就非常方便了, 但是对于java这种不常用结构体的语言, 处理起来就相当麻烦
    • 使用SOAP协议(WebService)作为消息报文的格式载体,由该方式生成的报文是基于文本格式的,同时还存在大量的XML描述信息,因此将会大大增加网络IO的负担。又由于XML解析的复杂性,这也会大幅降低报文解析的性能。总之,使用该设计方式将会使系统的整体运行性能明显下降。
    • Json交换格式, 目前比较理想的一种通信格式, 也是在Http请求数据时, 最常见的用法(Demo程序)
    • ProtocolBuffer(也称PB/GPB): google 的一种数据交换的格式, 可以实现跨平台, 方便的序列化&反序列化, 并且数据量相对json

    ProtocolBuffer

    1、简介

    • 跨平台
      ProtoBuf支持多平台和语言, 包括C++/Java/Python等等
    • 序列化&反序列号
      ProtoBuf支持直接将对象序列化成Data, 也支持直接将Data序列化为对象类型
    • 消息大小
      一条消息数据,用protobuf序列化后的大小是json的10分之一,xml格式的20分之一,是二进制序列化的10分之一

    2、安装Protobuf编译器

    ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
    brew install automake
    brew install libtool
    brew install protobuf
    git clone git@github.com:alexeyxo/protobuf-swift.git
    
    // cd protobuf-swift,反正一定要到scripts的父文件夹位置
    ./scripts/build.sh 
    

    Add ./src/ProtocolBuffers/ProtocolBuffers.xcodeproj in your project.
    或使用Cocoapod:pod 'ProtocolBuffers-Swift'
    注意:Mac项目貌似不能直接使用cocoapods集成
    详情请看:https://github.com/alexeyxo/protobuf-swift

    3、编码xxx.proto文件,并生成对应的swift代码
    protoc xxx.proto --swift_out="./"
    如果前面在终端没有成功执行./scripts/build.sh,该句将报错。

    实际开发中的异同

    • 心跳包
      心跳包是什么呢?我们首先要弄清楚,我们每一个客户端连接到服务器之后,服务器会怎么来处理我们的连接。
      我们知道每一个客户端在连接到服务器的时候,都会有一个单独的线程来处理。比如我们前面给大家看的那个房间的例子,一个房间2000人,那就要有2000个连接保持。这对服务器的负荷是非常大的,但是如果说客户端断开了,服务器还保持着这个线程,就非常的耗资源。
    • 数据我们和服务器沟通的时候究竟发送什么数据?
      在实际开发中,我们不可能仅仅简单的给服务器发一个字符串,服务器给我们回一个字符串,这是不现实的。

    下面将使用ysocket、protocolbuffer实现一个简单的IM项目。

    基本环境搭建

    1. 创建一个iOS工程作为客户端-Client,创建一个mac工程作为服务器-Server
    2. 集成SwiftSocket、ProtocolBuffers-Swift
    3. 编写IM.proto,并生成IM.proto.swift

    IM编程

    TCP连接过程.png
    • 客户端
      创建clientSocket;
      连接服务器;
      读取服务器可能发来的信息,即不断读取。而且读取信息是阻塞式的,两方面考虑都需要另开线程。代码之后再补充;
      发送信息给服务器。代码之后再补充
    import SwiftSocket
    
    class ViewController: UIViewController {
    
        var client: TCPClient = TCPClient(address: "127.0.0.1", port: 7878)
        var isConnected: Bool = false
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            switch client.connect(timeout: 10) {
            case .success:
                isConnected = true
                self.readServerMsg()
            case .failure(let error):
                print("连接服务器失败:\(error.localizedDescription)")
            }
        }
        
        // 读取服务器发送来的信息
        func readServerMsg() {
            DispatchQueue.global().async {
                while self.isConnected {
                    
                }
            }
        }
        
        // 发送信息给服务器
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            
        }
    }
    
    • 服务器
      创建serverSocket;
      创建clientSocket数组。数组的元素与客户端的clientSocket一一对应,并相互交流;
      serverSocket开启监听;
      不断接收客户端的clientSocket,并生成对应的clientSocket。需要另开线程;
      clientSocket不断读取客户端的clientSocket发来的信息。需要另开线程。代码之后再补充
    import SwiftSocket
    
    class ViewController: NSViewController {
    
        var server: TCPServer = TCPServer(address: "127.0.0.1", port: 7878)
        var clients: [TCPClient] = [TCPClient]()
        
        var serverRunning: Bool = false
        
        override func viewDidLoad() {
            super.viewDidLoad()
    
            switch server.listen() {
            case .success:
                serverRunning = true
                print("服务器已启动...")
                
                DispatchQueue.global().async {
                    while self.serverRunning {
                        if let client = self.server.accept() {
                            print("服务器接收到客户端: \(client.address)")
                            self.readClientMsg(client)
                        }
                    }
                }
            case .failure(let error):
                print("服务器监听失败:\(error.localizedDescription)")
            }
        }
        
        func stopRun() {
            serverRunning = false
        }
    
        // 读取服务器发送来的信息
        func readClientMsg(_ client: TCPClient) {
            clients.append(client)
            DispatchQueue.global().async {
                while self.serverRunning {
                    
                }
            }
        }
    }
    

    此时代码基本完成,也可以运行看到基本效果。只是客户端与服务端都缺少发送与接收信息的代码。

    发送与接收 .protocolbuffer 数据

    1、创建 IM.proto 文件,生成 swift 源码文件。将源码文件分别添加到客户端程序(Client)与服务器(Server)中。

    syntax = "proto2";
    
    message UserInfo {
        required string name = 1;
        required int64 level = 2;
        required string iconUrl = 3;
    }
    

    2、补充之前,客户端与服务端发送与接收数据的代码

    • 客户端发送信息给服务器
      这里发送一个消息采用一次性发送的方式
        // 发送信息给服务器
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            // 要发送的信息
            let userInfo = UserInfo.Builder()
            userInfo.name = "阿文\(arc4random_uniform(10))"
            userInfo.level = 20
            userInfo.iconUrl = "icon\(arc4random_uniform(5))"
            
            var data = (try! userInfo.build()).data()
            
            // 将消息长度添加进要发送的信息中
            var length = data.count
            data = data + Data(bytes: &length, count: 4)
            
            client.send(data: data)
        }
    
    • 服务器接收客户端发来的信息
        // 读取客户端发送来的信息
        func readClientMsg(_ client: TCPClient) {
            clients.append(client)
            DispatchQueue.global().async {
                while self.serverRunning {
                    // 读取信息长度
                    if let lengthBytes = client.read(4) {
                        let lengthData = Data(bytes: lengthBytes, count: 4)
                        var length = 0
                        (lengthData as NSData).getBytes(&length, length: 4)
                        
                        // 读取信息
                        if let dataBytes = client.read(length) {
                            let data = Data(bytes: dataBytes, count: length)
                            let userInfo = try! UserInfo.parseFrom(data: data)
                            print("用户名:\(userInfo.name) 用户级别:\(userInfo.level)")
                            
                            client.send(data: lengthData + data)
                        }
                    }
                }
            }
        }
    
    • 客户端接收服务器发来的信息
        // 读取服务器发送来的信息
        func readServerMsg() {
            DispatchQueue.global().async {
                while self.isConnected {
                    // 读取信息长度
                    if let lengthBytes = self.client.read(4) {
                        let lengthData = Data(bytes: lengthBytes, count: 4)
                        var length = 0
                        (lengthData as NSData).getBytes(&length, length: 4)
                        
                        // 读取信息
                        if let dataBytes = self.client.read(length) {
                            let data = Data(bytes: dataBytes, count: length)
                            let userInfo = try! UserInfo.parseFrom(data: data)
                            print("用户名:\(userInfo.name) 用户级别:\(userInfo.level)")
                        }
                    }
                }
            }
        }
    

    心跳包

    心跳包用于服务器检测客户端是否还在,其内在原理仍是客户端发送信息,服务器接收信息。不过心跳包信息必须表明独有的类型,所以发送一次信息除了信息长度与信息内容外,至少还包含一个标识符表明信息的类型。
    如果服务器在一定时间内检测不到客户端的心跳包,则断开该客户端的链接。

    代码:https://github.com/taoGod/IM-MG4

    相关文章

      网友评论

          本文标题:Socket + ProtoBuf

          本文链接:https://www.haomeiwen.com/subject/fjhysxtx.html