Java模板,泛型,通配符使用

背景

模板,泛型,通配符的使用记录

下单示例

从一个下单的例子来记录下模板与通配符的使用

定义接口

下单一般流程为报文定义,报文核对,落表数据生成,落表,落表后操作等等这些,具体视业务情况而定,这里主要记录模板泛型通配符,后续就简化成只操作一个落表接口

报文接口

这里定义一个报文接口的目的是将不同渠道格式的报文定义一个统一的内容(比如直接返回部分初始化的数据表对象),这样实现类就不用依赖于具体的某个请求报文而是下单报文接口(依赖倒置)

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

/**
* 下单报文接口
* @author: east
* @date: 2023/11/23
*/
public interface OrderRequest<T extends OrderInfo> {
/**
* 将不同渠道报文转为T的统一格式数据(比如转为落表所需数据)
* 通过T extends OrderInfo限制模板类型为OrderInfo及其子类
*
* @return T 统一数据
*/
public T generateOrderInfo();
}

/**
* 账单通用数据
*/
public class OrderInfo {
protected BigDecimal amt;
protected String serial;
protected String rcvNo;

public OrderInfo(BigDecimal amt, String serial, String rcvNo) {
this.amt = amt;
this.serial = serial;
this.rcvNo = rcvNo;
}

public OrderInfo() {
}

@Override
public String toString() {
return "OrderInfo{" +
"amt=" + amt +
", serial='" + serial + '\'' +
", rcvNo='" + rcvNo + '\'' +
'}';
}
}

下单接口

这里简化一下,只定义落表接口。落表接口接收不同类型的要落表信息,但要落表信息都是基础落表信息(BaseSavedInfo.java)的子类,所以使用”extends SavedInfo”关键字去限制一下,具体的实现类则必须传入SavedInfo或者其子类。因此这里可以使用模板接口或者模板方法,定义方法,这两种方法各有其特点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 下单要落表的最基本信息,如果除了账单信息,其他渠道需要落特别的数据表,则基础本类并重写Save接口即可,满足开闭原则
* 账单信息
* 其他信息自行继承本类实现
*
* @author: east
* @date: 2023/11/25
*/
public class BaseSavedInfo {
protected OrderInfo orderInfo;

public BaseSavedInfo(OrderInfo orderInfo) {
this.orderInfo = orderInfo;
}

@Override
public String toString() {
return "SavedInfo{" +
"orderInfo=" + orderInfo +
'}';
}
}
模板方法接口Save

这里Save接口有一个模板方法save,他的方法模板类型限制在BaseSavedInfo及其子类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 保存下单相关信息, 使用模板方法
*
* @author: east
* @date: 2023/11/23
*/
public interface Save {
/**
* 保存下单数据
*
* @param savedInfo 相关信息:账单表,统计表
*/
public <T extends BaseSavedInfo> void save(T savedInfo);
}
模板接口SaveT

这里定义的为一个模板接口SaveT,模板范围限制在BaseSavedInfo及其子类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 保存下单相关信息, 模板类
*
* @author: east
* @date: 2023/11/23
*/
public interface SaveT<T extends BaseSavedInfo> {
/**
* 保存下单数据
*
* @param savedInfo 相关信息:账单表,统计表
*/
public void save(T savedInfo);
}

下单行为抽象类

这里设置一个下单行为的抽象类目的是为了将下单流程组装起来,因为涉及到Save接口作为依赖,所以接口不适用需要换为抽象类。这里抽象类的定义可以分为三种

使用Save接口的抽象下单类:方式一
1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class AbstractOrder<T extends OrderRequest<?>> {
// 使用Save接口作为其依赖
protected Save save;

public AbstractOrder(Save save) {
this.save = save;
}

/**
* 下单接口;T为实现了OrderRequest接口的请求报文
*/
public abstract void order(T orderRequest);
}
  • 这里的”<T extends OrderRequest<?>” 表示AbstractOrder是一个模板类,且其实现类的模板类型限制在OrderRequest接口,这样做的好处是所有下单虚函数的实现类都实现了OrderRequest接口,从而统一下单行为,满足同一个标准(当然也可以不指定),后面的’?’表示任意通配符,也就是OrderRequest接口的泛型这里不关心

  • AbstractOrder抽象下单类使用Save接口作为其依赖因此直接定义Save类型的成员变量即可。如果是SaveT类型则需要特别处理

  • “public abstract void order(T orderRequest);” 表示这是一个抽象下单接口,接收一个模板类型参数,也就是实现了OrderRequest接口的请求报文体。该报文体根据下单渠道自行定义

使用SaveT接口的抽象下单类: 方式二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class AbstractOrderTR<T extends OrderRequest<?>, R extends BaseSavedInfo> {
// 使用SaveT接口作为其依赖
// 用模板指定了参数的类型为R
protected SaveT<R> save;

public AbstractOrderTR(SaveT<R> save) {
this.save = save;
}

/**
* 下单
*/
public abstract void order(T orderRequest);
}
  • 这里AbstractOrderTR抽象下单类使用SaveT接口作为其依赖,而且在类上通过模板”R extends BaseSavedInfo”指定SaveT模板类型为BaseSavedInfo及其子类
使用SaveT接口的抽象下单类: 方式三
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class AbstractOrderT<T extends OrderRequest<?>> {
// 使用SaveT接口作为其依赖
// 不通过了类模板指定,仅用通配符参数限定,
protected SaveT<? extends BaseSavedInfo> save; // note 如果实现类中不指定SaveT的类型,则save接口无法使用
// 原因: 编译器没那么聪明根据子类传入的saveT子对象推断出SaveT模板具体对应哪一个类,只能在这里明确指定或者子类重写

// 也可以为SaveT<?> 仅通配符不做限制
// protected SaveT<?> save;

// 也可以不限制,仅使用SaveT
// protected SaveT save;

public AbstractOrderT(SaveT<? extends BaseSavedInfo> save) {
this.save = save;
}

/**
* 下单
*/
public abstract void order(T orderRequest);
}
  • 这里AbstractOrderTR抽象下单类使用SaveT接口作为其依赖,但不像方式二通过模板来限制SaveT模板类型,而是使用SaveT<? extends BaseSavedInfo>通配符,来限制该模板类型仅为BaseSavedInfo及其子类,当然也可以不限制仅用SaveT<?>和SaveT。但是SaveT和(SaveT<?>,SaveT<? extends BaseSavedInfo>)不一样,SaveT并没有使用通配符,它更类似于方式一的用法,只是一个基类。而SaveT<?>和SaveT<? extends BaseSavedInfo>因为都作为通配符使用,所以在AbstractOrderTR的实现类AbstractOrderTRImp中不能直接使用,因为编译器并不能确定此处SaveT的具体类型,详见

