Docker使用/镜像制作

1.Docker基础

1.1 Docker 介绍

Docker解决了什么?

在别的电脑上不能跑的问题:

开发者经常遇到项目在自己的机器上能跑,部署到服务器上就出问题。Docker把运行环境也打包进去,确保“哪里都能跑”。

环境配置繁琐

安装依赖、配置系统库等工作复杂、容易出错。Docker一次配置好,别人直接使用镜像就可以。

版本冲突问题

比如两个项目用不同版本的Python、MySQL,传统的方式难以共存,Docker可以让它们分别在不同的容器里运行,互不影响。

开发、测试、部署一致性

Docker可以让开发环境、测试环境、线上环境完全一致,大大减少线上bug。

跨平台部署

Docker镜像支持构建一次,到处运行,跨平台部署变简单。

Docker是什么?

Docker 是一个开源的应用容器引擎,基于 Go 语言,并遵从 Apache2.0 协议开源。它可以让开发者打包应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 、windows等机器上,也可以实现虚拟化。Docker 可用于开发应用、交付应用、运行应用等场景。
容器是完全使用沙箱机制,相互之间不会有任何接口,更重要的是容器性能开销极低。Docker 允许用户将基础设施中的应用单独分割出来,形成更小的部分容器,从而提高软件交付速度。

Docker与虚拟机对比

如果物理机是一栋住宅楼,虚拟机就是大楼中的一个个套间,而容器技术就是套间里的一个个隔断。

虚拟化技术不同

  • VMware Workstation, VirtualBoX

    硬件辅助虚拟化: 是指通过硬件辅助支持模拟运行环境,使客户机操作系统可以独立运行,实现完全虚拟化的功能。

  • Docker

    操作系统层虚拟化: 这种技术将操作系统内核虚拟化,可以允许使用者空间软件实例被分割成几个独立单元,在内核中运行,而不是只有一个单一实例运行。软件实例,也被称为是一个容器、虚拟引擎、虚拟专用服务器。每个容器的进程是独立的,对使用者来说,就像是在使用自己的专用服务器。

应用场景不同

  • 虚拟机更擅长于彻底隔离整个运行环境。如:云服务提供商通常采用虚拟机技术隔离不同的用户。
  • Docker通常用与隔离不同的应用,例如前端、后端以及数据库。

资源的使用率不同

  • 虚拟机启动需要数分钟,而Docker容器可以数毫秒内启动。由于没有臃肿的操作系统,Docker可以节省大量的系统资源。

1.2 Docker架构与核心组件

IMG_0774

客户端

  • linux终端

通过终端,操作一些docker命令,完成对docker的操作

docker服务器

  • 是一个守护进程,不能和用户交互,在后台运行
  • 管理容器和镜像

docker镜像

  • 理解为一个可执行程序
  • 镜像可以启动,启动之后就是容器
  • 镜像一般不需要使用者制作

docker容器

  • 占用内存、cpu资源
  • 镜像可以启动,启动之后就是容器

镜像仓库

  • docker官方提供了一个镜像仓库 docker hub

2.Docker镜像管理

IMG_0788

2.1 镜像的搜索/获取/查看

镜像搜索

1
2
3
4
5
6
#命令
$ docker search 镜像的名字
#字段关键字
STARS - 下载量
OFFICIAL - 是否官方出品
AUTOMATED - 是否自动化编译(通过dockerfile制作)

下载镜像

1
2
3
4
#下载远程仓库(如Docker hub)中的镜像
$ docker pull 镜像名
#镜像存储目录
/var/lib/docker/image

查看镜像

1
2
3
4
5
6
#命令
#查看所有镜像
$ docker images
$ docker image ls
#查看特定镜像
$ docker image 镜像的名字

2.2 镜像别名/删除

镜像别名

1
2
$ docker tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]
#镜像ID不改变

镜像删除

1
2
3
:$ docker rmi 镜像名字/镜像ID
#如果镜像有别名,必须根据名字:tag(如果tag不是latest,如果是则可以省略)
#没有别名,可以 名字/ID 删除

镜像的导入导出

1
2
3
4
5
6
7
8
9
10
11
$ docker save [OPTIONS] IMAGE [IMAGE...]
- OPTIONS:
-o,--output string
导出的文件名
- IMAGE
本地镜像仓库中的镜像名

$ docker load [OPTIONS]
-i,--input string
string: 通过save导出之后的文件名
$ docker load < string
IMG_0788

3.Docker容器管理

IMG_0798

docker将镜像文件启动,得到一个容器,一个容器可以被看做一个操作系统

3.1 容器的查看/创建/启动

  • 容器查看
