Chemmy's Blog

chengming0916@outlook.com

DotNetty完全教程(四)

Excerpt

ByteBufferNetty中ByteBuffer的介绍Netty 的数据处理 API 通过两个组件暴露——abstract class ByteBuf 和 interfaceByteBufHolderDotNetty中有AbstractByteBuffer IByteBuffer IByteBufferHolder优点:它可以被用户自定义的缓冲区类型扩展;通过内置的复合缓冲区…


ByteBuffer

Netty中ByteBuffer的介绍

Netty 的数据处理 API 通过两个组件暴露——abstract class ByteBuf 和 interface
ByteBufHolder

DotNetty中有AbstractByteBuffer IByteBuffer IByteBufferHolder

优点:

  • 它可以被用户自定义的缓冲区类型扩展;
  • 通过内置的复合缓冲区类型实现了透明的零拷贝;
  • 容量可以按需增长(类似于 JDK 的 StringBuilder);
  • 在读和写这两种模式之间切换不需要调用 ByteBuffer 的 flip()方法;
  • 读和写使用了不同的索引;
  • 支持方法的链式调用;
  • 支持引用计数;
  • 支持池化

原理:

每一个ByteBuf都有两个索引,读索引和写索引,read和write会移动索引,set和get不会引动索引。

使用ByteBuf

  1. 堆缓冲区(使用数组的方式展示和操作数据)

使用支撑数组给ByteBuf提供快速的分配和释放的能力。适用于有遗留数据需要处理的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public override void ChannelRead(IChannelHandlerContext ctx, object msg)
{
IByteBuffer message = msg as IByteBuffer;
// 检查是否有支撑数组
if (message.HasArray)
{
// 获取数组
byte[] array = message.Array;
// 计算第一个字节的偏移
int offset = message.ArrayOffset + message.ReaderIndex;
// 获取可读字节数
int length = message.ReadableBytes;
// 调用方法,处理数据
HandleArray(array, offset, length);
}
Console.WriteLine("收到信息:" + message.ToString(Encoding.UTF8));
ctx.WriteAsync(message);
}
  1. 直接缓冲区
1
2
3
4
5
6
7
8
9
10
11
12
13
public override void ChannelRead(IChannelHandlerContext ctx, object msg)
{
IByteBuffer message = msg as IByteBuffer;
if (message.HasArray)
{
int length = message.ReadableBytes;
byte[] array = new byte[length];
message.GetBytes(message.ReaderIndex, array);
HandleArray(array, 0, length);
}
Console.WriteLine("收到信息:" + message.ToString(Encoding.UTF8));
ctx.WriteAsync(message);
}
  1. CompositeByteBuffer 复合缓冲区

如果要发送的命令是由两个ByteBuf拼接构成的,那么就需要复合缓冲区,比如Http协议中一个数据流由头跟内容构成这样的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public override void ChannelRead(IChannelHandlerContext ctx, object msg)
{
IByteBuffer message = msg as IByteBuffer;
// 创建一个复合缓冲区
CompositeByteBuffer messageBuf = Unpooled.CompositeBuffer();
// 创建两个ByteBuffer
IByteBuffer headBuf = Unpooled.CopiedBuffer(message);
IByteBuffer bodyBuf = Unpooled.CopiedBuffer(message);
// 添加到符合缓冲区中
messageBuf.AddComponents(headBuf, bodyBuf);
// 删除
messageBuf.RemoveComponent(0);

Console.WriteLine("收到信息:" + message.ToString(Encoding.UTF8));
ctx.WriteAsync(message);
}

字节级操作

  1. 读取(不移动索引)
1
2
3
4
5
6
7
8
9
10
11
12
13
public override void ChannelRead(IChannelHandlerContext ctx, object msg)
{
IByteBuffer message = msg as IByteBuffer;
for (int i = 0; i < message.Capacity; i++)
{
// 如此使用索引访问不会改变读索引也不会改变写索引
byte b = message.GetByte(i);
Console.WriteLine(b);
}

Console.WriteLine("收到信息:" + message.ToString(Encoding.UTF8));
ctx.WriteAsync(message);
}
  1. 丢弃可丢弃字节
    所谓可丢弃字节就是调用read方法之后,readindex已经移动过了的区域,这段区域的字节称为可丢弃字节。
1
message.DiscardReadBytes();

只有在内存十分宝贵需要清理的时候再调用这个方法,随便调用有可能会造成内存的复制,降低效率。
3. 读取所有可读字节(移动读索引)

1
2
3
4
while (message.IsReadable())
{
Console.WriteLine(message.ReadByte());
}
  1. 写入数据
1
2
3
4
5
// 使用随机数填充可写区域
while (message.WritableBytes > 4)
{
message.WriteInt(new Random().Next(0, 100));
}
  1. 管理索引
  • MarkReaderIndex ResetReaderIndex 标记和恢复读索引
  • MarkWriterIndex ResetWriterIndex 标记和恢复写索引
  • SetReaderIndex(int) SetWriterIndex(int) 直接移动索引
  • clear() 重置两个索引都为0,但是不会清除内容
  1. 查找
  • IndexOf()
  • 使用Processor
1
2
// 查找\r
message.ForEachByte(ByteProcessor.FindCR);
  1. 派生

派生的意思是创建一个新的ByteBuffer,这个ByteBuf派生于其他的ByteBuf,派生出来的子ByteBuf具有自己的读写索引,但是本质上指向同一个对象,这样就导致了改变一个,另一个也会改变。

  • duplicate();
  • slice();
  • slice(int, int);
  • Unpooled.unmodifiableBuffer(…);
  • order(ByteOrder);
  • readSlice(int)。
  1. 复制
    复制不同于派生,会复制出一个独立的ByteBuf,修改其中一个不会改变另一个。
  • copy
  1. 释放
1
2
// 显式丢弃消息
ReferenceCountUtil.release(msg);
  1. 增加引用计数防止释放
1
ReferenceCountUtil.retain(message)
  1. 其他api
    在这里插入图片描述

ByteBufHolder

  1. 目的
    再数据处理的过程中不仅仅有字节数据内容本身,还会有一些附加信息,比如HTTP响应的状态码,Cookie等。给ByteBuf附加信息就要用到ByteBufHolder.
  2. API

管理ByteBuffer

  1. 按需分配 ByteBufAllocator

    注意分配是池化的,最大程度上降低分配和释放内存的开销。

    1
    2
    3
    4
    5
    6
    7
    // 获取Allocator
    // 1
    IChannelHandlerContext ctx = null;
    IByteBufferAllocator allocator = ctx.Allocator;
    // 2
    IChannel channel = null;
    allocator = channel.Allocator;

有两种ByteBufAllocator的实现:PooledByteBufAllocator和UnpooledByteBufAllocator,前者池化了ByteBuf的实例,极大限度的提升了性能减少了内存碎片。
2. Unpooled缓冲区
获取不到 ByteBufAllocator的引用的时候我们可以使用Unpooled工具类来操作ByteBuf。
在这里插入图片描述

  1. ByteBufUtil
    这个类提供了一些通用的API,都是静态的辅助方法,例如hexdump方法可以以十六进制的方式打印ByteBuf的内容。还有equal方法判断bytebuf是否相等。

引用计数

  1. 目的

    ByteBuf和ByteBufHolder都有计数的机制。引用计数都从1开始,如果计数大于0则不被释放,如果等于0就会被释放。它的目的是为了支持池化的实现,降低了内存分配的开销。

  2. 异常

    如果访问一个计数为0的对象就会引发IllegalReferenceCountException。

DotNetty系列三:编码解码器,IdleStateHandler心跳机制

Excerpt

