0%

GITEE: https://gitee.com/isletspace/esp32iot

本项目是使用 微信小程序+腾讯云+乐鑫开发板+5V-多路-继电器+220V-AC-电器 的模式对家庭中的硬件进行检测和控制,本项目涉及到 网络前端服务器后端网络通信物联网硬件控制 方面的相关知识。

需要实现的功能

  1. 开发板能够自动连接家庭WiFi
  2. 开发板能够通过MQTT协议与后台服务器取得联系
  3. 开发板能够定时自动获取时钟并校准
  4. 开发板能够根据MQTT协议控制外围电路
  5. 开发板能够根据已经设置的参数与实时的传感器参数比对,并在一定环境条件内开启/关闭相关电路
  6. 手机可以通过微信小程序发送MQTT数据至服务器

软件开发平台:Mac

硬件开发IDE:Arduino

硬件准备

  1. 4路5V红板(光耦/高低电平)继电器 *1
  2. DHT11温度模块 *2
  3. 加热器 *1
  4. ESP32-WROOM-32开发板 *1

其他:

  1. 杜邦线
  2. 电烙铁
  3. 锡丝
  4. 松香
  5. 面包板

芯片模组比对选型

ESP32-WROOM-32X(此处 X 代表不确定) 和 ESP32-WROVER-X(此处 X 代表不确定) 在性能上基本一模一样,包括核心数、最大时钟频率、Flash大小、支持的接口类型 和 特殊传感器等等。除了在 PSRAM、工作温度范围、天线、WiFi最高频率 和 模组尺寸上有所区别外。 ESP32-WROVER-X 则具有PSRAM 和 最大的面积(18mm × 31.4mm × 3.3mm)。

官方展示的ESP32模组性能参数对比

芯片选型

无脑选了ESP32-WROOM-32SE,反正自己用,而且便宜(¥21.5包邮)。

芯片基本信息

Arduino所展示的开发板可调参数

Arduino IDE准备

使用 Arduino IDE 进行开发,但开发配置相关的 .json 文件 及其安装包都在 github.com 上,即使使用了科学上网进行加速,但因为加速器的加速层级可能和IDE拉取资源包所用的数据层级不在同一个上面,无法代理,就白天基本上无法下载,凌晨(正经程序员谁tm熬夜)才有机会下载东西。

英文版下的 首选项Preferences

修改附加开发板管理网址

然后去 开发板管理器 中搜索 esp32 进行安装。

目录-开发板-开发板管理器
搜索关键词“esp32”

解决github问题

方法一

本来应该用如下链接进行配置的:

1
https://dl.espressif.com/dl/package_esp32_index.json 

自己扒拉了一遍该文件中可能包含的所有资源包(主要是 github.com 上的),然后修改了 .json 文件,放到自己的桶里,然后放出来使用,还好这个文件里有明确提到要哪些资源不然也没法扒拉。

1
https://code.islet.space/espressif/dl/package_esp32_index.json

注意:Mac系统下可能会因为曾经下载过的缓存文件无法生效(如果自行修改了网站上的 .json 文件的话),需要自行清除缓存,打开 ~/Library/ArduinoXX/cache/ 并对应删除该网站的文件夹即可,如 code.islet.space 。(这里的 ArduinoXX 中的 XX 需要自己去看自己的文件夹,应该是受版本控制的)

修改链接后可以正常下载

方法二

自己去github,用浏览器或者下载器下好了以后放到 用户目录--资源库--程序缓存--指定位置(如下)。

1
~/Library/ArduinoXX/staging/packages/

这个文件夹也就需要放这么多文件吧。

资源库缓存指定位置下的文件

亲测,第二种方法更好用。

提示安装成功

Visual Studio Code IDE准备

总的来说,VS Code的IDE配置会比Arduino更为复杂,但是无奈在都是科学上网的情况下,VS Code下载毫无压力,且VS Code使用习惯之后更加方便。

至少前期配置工作来说,VS Code更为友好。

Espressif IDF插件安装

首先去插件市场搜索 Espressif IDF 插件进行安装,然后点击 查看 -> 命令面板

image-20211228182209737

到这里需要输入 configure esp-idf extension 以进入该插件的配置页面。

image-20211228182314161

必要工具安装

然后它就会提示你需要安装几个必要插件,有 Git / Python / CMake 和 Ninja。这些都可以从Espressif的 官方文档 中获取到安装步骤。

image-20211228182508965

下方为四个工具的官方下载链接,前三个都可以通过官方下载软件包进行安装,第四个 Ninja 我安装不成功,是通过 Mac Ports 进行安装的。

GIT : https://git-scm.com/

PYTHON : https://www.python.org/downloads/

CMAKE : https://cmake.org/download/

NINJA : https://github.com/ninja-build/ninja/releases

需要注意的是,CMAKE 和 Ninja 安装完还需要加入到系统环境变量 PATH 中去,Windows系统下的系统环境变量添加比较方便,下方为 Mac 系统下 CMake 的系统环境变量添加方法。

CMAKE命令行

打开Mac系统下的 CMake GUI,点击 Tools --> How to Install For Command Line Use ,可以查看到三种将 CMake 添加到命令行中去运行的方法。

点击“工具”菜单中的“如何为命令行使用进行安装”
可以查看到三种安装方法

使用的是第二种方法,向命令行输入 sudo "/Applications/CMake.app/Contents/bin/cmake-gui" --install 并输入管理员密码即可。

然后通过 cmake --version 可以查看到版本号说明安装成功,但需要注意的是 whereis cmake 还是没有用。

操作结果

工具安装验证

四个必须要安装的软件都已经安装完毕,通过 --version 可以查看版本号。

安装验证

配置继续

四个必要工具安装完毕之后,关闭 ESP-IDF Setup 页面,然后重新在 命令面板 输入 configure esp-idf extension 以进入该插件的配置页面。

再次进入配置页面

此处选择 EXPRESS 即可,里面会要求选择从哪个服务器下载(自己看情况选择Github / Espressif)、IDF版本(默认选最新)、IDF放置路径、工具路径、使用的Python3路径 等几个信息,选完了就进入下一界面,会自动下载安装 ESP-IDF。

详细配置页面

耐心等待下载完毕即可,我用 Espressif 服务器下载不下来,然后该用 Github 和 科学上网进行下载的。

image-20211228185748221

下载过程页面如下:

image-20211228190058612
image-20211228190027057

最后提示的安装成功:

image-20211228190438498

示例项目载入

命令面板 中输入 show examples projects

image-20211228190651117

即可调出 ESP-IDF的示例代码项目列表。

image-20211228190738993

选择 blink 作为测试代码。

image-20211228190839371

项目构建

这里可以用命令面板输入 esp build your project ,也可以点击GUI下方的 image-20211231124940242进行构建。

命令面板构建

构建完成后,会生成 build 文件夹,下次构建时CMake会查看变动的文件然后只编译变动过的代码。

也可以删除该 build 文件夹,使其全部重新构建,会比较费时。

build过程

点击下方的闪电按钮烧录按钮即可烧录,烧录成功提示如下:

烧录过程

连接WiFi

下图为使用示例代码更改的项目代码文件内容:

项目文件内容示例

利用ESP自带的示例代码station 进行调试和更改,需要注意以下几点:

  1. 不用去更改 isletSpaceIotMain.c 中的 SSIDPASS ,只需要在 sdkconfig 文件中进行更改即可(如下图)。

  2. 所有的 WiFi设置参数 都可以在 sdkconfig 文件中进行修改,如需要连接 AP 的 名称和密码,还有自身设备所显示的名称。

  3. 修改文件名之后,需要对两个 CMakeList.txt文件 和 一个Makefile 文件中的项目和文件名进行修改(如下图)。

如果修改代码名称需要修改main文件夹下的CMakeList文件
如果修改项目名称需要修改根目录下的CMakeList
如果修改项目名称需要修改根目录下的Makefile

MENUCONFIG 或者说 sdkconfig ,一个是图形界面的,一个是纯文本形式的。

用menuconfig的话只需要找到下方的齿轮按钮即可。

image-20211231130402110

可以找到WiFi相关的配置位置:

image-20211231130507316

sdkconfig 文件中的设备名称进行修改。

image-20211228223040901

修改完毕后可以在路由器 AP 的后台查看到已经连接的设备的名称,如下。

image-20211228223134009

MQTT协议

根据 Espressif 官网的 ESP-MQTT 开发文档可知,MQTT 协议支持以下几种形式:

  • MQTT over TCP

  • MQTT over SSL with mbedtls

  • MQTT over Websocket

  • MQTT over Websocket Secure.

各MQTT协议形式(与对应SDK里的 MQTT示例代码)及其所使用的端口号不同:

  • protocols/mqtt/tcp: MQTT over tcp, default port 1883
  • protocols/mqtt/ssl: MQTT over tcp, default port 8883
  • protocols/mqtt/ssl_psk: MQTT over tcp using pre-shared keys for authentication, default port 8883
  • protocols/mqtt/ws: MQTT over Websocket, default port 80
  • protocols/mqtt/wss: MQTT over Websocket Secure, default port 443

MQTT事件结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* MQTT event configuration structure
*/
typedef struct {
esp_mqtt_event_id_t event_id; /*!< MQTT event type */
esp_mqtt_client_handle_t client; /*!< MQTT client handle for this event */
void *user_context; /*!< User context passed from MQTT client config */
char *data; /*!< Data associated with this event */
int data_len; /*!< Length of the data for this event */
int total_data_len; /*!< Total length of the data (longer data are supplied with multiple events) */
int current_data_offset; /*!< Actual offset for the data associated with this event */
char *topic; /*!< Topic associated with this event */
int topic_len; /*!< Length of the topic for this event associated with this event */
int msg_id; /*!< MQTT messaged id of message */
int session_present; /*!< MQTT session_present flag for connection event */
esp_mqtt_error_codes_t *error_handle; /*!< esp-mqtt error handle including esp-tls errors as well as internal mqtt errors */
bool retain; /*!< Retained flag of the message associated with this event */
} esp_mqtt_event_t;

typedef esp_mqtt_event_t *esp_mqtt_event_handle_t;

MQTT相关示例代码

了解完官方开发文档中的信息,就可以知道MQTT示例代码里的这些文件夹命名含义,以及自己需要打开哪个示例代码了。

image-20211228223617579

MENUCONFIG 页面可以看到MQTT的相关配置信息。

image-20211229011902429

MQTT服务器开发

自行购买了腾讯云的服务器,然后完成了配置(步骤略),发现自测“主题订阅”不成功。

image-20211229033019004

然后刚好发现服务器的 Ubuntu 20 系统使用的防火墙是 iptables ,顺利打开 1883 端口。

1
sudo iptables -A INPUT -p tcp --dport 1883 -j ACCEPT

然后查看打开情况。

image-20211229032825212

顺便在服务器页面也打开相关配置。

image-20211229032753559

最后自测成功:

image-20211229032920902
image-20211229032953396

小程序开发(未完)

因为个人使用腾讯云注册了自己的域名 islet.space,且小程序也属于腾讯自家产品,为了后续开发的兼容性和便利性,就同步参阅了腾讯云的 物联网自主品牌小程序开发文档

image-20211227202550338

微信开发者工具

微信开发者工具下载页面:https://cloud.tencent.com/document/product/1081/47685

image-20211227202759080
image-20211227203058077

后台设置

物联网开发平台

DEBUG

烧录和调试过程报错

出现这种情况,可能是所选芯片不正确导致。

烧录错误提示

解决过程

重新选择了芯片,然后将烧录方法改为 UART ,重新烧录即可成功。

在插件中选择”扩展设置“
选择需要调试的设备类型
选择烧录方式
烧录成功提示

头文件查找错误

这个bug始终没有解决,之后再说吧。。。

提示无该文件

WIFI初始化出错

将WiFi-station的代码搬到 MQTT-TCP中,发现 wifiStationInit() 函数初始化错误,报错的行数是 117

wifi初始化函数出错