1
2
3
4
5
#命令
$ docker ps
#关键字
COMMAND: 启动之后默认执行的第一个命令
PORTS: 容器和宿主机对应的映射端口
  • 容器创建
1
2
3
4
5
6
7
8
9
10
#容器被创建,但是还不能使用,需要启动
$ docker create [OPTIONS] IMAGE [COMMAND] [ARG...]
- OPTIONS:
-i,--interactive: 创建的容器是否关联标准输入
-t,--tty: 是否给这个容器分配终端
--rm: 容器停止后是否自动销毁
--name: 给容器的名字,没指定随机生成
- IMAGE: 基于哪个镜像启动
- [COMMAND] [ARG...]
容器启动之后,指定一个默认执行的shell命令,根据实际情况指定
  • 创建新容器并启动
1
2
3
4
5
6
7
8
9
10
11
#创建并运行容器run == create + start
$ docker run [OPTIONS] IMAGE [COMMAND] [ARGS...]
- OPTIONS:
-i,--interactive:
-t,--tty:
--rm:
--name:
-d,--detach: 容器启动后是否为守护进程(后台运行)
- IMAGE:
- [COMMAND] [ARG...]
容器启动之后,指定一个默认执行的shell命令,根据实际情况指定

3.2 容器的暂停/重启

  • 暂停
1
$ docker pause 容器名/容器ID
  • 取消暂停
1
$ docker unpause 容器名/容器ID
  • 重启
1
$ docker restart 容器名/容器ID

3.3 容器的关闭/终止/删除

  • 关闭
1
2
3
#延时关闭,默认10s
$ docker stop 容器名/容器ID
-t,--time: 指定时长后关闭
  • 终止
1
2
#立刻关闭,不会延时
$ docker kill 容器名/容器ID
  • 删除
1
2
3
4
$ docker rm 容器名/容器ID
-f,--force: 可删除正在运行的容器
#批量删除容器
$ docker rm -f $(docker ps -aq)

3.4 容器的进入/查看

  • 进入容器
1
2
3
$ docker exec 参数 容器名 bash
-i: 关联标准输入
-t: 分配一个操作终端
  • 查看容器的详细信息
1
$ docker inspect 容器名/容器ID
  • 查看容器的端口信息
1
$ docker port 容器名/容器ID

IMG_0798

4.Docker数据管理

4.0 数据拷贝

1
2
3
#容器和宿主机之间的数据拷贝
$ docker cp 宿主机目录/文件 容器/容器ID:容器路径
$ docker cp 容器/容器ID:容器目录/文件 宿主路径

4.1 数据卷和使用

数据卷是什么

宿主机的存储目录,要将目录中的数据和docker容器进行数据共享

数据卷在docker中的应用

挂载目录

1
2
3
4
5
6
7
8
9
10
11
12
13
#挂载的时机:
#1.容器被创建的时候
$ docker create
#2.容器被创建并运行的时候
$ docker run
#添加需要挂载的卷
-v,--volume

#形成映射关系
$ docker run -itd --name -v 宿主机的路径:容器的路径 test ubuntu bash
-宿主机的路径:必须使用绝对路径,如果宿主机器路径不存在,会自动创建
-容器的路径:如果不存在会被自动创建

权限问题

通过-v进行数据卷挂载,默认是读写的挂载方式

  • 权限是限制容器的,rw
  • 可以修改权限,改为只读:ro

$ docker run -itd --name -v 宿主机的路径:容器的路径:ro test ubuntu bash

4.2 数据卷容器和使用

数据卷容器

就是一个普通的容器,在这个容器中提供了一个挂载目录(共享目录)

  • /数据卷容器只要被创建出来就可以使用: docker createdocker run

创建数据卷容器

1
2
3
docker create -itd --name 容器名 -v 容器的挂载目录 镜像名 shell命令
docker run -itd --name 容器名 -v 容器的挂载目录 镜像名 shell命令
- 容器的挂载目录: 如果不存在,就会自动创建

数据卷容器的挂载使用

1
2
3
4
5
6
7
8
9
10
#挂载数据卷容器的参数
--volumes-from 数据卷容器的名字/ID
#1.创建数据卷容器
docker run -itd --name containsVolume -v /volume ubuntu bash
#2.启动测试容器1,挂载数据卷容器
docker run -itd --name test1 --volumes-from containsVolume ubuntu bash
#3.启动测试容器2,挂载数据卷容器
docker run -itd --name test2 --volumes-from containsVolume ubuntu bash
#4.测试数据是否共享
docker exec -it test1 bash

数据卷容器数据卷备份

