0%

leetcode 总结 03 多数元素

多数元素

给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

输入:[3,2,3]
输出:3
示例 2:

输入:[2,2,1,1,1,2,2]
输出:2

进阶:

尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/top-interview-questions/xm77tm/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 解题思路:找到数量最多的数字 
// 1. 可以用map , 放进去每一个元素,然后更新这个元素的个数,最后查找map 中元素的第二位数值。(肯定可行,但是 时间复杂度 控件复杂度不合适)
// 如果只需要一次遍历 , 就得找一个数值用来记录最大的数量,还得有一个标识,用来记录当前这个最大数量指向的是哪个数值
class Solution {
public:
int majorityElement(vector<int>& nums) {
int resultNum = nums[0]; // 保存当前最大的数值,默认第一位就是最多的 [1,2,3,4,2,2,3]
int count = 0; // 保存最多的数据被记录了多少次
for(int i = 0; i < nums.size(); i++) // 正常遍历操作
{
if(nums[i] == resultNum) // 如果相等,那么 计数器加一
{
count ++;
}else // 如果不相等
{
count --; // 计数器减少 一
if(count == 0) //当计数器归零的时候,更换一下最多的记录
{
resultNum = nums[i]; // 认为当前的是最多的
count ++; // 计数器自增
}
}
}
return resultNum; // 返回当前认为记录最多的
}
};

leetcode 04 删除排序数组中的重复项

给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

示例 1:

给定数组 nums = [1,1,2],

函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。

你不需要考虑数组中超出新长度后面的元素。
示例 2:

给定 nums = [0,0,1,1,1,2,2,3,3,4],

函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。

你不需要考虑数组中超出新长度后面的元素。

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/top-interview-questions-easy/x2gy9m/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//解题思路:删除重复 , 索引 index , 然后判断下一个是不是跟当前的相同,如果相同,就不管,如果不相同 ,索引向后移动一位,当前放在当前的位置 
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int length = nums.size(); // 获得长度
if(length <= 1)
{
return length; // 小于等于 1 ,肯定就返回了
}
int i = 0; // 索引
for(int j = 1 ; j < nums.size();++j) // 遍历
{
if(nums[i] != nums[j]) // 如果不相同
{
i++; // 索引后移动
nums[i] = nums[j]; //当前的放在这个位置
}
}
return i+1; // i 为索引 从 0 开始, 返回个数是 需要加一
}
};

leetcode 05 旋转数组

给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。

进阶:

尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。
你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?

示例 1:

输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]
示例 2:

输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右旋转 1 步: [99,-1,-100,3]
向右旋转 2 步: [3,99,-1,-100]

提示:

1 <= nums.length <= 2 * 104
-231 <= nums[i] <= 231 - 1
0 <= k <= 105

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/top-interview-questions-easy/x2skh7/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//解题思路:在没有看清要求的时候就在考虑,取出最后一节,然后逐个拼接在最前面的, 但是 o(1)的限制貌似不太好
//思路二 , 最后几位逆序,然后逐个插入在数组最前面 , (方法可行,但是每次插入一个数子,整个数组移动,不合适)
// 非常要注意一点 , K 是移动的次数, 不是第几位
// 先分模块儿 , 再分
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int numLen = nums.size(); // 获取nums 的长度
if(k > numLen) // 如果超过数组本身长度
{
k = k % numLen; // 超过本身的就没必要去旋转
}
// [1,2,3,4,5,6,7] 3
// A [1,2,3,4] B [5,6,7]
// 分为 A B 两节 , 第一节 , 从 0 开始 到 numLen - 1 - k 长度结
myRotate(nums, 0, numLen - 1 - k); // 处理以后是 [4,3,2,1]
myRotate(nums, numLen - k, nums.size() - 1); // 第二节 从 上面长度结尾 后面的一位开始 , 到整个数组的结尾 处理以后是 [7,6,5]
myRotate(nums,0,nums.size() - 1); // 原本 [4,3,2,1,7,6,5] 整个翻转之后就变成了 [5,6,7,1,2,3,4] 符合需求
}
void myRotate(vector<int>& nums, int start, int end) // 使用双指针 ,头结点和尾节点,然后两者之间交换
{
while(start < end)
{
int tmp = nums[start];
nums[start] = nums[end];
nums[end] = tmp;
start ++;
end --;
}
}
};

leetcode 06 存在重复元素

给定一个整数数组,判断是否存在重复元素。

