大家好!在昨天的文章 官方文档没告诉你的:通过抓包,深入揭秘MCP协议底层通信 中,我们通过Fiddler工具,像侦探一样,一步步揭开了MCP(Model Context Protocol)在无状态HTTP模式下的神秘面纱。我们搞清楚了它的两步握手、SSE(Server-Sent Events)响应机制以及精巧的两种错误处理方式。

然而,仅仅停留在理论分析层面总感觉意犹未尽。更重要的是,当我们审视官方提供的 ModelContextProtocol.AspNetCore 这个NuGet包时(当前版本0.3.0-preview.3),会发现它目前引入了相当多的依赖项:

  • Microsoft.Bcl.Memory (>= 9.0.5)
  • Microsoft.Extensions.AI.Abstractions (>= 9.7.1)
  • Microsoft.Extensions.Logging.Abstractions (>= 8.0.3)
  • System.Diagnostics.DiagnosticSource (>= 8.0.1)
  • System.IO.Pipelines (>= 8.0.0)
  • System.Net.ServerSentEvents (>= 10.0.0-preview.4.25258.110)
  • System.Text.Json (>= 8.0.6)
  • System.Threading.Channels (>= 8.0.0)
  • Microsoft.Extensions.Hosting.Abstractions (>= 8.0.1)
  • ModelContextProtocol.Core (>= 0.3.0-preview.3)

其中,最令人不安的莫过于 System.Net.ServerSentEvents,它竟然是一个 .NET 10 的预览版包!在生产环境中使用预览版包,通常是大忌。

既然我们已经通过抓包掌握了协议的全部细节,那么,何不自己动手,实现一个轻量级、零预览版依赖的MCP服务端呢?这不仅是一次绝佳的学习实践,也能让我们对协议的理解更上一层楼。

今天,我们就来完成这个挑战:不依赖官方服务端库,直接用纯粹的ASP.NET Core代码,实现一个功能完备的MCP服务端。

我们的目标:保持工具定义的简洁性

在动手之前,我们先定一个目标。我们希望定义工具(Tools)的方式能够尽可能地简洁和直观,几乎和昨天的代码保持一致:

using System.ComponentModel;

public class Tools(IHttpContextAccessor http)
{
[Description("Echoes the message back to the client.")]
public string Echo(string message) => $"hello {message}"; [Description("Returns the IP address of the client.")]
public string EchoIP() => http.HttpContext?.Connection.RemoteIpAddress?.ToString() ?? "Unknown"; [Description("Counts from 0 to n, reporting progress at each step.")]
public async Task<int> Count(int n, IProgress<ProgressNotificationValue> progress)
{
for (int i = 0; i < n; ++i)
{
progress.Report(new ProgressNotificationValue()
{
Progress = i,
Total = n,
Message = $"Step {i} of {n}",
});
await Task.Delay(100);
}
return n;
} [Description("Throws an exception for testing purposes.")]
public string TestThrow()
{
throw new Exception("This is a test exception");
}
}

注意到变化了吗?我们去掉了官方库定义的 [McpServerToolType][McpServerTool] 特性。取而代之的是一种更符合ASP.NET Core直觉的方式:任何 public 方法都自动成为一个工具,并使用标准的 System.ComponentModel.DescriptionAttribute 来提供工具描述。

理想中的使用方式

我们期望最终的使用方式能像下面这样优雅:

WebApplicationBuilder builder = WebApplication.CreateBuilder();

// 1. 注册原生服务和我们的工具类
builder.Services.AddHttpContextAccessor();
builder.Services.AddTransient<Tools>(); WebApplication app = builder.Build(); // 2. 映射 MCP 端点,自动发现并使用 Tools 类
app.MapMcpEndpoint<Tools>("/"); // 3. 启动应用
app.Run();

