网络攻击方式

常见的网络攻击方式

  1. D-Dos攻击,分布式Dos攻击,即大批量请求目标服务器暴露出来的接口,使其无法正常响应其他用户的请求;
  2. SQL注入;
  3. DNS欺骗,攻击者伪造DNS响应,将用户重定向到恶意网站;
  4. 中间人攻击(篡改SSL证书,替换加密信息);
  5. 会话劫持,窃取用户会话标识符,如cookie,接管用户的会话;
  6. 跨站脚本攻击(浏览器跨域校验);
  7. 端口扫描(Port Scanning):攻击者扫描网络以查找开放的端口,以确定潜在的入侵点;
  8. 缓冲区溢出攻击,输入的数据超出程序缓冲区的容量,导致程序崩溃或执行攻击者的代码;

如何预防D-Dos攻击

  1. ip限制,设置ip白名单;
  2. ip限流,限制每个ip单位时间内的请求次数;
  3. 加缓存中间件,如redis,消息队列中间件等,避免短时间内大量请求直接访问到数据库;
  4. 接口设计优化,接口涉及IO即访问数据库的尽量不用GET请求,入参校验cookie,或token这些;
  5. 使用高防高IP服务器;
  6. 分布式部署,保证可用性;
  7. 加一层CDN,让CDN抗住攻击;

理解golang的Context设计

Context的使用场景

Context的设计初衷就是为了在多个goroutine之间传递和同步取消信号,以减少资源的消耗和占用,具体有以下使用场景:

  1. 作为请求API的调用方&被调用方时的超时控制,通常用于数据库或者网络连接的超时控制;
  2. 作为各协程之间共用的变量池例如Gin框架中各中间件链式调用时互相传递Context;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    type Context interface {
    // Deadline returns the time when work done on behalf of this context
    // should be canceled.
    Deadline() (deadline time.Time, ok bool)
    // Done returns a channel that's closed when work done on behalf of this
    // context should be canceled.
    Done() <-chan struct{}
    // If Done is not yet closed, Err returns nil.
    // If Done is closed, Err returns a non-nil error explaining why:
    // Canceled if the context was canceled
    // or DeadlineExceeded if the context's deadline passed.
    Err() error
    // Value returns the value associated with this context for key, or nil
    // if no value is associated with key. Successive calls to Value with
    // the same key returns the same result.
    Value(key any) any
    }

Context超时控制的实现原理

通过Done()传递超时信号,并使用Select监听超时信号是否传入,官方注释如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Done is provided for use in select statements:
//
// // Stream generates values with DoSomething and sends them to out
// // until DoSomething returns an error or ctx.Done is closed.
func Stream(ctx context.Context, out chan<- Value) error {
for {
v, err := DoSomething(ctx)
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case out <- v:
}
}
}

Context的Keys池如何保证线程安全

官方context包下的Context接口内部对Keys没有使用传统锁的机制来保证线程安全,每次对Context的操作都会创建一个新的上下文实例,而不是改变现有的实例(通过WithDeadline(),WithTimeOut()WithCancle()这些方法派生父节点上下文的副本),这样的设计避免了对共享资源的直接修改,从而避免了锁的需要。

1
2
3
4
5
6
7
8
9
10
11
12
func WithValue(parent Context, key, val any) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}

Gin框架实现的Context内部是通过读写锁保证Key-Value变量池的线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Set is used to store a new key/value pair exclusively for this context.
// It also lazy initializes c.Keys if it was not used previously.
func (c *Context) Set(key string, value any) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Keys == nil {
c.Keys = make(map[string]any)
}

c.Keys[key] = value
}

// Get returns the value for the given key, ie: (value, true).
// If the value does not exist it returns (nil, false)
func (c *Context) Get(key string) (value any, exists bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, exists = c.Keys[key]
return
}

记录设计验权系统时的一些随想

OAuth2.0协议内容

OAuth 2.0 是一个行业标准的协议,用于授权。它允许第三方应用获取有限的访问权限,而无需将用户名和密码提供给第三方应用。OAuth 2.0 专注于客户端开发者的简易性,同时为Web应用、桌面应用、手机和起居室设备提供专门的认证流程。以下是 OAuth 2.0 协议的核心内容和组成部分:

  1. 角色:
    • 资源所有者(Resource Owner):能够授权访问其资源的用户。
    • 资源服务器(Resource Server):存储受保护资源的服务器,只有经过授权的客户端才能访问。
    • 客户端(Client):请求访问受保护资源的应用程序。
    • 授权服务器(Authorization Server):负责颁发访问令牌给客户端。
  2. 流程:
    • 授权码模式(Authorization Code Grant):客户端引导资源所有者至授权服务器,用户登录并授权后,授权服务器返回一个授权码给客户端,客户端使用该码换取访问令牌。
    • 密码模式(Resource Owner Password Credentials Grant):客户端直接使用资源所有者的用户名和密码向授权服务器申请访问令牌。
    • 客户端模式(Client Credentials Grant):客户端直接向授权服务器申请访问令牌,适用于客户端代表自己访问资源的场景。
    • 隐式模式(Implicit Grant):适用于纯前端应用,客户端引导资源所有者至授权服务器,用户授权后,授权服务器直接返回访问令牌给客户端。
    • 设备模式(Device Authorization Grant):适用于无法通过常规流程进行用户交互的设备,例如物联网设备。
  3. 令牌:
    • 访问令牌(Access Token):允许客户端访问受保护资源的令牌。
    • 刷新令牌(Refresh Token):用于获取新的访问令牌,通常在访问令牌过期后使用。
  4. 安全性:
    • OAuth 2.0 要求使用HTTPS来保护所有OAuth流程中的通信,以防止令牌被截获。
    • 访问令牌和刷新令牌应该是随机的、不可预测的,并且具有足够的长度。
    • 客户端必须验证授权服务器的重定向URI。
  5. 扩展性:
    • OAuth 2.0 支持多种扩展,例如开放ID连接(OpenID Connect),它允许在OAuth 2.0授权过程中传递用户身份信息。
    OAuth 2.0 协议是复杂的,并且需要根据具体的应用场景和安全要求来正确实施。它旨在提供一个安全、灵活且可扩展的授权框架,以满足不同应用的需求。

