你好C#!C++为你服务





5.00/5 (23投票s)
一个用C#使用TcpClient实现的.NET Core客户端,和一个用C++实现的多线程服务器
引言
这是我正在开发的一个个人项目中的第四篇文章。虽然它仍在进行中,但我感觉有些事情开始步入正轨。
目前,我有一个C++框架,可以轻松创建一个使用TCP/IP通信的多线程服务器。
由于没有客户端的服务器相当无意义,我觉得用C#为.NET Core创建一个客户端库,以便与服务器一起使用会很有趣。
我希望你也会觉得有趣——因为它表现得相当不错
Wrote 527040 records in 1,7029398 seconds, 309488,33305792726 records per second.
Wrote 527040 records in 1,7059422 seconds, 308943,64416332514 records per second.
Retrieved 1054080 records in 1,4269109 seconds, 738714,6597590642 records per second.
Retrieved 1054080 records in 1,4674932 seconds, 718286,1222116736 records per second.
服务器使用微软的可扩展存储引擎(ESE)存储数据。
使用System.Net.Sockets.TcpClient
真的很容易,如果我们不考虑数据模型的DTO类,客户端只用了略多于600行的C#代码实现。我认为这相当简洁,因为
- 客户端实现了21个方法,可用于
- 连接到服务器
- 断开与服务器的连接
- 测试仅向服务器发送数据而不更新数据库的性能
- 对服务器实现的数据模型执行CRUD操作,更新数据库
- 有相当程度的错误处理,包括在.NET端抛出C++代码在服务器上抛出的异常。
服务器实现的数据模型是我为使用C++简化可扩展存储引擎(ESE)API - 第2部分创建的。
客户端和服务器使用一种高效且易于实现的自定义二进制格式交换数据。
我知道你以前听过这个
世界上只有10种人:一种是懂二进制的,一种是不懂二进制的。.
但如今,人们很容易忘记计算机只理解二进制数据。当然,你可以使用Snappy或类似工具来压缩JSON和XML以减少带宽需求,但这会增加生成和处理数据的成本——就好像解析和生成文本的成本还不够高一样。如果你尝试过为浮点数编写高效的解析器,你就会明白我的意思。
JSON和XML非常适合与其他方交换数据,因为这些格式在不同平台和技术栈中得到广泛支持,但一旦数据进入你的“池塘”,你就应该像谷歌、微软和任何其他大型云运营商一样,使用Protocol Buffers或类似工具来序列化和反序列化你的数据
- Protocol Buffers:根据常见问题解答,“Google内部几乎所有人都在使用Protocol buffers”
- Bond:根据项目的github页面,“Bond在微软的高规模服务中广泛使用。”
- Apache Thrift:最初由Facebook创建
- FlatBuffers:最初由Google为游戏开发和其他对性能要求高的应用程序创建。
- Cap'n Proto:由Protocol Buffers版本2的主要作者Kenton Varda编写
你的情况显然会有所不同,但对于云部署而言,你可以将运行后端服务的成本降低90%以上,同时提供响应更快的服务。
二进制与JSON
大多数人会觉得这很明显,但如果你有疑问:这里有一些数据来支持上述主张,这些数据是由用于演示如何使用C#客户端库与服务器交互的C#程序生成的。
首先,我们向服务器发送一批记录,不存储数据,只是为了了解其性能。
Sendt 527040000 records in 31.4532607 seconds, 16756291.343746118 records per second.
服务器收到了一千批527,040个SensorPoint
对象,这些对象被放置在一个std::vector
对象中,准备在服务器上进行进一步处理。
然后我们将相同数量的数据序列化并以JSON格式发送到服务器。
Serialized and sendt 527040000 records as JSON in 462,185403 seconds,
1140321,6037958688 records per second.
服务器接收到一千批527,040个JSON格式的SensorPoint
对象,这些对象被放置在一个std::string
对象中,等待解析。这比发送二进制有效负载花费了更多的时间。
我想指出一点:通常,导致JSON数据传输如此昂贵的不是网络传输,而是生成JSON的过程。
仅仅将数据序列化为JSON,而不发送到任何地方
Serialized 527040000 records to JSON in 419,02034 seconds,
1257790,970242638 records per second.
清楚地表明,超过90%的时间花在了将数据序列化为JSON上,而不是在服务器上进行数据传输和接收。
将JSON数据转换回计算机程序可以使用的形式,开销更大。
Deserialized 527040000 records from JSON in 618,1851519 seconds,
852560,108213754 records per second.
使用为服务器创建的二进制格式进行数据序列化、发送和反序列化,效率比使用JSON高出33倍以上,这是一项可以用来削减成本的知识。
要自行执行这些实验,您需要构建提供的两个项目:
ExampleSocketServer02
Harlinn.Examples.TestTcpClient.Net
要运行服务器,请使用以下命令:
ExampleSocketServer02.exe -c -r -t -u F:\Database\ESE\Test\Database.edb
其中“F:\Database\ESE\Test\Database.edb”是数据库目录的路径。Harlinn.Examples.TestTcpClient.Net
不带任何参数,只需从命令行执行它即可查看输出到控制台。
构建代码
在 $(SolutionDir)Readme 文件夹中的 Build.md 文件中提供了构建代码的说明。
ESE测试使用环境变量HCC_TEST_DATA_ROOT
,该变量必须设置为测试可以创建数据库的目录的完整路径。请确保包含此目录的驱动器上至少有10 GB的可用空间。
TcpClient
如果您之前尝试过使用TcpClient
并对其性能感到失望,那很可能是因为您尝试直接使用NetworkStream
执行了许多小的读写操作。读写小片段数据是数据序列化和反序列化的典型操作,因此任何能提高其性能的方法都很重要。幸运的是,.NET为此问题提供了开箱即用的解决方案:System.IO.BufferedStream
。
void InitializeSession(ulong id)
{
_sessionId = id;
var networkStream = GetNetworkStream();
_bufferedStream = new System.IO.BufferedStream(networkStream, ushort.MaxValue / 2);
_reader = new BinaryReader(_bufferedStream, System.Text.Encoding.Default, true);
_writer = new BinaryWriter(_bufferedStream, System.Text.Encoding.Default, true);
}
客户端在用于序列化和反序列化数据的BinaryReader
和BinaryWriter
对象之间放置了一个BufferedStream
,这确实大大提高了性能!
如你所知,使用BinaryReader
和BinaryWriter
进行序列化是一个直接的过程。
public class Named : IReadWrite
{
Guid _id;
string _name;
...
public virtual void Read(BinaryReader reader)
{
_id = reader.ReadGuid();
_name = reader.ReadString();
}
public virtual void Write(BinaryWriter writer)
{
writer.Write(_id);
writer.Write(_name);
}
}
在C++方面,我们也有一个BinaryReader
类和一个BinaryWriter
类,只要字符和字符串数据使用默认编码,它们就能很好地与.NET对应项配合使用。
每个C#客户端实现都执行相同的任务序列
- 写入请求
- 写入一个包含
RequestReplyType
的字节,用于标识要在服务器上调用的函数。 - 写入会话ID和请求ID。
- 如果函数有参数,则写入参数。
- 在
BinaryWriter
上调用Flush()
,使BufferedStream
将所有缓冲数据刷新到NetworkStream
,确保到目前为止写入的所有内容都发送到服务器。
- 写入一个包含
- 读取服务器回复
- 读取一个字节,其中包含标识在服务器上调用的函数的
RequestReplyType
,或者RequestReplyType.Fault
,表示回复包含错误信息。 - 如果
RequestReplyType
与发送的匹配- 从回复中读取会话ID和请求ID,检查它们是否与发送到服务器的匹配。
- 如果服务器预期有回复数据,则读取回复数据。
- 如果
RequestReplyType
等于RequestReplyType.Fault
- 读取服务器发送的错误信息并抛出异常。
- 如果
RequestReplyType
是其他任何值- 抛出异常。
- 读取一个字节,其中包含标识在服务器上调用的函数的
典型的服务器调用实现如下:
public CatalogItem GetCatalogItem(Guid itemId)
{
lock (_syncObj)
{
const RequestReplyType requestReplyType = RequestReplyType.GetCatalogItem;
CatalogItem result = null;
var requestId = WriteRequest(requestReplyType, itemId);
ReadAndValidateReplyHeader(requestReplyType, requestId);
var reader = Reader;
var found = reader.ReadBoolean();
if (found)
{
result = CatalogItemFactory.Read(reader);
}
return result;
}
}
客户端库实现的每个方法都与上述代码片段一样简单。
服务器向流中写入一个布尔值,如果找到了请求的CatalogItem
,则该值设置为true
。CatalogItem
是Catalog
和Asset
类的基类,CatalogItemFactory
会读取一个16位值,指示它是哪种类型,然后创建该类型的对象,该对象随后从流中反序列化该类型的其余数据。
public static CatalogItem Create(CatalogItemType kind)
{
switch (kind)
{
case CatalogItemType.Catalog:
return new Catalog();
case CatalogItemType.Asset:
return new Asset();
default:
throw new Exception("Unsupported catalog item type");
}
}
public static CatalogItem Read(BinaryReader reader)
{
var kind = CatalogItem.ReadKind(reader);
var result = Create(kind);
result.Read(reader);
return result;
}
相当简单,但也是一种强大的技术,可用于处理属于大型继承层次结构类型的反序列化。
在GetCatalogItem
的实现中,我调用了WriteRequest
ulong WriteRequest(RequestReplyType requestReplyType, Guid id)
{
var result = WriteSessionHeader(requestReplyType);
var writer = Writer;
writer.Write(id);
writer.Flush();
return result;
}
将RequestReplyType.GetCatalogItem
作为requestReplyType
参数传递,我将很快解释。
对writer.Flush()
的调用非常重要,因为它将导致BufferedStream
将其缓冲区内容写入NetworkStream
。这也是客户端在等待服务器回复之前做的最后一件事。
ulong WriteSessionHeader(RequestReplyType requestReplyType)
{
RequireValidSession();
var requestId = NewRequest();
var writer = Writer;
writer.Write((byte)requestReplyType);
writer.Write(_sessionId);
writer.Write(requestId);
return requestId;
}
WriteSessionHeader
是向服务器写入请求开始的方法,首先写入requestReplyType
参数的值作为单个字节,指示它希望服务器执行哪个功能。接下来是一个64位整数,标识服务器上的会话,然后是一个64位计数器,标识此客户端向服务器发出的请求。
RequestReplyType
、会话ID和请求ID将包含在服务器回复的头部中,因此我们知道服务器端没有出现严重的混淆。服务器使用线程池,其中会话不绑定到特定线程,因此我将此作为一种验证方式放入协议中,这就是ReadAndValidateReplyHeader
执行的任务。
void ReadAndValidateReplyHeader(RequestReplyType requestReplyType, ulong expectedRequestId)
{
var reader = Reader;
var replyType = (RequestReplyType)reader.ReadByte();
if (replyType != requestReplyType)
{
HandleInvalidReplyType(replyType, requestReplyType);
}
else if (replyType == RequestReplyType.Fault)
{
HandleFault(reader);
}
ulong sessionId = reader.ReadUInt64();
if (sessionId != _sessionId)
{
var message = string.Format("Invalid session id: {0}, expected:{1} ",
sessionId, _sessionId);
throw new Exception(message);
}
ulong requestId = reader.ReadUInt64();
if (requestId != expectedRequestId)
{
var message = string.Format("Invalid request id: {0}, expected:{1} ",
requestId, expectedRequestId);
throw new Exception(message);
}
}
HandleFault
方法抛出一个包含服务器错误消息的普通Exception
。
void HandleFault(BinaryReader reader)
{
var faultReply = new Types.FaultReply();
faultReply.Read(reader);
throw new Exception(faultReply.Message);
}
以上内容不多,但在建立从服务器到客户端传递错误的良好机制方面大有裨益。
以这种方式使用TcpClient
异常简单,从服务器每秒检索738,714条记录的方法代码也同样简单。
public SensorPoint[] GetSensorPoints(Guid sensorId,
DateTime intervalStart, DateTime intervalEnd)
{
lock (_syncObj)
{
const RequestReplyType requestReplyType = RequestReplyType.GetSensorPoints;
var requestId = WriteRequest(requestReplyType, sensorId, intervalStart, intervalEnd);
ReadAndValidateReplyHeader(requestReplyType, requestId);
var reader = Reader;
var count = reader.ReadInt32();
var result = new SensorPoint[count];
for (int i = 0; i < count; i++)
{
result[i].Read(reader);
}
return result;
}
}
一旦我们知道有一个有效的回复,我们就会从流中检索要读取的记录数量,并且由于SensorPoint
实现了IReadWrite
接口,它知道如何反序列化自身。
服务器::TcpSimpleListener
本文的代码基于我为上一篇关于ESE的文章创建的代码。我对该代码进行了一些小的修改,并且实现一个工作服务器示例所需的几乎所有其他内容都包含在一个头文件中:ServerEngine.h。
构建ExampleSocketServer02
项目后,使用以下命令运行服务器:
ExampleSocketServer02.exe -c -r -t -u F:\Database\ESE\Test\Database.edb
main
函数的实现很简单。
int main( int argc, char* argv[] )
{
try
{
EngineOptions options;
if ( ParseOptions( argc, argv, options ) )
{
ServerEngine engine( options, "TestInstance" );
WSA wsa;
constexpr size_t ThreadPoolSize = 12;
constexpr size_t SocketCount = 200;
IO::Context context( ThreadPoolSize );
Address address( options.Port );
Server::TcpSimpleListener<Protocol>
listener( context, address, SocketCount, &engine );
context.Start( );
puts( "Press enter to exit" );
while ( getc( stdin ) != '\n' );
context.Stop( );
}
}
catch ( std::exception& exc )
{
std::string message = exc.what( );
printf( "Exception: %s", message.c_str( ) );
}
return 0;
}
此服务器实现使用模板
template <typename ProtocolT>
class TcpSimpleListener : public TcpListener<TcpSimpleListener<ProtocolT>,
TcpSimpleConnectionHandler<TcpSimpleListener<ProtocolT>, ProtocolT> >
{
public:
using Base = TcpListener<TcpSimpleListener<ProtocolT>,
TcpSimpleConnectionHandler<TcpSimpleListener<ProtocolT>, ProtocolT> >;
template<typename ...Args>
TcpSimpleListener( IO::Context& context,
const Address& listenAddress,
size_t clientSocketCount,
Args&&... args )
: Base( context, listenAddress, clientSocketCount, std::forward<Args>( args )... )
{
}
};
可用于协议实现。任何实现具有以下签名的模板函数的类都可以使用
template<IO::StreamReader StreamReaderT, IO::StreamWriter StreamWriterT>
bool Process( IO::BinaryReader<StreamReaderT>& requestReader,
IO::BinaryWriter<StreamWriterT>& replyWriter )
{
...
}
框架将为每个连接创建一个由ProtocolT
模板参数指定的类的实例,将通过args
提供的参数存储在std::tuple<>
中,该元组将用于将这些参数传递给ProtocolT
类型的构造函数。
就像C#客户端一样,我们也有一个IO::BinaryReader<>
和一个IO::BinaryWriter<>
,它们将用于序列化和反序列化。协议的实际处理由Protocol
类执行。
读写操作都是缓冲的,对底层套接字的写入是完全异步的,而从套接字的读取是部分异步的。写入缓冲区来自所有连接处理程序共享的缓冲区池,而每个连接处理程序都有一个读取缓冲区。写入操作是排队的,使得协议实现能够生成大量输出而无需等待客户端跟上。
当Protocol::Process(…)
返回false
时,框架将关闭连接;当它返回true
时,它将在刷新连接写入队列中所有未完成的写入后,开始新的异步读取操作。
每当有数据可供处理时,框架就会调用Protocol::Process(…)
。Protocol
的实现只知道ServerEngine
,以及IO::BinaryReader<>
和IO::BinaryWriter<>
对象。这确保了Protocol
的实现与传输无关,这很好。
Protocol::Process(…)
函数只是读取标识要调用的函数的字节,并使用switch
来调用实现。
template<IO::StreamReader StreamReaderT, IO::StreamWriter StreamWriterT>
bool Process( IO::BinaryReader<StreamReaderT>& requestReader,
IO::BinaryWriter<StreamWriterT>& replyWriter )
{
bool result = true;
auto requestType = ReadRequestType( requestReader );
switch ( requestType )
{
...
case RequestReplyType::CloseSession:
{
CloseSession( requestReader, replyWriter );
result = false;
}
break;
case RequestReplyType::GetCatalogItem:
{
GetCatalogItem( requestReader, replyWriter );
}
break;
...
}
return result;
}
每个服务器端功能实现都执行相同的任务序列
- 读取会话ID和请求ID。
- 如果函数有参数,则读取预期参数。
- 使用会话ID查找会话实现。
- 如果这是一个有效的会话
- 锁定会话。
- 使用
session
对象执行请求的操作。 - 如果一切顺利
- 通过写入回复头回复客户端
- 写入一个包含
RequestReplyType
值的字节,用于标识函数。 - 写入会话ID和请求ID。
- 写入一个包含
- 如果函数产生了任何回复数据,则写入回复数据。
- 通过写入回复头回复客户端
- 如果捕获到异常
- 写入一个包含
RequestReplyType::Fault
值的字节 - 写入会话ID
- 写入错误代码
- 写入错误消息
- 写入一个包含
- 如果这不是一个有效的会话
- 使用与异常相同的格式向客户端写入错误回复。
既然你已经了解了GetCatalogItem
的客户端实现,那么理解它的服务器端对应部分应该很容易。
template<IO::StreamReader StreamReaderT, IO::StreamWriter StreamWriterT>
void GetCatalogItem( IO::BinaryReader<StreamReaderT>& requestReader,
IO::BinaryWriter<StreamWriterT>& replyWriter )
{
constexpr RequestReplyType ReplyType = RequestReplyType::GetCatalogItem;
auto [sessionId, requestId] = ReadSessionRequestHeader( requestReader );
auto itemId = requestReader.ReadGuid( );
auto* session = engine_.FindSession( sessionId );
if ( session )
{
SERVERSESSION_TRY
{
CatalogItem catalogItem;
auto found = session->GetCatalogItem( itemId, catalogItem );
WriteReplyHeader<ReplyType>( replyWriter, sessionId, requestId );
replyWriter.Write( found );
if ( found )
{
Write( replyWriter, catalogItem );
}
}
SERVERSESSION_CATCH( );
}
else
{
InvalidSession( replyWriter, sessionId );
}
}
ReadSessionRequestHeader
只是使用BinaryReader
读取会话ID和请求ID。
template<IO::StreamReader StreamReaderT>
std::pair<UInt64, UInt64> ReadSessionRequestHeader( IO::BinaryReader<StreamReaderT>& reader )
{
auto sessionId = reader.ReadUInt64( );
auto requestId = reader.ReadUInt64( );
return { sessionId, requestId };
}
然后,实现从流中读取单个参数itemId
。
至此,我们已经完成了请求信息的读取,现在是时候检索会话了。如果找到会话,我们将锁定该会话;如果未找到,则InvalidSession
函数将此情况报告回客户端。
#define SERVERSESSION_TRY std::unique_lock lock( *session ); try
SERVERSESSION_TRY
是一个宏,它锁定会话并启动一个try
/catch
块。由实现抛出的任何异常都将被SERVERSESSION_CATCH()
捕获,它将把适当的错误信息写入回复流。
#define SERVERSESSION_CATCH() \
catch ( const Core::Exception& ex ) \
{ \
WriteFault( replyWriter, sessionId, ex ); \
} \
catch ( const std::exception& exc ) \
{ \
WriteFault( replyWriter, sessionId, exc ); \
} \
catch ( const ThreadAbortException& ) \
{ \
throw; \
} \
catch ( ... ) \
{ \
UnknownException( replyWriter, sessionId ); \
}
只要没有写入回复,上述方法就能很好地工作。如果在序列化回复时出现问题,那将表明套接字状态存在严重问题,导致错误报告函数也抛出异常——这将导致服务器实现重置连接。
在对有效会话进行锁定后,GetCatalogItem
的实现只是调用session
对象上的GetCatalogItem
实现,然后将结果写入回复流。
ServerEngine 和 ServerSession
ServerEngine
类派生自template
,其中D
是派生类型ServerEngine
,T
是ServerSession
类型。
通过使用“奇异递归模板模式”,EngineT
模板可以将对其派生类型的引用传递给会话实现的构造函数。ServerEngine
类除了EngineT
模板实现的功能外,不提供任何功能,但模板需要知道它将管理哪种session
对象。
同样,ServerSession
派生自template
,其中T
是SessionEngine
类。ServerSession
添加了一些关键功能,这些功能是能够在使用线程池中的线程处理客户端请求时安全锁定会话所必需的。
ServerSession
类使用来自使用Visual C++和Windows API进行同步文章的CriticalSection
。
class ServerSession : public SessionT<ServerEngine>
{
...
std::unique_ptr<CriticalSection> criticalSection_;
public:
...
};
使用unique_ptr<>
来保存CriticalSection
的原因是,CriticalSection
的生命周期将略微超出会话的生命周期。
void Close( )
{
std::unique_lock lock( *criticalSection_ );
auto ciriticalSection = std::move( criticalSection_ );
Base::Close( );
}
Base::Close()
将关闭ESE会话句柄,然后通知ServerEngine
销毁会话,我们需要允许锁的析构函数安全地调用临界区的unlock()
,因此它不能在锁超出范围之前消失。
ServerSession
实现了C++ BasicLockable
要求,该要求描述了提供独占阻塞的类型的最低特征。
void lock( ) const
{
criticalSection_->Enter( );
auto& eseSession = EseSession( );
UInt64 context = (UInt64)this;
eseSession.SetContext( context );
printf( "Set context: %llu\n", context );
}
lock()
函数在CriticalSection
上调用Enter()
,然后调用ESE会话上的SetContext(…)
,将该会话与当前线程关联。此关联会覆盖ESE的默认行为,即给定会话的整个事务必须在同一线程中发生。
unlock()
函数在释放临界区上持有的锁之前,移除与当前线程的关联。
void unlock( ) const
{
auto& eseSession = EseSession( );
eseSession.ResetContext( );
criticalSection_->Leave( );
UInt64 context = (UInt64)this;
printf( "Reset context: %llu\n", context );
}
上述lock()
和unlock()
的实现允许会话在一次调用中由线程池中的一个线程服务,在下一次调用中由另一个线程服务。
使用C#客户端库
下载内容包括用于测试C#客户端库的可执行文件的源代码,这里有一些“亮点”。
客户端库的主类名为Session
,以下代码片段创建了一个会话对象并连接到服务器
void Connect()
{
_session = new Session("localhost", 42000);
_session.Connect();
}
关闭连接
void Disconnect()
{
if (_session != null)
{
_session.Close();
}
_session = null;
}
要构建数据模型,我们首先需要在目录结构的根部创建一个或多个目录。
var catalog = _session.CreateOrRetrieveCatalog(Guid.Empty, name);
传递一个空的Guid
表示这将是一个根目录,否则Guid
必须标识一个现有的目录。
一旦我们有了目录,我们就可以添加一个Asset
var asset = _session.CreateOrRetrieveAsset(catalog.Id, name);
然后我们可以创建一个Sensor
var sensor = _session.CreateOrRetrieveSensor(asset.Id, name);
有了Sensor
对象,我们就可以为该sensor
存储SensorPoint
数据的时间序列。
SensorPoint[] GenerateSensorPoints(Sensor sensor, TimeSpan offset )
{
var points = GenerateSensorPoints(offset);
var stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
_session.StoreSensorPoints(sensor.Id, points);
stopwatch.Stop();
var seconds = stopwatch.Elapsed.TotalSeconds;
var pointsPerSecond = points.Length / seconds;
Console.Out.WriteLine("Wrote {0} records in {1} seconds, {2} records per second.",
points.Length, seconds, pointsPerSecond);
return points;
}
检索某个时间间隔的时间序列数据是通过以下方式完成的:
_points1Retrieved = _session.GetSensorPoints(_sensor1a.Id, _intervalStart, _intervalEnd);
结束
希望您从本文中学到了一些有趣且有用的知识。我希望我能在这里提供足够的信息,让您对尝试其他方法来序列化和反序列化解决方案组件之间发送的数据感兴趣。
就我个人而言,我非常喜欢FlatBuffers和Cap'n Proto所能提供的性能,而微软的Bond具有我非常渴望的继承等特性。
Apache Thrift和Google的Protocol Buffers是使用最广泛的,它们拥有更大的生态系统。
它们各有优缺点,通过撰写本文,我的目标是提供一些见解,帮助您为您的项目选择合适的。
随着这个个人项目的推进,我将尝试找到方法,至少将其中一个集成到服务器框架中。
那么,下次再见:编程愉快!
历史
- 2020年9月18日
- 初次发布
- 2020年9月19日
- 修复了HCCEse.h中两个函数的内联缺失问题,ESE测试现在使用环境变量
HCC_TEST_DATA_ROOT
来配置测试数据库的创建位置。
- 修复了HCCEse.h中两个函数的内联缺失问题,ESE测试现在使用环境变量
- 2020年10月6日
- 错误修复,清理了大部分单元测试。
- 2020年10月7日
- 为
Harlinn.Common.Core
库添加更多单元测试
- 为
- 2020年10月11日
- 为
Harlinn.Common.Core
库添加更多单元测试
- 为
- 2020年10月13日
Harlinn.Common.Core
库的更多单元测试,以及Harlinn.Windows
库的两个新示例。
- 2020年10月17日
- 修复了
TimerQueue
和TimerQueueTimer
,Harlinn.Common.Core
库的更多单元测试。
- 修复了
- 2020年12月18日
IO::FileStream
的错误修复- 支持初始 HTTP 服务器开发
- 同步服务器:$(SolutionDir)Examples\Core\HTTP\Server\HttpServerEx01
- 异步服务器:$(SolutionDir)Examples\Core\HTTP\Server\HttpServerEx02
- 使用Windows线程池API简化Windows可等待内核对象的异步I/O、定时器、工作和事件:$(SolutionDir)Examples\Core\ThreadPools\HExTpEx01
- 2021年1月1日
- 改进了对异步服务器开发的支持
- 全新的套接字工作设计
- 基于概念的流实现
- 2021年2月11日
- Bug 修复
- 初始 C++ ODBC 支持
- 2021年2月25日
- 更新 LMDB
- 更新 xxHash
- 添加了使用 LMDB 对大型复杂键的超快速基于哈希的索引的初始实现
- 快速异步日志记录 - 几乎完成 :-)
- 2021年3月3日
- 新的授权相关类
SecurityId
:SID及相关操作的包装器ExplicitAccess
:EXCPLICIT_ACCESS
的包装器Trustee
:TRUSTEE
的包装器SecurityIdAndDomain
:存储LookupAccountName
的结果LocalUniqueId
:LUID
的包装器AccessMask
:便于检查分配给ACCESS_MASK
的权限AccessMaskT<>
EventWaitHandleAccessMask
:检查和操作EventWaitHandle
的权限MutexAccessMask
:检查和操作Mutex
的权限SemaphoreAccessMask
:检查和操作Semaphore
的权限WaitableTimerAccessMask
:检查和操作WaitableTimer
的权限FileAccessMask
:检查和操作文件相关权限DirectoryAccessMask
:检查和操作目录相关权限PipeAccessMask
:检查和操作管道相关权限ThreadAccessMask
:检查和操作线程相关权限ProcessAccessMask
:检查和操作进程相关权限
GenericMapping
:GENERIC_MAPPING
的包装器AccessControlEntry
:这是一组包装ACE结构的微型类AccessControlEntryBase<,>
AccessAllowedAccessControlEntry
AccessDeniedAccessControlEntry
SystemAuditAccessControlEntry
SystemAlarmAccessControlEntry
SystemResourceAttributeAccessControlEntry
SystemScopedPolicyIdAccessControlEntry
SystemMandatoryLabelAccessControlEntry
SystemProcessTrustLabelAccessControlEntry
SystemAccessFilterAccessControlEntry
AccessDeniedCallbackAccessControlEntry
SystemAuditCallbackAccessControlEntry
SystemAlarmCallbackAccessControlEntry
ObjectAccessControlEntryBase<,>
AccessAllowedObjectAccessControlEntry
AccessDeniedObjectAccessControlEntry
SystemAuditObjectAccessControlEntry
SystemAlarmObjectAccessControlEntry
AccessAllowedCallbackObjectAccessControlEntry
AccessDeniedCallbackObjectAccessControlEntry
SystemAuditCallbackObjectAccessControlEntry
SystemAlarmCallbackObjectAccessControlEntry
AccessControlList
:ACL的包装器PrivilegeSet
:PRIVILEGE_SET
的包装器SecurityDescriptor
:SECURITY_DESCRIPTOR
包装器的早期实现阶段SecurityAttributes
:SECURITY_ATTRIBUTES
包装器的非常早期实现阶段Token
:访问令牌包装器的早期实现阶段DomainObject
User
:关于本地、工作组或域用户的信息Computer
:关于本地、工作组或域计算机的信息Group
:本地、工作组或域组
Users
:User
对象的向量Groups
:Group
对象的向量
- 新的授权相关类
- 2021年3月14日 - 更多安全相关工作
Token
:一个Windows访问令牌的包装器,带有一些支持类,例如:TokenAccessMask
:Windows访问令牌的访问权限的访问掩码实现。TokenGroups
:WindowsTOKEN_GROUPS
类型的包装器/二进制兼容替换,具有C++容器样式接口。TokenPrivileges
:TOKEN_PRIVILEGES
类型的包装器/二进制兼容替换,具有C++容器样式接口。TokenStatistics
:WindowsTOKEN_STATISTICS
类型的二进制兼容替换,使用库实现的类型,例如LocalUniqueId
、TokenType
和ImpersonationLevel
。TokenGroupsAndPrivileges
:WindowsTOKEN_GROUPS_AND_PRIVILEGES
类型的包装器/二进制兼容替换。TokenAccessInformation
:WindowsTOKEN_ACCESS_INFORMATION
类型的包装器/二进制兼容替换。TokenMandatoryLabel
:WindowsTOKEN_MANDATORY_LABEL
类型的包装器。
SecurityPackage
:提供对Windows安全包信息的访问。SecurityPackages
:系统中安装的安全包信息的std::unordered_map
。CredentialsHandle
:WindowsCredHandle
类型的包装器。SecurityContext
:WindowsCtxtHandle
类型的包装器Crypto::Blob
和Crypto::BlobT
:C++风格的_CRYPTOAPI_BLOB
替换CertificateContext
:WindowsPCCERT_CONTEXT
类型的包装器,提供对X.509证书的访问。CertificateChain
:WindowsPCCERT_CHAIN_CONTEXT
类型的包装器,包含一个简单证书链数组和一个信任状态结构,指示所有连接的简单链的摘要有效性数据。ServerOcspResponseContext
:包含一个编码的OCSP响应。ServerOcspResponse
:表示与服务器证书链关联的OCSP响应的句柄。CertificateChainEngine
:表示应用程序的链引擎。CertificateTrustList
:WindowsPCCTL_CONTEXT
类型的包装器,包含CTL的编码和解码表示。它还包含一个已打开的HCRYPTMSG
句柄,用于解码的、经过密码签名的消息,该消息包含CTL_INFO
作为其内部内容。CertificateRevocationList
:包含证书吊销列表(CRL)的编码和解码表示CertificateStore
:证书、证书吊销列表(CRL)和证书信任列表(CTL)的存储。
- 2021年3月23日
- 更新至 Visual Studio 16.9.2
- 构建修复
SecurityDescriptor:
实现了安全描述符的序列化,从而实现了授权数据的持久化。