如果存在一值在数组中出现至少两次,函数返回 true 。如果数组中每个元素都不相同,则返回 false 。

示例 1:

输入: [1,2,3,1]
输出: true
示例 2:

输入: [1,2,3,4]
输出: false
示例 3:

输入: [1,1,1,3,3,4,3,2,4,2]
输出: true

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/top-interview-questions-easy/x248f5/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//解题思路:一开始在没看清楚题目的时候就直接去写了, 因为看到类似的题目,另外一个是需要得到数组中存在最多的数字,但是忽略了一个点,没法记录多少次
// 回来继续考虑,因为知道map 存在两个数据 ,第一个可以作为key , 第二个用来存放存在的数量,当然对于本题来讲,返回True 就可以了。于是代码如下

class Solution {
public:
bool containsDuplicate(vector<int>& nums) {
if(nums.size() <= 1) // 如果只有一个,或者这个数组就没内容,那么必须没有重复的
{
return false;
}
std::map <int ,int> myMap; // 定义用来存放的map key 作为 数值 value 作为出现的次数
std::map<int,int>::iterator it; // 定义it ,服务于map
for(int i = 0; i < nums.size();i++) // 一次遍历
{
it = myMap.find(nums[i]); // 查询是否存在
if(it != myMap.end()) // 如果已经存在 , 那么肯定这次是重复的
{
return true;
}else{
myMap[nums[i]] = 1; // 这里用来存放次数,这里是错误的,主要有用的是key
}
}
return false;
}
};

leetcode 07 加一

给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。

最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。

你可以假设除了整数 0 之外,这个整数不会以零开头。

示例 1:

输入:digits = [1,2,3]
输出:[1,2,4]
解释:输入数组表示数字 123。
示例 2:

输入:digits = [4,3,2,1]
输出:[4,3,2,2]
解释:输入数组表示数字 4321。
示例 3:

输入:digits = [0]
输出:[1]

提示:

1 <= digits.length <= 100
0 <= digits[i] <= 9

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/top-interview-questions-easy/x2cv1c/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//解题思路:因为是从后面往前开始去处理 , 竟然还是写了 i++  , 真的是头大了。
// 解题思路是 , 从最后一位开始,如果是 9,那么 设置数值为0,再往前遍历,如果数字小于 9 , 那么自增,然后直接返回数组
// 对于最后的,如果变成了 00000 , 那么需要用一个新vector ,放入 当前的内容和最头部的 1
class Solution {
public:
vector<int> plusOne(vector<int>& digits) {
int m_size = digits.size(); // 拿到vector 的length
for(int i = m_size - 1; i >= 0 ; i--) // 倒叙遍历, 逐个获取
{
if(digits[i] != 9) // 如果当前数值不是 9 , 那么 自增,然后返回数组
{
digits[i] += 1;
return digits;
}
digits[i] = 0; // 如果是 9 ,还没返回出去 ,那么置为0
}
vector<int> vec(m_size + 1); // 都变成 0 了之后 ,返回 当前vector
vec[0] = 1; // 首位方置为 1
return vec;
}
};

仓库损坏

如题,因为升级本地node 环境和hexo 版本,导致本地配置无法使用。

最直接的损失是,TensorFlow 系列无法直接显示。最近新增了一些C++ 的东西,先在可用的环境下进行提交,好消息是原始文件都存在。

Nodejs 使用Buffer

nodejs

为什么需要在Nodejs 中使用 buffer ?

如上图所示,Nodejs 最外层接口是JavaScript,内部的具体执行是C++ 。由于JavaScript的灵活易用性和使用广泛性,暴露接口为JavaScript这一点对于快速开发绝对是有利的。虽然纯粹JavaScript 语言本身可以使用buffer , 但是与C++相比,对于二进制数据类型内容处理,JavaScript 表现的并不擅长。因为Nodejs 的底层是依赖于C++ , 所以为了更好的发挥各项语言之间的擅长,提升产品的性能,JavaScript 与C++ 之间的buffer 通信就是一种必然。在Nodejs中存在着重要的一个模块儿,Google V8 。依赖于V8提供的扩展API,JavaScript 可以比较容易的跟C++ 进行通信。

我们的产品在PC端使用的是Electron(基于Nodejs),在关于图形图像和文件处理方面也会有使用场景。

如何使用,会有什么样的问题 ?

