0%

有时我们想查看C/C++代码对应的汇编代码,可以通过gcc/g++的编译选项-S来生成,但这样的代码可读性较差。

可以增加-fverbose-asm来增加可读性

1
g++ -O0 -o temp.s temp.cpp -std=c++17 -g -S -fverbose-asm

还有另一个方法,先生成目标文件再用objdump进行反汇编,我发现这样生成的汇编代码可读性更好。

1
g++ -O0 -o temp temp.cpp -std=c++17 -g && objdump -S -t -D temp > temp.s

之前写过在Python中监视卡死崩溃退出并打印卡死处的调用堆栈

在此记录一下C++的版本,不过没有在代码层面实现堆栈打印,可以通过core dump和gdb来查看崩溃时的堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// WatchDog.h
#pragma once

#include <cstdint>
#include <mutex>
#include <thread>
#include <atomic>
#include <condition_variable>

class WatchDog {
public:
WatchDog(int timeout=10, bool echo=false); // seconds
~WatchDog() { stop(); }
void stop();
void kick();

private:
void dog();
void bark();

private:
const int _timeout;
const int _echo;
std::atomic<int64_t> _last_kicked_ts;

std::mutex _mutex;
bool _stopped; // protected by _mutex
std::condition_variable _cond; // protected by _mutex

std::thread _dog;
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// WatchDog.cpp
#include "WatchDog.h"
#include <iostream>

using namespace std;

namespace {

int64_t get_gmtime_us(){
std::chrono::system_clock clock;
return std::chrono::duration_cast<std::chrono::microseconds>(
clock.now().time_since_epoch()).count();
}

} // namespace


WatchDog::WatchDog(int timeout, bool echo)
: _timeout(timeout)
, _echo(echo)
, _last_kicked_ts(get_gmtime_us())
, _stopped(false)
, _dog(&WatchDog::dog, this) {
}


void WatchDog::stop() {
do {
std::unique_lock<std::mutex> lock(_mutex);
_stopped = true;
_cond.notify_one(); // wake up the dog
} while(false);

try {
_dog.join();
}
catch (...) {
// it's ok, could already be dead
}
}

void WatchDog::kick() {
_last_kicked_ts = get_gmtime_us();
}

void WatchDog::dog() {
std::unique_lock<std::mutex> lock(_mutex);
while (true) {
if (_stopped) return;

int64_t ts = get_gmtime_us();
if (ts - _last_kicked_ts > _timeout * 1000000) {
bark();
}

if (_echo) {
std::cout << "Successful dog check"
<< " [ts] " << ts
<< " [last_kicked_ts] " << _last_kicked_ts << std::endl;
}

// wake up when notified, or every N seconds
int n = std::max(_timeout / 3, 1);
_cond.wait_for(lock, std::chrono::seconds(n));
}
}

void WatchDog::bark() {
if (_echo) {
std::cout << "\n!!!!! WATCH DOG FAILURE TRIGGERED !!!!!" << std::endl;
}
abort();
}

kTrade是一个交易柜台中间层,将不同券商提供的不同交易柜台封装成统一接口,以include+lib的方式提供统一的对接方案,交易系统只需要对接kTrade提供的统一接口,由kTrade处理各个柜台的区别。

QuoteApi

kTrade QuoteApi架构图

TradeApi

kTrade TradeApi架构图

iperf3用于测试两台主机间的带宽,主要参数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
-p, --port #,Server 端监听、Client 端连接的端口号; 
-f, --format [kmgKMG],报告中所用的数据单位,Kbits, Mbits, KBytes, Mbytes;
-i, --interval #,每次报告的间隔,单位为秒;
-F, --file name,测试所用文件的文件名。如果使用在 Client 端,发送该文件用作测试;如果使用在 Server 端,则是将数据写入该文件,而不是丢弃;
-A, --affinity n/n,m,设置 CPU 亲和力;
-B, --bind ,绑定指定的网卡接口;
-V, --verbose,运行时输出更多细节;
-J, --json,运行时以 JSON 格式输出结果;
--logfile f,输出到文件;
-d, --debug,以 debug 模式输出结果;
-v, --version,显示版本信息并退出;
-h, --help,显示帮助信息并退出。
Server 端参数:
-s, --server,以 Server 模式运行;
-D, --daemon,在后台以守护进程运行;
-I, --pidfile file,指定 pid 文件;
-1, --one-off,只接受 1 次来自 Client 端的测试,然后退出。
Client 端参数
-c, --client ,以 Client 模式运行,并指定 Server 端的地址;
-u, --udp,以 UDP 协议进行测试;
-b, --bandwidth #[KMG][/#],限制测试带宽。UDP 默认为 1Mbit/秒,TCP 默认无限制;
-t, --time #,以时间为测试结束条件进行测试,默认为 10 秒;
-n, --bytes #[KMG],以数据传输大小为测试结束条件进行测试;
-k, --blockcount #[KMG],以传输数据包数量为测试结束条件进行测试;
-l, --len #[KMG],读写缓冲区的长度,TCP 默认为 128K,UDP 默认为 8K;
--cport ,指定 Client 端运行所使用的 TCP 或 UDP 端口,默认为临时端口;
-P, --parallel #,测试数据流并发数量;
-R, --reverse,反向模式运行(Server 端发送,Client 端接收);
-w, --window #[KMG],设置套接字缓冲区大小,TCP 模式下为窗口大小;
-C, --congestion ,设置 TCP 拥塞控制算法(仅支持 Linux 和 FreeBSD );
-M, --set-mss #,设置 TCP/SCTP 最大分段长度(MSS,MTU 减 40 字节);
-N, --no-delay,设置 TCP/SCTP no delay,屏蔽 Nagle 算法;
-4, --version4,仅使用 IPv4;
-6, --version6,仅使用 IPv6;
-S, --tos N,设置 IP 服务类型(TOS,Type Of Service);
-L, --flowlabel N,设置 IPv6 流标签(仅支持 Linux);
-Z, --zerocopy,使用 “zero copy”(零拷贝)方法发送数据;
-O, --omit N,忽略前 n 秒的测试;
-T, --title str,设置每行测试结果的前缀;
--get-server-output,从 Server 端获取测试结果;
--udp-counters-64bit,在 UDP 测试包中使用 64 位计数器(防止计数器溢出)。

用例

服务端

1
iperf3 -s -p 30088

客户端

1
iperf3 -c server_ip -p 30088

找到C:\Program Files\Sublime Text 3\Packages\C++.sublime-package
复制到一个有写权限的目录,重命名为C++.sublime-package.zip
打开压缩包找到C Single File.sublime-buildC++ Single File.sublime-build

分别对应的C和C的单文件构建命令,以C为例,打开编辑,找到其中的

1
"shell_cmd": "g++ ***"

有两条,应该一条是编译一条是编译并运行,分别增加需要的编译参数即可,比如在g++的后面加上--std=c++17
把修改更新进压缩包,重命名回C++.sublime-package并覆盖回原来的位置即可。

使用gdb启动程序

1
gdb <可执行文件名>

启动后执行运行命令run可以简写为r

程序就会从入口开始运行,也可以带上命令行参数运行run [参数1] [参数2] ...

参看当前函数调用堆栈

1
backtrace

可以简写为bt,在程序崩溃后查看当前崩溃的位置和调用堆栈,这估计是gdb最常用的命令。

GDB常用的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
i b  // info breakpoint
b 10 // set breakpoint at line 10
b main.cpp:10 // set breakpoint at line 10 in file main.cpp
b func // set breakpoint at function "func"
b A::A // 设置成员函数的断点
b func(int) // 设置函数断点时指定函数类型,以便在有重载时区分
c // continue
s // 单步执行并在函数调用时进入函数 step in
n // 但不是执行并在函数调用时不进入函数而当成一步完成 step over
finish // 执行完当前函数跳到上层调用处 step out
p x // print the value of x
i ar // info args 查看函数参数的值
i lo // info locals 查看所有局部变量的值
bt // backtrace 查看调用栈
f 1 // 设置栈针到level 1,断点停的地方是level 0,level 1就是上一层,根据bt返回的结果设置,切换栈针后再用i ar和i lo查看当前栈针的变量信息
i thr // info thread 查看线程信息
thr 2 // thread 2 切换当前线程到id:2,根据i thr的返回结果设置
q // quit GDB

配置core dump

如果程序不是通过gdb启动运行的我们也想查看它崩溃时的调用堆栈,则可以通过core dump文件,它会保留崩溃时的现场。首先我们确保运行的可执行文件无论是Debug版还是Release版应该携带了调试符号,即编译选项中加入了-g或-ggdb。然后通过下面的方式启用core dump文件生成:

确保apportsystemd-coredump两个服务存在

1
2
dpkg -l | grep apport
dpkg -l | grep systemd-coredump

如果不存在则安装

1
2
apt install apport 
apt install systemd-coredump

如果存在则确保在运行

1
2
systemctl status apport
systemctl status systemd-coredump.socket

如果没有运行则启动

1
2
systemctl start apport
systemctl start systemd-coredump.socket

默认的core dump文件会生成在/var/lib/systemd/coredump/目录下,为了方便调试我们修改到可执行文件同目录

运行程序前在shell窗口或者shell脚本内先执行

1
2
ulimit -c unlimited
echo "core.%e.%p" | sudo tee /proc/sys/kernel/core_pattern

之后运行的程序崩溃时就会在程序启动的“当前目录”生成core dump文件。文件名是core.<可执行文件名>.<进程ID>

使用core dump

所谓使用core dump,就是通过gdb和core dump文件恢复崩溃时的现场,以便查看崩溃时的函数调用堆栈或者变量值。

用法:

1
gdb <可执行文件名> <core dump文件名>

然后就可以使用

1
bt

查看崩溃现场的调用堆栈了。

主动生成运行中进程的core dump

这种需求主要发生在进程卡死,想知道卡在何处时。

1
gcore -o <core dump文件名> <pid>

可以主动生成core dump文件,这个操作不会杀死进程,如果有需要可手动杀死。

然后参照[使用core dump](#使用core dump)的步骤去查看调用堆栈就可以了

C++11引入了新的随机数生成器mt19937

mt是因为这个伪随机数产生器基于Mersenne Twister算法。
19937是因为产生随的机数的周期长,可达到2^19937-1

虽然周期很长,但这是在种子固定后依次生成2^19937-1个数才能保证不重复,实际的应用是需要重启或者多开的,每次重启种子会重设,迭代次数又重新开始,如果我们把种子设成固定值显然每次生成的序列和前一次都会是重复的,如果我们用时间作为种子,这样可以让每次的序列都从一个随机位置开始迭代,但是这样也就无法保证本次周期内生成的数不会和之前的某次生成的重复,或许不巧的情况下生成了10个就重复了,也是有极小概率的。

为了解决这个问题,我们可以更进一步的,在用时间作为随机种子的基础上再在前面拼接当前时间,比如拼接“%Y%m%d%H%M%S”精确到秒,因为这串时间必然是递增的,即使后面不巧重复了,拼接后的整体也是唯一的,因此只要确保在一秒内没有重复即可,而在一秒内除非程序重启,否则由mt19937的周期保证不会重复。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <chrono>
#include <sstream>
#include <random>
#include <mutex>
#include <iomanip>

inline
std::string gen_uuid() {
static std::mutex s_mutex;
std::lock_guard lock(s_mutex);
static std::mt19937 seed_engine(
std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock().now().time_since_epoch()).count()
);
std::stringstream ss;
int64_t t = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock().now().time_since_epoch()).count();
auto tm = std::localtime(&t);
std::uniform_int_distribution<std::size_t> random_gen;
std::size_t value = random_gen(seed_engine);
ss << std::put_time(tm, "%Y%m%d%H%M%S") << std::hex << std::setw(16) << std::setfill('0') << value;
return ss.str();
}

