我们在第五节中提到一个问题,任务队列增长速度太快,与之对应的采集、分析、处理速度远远跟不上,造成内存快速增长,带宽占用过高,CPU使用率过高,这样是极度有害系统健康的。

我们在开发采集程序的时候,总是希望能够尽快将数据爬取下来,如果总任务数量很小(2~3K请求数之内),总耗费时长很短(1~2分钟之内),那么,对系统的正常运行不会造成太严重的影响,我们尽可以肆无忌惮。但,当总任务数量更多,总耗费时长更长,那么,无休止的任务堆积,就会给系统带来难以预料甚至是很严重的后果。

为此,我们不得不考虑几个问题:

  • 我们的任务总量大概在什么量级,全速采集大概需要耗费多少时间、多少资源,未来的发展是不是可控?
  • 采集系统自身依托的环境资源是否充足,是否能够满足随之而来的巨大的资源消耗?
  • 采集的目标资源系统是否具有某些反爬策略限制?
  • 采集的目标资源系统是否能够承受得住如此数量级的并发采集请求(无论单点或分布式采集系统,都要考虑这点)?
  • 随着采集结果返回,带来的后续分析、处理、存储能力是否能够满足大量数据的瞬时到来?

由以上问题也可以看出,一个爬虫系统策略的制定,需要考虑的问题也是全方位的,而不仅仅是采集本身,不同的环境、规模、目标,采用的策略也不尽相同。本节,我们将讨论一下,如果我们的能力不能满足上述条件的情况下,如何来制定一个并发策略以及如何实现它。

并发策略,从规模上可以分为全局并发策略和单点并发策略,全局并发策略包含单点并发策略,不过它也需要同时考虑负载均衡策略对制定并发策略的影响。目前,我们还没有将爬虫框架扩展到分布式框架,暂时先不考虑全局并发策略的制定。主要探讨一下单点并发策略制定与实现。

单点并发策略的制定

通常,我们在制定单点并发策略时,需要从哪些角度考虑,使用什么方法,以及如何决策?下面我们就来详细聊聊:)

1、我们先来梳理一下采集系统自身所依托的环境资源:

除了CPU、内存、存储器、带宽这些耳熟能详的资源外,还有就是比较容易被忽略的操作系统可用端口。对于各种资源的占用情况,下面给出一些建议(均值):

  • CPU:采集系统的占用总量建议不超过30%,CPU总使用量建议不超过50%。(虽然我这个疯子经常贪婪过渡T_T)。对于多核CPU,线程创建数量建议不超过CPU核数的两倍。
  • 内存:采集系统的占用总量建议不超过50%,内存总使用量建议不超过70%。
  • 存储器:对于商业或者大规模的爬虫体系,建议将存储分离,使用外部存储设备,比如NAS、分布式缓存、数据仓库等;当然,其他爬虫体系也这么建议,但如果条件不允许的话,只能存储在本地磁盘的话,就需要考虑磁盘的IOPS了,即使是使用缓存、数据库系统来作为中间存储媒介,实质上也是与磁盘IO打交道,不过一般的缓存、数据库系统都会对IO做优化,而且能干预的力度比较小,倒是可以略微“省心”。这个,本人也无法给出一个合理的通用的建议值,磁盘的性能千奇百怪,只能是按实际环境来拿捏了。
  • 带宽:分为上行、下行两个带宽指标,采集系统在这两个指标中的占用总量都不建议超过80%。除了考虑ISP分配的带宽,还要考虑会影响其效能的周边设备,比如猫、交换机、路由器甚至是网线的吞吐能力。说来尴尬,我经常在家里做实验,爬虫系统和目标资源系统都还OK,联通的光猫跪了……重启复活……又跪了……重启复活……又跪了……重启复活……
  • 可用端口:这个是一个隐性条件,也是经常被忽略的限制。拿Windows系统来说,可用的端口最大数量为UInt16.MaxValue(65535)个,而伴随着系统启动,就会有一系列的服务占用了部分端口,比如IIS中的网站、数据库、QQ,而系统本身也会保留一部分端口,比如443、3389等。而是否能够使用端口重用技术来缓解疼痛,对具体实现以及NAS端口映射规则的要求更高,不好或不可控。所以爬虫本身能够使用的端口数就有一个极限限制,这个也没有建议值,具体情况各不相同。

总之,资源总是有限的,大体原则就是:做人留一线,日后好相见:)

