当前位置: 首页 > 编程日记 > 正文

SRWebSocket源码浅析(上)

2017-06-12 涂耀辉 Cocoa开发者社区



一. 前言:


  • WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——可以通俗的解释为服务器主动发送信息给客户端。

  • 区别于MQTT、XMPP等聊天的应用层协议,它是一个传输通讯协议。它有着自己一套连接握手,以及数据传输的规范。

  • 而本文要讲到的SRWebSocket就是iOS中使用websocket必用的一个框架,它是用Facebook提供的。


关于WebSocket起源与发展,是怎么由:轮询、长轮询、再到websocket的,可以看看冰霜这篇文章:

微信,QQ这类IM app怎么做——谈谈Websocket


关于SRWebSocket的API用法,可以看看楼主之前这篇文章:

iOS即时通讯,从入门到“放弃”?


二. SRWebSocket的对外的业务流程:


首先贴一段SRWebSocket的API调用代码:


//初始化socket并且连接

- (void)connectServer:(NSString *)server port:(NSString *)port

{


    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"ws://%@:%@",server,port]]];

    _socket = [[SRWebSocket alloc] initWithURLRequest:request];

    _socket.delegate = self;

    [_socket open];

}


- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message

{


}

- (void)webSocketDidOpen:(SRWebSocket *)webSocket

{


}

- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error

{


}

- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean

{


}


要简单使用起来,总共就4行代码,并且实现你需要的代理即可,整个业务逻辑非常简洁。


但是就这么几个对外的方法,SRWebSocket.m里面用了2000行代码来进行封装,那么它到底做了什么?我们接着往下看:


三. SRWebSocket的初始化以及连接流程:


1首先我们初始化:


//初始化

- (void)_SR_commonInit;

{

    //得到url schem小写

    NSString *scheme = _url.scheme.lowercaseString;

        //如果不是这几种,则断言错误

    assert([scheme isEqualToString:@"ws"] || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]);


    _readyState = SR_CONNECTING;

    _webSocketVersion = 13;

     //初始化工作的队列,串行

    _workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);


    //给队列设置一个标识,标识为指向自己的,上下文对象为这个队列

    dispatch_queue_set_specific(_workQueue, (__bridge void *)self, maybe_bridge(_workQueue), NULL);


    //设置代理queue为主队列

    _delegateDispatchQueue = dispatch_get_main_queue();


    //retain主队列?

    sr_dispatch_retain(_delegateDispatchQueue);


    //读Buffer

    _readBuffer = [[NSMutableData alloc] init];

    //输出Buffer

    _outputBuffer = [[NSMutableData alloc] init];

    //当前数据帧

    _currentFrameData = [[NSMutableData alloc] init];

    //消费者数据帧的对象

    _consumers = [[NSMutableArray alloc] init];


    _consumerPool = [[SRIOConsumerPool alloc] init];

    //注册的runloop

    _scheduledRunloops = [[NSMutableSet alloc] init];

    ....省略了一部分代码

}


会初始化一些属性:


  • 包括对schem进行断言,只支持ws/wss/http/https四种。

  • 当前socket状态,是正在连接,还是已连接、断开等等。

  • 初始化工作队列,以及流回调线程等等。

  • 初始化读写缓冲区:_readBuffer、_outputBuffer。


2. 输入输出流的创建及绑定:


//初始化流

- (void)_initializeStreams;

{

    //断言 port值小于UINT32_MAX

    assert(_url.port.unsignedIntValue <= UINT32_MAX);

    //拿到端口

    uint32_t port = _url.port.unsignedIntValue;

    //如果端口号为0,给个默认值,http 80 https 443;

    if (port == 0) {

        if (!_secure) {

            port = 80;

        } else {

            port = 443;

        }

    }

    NSString *host = _url.host;


    CFReadStreamRef readStream = NULL;

    CFWriteStreamRef writeStream = NULL;

    //用host创建读写stream,Host和port就绑定在一起了

    CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, port, &readStream, &writeStream);


    //绑定生命周期给ARC  _outputStream = __bridge transfer

    _outputStream = CFBridgingRelease(writeStream);

    _inputStream = CFBridgingRelease(readStream);


    //代理设为自己

    _inputStream.delegate = self;

    _outputStream.delegate = self;

}