注意到函数整体用mutex加了锁,首先我不确定random_gen(seed_engine)是否线程安全,其次我确定std::localtime不是线程安全的,因为其返回一个std::tm结构体的指针,可能是指向std::localtime内的静态成员变量,我不清楚为什么STL这样设计这个函数。

进一步改进方式,如果考虑到程序多开,可以给每个程序一个ID,在生成时传递gen_uuid(app_id)然后拼接到生成的随机字符串中。

Python版本,后面通过{:016x}截断16位16进制数是为了和C++生成的保持长度一致

1
2
3
4
5
6
7

import datetime
import uuid

def gen_uuid():
return "{}{:016x}".format(datetime.datetime.now().strftime("%Y%m%d%H%M%S"), (uuid.uuid4().int&(0xFFFFFFFFFFFFFFFF)) )

Kafka在默认配置下,保证消息至少被消费一次,即不漏,但不保证不重,下面代码实现了去重,以及从任意时间订阅历史消息。

utils/kafka-util.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# -*- coding: utf-8 -*-
import kafka
import time
import datetime
import uuid
from queue import Queue


class KafkaProducer(kafka.KafkaProducer):
def __init__(self, server):
super().__init__(
bootstrap_servers=[server],
api_version=(2,3,1),
api_version_auto_timeout_ms=5000
)