是的,你没看错。核心就在于 builder.Services.AddTransient<Tools>();app.MapMcpEndpoint<Tools>("/"); 这两行。前者负责将我们的工具类注册到依赖注入容器,后者则是我们即将创建的魔法扩展方法,它会自动处理所有MCP协议的细节。

第一步:定义协议的“语言” - DTOs

要实现协议,首先要定义好通信双方所使用的“语言”,也就是数据传输对象(DTOs)。根据昨天的抓包分析,我们用C#的 record 类型来精确描述这些JSON结构。

using System.Text.Json.Serialization;

// --- JSON-RPC Base Structures ---
public record JsonRpcRequest(
[property: JsonPropertyName("jsonrpc")] string JsonRpc,
[property: JsonPropertyName("method")] string Method,
[property: JsonPropertyName("params")] object? Params,
[property: JsonPropertyName("id")] int? Id
); public record JsonRpcResponse(
[property: JsonPropertyName("jsonrpc")] string JsonRpc,
[property: JsonPropertyName("result")] object? Result,
[property: JsonPropertyName("error")] object? Error,
[property: JsonPropertyName("id")] int? Id
); public record JsonRpcError(
[property: JsonPropertyName("code")] int Code,
[property: JsonPropertyName("message")] string Message
); // --- MCP Specific Payloads --- // For initialize method
public record InitializeParams(
[property: JsonPropertyName("protocolVersion")] string ProtocolVersion,
[property: JsonPropertyName("clientInfo")] ClientInfo ClientInfo
);
public record ClientInfo([property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("version")] string Version); public record InitializeResult(
[property: JsonPropertyName("protocolVersion")] string ProtocolVersion,
[property: JsonPropertyName("capabilities")] ServerCapabilities Capabilities,
[property: JsonPropertyName("serverInfo")] ClientInfo ServerInfo
);
public record ServerCapabilities([property: JsonPropertyName("tools")] object Tools); // For tools/call method
public record ToolCallParams(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("arguments")] Dictionary<string, object?> Arguments,
[property: JsonPropertyName("_meta")] ToolCallMeta? Meta
);
public record ToolCallMeta([property: JsonPropertyName("progressToken")] string ProgressToken); // For tool call results
public record ToolCallResult(
[property: JsonPropertyName("content")] List<ContentItem> Content,
[property: JsonPropertyName("isError")] bool IsError = false
);
public record ContentItem([property: JsonPropertyName("type")] string Type, [property: JsonPropertyName("text")] string Text); // For tools/list results
public record ToolListResult(
[property: JsonPropertyName("tools")] List<ToolDefinition> Tools
); public record ToolDefinition(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("description")] string Description,
[property: JsonPropertyName("inputSchema")] object InputSchema
); // For progress notifications
public record ProgressNotification(
[property: JsonPropertyName("jsonrpc")] string JsonRpc,
[property: JsonPropertyName("method")] string Method,
[property: JsonPropertyName("params")] ProgressParams Params
);
public record ProgressParams(
[property: JsonPropertyName("progressToken")] string ProgressToken,
[property: JsonPropertyName("progress")] int Progress,
[property: JsonPropertyName("total")] int Total,
[property: JsonPropertyName("message")] string Message
); // This class is for the IProgress<T> interface in our Tools methods
public class ProgressNotificationValue
{
public int Progress { get; set; }
public int Total { get; set; }
public string Message { get; set; } = string.Empty;
}

第二步:打造核心引擎 - McpEndpointExtensions

接下来,就是实现我们魔法的源泉:一个IEndpointRouteBuilder的扩展方法。我们将所有逻辑都封装在一个静态类 McpEndpointExtensions 中。