在这里,我们根据传进来的url,类似ws://localhost:80,进行输入输出流CFStream的创建及绑定。


Output&Iput.png


到这里,初始化工作就完成了,接着我们调用了open开始建立连接:


//开始连接

- (void)open;

{

    assert(_url);

    //如果状态是正在连接,直接断言出错

    NSAssert(_readyState == SR_CONNECTING, @"Cannot call -(void)open on SRWebSocket more than once");


    //自己持有自己

    _selfRetain = self;

    //判断超时时长

    if (_urlRequest.timeoutInterval > 0)

    {

        dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, _urlRequest.timeoutInterval * NSEC_PER_SEC);

        //在超时时间执行

        dispatch_after(popTime, dispatch_get_main_queue(), ^(void){

            //如果还在连接,报错

            if (self.readyState == SR_CONNECTING)

                [self _failWithError:[NSError errorWithDomain:@"com.squareup.SocketRocket" code:504 userInfo:@{NSLocalizedDescriptionKey: @"Timeout Connecting to Server"}]];

        });

    }

    //开始建立连接

    [self openConnection];

}


open方法定义了一个超时,如果超时了还在SR_CONNECTING,则报错,并且断开连接,清除一些已经初始化好的参数。


//开始连接

- (void)openConnection;

{

    //更新安全、流配置

    [self _updateSecureStreamOptions];


    //判断有没有runloop

    if (!_scheduledRunloops.count) {

        //SR_networkRunLoop会创建一个带runloop的常驻线程,模式为NSDefaultRunLoopMode。

        [self scheduleInRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode];

    }


    //开启输入输出流

    [_outputStream open];

    [_inputStream open];

}


- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;

{

    [_outputStream scheduleInRunLoop:aRunLoop forMode:mode];

    [_inputStream scheduleInRunLoop:aRunLoop forMode:mode];


    //添加到集合里,数组

    [_scheduledRunloops addObject:@[aRunLoop, mode]];

}


开始连接主要是给输入输出流绑定了一个runloop,说到这个runloop,不得不提一下SRWebSocket线程的问题:


  • 一开始初始化我们提过SRWebSocket有一个工作队列:


dispatch_queue_t _workQueue;


这个工作队列是串行的,所有和控制有关的操作,除了一开始初始化和open操作外,所有后续的回调操作,数据写入与读取,出错连接断开,清除一些参数等等这些操作,全部是在这个_workQueue中进行的。


  • 而这里的runloop:


+ (NSRunLoop *)SR_networkRunLoop {

 static dispatch_once_t onceToken;

 dispatch_once(&onceToken, ^{

     networkThread = [[_SRRunLoopThread alloc] init];

     networkThread.name = @"com.squareup.SocketRocket.NetworkThread";

     [networkThread start];

     //阻塞方式拿到当前runloop

     networkRunLoop = networkThread.runLoop;

 });


 return networkRunLoop;

}


是新创建了一个NSThread的线程,然后起了一个runloop,这个是以单例的形式创建的,所以networkThread作为属性是一直存在的,而且起了一个runloop,这个runloop没有调用过退出的逻辑,所以这个networkThread是个常驻线程,即使socket连接断开,即使SRWebSocket对象销毁,这个常驻线程仍然存在。


可能很多朋友会觉得,那我都不用websocket了,什么都置空了,凭什么还有一个常驻线程,不停的空转,给内存和CPU造成一定开销呢?


楼主的理解是,作者这么做,可能考虑的是既然用户有长连接的需求,肯定断开连接甚至清空websocket对象只是一时的选择,肯定是很快会重新初始化并且重连的,这样这个常驻线程就可以得到复用,省去了重复创建,以及获取runloop等开销。


  • 那么SRWebSocket总共就有一个串行的_workQueue和一个常驻线程networkThread,前者用来控制连接,后者用来注册输入输出流,那么为什么这些操作不在一个常驻线程中去做呢?