# topic可以认为是消息的信道,和subscribe时对应
# key并没有明确的意义,可以根据需要自定义,例如表示消息的类型在后面再根据key进行不同的处理,kafka底层保证相同key的消息在kafka集群时会在相同的分区上处理,从而保证相同key的消息的有序性
def send(self, topic, key, data):
assert(isinstance(data, bytes))
# 用时间+UUID拼成32位唯一值用于去重和在调试时查看发送时间
timestamp = "{}{}".format(datetime.datetime.now().strftime("%Y%m%d%H%M%S%f")[:-3], uuid.uuid4().hex[:15])
timestamp = timestamp[:32]
value = timestamp.encode() + data
return super().send(topic, key=key.encode(), value=value)


class KafkaConsumer(kafka.KafkaConsumer):
def __init__(self, server, group_id=None):
super().__init__(
bootstrap_servers=[server],
group_id=group_id,
auto_offset_reset='latest', # earliest, latest
enable_auto_commit=True,
api_version=(2,3,1),
api_version_auto_timeout_ms=5000
)
self.server = server
self.q = Queue()
self.s = set()
self.DUP_DETECT_SIZE = 1000 # 去重检测大小

# 从最新消息开始订阅
def subscribe(self, topic):
if topic not in super().topics():
# 创建一个临时的producer发送一个空消息的以便把topic创建出来,key是空的,consumer会丢弃掉
# 这并不是必要的,我觉得可能是一个BUG,先订阅一个不存在的topic,之后再send消息到这topic上,consumer可能收不到
# 后续版本可能已经修复了,不清楚是kafka-python的还是kafka的
producer = kafka.KafkaProducer(
bootstrap_servers=[self.server],
api_version=(2,3,1),
api_version_auto_timeout_ms=5000
)
producer.send(topic, key=b"", value=b"")
producer.flush()
super().subscribe(topic)

