iOS 升级HTTPS通过ATS你所要知道的

苹果强制升级的HTTPS不仅仅是在接口HTTP上加个S那么简单:
它所有满足的是iOS9中新增App Transport Security(简称ATS)特性:
那满足ATS我们需要做什么呢
1.必须是苹果信任的CA证书机构颁发的证书
2.后台传输协议必须满足: TLS1.2 (这很重要, 后面的自制证书满足这个条件是前提)
3.签字算法只能是下面的一种:

TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA

4.证书必须使用SHA256或者更好的哈希算法进行签名,要么是2048位或者更长的RSA密钥,要么就是256位或更长的ECC密钥。

目前有两种升级到HTTPS得方法:
1.第三方认证的颁发CA证书(推荐)
2.自己制作证书(这种不知道能不能满足苹果的审核)

一: 第三方认证的颁发CA证书

证书到底长什么样子呢? 取个栗子:
大家请打开https://www.baidu.com
然后看到

百度的证书分析

那些证书机构颁发的证书能用:苹果官方信任证书

收费SSL证书: 网上百度一大把, 收费还挺贵的,自己可以多找几个对比一下
免费SSL证书: 除了收费的CA证书机构, 你还可以去腾讯云申请免费的SSL证书, 教程免费在腾讯云申请SSL证书的方法
沃通(WoSign)免费的SSL证书最近被苹果封杀了, 能不能用大家可以看一下苹果的公告: 您的苹果手机轻点“设置”>“通用”>“关于本机”>”证书信任设置”>”进一步了解被信任的证书”去了解

检测你的接口是否满足苹果的ATS要求, 有以下两种方法:

1.腾讯云提供的检测页面检测

腾讯云的检测页面

2 终端输入 nsurl --ats-diagnostics --verbose 你的接口地址
大家可以参考这篇文章,里面的说的很明白:
关于iOS9中的App Transport Security相关说明及适配(更新于2016.7.1)
里面会详细说明你的证书哪点不符合ATS要求
当然下面自己制作证书去实现HTTPS的,检测不通过的,所以我觉得审核会被拒
这种方法配置好了, 在手机端就什么都不用配置就可以请求了

二: 自己制作证书

苹果官方信任证书里说到有三种证书:

1 可信的根证书用于建立信任链,以验证由可信的根签署的其他证书,例如,与 Web 服务器建立安全连接。当 IT 管理员创建 iPhone、iPad 或 iPod touch 的配置描述文件时,无需提供这些可信的根证书。
2 始终询问的证书不受信任,但不受阻止。使用其中一个证书时,系统将提示您选择是否信任该证书。
3 已阻止的证书视为被盗用,将不再受信任。

自制证书我觉得应该就是属于第二种情况, 所以这种方法我也不知道能不能通过苹果的审核, 只是提供一个方法给大家参考, 看到网上有人说可以,有人说不可以, 不到1月1号,自己没试过都不敢说大话
这种方式拿到后台的接口用谷歌浏览器打开跟百度的证书是有区别的

自己制作证书

很明显没有绿锁, 当打开的时候会询问是否连接这个不受信任的连接才会进一步打开, 下面就来一步步的实现(包括怎么制作证书)
iOS使用自签名证书实现HTTPS请求
iOS Https协议 自签证书访问数据参考这个例子的时候,博主自带的Demo AFN框架请求不了数据, 我用了最新AFN版本的成功返回数据
还可以参考一下
iOS 10 适配 ATS app支持https通过App Store审核

我在利用原生的代码测试时遇到的问题

@interface ViewController () <NSURLSessionDelegate>

@end

@implementation ViewController

- (void)viewDidLoad {


}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];

   NSURLSessionDataTask *task =  [session dataTaskWithURL:[NSURL URLWithString:@"https://www.baidu.com"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
    }];
   [task resume];
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
    NSLog(@"接收到服务器响应");
    //注意:这里需要使用completionHandler回调告诉系统应该如何处理服务器返回的数据
    //默认是取消
    /**
     NSURLSessionResponseCancel = 0,            默认的处理方式,取消
     NSURLSessionResponseAllow = 1,             接收服务器返回的数据
     NSURLSessionResponseBecomeDownload = 2,    变成一个下载请求
     NSURLSessionResponseBecomeStream           变成一个流
     */
    completionHandler(NSURLSessionResponseAllow);
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data {
    NSLog(@"获取到服务段数据");
    NSLog(@"%@",[self jsonToDictionary:data]);
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error {
    NSLog(@"请求完成%@", error);
}

- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {
    NSLog(@"证书认证");
    if ([[[challenge protectionSpace] authenticationMethod] isEqualToString: NSURLAuthenticationMethodServerTrust]) {
        do
        {
            SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
            NSCAssert(serverTrust != nil, @"serverTrust is nil");
            if(nil == serverTrust)
                break; /* failed */
            /**
             *  导入多张CA证书(Certification Authority,支持SSL证书以及自签名的CA),请替换掉你的证书名称
             */
            NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"ca" ofType:@"cer"];//自签名证书
            NSData* caCert = [NSData dataWithContentsOfFile:cerPath];

            NSCAssert(caCert != nil, @"caCert is nil");
            if(nil == caCert)
                break; /* failed */

            SecCertificateRef caRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)caCert);
            NSCAssert(caRef != nil, @"caRef is nil");
            if(nil == caRef)
                break; /* failed */

            //可以添加多张证书
            NSArray *caArray = @[(__bridge id)(caRef)];

            NSCAssert(caArray != nil, @"caArray is nil");
            if(nil == caArray)
                break; /* failed */

            //将读取的证书设置为服务端帧数的根证书
            OSStatus status = SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)caArray);
            NSCAssert(errSecSuccess == status, @"SecTrustSetAnchorCertificates failed");
            if(!(errSecSuccess == status))
                break; /* failed */

            SecTrustResultType result = -1;
            //通过本地导入的证书来验证服务器的证书是否可信
            status = SecTrustEvaluate(serverTrust, &result);
            if(!(errSecSuccess == status))
                break; /* failed */
            NSLog(@"stutas:%d",(int)status);
            NSLog(@"Result: %d", result);

            BOOL allowConnect = (result == kSecTrustResultUnspecified) || (result == kSecTrustResultProceed);
            if (allowConnect) {
                NSLog(@"success");
            }else {
                NSLog(@"error");
            }

            /* kSecTrustResultUnspecified and kSecTrustResultProceed are success */
            if(! allowConnect)
            {
                break; /* failed */
            }

#if 0
            /* Treat kSecTrustResultConfirm and kSecTrustResultRecoverableTrustFailure as success */
            /*   since the user will likely tap-through to see the dancing bunnies */
            if(result == kSecTrustResultDeny || result == kSecTrustResultFatalTrustFailure || result == kSecTrustResultOtherError)
                break; /* failed to trust cert (good in this case) */
#endif

            // The only good exit point
            NSLog(@"信任该证书");

            NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
            return [[challenge sender] useCredential: credential
                          forAuthenticationChallenge: challenge];

        }
        while(0);
    }

    // Bad dog
    NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
    completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge,credential);
    return [[challenge sender] cancelAuthenticationChallenge: challenge];
}

- (NSDictionary *)jsonToDictionary:(NSData *)jsonData {
    NSError *jsonError;
    NSDictionary *resultDic = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableLeaves error:&jsonError];
    return resultDic;
}

@end

下面说说我在配置自己制作证书过程中遇到的问题:
1.转换证书: 把后台给你的.crt证书转化为.cer后缀
终端命令行openssl x509 -in 你的证书.crt -out 你的证书.cer -outform der

2.利用系统的方法来不到- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler { NSLog(@"证书认证"); }这个方法的时候, 是因为后台的传输协议还没升级到TLS1.2, 叫后台升级后就可以来到验证证书的这个方法了.

3.拖入证书读取不出证书数据
参考: https的证书错误,错误码-1012问题及解决方案

SDWebImage: 项目中大家用到AFN请求网络数据, 升级验证SSL证书的方案相信你看完上面的参考文章已经没问题了, 我给出的代码, 自定义网络请求也没问题了, 还有就是SDWebImage框架的请求HTTPS的图片时,大家可以绕过证书验证去加载图片[imageView sd_setImageWithURL:[NSURL URLWithString:urlString] placeholderImage:self.placeholder options:SDWebImageAllowInvalidSSLCertificates];

恩, 这就是这几天升级HTTPS觉得有帮助的参考和总结.希望帮到你

 

文/床前明月_光(简书作者)
原文链接:http://www.jianshu.com/p/2d72ef8dbf5a
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

Linux思维导图整理

前段时间整理了一个Java的思导图,今天整理了一下收集的Linux思维导图。

PS.图片都是在学习中或者偶然看到的,感谢感谢,因此我会附上图片的来源,如有侵权,请联系我,谢谢。

Linux学习路径:

Linux知识脑图.png

Linux桌面介绍:

Linux桌面环境.png

FHS(文件系统目录标准)

FHS.png

以上三张图,都是在学习实验楼上的课程——Linux 基础入门 ,教程里面看到的。

Linux需要特别注意的目录

Linux需要特别注意的目录.gif

这是在搜索的时候,偶然看到的,来源是:internetcn.net

linux 内核学习路线

Linux.png

这个我是在简书上看到的,地址:点这里

Linux Security Coaching

Linux Security Coaching.png

这个是在github上看到的,地址: Linux Security Coaching

Linux命令参考

Linux命令参考.jpg

上面有地址信息,就是:linuxTOY.org

另一张Linux命令速查表

Linux命令.png

这个,我是真的没记住,只记得当时看一篇外文,偶然看到的,然后就下载了下来,(>_<)

以上,如果你还有什么好的Linux思导图,希望可以私信我啊,我添加进来…

 

文/小柑(简书作者)
原文链接:http://www.jianshu.com/p/59f759207862
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

如何实现1080P延迟低于500ms的实时超清直播传输技术

最近由于公司业务关系,需要一个在公网上能实时互动超清视频的架构和技术方案。众所周知,视频直播用 CDN + RTMP 就可以满足绝大部分视频直播业务,我们也接触了和测试了几家 CDN 提供的方案,单人直播没有问题,一旦涉及到多人互动延迟非常大,无法进行正常的互动交谈。对于我们做在线教育的企业来说没有互动的直播是毫无意义的,所以我们决定自己来构建一个超清晰(1080P)实时视频的传输方案。

