跳到主要内容

FRP 内网穿透

· 阅读需 3 分钟

利用 frp 我我们可以把自己的私人电脑暴露在公网,当云服务器一样使用

所需资源

  1. 一台云主机
  2. 家用主机

下载

frp 上下载对应的安装包,解压

$ ls ~/frp/
frpc frpc.toml frps frps.toml LICENSE

其中 frpc 是客户端运行在家用主机,frps 是服务端运行在云主机

配置

frps.toml
[common]
bind_port = 7000
token = your_token # 和家用主机保持一致

# 添加HTTP和HTTPS虚拟主机端口
vhost_http_port = 80
vhost_https_port = 443

启动

  1. 在云主机上运行 ./frps -c fprs.toml
  2. 在家用主机上运行 ./frpc -frpc.toml
  3. 在任意地方 ssh -P 2222 your_user@your_host 远程登录。或者在任意地方访问 your_host访问的即是家里的主机

service 脚本

可将服务写个 service 脚本,使其开机自己,自动重启等

vim /etc/systemd/system/frpc.service

/etc/systemd/system/frpc.service
[Unit]
Description=frpc client
After=network.target

[Service]
Type=simple
WorkingDirectory=/home/wenyg/frp
ExecStart=/usr/bin/sudo /home/wenyg/frp/frpc -c /home/wenyg/frp/frpc.toml
Restart=always
RestartSec=5s
User=wenyg
Group=wenyg
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

执行脚本

sudo systemctl daemon-reload
sudo systemctl start frpc.service
sudo systemctl enable frpc.service

查看服务状态

sudo systemctl status frpc.service

字符串三剑客: awk

· 阅读需 2 分钟

awk 是一个强大的文本处理工具,用于处理结构化文本数据。以下是一些在使用 awk 时的小技巧:

  1. 基本用法:

    awk '{print $1}' filename

    这将打印文件中每行的第一个字段。

  2. 指定字段分隔符:

    awk -F':' '{print $1}' /etc/passwd

    这将使用冒号作为字段分隔符来处理 /etc/passwd 文件。

  1. 条件匹配和处理:

    awk '/pattern/ {print $2}' filename

    这将打印包含指定模式的行的第二个字段。

  2. 计算和使用变量:

    awk '{sum+=$1} END {print sum}' filename

    这将计算文件中第一个字段的总和,并在文件结束时打印结果。

  3. 自定义输出格式:

    awk '{printf "Name: %-10s Age: %s\n", $1, $2}' filename

    这将按照指定格式输出字段内容,使用 printf 函数。

  4. 处理列之间的关系:

    awk '$2 > 50 {print $1, "is greater than 50"}' filename

    这将打印第一个字段,如果第二个字段大于50,则输出附加信息。

  5. 统计行数:

    awk 'END {print NR}' filename

    这将在文件结束时打印行数。

  6. 自定义分隔符输出:

    awk '{print $1 "|" $2}' filename

    这将在输出中使用自定义分隔符。

定位软件 traccar

· 阅读需 1 分钟

安装手机端

  1. IOS 可在应用商店下载,Android 可在 traccar-client-android 下载
  2. 安装,国内可能被认定为病毒软件。
  3. 安转完之后有打开,有个设备编码可以自己设置。这个用于在服务端添加设备

部署服务端

docker run --name traccar \
-v ~/.traccar/data:/opt/traccar/data \
-p 8082:8082 \
traccar/traccar

打开自己的服务器地址,注册,添加设备即可。

其他

对于 Android 13以上的设备,可能需要下载源码重新编译,在 traccar-client-android/app/src/main/AndroidManifest.xml 添加

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

否则可能会被系统杀死。

利用坚果云自动备份文件

· 阅读需 1 分钟

坚果云提供了 webdav 协议,可以用 rclone 进行文件的上传。

  1. 登录坚果云 https://www.jianguoyun.com/#/safety 添加应用,获得密码
  2. 安装 rclone
  3. rclone config 添加坚果云配置,按照提示一步一步来就行,依次填写
    1. name: 随便填,下面与 jianguoyun 为例,后面传输文件的时候会用到该字段
    2. Storage: 选 WebDav
    3. url: 填坚果云地址 https://dav.jianguoyun.com/dav/
    4. vendor: 选 other
    5. user: 填自己的坚果云账户
    6. password: 填前面步骤中获取的应用密码,非坚果云账户密码
  4. 测试 rclone lsd jianguoyun: 如果能显示网盘的信息,就表示配置成功

上传文件

rclone copy /path/to/local jianguoyun:/path/to/remote

坚果云免费用户每月只提供 1G 上传流量

申请 ssl 证书

· 阅读需 1 分钟

Let's Encrypt 提供免费的 SSL/TLS 证书,Certbot 是官方推荐的自动化客户端,用于申请和续期证书。

  1. 服务器安装 certbot
  2. 域名解析,将域名解析到服务器上
  3. certbot 申请证书 (需要先停止该服务器上的 80 端口的服务)
    sudo certbot certonly -d  www.winn.cc --nginx
    Saving debug log to /var/log/letsencrypt/letsencrypt.log
    Requesting a certificate for www.winn.cc

    Successfully received certificate.
    Certificate is saved at: /etc/letsencrypt/live/www.winn.cc/fullchain.pem
    Key is saved at: /etc/letsencrypt/live/www.winn.cc/privkey.pem
    This certificate expires on 2025-07-23.
    These files will be updated when the certificate renews.
    Certbot has set up a scheduled task to automatically renew this certificate in the background.
  4. 第三步中的路径是个符号链接,用 realpath 获取真实文件路径
  5. 部署
  6. 到期时候执行 sudo certbot renew 即可更新证书。(需要先停止 80 端口服务)

C++ 包管理方案

· 阅读需 4 分钟

为什么 C++ 没有一个统一的包管理器

很多主流语言都有自己的包管理方案,比如 Python 的 pip, JavaScript 的 npm, Java中的 Maven 等,但是 C++ 并没有一个统一的包管理方案,归根到底有一定的历史原因。C++ 标准在不断地演进,C++98,C++03,到现代的C++11/14/17/20等,每个版本都引入一些新特性,在这些标准当中引入一个官方的包管理器要考虑到前后兼容性和现有系统的影响。另外 C++ 的生态系统非常庞大和多样化,涵盖了不同的编译器,操作系统,硬件平台等,一个官方的 C++ 管理器平台如何实现需要各个社区达成一致,制定一个统一的标准,这是一个复杂且需要时间的过程。

没有包管理器的开发环境是怎样的

首先,项目规模不大的话,有没有包管理器是影响不大的,选定好平台,安装或者自己编译需要的库一直用发下去就可以了。

但如果项目规模变大可能就会出现问题,依赖项管理困难,团队成员可能需要手动,安装和管理各种依赖和第三方库。进而会导致另外一个问题,版本控制和一致性,每个团队成员手动管理依赖项的时候,可能会使用不同版本的库或者工具,增加代码的不稳定性,出现一些潜在的BUG。

同时手动维护依赖会导致很多流程无法自动化,CI/CD等无法实施 随着团队规模变大,需要投入更大的时间和精力去管理这些流程,慢慢就会变得不可持续。

包管理的各个阶段

  1. 原生拷贝。各个团队成员直接拷贝代码或者库文件给他人使用,且没有版本管理。
  2. 加入版本管理。每个团队维护好自己的代码库,通过CI自动构建,供别人下载使用。
  3. 加入依赖管理。提供一定的策略,获取递归的获取代码库所有依赖。
  4. 解决依赖冲突。当设计到依赖冲突的时候,提供策略解决冲突问题。
  5. 多平台支持。支持不同的架构,操作系统。

一种包管理平台的设计思路

Conan, 一个开源的C++包管理器工具,虽然没有被大规模普及,但是它的一些策略和方案,可以用来学习。

首先一个 C++ 包需要有哪些属性,一个软件包最终都是编译成二进制,所以就要考虑二进制库文件有哪些属性,首先是二进制包是怎么来的

  1. 构建包的体系架构(x86/arm/???),操作系统(Linux/Windows/MacOS/???), 编译器(gcc/clang/msvc/???), 编译器版本,等平台的属性。
  2. 编译选项,比如静态库还是动态库,某些编译选项等包自定义的一些属性。
  3. 依赖项。依赖哪些项目,对项目的依赖粒度,比如是否限制大版本或者小版本。

如果只管理二进制包,即编译之后的库文件,只需要上面这些属性就可以了。同一个版本比如 OpenCV/3.4.2 在不同架构,操作系统,编译器,编译选项下可能会产生数十个版本。其他用户下载固定包的时候也会带上自己的平台属性,然后找到对应的包传递给客户即可。有上面的匹配机制,就保证下载下来的一定是兼容的。

如果不想直接管理二进制库,想要通过管理源代码的方式管理各个平台的二进制库还需要更复杂的策略,比如获取源代码的方式,构建方式,打包方式等。

TCP 随笔

· 阅读需 5 分钟

本文章记录TCP的一些杂七杂八的知识点,比较零碎。

  1. ICMP 可用于发现链路上的最小 MTU
  2. 一条 sock 连接有五元组(Proto, SourceIp, DestIp, SourcePort, DestPort) 组成,任意一个改变都是可以是一个新的连接。 比如两台机器都有 2个IP,2个端口,用于TCP连接。那么他们之间可以创建 (222*2)*2 = 32 个 TCP 连接。
  3. 传输层协议除了TCP 还有 UDP/SCTP/DCTP
  4. TCP中的序号 是字节流编号,而不是报文的序号。比如第一个报文序号是0,然后该报文有300个字节的数据,那么第二个报文的序号就该是300
  5. 确认号表示期望收到的下一个报文的第一个字节编号是多少,比如面收到第一个报文之后,确认号就回设置为300,表示接下来希望收到第一个字节编号为300的报文
  6. TCP的第一个序号通常假设为0, 但实际上可以随机的选择初始序号
  7. 如果发送端收到同一个序号的重复确认3次(冗余ACK)即可认为序号之后的报文已经丢失,可以进行快速重传
  8. TCP 接受方重传的时候可以跳过那些已被选择确认的报文,需要接收方支持选择确认。(SACK,在TCP首部的 options 字段里)
  9. TCP 接受方会维护一个 LastByteRead, 用户层已读取的最后一个字节的编号; LastByteRecv, 放入到缓存区的最后一个字节的编号。还有一个缓存区大小RecvBuffer。滑动窗口大小,就是缓存区大小(Buffer)减去已缓存的大小(Recv-Read)
  10. TCP 发送方会维护一个 LatByteSent, 已发送的序号; LastByteAcked, 已被确认的序号。Sent-Acked 需要小于滑动窗口的大小。来保证接口方的缓存区不会被溢出。
  11. 如果接收方缓存区满了之后,窗口会设置为0;之后发送方会发送只有一个字节的报文段,用来 “轮询” 窗口更新。
  12. TCP第三次握手确认的时候可以携带一些数据。
  13. 现在的主流操作系统都支持syn-cookie,在第三次握手之前,服务器并不维护客户端的信息, 可以有效的防御syn-flood攻击。
  14. MSS 最大报文段长度,避免物理层分片,通常比MTU小一点。(在options里协商)
  15. RTT 连接往返时间,即发出后到收到ACK的时间。
  16. 拥塞控制
    1. 慢启动,刚开始以一个MSS的值传输,后面2个,4个,成指数增长。直到发生拥塞,把此时窗口值的1/2,叫做慢启动阈值。
    2. 发生拥塞的时候,重新从1个MSS开始增加,直到增加到慢启动阈值, 之后不再指数增加,而是一个MSS一个MSS的增加。这个过程叫拥塞避免。
    3. 快速恢复,收到3次冗余ACK的时候,窗口不再从1个MSS开始,而是从慢启动阈值+3开始,然后开始重传。
  17. TCP 字段
  0                   1                   2                   3   
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window |
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

在RFC 3168 中 TCP 的 reserved 位置又使用了两位(CWE, ECE 用于处理拥塞控制和显式拥塞通知)

  0                   1                   2                   3   
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |C|E|U|A|P|R|S|F| |
| Offset|Reser. |W|C|R|C|S|S|Y|I| Window |
| | |R|E|G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  1. TCP与网络编程
  2. 首先是服务端 bind,listen 初始化接受队列
  3. 客户端 connect 选择本地端口,发起 syn 握手请求,同时启动重传定时器
  4. 服务端 回应 syn ack, 将连接加入到半连接队列,启动重传定时器
  5. 客户端收到 ack,清除定时器,设置为已连接,发送ack
  6. 创建 sock 从半连接队列中取出放到全连接队列。
  7. accept 从全连接队列中取出 socket

C++中的锁

· 阅读需 1 分钟

C++中,锁是用来管理并发访问共享资源的工具,下面是与锁相关的一些概念。

std::mutex

互斥锁,最基本的锁类型之一,提供了最基本的锁操作,lock()/unlock()/try_lock();

std::shared_mutex

共享锁,或者叫读写锁, C++17中引入的。允许多个线程同时读取共享锁。

std::lock_guard, std::unique_lock, std::shared_lock

对 mutex 进行了一层封装,更为抽象的封装,RAII风格,为 mutex 的管理类。

  • std::lock_guard 在构造函数时候加锁,在析构的时候释放锁。
  • std::unique_lock 支持手动释放锁,加锁。
  • std::shared_lock 与 std::shared_mutex 配合实现共享锁

std::atomic_flag

原子布尔类型,可用于实现自旋锁。提供有 test_and_set()/clear() 方法

std::condition_varable

条件变量不是锁,而是与锁结合使用来实现复杂的线程同步机制。它允许一个线程在条件变量上等待,被唤醒之后先判断表达式的值,如果为真再尝试获取锁。

C++虚继承下的内存布局

· 阅读需 6 分钟

讲虚继承之前,先讲讲多继承,下面是一个多继承的示例, C 继承了 A 和 B。

#include <cstdio>

class A {
public:
long varA;
virtual void funA1(){ std::puts("A::funA1()");};
virtual void funA2(){ std::puts("A::funA1()");};
};

class B {
public:
int varB;
virtual void funB1(){ std::puts("B::funB1()");};
virtual void funB2(){ std::puts("B::funB2()");};
};

class C: public A, public B {
public:
int varC;
virtual void funA1(){ std::puts("C::funA1()");};
virtual void funB2(){ std::puts("C::funB2()");};
virtual void funC(){ std::puts("C::funC()");};
};

先思考下面几个问题

  1. A, B, C的对象大小应该都是多大?
  2. A* a = new C();, 那么 typeid(*a) 返回的会 A 的信息,还是 C 的信息?
  3. 有函数 void process(A *a);, 该函数内部,能否访问到 B::funB1 / B::funB2 吗?
  4. 只给一个类C的指针,怎么不用他的函数接口来访问它的虚函数?
  5. 虚函数调用会增加访问调用开销吗?多重继承得到的虚函数和单层继承得到的虚函数,他们调用开销一样吗?

如果你对上面的问题了如指掌,建议跳过本文章。

内存布局

A,B,C 的内存布局与虚函数表如下

可以通过 g++ -fdump-lang-class -c base.cpp 来看到C++ 类的虚函数表和内存布局

我们先以class A 为例, 讲解下虚函数表的内容。

--------------
0 // Top Offset 原始对象的偏移量
--------------
typeinfo for A // RTTI信息,dynmaic_cast 转换的时候会根据这个判断是否能转换
--------------
A::funA1() // 虚函数表指针指向的位置,注意,虚函数表指针指向的是该位置,而不是虚函数表的开头
--------------
A::funA2()
--------------

在非虚继承当中,基类的内存布局要在派生类中保证完整性,比如示例中 C 的内存布局可以拆分成两块,一块用来表示子对象A,一块用来表示子对象B。上面的 Offset 原始对象指针的偏移。通常多继承的情况下,第一个子对象在内存布局的最顶部,所以 Offset 为 0,但是之后其它子对象的 Offset 就不为 0 了, 比如示例中的 子对象 B ,其 Offset 就为 -16,子对象B的指针向上偏移16就得到了原始对象的指针,该字段在基类向派生类转换的时候会用到。

基类派生类转换

派生类->基类

首先要知道,派生类到基类的转换,百分百会成功,因为所谓的转换就是对指针进行调整,使其指向子对象的位置,这个动作编译器在编译期间就已经确定了。

void fun(C *c){
B *b = c; // 编译器会进行隐式转换,使指针B指向C内部子对象b的位置。
printf("c: %p\n", c); // c: 0x000000000010
printf("b: %p\n", b); // b: 0x000000000020
}

基类->派生类

但是当基类到派生类转换的时候,如果通过基类对象的地址找到其原本派生类对象的地址呢? 这就用到了前面提到的 Top Offset, 基类对象的地址,加上该偏移就得到了原始对象的地址。

int main() {
C c1;
B *b = &c1;
C *c2 = dynamic_cast<C *>(b);
printf("c1: %p\n", &c1); // c1: 0x000000000010
printf("b : %p\n", b); // b : 0x000000000020
printf("c2: %p\n", c2); // c2: 0x000000000010
}

访问虚函数

下面是一份通过内存布局访问虚函数的代码,在 Compiler Explore 上查看

#include <cstdio>

class A {
public:
long varA;
virtual void funA1(){ std::puts("A::funA1()");};
virtual void funA2(){ std::puts("A::funA1()");};
};

class B {
public:
int varB;
virtual void funB1(){ std::puts("B::funB1()");};
virtual void funB2(){ std::puts("B::funB2()");};
};

class C: public A, public B {
public:
int varC;
virtual void funA1(){ std::puts("C::funA1()");};
virtual void funB2(){ std::puts("C::funB2()");};
virtual void funC(){ std::puts("C::funC()");};
};

int main(int argc, char **argv){
C c;
using Fun = void (*)();
Fun *virtual_table = ((Fun**)&c)[0];

// virtual_table[-2] 内部子对象 A 的偏移
// virtual_table[-1] typeinf
virtual_table[0]();
virtual_table[1]();
virtual_table[2]();
virtual_table[3]();
// virtual_table[3] 内部子对象 B 的偏移
// virtual_table[4] typeinfo
virtual_table[6]();
virtual_table[7](); // thunk 间接调用
}

思考

如果 类 A 和类 B 都继承了一个 Base 类,那么 A 和 B 内部都有了 Base 类的成员。 那么 C 内部岂不是有两份 Base 的数据成员?怎么解决这个问题?这个就讲的了虚继承

虚继承

虚继承和普通继承的区别,简单来说有两点

  1. 新增加了一个 vtt 表,也就是虚函数表的表,里面存放的是虚函数表的地址。
  2. 虚函数表内在Top Offset上新增加了字段,用来表示内部虚拟子对象的偏移。

内存布局

下面是一个虚继承下的内存布局与虚函数表示例

注意到 VTT 中有几处空白没有列出来,那几个是构造函数虚表,有兴趣可自行了解。

访问虚函数表

下面是一份通过内存布局访问虚函数的代码,在 Compiler Explorer上查看

#include <cstdio>

class Base {
public:
long varBase{1};
virtual void funBase() { std::puts("Base::funBase"); };
};

class A : virtual public Base {
public:
long varA{2};
virtual void funA1() { std::puts("A::funA1"); };
virtual void funA2() { std::puts("A::funA2"); };
};

class B : virtual public Base {
public:
long varB{3};
virtual void funB1() { std::puts("B::funB1"); };
virtual void funB2() { std::puts("B::funB2"); };
};

class C : public A, public B {
public:
long varC{4};
virtual void funA1() { std::puts("C::funA1"); };
virtual void funB2() { std::puts("C::funB2"); };
virtual void funC() { std::puts("C::funC"); };
};

void printMemoryLayout(void *ptr) {

printf("[0]:%p, vtable_ptr_for_A\n", *((void **)ptr));
printf("[1]:%ld, varA\n", *((long *)((char *)ptr + 8))); // varA = 2
printf("[2]:%p, vtable_ptr_for_B\n", *((void **)ptr + 2));
printf("[3]:%ld, varB\n", *((long *)((char *)ptr + 24))); // varB = 3
printf("[4]:%ld, varC\n", *((long *)((char *)ptr + 32))); // varC = 4
printf("[5]:%p, vtable_ptr_for_Base\n", *((void **)ptr + 5));
printf("[6]:%ld, varBase\n", *((long *)((char *)ptr + 48))); // varBase = 1

using Fun = void (*)();
Fun *vtable_for_a = *((Fun **)ptr);
Fun *vtable_for_b = *((Fun **)ptr + 2);
Fun *vtable_for_base = *((Fun **)ptr + 5);

(*vtable_for_a)();
(*vtable_for_b)();
(*vtable_for_base)();
}

int main() {
C c;
printMemoryLayout(&c);
}

计算机底层技术名词概览

· 阅读需 12 分钟

晶体管

CPU 中的晶体管有 3 个连接端,其中一个输入端的电平高低能决定另外两端是否能导通。有两种型号的晶体管,一个高电平连通,一个低电平连通。

逻辑门