备份

1
2
3
4
5
6
#创建一个临时新容器,挂载到数据卷容器上,并且和宿主机目录形成映射关系
docker run -itd --rm --name backup --volumes-from containsVolume -v /home/backup:/xxx \
#将volume中的内容拷贝到xxx目录
ubuntu cp /volume /xxx -r
#或者压缩
ubuntu tar zcvf /xxx/backup.tar.gz /volume

还原

1
2
3
4
5
6
#创建一个临时新容器,挂载到数据卷容器上,并且和宿主机目录形成映射关系
docker run -itd --rm --name backup --volumes-from containsVolume -v /home/backup:/xxx \
#将xxx目录拷贝到volume目录
ubuntu cp /xxx /volume -r
#或者解压缩
ubuntu tar zxpf /xxx/backup.tar.gz -C /

5.Docker网络管理

5.1 docker端口映射

随机端口映射

1
2
3
#创建容器时启动端口映射
docker run -itd -P --name mynginx nginx
#在宿主机随便找一个没有占用的空闲端口和容器的80端口进行映射

指定端口映射/指定多端口映射

1
2
3
4
5
6
#使用的参数
docker run -itd -p xxx --name mynginx nginx
-p 宿主机的IP:宿主机的端口:容器的端口
宿主机的端口:容器的端口
#指定多个端口映射
docker run -itd -p 8080:80 -p 8081:81 --name mynginx nginx

5.2 网络管理

相关命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 如果多个容器想进行通信,那么这些容器必须要在同一个网络中
$ docker network --help
Usage: docker network COMMAND

Manage networks

Commands:
connect 把一个容器加入到一个网络中
create 创建一个网络
disconnect 将一个容器从网络中移出
inspect 查看网络细节
ls 列出网络
prune 移除所有不用的网络
rm 移除指定网络

$ docker network ls
NETWORK ID NAME DRIVER SCOPE
5854ecbd3fd7 bridge bridge local
25d01b5a58fd host host local
e6ba185fe6c9 none null local

bridge网络模式

独立的网络栈,独立的IP,外部访问需要端口映射

创建bridge网络

1
$ docker network create -d 网络驱动的名字 要创建的网络的名字

自定义网段与网关

1
2
3
	--subnet:	指定子网络的网段
--gateway: 指定网关
$ docker network create -d bridge --subnet 180.18.10.0/24 --gateway 180.18.10.1

在自定义网路中启动容器

1
2
docker run -itd --name test3 --network 自定义网络名字 ubuntu bash
--network: 指定要加入的网络

容器断开/连接网络

1
2
3
4
5
6
#断开网络
docker network disconnect [OPTIONS] 网络名 容器名
-OPTIONS:
-f: 强制执行
#连接网络
docker network connect 网络名 容器名

host网络模式

容器直接使用宿主机的网络栈,不需要端口映射,无网络开销

1
2
$ docker run -itd --name test4 --network host ubuntu bash
--network host

6.Dockerfile

6.1 dockerfile介绍

按照文件的规则构建镜像,告诉Docker怎么一步步做出自己想要的镜像

1
2
3
4
5
6
7
8
9
10
11
12
#构建一个基于ubuntu的docker定制镜像
#基础镜像
FROM ubuntu:latest

#执行命令
RUN mkdir hello
RUN mkdir world
RUN apt update && apt install -y nginx

#声明暴露端口
EXPOSE 80

  • 宿主机创建一个空目录,将上边的dockerfile文件放到里边
  • 在dockerfile对应的目录中执行一个命令,构建新的镜像
1
2
$ docker build -t mynginx:v1.0 dockerfile所在的目录(./)
-t,--tag: 指定构建出的镜像名字和版本

6.2 dockerfile关键字

FROM

1
2
3
4
5
6
7
FROM 镜像名	
FROM 镜像名:TAG
FROM ubuntu:22.04
#FROM必须要出现在第一行(除注释),可以多次FROM
#如果本地和远程仓库都不存在,报错
FROM nginx
FROM redis

MAINTAINER

1
MAINTAINER 维护人员信息

RUN

1
2
3
4
5
#构建镜像时执行的shell命令,如果命令有确认操作,必须加-y
RUN shell命令
RUN apt update && apt install -y nginx
RUN g++ xxx -o out
RUN ["mkdir","/home/test","-p"]

EXPOSE

1
2
3
4
#设置对外开放的端口
#让宿主机和容器开放端口形成一个映射关系,就可以访问了
# docker run -itd -p 8888:80
EXPOSE 80

6.3 dockerfile运行时指令

CMD