先来解释下什么是实时视频,实时视频就是视频图像从产生到消费完成整个过程人感觉不到延迟,只要符合这个要求的视频业务都可以称为实时视频。关于视频的实时性归纳为三个等级:

  • 伪实时:视频消费延迟超过 3 秒,单向观看实时,通用架构是 CDN + RTMP + HLS,现在基本上所有的直播都是这类技术。
  • 准实时: 视频消费延迟 1 ~ 3 秒,能进行双方互动但互动有障碍。有些直播网站通过 TCP/UDP + FLV 已经实现了这类技术,YY 直播属于这类技术。
  • 真实时:视频消费延迟 < 1秒,平均 500 毫秒。这类技术是真正的实时技术,人和人交谈没有明显延迟感。QQ、微信、Skype 和 WebRTC 等都已经实现了这类技术。

市面上大部分真实时视频都是 480P 或者 480P 以下的实时传输方案,用于在线教育和线上教学有一定困难,而且有时候流畅度是个很大的问题。在实现超清晰实时视频我们做了大量尝试性的研究和探索,在这里会把大部分细节分享出来。

要实时就要缩短延迟,要缩短延迟就要知道延迟是怎么产生的,视频从产生、编码、传输到最后播放消费,各个环节都会产生延迟,总体归纳为下图:

( 点击图片可以全屏缩放)

成像延迟,一般的技术是毫无为力的,涉及到 CCD 相关的硬件,现在市面上最好的 CCD,一秒钟 50 帧,成像延迟也在 20 毫秒左右,一般的 CCD 只有 20 ~ 25 帧左右,成像延迟 40 ~ 50 毫秒。

编码延迟,和编码器有关系,在接下来的小结介绍,一般优化的空间比较小。

我们着重针对网络延迟和播放缓冲延迟来进行设计,在介绍整个技术细节之前先来了解下视频编码和网络传输相关的知识和特点。

一、视频编码那些事

我们知道从 CCD 采集到的图像格式一般的 RGB 格式的(BMP),这种格式的存储空间非常大,它是用三个字节描述一个像素的颜色值,如果是 1080P 分辨率的图像空间:1920 x 1080 x 3 = 6MB,就算转换成 JPG 也有近 200KB,如果是每秒 12 帧用 JPG 也需要近 2.4MB/S 的带宽,这带宽在公网上传输是无法接受的。

视频编码器就是为了解决这个问题的,它会根据前后图像的变化做运动检测,通过各种压缩把变化的发送到对方,1080P 进行过  H.264 编码后带宽也就在 200KB/S ~ 300KB/S 左右。在我们的技术方案里面我们采用 H.264 作为默认编码器(也在研究 H.265)。

1.1 H.264 编码

前面提到视频编码器会根据图像的前后变化进行选择性压缩,因为刚开始接收端是没有收到任何图像,那么编码器在开始压缩的视频时需要做个全量压缩,这个全量压缩在 H.264 中 I 帧,后面的视频图像根据这个I帧来做增量压缩,这些增量压缩帧叫做 P 帧,H.264 为了防止丢包和减小带宽还引入一种双向预测编码的 B 帧,B 帧以前面的 I 或 P 帧和后面的 P 帧为参考帧。H.264 为了防止中间 P 帧丢失视频图像会一直错误它引入分组序列(GOP)编码,也就是隔一段时间发一个全量 I 帧,上一个 I 帧与下一个 I 帧之间为一个分组 GOP。它们之间的关系如下图:

PS:在实时视频当中最好不要加入 B 帧,因为 B 帧是双向预测,需要根据后面的视频帧来编码,这会增大编解码延迟。

1.2 马赛克、卡顿和秒开

前面提到如果 GOP 分组中的P帧丢失会造成解码端的图像发生错误,其实这个错误表现出来的就是马赛克。因为中间连续的运动信息丢失了,H.264 在解码的时候会根据前面的参考帧来补齐,但是补齐的并不是真正的运动变化后的数据,这样就会出现颜色色差的问题,这就是所谓的马赛克现象,如图:

这种现象不是我们想看到的。为了避免这类问题的发生,一般如果发现 P 帧或者 I 帧丢失,就不显示本 GOP 内的所有帧,直到下一个 I 帧来后重新刷新图像。但是 I 帧是按照帧周期来的,需要一个比较长的时间周期,如果在下一个 I 帧来之前不显示后来的图像,那么视频就静止不动了,这就是出现了所谓的卡顿现象。如果连续丢失的视频帧太多造成解码器无帧可解,也会造成严重的卡顿现象。视频解码端的卡顿现象和马赛克现象都是因为丢帧引起的,最好的办法就是让帧尽量不丢

知道 H.264 的原理和分组编码技术后所谓的秒开技术就比较简单了,只要发送方从最近一个 GOP 的 I 帧开发发送给接收方,接收方就可以正常解码完成的图像并立即显示。但这会在视频连接开始的时候多发一些帧数据造成播放延迟,只要在接收端播放的时候尽量让过期的帧数据只解码不显示,直到当前视频帧在播放时间范围之内即可。

1.3 编码延迟与码率

前面四个延迟里面我们提到了编码延迟,编码延迟就是从 CCD 出来的 RGB 数据经过 H.264 编码器编码后出来的帧数据过程的时间。我们在一个 8 核 CPU 的普通客户机测试了最新版本 X.264 的各个分辨率的延迟,数据如下:


从上面可以看出,超清视频的编码延迟会达到 50ms,解决编码延迟的问题只能去优化编码器内核让编码的运算更快,我们也正在进行方面的工作。

在 1080P 分辨率下,视频编码码率会达到 300KB/S,单个 I 帧数据大小达到 80KB,单个 P 帧可以达到 30KB,这对网络实时传输造成严峻的挑战。

二、网络传输质量因素

实时互动视频一个关键的环节就是网络传输技术,不管是早期 VoIP,还是现阶段流行的视频直播,其主要手段是通过 TCP/IP 协议来进行通信。但是 IP 网络本来就是不可靠的传输网络,在这样的网络传输视频很容易造成卡顿现象和延迟。先来看看 IP 网络传输的几个影响网络传输质量关键因素。

2.1 TCP 和 UDP

对直播有过了解的人都会认为做视频传输首选的就是 TCP + RTMP,其实这是比较片面的。在大规模实时多媒体传输网络中,TCP 和 RTMP 都不占优势。TCP 是个拥塞公平传输的协议,它的拥塞控制都是为了保证网络的公平性而不是快速到达,我们知道,TCP 层只有顺序到对应的报文才会提示应用层读数据,如果中间有报文乱序或者丢包都会在 TCP 做等待,所以 TCP 的发送窗口缓冲和重发机制在网络不稳定的情况下会造成延迟不可控,而且传输链路层级越多延迟会越大。

关于 TCP 的原理:

http://coolshell.cn/articles/11564.html

关于 TCP 重发延迟:

http://weibo.com/p/1001603821691477346388

在实时传输中使用 UDP 更加合理,UDP 避免了 TCP 繁重的三次握手、四次挥手和各种繁杂的传输特性,只需要在 UDP 上做一层简单的链路 QoS 监测和报文重发机制,实时性会比 TCP 好,这一点从 RTP 和 DDCP 协议可以证明这一点,我们正式参考了这两个协议来设计自己的通信协议。

2.2 延迟

要评估一个网络通信质量的好坏和延迟一个重要的因素就是 Round-Trip Time(网络往返延迟),也就是 RTT。评估两端之间的 RTT 方法很简单,大致如下:

  1. 发送端方一个带本地时间戳 T1 的 ping 报文到接收端;
  2. 接收端收到 ping 报文,以 ping 中的时间戳 T1 构建一个携带 T1 的 pong 报文发往发送端;
  3. 发送端接收到接收端发了的 pong 时,获取本地的时间戳 T2,用 T2 – T1 就是本次评测的 RTT。

示意图如下:

( 点击图片可以全屏缩放)

上面步骤的探测周期可以设为 1 秒一次。为了防止网络突发延迟增大,我们采用了借鉴了 TCP 的 RTT 遗忘衰减的算法来计算,假设原来的 RTT 值为 rtt,本次探测的 RTT 值为 keep_rtt。那么新的 RTT 为:

new_rtt = (7 * rtt + keep_rtt) / 8

可能每次探测出来的 keep_rtt 会不一样,我们需要会计算一个 RTT 的修正值 rtt_var,算法如下:

new_rtt_var = (rtt_var * 3 + abs(rtt – keep_rtt)) / 4 

rtt_var 其实就是网络抖动的时间差值。

如果 RTT 太大,表示网络延迟很大。我们在端到端之间的网络路径同时保持多条并且实时探测其网络状态,如果 RTT 超出延迟范围会进行传输路径切换(本地网络拥塞除外)。

2.3 抖动和乱序

UDP 除了延迟外,还会出现网络抖动。什么是抖动呢?举个例子,假如我们每秒发送 10 帧视频帧,发送方与接收方的延迟为 50MS,每帧数据用一个 UDP 报文来承载,那么发送方发送数据的频率是 100ms 一个数据报文,表示第一个报文发送时刻 0ms, T2 表示第二个报文发送时刻 100ms . . .,如果是理想状态下接收方接收到的报文的时刻依次是(50ms, 150ms, 250ms, 350ms….),但由于传输的原因接收方收到的报文的相对时刻可能是(50ms, 120ms, 240ms, 360ms ….),接收方实际接收报文的时刻和理想状态时刻的差值就是抖动。如下示意图:


( 点击图片可以全屏缩放)

我们知道视频必须按照严格是时间戳来播放,否则的就会出现视频动作加快或者放慢的现象,如果我们按照接收到视频数据就立即播放,那么这种加快和放慢的现象会非常频繁和明显。也就是说网络抖动会严重影响视频播放的质量,一般为了解决这个问题会设计一个视频播放缓冲区,通过缓冲接收到的视频帧,再按视频帧内部的时间戳来播放既可以了。