这个类将负责:

  1. 路由映射:监听指定路径的 POSTGET 请求。
  2. 请求分发:根据JSON-RPC请求中的method字段,调用不同的处理函数。
  3. 工具发现与调用:使用反射来查找和执行TTools类中的工具方法。
  4. 响应构建:手动构建符合SSE规范的响应流。
  5. 错误处理:精确复现抓包分析中发现的两种错误模型。
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Primitives;
using System.ComponentModel;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization; public static class McpEndpointExtensions
{
// JSON-RPC Error Codes from your article's findings
private const int InvalidParamsErrorCode = -32602; // Invalid params
private const int MethodNotFoundErrorCode = -32601; // Method not found private static readonly JsonSerializerOptions s_jsonOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
}; /// <summary>
/// Maps an endpoint that speaks the Model Context Protocol.
/// </summary>
public static IEndpointRouteBuilder MapMcpEndpoint<TTools>(this IEndpointRouteBuilder app, string pattern) where TTools : class
{
// 预先通过反射发现所有工具方法,并转换为snake_case以匹配MCP命名习惯
Dictionary<string, MethodInfo> methods = typeof(TTools).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
.ToDictionary(k => ToSnakeCase(k.Name), v => v); app.MapPost(pattern, async (HttpContext context, [FromServices] IServiceProvider sp) =>
{
JsonRpcRequest? request = await JsonSerializer.DeserializeAsync<JsonRpcRequest>(context.Request.Body, s_jsonOptions);
if (request == null)
{
context.Response.StatusCode = 400; // Bad Request
return;
} // 核心:处理不同的MCP方法
switch (request.Method)
{
case "initialize":
await HandleInitialize(context, request);
break;
case "notifications/initialized":
// 在无状态模式下,这个请求只是一个确认,我们返回与initialize类似的信息
await HandleInitialize(context, request);
break;
case "tools/list":
await HandleToolList<TTools>(context, request);
break;
case "tools/call":
await HandleToolCall<TTools>(context, request, sp, methods);
break;
default:
JsonRpcResponse errorResponse = new("2.0", null, new JsonRpcError(MethodNotFoundErrorCode, "Method not found"), request.Id);
await WriteSseMessageAsync(context.Response, errorResponse);
break;
}
}); // 旧版SDK会发送GET请求,我们明确返回405
app.MapGet(pattern, context =>
{
context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed;
context.Response.Headers.Allow = "POST";
return Task.CompletedTask;
}); return app;
} private static string ToSnakeCase(string name)
{
if (string.IsNullOrEmpty(name)) return name;
var sb = new StringBuilder(name.Length);
for (int i = 0; i < name.Length; i++)
{
char c = name[i];
if (char.IsUpper(c))
{
if (sb.Length > 0 && i > 0 && !char.IsUpper(name[i-1])) sb.Append('_');
sb.Append(char.ToLowerInvariant(c));
}
else
{
sb.Append(c);
}
}
return sb.ToString();
} private static async Task HandleInitialize(HttpContext context, JsonRpcRequest request)
{
// 复用或创建 Session ID
string sessionId = context.Request.Headers.TryGetValue("Mcp-Session-Id", out StringValues existingSessionId)
? existingSessionId.ToString()
: WebEncoders.Base64UrlEncode(Guid.NewGuid().ToByteArray()); context.Response.Headers["Mcp-Session-Id"] = sessionId; // 构建与抓包一致的响应
InitializeResult result = new(
"2025-06-18", // Echo the protocol version
new ServerCapabilities(new { listChanged = true }), // Mimic the capabilities
new ClientInfo("PureAspNetCoreMcpServer", "1.0.0")
);
JsonRpcResponse response = new("2.0", result, null, request.Id);
await WriteSseMessageAsync(context.Response, response);
} private static async Task HandleToolList<TTools>(HttpContext context, JsonRpcRequest request) where TTools : class
{
EchoSessionId(context); List<ToolDefinition> toolDefs = [];
MethodInfo[] toolMethods = typeof(TTools).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); foreach (MethodInfo method in toolMethods)
{
string description = method.GetCustomAttribute<DescriptionAttribute>()?.Description ?? "No description."; // 简化的动态Schema生成
Dictionary<string, object> properties = [];
List<string> required = [];
foreach (ParameterInfo param in method.GetParameters())
{
if (param.ParameterType == typeof(IProgress<ProgressNotificationValue>)) continue; // 忽略进度报告参数
properties[param.Name!] = new { type = GetJsonType(param.ParameterType) };
if (!param.IsOptional)
{
required.Add(param.Name!);
}
}
var schema = new { type = "object", properties, required };
toolDefs.Add(new ToolDefinition(ToSnakeCase(method.Name), description, schema));
} ToolListResult result = new(toolDefs);
JsonRpcResponse response = new("2.0", result, null, request.Id);
await WriteSseMessageAsync(context.Response, response);
} private static async Task HandleToolCall<TTools>(HttpContext context, JsonRpcRequest request, IServiceProvider sp, Dictionary<string, MethodInfo> methods) where TTools : class
{
EchoSessionId(context); ToolCallParams? toolCallParams = JsonSerializer.Deserialize<ToolCallParams>(JsonSerializer.Serialize(request.Params, s_jsonOptions), s_jsonOptions);
if (toolCallParams == null) return; string toolName = toolCallParams.Name;
methods.TryGetValue(toolName, out MethodInfo? method); // 场景1: 调用不存在的工具 -> 返回标准JSON-RPC错误
if (method == null)
{
JsonRpcError error = new(InvalidParamsErrorCode, $"Unknown tool: '{toolName}'");
JsonRpcResponse response = new("2.0", null, error, request.Id);
await WriteSseMessageAsync(context.Response, response);
return;
} // 使用DI容器创建工具类的实例
using IServiceScope scope = sp.CreateScope();
TTools toolInstance = scope.ServiceProvider.GetRequiredService<TTools>(); object? resultValue;
bool isError = false; try
{
// 通过反射准备方法参数
ParameterInfo[] methodParams = method.GetParameters();
object?[] args = new object?[methodParams.Length];
for (int i = 0; i < methodParams.Length; i++)
{
ParameterInfo p = methodParams[i];
if (p.ParameterType == typeof(IProgress<ProgressNotificationValue>))
{
// 创建一个IProgress<T>的实现,它会将进度作为SSE消息发回客户端
args[i] = new ProgressReporter(context.Response, toolCallParams.Meta!.ProgressToken);
}
else if (toolCallParams.Arguments.TryGetValue(p.Name!, out object? argValue) && argValue is JsonElement element)
{
args[i] = element.Deserialize(p.ParameterType, s_jsonOptions);
}
else if (p.IsOptional)
{
args[i] = p.DefaultValue;
}
else
{
// 场景2a: 缺少必要参数 -> 抛出异常,进入catch块
throw new TargetParameterCountException($"Tool '{toolName}' requires parameter '{p.Name}' but it was not provided.");
}
} object? invokeResult = method.Invoke(toolInstance, args); // 处理异步方法
if (invokeResult is Task task)
{
await task;
resultValue = task.GetType().IsGenericType ? task.GetType().GetProperty("Result")?.GetValue(task) : null;
}
else
{
resultValue = invokeResult;
}
}
// 场景2b: 工具执行时内部抛出异常 -> isError: true
catch (Exception ex)
{
isError = true;
// 将异常信息包装在result中,而不是顶层error
resultValue = $"An error occurred invoking '{toolName}'. Details: {ex.InnerException?.Message ?? ex.Message}";
} List<ContentItem> content = [new("text", resultValue?.ToString() ?? string.Empty)];
ToolCallResult result = new(content, isError);
JsonRpcResponse finalResponse = new("2.0", result, null, request.Id);
await WriteSseMessageAsync(context.Response, finalResponse);
} // 手动实现SSE消息写入,告别预览版包
private static async Task WriteSseMessageAsync(HttpResponse response, object data)
{
if (!response.Headers.ContainsKey("Content-Type"))
{
response.ContentType = "text/event-stream";
response.Headers.CacheControl = "no-cache,no-store";
response.Headers.ContentEncoding = "identity";
response.Headers.KeepAlive = "true";
} string json = JsonSerializer.Serialize(data, s_jsonOptions);
string message = $"event: message\ndata: {json}\n\n";
await response.WriteAsync(message);
await response.Body.FlushAsync();
} private static void EchoSessionId(HttpContext context)
{
if (context.Request.Headers.TryGetValue("Mcp-Session-Id", out StringValues sessionId))
{
context.Response.Headers["Mcp-Session-Id"] = sessionId;
}
} private static string GetJsonType(Type type) => Type.GetTypeCode(type) switch
{
TypeCode.String => "string",
TypeCode.Int32 or TypeCode.Int64 or TypeCode.Int16 or TypeCode.UInt32 => "integer",
TypeCode.Double or TypeCode.Single or TypeCode.Decimal => "number",
TypeCode.Boolean => "boolean",
_ => "object"
}; // 专门用于处理进度报告的辅助类
private class ProgressReporter(HttpResponse response, string token) : IProgress<ProgressNotificationValue>
{
public void Report(ProgressNotificationValue value)
{
ProgressParams progressParams = new(token, value.Progress, value.Total, value.Message);
ProgressNotification notification = new("2.0", "notifications/progress", progressParams);
// 警告: 在同步方法中调用异步代码,在真实生产环境中需要更优雅的处理
WriteSseMessageAsync(response, notification).GetAwaiter().GetResult();
}
}
}