1
2
3
4
#容器启动后默认执行的命令
#改命令会被docker run指定的shell命令覆盖
CMD shell命令
CMD ["shell命令","参数1","参数2"]

ENTRYPOINT

1
2
3
4
#与CMD一样
#但是不会被docker run 指定的shell命令覆盖
ENTRYPOINT shell命令
ENTRYPOINT ["shell命令","参数1","参数2"]

6.4 dockerfile文件编辑指令

ADD

1
2
3
4
#将宿主机文件拷贝到镜像目录中
#如果是压缩包则进行解压缩
ADD ["宿主机文件","镜像目录/文件"]
ADD ["./a.txt","/home/test/a.txt"]

COPY

1
2
#与ADD类似,只是不会自动解压
COPY ["./a.tar.gz","/home/"]

VOLUME

1
2
3
4
5
6
7
8
#创建数据卷容器
docker run -itd --name containsVolume -v /volume ubuntu bash
#测试容器挂载数据卷容器
docker run -itd --name test1 --volumes-from containsVolume ubuntu bash


#VOLUME指令可以在镜像中声明挂载,这样只要通过该镜像创建的容器都有了挂载点
VOLUME ["/data"]

6.5 dockerfile环境指令

ENV

1
2
3
4
5
6
7
8
9
#设置环境变量, 可以在RUN之前使用,然后RUN命令时调用
#容器启动时这些环境变量都会被指定
ENV <key> <value>
ENV <key>=<value>
ENV HELLO 12345
ENV HELLO=12345
RUN mkdir /home/a/b/c/d/e/f/.../z
ENV MYPATH=/a/b/c/d/e/.../z
RUN cd /home $MYPATH

WORKDIR

1
2
3
4
5
6
7
8
9
#切换目录,为后续的RUN、CMD、ENTRYPOINT 指令配置工作目录。相当于cd
#可以多次切换
WORKDIR /path/to/work
RUN a.sh
WORKDIR /path
WORKDIR to #相对路径(cd to)
WORKDIR workdir
RUN pwd
/path/to/workdir

7.docker-compose

Compose是Docker容器进行编排的工具,定义和运行多容器应用,可以一条命令启动多个容器,使用Docker Compose不再需要使用shell脚本来启动容器。

Compose通过一个配置文件来管理多个Docker容器,在配置文件中,所有的容器通过services来定义,然后使用docker-compose脚本来启动,停止和重启应用,和应用中的服务以及所有依赖服务的容器,非常适合组合使用多个容器进行开发的场景。

配置文件默认名字: docker-compose.yaml/yml

7.0 yaml文件格式

  • 大小写敏感
  • 使用缩进表示层级关系
  • 不能使用tab建缩进,只能使用空格键
  • 缩进长度没有限制,只要对齐就表示这些元素属于一个层级
  • 字符串可以不用引号标注

7.1 docker-compose配置文件

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
version: '3'	#docker-compose 的版本
services: #服务
web: #服务名,自己起的,每个服务名对应一个启动的容器
image: nginx:latest
container_name: myweb
ports: #向外开放的端口
- "8080:80"
- "3000" #3000是容器对外开放的端口,宿主机随即分配一个与3000映射
networks:
- front-tier
- back-tier
enviroment: #环境变量
RACK_ENV: development
SHOW: 'true'
command: tree -L 3
extends:
file: common.yml
service: webapp

redis:
image: redis:latest
links:
- web
newtworks:
- back-tier

lb:
image: dockercloud/haproxy
ports:
- 80:80
networks:
-front-tier
-back-tier
#挂载目录,相当于-v
volumes:
- /var/run/docker/sock:/var/run/docker.sock
depend_on:
- web
- redis
- lb
networks: #声明网络
front-tier:
driver: bridge
back-tier:
driver: bridge

7.2 docker compose 命令

compose 服务启动、关闭、查看

1
2
3
4
5
6
7
8
9
10
11
12
前置条件:docker-compose.ymal
#启动docker容器
$ docker compose up -d
-d: 以守护进程方式启动(后台运行)#不加会占用一个终端,用来输出启动过程中的日志信息
#如果配置文件不叫docker-compose.yaml,叫temp.yaml,则要指定配置文件
$ docker compose -f temp.yaml up -d

#关闭,并删除容器
$ docker compose down

#查看启动的容器
$ docker compose ps

容器开启、关闭、删除

1
2
3
4
5
6
7
8
#启动某一个容器
$ docker compose start 服务名

#容器的关闭,没有删除
$ docker compose stop 服务名

#删除
$ docker compose rm 服务名

云服务器防止被攻击(Ubuntu/Debian)

