Compare commits

...

125 commits
v0.0.6 ... main

Author SHA1 Message Date
WJQSERVER
a6e278d458 print errlog (jsonv2 marshal) 2026-01-26 08:08:01 +08:00
WJQSERVER
7b536ac137
Merge pull request #59 from infinite-iroha/fix-slice-panic
refactor: Improve engine's tree processing and context handling.
2025-12-15 00:05:02 +08:00
WJQSERVER
b348d7d41f update TempSkippedNodesPool 2025-12-14 23:42:50 +08:00
WJQSERVER
60b2936eff add TempSkippedNodesPool 2025-12-14 23:16:29 +08:00
WJQSERVER
9cfc82a347 chore: update go module dependencies. 2025-12-14 22:57:48 +08:00
WJQSERVER
904aea5df8 refactor: Improve engine's tree processing and context handling. 2025-12-14 22:56:37 +08:00
WJQSERVER
ee0ebc986c
Merge pull request #54 from infinite-iroha/dev
context added FileText method
2025-10-21 15:06:39 +08:00
wjqserver
e4aaaa1583 fix path to filepath 2025-10-21 15:06:26 +08:00
wjqserver
1361f6e237 update 2025-10-21 14:47:29 +08:00
WJQSERVER
a6458cca16
Merge pull request #53 from infinite-iroha/dev
update
2025-10-12 15:48:48 +08:00
wjqserver
76a89800a2 update 2025-10-12 15:47:02 +08:00
WJQSERVER
4955fb9d03
Merge pull request #52 from infinite-iroha/dev
fix StaticFS
2025-09-14 08:27:29 +08:00
wjqserver
5b98310de5 fix StaticFS 2025-09-14 08:24:01 +08:00
WJQSERVER
f1ac0dd6ff
Merge pull request #51 from infinite-iroha/dev
0.3.7
2025-09-10 02:40:51 +08:00
wjqserver
38ff5126e3 fix 2025-09-10 02:40:41 +08:00
WJQSERVER
b4e073ae2f
Update sse.go 2025-09-07 02:24:28 +08:00
WJQSERVER
af0a99acda add sse intn support 2025-09-06 17:55:45 +00:00
wjqserver
3ffde5742c add wanf 2025-08-20 16:50:26 +08:00
WJQSERVER
016df0efe4
Merge pull request #50 from infinite-iroha/dev
0.3.6
2025-08-01 10:27:01 +08:00
wjqserver
3590a77f90 fix reqip val 2025-08-01 10:23:49 +08:00
wjqserver
74f5770b42 update tree 2025-08-01 10:21:32 +08:00
WJQSERVER
0f4d90faeb
Merge pull request #49 from infinite-iroha/fix-router-panic
Fix router panic
2025-08-01 09:09:59 +08:00
wjqserver
783370fd79 update 2025-08-01 09:09:46 +08:00
wjqserver
295852e1a1 update reqip 2025-08-01 09:05:09 +08:00
wjqserver
99b48371b3 update test 2025-08-01 09:05:00 +08:00
google-labs-jules[bot]
e43b12e343 fix: correct shallow copy in router backtracking
The router could panic with a 'slice bounds out of range' error when handling requests that trigger its backtracking logic.

The root cause was a shallow copy of the node's `children` slice when creating a `skippedNode` for backtracking. This could lead to a corrupted state if the router needed to backtrack and then proceed down a wildcard path.

