使用显示器时,很多人都会遇到字体或应用界面太大太小的问题,比如正在使用27寸2K显示器,更换了27寸4K显示器后应用界面都会变小,需要使用系统提供的缩放功能来放大。

Window和MacOS使用的不同的方法来解决高分辨率显示器带来的UI不适配的问题。

Window

Window为了解决不同尺寸不同分辨率的显示器在显示效果上的差异,引进了缩放功能,用户可以在不同的显示器上尝试不同的缩放来达到合适的UI呈现。比如一个800px x 600px的应用程序,在相同尺寸的显示器下,在1920x1080分辨率的显示器上占据的面积是,而在2560x1440分辨率的显示器上占据的面积是。因为显示器尺寸相同,这导致应用程序在高分辨率下变小了。为了能有统一的UI呈现,需要将2560x1440分辨率的显示器进行放大

Window是根据PPI(Pixels Per Inch)来解决缩放问题。PPI即每英寸的像素量,PPI约高显示效果约细腻。

UI物理大小的基准

由于历史原因,Window以96DPI的100%缩放为基准设计UI。DPI是渲染时像素数,即渲染时每英寸的像素量,反过来其是就是每个像素是几物理英寸,这与下面要介绍的MacOS的每pt的物理英寸大小很相似,但两者的设计原理不同。DPI是历史遗留,96DPI就是当时设置的标准。

DPI 缩放大小
96 100%
120 125%
144 150%
192 200%

软件开发这需要对系统提供缩放比例进行适配,对于未适配的软件会插值取样后强行缩放,会产生模糊。

有了DPI(渲染时每英寸的像素量)为基准,为了让UI大小更加合适,需要让显示器的PPI约接近DPI则UI大小约合适。即满足

24寸(1920x1080)

通过计算可得24寸(1920x1080)的PPI=93,所以,非常接近1,则只需要100%缩放即可。

27寸(3840x2160)

通过计算可得27寸(3840x2160)的PPI=163:

  • 按照100%缩放,,明显小于1,会导致UI变小
  • 按照175%缩放,,非常接近1,需要175%的缩放
  • 按照200%缩放,,大于1,会导致UI变大

Window还会提供GDI缩放,过程与MacOS的HiDPI类似,先整数倍缩放,然后再压缩到显示器的分辨率。

MacOS

MacOS为了解决不同尺寸不同分辨率的显示器在显示效果上的差异,提出了逻辑分辨率。UI的物理大小是由逻辑分辨率屏幕尺寸共同决定的,与屏幕分辨率无关。逻辑分辨率是提供给开发者使用的,开发者并不需要关心UI最终渲染在不同显示器上有多大,开发者只需要根据pt设计UI界面并提供矢量图或多倍图@1x/@2x/@3x资源,最终显示器的呈现由MacOS自动缩放。至于每pt的实际物理大小是以14寸和16寸Macbook Pro为基准,针对其他不同的显示器,MacOS会通过缩放尽量让UI表现与14寸和16寸Macbook Pro的基准相同。

UI物理大小的基准

以14寸和16寸Macbook Pro为基准 数据来源

14寸:14.2 英寸 (对角线) ,初始分辨率 3024 x 1964 (254 ppi)

16寸:16.2 英寸 (对角线) ,初始分辨率 3456 x 2234 (254 ppi)
线
14寸对角线分辨率:

16寸对角线分辨率:

因为ppi高于160会被MacOS认定为高刷屏,会启动hidpi,默认的放大倍数scale=2,并且满足完美的放大整数倍,所以他们的逻辑分辨率是初始分辨率的一半。(后面会讲解HiDPI,目前只需要知道逻辑分辨率*2=屏幕实际分辨率可以达到最好的效果)

14寸逻辑分辨率(pt): 1512 × 982(3024/2 × 1964/2)

16寸逻辑分辨率(pt): 1728 × 1117(3456/2 × 2234/2)