经检查,发现该函数示例代码的一部分,并未改动,且未添加其他自定义的 .h.c 文件进行编译,应该不存在人为修改错误。

检查函数定义

因为仅修改了本 isletSpaceIotMain.c 文件,就在本文件中搜索 esp_event_loop_create_default 关键字,发现在 app_main() 中也有该语句,初步判断可能是两次调用导致错误,就注释了 298 行 和 299 行进行重新编译和烧录,问题解决,设备可以正常连接到路由器。

注释重复语句

MQTT服务器连接错误

在设备成功接入WiFi后,发现MQTT客户端无法连接至服务器。给出的错误提示是 There are no transports valid ,大致意思即连接不可用。

Monitor提示无可用传输

经过检查,发现原来填写在 sdkconfig 文件中的 Broker URL 链接为 https://iot.islet.space ,而实际上,官网给出的配置方式如下:

查看官方URI定义

打开项目配置菜单,修改为 mqtt://iot.islet.space ,并重新 Build 和 Flash。

修改URL

可以发现错误已解决,设备已经正常连接至服务器。

Monitor提示成功

尝试在对应主题下发布信息进行测试:

尝试发布信息

通过终端Monitor可以观察到,设备正常接收到数据:

设备接收成功

eventHandler未进入

image-20211229182911935

参考

  1. Mac 安装 CMake 配置及环境配置
  2. Quick User Guide for the ESP-IDF VS Code Extension
  3. ESP-IDF 编程指南
  4. ESP-MQTT

请先确保电脑上已经安装了必须的软件,如 node.jsgit ,因为日常需要使用 代码数学公式流程图搜索 等工具,所以下方的插件安装也主要是与此相关的。

注意:以下仅适用于 hexo-next 主题。

插件安装

先放一个博客 islet.space 需要使用到的插件,都需要使用 npm 进行安装。

本博客使用到的一些插件

将下方这段脚本保存至 hexo_install.sh 文件中,并使用 chmod +x hexo_install.sh 命令为其添加执行权限,并使用 ./hexo_install.sh 进行执行即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# uninstall the unnecessary renderer,like marked and kramed
sudo npm uninstall hexo-renderer-marked --save
sudo npm uninstall hexo-renderer-kramed --save


# install the necessary plugins for hexo
sudo npm install @fancyapps/fancybox --save
sudo npm install @fortawesome/fontawesome-free --save
sudo npm install @next-theme/pjax --save
sudo npm install @next-theme/plugins --save
sudo npm install hexo-deployer-git --save
sudo npm install hexo-filter-mermaid-diagrams --save
sudo npm install hexo-generator-searchdb --save
sudo npm install hexo-math --save
sudo npm install hexo-renderer-ejs --save
sudo npm install hexo-renderer-pandoc --save
sudo npm install hexo-renderer-stylus --save

插件启用

pandoc

若要启用pandoc进行渲染,除了之前必须要卸载旧的渲染器(如 markedkramed 外),还需要去github页面下载一个pandoc的渲染器软件并进行安装。

pandoc下载链接:https://github.com/jgm/pandoc/releases

mermaid

mermaid需要显示的话也需要安装插件,需要在hexo-next主题文件夹下的 _config.yml 文件中进行启用,如下:

mermaid在主题配置文件中的启用

latex

latex数学公式可以由 pandoc 渲染器进行渲染,只需要在hexo-next主题文件夹下的 _config.yml 文件中启用 mathjax ,如下:

数学公式在主题配置文件中的启用

搜索功能功能不仅需要安装插件,还需要分别在博客文件夹根目录中的 _config.yml 和 hexo-next主题文件夹下的 _config.yml 文件中进行启用,如下:

博客根目录配置文件下的启用

注意:此处的 db.json 需要在 public 文件夹中也有一份的时候启用;如果 db.json 无法使用,则更换为 search.xml 。因为 db.jsonsearch.xml 是由插件生成的搜索数据库(可以这么看)。

hexo主题配置文件下的启用

因为也是人力资源管理专业出身,所以基于面试者(员工)角度来写这篇文章的时候,其实是左右手互博的感觉。

特殊性说明,本文所描述的岗位背景其实是 嵌入式DSP裸机软件(驱动)开发

注意:下面标灰色的部分都是跟HR工作相关的概念,如果看不太懂可以跳过。

面试流程

技术类岗位招聘通常会遇到 2~3轮面试 和一次 HR提供Offer 的环节,最经典的流程如下:

graph LR
简历投送 --> ID1((HR面)) --> ID2((项目面)) --> ID3((技术面)) --> 拿Offer

面试原则

面试者需要在面试过程中适当包装和美化一下自己的工作经历,但是还是需要秉承着相对诚实的原则。

为什么说是相对诚实的原则

你可以说“自己的工作很难但按时完成”,“自己独立完成”,“自主编写代码” 等等他人较难去鉴别的工作,这些都是一个人工作能力不错的表现。但假如把自己没做过的项目放进去,一旦被问技术面的时候问到细节就会露馅。即使面试时没有露馅,工作时露馅了,大概率也会被面试官知道的。

一个HR有可能是在一家公司专门从事招聘的专员,也有可能是为多家公司做招聘的猎头(专门从事招聘工作的人员,通常是成立一个猎头公司,专门帮其他公司找人才)。一旦技术面的时候夸大自己的工作经验,技术面也会跟HR反馈你的夸大情况,然后你就再也不能通过该猎头或者专员找到其他的工作了,相当于自己白白损失了一个资源口。

而,嵌入式开发之类的技术岗位(目前)是绝对的版本答案之一,企业通过猎头找人才已经是常态,因此绝对不能有这种作死行为。

HR面

由专门/兼职做人力资源的工作人员负责,主要有以下流程:

  1. 说明招聘信息(包括所招聘的岗位的基本要求和职责、企业背景、团队情况和项目情况)
  2. 在职情况(千万不能说自己已经离职,否则你就会很掉价,因为HR会觉得你急于找到新工作而压低你的价格——对,就是你的薪酬,你的卖身一个月的价格,当然也有其他方法可以来试着压低你的薪酬,后面再谈)
  3. 询问面试者的离职原因(以判断该员工是否足够稳定,因为薪酬给的更高的员工其离职后给企业造成的离职成本会更高,因此需要该员工足够稳定,后面详谈如何回答离职原因)
  4. 询问面试者当前的工作内容(以确定该员工与所招聘岗位的匹配度,就是说,面试者目前的工作内容如果和他招聘的岗位的工作内容差异太大,他就不会想要再继续去问了,这点应该比较容易懂吧。当然,面试者也不能明明不匹配就瞎说自己会该工作技能,不然就是坑自己也坑别人,这里尽量坦白一些比较好)
  5. 询问面试者的求职意向及工作意愿(就是会问面试者是否愿意从事本岗,一般到这里如果没有回答“愿意”,都没得聊了)
  6. 询问面试者期待的薪资及目前薪资(这一点在招聘网站看可以看到该企业所招聘的岗位能提供的薪资范围,通常也就是个薪酬区间,后面详谈如何回答这部分内容。询问目前薪资就是来确定你的人力成本,进而探寻能够压缩你人力成本空间的可能性的。)
  7. 约定下一步面试时间(这一点应该没什么好说,通常都是电话技术面或视频技术面,确认好双方有空闲的时间)
  8. 其他(会问是不是符合一些硬性条件,比如统招本科(就是学信网能够查到本科学历信息),毕业时间等等)

如何回答离职原因

这一部分看似回答很简单,其实会藏着很多坑。

首先,要明白为什么需要HR面,为什么面试第一个打来的都是HR,而不是技术员工直接打过来。

从企业运行的角度考虑,每一时刻的企业运作都是需要运行成本的,包括场地成本、设备成本(采购、折旧、损耗等等)、物料成本、人力成本(招聘成本、培训成本、薪酬成本等)等等,就特别多,专人专事这个大家都能理解。

如果一个员工匆忙从一家企业离职,该员工入职前的招聘成本、入职时的培训成本和所需要支付的薪酬成本就会拜拜支出,不能给企业带来盈利,但企业在招聘一个员工前后所支出的成本又是实实在在支出了的,就会造成巨大损失(绿厂就不是,就完全不担心这一点,因为给的太少了(通俗点就是成本不高),打着500强名号,韭菜一茬一茬就随便割,又不会特地挽留员工离职,而且全公司现状都是培训工作做得差,成本就不高)。

(个人觉得)企业当然不能实现所有人终身雇佣,没有新鲜血液是不行的,一定比例的人员流动可以给企业带来活力(这个原因我就不必讲了吧)。HR是学过这些东西的,需要站在企业角度去帮企业考虑招聘什么样子的人会让企业更加稳定,人员稳定(流失减少,并控制在一定范围内)就能帮助企业降低运行成本。

因此,面试者需要体现出自己的稳定性,证明自己是一个不太会轻易离职的人,并说明是一些客观的因素(并不是自己的原因)造成了面试者的离职。当然,面试者自己在一家企业就业时,也不应该轻易离职,如果是高校毕业者,最好在第一家企业从事1年及以上再离职(建议!)。以下列举了几个较为客观(不能说绝对客观)的原因:

  • 办公室氛围太过压抑,缺少团队活动
  • 团队协作性太差
  • 领导风格太过压迫和强势

一些员工的主观原因(或者说是令HR反感的原因),这些话是绝对不能说的,绝对禁区!

  • 自己不喜欢目前的工作
  • 工作太难了

一些员工可以说的话:

  • 自己不太挑工作,比较看重团队氛围和工作环境(一般有自信点的HR都会觉得这点没问题,“不太挑工作”的前提是,也得是自己觉得对口的才行,别不对口你不挑个啥。比如,做MCU/ARM/SOC/DSP的相通性较高,说自己不挑可以,但是跨行业太大也敢说自己不挑那就是找事儿)(另,不太挑工作其实对HR来说是个好品质,但自己也得好好掂量掂量自己)

如何回答目前薪资及期望薪资

HR之所以需要问你的目前薪资,不仅是了解本行业或者某个岗位在某个地区的大致薪酬情况(以对比公司招聘员工的薪酬竞争优势),也是在探寻面试者的薪酬上涨或可以压缩的空间。

面试者首先需要了解(或者说准备好)几点信息:

  1. 该行业该岗位在某个地区的 普遍薪资范围 是如何(当然不能拿最高薪资来当做普遍薪资,得看自己的能力和对应岗位的工作难度)
  2. 所面试的公司提供的薪酬范围
  3. 自己所期待的薪资底线
  4. 自己目前薪资

首先是,如果HR问目前薪资。面试者可以适当夸大自己目前的薪资,税后薪资可能都很低,可以说税前,多说几千没关系,不要与对方公司提供的薪酬底线相差太大即可。如果HR说自己公司有30%或者40%的涨薪幅度限制,那么可以放心,该HR觉得你岗位匹配度较高,只是想通过这个来压一压你的成本而已。

此时,你可以说自己期待在某个地区某个岗位的薪酬底线,低于这个就不行。HR态度一般都会更为委婉些。

如果你通过面试,到了拿offer的环节,HR还是会再跟你谈一遍。豪气的公司(一种薪酬策略,用高于行业平均水平的薪资招聘员工,但也要求更高)一般开价都会比你要求的更高,但这是后面的事情了。

HR面结束之后

(如果你对HR提供的岗位较为满意,接收了技术面/项目面的预约)需要记住一些最基础、关键、有用的信息。这些HR面时都已经说明了,最好是拿个小本本记好,这是对面试企业的一个尊重,也是为了防止后面出现的一些莫名其妙的(跟你面试岗位无关的)问题把你问倒。

  1. 岗位工作地点(很多大企业全国都有研究中心和技术中心,可以选择的,看对方给你提供的是哪个城市的就业岗位)
  2. 对方面试官的名字(至少记住姓啥)
  3. 对方的公司名称及背景(比如是帮别人做外包的还是做自研的,有没有上市或者有没有股权激励,成立多久了之类的)
  4. 岗位基本信息 (任职要求和工作职责等)