利用多个晶体管经过各种搭配就可以实现各种逻辑运算 (与,或,非,与非,或非,异或),这些门电路称为逻辑门。

加法器

门电路组合起来可以做加法器

算数逻辑单元 ALU

加法器可以经过扩展和修改,就有了乘法器,减法器,除法器。把它们打包在一起,加一些控制电路,就既可以做逻辑运算,也可以做算术运算。就是算术逻辑单元,是 CPU 中非常核心的部件。

指令集

CPU 中,不同的指令用不同的机器码表示,比如 0000 0001 表示加法,0000 0002 表示减法,所有的指令构成了指令集。对各种指令集进行配列组合已完成特定的功能的过程就是编程。

指令集又分为两个流派,一是精简指令集,指令长度固定,一个指令只完成一个基本操作。 一个是复杂指令集,一个指令可以完成一些复杂操作。x86 属于复杂指令集。

寄存器

CPU 工作过程中需要存储一些数据,比如要执行的指令地址,存储要计算的数据等,如果都在内存中读取,速度会很慢,所以 CPU 内部有一些电路用来存储数据叫做寄存器。

汇编语言

用一些助记符号代替机器码,比如 ADD 代表机器码 0000 0001,用助记符号来编程的语言就是汇编语言。

高级语言

助记符与 CPU 指令集相关联,人们有发明了高级语言,可以用接近人类语言的方式描述程序的功能,比如 int sum = a + b; 然后又发明了编译器,可以将高级语言变成机器指令,这个过程叫做编译。

指令执行过程

读取指令 -> 指令译码 -> 指令执行 -> 数据回写

流水线

指令执行过程分为了几个步骤,这几个步骤就可以用不同的电路来做,第一条指令执行到指令译码的时候,读取指令的电路已经开始读取第二条指令了,类似于工厂流水线。

流水线冒险

结构冒险,出现硬件资源竞争;
数据冒险,后面的指令等待前面的指令完成数据读写;
控制冒险,后面的指令需要根据前面的执行结果来决定下一步去哪执行;

缓存

读取某个数据的时候,其周边的数据也大概率会被访问,来回读取太费劲,CPU 内部就增加了一些电路用来保存一些内存的数据,这个技术叫做缓存

缓存行

CPU 通常以 64 字节大小为单元管理缓存,这个单元叫做缓存行

指令缓存和数据缓存

为了缓解流水线结构冒险问题,将缓存分为两块,分别存储数据和指令。

L1/L2/L3 缓存

之前的数据缓存和指令缓存那一层叫 L1 缓存,然后又增加了 L2 缓存。L1 和 L2 都是在一个 CPU 核心里。之后又加了一块较大的缓存,供所有 CPU 核心公用,称为 L3 缓存。

缓存失效

L2 的缓存是在各自的 CPU 核心里的,如果多个核心读取的是同一个缓存行的数据,会出现不一致的问题,需要一定的策略来保证缓存的一致性。

乱序执行

指令执行的时候,一些指令没有前后依赖关系,可以一起执行。之后再将执行结果进行重新排列。指令执行的过程变成了 指令分发到不同的执行单元 -> 多个执行单位乱序执行 -> 乱序执行结果重新排序

静态预测

为了缓解流水线控制冒险问题,先预测分支结果并执行某一分支,如果预测正确就将预测的执行结果拿来用,如果不正确就丢弃,执行另一分支。

动态预测

一些分支两边的概率并不是 50% 对 50%,统计最近多次的跳转结果,根据其最近跳转次数来进行预测,这个技术也叫分支预测。

SIMD

单指令多数据流 (Single Instruction Multiple Data)。 扩大一些寄存器的长度,比如 128 位,这样可以存储 4 个 32 位的整数。新增一些指令,可以并行计算这些数据。

超线程

每个 CPU 里都有一些不同的电路,比如整数运算电路,浮点型运算电路。不同指令用到的电路可能不一样,为了充分利用闲置的电路。新增加一套寄存器,内部协调好两套寄存器使用的缓存和 ALU。比如可以同时执行整数运算和浮点运算,对外看起来像是两个线程。 这个新增寄存器,复用缓存和计算资源的技术叫做超线程。

虚拟内存

程序访问的内存地址都不是真实的内存地址,访问真实地址内存的时候需要由一个 MMU (内存管理单元) 将地址映射到真实的内存。内存是按照页来管理的,通常一页为 4kb。

分页交换

系统内存资源有限,操作系统会将长时间不用的内存换到硬盘上,并做个标记,如果之后谁访问该内存,就会触发一个页错误中断,操作系统再去把那个页换回来。

虚拟地址翻译

虚拟内存一般是多级页表,虚拟地址到真实地址需要计算,这个过程叫虚拟地址翻译。

地址翻译缓存

和缓存行类似,翻译过的地址,接下来一段时间大概率还会被用到,缓存翻译地址能够减少翻译所做计算。

GPU 和 CPU

GPU 运算单元比较多,可以进行数据的批量计算,类似 CPU 的 SIMD。

操作系统

多程序并行

CPU 只有一个,由 CPU 控制程序控制许多程序在排队运行,如果某个程序在等待其他设备输入输出,CPU 就闲置了,可以让其他程序来运行

时钟中断

如果有程序在执行死循环,那么 CPU 控制程序也拿不到 CPU 控制权。为了解决这个问题,发明了一个 "中断" 技术,CPU 收到中断信号就停下来执行 CPU 控制程序。
为了能让 CPU 控制程序及时获取控制权,人们搞了一个中断源,周期性的发送中断信息,叫做时钟中断。

时间分片

时钟中断间隔就是时间分片,每个程序只能执行一小段时间。

状态

有些程序在 sleep 状态,有些程序在等待状态,这样也会分配时间片,但是时间片到了,它们什么也不做白白浪费时间。人们又把程序划分为不同的状态,只有准备就绪的程序才会分到时间片。

状态有创建,就绪,执行,阻塞,终止...

优先级

有些程序对实时性要求较高,人们有搞了优先级队列,如果有高优先级的程序出现,即使低优先级的程序时间片没用完,也会被剥夺执行机会。

进程地址空间

每个进程看到的地址空间都是虚拟的。访问虚拟地址的时候,MMU 会把他映射到真实物理地址。

进程和线程

一开始进程只有一个执行流,想要并发,就只能创建多个进程。但是进程间通信不是很方便。于是工程师们就琢磨一个线程里搞多个执行流,也就是多个线程。

每个线程都有自己的执行上下文和堆栈,互不影响。最重要的是,这些线程看到的地址空间都是同一个,线程之间通信就方便多了。

现在操作系统的最小调度单位,由进程变成了线程。

系统调用

操作系统吧管理文件,内存,网络,进程,线程,还有管理硬件设备等资源的操纵封装成一个个函数,以供应用程序调用。这些较低层的接口就是系统调用。

系统调用表

系统调用表就是所有的系统调用接口的集合,每个接口有一个系统调用号。

系统调用号用户空间接口内核空间接口
0readsys_read
1writesys_write
2opensys_open
3closesys_close
..................

在早起的 x86 架构中,系统调用是通过 int 0x80 软中断来进行的。但软中断需要再内存中查找中断表,为提升性能,在 x86-64 中优化了系统调用,并且提供了专门的系统调用指令 syscall

中断描述符表

中断描述符表(IDT,Interrupt Descriptor Table)存储了所有的中断和异常,以及其发生时候的处理程序信息,

信号

每个进程都有一个信号处理表。用户可以注册自己的信号处理函数,当某个信号发生时候,按照编号取出表里的函数地址,调用就可以了。

多线程中的信号处理

每个 task_struct 中的信号等待队列只存放线程自己的信号,另外单独设置队列存放进程的信号,所有的线程共享 。

发送信号的时候有个 group 参数用来决定是投给进程还是投递给线程。比如 kill 是发送给进程的,tkill 是发送给线程的。

进程中的信号有哪个线程进行处理呢,只要线程没有屏蔽信号,它都有机会去处理信号,先到先得。

处理信号的函数表格只有一份,是整个进程共享的。有线程修改的话,所有线程都会有影响。

原子操作

比如 i++, 有 3 个步骤 读数据,加 1,写数据,这三个步骤不能被拆分,中途不能被打断。这样的操作就原子操作。

自旋锁

锁有个状态标记当前有没有被占用,获取锁的函数内部不断循环的尝试获取锁。因为获取锁的时候线程会一直循环的检查状态,所以叫自旋锁。

自旋锁一直阻塞自旋,没有让出 CPU,只适合快速处理的场合。

互斥锁

获取锁的时候,把自己放进锁的等待队列中去,然后就让出 CPU 权限,进入睡眠,等到锁被其他线程释放的时候,再去唤醒等待队列里的线程,进行运行。

条件变量

等待条件变量的线程平时阻塞着,只有满足条件的时候,条件变量才被激活,等待的线程才会被唤醒。

信号量

升级版的互斥锁,可以指定最多允许多少个线程获取锁

chroot 和 pivot_root

通过这两个可以设置一个进程的根目录为指定目录,容器就是通过这两个限制容器进程的活动范围。

命名空间

命名空间相互独立,空间内的进程,用户,网络等,对空间外不可见。命名空间有好几个分别管理不同的资源,比如 PID 管理进程 id;网络命名空间管理网络接口,IP 地址,路由表等;UTS 管理主机名和域名; IPC 管理消息队列,共享内存等;User 管理用户和用户组;

Cgroup

Cgroup 和命名空间类似,可以通过划组限制每个分组的使用资源

进程 fork

fork 进程的时候,会将进程的结构体 task_stuct 拷贝一份,创建一个全新的进程地址空间和堆栈。

写时拷贝

进程地址空间都是虚拟的,新 fork 的进程内存页面和父进程的内存页面映射到了同一个物理内存页面上。只有进程尝试修改内存的时候,内核再重新复制一份。

线程会被 fork 吗

fork 创建子进程的时候只会拷贝当前线程,其他线程不会被拷贝。

进程间通信

信号,信号只能作为通知使用,不能携带数据

套接字,127.0.0.1 只走协议栈,不走网卡。

匿名管道,匿名管道需要有血缘关系的进程才能通信

消息队列

共享内存,将同一块物理内存分别映射到不同进程的地址空间中

I/O 多路复用

select,监听批量描述符,有监听上限,通常是 1024,内核把监听的文件描述符拷贝到内核空间,然后遍历,没有数据的话就进入睡眠。然后在数据可读或者超时的时候被唤醒。

poll,和 select 差不多,只不过是解决了监听描述符的数量限制。

epoll, 有个就绪队列,可读的文件都会进入这个队列中去,只需要处理这个队列就行。epoll 有两个模式,默认的水平触发模式,有数据就就一直触发。边缘模式,只触发一回,需要用户及时取走所有数据。

mmap

像 cpu 访问内存的时候有缓存一样,硬盘里的数据在内存中也有一份缓存。这样读写文件的时候会拷贝两次,从硬盘到内核缓存页,从内核缓存页到用户缓存。

mmap 把内核地址空间的缓存页和用户地址空间的缓存页映射到同样的物理内存中去,这个减少了一次内存拷贝。

协程

在用户空间实现的类似于操作系统的进程调度。

同时调用多个版本的"相同"函数

· 阅读需 2 分钟

在软件开发中,版本管理至关重要。但是,某些极端情况,需要同时运行一个接口的不同版本的函数,那么有没有可能实现呢。

比如下面有两个版本的 fun 函数,他们函数签名,都完全一样,能在一个程序中同时调用这两个函数吗?

// v1/fun.cpp
const char* fun() {
return "fun version 1";
}

// v2/fun.cpp
const char* fun() {
return "fun version 2";
}

在上述代码中,我们定义了两个版本的函数 fun(),分别位于目录 v1/v2/ 下。接下来,我们将这两个版本编译成共享库(.so 文件):

g++ -shared -fPIC -o v1/fun.so v1/fun.cpp
g++ -shared -fPIC -o v2/fun.so v2/fun.cpp

然后,我们在 main.cpp 中调用这两个共享库中的 fun 函数:

#include <dlfcn.h>
#include <iostream>

typedef const char* (*fun_ptr)();

int main() {
// 打开 v1 版本的共享库
void* handle_v1 = dlopen("v1/fun.so", RTLD_LAZY);
fun_ptr fun_v1 = reinterpret_cast<fun_ptr>(dlsym(handle_v1, "_Z3funv"));

// 打开 v2 版本的共享库
void* handle_v2 = dlopen("v2/fun.so", RTLD_LAZY);
fun_ptr fun_v2 = reinterpret_cast<fun_ptr>(dlsym(handle_v2, "_Z3funv"));

// 调用并输出两个版本的函数
std::cout << "Calling v1/fun(): " << fun_v1() << std::endl;
std::cout << "Calling v2/fun(): " << fun_v2() << std::endl;

// 关闭共享库
dlclose(handle_v1);
dlclose(handle_v2);

return 0;
}

在上述代码中,我们使用了 dlsym 函数来获取函数指针,注意参数中的符号是 _Z3funv,而不是函数名fun。在编译并运行程序之前,你可以通过 nm v1/fun.so 查看符号表以获取正确的符号名。有关符号修饰请查看 符号修饰 最后,编译并运行程序:

g++ main.cpp -ldl
./a.out

通过以上步骤,你将能够看到以下输出结果:

Calling v1/fun(): fun version 1
Calling v2/fun(): fun version 2

通过 dl 库,我们发现在一个程序中同时调用这一个函数的不同版本是可行的。(实际项目中一定要避免这种情况,做好版本管理)

上述示例代码可在 https://github.com/wenyg/dynamic_link_example 查看

常见工具国内镜像源

· 阅读需 1 分钟

docker 镜像

/etc/docker/daemon.json 添加以下内容

{
"registry-mirrors":[
"https://registry.docker-cn.com",
"https://docker.mirrors.ustc.edu.cn/",
"https://mirror.ccs.tencentyun.com/"
]
}

ubuuntu apt

/etc/apt/sources.list 替换为 清华源

sed -i 's#http://\(archive\|security\).ubuntu.com/ubuntu/#http://mirrors.tuna.tsinghua.edu.cn/ubuntu/#g' /etc/apt/sources.list

npm

npm config set registry https://registry.npmmirror.com

pip

python -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple --upgrade pip
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

oh-my-zsh

git clone https://mirrors.tuna.tsinghua.edu.cn/git/ohmyzsh.git
cd ohmyzsh/tools
REMOTE=https://mirrors.tuna.tsinghua.edu.cn/git/ohmyzsh.git sh install.sh

C++小实验,两个线程交替执行

· 阅读需 1 分钟

cpp小实验,两个线程,一个线程负责将变量减一,另一个线程负责打印线程。

#include <thread>
#include <condition_variable>
#include <mutex>
#include <iostream>

int global_value = 10;
bool ready_to_reduce{false};
std::mutex mtx;
std::condition_variable cv_reduce;
std::condition_variable cv_print;

void producer() {
while(true) {
std::unique_lock<std::mutex> lock(mtx);
cv_reduce.wait(lock,[](){
return ready_to_reduce;
});
global_value--;
ready_to_reduce = !ready_to_reduce;
cv_print.notify_one();
if (global_value == 0) {
break;
}
}
}

void consumer() {
while(true) {
std::unique_lock<std::mutex> lock(mtx);
cv_print.wait(lock,[] () {
return !ready_to_reduce;
});
std::cout << global_value << std::endl;
if (global_value == 0) {
break;
}
ready_to_reduce = true;
cv_reduce.notify_one();
}
}

int main() {
auto t1 = std::thread(producer);
auto t2 = std::thread(consumer);
t1.join();
t2.join();
return 0;
}

VSCode 容器中开发相关配置

· 阅读需 2 分钟

本文主要讲解在容器中利用 VSCode 进行开发的一些配置, 容器的优势这里不再赘述,假设你已具备了镜像构建以及其基本操作

容器环境准备

预设以下条件

  • 代码工程目录放置在 /workspace/my_project
  • 开发环境镜像在 my_image (这里假设是一个ubuntu镜像)
cd /workspace/my_project

docker run -ti --entrypoint=/bin/bash \
--net=host \
--ipc=host \
-v $(pwd):/$(basename $(pwd)) \
-v $(pwd)/.vscode-server:/root/.vscode-server \
--privileged \
--name $(basename $(pwd))_dev \
my_image

以上会启动一个 my_project_dev 容器,并将代码挂载到了容器内的 /my_project

  • -v $(pwd)/.vscode-server:/root/.vscode-server vscode 容器开发会在容器内的 $HOME/.vscode-server 里安装一些资源或者文件,包括容器内的插件也在这里,提前挂载进去是将这部分持久化,避免重新安装

Remote-SSH 配置

另一种方法,是在容器里启一个ssh服务,然后用ssh远程连接,和连接远程服务器一样,下面是 容器内远程连接的一些说明以及配置

  1. 容器要 --net=host 启动,且要将代码挂载进容器内
  2. 要选择一个容器 ssh 端口,比如 2222,连接容器内的时候要指定端口。(22通常是宿主机的 ssh 端口)

在容器安装ssh服务并开启,下面是 ssh-rsa xxxxxxxxxxxxxxxxxxxxx 是物理机的公钥, 用于免密登录,需要替换成自己的值。

公钥通常在 $HOME/.ssh/id_rsa.pub 下,可通过 ssh-keygen 生成。

apt install openssh-server -y
mkdir /var/run/sshd
echo "ssh-rsa xxxxxxxxxxxxxxxxxxxxx" > /root/.ssh/authorized_keys

/usr/sbin/sshd -p 2222

物理机验证

ssh -p 2222 root@my_ip

验证成功后就可以使用vscode远程连接了。

C++对象模型(5): 关于数据

· 阅读需 2 分钟
  • 空类也有一字节的大小,因为这才能对对象取地址
  • 一个对象的内存布局通常由三部分组成
    1. 非静态成员变量
    2. 内存对齐所填补的空间
    3. 为了支持 virtual 机制而引起的额外负担,比如 vptr
  • 传统上,vptr 被安置到所有明确声明的 member 后面,但要看具体编译器实现
  • 直观上,通过对象取值 object.xxx 比指针取值 object_point->xxx 更方便,但实际上是完全一样的,都会被编译器扩展
  • 编译器编译的时候会区分
    • 执行data member的指针,指向第一个member (object::member - 1)
    • 指向data member的指针,但是没有指向任何 member
  • 下面两种方式,有什么区别
    X x; 
    x.x == 0.0;

    X *px;
    px->x = 0.0;
    当 X 中含有一个虚基类, 而且 x 正好是虚基类中的成员时,有重大区别。前者可以编译期确定 x 的 offset,但是 px 只能再运行期确定
  • 支持多态带来的负担
    1. 导入virtual table 用来存放 virtual function 地址, 这个table的元素数量一般是virtual function的数目再加上一个或两个slots(用于支持RTTI)
    2. 在每一个object中安插一个 vptr 指向 virtual table
    3. 在 constructor 中安插代码用于设置 vptr
    4. 在 deconstructor 中安插代码用于抹除 vptr

C++对象模型(3): 关于数据

· 阅读需 2 分钟
  • 空类也有一字节的大小,因为这才能对对象取地址
  • 一个对象的内存布局通常由三部分组成
    1. 非静态成员变量
    2. 内存对齐所填补的空间
    3. 为了支持 virtual 机制而引起的额外负担,比如 vptr
  • 传统上,vptr 被安置到所有明确声明的 member 后面,但要看具体编译器实现
  • 直观上,通过对象取值 object.xxx 比指针取值 object_point->xxx 更方便,但实际上是完全一样的,都会被编译器扩展
  • 编译器编译的时候会区分
    • 执行data member的指针,指向第一个member (object::member - 1)
    • 指向data member的指针,但是没有指向任何 member
  • 下面两种方式,有什么区别
    X x; 
    x.x == 0.0;

    X *px;
    px->x = 0.0;
    当 X 中含有一个虚基类, 而且 x 正好是虚基类中的成员时,有重大区别。前者可以编译期确定 x 的 offset,但是 px 只能再运行期确定
  • 支持多态带来的负担
    1. 导入virtual table 用来存放 virtual function 地址, 这个table的元素数量一般是virtual function的数目再加上一个或两个slots(用于支持RTTI)
    2. 在每一个object中安插一个 vptr 指向 virtual table
    3. 在 constructor 中安插代码用于设置 vptr
    4. 在 deconstructor 中安插代码用于抹除 vptr

SFINAE应用场景: 检测成员

· 阅读需 3 分钟

在C++编程中,Substitution Failure Is Not An Error(SFINAE)是一种强大的技术,允许我们在编译时根据类型特征选择不同的实现。这个特性在许多现代C++库和框架中被广泛使用,其中之一是在处理不同结构的通用接口时。

让我们看一个简单而实用的例子,通过检查类型是否具有特定成员来优雅地处理不同结构的通用接口。在这个例子中,我们将使用SFINAE来检查类型是否包含名为 'header' 的成员,并相应地执行不同的处理。

#include <iostream>
#include <type_traits>