14寸对角线逻辑分辨率:

16寸对角线逻辑分辨率:

因为软件开发者在设计软件大小的时候使用的是pt,pt也就是逻辑分辨率,比如一个(800pt x 600pt)的软件会被显示在 (1512pt × 982pt) 或 (1728pt × 1117pt) 逻辑分辨率的屏幕内。

所以1pt在14寸Macbook pro中的物理大小就是屏幕的长度 / 屏幕的逻辑分辨率 (严格来说应该分别计算宽和高,这里就用对角线替代了)

同理,1pt在16寸Macbook pro中的物理大小为

到此,得出对于Macbook Pro的UI开发基准为 每1pt对应的物理大小是0.008英寸左右,这样的大小会让UI大小适中,符合开发者想让用户看到的UI大小(因为开发者是以Macbook Pro为开发参照的)

Apple还推出了Stuido Display 和 Pro Display XDR:

Studio Display Pro Display XDR
尺寸 对角线(inch) 27 inch 32 inch
实际分辨率 5120 x 2880 (5K) 6016 x 3384 (6k)
ppi 218 218
逻辑分辨率 2560 x 1440 (2K) 3008 × 1692

按照上面的提示,逻辑分辨率*2=屏幕实际分辨率可以达到最好的效果(后面会解释HiDPI整数倍缩放效果最好)可以推出对应的逻辑分辨率。

按照Macbook的计算公式,可以得出这两款显示器中1pt对应的物理大小:

可以看到每1pt对应的物理大小在[0.008inch/pt-0.012inch/pt]范围内,最合适的大小应该是接近0.008inch/pt


到此为止,简单了解了MacOS中UI物理大小(即在显示器上显示的大小)是与逻辑分辨率和显示器屏幕尺寸相关的。屏幕尺寸越大,UI物理大小越大;逻辑分辨率越高,UI物理大小越小。

MacOS这样的设计巧妙的避开了显示器的实际分辨率以及PPI,通过MacOS自动缩放来适应不同分辨率的显示器。

HiDPI

macOS 的 HiDPI(High Dots Per Inch)是一种高分辨率显示技术,旨在在保持界面元素物理尺寸合理的同时,提升屏幕内容的清晰度和细节表现。当DPI过高时,会导致程序界面变小,为了给用户提供一个合适UI物理尺寸,需要将UI进行缩放。HiDPI提供了一套自动处理的流程,用户和开发者只需要关注逻辑分辨率即可,实际的缩放由MacOS控制。

  1. 首先操作系统以逻辑分辨率呈现用户界面
  2. 将逻辑分辨率进行放大(整数倍如2x,3x)
  3. 将放大后的再压缩到显示器的实际分辨率(如果放大后的分辨率与显示器分辨率相同则直接呈现,这也是最好的效果)

只有显示器的DPI>150或者很高的时候才会主动开启HiDPI,因为HiDPI本身就是针对高分辨率显示器准备的。

整数倍缩放

以27寸4K(3840 x 2160)和27寸5K(5120 x 2880)为例:

