大狗哥传奇

  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

fork

发表于 2020-10-13 更新于 2020-12-02 分类于 linux
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
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>

void main()
{
char str[6]="hello";

pid_t pid=fork();

if(pid==0)
{
str[0]='b';
printf("子进程中str=%s\n",str);
printf("子进程中str指向的首地址:%x\n",(unsigned int)str);
}
else
{
sleep(1);
printf("父进程中str=%s\n",str);
printf("父进程中str指向的首地址:%x\n",(unsigned int)str);
}
}
// 子进程中str=bello
// 子进程中str指向的首地址:bfdbfc06
// 父进程中str=hello
// 父进程中str指向的首地址:bfdbfc06

这里涉及到逻辑地址(或称虚拟地址)和物理地址的概念。

  • 逻辑地址:CPU 所生成的地址。
  • 物理地址:内存单元所看到的地址。

用户程序看不到真正的物理地址。用户只生成逻辑地址,且认为进程的地址空间为 0 到 max。物理地址的方位从R+0到R+MAX。R 为基地址,内存管理单元(MMU),根据基地址将程序地址空间使用的逻辑地址变换为内存中的物理地址过程称为地址映射。

fork()会产生一个和父进程完全相同的子进程,但子进程在此后会多 exec 系统调用。出于效率考虑。linux 中引入了写时复制技术,也就是只有进程空间的各段(代码段,数据段,堆栈)的内容要发生变化时,在会将父进程的内容复制一份给子进程。在 fork 之后 exec 之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是执行父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。当父子进程有更改相应段的行为发生后,再为子进程相应的段分配物理空间。

fork 时子进程获得父进程数据空间,堆和栈的复制,所以变量的地址(虚拟地址)也是一样的,每个进程都有自己的虚拟空间,不同进程的相同的虚拟地址可以对应不同的物理地址。

fork 子进程完全复制父进程的栈空间,也复制了页表,但没有复制物理页面,所以这时虚拟地址相同,物理地址也相同,但是会把父子共享的页面标记为只读,如果父子进程一直对这个页面是同一个页面,直到其中任何一个进程要对共享的页面进行写操作,这时内核会复制一个物理页面给这个进程使用,同时修改页表,而把原来的只读页面标记为可写,留给另外一个进程使用。

内核一般会先调度子进程,很多情况下子进程要马上执行 exec,会情况栈,堆。这些和父进程共享的空间,加载新的代码段,这就避免了写时复制拷贝共享页面的机会。如果父进程先调度很可能写共享页面,会造成写时复制无用。

假定父进程 malloc 的指针指向 0x12345678, fork 后,子进程中的指针也是指向 0x12345678,但是这两个地址都是虚拟内存地址 (virtual memory),经过内存地址转换后所对应的 物理地址是不一样的。所以两个进城中的这两个地址相互之间没有任何关系。

(注 1:在理解时,你可以认为 fork 后,这两个相同的虚拟地址指向的是不同的物理地址,这样方便理解父子进程之间的独立性)

(注 2:但实际上,linux 为了提高 fork 的效率,采用了 copy-on-write 技术,fork 后,这两个虚拟地址实际上指向相同的物理地址(内存页),只有任何一个进程试图修改这个虚拟地址里的内容前,两个虚拟地址才会指向不同的物理地址(新的物理地址的内容从原物理地址中复制得到))

flink

发表于 2020-10-09 更新于 2020-12-02

简介

flink 是一个分布式处理引擎,用于在无边界和有边界数据流上进行有状态的计算。任何类型的数据都可以形成一种事件流。

官网。

数据可被作为有界或无界流来处理

  • 无界流 有定义流的开始,但是没有定义流的结束。它们会无休止的产生数据,无界流的数据在摄取后需要立即进行处理,因为输入是无限的,在任何时候输入都不会完成。处理无界数据通常以特定顺序摄取事件,例如事件发生的顺序,以便能够推断结果的完整性。
  • 有界流 有定义流的开始,也有定义流的结束。,有界流可以在摄取所有数据后再进行计算。有界流所有数据可以被排序,所以并不需要有序摄取。有界流处理通常被称为批处理。
flink_示例.png
flink_示例.png

基本组件

可以由流处理框架构建和执行的应用程序类型是由框架对流,状态,时间的支持程度来决定的。

流

数据流是流处理的基本要素。流拥有多种特征,这些特征决定了流如何以及何时被处理。

  • 有界和无界的数据流:流可以是无界的;也可以是有界的,例如固定大小的数据集。
  • 实时和历史记录的数据流:所有的数据都是以流的方式产生,但用户通常会使用两种截然不同的方式处理数据。或是在数据生成时进行实时的处理;亦或是先将数据流持久化到存储系统中(例如文件系统或对象存储),然后再进行批处理。

状态

具有一定复杂度的流处理应用都是有状态的,任何运行基本业务逻辑的流处理应用都需要在一定时间内存储所接收到的事件或中间结果,以供后续的某个时间点(例如收到下一个事件或经过一段特定时间)进行访问并进行后续处理。 flink_状态.png 应用状态是 Flink 中的一等公民,FLink 提供了许多状态管理相关的特性,其中包括:

  • 多种状态基础类型:flink 为多种不同的数据结构提供了相对应的状态基础类型,例如 value,list,map。
  • 插件化的 State Backend:负责管理程序应用状态,并在需要的时候进行 checkpoint。可以将状态存储在内存中或者 RocksDB。
  • 精确一次语义:Flink 的 checkpoint 和故障恢复算法保证了故障发生后应用状态的一致性。
  • 超大数据量状态:利用其异步以及增量式的 checkpoint 算法,存储数 TB 级别的应用状态。
  • 可弹性伸缩的应用:能够通过在更多或更少的工作节点上对状态进行重新分布,支持有状态应用的分布式的横向伸缩。

时间

时间是流处理应用的另一个重要的组成部分。因为事件总是在特定时间点发生,所以大多数的事件流都拥有事件本身所固定的时间语义。许多常见的流计算都基于时间。流处理的一个重要方面是应用程序如何衡量时间,即区分事件时间,和处理时间。

  • 事件时间模式:使用事件时间的流处理应用根据事件本身自带的时间戳进行结果的计算。因此,无论处理的是历史记录的事件还是实时的事件,事件时间模式的处理总能保证结果的准确性和一致性。
  • watermark 支持:用以衡量时间进展。watermark 也是一种平衡处理延时和完整性的灵活机制。
  • 迟到数据处理:当带有 watermark 的事件时间模式处理数据流时,在计算完成后之后扔会有相关数据到达。这样的时间被称为迟到事件。Flink 提供了多种处理迟到数据的选项,例如将这些数据重定向到旁路输出(side output)或者更新之前完成计算的结果。
  • 处理时间模式:处理时间默认根据处理引擎的机器时钟触发计算,一般适用于有着严格低延迟需求,并且能够容忍近似结果的流处理应用。

分层 API

FLink 根据抽象程度分层,提供了三种不同的 API。每一种 API 在简洁性上和表达力上有着不同的侧重,并且针对不同的应用场景。 flink_layer.png

  • ProcessFunction 是 FLink 所提供的最具有表达能力的接口。ProcessFunction 可以处理一或两个输入数据流中的单个事件或者归入一个特定窗口内的多个事件。它提供了对于时间和状态的细粒度控制。开发者可以在其中任意的修改状态,也能够注册定时器用以在未来的某一时刻触发回调函数。因此,你可以利用 ProcessFunction 实现所有有状态的事件驱动应用锁需要的基于单个事件的复杂业务逻辑。 下面的代码示例展示了如何在 KeyedStream 上利用 KeyedProcessFunction 对标记为 START 和 END 的事件进行处理。

    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
    /**

    * 将相邻的 keyed START 和 END 事件相匹配并计算两者的时间间隔
    * 输入数据为 Tuple2<String, String> 类型,第一个字段为 key 值,
    * 第二个字段标记 START 和 END 事件。
    */
    public static class StartEndDuration
    extends KeyedProcessFunction<String, Tuple2<String, String>, Tuple2<String, Long>> {

    private ValueState<Long> startTime;

    @Override
    public void open(Configuration conf) {
    // obtain state handle
    startTime = getRuntimeContext()
    .getState(new ValueStateDescriptor<Long>("startTime", Long.class));
    }

    /** Called for each processed event. */
    @Override
    public void processElement(
    Tuple2<String, String> in,
    Context ctx,
    Collector<Tuple2<String, Long>> out) throws Exception {

    switch (in.f1) {
    case "START":
    // set the start time if we receive a start event.
    startTime.update(ctx.timestamp());
    // register a timer in four hours from the start event.
    ctx.timerService()
    .registerEventTimeTimer(ctx.timestamp() + 4 * 60 * 60 * 1000);
    break;
    case "END":
    // emit the duration between start and end event
    Long sTime = startTime.value();
    if (sTime != null) {
    out.collect(Tuple2.of(in.f0, ctx.timestamp() - sTime));
    // clear the state
    startTime.clear();
    }
    default:
    // do nothing
    }
    }

    /** Called when a timer fires. */
    @Override
    public void onTimer(
    long timestamp,
    OnTimerContext ctx,
    Collector<Tuple2<String, Long>> out) {

    // Timeout interval exceeded. Cleaning up the state.
    startTime.clear();
    }
    }
  • DataStream API 为许多通用的流处理操作提供了处理原语。这些操作包括窗口,逐条记录的转换操作,在处理事件时进行外部数据库查询等。DataStream API 支持 Java 和 Scala 语言,预先定义了例如 map()、reduce()、aggregate()等函数。你可以通过扩展实现预定义接口或使用 Java,Scala 的 lambda 表达式实现自定义的函数。 下面的代码示例展示了如何捕捉会话事件范围内所有的点击事件,并对每一次会话的点击量进行计数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 网站点击 Click 的数据流
    DataStream<Click> clicks = ...

    DataStream<Tuple2<String, Long>> result = clicks
    // 将网站点击映射为 (userId, 1) 以便计数
    .map(
    // 实现 MapFunction 接口定义函数
    new MapFunction<Click, Tuple2<String, Long>>() {
    @Override
    public Tuple2<String, Long> map(Click click) {
    return Tuple2.of(click.userId, 1L);
    }
    })
    // 以 userId (field 0) 作为 key
    .keyBy(0)
    // 定义 30 分钟超时的会话窗口
    .window(EventTimeSessionWindows.withGap(Time.minutes(30L)))
    // 对每个会话窗口的点击进行计数,使用 lambda 表达式定义 reduce 函数
    .reduce((a, b) -> Tuple2.of(a.f0, a.f1 + b.f1));
  • SQL & Table API FLink 支持两种关系型的 API,TableAPI 和 SQL。这两个 API 都是批处理和流处理统一的 API,这意味着在无边界的实时数据流和有边界的历史记录数据流上,关系型 API 会以相同的语义执行查询,并产生相同的结果。Table API 和 SQL 借助了 Apache Calcite 来进行查询的解析,校验以及优化。它们可以与 DataStream 和 DataSet API 无缝集成,并支持用于自定义的标量函数,聚会函数以及表值函数。 FLink 的关系型 API 旨在简化数据分析,数据流水线和 ETL 应用的定义。 下面的代码示例展示了如何使用 SQL 语句捕获会话时间范围内所有的点击流事件,并对每一次会话的点击量进行计数。

    1
    2
    SELECT userId, COUNT(*) FROM clicks
    GROUP BY SESSION(clicktime, INTERVAL '30' MINUTE), userId

安装

下载地址,我们使用版本flink-1.10.2

java 版本仅支持 8 或 11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ tar -xzf  flink-1.10.2-bin-scala_2.11.tgz
$ cd flink-1.10.2

#启动 若默认端口8081被占用,则无法正常启动,需修改默认端口
$ ./bin/start-cluster.sh
Starting cluster.
Starting standalonesession daemon on host CentOS7.
[INFO] 1 instance(s) of taskexecutor are already running on CentOS7.
Starting taskexecutor daemon on host CentOS7.

#停止
./bin/stop-cluster.sh

#测试 提交一个任务
$ ./bin/flink run examples/streaming/WordCount.jar
# 可通过日志查看
$ tail log/flink-*-taskexecutor-*.out
(to,1)
(be,1)
(or,1)
(not,1)
(to,2)
(be,2)
# 也可以使用Web UI界面查看,默认端口为8081,可在conf/flink-conf.yaml中修改rest.port配置

应用场景

事件驱动型应用

什么是事件驱动型应用

事件驱动型应用是一类具有状态的应用,它从一个或多个事件流提取数据,并根据到来的事件触发计算、状态更新或其他外部动作。

事件驱动型应用是在计算存储分离的传统应用基础上演化而来。在传统架构中,应用需要读写远程事务型数据库。而事件驱动型应用是基于状态化流处理来完成的,通过这样的设计,数据和计算不会分离,应用只需访问本地(内存或磁盘)即可获取数据。系统容错性的实现依赖于定期向远程持久化存储写入 checkpoint。下图描述了传统应用和事件驱动型应用架构的区别。 flink_区别.png