# 实现从一个历史时间点进行消息订阅(能订阅到的消息取决于Kafka服务器配置的保留策略,基于目前的配置可以保证72小时内的消息可以重复消费)
def subscribe_from_datetime(self, topic, dt):
if topic not in super().topics():
# 创建一个临时的producer发送一个空消息的以便把topic创建出来,key是空的,consumer会丢弃掉
producer = kafka.KafkaProducer(
bootstrap_servers=[self.server],
api_version=(2,3,1),
api_version_auto_timeout_ms=5000
)
producer.send(topic, key=b"", value=b"")
producer.flush()
if type(dt) is int or type(dt) is float:
ts = dt
elif isinstance(dt, datetime.datetime):
ts = dt.timestamp()
else:
ts = time.mktime(time.strptime(f"{dt}", r"%Y-%m-%d %H:%M:%S"))
offset = self._get_offset_for_time(topic, ts)
partition = 0
tp = kafka.TopicPartition(topic, partition)
super().assign([tp])
super().seek(tp, offset)

def __iter__(self):
return self

# 重新封装阻塞取消息方式,增加去重
# 迭代的元素类型是三元组(key, data, timestamp)
def __next__(self):
while True:
message = super().__next__()
msg_type = message.key
if not msg_type or len(message.value) < 32:
continue
timestamp = message.value[:32].decode()
if timestamp in self.s: # 重复的消息
continue
if len(self.s) >= self.DUP_DETECT_SIZE:
e = self.q.get()
self.s.remove(e)
self.s.add(timestamp)
self.q.put(timestamp)

data = message.value[32:]
return (msg_type.decode(), data, timestamp)

# 重新封装非阻塞取消息方式,增加去重
# 返回三元组(key, data, timestamp)构成的list
def get_messages(self, max_records=20):
r = super().poll(timeout_ms=max_records*25, max_records=max_records)
ret = []
for messages in r.values():
for message in messages:
msg_type = message.key
if not msg_type or len(message.value) < 32:
continue
timestamp = message.value[:32].decode()
if timestamp in self.s: # 重复的消息
continue
if len(self.s) >= self.DUP_DETECT_SIZE:
e = self.q.get()
self.s.remove(e)
self.s.add(timestamp)
self.q.put(timestamp)