我觉得这里就涉及一个线程的任务调度问题了,试想,如果控制逻辑和输入输出流的回调都是在同一个线程,对于输入输出流来说,回调是会非常频繁的,首先写_outputStream是在当前流NSStreamEventHasSpaceAvailable还有空间可写的时候,一直会回调,而读_inputStream则在有数据到达时候,也会不停的回调,试想如果这时候,控制逻辑需要做什么处理,是不是会有很大的延迟?它需要等到排在它前面插入线程中的任务调度完毕,才能轮得到这些控制逻辑的执行。所以在这里,把控制逻辑放在一个串行队列,而数据流的回调放在一个常驻线程,两个线程不会互相污染,各司其职。


接着主流程往下走,我们open了输入输出流后,就调用到了流的代理方法了:


//开启流后,收到事件回调

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode;

{

    __weak typeof(self) weakSelf = self;


    // 如果是ssl,而且_pinnedCertFound 为NO,而且事件类型是有可读数据未读,或者事件类型是还有空余空间可写

    if (_secure && !_pinnedCertFound && (eventCode == NSStreamEventHasBytesAvailable || eventCode == NSStreamEventHasSpaceAvailable)) {

           //省略SSL的一些处理....

            //如果为NO,则验证失败,报错关闭

            if (!_pinnedCertFound) {

                  //关闭连接

                dispatch_async(_workQueue, ^{

                    NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : @"Invalid server cert" };

                    [weakSelf _failWithError:[NSError errorWithDomain:@"org.lolrus.SocketRocket" code:23556 userInfo:userInfo]];

                });

                return;

            } else if (aStream == _outputStream) {

                //如果流是输出流,则打开流成功

                dispatch_async(_workQueue, ^{

                    [self didConnect];

                });

            }

        }

    }

    dispatch_async(_workQueue, ^{

        [weakSelf safeHandleEvent:eventCode stream:aStream];

    });

}


这里如果我们一开始初始化的url是 wss/https,会做SSL认证,认证流程基本和楼主之前讲的CocoaAsyncSocket,这里就不赘述了,认证失败,会断开连接,


最终SSL或者非SSL都会走到这么一个方法:


//流打开成功后的操作,开始发送http请求建立连接

- (void)didConnect;

