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

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


  • 首页

  • 归档

  • 搜索

【死磕Java并发】-----JUC之重入锁:Reent

发表于 2021-11-25

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


此篇博客所有源码均来自JDK 1.8

ReentrantLock,可重入锁,是一种递归无阻塞的同步机制。它可以等同于synchronized的使用,但是ReentrantLock提供了比synchronized更强大、灵活的锁机制,可以减少死锁发生的概率。 API介绍如下:

一个可重入的互斥锁定 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁定相同的一些基本行为和语义,但功能更强大。ReentrantLock 将由最近成功获得锁定,并且还没有释放该锁定的线程所拥有。当锁定没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁定并返回。如果当前线程已经拥有该锁定,此方法将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查此情况是否发生。

ReentrantLock还提供了公平锁也非公平锁的选择,构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁与非公平锁的区别在于公平锁的锁获取是有顺序的。但是公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。

获取锁

我们一般都是这么使用ReentrantLock获取锁的:

1
2
3
csharp复制代码//非公平锁
ReentrantLock lock = new ReentrantLock();
lock.lock();

lock方法:

1
2
3
csharp复制代码public void lock() {
sync.lock();
}

Sync为ReentrantLock里面的一个内部类,它继承AQS(AbstractQueuedSynchronizer),它有两个子类:公平锁FairSync和非公平锁NonfairSync。

ReentrantLock里面大部分的功能都是委托给Sync来实现的,同时Sync内部定义了lock()抽象方法由其子类去实现,默认实现了nonfairTryAcquire(int acquires)方法,可以看出它是非公平锁的默认实现方式。下面我们看非公平锁的lock()方法:

1
2
3
4
5
6
7
8
scss复制代码final void lock() {
//尝试获取锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//获取失败,调用AQS的acquire(int arg)方法
acquire(1);
}

首先会第一次尝试快速获取锁,如果获取失败,则调用acquire(int arg)方法,该方法定义在AQS中,如下:

1
2
3
4
5
scss复制代码public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