This commit fixes the issue by introducing a `copyChildren` method on the `node` struct, which creates a safe copy of the children slice. This method is now used when creating a `skippedNode`, ensuring that the backtracking logic is isolated and robust.
2025-08-01 00:49:53 +00:00
WJQSERVER
1e7682ad84
Merge pull request #48 from infinite-iroha/dev
add RunShutdownWithContext
2025-07-31 20:18:47 +08:00
wjqserver
3cd8ef84a2 add RunShutdownWithContext 2025-07-31 20:18:24 +08:00
WJQSERVER
2c60e84067
Merge pull request #46 from infinite-iroha/dev
0.3.4
2025-07-28 21:02:20 +08:00
wjqserver
895cd6222b update deps 2025-07-28 21:01:18 +08:00
wjqserver
c9b8e966c4 remove too much log print 2025-07-27 16:34:46 +08:00
WJQSERVER
dee05b048e
Merge pull request #45 from infinite-iroha/dev
update about
2025-07-27 16:07:33 +08:00
wjqserver
3e76566917 update about 2025-07-26 18:51:30 +08:00
WJQSERVER
ccf25dee46
Merge pull request #44 from infinite-iroha/dev
fix cfdt
2025-07-25 00:35:43 +08:00
WJQSERVER
1f0724af94
fix cfdt 2025-07-25 00:35:12 +08:00
WJQSERVER
e77fcb10d3
Merge pull request #43 from infinite-iroha/dev
add testflow
2025-07-24 16:41:14 +08:00
WJQSERVER
5e74fa011e
Merge pull request #42 from infinite-iroha/license-compliance
add MPL 2.0 license headers to all go files
2025-07-24 16:40:46 +08:00
wjqserver
de4bbe7959 add testflow 2025-07-24 16:39:48 +08:00
WJQSERVER
507e2f3813
Merge pull request #41 from infinite-iroha/dev
Dev
2025-07-24 16:09:09 +08:00
google-labs-jules[bot]
504089b748 feat: add MPL 2.0 license headers to all go files 2025-07-24 08:07:38 +00:00
wjqserver
5bb58ee6d3 add tree test 2025-07-24 15:39:17 +08:00
wjqserver
6b3f3335ab replace to iox 2025-07-18 17:40:01 +08:00
WJQSERVER
e10c20c5d6
Merge pull request #40 from infinite-iroha/dev
add Text
2025-07-18 15:30:20 +08:00
wjqserver
b06b4a227f update deps 2025-07-18 15:30:03 +08:00
wjqserver
ad167b6646 add Text 2025-07-18 15:26:43 +08:00
WJQSERVER
583609945e
Merge pull request #38 from infinite-iroha/dependabot/go_modules/github.com/WJQSERVER-STUDIO/httpc-0.8.1
Bump github.com/WJQSERVER-STUDIO/httpc from 0.8.0 to 0.8.1
2025-07-17 19:14:53 +08:00
WJQSERVER
18e7b203ec
Merge pull request #39 from infinite-iroha/dev
update deps
2025-07-17 19:14:16 +08:00
dependabot[bot]
e75619fc7a
Bump github.com/WJQSERVER-STUDIO/httpc from 0.8.0 to 0.8.1
Bumps [github.com/WJQSERVER-STUDIO/httpc](https://github.com/WJQSERVER-STUDIO/httpc) from 0.8.0 to 0.8.1.
- [Commits](https://github.com/WJQSERVER-STUDIO/httpc/compare/v0.8.0...v0.8.1)

---
updated-dependencies:
- dependency-name: github.com/WJQSERVER-STUDIO/httpc
  dependency-version: 0.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-17 01:13:22 +00:00
wjqserver
bb8b53b5c3 update deps 2025-07-16 09:55:15 +08:00
WJQSERVER
a6171241ce
Merge pull request #37 from infinite-iroha/dev
refactor(internal break) Enhance static file serving for modularity a…
2025-07-09 03:20:31 +08:00
wjqserver
336b8ad958 fix: missed engine.UnMatchFSRoutes set value 2025-07-09 03:17:16 +08:00
wjqserver
989eb34c4c refactor(internal break) Enhance static file serving for modularity and performance 2025-07-09 03:10:46 +08:00
WJQSERVER
5d2ab04b6b
Merge pull request #36 from infinite-iroha/dev
add maxBytesReader & ctxMerge
2025-07-09 02:59:11 +08:00
wjqserver
49508b49c1 fix limitMaxSizeReader non use body close & fix mergeCtx Value 2025-07-09 00:17:52 +08:00
wjqserver
cb86cb935a add maxBytesReader & ctxMerge 2025-07-08 13:26:18 +08:00
WJQSERVER
780e640253
Merge pull request #35 from infinite-iroha/dev
update deps & use copyb high perfromance pool & switch to stream json…
2025-07-06 18:10:21 +08:00
wjqserver
17bab2dcfd remove unuse code 2025-07-06 18:09:37 +08:00
wjqserver
edca87906d update deps & use copyb high perfromance pool & switch to stream json encoder 2025-07-06 17:59:24 +08:00
WJQSERVER
7084f6d004
Merge pull request #33 from infinite-iroha/dev
Update README.md for add DeepWiki badge
2025-06-28 00:57:07 +08:00
WJQSERVER
2454a18422
Update README.md for add DeepWiki badge 2025-06-28 00:55:19 +08:00
WJQSERVER
87fc425dc4
Merge pull request #32 from infinite-iroha/dev
0.2.8
2025-06-25 17:50:00 +08:00
wjqserver
76d07364ae optimize defaulterrorhandle && add SetRespBodyFile 2025-06-25 17:49:03 +08:00
wjqserver
9ec1d1f2c6 update deps 2025-06-22 18:12:29 +08:00
WJQSERVER
6c96e189d3
Merge pull request #30 from infinite-iroha/dev
add slash settings && StaticFS
2025-06-22 18:11:14 +08:00
wjqserver
6c6a5a99b1 add slash settings && StaticFS 2025-06-22 18:06:19 +08:00
WJQSERVER
7733dc80cd
Merge pull request #25 from infinite-iroha/dev
update middlewareX
2025-06-18 08:55:27 +08:00
wjqserver
543b3165ca optimize midwareX 2025-06-18 08:55:15 +08:00
wjqserver
f434f517d4 update middlewareX 2025-06-18 08:47:20 +08:00
WJQSERVER
b941523186
Merge pull request #24 from infinite-iroha/dev
use new resolveRoutePath replace path.Join && add UseIf
2025-06-17 14:43:10 +08:00
wjqserver
00b9c283d1 update chain exp 2025-06-17 14:41:39 +08:00
wjqserver
21d048b5ab update UseIf 2025-06-17 14:34:52 +08:00
wjqserver
53544644af use new resolveRoutePath replace path.Join && add UseIf 2025-06-17 14:20:14 +08:00
WJQSERVER
229d15d405
Merge pull request #23 from infinite-iroha/dev
[context] add SetHeaders
2025-06-14 17:58:51 +08:00
wjqserver
bfc6b439e4 [context] add SetHeaders 2025-06-14 17:58:08 +08:00
WJQSERVER
556e23c1a4
Merge pull request #22 from infinite-iroha/dev
fix default errorhandle
2025-06-13 17:54:00 +08:00
wjqserver
ebb634b643 update default error handle 2025-06-13 17:53:48 +08:00
wjqserver
0076c7538c fix default error handle 2025-06-13 14:41:10 +08:00
wjqserver
57e92874d1 update deps 2025-06-12 10:02:33 +08:00
WJQSERVER
362ae16640
Merge pull request #21 from infinite-iroha/dev
update serve && add custom srv configure
2025-06-12 09:49:11 +08:00
wjqserver
803a6747f6 update serve && add custom srv configure 2025-06-12 09:40:46 +08:00
WJQSERVER
fa8f044b81
Merge pull request #19 from infinite-iroha/dev
update methods
2025-06-11 11:43:34 +08:00
wjqserver
896182417f fix header writer after status issue 2025-06-11 11:24:54 +08:00
wjqserver
9a2aeef0d0 remove ANY form MethodsSet to avoid conflict 2025-06-11 11:23:15 +08:00
wjqserver
bb822599b9 update methods 2025-06-11 11:11:54 +08:00
WJQSERVER
37917363e2
Merge pull request #18 from infinite-iroha/dev
update deps
2025-06-10 21:54:22 +08:00
wjqserver
96154fff78 update deps 2025-06-10 21:52:32 +08:00
WJQSERVER
76bf441ce8
Merge pull request #17 from infinite-iroha/dev
0.2.0
2025-06-10 21:42:57 +08:00
wjqserver
5ae9e9c12e updaten license 2025-06-10 21:39:37 +08:00
wjqserver
ce5efae287 update 2025-06-10 21:37:53 +08:00
wjqserver
e6b54eedbf remove tgzip 2025-06-10 00:04:15 +08:00
WJQSERVER
6fd5e84f6d
Merge pull request #16 from infinite-iroha/dev
remove ws, move to github.com/fenthope/ws
2025-06-09 23:34:34 +08:00
wjqserver
e891afe0b4 remove ws, move to github.com/fenthope/ws 2025-06-09 23:18:15 +08:00
WJQSERVER
d5eb1406ae
Merge pull request #15 from infinite-iroha/dev
update Readme
2025-06-08 04:34:35 +08:00
wjqserver
90ea798c87 update README 2025-06-08 04:33:43 +08:00
wjqserver
a4c22962ad update go version 2025-06-07 23:45:02 +08:00
WJQSERVER
0649a05f72
Create dependabot.yml 2025-06-07 23:08:27 +08:00
WJQSERVER
8eeba0df72
Merge pull request #14 from infinite-iroha/dev
0.1.0
2025-06-06 23:28:27 +08:00
wjqserver
1d5ffac153 [context] optimize context reset and reuse 2025-06-06 22:40:40 +08:00
wjqserver
450d6294ad remove dup engine.pool.Put(c) 2025-06-06 22:13:17 +08:00
wjqserver
4249f0192e add IsHijacked for respw && add recover for flush 2025-06-06 21:44:45 +08:00
wjqserver
740dce54a2 optimize recovery 2025-06-06 21:43:49 +08:00
wjqserver
81fd3902cb fix ctx lifetime 2025-06-06 21:43:16 +08:00
wjqserver
0d6109f6da add adapter 2025-06-06 21:42:24 +08:00
wjqserver
61e67bc2a0 [context] fix writer reset 2025-06-06 21:29:09 +08:00
WJQSERVER
da5d165de3
update readme typo 2025-06-06 10:15:19 +08:00
WJQSERVER
c46cf7bd03
Merge pull request #13 from infinite-iroha/dev
fix hijack
2025-06-06 01:32:05 +08:00
wjqserver
1618f89ba5 fix hijack 2025-06-06 01:25:35 +08:00
wjqserver
643fcd77ef update info 2025-06-05 21:06:09 +08:00
WJQSERVER
9c2c078f66
Merge pull request #12 from infinite-iroha/dev
fix NoRoutes
2025-06-05 20:48:02 +08:00
wjqserver
69469c0306 fix NoRoutes 2025-06-05 20:47:41 +08:00
WJQSERVER
41af56ebcf
Merge pull request #11 from infinite-iroha/dev
[engine] add NoRoutes
2025-06-05 20:40:06 +08:00
wjqserver
c6c65c04a7 [engine] add NoRoutes 2025-06-05 20:31:18 +08:00
WJQSERVER
677a7f037f
Merge pull request #9 from infinite-iroha/dev
add testutil
2025-06-05 19:15:04 +08:00
wjqserver
3f2dc21180 add testutil 2025-06-05 19:14:38 +08:00
WJQSERVER
f1ff1f935f
Merge pull request #8 from infinite-iroha/dev
[engine] add StaticFile && [context] Add Cookie Method, port from gin
2025-06-05 19:03:50 +08:00
wjqserver
757cd962ab [context] Add Cookie Method, port from gin 2025-06-05 18:35:28 +08:00
wjqserver
31d2065f07 [engine] add StaticFile 2025-06-04 21:50:03 +08:00
WJQSERVER
b1988305bb
Merge pull request #7 from infinite-iroha/dev
ctx add logreco method
2025-06-04 21:31:48 +08:00
wjqserver
b8a1c5523a ctx add logreco method 2025-06-04 21:30:56 +08:00
WJQSERVER
46fd5c7852
Merge pull request #6 from infinite-iroha/dev
add engine logger
2025-06-04 20:52:57 +08:00
WJQSERVER
52cc857d00
Merge pull request #5 from infinite-iroha/dev
add GetProtocol
2025-06-02 04:49:02 +08:00
WJQSERVER
7f32c15b4b
Merge pull request #4 from infinite-iroha/dev
0.0.4
2025-05-30 21:43:57 +08:00
WJQSERVER
4f83f0ee6a
Merge pull request #3 from infinite-iroha/dev
add Raw & GetReqBodyFull
2025-05-30 16:57:36 +08:00
WJQSERVER
f2c855966b
Merge pull request #2 from infinite-iroha/dev
0.0.2
2025-05-29 21:48:32 +08:00
WJQSERVER
3acf69e387
Merge pull request #1 from infinite-iroha/dev
init(v0.0.1)
2025-05-28 18:30:31 +08:00
29 changed files with 4957 additions and 1591 deletions

11
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "gomod" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"

24
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,24 @@
name: Go Test
on:
push:
tags:
- '*'
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Run tests
run: go test -v ./...
- name: Run tests race
run: go test -race -v ./...

516
LICENSE
View file

@ -1,199 +1,373 @@
WJQserver Studio 开源许可证
版本 v2.0
Mozilla Public License Version 2.0
==================================
版权所有 © WJQserver Studio 2025
版权所有 © Infinite Iroha 2025
版权所有 © WJQserver 2025
1. Definitions
--------------
定义
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
* 许可 (License): 指的是在本许可证内定义的使用、复制、分发与修改软件的条款与要求。
* 授权方 (Licensor): 指的是拥有版权的个人或组织,亦或是拥有版权的个人或组织所指派的实体,在本许可证中特指 WJQserver Studio。
* 贡献者 (Contributor): 指的是授权方以及根据本许可证授予贡献代码或软件的个人或实体。
* 您 (You): 指的是行使本许可授予的权限的个人或法律实体。
* 衍生作品 (Derivative Works): 指的是基于本软件或本软件任何部分的修改作品,无论修改程度如何。这包括但不限于基于本软件或其任何部分的修改、修订、改编、翻译或其他形式的创作,以及包含本软件或其部分的集合作品。
* 非营利性使用 (Non-profit Use): 指的是不以直接商业盈利为主要目的的使用方式,包括但不限于:
* 个人用途: 由个人为了个人学习、研究、实验、非商业项目、个人网站搭建、毕业设计、家庭内部娱乐等非直接商业目的使用软件。
* 教育用途: 在教育机构(如学校、大学、培训机构)内部用于教学、研究、学术交流等活动。
* 科研用途: 在科研院所、实验室等机构内部用于科学研究、实验开发等活动。
* 慈善与公益用途: 由慈善机构、公益组织等非营利性组织为了其公益使命或慈善事业内部运营使用,或对外提供不直接产生商业利润的公益服务。
* 内部运营用途 (非营利组织) 非营利性组织在其内部运营中使用软件,例如用于行政管理、会员管理、内部沟通、项目管理等非直接营利性活动。
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
开源与自由软件
1.3. "Contribution"
means Covered Software of a particular Contributor.
本项目为开源软件,允许用户在遵循本许可证的前提下访问和使用源代码。
本项目旨在向用户提供尽可能广泛的非商业使用自由,同时保障社区的共同发展和良性生态,并为商业创新提供清晰的路径。
强调版权所有,所有权利由 WJQserver Studio 及贡献者共同保留。
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
许可证条款
1.5. "Incompatible With Secondary Licenses"
means
1. 使用权限
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
* 1.1 非营利性使用: 您被授予在非营利性使用场景下,为了任何目的,自由使用本软件的权限。 非营利性使用的具体场景包括但不限于定义部分所列举的各种情况。
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
* 1.2 商业使用: 您可以在商业环境中使用本软件,无需获得额外授权,但您的商业使用行为必须遵守以下条款:
1.6. "Executable Form"
means any form of the work other than Source Code Form.
* 1.2.1 保持声明: 您在进行商业使用时,不得移除或修改软件中包含的原始版权声明、许可证声明以及来源声明。
* 1.2.2 开源继承 (Copyleft) 与互惠共享: 如果您或您的组织希望将本软件或其衍生作品用于任何商业用途,包括但不限于:
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
* 盈利性分发: 销售、出租、许可分发本软件或其衍生作品。
* 盈利性服务: 基于本软件或其衍生作品提供商业服务,例如 SaaS 服务、咨询服务、定制开发服务、收费技术支持服务等。
* 嵌入式商业应用: 将本软件或其衍生作品嵌入到商业产品或解决方案中进行销售。
* 组织内部商业运营: 在营利性组织的内部运营中使用修改后的版本以直接支持其商业活动,例如定制化内部系统,通过例如但不限于在软件或相关服务中投放广告 (例如 Google Ads 等),应用内购买 (内购), 会员订阅, 增值功能收费等方式直接或间接产生商业收入。
1.8. "License"
means this document.
您必须选择以下两种方式之一:
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
* i) 继承本许可证并开源: 您必须以本许可证或兼容的开源许可证分发您的衍生作品,并公开您的衍生作品的全部源代码,使得您的衍生作品的接收者也享有与您相同的权利,包括进一步修改和商业使用的权利。 本选项旨在促进社区的共同发展和知识共享,确保基于本软件的商业创新成果也能回馈社区。
* ii) 获得授权方明确授权: 如果您不希望以开源方式发布您的衍生作品,或者希望使用其他许可证进行分发,或者您希望在商业运营中使用修改后的版本但不开源,您必须事先获得 WJQserver Studio 的明确书面授权。 授权的具体条款和条件将由 WJQserver Studio 另行协商确定。
1.10. "Modifications"
means any of the following:
2. 复制与分发
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
* 2.1 原始版本复制与分发: 您可以复制和分发本软件的原始版本,前提是必须满足以下条件:
(b) any new file in Source Code Form that contains any Covered
Software.
* 保留所有声明: 完整保留所有原始版权声明、许可证声明、来源声明以及其他所有权声明。
* 附带许可证: 在分发软件时,必须同时附带本许可证的完整文本,确保接收者知悉并理解本许可证的全部条款。
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
* 2.2 衍生作品复制与分发: 您可以复制和分发基于本软件的衍生作品,您对衍生作品的分发行为将受到本许可证第 1.2.2 条(开源继承与互惠共享)的约束。
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
3. 修改权限
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
* 3.1 自由修改: 您被授予自由修改本软件的权限,无论修改目的是非营利性使用还是商业用途。
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
* 3.2 修改后使用与分发约束: 当您将修改后的版本用于商业用途或分发修改后的版本时,您需要遵守本许可证第 1.2.2 条(开源继承与互惠共享)以及第 2 条(复制与分发)的规定。 即使您不分发修改后的版本,只要您将其用于商业目的,也需要遵守开源继承条款或获得授权。
2. License Grants and Conditions
--------------------------------
* 3.3 贡献接受: WJQserver Studio 鼓励社区贡献代码。如果您向本项目贡献代码,您需要同意您的贡献代码按照本许可证条款进行许可。
2.1. Grants
4. 专利权
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
* 4.1 无专利担保,风险自担: 本软件以“现状”提供,授权方及贡献者明确声明,不对本软件的专利侵权问题做任何形式的担保,亦不承担任何因专利侵权可能产生的责任与后果。 用户理解并同意,使用本软件的专利风险完全由用户自行承担。
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
* 4.2 专利纠纷应对: 如因用户使用本软件而引发任何专利侵权指控、诉讼或索赔,用户应自行负责处理并承担全部法律责任。 授权方及贡献者无义务参与任何相关法律程序,亦不承担任何由此产生的费用或赔偿。
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
5. 免责声明
2.2. Effective Date
* 5.1 “现状”提供,无任何保证: 本软件按“现状”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途适用性及非侵权性。
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
* 5.2 责任限制: 在适用法律允许的最大范围内,在任何情况下,授权方或任何贡献者均不对因使用或无法使用本软件而产生的任何直接、间接、偶然、特殊、惩罚性或后果性损害(包括但不限于采购替代商品或服务;损失使用、数据或利润;或业务中断)负责,无论其是如何造成的,也无论依据何种责任理论,即使已被告知可能发生此类损害。
2.3. Limitations on Grant Scope
* 5.3 用户法律责任: 用户需根据当地法律对待本项目,确保遵守所有适用法规。
6. 许可证期限与终止
* 6.1 许可证期限: 除版权所有人主动宣布放弃本软件版权外,本许可证无限期生效。
* 6.2 许可证终止: 如果您未能遵守本许可证的任何条款或条件,授权方有权终止本许可证。 您的许可证将在您违反本许可证条款时自动终止。
* 6.3 终止后的效力: 许可证终止后,您根据本许可证所享有的所有权利将立即终止,但您在许可证终止前已合法分发的软件副本,其接收者所获得的许可及权利将不受影响,继续有效。 免责声明(第 5 条)和责任限制(第 5.2 条)在本许可证终止后仍然有效。
7. 条款修订
* 7.1 修订权利保留: 授权方保留随时修改本许可证条款的权利,以便更好地适应法律、技术发展以及社区需求。
* 7.2 修订生效与接受: 修订后的条款将在发布时生效,除非另行声明,否则继续使用、复制、分发或修改本软件即表示您接受修订后的条款。授权方鼓励用户定期查阅本许可证的最新版本。
8. 其他
* 8.1 法定权利: 本许可证不影响您作为最终用户在适用法律下的法定权利。
* 8.2 条款可分割性: 若本许可证的某些条款被认定为不可执行,其余条款仍然完全有效。
* 8.3 版本更新: 授权方可能会发布本许可证的修订版本或新版本。您可以选择是继续使用本许可证的旧版本还是选择适用新版本。
WJQserver Studio Open Source License
Version v2.0
Copyright © WJQserver Studio 2024
Definitions
* License: Refers to the terms and requirements for use, reproduction, distribution, and modification defined within this license.
* Licensor: Refers to the individual or organization that holds the copyright, or the entity designated by the copyright holder, specifically WJQserver Studio in this license.
* Contributor: Refers to the Licensor and individuals or entities who contribute code or software under this License.
* You: Refers to the individual or legal entity exercising permissions granted by this License.
* Derivative Works: Refers to works modified based on the Software or any part thereof, regardless of the extent of modification. This includes but is not limited to modifications, revisions, adaptations, translations, or other forms of creation based on the Software or any part thereof, as well as collective works containing the Software or parts thereof.
* Non-profit Use: Refers to uses not primarily intended for direct commercial profit, including but not limited to:
* Personal Use: Use by an individual for personal learning, research, experimentation, non-commercial projects, personal website development, graduation projects, home entertainment, and other non-directly commercial purposes.
* Educational Use: Use within educational institutions (such as schools, universities, training organizations) for activities such as teaching, research, and academic exchange.
* Scientific Research Use: Use within scientific research institutions, laboratories, and similar organizations for activities such as scientific research and experimental development.
* Charitable and Public Welfare Use: Use by charitable organizations, public welfare organizations, and similar non-profit entities for their public missions or internal operation of charitable activities, or to provide public services that do not directly generate commercial profit.
* Internal Operational Use (Non-profit Organizations): Use within the internal operations of non-profit organizations, such as for administrative management, membership management, internal communication, project management, and other non-directly profit-generating activities.
Open Source and Free Software
This project is open-source software, allowing users to access and use the source code under the premise of complying with this License.
This project aims to provide users with the broadest possible freedom for non-commercial use while ensuring the common development and healthy ecosystem of the community, and providing a clear path for commercial innovation.
Copyright is emphasized; all rights are jointly reserved by WJQserver Studio and Contributors.
License Terms
1. Permissions for Use
* 1.1 Non-profit Use: You are granted permission to freely use the Software for any purpose in non-profit use scenarios. Specific non-profit use scenarios include but are not limited to the various situations listed in the Definition section.
* 1.2 Commercial Use: You may use the Software in a commercial environment without additional authorization, but your commercial use must comply with the following terms:
* 1.2.1 Maintain Statements: When conducting commercial use, you must not remove or modify the original copyright notices, license notices, and source statements contained in the Software.
* 1.2.2 Open Source Inheritance (Copyleft) and Reciprocal Sharing: If you or your organization wish to use the Software or its Derivative Works for any commercial purpose, including but not limited to:
* Profit-generating Distribution: Selling, renting, licensing, or distributing the Software or its Derivative Works.
* Profit-generating Services: Providing commercial services based on the Software or its Derivative Works, such as SaaS services, consulting services, custom development services, and paid technical support services.
* Embedded Commercial Applications: Embedding the Software or its Derivative Works into commercial products or solutions for sale.
* Internal Commercial Operations: Using modified versions within the internal operations of for-profit organizations to directly support their commercial activities, such as customized internal systems, generating commercial revenue directly or indirectly through means including but not limited to placing advertisements in the software or related services (e.g., Google Ads), in-app purchases, membership subscriptions, and charging for value-added features.
You must choose one of the following two options:
* i) Inherit this License and Open Source: You must distribute your Derivative Works under this License or a compatible open-source license and publicly disclose the entire source code of your Derivative Works, so that recipients of your Derivative Works also enjoy the same rights as you, including the right to further modify and use commercially. This option aims to promote the common development and knowledge sharing of the community, ensuring that commercial innovation achievements based on this Software can also contribute back to the community.
* ii) Obtain Explicit Authorization from the Licensor: If you do not wish to release your Derivative Works in an open-source manner, or wish to distribute them under another license, or you wish to use a modified version in commercial operations without open-sourcing it, you must obtain explicit written authorization from WJQserver Studio in advance. The specific terms and conditions of authorization will be determined separately by WJQserver Studio through negotiation.
2. Reproduction and Distribution
* 2.1 Reproduction and Distribution of Original Version: You may reproduce and distribute the original version of the Software, provided that the following conditions are met:
* Retain All Statements: Completely retain all original copyright notices, license notices, source statements, and other proprietary notices.
* Accompany with License: When distributing the Software, you must also include the full text of this License to ensure that recipients are aware of and understand all terms of this License.
* 2.2 Reproduction and Distribution of Derivative Works: You may reproduce and distribute Derivative Works based on the Software. Your distribution of Derivative Works will be subject to the constraints of Clause 1.2.2 of this License (Open Source Inheritance and Reciprocal Sharing).
3. Modification Permissions
* 3.1 Free Modification: You are granted permission to freely modify the Software, regardless of whether the purpose of modification is for non-profit use or commercial use.
* 3.2 Constraints on Use and Distribution after Modification: When you use a modified version for commercial purposes or distribute a modified version, you need to comply with the provisions of Clause 1.2.2 of this License (Open Source Inheritance and Reciprocal Sharing) and Clause 2 (Reproduction and Distribution). Even if you do not distribute the modified version, as long as you use it for commercial purposes, you also need to comply with the open-source inheritance clause or obtain authorization.
* 3.3 Contribution Acceptance: WJQserver Studio encourages community contribution of code. If you contribute code to this project, you need to agree that your contributed code is licensed under the terms of this License.
4. Patent Rights
* 4.1 No Patent Warranty, Risk Self-Bearing: The software is provided “AS IS”, and the Licensor and Contributors explicitly declare that they do not provide any form of warranty regarding patent infringement issues of this software, nor do they assume any responsibility and consequences arising from patent infringement. Users understand and agree that the patent risk of using this software is entirely borne by the users themselves.
* 4.2 Handling of Patent Disputes: If any patent infringement allegations, lawsuits, or claims arise due to the user's use of this Software, the user shall be solely responsible for handling and bear all legal liabilities. The Licensor and Contributors are under no obligation to participate in any related legal proceedings, nor do they bear any costs or compensation arising therefrom.
5. Disclaimer of Warranty
* 5.1 “AS IS” Provision, No Warranty: The software is provided “AS IS” without any express or implied warranties, including but not limited to warranties of merchantability, fitness for a particular purpose, and non-infringement.
* 5.2 Limitation of Liability: To the maximum extent permitted by applicable law, in no event shall the Licensor or any Contributor be liable for any direct, indirect, incidental, special, punitive, or consequential damages (including but not limited to procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.
* 5.3 User Legal Responsibility: Users shall treat this project in accordance with local laws and regulations to ensure compliance with all applicable laws and regulations.
6. License Term and Termination
* 6.1 License Term: Unless the copyright holder proactively announces the abandonment of the copyright of this software, this License shall be effective indefinitely from the date of your acceptance.
* 6.2 License Termination: If you fail to comply with any terms or conditions of this License, the Licensor has the right to terminate this License. Your License will automatically terminate upon your violation of the terms of this License.
* 6.3 Effect after Termination: Upon termination of the License, all rights granted to you under this License will terminate immediately, but the licenses and rights obtained by recipients of software copies you have legally distributed before the termination of the License will not be affected and will remain valid. The Disclaimer of Warranty (Clause 5) and Limitation of Liability (Clause 5.2) shall remain in effect after the termination of this License.
7. Revision of Terms
* 7.1 Reservation of Revision Rights: The Licensor reserves the right to modify the terms of this License at any time to better adapt to legal, technological developments, and community needs.
* 7.2 Effectiveness and Acceptance of Revisions: Revised terms will take effect upon publication, and unless otherwise stated, continued use, reproduction, distribution, or modification of the Software indicates your acceptance of the revised terms. The Licensor encourages users to periodically review the latest version of this License.
8. Other
* 8.1 Statutory Rights: This License does not affect your statutory rights as an end-user under applicable laws.
* 8.2 Severability of Terms: If certain terms of this License are deemed unenforceable, the remaining terms shall remain in full force and effect.
* 8.3 Version Updates: The Licensor may publish revised versions or new versions of this License. You may choose to continue using the old version of this License or choose to apply the new version.
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View file

@ -1,9 +1,91 @@
# Touka 框架
# Touka(灯花)框架
Touka(灯花) 是一个基于 Go 语言构建的多层次、高性能 Web 框架。其设计目标是为开发者提供**更直接的控制、有效的扩展能力,以及针对特定场景的行为优化**。
**想深入了解 Touka 吗?请阅读我们的 -> [深度指南 (about-touka.md)](about-touka.md)**
这份深度指南包含了对框架设计哲学、核心功能(路由、上下文、中间件、错误处理等)的全面剖析,并提供了大量可直接使用的代码示例,帮助您快速上手并精通 Touka。
### 快速上手
```go
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"time"
"github.com/fenthope/reco"
"github.com/infinite-iroha/touka"
)
func main() {
r := touka.Default() // 使用带 Recovery 中间件的默认引擎
// 配置日志记录器 (可选)
logConfig := reco.Config{
Level: reco.LevelDebug,
Mode: reco.ModeText,
Output: os.Stdout,
Async: true,
}
r.SetLoggerCfg(logConfig)
// 配置统一错误处理器
r.SetErrorHandler(func(c *touka.Context, code int, err error) {
c.JSON(code, touka.H{"error_code": code, "message": http.StatusText(code)})
c.GetLogger().Errorf("发生HTTP错误: %d, 路径: %s, 错误: %v", code, c.Request.URL.Path, err)
})
// 注册路由
r.GET("/hello/:name", func(c *touka.Context) {
name := c.Param("name")
query := c.DefaultQuery("mood", "happy")
c.String(http.StatusOK, "Hello, %s! You seem %s.", name, query)
})
// 启动服务器 (支持优雅关闭)
log.Println("Touka Server starting on :8080...")
if err := r.RunShutdown(":8080", 10*time.Second); err != nil {
log.Fatalf("Touka server failed to start: %v", err)
}
}
```
## 中间件支持
### 内置
- **Recovery:** `r.Use(touka.Recovery())` (已包含在 `touka.Default()` 中)
### 第三方 (fenthope)
- [访问日志-record](https://github.com/fenthope/record)
- [Gzip](https://github.com/fenthope/gzip)
- [压缩-Compress(Deflate,Gzip,Zstd)](https://github.com/fenthope/compress)
- [请求速率限制-ikumi](https://github.com/fenthope/ikumi)
- [sessions](https://github.com/fenthope/sessions)
- [jwt](https://github.com/fenthope/jwt)
- [带宽限制](https://github.com/fenthope/toukautil/blob/main/bandwithlimiter.go)
## 文档与贡献
* **深度指南:** **[about-touka.md](about-touka.md)**
* **API 文档:** 访问 [pkg.go.dev/github.com/infinite-iroha/touka](https://pkg.go.dev/github.com/infinite-iroha/touka) 查看完整的 API 参考。
* **贡献:** 我们欢迎任何形式的贡献,无论是错误报告、功能建议还是代码提交。请遵循项目的贡献指南。
## 相关项目
- [gin](https://github.com/gin-gonic/gin): Touka 在路由和 API 设计上参考了 Gin。
- [reco](https://github.com/fenthope/reco): Touka 框架的默认日志库。
- [httpc](https://github.com/WJQSERVER-STUDIO/httpc): 一个现代化且易用的 HTTP Client作为 Touka 框架 Context 携带的 HTTPC。
## 许可证
本项目在v0阶段使用WJQSERVER STUDIO LICENSE许可证, 后续进行调整
本项目基于 [Mozilla Public License, v. 2.0](https://mozilla.org/MPL/2.0/) 许可。
tree部分来自[gin](https://github.com/gin-gonic/gin)与[httprouter](https://github.com/julienschmidt/httprouter)
[WJQSERVER/httproute](https://github.com/WJQSERVER/httprouter)是本项目的前身(一个[httprouter](https://github.com/julienschmidt/httprouter)的fork版本)
`tree.go` 部分代码源自 [gin](https://github.com/gin-gonic/gin) 与 [httprouter](https://github.com/julienschmidt/httprouter),其原始许可为 BSD-style。

577
about-touka.md Normal file
View file

@ -0,0 +1,577 @@
# 关于 Touka (灯花) 框架:一份深度指南
Touka (灯花) 是一个基于 Go 语言构建的、功能丰富且高性能的 Web 框架。它的核心设计目标是为开发者提供一个既强大又灵活的工具集,允许对框架行为进行深度定制,同时通过精心设计的组件和机制,优化在真实业务场景中的开发体验和运行性能。
本文档旨在提供一份全面而深入的指南,帮助您理解 Touka 的核心概念、设计哲学以及如何利用其特性来构建健壮、高效的 Web 应用。
---
## 核心设计哲学
Touka 的设计哲学根植于以下几个核心原则:
* **控制力与可扩展性:** 框架在提供强大默认功能的同时,也赋予开发者充分的控制权。我们相信开发者最了解自己的业务需求。因此,无论是路由行为、错误处理逻辑,还是服务器协议,都可以根据具体需求进行精细调整和扩展。
* **明确性与可预测性:** API 设计力求直观和一致,使得框架的行为易于理解和预测,减少开发过程中的意外。我们避免使用过多的“魔法”,倾向于让代码的意图清晰可见。
* **性能意识:** 在核心组件的设计中性能是一个至关重要的考量因素。通过采用如对象池、优化的路由算法等技术Touka 致力于在高并发场景下保持低延迟和高吞吐。
* **开发者体验:** 框架内置了丰富的辅助工具和便捷的 API例如与请求上下文绑定的日志记录器和 HTTP 客户端,旨在简化常见任务,提升开发效率。
---
## 核心功能深度剖析
### 1. 引擎 (Engine):框架的中央枢纽
`Engine` 是 Touka 框架的实例,也是所有功能的入口和协调者。它实现了 `http.Handler` 接口,可以无缝集成到 Go 的标准 HTTP 生态中。
#### 1.1. 初始化引擎
```go
// 创建一个“干净”的引擎,不包含任何默认中间件
r := touka.New()
// 创建一个带有默认中间件的引擎,目前仅包含 Recovery()
// 推荐在生产环境中使用,以防止 panic 导致整个服务崩溃
r := touka.Default()
```
#### 1.2. 引擎配置
`Engine` 提供了丰富的配置选项,允许您定制其核心行为。
```go
func main() {
r := touka.New()
// === 路由行为配置 ===
// 自动重定向尾部带斜杠的路径,默认为 true
// e.g., /foo/ 会被重定向到 /foo
r.SetRedirectTrailingSlash(true)
// 自动修复路径的大小写,默认为 true
// e.g., /FOO 会被重定向到 /foo (如果 /foo 存在)
r.SetRedirectFixedPath(true)
// 当路由存在但方法不匹配时,自动处理 405 Method Not Allowed默认为 true
r.SetHandleMethodNotAllowed(true)
// === IP 地址解析配置 ===
// 是否信任 X-Forwarded-For, X-Real-IP 等头部来获取客户端 IP默认为 true
// 在反向代理环境下非常有用
r.SetForwardByClientIP(true)
// 自定义用于解析 IP 的头部列表,按顺序查找
r.SetRemoteIPHeaders([]string{"X-Forwarded-For", "X-App-Client-IP", "X-Real-IP"})
// === 请求体大小限制 ===
// 设置全局默认的请求体最大字节数,-1 表示不限制
// 这有助于防止 DoS 攻击
r.SetGlobalMaxRequestBodySize(10 * 1024 * 1024) // 10 MB
// ... 其他配置
r.Run(":8080")
}
```
#### 1.3. 服务器生命周期管理
Touka 提供了对底层 `*http.Server` 的完全控制,并内置了优雅关闭的逻辑。
```go
func main() {
r := touka.New()
// 通过 ServerConfigurator 对 http.Server 进行自定义配置
r.SetServerConfigurator(func(server *http.Server) {
// 设置自定义的读写超时时间
server.ReadTimeout = 15 * time.Second
server.WriteTimeout = 15 * time.Second
fmt.Println("自定义的 HTTP 服务器配置已应用")
})
// 启动服务器,并支持优雅关闭
// RunShutdown 会阻塞,直到收到 SIGINT 或 SIGTERM 信号
// 第二个参数是优雅关闭的超时时间
fmt.Println("服务器启动于 :8080")
if err := r.RunShutdown(":8080", 10*time.Second); err != nil {
log.Fatalf("服务器启动失败: %v", err)
}
}
```
---
### 2. 路由系统 (Routing):强大、灵活、高效
Touka 的路由系统基于一个经过优化的**基数树 (Radix Tree)**,它支持静态路径、路径参数和通配符,并能实现极高的查找性能。
#### 2.1. 基本路由
```go
// 精确匹配的静态路由
r.GET("/ping", func(c *touka.Context) {
c.String(http.StatusOK, "pong")
})
// 注册多个 HTTP 方法
r.HandleFunc([]string{"GET", "POST"}, "/data", func(c *touka.Context) {
c.String(http.StatusOK, "Data received via %s", c.Request.Method)
})
// 注册所有常见 HTTP 方法
r.ANY("/any", func(c *touka.Context) {
c.String(http.StatusOK, "Handled with ANY for method %s", c.Request.Method)
})
```
#### 2.2. 参数化路由
使用冒号 `:` 来定义路径参数。
```go
r.GET("/users/:id", func(c *touka.Context) {
// 通过 c.Param() 获取路径参数
userID := c.Param("id")
c.String(http.StatusOK, "获取用户 ID: %s", userID)
})
r.GET("/articles/:category/:article_id", func(c *touka.Context) {
category := c.Param("category")
articleID := c.Param("article_id")
c.JSON(http.StatusOK, touka.H{
"category": category,
"id": articleID,
})
})
```
#### 2.3. 通配符路由 (Catch-all)
使用星号 `*` 来定义通配符路由,它会捕获该点之后的所有路径段。**通配符路由必须位于路径的末尾**。
```go
// 匹配如 /static/js/main.js, /static/css/style.css 等
r.GET("/static/*filepath", func(c *touka.Context) {
// 捕获的路径可以通过参数名 "filepath" 获取
filePath := c.Param("filepath")
c.String(http.StatusOK, "请求的文件路径是: %s", filePath)
})
```
#### 2.4. 路由组 (RouterGroup)
路由组是组织和管理路由的强大工具,特别适用于构建结构化的 API。
```go
func main() {
r := touka.New()
// 所有 /api/v1 下的路由都需要经过 AuthMiddleware
v1 := r.Group("/api/v1")
v1.Use(AuthMiddleware()) // 应用组级别的中间件
{
// 匹配 /api/v1/products
v1.GET("/products", getProducts)
// 匹配 /api/v1/products/:id
v1.GET("/products/:id", getProductByID)
// 可以在组内再嵌套组
ordersGroup := v1.Group("/orders")
ordersGroup.Use(OrderPermissionsMiddleware()) // 更具体的中间件
{
// 匹配 /api/v1/orders
ordersGroup.GET("", getOrders)
// 匹配 /api/v1/orders/:id
ordersGroup.GET("/:id", getOrderByID)
}
}
r.Run(":8080")
}
func AuthMiddleware() touka.HandlerFunc {
return func(c *touka.Context) {
// 模拟认证逻辑
fmt.Println("V1 Auth Middleware: Checking credentials...")
c.Next()
}
}
// ... 其他处理器
```
---
### 3. 上下文 (Context):请求的灵魂
`touka.Context` 是框架中最为核心的结构,它作为每个 HTTP 请求的上下文,在中间件和最终处理器之间流转。它提供了海量的便捷 API 来简化开发。
#### 3.1. 请求数据解析
##### 获取查询参数
```go
// 请求 URL: /search?q=touka&lang=go&page=1
r.GET("/search", func(c *touka.Context) {
// c.Query() 获取指定参数,不存在则返回空字符串
query := c.Query("q") // "touka"
// c.DefaultQuery() 获取参数,不存在则返回指定的默认值
lang := c.DefaultQuery("lang", "en") // "go"
category := c.DefaultQuery("cat", "all") // "all"
c.JSON(http.StatusOK, touka.H{
"query": query,
"language": lang,
"category": category,
})
})
```
##### 获取 POST 表单数据
```go
// 使用 curl 测试:
// curl -X POST http://localhost:8080/register -d "username=test&email=test@example.com"
r.POST("/register", func(c *touka.Context) {
username := c.PostForm("username")
email := c.DefaultPostForm("email", "anonymous@example.com")
// 也可以获取所有表单数据
// form, _ := c.Request.MultipartForm()
c.String(http.StatusOK, "注册成功: 用户名=%s, 邮箱=%s", username, email)
})
```
##### JSON 数据绑定
Touka 可以轻松地将请求体中的 JSON 数据绑定到 Go 结构体。
```go
type UserProfile struct {
Name string `json:"name" binding:"required"`
Age int `json:"age" binding:"gte=18"`
Tags []string `json:"tags"`
Address string `json:"address,omitempty"`
}
// 使用 curl 测试:
// curl -X POST http://localhost:8080/profile -H "Content-Type: application/json" -d '''
// {
// "name": "Alice",
// "age": 25,
// "tags": ["go", "web"]
// }
// '''
r.POST("/profile", func(c *touka.Context) {
var profile UserProfile
// c.ShouldBindJSON() 会解析 JSON 并填充到结构体中
if err := c.ShouldBindJSON(&profile); err != nil {
// 如果 JSON 格式错误或不满足绑定标签,会返回错误
c.JSON(http.StatusBadRequest, touka.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, touka.H{
"status": "success",
"profile": profile,
})
})
```
#### 3.2. 响应构建
##### 发送 JSON, String, Text
```go
r.GET("/responses", func(c *touka.Context) {
// c.JSON(http.StatusOK, touka.H{"framework": "Touka"})
// c.String(http.StatusOK, "Hello, %s", "World")
c.Text(http.StatusOK, "This is plain text.")
})
```
##### 渲染 HTML 模板
首先,需要为引擎配置一个模板渲染器。
```go
// main.go
import "html/template"
func main() {
r := touka.New()
// 加载模板文件
r.HTMLRender = template.Must(template.ParseGlob("templates/*.html"))
r.GET("/index", func(c *touka.Context) {
// 渲染 index.html 模板,并传入数据
c.HTML(http.StatusOK, "index.html", touka.H{
"title": "Touka 模板渲染",
"user": "Guest",
})
})
r.Run(":8080")
}
// templates/index.html
// <h1>{{ .title }}</h1>
// <p>Welcome, {{ .user }}!</p>
```
##### 文件和流式响应
```go
// 直接发送一个文件
r.GET("/download/report", func(c *touka.Context) {
// 浏览器会提示下载
c.File("./reports/latest.pdf")
})
// 将文件内容作为响应体
r.GET("/show/config", func(c *touka.Context) {
// 浏览器会直接显示文件内容(如果支持)
c.SetRespBodyFile(http.StatusOK, "./config.yaml")
})
// 流式响应,适用于大文件或实时数据
r.GET("/stream", func(c *touka.Context) {
// 假设 getRealTimeDataStream() 返回一个 io.Reader
// dataStream := getRealTimeDataStream()
// c.WriteStream(dataStream)
})
```
#### 3.3. Cookie 操作
Touka 提供了简单的 API 来管理 Cookie。
```go
r.GET("/login", func(c *touka.Context) {
// 设置一个有效期为 1 小时的 cookie
c.SetCookie("session_id", "user-12345", 3600, "/", "localhost", false, true)
c.String(http.StatusOK, "登录成功!")
})
r.GET("/me", func(c *touka.Context) {
sessionID, err := c.GetCookie("session_id")
if err != nil {
c.String(http.StatusUnauthorized, "请先登录")
return
}
c.String(http.StatusOK, "您的会话 ID 是: %s", sessionID)
})
r.GET("/logout", func(c *touka.Context) {
// 通过将 MaxAge 设置为 -1 来删除 cookie
c.DeleteCookie("session_id")
c.String(http.StatusOK, "已退出登录")
})
```
#### 3.4. 中间件数据传递
使用 `c.Set()``c.Get()` 可以在处理链中传递数据。
```go
// 中间件:生成并设置请求 ID
func RequestIDMiddleware() touka.HandlerFunc {
return func(c *touka.Context) {
requestID := fmt.Sprintf("req-%d", time.Now().UnixNano())
c.Set("RequestID", requestID)
c.Next()
}
}
func main() {
r := touka.New()
r.Use(RequestIDMiddleware())
r.GET("/status", func(c *touka.Context) {
// 在处理器中获取由中间件设置的数据
// c.MustGet() 在 key 不存在时会 panic适用于确定存在的场景
requestID := c.MustGet("RequestID").(string)
// 或者使用安全的 Get
// requestID, exists := c.GetString("RequestID")
c.JSON(http.StatusOK, touka.H{"status": "ok", "request_id": requestID})
})
r.Run(":8080")
}
```
#### 3.5. 集成的工具
##### 日志记录
Touka 集成了 `reco` 日志库,可以直接在 `Context` 中使用。
```go
r.GET("/log-test", func(c *touka.Context) {
userID := "user-abc"
c.Infof("用户 %s 访问了 /log-test", userID)
err := errors.New("一个模拟的错误")
if err != nil {
c.Errorf("处理请求时发生错误: %v, 用户: %s", err, userID)
}
c.String(http.StatusOK, "日志已记录")
})
```
##### HTTP 客户端
Touka 集成了 `httpc` 客户端,方便发起出站请求。
```go
r.GET("/fetch-data", func(c *touka.Context) {
// 使用 Context 携带的 httpc 客户端
resp, err := c.GetHTTPC().Get("https://api.github.com/users/WJQSERVER-STUDIO", httpc.WithTimeout(5*time.Second))
if err != nil {
c.ErrorUseHandle(http.StatusInternalServerError, err)
return
}
defer resp.Body.Close()
// 将外部响应直接流式传输给客户端
c.SetHeader("Content-Type", resp.Header.Get("Content-Type"))
c.WriteStream(resp.Body)
})
```
---
### 4. 错误处理:统一且强大
Touka 的一个标志性特性是其统一的错误处理机制。
#### 4.1. 自定义全局错误处理器
```go
func main() {
r := touka.New()
// 设置一个自定义的全局错误处理器
r.SetErrorHandler(func(c *touka.Context, code int, err error) {
// 检查是否是客户端断开连接
if errors.Is(err, context.Canceled) {
return // 不做任何事
}
// 记录详细错误
c.GetLogger().Errorf("捕获到错误: code=%d, err=%v, path=%s", code, err, c.Request.URL.Path)
// 根据错误码返回不同的响应
switch code {
case http.StatusNotFound:
c.JSON(code, touka.H{"error": "您要找的页面去火星了"})
case http.StatusMethodNotAllowed:
c.JSON(code, touka.H{"error": "不支持的请求方法"})
default:
c.JSON(code, touka.H{"error": "服务器内部错误"})
}
})
// 这个路由不存在,会触发 404
// r.GET("/this-route-does-not-exist", ...)
// 静态文件服务,如果文件不存在,也会被上面的 ErrorHandler 捕获
r.StaticDir("/files", "./non-existent-dir")
r.Run(":8080")
}
```
#### 4.2. `errorCapturingResponseWriter` 的魔力
Touka 如何捕获 `http.FileServer` 的错误?答案是 `errorCapturingResponseWriter`
当您使用 `r.StaticDir` 或类似方法时Touka 不会直接将 `http.FileServer` 作为处理器。相反,它会用一个自定义的 `ResponseWriter` 实现(即 `ecw`)来包装原始的 `ResponseWriter`,然后才调用 `http.FileServer.ServeHTTP`
这个包装器会:
1. **拦截 `WriteHeader(statusCode)` 调用:**`http.FileServer` 内部决定要写入一个例如 `404 Not Found` 的状态码时,`ecw` 会捕获这个 `statusCode`
2. **判断是否为错误:** 如果 `statusCode >= 400``ecw` 会将此视为一个错误信号。
3. **阻止原始响应:** `ecw` 会阻止 `http.FileServer` 继续向客户端写入任何内容(包括响应体)。
4. **调用全局 `ErrorHandler`** 最后,`ecw` 会调用您通过 `r.SetErrorHandler` 设置的全局错误处理器,并将捕获到的 `statusCode` 和一个通用错误传递给它。
这个机制确保了无论是动态 API 的错误还是静态文件服务的错误,都能被统一、优雅地处理,从而提供一致的用户体验。
---
### 5. 静态文件服务与嵌入式资源
#### 5.1. 服务本地文件
```go
// 将 URL /assets/ 映射到本地的 ./static 目录
r.StaticDir("/assets", "./static")
// 将 URL /favicon.ico 映射到本地的 ./static/img/favicon.ico 文件
r.StaticFile("/favicon.ico", "./static/img/favicon.ico")
```
#### 5.2. 服务嵌入式资源 (Go 1.16+)
使用 `go:embed` 可以将静态资源直接编译到二进制文件中,实现真正的单体应用部署。
```go
// main.go
package main
import (
"embed"
"io/fs"
"net/http"
"github.com/infinite-iroha/touka"
)
//go:embed frontend/dist
var embeddedFS embed.FS
func main() {
r := touka.New()
// 创建一个子文件系统,根目录为 embeddedFS 中的 frontend/dist
subFS, err := fs.Sub(embeddedFS, "frontend/dist")
if err != nil {
panic(err)
}
// 使用 StaticFS 来服务这个嵌入式文件系统
// 所有对 / 的访问都会映射到嵌入的 frontend/dist 目录
r.StaticFS("/", http.FS(subFS))
r.Run(":8080")
}
```
---
### 6. 与标准库的无缝集成
Touka 提供了适配器,可以轻松使用任何实现了标准 `http.Handler``http.HandlerFunc` 接口的组件。
```go
import "net/http/pprof"
// 适配一个标准的 http.HandlerFunc
r.GET("/legacy-handler", touka.AdapterStdFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("这是一个标准的 http.HandlerFunc"))
}))
// 适配一个标准的 http.Handler例如 pprof
debugGroup := r.Group("/debug/pprof")
{
debugGroup.GET("/", touka.AdapterStdFunc(pprof.Index))
debugGroup.GET("/cmdline", touka.AdapterStdFunc(pprof.Cmdline))
debugGroup.GET("/profile", touka.AdapterStdFunc(pprof.Profile))
// ... 其他 pprof 路由
}
```
这使得您可以方便地利用 Go 生态中大量现有的、遵循标准接口的第三方中间件和工具。

56
adapter.go Normal file
View file

@ -0,0 +1,56 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright 2024 WJQSERVER. All rights reserved.
// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization.
package touka
import (
"net/http"
)
// AdapterStdFunc 将一个标准的 http.HandlerFunc (func(http.ResponseWriter, *http.Request))
// 适配成一个 Touka 框架的 HandlerFunc (func(*Context))
// 这使得标准的 HTTP 处理器可以轻松地在 Touka 路由中使用
//
// 示例:
//
// stdHandlerFunc := func(w http.ResponseWriter, r *http.Request) {
// w.Write([]byte("Hello from a standard handler function!"))
// }
// r.GET("/std-func", touka.AdapterStdFunc(stdHandlerFunc))
//
// 注意: 被适配的处理器执行完毕后Touka 的处理链会被中止 (c.Abort())
// 因为我们假设标准处理器已经完成了对请求的响应
func AdapterStdFunc(f http.HandlerFunc) HandlerFunc {
return func(c *Context) {
// 从 Touka Context 中提取标准的 ResponseWriter 和 Request
// 并将它们传递给原始的 http.HandlerFunc
f(c.Writer, c.Request)
// 中止 Touka 的处理链,防止执行后续的处理器
c.Abort()
}
}
// AdapterStdHandle 将一个实现了 http.Handler 接口的对象
// 适配成一个 Touka 框架的 HandlerFunc (func(*Context))
// 这使得像 http.FileServer, http.StripPrefix 或其他第三方库的 Handler
// 可以直接在 Touka 路由中使用
//
// 示例:
//
// // 创建一个 http.FileServer
// fileServer := http.FileServer(http.Dir("./static"))
// // 将 FileServer 适配后用于 Touka 路由
// r.GET("/static/*filepath", touka.AdapterStdHandle(http.StripPrefix("/static", fileServer)))
//
// 注意: 被适配的处理器执行完毕后Touka 的处理链会被中止 (c.Abort())
func AdapterStdHandle(h http.Handler) HandlerFunc {
return func(c *Context) {
// 调用 Handler 接口的 ServeHTTP 方法
h.ServeHTTP(c.Writer, c.Request)
// 中止 Touka 的处理链
c.Abort()
}
}

File diff suppressed because it is too large Load diff

6
ecw.go
View file

@ -1,3 +1,7 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright 2024 WJQSERVER. All rights reserved.
// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization.
package touka
import (
@ -136,7 +140,7 @@ func (ecw *errorCapturingResponseWriter) processAfterFileServer() {
ecw.ctx.Next()
} else {
// 调用用户自定义的 ErrorHandlerFunc, 由它负责完整的错误响应
ecw.errorHandlerFunc(ecw.ctx, ecw.Status())
ecw.errorHandlerFunc(ecw.ctx, ecw.Status(), errors.New("file server error"))
ecw.ctx.Abort()
}
}

705
engine.go

File diff suppressed because it is too large Load diff

286
fileserver.go Normal file
View file

@ -0,0 +1,286 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright 2024 WJQSERVER. All rights reserved.
// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization.
package touka
import (
"errors"
"net/http"
"path"
"strings"
)
// === FileServer相关 ===
var allowedFileServerMethods = map[string]struct{}{
http.MethodGet: {},
http.MethodHead: {},
}
var (
ErrInputFSisNil = errors.New("input FS is nil")
ErrMethodNotAllowed = errors.New("method not allowed")
)
// FileServer方式, 返回一个HandleFunc, 统一化处理
func FileServer(fs http.FileSystem) HandlerFunc {
if fs == nil {
return func(c *Context) {
c.ErrorUseHandle(http.StatusInternalServerError, ErrInputFSisNil)
}
}
fileServerInstance := http.FileServer(fs)
return func(c *Context) {
FileServerHandleServe(c, fileServerInstance)
// 中止处理链,因为 FileServer 已经处理了响应
c.Abort()
}
}
func FileServerHandleServe(c *Context, fsHandle http.Handler) {
if fsHandle == nil {
c.AddError(ErrInputFSisNil)
// 500
c.ErrorUseHandle(http.StatusInternalServerError, ErrInputFSisNil)
return
}
// 检查是否是 GET 或 HEAD 方法
if _, ok := allowedFileServerMethods[c.Request.Method]; !ok {
// 如果不是,且启用了 MethodNotAllowed 处理,则继续到 MethodNotAllowed 中间件
if c.engine.HandleMethodNotAllowed {
c.Next()
} else {
if c.engine.noRoute == nil {
if c.Request.Method == http.MethodOptions {
//返回allow get
c.Writer.Header().Set("Allow", "GET, HEAD")
c.Status(http.StatusOK)
c.Abort()
return
} else {
// 否则,返回 405 Method Not Allowed
c.engine.errorHandle.handler(c, http.StatusMethodNotAllowed, ErrMethodNotAllowed)
}
} else {
c.Next()
}
}
return
}
// 使用自定义的 ResponseWriter 包装器来捕获 FileServer 可能返回的错误状态码
ecw := AcquireErrorCapturingResponseWriter(c)
defer ReleaseErrorCapturingResponseWriter(ecw)
// 调用 http.FileServer 处理请求
fsHandle.ServeHTTP(ecw, c.Request)
// 在 FileServer 处理完成后,检查是否捕获到错误状态码,并调用 ErrorHandler
ecw.processAfterFileServer()
}
// StaticDir 传入一个文件夹路径, 使用FileServer进行处理
// r.StaticDir("/test/*filepath", "/var/www/test")
func (engine *Engine) StaticDir(relativePath, rootPath string) {
// 清理路径
relativePath = path.Clean(relativePath)
rootPath = path.Clean(rootPath)
// 确保相对路径以 '/' 结尾,以便 FileServer 正确处理子路径
if !strings.HasSuffix(relativePath, "/") {
relativePath += "/"
}
// 创建一个文件系统处理器
fileServer := http.FileServer(http.Dir(rootPath))
// 注册一个捕获所有路径的路由,使用自定义处理器
// 注意:这里使用 ANY 方法,但 FileServer 通常只处理 GET 和 HEAD
// 我们可以通过在处理函数内部检查方法来限制
engine.ANY(relativePath+"*filepath", GetStaticDirHandleFunc(fileServer))
}
// Group的StaticDir方式
func (group *RouterGroup) StaticDir(relativePath, rootPath string) {
// 清理路径
relativePath = path.Clean(relativePath)
rootPath = path.Clean(rootPath)
// 确保相对路径以 '/' 结尾,以便 FileServer 正确处理子路径
if !strings.HasSuffix(relativePath, "/") {
relativePath += "/"
}
// 创建一个文件系统处理器
fileServer := http.FileServer(http.Dir(rootPath))
// 注册一个捕获所有路径的路由,使用自定义处理器
// 注意:这里使用 ANY 方法,但 FileServer 通常只处理 GET 和 HEAD
// 我们可以通过在处理函数内部检查方法来限制
group.ANY(relativePath+"*filepath", GetStaticDirHandleFunc(fileServer))
}
// GetStaticDirHandleFunc
func (engine *Engine) GetStaticDirHandle(rootPath string) HandlerFunc {
// 清理路径
rootPath = path.Clean(rootPath)
// 创建一个文件系统处理器
fileServer := http.FileServer(http.Dir(rootPath))
return GetStaticDirHandleFunc(fileServer)
}
// GetStaticDirHandleFunc
func (group *RouterGroup) GetStaticDirHandle(rootPath string) HandlerFunc { // 清理路径
return group.engine.GetStaticDirHandle(rootPath)
}
// GetStaticDirHandle
func GetStaticDirHandleFunc(fsHandle http.Handler) HandlerFunc {
return func(c *Context) {
requestPath := c.Request.URL.Path
// 获取捕获到的文件路径参数
filepath := c.Param("filepath")
// 构造文件服务器需要处理的请求路径
c.Request.URL.Path = filepath
FileServerHandleServe(c, fsHandle)
// 恢复原始请求路径,以便后续中间件或日志记录使用
c.Request.URL.Path = requestPath
// 中止处理链,因为 FileServer 已经处理了响应
c.Abort()
}
}
// Static File 传入一个文件路径, 使用FileServer进行处理
func (engine *Engine) StaticFile(relativePath, filePath string) {
// 清理路径
relativePath = path.Clean(relativePath)
filePath = path.Clean(filePath)
FileHandle := engine.GetStaticFileHandle(filePath)
// 注册一个精确匹配的路由
engine.GET(relativePath, FileHandle)
engine.HEAD(relativePath, FileHandle)
engine.OPTIONS(relativePath, FileHandle)
}
// Group的StaticFile
func (group *RouterGroup) StaticFile(relativePath, filePath string) {
// 清理路径
relativePath = path.Clean(relativePath)
filePath = path.Clean(filePath)
FileHandle := group.GetStaticFileHandle(filePath)
// 注册一个精确匹配的路由
group.GET(relativePath, FileHandle)
group.HEAD(relativePath, FileHandle)
group.OPTIONS(relativePath, FileHandle)
}
// GetStaticFileHandleFunc
func (engine *Engine) GetStaticFileHandle(filePath string) HandlerFunc {
// 清理路径
filePath = path.Clean(filePath)
// 创建一个文件系统处理器,指向包含目标文件的目录
dir := path.Dir(filePath)
fileName := path.Base(filePath)
fileServer := http.FileServer(http.Dir(dir))
return GetStaticFileHandleFunc(fileServer, fileName)
}
// GetStaticFileHandleFunc
func (group *RouterGroup) GetStaticFileHandle(filePath string) HandlerFunc {
// 清理路径
filePath = path.Clean(filePath)
// 创建一个文件系统处理器,指向包含目标文件的目录
dir := path.Dir(filePath)
fileName := path.Base(filePath)
fileServer := http.FileServer(http.Dir(dir))
return GetStaticFileHandleFunc(fileServer, fileName)
}
// GetStaticFileHandleFunc
func GetStaticFileHandleFunc(fsHandle http.Handler, fileName string) HandlerFunc {
return func(c *Context) {
requestPath := c.Request.URL.Path
// 构造文件服务器需要处理的请求路径
c.Request.URL.Path = "/" + fileName
FileServerHandleServe(c, fsHandle)
// 恢复原始请求路径
c.Request.URL.Path = requestPath
// 中止处理链,因为 FileServer 已经处理了响应
c.Abort()
}
}
// StaticFS
func (engine *Engine) StaticFS(relativePath string, fs http.FileSystem) {
// 清理路径
relativePath = path.Clean(relativePath)
// 确保相对路径以 '/' 结尾,以便 FileServer 正确处理子路径
if !strings.HasSuffix(relativePath, "/") {
relativePath += "/"
}
fileServer := http.StripPrefix(relativePath, http.FileServer(fs))
engine.ANY(relativePath+"*filepath", GetStaticFSHandleFunc(fileServer))
}
// Group的StaticFS
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) {
// 清理路径
relativePath = path.Clean(relativePath)
// 确保相对路径以 '/' 结尾,以便 FileServer 正确处理子路径
if !strings.HasSuffix(relativePath, "/") {
relativePath += "/"
}
fileServer := http.StripPrefix(relativePath, http.FileServer(fs))
group.ANY(relativePath+"*filepath", GetStaticFSHandleFunc(fileServer))
}
// GetStaticFSHandleFunc
func GetStaticFSHandleFunc(fsHandle http.Handler) HandlerFunc {
return func(c *Context) {
FileServerHandleServe(c, fsHandle)
// 中止处理链,因为 FileServer 已经处理了响应
c.Abort()
}
}
// GetStaticFSHandleFunc
func (engine *Engine) GetStaticFSHandle(fs http.FileSystem) HandlerFunc {
fileServer := http.FileServer(fs)
return GetStaticFSHandleFunc(fileServer)
}
// GetStaticFSHandleFunc
func (group *RouterGroup) GetStaticFSHandle(fs http.FileSystem) HandlerFunc {
fileServer := http.FileServer(fs)
return GetStaticFSHandleFunc(fileServer)
}

17
go.mod
View file

@ -1,13 +1,16 @@
module github.com/infinite-iroha/touka
go 1.24.3
go 1.25.1
require (
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4
github.com/WJQSERVER-STUDIO/httpc v0.5.1
github.com/fenthope/reco v0.0.1
github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8
github.com/gorilla/websocket v1.5.3
github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2
github.com/WJQSERVER-STUDIO/httpc v0.8.2
github.com/WJQSERVER/wanf v0.0.3
github.com/fenthope/reco v0.0.4
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e
)
require github.com/valyala/bytebufferpool v1.0.0 // indirect
require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/net v0.49.0 // indirect
)

22
go.sum
View file

@ -1,12 +1,14 @@
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKUGPOAijN1sMtEYoFg=
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc=
github.com/WJQSERVER-STUDIO/httpc v0.5.1 h1:+TKCPYBuj7PAHuiduGCGAqsHAa4QtsUfoVwRN777q64=
github.com/WJQSERVER-STUDIO/httpc v0.5.1/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE=
github.com/fenthope/reco v0.0.1 h1:GYcuXCEKYoctD0dFkiBC+t0RMTOyOiujBCin8bbLR3Y=
github.com/fenthope/reco v0.0.1/go.mod h1:mDkGLHte5udWTIcjQTxrABRcf56SSdxBOCLgrRDwI/Y=
github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 h1:o8UqXPI6SVwQt04RGsqKp3qqmbOfTNMqDrWsc4O47kk=
github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2 h1:AiIHXP21LpK7pFfqUlUstgQEWzjbekZgxOuvVwiMfyM=
github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2/go.mod h1:mCLqYU32bTmEE6dpj37MKKiZgz70Jh/xyK9vVbq6pok=
github.com/WJQSERVER-STUDIO/httpc v0.8.2 h1:PFPLodV0QAfGEP6915J57vIqoKu9cGuuiXG/7C9TNUk=
github.com/WJQSERVER-STUDIO/httpc v0.8.2/go.mod h1:8WhHVRO+olDFBSvL5PC/bdMkb6U3vRdPJ4p4pnguV5Y=
github.com/WJQSERVER/wanf v0.0.3 h1:OqhG7ETiR5Knqr0lmbb+iUMw9O7re2vEogjVf06QevM=
github.com/WJQSERVER/wanf v0.0.3/go.mod h1:q2Pyg+G+s1acMWxrbI4CwS/Yk76/BzLREEdZ8iFwUNE=
github.com/fenthope/reco v0.0.4 h1:yo2g3aWwdoMpaZWZX4SdZOW7mCK82RQIU/YI8ZUQThM=
github.com/fenthope/reco v0.0.4/go.mod h1:eMyS8HpdMVdJ/2WJt6Cvt8P1EH9Igzj5lSJrgc+0jeg=
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=

View file

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2013, Julien Schmidt
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -1,8 +1,13 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright 2024 WJQSERVER. All rights reserved.
// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization.
package touka
import (
"log"
"os"
"time"
"github.com/fenthope/reco"
)
@ -11,6 +16,7 @@ import (
var defaultLogRecoConfig = reco.Config{
Level: reco.LevelInfo,
Mode: reco.ModeText,
TimeFormat: time.RFC3339,
Output: os.Stdout,
Async: true,
DefaultFields: nil,
@ -32,3 +38,9 @@ func CloseLogger(logger *reco.Logger) {
return
}
}
func (engine *Engine) CloseLogger() {
if engine.LogReco != nil {
CloseLogger(engine.LogReco)
}
}

96
maxreader.go Normal file
View file

@ -0,0 +1,96 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright 2024 WJQSERVER. All rights reserved.
// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization.
package touka
import (
"fmt"
"io"
"sync/atomic"
)
// ErrBodyTooLarge 是当读取的字节数超过 MaxBytesReader 设置的限制时返回的错误.
// 将其定义为可导出的变量, 方便调用方使用 errors.Is 进行判断.
var ErrBodyTooLarge = fmt.Errorf("body too large")
// maxBytesReader 是一个实现了 io.ReadCloser 接口的结构体.
// 它包装了另一个 io.ReadCloser, 并限制了从其中读取的最大字节数.
type maxBytesReader struct {
// r 是底层的 io.ReadCloser.
r io.ReadCloser
// n 是允许读取的最大字节数.
n int64
// read 是一个原子计数器, 用于安全地在多个 goroutine 之间跟踪已读取的字节数.
read atomic.Int64
}
// NewMaxBytesReader 创建并返回一个 io.ReadCloser, 它从 r 读取数据,
// 但在读取的字节数超过 n 后会返回 ErrBodyTooLarge 错误.
//
// 如果 r 为 nil, 会 panic.
// 如果 n 小于 0, 则读取不受限制, 直接返回原始的 r.
func NewMaxBytesReader(r io.ReadCloser, n int64) io.ReadCloser {
if r == nil {
panic("NewMaxBytesReader called with a nil reader")
}
// 如果限制为负数, 意味着不限制, 直接返回原始的 ReadCloser.
if n < 0 {
return r
}
return &maxBytesReader{
r: r,
n: n,
}
}
// Read 方法从底层的 ReadCloser 读取数据, 同时检查是否超过了字节限制.
func (mbr *maxBytesReader) Read(p []byte) (int, error) {
// 在函数开始时只加载一次原子变量, 减少后续的原子操作开销.
readSoFar := mbr.read.Load()
// 快速失败路径: 如果在读取之前就已经达到了限制, 立即返回错误.
if readSoFar >= mbr.n {
return 0, ErrBodyTooLarge
}
// 计算当前还可以读取多少字节.
remaining := mbr.n - readSoFar
// 如果请求读取的长度大于剩余可读长度, 我们需要限制本次读取的长度.
// 这样可以保证即使 p 很大, 我们也只读取到恰好达到 maxBytes 的字节数.
if int64(len(p)) > remaining {
p = p[:remaining]
}
// 从底层 Reader 读取数据.
n, err := mbr.r.Read(p)
// 如果实际读取到了数据, 更新原子计数器.
if n > 0 {
readSoFar = mbr.read.Add(int64(n))
}
// 如果底层 Read 返回错误 (例如 io.EOF).
if err != nil {
// 如果是 EOF, 并且我们还没有读满 n 个字节, 这是一个正常的结束.
// 如果已经读满了 n 个字节, 即使是 EOF, 也可以认为成功了.
return n, err
}
// 读后检查: 如果这次读取使得总字节数超过了限制, 返回超限错误.
// 这是处理"跨越"限制情况的关键.
if readSoFar > mbr.n {
// 返回实际读取的字节数 n, 并附上超限错误.
// 上层调用者知道已经有 n 字节被读入了缓冲区 p, 但流已因超限而关闭.
return n, ErrBodyTooLarge
}
// 一切正常, 返回读取的字节数和 nil 错误.
return n, nil
}
// Close 方法关闭底层的 ReadCloser, 保证资源释放.
func (mbr *maxBytesReader) Close() error {
return mbr.r.Close()
}

122
mergectx.go Normal file
View file

@ -0,0 +1,122 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright 2024 WJQSERVER. All rights reserved.
// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization.
package touka
import (
"context"
"sync"
"time"
)
// mergedContext 实现了 context.Context 接口, 是 Merge 函数返回的实际类型.
type mergedContext struct {
// 嵌入一个基础 context, 它持有最早的 deadline 和取消信号.
context.Context
// 保存了所有的父 context, 用于 Value() 方法的查找.
parents []context.Context
// 用于手动取消此 mergedContext 的函数.
cancel context.CancelFunc
}
// MergeCtx 创建并返回一个新的 context.Context.
// 这个新的 context 会在任何一个传入的父 contexts 被取消时, 或者当返回的 CancelFunc 被调用时,
// 自动被取消 (逻辑或关系).
//
// 新的 context 会继承:
// - Deadline: 所有父 context 中最早的截止时间.
// - Value: 按传入顺序从第一个能找到值的父 context 中获取值.
func MergeCtx(parents ...context.Context) (ctx context.Context, cancel context.CancelFunc) {
if len(parents) == 0 {
return context.WithCancel(context.Background())
}
if len(parents) == 1 {
return context.WithCancel(parents[0])
}
var earliestDeadline time.Time
for _, p := range parents {
if deadline, ok := p.Deadline(); ok {
if earliestDeadline.IsZero() || deadline.Before(earliestDeadline) {
earliestDeadline = deadline
}
}
}
var baseCtx context.Context
var baseCancel context.CancelFunc
if !earliestDeadline.IsZero() {
baseCtx, baseCancel = context.WithDeadline(context.Background(), earliestDeadline)
} else {
baseCtx, baseCancel = context.WithCancel(context.Background())
}
mc := &mergedContext{
Context: baseCtx,
parents: parents,
cancel: baseCancel,
}
// 启动一个监控 goroutine.
go func() {
defer mc.cancel()
// orDone 会返回一个 channel, 当任何一个父 context 被取消时, 这个 channel 就会关闭.
// 同时监听 baseCtx.Done() 以便支持手动取消.
select {
case <-orDone(mc.parents...):
case <-mc.Context.Done():
}
}()
return mc, mc.cancel
}
// Value 返回当前Ctx Value
func (mc *mergedContext) Value(key any) any {
return mc.Context.Value(key)
}
// Deadline 实现了 context.Context 的 Deadline 方法.
func (mc *mergedContext) Deadline() (deadline time.Time, ok bool) {
return mc.Context.Deadline()
}
// Done 实现了 context.Context 的 Done 方法.
func (mc *mergedContext) Done() <-chan struct{} {
return mc.Context.Done()
}
// Err 实现了 context.Context 的 Err 方法.
func (mc *mergedContext) Err() error {
return mc.Context.Err()
}
// orDone 是一个辅助函数, 返回一个 channel.
// 当任意一个输入 context 的 Done() channel 关闭时, orDone 返回的 channel 也会关闭.
// 这是一个非阻塞的、不会泄漏 goroutine 的实现.
func orDone(contexts ...context.Context) <-chan struct{} {
done := make(chan struct{})
var once sync.Once
closeDone := func() {
once.Do(func() {
close(done)
})
}
// 为每个父 context 启动一个 goroutine.
for _, ctx := range contexts {
go func(c context.Context) {
select {
case <-c.Done():
closeDone()
case <-done:
// orDone 已经被其他 goroutine 关闭了, 当前 goroutine 可以安全退出.
}
}(ctx)
}
return done
}

80
midware_x.go Normal file
View file

@ -0,0 +1,80 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright 2024 WJQSERVER. All rights reserved.
// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization.
package touka
type MiddlewareXFunc func() HandlerFunc
// UseChainIf 是一个条件中间件包装器,用于一组中间件
// 如果 `condition` 为 true它将按顺序创建并执行提供的 `factories` 生成的中间件链
// 否则,它会直接跳过整个链
func (engine *Engine) UseChainIf(condition bool, factories ...MiddlewareXFunc) HandlerFunc {
// 如果条件不满足或没有提供任何工厂函数,返回一个“穿透”中间件
if !condition || len(factories) == 0 {
return func(c *Context) {
c.Next()
}
}
// 在配置路由时就创建好所有中间件实例
middlewares := make(HandlersChain, 0, len(factories))
for _, factory := range factories {
if factory != nil {
middlewares = append(middlewares, factory())
}
}
// 返回一个处理器,该处理器负责执行这个子链
// 这个实现通过临时替换 Context 的处理器链来注入子链,是健壮的
return func(c *Context) {
// 将当前的处理链和索引位置保存下来
originalHandlers := c.handlers
originalIndex := c.index
// 创建一个新的临时处理链
// 它由我们预先创建的 `middlewares` 和一个特殊的“恢复”处理器组成
subChain := make(HandlersChain, len(middlewares)+1)
copy(subChain, middlewares)
// 在子链的末尾添加“恢复”处理器
// 当所有 `middlewares` 都执行完毕并调用了 Next() 后,这个函数会被执行
subChain[len(middlewares)] = func(ctx *Context) {
// 恢复原始的处理链状态
ctx.handlers = originalHandlers
ctx.index = originalIndex
// 继续执行原始处理链中 `UseChainIf` 之后的下一个处理器
ctx.Next()
}
// 将 Context 的处理器链临时替换新的的子链,并重置索引以从头开始
c.handlers = subChain
c.index = -1
c.Next()
}
}
// UseIf 是一个条件中间件包装器
func (engine *Engine) UseIf(condition bool, middlewareX MiddlewareXFunc) HandlerFunc {
if !condition {
return func(c *Context) {
c.Next()
}
}
// 只有当条件为 true 时,才调用工厂函数创建中间件实例
// 注意:这会导致每次请求都创建一个新的中间件实例(如果中间件本身有状态)
// 如果中间件是无状态的,可以进行优化
// 优化:只创建一次
return func(c *Context) {
middleware := middlewareX()
if middleware != nil {
middleware(c)
} else {
c.Next()
}
}
}

53
path.go Normal file
View file

@ -0,0 +1,53 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright 2024 WJQSERVER. All rights reserved.
// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization.
package touka
import (
"path"
"strings"
)
// resolveRoutePath 安全地拼接基础路径和相对路径,并正确处理尾部斜杠。
// 这是一个为高性能路由注册优化的版本。
func resolveRoutePath(basePath, relativePath string) string {
// 如果相对路径为空,直接返回基础路径
if relativePath == "" {
return basePath
}
// 如果基础路径为空,直接返回相对路径(确保以/开头)
if basePath == "" {
return relativePath
}
// 使用 strings.Builder 来高效构建路径,避免多次字符串分配
var b strings.Builder
// 估算一个合理的容量以减少扩容
b.Grow(len(basePath) + len(relativePath) + 1)
b.WriteString(basePath)
// 检查 basePath 是否以斜杠结尾
if basePath[len(basePath)-1] != '/' {
b.WriteByte('/') // 如果没有,则添加
}
// 检查 relativePath 是否以斜杠开头,如果是,则移除
if relativePath[0] == '/' {
b.WriteString(relativePath[1:])
} else {
b.WriteString(relativePath)
}
// path.Clean 仍然是处理 '..' 和 '//' 等复杂情况最可靠的方式。
// 我们可以只在最终结果上调用一次,而不是在拼接过程中。
finalPath := path.Clean(b.String())
// 关键:如果原始 relativePath 有尾部斜杠,但 Clean 把它移除了,我们要加回来。
// 只有当最终路径不是根路径 "/" 时才需要加回。
if strings.HasSuffix(relativePath, "/") && finalPath != "/" {
return finalPath + "/"
}
return finalPath
}

99
path_test.go Normal file
View file

@ -0,0 +1,99 @@
// touka/path_test.go
package touka
import (
"fmt"
"path"
"strings"
"testing"
)
func TestResolveRoutePath(t *testing.T) {
// 定义一组测试用例
testCases := []struct {
basePath string
relativePath string
expected string
}{
// --- 基本情况 ---
{basePath: "/api", relativePath: "/v1", expected: "/api/v1"},
{basePath: "/api/", relativePath: "v1", expected: "/api/v1"},
{basePath: "/api", relativePath: "v1", expected: "/api/v1"},
{basePath: "/api/", relativePath: "/v1", expected: "/api/v1"},
// --- 尾部斜杠处理 ---
{basePath: "/api", relativePath: "/v1/", expected: "/api/v1/"},
{basePath: "/api/", relativePath: "v1/", expected: "/api/v1/"},
{basePath: "", relativePath: "/v1/", expected: "/v1/"},
{basePath: "/", relativePath: "/v1/", expected: "/v1/"},
// --- 根路径和空路径 ---
{basePath: "/", relativePath: "/", expected: "/"},
{basePath: "/api", relativePath: "/", expected: "/api/"},
{basePath: "/api/", relativePath: "/", expected: "/api/"},
{basePath: "/", relativePath: "/users", expected: "/users"},
{basePath: "/users", relativePath: "", expected: "/users"},
{basePath: "", relativePath: "/users", expected: "/users"},
// --- 路径清理测试 (由 path.Clean 处理) ---
{basePath: "/api/v1", relativePath: "../v2", expected: "/api/v2"},
{basePath: "/api/v1/", relativePath: "../v2/", expected: "/api/v2/"},
{basePath: "/api//v1", relativePath: "/users", expected: "/api/v1/users"},
{basePath: "/api/./v1", relativePath: "/users", expected: "/api/v1/users"},
}
for _, tc := range testCases {
// 使用 t.Run 为每个测试用例创建一个子测试,方便定位问题
testName := fmt.Sprintf("base:'%s', rel:'%s'", tc.basePath, tc.relativePath)
t.Run(testName, func(t *testing.T) {
result := resolveRoutePath(tc.basePath, tc.relativePath)
if result != tc.expected {
t.Errorf("resolveRoutePath('%s', '%s') = '%s'; want '%s'",
tc.basePath, tc.relativePath, result, tc.expected)
}
})
}
}
// 性能基准测试,用于观测优化效果
func BenchmarkResolveRoutePath(b *testing.B) {
basePath := "/api/v1/some/long/path"
relativePath := "/users/profile/details/"
// b.N 是由 testing 包提供的循环次数
for i := 0; i < b.N; i++ {
// 在循环内调用被测试的函数
resolveRoutePath(basePath, relativePath)
}
}
// (可选)可以保留旧的实现,进行性能对比
func resolveRoutePath_Old(basePath, relativePath string) string {
if relativePath == "/" {
if basePath != "" && basePath != "/" && !strings.HasSuffix(basePath, "/") {
return basePath + "/"
}
return basePath
}
finalPath := path.Clean(basePath + "/" + relativePath)
if strings.HasSuffix(relativePath, "/") && !strings.HasSuffix(finalPath, "/") {
return finalPath + "/"
}
return finalPath
}
func BenchmarkResolveRoutePath_Old(b *testing.B) {
basePath := "/api/v1/some/long/path"
relativePath := "/users/profile/details/"
for i := 0; i < b.N; i++ {
resolveRoutePath_Old(basePath, relativePath)
}
}
func BenchmarkJoinStd(b *testing.B) {
basePath := "/api/v1/some/long/path"
relativePath := "/users/profile/details/"
for i := 0; i < b.N; i++ {
path.Join(basePath, relativePath)
}
}

View file

@ -1,38 +1,144 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright 2024 WJQSERVER. All rights reserved.
// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization.
package touka
import (
"fmt"
"errors"
"io"
"log"
"net"
"net/http"
"net/http/httputil" // 用于 DumpRequest
"os"
"runtime/debug"
"strings"
)
// Recovery 返回一个 Touka 的 HandlerFunc用于捕获处理链中的 panic。
func Recovery() HandlerFunc {
// PanicHandlerFunc 定义了用户自定义的 panic 处理函数类型
// 它接收当前的 Context 和 panic 的值
type PanicHandlerFunc func(c *Context, panicInfo interface{})
// RecoveryWithOptions 返回一个可配置的 panic 恢复中间件
//
// 参数:
// - handler (PanicHandlerFunc): 一个可选的回调函数 如果提供了,当 panic 发生时,
// 它将被调用,允许用户进行自定义的日志记录、错误上报或响应
// 如果为 nil将使用默认的 panic 处理逻辑
func RecoveryWithOptions(handler PanicHandlerFunc) HandlerFunc {
// 如果未提供 handler则使用默认的 panic 处理器
if handler == nil {
handler = defaultPanicHandler
}
return func(c *Context) {
// 使用 defer 和 recover() 来捕获 panic
defer func() {
if r := recover(); r != nil {
// 记录 panic 信息和堆栈追踪
err := fmt.Errorf("panic occurred: %v", r)
log.Printf("[Recovery] %s\n%s", err, debug.Stack()) // 记录错误和堆栈
// 检查客户端是否已断开连接,如果已断开则不再尝试写入响应
select {
case <-c.Request.Context().Done():
log.Printf("[Recovery] Client disconnected, skipping response for panic: %v", r)
return // 客户端已断开,直接返回
default:
// 客户端未断开,返回 500 Internal Server Error
// 使用统一的错误处理机制
c.engine.errorHandle.handler(c, http.StatusInternalServerError)
// Abort() 确保后续的处理函数不再执行
c.Abort()
}
// 捕获到 panic调用配置的处理器
handler(c, r)
}
}()
c.Next() // 执行后续的处理链
}
}
// 继续执行处理链中的下一个处理函数
c.Next()
// Recovery 返回一个使用默认配置的 panic 恢复中间件
// 它是 RecoveryWithOptions(nil) 的一个便捷包装
func Recovery() HandlerFunc {
return RecoveryWithOptions(nil) // 使用默认处理器
}
// defaultPanicHandler 是默认的 panic 处理逻辑
func defaultPanicHandler(c *Context, r interface{}) {
// 检查连接是否已由客户端关闭
// 常见的错误类型包括 net.OpError (其内部错误可能是 os.SyscallError)
// 以及在 HTTP/2 中可能出现的特定 stream 错误
// isBrokenPipeError 是一个辅助函数,用于检查这些情况
if isBrokenPipeError(r) {
// 如果是客户端断开连接导致的 panic我们不应再尝试写入响应
// 只需要记录一个信息级别的日志,然后中止处理
log.Printf("[Recovery] Client connection closed for request %s %s. Panic: %v. No response sent.",
c.Request.Method, c.Request.URL.Path, r)
c.Abort() // 仅设置中止标志
return
}
// 对于其他类型的 panic我们认为是服务器端内部错误
// 记录详细的错误日志,包括请求信息和堆栈跟踪
// 使用 httputil.DumpRequest 来获取请求的快照,但注意不要读取 Body
httpRequest, _ := httputil.DumpRequest(c.Request, false)
// 隐藏敏感头部信息,例如 Authorization
headers := strings.Split(string(httpRequest), "\r\n")
for idx, header := range headers {
current := strings.SplitN(header, ":", 2)
if len(current) > 1 && strings.EqualFold(current[0], "Authorization") {
headers[idx] = current[0] + ": [REDACTED]" // 替换为脱敏信息
}
}
redactedRequest := strings.Join(headers, "\r\n")
// 使用英文记录日志
log.Printf("[Recovery] Panic recovered:\nPanic: %v\nRequest:\n%s\nStack:\n%s",
r, redactedRequest, string(debug.Stack()))
// 在发送 500 错误响应之前,检查响应是否已经开始写入
// 如果 c.Writer.Written() 返回 true说明响应头已经发送
// 此时再尝试写入状态码或响应体会导致错误或 panic所以应该直接中止
if c.Writer.Written() {
// 使用英文记录日志
log.Println("[Recovery] Response headers already sent. Cannot write 500 error.")
c.Abort()
return
}
// 尝试发送 500 Internal Server Error 响应
// 使用框架提供的统一错误处理器(如果可用)
if c.engine != nil && c.engine.errorHandle.handler != nil {
c.engine.errorHandle.handler(c, http.StatusInternalServerError, errors.New("Internal Panic Error"))
} else {
// 如果框架错误处理器不可用,提供一个备用的简单响应
// 返回英文错误信息
http.Error(c.Writer, "Internal Server Error", http.StatusInternalServerError)
}
// 确保 Touka 的处理链被中止
// errorHandle.handler 通常会调用 Abort但在这里再次调用是安全的
c.Abort()
}
// isBrokenPipeError 检查 recover() 捕获的值是否表示一个由客户端断开连接引起的网络错误
// 这对于防止在已关闭的连接上写入响应至关重要
func isBrokenPipeError(r interface{}) bool {
// 将 recover() 的结果转换为 error 类型
err, ok := r.(error)
if !ok {
return false // 如果 panic 的不是一个 error则不认为是 broken pipe
}
var opErr *net.OpError
// 检查错误链中是否存在 net.OpError
if errors.As(err, &opErr) {
var syscallErr *os.SyscallError
// 检查 net.OpError 的内部错误是否是 os.SyscallError
if errors.As(opErr.Err, &syscallErr) {
// 将系统调用错误转换为小写字符串进行检查
errMsg := strings.ToLower(syscallErr.Error())
// 常见的由客户端断开引起的错误消息
if strings.Contains(errMsg, "broken pipe") || strings.Contains(errMsg, "connection reset by peer") {
return true
}
}
}
// 还需要处理 HTTP/2 中的 stream closed 错误
// 在 Go 1.16+ 中,当写入已关闭的 HTTP/2 流时,可能会返回 io.EOF
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
// 在流式写入的上下文中io.EOF 或 net.ErrClosed 也常常表示连接已关闭
return true
}
if errors.Is(err, http.ErrAbortHandler) {
return true
}
return false
}

View file

@ -1,15 +1,21 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright 2024 WJQSERVER. All rights reserved.
// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization.
package touka
import (
"bufio"
"errors"
"log"
"net"
"net/http"
"runtime/debug"
)
// --- ResponseWriter 包装 ---
// ResponseWriter 接口扩展了 http.ResponseWriter 以提供对响应状态和大小的访问
// ResponseWriter 接口扩展了 http.ResponseWriter 以提供对响应状态和大小的访问
type ResponseWriter interface {
http.ResponseWriter
http.Hijacker // 支持 WebSocket 等
@ -18,26 +24,38 @@ type ResponseWriter interface {
Status() int // 返回写入的 HTTP 状态码,如果未写入则为 0
Size() int // 返回已写入响应体的字节数
Written() bool // 返回 WriteHeader 是否已被调用
IsHijacked() bool
}
// responseWriterImpl 是 ResponseWriter 的具体实现
// responseWriterImpl 是 ResponseWriter 的具体实现
type responseWriterImpl struct {
http.ResponseWriter
size int
status int // 0 表示尚未写入状态码
hijacked bool
}
// NewResponseWriter 创建并返回一个 responseWriterImpl 实例
// NewResponseWriter 创建并返回一个 responseWriterImpl 实例
func newResponseWriter(w http.ResponseWriter) ResponseWriter {
rw := &responseWriterImpl{
return &responseWriterImpl{
ResponseWriter: w,
status: 0, // 明确初始状态
size: 0,
hijacked: false,
}
return rw
}
func (rw *responseWriterImpl) reset(w http.ResponseWriter) {
rw.ResponseWriter = w
rw.status = 0
rw.size = 0
rw.hijacked = false
}
func (rw *responseWriterImpl) WriteHeader(statusCode int) {
if rw.hijacked {
return
}
if rw.status == 0 { // 确保只设置一次
rw.status = statusCode
rw.ResponseWriter.WriteHeader(statusCode)
@ -45,9 +63,12 @@ func (rw *responseWriterImpl) WriteHeader(statusCode int) {
}
func (rw *responseWriterImpl) Write(b []byte) (int, error) {
if rw.hijacked {
return 0, errors.New("http: response already hijacked")
}
if rw.status == 0 {
// 如果 WriteHeader 没被显式调用Go 的 http server 会默认为 200
// 我们在这里也将其标记为 200因为即将写入数据。
// 我们在这里也将其标记为 200因为即将写入数据
rw.status = http.StatusOK
// ResponseWriter.Write 会在第一次写入时自动调用 WriteHeader(http.StatusOK)
// 所以不需要在这里显式调用 rw.ResponseWriter.WriteHeader(http.StatusOK)
@ -69,17 +90,51 @@ func (rw *responseWriterImpl) Written() bool {
return rw.status != 0
}
// Hijack 实现 http.Hijacker 接口
// Hijack 实现 http.Hijacker 接口
func (rw *responseWriterImpl) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := rw.ResponseWriter.(http.Hijacker); ok {
return hj.Hijack()
// 检查是否已劫持
if rw.hijacked {
return nil, nil, errors.New("http: connection already hijacked")
}
// 尝试从底层 ResponseWriter 获取 Hijacker 接口
hj, ok := rw.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, errors.New("http.Hijacker interface not supported")
}
// Flush 实现 http.Flusher 接口。
// 调用底层的 Hijack 方法
conn, brw, err := hj.Hijack()
if err != nil {
// 如果劫持失败,返回错误
return nil, nil, err
}
// 如果劫持成功,更新内部状态
rw.hijacked = true
return conn, brw, nil
}
// Flush 实现 http.Flusher 接口
func (rw *responseWriterImpl) Flush() {
defer func() {
if r := recover(); r != nil {
// 记录捕获到的 panic 信息,这表明底层连接可能已经关闭或失效
// 使用 log.Printf 记录,并包含堆栈信息,便于调试
log.Printf("Recovered from panic during responseWriterImpl.Flush for request: %v\nStack: %s", r, debug.Stack())
// 捕获后,不继续传播 panic允许请求的 goroutine 优雅退出
}
}()
if rw.hijacked {
return
}
if fl, ok := rw.ResponseWriter.(http.Flusher); ok {
fl.Flush()
}
}
// IsHijacked 方法返回连接是否已被劫持
func (rw *responseWriterImpl) IsHijacked() bool {
return rw.hijacked
}

412
serve.go
View file

@ -1,3 +1,7 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright 2024 WJQSERVER. All rights reserved.
// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization.
package touka
import (
@ -17,222 +21,336 @@ import (
"github.com/fenthope/reco"
)
const defaultShutdownTimeout = 5 * time.Second // 定义默认的优雅关闭超时时间
// defaultShutdownTimeout 定义了在强制关闭前等待优雅关闭的最长时间
const defaultShutdownTimeout = 5 * time.Second
// resolveAddress 辅助函数,处理传入的地址参数。
// --- 内部辅助函数 ---
// resolveAddress 解析传入的地址参数,如果没有则返回默认的 ":8080"
func resolveAddress(addr []string) string {
switch len(addr) {
case 0:
return ":8080" // 默认端口
return ":8080"
case 1:
return addr[0]
default:
panic("too many parameters for Run method") // 参数过多则报错
panic("too many parameters provided for server address")
}
}
// Run 启动 HTTP 服务器。
// 接受一个可选的地址参数,如果未提供则默认为 ":8080"。
func (engine *Engine) Run(addr ...string) (err error) {
address := resolveAddress(addr) // 解析服务器地址
log.Printf("Touka server listening on %s\n", address)
err = http.ListenAndServe(address, engine) // 启动 HTTP 服务器
return
}
// getShutdownTimeout 解析可选的超时参数,如果未提供或无效,则返回默认超时。
// getShutdownTimeout 解析可选的超时参数,如果无效或未提供则返回默认值
func getShutdownTimeout(timeouts []time.Duration) time.Duration {
var timeout time.Duration
if len(timeouts) > 0 {
timeout = timeouts[0]
if timeout <= 0 {
log.Printf("Warning: Provided shutdown timeout (%v) is non-positive. Using default timeout %v.\n", timeout, defaultShutdownTimeout)
timeout = defaultShutdownTimeout
if len(timeouts) > 0 && timeouts[0] > 0 {
return timeouts[0]
}
} else {
timeout = defaultShutdownTimeout
}
return timeout
return defaultShutdownTimeout
}
// handleGracefulShutdown 处理一个或多个 http.Server 实例的优雅关闭。
// 它监听操作系统信号,并在指定超时时间内尝试关闭所有服务器。
// runServer 是一个内部辅助函数,负责在一个新的 goroutine 中启动一个 http.Server,
// 并处理其启动失败的致命错误
// serverType 用于在日志中标识服务器类型 (例如 "HTTP", "HTTPS")
func runServer(serverType string, srv *http.Server) {
go func() {
var err error
protocol := "http"
if srv.TLSConfig != nil {
protocol = "https"
}
log.Printf("Touka %s server listening on %s://%s", serverType, protocol, srv.Addr)
if srv.TLSConfig != nil {
// 对于 HTTPS 服务器,如果 srv.TLSConfig.Certificates 已配置,
// ListenAndServeTLS 的前两个参数可以为空字符串
err = srv.ListenAndServeTLS("", "")
} else {
err = srv.ListenAndServe()
}
// 如果服务器停止不是因为被优雅关闭 (http.ErrServerClosed),
// 则认为是一个严重错误,并终止程序
if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("Touka %s server failed: %v", serverType, err)
}
}()
}
// handleGracefulShutdown 监听系统信号 (SIGINT, SIGTERM) 并优雅地关闭所有提供的服务器
// 这是所有支持优雅关闭的 RunXXX 方法的最终归宿
func handleGracefulShutdown(servers []*http.Server, timeout time.Duration, logger *reco.Logger) error {
// 创建一个 channel 来接收操作系统信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 监听中断和终止信号
<-quit // 阻塞,直到接收到上述信号之一
log.Println("Shutting down Touka server(s)...")
// 关闭日志记录器
if logger != nil {
go func() {
log.Println("Touka Logger Clossing...")
log.Println("Closing Touka logger...")
CloseLogger(logger)
}()
}
// 创建一个带超时的上下文,用于 Shutdown
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var wg sync.WaitGroup
var errs []error
var errsMutex sync.Mutex // 保护 errs 切片
errChan := make(chan error, len(servers)) // 用于收集关闭错误的 channel
// 并发地关闭所有服务器
for _, srv := range servers {
srv := srv // capture loop variable
wg.Add(1)
go func() {
go func(s *http.Server) {
defer wg.Done()
if err := srv.Shutdown(ctx); err != nil {
errsMutex.Lock()
if err == context.DeadlineExceeded {
log.Printf("Server %s shutdown timed out after %v.\n", srv.Addr, timeout)
errs = append(errs, fmt.Errorf("server %s shutdown timed out", srv.Addr))
} else {
log.Printf("Server %s forced to shutdown: %v\n", srv.Addr, err)
errs = append(errs, fmt.Errorf("server %s forced to shutdown: %w", srv.Addr, err))
if err := s.Shutdown(ctx); err != nil {
// 将错误发送到 channel
errChan <- fmt.Errorf("server on %s shutdown failed: %w", s.Addr, err)
}
errsMutex.Unlock()
}
}()
}
wg.Wait() // 等待所有服务器的关闭 Goroutine 完成
if len(errs) > 0 {
return errors.Join(errs...) // 返回所有收集到的错误
}(srv)
}
wg.Wait() // 等待所有服务器的关闭 goroutine 完成
close(errChan) // 关闭 channel,以便可以安全地遍历它
// 收集所有关闭过程中发生的错误
var shutdownErrors []error
for err := range errChan {
shutdownErrors = append(shutdownErrors, err)
log.Printf("Shutdown error: %v", err)
}
if len(shutdownErrors) > 0 {
return errors.Join(shutdownErrors...) // Go 1.20+ 的 errors.Join,用于合并多个错误
}
log.Println("Touka server(s) exited gracefully.")
return nil
}
// RunShutdown 启动 HTTP 服务器并支持优雅关闭。
// 它监听操作系统信号 (SIGINT, SIGTERM),并在指定超时时间内优雅地关闭服务器。
// addr: 服务器监听的地址,例如 ":8080"。
// timeouts: 可选的超时时间,如果未提供,则默认为 5 秒。
func (engine *Engine) RunShutdown(addr string, timeouts ...time.Duration) error {
timeout := getShutdownTimeout(timeouts)
func handleGracefulShutdownWithContext(servers []*http.Server, ctx context.Context, timeout time.Duration, logger *reco.Logger) error {
// 创建一个 channel 来接收操作系统信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 监听中断和终止信号
// 启动服务器
serverStopped := make(chan error, 1)
for _, srv := range servers {
go func(s *http.Server) {
serverStopped <- s.ListenAndServe()
}(srv)
}
select {
case <-ctx.Done():
// Context 被取消 (例如,通过外部取消函数)
log.Println("Context cancelled, shutting down Touka server(s)...")
case err := <-serverStopped:
// 服务器自身停止 (例如,端口被占用,或 ListenAndServe 返回错误)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
return fmt.Errorf("Touka HTTP server failed: %w", err)
}
log.Println("Touka HTTP server stopped gracefully.")
return nil // 服务器已自行优雅关闭,无需进一步处理
case <-quit:
// 接收到操作系统信号
log.Println("Shutting down Touka server(s) due to OS signal...")
}
// 关闭日志记录器
if logger != nil {
go func() {
log.Println("Closing Touka logger...")
CloseLogger(logger)
}()
}
// 创建一个带超时的上下文,用于 Shutdown
shutdownCtx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var wg sync.WaitGroup
errChan := make(chan error, len(servers)) // 用于收集关闭错误的 channel
// 并发地关闭所有服务器
for _, srv := range servers {
wg.Add(1)
go func(s *http.Server) {
defer wg.Done()
if err := s.Shutdown(shutdownCtx); err != nil {
// 将错误发送到 channel
errChan <- fmt.Errorf("server on %s shutdown failed: %w", s.Addr, err)
}
}(srv)
}
wg.Wait()
close(errChan) // 关闭 channel,以便可以安全地遍历它
// 收集所有关闭过程中发生的错误
var shutdownErrors []error
for err := range errChan {
shutdownErrors = append(shutdownErrors, err)
log.Printf("Shutdown error: %v", err)
}
if len(shutdownErrors) > 0 {
return errors.Join(shutdownErrors...) // Go 1.20+ 的 errors.Join,用于合并多个错误
}
log.Println("Touka server(s) exited gracefully.")
return nil
}
// --- 公共 Run 方法 ---
// Run 启动一个不支持优雅关闭的 HTTP 服务器
// 这是一个阻塞调用,主要用于简单的场景或快速测试
// 建议在生产环境中使用 RunShutdown 或其他支持优雅关闭的方法
func (engine *Engine) Run(addr ...string) error {
address := resolveAddress(addr)
srv := &http.Server{Addr: address, Handler: engine}
// 即使是不支持优雅关闭的 Run,也应用默认和用户配置,以保持行为一致性
//engine.applyDefaultServerConfig(srv)
if engine.ServerConfigurator != nil {
engine.ServerConfigurator(srv)
}
log.Printf("Starting Touka HTTP server on %s (no graceful shutdown)", address)
return srv.ListenAndServe()
}
// RunShutdown 启动一个支持优雅关闭的 HTTP 服务器
func (engine *Engine) RunShutdown(addr string, timeouts ...time.Duration) error {
srv := &http.Server{
Addr: addr,
Handler: engine, // Engine 实现了 http.Handler 接口
Handler: engine,
}
// 启动服务器在单独的 Goroutine 中运行,以便主 Goroutine 可以监听信号
go func() {
log.Printf("Touka HTTP server listening on %s\n", addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Touka HTTP server listen error: %s\n", err)
}
}()
return handleGracefulShutdown([]*http.Server{srv}, timeout, engine.LogReco)
// 应用框架的默认配置和用户提供的自定义配置
//engine.applyDefaultServerConfig(srv)
if engine.ServerConfigurator != nil {
engine.ServerConfigurator(srv)
}
// RunWithTLS 启动 HTTPS 服务器并支持优雅关闭。
// 用户需自行创建并传入 *tls.Config 实例,以提供完整的 TLS 配置自由度。
// addr: 服务器监听的地址,例如 ":8443"。
// tlsConfig: 包含 TLS 证书、密钥及其他配置的 tls.Config 实例。
// timeouts: 可选的超时时间,如果未提供,则默认为 5 秒。
func (engine *Engine) RunWithTLS(addr string, tlsConfig *tls.Config, timeouts ...time.Duration) error {
runServer("HTTP", srv)
return handleGracefulShutdown([]*http.Server{srv}, getShutdownTimeout(timeouts), engine.LogReco)
}
// RunShutdown 启动一个支持优雅关闭的 HTTP 服务器
func (engine *Engine) RunShutdownWithContext(addr string, ctx context.Context, timeouts ...time.Duration) error {
srv := &http.Server{
Addr: addr,
Handler: engine,
}
// 应用框架的默认配置和用户提供的自定义配置
//engine.applyDefaultServerConfig(srv)
if engine.ServerConfigurator != nil {
engine.ServerConfigurator(srv)
}
return handleGracefulShutdownWithContext([]*http.Server{srv}, ctx, getShutdownTimeout(timeouts), engine.LogReco)
}
// RunTLS 启动一个支持优雅关闭的 HTTPS 服务器
func (engine *Engine) RunTLS(addr string, tlsConfig *tls.Config, timeouts ...time.Duration) error {
if tlsConfig == nil {
return errors.New("tls.Config must not be nil for RunWithTLS")
return errors.New("tls.Config must not be nil for RunTLS")
}
// 配置 HTTP/2 支持 (如果使用默认配置)
if engine.useDefaultProtocols {
engine.SetProtocols(&ProtocolsConfig{
Http1: true,
Http2: true, // 默认在 TLS 上启用 HTTP/2
})
}
timeout := getShutdownTimeout(timeouts)
srv := &http.Server{
Addr: addr,
Handler: engine,
TLSConfig: tlsConfig, // 使用用户传入的 tls.Config
TLSConfig: tlsConfig,
}
if engine.useDefaultProtocols {
//加入HTTP2支持
engine.SetProtocols(&ProtocolsConfig{
Http1: true,
Http2: true, // 默认启用 HTTP/2
Http2_Cleartext: false,
})
// 应用框架的默认配置和用户提供的自定义配置
// 优先使用 TLSServerConfigurator,如果未设置,则回退到通用的 ServerConfigurator
//engine.applyDefaultServerConfig(srv)
if engine.TLSServerConfigurator != nil {
engine.TLSServerConfigurator(srv)
} else if engine.ServerConfigurator != nil {
engine.ServerConfigurator(srv)
}
go func() {
log.Printf("Touka HTTPS server listening on %s\n", addr)
if err := srv.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
log.Fatalf("Touka HTTPS server listen error: %s\n", err)
}
}()
return handleGracefulShutdown([]*http.Server{srv}, timeout, engine.LogReco)
runServer("HTTPS", srv)
return handleGracefulShutdown([]*http.Server{srv}, getShutdownTimeout(timeouts), engine.LogReco)
}
// RunWithTLSRedir 启动 HTTP 和 HTTPS 服务器,并将所有 HTTP 请求重定向到 HTTPS。
// httpAddr: HTTP 服务器监听的地址,例如 ":80"。
// httpsAddr: HTTPS 服务器监听的地址,例如 ":443"。
// tlsConfig: 包含 TLS 证书、密钥及其他配置的 tls.Config 实例,用于 HTTPS 服务器。
// timeouts: 可选的超时时间,如果未提供,则默认为 5 秒。
func (engine *Engine) RunWithTLSRedir(httpAddr, httpsAddr string, tlsConfig *tls.Config, timeouts ...time.Duration) error {
// RunWithTLS 是 RunTLS 的别名,为了保持向后兼容性或更直观的命名
func (engine *Engine) RunWithTLS(addr string, tlsConfig *tls.Config, timeouts ...time.Duration) error {
return engine.RunTLS(addr, tlsConfig, timeouts...)
}
// RunTLSRedir 启动 HTTP 重定向服务器和 HTTPS 应用服务器,两者都支持优雅关闭
func (engine *Engine) RunTLSRedir(httpAddr, httpsAddr string, tlsConfig *tls.Config, timeouts ...time.Duration) error {
if tlsConfig == nil {
return errors.New("tls.Config must not be nil for RunWithTLSRedir")
return errors.New("tls.Config must not be nil for RunTLSRedir")
}
timeout := getShutdownTimeout(timeouts)
// HTTPS Server
// --- HTTPS 服务器 ---
if engine.useDefaultProtocols {
engine.SetProtocols(&ProtocolsConfig{Http1: true, Http2: true})
}
httpsSrv := &http.Server{
Addr: httpsAddr,
Handler: engine,
TLSConfig: tlsConfig, // 使用用户传入的 tls.Config
TLSConfig: tlsConfig,
}
//engine.applyDefaultServerConfig(httpsSrv)
if engine.TLSServerConfigurator != nil {
engine.TLSServerConfigurator(httpsSrv)
} else if engine.ServerConfigurator != nil {
engine.ServerConfigurator(httpsSrv)
}
if engine.useDefaultProtocols {
//加入HTTP2支持
engine.SetProtocols(&ProtocolsConfig{
Http1: true,
Http2: true, // 默认启用 HTTP/2
Http2_Cleartext: false,
// --- HTTP 重定向服务器 ---
redirectHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
host = r.Host
}
_, httpsPort, err := net.SplitHostPort(httpsAddr)
if err != nil {
// 如果 httpsAddr 没有端口,这是一个配置错误
log.Fatalf("Invalid HTTPS address for redirection '%s': must include a port.", httpsAddr)
}
targetURL := "https://" + host
// 只有在非标准 HTTPS 端口 (443) 时才附加端口号
if httpsPort != "443" {
targetURL = "https://" + net.JoinHostPort(host, httpsPort)
}
targetURL += r.URL.RequestURI()
http.Redirect(w, r, targetURL, http.StatusMovedPermanently)
})
}
// HTTP Server for redirection
httpSrv := &http.Server{
Addr: httpAddr,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 从 r.Host 提取 hostname例如 "localhost:8080" -> "localhost"
hostOnly, _, err := net.SplitHostPort(r.Host)
if err != nil { // r.Host 可能没有端口,例如 "example.com"
hostOnly = r.Host
Handler: redirectHandler,
}
//engine.applyDefaultServerConfig(httpSrv)
if engine.ServerConfigurator != nil {
engine.ServerConfigurator(httpSrv)
}
// 从 httpsAddr 提取目标 HTTPS 端口,例如 ":443" -> "443"
_, targetHttpsPort, err := net.SplitHostPort(httpsAddr)
if err != nil { // httpsAddr 必须包含一个有效的端口
log.Fatalf("Error: Invalid HTTPS address '%s' for redirection. Must specify a port (e.g., ':443').", httpsAddr)
// --- 启动服务器和优雅关闭 ---
runServer("HTTPS", httpsSrv)
runServer("HTTP Redirect", httpSrv)
return handleGracefulShutdown([]*http.Server{httpsSrv, httpSrv}, getShutdownTimeout(timeouts), engine.LogReco)
}
var redirectHost string
if targetHttpsPort == "443" {
redirectHost = hostOnly // 如果是默认 HTTPS 端口,则无需在 URL 中显式指定端口
} else {
redirectHost = net.JoinHostPort(hostOnly, targetHttpsPort) // 否则,显式指定端口
}
// 构建目标 HTTPS URL
targetURL := "https://" + redirectHost + r.URL.RequestURI()
http.Redirect(w, r, targetURL, http.StatusMovedPermanently) // 301 Permanent Redirect
}),
}
// Start HTTPS server
go func() {
log.Printf("Touka HTTPS server listening on %s\n", httpsAddr)
if err := httpsSrv.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { // 同样,传入空字符串
log.Fatalf("Touka HTTPS server listen error: %s\n", err)
}
}()
// Start HTTP redirect server
go func() {
log.Printf("Touka HTTP redirect server listening on %s\n", httpAddr)
if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Touka HTTP redirect server listen error: %s\n", err)
}
}()
return handleGracefulShutdown([]*http.Server{httpsSrv, httpSrv}, timeout, engine.LogReco)
// RunWithTLSRedir 是 RunTLSRedir 的别名,为了保持向后兼容性
func (engine *Engine) RunWithTLSRedir(httpAddr, httpsAddr string, tlsConfig *tls.Config, timeouts ...time.Duration) error {
return engine.RunTLSRedir(httpAddr, httpsAddr, tlsConfig, timeouts...)
}

184
sse.go Normal file
View file

@ -0,0 +1,184 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright 2025 WJQSERVER. All rights reserved.
// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization.
package touka
import (
"bytes"
"io"
"net/http"
"strings"
)
// Event 代表一个服务器发送事件(SSE).
type Event struct {
// Event 是事件的名称.
Event string
// Data 是事件的内容, 可以是多行文本.
Data string
// Id 是事件的唯一标识符.
Id string
// Retry 是指定客户端在连接丢失后应等待多少毫秒后尝试重新连接.
Retry string
}
// Render 将事件格式化并写入给定的 writer.
// 通过逐行处理数据, 此方法可防止因数据中包含换行符而导致的CRLF注入问题.
// 为了性能, 它使用 bytes.Buffer 并通过 WriteTo 直接写入, 以避免不必要的内存分配.
func (e *Event) Render(w io.Writer) error {
var buf bytes.Buffer
if len(e.Id) > 0 {
buf.WriteString("id: ")
buf.WriteString(e.Id)
buf.WriteString("\n")
}
if len(e.Event) > 0 {
buf.WriteString("event: ")
buf.WriteString(e.Event)
buf.WriteString("\n")
}
if len(e.Data) > 0 {
lines := strings.Split(e.Data, "\n")
for _, line := range lines {
buf.WriteString("data: ")
buf.WriteString(line)
buf.WriteString("\n")
}
}
if len(e.Retry) > 0 {
buf.WriteString("retry: ")
buf.WriteString(e.Retry)
buf.WriteString("\n")
}
// 每个事件都以一个额外的换行符结尾.
buf.WriteString("\n")
// 直接将 buffer 的内容写入 writer, 避免生成中间字符串.
_, err := buf.WriteTo(w)
return err
}
// EventStream 启动一个 SSE 事件流.
// 这是推荐的、更简单安全的方式, 采用阻塞和回调的设计, 框架负责管理连接生命周期.
//
// 详细用法:
//
// r.GET("/sse/callback", func(c *touka.Context) {
// // streamer 回调函数会在一个循环中被调用.
// c.EventStream(func(w io.Writer) bool {
// event := touka.Event{
// Event: "time-tick",
// Data: time.Now().Format(time.RFC1123),
// }
//
// if err := event.Render(w); err != nil {
// // 发生写入错误, 停止发送.
// return false // 返回 false 结束事件流.
// }
//
// time.Sleep(2 * time.Second)
// return true // 返回 true 继续事件流.
// })
// // 当事件流结束后(例如客户端关闭页面), 这行代码会被执行.
// fmt.Println("Client disconnected from /sse/callback")
// })
func (c *Context) EventStream(streamer func(w io.Writer) bool) {
// 为现代网络协议优化头部.
c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
c.Writer.Header().Set("Cache-Control", "no-cache, no-transform")
c.Writer.Header().Del("Connection")
c.Writer.Header().Del("Transfer-Encoding")
c.Writer.WriteHeader(http.StatusOK)
c.Writer.Flush() // 直接调用, ResponseWriter 接口保证了 Flush 方法的存在.
for {
select {
case <-c.Request.Context().Done():
return
default:
if !streamer(c.Writer) {
return
}
c.Writer.Flush()
}
}
}
// EventStreamChan 返回用于 SSE 事件流的 channel.
// 这是为高级并发场景设计的、更灵活的API.
//
// 重要:
// - 调用者必须 close(eventChan) 来结束事件流.
// - 调用者必须在独立的 goroutine 中消费 errChan 来处理错误和连接断开.
// - 为防止 goroutine 泄漏, 建议发送方在 select 中同时监听 c.Request.Context().Done().
//
// 详细用法:
//
// r.GET("/sse/channel", func(c *touka.Context) {
// eventChan, errChan := c.EventStreamChan()
//
// // 必须在独立的goroutine中处理错误和连接断开.
// go func() {
// if err := <-errChan; err != nil {
// c.Errorf("SSE channel error: %v", err)
// }
// }()
//
// // 在另一个goroutine中异步发送事件.
// go func() {
// // 重要: 必须在逻辑结束时关闭channel, 以通知框架.
// defer close(eventChan)
//
// for i := 1; i <= 5; i++ {
// select {
// case <-c.Request.Context().Done():
// return // 客户端已断开, 退出 goroutine.
// default:
// eventChan <- touka.Event{
// Id: fmt.Sprintf("%d", i),
// Data: "hello from channel",
// }
// time.Sleep(2 * time.Second)
// }
// }
// }()
// })
func (c *Context) EventStreamChan() (chan<- Event, <-chan error) {
eventChan := make(chan Event)
errChan := make(chan error, 1)
c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
c.Writer.Header().Set("Cache-Control", "no-cache, no-transform")
c.Writer.Header().Del("Connection")
c.Writer.Header().Del("Transfer-Encoding")
c.Writer.WriteHeader(http.StatusOK)
c.Writer.Flush()
go func() {
defer close(errChan)
for {
select {
case event, ok := <-eventChan:
if !ok {
return
}
if err := event.Render(c.Writer); err != nil {
errChan <- err
return
}
c.Writer.Flush()
case <-c.Request.Context().Done():
errChan <- c.Request.Context().Err()
return
}
}
}()
return eventChan, errChan
}

144
testutil.go Normal file
View file

@ -0,0 +1,144 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright 2024 WJQSERVER. All rights reserved.
// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization.
package touka
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
)
// CreateTestContext 为测试创建一个 *Context 和一个关联的 *Engine。
// 它使用 httptest.NewRecorder() (如果传入的 w 为 nil) 来捕获响应。
// 返回的 Context 已经过初始化,其 Writer 指向提供的 ResponseWriter (或新创建的 Recorder)
// 其 Request 是一个默认的 "GET /" 请求,并且其 engine 字段指向返回的 Engine 实例。
//
// 参数:
// - w (http.ResponseWriter): 可选的。如果为 nil函数内部会创建一个 httptest.ResponseRecorder。
// 通常在测试中,你会传入一个 httptest.ResponseRecorder 来检查响应。
//
// 返回:
// - c (*Context): 一个初始化的 Touka Context。
// - r (*Engine): 一个新的 Touka Engine 实例,与 c 相关联。
func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) {
// 1. 如果未提供 ResponseWriter则创建一个测试用的 Recorder
// ResponseRecorder 实现了 http.ResponseWriter 接口
var testResponseWriter http.ResponseWriter = w
if testResponseWriter == nil {
testResponseWriter = httptest.NewRecorder()
}
// 2. 创建一个新的 Engine 实例
// 使用 New() 而不是 Default() 以获得一个“干净”的引擎,不带默认中间件 (如 Recovery)
// 如果你的测试依赖于 Default() 的中间件,可以改为 r = Default()
r = New()
// 3. 从 Engine 的池中获取一个 Context 对象
// 这是模拟真实请求处理的最佳方式
c = r.pool.Get().(*Context)
// 4. 创建一个默认的 HTTP 请求
// 测试时可以根据需要修改这个请求的 Method, URL, Body, Headers 等
// http.NewRequest 的 body 可以是 nil (对于GET) 或 bytes.NewBufferString("body content") 等
req, err := http.NewRequest(http.MethodGet, "/", nil)
if err != nil {
// NewRequest 对于 "GET" 和 "/" 以及 nil body 通常不会失败
// 但作为健壮性考虑,可以 panic 或返回错误
panic("touka.CreateTestContext: Failed to create dummy request: " + err.Error()) // 英文 panic
}
// 确保请求有关联的 Context (尽管 c.reset 也会设置)
// req = req.WithContext(context.Background()) // 通常 reset 会处理这个
// 5. 重置/初始化 Context
// c.reset() 方法期望一个 http.ResponseWriter 和 *http.Request
// 它会将 c.Writer 包装成 touka.ResponseWriter (responseWriterImpl)
// 并设置 c.Request, c.Params (清空), c.handlers (nil), c.index (-1),
// c.Keys (新map), c.Errors (清空), c.ctx (来自 req.Context()),
// 以及 c.engine (在 c.pool.New 中已经设置,但 reset 会确保其他关联正确)。
c.reset(testResponseWriter, req)
// 确保 Context 中的 engine 字段指向我们创建的这个 Engine 实例
// 虽然 c.pool.New 应该已经做了,但显式确认或设置无害,尤其是如果我们不完全依赖 pool.New 的细节。
// 在当前的 Context.reset 实现中c.engine 是在从池中 New() 时由 Engine 自身设置的,
// reset 方法不会改变它。所以只要 c 是从 r.pool 获取的c.engine 就应该是 r。
return c, r
}
// CreateTestContextWithRequest 功能与 CreateTestContext 类似,但允许传入自定义的 *http.Request。
// 这对于测试需要特定请求方法、URL、头部或Body的处理器非常有用。
//
// 参数:
// - w (http.ResponseWriter): 可选。如果为 nil创建一个 httptest.ResponseRecorder。
// - req (*http.Request): 用户提供的 HTTP 请求。如果为 nil则内部创建一个默认的 "GET /"。
//
// 返回:
// - c (*Context): 一个初始化的 Touka Context。
// - r (*Engine): 一个新的 Touka Engine 实例,与 c 相关联。
func CreateTestContextWithRequest(w http.ResponseWriter, req *http.Request) (c *Context, r *Engine) {
var testResponseWriter http.ResponseWriter = w
if testResponseWriter == nil {
testResponseWriter = httptest.NewRecorder()
}
r = New() // 创建 Engine
c = r.pool.Get().(*Context) // 从池获取 Context
var finalReq *http.Request = req
if finalReq == nil { // 如果未提供请求,创建默认请求
var err error
finalReq, err = http.NewRequest(http.MethodGet, "/", nil)
if err != nil {
panic("touka.CreateTestContextWithRequest: Failed to create dummy request: " + err.Error()) // 英文 panic
}
}
c.reset(testResponseWriter, finalReq) // 使用提供的或默认的请求重置 Context
// c.engine 已由 r.pool.New 设置为 r
return c, r
}
// PerformRequest 在给定的 Engine 上执行一个模拟的 HTTP 请求,并返回响应记录器。
// 这是一个更高级别的测试辅助函数封装了创建请求、Context 和执行引擎的 ServeHTTP 方法。
//
// 参数:
// - engine (*Engine): 要测试的 Touka 引擎实例。
// - method (string): HTTP 请求方法 (例如 "GET", "POST")。
// - path (string): 请求的路径 (例如 "/", "/users/123?name=test")。
// - body (io.Reader): 可选的请求体。对于 GET, HEAD 等通常为 nil。
// - headers (http.Header): 可选的请求头部。
//
// 返回:
// - *httptest.ResponseRecorder: 包含响应状态、头部和主体的记录器。
//
// 示例:
//
// rr := touka.PerformRequest(myEngine, "GET", "/ping", nil, nil)
// assert.Equal(t, http.StatusOK, rr.Code)
// assert.Equal(t, "pong", rr.Body.String())
func PerformRequest(engine *Engine, method, path string, body io.Reader, headers http.Header) *httptest.ResponseRecorder {
req, err := http.NewRequest(method, path, body)
if err != nil {
// 通常 NewRequest 对于合法的方法和路径不会失败(除非路径解析错误)
panic(fmt.Sprintf("touka.PerformRequest: Failed to create request %s %s: %v", method, path, err)) // 英文 panic
}
// 设置请求头部 (如果提供)
if headers != nil {
req.Header = headers
}
// 创建一个 ResponseRecorder 来捕获响应
rr := httptest.NewRecorder()
// 直接调用 Engine 的 ServeHTTP 方法来处理请求
// Engine 会负责创建和管理 Context
engine.ServeHTTP(rr, req)
return rr
}

357
tgzip.go
View file

@ -1,357 +0,0 @@
package touka
import (
"bufio"
"compress/gzip"
"errors"
"io"
"net"
"net/http"
"strconv"
"strings"
"sync"
)
const (
headerAcceptEncoding = "Accept-Encoding" // 请求头部,客户端声明接受的编码
headerContentEncoding = "Content-Encoding" // 响应头部,服务器声明使用的编码
headerContentLength = "Content-Length" // 响应头部,内容长度
headerContentType = "Content-Type" // 响应头部,内容类型
headerVary = "Vary" // 响应头部,指示缓存行为
encodingGzip = "gzip" // Gzip 编码名称
)
var (
// 默认可压缩的 MIME 类型
defaultCompressibleTypes = []string{
"text/html", "text/css", "text/plain", "text/javascript",
"application/javascript", "application/x-javascript", "application/json",
"application/xml", "image/svg+xml",
}
)
// GzipOptions 用于配置 Gzip 中间件。
type GzipOptions struct {
// Level 设置 Gzip 压缩级别。
// 例如: gzip.DefaultCompression, gzip.BestSpeed, gzip.BestCompression。
Level int
// MinContentLength 是应用 Gzip 的最小内容长度。
// 如果响应的 Content-Length 小于此值,则不应用 Gzip。
// 默认为 0 (无最小长度限制)。
MinContentLength int64
// CompressibleTypes 是要压缩的 MIME 类型列表。
// 如果为空,将使用 defaultCompressibleTypes。
CompressibleTypes []string
// DecompressFn 是一个可选函数,用于解压缩请求体 (如果请求体是 gzipped)。
// 如果为 nil则禁用请求体解压缩。
// 注意: 本次实现主要关注响应压缩,请求解压可以作为扩展。
// DecompressFn func(c *Context)
}
// gzipResponseWriter 包装了 touka.ResponseWriter 以提供 Gzip 压缩功能。
type gzipResponseWriter struct {
ResponseWriter // 底层的 ResponseWriter (可能是 ecw 或 responseWriterImpl)
gzWriter *gzip.Writer // compress/gzip 的 writer
options *GzipOptions // Gzip 配置
wroteHeader bool // 标记 Header 是否已写入
doCompression bool // 标记是否执行压缩
statusCode int // 存储状态码,在实际写入底层 Writer 前使用
}
// --- 对象池 ---
var gzipResponseWriterPool = sync.Pool{
New: func() interface{} {
return &gzipResponseWriter{}
},
}
// gzip.Writer 实例的对象池。
// 注意: gzip.Writer.Reset() 不会改变压缩级别,所以对象池需要提供已正确初始化级别的 writer。
// 我们为每个可能的级别创建一个池。
var gzipWriterPools [gzip.BestCompression - gzip.BestSpeed + 2]*sync.Pool // 覆盖 -1 (Default) 到 9 (BestCompression)
func init() {
for i := gzip.BestSpeed; i <= gzip.BestCompression; i++ {
level := i // 捕获循环变量用于闭包
gzipWriterPools[level-gzip.BestSpeed] = &sync.Pool{
New: func() interface{} {
// 初始化时 writer 为 nil在 Reset 时设置
w, _ := gzip.NewWriterLevel(nil, level)
return w
},
}
}
// 为 gzip.DefaultCompression (-1) 映射一个索引
defaultLevelIndex := gzip.BestCompression - gzip.BestSpeed + 1
gzipWriterPools[defaultLevelIndex] = &sync.Pool{
New: func() interface{} {
w, _ := gzip.NewWriterLevel(nil, gzip.DefaultCompression)
return w
},
}
}
// 从对象池获取一个 gzip.Writer
func getGzipWriterFromPool(level int, underlyingWriter io.Writer) *gzip.Writer {
var poolIndex int
if level == gzip.DefaultCompression {
poolIndex = gzip.BestCompression - gzip.BestSpeed + 1
} else if level >= gzip.BestSpeed && level <= gzip.BestCompression {
poolIndex = level - gzip.BestSpeed
} else { // 无效级别,使用默认级别
poolIndex = gzip.BestCompression - gzip.BestSpeed + 1
level = gzip.DefaultCompression // 保证一致性
}
gz := gzipWriterPools[poolIndex].Get().(*gzip.Writer)
gz.Reset(underlyingWriter) // 重置并关联到底层的 io.Writer
return gz
}
// 将 gzip.Writer 返还给对象池
func putGzipWriterToPool(gz *gzip.Writer, level int) {
var poolIndex int
if level == gzip.DefaultCompression {
poolIndex = gzip.BestCompression - gzip.BestSpeed + 1
} else if level >= gzip.BestSpeed && level <= gzip.BestCompression {
poolIndex = level - gzip.BestSpeed
} else { // 不应该发生,如果 getGzipWriterFromPool 进行了标准化
poolIndex = gzip.BestCompression - gzip.BestSpeed + 1
}
gzipWriterPools[poolIndex].Put(gz)
}
// 从对象池获取一个 gzipResponseWriter
func acquireGzipResponseWriter(underlying ResponseWriter, opts *GzipOptions) *gzipResponseWriter {
gzw := gzipResponseWriterPool.Get().(*gzipResponseWriter)
gzw.ResponseWriter = underlying
gzw.options = opts
gzw.wroteHeader = false
gzw.doCompression = false
gzw.statusCode = 0 // 重置状态码
// gzWriter 将在 WriteHeader 中如果需要时获取
return gzw
}
// 将 gzipResponseWriter 返还给对象池
func releaseGzipResponseWriter(gzw *gzipResponseWriter) {
if gzw.gzWriter != nil {
// 确保它被关闭并返回到池中
_ = gzw.gzWriter.Close() // 关闭会 flush
putGzipWriterToPool(gzw.gzWriter, gzw.options.Level)
gzw.gzWriter = nil
}
gzw.ResponseWriter = nil // 断开引用
gzw.options = nil
gzipResponseWriterPool.Put(gzw)
}
// --- gzipResponseWriter 方法实现 ---
// Header 返回底层 ResponseWriter 的头部 map。
func (gzw *gzipResponseWriter) Header() http.Header {
return gzw.ResponseWriter.Header()
}
// WriteHeader 发送 HTTP 响应头部和指定的状态码。
// 在这里决定是否进行压缩。
func (gzw *gzipResponseWriter) WriteHeader(statusCode int) {
if gzw.wroteHeader {
return
}
gzw.wroteHeader = true
gzw.statusCode = statusCode // 存储状态码
// 在修改头部以进行压缩之前进行条件检查
// 1. 如果状态码是信息性(1xx)、重定向(3xx)、无内容(204)、重置内容(205)或未修改(304),则不压缩
if statusCode < http.StatusOK || statusCode == http.StatusNoContent || statusCode == http.StatusResetContent || statusCode == http.StatusNotModified {
gzw.ResponseWriter.WriteHeader(statusCode)
return
}
// 2. 如果响应已经被编码,则不压缩
if gzw.Header().Get(headerContentEncoding) != "" {
gzw.ResponseWriter.WriteHeader(statusCode)
return
}
// 3. 检查 Content-Type
contentType := strings.ToLower(strings.TrimSpace(strings.Split(gzw.Header().Get(headerContentType), ";")[0]))
compressibleTypes := gzw.options.CompressibleTypes
if len(compressibleTypes) == 0 {
compressibleTypes = defaultCompressibleTypes
}
isCompressible := false
for _, t := range compressibleTypes {
if strings.HasPrefix(contentType, t) { // 使用 HasPrefix 以匹配如 "text/html; charset=utf-8"
isCompressible = true
break
}
}
if !isCompressible {
gzw.ResponseWriter.WriteHeader(statusCode)
return
}
// 4. 检查 MinContentLength
if gzw.options.MinContentLength > 0 {
if clStr := gzw.Header().Get(headerContentLength); clStr != "" {
if cl, err := strconv.ParseInt(clStr, 10, 64); err == nil && cl < gzw.options.MinContentLength {
gzw.ResponseWriter.WriteHeader(statusCode)
return
}
}
// 如果未设置 Content-Length但设置了 MinContentLength我们可能仍会压缩。
// 这是一个权衡:可能会压缩小的动态内容。
}
// 所有检查通过,进行压缩
gzw.doCompression = true
gzw.Header().Set(headerContentEncoding, encodingGzip)
gzw.Header().Add(headerVary, headerAcceptEncoding) // 使用 Add 以避免覆盖其他 Vary 值
gzw.Header().Del(headerContentLength) // Gzip 会改变内容长度,所以删除它
// 从池中获取 gzWriter并将其 Reset 指向实际的底层 ResponseWriter
// 注意gzw.ResponseWriter 是被 Gzip 包装的 writer (例如,原始的 responseWriterImpl 或 ecw)
gzw.gzWriter = getGzipWriterFromPool(gzw.options.Level, gzw.ResponseWriter)
gzw.ResponseWriter.WriteHeader(statusCode) // 调用原始的 WriteHeader
}
// Write 将数据写入连接作为 HTTP 回复的一部分。
func (gzw *gzipResponseWriter) Write(data []byte) (int, error) {
if !gzw.wroteHeader {
// 如果在 WriteHeader 之前调用 Write根据 http.ResponseWriter 规范,
// 应写入 200 OK 头部。
gzw.WriteHeader(http.StatusOK)
}
if gzw.doCompression {
return gzw.gzWriter.Write(data)
}
return gzw.ResponseWriter.Write(data)
}
// Close 确保 gzip writer 被关闭并释放资源。
// 中间件应该在 c.Next() 之后调用它(通常在 defer 中)。
func (gzw *gzipResponseWriter) Close() error {
if gzw.gzWriter != nil {
err := gzw.gzWriter.Close() // Close 会 Flush
putGzipWriterToPool(gzw.gzWriter, gzw.options.Level)
gzw.gzWriter = nil // 标记为已返回
return err
}
return nil
}
// Flush 将所有缓冲数据发送到客户端。
// 实现 http.Flusher。
func (gzw *gzipResponseWriter) Flush() {
if gzw.doCompression && gzw.gzWriter != nil {
_ = gzw.gzWriter.Flush() // 确保 gzip writer 的缓冲被刷新
}
// 然后刷新底层的 writer (如果它支持)
if fl, ok := gzw.ResponseWriter.(http.Flusher); ok {
fl.Flush()
}
}
// Hijack 允许调用者接管连接。
// 实现 http.Hijacker。
func (gzw *gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
// 如果正在压缩hijack 的意义不大或不安全。
// 然而WriteHeader 应该会阻止对 101 状态码的压缩。
// 此调用必须转到实际的底层 ResponseWriter。
if hj, ok := gzw.ResponseWriter.(http.Hijacker); ok {
return hj.Hijack()
}
// 返回英文错误
return nil, nil, errors.New("touka.gzipResponseWriter: underlying ResponseWriter does not implement http.Hijacker")
}
// Status 返回已写入的 HTTP 状态码。委托给底层的 ResponseWriter。
// 这确保了与 ecw 或其他可能跟踪状态的包装器的兼容性。
func (gzw *gzipResponseWriter) Status() int {
if gzw.statusCode != 0 { // 如果我们在 WriteHeader 期间存储了它
return gzw.statusCode
}
return gzw.ResponseWriter.Status() // 委托
}
// Size 返回已写入的字节数。委托给底层的 ResponseWriter。
// 如果已压缩,这将是压缩后的大小 (由底层 writer 记录)。
func (gzw *gzipResponseWriter) Size() int {
return gzw.ResponseWriter.Size() // GzipResponseWriter 本身不直接跟踪大小,依赖底层
}
// Written 返回 WriteHeader 是否已被调用。委托给底层的 ResponseWriter。
func (gzw *gzipResponseWriter) Written() bool {
// 如果 gzw.wroteHeader 为 true说明 WriteHeader 至少被 gzw 处理过。
// 但最终是否写入底层取决于 gzw 的逻辑。
// 更可靠的是询问底层 writer。
return gzw.ResponseWriter.Written() // 委托
}
// --- Gzip 中间件 ---
// Gzip 返回一个使用 Gzip 压缩 HTTP 响应的中间件。
// 它会检查客户端的 "Accept-Encoding" 头部和响应的 "Content-Type"
// 来决定是否应用压缩。
// level 参数指定压缩级别 (例如 gzip.DefaultCompression)。
// opts 参数是可选的 GzipOptions。
func Gzip(level int, opts ...GzipOptions) HandlerFunc {
config := GzipOptions{ // 初始化默认配置
Level: level,
MinContentLength: 0, // 默认:无最小长度
CompressibleTypes: defaultCompressibleTypes,
}
if len(opts) > 0 { // 如果传入了 GzipOptions则覆盖默认值
opt := opts[0]
config.Level = opt.Level // 允许通过结构体覆盖级别
if opt.MinContentLength > 0 {
config.MinContentLength = opt.MinContentLength
}
if len(opt.CompressibleTypes) > 0 {
config.CompressibleTypes = opt.CompressibleTypes
}
}
// 验证级别
if config.Level < gzip.DefaultCompression || config.Level > gzip.BestCompression {
config.Level = gzip.DefaultCompression
}
return func(c *Context) {
// 1. 检查客户端是否接受 gzip
if !strings.Contains(c.Request.Header.Get(headerAcceptEncoding), encodingGzip) {
c.Next()
return
}
// 2. 包装 ResponseWriter
originalWriter := c.Writer
gzw := acquireGzipResponseWriter(originalWriter, &config)
c.Writer = gzw // 替换上下文的 writer
// defer 确保即使后续处理函数发生 panic也能进行清理
// 尽管恢复中间件应该自己处理 panic 响应。
defer func() {
// 必须关闭 gzip writer 以刷新其缓冲区。
// 这也会将 gzip.Writer 返回到其对象池。
if err := gzw.Close(); err != nil {
// 记录关闭 gzip writer 时的错误,但不应覆盖已发送的响应
// 通常这个错误不严重,因为数据可能已经大部分发送
// 使用英文记录日志
// log.Printf("Error closing gzip writer: %v", err)
c.AddError(err) // 可以选择将错误添加到 Context 中
}
// 恢复原始 writer 并将 gzipResponseWriter 返回到其对象池
c.Writer = originalWriter
releaseGzipResponseWriter(gzw)
}()
// 3. 调用链中的下一个处理函数
c.Next()
// c.Next() 执行完毕后,响应头部应该已经设置。
// gzw.WriteHeader 会被显式调用或通过第一次 Write 隐式调用。
// 如果 gzw.doCompression 为 true响应体已写入 gzw.gzWriter。
// defer 中的 gzw.Close() 会刷新最终的压缩字节。
}
}

View file

@ -1,3 +1,7 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright 2024 WJQSERVER. All rights reserved.
// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization.
package touka
import (
@ -42,3 +46,28 @@ type RouteInfo struct {
Handler string // 处理函数名称
Group string // 路由分组
}
// 维护一个Methods列表
var (
MethodGet = "GET"
MethodHead = "HEAD"
MethodPost = "POST"
MethodPut = "PUT"
MethodPatch = "PATCH"
MethodDelete = "DELETE"
MethodConnect = "CONNECT"
MethodOptions = "OPTIONS"
MethodTrace = "TRACE"
)
var MethodsSet = map[string]struct{}{
MethodGet: {},
MethodHead: {},
MethodPost: {},
MethodPut: {},
MethodPatch: {},
MethodDelete: {},
MethodConnect: {},
MethodOptions: {},
MethodTrace: {},
}

352
tree.go
View file

@ -2,51 +2,43 @@
// Use of this source code is governed by a BSD-style license that can be found
// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE
// This tree.go is gin's fork, you can see https://github.com/gin-gonic/gin/blob/master/tree.go
package touka // 定义包名为 touka该包可能是一个路由或Web框架的核心组件
package touka
import (
"bytes" // 导入 bytes 包,用于操作字节切片
"net/url" // 导入 net/url 包,用于 URL 解析和转义
"strings" // 导入 strings 包,用于字符串操作
"unicode" // 导入 unicode 包,用于处理 Unicode 字符
"unicode/utf8" // 导入 unicode/utf8 包,用于 UTF-8 编码和解码
"unsafe" // 导入 unsafe 包,用于不安全的类型转换,以避免内存分配
"net/url"
"strings"
"unicode"
"unicode/utf8"
"unsafe"
)
// StringToBytes 将字符串转换为字节切片,不进行内存分配。
// 更多详情,请参见 https://github.com/golang/go/issues/53003#issuecomment-1140276077。
// 注意:此函数使用 unsafe 包,应谨慎使用,因为它可能导致内存不安全。
// StringToBytes 将字符串转换为字节切片, 不进行内存分配.
// 更多详情, 请参见 https://github.com/golang/go/issues/53003#issuecomment-1140276077.
// 注意: 此函数使用 unsafe 包, 应谨慎使用, 因为它可能导致内存不安全.
func StringToBytes(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
// BytesToString 将字节切片转换为字符串,不进行内存分配。
// 更多详情,请参见 https://github.com/golang/go/issues/53003#issuecomment-1140276077。
// 注意:此函数使用 unsafe 包,应谨慎使用,因为它可能导致内存不安全。
// BytesToString 将字节切片转换为字符串, 不进行内存分配.
// 更多详情, 请参见 https://github.com/golang/go/issues/53003#issuecomment-1140276077.
// 注意: 此函数使用 unsafe 包, 应谨慎使用, 因为它可能导致内存不安全.
func BytesToString(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b))
}
var (
strColon = []byte(":") // 定义字节切片常量,表示冒号,用于路径参数识别
strStar = []byte("*") // 定义字节切片常量,表示星号,用于捕获所有路径识别
strSlash = []byte("/") // 定义字节切片常量,表示斜杠,用于路径分隔符识别
)
// Param 是单个 URL 参数,由键和值组成。
// Param 是单个 URL 参数, 由键和值组成.
type Param struct {
Key string // 参数的键名
Value string // 参数的值
}
// Params 是 Param 类型的切片,由路由器返回。
// 该切片是有序的,第一个 URL 参数也是切片中的第一个值。
// 因此,按索引读取值是安全的。
// Params 是 Param 类型的切片, 由路由器返回.
// 该切片是有序的, 第一个 URL 参数也是切片中的第一个值.
// 因此, 按索引读取值是安全的.
type Params []Param
// Get 返回键名与给定名称匹配的第一个 Param 的值,并返回一个布尔值 true。
// 如果未找到匹配的 Param,则返回空字符串和布尔值 false。
// Get 返回键名与给定名称匹配的第一个 Param 的值, 并返回一个布尔值 true.
// 如果未找到匹配的 Param, 则返回空字符串和布尔值 false.
func (ps Params) Get(name string) (string, bool) {
for _, entry := range ps {
if entry.Key == name {
@ -56,24 +48,24 @@ func (ps Params) Get(name string) (string, bool) {
return "", false
}
// ByName 返回键名与给定名称匹配的第一个 Param 的值
// 如果未找到匹配的 Param,则返回空字符串。
// ByName 返回键名与给定名称匹配的第一个 Param 的值.
// 如果未找到匹配的 Param, 则返回空字符串.
func (ps Params) ByName(name string) (va string) {
va, _ = ps.Get(name) // 调用 Get 方法获取值忽略第二个返回值
va, _ = ps.Get(name) // 调用 Get 方法获取值, 忽略第二个返回值
return
}
// methodTree 表示特定 HTTP 方法的路由树
// methodTree 表示特定 HTTP 方法的路由树.
type methodTree struct {
method string // HTTP 方法(例如 "GET", "POST"
method string // HTTP 方法(例如 "GET", "POST")
root *node // 该方法的根节点
}
// methodTrees 是 methodTree 的切片
// methodTrees 是 methodTree 的切片.
type methodTrees []methodTree
// get 根据给定的 HTTP 方法查找并返回对应的根节点
// 如果找不到,则返回 nil。
// get 根据给定的 HTTP 方法查找并返回对应的根节点.
// 如果找不到, 则返回 nil.
func (trees methodTrees) get(method string) *node {
for _, tree := range trees {
if tree.method == method {
@ -83,7 +75,7 @@ func (trees methodTrees) get(method string) *node {
return nil
}
// longestCommonPrefix 计算两个字符串的最长公共前缀的长度
// longestCommonPrefix 计算两个字符串的最长公共前缀的长度.
func longestCommonPrefix(a, b string) int {
i := 0
max_ := min(len(a), len(b)) // 找出两个字符串中较短的长度
@ -93,64 +85,61 @@ func longestCommonPrefix(a, b string) int {
return i // 返回公共前缀的长度
}
// addChild 添加一个子节点,并将通配符子节点(如果存在)保持在数组的末尾。
// addChild 添加一个子节点, 并将通配符子节点(如果存在)保持在数组的末尾.
func (n *node) addChild(child *node) {
if n.wildChild && len(n.children) > 0 {
// 如果当前节点有通配符子节点,且已有子节点,则将通配符子节点移到末尾
// 如果当前节点有通配符子节点, 且已有子节点, 则将通配符子节点移到末尾
wildcardChild := n.children[len(n.children)-1]
n.children = append(n.children[:len(n.children)-1], child, wildcardChild)
} else {
// 否则直接添加子节点
// 否则, 直接添加子节点
n.children = append(n.children, child)
}
}
// countParams 计算路径中参数(冒号)和捕获所有(星号)的数量。
// countParams 计算路径中参数(冒号)和捕获所有(星号)的数量.
func countParams(path string) uint16 {
var n uint16
s := StringToBytes(path) // 将路径字符串转换为字节切片
n += uint16(bytes.Count(s, strColon)) // 统计冒号的数量
n += uint16(bytes.Count(s, strStar)) // 统计星号的数量
return n
colons := strings.Count(path, ":")
stars := strings.Count(path, "*")
return uint16(colons + stars)
}
// countSections 计算路径中斜杠'/')的数量,即路径段的数量。
// countSections 计算路径中斜杠('/')的数量, 即路径段的数量.
func countSections(path string) uint16 {
s := StringToBytes(path) // 将路径字符串转换为字节切片
return uint16(bytes.Count(s, strSlash)) // 统计斜杠的数量
return uint16(strings.Count(path, "/"))
}
// nodeType 定义了节点的类型
// nodeType 定义了节点的类型.
type nodeType uint8
const (
static nodeType = iota // 静态节点路径中不包含参数或通配符
static nodeType = iota // 静态节点, 路径中不包含参数或通配符
root // 根节点
param // 参数节点(例如:name
catchAll // 捕获所有节点(例如*path
param // 参数节点(例如:name)
catchAll // 捕获所有节点(例如*path)
)
// node 表示路由树中的一个节点
// node 表示路由树中的一个节点.
type node struct {
path string // 当前节点的路径段
indices string // 子节点第一个字符的索引字符串用于快速查找子节点
wildChild bool // 是否包含通配符子节点:param 或 *catchAll
nType nodeType // 节点的类型(静态、根、参数、捕获所有)
priority uint32 // 节点的优先级用于查找时优先匹配
children []*node // 子节点切片最多有一个 :param 风格的节点位于数组末尾
indices string // 子节点第一个字符的索引字符串, 用于快速查找子节点
wildChild bool // 是否包含通配符子节点(:param 或 *catchAll)
nType nodeType // 节点的类型(静态, 根, 参数, 捕获所有)
priority uint32 // 节点的优先级, 用于查找时优先匹配
children []*node // 子节点切片, 最多有一个 :param 风格的节点位于数组末尾
handlers HandlersChain // 绑定到此节点的处理函数链
fullPath string // 完整路径用于调试和错误信息
fullPath string // 完整路径, 用于调试和错误信息
}
// incrementChildPrio 增加给定子节点的优先级并在必要时重新排序
// incrementChildPrio 增加给定子节点的优先级并在必要时重新排序.
func (n *node) incrementChildPrio(pos int) int {
cs := n.children // 获取子节点切片
cs[pos].priority++ // 增加指定位置子节点的优先级
prio := cs[pos].priority // 获取新的优先级
// 调整位置(向前移动)
// 调整位置(向前移动)
newPos := pos
// 从当前位置向前遍历,如果前一个子节点的优先级小于当前子节点,则交换位置
// 从当前位置向前遍历, 如果前一个子节点的优先级小于当前子节点, 则交换位置
for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
// 交换节点位置
cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
@ -158,9 +147,9 @@ func (n *node) incrementChildPrio(pos int) int {
// 构建新的索引字符字符串
if newPos != pos {
// 如果位置发生变化则重新构建 indices 字符串
// 如果位置发生变化, 则重新构建 indices 字符串
// 前缀部分 + 移动的索引字符 + 剩余部分
n.indices = n.indices[:newPos] + // 未改变的前缀可能为空
n.indices = n.indices[:newPos] + // 未改变的前缀, 可能为空
n.indices[pos:pos+1] + // 被移动的索引字符
n.indices[newPos:pos] + n.indices[pos+1:] // 除去原位置字符的其余部分
}
@ -168,13 +157,13 @@ func (n *node) incrementChildPrio(pos int) int {
return newPos // 返回新的位置
}
// addRoute 为给定路径添加一个带有处理函数的节点
// 非并发安全
// addRoute 为给定路径添加一个带有处理函数的节点.
// 非并发安全!
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path // 记录完整的路径
n.priority++ // 增加当前节点的优先级
// 如果是空树(根节点)
// 如果是空树(根节点)
if len(n.path) == 0 && len(n.children) == 0 {
n.insertChild(path, fullPath, handlers) // 直接插入子节点
n.nType = root // 设置为根节点类型
@ -185,12 +174,12 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
walk: // 外部循环用于遍历和构建路由树
for {
// 找到最长公共前缀
// 这也意味着公共前缀不包含 ':' 或 '*',因为现有键不能包含这些字符
// 找到最长公共前缀.
// 这也意味着公共前缀不包含 ':' 或 '*',因为现有键不能包含这些字符.
i := longestCommonPrefix(path, n.path)
// 分裂边 (Split edge)
// 如果公共前缀小于当前节点的路径长度说明当前节点需要被分裂
// 如果公共前缀小于当前节点的路径长度, 说明当前节点需要被分裂
if i < len(n.path) {
child := node{
path: n.path[i:], // 子节点路径是当前节点路径的剩余部分
@ -199,27 +188,27 @@ walk: // 外部循环用于遍历和构建路由树
indices: n.indices, // 继承索引
children: n.children, // 继承子节点
handlers: n.handlers, // 继承处理函数
priority: n.priority - 1, // 优先级减1因为分裂会降低优先级
priority: n.priority - 1, // 优先级减1, 因为分裂会降低优先级
fullPath: n.fullPath, // 继承完整路径
}
n.children = []*node{&child} // 当前节点现在只有一个子节点新分裂出的子节点
n.children = []*node{&child} // 当前节点现在只有一个子节点: 新分裂出的子节点
// 将当前节点的 indices 设置为新子节点路径的第一个字符
n.indices = BytesToString([]byte{n.path[i]}) // []byte 用于正确的 Unicode 字符转换
n.path = path[:i] // 当前节点的路径更新为公共前缀
n.handlers = nil // 当前节点不再有处理函数(因为它被分裂了)
n.handlers = nil // 当前节点不再有处理函数(因为它被分裂了)
n.wildChild = false // 当前节点不再是通配符子节点
n.fullPath = fullPath[:parentFullPathIndex+i] // 更新完整路径
}
// 将新节点作为当前节点的子节点
// 如果路径仍然有剩余部分(即未完全匹配)
// 如果路径仍然有剩余部分(即未完全匹配)
if i < len(path) {
path = path[i:] // 移除已匹配的前缀
c := path[0] // 获取剩余路径的第一个字符
// '/' 在参数之后
// 如果当前节点是参数类型,且剩余路径以 '/' 开头,并且只有一个子节点
// 如果当前节点是参数类型, 且剩余路径以 '/' 开头, 并且只有一个子节点
// 则继续遍历其唯一的子节点
if n.nType == param && c == '/' && len(n.children) == 1 {
parentFullPathIndex += len(n.path) // 更新父节点完整路径索引
@ -238,8 +227,8 @@ walk: // 外部循环用于遍历和构建路由树
}
}
// 否则插入新节点
// 如果第一个字符不是 ':' 也不是 '*'且当前节点不是 catchAll 类型
// 否则, 插入新节点
// 如果第一个字符不是 ':' 也不是 '*', 且当前节点不是 catchAll 类型
if c != ':' && c != '*' && n.nType != catchAll {
// 将新字符添加到索引字符串
n.indices += BytesToString([]byte{c}) // []byte 用于正确的 Unicode 字符转换
@ -250,18 +239,18 @@ walk: // 外部循环用于遍历和构建路由树
n.incrementChildPrio(len(n.indices) - 1) // 增加新子节点的优先级并重新排序
n = child // 移动到新子节点
} else if n.wildChild {
// 正在插入一个通配符节点需要检查是否与现有通配符冲突
// 正在插入一个通配符节点, 需要检查是否与现有通配符冲突
n = n.children[len(n.children)-1] // 移动到现有的通配符子节点
n.priority++ // 增加其优先级
// 检查通配符是否匹配
// 如果剩余路径长度大于等于通配符节点的路径长度且通配符节点路径是剩余路径的前缀
// 并且不是 catchAll 类型(不能有子路由),
// 如果剩余路径长度大于等于通配符节点的路径长度, 且通配符节点路径是剩余路径的前缀
// 并且不是 catchAll 类型(不能有子路由),
// 并且通配符之后没有更多字符或紧跟着 '/'
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
// 不能向 catchAll 添加子节点
n.nType != catchAll &&
// 检查更长的通配符例如 :name 和 :names
// 检查更长的通配符, 例如 :name 和 :names
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
continue walk // 继续外部循环
}
@ -269,7 +258,7 @@ walk: // 外部循环用于遍历和构建路由树
// 通配符冲突
pathSeg := path
if n.nType != catchAll {
pathSeg, _, _ = strings.Cut(pathSeg, "/") // 如果不是 catchAll则截取到下一个 '/'
pathSeg, _, _ = strings.Cut(pathSeg, "/") // 如果不是 catchAll, 则截取到下一个 '/'
}
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path // 构造冲突前缀
panic("'" + pathSeg + // 抛出 panic 表示通配符冲突
@ -279,13 +268,13 @@ walk: // 外部循环用于遍历和构建路由树
"'")
}
n.insertChild(path, fullPath, handlers) // 插入子节点(可能包含通配符)
n.insertChild(path, fullPath, handlers) // 插入子节点(可能包含通配符)
return // 完成添加路由
}
// 否则将处理函数添加到当前节点
// 否则, 将处理函数添加到当前节点
if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'") // 如果已注册处理函数则报错
panic("handlers are already registered for path '" + fullPath + "'") // 如果已注册处理函数, 则报错
}
n.handlers = handlers // 设置处理函数
n.fullPath = fullPath // 设置完整路径
@ -293,20 +282,20 @@ walk: // 外部循环用于遍历和构建路由树
}
}
// findWildcard 搜索通配符段并检查名称是否包含无效字符
// 如果未找到通配符,则返回 -1 作为索引。
// findWildcard 搜索通配符段并检查名称是否包含无效字符.
// 如果未找到通配符, 则返回 -1 作为索引.
func findWildcard(path string) (wildcard string, i int, valid bool) {
// 查找开始位置
escapeColon := false // 是否正在处理转义字符
for start, c := range []byte(path) {
if escapeColon {
escapeColon = false
if c == ':' { // 如果转义字符是 ':'则跳过
if c == ':' { // 如果转义字符是 ':', 则跳过
continue
}
panic("invalid escape string in path '" + path + "'") // 无效的转义字符串
}
if c == '\\' { // 如果是反斜杠则设置转义标志
if c == '\\' { // 如果是反斜杠, 则设置转义标志
escapeColon = true
continue
}
@ -319,36 +308,36 @@ func findWildcard(path string) (wildcard string, i int, valid bool) {
valid = true // 默认为有效
for end, c := range []byte(path[start+1:]) {
switch c {
case '/': // 如果遇到斜杠说明通配符段结束
case '/': // 如果遇到斜杠, 说明通配符段结束
return path[start : start+1+end], start, valid
case ':', '*': // 如果在通配符段中再次遇到 ':' 或 '*'则无效
case ':', '*': // 如果在通配符段中再次遇到 ':' 或 '*', 则无效
valid = false
}
}
return path[start:], start, valid // 返回找到的通配符起始索引和有效性
return path[start:], start, valid // 返回找到的通配符, 起始索引和有效性
}
return "", -1, false // 未找到通配符
}
// insertChild 插入一个带有处理函数的节点
// 此函数处理包含通配符的路径插入逻辑
// insertChild 插入一个带有处理函数的节点.
// 此函数处理包含通配符的路径插入逻辑.
func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) {
for {
// 找到第一个通配符之前的前缀
wildcard, i, valid := findWildcard(path)
if i < 0 { // 未找到通配符结束循环
if i < 0 { // 未找到通配符, 结束循环
break
}
// 通配符名称只能包含一个 ':' 或 '*' 字符
if !valid {
panic("only one wildcard per path segment is allowed, has: '" +
wildcard + "' in path '" + fullPath + "'") // 报错每个路径段只允许一个通配符
wildcard + "' in path '" + fullPath + "'") // 报错: 每个路径段只允许一个通配符
}
// 检查通配符是否有名称
if len(wildcard) < 2 {
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") // 报错通配符必须有非空名称
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") // 报错: 通配符必须有非空名称
}
if wildcard[0] == ':' { // 如果是参数节点 (param)
@ -368,7 +357,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
n = child // 移动到新创建的参数节点
n.priority++ // 增加优先级
// 如果路径不以通配符结束则会有一个以 '/' 开头的子路径
// 如果路径不以通配符结束, 则会有一个以 '/' 开头的子路径
if len(wildcard) < len(path) {
path = path[len(wildcard):] // 剩余路径去除通配符部分
@ -376,19 +365,19 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
priority: 1, // 新子节点优先级
fullPath: fullPath, // 设置子节点的完整路径
}
n.addChild(child) // 添加子节点(通常是斜杠后的静态部分)
n.addChild(child) // 添加子节点(通常是斜杠后的静态部分)
n = child // 移动到这个新子节点
continue // 继续循环查找下一个通配符或结束
continue // 继续循环, 查找下一个通配符或结束
}
// 否则,我们已经完成。将处理函数插入到新叶节点中
// 否则, 我们已经完成. 将处理函数插入到新叶节点中
n.handlers = handlers // 设置处理函数
return // 完成
}
// 如果是捕获所有节点 (catchAll)
if i+len(wildcard) != len(path) {
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") // 报错捕获所有路由只能在路径末尾
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") // 报错: 捕获所有路由只能在路径末尾
}
// 检查路径段冲突
@ -397,22 +386,22 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
if len(n.children) != 0 {
pathSeg, _, _ = strings.Cut(n.children[0].path, "/")
}
panic("catch-all wildcard '" + path + // 报错捕获所有通配符与现有路径段冲突
panic("catch-all wildcard '" + path + // 报错: 捕获所有通配符与现有路径段冲突
"' in new path '" + fullPath +
"' conflicts with existing path segment '" + pathSeg +
"' in existing prefix '" + n.path + pathSeg +
"'")
}
// 当前固定宽度为 1用于 '/'
// 当前固定宽度为 1, 用于 '/'
i--
if i < 0 || path[i] != '/' {
panic("no / before catch-all in path '" + fullPath + "'") // 报错捕获所有之前没有 '/'
panic("no / before catch-all in path '" + fullPath + "'") // 报错: 捕获所有之前没有 '/'
}
n.path = path[:i] // 当前节点路径更新为 catchAll 之前的部分
// 第一个节点路径为空的 catchAll 节点
// 第一个节点: 路径为空的 catchAll 节点
child := &node{
wildChild: true, // 标记为有通配符子节点
nType: catchAll, // 类型为 catchAll
@ -420,11 +409,11 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
}
n.addChild(child) // 添加子节点
n.indices = string('/') // 索引设置为 '/'
n.indices = "/" // 索引设置为 '/'
n = child // 移动到新创建的 catchAll 节点
n.priority++ // 增加优先级
// 第二个节点包含变量的节点
// 第二个节点: 包含变量的节点
child = &node{
path: path[i:], // 路径为 catchAll 的实际路径段
nType: catchAll, // 类型为 catchAll
@ -437,7 +426,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
return // 完成
}
// 如果没有找到通配符简单地插入路径和处理函数
// 如果没有找到通配符, 简单地插入路径和处理函数
n.path = path // 设置当前节点路径
n.handlers = handlers // 设置处理函数
n.fullPath = fullPath // 设置完整路径
@ -451,16 +440,16 @@ type nodeValue struct {
fullPath string // 匹配到的完整路径
}
// skippedNode 结构体用于在 getValue 查找过程中记录跳过的节点信息,以便回溯。
// skippedNode 结构体用于在 getValue 查找过程中记录跳过的节点信息, 以便回溯.
type skippedNode struct {
path string // 跳过时的当前路径
node *node // 跳过的节点
paramsCount int16 // 跳过时已收集的参数数量
}
// getValue 返回注册到给定路径key的处理函数。通配符的值会保存到 map 中。
// 如果找不到处理函数,则在存在一个带有额外(或不带)尾部斜杠的处理函数时,
// 建议进行 TSR(尾部斜杠重定向)。
// getValue 返回注册到给定路径(key)的处理函数. 通配符的值会保存到 map 中.
// 如果找不到处理函数, 则在存在一个带有额外(或不带)尾部斜杠的处理函数时,
// 建议进行 TSR(尾部斜杠重定向).
func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode, unescape bool) (value nodeValue) {
var globalParamsCount int16 // 全局参数计数
@ -471,11 +460,16 @@ walk: // 外部循环用于遍历路由树
if path[:len(prefix)] == prefix { // 如果路径以当前节点的前缀开头
path = path[len(prefix):] // 移除已匹配的前缀
// 优先尝试所有非通配符子节点,通过匹配索引字符
// 在访问 path[0] 之前进行安全检查
if len(path) == 0 {
continue walk
}
// 优先尝试所有非通配符子节点, 通过匹配索引字符
idxc := path[0] // 剩余路径的第一个字符
for i, c := range []byte(n.indices) {
if c == idxc { // 如果找到匹配的索引字符
// 如果当前节点有通配符子节点,则将当前节点添加到 skippedNodes以便回溯
// 如果当前节点有通配符子节点, 则将当前节点添加到 skippedNodes, 以便回溯
if n.wildChild {
index := len(*skippedNodes)
*skippedNodes = (*skippedNodes)[:index+1]
@ -518,20 +512,20 @@ walk: // 外部循环用于遍历路由树
}
}
// 未找到
// 如果存在一个带有额外(或不带)尾部斜杠的处理函数,
// 我们可以建议重定向到相同 URL,不带尾部斜杠。
value.tsr = path == "/" && n.handlers != nil // 如果路径是 "/" 且当前节点有处理函数则建议 TSR
// 未找到.
// 如果存在一个带有额外(或不带)尾部斜杠的处理函数,
// 我们可以建议重定向到相同 URL, 不带尾部斜杠.
value.tsr = path == "/" && n.handlers != nil // 如果路径是 "/" 且当前节点有处理函数, 则建议 TSR
return value
}
// 处理通配符子节点它总是位于数组的末尾
// 处理通配符子节点, 它总是位于数组的末尾
n = n.children[len(n.children)-1] // 移动到通配符子节点
globalParamsCount++ // 增加全局参数计数
switch n.nType {
case param: // 参数节点
// 查找参数结束位置'/' 或路径末尾)
// 查找参数结束位置('/' 或路径末尾)
end := 0
for end < len(path) && path[end] != '/' {
end++
@ -539,7 +533,7 @@ walk: // 外部循环用于遍历路由树
// 保存参数值
if params != nil {
// 如果需要预分配容量
// 如果需要, 预分配容量
if cap(*params) < int(globalParamsCount) {
newParams := make(Params, len(*params), globalParamsCount)
copy(newParams, *params)
@ -559,12 +553,12 @@ walk: // 外部循环用于遍历路由树
}
}
(*value.params)[i] = Param{ // 存储参数
Key: n.path[1:], // 参数键名(去除冒号)
Key: n.path[1:], // 参数键名(去除冒号)
Value: val, // 参数值
}
}
// 我们需要继续深入
// 我们需要继续深入!
if end < len(path) {
if len(n.children) > 0 {
path = path[end:] // 移除已提取的参数部分
@ -573,16 +567,16 @@ walk: // 外部循环用于遍历路由树
}
// ... 但我们无法继续
value.tsr = len(path) == end+1 // 如果路径只剩下斜杠则建议 TSR
value.tsr = len(path) == end+1 // 如果路径只剩下斜杠, 则建议 TSR
return value
}
if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath
return value // 如果当前节点有处理函数则返回
return value // 如果当前节点有处理函数, 则返回
}
if len(n.children) == 1 {
// 未找到处理函数。检查是否存在此路径加尾部斜杠的处理函数,以进行 TSR 建议
// 未找到处理函数. 检查是否存在此路径加尾部斜杠的处理函数, 以进行 TSR 建议
n = n.children[0]
value.tsr = (n.path == "/" && n.handlers != nil) || (n.path == "" && n.indices == "/")
}
@ -591,7 +585,7 @@ walk: // 外部循环用于遍历路由树
case catchAll: // 捕获所有节点
// 保存参数值
if params != nil {
// 如果需要预分配容量
// 如果需要, 预分配容量
if cap(*params) < int(globalParamsCount) {
newParams := make(Params, len(*params), globalParamsCount)
copy(newParams, *params)
@ -611,7 +605,7 @@ walk: // 外部循环用于遍历路由树
}
}
(*value.params)[i] = Param{ // 存储参数
Key: n.path[2:], // 参数键名(去除星号)
Key: n.path[2:], // 参数键名(去除星号)
Value: val, // 参数值
}
}
@ -627,7 +621,7 @@ walk: // 外部循环用于遍历路由树
}
if path == prefix { // 如果路径完全匹配当前节点的前缀
// 如果当前路径不等于 '/' 且节点没有注册的处理函数且最近匹配的节点有子节点
// 如果当前路径不等于 '/' 且节点没有注册的处理函数, 且最近匹配的节点有子节点
// 当前节点需要回溯到最后一个有效的 skippedNode
if n.handlers == nil && path != "/" {
for length := len(*skippedNodes); length > 0; length-- {
@ -644,26 +638,26 @@ walk: // 外部循环用于遍历路由树
}
}
}
// 我们应该已经到达包含处理函数的节点
// 检查此节点是否注册了处理函数
// 我们应该已经到达包含处理函数的节点.
// 检查此节点是否注册了处理函数.
if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath
return value // 如果有处理函数则返回
return value // 如果有处理函数, 则返回
}
// 如果此路由没有处理函数,但此路由有通配符子节点,
// 则此路径必须有一个带有额外尾部斜杠的处理函数
// 如果此路由没有处理函数, 但此路由有通配符子节点,
// 则此路径必须有一个带有额外尾部斜杠的处理函数.
if path == "/" && n.wildChild && n.nType != root {
value.tsr = true // 建议 TSR
return value
}
if path == "/" && n.nType == static {
value.tsr = true // 如果是静态节点且路径是根则建议 TSR
value.tsr = true // 如果是静态节点且路径是根, 则建议 TSR
return value
}
// 未找到处理函数。检查此路径加尾部斜杠是否存在处理函数,以进行尾部斜杠重定向建议
// 未找到处理函数. 检查此路径加尾部斜杠是否存在处理函数, 以进行尾部斜杠重定向建议
for i, c := range []byte(n.indices) {
if c == '/' { // 如果索引中包含 '/'
n = n.children[i] // 移动到对应的子节点
@ -676,11 +670,11 @@ walk: // 外部循环用于遍历路由树
return value
}
// 未找到。我们可以建议重定向到相同 URL添加一个额外的尾部斜杠
// 如果该路径的叶节点存在
// 未找到. 我们可以建议重定向到相同 URL, 添加一个额外的尾部斜杠,
// 如果该路径的叶节点存在.
value.tsr = path == "/" || // 如果路径是根路径
(len(prefix) == len(path)+1 && prefix[len(path)] == '/' && // 或者前缀比路径多一个斜杠
path == prefix[:len(prefix)-1] && n.handlers != nil) // 且路径是前缀去掉最后一个斜杠且有处理函数
path == prefix[:len(prefix)-1] && n.handlers != nil) // 且路径是前缀去掉最后一个斜杠, 且有处理函数
// 回溯到最后一个有效的 skippedNode
if !value.tsr && path != "/" {
@ -703,17 +697,17 @@ walk: // 外部循环用于遍历路由树
}
}
// findCaseInsensitivePath 对给定路径进行不区分大小写的查找,并尝试找到处理函数。
// 它还可以选择修复尾部斜杠
// 它返回大小写校正后的路径和一个布尔值,指示查找是否成功。
// findCaseInsensitivePath 对给定路径进行不区分大小写的查找, 并尝试找到处理函数.
// 它还可以选择修复尾部斜杠.
// 它返回大小写校正后的路径和一个布尔值, 指示查找是否成功.
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) {
const stackBufSize = 128 // 栈上缓冲区的默认大小
// 在常见情况下使用栈上静态大小的缓冲区
// 如果路径太长,则在堆上分配缓冲区。
// 在常见情况下使用栈上静态大小的缓冲区.
// 如果路径太长, 则在堆上分配缓冲区.
buf := make([]byte, 0, stackBufSize)
if length := len(path) + 1; length > stackBufSize {
buf = make([]byte, 0, length) // 如果路径太长则分配更大的缓冲区
buf = make([]byte, 0, length) // 如果路径太长, 则分配更大的缓冲区
}
ciPath := n.findCaseInsensitivePathRec(
@ -726,7 +720,7 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]by
return ciPath, ciPath != nil // 返回校正后的路径和是否成功找到
}
// shiftNRuneBytes 将字节数组中的字节向左移动 n 个字节
// shiftNRuneBytes 将字节数组中的字节向左移动 n 个字节.
func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
switch n {
case 0:
@ -742,12 +736,12 @@ func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
}
}
// findCaseInsensitivePathRec 由 n.findCaseInsensitivePath 使用的递归不区分大小写查找函数
// findCaseInsensitivePathRec 由 n.findCaseInsensitivePath 使用的递归不区分大小写查找函数.
func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) []byte {
npLen := len(n.path) // 当前节点的路径长度
walk: // 外部循环用于遍历路由树
// 只要剩余路径长度大于等于当前节点路径长度,且当前节点路径(除第一个字符外)不区分大小写匹配剩余路径
// 只要剩余路径长度大于等于当前节点路径长度, 且当前节点路径(除第一个字符外)不区分大小写匹配剩余路径
for len(path) >= npLen && (npLen == 0 || strings.EqualFold(path[1:npLen], n.path[1:])) {
// 将公共前缀添加到结果中
oldPath := path // 保存原始路径
@ -755,13 +749,13 @@ walk: // 外部循环用于遍历路由树
ciPath = append(ciPath, n.path...) // 将当前节点的路径添加到不区分大小写路径中
if len(path) == 0 { // 如果路径已完全匹配
// 我们应该已经到达包含处理函数的节点
// 检查此节点是否注册了处理函数
// 我们应该已经到达包含处理函数的节点.
// 检查此节点是否注册了处理函数.
if n.handlers != nil {
return ciPath // 如果有处理函数则返回校正后的路径
return ciPath // 如果有处理函数, 则返回校正后的路径
}
// 未找到处理函数
// 未找到处理函数.
// 尝试通过添加尾部斜杠来修复路径
if fixTrailingSlash {
for i, c := range []byte(n.indices) {
@ -775,11 +769,11 @@ walk: // 外部循环用于遍历路由树
}
}
}
return nil // 未找到返回 nil
return nil // 未找到, 返回 nil
}
// 如果此节点没有通配符(参数或捕获所有)子节点,
// 我们可以直接查找下一个子节点并继续遍历树
// 如果此节点没有通配符(参数或捕获所有)子节点,
// 我们可以直接查找下一个子节点并继续遍历树.
if !n.wildChild {
// 跳过已处理的 rune 字节
rb = shiftNRuneBytes(rb, npLen)
@ -799,9 +793,9 @@ walk: // 外部循环用于遍历路由树
// 处理一个新的 rune
var rv rune
// 查找 rune 的开始位置
// Runes 最长为 4 字节
// -4 肯定会是另一个 rune
// 查找 rune 的开始位置.
// Runes 最长为 4 字节.
// -4 肯定会是另一个 rune.
var off int
for max_ := min(npLen, 3); off < max_; off++ {
if i := npLen - off; utf8.RuneStart(oldPath[i]) {
@ -822,17 +816,17 @@ walk: // 外部循环用于遍历路由树
for i, c := range []byte(n.indices) {
// 小写匹配
if c == idxc {
// 必须使用递归方法因为大写字节和小写字节都可能作为索引存在
// 必须使用递归方法, 因为大写字节和小写字节都可能作为索引存在
if out := n.children[i].findCaseInsensitivePathRec(
path, ciPath, rb, fixTrailingSlash,
); out != nil {
return out // 如果找到则返回
return out // 如果找到, 则返回
}
break
}
}
// 如果未找到匹配项,则对大写 rune 执行相同操作(如果它不同)
// 如果未找到匹配项, 则对大写 rune 执行相同操作(如果它不同)
if up := unicode.ToUpper(rv); up != lo {
utf8.EncodeRune(rb[:], up) // 将大写 rune 编码到缓冲区
rb = shiftNRuneBytes(rb, off)
@ -850,18 +844,18 @@ walk: // 外部循环用于遍历路由树
}
}
// 未找到。我们可以建议重定向到相同 URL不带尾部斜杠
// 如果该路径的叶节点存在
// 未找到. 我们可以建议重定向到相同 URL, 不带尾部斜杠,
// 如果该路径的叶节点存在.
if fixTrailingSlash && path == "/" && n.handlers != nil {
return ciPath // 如果可以修复尾部斜杠且有处理函数则返回
return ciPath // 如果可以修复尾部斜杠且有处理函数, 则返回
}
return nil // 未找到返回 nil
return nil // 未找到, 返回 nil
}
n = n.children[0] // 移动到通配符子节点(通常是唯一一个)
n = n.children[0] // 移动到通配符子节点(通常是唯一一个)
switch n.nType {
case param: // 参数节点
// 查找参数结束位置'/' 或路径末尾)
// 查找参数结束位置('/' 或路径末尾)
end := 0
for end < len(path) && path[end] != '/' {
end++
@ -870,7 +864,7 @@ walk: // 外部循环用于遍历路由树
// 将参数值添加到不区分大小写路径中
ciPath = append(ciPath, path[:end]...)
// 我们需要继续深入
// 我们需要继续深入!
if end < len(path) {
if len(n.children) > 0 {
// 继续处理子节点
@ -882,45 +876,45 @@ walk: // 外部循环用于遍历路由树
// ... 但我们无法继续
if fixTrailingSlash && len(path) == end+1 {
return ciPath // 如果可以修复尾部斜杠且路径只剩下斜杠则返回
return ciPath // 如果可以修复尾部斜杠且路径只剩下斜杠, 则返回
}
return nil // 未找到返回 nil
return nil // 未找到, 返回 nil
}
if n.handlers != nil {
return ciPath // 如果有处理函数则返回
return ciPath // 如果有处理函数, 则返回
}
if fixTrailingSlash && len(n.children) == 1 {
// 未找到处理函数检查此路径加尾部斜杠是否存在处理函数
// 未找到处理函数. 检查此路径加尾部斜杠是否存在处理函数
n = n.children[0]
if n.path == "/" && n.handlers != nil {
return append(ciPath, '/') // 返回添加斜杠后的路径
}
}
return nil // 未找到返回 nil
return nil // 未找到, 返回 nil
case catchAll: // 捕获所有节点
return append(ciPath, path...) // 返回添加剩余路径后的路径(捕获所有)
return append(ciPath, path...) // 返回添加剩余路径后的路径(捕获所有)
default:
panic("invalid node type") // 无效的节点类型
}
}
// 未找到
// 未找到.
// 尝试通过添加/删除尾部斜杠来修复路径
if fixTrailingSlash {
if path == "/" {
return ciPath // 如果路径是根路径则返回
return ciPath // 如果路径是根路径, 则返回
}
// 如果路径长度比当前节点路径少一个斜杠,且末尾是斜杠,
// 且不区分大小写匹配且当前节点有处理函数
// 如果路径长度比当前节点路径少一个斜杠, 且末尾是斜杠,
// 且不区分大小写匹配, 且当前节点有处理函数
if len(path)+1 == npLen && n.path[len(path)] == '/' &&
strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handlers != nil {
return append(ciPath, n.path...) // 返回添加当前节点路径后的路径
}
}
return nil // 未找到返回 nil
return nil // 未找到, 返回 nil
}

1078
tree_test.go Normal file

File diff suppressed because it is too large Load diff

130
ws.go
View file

@ -1,130 +0,0 @@
package touka
import (
"errors"
"log"
"net/http"
"github.com/gorilla/websocket"
)
// WebSocketHandler 是用户提供的用于处理 WebSocket 连接的函数类型。
// conn 是一个已经完成握手的 WebSocket 连接。
type WebSocketHandler func(c *Context, conn *websocket.Conn)
// WebSocketUpgradeOptions 用于配置 WebSocket 升级中间件。
type WebSocketUpgradeOptions struct {
// Upgrader 是 gorilla/websocket.Upgrader 的实例。
// 用户可以配置 ReadBufferSize, WriteBufferSize, CheckOrigin, Subprotocols 等。
// 如果为 nil将使用一个带有合理默认值的 Upgrader。
Upgrader *websocket.Upgrader
// Handler 是在 WebSocket 成功升级后调用的处理函数。
// 这个字段是必需的。
Handler WebSocketHandler
// OnError 是一个可选的回调函数,用于处理升级过程中发生的错误。
// 如果未提供,错误将导致一个标准的 HTTP 错误响应(例如 400 Bad Request
OnError func(c *Context, status int, err error)
}
// defaultWebSocketUpgrader 返回一个具有合理默认值的 websocket.Upgrader。
func defaultWebSocketUpgrader() *websocket.Upgrader {
return &websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
// CheckOrigin 应该由用户根据其安全需求来配置。
// 默认情况下,如果 Origin 头部存在且与 Host 头部不匹配,会拒绝连接。
// 对于开发,可以暂时设置为 func(r *http.Request) bool { return true }
// 但在生产环境中必须小心配置。
CheckOrigin: func(r *http.Request) bool {
// 简单的同源检查或允许所有 (根据需要调整)
// return r.Header.Get("Origin") == "" || strings.HasPrefix(r.Header.Get("Origin"), "http://"+r.Host) || strings.HasPrefix(r.Header.Get("Origin"), "https://"+r.Host)
return true // 示例:允许所有,生产环境请谨慎
},
}
}
// defaultWebSocketOnError 是默认的错误处理函数。
func defaultWebSocketOnError(c *Context, status int, err error) {
// 使用框架的错误处理机制或简单的字符串响应
// 确保不要写入一个已经开始的响应
if !c.Writer.Written() {
// 返回英文错误信息
errMsg := http.StatusText(status)
if err != nil {
errMsg = err.Error() // 可以考虑是否暴露详细错误
}
c.String(status, "%s", errMsg) // 或者 c.engine.errorHandle.handler(c, status)
}
c.Abort() // 总是中止
}
// WebSocketUpgrade 返回一个 WebSocket 升级中间件。
// 它能自动感知 HTTP/1.1 的 Upgrade 请求和 HTTP/2 的扩展 CONNECT 请求 (RFC 8441)。
func WebSocketUpgrade(opts WebSocketUpgradeOptions) HandlerFunc {
if opts.Handler == nil {
panic("touka: WebSocketUpgradeOptions.Handler cannot be nil")
}
upgrader := opts.Upgrader
if upgrader == nil {
upgrader = defaultWebSocketUpgrader()
}
onError := opts.OnError
if onError == nil {
onError = defaultWebSocketOnError
}
return func(c *Context) {
// 调试日志,查看请求详情
// reqBytes, _ := httputil.DumpRequest(c.Request, true)
// log.Printf("WebSocketUpgrade: Incoming request for path %s:\n%s", c.Request.URL.Path, string(reqBytes))
// log.Printf("Request Proto: %s, Method: %s", c.Request.Proto, c.Request.Method)
// 对于我们的目的,让 gorilla/websocket 的 Upgrade 方法去判断更佳,
// 它已经实现了 RFC 8441 的支持。
// 我们不再需要手动区分 HTTP/1.1 和 HTTP/2 的逻辑,
// gorilla/websocket.Upgrader.Upgrade 会自动处理。
// 它会检查请求是 HTTP/1.1 Upgrade 还是 HTTP/2 CONNECT with :protocol=websocket。
// 对于 HTTP/2Upgrade() 方法不会发送 101而是处理 CONNECT 的 200 OK。
// 它也不会调用 Hijack因为连接已经在 HTTP/2 流上。
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
// 升级失败。gorilla/websocket.Upgrade 会处理错误响应的发送。
// (对于 HTTP/1.1 会是 400/403 等;对于 HTTP/2 也是类似的非 2xx 响应)
var httpErr websocket.HandshakeError
statusCode := http.StatusBadRequest // 默认
if errors.As(err, &httpErr) {
// 尝试获取更具体的错误信息,但状态码可能不直接暴露
}
// 使用英文记录日志
log.Printf("WebSocket upgrade/handshake failed for %s (Proto: %s): %v", c.Request.RemoteAddr, c.Request.Proto, err)
onError(c, statusCode, err)
if !c.IsAborted() {
c.Abort()
}
return
}
// 升级/握手成功
// 使用英文记录日志
log.Printf("WebSocket connection established for %s (Proto: %s)", c.Request.RemoteAddr, c.Request.Proto)
if !c.IsAborted() {
c.Abort() // 确保 HTTP 处理链中止
}
defer func() {
// 使用英文记录日志
log.Printf("Closing WebSocket connection for %s", conn.RemoteAddr())
_ = conn.Close()
}()
opts.Handler(c, conn) // 执行用户定义的 WebSocket 逻辑
}
}