{

    SRFastLog(@"Connected");

    //创建一个http request  url

    CFHTTPMessageRef request = CFHTTPMessageCreateRequest(NULL, CFSTR("GET"), (__bridge CFURLRef)_url, kCFHTTPVersion1_1);


    // Set host first so it defaults

    //设置head, host:  url+port

    CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Host"), (__bridge CFStringRef)(_url.port ? [NSString stringWithFormat:@"%@:%@", _url.host, _url.port] : _url.host));

    //密钥数据(生成对称密钥)

    NSMutableData *keyBytes = [[NSMutableData alloc] initWithLength:16];

    //生成随机密钥

    SecRandomCopyBytes(kSecRandomDefault, keyBytes.length, keyBytes.mutableBytes);


    //根据版本用base64转码

    if ([keyBytes respondsToSelector:@selector(base64EncodedStringWithOptions:)]) {

        _secKey = [keyBytes base64EncodedStringWithOptions:0];

    } else {

#pragma clang diagnostic push

#pragma clang diagnostic ignored "-Wdeprecated-declarations"

        _secKey = [keyBytes base64Encoding];

#pragma clang diagnostic pop

    }


    //断言编码后长度为24

    assert([_secKey length] == 24);


    // Apply cookies if any have been provided

    //提供cookies

    NSDictionary * cookies = [NSHTTPCookie requestHeaderFieldsWithCookies:[self requestCookies]];

    for (NSString * cookieKey in cookies) {

        //拿到cookie值

        NSString * cookieValue = [cookies objectForKey:cookieKey];

        if ([cookieKey length] && [cookieValue length]) {

            //设置到request的 head里

            CFHTTPMessageSetHeaderFieldValue(request, (__bridge CFStringRef)cookieKey, (__bridge CFStringRef)cookieValue);

        }

    }


    // set header for http basic auth

    //设置http的基础auth,用户名密码认证

    if (_url.user.length && _url.password.length) {

        NSData *userAndPassword = [[NSString stringWithFormat:@"%@:%@", _url.user, _url.password] dataUsingEncoding:NSUTF8StringEncoding];

        NSString *userAndPasswordBase64Encoded;

        if ([keyBytes respondsToSelector:@selector(base64EncodedStringWithOptions:)]) {

            userAndPasswordBase64Encoded = [userAndPassword base64EncodedStringWithOptions:0];

        } else {

#pragma clang diagnostic push

#pragma clang diagnostic ignored "-Wdeprecated-declarations"

            userAndPasswordBase64Encoded = [userAndPassword base64Encoding];

#pragma clang diagnostic pop

        }

        //编码后用户名密码

        _basicAuthorizationString = [NSString stringWithFormat:@"Basic %@", userAndPasswordBase64Encoded];

        //设置head Authorization

        CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Authorization"), (__bridge CFStringRef)_basicAuthorizationString);

    }

    //web socket规范head

    CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Upgrade"), CFSTR("websocket"));

    CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Connection"), CFSTR("Upgrade"));

    CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Key"), (__bridge CFStringRef)_secKey);

    CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Version"), (__bridge CFStringRef)[NSString stringWithFormat:@"%ld", (long)_webSocketVersion]);


    //设置request的原始 Url

    CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Origin"), (__bridge CFStringRef)_url.SR_origin);


    //用户初始化的协议数组,可以约束websocket的一些行为

    if (_requestedProtocols) {

        CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Protocol"), (__bridge CFStringRef)[_requestedProtocols componentsJoinedByString:@", "]);

    }


    //吧 _urlRequest中原有的head 设置到request中去

    [_urlRequest.allHTTPHeaderFields enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {


        CFHTTPMessageSetHeaderFieldValue(request, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);

    }];


    //返回一个序列化 , CFBridgingRelease和 __bridge transfer一个意思, CFHTTPMessageCopySerializedMessage copy一份新的并且序列化,返回CFDataRef

    NSData *message = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(request));


    //释放request

    CFRelease(request);


    //把这个request当成data去写

    [self _writeData:message];

    //读取http的头部

    [self _readHTTPHeader];

}


这个方法有点长,大家都知道,WebSocket建立连接前,都会以http请求作为握手的方式,这个方法就是在构造http的请求头。


我们来看看RFC规范的标准客户端请求头:


GET /chat HTTP/1.1

Host: server.example.com

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

Origin: http://example.com

Sec-WebSocket-Protocol: chat, superchat

Sec-WebSocket-Version: 13


标准的服务端响应头:


HTTP/1.1 101 Switching Protocols

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Sec-WebSocket-Protocol: chat


这里需要讲的是这Sec-WebSocket-Key和Sec-WebSocket-Accept这一对值,前者是我们客户端自己生成一个16字节的随机data,然后经过base64转码后的一个随机字符串。


而后者则是服务端返回回来的,我们需要用一开始的Sec-WebSocket-Key与服务端返回的Sec-WebSocket-Accept进行校验:


//检查握手信息

- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage;

{

    //是否是允许的header

    NSString *acceptHeader = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Sec-WebSocket-Accept")));


    //为空则被服务器拒绝

    if (acceptHeader == nil) {

        return NO;

    }


    //得到

    NSString *concattedString = [_secKey stringByAppendingString:SRWebSocketAppendToSecKeyString];

    //期待accept的字符串

    NSString *expectedAccept = [concattedString stringBySHA1ThenBase64Encoding];


    //判断是否相同,相同就握手信息对了

    return [acceptHeader isEqualToString:expectedAccept];

}


服务端这个Accept会用这么一个字符串拼接加密:


static NSString *const SRWebSocketAppendToSecKeyString = @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";


这个字符串是RFC规范定死的,至于为什么是这么一串,楼主也不知所以然。


我们发出这个http请求后,得到服务端的响应头,去按照服务端的方式加密Sec-WebSocket-Key,判断与Sec-WebSocket-Accept是否相同,相同则表明握手成功,否则失败处理。


handshake.png