// 首先,让我们定义一个模板结构 HasHeader,该结构通过SFINAE技术检查是否存在 'header' 成员。
template <typename T, typename = void>
struct HasHeader : std::false_type {};

template <typename T>
struct HasHeader<T, std::void_t<decltype(std::declval<T>().header)>> : std::true_type {};

// 接下来,我们定义一个使用SFINAE的函数模板 process_header,该模板在类型具有 'header' 成员时执行特定处理。
template <typename T>
std::enable_if_t<HasHeader<T>::value> process_header(const T &data) {
std::cout << "处理 header: " << data.header << std::endl;
}

template <typename T>
std::enable_if_t<!HasHeader<T>::value> process_header(const T &data) {
std::cout << "没有要处理的 header。" << std::endl;
}

// 最后,我们定义两个示例类,一个具有 'header' 成员,另一个没有。
struct ExampleWithHeader {
int header = 42;
};

struct ExampleWithoutHeader {};

// 在 main 函数中,我们使用 process_header 函数处理这两个示例类的实例。
int main() {
ExampleWithHeader obj1;
ExampleWithoutHeader obj2;

process_header(obj1); // 输出: 处理 header: 42
process_header(obj2); // 输出: 没有要处理的 header。

return 0;
}

在实际项目中,这种技术可以用于实现泛型算法、库组件和其他需要在编译时进行类型分派的场景

C++ Template 小课堂

decltypestd::declvalstd::enable_if_t 简要解释:

  1. decltype:
    • 作用: decltype 用于获取表达式的类型,而不执行该表达式。
    • 用法: decltype(expr) 返回表达式 expr 的类型。通常在模板编程中用于推导类型。
  2. std::declval:
    • 作用: std::declval 用于生成对于任何类型 T 的右值引用(rvalue reference)。通常与 decltype 一起使用,以在不创建对象的情况下获取类型信息。
    • 用法: std::declval<T>() 返回类型 T 的右值引用。
  3. std::enable_if_t:
    • 作用: std::enable_if_t 用于在编译时根据条件启用或禁用模板特化。
    • 用法: std::enable_if_t<Condition, Type> 如果 Condition 为真,则定义 Type;否则,不定义。

C++: std::decay_t

· 阅读需 1 分钟

std::decay_t是一个类型转换工具,它接受一个类型,并将其转换为对应的"衰减类型"。所谓"衰减类型"指的是将某个类型以如下方式处理得到的类型:

  • 对于数组或函数类型,将其转换为指针类型。
  • 对于const/volatile限定符和引用类型,去除这些修饰符,得到被修饰类型本身。
  • 对于非上述类型,保持其原样。

以下是一个使用std::decay_t的例子:

#include <type_traits>

int main() {
int arr[3] = {1, 2, 3};
using type1 = std::decay_t<decltype(arr)>; // type1会被推导为int*

const volatile double& ref = 42.0;
using type2 = std::decay_t<decltype(ref)>; // type2会被推导为double

struct S {};
using type3 = std::decay_t<S>; // type3会被推导为S
}

tar 带密码压缩

· 阅读需 2 分钟

比如对 app 目录进行压缩备份,带上密码,可以使用以下操作

tar -czvf - /app | openssl enc -aes-256-cbc -e > backup.tar.enc

解释一下以上命令:

  • tar -czvf - /app 将/app目录压缩成一个tar包,并将其输出到stdout。
  • openssl enc -aes-256-cbc -e 将stdin中的数据用AES-256-CBC算法进行加密,并将加密后的数据输出到stdout。
  • > backup.tar.enc 将stdout中的数据输出到backup.tar.enc文件中。

在执行以上命令时,会提示输入密码,输入密码后就会生成加密后的backup.tar.enc文件。要解密该文件,可以使用以下命令:

openssl enc -aes-256-cbc -d -in backup.tar.enc | tar -xzvf -

解释一下以上命令:

  • openssl enc -aes-256-cbc -d -in backup.tar.enc 从backup.tar.enc文件中读取加密后的数据,并使用AES-256-CBC算法进行解密,将解密后的数据输出到stdout。
  • tar -xzvf - 将stdin中的数据解压缩,并将解压缩后的数据输出到stdout。 通过将以上两个命令结合使用,就可以对/app目录进行加密备份和解密恢复了。

不使用交互式

密码直接写在命令行中

压缩

tar -czvf - /app | openssl enc -aes-256-cbc -e -pass "pass:my_super_secret_password" > backup.tar.enc

解压

openssl enc -aes-256-cbc -d -in backup.tar.enc -pass "pass:my_super_secret_password" | tar -xzvf -

Shell 使用技巧

· 阅读需 2 分钟

修改全局变量

#!/bin/bash

my_var="initial value"

my_function() {
# 声明 my_var 为全局变量
declare -g my_var
my_var="new value"
}

echo "Before function call: my_var=$my_var"
my_function
echo "After function call: my_var=$my_var"

获取脚本所在绝对路径

脚本中涉及到一些路径的时候可以用绝对路径,这样可以再任意地方执行脚本。而不用 cd 到固定的目录

## 获取脚本自身所在目录绝对路径
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
echo "SCRIPT_DIR: $SCRIPT_DIR"

文件路径相关操作

# 获取目录名
DIR=$(dirname /path/file.txt) # /path

# 获取文件名
FILE=$(basename /path/file.txt) # file.txt

# 获取文件名,不带文件类型后缀
FILE_NAME=${FILE.*} # file

获取环境变量的值

# 获取环境变量 VAR 的值,如果没有则是 default_value
VAR="${VAR:-default_value}"

多行字符串换行符处理

某些 CI/CD 流程中可能会涉及到一些多行字符串,需要对换行符进行特殊处理,比如转义

# 原始字符串
original_string=" 这是一个包含
换行符的
字符串 "

# 将换行符替换成 ###
escape_string=$(echo "$original_string" | sed ':a;N;$!ba; s/\n/###/g')
echo $escape_string
# 会输出 "这是一个包含 ### 换行符的 ### 字符串"

后台执行程序

$(COMMOND &)

nohup COMMOND &

C++: 进程间同步之共享内存

· 阅读需 3 分钟

为了实现多进程间的数据同步,我们可以使用进程间通信(IPC)机制。下面是一个使用共享内存实现多进程间数据同步的示例代码:

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>

using namespace std;

// 共享内存的结构体
struct SharedMemory {
int request_count; // 请求数量
};

int main() {
const char* kSharedMemoryName = "/my_shared_memory"; // 共享内存名称
const int kSharedMemorySize = sizeof(SharedMemory); // 共享内存大小

// 创建或打开共享内存
int shm_fd = shm_open(kSharedMemoryName, O_CREAT | O_RDWR, 0666);
if (shm_fd == -1) {
perror("shm_open");
exit(1);
}

// 调整共享内存大小
if (ftruncate(shm_fd, kSharedMemorySize) == -1) {
perror("ftruncate");
exit(1);
}

// 映射共享内存到当前进程的地址空间
void* shared_memory = mmap(NULL, kSharedMemorySize, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
if (shared_memory == MAP_FAILED) {
perror("mmap");
exit(1);
}

// 关闭文件描述符,不再需要
close(shm_fd);

// 初始化共享内存的内容
SharedMemory* p_shared_memory = reinterpret_cast<SharedMemory*>(shared_memory);
p_shared_memory->request_count = 0;

// 模拟多进程并发请求的情况
const int kNumProcesses = 4;
for (int i = 0; i < kNumProcesses; i++) {
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(1);
} else if (pid == 0) {
// 子进程逻辑
for (int j = 0; j < 10000; j++) {
// 模拟一次请求
p_shared_memory->request_count++;
}
exit(0);
}
}

// 父进程等待所有子进程结束
for (int i = 0; i < kNumProcesses; i++) {
wait(NULL);
}

// 输出最终请求数量
cout << "Total request count: " << p_shared_memory->request_count << endl;

// 解除共享内存映射
if (munmap(shared_memory, kSharedMemorySize) == -1) {
perror("munmap");
exit(1);
}

// 删除共享内存
if (shm_unlink(kSharedMemoryName) == -1) {
perror("shm_unlink");
exit(1);
}

return 0;
}

在这个示例代码中,我们使用了 shm_open、ftruncate、mmap、munmap 和 shm_unlink 函数来操作共享内存。其中,shm_open 函数用于创建或打开共享内存,ftruncate 函数用于调整共享内存的大小,mmap 函数用于映射共享内存到当前进程的地址空间,munmap 函数用于解除共享内存映射,shm_unlink 函数用于删除共享内存。

在初始化共享内存的内容后,我们创建了多个子进程,并在每个子进程中模拟了一定数量的请求,这些请求会增加共享内存中的请求数量。最后,父进程等待所有子进程结束后,输出最终的请求数量,并删除共享内存。

需要注意的是,使用共享内存进行进程间通信需要确保不同进程对共享内存中的数据进行访问时不会产生竞态条件。在本示例代码中,我们使用了共享内存的结构体成员来保存请求数量,这样可以避免多个进程同时修改同一个变量的问题。同时,由于共享内存是一块共享的内存区域,因此不同进程可以通过相同的指针来访问共享内存中的数据。

需要注意的是,共享内存虽然是一种高效的进程间通信方式,但是由于多个进程共享同一块内存,因此容易产生数据一致性问题,需要采取相应的同步措施来避免这些问题的出现。在本示例代码中,我们没有使用任何同步措施,因为对于只有一个变量的情况下,并发修改不会导致数据不一致的问题。但是,在实际情况下,如果需要对多个变量进行并发修改,就需要使用锁或者其他同步机制来保证数据的一致性。

C++: 右值引用和移动构造

· 阅读需 4 分钟

C++ 11 引入了移动语义和右值引用,使得代码的性能得到了大幅提升。其中,移动构造函数是一种特殊的构造函数,它可以接受一个右值引用参数,用来实现将一个对象的资源转移到另一个对象而不需要复制数据的操作。本文将详细介绍移动构造函数和右值引用。

右值引用

在 C++ 11 中,引入了新的类型:右值引用。右值是指一个临时对象,它只能存在于表达式的右边,并且其生命周期只能存在于这个表达式内部。右值引用则是指对右值进行引用的方式,其使用的语法是在类型名后面加上 &&,例如:

int&& a = 5;

在这个例子中,a 是一个右值引用,它引用了一个临时的 int 类型对象。由于这个对象是一个右值,所以它的生命周期只存在于这个语句中。右值引用的主要作用是实现移动语义。

移动构造函数

移动构造函数是一个特殊的构造函数,它接受一个右值引用参数,用来实现将一个对象的资源转移到另一个对象而不需要复制数据的操作。移动构造函数通常用于容器类、智能指针等需要管理资源的类中,以提高程序的性能。

移动构造函数的语法如下:

class MyClass {
public:
MyClass(MyClass&& other) noexcept {
// 资源转移代码
}
};

在这个例子中,MyClass 的移动构造函数接受一个右值引用参数 other,用来接收另一个 MyClass 对象的资源。在移动构造函数中,可以将 other 对象的资源移动到当前对象中,而不需要进行深拷贝或浅拷贝。由于移动构造函数的特殊性质,其通常会使用 noexcept 关键字来标记其不会抛出异常,从而使得代码更加高效。

使用移动构造函数

使用移动构造函数通常需要遵循一些规则:

  • 对象必须是右值:只有右值可以通过移动构造函数来进行资源的转移。如果需要移动一个左值,可以使用 std::move 函数来将其转换为右值。
  • 转移后原对象的状态是未定义的:移动构造函数会将原对象的资源转移到新对象中,原对象的状态是未定义的。因此,在使用移动构造函数后,原对象不能再被使用,否则可能会引发未定义行为。

下面是一个使用移动构造函数的例子:

class MyClass {
public:
MyClass() : data_(new int[100]) {}
MyClass(MyClass&& other) noexcept : data_(other.data_) {
other.data_ = nullptr;
}

private:
int* data_;
};
MyClass func() {
MyClass a;
// ...
return std::move(a);
}

int main() {
MyClass b = func();
return 0;
}

在这个例子中,func 函数返回一个 MyClass 对象,由于返回值是一个临时对象,因此可以通过移动构造函数来转移资源。在 main 函数中,通过使用 std::move 函数将返回值转换为右值,从而使得可以使用 MyClass 的移动构造函数来进行资源的转移。这样,就可以避免不必要的数据复制,从而提高程序的性能。

总结

移动构造函数可以用来实现将一个对象的资源转移到另一个对象而不需要复制数据的操作,从而提高程序的性能。使用移动构造函数需要遵循一些规则,例如对象必须是右值,转移后原对象的状态是未定义的等。通过掌握移动构造函数和右值引用,可以编写更加高效的 C++ 代码。

程序员的自我修养: 延迟绑定

· 阅读需 2 分钟

调动动态库中的函数时候,涉及到地址重定位,函数地址在链接的时候才能确定下来。链接器会额外生成两张表,一个 PLT(Procedure Link Table) 程序链接表,一个是 GOT(Global Offset Table) 全局偏移表,两张表都在数据段中。

  • 全局偏移表,用来存放 "外部的函数地址"。
  • 程序链接表,用来存放 "获取外部函数地址的代码"。

比如简单的函数调用

printf("Hello world\n");

实际上会先调用 plt 表

call printf@plt

printf@plt 的调用如下

jmp *printf@got

printf@got 的地址就是真正的 printf 地址。

延迟绑定

要使得所有的函数能正常调用, GOT 表中就要填入正确的函数地址。如果一开始就对所有函数就进行重定位显然效率上会有问题。所以 linux 引入延迟绑定机制,即只有在首次调用函数的时候才进行重定位

假设 GOT[4]是 用来存放 printf 的函数的地址. 延迟绑定中 printf@plt 执行过程大致如下。

jmp *GOT[4]
push $0x1
jump PLT[0]

*GOT[4] 初始值是其PLT指定的第二条指令地址(push $0x1),即又跳了回来。 PLT[0] 是一个特殊条目,它跳到动态链接器中,解析函数地址,并且重写 GOT 表,然后再调用函数。

显然经过这次步骤之后,GOT[4] 已经写入了 printf 的地址,下次调用 printf@plt 的时候会直接跳转到 printf。

C++: std::condition_variable

· 阅读需 3 分钟

不要无条件的等待。否则可能会错过唤醒,或者唤醒了发现无事可做。

condition_variable.wait() 有两个重载函数

  • void wait (unique_lock& lck)。 无条件的等待
  • void wait (unique_lock& lck, Predicate pred)。 有条件的等待. 大致实现如下
    template<typename _Predicate>
    void wait(unique_lock<mutex>& __lock, _Predicate __p)
    {
    while (!__p())
    wait(__lock);
    }

下面来看个简单的例子

#include <condition_variable>
#include <iostream>
#include <thread>

std::mutex mutex_;
std::condition_variable condVar;

bool dataReady{false};

void waitingForWork(){
std::cout << "Waiting " << std::endl;
std::unique_lock<std::mutex> lck(mutex_);
condVar.wait(lck, []{ return dataReady; }); // (4)
std::cout << "Running " << std::endl;
}

void setDataReady(){
{
std::lock_guard<std::mutex> lck(mutex_);
dataReady = true;
}
std::cout << "Data prepared" << std::endl;
condVar.notify_one(); // (3)
}

int main(){

std::cout << std::endl;

std::thread t1(waitingForWork); // (1)
std::thread t2(setDataReady); // (2)

t1.join();
t2.join();

std::cout << std::endl;

}

为什么一个如此简单的程序会看起来都会这么麻烦呢。如果不使用 dataReady 会怎样呢?

先看看正常情况下 上面 (4)语句

condVar.wait(lck, []{ return dataReady; });

如果 []{ return dataReady; } 为真, 那么程序继续运行。 如果 []{ return dataReady; } 为假, condVar 会释放锁,然后阻塞自己,等待其他线程的 notify 信号。

如果接下来,condVar收到了其他线程的 notify 信号被唤醒。(也有可能被虚假唤醒)线程会被唤醒,并获取锁,并检查 []{ return dataReady; } 的结果。跟之前一样,根据结果来决定接下来的行为。

回到最开始的问题,如果不使用 dataReady 会怎样的呢, 即无条件的等待。

void waitingForWork(){
std::cout << "Waiting " << std::endl;
std::unique_lock<std::mutex> lck(mutex_);
condVar.wait(lck); // (1)
std::cout << "Running " << std::endl;
}

void setDataReady(){
std::cout << "Data prepared" << std::endl;
condVar.notify_one();
}

可能会发生如下现象。

  1. waitingForWork 在收到信号之前,setDataReady 线程中已经发出了 notify 信号。那么 waitingForWork 会永远出于阻塞状态,看起来像死锁一样。

用不加锁的变量控制也一样

std::atomic<bool> dataReady{false};

void waitingForWork(){
std::cout << "Waiting " << std::endl;
std::unique_lock<std::mutex> lck(mutex_);
condVar.wait(lck, []{ return dataReady.load(); }); // (1)
std::cout << "Running " << std::endl;
}

void setDataReady(){
dataReady = true;
std::cout << "Data prepared" << std::endl;
condVar.notify_one();
}