完整代码已备好!

为了方便大家动手实践,我已经将上述所有可直接运行的示例代码上传到了 GitHub Gist。您可以通过以下链接访问:

该Gist中包含了两个文件:

  • mcp-server-raw.linq: 我们刚刚从零开始构建的轻量级MCP服务端。
  • mcp-client.linq: 用于测试的客户端。

这两个文件都可以直接在最新版的 LINQPad 中打开并运行,让您能够立即体验和调试,如果您访问 Github Gist 有困难,则可以访问这个备用地址:https://github.com/sdcb/blog-data/tree/master/2025

第三步:见证奇迹的时刻

现在,我们所有的准备工作都已就绪。我们可以用和昨天一模一样的客户端代码来测试我们的新服务端了:

// 客户端代码完全不变!
var clientTransport = new SseClientTransport(new SseClientTransportOptions()
{
Name = "MyServer",
Endpoint = new Uri("http://localhost:5000"), // 注意端口可能不同
}); var client = await McpClientFactory.CreateAsync(clientTransport); // 1. 列出工具
(await client.ListToolsAsync()).Select(x => new { x.Name, Desc = JsonObject.Parse(x.JsonSchema.ToString()) }).Dump(); // 2. 调用简单工具
(await client.CallToolAsync(
"echo",
new Dictionary<string, object?>() { ["message"] = ".NET is awesome!" },
cancellationToken: CancellationToken.None)).Dump(); // 3. 调用带进度的工具
(await client.CallToolAsync(
"count",
new Dictionary<string, object?>() { ["n"] = 5 },
new Reporter(),
cancellationToken: CancellationToken.None)).Dump(); // 4. 调用会抛出异常的工具
(await client.CallToolAsync("test_throw", cancellationToken: CancellationToken.None)).Dump(); // 5. 调用不存在的工具
(await client.CallToolAsync("not-existing-tool", cancellationToken: CancellationToken.None)).Dump(); // ... Reporter class as before ...