为了获得最好的缩放效果,以整数倍进行缩放是最由的选择,这里选择缩放比例scale=2,即27寸4K(3840 x 2160)选择1920x1080的逻辑分辨率,27寸5K(5120 x 2880)选择2560x1440的逻辑分辨率。

  1. MacOS以逻辑分辨率呈现程序界面**(强调:只是UI布局按逻辑分辨率计算,并不是以逻辑分辨率进行渲染)**

    在27寸4K的1920x1080的逻辑分辨率下显示一个800x600pt的程序

    在27寸5K的2560x1440的逻辑分辨率下显示一个800x600pt的程序

    在屏幕尺寸一样(同27寸)的情况下,明显2560x1440下的800x600pt的程序会显得更小。

  2. 将逻辑分辨率进行放大到渲染分辨率,宽高放大两倍相当于1个像素点变为4(2x2)个像素点**(此过程GPU才进行渲染,即每pt需要用2x2个像素来渲染)**

    27寸4K的1920x1080的逻辑分辨率放大为3840 x 2160,800x600pt的程序变为1600x1200px(强调:UI的物理大小是没有变化的)

    27寸5K的2560x1440的逻辑分辨率放大为5120 x 2880,800x600pt的程序变为1600x1200px

    这个放大可以理解为 逻辑分辨率 与 显示器屏幕尺寸 决定了 UI的物理大小,但这个逻辑分辨率是低于显示器的实际分辨率的,如果直接用逻辑分辨率渲染对显示器来说是浪费,所以首先放大逻辑分辨率,将放大后的图像映射到显示器的实际分辨率上。

  3. 将放大后渲染分辨率的再压缩到显示器的实际分辨率**(如果两者相同直接呈现不需要压缩)**

    以上两个例子27寸4K的1920x1080的逻辑分辨率放大为3840 x 2160和27寸5K的2560x1440的逻辑分辨率放大为5120 x 2880都是放大到了显示器的实际分辨率,直接交给显示器呈现即可,不需要压缩,所以这种模式下是最好的显示效果。

最后计算一下 27寸4K(3840 x 2160)以1920x1080的逻辑分辨率27寸5K(5120 x 2880)以2560x1440的逻辑分辨率 的每pt对应的物理大小:

对于在完美的缩放情况下,27寸4K(3840 x 2160)以1920x1080的逻辑分辨率 会显示的略微偏大,而27寸5K(5120 x 2880)以2560x1440的逻辑分辨率 的显示比较合适,接近0.008inch/pt。

图示总结:

image-20251208154321962

  • 屏幕尺寸 和 逻辑分辨率 共同决定UI大小
  • 虽然27寸2K(原生渲染)与27寸4K(逻辑分辨率2K)中UI大小是一致的,但渲染分辨率相差4倍,27寸2K的1个像素在27寸4K中用4个像素表示
  • 对于程序员,只需要设计程序大小为xx pt,实际显示大小由MacOS决定。例如:应用程序的大小为 800pt x 600pt
  • MacOS会提供一系列逻辑分辨率供用户选择,比如 1920x1080,2560x1440等,实际上就是对应的1920ptx1080pt,2560ptx1440pt,比如一个800ptx600pt的应用,会占据1920x1080屏幕宽度的41%,会占据2560x1440屏幕宽度的31% (800/1920 = 0.41, 800/2560 = 0.31)
  • 但MacOS的最佳的逻辑分辨率只与显示器屏幕的大小有关,当显示器屏幕大小一定时,最佳的逻辑分辨率就已经确定了(根据14寸或16寸的Macbook为基准)。所以将最佳逻辑分辨率与显示器屏幕结合,就能计算出UI的物理大小,每pt占据的物理大小在任何分辨率任何尺寸的显示器上都是一样的(或者相似),都接近0.008inch/pt

非整数倍缩放

下面给一个非整数倍缩放,以27寸4K(3840 x 2160)以2560x1440的逻辑分辨率为例,首先以整数倍2倍放大,2560 x 1440放大为5120 x 2880,但显示器的实际分辨率为3840 x 2160,不足以显示5120 x 2880,所以要把5120 x 2880压缩到3840 x 2160再显示(这个压缩过程会导致画面模糊并增加GPU负担,这也是推荐整数倍缩放的原因)。但27寸4K以2560 x 1440的逻辑分辨率每pt对应的物理大小就是0.009192inch,UI的物理大小会更加合适。


根据上面的计算,给出针对MacOS外接显示器的配置推荐:

24寸 27寸 32寸
1920x1080 (不开启HiDPI) 2560x1440 (不开启HiDPI) 3840x2160 (不开启HiDPI)
3840x2160 (逻辑分辨率: 1920x1080) 5120x2880 (逻辑分辨率: 2560x1440) 6016x3384(逻辑分辨率: 3008x1692)
1080p 1920x1080
2K 2560x1440
4K 3840x2160
5K 5120x2880
6K 6016x3384