事件驱动型应用的优势

事件驱动型应用无须查询远程数据库,本地数据访问使得它具有更高的吞吐量和更低的延迟。而由于定期向远程持久化存储的 checkpoint 工作可以异步、增量式完成,因此对于正常事件的处理的影响甚微。事件驱动型应用的优势不仅限于本地数据访问,在传统分层架构下,通常多个应用会共享同一个数据库,因而任何对数据库自身的更改都需谨慎协调。反观事件驱动型应用,由于只需考虑自身数据,因此在更改数据表示或服务扩容时所需要的协调工作将大大减少。

FLink 如何支持事件驱动型应用

事件驱动型应用会受制于底层流处理系统对时间和状态的把控能力,FLink 诸多特质都是围绕这些方面来设计的。它提供了一系列丰富的状态操作原语,允许以精确一次的一致性语义合并海量规模的状态数据。此外,Flink 还支持事件时间和自由度极高的定制化窗口逻辑,而且它内置的 ProcessFunction 支持细粒度时间控制,方便实现一些高级业务逻辑。同时,Flink 还拥有一个复杂事件处理类库(CEP),可以用来检测数据流中的模式。 savepoint 是一个一致性的状态镜像,它可以用来 初始化任意状态兼容的应用。在完成一次 savepoint 后,即可放心对应用升级或扩容,还可以启动多个版本的应用来完成 A/B 测试。

数据分析应用

什么是数据分析应用

数据分析任务需要从原始数据中提取有价值的信息和指标。传统的分析方式通常是利用批查询,或将事件记录下来并基于此有限数据集构建应用来完成。为了得到最新数据的分析结果,必须先将它们加入分析数据集并重新执行查询或运行应用,随后将结果写入存储系统或生成报告。

借助一些先进的流处理引擎,还可以实时地进行数据分析。流式查询或应用可以进入实时事件流,并随着事件消费持续产生和更新结果。这些结果数据可能会写入外部数据库或以内部状态的形式维护。仪表展示应用可以相应地从外部数据库读取数据或直接查询应用的内部状态。

如下图所示,Flink 同时支持流式及批处理应用

flink_analytics.png
flink_analytics.png

流式分析应用的优势

和批量分析相比,由于流式分析省掉了周期性的数据导入和查询过程,因此从事件中获取指标的延迟更低。不仅如此,批量查询必须处理那些由定期导入和输入有界性导致的人工数据边界,而流式查询则无需考虑该问题。 另一方面,流式分析会简化应用抽象。批量查询的流水线通常由多个独立部件组成,需要周期性调度提取数据和执行查询。如此复杂的流水线操作起来并不容易,一旦某个组件出错将会影响流水线的后续步骤。而流式分析应用整体运行在 Flink 之类的高端处理系统之上,涵盖了从数据接入到连续结果计算的所有步骤,因此可以依赖底层引擎提供的故障恢复机制。

Flink 如何支持数据分析类应用

Flink 为持续流式分析和批量分析都提供了良好的支持。具体而言,它内置了一个符合 ANSI 标准的 SQL 接口,将批,流查询的语义统一起来。无论是在记录时间的静态数据集上还是实时事件流上。相同的 SQL 查询都会得到一致的结果。同时 Flink 还支持丰富的用户自定义函数,运行在 SQL 执行定制化代码。如何还需进一步定制逻辑,可以利用 Flink DataStream API 和 DataSet API 进行更低层次的控制。此外,Flink 的 Gelly 库为基于批量数据集的大规模高性能图分析提供了算法和构建模块支持。

数据管道应用

什么是数据管道

Extract-transform-load(ETL)是一种在存储系统之间进行数据转换和迁移的常用方法。ETL 作业通常会周期性地触发,将数据从事务型数据库拷贝到分析型数据库或数据仓库。 数据管道和 ETL 作业的用途相似,都可以转换、丰富数据,并将其从某个存储系统移动到另一个。但数据管道是以持续流模式运行,而非周期性触发。因此它支持一个不断生成数据的源头读取记录,并将它们以低延迟移动到终点。例如:数据管道可以用来监控文件系统目录的新文件,并将其数据写入事件日志;另一个应用可能会将数据流物化到数据库或增量构建和优化查询索引。

下图 描述了周期性 ETL 作业和持续数据管道的差异。 flink_etl作业和持续数据管道的差异.png

数据管道的优势

和 ETL 作业相比,持续数据管道可以明显降低将数据移动到目的短的延迟。此外,由于它能够持续消费和发送数据,因此用途更广,支持用例更多。

Flink 如何支持数据管道应用

很多常见的数据转换和增强操作可以利用 Flink 的 SQL 接口(或者 Table API)以及用户自定义函数解决。如果数据管道有更高级的需求,可以选择更通用的 DataStream API 来实现。Flink 为多种数据存储系统(如:Kafka、Kinesis、Elasticsearch、JDBC 数据库系统等)内置了连接器。同时它还提供了文件系统的连续型数据源即数据汇,可用来监控目录变化和以时间分区的方式写入文件。

c

发表于 2020-09-21 更新于 2020-12-02 分类于 c

基础知识

关键字

  • const 指向 const 的变量的值为只读。指向 const 的指针通常用于函数形参中,表明该函数不会使用指针改变数据。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // const 数据或非 const 数据的地址初始化为指向 const 的指针或为其赋值时合法的
    // 只能把非 const 数据的地址赋给普通指针
    int rates[1]={1};
    const int locked[1]={10};

    const int *pc = rates; //有效
    pc = locked; //有效
    pc = &locked[0]; //有效

    int *pnc = rates; //有效
    pnc = locked; //无效
    pnc = &locked[0]; //有效
    1
    2
    3
    const * pc; //不能修改指针指向地址上的值, 但是可以修改它所指向的地址
    * const pc; //能修改指针指向地址上的值, 但是不能修改它所指向的地址
    const * const pc; //不能修改指针指向地址上的值, 也不能修改它所指向的地址
  • static 声明变量具有静态存储期
  • _thread_local 声明变量为线程独占
  • register 声明变量为寄存器变量
  • extern 声明变量的定义在别处
  • restrict 它只可以用于限定和约束指针,并表明指针是访问一个数据对象的唯一且初始的方式。关键字 restrict 有两个读者。一个是编译器,它告诉编译器可以自由地做一些有关优化的假定。另一个读者是用户,他告诉用户仅使用满足 restrict 要求的参数。
  • unsigned 将数字类型无符号化,  例如 int 型的范围:-2^du31 ~ 2^31 - 1,而 unsigned int 的范围:0 ~ 2^32。
  • typedef 为某一类型自定义名称

    1. 与#define不同,typedef 创建的符号名只受限于类型,不能用于值
    2. typedef 由编译器解释,不是预处理器
    3. 在其首先范围内,typedef 比#define更灵活 例如
    1
    2
    3
    4
    5
    6
    7
    typedef unsigned char BYTE;
    typedef char * STRING; //一个指向char的指针的类型
    //也可以用于结构,可以省略标签名
    typedef struct complex {
    float read;
    float imag;
    } COMPLEX

存储类别

存储类别 存储器 作用域 链接 申明方式
自动 自动 块 无 块内
寄存器 自动 块 无 块内,使用关键字 register
静态外部链接 静态 文件 外部 所有函数外
静态内部链接 静态 文件 内部 所有函数外,使用关键字 static
静态无链接 静态 块 无 块内,使用关键字 static
  • 使用 static 修饰的变量称为静态变量,该变量在内存中的地址不变,它的值可以改变。
  • 具有文件作用域的外部变量自动具有静态存储器。
  • 外部变量若为初始化,则自动被初始化为 0,外部变量只能使用常量来初始化。
1
2
3
4
5
6
7
8
9
10
11
12
while (1) {

static int a = 1;
printf("%d %p \n", a++, &a);
if (a > 3) {
return 0;
}
}

// 1 0x1025c0018
// 2 0x1025c0018
// 3 0x1025c0018
1
2
3
4
int tern = 1; //定义式声明
int main(void){
extern int tern; //引用式声明
}

复合字面量

创建一个与数组类似的匿名数组,必须在创建的同时使用它,使用指针记录地址就是一种用法。

1
2
3
int *p = (int[2]){10, 20};
printf("%d\n", *p);
printf("%d\n", p[0]);

也可以作为实际参数传递给带有匹配形式参数的函数

函数原型

C 语言代码由上到下依次执行,原则上函数定义要出现在函数调用之前,否则就会报错。但在实际开发中,经常会在函数定义之前使用它们,这个时候就需要提前声明。函数声明的格式非常简单,相当于去掉函数定义中的函数体再加上分号;,如下所示:返回值类型 函数名( 类型 形参, 类型 形参… );也可以不写形参,只写数据类型:返回值类型 函数名( 类型, 类型…);

预处理器

#define

使用#define 指令来定义明示常量(manifest constant,也叫做符号常量),其格式为

1
2
3
 #define     PX   printf("x is %d.\n",x);

//预处理指令 宏 替换体

#define 在编译时替换,从宏编程最终替换文本的过程称为宏展开。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#define PI 3.14
#define PRINT printf("%f \n"

int main(void)
{
printf("begin\n");
PRINT,PI);
}

// begin
// 3.140000

#define使用参数,称为类函数宏,其格式为

1
2
3
//              宏参数
#define PX(X,Y) (((X)+(Y))/2)
//预处理指令 宏 替换体
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#define SQ(X) X*X

int main(){
int x = 2;
int a = SQ(x);
printf("%d\n",a);

a= SQ(x+5);
printf("%d\n",a);
//替换后其实为 2+5*2+5
}

预处理器不做计算,不求值,只替换字符序列

在类函数宏的替换体重,#号作为一个预处理运算符,可以把几号转换为字符串,例如,如果 x 是一个宏形参,那么#x 就转换为字符串 x 的形参名。这个过程称为字符串化(stringizing)

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#define SQ(X) printf("-->" #X "<--is %d\n",X);
int main() {
int y = 2;
SQ(y);
SQ(y + 5);
}
// -->y<--is 2
// -->y + 5<--is 7

预处理器粘合剂:##运算符,把两个几号组合成一个记号。例如

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

#define XNAME(N) x##N
#define PRINT_XN(N) printf( "X"#N" - %d\n",x##N);

int main() {
int XNAME(1) = 14; //变成int x1 = 14;
int XNAME(2) = 14; //变成int x2 = 20;
PRINT_XN(1); //变成 printf("x1 = %d\n",x1);
PRINT_XN(2); //变成 printf("x2 = %d\n",x2);

}

变参宏:...和_ _VA_ARGS_ _

1
2
3
4
5
6
7
8
9
#include <stdio.h>

#define PR(...) printf(__VA_ARGS__)

int main() {

PR("Howdy\n");
PR("weight=%d,shipping=$%.2f\n",10,10.1f);
}

可以使用undef取消已定义的#define指令,即使原来没有定义,#define宏的作用域从它在文件中的声明处开始,直到用#undef指令取消宏为止,或文件尾。

#include

当预处理器发现#include指令时,会查找后面的文件名把文件的内容包含到当前文件中,即替换源文件中的#include指令。这相当于把被包含文件的全部内容输入到源文件#include指令所在的文职。

1
2
3
#include <stdio.h>           //查找系统目录
#include "hot.h" //查找当前工作目录,未找到在查找系统目录
#include "/usr/biff/p.h" //查找指定目录文件

我们可以定义头文件

定义constant.h文件

1
2
#define STARS "********************"
#define BAR_WIDTH 40

那么我们在主程序里就可以引入

1
2
3
4
5
6
7
8
#include <stdio.h>
#include "constant.h"

int main()
{
printf("starts %d\n",BAR_WIDTH)
return 0;
}

条件编译

可以使用其他指令穿件条件编译(conditional compilation),也就是说,可以使用这些指令告诉编译器根据编译时的条件执行或忽略信息(或代码)块。

if、#ifdef、#else、#endif、ifndef(判断是否是未定义的)

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

//#define H 0

#ifdef H
#define M 1
#else
#define M 2
#endif

int main() {
printf("%d\n", M); //根据H是否定义,M的值为1或2
}

预定义宏

宏 含义
_ _DATE_ _ 预处理的日期(“Mmm dd yyyy”)形式的字符串字面量
_ _FILE_ _ 当前源码文件名
_ _ LINE_ _ 当前源码行号
_ _STDC_ _ 设置为 1 表示实现遵循 C 标准
_ _STDC_HOSTED_ _ 本机环境设置为 1,否则设置为 0
_ _STDC_VERSION_ _ 支持 C99 标准,设置为 199901L;支持 C11 标准,设置为 201112L
_ _TIME_ _ 代码的时间,格式为“hh:mm:ss”

#line 和 #error

#line指令重置_ _LINE_ _和_ _File_ _宏报告的行号和文件名

1
2
#line 1000
#line 10 "cool.c"