JWT和Session-Cookie的使用场景差异

JWT(JSON Web Tokens)和Session-Cookie是两种不同的技术,它们在Web应用中用于身份验证和信息传递,但它们之间存在一些关键的区别:

  1. 存储位置:
    • Session-Cookie:Session存在服务器中,对应Cookie存储在客户端浏览器中,由浏览器自动处理。
    • JWT:可以存储在客户端(通常作为URL的一部分、存储在Cookie中或者LocalStorage/SessionStorage中),也可以存储在服务端。
  2. 安全性:
    • Session-Cookie:可以设置为仅通过HTTPS传输(Secure标志),并且可以设置HttpOnly标志,使得JavaScript无法访问,减少XSS攻击的风险。
    • JWT:本身是自包含的,包含所有必要的信息,因此不需要在服务器上存储会话信息。但是,JWT需要通过HTTPS传输以防止中间人攻击,且应使用强算法(如RS256)签名以防止篡改。
  3. 大小:
    • Cookie:大小受限(通常每个域名限制为20-30个Cookie,每个Cookie大小限制在4KB左右)。
    • JWT:理论上可以更大,但因为需要在HTTP请求头中传输,所以也不宜过大,以免影响性能。
  4. 性能:
    • Session-Cookie:服务端每次响应请求前,需要在服务器上查找会话信息以确定Cookie是否有效,会增加整体请求响应耗时。
    • JWT:需要在每次请求的HTTP头中明确指定,可能会增加请求头的大小,但不需要在服务器上查找会话信息。
  5. 会话管理:
    • Session-Cookie:依赖于服务器端的会话存储,可以很容易地实现会话的注销和失效。
    • JWT:由于是自包含的,不需要服务器存储会话信息,但一旦发出,除非过期,否则不能轻易撤销。
  6. 跨域访问:
    • Session-Cookie:受到同源策略的限制,不能跨域访问。
    • JWT:由于是作为请求的一部分发送,可以跨域使用。
  7. 用途:
    • Session-Cookie:主要用于跟踪会话状态,也可以用于存储用户偏好等信息。
    • JWT:主要用于身份验证和信息交换,尤其是在分布式系统中。

总的来说,Session-Cookie更适合用于会话管理,如果账号被盗用或者登录信息修改了可以立即取消上一次登录生成的会话信息,也因为session只保存在服务器中,也能保证敏感信息不会被窃取,而JWT更适合用于分布式系统中的身份验证和信息交换,不需要另外存储会话信息,对性能的影响小。

JWT(Json Web Tokens)包含哪些信息

JWT(JSON Web Tokens)是一种紧凑且自包含的方式,用于在各方之间以JSON对象的形式安全地传输信息。JWT通常包含三个部分,用点.分隔,分别是Header(头部)、Payload(负载)和Signature(签名)。下面是每个部分通常包含的信息:

  1. Header(头部):
    • typ:类型,通常为JWT。
    • alg:指定签名算法,如HS256(HMAC SHA-256)或RS256(RSA签名)。
  2. Payload(负载):
    • iss:发行人(Issuer),指的是token签发的主体。
    • exp:过期时间(Expiration Time),token的有效期。
    • iat:发行时间(Issued At),token签发的时间。
    • nbf:生效时间(Not Before),token在此时间前不可用。
    • sub:主题(Subject),通常指用户的唯一标识。
    • aud:受众(Audience),token的预期接收者。
    • jti:JWT ID,提供token的唯一标识,防止重复使用。
    • 自定义声明:除了以上标准声明外,还可以包含其他业务相关的自定义声明。
  3. Signature(签名):
    • 签名部分是由Header指定的算法、一个秘密(对于HS算法)或公钥/私钥对(对于RS或ES算法)以及Header和Payload的编码后的字符串组成的。签名用于验证消息在传输过程中未被篡改,并且,对于使用私钥签名的token,还可以验证发送者的身份。
    JWT的结构如下所示:
    1
    <Header>.<Payload>.<Signature>

• Header和Payload通常使用Base64Url编码。
• Signature是使用Header中指定的算法和密钥对Header和Payload的编码字符串进行签名的结果。
JWT的设计目的是在各方之间安全地传输信息,同时确保信息的完整性和可验证性。由于JWT可以包含敏感信息,因此在使用JWT时,必须通过HTTPS等安全协议传输,以防止中间人攻击。

使用哈希加密算法加密保存敏感信息的一些注意事项