项目面和技术面

普遍情况下,二轮/三轮面试都会问你一些技术和项目上的东西,根据所闻内容的不同,将其分别称呼为 技术面项目面

有些公司会将这两轮分开,面试的先后顺序不一,总得占比是项目经历会问得更多一些,技术面的占比较少。

有些公司也会将项目面和技术面合在一起(就是同一次面试里就有技术面和项目面),夹杂交替着问。

项目面

主要是问你从事(主要负责或者协助负责)过的一些项目/经历,了解你对研发的产品、芯片、硬件、软件、工具、设备的认知。项目面会有三种类型的提问:

  1. 工具认知提问。了解面试者对工具使用的经验。
  2. 问题解决提问。了解面试者对一些解决问题的方法和经历。
  3. 情景假设提问。考验面试者解决问题的思路。

项目面的面试形式:一般都是电话提问和视频提问。

面试时长:单轮面试时间都是在15分钟以上,最长30分钟左右。

工具认知提问

就是会问你开发过程中遇到的一些工具模块是如何使用的,一些算法或者逻辑的原理。这部分比较简单,都是经验而已。

例如,面试官问我ADC的使用,我就如实回答我是如何用ADC的,以及我对ADC模块的认知。

问题解决提问

有很大概率(不一定是所有公司都会问的),会问你一些“发现问题解决问题”的经验。这里面试官主要是想知道以下几点情况:

  1. 面试者对工作问题的解决思路。是否足够清晰,有什么角度可以去考虑,需要面试者自己去总结,然后可以清晰地向他人阐述。
  2. 面试者在解决问题时的耐心。面试者需要介绍一个以上的案例,向面试官说明问题现象,自己的思考方式、检查方法、解决方法、过程 和 结果 等等。
  3. 面试者的经验。你能说出自己在遇到一些问题(比如调试问题)时的解决思路,或者看待问题(较为全面)的角度时,就说明了你已经具有足够经验了。

如果面试官问你“发现问题解决问题”的经验,你回答自己不知道,不太记得,忘记了这些话,就妥妥失败了。

因为这些工作问题的发现、解决和记录工作,在日常工作过程中其实都是可以稍微花点时间去完成的(这并不难,只是你有没有耐心而已),如果面试者自己没有做这部分 经验积累工作,那就很吃亏,大部分人都有相关的工作经验,自己不会去 总结 而已。

情景假设提问

面试官可能会抛出情景题,例如问你给你一个芯片,2周之内完成开发,你需要怎么准备,有什么计划。其实就是一个经验问题,或者思路问题。回答方法我就不详细说了。

技术面

技术面,就是会问一些项目之下的一些底层技术问题,一般来说都不会太难,也偶尔会有一些面试官问的问题比较偏门(开发过程中较难出现的问题)。例如,我经历过的偏门提问,会问C语言的野指针出现和排查,C++的继承概念等等(这些是我项目上比较难涉及到的)。

但大部分情况下,这些技术提问,都是提问一些你做过的东西,比如你做过某种通信协议的开发,请你表述以下该协议的相关内容(例如,时序、通信格式(通信起始和结束的变化)、电气特性(电平、电压范围)等)。这些其实也是经验的一部分,真的做过的话就应该自己做做笔记,一定会有点印象的,不应该去抱佛脚。

面试原则:问到你会的,你就自信回答。问到偏门的,开发过程中较少出现/遇到和使用的,你就如实回答就好了,不要不懂装懂。

技术面的面试形式:可能是让你线上答卷,现场答卷,也可以是电话提问、视频提问。

面试时长:同项目面,也可能夹杂着在项目面中间进行提问。

拿Offer

拿到Offer后,你可能需要向HR详细确认的信息:

  1. 每天上班时间,从几点到几点
  2. 每个月的薪资计算时间是从几号到几号
  3. 每个月的薪资发放日期、时间
  4. 是否双休、年假情况
  5. 每天的加班强度、每个月的加班强度、加班薪资等
  6. 每年发放多少个月的薪资(就是年终奖发放情况,好一点的是15薪以上,最基本的13薪)
  7. 每个(税前)月薪资是多少(可能需要关心一下社保和公积金的缴纳情况等)

基本信息了解完毕之后,需要自己有意识地去提问的一些问题:

  1. 近期可以签订合同的最晚时间(合同期限一般都是3年,这个就比较不用担心)
  2. 期望入职时间(自己期望什么时候入职,对方期望最晚什么时候入职,可否协调程度)
  3. 涨薪计划(入职后会有哪些涨薪激励计划,或之前其他股权发放啊、年终奖增长等等)
  4. 绩效计划(可能这时候不太愿意说,但是入职后可以去了解)

双十一豪豪酱买了群晖DS220j家用NAS服务器,为了方便工作文件的备份传输,让电脑(PC、Mac)能用上远程网络驱动,就需要让服务器穿透内网,并申请一个独立域名,想着就用腾讯云和家里的小米路由器AX3600来干吧。下方是对硬件上需要的一些要求:

光猫:使用 桥接模式,具有 独立公网IP

路由器:使用 PPPoE模式 进行拨号、支持 端口映射端口转发DMZ

群晖NAS:支持 DDNSUPnP 协议、支持 证书使用范围管理

前期准备工作

更改工作模式

光猫路由器 都支持 路由模式桥接模式,都支持多个无线接入和有线接入,但因为自己购买的路由器具有更多不一样的功能,例如支持UPnP等,而且光猫是连接物理光纤进行上网的,而自己购买的路由器不能直接连接光纤上网,还得依赖于光猫,所以可以认为是两种不一样的工作硬件。

如果光猫已经工作在桥接模式(通过查看路由器后台的 上网方式 是否为PPPoE,如果是则说明自己购买的路由器工作在路由模式,电信光猫工作在桥接模式)。一般没有此类服务器需求的家庭网络,都是让光猫工作在路由拨号模式(即路由模式)的,而路由器则工作在桥接模式,此情况则需要将两个硬件设备的工作模式进行互换,参考以下步骤:

  1. 打电话给电信营业厅 (0000)10000 (前方的 (0000) 是自己的市区号),确认一下光猫是路由模式还是桥接模式,如果是路由模式,则将其改成桥接模式。
  2. 确认改成桥接模式后,询问宽带的账户和密码,在路由器后台输入账户密码并连接测试,确保路由器工作在拨号模式,而光猫工作在桥接模式。

目前国内家庭宽带都是默认内网IP,想要公网IP就只能向运营商申请了,这并不需要额外的收费。电信手上的IP资源最多,也最大方。我也是照着别人的说法,打电话给电信客服,说装监控需要公网IP,客服登记之后,过几个小时就打电话说已经换好了,干净利落,服务好。目前好像还没见到过电信要不到公网IP的情况。

——《BT下载教程 篇一:BT下载大提速! 获取公网IP和端口映射转发简单教程

image-20211113000614809

多重NAT改成公网IP

同样是打给电信,然后申请公网IP,电信客服专员一般会在一个小时内处理完毕,然后就可以得到公网IP了。

“谁拨号,谁就有公网IP” ——即此时如果路由器已经成功设置了路由模式(使用PPPoE拨号上网),则可以在路由器后台查看到其公网IP地址,如:

image-20211113001927548

DDNS域名绑定

虽然已经获得公网IP,但是该IP还是会随机变动,还会需要使用DDNS服务将公网IP映射到对应域名上,而群辉服务器则提供此功能,此处需要自己申请一个域名(此步骤省略)。进入群辉服务器后台,开启DDNS功能,将域名和随机公网IP绑定和更新。

另外,需要自己上所用的服务器上开启API Token,并提供给群辉DDNS服务(类似于授权,可以由DDNS服务自己检测家庭网络的公网IP,并自动绑定最新的公网IP)。

API Token申请

下方以腾讯云的DNSPOD为例,点击DNSPOD中的 API密钥,进行 新建密钥 即可,将对应的 SecretIdSecretKey 复制下来。

image-20211113002208036
image-20211113001142315

DDNS添加

将获取到的 SecretId 填写到 用户名/电子邮件 中, 将 SecretKey 填写到 密码/密钥 中。

image-20211113001623814

经过前面的步骤,可以看到群晖NAS服务器自动测试的 DDNS状态正常 即可。

image-20211112175853513

端口映射和转发

端口映射:Port Mapping, 即将内网中的主机的一个端口映射到外网主机的一个端口,以提供相应的服务。当用户访问外网IP的这个端口时,服务器自动将请求映射到对应局域网内部的机器上。

端口转发:Port Forwarding, 即将外网对应 EA 端口上收到的数据转发到内网某个主机对应的某个 IA 端口上,或者反过来将内网某个主机 IB 端口的数据转发到对应外网的 EB端口 上。

UPnP协议:Universal Plug and Play,即 即插即用。主要是微软在推行的一个标准,适用于家庭网络,用于设备间的发现和连接。UPnP 最大的愿景就是希望实现任何设备只要一接入网络就能被网络中的所有其它设备发现,做到完全的即插即用。UPnP是一个多层协议构成的框架体系,每一层都以相邻的下层为基础,同时又是相邻上层的基础。直至达到应用层为止。

手动端口转发或自动端口转发

手动端口转发:

自动端口转发:

image-20211112180357037
image-20211112175632036
image-20211112180450097

配置DMZ

通过配置DMZ,我们可以将需要保护的Web应用程序服务器和数据库系统放在内网中,把没有包含敏感数据、担当代理数据访问职责的主机放置于DMZ中,这样就为应用系统安全提供了保障。

DMZ使包含重要数据的内部系统免于直接暴露给外部网络而受到攻击,攻击者即使初步入侵成功,还要面临DMZ设置的新的障碍。

image-20211112172411026

DMZ开启前

image-20211112172343953

DMZ开启后

虽然也是随机公网IP,但是可以使用DMZ将连接到路由器的设备映射到外网上。

image-20211112175925384

安全证书设置

设置安全证书及其使用范围,不要使用自我签名的证书,所有浏览器都会识别和阻止你访问的

经过前面DDNS的 API token 配置之后,群晖NAS能够获取到对应域名的免费证书,并提供页面进行管理,将所有使用到的应用安全验证证书都设置为对应云服务提供商提供的证书即可。

image-20211112180133874

后台服务器访问

经过上面复杂的折腾之后,就可以使用自定义的域名进行后台登录和管理了。

输入 https://nax.xxx.xxx:5000 即可进行后台服务器的访问,也可以使用 ping 进行测试,如下:

image-20211113002928045

远程驱动器设置

  1. 在文件管理器中右键点击 此电脑 的属性
  2. 然后点击 映射网络驱动器
image-20211112174837538
  1. 选择本地的 虚拟驱动号 进行设置
  2. 输入 网络地址端口号,格式如 https://nas.xxx.xxx:5006
image-20211112175022321
  1. 按照提示输入群晖提供的账户名和密码即可进行连接
image-20211112174513370
  1. 连接完毕即可查看到对应的网络驱动器
image-20211112174559989

参考

  1. 【干货】什么是端口转发?什么是端口映射?如何设置端口映射?
  2. DMZ
  3. upnp协议简介(一)

本笔记不是全面的C++学习笔记,而是基于个人在原有的C语言基础和浅薄的C++的开发经验上的补足笔记。

本笔记仅供个人学习记录,不提供完整学习指导,如有需要请自行阅读《C++ Primer Plus》。

本文会参考 GNU C++ 进行代码调试,部分文档内容会参考 MS C++ 进行理解。

简介

C++融合了三种不同的编程方式:

  • 面向过程编程(继承于C)
  • 面向对象编程
  • 泛型编程(Generic Programming),即C++模板(Template)

过程 VS 对象

面向过程编程(Process Oriented Programming),即 结构化编程过程结构化编程,是指根据执行的操作来构思一个程序。程序任务需要解决的问题按照 “1、2、3、4” 这样的顺序一一编写,如果程序任务过大,则将较大的任务拆解成较小的可以理清结构逻辑的小任务去完成。C语言的编程开发思路就是使用程序模块(函数)来表示各个任务模块。