计算24寸,27寸,32寸在市面上常见的比较合适的逻辑分辨率,最佳的len(pt)约为0.008(inch/pt)

注:上面的缩放倍数都是以2倍进行的缩放,但按照HiDPI的设计,也可以进行3x,4x等整数倍的缩放,比如24寸的逻辑分辨率是1920x1080,但24寸显示器的分辨率是5760x3240,此时MacOS会选择3倍缩放,但目前来说24寸显示器不会达到这么高的分辨率,一般3倍会用在手机上。

总结

HiDPI的设计目标是由 逻辑分辨率 + 屏幕尺寸 决定UI的实际物理大小, 实际分辨率 决定UI的清晰度细腻程度。Window系统的是将UI实际物理大小和细腻程度都交给了DPI,以96DPI的100%缩放为基准设计的。

对于MacOS选择显示器,需要先找出对应屏幕大小的比较适合的逻辑分辨率,然后显示器的实际分辨率最好为该逻辑分辨率的整数倍(一般是2倍),不是整数倍就会牵扯到压缩问题,会出现图像模糊并增加GPU压力。

困惑

看到这里相信不少人会出现这样一个困惑:

  • 对于27寸4K的显示器,Window系统可以直接选择1080p输出,而MacOS以1080p的逻辑分辨率输出到4K上。两者都经历了放大过程,Window通过GPU输出1080p的画面在显示器上放大两倍呈现,MacOS也是先用1080p的逻辑分辨率布局然后在通过HiDPI放大两倍呈现。两者效果为什么不同?

MacOS 的实际工作流程:

  1. 逻辑分辨率(Logical Resolution):1920×1080
    • 这是应用程序“看到”的坐标空间,UI 布局按此计算。
  2. Backing Scale Factor(缩放因子):2.0
    • 系统告诉 GPU 和 App:每个逻辑像素要用 2×2 = 4 个物理像素来绘制。
  3. 实际渲染分辨率(Backing Store):3840×2160
    • 所有 UI 元素(文字、图标、窗口边框)都以 4K 原生精度渲染
  4. 最终输出:直接以 3840×2160 输出到显示器,无任何拉伸或插值

结果:UI 大小和 1080p 一样,但清晰度是 4K 级别

Windows 实际工作流程:

  1. 系统分辨率设为 1920×1080
    → 整个桌面合成器、所有应用都按 1080p 渲染。
  2. 显卡/显示器将 1080p 信号拉伸到 4K 物理像素
    → 使用插值算法(如双线性)填充缺失像素。

综上可以得出:MacOS的逻辑分辨率只是用来UI布局使用的,真正UI渲染时仍然采用放大后的渲染分辨率来渲染。而Window中UI布局和渲染都是采用的1080p,最后强行拉伸到了4K。

在 Windows 上,永远优先使用显示器的原生分辨率 + DPI 缩放来达到合适UI大小。

4K + 200% 是把 1080p 的画面放大两倍是不完全正确:4K+200% 的 UI 是在高分辨率下按“逻辑像素”绘制(每逻辑像素映射 2×2 物理像素),不是简单的把 1080p 的位图插值放大;如果软件支持 HiDPI,绘制是高分辨率的,细节是原生的。

Windows 的 “4K + 200% 缩放” 和 macOS 的 “1920×1080 逻辑分辨率(HiDPI)的对比

项目 Windows:4K + 200% 缩放 macOS:1920×1080 HiDPI
物理分辨率 3840×2160(原生) 3840×2160(原生)
逻辑分辨率(App 看到的) 1920×1080(通过 DPI 缩放抽象) 1920×1080(通过 backing scale=2 抽象)
缩放因子(Scale Factor) 2.0(200%) 2.0
实际渲染缓冲区大小 通常为 3840×2160(对 DPI-aware 应用) 总是 3840×2160(或等效高 DPI buffer)
最终输出 原生 4K,无拉伸 原生 4K,无拉伸