定义实现

这里用外部渠道下单和内部渠道下单来模拟实现。一些实体类定义不具体展示,详细可在这里查看完整包

内部渠道下单实现类

InnerOrder类继承了抽象下单类AbstractOrder(方式一),并指定了模板类型为InnerOrderRequest(内部请求报文)

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
/**
* 内部下单实现类
*
* @author: east
* @date: 2023/11/24
*/
public class InnerOrder extends AbstractOrder<InnerOrderRequest> {

// InnerSaveOrder为Save接口实现类
public InnerOrder(InnerSaveOrder save) {
super(save);
}

//InnerOrderRequest为OrderRequest实现类
@Override
public void order(InnerOrderRequest innerOrderRequest) {
try {

// 校验等接口调用....

// InnerBaseSavedInfo为内部渠道需要落表的数据,是BaseSavedInfo的子类
// 落表 这里save为模板方法
save.save(new InnerBaseSavedInfo(innerOrderRequest.generateOrderInfo(), "this is inner special message"));

} catch (Exception e) {
throw new RuntimeException(e);
}
}

// public static void main(String[] args) {
// InnerSaveOrder innerSaveOrder = new InnerSaveOrder();
// InnerOrder innerOrder = new InnerOrder(innerSaveOrder);
// innerOrder.order(new InnerOrderRequest(BigDecimal.ONE, "innerSerial", "rcvNo"));
// }

}

/**
* 内部渠道保存账单
* Save接口的实现类
*
* @author: east
* @date: 2023/11/24
*/
public class InnerSaveOrder implements Save {

/**
* 保存下单数据
*
* @param savedInfo 相关信息:账单表,统计表
*/
@Override
public <T extends BaseSavedInfo> void save(T savedInfo) {

// 落表
System.out.println("内部渠道下单信息开始落表");

// todo 这里有个很大的问题就是如果不通过强制类型转换无法拿到特定种类的数据
// InnerSavedInfo innerSavedInfo=(InnerSavedInfo) savedInfo;
// System.out.println("内部渠道特有数据获取: "+innerSavedInfo.getSpecialMsg());

System.out.println(savedInfo.toString());
}
}
  • 从代码里面可以看到Save接口采用模板方法这种方式有个麻烦的地方在于实现类中因为是重写save方法所以也必须是模板方法,虽然这时已经明确知道参数类型为BaseSavedInfo的实现类InnerSavedInfo,但是在方法中无法直接获取其特定属性(比如获取InnerSavedInfo子类独有的字段),暂时没明白除了强制类型转换外该怎么解决

外部渠道下单实现类(AbstractOrderTR)

OuterOrderTR继承了下单虚函数AbstractOrderTR(方式二),并指定了AbstractOrderTR的两个模板为OuterOrderRequest和OuterBaseSavedInfo,从而明确指定了抽象类的SaveT这个成员函数的模板类型为OuterBaseSavedInfo

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
public class OuterOrderTR extends AbstractOrderTR<OuterOrderRequest, OuterBaseSavedInfo> {

public OuterOrderTR(OuterSaveOrderImp save) {
super(save);
}

@Override
public void order(OuterOrderRequest outerOrderRequest) {
try {

// 校验等逻辑....

// 落表
// note 这里通过模板类全量指定
save.save(new OuterBaseSavedInfo(outerOrderRequest.generateOrderInfo()));

} catch (Exception e) {
throw new RuntimeException(e);
}
}

// public static void main(String[] args) {
// OuterSaveOrderImp saveOrderAction = new OuterSaveOrderImp();
// OuterOrderTR orderAction = new OuterOrderTR(saveOrderAction);
// orderAction.order(new OuterOrderRequest(BigDecimal.ONE, "outSerialTR", "rcvNo"));
// }

}

外部渠道下单实现类(AbstractOrderT)

这里OuterOrderT继承了AbstractOrderT虚函数方式三,成员函数SaveT<? extends BaseSavedInfo> save只通过上界通配符限定,我一开始觉得编译器能通过传入的OuterBaseSavedInfo类对象来初始化SaveT对象时推断出SaveT的模板类型为OuterBaseSavedInfo而不是InnerBaseSavedInfo(BaseSavedInfo的另一个实现类),但实际上编译器好像无法推测出save的类型导致SaveT对象的public void save(T savedInfo)方法无法接收任何参数(除了null),这里和List<? extends Number> list无法list.add(1)问题很像,调用save方法时会导致类型安全问题,无法编译通过。

相关解释

解决办法为明确指定SaveT模板类型

1
protected SaveT<OuterBaseSavedInfo> save;

类定义如下

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
public class OuterOrderT extends AbstractOrderT<OuterOrderRequest> {

protected SaveT<OuterBaseSavedInfo> save; // note 通配符如果不在模板指定,则只有在这里明确指定,save的方法才能正常调用, 可以将这行去掉试试

public OuterOrderT(OuterSaveOrderImp save) {
super(save);
this.save = save;
}

@Override
public void order(OuterOrderRequest outerOrderRequest) {
try {

// 校验等逻辑....

// 落表
// 通过父类调用 note: 这里没有通过模板指定(如果也不在成员函数指定的话则会报错,? extends通配只是占位,不指定类型则无法操作
save.save(new OuterBaseSavedInfo(outerOrderRequest.generateOrderInfo()));

} catch (Exception e) {
throw new RuntimeException(e);
}
}

// public static void main(String[] args) {
// OuterSaveOrderImp saveOrderAction = new OuterSaveOrderImp();
// OuterOrderT orderAction = new OuterOrderT(saveOrderAction);
// orderAction.order(new OuterOrderRequest(BigDecimal.ONE, "outSerialT", "rcvNo"));
// }

}

总结

  • 上面的三个实现类大概展示了通配符与模板使用的示例,以及一些设计原则(依赖倒置,接口隔离,单一,开闭)的使用,以及其中的一些坑,建议大家结合具体的示例或实际的业务来理解。

  • 对于上界和下界通配符,个人感觉<? extends ClassA>这种用得多一点,这种相当于指定了一个规范ClassA,后续传入的参数都要实现这个规范ClassA或者继承ClassA。至于上下界的定义为什么那么叫,个人感觉没那么重要