面向对象编程(Object Oriented Programming),简称 OOP。

结构化编程在程序逻辑的清晰度和可靠性上占据优势,但是在代码量上和面向对象相比不占优势

为了应付代码量的挑战,与强调算法的过程性编程不同,OOP强调数据。过程性编程试图使问题满足语言要求,OOP则是使语言满足问题的要求。

(Class) 是规定了 数据操作数据类型(Data Type)。类所创建的实体称为对象。

OOP 强调的是编程的数据方面,GP编程强调的是 独立于特定数据类型,即强调 通用非特定 类型。

image-20211027135117490

系统的位数

  • 系统的位数是由什么决定的?包括嵌入式系统的8位、16位、32位和通用系统的32位和64位?

位数 是由CPU的最大寻址空间决定的。

New Features in C++

以下主要从C++和C的区别点进行记录,是个人之前从未掌握过的知识,另外也会记录C++的一些高级用法。

Statement

  • C与C++的语句申明位置有哪些差异?

C语言的变量声明通常都应位于函数或过程开始位置(Pascal也是),而C++并没有该限制,因此这点可以说是C++的优点也可以是缺点。

Range-based for

C++11 引入了一种崭新的 for() 循环形式,可以逐一迭代某个给定的 区间数组集合(range, array or collection) 内的每一个元素。而其他语言可能称之为 foreach 循环,其一般性用法如下:

1
2
3
for(decl:coll){
statement
}

其中:

  • decl 是给定之 coll 集合中的每个元素的声明。
  • statement 会针对给定之 decl 进行执行

使用样例(下左)及其输出结果(下右)如:

image-20211030084723644

而且上面这种是基于 C-style array 的新旧语法搭配的 C++,实际上如果 coll 集合提供成员函数 begin()end() ,那么使用 for(decl:coll) 时便等同于使用 for(auto _pos=coll.begin(), _end=coll.end(); _pos!=_end;++_pos)

Data Type

  • 数据类型的大小由什么决定?数据类型大小(即占空内存空间)与系统位数、编译器的关联性?
  • 嵌入式系统与通用系统的数据类型大小差异在哪?
  • double 和 float 在内存上的差异?

在 C++ 中, 变量(Variable) 一词通常用于 引用 标量数据类型 的 实例,而 其他类型(class / struct)的实例通常称为 对象(Object)。

C++ 是 强类型语言 ,也是 静态类型

在代码中声明变量时,必须 显式指定其类型,或使用 关键字指示编译器从初始值表达式 auto 推断类型。 在代码中声明函数时,必须指定每个参数的类型及其返回值;如果函数未返回任何 void 值,则必须指定 。当使用允许任意类型参数的函数模板时例外。

基本(内置)类型

基本数据类型,简称 基本类型基类型

下图显示了 Microsoft C++ 实现中的内置类型的相对大小:

多个内置类型的相对大小(以字节为单位)的关系图。

下表列出了最常用的基本类型及其在 Microsoft C++ 实现中的大小:

类型 大小 评论
int 4 个字节 整数值的默认选择。
double 8 个字节 浮点值的默认选择。
bool 1 个字节 表示可为 true 或 false 的值。
char 1 个字节 用于早期 C 样式字符串或 std:: 字符串对象中无需转换为 UNICODE 的 ASCII 字符。
wchar_t 2 个字节 表示可能以 UNICODE 格式进行编码的“宽”字符值(Windows 上为 UTF-16,其他操作系统上可能不同)。 这是用于 std::wstring 类型字符串的字符类型。
unsigned char 1 个字节 C++ 没有内置字节类型。 使用 unsigned char 表示字节值。
unsigned int 4 个字节 位标志的默认选项。
long long 8 个字节 表示非常大的整数值。

类型数据的宽度(Width),即占用内存大小是由 计算机字长编译程序 决定的。计算机字长提供了硬件计算精度的支持,编译程序则提供了源程序到机器码的转换。

下方为C/C++ 在通用系统或嵌入式系统中的常见数据类型的大小,所有值的单位均为 bit

BASIC DATA TYPE 8 bit MCU 16bit MCU (51) 32bit (x86) 32bit (TI F28004x)
CLA ON
32bit (TI F28004x)
CLA OFF
64bit (x86)
in C++ (g++)
64bit (x86)
in C (gcc)
char 8 8 8 8 8 8
short 16 16 16 8 16 16
int 16 32 32 16 32 32
long 32 32 32 16 64 64
long long / 64 64 32 64 64
float 32 32 32 32 32 32
double 32 64 16 64 64
long double / 32 128 128

下方内容转自网络,待验证。

(x86平台下)long intlong,给人的感觉好像是长整型,但实际上,它和 int 一样,只有32位。cppreference 给出的定义如下,但在实际的使用中,longint 几乎没有区别。

int - basic integer type. The keyword int may be omitted if any of the modifiers listed below are used. If no length modifiers are present, it’s guaranteed to have a width of at least 16 bits. However, on 32/64 bit systems it is almost exclusively guaranteed to have width of at least 32 bits. long - target type will have width of at least 32 bits.

实际上经过测试:在 x86_64 平台下,C 和 C++ 中的 long 长度都是要比 int 更长的,即 int 默认为 32bit 宽度,long64bit 宽度。

image-20211028101248341

Embedded System

嵌入式C语言中常用的数据类型如下图所示,而不同的

image-20211027155153218

double / float

IEEE二进制浮点数算术标准IEEE 754)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用。这个标准定义了表示浮点数的格式(包括负零-0)与反常值(denormal number)),一些特殊数值(无穷(Inf)与非数值(NaN)),以及这些数值的“浮点数运算符”;它也指明了四种数值舍入规则和五种例外状况(包括例外发生的时机与处理方式)。

IEEE 754规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80比特实做)。只有32位模式有强制要求,其他都是选择性的。大部分编程语言都有提供IEEE浮点数格式与算术,但有些将其列为非必需的。例如,IEEE 754问世之前就有的C语言,现在有包括IEEE算术,但不算作强制要求(C语言的float通常是指IEEE单精确度,而double是指双精确度)。

该标准的全称为IEEE二进制浮点数算术标准(ANSI/IEEE Std 754-1985),又称IEC 60559:1989,微处理器系统的二进制浮点数算术(本来的编号是IEC 559:1989)。后来还有“与基数无关的浮点数”的“IEEE 854-1987标准”,有规定基数为2跟10的状况。现在最新标准是“IEEE 854-2008标准”。

Variable

C++ 与 ANSI C(C99标准)不同之处,在于后者只保证名称中的前63个字符有意义,即使第64个字符不同,但是只要前63个字符相同的变量则被认为是相同的。

在某些情况下,其他程序员会使用的变量命名风格及其意义:

  • strsz 前缀:表示以空字符结束的字符串
  • b 前缀:表示布尔值
  • p 前缀:表示指针
  • c 前缀:表示单个字符

Class

:(用户定义/标准的)数据类型规范,详细描述了如何表示信息以及对数据执行的操作。

对象:是根据类规范创建的实体(好似之于变量和基本数据类型)。

就像函数可以来自函数库一样,类也可以来自 类库。从技术上说,大部分类库都没有被内置到C++语言中,而是语言标准指定的类。类定义 位于类库文件中,并没有被内置到编译器里(有需要可以修改,虽然不建议)。

C++之所以如此有吸引力,很大程度上是由于存在大量支持UNIX、Macintosh 和 Windows 编程的类库。