共同点:两者都以 2× 物理像素渲染每个逻辑 UI 单元,因此理论上清晰度一致(前提是应用都已经适配高分辨)。

单点登录(Single Sign-On,简称 SSO)是一种在多个系统间共享登录状态的机制,使用户只需在一个地方登录一次,就能访问多个相互信任的系统,无需重复输入用户名和密码。

流程

服务器:

  • app1.com (应用1服务器)
  • app2.com (应用2服务器)
  • sso.com (授权服务器)

客户端:

  • client (浏览器)
  1. 浏览器访问应用1 “https://app1.com“ [cookie:none]
  2. 因为浏览器没有app1.com的cookie,应用1需要重定向到授权服务器(sso.com) “https://sso.com/oauth2/authorize?response_type=code&client_id=xxx&redirect_uri=https://app1.com/callback“ [cookie:none]
  3. 因为浏览器没有sso.com的cookie,授权服务器会返回登陆窗口 “https://sso.com/login“ [cookie:sso.com](GET请求 该地址由授权服务器自定义)
  4. 浏览器登陆,将用户名和密码提交给授权服务器。提交POST请求到 “https://sso.com/login“ (POST请求 该地址由授权服务器自定义)
  5. 登陆成功后会将[cookie:sso.com]保存在授权服务器,生成code并重定向到redirect_uri,即应用1服务器(app1.com)提供的监听端口。 “https://app1.com/callback?code=abcd1234
  6. 应用1服务器(app1.com)接收到授权服务器(sso.com)的请求后,携带code向授权服务器请求token(如access_token, refresh_token, id_token) “https://sso.com/oauth2/token?code=abcd1234
  7. 授权服务器(sso.com)验证code后,颁发对应的token
  8. 应用1服务器(app1.com)收到请求token的回复后,验证收到token的合法性。
    • JWT Token: 向授权服务器(sso.com)获取公钥,然后在本地验证 “https://sso.com/oauth2/jwks
    • Opaque Token: 将token发给授权服务器(sso.com)并由授权服务器(sso.com)验证token是否合法
  9. 应用1服务器(app1.com)从缓存中拿出浏览器最初的请求(步骤1)并为该请求生成session,浏览器重定向到该请求并带有cookie [cookie:app1.com],之后的浏览器与应用1服务器(app1.com)的交互都携带cookie [cookie:app1.com]进行身份认证
  10. 浏览器访问应用2 “https://app2.com“ [cookie:none]
  11. 因为浏览器没有app2.com的cookie,应用2服务器需要重定向到授权服务器(sso.com),因为在步骤3 浏览器已经缓存了授权服务器(sso.com)的cookie,故在浏览器访问授权服务器(sso.com)时,会携带之前的cookie “https://sso.com/oauth2/authorize?response_type=code&client_id=xxx&redirect_uri=https://app2.com/callback“ [cookie:sso.com]
  12. 授权服务器(sso.com)验证[cookie:sso.com],可得到之前的登陆状态,直接返回code给应用2服务器(app2.com) “https://app2.com/callback?code=abcd1234
  13. 应用2服务器(app2.com)向授权服务器(sso.com)发送请求换取token “https://sso.com/oauth2/token
  14. 授权服务器(sso.com)验证code并颁发对应的token
  15. 应用2服务器(app2.com)收到token后,验证token的合法性
  16. 应用2服务器(app2.com)从缓存中拿出浏览器最初的请求(步骤10)并为该请求生成session,浏览器重定向到该请求并带有cookie [cookie:app2.com],之后的浏览器与应用2服务器(app2.com)的交互都携带cookie [cookie:app2.com]进行身份认证

Spring Security 自定义的登陆和授权页面(Thymleaf)

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
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>授权确认</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="bg-gray-50 min-h-screen flex items-center justify-center">

