代码编织梦想

序列化一般有两个主要目的:

  • 网络传输
  • 对象持久化

一般评判一个编解码框架的优劣时,会考虑如下几个因素:

  • 是否支持跨语言;
  • 编码后的码流大小;
  • 编解码性能;
  • 类库是否小巧,API 使用是否方便;
  • 使用者需要手工开发的工作量和难度;
  • 类库的开源性以及社区活跃度;

在同等环境下,编码后的字节数越大,存储的时候就越占空间,存储的硬件成本就越高,并且在网络传输时更占带宽,导致系统的吞吐量降低。

主流编解码框架

Google 的 Protobuf

Protobuf(Google Protocol Buffers),由谷歌开源。它将数据结构以 .proto 文件进行描述,通过代码生成工具可以生成对应数据结构的 POJO 对象和 Protobuf 相关的方法和属性。

它的特点:

  • 结构化数据存储格式
  • 高效的编解码性能
  • 语言无关、平台无关、扩展性好
  • 支持的语言比较丰富

对比 XML, 尽管 XML 的可读性和可扩展性非常好,也非常适合描述数据结构,但是 XML 解析的时间开销和 XML 为了可读性而牺牲的空间开销都非常大,因此不适合做高性能的通信协议。Protobuf 使用二进制编码,在空间和性能上具有更大优势。

Facebook 的 Thrift

Thrift 是 Facebook 于2007年开发的跨语言的 rpc 服框架,提供多语言的编译功能,并提供多种服务器工作模式;用户通过 Thrift 的 IDL(接口定义语言)来描述接口函数及数据类型,然后通过 Thrift 的编译环境生成各种语言类型的接口文件,用户可以根据自己的需要采用不同的语言开发客户端代码和服务器端代码。

实战 Protobuf

安装

您将需要 protobuf 的编译器来生成指定语言的代码。

因此需要安装编译器,教程来源:apple/swift-protobuf

$ brew install swift-protobuf
$ protoc --version
libprotoc 3.14.0

.proto

创建 .proto 文件,然后在里面定义消息,再通过 protobuf 编译器生成 Swift代码。

对于 .proto 文件,我们可以用任意的 IDE 或者文本编辑器进行编辑,个人建议用 VSCode 然后结合 vscode-proto3 插件。

Google protocol buffer 的官方文档:文档

我们简单的创建个 movie.proto
这里有一个iOS交流圈:891 488 181 有兴趣的都可以来了解,分享BAT,阿里面试题、面试经验,讨论技术,裙里资料直接下载就行, 大家一起交流学习!

syntax = "proto3";

message Movie {
    enum Genre {
        COMEDY = 0;
        ACTION = 1;
        HORROR = 2;
        ROMANCE = 3;
        DRAMA = 4;
    }

    string title = 1;
    Genre genre = 2;
    int32 year = 3;
}

然后将其进行编译:

$ protoc --swift_out=. movie.proto

执行后,编译器会生成 movies.pb.swift 文件。

功能需求

创建 MovieClientMovieServer 两端,然后使用 SwiftNIO 将我们的 Movie 结构从客户端发送到服务器。

两端都需要依赖 apple/swift-nioapple/swift-protobuf

// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "MovieClient",
    dependencies: [
        .package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"),
        .package(name: "SwiftProtobuf", url: "https://github.com/apple/swift-protobuf.git", from: "1.14.0")
    ],
    targets: [

        .target(
            name: "MovieClient",
            dependencies: [
                .product(name: "NIO", package: "swift-nio"),
                "SwiftProtobuf"
            ]),
        .testTarget(
            name: "MovieClientTests",
            dependencies: ["MovieClient"]),
    ]
)

客户端和服务器端的工程都导入 movies.pb.swift

MoviewClient 的工程目录:

MovieServer 的工程目录:

两端代码的 Handler 是各自业务的核心,其它代码是 SwiftNIO 比较常见的使用方式,大家可借鉴使用。