启动我们的新服务端,再运行客户端代码。打开抓包工具,你会发现,所有HTTP请求和SSE响应的格式、内容和行为,都与昨天分析的官方库实现完全一致!我们成功了!

对错误处理的进一步思考

值得一提的是,昨天的文章没有深入探讨参数错误的情况。比如 count 工具需要一个名为 nint 类型参数,如果客户端错误地传递了一个 n2 参数,会发生什么?

在我今天实现的 HandleToolCall 方法中,参数匹配逻辑会因为找不到名为 n 的键而抛出 TargetParameterCountException。这个异常会被 try-catch 块捕获,然后和 test_throw 的情况一样,返回一个调用“成功”(HTTP 200)、但在 result 载荷中包含 "isError": true 和详细错误信息的响应。这恰好证明了MCP这种错误处理设计的健壮性:它能统一处理业务逻辑层面(工具内部异常)和参数绑定层面(调用约定不匹配)的多种失败情况。

总结

通过本次实践,我们不仅重温了MCP协议的通信原理,更重要的是,我们亲手实现了一个轻量级、无预览版依赖的MCP服务端。这次旅程的核心收获是:

  1. 协议是根基:一旦深刻理解了协议本身,即使没有官方SDK,我们也能在任何支持HTTP的环境中实现它。
  2. 化繁为简:我们用一个扩展方法和一些辅助类,就替代了官方库及其繁杂的依赖,代码清晰且易于掌控。
  3. 反射与元编程的威力:通过巧妙运用反射,我们实现了工具的自动发现和动态调用,大大提高了代码的灵活性和可扩展性。
  4. 知其然,知其所以然:现在,我们不仅知道MCP如何工作,更通过自己动手理解了它为何如此设计,比如两步握手、SSE流式响应以及分层的错误处理机制。

