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

SRWebSocket源码浅析(下)

接上文)


四. 接着来讲讲数据的读和写:


当建立连接成功后,就会循环调用这么一个方法:


//读取http头部

- (void)_readHTTPHeader;

{

    if (_receivedHTTPHeaders == NULL) {

        //序列化的http消息

        _receivedHTTPHeaders = CFHTTPMessageCreateEmpty(NULL, NO);

    }

    //不停的add consumer去读数据

    [self _readUntilHeaderCompleteWithCallback:^(SRWebSocket *self,  NSData *data) {


        //拼接数据,拼到头部

        CFHTTPMessageAppendBytes(_receivedHTTPHeaders, (const UInt8 *)data.bytes, data.length);


        //判断是否接受完

        if (CFHTTPMessageIsHeaderComplete(_receivedHTTPHeaders)) {

            SRFastLog(@"Finished reading headers %@", CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(_receivedHTTPHeaders)));

            [self _HTTPHeadersDidFinish];

        } else {

            //没读完递归调

            [self _readHTTPHeader];

        }

    }];

}


记得楼主之前写过一篇即时通讯下数据粘包、断包处理实例(基于CocoaAsyncSocket),因此抛出一个问题,WebSocket需要处理数据的断包和粘包么?


答案是基本不需要。引用知乎上的一段回答:


RFC规范指出,WebSocket是一个message-based的协议,它可以自动将数据分片,并且自动将分片的数据组装。


也就是说,WebSocket的RFC标准是不会产生粘包、断包问题的。无需应用层开发人员关心缓存以及手工组装message。


然而理想与现实的不一致:RFC规范与实现的不一致,现实当中有几个问题:


  1. 每个message可以是一个或多个分片。message不记录长度,分片才记录长度。

  2. message最大的长度可以达到 9,223,372,036,854,775,807 字节,是由于Payload的数据长度有63bit的限制。

  3. 很多WebSocket的实现其实并不按照标准的RFC实现完全,很多仅仅实现了50%就拿来用了。这就导致了,在WebSocket实现上的最大长度很难达到这个大小,于是,很多API的实现上是会有限制的,可能会限制你的发送的长度,也可能会把过长的数据直接以流式发送。


而SRWebSocket中实现的方式上彻底解决了数据粘包,断包的可能。

数据是通过CFStream流的方式回调回来的,每次拿到流数据,都是先放在数据缓冲区中,然后去读当前消息帧的头部,得到当前数据包的大小,然后再去创建消费者对象consumer,去读取缓冲区指定数据包大小的内容,读完才会回调给我们上层用户,所以,我们如果用SRWebSocket完全不需要考虑数据断包、粘包的问题,每次到达的数据,都是一条完整的数据。


接着我们大概来看看这个流程:


//读取CRLFCRLFBytes,直到回调回来

- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler;

{

    [self _readUntilBytes:CRLFCRLFBytes length:sizeof(CRLFCRLFBytes) callback:dataHandler];

}


//读取数据 CRLFCRLFBytes,边界符

- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler;

{

    // TODO optimize so this can continue from where we last searched


    //消费者需要消费的数据大小

    stream_scanner consumer = ^size_t(NSData *data) {

        __block size_t found_size = 0;

        __block size_t match_count = 0;

        //得到数据长度

        size_t size = data.length;

        //得到数据指针

        const unsigned char *buffer = data.bytes;

        for (size_t i = 0; i < size; i++ ) {

            //匹配字符

            if (((const unsigned char *)buffer)[i] == ((const unsigned char *)bytes)[match_count]) {

                //匹配数+1

                match_count += 1;

                //如果匹配了

                if (match_count == length) {

                    //读取数据长度等于 i+ 1

                    found_size = i + 1;

                    break;

                }

            } else {

                match_count = 0;

            }

        }

        //返回要读取数据的长度,没匹配成功就是0

        return found_size;

    };

    [self _addConsumerWithScanner:consumer callback:dataHandler];

}


上面这个方法就是一个读取头部的方法,之前我写过断包粘包的文章就是用一个\r\n来分割头部和正文,这里是用了\r\n\r\n,每次读到这个标识符为止,就是读取了一个完整的WebSocket的消息帧头部。


这里我们先需要说清楚的是,数据一到达,就在stream的代理中回调中,写到了我们的_readBuffer缓冲区中去了:


case NSStreamEventHasBytesAvailable: {

    SRFastLog(@"NSStreamEventHasBytesAvailable %@", aStream);

    const int bufferSize = 2048;

    uint8_t buffer[bufferSize];

    //如果有可读字节

    while (_inputStream.hasBytesAvailable) {

        //读取数据,一次读2048

        NSInteger bytes_read = [_inputStream read:buffer maxLength:bufferSize];


        if (bytes_read > 0) {

            //拼接数据

            [_readBuffer appendBytes:buffer length:bytes_read];

        } else if (bytes_read < 0) {

            //读取错误

            [self _failWithError:_inputStream.streamError];

        }

        //如果读取的不等于最大的,说明读完了,跳出循环

        if (bytes_read != bufferSize) {

            break;

        }

    };

    //开始扫描,看消费者什么时候消费数据

    [self _pumpScanner];

    break;

}


接着我们来看添加消费者这个方法:


//指定数据读取

- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback;

{

    [self assertOnWorkQueue];

    [self _addConsumerWithScanner:consumer callback:callback dataLength:0];

}


//添加消费者,用一个指定的长度,是否读到当前帧

- (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;

{   

    [self assertOnWorkQueue];

    assert(dataLength);

    //添加到消费者队列去

    [_consumers addObject:[_consumerPool consumerWithScanner:nil handler:callback bytesNeeded:dataLength readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes]];

    [self _pumpScanner];

}


- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength;

{    

    [self assertOnWorkQueue];

    [_consumers addObject:[_consumerPool consumerWithScanner:consumer handler:callback bytesNeeded:dataLength readToCurrentFrame:NO unmaskBytes:NO]];

    [self _pumpScanner];

}


其实就是添加了一个stream_scanner类型的对象,到我们的_consumers数组中去了,以后我们读取数据,都会先取出_consumers中的消费者,要读取多少,就给你从_readBuffer里去读多少数据。


//开始扫描

-(void)_pumpScanner;

{

    [self assertOnWorkQueue];

    //判断是否在扫描

    if (!_isPumping) {

        _isPumping = YES;

    } else {

        return;

    }

    //只有为NO能走到这里,开始循环检测,可读可写数据

    while ([self _innerPumpScanner]) {


    }

    _isPumping = NO;

}


这个方法就是做这么一件事,根据consumer的要求,循环去_readBuffer中读取数据。


至于读的过程,大家可以自己去看下吧,楼主提供的源码注释里已经写的很清楚了,有点略长,这里就不放代码了,方法如下:


- (BOOL)_innerPumpScanner 

{

    ...

}


至此我们讲了握手的头部信息的读取,与判断是否握手成功,然后数据到达是怎么从stream到_readBuffer中去的,并且简单介绍了_pumpScanner会根据消费者对象,去从_readBuffer中读取数据,读取完成并且回调consumer的handler


现在我们来讲讲一个数据从头部开始,到内容的读取过程:


每次我们读取新的一帧数据,都会调用这么个方法:


//读取新的消息帧

- (void)_readFrameNew;

{

    dispatch_async(_workQueue, ^{

        //清空上一帧的

        [_currentFrameData setLength:0];


        _currentFrameOpcode = 0;

        _currentFrameCount = 0;

        _readOpCount = 0;

        _currentStringScanPosition = 0;

        //继续读取

        [self _readFrameContinue];

    });

}


会清空上一帧的一些信息,然后开始当前帧的读取,我们来简单看看一个WebSocket消息帧里包含什么:


 0                   1                   2                   3

 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1

 +-+-+-+-+-------+-+-------------+-------------------------------+

 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |

 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |

 |N|V|V|V|       |S|             |   (if payload len==126/127)   |

 | |1|2|3|       |K|             |                               |

 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +

 |     Extended payload length continued, if payload len == 127  |

 + - - - - - - - - - - - - - - - +-------------------------------+

 |                               |Masking-key, if MASK set to 1  |

 +-------------------------------+-------------------------------+

 | Masking-key (continued)       |          Payload Data         |

 +-------------------------------- - - - - - - - - - - - - - - - +

 :                     Payload Data continued ...                :

 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +

 |                     Payload Data continued ...                |

 +---------------------------------------------------------------+


就是这么一张图,大家应该经常见,这个图是RFC的标准规范。简单的说明下这些标识着什么:


FIN 1bit 表示信息的最后一帧,flag,也就是标记符

RSV 1-3 1bit each 以后备用的 默认都为 0

Opcode 4bit 帧类型,稍后细说

Mask 1bit 掩码,是否加密数据,默认必须置为1

Payload 7bit 数据的长度 (2^7 -1 最大到127)

Masking-key 1 or 4 bit 掩码 //用来编码数据

Payload data (x + y) bytes 数据 //

Extension data x bytes 扩展数据

Application data y bytes 程序数据


更详细的可以看看:WebSocket数据帧规范


接着我们读取消息,会用到其中的一些字段,包括FIN、 MASK、Payload len等等。


然后来看看这个读取当前消息帧的方法:


//开始读取当前消息帧

- (void)_readFrameContinue;

{

    //断言要么都为空,要么都有值

    assert((_currentFrameCount == 0 && _currentFrameOpcode == 0) || (_currentFrameCount > 0 && _currentFrameOpcode > 0));

    //添加一个consumer,数据长度为2字节 frame_header 2个字节

    [self _addConsumerWithDataLength:2 callback:^(SRWebSocket *self, NSData *data) {


        //

        __block frame_header header = {0};


        const uint8_t *headerBuffer = data.bytes;

        assert(data.length >= 2);


        //判断第一帧 FIN

        if (headerBuffer[0] & SRRsvMask) {

            [self _closeWithProtocolError:@"Server used RSV bits"];

            return;

        }

        //得到Qpcode

        uint8_t receivedOpcode = (SROpCodeMask & headerBuffer[0]);


        //判断帧类型,是否是指定的控制帧

        BOOL isControlFrame = (receivedOpcode == SROpCodePing || receivedOpcode == SROpCodePong || receivedOpcode == SROpCodeConnectionClose);


        //如果不是指定帧,而且receivedOpcode不等于0,而且_currentFrameCount消息帧大于0,错误关闭

        if (!isControlFrame && receivedOpcode != 0 && self->_currentFrameCount > 0) {

            [self _closeWithProtocolError:@"all data frames after the initial data frame must have opcode 0"];

            return;

        }

        // 没消息

        if (receivedOpcode == 0 && self->_currentFrameCount == 0) {

            [self _closeWithProtocolError:@"cannot continue a message"];

            return;

        }


        //正常读取

        //得到opcode

        header.opcode = receivedOpcode == 0 ? self->_currentFrameOpcode : receivedOpcode;

        //得到fin

        header.fin = !!(SRFinMask & headerBuffer[0]);


        //得到Mask

        header.masked = !!(SRMaskMask & headerBuffer[1]);

        //得到数据长度

        header.payload_length = SRPayloadLenMask & headerBuffer[1];


        headerBuffer = NULL;


        //如果是带掩码的,则报错,因为客户端是无法得知掩码的值得。

        if (header.masked) {

            [self _closeWithProtocolError:@"Client must receive unmasked data"];

        }


        size_t extra_bytes_needed = header.masked ? sizeof(_currentReadMaskKey) : 0;

        //得到长度

        if (header.payload_length == 126) {

            extra_bytes_needed += sizeof(uint16_t);

        } else if (header.payload_length == 127) {

            extra_bytes_needed += sizeof(uint64_t);

        }


        //如果多余的需要的bytes为0

        if (extra_bytes_needed == 0) {

            //

            [self _handleFrameHeader:header curData:self->_currentFrameData];

        } else {

            //读取payload

            [self _addConsumerWithDataLength:extra_bytes_needed callback:^(SRWebSocket *self, NSData *data) {


                size_t mapped_size = data.length;

                #pragma unused (mapped_size)

                const void *mapped_buffer = data.bytes;

                size_t offset = 0;


                if (header.payload_length == 126) {

                    assert(mapped_size >= sizeof(uint16_t));

                    uint16_t newLen = EndianU16_BtoN(*(uint16_t *)(mapped_buffer));

                    header.payload_length = newLen;

                    offset += sizeof(uint16_t);

                } else if (header.payload_length == 127) {

                    assert(mapped_size >= sizeof(uint64_t));

                    header.payload_length = EndianU64_BtoN(*(uint64_t *)(mapped_buffer));

                    offset += sizeof(uint64_t);

                } else {

                    assert(header.payload_length < 126 && header.payload_length >= 0);

                }


                if (header.masked) {

                    assert(mapped_size >= sizeof(_currentReadMaskOffset) + offset);

                    memcpy(self->_currentReadMaskKey, ((uint8_t *)mapped_buffer) + offset, sizeof(self->_currentReadMaskKey));

                }

                //把已读到的数据,和header传出去

                [self _handleFrameHeader:header curData:self->_currentFrameData];

            } readToCurrentFrame:NO unmaskBytes:NO];

        }

    } readToCurrentFrame:NO unmaskBytes:NO];

}


这个方法是先去读取了当前消息帧的前2个字节,大概就是这么一部分:


 0                   1                   

 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 

 +-+-+-+-+-------+-+-------------+

 |F|R|R|R| opcode|M| Payload len |    

 |I|S|S|S|  (4)  |A|     (7)     |            

 |N|V|V|V|       |S|             |   

 | |1|2|3|       |K|             |                    

 +-+-+-+-+-------+-+-------------+


然后会去对头部信息进行一些判断,但是最主要的还是去获取payload,也就是真实数据的长度,然后还是调用:


[self _addConsumerWithDataLength:extra_bytes_needed callback:^(SRWebSocket *self, NSData *data) {

    ...

}];


去读取真实数据的长度,然后会在下面这个方法中判断当前帧的数据是否读取完成:


- (void)_handleFrameHeader:(frame_header)frame_header curData:(NSData *)curData;

{

    ...

    if(complete)

    {

       [self _handleFrameWithData:_currentFrameData opCode:frame_header.opcode];


    }else{

       [self _readFrameContinue];


    }

    ...

}


如果没读取完成,会继续去读取,否则就调用完成的方法,在完成的方法中会回调暴露给我们的代理:


 [self _performDelegateBlock:^{

    [self.delegate webSocket:self didReceiveMessage:message];

}];


并且继续去读下一帧的数据


[self _readFrameNew];


整个数据读取过程就完成了。


接着我们来看看数据的写:


//写数据

- (void)_writeData:(NSData *)data;

{

    //断言当前queue

    [self assertOnWorkQueue];

    //如果标记为写完成关闭,则直接返回

    if (_closeWhenFinishedWriting) {

            return;

    }

    //输出buffer拼接数据

    [_outputBuffer appendData:data];

    //开始写

    [self _pumpWriting];

}

- (void)_pumpWriting

{

    ...

    //写入进去,就会直接发送给对方了!这一步send

    NSInteger bytesWritten = [_outputStream write:_outputBuffer.bytes + _outputBufferOffset maxLength:dataLength - _outputBufferOffset];


    ...

}


基本上非常简单,区别于之前CocoaAsyncSocket,读和写都没多少代码,原因是因为CocoaAsyncSocket整篇都用的是CFStream等相对上层的API。


SRWebSocket全篇代码注释地址:SRWebSocket注释。

相关文章:

(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;是…

Docker的使用(二:Docker客户端常用指令练习)

1、列出镜像&#xff1b; 2、搜索镜像&#xff1b; 3、拉取镜像&#xff1b; 4、构建镜像&#xff1b; 4.1、在Dockerfile文件所在目录构建镜像&#xff1b; 4.2、在其他目录构建镜像&#xff1b; 4.3、查看镜像是否构建成功&#xff1b; 5、删除镜像&#xff1b; 6、创建并启…

实现简书个人中心UI效果

这两天比较闲&#xff0c;简单实现了一下个人中心页面scrollView嵌套的效果&#xff0c;也就是下边这个页面,大家感受一下先&#xff1a; JSDemo2.gif 首先讲下实现思路&#xff0c;很多人看到这个界面觉得是多个scrollView嵌套实现的&#xff0c;其实苹果不推荐scrollView的嵌…

PHPCMSv9首页显示分页点击下一页跳转链接出现错误,跳转到后台的解决方案

1 引用页写为 {pc:content action"lists" catid"10" order"updatetime DESC" thumb"0" num"1" page"$_GET[page]"}{loop $data $v}....{/loop}{$pages} {/pc}2 phpcms/libs/functions/global.func.php文件 get_…

顺序查找和二分查找

{线性的顺序查找}function seqSearch(sArr: array of Integer;aCount: Integer;const index: Integer):Integer;var i: Integer;begin Result : -1; for i : 0 to aCount do if sArr[i]index then begin Result : i; Break; end;end;{对数性的二分查找}f…

Docker的使用(三:Docker Hub远程镜像管理)

1、登录 Docker Hub&#xff1b; 2、修改镜像名称&#xff1b; 3、登录认证&#xff1b; 4、推送镜像&#xff1b; 5、查看验证&#xff1b;

啊里大鱼短信发送API

https://api.alidayu.com/doc2/apiDetail?spm0.0.0.0.SEe3dm&apiId25450 转载于:https://www.cnblogs.com/shiningrise/p/5626708.html

GCD API

&#xff08;可直接复制到Xcode中查看&#xff09; /***********************************************************************************************************************************##目录##知识点&#xff1a;GCD中有2个核心概念&#xff1a;任务和队列任务&#…

查看Linux系统中某目录的大小

命令&#xff1a;du -sh 目录名来查看&#xff0c;如下 du -sh /root 命令显示结果为&#xff1a;1.2M /root 检查是否有分区使用率use&#xff05;过高&#xff0c;如发现某个分区空间接近用完&#xff0c;可以进入该分区的挂载点&#xff0c;用以下命令找出占用空间最多的文件…