最近云server部署不到一天就被暴力攻击了,甚至直接登录了root账户,还把我自己的本地ip封禁了,现在正好列举一些防止被网络攻击的方法

公网ip实属难得,有利有弊,一方面可以让自己非常快地连接server并且搭建网站,另一方面非常容易被坏人攻击

SSH安全性加强

1.只允许密钥登录(非密码登录)

编辑SSH配置文件

1
sudo vim /etc/ssh/sshd_config

找到并修改内容

1
2
PasswordAuthentication no #root密码登录
PubkeyAuthentication yes #ssh密钥登录

重启SSH服务

1
sudo systemctl restart ssh

2.修改默认SSH端口

讲默认的22端口改为不常见的端口(防止被扫)

1
2
#sshd_config
Port 22222 #防火墙也要放行端口

防火墙配置

启动并配置UWF防火墙

1
2
3
4
5
6
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH # 或你自定义的 SSH 端口
sudo ufw allow http
sudo ufw allow https
sudo ufw enable

Fail2ban防爆破

安装并启用Fail2ban

1
2
3
sudo apt install fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

可以屏蔽多次失败登录的IP,但自己不要故意去试,因为我已经试过了

C++实现日志库

调用日志库有两种高效的方式,基本思路是全局定义宏:这里使用第二种

  • 1.使用operator<<重载,将log信息当作流,类似于cout<<
  • 2.使用函数调用,将log信息当作函数参数,类似于print()
1
2
3
4
5
6
LOG_INFO<<"服务器启动成功,端口:"<<port;
LOG_WARN<<"连接超时:"<<connId;
LOG_ERROR<<"数据库连接失败"
LOG_INFO("服务器启动成功,端口:%d",port);
LOG_WARN("连接超时: %d",connId);
LOG_ERROR("数据库连接失败");

日志库功能

日志库(Logging Library)是用于记录运行时信息的工具组件。

作为server唯一输出的内容,日志库显得尤为重要

基本功能

  • 日志级别 : 支持不同级别(如 DEBUG、INFO、WARN、ERROR、FATAL),方便控制输出粒度。
  • 时间戳:每条日志通常包含精确的时间戳,帮助排查问题。
  • 线程信息: 可选地记录线程 ID。

日志输出

日志库可以将日志信息输出到不同的目标,包括:

  • 终端输出:打印到终端或命令行界面,用于开发和调试。
  • 文件输出:将日志写入文件中,持久存储在日志文件中。
  • 网络输出:通过网络将日志发送到远程服务器或日志收集系统。
  • 数据库:有些日志库允许将日志信息存储到数据库中。

输出示例

1
2
3
4
[2025-03-14 03:40:27][TRACE] updateChannel Poller.cc:49: fd=3 events=3
[2025-03-14 03:40:27][TRACE] EventLoop EventLoop.cc:16: EventLoop created 0x7ffd65bb0120 in thread 12521
[2025-03-14 03:40:28][TRACE] poll Poller.cc:15: 1 events happended
[2025-03-14 03:40:28][TRACE] readTimerfd TimerQueue.cc:60: TimerQueue::handleRead() reads 8 at 1745034028.327096

Logger类设计

最基本的功能 log(),输出日志

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
class Logger
{
public:
enum LogLevel
{
TRACE,
DEBUG,
INFO,
WARN,
ERROR,
FATAL,
NUM_LOG_LEVALS,
};
static void log(SourceFile file, int line, LogLevel level, const char *fmt, ...);
static void log(SourceFilefile, int line, LogLevel level, const char *func, const char *fmt...);
static void logSys(SourceFile file, int line, bool fatal, const char *fmt, ...);

static LogLevel logLevel();//设置日当前志级别
static void setLogLevel(LogLevel level);
typedef void (*OutputFunc)(const char *msg, int len);
typedef void (*FlushFunc)();
static void setOutput(OutputFunc);//设置输出方式,自定义回调如输出到终端或者输出到文件
static void setFlush(FlushFunc);//设置刷新缓冲方式

private:
Logger() = delete;//不允许实例化Logger类
};

配置的SouceFIle是一个小工具类,用于从__FILE__中提取文件名,避免日志中出现过长的路径

1
2
3
4
5
6
7
8
9
10
11
class SourceFile
{
public:
const char *data_;
int size_;

template <int N>
SourceFile(const char (&arr)[N]) : data_(arr), size_(N - 1);
explicit SourceFile(const char *filename) : data_(filename);
};

日志输出逻辑

我们重点实现的格式化日志调用方式是这样的:

1
Logger::log(__FILE__,__LINE__,Logger::INFO,fmt,...);