#error 指令让预处理器发出一条错误信息,该消息包含指令中的文本。如果可能的化,编译过程应该中断。

1
2
3
#if __STDC_VERSION__  !=1L
#error fuck you
#endif

在编译过程中

1
2
3
4
5
$ gcc hello.c  -o hello  && ./hello
hello.c:4:2: error: fuck you
#error fuck you
^
1 error generated.

泛型选择表达式

泛型编程指那些没有特定类型,但是一旦指定一种类型,就可以转换成指定类型的代码。C11 新增了一种表达式,叫做泛型选择表达式可根据表达式的类型选择一个值。泛型选择表达式不是预处理指令,但是在一些泛型编程中它常用作#define 宏定义的一部分。

1
_Generic(x,int:0,float:1,double:2,default:3)

泛型选择语句与 switch 语句类似,泛型选择语句以类型匹配标签,

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
#include <stdio.h>

#define M(X) _Generic((X),int:1,double:2,default:3)
#define F(X) _Generic((X),int:m1,default:m2)
#define F2(X) _Generic((X),int:m1,default:m2)()
#define F3(X) _Generic((X),int:m3,default:m4)(X)

void m1(){
puts("m1");
}
void m2(){
puts("m2");
}
void m3(int x){
printf("m3:%d\n",x);
}
void m4(float x){
printf("m4:%f\n",x);
}
int main() {

printf("%d\n", M(1));
F(1)();
F2(2.0);
F3(2.0f);

}
// 1
// m1
// m2
// m4:2.000000

对一个泛型选择表达式求值时,程序不会先对第一项求值,它只确定类型。只有匹配标签的类型后才会对表达式求值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

#define M(X) _Generic((X),int:1,double:2,default:3)

void m1(){
puts("m1");
}
int m2(){
puts("m2");
return 0;
}
int main() {
//只确定m1,m2的类型,而不会进行调用
printf("%d\n", M(m1()));
printf("%d\n", M(m2()));
}
// 3
// 1

内联函数

内联函数应该短小。内联函数具有内部链接

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

inline static void print(char * x){
puts(x);
}

int main() {
char * x = "hello";
puts(x);
print(x);
}

断言

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <assert.h>


int main() {
int i =1;
assert(i==2);
}
//Assertion failed: (i==2), function main, file /Users/li/code/c_plus/c/main.c, line 7.

可以使用在#include <assert.h>前加入#define NDEBUG 禁用断言

1
2
3
4
5
6
7
8
#include <stdio.h>
#define NDEBUG
#include <assert.h>

int main() {
int i =1;
assert(i==2);
}

C11 新增一个_Static_assert编译时断言

优先级

一些复杂的声明

1
2
3
4
5
6
7
int board[8][8]; //声明一个内涵int数组的数组
int ** ptr; //声明一个指向指针的指针,被指向的指针指向int
int *risks[10]; //声明一个内涵10个元素的数组,每个元素都是一个执行int的指针
int (* rusks)[10]//声明一个执行数组的指针,该数组内含10个int类型的值
int * oof[3][4] //声明一个3x4的二维数组,每个元素都是执行int的指针
int (* uuf)[3][4]//声明一个指向3x4的二维数组的指针,该数组中内含int类型值
int (* uof[3])[4]//声明一个内涵3个指针元素的额数组,其中每个指针都指向一个内含4个int类型元素的数组

要看懂上述声明,关键要理解*,(),[]的优先级。(),[]具有相同的优先级,它们比*(解引用运算符)的优先级高

指针

  1. &运算符访问变量地址
  2. *运算符获取地址上的值
  3. 指针变量的值可以使用*variable的方式去取值,赋值
  4. 函数定义中的形式参数若为指针需要声明为*variable
  5. 指针+1表示增加一个存储单元,对于数组而言,意味着访问下一个元素的指针地址
  6. 数组的申明为指针地址,其值即为数组的首元素指针地址
  7. 指针变量也有自己的内存地址和值
  8. 指向 void 的指针作为一个通用指针,用于指针指向不同类型,C99 为此描述加入新的关键字 restrict
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int pooh = 24;
int *ptr = &pooh;
printf("%d %p %d %p\n", pooh, &pooh, *ptr, ptr);
*ptr = 100;
printf("%d %p %d %p\n", pooh, &pooh, *ptr, ptr);
int other =-1;
ptr = &other;
*ptr = 100;
printf("%d %p %d %p\n", other, &other, *ptr, ptr);

// 24 0x7fff5770d46c 24 0x7fff5770d46c
//100 0x7fff5770d46c 100 0x7fff5770d46c
//100 0x7fff5770d45c 100 0x7fff5770d45c

void change(int *u, int *v)
{
printf("%p %p \n", u, v);
printf("%d %d \n", *u, *v);
int temp;
temp = *u;
*u = *v;
*v = temp;
}
1
2
3
4
5
6
7
int a = 1;
int *p = &a;
printf("%p,%d\n", p, *p);
p++;
printf("%p,%d\n", p, *p);
// 0x7fff5024344c,1
// 0x7fff50243450,1344550016
1
2
3
4
5
6
7
int days[] = {0, 1, [2] = 100};
printf("%p,%p\n", days, &days[0]);
printf("%p,%d\n", (days + 1), *(days + 1));
printf("%p,%d\n", (days + 2), *(days + 2));
//0x7fff5375b44c,0x7fff5375b44c
//0x7fff5375b450,1
//0x7fff5375b454,10
1
2
3
4
5
6
int a = 1;
int *p = &a;
printf("%p,%p\n", &p, p);
printf("%p,%p\n", *(&p), p)
// 0x7fff5ab00440,0x7fff5ab0044c
// 0x7fff5ab0044c,0x7fff5ab0044c

函数指针

如果在程序中定义了一个函数,那么在编译时系统就会这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫做函数指针变量,简称函数指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int test(){
puts("test");
return 1;
}
int main(){
printf("%p %ld\n",test, sizeof(test));
int(*p)(void) = test;
printf("%p %ld\n",p, sizeof(p));

//使用函数指针的两种方法
p();
(*p)();
}

// 0x10263dee0 1
// 0x10263dee0 8

函数指针的定义方式为: 函数返回值类型 (* 指针变量名) (函数参数列表);

函数指针也可以作为函数的形参

1
void show((* fp) (char *),char *str);

数组

数组默认使用的值是内存上相应位置现有值,若部分初始化数组,剩余的元素都会被初始化为 0。

1
2
3
4
int [] days ={}

// 计算数组的实际大小
sizeof days / sizeof days[0]

数组的指针地址为数组中首元素的指针地址,数组的申明即是其指针申明

1
2
3
4
5
6
7
8
9
10
int a[] = {1};
int *p = a;
printf("%p\n", a);
printf("%p\n", p);
printf("%d\n", *a);
printf("%d\n", *p);
// 0x7fff53fe344c
// 0x7fff53fe344c
// 1
// 1

字符串

字符串是以空字符\0结尾的 char 类型数组,可使用字符串常量,char 类型数组,指向 char 的指针来定义字符串,字符串的末尾会自动加入\0字符。字符串的指针地址即为其 char 数组的首位元素地址,*"hello"的值为h。

字符串常量属于静态存储类别(static storage class ),该字符串只会被存储一次

1
2
3
4
5
char car[10] = "Tata";
//以下表达式都为true
car == &car[0];
*car == 'T';
*(car+1) == car[1] == 'a';

初始化数组字符串是吧静态存储区的字符串拷贝到新生成的数组中,而初始化指针只把字符串的地址拷贝给指针。指针字符串不允许修改字符数组的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#define SIZE 2
#define MSG "hello"

int main(int argc, char const *argv[])
{
char m1[] = "hello";
char *m2 = "hello";
*m1 = 'f';
//*m2 = 'f'; 非法
printf("%p,%p,%p\n", &MSG, &m1, m2);
puts(MSG);
puts(m1);
puts(m2);
}
// 0x10c063fa0,0x7fff53b9c44a,0x10c063fa0
// hello
// fello
// hell

遍历字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
char *string = "hello";
int i = 0;
printf("%d %p %c\n", ++i, string, *string);
while (*string)
{
printf("%d %p %c\n", ++i, string, *string);
string++;
}

printf("%d %p %c\n", ++i, string, *string);

// 可以看到首字符和数组的指针地址是相同的,尾字符为\0,\0作为作为bool时其值等效于0
// 1 0x10d2ddf9e h
// 2 0x10d2ddf9e h
// 3 0x10d2ddf9f e
// 4 0x10d2ddfa0 l
// 5 0x10d2ddfa1 l
// 6 0x10d2ddfa2 o
// 7 0x10d2ddfa3

结构

下述结构在内存中的布局 c_struct.png

在有些系统中,一个结构的大小可能大于它各个成员大小之和。这是因为系统对数据进行校准的过程中产生了一些“缝隙”,例如,有些系统必须把每个成员都放在偶数地址上,或 4 的倍数的地址上。在这种系统中,结构内部就存在未使用的“缝隙”

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
#include <stdio.h>

struct stuff {
int number;
char code[4];
float cost;
};
int main(void)

{

// 使用结构并初始化结构,各初始化项用逗号分隔
struct stuff aname = {
10,
"hel",
3.14f
};

// 另外一种用法是符合字面量,可以提供可替换的值。也可以直接作为参数传递,
aname = (strcut stuff){
10,
"hel",
3.14f
};
// 访问结构的成员,.的优先级高于&,我们也可以看到number的指针地址和aname的指针地址是相同的
printf("%d\n",aname.number);
printf("%p %p\n",&aname,&aname.number);
// 10
// 0x7fff59eb8780 0x7fff59eb8780

//使用指针,和数组不同,结构的值不为指针地址
struct stuff *p = &aname;
//指针访问成员
printf("%d\n",p->number);
printf("%d\n",(*p).number);

//结构可以赋值给另一个结构,相当于可拷贝一份新的结构,使用不同的内存块
//当结构作为形参时,实际传递的一定是结构的副本
struct stuff p2 = *p;
p->number = 100;
p->code[0] = 'f';
printf("%d %s \n", p->number,p->code);
printf("%d %s \n", p2.number,p2.code);
// 100 fel
// 10 hel
}

结构中声明指针变量时,由于其是未经初始化的变量,地址可以是任何值,当对其进行赋值时,可能会导致程序崩溃。若要使用指针变量时,其应该只用于管理那些已经定义好的在别处分配的值。也可以使用 malloc()函数分配内存空间,并把字符串拷贝到新分配的存储空间中,该字符串并未存储在结构中,而存储在 malloc()分配的内存块中,结构中存储着两个字符串的地址,处理字符串的函数通常都要使用字符串的地址。

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct  pst{
int number;
int * pointer;
};
int main(void)

{
int a = 100;
struct pst ps ;
printf("%3d,%p,%p,%p,%p,%p\n", *ps.pointer, &ps,&ps.number,&ps.pointer, ps.pointer, &ps.pointer);
ps.pointer = &a;
ps.number = 3;
printf("%3d,%p,%p,%p,%p,%p\n", *ps.pointer, &ps,&ps.number,&ps.pointer, ps.pointer, &ps.pointer);

}

// 0,0x7fff5317b778,0x7fff5317b778,0x7fff5317b780,0x7fff5317b7a0,0x7fff5317b780
// 100,0x7fff5317b778,0x7fff5317b778,0x7fff5317b780,0x7fff5317b78c,0x7fff5317b780
// 可以看出结构ps的指针地址就是其成员变量p的指针地址。p的值是一个指针

使用 malloc()赋值字符串指针示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

struct pst{
char * pointer;
};
int main(void)

{

char *temp = "hello world";
struct pst ps;
printf("%p,%s\n",ps.pointer, ps.pointer);
ps.pointer = malloc(strlen(temp)+1);
strcpy(ps.pointer,temp);
printf("%p,%s\n",ps.pointer, ps.pointer);

}
// 0x7fff50aa67a0,
// 0x7fb5e1403150,hello world

伸缩型数组

声明伸缩型数组的规则

  • 伸缩型数组成员必须是结构的最后一个成员
  • 结构中必须至少有一个其他成员
  • 伸缩数组的生命类似于普通数组,只是它的方括号是空的
1
2
3
4
struct  flex{
int count;
double scores[];
};

声明 struct flex 类型的结构变量时,不能用 scores 做任何事,因为没有给这个数组预留任何存储空间。C99 希望你声明一个指向 struct flex 类型的指针,然后用 malloc()来分配足够的空间,以存储 struct flex 类型结构的常规内存和伸缩型数组成员所需的额外空间。

1
2
3
4
5
6
struct flex *pf;
//申请一个结构和数组分配的内存空间
pf = malloc(sizeof(struct flex)+5* sizeof(double));
pf ->count=5;
pf -> scores[2]=18.5;
printf("%d %g",pf->count,pf->scores[2]);

匿名结构

匿名结构是一个没有名称的结构成员,为了理解它的工作原理,我们先考虑如何创建嵌套结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

struct names {
char first[20];
char last[20];
};