根据NodeJs 的架构图我们也能发现一些问题,我们需要处理不同语言之间的信息交互。所以单单从语言层面来讲,通用的做法,两个语言之间进行通信,需要在不同的语言中各自分配一份内存用于存放数据交换的内容。对于buffer 数据而言,在长度不固定前提下,两个语言都要重新分配和释放连续内存再加上内容拷贝就会显得效率不会很高。但是Nodejs 中有V8。

因为Nodejs 有V8 的存在,JavaScript 中间需要分配的交换数据可以经过V8分配和持有的内存用来存储,但是V8 持有的内存又跟平常C++ 分配的内存一样。V8 Data 持有的储单元是可通过 V8 的 C++ API 访问的,但这些又不是普通的 C++ 变量,这些存储单元只能够通过受限的方式访问。

如果把V8存储单元作为“桥”,那么数据拷贝的交互模式就会如下。

JavaScript 向C++ 发送数据的流程就是,JavaScript 准备需要发送的buffer 数据,让V8存储单元进行存储,然后通过Nodejs 提供的交互API,调用C++,先在C++ 层进行数据拷贝。这样就完成了数据的单向发送。同步方式阻塞方式直接返回数据的流程就是,在C++ 中获取到处理后的Output数据,通过V8 持有存储单元,执行Nodejs 提供的API , 可以把数据交给JavaScript。

这样的做法似乎没什么问题,但是如果在处理大量的交互数据时候,从V8侧拷贝交换数据再给到C++ 就会需要一定的性能损失。还存在另外一个问题,就是既然使用Nodejs,依赖于C++,目的就是不能阻塞JavaScript 单线程。异步交互就会显得比较重要,在Nodejs 中,如果需要使用异步操作,那么Libuv是另外一个绕不过去的重点。

具体的执行步骤如下。

copying

如果去掉内容拷贝,C++ 和 JavaScript 能指向并且能够直接使用V8 存储单元的数据,就是比较理想的状况。

inplace

Nodejs 对于这类问题的处理提供了一个比较合适的组件,Buffer。

用法

参考 Nodejs官方文档

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 构造 buffer 对象
const buf = Buffer.alloc(5);
console.log(buf);
// Prints: <Buffer 00 00 00 00 00>

//Buffer.from 用法
// Creates a Buffer containing the bytes [1, 2, 3].
const buf4 = Buffer.from([1, 2, 3]);
// 字符串 后面是编码格式
const buf = Buffer.from('hello world', 'utf8');
console.log(buf.toString('hex'));
// Prints: 68656c6c6f20776f726c64

// 参考上面官网连接可以有更多的特殊用法,常用的就是如上面 如果觉得省事儿可以理解为数组, 但是不同点的是,Buffer 在nodejs 中分配的内存并不是由JavaScript 持有, 也不由底层C++ 直接访问持有 , 而是属于V8 的一部分存储单元

C++ 侧

正如上面官网内容,没有丝毫的跟下层C++ 交互的内容。

为了方便理解和使用,这里提供一个Demo 方便于参考。

1
2
3
4
5
6
7
8
9
10
// 从JavaScript 开始  示例 1  设计一个 从JavaScript 传递buffer 到C++ , C++ 拿到传递数据之后修改部分数据之后直接返回
// 示例 1 主动传递buffer ,并且接收 C++ 直接返回
console.log("---------------->>>>>>>>=========");
// 输入buffer , C++ 内部实现在拿到buffer 数据之后对数据的后部分做 “加一” 操作
// 返回数据应该为 Hello C++ Beepo"""
var bufSource = Buffer.from("Hello C++ Addon!!!");
console.log("bufSource is : " + bufSource);
var buffer = global.logBridge.getNewBuffer(bufSource);
console.log("Direct return buffer is : " + buffer);
console.log("----------------<<<<<<<<<=========");

对于V8操作,这里需要先补充以下知识点

Isolate是一个独立的V8实例,也可以说一个独立虚拟机,其中可以包含一个或多个线程,但同一时间,只有一个线程是执行状态。

Context代表一个执行上下文(执行环境),它使得可以在一个 V8 实例中运行相互隔离且无关的 JavaScript 代码. 你必须为你将要执行的 JavaScript 代码显式的指定一个 context。Context支持嵌套。

Handle是一个指向堆内存的指针,在V8中JavaScript的值和对象也都存放在堆中,Handle提供了一个JS对象在堆内存中的地址的引用。有人会有疑问我们直接操作JS变量指针不可以嘛?由于V8的GC策略,可能会对堆中的JS变量移动其内存位置,Handle的出现可以跟踪相应变量的地址。

