开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Redis Server 连接管理 连接初始化 事件处理

发表于 2021-11-26

连接初始化

接收连接

Redis 服务支持以下几种连接方式:

  • TCP
  • TLS
  • Unix Socket

收到不同方式的连接请求之后,Redis 会调用启动时绑定的处理函数。TCP 连接使用 acceptTcpHandler 处理,TLS 连接使用 acceptTLSHandler 处理,Unix socket 连接使用 acceptUnixHandler 处理。这几个处理函数最终都会调用通用函数 acceptCommonHandler

acceptCommonHandler 步骤

  1. 检查连接数,如果超过了配置的最大连接数会拒绝请求,并向客户端发送错误信息。最大连接数可以在 redis.conf 的 maxclients 参数修改。
  2. 创建客户端对象
  3. 保存 flag 参数,主要用于之后区分使用 Unix Socket 连接的客户端
  4. 调用客户端连接请求处理函数 clientAcceptHandler,主要用于处理默认保护模式下,拒绝处理没有设置密码的外部设备(非 localhost)连接请求。
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
c复制代码// networking.c
static void acceptCommonHandler(connection *conn, int flags, char *ip) {
client *c;
UNUSED(ip);

// 1. 连接数检查
if (listLength(server.clients) >= server.maxclients) {
// 错误处理
// ...
return;
}

// 2. 创建客户端对象
if ((c = createClient(conn)) == NULL) {
// 错误处理
// ...
return;
}

// 3. 保存接收连接的相关参数
c->flags |= flags;

// 4. 调用 clientAcceptHandler 处理客户端连接请求
if (connAccept(conn, clientAcceptHandler) == C_ERR) {
// 错误处理
// ...
return;
}
}

创建客户端对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
c复制代码client *createClient(connection *conn) {
client *c = zmalloc(sizeof(client));

// 连接初始化
if (conn) {
// 1. 将连接设为非阻塞模式
connNonBlock(conn);
// 2. 禁用 nagel 算法
connEnableTcpNoDelay(conn);
// 3. 设置 TCP keepalive
if (server.tcpkeepalive)
connKeepAlive(conn,server.tcpkeepalive);

// 4. 设置请求处理函数
connSetReadHandler(conn, readQueryFromClient);
// 5. 让 conn->private_data 指向 client 对象
connSetPrivateData(conn, c);
}
...

步骤:

  1. 将连接设为非阻塞模式。

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/59e60246228f4a4db9fd67bc3cbaf2e7~tplv-k3u1fbpfcp-zoom-1.image

如果程序 A 不断调用 send 将数据将数据拷贝到内核缓冲区,而应用程序 B 不调用 recv ,则 B 的内核缓冲区被填满后 A 的内核缓冲也会被填满,此时 A 继续调用 send 函数结果与 socket 模式有关

* 阻塞模式:继续调用 `send/recv` 时会阻塞在调用处
* 非阻塞模式:立即出错并退出,得到错误码 EWOULDBLOCK 或 EAGAIN
  1. 设置 TCP_NODELAY,禁用 nagle 算法,存放到内核缓冲区中的数据会立即发出。否则如果一次放到内核缓冲区中的数据数据包太小,则会在多个小的数据包凑成一个足够大的数据包后才会将数据发出。
  2. 设置 TCP keepalive,作用如下:
1. 检测因服务停止、网络波动、宕机、应用重启等原因挂掉的连接
2. 防止因为网络不活动而断连(使用NAT代理或者防火墙的时候,经常会出现这种问题)
3. TCP层面的心跳检测
  1. 将请求处理函数设为 readQueryFromClient ,用于解析和处理客户端发来的请求命令。
  2. 让 conn->private_data 指向 client 对象,使 client 对象与 conn 对象相互引用
1
2
3
4
5
6
7
8
9
10
c复制代码		// 初始化 client 属性
// ...
//

// 6. 保存 client 对象
if (conn) linkClient(c);
// 7. 初始化 MULTI/EXEC 相关的参数
initClientMultiState(c);
return c;
}
  1. linkClient 保存 client 对象
1. 将 client 对象存到双向链表 `server.clients` 尾部的节点
2. 将 `server.clients` 尾部节点保存到 client 对象的 `client_list_node` 字段
3. 反转 client id 字节序,将转换后的 id 作为 key,client 对象作为 value,保存到基数树 `server.clients_index`。当后续需要通过 id 获取 client 对象时会(例如 `CLIENT UNBLOCK` 命令)从基数树中查询。反转 client id 字节序时使用 `memrev64` 函数,先将 64 位的 unsigned int 转换成 char\*,然后在 char\* 内部交换字符的顺序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c复制代码void memrev64(void *p) {
unsigned char *x = p, t;

t = x[0];
x[0] = x[7];
x[7] = t;
t = x[1];
x[1] = x[6];
x[6] = t;
t = x[2];
x[2] = x[5];
x[5] = t;
t = x[3];
x[3] = x[4];
x[4] = t;
}

对于长整型数据的映射,利用基数树可以根据一个长整型快速查找到其对应的对象指针。避免了使用 hash 映射 hash 函数难以设计,不恰当的 hash 函数可能增大冲突,或浪费空间。

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/04cb1ab60dbd41deb14c43e4c6ae4553~tplv-k3u1fbpfcp-zoom-1.image

基数树可视化工具:www.cs.usfca.edu/~galles/vis…

执行 createClient 命令时支持传入 NULL,是因为 redis 中所有命令的执行都依赖一个 client 上下文,但是在 Lua 解释器中执行脚本等情况下并没有活跃的连接,因此需要用到 conn 为 NULL 的 client。

conn 为 NULL 的 client 不会被添加到 server.clients 和 server.clients_index。

事件处理

读事件

当一个新的客户端连接到服务器时, 服务器会给为该客户端绑定读事件, 直到客户端断开连接之后, 这个读事件才会被移除。

  • 当客户端只是连接到服务器,但并没有向服务器发送命令时,读事件就处于等待状态。
  • 当客户端给服务器发送命令请求,并且请求已到达时,该客户端的读事件处于就绪状态

写事件

当服务器有命令结果要传回给客户端时, 会为客户端关联写事件, 在命令结果传送完毕之后, 客户端和写事件的关联就会被移除。

  • 当服务器有命令结果需要返回给客户端,但客户端还未能执行无阻塞写,那么写事件处于等待状态。
  • 当服务器有命令结果需要返回给客户端,并且客户端可以进行无阻塞写,那么写事件处于就绪状态。

当出现读事件和写事件同时就绪的情况时, 优先处理读事件

client 对象与事件循环

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/138e697f0c6b429d9b315ae6f1ff1fa3~tplv-k3u1fbpfcp-zoom-1.image

Redis server 启动时,会在全局对象 aeEventLoop 中使用 events 和 fired 两个字段保存了事件相关的对象:

  • events:保存注册的事件
  • fired:保存触发的事件

另外当事件的到来,就将所有就绪的事件从内核事件表中复制到 apidata→events

events, fired, apidata→events 数组的大小相同,下标是 clientfd

当 clientfd 的读事件触发后,redis server 执行 connSocketSetReadHandler 函数,然后触发 aeCreateFileEvent 函数

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
c复制代码// 读事件调用 aeCreateFileEvent 时参数依次为 
// * 全局 eventLoop 对象
// * client fd
// * AE_READABLE
// * connSocketEventHandler 函数
// * connection 对象
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData)
{
// ...

// 读取 redis server 初始化时预留的空 aeFileEvent
aeFileEvent *fe = &eventLoop->events[fd];

// aeApiAddEvent 会根据 OS 分别调用 select, epoll, kqueue, evport 4 种实现
// 其中会生成一个 epoll_event 对象 ee,将 EPOLLIN 添加到 ee.events,并将 clientfd
// 添加到 ee.data.fd
if (aeApiAddEvent(eventLoop, fd, mask) == -1)
return AE_ERR;
fe->mask |= mask;
// 设置读事件的回调函数
if (mask & AE_READABLE) fe->rfileProc = proc;
if (mask & AE_WRITABLE) fe->wfileProc = proc;
fe->clientData = clientData;
if (fd > eventLoop->maxfd)
eventLoop->maxfd = fd;
return AE_OK;
}

epoll 的 aeApiAddEvent 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
c复制代码static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;

// 判断是新增还是修改
int op = eventLoop->events[fd].mask == AE_NONE ?
EPOLL_CTL_ADD : EPOLL_CTL_MOD;

ee.events = 0;

// 保留原有的 mask
mask |= eventLoop->events[fd].mask;

if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;

// 将 clientfd 添加到 epoll_event 的 data
ee.data.fd = fd;
if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
return 0;
}

事件循环中aeApiPoll 调用 epoll_wait , epoll_wait 将触发的事件复制到 apidate->events ,然后由 aeApiPoll 中的逻辑将本次触发事件的序号作为数组下标,将 fd、事件掩码记录到 eventLoop->fired 数组对应的位置上。

aeProcessEvents 在 aeApiPoll 返回后遍历 eventLoop->fired 数组,取出有效的数组元素,得到有事件的 fd 和事件掩码 mask

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
c复制代码static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
// tvp 是后续定时任务允许等待的最大事件

aeApiState *state = eventLoop->apidata;
int retval, numevents = 0;

// epoll_wait 将所有就绪的事件从内核事件表中复制到 state->events
retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);

if (retval > 0) {
int j;

numevents = retval;
// 将触发的事件复制到 eventLoop->fired
for (j = 0; j < numevents; j++) {
int mask = 0;
struct epoll_event *e = state->events+j;

if (e->events & EPOLLIN) mask |= AE_READABLE;
if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
if (e->events & EPOLLERR) mask |= AE_WRITABLE|AE_READABLE;
if (e->events & EPOLLHUP) mask |= AE_WRITABLE|AE_READABLE;
eventLoop->fired[j].fd = e->data.fd;
eventLoop->fired[j].mask = mask;
}
}
return numevents;
}

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0fd48f5b01d64dc5be888360ae7dfdf9~tplv-k3u1fbpfcp-zoom-1.image

clientfd 为下标从 eventLoop->events 中取出 aeFileEvent 对象,然后通过 aeFileEvent 的 clientData 取出 connection 对象,进而通过 connection 对象的 private_data 得到 client 对象,用于后续处理。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

GitLab CI / CD 持续集成和单元测试

发表于 2021-11-26

这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战

前言

上一篇,主要对持续集成CI/CD的认识,这一篇主要实践操作下,解决2个问题:

  • 配置.gitlab-ci.yml
  • 接入单元测试

.gitlab-ci.yml说明

一个完整的项目的生命周期都要经历的流程:开发–打包–测试–部署。现比较流行的CI/CD手段可以有效的约束代码质量 ,完 快速部署上线。

接下来实践:基于GitLab CI / CD 持续集成Node项目

写个简单的node项目使用koa框架,增加.gitlab-ci.yml配置文件,其基本写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
yaml复制代码# 定义stages
stages:
- build
- test
- deploy
#定义job
build:
stage: build # 指向对应的stage
script:
  - npm install
#定义job
test:
stage: test
script:
    - echo "start:npm run test"
#定义job
deploy:
stage: deploy
script:
  - echo "deploy:push the project to the Product"
​
  • 关键字 stages :定义Pipeline 中的各个构建阶段,并使用非关键字来定义jobs,如文件中有三个jobs的定义:build、test、deploy,且执行时也将按照此顺序执行下去。
  • Job1/ Job2 非关键字意为各个Job名称,且可用关键字 stage 来指定该 Job 对应哪个 stage 。
  • 关键字 script :每个Job 都必须有的,表示每个Job要执行的命令,主要由 GitLab Runner 执行的shell脚本。
  • 关键字 before_script 和 after_script :是定义任何 Jobs 运行前/后都会执行的命令。

还有其他相关关键字,可以从官网上查看。了解整个基础的过程后,就可以知道怎么去设置整个集成的过程。最关键的就是script 这部分了。

CI接入单元测试

学会了持续集成,那么再增加通过CI进行单元测试,完成自动化测试工作。

根据上面所说,script 部分主要是由gitlab runner 执行shell 脚本,所以只需要在正式上线之前进行最后一次自动化测试工作就可以。所以,可以再定义一个 stage 执行单元测试 。

1
2
3
4
5
6
7
8
9
10
11
12
markdown复制代码stages:
- unitTest

unitTest:
stage: unitTest
only:
- dev
- master
script:
- echo "unit test start"
- yarn test
​

注意到看设置了Job.only,表示只有在dev 分支和 master 分支才会触发到相关Jobs。

未学习CI/CD之前一直都不是很了解整个过程,并且当时觉得加入单元测试是一项很难工作,无从入手。通过一番学习后,对整个CI/CD有个整体了解后就知道都是通过配置文件,并且配置文件中脚本是shell 脚本命令来完成整个工作。不仅可以实现接入单元测试,也可实现测试代码覆盖率并通过gitLab pages 显示单测的结果,甚至也是可以支持在其中进行 webhook 方式通知结果。

参考资料:

scarletsky.github.io/2016/07/29/…

\

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

老生常谈的TCP三次握手和四次挥手,你会了吗?

发表于 2021-11-26

本文正在参与 “网络协议必知必会”征文活动

基本概念

在了解TCP的三次握手和四次挥手前,我们首先需要了解一些基本概念,更有助于我们理解。

1、TCP序号:

TCP序号的作用是确定数据发送的顺序,比如HelloWorld可能会被切成三个数据包发送,而服务端是如何确定这三个数据包的顺序呢?这里依靠的就是TCP序号,详细内容请戳这里

下面要讲的内容会涉及到两种序号,分别是seq序号和ack序号,seq序号标志了数据包的顺序;ack序号则是确认序号,用于确认数据包是否被接收。

2、TCP报文标志位:

  • SYN:synchronous。当SYN为1时,表示发起一个连接
  • ACK:acknowledgement。当ACK为1时,表示收到数据包(注意这里的ACK是标志位,与上文的ack序号是不一样的)
  • FIN:finish。当FIN为1,表示结束一个连接

注:TCP规定当SYN被置为1的报文段不能携带数据

三次握手

关于三次握手,我们先来看看这三次握手的流程:
66135950d3ca40d0aa69608eb88432da.png

注:图片来源于参考的第二篇文章,侵权删

首先我们知道,客户端其实是通过IP+端口找到对应的应用的,所以服务器这一端必须先启动一个端口监听请求(即LISTEN)

第一次握手:从图中可以看到,第一次握手发送的数据包是SYN=1, seq=x,表示客户端请求与服务端建立连接。SYN=1的作用前面已经提过,表示发起一个连接,而seq=x则是一个序号,用来表示包的顺序。

第二次握手:假设网络畅通,服务端接收到了SYN=1, seq=x,这时它知道客户端想建立连接,为了让客户端知道服务端这边一切正常,允许建立连接,所以服务端发送了SYN=1, ACK=1, seq=y, ack=x+1的报文。

这段报文SYN=1的作用是告知客户端,服务端允许建立连接;ACK=1的作用是告知客户端,服务端已经收到之前第一次握手的报文;seq=y是服务端产生的TCP序号,以便第三次握手时进行确认;ack=x+1的作用是确定这段报文来自其请求的服务器,而不是其他服务器,因为seq并不是一个确定的数。

第三次握手:这次握手客户端会发送ACK=1, seq=x+1, ack=y+1的报文,以便让服务端知道客户端这边也准备好了,连接建立成功,可以发数据了。


整个流程其实理解不难,接下来进入经典问答环节:
1、为什么是三次握手,不能二次握手吗?

大家可以想想,建立TCP连接的目的其实是为了客户端和服务端之间能够互相传递数据,所以在传递之前,即建立连接之前,客户端和服务端必须确定对方都具备发送和接收数据的能力(双方都必须具备发送和接收数据的能力,再强调一遍),这个是理解的重点!!

如果只有两次握手,对于客户端,它是知道服务端具备发送(第二次握手)和接收(第一次握手)的能力的,但是!!!服务端只知道客户端具备发送的能力,并不知道它是否能接收数据,这就是为什么要进行第三次握手的原因。

2、什么是SYN攻击?

由上图可知,第二次握手后,服务端处于SYN_RCVD状态,当收到第三次握手的ACK后,状态才变为ESTABLISHED状态。

SYN攻击就是在短时间内伪造大量不存在的IP地址(这里的IP指的是客户端的IP),并向服务端不断发送SYN包(即第一次握手),服务端收到SYN包肯定会进行响应,但由于客户端IP不存在,收不到ACK包,所以服务端会不断重发数据包直至超时,这些伪造的SYN包将长时间占用未连接队列,导致正常的SYN请求因为队列慢而被丢弃,从而引起网络堵塞,客户端的感觉就是很卡,或者访问奔溃。

四次挥手

关于四次握手,我们继续看TCP四次挥手流程图
b2334da4fc78469184e49534d38511e1.png

注:图片来源于参考的第二篇文章,侵权删


整个流程其实和三次握手大同小异,所以我们直接进入问答环节:

1、为什么要四次挥手?两次或者三次不行吗?

这个是因为TCP连接虽然每次只处理一个请求,但这个请求是包含多个数据包的往来的,即在一个请求中,客户端有可能向服务端发送了多个数据包。

这也就是意味着服务端并不知道客户端的数据包什么时候发送完毕,所以一般关闭连接由客户端发起。(只是一般情况下),这也对应了我们上图的第一次挥手。这里还有一个注意点,第一次挥手,客户端只是停止发送数据,但还是会接收数据的。

当服务端收到了第一次挥手的数据包,如果它没有数据需要发送给客户端,它就可以同时关闭发送数据和接收数据的通道,因为客户端不会再发数据给它,它也没有数据给客户端。但它要告知客户端可以关闭接收的通道了,所以就有了第二次挥手。

所以,如果在第一次挥手后,服务端没有数据需要给客户端,两次挥手是可以关闭TCP连接的(但这时的第二次挥手会多一个FIN标志位)!

但是!!!

如果在第一次挥手时,服务端还有数据发送给客户端,此时第二次挥手只是告知客户端,服务端已经收到客户端关闭的请求了,但由于还有数据需要发送,所以第二次挥手并没有FIN标志位。

当服务端数据发送完了,它就会进行第三次挥手,告知客户端你可以关闭接收数据的通道了,客户端收到信息后,进行第四次挥手,告知服务端我这边已经关闭了,你也可以关闭了。此时TCP连接正式断开!

2、TIME-WAIT状态的作用?

细心的人或许还能发现,上面的四次挥手中,客户端还有一个TIME-WAIT的状态,在等待2MSL后,才会彻底断开连接。

之所以出现TIME-WAIT状态,有两个原因:

1.当第二次挥手的数据包比第三次挥手的数据包晚到达,客户端接收的数据就不完整,所以有个延时(TIME-WAIT),可以解决大部分丢包的问题

2.可能会由于网络问题,客户端第四次挥手的报文丢失,或者服务端比较晚收到,这时服务端处于last-ack状态,会一直重试第三次挥手的报文,如果这时候客户端的连接直接断开,没有等待,那么就无法响应。
当客户端再次发起建立连接的请求时,服务端由于处于last-ack状态,就会发送RST报文拒绝连接。

综上,这就是TIME-WAIT状态存在的原因。

参考

Tcp协议的三次喔手和四次挥手

TCP连接的建立为什么需要三次握手?为什么不能两次或者四次?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Echo 框架 基于云原生环境,区分配置文件 介绍 安装

发表于 2021-11-26

这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战

介绍

通过一个完整例子,在 Echo 框架中,根据环境区分配置文件。也就是如何在【测试】,【线上】等环境中,读取不同的配置文件。

我们将会使用 rk-boot 来启动 Echo 框架微服务。

请访问如下地址获取完整教程:

  • rkdocs.netlify.app/cn

安装

1
2
go复制代码go get github.com/rookie-ninja/rk-boot
go get github.com/rookie-ninja/rk-echo

快速开始

我们会创建 config/beijing.yaml, config/shanghai.yaml, config/default.yaml 三个配置文件,然后根据不同的环境变量读取不同的文件。

rk-boot 使用 REALM,REGION,AZ,DOMAIN 环境变量来区分不同的环境。这也是我们推荐的云原生环境分辨法。
比如,REALM=”你的业务”,REGION=”北京”,AZ=”北京一区”,DOMAIN=”测试环境”。

rk-boot 集成了 viper 来处理配置文件。

1.创建配置文件

  • config/beijing.yaml
1
2
yaml复制代码---
region: beijing
  • config/shanghai.yaml
1
2
yaml复制代码---
region: shanghai
  • config/default.yaml
1
2
yaml复制代码---
region: default

2.创建 boot.yaml

boot.yaml 文件告诉 rk-boot 如何启动 Echo 服务。

我们使用 config 作为 boot.yaml 中配置文件的入口,可以提供多个 config 文件路径。

locale 代表 Config 的环境,我们使用 locale 来区分不同的 Config。

为什么 config.name 使用同一个名字?

我们希望使用同一套代码,但是读取不同的文件,并且希望文件的名字也不一样。
所以通过 locale 来区分不同文件。我们在后面具体介绍 locale 的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yaml复制代码config:
# 默认
- name: my-config
locale: "*::*::*::*"
path: config/default.yaml
# 如果环境变量 REGION=beijing,读取此文件
- name: my-config
locale: "*::beijing::*::*"
path: config/beijing.yaml
# 如果环境变量 REGION=shanghai,读取此文件
- name: my-config
locale: "*::shanghai::*::*"
path: config/shanghai.yaml
echo:
- name: greeter
port: 8080
enabled: true

3.创建 main.go

设置环境变量:REGION=”beijing”,然后读取配置文件,config/beijing.yaml 会被读取。

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
go复制代码package main

import (
"context"
"fmt"
"github.com/rookie-ninja/rk-boot"
_ "github.com/rookie-ninja/rk-echo/boot"
"os"
)

// Application entrance.
func main() {
// Set REGION=beijing
os.Setenv("REGION", "beijing")

// Create a new boot instance.
boot := rkboot.NewBoot()

// Load config which is config/beijing.yaml
fmt.Println(boot.GetConfigEntry("my-config").GetViper().GetString("region"))

// Bootstrap
boot.Bootstrap(context.Background())

// Wait for shutdown sig
boot.WaitForShutdownSig(context.Background())
}

4.文件夹结构

1
2
3
4
5
6
7
8
9
10
go复制代码$ tree
.
├── boot.yaml
├── config
│ ├── beijing.yaml
│ ├── default.yaml
│ └── shanghai.yaml
├── go.mod
├── go.sum
└── main.go

5.验证

1
go复制代码$ go run main.go

我们会得到如下的输出:

1
复制代码beijing

6.未找到匹配的环境变量

如果 REGION=”not-matched”,即未找到匹配的环境变量,则会读取默认的配置文件(config/default.yaml)。因为 config/default.yaml 的 locale 属性为 *::*::*::*

1
2
3
4
5
6
7
8
9
10
go复制代码// Application entrance.
func main() {
// Set REGION=not-matched
os.Setenv("REGION", "not-matched")

...
// Load config which is config/beijing.yaml
fmt.Println(boot.GetConfigEntry("my-config").GetViper().GetString("region"))
...
}
1
arduino复制代码default

7.环境变量未配置

如果我们没有配置 REGION 环境变量,则会读取 config/default.yaml 文件。

1
2
3
4
5
6
7
go复制代码// Application entrance.
func main() {
...
// Load config which is config/beijing.yaml
fmt.Println(boot.GetConfigEntry("my-config").GetViper().GetString("region"))
...
}
1
arduino复制代码default

概念

rk-boot 使用 REALM,REGION,AZ,DOMAIN 四个环境变量来区分配置文件。

这四个环境变量可以是任意的值。

最佳实践

举个例子,我们有一个【云相册】业务。此业务在不同环境里使用的 MySQL 的 IP 地址不一样,则可以这么配置。

架构

假定,我们的业务在【北京】,【上海】都有服务器,同时为了提高服务可用性,在【北京】和【上海】又各开了2个区。

这时候,我们可以机器上配置如下的环境变量,可以通过 Ansible 等工具来批量设置。

环境 对应环境变量
北京,一区,测试 REALM=”cloud-album”,REGION=”bj”,AZ=”bj-1”,DOMAIN=”test”
北京,一区,线上 REALM=”cloud-album”,REGION=”bj”,AZ=”bj-1”,DOMAIN=”prod”
北京,二区,测试 REALM=”cloud-album”,REGION=”bj”,AZ=”bj-2”,DOMAIN=”test”
北京,二区,线上 REALM=”cloud-album”,REGION=”bj”,AZ=”bj-2”,DOMAIN=”prod”
上海,一区,测试 REALM=”cloud-album”,REGION=”sh”,AZ=”sh-1”,DOMAIN=”test”
上海,一区,线上 REALM=”cloud-album”,REGION=”sh”,AZ=”sh-1”,DOMAIN=”prod”
上海,二区,测试 REALM=”cloud-album”,REGION=”sh”,AZ=”sh-2”,DOMAIN=”test”
上海,二区,线上 REALM=”cloud-album”,REGION=”sh”,AZ=”sh-2”,DOMAIN=”prod”

同时,如果我们不使用类似 ETCD,Consul 等服务远程拉取配置文件,可以直接在机器中添加如下文件。每个文件都有不同的 MySQL IP 地址。

文件夹结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码.
├── boot.yaml
├── config
│ ├── bj-1-test.yaml
│ ├── bj-1-prod.yaml
│ ├── bj-2-test.yaml
│ ├── bj-2-prod.yaml
│ ├── sh-1-test.yaml
│ ├── sh-1-prod.yaml
│ ├── sh-2-test.yaml
│ ├── sh-2-prod.yaml
│ └── default.yaml
├── go.mod
├── go.sum
└── main.go

boot.yaml

接下来,我们在 boot.yaml 里添加如下 config 入口。

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
yaml复制代码config:
# 默认入口
- name: my-config
locale: "*::*::*::*"
path: config/default.yaml
# 北京,一区,测试环境
- name: my-config
locale: "cloud-album::bj::bj-1::test"
path: config/bj-1-test.yaml
# 北京,一区,线上环境
- name: my-config
locale: "cloud-album::bj::bj-1::prod"
path: config/bj-1-prod.yaml
# 北京,二区,测试环境
- name: my-config
locale: "cloud-album::bj::bj-2::test"
path: config/bj-2-test.yaml
# 北京,二区,线上环境
- name: my-config
locale: "cloud-album::bj::bj-2::prod"
path: config/bj-2-prod.yaml
# 上海,一区,测试环境
- name: my-config
locale: "cloud-album::sh::sh-1::test"
path: config/sh-1-test.yaml
# 上海,一区,线上环境
- name: my-config
locale: "cloud-album::sh::sh-1::prod"
path: config/sh-1-prod.yaml
# 上海,二区,测试环境
- name: my-config
locale: "cloud-album::sh::sh-2::test"
path: config/sh-2-test.yaml
# 上海,二区,线上环境
- name: my-config
locale: "cloud-album::sh::sh-2::prod"
path: config/sh-2-prod.yaml
echo:
- name: greeter
port: 8080
enabled: true

main.go 中读取配置文件。

因为,所有的 Config 都命名为 my-config,在 main.go 中读取的时候,我们可以使用 my-config 获取 ConfigEntry。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码package main

import (
"context"
"fmt"
_ "github.com/rookie-ninja/rk-echo/boot"
"github.com/rookie-ninja/rk-boot"
"os"
)

// Application entrance.
func main() {
// Create a new boot instance.
boot := rkboot.NewBoot()

// Get viper instance based on environment variable
boot.GetConfigEntry("my-config").GetViper()

// Bootstrap
boot.Bootstrap(context.Background())

// Wait for shutdown sig
boot.WaitForShutdownSig(context.Background())
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Oracle在线重定义之COPY_TABLE_DEPENDE

发表于 2021-11-26

「这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战」

当使用在线重定义功能进行非分区表转换时,过程中需要对中间表进行索引,约束等依赖进行重建,Oracle提供了两种方式:

本文参考:oracle-base.com/articles/mi…

一、COPY_TABLE_DEPENDENTS

使用DBMS_REDEFINITION包自带的procedure:DBMS_REDEFINITION.copy_table_dependents来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sql复制代码SET SERVEROUTPUT ON
DECLARE
l_errors NUMBER;
BEGIN
DBMS_REDEFINITION.copy_table_dependents(
uname => USER,
orig_table => 'BIG_TABLE',
int_table => 'BIG_TABLE2',
copy_indexes => DBMS_REDEFINITION.cons_orig_params,
copy_triggers => TRUE,
copy_constraints => TRUE,
copy_privileges => TRUE,
ignore_errors => FALSE,
num_errors => l_errors,
copy_statistics => FALSE,
copy_mvlog => FALSE);

DBMS_OUTPUT.put_line('Errors=' || l_errors);
END;
/

用法可参考官方文档:docs.oracle.com/en/database…

Table 134-7 COPY_TABLE_DEPENDENTS Procedure Parameters

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码DBMS_REDEFINITION.COPY_TABLE_DEPENDENTS(
uname IN VARCHAR2,
orig_table IN VARCHAR2,
int_table IN VARCHAR2,
copy_indexes IN PLS_INTEGER := 1,
copy_triggers IN BOOLEAN := TRUE,
copy_constraints IN BOOLEAN := TRUE,
copy_privileges IN BOOLEAN := TRUE,
ignore_errors IN BOOLEAN := FALSE,
num_errors OUT PLS_INTEGER,
copy_statistics IN BOOLEAN := FALSE,
copy_mvlog IN BOOLEAN := FALSE);

此方式的优缺点:

优点:可以根据你传入的参数,选择需要复制的依赖,有索引,触发器,约束,权限,统计信息。当重定义dbms_redefinition.finish_redef_table之后,会自动切换这些依赖到分区表中,不需要人为rename操作。

缺点:使用此方式复制的索引,会保留非分区表的索引类型,依然是GLOBAL的全局索引,并不会根据分区自动转换为LOCAL本地索引。

Notes:如果不考虑将索引建为LOCAL本地索引,可以使用如上方式进行复制。

二、手动创建

通过手动创建索引,指定LOCAL本地索引方式创建,但是需要在重定义dbms_redefinition.finish_redef_table之后,手动重新rename。

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
sql复制代码-- Add new keys, FKs and triggers.
ALTER TABLE big_table2 ADD (
CONSTRAINT big_table_pk2 PRIMARY KEY (id)
);

CREATE INDEX bita_created_date_i2 ON big_table2(created_date) LOCAL;

CREATE INDEX bita_look_fk_i2 ON big_table2(lookup_id) LOCAL;

ALTER TABLE big_table2 ADD (
CONSTRAINT bita_look_fk2
FOREIGN KEY (lookup_id)
REFERENCES lookup(id)
);

-- Gather statistics on the new table.
EXEC DBMS_STATS.gather_table_stats(USER, 'BIG_TABLE2', cascade => TRUE);


-- Remove original table which now has the name of the interim table.
DROP TABLE big_table2;

-- Rename all the constraints and indexes to match the original names.
ALTER TABLE big_table RENAME CONSTRAINT big_table_pk2 TO big_table_pk;
ALTER TABLE big_table RENAME CONSTRAINT bita_look_fk2 TO bita_look_fk;
ALTER INDEX big_table_pk2 RENAME TO big_table_pk;
ALTER INDEX bita_look_fk_i2 RENAME TO bita_look_fk_i;
ALTER INDEX bita_created_date_i2 RENAME TO bita_created_date_i;

优点:可以根据用户的需求,以适当的方式来创建索引等依赖。

缺点:由于是用户自己创建并且切换,所以需要有一定的基础,不能漏掉任何依赖,需要考虑完全。

三、COPY_TABLE_DEPENDENTS + 手动创建索引

也可以通过组合使用,通过COPY_TABLE_DEPENDENTS来复制其他依赖,索引手动创建。

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
sql复制代码--排除索引
SET SERVEROUTPUT ON
DECLARE
l_errors NUMBER;
BEGIN
DBMS_REDEFINITION.copy_table_dependents(
uname => USER,
orig_table => 'BIG_TABLE',
int_table => 'BIG_TABLE2',
copy_indexes => 0,
copy_triggers => TRUE,
copy_constraints => TRUE,
copy_privileges => TRUE,
ignore_errors => FALSE,
num_errors => l_errors,
copy_statistics => FALSE,
copy_mvlog => FALSE);

DBMS_OUTPUT.put_line('Errors=' || l_errors);
END;
/

--创建索引LOCAL(主键索引无法创建LOCAL本地索引)
CREATE INDEX bita_created_date_i2 ON big_table2(created_date) tablespace USERS LOCAL;

CREATE INDEX bita_look_fk_i2 ON big_table2(lookup_id) tablespace USERS LOCAL;

--重定义完成后,rename索引名称
-- Rename all the constraints and indexes to match the original names.
ALTER INDEX bita_look_fk_i2 RENAME TO bita_look_fk_i;
ALTER INDEX bita_created_date_i2 RENAME TO bita_created_date_i;

优点:综合上述两种方式,此方式只需要关注索引是否遗漏,无需关注触发器,权限,约束等依赖。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何制作一个Python SDK并实现私服上传下载

发表于 2021-11-26

这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战

在我们日常工作中,经常需要与上下游交互,特别是当我们做一些基础服务时,需要提供个其他部门使用,因此,需要编写相应的SDK上传至公司私服,供其他部门使用。今天,果冻就来和大家一起实现一个简单的Python SDK,并进行私服的上传和下载。

编写Python SDK代码

工程目录结构

1
2
3
4
5
6
7
8
9
markdown复制代码├──── easyhttp                   // SDK目录
│ ├── __init__.py
│ ├── https.py // http工具类
├── tests // 单元测试目录
│ ├── __init__.py
│ ├── test_https.py // http单元测试
├── README.md
├── requirements.txt //依赖包
└── setup.py //setuptools安装

requirements.txt

1
txt复制代码requests==2.24.0

https.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
python复制代码# -*- coding:utf8 -*-
"""
@Project: easyhttp
@File: https.py
@Version: v1.0.0
@Time: 2020/6/24 17:22
@Author: guodong.li
@Description: http
"""
from typing import Optional

import requests
import logging

from requests import Response

logging.basicConfig(format='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s',
level=logging.DEBUG)


class HttpUtils:
headers = {
"Content-Type": "application/json"
}

# http://10.193.199.44:5610/api/v1/manual/sleep?time=0
@staticmethod
def base_get(base_path: str='', detail_path: str='', params: Optional[dict]=None)-> Response:
"""
GET请求
:param base_path: 域名
:param detail_path: 接口详情
:param params: 参数
:return:
"""
logging.info("请求方式:GET, 请求url: %s , 请求参数: %s " % (base_path + detail_path, params))
response = requests.get(base_path + detail_path, params=params)
logging.info("请求方式:GET, 请求url: %s , 请求参数: %s , 结果:%s" % (base_path + detail_path, params, response))
return response

@classmethod
def base_post(cls, base_path: str='', detail_path: str='', params: Optional[dict]=None)-> Response:
"""
POST请求
:param cls:
:param base_path: 域名
:param detail_path: 接口详情
:param params: 参数
:return:
"""
logging.info("请求方式:POST, 请求url: %s ,请求参数: %s " % (base_path + detail_path, params))
response = requests.post(base_path + detail_path, data=params, headers=cls.headers)
logging.info("请求方式:POST, 请求url: %s , 请求参数: %s , 结果:%s" % (base_path + detail_path, params, response))
return response

test_https.py

1
2
3
4
5
6
7
8
9
10
11
python复制代码import requests
import logging
from easyhttp.https import HttpUtils

logging.basicConfig(format='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s',
level=logging.DEBUG)

r = requests.get("http://xxx.xxx.xxx.xxx:5610/api/v1/manual/sleep?time=0")
logging.info(r) # <Response [200]>
logging.info(type(r)) # <class 'requests.models.Response'>
logging.info(r.status_code) # 200

代码写完了之后,打包并上传到私服。

打包并上传私服

安装twine包

1
复制代码pip install twine

编写构建工具setup.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
python复制代码# -*- coding:utf8 -*-
"""
@author: guodong.li
@email: liguodongiot@163.com
@time: 2019/7/31 14:04
@file: setup.py
@desc:
"""

# 引入构建包信息的模块
from setuptools import setup, find_packages

try: # for pip >= 10
from pip._internal.req import parse_requirements
from pip._internal.network.session import PipSession
except ImportError: # for pip <= 9.0.3
from pip.req import parse_requirements
from pip.download import PipSession

# parse_requirements() returns generator of pip.req.InstallRequirement objects
install_reqs = parse_requirements('requirements.txt', session=PipSession())

# reqs is a list of requirement
# e.g. ['django==1.5.1', 'mezzanine==1.4.6']
reqs = [str(ir.req) for ir in install_reqs]

# 定义发布的包文件的信息
setup(
name="easyhttp", # 发布的包的名称
version="1.0.0", # 发布包的版本序号
description="easy use http", # 发布包的描述信息
author="guodong.li", # 发布包的作者信息
author_email="liguodongiot@163.com", # 作者的联系邮箱
packages=["easyhttp"],
# include_package_data=True, # include everything in source control
# ...but exclude README.txt from all packages
exclude_package_data={'': ['README.md'],
'tests': ['*.py']},
install_requires=reqs,
)

setup.py各参数简单介绍如下:

  • –name 包名称
  • –version (-V) 包版本
  • –author 程序的作者
  • –author_email 程序的作者的邮箱地址
  • –maintainer 维护者
  • –maintainer_email 维护者的邮箱地址
  • –url 程序的官网地址
  • –license 程序的授权信息
  • –description 程序的简单描述
  • –long_description 程序的详细描述
  • –platforms 程序适用的软件平台列表
  • –classifiers 程序的所属分类列表
  • –keywords 程序的关键字列表
  • –packages 需要处理的包目录(包含__init__.py的文件夹)
  • –py_modules 需要打包的python文件列表
  • –download_url 程序的下载地址
  • –data_files 打包时需要打包的数据文件,如图片,配置文件等
  • –scripts 安装时需要执行的脚步列表
  • –package_dir 告诉setuptools哪些目录下的文件被映射到哪个源码包。一个例子:package_dir = {‘’: ‘lib’},表示“root package”中的模块都在lib 目录中。
  • –requires 定义依赖哪些模块
  • –provides 定义可以为哪些模块提供依赖
  • –find_packages() 对于简单工程来说,手动增加packages参数很容易,刚刚我们用到了这个函数,它默认在和setup.py同一目录下搜索各个含有 init.py的包。其实我们可以将包统一放在一个src目录中,另外,这个包内可能还有aaa.txt文件和data数据文件夹。还可以排除一些特定的包find_packages(exclude=[“.tests”, “.tests.*”, “tests.*”, “tests”])
  • –install_requires = [“requests”] 需要安装的依赖包
  • –entry_points 动态发现服务和插件

新增.pypirc文件

touch ~/.pypirc

在.pypirc文件添加如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
toml复制代码[distutils]
index-servers =
pypi
nexus

[pypi]
repository:https://pypi.python.org/pypi
username:your_username
password:your_password


[nexus]
repository=http://192.168.12.196:8081/repository/mypypi-hosted/
username=your_username
password=your_password

打包并上传至私服仓库nexus

python setup.py sdist bdist_wheel upload -r nexus

或者打包命令和上传命令分开操作

1、打包命令

python setup.py sdist bdist_wheel
2、上传命令
twine upload -r nexus dist/* # -r 可以选择仓库地址

创建虚拟环境,并下载私服包进行验证

创建虚拟环境

1
bash复制代码virtualenv -p /usr/bin/python venv

激活虚拟环境

1
bash复制代码source venv/bin/activate

下载包

1
bash复制代码pip  install easyhttp==1.0.0 -i http://your_username:your_password@192.168.12.196:8081/repository/mypypi-hosted/simple/  --trusted-host 192.168.12.196

进入python shell环境

python

代码验证

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码>>> from pai.utils.https import HttpUtils
>>> import logging
>>> logging.basicConfig(format='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s',level=logging.INFO)
>>> r = requests.get("http://10.xxx.xxx.xxx:5610/api/v1/manual/sleep?time=0")
2020-07-02 11:31:50,903 - /root/python/20200702/venv/lib/python3.7/site-packages/urllib3/connectionpool.py[line:230] - DEBUG: Starting new HTTP connection (1): 10.xxx.xxx.xxx:5610
2020-07-02 11:31:51,065 - /root/python/20200702/venv/lib/python3.7/site-packages/urllib3/connectionpool.py[line:442] - DEBUG: http://10.xxx.xxx.xxx:5610 "GET /api/v1/manual/sleep?time=0 HTTP/1.1" 200 None
>>> logging.info(r) # <Response [200]>
2020-07-02 11:32:15,420 - <stdin>[line:1] - INFO: <Response [200]>
>>>
>>> logging.info(type(r)) # <class 'requests.models.Response'>
2020-07-02 11:32:27,371 - <stdin>[line:1] - INFO: <class 'requests.models.Response'>
>>> logging.info(r.status_code) # 200
2020-07-02 11:32:39,069 - <stdin>[line:1] - INFO: 200

至此,一个简单的Python SDK就已经制作完成,并且实现了SDK到私服的上传与下载。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Bean的生命周期

发表于 2021-11-26

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

【单实例 bean 】 的全生命周期为主线讨论,具体包含以下部分:

  • BeanDefinition 部分
+ `BeanDefinition` 的解析
+ `BeanDefinition` 的注册
  • bean 实例部分
+ bean 的实例化
+ bean 的属性赋值 + 依赖注入
+ bean 的初始化流程
+ bean 的启动与停止
+ bean 的销毁

我们就可以对 Spring Bean 的声明周期过程如下****(方法级别**):准确来说是实例化部分的生命周期

1.Spring 容器根据实例化策略对 Bean 进行实例化。

在实例化 bean 过程中,Spring 采用“策略模式”来决定采用哪种方式来实例化 bean,一般有反射和 CGLIB 动态字节码两种方式。默认情况下,Spring 采用 CglibSubclassingInstantiationStrategy。

2.实例化完成后,如果该 bean 设置了一些属性的话,则利用 set 方法设置一些属性。

3.如果该 Bean 实现了 BeanNameAware 接口,则调用 #setBeanName(String beanName) 方法。

Aware 接口为 Spring 容器的核心接口,是一个具有标识作用的超级接口,实现了该接口的 bean 是具有被 Spring 容器通知的能力,通知的方式是采用回调的方式

4.如果该 bean 实现了 BeanClassLoaderAware 接口,则调用 setBeanClassLoader(ClassLoader classLoader) 方法。

5.如果该 bean 实现了 BeanFactoryAware接口,则调用 setBeanFactory(BeanFactory beanFactory) 方法。

6.如果该容器注册了 BeanPostProcessor,则会调用#postProcessBeforeInitialization(Object bean, String beanName) 方法,完成 bean 前置处理

初始化第二个阶段则是BeanPostProcessor 增强处理,在该阶段 BeanPostProcessor 会处理当前容器内所有符合条件的实例化后的 bean 对象。它主要是对 Spring 容器提供的 bean 实例对象进行有效的扩展,允许 Spring 在初始化 bean 阶段对其进行定制化修改,如处理标记接口或者为其提供代理实现。

7.如果该 bean 实现了 InitializingBean 接口,则调用#afterPropertiesSet() 方法。

8.如果该 bean 配置了 init-method 方法,则调用其指定的方法。

9.初始化完成后,如果该容器注册了 BeanPostProcessor 则会调用 #postProcessAfterInitialization(Object bean, String beanName) 方法,完成 bean 的后置处理。

10.对象完成初始化,开始方法调用。

11.在容器进行关闭之前,如果该 bean 实现了 DisposableBean 接口,则调用 #destroy() 方法。

12.在容器进行关闭之前,如果该 bean 配置了 destroy-method ,则调用其指定的方法。


13.到这里一个 bean 也就完成了它的一生。

图片.png

小结:

首先,bean 的生命周期分为 BeanDefinition 阶段和 bean 实例阶段。

1)BeanDefinition 阶段分为加载 xml 配置文件、解析注解配置类、编程式构造 BeanDefinition 、BeanDefinition 的后置处理,一共四个部分

2)bean 实例阶段的生命周期包含四大步骤:

​ a)bean 的实例化 ​ b)属性赋值 + 依赖注入 ​ c)bean 的初始化生命周期回调 ​ d)bean 实例的销毁

注:理解 bean 的生命周期,首先要从 BeanDefinition 开始切入,由 BeanDefinition 再到 bean 实例,这个回答的逻辑和脉络才是一个好的回答

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

查看Linux机器上端口号信息

发表于 2021-11-26

查看端口号及对应的进程信息

注下面的示例均是在 root 帐号进行操作,示例代码可以根据实际情况调整。

  • 通过 netstat -ntlp 查看目前机器上的端口占用情况
    image.png
  • 获取 port pid 信息
1
bash复制代码netstat -ntlp | awk -F'/' '{print $1}' | awk '{print $4,$NF}' | awk -F':' '{if (NR>2){print $NF}}'

image.png

  • 根据进程查看进程下详细信息

image.png

/proc/pid/ 目录下文件说明

proc - process information pseudo-filesystem (进程信息伪文件系统)。proc文件系统中的大多数文件都是只读的,但也有一些文件是可写的,允许更改内核变量。 下面列出的文件及目录的详细描述参考

  • attr
+ current
+ exec
+ fscreate
+ keycreate
+ prev
+ sockcreate
  • autogroup
  • auxv

包含传递给进程的ELF解释器信息,格式是每一项都是一个unsigned long长度的ID加上一个unsigned long长度的值。最后一项以连续的两个0x00开头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
yaml复制代码hexdump auxv
0000000 0021 0000 0000 0000 f000 685e 7ffd 0000
0000010 0010 0000 0000 0000 fbff bfeb 0000 0000
0000020 0006 0000 0000 0000 1000 0000 0000 0000
0000030 0011 0000 0000 0000 0064 0000 0000 0000
0000040 0003 0000 0000 0000 0040 0020 0000 0000
0000050 0004 0000 0000 0000 0038 0000 0000 0000
0000060 0005 0000 0000 0000 000b 0000 0000 0000
0000070 0007 0000 0000 0000 a000 b6f5 7f2e 0000
0000080 0008 0000 0000 0000 0000 0000 0000 0000
0000090 0009 0000 0000 0000 1000 004b 0000 0000
00000a0 000b 0000 0000 0000 27b1 0000 0000 0000
00000b0 000c 0000 0000 0000 27b1 0000 0000 0000
00000c0 000d 0000 0000 0000 271a 0000 0000 0000
00000d0 000e 0000 0000 0000 271a 0000 0000 0000
00000e0 0017 0000 0000 0000 0000 0000 0000 0000
00000f0 0019 0000 0000 0000 6f59 685e 7ffd 0000
0000100 001a 0000 0000 0000 0000 0000 0000 0000
0000110 001f 0000 0000 0000 8fdc 685e 7ffd 0000
0000120 000f 0000 0000 0000 6f69 685e 7ffd 0000
0000130 0000 0000 0000 0000 0000 0000 0000 0000
0000140
  • cgroup
  • clear_refs
  • cmdline

是一个只读文件,包含进程的完整命令行信息.

1
2
ini复制代码# cat cmdline
/usr/local/bin/chromedriver--port=26443
  • comm

包含进程的命令名。

1
2
shell复制代码# cat comm
chromedriver
  • coredump_filter
  • cpuset
  • cwd

是进程当前工作目录的符号链接。

1
2
perl复制代码# ll cwd
lrwxrwxrwx 1 xxx deploy 0 Nov 25 10:55 cwd -> /home/xxx/work/xxxx
  • environ

显示进程的环境变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码# strings environ
XDG_SESSION_ID=282753
SHELL=/bin/sh
USER=xxx
PATH=/usr/bin:/bin
PWD=/home/xxx/work/xxxx
LANG=en_US.UTF-8
SHLVL=1
HOME=/home/xxx
LOGNAME=xxx
XDG_RUNTIME_DIR=/run/user/10161
OLDPWD=/home/xxx
_=/home/xxx/venv3/bin/python
DJANGO_SETTINGS_MODULE=settings
TZ=Asia/Shanghai
  • exe

为实际运行程序的符号链接。

  • fd

是一个目录,包含进程打开文件的情况。

  • fdinfo
  • gid_map
  • io
  • limits

显示当前进程的资源限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码# cat limits
Limit Soft Limit Hard Limit Units
Max cpu time unlimited unlimited seconds
Max file size unlimited unlimited bytes
Max data size unlimited unlimited bytes
Max stack size 8388608 unlimited bytes
Max core file size 0 unlimited bytes
Max resident set unlimited unlimited bytes
Max processes 127912 127912 processes
Max open files 65535 65535 files
Max locked memory 65536 65536 bytes
Max address space unlimited unlimited bytes
Max file locks unlimited unlimited locks
Max pending signals 127912 127912 signals
Max msgqueue size 819200 819200 bytes
Max nice priority 0 0
Max realtime priority 0 0
Max realtime timeout unlimited unlimited us
  • loginuid
  • map_files
  • maps

显示进程的内存区域映射信息。

1
2
3
4
5
6
7
8
9
10
11
lua复制代码# cat maps  | head
00200000-004b1000 r--p 00000000 08:04 4317821208 /usr/local/bin/chromedriver
004b1000-009ef000 r-xp 002b1000 08:04 4317821208 /usr/local/bin/chromedriver
009ef000-00a2b000 rw-p 007ef000 08:04 4317821208 /usr/local/bin/chromedriver
00a2b000-00a4b000 rw-p 00000000 00:00 0
26108c113000-26108c114000 ---p 00000000 00:00 0
26108c114000-26108c133000 rw-p 00000000 00:00 0
26108c133000-26108c134000 ---p 00000000 00:00 0
26108c134000-26108c153000 rw-p 00000000 00:00 0
26108c153000-26108c154000 ---p 00000000 00:00 0
26108c154000-26108c18a000 rw-p 00000000 00:00 0
  • mem
  • mountinfo
  • mounts
  • mountstats
  • net
  • ns
  • numa_maps
  • oom_adj
  • oom_score
  • oom_score_adj
  • pagemap
  • personality
  • projid_map
  • root
  • sched
  • schedstat
  • sessionid
  • setgroups
  • smaps
  • stack

显示当前进程的内核调用栈信息,只有内核编译时打开了CONFIG_STACKTRACE编译选项,才会生成这个文件。

1
2
3
4
5
6
7
ini复制代码# cat stack
[<ffffffff810f5126>] futex_wait_queue_me+0xc6/0x130
[<ffffffff810f5deb>] futex_wait+0x17b/0x280
[<ffffffff810f7b26>] do_futex+0x106/0x5a0
[<ffffffff810f8040>] SyS_futex+0x80/0x180
[<ffffffff816b4fc9>] system_call_fastpath+0x16/0x1b
[<ffffffffffffffff>] 0xffffffffffffffff
  • stat
  • statm

显示进程所占用内存大小的统计信息,包含七个值,度量单位是page(page大小可通过getconf PAGESIZE得到)。

1
2
3
4
5
yaml复制代码# cat statm
51478 730 445 2031 0 12816 0

# getconf PAGESIZE
4096

七个值的含义分别为

+ 进程占用的总的内存
+ 进程当前时刻占用的物理内存
+ 同其它进程共享的内存
+ 进程的代码段
+ 共享库(从2.6版本起,这个值为0)
+ 进程的堆栈
+ dirty pages(从2.6版本起,这个值为0)
+ status包含进程的状态信息。其很多内容与`/proc/[pid]/stat`和`/proc/[pid]/statm`,但是却是以一种更清晰地方式展现出来。
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
yaml复制代码# cat status
Name: chromedriver
Umask: 0022
State: S (sleeping)
Tgid: 13834
Ngid: 0
Pid: 13834
PPid: 1
TracerPid: 0
Uid: 10161 10161 10161 10161
Gid: 10010 10010 10010 10010
FDSize: 64
Groups: 989 10010
VmPeak: 206004 kB
VmSize: 205912 kB
VmLck: 0 kB
VmPin: 0 kB
VmHWM: 5144 kB
VmRSS: 2920 kB
RssAnon: 1140 kB
RssFile: 1780 kB
RssShmem: 0 kB
VmData: 51132 kB
VmStk: 132 kB
VmExe: 8124 kB
VmLib: 4912 kB
VmPTE: 148 kB
VmSwap: 0 kB
Threads: 5
SigQ: 0/127912
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000000000
SigCgt: 0000000180000000
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000001fffffffff
CapAmb: 0000000000000000
Seccomp: 0
Cpus_allowed: ffffffff
Cpus_allowed_list: 0-31
Mems_allowed: 00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000003
Mems_allowed_list: 0-1
voluntary_ctxt_switches: 10
nonvoluntary_ctxt_switches: 1
  • syscall

显示当前进程正在执行的系统调用

1
2
shell复制代码# cat syscall
202 0x7ffd685e6754 0x80 0x1 0x0 0x7ffd685e6700 0x0 0x7ffd685e5fd0 0x7f2eb6b45945
  • task
  • timers
  • uid_map
  • wchan

显示当进程sleep时,kernel当前运行的函数。

1
2
shell复制代码# cat wchan
futex_wait_queue_me

代码实现

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
golang复制代码import (
"bufio"
"io"
"os/exec"
"strings"
"sync"
"fmt"
)

var wg sync.WaitGroup

type HasBeenUsedPortInfo struct {
Pid string
Port string
Program string
}

// GetHostnameHasBeenUsedPort 获取当前机器占用的端口号及端口启用的进程id
func GetHostnameHasBeenUsedPortAndPid() []map[string]string {
var portAndPid []map[string]string
// 该命令为直接获取当前机器上运行端口号和启动端口号的进程pid
cmd := exec.Command("/bin/sh", "-c", `netstat -ntlp | awk -F'/' '{print $1}' | awk '{print $4,$NF}' | awk -F':' '{if (NR>2){print $NF}}'`)
stdout, _ := cmd.StdoutPipe()
cmd.Start()
reader := bufio.NewReader(stdout)

for {
line, err := reader.ReadString('\n')
line = strings.TrimSpace(line)
if err != nil || io.EOF == err {
break
}
lines := strings.Split(line, " ")
port, pid := lines[0], lines[1]

portAndPid = append(portAndPid, map[string]string{
"pid": pid,
"port": port,
})
}
cmd.Wait()

return portAndPid
}

func CheckPort() {
hostname, _ := os.Hostname()
port := GetHostnameHasBeenUsedPortAndPid()
var usedPortInfo []HasBeenUsedPortInfo

wg.Add(len(port))
for _, v := range port {
go func(pid, port string) {
defer wg.Done()
// 该命令获取进程运行的目录及启动的命令
cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf(`sudo su -c"ls -l /proc/%s/{cwd,exe}" | awk '{print $NF}'`, pid))
process, _ := cmd.CombinedOutput()
line := strings.TrimSpace(string(process))

lines := strings.Split(line, "\n")
program := fmt.Sprintf(`{"cwd":"%s", "exe":"%s"}`, lines[0], lines[1])

usedPortInfo = append(usedPortInfo, HasBeenUsedPortInfo{
Pid: pid,
Port: port,
Program: program,
})
}(v["pid"], v["port"])
}
wg.Wait()

// 获取所有已被占用的端口及进行信息
fmt.Println("hostname:", usedPortInfo, "usedPortInfo": usedPortInfo)
}

参考

  • man7.org/linux/man-p…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

「Linux 奏章 20」日志管理

发表于 2021-11-26

「这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战」。

  1. 日志管理介绍

日志是重要的系统信息文件,其中记录了许多重要的系统事件,包括用户的登录信息、系统启动信息、系统安全信息、邮件相关信息、各种服务相关信息等。

日志对于安全来说也很重要,它记录了系统每天发生的各种事情,通过日志来检查错误发生的原因,或者收到攻击时攻击者留下的痕迹。

⭐/var/log 目录就是系统日志文件的保存位置:

img

1.1 系统常用的日志

日志文件 说明
/var/log/boot.log 系统启动日志
/var/log/cron 记录与系统定时任务相关的日志
/var/log/cups 记录打印信息的日志
/var/log/dmesg 记录了系统在开机时内核自检的信息,也可以使用 dmesg 命令直接查看内核自检信息
/var/log/btmp 记录错误登录的日志。这个文件是二进制文件,不能直接使用 vi 查看,而要使用 lastb 命令查看img
/var/log/lasllog 记录系统中所有用户最后一次登录时间的日志。同样而是二进制文件,lastlog 命令查看
/var/log/mailog 记录邮件信息的日志
/var/log/message 记录 Linux 系统绝大多数重要消息,如果出现问题,首先检查该日志文件
/var/log/secure 记录验证和授权方面的信息,只要涉及账户和密码的程序都会记录,比如系统的登录、ssh 的登录、su 切换用户、sudo 授权,甚至添加用户和修改用户密码都会记录在这个日志文件中
/var/log/wtmp 永久记录所有用户的登录、注销信息,同时记录系统的启动、重启、关机事件,是二进制文件,使用 last 命令查看
/var/tun/ulmp 只记录当前登录用户的信息,所以该文件会随着用户的登录和注销而不断变化,无法使用 vi 查看,使用 w、who、users 等命令查看

1.2 案例

使用 root 用户通过 Xshell 登录,第一次错误登录,第二次正确登录,查看 /var/log/secure 中记录的日志信息:

img

  1. 日志管理服务 rsyslogd

2.1 日志服务原理图

⭐rsyslogd 是后台程序 (daemon、服务),那 rsyslogd 服务如何知道将各类型日志信息记录至日志文件保存位置 /var/log/xxx 呢?

⭐得益于 /etc/rsyslog.conf 配置文件,/etc/rsyslog.conf 中记录了日志文件管理的规则:

img

2.2 rsyslogd

CentOS 7 日志管理服务是 rsyslogd(后台程序 daemon),CentOS 6.x 日志管理服务是 syslogd。rsyslogd 功能更强大,且 rsyslogd 向下兼容 syslogd 服务。

  • ⭐查询 Linux 中的 rsyslogd 服务是否启动
    • ps -aux | grep "rsyslog" | grep -v "grep" :
      • grep 不加参数是正向过滤(想要什么)、 -v 参数为反向过滤(不想要什么)
      • img
  • ⭐查询 rsyslogd 服务的自启动状态
    • systemctl list-unit-files | grep "rsyslog"
    • img

2.3 日志管理规则

⭐查看配置文件 /etc/rsyslog.conf 日志管理规则:

img

2.4 日志类型 & 日志级别 ==> *.*

  • 编辑文件时的格式为 : *.* (第一个 * 代表日志类型、第二个 * 代表日志级别)
    • 日志类型
      • auth pam产生的日志
      • authpriv ssh、ftp等登录信息的验证信息
      • corn 时间任务相关
      • kern 内核
      • lpr 打印
      • mail 邮件
      • mark(syslog)-rsyslog 服务内部的信息,事件标识
      • news 新闻组
      • users 用户程序产生的相关信息
      • uucp unix to nuix copy主机之间相关的通信
      • local 1-7 自定义日志设备
    • 日志级别(自上到下,级别从低到高,记录信息越来越少)
      • debug
      • info
      • notice
      • warning
      • err
      • crit
      • alert
      • emerg
      • none

2.5 日志文件格式信息

日志文件的格式包含以下 4 列

  • 1、事件产生的时间
  • 2、产生事件的服务器的主机名
  • 3、产生事件的服务名或程序名
  • 4、事件的具体信息

img

  1. 自定义日志服务

vim /etc/rsyslog.conf: 编辑日志管理规则:

1
2
shell复制代码# 增加自定义日志(*.*==>所有类型所有级别的日志信息)
*.* /var/log/wyk.log

img

此时重启下 CentOS,然后查看日志信息:

  1. 日志轮替

4.1 日志轮替介绍

日志轮替就是把旧日志文件移动并改名,同时建立新的空日志文件,当旧日志文件超出保存的范围后,就会被自动删除。

4.2 日志轮替文件命名

  • CentOS 7 中使用 logrotate 进行日志轮替管理,要想改变日志轮替文件名字,通过 /etc/logrotate.conf 配置文件中 “dateext“ 参数
  • 如果配置文件中有 “dateext“ 参数,那么日志会用日期来作为日志文件的后缀,例如:”secure-20211114“。这样日志文件名就不会重叠,也就不需要日志文件的改名了,只需要自定保存日志个数,删除多余的日志文件即可。
  • 如果日志文件中没有 “dateext“ 参数,日志文件就需要进行改名了。
    • 第一次进行日志轮替:当前的 “secure” 日志会自动改名为 “secure.1”,然后新建 “secure” 日志,用于保存新的日志。
    • 第二次进行日志轮替:”secure.1” 自动改名为 “secure.2”,当前的 “secure” 日志会自动改名为 “secure.1”,然后新建 “secure” 日志用于保存新日志,以此类推。

vim /etc/logrotate.conf:

4.3 logrotate 配置文件参数说明

img

4.4 自定义日志轮替

⭐方法一:直接在 /etc/logrotate.conf 配置文件中写入该日志的轮替策略。

⭐方法二:/etc/logrotate.d/ 目录下新建该文件的日志轮替文件,然后在 /etc/logrotate.conf 中引入该目录 (include)

例:

1
2
3
4
5
6
7
8
shell复制代码/var/log/wyk.log
{
missingok
daily
copytruncate
rotate 7
notifempty
}

4.5 日志轮替机制原理

日志轮替之所以可以在指定时间备份日志,是依赖系统的定时任务。在 /etc/cron.daily/ 目录,就会发现这个目录中有 logrotate 文件(可执行),logrotate 通过这个文件依赖定时任务执行。

4.6 内存日志

journalctl : 可以查看内存日志,以下介绍一些常用的指令

  • journalctl:查看全部
  • journalctl -n 3:查看最新 3 条

  • journalctl --since 19:00 --until 19:10:10:查看起始时间到结束时间的日志可加日期
  • journalctl -p err:报错日志

  • journalctl -o verbose:日志详细内容
  • journalctl _PID=1234 _COMM=sshd:查看包含这些参数的日志
  • journalctl | grep sshd
  • 注:journalctl 指令查看的是内存日志,重启后会被清空。
  1. 盘点日志管理的相关文件

  • /var/log/ : 系统日志文件目录
  • /etc/rsyslog.conf : 日志管理规则配置文件(什么日志信息放到什么地方)
  • /etc/logrotate.conf : 全局的日志轮替策略/规则,可以单独给某个日志文件制定策略 /etc/logrotate.d(include 引入)
  • /etc/logrotate.d/ : 单文件设置日志轮替规则,该目录可被 /etc/logrotate.conf 文件引入 : include /etc/logrotate.d

希望本文对你有所帮助🧠

欢迎在评论区留下你的看法🌊,我们一起讨论与分享🔥

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java 多线程基础

发表于 2021-11-26

1、进程、线程

进程:操作系统分配资源的最小单位

线程:操作系统调动的最小单位

2、使用多线程

  1. 继承 Thread 类

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class Run {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
}
}

class MyThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("thread running");
}

}

输出:

thread running

缺点:不支持多继承
2. 实现 Runnable 接口

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class Run {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
}
}

class MyThread implements Runnable {
@Override
public void run() {
System.out.println("thread running");
}

}

输出:

thread running

优点:“间接”支持多继承
3. 分析多线程的命令

* jps+jstack.exe
* jmc.exe(推荐)
* jvisualvm.exe

3、currentThread() 方法

​ 作用:当前代码块正在被哪个线程调用

​ 例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class Run {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
}
}

class MyThread extends Thread {
@Override
public void run() {
System.out.println(currentThread().getName());
}
}

​ 输出:

Thread-0

4、isAlive() 方法

​ 作用:当前线程是否处于活动状态(正在运行、准备开始运行)

​ 例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class Run {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
System.out.println("start->" + thread.isAlive());
thread.start();
Thread.sleep(100);
System.out.println("end->" + thread.isAlive());
}
}

class MyThread extends Thread {
@Override
public void run() {
System.out.println("run->" + isAlive());
}
}

​ 输出:

start->false

run->true

end->false

5、sleep(long millis) 方法和sleep(long millis, int nanos) 方法

​ 作用:在指定的时间内(毫秒/毫秒+纳秒)内让“正在执行的线程”休眠,正在执行的线程指 this.currentThread() 返回的线程

​ 例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class Run {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
}
}

class MyThread extends Thread {
@Override
public void run() {
System.out.println("start time->"+System.currentTimeMillis());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end time-> "+System.currentTimeMillis());
}
}

​ 输出:

start time->1637762721620

end time-> 1637762723623

6、getId() 方法

​ 作用:获得线程的唯一标识

​ 例子:

1
2
3
4
5
6
java复制代码public class Run {
public static void main(String[] args) throws InterruptedException {
Thread thread = Thread.currentThread();
System.out.println(thread.getId());
}
}

​ 输出:

1

7、停止线程

  1. 判断线程是否为停止状态
* `public boolean isInterrupted()`


作用:测试 `currentThread()` 是否已经中断,不清除状态标志


例子:



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class Run {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(100);
thread.interrupt();
System.out.println("is thread stop:"+thread.isInterrupted());
System.out.println("is thread stop:"+thread.isInterrupted());
System.out.println("end");
}
}

class MyThread extends Thread {
@Override
public void run() {
super.run();
for (int i = 0; i < 1000; i++) {
System.out.println("i=" + i);
}
}
}
输出: > i=755 > > i=756 > > is thread stop:true > > is thread stop:true > > i=757 > > i=758 * `public static boolean interrupted()` 作用:测试 `this` 关键字所在的类是否已经中断,执行后具有清除状态标志为 false 的功能 例子:
1
2
3
4
5
6
7
8
java复制代码public class Run {
public static void main(String[] args) throws InterruptedException {
Thread.currentThread().interrupt();
System.out.println("is thread stop:" + Thread.interrupted());
System.out.println("is thread stop:" + Thread.interrupted());
System.out.println("end");
}
}
输出: > is thread stop:true > > is thread stop:false > > end 第二次输出 false 的原因: `interrupted()` 具有清除状态的功能,第一次调用时,打印了当前线程被中断,然后将状态清除,此时当前线程的状态为不中断;在第二次调用时,由于上次的调用原因,此时的线程状态已经是不中断,所以打印为 false
  1. 异常法停止线程

例子:

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
java复制代码public class Run {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(100);
thread.interrupt();
}
}

class MyThread extends Thread {

@Override
public void run() {
super.run();
try {
for (int i = 0; i < 2000; i++) {
if (Thread.interrupted()) {
System.out.println("already stop,exit");
throw new InterruptedException();
}
System.out.println("i=" + i);
}
System.out.println("end");
} catch (InterruptedException e) {
System.out.println("enter exception");
}

}
}

输出:

i=686

i=687

already stop,exit

enter exception
3. ###### stop() 法暴力停止线程

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class Run {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(50);
thread.stop();
}
}

class MyThread extends Thread {

@Override
public void run() {
super.run();
for (int i = 0; i < 1000; i++) {
System.out.println("i=" + i);
}
}
}

输出:

i=374

i=375

i=376

i=377

i=378

缺点:

stop() 会释放锁导致数据不一致,已被弃用
4. return 法停止线程

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class Run {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(50);
thread.interrupt();
}
}

class MyThread extends Thread {

@Override
public void run() {
while (true) {
if (this.isInterrupted()) {
System.out.println("stop");
return;
}
System.out.println(System.currentTimeMillis());
}
}
}

输出:

1637766071932

1637766071932

1637766071932

stop
5. 在 sleep 状态下停止线程

不管调用顺序,只要 interrupt() 与 stop() 碰到一起就会出现异常

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public class Run {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(200);
thread.interrupt();
}
}

class MyThread extends Thread {

@Override
public void run() {
try {
System.out.println("thread begin");
Thread.sleep(20000);
System.out.println("thread end");
} catch (InterruptedException e) {
e.printStackTrace();
}

}
}

输出:

thread begin

java.lang.InterruptedException: sleep interrupted

at java.base/java.lang.Thread.sleep(Native Method)  

at demo.MyThread.run(Run.java:18)

8、暂停线程

  1. suspend() 与 resume() 的使用

例子:

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
java复制代码public class Run {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(5000);
thread.suspend();
System.out.println("A=" + System.currentTimeMillis() + " i=" + thread.getI());
Thread.sleep(5000);
System.out.println("A=" + System.currentTimeMillis() + " i=" + thread.getI());

thread.resume();
Thread.sleep(5000);

thread.suspend();
System.out.println("B=" + System.currentTimeMillis() + " i=" + thread.getI());
Thread.sleep(5000);
System.out.println("B=" + System.currentTimeMillis() + " i=" + thread.getI());

}
}

class MyThread extends Thread {

private long i = 0;

public long getI() {
return i;
}

public void setI(long i) {
this.i = i;
}

@Override
public void run() {
while (true) {
i++;
}
}
}

输出:

A=1637855269659 i=8483654775

A=1637855274661 i=8483654775

B=1637855279666 i=17266749694

B=1637855284669 i=17266749694
2. suspend() 与 resume() 的缺点

* 独占
* 数据不完成
  1. suspend() 与 resume() 的替代

suspend() -> wait()

resume() -> notify() / notifyAll()

9、yield() 方法

​ 作用:放弃当前的 CPU 资源,让其他任务去占用 CPU 时间片

​ 例子:

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
java复制代码public class Run {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
MyThreadYield threadYield=new MyThreadYield();
threadYield.start();
}
}

class MyThread extends Thread {

@Override
public void run() {
long beginTime = System.currentTimeMillis();
for (int i = 0; i < 200000; i++) {

}
long endTime = System.currentTimeMillis();
System.out.println("time:" + (endTime - beginTime));
}
}

class MyThreadYield extends Thread {

@Override
public void run() {
long beginTime = System.currentTimeMillis();
for (int i = 0; i < 200000; i++) {
Thread.yield();
}
long endTime = System.currentTimeMillis();
System.out.println("time:" + (endTime - beginTime));
}
}

​ 输出:

time:1

time:24

10、线程的优先级

设置优先级 public final void setPriority(int newPriority)

预定三个优先级:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    /**
* The minimum priority that a thread can have.
*/
public static final int MIN_PRIORITY = 1;

/**
* The default priority that is assigned to a thread.
*/
public static final int NORM_PRIORITY = 5;

/**
* The maximum priority that a thread can have.
*/
public static final int MAX_PRIORITY = 10;

优先级具有继承性,例如A线程启动B线程,则B的优先级和A一样

11、守护线程

Java中存在两种线程:用户线程(非守护线程)、守护线程

只有进程中不存在非守护线程,则守护线程自动销毁;若存在任何一个非守护线程,守护线程就要继续工作

主线程(main)属于非守护线程

设置守护线程:setDaemon(true),需要在 start() 方法前设置

例子:

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
java复制代码public class Run {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.setDaemon(true);
thread.start();
Thread.sleep(5000);
System.out.println("exit");
}
}

class MyThread extends Thread {

@Override
public void run() {
int i = 0;
while (true) {
i++;
System.out.println("i=" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

输出:

i=1

i=2

i=3

i=4

i=5

exit

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…183184185…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%