如果执行到 condVar.wait(lck, []{ return dataReady.load(); }); 的时候。 信号的通知的发生在执行 []{ return dataReady.load() 和 获取到锁之间。 那么可以想象的到。即执行线程顺序如下

  • setDataReady 线程 dataReady = true;
  • waitingForWork 线程 []{ return dataReady.load(); }
  • setDataReady 线程 condVar.notify_one();
  • waitingForWork condVar.wait(lck,

很明显,信号通知也会被错过,程序会永远阻塞。原因在于 dataReady 的修改并没有正确的同步到,如果dataReady 在加锁的情况下修改,就不会发生这种现象。

其他

unique_lock/lock_guard

lock_guard 是一个 scope 锁。创建时获取锁,离开作用域是自动解锁。由于只有在析构的时候才会解锁,如果这个定义域比较大的话,那么锁的粒度就比较大,可能会影响程序效率。所以尽可能的是定义域小。

unique_lock 与lock_guard一样,但它额外提供了一个 unlock() 来主动解锁。

notify_one/notify_all

notify_one 只唤醒等待队列中的第一个线程。其余线程只能等待下次 notify_xxx

notify_all 所有线程都会被唤醒。所有线程争用锁,并执行接下里的任务,然后释放锁。

参考链接

https://www.modernescpp.com/index.php/c-core-guidelines-be-aware-of-the-traps-of-condition-variables

C++: std::bind

· 阅读需 3 分钟

在C++11中,引入了一个新的库 functional ,其中包括了许多有用的函数对象和函数适配器。其中之一是 std::bind。

std::bind 它可以绑定一个函数或成员函数的参数,并返回一个可调用对象。这意味着可以将参数延迟到稍后再使用。

基本用法

下面是一个简单的例子,展示了如何使用 std::bind 实现参数绑定:

#include <iostream>
#include <functional>

void sum(int a, int b)
{
std::cout << "sum: " << a + b << std::endl;
}

int main()
{
auto add_five = std::bind(sum, std::placeholders::_1, 5); // 绑定第二个参数为5
add_five(10); // 输出:sum: 15
return 0;
}

在这个例子中,我们创建了一个名叫 add_five 的可调用对象,它实际上是 sum 函数的参数绑定版本。我们使用 std::bind 将第一个参数绑定为 _1(占位符),第二个参数绑定为 5,以此类推。最终,我们可以调用 add_five 并将一个整型值作为第一个参数传入,生成最终结果。

可绑定对象

除了普通函数外,std::bind 还可以接收能够被调用的任何对象,例如函数对象和成员函数。

#include <iostream>
#include <functional>

class A
{
public:
void print(int n, char c)
{
std::cout << "print called: " << n << ", " << c << std::endl;
}
};

int main()
{
auto a = A();
auto func = std::bind(&A::print, &a, 10, std::placeholders::_1); // 绑定第二个参数为1
func('A'); // 输出:print called: 10, A
return 0;
}

在这个例子中,我们创建了一个名为 a 的 A 类实例,并将其地址传递给 std::bind。我们还绑定了 print 成员函数的第一个参数为 10,第二个参数使用 _1 占位符。最后我们可以调用 func 函数,并为其传递一个 char 类型的参数,生成最终结果。

组合函数

还可以使用 std::bind 来组合多个函数。

#include <iostream>
#include <functional>

int sum(int a, int b)
{
return a + b;
}

int mul(int a, int b)
{
return a * b;
}

int main()
{
auto func = std::bind(sum, std::bind(mul, std::placeholders::_1, 5), 10);
std::cout << "Result: " << func(2) << std::endl; // 输出:30
return 0;
}

在这个例子中,我们首先将第一个参数与 5 相乘,然后将结果与 10 相加。我们使用 std::bind 将 mul 函数的第二个参数绑定为 5。最终,我们调用 func(2) 并得到 30 作为输出。

使用 std::placeholders 占位符

当使用 std::bind 时,需要使用 _1, _2, _3, ... _N 等占位符来表示绑定的函数的参数。

auto add_five = std::bind(sum, std::placeholders::_1, 5);

在这个例子中,_1 表示将要绑定的函数的第一个参数。

另外,如果想对多个参数进行绑定,则需要使用不同的占位符:

在这个例子中,_1 表示原函数的第一个参数,_2 表示原函数的第二个参数。

C++: std::function

· 阅读需 2 分钟

std::function 是一个函数包装器,它可以包装任何可调用函数实体。

  • 函数
  • 函数指针
  • 成员函数
  • 静态函数
  • lambda 表达式
  • 函数对象

函数对象赋值方法

#include <functional>
#include <iostream>
#include <type_traits>

int func(int a){
return a;
}

class FuncClass{
public:
int operator()(int a){
return a;
}
int member_func(int a){
return a;
}
static int static_memory_func(int a){
return a;
}
};

int main(){
std::function<int(int)> f;
if (!f){ // 函数对象重载了 bool 运算符
std::cout << "函数对象还没赋值" << std::endl;
}

// 函数
f = func;
std::cout << f(10) << std::endl;

// 函数指针
int (*func_ptr)(int) = func;
f = func_ptr;
std::cout << f(11) << std::endl;

// 可调用对象
FuncClass func_class_obj;
f = func_class_obj;
std::cout << f(12) << std::endl;

// 类静态成员函数
f = FuncClass::static_memory_func;
std::cout << f(13) << std::endl;

// 成员函数
f = std::bind(&FuncClass::member_func, &func_class_obj, std::placeholders::_1);
std::cout << f(14) << std::endl;

// lamda
f = [](int a){
return a;
};
std::cout << f(15) << std::endl;

// reset
f = nullptr;
std::cout << "无效函数对象,会抛异常" << std::endl;
std::cout << f(16) << std::endl;

return 0;
}

输出如下

函数对象还没赋值
10
11
12
13
14
15
无效函数对象,会抛异常
terminate called after throwing an instance of 'std::bad_function_call'
what(): bad_function_call

程序员的自我修养(7): 动态链接

· 阅读需 8 分钟

静态库暴露有以下缺点

  1. 内存和磁盘空间占用大。如果一个lib.o被多个程序使用,那么每个程序内都会有 lib.o的一个副本,如果多个程序同时运行,在内存中还会也会有多个副本。比如 C 语言静态库,一个普通程序使用到 C 语言静态库至少 1M 以上,如果有 100 个这样的程序,那么就至少要 100M 空间。很多 linux 机器上,/bin/下甚至有上千个程序,那么就要浪费几个 G 的空间。
  2. 程序更新与发布。每次更新一个静态库,整个程序都要重新编译更新。

动态链接

简单的说,动态链接就是链接过程推迟到运行时在进行,这就是动态链接的基本思想。

系统检查程序所需要的库及其依赖,并将其依次加载到内存中,当把所有库都加载完时,检查依赖关系是否满足,然后再进行链接工作,链接工作和静态链接非常相似,包括符号解析地址重定位等。

后续再加载其他程序时也一样,依次加载库和依赖,如果依赖的库在之前加载其他程序时已经被加载进去了,那么就不用再加载了,只需要链接即可。

很明显,动态链接解决了静态链接库浪费磁盘空间和内存的问题。除此之外它还减少了物理页面的换入换出,也可以增加 CPU 缓存命中率。因为不同进程间的数据和指令访问都集中在了一个共享模块上

动态链接的基本实现

动态链接的基本思想是将程序按照模块分割成各个相对独立的部分,在程序运行时才将它们链接成一个完整的程序。那么我们能不能直接使用目标文件进行动态链接呢。理论上是可行的,但实际上动态链接的实现方案和直接使用目标文件稍有差别。

动态链接涉及运行时的链接,以及多个文件的装载,必须有操作系统的支持。因为动态链接的情况下,进程的虚拟地址分布也会比静态链接更为复杂,还有一些存储管理,内存共享,进程线程也会有一些微妙的变化。

程序真正的链接工作是由动态链接器来完成的,动态链接是把链接的这个过程从程序装载前,推迟到了装载的时候。 程序每次装载都要进行重新链接,会有一定的性能损失。但是动态链接过程可以优化,比如延迟绑定。可以使得动态链接的性能损失降到最小。

据估算,动态链接相比于静态链接,性能损失约 5%。

简单动态链接例子

program1.c

#include "lib.h"
int main(){
foobar(1);
return 0;
}

lib.c

#include <stdio.h>
void foobar(int i){
printf("Print %d\n",i);
}

lib.h

#ifndef LIB_H_
#define LIB_H_

void foobar(int i);

#endif

当 program1.c 编编译成 program.o 时,编译器还不知道 foobar 的地址,当链接器将program.o链接成可执行文件时候,它必须知道 foobar 函数的性质

  • 如果 foobar 是一个定义在其它静态目标文件中的函数,那么链接器会按照静态链接规则,将 program1.o 中的 foobar 地址引用重定位
  • 如果 foobar 是一个定义在某个共享动态库中的函数,那么链接器会将这个符号的引用标记为一个动态链接的符号。不对它进行地址重定位,把这个过程留到装载时再进行。

那么链接器是如何知道 foobar 是一个静态符号还是一个动态符号呢。这就是我们链接是需要指定动态库的一个原因了。动态库中包含了完整的符号信息。链接器在解析符号时就知道它是一个动态符号还是一个静态符号。

共享对象的最终装载地址在编译时是不确定的

地址无关代码

共享对象在被装载时,如何确定它在虚拟地址空间中的位置? 为了实现动态链接,我们首先遇到的问题就会是共享对象地址的冲突问题。 程序模块的指令和数据中可能会包含一些绝对地址的引用,我们在链接产生输出文件时,就要假设模块被装载的目标地址。很明显,如果不同的模块目标装载地址都是相同的显然是不行的。

我们能不能让共享对象在任意位置加载?换个说法就是: 共享对象在编译时能不能假设自己在进程虚拟地址空间的位置。 与此不同的是,可执行文件基本上知道自己在进程虚拟地址空间的其实位置,因为可执行文件往往是第一个被加载的文件。它可以选择一个固定的地址。

装载时重定位

一旦模块装载地址确定,即目标地址确定,那么系统对程序中所有的引用地址进行重定位。比如 foobar 相对于代码段的地址是 0x100,当模块被装载到 0x10000000时,那么就可以确定 foobar 的地址为 0x10000100, 这时候,系统遍历模块化中的重定位表,把所有对 foobar 的地址引用都重定位到 0x10000100

装载时重定位需要修改指令。那么多个进程就无法共享这些指令了。动态链接库中将指令中可修改的部分提取出来和数据部分放在一起,每个进程各自拥有一个副本。这样大部分指令就可以共享了。 这个方案就称为 地址无关代码(PIC, Position-independent Code 技术

地址引用方式

按照是否跨模块分为两类,模块内引用,模块外引用
按照不同的引用方式分为,指令引用,数据引用

static int a;
extern in b;
extern void ext();

void bar(){
a = 1; // 2.模块内部数据访问
b = 2; // 4.模块间数据访问
}

void foo(){
bar(); // 1.模块内部调用或跳转
ext(); // 3.模块外函数调用
}

编译器在编译上述内容时候,并不能确定 b 和函数 ext() 是模块外部还是模块内部的。 因为他们可能定义在同一个共享对象的其他目标文件中。由于没法确定,编译器只能把他们都当做模块外部的函数和变量来处理。

模块内部调用或跳转

这种是最简单的,模块内部的函数跳转都可以是相对地址调用,所以这种指令是不需要重定位的。只要 bar 和 foo 的相对位置不变,那么 bar() 的调用就是地址无关的,即无论模块被装载到什么位置,指令都是有效的。

模块内部数据访问

模块内各个任何一条指令和它要访问的内部数据之间的相对位置也是固定的。通过访问当前位置加上固定的偏移量就可以访问模块内部数据了。

模块间数据访问

模块间的数据访问目标地址要等到装载时才确定。要使得代码地址无关,基本思想是把跟地址相关的部分放到数据里面。动态库的做法是在数据段里建立一个指向这些变量的指针数组,也被称为 全局偏移表。当代码需要访问全局变量时,可以通过 GOT 中对应的项间接引用。GOT 本身是放在数据段的,它可以在模块装载时被修改。并且每个进程都可以有独立的副本,相互不受影响。

模块间调用跳转

和模块间数据访问类似,只不过 GOT 中保存的是目标函数地址。这种方法和简单,但是存在一些性能问题。实际上 ELF 采用了一种更加复杂和精巧的方法。见 延迟绑定

C++对象模型(1): 关于对象

· 阅读需 3 分钟

在 C 语言当中,“数据” 和 “处理数据的函数” 是分开声明的,语言本身并没有支持 “数据和函数” 之间的关联性。C++ 则有抽象数据类型,其中不仅有“数据”, 还有处理数据相关的操作。

将 C 中的 strcut+函数 转换成 C++的抽象数据类型,会增加多少成本呢? 经常有人说,C++ 会比 C 慢, 慢在哪里呢?

答案是并没没有增加成本,C++ 也不会比 C 慢。 通常 C++ 比 C 慢主要是 virtual 引起的

  • virtual function 机制,用于支持执行期绑定
  • virtual base class, 用于实现“多次出现在继承体系中的 base class,有一个单一而被共享的实例”

C++ 对象模型

C++对象模型包含哪些东西

  • 成员变量,静态成员变量
  • 函数,静态函数,虚函数

成员变量被配置与每一个对象中,静态变量,函数以及静态函数则是在对象之外。虚函数则稍微不同

  • 每一个 class 产生一堆指向虚函数的指针,放在表格这种,这个表格被称为虚函数表
  • 每一个 class object 被安插一个指针,指向相关的虚函数表。通常这个指针称为虚函数表指针。虚函数表指针的初始化,重置都由class 的构建函数,析构函数,copy运算符自动完成。
  • 每一个 class 所关联的 type_info object (用以只会RTTI) 也经由虚函数表被指出来,通常被放在表格的第一个 slot

C++中 struct / class

C 语言中,struct 的内存布局就如我们看到的一样,按照声明从上往下排列。你可能会在 C 语言中看到一些 “巧妙” 的设计(或许是陋习?)

struct mumble{
//...
char pc[1];
}

struct mumble *pmumb1 = (struct mumble*)malloc(sizeof(struct mumble) + strlen(string) + 1);
strcpy(&muble.pc, string);

但是在 C++中或许不行,C++中凡出于同一个 access section的数据,必定保证其声明顺序出现在内存布局当中。然而多个 access section的数据,其排列顺序就不一定了。前面的 C 技巧,或许能够有效运行,或许不能。

如果一个程序员迫切的需要一个相当复杂的 C++ class 的某部分数据,使它像 C 声明的那种模样,那么最好将该部分抽取出来成为一个单独的 struct 声明。然后使用组合的方式

struct C_point {...};
class Point{
public:
operator C_point(){return _c_point;}
private:
C_point _c_point;
};

C struct 在 C++ 中的一个合理用途,就是你需要传递一个 复杂的 class 数据到某个 C 函数中,struct 声明可以将数据封装起来,并保证拥有与 C 兼容的空间布局。

C++对象模型(6): 运行时

· 阅读需 4 分钟

想象一下下面这句话执行的时候会发生什么

if (yy == xx.getValue()){
//...
}

xx 和 yy 定义如下

class Y{
public:
Y();
~Y();
bool operator==(const Y&) const;
};
class X{
public:
X();
~X();
operator Y() const;
X getValue();
};

yy == 明显需要调用一个等号运算符yy.operator==()。 但是 xx.getValue() 返回的是一个 X 对象,所以需要调用 xx.openrator Y() 运算符将其转换为 Y 对象。上面的展开了其实就是

if (yy.operator==(xx.getValue().operator Y())){
//...
}

以上的发生的一切,都是编译器根据 class 的隐含语义生成的。当然我们也可以明确的写出这样的例子,但是不建议,它只会时编译速度稍微快一些。并没有其他好处。

runtime_semantincs_1.png

实际程序运行时会比这复杂

  1. 产生临时 x 对象,放置 getValue() 返回值
  2. 产生临时 y 对象,放置 operator Y() 返回值
  3. 产生临时 int 对象,放置 yy.operator==() 返回值

所以最开始的一行代码,会被转换为如下形式

X temp1 == xx.getValue();
Y temp2 == temp1.operator Y();
int temp3 = yy.operator==(temp2);

if (temp3){
//...
}
temp2.Y::~Y();
temp1.X::~X();

C++就是这样,不太容易从源码表达式上看出它的复杂度。

对象的构造和析构

一般来说,对应的构造和析构安插如我们所想的那样

{
Point point;
// point.Point::Point();
...
// point.Point::~Point();
}

如果区段中有多个离开点(多处 return),情况会复杂一些,析构必须被放置在每一个离开点之前。
同时我们声明变量的时候,最好在使用他的时候才开始定义,否则可能会造成不必要的构造,析构。

if (cache){
return 1;
}

Point point;

如果我们在检查 cache 之前就声明 point。那么就可能会造成 Point 对象不必要的构造与析构开销

全局对象

假如我们有以下代码

Matrix identity;

int main(){
Matrix m1 = identity;

return 0;
}

C++ 保证,一定会在第一次用到全局变量之前把它构造出来,并且在 main 函数结束之前,把它销毁掉。

通常不建议直接使用全局变量,建议将全局变量包装成一个函数。否则容易造成初始化灾难,比如两个全局变量,分别在不同的源文件中,并且其中一个变量用到了另一个变量,就可能会造成使用前未被初始化的问题。

Matrix& x(){
static Matrix* identity = new Matrix();
return *identity;
}

局部静态对象

假设有以下片段

const Matirx& identity(){
static Matrix mat_identity;
return mat_identity;
}

局部静态对象保证,mat_identity的构造和析构只调用一次,即使 identity() 可能被调用多次。

如果你是一个编译器开发者,你会怎么保证该特性

现在C++标准中要求,编译单位中的局部静态对象必须被摧毁--以构造相反的顺序摧毁,。但是由于这些 object 是在需要时才被构造,所以编译时期无法预知其集合以及顺序。为了支持这个规则,可能需要对被产生出来的局部对象保持一个执行器链表。

对象数组

假设有一下数组定义

Point knots[10];

编译会有对应的函数来生成该数组,通常函数形式可能如下

void *vec_new(
void *array,
size_t elem_size,
int elem_count,
void (*constructor)(void *),
void (*destructor)(void *, char)
)

对于支持异常处理的编译器,传入一个destructor 是有必要的

编译器实际运行的时候就可能对我们声明的数组做 vec_new 操作

Point knots[10];
vec_new(&knots, sizeof(Point), 10, &Point::Point, 0)

显然释放也一样

void *vec_delete(
void *array,
size_t elem_size,
int elem_count,
void (*destructor)(void *, char)
)

有些编译器会另外增加一些参数,便于有条件的导引 vec_delete 的逻辑

new 和 delete 运算符

new 运算符总是以 C 的 malloc() 完成,虽然没有规定一定这么做。 相同情况,delte 总是以 free() 完成

临时对象

临时对象的被摧毁,应该是对完整表达式求值过程中的最后一个步骤

((objA > 1024) && (objB > 1024)) ? objA + objB : foo(objA, objB)

以上包含 5 个表达式,任何一个表达式所产生的任何一个临时对象,都应该在完整表达式被求值完成后,才可以被毁去。

初探模板元编程

· 阅读需 3 分钟

模板特化

先讲讲模板特化,因为元编程中用到了大量的模板特化。什么是模板特化,比如

// 主函数
template <typename T> void process(T data);

// 针对int的特化
template <> void process<int>(int data);
// 针对float的特化
template <> void process<float>(float data);

C++的模板元编程中就用到了大量的模板特化, 你如 remove_const 用于去除类型的 const 修饰符

  template<typename _Tp>
struct remove_const
{ typedef _Tp type; };

template<typename _Tp>
struct remove_const<_Tp const>
{ typedef _Tp type; };

实际使用的时候

std::remove_const<int>::type a1; // 匹配第一个模板,a1 类型是 int 
std::remove_const<int const>::type a2; // 匹配第二个模板, a2 的类型也是 int

std::integral_constant

大部分模板都继承了 std::integral_constant, 它是一个具有指定值类型,指定值的编译期常量。

std::integral_constant<int, 42>::value // 等同于 42
std::integral_constant<int, 42>::type // 等同于 int

相关定义如下

  /// integral_constant
template<typename _Tp, _Tp __v>
struct integral_constant
{
static constexpr _Tp value = __v;
typedef _Tp value_type;
typedef integral_constant<_Tp, __v> type;
constexpr operator value_type() const noexcept { return value; }
};

typedef integral_constant<bool, true> true_type;

/// The type used as a compile-time boolean with false value.
typedef integral_constant<bool, false> false_type;

注意,它特化了两个类型, true_type 和 false_type, 大量的类型判断模板都继承了这两个类型

is_xxx

is_xxx 就是大量的模板特化, 拿 is_integral 举例

  template<typename>
struct is_integral
: public false_type { };

template<>
struct is_integral<bool>
: public true_type { };

template<>
struct is_integral<char>
: public true_type { };

template<>
struct is_integral<int>
: public true_type { };

// short, long, longlong, unsigned char, unsigned short 等等等

大部分 is_xxx 的模板都默认继承了 false_type, 然后特化 xxx 类型继承自 true_type

std::is_integral<int>::value // true
std::is_integral<float>::value // false

其他的 is_xxx 大同小异

  • is_array 特化 T[Size] 或者 T[]
  • is_pointer 特化 T*
  • is_lvalue_reference 特化 T&
  • is_rvalue_reference 特化 T&&
  • ......

但有一部分比如 is_enum, is_union, is_class 无法通过其接口形式来判断,只能通过编译器内置的函数来判断。

还有很多 is_xxx 是复合类型, 就是上面的几个基础类型组合

  • is_arithmetic 是否是算数类型 (is_float_point, is_integral)
  • is_fundamental 是否是基础类型 (is_arithmetic, is_void, is_nullptr)
  • is_reference 是否是左值或者右值引用
  • ......

其他

不仅可以判断类型,模板元编程还提供了一些其他的模板类,比如

  1. 判断属性类型
    • is_const
    • is_volatile
    • ...
  2. 查询支持的操作
    • 是否有默认构造函数
    • 是否有虚析构函数
    • ...
  3. 查询类型关系
    • is_same
    • is_base_of 是否是另一个类的基类
    • is_convertible 是否能转换到另一个类型
    • ...
  4. 变化类型
    • 去除增加 const/volatile
    • 添加或移除引用
    • 添加或移除 singed
    • 添加或移除指针
  5. 其他
    • enable_if 条件性的移除函数重载或者模板特化

对模板元编程有了大致的了解之后,你就可以对一些模板库初窥门径了,关于模板编程的最好学习资料就是开源库。

CPU 缓存一致性

· 阅读需 5 分钟

有两个独立的线程,一个线程读写 var1, 一个线程读写 var2。这两个线程的读写会相互影响吗?

struct SharedData {
char var1;
// double magic;
char var2;
};

下面我们来做个实验

实验

// test.cpp
#include <iostream>
#include <thread>

struct SharedData {
char var1;
// double magic;
char var2;
};

SharedData data;

void Thread1() {
for (int i = 0; i < 100000000; ++i) {
data.var1++;
}
}

void Thread2() {
for (int i = 0; i < 100000000; ++i) {
data.var2++;
}
}

int main() {
std::thread t1(Thread1);
std::thread t2(Thread2);

t1.join();
t2.join();

return 0;
}

实验一

将上面的代码保存为 test.cpp, 然后用 g++ test.cpp -lpthread 编译, 运行 10 次统计平均运行时间

$ for i in {1..10}; do (time ./a.out); done;
./a.out 1.93s user 0.01s system 191% cpu 1.015 total
./a.out 1.42s user 0.06s system 180% cpu 0.819 total
./a.out 0.85s user 0.16s system 142% cpu 0.706 total
./a.out 1.12s user 0.06s system 186% cpu 0.633 total
./a.out 1.56s user 0.00s system 182% cpu 0.861 total
./a.out 1.22s user 0.01s system 143% cpu 0.857 total
./a.out 1.67s user 0.02s system 185% cpu 0.911 total
./a.out 1.68s user 0.01s system 182% cpu 0.924 total
./a.out 1.63s user 0.02s system 175% cpu 0.941 total
./a.out 1.68s user 0.02s system 184% cpu 0.919 total

total 平均时间为 0.8366 秒

实验二

然后将 SharedData 里面的 // double magic; 注释打开

struct SharedData {
char var1;
double magic;
char var2;
};

重新编译运行

$ for i in {1..10}; do (time ./a.out); done;
./a.out 0.71s user 0.00s system 191% cpu 0.369 total
./a.out 0.67s user 0.01s system 175% cpu 0.390 total
./a.out 0.65s user 0.00s system 154% cpu 0.420 total
./a.out 0.68s user 0.00s system 175% cpu 0.389 total
./a.out 0.69s user 0.00s system 184% cpu 0.374 total
./a.out 0.70s user 0.00s system 183% cpu 0.381 total
./a.out 0.67s user 0.02s system 180% cpu 0.385 total
./a.out 0.65s user 0.04s system 161% cpu 0.425 total
./a.out 0.63s user 0.00s system 116% cpu 0.540 total
./a.out 0.70s user 0.00s system 182% cpu 0.386 total

total 平均时间为 0.4269 秒

结果

实验一耗时几乎是实验二的两倍,为什么?这里就涉及到 CPU 缓存行的概念

缓存行

缓存行是计算机体系结构中的基本缓存单元,通常是一组相邻的内存位置。当一个线程修改了共享的内存位置时,它会将整个缓存行加载到CPU缓存中。

在上述实验中, SharedData 结构中的两个 char 变量 var1 和 var2 可能处于相同的缓存行,因为它们是相邻的。当一个线程修改 var1 时,整个缓存行被加载到该线程的 CPU 缓存中。如果另一个线程正在修改var2,它会导致缓存行无效(缓存失效),从而迫使其它的线程重主存重新加载最新的数据。

// double magic; 被注释打开时,结构的大小变大,可能使 var1 和 var2 不再在同一个缓存行上。这样,两个线程可以独立地修改各自的变量,减少了缓存失效的可能性。

缓存一致性

CPU缓存一致性是指多个处理器或核心之间共享数据时,确保它们看到的数据是一致的。在多核处理器系统中,每个核心都有自己的缓存,当一个核心修改了共享数据时,其他核心可能仍然持有旧的缓存值。为了保证数据的一致性,需要采取一些机制来同步各个核心之间的缓存。

MESI协议是一种常见的缓存一致性协议,它定义了四种状态,分别是:

  1. (M)Modified:缓存行被修改,并且是唯一的拥有者,与主内存不一致。如果其他缓存需要该数据,必须先写回主内存。
  2. (E)Exclusive:缓存行是唯一的拥有者,与主内存一致,且未被修改。其他缓存可以直接读取这个缓存行,而不需要从主内存读取。
  3. (S)Shared:缓存行是共享的,与主内存一致,且未被修改。多个缓存可以同时拥有相同的缓存行。
  4. (I)Invalid:缓存行无效,不能被使用。可能是因为其他缓存修改了这个行,导致当前缓存的数据不再有效。

状态的变化可以通过以下例子来说明:

假设有两个核心,A 和 B,它们共享某个数据的缓存行:

  1. 初始状态:A 和 B 的缓存都标记为 Invalid(I),因为还没有任何核心读取或修改这个数据。
  2. 核心A读取数据:A 将缓存行标记为 Exclusive(E),表示A是唯一的拥有者,并且数据与主内存一致。
  3. 核心B读取数据:由于 A 是唯一的拥有者,B 可以直接从 A 的缓存行中读取数据,此时B 的缓存也标记为 Shared(S)。
  4. 核心A修改数据:A 将缓存行标记为 Modified(M),表示数据已被修改且A是唯一的拥有者。同时,A 会通知其他缓存失效,因为此时数据在 A 的缓存中已不一致。
  5. 核心B尝试读取数据:由于 A 将数据标记为 Modified,B 的缓存行变为 Invalid(I),B 需要从主内存重新读取最新的数据。

PyTorch 与 C++ Torch 混合编程问题排查

· 阅读需 2 分钟

问题

在公司的一个项目中,需要同时使用 C++ 和 Python 的一些接口。项目的主要语言是 Python,通过 pybind11 调用 C++ 的接口。然而,在实际运行过程中出现了 torch 符号未定义的问题,尽管 Python 和 C++ 使用的 torch 都是相同版本(PyTorch 调用的也是 C++ 的 so),理论上不应该出现这样的问题。

排查

尽管两个 torch 版本相同,但它们的安装方式不同。PyTorch 是通过 pip 安装的,而 C++ Libtorch 是通过源码编译的方式安装的。虽然两者版本相同,但通过使用 nm 查看符号表发现两个 torch.so 的符号表确实不一样。最终排查发现,由于 Dual ABI 的原因,libtorch 在编译时采用的是 cxx11 ABI,而 Python 中的 torch 使用的是 Pre-cxx11 ABI,两者版本的符号不兼容,导致冲突问题。

解决方案

  1. 重新编译 C++ 项目相关的代码和库,全部使用旧 ABI。然而,使用旧 ABI 编译的可行性尚待调研。考虑到目前几乎所有的代码都是以 cxx11 编译,包括很多系统库也选择了 cxx11 的 ABI,因此全部重新编译并不现实。该方案被否决。
  2. 重新编译 Python 中的 torch,使用新的 ABI。

ubuntu 更新为清华源

· 阅读需 1 分钟

将 ubuntu 默认源换为 清华源

sed -i 's/http:\/\/\(archive\|security\).ubuntu.com\/ubuntu\//http:\/\/mirrors.tuna.tsinghua.edu.cn\/ubuntu\//g' /etc/apt/sources.list

一键安装 Docker

· 阅读需 2 分钟

安装 docker

sudo apt install apt-transport-https ca-certificates curl software-properties-common -y
curl -fsSL https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/gpg | sudo apt-key add -
sudo apt-key fingerprint 0EBFCD88
sudo add-apt-repository "deb [arch=$(dpkg --print-architecture)] https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu $(lsb_release -cs) stable" -y
sudo apt update
sudo apt --fix-broken install -y docker.io
sudo groupadd docker
sudo gpasswd -a $USER docker
sudo systemctl restart docker

安装 nvidia-docker

distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list
sudo apt-get update
sudo apt-get install -y nvidia-docker2
sudo systemctl restart docker

验证是否安装成功

docker run -ti --gpus=all ubuntu:18.04 nvidia-smi

nvidia-driver

# 先卸载
sudo apt-get --purge remove "*nvidia*"

# 查看 可用驱动版本
apt list | grep nvidia-driver

# 选择一个版本安装, 比如 530
sudo apt install nvidia-driver-530

# 重启机器
sudo reboot

其他设置

锁定内核版本

锁定内核当前使用版本,否则系统有可能自动更新内核,那样 nvidia-driver 也要重新安装

sudo apt-mark hold linux-image-$(uname -r)

C++ 符号修饰

· 阅读需 3 分钟

什么是符号修饰?

在 C++ 中,符号修饰是指编译器对函数和变量名进行的一种变换或修饰。这种修饰是为了解决 C++ 的函数重载、命名空间和类等特性引入的命名冲突问题。通过符号修饰,编译器可以在目标文件中唯一标识不同的实体,确保在链接阶段能够正确地找到并匹配相应的函数或变量。

符号修饰的原理

C++ 编译器通过一种名为“名字翻译”(Name Mangling)的技术来实现符号修饰。名字翻译将源代码中的函数和变量名转换成目标文件中的唯一标识符。这种转换过程包括以下几个方面:

1. 函数重载

C++ 允许函数重载,即在同一作用域内定义多个同名函数,但它们的参数类型或个数不同。为了在目标文件中区分这些重载函数,编译器会根据函数的参数类型和个数生成不同的符号。

2. 命名空间

命名空间是 C++ 中组织代码的一种方式,但它可能导致相同名字的函数或变量在目标文件中发生冲突。符号修饰通过在符号前添加命名空间信息,解决了这一问题。

3. 类成员函数

类的成员函数通常需要包含类的信息,以便正确访问对象的成员。符号修饰通过在符号中嵌入类的信息,确保了正确的成员函数匹配。

示例:符号修饰的实际应用

考虑以下 C++ 代码片段:

#include <iostream>

namespace Math {
class Calculator {
public:
int add(int a, int b);
};
}

int Math::Calculator::add(int a, int b) {
return a + b;
}

int main() {
Math::Calculator calc;
std::cout << calc.add(3, 4) << std::endl;
return 0;
}

通过编译并查看符号表,我们可以看到 Math::Calculator::add 函数的符号修饰:

$ nm a.out | grep add
000000000040157e T _ZN4Math9Calculator3addEii

在这个例子中,符号 _ZN4Math9Calculator3addEii 就是经过修饰的 Math::Calculator::add 函数名。

如何处理符号修饰

在正常情况下,大多数 C++ 程序员在日常编程中不需要过多关注符号修饰。编译器会自动处理符号修饰,而开发者通常只需使用函数和变量的原始名称即可。只有在涉及到库的开发、跨语言交互或者遇到符号冲突的特殊情况下,才需要更深入地了解符号修饰。

字符串三剑客: sed

· 阅读需 2 分钟

sed 小技巧:

  1. 替换字符串:

    sed 's/old_string/new_string/g' filename

    这将在文件中将所有的 old_string 替换为 new_string

  2. 删除行:

    sed '/pattern/d' filename

    这将删除包含指定模式的行。

  1. 在行首或行尾插入文本:

    sed 's/^/prefix/' filename   # 在每一行行首添加前缀
    sed 's/$/suffix/' filename # 在每一行行尾添加后缀
  2. 显示特定行或行范围:

    sed -n '5p' filename          # 显示第5行
    sed -n '5,10p' filename # 显示第5到第10行
  3. 使用变量:

    my_variable="new_value"
    sed "s/old_value/$my_variable/" filename
  4. 多个替换:

    sed -e 's/old1/new1/g' -e 's/old2/new2/g' filename

    这可以在同一次 sed 命令中执行多个替换。

  5. 保留替换前的备份文件:

    sed -i.bak 's/old_string/new_string/g' filename

    -i.bak 将替换前的文件备份为 .bak 文件。

  6. 仅显示匹配部分:

    sed -n 's/pattern/\1/p' filename

    这将仅显示匹配到的部分,使用 \1 表示匹配到的内容。

  7. 转换大小写:

    sed 's/[a-z]/\U&/g' filename   # 将小写字母转换为大写
    sed 's/[A-Z]/\L&/g' filename # 将大写字母转换为小写

    \U\L 分别用于将后面的文本转换为大写和小写。

字符串三剑客: grep

· 阅读需 2 分钟

grep 命令小技巧

  1. 简单搜索:

    grep "pattern" filename

    这将在文件中搜索匹配指定模式的行。

  2. 忽略大小写:

    grep -i "pattern" filename

    -i 选项将忽略大小写。

  1. 显示匹配行的行号:

    grep -n "pattern" filename

    -n 选项将显示匹配行的行号。

  2. 显示不匹配的行:

    grep -v "pattern" filename

    -v 选项将显示不包含匹配模式的行。

  3. 只显示匹配部分:

    grep -o "pattern" filename

    -o 选项将只显示匹配到的部分。

  4. 显示匹配行之前或之后的行:

    grep -A 2 "pattern" filename   # 显示匹配行及后面2行
    grep -B 2 "pattern" filename # 显示匹配行及前面2行
    grep -C 2 "pattern" filename # 显示匹配行及前后各2行

    -A-B-C 选项用于显示匹配行之前或之后的指定行数。

  5. 递归搜索子目录:

    grep -r "pattern" directory

    -r 选项将递归搜索指定目录及其子目录。

  6. 显示匹配行的上下文:

    grep -C 2 "pattern" filename   # 显示匹配行及前后各2行

    -C 选项用于显示匹配行的上下文。

  7. 仅显示匹配的行数:

    grep -c "pattern" filename

    -c 选项将仅显示匹配的行数,而不是具体的行内容。

搭建 sftp server

· 阅读需 1 分钟

有些时候我们开发需要接触涉及到一些sftp接口,比如 sftp 上传文件, sftp下载文件,这时候可以用 docker 快速的搭建一个sftp server 用于测试开发。

拉镜像

docker pull atmoz/sftp

启动 sftp 服务

docker run -d -v /path/to/shared/folder:/home/username/upload -p 2222:22 -e SFTP_USERS=username:password:::upload atmoz/sftp
  • /path/to/shared/folder 是想要共享的文件夹的本地路径。
  • username 是要创建的用户名。
  • password 是用户的密码。
  • -p 2222:22 将容器的 22 端口映射到主机的 2222 端口,这是 SFTP 默认的端口。

测试

sftp -P 2222 username@localhost:/upload/xxx xxx
  • 要用 sftp 而不是 scp
  • 下载路径是 /upload/, 而不是 /home/username/upload

怎样编写 OpenAPI

· 阅读需 1 分钟

OpenAPI 是一种与语言无关的文档,用来描述 web 服务。

openapi: 3.0.3
info:
title: hi
version: 0.0.1

paths:
/hi:
get:
responses:
'200':
description: OK
content:
text/plain:
schema:
type: string
enum: ["hello"]

比如以上文档就描述了这样一个服务, 当你用 GET 方法访问 /hi 接口时

GET /hi

就会得到一个 body 为 hello 的响应

HTTP/1.0 200 OK
Content-Type: text/plain

hello

接下来我们介绍如何在 OpenAPI 中描述一个接口, 详细文档请移步: 编写 OpenAPI

设计原则概览

· 阅读需 1 分钟

单一职责原则

每个模块只做一件事 (类根绝业务切分可大可小,并不是说越细越好)

开闭原则

对扩展开放,对修改关闭。修改程序时,不需要修改类内部代码就可以扩展类的功能

里式替换原则

任何基类出现的地方,都可以用派生类替换

依赖倒置原则

针对接口(纯虚函数)编程,而非针对实现编程

接口分离原则

接口功能的粒度应该尽可能小

共同封装原则

一同变更的类应该合在一起

共同复用原则

不能一起被复用的类不能被分到一组

docker常见操作

· 阅读需 1 分钟
  1. 列出已有镜像

    docker image ls
  2. 列出镜像实际占用空间

    docker system df
  3. 删除“旧”镜像

    docker image prune
  4. 删除本地镜像

    docker image rm [镜像ID]
  5. 挂载当前目录到镜像

    docker run -v "$(pwd)"/:/workspace/ ubuntu:16.04

    挂载主目录下的文件到镜像

    docker run -v $HOME/.ssh:/root/.ssh ubuntu:16.04
  6. 挂载单个文件到镜像

    docker run --mount \
    type=bind,source="$(pwd)"/sources.list,target=/etc/apt/sources.list \
    ubuntu
  7. 显示正在运行的容器

     docker container ls

    列出所有容器

    docker container ls -a

    删除所有停止的容器

    docker container prune
  8. 进入容器

    docker run -ti --entrypoint=/bin/bash node
    docker run -ti node /bin/bash

    进入已在运行的容器

    docker exec -it [镜像ID] /bin/bash
  9. 镜像导入导出

    docker save myimage:tag > myimage.tar
    docker load < myimage.tar
  10. 把容器端口4000映射到主机80

docker run -p 80:4000 nginx

ubuntu下构建deb包

· 阅读需 2 分钟

本文讲解如何构建一个deb包

dpgk

首先介绍下dpkg,dpkg是debian系统下软件包管理工具,用dpkg可以很方便的安装管理我们的deb包,为什么呢,因为deb包都是用dpkg构建的。类似于rethat系列的软件用rpm一样,rethat用rpm管理软件,用rpmbuild构建软件。

如何用dpkg构建一个软件包呢

  1. 模仿,找一个流行的deb包,拆开里面的内容,看看它们是怎么写的,模仿他们准没错。我们可以上 dpkg.org 去查找比较流行的程序。这里我以sshd服务举例

  2. 解压sshd基本资源

    dpkg -x openssh-server_6.6p1-2ubuntu1_amd64.deb openssh

    现在解压的都是sshd自身的东西,比如可执行程序,man文档,启动脚本之类的

  3. 解压 debian 包构建脚本

    dpkg -e openssh-server_6.6p1-2ubuntu1_amd64.deb openssh/DEBIAN

    这DEBIAN里面就是构建deb包所需要的脚本, 我们最需要关注的就是control,其他的可有可无

  4. 以上内容就是构建sshd服务的所有内容了,重新打包下试试

    dpkg -b openssh openssh.deb

    以上就是构建包的整理流程,但是你要是构建软件,你可能还需要一些其他的知识,比如Linux service脚本编写, /etc/init.d/脚本编写等等。

Ubuntu18.04安装搜狗输入法

· 阅读需 2 分钟

虽然说大多数程序的安装,按找官方文档一步一步往下执行就好了,但是还是有一些软件官方文档支持的不好,比如搜狗Linux下的输入法,按官方文档安装完之后,输入法还是不能用。记录下ubuntu下搜狗输入法的安装步骤。

  1. 安装fcitx。 fcitx是搜狗输入法的依赖项

    sudo apt install fcitx-bin
    sudo apt install fcitx-table
  2. 语言支持(Language Support)中配置fcitx。

    键盘输入法系统(Keyboard input method system)的默认方法 iBus 替换为 fcitx

  3. 重启系统,重启系统,重启系统。 重要的事情说三遍。

  4. 下载 搜狗输入法For Linux 安装

  5. 还是要重启,之后就可以在右上角小键盘那里设置输出法了

    如果你桌面环境是英文,还可能需要手动添加搜狗输入法,如果已有搜狗输入法则不需要。

    1. 右上角小键盘
    2. configure
    3. 左下角加号
    4. 去掉 Only Show Current Language前面的钩
    5. 拉到最下面,选中搜狗输入法,点击ok
  6. 之后便可以使用搜狗输入法了

  7. 最后,其实安装不上也没关系,多用用英语吧,挺有用的。

tar打包

· 阅读需 1 分钟

tar 打包常用命令, 经常用还是一直忘

tips: tar命令要再记不住就要找工作了

记不住 -j bz2, -j 打包的是bz2格式

找工作 -z gz, -z 打包的是gz格式

打包

tar -cvf ***.tar data
tar -czvf ***.tar.gz data
tar -cjvf ***.tar.bz2 data

解压

tar -xvf ***.tar
tar -xzvf ***.tar.gz
tar -xjvf ***.tar.bz2

查看包内内容

tar -tvf ***.tar
tar -tzvf ***.tar.gz
tar -tjvf ***.tar.bz2

systemctl 服务编写

· 阅读需 2 分钟

Linux 服务

启动Linux服务,一般有两种方法, 一种是service, 一种是systemctl

service nginx start
systemctl start nginx

service是比较老的系统的管理方式, systemctl是比较新的管理方式,一些老的系统不支持systemctl, 但新的系统systemctl会兼容service

service start其实执行的是/etc/init.d/下面的shell脚本,脚本中定义了start,stop等操作。这些脚本需要我们自己编写。

systemctl是比较新的系统里的服务管理方式,systemctl脚本都放在/etc/systemd/system/(Ubuntu)或者/usr/lib/systemd/system(Centos)下。如果在该目录下找不到相应的脚本,它会去/etc/init.d目录下找service的启动脚本。

systemd init系统

大部分Linux发行版,如Rhel,CentOS,Fedora,Ubuntu,Debian和Archlinux,都采用systemd作为其初始系统。实际上,Systemd不仅仅是一个init系统,这也是为什么有些人强烈反对其设计的原因之一,这违背了公认的unix座右铭:“做一件事,做得好”。systemd使用自己的.service文件, 其他init系统使用简单的shell脚本来管理服务,不过慢慢的就会被systemctl取代,这里主要介绍systemcl的一些操作以及systemd服务脚本编写。

基本命令

  1. 启动与停止

    systemctl stop nginx
    systemctl start nginx
  2. 查看服务状态

    systemctl status nginx
  3. 设置服务开机启动

    systemctl enable nginx
  4. 查看服务log

    journalctl -u foo-daemon

自定义服务

新建一个systemd service file /etc/systemd/system/demo.service

sudo touch /etc/systemd/system/demo.service
sudo chmod 664 /etc/systemd/system/demo.service

写入以下内容, /usr/sbin/demo 可以是自己随便写一个小程序

[Unit]
Description=A Systemd Service Demo

[Service]
ExecStart=/usr/sbin/demo

[Install]
WantedBy=multi-user.target

加载脚本

sudo systemctl daemon-reload

现在 就创建了一个linux服务,我们就可以用systemctl操作我们服务了

sudo systemctl start demo
sudo systemctl stop demo
sudo systemctl restart demo
systemctl status demo

更多有关systemctl介绍 请查看systemd

linux codedump设置

· 阅读需 1 分钟
  1. 打开core开关

    ulimit -c unlimited
  2. 设置core文件生成位置格式

    echo "/corefile/core-%e-%p" > /proc/sys/kernel/core_pattern
  3. 设置之后程序coredump的时候就会在/corefile/下生成 code-程序名-进程ID格式的codedump文件了

    之后便可以用gdb来调试,前提是编译程序的时候加上了-g选项

    gdb ./a.out code-a.out-28281

    进入gdb之后输入bt 就能打印出crash时候的函数调用栈了

Ubuntu 安装sublime

· 阅读需 1 分钟
  1. 信任sublime的密钥

    wget -qO - https://download.sublimetext.com/sublimehq-pub.gpg | sudo apt-key add -
  2. 添加subilme的仓库

    echo "deb https://download.sublimetext.com/ apt/stable/" | sudo tee \
    /etc/apt/sources.list.d/sublime-text.list
  3. 安装sublime

    sudo apt-get update
    sudo apt-get install sublime-text
  4. 启动

    subl

C++ 递归创建文件夹

· 阅读需 1 分钟

code

#include <string.h>
#include <limits.h> /* PATH_MAX */
#include <sys/stat.h> /* mkdir(2) */
#include <errno.h>

int mkdir_recursive(const string &path) {
const size_t len = path.length();
char _path[PATH_MAX];
char *p;

errno = 0;

/* Copy string so its mutable */
if (len > sizeof(_path) - 1) {
errno = ENAMETOOLONG;
return -1;
}
strcpy(_path, path.c_str());

/* Iterate the string */
for (p = _path + 1; *p; p++) {
if (*p == '/') {
/* Temporarily truncate */
*p = '\0';

if (mkdir(_path, S_IRWXU) != 0) {
if (errno != EEXIST)
return -1;
}

*p = '/';
}
}

if (mkdir(_path, S_IRWXU) != 0) {
if (errno != EEXIST)
return -1;
}

return 0;
}

Ubuntu18.04 安装 Docker

· 阅读需 2 分钟

Docker安装

  1. 更换国内软件源,推荐 清华大学开源软件镜像源 。Ubuntu的软件配置文件是/etc/apt/sources.list, 将系统自带的文件做个备份,将该文件替换为下面内容,即可使用清华的软件源镜像。

    # 默认注释了源码镜像以提高 apt update 速度,如有需要可自行取消注释
    deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic main restricted universe multiverse
    # deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic main restricted universe multiverse
    deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-updates main restricted universe multiverse
    # deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-updates main restricted universe multiverse
    deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-backports main restricted universe multiverse
    # deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-backports main restricted universe multiverse
    deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-security main restricted universe multiverse
    # deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-security main restricted universe multiverse
  2. 如果你过去安装过docker,先删掉:

    sudo apt-get remove docker docker-engine docker.io
  3. 首先安装依赖

    sudo apt-get install apt-transport-https ca-certificates \
    curl gnupg2 software-properties-common
  4. 信任Docker的GPG公钥

    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
  5. 添加清华的软件仓库

    sudo add-apt-repository \
    "deb [arch=amd64] https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu \
    $(lsb_release -cs) \
    stable"
  6. 安装

    sudo apt-get update
    sudo apt-get install docker-ce

Docker一些设置

  1. 设置开机启动。(安装后默认设置开机启动,可忽略)

    sudo systemctl enable docker
    sudo systemctl start docker
  2. 添加当前用户到docker用户组,可以不用 sudo 运行docker

    sudo groupadd docker
    sudo usermod -aG docker $USER

基于docker的hexo博客环境

· 阅读需 1 分钟

Docker介绍

Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的 Linux或Windows 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口,本文讲述的是如何基于docker构建一个博客环境

构建docker

在一个空目录下新建一个Dockfile,写入以下内容

FROM node:latest
RUN npm install -g cnpm --registry=https://registry.npm.taobao.org
RUN cnpm install hexo-cli hexo-server -g
RUN hexo init blog && cd blog && cnpm install \
&& cnpm install hexo-renderer-scss --save \
&& cnpm install hexo-deployer-git --save
RUN git config --global user.email "yg_wen@126.com" \
&& git config --global user.name "wenyg"
CMD /bin/bash
WORKDIR /blog
EXPOSE 4000

构建docker

sudo docker build -t hexo .

博客启动停止

启动

sudo docker run -it --rm -p 80:4000 hexo hexo s

停止

sudo docker stop hexo 

CMAKE实践

· 阅读需 3 分钟

由于之前编译代码一直用Makefile, 但是新公司都用CMake来编译,就花了一天的时间把Makefile换成CMakeLists.txt. 把学习的过程记录下来

先介绍下代码目录

.
├── CMakeLists.txt
├── include
│ ├── event.h
│ ├── openssl
├── lib
│ ├── libevent.a
│ └── libssl.a
├── server
│ ├── CMakeLists.txt
│ ├── server.cpp
│ ├── server.h
│ ├── licAuth.conf
├── src
│ ├── CMakeLists.txt
│ ├── src1.cpp
│ ├── src1.h
└── test
├── CMakeLists.txt
├── test1.cpp
└── test2.cpp
  • include: 头文件, gcc/g++的 -I后面跟的就是此目录
  • lib: 静态库, -L 选项后面跟的目录
  • server: server程序,这个程序要调用src目录下的所有代码产生的静态库, 下面有个licAuth.conf是默认的配置文件,make install的时候要安装到/etc/目录下面
  • src: 源文件, 要封装成静态库以供别人调用
  • test: 一些测试程序,测试src目录在的代码

开始

在我们写的代码目录src, server, test下都新建一个CMakeLIsts.txt, 根目录下也要有, 注意不要拼错了,Lists大写的后面跟s.

根目录下**./CMakeLists.txt**

cmake_minimum_required (VERSION 2.8) #照抄就行了
project(licAuth) # 随便起个名字 然后LicAuth_SOURCE_DIR这个变量就等于该目录
add_subdirectory(src) #表示编译src目录
add_subdirectory(server) #表示编译server目录
add_subdirectory(test) #同上

src目录**./src/CMakeLists.txt**

cmake_minimum_required(VERSION 2.8) #照抄
project(lib) #还是随便起 同时添加了一个lib_SOURCE_DIR变量

include_directories(${licAuth_SOURCE_DIR}/include) #添加头文件目录 相当于 -I
link_directories(${licAuth_SOURCE_DIR}/lib) # 添加静态库目录 相当于 -L

# 添加一些编译选项, 比如-g之类的
add_compile_options(-Wall -std=c++11 -Wunused-variable)
# 下面这两个语句代表吧本目录在的所有源文件打包成一个静态库 liblic.a
aux_source_directory(. DIR_LIB_SRCS)
add_library (lic ${DIR_LIB_SRCS})

server目录 ./server/CMakeLists.txt

cmake_minimum_required (VERSION 2.8)
project (server)

# 添加include 和src下的头文件
include_directories(${licAuth_SOURCE_DIR}/include ${lib_SOURCE_DIR})
# 添加lib和src下生成的静态库
link_directories(${licAuth_SOURCE_DIR}/lib ${lib_SOURCE_DIR})

#还是编译选项 自己看着加
add_compile_options(-Wall -std=c++11 -O3 -flto -Wl,--no-as-needed -Wunused-variable)

# 这里就要生成程序了 server是生成目标,后面跟源文件
add_executable(server server.cpp)
# 这是添加需要的库, 第一是目标后面是需要的库 -liblic -lssl -lcrypto
target_link_libraries(server lic ssl crypto pthread protobuf event machineid)
#上面这两句话相当与 gcc -o server server.cpp -liblic -lssl -lcrypto

add_executable(client client.cpp)
target_link_libraries(client lic ssl crypto pthread protobuf event machineid)

# make install的配置, 把licAuth.conf 复制到/etc/目录
INSTALL(FILES licAuth.conf DESTINATION /etc/)

test目录下的就跟server目录差不多了

编译

编译的时候最好新建一个目录,在新建的目录在cmake, 这样可以很干净的删除cmake的中间产物,比如

mkdir build
cd build
cmake ..
make

当测试完毕的时候 直接把build目录删除就好了 或者cmake出错的时候,也能清空build目录重来.

libevent之http服务器

· 阅读需 6 分钟

libevent简单服务器

编译

gcc http_server.c -o http_server -levent

http_server.c

/*
A trivial static http webserver using Libevent's evhttp.

This is not the best code in the world, and it does some fairly stupid stuff
that you would never want to do in a production webserver. Caveat hackor!

*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <sys/types.h>
#include <sys/stat.h>

#include <sys/stat.h>
#include <sys/socket.h>
#include <signal.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>

#include <event2/event.h>
#include <event2/http.h>
#include <event2/buffer.h>
#include <event2/util.h>
#include <event2/keyvalq_struct.h>

#ifdef EVENT__HAVE_NETINET_IN_H
#include <netinet/in.h>
# ifdef _XOPEN_SOURCE_EXTENDED
# include <arpa/inet.h>
# endif
#endif

char uri_root[512];

static const struct table_entry {
const char *extension;
const char *content_type;
} content_type_table[] = {
{ "txt", "text/plain" },
{ "c", "text/plain" },
{ "h", "text/plain" },
{ "html", "text/html" },
{ "htm", "text/htm" },
{ "css", "text/css" },
{ "gif", "image/gif" },
{ "jpg", "image/jpeg" },
{ "jpeg", "image/jpeg" },
{ "png", "image/png" },
{ "pdf", "application/pdf" },
{ "ps", "application/postscript" },
{ NULL, NULL },
};

/* Try to guess a good content-type for 'path' */
static const char *
guess_content_type(const char *path)
{
const char *last_period, *extension;
const struct table_entry *ent;
last_period = strrchr(path, '.');
if (!last_period || strchr(last_period, '/'))
goto not_found; /* no exension */
extension = last_period + 1;
for (ent = &content_type_table[0]; ent->extension; ++ent) {
if (!evutil_ascii_strcasecmp(ent->extension, extension))
return ent->content_type;
}

not_found:
return "application/misc";
}

/* Callback used for the /dump URI, and for every non-GET request:
* dumps all information to stdout and gives back a trivial 200 ok */
static void
dump_request_cb(struct evhttp_request *req, void *arg)
{
const char *cmdtype;
struct evkeyvalq *headers;
struct evkeyval *header;
struct evbuffer *buf;

switch (evhttp_request_get_command(req)) {
case EVHTTP_REQ_GET: cmdtype = "GET"; break;
case EVHTTP_REQ_POST: cmdtype = "POST"; break;
case EVHTTP_REQ_HEAD: cmdtype = "HEAD"; break;
case EVHTTP_REQ_PUT: cmdtype = "PUT"; break;
case EVHTTP_REQ_DELETE: cmdtype = "DELETE"; break;
case EVHTTP_REQ_OPTIONS: cmdtype = "OPTIONS"; break;
case EVHTTP_REQ_TRACE: cmdtype = "TRACE"; break;
case EVHTTP_REQ_CONNECT: cmdtype = "CONNECT"; break;
case EVHTTP_REQ_PATCH: cmdtype = "PATCH"; break;
default: cmdtype = "unknown"; break;
}

printf("Received a %s request for %s\nHeaders:\n",
cmdtype, evhttp_request_get_uri(req));

headers = evhttp_request_get_input_headers(req);
for (header = headers->tqh_first; header;
header = header->next.tqe_next) {
printf(" %s: %s\n", header->key, header->value);
}

buf = evhttp_request_get_input_buffer(req);
puts("Input data: <<<");
while (evbuffer_get_length(buf)) {
int n;
char cbuf[128];
n = evbuffer_remove(buf, cbuf, sizeof(cbuf));
if (n > 0)
(void) fwrite(cbuf, 1, n, stdout);
}
puts(">>>");

evhttp_send_reply(req, 200, "OK", NULL);
}

/* This callback gets invoked when we get any http request that doesn't match
* any other callback. Like any evhttp server callback, it has a simple job:
* it must eventually call evhttp_send_error() or evhttp_send_reply().
*/
static void
send_document_cb(struct evhttp_request *req, void *arg)
{
struct evbuffer *evb = NULL;
const char *docroot = arg;
const char *uri = evhttp_request_get_uri(req);
struct evhttp_uri *decoded = NULL;
const char *path;
char *decoded_path;
char *whole_path = NULL;
size_t len;
int fd = -1;
struct stat st;

if (evhttp_request_get_command(req) != EVHTTP_REQ_GET) {
dump_request_cb(req, arg);
return;
}

printf("Got a GET request for <%s>\n", uri);

/* Decode the URI */
decoded = evhttp_uri_parse(uri);
if (!decoded) {
printf("It's not a good URI. Sending BADREQUEST\n");
evhttp_send_error(req, HTTP_BADREQUEST, 0);
return;
}

/* Let's see what path the user asked for. */
path = evhttp_uri_get_path(decoded);
if (!path) path = "/";

/* We need to decode it, to see what path the user really wanted. */
decoded_path = evhttp_uridecode(path, 0, NULL);
if (decoded_path == NULL)
goto err;
/* Don't allow any ".."s in the path, to avoid exposing stuff outside
* of the docroot. This test is both overzealous and underzealous:
* it forbids aceptable paths like "/this/one..here", but it doesn't
* do anything to prevent symlink following." */
if (strstr(decoded_path, ".."))
goto err;

len = strlen(decoded_path)+strlen(docroot)+2;
if (!(whole_path = malloc(len))) {
perror("malloc");
goto err;
}
evutil_snprintf(whole_path, len, "%s/%s", docroot, decoded_path);

if (stat(whole_path, &st)<0) {
goto err;
}

/* This holds the content we're sending. */
evb = evbuffer_new();

if (S_ISDIR(st.st_mode)) {
/* If it's a directory, read the comments and make a little
* index page */
DIR *d;
struct dirent *ent;
const char *trailing_slash = "";

if (!strlen(path) || path[strlen(path)-1] != '/')
trailing_slash = "/";

if (!(d = opendir(whole_path)))
goto err;

evbuffer_add_printf(evb,
"<!DOCTYPE html>\n"
"<html>\n <head>\n"
" <meta charset='utf-8'>\n"
" <title>%s</title>\n"
" <base href='%s%s'>\n"
" </head>\n"
" <body>\n"
" <h1>%s</h1>\n"
" <ul>\n",
decoded_path, /* XXX html-escape this. */
path, /* XXX html-escape this? */
trailing_slash,
decoded_path /* XXX html-escape this */);

while ((ent = readdir(d))) {
const char *name = ent->d_name;
evbuffer_add_printf(evb,
" <li><a href=\"%s\">%s</a>\n",
name, name);/* XXX escape this */
}
evbuffer_add_printf(evb, "</ul></body></html>\n");
closedir(d);
evhttp_add_header(evhttp_request_get_output_headers(req),
"Content-Type", "text/html");
} else {
/* Otherwise it's a file; add it to the buffer to get
* sent via sendfile */
const char *type = guess_content_type(decoded_path);
if ((fd = open(whole_path, O_RDONLY)) < 0) {
perror("open");
goto err;
}

if (fstat(fd, &st)<0) {
/* Make sure the length still matches, now that we
* opened the file :/ */
perror("fstat");
goto err;
}
evhttp_add_header(evhttp_request_get_output_headers(req),
"Content-Type", type);
evbuffer_add_file(evb, fd, 0, st.st_size);
}

evhttp_send_reply(req, 200, "OK", evb);
goto done;
err:
evhttp_send_error(req, 404, "Document was not found");
if (fd>=0)
close(fd);
done:
if (decoded)
evhttp_uri_free(decoded);
if (decoded_path)
free(decoded_path);
if (whole_path)
free(whole_path);
if (evb)
evbuffer_free(evb);
}

static void
syntax(void)
{
fprintf(stdout, "Syntax: http-server <docroot>\n");
}

int
main(int argc, char **argv)
{
struct event_base *base;
struct evhttp *http;
struct evhttp_bound_socket *handle;

ev_uint16_t port = 0;
if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
return (1);

if (argc < 2) {
syntax();
return 1;
}

base = event_base_new();
if (!base) {
fprintf(stderr, "Couldn't create an event_base: exiting\n");
return 1;
}

/* Create a new evhttp object to handle requests. */
http = evhttp_new(base);
if (!http) {
fprintf(stderr, "couldn't create evhttp. Exiting.\n");
return 1;
}

/* The /dump URI will dump all requests to stdout and say 200 ok. */
evhttp_set_cb(http, "/dump", dump_request_cb, NULL);

/* We want to accept arbitrary requests, so we need to set a "generic"
* cb. We can also add callbacks for specific paths. */
evhttp_set_gencb(http, send_document_cb, argv[1]);

/* Now we tell the evhttp what port to listen on */
handle = evhttp_bind_socket_with_handle(http, "0.0.0.0", port);
if (!handle) {
fprintf(stderr, "couldn't bind to port %d. Exiting.\n",
(int)port);
return 1;
}

{
/* Extract and display the address we're listening on. */
struct sockaddr_storage ss;
evutil_socket_t fd;
ev_socklen_t socklen = sizeof(ss);
char addrbuf[128];
void *inaddr;
const char *addr;
int got_port = -1;
fd = evhttp_bound_socket_get_fd(handle);
memset(&ss, 0, sizeof(ss));
if (getsockname(fd, (struct sockaddr *)&ss, &socklen)) {
perror("getsockname() failed");
return 1;
}
if (ss.ss_family == AF_INET) {
got_port = ntohs(((struct sockaddr_in*)&ss)->sin_port);
inaddr = &((struct sockaddr_in*)&ss)->sin_addr;
} else if (ss.ss_family == AF_INET6) {
got_port = ntohs(((struct sockaddr_in6*)&ss)->sin6_port);
inaddr = &((struct sockaddr_in6*)&ss)->sin6_addr;
} else {
fprintf(stderr, "Weird address family %d\n",
ss.ss_family);
return 1;
}
addr = evutil_inet_ntop(ss.ss_family, inaddr, addrbuf,
sizeof(addrbuf));
if (addr) {
printf("Listening on %s:%d\n", addr, got_port);
evutil_snprintf(uri_root, sizeof(uri_root),
"http://%s:%d",addr,got_port);
} else {
fprintf(stderr, "evutil_inet_ntop failed\n");
return 1;
}
}

event_base_dispatch(base);

return 0;
}

libevent之ssl通信

· 阅读需 3 分钟

说明

这里用bufferevent进行ssl通信, 是一个简单的回显服务器,接受客户端的消息,并原封不动的响应回去

这里仅仅是加密的tcp, 并不是https

代码

bufferevent_ssl.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/rand.h>

#include <event.h>
#include <event2/listener.h>
#include <event2/bufferevent_ssl.h>

#define SERVER_CRT "server.crt"
#define SERVER_KEY "server.key"
#define SERVER_PORT 9999
static void
ssl_readcb(struct bufferevent * bev, void * arg)
{
struct evbuffer *in = bufferevent_get_input(bev);

printf("Received %zu bytes\n", evbuffer_get_length(in));
printf("----- data ----\n");
printf("%.*s\n", (int)evbuffer_get_length(in), evbuffer_pullup(in, -1));

bufferevent_write_buffer(bev, in);
}

static void
ssl_acceptcb(struct evconnlistener *serv, int sock, struct sockaddr *sa,
int sa_len, void *arg)
{
struct event_base *evbase;
struct bufferevent *bev;
SSL_CTX *server_ctx;
SSL *client_ctx;

server_ctx = (SSL_CTX *)arg;
client_ctx = SSL_new(server_ctx);
evbase = evconnlistener_get_base(serv);

bev = bufferevent_openssl_socket_new(evbase, sock, client_ctx,
BUFFEREVENT_SSL_ACCEPTING,
BEV_OPT_CLOSE_ON_FREE);

bufferevent_enable(bev, EV_READ);
bufferevent_setcb(bev, ssl_readcb, NULL, NULL, NULL);
}

static SSL_CTX *
evssl_init(void)
{
SSL_CTX *server_ctx;

/* 初始化openssl库 */
SSL_load_error_strings();
SSL_library_init();
/* 初始化随机种子 */
if (!RAND_poll())
return NULL;

server_ctx = SSL_CTX_new(SSLv23_server_method());

if (! SSL_CTX_use_certificate_chain_file(server_ctx, SERVER_CRT) ||
! SSL_CTX_use_PrivateKey_file(server_ctx, SERVER_KEY, SSL_FILETYPE_PEM)) {
puts("Couldn't read 'server.key' or 'server.crt' file. To generate a key\n"
"To generate a key and certificate, run:\n"
" openssl genrsa -out server.key 2048\n"
" openssl req -new -key server.key -out server.crt.req\n"
" openssl x509 -req -days 365 -in server.crt.req -signkey server.key -out server.crt");
return NULL;
}
SSL_CTX_set_options(server_ctx, SSL_OP_NO_SSLv2);

return server_ctx;
}

int
main(int argc, char **argv)
{
SSL_CTX *ctx;
struct evconnlistener *listener;
struct event_base *evbase;
struct sockaddr_in sin;

memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(SERVER_PORT);
sin.sin_addr.s_addr = htonl(0x7f000001); /* 127.0.0.1 */

ctx = evssl_init();
if (ctx == NULL){
return 1;
}
evbase = event_base_new();
listener = evconnlistener_new_bind(
evbase, ssl_acceptcb, (void *)ctx,
LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, 1024,
(struct sockaddr *)&sin, sizeof(sin));

event_base_loop(evbase, 0);

evconnlistener_free(listener);
SSL_CTX_free(ctx);

return 0;
}

编译

需要libevent-openssl库, 先用apt search libevent-openssl查找版本名,然后再安装,带上版本号如libevent-openssl-2.0-5

$ apt search libevent-openssl
Sorting... Done
Full Text Search... Done
libevent-openssl-2.0-5/xenial-updates,xenial-security,now 2.0.21-stable-2ubuntu0.16.04.1 amd64 [installed]
Asynchronous event notification library (openssl)
gcc bufferevent_ssl.c -lssl -lcrypto -levent -levent_openssl
./a.out

由于ssl需要密钥,证书,第一次运行会失败,按照屏幕提示生成证书. 中间第二步骤会要求输入一些信息,一路回车即可

测试

待补充

从TCP到HTTPS代码实现-https服务器

· 阅读需 4 分钟

https 就是在http和tcp之间加一道加密的过程, 在代码上的实现和一般的TCP Server区别就两点

  1. 在accpet之后要由ssl接管套接字, 协商加密算法,交换密钥等.
  2. 之后的send(), recv()替换成SSL的 SSL_write(), SSL_read()

代码实现

https_client.c

#include <openssl/bio.h>  
#include <openssl/ssl.h>
#include <openssl/err.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

#define SERVER_PORT 443
#define CA_CERT_FILE "server/ca.crt"
#define SERVER_CERT_FILE "server/server.crt"
#define SERVER_KEY_FILE "server/server.key"

SSL_CTX *ssl_ctx_int();
SSL *client_ssl_init(SSL_CTX *ctx, int fd);
int bind_and_listen();

int main(int argc, char **argv)
{
printf("Server Running at hppts://127.0.0.1/\n");

int data_len;
struct sockaddr_in addr;
int listen_fd, accept_fd;
socklen_t len = sizeof(addr);
SSL_CTX *ctx = ssl_ctx_int();
listen_fd = bind_and_listen();
int times = 0;
while(1){
char recvbuf[1024] = {0};
char sendbuf[1024] = {0};

accept_fd = accept(listen_fd, (struct sockaddr *)&addr, &len);
SSL *ssl = client_ssl_init(ctx, accept_fd);
data_len = SSL_read(ssl,recvbuf, sizeof(recvbuf));
fprintf(stdout, "[%d] Get %d data:\n%s\n",times++, data_len, recvbuf);

sprintf(sendbuf, "HTTP/1.0 200 OK\r\n\r\n<h1>hello ssl! [%d]</h1>", times);
SSL_write(ssl, sendbuf, strlen(sendbuf));

SSL_free (ssl);
close(accept_fd);
}
SSL_CTX_free (ctx);
return 0;
}

ssl_ctx_int()

SSL初始化,服务器加载证书私钥

SSL_CTX  *ssl_ctx_int(){
SSLeay_add_ssl_algorithms();
OpenSSL_add_all_algorithms();
SSL_load_error_strings();
ERR_load_BIO_strings();
SSL_CTX *ctx = SSL_CTX_new (SSLv23_method());
if(ctx == NULL){
printf("SSL_CTX_new error!\n");
exit(0);
}

// 是否要求校验对方证书 此处不验证客户端身份所以为: SSL_VERIFY_NONE
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);

// 加载CA的证书
if(!SSL_CTX_load_verify_locations(ctx, CA_CERT_FILE, NULL)){
printf("SSL_CTX_load_verify_locations error!\n");
ERR_print_errors_fp(stderr);
exit(0);
}

// 加载自己的证书
if(SSL_CTX_use_certificate_file(ctx, SERVER_CERT_FILE, SSL_FILETYPE_PEM) <= 0){
printf("SSL_CTX_use_certificate_file error!\n");
ERR_print_errors_fp(stderr);
exit(0);
}

//加载自己的私钥 私钥的作用是,ssl握手过程中,对客户端发送过来的随机
//消息进行加密,然后客户端再使用服务器的公钥进行解密,若解密后的原始消息跟
//客户端发送的消息一直,则认为此服务器是客户端想要链接的服务器
if(SSL_CTX_use_PrivateKey_file(ctx, SERVER_KEY_FILE, SSL_FILETYPE_PEM) <= 0){
printf("SSL_CTX_use_PrivateKey_file error!\n");
ERR_print_errors_fp(stderr);
exit(0);
}

// 判定私钥是否正确
if(!SSL_CTX_check_private_key(ctx)){
printf("SSL_CTX_check_private_key error!\n");
ERR_print_errors_fp(stderr);
exit(0);
}
return ctx;
}

client_ssl_init()

和客户端ssl握手,协商算法,交换公钥等

SSL *client_ssl_init(SSL_CTX *ctx, int fd)
{
if (ctx == NULL){
printf("The SSL_CTX is NULL\n");
exit(0);
}
// 将连接付给SSL
SSL *ssl = SSL_new (ctx);
if(!ssl){
printf("SSL_new error!\n");
ERR_print_errors_fp(stderr);
exit(0);
}
SSL_set_fd (ssl, fd);
if(SSL_accept (ssl) != 1){
int icode = -1;
ERR_print_errors_fp(stderr);
int iret = SSL_get_error(ssl, icode);
printf("SSL_accept error! code = %d, iret = %d\n", icode, iret);
}

return ssl;
}

bind_and_listen()

建立套接字,绑定并监听,这个没什么说的

int bind_and_listen()
{
int listen_fd;

listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if( listen_fd == -1 ){
printf("socket error\n");
exit(0);
}
int one = 1;
if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0) {
printf("setsockopt error\n");
close(listen_fd);
}
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = 0;
sin.sin_port = htons(SERVER_PORT);

if(bind(listen_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0 ){
printf("Bind error\n");
exit(0);
}

if(listen(listen_fd, 5) < 0){
printf("listen error\n");
exit(0);
}

return listen_fd;
}

编译运行

把上面的代码写到一个文件当中命名为https_server.c

gcc https_server.c -lssl -lcrypto -o https_server

此时还不能运行,还要生成服务器密钥,证书才可以, 看一下代码中宏定义的路径,生成证书放到相应路径

sudo ./https_server #监听443端口 需要root权限

然后打开浏览器,访问 https://127.0.0.1/, 因为证书是自制的,所以一般会拦截. 测试Chrome浏览器会拦截无法访问, 用Firefox忽略风险可以继续访问.

openssl证书链验证

· 阅读需 2 分钟

证书链

浏览器是怎么保证访问的网站是正经的官方网站而不是其他的钓鱼网站呢,Chome浏览器访问网站时,可信任的网站地址旁边会有一个绿色的锁标准,表明该网站是可信任的,它是怎么知道该网站是可信任的呢。

因为浏览器会内置一些证书,其他证书都是有这些证书签发的, 通过内置的证书来验证其他证书的有效性。这些浏览器内置的证书叫做Root CA(根CA证书), 其他网站的证书都是由Root CA证书一层一层往下签发的。

证书认证原理

  1. 服务器首先生成一个密钥对,把公钥提交给CA
  2. CA用自己的私钥对服务器提供的公钥进行签名得到证书
  3. https服务器在与客户端进行连接的时候会将证书和公钥一起发给客户端,客户端用CA的公钥对证书进行验证,对比一致则证明该证书确实是CA发布的。

通过以上的机制就就确认的网站的真实性。

openssl生成证书链

  1. 生成Root CA密钥和自签证书
openssl genrsa -out ca.key 2048
openssl req -new -x509 -key ca.key -out ca.crt -days 3650
  1. 生成server端密钥和证书请求
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr
  1. 用Root CA给server颁发证书前先构造环境(ubuntu 18.04测试通过)
cp /etc/ssl/openssl.cnf .
mkdir demoCA/newcerts -p
touch demoCA/index.txt
touch demoCA/index.txt.attr
echo "00" > demoCA/serial
  1. 用自签Root CA颁发证书。注意,根证书和要签发的证书,国家/省/城市字段要一样,否者会失败
openssl ca -in server.csr -out server.crt -cert ca.crt -keyfile ca.key -config openssl.cnf

一次DNS服务器BUG调试

· 阅读需 3 分钟

背景

公司做了一个DNS服务器,测试没问题了,架设到网关上使用,出现了电脑能解析域名,手机不能解析的情况问题。

问题解决过程

有时候找到问题在哪比解决问题本身更难。同一个路由器的下的电脑能访问,手机不能访问。先在网关上进行抓包,分析手机发送的DNS请求和电脑发送的DNS请求有什么不同,无果,手机和电脑发送的DNS请求并没有什么不同,服务器端返回的响应也一样。找了一下午也没有找到。有时候找bug就像写作一样,当写不出东西的时候不如出去走走,弄不好灵感就来了。第二天偶然发现手机上并不是所有的域名都不能解析,有个别的域名还是能访问的,又进行了一些域名进行尝试,总结出一个规律:知名的网站大多数不能访问比如百度、腾讯、网易等等,而一些不知名的小网站能够访问。尝试尝试自己注册的域名,果然能访问。有突破口就好办了,然后开始分析公共DNS服务器返回的大网站和小网站的DNS报文有什么不同。

用dig查询baidu域名的IP(手机不能访问)

;; QUESTION SECTION: ;www.baidu.com. IN A

;; ANSWER SECTION: www.baidu.com. 358 IN CNAME www.a.shifen.com. www.a.shifen.com. 58 IN A 115.239.211.112 www.a.shifen.com. 58 IN A 115.239.210.27

个人小网站域名winn.cc

;; QUESTION SECTION: ;www.winn.cc. IN A

;; ANSWER SECTION: www.winn.cc. 58 IN A 202.196.35.225

其他的一些域名和上面的类似,带CNAME记录的全部不能访问,看到这一点,问题已经找到在哪了,之前BOSS为了减少流量,把CNAME记录给删除了,只返回给IP。 比如百度的域名 我们返回的响应是这样的:

;; QUESTION SECTION: ;www.baidu.com. IN A

;; ANSWER SECTION: www.a.shifen.com. 58 IN A 115.239.211.112 www.a.shifen.com. 58 IN A 115.239.210.27

这样的响应理解起来是这样子的,有人(客户端)来找看门大爷(DNS服务器)问老王(域名)家住哪,老王有个外号(CNAME)叫胖子,但是来人并不知道老王的外号。

客户:大爷,老王家在哪啊 看门大爷:胖子家在第一栋楼502号 客户:?????(一脸懵逼)

正常的流程应该是这样的

客户:大爷,老王家在哪啊 看门大爷:老王,你说那个胖子啊,胖子家在第一栋楼502号 客户:哦,那谢谢您嘞。(去找老王)

问题就在上层DNS服务器返回了多条记录,这几条记录可能是相关联的,我们删掉了其中的一条记录,使这些记录关联不到一起。手机系统看来是服务器答非所问不能解析。至于电脑为什么能解析,可能Windows系统和unix的处理流程方法不一样吧。

解决方法

  1. 在返回记录里添加CANME记录
  2. 在返回记录里不添加CNAME记录,但把CNAME换成QNAME

原始响应

;; QUESTION SECTION: ;www.baidu.com. IN A

;; ANSWER SECTION: www.baidu.com. 358 IN CNAME www.a.shifen.com. www.a.shifen.com. 58 IN A 115.239.211.112 www.a.shifen.com. 58 IN A 115.239.210.27

修改后的响应

;; QUESTION SECTION: ;www.baidu.com. IN A

;; ANSWER SECTION: ;www.baidu.com. 58 IN A 115.239.211.112 ;www.baidu.com. 58 IN A 115.239.210.27

bind中基数树的建立

· 阅读需 4 分钟

BIND9新引入了视图的概念,简单的来讲就是能根据不同的来源IP来返回不同的数据。其中网段的存储,网段的快速匹配都是用基数树来实现的。下面是BIND创建基数树的代码。

相关结构体

BIND的IP地址结构

struct isc_netaddr {
unsigned int family;
union {
struct in_addr in;
struct in6_addr in6;
#ifdef ISC_PLATFORM_HAVESYSUNH
char un[sizeof(((struct sockaddr_un *)0)->sun_path)];
#endif
} type;
isc_uint32_t zone;
};

BIND 二叉树中表示网段的结构,该结构由sturct in_addr结构处理转换而来, bitlen表示掩码位

typedef struct isc_prefix {
unsigned int family; /* AF_INET | AF_INET6, or AF_UNSPEC for "any" */
unsigned int bitlen; /* 0 for "any" */
isc_refcount_t refcount;
union {
struct in_addr sin;
struct in6_addr sin6;
} add;
} isc_prefix_t;

IP结构转换为节点结构

#define NETADDR_TO_PREFIX_T(na,pt,bits) \
do { \
memset(&(pt), 0, sizeof(pt)); \
if((na) != NULL) { \
(pt).family = (na)->family; \
(pt).bitlen = (bits); \
if ((pt).family == AF_INET6) { \
memcpy(&(pt).add.sin6, &(na)->type.in6, \
((bits)+7)/8); \
} else \
memcpy(&(pt).add.sin, &(na)->type.in, \
((bits)+7)/8); \
} else { \
(pt).family = AF_UNSPEC; \
(pt).bitlen = 0; \
} \
isc_refcount_init(&(pt).refcount, 0); \
} while(0)

节点结构,其中的bit表示网段的掩码位,决定着redix tree的结构,redix tree的插入和匹配过程都会用到它

typedef struct isc_radix_node {
isc_uint32_t bit; /* bit length of the prefix */
isc_prefix_t *prefix; /* who we are in radix tree */
struct isc_radix_node *l, *r; /* left and right children */
struct isc_radix_node *parent; /* may be used */
void *data[2]; /* pointers to IPv4 and IPV6 data */
int node_num[2]; /* which node this was in the tree,
or -1 for glue nodes */
} isc_radix_node_t;

根节点结构

typedef struct isc_radix_tree {
unsigned int magic;
isc_mem_t *mctx;
isc_radix_node_t *head;
isc_uint32_t maxbits; /* for IP, 32 bit addresses */
int num_active_node; /* for debugging purposes */
int num_added_node; /* total number of nodes */
} isc_radix_tree_t;

函数

插入节点函数,其中redix是树的根节点,prefix是要插入的节点。

isc_result_t
isc_radix_insert(isc_radix_tree_t *radix, isc_radix_node_t **target,
isc_radix_node_t *source, isc_prefix_t *prefix)
{

从根节点开始往下搜寻插入位置,bitlen是新插入节点的掩码位。addr是4字节的char型字符串,占32位, 存储着要插入的节点IP地址。根据bit,addr来查找“插入位置”: 判断addr的第bit位是不是为1,如果是,则与右节点进行比较,否则与左节点进行比较。直到匹配到bit值大于或者等于bitlen的叶节点。(内部节点的IP是网段的中间值,比如网段是192.168.8.0/24,那么节点的ip就是192.168.8.128, 比较时先假设网段相同,IP小的在左边,IP大的在右边)

while (node->bit < bitlen || node->prefix == NULL) {
if (node->bit < radix->maxbits &&
BIT_TEST(addr[node->bit >> 3], 0x80 >> (node->bit & 0x07)))
{
if (node->r == NULL)
break;
node = node->r;
} else {

if (node->l == NULL)
break;
node = node->l;
}

INSIST(node != NULL);
}

比较新节点和node,找到第一个不同的位

/* Find the first bit different. */
check_bit = (node->bit < bitlen) ? node->bit : bitlen;
differ_bit = 0;

for (i = 0; i*8 < check_bit; i++) {
if ((r = (addr[i] ^ test_addr[i])) == 0) {

differ_bit = (i + 1) * 8;
continue;
}
/* I know the better way, but for now. */
for (j = 0; j < 8; j++) {
if (BIT_TEST (r, (0x80 >> j)))
break;
}
/* Must be found. */
INSIST(j < 8);
differ_bit = i * 8 + j;
break;
}

如果differ_bit小于node->parent->bit,说明新节点和node不在同一个网段,node节点上移,直到node和新节点 同处于node的父节点网段下

if (differ_bit > check_bit)
differ_bit = check_bit;

parent = node->parent;
while (parent != NULL && parent->bit >= differ_bit) {
node = parent;
parent = node->parent;
}

下面就分情况,具体就是确定父节点,子节点 和为节点的prefix赋值

1、 新节点和node是同一网段

if (differ_bit == bitlen && node->bit == bitlen) {
......
}

2、 新节点是node的子网段

if (node->bit == differ_bit) {
......
}

3、 node是新节点的子网段

if (bitlen == differ_bit) {
......
}

4、新节点和node是两个不相关的网段

tinyhttpd源码解析,最简单的HTTP服务器

· 阅读需 15 分钟

简介

tinyhttpd 是一个不到 500 行的超轻量型 Http Server,全部用ANSI C编写,用来学习非常不错,可以帮助我们真正理解服务器程序的本质。 看完所有源码,真的感觉有很大收获,无论是 unix 的编程,还是 GET/POST 的 Web 处理流程,都清晰了不少。废话不说,开始我们的 Server 探索之旅。

主要函数

所有函数的声明

void accept_request(int);  
void bad_request(int);
void cat(int, FILE *);
void cannot_execute(int);
void error_die(const char *);
void execute_cgi(int, const char *, const char *, const char *);
int get_line(int, char *, int);
void headers(int, const char *);
void not_found(int);
void serve_file(int, const char *);
int startup(u_short *);
void unimplemented(int);

先简单地解释每个函数的作用:

accept_request: 处理从套接字上监听到的一个 HTTP 请求,在这里可以很大一部分地体现服务器处理请求流程。

bad_request: 返回给客户端这是个错误请求,HTTP 状态吗 400 BAD REQUEST.

cat: 读取服务器上某个文件写到 socket 套接字。

cannot_execute: 主要处理发生在执行 cgi 程序时出现的错误。

error_die: 把错误信息写到 perror 并退出。

execute_cgi: 运行 cgi 程序的处理,也是个主要函数。

get_line: 读取套接字的一行,把回车换行等情况都统一为换行符结束。

headers: 把 HTTP 响应的头部写到套接字。

not_found: 主要处理找不到请求的文件时的情况。

sever_file: 调用 cat 把服务器文件返回给浏览器。

startup: 初始化 httpd 服务,包括建立套接字,绑定端口,进行监听等。

unimplemented: 返回给浏览器表明收到的 HTTP 请求所用的 method 不被支持。

工作流程

  1. 服务器启动,在指定端口或随机选取端口绑定 httpd 服务。
  2. 收到一个 HTTP 请求时(其实就是 listen 的端口 accpet 的时候),派生一个线程运行 accept_request 函数。
  3. 取出 HTTP 请求中的 method (GET 或 POST) 和 url,。对于 GET 方法,如果有携带参数,则 query_string 指针指向 url 中 ? 后面的 GET 参数。
  4. 格式化 url 到 path 数组,表示浏览器请求的服务器文件路径,在 tinyhttpd 中服务器文件是在 htdocs 文件夹下。当 url 以 / 结尾,或 url 是个目录,则默认在 path 中加上 index.html,表示访问主页。
  5. 如果文件路径合法,对于无参数的 GET 请求,直接输出服务器文件到浏览器,即用 HTTP 格式写到套接字上,跳到(10)。其他情况(带参数 GET,POST 方式,url 为可执行文件),则调用 excute_cgi 函数执行 cgi 脚本。
  6. 读取整个 HTTP 请求并丢弃,如果是 POST 则找出 Content-Length. 把 HTTP 200 状态码写到套接字。
  7. 建立两个管道,cgi_input 和 cgi_output, 并 fork 一个进程。
  8. 在子进程中,把 STDOUT 重定向到 cgi_outputt 的写入端,把 STDIN 重定向到 cgi_input 的读取端,关闭 cgi_input 的写入端 和 cgi_output 的读取端,设置 request_method 的环境变量,GET 的话设置 query_string 的环境变量,POST 的话设置 content_length 的环境变量,这些环境变量都是为了给 cgi 脚本调用,接着用 execl 运行 cgi 程序。
  9. 在父进程中,关闭 cgi_input 的读取端 和 cgi_output 的写入端,如果 POST 的话,把 POST 数据写入 cgi_input,已被重定向到 STDIN,读取 cgi_output 的管道输出到客户端,该管道输入是 STDOUT。接着关闭所有管道,等待子进程结束。
  10. 关闭与浏览器的连接,完成了一次 HTTP 请求与回应,因为 HTTP 是无连接的。

注释版源码

/* J. David's webserver */
/* This is a simple webserver.
* Created November 1999 by J. David Blackstone.
* CSE 4344 (Network concepts), Prof. Zeigler
* University of Texas at Arlington
*/
/* This program compiles for Sparc Solaris 2.6.
* To compile for Linux:
* 1) Comment out the #include <pthread.h> line.
* 2) Comment out the line that defines the variable newthread.
* 3) Comment out the two lines that run pthread_create().
* 4) Uncomment the line that runs accept_request().
* 5) Remove -lsocket from the Makefile.
*/
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <strings.h>
#include <string.h>
#include <sys/stat.h>
#include <pthread.h>
#include <sys/wait.h>
#include <stdlib.h>

#define ISspace(x) isspace((int)(x))

#define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n"

void accept_request(int);
void bad_request(int);
void cat(int, FILE *);
void cannot_execute(int);
void error_die(const char *);
void execute_cgi(int, const char *, const char *, const char *);
int get_line(int, char *, int);
void headers(int, const char *);
void not_found(int);
void serve_file(int, const char *);
int startup(u_short *);
void unimplemented(int);

/**********************************************************************/
/* A request has caused a call to accept() on the server port to
* return. Process the request appropriately.
* Parameters: the socket connected to the client */
/**********************************************************************/
void accept_request(int client)
{
char buf[1024];
int numchars;
char method[255];
char url[255];
char path[512];
size_t i, j;
struct stat st;
int cgi = 0; /* becomes true if server decides this is a CGI program */
char *query_string = NULL;

/*得到请求的第一行*/
numchars = get_line(client, buf, sizeof(buf));
i = 0; j = 0;
/*把客户端的请求方法存到 method 数组*/
while (!ISspace(buf[j]) && (i < sizeof(method) - 1))
{
method[i] = buf[j];
i++; j++;
}
method[i] = '\0';

/*如果既不是 GET 又不是 POST 则无法处理 */
if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
{
unimplemented(client);
return;
}

/* POST 的时候开启 cgi */
if (strcasecmp(method, "POST") == 0)
cgi = 1;

/*读取 url 地址*/
i = 0;
while (ISspace(buf[j]) && (j < sizeof(buf)))
j++;
while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf)))
{
/*存下 url */
url[i] = buf[j];
i++; j++;
}
url[i] = '\0';

/*处理 GET 方法*/
if (strcasecmp(method, "GET") == 0)
{
/* 待处理请求为 url */
query_string = url;
while ((*query_string != '?') && (*query_string != '\0'))
query_string++;
/* GET 方法特点,? 后面为参数*/
if (*query_string == '?')
{
/*开启 cgi */
cgi = 1;
*query_string = '\0';
query_string++;
}
}

/*格式化 url 到 path 数组,html 文件都在 htdocs 中*/
sprintf(path, "htdocs%s", url);
/*默认情况为 index.html */
if (path[strlen(path) - 1] == '/')
strcat(path, "index.html");
/*根据路径找到对应文件 */
if (stat(path, &st) == -1) {
/*把所有 headers 的信息都丢弃*/
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));
/*回应客户端找不到*/
not_found(client);
}
else
{
/*如果是个目录,则默认使用该目录下 index.html 文件*/
if ((st.st_mode & S_IFMT) == S_IFDIR)
strcat(path, "/index.html");
if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH) )
cgi = 1;
/*不是 cgi,直接把服务器文件返回,否则执行 cgi */
if (!cgi)
serve_file(client, path);
else
execute_cgi(client, path, method, query_string);
}