Handle Scope是一个Handle的容器,为了解决一个个释放handle过于繁琐,将一些handle接入handle scope中,方便统一管理(释放等)。

isolate

具体接受和处理代码如下

  1. 主动传递和接收
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// C++ 侧
// 按照规则要求需要先提供一个可以被JavaScript 调用的 getNewBuffer 函数
Nan::SetPrototypeMethod(tpl, "getNewBuffer", getNewBuffer); // 暴露JavaScript调用接口 在Init 函数中

void MyBuffer::getNewBuffer(const Nan::FunctionCallbackInfo<v8::Value> &info)
{
Isolate *isolate = info.GetIsolate(); // 获取当前isolate
v8::HandleScope scope(isolate); // 持有当前 isolate
// 获取 getNewBuffer 调用时候 第一个参数 并且转化为 V8local Onject
v8::Local<v8::Object> infoObj = v8::Local<v8::Object>::Cast(info[0]);
unsigned char *buffer = (unsigned char *)node::Buffer::Data(infoObj); // 获取熟知的 char * buffer
size_t size = node::Buffer::Length(infoObj); // 获取传递的buffer 长度
// 对 buffer 长度的后半段进行数据 ‘ + 1’ 操作
for (int i = 0; i < size; i++)
{
if (i > size / 2)
{
buffer[i] += 1;
}
}
// 在使用 Nan::NewBuffer 的时候一定要注意内存管理 , 这里使用了 Nan::CopyBuffer 直接在这里返回处理后的buffer
info.GetReturnValue().Set(Nan::CopyBuffer((char *)buffer, size).ToLocalChecked());
}

单向从JavaScript 到 C++ , 经过V8 Buffer 再回调回JavaScript , 整个一圈就可以走完。如果发起方一直是JavaScript ,那么这个流程无疑是最直观能理解到的,但是还会存在其他的情况。 事件的发起方是C++ , 而且是多线程处理,需要把相关数据交给JavaScript,而且是多次异步回调。

补充提醒,JavaScript 只有单线程,而C++ 可以有多线程。

  1. C++ 主动异步多线程发送,JavaScript 被动接收
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 用于保存和设置 JavaScript 回调函数
void MyBuffer::setCallback(const Nan::FunctionCallbackInfo<v8::Value> &info)
{
v8::Local<v8::Context> context = info.GetIsolate()->GetCurrentContext(); // get context
Isolate *isolate = info.GetIsolate(); // get isolate
Nan::HandleScope mscope; // HandleScope
v8::HandleScope scope(isolate);
Local<Value> arg = info[0]; // get input JavaScript callback
if (!arg->IsFunction()) // 类型判断
{
return;
}
// 持久化存储 JavaScript callback
v8::Local<v8::Function> func = v8::Local<v8::Function>::Cast(info[0]); // to Local function
MyBuffer::getInstance()->callback.Reset(func); //放在 Nan::Persistent 持久化 JavaScript
uv_async_init(uv_default_loop(), &MyBuffer::getInstance()->m_callback_async, async_on_callback); // libuv init
// 这里为了测试先在这里启动timer 多次调用请注意合理关闭timer
MTimer *t = new MTimer(); // 这里timer 中开启了新的线程,所以需要考虑线程安全的内容 于是回调就绕不开 Libuv
t->setInterval([&]() {
MyBuffer::getInstance()->m_counter++;
cout << "time is : " << MyBuffer::getInstance()->m_counter << endl;
// 构造假数据
string dataStr = "TimerCallBack count is " + to_string(MyBuffer::getInstance()->m_counter) + " hello ";
MyCallbackData *m_data = new MyCallbackData();
m_data->data = new char[strlen(dataStr.c_str()) + 1];
strncpy(m_data->data, dataStr.c_str(), strlen(dataStr.c_str()));
m_data->data[strlen(dataStr.c_str())] = '\0';
m_data->size = strlen(dataStr.c_str()); // 目的 , copy 这里构造的假数据,用于回调给JavaScript
// 数据入队 线程安全方式入队
MyBuffer::getInstance()->m_BufferData_l.enqueue(m_data);
// 通知libuv 异步句柄允许用户“唤醒”事件循环并获取从另一个线程调用的回调。
uv_async_send(&MyBuffer::getInstance()->m_callback_async); // 线程安全
},
20); // 20 Ms 回调一次
}