至此都成功的话,一个WebSocket连接建立完毕。


(接下文)

相关文章:

Mason 简单笔记

Mason的对象 ------------------------------- Request对象 Mason有两个全局预处理对象叫做&#xff1a;$r和$m $r是mod_perl的请求对象&#xff0c;它提供了Perl的API来执行Apache的请求。 $r->;uri #获得用户请求的地址 $r->;content_type #获得…

多级页表如何节省内存

在谈到多级页表的优势的时候&#xff0c;很多地方都是这么说的&#xff1a;32位地址空间的分页系统&#xff0c;如果页面大小为4KB&#xff0c;则每个进程可达1M个页&#xff0c;假设每个页表项占用4个字节&#xff0c;这样每个进程仅仅页表项就占用了4MB连续的内存空间。 那么…

Spring Cloud应用开发(二:实现服务间的调用)

1、搭建订单服务工程。 注&#xff1a;在父工程cloud中&#xff0c;创建Maven子模块ms-spring-eureka-server&#xff1b; 1.1、添加依赖&#xff0c;在pom文件中添加Eureka依赖&#xff1b; 1.2、写配置文件&#xff0c;在配置文件中添加Eureka服务实例的端口号、服务端地址等…

webSocket详解

前言 本文会用实例的方式&#xff0c;将iOS各种IM的方案都简单的实现一遍。并且提供一些选型、实现细节以及优化的建议。 注&#xff1a;文中的所有的代码示例&#xff0c;在github中都有demo&#xff1a; iOS即时通讯&#xff0c;从入门到“放弃”&#xff1f;(demo) 可以打…

2013多校第三场

hdu 4629 题意&#xff1a;给你n个三角形&#xff0c;问覆盖1~n次的面积各是多少&#xff0c;n < 50; 分析&#xff1a;取出所有端点和交点的x坐标&#xff0c;排序&#xff0c;然后对于每一段xi~xi1的范围的线段都是不相交的&#xff0c;所以组成的 面积要么是三角形&#…

React+Reflux博客实践

年初用ReactExpressMongodb写的一个简单的博客。分享给各位朋友参考。 前端&#xff1a;ReactReact RouterRefluxReact-BootstrapWebpack后端&#xff1a;Express(Node.js)Ejs(Index)Mongoose(mongodb) 博客Demo地址&#xff1a;http://itdotaerblog.herokuapp.comGithub Addre…

Spring Cloud应用开发(三:客户端的负载均衡)

1、Ribbon的使用 注&#xff1a;在石榴啊RestTemplate的方法上添加LoadBalanced注解&#xff0c;并在其执行方法中使用服务实例的名称即可&#xff1b; 1.1、添加LoadBalanced注解&#xff0c;在ms-spring-eureka-user工程引导类中的RestTemplate&#xff08;&#xff09;方法…

SRWebSocket源码浅析(下)

接上文&#xff09; 四. 接着来讲讲数据的读和写&#xff1a; 当建立连接成功后&#xff0c;就会循环调用这么一个方法&#xff1a; //读取http头部 - (void)_readHTTPHeader; { if (_receivedHTTPHeaders NULL) { //序列化的http消息 _receivedHTTPHeaders CFHTTPMessageCre…

(IOS)签名Demo

思路是将每一次按下屏幕的touch move时的点存到一个数组里&#xff0c;即一个数组相当于一个笔画&#xff1b;再将该代表笔画的数组保存到一个大数组中&#xff0c;每组每次touch的移动都历遍大数组和笔画数组&#xff0c;将点于点之间连接起来。 #import <UIKit/UIKit.h>…

debug运行可以,release运行报错的原因及修改方法

通常我们开发的程序有2种模式:Debug模式和Release模式在Debug模式下,编译器会记录很多调试信息,也可以加入很多测试代码,方便我们程序员测试,以及出现bug时的分析解决Release模式下,就没有上述那些调试信息,而且编译器也会自动优化一些代码,这样生成的程序性能是最优的,但是如果…

Spring Cloud应用开发(四:服务容错保护)