完整代码见:

简略版

详细版

docker安装kafka

背景

最近学习到Kafka相关知识,所以用docker在vps上安装单节点版和集群版方便后续学习

环境

  • Debian10
  • Kafka:

单节点版

使用docker compose安装。注意在yml目录下创建一个data目录放上两个文件message.json和update_run.sh,做好权限修改后up一下就行

docker-compose.yml

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
version: '2'
services:

kafka1:
image: confluentinc/cp-kafka:7.2.1
hostname: kafka1
container_name: kafka1
ports:
- "9092:9092"
- "9997:9997"
environment:
KAFKA_BROKER_ID: 1
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'
KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka1:29092,PLAINTEXT_HOST://154.40.46.38:9092'
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_JMX_PORT: 9997
KAFKA_JMX_HOSTNAME: localhost
KAFKA_PROCESS_ROLES: 'broker,controller'
KAFKA_NODE_ID: 1
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka1:29093'
KAFKA_LISTENERS: 'PLAINTEXT://kafka1:29092,CONTROLLER://kafka1:29093,PLAINTEXT_HOST://0.0.0.0:9092'
KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'
KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'
volumes:
- ./data/update_run.sh:/tmp/update_run.sh
command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'"

schemaregistry1:
image: confluentinc/cp-schema-registry:7.2.1
ports:
- 18085:8085
depends_on:
- kafka1
volumes:
- ./data/jaas:/conf
environment:
SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka1:29092
SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT
SCHEMA_REGISTRY_HOST_NAME: schemaregistry1
SCHEMA_REGISTRY_LISTENERS: http://schemaregistry1:8085

# ui那边SCHEMA模块默认账户密码: admin/letmein
SCHEMA_REGISTRY_AUTHENTICATION_METHOD: BASIC
SCHEMA_REGISTRY_AUTHENTICATION_REALM: SchemaRegistryProps
SCHEMA_REGISTRY_AUTHENTICATION_ROLES: admin
SCHEMA_REGISTRY_OPTS: -Djava.security.auth.login.config=/conf/schema_registry.jaas

SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: "http"
SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO
SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas

kafka-init-topics:
image: confluentinc/cp-kafka:7.2.1
volumes:
- ./data/message.json:/data/message.json
depends_on:
- kafka1
command: "bash -c 'echo Waiting for Kafka to be ready... && \
cub kafka-ready -b kafka1:29092 1 30 && \
kafka-topics --create --topic users --partitions 3 --replication-factor 1 --if-not-exists --bootstrap-server kafka1:29092 && \
kafka-topics --create --topic messages --partitions 2 --replication-factor 1 --if-not-exists --bootstrap-server kafka1:29092 && \
kafka-console-producer --bootstrap-server kafka1:29092 --topic users < /data/message.json'"

# ui控制
kafka-ui:
container_name: kafka-ui
image: provectuslabs/kafka-ui:latest
ports:
- 18080:8080
depends_on:
- kafka1
- schemaregistry1
environment:
KAFKA_CLUSTERS_0_NAME: local
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka1:29092
KAFKA_CLUSTERS_0_METRICS_PORT: 9997
KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schemaregistry1:8085
KAFKA_CLUSTERS_0_SCHEMAREGISTRYAUTH_USERNAME: admin
KAFKA_CLUSTERS_0_SCHEMAREGISTRYAUTH_PASSWORD: letmein
AUTH_TYPE: "LOGIN_FORM"
SPRING_SECURITY_USER_NAME: username
SPRING_SECURITY_USER_PASSWORD: passwd

ref: https://blog.csdn.net/dghkgjlh/article/details/133418837

https://flxdu.cn/2023/01/23/Kafka-in-Docker-%E5%8D%95%E8%8A%82%E7%82%B9-%E5%A4%9A%E8%8A%82%E7%82%B9/

update_run.sh

1
2
3
4
5
6
7
8
9
10
11
# This script is required to run kafka cluster (without zookeeper)
#!/bin/sh

# Docker workaround: Remove check for KAFKA_ZOOKEEPER_CONNECT parameter
sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure

# Docker workaround: Ignore cub zk-ready
sed -i 's/cub zk-ready/echo ignore zk-ready/' /etc/confluent/docker/ensure

# KRaft required step: Format the storage directory with a new cluster ID
echo "kafka-storage format --ignore-formatted -t $(kafka-storage random-uuid) -c /etc/kafka/kafka.properties" >> /etc/confluent/docker/ensure

message.json

放个空的就行

1
{}

Kafka集群版

这里用多个docker模拟集群的多节点, 直接复制yml然后up就行

参考

docker-compose.yml

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
# Copyright VMware, Inc.
# SPDX-License-Identifier: APACHE-2.0

version: "2"