2、对于目标资源系统的资源环境:

通常,我们无法探知具体的资源情况,再加上对方可能使用反爬策略,就是知道具体的资源情况,也不见得就有用。对于制定并发策略,我们更关心的是对方能够吃的下多大的鸭梨,以及探索其反爬策略允许的极限。为此,我们需要使用下述的方法,来辅助我们制定策略。

3、通常使用的方法:

3.1、需要找到目标资源系统中的一个URI,原则是轻量、成功率高(最好是100%),比如,一张小小的图片、一个简单短小的ajax接口、一个静态html甚至是一个xxx.min.css,但要注意,我们选取的URI可千万不要是经过CDN加速的,否则T_T;

3.2、接下来,我们就针对选取的URI进行周期采集,对于一般的资源站点,初始频率设置为1秒1次,就可以了;

3.3、然后就是运行一段时间观察结果,后面我们再说运行多长时间观察为合适;

3.4、如果观察结果OK,成功率能够达到95%+,那么,我们就可以适量缩小采集周期,反之,就要适当延长采集周期;

3.5、重复3.3~3.4,最后得到一个合理的极限周期;

3.6、至于一次观察多长时间,不同的反爬策略,有着不同的限制,这个需要小心。我曾经的一个项目,当时就比较心急,观测了5分钟,没什么问题,就丢出去了,结果后来现实告诉我,他们的策略是1分钟累计限制、10分钟累计限制、20分钟累计限制、30分钟累计限制、1小时累计限制……而且累计限制逐级递减,也就是说,你满足了1分钟的累计限制,x10,就不一定满足10分钟的累计限制,x60就有可能远远超出了1小时累计限制。这里给出一个建议,至少30分钟。因为目标系统去统计每一个来源IP的访问周期,也是一个不小的代价,所以也不可能做到无限期的监测,通常半小时到一小时已经是极限了。这里也给出一个最保险的观测周期,那就是根据请求总量及当前频率,预估耗费总时长,作为观测周期,这样是最稳妥的,但,这也可能是不切实际的:(

4、如何制定并发策略:

通过上述3步,结合自身的资源情况、目标的反爬策略及承受能力、以及观测结果,我们就可以制定一个大概的并发量了,制定决策也就不那么困难了;

我们的任务都是存储在队列中,并发的限制,无非就是控制入队的频率,所以,只需要把前面的统计结果转化为最小请求间隔,就是我们最终的并发策略了;

为什么是控制入队,而不是出队呢?因为如果不控制入队,那么队列还是会无限暴增,直至“死亡”,而限制入队,一方面避免队列暴增,另一方面,阻塞新任务的生成,降低CPU及内存使用量;

单点并发策略的实现

有了理论基础,在技术实现上,就不是什么难事儿了。

 namespace MikeWare.Core.Components.CrawlerFramework.Policies
{
using System; public abstract class AConcurrentPolicy
{
public virtual bool WaitOne(TimeSpan timeout) => throw new NotImplementedException(); public virtual void ReleaseOne() => throw new NotImplementedException();
}
}

并发策略 —— AConcurrentPolicy

这是一个抽象类,具有两个抽象方法,作为并发策略的基础实现;

我写了两种并发策略的具体实现,PeriodConcurrentPolicy和SemaphoreConcurrentPolicy,他们的目的都是用来控制入队的频率,目标一致,方法不同,您也可以实现自己的并发策略;

本节,我们主要说道说道System.Threading.Semaphore的使用及SemaphoreConcurrentPolicy的实现原理;

 namespace MikeWare.Core.Components.CrawlerFramework.Policies
{
using System;
using System.Threading; public class SemaphoreConcurrentPolicy : AConcurrentPolicy
{
private Semaphore semaphore = null; public SemaphoreConcurrentPolicy(int init, int max)
{
semaphore = new Semaphore(init, max);
} public override bool WaitOne(TimeSpan timeout)
{
return semaphore.WaitOne(timeout);
} public override void ReleaseOne()
{
semaphore.Release();
}
}
}

并发策略实现 —— SemaphoreConcurrentPolicy

SemaphoreConcurrentPolicy继承自AConcurrentPolicy,定义了一个私有变量Semaphore semaphore,以及重写了基类的两个抽象方法;

namespace System.Threading
{
//
// Summary:
// Limits the number of threads that can access a resource or pool of resources
// concurrently.
public sealed class Semaphore : WaitHandle
{
//
// Summary:
// Initializes a new instance of the System.Threading.Semaphore class, specifying
// the initial number of entries and the maximum number of concurrent entries.
//
// Parameters:
// initialCount:
// The initial number of requests for the semaphore that can be granted concurrently.
//
// maximumCount:
// The maximum number of requests for the semaphore that can be granted concurrently.
//
// Exceptions:
// T:System.ArgumentException:
// initialCount is greater than maximumCount.
//
// T:System.ArgumentOutOfRangeException:
// maximumCount is less than 1. -or- initialCount is less than 0.
public Semaphore(int initialCount, int maximumCount);
//
// Summary:
// Initializes a new instance of the System.Threading.Semaphore class, specifying
// the initial number of entries and the maximum number of concurrent entries, and
// optionally specifying the name of a system semaphore object.
//
// Parameters:
// initialCount:
// The initial number of requests for the semaphore that can be granted concurrently.
//
// maximumCount:
// The maximum number of requests for the semaphore that can be granted concurrently.
//
// name:
// The name of a named system semaphore object.
//
// Exceptions:
// T:System.ArgumentException:
// initialCount is greater than maximumCount. -or- name is longer than 260 characters.
//
// T:System.ArgumentOutOfRangeException:
// maximumCount is less than 1. -or- initialCount is less than 0.
//
// T:System.IO.IOException:
// A Win32 error occurred.
//
// T:System.UnauthorizedAccessException:
// The named semaphore exists and has access control security, and the user does
// not have System.Security.AccessControl.SemaphoreRights.FullControl.
//
// T:System.Threading.WaitHandleCannotBeOpenedException:
// The named semaphore cannot be created, perhaps because a wait handle of a different
// type has the same name.
public Semaphore(int initialCount, int maximumCount, string name);
//
// Summary:
// Initializes a new instance of the System.Threading.Semaphore class, specifying
// the initial number of entries and the maximum number of concurrent entries, optionally
// specifying the name of a system semaphore object, and specifying a variable that
// receives a value indicating whether a new system semaphore was created.
//
// Parameters:
// initialCount:
// The initial number of requests for the semaphore that can be satisfied concurrently.
//
// maximumCount:
// The maximum number of requests for the semaphore that can be satisfied concurrently.
//
// name:
// The name of a named system semaphore object.
//
// createdNew:
// When this method returns, contains true if a local semaphore was created (that
// is, if name is null or an empty string) or if the specified named system semaphore
// was created; false if the specified named system semaphore already existed. This
// parameter is passed uninitialized.
//
// Exceptions:
// T:System.ArgumentException:
// initialCount is greater than maximumCount. -or- name is longer than 260 characters.
//
// T:System.ArgumentOutOfRangeException:
// maximumCount is less than 1. -or- initialCount is less than 0.
//
// T:System.IO.IOException:
// A Win32 error occurred.
//
// T:System.UnauthorizedAccessException:
// The named semaphore exists and has access control security, and the user does
// not have System.Security.AccessControl.SemaphoreRights.FullControl.
//
// T:System.Threading.WaitHandleCannotBeOpenedException:
// The named semaphore cannot be created, perhaps because a wait handle of a different
// type has the same name.
public Semaphore(int initialCount, int maximumCount, string name, out bool createdNew); //
// Summary:
// Opens the specified named semaphore, if it already exists.
//
// Parameters:
// name:
// The name of the system semaphore to open.
//
// Returns:
// An object that represents the named system semaphore.
//
// Exceptions:
// T:System.ArgumentException:
// name is an empty string. -or- name is longer than 260 characters.
//
// T:System.ArgumentNullException:
// name is null.
//
// T:System.Threading.WaitHandleCannotBeOpenedException:
// The named semaphore does not exist.
//
// T:System.IO.IOException:
// A Win32 error occurred.
//
// T:System.UnauthorizedAccessException:
// The named semaphore exists, but the user does not have the security access required
// to use it.
public static Semaphore OpenExisting(string name);
//
// Summary:
// Opens the specified named semaphore, if it already exists, and returns a value
// that indicates whether the operation succeeded.
//
// Parameters:
// name:
// The name of the system semaphore to open.
//
// result:
// When this method returns, contains a System.Threading.Semaphore object that represents
// the named semaphore if the call succeeded, or null if the call failed. This parameter
// is treated as uninitialized.
//
// Returns:
// true if the named semaphore was opened successfully; otherwise, false.
//
// Exceptions:
// T:System.ArgumentException:
// name is an empty string. -or- name is longer than 260 characters.
//
// T:System.ArgumentNullException:
// name is null.
//
// T:System.IO.IOException:
// A Win32 error occurred.
//
// T:System.UnauthorizedAccessException:
// The named semaphore exists, but the user does not have the security access required
// to use it.
public static bool TryOpenExisting(string name, out Semaphore result);
//
// Summary:
// Exits the semaphore and returns the previous count.
//
// Returns:
// The count on the semaphore before the System.Threading.Semaphore.Release* method
// was called.
//
// Exceptions:
// T:System.Threading.SemaphoreFullException:
// The semaphore count is already at the maximum value.
//
// T:System.IO.IOException:
// A Win32 error occurred with a named semaphore.
//
// T:System.UnauthorizedAccessException:
// The current semaphore represents a named system semaphore, but the user does
// not have System.Security.AccessControl.SemaphoreRights.Modify. -or- The current
// semaphore represents a named system semaphore, but it was not opened with System.Security.AccessControl.SemaphoreRights.Modify.
public int Release();
//
// Summary:
// Exits the semaphore a specified number of times and returns the previous count.
//
// Parameters:
// releaseCount:
// The number of times to exit the semaphore.
//
// Returns:
// The count on the semaphore before the System.Threading.Semaphore.Release* method
// was called.
//
// Exceptions:
// T:System.ArgumentOutOfRangeException:
// releaseCount is less than 1.
//
// T:System.Threading.SemaphoreFullException:
// The semaphore count is already at the maximum value.
//
// T:System.IO.IOException:
// A Win32 error occurred with a named semaphore.
//
// T:System.UnauthorizedAccessException:
// The current semaphore represents a named system semaphore, but the user does
// not have System.Security.AccessControl.SemaphoreRights.Modify rights. -or- The
// current semaphore represents a named system semaphore, but it was not opened
// with System.Security.AccessControl.SemaphoreRights.Modify rights.
public int Release(int releaseCount);
}
}

System.Threading.Semaphore

看它的summary,我们大体了解这个类就是专门用来做并发限制的,它具有三个构造函数,我们最关心的,就是其中两个参数int initialCount, int maximumCount及其涵义;

initialCount:能够被Semaphore 授予的数量的初始值;

maximumCount:能够被Semaphore 授予的最大值;

字面意思可能不太好理解,我们来把官宣翻译成普通话:)