UDP 除了小范围的抖动以外,还是出现大范围的乱序现象,就是后发的报文先于先发的报文到达接收方。乱序会造成视频帧顺序错乱,一般解决的这个问题会在视频播放缓冲区里做一个先后排序功能让先发送的报文先进行播放。

播放缓冲区的设计非常讲究,如果缓冲过多帧数据会造成不必要的延迟,如果缓冲帧数据过少,会因为抖动和乱序问题造成播放无数据可以播的情况发生,会引起一定程度的卡顿。关于播放缓冲区内部的设计细节我们在后面的小节中详细介绍。

2.4 丢包

UDP 在传输过程还会出现丢包,丢失的原因有多种,例如:网络出口不足、中间网络路由拥堵、socket 收发缓冲区太小、硬件问题、传输损耗问题等等。在基于 UDP 视频传输过程中,丢包是非常频繁发生的事情,丢包会造成视频解码器丢帧,从而引起视频播放卡顿。这也是大部分视频直播用 TCP 和 RTMP 的原因,因为 TCP 底层有自己的重传机制,可以保证在网络正常的情况下视频在传输过程不丢。基于 UDP 丢包补偿方式一般有以下几种:

报文冗余

报文冗余很好理解,就是一个报文在发送的时候发送 2 次或者多次。这个做的好处是简单而且延迟小,坏处就是需要额外 N 倍(N 取决于发送的次数)的带宽。

FEC

Forward Error Correction,即向前纠错算法,常用的算法有纠删码技术(EC),在分布式存储系统中比较常见。最简单的就是 A B 两个报文进行 XOR(与或操作)得到 C,同时把这三个报文发往接收端,如果接收端只收到 AC,通过 A 和 C 的 XOR 操作就可以得到 B 操作。这种方法相对增加的额外带宽比较小,也能防止一定的丢包,延迟也比较小,通常用于实时语音传输上。对于  1080P 300KB/S 码率的超清晰视频,哪怕是增加 20% 的额外带宽都是不可接受的,所以视频传输不太建议采用 FEC 机制。

丢包重传

丢包重传有两种方式,一种是 push 方式,一种是 pull 方式。Push 方式是发送方没有收到接收方的收包确认进行周期性重传,TCP 用的是 push 方式。pull 方式是接收方发现报文丢失后发送一个重传请求给发送方,让发送方重传丢失的报文。丢包重传是按需重传,比较适合视频传输的应用场景,不会增加太对额外的带宽,但一旦丢包会引来至少一个 RTT 的延迟。

2.5 MTU 和最大 UDP

IP 网定义单个 IP 报文最大的大小,常用 MTU 情况如下:

超通道 65535

16Mb/s 令牌环 179144

Mb/s 令牌环 4464

FDDI 4352

以太网 1500

IEEE 802.3/802.2 1492

X.25 576

点对点(低时延)296

红色的是 Internet 使用的上网方式,其中 X.25 是个比较老的上网方式,主要是利用 ISDN 或者电话线上网的设备,也不排除有些家用路由器沿用 X.25 标准来设计。所以我们必须清晰知道每个用户端的 MTU 多大,简单的办法就是在初始化阶段用各种大小的 UDP 报文来探测 MTU 的大小。MTU 的大小会影响到我们视频帧分片的大小,视频帧分片的大小其实就是单个 UDP 报文最大承载的数据大小。

分片大小 = MTU – IP 头大小 – UDP 头大小 – 协议头大小;

IP 头大小 = 20 字节, UDP 头大小 = 8 字节。

为了适应网络路由器小包优先的特性,我们如果得到的分片大小超过 800 时,会直接默认成 800 大小的分片。

三、传输模型

我们根据视频编码和网络传输得到特性对 1080P 超清视频的实时传输设计了一个自己的传输模型,这个模型包括一个根据网络状态自动码率的编解码器对象、一个网络发送模块、一个网络接收模块和一个 UDP 可靠到达的协议模型。各个模块的关系示意图如下:


( 点击图片可以全屏缩放)

3.1 通信协议

先来看通信协议,我们定义的通信协议分为三个阶段:接入协商阶段、传输阶段、断开阶段。

接入协商阶段:

主要是发送端发起一个视频传输接入请求,携带本地的视频的当前状态、起始帧序号、时间戳和 MTU 大小等,接收方在收到这个请求后,根据请求中视频信息初始化本地的接收通道,并对本地 MTU 和发送端 MTU 进行比较取两者中较小的回送给发送方, 让发送方按协商后的 MTU 来分片。示意图如下:

( 点击图片可以全屏缩放)

传输阶段:

传输阶段有几个协议,一个测试量 RTT 的 PING/PONG 协议、携带视频帧分片的数据协议、数据反馈协议和发送端同步纠正协议。其中数据反馈协议是由接收反馈给发送方的,携带接收方已经接收到连续帧的报文 ID、帧 ID 和请求重传的报文 ID 序列。同步纠正协议是由发送端主动丢弃发送窗口缓冲区中的报文后要求接收方同步到当前发送窗口位置,防止在发送主动丢弃帧数据后接收方一直要求发送方重发丢弃的数据。示意图如下:

( 点击图片可以全屏缩放)

断开阶段:

就一个断开请求和一个断开确认,发送方和接收方都可以发起断开请求。

3.2 发送

发送主要包括视频帧分片算法、发送窗口缓冲区、拥塞判断算法、过期帧丢弃算法和重传。先一个个来介绍。

帧分片

前面我们提到 MTU 和视频帧大小,在 1080P 下大部分视频帧的大小都大于 UDP 的 MTU 大小,那么就需要对帧进行分片,分片的方法很简单,按照先连接过程协商后的 MTU 大小来确定分片大小(确定分片大小的算法在 MTU 小节已经介绍过),然后将 帧数据按照分片大小切分成若干份,每一份分片以 segment 报文形式发往接收方。

重传

重传比较简单,我们采用 pull 方式来实现重传,当接收方发生丢包,如果丢包的时刻 T1 + rtt_var< 接收方当前的时刻 T2,就认为是丢包了,这个时候就会把所有满足这个条件丢失的报文 ID 构建一个 segment ack 反馈给发送方,发送方收到这个反馈根据 ID 到重发窗口缓冲区中查找对应的报文重发即可。

为什么要间隔一个 rtt_var 才认为是丢包了?因为报文是有可能乱序到达,所有要等待一个抖动周期后认为丢失的报文还没有来才确认是报文丢失了,如果检测到丢包立即发送反馈要求重传,有可能会让发送端多发数据,造成带宽让费和网络拥塞。

发送窗口缓冲区

发送窗口缓冲区保存这所有正在发送且没有得到发送方连续 ID 确认的报文。当接收方反馈最新的连续报文 ID,发送窗口缓冲就会删除所有小于最新反馈连续的报文 ID,发送窗口缓冲区缓冲的报文都是为了重发而存在。这里解释下接收方反馈的连续的报文 ID,举个例子,假如发送方发送了 1. 2. 3. 4. 5,接收方收到 1.2. 4. 5。这个时候最小连续 ID = 2,如果后面又来了 3,那么接收方最小连续 ID = 5。

拥塞判断

我们把当前时间戳记为 curr_T,把发送窗口缓冲区中最老的报文的时间戳记为 oldest_T,它们之间的间隔记为 delay,那么

delay = curr_T – oldest_T

在编码器请求发送模块发送新的视频帧时,如果 delay > 拥塞阈值 Tn,我们就认为网络拥塞了,这个时候会根据最近 20 秒接收端确认收到的数据大小计算一个带宽值,并把这个带宽值反馈给编码器,编码器收到反馈后,会根据带宽调整编码码率。如果多次发生要求降低码率的反馈,我们会缩小图像的分辨率来保证视频的流畅性和实时性。Tn 的值可以通过 rtt 和 rtt_var 来确定。

但是网络可能阶段性拥塞,过后却恢复正常,我们设计了一个定时器来定时检查发送方的重发报文数量和 delay,如果发现恢复正常,会逐步增大编码器编码码率,让视频恢复到指定的分辨率和清晰度。

过期帧丢弃

在网络拥塞时可能发送窗口缓冲区中有很多报文正在发送,为了缓解拥塞和减少延迟我们会对整个缓冲区做检查,如果有超过一定阈值时间的 H.264 GOP 分组存在,我们会将这个 GOP 所有帧的报文从窗口缓冲区移除。并将它下一个 GOP 分组的 I 的帧 ID 和报文 ID 通过 wnd sync 协议同步到接收端上,接收端接收到这个协议,会将最新连续 ID 设置成同步过来的 ID。这里必须要说明的是如果频繁出现过期帧丢弃的动作会造成卡顿,说明当前网络不适合传输高分辨率视频,可以直接将视频设成更小的分辨率

3.3 接收

接收主要包括丢包管理、播放缓冲区、缓冲时间评估和播放控制,都是围绕播放缓冲区来实现的,一个个来介绍。

丢包管理

丢包管理包括丢包检测和丢失报文 ID 管理两部分。丢包检测过程大致是这样的,假设播放缓冲区的最大报文 ID 为 max_id,网络上新收到的报文 ID 为 new_id,如果 max_id + 1 < new_id,那么可能发生丢包,就会将 [max_id + 1, new_id -1] 区间中所有的 ID 和当前时刻作为 K/V 对加入到丢包管理器当中。如果 new_id < max_id,那么就将丢包管理中的 new_id 对应的 K/V 对删除,表示丢失的报文已经收到。当收包反馈条件满足时,会扫描整个丢包管理,将达到请求重传的丢包 ID 加入到 segment ack 反馈消息中并发往发送方请求重传,如果 ID 被请求了重传,会将当前时刻设置为 K/V 对中,增加对应报文的重传计数器 count,这个扫描过程会统计对包管理器中单个重发最多报文的重发次数 resend_count。

缓冲时间评估

在前面的抖动与乱序小节中我们提到播放端有个缓冲区,这个缓冲区过大时延迟就大,缓冲区过小时又会出现卡顿现象,我们针对这个问题设计了一个缓冲时间评估的算法。缓冲区评估先会算出一个 cache timer,cache timer 是通过扫描对包管理得到的 resend count 和 rtt 得到的,我们知道从请求重传报文到接收方收到重传的报文的时间间隔是一个 RTT 周期,所以 cache timer 的计算方式如下。