struct person {
int id;
struct names name;
};
int main(void)
{

struct person ted = {8483,{"Ted","Grass"}};
puts(ted.name.first);

}

在 C11 中,我们用嵌套的匿名成员结构定义 person

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

struct person {
int id;
struct {
char first[20];
char last[20];
};
};
int main(void)
{
//初始化的方式相同
struct person ted = {8483,{"Ted","Grass"}};
//访问ted简化了步骤,只需把first看做是person的成员那样使用它
puts(ted.first);

}

联合

联合体(union)

  1. 联合体是一个结构体
  2. 它的所有成员相对于基地址的偏移量都为 0
  3. 次结构空间要大到足够容纳最“宽”的成员
  4. 其内存对齐方式要适合其中的所有成员

对 union 的成员进行赋值,首先会清空所有成员后再进行赋值。当以任意成员访问时,可能会涉及到类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
union hold {
int digit;
double bigf;
char letter;
//匿名联合
union {
int inner;
};
}

//初始化
union hold a;
a.letter = 'R';
union hold b = a;
union hold c = {88};;
union hold d = {.bigf = 118.2};

//使用
c.letter='H';

可以去定义 union 数组,这样各个元素就可以使用不同的类型

枚举

enum 常量是 int 类型,只要能使用 int 类型的地方就可以使用枚举类型,c 枚举的一些特性并不适用于 c++。例如 c 允许枚举变量使用++运算符

1
2
3
4
5
6
7
8
enum  spectrum {
red,orange,yello,green,blue,violet
};
int main(){

enum spectrum color = violet;
printf("%d",color);
}

枚举常量可指定整数值,后面没有进行赋值操作的常量会自动赋予后续的值。

1
enum levels {low=100,medium=500,high}

输入输出

重定向

c 语言的输入输出与 linux 和 unix 的输入输出重定向是一致的,也可以使用 here document 的方式输入

1
2
3
4
5
6
7
example < hello.txt
example > hello.txt
example >> hello.txt
example << eof
>hello
>world
>eof

printf