举个栗子,我们把Semaphore看成是一个用来装钥匙的盒子,每一个想要进入队列这道“门”的任务,都需要先从盒子里取一把钥匙,才能进入;initialCount,就是说,这个盒子,一开始的时候,放几把钥匙,但是进入队列的任务,时时不肯出来,不归还钥匙,无钥匙可用,这时管理员就决定再多配一些钥匙,以备用,于是,一些新钥匙又被放入盒子里,但盒子的容积有限,一共能容纳多少把钥匙,就是maximumCount了。

当然,我们常见的情况是构造盒子的时候,initialCount == maximumCount,特殊场景下,会设置不相同,这个视具体业务而定。然而,maximumCount不能小于initialCount,initialCount不能小于0,这个是硬性的。

这样是不是initialCount 和 maximumCount就很容易理解了。

同时,Semaphore 还有非常重要的方法(Release)方法,再把上面的栗子举起来说话,Release就是归还钥匙,任务结束了,那么就出门还钥匙,然后其它在门口等待的任务就可以领到钥匙进门了:)

再者,Semaphore 继承自System.Threading.WaitHandle,于是乎,它就具有了一系列Wait方法,当有新任务来领钥匙,一看,盒子空了,那怎么办呢,等吧,但是等多久呢,是一直等下去还是等一个超时时间,这就看业务逻辑了。