1、Spring Cloud Hystrix的使用 1.1、创建microservice-eureka-user-hystrix工程&#xff0c;并在其pom.xml中引入eureka和hystrix的依赖&#xff1b; 1.2、编写配置文件。在配置文件中添加Eureka服务实例的端口号&#xff0c;服务端地址等&#xff1b; 1.3、在工程主类Applic…

计量注册师考试一些关于期限、时间、机构的总结

1&#xff1a;有效期&#xff1a; 认证5年&#xff0c;基准5年&#xff0c;标准4年&#xff0c;机构授权3年&#xff0c;注册计量师注册证3年&#xff0c;制造、修理许可证3年。 提前量&#xff1a;标准考核提前6个月&#xff0c;注册计量师在有效期满前30工作日内提出申请延续…

TinyCrayon-iOS-SDK:强大到使人惊讶的 Mask 及切图工具库

原文链接&#xff1a;https://github.com/TinyCrayon/TinyCrayon-iOS-SDKTinyCrayon-iOS-SDK&#xff1a;强大到使人惊讶的 Mask 及切图工具库。# 为开源点赞# —— 由SwiftLanguage分享A smart and easy-to-use image masking and cutout SDK for mobile apps. TinyCrayon SDK…

Android之自定义AlertDialog无法监听控件

参考&#xff1a;http://www.cnblogs.com/511mr/archive/2011/10/21/2220253.html 要做一个自定义的弹出框&#xff0c;以前都是用一个Activity来实现&#xff0c;总觉得不是很好看&#xff0c;弹出的框有时候感觉有点大&#xff0c;所以上网查资料说&#xff0c;可以给AlertDi…

Spring Cloud应用开发(五:API网关服务)

1、使用Zuul构建API网关服务&#xff1b; 注&#xff1a;本服务涉及到3个工程&#xff0c;起作用分别如下&#xff1a; ms-spring-eureka-server工程&#xff1a;服务注册中心&#xff0c;端口为8761。ms-spring-eureka-order工程&#xff1a;服务提供者&#xff0c;需要启动…

多态---父指针指向子类对象(父类引用指向子类对象)

我们都知道&#xff0c;面向对象程序设计中的类有三大特性&#xff1a;继承&#xff0c;封装&#xff0c;多态&#xff0c;这个也是介绍类的时候&#xff0c;必须提到的话题&#xff0c;那么今天就来看一下OC中类的三大特性&#xff1a; 一、封装 封装就是对类中的一些字段&…

ARKit从入门到精通-ARKit工作原理及流程介绍

2017-06-15 坤小 Cocoa开发者社区转载请注明出处:http://www.jianshu.com/p/0492c7122d2f 1.1-写在前面的话 1.2-ARKit与SceneKit的关系 1.3-ARKit工作原理 1.3.1-ARSCNView与ARSession 1.3.2-ARWorldTrackingSessionConfiguration与ARFrame 1.4-ARKit工作完整流程 1…

【C语言也能干大事】第五讲 组合框控件,下拉列表