services:
kafka-0:
image: docker.io/bitnami/kafka:3.6
ports:
- "9092:9092"
restart: unless-stopped
environment:
# KRaft settings
# 结点序号1
- KAFKA_CFG_NODE_ID=0
# 结点角色:controller,broker(同一时间只会有一个controller)
- KAFKA_CFG_PROCESS_ROLES=controller,broker
# controller选举投票权结点
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka-0:9093,1@kafka-1:9093,2@kafka-2:9093
# 禁用自动创建topic
- KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=false
# 日志保留时间
- KAFKA_CFG_LOG_RETENTION_HOURS=168
# 单个分区大小
- KAFKA_CFG_LOG_RETENTION_BYTES=1073741824
# 启用日志压缩
- KAFKA_CFG_LOG_CLEANER_ENABLE=true
# 消息体最大为1M
- KAFKA_CFG_MESSAGE_MAX_BYTES=1000012
# ISR中最小个数,小于这个生产者可能发送失败,数据丢失风险增加,可靠性降低
- KAFKA_CFG_MIN_IN_SYNC_REPLICAS=2
# 集群ID
- KAFKA_KRAFT_CLUSTER_ID=abcdefghijklmnopqrstuv
# Listeners
# 服务监听部分,监听器PLAINTEXT(没错这只是个监听器名称)监听9092,监听器CONTROLLER监听9093端口
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093
# 连接建议监听器为PLAINTEXT,端口为9092,这里为了外部访问需要指定外部ip(如果外部有代理则这里就用集群内部格式就行)
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://103.171.26.248:9092
# 映射监听器名称和使用的协议,PLAINTEXT使用的协议是PLAINTEXT,CONTROLLER也用的PLAINTEXT(比较方便)
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
# 指定用于控制器通信的监听器
- KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
# broker间通信使用的监听器
- KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT
# Clustering
# 主题消费记录偏移量的副本个数(记录消费组消费主题的偏移量的副本)
- KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=3
# Kafka 会在集群中的三个不同节点上保持事务状态日志的副本
- KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=3
# 至少有两个副本(包括领导副本)需要是同步的,才能继续事务操作
- KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=2
volumes:
- kafka_0_data:/bitnami/kafka
kafka-1:
image: docker.io/bitnami/kafka:3.6
ports:
- "9093:9092"
restart: unless-stopped
environment:
# KRaft settings
- KAFKA_CFG_NODE_ID=1
- KAFKA_CFG_PROCESS_ROLES=controller,broker
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka-0:9093,1@kafka-1:9093,2@kafka-2:9093
- KAFKA_KRAFT_CLUSTER_ID=abcdefghijklmnopqrstuv
- KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=false
- KAFKA_CFG_LOG_RETENTION_HOURS=168
- KAFKA_CFG_LOG_RETENTION_BYTES=1073741824
- KAFKA_CFG_LOG_CLEANER_ENABLE=true
- KAFKA_CFG_MESSAGE_MAX_BYTES=1000012
- KAFKA_CFG_MIN_IN_SYNC_REPLICAS=2
# Listeners
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://103.171.26.248:9093
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
- KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
- KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT
# Clustering
- KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=3
- KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=3
- KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=2
volumes:
- kafka_1_data:/bitnami/kafka
kafka-2:
image: docker.io/bitnami/kafka:3.6
ports:
- "9094:9092"
restart: unless-stopped
environment:
# KRaft settings
- KAFKA_CFG_NODE_ID=2
- KAFKA_CFG_PROCESS_ROLES=controller,broker
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka-0:9093,1@kafka-1:9093,2@kafka-2:9093
- KAFKA_KRAFT_CLUSTER_ID=abcdefghijklmnopqrstuv
- KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=false
- KAFKA_CFG_LOG_RETENTION_HOURS=168
- KAFKA_CFG_LOG_RETENTION_BYTES=1073741824
- KAFKA_CFG_LOG_CLEANER_ENABLE=true
- KAFKA_CFG_MESSAGE_MAX_BYTES=1000012
- KAFKA_CFG_MIN_IN_SYNC_REPLICAS=2
# Listeners
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://103.171.26.248:9094
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
- KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
- KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT
# Clustering
- KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=3
- KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=3
- KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=2
volumes:
- kafka_2_data:/bitnami/kafka

volumes:
kafka_0_data:
driver: local
kafka_1_data:
driver: local
kafka_2_data:
driver: local

查看集群内的topic

1
2
3
4
5
# 进入容器
docker exec -it [容器名称或ID] /bin/bash

# 调用kafka脚本
kafka-topics.sh --bootstrap-server localhost:9092 --list

集群内新建一个topic

1
2
# 用脚本创建
kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 3 --partitions 3 --topic TOPIC_LY_TEST

查看某个topic信息

1
kafka-topics.sh --describe --topic TOPIC_LY_TEST --bootstrap-server localhost:9092

Win11配置两个git用户

背景

该方法太麻烦,不建议使用,建议参考这篇Git多用户环境隔离提交

有两个github账号,一个主要负责公开的内容,一个私人的,需要在同一台电脑上满足代码提交且互不干扰。核心操作分为三步:

  1. 配置ssh的config文件
  2. 切换用户
  3. 关闭全局用户名称(可选)

测试环境

  • Win: 11
  • OpenSSH: 8.6
  • Git: 2.39.1.windows.1

1. 配置.ssh文件夹下config文件

生成key

自己有秘钥对就不用生成了

1
2
3
ssh-keygen -t rsa -b 4096 -C "luyublog@gmail.com"

输入保存位置,密码(最好需要,否则我的环境下后续会报错)等信息,这里有几个账号就生成几次

绑定key

将public key与github关联

更改ssh配置文件

目录:~/.ssh/ 下的 config,没有则新建一个,IdentityFile后面接私钥的路径,内容如下:

1
2
3
4
5
6
7
8
9
Host east
HostName github.com
User git
IdentityFile the/path/to/your/privake_key

Host luyublog
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa_github_luyublog

检查本地的OpenSSH服务安装状态

设置->可选功能->OpenSSH 保证已经安装好了服务。

增加key

这里我是切换到了密钥对所在路径执行的命令所以不用带路径

1
2
3
4
5
6
7
8
// 增加第一个秘钥
ssh-add the_path_to_your_prikey_key

// 增加第二个秘钥
ssh-add id_rsa_github_luyublog

# 验证识别是否正确,如果ok的话会有成功提示
ssh -T luyublog

增加key时报错:Error connecting to agent: No such file or directory

原因:OpenSSH服务没有开启
参考

解决办法:

1
2
3
4
5
6
7
8
9
10
11
12
# 查看服务状态,显示Stopped说明就是有安装且关闭状态,开启一下就行
get-service ssh*

结果:
Status Name DisplayName
------ ---- -----------
Stopped ssh-agent OpenSSH Authentication Agent

# 手动开启服务
Set-Service -Name ssh-agent -StartupType Manual

Start-Service ssh-agent

增加key时报错:Bad permissions. Try removing permissions for user balabala

原因:私钥权限太低,建议重新生成带密码的的密钥对

2. 切换用户

其实没有切换用户的这个功能,git是根据上面的config和你的远程仓库@后面的host来判断使用哪个用户提交。所以使用不同的用户核心在于远程仓库的配置

配置示例

最好找一个现有测试工程验证,在测试仓库的路径下执行以下命令

1
2
3
4
5
6
7
8
# 清除仓库之前的关联
git remote rm origin

# 添加新的ssh格式仓库地址,注意这里格式:git@luyublog:luyublog:第一个luyublog是config里面host,第二个是github的用户名,不能直接使用github提供的ssh链接

git remote add origin git@luyublog:luyublog/SpringBootDemo.git

# 随便修改点东西然后提交,如果是当前用户的仓库且远程地址配置正确应该就会弹出密码验证,不是的话应该就报没权限了,建议检查仓库地址是否配置正确

提交时报错: Permission denied

git@github.com: Permission denied (publickey). Could not read from remote repository. Please make sure you have the correct access rights and the repository exists

可能的原因:

  1. 用户不对,用了a用户去提交b用户的仓库
  2. 检查远程仓库格式不对,直接使用了github提供的链接