/*断开与客户端的连接(HTTP 特点:无连接)*/
close(client);
}

/**********************************************************************/
/* Inform the client that a request it has made has a problem.
* Parameters: client socket */
/**********************************************************************/
void bad_request(int client)
{
char buf[1024];

/*回应客户端错误的 HTTP 请求 */
sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n");
send(client, buf, sizeof(buf), 0);
sprintf(buf, "Content-type: text/html\r\n");
send(client, buf, sizeof(buf), 0);
sprintf(buf, "\r\n");
send(client, buf, sizeof(buf), 0);
sprintf(buf, "<P>Your browser sent a bad request, ");
send(client, buf, sizeof(buf), 0);
sprintf(buf, "such as a POST without a Content-Length.\r\n");
send(client, buf, sizeof(buf), 0);
}

/**********************************************************************/
/* Put the entire contents of a file out on a socket. This function
* is named after the UNIX "cat" command, because it might have been
* easier just to do something like pipe, fork, and exec("cat").
* Parameters: the client socket descriptor
* FILE pointer for the file to cat */
/**********************************************************************/
void cat(int client, FILE *resource)
{
char buf[1024];

/*读取文件中的所有数据写到 socket */
fgets(buf, sizeof(buf), resource);
while (!feof(resource))
{
send(client, buf, strlen(buf), 0);
fgets(buf, sizeof(buf), resource);
}
}