常用的输出控制符主要有以下几个: |控制符 |说明| |:--|:--| |%a| 浮点数、十六进制| |%c| 用来输出一个字符。| |%d| 按十进制整型数据的实际长度输出。| |%e| 浮点数 e 记数法。| |%f| 用来输出实数,包括单精度和双精度,以小数形式输出。不指定字段宽度,由系统自动指定,整数部分全部输出,小数部分输出 6 位,超过 6 位的四舍五入。| |%g| 根据值的不同,自动选择%f 或%e| |%o| 以八进制整数形式输出,这个就用得很少了,了解一下就行了。| |%p| 指针| |%s| 用来输出字符串。用 %s 输出字符串同前面直接输出字符串是一样的。但是此时要先定义字符数组或字符指针存储或指向字符串,这个稍后再讲。| |%u| 输出无符号整型(unsigned)。输出无符号整型时也可以用 %d,这时是将无符号转换成有符号数,然后输出。但编程的时候最好不要这么写,因为这样要进行一次转换,使 CPU 多做一次无用功。| |%x| (或 %X 或 %#x 或 %#X)以十六进制形式输出整数,这个很重要。| |%ld| 输出长整型数据。| |%md| m 为指定的输出字段的宽度。如果数据的位数小于 m,则左端补以空格,若大于 m,则按实际位数输出。| |%.mf| 输出实数时小数点后保留 m 位,注意 m 前面有个点。|

常用函数

puts

只显示字符串,且自动在实现的字符串末尾加上换行符

fputs

将字符串输入到指定文件中

sizeof

sizeof 是一个操作符(operator)。其作用是返回一个对象或类型所占的内存字节数。 sizeof 有三种语法形式:

  1. sizeof (object); //sizeof (对象)
  2. sizeof object; //sizeof 对象
  3. sizeof (type_name); //sizeof (类型)

sizeof 对对象求内存大小,最终都是转换为对对象的数据类型进行求值。

  1. 基本数据类型的 sizeof 这里的基本数据类型是指 short、int、long、float、double 这样的简单内置数据类型。由于它们的内存大小是和系统相关的,所以在不同的系统下取值可能不同。
  2. 结构体的 sizeof 结构体的 sizeof 涉及到字体对齐问题。为什么需要字节对齐,计算机组成原理教导我们有助于加快计算机的取数速度,否则就得多花指令周期了。为此,编译器默认会对结构体进行处理(实际上其他地方的数据变量也是如此)。让宽度为 2 的基本数据类型(short 等)都位于能被 2 整除的地址上,让宽度为 4 的基本数据类型(int 等)都位于能被 4 整除的地址上,依次类推。这样,两个数中间就可能需要加入填充字节,所有整个结构体的 sizeof 值就增长了。

    字节对齐的细节和编译器的实现相关,但一般而言,满足三个准则:

    1. 结构体变量的首地址能够被其最宽基本类型成员的大小锁整除
    2. 结构体的每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要,编译器会在成员之间加上填充字节(internal adding)。
    3. 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员后加上填充字节。

    注意:空结构体(不含数据成员)的 sizeof 值为 1,空结构体分配一个字节的空间

  3. 联合体的 sizeof 联合体是重叠式,各成员共享一段内存;所以整个联合体的 sizeof 也就是每个成员 sizeof 的最大值
  4. 数组的 sizeof 数组的 sizeof 值等于数组所占用的内存字节数 注意:
    1. 当字符串数组表示字符串时,其 sizeof 值将\0计算进去
    2. 当数组为形参时,其 sizeof 值相当于指针的 sizeof 值
  5. 指针的 sizeof 指针是用来记录另一对象的地址,所以指针的内存大小当然就等于计算机内部地址总线的宽度,在 64 位计算机中,一个指针变量的返回值必定是 8,指针变量的 sizeof 与指针所指的对象没有任何关系。
  6. 函数的 sizeof sizeof 也可对一个函数调用求值,其结果是函数返回值类型的大小,函数并不会被调用。 对函数求值的形式:sizeof(函数名(实参表))

    注意:

    1. 不可以对返回值类型为空的函数求值。
    2. 不可以对函数名求值。
    3. 对有参数的函数,在用 sizeof 时,须写上实参表。

memcpy 和 memmove

从 s2 向指向的位置拷贝 n 字节到 s1 指向的位置,返回 s1 的值

1
2
void *memcpy(void * restrict s1,const void * restrict s2, size_t n);
void *memmove(void * s1,void * s2, size_t n);

可变参数:stdarg.h

stdarg.h 头文件为函数提供了一个可变参数的功能,但是用法比较复杂,必须按照如下的格式

  1. 提供一个使用省略号的函数原型,这种函数的原型应该有一个形参列表,其中至少一个形参,和一个在最后位置上的省略号。最右边的形参(即省略号前一个形参)起着特殊的作用。标准中用 parmN 来描述该形参
  2. 在函数定义中创建一个 va_list 类型的变量
  3. 用宏把该变量初始化为一个参数列表
  4. 用宏访问参数列表
  5. 用宏完成清理工作
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
#include <stdio.h>
#include <stdarg.h>

double sum(int, ...);

int main() {

double s = sum(4,1.0,2.0,3.0,4.0);
printf("%g\n",s);
}

double sum(int lim, ...) {
va_list ap; //声明一个对象存储参数
double total=0;
int i;

va_start(ap, lim); //把ap初始化为参数列表
for (i = 0; i < lim; i++) {
total += va_arg(ap, double); //访问参数列表的每一项,类型需要严格满足
printf("%g\n",total);

}
va_end(ap);

return total;
}

make

发表于 2020-09-14 更新于 2020-12-02

可参考博客的详细解释一起写 Makefile

概述

基本规则

1
2
3
4
target ... : prerequisites ...
command
...
...
  • target 可以是一个 object file(目标文件),也可以是一个执行文件,还可以是一个标签(label)。即:后面什么也没有,make 就不会自动查找它的依赖性,也不会执行其定义的命令,可以通过make <label>的方式手动执行。makefile 的第一个 target 会被默认执行。
  • prerequisites 生成该 target 所依赖的文件和/或 target。如果 prerequisites 文件的日期要比 targets 文件的日期要新,或者 target 不存在的话,那么,make 就会执行后续定义的命令。 make 会一层一层的去找文件的依赖关系,直到最终编译出第一个目标文件。
  • command 该 target 要执行的命令(任意的 shell 命令),要以一个 Tab 键作为开头,,命令可以为多行,每行命令在独立的进程中执行,不会共享变量,
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

#可以定义变量
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

edit : $(objects)
cc -o edit $(objects)
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c

#标记clean为伪目标
.PHONY : clean
#伪目标,可以通过make clean执行
clean :
rm edit $(objects)

伪目标一般没有依赖文件,但是也可以为伪目标制定锁依赖的文件。伪目标同样也可以作为“默认目标”,只要将其放在第一个。伪目标只是一个标签不会生成文件,它总是会被执行

1
2
3
4
5
6
7
8
9
10
11
all:prog1 prog2 prog3
.PHONY: all

prog1: prog1.o utils.o
cc -o prog1 prog1.o utils.o

prog2: prog2.o
cc -o prog2 prog2.o

prog3: prog3.o sort.o utils.o
cc -o prog3 prog3.o sort.o utils.o

参数

  • -j 并行编译

libvpx

1
2
3
4
./configure --enable-pic --enable-static --enable-shared --as=yasm --target=generic-gnu
# 清空编译临时文件目录
make clean
make

gcc 版本

Centos7 gcc 版本默认 4.8.3,Red Hat 为了软件的稳定和版本支持,yum 上版本也是 4.8.3,所以无法使用 yum 进行软件更新,所以使用 scl。

scl 软件集(Software Collections),是为了给 RHEL/CentOS 用户提供一种以方便、安全地安装和使用应用程序和运行时环境的多个(而且可能是更新的)版本的方式,同时避免把系统搞乱。

使用 scl 升级 gcc 步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 安装scl源:
yum install centos-release-scl scl-utils-build
# 列出scl有哪些源可以用
yum list all --enablerepo='centos-sclo-rh'
# 安装5.3版本的gcc、gcc-c++、gdb
yum install devtoolset-4-gcc.x86_64 devtoolset-4-gcc-c++.x86_64 devtoolset-4-gcc-gdb-plugin.x86_64
# .查看从 SCL 中安装的包的列表
scl --list 或 scl -l
# 查看版本
gcc -v
# 切换版本 临时指定,退出bash后失效
# 使用scl创建一个scl包的bash会话环境
scl enable devtoolset-4 bash

elasticsearch

发表于 2020-09-10 更新于 2020-12-02 分类于 db

查询语法,

  • query parameter
  • request body
  • response body

使用 query parameter 实现模糊搜索

1
2
3
4
5
# 可以模糊搜索text标签中带hello的字符
# q 搜索
# size 默认搜索10条
# order 排序 desc 反向排序
GET /_search?q=text:*hello*&size=100&order=nanotime:desc

wildcard

wildcard 通配符基于分词器分词后的短语进行匹配的,详细说明文档

分词器

测试分词效果

1
2
3
4
5
POST _analyze
{
"analyzer": "pattern",
"text": "The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
}

内置分词器

postgres

发表于 2020-09-09 更新于 2020-12-02 分类于 db

省略 linux 安装过程,安装后一般会创建一个 postgres 用户,该用户具有数据库超级用户权限。

1
2
3
4
5
~$ psql
psql(9.5.17)
Type "help" for help.

postgres=#

也可以使用命令行登录

1
psql -h localhost -p 5432 -U postgress myBd

在 postgres cli 操作界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 退出postgres
\q

# 查看所有数据库
\l

# c + 数据库 进入数据库
\c myDB


# 显示所有表
\d

# 显示表信息
\d tableName

sql 语句,在 postgres cli 操作界面写 sql 语句,需要以;分割,可以用回车将 sql 分多行来写

sip

发表于 2020-09-04 更新于 2020-12-02 分类于 ivr

SIP 是一个对等的协议,类似 P2P。它可以在不需要服务器的情况下进行通信,只要通信双方都彼此指导对方的地址(或者只有一方知道另一方的地址)即可,这种情况称为点对点通信。详细标准可查看RFC3261

SIP 协议采用 Client/Server 模型,每一个请求(request),Server 从接受到请求到处理完毕,要回复多个临时响应,和有且仅有一个终结响应(response)。

概念

  1. Transaction 请求和所有的响应构成一个事务,一个完整的呼叫过程包括多个事件。

  2. UA 用户代理,是发起或接受呼叫的逻辑实体

  3. UAC 用户代理客户端,用于发起请求

  4. UAS 用户代理服务器,用于接受请求

  5. UAC 和 UAS 的划分是针对一个事务的,在一个呼叫的多个事务中,UAC 和 UAS 的角色是可以互相转换的

  6. B2BUA 是一个 SIP 中逻辑上的网络组件,用于操作不同会话的端点,它将 channel 划分为两路通话,在不同会话的端点直接通信。例如,当建立一通呼叫时,B2BUA 作为一个 UAS 接受所有用户请求,处理后以 UAC 角色转发至目标端。 freeswitch_B2BUA.png

SIP URI

假设 Bob 在服务器 192.168.1.100,Alice 在 192.168.1.200 上,FreeSwitch 在 192.168.1.9 上,Alice 注册到 FreeSwitch 上,Bob 呼叫 Alice 时,使用 Alice 的服务器地(又称逻辑地址)(Bob 只知道服务器地址),即 sip:Alice@192.168.1.9,FreeSwitch 接受到请求后,查找本地数据库,发现 Alice 的实际地址(Contact 地址,又叫联系地址,亦称物理地址)是 sip:Alice@192.168.1.100,便可以建立呼叫。Bob 作为主叫方,它已经知道服务器地址,可以直接发送 INVITE 请求,是不需要注册的,而 Alice 作为被叫的一方,为了让服务器能找到它,它必须事先通过 REGISTER 消息注册到服务器上。

媒体

音频编码

从模拟信号变成数字信号的过程称模数转换(Analog Digital Convert ,AD)。AD 转换要经过采样、量化、编码三个过程。编码就是按照一定的规则将采样所得的信号用一组二进制或者其他进制的数来表示。经过编码后的数据便于在网络上传输,到达对端后,在通过解码过程变成原始信号,进而经过数模转换(DA)在恢复为模拟量,即转换为人们能够感知的信号。一般来说,编码与解码都是成对出现的,称为编解码(codec),一般简称编码。

音频编码最基本的两个技术参数就是采样率和打包周期。

  • 采样率 采样频率越高,声音就越清晰,保留的细节就越多。对于普通的人声通话来说,8000Hz 就够了。
  • 打包周期 打包周期和传输有关,打包周期越短,延迟越小,相对而言传输开销就会越多。大部分编码都支持多种打包周期,常见的打包周期为 20ms。

FreeSwitch 支持的语音编码,

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
freeswitch@CentOS7> show codec
type,name,ikey
#编码名称 参数 具体实现模块
codec,ADPCM (IMA),mod_spandsp
codec,AMR / Bandwidth Efficient,mod_amr
codec,AMR / Octet Aligned,mod_amr
codec,G.711 alaw,CORE_PCM_MODULE
codec,G.711 ulaw,CORE_PCM_MODULE
codec,G.722,mod_spandsp
codec,G.723.1 6.3k,mod_g723_1
codec,G.726 16k,mod_spandsp
codec,G.726 16k (AAL2),mod_spandsp
codec,G.726 24k,mod_spandsp
codec,G.726 24k (AAL2),mod_spandsp
codec,G.726 32k,mod_spandsp
codec,G.726 32k (AAL2),mod_spandsp
codec,G.726 40k,mod_spandsp
codec,G.726 40k (AAL2),mod_spandsp
codec,G.729,mod_g729
codec,GSM,mod_spandsp
codec,LPC-10,mod_spandsp
codec,PROXY PASS-THROUGH,CORE_PCM_MODULE
codec,PROXY VIDEO PASS-THROUGH,CORE_PCM_MODULE
codec,RAW Signed Linear (16 bit),CORE_PCM_MODULE
codec,Speex,CORE_SPEEX_MODULE
codec,VP8 Video,CORE_VPX_MODULE
codec,VP9 Video,CORE_VPX_MODULE

更多编码可参考wiki

当我们重新加载模块时,可以看到相关音频编码的加载过程

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
freeswitch@CentOS7> reload mod_g723_1
+OK Reloading XML
+OK module unloaded
+OK module loaded

2020-09-07 16:33:34.525691 [ERR] switch_stun.c:900 STUN Failed! [Timeout]
2020-09-07 16:33:34.525691 [ERR] switch_xml.c:175 stun-set failed.
2020-09-07 16:33:44.566137 [ERR] switch_stun.c:900 STUN Failed! [Timeout]
2020-09-07 16:33:44.566137 [ERR] switch_xml.c:175 stun-set failed.
2020-09-07 16:33:44.585289 [ERR] switch_xml.c:1370 Error including /etc/freeswitch/lang/de/*.xml
2020-09-07 16:33:44.585289 [ERR] switch_xml.c:1370 Error including /etc/freeswitch/lang/fr/*.xml
2020-09-07 16:33:44.585289 [ERR] switch_xml.c:1370 Error including /etc/freeswitch/lang/ru/*.xml
2020-09-07 16:33:44.585289 [ERR] switch_xml.c:1370 Error including /etc/freeswitch/lang/he/*.xml
2020-09-07 16:33:44.585289 [INFO] switch_xml.c:1373 No files to include at /etc/freeswitch/lang/es/es_ES.xml
2020-09-07 16:33:44.585289 [INFO] switch_xml.c:1373 No files to include at /etc/freeswitch/lang/pt/pt_BR.xml
2020-09-07 16:33:44.605574 [NOTICE] switch_loadable_module.c:1186 Deleting Codec G723 4 G.723.1 6.3k 8000hz 120ms
2020-09-07 16:33:44.605574 [INFO] mod_enum.c:884 ENUM Reloaded
2020-09-07 16:33:44.605574 [NOTICE] switch_loadable_module.c:1186 Deleting Codec G723 4 G.723.1 6.3k 8000hz 90ms
2020-09-07 16:33:44.605574 [NOTICE] switch_loadable_module.c:1186 Deleting Codec G723 4 G.723.1 6.3k 8000hz 60ms
2020-09-07 16:33:44.605574 [NOTICE] switch_loadable_module.c:1186 Deleting Codec G723 4 G.723.1 6.3k 8000hz 30ms
2020-09-07 16:33:44.605574 [CONSOLE] switch_loadable_module.c:2399 mod_g723_1 has no shutdown routine
2020-09-07 16:33:44.605574 [CONSOLE] switch_loadable_module.c:2416 mod_g723_1 unloaded.
2020-09-07 16:33:44.605574 [CONSOLE] switch_loadable_module.c:1803 Successfully Loaded [mod_g723_1]
2020-09-07 16:33:44.605574 [NOTICE] switch_loadable_module.c:241 Adding Codec G723 4 G.723.1 6.3k 8000hz 120ms 1ch 6300bps
2020-09-07 16:33:44.605574 [NOTICE] switch_loadable_module.c:241 Adding Codec G723 4 G.723.1 6.3k 8000hz 90ms 1ch 6300bps
2020-09-07 16:33:44.605574 [NOTICE] switch_loadable_module.c:241 Adding Codec G723 4 G.723.1 6.3k 8000hz 60ms 1ch 6300bps
2020-09-07 16:33:44.605574 [NOTICE] switch_loadable_module.c:241 Adding Codec G723 4 G.723.1 6.3k 8000hz 30ms 1ch 6300bps
2020-09-07 16:33:44.605574 [INFO] switch_time.c:1430 Timezone reloaded 1750 definitions

媒体协商

不同的 SIP 终端有不同的特性,支持不同的语音编码。所以不同的 SIP 终端进行通信时需要先与支持的编码进行“协商”,以便双方互相能够理解对方发来的媒体流中的数据。 我们来看一个最简单的编码协商过程。在 FreeSwitch 中我们将日志级别调整为

1
2
3
# 或者按快捷键F8
freeswitch@CentOS7> /log 7
+OK log level 7 [7]

在 log 中我们可以看到如下的协商过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Audio Codec Compare [opus:113:48000:20:0:2]/[G722:9:8000:20:64000:1]
Audio Codec Compare [opus:113:48000:20:0:2]/[PCMU:0:8000:20:64000:1]
Audio Codec Compare [opus:113:48000:20:0:2]/[PCMA:8:8000:20:64000:1]
Audio Codec Compare [G722:9:8000:20:64000:1]/[G722:9:8000:20:64000:1]
Audio Codec Compare [G722:9:8000:20:64000:1] ++++ is saved as a match
Audio Codec Compare [G722:9:8000:20:64000:1]/[PCMU:0:8000:20:64000:1]
Audio Codec Compare [G722:9:8000:20:64000:1]/[PCMA:8:8000:20:64000:1]
Audio Codec Compare [PCMU:0:8000:20:64000:1]/[G722:9:8000:20:64000:1]
Audio Codec Compare [PCMU:0:8000:20:64000:1]/[PCMU:0:8000:20:64000:1]
Audio Codec Compare [PCMU:0:8000:20:64000:1] ++++ is saved as a match
Audio Codec Compare [PCMU:0:8000:20:64000:1]/[PCMA:8:8000:20:64000:1]
Audio Codec Compare [PCMA:8:8000:20:64000:1]/[G722:9:8000:20:64000:1]
Audio Codec Compare [PCMA:8:8000:20:64000:1]/[PCMU:0:8000:20:64000:1]
Audio Codec Compare [PCMA:8:8000:20:64000:1]/[PCMA:8:8000:20:64000:1]
Audio Codec Compare [PCMA:8:8000:20:64000:1] ++++ is saved as a match
Set telephone-event payload to 101@8000
Set Codec sofia/internal/1001@10.211.55.6 G722/8000 20 ms 160 samples 64000 bits 1 channels
sofia/internal/1001@10.211.55.6 Original read codec set to G722:9

SIP 采用 Off/Anwser(请求/应答)机制来协商。请求发起的一方提供(Offer)自己支持的媒体编码列表,被请求的一方比较自己支持的媒体列表最终选择一种(或几种)编码以应答(Anwser)方式通知请求者,然后他们就可以使用兼容的编码进行通信了。上述 Log,最终可以看到,他也是将客户端与服务器的编码进行逐一比较,最后Set Codec sofia/internal/1001@10.211.55.6 G722/8000 20 ms 160 samples 64000 bits 1 channels,表示本次协商成功并把该 Channel 的编码设置为 G722 编码

我们通过拨打一通电话,抓取 tcpdump 报文,详细查看一下相关的 SIP 信令

我们查看一个 INVITE 请求(删减部分信息)

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
INVITE sip:5000@10.211.55.6 SIP/2.0
From: "centos7" <sip:1001@10.211.55.6>;tag=8.AL-QAaB3pR6AtISmelTqJqewW4zcKh
To: <sip:5000@10.211.55.6>
User-Agent: Blink Lite 4.6.0 (MacOSX)
Content-Type: application/sdp
Content-Length: 427

v=0
o=- 3808463179 3808463179 IN IP4 10.211.55.2
s=Blink Lite 4.6.0 (MacOSX)
t=0 0
# audio表示音频数据 50008表示端口号,该端口用于收发RTP数据 RTP/AVP表示RTP格式,9 0 8 表示音频编码的IANA的代码 G722 PCMU PCMA
m=audio 50008 RTP/AVP 113 9 0 8 101
# 媒体所在机器的IP地址
c=IN IP4 10.211.55.2
a=rtcp:50009
a=rtpmap:113 opus/48000/2
a=fmtp:113 useinbandfec=1
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:101 telephone-event/8000
a=fmtp:101 0-16
a=zrtp-hash:1.10 5049b4964ef2ea5a3f01fb61c8c264ec564df6bcdef57d74d26548abf58f19e3
# sendrecv表示双向收发 其他的还有sendonly,recvonly,inactive
a=sendrecv

当 Freeswitch 收到请求后,即启动协商过程。根据前面的 Log 所以,服务器提供 OPUS,G722,PCMU,PCMA 等。因此当比较到 G7222 时协商成功,FreeSwitch 返回如下 SIP 消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SIP/2.0 200 OK
From: "centos7" <sip:1001@10.211.55.6>;tag=8.AL-QAaB3pR6AtISmelTqJqewW4zcKh
To: <sip:5000@10.211.55.6>;tag=F0mU0rHp6gUHS
Accept: application/sdp
Content-Type: application/sdp
Content-Disposition: session
Content-Length: 251

v=0
o=FreeSWITCH 1599452646 1599452647 IN IP4 10.211.55.6
s=FreeSWITCH
c=IN IP4 10.211.55.6
t=0 0
# FreeSwitch这端的RTP端口号是21734,使用G7222编码
m=audio 21734 RTP/AVP 9 101
a=rtpmap:9 G722/8000
a=rtpmap:101 telephone-event/8000
a=fmtp:101 0-16
a=ptime:20
a=rtcp:21735 IN IP4 10.211.55.6
pidf:time-offset

协商过程完毕,双方互相知道了对方的 IP 地址和端口好,就可以互发音频 RTP 包了。

FreeSwitch 协商策略

  • generous 优先采用客户端的编码
  • greedy 优先选择服务端即 FreeSwitch 的编码
  • scrooge 优先选择服务端即 FreeSwitch 的编码并强制使用自己的采样率

FreeSwitch 转码

FreeSwitch 作为一个 B2BUA,因而在桥接两条腿时,如果两条腿分别使用不同的编码,则需要经过一个转码过程分别转成对方需要的编码,当需要转码时,FreeSwitch 回将收到的音频数据转换成一种中间格式,称为 L16,即线性 16 位的编码,这种格式可以和其他各种编码进行转换。

FreeSwitch 其他

  • 透传 指在不经过转码的情况下,将从一方收到的媒体流原样转给另一方
  • 媒体绕过 媒体绕过技术,即真正的媒体流使用点到点传输,根本不经过 freeswitch
  • 媒体代理 即不管 freeswitch 是否支持对该种编码转码,他都对 rtp 数据在不进行任何处理的情况下发送给另一方,与透传的区别是:他只改变 sdp 中的“c=”部分。
  • 媒体 Bug,用作监听,检测等
  • 使用 uuid_debug_media 拍错,查看日志Audio Codec Compare相关行

SDP

SIP 负责建立和释放会话,一般来说,会话会包含相关的媒体,如视频和音频。媒体数据是由 SDP(Session Description Protocol,会话描述协议)描述的,SDP 一般不单独使用,它与 SIP 配合使用时会放到 SIP 协议的 Boby(正文)中。会话建立时,需要媒体协商,双方才能确定对方的媒体能力以交互媒体数据,比如确认支持的数据格式。 SDP 的特点

  • 是一个结构化的文本协议
  • 表述 session 的媒体,协议,编码译码格式等
  • 通常的 SDP 消息格式为 type=parameter1 parameter2 ... parameterN

SDP 参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# *表示可选参数


# 协议版本号
v= (protocol version)

#username(用户名) sess-id(会话ID) sess-version(会话版本号) nettype(网络类型) addrtype(地址类型) unicast-address(单播地址)
o= (owner/creator and session identifier)
s= (session name)
i=* (session information)
u=* (URI of description)
e=* (email address)
p=* (phone number)
#  网络类型 网络地址 (RTP数据流发送到该地址)
c=* (connection information - not required if included in all media)
# 带宽类型
b=* (bandwidth information)
z=* (time zone adjustments)
k=* (encryption key)
t=start-time stop-time
# 媒体类型 音频端口号 传输协议 支持的codec类型
m= media port transport format-list
# 描述上面音频的属性,一般m后面跟多个a
a=* (zero or more session attribute lines)

支持的 codec 类型,在 SDP 中使用数字表示,常见的如下

数字 编码
8 PCMA

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
v = 0
o = mhandley2890844526 2890842807 IN IP4 126.16.64.4
s = SDP Seminar
i = A Seminar on the session description protocol
u = http://www.cs.ucl.ac.uk/staff/M.Handley/sdp.03.ps
e = mjh@isi.edu(Mark Handley)
c = IN IP4 224.2.17.12/127
t = 2873397496 2873404696
a = recvonly
m = audio 49170 RTP/AVP 0
m = video 51372 RTP/AVP 31
m = application 32416udp wb
a = orient:portrait

一个标准的呼叫过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Alice                     Bob
| |
| INVITE F1 |
|----------------------->|
| 180 Ringing F2 |
|<-----------------------|
| |
| 200 OK F3 |
|<-----------------------|
| ACK F4 |
|----------------------->|
| Both Way RTP Media |
|<======================>|
| |
| BYE F5 |
|<-----------------------|
| 200 OK F6 |
|----------------------->|
| |

其 sip 报文大致如下:

  1. F1 INVITE Alice -> Bob

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    INVITE sip:bob@biloxi.example.com SIP/2.0
    Via: SIP/2.0/TCP client.atlanta.example.com:5060;branch=z9hG4bK74bf9
    Max-Forwards: 70
    From: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
    To: Bob <sip:bob@biloxi.example.com>
    Call-ID: 3848276298220188511@atlanta.example.com
    CSeq: 1 INVITE
    Contact: <sip:alice@client.atlanta.example.com;transport=tcp>
    Content-Type: application/sdp
    Content-Length: 151

    v=0
    o=alice 2890844526 2890844526 IN IP4 client.atlanta.example.com
    s=-
    c=IN IP4 192.0.2.101
    t=0 0
    m=audio 49172 RTP/AVP 0
    a=rtpmap:0 PCMU/8000
  2. F2 180 Ringing Bob -> Alice

    1
    2
    3
    4
    5
    6
    7
    8
    9
    SIP/2.0 180 Ringing
    Via: SIP/2.0/TCP client.atlanta.example.com:5060;branch=z9hG4bK74bf9
    ;received=192.0.2.101
    From: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
    To: Bob <sip:bob@biloxi.example.com>;tag=8321234356
    Call-ID: 3848276298220188511@atlanta.example.com
    CSeq: 1 INVITE
    Contact: <sip:bob@client.biloxi.example.com;transport=tcp>
    Content-Length: 0
  3. F3 200 OK Bob -> Alice

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    SIP/2.0 200 OK
    Via: SIP/2.0/TCP client.atlanta.example.com:5060;branch=z9hG4bK74bf9
    ;received=192.0.2.101
    From: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
    To: Bob <sip:bob@biloxi.example.com>;tag=8321234356
    Call-ID: 3848276298220188511@atlanta.example.com
    CSeq: 1 INVITE
    Contact: <sip:bob@client.biloxi.example.com;transport=tcp>
    Content-Type: application/sdp
    Content-Length: 147

    v=0
    o=bob 2890844527 2890844527 IN IP4 client.biloxi.example.com
    s=-
    c=IN IP4 192.0.2.201
    t=0 0
    m=audio 3456 RTP/AVP 0
    a=rtpmap:0 PCMU/8000
  4. F4 ACK Alice -> Bob

    1
    2
    3
    4
    5
    6
    7
    8
    ACK sip:bob@client.biloxi.example.com SIP/2.0
    Via: SIP/2.0/TCP client.atlanta.example.com:5060;branch=z9hG4bK74bd5
    Max-Forwards: 70
    From: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
    To: Bob <sip:bob@biloxi.example.com>;tag=8321234356
    Call-ID: 3848276298220188511@atlanta.example.com
    CSeq: 1 ACK
    Content-Length: 0
  5. 省略通话中的 RTP 媒体信息,通话过程中一般不再有 SIP 消息交互,所有的语音数据都是在 RTF 中传送。

  6. F5 BYE Bob -> Alice

    1
    2
    3
    4
    5
    6
    7
    8
    BYE sip:alice@client.atlanta.example.com SIP/2.0
    Via: SIP/2.0/TCP client.biloxi.example.com:5060;branch=z9hG4bKnashds7
    Max-Forwards: 70
    From: Bob <sip:bob@biloxi.example.com>;tag=8321234356
    To: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
    Call-ID: 3848276298220188511@atlanta.example.com
    CSeq: 1 BYE
    Content-Length: 0
  7. F6 200 OK Alice -> Bob

    1
    2
    3
    4
    5
    6
    7
    8
    SIP/2.0 200 OK
    Via: SIP/2.0/TCP client.biloxi.example.com:5060;branch=z9hG4bKnashds7
    ;received=192.0.2.201
    From: Bob <sip:bob@biloxi.example.com>;tag=8321234356
    To: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
    Call-ID: 3848276298220188511@atlanta.example.com
    CSeq: 1 BYE
    Content-Length: 0

基本方法

REGISTER

通常的注册流程是 Bob 向 FreeSwitch 发起注册(REGISTER)请求,FreeSwitch 返回 Challenge,Bob 将自己的用户名与密码和 Challenge 进行计算,并将计算结果加密附加到下一次 REGISTER 请求上,重新发起注册。FreeSwitch 收到后对本地数据库中保存的 Alice 信息使用同样的算法进行计算和加密,与 Bob 发送的计算结果进行比对,如果相同,则认证通过。其交互流程如下

1
2
3
4
5
6
7
8
9
10
11
Bob                          FreeSwitch
| |
| REGISTER F1 |
|------------------------------>|
| 401 Unauthorized F2 |
|<------------------------------|
| REGISTER F3 |
|------------------------------>|
| 200 OK F4 |
|<------------------------------|
| |

使用 tcpdump 抓取报文,可以观察到具体细节

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
16:15:09.171545 IP 10.211.55.2.56752 > 10.211.55.6.5060: SIP: REGISTER sip:10.211.55.6 SIP/2.0
E.."+E..@...
.7.
.7........REGISTER sip:10.211.55.6 SIP/2.0
Via: SIP/2.0/UDP 172.20.10.4:56752;rport;branch=z9hG4bKPjinGmFmdA2qA-3UgRFazZJ5pVtLjaVXxy
Max-Forwards: 70
From: "centos7" <sip:1001@10.211.55.6>;tag=in4araCghyxgB69gIFzqY.IPfBSKRrRG
To: "centos7" <sip:1001@10.211.55.6>
Contact: <sip:38201945@10.211.55.2:56752>;+sip.instance="<urn:uuid:ee04c296-3dfe-4cf0-9402-6df75240f128>"
Call-ID: .vPy3ECALSPa-xVP9cB0XYcP.iwHjbV2
CSeq: 1 REGISTER
Expires: 600
Supported: gruu
User-Agent: Blink Lite 4.6.0 (MacOSX)
Content-Length: 0

................
16:15:09.173451 IP 10.211.55.6.5060 > 10.211.55.2.56752: SIP: SIP/2.0 401 Unauthorized
E.......@...
.7.
.7........nSIP/2.0 401 Unauthorized
Via: SIP/2.0/UDP 172.20.10.4:56752;rport=56752;branch=z9hG4bKPjinGmFmdA2qA-3UgRFazZJ5pVtLjaVXxy;received=10.211.55.2
From: "centos7" <sip:1001@10.211.55.6>;tag=in4araCghyxgB69gIFzqY.IPfBSKRrRG
To: "centos7" <sip:1001@10.211.55.6>;tag=XNFFr99m762FQ
Call-ID: .vPy3ECALSPa-xVP9cB0XYcP.iwHjbV2
CSeq: 1 REGISTER
User-Agent: FreeSWITCH-mod_sofia/1.10.3-release.5~64bit
Allow: INVITE, ACK, BYE, CANCEL, OPTIONS, MESSAGE, INFO, UPDATE, REGISTER, REFER, NOTIFY, PUBLISH, SUBSCRIBE
Supported: timer, path, replaces
WWW-Authenticate: Digest realm="10.211.55.6", nonce="93c5a8f6-7fe8-42ba-b54c-c72c610f158d", algorithm=MD5, qop="auth"
Content-Length: 0

................
16:15:09.173748 IP 10.211.55.2.56752 > 10.211.55.6.5060: SIP: REGISTER sip:10.211.55.6 SIP/2.0
E..!....@.jP
.7.
.7.......D.REGISTER sip:10.211.55.6 SIP/2.0
Via: SIP/2.0/UDP 172.20.10.4:56752;rport;branch=z9hG4bKPjBzzTttNMXs3zHzq2034G2LLu4LSJf6iD
Max-Forwards: 70
From: "centos7" <sip:1001@10.211.55.6>;tag=in4araCghyxgB69gIFzqY.IPfBSKRrRG
To: "centos7" <sip:1001@10.211.55.6>
Contact: <sip:38201945@10.211.55.2:56752>;+sip.instance="<urn:uuid:ee04c296-3dfe-4cf0-9402-6df75240f128>"
Call-ID: .vPy3ECALSPa-xVP9cB0XYcP.iwHjbV2
CSeq: 2 REGISTER
Expires: 600
Supported: gruu
User-Agent: Blink Lite 4.6.0 (MacOSX)
Authorization: Digest username="1001", realm="10.211.55.6", nonce="93c5a8f6-7fe8-42ba-b54c-c72c610f158d", uri="sip:10.211.55.6", response="31d6b0f15f495b45f8d0e5abbd55dd65", algorithm=MD5, cnonce="WbsQ8hhdcIr1uEeN2aOlLx6GhabkDdVx", qop=auth, nc=00000001
Content-Length: 0

................
16:15:09.173995 IP 10.211.55.2.56752 > 10.211.55.6.5060: SIP: SUBSCRIBE sip:1001@10.211.55.6 SIP/2.0
E....K..@..C
.7.
.7.......a.SUBSCRIBE sip:1001@10.211.55.6 SIP/2.0
Via: SIP/2.0/UDP 172.20.10.4:56752;rport;branch=z9hG4bKPjCaDuWA9sZj4vsxyWPDY0eYl0Thvuxz4s
Max-Forwards: 70
From: "centos7" <sip:1001@10.211.55.6>;tag=SLN1U96goUyi.DJ6uBNbYf5Diwjuqe2A
To: <sip:1001@10.211.55.6>
Contact: <sip:38201945@10.211.55.2:56752>
Call-ID: emtkXsRqnqqJ4DvYB6G8wxSUyYELlxC6
CSeq: 2381 SUBSCRIBE
Event: message-summary
Expires: 600
Supported: 100rel, replaces, norefersub, gruu
Accept: application/simple-message-summary
Allow-Events: conference, message-summary, dialog, presence, presence.winfo, xcap-diff, dialog.winfo, refer
User-Agent: Blink Lite 4.6.0 (MacOSX)
Content-Length: 0

................
16:15:09.175484 IP 10.211.55.6.5060 > 10.211.55.2.56752: SIP: SIP/2.0 200 OK
E.......@...
.7.
.7........ISIP/2.0 200 OK
Via: SIP/2.0/UDP 172.20.10.4:56752;rport=56752;branch=z9hG4bKPjBzzTttNMXs3zHzq2034G2LLu4LSJf6iD;received=10.211.55.2
From: "centos7" <sip:1001@10.211.55.6>;tag=in4araCghyxgB69gIFzqY.IPfBSKRrRG
To: "centos7" <sip:1001@10.211.55.6>;tag=yy87S4tr4FS2j
Call-ID: .vPy3ECALSPa-xVP9cB0XYcP.iwHjbV2
CSeq: 2 REGISTER
Contact: <sip:38201945@10.211.55.2:56752>;expires=600
Date: Fri, 04 Sep 2020 08:15:09 GMT
User-Agent: FreeSWITCH-mod_sofia/1.10.3-release.5~64bit
Allow: INVITE, ACK, BYE, CANCEL, OPTIONS, MESSAGE, INFO, UPDATE, REGISTER, REFER, NOTIFY, PUBLISH, SUBSCRIBE
Supported: timer, path, replaces
Content-Length: 0

当密码输错的时候,会返回 403

1
2
3
4
5
6
7
8
9
10
11
12
13
14
................
16:59:04.793322 IP 10.211.55.6.5060 > 10.211.55.2.56752: SIP: SIP/2.0 403 Forbidden
E..IP$..@...
.7.
.7......5..SIP/2.0 403 Forbidden
Via: SIP/2.0/UDP 172.20.10.4:56752;rport=56752;branch=z9hG4bKPjsNvqOhF5Lbmtb4akKPU1hjvy26NfwF8I;received=10.211.55.2
From: "centos7" <sip:1001@10.211.55.6>;tag=40Anh2eSlQx7V25EWr2j0i.wfCuRCblN
To: "centos7" <sip:1001@10.211.55.6>;tag=7Fc79Qjra01aB
Call-ID: 9LWhk3GzN5WsrbOehtaNzpwMPQdg6dbB
CSeq: 2 REGISTER
User-Agent: FreeSWITCH-mod_sofia/1.10.3-release.5~64bit
Allow: INVITE, ACK, BYE, CANCEL, OPTIONS, MESSAGE, INFO, UPDATE, REGISTER, REFER, NOTIFY, PUBLISH, SUBSCRIBE
Supported: timer, path, replaces
Content-Length: 0

INVITE

初始化一个会话,可以理解为发起一个呼叫

ACK

对 INVITE 消息的最终响应

CANCEL

取消一个等待处理或正在处理的请求

BYE

终止一个会话

OPTIONS

可以用来查询服务器支持的信令,codes 等,也可以用作 ping 测试。

1
2
3
4
5
6
7
8
OPTIONS sip:bob@gauss.com SIP/2.0
Via: SIP/2.0/UDP pc10.newton.com;branch=z9hG4bK1ea Max-Forwards: 70
To: Bob <sip:bob@gauss.com>
From: Alice <sip:alice@newton.com>;tag=14124
Call-ID: a8a931aa@pc10.newton.com
CSeq: 1000 OPTIONS
Contact: <sip:alice@pc10.newton.com>
Content-Length: 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SIP/2.0 200 OK
Via: SIP/2.0/UDP pc10.newton.com;branch=z9hG4bK1ea;received=10.0.0.1
Max-Forwards: 70
To: Bob <sip:bob@gauss.com>;tag=3843
From: Alice <sip:alice@newton.com>;tag=14124
Call-ID: a8a931aa@pc10.newton.com
CSeq: 1000 OPTIONS
Contact: <sip:bob@host2.gauss.com>
Allow: INVITE, ACK, CANCEL, OPTIONS, BYE
Accept: application/sdp
Supported: 100rel
Content-Type: application/sdp
Content-Length: 146
v=0
o=bob 3812844327 3812844327 IN IP4 host2.gauss.com s=-
t=0 0
c=IN IP4 host2.gauss.com
m=audio 0 RTP/AVP 0 8
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
  • Allow 表示服务端可处理的 sip 信令
  • Accept 表示服务端可处理 SDP
  • SDP 消息中的 m 行,数字 0 用来防止媒体流初始化。后面的0 8,表示支持的 audio 的 codecs。下面的两个 a 行,是对其具体描述

SIP 报文头域

所有 SIP 消息都必须包含以下前 6 个头域

  1. Call-ID 用于区分不同会话的唯一标志
  2. CSeq 序列号,用于在同一会话中区分事务
  3. From 说明请求来源
  4. To 说明请求接受方
  5. Max-Forwars 限制跳跃点数和最大转发次数
  6. Via 描述请求消息经过的路径

request 和 response 报文的 From 和 To 是完全一致的,尽管他们的方向是相反的。From 和 To 的值是根据 request 来定义的。 Via 中的 branch 标记腿的 id 当有使用 proxy 代理时,请求转发时用的还是同一个 branch,From 和 To 中的 tag 可以作为 id 标记是否为原始请求。

Via 的一个示例 sip_Via.png

更多的请参考SIP 参数

状态码

与 HTTP 响应类似,状态码由 3 位数字组成

  • 1xx 临时状态,表明呼叫进展的情况
  • 2xx 表明请求一杯成功收到,理解和接收
  • 3xx 重定向,表明 SIP 请求需要转向到另一个 UAS 处理
  • 4xx 表明请求失败,这种失败一般由客户端或网络引起的,如密码错误,空号,客户端应该重新修改请求,然后重发
  • 5xx 服务器内部错误
  • 6xx 全局性错误,如 600 Busy Everywhere

状态码后面跟着一个原因短语(如 200 OK 中的 OK),它是对前面的状态码的一个简单解释

常用状态码 | 状态码| 说明| | :---- | :---- | |180|振铃| |488|不兼容的媒体类型|

具体状态码见SIP 状态码 WIKI

FreeSwitch 中的 SIP 模块

基本概念

  1. Sofia-SIP FreeSwitch 的 SIP 功能是在 mod_sofia 模块中实现的。FreeSwitch 并没有自己开发新的 SIP 协议栈,而是使用了比较成熟的开源 SIP 协议栈 Sofia-SIP。
  2. Endpoint 在 FreeSwitch 中,实现一些互联网协议接口的模块称为 Endpoint。FreeSwitch 支持很多类型的 Endpoint,如 SIP,H232 等。这些不同的 Endpoint 主要是使用不同的控制协议跟其他的 Endpoint 通话。
  3. mod_sofia mod_sofia 实现了 SIP 中的注册服务器、重定向服务器、媒体服务器,呈现服务器、SBC 等各种功能。它的定位是一个 B2BUA。
  4. SIP Profile 在 mod_sofia 中,SIP Profile 相当于一个 SIPUA,通过各种不同的参数可以配置一个 UA 的行为。一个系统可以有多个 SIP Profile,每个 SIP Profile 都可以监听不同的 IP 地址和端口。
  5. Gateway 一个 SIP Profile 中有多个 Gateway(网关),它主要用于定义一个远程的 SIP 服务器,使 Freeswitch 可以与其他服务器通信。
  6. 本地 SIP 用户 FreeSwitch 可以作为注册服务器,这时候其他 SIP 客户端就可以向它注册。FreesWitch 将通过用户目录中(conf/Directory)中的配置信息对注册用户进行鉴权。这些 SIP 客户端锁代表的用户就称为本地 SIP 用户,简称本地用户
  7. 来电去话,中继来电,中继去话

配置文件

Sofia 的配置文件在autoload_configs/sofia.conf.xml中。Sofia 支持多个 Profile,而每一个 Profile 相当于一个 SIP UA,在启动后悔监听一个“IP:PORT”对。FreeSwitch 默认的配置带了三个 Profile(也就是三个 UA)。我们不讨论 IPv6,仅讨论 internal 和 external(分别在 internal.xml 和 external.xml 中定义的,分别运行在 5060,5080 端口上)

Profile 的几个重要参数,节选 internal.xml 部分配置

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
<profile name="internal">
<aliases>
<!-- 别名 呼叫字符串中可以使用别名 例如 sofia/default/10086@127.0.0.1-->
<alias name="default"/>
</aliases>
<!--网关配置,一般在external上定义网关配置,网关配置就是对远程SIP服务器的一些参数配置,具体的参数配置由SIP服务器来决定-->
<gateways></gateways>
<domains>
<domain name="all" alias="true" parse="false"/>
</domains>
<settings>
<!--设置来话将进入Dialplan中的哪个Context进行路由-->
<param name="context" value="public"/>
<!-- 设置默认的dialplan类型,即在该Profile上有电话呼入后到哪个Dialplan中进行路由-->
<param name="dialplan" value="XML"/>
<!-- 设置支持的来电媒体编码,用于编码协商-->
<param name="inbound-codec-prefs" value="$${global_codec_prefs}"/>
<!-- 设置支持的去话媒体编码,用于编码协商-->
<param name="outbound-codec-prefs" value="$${global_codec_prefs}"/>
<!-- ip address to use for rtp, DO NOT USE HOSTNAMES ONLY IP ADDRESSES -->
<param name="rtp-ip" value="$${local_ip_v4}"/>
<!-- ip address to bind to, DO NOT USE HOSTNAMES ONLY IP ADDRESSES -->
<param name="hold-music" value="$${hold_music}"/>
<!--是否开启媒体绕过功能-->
<!--<param name="inbound-bypass-media" value="true"/>-->
<!-- 是否对来电进行鉴权-->
<param name="auth-calls" value="$${internal_auth_calls}"/>
<!-- external_sip_ip
Used as the public IP address for SDP.
Can be an one of:
ip address - "12.34.56.78"
a stun server lookup - "stun:stun.server.com"
a DNS name - "host:host.server.com"
auto - Use guessed ip.
auto-nat - Use ip learned from NAT-PMP or UPNP
-->
<param name="ext-rtp-ip" value="$${external_rtp_ip}"/>
<param name="ext-sip-ip" value="$${external_sip_ip}"/>
</settings>
</profile>

external.xml 的配置与 internal.xml 的配置大部分相同,最大的不同是auth-calls参数,internal.xml 默认为 true,而 external.xml 默认为 false。也就是说,客户端发往 FreeSwitch 的 5060 端口的 SIP 消息需要鉴权(一般只对 REGISTER 和 INVITE 消息进行鉴权),而发往 5080 端口的消息不需要鉴权。我们一般把本地用户都注册到 5060 上,所以它们打电话时要经过鉴权,保证只有授权用户(本地用户目录中配置的)才能注册和拨打电话。而 5080 则不同,任何人均可以向该端口发送 SIP INVITE 请求。

Gateway(网关)

在 external.xml 中我们可以看到它使用预处理指令将 external 目录下的所有 XML 配置文件都装入到 external 的 Profile 文件的 gateways 标签中:

1
2
3
4
5
<profile name="external">
<gateways>
<X-PRE-PROCESS cmd="include" data="external/*.xml"/>
</gateways>
</profile>

节选部分 Gateway 配置

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
<gateway name="唯一网关名称">
<param name="realm" value="SIP服务器地址:默认端口5060"/>
<param name="username" value="用户名"/>
<param name="password" value="密码"/>
<!-- SIP消息中From字段的值,默认与username相同 -->
<param name="from-user" value="cluecon"/>
<!-- From字段的domain值,默认与realm相同 -->
<param name="from-domain" value="asterlink.com"/>
<!-- 来话中的分机号,即被叫号码,默认与username相 -->
<param name="extension" value="cluecon"/>
<!-- 代理服务器地址,默认与realm相同 -->
<param name="proxy" value="asterlink.com"/>
<!-- 代理注册服务器地址,默认与realm相同 -->
<param name="register-proxy" value="mysbc.com"/>
<!-- 注册SIP消息的Expires字段的值,单位为minute -->
<param name="expire-seconds" value="60"/>
<!-- 是否需要注册,有些网关必须注册才能打电话,有些不需要 -->
<param name="register" value="false"/>
<!-- SIP消息是否udp还是tcp -->
<param name="register-transport" value="udp"/>
<!-- 注册失败或超时后,等待多少秒后重新注册 -->
<param name="retry-seconds" value="30"/>
<!-- 将主叫号码放到SIP的From字段中。 -->
<param name="caller-id-in-from" value="false"/>
<!-- 设置SIP协议中的Contact字段中额外的参数 -->
<param name="contact-params" value=""/>
<!-- 每隔一段时间发送一个SIP OPTIONS消息,如果失败,则会从该网关注销,并将其设置为down状态。心跳检测服务是否畅通 -->
<param name="ping" value="25"/>
</gateway>

呼叫是如何工作的

我们假设用的是默认配置,并从 1000 呼叫 1001。

  1. 1000 的 SIP 话机作为 UAC 会发送 INVITE 请求到 FreeSwitch 的 5060 端口,也就是达到 mod_sofia 的 internal 这个 Profile 所配置的 UAS,该 UAS 收到正确的 INVITE 请求后会返回 100 响应码,表示我收到你的请求了。该 UAS 对所有收到的 INVITE 都要进行鉴权(因为 auth-calls=true)。它会检测 ACL(访问控制列表,一般用于 IP 鉴权)。默认的 ACL 检查是不通过的因此就会走到密码鉴权(HTTP 协议中的 Digest Auth)阶段。一般是 UAS 回复 401。
  2. UAC 重新发送带鉴权信息的 INVITE,UAS 收到后,便将鉴权信息提交到上层的 FreeSwitch 代码,FreeSwitch 就到会 Directory(用户目录)查找相应的用户。此处,它会找到conf/directory/default/1000.xml文件中配置的用户信息,并根据其中配置的密码进行鉴权。如果鉴权不通过,返回 403 Forbidden 等错误信息,通话结束。如果鉴权通过,FreeSwitch 就取到了用户的信息,比较重要的是 user_context,在我们的例子中它的值为 default。接下来电话进入路由(routing)阶段,开始查找 Dialplan。用于该用户的 Context 是 default,因此路由就从 default 这个 Dialplan 查起(即conf/dialplan/default.xml)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <include>
    <user id="1001">
    <params>
    <param name="password" value="$${default_password}"/>
    <param name="vm-password" value="1001"/>
    </params>
    <variables>
    <variable name="toll_allow" value="domestic,international,local"/>
    <variable name="accountcode" value="1001"/>
    <variable name="user_context" value="default"/>
    <variable name="effective_caller_id_name" value="Extension 1001"/>
    <variable name="effective_caller_id_number" value="1001"/>
    <variable name="outbound_caller_id_name" value="$${outbound_caller_name}"/>
    <variable name="outbound_caller_id_number" value="$${outbound_caller_id}"/>
    <variable name="callgroup" value="techsupport"/>
    </variables>
    </user>
    </include>
  3. 查找 Dialplan,找到 1001 这个用户,并执行 bridge user/1001,在这里 user/1001 称为呼叫字符串,它会再次查找 Directory,找到conf/directory/default/1001.xml里配置的参数,由于 1001 是被叫,因此他会进一步查找直到查到 1001 实际注册的位置,由于所有用户的规则都是一样,因此该参数被放到conf/direcotry/default.xml中,在该文件中可以看到如下配置

    1
    2
    3
    4
    5
    6
    7
    <domain name="$${domain}">
    <params>
    <param name="dial-string" value="{^^:sip_invite_domain=${dialed_domain}:presence_id=${dialed_user}@${dialed_domain}}${sofia_contact(*/${dialed_user}@${dialed_domain})},${verto_contact(${dialed_user}@${dialed_domain})}"/>
    <!-- These are required for Verto to function properly -->
    <param name="jsonrpc-allowed-methods" value="verto"/>
    <!-- <param name="jsonrpc-allowed-event-channels" value="demo,conference,presence"/> -->
    </params>

    其中,最关键的是 sofia_contact 这个 API 调用,它会查找数据库,找到 1001 实际注册的 Contact 地址,并返回真正的呼叫字符串。

    1
    2
    freeswitch@CentOS7> sofia_contact  1001
    sofia/internal/sip:46598071@10.211.55.2:51032
  4. 找到呼叫字符串后,FreeSwitch 又启动另外一个会话作为一个 UAC 给 1001 发送 INVITE 请求,如果 1001 摘机,则 1001 向 FreeSwitch 回送 200 OK 消息,FreeSwitch 再向 100 返回 200OK,通话开始。