cache timer = (2 * resend_count+ 1) * (rtt + rtt_var) / 2

有可能 cache timer 计算出来很小(小于视频帧之间间隔时间 frame timer),那么 cache timer = frame timer,也就是说网络再好,缓冲区缓冲区至少 1 帧视频的数据,否则缓冲区是毫无意义的。

如果单位时间内没有丢包重传发生,那么 cache timer 会做适当的缩小,这样做的好处是当网络间歇性波动造成 cache timer 很大,恢复正常后 cache timer 也能恢复到相对小位置,缩减不必要的缓冲区延迟。

播放缓冲区

我们设计的播放缓冲区是按帧 ID 为索引的有序循环数组,数组内部的单元是视频帧的具体信息:帧 ID、分片数、帧类型等。缓冲区有两个状态:waiting 和 playing,waiting 状态表示缓冲区处于缓冲状态,不能进行视频播放直到缓冲区中的帧数据达到一定的阈值。Playing 状态表示缓冲区进入播放状态,播放模块可以从中取出帧进行解码播放。我们来介绍下这两个状态的切换关系:

  1. 当缓冲区创建时会被初始化成 waiting 状态。
  2. 当缓冲区中缓冲的最新帧与最老帧的时间戳间隔 > cache timer 时,进入 playing 状态并更当前时刻设成播放绝对时间戳 play ts。
  3. 当缓冲区处于 playing 状态且缓冲区是没有任何帧数据,进入 waiting 状态直到触发第 2 步。

播放缓冲区的目的就是防止抖动和应对丢包重传,让视频流能按照采集时的频率进行播放,播放缓冲区的设计极其复杂,需要考虑的因素很多,实现的时候需要慎重。

播放控制

接收端最后一个环节就是播放控制,播放控制就是从缓冲区中拿出有效的视频帧进行解码播放。但是怎么拿?什么时候拿?我们知道视频是按照视频帧从发送端携带过来的相对时间戳来做播放,我们每一帧视频都有一个相对时间戳 TS,根据帧与帧之间的 TS 的差值就可以知道上一帧和下一帧播放的时间间隔,假如上一帧播放的绝对时间戳为 prev_play_ts,相对时间戳为 prev_ts,当前系统时间戳为 curr_play_ts,当前缓冲区中最小序号帧的相对时间戳为  frame_ts,只要满足:

Prev_play_ts + (frame_ts – prev_ts) < curr_play_ts 且这一帧数据是所有的报文都收齐了

这两个条件就可以进行解码播放,取出帧数据后将 Prev_play_ts = cur_play_ts,但更新 prev_ts 有些讲究,为了防止缓冲延迟问题我们做了特殊处理。

如果 frame_ts + cache timer < 缓冲区中最大帧的 ts,表明缓冲的时延太长,则 prev_ts = 缓冲区中最大帧的 ts – cache timer。 否则 prev_ts = frame_ts。

四、测量

再好的模型也需要有合理的测量方式来验证,在多媒体这种具有时效性的传输领域尤其如此。一般在实验室环境我们采用 netem 来进行模拟公网的各种情况进行测试,如果在模拟环境已经达到一个比较理想的状态后会组织相关人员在公网上进行测试。下面来介绍怎么来测试我们整个传输模型的。

4.1 netem 模拟测试

Netem 是 Linux 内核提供的一个网络模拟工具,可以设置延迟、丢包、抖动、乱序和包损坏等,基本能模拟公网大部分网络情况。

关于 netem 可以访问它的官网:

https://wiki.linuxfoundation.org/networking/netem

我们在实验环境搭建了一个基于服务器和客户端模式的测试环境,下面是测试环境的拓扑关系图:

我们利用 Linux 来做一个路由器,服务器和收发端都连接到这个路由器上,服务器负责客户端的登记、数据转发、数据缓冲等,相当于一个简易的流媒体服务器。Sender 负责媒体编码和发送,receiver 负责接收和媒体播放。为了测试延迟,我们把 sender 和 receiver 运行在同一个 PC 机器上,在 sender 从 CCD 获取到 RGB 图像时打一个时间戳,并把这个时间戳记录在这一帧数据的报文发往 server 和 receiver,receiver 收到并解码显示这帧数据时,通过记录的时间戳可以得到整个过程的延迟。我们的测试用例是用 1080P 码率为 300KB/S 视频流,在 router 用 netem 上模拟了以下几种网络状态:

  1. 环路延迟 10m,无丢包,无抖动,无乱序
  2. 环路延迟 30ms,丢包 0.5%,抖动 5ms, 2% 乱序
  3. 环路延迟 60ms,丢包 1%,抖动 20ms, 3% 乱序,0.1% 包损坏
  4. 环路延迟 100ms,丢包 4%,抖动 50ms, 4% 乱序,0.1% 包损坏
  5. 环路延迟 200ms,丢包 10%,抖动 70ms, 5% 乱序,0.1% 包损坏
  6. 环路延迟 300ms,丢包 15%,抖动 100ms, 5% 乱序,0.1% 包损坏

因为传输机制采用的是可靠到达,那么检验传输机制有效的参数就是视频延迟,我们统计 2 分钟周期内最大延迟,以下是各种情况的延迟曲线图:

从上图可以看出,如果网络控制在环路延迟在 200ms 丢包在 10% 以下,可以让视频延迟在 500ms 毫秒以下,这并不是一个对网络质量要求很苛刻的条件。所以我们在后台的媒体服务部署时,尽量让客户端到媒体服务器之间的网络满足这个条件,如果网路环路延迟在 300ms 丢包 15% 时,依然可以做到小于 1 秒的延迟,基本能满足双向互动交流。

4.2 公网测试

公网测试相对比较简单,我们将 Server 部署到 UCloud 云上,发送端用的是上海电信 100M 公司宽带,接收端用的是河北联通 20M 小区宽带,环路延迟在 60ms 左右。总体测试下来 1080P 在接收端观看视频流畅自然,无抖动,无卡顿,延迟统计平均在 180ms 左右。

五、坑

在整个 1080P 超清视频的传输技术实现过程中,我们遇到过比较多的坑。大致如下:

Socket 缓冲区问题

我们前期开发阶段都是使用 socket 默认的缓冲区大小,由于 1080P 图像帧的数据非常巨大(关键帧超过 80KB),我们发现在在内网测试没有设置丢包的网络环境发现接收端有严重的丢包,经查证是 socket 收发缓冲区太小造成丢包的,后来我们把 socket 缓冲区设置到 128KB 大小,问题解决了。

H.264 B 帧延迟问题

前期我们为了节省传输带宽和防丢包开了 B 帧编码,由于 B 帧是前后双向预测编码的,会在编码期滞后几个帧间隔时间,引起了超过 100ms 的编码延时,后来我们为了实时性干脆把 B 帧编码选项去掉。

Push 方式丢包重传

在设计阶段我们曾经使用发送端主动 push 方式来解决丢包重传问题,在测试过程发现在丢包频繁发生的情况下至少增加了 20% 的带宽消耗,而且容易带来延迟和网络拥塞。后来几经论证用现在的 pull 模式来进行丢包重传。

Segment 内存问题

在设计阶段我们对每个视频缓冲区中的帧信息都是动态分配内存对象的,由于 1080P 在传输过程中每秒会发送 400 – 500 个 UDP 报文,在 PC 端长时间运行容易出现内存碎片,在服务器端出现莫名其妙的 clib 假内存泄露和并发问题。我们实现了一个 memory slab 管理频繁申请和释放内存的问题。

音频和视频数据传输问题

在早期的设计之中我们借鉴了 FLV 的方式将音频和视频数据用同一套传输算法传输,好处就是容易实现,但在网络波动的情况下容易引起声音卡顿,也无法根据音频的特性优化传输。后来我们把音频独立出来,针对音频的特性设计了一套低延迟高质量的音频传输体系,定点对音频进行传输优化。

后续的工作是重点放在媒体器多点分布、多点并发传输、P2P 分发算法的探索上,尽量减少延迟和服务带宽成本,让传输变的更高效和更低廉。

Q&A

提问:在优化到 500ms 方案中,哪一块是最关键的?

袁荣喜:主要是丢包重传 拥塞和播放缓冲这三者之间的协调工作最为关键,要兼顾延迟控制和视频流畅性。

提问:多方视频和单方有哪些区别,用到了 CDN 推流吗?

袁荣喜:我们公司是做在线教育的,很多场景需要老师和学生交谈,用 CDN 推流方式延迟很大,我们这个视频主要是解决多方通信之间交谈延迟的问题。我们现在观看放也有用 CDN 推流,但只是单纯的观看。我们也在研发基于 UDP 的观看端分发协议,目前这部分工作还没有完成。

移动开发必读书单

为什么要列这一个书单?我认为某一领域的技术人,在他的职业生涯中,一定有一些绕不过去的技术和非技术的知识。有的时候,靠自己摸索、到处偷师,倒也能掌握。但是,这些别人早就趟过去的坎,大多已经有了非常棒的书籍作为总结。看了这些书,脚下的路就要平一些。这个书单,就是为了找到移动领域的这些书。

这些书籍都是移动前线群里推荐的,都是他们自己读过,甚至是反复读的书。如果你对这些书有什么看法,欢迎发表意见,或者添上你觉得认为必须的书。

这个书单也不会一成不变,欢迎大家的增补修改。

计算机基础

《程序是怎样跑起来的》

推荐理由:类似深入理解计算机的图解版本,如果第一次看深入理解计算机看不进去,可以先看看,了解个大概,再看深入理解计算机这本就容易很多。

《深入理解计算机系统》

推荐理由:了解一个程序的编译、链接、执行过程,以及虚拟内存是如何分配等。

通用编程

《算法(第四版)》

推荐理由:难度不及算法导论,但是更适合工程领域,导论恐高症患者的福音。

《编程珠玑》

推荐理由:编程珠玑这本书推荐给有代码基础的小伙伴,推荐理由是书里会有一些优化和算法基础的思想,适合入门之后的进阶,对优化和算法能够有一些基础的认识。对程序员写出优质而高效的代码比较有帮助,至少我个人阅后感受如此。

