500px技术周报003

概览

1、websocket深入

2、自动化编译打包推送

3、震动反馈,即点赞或者其他操作手机轻微震动

websocket深入

下面是SocketIO的使用方法,上一次已经讲过,只不过上次只是讲的各种用法,并未深入。

@import SocketIO;

NSString* socketUrlString = @"http://localhost:3000"";

NSDictionary* config = @{@"log":@NO,@"forcePolling":@YES,@"reconnects":@YES};

self.socketIOClient = [[SocketIOClient alloc] initWithSocketURL:[NSURL URLWithString:socketUrlString] config:config];

[self.socketIOClient on:@"connect" callback:^(NSArray * data, SocketAckEmitter * ack) {
NSLog(@"socketIOClientConnect");

}];


[self.socketIOClient on:@"disconnect" callback:^(NSArray * data, SocketAckEmitter * ack) {
NSLog(@"socketIOClientDisconnect");
NSLog(@"data = %@",data);

}];

[self.socketIOClient on:@"error" callback:^(NSArray * data, SocketAckEmitter * ack) {
NSLog(@"socketIOClientError");
NSLog(@"data = %@",data);

}];

[_socketIOClient on:@"reconnect" callback:^(NSArray * data, SocketAckEmitter * ack) {
NSLog(@"socketIOClientReconnect");
NSLog(@"data = %@",data);

}];

[self.socketIOClient on:@"message" callback:^(NSArray * data, SocketAckEmitter * ack) {
NSLog(@"socketIOClientMessage");
NSLog(@"data = %@",data);
NSString *string =data[0];
NSLog(@"Received \"%@\"", string);
}];

[_socketIOClient connect];

接上次的提问,forcePolling是干什么用的。由下面代码可以看到如果设置了forceWebsockets为@YES,则会走websocket,或者设置forcePolling为@NO,当请求接口http://localhost:3000/socket.io/?transport=polling&b64=1的时候会返回

97:0{"sid":"Sp2XOg8MbSc6A3O0AAAA","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":60000}

结果中带upgrades字段,里面有websocket,所有如果设置forcePolling为@NO则还是会走websocket。如果设为@YES则不走websocket,走的是http。

 /// Starts the connection to the server
    public func connect() {
        if connected {
            DefaultSocketLogger.Logger.error("Engine tried opening while connected. Assuming this was a reconnect", type: logType)
            disconnect(reason: "reconnect")
        }

        DefaultSocketLogger.Logger.log("Starting engine. Server: %@", type: logType, args: url)
        DefaultSocketLogger.Logger.log("Handshaking", type: logType)

        resetEngine()

        if forceWebsockets {
            polling = false
            websocket = true
            createWebsocketAndConnect()
            return
        }

        let reqPolling = NSMutableURLRequest(url: urlPolling)

        if cookies != nil {
            let headers = HTTPCookie.requestHeaderFields(with: cookies!)
            reqPolling.allHTTPHeaderFields = headers
        }

        if let extraHeaders = extraHeaders {
            for (headerName, value) in extraHeaders {
                reqPolling.setValue(value, forHTTPHeaderField: headerName)
            }
        }

        doLongPoll(for: reqPolling as URLRequest)
    }


    private func handleOpen(openData: String) {
        guard let json = try? openData.toNSDictionary() else {
            didError(reason: "Error parsing open packet")
            
            return
        }
        
        guard let sid = json["sid"] as? String else {
            didError(reason: "Open packet contained no sid")
            
            return
        }
        
        let upgradeWs: Bool
        
        self.sid = sid
        connected = true
        pongsMissed = 0
        
        if let upgrades = json["upgrades"] as? [String] {
            upgradeWs = upgrades.contains("websocket")
        } else {
            upgradeWs = false
        }
        
        if let pingInterval = json["pingInterval"] as? Double, let pingTimeout = json["pingTimeout"] as? Double {
            self.pingInterval = pingInterval / 1000.0
            self.pingTimeout = pingTimeout / 1000.0
        }
        
        if !forcePolling && !forceWebsockets && upgradeWs {
            createWebsocketAndConnect()
        }
        
        sendPing()
        
        if !forceWebsockets {
            doPoll()
        }
        
        client?.engineDidOpen(reason: "Connect")
    }

再来看看如果用http如果连接socket.io。

1、访问 http://localhost:3000/socket.io/?transport=polling&b64=1

97:0{"sid":"Sp2XOg8MbSc6A3O0AAAA","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":60000}

字符:右边的第一位代表状态,左边的数值97代表右侧字符的个数

@objc public enum SocketEnginePacketType : Int {
    case open, close, ping, pong, message, upgrade, noop
}

后面的sid是第二次连接需要带的参数。upgrades里面的内容上面有提到过。pingInterval是25秒,代表25秒内要发送心跳包。pingTimeout是心跳包超时时间。

2、第二次访问 http://localhost:3000/socket.io/?transport=polling&b64=1&sid=Sp2XOg8MbSc6A3O0AAAA