实现代码核心逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void formatTime(char *buf, size_t size)
{
time_t now = time(nullptr);
if (now != t_lastSecond)
{
t_lastSecond = now;
struct tm tm_time;
gmtime_r(&now, &tm_time);
snprintf(buf, size, "%4d%02d%02d %02d:%02d:%02d",
tm_time.tm_year + 1900, tm_time.tm_mon + 1, tm_time.tm_mday,
tm_time.tm_hour, tm_time.tm_min, tm_time.tm_sec);
}
}

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
void Logger::log(SourceFile file, int line, LogLevel level, const char *fmt, ...)
{
if (level < g_logLevel)
return;

std::string time_buf;
Timestamp timestamp = Timestamp::now();
time_buf = timestamp.toFormattedString(showMicroseconds);
/*初始化时间也可:
char timebuf[64];
formatTime(buf,sizeof(buf));
这里使用时间戳类避免再次实现*/

//格式化用户传入的消息
char msg_buf[1024];
va_list args;
va_start(args, fmt);
vsnprintf(msg_buf, sizeof(msg_buf), fmt, args);
va_end(args);

//拼接最终输出内容
char line_buf[2048];
snprintf(line_buf, sizeof(line_buf), "[%s][%s] %s:%d: %s\n",
time_buf.c_str(), levelNames[level], file.data_, line, msg_buf);

//加锁输出日志
std::lock_guard<std::mutex> lock(g_mutex);
g_output(line_buf, strlen(line_buf));
g_flush();
//如果是FATAL级别,终止程序
if (level == FATAL)
abort();
}

日志宏封装

使用宏是为了让调用更简洁,自动传入__FILE__和__LINE__,日志级别等元信息:

1
2
#define LOG_INFO(fmt, ...) \
Logger::log(Logger::SourceFile(__FILE__), __LINE__, Logger::INFO, fmt, ##__VA_ARGS__)

其他宏如LOG_DEBUG,LOG_ERROR等原理类似,且按需包含函数名

日志等级设置

可以动态设置日志等级:

1
Logger::setLogLevel(Logger::WARN);//只显示WARN以上的日志

日志等级是静态变量:

1
static Logger::LogLevel g_logLevel=Logger::INFO;

自定义输出与刷新

可以通过函数指针实现对输出的灵活回调:

1
2
3
Logger::setOutput([](const char*msg,int len)
{fwrite(msg,1,len,stdout);});
//输出到终端

也可以重定向输出到文件:

1
2
3
FILE*fp=fopen("log.txt","a");
Logger::setOutput([fp](const char* msg,int len)
{fwrite(msg,1,len,fp);});

添加日志颜色

更改levelNames[]

1
2
3
4
5
6
7
8
9
static const char *levelNames[] = {
"TRACE",
"DEBUG",
"INFO",
"WARN",
"ERROR",
"FATAL",
};

1
2
3
4
5
6
7
8
static const char *levelNames[] = {
"\033[36mTRACE\033[0m",
"\033[34mDEBUG\033[0m" ,
"\033[32mINFO\033[0m" ,
"\033[33mWARN\033[0m",
"\033[31mERROR\033[0m",
"\033[35mFATAL\033[0m",
};

注意事项

如果输出到文件,也会写入颜色控制代码(就是那些\033[32m),也会污染日志文件,可以通过条件判断是否加颜色 (比如enableColorLog)决定是否加颜色

源代码

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
/*Logger.h*/
#ifndef LOGGER_H
#define LOGGER_H
#include <cstring>
namespace mylib
{
#define LOG_TRACE(fmt, ...) \
if (mylib::Logger::logLevel() <= mylib::Logger::TRACE) \
mylib::Logger::log(mylib::Logger::SourceFile(__FILE__), __LINE__, mylib::Logger::TRACE, __func__, fmt, ##__VA_ARGS__)
#define LOG_DEBUG(fmt, ...) \
if (mylib::Logger::logLevel() <= mylib::Logger::DEBUG) \
mylib::Logger::log(mylib::Logger::SourceFile(__FILE__), __LINE__, mylib::Logger::DEBUG, __func__, fmt, ##__VA_ARGS__)
#define LOG_INFO(fmt, ...) \
mylib::Logger::log(mylib::Logger::SourceFile(__FILE__), __LINE__, mylib::Logger::INFO, fmt, ##__VA_ARGS__)
#define LOG_WARN(fmt, ...) \
mylib::Logger::log(mylib::Logger::SourceFile(__FILE__), __LINE__, mylib::Logger::WARN, fmt, ##__VA_ARGS__)
#define LOG_ERROR(fmt, ...) \
mylib::Logger::log(mylib::Logger::SourceFile(__FILE__), __LINE__, mylib::Logger::ERROR, fmt, ##__VA_ARGS__)
#define LOG_FATAL(fmt, ...) \
mylib::Logger::log(mylib::Logger::SourceFile(__FILE__), __LINE__, mylib::Logger::FATAL, fmt, ##__VA_ARGS__)
#define LOG_SYSERR(fmt, ...) \
mylib::Logger::logSys(mylib::Logger::SourceFile(__FILE__), __LINE__, false, fmt, ##__VA_ARGS__)
#define LOG_SYSFATAL(fmt, ...) \
mylib::Logger::logSys(mylib::Logger::SourceFile(__FILE__), __LINE__, true, fmt, ##__VA_ARGS__)

class Logger
{
public:
enum LogLevel
{ TRACE,DEBUG,INFO,WARN,ERROR,FATAL,NUM_LOG_LEVALS,};
class SourceFile
{
public:
const char *data_;
int size_;

template <int N>
SourceFile(const char (&arr)[N]) : data_(arr), size_(N - 1)
{
const char *slash = strrchr(data_, '/');
if (slash)
{
data_ = slash + 1;
size_ -= static_cast<int>(data_ - arr);
}
}
explicit SourceFile(const char *filename) : data_(filename)
{
const char *slash = strrchr(filename, '/');
if (slash)
{ data_ = slash + 1;}
size_ = static_cast<int>(strlen(data_));
}
};

static void log(SourceFile file, int line, LogLevel level, const char *fmt, ...);
static void log(SourceFile file, int line, LogLevel level, const char *func, const char *fmt, ...);
static void logSys(SourceFile file, int line, bool fatal, const char *fmt, ...);

static LogLevel logLevel();
static void setLogLevel(LogLevel level);
typedef void (*OutputFunc)(const char *msg, int len);
typedef void (*FlushFunc)();
static void setOutput(OutputFunc);
static void setFlush(FlushFunc);

private:
Logger() = delete;
};

extern Logger::LogLevel g_logLevel;
extern bool showMicroseconds;
extern bool enableColorLog;

};

#endif
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
146
147
148
149
150
/*Logger.cc*/
#include "Logger.h"
#include "Timestamp.h"//可以使用formatTime函数代替时间戳
#include <mutex>
#include <thread>
#include <cstdarg>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <string>
namespace mylib
{

Logger::LogLevel g_logLevel = Logger::TRACE;
bool showMicroseconds = false;
bool enableColorLog = true;

static std::mutex g_mutex;
static Logger::OutputFunc g_output = [](const char *msg, int len)
{ fwrite(msg, 1, len, stdout); };
static Logger::FlushFunc g_flush = []()
{ fflush(stdout); };

__thread char t_errnobuf[512];
__thread char t_time[32];
__thread time_t t_lastSecond;

const char *strerror_tl(int savedErrno)
{
return strerror_r(savedErrno, t_errnobuf, sizeof(t_errnobuf));
}

static void formatTime(char *buf, size_t size)
{
time_t now = time(nullptr);
if (now != t_lastSecond)
{
t_lastSecond = now;
struct tm tm_time;
gmtime_r(&now, &tm_time);
snprintf(buf, size, "%4d%02d%02d %02d:%02d:%02d",
tm_time.tm_year + 1900, tm_time.tm_mon + 1, tm_time.tm_mday,
tm_time.tm_hour, tm_time.tm_min, tm_time.tm_sec);
}
}

static const char *levelNames[] = {
mylib::enableColorLog ? "\033[36mTRACE\033[0m" : "TRACE",
mylib::enableColorLog ? "\033[34mDEBUG\033[0m" : "DEBUG",
mylib::enableColorLog ? "\033[32mINFO\033[0m" : "INFO",
mylib::enableColorLog ? "\033[33mWARN\033[0m" : "WARN",
mylib::enableColorLog ? "\033[31mERROR\033[0m" : "ERROR",
mylib::enableColorLog ? "\033[35mFATAL\033[0m" : "FATAL",
};
void Logger::log(SourceFile file, int line, LogLevel level, const char *fmt, ...)
{
if (level < g_logLevel)
return;

std::string time_buf;
Timestamp timestamp = Timestamp::now();
time_buf = timestamp.toFormattedString(showMicroseconds);

char msg_buf[1024];
va_list args;
va_start(args, fmt);
vsnprintf(msg_buf, sizeof(msg_buf), fmt, args);
va_end(args);

char line_buf[2048];
snprintf(line_buf, sizeof(line_buf), "[%s][%s] %s:%d: %s\n",
time_buf.c_str(), levelNames[level], file.data_, line, msg_buf);

std::lock_guard<std::mutex> lock(g_mutex);
g_output(line_buf, strlen(line_buf));
g_flush();
if (level == FATAL)
abort();
}

void Logger::log(SourceFile file, int line, LogLevel level, const char *func, const char *fmt, ...)
{
if (level < g_logLevel)
return;

std::string time_buf;
Timestamp timestamp = Timestamp::now();
time_buf = timestamp.toFormattedString(showMicroseconds);

char msg_buf[1024];
va_list args;
va_start(args, fmt);
vsnprintf(msg_buf, sizeof(msg_buf), fmt, args);
va_end(args);

char line_buf[2048];
snprintf(line_buf, sizeof(line_buf), "[%s][%s] %s %s:%d: %s\n",
time_buf.c_str(), levelNames[level], func, file.data_, line, msg_buf);

std::lock_guard<std::mutex> lock(g_mutex);
g_output(line_buf, strlen(line_buf));
g_flush();
if (level == FATAL)
abort();
}

void Logger::logSys(SourceFile file, int line, bool fatal, const char *fmt, ...)
{
int savedErrno = errno;
LogLevel level = fatal ? FATAL : ERROR;
if (level < g_logLevel)
return;

std::string time_buf;
Timestamp timestamp = Timestamp::now();
time_buf = timestamp.toFormattedString(showMicroseconds);

char msg_buf[1024];
va_list args;
va_start(args, fmt);
vsnprintf(msg_buf, sizeof(msg_buf), fmt, args);
va_end(args);

char line_buf[2048];
snprintf(line_buf, sizeof(line_buf), "[%s][%s] %s:%d: %s (errno=%d: %s)\n",
time_buf.c_str(), levelNames[level], file.data_, line, msg_buf, savedErrno, strerror_tl(savedErrno));

std::lock_guard<std::mutex> lock(g_mutex);
g_output(line_buf, strlen(line_buf));
g_flush();
if (fatal)
abort();
}

Logger::LogLevel Logger::logLevel() { return g_logLevel; }

void Logger::setLogLevel(LogLevel level) { g_logLevel = level; }

void Logger::setOutput(OutputFunc output)
{
std::lock_guard<std::mutex> lock(g_mutex);
g_output = output;
}

void Logger::setFlush(FlushFunc flush)
{
std::lock_guard<std::mutex> lock(g_mutex);
g_flush = flush;
}
};

解决github Connection closed by port 22

最近china的防火墙升级,屏蔽了22端口。GitHub 支持通过 443 端口使用 SSH(因为 443 通常用于 HTTPS 流量,不易被屏蔽)。

问题示例

1
2
3
4
5
test git:(master) git push origin master
Connection closed by 20.205.243.166 port 22
fatal: 无法读取远程仓库。

请确认您有正确的访问权限并且仓库存在。

配置SSH默认使用443端口

1.修改SSH配置文件

1
vim ~/.ssh/config
1
2
3
4
5
#添加以下内容
Host github.com
Hostname ssh.github.com
Port 443
User git

2.设置文件权限

1
chmod 600 ~/.ssh/config

3.测试连接

1
ssh -T git@github.com

可以看到

1
Hi guy! You've successfully authenticated, but GitHub does not provide shell access.

这样就可以重新 git push origin main/master

无代理下安装V2RayA(Debian/Ubuntu)

阿安装v2ray的一般方法需要梯子,这几乎是个死循环,这里有两个不常见的方法

方法一:通过软件源安装(推荐)

安全添加 V2RayA 软件源的 GPG 公钥

1
wget -qO - https://apt.v2raya.org/key/public-key.asc | sudo tee /etc/apt/keyrings/v2raya.asc

添加V2RayA软件源

1
2
echo "deb [signed-by=/etc/apt/keyrings/v2raya.asc] https://apt.v2raya.org/ v2raya main" | sudo tee /etc/apt/sources.list.d/v2raya.list
sudo apt update

安装V2RayA

1
sudo apt install v2raya v2ray 

启动v2rayA/自启动v2rayA

  • 启动v2rayA

    1
    sudo systemctl start v2raya.service
  • 设置开机自启动

    1
    sudo systemctl enable v2raya.service

方法二:通过Snap包安装

安装Snap

1
2
sudo apt update
sudo apt install snapd

安装V2RayA

1
sudo snap install v2raya  #默认启动和自启动

为什么不推荐snap 呢,因为snap安装的v2raya有时候有点bug