使用哈希加密算法加密保存敏感信息时,需要注意以下几个重要的事项:

  1. 选择合适的哈希算法:
    • 应使用当前认为安全的哈希算法,如SHA-256或SHA-512,避免使用已不安全的算法,如MD5和SHA-1。
  2. 盐值的使用:
    • 为了防止彩虹表攻击,每个密码在哈希前应该加上一个唯一的盐值。盐值可以增加密码破解的难度。
  3. 哈希输出长度:
    • 哈希输出应足够长(建议256位以上),以防止暴力破解和生日攻击。
  4. 安全存储哈希和盐值:
    • 存储哈希值时,应将盐值与哈希值一起存储,但盐值本身不需要保密。
  5. 避免时序和功耗分析攻击:
    • 使用恒定时间和随机化哈希算法,以防止通过分析算法执行来破解哈希值。
  6. 密码轮换和多因素身份验证:
    • 在遭受攻击后,仅存储哈希值是不够的。应采用密码轮换和多因素身份验证。
  7. 数据传输安全:
    • 敏感数据的传输应通过安全的通信通道(如HTTPS/TLS)进行,以防止中间人攻击。
  8. 使用最新的安全标准:
    • 加密与哈希算法不断发展,应定期更新应用程序,使用最新的安全标准和库。
  9. 密码存储格式:
    • 一些哈希算法(如bcrypt)的哈希值包含了算法标识、成本因子和盐值,这些信息都嵌入在哈希值中,无需单独存储。
  10. 密码长度限制:
    • 某些哈希算法(如bcrypt)对密码长度有限制,若密码超出此限制,需要进行额外处理。
  11. 不可逆性:
    • 哈希算法的设计理念和加密原理确保了它在密码保护中的不可替代性。通过盐、成本因子以及不可逆的特性,可以有效抵御暴力破解和彩虹表攻击。
  12. 安全开发实践:
    • 采用安全的开发实践对于保障敏感信息的安全至关重要,包括进行代码审查、定期的安全培训、漏洞扫描等。
    遵循这些注意事项可以帮助提高敏感信息的安全性,保护用户数据免受恶意攻击。

MD5保存密码为什么不够安全了,SHA-256相比MD5有什么优势

MD5不如SHA-256安全的原因主要包括以下几点:

  1. 哈希长度:
    • MD5产生128位的哈希值,而SHA-256产生256位的哈希值。更长的哈希值意味着更大的哈希空间,从而使得碰撞的概率更小,提高了安全性。
  2. 抗碰撞性:
    • MD5已被证明存在漏洞,容易受到碰撞攻击,即可以找到两个不同的输入值,它们产生相同的MD5哈希值。相比之下,SHA-256对碰撞攻击的抵抗力更强,目前还没有发现有效的攻击方法。
  3. 安全性:
    • MD5的安全性较弱,存在已知的漏洞,而SHA-256目前被认为更安全,没有发现严重的漏洞。
  4. 计算性能:
    • 尽管MD5在计算速度上比SHA-256快,但安全性是更重要的考量因素。SHA-256虽然计算速度较慢,但由于其更长的哈希长度,提供了更高的安全性。
  5. 应用场景:
    • SHA-256由于其高安全性,被广泛应用于数字签名、区块链、文件校验等需要高安全性保障的场景。而MD5则更多用于一些对安全性要求不高的场景。
  6. 雪崩效应:
    • SHA-256具有高的雪崩效应,即输入的微小变化会导致输出的哈希值发生巨大变化,这增强了其安全性。
  7. 密码学弱点:
    • MD5的密码学弱点已经被广泛研究和利用,包括长度扩展攻击和碰撞攻击,而SHA-256在这方面的表现更为稳健。
    综上所述,SHA-256在安全性、抗碰撞性、输出长度等方面都优于MD5,因此在需要高安全性的应用中,SHA-256是更安全的选择。

GPM协程调度模型的工作原理

理解GPM协程调度模型工作原理之前,先了解下协程、线程、进程之间的区别:
进程是操作系统进行资源分配的基本单位,线程是CPU调度的基本单位,其中协程相对于CPU调度的线程可以区分为用户态的线程和内核态的线程,实际运行时,CPU仍然只切换内核态的线程,协程可以通过协程调度器,让多个用户态的线程队列和一个CPU轮转期内核态的线程绑定运行也即在用户态实现并发,以达到减少线程切换时CPU资源浪费的目的。
协程的目的是在执行多线程任务时,减少CPU在切换线程上的调度消耗,以达到提高CPU的利用率。

GPM的G是goroutine协程,P是processor处理器是抽象的处理器,包含了协程运行的环境资源,M是Machine线程,内核状态的线程。goroutine相比于广义的协程,占用更小的内存空间,只占几KB的内存(线程占用约4MB,广义协程也是MB级别),调度更加灵活

G 表示 Goroutine,也就是 Go 语言中的轻量级线程。Goroutine 是一种并发执行的抽象,它比传统的线程更轻量、更高效,并且可以很容易地创建和销毁。

P 表示 Processor,也可以理解为上下文处理器。每个 P 其实就是一个相当于操作系统线程的抽象,负责执行 Goroutine。P 的个数可以通过 GOMAXPROCS 环境变量或者 runtime.GOMAXPROCS 函数进行设置。

M 表示 Machine,也就是操作系统线程和内核的直接映射,它负责管理真实的线程资源,并与操作系统进行交互。在 GPM 调度模型中,有多个 M,可以与多个 P 绑定。

N 表示工作队列的长度。工作队列用来存放待执行的 Goroutine,当某个 P 处于空闲状态时,会从工作队列中取出 Goroutine 执行。

GPM 调度模型的工作流程如下:

  1. 当创建一个 Goroutine 时,它被放入当前 P 的本地队列(local run queue)中。
  2. 当本地队列为空时,P 会尝试从全局队列(global run queue)中偷取 Goroutine。
  3. 如果全局队列也为空,P 则会去其他空闲的 P 偷取 Goroutine。
  4. 当 P 获取到 Goroutine 后,将其放入自己的本地队列并执行。
  5. P 执行完毕后,会检查是否需要进行垃圾回收操作。
  6. 若某个 P 长时间没有进展(例如发生了系统调用或者进入了休眠状态),它会释放与之关联的 M,并允许其他 Goroutine 使用。
  7. 被释放的 M 可以绑定到其他的 P,并开始执行新的 Goroutine。