解决:

  1. 检查上文 配置示例 切换正确远程仓库
  2. 还是检查上文的仓库链接格式 配置示例

3. 关闭全局name和email(可选)

如果你的两个账户需要区分名称那么关闭name和email的全局配置很重要,不然两个用户提交的名称都是一样就尴尬了。需要注意的是,局部email最好和你github使用的email是同一个,否则提交是按两个人算的。但是这里有个奇怪的地方就是信息改成局部后在github上看到的名称和你config设置的并不一样貌似用的github用户名,这里我没有深究原因。注意,关闭全局配置后每个工程都要重新设置一遍局部信息。

1
2
3
4
5
6
7
8
9
10
# 看下当前全局配置
git config --global -l

# 取消掉全局配置
git config --global --unset user.name
git config --global --unset user.email

# 设置局部配置,不设置无法提交
git config user.name luyublog
git config user.email luyublog@gmail.com

参考

  1. https://juejin.cn/post/6844903831000596488

  2. 如何在一台电脑上管理/切换多个github账户

修改经过Spring Gateway的Json数据

背景

使用Spring Cloud Gateway作为网关时经常会需要对报文内的json数据进行修改,但是目前看到的实现方法看起来都很复杂,这里提供一种使用Spring官方提供的ModifyRequestBodyGatewayFilterFactory类来修改json报文的方法

依赖

  • Spring Boot版本:2.7.15
  • Hutool: 5.8.21
  • Java: 11

实现逻辑

实现分为两个部分

  • filter:在自定义的filter内注入ModifyRequestBodyGatewayFilterFactory类,然后调用,该类会自动完成对修改结果的重新包装。
  • service: 自定义的service实现ModifyRequestBodyGatewayFilterFactory类的RewriteFunction<T, R>接口完成对json数据的处理

自定义filter

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
@Component
@Slf4j
public class RequestModifyFilter implements GlobalFilter, Ordered {
@Autowired
private ModifyRequestBodyGatewayFilterFactory modifyRequestBodyFilter;
@Autowired
private JsonRequestBodyRewriteService jsonRequestBodyRewriteService;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
if (MediaType.APPLICATION_json.isCompatibleWith(mediaType)) {
// 纯json报文处理逻辑
return modifyRequestBodyFilter
.apply(
new ModifyRequestBodyGatewayFilterFactory.Config()
.setRewriteFunction(byte[].class, byte[].class, jsonRequestBodyRewriteService))
.filter(exchange, chain);
} else {
return filter(exchange, chain);
}

}

@Override
public int getOrder() {
return OrderConstant.REQUEST_MODIFY_FILTER.getOrder();
}
}

自定义service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
@Slf4j
public class JsonRequestBodyRewriteService implements RewriteFunction<byte[], byte[]> {
@Override
public Publisher<byte[]> apply(ServerWebExchange exchange, byte[] body) {
jsonObject request = jsonUtil.parseObj(body);
log.info("原始报文:{}", request.toString());
try {
request.set("empId", "2345");
request.set("department", "Engineering");

log.info("修改后报文:{}", request);
return Mono.just(request.toString().getBytes());
} catch (Exception e) {
log.error("修改报文时出错",e);
throw e;
}
}
}

相关代码

https://github.com/eastcukt/demo-gatway

参考

https://amitkhurana92.medium.com/request-body-transformation-in-spring-cloud-gateway-bb9c234d5f3d

修改经过Spring Gateway的表单中的Json数据

背景

使用Spring Cloud Gateway作为网关时有时候一个请求是既包含excel又包含json的表单数据,出于各种层面考虑网关需要获取并更新其中的json数据

依赖

  • Spring Boot版本:2.7.15
  • Hutool: 5.8.21
  • Java: 11

实现逻辑

实现分为2个部分

  1. 使用上文提到的ModifyRequestBodyGatewayFilterFactory类来修改请求体,这样最后就不用我们手动包装
  2. 核心service通过将表单转为String,然后根据其中的boundary进行分割,提取修改json报文部分后再进行组装

注意:示例代码的核心service处理的表单内容只是2个,Json数据的key指定为json,另一个excel文件流

自定义filter

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
@Component
@Slf4j
public class RequestModifyFilter implements GlobalFilter, Ordered {
@Autowired
private ModifyRequestBodyGatewayFilterFactory modifyRequestBodyFilter;
@Autowired
private JsonRequestBodyRewriteService jsonRequestBodyRewriteService;
@Autowired
private FormDataRequestBodyRewriteService formDataRequestBodyRewriteService;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
if (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) {
// 纯json报文处理逻辑
return modifyRequestBodyFilter
.apply(
new ModifyRequestBodyGatewayFilterFactory.Config()
.setRewriteFunction(byte[].class, byte[].class, jsonRequestBodyRewriteService))
.filter(exchange, chain);
} else if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(mediaType)) {
// form表单数据处理
return modifyRequestBodyFilter
.apply(
new ModifyRequestBodyGatewayFilterFactory.Config()
.setRewriteFunction(byte[].class, byte[].class, formDataRequestBodyRewriteService))
.filter(exchange, chain);
} else {
return filter(exchange, chain);
}

}

@Override
public int getOrder() {
return OrderConstant.REQUEST_MODIFY_FILTER.getOrder();
}
}