Libuv 回调,真正执行回调JavaScript 部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 真正用于异步执行JavaScript 存放在C++ 中的回调函数。 由libuv 触发
void async_on_callback(uv_async_t *handle)
{
v8::Isolate *isolate = v8::Isolate::GetCurrent(); // isolate
v8::HandleScope scope(isolate); // HandleScope
MyCallbackData *m_bufferData; // 数据出队
while (MyBuffer::getInstance()->m_BufferData_l.try_dequeue(m_bufferData))
{
// 留意这个 copy 字段 , 存在内存重新分配 使用时候要小心 v8 data 不在C++ 也不在JavaScript
v8::Local<v8::Object> buferObj = Nan::CopyBuffer(m_bufferData->data, m_bufferData->size).ToLocalChecked();
//std::string msg = std::string(m_bufferData->data,m_bufferData->size); //用于debug 输出
//std::cout << "---" << msg << std::endl; // 用于debug 输出
delete [](m_bufferData->data); // 记得销毁之前分配的buffer
delete(m_bufferData); // delete 之前new 出来的对象
m_bufferData = nullptr; // 指针置空
//准备返回 JavaScript 数据
const unsigned argc = 1;
v8::Local<v8::Value> argv[argc] = {buferObj};
try
{ // C++ 方式try catch (不是很必要) // 下面这一行是从 Nan::Persistent function 转化为 local function
v8::Local<v8::Function> m_call = v8::Local<v8::Function>::New(isolate, MyBuffer::getInstance()->callback);
Nan::Call(m_call, isolate->GetCurrentContext()->Global(), argc, argv); // 执行回调
}
catch (const std::exception &e)
{
std::cerr << e.what() << '\n';
}
}
}

Libuv 总结

1
2
3
4
// Libuv 需要以下
uv_async_init(); // 初始化异步回调
uv_async_send(); // 子线程发起事件
void async_on_callback(uv_async_t *handle); // 执行异步回调

通过libuv 就可以异步从 C++ 子线程发起回调启动, libuv 通过内部loop 在合适时机交给主线程 ,然后执行具体 回调函数。

附赠代码demo NodejsC++AddonBuffer

Websocket数据发送

Websocket 的数据传输是依靠数据片段组成的,或者叫做数据帧(frame),是数据传输中的最小单元。

在Websocket 协议中,数据帧又分为 控制帧(Control Frame)和数据帧(Data Frame)两种。

因为Websocket 是全双工协议,所以发起方在连接以后可以由Client 或者是Server随时发起。

数据帧

WebsocketDataFrame

Websocket

整理一下Websocket,方便以后问题查找

什么是Websocket ?

Websocket 是在TCP协议上封装的另一种应用层协议,主要用于Client 和Server 之间的全双工通信,WebSocket API也被W3C定为标准。因为本身设计的目的就是为了满足双工通信,所以解决了之前TCP 只能通过客户端请求服务端的限制,得到了比较广泛的使用。

HTTP 也是基于TCP 的一种常见的协议,是一个基于请求与响应模式的、无状态的、应用层的协议,常基于TCP的连接方式。正是因为这些特性,Web开发中的大多数网络请求走的是Http 的协议,走的是短链接形式。

既然Websocket 是基于TCP的,那么可以先了解一下TCP 工作的方式。

TCP 的握手方式

直观的以gif 为例,建立连接需要三次握手

tcp_handshake

第一次握手:客户端发送请求报文将SYN = 1同步序列号和初始化序列号seq = x发送给服务端,发送完之后客户端处于SYN_Send状态。

第二次握手:服务端受到SYN请求报文之后,如果同意连接,会以自己的同步序列号SYN(服务端) = 1、初始化序列号seq = y和确认序列号(期望下次收到的数据包)ack = x+ 1以及确认号ACK = 1报文作为应答,服务器为SYN_Receive状态。

第三次握手: 客户端接收到服务端的SYN + ACK之后,知道可以下次可以发送了下一序列的数据包了,然后发送同步序列号ack = y + 1和数据包的序列号seq = x + 1以及确认号ACK = 1确认包作为应答,客户端转为established状态。

回归正题,继续讲解Websocket

websocket_connect

对于握手,Websocket 需要多一次,先经过三次握手,建立连接,然后再发送Upgrade

websocket_handshake

因为 Websocket 已经被作为了标准,主流的浏览器都已经支持,在使用方面也已经成熟。来看一下Http 和 Websocket 在发起连接的报文区别.

Websocket 请求头