类描述 指定了可以对类对象执行的所有操作。要对特定对象执行这些允许的操作有两种方法,一是直接使用类方法(本质即函数调用),二是重定义运算符(如 cincout 使用的 >><<

Function

被调用的函数称为 被调用函数(called function),包含函数调用的函数称为 调用函数(calling function)。

函数原型:即 函数声明,函数原型或声明只描述函数接口,指仅有函数头和分号 ; 而没有函数体的语句。函数原型定义了需要传递给函数的参数以及函数本身需要返回的值的类型。

库文件:存放了函数编译代码的文件。

头文件:包含了函数原型的文件。

image-20211028085847535

在有些语言中,有返回值的函数被称为 函数(function),无返回值的函数被称为 过程(procedure) 或 子程序(subroutine)。

但在C/C++中,都被称为 函数

Main Function

  • 有什么有些IDE中的main函数的括号里是带参数的?

是否在 主函数main() 参数括号 () 中使用关键字 void ,在C++和C中有明显的区别:

  • 在C++中,让括号空着和在括号中写 void 等效。
  • 而在C中,让括号空着意味着对是否接受参数保持沉默。

int main(int argc, const char * argv[]) 是UNIX和linux中的标准写法。int main() 只是默许的用法。

使用main函数时经常都是不带参数的,因此main 后的括号都是空括号。实际上,main函数可以带参数,这个参数可以认为是 main函数的 形式参数

C语言规定

  1. main函数的参数只能有两个,习惯上这两个参数写为 argcargv。因此,main函数的函数头可写为:main (argc,argv)
  2. argc (第一个形参)必须是 整型变量argv (第二个形参)必须是指向字符串的 指针数组

由于main函数不能被其它函数调用,因此不可能在程序内部取得实际值。那么,在何处把实参值赋予main函数的形参呢?

实际上,main函数的参数值是从 操作系统命令行/Terminal 上获得的。当我们要运行一个可执行文件时,输入空格,输入文件名,再输入实际参数即可把这些实参传送到main的形参中去。

argc 的数值会随着通过命令行传递给可执行文件的参数的增加而增加,即 argc 其实是代表着 参数的数量

image-20211027144512641

编译指令

using 是编译指令的关键字,

参考

  1. C++ Primer Plus
  2. int main(int argc, const char * argv[])
  3. C++ 类型系统
  4. IEEE二进制浮点数算术标准(IEEE 754)

WSL

在windows的 linux子系统下安装的具有图形界面的软件都可以在windows上打开,但是如果直接在MS商店安装的 kali linux 子系统是最精简版本,很多开发者需要用的软件(如 gcc / make / vim 等)都不存在,需要使用官方提供的完整安装命令去安装一些必须程序。

按照官方指示安装完必要程序后,可以在文件管理器里看到 linux 子系统的根目录,一定程度上很方便windows的用户使用linux系统并用图形化界面管理其文件。

image-20211027103100831

百度云盘安装

百度云盘是个人经常使用到的工具,在linux系统上也会安装,但wsl系统的话,多少可能会因为某些依赖文件缺失导致安装失败。

缺失的文件可以通过先更新源库(sudo apt update)再进行安装的方式解决,但有些时候可能是源库的问题,导致该依赖的包不存在(如下)。

image-20211027104129827

因此需要通过手动下载(利用 wget )该依赖包,然后自行安装(利用 sudo dpkg -i )的方式来解决,其中百度云盘缺失的依赖文件及其包连接如下:

  • libappindicator3-1:http://mirrors.ustc.edu.cn/debian/pool/main/liba/libappindicator/libappindicator3-1_0.4.92-3.1_amd64.deb
  • libindicator3-7:http://mirrors.ustc.edu.cn/debian/pool/main/libi/libindicator/libindicator3-7_0.5.0-2_amd64.deb
  • libdbusmenu-glib4:http://ftp.br.debian.org/debian/pool/main/libd/libdbusmenu/libdbusmenu-glib4_18.10.20180917~bzr490+repack1-1_amd64.deb
  • libdbusmenu-gtk3-4:http://ftp.br.debian.org/debian/pool/main/libd/libdbusmenu/libdbusmenu-gtk3-4_18.10.20180917~bzr490+repack1-1_amd64.deb

安装完毕之后即可正常进行软件使用了,虽然 wsl 的界面还是差强人意了些,哈哈。

image-20211027104032244

Traceroute(路由追踪)的原理及实现

traceroute 和 tracert 是Linux和Windows平台下用于追踪网络设备距离远近的工具,向目标设备发包,从 TTL=1 开始向外发包,逐渐增加 TTL 的值,直到目标主机。在介绍traceroute和tracert的原理之前,需要了解几个技术名词:

IP,协议是TCP/IP协议族中最核心的部分,它的作用是在两台主机之间传输数据,所有上层协议的数据(HTTP、TCP、UDP等)都会被封装在一个个的IP数据包中被发送到网络上。

ICMP,即 Internet Control Message Protocol,互联网控制报文协议,它常用于传递错误信息,ICMP协议是IP层的一部分,它的报文也是通过IP数据包来传输的。

TTL,即 time-to-live,是IP数据包中的一个字段,它指定了数据包最多能经过几次路由器。从我们源主机发出去的数据包在到达目的主机的路上要经过许多个路由器的转发,在发送数据包的时候源主机会设置一个TTL的值,每经过一个路由器TTL就会被减去一,当TTL为0的时候该数据包会被直接丢弃(不再继续转发),并发送一个超时ICMP报文给源主机。

实现方案

tracert (windows)只支持基于 ICMP 报文发送,而 traceroute(Linux/Mac) 支持多种报文协议(UDP、ICMP、TCP)的发送,但不带任何选项(Options)时默认使用的是UDP。(具体参数和选项自行请自行 man 以查看手册)

下方所示图片分别为 tracert (windows)的帮助手册,以及在traceroute(Linux)上分别使用三种协议追踪 baidu.com 的结果截图。后续仅对UDP及ICMP追踪做较为详细的说明。

tracert的帮助说明

使用 TCP 报文的 traceroute:

基于TCP报文的traceroute

使用 ICMP 报文的 traceroute:

基于ICMP报文的traceroute

使用 UDP 报文的 traceroute:

基于UDP报文的traceroute

基于UDP实现

在基于UDP的实现中,客户端发送的数据包是通过UDP协议来传输的,使用了一个大于 30000 的端口号,服务器在收到这个数据包的时候会返回一个端口不可达的ICMP错误信息,客户端通过判断收到的错误信息是TTL超时还是端口不可达来判断数据包是否到达目标主机,具体的流程如图:

image-20211022085752243

实现流程

  1. 客户端发送一个TTL为 1 ,端口号大于 30000 的UDP数据包,到达第一站路由器之后TTL被减去 1 ,返回了一个超时的ICMP数据包,客户端得到第一跳路由器的地址。
  2. 客户端发送一个TTL为 2 的数据包,在第二跳的路由器节点处超时,得到第二跳路由器的地址。
  3. 客户端发送一个TTL为 3 的数据包,数据包成功到达目标主机,返回一个端口不可达错误,traceroute结束。

Linux和macOS系统自带了一个traceroute指令,可以结合Wireshark抓包来看看它的实现原理。首先对百度的域名进行traceroute:traceroute www.baidu.com,每一跳默认发送三个数据包,我们会看到下面这样的输出:

image-20211022085720428

对该域名的IP:115.239.210.27进行traceroute,此时Wireshark抓包的结果如下:

image-20211022085739900

抓包结果

注意看红框处的内容,跟第一张图对比,可以看到traceroute程序首先通过UDP协议向目标地址115.239.210.27发送了一个TTL为1的数据包,然后在第一个路由器中TTL超时,返回一个错误类型为Time-to-live exceeded的ICMP数据包,此时我们通过该数据包的源地址可知第一站路由器的地址为10.242.0.1。之后只需要不停增加TTL的值就能得到每一跳的地址了。

然而一直跑下去会发现,traceroute并不能到达目的地,当TTL增加到一定大小之后就一直拿不到返回的数据包了:

image-20211022085834275

结果全是丢失,其实这个时候数据包已经到达目标服务器了,但是因为安全问题大部分的应用服务器都不提供UDP服务(或者被防火墙挡掉),所以我们拿不到服务器的任何返回,程序就理所当然的认为还没有结束,一直尝试增加数据包的TTL。

目前在网上找到许多开源iOS traceroute实现大多都是基于UDP的方案,实际用起来并不能达到想要的效果,所以我们需要采用另一种方案来实现。

基于ICMP实现

上述方案失败的原因是由于服务器对于UDP数据包的处理,所以在这一种实现中我们不使用UDP协议,而是直接发送一个ICMP回显请求(echo request)数据包,服务器在收到回显请求的时候会向客户端发送一个ICMP回显应答(echo reply)数据包,在这之后的流程还是跟第一种方案一样。这样就避免了我们的traceroute数据包被服务器的防火墙策略墙掉。

采用这种方案的实现流程如下:

image-20211022085752243

实现流程

  1. 客户端发送一个TTL为1的ICMP请求回显数据包,在第一跳的时候超时并返回一个ICMP超时数据包,得到第一跳的地址。
  2. 客户端发送一个TTL为2的ICMP请求回显数据包,得到第二跳的地址。
  3. 客户端发送一个TTL为3的ICMP请求回显数据包,到达目标主机,目标主机返回一个ICMP回显应答,traceroute结束。

可以看出与第一种实现相比,区别主要在发送的数据包类型以及对于结束的判断上,大体的流程还是一致的。

HPP头文件

.hpp,其实质就是将 .cpp 的实现代码混入 .h 头文件当中,定义与实现都包含在同一文件,则该类的使用只需要调用 #include<xxx.hpp> 以引用该文件即可,无需再将 .cpp 加入到project中进行编译。

而实现代码将直接编译到调用者的 .obj 文件中,不再生成单独的 .obj,采用 .hpp 将大幅度减少调用 project中的 .cpp 文件数与编译次数,也不用再发布烦人的 .lib.dll 文件,因此非常适合用来编写公用的开源库。

使用注意

.hpp 头文件的优点不少,但是编写中有以下几点要注意:

  1. .h 类似,但 .hppHeader Plus Plus 的简写,是 C++程序头文件 。

  2. VCL专用的头文件,已预编译。

  3. 是一般 模板类 的头文件。

  4. 一般来说,.h 里面只有声明,没有实现,而 .hpp 里声明实现都有,后者可以减少 .cpp 的数量。

  5. .h 里面可以有 using namespace std;,而 .hpp 里则无。

  6. 不可包含 全局对象全局函数 。由于 .hpp 本质上是作为 .h 被调用者所include,所以当 .hpp 文件中存在全局对象或者全局函数,而该 .hpp 被多个调用者include时,将在链接时导致符号重定义错误要避免这种情况,需要去除全局对象,将全局函数封装为类的静态方法

  7. 类之间不可循环调用

在.h和.cpp的场景中,当两个类或者多个类之间有循环调用关系时,只要预先在头文件做被调用类的声明即可,如下:

1
2
3
4
5
6
7
8
9
10
11
class B;

class A{
public:
void someMethod(B b);
};

class B{
public :
void someMethod(A a);
};

.hpp 场景中,由于定义与实现都已经存在于一个文件,调用者必需明确知道被调用者的所有定义,而不能等到 .cpp 中去编译。因此hpp中必须整理类之间调用关系,不可产生循环调用。同理,对于当两个类A和B分别定义在各自的 .hpp 文件中,形如以下的循环调用也将导致编译错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
//a.hpp
#include "b.hpp"
class A{
public :
void someMethod(B b);
};

//b.hpp
#include "a.hpp"
class B{
public :
void someMethod(A a);
};
  1. 不可使用静态成员

静态成员的使用限制在于如果类含有静态成员,则在 .hpp 中必需加入静态成员初始化代码,当该 .hpp 被多个文档include时,将产生符号重定义错误。唯一的例外是 const static 整型成员,因为在vs2003中,该类型允许在定义时初始化,如:

1
2
3
4
class A{
public:
const static int intValue = 123;
};

由于静态成员的使用是很常见的场景,无法强制清除,因此可以考虑以下几种方式(以下示例均为同一类中方法)

  1. 类中仅有一个静态成员时,且仅有一个调用者时,可以通过 局域静态变量模拟
1
2
3
4
5
//方法模拟获取静态成员
someType getMember(){
static someType value(xxx);//作用域内静态变量
return value;
}
  1. 类中有多个方法需要调用静态成员,而且可能存在多个静态成员时,可以将每个静态成员封装一个模拟方法,供其他方法调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
someType getMemberA() {
static someType value(xxx);//作用域内静态变量
return value;
}

someType getMemberB(){
static someType value(xxx);//作用域内静态变量
return value;
}

void accessMemberA(){
someType member = getMemberA();//获取静态成员
};

//获取两个静态成员
void accessStaticMember(){
someType a = getMemberA();//获取静态成员
someType b = getMemberB();
};

  1. 第二种方法对于大部分情况是通用的,但是当所需的静态成员过多时,编写封装方法的工作量将非常巨大,在此种情况下,建议使用 Singleton模式,将被调用类定义成普通类,然后使用Singleton将其变为全局唯一的对象进行调用。

如原 .h.cpp 中的定义如下:

1
2
3
4
5
6
7
class A{
public :
type getMember(){
return member;
}
static type member;//静态成员
}

采用singleton方式,实现代码可能如下(singleton实现请自行查阅相关文档)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//实际实现类
class Aprovider{
public :
type getMember(){
return member;
}
type member;//变为普通成员
}

//提供给调用者的接口类
class A{
public :
type getMember(){
return Singleton<AProvider >::getInstance()->getMember();
}
}

分离式编译

在介绍分离式编译之前需要先介绍一下 分离式代码,在C++代码中,声明和定义是可以分开写在多个文件中,当然也可以写在同一个文件里面的,如 .hpp

往往是为了逻辑条理的清晰而分开书写,但使用 g++ 和 Terminal 直接编译 main.cpp 时,都是使用的 g++ main.cpp -o main 的命令,也就是只能编译该 main.cpp 一个文件,以生成 main 为名的二进制可运行文件,实际上,书写在其他 .cpp 文件中的代码也需要一同编译,这些书写在其他 .cpp 文件中的代码被称为 分离式代码,只编译 main.cpp 就会出现 undefined reference 之类的错误。

命令行G++

在 Windows / Linux / Mac 的 Terminal 下都可以使用 g++ 命令进行编译,格式如下:

1
g++ -c main.cpp xxx1.cpp xxx2.cpp

使用 -c 选项,将包含 main()main.cpp 与 其他分离式代码文件 xxx1.cppxxx2.cpp 一同编译,然后生成一个个对象文件( .o.obj )。上面这种方法会生成三个文件 main.oxxx1.oxxx2.o ,每个 .o 都是一个对象文件,但不一定可执行(因为缺少 main() 函数),仍需要通过进一步 链接 成可执行文件:

1
g++ -o main main.o xxx1.o xxx2.o

通过上面这一行代码可以生成名为 main 的可执行二进制文件。

以上两句命令也可以通过下面这句命令替代:

1
g++ -o main main.cpp xxx1.cpp xxx2.cpp

Makefile

Makefile 文件描述了 Linux 系统下 C/C++ 工程的编译规则,它用来自动化编译 C/C++ 项目。一旦写编写好 Makefile 文件,只需要一个 make 命令,整个工程就开始自动编译,不再需要手动执行 GCC 命令。

一个中大型 C/C++ 工程的源文件有成百上千个,它们按照功能、模块、类型分别放在不同的目录中,Makefile 文件定义了一系列规则,指明了源文件的编译顺序、依赖关系、是否需要重新编译等。

如果是在 Windows 下作开发的话不需要去考虑这个问题,因为 Windows 下的集成开发环境(IDE)。当然,Windows 下的 Visual Studio Code如果没配置好,也只是个编辑器而已,不算是个IDE。一般的MVS(Microsoft Visual Studio)都已经内置了 Makefile,或者说会自动生成 Makefile,不用去手动编写。

Linux 中却不能这样,需要去手动的完成这项工作。Linux 下可以学习的开发语言有很多,常见的有 C/C++语言、python、java 等等。在 Linux(Unix) 下做开发的话,不了解 Makefile 是一件非常失败的事情。不懂 Makefile,就操作不了多文件编程,就完成不了相对于大的工程项目的操作。Makefile 可以说是必须掌握的一项技能。

Makefile 可以简单的认为是一个工程文件的编译规则,描述了整个工程的 编译链接 等规则。其中包含了那些文件需要编译,那些文件不需要编译,那些文件需要 先编译,那些文件需要 后编译,那些文件需要 重建 等等。编译整个工程需要涉及到的,在 Makefile 中都可以进行描述。换句话说,Makefile 可以使得我们的项目工程的编译变得自动化,不需要每次都手动输入一堆源文件和参数。

Makefile 可以彻底简化编译的操作。把要链接的库文件放在 Makefile 中,制定相应的规则和对应的链接顺序。这样只需要执行 make 命令,工程就会自动编译。每次想要编译工程的时候就执行 make 命令,省略掉手动编译中的参数选项和命令,非常的方便。

Makefile 支持多线程并发操作,会极大的缩短编译时间,并且当修改了源文件之后,编译整个工程的时候,make 命令只会编译修改过的文件,没有修改的文件不用重新编译,也极大的解决了耗费时间的问题。

Makefile 格式

image-20211012134610578
  • targets:规则的目标,可以是 Object File(一般称它为中间文件),也可以是可执行文件,还可以是一个标签;
  • prerequisites:是我们的依赖文件,要生成 targets 需要的文件或者是目标。可以是多个,也可以是没有;
  • command:make 需要执行的命令(任意的 shell 命令)。可以有多条命令,每一条命令占一行。

注意:我们的 目标(target) 和 依赖文件(prerequisite) 之间要使用 冒号 : 分隔开,命令的开始(before the command) 一定要使用 Tab 键。

Reference

  1. hpp.h与.h的区别
  2. Makefile教程:Makefile文件编写1天入门 (biancheng.net)

PART 1 THE BASICS

WHY TEMPLATES?

C++ 要求我们使用指定的类型来声明变量、函数 和 大部分其他实体。然而,很多代码对于不同的类型看起来都是一样的。比如,对于不同的数据结构,如 int 数组或 string 字符串向量,只要包含的类型可以相互比较,快速排序算法的实现在结构上看起来是一样的。

如果使用的编程语言不支持 通用性(genericity)的 特殊语言功能(special language feature),将不得不面临如下选择,坏的选择(bad alternatives):

  • 你可以为不同的数据类型一遍又一遍地进行着相同的行为声明;
  • 你可以为常见的基本类型(common base type)编写一段代码,例如 objectvoid*
  • 你可以使用特殊的预处理器。

如果使用其他语言,可能以前就做过一些或者所有上方所述的内容了。然而,这些方法的每一种都有其缺点:

  • 如果你一遍又一遍地实现着这些行为,毫无疑问是叠矩重规(reinvent the wheel)。你犯了相同的错误,并且你试图寻找避免复杂但能更好的算法,因为它们会制造出更多的错误。
  • 如果你为常见基本类编写代码,你会失去类型检查的好处。另外,类可能要求继承于(be derived from)能够使代码更加难以维护的特殊基础类。
  • 如果你使用特殊的预处理器,代码会被一些没有 作用范围(scope)和类型且能够造成 奇怪语义错误(strange semantic errors)的 ”愚蠢的文本替换机制“ 替代掉。

模板是能够解决重复代码编写问题且不会造成上述这些短板的方案。模板是为一种或更多尚未声明类型的函数或者类。当使用模板时,可以隐式或显式地将类型当做参数传递过去。因为模板是语言的特点,可以拥有全部的类型检查和作用范围的支持(full support of type checking and scope)。

在C++标准库中,几乎所有的代码都是模板代码。该库提供排序算法以对 对象、指定类型的值、数据结构(也叫做 容器类(Container Classes)) 进行排序,以管理指定类型的元素 和 字符串(被参数化的字符串类型)等等。

然而,以上仅是模板的使用入门,模板还允许 参数化行为(parameterize behavior) 以 优化代码(optimize code) 和 参数化信息(parameterize infomation)。

FUNCTION TEMPLATES

函数模板(function templates),即 参数化的函数(functions that are parameterized),因此代表了 一个函数系列(a family of functions)。

函数模板为不同的数据类型提供了相同的功能行为调用,或换句话说,代表了一个函数系列。该函数系列看起来就像个普通函数,除了函数的某些元素未被确定,因为这些元素被参数化了。

TEMPLATES DELARATION AND DEFINITION

EXAMPLE

以下样例 声明 了一个函数系列,且下面参数的类型被空置(is left open),如参数 T

1
2
3
4
5
template<typename T>
T max(T a, T b){
// if b < a then yield a else yield b
return b < a ? a : b;
}

在上述样例中,模板参数必须使用 template <comma-separated-list-of-parameters> 的格式进行声明。

此处的关键字 typename 代表着类型参数,这是迄今为止C++程序中最常见的一种模板参数,当然其他参数也可以,后续介绍。此处的类型参数是 T ,可以使用任意标识符当做参数名称,但使用 T 就是惯例而已。类型参数代表着 任意类型(arbitrary type),当函数调用时,由调用的函数决定具体数据类型。开发者可以调用任何类型,只要其提供模板使用的操作。

在上述样例中,类型 T 必须支持 操作符(operator) < ,因为 ab 使用该操作符进行比较,也许在 max() 的定义中很难发现,但要说明的是,T 类型必须是可复制的才能够被返回(T must be copyable in order to be returned)。

由于历史遗留原因,仍可以使用关键词 class 来定义类型参数。typename 关键字是在 C++98 标准之后才出现的,在那之前,class 是引入类型参数的唯一方法,至今仍可以使用。因此,上述模板也可以用下面的代码平替(在语义上没有区别)。

1
2
3
4
temelate<class T>
T max(T a, T b){
return b<a?a:b;
}

但因为 class 也是 的关键字,可能导致二义性,最好在声明模板时使用关键字 typename 。需要注意的是,与类声明不同,当声明类型参数时,关键字 struct 不能用来代替 typename

在C++17之前,类型 T 也必须是可复制的,以确保能够传递参数。但C++17之后,可以传递临时数(temporary),即使没有一个备份或者一个构造器可用(even if neither a copy nor a move constructor is valid)。

如何编写多个函数模板,需要注意什么?

同一个文件内可以编写多个不同的模板函数,如下:

1
2
3
4
5
6
7
8
template<typename T>
T Max(T a, T b){
return b<a?a:b;
}

template<typename t2>
t2 foo(t2*){
}

上面两个模板函数就在同一个文件下,typename 关键字后面的 T 或者 t2 需要具有唯一性。

USING TEMPLATES

需要注意的是,在使用该函数模板时,要在该函数前加上双冒号 :: ,以确保函数模板能够在全局命名空间中被查找到。如果出现下方的 call to ’sth.’ is ambiguous 错误, 即是说需要调用的函数模糊不清,因为标准库中也有一个 std::max(),编译器查找不到该函数具体是在哪个函数文件中。

image-20211005171952999

当然,以上错误也可以通过写不同的函数名称来避免。

模板并不是被编译成一个可以处理所有数据类型的实体,而是被编译成了所有不同数据类型的实体。 即,int max()short max()string max() 等等。单实体多适应 的模板虽然好像可行,但是实际上并不存在。所有的语言规则都遵循 ” 不同的模板参数生成不同的实体“ 的原则。

上面这种用具体类型取代模板参数的过程被称为 实体化(instantiation)。需要注意的是,仅仅是对函数模板的使用就可以触发该实体化过程,因此开发者就没有必要要求实体化过程单独进行(request the instantiation separately)。

另外,只要产生的代码是有效的,void 型也是可用的模板参数,例如:

1
2
3
4
template<typename T>
T foo(T*){

}

1
2
void* vp = nullptr;  // 引出 void foo(void*)
void(vp);

TWO-PHASE TRANSLATION

两段式编译,即Two-phase translation。

如果试图为一个不支持所有操作的模板进行实例化,将会导致编译时错误。

1
2
3
std::complex<float> c1, c2;  //doesn't provide operator <

::max(c1, c2); //ERROR at compile time

因此,模板在被编译时会经过如下两个阶段:

  1. 在定义且没有实例化时,忽略模板参数来检查自身代码的正确性:
    • 标点符号错误被发现,例如缺少分号 ;
    • 使用不依赖已知模板参数的未知命名(类型名,函数名等);
    • 不依赖于已检查模板参数的 静态断言(static assertions)
  2. 在实例化时,模板代码会被再次检查以确保可用,特别是依赖于模板参数的都会被二次检查(double-checked),例如:
1
2
3
4
5
6
7
template<typename T>
void foo(T t){
undeclared();
undeclared(t);
static_assert(sizeof(int>10, "int too small");
static_assert(sizeof(T>10, "T too small");
}

注意到某些编译器在第一阶段没有进行全面检查(don’t perform the full checks),所以直到在最后一阶段的模板代码实例化之前都没办法看到问题。

COMPILE AND LINK

在实际处理模板时,两段式编译会导致很多重要的问题:当函数模板被用于触发其实例化时,编译器(在某些点)需要查看模板定义。当一个函数的声明足以编译它时,就打破了普通函数通常的编译和连接的区别。

TEMPLATE ARGUMENT DEDUCTION

模板实参推断,即 Template argument deduction。

当给函数模板传递实参时,模板参数由我们传递过去的实参决定。如果传递的是两个 int 型实参给 参数类型 T ,则C++编译器就能推断出此时的 T 一定是 int

然而 T 可能只是该参数类型的一部分。例如,声明 max() 可以使用 常参(constant references)。

1
2
3
4
template<typename T>
T max(T const& a, T const& b){
return b<a?a:b;
}

如上方代码所示,传递 整型 int 参数,T 又会被推断为 int ,因为函数参数和 int const& 匹配。

类型推导过程中的类型转换

注意,自动类型转换被限制在类型推导期间:

  • 引用传递:当通过引用声明来调用参数(declaring call parameters by reference)时,即使是 微不足道的转换(trivial conersion) 也不适用于类型推导。用同一个模板参数 T 声明的两个参数类型必须完全匹配。
  • 按值传递:当按数值声明调用参数时,只支持 decay 的琐碎转换。带有const或volatile的限定被忽略,引用转换为被引用的类型,而原始数组或函数转换为相应的指针类型。对于用同一模板参数T声明的两个参数,decayed 的类型必须匹配。

错误提示:对int类型的非恒定值引用不能与int类型的临时值绑定

image-20211007035818560

前方已经说了,类型的自动转换相关注意事项,同一模板参数的两个参数类型必须完全匹配。如果在同一个 .cpp 文件中书写下面下面代码,则会出现下方报错提示 “推导类型冲突(deduced conflicting types for parameter ’T’)”。

1
2
Max(4, 7.2);
Max("Hello",s);

错误提示如下:

image-20211007040416472

但是如果非要使用不一样的数据类型的两个参数来套用同一模板,以下有三种解决方法:

  1. .cpp 文件中进行参数传递时,使用参数类型强制转换。如 Max(static_cast<double>(4), 7.2);

TERMINOLOGY

  1. 按值传递 passing by value
  2. 引用传递 passing by reference

REFERENCE

  1. <C++ Templates> David Vandecoorde

计算机网络体系结构

计算机网络是计算机技术和通信技术相结合的技术领域。由若干计算机用通信信道连接至一起,互相交换信息共享资源,形成计算机网络。

OSI 五层模型

在国际标准化组织(ISO)提出 开放系统互联(OSI,Open System Interconnection)参考模型中,网络系统结构划分为7层。

该OSI模型由上至下分别是 应用层表示层会话层传输层网络层数据链路层物理层

img

在本模型中,物理层数据链路层网络层面向网络通信 的层级;会话层表示层应用层面向信息处理 的层级。

应用层

应用层,即Application Layer。OSI参考模型中最靠近用户的一层,是为计算机用户提供应用接口,也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有:HTTP,HTTPS,FTP,POP3、SMTP等。

实际公司A的老板就是我们所述的用户,而他要发送的商业报价单,就是应用层提供的一种网络服务,当然,老板也可以选择其他服务,比如说,发一份商业合同,发一份询价单,等等。

OSI七层模型中,除了应用层以外,其他功能层级都与用户实际应用没有什么直接联系,能够直接为用户提供各种应用服务的是应用层。应用层可以包含各种应用程序,有些由于使用普遍而实行了标准化,进而形成了应用层上的各种应用协议,诸如SMTP、POP3、WWW、TELNET、FTP、HTTP、HTTPS等。

表示层

表示层,即 Presentation Layer。其提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别。如果必要,该层可提供一种标准表示形式,用于将计算机内部的多种数据格式转换成通信中采用的标准表示形式。数据压缩和加密也是表示层可提供的转换功能之一。

由于公司A和公司B是不同国家的公司,他们之间的商定统一用英语作为交流的语言,所以此时表示层(公司的文秘),就是将应用层的传递信息转翻译成英语。同时为了防止别的公司看到,公司A的人也会对这份报价单做一些加密的处理。这就是表示的作用,将应用层的数据转换翻译等。

会话层

会话层,即 Section Layer。负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。

会话层的同事拿到表示层的同事转换后资料,(会话层的同事类似公司的外联部),会话层的同事那里可能会掌握本公司与其他好多公司的联系方式,这里公司就是实际传递过程中的实体。他们要管理本公司与外界好多公司的联系会话。当接收到表示层的数据后,会话层将会建立并记录本次会话,他首先要找到公司B的地址信息,然后将整份资料放进信封,并写上地址和联系方式。准备将资料寄出。等到确定公司B接收到此份报价单后,此次会话就算结束了,外联部的同事就会终止此次会话。

传输层

传输层,即 Transport Layer。负责建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括 处理差错控制流量控制 等问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。我们通常说的,TCP/UDP就是在这一层。端口号既是这里的“端”。

传输层就相当于公司中的负责快递邮件收发的人,公司自己的投递员,他们负责将上一层的要寄出的资料投递到快递公司或邮局。

传输层位于第三层和高三层之间,也是 面向网路通信面向信息处理 之间的重要层级,是整个协议层次的核心。

传输层的任务是为高层从源端机到目的机提供可靠、经济的数据传输服务,而与具体网络无关。

为了向用户提供经济有效的服务,传输层还提供多路复用和分流的功能。

网络层

网络层,即 Network Layer。负责通过IP寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的IP层。这一层就是我们经常说的IP协议层。IP协议是Internet的基础。

网络层就相当于快递公司庞大的快递网络,全国不同的集散中心,比如说,从深圳发往北京的顺丰快递(陆运为例啊,空运好像直接就飞到北京了),首先要到顺丰的深圳集散中心,从深圳集散中心再送到武汉集散中心,从武汉集散中心再寄到北京顺义集散中心。这个每个集散中心,就相当于网络中的一个IP节点。

数据链路层

数据链路层,即 Data Linker Layer。负责将比特组合成字节,再将字节组合成帧,使用链路层地址 (以太网使用MAC地址)来访问介质,并进行差错检测。数据链路层又分为2个子层:逻辑链路控制子层(LLC)和媒体访问控制子层(MAC)。MAC子层处理CSMA/CD算法、数据出错校验、成帧等;LLC子层定义了一些字段使上次协议能共享数据链路层。 在实际使用中,LLC子层并非必需的。

这个没找到合适的例子

在物理线路上,由于噪声干扰、信号衰减畸变等原因,传输过程中常常出现差错,物理层只负责透明传输无结构的原始比特流,不能进行任何差错控制。

因此,在一条线路上传输数据时,除了必须有一条物理线路外,还需要必要规程来控制数据传输。把这些规程的软件/硬件加到链路上,就构成了数据链路层(Data Linker Layer)。

作用:通过一系列数据链路层协议,在不可靠的物理链路上实现可靠的数据传输。

为此,通常将原始数据分割成一定长度的数据单元(帧),一帧内包含 同步信号差错控制流量控制控制信息数据信息寻址 等。

差错控制

差错控制涉及两方面的问题,即 如何检测错误如何纠正错误

检查错误:要判断一个数据块是否存在错误,发送端必须在数据块中加入冗余信息,使得数据块和冗余信息间存在某种关联,接收端通过验证其关联性来判断数据是否出错。在数据块中加入冗余信息的过程称为 差错编码

差错编码有两种策略,即 仅带有检错能力的检错码带有纠错能力的检错码/纠错码。但任何一种检/纠错码的能力都是有限的,即 不能检/纠出所有的错误。一般检错码的能力越强,所需的冗余信息就会越多,编码效率会随之降低。

常见的差错编码有:

  1. 海明码,即 Hamming Code。由 Richard Hamming 于1950年提出,可纠正一个 bit 错误的编码。
  2. 循环冗余码,即 Cyclic Redundancy Code,又名 多项式码。最广泛使用的编码,但 漏检率很低,只需要简单电路即可实现。
  3. 奇偶校验码,即 Parity Check Code。 最常见也最简单的编码,只需要一个比特,但只能检出奇数个错误,漏检率达50%
  4. 校验和码,即 Checksum Code。也是最常见的检错方式,是传输的数据块中 各字节累加 后得到的 一个字节 或 按字“异或”运算 的结果。

物理层

物理层,即 Physical Layer。实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速度和电缆针脚。常用设备有(各种物理设备)集线器、中继器、调制解调器、网线、双绞线、同轴电缆。这些都是物理层的传输介质。

快递寄送过程中的交通工具,就相当于我们的物理层,例如汽车,火车,飞机,船。

其中物理层有几个概念需要了解和区分,包括 传输速率信道容量传输媒体调制解调交换技术网络拓扑多路复用

传输速率

比特率:每秒传输的二进制位数。

波特率:每秒传输的码元率。

码元:单位携带的比特信息量。

如果码元率为1比特,则波特率和比特率数值上相等。

信道容量

信道容量,即信道能支持的最大数据传输速率,由信道带宽和信噪比决定。

传输媒体

传输媒体,即传输电信号的物理介质。可以是有线、无线、双绞线、同轴电缆 或 光纤 等。

调制解调

调制解调,即一种数据转换成适合在信道上传输的某种电信号形式。

数字信号转换为模拟信号有 调幅调频调相 等方式。

数字信号的信道编码方式有 单极型脉冲双极性脉冲编码 (有归零码 和 不归零码 之分)及 曼彻斯特编码 等(信道编码)。

交换技术

物理层的交换技术有三种:电路交换报文交换分组交换

TYPE PRINCIPLE ADVANTAGES DISADVANTAGES
电路交换 要求通信双方之间建立一条实际的物理通道,并在整个通信过程中 通路独占 数据在中间环节无停留,传输可靠,实时效应好; 电路不共享,资源浪费大,同时电路的建立和撤出的时间较长。
报文交换 一个 大报文(长度无限制的数据块)在通过从源站到目的站之间的中间站时采用 存储-转发 方式(有缓冲区)。 提高线路利用率 大报文延迟时间长,出错率高。
分组交换 将一个大报文分割成一定长度的信息单元(分组),各单元以此编号,以分组为单位进行 存储-转发 除了线路共享外,要求中间环节的缓存区减少,也减少了分组在网络中的延迟时间。
由于各分组在网络中可以走不同路径,该并行传输降低了报文的传输时间。分组长度变短同时降低了出错率(发现出错时重发数据所需时间也缩短)。

网络拓扑

网络拓扑(Network Topology)即网络中节点的互联结构形式,主要分为 星型结构总线型结构树型结构环型结构网型结构。(网络上很多文章都有 混合型结构 这一拓扑类型)

1002_1

TYPE PRINCIPLE SWITCHING ADVANTAGES DISADVANTAGES
星型结构 通过点对点连接至中央节点,任意两点的通信都依赖中央节点。 电路交换 任意节点故障都只会影响本站,而不影响全网。 极大以来中央节点,对中央节点的可靠性和容量要求很高,同时因为需要中央节点连接,耗费大量电缆。
总线型结构 采用单一信道作为传输介质,所有站点通过相应硬件结构接到公共信道(总线)上,任意站点发送的信息,所有其他站都能收到。 分组交换 所需电缆长度短,布线容易。
且总线仅仅是传输信道,无任何处理功能,属于无源器件,可靠性高,增加或减少站点都相对方便。
由于所有节点共享一条公共信道,当多点同时发送信号时,信号会因相互碰撞而造成传输失败,称之为 冲突
系统范围受到限制(传输速率和传输距离相互制约)。一个站点的故障可能影响整个网络,故障检测需要在各站点上进行。(待考究)
树形结构 由总线型结构演化而来,从树根开始,每一个节点向下都可以由许多分支。 故障比较容易隔离和检查。
环型结构 站点和连接站点的点-点链路组成一个闭合环路,每个站点从一条链路上接受数据,然后以相同的速率从另一条链路上发送出去。
链路大多数是单方向的,即数据沿一个方向在网上环行。
所需介质长度较短,由于链路单方向性,可以用光纤作为传输介质; 与总线型结构一样存在冲突问题,一个站点故障会引起全网故障。
网型结构 每个站点都有一条或几条链路同其他站点连接。 由于站点间存在多条路径,数据传输时可以选择空闲站点或绕开故障点,因而 网络资源利用较为充分
但站点或线路故障对网络整体影响较小,可靠性较高
结构较为复杂,成本较高。

多路复用技术

传输媒体的能力(频带宽)往往很强,对传输资源能力的应用(复用)是指将多路信号组合在一条物理信道上进行传输,然后接收端再将各路信号分离开。常见的多路复用技术如 频分多路复用(FDM)、时分多路复用(TDM) 和 码分多址(CDMA)等。

TYPE PRINCIPLE ADVANTAGES DISADVANTAGES
频分多路复用 将信道带宽按频率分割为若干子信道,每个子信道用来传输一路信号。
时分多路复用 将使用信道的时间分成一个个时间片,按一定规律将时间片分配给各路信号,每路信号只能在自己的时间片内独占信道进行传输。
码分多址 允许所有站点在同一时间使用整个信道进行数据传输。

以上三类多路复用技术可以形象地比喻为多个人要发言讨论不同问题时,如何使用同一个会议厅(信道)。

  • 可以把会议厅分成几个小厅(好比频分多路复用),各小厅同时进行各自不同的发言,互不干扰;

  • 可以在一个厅内让各议题在固定的时间片内轮流发言(好比时分多路复用);

  • 可以在一个厅内让各自议题同时发言,但是要用不同的语言(好比码分多址),对某个议题的人来说只能听懂自己的语言,而其他语言被视为随机噪音,可以排除。

TCP/IP 五层模型

实际TCP/IP使用的是5层模型,其中OSI模型中的 应用层、表示层 和 会话层 都用一层 应用层 进行表示,如下图。在每一层都工作着不同的设备,比如我们常用的交换机就工作在数据链路层的,一般的路由器是工作在网络层的。

img

在每一层实现的协议也各不同,即每一层的服务也不同,下图列出了每层主要的协议。

img

现场总线

在工业数据通信领域,总线 是指由导线组成的传输线束,连接多个传感器和执行器,实现各部件之间传送信息的公共通信干线。

然后再来看国际电工技术委员会(IEC)在IEC 61158中对现场总线的标准定义:现场总线 是安装在制造或过程区域的现场装置与控制室内的自动控制装置之间的数字式、串行、多点通信的数据总线。

从定义中可以看到,现场总线是一种数据总线技术,是一种通信协议,且该通信是数字式、串行、多节点的。

目前,在工控领域,车间现场应用最广泛的是 工业以太网 技术和 工业现场总线 技术,为工厂实现自动化带来有力推动。

现场总线应当是应用在生产最底层的一种总线型拓扑网络,即该总线是用于现场控制系统,直接与所有受控(设备)节点串行相连的通信网络。

工业自动化控制的现场一般可以从一台家电设备到一个车间、一个工厂。控制设备和网络所处的环境以及报文结构都有其特殊性,对信号的干扰往往是多方面的,而要求控制必须实时性很强。

现场总线技术

现场总线(Fieldbus)是电气工程及其自动化领域发展起来的一种工业数据总线,它主要解决工业现场的智能化仪器仪表、控制器、执行机构等现场设备间的数字通信以及这些现场控制设备和高级控制系统之间的信息传递问题。由于现场总线简单、可靠、经济实用等一系列突出的优点,因而受到了许多标准团体和计算机厂商的高度重视

传统控制系统难以实现设备之间 以及 系统与外界之间的信息交换,是一个“信息孤岛”。要满足自动化控制技术现代化的要求,同时实现整个企业的信息集成,实施综合自动化,就必须设计出一种能在工业设备之间的多点数字通信,实现底层现场设备之间以及生产现场与外界的信息交换。

现场总线控制系统(Field Control System,FCS)既是一个开放通信网络,又是一种全分布控制系统。作为智能设备的联系纽带,把挂接在总线上、作为网络节点的智能设备连接为网络系统,并进一步构成合成系统,实现基本控制、补偿计算、参数修改、报警、显示、监控、优化 及 管控一体化的综合自动化功能。

现场总线技术是一项集嵌入式系统、控制、计算机、数字通信、网络为一体的综合技术。

局限性

也可以说现场总线是工业控制和计算机网络两者的边缘产物。从纯理论的角度看,它应属于网络范畴。但是现有的网络技术不能完全适应工业现场控制系统的要求,无论是从网络的结构、协议、实时性,还是从适应性、灵活性、可靠性 乃至 成本 上进行考虑,工业控制的底层都有它的特殊性。

现场总线其规模应属于局域网、总线型结构,简单但能满足现场使用需求,所传输信息短小且实时性很强、可靠性高(网络结构层次少,信息帧短小有利于提高实时性和降低受干扰的概率)。然而现场的环境干扰因数众多,有些很强烈且带突发性。

现场总线发展

一般把50年代前的气动信号控制系统PCS称作 第一代,把4~20mA等电动模拟信号控制系统称为 第二代,把数字计算机集中式控制系统称为 第三代,而把70年代中期以来的集散式分布控制系统DCS称作 第四代

现场总线控制系统FCS作为新一代控制系统,一方面,突破了DCS系统采用通信专用网络的局限,采用了基于公开化、标准化的解决方案,克服了封闭系统所造成的缺陷;另一方面把DCS的集中与分散相结合的集散系统结构,变成了新型全分布式结构,把控制功能彻底下放到现场。可以说,开放性、分散性与数字通讯是现场总线系统最显著的特征。

  • 1984年美国Intel公司提出一种 计算机分布式控制系统-位总线(BITBUS),它主要是将低速的面向过程的 输入输出通道与高速的计算机多总线(MULTIBUS)分离,形成了现场总线的最初概念。
  • 80年代中期,美国Rosemount 公司开发了一种可寻址的远程传感器(HART)通信协议。采用在4~20mA模拟量叠加了一种频率信号,用双绞线实现数字信号传输。HART协议已是现场总线的雏形。
  • 1985年由Honeywell和Bailey等大公司发起,成立了World FIP制定了FIP协议。
  • 1987年,以Siemens,Rosemount,横河等几家著名公司为首也成立了一个专门委员会互操作系统协议(ISP)并制定了PROFIBUS协议。后来美国仪器仪表学会也制定了现场总线标准IEC/ISA SP50。
  • 随着时间的推移,世界逐渐形成了两个针锋相对的互相竞争的现场总线集团:一个是以Siemens、Rosemount,横河为首的ISP集团;另一个是由Honeywell、Bailey等公司牵头的WorldFIP集团。1994年,两大集团宣布合并,融合成现场总线基金会(Fieldbus Foundation)简称FF。对于现场总线的技术发展和制定标准,基金委员会取得以下共识:共同制定遵循IEC/ISA SP50协议标准;商定现场总线技术发展阶段时间表。

现场总线能力

部分应用场景或产品开发时,会对现场总线的能力提出如下要求:

本质安全防爆本质安全 是指通过设计等手段使生产设备或生产系统本身具有安全性,即使在误操作或发生故障的情况下也不会造成事故的功能。具体包括 失误—安全(误操作不会导致事故发生或自动阻止误操作)和 故障—安全 功能(设备、工艺发生故障时还能暂时正常工作或自动转变安全状态)。

本质安全型电气设备的防爆原理:通过限制电气设备电路的各种参数,或采取保护措施来限制电路的火花放电能量和热能,使其在正常工作和规定的故障状态下产生的电火花和热效应均不能点燃周围环境的爆炸性混合物,从而实现了电气防爆,这种电气设备的电路本身就具有防爆性能,也就是从“本质”上就是安全的。

现场总线分类

国际上有40多种现场总线,但没有任何一种现场总线能覆盖所有的应用面,按其传输数据的大小可分为3类:

  1. 传感器总线(sensor bus),属于位传输;
  2. 设备总线(device bus),属于字节传输;
  3. 现场总线,属于数据流传输。

FF

FF,即 Foundation Field bus,基金会现场总线。

是以美国Fisher-Rouse mount公司为首的联合了横河、ABB、西门子、英维斯等80家公司制定的ISP协议和以Honeywell公司为首的联合欧洲等地150余家公司制定的World FIP协议于1994年9月合并的。该总线在过程自动化领域得到了广泛的应用,具有良好的发展前景。

FF 总线采用国际标准化组织ISO的开放化系统互联OSI的简化模型(1,2,7层),即物理层、数据链路层、应用层,另外增加了用户层。

FF 分低速H1和高速H2两种通信速率,前者传输速率为31.25Kbit/秒,通信距离可达1900m,可支持总线供电和本质安全防爆环境。后者传输速率为1Mbit/秒和2.5Mbit/秒,通信距离为750m和500m,支持双绞线、光缆和无线发射,协议符号IEC1158-2标准。

FF 的物理媒介的传输信号采用曼切斯特编码。

CAN

CAN,即Controller Area Network,控制器局域网络。

最早由德国BOSCH公司推出,它广泛用于离散控制领域,其总线规范已被ISO国际标准组织制定为国际标准,得到了Intel、Motorola、NEC等公司的支持。CAN协议分为二层:物理层和数据链路层。CAN的信号传输采用短帧结构,传输时间短,具有自动关闭功能,具有较强的抗干扰能力。CAN支持多主工作方式,并采用了非破坏性总线仲裁技术,通过设置优先级来避免冲突,通讯距离最远可达10KM/5Kbps/s,通讯速率最高可达40M/1Mbp/s,网络节点数实际可达110个。目前已有多家公司开发了符合CAN协议的通信芯片。

LonWorks

由美国Echelon公司推出,并由Motorola、Toshiba公司共同倡导。它采用ISO/OSI模型的全部7层通讯协议,采用面向对象的设计方法,通过网络变量把网络通信设计简化为参数设置。支持双绞线、同轴电缆、光缆和红外线等多种通信介质,通讯速率从300bit/s至1.5M/s不等,直接通信距离可达2700m(78Kbit/s),被誉为通用控制网络。Lonworks技术采用的Lon Talk协议被封装到Neuron(神经元)的芯片中,并得以实现。采用LonWorks技术和神经元芯片的产品,被广泛应用在楼宇自动化、家庭自动化、保安系统、办公设备、交通运输、工业过程控制等行业。

Device Net

Device Net是一种低成本的通信连接也是一种简单的网络解决方案,有着开放的网络标准。Device Net具有的直接互联性不仅改善了设备间的通信而且提供了相当重要的设备级阵地功能。Device Net基于CAN技术,传输率为125Kbit/s至500Kbit/s,每个网络的最大节点为64个,其通信模式为:生产者/客户(Producer/Consumer),采用多信道广播信息发送方式。位于Device Net网络上的设备可以自由连接或断开,不影响网上的其他设备,而且其设备的安装布线成本也较低。Device Net总线的组织结构是Open Device Net Vendor Association(开放式设备网络供应商协会,简称“ODVA”)。

PROFIBUS

PROFIBUS是德国标准(DIN19245)和欧洲标准(EN50170)的现场总线标准。由PROFIBUS--DP、PROFIBUS-FMS、PROFIBUS-PA系列组成。DP用于分散外设间高速数据传输,适用于加工自动化领域。FMS适用于纺织、楼宇自动化、可编程控制器、低压开关等。PA用于过程自动化的总线类型,服从IEC1158-2标准。PROFIBUS支持主-从系统、纯主站系统、多主多从混合系统等几种传输方式。PROFIBUS的传输速率为9.6Kbit/s至12Mbit/s,最大传输距离在9.6Kbit/s下为1200m,在12Mbit/s小为200m,可采用中继器延长至10km,传输介质为双绞线或者光缆,最多可挂接127个站点。

HART

HART,即 Highway Addressable Remote Transducer,高速可寻址远程传感器。

最早由Rosemount公司开发。其特点是在现有模拟信号传输线上实现数字信号通信,属于模拟系统向数字系统转变的过渡产品。其通信模型采用物理层、数据链路层和应用层三层,支持点对点主从应答方式和多点广播方式。由于它采用模拟数字信号混和,难以开发通用的通信接口芯片。

HART能利用总线供电,可满足本质安全防爆的要求,并可用于由手持编程器与管理系统主机作为主设备的双主设备系统。

CC-Link是Control&Communication Link(控制与通信链路系统)的缩写,在1996年11月,由三菱电机为主导的多家公司推出,其增长势头迅猛,在亚洲占有较大份额。在其系统中,可以将控制和信息数据同是以10Mbit/s高速传送至现场网络,具有性能卓越、使用简单、应用广泛、节省成本等优点。其不仅解决了工业现场配线复杂的问题,同时具有优异的抗噪性能和兼容性。CC-Link是一个以设备层为主的网络,同时也可覆盖较高层次的控制层和较低层次的传感层。2005年7月CC-Link被中国国家标准委员会批准为中国国家标准指导性技术文件。

World FIP

World FIP的北美部分与ISP合并为FF以后,World FIP的欧洲部分仍保持独立,总部设在法国。其在欧洲市场占有重要地位,特别是在法国占有率大约为60%。World FIP的特点是具有单一的总线结构来适用不同的应用领域的需求,而且没有任何网关或网桥,用软件的办法来解决高速和低速的衔接。World FIP与FFHSE可以实现“透明联接”,并对FF的H1进行了技术拓展,如速率等。在与IEC61158第一类型的连接方面,World FIP做得最好,走在世界前列。

此外较有影响的现场总线还有丹麦公司Process-Data A/S提出的P-Net,该总线主要应用于农业、林业、水利、食品等行业;Swift Net现场总线主要使用在航空航天等领域,还有一些其他的现场总线这里就不再赘述了。

INTERBUS

INTERBUS是德国Phoenix公司推出的较早的现场总线,2000年2月成为国际标准IEC61158。INTERBUS采用国际标准化组织ISO的开放化系统互联OSI的简化模型(1,2,7层),即物理层、数据链路层、应用层,具有强大的可靠性、可诊断性和易维护性。其采用集总帧型的数据环通信,具有低速度、高效率的特点,并严格保证了数据传输的同步性和周期性;该总线的实时性、抗干扰性和可维护性也非常出色。INTERBUS广泛地应用到汽车、烟草、仓储、造纸、包装、食品等工业,成为国际现场总线的领先者。

其他

RS-485和现场总线同属于总线;ModBus和现场总线同属于通信协议;

严格来讲,RS-485≠现场总线,ModBus≠现场总线,RS-485+ModBus(还有其他什么的)才构成现场总线,其中RS-485工作在现场总线的物理层,ModBus工作在现场总线的应用层;

参考

  1. 《现场总线CAN原理与技术应用》第二版,北京航空航天大学出版社,饶运涛、邹继军、王进宏、郑勇芸 编著
  2. 现场总线
  3. OSI网络模型
  4. 本质安全
  5. 本质安全型电气设备防爆原理