核心service

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
@Service
@Slf4j
public class FormDataRequestBodyRewriteService implements RewriteFunction<byte[], byte[]> {
private final String BOUNDARY_PREFIX_IN_CONTENT_TYPE = "----WebKitFormBoundary";
private final String BOUNDARY_PREFIX_IN_FORM_DATA = "------WebKitFormBoundary";
private final String BOUNDARY_SUFFIX = "--\r\n";

@Override
public Publisher<byte[]> apply(ServerWebExchange exchange, byte[] body) {
String finalResultString = "";

// 将表单转为字符串格式从而根据boundary分割表单数据。注意这里不能用默认编码,需要iso8859单字节处理,其他编码会补充字节数导致文件损坏
String request = StrUtil.str(body, StandardCharsets.ISO_8859_1);

// 获取boundary的随机字符信息
String contentType = exchange.getRequest().getHeaders().getContentType().toString();
String randomStr = contentType.substring(contentType.indexOf(BOUNDARY_PREFIX_IN_CONTENT_TYPE) + BOUNDARY_PREFIX_IN_CONTENT_TYPE.length());

// 这里和前端约定json数据的表单key为json
String keyPart = "^\r\nContent-Disposition: form-data; name=\"json\"";
Pattern r = Pattern.compile(keyPart);

// 根据表单内分割线进行分割。并通过关键段落keyPart来找到目标json数据
String[] split = request.split(BOUNDARY_PREFIX_IN_FORM_DATA + randomStr);
for (int x = 0; x < split.length - 1; x++) {
Matcher m = r.matcher(split[x]);
if (m.find()) {
// 找到了json报文部分数据
String originalJsonString = split[x];

// 找到 JSON 数据的起始和结束位置
int startIndex = originalJsonString.indexOf("{\"");
int endIndex = originalJsonString.indexOf("\"}") + 2;
// 提取 JSON 数据
String jsonData = originalJsonString.substring(startIndex, endIndex);
log.info("原始报文为:{}", jsonData);

JSONObject jsonObject = JSONUtil.parseObj(jsonData);
jsonObject.set("empId", "2345");
jsonObject.set("department", "Engineering");
String modifiedString = originalJsonString.substring(0, startIndex) + jsonObject + originalJsonString.substring(endIndex);
log.info("修改后报文为:{}", modifiedString);

// 重新组装split数组
finalResultString = finalResultString + modifiedString + BOUNDARY_PREFIX_IN_FORM_DATA + randomStr;
} else {
// 重组表单数据
finalResultString = finalResultString + split[x] + BOUNDARY_PREFIX_IN_FORM_DATA + randomStr;
}
}

// 补上最后一截数据
finalResultString = finalResultString + BOUNDARY_SUFFIX;

return Mono.just(finalResultString.getBytes(StandardCharsets.ISO_8859_1));
}
}

相关代码

https://github.com/cheedonghu/SpringCloudGatewayDemo

其他

核心service获取表单中的json数据逻辑挺复杂,根本原因是没有合适的方法进行对象转换,如果有像使用@RequestPart(value = “json”)注解一样方便的方法将会非常方便也不用自己截取,各位大佬有更方便的方法感谢分享一下

参考

https://blog.csdn.net/qq_36966137/article/details/128536391

docker安装oracle

背景

公司用的数据库是oracle,家里装的是mysql,start with递归查询的sql只能用oracle处理,所以需要再装一个oracle,docker肯定是最方便的。

环境信息

docker-compose

为了方便这里用docker-compose。考虑用sudo -i切到root用户会方便很多

参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
version: "3"
services:
oracle_db:
image: wnameless/oracle-xe-11g-r2
ports:
- 15210:1521
environment:
- ORACLE_SID=ORCLCDB
- ORACLE_PDB=ORCLPDB1
volumes:
# 这里用命名卷不要用挂载点
- oracle_vol:/u01/app/oracle

volumes:
oracle_vol:

一些解释

  1. 把端口重新映射到15210是为了减少一些密码爆破

  2. 这里只能用命名卷进行挂载,挂载点会报错

可能的报错

cp: cannot stat ‘/u01/app/oracle/product/11.2.0/xe/network/admin/listener.ora.tmpl’: No such file or directory

docker主要有两种挂载方式,命名卷和挂载点,内部实现不同,如果是这个报错说明你可能是用的挂载点这种卷使用方式,改成上面yml里的命名卷就行。根因的话,需要深入研究两种挂载方式的实现原理。

连接说明

  1. database(serviceName)是XE
  2. 初始用户名密码是system/oracle

修改密码

参考

初始用户名密码是system/oracle,最好是需要改一下的。另密码貌似只能字母数字,常用的特殊字符都不行,解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 找到oracle镜像id
docker ps

# 进入镜像
docker exec -it 镜像id bash

# 切换用户
su - oracle

# 连接
sqlplus /nolog

# 用户名密码
connect system/oracle as sysdba

# 修改system用户密码为123456,注意这是sql有;号
alter user system identified by 123456;

# sys用户也要改一下密码
# connect sys/oracle as sysdba
alter user sys identified by 123456;

# 退出
quit

数据备份与恢复

备份

  1. 这里要备份的docker卷一般在/var/lib/docker/volumes/目录下,而且卷名称可能和你在yml里命名不一样,可以看下日志看看具体名称,需要自己判别一下。
  2. 备份的时候容器需要关掉
  3. 如果cpu很烂就仅打包不压缩
  4. docker volume相关命令可以看卷信息
1
2
3
4
5
# 打包并压缩
tar cvzf /path/on/host/backup/oracle_vol_backup.tar.gz -C /path/to/your/docker/volume/target/_data .

# 或者只打包,如果cpu很烂的话
tar cvf /path/on/host/backup/oracle_vol_backup.tar -C /var/lib/docker/volumes/oracle_oracle_vol/_data .

恢复

  1. 运行相同的compose配置文件
  2. 将备份文件解压覆盖新的卷
1
2
3
4
5
# 解包解压缩
tar xvzf /path/on/host/backup/oracle_vol_backup.tar.gz -C /path/to/your/docker/volume/target/_data

# 或仅解包
tar xvf /path/on/host/backup/oracle_vol_backup.tar -C /var/lib/docker/volumes/oracle_oracle_vol/_data

NAT机使用方式

NAT机和VPS机区别

一个宿主机上可以开N个NAT鸡,他们共享同一个公网IP和带宽。和VPS一样,你可以得到完整的root权限,但不同是你只能得到有限的端口。

用法

1 国内高速下载

如果希望给用户提供高速下载,一般的选择是要么买CDN分发服务,要么买大带宽服务器。因为国情限制,这两种方式的成本都很高,NAT的作用就出来了,昂贵的ip和带宽成本可以分摊到许多台机器上(理论上多达六万台),因此可以用廉价的方式在共享带宽下提供高速下载,性价比远超用CDN和买普通大带宽服务器。

2 代理使用

如果共享的IP有特殊属性比如家宽NAT机,那么就可以做到普通IP做不到的,比如流媒体解锁。

使用逻辑

客户端->NAT解锁机->目标服务

使用流程

如果数据传输是利用基于TCP和TLS实现的协议,首先通过CNAME和DNS+TXT记录给NAT机申请证书,后面就和普通VPS机一样处理就行。
对于SS这类和普通VPS一样使用。
注:记得在面板开放端口。

3 中转使用

这类NAT一般是优质线路或者IPLC

中转逻辑

客户端->NAT->落地机->目标服务

客户端->NAT

从客户端到NAT的流量一般都要加密,这里要根据商家限制选择,有的支持SS,有的只允许通过TLS加密后的流量。

NAT->落地机