获得组合框控件的句柄HWND hwndCombo1 GetDlgItem(hwnd, IDC_COMBO1); 确定目前选项的索引 int curSel ComboBox_GetCurSel(hwndCombo1); 删除项 ComboBox_DeleteString(hwndCombo1, 2); 取得有多少项int getCount ComboBox_GetCount(hwndCombo1);TCHAR getcount[256];itoa(…

Spring Cloud应用开发(六:使用本地存储方式实现分布式配置管理 )

1、搭建Config Server&#xff1b; 1.1、创建配置中心工程microservice-config-server&#xff0c;并在其pom.xml中引入Config Server的依赖&#xff1b; 1.2、编写配置文件application.yml&#xff0c;添加服务端口号和存储属性等信息&#xff1b; 1.3、在scr/main/resources…

PL SQL笔记(三)

loopif credit_rating < 3 then..exit;end if; end loop; select to_char(sysdate, YYYY-MM-DD HH24:MI:SS) from dual; select cast(sysdate as timestamp) from dual; 复合类型数据 1.记录: declaretypeemp_record_typeis record(r_name emp.ename%type,r_job emp.job%typ…

iOS-仿膜拜贴纸滚动(物理仿真)

导读 简单用OC写了一个小球滚动效果; 类似平衡球. GitHub地址&#xff1a;https://github.com/wangliujiayou/WLBallView 欢迎Star. 膜拜滚动进入正题-(传感器) 传感器是一种感应\检测装置, 目前已经广泛应用于智能手机上&#xff0c;用于感应\检测设备周边的信息&#xff0c;不…

Redhat、centos安装配置postgresql

一.安装postgresql 本文仅以 redhat&#xff0c;postgresql9.4为例&#xff0c;使用yum方式进行介绍。 官网&#xff1a;http://www.postgresql.org/download/linux/redhat/ 1.下载postgresql的yum源 yum install http://yum.postgresql.org/9.4/redhat/rhel-6-x86_64/pgdg-red…

Spring Cloud应用开发(七:使用Git存储方式实现分布式配置管理 )

1、使用Git存储实现管理&#xff1b; 1.1、配置Git。在Git上创建microservice-study-config目录&#xff0c;并在目录中添加开发&#xff0c;预发布和测试的配置文件&#xff1b; 1.2、修改服务端配置文件。将microservice-config-server工程的配置文件中本地文件存储方式的配…

IOS 自定义相机, 使用 AVFoundation(附实现部分腾讯水印相机功能 demo)

原文链接&#xff1a;http://www.jianshu.com/p/c64bf543f16a这是一款使用 AVFoundation 自定义的相机&#xff0c;与系统原生相机有一样的外观但比系统的相机更好、更符合实际的开发需要、可以自己修改 UI、实现拍照、取消、闪光灯控制、前后摄像头控制、聚焦、放大缩小、拍照…

如何成为一个好的测试工程师(转载,大意)

对于测试功能是的两个不同观点&#xff1a;软实力和技术能力。 个人觉得技术能力80%可以被大众掌握&#xff0c;但是软实力是需要花费很多时间去培养和塑造的。一下几点也是能够衡量个人技能的一些标准&#xff1a; 1&#xff0c;沟通技能-口头和书面能力 与人的第一印象&#…

ubuntu下7z文件的解压方法

apt-get install p7zip-full 控制台会打出以下信息&#xff1a; 正在读取软件包列表... 完成正在分析软件包的依赖关系树 正在读取状态信息... 完成 建议安装的软件包&#xff1a; p7zip-rar下列【新】软件包将被安装&#xff1a; p7zip-full升级了 0 个软件包&…

Docker的使用(一:Docker入门程序)

1、编写Dockerfile文件&#xff1b; 注&#xff1a;创建一个空的Docker工作目录dockerspace&#xff0c;进而进入该目录&#xff0c;并创建编写dockerfile文件&#xff1b; 2、编写外部文件。 注&#xff1a;在当前目录&#xff08;dockerspace&#xff09;下分别创建require…

iOS OpenCV 图像灰度处理

2017-06-21 小溪彼岸 Cocoa开发者社区推荐人&#xff1a;wo709128079 有时候开发过程中&#xff0c;切图满足不了我们的需求&#xff0c;此时我们需要对图像进行灰度处理&#xff0c;例如QQ头像在线、离线等不同状态等。 可以尝试的解决方案&#xff1a; 第一种&#xff1a;让U…

【VS开发】【电子电路技术】RJ45以太网传输线研究

RJ45以太网传输线研究 最近研究远距离差分视频传输方案&#xff0c;理所当然想到了LVDS协议。至于选用cameralink传输线&#xff0c;还是选用其他方案&#xff0c;本人更倾向于廉价的RJ45以太网线来实现LVDS差分信号的传输。 由于RJ45网线内部为4对双绞线&#xff0c;至于以太网…

Wiz开发 定时器的使用与处理

这只是一些代码片段&#xff0c;由于Wiz开发的资料实在不多&#xff0c;而且内容都不够新。这里的代码主要参考Tools.Timer这个插件&#xff0c;但是由于内部实现的很多变化&#xff0c;Tools.Timer这个插件基本上已经无法使用了。定时器的注册与删除 使用定时器&#xff0c;是…