/**********************************************************************/
/* Inform the client that a CGI script could not be executed.
* Parameter: the client socket descriptor. */
/**********************************************************************/
void cannot_execute(int client)
{
char buf[1024];

/* 回应客户端 cgi 无法执行*/
sprintf(buf, "HTTP/1.0 500 Internal Server Error\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-type: text/html\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "<P>Error prohibited CGI execution.\r\n");
send(client, buf, strlen(buf), 0);
}

/**********************************************************************/
/* Print out an error message with perror() (for system errors; based
* on value of errno, which indicates system call errors) and exit the
* program indicating an error. */
/**********************************************************************/
void error_die(const char *sc)
{
/*出错信息处理 */
perror(sc);
exit(1);
}

/**********************************************************************/
/* Execute a CGI script. Will need to set environment variables as
* appropriate.
* Parameters: client socket descriptor
* path to the CGI script */
/**********************************************************************/
void execute_cgi(int client, const char *path, const char *method, const char *query_string)
{
char buf[1024];
int cgi_output[2];
int cgi_input[2];
pid_t pid;
int status;
int i;
char c;
int numchars = 1;
int content_length = -1;

buf[0] = 'A'; buf[1] = '\0';
if (strcasecmp(method, "GET") == 0)
/*把所有的 HTTP header 读取并丢弃*/
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));
else /* POST */
{
/* 对 POST 的 HTTP 请求中找出 content_length */
numchars = get_line(client, buf, sizeof(buf));
while ((numchars > 0) && strcmp("\n", buf))
{
/*利用 \0 进行分隔 */
buf[15] = '\0';
/* HTTP 请求的特点*/
if (strcasecmp(buf, "Content-Length:") == 0)
content_length = atoi(&(buf[16]));
numchars = get_line(client, buf, sizeof(buf));
}
/*没有找到 content_length */
if (content_length == -1) {
/*错误请求*/
bad_request(client);
return;
}
}