NAT到落地可以直接使用流量转发,常见的有iptables,gost和brook。
值得注意的是有些商家即使是这一阶段也不允许转发未加密或弱加密流量(例如SS,SSR),所以是直接转发流量还是脱密后转发要看情况选择。

1
2
# gost安装脚本
wget --no-check-certificate -O gost.sh https://raw.githubusercontent.com/KANIKIG/Multi-EasyGost/master/gost.sh && chmod +x gost.sh && ./gost.sh

僵尸毁灭工程服务器搭建

背景

最近准备和女朋友玩僵毁联机,翻了下以前的笔记,太老了,更新一下。
对linux不熟悉建议使用Debian或者ubuntu系统,centos属实没必要。

环境信息

系统:Debian 10
内存:4G
端口开放:
16261 UDP (PZ服务器)
8766 TCP (貌似不需要)

注:如果vps没重要信息的话建议切换到root用户操作。

安装命令解析

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
# 更新一下源
apt update

# 安装一下必要的运行库
apt install -y lib32gcc1 libsm6 libxext6 libxrender-dev
# 如果上面的lib32gcc1报错,运行下面这个
apt install -y lib32gcc-s1 libsm6 libxext6 libxrender-dev

# 新建一个文件夹存放游戏 路径随意
mkdir /usr/local/games

# 新建一个文件夹存放steam
mkdir /usr/local/etc/steam && cd /usr/local/etc/steam

# 下载steam
wget http://media.steampowered.com/installer/steamcmd_linux.tar.gz

# 解压安装包
tar -xvzf steamcmd_linux.tar.gz

# 启动steam
./steamcmd.sh

# 设置一下安装路径(可选)
force_install_dir /usr/local/games

# 登录你的steam,僵毁我看steam官网文档上说不支持匿名下载(确信108600是不行的)
login 你的用户名 密码

# 下载僵毁
# 下载最新的版本,unstable可以换成steam上你看到的其他分支(国内机器经常断联多跑几次)
app_update 108600 -beta unstable validate
# 也可以下载默认版本
或者app_update 108600 validate 默认的

# 下载完成后退出
exit

# 查看游戏的下载位置,运行下面这个找找到底装哪里去了
find / -name ProjectZomboid

# 进入路径
cd 屏幕上显示的路径

# 进入游戏目录并运行开服脚本,然后按提示输入管理员密码就行了
cd projectzomboid && ./start-server

# 最后去游戏里面应该就可以搜到了

下载安装出现的问题

1. linux32/steamcmd: No such file or directory

原因:缺库
解决办法:apt-get install lib32gcc1
参考

2. error while loading shared libraries: libSM.so.6: cannot open shared object file: No such file or directory

原因:缺库
解决办法:apt-get install -y libsm6 libxext6 libxrender-dev

3.SteamAPI_Init() failed; create pipe failed.Message: ‘Fatal Error’, Detail: ‘Steam must be running to play this game (SteamAPI_Init() failed).

原因1:steam要运行在后台
~~ 解决办法:
将ProjectZomboid64.json里面的参数修改~~
“-Dzomboid.steam=1”
改为
“-Dzomboid.steam=0”
参考~~

原因2:你运行错了文件(比如我),开服应该运行start-server.sh这个文件

4.Package ‘lib32gcc1’ has no installation candidate

原因:库不同,装个兼容的
apt-get install -y lib32gcc-s1

5.Sys_LoadModule failed to load: /root/.steam/sdk64/steamclient.so

原因1:大概率是没有指定下载位置导致运行缺库
先创建这个文件夹:mkdir /root/.steam/sdk64
找到steamclient.so文件在哪
find / -name steamclient.so
复制linux64路径下的那个文件到目标地区
cp /usr/local/etc/steam/linux64/steamclient.so /root/.steam/sdk64/steamclient.so
回到ProjectZomboid的目录下执行./projectzomboid.sh

原因2:运行的文件错误(比如我),开服应该是start-server.sh

6. 开服成功但是搜索不到

原因很多,大概以下几点:

  1. servertest.ini文件的Open没设为true
  2. 主机厂商安全组策略
  3. start-server.sh这个文件的Dzomboid.steam参数改为1(卡了我一晚上,如果有关键字Steam is not enabled应该就是这个原因。)
  4. 其他原因:可以尝试直接通过ip地址和服务器端口进行连接

设置后台启动方式

方法一

nohup 命令
缺点:不好关

方法二:

WorkingDirectory和ExecStart改为你的路径,不清楚的find一下就行,一次性复制下面所有代码并执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cat >/usr/lib/systemd/system/zomboid.service <<'EOL'
[Unit]
Description=Project Zomboid Server
After=network.target

[Service]
PrivateTmp=true
Type=simple
User=root
WorkingDirectory=/usr/local/etc/steam/steamapps/common/ProjectZomboid/projectzomboid/
ExecStart=/usr/local/etc/steam/steamapps/common/ProjectZomboid/projectzomboid/start-server.sh

[Install]
WantedBy=multi-user.target
EOL

之后就可以通过这些命令启动重启了

1
2
3
4
5
6
7
8
9
10
11
# 启动
systemctl start zomboid.service

# stop a server
systemctl stop zomboid.service

# restart a server
systemctl restart zomboid.service

# 查看状态,执行后按q可以退出
systemctl status zomboid.service

配置沙盒参数

官方参考

servertest.ini 文件

servertest_SandboxVars.lua

servertest_spawnpoints.lua

servertest_spawnregions.lua

ProjectZomboid64.json

IPLC专线测试

背景

想要既能丝滑打开GitHub又能作为游戏加速器使用,最好的选择就是iplc专线

种类

沪港,深港,广港,苏日等

协议

协议的选择要根据商家判断,每家运行的协议都不一样。SS优先级大于Trojan

准备

这里准备了两台HK落地机,一台国内直连,一台绕美,和一台日本软银东京落地机

中转效果测试

注:iplc关键是低延迟和稳定,速度不是关键

转发线路:深港IPLC

时间:晚上10点
内核:bbr+cake
客户端线路:电信
测速文件大小:10M

原线路 协议 原tcp 原真连接 原速度(MB/s) 中转前PUBG延迟 中转后tcp 中转后真连接 中转后速度 中转后PUBG延迟
BGP香港直连 Trojan 35ms 200ms 9M 70ms左右 35ms 250ms 5w浮动 70ms左右
香港非直连 Trojan 300ms左右 600ms左右 1.5M 无意义 38ms 350ms 3.5M
BGP香港直连 vmess 35ms 200ms 9M 70ms左右 40ms 400ms 5w浮动 70ms左右
香港非直连 vmess 300ms左右 50-500ms? 1.5M 无意义 38ms 600ms?? 3.5M
洛杉矶9929 vless 160ms 810ms左右 3.8M 无意义 40ms 200ms 3w