2:40

返回值4代表message

3、第三次访问 http://localhost:3000/socket.io/?transport=polling&b64=1&sid=Sp2XOg8MbSc6A3O0AAAA

此时就是连接状态别人发过socket就会收到。这时候需要再25秒内发送心跳包。前面访问链接都是get方式,发送心跳包要用post方式。代码如下

NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@&sid=%@",self.socketURLStr,self.sid]]];
    request.HTTPMethod=@"POST";
    request.HTTPBody = [@"1:2" dataUsingEncoding:NSUTF8StringEncoding];
    [request setValue:@"text/plain; charset=UTF-8" forHTTPHeaderField:@"Content-Type"];
    [request setValue:@(@"1:2".length).stringValue forHTTPHeaderField:@"Content-Length"];
    
    NSURLSession *session=[NSURLSession sharedSession];
    __weak __typeof__(self) weakSelf = self;
    NSURLSessionDataTask *task=[session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        __strong __typeof__(weakSelf) strongSelf = weakSelf;
        strongSelf.waitingForPost = NO;
    }];
    
    [task resume];

如果想要断开链接发送@”1:1”.

发送消息代码如下

- (void)emitWithMessage:(NSString*)message {
    if (!self.connected) {
        return;
    }
    NSString *tempMessage = [NSString stringWithFormat:@"42[\"message\",\"%@\"]",message];
    NSString *sendMessage = [NSString stringWithFormat:@"%@:%@",@(tempMessage.length),tempMessage];
    NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@&sid=%@",self.socketURLStr,self.sid]]];
    request.HTTPMethod=@"POST";
    request.HTTPBody = [sendMessage dataUsingEncoding:NSUTF8StringEncoding];
    [request setValue:@"text/plain; charset=UTF-8" forHTTPHeaderField:@"Content-Type"];
    [request setValue:@(sendMessage.length).stringValue forHTTPHeaderField:@"Content-Length"];
    
    NSURLSession *session=[NSURLSession sharedSession];
    __weak __typeof__(self) weakSelf = self;
    NSURLSessionDataTask *task=[session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        __strong __typeof__(weakSelf) strongSelf = weakSelf;
        strongSelf.waitingForPost = NO;
    }];
    
    [task resume];
}

以上就是http连接socket.io的过程,我已经整理成oc代码QGSocketIO-objc,在微信上运行了一段时间,效果还可以。

自动化编译打包推送

目前采用crontab定时任务,自动触发一个shell脚本,执行go编译好的程序。

crontab

几个简单例子

每天早上6点 
0 6 * * * echo "Good morning." >> /tmp/test.txt //注意单纯echo,从屏幕上看不到任何输出,因为cron把任何输出都email到root的信箱了。

每两个小时 
0 */2 * * * echo "Have a break now." >> /tmp/test.txt  

晚上11点到早上8点之间每两个小时和早上八点 
0 23-7/2,8 * * * echo "Have a good dream" >> /tmp/test.txt

每个月的4号和每个礼拜的礼拜一到礼拜三的早上11点 
0 11 4 * 1-3 command line

1月1日早上4点 
0 4 1 1 * command line SHELL=/bin/bash PATH=/sbin:/bin:/usr/sbin:/usr/bin MAILTO=root //如果出现错误,或者有数据输出,数据作为邮件发给这个帐号 HOME=/ 

每小时执行/etc/cron.hourly内的脚本
01 * * * * root run-parts /etc/cron.hourly
每天执行/etc/cron.daily内的脚本
02 4 * * * root run-parts /etc/cron.daily 

每星期执行/etc/cron.weekly内的脚本
22 4 * * 0 root run-parts /etc/cron.weekly 

每月去执行/etc/cron.monthly内的脚本 
42 4 1 * * root run-parts /etc/cron.monthly 

注意: "run-parts"这个参数了,如果去掉这个参数的话,后面就可以写要运行的某个脚本名,而不是文件夹名。   

每天的下午4点、5点、6点的5 min、15 min、25 min、35 min、45 min、55 min时执行命令。 
5,15,25,35,45,55 16,17,18 * * * command

每周一,三,五的下午3:00系统进入维护状态,重新启动系统。
00 15 * * 1,3,5 shutdown -r +5

每小时的10分,40分执行用户目录下的innd/bbslin这个指令: 
10,40 * * * * innd/bbslink 

每小时的1分执行用户目录下的bin/account这个指令: 
1 * * * * bin/account