1
2
3
4
5
6
7
GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13

Http 请求头

1
2
3
4
GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi
1
2
3
// 这两个是Websocket 与 Http 不同的部分 用来标识是使用Websocket 协议
Upgrade: websocket
Connection: Upgrade

Sec-WebSocket-Key 是由浏览器随机生成的,提供基本的防护,防止恶意或者无意的连接。

Sec-WebSocket-Version 表示 WebSocket 的版本,最初 WebSocket 协议太多,不同厂商都有自己的协议版本,不过现在已经定下来了。如果服务端不支持该版本,需要返回一个 Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。

对于返回包

Websocket 返回包

1
2
3
4
5
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

Http 返回包

1
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: "34aa387-d-1568eb00"
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain

对于Websocket 返回包

  1. 首先,101 状态码表示服务器已经理解了客户端的请求,并将通过 Upgrade 消息头通知客户端采用不同的协议来完成这个请求;
  2. 然后, Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key
  3. 最后, Sec-WebSocket-Protocol 则是表示最终使用的协议。

Sec-WebSocket-Accept 的计算方法:

  1. Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接;
  2. 通过 SHA1 计算出摘要,并转成 base64 字符串。

注意: Sec-WebSocket-Key/ Sec-WebSocket-Accept 的换算,只能带来基本的保障,但连接是否安全、数据是否安全、客户端 / 服务端是否合法的 ws 客户端、ws 服务端,其实并没有实际性的保证

Websocket 跟HTTP 进行比较

共同点:

  1. Websocket使用和 HTTP 相同的 TCP 端口,可以绕过大多数防火墙的限制。默认情况下,Websocket协议使用80端口;运行在TLS之上时,默认使用443端口。
  2. 两者都基于TCP ,提供可靠连接。

不同点:

  1. 较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
  2. 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
  3. 保持连接状态。于HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
  4. 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
  5. 可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
  6. 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。

Websocket 通过 HTTP/1.1 协议的101状态码进行握手。 为了创建Websocket连接,需要通过客户端发出请求,之后服务器进行回应,这个过程通常称为“握手”。

常见问题如何分析

如果 返回码不是 101 那么出现的错误就是 Invalid HTTP status.

如果是 回包消息中没有 UpgradeConnectionSec-WebSocket-KeySec-WebSocket-Accept 没有这几个货,错误消息是 A required HTTP header is missing

然后Websocket 内部就会自动走关闭connection 逻辑,执行 handle_terminate , 根据之前的错误类型,就会执行回调给到 on_fail 或者是 on_close

同时会携带错误代码 , 比如 异常退出 1006, 正常退出 1000 等

从C++ 使用OpenCV + AI美颜 到 Electron

前提:

​ OpenCV :跨平台计算机视觉和机器学习软件库。

​ Electron :一个跨平台的、基于 Web 前端技术的桌面 GUI 应用程序开发框架,基于 Chromium 和 Node.js。

目的:如标题,需要摄像头数据经过美颜处理,输出frame buffer 给到 Electron JavaScript 进行渲染。

思路:调用C++ OpenCV 获取原始图像信息,美颜SDK处理,通过NodeJs Addon 把C++ 获取到的 FrameBuffer 转交给Javascript然后调用H5 渲染。

拆解:C++ 使用 OpenCV 采集数据部分,美颜SDK 处理采集后的数据部分,NodeJs C++交互部分。

思路和逻辑都不会很麻烦,所以理论上实现起来问题应该不会很大。事实上,并不是。前期C++业务部分,虽然不太顺利,但相对还好。后期的Electron 部分就比较坑。如果不知道 Electron vue C++ 联调的方式,就会显得比较抓狂。一般情况下,异常部分可以依靠log,在electron vue 中, 仅有的C++的log 没办法打印,无法判定真正出错的地方就会大大增加修改的难度。好不容易找到了NodeJs 跟C++ Debug的方式,NodeJs调试好了之后,Electron 中又出现新的异常。

鉴于开始写记录过程的时候基本处于完工,比较折腾的路线就不再介绍。

做法

需要使用OpenCV , 也需要跨平台,C++ 就不得不是一个选项。

理论来讲使用OpenCV 不是一件特别麻烦的事情,理论和现实可能存在差异。

我们需要在win32 下面来做,因为我们的Electron 项目需要 32 位环境,那么构建 32 位编译调试环境就是第一步。

编译和使用依赖库