使用 8030 作为服务器的端口。

如果你对创建 Swift 项目不太熟悉,建议查阅 OldBirds 公众号往期文章

实现 MovieClient

MovieClient.swift 的实现如下:

import Foundation
import NIO

final class MovieClient {
    private let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)

    private var host: String
    private var port: Int

    init(host: String, port: Int) {
        self.host = host
        self.port = port
    }

    func start() throws {
        do {
            let channel = try bootstrap.connect(host: host, port: port).wait()
            try channel.closeFuture.wait()
        } catch let error {
            throw error
        }
    }

    func stop() {
        do {
            try group.syncShutdownGracefully()
        } catch let error {
            print("Error shutting down \(error.localizedDescription)")
            exit(0)
        }
        print("Client connection closed")
    }

    private var bootstrap: ClientBootstrap {
        return ClientBootstrap(group: group)
            // Enable SO_REUSEADDR.
            .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
            .channelInitializer { channel in
                channel.pipeline.addHandler(MovieClientHandler())
        }
    }
}

MovieClientHandler.swift 的实现:

import Foundation
import NIO

class MovieClientHandler: ChannelInboundHandler {
    typealias InboundIn = ByteBuffer
    typealias OutboundOut = ByteBuffer

    func channelActive(context: ChannelHandlerContext) {
        var movie = Movie()
        movie.genre = .romance
        movie.title = "那些年我们一起追过的女孩"
        movie.year = 2011

        do {
            /// 序列化对象
            let binaryData: Data = try movie.serializedData()

            // 创建 buffer
            var buffer = context.channel.allocator.buffer(capacity: binaryData.count)

            // 将数据写入 buffer
            buffer.writeBytes(binaryData)

            let promise: EventLoopPromise<Void> = context.eventLoop.makePromise()
            promise.futureResult.whenComplete { (_) in
                print("Sent data, closing the channel")
                context.close(promise: nil)
            }

            // write and flush the data
            context.writeAndFlush(wrapOutboundOut(buffer), promise: promise)
        } catch let error {
            print(error.localizedDescription)
        }
    }

    func errorCaught(context: ChannelHandlerContext, error: Error) {
        print(error.localizedDescription)
        context.close(promise: nil)
    }
}

main.swift 的实现:

let client = MovieClient(host: "localhost", port: 8030)

do {
    try client.start()
} catch let error {
    print("Error: \(error.localizedDescription)")
    client.stop()
}

实现 MovieServer

MovieServer.swift 的实现:


import Foundation
import NIO

final class MovieServer {

    private let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
    private var host: String
    private var port: Int

    init(host: String, port: Int) {
        self.host = host
        self.port = port
    }

    func start() throws {
        do {
            let channel = try serverBootstrap.bind(host: host, port: port).wait()
            print("Listening on \(String(describing: channel.localAddress))...")
            try channel.closeFuture.wait()
        } catch let error {
            throw error
        }
    }

    func stop() {
        do {
            try group.syncShutdownGracefully()
        } catch let error {
            print("Error shutting down \(error.localizedDescription)")
            exit(0)
        }
        print("Client connection closed")
    }

    private var serverBootstrap: ServerBootstrap {
        return ServerBootstrap(group: group)
            // Specify backlog and enable SO_REUSEADDR for the server itself
            .serverChannelOption(ChannelOptions.backlog, value: 256)
            .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
            .childChannelInitializer { channel in
                // Ensure we don't read faster than we can write by adding the BackPressureHandler into the pipeline.
                channel.pipeline.addHandler(BackPressureHandler()).flatMap { v in
                    channel.pipeline.addHandler(MovieServerHandler())
                }
            }
            .childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
            // Enable SO_REUSEADDR for the accepted Channels
            .childChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
            .childChannelOption(ChannelOptions.maxMessagesPerRead, value: 16)
            .childChannelOption(ChannelOptions.recvAllocator, value: AdaptiveRecvByteBufferAllocator())
    }

}