《设计模式之禅》

推荐理由:读完,通俗易懂,更深刻理解面向对象和面向接口。

注:设计模式这几本书难以取舍,选一本读即可。

《大话设计模式》,《大话数据结构》

推荐理由:都是菜鸟和大牛的对话模式,看着不枯燥,内容也很全。

推荐理由2:推荐初级程序员看<大话设计模式>,这本书有个好处是他是演进式的,虽然内容很简单,看完也没办法直接指导到开发上。但是会有一种恍然大悟的感觉,原来这就是设计模式。比起四人组的设计模式,太过于专业的说明和定义,对于初级开发人员来说,往往无法消化甚至抵触。回到大话设计模式,将实践和故事相结合,即让设计模式接了地气,又引发自己的思考,我怎么可以改造既有的项目。同时作为入门书籍,看完后你会更渴望更专业的设计模式的内容也算是这本书的一个意义。总结就是,一本非常入门接地气的设计模式书籍。

《Head first 设计模式》

推荐理由:漫画形式的以对话为主,都是从生活中的小例子入手,更理解一句话了:万物皆对象。

《构建之法》

推荐理由:一本有趣的软件工程书,新手哪知道什么是软件工程。书里描述了作者在的团队遇到了哪些问题,如何正视,怎样改善,不断改进。理论和实践相结合。面向实战。讲述软件工程中不同角色的作用。看完后可以帮助新手更好地理解软件开发是怎么个回事,更快地走向成熟。

《代码大全》

推荐理由:代码大全全面讲述了工程实践的要点,深入每个细节。可以帮助新手拓展视野,熟悉规范,也可以在学习一些概念或技术时当做字典查。总之代码大全的英文名code complete很能说明问题:帮助你完成整个编码活动的全过程。

《代码整洁之道》

推荐理由:这几天在看,讲解详细,有提供示例,可以提高自己编码素质。

《图解http》

推荐理由:做网络编程必须要懂的基础书,看这本书对做网络缓存和网络性能优化都有不小的帮助,最主要是书内容深入浅出 图文并茂,通俗易懂。

《单元测试的艺术》

推荐理由:由浅入深地介绍了单元测试的各方面知识,告诉我们如何写好单元测试,如何写易于测试的代码,如何处理遗留代码等问题,有理论有案例。

《修改代码的艺术》

推荐理由:详细讲解重构技巧的书,教你怎么处理各种遗留的烂代码,非常详实的重构操作手册。

iOS开发

《Effective Objective-C 2.0》

推荐理由:这本书很适合初学者,能帮助初学者一开始就打下比较规范的基础,里面讲了很多规范。读了这本书能让初学者少走很多弯路。

推荐理由2:精炼,针对性强,对一些生僻容易懵圈的知识点讲得比较细。

《iOS编程实战》

推荐理由:尤其推荐小公司的没有系统学习过iOS的同学看,实战和自己闭门造车完全两码事,这书不是教人这个api怎么用,而是应该这样用。

《Objective-C高级编程 iOS与OS X多线程和内存管理》

推荐理由:这本书给我带来的最大价值是:要让自己对一项技术有全面的认识,光从官方文档、开源代码中寻找答案并不够,还要会用各种工具来验证自己的想法。可以看出作者对技术非常严谨的态度,虽然偶尔有些翻译并不容易让人理解,但真的是一本进阶型的书。

《iOS编程(第4版)》

推荐理由:作者之一Aaron Hillegass曾就职于NeXT公司和Apple公司。 书涵盖了初级到中级的,书中在讲到第一次出现的知识点的时候,通常会简要的解释一下,所以初学者很容易上手。缺点是这书有点老,但OC的基础知识讲解的很扎实。

《Swift Apprentice》

推荐理由:这本书的优势就是从浅入深,抽死剥茧,从最基础的知识带领你到函数编程奥义的天堂,充分的实例,全英文,免去你被中文版带沟里的风险,相信这本书能告诉初学者swift2为什是大势所趋,为什么swift2即是面向对象编程语言又是函数编程语言。

《Swifter – 100 个 Swift 必备 tips》

推荐理由:比较偏帮助手册类的tips,虽然没有讲Swift比较深入的,但对前期学习Swift梳理与Objective-C及混编有很大帮助。

《iOS应用逆向工程 第2版》推荐理由:iOS应用逆向最有价值参考书籍,内容涵盖多个实例,阐述class-dump、Theos、Cycript、Reveal、IDA、LLDB等常用工具的使用,通俗易懂;总结提炼出一套从UI观察切入代码分析的iOS应用逆向工程方法论,激发iOS开发人员对应用安全的思考,把主动防御的思想渗透到项目开发中。

Android开发

《第一行代码》

推荐理由:我刚刚大体看了一遍 ,这本书学完之后完全足够入门,而且不会令初学者感到枯燥。在入门阶段还可以作为工具书不断去翻阅,不同时期会有不同的感受。 里面打小怪兽升级的旁白,现在回过头看还确实挺有趣的。

《App研发录》

推荐理由:研发辅助类,包含了异常的分析,竞品分析和项目管理,书不厚,但是有些能增加知识的广度。

《Embedded Android》

推荐理由:从底层讲解android系统的机制与设计,比较适合具有一定经验的开发者,属于进阶书籍。

《Java并发编程实战》

推荐理由:通过java现成的并发工具类介绍和实例,深入浅出的说明如何安全地进行多线程操作和优化。

《Android.C++.with.the.NDK》

推荐理由:android jni入门与提高,系统讲解android jni的编译、提供的接口、java jni的通讯方式等,对于需要学习jni开发的同学是必读,毕竟jni文档缺乏。

《Android开发艺术与探索》

推荐理由:主要讲一些原理,主要学习为什么这么用,以及源码解析说明。

《深入了解JVM虚拟机》

推荐理由:这本书对了解底层机制很有帮助,是进阶必选。

《Java编程思想》

推荐理由:Java编程思想是Java圣经,要有一定经验去看效率才不错。

人文类

《程序员的职业素养》推荐理由:从企业,团队,技术,自身等各个角度阐述了作为一个专业的程序员应该如何去做到更好,不卑不亢,不投机取巧,树立非常正能量的价值观,举了很多工作学习中经常碰到的案例,读完相见恨晚。书不是太厚,个人净阅读时间在10小时左右。

《在人生拐角处》

推荐理由:很多程序员对于自身的职业规划并没有,所以这本书就是帮助你化解你的迷茫,做好职业规划,里面是生涯规划师写的案例,都很贴近生活,适合不知道自己该怎么走下去的人看。

《程序员健康指南》

推荐理由:本书是为了程序员量身订造的健康指南,针对头痛,眼部疲劳,背部疼痛和手腕疼痛等常见问题,简要介绍了其成因,测试方法,并列出了每天的行动计划,从运动,饮食等方面给出详细指导,帮助程序员在不改变工作方式的情况下轻松拥有健康。

《重新定义公司 How Google Works》

推荐理由:一分理论三分实践相结合,告诉你互联网公司运作的合理方式。亮点是针对理论有 Google 实际的实行方式,也有多个 Google 内部项目成长的曲折过程,还有谷歌退出中国的内部爆料。

《黑客与画家》推荐理由:非常有深度的一本书,作者在经济,社科,历史,等很多方面都很有自己的见解,更不用说技术和创业了。书中很多观点有准确的例证引用,让人信服。

《Rework》推荐理由:一句话:太赞的一本书了!!!创业者必读!当然,不创业的人也能从中得到非常多的insight。作者是37signals.com的两个创始人,其中之一还是Ruby on Rails的creator。讲的是创业中需要注意的一些事情,但这些事情不仅仅是创业中才需要注意的。另外,英文原版的写的也很通俗易懂,推荐看原版的。

推荐理由: 这本书本身就是小而美的37Singal自身成长的总结,特别适合小团队初创团队,很好的诠释了“好铁用在刀刃上”。

《Getting Real》推荐理由:37Signals的又一本书,也是第一本,强调用一种务实的方式做一个Web app,从产品组队实现上线推广各方面的点点滴滴。内容依然很棒,但是跟《Rework》有大量重复的地方,看过那本以后已经没有那种惊艳的感觉了。也算是知道了为这么这本书豆瓣评分达9.1, 而那本“只有”8.7了。

《代码的未来》推荐理由:Ruby发明人Matz的大作,这是一本技术性随笔,介绍了现代编程的方方面面,从lisp宏到gc技术,再到元编程,IPC,多核及云计算,NoSQL。。。虽说是随笔,还是具备一定的技术性的,很多地方要认真的看才看得懂。作者的语气很谦虚亲切,很多地方甚至有卖萌的嫌疑,哈!总之,是一本广大程序猿增长见识,拓宽视野的极佳之作!

《GEB》

推荐理由:通过对哥德尔的数理逻辑,艾舍尔的版画和巴赫的音乐三者的综合阐述,引人入胜地介绍了数理逻辑学、可计算理论、人工智能学、语言学、遗传学、音乐、绘画的理论等方面。抽象级别比设计模式高上几个数量级,看懂这本书,不仅是抽象思维上质的提升,对个人也将产生深远的影响。

效率类

《暗时间》

推荐理由:讲的是关于时间管理,告诉我们怎么学习专业领域技能和非专业领域技能以及我们应该怎么自我管理性格。

《把时间当做朋友》

推荐理由:它的理念就是,时间是不可以管理的,最主要改变的是自己的心智!

技术管理

《门后的秘密》

推荐理由:以场景、对话的方式讲解日常基础技术管理,适合技术人员转管理的细细阅读,入门管理实用。

《人月神话》

推荐理由:管理复杂项目的见解深刻,并有大量软件工程的实践。

《人件》

推荐理由:给技术管理者或者希望走向管理方向的人参考。最近在看第二遍,收获很大。软件即人件,软件开发不光要解决技术问题,更重要是人的问题。为何很多工作加班才能解决?公司为何无法留住员工?读完这本书可以找到一些答案。

Google 和 Baidu 常用的搜索技巧