data = message.value[32:]
ret.append((msg_type.decode(), data, timestamp))
return ret


def _get_latest_offset(self, topic):
partition = 0
tp = kafka.TopicPartition(topic, partition)
super().assign([tp])
off_set_dict = super().end_offsets([tp])
return list(off_set_dict.values())[0]

def _get_offset_for_time(self, topic, ts):
partition = 0
tp = kafka.TopicPartition(topic, partition)
super().assign([tp])
offset_dict = super().offsets_for_times({tp: int(ts*1000)})
offset = list(offset_dict.values())[0]
if offset is None:
return self.get_latest_offset(topic)
else:
return offset.offset

使用时可以配合json或msgpack之类的序列化方式,如果使用json,下面的jsonable函数可能会对你有用,它可以让日期、时间、自定义类型对象支持通过json序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import time
import datetime
from utils.kafka_util import KafkaProducer
import json
import decimal

def to_jsonable(o):
if o is None:
return o
if isinstance(o, int) or isinstance(o, float) or isinstance(o, str) or isinstance(o, bool):
return o
if isinstance(o, list) or isinstance(o, tuple) or isinstance(o, set):
return [to_jsonable(e) for e in o]
if isinstance(o, dict):
return {k: to_jsonable(v) for (k,v) in o.items()}
if isinstance(o, datetime.datetime):
return o.strftime('%Y-%m-%d %H:%M:%S')
if isinstance(o, datetime.date):
return o.strftime('%Y-%m-%d')
if isinstance(o, datetime.time):
return o.strftime('%H:%M:%S')
if isinstance(o, decimal.Decimal):
return float(o)
try:
return { k: to_jsonable(v) for k,v in vars(o).items()}
except Exception as e:
return o

producer = KafkaProducer(server="192.168.1.99")

class A:
def __init__(self):
self.x = 1
self.y = 3.14

class B:
def __init__(self):
self.x = A()
self.y = datetime.datetime.now()

producer.send("topic-test", "obj", json.dumps(to_jsonable(B())).encode() )
time.sleep(1)

如果在key中记录发送对象的类型,则接收时就可以想办法进行还原

1
2
3
4
5
6
7
8
9
10
11
12
13
from utils.kafka_util import KafkaConsumer
import time
import json


consumer = KafkaConsumer(server="192.168.1.99")
consumer.subscribe("topic-test")
while True:
messages = consumer.get_messages()
for (key, data, timestamp) in messages:
print(key, json.loads(data), timestamp)
time.sleep(0.1)

最常用的编码转换就是GB2312 -> UTF-8的转换了,GB2312是简体中文Windows的默认编码,在记事本另存为时选择的ANSI就是GB2312编码(ANSI在不同版本的操作系统中指代不同编码,仅简体中文系统中表示GB2312),GBK是GB2312的超集,GB18030是GBK的超集,相比GB2312扩充的内容包括繁体字、日韩语中的汉字、少数民族的汉字等不常用汉字。UTF-8是Linux的默认编码,UTF-8和GB2312两种编码都兼容ASCII编码,UTF-8的编码设计更灵活,是变长的编码,理论上可以无限扩充下去,可以把简体字繁体字日文韩文以及未来可能出现的新文字和符号都定义在内。

现在我们只考虑GB2312和UTF-8都能表达的简体中文以及ASCII的部分的相互转换,这也是最常用的转换。

在Linux上我们可以使用iconv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#pragma once

#include <string>
#include <iconv.h>

class EncodingConvertor
{
public:
// "UTF-8", "GBK", "GB2312", "GB18030"
EncodingConvertor(const char* toEncoding, const char* fromEncoding);

~EncodingConvertor();

std::string Convert(const char* in, size_t in_len);
std::string Convert(const char* in);
std::string Convert(const std::string& in);

private:
iconv_t _iconv;
};

std::string toUTF8(const std::string& s);
std::string toGB2312(const std::string& s);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#include "EncodingConvertor.h"
#include <cstring>

using namespace std;

EncodingConvertor::EncodingConvertor(const char* toEncoding, const char* fromEncoding) {
_iconv = iconv_open(toEncoding, fromEncoding);
}

EncodingConvertor::~EncodingConvertor() {
iconv_close(_iconv);
}