在我的SemaphoreConcurrentPolicy实现里,会提供一个超时时间,爬虫蚂蚁小队长会判断,如果没拿到钥匙,就会再次回来尝试取钥匙。

OK,接下来,就是对我们的蚂蚁小队长进行改造了:

 namespace MikeWare.Core.Components.CrawlerFramework
{
using MikeWare.Core.Components.CrawlerFramework.Policies;
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks; public class LeaderAnt : Ant
{
private ConcurrentQueue<JobContext> Queue;
private ManualResetEvent mre = new ManualResetEvent(false);
public AConcurrentPolicy EnqueuePolicy { get; set; } …… public void Enqueue(JobContext context)
{
if (null != EnqueuePolicy)
{
while (!EnqueuePolicy.WaitOne(TimeSpan.FromMilliseconds()) && !mre.WaitOne())
continue;
} Queue.Enqueue(context);
} ……
}

领队 —— LeaderAnt

主要是在入队的时候,增加了拿钥匙的环节;

 namespace MikeWare.Crawlers.EBooks.Bizs
{
using MikeWare.Core.Components.CrawlerFramework;
using MikeWare.Core.Components.CrawlerFramework.Policies;
using MikeWare.Crawlers.EBooks.Entities;
using System;
using System.Collections.Generic;
using System.Net; public class EBooksCrawler
{
public static void Start(int pageIndex, DateTime lastUpdateTime)
{
var leader = new LeaderAnt()
{
EnqueuePolicy = new SemaphoreConcurrentPolicy(, )
//EnqueuePolicy = new PeriodEnqueuePolicy(TimeSpan.FromMilliseconds(150))
}; var newContext = new JobContext
{
JobName = $"奇书网-最新电子书-列表-第{pageIndex.ToString("")}页",
Uri = $"http://www.xqishuta.com/s/new/index_{pageIndex}.html",
Method = WebRequestMethods.Http.Get,
InParams = new Dictionary<string, object>(),
Analizer = new BooksListAnalizer(),
};
newContext.InParams.Add(Consts.PAGE_INDEX, );
newContext.InParams.Add(Consts.LAST_UPDATE_TIME, DateTime.MinValue); leader.Enqueue(newContext); leader.Work();
}
}
}

业务层 —— EBooksCrawler

主要是在构造LeaderAnt的时候,为其指定了我们要使用的策略;

同时需要注意的是,这个SemaphoreConcurrentPolicy并发策略的实现,并没有规定入队的时间间隔,而是控制了最大的队列长度,所以,并发的频率可能高,可能低,这个策略可以用来制衡资源的使用情况。关于入队时间间隔,可以使用PeriodConcurrentPolicy或自己实现策略来控制;

另一个策略的实现,我们就不在这里细说了。有兴趣的同学可以看看源码。

好了,本节的内容就这么多吧,相信大家对并发策略的制定与实现,都有了各自的理解。

后续章节同样精彩,敬请期待……

喜欢本系列丛书的朋友,可以点击链接加入QQ交流群(994761602)【C# 破境之道】
方便各位在有疑问的时候可以及时给我个反馈。同时,也算是给各位志同道合的朋友提供一个交流的平台。
需要源码的童鞋,也可以在群文件中获取最新源代码。

《C# 爬虫 破境之道》:第二境 爬虫应用 — 第七节:并发控制与策略的更多相关文章