每天早晨三点二十分执行用户目录下如下所示的两个指令(每个指令以;分隔): 
20 3 * * * (/bin/rm -f expire.ls logins.bad;bin/expire$#@62;expire.1st)  

每年的一月和四月,4号到9号的3点12分和3点55分执行/bin/rm -f expire.1st这个指令,并把结果添加在mm.txt这个文件之后(mm.txt文件位于用户自己的目录位置)。 
12,55 3 4-9 1,4 * /bin/rm -f expire.1st$#@62;$#@62;mm.txt 

相关命令


crontab ipaCrontab //执行某个文件里的crontab
crontab -r         //删除所有的定时任务
crontab -l         //列出所有的定时任务


项目中ipaCrontab的内容如下

0 */2 * * * ~/creatIpa/run.sh >> ~/creatIpa/run.log 2>&1

2个小时运行一下run.sh并将日志输出到文件~/creatIpa/run.log里面

run.sh里面的内容如下

#!/usr/bin/env bash

export PATH=$PATH:/usr/local/bin
export TZ=Asia/Shanghai

echo "*********************************"
echo "*********************************"
date
echo "*********************************"
echo "*********************************"

cd ~/creatIpa && ./AutoRealeseIPA 

echo "*********************************"
echo "*********************************"
date
echo "*********************************"
echo "*********************************"

主要是打印分隔符 执行时间,结束时间。cd到某个文件夹下执行AutoRealeseIPA

AutoRealeseIPA

AutoRealeseIPA由GO语言实现,实现的内容主要有

环境检测

1、检查Xcode是否安装。

2、PackageApplication是否安装

代码更新

1、忽略本地所有修改

git checkout .

2、切换到某个指定的分支

git checkout develop

3、拉代码

git pull

4、检测返回结果中是否包含Already up-to-date,如果包含则是最新代码,无需执行之后内容,如果不包含则有代码更新并且已经更新

5、第三方库安装

pod install

检测有无需要发布的提交信息

git log  --grep=\"||||\" --pretty=%B --since=00:00:00

这里需要记录最后一次日志,每次定时任务会对比此次的结果的最新日志与保存的是否一致,如果一致说明此次定时任务并没有需要打包内容。

更新项目plist文件中对应的小版本号

程序会先访问本地是否有记录的版本号,如果没有则为70914.1此种格式,如果有则.后面的数字加一

编译打包

1、删除上次编译内容

rm -rf */build   //*为项目的位置

2、编译

xcodebuild clean &&  xcodebuild -workspace %s.xcworkspace -scheme %s -sdk iphoneos -configuration Release -derivedDataPath build

其中%s为项目名称

3、打包

xcrun -sdk iphoneos PackageApplication -v %s -o %s 

第一个参数为app的位置,第二个参数为ipa的位置

上传

这里需要上传3个文件ipa、plist、html。有了这三个文件才能完成用户点击安装的动作

微信群通知

需要调用已经定义好的接口发送消息,消息内容包括下载地址和更新日志

错误排查

shell脚本手动执行并无报错,crontab执行报错如下

exit status 65
=== CLEAN TARGET umengPush OF PROJECT 500px WITH THE DEFAULT CONFIGURATION (Release) ===

Check dependencies
[BCEROR]No signing certificate "iOS Development" found:  No "iOS Development" signing certificate matching team ID "Y8NGAST6D5" with a private key was found.


需要钥匙串中开发证书从登录中拷贝到系统中即可。

震动反馈

自iOS10开始,iPhone7和iPhone7 plus开始支持了触摸震动反馈。iOS共有三个类,下面是详细说明。 UINotificationFeedbackGenerator:Indicates that a task or action, such as depositing a check or unlocking a vehicle, has completed, failed, or produced a warning of some kind.

UIImpactFeedbackGenerator:Provides a physical experience that complements the visual feedback for an action or task. For example, the user might feel a thud when a view slides into place or two objects collide.

UISelectionFeedbackGenerator:Indicates that the selection is actively changing. For example, the user feels light taps while scrolling a picker wheel.

第一种是通知类型,参考网易新闻在没网的时候下拉数据失败的震动

第二种是撞击类型,参考网易新闻在下拉时地球动画中大圈套住地球时候的震动

第三种是选择类型,选择日期等时候会有这种提示

var generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
generator.impactOccurred()
 
generator.notificationOccurred(.warning)
generator.impactOccurred()


var generator = UIImpactFeedbackGenerator(style: .light)
generator.impactOccurred()
 
generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred()
 
generator = UIImpactFeedbackGenerator(style: .heavy)
generator.impactOccurred()



let generator = UISelectionFeedbackGenerator()
generator.selectionChanged()

另外在iPhone6 上你可以通过以下方式:

import AudioToolbox
 
AudioServicesPlaySystemSound(1519) // Actuate `Peek` feedback (weak boom)
AudioServicesPlaySystemSound(1520) // Actuate `Pop` feedback (strong boom)
AudioServicesPlaySystemSound(1521) // Actuate `Nope` feedback (series of three weak booms)

其他机型请忽略。

参考网址:http://www.mikitamanko.com/blog/2017/01/29/haptic-feedback-with-uifeedbackgenerator/ https://developer.apple.com/library/content/releasenotes/General/WhatsNewIniOS/Articles/iOS10.html

Written on September 14, 2017