这个方法首先调用tryAcquire(int arg)方法,在AQS中讲述过,tryAcquire(int arg)需要自定义同步组件提供实现,非公平锁实现如下:

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
java复制代码protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
//当前线程
final Thread current = Thread.currentThread();
//获取同步状态
int c = getState();
//state == 0,表示没有该锁处于空闲状态
if (c == 0) {
//获取锁成功,设置为当前线程所有
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//线程重入
//判断锁持有的线程是否为当前线程
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

该方法主要逻辑:首先判断同步状态state == 0 ?,如果是表示该锁还没有被线程持有,直接通过CAS获取同步状态,如果成功返回true。如果state != 0,则判断当前线程是否为获取锁的线程,如果是则获取锁,成功返回true。成功获取锁的线程再次获取锁,这是增加了同步状态state。

释放锁

获取同步锁后,使用完毕则需要释放锁,ReentrantLock提供了unlock释放锁:

1
2
3
csharp复制代码public void unlock() {
sync.release(1);
}

unlock内部使用Sync的release(int arg)释放锁,release(int arg)是在AQS中定义的:

1
2
3
4
5
6
7
8
9
java复制代码public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

与获取同步状态的acquire(int arg)方法相似,释放同步状态的tryRelease(int arg)同样是需要自定义同步组件自己实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码protected final boolean tryRelease(int releases) {
//减掉releases
int c = getState() - releases;
//如果释放的不是持有锁的线程,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//state == 0 表示已经释放完全了,其他线程可以获取同步状态了
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}

只有当同步状态彻底释放后该方法才会返回true。当state == 0 时,则将锁持有线程设置为null,free= true,表示释放成功。

公平锁与非公平锁

公平锁与非公平锁的区别在于获取锁的时候是否按照FIFO的顺序来。释放锁不存在公平性和非公平性,上面以非公平锁为例,下面我们来看看公平锁的tryAcquire(int arg):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

比较非公平锁和公平锁获取同步状态的过程,会发现两者唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors(),定义如下:

1
2
3
4
5
6
7
8
9
10
11
java复制代码public final boolean hasQueuedPredecessors() {
Node t = tail; //尾节点
Node h = head; //头节点
Node s;

//头节点 != 尾节点
//同步队列第一个节点不为null
//当前线程是同步队列第一个节点
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

该方法主要做一件事情:主要是判断当前线程是否位于CLH同步队列中的第一个。如果是则返回true,否则返回false。

ReentrantLock与synchronized的区别

前面提到ReentrantLock提供了比synchronized更加灵活和强大的锁机制,那么它的灵活和强大之处在哪里呢?他们之间又有什么相异之处呢?

首先他们肯定具有相同的功能和内存语义。

  1. 与synchronized相比,ReentrantLock提供了更多,更加全面的功能,具备更强的扩展性。例如:时间锁等候,可中断锁等候,锁投票。
  2. ReentrantLock还提供了条件Condition,对线程的等待、唤醒操作更加详细和灵活,所以在多个条件变量和高度竞争锁的地方,ReentrantLock更加适合(以后会阐述Condition)。
  3. ReentrantLock提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而synchronized则一旦进入锁请求要么成功要么阻塞,所以相比synchronized而言,ReentrantLock会不容易产生死锁些。
  4. ReentrantLock支持更加灵活的同步代码块,但是使用synchronized时,只能在同一个synchronized块结构中获取和释放。注:ReentrantLock的锁释放一定要在finally中处理,否则可能会产生严重的后果。
  5. ReentrantLock支持中断处理,且性能较synchronized会好些。

推荐阅读

上面的获取锁,释放锁过程中有很多方法都是组合使用了AQS中的方法,作为同步组件的基础,AQS做了太多的工作,自定义同步组件只需要简单地实现自定义方法,然后加上AQS提供的模板方法,就可以实现强大的自定义同步组件,所以看完下面四篇博客,ReentrantLock理解起来真的是小菜一碟。

  1. 【死磕Java并发】—–J.U.C之AQS:AQS简介
  2. 【死磕Java并发】—–J.U.C之AQS:CLH同步队列
  3. 【死磕Java并发】—–J.U.C之AQS:同步状态的获取与释放
  4. 【死磕Java并发】—–J.U.C之AQS:阻塞和唤醒线程

参考资料

  1. Doug Lea:《Java并发编程实战》
  2. 方腾飞:《Java并发编程的艺术》

本文转载自: 掘金

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

如何体面应对弱联网和SlowPost攻击?

发表于 2021-11-25

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

引子

随着后端的架构设计逐渐往微服务架构转型,使得后端开发可以专心的开发业务逻辑(CRUD), 这也导致了部分开发人员对于客户端网络请求的处理方式不甚了解,因为他们只需要关心接口的出入参即可,并与客户端开发人员对接即可。

会注意到这个问题,是在内网排查问题时发现有几十个条接口耗时异常记录,此外还出现了部分接口鉴权失败(超时)的情况,虽然这些异常日志条数不多,但还是引起了我的警觉。一通分析后,发现接口的业务逻辑并不复杂,遂定位设备,发现原来是测试妹子在搞弱联网测试。

鉴权失败的原因是:服务端接收请求所消耗的时间已经耗光了整个接口的时间,到鉴权服务时已经没有多少时间可用了,进而出现了鉴权失败的问题(超时)。

项目中的鉴权服务是独立部署的

什么是弱联网

大多数后端开发人员并不在乎用户的网络情况是怎样的,也不在乎客户端程序员有没有对请求进行排队防抖节流啥的, 只关心怎么处理请求。而实际上在移动的设备普及的今天,大多数用户的网络稳定情况基本上都是处于一个不断变化的状态。

比如,一个人走进了厕所,其手机的网络大概率会发生波动(有些地方甚至会在厕所安装信号屏蔽器)。

更多弱联网的细节,可以参考移动端IM开发者必读(一):通俗易懂,理解移动网络的“弱”和“慢”

要理解网络波动会产生什么后果,我们需要来看一道经典的面试题:

一个HTTP请求从发出到接收到响应都会经历哪些阶段?

在没有使用HTTP连接池的情况下:

  • 域名解析阶段, 此阶段主要是解析出服务端的IP地址
  • 与服务端建立TCP连接, 发送HTTP报文
  • 等待服务端响应, 读到HTTP响应报文之后开始渲染页面或者执行其他操作

本文所有的讨论均基于HTTP1.1
更多详细细节, 请阅读一个HTTP请求的曲折经历

以注册接口为例,其请求发送到接收到响应数据所花费的时间将分为以下几段

image.png

那么很明显的一个问题就是用户处于弱联网的环境中的话相应的将导致请求数据的发送和响应数据接收的出现不稳定的情况,进而导致而客户端认为HTTP请求失败了。

对于由弱网络导致的请求报文发送失败的问题,服务端无需做任何处理,因为请求还没到达业务端(可能被Nginx或网关以Read timeout为由拒绝了)。客户端则需要做重试逻辑。

我们要重点关注的是接收响应报文阶段,即:客户都端请求已发出并且被后端处理了,但是接收响应数据阶段失败了。

  • 对于只读接口后端通常无需做任何处理,客户端网络请求失败后直接重试即可。
  • 对于数据变更接口, 由于在写出响应报文之前服务端已经提交了事务,因此如果客户端进行了重试服务端需要做好请求幂等性处理。(幂等性处理实用性很强但又很八股的知识点了属于是)

SLOW POST

SLOW POST是DDOS攻击的手段之一, 其特征为缓慢的读取数据以及缓慢的发送数据,让服务端长时间去维护一个无意义的TCP连接 从而把服务端的资源压榨得一滴不剩,达到攻击的目的。

之所以将其和弱联网扯到一起,是因为二者都有相似之处即在数据发送和接收方面均表现出了不稳定。区别在于是否主动。

其实现原理很简单, 只需构造出一个合法的HTTP请求报文,然后逐个发送字节或者逐个字节的接收响应报文,这么做的后果就是服务端的操作系统需要在内核中长时间维护恶意用户的TCP连接从而挤压正常用户的资源,最终导致服务崩溃。

具体如何实现可以参考开源项目 slowhttptest

防御手段

防御手段的原理很简单, 就是为每一个HTTP请求设置读/写超时时间(Read/Writetimeout)。

  • 在指定时间内请求没有被读完,则返回Request timeout错误
  • 在指定时间内响应数据没有写完,则关闭连接释放资源

之所以说是体面,因为这些问题开源软件的作者都已经考虑到,并且提供了参数以供控制, 但这些开源软件的默认值大都数都设置了一个比较高的数值或者干脆就没有设置,因此我们需要根据实际情况进行调整。

Niginx

  • 通过client_header_timeout和client_body_timeout两个参数分配控制读HTTP请求头(HttpHeader)和请求内容(Body)的超时时间
  • 通过send_timeout来控制发送响应数据的超时时间

详细请参考此文

Tomcat

通过调整server.xml中Connector的connectionTimeout数来控制整个请求的超时时间, 相比Nginx来说确实没那么细

1
2
3
xml复制代码    <Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />

Golang

golang的项目可以在初始化http.Server的时候设置读写超时时间并且可以设置keep-alive超时时间(多久没活动就关闭连接).

1
2
3
4
5
6
go复制代码	server := http.Server{
ReadTimeout: 5 * time.Second, //读超时
ReadHeaderTimeout: 5 * time.Second, //读请求头超时
WriteTimeout: 5 * time.Second, //写超时
IdleTimeout: 5 * time.Second, //Keep-alive超时
}

总结

  • 应对弱联网, 保证接口幂等性
  • 应对SLOW POST攻击, 设置读写超时间
  • 能用Nginx做代理就别直接暴露服务了

本文转载自: 掘金

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

【Sentinel系列】Sentinel Dashboard

发表于 2021-11-25

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

  • 修改resources/app/scripts/directives/sidebar/sidebar.html文件部分代码,把dashboard.flowV1改成dashboard.flow。
1
2
3
4
5
html复制代码<li ui-sref-active="active" ng-if="entry.isGateway">
<a ui-sref="dashboard.gatewayFlow({app: entry.app})">
<!--<a ui-sref="dashboard.gatewayFlowV1({app: entry.app})">-->
<i class="glyphicon glyphicon-filter"></i>&nbsp;&nbsp;流控规则</a>
</li>

修改后会调佣FlowControllerV2的接口。

  • 在com.alibaba.csp.sentinel.dashboard.rule包中创建一个Nacos包,并创建一个类用加载外部化配置。
1
2
3
4
5
6
7
8
java复制代码@Data
@ConfiguartionProperties(prefix="sentinel.nacos")
public class NacosPropertiesConfiguration{
private String serverAddr;
private String groupId = "DEFAULT_GROUP";
private String namespace;
private String dataId;
}
  • 创建一个Nacos配置类NacosConfiguration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@EnableConfigurationProperties(NacosPropertiesConfiguration.class)
@Configuration
public class NacosConfiguration{
@Bean
public Converter<List<FlowRuleEntity>,String> flowRuleEntityEncoder(){
retur JSON::toJSONString;
}
@Bean
public Converter<String,List<FlowRuleEntity>> flowRuleDecoder(){
return s->JSON.parseArray(s,FlowRuleEntity.class);
}
@Bean
public ConfigService nacosCOnfigService(NacosPropertiesConfiguartion nacosPropertiesConfiguration) throws NacosException{
Properties properties = new Properties();
properties.put(PropertyKeyConst.NAMESPACE,nacosPropertiesConfiguration.getNamespace());
properties.put(PropertyKeyConst.SERVER_ADDR,nacosPropertiesConfiguration.getServerAddr());
return ConfigFactory.createConfigService(properties);
}
}

注入Controller转换器,把FlowRuleEntity转化为FlowRule,并且反向转化
注入Nacos配置服务ConfigService

  • 创建一个常量类NacosConstants,分别表示默认的data_id和group_id的后缀。
1
2
3
4
java复制代码public class NacosConstants{
public static final String DATA_ID_POSTFIX = "-sentinel-flow";
public static final String GROUP_ID = "DEFAULT_GROUP";
}
  • 实现动态从Nacos配置中心获取流控规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码@Service
@Slf4j
public class FlowRuleNacosProvider implements DynamicRuleProvider<List<FlowRuleEntity>>
{

@Autowired
private NacosPropertiesConfiguration nacosPropertiesConfiguration;
@Autowired
private Converter<String,List<FlowRuleEntity>> converter;

@Override
public List<FlowRuleEntity> getRules(String appName) throw Exception{
String dataId=new StringBuilder(appName).append(NacosConstants.DATA_ID_POSTFIX).toString();
String rules=ConfigService.getConfig(dataId,nacosPropertiesConfiguration.getGroupId(),3000);
log.info("从Nacos配置中心推送流控规则:{}",rules);
if(StringUtils.isEmpty(rules)){
return new ArrayList<>();
}
return converter.convert(rules);
}
}

通过ConfigService.getConfig方法从Nacos Config服务端中读取指定配置信息,并且通过converter转化为FlowRule规则。

  • 创建一个流控规则发布类,在Sentinel Dashboard 上修改配置后,需要调用发布方法将数据持久化到Nacos配置中心上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Service
public class FlowRuleNacosPublisher implements DynamicRulePublisher<List<FlowRuleEntity>>{
@Autowired
private NacosPropertiesConfiguration nacosPropertiesConfiguration;
@Autowired
private Converter<String,List<FlowRuleEntity>> converter;

@Override
public void publish(String appName,List<FlowRuleEntity> rules) throws Exception{
AssertUtil.notEmpty(appName,"appName 不能为空");
if(rules == null){
return;
}
String dataId = new StringBuilder(appName).append(nacosPropertiesConfiguration.DATA_ID_POSTFIX).toString();
configService.publishConfig(dataId,nacosPropertiesConfiguration.getGroupId(),converter.convert(rules));
}
}

本文转载自: 掘金

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

趣谈“分布式链路追踪“组件发展史

发表于 2021-11-25

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

你好,我是悟空呀。

发展史

CAL 和 CAT 傻傻分不清

eBay-CAL:咦,这是什么群啊?

点评-CAT:大佬好啊,我是你的小迷弟。

eBay-CAL:你好你好,你的名字和我的怎么这么像?我差点以为我和你是同一个。。

Google-Dapper:CAL 老哥,你不知道吧,CAT 就是基于你进行改造的,嘿嘿~

点评-CAT:老大,因为当时您那边没有开源,我基于您的设计理念,将 CAL 在大众点评发扬光大了,现在也开源了~

涉及的故事

eBay 2002 年,业务快速增长,流量猛增,非常需要一款链路监控工具,CAL 应运而生,被称作 eBay 的三大神器之一。CAL 全称:Centralized Application Logging

老吴从 eBay 跳槽到大众点评后,主导研发了 CAT,2011 年诞生。所以 CAT 和 CAL 有很多相似的地方。CAT 在国内很早就开源了,采用 Java 语言编写,社区也比较活跃。CAT 全称:Centralized Application Tacking。

Dapper 的继承者

mark

Twitter-ZipKin:Dapper Big Old!我深刻学习了您发表的 Dapper 论文,受益良多

Naver-Pinpoint:Dapper 대장부!

Apache-Skywalking:Pinpoint 大佬好!

Uber-Jaeger:Zipking Big Old!

点评-CAT:你们几个什么意思?把我和 CAL 老大晾到一边了?

eBay-CAL:时隔 19 年,竟然涌现了这么多链路追踪组件。。

Google-Dapper:没想到我的那篇 Dapper 论文竟然有这么大的功效。

涉及的故事

Google-Dapper:Google 公司内部有一款链路追踪组件 Dapper,非常强大,但是没有开源。在 2010 年,Google 发表了一篇 Dapper 的论文,介绍了 Dapper 链路追踪的原理,后来成为多家链路追踪组件的鼻祖。

Twitter-ZipKin:米国的 Twitter 公司大家应该知道吧,类似于我们的新浪微博,而 Zipkin 就是他们的链路追踪产品,在 2012 年早期开源,基于 Dapper 论文开发。

Naver-Pinpoint:Naver 是韩国的一家公司,聊天记录里面的打招呼 대장부 翻译过来就是“大佬好”。Pinpoint 也是基于 Dapper 论文的思想进行开发,功能丰富,2012 年开源,也是非常受欢迎的一款产品。

Uber-Jaeger:米国的 Uber 大家应该熟悉,曾经在国内的打车市场非常火爆,不过现在打车软件都是滴滴和其他平台了。Jaeger 时 Uber 公司的一款链路追踪产品,在 2016 年开源,吸收了 Zipkin 的设计思想,用的语言是 Golang,可以认为是 Zipkin 的克隆版,但是也有它自身的优点和亮点。

Apache-Skywalking:Skywalking 是国产的,Made In China,项目发起人吴晟结合了 OneAPM + PinPoint,打造的一款链路追踪组件,Skywalking 已经进入 Apache 孵化,国内社区活跃,可以进官方群,很多问题都可以第一时间得到大家的帮助。

作者简介:悟空,8年一线互联网开发和架构经验,用故事讲解分布式、架构设计、Java 核心技术。《JVM性能优化实战》专栏作者,开源了《Spring Cloud 实战 PassJava》项目,公众号:悟空聊架构。本文已收录至 www.passjava.cn

本文转载自: 掘金

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

Table of Contents 字符驱动 问题及思考 c

发表于 2021-11-25
  1. 字符驱动
    1. 注册字符设备
      1. 分配设备编号dev_t
      2. 分配注册cdev
    2. 实现简单设备操作函数
    3. 创建设备
    4. 扩展设备操作函数read and write
    5. 编写测试程序读写创建的设备
  2. 问题及思考
    1. linux内核模块和普通用户程序的区别
    2. Makefile各个部分的作用
  3. char_drive源码

字符驱动

注册字符设备

分配设备编号dev_t

在linux中,每一个设备都有一个对应的主设备号和次设备号,linux在内核中使用dev_t持有设备编号,传统上dev_t为32位,12位为主设备号,20位为次设备号,主编号用来标识设备使用的驱动,也可以说是设备类型,次编号用来标识具体是那个设备,使用动态分配函数alloc_chrdev_region可以让内核自动为我们分配一个主设备号,同时在设备停止使用后,应当释放这些设备编号,释放设备编号的工作应该在卸载模块时完成,释放设备编号可以使用unregister_chrdev_region函数,分配和释放的部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
c复制代码// 分配设备主次编号
dev_t dev;
int error = alloc_chrdev_region(&dev, 0, 2, "cdrive");

if (error < 0) {
printk("allocate device number fail");
} else {
dev_number = dev;
}

// 取回分配编号
unregister_chrdev_region(dev_number, 2);

分配注册cdev

内核在内部使用struct cdev 结构来代表字符设备. 在内核调用设备操作前, 必须分配并注册一个或几个这些结构. 为此, 代码应当包含 <linux/cdev.h>,其中定义了这个结构和与之相关的一些函数,为了在运行时获得一个独立的cdev结构,我们可以使用cdev_alloc函数来获取一个cdev结构,并设置该结构对应的设备文件的文件操作函数,这些设备操作函数我们在程序开始就给予了声明。同时应将cdev的owner字段设置为THIS_MODULE,此时我们要对该结构进行初始化并通过cdev_add告知内核有关的信息.在设备使用结束时,应当删除该结构,该部分的代码如下:

1
2
3
4
5
6
7
8
9
c复制代码// 注册并分配cdev设备
my_cdev = cdev_alloc();
my_cdev->ops = &cdev_ops;
my_cdev->owner = THIS_MODULE;
cdev_init(my_cdev, &cdev_ops);
cdev_add(my_cdev, dev, 1);

// 删除注册的设备
cdev_del(my_cdev);

实现简单设备操作函数

在完成一系列分配及初始化工作后,对设备文件对应的文件操作进行实现,这里仅让设备在被打开,关闭,读入,写出时都打印一条内核提示信息。并将文件操作的结构的open,close,read,write函数指针成员设置为我们定义的函数。这里可以使用C99语法。之所以要设定这些函数,是因为内核通过VFS与设备文件进行交互时会使用这些驱动程序设定的I/O函数,如果file_operations结构中对应的函数指针未被初始化,则会被默认设定为NULL,这样做是因为对某一特定类型的设备并非需要支持全部的操作。对于file_operations,我们之前已经在内核关键数据结构的实验中讨论过,这里只再提一下它的owner字段,它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 THIS_MODULE。该部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
C复制代码static int char_open(struct inode *, struct file *);
static int char_release(struct inode *, struct file *);
static ssize_t char_read(struct file *, char *, size_t, loff_t *);
static ssize_t char_write(struct file *, const char *, size_t, loff_t *);

// 初始化file_operations结构
struct file_operations cdev_ops = {.open = char_open,
.release = char_release,
.read = char_read,
.write = char_write,
.owner = THIS_MODULE};

创建设备

完成上述预备工作后,还未拥有一个真实存在的设备文件,需要创建一个设备,按理来说如果cdev是表示一个字符设备的结构的话,已经使用cdev_add向内核添加了有关该结构的信息,此时应该已经可以使用这个设备了,在dev目录下理应有我们注册的设备名,但实际上并非如此。其原因在于,内核并不使用cdev作为一个设备,而是将其作为一个设备接口,使用这个接口我们可以派生出具体的设备,这里需要深究cdev_add到底做了什么,实际上,注册一个cdev到内核只是将它放到cdev_map中,内核中真正用来管理设备的是kobject结构,该结构包含了大量设备必须的信息,kobject结构对应的是真正的设备,而cdev_map可以简单理解为完成从cdev到kobject的映射,因此如果我们想真正使用一个设备,还需要创建设备,可以使用mknod用对应设备号创建一个设备,为了方便,这里直接在模块中用device_create创建对应的设备,创建及销毁设备的部分如下:

1
2
3
4
5
6
7
8
C复制代码// 创建设备文件,并注册到sysfs中
my_device = class_create(THIS_MODULE, "cdrive");
device_create(my_device, NULL, dev, NULL, "my-cdevice");

// 回收设备
device_destroy(my_device, dev_number);
class_unregister(my_device);
class_destroy(my_device);

至此设备已经可以访问了,make生成模块,并insmod插入,可以在/dev目录下看到我们的设备
img
用cat命令访问设备并用dmesg查看设备是否正常响应

img

img

img
可见,设备如预期正常工作

扩展设备操作函数read and write

到这一步,需要对设备操作函数read,write进行扩展,使其能够接收用户给它的输入并且用户可以从该设备获取数据。这时,回顾一下file_operations中read和write的原型。
img
注意到此处参数中包含字符串__user,这种注解是一种文档形式,它告诉我们,这个指针是一个不能被直接解引用的用户空间地址. 对于正常的编译, __user没有效果, 但是它可被外部检查软件使用来找出对用户空间地址的错误使用.既然是用户空间的指针,那么他就不能被内核直接解引用,理由有下

  • 用户空间指针当运行于内核模式可能根本是无效的. 可能没有那个地址的映射, 或者它可能指向一些其他的随机数据.
  • 用户空间内存是分页的, 在做系统调用时这个内存可能没有在 RAM 中. 试图直接引用用户空间内存可能产生一个页面错误, 这是内核代码不允许做的事情. 结果可能是一个“oops”, 导致进行系统调用的进程死亡.
  • 指针由一个用户程序提供, 它可能是错误的或者恶意的. 如果你的驱动盲目地解引用一个用户提供的指针,它提供了一个打开的门路使用户空间程序存取或覆盖系统任何地方的内存.

既然如此,就不能直接使用用户空间的指针,这时还需要能够从用户空间获取信息以完成工作,为安全起见必须使用内核提供的函数来完成这一任务,其中两个常用的读写函数原型如下:
img
使用这两个函数修改设备文件操作的read和wirte函数

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
c复制代码static ssize_t char_read(struct file *file, char *str, size_t size,
loff_t *offset) {
char *data = "I haven't recieve any data!\n";
size_t datalen = strlen(data);
char *data2 = "I have user data!\n";
size_t datalen2 = strlen(data2);

if (!read_num) {
if (size > datalen) {
size = datalen;
}
if (_copy_to_user(str, data, size)) {
return -EFAULT;
}
} else {
if (size > datalen2) {
size = datalen2;
}
if (_copy_to_user(str, data2, size)) {
return -EFAULT;
}
}
printk("device is being read!");
return size;
}

static ssize_t char_write(struct file *file, const char *str, size_t size,
loff_t *offset) {
size_t maxdata = 20, copied;
char buf[maxdata];
if (size < maxdata) {
maxdata = size;
}
copied = _copy_from_user(buf, str, maxdata);
if (copied == 0) {
printk("get %zd bytes from user\n", maxdata);
read_num = 1;
} else {
printk("can't copy %zd bytes from user\n", copied);
}
buf[maxdata] = '\0';
printk("Data from user :%s\n", buf);
return size;
}

编写测试程序读写创建的设备

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
c复制代码#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BUFFER_LENGTH 256 ///< The buffer length (crude but fine)
static char receive[BUFFER_LENGTH]; ///< The receive buffer from the LKM

int main() {
int ret, fd;
char stringToSend[BUFFER_LENGTH];
printf("Starting device test code example...\n");
fd =
open("/dev/my-cdevice", O_RDWR); // Open the device with read/write access
if (fd < 0) {
perror("Failed to open the device...");
return errno;
}
printf("Type in a short string to send to the kernel module:\n");
scanf("%[^\n]%*c", stringToSend); // Read in a string (with spaces)
printf("Writing message to the device [%s].\n", stringToSend);
ret = write(fd, stringToSend,
strlen(stringToSend)); // Send the string to the LKM
if (ret < 0) {
perror("Failed to write the message to the device.");
return errno;
}

printf("Press ENTER to read back from the device...\n");
getchar();

printf("Reading from the device...\n");
ret = read(fd, receive, BUFFER_LENGTH); // Read the response from the LKM
if (ret < 0) {
perror("Failed to read the message from the device.");
return errno;
}
printf("The received message is: [%s]\n", receive);
printf("End of the program\n");
return 0;
}

测试结果:

img

问题及思考

linux内核模块和普通用户程序的区别

linux内核模块和普通用户程序有许多不同,比如最直观的内核模块的入口是init_module,而用户程序的入口一般为main,内核中不能使用C标准库。从系统的角度来说,内核模块工作在内核模式,而用户程序工作在用户模式,即内核在ring0,用户程序在ring3。因为内核模块具有很高的特权级,因此不能直接访问用户空间的数据,以防止恶意用户程序对系统造成损害。用户想与内核交互必须通过系统调用函数来完成,这些系统调用函数是由操作系统定义的,通过特殊的处理方式保证了一般情况下的安全性。

Makefile各个部分的作用

首先设定一个变量为MODULE_NAME,其值为我编写的模块的名字,随后obj-m表示把文件MODULE_NAME.o作为模块进行编译,不会直接编译到内核,但是会生成一个独立的ko文件,all指令下命令意味着先进入到本主机系统下build的文件夹运行make命令,然后返回当前文件夹生成一个模块,clean则是对生成的文件进行清理。
Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
makefile复制代码##
# drive
#
# @file
# @version 0.1
MODULE_NAME :=char_drive
obj-m :=$(MODULE_NAME).o

all:
$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules

clean:
$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean
# end

char_drive源码

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
c复制代码#include <asm-generic/errno-base.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/uaccess.h>
#include <stddef.h>

MODULE_LICENSE("GPL");
static dev_t dev_number;
static struct cdev *my_cdev;
static struct class *my_device;
static int read_num = 0;

static int char_open(struct inode *, struct file *);
static int char_release(struct inode *, struct file *);
static ssize_t char_read(struct file *, char *, size_t, loff_t *);
static ssize_t char_write(struct file *, const char *, size_t, loff_t *);

// 初始化file_operations结构
struct file_operations cdev_ops = {.open = char_open,
.release = char_release,
.read = char_read,
.write = char_write,
.owner = THIS_MODULE};

static int module_init_function(void) {
// 分配设备主次编号
dev_t dev;
int error = alloc_chrdev_region(&dev, 0, 2, "cdrive");

if (error < 0) {
printk("allocate device number fail");
} else {
dev_number = dev;
}
// 注册并分配cdev设备
my_cdev = cdev_alloc();
my_cdev->ops = &cdev_ops;
my_cdev->owner = THIS_MODULE;
cdev_init(my_cdev, &cdev_ops);
cdev_add(my_cdev, dev, 1);
// 创建设备文件,并注册到sysfs中
my_device = class_create(THIS_MODULE, "cdrive");
device_create(my_device, NULL, dev, NULL, "my-cdevice");
return 0;
}

static void module_exit_function(void) {
// 回收设备
device_destroy(my_device, dev_number);
class_unregister(my_device);
class_destroy(my_device);
// 删除注册的设备
cdev_del(my_cdev);
// 取回分配编号
unregister_chrdev_region(dev_number, 2);
}

//实现设备读写等操作
static int char_open(struct inode *inode, struct file *file) {
printk("device is open!");
return 0;
}

static int char_release(struct inode *inode, struct file *file) {
printk("divice is closed!");
return 0;
}

static ssize_t char_read(struct file *file, char *str, size_t size,
loff_t *offset) {
char *data = "I haven't recieve any data!\n";
size_t datalen = strlen(data);
char *data2 = "I have user data!\n";
size_t datalen2 = strlen(data2);

if (!read_num) {
if (size > datalen) {
size = datalen;
}
if (_copy_to_user(str, data, size)) {
return -EFAULT;
}
} else {
if (size > datalen2) {
size = datalen2;
}
if (_copy_to_user(str, data2, size)) {
return -EFAULT;
}
}
printk("device is being read!");
return size;
}

static ssize_t char_write(struct file *file, const char *str, size_t size,
loff_t *offset) {
size_t maxdata = 20, copied;
char buf[maxdata];
if (size < maxdata) {
maxdata = size;
}
copied = _copy_from_user(buf, str, maxdata);
if (copied == 0) {
printk("get %zd bytes from user\n", maxdata);
read_num = 1;
} else {
printk("can't copy %zd bytes from user\n", copied);
}
buf[maxdata] = '\0';
printk("Data from user :%s\n", buf);
return size;
}

module_init(module_init_function);
module_exit(module_exit_function);

本文转载自: 掘金

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

Python实用模块之base64

发表于 2021-11-25

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

环境

  • windows 10 64bits
  • anaconda with python 3.7
  • base64
  • flask 1.1.2

前言

图片处理是 Python 编程中需要掌握的基本技能,而 python 中也内置了相应的库,它就是 base64。本篇就来分享如何利用 base64 库来将图片与字符串进行互相转换。

图片转成字符串

以我网站的 logo 图片为例

base64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码import base64

# 以rb方式读取图片文件,获得原始字节码,b是二进制的意思
with open("logo.jpg", 'rb') as jpg_file:
byte_content = jpg_file.read()

if byte_content:
# 编码成base64字节码
base64_bytes = base64.b64encode(byte_content)

# 转换成字符串
base64_string = base64_bytes.decode('utf8')

print(base64_string)

程序执行结果是这样的

base64

如果需要将字符串通过 json 的方式进行传输的话,就可以结合 json 库一起操作了

字符串转换成图片

这种情况,图片数据一般都是 json 的方式传输,在接收端看到的就是编码后的字符串,拿到字符串后,就可以使用 base64 提供的解码方法解码并保存到本地,为了示例的完整性,这里使用2个外部工具,一个是在线的图片转换工具,网站地址是 www.base64-image.de/,上传一张图片得到 base64 编码后的字符串;另一个工具是 postman,通过它模拟一个客户端的 POST 请求,而在服务器,我们利用 flask 框架实现一个后台服务,处理这个 http 请求

base64

在上图中,图片的数据是从标注的位置开始的,把这串字符拷贝下来,填充到 postman 中,如下所示

base64

这是一个 POST 请求,body 中是一个 json 数据,格式是这样的

1
arduino复制代码{"img": "图片base64编码字符串"}

然后在请求的 Header 部分,加上 Content-Type 字段,它的值为 application/json,这样,客户端的部分就准备好了

base64

接下来,编写服务器端的代码,使用 flask 这个轻量级的 web 框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
python复制代码from flask import Flask, request, jsonify
import base64

app = Flask(__name__)

@app.route('/', methods=['POST'])
def get_image():
# 取出字符串
image_base64_string = request.get_json()['img']
print(image_base64_string)

# 解码字符串
image_data = base64.b64decode(image_base64_string)
with open('test.jpg', "wb") as jpg_file:
jpg_file.write(image_data)

return jsonify(
{
"code": 200
}
)

if __name__ == '__main__':
app.run(port=3000, debug=True)

执行上述代码,启动 flask 服务

base64

然后来到 postman,发送刚才准备好的 http 请求,可以看到,服务器端返回了 json 数据

base64

而此时,服务器端也成功地接收到了图片字符串并解码存储到了本地硬盘

base64

参考资料

  • en.wikipedia.org/wiki/Base64

本文转载自: 掘金

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

Hive(5)--Hive操作语句(1)

发表于 2021-11-25

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

数据库级别语句

展示数据库

语法:

1
sql复制代码show databases;

案例:

1
2
3
4
5
6
sql复制代码show databases;

database_name |
--------------+
default |
hive_databases|

创建数据库

语法:

1
2
3
4
sql复制代码CREATE (DATABASE|SCHEMA) [IF NOT EXISTS] 数据库名称   --DATABASE|SCHEMA 是等价的
[COMMENT 数据库注释] --数据库注释
[LOCATION HDFS要存放的路径 ] --存储在 HDFS 上的位置
[WITH DBPROPERTIES (property_name=property_value, ...)]; --指定额外属性

案例:

1
2
3
sql复制代码  CREATE DATABASE IF NOT EXISTS hive_databases
COMMENT 'hive数据库'
WITH DBPROPERTIES ('create'='jacquesh');

选择数据库

语法:

1
sql复制代码use 数据库名称;

案例:

1
sql复制代码use hive_databases;

删除数据库

语法:

1
2
3
4
sql复制代码
DROP (DATABASE|SCHEMA) [IF EXISTS] 数据库名称 [RESTRICT|CASCADE];

**默认行为是** RESTRICT,**如果数据库中存在表则删除失败。要想删除库及其中的表,可以使用** CASCADE **级联删除**。

案例:

1
sql复制代码DROP DATABASE IF EXISTS hive_databases CASCADE;

显示数据库详情

语法:

1
sql复制代码DESC DATABASE [EXTENDED] 数据库名称;   --EXTENDED 表示是否显示额外属性

案例:

1
2
3
4
sql复制代码DESC DATABASE  EXTENDED hive_databases;
db_name |comment|location |owner_name|owner_type|parameters |
--------------+-------+----------------------------------------------------+----------+----------+-----------------+
hive_databases|hive???|hdfs://cluster/user/hive/warehouse/hive_databases.db|hive2 |USER |{create=jacquesh}|

表级别操作语句

表的创建操作

内部表与外部表

语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sql复制代码CREATE [TEMPORARY] [EXTERNAL] TABLE [IF NOT EXISTS] [数据库.]表名     --表名
[(col_name data_type [COMMENT col_comment],
... [constraint_specification])] --列名 列数据类型
[COMMENT 表描述] --表描述
[PARTITIONED BY (col_name data_type [COMMENT 分区表分区的规则], ...)] --分区表分区规则
[
CLUSTERED BY (col_name, col_name, ...)
[SORTED BY (col_name [ASC|DESC], ...)] INTO num_buckets BUCKETS
] --分桶表分桶规则
[SKEWED BY (col_name, col_name, ...) ON ((col_value, col_value, ...), (col_value, col_value, ...), ...)
[STORED AS DIRECTORIES]
] --指定倾斜列和值
[
[ROW FORMAT row_format]
[STORED AS file_format]
| STORED BY 'storage.handler.class.name' [WITH SERDEPROPERTIES (...)]
] -- 指定行分隔符、存储文件格式或采用自定义存储格式
[LOCATION 表在HDFS的存储位置] -- 指定表的存储位置
[TBLPROPERTIES (property_name=property_value, ...)] --指定表的属性
[AS select_statement]; --从查询结果创建表

内部表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sql复制代码CREATE TABLE temps
(
---------------字段配置开始-------------------
empno INT,
ename STRING,
job STRING,
mgr INT,
hiredate TIMESTAMP,
sal DECIMAL(7,2),
comm DECIMAL(7,2),
deptno INT
---------------字段配置结束-------------------
)
ROW FORMAT DELIMITED
fields terminated by "\t"; **设置字段的分隔符为 “\t”

//创建表之后可以再HDFS的目录中查看到表文件(默认配置的)**

外部表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sql复制代码CREATE  EXTERNAL  TABLE temps                      **外部表的创建需要加上external关键字修饰**
(
---------------字段配置开始-------------------
empno INT,
ename STRING,
job STRING,
mgr INT,
hiredate TIMESTAMP,
sal DECIMAL(7,2),
comm DECIMAL(7,2),
deptno INT
---------------字段配置结束-------------------
)
ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t" **设置字段的分隔符为 “\t”**
LOCATION **'/hive/emp_external'; 配置数据路径 也是的hdfs 的路径**

本文转载自: 掘金

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

分布式系统的架构演进过程(二)

发表于 2021-11-25

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

分布式架构

一般。在分布式架构中,我们会将整体系统拆分成表现层和服务层。服务层的封装是为了具体的。逻辑表现层调用表现层则负责处理业务。前端。和后端的交互任务。

举个例子。通常我们会有电商的系统,比如有电商的商品管理。和用户,以及支付的一些管理系统。我们称作为电商交易系统。从另一个层次看电商交易系统,它也可以有自己的后台管理系统,有自己的数据分析系统,有自己的广告系统,有自己的结算系统,有自己的派单系统。共同组成了一个庞大的电商平台。

这种业务是将代码抽象出来,形成公共的访问服务,提高了代码的复用性。
架构。和服务之间进行性能优化,提高了整体的访问速度。提升了。用户体验

系统之间的调用变得更加复杂,依赖关系也更加集中。

系统的维护成本也更高

SOA架构

在分布式架构下,当部署的服务越来越多时,重复复用的代码会变得越来越多。这时候,我们就需要加入一个统一的资源调度中心,对集群进行实时管理。比如说注册中心。心跳机制或者统一的网关服务。

通常来说,soc系统的话,拥有一个庞大的体系调用。而这个体系调用又分为多个中台性的。模块,比如数据中心或者说业务中心。或者说广告中心。或者说处理的一些中心。或者说数据补偿模块。

这种架构是通过注册中心解决各个服务之间的系统调用。缺点就是存在依赖关系,某个服务故障可能会引起各个服务之间的缓存雪崩。崩塌或者是服务器崩溃。服务之间的依赖关系调用关系复杂增加,增加了维护成本。

本文转载自: 掘金

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

每日python,第二十五篇,Django第二篇

发表于 2021-11-25

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

这里是清安,V:qing_an_an,欢迎一起交流,内设小群一个。博主也是新人一个,所以Django篇,其实也是自己做笔记,复习的一个过程。

本篇带你先了解一下文件中的py文件都有什么用处!

Django 采⽤了 MVT 的软件设计模式,即模型(Model),视图(View)和模板(Template):

1
2
3
4
5
markdown复制代码            M全拼为Model,与MVC中的M功能相同,负责和数据库交互,进⾏数据处理。

V全拼为View,与MVC中的C功能相同,接收请求,进⾏业务处理,返回应答。

T全拼为Template,与MVC中的V功能相同,负责封装构造要返回的html。

了解完这些,我看直接进入正题看看怎么会是吧。

在此之前呢,先启动一下数据库:

如上图所示,qingan文件下还有一个qingan文件,这里干什么用呢:

qingan:只是你项⽬的容器, ⽬录名称对 Django 没有影响,你可 以将它重命名为任何你喜欢的名称。

qingan:⼀个 Python 包。它的名字就是当你引⽤它内部 任何东⻄时需要⽤到的 Python 包名。

init.py:⼀个空⽂件,告诉 Python 这个⽬录应该被认为是⼀ 个 Python 包。⽤于申明。

asgi.py:作为你的项⽬的运⾏在 ASGI 兼容的 Web 服务器上的⼊ ⼝。使⽤ ASGI 来部署的时候使⽤。

settings.py:Django 项⽬的配置⽂件。

urls.py:Django 项⽬的 URL 声明,就像你⽹站的“⽬录”。

wsgi.py:作为你的项⽬的运⾏在 WSGI 兼容的Web服务器上的⼊⼝。 使⽤使⽤ WSGI 进⾏部署的时候使⽤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
markdown复制代码manage.py:⼀个让你⽤各种⽅式管理 Django 项⽬的命令⾏⼯具。

那么创建应用里面的文件有些什么呢?

testqing:⼀个 Python 包。它的名字就是当你引⽤它内部任何东⻄时需要⽤到的 Python 包名。

migrations:⼀个 Python 包。它的名字就是当你引⽤它内部
任何东⻄时需要⽤到的 Python 包名。

migrations中的__init__.py <file>:⼀个空⽂件,告诉 Python 这个⽬录应该被认为是⼀
个 Python 包。⽤于申明。

__init__.py <file>:⼀个空⽂件,告诉 Python 这个⽬录应该被认为是⼀个
Python 包。⽤于申明。
- admin.py <file>:⾃定义 Django 管理⼯具。
- apps.py <file>:Django 应⽤的配置⽂件。
- models.py <file>:模型管理⽂件
- tests.py <file>:测试⽂件
- views.py <file>:视图管理⽂件

注册应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码    这里为什么要注册,不注册就相当于你是一个黑户,虽然看着没什么事情,但是很多地方收限制。

找到qingan中的setting.py文件,将应用名字添加进去:

INSTALLED_APPS = [
'testqing',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]

此处只添加一个应用名字,testqing,其他的不变,别搞混了。这里有一个关系需要注意一下:⼀个项⽬可以包含多个应⽤程 序。⼀个应⽤程序可以在多个项⽬中。

构建应用视图

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
xml复制代码    打开应用中的views.py文件,也就是testqing/views.py。

from django.http import HttpResponse

def html_index(request):
return render(request, "hello.html")


def index(request):
return render(request, "hello1.html")


def hello_word(request):
return HttpResponse('Hello world')

写入三条数据。缺包的记得导入包。这里最简单的视图构建完了。

我们接下来打开qingan/urls.py文件。写入数据:

from testqing.views import index
from testqing.views import hello_word
from testqing.views import html_index

urlpatterns += [
path("index/",index),
path("hello/",hello_word),
path("qingqing/",html_index)
]

这里就是路由文件了。为项目添加一个可以访问的地址:index/;并返回某个视图的内容。

注意:此处会有红线提示路径错误,这里可以不用管,解决该问题很简单,退出pycharm找到你的文件地址,直接打开qingan文件而不是EVN_Django文件。

什么是路由:路由就是URL到函数的映射。当你访问qingqing/时,浏览器中就会 显示上述hello_word函数方法中的Hello world内容。

这里我们还创建了两个HTML文件。记住了:testqing/templates,这个文件名称是固定的,不可变的。

# hello代码

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Hello world</h1>>

</body>
</html>

# hello1代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Hello world</h1>>
<a href="http://localhost:8000/hello">click</a>

</body>
</html>

这里注意的是别把我的注释带到HTML文件中去了,这里做的是有hello1文件跳转到hello文件的一个小测试功能。

接下来就是运行,看结果的时候了:运行代码步骤:

# 创建文件
django-admin startproject qingan
# 初始化数据库
manage.py migrate/python manage.py migrate
# 创建应用
manage.py startapp testapp/python manage.py startapp testapp
# 注册应用文中有讲
# 写视图、写路由文件
# 运行代码
manage.py runserver/python manage.py runserver
# 打开网址http://127.0.0.1:8080
# 打开网址后接路由文件中的路径如:http://127.0.0.1:8080/qingqing

在这里你会看到默认访问的可能是一个超级管理员登录界面,所以加路径就是了,加了就能看到效果了。这里就不展示效果图了。各位自行操作,不懂的可以扣我:qing_an_an。

本文转载自: 掘金

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

若依系统分页工具学习-PageHelper篇十三

发表于 2021-11-25

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

在昨天的文章中,我们介绍了PageHelper中的cache包以及简单介绍了包中各个类的属性与方法;还介绍了Java中一种加载类的方式:Class.forName,并且通过查看com.mysql.jdbc.Driver代码,我们知道,可以通过这种方式可以执行类中的静态代码段。

昨天的问题

我们先来看一下昨天最后留的问题:

com.google.common.cache.Cache是什么样的呢?

我们先来看一下com.google.common.cache.Cache的源代码:

1
2
3
4
5
java复制代码package com.google.common.cache;
public abstract interface Cache<K,V> {
public abstract V getIfPresent(java.lang.Object arg0);
// 此处省略其他方法代码
}

我们发现com.google.common.cache.Cache实际上是一个接口,并且其中也并没有任务的静态static代码段需要执行,其中的方法也都是抽象abstract修饰的。

反过来我们再去看PageHelper中的那句代码:

1
java复制代码Class.forName("com.google.common.cache.Cache");

就成了单纯检测执行环境中是否能够加载com.google.common.cache.Cache类。

CacheFactory指定类名的方式

我们来看看CacheFactory中createCache是如何通过参数sqlCacheClass指定类名生成指定对象的。

当指定sqlCacheClass时,将执行以下代码:

1
2
3
4
5
6
7
8
9
10
11
java复制代码try {
Class<? extends Cache> clazz = (Class<? extends Cache>) Class.forName(sqlCacheClass);
try {
Constructor<? extends Cache> constructor = clazz.getConstructor(Properties.class, String.class);
return constructor.newInstance(properties, prefix);
} catch (Exception e) {
return clazz.newInstance();
}
} catch (Throwable t) {
throw new PageException("Created Sql Cache [" + sqlCacheClass + "] Error", t);
}

第一行结合我们之前对Class.forName的解析很好理解,生成指定的类。

第二行:

1
java复制代码Constructor<? extends Cache> constructor = clazz.getConstructor(Properties.class, String.class);

从字面意思我们判断,首先是通过第一句获得指定类,然后通过getConstructor获取类的构造器。

第三行:

1
java复制代码return constructor.newInstance(properties, prefix);

通过构造器生成指定类的一个对象,并且通过上面构造器后面的<? Extends Cache>,我们知道,实现的缓存类需要是com.github.pagehelper.cache.Cache接口的一个实现。

当然,在PageHelper中就有两个实现GuavaCache以及SimpleCache。

并且通过查看SimpleCache的代码,我们发现其中的构造器通过cacheBuilder一步一步的实现了成员变量CACHE的初始化,一步一步的判断是否设置了配置文件xxx.typeClass,.evictionClass,xxx..flushInterval,.size等等。

而GuavaCache同样也是使用cacheBuilder去逐步构造对象的。

这种构造数据的方式曾在《Effective Java》中提到过,非常便利,值得学习!

本文转载自: 掘金

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

1…187188189…956

开发者博客

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