FreeSwitch 是一个 B2BUA,上面的过程建立了一通会话,其中有两个 Channel。我们可以跟踪 SIP 消息试一下sofia profile internal siptrace on/off

在 FreeSwitch 的默认配置中,external 对应的 Profile 是不鉴权的,凡是送到 5080 端口的 INVITE 都不需要鉴权。

  • SIPUA 直接把 INVITE 送到任意端口,一般用于中继方式对接
  • FreeSwitch 作为一个客户端,若要添加一个网关,则该网关会被放到sip_profiles/external/的文件中,它就会被包含到sip_profiles/external.xml中。它向其他服务器注册时,其中的 Contact 地址就是 IP:5080,如果有来话,对方的服务器就会把 INVITE 送到它的 5080 端口

zookeeper

发表于 2020-09-02 更新于 2020-12-02

sql

发表于 2020-09-02 更新于 2020-12-02

合并多行表记录

1
select code,sum(nums) as counts from table group by code

将几种 group 合并为一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
select
thecode,
sum(counts)
from
(
select
case
when code = '01' then '00'
else code
end as thecode,
counts
from
(
select
code,
sum(nums) as counts
from
table
group by
code
)
)
group by
thecode

db2

js

发表于 2020-08-28 更新于 2020-12-02

名字空间