希望本文能帮助你彻底搞懂并掌握MCP协议的实现细节。现在,你拥有了完全控制MCP通信的能力,无论是进行二次开发、跨语言实现,还是仅仅为了满足那份技术探索的好奇心。


感谢您的阅读,如果您有任何问题或想法,欢迎在评论区留言讨论。

也欢迎加入我们的 .NET骚操作 QQ群一起探讨:495782587

抛开官方库,手撸一个轻量级 MCP 服务端的更多相关文章

  1. C#基于Mongo的官方驱动手撸一个Super简易版MongoDB-ORM框架

    C#基于Mongo的官方驱动手撸一个简易版MongoDB-ORM框架 如题,在GitHub上找了一圈想找一个MongoDB的的ORM框架,未偿所愿,就去翻了翻官网(https://docs.mongo ...

  2. Golang:手撸一个支持六种级别的日志库

    Golang标准日志库提供的日志输出方法有Print.Fatal.Panic等,没有常见的Debug.Info.Error等日志级别,用起来不太顺手.这篇文章就来手撸一个自己的日志库,可以记录不同级别 ...

  3. 手撸一个SpringBoot-Starter

    1. 简介 通过了解SpringBoot的原理后,我们可以手撸一个spring-boot-starter来加深理解. 1.1 什么是starter spring官网解释 starters是一组方便的依 ...

  4. 使用Java Socket手撸一个http服务器

    原文连接:使用Java Socket手撸一个http服务器 作为一个java后端,提供http服务可以说是基本技能之一了,但是你真的了解http协议么?你知道知道如何手撸一个http服务器么?tomc ...

  5. 【手撸一个ORM】MyOrm的使用说明

    [手撸一个ORM]第一步.约定和实体描述 [手撸一个ORM]第二步.封装实体描述和实体属性描述 [手撸一个ORM]第三步.SQL语句构造器和SqlParameter封装 [手撸一个ORM]第四步.Ex ...

  6. 第二篇-用Flutter手撸一个抖音国内版,看看有多炫

    前言 继上一篇使用Flutter开发的抖音国际版 后再次撸一个国内版抖音,大部分功能已完成,主要是Flutter开发APP速度很爽,  先看下图 项目主要结构介绍 这次主要的改动在api.dart 及 ...

  7. 通过 Netty、ZooKeeper 手撸一个 RPC 服务

    说明 项目链接 微服务框架都包括什么? 如何实现 RPC 远程调用? 开源 RPC 框架 限定语言 跨语言 RPC 框架 本地 Docker 搭建 ZooKeeper 下载镜像 启动容器 查看容器日志 ...

  8. 手撸一个springsecurity,了解一下security原理

    手撸一个springsecurity,了解一下security原理 转载自:www.javaman.cn 手撸一个springsecurity,了解一下security原理 今天手撸一个简易版本的sp ...

  9. 五分钟,手撸一个Spring容器!

    大家好,我是老三,Spring是我们最常用的开源框架,经过多年发展,Spring已经发展成枝繁叶茂的大树,让我们难以窥其全貌. 这节,我们回归Spring的本质,五分钟手撸一个Spring容器,揭开S ...

  10. 从零讲解搭建一个NIO消息服务端

    本文首发于本博客,如需转载,请申明出处. 假设 假设你已经了解并实现过了一些OIO消息服务端,并对异步消息服务端更有兴趣,那么本文或许能带你更好的入门,并了解JDK部分源码的关系流程,正如题目所说,笔 ...