库编译
  1. GitHub 中 OpenCV 提供的编译完成的OpenCV 4.5 版本是64 位,无法直接使用,首要解决的问题是编译32 位的 OpenCV 库。CMake 、Visual Studio 相关配置和设置必不可少 。找到合适的编译教程,剩下的只是时间问题,推荐链接 OpenCV Windows编译

OpenCVCmake
OpenCVVisualStudio
OpenCVBuildLibs
opencvBuildOutput
opencvWindowsConfig

  1. 根推荐链接,不出意外,运气好的话大半天儿时间没有了(CMake 过程中会有相关指定库下载,这里存在下载不成功的可能性)。等32位库编译完成,配置好windows 系统环境变量,最终才可以进行OpenCV 的开发。
使用OpenCV
  1. 鉴于计算机视觉在现在的比较广泛的使用,能够从很多的途径拿到C++ 版本如何使用,确认好Demo 中使用的OpenCV 版本,然后根据例子就可以找到简洁易用的使用方法。于是我们可以通过OpenCV 获取到摄像头中的每一帧数据。
  2. OpenCV 获取到的每一帧画面其实可以理解为一张图片,有着长宽和颜色深度的矩阵数据。美颜部分就是拿到这一堆数据之后通过一定的算法把每一帧内容进行数据处理然后输出。比如可以通过TensorFlow Keras 或者Pytorch 神经网络训练库进行数据训练数据处理,处理训练之后生成可用的模型文件,在使用的时候利用框架,载入训练好的模型文件,去处理相应的图形数据。如果不依赖神经网络,特定算法也可以进行图片特征处理。这里我们使用了别人封装好的SDK,在给到输入的数据buffer 之后处理为可使用的美颜格式buffer。神经网络的部分这次先Pass ,我们先按照SDK的方式继续。
  3. 获取到文件buffer,交给JavaScript,依赖于NodeJs Addon 。如果按照Addon 的写法,应该问题也不是特别大。然而,NodeJs Addon 部分却是最耗时和麻烦的地方。
C++ JavaScript 相互通信

思路:Javascript 侧设置回调函数给到C++ , C++持有 Javascript 中的回调函数对象,通过异步回调方式给到Javascript。

前提: Javascript 只有单线程, C++ 可以有多线程,Javascript 设置回调函数调用C++,C++内部执行逻辑不能卡C++ 的主线程。Javascript 跟C++ 交互在于C++ 的主线程,不定时回调内容需要在C++ 中的工作线程(子线程),C++ 内部需要子线程跟主线程通信。Nodejs 能够高效的原因是因为NodeJS的底层是C++,JavaScript 是单线程,想要在Nodejs 中具有多线程就得依赖libuv。C++ 需要在NodeJs 上面的执行最终需要依赖 node-gyp 进行编译。

  1. node-gyp 的使用node-gyp (如果很熟悉这一步,请Pass,如果不是,请一行一行的看)首要解决掉编译和链接才能有下一步的调用,如果没有经过特别精细的设置,唯一的后果就是无法正常生成node文件,也就没办法进行C++ 编译和链接。node-gyp 的配置部分比较麻烦,需要仔细去确认。建议参考别人能够work 的项目工程进行理解,合理设置搜索路径和库链接(系统环境配置和软件运行环境配置最没技术含量,却有出奇的耗时)。
  2. 当我们终于配置完成库编译的时候,如何跟Electron 进行断点调试就是一个问题了。项目中我们这边使用的是Electron Vue ,有着各种的限制,不太容易去直接坐到做到。 但是好消息是,网上提供了可以用于调试C++ 的测试工程。借助这个测试工程,依赖 Nodejs 中的插件 CodeLLDB ,经过详细的插件配置,大半天儿之后NodeJs 和 C++ 可以断点联调了。请参考 如何使用vscode 调试 nodejs + C++
  3. 等以上全部做完,才开始正确的代码开发部分。借助于 Node Nan ,在JavaScript中设置回调函数给到C++, C++ 中接收JavaScript 传参,使用Nan::Persistentv8::Function 可以对JavaScript 中的函数进行持久化保存(避免function 被GC掉)。需求是设置一次JavaScript的回调函数,然后C++ 内部异步多次返回需要的数据。对于Nan 封装的高阶API AsyncWorker 不能使用,原因是在执行异步之后,JavaScript 的 Callback 会被干掉。因为想要得到OpenCV 的逐帧返回内容, C++ 这里获取每一帧内容需要存在于单独线程,那么libuv 就不得不使用了。