/* 正确,HTTP 状态码 200 */
sprintf(buf, "HTTP/1.0 200 OK\r\n");
send(client, buf, strlen(buf), 0);

/* 建立管道*/
if (pipe(cgi_output) < 0) {
/*错误处理*/
cannot_execute(client);
return;
}
/*建立管道*/
if (pipe(cgi_input) < 0) {
/*错误处理*/
cannot_execute(client);
return;
}

if ((pid = fork()) < 0 ) {
/*错误处理*/
cannot_execute(client);
return;
}
if (pid == 0) /* child: CGI script */
{
char meth_env[255];
char query_env[255];
char length_env[255];

/* 把 STDOUT 重定向到 cgi_output 的写入端 */
dup2(cgi_output[1], 1);
/* 把 STDIN 重定向到 cgi_input 的读取端 */
dup2(cgi_input[0], 0);
/* 关闭 cgi_input 的写入端 和 cgi_output 的读取端 */
close(cgi_output[0]);
close(cgi_input[1]);
/*设置 request_method 的环境变量*/
sprintf(meth_env, "REQUEST_METHOD=%s", method);
putenv(meth_env);
if (strcasecmp(method, "GET") == 0) {
/*设置 query_string 的环境变量*/
sprintf(query_env, "QUERY_STRING=%s", query_string);
putenv(query_env);
}
else { /* POST */
/*设置 content_length 的环境变量*/
sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
putenv(length_env);
}
/*用 execl 运行 cgi 程序*/
execl(path, path, NULL);
exit(0);
} else { /* parent */
/* 关闭 cgi_input 的读取端 和 cgi_output 的写入端 */
close(cgi_output[1]);
close(cgi_input[0]);
if (strcasecmp(method, "POST") == 0)
/*接收 POST 过来的数据*/
for (i = 0; i < content_length; i++) {
recv(client, &c, 1, 0);
/*把 POST 数据写入 cgi_input,现在重定向到 STDIN */
write(cgi_input[1], &c, 1);
}
/*读取 cgi_output 的管道输出到客户端,该管道输入是 STDOUT */
while (read(cgi_output[0], &c, 1) > 0)
send(client, &c, 1, 0);

/*关闭管道*/
close(cgi_output[0]);
close(cgi_input[1]);
/*等待子进程*/
waitpid(pid, &status, 0);
}
}