全局变量会绑定到window上,不同的 JavaScript 文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。

减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。例如:

1
2
3
4
5
6
7
8
9
10
11
// 唯一的全局变量MYAPP:
var MYAPP = {};

// 其他变量:
MYAPP.name = "myapp";
MYAPP.version = 1.0;

// 其他函数:
MYAPP.foo = function () {
return "foo";
};

把自己的代码全部放入唯一的名字空间MYAPP中,会大大减少全局变量冲突的可能。

许多著名的 JavaScript 库都是这么干的:jQuery,YUI,underscore 等等。

标准对象

总结一下,有这么几条规则需要遵守:

  • 不要使用new Number()、new Boolean()、new String()创建包装对象;
  • 用parseInt()或parseFloat()来转换任意类型到number;
  • 用String()来转换任意类型到string,或者直接调用某个对象的toString()方法;
  • 通常不必把任意类型转换为boolean再判断,因为可以直接写if (myVar) {...};
  • typeof操作符可以判断出number、boolean、string、function和undefined;
  • 判断Array要使用Array.isArray(arr);
  • 判断null请使用myVar === null;
  • 判断某个全局变量是否存在用typeof window.myVar === 'undefined';
  • 函数内部判断某个变量是否存在用typeof myVar === 'undefined'。

最后有细心的同学指出,任何对象都有toString()方法吗?null和undefined就没有!确实如此,这两个特殊值要除外,虽然null还伪装成了object类型。

