前言
写上一篇文章之前完全是想总结一下自己对socket通讯流程的总结,加深自己的印象,没有想到会有很多人关注这一块,再说自己是个socket的新手,写出来的文章没有多少人去看,所以上一篇文章写得不是很详细,但是发现还是有很多人关注,所以我就趁今天是个周末,花个2,3小时来总结一下本周socket通讯的进程,供想要了解的朋友参考。
正文
首先提一下网上CocoaAsyncSocket
框架主要包括AsyncSocket
和GCDSocket
,我使用的是后者,就是多线程Socket,主要区别就是前者基于NSRunLoop
,后者是在多线程进行,据我项目目前的情况来看,GCDSocket
内至少开辟了5个线程。
自己项目目前的进度是将socket单独封装成一个单独的类,也就是写成一个单例类,这样写的好处显而易见,这样我们建立通讯连接,数据请求就方便了很多,因为我们不可能去每一个需要数据的界面去创建socket进行连接,想办法把问题简单化使我们程序员必须做的重要一部分。想必网上关于socket
的文章大多数都是大家你抄我我抄你而来,写一下socket创建,建立连接,实现代理方法,收发数据,没有更深一步的文章。其次大家有没有这样的一个疑惑,网上为什么没有开源的关于socket通讯的集成好的第三方框架供我使用呢?我直接收发数据就好了,还要那么麻烦建立连接,一堆问题去处理。像普通的网络数据请求,网上有封装好的AFNetworking
,为什么socket没有!!!那我来告诉你基于socket的TCP的长连接往往数据传输协议是自定义的,所以这个不可能有现成的框架来用,必须根据自己的定义类型来收发数据,否则就无法解析。举个例子,我收数据需要这样的格式[^1^2]
,那当我收到数据data
我必须得按这种格式校验,否则我就无法收到。上面首先将socket的逻辑说清楚,下面我们上代码,首先说明一点,我这个单例封装的我个人觉得比较完美,跟普通网络请求数据那种格式一某一样,最大的不同我是采用代理的方式回传数据,而不是block
。
#import "GCDAsyncSocket.h"
@protocol GPSSocketServeDelegate <NSObject>
/*** 连接服务器成功以后回调 */
- (void)connectSeverSucess:(NSString *)sucess;
/*** 登录返回判断 */
- (void)ClickIsSucess:(BOOL)isSucess StrParam2:(NSString *)strParam2;
/*** 返回请求数据 */
- (void)ClintReceCommData:(NSMutableArray *)data StrDataType:(NSString *)strDataType strParam2:(NSString *)strParam2;
@end
@class GPSSocketServeDelegate;
@interface GPSSocketServe : NSObject <GCDAsyncSocketDelegate>
@property (nonatomic,weak) id<GPSSocketServeDelegate>delegate;
@property (nonatomic,strong) GCDAsyncSocket *socket;
//在.h文件里面我给出了以下6个接口,建立连接,断开连接,其他的就是请求数据接口的封装
/*** 获取本类对象 */
+ (GPSSocketServe *)sharedSocketServe;
/*** socket连接 */
- (void)startConnectSocket;
/*** 断开socket开始连接 */
- (void)disConnectSocket;
/**
* 登录接口
*
* @param username 用户名
* @param password 用户密码
*/
- (void)userClick:(NSString *)username UserPassword:(NSString *)password;
/**
* 用户登录调第一集部门表
*
* @param p_strManagerCode 为用户的最高部门code
* @param p_strWGLoginName 用户名称
*/
- (void)requestManagerDep:(NSString *)p_strManagerCode P_strWGLoginName:(NSString *)p_strWGLoginName;
/**
* 用户调第二级以及以后的部门表
*
* @param p_strManagerCode 为当前级的部门code
* @param p_strCurrentDepName 为当前级的部门名称
*/
- (void)requestSencondManagerDep:(NSString *)p_strCurrentDepCode P_strCurrentDepName:(NSString *)p_strCurrentDepName;
/**
* 调查询部门下的车辆列表
*
* @param p_strManagerCode 为当前级的部门code
* @param p_strCurrentDepName 为当前级的部门名称
*/
- (void)requestCarsOfDep:(NSString *)p_strCurrentDepCode P_strCurrentDepName:(NSString *)p_strCurrentDepName;
GPSSocketServe.m文件,接口的实现
//创建单例对象,重写allocWithZone方法,保证这个对象在内存中只有一份
+ (GPSSocketServe *)sharedSocketServe{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
socketServe = [[self alloc] init];
});
return socketServe;
}
+(id)allocWithZone:(NSZone *)zone
{
@synchronized(self)
{
if (socketServe == nil)
{
socketServe = [super allocWithZone:zone];
return socketServe;
}
}
return nil;
}
- (void)startConnectSocket
{
//这个里面主要就是创建socket对象,建立连接,代码就不考了,可以参照上一篇
}
然后我说说我主要遇到的问题吧
1.原始的收到数据和我发送数据是在两个不同的方法里面,我们如何回传数据?
2.当我调用一级部门表的时候,发现收不到回传数据,然后一步一步的调,发现我发送的数据少5个字符长度,这个哪里出了问题?
关于第一个问题,其实我第一个感觉就是使用block,这样呢回传数据感觉很方便,例如当我发送登陆,就可以收到回调成功然后进行跳转,当时我已经成功实现了block回传数据,大概写一下实现的方法
//定义一个全局的block
//原因:发送请求和收到数据的不在一个方法里面
//定义一个block,申明一个属性,这个block带两个参数,一个是类型的字符串,另一个便是回传数据
typedef void(^requestDataBlock)(NSString *string,NSMutableArray *data);
@property (nonatomic,copy) requestDataBlock dataBlock;
//发送数据的同一接口
- (void)ClintSendCommData:(short)intDataType strDataType:(NSString *)strDataType stSetType:(NSString *)stSetType strSetSN:(NSString *)strSetSN strSetSN1:(NSString *)strSetSN1 strAlmComType:(NSString *)strAlmComType strHisType:(NSString *)strHisType strPosType:(NSString *)strPosType strFadeType:(NSString *)strFadeType strRecogType:(NSString *)strRecogType strRecogType1:(NSString *)strRecogType1 StrParam1:(NSString *)strParam1 StrParam2:(NSString *)strParam2 StrParam3:(NSString *)strParam3 StrParam4:(NSString *)strParam4 StrParam5:(NSString *)strParam5 StrParam6:(NSString *)strParam6 StrParam7:(NSString *)strParam7 StrParam8:(NSString *)strParam8
//在这个方法后面添加上block `success:(^requestDataBlock)(NSString *string,NSMutableArray *data)`
//在这个方法里面self.dataBlock = requestDataBlock;
//再接收数据的方法里面回调block
self.dataBlock(参数一,返回数据);
这样的话就实现了block回调数据,比较容易,前提是你对block足够的了解。
可是为什么我放弃了这个方法了,因为1.代码的冗余率太多,因为是多线程,当我更新UI我必须回到主线程,这个代码得多大一块;而且2.可扩展性不好,这个登录接口需要回传一个参数,调用需要回传多个参数,这样共用性不好,所以我就想到了代理,不同的接口我调用不同的代理方法,以后再有新的类型回传数据,我大不了再写一个回传接口而已。回调函数请看上面。
//这个就是后台提供给我的登录接口,这写也是用Java写的,不过我已经将他们改成oc的方法
ClintSendCommData(1105, "0002", "", "", "", "", "", "", "", "", "", p_strWGLoginName,p_strWGPassword, "", "", "", "", "", "");
大家一看这调用接口,需要传的参数就两个,所以你们想到了什么?反正我想到的是再封装一层
- (void)userClick:(NSString *)username UserPassword:(NSString *)password
{
[self ClintSendCommData:1105 strDataType:@"0002" stSetType:@"" strSetSN:@"" strSetSN1:@"" strAlmComType:@"" strHisType:@"" strPosType:@"" strFadeType:@"" strRecogType:@"" strRecogType1:@"" StrParam1:username StrParam2:password StrParam3:@"" StrParam4:@"" StrParam5:@"" StrParam6:@"" StrParam7:@"" StrParam8:@""];
}
这样我就调用这个方法就OK,其他的调用数据的方法类似。
第二个问题出在什么地方呢?其实还是跟上一篇文章编码有关,后台服务器采用的是GB2312
,我将它转换为UTF-8
了,在发送数据的时候
//自定义发送数据接口,底层其实是调用发送数据的方法,writedata:,我只不过封装了一层
[self SendData:intDataType CharDatahead:(char *)[strData cStringUsingEncoding:enc] DataLen:intDataLen];
前面提到,我发送时候少了5个字符,这是跟我发送的里面包含了汉字,汉字的一般占用2个字符,而我们普通计算这个长度length当做一个字符来算,所以intDataLen
计算是不对的,登录接口之所以对,那是因为没有汉字,我首先在计算长度的NSString的方法里面没有找到相应的方法,不知道有没有朋友找到,有找到的可以留言给我,非常感谢!那我说说我在网上找到的计算包含汉字的方法(其实这个方法是经过我改造过的方法)
/**
* 计算包含中文的字符的字符串长度
*/
-(int)lengthOfStringContainChinese:(NSString*)c{
int strlength = 0;
char* p = (char*)[c cStringUsingEncoding:NSUnicodeStringEncoding];
for (int i=0 ; i<[c lengthOfBytesUsingEncoding:NSUnicodeStringEncoding] ;i++) {
if (*p) {
p++;
strlength++;
}
else {
p++;
}
}
return strlength;
}
这样计算就正确了。
以上是我在解决的主要问题,其实我在真机调试的时候,发现了一个比较严重的问题,在网络请求的时候,网络不好,会出现严重的内存暴增,程序就会闪退,不过这个问题我已经解决掉了,通过的是调试工具,这个下一期我会教大家调试的方法,首先科普一下,手机内存一般达到30M的话就会自动闪退,有遇到这个问题的朋友可以仔细研究研究。