MovieServerHandler.swift 的实现:

import Foundation
import NIO

class MovieServerHandler: ChannelInboundHandler {
    typealias InboundIn = ByteBuffer
    typealias OutboundOut = ByteBuffer

    func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        // 将 NIOAny 转化为 ByteBuffer
        var buffer = unwrapInboundIn(data)

        // 获取可读的字节数
        let readableBytes = buffer.readableBytes

        // 从 buffer 读取
        guard let received = buffer.readBytes(length: readableBytes) else {
            return
        }

        // 将 bytes 转化为 data
        let receivedData = Data(bytes: received, count: received.count)

        do {
            // 反序列化
            let movie = try Movie(serializedData: receivedData)
            print("收到: \(movie)")
            // 做其他事情
        } catch let error {
            print("error: \(error.localizedDescription)")
        }
    }

    func channelReadComplete(context: ChannelHandlerContext) {
        context.flush()
    }

    func errorCaught(context: ChannelHandlerContext, error: Error) {
        print("error: \(error.localizedDescription)")
        context.close(promise: nil)
    }
}

main.swift 的实现:

let server = MovieServer(host: "localhost", port: 8030)

do {
    try server.start()
} catch let error {
    print("Error: \(error.localizedDescription)")
    server.stop()
}

运行结果

客户端输出:

Sent data, closing the channel
Program ended with exit code: 0

服务器端输出:

Listening on Optional([IPv4]127.0.0.1/127.0.0.1:8030)...
收到: MovieServer.Movie:
title: "那些年我们一起追过的女孩"
genre: ROMANCE
year: 2011

完成功能需求。

总结

本文的主要目的是介绍下网络编解码的一些入门知识,其中以 Protobuf 的样例进行简单的讲解,从而知道 Protobuf 在 SwiftNIO 的基本使用的流程,总体来讲也比较简单:

  1. 通过声明 movie.proto,然后用 probuf 的编译器生成 swift 代码,这这份生成的代码引入客户端和服务器端;
  2. 在客户端中创建 Movie 对象,然后序列化成 Data,通过 SwiftNIO 发送给服务器端;
  3. 服务器端接收到客户端的数据,从 ByteBuffer 中读取到数据,然后将其转化为 Data,在通过方法将 Data 反序列化成 Movie 对象,完成解码。

proto文件生成目标代码,简单易用; 序列化反序列化直接对应程序中的数据类,不需要解析后在进行映射; 二进制消息,性能好/效率高;

对我而言,可能成本在于写 proto,需要学习其语法。在客户端层面,普及率还是没有 json 广泛。

参阅

文章链接:https://juejin.cn/post/6904300742090162183

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接: https://blog.csdn.net/henry_lei/article/details/111054192

ffmpeg4.3.1 iOS初体验-爱代码爱编程

xcode12.2,FFmpeg4.3.1 1. 下载FFmpeg git clone https://git.ffmpeg.org/ffmpeg.git 也可以点击下载压缩包,我下的 .bz2 2. 编译FFmpeg 下载gas-preprocessor:https://github.com/libav/gas-preprocessor,亲测可

iOS面试题-Swift篇(一)-爱代码爱编程

这篇文章主要介绍了iOS面试题-Swift篇,对大家的学习或者工作具有一定的参考学习价值,感兴趣的小伙伴们可以了解一下哦 介绍Swift Swift是苹果在2014年6月WWDC发布的全新编程语言,借鉴了JS,Python,C#,Ruby等语言特性,看上去偏脚本化,Swift 仍支持 cocoa touch 框架 Swift的优点: Sw

iOS开发使用UIKeyInput自定义密码输入框-爱代码爱编程

前言 开发中很多地方都会遇到密码输入,这时候往往需要根据UI设计自定义。这里遵守UIKeyInput,实现协议中的方法,让自定义View可以进行文字输入;再通过func draw(_ rect: CGRect)绘制现自定义UI;使用配置类来统一接口;使用代理来管理各种输入相关的事件。文章末尾有提供OC和Swift双语的CLDemo下载,这里讲解就使用Sw

IOS-网络请求-爱代码爱编程

IOS 基于 NSURLSession 进行 HTTP 请求 前言GET 请求POST 请求以 application/json 数据段格式的 POST 请求为例以 multipart/form-data 数据段格式的 POST 请求为拓展文件下载文件上传结语 前言 使用 IOS 内置控件 NSURLSession 进行 HTTP GET 请

App 多窗口支持架构-爱代码爱编程

Python实战社群 Java实战社群 长按识别下方二维码,按需求添加 扫码关注添加客服 进Python社群▲ 扫码关注添加客服 进Java社群▲ 作者:Bill,前滴滴 iOS,现就职于抖音商业化,偶尔写写 FE 和跨端。 来源公众号丨老司机技术周报(ID:LSJCoding) 本文发表于 2019/07/01

规范你的Android应用通知渠道-爱代码爱编程

/   今日科技快讯   / 据国家企业信用信息公示系统的披露,苏宁控股集团股东张近东、张康阳及南京润贤企业管理中心(有限合伙)已将公司全部股权出质给淘宝(中国)软件有限公司。股权出质登记日期为2020年12月4日,合计出质股权数额10亿元人民币,与苏宁控股集团的注册资本金额等同。 /   作者简介   / 明天就是周六啦,祝大家周末愉快!

ffmpeg4.3.1 iOS初体验-爱代码爱编程

xcode12.2,FFmpeg4.3.1 1. 下载FFmpeg git clone https://git.ffmpeg.org/ffmpeg.git 也可以点击下载压缩包,我下的 .bz2 2. 编译FFmpeg 下载gas-preprocessor:https://github.com/libav/gas-preprocessor,亲测可

iOS 静态库详解与开发的详解-爱代码爱编程

一:介绍 1. 什么是库? 库是共享程序代码的方式,一般分为静态库和动态库。 静态库:链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。 动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。 2. 两种形式中.framework的区别 如上图所示,静态库的形式包含.a和.framewo

Swift 访问权限fileprivate和 open-爱代码爱编程

在swift 3中新增加了两种访问控制权限fileprivate和open。 下面结合网上资料和个人理解整理一下两个属性的原理与介绍。 fileprivate 在原有的swift中的private其实并不是真正的私有,如果一个变量定义为private,在同一个文件中的其他类依然是可以访问到的。这个场景在使用extension的时候很明显。 clas

iOS中的OC和Swift进行互相调用-爱代码爱编程

有时候 ,我们会涉及到双向混合编程,特别是OC和swift的互相引用。 swift调用oc的方法: 1、桥接文件,一般是swift工程,在创建一个oc文件时,系统自动添加(不用改名,直接默认即可) 2、将需要引用的oc文件 .h头文件 添加到桥接类中。 如下: 然后在swift调用处,直接就可以使用了。 oc调用swift的方法: 1、首

iOS 音视频开发,AVAudioRecorder实现录音功能!!-爱代码爱编程

AVAudioRecorder、AVAudioPlayer 属于AVFoundation框架,使用时需要先导入**<AVFoundation/AVFoundation.h>**框架头文件。 AVFoundation 是苹果的现代媒体框架,它包含了一些不同用途的 API 和不同层级的抽象。其中有一些是Objective-C

学习编程需要什么基础?从基础到高级?-爱代码爱编程

程序员薪酬高、工作环境好,是很多同学向往的职业,让很多非计算机专业的同学羡慕不已。非计算机专业难道就不能成为程序员了吗? 一、学编程需要什么基础? 1、数学基础 从计算机发展和应用的历史来看计算机的数学模型和体系结构等都是有数学家提出的,最早的计算机也是为数值计算而设计的。因此,要学好计算机就要有一定的数学基础,初学者有高中水平就差不多了。