更细心的同学指出,number对象调用toString()报 SyntaxError:

1
123.toString(); // SyntaxError

遇到这种情况,要特殊处理一下:

1
2
(123).toString(); // '123', 注意是两个点!
(123).toString(); // '123'

不要问为什么,这就是 JavaScript 代码的乐趣!

JSON

在 JSON 中,一共就这么几种数据类型,并且,JSON 还定死了字符集必须是 UTF-8,表示多语言就没有问题了。为了统一解析,JSON 的字符串规定必须用双引号"",Object 的键也必须用双引号""。

  • number:和 JavaScript 的 number 完全一致;

  • boolean:就是 JavaScript 的 true 或 false;

  • string:就是 JavaScript 的 string;

  • null:就是 JavaScript 的 null;

  • array:就是 JavaScript 的 Array 表示方式——[];

  • object:就是 JavaScript 的{ ... }表示方式。

COOKIE

服务器在设置 Cookie 时可以使用 httpOnly,设定了 httpOnly 的 Cookie 将不能被 JavaScript 读取。这个行为由浏览器实现,主流浏览器均支持 httpOnly 选项,IE 从 IE6 SP1 开始支持。

为了确保安全,服务器端在设置 Cookie 时,应该始终坚持使用 httpOnly。

作用域

  1. 在 object 内的 function this 指向 object,而属于 function 内部的闭包函数 this 指向 window 对象,纯 function this 也指向 window 对象。

  2. “自由变量”。在 A 作用域中使用的变量 x,却没有在 A 作用域中声明(即在其他作用域中声明的),对于 A 作用域来说,x 就是一个自由变量。如下

    1
    2
    3
    4
    5
    var x = 10;
    function fn() {
    var b = 20;
    console.log(x + b); //这里的x在这里就是一个自由变量
    }
  3. 为 Object.prototype 赋值,相当于在 window 对象上赋值

如上程序中,在调用 fn()函数时,函数体中第 6 行。取 b 的值就直接可以在 fn 作用域中取,因为 b 就是在这里定义的。而取 x 的值时,就需要到另一个作用域中取。到哪个作用域中取呢?

有人说过要到父作用域中取,其实有时候这种解释会产生歧义。例如:

1
2
3
4
5
6
7
8
9
10
11
var x = 10;
function fn() {
console.log(x);
}
function show(f) {
var x = 20;
(function () {
f(); //10 而不是20
})();
}
show(fn);

要到创建这个函数的那个作用域中取值——是“创建”,而不是“调用”,切记切记——其实这就是所谓的“静态作用域”。

原型

1
2
3
4
5
6
7
8
9
function Fn() {}
Fn.prototype.name = "xiaoming";
Fn.prototype.getYear = function () {
return 1988;
};

var fn = new Fn();
console.log(fn.name);
console.log(fn.getYear());
  1. 函数 Fn 也是对象,每个函数都有一个属性prototype,prototype的值为原型对象
  2. fn 为 Fn 函数的一个实例对象,每个对象都有一个隐藏属性 __proto__,fn 的__proto__指向 Fn 的prototype,即原型对象。
  3. Object.prototype 是一个特例——它的__proto__指向的是 null
  4. 访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着__proto__这条链向上找,这就是原型链。

    js-tips_2020-04-25-13-18-14.png
    js-tips_2020-04-25-13-18-14.png

我们以一个例子来说明

1
2
3
4
5
6
7
8
9
10
11
12
var ob = {
f: function () {
console.log(this.a);
console.log(a);
},
};
// 变量ob的声明为语法糖,等同于 ob = Object()
// ob的__proto__指向 Object.prototype,即所有实例对象的最顶层的原型对象
// ob.f 方法中 this.a,
ob.__proto__.a = 1;
ob.__proto__ = { a: 2 };
ob.f();

输出结果

2 1

this

this 到底取何值,是在函数真正被调用执行的时候确定的,函数定义的时候确定不了

  1. 在全局环境下,this 永远是 window
  2. 函数作为构造函数用,那么其中的 this 就代表它即将 new 出来的对象
  3. 如果函数作为对象的一个属性时,并且作为对象的一个属性被调用时,函数中的 this 指向该对象
  4. 一个函数被 call 和 apply 调用时,this 的值就取传入的对象的值
  5. 箭头函数体内的 this 对象就是定义时所在的对象,而不是使用时的对象

执行上下文

函数表达式”和“函数声明”。虽然两者都很常用,但是这两者在“准备工作”时,却是两种待遇。

1
2
3
4
console.log(f1); // function f1(){};
console.log(f2); //undefined
function f1() {}
var f2 = function () {};

在初始化时,对待函数表达式就像对待“ var a = 10 ”这样的变量一样,只是声明。而对待函数声明时,却把函数整个赋值了。

全局代码的上下文环境数据内容为:

好了,总结完了函数的附加内容,我们就此要全面总结一下上下文环境的数据内容。

全局代码的上下文环境数据内容为:

  • 普通变量(包括函数表达式),如: var a = 10; 声明(默认赋值为undefined)
    • 函数声明,如: function fn() { } 赋值
      • this 赋值

如果代码段是函数体,那么在此基础上需要附加:

  • 参数 赋值
    • arguments 赋值
    • 自由变量的取值作用域 赋值

给执行上下文环境下一个通俗的定义——在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值了,有的先用undefined占个空。

匿名函数

js 函数前加分号和感叹号是什么意思?有什么用?

1
2
3
4
5
6
7
8
// 这么写会报错,因为这是一个函数定义:
function() {}()

// 常见的(多了一对括号),调用匿名函数:
(function() {})()

// 但在前面加上一个布尔运算符(只多了一个感叹号),就是表达式了,将执行后面的代码,也就合法实现调用
!function() {}()

在前面加上~+-等一元操作符也可以。。其实还有好几种符合都可以保证匿名函数声明完就立即执行

var hi = function(){ alert("hi") }; hi(); 等于... (function(){ alert("hi") })(); !、+和()一样的效果可以把换成 !function(){ alert("hi") }(); !比()节省一个字符,或者说比()好看些 我们都知道分号是为了和前面的代码隔开,js 可以用换行分隔代码,但是合并压缩多个 js 文件之后,换行符一般会被删掉,所以连在一起可能会出错,加上分号就保险了。 一元操作符会对 func 的返回值进行实际运算

语法糖

  1. 一般对象格式如下

    1
    2
    3
    4
    obj = {
    a: "a",
    b: "b",
    };

    当值为一个对象的时候,我们可以使用简写方式

    1
    2
    3
    4
    c = {};
    obj = {
    c,
    };
  2. 变量声明

    1
    2
    3
    4
    5
    6
    7
    8
    obj = {
    a: "a",
    b: "b",
    };

    var { a, b } = obj;
    console.log(a);
    console.log(b);
123…15

lijun

与其感慨路难行,不如马上就出发
145 日志
30 分类
117 标签
© 2019 – 2020 lijun