在上一节基础上,实现编码解码器。1.创建一个类库项目。用于实现编码解码器。编码器: public class CommonServerEncoder : MessageToByteEncoder<string> { protected override void Encode(IChannelHandlerContext context, s…


在上一节基础上,实现编码解码器。

1.创建一个类库项目。用于实现编码解码器。

编码器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CommonServerEncoder : MessageToByteEncoder<string>    
{
protected override void Encode(IChannelHandlerContext context, string message, IByteBuffer output)
{
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
IByteBuffer initialMessage = Unpooled.Buffer(messageBytes.Length);
initialMessage.WriteBytes(messageBytes);
output.WriteBytes(initialMessage);
}
}

public class CommonClientEncoder : MessageToByteEncoder<string>
{
protected override void Encode(IChannelHandlerContext context, string message, IByteBuffer output)
{
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
IByteBuffer initialMessage = Unpooled.Buffer(messageBytes.Length);
initialMessage.WriteBytes(messageBytes);
output.WriteBytes(initialMessage);
}
}

解码器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CommonServerDecoder : ByteToMessageDecoder    
{
protected override void Decode(IChannelHandlerContext context, IByteBuffer input, List<object> output)
{
byte[] array = new byte[input.ReadableBytes];
input.GetBytes(input.ReaderIndex, array, 0, input.ReadableBytes);
input.Clear();
output.Add(array);
}
}

public class CommonClientDecoder : ByteToMessageDecoder
{
protected override void Decode(IChannelHandlerContext context, IByteBuffer input, List<object> output)
{
byte[] array = new byte[input.ReadableBytes];
input.GetBytes(input.ReaderIndex, array, 0, input.ReadableBytes);
input.Clear();
output.Add(array);
}
}

2.服务端里添加:

                        //配置编码解码器
                        pipeline.AddLast(new CommonServerEncoder());
                        pipeline.AddLast(new CommonServerDecoder());

客户端里添加:

                        //配置编码解码器
                        pipeline.AddLast(new CommonClientEncoder());
                        pipeline.AddLast(new CommonClientDecoder());

3.服务端接收和发送:

1
2
3
4
5
6
7
8
9
public override void ChannelRead(IChannelHandlerContext context, object message)        
{
if (message is byte[] o)
{
Console.WriteLine($"解码器方式,从客户端接收:{Encoding.UTF8.GetString(o)}:{DateTime.Now}");
}
string msg = "服务端从客户端接收到内容后返回,我是服务端";
context.WriteAsync(msg);
}

客户端接收和发送:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public override void ChannelActive(IChannelHandlerContext context)        
{
Console.WriteLine("我是客户端.");
Console.WriteLine($"连接至服务端{context}.");
string message = "客户端1";
context.WriteAndFlushAsync(message);
}

public override void ChannelRead(IChannelHandlerContext context, object message)
{
if (message is byte[] o)
{
Console.WriteLine($"解码器方式,从服务端接收:{Encoding.UTF8.GetString(o)}:{DateTime.Now}");
}
}

实现了上一节一样的效果。

4.IdleStateHandler心跳机制:

4.1服务端添加IdleStateHandler心跳检测处理器,添加自定义处理Handler类实现userEventTriggered()方法作为超时事件的逻辑处理.

IdleStateHandler心跳检测每十五秒进行一次读检测,如果十五秒内ChannelRead()方法未被调用则触发一次userEventTrigger()方法.

                        // IdleStateHandler 心跳
                        //服务端为读IDLE
                        pipeline.AddLast(new IdleStateHandler(15, 0, 0));//第一个参数为读,第二个为写,第三个为读写全部

4.2服务端Handler重载UserEventTriggered:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private int lossConnectCount = 0;
public override void UserEventTriggered(IChannelHandlerContext context, object evt) {
Console.WriteLine("已经15秒未收到客户端的消息了!");
if (evt is IdleStateEvent eventState)
{
if (eventState.State == IdleState.ReaderIdle)
{
lossConnectCount++;if (lossConnectCount > 2)
{
Console.WriteLine("关闭这个不活跃通道!");
context.CloseAsync();
} }
}
else
{
base.UserEventTriggered(context, evt);
}
}

接收部分改为判断心跳:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public override void ChannelRead(IChannelHandlerContext context, object message)        
{
if (message is byte[] o)
{
Console.WriteLine($"解码器方式,从客户端接收:{Encoding.UTF8.GetString(o)}:{DateTime.Now}");
if (Encoding.UTF8.GetString(o).Contains("biubiu:"))
{
string temp = "服务端接收到心跳连接";
context.WriteAsync(temp);return;
}
}
string msg = "服务端从客户端接收到内容后返回,我是服务端";
context.WriteAsync(msg);
}

4.3客户端添加IdleStateHandler心跳检测处理器,并添加自定义处理Handler类实现userEventTriggered()方法作为超时事件的逻辑处理;

设定IdleStateHandler心跳检测每十秒进行一次写检测,如果十秒内write()方法未被调用则触发一次userEventTrigger()方法,实现客户端每十秒向服务端发送一次消息;

                        // IdleStateHandler 心跳
                        //客户端为写IDLE
                        pipeline.AddLast(new IdleStateHandler(0, 10, 0));//第一个参数为读,第二个为写,第三个为读写全部

4.4客户端Handler重载UserEventTriggered:

1
2
3
4
5
6
7
8
9
10
public override void UserEventTriggered(IChannelHandlerContext context, object evt)        {           
Console.WriteLine("客户端循环心跳监测发送: " + DateTime.Now);
if (evt is IdleStateEvent eventState)
{
if (eventState.State == IdleState.WriterIdle)
{
context.WriteAndFlushAsync($"biubiu:{DateTime.Now}");
}
}
}

4.5实现效果:

5.群发:将客户端上下线通知,群发至所有客户端。只在服务端修改

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
static volatile IChannelGroup groups;
public override void HandlerAdded(IChannelHandlerContext context)
{
Console.WriteLine($"客户端{context}上线.");

base.HandlerAdded(context);
IChannelGroup g = groups;if (g == null)
{
lock (this)
{
if (groups == null)
{
g = groups = new DefaultChannelGroup(context.Executor);
}
}
}
g.Add(context.Channel);
groups.WriteAndFlushAsync($"欢迎{context.Channel.RemoteAddress}加入.");
}

public override void HandlerRemoved(IChannelHandlerContext context)
{
Console.WriteLine($"客户端{context}下线.");
base.HandlerRemoved(context);
groups.Remove(context.Channel);
groups.WriteAndFlushAsync($"恭送{context.Channel.RemoteAddress}离开.");
}

实现效果:

项目下载地址:项目下载

原文参考:CentOS 8 都发布了,你还不会用 nftables?

一、引言:为什么是 nftables?

随着 CentOS 8 和 RHEL 8 的发布,一个重要的变化是:**nftables 框架已取代 iptables 成为默认的网络包过滤工具**。如果你还在使用 iptables,现在是时候了解它的继任者了。

nftables 是什么?
它是 netfilter 项目推出的新框架,旨在全面替代现有的 {ip,ip6,arp,eb}tables。它提供了一个全新的包过滤框架、用户空间工具 nft 以及向后兼容层。

核心优势对比 iptables:

  • 数据结构:iptables 使用数组式布局,nftables 使用链式布局(链表),更灵活。
  • 可扩展性:iptables 功能扩展需修改内核;nftables 大部分工作在用户态完成,添加功能更容易。
  • 性能:iptables 存在内置链和计数器,有空链开销;nftables 无内置链,按需创建。
  • 易用性:原生支持集合、字典和映射,简化了 IPv4/IPv6 双栈管理。

本文主要介绍用户空间命令行工具 nft 的使用方法。

二、基础概念与快速开始

2.1 核心组件

nftables 由三个核心层级构成:

  1. 内核:提供 netlink 配置接口和规则集评估。
  2. libnl:提供与内核通信的基础库。
  3. **nft (用户空间)**:供用户交互的命令行工具。

2.2 规则结构

与 iptables 类似,nftables 也由 表(Table) -> 链(Chain) -> 规则(Rule) 层级构成。规则是真正的执行动作。

检查当前规则集:

1
nft list ruleset

初始状态下,如果没有启用 firewalld 等服务,输出为空,这印证了 nftables 没有内置链的特性。

三、实战操作:从创建到管理

3.1 创建表(Table)

表是规则的顶级容器,每个表只属于一个特定的“地址簇”。

nftables 地址簇 对应的传统工具 说明
ip iptables 仅处理 IPv4 数据包
ip6 ip6tables 仅处理 IPv6 数据包
inet iptables & ip6tables 处理 IPv4 和 IPv6(推荐)
arp arptables 处理 ARP 层数据包
bridge ebtables 处理桥接数据包

创建表示例(使用 inet 簇):

1
nft add table inet my_table

列出所有规则以验证:

1
2
3
4
nft list ruleset
# 输出:
# table inet my_table {
# }

3.2 创建链(Chain)

链是规则的载体,必须显式创建。链有两种类型:

  • 常规链:用于规则分类和跳转,无需指定钩子。
  • 基本链:数据包的入口点,必须指定钩子类型和优先级。

创建常规链:

1
nft add chain inet my_table my_utility_chain

创建基本链(作为数据包入口):

1
nft add chain inet my_table my_filter_chain '{ type filter hook input priority 0; }'
  • type filter:链类型为过滤。
  • hook input:钩子点为输入。
  • priority 0:优先级(数值越小,优先级越高)。
  • 注意:分号 ; 需要用反斜杠 \ 转义,或直接用引号包裹整个参数。

3.3 创建规则(Rule)

规则由语句构成,放置在链中。

在链末尾添加规则(允许 SSH):

1
nft add rule inet my_table my_filter_chain tcp dport ssh accept

在链开头插入规则(允许 HTTP):

1
nft insert rule inet my_table my_filter_chain tcp dport http accept

在指定位置插入规则:
有两种方式,推荐使用稳定的 handle 而非易变的 index

  1. 使用 index(从0开始):
    1
    2
    # 在索引1的规则前插入(即第二条规则前)
    nft insert rule inet my_table my_filter_chain index 1 tcp dport nfs accept
  2. 使用 handle(句柄值唯一且不变):
    1
    2
    3
    4
    5
    # 首先查看规则的句柄
    nft --handle list ruleset
    # 假设 `tcp dport http accept` 的句柄是 4
    # 在该句柄的规则后添加新规则
    nft add rule inet my_table my_filter_chain handle 4 tcp dport 2345 accept
    创建规则时获取句柄:
    1
    2
    nft --echo --handle add rule inet my_table my_filter_chain udp dport 3333 accept
    # 输出会包含新规则的句柄,例如 `# handle 10`

3.4 删除规则

规则只能通过其唯一的 handle 删除。

1
2
3
4
# 1. 查找要删除规则的句柄
nft --handle list ruleset
# 2. 使用句柄删除
nft delete rule inet my_table my_filter_chain handle 8

3.5 查看规则

支持不同粒度的查看。

1
2
3
4
5
6
# 查看所有规则
nft list ruleset
# 查看特定表
nft list table inet my_table
# 查看特定链
nft list chain inet my_table my_filter_chain

四、高级特性:集合与字典

4.1 集合(Sets)

集合用于匹配多个条件(如多个IP、端口),分为匿名集合和命名集合。

匿名集合(适用于固定条件):

1
2
3
4
# 允许来自两个特定IP的流量
nft add rule inet my_table my_filter_chain ip saddr { 10.10.10.123, 10.10.10.231 } accept
# 简化多个端口的规则
nft add rule inet my_table my_filter_chain tcp dport { http, nfs, ssh } accept

命名集合(可动态修改):

  1. 创建集合(指定元素类型):
    1
    nft add set inet my_table my_set '{ type ipv4_addr; }'
  2. 在规则中引用集合(使用 @ 符号):
    1
    nft add rule inet my_table my_filter_chain ip saddr @my_set drop
  3. 向集合添加/删除元素
    1
    2
    nft add element inet my_table my_set { 10.10.10.22, 10.10.10.33 }
    nft delete element inet my_table my_set { 10.10.10.33 }

支持区间的集合(需添加 interval 标志):

1
2
3
4
nft add set inet my_table my_range_set '{ type ipv4_addr; flags interval; }'
nft add element inet my_table my_range_set { 10.20.20.0/24 }
# 或直接使用区间
# nft add element inet my_table my_range_set { 10.20.20.0-10.20.20.255 }

级联类型集合(组合匹配):

1
2
3
4
5
6
# 创建可同时匹配IP、协议、端口的集合
nft add set inet my_table my_concat_set '{ type ipv4_addr . inet_proto . inet_service; }'
# 添加元素
nft add element inet my_table my_concat_set { 10.30.30.30 . tcp . telnet }
# 在规则中引用,需指明对应关系
nft add rule inet my_table my_filter_chain ip saddr . meta l4proto . tcp dport @my_concat_set accept

4.2 字典(Maps)

字典提供高效的键值映射,用于根据条件跳转到不同链或执行不同动作,避免链式跳转的性能开销。

创建字典并用于规则跳转:

1
2
3
4
5
# 1. 创建目标链
nft add chain inet my_table my_tcp_chain
nft add chain inet my_table my_udp_chain
# 2. 使用匿名字典根据协议跳转
nft add rule inet my_table my_filter_chain meta l4proto vmap { tcp : jump my_tcp_chain, udp : jump my_udp_chain }

命名字典:

1
2
3
4
5
6
# 1. 创建字典(键类型: 值类型)
nft add map inet my_table my_vmap '{ type ipv4_addr : verdict; }'
# 2. 添加映射关系
nft add element inet my_table my_vmap { 192.168.0.10 : drop, 192.168.0.11 : accept }
# 3. 在规则中引用字典
nft add rule inet my_table my_filter_chain ip saddr vmap @my_vmap

五、架构设计与命名空间

nftables 中,每个表都是一个独立的命名空间。这意味着不同表中的链、集合可以同名,互不干扰。

1
2
3
4
nft add table inet table_one
nft add chain inet table_one my_chain
nft add table inet table_two
nft add chain inet table_two my_chain # 同名,但允许

重要提示:数据包必须通过所有表中相关链的允许,才能被放行。链的执行顺序由 priority 决定,值越小优先级越高。

六、持久化配置

命令行配置是临时的。要使规则永久生效,需进行备份和恢复。

备份当前规则集:

1
nft list ruleset > /root/nftables.conf

从文件恢复规则集:

1
nft -f /root/nftables.conf

在 CentOS 8/RHEL 8 中:

  • 系统服务 nftables.service 默认从 /etc/nftables.conf 加载规则。
  • 该文件通常会 include 其他配置文件,如 /etc/sysconfig/nftables.conf(默认被注释)。

七、总结

nftables 作为 iptables 的现代替代品,通过更清晰的数据结构、原生集合/字典支持、独立的命名空间等特性,提供了更强大、灵活且高效的防火墙配置体验。

学习路径建议:

  1. 理解 表 -> 链 -> 规则 的核心层级。
  2. 掌握基础的 增(add/insert)、删(delete)、查(list) 操作。
  3. 熟练使用 集合 来简化多条件规则。
  4. 在复杂策略中应用 字典 进行高效跳转。
  5. 最后通过 备份/恢复 实现配置持久化。

问题描述

需要使用MSBuild将Tools文件夹及其所有子文件夹和文件递归复制到应用程序的输出目录(如Debug文件夹)中。

目录结构示例:

1
2
3
4
5
{ProjectName}
├── Source
└── Tools
└── Viewer
└── {约5个子目录}

解决方案

方案一:使用Copy任务(推荐)

通过RecursiveDir元数据保持目录结构:

1
2
3
4
5
6
7
8
<Target Name="AfterBuild">
<ItemGroup>
<Viewer Include="..\$(ApplicationDirectory)\Tools\viewer\**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(Viewer)"
DestinationFolder="$(TargetDir)\%(RecursiveDir)"
SkipUnchangedFiles="true" />
</Target>

关键点

  • 使用%(RecursiveDir)保留源目录结构
  • SkipUnchangedFiles="true"避免重复复制
  • 建议在AfterBuild阶段执行(确保构建完成)

方案二:完整项目配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
<PropertyGroup>
<YourDestinationDirectory>..\SomeDestinationDirectory</YourDestinationDirectory>
<YourSourceDirectory>..\SomeSourceDirectory</YourSourceDirectory>
</PropertyGroup>

<Target Name="BeforeBuild">
<CreateItem Include="$(YourSourceDirectory)\**\*.*">
<Output TaskParameter="Include" ItemName="YourFilesToCopy" />
</CreateItem>

<Copy SourceFiles="@(YourFilesToCopy)"
DestinationFiles="@(YourFilesToCopy->'$(YourDestinationDirectory)\%(RecursiveDir)%(Filename)%(Extension)')" />
</Target>
</Project>

方案三:使用Exec调用xcopy(简单场景)

1
2
3
4
<Target Name="CopyToDeployFolder" DependsOnTargets="CompileWebSite">
<Exec Command="xcopy.exe $(OutputDirectory) $(DeploymentDirectory) /e"
WorkingDirectory="C:\Windows\" />
</Target>

注意:此方法依赖系统命令,跨平台兼容性差


常见错误排查

1. 路径问题

  • 确保$(ApplicationDirectory)$(OutputPath)已正确定义
  • 验证源路径是否存在:..\$(ApplicationDirectory)\Tools\viewer\

2. 目标路径格式

错误写法:

1
DestinationFolder="@(Viewer->'$(OutputPath)\\Tools')"

正确写法应使用RecursiveDir

1
DestinationFolder="$(TargetDir)\%(RecursiveDir)"

3. 文件匹配模式

  • 使用\**\*.*递归匹配所有文件
  • 确保包含隐藏文件(如需要):\**\*

最佳实践

  1. 使用标准属性

    • $(TargetDir) 替代 $(OutputPath)
    • $(ProjectDir) 获取项目根目录
  2. 增量构建优化

    1
    2
    3
    <Copy SourceFiles="@(Files)" 
    DestinationFiles="@(Files->'$(Dest)\%(RecursiveDir)%(Filename)%(Extension)')"
    SkipUnchangedFiles="true" />
  3. 错误处理

    1
    <Copy ... ContinueOnError="WarnAndContinue" />

官方参考

Linux常用命令

Linux的命令确实非常多,然而熟悉Linux的人从来不会因为Linux的命令太多而烦恼。因为我们仅仅只需要掌握常用命令,就完全可以驾驭Linux。

接下来,让我们一起来看看都有那些常用的Linux命令吧!

一、文件目录操作

1. ls命令

ls命令不仅可以查看Linux文件夹包含的文件,而且可以查看文件权限(包括目录、文件夹、文件权限)和目录信息等。

命令格式

1
ls [选项] [目录]

常用参数

  • -l:列出长数据串,包含文件的属性与权限数据等
  • -a:列出全部的文件,连同隐藏文件(开头为.的文件)一起列出来(常用)
  • -d:仅列出目录本身,而不是列出目录的文件数据
  • -h:将文件容量以较易读的方式(GB,kB等)列出来
  • -R:连同子目录的内容一起列出(递归列出),等于该目录下的所有文件都会显示出来

使用实例

  1. 列出home目录下的所有文件和目录的详细资料

    1
    2
    ls -a -l /home
    ls -al /home
  2. 列出当前目录下所有以”d”开头的文件目录详情内容

    1
    ls -l d*

2. cd命令

最基本的命令语句,其他命令语句要进行操作,都是建立在使用cd命令上的。用于切换当前目录至指定目录。

命令格式

1
cd [目录名]

操作案例

  1. 从当前目录进入系统根目录

    1
    cd /
  2. 跳转到home/Code目录

    1
    cd /home/Code

3. pwd命令

查看”当前工作目录”的完整路径。

命令格式

1
pwd [选项]

常用参数

  • -P:显示实际物理路径,而非使用连接(link)路径
  • -L:当目录为连接路径时,显示连接路径

操作案例

  1. 显示当前所在路径
    1
    pwd

4. mkdir命令

用来创建指定名称的目录,要求创建目录的用户在当前目录中具有写权限,并且指定的目录名不能是当前目录中已有的目录。

命令格式

1
mkdir [选项] 目录

常用参数

  • -m, --mode=模式:设定权限<模式>(类似chmod),而不是rwxrwxrwx减umask
  • -p, --parents:可以是一个路径名称。此时若路径中的某些目录尚不存在,加上此选项后,系统将自动建立好那些尚不存在的目录,即一次可以建立多个目录
  • -v, --verbose:每次创建新目录都显示信息
  • --help:显示此帮助信息并退出
  • --version:输出版本信息并退出

使用实例

  1. 创建一个空目录

    1
    mkdir test
  2. 递归创建多个目录

    1
    mkdir -p test/test1
  3. 创建权限为777的目录

    1
    mkdir -m 777 test2
  4. 创建目录都显示信息

    1
    mkdir -v test4

5. rm命令

删除一个目录中的一个或多个文件或目录,如果没有使用-r选项,则rm不会删除目录。如果使用rm来删除文件,通常仍可以将该文件恢复原状。

命令格式

1
rm [选项] 文件

常用参数

  • -f, --force:忽略不存在的文件,从不给出提示
  • -i, --interactive:进行交互式删除
  • -r, -R, --recursive:指示rm将参数中列出的全部目录和子目录均递归地删除
  • -v, --verbose:详细显示进行的步骤
  • --help:显示此帮助信息并退出
  • --version:输出版本信息并退出

使用实例

  1. 删除文件test.txt,系统会提示是否删除

    1
    rm test.txt
  2. 强制删除test.txt,系统不再提示

    1
    rm -f test.txt
  3. 将test子目录及目录中所有档案删除

    1
    rm -r test

6. rmdir命令

该命令从一个目录中删除一个或多个子目录项,删除某目录时也必须具有对父目录的写权限。

命令格式

1
rmdir [选项] 目录

常用参数

  • -p:递归删除目录dirname,当子目录删除后其父目录为空时,也一同被删除。如果整个路径被删除或者由于某种原因保留部分路径,则系统在标准输出上显示相应的信息
  • -v, --verbose:显示指令执行过程

使用实例

  1. 删除空目录test1,非空目录无法删除

    1
    rmdir test1
  2. 当子目录被删除后使它也成为空目录的话,则顺便一并删除

    1
    rmdir -p test2  # test目录下仅有test2

7. mv命令

可以用来移动文件或者将文件改名(move (rename) files)。当第二个参数类型是文件时,mv命令完成文件重命名。当第二个参数是已存在的目录名称时,源文件或目录参数可以有多个,mv命令将各参数指定的源文件均移至目标目录中。

命令格式

1
mv [选项] 源文件或目录 目标文件或目录

常用参数

  • -b:若需覆盖文件,则覆盖前先行备份
  • -f:force强制的意思,如果目标文件已经存在,不会询问而直接覆盖
  • -i:若目标文件(destination)已经存在时,就会询问是否覆盖
  • -u:若目标文件已经存在,且source比较新,才会更新(update)
  • -t--target-directory=DIRECTORY move all SOURCE arguments into DIRECTORY,即指定mv的目标目录,该选项适用于移动多个源文件到一个目录的情况,此时目标目录在前,源文件在后

使用实例

  1. 将test1.txt重命名为test2.txt

    1
    mv test1.txt test2.txt
  2. 移动文件test1.txt到目录test2

    1
    mv test1.txt test2
  3. 将文件test1.txt、test2.txt、test3.txt移动到目录test3

    1
    mv test1.txt test2.txt test3.txt test3

8. cp命令

将源文件复制至目标文件,或将多个源文件复制至目标目录。

命令格式

1
2
3
cp [选项] 源文件 目录

cp [选项] -t 目录 源文件

常用参数

  • -t --target-directory:指定目标目录
  • -i --interactive:覆盖前询问(使前面的-n选项失效)
  • -n --no-clobber:不要覆盖已存在的文件(使前面的-i选项失效)
  • -f --force:强行复制文件或目录,不论目的文件或目录是否已经存在
  • -u --update:使用这项参数之后,只会在源文件的修改时间较目的文件更新时,或是对应的目的文件并不存在,才复制文件

使用实例

  1. 复制文件test1.txt到test1目录

    1
    cp test1.txt test1  # 若文件存在,会提示是否覆盖。若不存在直接完成复制
  2. 复制test1整个目录到test2

    1
    cp -a test1 test2

9. touch命令

touch命令参数可更改文档或目录的日期时间,包括存取时间和更改时间。

命令格式

1
touch [选项] 文件

常用参数

  • -a--time=atime--time=access--time=use:只更改存取时间
  • -c--no-create:不建立任何文档
  • -d:使用指定的日期时间,而非现在的时间
  • -f:此参数将忽略不予处理,仅负责解决BSD版本touch指令的兼容性问题
  • -m--time=mtime--time=modify:只更改变动时间
  • -r:把指定文档或目录的日期时间,统统设成和参考文档或目录的日期时间相同
  • -t:使用指定的日期时间,而非现在的时间

使用实例

  1. 创建不存在的文件test.txt

    1
    touch test.txt
  2. 更新test.txt的时间戳和test1.txt时间戳相同

    1
    touch -r test.txt test1.txt

10. cat命令

用来显示文件内容,或者将几个文件连接起来显示,或者从标准输入读取内容并显示,它常与重定向符号配合使用。

命令格式

1
cat [选项] [文件]

常用参数

  • -A, --show-all:等价于-vET
  • -b, --number-nonblank:对非空输出行编号
  • -e:等价于-vE
  • -E, --show-ends:在每行结束处显示$
  • -n, --number:对输出的所有行编号,由1开始对所有输出的行数编号
  • -s, --squeeze-blank:有连续两行以上的空白行,就代换为一行的空白行
  • -t:与-vT等价
  • -T, --show-tabs:将跳格字符显示为^I
  • -u:(被忽略)
  • -v, --show-nonprinting:使用^M-引用,除了LFD和TAB之外

使用实例

  1. 把test.log的文件内容加上行号后输入test1.log这个文件里

    1
    cat -n test.log > test1.log
  2. 将test.log的文件内容反向显示

    1
    tac test.log

11. nl命令

输出的文件内容自动的加上行号!其默认的结果与cat -n有点不太一样,nl可以将行号做比较多的显示设计,包括位数与是否自动补齐0等等的功能。

命令格式

1
nl [选项] [文件]

常用参数

  • -b:指定行号指定的方式,主要有两种:
    • -b a:表示不论是否为空行,也同样列出行号(类似cat -n
    • -b t:如果有空行,空的那一行不要列出行号(默认值)
  • -n:列出行号表示的方法,主要有三种:
    • -n ln:行号在萤幕的最左方显示
    • -n rn:行号在自己栏位的最右方显示,且不加0
    • -n rz:行号在自己栏位的最右方显示,且加0
  • -w:行号栏位的占用的位数

使用实例

  1. 用nl列出test.log的内容

    1
    nl test.log
  2. 用nl列出test.log的内容,空本行也加上行号

    1
    nl -b a test.log

12. more命令

more命令和cat的功能一样都是查看文件里的内容,但有所不同的是more可以按页来查看文件的内容,还支持直接跳转行等功能。

命令格式

1
more [-dlfpcsu] [-num] [+/pattern] [+linenum] [file...]

常用参数

  • +n:从第n行开始显示
  • -n:定义屏幕大小为n行
  • +/pattern:在每个档案显示前搜寻该字串(pattern),然后从该字串前两行之后开始显示
  • -c:从顶部清屏,然后显示
  • -d:提示”Press space to continue,’q’ to quit(按空格键继续,按q键退出)”,禁用响铃功能
  • -l:忽略Ctrl+l(换页)字符
  • -p:通过清除窗口而不是滚屏来对文件进行换页,与-c选项相似
  • -s:把连续的多个空行显示为一行
  • -u:把文件内容中的下画线去掉

操作指令

  • Enter:向下n行,需要定义。默认为1行
  • Ctrl+F:向下滚动一屏
  • 空格键:向下滚动一屏
  • Ctrl+B:返回上一屏
  • =:输出当前行的行号
  • :f:输出文件名和当前行的行号
  • V:调用vi编辑器
  • !命令:调用Shell,并执行命令
  • q:退出more

使用实例

  1. 显示文件test.log第3行起内容

    1
    more +3 test.log
  2. 从文件test.log查找第一个出现”day3”字符串的行,并从该处前2行开始显示输出

    1
    more +/day3 test.log
  3. 设置每屏显示行数

    1
    more -5 test.log

13. less命令

less与more类似,但使用less可以随意浏览文件,而more仅能向前移动,却不能向后移动,而且less在查看之前不会加载整个文件。

命令格式

1
less [参数] 文件

常用参数

  • -b <缓冲区大小>:设置缓冲区的大小
  • -e:当文件显示结束后,自动离开
  • -f:强迫打开特殊文件,例如外围设备代号、目录和二进制文件
  • -g:只标志最后搜索的关键词
  • -i:忽略搜索时的大小写
  • -m:显示类似more命令的百分比
  • -N:显示每行的行号
  • -o <文件名>:将less输出的内容在指定文件中保存起来
  • -Q:不使用警告音
  • -s:显示连续空行为一行
  • -S:行过长时间将超出部分舍弃
  • -x <数字>:将”tab”键显示为规定的数字空格

操作命令

  • /字符串:向下搜索”字符串”的功能
  • ?字符串:向上搜索”字符串”的功能
  • n:重复前一个搜索(与/?有关)
  • N:反向重复前一个搜索(与/?有关)
  • b:向后翻一页
  • d:向后翻半页
  • h:显示帮助界面
  • Q:退出less命令
  • u:向前滚动半页
  • y:向前滚动一行
  • 空格键:滚动一行
  • 回车键:滚动一页
  • [pagedown]:向下翻动一页
  • [pageup]:向上翻动一页

使用实例

  1. 查看文件test.log
    1
    less test.log

14. head命令

head用来显示档案的开头至标准输出中,默认head命令打印其相应文件的开头10行。

命令格式

1
head [参数] [文件]

常用参数

  • -q:隐藏文件名
  • -v:显示文件名
  • -c<字节>:显示字节数
  • -n<行数>:显示的行数

使用实例

  1. 显示文件test.log的前5行

    1
    head -n 5 test.log
  2. 显示文件test.log前20个字节

    1
    head -c 20 test.log

15. tail命令

显示指定文件末尾内容,不指定文件时,作为输入信息进行处理。常用查看日志文件。

命令格式

1
tail [必要参数] [选择参数] [文件]

常用参数

  • -f:循环读取
  • -q:不显示处理信息
  • -v:显示详细的处理信息
  • -c<数目>:显示的字节数
  • -n<行数>:显示行数
  • --pid=PID:与-f合用,表示在进程ID,PID死掉之后结束
  • -q, --quiet, --silent:从不输出给出文件名的首部
  • -s, --sleep-interval=S:与-f合用,表示在每次反复的间隔休眠S秒

使用实例

  1. 显示文件test.log最后5行内容

    1
    tail -n 5 test.log
  2. 循环查看文件内容

    1
    tail -f test.log

二、文件查找

16. which命令

which指令会在PATH变量指定的路径中,搜索某个系统命令的位置,并且返回第一个搜索结果。

命令格式

1
which 可执行文件名称

常用参数

  • -n:指定文件名长度,指定的长度必须大于或等于所有文件中最长的文件名
  • -p:与-n参数相同,但此处的包括了文件的路径
  • -w:指定输出时栏位的宽度
  • -V:显示版本信息

使用实例

  1. 查找文件、显示命令路径

    1
    which pwd
  2. 用which去找出which

    1
    which which

17. whereis命令

whereis命令是定位可执行文件、源代码文件、帮助文件在文件系统中的位置。

命令格式

1
whereis [-bmsu] [BMS 目录名 -f] 文件名

常用参数

  • -b:定位可执行文件
  • -m:定位帮助文件
  • -s:定位源代码文件
  • -u:搜索默认路径下除可执行文件、源代码文件、帮助文件以外的其它文件
  • -B:指定搜索可执行文件的路径
  • -M:指定搜索帮助文件的路径
  • -S:指定搜索源代码文件的路径

使用实例

  1. 将和svn文件相关的文件都查找出来

    1
    whereis svn
  2. 只将二进制文件查找出来

    1
    whereis -b svn

18. locate命令

可以很快速的搜寻档案系统内是否有指定的档案。

命令格式

1
locate [选择参数] [样式]

常用参数

  • -e:将排除在寻找的范围之外
  • -1:如果是1,则启动安全模式。在安全模式下,使用者不会看到权限无法看到的档案。这会始速度减慢,因为locate必须至实际的档案系统中取得档案的权限资料
  • -f:将特定的档案系统排除在外,例如我们没有到理要把proc档案系统中的档案放在资料库中
  • -q:安静模式,不会显示任何错误讯息
  • -n:至多显示n个输出
  • -r:使用正规运算式做寻找的条件
  • -o:指定资料库存的名称
  • -d:指定资料库的路径

使用实例

  1. 查找和pwd相关的所有文件

    1
    locate pwd
  2. 搜索etc目录下,所有以m开头的文件

    1
    locate /etc/m

19. find命令

主要作用是沿着文件层次结构向下遍历,匹配符合条件的文件,并执行相应的操作。

命令格式

1
find [选项] [搜索路径] [表达式]

常用参数

  • -print:find命令将匹配的文件输出到标准输出
  • -exec:find命令对匹配的文件执行该参数所给出的shell命令
  • -name:按照文件名查找文件
  • -type:查找某一类型的文件

使用实例

  1. 打印当前目录文件目录列表

    1
    find . -print
  2. 打印当前目录下所有不以.txt结尾的文件名

    1
    find . ! -name "*.txt"
  3. 打印当前目录下所有权限为777的php文件

    1
    find . -type f -name "*.php" -perm 777
  4. 找到当前目录下所有php文件,并显示其详细信息

    1
    find . -name "*.php" -exec ls -l {} \;
  5. 查找当前目录下所有c代码文件,统计总行数

    1
    find . -type f -name "*.c" | xargs wc -l

xargs命令说明:xargs命令可以从标准输入接收输入,并把输入转换为一个特定的参数列表。
命令格式:command | xargs [选项] [command]
xargs命令应该紧跟在管道操作符之后,因为它以标准输入作为主要的源数据流。
常用参数:

  • -n:指定每行最大的参数数量
  • -d:指定分隔符

三、文件打包上传和下载

20. tar命令

用来压缩和解压文件。tar本身不具有压缩功能。它是调用压缩功能实现的。

命令格式

1
tar [必要参数] [选择参数] [文件]

常用参数

必要参数:

  • -A:新增压缩文件到已存在的压缩
  • -B:设置区块大小
  • -c:建立新的压缩文件
  • -d:记录文件的差别
  • -r:添加文件到已经压缩的文件
  • -u:添加改变了和现有的文件到已经存在的压缩文件
  • -x:从压缩的文件中提取文件
  • -t:显示压缩文件的内容
  • -z:支持gzip解压文件
  • -j:支持bzip2解压文件
  • -Z:支持compress解压文件
  • -v:显示操作过程
  • -l:文件系统边界设置
  • -k:保留原有文件不覆盖
  • -m:保留文件不被覆盖
  • -W:确认压缩文件的正确性

可选参数:

  • -b:设置区块数目
  • -C:切换到指定目录
  • -f:指定压缩文件
  • --help:显示帮助信息
  • --version:显示版本信息

使用实例

  1. 将文件全部打包成tar包

    1
    2
    3
    tar -cvf test.tar test.log    # 仅打包,不压缩!
    tar -zcvf test.tar.gz test.log # 打包后,以gzip压缩
    tar -jcvf test.tar.bz2 test.log # 打包后,以bzip2压缩
  2. 将tar包解压缩

    1
    tar -zxvf test.tar.gz

21. gzip命令

使用广泛的压缩程序,文件经它压缩过后,其名称后面会多出”.gz”的扩展名。

命令格式

1
gzip [参数] [文件或者目录]

常用参数

  • -a--ascii:使用ASCII文字模式
  • -c--stdout--to-stdout:把压缩后的文件输出到标准输出设备,不去更动原始文件
  • -d--decompress--uncompress:解开压缩文件
  • -f--force:强行压缩文件。不理会文件名称或硬连接是否存在以及该文件是否为符号连接
  • -h--help:在线帮助

使用实例

  1. 把test1目录下的每个文件压缩成.gz文件
    1
    gzip *

四、文件权限设置

22. chmod命令

用于改变Linux系统文件或目录的访问权限。

命令格式

1
chmod [-cfvR] [--help] [--version] mode file

常用参数

必要参数:

  • -c:当发生改变时,报告处理信息
  • -f:错误信息不输出
  • -R:处理指定目录以及其子目录下的所有文件
  • -v:运行时显示详细处理信息

选择参数:

  • --reference=<目录或者文件>:设置成具有指定目录或者文件具有相同的权限
  • --version:显示版本信息
  • <权限范围>+<权限设置>:使权限范围内的目录或者文件具有指定的权限
  • <权限范围>-<权限设置>:删除权限范围的目录或者文件的指定权限
  • <权限范围>=<权限设置>:设置权限范围内的目录或者文件的权限为指定的值

权限范围:

  • u:目录或者文件的当前的用户
  • g:目录或者文件的当前的群组
  • o:除了目录或者文件的当前用户或群组之外的用户或者群组
  • a:所有的用户及群组

权限代号:

  • r:读权限,用数字4表示
  • w:写权限,用数字2表示
  • x:执行权限,用数字1表示
  • -:删除权限,用数字0表示

使用实例

  1. 增加文件所有用户组可执行权限

    1
    chmod a+x test.log
  2. 删除所有用户的可执行权限

    1
    chmod a-x test.log

23. chgrp命令

可采用群组名称或群组识别码的方式改变文件或目录的所属群组。

命令格式

1
chgrp [选项] [组] [文件]

常用参数

必要参数:

  • -c:当发生改变时输出调试信息
  • -f:不显示错误信息
  • -R:处理指定目录以及其子目录下的所有文件
  • -v:运行时显示详细的处理信息
  • --dereference:作用于符号链接的指向,而不是符号链接本身
  • --no-dereference:作用于符号链接本身

选择参数:

  • --reference=<文件或者目录>
  • --help:显示帮助信息
  • --version:显示版本信息

使用实例

  1. 改变文件的群组属性
    1
    chgrp -v bin test.log

24. chown命令

通过chown改变文件的拥有者和群组。

命令格式

1
chown [选项] [所有者][:[组]] 文件

常用参数

必要参数:

  • -c:显示更改的部分的信息
  • -f:忽略错误信息
  • -h:修复符号链接
  • -R:处理指定目录以及其子目录下的所有文件
  • -v:显示详细的处理信息
  • --deference:作用于符号链接的指向,而不是符号链接本身

选择参数:

  • --reference=<目录或文件>:把指定的目录/文件作为参考,把操作的文件/目录设置成参考文件/目录相同拥有者和群组
  • --from=<当前用户:当前群组>:只有当前用户和群组跟指定的用户和群组相同时才进行改变
  • --help:显示帮助信息
  • --version:显示版本信息

使用实例

  1. 改变拥有者和群组
    1
    chown mail:mail test.log

五、磁盘管理

25. df命令

显示磁盘空间使用情况。

命令格式

1
df [选项] [文件]

常用参数

  • -a:全部文件系统列表
  • -h:方便阅读方式显示
  • -i:显示inode信息
  • -k:区块为1024字节
  • -l:只显示本地磁盘
  • -T:文件系统类型

使用实例

  1. 显示磁盘使用情况

    1
    df -l
  2. 以易读方式列出所有文件系统及其类型

    1
    df -haT

26. du命令

显示每个文件和目录的磁盘使用空间。

命令格式

1
du [选项] [文件]

常用参数

  • -a:显示目录中个别文件的大小
  • -b:显示目录或文件大小时,以byte为单位
  • -c:除了显示个别目录或文件的大小外,同时也显示所有目录或文件的总和
  • -k:以KB(1024bytes)为单位输出
  • -m:以MB为单位输出
  • -s:仅显示总计,只列出最后加总的值
  • -h:以K,M,G为单位,提高信息的可读性
  • -x:以一开始处理时的文件系统为准,若遇上其它不同的文件系统目录则略过
  • -L:显示选项中所指定符号链接的源文件大小
  • -S:显示个别目录的大小时,并不含其子目录的大小
  • -X<文件>:在<文件>指定目录或文件
  • --exclude=<目录或文件>:略过指定的目录或文件
  • -D:显示指定符号链接的源文件大小
  • -H:与-h参数相同,但是K,M,G是以1000为换算单位
  • -l:重复计算硬件链接的文件

使用实例

  1. 以易读方式显示文件夹内及子文件夹大小

    1
    du -h test/
  2. 显示几个文件或目录各自占用磁盘空间的大小,并统计总和

    1
    du -ch test1 test2

27. ln命令

为某一个文件在另外一个位置建立一个同步的链接。

命令格式

1
ln [参数] [源文件或目录] [目标文件或目录]

常用参数

  • -b:删除,覆盖以前建立的链接
  • -d:允许超级用户制作目录的硬链接
  • -f:强制执行
  • -i:交互模式,文件存在则提示用户是否覆盖
  • -n:把符号链接视为一般目录
  • -s:软链接(符号链接)
  • -v:显示详细的处理过程

使用实例

  1. 给文件创建软链接

    1
    ln -s /usr/local/mysql/bin/mysql /usr/bin
  2. 给文件创建硬链接

    1
    ln /usr/local/mysql/bin/mysql /usr/bin

六、系统管理

28. ps命令

用来列出系统中当前运行的那些进程。

命令格式

1
ps [参数]

常用参数

  • -a:显示所有进程
  • -u:用户以及其他详细信息
  • -x:显示没有控制终端的进程

使用实例

  1. 显示所有进程信息

    1
    ps -A
  2. 显示指定用户信息

    1
    ps -u root

29. kill命令

发送指定的信号到相应进程。

命令格式

1
kill [参数] [进程号]

常用参数

  • -l:信号,若不加信号的编号参数,则使用-l参数会列出全部的信号名称
  • -a:当处理当前进程时,不限制命令名和进程号的对应关系
  • -p:指定kill命令只打印相关进程的进程号,而不发送任何信号
  • -s:指定发送信号
  • -u:指定用户

使用实例

  1. 先使用ps查找进程pro1,然后用kill杀掉
    1
    kill -9 $(ps -ef | grep pro1)

30. top命令

显示当前系统正在执行的进程的相关信息,包括进程ID、内存占用率、CPU占用率等。

命令格式

1
top [参数]

常用参数

  • -b:批处理
  • -c:显示完整的治命令
  • -I:忽略失效过程
  • -s:保密模式
  • -S:累积模式
  • -i<时间>:设置间隔时间
  • -u<用户名>:指定用户名
  • -p<进程号>:指定进程
  • -n<次数>:循环显示的次数

使用实例

  1. 显示进程信息

    1
    top
  2. 显示完整命令

    1
    top -c

31. free命令

显示系统内存使用情况,包括物理内存、交互区内存(swap)和内核缓冲区内存。

命令格式

1
free [参数]

常用参数

  • -b:以Byte为单位显示内存使用情况
  • -k:以KB为单位显示内存使用情况
  • -m:以MB为单位显示内存使用情况
  • -g:以GB为单位显示内存使用情况
  • -o:不显示缓冲区调节列
  • -s<间隔秒数>:持续观察内存使用状况
  • -t:显示内存总和列
  • -V:显示版本信息

使用实例

  1. 显示内存使用情况
    1
    free -m

32. vmstat命令

显示虚拟内存状态。

命令格式

1
vmstat [参数]

常用参数

  • -a:显示活动内页
  • -f:显示启动后创建的进程总数
  • -m:显示slab信息
  • -n:头信息仅显示一次
  • -s:以表格方式显示事件计数器和内存状态
  • -d:报告磁盘状态
  • -p:显示指定的硬盘分区状态
  • -S:输出信息的单位

使用实例

  1. 显示活动和非活动内存
    1
    vmstat -a 2 5

33. iostat命令

被用于监视系统输入输出设备和CPU的使用情况。

命令格式

1
iostat [参数] [时间] [次数]

常用参数

  • -c:仅显示CPU使用情况
  • -d:仅显示设备利用率
  • -k:显示状态以千字节每秒为单位,而不使用块每秒
  • -m:显示状态以兆字节每秒为单位
  • -p:仅显示块设备和所有被使用的其他分区的状态
  • -t:显示每个报告产生时的时间
  • -V:显示版号并退出
  • -x:显示扩展状态

使用实例

  1. 显示所有设备负载情况
    1
    iostat

34. watch命令

可以将命令的输出结果输出到标准输出设备,多用于周期性执行命令/定时执行命令。

命令格式

1
watch [参数] [命令]

常用参数

  • -n:或--interval:watch缺省每2秒运行一下程序,可以用-n--interval来指定间隔的时间
  • -d:或--differences:用-d--differences选项watch会高亮显示变化的区域。-d=cumulative选项会把变动过的地方(不管最近的那次有没有变动)都高亮显示出来
  • -t:或-no-title:会关闭watch命令在顶部的时间间隔,命令,当前时间的输出
  • -h:或--help:查看帮助文档

使用实例

  1. 每隔一秒高亮显示网络链接数的变化情况

    1
    watch -n 1 -d netstat -ant
  2. 每隔一秒高亮显示http链接数的变化情况

    1
    watch -n 1 -d 'pstree|grep http'

35. at命令

在指定时间执行一个任务,只能执行一次。

命令格式

1
at [参数] [时间]

常用参数

  • -m:当指定的任务被完成之后,将给用户发送邮件,即使没有标准输出
  • -I:atq的别名
  • -d:atrm的别名
  • -v:显示任务将被执行的时间
  • -c:打印任务的内容到标准输出
  • -V:显示版本信息
  • -q<队列>:使用指定的队列
  • -f<文件>:从指定文件读入任务而不是从标准输入读入
  • -t<时间参数>:以时间参数的形式提交要运行的任务

使用实例

  1. 三天后的下午5点执行/bin/ls

    1
    2
    at 5pm + 3 days
    /bin/ls
  2. 明天17点钟,输出时间到指定文件内

    1
    2
    at 17:20 tomorrow
    date > /root/2016.log

36. crontab命令

被用来提交和管理用户的需要周期性执行的任务。

命令格式

1
crontab [选项] [文件]

常用参数

  • -e:编辑该用户的计时器设置
  • -l:列出该用户的计时器设置
  • -r:删除该用户的计时器设置
  • -u<用户名称>:指定要设定计时器的用户名称

使用实例

  1. 每1分钟执行一次command

    1
    * * * * * command
  2. 每小时的第3和第15分钟执行

    1
    3,15 * * * * command
  3. 在上午8点到11点的第3和第15分钟执行

    1
    3,15 8-11 * * * command

37. scp命令

用于Linux之间复制文件和目录。

命令格式

1
scp [参数] [原路径] [目标路径]

常用参数

  • -1:强制scp命令使用协议ssh1
  • -2:强制scp命令使用协议ssh2
  • -4:强制scp命令只使用IPv4寻址
  • -6:强制scp命令只使用IPv6寻址
  • -B:使用批处理模式(传输过程中不询问传输口令或短语)
  • -C:允许压缩(将-C标志传递给ssh,从而打开压缩功能)
  • -p:保留原文件的修改时间,访问时间和访问权限
  • -q:不显示传输进度条
  • -r:递归复制整个目录
  • -v:详细方式显示输出。scp和ssh(1)会显示出整个过程的调试信息。这些信息用于调试连接,验证和配置问题
  • -c cipher:以cipher将数据传输进行加密,这个选项将直接传递给ssh
  • -F ssh_config:指定一个替代的ssh配置文件,此参数直接传递给ssh
  • -i identity_file:从指定文件中读取传输时使用的密钥文件,此参数直接传递给ssh
  • -l limit:限定用户所能使用的带宽,以Kbit/s为单位
  • -o ssh_option:如果习惯于使用ssh_config(5)中的参数传递方式
  • -P port:注意是大写的P,port是指定数据传输用到的端口号
  • -S program:指定加密传输时所使用的程序。此程序必须能够理解ssh(1)的选项

使用实例

  1. 从本地复制到远程

    1
    2
    3
    4
    scp local_file remote_username@remote_ip:remote_folder
    scp local_file remote_username@remote_ip:remote_file
    scp local_file remote_ip:remote_folder
    scp local_file remote_ip:remote_file
  2. 从远程复制到本地

    1
    2
    scp root@www.test.com:/home/root/others/music /home/space/music/1.mp3
    scp -r www.test.com:/home/root/others/ /home/space/music/

38. wget命令

用来从指定的URL下载文件。

命令格式

1
wget [参数] [URL地址]

常用参数

  • -b:后台下载模式
  • -P:下载到指定目录
  • -t:最大尝试次数
  • -c:断点续传
  • -p:下载页面内所有资源,包括图片、视频等
  • -r:递归下载

使用实例

  1. 使用wget下载单个文件

    1
    wget http://www.test.com/testfile.zip
  2. 下载并以不同的文件名保存

    1
    wget -O test.zip http://www.test.com/download?file=testfile.zip

七、网络管理

39. ifconfig命令

被用于配置和显示Linux内核中网络接口的网络参数。

命令格式

1
ifconfig [参数]

常用参数

  • add<地址>:设置网络设备IPv6的IP地址
  • del<地址>:删除网络设备IPv6的IP地址
  • down:关闭指定的网络设备
  • up:启动指定的网络设备
  • IP地址:指定网络设备的IP地址

使用实例

  1. 显示网络设备信息

    1
    ifconfig
  2. 启动关闭指定网卡

    1
    2
    ifconfig eth0 down
    ifconfig eth0 up

40. ping命令

用于测试主机之间网络的连通性。

命令格式

1
ping [参数] [主机名或IP地址]

常用参数

  • -d:使用Socket的SO_DEBUG功能
  • -c<完成次数>:设置完成要求回应的次数
  • -f:极限检测
  • -i<间隔秒数>:指定收发信息的间隔时间
  • -I<网络界面>:使用指定的网络界面送出数据包
  • -l<前置载入>:设置在送出要求信息之前,先行发出的数据包
  • -n:只输出数值
  • -p<范本样式>:设置填满数据包的范本样式
  • -q:不显示指令执行过程,开头和结尾的相关信息除外
  • -r:忽略普通的Routing Table,直接将数据包送到远端主机上
  • -R:记录路由过程
  • -s<数据包大小>:设置数据包的大小
  • -t<存活数值>:设置存活数值TTL的大小
  • -v:详细显示指令的执行过程

使用实例

  1. 检测是否与主机连通
    1
    ping www.google.com

41. netstat命令

用于显示网络状态。

命令格式

1
netstat [参数]

常用参数

  • -a--all:显示所有连线中的Socket
  • -A<网络类型>--<网络类型>:列出该网络类型连线中的相关地址
  • -c--continuous:持续列出网络状态
  • -C--cache:显示路由器配置的快取信息
  • -e--extend:显示网络其他相关信息
  • -F--fib:显示FIB
  • -g--groups:显示多重广播功能群组组员名单
  • -h--help:在线帮助
  • -i--interfaces:显示网络界面信息表单
  • -l--listening:显示监控中的服务器的Socket
  • -M--masquerade:显示伪装的网络连线
  • -n--numeric:直接使用IP地址,而不通过域名服务器
  • -N--netlink--symbolic:显示网络硬件外围设备的符号连接名称
  • -o--timers:显示计时器
  • -p--programs:显示正在使用Socket的程序识别码和程序名称
  • -r--route:显示Routing Table
  • -s--statistice:显示网络工作信息统计表
  • -t--tcp:显示TCP传输协议的连线状况
  • -u--udp:显示UDP传输协议的连线状况
  • -v--verbose:显示指令执行过程
  • -V--version:显示版本信息
  • -w--raw:显示RAW传输协议的连线状况
  • -x--unix:此参数的效果和指定”-A unix”参数相同
  • --ip--inet:此参数的效果和指定”-A inet”参数相同

使用实例

  1. 列出所有端口

    1
    netstat -a
  2. 显示TCP端口

    1
    netstat -at

42. telnet命令

用于登录远程主机,对远程主机进行管理。

命令格式

1
telnet [参数] [主机]

常用参数

  • -8:允许使用8位字符资料,包括输入与输出
  • -a:尝试自动登入远端系统
  • -b<主机别名>:使用别名指定远端主机名称
  • -c:不读取用户专属目录里的.telnetrc文件
  • -d:启动排错模式
  • -e<脱离字符>:设置脱离字符
  • -E:滤除脱离字符
  • -f:此参数的效果和指定”-F”参数相同
  • -F:使用Kerberos V5认证时,加上此参数可把本地主机的认证数据上传到远端主机
  • -k<域名>:使用Kerberos认证时,加上此参数让远端主机采用指定的领域名,而非该主机的域名
  • -K:不自动登入远端主机
  • -l<用户名称>:指定要登入远端主机的用户名称
  • -L:允许输出8位字符资料
  • -n<记录文件>:指定文件记录相关信息
  • -r:使用类似rlogin指令的用户界面
  • -S<服务类型>:设置telnet连线所需的IP TOS信息
  • -x:假设主机有支持数据加密的功能,就使用它
  • -X<认证形态>:关闭指定的认证形态

使用实例

  1. 远程服务器无法访问
    1
    telnet 192.168.0.5 8080

43. ssh命令

用于远程登录上Linux主机。

命令格式

1
ssh [参数] [远程主机]

常用参数

  • -1:强制使用ssh协议版本1
  • -2:强制使用ssh协议版本2
  • -4:强制使用IPv4地址
  • -6:强制使用IPv6地址
  • -A:开启认证代理连接转发功能
  • -a:关闭认证代理连接转发功能
  • -b<IP地址>:使用本机指定的地址作为对位连接的源IP地址
  • -c<请求认证>:选择所请求的认证方式。用逗号分隔列表
  • -C:请求压缩所有数据
  • -F<配置文件>:指定ssh指令的配置文件
  • -f:后台执行ssh指令
  • -g:允许远程主机连接主机的转发端口
  • -i<身份文件>:指定身份文件(即私钥文件)
  • -l<登录名>:指定连接远程服务器的登录用户名
  • -N:不执行远程指令
  • -o<选项>:指定配置选项
  • -p<端口>:指定远程服务器上的端口
  • -q:静默模式,所有的警告和诊断信息被禁止输出
  • -s:请求远程系统上的一个子系统调用。子系统是SSH2协议的一个特性,有助于将SSH用作其他应用程序的安全传输。子系统由远程命令指定
  • -T:禁止分配伪终端
  • -t:强制分配伪终端。可以在远程机器上执行任何全屏幕(screen-based)程序,所以非常有用,例如菜单服务。重复使用-t选项可以强制分配终端,即使没有本地终端
  • -v:详细模式。使ssh打印关于进度的调试信息。这对于调试连接、认证和配置问题很有帮助。多个-v选项会增加详细程度。最大为3
  • -V:显示版本信息
  • -x:关闭X11转发
  • -X:开启X11转发
  • -y:开启信任X11转发

使用实例

  1. 登录远程服务器
    1
    ssh 192.168.0.11

八、用户管理

44. useradd命令

用于建立用户帐号。

命令格式

1
useradd [选项] 用户名

常用参数

  • -c<备注>:加上备注文字。备注文字会保存在passwd的备注栏位中
  • -d<登入目录>:指定用户登入时的启始目录
  • -D:变更预设值
  • -e<有效期限>:指定帐号的有效期限
  • -f<缓冲天数>:指定在密码过期后多少天即关闭该帐号
  • -g<群组>:指定用户所属的群组
  • -G<群组>:指定用户所属的附加群组
  • -m:自动建立用户的登入目录
  • -M:不要自动建立用户的登入目录
  • -n:取消建立以用户名称为名的群组
  • -r:建立系统帐号
  • -s<shell>:指定用户登入后所使用的shell
  • -u<uid>:指定用户id

使用实例

  1. 新建用户加入组
    1
    useradd -g root testuser

45. userdel命令

用于删除用户帐号。

命令格式

1
userdel [选项] 用户名

常用参数

  • -f:强制删除用户,即使用户当前已登录
  • -r:删除用户的同时,删除与用户相关的所有文件

使用实例

  1. 删除用户账号
    1
    userdel testuser

46. passwd命令

用于设置用户的认证信息,包括用户密码、密码过期时间等。

命令格式

1
passwd [选项] [用户名]

常用参数

  • -d:删除密码,仅有系统管理者才能使用
  • -f:强制执行
  • -k:设置只有在密码过期失效后,方能更新
  • -l:锁住密码
  • -s:列出密码的相关信息,仅有系统管理者才能使用
  • -u:解开已上锁的帐号

使用实例

  1. 修改用户密码
    1
    passwd testuser

47. su命令

用于切换当前用户身份到其他用户身份。

命令格式

1
su [选项] [用户名]

常用参数

  • -c<指令>--command=<指令>:执行完指定的指令后,即恢复原来的身份
  • -f--fast:适用于csh与tsch,使shell不用去读取启动文件
  • -l--login:改变身份时,也同时变更工作目录,以及HOME,SHELL,USER,LOGNAME。此外,也会变更PATH变量
  • -m-p--preserve-environment:变更身份时,不要变更环境变量
  • -s<shell>--shell=<shell>:指定要执行的shell
  • --help:显示帮助
  • --version:显示版本信息

使用实例

  1. 变更帐号为root并在执行ls指令后退出变回原使用者
    1
    su -c ls root

九、其他常用命令

48. clear命令

用于清除当前终端屏幕。

命令格式

1
clear

使用实例

  1. 清屏
    1
    clear

49. history命令

用于显示历史命令。

命令格式

1
history [选项] [参数]

常用参数

  • -c:清空当前历史命令
  • -a:将历史命令缓冲区中命令写入历史命令文件中
  • -r:将历史命令文件中的命令读入当前历史命令缓冲区
  • -w:将当前历史命令缓冲区命令写入历史命令文件中

使用实例

  1. 显示所有历史命令

    1
    history
  2. 执行历史记录中第n条命令

    1
    !n

50. alias命令

用于设置指令的别名。

命令格式

1
alias [别名]=[指令名称]

使用实例

  1. 查看系统当前所有别名

    1
    alias
  2. 设置别名

    1
    alias l='ls -l'

51. unalias命令

用于删除别名。

命令格式

1
unalias [别名]

使用实例

  1. 删除别名
    1
    unalias l

52. date命令

显示或设定系统的日期与时间。

命令格式

1
date [选项] [格式]

常用参数

  • -d<字符串>:显示字符串所指的日期与时间。字符串前后必须加上双引号
  • -s<字符串>:根据字符串来设置日期与时间。字符串前后必须加上双引号
  • -u:显示GMT
  • --help:在线帮助
  • --version:显示版本信息

使用实例

  1. 显示当前时间

    1
    date
  2. 设置系统时间

    1
    date -s "2024-01-01 12:00:00"

53. cal命令

用于显示当前日历,或者指定日期的日历。

命令格式

1
cal [选项] [[[日] 月] 年]

常用参数

  • -1:显示一个月的日历
  • -3:显示前一个月、当前月、下一个月的日历
  • -s:星期天作为一周的第一天
  • -m:星期一作为一周的第一天
  • -j:显示在当年中的第几天(一年日期按天算,从1月1日算起,默认显示当前月在一年中的天数)
  • -y:显示当前年份的日历

使用实例

  1. 显示当前月份日历

    1
    cal
  2. 显示指定年份的日历

    1
    cal 2024

54. echo命令

用于在shell中打印shell变量的值,或者直接输出指定的字符串。

命令格式

1
echo [选项] [字符串]

常用参数

  • -e:激活转义字符
  • -n:不换行输出

使用实例

  1. 输出字符串

    1
    echo "Hello World"
  2. 输出变量值

    1
    echo $PATH

55. grep命令

强大的文本搜索工具,能使用正则表达式搜索文本,并把匹配的行打印出来。

命令格式

1
grep [选项] 模式 [文件]

常用参数

  • -i:忽略大小写
  • -v:反向选择,即显示不包含匹配文本的所有行
  • -n:显示匹配行及行号
  • -c:计算匹配的行数
  • -r:递归搜索
  • -l:只显示文件名
  • -w:匹配整个单词

使用实例

  1. 在文件中搜索指定字符串

    1
    grep "error" test.log
  2. 递归搜索目录

    1
    grep -r "function" /home/user/code/

56. sed命令

流编辑器,用于对文本进行过滤和转换。

命令格式

1
sed [选项] '命令' 文件

常用参数

  • -n:只显示处理后的结果
  • -e:直接在命令行模式上进行sed动作编辑
  • -i:直接修改文件内容
  • -r:使用扩展正则表达式

常用命令

  • s:替换
  • d:删除
  • p:打印
  • a:追加
  • i:插入

使用实例

  1. 替换文件中的字符串

    1
    sed 's/old/new/g' file.txt
  2. 删除文件中的空行

    1
    sed '/^$/d' file.txt

57. awk命令

强大的文本分析工具,用于处理文本文件。

命令格式

1
awk [选项] '模式 {动作}' 文件

常用参数

  • -F:指定分隔符
  • -v:定义变量
  • -f:从脚本文件中读取awk命令

使用实例

  1. 打印文件的第一列

    1
    awk '{print $1}' file.txt
  2. 使用冒号作为分隔符

    1
    awk -F: '{print $1}' /etc/passwd

58. sort命令

用于对文本文件的行进行排序。

命令格式

1
sort [选项] [文件]

常用参数

  • -n:依照数值的大小排序
  • -r:以相反的顺序来排序
  • -k:指定排序的列
  • -u:去除重复行
  • -t:指定分隔符

使用实例

  1. 对文件进行排序

    1
    sort file.txt
  2. 按数值排序

    1
    sort -n file.txt

59. uniq命令

用于报告或忽略文件中的重复行。

命令格式

1
uniq [选项] [输入文件] [输出文件]

常用参数

  • -c:在每行旁边显示该行重复出现的次数
  • -d:仅显示重复出现的行
  • -u:仅显示出现一次的行
  • -i:忽略大小写

使用实例

  1. 显示重复行

    1
    uniq -d file.txt
  2. 统计每行出现次数

    1
    uniq -c file.txt

60. wc命令

用于计算文件的字节数、字数、行数。

命令格式

1
wc [选项] [文件]

常用参数

  • -c:显示字节数
  • -l:显示行数
  • -w:显示字数
  • -m:显示字符数

使用实例

  1. 统计文件行数

    1
    wc -l file.txt
  2. 统计多个文件

    1
    wc file1.txt file2.txt

十、总结

Linux命令虽然繁多,但掌握这些常用命令已经足够应对日常的系统管理和开发工作。建议:

  1. 分类记忆:将命令按功能分类,便于记忆和使用
  2. 实践为主:多在实际工作中使用这些命令
  3. 善用帮助:使用man命令查看命令的详细说明
  4. 组合使用:学会将多个命令通过管道组合使用
  5. 建立别名:为常用命令设置别名,提高工作效率

记住,熟练使用Linux命令的关键在于实践。随着使用经验的积累,这些命令会逐渐成为你的第二本能。

wpa_supplicant是一个连接、配置WIFI的工具,它主要包含wpa_supplicantwpa_cli两个程序。通常情况下,可以通过wpa_cli来进行WIFI的配置与连接,如果有特殊的需要,可以编写应用程序直接调用wpa_supplicant的接口直接开发。

启动wpa_supplicant应用

1
$ wpa_supplicant -D nl80211 -i wlan0 -c /etc/wpa_supplicant.conf -B

/etc/wpa_supplicant.conf文件里,添加下面代码:

1
ctrl_interface=/var/run/wpa_supplicant update_config=1

启动wpa_cli应用

1
2
3
$ wpa_cli -i wlan0 scan # 搜索附近wifi网络 
$ wpa_cli -i wlan0 scan_result # 打印搜索wifi网络结果
$ wpa_cli -i wlan0 add_network # 添加一个网络连接

如果要连接加密方式是[WPA-PSK-CCMP+TKIP][WPA2-PSK-CCMP+TKIP][ESS] (wpa加密),wifi名称是namewifi密码是:psk

1
2
3
$ wpa_cli -i wlan0 set_network 0 ssid '"name"' 
$ wpa_cli -i wlan0 set_network 0 psk '"psk"'
$ wpa_cli -i wlan0 enable_network 0

如果要连接加密方式是[WEP][ESS] (wep加密),wifi名称是namewifi密码是psk

1
2
3
4
$ wpa_cli -i wlan0 set_network 0 ssid '"name"' 
$ wpa_cli -i wlan0 set_network 0 key_mgmt NONE
$ wpa_cli -i wlan0 set_network 0 wep_key0 '"psk"'
$ wpa_cli -i wlan0 enable_network 0

如果要连接加密方式是[ESS] (无加密),wifi名称是name

1
2
3
$ wpa_cli -i wlan0 set_network 0 ssid '"name"' 
$ wpa_cli -i wlan0 set_network 0 key_mgmt NONE
$ wpa_cli -i wlan0 enable_network 0

分配ip/netmask/gateway/dns

1
$ udhcpc -i wlan0 -s /etc/udhcpc.script -q

执行完毕,就可以连接网络了。

保存连接

1
$ wpa_cli -i wlan0 save_config

断开连接

1
$ wpa_cli -i wlan0 disable_network 0

连接已有的连接

1
2
3
$ wpa_cli -i wlan0 list_network #列举所有保存的连接 
$ wpa_cli -i wlan0 select_network 0 #连接第1个保存的连接
$ wpa_cli -i wlan0 enable_network 0 #使能第1个保存的连接

断开wifi

1
2
3
$ ifconfig wlan0 down 
$ killall udhcpc
$ killall wpa_supplicant

wpa_wifi_tool使用方法

wpa_wifi_tool是基于wpa_supplicantwpa_cli的一个用于快速设置wifi的工具,方便调试时连接wifi使用。使用方法:1、运行wpa_wifi_tool;2、输入help进行命令查看;3、s进行SSID扫描;4、c[n]进行wifi连接,连接时若为新的SSID则需输入密码,若为已保存的SSID则可以使用保存过的密码或者重新输入密码;5、e退出工具。

一、下载链接https://portal.influxdata.com/downloads,选windows版

二、解压到安装盘,目录如下

三、修改conf文件,代码如下,直接复制粘贴(1.4.2版本),注意修改路径,带D盘的改为你的安装路径就好,一共三个,注意网上有配置admin进行web管理,但新版本配置文件里没有admin因为官方给删除了,需下载Chronograf,后文会介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
### Welcome to the InfluxDB configuration file.

# The values in this file override the default values used by the system if
# a config option is not specified. The commented out lines are the configuration
# field and the default value used. Uncommenting a line and changing the value
# will change the value used at runtime when the process is restarted.

# Once every 24 hours InfluxDB will report usage data to usage.influxdata.com
# The data includes a random ID, os, arch, version, the number of series and other
# usage data. No data from user databases is ever transmitted.
# Change this option to true to disable reporting.
# reporting-disabled = false

# Bind address to use for the RPC service for backup and restore.
# bind-address = "127.0.0.1:8088"

###
### [meta]
###
### Controls the parameters for the Raft consensus group that stores metadata
### about the InfluxDB cluster.
###

[meta]
# Where the metadata/raft database is stored
dir = "D:/influxdb-1.4.2-1/meta"

# Automatically create a default retention policy when creating a database.
retention-autocreate = true

# If log messages are printed for the meta service
logging-enabled = true

###
### [data]
###
### Controls where the actual shard data for InfluxDB lives and how it is
### flushed from the WAL. "dir" may need to be changed to a suitable place
### for your system, but the WAL settings are an advanced configuration. The
### defaults should work for most systems.
###

[data]
# The directory where the TSM storage engine stores TSM files.
dir = "D:/influxdb-1.4.2-1/data"

# The directory where the TSM storage engine stores WAL files.
wal-dir = "D:/influxdb-1.4.2-1/wal"

# The amount of time that a write will wait before fsyncing. A duration
# greater than 0 can be used to batch up multiple fsync calls. This is useful for slower
# disks or when WAL write contention is seen. A value of 0s fsyncs every write to the WAL.
# Values in the range of 0-100ms are recommended for non-SSD disks.
# wal-fsync-delay = "0s"


# The type of shard index to use for new shards. The default is an in-memory index that is
# recreated at startup. A value of "tsi1" will use a disk based index that supports higher
# cardinality datasets.
# index-version = "inmem"

# Trace logging provides more verbose output around the tsm engine. Turning
# this on can provide more useful output for debugging tsm engine issues.
# trace-logging-enabled = false

# Whether queries should be logged before execution. Very useful for troubleshooting, but will
# log any sensitive data contained within a query.
query-log-enabled = true

# Settings for the TSM engine

# CacheMaxMemorySize is the maximum size a shard's cache can
# reach before it starts rejecting writes.
# Valid size suffixes are k, m, or g (case insensitive, 1024 = 1k).
# Vaues without a size suffix are in bytes.
# cache-max-memory-size = "1g"

# CacheSnapshotMemorySize is the size at which the engine will
# snapshot the cache and write it to a TSM file, freeing up memory
# Valid size suffixes are k, m, or g (case insensitive, 1024 = 1k).
# Values without a size suffix are in bytes.
# cache-snapshot-memory-size = "25m"

# CacheSnapshotWriteColdDuration is the length of time at
# which the engine will snapshot the cache and write it to
# a new TSM file if the shard hasn't received writes or deletes
# cache-snapshot-write-cold-duration = "10m"

# CompactFullWriteColdDuration is the duration at which the engine
# will compact all TSM files in a shard if it hasn't received a
# write or delete
# compact-full-write-cold-duration = "4h"

# The maximum number of concurrent full and level compactions that can run at one time. A
# value of 0 results in 50% of runtime.GOMAXPROCS(0) used at runtime. Any number greater
# than 0 limits compactions to that value. This setting does not apply
# to cache snapshotting.
# max-concurrent-compactions = 0

# The maximum series allowed per database before writes are dropped. This limit can prevent
# high cardinality issues at the database level. This limit can be disabled by setting it to
# 0.
# max-series-per-database = 1000000

# The maximum number of tag values per tag that are allowed before writes are dropped. This limit
# can prevent high cardinality tag values from being written to a measurement. This limit can be
# disabled by setting it to 0.
# max-values-per-tag = 100000

###
### [coordinator]
###
### Controls the clustering service configuration.
###

[coordinator]
# The default time a write request will wait until a "timeout" error is returned to the caller.
# write-timeout = "10s"

# The maximum number of concurrent queries allowed to be executing at one time. If a query is
# executed and exceeds this limit, an error is returned to the caller. This limit can be disabled
# by setting it to 0.
# max-concurrent-queries = 0

# The maximum time a query will is allowed to execute before being killed by the system. This limit
# can help prevent run away queries. Setting the value to 0 disables the limit.
# query-timeout = "0s"

# The time threshold when a query will be logged as a slow query. This limit can be set to help
# discover slow or resource intensive queries. Setting the value to 0 disables the slow query logging.
# log-queries-after = "0s"

# The maximum number of points a SELECT can process. A value of 0 will make
# the maximum point count unlimited. This will only be checked every second so queries will not
# be aborted immediately when hitting the limit.
# max-select-point = 0

# The maximum number of series a SELECT can run. A value of 0 will make the maximum series
# count unlimited.
# max-select-series = 0

# The maxium number of group by time bucket a SELECT can create. A value of zero will max the maximum
# number of buckets unlimited.
# max-select-buckets = 0

###
### [retention]
###
### Controls the enforcement of retention policies for evicting old data.
###

[retention]
# Determines whether retention policy enforcement enabled.
enabled = true

# The interval of time when retention policy enforcement checks run.
check-interval = "30m"

###
### [shard-precreation]
###
### Controls the precreation of shards, so they are available before data arrives.
### Only shards that, after creation, will have both a start- and end-time in the
### future, will ever be created. Shards are never precreated that would be wholly
### or partially in the past.

[shard-precreation]
# Determines whether shard pre-creation service is enabled.
enabled = true

# The interval of time when the check to pre-create new shards runs.
check-interval = "10m"

# The default period ahead of the endtime of a shard group that its successor
# group is created.
advance-period = "30m"

###
### Controls the system self-monitoring, statistics and diagnostics.
###
### The internal database for monitoring data is created automatically if
### if it does not already exist. The target retention within this database
### is called 'monitor' and is also created with a retention period of 7 days
### and a replication factor of 1, if it does not exist. In all cases the
### this retention policy is configured as the default for the database.

[monitor]
# Whether to record statistics internally.
store-enabled = true

# The destination database for recorded statistics
store-database = "_internal"

# The interval at which to record statistics
store-interval = "10s"

###
### [http]
###
### Controls how the HTTP endpoints are configured. These are the primary
### mechanism for getting data into and out of InfluxDB.
###

[http]
# Determines whether HTTP endpoint is enabled.
enabled = true

# The bind address used by the HTTP service.
bind-address = ":8086"

# Determines whether user authentication is enabled over HTTP/HTTPS.
# auth-enabled = false

# The default realm sent back when issuing a basic auth challenge.
# realm = "InfluxDB"

# Determines whether HTTP request logging is enabled.
# log-enabled = true

# Determines whether detailed write logging is enabled.
# write-tracing = false

# Determines whether the pprof endpoint is enabled. This endpoint is used for
# troubleshooting and monitoring.
# pprof-enabled = true

# Determines whether HTTPS is enabled.
# https-enabled = false

# The SSL certificate to use when HTTPS is enabled.
# https-certificate = "/etc/ssl/influxdb.pem"

# Use a separate private key location.
# https-private-key = ""

# The JWT auth shared secret to validate requests using JSON web tokens.
# shared-secret = ""

# The default chunk size for result sets that should be chunked.
# max-row-limit = 0

# The maximum number of HTTP connections that may be open at once. New connections that
# would exceed this limit are dropped. Setting this value to 0 disables the limit.
# max-connection-limit = 0

# Enable http service over unix domain socket
# unix-socket-enabled = false

# The path of the unix domain socket.
# bind-socket = "/var/run/influxdb.sock"

# The maximum size of a client request body, in bytes. Setting this value to 0 disables the limit.
# max-body-size = 25000000


###
### [ifql]
###
### Configures the ifql RPC API.
###

[ifql]
# Determines whether the RPC service is enabled.
# enabled = true

# Determines whether additional logging is enabled.
# log-enabled = true

# The bind address used by the ifql RPC service.
# bind-address = ":8082"


###
### [subscriber]
###
### Controls the subscriptions, which can be used to fork a copy of all data
### received by the InfluxDB host.
###

[subscriber]
# Determines whether the subscriber service is enabled.
# enabled = true

# The default timeout for HTTP writes to subscribers.
# http-timeout = "30s"

# Allows insecure HTTPS connections to subscribers. This is useful when testing with self-
# signed certificates.
# insecure-skip-verify = false

# The path to the PEM encoded CA certs file. If the empty string, the default system certs will be used
# ca-certs = ""

# The number of writer goroutines processing the write channel.
# write-concurrency = 40

# The number of in-flight writes buffered in the write channel.
# write-buffer-size = 1000


###
### [[graphite]]
###
### Controls one or many listeners for Graphite data.
###

[[graphite]]
# Determines whether the graphite endpoint is enabled.
# enabled = false
# database = "graphite"
# retention-policy = ""
# bind-address = ":2003"
# protocol = "tcp"
# consistency-level = "one"

# These next lines control how batching works. You should have this enabled
# otherwise you could get dropped metrics or poor performance. Batching
# will buffer points in memory if you have many coming in.

# Flush if this many points get buffered
# batch-size = 5000

# number of batches that may be pending in memory
# batch-pending = 10

# Flush at least this often even if we haven't hit buffer limit
# batch-timeout = "1s"

# UDP Read buffer size, 0 means OS default. UDP listener will fail if set above OS max.
# udp-read-buffer = 0

### This string joins multiple matching 'measurement' values providing more control over the final measurement name.
# separator = "."

### Default tags that will be added to all metrics. These can be overridden at the template level
### or by tags extracted from metric
# tags = ["region=us-east", "zone=1c"]

### Each template line requires a template pattern. It can have an optional
### filter before the template and separated by spaces. It can also have optional extra
### tags following the template. Multiple tags should be separated by commas and no spaces
### similar to the line protocol format. There can be only one default template.
# templates = [
# "*.app env.service.resource.measurement",
# # Default template
# "server.*",
# ]

###
### [collectd]
###
### Controls one or many listeners for collectd data.
###

[[collectd]]
# enabled = false
# bind-address = ":25826"
# database = "collectd"
# retention-policy = ""
#
# The collectd service supports either scanning a directory for multiple types
# db files, or specifying a single db file.
# typesdb = "/usr/local/share/collectd"
#
# security-level = "none"
# auth-file = "/etc/collectd/auth_file"

# These next lines control how batching works. You should have this enabled
# otherwise you could get dropped metrics or poor performance. Batching
# will buffer points in memory if you have many coming in.

# Flush if this many points get buffered
# batch-size = 5000

# Number of batches that may be pending in memory
# batch-pending = 10

# Flush at least this often even if we haven't hit buffer limit
# batch-timeout = "10s"

# UDP Read buffer size, 0 means OS default. UDP listener will fail if set above OS max.
# read-buffer = 0

# Multi-value plugins can be handled two ways.
# "split" will parse and store the multi-value plugin data into separate measurements
# "join" will parse and store the multi-value plugin as a single multi-value measurement.
# "split" is the default behavior for backward compatability with previous versions of influxdb.
# parse-multivalue-plugin = "split"
###
### [opentsdb]
###
### Controls one or many listeners for OpenTSDB data.
###

[[opentsdb]]
# enabled = false
# bind-address = ":4242"
# database = "opentsdb"
# retention-policy = ""
# consistency-level = "one"
# tls-enabled = false
# certificate= "/etc/ssl/influxdb.pem"

# Log an error for every malformed point.
# log-point-errors = true

# These next lines control how batching works. You should have this enabled
# otherwise you could get dropped metrics or poor performance. Only points
# metrics received over the telnet protocol undergo batching.

# Flush if this many points get buffered
# batch-size = 1000

# Number of batches that may be pending in memory
# batch-pending = 5

# Flush at least this often even if we haven't hit buffer limit
# batch-timeout = "1s"

###
### [[udp]]
###
### Controls the listeners for InfluxDB line protocol data via UDP.
###

[[udp]]
# enabled = false
# bind-address = ":8089"
# database = "udp"
# retention-policy = ""

# These next lines control how batching works. You should have this enabled
# otherwise you could get dropped metrics or poor performance. Batching
# will buffer points in memory if you have many coming in.

# Flush if this many points get buffered
# batch-size = 5000

# Number of batches that may be pending in memory
# batch-pending = 10

# Will flush at least this often even if we haven't hit buffer limit
# batch-timeout = "1s"

# UDP Read buffer size, 0 means OS default. UDP listener will fail if set above OS max.
# read-buffer = 0

###
### [continuous_queries]
###
### Controls how continuous queries are run within InfluxDB.
###

[continuous_queries]
# Determines whether the continuous query service is enabled.
# enabled = true

# Controls whether queries are logged when executed by the CQ service.
# log-enabled = true

# Controls whether queries are logged to the self-monitoring data store.
# query-stats-enabled = false

# interval for how often continuous queries will be checked if they need to run
# run-interval = "1s"

四、使配置生效并打开数据库连接,双击influxd.exe就好,然后双击influx.exe进行操作,网上有操作教程,注意操作数据库时不能关闭influxd.exe,我不知道为什么总有这么个提示:There was an error writing history file: open : The system cannot find the file specified.不过好像没啥影响

五、要使用web管理需要下载Chronograf,https://portal.influxdata.com/downloads第三个就是,下载完直接解压,双击exe程序,在浏览器输入http://localhost:8888/,一开始登录要账户密码,我都用admin就进去了

这个是查看建立的数据库

这个是查看数据库的数据

没了

关于子仓库或者说是仓库共用,git官方推荐的工具是git subtree。 我自己也用了一段时间的git subtree,感觉比git submodule好用,但是也有一些缺点,在可接受的范围内。
所以对于仓库共用,在git subtree 与 git submodule之中选择的话,我推荐git subtree。

git subtree 可以实现一个仓库作为其他仓库的子仓库。

使用git subtree 有以下几个原因:

  • 旧版本的git也支持(最老版本可以到 v1.5.2).
  • git subtree与git submodule不同,它不增加任何像.gitmodule这样的新的元数据文件.
  • git subtree对于项目中的其他成员透明,意味着可以不知道git subtree的存在.

当然,git subtree也有它的缺点,但是这些缺点还在可以接受的范围内:

  • 必须学习新的指令(如:git subtree).
  • 子仓库的更新与推送指令相对复杂。

git subtree的主要命令有:

1
2
3
4
5
6
git subtree add   --prefix=<prefix> <commit>
git subtree add --prefix=<prefix> <repository> <ref>
git subtree pull --prefix=<prefix> <repository> <ref>
git subtree push --prefix=<prefix> <repository> <ref>
git subtree merge --prefix=<prefix> <commit>
git subtree split --prefix=<prefix> [OPTIONS] [<commit>]

准备

我们先准备一个仓库叫photoshop,一个仓库叫libpng,然后我们希望把libpng作为photoshop的子仓库。
photoshop的路径为https://github.com/test/photoshop.git,仓库里的文件有:

1
2
3
4
5
6
photoshop
|
|-- photoshop.c
|-- photoshop.h
|-- main.c
\-- README.md

libPNG的路径为https://github.com/test/libpng.git,仓库里的文件有:

1
2
3
4
5
libpng
|
|-- libpng.c
|-- libpng.h
\-- README.md

以下操作均位于父仓库的根目录中。

在父仓库中新增子仓库

我们执行以下命令把libpng添加到photoshop中:

1
git subtree add --prefix=sub/libpng https://github.com/test/libpng.git master --squash

(--squash参数表示不拉取历史信息,而只生成一条commit信息。)

执行git status可以看到提示新增两条commit:

image

git log查看详细修改:

image

执行git push把修改推送到远端photoshop仓库,现在本地仓库与远端仓库的目录结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
photoshop
|
|-- sub/
| |
| \--libpng/
| |
| |-- libpng.c
| |-- libpng.h
| \-- README.md
|
|-- photoshop.c
|-- photoshop.h
|-- main.c
\-- README.md

注意,现在的photoshop仓库对于其他项目人员来说,可以不需要知道libpng是一个子仓库。什么意思呢?
当你git clone或者git pull的时候,你拉取到的是整个photoshop(包括libpng在内,libpng就相当于photoshop里的一个普通目录);当你修改了libpng里的内容后执行git push,你将会把修改push到photoshop上。
也就是说photoshop仓库下的libpng与其他文件无异。

从源仓库拉取更新

如果源libpng仓库更新了,photoshop里的libpng如何拉取更新?使用git subtree pull,例如:

1
git subtree pull --prefix=sub/libpng https://github.com/test/libpng.git master --squash

推送修改到源仓库

如果在photoshop仓库里修改了libpng,然后想把这个修改推送到源libpng仓库呢?使用git subtree push,例如:

1
git subtree push --prefix=sub/libpng https://github.com/test/libpng.git master

简化git subtree命令

我们已经知道了git subtree 的命令的基本用法,但是上述几个命令还是显得有点复杂,特别是子仓库的源仓库地址,特别不方便记忆。
这里我们把子仓库的地址作为一个remote,方便记忆:

1
git remote add -f libpng https://github.com/test/libpng.git

然后可以这样来使用git subtree命令:

1
2
3
git subtree add --prefix=sub/libpng libpng master --squash
git subtree pull --prefix=sub/libpng libpng master --squash
git subtree push --prefix=sub/libpng libpng master

InfluxDB是一个开源的时序数据库,使用GO语言开发,特别适合用于处理和分析资源监控数据这种时序相关数据。而InfluxDB自带的各种特殊函数如求标准差,随机取样数据,统计数据变化比等,使数据统计和实时分析变得十分方便。在我们的容器资源监控系统中,就采用了InfluxDB存储cadvisor的监控数据。本文对InfluxDB的基本概念和一些特色功能做一个详细介绍,内容主要是翻译整理自官网文档,如有错漏,请指正。

这里说一下使用docker容器运行influxdb的步骤,物理机安装请参照官方文档。拉取镜像文件后运行即可,当前最新版本是1.3.5。启动容器时设置挂载的数据目录和开放端口。InfluxDB的操作语法InfluxQL与SQL基本一致,也提供了一个类似mysql-client的名为influx的CLI。InfluxDB本身是支持分布式部署多副本存储的,本文介绍都是针对的单节点单副本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17


f216e9be15bff545befecb30d1d275552026216a939cc20c042b17419e3bde31

root@f216e9be15bf:/
Connected to http:
InfluxDB shell version: 1.3.5
> create database cadvisor
> show databases
name: databases
name
----
_internal
cadvisor
> CREATE USER testuser WITH PASSWORD 'testpwd'
> GRANT ALL PRIVILEGES ON cadvisor TO testuser
> CREATE RETENTION POLICY "cadvisor_retention" ON "cadvisor" DURATION 30d REPLICATION 1 DEFAULT

influxdb里面有一些重要概念:database,timestamp,field key, field value, field set,tag key,tag value,tag set,measurement, retention policy ,series,point。结合下面的例子数据来说明这几个概念:

1
2
3
4
5
6
7
8
9
10
11
name: census
-————————————
time butterflies honeybees location scientist
2015-08-18T00:00:00Z 12 23 1 langstroth
2015-08-18T00:00:00Z 1 30 1 perpetua
2015-08-18T00:06:00Z 11 28 1 langstroth
2015-08-18T00:06:00Z 3 28 1 perpetua
2015-08-18T05:54:00Z 2 11 2 langstroth
2015-08-18T06:00:00Z 1 10 2 langstroth
2015-08-18T06:06:00Z 8 23 2 perpetua
2015-08-18T06:12:00Z 7 22 2 perpetua

timestamp

既然是时间序列数据库,influxdb的数据都有一列名为time的列,里面存储UTC时间戳。

field key,field value,field set

butterflies和honeybees两列数据称为字段(fields),influxdb的字段由field key和field value组成。其中butterflies和honeybees为field key,它们为string类型,用于存储元数据。

而butterflies这一列的数据12-7为butterflies的field value,同理,honeybees这一列的23-22为honeybees的field value。field value可以为string,float,integer或boolean类型。field value通常都是与时间关联的。

field key和field value对组成的集合称之为field set。如下:

1
2
3
4
5
6
7
8
butterflies = 12 honeybees = 23
butterflies = 1 honeybees = 30
butterflies = 11 honeybees = 28
butterflies = 3 honeybees = 28
butterflies = 2 honeybees = 11
butterflies = 1 honeybees = 10
butterflies = 8 honeybees = 23
butterflies = 7 honeybees = 22

在influxdb中,字段必须存在。注意,字段是没有索引的。如果使用字段作为查询条件,会扫描符合查询条件的所有字段值,性能不及tag。类比一下,fields相当于SQL的没有索引的列。

tag key,tag value,tag set

location和scientist这两列称为标签(tags),标签由tag key和tag value组成。location这个tag key有两个tag value:1和2,scientist有两个tag value:langstroth和perpetua。tag key和tag value对组成了tag set,示例中的tag set如下:

1
2
3
4
location = 1, scientist = langstroth
location = 2, scientist = langstroth
location = 1, scientist = perpetua
location = 2, scientist = perpetua

tags是可选的,但是强烈建议你用上它,因为tag是有索引的,tags相当于SQL中的有索引的列。tag value只能是string类型 如果你的常用场景是根据butterflies和honeybees来查询,那么你可以将这两个列设置为tag,而其他两列设置为field,tag和field依据具体查询需求来定。

measurement

measurement是fields,tags以及time列的容器,measurement的名字用于描述存储在其中的字段数据,类似mysql的表名。如上面例子中的measurement为census。measurement相当于SQL中的表,本文中我在部分地方会用表来指代measurement。

retention policy

retention policy指数据保留策略,示例数据中的retention policy为默认的autogen。它表示数据一直保留永不过期,副本数量为1。你也可以指定数据的保留时间,如30天。

series

series是共享同一个retention policy,measurement以及tag set的数据集合。示例中数据有4个series,如下:

Arbitrary series number

Retention policy

Measurement

Tag set

series 1

autogen

census

location = 1,scientist = langstroth

series 2

autogen

census

location = 2,scientist = langstroth

series 3

autogen

census

location = 1,scientist = perpetua

series 4

autogen

census

location = 2,scientist = perpetua

point

point则是同一个series中具有相同时间的field set,points相当于SQL中的数据行。如下面就是一个point:

1
2
3
4
name: census
-----------------
time butterflies honeybees location scientist
2015-08-18T00:00:00Z 1 30 1 perpetua

database

上面提到的结构都存储在数据库中,示例的数据库为my_database。一个数据库可以有多个measurement,retention policy, continuous queries以及user。influxdb是一个无模式的数据库,可以很容易的添加新的measurement,tags,fields等。而它的操作却和传统的数据库一样,可以使用类SQL语言查询和修改数据。

influxdb不是一个完整的CRUD数据库,它更像是一个CR-ud数据库。它优先考虑的是增加和读取数据而不是更新和删除数据的性能,而且它阻止了某些更新和删除行为使得创建和读取数据更加高效。

influxdb函数分为聚合函数,选择函数,转换函数,预测函数等。除了与普通数据库一样提供了基本操作函数外,还提供了一些特色函数以方便数据统计计算,下面会一一介绍其中一些常用的特色函数。

  • 聚合函数:FILL(), INTEGRAL()SPREAD()STDDEV()MEAN(), MEDIAN()等。
  • 选择函数: SAMPLE(), PERCENTILE(), FIRST(), LAST(), TOP(), BOTTOM()等。
  • 转换函数: DERIVATIVE(), DIFFERENCE()等。
  • 预测函数:HOLT_WINTERS()

先从官网导入测试数据(注:这里测试用的版本是1.3.1,最新版本是1.3.5):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ curl https://s3.amazonaws.com/noaa.water-database/NOAA_data.txt -o NOAA_data.txt
$ influx -import -path=NOAA_data.txt -precision=s -database=NOAA_water_database
$ influx -precision rfc3339 -database NOAA_water_database
Connected to http://localhost:8086 version 1.3.1
InfluxDB shell 1.3.1
> show measurements
name: measurements
name
----
average_temperature
distincts
h2o_feet
h2o_pH
h2o_quality
h2o_temperature

> show series from h2o_feet;
key
---
h2o_feet,location=coyote_creek
h2o_feet,location=santa_monica

下面的例子都以官方示例数据库来测试,这里只用部分数据以方便观察。measurement为h2o_feet,tag key为location,field key有level descriptionwater_level两个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> SELECT * FROM "h2o_feet" WHERE time >= '2015-08-17T23:48:00Z' AND time <= '2015-08-18T00:30:00Z'
name: h2o_feet
time level description location water_level
---- ----------------- -------- -----------
2015-08-18T00:00:00Z between 6 and 9 feet coyote_creek 8.12
2015-08-18T00:00:00Z below 3 feet santa_monica 2.064
2015-08-18T00:06:00Z between 6 and 9 feet coyote_creek 8.005
2015-08-18T00:06:00Z below 3 feet santa_monica 2.116
2015-08-18T00:12:00Z between 6 and 9 feet coyote_creek 7.887
2015-08-18T00:12:00Z below 3 feet santa_monica 2.028
2015-08-18T00:18:00Z between 6 and 9 feet coyote_creek 7.762
2015-08-18T00:18:00Z below 3 feet santa_monica 2.126
2015-08-18T00:24:00Z between 6 and 9 feet coyote_creek 7.635
2015-08-18T00:24:00Z below 3 feet santa_monica 2.041
2015-08-18T00:30:00Z between 6 and 9 feet coyote_creek 7.5
2015-08-18T00:30:00Z below 3 feet santa_monica 2.051

GROUP BY,FILL()

如下语句中GROUP BY time(12m),* 表示以每12分钟和tag(location)分组(如果是GROUP BY time(12m)则表示仅每12分钟分组,GROUP BY 参数只能是time和tag)。然后fill(200)表示如果这个时间段没有数据,以200填充,mean(field_key)求该范围内数据的平均值(注意:这是依据series来计算。其他还有SUM求和,MEDIAN求中位数)。LIMIT 7表示限制返回的point(记录数)最多为7条,而SLIMIT 1则是限制返回的series为1个。

注意这里的时间区间,起始时间为整点前包含这个区间第一个12m的时间,比如这里为 2015-08-17T:23:48:00Z,第一条为 2015-08-17T23:48:00Z <= t < 2015-08-18T00:00:00Z这个区间的location=coyote_creekwater_level的平均值,这里没有数据,于是填充的200。第二条为 2015-08-18T00:00:00Z <= t < 2015-08-18T00:12:00Z区间的location=coyote_creekwater_level平均值,这里为 (8.12+8.005)/ 2 = 8.0625,其他以此类推。

GROUP BY time(10m)则表示以10分钟分组,起始时间为包含这个区间的第一个10m的时间,即 2015-08-17T23:40:00Z。默认返回的是第一个series,如果要计算另外那个series,可以在SQL语句后面加上 SOFFSET 1

那如果时间小于数据本身采集的时间间隔呢,比如GROUP BY time(10s)呢?这样的话,就会按10s取一个点,没有数值的为空或者FILL填充,对应时间点有数据则保持不变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
## GROUP BY time(12m)
> SELECT mean("water_level") FROM "h2o_feet" WHERE time >= '2015-08-17T23:48:00Z' AND time <= '2015-08-18T00:30:00Z' GROUP BY time(12m),* fill(200) LIMIT 7 SLIMIT 1
name: h2o_feet
tags: location=coyote_creek
time mean
---- ----
2015-08-17T23:48:00Z 200
2015-08-18T00:00:00Z 8.0625
2015-08-18T00:12:00Z 7.8245
2015-08-18T00:24:00Z 7.5675

## GROUP BY time(10m),SOFFSET设置为1
> SELECT mean("water_level") FROM "h2o_feet" WHERE time >= '2015-08-17T23:48:00Z' AND time <= '2015-08-18T00:30:00Z' GROUP BY time(10m),* fill(200) LIMIT 7 SLIMIT 1 SOFFSET 1
name: h2o_feet
tags: location=santa_monica
time mean
---- ----
2015-08-17T23:40:00Z 200
2015-08-17T23:50:00Z 200
2015-08-18T00:00:00Z 2.09
2015-08-18T00:10:00Z 2.077
2015-08-18T00:20:00Z 2.041
2015-08-18T00:30:00Z 2.051

INTEGRAL(field_key, unit)

计算数值字段值覆盖的曲面的面积值并得到面积之和。测试数据如下:

1
2
3
4
5
6
7
8
9
10
11
> SELECT "water_level" FROM "h2o_feet" WHERE "location" = 'santa_monica' AND time >= '2015-08-18T00:00:00Z' AND time <= '2015-08-18T00:30:00Z'

name: h2o_feet
time water_level
---- -----------
2015-08-18T00:00:00Z 2.064
2015-08-18T00:06:00Z 2.116
2015-08-18T00:12:00Z 2.028
2015-08-18T00:18:00Z 2.126
2015-08-18T00:24:00Z 2.041
2015-08-18T00:30:00Z 2.051

使用INTERGRAL计算面积。注意,这个面积就是这些点连接起来后与时间围成的不规则图形的面积,注意unit默认是以1秒计算,所以下面语句计算结果为3732.66=2.028*1800+分割出来的梯形和三角形面积。如果unit改为1分,则结果为3732.66/60 = 62.211。unit为2分,则结果为3732.66/120 = 31.1055。以此类推。

1
2
3
4
5
6
7
8
9
10
11
12
13
# unit为默认的1秒
> SELECT INTEGRAL("water_level") FROM "h2o_feet" WHERE "location" = 'santa_monica' AND time >= '2015-08-18T00:00:00Z' AND time <= '2015-08-18T00:30:00Z'
name: h2o_feet
time integral
---- --------
1970-01-01T00:00:00Z 3732.66

# unit为1分
> SELECT INTEGRAL("water_level", 1m) FROM "h2o_feet" WHERE "location" = 'santa_monica' AND time >= '2015-08-18T00:00:00Z' AND time <= '2015-08-18T00:30:00Z'
name: h2o_feet
time integral
---- --------
1970-01-01T00:00:00Z 62.211

SPREAD(field_key)

计算数值字段的最大值和最小值的差值。

1
2
3
4
5
6
7
8
> SELECT SPREAD("water_level") FROM "h2o_feet" WHERE time >= '2015-08-17T23:48:00Z' AND time <= '2015-08-18T00:30:00Z' GROUP BY time(12m),* fill(18) LIMIT 3 SLIMIT 1 SOFFSET 1
name: h2o_feet
tags: location=santa_monica
time spread
---- ------
2015-08-17T23:48:00Z 18
2015-08-18T00:00:00Z 0.052000000000000046
2015-08-18T00:12:00Z 0.09799999999999986

STDDEV(field_key)

计算字段的标准差。influxdb用的是贝塞尔修正的标准差计算公式 ,如下:

  • mean=(v1+v2+…+vn)/n;
  • stddev = math.sqrt(
    ((v1-mean)2 + (v2-mean)2 + …+(vn-mean)2)/(n-1)
    )
1
2
3
4
5
6
7
8
9
> SELECT STDDEV("water_level") FROM "h2o_feet" WHERE time >= '2015-08-17T23:48:00Z' AND time <= '2015-08-18T00:30:00Z' GROUP BY time(12m),* fill(18) SLIMIT 1;
name: h2o_feet
tags: location=coyote_creek
time stddev
---- ------
2015-08-17T23:48:00Z 18
2015-08-18T00:00:00Z 0.08131727983645186
2015-08-18T00:12:00Z 0.08838834764831845
2015-08-18T00:24:00Z 0.09545941546018377

PERCENTILE(field_key, N)

选取某个字段中大于N%的这个字段值。

如果一共有4条记录,N为10,则10%*4=0.4,四舍五入为0,则查询结果为空。N为20,则 20% * 4 = 0.8,四舍五入为1,选取的是4个数中最小的数。如果N为40,40% * 4 = 1.6,四舍五入为2,则选取的是4个数中第二小的数。由此可以看出N=100时,就跟MAX(field_key)是一样的,而当N=50时,与MEDIAN(field_key)在字段值为奇数个时是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> SELECT PERCENTILE("water_level",20) FROM "h2o_feet" WHERE time >= '2015-08-17T23:48:00Z' AND time <= '2015-08-18T00:30:00Z' GROUP BY time(12m)
name: h2o_feet
time percentile
---- ----------
2015-08-17T23:48:00Z
2015-08-18T00:00:00Z 2.064
2015-08-18T00:12:00Z 2.028
2015-08-18T00:24:00Z 2.041

> SELECT PERCENTILE("water_level",40) FROM "h2o_feet" WHERE time >= '2015-08-17T23:48:00Z' AND time <= '2015-08-18T00:30:00Z' GROUP BY time(12m)
name: h2o_feet
time percentile
---- ----------
2015-08-17T23:48:00Z
2015-08-18T00:00:00Z 2.116
2015-08-18T00:12:00Z 2.126
2015-08-18T00:24:00Z 2.051

SAMPLE(field_key, N)

随机返回field key的N个值。如果语句中有GROUP BY time(),则每组数据随机返回N个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> SELECT SAMPLE("water_level",2) FROM "h2o_feet" WHERE time >= '2015-08-17T23:48:00Z' AND time <= '2015-08-18T00:30:00Z';
name: h2o_feet
time sample
---- ------
2015-08-18T00:00:00Z 2.064
2015-08-18T00:12:00Z 2.028

> SELECT SAMPLE("water_level",2) FROM "h2o_feet" WHERE time >= '2015-08-17T23:48:00Z' AND time <= '2015-08-18T00:30:00Z' GROUP BY time(12m);
name: h2o_feet
time sample
---- ------
2015-08-18T00:06:00Z 2.116
2015-08-18T00:06:00Z 8.005
2015-08-18T00:12:00Z 7.887
2015-08-18T00:18:00Z 7.762
2015-08-18T00:24:00Z 7.635
2015-08-18T00:30:00Z 2.051

CUMULATIVE_SUM(field_key)

计算字段值的递增和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> SELECT CUMULATIVE_SUM("water_level") FROM "h2o_feet" WHERE time >= '2015-08-17T23:48:00Z' AND time <= '2015-08-18T00:30:00Z';
name: h2o_feet
time cumulative_sum
---- --------------
2015-08-18T00:00:00Z 8.12
2015-08-18T00:00:00Z 10.184
2015-08-18T00:06:00Z 18.189
2015-08-18T00:06:00Z 20.305
2015-08-18T00:12:00Z 28.192
2015-08-18T00:12:00Z 30.22
2015-08-18T00:18:00Z 37.982
2015-08-18T00:18:00Z 40.108
2015-08-18T00:24:00Z 47.742999999999995
2015-08-18T00:24:00Z 49.78399999999999
2015-08-18T00:30:00Z 57.28399999999999
2015-08-18T00:30:00Z 59.334999999999994

DERIVATIVE(field_key, unit) 和 NON_NEGATIVE_DERIVATIVE(field_key, unit)

计算字段值的变化比。unit默认为1s,即计算的是1秒内的变化比。

如下面的第一个数据计算方法是 (2.116-2.064)/(6*60) = 0.00014..,其他计算方式同理。虽然原始数据是6m收集一次,但是这里的变化比默认是按秒来计算的。如果要按6m计算,则设置unit为6m即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> SELECT DERIVATIVE("water_level") FROM "h2o_feet" WHERE "location" = 'santa_monica' AND time >= '2015-08-18T00:00:00Z' AND time <= '2015-08-18T00:30:00Z'
name: h2o_feet
time derivative
---- ----------
2015-08-18T00:06:00Z 0.00014444444444444457
2015-08-18T00:12:00Z -0.00024444444444444465
2015-08-18T00:18:00Z 0.0002722222222222218
2015-08-18T00:24:00Z -0.000236111111111111
2015-08-18T00:30:00Z 0.00002777777777777842

> SELECT DERIVATIVE("water_level", 6m) FROM "h2o_feet" WHERE "location" = 'santa_monica' AND time >= '2015-08-18T00:00:00Z' AND time <= '2015-08-18T00:30:00Z'
name: h2o_feet
time derivative
---- ----------
2015-08-18T00:06:00Z 0.052000000000000046
2015-08-18T00:12:00Z -0.08800000000000008
2015-08-18T00:18:00Z 0.09799999999999986
2015-08-18T00:24:00Z -0.08499999999999996
2015-08-18T00:30:00Z 0.010000000000000231

而DERIVATIVE结合GROUP BY time,以及mean可以构造更加复杂的查询,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> SELECT DERIVATIVE(mean("water_level"), 6m) FROM "h2o_feet" WHERE time >= '2015-08-18T00:00:00Z' AND time <= '2015-08-18T00:30:00Z' group by time(12m), *
name: h2o_feet
tags: location=coyote_creek
time derivative
---- ----------
2015-08-18T00:12:00Z -0.11900000000000022
2015-08-18T00:24:00Z -0.12849999999999984

name: h2o_feet
tags: location=santa_monica
time derivative
---- ----------
2015-08-18T00:12:00Z -0.00649999999999995
2015-08-18T00:24:00Z -0.015499999999999847

这个计算其实是先根据GROUP BY time求平均值,然后对这个平均值再做变化比的计算。因为数据是按12分钟分组的,而变化比的unit是6分钟,所以差值除以2(12/6)才得到变化比。如第一个值是 (7.8245-8.0625)/2 = -0.1190

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> SELECT mean("water_level") FROM "h2o_feet" WHERE time >= '2015-08-18T00:00:00Z' AND time <= '2015-08-18T00:30:00Z' group by time(12m), *
name: h2o_feet
tags: location=coyote_creek
time mean
---- ----
2015-08-18T00:00:00Z 8.0625
2015-08-18T00:12:00Z 7.8245
2015-08-18T00:24:00Z 7.5675

name: h2o_feet
tags: location=santa_monica
time mean
---- ----
2015-08-18T00:00:00Z 2.09
2015-08-18T00:12:00Z 2.077
2015-08-18T00:24:00Z 2.0460000000000003

NON_NEGATIVE_DERIVATIVEDERIVATIVE不同的是它只返回的是非负的变化比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> SELECT DERIVATIVE(mean("water_level"), 6m) FROM "h2o_feet" WHERE location='santa_monica' AND time >= '2015-08-18T00:00:00Z' AND time <= '2015-08-18T00:30:00Z' group by time(6m), *
name: h2o_feet
tags: location=santa_monica
time derivative
---- ----------
2015-08-18T00:06:00Z 0.052000000000000046
2015-08-18T00:12:00Z -0.08800000000000008
2015-08-18T00:18:00Z 0.09799999999999986
2015-08-18T00:24:00Z -0.08499999999999996
2015-08-18T00:30:00Z 0.010000000000000231

> SELECT NON_NEGATIVE_DERIVATIVE(mean("water_level"), 6m) FROM "h2o_feet" WHERE location='santa_monica' AND time >= '2015-08-18T00:00:00Z' AND time <= '2015-08-18T00:30:00Z' group by time(6m), *
name: h2o_feet
tags: location=santa_monica
time non_negative_derivative
---- -----------------------
2015-08-18T00:06:00Z 0.052000000000000046
2015-08-18T00:18:00Z 0.09799999999999986
2015-08-18T00:30:00Z 0.010000000000000231

4.1 基本语法

连续查询(CONTINUOUS QUERY,简写为CQ)是指定时自动在实时数据上进行的InfluxQL查询,查询结果可以存储到指定的measurement中。基本语法格式如下:

1
2
3
4
5
6
7
8
CREATE CONTINUOUS QUERY <cq_name> ON <database_name>
BEGIN
<cq_query>
END

cq_query格式:
SELECT <function[s]> INTO <destination_measurement> FROM <measurement> [WHERE <stuff>] GROUP BY time(<interval>)[,<tag_key[s]>]

CQ操作的是实时数据,它使用本地服务器的时间戳、GROUP BY time()时间间隔以及InfluxDB预先设置好的时间范围来确定什么时候开始查询以及查询覆盖的时间范围。注意CQ语句里面的WHERE条件是没有时间范围的,因为CQ会根据GROUP BY time()自动确定时间范围。

CQ执行的时间间隔和GROUP BY time()的时间间隔一样,它在InfluxDB预先设置的时间范围的起始时刻执行。如果GROUP BY time(1h),则单次查询的时间范围为 now()-GROUP BY time(1h)now(),也就是说,如果当前时间为17点,这次查询的时间范围为 16:00到16:59.99999。

下面看几个示例,示例数据如下,这是数据库transportation中名为bus_data的measurement,每15分钟统计一次乘客数和投诉数。数据文件bus_data.txt如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# DDL
CREATE DATABASE transportation

# DML
# CONTEXT-DATABASE: transportation

bus_data,complaints=9 passengers=5 1472367600
bus_data,complaints=9 passengers=8 1472368500
bus_data,complaints=9 passengers=8 1472369400
bus_data,complaints=9 passengers=7 1472370300
bus_data,complaints=9 passengers=8 1472371200
bus_data,complaints=7 passengers=15 1472372100
bus_data,complaints=7 passengers=15 1472373000
bus_data,complaints=7 passengers=17 1472373900
bus_data,complaints=7 passengers=20 1472374800

导入数据,命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
root@f216e9be15bf:/# influx -import -path=bus_data.txt -precision=s
root@f216e9be15bf:/# influx -precision=rfc3339 -database=transportation
Connected to http://localhost:8086 version 1.3.5
InfluxDB shell version: 1.3.5
> select * from bus_data
name: bus_data
time complaints passengers
---- ---------- ----------
2016-08-28T07:00:00Z 9 5
2016-08-28T07:15:00Z 9 8
2016-08-28T07:30:00Z 9 8
2016-08-28T07:45:00Z 9 7
2016-08-28T08:00:00Z 9 8
2016-08-28T08:15:00Z 7 15
2016-08-28T08:30:00Z 7 15
2016-08-28T08:45:00Z 7 17
2016-08-28T09:00:00Z 7 20

示例1 自动缩小取样存储到新的measurement中

对单个字段自动缩小取样并存储到新的measurement中。

1
2
3
4
CREATE CONTINUOUS QUERY "cq_basic" ON "transportation"
BEGIN
SELECT mean("passengers") INTO "average_passengers" FROM "bus_data" GROUP BY time(1h)
END

这个CQ的意思就是对bus_data每小时自动计算取样数据的平均乘客数并存储到 average_passengers中。那么在2016-08-28这天早上会执行如下流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
At 8:00 cq_basic 执行查询,查询时间范围 time >= '7:00' AND time < '08:00'.
cq_basic写入一条记录到 average_passengers:
name: average_passengers
------------------------
time mean
2016-08-28T07:00:00Z 7
At 9:00 cq_basic 执行查询,查询时间范围 time >= '8:00' AND time < '9:00'.
cq_basic写入一条记录到 average_passengers:
name: average_passengers
------------------------
time mean
2016-08-28T08:00:00Z 13.75

# Results
> SELECT * FROM "average_passengers"
name: average_passengers
------------------------
time mean
2016-08-28T07:00:00Z 7
2016-08-28T08:00:00Z 13.75

示例2 自动缩小取样并存储到新的保留策略(Retention Policy)中

1
2
3
4
CREATE CONTINUOUS QUERY "cq_basic_rp" ON "transportation"
BEGIN
SELECT mean("passengers") INTO "transportation"."three_weeks"."average_passengers" FROM "bus_data" GROUP BY time(1h)
END

与示例1类似,不同的是保留的策略不是autogen,而是改成了three_weeks(创建保留策略语法 CREATE RETENTION POLICY "three_weeks" ON "transportation" DURATION 3w REPLICATION 1)。

1
2
3
4
5
6
> SELECT * FROM "transportation"."three_weeks"."average_passengers"
name: average_passengers
------------------------
time mean
2016-08-28T07:00:00Z 7
2016-08-28T08:00:00Z 13.75

示例3 使用后向引用(backreferencing)自动缩小取样并存储到新的数据库中

1
2
3
4
CREATE CONTINUOUS QUERY "cq_basic_br" ON "transportation"
BEGIN
SELECT mean(*) INTO "downsampled_transportation"."autogen".:MEASUREMENT FROM /.*/ GROUP BY time(30m),*
END

使用后向引用语法自动缩小取样并存储到新的数据库中。语法 :MEASUREMENT 用来指代后面的表,而 /.*/则是分别查询所有的表。这句CQ的含义就是每30分钟自动查询transportation的所有表(这里只有bus_data一个表),并将30分钟内数字字段(passengers和complaints)求平均值存储到新的数据库 downsampled_transportation中。

最终结果如下:

1
2
3
4
5
6
7
8
> SELECT * FROM "downsampled_transportation."autogen"."bus_data"
name: bus_data
--------------
time mean_complaints mean_passengers
2016-08-28T07:00:00Z 9 6.5
2016-08-28T07:30:00Z 9 7.5
2016-08-28T08:00:00Z 8 11.5
2016-08-28T08:30:00Z 7 16

示例4 自动缩小取样以及配置CQ的时间范围

1
2
3
4
CREATE CONTINUOUS QUERY "cq_basic_offset" ON "transportation"
BEGIN
SELECT mean("passengers") INTO "average_passengers" FROM "bus_data" GROUP BY time(1h,15m)
END

与前面几个示例不同的是,这里的GROUP BY time(1h, 15m)指定了一个时间偏移,也就是说 cq_basic_offset执行的时间不再是整点,而是往后偏移15分钟。执行流程如下:

1
2
3
4
5
6
7
8
9
10
At 8:15 cq_basic_offset 执行查询的时间范围 time >= '7:15' AND time < '8:15'.
name: average_passengers
------------------------
time mean
2016-08-28T07:15:00Z 7.75
At 9:15 cq_basic_offset 执行查询的时间范围 time >= '8:15' AND time < '9:15'.
name: average_passengers
------------------------
time mean
2016-08-28T08:15:00Z 16.75

最终结果:

1
2
3
4
5
6
> SELECT * FROM "average_passengers"
name: average_passengers
------------------------
time mean
2016-08-28T07:15:00Z 7.75
2016-08-28T08:15:00Z 16.75

4.2 高级语法

InfluxDB连续查询的高级语法如下:

1
2
3
4
5
CREATE CONTINUOUS QUERY <cq_name> ON <database_name>
RESAMPLE EVERY <interval> FOR <interval>
BEGIN
<cq_query>
END

与基本语法不同的是,多了RESAMPLE关键字。高级语法里CQ的执行时间和查询时间范围则与RESAMPLE里面的两个interval有关系。

高级语法中CQ以EVERY interval的时间间隔执行,执行时查询的时间范围则是FOR interval来确定。如果FOR interval为2h,当前时间为17:00,则查询的时间范围为15:00-16:59.999999RESAMPLE的EVERY和FOR两个关键字可以只有一个

示例的数据表如下,比之前的多了几条记录为了示例3和示例4的测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
name: bus_data
--------------
time passengers
2016-08-28T06:30:00Z 2
2016-08-28T06:45:00Z 4
2016-08-28T07:00:00Z 5
2016-08-28T07:15:00Z 8
2016-08-28T07:30:00Z 8
2016-08-28T07:45:00Z 7
2016-08-28T08:00:00Z 8
2016-08-28T08:15:00Z 15
2016-08-28T08:30:00Z 15
2016-08-28T08:45:00Z 17
2016-08-28T09:00:00Z 20

示例1 只配置执行时间间隔

1
2
3
4
5
CREATE CONTINUOUS QUERY "cq_advanced_every" ON "transportation"
RESAMPLE EVERY 30m
BEGIN
SELECT mean("passengers") INTO "average_passengers" FROM "bus_data" GROUP BY time(1h)
END

这里配置了30分钟执行一次CQ,没有指定FOR interval,于是查询的时间范围还是GROUP BY time(1h)指定的一个小时,执行流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
At 8:00, cq_advanced_every 执行时间范围 time >= '7:00' AND time < '8:00'.
name: average_passengers
------------------------
time mean
2016-08-28T07:00:00Z 7
At 8:30, cq_advanced_every 执行时间范围 time >= '8:00' AND time < '9:00'.
name: average_passengers
------------------------
time mean
2016-08-28T08:00:00Z 12.6667
At 9:00, cq_advanced_every 执行时间范围 time >= '8:00' AND time < '9:00'.
name: average_passengers
------------------------
time mean
2016-08-28T08:00:00Z 13.75

需要注意的是,这里的 8点到9点这个区间执行了两次,第一次执行时时8:30,平均值是 (8+15+15)/ 3 = 12.6667,而第二次执行时间是9:00,平均值是 (8+15+15+17) / 4=13.75,而且最后第二个结果覆盖了第一个结果。InfluxDB如何处理重复的记录可以参见这个文档

最终结果:

1
2
3
4
5
6
> SELECT * FROM "average_passengers"
name: average_passengers
------------------------
time mean
2016-08-28T07:00:00Z 7
2016-08-28T08:00:00Z 13.75

示例2 只配置查询时间范围

1
2
3
4
5
CREATE CONTINUOUS QUERY "cq_advanced_for" ON "transportation"
RESAMPLE FOR 1h
BEGIN
SELECT mean("passengers") INTO "average_passengers" FROM "bus_data" GROUP BY time(30m)
END

只配置了时间范围,而没有配置EVERY interval。这样,执行的时间间隔与GROUP BY time(30m)一样为30分钟,而查询的时间范围为1小时,由于是按30分钟分组,所以每次会写入两条记录。执行流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
At 8:00 cq_advanced_for 查询时间范围:time >= '7:00' AND time < '8:00'.
写入两条记录。
name: average_passengers
------------------------
time mean
2016-08-28T07:00:00Z 6.5
2016-08-28T07:30:00Z 7.5
At 8:30 cq_advanced_for 查询时间范围:time >= '7:30' AND time < '8:30'.
写入两条记录。
name: average_passengers
------------------------
time mean
2016-08-28T07:30:00Z 7.5
2016-08-28T08:00:00Z 11.5
At 9:00 cq_advanced_for 查询时间范围:time >= '8:00' AND time < '9:00'.
写入两条记录。
name: average_passengers
------------------------
time mean
2016-08-28T08:00:00Z 11.5
2016-08-28T08:30:00Z 16

需要注意的是,cq_advanced_for每次写入了两条记录,重复的记录会被覆盖。

最终结果:

1
2
3
4
5
6
7
8
> SELECT * FROM "average_passengers"
name: average_passengers
------------------------
time mean
2016-08-28T07:00:00Z 6.5
2016-08-28T07:30:00Z 7.5
2016-08-28T08:00:00Z 11.5
2016-08-28T08:30:00Z 16

示例3 同时配置执行时间间隔和查询时间范围

1
2
3
4
5
CREATE CONTINUOUS QUERY "cq_advanced_every_for" ON "transportation"
RESAMPLE EVERY 1h FOR 90m
BEGIN
SELECT mean("passengers") INTO "average_passengers" FROM "bus_data" GROUP BY time(30m)
END

这里配置了执行间隔为1小时,而查询范围90分钟,最后分组是30分钟,每次插入了三条记录。执行流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
At 8:00 cq_advanced_every_for 查询时间范围 time >= '6:30' AND time < '8:00'.
插入三条记录
name: average_passengers
------------------------
time mean
2016-08-28T06:30:00Z 3
2016-08-28T07:00:00Z 6.5
2016-08-28T07:30:00Z 7.5
At 9:00 cq_advanced_every_for 查询时间范围 time >= '7:30' AND time < '9:00'.
插入三条记录
name: average_passengers
------------------------
time mean
2016-08-28T07:30:00Z 7.5
2016-08-28T08:00:00Z 11.5
2016-08-28T08:30:00Z 16

最终结果:

1
2
3
4
5
6
7
8
9
> SELECT * FROM "average_passengers"
name: average_passengers
------------------------
time mean
2016-08-28T06:30:00Z 3
2016-08-28T07:00:00Z 6.5
2016-08-28T07:30:00Z 7.5
2016-08-28T08:00:00Z 11.5
2016-08-28T08:30:00Z 16

示例4 配置查询时间范围和FILL填充

1
2
3
4
5
CREATE CONTINUOUS QUERY "cq_advanced_for_fill" ON "transportation"
RESAMPLE FOR 2h
BEGIN
SELECT mean("passengers") INTO "average_passengers" FROM "bus_data" GROUP BY time(1h) fill(1000)
END

在前面值配置查询时间范围的基础上,加上FILL填充空的记录。执行流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
At 6:00, cq_advanced_for_fill 查询时间范围:time >= '4:00' AND time < '6:00',没有数据,不填充。

At 7:00, cq_advanced_for_fill 查询时间范围:time >= '5:00' AND time < '7:00'. 写入两条记录,没有数据的时间点填充1000。
------------------------
time mean
2016-08-28T05:00:00Z 1000 <------ fill(1000)
2016-08-28T06:00:00Z 3 <------ average of 2 and 4

[…] At 11:00, cq_advanced_for_fill 查询时间范围:time >= '9:00' AND time < '11:00'.写入两条记录,没有数据的点填充1000。
name: average_passengers
------------------------
2016-08-28T09:00:00Z 20 <------ average of 20
2016-08-28T10:00:00Z 1000 <------ fill(1000)

At 12:00, cq_advanced_for_fill 查询时间范围:time >= '10:00' AND time < '12:00'。没有数据,不填充。

最终结果:

1
2
3
4
5
6
7
8
9
10
> SELECT * FROM "average_passengers"
name: average_passengers
------------------------
time mean
2016-08-28T05:00:00Z 1000
2016-08-28T06:00:00Z 3
2016-08-28T07:00:00Z 7
2016-08-28T08:00:00Z 13.75
2016-08-28T09:00:00Z 20
2016-08-28T10:00:00Z 1000

写在开头:
1看这里的时候,请确保你已将熟悉JavaScript以及了解Vue的语法, Django的语法也略懂一二。
如果不是很了解,请点击这里查看学习文档VueDjango,否则下文可能有些不好理解。
2文章有点长 ,因为包含了一个Index.vue页面。
3第一次写长文章,所以排版很尴尬,请指正。

  1. 安装Vue环境
  2. 安装element-ui组件 使用其组件美化界面
1
npm i element-ui -S ||  npm install element-ui --save
1
2
3
4
5
main.js 
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

Vue.use(ElementUI)
  1. 安装axios 使用其完成前端到后端的请求

由于axios 使用Vue.use(无效),所以要将其绑定在Vue原型上

1
npm install axios --save |  brew install axios --save
1
2
3
4
5
import axios from 'axios'

axios.defaults.baseURL = 'http://localhost:8000'

Vue.prototype.$axios = axios
  1. 安装Django及配置环境
  2. 配置mysql数据库,使用sqlite3的 跳过此步骤无需配置
1
2
3
4
5
6
7
8
9
10
11
settings.py
DATABASES = {
'default': { #
'ENGINE': 'django.db.backends.mysql', # 不同库有不同的殷勤
'NAME': 'python_use', # 使用的库名
'USER': 'root',
'PASSWORD': '',
'HOST': '127.0.0.1',
'PORT': '3306',
}
}

配置完成后请查看django是否报错,不报错即连接成功

  1. 安装 pipdjango-cors-headers
1
2

pip install django-cors-headers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
settings.py

INSTALLED_APPS = {
...
'corsheaders',
...
}


MIDDLEWARE = [
...

'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
...
]


CORS_ORIGIN_ALLOW_ALL = True





至此,已将Vue和Django安装并配置好,接下来写一个简单的CRUD操作。
请确认你的整个项目目录与此类似

![](Vue + Django/2064404-d3a828d4530715b4.png)

项目目录结构

以下使用的目录均为此图所示


  1. 配置路由
1
2
3
4
5
first/urls.py 
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'', include('crud.urls')),
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
crud/urls.py




from django.conf.urls import url
from . import views

urlpatterns = [
url('create/', views.create, name = 'create'),
url('read', views.read, name = 'read'),
url('update/', views.update, name = 'update'),
url('delete/', views.delete, name = 'delete'),
url('search', views.search, name = 'search')
]
  1. 创建models,即在数据库中创建表
1
2
3
4
5
from django.db import models
class Books ( models.Model ):
book_name = models.CharField( max_length = 255 )
book_price = models.DecimalField( max_digits = 5, decimal_places = 2 )
book_time = models.DateTimeField( '保存日期', auto_now_add = True )

Models创建完成后运行命令 将其应用到数据库中并创建表
如果不懂 请返回顶部阅读Django文档

1
2
python manage.py makemigrations
python manage.py migrate
  1. 编写views.py 完成增删改查的逻辑
1
2
3
4
5
6
7
8
9
10
11
12
# 1 获取前端传递来的参数
# 1.1 get方法发送的参数
request.GET['content']
# 1.2 post方法发送的参数
obj = json.loads(request.body)
name = obj['name']
# 2 由于使用Books.objects下的方法,获取到的数据为Query Set类型,
# 所以需要使用serializers.serialize("json", books)
# 将查询到的数据进行序列化,变成可读的对象。
# 3 向前端返回处理结果
return HttpResponse(json.dumps(res), content_type="application/json")
# 将res变成json字符串返回给前端。
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

from __future__ import unicode_literals
from django.shortcuts import render
from django.http import HttpResponse
import json
from django.core import serializers
from django.utils import timezone
from crud.models import Books

def search(request):
content = request.GET['content']
try:
books = serializers.serialize("json",Books.objects.filter(book_name__contains=content))
res = {
"code": 200,
"data": books
}
print(books)
except Exception,e:
res = {
"code": 0,
"errMsg": e
}
return HttpResponse(json.dumps(res), content_type="application/json")

def create(request):
print('create')
obj = json.loads(request.body)
name = obj['name']
price = obj['price']
try:
book = Books(book_name=name, book_price=price, book_time=timezone.now())
book.save()
res = {
"code": 200,
}
except Exception,e:
res = {
"code": 0,
"errMsg": e
}
return HttpResponse(json.dumps(res), content_type="application/json")

def read(request):
print('read')
try:
res = {
"code": 200,
"data": serializers.serialize("json",Books.objects.filter())
}
except Exception,e:
res = {
"code": 0,
"errMsg": e
}
return HttpResponse(json.dumps(res), content_type="application/json")

def update(request):
print('update')
obj = json.loads(request.body)
pid = obj['id']
name = obj['name']
price = obj['price']
try:
Books.objects.filter(id=pid).update(book_price=price, book_name=name)
res = {
"code": 200
}
except Exception,e:
res = {
"code": 0,
"errMsg": e
}
return HttpResponse(json.dumps(res), content_type="application/json")

def delete(request):
print('delete')
obj = json.loads(request.body)
print(obj)
pid = obj['id']
try:
Books.objects.filter(id=pid).delete()
res = {
"code": 200
}
except Exception,e:
res = {
"code": 0,
"errMsg": e
}
return HttpResponse(json.dumps(res), content_type="application/json")

  1. 配置路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
frontend/src/router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import Index from '@/components/Index'

Vue.use(Router)

export default new Router({
routes: [
{
path: '/',
name: 'index',
component: Index
}
]
})
  1. 编写路由中使用到的组件 与上面import所用名称和路径需要一致,请耐心看完注释。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21








this.$axios.get('/search', {
params: {
content: this.search
}
}).then(res => {
console.log(res)
})
this.$axios.post('/delete/', JSON.stringify(row)).then(res => {

console.log(res)


})

以下为Index.vue的全部页面,包含增删改查的基本操作,以及更改和新增时的弹出框:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
frontend/src/components/Index.vue
<template>
<div>
<el-button type="primary" round @click="handleShowCreate">增加书籍</el-button>
<el-input v-model="search" placeholder="请输入内容" style="width: 200px" @keyup.enter.native="handleSearch"/>
<el-button type="primary" round @click="handleSearch">搜索</el-button>
<el-table :data="booksData" height="250" border style="width: 600px; margin: 40px auto;" v-loading="loading">
<el-table-column
prop="book_name"
label="书名"
align="center"
width="200">
</el-table-column>
<el-table-column
prop="book_price"
label="价格"
align="center"
width="200">
</el-table-column>
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<el-button
size="mini"
@click="handleUpdate(scope.$index, scope.row)">编辑</el-button>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.$index, scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog title="修改书籍" :visible.sync="dialogUpdateVisible">
<el-form :model="updateData">
<el-form-item label="书籍名称">
<el-input auto-complete="off" v-model="updateData.name"></el-input>
</el-form-item>
<el-form-item label="书籍价格">
<el-input-number v-model="updateData.price" :precision="2" :step="0.01" :max="9999"></el-input-number>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleCancel('dialogUpdateVisible')">Cancel</el-button>
<el-button type="primary" @click="handleConfirm('dialogUpdateVisible')">Submit</el-button>
</div>
</el-dialog>
<el-dialog title="增加书籍" :visible.sync="dialogCreateVisible">
<el-form :model="createData">
<el-form-item label="书籍名称">
<el-input auto-complete="off" v-model="createData.name"></el-input>
</el-form-item>
<el-form-item label="书籍价格">
<el-input-number v-model="createData.price" :precision="2" :step="0.01" :max="9999"></el-input-number>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleCancel('dialogCreateVisible')">Cancel</el-button>
<el-button type="primary" @click="handleCreate('dialogCreateVisible')">Submit</el-button>
</div>
</el-dialog>
</div>
</template>

<script>
export default {
name: 'index',
data () {
return {
search: '',
booksData: [],
oldData: {},
updateData: {},
createData: {
name: '',
price: 0
},
dialogUpdateVisible: false,
dialogCreateVisible: false,
loading: true
}
},
methods: {
handleShowCreate () {
this.dialogCreateVisible = true
},
handleCreate () {
if (this.createData.name === '') {
this.$message.error('please input book name')
return
}
if (this.createData.price === 0) {
this.$message.error('please input book price')
return
}
this.$axios.post('/create/', JSON.stringify(this.createData)).then(res => {
if (res.data.code === 200) {
this.$message.success(`create ${this.createData.name} success`)
this.dialogCreateVisible = false
this.handleRead()
} else {
this.$message.error("can't read books database")
}
})
console.log(this.createData)
},
handleRead () {
this.booksData = []
this.$axios.get('/read').then(res => {
this.loading = false
if (res.data.code === 200) {
let books = JSON.parse(res.data.data)
for (let i in books) {
books[i].fields.id = books[i].pk
books[i].fields.book_price = Number(books[i].fields.book_price)
this.booksData.push(books[i].fields)
}
console.log(this.booksData)
} else {
this.$message.console.error("can't read books database")
}
}).catch((res) => {
console.log(res)
})
},
handleUpdate (index, row) {
this.dialogUpdateVisible = true
this.updateData = Object.assign({}, {
id: row.id,
name: row.book_name,
price: row.book_price,
time: row.book_time
})
this.oldData = Object.assign({}, {
id: row.id,
name: row.book_name,
price: row.book_price,
time: row.book_time
})
},
handleDelete (index, row) {
this.$confirm(`are you sure to delete ${this.updateData.name} ?`, '', {
confirmButtonText: 'submit',
cancelButtonText: 'cancel',
type: 'warning'
}).then(() => {
this.$axios.post('/delete/', JSON.stringify(row)).then(res => {
if (res.data.code === 200) {
this.$message.success(`delete ${this.updateData.name} success`)
this.handleRead()
} else {
this.$message.error("can't read books database")
}
})
}).catch(() => {
this.$message.info('cancel delete')
})
},
handleCancel (arg) {
this.$message.info('cancel')
this[arg] = false
},
handleConfirm (arg) {
if (this.updateData.name === this.oldData.name && this.updateData.price === this.oldData.price) {
this.$message.error('please update something or cancel')
return
}
this[arg] = false
this.$axios.post('/update/', JSON.stringify(this.updateData)).then(res => {
if (res.data.code === 200) {
this.$message.success(`update ${this.updateData.name} success`)
this.handleRead()
} else {
this.$message.error("can't read books database")
}
})
},
handleSearch () {
this.$axios.get('search', {
params: {
content: this.search
}
}).then(res => {
if (res.data.code === 200) {
if (res.data.data && JSON.parse(res.data.data).length > 0) {
this.booksData = []
let books = JSON.parse(res.data.data)
for (let i in books) {
let obj = {
id: books[i].pk,
book_name: books[i].fields.book_name,
book_price: Number(books[i].fields.book_price),
book_time: books[i].fields.book_time
}
this.booksData.push(obj)
}
} else {
this.$message.error(`can't search contains of '${this.search}' in database`)
}
} else {
this.$message.error(`can't search books in database`)
}
})
}
},
mounted () {
this.handleRead()
}
}
</script>

到这里,一个增删改查基本操作的页面就写完了,如果哪里有问题可以留言指正。 git源码以上传, 没事可以star/fork 更新将在以下附注后增加。

https://github.com/RogersLei/django-vue


附注 :

  1. Vue添加事件所用到的修饰符:

    ![](Vue + Django/2064404-1aa984b701bf3e11.png)

    Vue事件绑定修饰符

  2. Django中模糊查询用到的语法:

YourModels.objects.filter(headline__contains=str)
字段名__contains / __icontains 忽略大小写

更多精彩内容,就在简书APP

“小礼物走一走,来简书关注我”

还没有人赞赏,支持一下

总资产23共写了2.2W字获得33个赞共22个粉丝

推荐阅读更多精彩内容

  • 一.前言 最近接手了一个项目,后端是django,前端是django自带的模板,用的是jinja2,写了一段时间发…

  • 组织文章借鉴 ——培训师的21项修炼 书籍结构:错误的案例情景重现-抛出问题,传道受业解惑也 我们假设一个场景,大…

  • 每天总是忙忙碌碌,感觉时间完全不够用,更不要说是学习了,可是忙忙碌碌到最后感觉收获也很小,就像大家说的,瞎忙活。…

  • 和姑姑聊起当时借钱给已故父亲治病时的场景,我依稀记得当时我和涛古,妈妈给厂里老板下跪借那三万块的场景。这辈子希望以…

本文整合Django和Vue.js  并引入elementUi 实现前后端分离项目环境

最终的成品是设计出一个ElementUi风格的页面可以添加和显示学生信息.

Django作为Python 三大Web框架之一,因其集成了大量的组件(例如: Models Admin Form 等等)而大受欢迎,但是他本身自带的template模板实在是有点弱.于是考虑整合Vue.js同时引入ElementUI 组件,可以更加快速高效的开发出美观实用的Web页面.

Python

本文版本:Python 3.5

安装教程: https://www.runoob.com/python3/python3-install.html

Pycharm

本文版本:2019.1.3

PyCharm 2019.1.3 (Community Edition)

安装教程:https://www.runoob.com/w3cnote/pycharm-windows-install.html

Django

本文版本:2.2.3

安装教程:https://www.runoob.com/django/django-install.html

node.js

本文版本:10.16.3

安装教程:https://www.runoob.com/nodejs/nodejs-install-setup.html

MySQL

本文版本: 8.0.13 for Win64

安装教程:https://www.runoob.com/mysql/mysql-install.html

本文的Pycharm为社区版,如果为专业版则字段Django项目的创建选项,创建项目将更加简单.

1.创建django项目:DjangoElementUI

创建文件夹E:\PycharmProjects:

在项目文件夹目录输入Windows 命令行如下

1
django-admin.py startproject DjangoElementUI

成功创建项目完成后文件夹结构如下图:

进入项目文件夹目录,在目录中输入命令

1
python manage.py runserver 0.0.0.0:8000

看到如下提示则为项目创建成功

在浏览器输入你服务器的 ip(这里我们输入本机 IP 地址: 127.0.0.1:8000) 及端口号,如果正常启动,输出结果如下:

2.数据库配置

Django 对各种数据库提供了很好的支持,包括:PostgreSQL、MySQL、SQLite、Oracle。

Django 为这些数据库提供了统一的调用API。 我们可以根据自己业务需求选择不同的数据库。

MySQL 是 Web 应用中最常用的数据库。

本文采用MySQL

第一次使用MySQL需要安装 MySQL驱动,在项目文件夹目录下执行以下命令安装:

1
pip install pymysql

Django无法直接创建数据库(只能操作到数据表层),我们需要手工创建MySQL数据库.

以下通过命令行创建 MySQL 数据库:Django_ElementUI

登录数据库:

数据库安装文件夹bin文件夹下输入命令

1
mysql -u root -p 

创建数据库:

1
create DATABASE Django_ElementUI DEFAULT CHARSET utf8;

Django配置数据库

在项目的 settings.py 文件中找到 DATABASES 配置项,将其信息修改为:

1
'ENGINE': 'django.db.backends.mysql',  'NAME': 'Django_ElementUI',  

在与 settings.py 同级目录下的 __init__.py 中引入模块和进行配置 (告诉 Django 使用 pymysql 模块连接 mysql 数据库)

1
pymysql.install_as_MySQLdb()

3.利用Django模型设计数据库表

Django 规定,如果要使用模型,必须要创建一个 app。

创建Django APP:myApp

我们使用以下命令创建一个Django app:myApp

1
django-admin.py startapp myApp

成功后的项目文件夹目录如下:

设计数据库表

在myApp下的models.py设计表:

这里我们设计一个Student表,用来存储学生信息.

表字段

字段类型

含义

student_name

Varchar类型

学生姓名

student_sex

Varchar类型

学生性别

create_time

Datetime类型

创建日期时间

1
from django.db import modelsclass Student(models.Model):    student_name = models.CharField(max_length=64)    student_sex = models.CharField(max_length=3)    create_time = models.DateTimeField(auto_now=True)

在 settings.py 中找到INSTALLED_APPS这一项,如下:

1
'django.contrib.contenttypes','django.contrib.sessions','django.contrib.messages','django.contrib.staticfiles',

生成数据库迁移文件

在命令行中运行:

1
python manage.py makemigrations myApp

执行成功后结果:

执行迁移文件来完成数据库表的创建

在命令行中运行:

1
python manage.py migrate myApp

执行成功后结果:

查看数据库中数据库表已经生成成功

(django默认在makemigrations会为表对象创建主键id,id = models.AutoField(primary_key=True))

4.Django创建新增和查询学生信息接口

在myApp目录下的views.py中创建两个视图函数

1
from __future__ import unicode_literalsfrom django.http import JsonResponsefrom django.core import serializersfrom django.shortcuts import renderfrom django.views.decorators.http import require_http_methodsfrom myApp.models import Student@require_http_methods(["GET"])def add_student(request):        student = Student(student_name=request.GET.get('student_name'))        response['msg'] = 'success'        response['error_num'] = 0        response['error_num'] = 1return JsonResponse(response)@require_http_methods(["GET"])def show_students(request):        students = Student.objects.filter()        response['list'] = json.loads(serializers.serialize("json", students))        response['msg'] = 'success'        response['error_num'] = 0        response['error_num'] = 1return JsonResponse(response)

5.配置路由

1.在myApp目录下,新增一个urls.py文件,用于创建此APP下的分支路由,把新增的两个视图函数添加到路由里面.

1
from django.conf.urls import url    url(r'^add_book/', views.add_book),    url(r'^show_books/', views.show_books),

2.把上面创建的myApp下的分支路由加到DjangoElementUI下的主路由中urls.py.

1
from django.contrib import adminfrom django.urls import pathfrom django.conf.urls import urlfrom django.conf.urls import include    url(r'^admin/', admin.site.urls),    url(r'^api/', include(urls)),

至此Django部分已经完成,总结下我们利用Django完成了数据库的创建,并创建了两个视图函数作为接口给前端调用.

1.安装vue-cli脚手架

在DjangoElementUI根目录下输入命令:

1
npm install -g vue-cli

2.安装好后,新建一个前端工程目录:appfront

在DjangoElementUI项目根目录下输入命令:

1
vue-init webpack appfront

3.进入appfront目录安装vue所需要的依赖

1
npm install

4.安装ElementUI

1
npm i element-ui -S

5.创建新vue页面

在src/component文件夹下新建一个名为Studengt.vue的组件,通过调用之前在Django上写好的api,实现添加学生和展示学生信息的功能.

1
<el-row display="margin-top:10px"><el-input v-model="input" placeholder="请输入学生姓名" style="display:inline-table; width: 30%; float:left"></el-input><el-button type="primary" @click="addStudent()" style="float:left; margin: 2px;">新增</el-button><el-table :data="studentList" style="width: 100%" border><el-table-column prop="id" label="编号" min-width="100"><template scope="scope"> {{ scope.row.pk }} </template><el-table-column prop="student_name" label="姓名" min-width="100"><template scope="scope"> {{ scope.row.fields.student_name }} </template><el-table-column prop="student_sex" label="性别" min-width="100"><template scope="scope"> {{ scope.row.fields.student_sex }} </template><el-table-column prop="add_time" label="添加时间" min-width="100"><template scope="scope"> {{ scope.row.fields.create_time }} </template>this.$http.get('http://127.0.0.1:8000/api/add_student?student_name=' + this.input)var res = JSON.parse(response.bodyText)if (res.error_num === 0) {this.$message.error('新增学生失败,请重试')this.$http.get('http://127.0.0.1:8000/api/show_students')var res = JSON.parse(response.bodyText)if (res.error_num === 0) {this.studentList = res['list']this.$message.error('查询学生失败')<!-- Add "scoped" attribute to limit CSS to this component only -->

6.配置路由

appfront/router文件夹下的index.js中增加页面路由.

1
import Router from 'vue-router'import HelloWorld from '@/components/HelloWorld'import Student from '@/components/Student'export default new Router({

appfront文件夹下的main.js中引入ElementUI并注册.

1
import router from './router'import '../node_modules/element-ui/lib/theme-chalk/index.css'import ElementUI from 'element-ui'Vue.config.productionTip = false

7.打包并启动前端项目

打包vue项目

1
npm run build

启动前端项目

1
npm run dev

出现下面信息则说明我们前端项目已经构建成功.

去浏览器访问页面地址:http://localhost:8080/#/student

出现如下页面说明我们的页面已经成功.

截止到目前,我们已经成功通过Django创建了一个后端服务,通过Vue.js + ElementUI 实现了前端页面的构建,但是他们运行在各自的服务器,而且前端页面还无法调用后端的接口.

接下来我们需要将两个项目真正的整合到一个成一个项目.

1.引入用于HTTP解析的vue-resource

前端vue项目调用后端需要引入vue-resource

在appfront文件下运行命令:

1
npm install 

安装完成后在main.js中引入vue-resource

1
import router from './router'import '../node_modules/element-ui/lib/theme-chalk/index.css'import ElementUI from 'element-ui'import VueResource from 'vue-resource'Vue.config.productionTip = false

2.在Django层注入header

为了让后端可以识别前端需求,我们须要在Django层注入header,用Django的第三方包django-cors-headers来解决跨域问题:

在DjangoElementUI根目录下输入命令:

1
pip install django-cors-headers

在settings.py中增加相关中间件代码

1
'django.middleware.security.SecurityMiddleware','django.contrib.sessions.middleware.SessionMiddleware','corsheaders.middleware.CorsMiddleware',     'django.middleware.common.CommonMiddleware','django.middleware.csrf.CsrfViewMiddleware','django.contrib.auth.middleware.AuthenticationMiddleware','django.contrib.messages.middleware.MessageMiddleware','django.middleware.clickjacking.XFrameOptionsMiddleware',CORS_ORIGIN_ALLOW_ALL = True   

3.修改Django路由

这一步我们通过Django路由配置连接前后端资源.

首先我们把Django的TemplateView指向我们刚才生成的前端dist文件

在DjangoElementUI目录下的urls.py中增加代码:

1
from django.conf.urls import urlfrom django.contrib import adminfrom django.conf.urls import includefrom django.views.generic import TemplateView    url(r'^admin/', admin.site.urls),    url(r'^api/', include(urls)),    url( r'^vue/', TemplateView.as_view( template_name="index.html" ) )

接着修改静态资源文件路径也指向前端appfront 相关文件

在DjangoElementUI目录下的setting.py中增加代码:

1
'BACKEND': 'django.template.backends.django.DjangoTemplates','DIRS': [os.path.join(BASE_DIR, 'appfront/dist')],  'django.template.context_processors.debug','django.template.context_processors.request','django.contrib.auth.context_processors.auth','django.contrib.messages.context_processors.messages',    os.path.join(BASE_DIR, "appfront/dist/static")

3.重新构建前端项目

appfront目录下输入命令:

1
npm run build

重新启动Django项目

1
python manage.py runserver

输入地址:http://localhost:8000/vue/#/student

添加一条记录

至此,大功告成!

此份指南在配置的过程踩过不少坑,以下是踩的印象较深的坑.

1.数据库创建的过程中务必注意大小写的问题,数据库字段和Django的Models页面,View页面和Vue中的组件页面都有关联.很容易一个大小写不注意,导致整个接口无法使用.

2.连接MySQL需要按照对应的包,同时需要在根目录的_ini_.py中引入pymysql

3.在整个环境的搭建过程中VUE环境的搭建需要耗费较长的npm安装时间,需要耐心等待.

4.前后台连接需要在前端引入vue-resource,Django需要引入django-cors-headers

引言

大U的技术课堂 的新年第一课,祝大家新的一年好好学习,天天向上:)

本篇将手把手教你如何快速而优雅的构建前后端分离的项目,想直接上手请往后翻!

目录:

  1. 我为什么要选择Django与VueJS?

  2. Django和VueJS是如何结合起来的?

  3. 实操

  4. 创建 Django 项目

  5. 创建 Django App 做为后端

  6. 创建 VueJS 项目作为前端

  7. 使用 Webpack 处理前端代码

  8. 配置 Django 模板的搜索路径

  9. 配置 Django 静态文件搜索路径

  10. 开发环境

  11. 生产环境(部署到 UCloud)

正文:

我为什么要选择Django与VueJS?

首先介绍一下我看重的点:

Django (MVC框架) - The Web framework for perfectionists with deadlines

  • Python

  • ORM

  • 简单、清晰的配置

  • Admin app

Django 仅因为 Python 的血统,就已经站在了巨人的肩膀上,配置管理( SaltStack、Ansible )
,数据分析( Pandas ),任务队列( Celery ),Restful API( Django REST framework ),HTTP请求( requests ),再加上高度抽象的ORM,功能强大的 Query Expressions,简单清晰的配置,着重提一下堪称神器的自带App: Admin,有了它你再也不用将一些经常变化的配置写在文件里面,每次增删改都重新发布一次,你只需要定义出配置的 data scheme ,只需要几行代码,Django Admin便为你提供美观,并带有权限控制的增删改查界面,而且可以通过ORM为它生成的API来做到定制化的更新,比如直接读某个wiki上的配置,自动的写入数据库,伪代码如下:

1
2
3
4
import pandas as pd
settings = pd.read_html('http://某个gitlab的README 或者 某个redmine wiki')
settings = clean(settings)
update(settings)

最后还可以使用 django-celery 的 celery-beat 按 Interval/crontab 的方式扔更新配置的任务到 celery 队列里面,最最重要的是,这些都可以在Django Admin后台直接配置哦,还不够优雅?请联系我

VueJS (MVVM框架) - Vue.js

  • 数据双向绑定
  • 单文件组件
  • 清晰的生命周期
  • 学习曲线平滑
  • vue-cli

前端是DevOps的弱项,我需要一个 MVVM 框架来提升交互和节约时间,在试过 AngularJS ,ReactJS,VueJS之后我选择了VueJS,因为我觉得写 VueJS 代码的感觉最接近写 Python

着重提一下单文件组件:

特别清晰,一个文件包含且仅包含三块

  1. 前端渲染的模板
  2. 专为此模板写渲染逻辑的
  3. 专为此模板写样式的

这样可以达到什么效果呢?一个文件一个组件,每个组件有它自己的逻辑与样式,你不用关心什么 local 什么 global ,CSS样式加载先后、覆盖问题,因为它是『闭包』的,而且『自给自足』,不知道这样说好不好理解

当然组件之间也是可以通信的,举个例子,我有一个组件叫 ListULB ,使用表格展示了我拥有的所有 ULB (负载均衡),ListULB 做了一件事,从 API 获取 ULB 对象列表并 for 循环展现出来, ListULB 可以放到某个页面里,可以放到弹框里,放到模态框里,任何地方都可以,因为这个组件对外交互的只有API

如果我现在要写一个组件叫 AddVServer ,功能是可以为任意一个 ULB 对象添加VServer,我的写法是将在 AddVServer 组件创建的时候,将 ULB 对象传给 AddVServer 组件,这样AddVServer 组件拿到这个对象,就可以直接根据对象的ID等,创建出当前行的ULB的VServer了,伪代码如下:

1
2
3
4
5
6
<ListULB>
for **ulb_object** in ulbs_list:
{{ ulb_object.name }}
{{ ulb_object.id }}
<AddVServer :current_ulb='**ulb_object**'></AddVServer>
</ListULB>

注意双星号包着的对象,在 ListULB 组件里面是每行的ULB,传给AddServer组件之后,变成了 current_ulb 对象,拿到id为 current_ulb.id 尽情的为它创建 VServer 吧

如果我要为指定 VServer 创建 RServer 呢,一样的

看出来了吧,进行开发之前,前端组件的结构与数据的结构对应起来可以省好多时间,数据驱动前端组件,棒吗?

谁不喜欢优雅的代码呢, 『Data drive everything』 多么的省脑细胞

以上就是我选择Python与VueJS的原因

Django与VueJS是如何结合起来?

  • 首先我选择了VueJS的前端渲染,自然放弃了Django的后端模板引擎渲染
  • 然后业务逻辑放到了前端,放弃了Django的View(其实也就是前后端分离必要的条件)
  • 保留了Django的 Controller (URLconf) 来实现前端路由的父级路由,可以达到不同页面使用不同的前端框架, 页面内部使用各自独有的前端路由的效果,万一老大给你配了前端呢,万一前端只想写 ReactJS 呢
  • 保留了Django的 Model ,前面说了Django的ORM太好用了,而且可以配合Django Admin

所以综合来说就是:

M(Django) + C(Django) + MVVM (VueJS) = M + MVVM + C = MMVVMC

(为了容易理解,并没有使用Django自称的MTV模式理解,感兴趣看看我画的图)

总结:作为以改变世界为己任的 DevOps ,MVC框架后端渲染的柔弱表现力与繁杂的交互已经不能满足我们了,…..省略1000子…..,所以我选择这样构建项目,嗯…

好吧,也该开始了

代码块中的修改都会用爽星号括起来,比如: **changed**

本文为了精简篇幅,默认您已经安装了必要的 命令行界面(CLI),比如 vue-cli等

1. 创建Django项目

命令:

1
django-admin startproject ulb_manager

结构:

1
2
3
4
5
6
7
.
├── manage.py
└── ulb_manager
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py

2. 进入项目根目录,创建一个 app 作为项目后端

命令:

1
2
cd ulb_manager
python manage.py startapp backend

即:app 名叫做 backend

结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
├── backend
│ ├── __init__.py
│ ├── admin.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── manage.py
└── ulb_manager
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py

3. 使用vue-cli创建一个vuejs项目作为项目前端

命令:

1
vue-init webpack frontend

即:项目名叫 frontend

结构:

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
.
├── backend
│ ├── __init__.py
│ ├── admin.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── frontend
│ ├── README.md
│ ├── build
│ │ └── ....
│ ├── config
│ │ ├── dev.env.js
│ │ ├── index.js
│ │ ├── prod.env.js
│ │ └── test.env.js
│ ├── index.html
│ ├── package.json
│ ├── src
│ │ ├── App.vue
│ │ ├── assets
│ │ │ └── logo.png
│ │ ├── components
│ │ │ └── Hello.vue
│ │ └── main.js
│ ├── static
│ └── test
│ └── ...
├── manage.py
└── ulb_manager
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py

结构总结:

可以看到项目根目录有两个新文件夹,一个叫 backend ,一个叫 frontend,分别是:

  • backend Django的一个app
  • frontend Vuejs项目

4. 接下来我们使用 webpack 打包Vusjs项目

命令:

1
2
3
cd frontend
npm install
npm run build

结构:

我引入了一些包,比如element-ui等,你的static里面的内容会不同,没关系 index.html 和 static 文件夹相同就够了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dist
├── index.html
└── static
├── css
│ ├── app.42b821a6fd065652cb86e2af5bf3b5d2.css
│ └── app.42b821a6fd065652cb86e2af5bf3b5d2.css.map
├── fonts
│ ├── element-icons.a61be9c.eot
│ └── element-icons.b02bdc1.ttf
├── img
│ └── element-icons.09162bc.svg
└── js
├── 0.8750b01fa7ffd70f7ba6.js
├── vendor.804853a3a7c622c4cb5b.js
└── vendor.804853a3a7c622c4cb5b.js.map

构建完成会生成一个 文件夹名字叫dist,里面有一个 index.html 和一个 文件夹static ,

5. 使用Django的通用视图 TemplateView

找到项目根 urls.py (即ulb_manager/urls.py),使用通用视图创建最简单的模板控制器,访问 『/』时直接返回 index.html

1
2
3
4
5
urlpatterns = [
url(r'^admin/', admin.site.urls),
**url(r'^$', TemplateView.as_view(template_name="index.html")),**
url(r'^api/', include('backend.urls', namespace='api'))
]

6. 配置Django项目的模板搜索路径

上一步使用了Django的模板系统,所以需要配置一下模板使Django知道从哪里找到index.html

打开 settings.py (ulb_manager/settings.py),找到TEMPLATES配置项,修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
# 'DIRS': [],
**'DIRS': ['frontend/dist']**,
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]

注意这里的 frontend 是VueJS项目目录,dist则是运行 npm run build 构建出的index.html与静态文件夹 static 的父级目录

这时启动Django项目,访问 / 则可以访问index.html,但是还有问题,静态文件都是404错误,下一步我们解决这个问题

7. 配置静态文件搜索路径

打开 settings.py (ulb_manager/settings.py),找到 STATICFILES_DIRS 配置项,配置如下:

1
2
3
4
# Add for vuejs
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "frontend/dist/static"),
]

这样Django不仅可以将/ulb 映射到index.html,而且还可以顺利找到静态文件

此时访问 /ulb 我们可以看到使用Django作为后端的VueJS helloworld

ALL DONE.

8. 开发环境

因为我们使用了Django作为后端,每次修改了前端之后都要重新构建(你可以理解为不编译不能运行)

除了使用Django作为后端,我们还可以在dist目录下面运行以下命令来看效果:

但是问题依然没有解决,我想过检测文件变化来自动构建,但是构建是秒级的,太慢了,所以我直接使用VueJS的开发环境来调试

毫秒,但是有个新问题,使用VueJS的开发环境脱离了Django环境,访问Django写的API,出现了跨域问题,有两种方法解决,一种是在VueJS层上做转发(proxyTable),另一种是在Django层注入header,这里我使用后者,用Django的第三方包 django-cors-headers 来解决跨域问题

安装

1
pip install django-cors-headers

配置(两步)

1. settings.py 修改

1
2
3
4
5
6
7
8
9
10
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
**'corsheaders.middleware.CorsMiddleware',**
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

这里要注意中间件加载顺序,列表是有序的哦

2. settings.py 添加

1
CORS_ORIGIN_ALLOW_ALL = True

至此,我的开发环境就搭建完成了

9. 生产环境部署(部署到 UCloud )

9.1 创建主机

  1. 注册 UCloud - 专业云计算服务商
  2. 点击左侧的 云主机,然后点击 创建主机
  3. 右侧选择 付费方式,点击 立即购买
  4. 在支付确认页面,点击 确认支付

购买成功后回到主机管理列表,如下所示:

这里注意记住你的外网IP,下面的ip替换成你的

9.2 环境搭建与部署

登录主机,用你刚填写的密码:

ssh root@120.132.**.75

CentOS 系统可以使用 yum 安装必要的包

1
2
3
4
5
6
7
8
# 如果你使用git来托管代码的话
yum install git

# 如果你要在服务器上构建前端
yum install nodejs
yum install npm

yum install nginx

我们使用 uwsgi 来处理 Django 请求,使用 nginx 处理 static 文件(即之前 build 之后 dist 里面的static,这里默认前端已经打包好了,如果在服务端打包前端需要安装nodejs,npm等)

安装uWsgi

1
2
3
yum install uwsgi
# 或者
pip install uwsgi

我们使用配置文件启动uwsgi,比较清楚

uwsgi配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
[uwsgi]
socket = 127.0.0.1:9292
stats = 127.0.0.1:9293
workers = 4
# 项目根目录
chdir = /opt/inner_ulb_manager
touch-reload = /opt/inner_ulb_manager
py-auto-reload = 1
# 在项目跟目录和项目同名的文件夹里面的一个文件
module= inner_ulb_manager.wsgi
pidfile = /var/run/inner_ulb_manager.pid
daemonize = /var/log/inner_ulb_manager.log

nginx 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
listen 8888;
server_name 120.132.**.75;
root /opt/inner_ulb_manager;
access_log /var/log/nginx/access_narwhals.log;
error_log /var/log/nginx/error_narwhals.log;

location / {
uwsgi_pass 127.0.0.1:9292;
include /etc/nginx/uwsgi_params;
}
location /static/ {
root /opt/inner_ulb_manager/;
access_log off;
}
location ^~ /admin/ {
uwsgi_pass 127.0.0.1:9292;
include /etc/nginx/uwsgi_params;
}
}

/opt/inner_ulb_manager/static 即为静态文件目录,那么现在我们静态文件还在 frontend/dist 怎么办,不怕,Django给我们提供了命令:

先去settings里面配置:

1
STATIC_ROOT = os.path.join(BASE_DIR, "static")

然后在存在manage.py的目录,即项目跟目录执行:

1
python manage.py collectstatic

这样frontend/dist/static里面的东西就到了项目根目录的static文件夹里面了

那么为什么不直接手动把构建好的dist/static拷过来呢,因为开始提过Django自带的App:admin 也有一些静态文件(css,js等),它会一并collect过来,毕竟nginx只认项目跟目录的静态文件,它不知道django把它自己的需求文件放到哪了

开头说过Django配置灵活,那么我们专门为Django创建一个生产环境的配置 prod.py

prod.py 与 默认 settings.py 同目录

1
2
3
4
5
6
7
8
9
10
11
# 导入公共配置
from .settings import *

# 生产环境关闭DEBUG模式
DEBUG = False

# 生产环境开启跨域
CORS_ORIGIN_ALLOW_ALL = False

# 特别说明,下面这个不需要,因为前端是VueJS构建的,它默认使用static作为静态文件入口,我们nginx配置static为入口即可,保持一致,没Django什么事
STATIC_URL = '/static/'

如何使用这个配置呢,进入 wisg.py 即uwsgi配置里面的module配置修改为:

1
2
3
4
5
6
7
import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "**inner_ulb_manager.prod**")