<div class="bg-white shadow-lg rounded-2xl p-8 w-full max-w-md">
<!-- Header -->
<div class="text-center mb-6">
<h2 class="text-2xl font-semibold text-gray-800">授权请求</h2>
<p class="text-gray-600 mt-2">
应用
<span class="font-medium text-indigo-600" th:text="${clientId}"></span>
请求访问你的账户
</p>
</div>

<!-- User Info -->
<div class="bg-gray-100 rounded-lg p-3 mb-6 text-sm text-gray-700">
当前登录用户:
<span class="font-semibold text-gray-900" th:text="${principalName}"></span>
</div>

<!-- Consent Form -->
<form method="post" th:action="@{/oauth2/authorize}" class="space-y-6">
<input type="hidden" name="client_id" th:value="${clientId}"/>
<input type="hidden" name="state" th:value="${state}"/>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>

<!-- Scope List -->
<div>
<p class="font-medium text-gray-800 mb-2">该应用请求以下权限:</p>
<div class="space-y-2">
<div th:each="scope : ${scopes}" class="flex items-center space-x-2">
<input type="checkbox" name="scope"
th:value="${scope}" checked
class="w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500">
<label th:text="${scope}" class="text-gray-700"></label>
</div>
</div>
</div>

<!-- Buttons -->
<div class="flex justify-around mt-6">
<button type="submit" name="consent_action" value="cancel"
class="px-4 py-2 text-gray-700 bg-gray-200 hover:bg-gray-300 rounded-lg font-medium">
拒绝
</button>
<button type="submit" name="consent_action" value="approve"
class="px-4 py-2 bg-indigo-600 text-white hover:bg-indigo-700 rounded-lg font-medium">
同意授权
</button>
</div>
</form>

<!-- Footer -->
<p class="text-center text-xs text-gray-400 mt-8">
安全认证由 Amber IDP 提供
</p>
</div>

</body>
</html>

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
<!-- src/main/resources/templates/login.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 flex items-center justify-center h-screen">
<div class="bg-white shadow-2xl rounded-2xl p-10 w-96">
<h2 class="text-2xl font-bold text-center text-gray-800 mb-6">欢迎登录</h2>

<form th:action="@{/login}" method="post" class="space-y-5">
<div>
<label for="username" class="block text-gray-600 mb-1">用户名</label>
<input type="text" id="username" name="username"
class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-indigo-400 focus:outline-none"
placeholder="请输入用户名" required>
</div>

<div>
<label for="password" class="block text-gray-600 mb-1">密码</label>
<input type="password" id="password" name="password"
class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-indigo-400 focus:outline-none"
placeholder="请输入密码" required>
</div>

<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>

<button type="submit"
class="w-full bg-indigo-600 text-white font-semibold py-2 rounded-lg hover:bg-indigo-700 transition">
登录
</button>

<div th:if="${param.error}" class="text-red-600 text-sm text-center mt-2">
用户名或密码错误,请重试。
</div>

<div th:if="${param.logout}" class="text-green-600 text-sm text-center mt-2">
您已成功登出。
</div>
</form>
</div>
</body>
</html>

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
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioIoHandler;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.concurrent.GlobalEventExecutor;
import lombok.extern.slf4j.Slf4j;

/**
*
* @author yingzheng
* @date 2025-09-18
*/
@Slf4j
public class WebSocketServer {

public void start() {
EventLoopGroup boss = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory());
EventLoopGroup worker = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory());

try {
ServerBootstrap b = new ServerBootstrap();
b.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
pipeline.addLast(new WebSocketFrameHandler());
}
});
ChannelFuture f = b.bind(8000).sync();
f.channel().closeFuture().sync();
} catch (Exception e) {
log.error(e.getMessage());
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}

public static void main(String[] args) {
new WebSocketServer().start();
}
}