学计算机的,在学习的过程中,肯定会遇到很多的 Bug,有时候,并不会有前辈在我们的身边指导我们该如何地去学习。这个时候,我们就得靠自己去使用搜索引擎去解决我们的问题。

幸运的是,我们所运到的问题,前人都已经遇到过了,并且给出了相应的解决办法。基本上百分之九十的问题,我们都能在互联网上搜索得到。

下面我给大家介绍下,Google 和 Baidu 这两个搜索引擎的常规搜索技巧

Google 常用的搜索技巧

1. 精确搜索:双引号

精确搜索,就是在你要搜索的词上,加上双引号,这个Google搜索引擎,就会完全的匹配你所要的词

2. 站内搜索:site

这是一个比较常用的搜索方法,site 搜索,就是在站内进行搜索,语法是:site:stackoverflow.com/site:后面加上你要搜索的网站地址。一般程序猿解决问题,用site:stackoverflow.com/,大部分解决不了的问题,都会有答案了。

再举个比较贴近生活的例子,大家平时工作在外,租房子肯定是一件非常困难的事情,黑中介非常的多,使用 Google 搜索房源,能够很快的帮助你找到房子,如成都天府软件园租房

光这样,还远远不够的,毕竟有些房子是发布了好久,那有没有办法搜索最近几天的租房信息呢,有的,见图

有时候,如果搜索到的信息过少,可以考虑下替换关键词

3. 通配符搜索:*

这也是一个比较常用的搜索方法,通常通配符搜索,用在模糊印象的地方比较多。当我想搜一句歌词,边走边爱,反正人山人海但是前边的 边走边爱,就可以用这个方法进行搜索了。搜索* 反正人山人海

4. 减号排除,缩小范围:-

当搜索量比较大的时候,使用减号 - 也是一个非常不错的选择,比如你搜索一个人的信息的时候,通过减号,能够去掉一些无关的搜索,比如:

减去 America 这个信息

5 . 文档搜索:filetype

文档搜索命令 filetype,多数情况下用以查找我们所需要的资料,返回的页面是你搜索的文档相应格式,如搜谭浩强C语言的pdf,版本: filetype:pdf 谭浩强C语言

还再比如,平时的期末考试,找不到科目复习卷子,用 Google 搜索,如图

如果要搜索自己的学校卷子,可以参考下第一条双引号特定搜索,如果 pdf 格式的没有,可以换个关键词,考虑一下其他的文档格式,如 doc 等

6. 图片搜索

平常在网上考到一张好的图片,可以保持下来,但是由于图片的尺寸过小,或者像素不合适,这个时候,只要用谷歌图片搜索,就能找到许多类似的,或者尺寸清晰度更好的同一张

Baidu 常用的搜索技巧

百度常用的技巧,许多和谷歌大同小异,常用的这些命令,这四五个就够用了,其他高级技巧,不是做 SEO 没得必要去记太多

1. 精确搜索:双引号

精确搜索,就是在你要搜索的词上,加上双引号,这个 Baidu 搜索引擎,就会完全的匹配你所要的词

2. 站内搜索:site

这个也是跟谷歌一样,比如搜知乎的内容,site:zhihu.com 长得帅却没有女朋友是什么体验

3. 通配符搜索:*

这个也是跟谷歌一样,比如搜知乎的内容,*谁能凭爱意要富士山私有

4. 减号排除,缩小范围:-

这个还是同谷歌的搜索技巧,我们搜索:Hillary Clinton

缩小范围:

5. 文档搜索

百度的文档搜索,也是这个命令: filetype:pdf 谭浩强C语言 ,不过相对谷歌而已,百度的搜索是把自己的产品,放在前面,见图,排在前面的都是百度自己的产品,百度文库

6. 图片搜索

百度和谷歌一样,都是提供了这个功能,


基本靠着这些方法,就能解决你许多搜索的问题了,其中 site 命令和双引号关键词 " "这两个命令用得相对较多,其他高级少用的技巧,就不一一列举了。

使用go实现简单的文章爬虫功能

需求:

  1. 使用go语言爬取CSDN上某位博主的所有博客

分析:

1、 对于一般的博客文章,主要采集文章标题、发表时间、文章标签、文章链接、文章内容

2、一般都会分页展示博客文章

如果是采用单线程的方式,其步骤是:依次获取每一页的文章列表,将获取的列表存入待爬取的队列里,效率低。

如果是采用并发的方式,其步骤是:并发获取每一页的文章列表,将这些列表存入待爬取的文章通道里,加一个goroutine不断的从通道里取文章基本信息并去爬文章内容。

对于简单的单线程,这里不贴代码了,主要着重并发获取。