随机推荐

  1. 通过 Nuke 为 Dotnet Core 应用构建自动化流程

    为什么使用Nuke 最开始了解Nuke,是浏览github时,刷到了这个项目,看简介可以通过C# 来定义构建任务和流程,这一点很新颖,对我来讲,c# 显然更容易理解和维护. 再看给出的示例,确实比较清 ...

  2. Flutter视频压缩技术:如何在应用中优化视频文件的质量和大小?

    @charset "UTF-8"; .markdown-body { line-height: 1.75; font-weight: 400; font-size: 15px; o ...

  3. Mysql索引为什么要采用B+Tree而非B-Tree

       B+树非叶子节点不存储数据只存储索引,B树非叶子节点存储数据.    B+树查询效率更高.B+树使用双向链表串连所有叶子节点,区间查询效率更高(因为所有数据都在B+树的叶子节点,扫描数据库 只需 ...

  4. Openmv简明使用教程

    Openmv简明使用教程 写在前面 本教程主要目的是指明学习资源在哪,可以怎么学,不教具体怎么使用,因为没有什么教程比官网上的教程更详细了,希望大家看完这篇文章后,能对如何学习使用Openmv有一个清 ...

  5. 为博客添加Live图

    为博客添加Live图 Apple提供了很生动的Live实况图,在实际展示的过程中非常生动形象,在撰写博客的过程中,我自己也尝试将博客中嵌入实况图片 其实Apple提供的iCloud网页版为我们提供了很 ...

  6. Client-go的四种客户端的简单使用

    Client-go的四种客户端使用 我们知道kubectl是通过命令行交互的方式与Kubernetes API Server进行交互的,Kubernetes还提供了通过编程的方式与Kubernetes ...

  7. ChatMoney:你的短视频脚本制作利器

    本文由 ChatMoney团队出品 在当今这个快节奏的数字时代,短视频以其短小精悍.内容丰富的特点迅速崛起,成为大众娱乐和信息传播的重要载体.然而,对于许多创作者而言,如何构思一个引人入胜.富有创意的 ...

  8. 肝了一个月整理了这份Java学习路线导图

    很多人都在问应该怎么样学习java的知识点,java有哪些知识点?最近准备面试了,java知识点太多了又不知道如何开始复习?java的知识点太多太多,学完了又忘了.所以我们可以为每个知识点都整理成一份 ...

  9. 图解JavaScript原型:原型链及其分析 02 | JavaScript图解

    ​​ 任何函数既可以看成一个实例对象又可以看成一个函数 作为一个实例对象其隐式原型对象指向其构造函数的显式原型对象 作为一个函数其显式原型对象指向一个空对象 任何一个函数其隐式原型对象指向其构造函数的 ...

  10. solana杂谈(1)

    solana杂谈(1) 本文适用于"只需大致了解 Solana"的读者,部分说法可能不够准确或不够深入.如需详细了解,建议阅读 Solana 的官方文档:https://solan ...