  1. Python爬虫实践 -- 记录我的第二只爬虫

    1.爬虫基本原理 我们爬取中国电影最受欢迎的影片<红海行动>的相关信息.其实,爬虫获取网页信息和人工获取信息,原理基本是一致的. 人工操作步骤: 1. 获取电影信息的页面 2. 定位(找到 ...

  2. 《C# 爬虫 破境之道》:第二境 爬虫应用 — 第二节:以事件驱动状态、数据处理

    续上一节内容,对Web爬虫进行进一步封装,通过委托将爬虫自己的状态变化以及数据变化暴露给上层业务处理或应用程序. 为了方便以后的扩展,我先定义一个蚂蚁抽象类(Ant),并让WorkerAnt(工蚁)继 ...

  3. 《C# 爬虫 破境之道》:第二境 爬虫应用 — 第一节:HTTP协议数据采集

    首先欢迎您来到本书的第二境,本境,我们将全力打造一个实际生产环境可用的爬虫应用了.虽然只是刚开始,虽然路漫漫其修远,不过还是有点小鸡冻:P 本境打算针对几大派生类做进一步深耕,包括与应用的结合.对比它 ...

  4. 《C# 爬虫 破境之道》:第二境 爬虫应用 — 第三节:处理压缩数据

    续上一节内容,本节主要讲解一下Web压缩数据的处理方法. 在HTTP协议中指出,可以通过对内容压缩来减少网络流量,从而提高网络传输的性能. 那么问题来了,在HTTP中,采用的是什么样的压缩格式和机制呢 ...

  5. 《C# 爬虫 破境之道》:第一境 爬虫原理 — 第二节:WebRequest

    本节主要来介绍一下,在C#中制造爬虫,最为常见.常用.实用的基础类 ------ WebRequest.WebResponse. 先来看一个示例 [1.2.1]: using System; usin ...

  6. 《C# 爬虫 破境之道》:第二境 爬虫应用 — 第四节:小说网站采集

    之前的章节,我们陆续的介绍了使用C#制作爬虫的基础知识,而且现在也应该比较了解如何制作一只简单的Web爬虫了. 本节,我们来做一个完整的爬虫系统,将之前的零散的东西串联起来,可以作为一个爬虫项目运作流 ...

  7. 《C# 爬虫 破境之道》:第二境 爬虫应用 — 第五节:小总结带来的优化与重构

    在上一节中,我们完成了一个简单的采集示例.本节呢,我们先来小结一下,这个示例可能存在的问题: 没有做异常处理 没有做反爬应对策略 没有做重试机制 没有做并发限制 …… 呃,看似平静的表面下还是隐藏着不 ...

  8. 《C# 爬虫 破境之道》:第二境 爬虫应用 — 第六节:反爬策略研究

    之前的章节也略有提及反爬策略,本节,我们就来系统的对反爬.反反爬的种种,做一个了结. 从防盗链说起: 自从论坛兴起的时候,网上就有很多人会在论坛里发布一些很棒的文章,与当下流行的“点赞”“分享”一样, ...

  9. 《C# 爬虫 破境之道》:第一境 爬虫原理 — 第六节:第一境尾声

    在第一境中,我们主要了解了爬虫的一些基本原理,说原理也行,说基础知识也罢,结果就是已经知道一个小爬虫是如何诞生的了~那么现在,请默默回想一下,在第一境中,您都掌握了哪些内容?哪些还比较模糊?如果还有什 ...

随机推荐

  1. 使用redis的zset实现高效分页查询(附完整代码)

    一.需求 移动端系统里有用户和文章,文章可设置权限对部分用户开放.现要实现的功能是,用户浏览自己能看的最新文章,并可以上滑分页查看. 二.数据库表设计 涉及到的数据库表有:用户表TbUser.文章表T ...

  2. C语言之枚举数据类型

    枚举数据类型概述:1.枚举类型是C语言的一种构造类型.它用于声明一组命名的常数,2.当一个变量有几种可能的取值时,可以将它定义为枚举类型.3.枚举类型是由用户自定义的由多个命名枚举常量构成的类型,其声 ...

  3. 详细解析Java虚拟机的栈帧结构

    欢迎关注微信公众号:万猫学社,每周一分享Java技术干货. 什么是栈帧? 正如大家所了解的,Java虚拟机的内存区域被划分为程序计数器.虚拟机栈.本地方法栈.堆和方法区.(什么?你还不知道,赶紧去看看 ...

  4. 1、使用 as 而不要用 is

    public class ShouldAsNotIs { public void ShouldAs() { object a = new ShouldAsNotIs(); var b = a as S ...

  5. vue学习笔记2:藕断丝连的 v-show 和 v-if

    一.知识点 vue指令 v-show v-if 二.代码案例 v-show <div v-show="isShow">动态显示或隐藏</div> <! ...

  6. Python Global和Nonlocal的用法

    nonlocal 和 global 也很容易混淆.简单记录下自己的理解. 解释 global 总之一句话,作用域是全局的,就是会修改这个变量对应地址的值. global 语句是一个声明,它适用于整个当 ...

  7. Ansible Playbooks常用模块

    File模块 在目标主机创建文件或目录,并赋予其系统权限 - name: create a file file:'path=/oot/foo.txt state=touch mode=0755 own ...

  8. 20190925Java课堂记录(一)

    判断字符串是否回文 设计思想 利用递归,每次返回长度减二的字符串,最终返回结果 源程序代码 import java.util.Scanner; public class palindrome { st ...

  9. Asp.Net Core 已支持 gRPC-Web !!

    grpc-dotnet 项目在 PR #695 完成了 ASP.NET Core 服务与 .NET Core gRPC 客户端的 gRPC-Web 实现. 虽然目前还是实验性项目,但是并不阻碍我们为之 ...

  10. [题解][Codeforces]Good Bye 2019 简要题解

    构造题好评,虽然这把崩了 原题解 A 题意 二人游戏,一个人有 \(k_1\) 张牌,另一个人 \(k_2\) 张,满足 \(2\le k_1+k_2=n\le 100\),每张牌上有一个数,保证所有 ...