总结起来,GPM 调度模型通过将 Goroutine 分配给不同的 P 来实现并发执行,同时利用工作队列和偷取策略来平衡负载。这种调度模型在高并发、异步执行的场景下表现出色,使得开发者可以轻松地编写高效、高并发的程序。

尝试用python实现贪吃蛇

需求分析

流程图

流程图

1
2
3
4
5
6
7
8
9
10
11
12
st=>start: KEYDOWN.K_ENTER
op0=>operation: 食物刷新
op1=>operation: 控制行进方向
cond1=>condition: 吃到食物?
cond2=>condition: 触碰到边界或身体?
e=>end: Game Over

st->op0->op1->cond1->cond2
cond1(yes)->op0
cond1(no)->cond2
cond2(yes)->e
cond2(no)->op1

场景元素图

场景元素图-贪吃蛇

动作

方向调整

事件

刷新食物位置

吃到食物

触碰到边界

数据结构设计

二维画布

一维蛇

点食物

代码实现

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# author:Wangdali time:2021年1月20日16:08:44
#python实现:贪吃蛇
'''
游戏玩法:回车开始游戏;空格暂停游戏/继续游戏;方向键/wsad控制小蛇走向
'''
'''
思路:用列表存储蛇的身体;用浅色表示身体,深色背景将身体凸显出来;
蛇的移动:仔细观察,是:身体除头和尾不动、尾部消失,头部增加,所以,新添加的元素放在列表头部、删除尾部元素;
游戏结束判定策略:超出边界;触碰到自己的身体:蛇前进的下一格子为身体的一部分(即在列表中)。
'''
#注:因为在列表中需要频繁添加和删除元素,所以用deque容器代替列表;是因为deque具有高效的插入和删除效率
#初始化蛇,长度为3,放置在屏幕左上角;
#导包
import random
import sys
import time
import pygame
from pygame.locals import *
from collections import deque
#基础设置
Screen_Height=720
Screen_Width=1280
Size=20#小方格大小
Line_Width=1
#游戏区域的坐标范围
Area_x=(0,Screen_Width//Size-1) #0是左边界,1是右边界 #注:python中//为整数除法;/为浮点数除法
Area_y=(2,Screen_Height//Size-1)
#食物的初步设置
#食物的分值+颜色
Food_Style_List=[(10,(255,100,100)),(20,(100,255,100)),(30,(100,100,255))]
#整体颜色设置
Light=(100,100,100)
Dark=(200,200,200)
Black=(0,0,0)
Red=(200,30,30)
Back_Ground=(40,40,60)
#文本输出格式设置
def Print_Txt(screen,font,x,y,text,fcolor=(255,255,255)):
#font.render参数意义:.render(内容,是否抗锯齿,字体颜色,字体背景颜色)
Text=font.render(text,True,fcolor)
screen.blit(Text,(x,y))
#初始化蛇
def init_snake():
snake=deque()
snake.append((2,Area_y[0]))
snake.append((1,Area_y[0]))
snake.append((0,Area_y[0]))
return snake
#食物设置
#注意需要对食物出现在蛇身上的情况进行判断
def Creat_Food(snake):
'''
注:randint 产生的随机数区间是包含左右极限的,
也就是说左右都是闭区间的[1, n],能取到1和n。
而 randrange 产生的随机数区间只包含左极限,
也就是左闭右开的[1, n),1能取到,而n取不到。randint
产生的随机数是在指定的某个区间内的一个值,
而 randrange 产生的随机数可以设定一个步长,也就是一个间隔。
'''
food_x=random.randint(Area_x[0],Area_x[1]) #此处有疑问
food_y=random.randint(Area_y[0],Area_y[1])
#如果食物出现在蛇上,重来;
while(food_x,food_y)in snake:
food_x = random.randint(Area_x[0], Area_x[1])
food_y = random.randint(Area_y[[0], Area_y[1]])
return food_x,food_y
#食物风格
def Food_Style():
return Food_Style_List[random.randint(0,2)] #返回随机的分值和颜色
def main():
pygame.init()
screen=pygame.display.set_mode((Screen_Width,Screen_Height)) #初始化一个准备显示的窗口或屏幕
pygame.display.set_caption('贪吃蛇') #Set the current window caption
#得分字体设置
font1=pygame.font.SysFont('SimHei',24)
#GO字体设置
font2 = pygame.font.SysFont(None, 72)
fwidth, fheight = font2.size('GAME OVER') ###
#程序bug修复:如果蛇在向右移动,快速点击分别施加向下、向左的命令,向下的命令会被覆盖,只有向左的命令被接受,直接GameOver
# b变量为了防止这个情况发生
b=True
#蛇
snake=init_snake()
#食物
food=Creat_Food(snake)
food_style=Food_Style()
#方向控制
pos=(1,0) ###
#启动游戏相关变量初始化
game_over=True #结束标志 # 是否开始,当start = True,game_over = True 时,才显示 GAME OVER
game_start=False #开始标志
score=0 #得分
orispeed=0.3 #蛇初始速度
speed=orispeed #蛇速度
last_move_time=None
pause=False #暂停
while True:
for event in pygame.event.get():
if event.type==QUIT:
sys.exit()
elif event.type==KEYDOWN:
if event.key==K_RETURN:
if game_over:
game_start=True
game_over=False
b=True
snake=init_snake()
food=Creat_Food(snake)
food_style=Food_Style()
pos=(1,0)
#得分
score=0
last_move_time=time.time()
elif event.key==K_SPACE:
if not game_over:
pause=not pause
#以下为防止蛇在向右移动时按向左键,导致GameOver
elif event.key in (K_UP,K_w):
if b and not pos[1]: ###
pos=(0,-1)
b=False
elif event.key in (K_DOWN,K_s):
if b and not pos[1]:
pos = (0, 1)
b = False
elif event.key in (K_LEFT,K_a):
if b and not pos[0]:
pos = (-1, 0)
b = False
elif event.key in (K_RIGHT,K_d):
if b and not pos[0]:
pos = (1, 0)
b = False
#填充背景色
screen.fill(Back_Ground)
###
#画网格线、竖线
for x in range(Size, Screen_Width, Size):
pygame.draw.line(screen, Black, (x, Area_y[0] * Size), (x, Screen_Height), Line_Width)
#画网格线、横线
for y in range(Area_y[0] * Size, Screen_Height, Size):
pygame.draw.line(screen, Black, (0, y), (Screen_Width, y), Line_Width)
#蛇的爬行过程
if not game_over:
curTime=time.time()
if curTime-last_move_time>speed: ###
if not pause:
b=True
last_move_time=curTime
next_s = (snake[0][0] + pos[0], snake[0][1] + pos[1])
#如果吃到了食物
if next_s==food:
snake.appendleft(next_s)
score+=food_style[0]
speed = orispeed - 0.03 * (score // 100)
food = Creat_Food(snake)
food_style = Food_Style()
else:
#在区域内
if Area_x[0]<=next_s[0]<=Area_x[1] and Area_y[0]<=next_s[1]<=Area_y[1] and next_s not in snake:
snake.appendleft(next_s)
snake.pop()
else :
game_over=True
#画食物
if not game_over:
'''
rect(Surface,color,Rect,width=0)
第一个参数指定矩形绘制到哪个Surface对象上

第二个参数指定颜色

第三个参数指定矩形的范围(left,top,width,height)

第四个参数指定矩形边框的大小(0表示填充矩形)

例如绘制三个矩形:

pygame.draw.rect(screen, BLACK, (50, 50, 150, 50), 0)
pygame.draw.rect(screen, BLACK, (250, 50, 150, 50), 1)
pygame.draw.rect(screen, BLACK, (450, 50, 150, 50), 10)
'''
# 避免 GAME OVER 的时候把 GAME OVER 的字给遮住了
pygame.draw.rect(screen, food_style[1], (food[0] * Size, food[1] * Size, Size, Size), 0)
#画蛇
for s in snake:
pygame.draw.rect(screen, Dark, (s[0] * Size + Line_Width, s[1] * Size + Line_Width,
Size - Line_Width * 2, Size - Line_Width * 2), 0)
Print_Txt(screen, font1, 30, 7, f'速度: {score // 100}')
Print_Txt(screen, font1, 450, 7, f'得分: {score}')
#画GameOver
if game_over:
if game_start:
#print('GameOver')
Print_Txt(screen, font2, (Screen_Width - fwidth) // 2, (Screen_Height - fheight) // 2, 'GAME OVER',Red)
pygame.display.update()
if __name__=='__main__':
main()

golang初学笔记

学习网站

A Tour of Go

Go by Example

Learn Go with tests - learn-go-with-tests (gitbook.io)

基本数据类型

Go基本数据类型_TCatTime的博客-CSDN博客_go 数据类型

基本语法

Go语言基础语法-Golang基础语法-Golang中国 (qfgolang.com)

声明变量

声明方式 特点
var 数据名称 type 数据值为默认值
数据名 := 值 :=让编译器根据值自动判断数据类型)省去关键字var

变量不可重复声明!

命名首字母是否大小写决定是否能够跨包调用,包内定义方法的方法名若是大写则表名可以被其他包调用,否则只能包内调用,类名,属性名同理;

特性

import

  1. import _ "包路径"因为go语言存在不使用即声明或直接导入则无法通过编译的规则,所以为了满足某些情况下仅需要调用某包的init函数的情景,可以进行匿名导入,即在包路径前添加”_ “ ;
  2. import 别名 "包路径",起别名也是在包路径前添加”别名 “;
  3. import . "包路径",将包内所有方法导入到本包中,这样调用导入包中方法时,可以直接通过方法名调用;

iota

在 Go 中枚举常量是使用 iota 枚举器创建的,在功能上,iota 关键字表示从 0 开始的整数常量;在作用上可以简化使用自动递增数字的常量定义,非常方便。

以前定义一个枚举值:

1
2
3
4
5
const (
a = 0
b = 1
c = 2
)

Go 有了 iota 关键字后:

1
2
3
4
5
const (
a = iota
b
c
)

对应的值结果:

1
2
3
a=0
b=1
c=2

甚至还可以跳着来:

1
2
3
4
5
6
const (
a = iota
_
b
c
)

对应的值结果:

1
2
3
a=0
b=2
c=3

也可以玩出花来:

1
2
3
4
5
6
const (
bit0, mask0 = 1 << iota, 1<<iota - 1//x << y表示x左移y位
bit1, mask1
_, _
bit3, mask3
)

对应的值结果:

1
2
3
4
bit0 == 1, mask0 == 0  (iota == 0)
bit1 == 2, mask1 == 1 (iota == 1)
(iota == 2, unused)
bit3 == 8, mask3 == 7 (iota == 3)

defer关键字

  1. 用于修饰方法,执行代码时会将defer修饰的方法输出结果依次压入栈中,在执行时完中括号内所有内容后再按出栈顺序输出结果;

  2. deferreturn的执行顺序问题:

    结论:同一方法内,被defer修饰的语句,其执行顺序在return动作之后;

声明数组

[数组长度]type{值}

若值的个数小于数组长度,则剩余元素默认值为0,若大于则无法通过编译,且不同长度的数组无法互相传递;

切片(动态数组)

​ 当数组声明时大括号内没有声明数组长度,则数组为动态数组;

声明方式
  1. 声明方式一:

    1
    2
    var slice1 []int 
    slice2 := []int

    声明slice是一个动态数组,但是并没有给slice分配空间,要使用slice1需要先用make([]int, len)为其开辟空间 PS:用:= 代替 = 则可省略关键字var;

  2. 声明方式二:

    1
    slice1 := make([]type, len) 

    其中len为数组的初始长度;

  3. 声明方式三:

    1
    myArray[] := []int{1,2,3,4} 
使用方式
  1. 传递方式:切片的传递是引用传递,方法内对形参数组元素的修改会生效在实参上,且不同长度的动态数组都可以进行引用传递;

  2. for range 遍历:

    1
    for index,value := range slice{}
  3. len()和cap()函数:len()可以获取切片当前长度,cap()可以获取切片当前容量,容量即切片实际开辟空间,长度则为读取切片的索引最大值,且cap也是动态扩容时的cap追加的数量(重要扩容机制)

  4. append(slice, key):向切片末尾添加元素;

  5. 取切片元素 s1 := slice[0:2],表明取切片的第1至第2个元素并赋值给s1,区间是左闭右开;

  6. copy(copySlice2, slice1),将slice1的内容copy到copySlice2中,为值传递

map

底层

GO中对map排序_雨雨不怕雨的博客-CSDN博客_go map 排序

GO语言中,map是哈希表,能够将特定类型的key映射到特定类型的Value上。在查询Map里面的内容时,其时间复杂度为O(1)非常高效。但其存储并不是线性的,遍历输出时,也没有顺序可言。

声明方式
  1. 声明方式一:

    1
    var myMap map[keyType]valueType

    mapslice的特性一致,可以动态开辟空间,声明若采用此方式,则需要通过make(myMap, len)为其开辟空间进行初始化后才能使用;

  2. 声明方式二:

    1
    myMap := make(map[string]string, len)

    其中len可以省略,省略编译器会默认给myMap分配一定空间;

  3. 声明方式三:

1
2
3
4
5
6
7
8
9
10
11
myMap := map[string]string {
"key1": "value1",
"key2": "value2",
"key3": "value3"
}

var myMap = map[string]string {
"key1": "value1",
"key2": "value2",
"key3": "value3"
}
使用方式
  1. 传递方式:同切片,为引用传递

  2. 添加:

    1
    2
    3
    myMap["key1"] = "value1"
    myMap["key2"] = "value2"
    myMap["key3"] = "value3"
  3. 遍历:

    1
    2
    3
    4
    //for range遍历
    for key, value := range myMap{}
    //print
    printMap(myMap)
  4. 删除:

    1
    delete(myMay, "key1")
  5. 修改:

    1
    myMap["key1"] = "value1_new"
  6. 查找:

    1
    2
    3
    4
    5
    6
    7
    //演示map查找
    val , ok :=cities["no2"]
    if ok{
    fmt.Printf("找到了 值为%v",val)
    }else{
    fmt.Printf("没有找到")
    }

struct,封装

  1. 给已定义数据类型起别名

    1
    type myStruct int
  2. 定义结构体

    1
    2
    3
    4
    5
    type myStruct struct {
    Key int
    Value string
    Length int
    }
  3. getter和setter方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    type myStruct struct {
    Key int
    Value string
    Length int
    }
    func (this *myStruct) GetKey() {
    fmt.Println("Key = " this.Key)
    }
    func (this *myStruct) SetKey(newKey int) {
    this.Key = newKey
    }
  4. 创建对象

    1
    demo := myStruct{Key:11, Value:"test", Length:10}//属性名亦可省略不写

继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func(this *myStruct) Sort(){
fmt.Println("this is Sort of myStruct")
}

type superStruct struct{
myStruct //superStruct继承了myStruct的方法

Height int
}

//继承并重写父类myStruct的方法
func(this *superStruct) Sort(){
fmt.Println("this is Sort of superStruct")
}

//创建子类对象 方式一
demo1 := superStruct{myStruct{11,"test",10},120}
//创建子类对象 方式二
var demo2 superStruct
demo2.Key = 11
demo2.Value = "test"
demo2.Length = 10
demo2.Height = 20

多态

  1. interface

    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
    //interface的本质是一个指针,指向结构体及其内部的函数列表
    type AnimalIf interface{
    Sleep()
    Eat()
    GetColor() String//获取动物颜色
    }
    //具体的类
    type Cat struct{
    Color string
    }
    func(this *Cat) Sleep(){
    fmt.Println("Cat is sleeping")
    }
    func(this *Cat) Eat(){
    fmt.Println("Cat is eating")
    }
    func(this *Cat) GetColor() string{
    return this.color
    }

    type Dog struct{
    Color string
    }
    func(this *Dog) Sleep(){
    fmt.Println("Dog is sleeping")
    }
    func(this *Dog) Eat(){
    fmt.Println("Dog is eating")
    }
    func(this *Dog) GetColor() string{
    return this.color
    }

    var animal AnimalIf
    //因为interface本质是指针,所以传值给接口对象时需要用取地址符&
    animal = &Cat{"Black"}
    animal.Sleep()
    animal = &Dog{"White"}
    animal.Sleep()

    func animalSleep(animal AnimalIf){
    animal.Sleep()
    }

万能类型

interface{} : 空接口

基本数据类型都实现了空接口

  1. 作为形参时,可以接收所有类型的参数

  2. interface{}提供 “断言” 的机制

    1
    2
    3
    4
    5
    6
    value, ok := demo.(string)//demo是interface类型,返回 value和ok
    if !ok{
    fmt.Println("demo is not a string type")
    }else{
    fmt.Println("demo is a string type, value= ", value)
    }

pair

变量内置的pair结构:

1
2
var a int = 100// a: pair<statictype:int, value:100> 
var demo myStruct = {10,"test",99.9}//demo: pair<type:myStruct, value{10,"test",99.9}>

pair的结构用于断言判断类型

defer

defer定义的语句会在程序结束时,即return或panic之前执行,这意味着,即便程序出现error并panic了,也依旧会执行defer语句。如果要判断是因为何种情况要结束程序才执行的defer,则需要通过recover()去捕获panic,若返回值不为nil则说明来自panic。

1
2
3
4
5
6
7
8
9
10
11
func example() {
defer func() {
if err := recover(); err != nil {
log.Println("panic occurred:", err)
}
}()
err := findErr()
if err != nil{
panic(err)
}
}

如果定义了多个 defer 语句,它们会以逆序执行(Last In First Out,即后入先出)的顺序依次执行。这意味着最后一个定义的 defer 语句将是第一个执行的,并且最先定义的 defer 语句将是最后一个执行的。

reflct

反射通过pair获取变量的类型和值。

方法:

  1. reflct.ValueOf()

  2. reflct.TypeOf()

使用场景:通过interface{}万能类型接收变量,再通过TypeOf()获得变量的动态类型,并通过ValueOf()获得参数运行时的值。

Struct Tag

通过reflect.Type获取结构体成员信息reflect.StructField结构中的 Tag 被称为结构体标签(Struct Tag)。结构体标签是对结构体字段的额外信息标签。使用场景有如定义结构体别名,使得在参数绑定时更好的一一对应

1
2
3
4
5
//书写格式
type myStruct struct{
Name string `tag1:"name" tag2:"名字"`//以键值对的方式书写并用反引号括起来
Sex string `tag1:"sex" tag2:"性别"'`
}

并发

Go语言基础之并发 | 李文周的博客 (liwenzhou.com)

goroutine

goroutine是用户态的线程,由Go语言的运行时(runtime)调度完成。

使用goroutine

在调用函数的时候在前面加上 go 关键字,就可以为函数创建一个goroutine

一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。

特殊机制

在程序启动时,Go程序就会为main()函数创建一个默认的goroutine

当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var wg sync.WaitGroup//sync.WaitGroup可以让main等待所有goroutine结束后再结束main()

func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("Hello Goroutine!", i)
}
func main() {

for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}
!!!需要注意的点!!!

若以闭包的方式运行goroutine时(如下所示)

闭包函数:关于闭包,最简单的描述就是 ECMAScript 允许使用内部函数--即函数定义和函数表达式位于另一个函数的函数体内。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。

也就是说,内部函数会在外部函数返回后被执行。而当这个内部函数执行时,它仍然必需访问其外部函数的局部变量、参数以及其他内部函数。这些局部变量、参数和函数声明(最初时)的值是外部函数返回时的值,但也会受到内部函数的影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var wg sync.WaitGroup//sync.WaitGroup可以让main等待所有goroutine结束后再结束main()

func main() {
wg.Add(10000) // 计数牌+1
for i := 0; i < 10000; i++ {
go func() {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("Hello Goroutine!", i)
}()
}
fmt.Println("hello main")
wg.Wait() // 等待所有登记的goroutine都结束
}
/*
闭包内含有一个作用域外部的变量,所以执行对应goroutine内容时需要再去访问该变量的值,此时该变量作为flag值已经在循环中并行执行了很多次,故绝大多数goroutine取到的变量值都是最后一次循环的变量值。
*/

解决方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
var wg sync.WaitGroup//sync.WaitGroup可以让main等待所有goroutine结束后再结束main()

func main() {
wg.Add(10000) // 计数牌+1
for i := 0; i < 10000; i++ {
go func(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("Hello Goroutine!", i)
}(i) // 解决方式:显式传递值给匿名函数,避免闭包
}
fmt.Println("hello main")
wg.Wait() // 等待所有登记的goroutine都结束
}
goroutine调度

深入Golang调度器之GMP模型 - sunsky303 - 博客园 (cnblogs.com)

channel

channel用于实现在多个goroutine间进行通信。

虽然goroutine可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

声明

channel是一种类型,一种引用类型。声明通道类型的格式如下:

1
var 变量 chan 元素类型

举几个例子:

1
2
3
var ch1 chan int   // 声明一个传递整型的通道
var ch2 chan bool // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道
创建channel

通道是引用类型,通道类型的空值是nil

1
2
var ch chan int
fmt.Println(ch) // <nil>

声明的通道后需要使用make函数初始化之后才能使用。

创建channel的格式如下:

1
make(chan 元素类型, [缓冲大小])

channel的缓冲大小是可选的。

举几个例子:

1
2
3
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)
channel操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。

发送和接收都使用<-符号。

现在我们先使用以下语句定义一个通道:

1
ch := make(chan int)

发送

将一个值发送到通道中。

1
ch <- 10 // 把10发送到ch中

接收

从一个通道中接收值。

1
2
x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果

关闭

我们通过调用内置的close函数来关闭通道。

1
close(ch)

关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

关闭后的通道有以下特点:

  1. 对一个关闭的通道再发送值就会导致panic。
  2. 对一个关闭的通道进行接收会一直获取值直到通道为空。
  3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
  4. 关闭一个已经关闭的通道会导致panic。

无缓冲的通道

无缓冲的通道又称为阻塞的通道。我们来看一下下面的代码:

1
2
3
4
5
func main() {
ch := make(chan int)
ch <- 10
fmt.Println("发送成功")
}

上面这段代码能够通过编译,但是执行的时候会出现以下错误:

1
2
3
4
5
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
.../src/github.com/Q1mi/studygo/day06/channel02/main.go:8 +0x54

为什么会出现deadlock错误呢?

因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。

上面的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?

一种方法是启用一个goroutine去接收值,例如:

1
2
3
4
5
6
7
8
9
10
func recv(c chan int) {
ret := <-c
fmt.Println("接收成功", ret)
}
func main() {
ch := make(chan int)
go recv(ch) // 启用goroutine从通道接收值
ch <- 10
fmt.Println("发送成功")
}

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。

使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道

有缓冲的通道

解决上面问题的方法还有一种就是使用有缓冲区的通道。我们可以在使用make函数初始化通道的时候为其指定通道的容量,例如:

1
2
3
4
5
func main() {
ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
ch <- 10
fmt.Println("发送成功")
}

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。

我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做。

从通道中取值的方式
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
package main

import "fmt"

// 开启goroutine将0~100的数发送到ch中
func f1(ch chan int) {
for i := 0; i < 100; i++ {
ch <- i
}
close(ch)
}

// 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
func f2(ch1 chan int, ch2 chan int) {
for {
//从通道中取值的方式1
i, ok := <-ch1 // 通道关闭后再取值ok=false
if !ok {
break
}
ch2 <- i * i
}
close(ch2)
}

func main() {
ch1 := make(chan int, 100)
ch2 := make(chan int, 200)
go f1(ch1)
go f2(ch1, ch2)
//从通道中取值的方式2
for ret := range ch2 {
fmt.Println(ret)
}
}

Docker及个人云盘搭建学习笔记

前几天去朋友家,他给我演示了下云服务器的妙用,只见他轻轻敲出几行命令后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
yum install docker
//安装docker,我的云服务器系统是CentOS 7.5 64位

docker pull nextcloud
//拉取nextcloud镜像

docker pull mysql:8.0
//拉取mysql:8.0镜像

systemctl start docker

systemctl enable docker
//设置docker开机自启动

docker run -d --name mysql -v document_mysql:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=pw1 -e MYSQL_DATABASE=nextcloud -e MYSQL_USER=user -e MYSQL_PASSWORD=pw2 -p 3306:3306 mysql:8.0
//在名为mysql的容器中启动mysql:8.0,并配置mysql环境变量:;-d 在后台运行容器,并且打印容器id,--name为容器指定一个名称;-volume,-v 绑定一个卷,格式为-v 本地目录:容器目录 或 -v 容器目录;-e 设置环境变量;

docker run -d --name nextcloud -v nextcloud:/var/www/html --link mysql:mysql -p 8080:80 nextcloud
//在名为nextcloud的容器中启动nextcloud:--link 添加链接到另一个容器;-p 指定端口映射,格式为:主机(宿主)端口:容器端口

我空空如也的服务器上就装上了可以多用户登陆的”个人网盘“!

嘿嘿嘿

回家后想起他神乎其神的操作,内心久久不能平静,所以自己查询资料做了点小笔记:

快速搭建私有云盘参考链接:https://baiyue.one/archives/453.html

Docker

基于Go语言开发的开源项目

术语:

概念 说明
Docker 镜像(Images) Docker 镜像是用于创建 Docker 容器的模板,比如 Ubuntu 系统。
Docker 容器(Container) 容器是独立运行的一个或一组应用,是镜像运行时的实体。
Docker 客户端(Client) Docker 客户端通过命令行或者其他工具使用 Docker SDK (https://docs.docker.com/develop/sdk/) 与 Docker 的守护进程通信。
Docker 主机(Host) 一个物理或者虚拟的机器用于执行 Docker 守护进程和容器。
Docker Registry Docker 仓库用来保存镜像,可以理解为代码控制中的代码仓库。Docker Hub(https://hub.docker.com) 提供了庞大的镜像集合供使用。一个 Docker Registry 中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);每个标签对应一个镜像。通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签> 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。
Docker Machine Docker Machine是一个简化Docker安装的命令行工具,通过一个简单的命令行即可在相应的平台上安装Docker,比如VirtualBox、 Digital Ocean、Microsoft Azure。
Volume(数据卷) 通过数据卷可以在容器之间实现共享和重用;
对数据卷的修改会立马生效(非常适合作为开发环境);
对数据卷的更新,不会影响镜像;
卷会一直存在,直到没有容器使用

和虚拟机的异同:

相同点:

虚拟机和Docker同是使用虚拟化技术,来达到在同一平台上配备多个运行环境的目的;

不同点:

  1. 虚拟机是通过软件模拟当前的硬件环境(硬件资源的虚拟化)来运行一个完整的操作系统,Docker自建的Docker容器(Container)是共用一个系统内核(内核级别的虚拟化);

VM vs Docker

  1. 虚拟机相对Docker非常笨重,因为承载了一个完整的操作系统导致占用硬件资源十分多,启动也很慢,而Docker中每个镜像文件大小十分轻便(MB级别大小),可以做到秒级启动

参考链接:https://www.runoob.com/docker/docker-architecture.html