@Slf4j
class WebSocketFrameHandler extends SimpleChannelInboundHandler<WebSocketFrame> {

private static final ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

@Override
protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) {
if (frame instanceof TextWebSocketFrame) {
// 处理文本消息
String text = ((TextWebSocketFrame) frame).text();
log.info("收到文本消息: {}", text);
ctx.channel().writeAndFlush(new TextWebSocketFrame("服务端收到: " + text));

} else if (frame instanceof BinaryWebSocketFrame) {
// 处理二进制消息
log.info("收到二进制消息, 长度={}", frame.content().readableBytes());
ctx.channel().writeAndFlush(
new BinaryWebSocketFrame(frame.content().retain())
);

} else {
// 其他类型(Ping、Pong、Close)交由 WebSocketServerProtocolHandler 自动处理
log.debug("忽略的消息类型: {}", frame.getClass().getSimpleName());
}
}

@Override
public void handlerAdded(ChannelHandlerContext ctx) {
Channel incoming = ctx.channel();
channels.add(incoming);
log.info("新连接: {}", incoming.id().asShortText());
}

@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
Channel outcoming = ctx.channel();
channels.remove(outcoming);
log.info("断开连接: {}", ctx.channel().id().asShortText());
}
}

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
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioIoHandler;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

import java.util.Date;

/**
*
* @author yingzheng
* @date 2025-09-14
*/

class TimeClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg){
ByteBuf m = (ByteBuf) msg;
try {
long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
System.out.println(new Date(currentTimeMillis));
ctx.close();
} finally {
m.release();
}
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause){
cause.printStackTrace();
ctx.close();
}
}

public class TimeClient {
public static void main(String[] args) throws Exception{
String host = "127.0.0.1";
int port = 8000;

EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory());

try {
Bootstrap b = new Bootstrap();
b.group(workerGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeClientHandler());
}
});
ChannelFuture f = b.connect(host, port).sync();
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
}
}
}

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
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioIoHandler;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
*
* @author yingzheng
* @date 2025-09-14
*/

class DiscardServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ctx.writeAndFlush(msg);
// ByteBuf in = (ByteBuf) msg;
// try {
// System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII));
// } finally {
// ReferenceCountUtil.release(msg);
// }
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 释放资源
cause.printStackTrace();
ctx.close();
}
}

class TimeHandler extends ChannelInboundHandlerAdapter {

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
final ByteBuf time = ctx.alloc().buffer(4);
time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));
ChannelFuture f = ctx.writeAndFlush(time);
f.addListener((ChannelFutureListener) future -> {
assert f == future;
ctx.close();
});
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 释放资源
cause.printStackTrace();
}
}

public class DiscardServer {
private final int port;

public DiscardServer(int port) {
this.port = port;
}

public void run() throws Exception {
EventLoopGroup bossGroup = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory());
EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory());

try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeHandler());
}
});
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}

public static void main(String[] args) {
DiscardServer server = new DiscardServer(8000);
try {
server.run();
} catch (Exception e) {
e.printStackTrace();
}
}
}

算法原理

算法实现

1
2
3
4
5
6
7
public int binarySearch(T[] arr, T targ){
int left=0, right=arr.length-1;
while(left <= right){
int mid = left + (right - left) >> 1;

}
}

例子

List

ArrayList

LinkedList

Map

HashMap

LinkedHashMap

TreeMap

Set

HashSet

LinkedHashSet

TreeSet

Deque

ArrayDeque