std::string EncodingConvertor::Convert(const char* in, size_t inlen) {
constexpr size_t BLOCK_SIZE = 1024; // iconv可以分块转换,对于特别长的字符串(特别大的文件)
size_t outbuflen = std::max(inlen * 2, BLOCK_SIZE);
const char* pin = in;
char* outbuf = (char*)malloc(outbuflen);
char* pout = outbuf;

int ret = 0;
do {
size_t perlen = BLOCK_SIZE;
if (pout + perlen > outbuf + outbuflen - 1) {
// 如果outbuf后面剩余空间不足一个转换块大小,就重新分配空间
int pout_offset = pout - outbuf;
outbuflen = std::max(outbuflen * 2, BLOCK_SIZE);
outbuf = (char*)realloc(outbuf, outbuflen);
pout = outbuf + pout_offset; // outbuf可能指向新地址,所以要把pout偏移过去
}
ret = iconv(_iconv, (char**)&pin, &inlen, &pout, &perlen);

if (perlen == BLOCK_SIZE) {
// 转移前后perlen没有变化应该是出错了
outbuf[0] = 0;
break;
}
} while (ret == -1);
*pout = 0;
std::string r(outbuf, pout - outbuf);
free(outbuf);
return r;
}

std::string EncodingConvertor::Convert(const char* in) {
return this->Convert(in, strlen(in));
}

std::string EncodingConvertor::Convert(const std::string& in) {
return this->Convert(in.c_str(), in.length());
}

static bool isUTF8(const char* s) {
const unsigned char* rawtext = (const unsigned char*)s;
int i, rawtextlen = 0;
int goodbytes = 0, asciibytes = 0;

// Maybe also use UTF8 Byte Order Mark: EF BB BF

// Check to see if characters fit into acceptable ranges
rawtextlen = strlen(s);
for (i = 0; i < rawtextlen; i++) {
if ((rawtext[i] & 0x7F) == rawtext[i]) { // One byte
asciibytes++;
// Ignore ASCII, can throw off count
} else {
int m_rawInt0 = (int)(unsigned char)(rawtext[i]);
int m_rawInt1 = (int)(unsigned char)(rawtext[i + 1]);
int m_rawInt2 = (int)(unsigned char)(rawtext[i + 2]);

if (256 - 64 <= m_rawInt0 && m_rawInt0 <= 256 - 33 && // Two bytes
i + 1 < rawtextlen &&
256 - 128 <= m_rawInt1 && m_rawInt1 <= 256 - 65) {
goodbytes += 2;
i++;
} else if (256 - 32 <= m_rawInt0 && m_rawInt0 <= 256 - 17 && // Three bytes
i + 2 < rawtextlen &&
256 - 128 <= m_rawInt1 && m_rawInt1 <= 256 - 65 &&
256 - 128 <= m_rawInt2 && m_rawInt2 <= 256 - 65) {
goodbytes += 3;
i += 2;
}
}
}

// 全都是ASCII码
if (asciibytes == rawtextlen) { return true; }

// 非ASCII码的均符合UTF-8规则
return (goodbytes == rawtextlen - asciibytes);
}

std::string toUTF8(const std::string& s) {
if(isUTF8(s.c_str())) return s;
return EncodingConvertor("UTF-8", "GB2312").Convert(s);
}

std::string toGB2312(const std::string& s) {
if(isUTF8(s.c_str())) return EncodingConvertor("GB2312", "UTF-8").Convert(s);
else return s;
}

在Windows可以通过WideCharToMultiByteMultiByteToWideChar两个方法完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
wchar_t * ANSIToUnicode( const char* str ) {
int textlen ;
wchar_t * result;
textlen = MultiByteToWideChar( CP_ACP, 0, str,-1, NULL,0 );
result = (wchar_t *)malloc((textlen+1)*sizeof(wchar_t));
memset(result,0,(textlen+1)*sizeof(wchar_t));
MultiByteToWideChar(CP_ACP, 0,str,-1,(LPWSTR)result,textlen );
return result;
}

char * UnicodeToANSI( const wchar_t *str ) {
char * result;
int textlen;
// wide char to multi char
textlen = WideCharToMultiByte( CP_ACP, 0, str, -1, NULL, 0, NULL, NULL );
result =(char *)malloc((textlen+1)*sizeof(char));
memset( result, 0, sizeof(char) * ( textlen + 1 ) );
WideCharToMultiByte( CP_ACP, 0, str, -1, result, textlen, NULL, NULL );
return result;
}