1
2
3
4
5
6
 // 用于主线程监听,在收到libuv 回调之后在合适时机执行
static void async_on_message(uv_async_t *handle);
// 用 Nan::Persistent 来持久化 JavaScript 提供的CallBack
Nan::Persistent<v8::Function> m_callBack;
// Async handle 用于触发,唤起 回调函数, 可以从其他线程触发
uv_async_t m_async;
libuv 使用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// libuv 回调
void BeautyFaceInterface::async_on_message(uv_async_t *handle)
{
Nan::HandleScope m_scope;
// isolate 需要临时获取,一定不要存储
v8::Isolate *isolate = v8::Isolate::GetCurrent();
v8::HandleScope scope(isolate);
//get finalResult
...
...

v8::Local<v8::Integer> v8_width = v8::Integer::New(isolate, (int)finalResult.cols);
v8::Local<v8::Integer> v8_height = v8::Integer::New(isolate, (int)finalResult.rows);
// CopyBuffer 注意内存, 深度拷贝, 因为nan 使用了 c++ 跟 JavaScript Buffer 共享, 需要在 JavaScript 使用完成Buffer 之后置空操作,让内存释放
v8::Local<v8::Object> buffer = Nan::CopyBuffer((char *)finalResult.data, len).ToLocalChecked();
v8::Local<v8::Value> argv[] = {v8_width, v8_height, buffer};
const unsigned argc = 3;

v8::Local<v8::Function> m_call = Nan::New(BeautyFaceInterface::getInstance()->m_callBack);
TryCatch try_catch;
m_call->Call(isolate->GetCurrentContext()->Global(), argc, argv);
if (try_catch.HasCaught())
{
FatalException(try_catch);
}
}

// libuv Callback 初始化和 回调函数设置
void BeautyFaceInterface::start(const Nan::FunctionCallbackInfo<v8::Value> &args)
{
Isolate *isolate = args.GetIsolate();
v8::HandleScope scope(isolate);
Local<Value> arg = args[0];
if (!arg->IsFunction())
{
return;
}
//构造 持久化 JavaScript function
v8::Local<v8::Function> func = v8::Local<v8::Function>::Cast(args[0]);
BeautyFaceInterface::getInstance()->m_callBack.Reset(func);
// libuv uv_async_init 设置回调函数
uv_async_init(uv_default_loop(), &BeautyFaceInterface::getInstance()->m_async, BeautyFaceInterface::async_on_message);
}

/*
下面是在任意的函数内部执行,目的是为了启动和唤醒 async_on_message
*/

// 设置timer
Timer *t = new Timer();
t->setInterval([&]() {
//新的子线程中 fire m_async uv_async_send 线程安全 这里可能会出现多次fire ,但是 回调地方只会有一次的现象,这里不能使用 m_async 来进行传值,需要在这里交给主线程数据的话需要使用线程安全的队列来执行。
uv_async_send(&BeautyFaceInterface::getInstance()->m_async);
},
30); // 30 Ms


以上是正常的使用方法,在之前的时候出现过一些奇异的异常,比如在NodeJs 中可以完美运行,但是 载入Electron Vue 之后就在等待一段时间之后出现异常的现象。

libuv 异常部分
  1. 如下图,不能正常fire Async handle

ElectronIsolate

  1. 在回调函数中也遇到过一下的崩溃,这个是需要运行一段实现以后才会出现。
1
2
0x0eccee98 {isolate_=0xdddddddd {...} context_={...} m_callBack=0xdddddddd {handle_={...} } ...}
Nan::ObjectWrap:{refs_=-572662307 handle_={...} }

很明显能看出来 isolate_ 无法访问了 , m_callBack 也无法访问。 猜测大概率原因是经过了一些时间的运行,JavaScript 的虚拟环境发生了改变,或者 回调函数被GC 掉了。

最后查明原因是 isolate 不能进行存储(网上的Demo 有一些很不负责),JavaScript 提供的CallBack 需要使用 Nan::Persistent 来进行持久化(避免GC)。

所以会好奇如何拿到这些数据的对吧,配置调试环境放在第一位!!! 配置调试环境放在第一位!!! 配置调试环境放在第一位!!!

会很耗时间,但是总好过完全不知道错误原因

结尾

OK 到此为止,从C++ 获取OpenCV 数据 到 Electron 中经过 Canvas 渲染,最终能把画面呈现到 Electron 上面。

Demo 地址: 先等等,需要配置出包环境。