版本

  • Servlet 1.0:定义了Servlet组件,一个Servlet组件运行在Servlet容器(Container)中,通过与容器交互,就可以响应一个HTTP请求;
  • Servlet 2.0:定义了JSP组件,一个JSP页面可以被动态编译为Servlet组件;
  • Servlet 2.4:定义了Filter(过滤器)组件,可以实现过滤功能;
  • Servlet 2.5:支持注解,提供了ServletContextListener接口,增加了一些安全性相关的特性;
  • Servlet 3.0:支持异步处理的Servlet,支持注解配置Servlet和过滤器,增加了SessionCookieConfig接口;
  • Servlet 3.1:提供了WebSocket的支持,增加了对HTTP请求和响应的流式操作的支持,增加了对HTTP协议的新特性的支持;
  • Servlet 4.0:支持HTTP/2的新特性,提供了HTTP/2的Server Push等特性;
  • Servlet 5.0:主要是把javax.servlet包名改成了jakarta.servlet
  • Servlet 6.0:继续增加一些新功能,并废除一部分功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 ┌────────────────────────────────┐
│ ServletContext │
│ │
│HttpServletRequest ┌─────────┐ │
─┼───────────────────▶│ Filter │ │
│HttpServletResponse └─────────┘ │
│ │ │
│ ▼ │
│ ┌─────────┐ │
│ │ Filter │ │
│ └─────────┘ │
│ │ │
│ ┌─────────┐ ▼ │
│ │Listener │ ┌─────────┐ │
│ └─────────┘ │ Filter │ │
│ ┌─────────┐ └─────────┘ │
│ │Listener │ │ │
│ └─────────┘ ▼ │
│ ┌─────────┐ ┌─────────┐ │
│ │Listener │ │ Servlet │ │
│ └─────────┘ └─────────┘ │
└────────────────────────────────┘

目的

面向对象设计鼓励模块间基于接口而非具体实现编程,以降低模块间的耦合,遵循依赖倒置原则,并支持开闭原则(对扩展开放,对修改封闭)。然而,直接依赖具体实现会导致在替换实现时需要修改代码,违背了开闭原则。

Service Provider Interface(SPI)将服务接口具体的服务实现分离开来,将服务调用方服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方

image-20250812162647688

图下方SPI实例中接口与调用方紧密结合,而实现方可以是不同的提供方。

具体的实例:数据库加载驱动、日志接口等

使用

接口

定义功能接口,其中包含接口中提供的功能方法。

1
2
3
4
5
package top.chc.api;

public interface MyService {
String doService();
}

调用方使用

调用方引入上述接口,并通过ServiceLoader.load()寻找路径下所有实现该接口的实现类。

1
2
3
4
5
6
7
8
9
10
11
12
13
package top.chc.call;

import java.util.ServiceLoader;
import top.chc.api.MyService;

public class Main {
public static void main(String[] args) {
ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
for(MyService myService : loader){
System.out.println(myService.doService());
}
}
}

以上便是调用方的全部代码了,后续不需要再修改上述所有代码,即调用方代码无需再修改。

实现方代码

实现方引入上述接口,并创建该接口的实现类。

1
2
3
4
5
6
7
8
9
10
11
package com.chc;

import top.chc.api.MyService;

public class MyServiceImpl implements MyService {
@Override
public String doService(){
return "service";
}
}

调用方配置

配置调用方工程的 resources/META-INF/services 文件夹,创建文件src/main/resources/META-INF/services/top.chc.api.MyService并写入实现类的权限定名。

  • 路径:resources/META-INF/services/
  • 文件名:接口的全限定名
  • 文件内容:实现类的全限定名(一个或多个,每行一个)
1
2
# 文件路径:src/main/resources/META-INF/services/top.chc.api.MyService
com.chc.MyServiceImpl

工作原理:

ServiceLoader 工作原理:

  1. 找到 META-INF/services/<接口全名> 文件
  2. 读取里面的每一行实现类名
  3. Class.forName() 加载这些类
  4. 通过无参构造方法 newInstance() 实例化

其中 接口类实现类 都必须放在调用方的 classpath 中。

代码实现

创建三个工程:

  • api项目:提供接口
  • achieve项目:提供实现类,其中包含api项目
  • use项目:调用类,其中包含api项目和achieve项目

实现代码:https://github.com/caohongchuan/java_spi_example

0%