wchar_t * UTF8ToUnicode( const char* str ) {
int textlen ;
wchar_t * result;
textlen = MultiByteToWideChar( CP_UTF8, 0, str,-1, NULL,0 );
result = (wchar_t *)malloc((textlen+1)*sizeof(wchar_t));
memset(result,0,(textlen+1)*sizeof(wchar_t));
MultiByteToWideChar(CP_UTF8, 0,str,-1,(LPWSTR)result,textlen );
return result;
}

char * UnicodeToUTF8( const wchar_t *str ) {
char * result;
int textlen;
// wide char to multi char
textlen = WideCharToMultiByte( CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL );
result =(char *)malloc((textlen+1)*sizeof(char));
memset(result, 0, sizeof(char) * ( textlen + 1 ) );
WideCharToMultiByte( CP_UTF8, 0, str, -1, result, textlen, NULL, NULL );
return result;
}

docsify 是一个动态生成文档网站的工具。不同于 GitBook、Hexo 的地方是它不会将 .md 转成 .html 文件,所有转换工作都是在运行时进行。

需要到docsify-cli来启动web服务器

我在ubuntu18.04的环境下用node v16.19.1 (npm v9.5.1) 可以安装成功,其他环境需要自己试一下

使用nvm安装node

1
nvm install v16

升级npm

1
npm install -g npm@9.5.1

安装docsify-cli

1
npm i docsify-cli -g

初始化文档目录

1
docsify init .

启动web服务器

1
docsify serve .

默认启动在3000端口,可以通过nginx做反向代理

参考

docsify

主页模板

默认生成的index.html不太好用,这里给一个好用的模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>docsify</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0"
/>
<link
rel="stylesheet"
href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css"
title="vue"
/>
<link
rel="stylesheet"
href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/dark.css"
title="dark"
disabled
/>
<link
rel="stylesheet"
href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/buble.css"
title="buble"
disabled
/>
<link
rel="stylesheet"
href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/pure.css"
title="pure"
disabled
/>
<link
rel="stylesheet"
href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/dolphin.css"
title="dolphin"
disabled
/>
<style>
nav.app-nav li ul {
min-width: 100px;
}

#carbonads {
box-shadow: none !important;
width: auto !important;
}
</style>
</head>

<body>
<div id="app">Loading ...</div>
<script src="//cdn.jsdelivr.net/npm/docsify-plugin-carbon@1"></script>
<script>

// Docsify configuration
window.$docsify = {
auto2top: true,
coverpage: false,
executeScript: true,
loadSidebar: true,
loadNavbar: false,
mergeNavbar: false,
maxLevel: 4,
subMaxLevel: 4,
name: 'docsify',
search: 'auto',
};
</script>
<script src="//cdn.jsdelivr.net/npm/docsify@4/lib/docsify.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/docsify@4/lib/plugins/search.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-bash.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-markdown.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-nginx.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-php.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/vue@2/dist/vue.min.js"></script>
<!-- <script src="//cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script> -->
</body>
</html>

自动生成_sidebar.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# -*- coding: utf-8 -*-
import sys
import os
from functools import cmp_to_key


def mycmp_filename(name1, name2):
if name1.upper() == "README.MD":
return -1
elif name2.upper() == "README.MD":
return 1
elif name1 < name2:
return -1
elif name2 < name1:
return 1
return 0


def gen(e, path, level):
content = ""
name, ext = os.path.splitext(e)
if os.path.isfile(path):
if not name.startswith('_') and ext.lower() == ".md":
if os.path.getsize(path):
content += "{}- [{}]({})\n".format(" "*level, name, path.replace('\\', '/') )
elif os.path.isdir(path):
if name.startswith('_'):
return ""
content2 = ""
for e2 in sorted(os.listdir(path), key=cmp_to_key(mycmp_filename)):
content2 += gen(e2, os.path.join(path, e2), level+1)
if content2:
if name:
content += "{}- {}\n".format(" "*level, name)
content += content2
else:
content = ""
return content


def main():
with open("_sidebar.md", "w", encoding="utf-8") as fp:
fp.write("<!-- docs/_sidebar.md -->\n")
fp.write("{}\n".format(gen("", ".", -1)))

if __name__ == '__main__':
main()