第一步:根据每一页获取文章列表信息

  1. package main
  2.  
  3. import (
  4.    “github.com/PuerkitoBio/goquery”
  5.    “fmt”
  6. )
  7.  
  8.  
  9. //定向爬某个博客
  10. type Bloginfo struct {
  11.    HomeUrl string
  12.    BaseUrl string
  13.    PageUrl string
  14.  
  15. }
  16.  
  17. type BlogItem struct {
  18.    Title string
  19.    Link string
  20.    GmtPublish string
  21.    Tag string
  22.    Content string
  23. }
  24.  
  25.  
  26. func main() {
  27.  
  28.    blogInfo := Bloginfo{
  29.       “http://blog.csdn.net/testcs_dn”,
  30.       “http://blog.csdn.net”,
  31.       “http://blog.csdn.net/testcs_dn/article/list/%d”}
  32.  
  33.    d := get_page_list(blogInfo, 1)
  34.    fmt.Println(d)
  35.  
  36. }
  37.  
  38. //获取每一页的文章列表
  39. func get_page_list(info Bloginfo, page int) []BlogItem{
  40.    rst := make([]BlogItem,0);
  41.    doc,:= goquery.NewDocument(fmt.Sprintf(info.PageUrl, page))
  42.    if e!= nil {
  43.       panic(e)
  44.       return rst
  45.    }
  46.    doc.Find(“.article_item”).Each(func(int, e *goquery.Selection){
  47.       item := BlogItem{}
  48.       item.Title = e.Find(“.link_title”).Text()
  49.       item.Link,= e.Find(“.link_title a”).Attr(“href”)
  50.       item.Link = info.BaseUrl + item.Link
  51.       item.GmtPublish = e.Find(“.link_postdate”).Text()
  52.       rst = append(rst, item)
  53.    });
  54.    return rst
  55. }

亲测有效

  1. [{
  2.         [置顶]
  3.         作为一个程序员我为什么要写博客?            
  4.          /testcs_dn/article/details/51231922 20160424 14:08 } {
  5.         [置顶]
  6.         有学历的程序员永远不懂没学历的痛,就像白天不懂夜的黑            
  7.          /testcs_dn/article/details/51170327 20160417 21:06 } {
  8.         [置顶]
  9.         【读评】为什么你有10年经验,但成不了专家?            
  10.          /testcs_dn/article/details/51158562 20160416 14:17 } {
  11.         [置顶]
  12.         After 500:写500篇博客其实和写一篇是一样的            
  13.          /testcs_dn/article/details/50791702 20160304 09:07 } {
  14.         [置顶]
  15.         C#软件开发实例.私人订制自己的屏幕截图工具(一)功能概览            
  16.          /testcs_dn/article/details/23169549 20140408 11:50 } {
  17.         [置顶]
  18.         MySQL学习(二)图形界面管理工具Navicat for MySQL安装和使用            
  19.          /testcs_dn/article/details/21122035 20140312 23:17 } {
  20.         [置顶]
  21.         ThinkPHP学习(一) WindowsNginx+PHP5+ThinkPHP_3.2.1的安装与配置            
  22.          /testcs_dn/article/details/21036345 20140311 21:20 } {
  23.         TFS部署:create_block allocate space error. ret: 1, error: 28, error desc: No space left on device            
  24.          /testcs_dn/article/details/51955536 20160719 15:06 } {
  25.         TFS部署:ERROR create_fs_dir (blockfile_manager.cpp:1191) make extend dir error. ret: 1, error: 17            
  26.          /testcs_dn/article/details/51954873 20160719 13:14 } {
  27.         2016年中总结、半年总结            
  28.          /testcs_dn/article/details/51841165 20160717 22:12 } {
  29.         TFS安装:base_packet.cpp:246: 错误:从类型‘const char*’到类型‘pthread_t’的转换无效            
  30.          /testcs_dn/article/details/51911785 20160714 18:37 } {
  31.         CentOS 6.5 安装 Redis 执行 make #error “Newer version of jemalloc required”            
  32.          /testcs_dn/article/details/51867879 20160709 18:27 } {
  33.         service mysqld start MySQL Daemon failed to start.            
  34.          /testcs_dn/article/details/51811900 20160704 10:26 } {
  35.         我如何添加一个空目录到Git仓库?            
  36.          /testcs_dn/article/details/51811974 20160704 10:24 } {
  37.         IntelliJ IDEA 简单的项目配置            
  38.          /testcs_dn/article/details/51815950 20160703 17:18 } {
  39.         IntelliJ IDEA 的智能编码功能            
  40.          /testcs_dn/article/details/51815933 20160703 17:12 } {
  41.         IntelliJ IDEA 快速入门指南            
  42.          /testcs_dn/article/details/51759461 20160703 17:05 } {
  43.         熟悉 IntelliJ IDEA 的主界面            
  44.          /testcs_dn/article/details/51814897 20160703 13:35 } {
  45.         centos 删除文件夹 permission denied, xxx is not in the sudoers file.            
  46.          /testcs_dn/article/details/51814585 20160703 11:56 } {
  47.         IntelliJ IDEA 运行你的第一个Java应用程序            
  48.          /testcs_dn/article/details/51793511 20160630 22:03 } {
  49.         js跨域交互(jQuery+php)之jsonp使用心得            
  50.          /testcs_dn/article/details/51785002 20160630 13:14 } {
  51.         Linux 下安装IntelliJ IDEA Community Edition            
  52.          /testcs_dn/article/details/51776058 20160628 18:45 } {
  53.         Mac OS 下安装IntelliJ IDEA Community Edition            
  54.          /testcs_dn/article/details/51771422 20160627 21:48 } {
  55.         IntelliJ IDEA的安装和启动            
  56.          /testcs_dn/article/details/51755616 20160624 21:48 } {
  57.         WindowsXP下安装IntelliJ IDEA Ultimate Edition            
  58.          /testcs_dn/article/details/51754979 20160624 18:32 } {
  59.         IntelliJ IDEA的安装环境要求            
  60.          /testcs_dn/article/details/51741298 20160623 15:11 } {
  61.         Windows7下安装IntelliJ IDEA Community Edition 2016.1.3(64)            
  62.          /testcs_dn/article/details/51742613 20160623 14:57 }]

第二步、并发获取

  1. package main
  2.  
  3. import (
  4.    “github.com/PuerkitoBio/goquery”
  5.    “fmt”
  6.    “sync”
  7. )
  8.  
  9.  
  10. //定向爬某个博客
  11. type Bloginfo struct {
  12.    HomeUrl string
  13.    BaseUrl string
  14.    PageUrl string
  15.  
  16. }
  17.  
  18. type BlogItem struct {
  19.    Title string
  20.    Link string
  21.    GmtPublish string
  22.    Tag string
  23.    Content string
  24.  
  25. }
  26.  
  27.  
  28. func main() {
  29.  
  30.    blogInfo := Bloginfo{
  31.       “http://blog.csdn.net/testcs_dn”,
  32.       “http://blog.csdn.net”,
  33.       “http://blog.csdn.net/testcs_dn/article/list/%d”}
  34.  
  35.    pages := 20
  36.  
  37.    result := make([]BlogItem,0)
  38.  
  39.    articleChannel := make(chan []BlogItem,10)
  40.  
  41.  
  42.    wg := sync.WaitGroup{}
  43.    for i:=0;i<pages;i++ {
  44.       wg.Add(1)
  45.       go func(page int){
  46.          fmt.Println(“down list”)
  47.          articleChannel <- get_page_list(blogInfo, page)
  48.          wg.Done()
  49.       }(i)
  50.    }
  51.  
  52.    go func(){
  53.       wg.Wait()
  54.       fmt.Println(“closed”)
  55.       close(articleChannel)
  56.    }()
  57.  
  58.    for articleLists := range articleChannel {
  59.       //爬取内容
  60.       for _,item := range articleLists {
  61.          fmt.Println(“下载…” + item.Link)
  62.          result = append(result, get_article(item))
  63.       }
  64.    }
  65.    fmt.Println(result)
  66. }
  67.  
  68. //获取每一页的文章列表
  69. func get_page_list(info Bloginfo, page int) []BlogItem{
  70.    rst := make([]BlogItem,0);
  71.    doc,:= goquery.NewDocument(fmt.Sprintf(info.PageUrl, page))
  72.    if e!= nil {
  73.       panic(e)
  74.       return rst
  75.    }
  76.    doc.Find(“.article_item”).Each(func(int, e *goquery.Selection){
  77.       item := BlogItem{}
  78.       item.Title = e.Find(“.link_title”).Text()
  79.       item.Link,= e.Find(“.link_title a”).Attr(“href”)
  80.       item.Link = info.BaseUrl + item.Link
  81.       item.GmtPublish = e.Find(“.link_postdate”).Text()
  82.       rst = append(rst, item)
  83.    });
  84.    return rst
  85. }
  86.  
  87. //获取文章内容
  88. func get_article(item BlogItem) BlogItem {
  89.    doc, e := goquery.NewDocument(item.Link)
  90.  
  91.    if e!= nil {
  92.       panic(e)
  93.    }
  94.  
  95.    item.Content,= doc.Find(“#article_details”).Html()
  96.    item.Tag = doc.Find(“.link_categories”).Text()
  97.    return item
  98. }

Golang实现的一个并发爬虫框架

 

利用golan能简单编写并发程序和擅长网络编程的特性实现了一个并发爬虫框架。

一些特点

  • 轻量,易使用
  • 自定义解析库(方法),自定义数据处理方式
  • 组件独立模块化,易扩展,并发实现
  • 多规则,参数支持(爬行深度,http制定等)

基本结构

包含四个组件和一个控制中心,参考了Scarpy的架构,如图

01

Controller控制器

控制器负责调度整个爬虫的运行流程和协调各组件间的工作,包括组件的初始化,数据传递,停止等,使各组件的工作专注于自己的职责

Downloader下载器

下载器是爬虫与互联网交互的部分,把请求通过http与目标交互,获得响应后封装输送至分析器进行解析。这里会提供到对http请求的各种包装(Header, Cookie)接口。

Analyzer分析器

分析器对http响应进行解析,这里会让框架使用者自定义解析方法,提取所需要抓取的数据和下一步请求的链接,把这些数据送至处理器助理,还有对http响应的一些过滤等。

Porcessor处理器

处理器对分析器输送出来的数据和链接进行处理。数据会由使用者提供的方法来进行持久化储存,可选择文件,数据库等多种形式,这里把网络处理与数据储存细节分离开。而链接会进行筛选(去重, host控制),封装为请求以后发送至下载器到下一个流程。处理器还对通道传输有一定的缓存作用。

Monitor监视器

监视器对爬虫整个运行状态进行监控,包括一些实时的抓取数据,组件间数据传输状态,根据情况再优化爬取流程。

四个组件中除了监视器,其他都能由控制器调度拥有多个实例同时工作,得益于golang的goroutine和channel能够简洁的实现。各组件的职责专一降低了框架的耦合性。数据通过channel传输,

部分实现

分析器接口

type Parser func(httpRes *http.Response) ([]string, []basic.Item)

type Analyzer struct {
    linklist []string
    itemlist []basic.Item
}

type GenAnalyzer interface {
    Analyze(httpRes *http.Response, parser Parser) ([]string, []basic.Item)
}

分析器的接口,数据结构,和自定义的解析方法,Parser解析函数由使用者定义并传入分析器,接受一个response, 返回linklist, itemlist,这里使其与框架分离,是使用者按照自己的习惯和爱好选择解析方法(有类似于jquery解析方法的github.com/PuerkitoBio/goquery包,xpath语言的launchpad.net/xmlpath包等等)。对item的储存方法的接口一样的处理

爬虫停止流程

由于涉及多个channel,关闭通道的顺序尤为重要,因为本身也是靠循环通道获取值来流通整个爬虫流程的数据交流。在非意外情况下,当爬行深度大于指定值是发送停止信号,这是在Processor中出现,这时候应该关闭请求通道,按照channel的特点,去除通道的所有值时才会退出循环,退出循环时关闭响应通道。同理在响应通道数据接收完毕后跳出循环关闭link和item通道。

ReqChannel -> ResChannel -> LinkChannel && ItemChannel

工作池

没有去实现类似线程池的goroutine池,一个工作池已经能够完成任务。工作池接收一个work函数,以num个goroutine去运行,在work通过channel来控制阻塞和流通。这里也可以很容易扩展work为接口。

func (self *WorkPool) Pool(num int, work func()) {
    for w := 0; w < num; w++ {
        go work()
    }
}

全局配置

一些可以默认又提供自定义的全局参数配置,以下是一部分:

type config struct {
    Name             string
    StartUrl         string
    RequestMethod    string
    HttpHeader       map[string]string
    DownloaderNumber int
    AnalyzerNumber   int
    ProcessorNumber  int
    ReqChanLength    int
    ResChanLength    int
    LinkChanLength   int
    ItemChanLength   int
}

爬虫启动后会执行初始化,如果没有在main函数里配置相关选项将会使用一个默认的参数。使用者可以根据监视器反映的情况来调整这些参数

快速使用

下面是一个简单的例子

func main() {
    //创建一个控制器,这里有4个必须给予的参数:
    //爬取的初始url,爬取深度,解析函数,储存函数
    controller := controller.NewController("http://www.ccse.uestc.edu.cn/", 1, Parser, Store)
    //启动爬虫
    controller.Go()
}

func Parser(httpRes *http.Response) ([]string, []basic.Item) {
    //两个需要返回的列表
    linklist := make([]string, 0) //下一步需要请求的链接
    itemlist := make([]basic.Item, 0)//保存的数据类型为 map[string]interface{}

    //自定义部分
    //抓取所有链接
    doc, _ := goquery.NewDocumentFromResponse(httpRes)
    doc.Find("a").Each(func(i int, s *goquery.Selection) {
        link, exits := s.Attr("href")
        if exits {
            link = basic.CheckLink(link)
            if link != "" {
                linklist = append(linklist, link)
            }
        }
    })
    //保存每个页面的标题
    title := strings.TrimSpace(doc.Find("head title").Text())
    if title != "" {
        item := make(map[string]interface{})
        item["标题"] = title
        itemlist = append(itemlist, item)
    }

    return linklist, itemlist
}

//储存函数定义
func Store(item basic.Item) {
    //这里只打印抓取的数据
    fmt.Println(item)
}

实现了两个函数用于页面解析和数据处理(函数名任意),然后在main函数里创建一个控制器,再执行Go()函数就能开启爬虫,

还需要做的事

  • 程序不少细节待完善,包括错误处理,日志记录,监控细节,本身的Bug等等
  • 对可自定义的部分进一步完善接口
  • 考虑实现一个更通用,简洁易用的解析方法(按给定的方法去配置,类似Config),真正的解析交给分析器做
  • 在平时的使用中来完善,不断更新迭代

附上源码: Github

~~

  • 越来越喜欢Go的简洁易用,和Python配合基本能满足我的需求了
  • 再次感受到自己更乐意以解决问题的目的编程而不是创造东西的目的,只有面对问题才会激起我的欲望~~
  • Enjoy Coding

如何做一个小型公司的技术总监

资深程序员是团队中最强大的生产力,但往往被不合理的工作安排浪费掉。因此作为一个团队的技术的“头”,必须要有明确清晰的认识,把主要的事务性工作剥离出来。并且放弃大量的管理“权力”,以提高团队开发质量和效率为最主要的目标去安排自己的工作。一般来说技术总监其实会被要求做事实上是2个职位的工作:主程、项目经理(技术化)

因此必须明确此两个职位的工作任务分割。然后把项目经理的工作,安排给另外一个人做,当然其职称可能同样也得叫“技术总监”或“主程”,总之听起来越牛X越好。

而真正的主程(技术总监)则应该投身于尽量多的技术工作中。而最重要的工作则是开发——生产代码和文档。

主程的工作:

一、开发


从来没有一个资深的外科医生会放下手术刀,而转到手术室外面指手画脚。一个资深的程序员也不应该离开代码和文档的编写,而只是做做架构图。作为对一个复杂系统的负责人,必须亲手领导和参与建造,才能有足够的能力去负担起这个责任。因此需要至少使用60%的时间来参与开发的工作,并且建议从一开始上班就开始,虽然早上的效率很低,但是跟任何艰巨工作都一样:万事开头难。在你好不容易等待电脑慢吞吞的打开了所有的IDE、需求文档、参考资料、工作计划这堆要命的东西之后,你就迈出了最重要的一步,你会发现你不在需要在网上看微博和聊QQ来提振开始工作的激情,而会被某一个优化代码的灵感而激励,或者被一个复杂而有趣的问题所吸引,从而更快的能投入到开发中。坚持打开电脑做的第一件事是打开IDE软件,是这一切最重要的一步。

开发的工作内容包括有:

1.     提出非功能性需求

一般来说功能需求总是让开发人员焦头烂额的主要原因。但是实际上很多项目死在发布之后,却是因为性能、产品质量、扩展性、二次开发效率等非功能性需求没认真去解决而导致的。主程作为经验最丰富的成员,必须要利用自己曾经的经验和教训(在这里教训往往比经验重要),提出那些自己折腾自己的“非功能性需求”,来保障整个项目在发布后不会轰然倒塌。这是个吃力不讨好的工作,因为老板和客户往往只会抱怨技术人员在玩弄把戏,骗取更多的资源或者杞人忧天。如何说服这些家伙也许不是主程的工作,但是主程必须要以高度的责任心把问题放到台面上来。沟通的工作也许让项目经理去做会更好,他们有一整套如何威逼利诱老板和客户的戏法。

2.     设计和修正软件架构

软件架构设计至关重要,而且工作繁重。不画图纸就敢开工的技术人员要么是天才要么是笨蛋。对于团队来说,架构在分工合作、避免风险、提高质量等多个方面有无可替代的作用。架构要避免成为空洞的文档,最重要的一步是有人来掌控和实施。而主程主持设计和修正的架构,并且亲手实施,让团队中的腹诽之徒完全无法避开,否则代码将无法运行!所谓设计和修正架构,并不意味所有的文档应该一个人写,而是指这个架构的每个环节,都是经过主程决策同意的。当然最好这些文档能尽量由他撰写,对于“菜鸟”团队来说,输出这种文档本身就意味着“权势”,有助于主程建立个人威信——这种看起来有点肮脏的“政治”东西,在避免团队内无止境的扯皮,以及稳定那些随时准备跳槽的成员来说,都是相当实用的。

3.     难点代码(关键需求)的开发

主程必须写代码,写那些大家都认为风险大的代码。有的系统对于性能要求很高,他就必须去完成容易出性能问题的部分,比如IO操作或者设计数据库索引。有些系统的需求非常飘忽,他就要去想办法完成框架代码或者脚本引擎,以便众多小弟可以跟着产品人员疲于奔命。这种工作内容会让主程不必完全的读过所有代码,而能牢牢的“掌握”代码,以免团队成员甩耙子的时候能充当备胎。因为融入团队的代码开发,也是一个让架构设计从日常工作中真正控制系统的工作。而且主程代码通常会被别人接触,能直接教育其他团队成员,同时也能建立——威信。

4.     救火和杀虫

这个工作其实和代码开发是一致的,如果没有平日的开发,通常紧急问题的解决也是比较难处理的。但是这个也有一个调试技巧的要求,比如要求会使用各种诊断工具。这些工具一般的开发人员可能会比较少使用。找问题的过程本身也可以提高团队其他人的技术水平。

二、培训


培训的工作应该占用30%左右的工作时间。培训是稳定团队人员最重要的手段。也是提高团队开发效率最有效的手段。工具、过程、制度、奖惩,这些都代替不了程序员一行行的去写代码,最直接的方法是让他们做的更快更好,这些需要经验和知识的积累。

1.     代码审查

关于代码审查,有太多的论述。但是代码审查还是一种“强迫”推行某种风格或者技巧的手段,这是最真实的“控制”系统的手段。也是推广知识和经验最直接的手段。一个人写的代码通常应对的问题不会特别“广泛”,因此只要审查其中一部分代码,就能给大部分别的代码带来好处。

2.     技术方案评审

什么事情应该写一个技术方案,然后进行评审,这是一个关键的问题。一般认为开发时间在2周以上的单项工作应该先做个方案。往往技术方案是系统架构的完善和补充,或者是挑战。所以主程的参与是非常必要的。但是要注意不需要去做的太琐碎,而是要提炼出“关键”的需求和“关键”的解决方案进行评审,而这些“关键”往往不是功能,而是质量上的需求,如这个系统的扩展性,是否能方便后续开发等等。也有可能在这些会议上会发生争吵,但是决策人是主程的地位是不容动摇的。君子和而不同,每个程序员都可以拥有自己的看法,但是代码必须能按方案运行起来,主程必须经常申明这点。

3.     学习与讲座

如果团队碰到问题,没有新的方法和技术去解决,是不会提高开发效率的。就好像你用牛来耕地,不管用什么管理方法,都不会赶上机械化的速度。而主程承担着不断突破自己的技术上限,介绍和推动团队使用更新的技术来解决问题的责任。抱残守缺,思想僵化,最后会被团队成员所抛弃,而且也会让团队的效能落后于业界,最后直接影响产品的生死。每年学一门新语言,这个说法可能有点激进,但是这也是作为程序员应该有的激情。

三、管理


管理等于权势?管理等于沟通?管理等于文山会海?多年专业训练出来的技术人员如何去做管理?

管理的目标是提高绩效,如果和这个目标无关,而只是和“管理者”这个头衔有关的事情,最好丢给别人去做,包括那个头衔。管理主要手段是创新:想出新的方法去解决问题,而不是繁杂的事务性工作!——一个专业秘书能比主程做的好一百倍。技术工作的创新,最主要还是在技术工作里面,而不是跳出来说:做这个,做那个。

管理的事情如果超过10%的工作时间,等于说你更像一个项目经理而非主程。

1.     绩效评定

以专业的意见来衡量别人的工作,这个负担是无人能够承担的。这个工作往往是利益分配的一种手段。类似奖惩手段。这种管理方法已经不是新事物了。但是实际上技术人员对于绩效往往持一定保留和暧昧的态度,因为这种事情难以很清晰的界定出来。需要判断而非量度,才是绩效的真正手段。如果一定要打分,一共两项足够了:进度、质量,5分制即可。更重要的事情是,告诉每个人主程的看法,告诉别人,怎样做才是更好。或者告诉团队,怎样做才更有利于我们成功(发财、上市、赢得老板和客户……)——把目标清晰告诉团队,发挥他们的主动性,是绩效评定最重要的目标。

2.     需求评定

最让技术人员头疼的可能就是和客户谈判。这个事情实际上不应该让技术人员来伤心,有项目经理就可以了。而需求评定更多的是可行性的讨论。主程如果参加每个需求评定,他要三头六臂也搞不定,正确的做法应该是具体开发的团队人员参加,而主程在开会前给与自己的意见,或者会后听取参与者的总结。——这是了解别人做什么事的一个重要手段,但无需陷入太深,因为还有代码评审和项目经理的帮忙。

3.     跨部门沟通

实在没必要参加,能躲就躲,这是扯皮的天堂。让项目经理去吧,他们的专业技巧能让这些事情更加有效。只要回来后让项目经理告诉你发生了什么事情就可以了。

4.     进度审核和任务分派

又是一个很有“权势”的工作,实际上团队成员的情况大家都知道,决定谁应该做什么事情并非需要很多时间去想的事情。所以大可以把方向性的意见告诉项目经理,让他去做。很多优秀的开发者玩EXCELPROJECT之类的水平还不如只有一年工作经验的秘书,别折腾自己了。

5.     面试

如果真想帮忙,准备一份有区分度的笔试题目吧。不靠谱的人太多,老板可不是花钱请你和他们聊天的。让项目经理去聊,不用担心他们技术不强,再不够,也会比大多数面试者要牛X。他们搞不定的人,就是应该雇佣的家伙。毕业生招聘怎么办?只要看看他们课外活动是不是有搞些专业的事情就可以了,上进心比别的东西都重要,HR会比主程看的更准,相信我。

6.     各种会议

饭无好饭,会无好会,超过6个人的会议应该坚决抵制。如果你有一个程序等着你去写,你一定无比痛恨这些会议,顺应你的内心吧!上帝保佑你。

最后说说项目经理的工作:

项目经理就像下水道的清洁工,所有那些主程不愿意去做的事情,他们都弯下腰去认真的把玩,实在是太伟大了。既然如此,为何不让他们拥有更好一点的头衔呢?如果没有他们去处理这些工作,任何一个主程都会被逼疯掉,或者他们自己变成了项目经理,让团队损失了最强力的一台代码发动机。

一、进度

1.     指定工作计划

2.     进度检查和告警

3.     工作总结和统计

二、资源

1.     整合提供各种资源,如找DBA,IT,运维人员,硬件,SVN权限,测试环境,福利,周末的活动……

2.     面试:人员是最重要的资源,不是吗?

3.     资源谈判:往往是和老板谈判,让别人明白现在的真实情况。又一个吃力不讨好的差事,但是总需要人做。

三、沟通

1.     需求评审:和需求方讨价还价,项目经理真是命苦啊……

2.     组织会议或者用其他方式通知信息给所有人:小喇叭、大喇叭、全服广播、世界频道……

对于一个小型公司,职权,头衔,收益,往往会更加敏感。但是这些都不是让项目失败的理由。一颗叫程序员的种子说:长大了我就是叫管理者的树。这个错误的观念只会让这个种子永远无法发芽。软件开发是类似外科医生的行业,而不是血汗工厂,所以不需要手持皮鞭的经理,而需要仁心仁术的神医。