application = get_wsgi_application()

启动uwsgi

1
uwsgi --ini inner_ulb_manager.ini

启动ngingx

至此,部署就完成了

10. 效果图

List 组件:

传单个 ULB 对象给 Detail 组件使用即可

Detail 组件:

当然里面还实现了前面提到的 ULB 的 VServer 创建,VServer 的 RServer 的创建等。

————————

本文由『UCloud平台产品研发团队』提供。

项目源码文件戳下面链接查看,大家可以马上拿源码上手试起来,操作过程中遇到问题也可直接在github上留言:)https://github.com/tmpbook/django-with-vuejs

现在注册使用UCloud,还免费试用 及 首充返现优惠,最高可返3000元代金券!活动传送门:用UCloud!3000元限量版礼盒等你来拆!

另,欢迎添加UCloud运营小妹个人微信号:Surdur,陪聊很专业:)

关于作者:

星辰(@星辰), UCloud平台产品研发工程师,DevOps一枚。你也可以去他的知乎专栏 《随心DevOps》 上逛逛,干货满满,带你更优雅的改变世界。

相关阅读推荐:

机器学习进阶笔记之八 | TensorFlow与中文手写汉字识别

机器学习进阶笔记之七 | MXnet初体验
机器学习进阶笔记之六 | 深入理解Fast Neural Style
机器学习进阶笔记之五 | 深入理解VGG\Residual Network
机器学习进阶笔记之四 | 深入理解GoogLeNet
机器学习进阶笔记之三 | 深入理解Alexnet
机器学习进阶笔记之二 | 深入理解Neural Style
机器学习进阶笔记之一 | TensorFlow安装与入门

「UCloud机构号」将独家分享云计算领域的技术洞见、行业资讯以及一切你想知道的相关讯息。

欢迎提问&求关注 o(*////▽////*)q~

以上。

0%