注:

  • HK GIA和BGP类似(其实就是稳定性高了些)
  • 速度不重要,因为主要看转发和落地机的带宽

转发线路:苏日IPLC

时间:早上10点
内核:bbrplus+fq
客户端线路:电信
测速文件大小:10M

原线路 协议 原tcp 原真连接 原速度(MB/s) 中转前pubg延迟 中转后tcp 中转后真连接 中转后速度 中转前pubg延迟
东京软银 Trojan 45ms 250ms 6M 无意义 45ms 220ms 7w 75ms左右
东京软银 ss 42ms 250ms 6M 无意义 45ms 220ms 7w

结论

  1. iplc不过墙,但入口审查同样存在,如果只能使用tls加密那么肯定增加延迟。
  2. 想要更低延迟就需要入口机和落地机配合,专线入口越近越好。对于出口来说深港的落地最好是国际互联好的香港机,苏日最好就是出口机器本地地区且国际互联好的机器。
  3. 实际使用的时候延迟有波动,且丢包有点多,单纯转发并不稳定,如果游戏是基于udp可能会好点。
  4. 等有机会试试nat机,相对来说可能好点。

如何让静态博客响应更快

0x00 背景

粗略的记录一下从各个方面让博客有更快的响应速度

0x01 基本知识

一次HTTP的请求过程示例:

用户在浏览器输入luyublog.com后浏览器处理流程(简略版本,不考虑具体交互)

  1. 浏览器查询本地DNS缓存获取luyublog.com的IP,若没有则查询操作系统DNS缓存,若还没有则请求公共DNS服务器(114,阿里等)
  2. 获取到网站IP地址后通过TCP建立连接
  3. 通过HTTP协议进行报文交互

0x02 可能的优化点

了解基本流程后就可以一步步进行优化,例如:

  1. 用户浏览器DNS解析步骤
  2. TCP连接建立过程
  3. 服务器处理速度

DNS解析优化

DNS解析优化的关键点有两个:

  1. 解析速度
  2. 解析到最合适的IP地址(如果你有多个服务器)

DNS解析速度

若本地DNS缓存没有命中的话,DNS解析本质上还是HTTP请求,所以理论上国内的DNS服务肯定是优于国外的,国内出名的有DNSPOD,国外的有CloudFlare。具体要用哪个需要自行判断。

DNS智能解析(前提:有多IP)

DNS智能解析的意思是将域名解析到最合适的服务器IP上,例如距离用户最近的服务器或响应最快的服务器,智能解析这部分主要有:

  • 地域解析(GEODNS)
  • 运营商线路解析
  • 任播(AnyCast)

DNS地域解析(GEODNS)

  • 前提:有多服务器IP 其中1.1.1.1位于亚洲 2.2.2.2位于美洲
  • 例子:若亚洲用户访问luyublog.com则通过GEODNS将域名解析到1.1.1.1,美洲用户解析到2.2.2.2

通过DNS将用户解析到其最近的服务器上,理论上就会缩短网站服务的响应时间,毕竟物理距离摆在那。国内很多免费DNS解析商都提供这项功能,国外免费的目前只发现gcore提供,ns2貌似也有(但感觉容易被付费)。

DNS运营商线路解析

  • 前提:有多服务器IP 其中1.1.1.1服务器移动线路友好 2.2.2.2电信线路友好
  • 例子:若移动用户访问luyublog.com则将域名解析到1.1.1.1,电信用户解析到2.2.2.2

举个例子,如果你的服务器1.1.1.1线路是移动直连,联通电信入网绕路。2.2.2.2电信直连,移动绕路,那么就可以将移动用户解析到1.1.1.1。电信,联通用户解析到2.2.2.2

如果服务器在国内这项功能基本用不到因为都差不多,但是在国外的话就有些用了,因为晚高峰除了像CN2,9929,CMI这些高级线路,国际普通线路丢包率和延迟普遍都高。

这项功能大型DNS服务商貌似都有但国外没看到免费的,国内的话,大厂DNS都提供。

DNS任播(AnyCast)

  • 前提:有多服务器IP 其中1.1.1.1位于亚洲 2.2.2.2位于美洲
  • 介绍:详情可以自行搜索,简单来说就是利用网络拓补结构,通过算法寻找到最合适的特定节点(综合考虑地理位置,服务器状态,线路质量等),根据老外测试加速效果和GEODNS差不多GeoDNS vs Anycast

这个国外没找到免费的。

服务器连接线路优化

上面说完DNS解析拿到服务器IP,下面记录从浏览器到服务器建立连接过程优化,如果是国内的服务器就别看了,基本用不上。

国外服务器到国内是有线路区别的,晚高峰国内入口必然会拥堵,所以尽量选择线路好一点的服务器或者使用CDN加速。

优质线路

  • 联通9929
  • 移动CMI
  • 电信CN2

CDN加速

CDN详细解释自行搜索,用上CDN后对网站响应,对服务器负载和带宽都有好处,这里还是说说免费的,国外出名的有Cloudflare,但是在国内效果不稳定。国内的CDN服务需要网站先备案。这里就不讨论了。

服务器处理优化

优化服务器处理之前首先得弄明白http请求到达服务器后的流程,这里大概归结为:http请求->缓存->内容->返回,所以这部分可以优化的部分大致为

  • cpu(INTEL,AMD)
  • 内存
  • 磁盘(NVME,SSD,HDD)
  • 缓存

对于前三样没什么好说的,好的CPU,内存和磁盘必然更快,一分钱一分货。下面说一下软件层面的优化,主要就是负载均衡和缓存。

负载均衡

这里说的负载均衡只是用在服务器响应方面的,完整的负载均衡是个很大的概念,
简单来说就是通过一定的策略让用户更好的被服务,让服务器保持一致性,可用性。详细的解释可以自行搜索一下,这里介绍一些负载均衡的实现:

缓存

这里的缓存主要指的是请求发到nginx再到获取内容后返回。其中优化点主要就是nginx缓存,具体细节可以看看这篇文章

0x03 最后补充

这里介绍的方法只是针对静态博客而言,建议根据自身网站特性针对性优化。

参考: