iOS 基于Socket使用Protobuf进行数据传输

题记:

最近有需求场景涉及到数据传输,要求尝试一下Protobuf的方式,基于Socket进行传输。

那么这个Protobuf是个啥?我也是mb的,好吧,让我们看看这究竟是什么。


1.Protobuf简介:


简介:
Protobuf是一个开源项目,Google开发的,是一个与语言、平台无关,可扩展的序列化、结构化数据交换格式;和我们熟悉的Json和xml做的事情其实差不多;
Protobuf基于自己的数据格式规范,用相应的工具转换成不同平台的数据结构类型,主要用在数据存储、传输协议格式等场合;
Google提供了Protobuf的多语言的实现:C++,python等,也包括我们所用到的OC,每一种实现都包含了相应语言的编译器以及库文件;

优点:
它是一种二进制格式,传输交换会快很多,开销也小很多,这也是主要优点;

示例分析:
在使用Protobuf时,我们可以用相应的语法规则定义描述我们要通讯的协议中使用的结构化数据:

message Person {
    required string name=1;
    required int32 id=2;
    optional string email=3;


    enum PhoneType {
        MOBILE=0;
        HOME=1;
        WORK=2;
    }


    message PhoneNumber {
        required string number=1;
        optional PhoneType type=2 [default=HOME];
    }


    repeated PhoneNumber phone=4;
    optional string surname = 5;
}

message AddressBook {
  repeated Person person = 1;
}

message Test {
  repeated string person = 2;
}

字段格式:限定修饰符① | 数据类型② | 字段名称③ | = | 字段编码值④ | [字段默认值⑤]

①.限定修饰符包含 required\optional\repeated

②.数据类型
protobuf 数据类型描述打包C++语言描述
bool布尔类型1字节bool
double64位浮点数Ndouble
float32为浮点数Nfloat
int3232位整数Nint
uin32无符号32位整数Nunsigned int
int6464位整数N__int64
uint6464为无符号整Nunsigned __int64
sint3232位整数,处理负数效率更高Nint32
sing6464位整数 处理负数效率更高N__int64
fixed3232位无符号整数4unsigned int32
fixed6464位无符号整数8unsigned __int64
sfixed3232位整数、能以更高的效率处理负数4unsigned int32
sfixed6464为整数8unsigned __int64
string只能处理 ASCII字符Nstd::string
bytes用于处理多字节的语言字符、如中文Nstd::string
enum可以包含一个用户自定义的枚举类型uint32N(uint32)enum
message可以包含一个用户自定义的消息类型Nobject of class
    

N 表示打包的字节并不是固定。而是根据数据的大小或者长度。

③.字段名称
protobuf建议字段的命名采用以下划线分割的驼峰式。

④.字段编码值
有了该值,通信双方才能互相识别对方的字段。当然相同的编码值,其限定修饰符和数据类型必须相同。
建议:项目投入运营以后涉及到版本升级时的新增消息字段全部使用optional或者repeated,尽量不实用required。如果使用了required,需要全网统一升级,如果使用optional或者repeated可以平滑升级。

⑤.默认值
当在传递数据时,对于required数据类型,如果用户没有设置值,则使用默认值传递到对端。当接受数据是,对于optional字段,如果没有接收到optional字段,则设置为默认值。

其他:
1.关于import
protobuf 接口文件可以像C语言的h文件一个,分离为多个,在需要的时候通过 import导入需要对文件。其行为和C语言的#include或者java的import的行为大致相同。

2.关于package
避免名称冲突,可以给每个文件指定一个package名称,对于java解析为java中的包。对于C++则解析为名称空间。

3.关于message
支持嵌套消息,消息可以包含另一个消息作为其字段。也可以在消息内定义一个新的消息。

4.关于enum
枚举的定义和C++相同,但是有一些限制。
枚举值必须大于等于0的整数。
使用分号(;)分隔枚举变量而不是C++语言中的逗号(,)

语法指南:

说明:
本指南描述了怎样使用protocol buffer 语法来构造你的protocol buffer数据,包括.proto文件语法以及怎样生成.proto文件的数据访问类。
(本文只针对proto2的语法)

1.定义一个消息类型
先来看一个非常简单的例子。假设你想定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数,以及每一页多少条查询结果。可以采用如下的方式来定义消息类型的.proto文件了:

message SearchRequest {
  required string query = 1;
  optional int32 page_number = 2;
  optional int32 result_per_page = 3;
}

1.1指定字段类型
在上面的例子中,所有字段都是标量类型:两个整型(page_number和result_per_page),一个string类型(query)。当然,你也可以为字段指定其他的合成类型,包括枚举(enumerations)或其他消息类型。

1.2分配标识号
正如上述文件格式,在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。

1.3指定字段规则
所指定的消息字段修饰符必须是如下之一:
required:一个格式良好的消息一定要含有1个这种字段。表示该值是必须要设置的;
optional:消息格式中该字段可以有0个或1个值(不超过1个)。
repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。表示该值可以重复,相当于java中的List。
由于一些历史原因,基本数值类型的repeated的字段并没有被尽可能地高效编码。在新的代码中,用户应该使用特殊选项[packed=true]来保证更高效的编码。如:

repeated int32 samples = 4 [packed=true];

1.4添加更多消息类型
在一个.proto文件中可以定义多个消息类型。
如果想定义与SearchResponse消息类型对应的回复消息格式的话,你可以将它添加到相同的.proto文件中。

1.5添加注释
双斜杠(//) 语法格式。

1.6从.proto文件生成了什么?
当用protocolbuffer编译器来运行.proto文件时,编译器将生成所选择语言的代码,这些代码可以操作在.proto文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。
对C++来说,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。

1.7标量数值类型
参见Protobuf示例分析。

1.8Optional的字段和默认值
如上所述,消息描述中的一个元素可以被标记为“可选的”(optional)。一个格式良好的消息可以包含0个或一个optional的元素。当解 析消息时,如果它不包含optional的元素值,那么解析出来的对象中的对应字段就被置为默认值。默认值可以在消息描述文件中指定。例如,要为 SearchRequest消息的result_per_page字段指定默认值10,在定义消息格式时如下所示:

optional int32 result_per_page = 3 [default = 10];

如果没有为optional的元素指定默认值,就会使用与特定类型相关的默认值:对string来说,默认值是空字符串。对bool来说,默认值是false。对数值类型来说,默认值是0。对枚举来说,默认值是枚举类型定义中的第一个值。

1.9枚举
message SearchRequest {
  required string query = 1;
  optional int32 page_number = 2;
  optional int32 result_per_page = 3 [default = 10];
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  optional Corpus corpus = 4 [default = UNIVERSAL];
}

2.使用其他消息类型
你可以将其他消息类型用作字段类型。
假设在每一个SearchResponse消息中包含Result消息,此时可以在相同的.proto文件中定义一个Result消息类型,然后在SearchResponse消息中指定一个Result类型的字段,如:

message SearchResponse {
  repeated Result result = 1;
}
message Result {
  required string url = 1;
  optional string title = 2;
  repeated string snippets = 3;
}

2.1导入定义
在上面的例子中,Result消息类型与SearchResponse是定义在同一文件中的。如果想要使用的消息类型已经在其他.proto文件中已经定义过了呢?
你可以通过导入(importing)其他.proto文件中的定义来使用它们。要导入其他.proto文件的定义,你需要在你的文件中添加一个导入声明,如:
import "myproject/other_protos.proto";
protocol编译器就会在一系列目录中查找需要被导入的文件,这些目录通过protocol编译器的命令行参数-I/–import_path指定。如果不提供参数,编译器就在其调用目录下查找。

2.2嵌套类型
你可以在其他消息类型中定义、使用消息类型。

message SearchResponse {
  message Result {
    required string url = 1;
    optional string title = 2;
    repeated string snippets = 3;
  }
  repeated Result result = 1;
}


如果你想在它的父消息类型的外部重用这个消息类型,你需要以Parent.Type的形式使用它,如:

message SomeOtherMessage {
  optional SearchResponse.Result result = 1;
}

2.3组
注:该特性已被弃用。

2.4更新一个消息类型
如果一个已有的消息格式已无法满足新的需求——如,要在消息中添加一个额外的字段——但是同时旧版本写的代码仍然可用。不用担心!更新消息而不破坏已有代码是非常简单的。在更新时只要记住以下的规则即可。

不要更改任何已有的字段的数值标识。
*所添加的任何字段都必须是optional或repeated的。这就意味着任何使用“旧”的消息格式的代码序列化的消息可以被新的代码所解析,因为它们 不会丢掉任何required的元素。应该为这些元素设置合理的默认值,这样新的代码就能够正确地与老代码生成的消息交互了。类似地,新的代码创建的消息 也能被老的代码解析:老的二进制程序在解析的时候只是简单地将新字段忽略。然而,未知的字段是没有被抛弃的。此后,如果消息被序列化,未知的字段会随之一 起被序列化——所以,如果消息传到了新代码那里,则新的字段仍然可用。注意:对Python来说,对未知字段的保留策略是无效的。
非required的字段可以移除——只要它们的标识号在新的消息类型中不再使用(更好的做法可能是重命名那个字段,例如在字段前添加“OBSOLETE_”前缀,那样的话,使用的.proto文件的用户将来就不会无意中重新使用了那些不该使用的标识号)。
一个非required的字段可以转换为一个扩展,反之亦然——只要它的类型和标识号保持不变。
int32, uint32, int64, uint64,和bool是全部兼容的,这意味着可以将这些类型中的一个转换为另外一个,而不会破坏向前、 向后的兼容性。如果解析出来的数字与对应的类型不相符,那么结果就像在C++中对它进行了强制类型转换一样(例如,如果把一个64位数字当作int32来 读取,那么它就会被截断为32位的数字)。
sint32和sint64是互相兼容的,但是它们与其他整数类型不兼容。
string和bytes是兼容的——只要bytes是有效的UTF-8编码。
嵌套消息与bytes是兼容的——只要bytes包含该消息的一个编码过的版本。
fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的。

2.5扩展
通过扩展,可以将一个范围内的字段标识号声明为可被第三方扩展所用。然后,其他人就可以在他们自己的.proto文件中为该消息类型声明新的字段,而不必去编辑原始文件了。看个具体例子:
message Foo {
  // ...
  extensions 100 to 199;
}
这个例子表明:在消息Foo中,范围[100,199]之内的字段标识号被保留为扩展用。现在,其他人就可以在他们自己的.proto文件中添加新字段到Foo里了,但是添加的字段标识号要在指定的范围内——例如:

extend Foo {
  optional int32 bar = 126;
}

消息Foo现在有一个名为bar的optional int32字段。

然而,要在程序代码中访问扩展字段的方法与访问普通的字段稍有不同——生成的数据访问代码为扩展准备了特殊的访问函数来访问它。

Foo foo;
foo.SetExtension(bar, 15);

类似地,Foo类也定义了模板函数 HasExtension(),ClearExtension(),GetExtension(),MutableExtension(),以及 AddExtension()。这些函数的语义都与对应的普通字段的访问函数相符。要查看更多使用扩展的信息,请参考相应语言的代码生成指南。注:扩展可 以是任何字段类型,包括消息类型。

2.6嵌套的扩展
可以在另一个类型的范围内声明扩展。

message Baz {
  extend Foo {
    optional int32 bar = 126;
  }
  ...
}

在此例中,访问此扩展的C++代码如下:

Foo foo;
foo.SetExtension(Baz::bar, 15);

2.7选择可扩展的标量符号
在同一个消息类型中一定要确保两个用户不会扩展新增相同的标识号,否则可能会导致数据的不一致。可以通过为新项目定义一个可扩展标识号规则来防止该情况的发生。

message Foo {
  extensions 1000 to max;
}

max 是 2^29 - 1, 或者 536,870,911.
通常情况下在选择标符号时,标识号产生的规则中应该避开[19000-19999]之间的数字,因为这些已经被Protocol Buffers实现中预留了。

3.Oneof
如果你的消息中有很多可选字段, 并且同时至多一个字段会被设置, 你可以加强这个行为,使用oneof特性节省内存.
Oneof字段就像可选字段, 除了它们会共享内存, 至多一个字段会被设置。 设置其中一个字段会清除其它oneof字段。 你可以使用case()或者WhichOneof() 方法检查哪个oneof字段被设置, 看你使用什么语言了.

3.1使用Oneof
message SampleMessage {
  oneof test_oneof {
     string name = 4;
     SubMessage sub_message = 9;
  }
}

可以增加任意类型的字段, 但是不能使用 required, optional, repeated 关键字。

在产生的代码中, oneof字段拥有同样的 getters 和setters, 就像正常的可选字段一样. 也有一个特殊的方法来检查到底那个字段被设置. 你可以在相应的语言API中找到oneof API介绍。

Oneof 特性:
1.设置oneof会自动清楚其它oneof字段的值. 所以设置多次后,只有最后一次设置的字段有值.

SampleMessage message;
message.set_name(“name”);
CHECK(message.has_name());
message.mutable_sub_message();   // Will clear name field.
CHECK(!message.has_name());


2.If the parser encounters multiple members of the same oneof on the wire, only the last member seen is used in the parsed message.
3.oneof不支持扩展.
4.oneof不能 repeated.
5.反射API对oneof 字段有效.
6.如果使用C++,需确保代码不会导致内存泄漏. 下面的代码会崩溃, 因为sub_message 已经通过set_name()删除了.

SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name(“name”);      // Will delete sub_message
sub_message.set_…              // Crashes here

7.Again in C++, if you Swap() two messages with oneofs, each message will end up with the other’s oneof case: in the example below, msg1 will have a sub_message and msg2 will have a name.

SampleMessage msg1;
msg1.set_name(“name”);
SampleMessage msg2;
msg2.mutable_sub_message();
msg1.swap(&msg2);
CHECK(msg1.has_sub_message());
CHECK(msg2.has_name());

3.2向后兼容性问题
当增加或者删除oneof字段时一定要小心. 如果检查oneof的值返回None/NOT_SET, 它意味着oneof字段没有被赋值或者在一个不同的版本中赋值了。 你不会知道是哪种情况。

4.包(Package)
当然可以为.proto文件新增一个可选的package声明符,用来防止不同的消息类型有命名冲突。如:

package foo.bar;
message Open { ... }

在其他的消息格式定义中可以使用包名+消息名的方式来定义域的类型,如:

message Foo {
  ...
  required foo.bar.Open open = 1;
  ...
}

包的声明符会根据使用语言的不同影响生成的代码。

对于C++,产生的类会被包装在C++的命名空间中,如上例中的Open会被封装在 foo::bar空间中;
对于Java,包声明符会变为java的一个包,除非在.proto文件中提供了一个明确有java_package;
对于 Python,这个包声明符是被忽略的,因为Python模块是按照其在文件系统中的位置进行组织的。

4.1包及名称的解析
Protocol buffer语言中类型名称的解析与C++是一致的:首先从最内部开始查找,依次向外进行,每个包会被看作是其父类包的内部类。当然对于 (foo.bar.Baz)这样以“.”分隔的意味着是从最外围开始的。ProtocolBuffer编译器会解析.proto文件中定义的所有类型名。 对于不同语言的代码生成器会知道如何来指向每个具体的类型,即使它们使用了不同的规则。

5.定义服务(Service)
如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器将会根据所选择的不同语言生成服务接口代码及存根。如,想要定义一个RPC服务并具有一个方法,该方法能够接收 SearchRequest并返回一个SearchResponse,此时可以在.proto文件中进行如下定义:

service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

略( http://colobu.com/2015/01/07/Protobuf-language-guide/#定义一个消息类型

6.选项(Options)
略( http://colobu.com/2015/01/07/Protobuf-language-guide/#定义一个消息类型

6.1自定义选项
略( http://colobu.com/2015/01/07/Protobuf-language-guide/#定义一个消息类型

7.生成访问类
可以通过定义好的.proto文件来生成Java、Python、C++代码,需要基于.proto文件运行protocol buffer编译器protoc。运行的命令如下所示:
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto


2.Protobuf安装:


Protocol Buffers v3.3.0我们使用的是这个版本;
选择相应的压缩文件进行下载:


我用的环境是xcode8.3,protobuf3.0.0以上才支持OC,且基于性能原因没有使用ARC;

具体的安装步骤:

1.需要先安装(没有安装的话)autoconf、automake、libtool;在shell执行brew install XXX即可;没有brew那就再装下brew;
2.解压下载好的protobuf-objectivec-3.3.0.zip,cd到相应的目录,依次执行:
$ ./configure
$ ./make
$ ./make check
$ ./sudo make install
再执行 objectivec/DevTools/full_mac_build.sh;
时间大概几分钟,执行完之后,会看见src目录下生成的protoc二进制文件;
3.使用protoc转换.proto文件(按照语法指南进行定义):



这是一个示例:protoc目录下,执行:protoc --proto_path=... --objc_out=... xxx.proto
其中proto_path是.proto文件所在目录,objc_out为OC文件输出的路径,xxx.proto是我们创建的proto文件,一次可以转多个,加在xxx.proto后面即可;



这里chat中描述的数据格式内容如下:

package cn.pio.protocol.protobuf;
option java_package = "cn.pio.protocol.protobuf";
//option java_multiple_files = true;
//类型
enum DeviceType {
	OTHER_DEVICE = 1;	//
	PC = 2;             //PC
	ANDROID = 3;        //安卓
	IOS = 4;            //IOS
	WEB = 5;            //WEB
	TERMINAL = 6 ;		//终端机
	SERVER = 9;			//server reply
}

enum PacketType {
	CHAT = 1;			//单聊消息,指定to为个人id
	GROUP_CHAT = 2 ;	//群聊消息,指定to为群id
	COMMAND = 3 ;		//命令,请求或者设置
	PRESENCE = 4;		//状态
	ERROR = 5 ;			//错误消息,更多指服务器返回的错误提示
}

message Packet {
	required Header header = 1;				//消息id
	optional string body = 2;				//消息内容
}

message Header	 {
	required string pid = 1;				//消息id
	optional string from = 2;				//发送者jie,如果是server则无from
	optional string to = 3;					//接受者用户id
	required DeviceType deviceType = 4;		//发送者设备类型
 	optional int64 time = 5;				//时间
 	required PacketType packetType = 6 ;	//消息类型
 	required int32 packetCode = 7 ;			//消息编码,指定消息类型下的唯一编码值
 	optional string domain = 8 ;			//服务域名号
}



 	

3.Protobuf在OC中的使用:


1.将生成的OC文件(Chat.pbobjc.h Chat.pbobjc.m)放到项目中,在ARC的项目中,需要设置.m文件的complier flags设置为-fno-objc-arc;
2.加入protobuf库:(两种方式)
1)一种使用CocoaPods集成;
2)把相关项目拖到项目中:将objectivec文件夹下的所有.h .m文件(除GPBProtocolBuffers.m外)以及整个google文件夹add到项目中;


ARC下,上述所有的.m都要把Compiler Flags设为-fno-objc-arc(CocoaPods集成的会自动添加);

3.编译:
这里可能会进行一些修改,除了添加complier flags外,可能还需要修改导入的头文件的路径(否则会提示路径找不到);
导入并修改之后的protobuf路径可能是这样的:



4.最后,导入头文件,测试使用:
        Header * header = [[Header alloc]init];
        header.pid = @"1_pid";
        header.from = @"1_from";
        header.to = @"1_to";
        header.deviceType = DeviceType_Ios;
        header.time = [NSDate date].timeIntervalSince1970;
        header.packetType = PacketType_Chat;
        header.packetCode = 1;
        header.domain = @"com.senyint.flower";
        
        Packet * packet = [[Packet alloc]init];
        packet.header = header;
        packet.body = @"这是flower的第一条测试消息,用于测试protobuf的使用。";

4.Socket简介及在OC中的使用:


关于iOS socket都在这里了》参看一下这篇文章,如果需要详细了解socket建议去找一些书籍、视频仔细看下。


5.使用Socket发送protobuf格式定义的数据:


这里使用的是CocoaAsyncSocket这个第三方库,直接将操作的Code贴在下边了,代码已经很明了了:

//
//  ViewController.m
//  HDMOCSocketProject
//
//  Created by 花强 on 2017/7/4.
//  Copyright © 2017年 花强. All rights reserved.
//

#import "ViewController.h"
#import "CocoaAsyncSocket.h"
#import "GCDAsyncSocket.h"

#import "Person.pbobjc.h"
#import "Chat.pbobjc.h"

@interface ViewController ()<GCDAsyncSocketDelegate>

@property (nonatomic , strong) GCDAsyncSocket * socket;

@property (nonatomic , strong) UILabel * labelShow;

@property (nonatomic , strong) UIButton * buttonConnect;
@property (nonatomic , strong) UIButton * buttonSend;
@property (nonatomic , strong) UIButton * buttonDisconnect;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    /*
     socket(套接字)是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。
     
     建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket,另一个运行于服务器端,称为ServerSocket。套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。
     
     Socket可以支持不同的传输层协议(TCP或UDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接,UDP连接同理。
     
     建立起一个TCP连接需要经过“三次握手”:
     
     第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
     
     第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
     
     第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
     
     三次握手(Three-way Handshake)即建立一个TCP连接时,需要客户端和服务器总共发送3个包。三次握手的目的是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号并交换TCP 窗口大小信息。在socket编程中,客户端执行connect()时,将触发三次握手。
     */
    [self.labelShow setFrame:CGRectMake(10, 20, 200, 100)];
    [self.view addSubview:self.labelShow];
    
    [self.buttonConnect setFrame:CGRectMake(10, 140, 100, 30)];
    [self.view addSubview:self.buttonConnect];
    
    [self.buttonSend setFrame:CGRectMake(10, 180, 100, 30)];
    [self.view addSubview:self.buttonSend];
    
    [self.buttonDisconnect setFrame:CGRectMake(10, 220, 100, 30)];
    [self.view addSubview:self.buttonDisconnect];
    
    [self startClientSocket];
    
}

-(void)startClientSocket{
    
    //创建Socket
    self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
}
-(void)connectSocket{
    if (!self.socket.isConnected) {
        NSString * host = @"43.241.229.134";
        int port = 24678;
        //链接
        NSError * error = nil;
        [_socket connectToHost:host onPort:port withTimeout:15 error:&error];
        if (error) {
            NSLog(@"%@",error);
            return;
        }
    }
}
-(void)sendMessageSocketWithMessage:(NSString *)msg{
    if (self.socket.isConnected) {
        /*
        Person *person = [[Person alloc] init];
        person.id_p = 111;
        person.username = @"test";
        person.phone = @"111111";
        NSData * data = [person data];
        Person *p = [Person parseFromData:data error:nil];
        NSLog(@"person:%@",p);
        
        //    NSData * data1 = [msg dataUsingEncoding:NSUTF8StringEncoding];
        [self.socket writeData:data withTimeout:15 tag:0];
         */
        Header * header = [[Header alloc]init];
        header.pid = @"1_pid";
        header.from = @"1_from";
        header.to = @"1_to";
        header.deviceType = DeviceType_Ios;
        header.time = [NSDate date].timeIntervalSince1970;
        header.packetType = PacketType_Chat;
        header.packetCode = 1;
        header.domain = @"com.senyint.flower";
        
        Packet * packet = [[Packet alloc]init];
        packet.header = header;
        packet.body = @"这是flower的第一条测试消息,用于测试protobuf的使用。";
     
        [self.socket writeData:[packet data] withTimeout:15 tag:1];
        
        self.labelShow.text = packet.body;
    }
}
-(void)disconnectSocket{
    if (self.socket.isConnected) {
        [self.socket disconnect];
    }
}

#pragma mark -
//链接成功
-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port{
    NSLog(@"%s",__func__);
    NSLog(@"链接成功");
}
//链接失败
-(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
    if (err) {
        NSLog(@"连接失败");
    }else{
        NSLog(@"正常断开");
    }
}
//发送数据成功
-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
    NSLog(@"%s",__func__);
    //发送完数据循环读取,-1不设置超时
    [sock readDataWithTimeout:-1 tag:tag];
}
//读取数据
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
    NSString * receiveStr = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"%s %@",__func__,receiveStr);
    
    [self.labelShow setText:[NSString stringWithFormat:@"%@ %@",self.labelShow.text,receiveStr]];
}



- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

-(void)buttonAction:(UIButton *)button{
    switch (button.tag) {
        case 1:
            //链接
            [self connectSocket];
            break;
        case 2:
            //发送
            [self sendMessageSocketWithMessage:self.labelShow.text];
            break;
        case 3:
            //断开
            [self disconnectSocket];
            break;
        default:
            break;
    }
}

-(UILabel *)labelShow{
    if (_labelShow == nil) {
        _labelShow = [[UILabel alloc]init];
        [_labelShow setText:@"Message"];
        [_labelShow setTextColor:[UIColor blueColor]];
        [_labelShow setTextAlignment:NSTextAlignmentLeft];
        [_labelShow setFont:[UIFont systemFontOfSize:14]];
        _labelShow.numberOfLines = 0;
    }
    return _labelShow;
}

-(UIButton *)buttonConnect{
    if (_buttonConnect == nil) {
        _buttonConnect = [UIButton buttonWithType:UIButtonTypeCustom];
        [_buttonConnect setBackgroundColor:[UIColor redColor]];
        [_buttonConnect setTitle:@"链接" forState:UIControlStateNormal];
        [_buttonConnect addTarget:self action:@selector(buttonAction:) forControlEvents:UIControlEventTouchUpInside];
        _buttonConnect.tag = 1;
    }
    return _buttonConnect;
}
-(UIButton *)buttonSend{
    if (_buttonSend == nil) {
        _buttonSend = [UIButton buttonWithType:UIButtonTypeCustom];
        [_buttonSend setBackgroundColor:[UIColor redColor]];
        [_buttonSend setTitle:@"发送" forState:UIControlStateNormal];
        [_buttonSend addTarget:self action:@selector(buttonAction:) forControlEvents:UIControlEventTouchUpInside];
        _buttonSend.tag = 2;
    }
    return _buttonSend;
}
-(UIButton *)buttonDisconnect{
    if (_buttonDisconnect == nil) {
        _buttonDisconnect = [UIButton buttonWithType:UIButtonTypeCustom];
        [_buttonDisconnect setBackgroundColor:[UIColor redColor]];
        [_buttonDisconnect setTitle:@"断开" forState:UIControlStateNormal];
        [_buttonDisconnect addTarget:self action:@selector(buttonAction:) forControlEvents:UIControlEventTouchUpInside];
        _buttonDisconnect.tag = 3;
    }
    return _buttonDisconnect;
}

@end



这里socket另一端返回的数据还未测通,待调试之后,将内容补充完整。


总结

网络一直都是核心,有关网络通讯的内容需要我们深入的学习,二十七八岁的年纪,加油!




  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值