/**********************************************************************/
/* Get a line from a socket, whether the line ends in a newline,
* carriage return, or a CRLF combination. Terminates the string read
* with a null character. If no newline indicator is found before the
* end of the buffer, the string is terminated with a null. If any of
* the above three line terminators is read, the last character of the
* string will be a linefeed and the string will be terminated with a
* null character.
* Parameters: the socket descriptor
* the buffer to save the data in
* the size of the buffer
* Returns: the number of bytes stored (excluding null) */
/**********************************************************************/
int get_line(int sock, char *buf, int size)
{
int i = 0;
char c = '\0';
int n;

/*把终止条件统一为 \n 换行符,标准化 buf 数组*/
while ((i < size - 1) && (c != '\n'))
{
/*一次仅接收一个字节*/
n = recv(sock, &c, 1, 0);
/* DEBUG printf("%02X\n", c); */
if (n > 0)
{
/*收到 \r 则继续接收下个字节,因为换行符可能是 \r\n */
if (c == '\r')
{
/*使用 MSG_PEEK 标志使下一次读取依然可以得到这次读取的内容,可认为接收窗口不滑动*/
n = recv(sock, &c, 1, MSG_PEEK);
/* DEBUG printf("%02X\n", c); */
/*但如果是换行符则把它吸收掉*/
if ((n > 0) && (c == '\n'))
recv(sock, &c, 1, 0);
else
c = '\n';
}
/*存到缓冲区*/
buf[i] = c;
i++;
}
else
c = '\n';
}
buf[i] = '\0';

/*返回 buf 数组大小*/
return(i);
}

/**********************************************************************/
/* Return the informational HTTP headers about a file. */
/* Parameters: the socket to print the headers on
* the name of the file */
/**********************************************************************/
void headers(int client, const char *filename)
{
char buf[1024];
(void)filename; /* could use filename to determine file type */

/*正常的 HTTP header */
strcpy(buf, "HTTP/1.0 200 OK\r\n");
send(client, buf, strlen(buf), 0);
/*服务器信息*/
strcpy(buf, SERVER_STRING);
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), 0);
strcpy(buf, "\r\n");
send(client, buf, strlen(buf), 0);
}

/**********************************************************************/
/* Give a client a 404 not found status message. */
/**********************************************************************/
void not_found(int client)
{
char buf[1024];

/* 404 页面 */
sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");
send(client, buf, strlen(buf), 0);
/*服务器信息*/
sprintf(buf, SERVER_STRING);
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "<HTML><TITLE>Not Found</TITLE>\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "<BODY><P>The server could not fulfill\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "your request because the resource specified\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "is unavailable or nonexistent.\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "</BODY></HTML>\r\n");
send(client, buf, strlen(buf), 0);
}

/**********************************************************************/
/* Send a regular file to the client. Use headers, and report
* errors to client if they occur.
* Parameters: a pointer to a file structure produced from the socket
* file descriptor
* the name of the file to serve */
/**********************************************************************/
void serve_file(int client, const char *filename)
{
FILE *resource = NULL;
int numchars = 1;
char buf[1024];

/*读取并丢弃 header */
buf[0] = 'A'; buf[1] = '\0';
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));

/*打开 sever 的文件*/
resource = fopen(filename, "r");
if (resource == NULL)
not_found(client);
else
{
/*写 HTTP header */
headers(client, filename);
/*复制文件*/
cat(client, resource);
}
fclose(resource);
}

/**********************************************************************/
/* This function starts the process of listening for web connections
* on a specified port. If the port is 0, then dynamically allocate a
* port and modify the original port variable to reflect the actual
* port.
* Parameters: pointer to variable containing the port to connect on
* Returns: the socket */
/**********************************************************************/
int startup(u_short *port)
{
int httpd = 0;
struct sockaddr_in name;

/*建立 socket */
httpd = socket(PF_INET, SOCK_STREAM, 0);
if (httpd == -1)
error_die("socket");
memset(&name, 0, sizeof(name));
name.sin_family = AF_INET;
name.sin_port = htons(*port);
name.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
error_die("bind");
/*如果当前指定端口是 0,则动态随机分配一个端口*/
if (*port == 0) /* if dynamically allocating a port */
{
int namelen = sizeof(name);
if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
error_die("getsockname");
*port = ntohs(name.sin_port);
}
/*开始监听*/
if (listen(httpd, 5) < 0)
error_die("listen");
/*返回 socket id */
return(httpd);
}

/**********************************************************************/
/* Inform the client that the requested web method has not been
* implemented.
* Parameter: the client socket */
/**********************************************************************/
void unimplemented(int client)
{
char buf[1024];

/* HTTP method 不被支持*/
sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n");
send(client, buf, strlen(buf), 0);
/*服务器信息*/
sprintf(buf, SERVER_STRING);
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "<HTML><HEAD><TITLE>Method Not Implemented\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "</TITLE></HEAD>\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "<BODY><P>HTTP request method not supported.\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "</BODY></HTML>\r\n");
send(client, buf, strlen(buf), 0);
}

/**********************************************************************/

int main(void)
{
int server_sock = -1;
u_short port = 0;
int client_sock = -1;
struct sockaddr_in client_name;
int client_name_len = sizeof(client_name);
pthread_t newthread;

/*在对应端口建立 httpd 服务*/
server_sock = startup(&port);
printf("httpd running on port %d\n", port);

while (1)
{
/*套接字收到客户端连接请求*/
client_sock = accept(server_sock,(struct sockaddr *)&client_name,&client_name_len);
if (client_sock == -1)
error_die("accept");
/*派生新线程用 accept_request 函数处理新请求*/
/* accept_request(client_sock); */
if (pthread_create(&newthread , NULL, accept_request, client_sock) != 0)
perror("pthread_create");
}

close(server_sock);

return(0);
}

C语言发送http请求

· 阅读需 2 分钟

C语言发送http请求和普通的socket通讯,原理是一样的.无非就三步connect()连上服务器,send()发送数据,recv()接收数据.只不过发送的数据有特定的格式.下面的是简单发送一个http请求的例子

#include <netinet/in.h>
#include <sys/socket.h>
#include <netdb.h>

#include <unistd.h>
#include <string.h>
#include <stdio.h>
int main(int argc, char **argv)
{
/* 构造查询包 */
const char quary[] =
"GET / HTTP/1.0\r\n"
"Host: blog.csdn.net\r\n"
"\r\n";

const char hostname[] = "blog.csdn.net";
struct sockaddr_in sin;
struct hostent *h;
const char *cp;
int fd;
ssize_t n_written, remaining;
char buf[1024];

/* 查找该域名的地址 */
h = gethostbyname(hostname);
if(h == NULL)
{
fprintf(stderr, "Couldn't lookup %s:%s\n", hostname, hstrerror(h_errno));
return 1;
}
if (h->h_addrtype != AF_INET)
{
fprintf(stderr, "No ipv6 support, sorry.\n");
return 1;
}

/* 创建一个套接字用来连接服务器 */
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0)
{
perror("socket");
return 1;
}

/* 连接到服务器 */
sin.sin_family = AF_INET;
sin.sin_port = htons(80);
sin.sin_addr = *(struct in_addr*)h->h_addr;
if (connect(fd, (struct sockaddr *)&sin, sizeof(sin)))
{
perror("connect");
close(fd);
return 1;
}

/* 发送请求 */
cp = quary;
remaining = strlen(quary);
while(remaining)
{
n_written = send(fd,cp, remaining, 0);
if (n_written <= 0)
{
perror("send");
return 1;
}
remaining -= n_written;
cp += n_written;
}

/* 获取响应 */
while(1)
{
ssize_t result = recv(fd, buf, sizeof(buf), 0);
if (result == 0)
{
break;
}
else if (result < 0)
{
perror("recv\n");
close(fd);
return 1;
}
fwrite(buf, 1, result, stdout);
}
close(fd);
return 0;
}

C语言发送邮件(带帐号密码认证),简单的libesmtp实例

· 阅读需 3 分钟

编译

需要安装libesmtp开发环境,centos下可以用yum安装。

yum install libesmtp-devel

编译时加上-lesmtp选项,账号密码等替换成自己的

gcc -o mail mail.c -lesmtp ./mail

代码

/* mail.c */
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <unistd.h>

#include <errno.h>
#include <openssl/ssl.h>
#include <auth-client.h>
#include <libesmtp.h>

#define FAIL -1
#define OK 0

#define SUBJECT "Subject" /* 邮件主题*/
#define TO "to@to.com" /* 收件人 */
#define BODY "hello" /* 邮件正文 */
#define SERVER "smtp.163.com:25" /* 邮件服务器地址 */
#define USERNAME "wen-4@163.com" /* 邮箱账号 */
#define PASSWOED "123456789" /* 邮箱密码 */

/* 回调函数 设置账号密码 */
int authinteract(auth_client_request_t request, char **result, int fields, void *args)
{
int i;

for (i=0; i < fields; i++)
{
if (request[i].flags & AUTH_PASS)
result[i] = PASSWOED;
else
result[i] = USERNAME;
}
return 1;
}

/* 回调函数 打印客户端与服务器交互情况 可以不要 */
void monitor_cb(const char *buf, int buflen, int writing, void *arg)
{
int i;

if (writing == SMTP_CB_HEADERS)
printf("H: ");
else if(writing)
printf("C: ");
else
printf("S: ");

const char *p = buf;
for(i=0; p[i]!='\n'; i++)
{
printf("%c", p[i]);
}
printf("\n");
}

int main()
{
int ret;
FILE *tmp_fp = NULL;
smtp_session_t session;
smtp_message_t message;
smtp_recipient_t recipient;
auth_context_t authctx;
const smtp_status_t *status;

auth_client_init();
session = smtp_create_session();
message = smtp_add_message(session);

smtp_set_monitorcb(session, monitor_cb, stdout, 1);

/* 设置邮件主题 */
smtp_set_header(message, "Subject",SUBJECT);
smtp_set_header_option(message, "Subject", Hdr_OVERRIDE, 1);

/* 设置收件人 */
smtp_set_header(message, "To", NULL, TO);

/* 设置发件人 */
smtp_set_reverse_path(message, USERNAME);

/* 添加收件人 */
recipient = smtp_add_recipient(message, TO);

/* 设置服务器域名和端口 */
smtp_set_server(session, SERVER);

/* 设置邮件正文 */
tmp_fp = tmpfile();
if (tmp_fp == NULL)
{
printf("create temp file failed: %s\n", strerror(errno));
return FAIL;
}
fprintf(tmp_fp, "\r\n%s",BODY);
rewind(tmp_fp);
smtp_set_message_fp(message, tmp_fp);

/* 设置登录验证相关的东西 */
authctx = auth_create_context();
if (authctx == NULL)
{
return FAIL;
}

auth_set_mechanism_flags(authctx, AUTH_PLUGIN_PLAIN, 0);
auth_set_interact_cb(authctx, authinteract, NULL);
smtp_auth_set_context(session, authctx);

if (!smtp_start_session(session))
{
char buf[128];
printf( "SMTP server problem %s", smtp_strerror(smtp_errno(), buf, sizeof buf));
ret = FAIL;
}
else
{
/* 输出邮件发送情况 如果 status->code== 250则表明发送成功 */
status = smtp_message_transfer_status(message);
printf("%d %s", status->code, (status->text != NULL) ? status->text : "\n");
ret = OK;
}

/* 释放资源 */
smtp_destroy_session(session);
if (authctx)
auth_destroy_context(authctx);
auth_client_exit();
fclose(tmp_fp);
return ret;
}

运行结果

下面是程序运行的实际情况, 如果提示535 Error或者authentication failed表明验证失败,说明是账号密码错误,或者邮箱的设置问题。 程序运行实例

tcpdump抓包

· 阅读需 2 分钟

tcpdump是Linux下的抓包工具,通过tcpdump可以帮助我们分析网络,排除故障,安全测试等。了解tcpdump是一个系统管理员,网络工程师必不可少的专业技能。

基础知识

基本命令解析

sudo tcpdump -i eth0 -nn -v port 80

-i : 选择要抓包的接口,通常是以太网卡或者无线适配器,但也可能是vlan或者更稀奇古怪的东西。

-nn : -n显示主机名,-nn显示主机名和端口。

-v : 显示详细信息,-vv显示更多。

port 80 : 只抓80端口的数据包,当然通常是HTTP包。

显示ASCII文本

添加-A到命令行将使输出包括ascii捕获中的字符串。这样可以轻松读取并使用grep其他命令解析输出。

 sudo tcpdump -A port 80

抓某个协议的包

比如抓UDP协议的包(协议号17)

sudo tcpdump -i eth0 udp 
sudo tcpdump -i eth0 proto 17

抓某个IP的包

使用host过滤器抓取某个IP的包,包括发往这个ip的包和这个ip发过来的包

sudo tcpdump -i eth0 host 10.10.1.1

或者使用 src, dst 抓取单向传输的包

sudo tcpdump -i eth0 dst 10.10.1.20

把抓取的包写进文件

将抓的包写入文件之后可以用Wireshark或者其他分析工具进行分析

sudo tcpdump -i eth0 -s0 -w test.pcap