init
This commit is contained in:
commit
70d467d052
97 files changed
+10386
No files matched your search
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
run_if_exists() {
|
||||
local script="$1"
|
||||
echo "---- run ${script} ----"
|
||||
|
||||
if [ -f "$script" ]; then
|
||||
bash "$script"
|
||||
fi
|
||||
}
|
||||
|
||||
inst_module() {
|
||||
local module="$1"
|
||||
echo "======== Install ${module} ========"
|
||||
|
||||
local set_file="./${module}/_set.sh"
|
||||
|
||||
if [ -f "$set_file" ]; then
|
||||
run_if_exists "$set_file"
|
||||
return 0
|
||||
else
|
||||
echo "Failed" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
inst_module "libth"
|
||||
inst_module "libhttp"
|
||||
inst_module "libcrypt"
|
||||
inst_module "core"
|
||||
inst_module "gateway"
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Debian
|
||||
sudo apt update
|
||||
sudo apt install libboost-all-dev libcurl4-gnutls-dev python3 python3-venv python3-pip g++ patchelf
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
#prj
|
||||
users/
|
||||
*.crt
|
||||
*.key
|
||||
*.pem
|
||||
build/
|
||||
build_*/
|
||||
nidus
|
||||
|
||||
# ide
|
||||
.vscode/
|
||||
.arts/
|
||||
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
# Compiled Object files
|
||||
*.slo
|
||||
*.lo
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Compiled Dynamic libraries
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
|
||||
# Fortran module files
|
||||
*.mod
|
||||
*.smod
|
||||
|
||||
# Compiled Static libraries
|
||||
*.lai
|
||||
*.la
|
||||
*.a
|
||||
*.lib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
|
||||
|
||||
|
||||
# cangjie
|
||||
build/
|
||||
target/
|
||||
output/
|
||||
module-resolve.json
|
||||
module-lock.json
|
||||
cjpm.lock
|
||||
.cjpm-history
|
||||
*.macrocall
|
||||
*.gcda
|
||||
*.gcno
|
||||
|
||||
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Compiled Dynamic libraries
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
|
||||
# Fortran module files
|
||||
*.mod
|
||||
*.smod
|
||||
|
||||
# Compiled Static libraries
|
||||
*.lai
|
||||
*.la
|
||||
*.a
|
||||
*.lib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
|
||||
|
||||
|
||||
# 我加的
|
||||
.env/
|
||||
*.db
|
||||
*.api_key
|
||||
*.key
|
||||
*.pem
|
||||
pmcd
|
||||
rzgs_2
|
||||
crt
|
||||
cert
|
||||
|
||||
# 其他
|
||||
log/
|
||||
report/
|
||||
tmp/
|
||||
*.cache
|
||||
*.pyc
|
||||
*.cython
|
||||
#*.jpg
|
||||
#*.png
|
||||
*.pth
|
||||
.eggs/
|
||||
*.egg-info
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# nodejs
|
||||
node_modules/
|
||||
|
||||
#python
|
||||
__pycache__/
|
||||
venv
|
||||
|
||||
# do not ignore the log folder by pmc-mgmt
|
||||
!./mgmt/app/log/
|
||||
|
||||
# go
|
||||
go.mod
|
||||
go.sum
|
||||
|
||||
|
||||
# VS
|
||||
x64
|
||||
.vs
|
||||
+674
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -0,0 +1,88 @@
|
||||
VERSION := 0.0.4
|
||||
|
||||
APP_NAME := nidus
|
||||
|
||||
INSTL_DIRS := /usr/local/bin
|
||||
|
||||
CONF_NAME := init.txt
|
||||
|
||||
CONF_DIR := /etc/Nidus
|
||||
|
||||
SRV_NAME := Nidus.service
|
||||
|
||||
SRV_DIR := /etc/systemd/system
|
||||
|
||||
CPP:= g++
|
||||
|
||||
INCS:= -I include/
|
||||
|
||||
MY_LIBS := -L/usr/lib -lus_th -lus_http
|
||||
LIBS := -lpthread -lssl -lcrypto -lboost_program_options -lboost_json -lboost_thread -lboost_filesystem
|
||||
|
||||
CPPFLAGS := --std=c++20 -O3 -pipe #-Wall -Werror
|
||||
|
||||
BUILD_DIR = build
|
||||
$(shell mkdir -p $(BUILD_DIR))
|
||||
|
||||
#--------
|
||||
all: ${APP_NAME}.out
|
||||
${APP_NAME}.out: $(BUILD_DIR)/main.o $(BUILD_DIR)/PPool.o $(BUILD_DIR)/ProcessTask.o $(BUILD_DIR)/HttpTask.o $(BUILD_DIR)/Tttask.o $(BUILD_DIR)/pmc_mtd.o $(BUILD_DIR)/subsys_call.o
|
||||
$(CPP) $^ $(MY_LIBS) $(LIBS) -o $@
|
||||
|
||||
$(BUILD_DIR)/main.o: src/main.cpp
|
||||
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
|
||||
|
||||
$(BUILD_DIR)/PPool.o: src/main/PPool.cpp
|
||||
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
|
||||
|
||||
$(BUILD_DIR)/ProcessTask.o: src/main/ProcessTask.cpp
|
||||
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
|
||||
|
||||
$(BUILD_DIR)/Tttask.o: src/main/Tttask.cpp
|
||||
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
|
||||
|
||||
$(BUILD_DIR)/HttpTask.o: src/main/HttpTask.cpp
|
||||
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
|
||||
|
||||
$(BUILD_DIR)/pmc_mtd.o: src/main/pmc_mtd.cpp
|
||||
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
|
||||
|
||||
$(BUILD_DIR)/subsys_call.o: src/main/subsys_call.cpp
|
||||
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
|
||||
|
||||
|
||||
|
||||
install: $(APP_NAME).out
|
||||
@echo "正在安装 $(APP_NAME) 到$(INSTL_DIRS)"
|
||||
@
|
||||
#@mkdir -p $(INSTL_DIRS)
|
||||
@cp -f $(APP_NAME).out $(INSTL_DIRS)/$(APP_NAME)
|
||||
#@chmod 755 $(INSTL_DIRS)/pmcd
|
||||
@
|
||||
@echo "正在配置 $(APP_NAME)"
|
||||
@mkdir -p $(CONF_DIR)
|
||||
@cp $(CONF_NAME) $(CONF_DIR)/$(CONF_NAME)
|
||||
@cp $(SRV_NAME) $(SRV_DIR)/$(SRV_NAME)
|
||||
@systemctl daemon-reload
|
||||
@systemctl enable $(SRV_NAME)
|
||||
@systemctl restart $(SRV_NAME)
|
||||
@
|
||||
@echo "安装完成!"
|
||||
|
||||
uninstall:
|
||||
@echo "正在从 $(INSTL_DIRS) 卸载 $(APP_NAME)"
|
||||
@
|
||||
@rm -f $(INSTL_DIRS)/$(APP_NAME)
|
||||
@rm -f $(CONF_DIRS)/$(CONF_NAME)
|
||||
@rm -f -d ${CONF_DIRS}
|
||||
@
|
||||
@systemctl stop $(SRV_NAME)
|
||||
@systemctl disable $(SRV_NAME)
|
||||
@rm -f $(SRV_DIRS)/$(SRV_NAME)
|
||||
@systemctl daemon-reload
|
||||
@
|
||||
@echo "卸载完成!"
|
||||
|
||||
clear:
|
||||
@rm -rvf $(BUILD_DIR)
|
||||
@rm -vf *.out
|
||||
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=PMC Http Interfaces
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=qing
|
||||
Group=qing
|
||||
ExecStart=nidus -i /etc/Nidus/init.txt --serve 5200
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,56 @@
|
||||
# 进程机器(PMC)
|
||||
这是一个可以创建、管理子进程以及模块的程序。
|
||||
|
||||
## pmc是一个什么项目?
|
||||
这是一个可扩展的模块,可以在文件夹下建立单独的模块,pmc核心模块可以托管运行这些模块。
|
||||
|
||||
|
||||
## 为什么要做一个这样的项目?
|
||||
2024年的时候,我想做一个树莓派机器人,准备用python来写控制程序,可是当时python的多线程能力较弱,所以想到使用子系统,就自己写了一个进程托管程序
|
||||
|
||||
|
||||
## 项目的文件结构是怎样的?
|
||||
users用户可以在目录下创建子模块,extra下面还有一些额外的模块。
|
||||
|
||||
请留意模块的README文件
|
||||
|
||||
|
||||
## 如何托管外部模块
|
||||
编辑启动列表程序,常为init.txt
|
||||
|
||||
|
||||
## PMC的安装方式
|
||||
``` bash
|
||||
# 1.安装依赖
|
||||
|
||||
# 安装UniServe的libth包
|
||||
git clone https://github.com/gopsc/UniServe_libth
|
||||
cd UniServe_libth
|
||||
sudo bash _set.sh
|
||||
|
||||
# 安装别的依赖
|
||||
sudo apt update
|
||||
sudo apt install git g++ make
|
||||
sudo apt install libboost-all-dev
|
||||
#sudo apt install libssl-dev
|
||||
#sudo apt install libcurl4-openssl-dev
|
||||
#sudo apt install libcurl4-gnutls-dev
|
||||
|
||||
# 2. 创建项目
|
||||
git clone https://github.com/gopsc/UniServe_core
|
||||
cd UniServe_core
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
## 加密通信机制(废弃)
|
||||
PMC在启动时生成一个AES密钥,然后用一个RSA公钥将其加密发送给访问者。
|
||||
|
||||
然后访问者与客户端之间的交流全部用此AES密钥加密
|
||||
|
||||
|
||||
## 如何使用并发机器运行程序
|
||||
首先应该在模块中创建一个启动脚本,脚本中应该完成工作目录的切换、虚拟环境的激活,以及程序的启动。
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
./nidus.out -i /etc/Nidus/init.txt --serve 5200
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
make clear && make $@
|
||||
make install
|
||||
@@ -0,0 +1,177 @@
|
||||
|
||||
//作者:简单
|
||||
//修改:qing
|
||||
/*
|
||||
* C++17支持了中文变量名,配合define预编译指令可以将代码中文化。
|
||||
*
|
||||
* 在C语言中,变量名只能由字母、数字或是下划线组成。
|
||||
* 由于C++语言兼容C语言,最开始的C++是不能够使用多字节变量名的。
|
||||
*
|
||||
* */
|
||||
#ifndef __中文模式
|
||||
#define __中文模式
|
||||
#include <stdint.h>
|
||||
/*保留字*/
|
||||
#define 使用 using
|
||||
#define 命名空间 namespace
|
||||
#define 取别名 typedef
|
||||
#define 覆盖 override
|
||||
#define 取内存大小 sizeof
|
||||
#define 返回 return
|
||||
#define 如果 if
|
||||
#define 否则 else
|
||||
#define 又或者 else if
|
||||
#define 真 true
|
||||
#define 假 false
|
||||
//#define 且 and
|
||||
//#define 或 or
|
||||
#define 空类型 void
|
||||
#define 常量 const
|
||||
#define 有符号 signed
|
||||
#define 无符号 unsigned
|
||||
#define 文件 FILE
|
||||
#define 文件结束 EOF
|
||||
#define 类 class
|
||||
#define 结构体 struct
|
||||
#define 枚举类型 enum
|
||||
#define 对象 this
|
||||
#define 抛出异常 throw
|
||||
#define 新建 new
|
||||
//#define 新的 new
|
||||
#define 删除 delete
|
||||
#define 跳出 break
|
||||
#define 继续 continue
|
||||
#define 判断循环 while
|
||||
#define 循环判断 do
|
||||
#define 循环判断尾 while
|
||||
#define 分支选择 switch
|
||||
#define 支系 case
|
||||
#define 缺省 default
|
||||
#define 计次循环 for
|
||||
#define 尝试 try
|
||||
#define 获取异常 catch
|
||||
#define 捕获异常 catch
|
||||
#define 退出 exit
|
||||
#define 公开的 public
|
||||
#define 受保护的 protected
|
||||
#define 私有的 private
|
||||
#define 外部的 extern
|
||||
#define 静态的 static
|
||||
#define 虚拟的 virtual
|
||||
|
||||
#define 有号8位整 int8_t
|
||||
#define 无号8位整 uint8_t
|
||||
#define 有号16位整 int16_t
|
||||
#define 无号16位整 uint16_t
|
||||
#define 有号32位整 int32_t
|
||||
#define 无号32位整 uint32_t
|
||||
#define 有号64位整 int64_t
|
||||
#define 无号64位整 uint64_t
|
||||
|
||||
#define 空的 NULL
|
||||
#define 空指针 nullptr
|
||||
#define 整数 int
|
||||
#define 字符 char
|
||||
#define 短整数 short
|
||||
#define 长型 long
|
||||
#define 超长型 long long
|
||||
#define 单精度小数 float
|
||||
#define 双精度小数 double
|
||||
#define 长度单位 size_t
|
||||
#define 布尔 bool
|
||||
#define 逻辑型 bool
|
||||
#define 可变类型 auto
|
||||
#define 自动的 auto
|
||||
#define 适应 auto
|
||||
#define _文件 FILE
|
||||
#define _文件结束 EOF
|
||||
#define _读模式 "r"
|
||||
#define _写模式 "w"
|
||||
#define 模板 template
|
||||
#define 类型名 typename
|
||||
#define 默认的 default
|
||||
#define 被删除 delete
|
||||
#define 入口 main
|
||||
#define 主函数 main
|
||||
#define 或 ||
|
||||
#define 或者 ||
|
||||
#define 而且 &&
|
||||
#define 且 &&
|
||||
#define 等于 ==
|
||||
#define 大于等于 >=
|
||||
#define 小于等于 <=
|
||||
#define 大于 >
|
||||
#define 小于 <
|
||||
#define 不报错的 noexcept
|
||||
#define 不转换的 explicit
|
||||
#define 纯函数的 consteval
|
||||
#define 预求值的 constexpr
|
||||
|
||||
#define _加法重载 operator+
|
||||
#define _取下标重载 operator[]
|
||||
#define _开始 begin
|
||||
#define _结束 end
|
||||
//-----------------------------------------------------------
|
||||
//-----------------------------------------------------------
|
||||
//基本单元
|
||||
#ifdef __cplusplus
|
||||
#include <iostream>
|
||||
|
||||
命名空间 中文标准{
|
||||
|
||||
类 字符串 {
|
||||
公开的:
|
||||
/* 初始化为空字符串 */
|
||||
字符串() {
|
||||
对象->s = "";
|
||||
}
|
||||
/* 以字面量初始化 */
|
||||
字符串(常量 字符* 源) {
|
||||
对象->s = 源;
|
||||
}
|
||||
/* 访问器 */
|
||||
常量 字符* 取值() {
|
||||
返回 s.c_str();
|
||||
}
|
||||
/* */
|
||||
字符串& _加法重载(常量 字符* 乙方) {
|
||||
对象->s += 乙方;
|
||||
返回 *对象;
|
||||
}
|
||||
/* 字符串之间的连接 */
|
||||
字符串& _加法重载(字符串& 乙) {
|
||||
对象->s += 乙.取值();
|
||||
返回 *对象;
|
||||
}
|
||||
私有的:
|
||||
std::string s;
|
||||
};
|
||||
|
||||
类 标准输出{
|
||||
公开的:
|
||||
空类型 调试输出(常量 字符* 消息) {
|
||||
std::cout << 消息;
|
||||
}
|
||||
空类型 调试输出(字符串& 信) {
|
||||
std::cout<< 信.取值();
|
||||
}
|
||||
};
|
||||
|
||||
类 标准输入{
|
||||
公开的:
|
||||
字符串 取字符串(){
|
||||
std::string s;
|
||||
std::cin >> s;
|
||||
返回 字符串(s.c_str());
|
||||
}
|
||||
整数 取整数() {
|
||||
整数 数;
|
||||
std::cin >> 数;
|
||||
返回 数;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
#endif //__cplusplus
|
||||
#endif //__中文模式
|
||||
@@ -0,0 +1,129 @@
|
||||
#pragma once
|
||||
#include "中文化.hpp"
|
||||
#include "异常.hpp"
|
||||
命名空间 中文标准 {
|
||||
模板 <类型名 类型1>
|
||||
类 向量{
|
||||
公开的:
|
||||
|
||||
/* 注意:当长度单位为0时,内部指针是空的 */
|
||||
向量() {
|
||||
对象->指针 = 空指针;
|
||||
对象->容量 = 0;
|
||||
}
|
||||
/* 专门用于处理花括号初始化列表 */
|
||||
向量(常量 std::initializer_list<类型1>& init = {}) {
|
||||
对象->指针 = 新建 类型1[init.size()] {空的};
|
||||
对象->容量 = init.size();
|
||||
长度单位 计数 = 0;
|
||||
计次循环(适应 & 东西: init) {
|
||||
对象->指针[计数++] = 东西;
|
||||
}
|
||||
}
|
||||
|
||||
~向量() {
|
||||
删除 对象->指针;
|
||||
对象->指针 = 空指针;
|
||||
对象->容量 = 0;
|
||||
}
|
||||
/*----------访问器----------*/
|
||||
长度单位 取容量() {
|
||||
返回 对象->容量;
|
||||
}
|
||||
|
||||
布尔 是否为空() {
|
||||
返回 对象->容量 等于 0;
|
||||
}
|
||||
|
||||
/*--------运算符重载--------*/
|
||||
向量& _取下标重载( 常量 长度单位 下标 ) {
|
||||
如果(下标 < 0 或 下标 >= 对象->容量)
|
||||
抛出异常 下标越界{"向量", 对象->容量, 下标};
|
||||
返回 对象->指针[下标];
|
||||
}
|
||||
|
||||
/*--------可范围遍历--------*/
|
||||
类型1* _开始() {
|
||||
返回 对象->指针;
|
||||
}
|
||||
|
||||
类型1* _结束() {
|
||||
返回 对象->指针 + 对象->容量;
|
||||
}
|
||||
|
||||
/*----------变异器----------*/
|
||||
空类型 插入元素(常量 长度单位 位置, 常量 向量& 媒介) {
|
||||
|
||||
/* 越界检查 */
|
||||
如果(位置 < 0 或 位置 > 对象->容量)
|
||||
抛出异常 下标越界{"向量元素插入", 对象->容量+1, 位置};
|
||||
|
||||
适应 长度 = 取容量();
|
||||
适应 新的 = 新建 类型1[长度 + 1] { 空的 };
|
||||
|
||||
/* 插入位置之前 */
|
||||
计次循环(适应 计数 = 0; 计数 < 位置; ++计数)
|
||||
新的[计数] = 对象->指针[计数];
|
||||
|
||||
/* 执行插入 */
|
||||
新的[位置] = 媒介;
|
||||
|
||||
/* 插入位置之后 */
|
||||
计次循环(适应 计数 = 位置 + 1; 计数 <= 长度; ++计数)
|
||||
新的[计数] = 对象->指针[计数 - 1];
|
||||
|
||||
如果(对象->指针) 删除[] 对象->指针; /* 可能为空 */
|
||||
对象->指针 = 新的;
|
||||
}
|
||||
|
||||
空类型 追加元素(常量 向量& 媒介) {
|
||||
适应 长度 = 取容量();
|
||||
适应 新的 = 新建 类型1[长度 +1] {空的};
|
||||
计次循环(适应 计数 = 0; 计数 < 长度; ++计数)
|
||||
新的[计数] = 对象->指针[计数];
|
||||
新的[长度] = 媒介;
|
||||
如果(对象->指针) 删除[] 指针; /* 可能为空 */
|
||||
对象->指针 = 新的;
|
||||
}
|
||||
|
||||
空类型 删除元素(常量 长度单位 位置) {
|
||||
|
||||
/* 越界检查 */
|
||||
如果(位置 < 0 或 位置 >= 对象->容量)
|
||||
抛出异常 下标越界{"向量元素插入", 对象->容量 + 1, 位置};
|
||||
|
||||
适应 长度 = 取容量();
|
||||
适应 新的 = (长度 等于 1)
|
||||
? 空指针 : 新建 类型1[长度 - 1] { 空的 };
|
||||
|
||||
/* 目标位置之前 */
|
||||
计次循环(适应 计数 = 0; 计数 < 位置; ++计数)
|
||||
新的[计数] = 对象->指针[计数];
|
||||
|
||||
/* 目标位置之后 */
|
||||
计次循环(适应 计数 = 位置; 计数 < 长度; ++计数)
|
||||
新的[计数] = 对象->指针[计数 + 1];
|
||||
|
||||
删除[] 对象->指针;
|
||||
对象->指针 = 新的;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*----------------*/
|
||||
//类 迭代器{
|
||||
//公开的:
|
||||
// 迭代器(常量 std::vector<类型1>::iterator& it) {
|
||||
// this->it = it;
|
||||
// }
|
||||
//私有的:
|
||||
// std::vector<类型1>::iterator it;
|
||||
//};
|
||||
/*----------------*/
|
||||
|
||||
|
||||
私有的:
|
||||
类型1 *指针;
|
||||
长度单位 容量;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#include "中文化.hpp"
|
||||
命名空间 中文标准 {
|
||||
类 标准异常 : 公开的 std::exception {
|
||||
公开的:
|
||||
不转换的 标准异常(常量 字符* 信息): 信息(信息) {}
|
||||
常量 字符* what() 常量 不报错的 覆盖 {
|
||||
返回 信息;
|
||||
}
|
||||
私有的:
|
||||
常量 字符* 信息;
|
||||
};
|
||||
|
||||
类 下标越界 : 公开的 标准异常{
|
||||
公开的:
|
||||
下标越界(常量 字符 * 提示, 常量 长度单位 标准值, 常量 长度单位 目前值)
|
||||
: 标准异常(提示), 标准值(标准值), 目前值(目前值) {
|
||||
};
|
||||
长度单位 取标准值() { 返回 标准值; }
|
||||
长度单位 取目前值() { 返回 目前值; }
|
||||
私有的:
|
||||
长度单位 标准值, 目前值;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
#include "中文化.hpp"
|
||||
#include "异常.hpp"
|
||||
#include "顺序表.hpp"
|
||||
命名空间 中文标准 {
|
||||
模板 <类型名 类型1, 长度单位 节点大小 = 64>
|
||||
类 链串 {
|
||||
公开的:
|
||||
/* 初始化为空链串 */
|
||||
链串() {
|
||||
对象->长度 = 0;
|
||||
对象->下一个 = 空指针;
|
||||
对象->上一个 = 空指针;
|
||||
对象->头 = 对象; /* 每一个子节点都记录同一个头结点 */
|
||||
对象->尾 = 对象; /* 为了方便,只有头结点记录尾节点 */
|
||||
}
|
||||
|
||||
链串 (常量 std::initializer_list<类型1>& init = {})
|
||||
{
|
||||
计次循环(适应 & 东西: init) {
|
||||
插入(东西);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 链式析构
|
||||
*
|
||||
* 析构时,会递归析构所有节点。
|
||||
*/
|
||||
~链串() {
|
||||
删除[] 对象->下一个;
|
||||
}
|
||||
|
||||
空类型 插入(常量 类型1& 元素) {
|
||||
链串 *暂时 = 对象->尾;
|
||||
如果(暂时->长度 等于 节点大小)
|
||||
_推();
|
||||
暂时->存储[暂时->长度++] = 元素;
|
||||
}
|
||||
|
||||
长度单位 统计() {
|
||||
长度单位 总数 = 0;
|
||||
链串 *暂时 = 对象->头;
|
||||
判断循环(暂时 != 空指针) {
|
||||
总数 += 暂时->长度;
|
||||
暂时 = 暂时->下一个;
|
||||
}
|
||||
返回 总数;
|
||||
}
|
||||
|
||||
/*
|
||||
* 链串重组
|
||||
*
|
||||
* 将链串重组为顺序表W
|
||||
*/
|
||||
顺序表<类型1> 重组() {
|
||||
顺序表<类型1> 结果(统计());
|
||||
链串 *暂时 = 对象->头;
|
||||
长度单位 计数 = 0;
|
||||
判断循环(暂时 != 空指针) {
|
||||
计次循环(适应 索引 = 0; 索引 < 暂时->长度; ++索引) {
|
||||
结果[计数++] = 暂时->存储[索引];
|
||||
}
|
||||
暂时 = 暂时->下一个;
|
||||
}
|
||||
返回 结果;
|
||||
}
|
||||
|
||||
类 链串异常: 公开的 标准异常{
|
||||
公开的:
|
||||
链串异常(常量 字符* 消息) : 标准异常(消息) {}
|
||||
};
|
||||
私有的:
|
||||
类型1 存储[节点大小];
|
||||
长度单位 长度; /* 存储中的元素数量 */
|
||||
链串 *下一个;
|
||||
链串 *上一个;
|
||||
链串 *头;
|
||||
链串 *尾;
|
||||
|
||||
空类型 _推() {
|
||||
链串*新的;
|
||||
尝试{ 新的 = 新建 链串(); }
|
||||
捕获异常 (std::exception& exp){
|
||||
抛出异常 链串异常{"链串推失败"};
|
||||
}
|
||||
_拼接节点(新的);
|
||||
}
|
||||
|
||||
空类型 _拼接节点(链串* 新的) {
|
||||
对象->尾->下一个 = 新的;
|
||||
新的->上一个 = 对象->尾;
|
||||
对象->尾 = 新的;
|
||||
新的->头 = 对象;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
#include "中文化.hpp"
|
||||
#include "异常.hpp"
|
||||
命名空间 中文标准 {
|
||||
模板<类型名 类型1>
|
||||
类 顺序表 {
|
||||
公开的:
|
||||
顺序表(长度单位 大小) {
|
||||
存储 = 新建 类型1[大小];
|
||||
长度 = 大小;
|
||||
}
|
||||
~顺序表() {
|
||||
删除[] 存储;
|
||||
}
|
||||
类型1 _取下标重载(长度单位 下标) {
|
||||
如果(下标 >= 长度)
|
||||
抛出异常 顺序表异常{"下标超出范围"};
|
||||
返回 存储[下标];
|
||||
}
|
||||
类 顺序表异常: 公开的 标准异常{
|
||||
公开的:
|
||||
顺序表异常(常量 字符* 消息) : 标准异常(消息) {}
|
||||
};
|
||||
私有的:
|
||||
类型1 *存储;
|
||||
长度单位 长度;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/* 日志 */
|
||||
#pragma once
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
#include <chrono>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <ctime>
|
||||
|
||||
enum class LogLevel {
|
||||
DEBUG,
|
||||
INFO,
|
||||
WARNING,
|
||||
ERROR,
|
||||
CRITICAL
|
||||
};
|
||||
|
||||
namespace qing {
|
||||
/*
|
||||
* 这是由Deepseek创作的,用于记录日志的类模块
|
||||
* 简化版本,只输出到控制台
|
||||
*
|
||||
* 优化意见:改为存储日志文件
|
||||
*/
|
||||
class Logger {
|
||||
public:
|
||||
//获取日志实例(单例模式)
|
||||
static Logger& getInstance() {
|
||||
static Logger instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
//初始化日志系统
|
||||
void init(LogLevel minLogLevel = LogLevel::INFO,
|
||||
bool consoleOutput = true) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
if (initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
minLogLevel_ = minLogLevel;
|
||||
consoleOutput_ = consoleOutput;
|
||||
initialized_ = true;
|
||||
}
|
||||
|
||||
// 设置最低日志级别
|
||||
void setMinLogLevel(LogLevel level) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
minLogLevel_ = level;
|
||||
}
|
||||
|
||||
// 日志输出方法
|
||||
void log(LogLevel level, const std::string& message) {
|
||||
if (level < minLogLevel_ || !consoleOutput_) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto in_time_t = std::chrono::system_clock::to_time_t(now);
|
||||
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now.time_since_epoch()) % 1000;
|
||||
|
||||
std::tm tm;
|
||||
localtime_r(&in_time_t, &tm);
|
||||
|
||||
std::stringstream ss;
|
||||
ss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S")
|
||||
<< "." << std::setfill('0') << std::setw(3) << ms.count()
|
||||
<< " [" << logLevelToString(level) << "] "
|
||||
<< message;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::cout << ss.str() << std::endl;
|
||||
}
|
||||
|
||||
Logger(const Logger&) = delete;
|
||||
Logger& operator=(const Logger&) = delete;
|
||||
|
||||
private:
|
||||
Logger() = default;
|
||||
~Logger() = default;
|
||||
|
||||
std::string logLevelToString(LogLevel level) {
|
||||
switch (level) {
|
||||
case LogLevel::DEBUG: return "DEBUG";
|
||||
case LogLevel::INFO: return "INFO";
|
||||
case LogLevel::WARNING: return "WARNING";
|
||||
case LogLevel::ERROR: return "ERROR";
|
||||
case LogLevel::CRITICAL: return "CRITICAL";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
std::mutex mutex_;
|
||||
LogLevel minLogLevel_ = LogLevel::INFO;
|
||||
bool consoleOutput_ = true;
|
||||
bool initialized_ = false;
|
||||
};
|
||||
}
|
||||
|
||||
#define LOG_DEBUG(message) qing::Logger::getInstance().log(LogLevel::DEBUG, message)
|
||||
#define LOG_INFO(message) qing::Logger::getInstance().log(LogLevel::INFO, message)
|
||||
#define LOG_WARNING(message) qing::Logger::getInstance().log(LogLevel::WARNING, message)
|
||||
#define LOG_ERROR(message) qing::Logger::getInstance().log(LogLevel::ERROR, message)
|
||||
#define LOG_CRITICAL(message) qing::Logger::getInstance().log(LogLevel::CRITICAL, message)
|
||||
@@ -0,0 +1,110 @@
|
||||
#pragma once
|
||||
#include <us/Thread.hpp>
|
||||
#include <us/ITask.hpp>
|
||||
#include <us/HttpServer.hpp>
|
||||
namespace qing {
|
||||
namespace http = boost::beast::http;
|
||||
/* ----------------
|
||||
* HttpTask类 - 超文本服务器任务 */
|
||||
class HttpTask: public ITask {
|
||||
public:
|
||||
|
||||
/* 接受一个回调函数,其接受pmc::net::HttpServer对象的引用,在回调函数中对服务器进行配置 */
|
||||
HttpTask(int port, std::function<void(pmc::net::HttpServer& server)> handler);
|
||||
|
||||
/* 在一个线程中判断线程是否正在有效运行 */
|
||||
bool isRunning() override;
|
||||
|
||||
/* 启动该项任务 */
|
||||
void start() override;
|
||||
|
||||
/* 停止该项任务 */
|
||||
void stop() override;
|
||||
|
||||
private:
|
||||
int port;
|
||||
std::function<void(pmc::net::HttpServer& server)> handler;
|
||||
std::unique_ptr<Thread> th; /* ---- 可控线程指针 */
|
||||
std::unique_ptr<pmc::net::HttpServer> server; /* ---- HTTP服务器的指针 ---- */
|
||||
|
||||
/* 初始化线程,定义线程的行为(Http服务器) */
|
||||
void init_thread();
|
||||
};
|
||||
|
||||
/*-----------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
|
||||
/* ================
|
||||
* 进程托管http服务器
|
||||
* ================
|
||||
* 对操作进程列表的http服务器进行配置,即是输入回调函数 */
|
||||
namespace process_http_server {
|
||||
|
||||
/* ---- 注册POST路由 ---- */
|
||||
void registerRoutes(pmc::net::HttpServer& server);
|
||||
|
||||
/**
|
||||
* @brief 解析请求体中的JSON数据
|
||||
* @param body 请求体字符串
|
||||
* @return JSON解析后的属性树
|
||||
*/
|
||||
boost::property_tree::ptree parseJsonBody(const std::string& body);
|
||||
|
||||
/**
|
||||
* @brief 构建JSON响应
|
||||
* @param success 是否成功
|
||||
* @param message 消息
|
||||
* @param data 附加数据
|
||||
* @return JSON字符串
|
||||
*/
|
||||
std::string buildJsonResponse(bool success, const std::string& message,
|
||||
const boost::property_tree::ptree& data = boost::property_tree::ptree());
|
||||
|
||||
/**
|
||||
* @brief 从请求中提取target参数
|
||||
* @param params 查询参数
|
||||
* @return target值(空字符串表示本机)
|
||||
*/
|
||||
std::string getTarget(const std::unordered_map<std::string, std::string>& params);
|
||||
|
||||
/**
|
||||
* @brief 从请求中提取exec参数
|
||||
* @param params 查询参数
|
||||
* @return 要执行的指令
|
||||
*/
|
||||
std::string getExecCommand(const std::unordered_map<std::string, std::string>& params);
|
||||
|
||||
/**
|
||||
* @brief 从请求中提取kill参数
|
||||
* @param params 查询参数
|
||||
* @return 目标进程号
|
||||
*/
|
||||
int getPid(const std::unordered_map<std::string, std::string>& params);
|
||||
|
||||
/* POST方法处理器 */
|
||||
http::response<http::string_body> handleList(
|
||||
const http::request<http::string_body>& req,
|
||||
const std::unordered_map<std::string, std::string>& params);
|
||||
|
||||
http::response<http::string_body> handleExec(
|
||||
const http::request<http::string_body>& req,
|
||||
const std::unordered_map<std::string, std::string>& params);
|
||||
|
||||
http::response<http::string_body> handleKill(
|
||||
const http::request<http::string_body>& req,
|
||||
const std::unordered_map<std::string, std::string>& params);
|
||||
|
||||
/** 内部实现函数 */
|
||||
std::string listProcesses(const std::string& target);
|
||||
std::string execCommand(const std::string& command, const std::string& target);
|
||||
bool killProcess(int pid, const std::string& target);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <list>
|
||||
#include "main/ProcessTask.hh"
|
||||
|
||||
namespace qing {
|
||||
|
||||
|
||||
|
||||
/* -----------------
|
||||
* subprocess pool 子进程池
|
||||
* ----------------
|
||||
* FIXME: 将列表(序列化)功能放到这里来 */
|
||||
class PPool {
|
||||
public:
|
||||
|
||||
/* CREATE PROCESS - 创建进程
|
||||
*
|
||||
* cmd: 启动命令 */
|
||||
void crtp(const std::string& cmd);
|
||||
|
||||
|
||||
/* CLEAR 清理 - 删除所有已经停下的进程 */
|
||||
void clr();
|
||||
|
||||
|
||||
/* 获取进程池的大小 */
|
||||
size_t size() { return pool.size(); }
|
||||
|
||||
|
||||
/* 删除一个进程,需要输入进程的下标。
|
||||
*
|
||||
* FIXME: 输入进程下标似乎不太自然 */
|
||||
void kill(int idx);
|
||||
|
||||
/* 取下标运算符重载 - 取元素对象 */
|
||||
ProcessTask& operator[](const size_t idx);
|
||||
|
||||
|
||||
/* TODO: 实现迭代器和begin()、end()
|
||||
* ...... */
|
||||
|
||||
private:
|
||||
|
||||
/* 进程任务的shared指针的容器 */
|
||||
std::list<std::shared_ptr<ProcessTask>> pool{};
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
#include <boost/process.hpp>
|
||||
#include <us/ITask.hpp>
|
||||
|
||||
namespace qing {
|
||||
|
||||
/* ----------------
|
||||
* 【子进程任务】
|
||||
* ----------------
|
||||
* 输入要执行的命令,构造子进程任务
|
||||
* TODO: 我们可以重定向子进程的输入和输出,实时交互 */
|
||||
class ProcessTask: public ITask {
|
||||
public:
|
||||
|
||||
/* 输入一个字符串(要执行的命令)(但是并没有执行起来) */
|
||||
ProcessTask(std::string cmd): cmd(cmd) {}
|
||||
|
||||
/* 任务是否在运行中 */
|
||||
bool isRunning() override { return p && p->running(); }
|
||||
|
||||
/* 启动任务 */
|
||||
void start() override {
|
||||
if (!isRunning())
|
||||
this->p = std::make_unique<boost::process::child>(cmd); }
|
||||
|
||||
/* 终止该任务(并且重置子进程指针) */
|
||||
void stop() override {
|
||||
if (isRunning()) p->terminate();
|
||||
p.reset();
|
||||
}
|
||||
|
||||
/* 获取该任务(子进程)的命令 */
|
||||
std::string check() { return this->cmd; }
|
||||
|
||||
/* 获取该子进程的pid(进程编号) */
|
||||
pid_t pid() { return p->id(); }
|
||||
|
||||
private:
|
||||
std::unique_ptr<boost::process::child> p; /* 子进程指针 */
|
||||
std::string cmd; /* 该进程执行的命令 */
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
#ifdef __NEW_VER
|
||||
/* ---- [子进程任务]boost.process库升级到了v2 ---- */
|
||||
class ProcessTask: public ITask {
|
||||
public:
|
||||
ProcessTask(std::string cmd): cmd(cmd) {}
|
||||
bool isRunning() override;
|
||||
int getExitCode();
|
||||
void start() override;
|
||||
/* ---- NOTE(202606025-qing): 杀死进程失败只是打印错误信息 ---- */
|
||||
void stop() override;
|
||||
std::string check() {
|
||||
return this->cmd;
|
||||
}
|
||||
pid_t pid() {
|
||||
return p->id();
|
||||
}
|
||||
|
||||
private:
|
||||
boost::asio::io_context ctx; /* 异步上下文 */
|
||||
std::unique_ptr<boost::process::v2::process> p; /* 子进程指针 */
|
||||
boost::system::error_code exit_code; /* 进程退出的代码 */
|
||||
std::string cmd; /* 要执行的命令 */
|
||||
};
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,58 @@
|
||||
#pragma onces
|
||||
#include "tt/Mq.h"
|
||||
namespace qing {
|
||||
|
||||
|
||||
/*
|
||||
* TRANSMIT TASK 通信任务
|
||||
* (创建一个通信伺服器,它收到消息就执行回调)
|
||||
* (长时间伺服运行会不会有问题)
|
||||
*/
|
||||
class Tttask: public ITask {
|
||||
public:
|
||||
|
||||
/* 构造【通信任务】
|
||||
* msglen: 单条消息的长度
|
||||
* msgcnt: 消息伺服长度
|
||||
* callback: 回调函数 */
|
||||
Tttask(const size_t msglen, size_t msgcnt, const sf_t callback);
|
||||
|
||||
/* 启动该通信伺服任务 */
|
||||
void start() override;
|
||||
|
||||
/* 终止这个通信伺服任务
|
||||
*
|
||||
* TODO: 研究一下要不要释放一些资源
|
||||
* TODO: 增加超时 */
|
||||
void stop() override {
|
||||
if (this->isRunning())
|
||||
th->WaitClose();
|
||||
}
|
||||
|
||||
|
||||
/* 该任务是否在运行
|
||||
*
|
||||
* FIXME: 对START状态的处理还应该考虑
|
||||
* FIXME: 如果在STOP状态而没有stop()任务,可能会造成线程未关闭,一直占用资源? */
|
||||
bool isRunning() override;
|
||||
|
||||
|
||||
private:
|
||||
std::unique_ptr<Thread> th; /* 可控线程指针 */
|
||||
std::unique_ptr<sf_t> callback; /* 回调行为(接收之后)指针 */
|
||||
std::unique_ptr<Mq> mq; /* 消息队列指针 */
|
||||
size_t msglen=0, msgcnt=0; /* 消息的缓存规格 */
|
||||
std::string name; /* 消息队列名(进程号) */
|
||||
|
||||
|
||||
|
||||
|
||||
/* 初始化通信伺服线程,
|
||||
* 初始化方法就是通过传入一些函数,
|
||||
* 构建线程 */
|
||||
void init_thread();
|
||||
|
||||
|
||||
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include <iostream>
|
||||
namespace qing {
|
||||
/* ----------------
|
||||
* 进程托管服务方法集
|
||||
* ----------------
|
||||
* 直接对进程组列表进行操作
|
||||
*
|
||||
*/
|
||||
namespace pmc_mtd
|
||||
{
|
||||
auto list() -> std::string; /* 列出所有的进程 */
|
||||
auto kill(const int pid) -> bool; /* 杀死一个进程,需提供PID */
|
||||
auto exec(const std::string& cmd) -> bool; /* 执行一个进程,输入要执行的命令行 */
|
||||
auto parse_self_init_list(std::string &path) -> void; /* 解析自启动脚本 */
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
namespace qing {
|
||||
|
||||
/* ----------------
|
||||
* 子系统调用
|
||||
* ----------------
|
||||
* 通过进程间通信与子系统进行通讯,调用不同的功能
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
namespace subsys_call
|
||||
{
|
||||
std::string send_list_and_recv_pipe(const std::string& name_mq);
|
||||
std::string send_start_and_recv_pipe(const std::string& name_mq, const std::string& exec);
|
||||
std::string send_kill_and_recv_pipe(const std::string& name_mq, const int kill);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 目前是Message Queue 的发送端(不涉及创建消息队列)
|
||||
*
|
||||
* FIXME: 集成发送、接收和创建
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <boost/interprocess/ipc/message_queue.hpp>
|
||||
|
||||
namespace qing {
|
||||
namespace bip = boost::interprocess;
|
||||
using sf_t = std::function<void(const char*)>; /* 通信成功的回调函数 success */
|
||||
using ef_t = std::function<void(void)>; /* 通信失败的回调函数 error */
|
||||
class Mq {
|
||||
public:
|
||||
enum {CREATOR=0, USER=1};
|
||||
Mq(const std::string& name, int msglen, size_t msgcnt, int mode = USER) {
|
||||
this->name = std::string("/") + name;
|
||||
this->msglen = msglen;
|
||||
this->msgcnt = msgcnt;
|
||||
this->mode = mode;
|
||||
switch (mode) {
|
||||
case USER:
|
||||
mqp = std::make_unique<bip::message_queue>(bip::open_only, this->name.c_str());
|
||||
break;
|
||||
case CREATOR: /* 由创建者创建消息队列 */
|
||||
bip::message_queue::remove(this->name.c_str());
|
||||
mqp = std::make_unique<bip::message_queue>(
|
||||
bip::create_only, this->name.c_str(),
|
||||
msgcnt, msglen * sizeof(char)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* 创建者负责清理 */
|
||||
~Mq() {
|
||||
if (mode == CREATOR)
|
||||
bip::message_queue::remove(name.c_str());
|
||||
}
|
||||
|
||||
/* */
|
||||
Mq(const Mq&) = delete;
|
||||
Mq& operator=(const Mq&) = delete;
|
||||
|
||||
|
||||
/* 发送消息 */
|
||||
void send(std::string &msg)
|
||||
{
|
||||
if (msg.size() + 1 > msglen) /* over the len */
|
||||
throw std::length_error("Message queue Send Length over");
|
||||
|
||||
mqp->send(msg.c_str(), msg.size() + 1, 0); /* include \0 */
|
||||
}
|
||||
|
||||
/* 尝试接收消息 */
|
||||
void recv(sf_t success, ef_t notyet) {
|
||||
|
||||
auto data = std::make_unique<char[]>(msglen); /* 接收缓冲区 */
|
||||
unsigned int priority; /* 优先级 */
|
||||
bip::message_queue::size_type recvd_size;
|
||||
|
||||
try { /* try_receive()返回bool类型 */
|
||||
auto r = mqp->try_receive(
|
||||
data.get(), msglen*sizeof(char),
|
||||
recvd_size, priority
|
||||
);
|
||||
|
||||
if (r) /* 成功接收到数据 */
|
||||
success(data.get());
|
||||
|
||||
else /* 没有收到数据 */
|
||||
notyet();
|
||||
}
|
||||
|
||||
catch (std::exception &exp) {
|
||||
std::cerr << "exception type: " << typeid(exp).name() << std::endl;
|
||||
std::cerr << "exception message: " << exp.what() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
/* */
|
||||
std::unique_ptr<bip::message_queue> mqp;
|
||||
std::string name;
|
||||
size_t msglen=0, msgcnt=0;
|
||||
int mode;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h> /* open() */
|
||||
#include <sys/stat.h> /* mkfifo() */
|
||||
#include <sys/select.h>
|
||||
|
||||
namespace qing {
|
||||
|
||||
/* 20260309(qing): 该类不会抛出异常 */
|
||||
class Pipe{
|
||||
public:
|
||||
static const int CREATOR = 1;
|
||||
static const int USER =2;
|
||||
static const ssize_t PIPE_MAX_LEN = 2048;
|
||||
Pipe(const std::string& path, int mode)
|
||||
: fd(-1), isCreator(mode == CREATOR) {
|
||||
std::string real_path = "/tmp/pst-";
|
||||
real_path += path;
|
||||
fifo_path = real_path;
|
||||
if (isCreator && mkfifo(fifo_path.c_str(), 0666) == -1 && errno != EEXIST) /* wr_wr_wr */
|
||||
perror("mkfifo"); /* 文件已存在是正常情况,其他错误才需要处理 */
|
||||
}
|
||||
|
||||
~Pipe() {
|
||||
/* 如果打开了文件,则关闭文件 */
|
||||
if (fd != -1) close(fd);
|
||||
/**********************/
|
||||
/* 如果管道由该对象创建,就要负责清理 */
|
||||
if (isCreator) unlink(fifo_path.c_str()); /* 创建者负责清理 */
|
||||
}
|
||||
Pipe(const Pipe&) = delete;
|
||||
Pipe& operator=(const Pipe&) = delete;
|
||||
|
||||
/* 这里才是打开文件的指令 - 以读模式打开,可以非阻塞 */
|
||||
bool openForRead(bool nonblock = false) {
|
||||
|
||||
/* 设置是否以非阻塞方式打开文件 */
|
||||
int flags = O_RDONLY;
|
||||
if (nonblock) {
|
||||
flags |= O_NONBLOCK;
|
||||
}
|
||||
|
||||
fd = open(fifo_path.c_str(), flags);
|
||||
|
||||
if (fd == -1) {
|
||||
perror("open for read");
|
||||
return false;
|
||||
}
|
||||
|
||||
else {
|
||||
FD_ZERO(&read_fds);
|
||||
FD_SET(fd, &read_fds);
|
||||
if (fd > max_fd) max_fd = fd;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* 以写模式打开文件 */
|
||||
bool openForWrite(/*bool nonblock = falsei*/) {
|
||||
|
||||
/* 写入管道是不需要阻塞/非阻塞的? */
|
||||
int flags = O_WRONLY;
|
||||
//if (nonblock) {
|
||||
// flags |= O_NONBLOCK;
|
||||
//}
|
||||
fd = open(fifo_path.c_str(), flags);
|
||||
if (fd == -1) {
|
||||
perror("open for write");
|
||||
return false;
|
||||
}
|
||||
|
||||
else {
|
||||
FD_ZERO(&write_fds);
|
||||
FD_SET(fd, &write_fds);
|
||||
if (fd > max_fd) max_fd = fd;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* 写入管道,如果失败返回-1。成功返回写入的字符数。 */
|
||||
ssize_t writePipe(const std::string &data) {
|
||||
if (fd == -1) {
|
||||
std::cerr << "Pipe not opened for writing" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
else {
|
||||
return write(fd, data.c_str(), data.size());
|
||||
}
|
||||
}
|
||||
|
||||
/* 将探测与读取解耦,使用者可以自行决定要不要阻塞 */
|
||||
bool waitForRead(int timeout_sec = 0, int timeout_usec = 0) {
|
||||
|
||||
/* 如果文件没打开,等待是失败的 */
|
||||
if (fd == -1) return false;
|
||||
|
||||
fd_set temp_fds = read_fds;
|
||||
struct timeval timeout = {timeout_sec, timeout_usec};
|
||||
|
||||
int ret = select(max_fd + 1, &temp_fds, nullptr, nullptr, &timeout);
|
||||
|
||||
if (ret == -1) {
|
||||
perror("select");
|
||||
return false;
|
||||
}
|
||||
|
||||
else {
|
||||
return ret > 0 && FD_ISSET(fd, &temp_fds);
|
||||
}
|
||||
}
|
||||
|
||||
/* 读取数据到字符串,返回实际读取的字节数 */
|
||||
ssize_t readPipe(std::string *output) {
|
||||
|
||||
if (fd == -1) {
|
||||
std::cerr << "Pipe not opened for reading" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
else
|
||||
|
||||
{
|
||||
|
||||
char buffer[PIPE_MAX_LEN];
|
||||
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
|
||||
if (n >0) {
|
||||
buffer[n] = '\0';
|
||||
*output = buffer;
|
||||
}
|
||||
|
||||
else if (n == 0) {
|
||||
/* 管道写入端关闭 */
|
||||
*output = "";
|
||||
}
|
||||
|
||||
else {
|
||||
perror("read");
|
||||
}
|
||||
|
||||
return n;
|
||||
|
||||
}
|
||||
}
|
||||
private:
|
||||
std::string fifo_path;
|
||||
int fd;
|
||||
bool isCreator;
|
||||
fd_set read_fds;
|
||||
fd_set write_fds;
|
||||
fd_set except_fds;
|
||||
int max_fd;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
bash /home/qing/micro_servers/gateway/_run.sh
|
||||
bash /home/qing/micro_servers/static/_run.sh
|
||||
@@ -0,0 +1,306 @@
|
||||
/* ===============================================
|
||||
* 通用子系统、进程机器、进程托管服务
|
||||
*
|
||||
*
|
||||
* 【主函数文件】
|
||||
*
|
||||
* 直接子系统调用,或者启动一个pmc子系统(可以打开http)。
|
||||
* ===============================================
|
||||
*/
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <list>
|
||||
#include <cstddef>
|
||||
#include <csignal>
|
||||
#include <cctype>
|
||||
#include <iomanip>
|
||||
#include <unistd.h>
|
||||
#include <sys/types.h>
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/program_options.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
|
||||
#include "logs/Logger.hpp"
|
||||
#include <us/Thread.hpp>
|
||||
#include <us/Cv_wait.hpp>
|
||||
#include <us/ITask.hpp>
|
||||
#include "tt/Pipe.h"
|
||||
#include "main/Tttask.hh"
|
||||
#include "main/HttpTask.hh"
|
||||
#include "main/pmc_mtd.hh"
|
||||
#include "main/subsys_call.hh"
|
||||
|
||||
/* 中文模式 Chinese Mode */
|
||||
#include "cn/中文化.hpp"
|
||||
|
||||
/* 静态区 - 存放全局变量 */
|
||||
static const char *PMC_VERSION = "0.0.10"; /* 版本号 */
|
||||
static const size_t MSGLEN = 2048; /* 单条消息的长度 */
|
||||
static const size_t MSGCNT = 100; /* 消息条数 */
|
||||
|
||||
/* ---- 命名空间 ---- */
|
||||
using namespace qing;
|
||||
|
||||
/* ---- 给命令行参数的命名空间取别名 ---- */
|
||||
namespace po = boost::program_options;
|
||||
|
||||
|
||||
void pmc_init(po::variables_map&); /* 初始化pmc并行机器 */
|
||||
void pmc_serve(po::variables_map&); /* pmc的http伺服器模式 */
|
||||
void parse_data_from_mq(std::string&); /* 解析消息队列收到的消息 */
|
||||
|
||||
//-------------------------------------------------------------
|
||||
//-------------------------------------------------------------
|
||||
|
||||
|
||||
/*-------------------------------------------------------------------------*/
|
||||
/*-------------------------------------------------------------------------*/
|
||||
Cv_wait cv = Cv_wait(); /* 条件变量的等待机制(让主程序等待中断信号) */
|
||||
|
||||
auto taskPool = std::vector<std::shared_ptr<ITask>>{}; /* 基层任务池 - !!! 非子进程 !!! */
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*-------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
/* 捕获中断信号,结束线程并且退出 */
|
||||
void signalHandler(int signum)
|
||||
{
|
||||
LOG_INFO("收到终止信号,准备停止服务器...");
|
||||
cv.WakeCv(); /* 唤醒条件信号即关闭程序 */
|
||||
//if (server) server->stop(); /* TODO: 封装成ITask */
|
||||
}
|
||||
|
||||
/*---------------- SIGNAL ACTION ----------------*/
|
||||
//
|
||||
/* 对信号的响应行为 TODO: 我应该系统地学习一下这个库 */
|
||||
void set_signal_action() {
|
||||
|
||||
/* 该结构体用于描述信号的处理方式 */
|
||||
struct sigaction sa;
|
||||
|
||||
|
||||
/* 绑定处理函数 */
|
||||
sa.sa_handler = signalHandler;
|
||||
|
||||
/*
|
||||
* 清空并初始化一个信号掩码集 --------------> 用于指定哪些信号在当前或即将执行的信号处理函数期间应该被阻塞(位掩码)
|
||||
* “在执行这个信号处理函数时,除了当前正在处理的信号外,还希望临时阻塞其他某些信号”
|
||||
* 不阻塞其他信号
|
||||
* 确保在执行期间没有其他特定的信号会被阻塞
|
||||
* 防止处理过程中被其他信号中断
|
||||
*/
|
||||
sigemptyset(&sa.sa_mask);
|
||||
|
||||
/*
|
||||
* 用于设置信号处理函数的行为特性。
|
||||
* 0 - 默认选项
|
||||
* SA_NODEFER - 表示在执行信号处理函数时不允许其他信号被阻塞
|
||||
* SA_RESETHAND - 在信号处理函数返回后,将信号的处理方式重置为默认
|
||||
* SA_NOCLDSTOP - 对于子进程发送的停止信号,父进程不会停止。
|
||||
* SA_NOCLDWAIT - 父进程在等待等待子进程终止时不会因为子进程发送信号而收到通知
|
||||
* SA_SIGINFO - 使用扩展的信号信息,信号处理函数接收三个参数而不是一个
|
||||
*/
|
||||
sa.sa_flags = 0;
|
||||
|
||||
/* 注册信号 */
|
||||
sigaction(SIGINT, &sa, nullptr); /* Ctrl + C */
|
||||
sigaction(SIGTERM, &sa, nullptr); /* kill 或 systemctl stop */
|
||||
|
||||
}
|
||||
|
||||
/*-------------------------------------------------------------------------*/
|
||||
/*-------------------------------------------------------------------------*/
|
||||
/* 入口函数 */
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
/*------------------------------*/
|
||||
/* 设置信号行为*/
|
||||
set_signal_action();
|
||||
|
||||
/*------------------------------*/
|
||||
/* 解析命令行参数 */
|
||||
po::options_description desc("pmc subsystem start options");
|
||||
desc.add_options() /* 定义选项 */
|
||||
("help,h", "display help message") /* 帮助菜单 */
|
||||
("version,v", "display version message") /* 显示版本 */
|
||||
/******** 子系统调用模式 *********/
|
||||
("send,s", po::value<std::string>(), "send a message to pmc sys") /* 输入目标pid */
|
||||
("exec", po::value<std::string>(), "message body") /* 启动一个进程 */
|
||||
("list", "list process all run") /* 列出运行中的进程 */
|
||||
("kill", po::value<int>(), "kill a process") /* 杀死一个进程 */
|
||||
/********* 子系统服务模式 ********/
|
||||
("init,i", po::value<std::string>(), "self-init list") /* 输入自启动列表 */
|
||||
("serve", po::value<int>(), "Run as http server"); /* 启用http伺服器 */
|
||||
|
||||
/* 参数变量映射关系 */
|
||||
po::variables_map vm;
|
||||
|
||||
try { /* 开始解析 */
|
||||
po::store(po::parse_command_line(argc, argv, desc), vm);
|
||||
po::notify(vm);
|
||||
if (vm.count("help")) { /* 获取帮助页面 */
|
||||
std::cout << desc << std::endl;
|
||||
std::cout << "--------- API MAP ---------" << std::endl;
|
||||
std::cout << "Available endpoints:" << std::endl;
|
||||
std::cout << " POST /list?target=optional" << std::endl;
|
||||
std::cout << " POST /exec?command=ls&target=optional" << std::endl;
|
||||
std::cout << " POST /kill?pid=1234&target=optional" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
if (vm.count("version")) { /* 获取版本信息 */
|
||||
std::cout << PMC_VERSION << std::endl;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "ERROR: " << e.what() << std::endl;
|
||||
std::cerr << "using --help to check options message" << std::endl;
|
||||
throw e;
|
||||
}
|
||||
|
||||
/*----------------------------------------*/
|
||||
|
||||
if ( vm.count("send") && vm.count("exec")) {
|
||||
auto send = vm["send"].as<std::string>();
|
||||
auto exec = vm["exec"].as<std::string> ();
|
||||
auto msg = subsys_call::send_start_and_recv_pipe(send, exec);
|
||||
std::cout << msg << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
else if ( vm.count("send") && vm.count("list")){
|
||||
auto send = vm["send"].as<std::string> ();
|
||||
auto msg = subsys_call::send_list_and_recv_pipe(send);
|
||||
std::cout << msg << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
else if (vm.count("send") && vm.count("kill")) {
|
||||
auto send = vm["send"].as<std::string> ();
|
||||
auto kill = vm["kill"].as<int>();
|
||||
auto msg = subsys_call::send_kill_and_recv_pipe(send, kill);
|
||||
std::cout << msg << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 初始化日志模块 */
|
||||
Logger::getInstance().init(LogLevel::DEBUG, true);
|
||||
|
||||
|
||||
/* 列表自启动 */
|
||||
if (vm.count("init")){
|
||||
std::string path = vm["init"].as<std::string>();
|
||||
pmc_mtd::parse_self_init_list(path);
|
||||
}
|
||||
|
||||
|
||||
/**********************************************/
|
||||
|
||||
/* TODO: 抽象出来 */
|
||||
if (vm.count("serve"))
|
||||
pmc_serve(vm);
|
||||
|
||||
|
||||
/* pmc初始化函数 */
|
||||
pmc_init(vm);
|
||||
|
||||
/*
|
||||
* 主线程等待
|
||||
*
|
||||
* 触发器在信号处理那里
|
||||
*/
|
||||
cv.Wait();
|
||||
|
||||
/* 主线程被唤醒,停止所有任务 */
|
||||
for (auto& task: taskPool) {
|
||||
task->stop();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*-----------------------------------------------------------------------------*/
|
||||
/* 初始化子系统 即是启动通信 */
|
||||
void pmc_init(po::variables_map& vm) {
|
||||
|
||||
/*-------------------*/
|
||||
auto ttt = std::make_shared<Tttask> (MSGLEN, MSGCNT,
|
||||
[](const char *data) { /* 处理收到的数据 */
|
||||
auto json_str = std::string(data);
|
||||
parse_data_from_mq(json_str);
|
||||
});
|
||||
ttt->start();
|
||||
taskPool.push_back(ttt);
|
||||
}
|
||||
|
||||
/* http伺服器模式(只监听本地)TODO: 集成为ITask */
|
||||
void pmc_serve(po::variables_map& vm)
|
||||
{
|
||||
int port = vm["serve"].as<int>();
|
||||
auto server = std::make_shared<HttpTask> (port, [](pmc::net::HttpServer& server_) {
|
||||
process_http_server::registerRoutes(server_);
|
||||
});
|
||||
server->start();
|
||||
taskPool.push_back(server);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* 解析从消息队列中接收到的数据,运行在服务端 */
|
||||
void parse_data_from_mq(std::string& json_str)
|
||||
try {
|
||||
boost::json::value jv = boost::json::parse(json_str);
|
||||
std::string namepipe = jv.at("pipe").as_string().c_str();
|
||||
std::string typ = jv.at("type").as_string().c_str();
|
||||
auto pipe = Pipe(namepipe, Pipe::USER);
|
||||
pipe.openForWrite();
|
||||
|
||||
|
||||
if (typ == "list") {
|
||||
auto msg = pmc_mtd::list();
|
||||
pipe.writePipe(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
else if (typ == "kill") {
|
||||
int trg = jv.at("kill").as_int64();
|
||||
auto flag = pmc_mtd::kill(trg);
|
||||
std::string msg = flag ? "OK" : "Nope";
|
||||
pipe.writePipe(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
else if (typ == "exec"){
|
||||
std::string cmd = jv.at("exec").as_string().c_str();
|
||||
std::string msg = (pmc_mtd::exec(cmd)) ? "OK" : "ERR";
|
||||
pipe.writePipe(msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* 捕获异常后,仅仅只是打印出来? */
|
||||
catch (std::exception &exp) {
|
||||
std::cerr << "Exception at: parse_data_from_mq()" << std::endl;
|
||||
std::cerr << "Exception type: " << typeid(exp).name() << std::endl;
|
||||
std::cerr << "Exception info: " << exp.what() << std::endl;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
#include <iostream>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include "main/HttpTask.hh"
|
||||
#include "main/pmc_mtd.hh"
|
||||
#include "main/subsys_call.hh"
|
||||
|
||||
using namespace qing;
|
||||
namespace qing {
|
||||
|
||||
|
||||
/* -------- HttpTask -------- */
|
||||
|
||||
HttpTask::HttpTask(int port, std::function<void(pmc::net::HttpServer& server)> handler)
|
||||
: port(port), handler(handler) {
|
||||
this->th = std::make_unique<Thread> ();
|
||||
init_thread();
|
||||
}
|
||||
|
||||
bool HttpTask::isRunning() {
|
||||
auto stat = th->check();
|
||||
return !(stat == Fsm::Stat::STOP || stat == Fsm::Stat::SHUT);
|
||||
}
|
||||
|
||||
|
||||
void HttpTask::start() {
|
||||
if (!this->isRunning()) {
|
||||
th->Activate(); /* FIXME: 这里获取了资源,不知道会不会造成内存泄漏 */
|
||||
th->WaitStart(); /* TODO: add timeout 增加超时 */
|
||||
}
|
||||
}
|
||||
|
||||
void HttpTask::stop() {
|
||||
this->server->stop();
|
||||
if (this->isRunning())
|
||||
th->WaitClose();
|
||||
}
|
||||
|
||||
void HttpTask::init_thread() {
|
||||
|
||||
/* 构造通信线程 */
|
||||
this->th = std::make_unique<Thread> ();
|
||||
|
||||
/* stop callback 静止事件 */
|
||||
this->th->set_stop( [](Thread& th) -> void {
|
||||
/* 由线程提供的等待状态机发生改变信号的方法 */
|
||||
th.suspend();
|
||||
});
|
||||
|
||||
|
||||
/* start callback 唤醒事件 */
|
||||
this->th->set_wake( [this](Thread& th) -> void {
|
||||
this->server = std::make_unique<pmc::net::HttpServer> ("127.0.0.1", port, "server.crt", "server.key", 4);
|
||||
handler(*this->server);
|
||||
th.run();
|
||||
|
||||
this->server->start(); /* 这里包含了端口启动提示符输出 */
|
||||
this->server->run(); /* 堵死线程,等待服务器运行结束 */
|
||||
});
|
||||
|
||||
|
||||
/* loop callback 循环事件 */
|
||||
this->th->set_loop( [this](Thread& th) -> void {
|
||||
/* 如果服务器是主动退出的 */
|
||||
th.stop(); /* 停止线程,防止跌入死循环 */
|
||||
});
|
||||
|
||||
|
||||
/* clean callback 清理事件 */
|
||||
this->th->set_clear( [this] (Thread& th) -> void {
|
||||
this->server.reset();;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
/*==============================================================*/
|
||||
/* 这些是尚未整理的工具 */
|
||||
|
||||
/* --------------------------- */
|
||||
/* json字符串转boost.ptree
|
||||
* FIXME: 这个方法好像在ProcessManagerService进程托管服务中集成了*/
|
||||
boost::property_tree::ptree parseJsonToPtree(const std::string& jsonStr) {
|
||||
boost::property_tree::ptree pt;
|
||||
try {
|
||||
std::stringstream ss(jsonStr);
|
||||
boost::property_tree::read_json(ss, pt);
|
||||
} catch (const boost::property_tree::json_parser_error& e) {
|
||||
// 解析失败处理
|
||||
std::cerr << "JSON parse error: " << e.what() << std::endl;
|
||||
}
|
||||
return pt;
|
||||
}
|
||||
|
||||
/* --------------------------- */
|
||||
/* url 解码 */
|
||||
static std::string url_decode(const std::string& str) {
|
||||
std::string result;
|
||||
std::istringstream iss(str);
|
||||
char ch;
|
||||
|
||||
while (iss.get(ch)) {
|
||||
if (ch == '%') {
|
||||
int hex;
|
||||
if (iss >> std::hex >> hex) {
|
||||
result += static_cast<char>(hex);
|
||||
}
|
||||
} else if (ch == '+') {
|
||||
result += ' ';
|
||||
} else {
|
||||
result += ch;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/*==============================================================*/
|
||||
|
||||
|
||||
/**
|
||||
* @brief 进程托管服务 - 相关模块
|
||||
*
|
||||
* 提供进程的列表、执行和终止功能
|
||||
* TODO(20260629-qing): 将这个模块合入HttpTask类模块
|
||||
*/
|
||||
|
||||
namespace process_http_server {
|
||||
|
||||
/* ---- 注册POST路由 ---- */
|
||||
void registerRoutes(pmc::net::HttpServer& server) {
|
||||
server.post("/list", [](const auto& req, const auto& params) {
|
||||
return handleList(req, params);
|
||||
});
|
||||
|
||||
server.post("/exec", [](const auto& req, const auto& params) {
|
||||
return handleExec(req, params);
|
||||
});
|
||||
|
||||
server.post("/kill", [](const auto& req, const auto& params) {
|
||||
return handleKill(req, params);
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- 解析json ---- */
|
||||
boost::property_tree::ptree parseJsonBody(const std::string& body) {
|
||||
boost::property_tree::ptree pt;
|
||||
try {
|
||||
std::stringstream ss(body);
|
||||
boost::property_tree::read_json(ss, pt);
|
||||
}
|
||||
catch (...) {
|
||||
/* ---- JSON解析失败,返回空树 ---- */
|
||||
}
|
||||
return pt;
|
||||
}
|
||||
|
||||
std::string buildJsonResponse(bool success, const std::string& message,
|
||||
const boost::property_tree::ptree& data) {
|
||||
boost::property_tree::ptree response;
|
||||
response.put("success", success);
|
||||
response.put("message", message);
|
||||
if (!data.empty()) {
|
||||
response.add_child("data", data);
|
||||
}
|
||||
|
||||
std::stringstream ss;
|
||||
boost::property_tree::write_json(ss, response);
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string getTarget(const std::unordered_map<std::string, std::string>& params) {
|
||||
auto it = params.find("target");
|
||||
if (it != params.end())
|
||||
return url_decode(it->second); // <--- 这里
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string getExecCommand(const std::unordered_map<std::string, std::string>& params) {
|
||||
auto it = params.find("command");
|
||||
if (it != params.end())
|
||||
return url_decode(it->second); // <--- 这里
|
||||
return "";
|
||||
}
|
||||
|
||||
int getPid(const std::unordered_map<std::string, std::string>& params) {
|
||||
auto it = params.find("pid");
|
||||
if (it != params.end()) {
|
||||
std::string decoded = url_decode(it->second); // <--- 这里
|
||||
return std::stoi(decoded);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
http::response<http::string_body> handleList(
|
||||
const http::request<http::string_body>& req,
|
||||
const std::unordered_map<std::string, std::string>& params) {
|
||||
|
||||
http::response<http::string_body> res;
|
||||
res.version(req.version());
|
||||
res.set(http::field::content_type, "application/json");
|
||||
|
||||
try {
|
||||
std::string target = getTarget(params);
|
||||
|
||||
std::string result = listProcesses(target);
|
||||
boost::property_tree::ptree data = parseJsonToPtree(result);
|
||||
|
||||
res.result(http::status::ok);
|
||||
res.body() = buildJsonResponse(true, "Process list retrieved successfully", data);
|
||||
}
|
||||
|
||||
catch (const std::exception& e) {
|
||||
res.result(http::status::internal_server_error);
|
||||
res.body() = buildJsonResponse(false, std::string("Error: ") + e.what());
|
||||
}
|
||||
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
}
|
||||
|
||||
http::response<http::string_body> handleExec(
|
||||
const http::request<http::string_body>& req,
|
||||
const std::unordered_map<std::string, std::string>& params) {
|
||||
|
||||
http::response<http::string_body> res;
|
||||
res.version(req.version());
|
||||
res.set(http::field::content_type, "application/json");
|
||||
|
||||
try {
|
||||
std::string target = getTarget(params);
|
||||
std::string command = getExecCommand(params);
|
||||
|
||||
if (command.empty()) {
|
||||
// 尝试从POST body中获取command
|
||||
auto body_json = parseJsonBody(req.body());
|
||||
command = body_json.get<std::string>("command", "");
|
||||
}
|
||||
|
||||
if (command.empty()) {
|
||||
res.result(http::status::bad_request);
|
||||
res.body() = buildJsonResponse(false, "Missing required parameter: command");
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
}
|
||||
|
||||
// 实现命令执行逻辑
|
||||
std::string result = execCommand(command, target);
|
||||
|
||||
boost::property_tree::ptree data;
|
||||
data.put("command", command);
|
||||
data.put("output", result);
|
||||
|
||||
res.result(http::status::ok);
|
||||
res.body() = buildJsonResponse(true, "Command executed successfully", data);
|
||||
}
|
||||
|
||||
catch (const std::exception& e) {
|
||||
res.result(http::status::internal_server_error);
|
||||
res.body() = buildJsonResponse(false, std::string("Error: ") + e.what());
|
||||
}
|
||||
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
}
|
||||
|
||||
http::response<http::string_body> handleKill(
|
||||
const http::request<http::string_body>& req,
|
||||
const std::unordered_map<std::string, std::string>& params) {
|
||||
|
||||
http::response<http::string_body> res;
|
||||
res.version(req.version());
|
||||
res.set(http::field::content_type, "application/json");
|
||||
|
||||
try {
|
||||
std::string target = getTarget(params);
|
||||
int pid = getPid(params);
|
||||
|
||||
if (pid <= 0 && !req.body().empty()) {
|
||||
// 尝试从POST body中获取pid
|
||||
auto body_json = parseJsonBody(req.body());
|
||||
pid = body_json.get<int>("pid", -1);
|
||||
}
|
||||
|
||||
if (pid <= 0) {
|
||||
res.result(http::status::bad_request);
|
||||
res.body() = buildJsonResponse(false, "Missing or invalid parameter: pid");
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
}
|
||||
|
||||
bool success = killProcess(pid, target);
|
||||
|
||||
if (success) {
|
||||
boost::property_tree::ptree data;
|
||||
data.put("pid", pid);
|
||||
res.result(http::status::ok);
|
||||
res.body() = buildJsonResponse(true, "Process terminated successfully", data);
|
||||
} else {
|
||||
res.result(http::status::internal_server_error);
|
||||
res.body() = buildJsonResponse(false, "Failed to terminate process");
|
||||
}
|
||||
}
|
||||
|
||||
catch (const std::exception& e) {
|
||||
res.result(http::status::internal_server_error);
|
||||
res.body() = buildJsonResponse(false, std::string("Error: ") + e.what());
|
||||
}
|
||||
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
}
|
||||
|
||||
// 如果target为空,在本机执行
|
||||
// 否则在目标服务器执行
|
||||
std::string listProcesses(const std::string& target)
|
||||
{
|
||||
if (target == "")
|
||||
return pmc_mtd::list();
|
||||
else
|
||||
return subsys_call::send_list_and_recv_pipe(target);
|
||||
}
|
||||
|
||||
std::string execCommand(const std::string& command, const std::string& target)
|
||||
{
|
||||
if (target == "")
|
||||
return pmc_mtd::exec(command) ? "OK" :"ERR";
|
||||
else
|
||||
return subsys_call::send_start_and_recv_pipe(target, command); /* 发送启动命令并且接收回复 */
|
||||
}
|
||||
|
||||
bool killProcess(int pid, const std::string& target)
|
||||
{
|
||||
if (target == "")
|
||||
return pmc_mtd::kill(pid);
|
||||
else
|
||||
return subsys_call::send_kill_and_recv_pipe(target, pid) == "OK";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include "main/PPool.hh"
|
||||
|
||||
namespace qing {
|
||||
|
||||
void PPool::crtp(const std::string& cmd) try
|
||||
{
|
||||
auto pt = std::make_shared<ProcessTask>(cmd);
|
||||
pt->start(); /* 直接创建了进程丢进容器 */
|
||||
pool.push_back(pt);
|
||||
}
|
||||
|
||||
/* 如果boost的子进程模块报错,表示子进程创建失败? */
|
||||
catch (boost::system::system_error &exp) {
|
||||
std::cerr << "Failed to execute this command: " << cmd << std::endl;
|
||||
}
|
||||
|
||||
|
||||
void PPool::clr() {
|
||||
auto it = --pool.end(); /* 由最后一个元素开始 */
|
||||
for (; it != pool.begin(); --it) /* 回溯遍历 */
|
||||
if (!(*it)-> isRunning()) { /* 如果任务已经终止 */
|
||||
auto tmp= it; /* 储存当前位置 */
|
||||
it++; /* 返回上一个结点 */
|
||||
pool.erase(tmp); /* 自动释放终止的节点 */
|
||||
}
|
||||
|
||||
/* 循环结束时it == begin(),如果容器为空则end() == begin() */
|
||||
if (pool.size() > 0 && !(*it)->isRunning()) /* */
|
||||
pool.erase(it); /* 如果end() == begin(),此步将会试图删除哨兵 */
|
||||
}
|
||||
|
||||
void PPool::kill(int idx) {
|
||||
|
||||
|
||||
/* 从头部向后计数idx */
|
||||
auto it = pool.begin();
|
||||
int i = 0;
|
||||
for (; i < idx && it != pool.end(); ++i, ++it);
|
||||
|
||||
if (it != pool.end()) { /* 如果找到了目标(因到达idx而停止)(计数器没有指向尾节点) */
|
||||
(*it)->stop(); /* 关闭当前任务 */
|
||||
pool.erase(it); /* 从容器中移除当前任务 */
|
||||
}
|
||||
|
||||
/* 因抵达终点而停下(下标越界) */
|
||||
else {
|
||||
throw std::out_of_range("PPool::kill()");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ProcessTask& PPool::operator[](const size_t idx) {
|
||||
auto it = pool.begin();
|
||||
for (int i = 0; i < idx; ++it, ++i) /* 遍历idx次 */
|
||||
if (it == pool.end()) /* 如果该节点是尾部哨兵(下标越界) */
|
||||
throw std::out_of_range("PPool::operator[]()");
|
||||
return **it; /* 返回当前节点 */
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#include <iostream>
|
||||
#include "main/ProcessTask.hh"
|
||||
|
||||
#ifdef __NEW_VER
|
||||
namespace qing {
|
||||
bool ProcessTask::isRunning() {
|
||||
return p && p->running(exit_code);
|
||||
}
|
||||
int ProcessTask::getExitCode() {
|
||||
return exit_code.value();
|
||||
}
|
||||
void ProcessTask::start() {
|
||||
if (!isRunning()) {
|
||||
auto ccmd = boost::process::v2::shell(cmd);
|
||||
auto exe = ccmd.exe();
|
||||
auto args = ccmd.args();
|
||||
this->p = std::make_unique<boost::process::v2::process>(ctx, exe, args);
|
||||
}
|
||||
}
|
||||
void ProcessTask::stop() {
|
||||
p->terminate(exit_code);
|
||||
if (exit_code) {
|
||||
std::cerr << "杀死进程失败: " << exit_code.message() << std::endl;
|
||||
}
|
||||
else {
|
||||
p.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,69 @@
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <us/Thread.hpp>
|
||||
#include <us/ITask.hpp>
|
||||
#include "main/Tttask.hh"
|
||||
|
||||
namespace qing {
|
||||
Tttask::Tttask(const size_t msglen, size_t msgcnt, const sf_t callback)
|
||||
: msglen(msglen), msgcnt(msgcnt) {
|
||||
this->callback = std::make_unique<sf_t>(callback); /* FIXME: 使用指针似乎有所不妥 */
|
||||
init_thread();
|
||||
}
|
||||
|
||||
void Tttask::start() {
|
||||
if (!this->isRunning()) {
|
||||
|
||||
th->Activate(); /* FIXME: 这里获取了资源,不知道会不会造成内存泄漏 */
|
||||
th->WaitStart(); /* TODO: add timeout 增加超时 */
|
||||
}
|
||||
}
|
||||
|
||||
bool Tttask::isRunning() {
|
||||
auto stat = th->check();
|
||||
return !(stat == Fsm::Stat::STOP
|
||||
|| stat == Fsm::Stat::SHUT);
|
||||
}
|
||||
|
||||
void Tttask::init_thread() {
|
||||
|
||||
/* 构造通信线程 */
|
||||
this->th = std::make_unique<Thread> ();
|
||||
|
||||
/* stop callback 静止事件 */
|
||||
this->th->set_stop( [](Thread& th) -> void {
|
||||
|
||||
/* 由线程提供的等待状态机发生改变信号的方法 */
|
||||
th.suspend();
|
||||
});
|
||||
|
||||
/* start callback 唤醒事件 */
|
||||
this->th->set_wake( [this](Thread& th) -> void {
|
||||
name = std::to_string(getpid()); /* 以进程名构建消息队列 */
|
||||
mq = std::make_unique<Mq> (name, msglen, msgcnt, Mq::CREATOR);
|
||||
th.run();
|
||||
});
|
||||
|
||||
|
||||
/* loop callback 循环事件 */
|
||||
this->th->set_loop( [this](Thread& th) -> void {
|
||||
|
||||
/* 第一个是接收成功的处理事件,第二个是接收失败的处理事件
|
||||
*
|
||||
* TODO: 作为参数传入延时
|
||||
* TODO: 使用标准库的线程延时*/
|
||||
mq->recv(*callback, []() { usleep(10000); });
|
||||
|
||||
});
|
||||
|
||||
|
||||
/* clean callback 清理事件 */
|
||||
this->th->set_clear( [this] (Thread& th) -> void {
|
||||
|
||||
/* 直接重置指针 */
|
||||
mq.reset();
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
|
||||
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include "main/PPool.hh"
|
||||
#include "main/pmc_mtd.hh"
|
||||
|
||||
namespace qing {
|
||||
|
||||
/* -------- 用于存放子进程的进程池 -------- */
|
||||
static PPool ppool;
|
||||
|
||||
/*-------------------------------------------------------------------------*/
|
||||
|
||||
/* --------【pmc method子进程操作】集合 -------- */
|
||||
namespace pmc_mtd {
|
||||
|
||||
/*
|
||||
* 列举出所有子进程。
|
||||
*
|
||||
* {
|
||||
* "pipe": "<int>",
|
||||
* "type": "list",
|
||||
* } */
|
||||
auto list() -> std::string {
|
||||
boost::json::array processes;
|
||||
|
||||
/* 遍历子进程池
|
||||
*
|
||||
* FIXME: 子进程池的迭代器完成后,改为使用for-in迭代 */
|
||||
//ppool.clr() /* 保留已经跑完的进程,因为可能会涉及到输出的提取 *?
|
||||
for (int i = 0; i < ppool.size(); ++i) {
|
||||
processes.push_back({ /* 向json中推入对象 */
|
||||
{"index", i},
|
||||
{"pid", ppool[i].pid()},
|
||||
{"status", ppool[i].check()}
|
||||
});
|
||||
}
|
||||
|
||||
/* 返回序列化的json对象 */
|
||||
return boost::json::serialize(processes);
|
||||
}
|
||||
|
||||
/*
|
||||
* 删除一个子进程,需要提供pid
|
||||
*
|
||||
* {
|
||||
* "pipe": "<integer>",
|
||||
* "type": "kill",
|
||||
* "kill": <integer>
|
||||
* } */
|
||||
auto kill(const int pid) -> bool {
|
||||
|
||||
/* 遍历子进程池对比pid */
|
||||
for (int i=0; i<ppool.size(); ++i)
|
||||
if ( ppool[i].pid() == pid) { /* 寻找目标进程 */
|
||||
ppool.kill(i); /* 杀死该进程 */
|
||||
return true; } /* 成功返回逻辑真 */
|
||||
|
||||
/* 失败(没找到)返回逻辑假 */
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* 启动一个子进程
|
||||
*
|
||||
* {
|
||||
* "pipe": "<integer>",
|
||||
* "type": "exec",
|
||||
* "exec": "<commandline>"
|
||||
* } */
|
||||
auto exec(const std::string& cmd) -> bool try
|
||||
{
|
||||
//ppool.clr(); /* 保留跑完的子进程 */
|
||||
ppool.crtp(cmd);
|
||||
return true; /* 成功返回逻辑真 */
|
||||
}
|
||||
|
||||
/* 如果出现任何异常,返回假 */
|
||||
catch(std::exception& exp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 解析自启动列表、添加自启动项
|
||||
*
|
||||
* 如果在解析过程中出错,只是打印异常? */
|
||||
void parse_self_init_list(std::string &path)
|
||||
try {
|
||||
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open()) { /* ---- 没能打开自启动文件 ---- */
|
||||
std::cerr << "Failed to open self-init list" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
std::string row = "";
|
||||
char ch;
|
||||
while (file.get(ch))
|
||||
|
||||
if (ch == '\n') {
|
||||
|
||||
auto pos = row.find("#");
|
||||
if (pos != -1)
|
||||
row = row.substr(0, pos);
|
||||
if (row != "") { /* trim() */
|
||||
ppool.clr();
|
||||
ppool.crtp(row); /* 创建进程 */
|
||||
row = "";
|
||||
}
|
||||
}
|
||||
|
||||
else row += ch;
|
||||
|
||||
file.close();
|
||||
|
||||
}
|
||||
catch (std::exception &exp) {
|
||||
std::cerr << "Exception at: parse_self_init_list()" << std::endl;
|
||||
std::cerr << "Exception type: " << typeid(exp).name() << std::endl;
|
||||
std::cerr << "Exception info: " << exp.what() << std::endl;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
|
||||
#include "iostream"
|
||||
#include "fstream"
|
||||
#include <boost/json.hpp>
|
||||
#include "tt/Mq.h"
|
||||
#include "tt/Pipe.h"
|
||||
#include "main/subsys_call.hh"
|
||||
|
||||
namespace qing {
|
||||
namespace subsys_call {
|
||||
|
||||
|
||||
/*-------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
/* -------- 向pmc的消息队列发送启动命令并且从管道接收回复 -------- */
|
||||
std::string send_start_and_recv_pipe(const std::string& name_mq, const std::string& exec)
|
||||
try {
|
||||
|
||||
auto name_pipe = std::to_string(getpid());
|
||||
auto mq = Mq(name_mq, 2048, 100, Mq::USER); /* 放在这里避免管道文件的残留 */
|
||||
|
||||
boost::json::object obj;
|
||||
obj["pipe"] = name_pipe;
|
||||
obj["type"] = "exec";
|
||||
obj["exec"] = exec;
|
||||
auto jsonstr = boost::json::serialize(obj);
|
||||
|
||||
|
||||
auto pipe = Pipe(name_pipe, Pipe::CREATOR); /* FIXME: 创建管道文件到/tmp下面 */
|
||||
if (!pipe.openForRead(true)) {
|
||||
std::cerr << "Failed to open pipe" << std::endl;
|
||||
return "ERR";
|
||||
}
|
||||
|
||||
mq.send(jsonstr);
|
||||
if (!pipe.waitForRead(1, 0)) {
|
||||
std::cerr << "Pipe Read Timeout" << std::endl;
|
||||
return "ERR";
|
||||
}
|
||||
|
||||
std::string res = "";
|
||||
if (pipe.readPipe(&res) < 0) {
|
||||
std::cerr << "Failed to read pipe" << std::endl;
|
||||
return "ERR";
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
//catch (boost::interprocess::interprocess_exception &exp) {
|
||||
// std::cerr << "Target Pmc Is Not Found" << std::endl;
|
||||
// return 1;
|
||||
//}
|
||||
|
||||
catch (std::exception &exp) {
|
||||
std::cerr << "Exception at: send_start_and_recv_pipe()" << std::endl;
|
||||
std::cerr << "Exception type: " << typeid(exp).name() << std::endl;
|
||||
std::cerr << "Exception info: " << exp.what() << std::endl;
|
||||
return "ERR";
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* -------- 向目标子系统发送列举命令并收集结果 -------- */
|
||||
std::string send_list_and_recv_pipe(const std::string& name_mq)
|
||||
try {
|
||||
|
||||
auto name_pipe = std::to_string(getpid());
|
||||
auto mq = Mq(name_mq, 2048, 100, Mq::USER);
|
||||
|
||||
boost::json::object obj;
|
||||
obj["pipe"] = name_pipe;
|
||||
obj["type"] = "list";
|
||||
auto jsonstr = boost::json::serialize(obj);
|
||||
auto pipe = Pipe(name_pipe, Pipe::CREATOR);
|
||||
|
||||
if (!pipe.openForRead(true)) {
|
||||
std::cerr << "Failed to open pipe" << std::endl;
|
||||
return "ERR";
|
||||
}
|
||||
|
||||
|
||||
mq.send(jsonstr);
|
||||
if (!pipe.waitForRead(1, 0)) {
|
||||
std::cerr << "Pipe Read Timeout" << std::endl;
|
||||
return "ERR";
|
||||
}
|
||||
|
||||
std::string res = "";
|
||||
if (pipe.readPipe(&res) < 0) {
|
||||
std::cerr << "Failed to read pipe" << std::endl;
|
||||
return "ERR";
|
||||
}
|
||||
|
||||
|
||||
return res;
|
||||
|
||||
}
|
||||
|
||||
//catch (boost::interprocess::interprocess_exception &exp) {
|
||||
// std::cerr << "Target Pmc Is Not Found" << std::endl;
|
||||
// return 1;
|
||||
//}
|
||||
|
||||
catch (std::exception &exp) {
|
||||
std::cerr << "Exception at: send_list_and_recv_pipe()" << std::endl;
|
||||
std::cerr << "Exception type: " << typeid(exp).name() << std::endl;
|
||||
std::cerr << "Exception info: " << exp.what() << std::endl;
|
||||
return "ERR";
|
||||
}
|
||||
|
||||
/* -------- 删除操作调用 -------- */
|
||||
std::string send_kill_and_recv_pipe(const std::string& name_mq, const int kill)
|
||||
try{
|
||||
|
||||
auto name_pipe = std::to_string(getpid());
|
||||
auto mq = Mq(name_mq, 2048, 100, Mq::USER);
|
||||
|
||||
boost::json::object obj;
|
||||
obj["pipe"] = name_pipe;
|
||||
obj["type"] = "kill";
|
||||
obj["kill"] = kill;
|
||||
auto jsonstr = boost::json::serialize(obj);
|
||||
|
||||
auto pipe = Pipe(name_pipe, Pipe::CREATOR);
|
||||
if (!pipe.openForRead(true)) {
|
||||
std::cerr << "Failed to open pipe" << std::endl;
|
||||
return "ERR";
|
||||
}
|
||||
|
||||
mq.send(jsonstr);
|
||||
if (!pipe.waitForRead(1, 0)) {
|
||||
std::cerr << "Pipe Read Timeout" << std::endl;
|
||||
return "ERR";
|
||||
}
|
||||
|
||||
std::string res = "";
|
||||
if (pipe.readPipe(&res) < 0) {
|
||||
std::cerr << "Failed to read pipe" << std::endl;
|
||||
return "ERR";
|
||||
}
|
||||
else return res;
|
||||
|
||||
}
|
||||
|
||||
/* 没有打开目标子系统的消息队列时,会触发这个异常
|
||||
* 但是实际上这里捕获的是进程间通信通用异常(旧版) */
|
||||
//catch (boost::interprocess::interprocess_exception &exp) {
|
||||
// std::cerr << "Target Pmc Is Not Found" << std::endl;
|
||||
// return 1;
|
||||
//}
|
||||
|
||||
/* 这些函数抛出异常后可能导致管道文件残留 */
|
||||
catch (std::exception &exp) {
|
||||
std::cerr << "Exception at: send_kill_and_recv_pipe()" << std::endl;
|
||||
std::cerr << "Exception type: " << typeid(exp).name() << std::endl;
|
||||
std::cerr << "Exception info: " << exp.what() << std::endl;
|
||||
return "ERR";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
#PRJ
|
||||
*.db
|
||||
*.crt
|
||||
*.key
|
||||
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
# Compiled Object files
|
||||
*.slo
|
||||
*.lo
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Linker files
|
||||
*.ilk
|
||||
|
||||
# Debugger Files
|
||||
*.pdb
|
||||
|
||||
# Compiled Dynamic libraries
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
*.so.*
|
||||
|
||||
|
||||
# Fortran module files
|
||||
*.mod
|
||||
*.smod
|
||||
|
||||
# Compiled Static libraries
|
||||
*.lai
|
||||
*.la
|
||||
*.a
|
||||
*.lib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
|
||||
# Build directories
|
||||
build/
|
||||
Build/
|
||||
build-*/
|
||||
|
||||
# CMake generated files
|
||||
CMakeFiles/
|
||||
CMakeCache.txt
|
||||
cmake_install.cmake
|
||||
#Makefile
|
||||
install_manifest.txt
|
||||
compile_commands.json
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.log
|
||||
*.bak
|
||||
*.swp
|
||||
|
||||
# vcpkg
|
||||
vcpkg_installed/
|
||||
|
||||
# debug information files
|
||||
*.dwo
|
||||
|
||||
# test output & cache
|
||||
Testing/
|
||||
.cache/
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 dashun
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,19 @@
|
||||
VERSION := 0.0.1
|
||||
APP_NAME := gateway
|
||||
INSTL_DIR := /usr/local/bin
|
||||
CPP := g++
|
||||
INCS := -I inc/
|
||||
MY_LIBS := -L/usr/lib -lus_http -lus_crypt
|
||||
LIBS := -lcurl -lssl -lsqlite3 -lz -lcrypto -lboost_json -lboost_program_options
|
||||
CPPFLAGS := --std=c++20 -pipe # -Wall -Werror
|
||||
BUILD_DIR = build
|
||||
$(shell mkdir -p $(BUILD_DIR))
|
||||
|
||||
#--------
|
||||
all: $(BUILD_DIR)/$(APP_NAME).out
|
||||
$(BUILD_DIR)/$(APP_NAME).out: $(BUILD_DIR)/main.o
|
||||
$(CPP) $^ $(LIBS) $(MY_LIBS) -o $@
|
||||
$(BUILD_DIR)/main.o: src/main.cpp
|
||||
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
|
||||
clear:
|
||||
@rm -rvf build/
|
||||
@@ -0,0 +1,7 @@
|
||||
# UniServe_gateway2
|
||||
US Framework gateway C++ version
|
||||
|
||||
# Dependancy
|
||||
|
||||
libboost
|
||||
libcurl
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
openssl req -x509 -newkey rsa:2048 -nodes -keyout server.key -out server.crt -days 365 -subj "/CN=localhost"
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
exec ./build/gateway.out -i example.ini #1>/dev/null
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
make clear && make $@
|
||||
@@ -0,0 +1,9 @@
|
||||
[Server]
|
||||
address = 0.0.0.0
|
||||
port = 9201
|
||||
|
||||
[Proxy]
|
||||
default = http://127.0.0.1:9200
|
||||
|
||||
[Database]
|
||||
path = sqlite3.db
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
#include <iostream>
|
||||
#include <exception>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/ini_parser.hpp>
|
||||
namespace property_tree = boost::property_tree;
|
||||
using ptree = property_tree::ptree;
|
||||
namespace qing {
|
||||
|
||||
class ConfIni {
|
||||
public:
|
||||
ConfIni(const std::string &filename) {
|
||||
try {
|
||||
std::cout << "open and parse ini file: " << filename << std::endl;
|
||||
property_tree::ini_parser::read_ini(filename, pt);
|
||||
}
|
||||
catch (const boost::property_tree::ini_parser_error& e) {
|
||||
std::cerr << "INI parsing error: " << e.what() << std::endl;
|
||||
throw IniConfigureFileParseException("INI parsing error!");
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T get(const std::string &path, const T &_defaut) {
|
||||
return pt.get<T>(path, _defaut);
|
||||
}
|
||||
|
||||
class IniConfigureFileParseException: public std::exception {
|
||||
public:
|
||||
explicit IniConfigureFileParseException(const std::string &msg)
|
||||
: message(msg) {}
|
||||
const char* what() const noexcept override {
|
||||
return message.c_str();
|
||||
}
|
||||
private:
|
||||
std::string message;
|
||||
};
|
||||
|
||||
private:
|
||||
ptree pt;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
#ifndef EXCEPTION_BASE_H
|
||||
#define EXCEPTION_BASE_H
|
||||
|
||||
#include <exception>
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <source_location>
|
||||
#include <sstream>
|
||||
#include <type_traits>
|
||||
|
||||
/**
|
||||
* @brief 通用异常基类
|
||||
*
|
||||
* 特性:
|
||||
* 1. 完整的错误信息管理(错误码、消息、位置信息)
|
||||
* 2. 支持异常链(嵌套异常)
|
||||
* 3. 多态支持(clone、自定义消息)
|
||||
* 4. 线程安全的 what() 缓存
|
||||
* 5. 支持 C++20 source_location
|
||||
*/
|
||||
class ExceptionBase : public std::exception {
|
||||
public:
|
||||
// ============ 构造函数 ============
|
||||
|
||||
explicit ExceptionBase(
|
||||
int code = 0,
|
||||
const std::string& message = "",
|
||||
const std::source_location& location = std::source_location::current()
|
||||
) noexcept
|
||||
: m_code(code)
|
||||
, m_message(message)
|
||||
, m_file(location.file_name())
|
||||
, m_line(location.line())
|
||||
, m_function(location.function_name())
|
||||
, m_what_cache()
|
||||
{
|
||||
}
|
||||
|
||||
// 支持从字符串字面量构造
|
||||
explicit ExceptionBase(
|
||||
const std::string& message,
|
||||
const std::source_location& location = std::source_location::current()
|
||||
) noexcept
|
||||
: m_code(0)
|
||||
, m_message(message)
|
||||
, m_file(location.file_name())
|
||||
, m_line(location.line())
|
||||
, m_function(location.function_name())
|
||||
, m_what_cache()
|
||||
{
|
||||
}
|
||||
|
||||
// ============ 拷贝和移动 ============
|
||||
|
||||
ExceptionBase(const ExceptionBase& other) noexcept
|
||||
: std::exception(other)
|
||||
, m_code(other.m_code)
|
||||
, m_message(other.m_message)
|
||||
, m_file(other.m_file)
|
||||
, m_line(other.m_line)
|
||||
, m_function(other.m_function)
|
||||
, m_nested(other.m_nested ? other.m_nested->clone() : nullptr)
|
||||
, m_what_cache()
|
||||
{
|
||||
}
|
||||
|
||||
ExceptionBase(ExceptionBase&& other) noexcept
|
||||
: std::exception(std::move(other))
|
||||
, m_code(other.m_code)
|
||||
, m_message(std::move(other.m_message))
|
||||
, m_file(std::move(other.m_file))
|
||||
, m_line(other.m_line)
|
||||
, m_function(std::move(other.m_function))
|
||||
, m_nested(std::move(other.m_nested))
|
||||
, m_what_cache(std::move(other.m_what_cache))
|
||||
{
|
||||
other.m_code = 0;
|
||||
other.m_line = 0;
|
||||
}
|
||||
|
||||
virtual ~ExceptionBase() noexcept = default;
|
||||
|
||||
// ============ 赋值运算符 ============
|
||||
|
||||
ExceptionBase& operator=(const ExceptionBase& other) noexcept {
|
||||
if (this != &other) {
|
||||
m_code = other.m_code;
|
||||
m_message = other.m_message;
|
||||
m_file = other.m_file;
|
||||
m_line = other.m_line;
|
||||
m_function = other.m_function;
|
||||
m_nested = other.m_nested ? other.m_nested->clone() : nullptr;
|
||||
m_what_cache.clear();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
ExceptionBase& operator=(ExceptionBase&& other) noexcept {
|
||||
if (this != &other) {
|
||||
m_code = other.m_code;
|
||||
m_message = std::move(other.m_message);
|
||||
m_file = std::move(other.m_file);
|
||||
m_line = other.m_line;
|
||||
m_function = std::move(other.m_function);
|
||||
m_nested = std::move(other.m_nested);
|
||||
m_what_cache = std::move(other.m_what_cache);
|
||||
other.m_code = 0;
|
||||
other.m_line = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// ============ 访问器 ============
|
||||
|
||||
int code() const noexcept { return m_code; }
|
||||
const std::string& message() const noexcept { return m_message; }
|
||||
const std::string& file() const noexcept { return m_file; }
|
||||
int line() const noexcept { return m_line; }
|
||||
const std::string& function() const noexcept { return m_function; }
|
||||
const ExceptionBase* nested() const noexcept { return m_nested.get(); }
|
||||
|
||||
// ============ 异常链操作 ============
|
||||
|
||||
void set_nested(std::unique_ptr<ExceptionBase> nested) noexcept {
|
||||
m_nested = std::move(nested);
|
||||
m_what_cache.clear();
|
||||
}
|
||||
|
||||
template<typename ExType>
|
||||
void set_nested(const ExType& nested_ex) noexcept {
|
||||
static_assert(std::is_base_of<ExceptionBase, ExType>::value,
|
||||
"ExType must inherit from ExceptionBase");
|
||||
m_nested = std::make_unique<ExType>(nested_ex);
|
||||
m_what_cache.clear();
|
||||
}
|
||||
|
||||
// ============ std::exception 接口 ============
|
||||
|
||||
const char* what() const noexcept override {
|
||||
if (m_what_cache.empty()) {
|
||||
build_what_message();
|
||||
}
|
||||
return m_what_cache.c_str();
|
||||
}
|
||||
|
||||
// ============ 多态支持 ============
|
||||
|
||||
virtual std::unique_ptr<ExceptionBase> clone() const {
|
||||
return std::make_unique<ExceptionBase>(*this);
|
||||
}
|
||||
|
||||
// ============ 重新抛出 ============
|
||||
|
||||
[[noreturn]] void rethrow() const {
|
||||
throw *this;
|
||||
}
|
||||
|
||||
protected:
|
||||
// ============ 可重写的消息构建方法 ============
|
||||
|
||||
virtual std::string build_message() const {
|
||||
std::string result;
|
||||
|
||||
// 错误码
|
||||
if (m_code != 0) {
|
||||
result += "[Error " + std::to_string(m_code) + "] ";
|
||||
}
|
||||
|
||||
// 错误消息
|
||||
result += m_message;
|
||||
|
||||
// 位置信息
|
||||
if (!m_file.empty()) {
|
||||
result += " (at " + m_file;
|
||||
if (m_line > 0) {
|
||||
result += ":" + std::to_string(m_line);
|
||||
}
|
||||
if (!m_function.empty()) {
|
||||
result += " in " + m_function;
|
||||
}
|
||||
result += ")";
|
||||
}
|
||||
|
||||
// 嵌套异常
|
||||
if (m_nested) {
|
||||
result += "\n Caused by: " + std::string(m_nested->what());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
// ============ 内部方法 ============
|
||||
|
||||
void build_what_message() const noexcept {
|
||||
try {
|
||||
m_what_cache = build_message();
|
||||
} catch (...) {
|
||||
// 降级处理
|
||||
m_what_cache = "Exception: " + m_message;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
// ============ 成员变量 ============
|
||||
|
||||
int m_code;
|
||||
std::string m_message;
|
||||
std::string m_file;
|
||||
int m_line;
|
||||
std::string m_function;
|
||||
std::unique_ptr<ExceptionBase> m_nested;
|
||||
mutable std::string m_what_cache;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 宏定义 - 方案二完整版
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 抛出指定类型的异常(自动添加 source_location)
|
||||
*
|
||||
* 适用场景:子类构造函数与 ExceptionBase 相同
|
||||
*
|
||||
* 使用示例:
|
||||
* THROW_EX(NetworkException, 404, "Server not found")
|
||||
* THROW_EX(DatabaseException, 100, "Connection timeout")
|
||||
*/
|
||||
#define THROW_EX(ExType, code, msg) \
|
||||
throw ExType(code, msg, std::source_location::current())
|
||||
|
||||
/**
|
||||
* @brief 抛出异常(仅消息,不传错误码)
|
||||
*
|
||||
* 适用场景:子类支持只传消息的构造函数
|
||||
*
|
||||
* 使用示例:
|
||||
* THROW_EX_MSG(ValidationException, "Invalid input data")
|
||||
*/
|
||||
#define THROW_EX_MSG(ExType, msg) \
|
||||
throw ExType(msg, std::source_location::current())
|
||||
|
||||
/**
|
||||
* @brief 抛出异常(支持任意构造函数参数)
|
||||
*
|
||||
* 适用场景:子类有额外的构造函数参数
|
||||
*
|
||||
* 使用示例:
|
||||
* // 子类定义
|
||||
* class FileException : public ExceptionBase {
|
||||
* public:
|
||||
* FileException(int code, const std::string& msg,
|
||||
* const std::string& filename,
|
||||
* const std::source_location& loc = std::source_location::current())
|
||||
* : ExceptionBase(code, msg, loc), m_filename(filename) {}
|
||||
* };
|
||||
*
|
||||
* // 使用
|
||||
* THROW_EX_CUSTOM(FileException, 404, "File not found", "/etc/config.txt")
|
||||
*/
|
||||
#define THROW_EX_CUSTOM(ExType, ...) \
|
||||
throw ExType(__VA_ARGS__, std::source_location::current())
|
||||
|
||||
/**
|
||||
* @brief 抛出异常并携带嵌套异常
|
||||
*
|
||||
* 适用场景:需要保留原始异常信息
|
||||
*
|
||||
* 使用示例:
|
||||
* try {
|
||||
* connect_to_server();
|
||||
* } catch (const NetworkException& e) {
|
||||
* THROW_EX_NESTED(ServiceException, 500, "Service failed", e)
|
||||
* }
|
||||
*/
|
||||
#define THROW_EX_NESTED(ExType, code, msg, nested_ex) \
|
||||
do { \
|
||||
auto ex = ExType(code, msg, std::source_location::current()); \
|
||||
ex.set_nested(nested_ex); \
|
||||
throw ex; \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* @brief 抛出异常并携带嵌套异常(支持自定义参数)
|
||||
*
|
||||
* 使用示例:
|
||||
* THROW_EX_NESTED_CUSTOM(FileException, 500, "Read failed",
|
||||
* original_ex, "/data/file.txt")
|
||||
*/
|
||||
#define THROW_EX_NESTED_CUSTOM(ExType, code, msg, nested_ex, ...) \
|
||||
do { \
|
||||
auto ex = ExType(code, msg, __VA_ARGS__, std::source_location::current()); \
|
||||
ex.set_nested(nested_ex); \
|
||||
throw ex; \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* @brief 检查条件,失败则抛出异常
|
||||
*
|
||||
* 使用示例:
|
||||
* THROW_IF(ptr == nullptr, NullPointerException, 100, "Pointer is null")
|
||||
* THROW_IF(data.empty(), ValidationException, 200, "Data cannot be empty")
|
||||
*/
|
||||
#define THROW_IF(cond, ExType, code, msg) \
|
||||
do { \
|
||||
if (cond) { \
|
||||
THROW_EX(ExType, code, msg); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* @brief 检查条件,失败则抛出异常(仅消息)
|
||||
*/
|
||||
#define THROW_IF_MSG(cond, ExType, msg) \
|
||||
do { \
|
||||
if (cond) { \
|
||||
THROW_EX_MSG(ExType, msg); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* @brief 检查条件,失败则抛出异常(自定义参数)
|
||||
*/
|
||||
#define THROW_IF_CUSTOM(cond, ExType, ...) \
|
||||
do { \
|
||||
if (cond) { \
|
||||
THROW_EX_CUSTOM(ExType, __VA_ARGS__); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* @brief 检查条件,失败则抛出异常(带嵌套异常)
|
||||
*/
|
||||
#define THROW_IF_NESTED(cond, ExType, code, msg, nested_ex) \
|
||||
do { \
|
||||
if (cond) { \
|
||||
THROW_EX_NESTED(ExType, code, msg, nested_ex); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* @brief 检查条件,失败则抛出异常(带嵌套异常和自定义参数)
|
||||
*/
|
||||
#define THROW_IF_NESTED_CUSTOM(cond, ExType, code, msg, nested_ex, ...) \
|
||||
do { \
|
||||
if (cond) { \
|
||||
THROW_EX_NESTED_CUSTOM(ExType, code, msg, nested_ex, __VA_ARGS__); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
// ============================================================================
|
||||
// 辅助宏 - 用于格式化错误消息
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 格式化错误消息(使用流式语法)
|
||||
*
|
||||
* 使用示例:
|
||||
* THROW_EX_FMT(NetworkException, 404, "Failed to connect to " << host << ":" << port)
|
||||
*/
|
||||
#define THROW_EX_FMT(ExType, code, msg_expr) \
|
||||
do { \
|
||||
std::ostringstream _oss; \
|
||||
_oss << msg_expr; \
|
||||
THROW_EX(ExType, code, _oss.str()); \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* @brief 格式化错误消息(使用 printf 风格)
|
||||
*
|
||||
* 使用示例:
|
||||
* THROW_EX_PRINTF(NetworkException, 404, "Failed to connect to %s:%d", host.c_str(), port)
|
||||
*/
|
||||
#define THROW_EX_PRINTF(ExType, code, fmt, ...) \
|
||||
do { \
|
||||
char _buf[1024]; \
|
||||
snprintf(_buf, sizeof(_buf), fmt, __VA_ARGS__); \
|
||||
THROW_EX(ExType, code, std::string(_buf)); \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* @brief 条件检查 + 格式化消息
|
||||
*/
|
||||
#define THROW_IF_FMT(cond, ExType, code, msg_expr) \
|
||||
do { \
|
||||
if (cond) { \
|
||||
THROW_EX_FMT(ExType, code, msg_expr); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* @brief 条件检查 + printf 风格格式化
|
||||
*/
|
||||
#define THROW_IF_PRINTF(cond, ExType, code, fmt, ...) \
|
||||
do { \
|
||||
if (cond) { \
|
||||
THROW_EX_PRINTF(ExType, code, fmt, __VA_ARGS__); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
#endif // EXCEPTION_BASE_H
|
||||
@@ -0,0 +1,523 @@
|
||||
#include <us/HttpServer.hpp>
|
||||
#include <us/HttpClient.hpp>
|
||||
#include <us/MyRSA.hpp>
|
||||
#include <us/Base64.hpp>
|
||||
#include <us/SHA256.hpp>
|
||||
#include <us/Sqlite3.hpp>
|
||||
#include <iostream>
|
||||
#include <typeinfo>
|
||||
#include <memory>
|
||||
#include <csignal>
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/program_options.hpp>
|
||||
#include <openssl/provider.h>
|
||||
#include "ExpBase.hpp"
|
||||
#include "Conf.hpp"
|
||||
|
||||
using namespace pmc::net;
|
||||
using namespace qing;
|
||||
namespace json = boost::json;
|
||||
namespace po = boost::program_options;
|
||||
|
||||
constexpr const char *_VER = "0.2.1";
|
||||
|
||||
|
||||
class DatabaseException: public ExceptionBase {
|
||||
public:
|
||||
using ExceptionBase::ExceptionBase;
|
||||
|
||||
std::string build_message() const override {
|
||||
return "[Database]" + ExceptionBase::build_message();
|
||||
}
|
||||
};
|
||||
|
||||
class HttpException: public ExceptionBase {
|
||||
public:
|
||||
HttpException(int code, const std::string& msg,
|
||||
int http_status, const std::string& url,
|
||||
const std::source_location& loc = std::source_location::current())
|
||||
: ExceptionBase(code, msg, loc)
|
||||
, m_http_status(http_status)
|
||||
, m_url(url) {}
|
||||
std::string build_message() const override {
|
||||
return "[Http]" + ExceptionBase::build_message();
|
||||
}
|
||||
int status() const {
|
||||
return m_http_status;
|
||||
}
|
||||
const std::string& url() const {
|
||||
return m_url;
|
||||
}
|
||||
private:
|
||||
int m_http_status;
|
||||
std::string m_url;
|
||||
};
|
||||
|
||||
void db_init();
|
||||
void registerRoutes(HttpServer &server);
|
||||
|
||||
http::response<http::string_body> wrapperHttp(
|
||||
const http::request<http::string_body>& req,
|
||||
const std::unordered_map<std::string, std::string>& params,
|
||||
HttpRequestHandler _main);
|
||||
|
||||
http::response<http::string_body> handleProxy(
|
||||
const http::request<http::string_body>& req,
|
||||
const std::unordered_map<std::string, std::string>& params,
|
||||
const char *method
|
||||
);
|
||||
|
||||
http::response<http::string_body> handleLogin(
|
||||
const http::request<http::string_body>& req,
|
||||
const std::unordered_map<std::string, std::string>& params);
|
||||
|
||||
http::response<http::string_body> handleRegister(
|
||||
const http::request<http::string_body>& req,
|
||||
const std::unordered_map<std::string, std::string>& params);
|
||||
|
||||
|
||||
/* headers can't processed */
|
||||
bool is_special_header(const std::string &header);
|
||||
|
||||
/* configure items */
|
||||
static std::string _ADDR;
|
||||
static int _PORT;
|
||||
static std::string _DEFT_URL;
|
||||
static std::string _DB_PATH;
|
||||
static std::string _DEFT_ADMIN_USER = "admin";
|
||||
static std::string _DEFT_ADMIN_PASS = "44112233";
|
||||
|
||||
/* instance items */
|
||||
static std::unique_ptr<HttpServer> _SERVE;
|
||||
static std::unique_ptr<MyRSA::Generator> _KEYGEN;
|
||||
static std::unique_ptr<Sqlite3> _SQLDB;
|
||||
|
||||
void signalHandler(int signum) {
|
||||
|
||||
std::cout << "Receive close signal, prepare to shutdown...\n";
|
||||
if (_SERVE) {
|
||||
_SERVE->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void set_signal_action() {
|
||||
|
||||
struct sigaction sa;
|
||||
sa.sa_handler = signalHandler;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sa.sa_flags = 0;
|
||||
sigaction(SIGINT, &sa, nullptr);
|
||||
sigaction(SIGTERM, &sa, nullptr);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
po::options_description desc("cpp language web gateway");
|
||||
desc.add_options()
|
||||
("help,h", "output help message")
|
||||
("version,v", "output version message")
|
||||
("input,i", po::value<std::string>(), "input a configure text file")
|
||||
;
|
||||
po::variables_map vm;
|
||||
/* parse commandline params */
|
||||
{
|
||||
try {
|
||||
po::store(po::parse_command_line(argc, argv, desc), vm);
|
||||
po::notify(vm);
|
||||
}
|
||||
|
||||
catch (const std::exception &e) {
|
||||
std::cerr << "ERROR: " << e.what() << std::endl;
|
||||
std::cerr << "using --help to check options message" << std::endl;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (vm.count("help")) {
|
||||
std::cout << desc << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!vm.count("input")) {
|
||||
std::cerr << "Please input configure file via -i" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* parse configure file */
|
||||
{
|
||||
try {
|
||||
|
||||
ConfIni cf(vm["input"].as<std::string>());
|
||||
_ADDR = cf.get<std::string>("Server.address", "");
|
||||
_PORT = cf.get<int> ("Server.port", -1);
|
||||
_DEFT_URL = cf.get<std::string>("Proxy.default", "");
|
||||
_DB_PATH = cf.get<std::string>("Database.path", "");
|
||||
}
|
||||
catch (ConfIni::IniConfigureFileParseException &e) {
|
||||
std::cerr << "ERROR: " << e.what() << std::endl;
|
||||
std::cerr << "ini format input file error" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (_ADDR == "" || _PORT == -1) {
|
||||
std::cerr << "Please setup addree and port in Serve section in configure file";
|
||||
std::cerr << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
_SERVE = std::make_unique<HttpServer> (_ADDR, _PORT, "full_chain.crt", "server.key", 4);
|
||||
_KEYGEN = std::make_unique<MyRSA::Generator> ();
|
||||
_SQLDB = std::make_unique<Sqlite3> (_DB_PATH);
|
||||
|
||||
db_init();
|
||||
registerRoutes(*_SERVE);
|
||||
set_signal_action();
|
||||
|
||||
_SERVE->start();
|
||||
_SERVE->run();
|
||||
|
||||
}
|
||||
|
||||
void db_init() {
|
||||
|
||||
std::string sql = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='Users'";
|
||||
|
||||
auto flag = false;
|
||||
{
|
||||
auto stmt = _SQLDB->stmt(sql);
|
||||
if (stmt.step()) {
|
||||
auto num = stmt.read_int(0);
|
||||
if (num == 0) {
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (flag) {
|
||||
std::cout << "Start database init..." << std::endl;
|
||||
std::string sql_crtt = "-- create users table, username as email\n"
|
||||
"CREATE TABLE Users (\n"
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
|
||||
"email TEXT UNIQUE NOT NULL,\n"
|
||||
"password_hash TEXT NOT NULL,\n"
|
||||
"create_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n"
|
||||
"last_login DATETIME,\n"
|
||||
"is_active BOOLEAN DEFAULT 1\n"
|
||||
");\n\n"
|
||||
|
||||
"CREATE INDEX idx_users_email ON users(email);\n";
|
||||
|
||||
_SQLDB->exec(sql_crtt);
|
||||
|
||||
{
|
||||
auto password_hash = SHA256::sha256(_DEFT_ADMIN_PASS);
|
||||
std::cout << password_hash << std::endl;
|
||||
std::string sql_crta = "INSERT INTO Users(email, password_hash, last_login) VALUES (?, ?, datetime('now', 'localtime'))";
|
||||
auto stmt = _SQLDB->stmt(sql_crta);
|
||||
stmt.bind_text(1, _DEFT_ADMIN_USER);
|
||||
stmt.bind_text(2, password_hash);
|
||||
if (stmt.step()) {
|
||||
std::cout << stmt.read_text(1) << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "done" << std::endl;
|
||||
}
|
||||
|
||||
else {
|
||||
std::cout << "The database already init" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void registerRoutes(HttpServer &server) {
|
||||
|
||||
server.post("/login", [](
|
||||
const http::request<http::string_body>& req,
|
||||
const std::unordered_map<std::string, std::string>& params
|
||||
) -> http::response<http::string_body> {
|
||||
return wrapperHttp(req, params, [](
|
||||
const auto& req, const auto& params
|
||||
) {
|
||||
return handleLogin(req, params);
|
||||
});
|
||||
});
|
||||
|
||||
server.post("/register", [](const auto& req, const auto& params) {
|
||||
return handleRegister(req, params);
|
||||
});
|
||||
|
||||
server.get("*", [](const auto& req, const auto& params) {
|
||||
return handleProxy(req, params, "get");
|
||||
});
|
||||
|
||||
server.post("*", [](const auto& req, const auto& params) {
|
||||
return handleProxy(req, params, "post");
|
||||
});
|
||||
}
|
||||
|
||||
http::response<http::string_body> wrapperHttp(
|
||||
const http::request<http::string_body>& req,
|
||||
const std::unordered_map<std::string, std::string>& params,
|
||||
HttpRequestHandler _main)
|
||||
|
||||
try {
|
||||
|
||||
return _main(req, params);
|
||||
}
|
||||
|
||||
catch (const std::exception& e) {
|
||||
|
||||
http::response<http::string_body> res;
|
||||
res.version(req.version());
|
||||
const auto& ti = typeid(e);
|
||||
json::value jv_r = {
|
||||
{"success", false},
|
||||
{"message", std::string("<") + ti.name() + "> " + e.what()}
|
||||
};
|
||||
res.set(http::field::content_type, "text/plain");
|
||||
res.result(http::status::internal_server_error);
|
||||
res.body() = json::serialize(jv_r);
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
}
|
||||
|
||||
/* curl -X POST http://127.0.0.1:9201/login -d '{"username": "qing", "password": "12345678"}' */
|
||||
http::response<http::string_body> handleLogin(
|
||||
const http::request<http::string_body>& req,
|
||||
const std::unordered_map<std::string, std::string>& params)
|
||||
{
|
||||
http::response<http::string_body> res;
|
||||
res.version(req.version());
|
||||
|
||||
try {
|
||||
//std::cout << req.body() << std::endl;
|
||||
json::value jv;
|
||||
try {
|
||||
jv = json::parse(req.body());
|
||||
}
|
||||
catch (const boost::wrapexcept<boost::system::system_error>& e) {
|
||||
THROW_EX_CUSTOM(HttpException, 2001, "Invalid_json_Argument",
|
||||
400, "/login");
|
||||
}
|
||||
|
||||
|
||||
if (!jv.is_object()) {
|
||||
THROW_EX_CUSTOM(HttpException, 2002, "Invalid_json_object_Argument",
|
||||
400, "/login");
|
||||
}
|
||||
auto& obj = jv.as_object();
|
||||
auto username = std::string(obj.at("username").as_string());
|
||||
auto password = std::string(obj.at("password").as_string());
|
||||
auto password_hash = SHA256::sha256(password);
|
||||
std::cout << username << " " << password_hash << std::endl;
|
||||
|
||||
std::string sql = "SELECT COUNT(*) FROM Users WHERE email=? AND password_hash=?;";
|
||||
auto stmt = _SQLDB->stmt(sql);
|
||||
stmt.bind_text(1, username);
|
||||
stmt.bind_text(2, password_hash);
|
||||
if (stmt.step() && stmt.read_int(0) == 1) {
|
||||
//update
|
||||
}
|
||||
|
||||
else {
|
||||
THROW_EX_CUSTOM(HttpException, 2101, "Invalid Username or Password",
|
||||
401, "/login");
|
||||
}
|
||||
|
||||
auto _PRIV = MyRSA::Private_Key::from_pem(_KEYGEN->get_private_key_pem());
|
||||
|
||||
|
||||
//int created_at = 0;
|
||||
int keep_time = 3600; // second
|
||||
|
||||
json::value jv_r = {
|
||||
{"user", username},
|
||||
//{"created_at", created_at},
|
||||
{"keep_time", keep_time}
|
||||
};
|
||||
|
||||
std::string data = json::serialize(jv_r);
|
||||
std::string token = Base64::base64_encode(data);
|
||||
std::string sign = _PRIV.Sign(data);
|
||||
|
||||
res.set("Connection", "close");
|
||||
res.result(http::status::ok);
|
||||
res.body() = token + "_" + sign;
|
||||
}
|
||||
|
||||
catch (const HttpException& e) {
|
||||
|
||||
json::value jv_r = {
|
||||
{"success", false},
|
||||
{"message", e.what()}
|
||||
};
|
||||
res.set(http::field::content_type, "application/json");
|
||||
res.result(e.status());
|
||||
res.body() = json::serialize(jv_r);
|
||||
}
|
||||
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
}
|
||||
|
||||
/* curl -X POST http://127.0.0.1:9201/register -d '{"username": "qing", "password": "12345678"}' */
|
||||
/* FIXME: strong password check */
|
||||
http::response<http::string_body> handleRegister(
|
||||
const http::request<http::string_body>& req,
|
||||
const std::unordered_map<std::string, std::string>& params)
|
||||
{
|
||||
http::response<http::string_body> res;
|
||||
res.version(req.version());
|
||||
|
||||
try {
|
||||
|
||||
auto jv = json::parse(req.body());
|
||||
if (!jv.is_object()) {
|
||||
throw std::runtime_error("400");
|
||||
}
|
||||
auto& obj = jv.as_object();
|
||||
auto username = std::string(obj.at("username").as_string());
|
||||
auto password = std::string(obj.at("password").as_string());
|
||||
|
||||
auto password_hash = SHA256::sha256(password);
|
||||
|
||||
std::string sql = "INSERT INTO Users (email, password_hash, last_login) VALUES (?, ?, datetime('now', 'localtime'));";
|
||||
auto stmt = _SQLDB->stmt(sql);
|
||||
stmt.bind_text(1, username);
|
||||
stmt.bind_text(2, password_hash);
|
||||
stmt.step();
|
||||
|
||||
json::value jv_r = {{"success", true}};
|
||||
res.result(http::status::ok);
|
||||
res.body() = json::serialize(jv_r);
|
||||
}
|
||||
//catch
|
||||
|
||||
catch (const std::exception& e) {
|
||||
const auto& ti = typeid(e);
|
||||
res.set(http::field::content_type, "text/plain");
|
||||
res.result(http::status::internal_server_error);
|
||||
res.body() = std::string("<") + ti.name() + "> " + e.what();
|
||||
}
|
||||
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
}
|
||||
|
||||
/* curl -H "Target-Url: https://www.baidu.com" http://127.0.0.1:9201 -X GET */
|
||||
http::response<http::string_body> handleProxy(
|
||||
const http::request<http::string_body>& req,
|
||||
const std::unordered_map<std::string, std::string>& params,
|
||||
const char *method)
|
||||
{
|
||||
http::response<http::string_body> res;
|
||||
res.version(req.version());
|
||||
|
||||
try {
|
||||
auto _PUBL = MyRSA::Public_Key::from_pem(_KEYGEN->get_public_key_pem());
|
||||
|
||||
std::string url = "";
|
||||
auto flag = false;
|
||||
{
|
||||
auto it = req.base().find("Target-Url");
|
||||
if (it != req.base().end()) {
|
||||
url = req["Target-Url"];
|
||||
}
|
||||
|
||||
it = req.base().find("target-url");
|
||||
if (it != req.base().end()) {
|
||||
url = req["target-url"];
|
||||
}
|
||||
|
||||
if (url == "") {
|
||||
url = _DEFT_URL;
|
||||
flag = true;
|
||||
}
|
||||
|
||||
url += "/";
|
||||
url += req.target();
|
||||
}
|
||||
|
||||
/* check auth */
|
||||
std::string auth = "";
|
||||
{
|
||||
auto it = req.base().find("Authorization");
|
||||
if (it == req.base().end() && !flag) {
|
||||
throw std::runtime_error("401");
|
||||
}
|
||||
std::string raw = req["Authorization"];
|
||||
if (raw.substr(0, 7) == "Bearer ") {
|
||||
raw = raw.substr(7, raw.size() - 7);
|
||||
size_t idx = raw.find("_");
|
||||
if (idx == std::string::npos) {
|
||||
throw std::runtime_error("Header 400 Bad Request");
|
||||
}
|
||||
std::string data = raw.substr(0, idx);
|
||||
std::string sign = raw.substr(idx+1, raw.size()-idx-1);
|
||||
auth = Base64::base64_decode_to_string(data);
|
||||
if (!_PUBL.Verify(auth, sign)) {
|
||||
throw std::runtime_error("Header 401 Unauthorized");
|
||||
}
|
||||
std::cout << "auth data: " << auth << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
auto callback = [&res](const auto& ret) {
|
||||
res.body() += ret; // TODO: too big file
|
||||
};
|
||||
|
||||
|
||||
auto client = HttpClient();
|
||||
for (auto const& field: req) {
|
||||
if (is_special_header(field.name_string()))
|
||||
continue;
|
||||
auto h = std::string();
|
||||
h += field.name_string();
|
||||
h += ": ";
|
||||
h += field.value();
|
||||
client.set_header(h);
|
||||
}
|
||||
|
||||
|
||||
if (method == "get")
|
||||
client.get_stream_sync(url, callback);
|
||||
|
||||
else if (method == "post") {
|
||||
std::string payload = req.body();
|
||||
client.post_stream_sync(url, payload, callback);
|
||||
}
|
||||
|
||||
|
||||
for (const auto& pair: client.get_response_headers() ) {
|
||||
res.set(pair.first, pair.second);
|
||||
}
|
||||
res.set("Connection", "close");
|
||||
res.result(client.get_response_code());
|
||||
}
|
||||
//catch
|
||||
|
||||
catch (const std::exception& e) {
|
||||
const auto& ti = typeid(e);
|
||||
res.set(http::field::content_type, "text/plain");
|
||||
res.result(http::status::internal_server_error);
|
||||
json::value jv_r = {
|
||||
{"success", false},
|
||||
{"err_name", ti.name()},
|
||||
{"err_message", e.what()}
|
||||
};
|
||||
res.body() = json::serialize(jv_r);
|
||||
}
|
||||
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
}
|
||||
|
||||
/* they are special because they point to raw request */
|
||||
bool is_special_header(const std::string &header) {
|
||||
|
||||
return header == "Host" || header == "connection"
|
||||
|| header == "Accept-Encoding";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
# Compiled Object files
|
||||
*.slo
|
||||
*.lo
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Linker files
|
||||
*.ilk
|
||||
|
||||
# Debugger Files
|
||||
*.pdb
|
||||
|
||||
# Compiled Dynamic libraries
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
*.so.*
|
||||
|
||||
|
||||
# Fortran module files
|
||||
*.mod
|
||||
*.smod
|
||||
|
||||
# Compiled Static libraries
|
||||
*.lai
|
||||
*.la
|
||||
*.a
|
||||
*.lib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
|
||||
# Build directories
|
||||
build/
|
||||
Build/
|
||||
build-*/
|
||||
|
||||
# CMake generated files
|
||||
CMakeFiles/
|
||||
CMakeCache.txt
|
||||
cmake_install.cmake
|
||||
#Makefile
|
||||
install_manifest.txt
|
||||
compile_commands.json
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.log
|
||||
*.bak
|
||||
*.swp
|
||||
|
||||
# vcpkg
|
||||
vcpkg_installed/
|
||||
|
||||
# debug information files
|
||||
*.dwo
|
||||
|
||||
# test output & cache
|
||||
Testing/
|
||||
.cache/
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 建昌风物大顺脚本
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,45 @@
|
||||
VERSION := 0.0.1
|
||||
FRAMEWORK_NAME := us
|
||||
LIB_NAME := libus_crypt
|
||||
INSTL_TDIR := /lib
|
||||
INCLU_TDIR := /usr/include
|
||||
INCLU_SDIR := inc
|
||||
CPP := g++
|
||||
INCS := -I inc
|
||||
CPPFLAGS := --std=c++20 -O3 -pipe -Wall -Werror
|
||||
LIBS :=
|
||||
BUILD_TDIR := build
|
||||
$(shell mkdir -p $(BUILD_TDIR))
|
||||
|
||||
#--------
|
||||
all: $(BUILD_TDIR)/$(LIB_NAME).so
|
||||
|
||||
$(BUILD_TDIR)/$(LIB_NAME).so: $(BUILD_TDIR)/Crypto_Basic.o
|
||||
$(CPP) -shared $^ $(LIBS) -o $@
|
||||
|
||||
$(BUILD_TDIR)/Crypto_Basic.o: src/Crypto_Basic.cpp
|
||||
$(CPP) $(CPPFLAGS) -fPIC -c $^ $(INCS) -o $@
|
||||
|
||||
clear:
|
||||
@rm -rvf $(BUILD_TDIR)
|
||||
|
||||
install:
|
||||
@echo "install UniServe_libcrypt..."
|
||||
@mkdir -vp $(INCLU_TDIR)/$(FRAMEWORK_NAME)
|
||||
@cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/Crypto_Basic.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/
|
||||
@cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/Base64.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/
|
||||
@cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/SHA256.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/
|
||||
@cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/AES_CBC.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/
|
||||
@cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/MyRSA.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/
|
||||
@cp -vf $(BUILD_TDIR)/$(LIB_NAME).so $(INSTL_TDIR)/
|
||||
@echo "done."
|
||||
|
||||
uninstall:
|
||||
@echo "uninstall UniServe_libcrypt..."
|
||||
@rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/Crypto_Basic.hpp
|
||||
@rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/Base64.hpp
|
||||
@rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/SHA256.hpp
|
||||
@rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/AES_CBC.hpp
|
||||
@rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/MyRSA.hpp
|
||||
@rm -vf $(INSTL_TDIR)/$(LIB_NAME).so
|
||||
@echo "done."
|
||||
@@ -0,0 +1,2 @@
|
||||
# UniServe_libcrypt
|
||||
UniServe Framework Cryption Library
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
make clear
|
||||
make
|
||||
make install
|
||||
@@ -0,0 +1,140 @@
|
||||
#include "Crypto_Basic.hpp"
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace qing {
|
||||
class AES_CBC: public qing::Crypto_Basic {
|
||||
public:
|
||||
/* 构造函数 初始化一个随即的密钥和初始化向量 */
|
||||
AES_CBC() {
|
||||
generate_key_iv(key, iv);
|
||||
}
|
||||
AES_CBC(std::vector<unsigned char> &key, std::vector<unsigned char> &iv) {
|
||||
this->key = key;
|
||||
this->iv = iv;
|
||||
}
|
||||
std::string get_key() {
|
||||
return bytes_to_hex(key);
|
||||
}
|
||||
std::string get_iv() {
|
||||
return bytes_to_hex(iv);
|
||||
}
|
||||
|
||||
std::string encrypt(std::string original_text) {
|
||||
std::vector<unsigned char> plaintext(original_text.begin(), original_text.end());
|
||||
std::vector<unsigned char> ciphertext = aes_encrypt(plaintext, key, iv);
|
||||
|
||||
// hex-bytes转换测试
|
||||
/*std::string hex = bytes_to_hex(ciphertext);
|
||||
std::vector<unsigned char> cipher2 = hex_to_bytes(hex);
|
||||
print_bytes(ciphertext);
|
||||
print_bytes(cipher2);*/
|
||||
|
||||
return bytes_to_hex(ciphertext);
|
||||
}
|
||||
|
||||
std::string decrypt(std::string cipher_hex) {
|
||||
std::vector<unsigned char> ciphertext = hex_to_bytes(cipher_hex);
|
||||
std::vector<unsigned char> decrypted = aes_decrypt(ciphertext, key, iv);
|
||||
return std::string(decrypted.begin(), decrypted.end());
|
||||
}
|
||||
|
||||
static void print_bytes(std::vector<unsigned char>& bytes) {
|
||||
for (unsigned char byte : bytes) {
|
||||
std::cerr << std::setw(2) << static_cast<int>(byte);
|
||||
}
|
||||
std::cerr << std::endl;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<unsigned char> key;
|
||||
std::vector<unsigned char> iv;
|
||||
// 生成随机密钥和IV
|
||||
static void generate_key_iv(std::vector<unsigned char>& key,
|
||||
std::vector<unsigned char>& iv) {
|
||||
key.resize(32); // AES-256 需要 32 字节密钥
|
||||
iv.resize(16); // AES 块大小 16 字节(128-bit)
|
||||
|
||||
// 生成随即密钥
|
||||
CRYPTO_CHECK(RAND_bytes(key.data(), key.size()) == 1, "密钥生成失败");
|
||||
|
||||
// 生成随即IV
|
||||
CRYPTO_CHECK(RAND_bytes(iv.data(), iv.size()) == 1, "IV生成失败");
|
||||
}
|
||||
|
||||
// AES-256-CBC 加密
|
||||
static std::vector<unsigned char> aes_encrypt(const std::vector<unsigned char>& plaintext,
|
||||
const std::vector<unsigned char>& key,
|
||||
const std::vector<unsigned char>& iv) {
|
||||
// 创建上下文
|
||||
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
|
||||
CRYPTO_CHECK(ctx != nullptr, "无法创建加密上下文");
|
||||
|
||||
// 初始化加密操作
|
||||
CRYPTO_CHECK(EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr,
|
||||
key.data(), iv.data()) , "加密初始化失败");
|
||||
|
||||
// 输出缓冲区 (密文可能比明文长一个块)
|
||||
std::vector<unsigned char> ciphertext(plaintext.size() + EVP_CIPHER_CTX_block_size(ctx));
|
||||
int len = 0;
|
||||
int ciphertext_len = 0;
|
||||
|
||||
// 提供明文数据
|
||||
CRYPTO_CHECK(EVP_EncryptUpdate(ctx, ciphertext.data(), &len,
|
||||
plaintext.data(), plaintext.size()) == 1, "加载更新失败");
|
||||
ciphertext_len = len;
|
||||
|
||||
// 完成加密(处理填充)
|
||||
CRYPTO_CHECK(EVP_EncryptFinal_ex(ctx, ciphertext.data() + len, &len) == 1, "加密结束失败");
|
||||
ciphertext_len += len;
|
||||
|
||||
// 调整到实际密文长度
|
||||
ciphertext.resize(ciphertext_len);
|
||||
|
||||
// 处理上下文
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
|
||||
return ciphertext;
|
||||
|
||||
}
|
||||
|
||||
// AES-256-CBC
|
||||
static std::vector<unsigned char> aes_decrypt(const std::vector<unsigned char>& ciphertext,
|
||||
const std::vector<unsigned char>& key,
|
||||
const std::vector<unsigned char>& iv) {
|
||||
// 创建上下文
|
||||
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
|
||||
CRYPTO_CHECK(ctx != nullptr, "无法创建解密上下文");
|
||||
|
||||
// 初始化解密操作
|
||||
CRYPTO_CHECK(EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr,
|
||||
key.data(), iv.data()) == 1, "解密初始化失败");
|
||||
|
||||
// 输出缓冲区(明文可能比密文短,但最大为密文长度)
|
||||
std::vector<unsigned char> plaintext(ciphertext.size());
|
||||
int len = 0;
|
||||
int plaintext_len = 0;
|
||||
|
||||
// 提供密文数据
|
||||
CRYPTO_CHECK(EVP_DecryptUpdate(ctx, plaintext.data(), &len,
|
||||
ciphertext.data(), ciphertext.size()) == 1, "解密更新失败");
|
||||
plaintext_len = len;
|
||||
|
||||
// 完成解密(处理填充)
|
||||
CRYPTO_CHECK(EVP_DecryptFinal_ex(ctx, plaintext.data() + len, &len) == 1, "解密结束失败");
|
||||
plaintext_len += len;
|
||||
|
||||
// 调整到实际明文长度
|
||||
plaintext.resize(plaintext_len);
|
||||
|
||||
// 清理上下文
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/buffer.h>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <stdexcept>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include "Crypto_Basic.hpp"
|
||||
|
||||
namespace qing {
|
||||
class Base64 : public qing::Crypto_Basic {
|
||||
public:
|
||||
/* 默认构造函数 */
|
||||
Base64() = default;
|
||||
|
||||
/* 编码:将字符串编码为Base64 */
|
||||
std::string encode(const std::string& input_text) {
|
||||
return base64_encode(input_text);
|
||||
}
|
||||
|
||||
/* 编码:将字节向量编码为Base64 */
|
||||
std::string encode(const std::vector<unsigned char>& input_bytes) {
|
||||
return base64_encode(input_bytes);
|
||||
}
|
||||
|
||||
/* 解码:将Base64字符串解码为原始字符串 */
|
||||
std::string decode_to_string(const std::string& base64_text) {
|
||||
std::vector<unsigned char> decoded = base64_decode_to_bytes(base64_text);
|
||||
return std::string(decoded.begin(), decoded.end());
|
||||
}
|
||||
|
||||
/* 解码:将Base64字符串解码为字节向量 */
|
||||
std::vector<unsigned char> decode_to_bytes(const std::string& base64_text) {
|
||||
return base64_decode_to_bytes(base64_text);
|
||||
}
|
||||
|
||||
/* 静态方法:直接编码字符串(工具函数) */
|
||||
static std::string base64_encode(const std::string& input_text) {
|
||||
std::vector<unsigned char> input(input_text.begin(), input_text.end());
|
||||
return base64_encode(input);
|
||||
}
|
||||
|
||||
/* 静态方法:直接编码字节向量(工具函数) */
|
||||
static std::string base64_encode(const std::vector<unsigned char>& input_bytes) {
|
||||
// 创建BIO链:内存BIO -> Base64 BIO
|
||||
BIO* bio = BIO_new(BIO_s_mem());
|
||||
CRYPTO_CHECK(bio != nullptr, "无法创建内存BIO");
|
||||
|
||||
BIO* b64 = BIO_new(BIO_f_base64());
|
||||
CRYPTO_CHECK(b64 != nullptr, "无法创建Base64 BIO");
|
||||
|
||||
// 将Base64 BIO连接到内存BIO
|
||||
bio = BIO_push(b64, bio);
|
||||
|
||||
// 设置不添加换行符(默认会添加换行)
|
||||
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
|
||||
|
||||
// 写入数据
|
||||
int written = BIO_write(bio, input_bytes.data(), input_bytes.size());
|
||||
CRYPTO_CHECK(written == (int)input_bytes.size(), "Base64编码写入失败");
|
||||
|
||||
// 刷新缓冲区
|
||||
CRYPTO_CHECK(BIO_flush(bio) == 1, "Base64编码刷新失败");
|
||||
|
||||
// 读取编码结果
|
||||
BUF_MEM* bptr;
|
||||
BIO_get_mem_ptr(bio, &bptr);
|
||||
|
||||
// 复制结果到字符串(不包括空终止符)
|
||||
std::string result(bptr->data, bptr->length);
|
||||
|
||||
// 清理BIO
|
||||
BIO_free_all(bio);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/* 静态方法:直接解码为字符串(工具函数) */
|
||||
static std::string base64_decode_to_string(const std::string& base64_text) {
|
||||
std::vector<unsigned char> decoded = base64_decode_to_bytes(base64_text);
|
||||
return std::string(decoded.begin(), decoded.end());
|
||||
}
|
||||
|
||||
/* 静态方法:直接解码为字节向量(工具函数) */
|
||||
static std::vector<unsigned char> base64_decode_to_bytes(const std::string& base64_text) {
|
||||
// 创建BIO链:Base64 BIO -> 内存BIO
|
||||
BIO* bio = BIO_new_mem_buf(base64_text.c_str(), -1);
|
||||
CRYPTO_CHECK(bio != nullptr, "无法创建内存BIO");
|
||||
|
||||
BIO* b64 = BIO_new(BIO_f_base64());
|
||||
CRYPTO_CHECK(b64 != nullptr, "无法创建Base64 BIO");
|
||||
|
||||
// 将Base64 BIO连接到内存BIO
|
||||
bio = BIO_push(b64, bio);
|
||||
|
||||
// 设置不添加换行符(解码时也要匹配)
|
||||
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
|
||||
|
||||
// 准备接收缓冲区
|
||||
std::vector<unsigned char> result;
|
||||
char buffer[4096];
|
||||
int bytes_read;
|
||||
|
||||
// 循环读取解码数据
|
||||
while ((bytes_read = BIO_read(bio, buffer, sizeof(buffer))) > 0) {
|
||||
result.insert(result.end(), buffer, buffer + bytes_read);
|
||||
}
|
||||
|
||||
// 检查是否解码成功(BIO_read返回0表示结束,负值表示错误)
|
||||
CRYPTO_CHECK(bytes_read >= 0, "Base64解码失败");
|
||||
|
||||
// 清理BIO
|
||||
BIO_free_all(bio);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/* 验证Base64格式是否有效 */
|
||||
static bool is_valid_base64(const std::string& base64_text) {
|
||||
// 检查是否只包含Base64字符(A-Z, a-z, 0-9, +, /, =)
|
||||
if (base64_text.empty()) return false;
|
||||
|
||||
for (char c : base64_text) {
|
||||
if (!((c >= 'A' && c <= 'Z') ||
|
||||
(c >= 'a' && c <= 'z') ||
|
||||
(c >= '0' && c <= '9') ||
|
||||
c == '+' || c == '/' || c == '=')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* 打印编码结果(调试用) */
|
||||
static void print_encoded(const std::string& encoded_text) {
|
||||
std::cerr << "Base64 Encoded: " << encoded_text << std::endl;
|
||||
}
|
||||
|
||||
/* 打印解码结果(调试用) */
|
||||
static void print_decoded(const std::vector<unsigned char>& decoded_bytes) {
|
||||
std::cerr << "Decoded bytes: ";
|
||||
for (unsigned char byte : decoded_bytes) {
|
||||
std::cerr << std::setw(2) << static_cast<int>(byte);
|
||||
}
|
||||
std::cerr << std::endl;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <stdexcept>
|
||||
|
||||
#define CRYPTO_CHECK(condition, message) \
|
||||
if (!(condition)) { \
|
||||
throw std::runtime_error(message); \
|
||||
}
|
||||
namespace qing {
|
||||
class Crypto_Basic {
|
||||
public:
|
||||
class CryptoExp: public std::runtime_error {
|
||||
public:
|
||||
CryptoExp(const std::string& msg): std::runtime_error(msg) {}
|
||||
};
|
||||
protected:
|
||||
/* 字节码转十六进制字符串 */
|
||||
static std::string bytes_to_hex(const std::vector<unsigned char>& data);
|
||||
|
||||
/* 十六进制字符串转字节码 */
|
||||
static std::vector<unsigned char> hex_to_bytes(const std::string &hex);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,578 @@
|
||||
#pragma once
|
||||
#include "Crypto_Basic.hpp"
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <sstream>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/core_names.h>
|
||||
#include <openssl/rsa.h>
|
||||
|
||||
// 错误处理宏(打印完整错误栈,便于调试)
|
||||
#define HANDLE_SSL_ERROR() { \
|
||||
ERR_print_errors_fp(stderr); \
|
||||
}
|
||||
|
||||
// 自定义删除器用于管理OpenSSL资源
|
||||
struct EVP_PKEY_deleter {
|
||||
void operator()(EVP_PKEY* pkey) const { EVP_PKEY_free(pkey); }
|
||||
};
|
||||
using EVP_PKEY_ptr = std::unique_ptr<EVP_PKEY, EVP_PKEY_deleter>;
|
||||
|
||||
struct EVP_PKEY_CTX_deleter {
|
||||
void operator()(EVP_PKEY_CTX* ctx) const { EVP_PKEY_CTX_free(ctx); }
|
||||
};
|
||||
using EVP_PKEY_CTX_ptr = std::unique_ptr<EVP_PKEY_CTX, EVP_PKEY_CTX_deleter>;
|
||||
|
||||
struct BIO_deleter {
|
||||
void operator()(BIO* bio) const { BIO_free_all(bio); }
|
||||
};
|
||||
using BIO_ptr = std::unique_ptr<BIO, BIO_deleter>;
|
||||
|
||||
struct EVP_MD_CTX_deleter {
|
||||
void operator()(EVP_MD_CTX* ctx) const { EVP_MD_CTX_free(ctx); }
|
||||
};
|
||||
using EVP_MD_CTX_ptr = std::unique_ptr<EVP_MD_CTX, EVP_MD_CTX_deleter>;
|
||||
|
||||
namespace qing {
|
||||
// 签名填充模式枚举
|
||||
enum class RSASignaturePadding {
|
||||
PKCS1_v1_5, // 传统 PKCS#1 v1.5 填充(兼容旧系统)
|
||||
PSS // 概率签名方案(更安全,推荐)
|
||||
};
|
||||
|
||||
class MyRSA : public qing::Crypto_Basic {
|
||||
public:
|
||||
class Generator {
|
||||
public:
|
||||
Generator() {
|
||||
key = generate_rsa_key();
|
||||
}
|
||||
|
||||
// 保存私钥为 PKCS#8 格式(AES-256-CBC 加密),支持密码保护
|
||||
void save_pri_key(const std::string& filename, const char* password = nullptr) {
|
||||
if (!save_private_key(key.get(), filename, password)) {
|
||||
throw Crypto_Basic::CryptoExp("保存私钥失败。");
|
||||
}
|
||||
}
|
||||
|
||||
// 保存公钥(现代 SubjectPublicKeyInfo 格式)
|
||||
void save_pub_key(const std::string& filename) {
|
||||
if (!save_public_key(key.get(), filename)) {
|
||||
throw Crypto_Basic::CryptoExp("保存公钥失败。");
|
||||
}
|
||||
}
|
||||
|
||||
// 获取密钥(直接使用)
|
||||
EVP_PKEY* get_private_key() const { return key.get(); }
|
||||
|
||||
// 导出为 PEM 字符串(PKCS#8 格式)
|
||||
std::string get_private_key_pem(const char* password = nullptr) {
|
||||
return key_to_pem(key.get(), true, password);
|
||||
}
|
||||
|
||||
std::string get_public_key_pem() {
|
||||
return key_to_pem(key.get(), false);
|
||||
}
|
||||
|
||||
private:
|
||||
EVP_PKEY_ptr key;
|
||||
|
||||
static EVP_PKEY_ptr generate_rsa_key(size_t key_bits = 2048) {
|
||||
EVP_PKEY_CTX_ptr ctx(EVP_PKEY_CTX_new_from_name(nullptr, "RSA", nullptr));
|
||||
if (!ctx) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return nullptr;
|
||||
}
|
||||
if (EVP_PKEY_keygen_init(ctx.get()) <= 0) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
OSSL_PARAM params[] = {
|
||||
OSSL_PARAM_construct_size_t(OSSL_PKEY_PARAM_RSA_BITS, &key_bits),
|
||||
OSSL_PARAM_END
|
||||
};
|
||||
|
||||
if (EVP_PKEY_CTX_set_params(ctx.get(), params) <= 0) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EVP_PKEY* pkey = nullptr;
|
||||
if (EVP_PKEY_generate(ctx.get(), &pkey) <= 0) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return EVP_PKEY_ptr(pkey);
|
||||
}
|
||||
|
||||
// 保存私钥为 PKCS#8 加密格式
|
||||
static bool save_private_key(EVP_PKEY* pkey, const std::string& filename,
|
||||
const char* password = nullptr) {
|
||||
BIO_ptr bio(BIO_new_file(filename.c_str(), "wb"));
|
||||
if (!bio) {
|
||||
std::cerr << "Failed to open file: " << filename << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
const EVP_CIPHER* cipher = password ? EVP_aes_256_cbc() : nullptr;
|
||||
int ret = PEM_write_bio_PKCS8PrivateKey(bio.get(), pkey, cipher,
|
||||
(char*)password,
|
||||
password ? static_cast<int>(strlen(password)) : 0,
|
||||
nullptr, nullptr);
|
||||
if (ret != 1) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool save_public_key(EVP_PKEY* pkey, const std::string& filename) {
|
||||
BIO_ptr bio(BIO_new_file(filename.c_str(), "wb"));
|
||||
if (!bio) {
|
||||
std::cerr << "Failed to open file: " << filename << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
int ret = PEM_write_bio_PUBKEY(bio.get(), pkey);
|
||||
if (ret != 1) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::string key_to_pem(EVP_PKEY* pkey, bool is_private,
|
||||
const char* password = nullptr) {
|
||||
BIO_ptr bio(BIO_new(BIO_s_mem()));
|
||||
if (!bio) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return "";
|
||||
}
|
||||
|
||||
int ret;
|
||||
if (is_private) {
|
||||
const EVP_CIPHER* cipher = password ? EVP_aes_256_cbc() : nullptr;
|
||||
ret = PEM_write_bio_PKCS8PrivateKey(bio.get(), pkey, cipher,
|
||||
(char*)password,
|
||||
password ? static_cast<int>(strlen(password)) : 0,
|
||||
nullptr, nullptr);
|
||||
} else {
|
||||
ret = PEM_write_bio_PUBKEY(bio.get(), pkey);
|
||||
}
|
||||
|
||||
if (ret != 1) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return "";
|
||||
}
|
||||
|
||||
char* data;
|
||||
long len = BIO_get_mem_data(bio.get(), &data);
|
||||
return std::string(data, len);
|
||||
}
|
||||
};
|
||||
|
||||
// 私钥类:支持 PKCS#1 和 PKCS#8 格式(含加密)
|
||||
class Private_Key : Crypto_Basic {
|
||||
public:
|
||||
// 从文件加载(支持密码)
|
||||
Private_Key(const std::string& fullpath, bool from_file = true,
|
||||
const char* password = nullptr) {
|
||||
if (from_file) {
|
||||
if (!(pri_key = load_private_key_from_file(fullpath, password)))
|
||||
throw Crypto_Basic::CryptoExp("读取私钥失败");
|
||||
} else {
|
||||
if (!(pri_key = load_private_key_from_string(fullpath, password)))
|
||||
throw Crypto_Basic::CryptoExp("从字符串加载私钥失败");
|
||||
}
|
||||
}
|
||||
|
||||
static Private_Key from_pem(const std::string& pem_string,
|
||||
const char* password = nullptr) {
|
||||
return Private_Key(pem_string, false, password);
|
||||
}
|
||||
|
||||
// 解密(OAEP)
|
||||
std::string Decrypt(const std::string& cipher_hex) {
|
||||
std::vector<unsigned char> ciphertext = hex_to_bytes(cipher_hex);
|
||||
std::vector<unsigned char> decrypted = rsa_decrypt(pri_key.get(), ciphertext);
|
||||
if (decrypted.empty()) throw Crypto_Basic::CryptoExp("解密失败");
|
||||
return std::string(decrypted.begin(), decrypted.end());
|
||||
}
|
||||
|
||||
// 签名(支持 PSS 和 PKCS#1 v1.5)
|
||||
std::string Sign(const std::string& message,
|
||||
RSASignaturePadding padding = RSASignaturePadding::PSS) {
|
||||
std::vector<unsigned char> msg(message.begin(), message.end());
|
||||
std::vector<unsigned char> signature = rsa_sign(pri_key.get(), msg, padding);
|
||||
if (signature.empty()) throw Crypto_Basic::CryptoExp("签名失败");
|
||||
return bytes_to_hex(signature);
|
||||
}
|
||||
|
||||
std::vector<unsigned char> SignBytes(const std::string& message,
|
||||
RSASignaturePadding padding = RSASignaturePadding::PSS) {
|
||||
std::vector<unsigned char> msg(message.begin(), message.end());
|
||||
return rsa_sign(pri_key.get(), msg, padding);
|
||||
}
|
||||
|
||||
std::string Sign(const std::vector<unsigned char>& message,
|
||||
RSASignaturePadding padding = RSASignaturePadding::PSS) {
|
||||
std::vector<unsigned char> signature = rsa_sign(pri_key.get(), message, padding);
|
||||
if (signature.empty()) throw Crypto_Basic::CryptoExp("签名失败");
|
||||
return bytes_to_hex(signature);
|
||||
}
|
||||
|
||||
std::vector<unsigned char> SignBytes(const std::vector<unsigned char>& message,
|
||||
RSASignaturePadding padding = RSASignaturePadding::PSS) {
|
||||
return rsa_sign(pri_key.get(), message, padding);
|
||||
}
|
||||
|
||||
EVP_PKEY* get() const { return pri_key.get(); }
|
||||
|
||||
private:
|
||||
EVP_PKEY_ptr pri_key;
|
||||
|
||||
static EVP_PKEY_ptr load_private_key_from_file(const std::string& filename,
|
||||
const char* password = nullptr) {
|
||||
BIO_ptr bio(BIO_new_file(filename.c_str(), "rb"));
|
||||
if (!bio) {
|
||||
std::cerr << "Failed to open file: " << filename << std::endl;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
pem_password_cb* cb = nullptr;
|
||||
if (password) {
|
||||
cb = [](char* buf, int size, int rwflag, void* u) -> int {
|
||||
if (!u) return 0;
|
||||
const char* pass = (const char*)u;
|
||||
int len = strlen(pass);
|
||||
if (len > size) len = size;
|
||||
memcpy(buf, pass, len);
|
||||
return len;
|
||||
};
|
||||
}
|
||||
|
||||
EVP_PKEY* pkey = PEM_read_bio_PrivateKey(bio.get(), nullptr, cb, (void*)password);
|
||||
if (!pkey) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return nullptr;
|
||||
}
|
||||
return EVP_PKEY_ptr(pkey);
|
||||
}
|
||||
|
||||
static EVP_PKEY_ptr load_private_key_from_string(const std::string& pem_string,
|
||||
const char* password = nullptr) {
|
||||
BIO_ptr bio(BIO_new_mem_buf(pem_string.c_str(), -1));
|
||||
if (!bio) {
|
||||
std::cerr << "Failed to create BIO from string" << std::endl;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
pem_password_cb* cb = nullptr;
|
||||
if (password) {
|
||||
cb = [](char* buf, int size, int rwflag, void* u) -> int {
|
||||
if (!u) return 0;
|
||||
const char* pass = (const char*)u;
|
||||
int len = strlen(pass);
|
||||
if (len > size) len = size;
|
||||
memcpy(buf, pass, len);
|
||||
return len;
|
||||
};
|
||||
}
|
||||
|
||||
EVP_PKEY* pkey = PEM_read_bio_PrivateKey(bio.get(), nullptr, cb, (void*)password);
|
||||
if (!pkey) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return nullptr;
|
||||
}
|
||||
return EVP_PKEY_ptr(pkey);
|
||||
}
|
||||
|
||||
// RSA 解密(OAEP)
|
||||
static std::vector<unsigned char> rsa_decrypt(EVP_PKEY* private_key,
|
||||
const std::vector<unsigned char>& ciphertext) {
|
||||
EVP_PKEY_CTX_ptr ctx(EVP_PKEY_CTX_new(private_key, nullptr));
|
||||
if (!ctx) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (EVP_PKEY_decrypt_init(ctx.get()) <= 0) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_OAEP_PADDING) <= 0) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return {};
|
||||
}
|
||||
|
||||
size_t outlen = 0;
|
||||
if (EVP_PKEY_decrypt(ctx.get(), nullptr, &outlen,
|
||||
ciphertext.data(), ciphertext.size()) <= 0) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<unsigned char> plaintext(outlen);
|
||||
if (EVP_PKEY_decrypt(ctx.get(), plaintext.data(), &outlen,
|
||||
ciphertext.data(), ciphertext.size()) <= 0) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return {};
|
||||
}
|
||||
|
||||
plaintext.resize(outlen);
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
// RSA 签名(修正了 PSS 参数顺序)
|
||||
static std::vector<unsigned char> rsa_sign(EVP_PKEY* private_key,
|
||||
const std::vector<unsigned char>& message,
|
||||
RSASignaturePadding padding) {
|
||||
EVP_MD_CTX_ptr ctx(EVP_MD_CTX_new());
|
||||
if (!ctx) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return {};
|
||||
}
|
||||
|
||||
const EVP_MD* md = EVP_sha256();
|
||||
|
||||
if (EVP_DigestSignInit(ctx.get(), nullptr, md, nullptr, private_key) <= 0) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return {};
|
||||
}
|
||||
|
||||
EVP_PKEY_CTX* pkey_ctx = EVP_MD_CTX_get_pkey_ctx(ctx.get());
|
||||
if (!pkey_ctx) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return {};
|
||||
}
|
||||
|
||||
int padding_mode;
|
||||
if (padding == RSASignaturePadding::PKCS1_v1_5) {
|
||||
padding_mode = RSA_PKCS1_PADDING;
|
||||
} else {
|
||||
padding_mode = RSA_PKCS1_PSS_PADDING;
|
||||
}
|
||||
|
||||
// 先设置填充模式(关键)
|
||||
if (EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, padding_mode) <= 0) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return {};
|
||||
}
|
||||
|
||||
// 如果是 PSS,再设置盐长度
|
||||
if (padding == RSASignaturePadding::PSS) {
|
||||
if (EVP_PKEY_CTX_set_rsa_pss_saltlen(pkey_ctx, -1) <= 0) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
size_t siglen = 0;
|
||||
if (EVP_DigestSign(ctx.get(), nullptr, &siglen,
|
||||
message.data(), message.size()) <= 0) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<unsigned char> signature(siglen);
|
||||
if (EVP_DigestSign(ctx.get(), signature.data(), &siglen,
|
||||
message.data(), message.size()) <= 0) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return {};
|
||||
}
|
||||
|
||||
signature.resize(siglen);
|
||||
return signature;
|
||||
}
|
||||
};
|
||||
|
||||
// 公钥类
|
||||
class Public_Key : public Crypto_Basic {
|
||||
public:
|
||||
Public_Key(const std::string& filename, bool from_file = true) {
|
||||
if (from_file) {
|
||||
if (!(pub_key = load_public_key_from_file(filename)))
|
||||
throw Crypto_Basic::CryptoExp("读取公钥失败");
|
||||
} else {
|
||||
if (!(pub_key = load_public_key_from_string(filename)))
|
||||
throw Crypto_Basic::CryptoExp("从字符串加载公钥失败");
|
||||
}
|
||||
}
|
||||
|
||||
static Public_Key from_pem(const std::string& pem_string) {
|
||||
return Public_Key(pem_string, false);
|
||||
}
|
||||
|
||||
// 加密(OAEP)
|
||||
std::string Encrypt(const std::string& original_text) {
|
||||
std::vector<unsigned char> plaintext(original_text.begin(), original_text.end());
|
||||
std::vector<unsigned char> ciphertext = rsa_encrypt(pub_key.get(), plaintext);
|
||||
if (ciphertext.empty()) throw Crypto_Basic::CryptoExp("加密失败");
|
||||
return bytes_to_hex(ciphertext);
|
||||
}
|
||||
|
||||
// 验证签名(支持 PSS 和 PKCS#1 v1.5)
|
||||
bool Verify(const std::string& message, const std::string& signature_hex,
|
||||
RSASignaturePadding padding = RSASignaturePadding::PSS) {
|
||||
std::vector<unsigned char> msg(message.begin(), message.end());
|
||||
std::vector<unsigned char> signature = hex_to_bytes(signature_hex);
|
||||
return rsa_verify(pub_key.get(), msg, signature, padding);
|
||||
}
|
||||
|
||||
bool Verify(const std::string& message, const std::vector<unsigned char>& signature,
|
||||
RSASignaturePadding padding = RSASignaturePadding::PSS) {
|
||||
std::vector<unsigned char> msg(message.begin(), message.end());
|
||||
return rsa_verify(pub_key.get(), msg, signature, padding);
|
||||
}
|
||||
|
||||
bool Verify(const std::vector<unsigned char>& message, const std::string& signature_hex,
|
||||
RSASignaturePadding padding = RSASignaturePadding::PSS) {
|
||||
std::vector<unsigned char> signature = hex_to_bytes(signature_hex);
|
||||
return rsa_verify(pub_key.get(), message, signature, padding);
|
||||
}
|
||||
|
||||
bool Verify(const std::vector<unsigned char>& message,
|
||||
const std::vector<unsigned char>& signature,
|
||||
RSASignaturePadding padding = RSASignaturePadding::PSS) {
|
||||
return rsa_verify(pub_key.get(), message, signature, padding);
|
||||
}
|
||||
|
||||
EVP_PKEY* get() const { return pub_key.get(); }
|
||||
|
||||
private:
|
||||
EVP_PKEY_ptr pub_key;
|
||||
|
||||
static EVP_PKEY_ptr load_public_key_from_file(const std::string& filename) {
|
||||
BIO_ptr bio(BIO_new_file(filename.c_str(), "rb"));
|
||||
if (!bio) {
|
||||
std::cerr << "Failed to open file: " << filename << std::endl;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EVP_PKEY* pkey = PEM_read_bio_PUBKEY(bio.get(), nullptr, nullptr, nullptr);
|
||||
if (!pkey) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return nullptr;
|
||||
}
|
||||
return EVP_PKEY_ptr(pkey);
|
||||
}
|
||||
|
||||
static EVP_PKEY_ptr load_public_key_from_string(const std::string& pem_string) {
|
||||
BIO_ptr bio(BIO_new_mem_buf(pem_string.c_str(), -1));
|
||||
if (!bio) {
|
||||
std::cerr << "Failed to create BIO from string" << std::endl;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EVP_PKEY* pkey = PEM_read_bio_PUBKEY(bio.get(), nullptr, nullptr, nullptr);
|
||||
if (!pkey) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return nullptr;
|
||||
}
|
||||
return EVP_PKEY_ptr(pkey);
|
||||
}
|
||||
|
||||
static std::vector<unsigned char> rsa_encrypt(EVP_PKEY* public_key,
|
||||
const std::vector<unsigned char>& plaintext) {
|
||||
EVP_PKEY_CTX_ptr ctx(EVP_PKEY_CTX_new(public_key, nullptr));
|
||||
if (!ctx) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (EVP_PKEY_encrypt_init(ctx.get()) <= 0) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_OAEP_PADDING) <= 0) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return {};
|
||||
}
|
||||
|
||||
size_t outlen = 0;
|
||||
if (EVP_PKEY_encrypt(ctx.get(), nullptr, &outlen,
|
||||
plaintext.data(), plaintext.size()) <= 0) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<unsigned char> ciphertext(outlen);
|
||||
if (EVP_PKEY_encrypt(ctx.get(), ciphertext.data(), &outlen,
|
||||
plaintext.data(), plaintext.size()) <= 0) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return {};
|
||||
}
|
||||
|
||||
ciphertext.resize(outlen);
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
// RSA 验证(修正了 PSS 参数顺序)
|
||||
static bool rsa_verify(EVP_PKEY* public_key,
|
||||
const std::vector<unsigned char>& message,
|
||||
const std::vector<unsigned char>& signature,
|
||||
RSASignaturePadding padding) {
|
||||
EVP_MD_CTX_ptr ctx(EVP_MD_CTX_new());
|
||||
if (!ctx) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return false;
|
||||
}
|
||||
|
||||
const EVP_MD* md = EVP_sha256();
|
||||
|
||||
if (EVP_DigestVerifyInit(ctx.get(), nullptr, md, nullptr, public_key) <= 0) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return false;
|
||||
}
|
||||
|
||||
EVP_PKEY_CTX* pkey_ctx = EVP_MD_CTX_get_pkey_ctx(ctx.get());
|
||||
if (!pkey_ctx) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return false;
|
||||
}
|
||||
|
||||
int padding_mode;
|
||||
if (padding == RSASignaturePadding::PKCS1_v1_5) {
|
||||
padding_mode = RSA_PKCS1_PADDING;
|
||||
} else {
|
||||
padding_mode = RSA_PKCS1_PSS_PADDING;
|
||||
}
|
||||
|
||||
// 先设置填充模式(关键)
|
||||
if (EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, padding_mode) <= 0) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果是 PSS,再设置盐长度
|
||||
if (padding == RSASignaturePadding::PSS) {
|
||||
if (EVP_PKEY_CTX_set_rsa_pss_saltlen(pkey_ctx, -1) <= 0) {
|
||||
HANDLE_SSL_ERROR();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int result = EVP_DigestVerify(ctx.get(), signature.data(), signature.size(),
|
||||
message.data(), message.size());
|
||||
|
||||
if (result == 1) {
|
||||
return true;
|
||||
} else if (result == 0) {
|
||||
return false;
|
||||
} else {
|
||||
HANDLE_SSL_ERROR();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
} // namespace qing
|
||||
@@ -0,0 +1,113 @@
|
||||
#pragma once
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/err.h>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <stdexcept>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include "us/Crypto_Basic.hpp"
|
||||
|
||||
namespace qing {
|
||||
|
||||
class SHA256: public qing::Crypto_Basic {
|
||||
|
||||
public:
|
||||
SHA256() = default;
|
||||
|
||||
/* 计算字符串的SHA-256哈希值,返回十六进制字符串 */
|
||||
std::string hash(const std::string& input_text) {
|
||||
std::vector<unsigned char> input(input_text.begin(), input_text.end());
|
||||
std::vector<unsigned char> digest = sha256_hash(input);
|
||||
return bytes_to_hex(digest);
|
||||
}
|
||||
|
||||
/* 计算字节向量的SHA-256哈希值,返回十六进制字符串 */
|
||||
std::string hash(const std::vector<unsigned char>& input_bytes) {
|
||||
std::vector<unsigned char> digest = sha256_hash(input_bytes);
|
||||
return bytes_to_hex(digest);
|
||||
}
|
||||
|
||||
/* 计算字符串的SHA-256哈希值,返回字节向量 */
|
||||
std::vector<unsigned char> hash_bytes(const std::string& input_text) {
|
||||
std::vector<unsigned char> input(input_text.begin(), input_text.end());
|
||||
return sha256_hash(input);
|
||||
}
|
||||
|
||||
/* 计算字节向量的SHA-256哈希值,返回字节向量 */
|
||||
std::vector<unsigned char> hash_bytes(const std::vector<unsigned char>& input_bytes) {
|
||||
return sha256_hash(input_bytes);
|
||||
}
|
||||
|
||||
/* 验证数据是否与给定的哈希值匹配 */
|
||||
bool verify(const std::string& input_text, const std::string& expected_hash_hex) {
|
||||
std::string computed_hash = hash(input_text);
|
||||
return computed_hash == expected_hash_hex;
|
||||
}
|
||||
|
||||
/* 验证数据是否与给定的哈希值匹配(字节向量版本) */
|
||||
bool verify(const std::vector<unsigned char>& input_bytes, const std::string& expected_hash_hex) {
|
||||
std::string computed_hash = hash(input_bytes);
|
||||
return computed_hash == expected_hash_hex;
|
||||
}
|
||||
|
||||
/* 静态方法:直接计算SHA-256哈希(工具函数) */
|
||||
static std::string sha256(const std::string& input_text) {
|
||||
SHA256 hasher;
|
||||
return hasher.hash(input_text);
|
||||
}
|
||||
|
||||
/* 静态方法:直接计算SHA-256哈希,返回字节向量 */
|
||||
static std::vector<unsigned char> sha256_bytes(const std::string& input_text) {
|
||||
SHA256 hasher;
|
||||
return hasher.hash_bytes(input_text);
|
||||
}
|
||||
|
||||
/* 打印哈希值(调试用) */
|
||||
static void print_hash(const std::vector<unsigned char>& hash_bytes) {
|
||||
std::string hex = bytes_to_hex(hash_bytes);
|
||||
std::cerr << hex << std::endl;
|
||||
}
|
||||
|
||||
private:
|
||||
/* SHA-256哈希核心实现 */
|
||||
static std::vector<unsigned char> sha256_hash(const std::vector<unsigned char>& input) {
|
||||
// 创建EVP上下文
|
||||
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
|
||||
CRYPTO_CHECK(ctx != nullptr, "无法创建哈希上下文");
|
||||
|
||||
// 初始化SHA-256操作
|
||||
CRYPTO_CHECK(EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr) == 1,
|
||||
"SHA-256初始化失败");
|
||||
|
||||
// 提供输入数据
|
||||
CRYPTO_CHECK(EVP_DigestUpdate(ctx, input.data(), input.size()) == 1,
|
||||
"SHA-256更新失败");
|
||||
|
||||
// 获取哈希结果
|
||||
std::vector<unsigned char> digest(EVP_MD_CTX_size(ctx)); // SHA-256输出32字节
|
||||
unsigned int digest_len = 0;
|
||||
CRYPTO_CHECK(EVP_DigestFinal_ex(ctx, digest.data(), &digest_len) == 1,
|
||||
"SHA-256完成失败");
|
||||
|
||||
// 调整到实际长度(应该是32字节)
|
||||
digest.resize(digest_len);
|
||||
|
||||
// 清理上下文
|
||||
EVP_MD_CTX_free(ctx);
|
||||
|
||||
return digest;
|
||||
}
|
||||
|
||||
/* 辅助函数:字节向量转十六进制字符串 */
|
||||
static std::string bytes_to_hex(const std::vector<unsigned char>& bytes) {
|
||||
std::stringstream ss;
|
||||
ss << std::hex << std::setfill('0');
|
||||
for (unsigned char byte : bytes) {
|
||||
ss << std::setw(2) << static_cast<int>(byte);
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "us/Crypto_Basic.hpp"
|
||||
|
||||
namespace qing {
|
||||
std::string Crypto_Basic::bytes_to_hex(const std::vector<unsigned char>& data) {
|
||||
static const char hex_chars[] = "0123456789abcdef";
|
||||
std::string hex_string;
|
||||
hex_string.reserve(data.size() * 2);
|
||||
|
||||
for (unsigned char byte: data) {
|
||||
hex_string.push_back(hex_chars[byte >> 4]); // 高4位
|
||||
hex_string.push_back(hex_chars[byte & 0x0F]); // 低4位
|
||||
}
|
||||
|
||||
return hex_string;
|
||||
|
||||
}
|
||||
|
||||
std::vector<unsigned char> Crypto_Basic::hex_to_bytes(const std::string &hex) {
|
||||
// 检查长度是否为偶数
|
||||
if (hex.size() % 2 != 0) {
|
||||
throw std::invalid_argument("十六进制字符串长度必须为偶数");
|
||||
}
|
||||
|
||||
std::vector<unsigned char> bytes;
|
||||
bytes.reserve(hex.size() / 2);
|
||||
|
||||
auto char_to_nibble = [](char c) -> unsigned char {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return 10 + (c - 'a');
|
||||
if (c >= 'A' && c <= 'F') return 10 + (c - 'A');
|
||||
throw std::invalid_argument("无效的十六进制字符:" + std::string(1, c));
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < hex.size(); i += 2) {
|
||||
unsigned char high = char_to_nibble(hex[i]);
|
||||
unsigned char low = char_to_nibble(hex[i + 1]);
|
||||
bytes.push_back((high << 4) | low);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
# Compiled Object files
|
||||
*.slo
|
||||
*.lo
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Linker files
|
||||
*.ilk
|
||||
|
||||
# Debugger Files
|
||||
*.pdb
|
||||
|
||||
# Compiled Dynamic libraries
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
*.so.*
|
||||
|
||||
|
||||
# Fortran module files
|
||||
*.mod
|
||||
*.smod
|
||||
|
||||
# Compiled Static libraries
|
||||
*.lai
|
||||
*.la
|
||||
*.a
|
||||
*.lib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
|
||||
# Build directories
|
||||
build/
|
||||
Build/
|
||||
build-*/
|
||||
|
||||
# CMake generated files
|
||||
CMakeFiles/
|
||||
CMakeCache.txt
|
||||
cmake_install.cmake
|
||||
#Makefile
|
||||
install_manifest.txt
|
||||
compile_commands.json
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.log
|
||||
*.bak
|
||||
*.swp
|
||||
|
||||
# vcpkg
|
||||
vcpkg_installed/
|
||||
|
||||
# debug information files
|
||||
*.dwo
|
||||
|
||||
# test output & cache
|
||||
Testing/
|
||||
.cache/
|
||||
@@ -0,0 +1,145 @@
|
||||
cmake_minimum_required(VERSION 3.12)
|
||||
project(us VERSION 0.0.1)
|
||||
|
||||
# Set C++ standard
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
# Set compiler flags based on compiler type
|
||||
if(MSVC)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /O2 /W4 /bigobj")
|
||||
else()
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -pipe -Wall")
|
||||
endif()
|
||||
|
||||
# Set library names
|
||||
set(SHARED_LIB_NAME "us_http")
|
||||
set(STATIC_LIB_NAME "us_http_static")
|
||||
set(FRAMEWORK_NAME "us")
|
||||
|
||||
# ===== Boost Library Configuration (Header-Only) =====
|
||||
if(DEFINED ENV{BOOST_ROOT})
|
||||
set(BOOST_ROOT $ENV{BOOST_ROOT})
|
||||
message(STATUS "Using BOOST_ROOT from environment: ${BOOST_ROOT}")
|
||||
elseif(NOT DEFINED BOOST_ROOT)
|
||||
set(BOOST_ROOT "C:/local/boost_1_91_0" CACHE PATH "Boost installation root")
|
||||
endif()
|
||||
|
||||
message(STATUS "BOOST_ROOT: ${BOOST_ROOT}")
|
||||
|
||||
set(Boost_INCLUDE_DIR "${BOOST_ROOT}" CACHE PATH "Boost include directory")
|
||||
message(STATUS "Boost include directory: ${Boost_INCLUDE_DIR}")
|
||||
|
||||
if(NOT EXISTS ${Boost_INCLUDE_DIR})
|
||||
message(FATAL_ERROR "Boost include directory not found: ${Boost_INCLUDE_DIR}")
|
||||
endif()
|
||||
|
||||
include_directories(${Boost_INCLUDE_DIR})
|
||||
|
||||
# Include directory (project headers)
|
||||
set(INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/inc")
|
||||
include_directories(${INCLUDE_DIR})
|
||||
|
||||
# Source files
|
||||
set(SOURCES
|
||||
src/HttpServer.cpp
|
||||
src/WebSocketServer.cpp
|
||||
src/WebSocketClient.cpp
|
||||
)
|
||||
|
||||
# Header files list
|
||||
set(HEADERS
|
||||
inc/${FRAMEWORK_NAME}/HttpServer.hpp
|
||||
inc/${FRAMEWORK_NAME}/HttpClient.hpp
|
||||
inc/${FRAMEWORK_NAME}/WebSocketServer.hpp
|
||||
inc/${FRAMEWORK_NAME}/WebSocketClient.hpp
|
||||
)
|
||||
|
||||
# Create shared library (DLL)
|
||||
add_library(${SHARED_LIB_NAME} SHARED ${SOURCES})
|
||||
|
||||
# Create static library (LIB)
|
||||
add_library(${STATIC_LIB_NAME} STATIC ${SOURCES})
|
||||
|
||||
# Set properties for shared library
|
||||
if(WIN32)
|
||||
target_link_libraries(${SHARED_LIB_NAME}
|
||||
ws2_32
|
||||
)
|
||||
else()
|
||||
target_link_libraries(${SHARED_LIB_NAME}
|
||||
pthread
|
||||
)
|
||||
endif()
|
||||
|
||||
# Set output directories
|
||||
set_target_properties(${SHARED_LIB_NAME} ${STATIC_LIB_NAME} PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/build"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/build"
|
||||
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/build"
|
||||
)
|
||||
|
||||
# Additional properties for Windows
|
||||
if(WIN32)
|
||||
set_target_properties(${SHARED_LIB_NAME} PROPERTIES
|
||||
PREFIX ""
|
||||
SUFFIX ".dll"
|
||||
)
|
||||
set_target_properties(${STATIC_LIB_NAME} PROPERTIES
|
||||
PREFIX ""
|
||||
SUFFIX ".lib"
|
||||
)
|
||||
else()
|
||||
set_target_properties(${SHARED_LIB_NAME} PROPERTIES
|
||||
PREFIX "lib"
|
||||
SUFFIX ".so"
|
||||
)
|
||||
set_target_properties(${STATIC_LIB_NAME} PROPERTIES
|
||||
PREFIX "lib"
|
||||
SUFFIX ".a"
|
||||
)
|
||||
endif()
|
||||
|
||||
# Position independent code for static library on Unix
|
||||
if(UNIX AND NOT APPLE)
|
||||
set_target_properties(${STATIC_LIB_NAME} PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON
|
||||
)
|
||||
set_target_properties(${SHARED_LIB_NAME} PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON
|
||||
)
|
||||
endif()
|
||||
|
||||
# Install targets
|
||||
install(TARGETS ${SHARED_LIB_NAME} ${STATIC_LIB_NAME}
|
||||
LIBRARY DESTINATION lib
|
||||
RUNTIME DESTINATION bin
|
||||
ARCHIVE DESTINATION lib
|
||||
)
|
||||
|
||||
# Install headers
|
||||
install(FILES ${HEADERS}
|
||||
DESTINATION include/${FRAMEWORK_NAME}
|
||||
)
|
||||
|
||||
# Clean target
|
||||
add_custom_target(clear
|
||||
COMMAND ${CMAKE_COMMAND} -E remove_directory "${CMAKE_CURRENT_BINARY_DIR}/build"
|
||||
COMMENT "Cleaning build directory..."
|
||||
)
|
||||
|
||||
# Print configuration info
|
||||
message(STATUS "========================================")
|
||||
message(STATUS "Project: ${PROJECT_NAME}")
|
||||
message(STATUS "Version: ${PROJECT_VERSION}")
|
||||
message(STATUS "Shared library: ${SHARED_LIB_NAME}")
|
||||
message(STATUS "Static library: ${STATIC_LIB_NAME}")
|
||||
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
|
||||
message(STATUS "Install prefix: ${CMAKE_INSTALL_PREFIX}")
|
||||
message(STATUS "Compiler: ${CMAKE_CXX_COMPILER_ID}")
|
||||
if(MSVC)
|
||||
message(STATUS "MSVC version: ${MSVC_VERSION}")
|
||||
endif()
|
||||
message(STATUS "BOOST_ROOT: ${BOOST_ROOT}")
|
||||
message(STATUS "========================================")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 dashun
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,49 @@
|
||||
VERSION := 0.0.1
|
||||
FRAMEWORK_NAME := us
|
||||
LIB_NAME := libus_http
|
||||
INSTL_TDIR := /lib
|
||||
INCLU_TDIR := /usr/include
|
||||
INCLU_SDIR := inc
|
||||
CPP := g++
|
||||
INCS := -I inc
|
||||
CPPFLAGS := --std=c++20 -O3 -pipe -Wall -Werror
|
||||
LIBS :=
|
||||
BUILD_TDIR := build
|
||||
$(shell mkdir -p $(BUILD_TDIR))
|
||||
|
||||
#--------
|
||||
all: $(BUILD_TDIR)/$(LIB_NAME).so
|
||||
|
||||
$(BUILD_TDIR)/$(LIB_NAME).so: $(BUILD_TDIR)/HttpServer.o #$(BUILD_TDIR)/WebSocketServer.o $(BUILD_TDIR)/WebSocketClient.o
|
||||
$(CPP) -shared $^ $(LIBS) -o $@
|
||||
|
||||
$(BUILD_TDIR)/HttpServer.o: src/HttpServer.cpp
|
||||
$(CPP) $(CPPFLAGS) -fPIC -c $^ $(INCS) -o $@
|
||||
|
||||
$(BUILD_TDIR)/WebSocketServer.o: src/WebSocketServer.cpp
|
||||
$(CPP) $(CPPFLAGS) -fPIC -c $^ $(INCS) -o $@
|
||||
|
||||
$(BUILD_TDIR)/WebSocketClient.o: src/WebSocketClient.cpp
|
||||
$(CPP) $(CPPFLAGS) -fPIC -c $^ $(INCS) -o $@
|
||||
|
||||
clear:
|
||||
@rm -rvf $(BUILD_TDIR)
|
||||
|
||||
install:
|
||||
@echo "install UniServe_libth..."
|
||||
@mkdir -vp $(INCLU_TDIR)/$(FRAMEWORK_NAME)
|
||||
@cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/HttpServer.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/
|
||||
@cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/HttpClient.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/
|
||||
#@cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/WebSocketServer.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/
|
||||
#@cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/WebSocketClient.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/
|
||||
@cp -vf $(BUILD_TDIR)/$(LIB_NAME).so $(INSTL_TDIR)/
|
||||
@echo "done."
|
||||
|
||||
uninstall:
|
||||
@echo "uninstall UniServe_libth..."
|
||||
@rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/HttpServer.hpp
|
||||
@rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/HttpClient.hpp
|
||||
@rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/WebSocketServer.hpp
|
||||
@rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/WebSocketClient.hpp
|
||||
@rm -vf $(INSTL_TDIR)/$(LIB_NAME).so
|
||||
@echo "done."
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
make clear
|
||||
make
|
||||
make install
|
||||
@@ -0,0 +1,200 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <curl/curl.h>
|
||||
|
||||
namespace pmc::net {
|
||||
|
||||
enum class HttpMethod {
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
DELETE,
|
||||
PATCH,
|
||||
HEAD,
|
||||
OPTIONS
|
||||
};
|
||||
|
||||
class HttpClient {
|
||||
public:
|
||||
HttpClient() {
|
||||
curl_ = curl_easy_init();
|
||||
if (!curl_) {
|
||||
throw std::runtime_error("Failed to initialize CURL");
|
||||
}
|
||||
}
|
||||
|
||||
~HttpClient() {
|
||||
if (curl_) {
|
||||
curl_easy_cleanup(curl_);
|
||||
}
|
||||
if (headers_) {
|
||||
curl_slist_free_all(headers_);
|
||||
}
|
||||
}
|
||||
|
||||
void set_header(const std::string& header) {
|
||||
headers_ = curl_slist_append(headers_, header.c_str());
|
||||
}
|
||||
|
||||
void clear_headers() {
|
||||
if (headers_) {
|
||||
curl_slist_free_all(headers_);
|
||||
headers_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
const std::map<std::string, std::string>& get_response_headers() const {
|
||||
return response_headers_;
|
||||
}
|
||||
|
||||
long get_response_code() const {
|
||||
return response_code_;
|
||||
}
|
||||
|
||||
// ===== 修复:直接转发,不按行处理 =====
|
||||
void request_stream_sync(
|
||||
HttpMethod method,
|
||||
const std::string& url,
|
||||
const std::string& data,
|
||||
std::function<void(const std::string&)> callback
|
||||
) {
|
||||
if (!curl_) {
|
||||
throw std::runtime_error("CURL not initialized");
|
||||
}
|
||||
|
||||
response_headers_.clear();
|
||||
response_code_ = 0;
|
||||
|
||||
curl_easy_reset(curl_);
|
||||
curl_easy_setopt(curl_, CURLOPT_URL, url.c_str());
|
||||
set_http_method(method, data);
|
||||
curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, headers_);
|
||||
curl_easy_setopt(curl_, CURLOPT_HEADERFUNCTION, header_callback);
|
||||
curl_easy_setopt(curl_, CURLOPT_HEADERDATA, this);
|
||||
|
||||
// ===== 关键修复:直接收集原始数据,不按行处理 =====
|
||||
std::string full_response;
|
||||
curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &full_response);
|
||||
|
||||
curl_easy_setopt(curl_, CURLOPT_TIMEOUT, 30L);
|
||||
curl_easy_setopt(curl_, CURLOPT_CONNECTTIMEOUT, 10L);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl_);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
throw std::runtime_error(std::string("CURL error: ") + curl_easy_strerror(res));
|
||||
}
|
||||
|
||||
curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &response_code_);
|
||||
|
||||
// ===== 完整转发所有数据(包括换行符) =====
|
||||
if (!full_response.empty()) {
|
||||
callback(full_response);
|
||||
}
|
||||
}
|
||||
|
||||
void get_stream_sync(const std::string& url, std::function<void(const std::string&)> callback) {
|
||||
request_stream_sync(HttpMethod::GET, url, "", callback);
|
||||
}
|
||||
|
||||
void post_stream_sync(const std::string& url, const std::string& data, std::function<void(const std::string&)> callback) {
|
||||
request_stream_sync(HttpMethod::POST, url, data, callback);
|
||||
}
|
||||
|
||||
void put_stream_sync(const std::string& url, const std::string& data, std::function<void(const std::string&)> callback) {
|
||||
request_stream_sync(HttpMethod::PUT, url, data, callback);
|
||||
}
|
||||
|
||||
void delete_stream_sync(const std::string& url, std::function<void(const std::string&)> callback) {
|
||||
request_stream_sync(HttpMethod::DELETE, url, "", callback);
|
||||
}
|
||||
|
||||
private:
|
||||
CURL* curl_ = nullptr;
|
||||
struct curl_slist* headers_ = nullptr;
|
||||
std::map<std::string, std::string> response_headers_;
|
||||
long response_code_ = 0;
|
||||
|
||||
void set_http_method(HttpMethod method, const std::string& data) {
|
||||
switch (method) {
|
||||
case HttpMethod::GET:
|
||||
curl_easy_setopt(curl_, CURLOPT_HTTPGET, 1L);
|
||||
break;
|
||||
|
||||
case HttpMethod::POST:
|
||||
curl_easy_setopt(curl_, CURLOPT_POST, 1L);
|
||||
if (!data.empty()) {
|
||||
curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, data.c_str());
|
||||
curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, data.size());
|
||||
}
|
||||
break;
|
||||
|
||||
case HttpMethod::PUT:
|
||||
curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, "PUT");
|
||||
if (!data.empty()) {
|
||||
curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, data.c_str());
|
||||
curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, data.size());
|
||||
}
|
||||
break;
|
||||
|
||||
case HttpMethod::DELETE:
|
||||
curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, "DELETE");
|
||||
break;
|
||||
|
||||
case HttpMethod::PATCH:
|
||||
curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, "PATCH");
|
||||
if (!data.empty()) {
|
||||
curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, data.c_str());
|
||||
curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, data.size());
|
||||
}
|
||||
break;
|
||||
|
||||
case HttpMethod::HEAD:
|
||||
curl_easy_setopt(curl_, CURLOPT_NOBODY, 1L);
|
||||
break;
|
||||
|
||||
case HttpMethod::OPTIONS:
|
||||
curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, "OPTIONS");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static size_t header_callback(char* buffer, size_t size, size_t nitems, void* userdata) {
|
||||
size_t total_size = size * nitems;
|
||||
HttpClient* client = static_cast<HttpClient*>(userdata);
|
||||
|
||||
std::string header_line(buffer, total_size);
|
||||
size_t colon_pos = header_line.find(':');
|
||||
if (colon_pos != std::string::npos) {
|
||||
std::string key = header_line.substr(0, colon_pos);
|
||||
std::string value = header_line.substr(colon_pos + 1);
|
||||
|
||||
key.erase(0, key.find_first_not_of(" \t"));
|
||||
key.erase(key.find_last_not_of(" \t") + 1);
|
||||
value.erase(0, value.find_first_not_of(" \t"));
|
||||
value.erase(value.find_last_not_of(" \t\r\n") + 1);
|
||||
|
||||
if (!key.empty() && !value.empty()) {
|
||||
client->response_headers_[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return total_size;
|
||||
}
|
||||
|
||||
// ===== 修复:直接写入原始数据,不按行处理 =====
|
||||
static size_t write_callback(char* ptr, size_t size, size_t nmemb, void* userdata) {
|
||||
size_t total_size = size * nmemb;
|
||||
std::string* response = static_cast<std::string*>(userdata);
|
||||
|
||||
// 直接追加原始数据,保留所有换行符和特殊字符
|
||||
response->append(ptr, total_size);
|
||||
|
||||
return total_size;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace pmc::net
|
||||
@@ -0,0 +1,126 @@
|
||||
#ifndef HTTPSERVER_HPP
|
||||
#define HTTPSERVER_HPP
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/beast.hpp>
|
||||
#include <boost/beast/http.hpp>
|
||||
#include <boost/beast/version.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/config.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <regex>
|
||||
|
||||
namespace pmc {
|
||||
namespace net {
|
||||
|
||||
namespace beast = boost::beast;
|
||||
namespace http = beast::http;
|
||||
namespace asio = boost::asio;
|
||||
using tcp = boost::asio::ip::tcp;
|
||||
|
||||
using HttpRequestHandler = std::function<http::response<http::string_body>(
|
||||
const http::request<http::string_body>&,
|
||||
const std::unordered_map<std::string, std::string>&)>;
|
||||
|
||||
using MiddlewareHandler = std::function<bool(
|
||||
const http::request<http::string_body>&,
|
||||
http::response<http::string_body>&,
|
||||
const std::unordered_map<std::string, std::string>&)>;
|
||||
|
||||
struct RouteEntry {
|
||||
std::string path;
|
||||
std::regex pattern;
|
||||
HttpRequestHandler handler;
|
||||
bool is_wildcard;
|
||||
};
|
||||
|
||||
class HttpServer {
|
||||
public:
|
||||
explicit HttpServer(const std::string& address, unsigned short port,
|
||||
unsigned int threads = 1);
|
||||
explicit HttpServer(const std::string& address, unsigned short port,
|
||||
const std::string& cert_file, const std::string& key_file,
|
||||
unsigned int threads = 1);
|
||||
~HttpServer();
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
void get(const std::string& path, HttpRequestHandler handler);
|
||||
void post(const std::string& path, HttpRequestHandler handler);
|
||||
void put(const std::string& path, HttpRequestHandler handler);
|
||||
void del(const std::string& path, HttpRequestHandler handler);
|
||||
void use(MiddlewareHandler middleware);
|
||||
void setStaticDirectory(const std::string& path);
|
||||
|
||||
bool isRunning() const;
|
||||
unsigned short getPort() const;
|
||||
std::string getAddress() const;
|
||||
bool isSSLEnabled() const;
|
||||
|
||||
void run();
|
||||
void showStatus() const;
|
||||
|
||||
void setPrivateKeyPassword(const std::string& password);
|
||||
|
||||
class debug {
|
||||
public:
|
||||
static void print_all_header_fields(const http::request<http::string_body>& req);
|
||||
};
|
||||
|
||||
private:
|
||||
class Session;
|
||||
class Listener;
|
||||
|
||||
std::string address_;
|
||||
unsigned short port_;
|
||||
unsigned int threads_;
|
||||
std::atomic<bool> running_{false};
|
||||
|
||||
bool ssl_enabled_{false};
|
||||
std::string cert_file_;
|
||||
std::string key_file_;
|
||||
std::string private_key_password_;
|
||||
|
||||
std::unique_ptr<asio::ssl::context> ssl_ctx_;
|
||||
std::unique_ptr<asio::io_context> ioc_;
|
||||
|
||||
std::shared_ptr<Listener> listener_;
|
||||
std::vector<std::thread> worker_threads_;
|
||||
|
||||
std::vector<RouteEntry> get_handlers_;
|
||||
std::vector<RouteEntry> post_handlers_;
|
||||
std::vector<RouteEntry> put_handlers_;
|
||||
std::vector<RouteEntry> delete_handlers_;
|
||||
std::vector<MiddlewareHandler> middlewares_;
|
||||
std::string static_directory_;
|
||||
|
||||
std::vector<std::weak_ptr<Session>> sessions_;
|
||||
std::mutex sessions_mutex_;
|
||||
|
||||
std::unordered_map<std::string, std::string> parseQueryParams(const std::string& query);
|
||||
std::regex wildcardToRegex(const std::string& pattern);
|
||||
bool matchPath(const std::string& path, const std::regex& pattern);
|
||||
http::response<http::string_body> handleRequest(const http::request<http::string_body>& req);
|
||||
HttpRequestHandler findRoute(const std::string& method, const std::string& path);
|
||||
|
||||
void addSession(const std::shared_ptr<Session>& session);
|
||||
void removeSession(const Session* session);
|
||||
void closeAllSessions();
|
||||
void initSSLContext();
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace pmc
|
||||
|
||||
#endif // HTTPSERVER_HPP
|
||||
@@ -0,0 +1,63 @@
|
||||
#ifndef PMC_NET_WEBSOCKET_CLIENT_HPP
|
||||
#define PMC_NET_WEBSOCKET_CLIENT_HPP
|
||||
|
||||
#include "WebSocketClientBase.hpp"
|
||||
|
||||
namespace pmc {
|
||||
namespace net {
|
||||
|
||||
/**
|
||||
* @brief Asynchronous WebSocket client based on Boost.Beast (non-SSL version)
|
||||
*/
|
||||
class WebSocketClient : public WebSocketClientBase {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructor
|
||||
* @param host Server hostname or IP address
|
||||
* @param port Server port number
|
||||
* @param path WebSocket path (default "/")
|
||||
*/
|
||||
WebSocketClient(const std::string& host, const std::string& port, const std::string& path = "/");
|
||||
|
||||
/**
|
||||
* @brief Destructor
|
||||
*/
|
||||
~WebSocketClient();
|
||||
|
||||
// Implement WebSocketClientBase interface
|
||||
void setMessageCallback(MessageCallback callback) override;
|
||||
void setErrorCallback(ErrorCallback callback) override;
|
||||
void setConnectCallback(ConnectCallback callback) override;
|
||||
void setCloseCallback(CloseCallback callback) override;
|
||||
|
||||
bool connect(int timeout_ms = 5000) override;
|
||||
void disconnect() override;
|
||||
bool isConnected() const override;
|
||||
|
||||
bool send(const std::string& message) override;
|
||||
bool sendBinary(const void* data, size_t size) override;
|
||||
|
||||
void setAutoReconnect(bool enable, int interval_ms = 3000) override;
|
||||
void setHeartbeatInterval(int interval_ms = 30000) override;
|
||||
|
||||
std::string getHost() const override;
|
||||
std::string getPort() const override;
|
||||
std::string getPath() const override;
|
||||
|
||||
// SSL/TLS related (non-SSL version returns default values)
|
||||
bool isSecure() const override { return false; }
|
||||
void setVerifyCertificate(bool verify) override { (void)verify;/* Ignored */ }
|
||||
void setCertificateFile(const std::string& cert_file) override { (void)cert_file; /* Ignored */ }
|
||||
void setPrivateKeyFile(const std::string& key_file) override { (void)key_file;/* Ignored */ }
|
||||
void setCertificateAuthorityFile(const std::string& ca_file) override { (void)ca_file;/* Ignored */ }
|
||||
|
||||
private:
|
||||
// Forward declaration of implementation class
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace pmc
|
||||
|
||||
#endif // PMC_NET_WEBSOCKET_CLIENT_HPP
|
||||
@@ -0,0 +1,61 @@
|
||||
#ifndef PMC_NET_WEBSOCKET_CLIENT_BASE_HPP
|
||||
#define PMC_NET_WEBSOCKET_CLIENT_BASE_HPP
|
||||
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace pmc {
|
||||
namespace net {
|
||||
|
||||
/**
|
||||
* @brief WebSocket client base class
|
||||
*
|
||||
* Defines the common interface for all WebSocket clients
|
||||
*/
|
||||
class WebSocketClientBase {
|
||||
public:
|
||||
// Callback function types
|
||||
using MessageCallback = std::function<void(const std::string&)>;
|
||||
using ErrorCallback = std::function<void(const std::string&)>;
|
||||
using ConnectCallback = std::function<void(bool)>;
|
||||
using CloseCallback = std::function<void()>;
|
||||
|
||||
virtual ~WebSocketClientBase() = default;
|
||||
|
||||
// Callback setters
|
||||
virtual void setMessageCallback(MessageCallback callback) = 0;
|
||||
virtual void setErrorCallback(ErrorCallback callback) = 0;
|
||||
virtual void setConnectCallback(ConnectCallback callback) = 0;
|
||||
virtual void setCloseCallback(CloseCallback callback) = 0;
|
||||
|
||||
// Connection management
|
||||
virtual bool connect(int timeout_ms = 5000) = 0;
|
||||
virtual void disconnect() = 0;
|
||||
virtual bool isConnected() const = 0;
|
||||
|
||||
// Message sending
|
||||
virtual bool send(const std::string& message) = 0;
|
||||
virtual bool sendBinary(const void* data, size_t size) = 0;
|
||||
|
||||
// Configuration options
|
||||
virtual void setAutoReconnect(bool enable, int interval_ms = 3000) = 0;
|
||||
virtual void setHeartbeatInterval(int interval_ms = 30000) = 0;
|
||||
|
||||
// Information retrieval
|
||||
virtual std::string getHost() const = 0;
|
||||
virtual std::string getPort() const = 0;
|
||||
virtual std::string getPath() const = 0;
|
||||
|
||||
// SSL/TLS related
|
||||
virtual bool isSecure() const = 0;
|
||||
virtual void setVerifyCertificate(bool verify) = 0;
|
||||
virtual void setCertificateFile(const std::string& cert_file) = 0;
|
||||
virtual void setPrivateKeyFile(const std::string& key_file) = 0;
|
||||
virtual void setCertificateAuthorityFile(const std::string& ca_file) = 0;
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace pmc
|
||||
|
||||
#endif // PMC_NET_WEBSOCKET_CLIENT_BASE_HPP
|
||||
@@ -0,0 +1,176 @@
|
||||
// WebSocketServer.hpp - WebSocket server header file
|
||||
// Based on Boost.Beast and Boost.Asio
|
||||
|
||||
#ifndef PMC_NET_WEBSOCKETSERVER_HPP
|
||||
#define PMC_NET_WEBSOCKETSERVER_HPP
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
|
||||
namespace pmc::net {
|
||||
|
||||
/**
|
||||
* @brief WebSocket connection handler type definition
|
||||
*
|
||||
* Handles message sending and receiving after WebSocket connection is established
|
||||
*/
|
||||
using WebSocketMessageHandler = std::function<void(
|
||||
const std::string& clientId,
|
||||
const std::string& message,
|
||||
std::function<void(const std::string&)> sendCallback)>;
|
||||
|
||||
/**
|
||||
* @brief WebSocket connection callback type definition
|
||||
*/
|
||||
using WebSocketConnectHandler = std::function<void(
|
||||
const std::string& clientId,
|
||||
const std::string& remoteAddress)>;
|
||||
|
||||
/**
|
||||
* @brief WebSocket disconnection callback type definition
|
||||
*/
|
||||
using WebSocketDisconnectHandler = std::function<void(
|
||||
const std::string& clientId)>;
|
||||
|
||||
/**
|
||||
* @brief WebSocket server class
|
||||
*
|
||||
* WebSocket server implemented based on Boost.Beast and Boost.Asio
|
||||
*/
|
||||
class WebSocketServer {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructor
|
||||
* @param port Listening port
|
||||
* @param threads Number of threads (default 2)
|
||||
*/
|
||||
WebSocketServer(unsigned short port, unsigned int threads = 2);
|
||||
|
||||
/**
|
||||
* @brief Destructor
|
||||
*/
|
||||
~WebSocketServer();
|
||||
|
||||
/**
|
||||
* @brief Start the server
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* @brief Stop the server
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* @brief Register message handler
|
||||
* @param path Path (for routing)
|
||||
* @param handler Message handler
|
||||
*/
|
||||
void onMessage(const std::string& path, WebSocketMessageHandler handler);
|
||||
|
||||
/**
|
||||
* @brief Register connection handler
|
||||
* @param handler Connection handler
|
||||
*/
|
||||
void onConnect(WebSocketConnectHandler handler);
|
||||
|
||||
/**
|
||||
* @brief Register disconnection handler
|
||||
* @param handler Disconnection handler
|
||||
*/
|
||||
void onDisconnect(WebSocketDisconnectHandler handler);
|
||||
|
||||
/**
|
||||
* @brief Send message to a specific client
|
||||
* @param clientId Client ID
|
||||
* @param message Message content
|
||||
* @return Whether sending was successful
|
||||
*/
|
||||
bool sendToClient(const std::string& clientId, const std::string& message);
|
||||
|
||||
/**
|
||||
* @brief Broadcast message to all clients
|
||||
* @param message Message content
|
||||
* @return Number of clients that successfully received the message
|
||||
*/
|
||||
size_t broadcast(const std::string& message);
|
||||
|
||||
/**
|
||||
* @brief Disconnect a specific client
|
||||
* @param clientId Client ID
|
||||
*/
|
||||
void disconnectClient(const std::string& clientId);
|
||||
|
||||
/**
|
||||
* @brief Get the number of currently connected clients
|
||||
* @return Number of clients
|
||||
*/
|
||||
size_t getClientCount() const;
|
||||
|
||||
/**
|
||||
* @brief Check if the server is running
|
||||
* @return Running status
|
||||
*/
|
||||
bool isRunning() const { return running_; }
|
||||
|
||||
/**
|
||||
* @brief Get the server listening port
|
||||
* @return Port number
|
||||
*/
|
||||
unsigned short getPort() const { return port_; }
|
||||
|
||||
private:
|
||||
// Internal Session class
|
||||
class Session;
|
||||
|
||||
// Internal Listener class
|
||||
class Listener;
|
||||
|
||||
// Internal implementation structure
|
||||
struct Impl {
|
||||
boost::asio::io_context ioc_;
|
||||
std::shared_ptr<Listener> listener_;
|
||||
std::vector<std::thread> threads_;
|
||||
};
|
||||
|
||||
// Server configuration
|
||||
unsigned short port_;
|
||||
unsigned int threads_;
|
||||
std::atomic<bool> running_{false};
|
||||
|
||||
// Internal implementation
|
||||
std::unique_ptr<Impl> impl_;
|
||||
|
||||
// Generate client ID
|
||||
std::string generateClientId() const;
|
||||
|
||||
// Add client
|
||||
void addClient(const std::string& clientId, std::shared_ptr<Session> session);
|
||||
|
||||
// Remove client
|
||||
void removeClient(const std::string& clientId);
|
||||
|
||||
// Get client session
|
||||
std::shared_ptr<Session> getClient(const std::string& clientId);
|
||||
|
||||
// Callback functions
|
||||
std::unordered_map<std::string, WebSocketMessageHandler> messageHandlers_;
|
||||
WebSocketConnectHandler connectHandler_;
|
||||
WebSocketDisconnectHandler disconnectHandler_;
|
||||
|
||||
// Client management
|
||||
mutable std::mutex clientsMutex_;
|
||||
std::unordered_map<std::string, std::shared_ptr<Session>> clients_;
|
||||
};
|
||||
|
||||
} // namespace pmc::net
|
||||
|
||||
#endif // PMC_NET_WEBSOCKETSERVER_HPP
|
||||
@@ -0,0 +1,604 @@
|
||||
#include "us/HttpServer.hpp"
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/ssl.h> // SSL_CTX_set_cipher_list
|
||||
|
||||
namespace pmc {
|
||||
namespace net {
|
||||
|
||||
// ===================== 自定义异常 =====================
|
||||
class HttpServerException : public std::runtime_error {
|
||||
public:
|
||||
explicit HttpServerException(const std::string& msg) : runtime_error(msg) {}
|
||||
};
|
||||
class HttpListenerException : public HttpServerException {
|
||||
public:
|
||||
explicit HttpListenerException(const std::string& msg) : HttpServerException(msg) {}
|
||||
};
|
||||
class SSLInitException : public HttpServerException {
|
||||
public:
|
||||
explicit SSLInitException(const std::string& msg) : HttpServerException(msg) {}
|
||||
};
|
||||
class CreateObjectFailed : public HttpServerException {
|
||||
public:
|
||||
explicit CreateObjectFailed(const std::string& msg) : HttpServerException(msg) {}
|
||||
};
|
||||
|
||||
// ===================== Session =====================
|
||||
class HttpServer::Session : public std::enable_shared_from_this<Session> {
|
||||
public:
|
||||
// HTTP 构造
|
||||
Session(tcp::socket&& socket, HttpServer* server)
|
||||
: socket_(std::move(socket)), server_(server), ssl_enabled_(false),
|
||||
strand_(asio::make_strand(socket_.get_executor())) {}
|
||||
|
||||
// HTTPS 构造
|
||||
Session(tcp::socket&& socket, asio::ssl::context& ssl_ctx, HttpServer* server)
|
||||
: socket_(std::move(socket)), server_(server), ssl_enabled_(true),
|
||||
strand_(asio::make_strand(socket_.get_executor())) {
|
||||
ssl_stream_ = std::make_unique<asio::ssl::stream<tcp::socket>>(
|
||||
std::move(socket_), ssl_ctx);
|
||||
}
|
||||
|
||||
~Session() { close(); }
|
||||
|
||||
void run() {
|
||||
if (ssl_enabled_) doSSLHandshake();
|
||||
else doRead();
|
||||
}
|
||||
|
||||
void setRemoteInfo(const std::string& ip, unsigned short port) {
|
||||
remote_ip_ = ip;
|
||||
remote_port_ = port;
|
||||
}
|
||||
|
||||
private:
|
||||
void close() {
|
||||
beast::error_code ec;
|
||||
if (ssl_enabled_ && ssl_stream_) {
|
||||
ssl_stream_->shutdown(ec);
|
||||
ssl_stream_->lowest_layer().close(ec);
|
||||
} else if (socket_.is_open()) {
|
||||
socket_.shutdown(tcp::socket::shutdown_send, ec);
|
||||
socket_.close(ec);
|
||||
}
|
||||
}
|
||||
|
||||
void doSSLHandshake() {
|
||||
auto self = shared_from_this();
|
||||
std::cout << "[SSL] Starting handshake for "
|
||||
<< remote_ip_ << ":" << remote_port_ << std::endl;
|
||||
|
||||
ssl_stream_->async_handshake(
|
||||
asio::ssl::stream_base::server,
|
||||
asio::bind_executor(strand_,
|
||||
[this, self](beast::error_code ec) {
|
||||
if (!ec) {
|
||||
std::cout << "[SSL] Handshake succeeded: "
|
||||
<< remote_ip_ << ":" << remote_port_ << std::endl;
|
||||
doRead();
|
||||
} else {
|
||||
std::cerr << "[SSL] Handshake error: "
|
||||
<< remote_ip_ << ":" << remote_port_
|
||||
<< " - " << ec.message()
|
||||
<< " (code=" << ec.value() << ")" << std::endl;
|
||||
// 输出 OpenSSL 详细错误堆栈
|
||||
ERR_print_errors_fp(stderr);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void doRead() {
|
||||
auto self = shared_from_this();
|
||||
if (ssl_enabled_ && ssl_stream_) {
|
||||
http::async_read(*ssl_stream_, buffer_, request_,
|
||||
asio::bind_executor(strand_,
|
||||
[this, self](beast::error_code ec, std::size_t) { handleRead(ec); }));
|
||||
} else {
|
||||
http::async_read(socket_, buffer_, request_,
|
||||
asio::bind_executor(strand_,
|
||||
[this, self](beast::error_code ec, std::size_t) { handleRead(ec); }));
|
||||
}
|
||||
}
|
||||
|
||||
void handleRead(beast::error_code ec) {
|
||||
if (!ec) {
|
||||
auto req_copy = std::move(request_);
|
||||
request_ = {};
|
||||
processRequest(std::move(req_copy));
|
||||
} else if (ec != http::error::end_of_stream) {
|
||||
std::cerr << "[Read] Error from " << remote_ip_ << ":" << remote_port_
|
||||
<< ": " << ec.message() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void processRequest(http::request<http::string_body> req) {
|
||||
try {
|
||||
auto resp = std::make_shared<http::response<http::string_body>>(
|
||||
server_->handleRequest(req));
|
||||
doWrite(resp);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[Request] Handler exception: " << e.what() << std::endl;
|
||||
auto err_resp = std::make_shared<http::response<http::string_body>>();
|
||||
err_resp->version(req.version());
|
||||
err_resp->result(http::status::internal_server_error);
|
||||
err_resp->set(http::field::content_type, "text/plain");
|
||||
err_resp->body() = "500 Internal Server Error\n";
|
||||
err_resp->prepare_payload();
|
||||
doWrite(err_resp);
|
||||
}
|
||||
}
|
||||
|
||||
void doWrite(std::shared_ptr<http::response<http::string_body>> response) {
|
||||
auto self = shared_from_this();
|
||||
if (ssl_enabled_ && ssl_stream_) {
|
||||
http::async_write(*ssl_stream_, *response,
|
||||
asio::bind_executor(strand_,
|
||||
[this, self, response](beast::error_code ec, std::size_t) {
|
||||
if (ec) {
|
||||
std::cerr << "[Write] Error to " << remote_ip_ << ":" << remote_port_
|
||||
<< ": " << ec.message() << std::endl;
|
||||
return;
|
||||
}
|
||||
if (!response->keep_alive()) close();
|
||||
else doRead();
|
||||
}));
|
||||
} else {
|
||||
http::async_write(socket_, *response,
|
||||
asio::bind_executor(strand_,
|
||||
[this, self, response](beast::error_code ec, std::size_t) {
|
||||
if (ec) {
|
||||
std::cerr << "[Write] Error to " << remote_ip_ << ":" << remote_port_
|
||||
<< ": " << ec.message() << std::endl;
|
||||
return;
|
||||
}
|
||||
if (!response->keep_alive()) close();
|
||||
else doRead();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
tcp::socket socket_;
|
||||
std::unique_ptr<asio::ssl::stream<tcp::socket>> ssl_stream_;
|
||||
beast::flat_buffer buffer_;
|
||||
http::request<http::string_body> request_;
|
||||
HttpServer* server_;
|
||||
bool ssl_enabled_;
|
||||
|
||||
// 使用 any_io_executor 兼容多种执行器
|
||||
asio::strand<asio::any_io_executor> strand_;
|
||||
|
||||
std::string remote_ip_ = "unknown";
|
||||
unsigned short remote_port_ = 0;
|
||||
};
|
||||
|
||||
// ===================== Listener =====================
|
||||
class HttpServer::Listener : public std::enable_shared_from_this<Listener> {
|
||||
public:
|
||||
Listener(asio::io_context& ioc, tcp::endpoint endpoint, HttpServer* server)
|
||||
: ioc_(ioc), acceptor_(ioc), server_(server), ssl_enabled_(false) {
|
||||
initAcceptor(endpoint);
|
||||
}
|
||||
|
||||
Listener(asio::io_context& ioc, tcp::endpoint endpoint,
|
||||
asio::ssl::context& ssl_ctx, HttpServer* server)
|
||||
: ioc_(ioc), acceptor_(ioc), ssl_ctx_(&ssl_ctx), server_(server), ssl_enabled_(true) {
|
||||
initAcceptor(endpoint);
|
||||
}
|
||||
|
||||
void run() {
|
||||
if (acceptor_.is_open()) doAccept();
|
||||
}
|
||||
|
||||
private:
|
||||
void initAcceptor(tcp::endpoint ep) {
|
||||
beast::error_code ec;
|
||||
acceptor_.open(ep.protocol(), ec);
|
||||
if (!ec) {
|
||||
acceptor_.set_option(asio::socket_base::reuse_address(true), ec);
|
||||
acceptor_.bind(ep, ec);
|
||||
if (!ec) acceptor_.listen(asio::socket_base::max_listen_connections, ec);
|
||||
}
|
||||
if (ec) throw HttpListenerException("Listener error: " + ec.message());
|
||||
}
|
||||
|
||||
void doAccept() {
|
||||
acceptor_.async_accept(
|
||||
[this, self = shared_from_this()](beast::error_code ec, tcp::socket socket) {
|
||||
if (!ec) {
|
||||
auto ep = socket.remote_endpoint(ec);
|
||||
std::string ip = ec ? "unknown" : ep.address().to_string();
|
||||
unsigned short port = ec ? 0 : ep.port();
|
||||
std::cout << "[Listener] New connection from " << ip << ":" << port << std::endl;
|
||||
|
||||
std::shared_ptr<Session> session;
|
||||
if (ssl_enabled_ && ssl_ctx_) {
|
||||
session = std::make_shared<Session>(std::move(socket), *ssl_ctx_, server_);
|
||||
} else {
|
||||
session = std::make_shared<Session>(std::move(socket), server_);
|
||||
}
|
||||
session->setRemoteInfo(ip, port);
|
||||
session->run();
|
||||
} else {
|
||||
std::cerr << "[Listener] Accept error: " << ec.message() << std::endl;
|
||||
}
|
||||
doAccept();
|
||||
});
|
||||
}
|
||||
|
||||
asio::io_context& ioc_;
|
||||
tcp::acceptor acceptor_;
|
||||
asio::ssl::context* ssl_ctx_ = nullptr;
|
||||
HttpServer* server_;
|
||||
bool ssl_enabled_;
|
||||
};
|
||||
|
||||
// ===================== HttpServer 实现 =====================
|
||||
|
||||
// ---- 构造函数 ----
|
||||
HttpServer::HttpServer(const std::string& address, unsigned short port, unsigned int threads)
|
||||
: address_(address), port_(port), threads_(threads), ssl_enabled_(false),
|
||||
ioc_(std::make_unique<asio::io_context>(threads)) {
|
||||
if (!ioc_) throw CreateObjectFailed("Failed to create io_context");
|
||||
}
|
||||
|
||||
HttpServer::HttpServer(const std::string& address, unsigned short port,
|
||||
const std::string& cert_file, const std::string& key_file,
|
||||
unsigned int threads)
|
||||
: address_(address), port_(port), threads_(threads), ssl_enabled_(true),
|
||||
cert_file_(cert_file), key_file_(key_file),
|
||||
ioc_(std::make_unique<asio::io_context>(threads)) {
|
||||
if (!ioc_) throw CreateObjectFailed("Failed to create io_context");
|
||||
initSSLContext();
|
||||
}
|
||||
|
||||
HttpServer::~HttpServer() { stop(); }
|
||||
|
||||
// ---- SSL 初始化 ----
|
||||
void HttpServer::setPrivateKeyPassword(const std::string& password) {
|
||||
private_key_password_ = password;
|
||||
}
|
||||
|
||||
void HttpServer::initSSLContext() {
|
||||
if (!ssl_enabled_) return;
|
||||
try {
|
||||
ssl_ctx_ = std::make_unique<asio::ssl::context>(asio::ssl::context::tls);
|
||||
ssl_ctx_->set_options(
|
||||
asio::ssl::context::default_workarounds |
|
||||
asio::ssl::context::no_sslv2 |
|
||||
asio::ssl::context::no_sslv3 |
|
||||
asio::ssl::context::single_dh_use);
|
||||
|
||||
// 设置现代加密套件,确保兼容主流客户端
|
||||
SSL_CTX_set_cipher_list(ssl_ctx_->native_handle(),
|
||||
"ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!MD5:!DSS");
|
||||
|
||||
if (!private_key_password_.empty()) {
|
||||
ssl_ctx_->set_password_callback(
|
||||
[this](std::size_t, asio::ssl::context::password_purpose) {
|
||||
return private_key_password_;
|
||||
});
|
||||
}
|
||||
|
||||
std::cout << "[SSL] Loading certificate chain: " << cert_file_ << std::endl;
|
||||
ssl_ctx_->use_certificate_chain_file(cert_file_);
|
||||
|
||||
std::cout << "[SSL] Loading private key: " << key_file_ << std::endl;
|
||||
ssl_ctx_->use_private_key_file(key_file_, asio::ssl::context::pem);
|
||||
|
||||
std::cout << "[SSL] SSL context ready." << std::endl;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[SSL] Init failed: " << e.what() << std::endl;
|
||||
ERR_print_errors_fp(stderr);
|
||||
throw SSLInitException("Failed to initialize SSL context");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 启动/停止 ----
|
||||
void HttpServer::start() {
|
||||
if (running_) return;
|
||||
try {
|
||||
auto addr = asio::ip::make_address(address_);
|
||||
tcp::endpoint ep(addr, port_);
|
||||
if (ssl_enabled_ && ssl_ctx_) {
|
||||
listener_ = std::make_shared<Listener>(*ioc_, ep, *ssl_ctx_, this);
|
||||
} else {
|
||||
listener_ = std::make_shared<Listener>(*ioc_, ep, this);
|
||||
}
|
||||
listener_->run();
|
||||
running_ = true;
|
||||
|
||||
for (unsigned i = 0; i < threads_; ++i) {
|
||||
worker_threads_.emplace_back([this]() {
|
||||
try { ioc_->run(); }
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "[Thread] Exception: " << e.what() << std::endl;
|
||||
}
|
||||
});
|
||||
}
|
||||
std::cout << (ssl_enabled_ ? "HTTPS" : "HTTP")
|
||||
<< " Server started on " << address_ << ":" << port_
|
||||
<< " with " << threads_ << " threads\n";
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[Start] Failed: " << e.what() << std::endl;
|
||||
running_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void HttpServer::stop() {
|
||||
if (!running_) return;
|
||||
running_ = false;
|
||||
closeAllSessions();
|
||||
if (ioc_) ioc_->stop();
|
||||
for (auto& t : worker_threads_) if (t.joinable()) t.join();
|
||||
worker_threads_.clear();
|
||||
listener_.reset();
|
||||
std::cout << (ssl_enabled_ ? "HTTPS" : "HTTP") << " Server stopped\n";
|
||||
}
|
||||
|
||||
void HttpServer::run() {
|
||||
start();
|
||||
if (ioc_) ioc_->run();
|
||||
}
|
||||
|
||||
// ---- 状态查询 ----
|
||||
bool HttpServer::isRunning() const { return running_; }
|
||||
unsigned short HttpServer::getPort() const { return port_; }
|
||||
std::string HttpServer::getAddress() const { return address_; }
|
||||
bool HttpServer::isSSLEnabled() const { return ssl_enabled_; }
|
||||
|
||||
// ---- 路由注册 ----
|
||||
void HttpServer::get(const std::string& path, HttpRequestHandler handler) {
|
||||
RouteEntry entry;
|
||||
entry.path = path;
|
||||
entry.handler = handler;
|
||||
entry.is_wildcard = (path.find('*') != std::string::npos ||
|
||||
path.find('?') != std::string::npos);
|
||||
if (entry.is_wildcard) entry.pattern = wildcardToRegex(path);
|
||||
get_handlers_.push_back(entry);
|
||||
}
|
||||
|
||||
void HttpServer::post(const std::string& path, HttpRequestHandler handler) {
|
||||
RouteEntry entry;
|
||||
entry.path = path;
|
||||
entry.handler = handler;
|
||||
entry.is_wildcard = (path.find('*') != std::string::npos ||
|
||||
path.find('?') != std::string::npos);
|
||||
if (entry.is_wildcard) entry.pattern = wildcardToRegex(path);
|
||||
post_handlers_.push_back(entry);
|
||||
}
|
||||
|
||||
void HttpServer::put(const std::string& path, HttpRequestHandler handler) {
|
||||
RouteEntry entry;
|
||||
entry.path = path;
|
||||
entry.handler = handler;
|
||||
entry.is_wildcard = (path.find('*') != std::string::npos ||
|
||||
path.find('?') != std::string::npos);
|
||||
if (entry.is_wildcard) entry.pattern = wildcardToRegex(path);
|
||||
put_handlers_.push_back(entry);
|
||||
}
|
||||
|
||||
void HttpServer::del(const std::string& path, HttpRequestHandler handler) {
|
||||
RouteEntry entry;
|
||||
entry.path = path;
|
||||
entry.handler = handler;
|
||||
entry.is_wildcard = (path.find('*') != std::string::npos ||
|
||||
path.find('?') != std::string::npos);
|
||||
if (entry.is_wildcard) entry.pattern = wildcardToRegex(path);
|
||||
delete_handlers_.push_back(entry);
|
||||
}
|
||||
|
||||
void HttpServer::use(MiddlewareHandler m) {
|
||||
middlewares_.push_back(m);
|
||||
}
|
||||
|
||||
void HttpServer::setStaticDirectory(const std::string& p) {
|
||||
static_directory_ = p;
|
||||
}
|
||||
|
||||
// ---- 通配符转换 ----
|
||||
std::regex HttpServer::wildcardToRegex(const std::string& pattern) {
|
||||
std::string r;
|
||||
r.reserve(pattern.size() * 2);
|
||||
for (char c : pattern) {
|
||||
if (c == '*') r += ".*";
|
||||
else if (c == '?') r += ".";
|
||||
else if (c == '.' || c == '+' || c == '(' || c == ')' || c == '[' || c == ']' ||
|
||||
c == '{' || c == '}' || c == '^' || c == '$' || c == '|' || c == '\\') {
|
||||
r += '\\';
|
||||
r += c;
|
||||
} else r += c;
|
||||
}
|
||||
return std::regex(r, std::regex::icase);
|
||||
}
|
||||
|
||||
bool HttpServer::matchPath(const std::string& path, const std::regex& pattern) {
|
||||
return std::regex_match(path, pattern);
|
||||
}
|
||||
|
||||
// ---- 查询参数解析 ----
|
||||
std::unordered_map<std::string, std::string> HttpServer::parseQueryParams(const std::string& query) {
|
||||
std::unordered_map<std::string, std::string> params;
|
||||
if (query.empty()) return params;
|
||||
|
||||
std::vector<std::string> pairs;
|
||||
boost::split(pairs, query, boost::is_any_of("&"));
|
||||
for (const auto& pair : pairs) {
|
||||
std::vector<std::string> kv;
|
||||
boost::split(kv, pair, boost::is_any_of("="));
|
||||
if (kv.size() == 2) params[kv[0]] = kv[1];
|
||||
else if (kv.size() == 1) params[kv[0]] = "";
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
// ---- 路由查找 ----
|
||||
HttpRequestHandler HttpServer::findRoute(const std::string& method, const std::string& path) {
|
||||
const std::vector<RouteEntry>* handlers = nullptr;
|
||||
if (method == "GET") handlers = &get_handlers_;
|
||||
else if (method == "POST") handlers = &post_handlers_;
|
||||
else if (method == "PUT") handlers = &put_handlers_;
|
||||
else if (method == "DELETE") handlers = &delete_handlers_;
|
||||
else return nullptr;
|
||||
|
||||
// 精确匹配优先
|
||||
for (const auto& e : *handlers) {
|
||||
if (!e.is_wildcard && e.path == path) return e.handler;
|
||||
}
|
||||
// 通配符匹配
|
||||
for (const auto& e : *handlers) {
|
||||
if (e.is_wildcard && matchPath(path, e.pattern)) return e.handler;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ---- 请求处理核心 ----
|
||||
http::response<http::string_body> HttpServer::handleRequest(const http::request<http::string_body>& req) {
|
||||
std::string target_path(req.target().data(), req.target().size());
|
||||
std::string query_string;
|
||||
auto pos = target_path.find('?');
|
||||
if (pos != std::string::npos) {
|
||||
query_string = target_path.substr(pos + 1);
|
||||
target_path = target_path.substr(0, pos);
|
||||
}
|
||||
auto params = parseQueryParams(query_string);
|
||||
|
||||
http::response<http::string_body> res;
|
||||
res.version(req.version());
|
||||
res.set(http::field::server, "Boost.Beast HttpServer");
|
||||
res.set(http::field::content_type, "text/plain");
|
||||
res.keep_alive(req.keep_alive());
|
||||
|
||||
// 执行中间件
|
||||
bool middleware_handled = false;
|
||||
for (auto& m : middlewares_) {
|
||||
if (!m(req, res, params)) {
|
||||
middleware_handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (middleware_handled) {
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string method = http::to_string(req.method());
|
||||
auto handler = findRoute(method, target_path);
|
||||
|
||||
// 静态文件
|
||||
if (!handler && !static_directory_.empty()) {
|
||||
std::string file_path = static_directory_ + target_path;
|
||||
std::ifstream file(file_path, std::ios::binary);
|
||||
if (file.is_open()) {
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
res.result(http::status::ok);
|
||||
res.body() = buffer.str();
|
||||
|
||||
if (boost::algorithm::ends_with(file_path, ".html"))
|
||||
res.set(http::field::content_type, "text/html");
|
||||
else if (boost::algorithm::ends_with(file_path, ".css"))
|
||||
res.set(http::field::content_type, "text/css");
|
||||
else if (boost::algorithm::ends_with(file_path, ".js"))
|
||||
res.set(http::field::content_type, "application/javascript");
|
||||
else if (boost::algorithm::ends_with(file_path, ".json"))
|
||||
res.set(http::field::content_type, "application/json");
|
||||
else if (boost::algorithm::ends_with(file_path, ".png"))
|
||||
res.set(http::field::content_type, "image/png");
|
||||
else if (boost::algorithm::ends_with(file_path, ".jpg") ||
|
||||
boost::algorithm::ends_with(file_path, ".jpeg"))
|
||||
res.set(http::field::content_type, "image/jpeg");
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
// 路由处理
|
||||
if (handler) {
|
||||
try {
|
||||
return handler(req, params);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[Handler] Exception: " << e.what() << std::endl;
|
||||
res.result(http::status::internal_server_error);
|
||||
res.body() = "500 Internal Server Error\n";
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
// 404
|
||||
res.result(http::status::not_found);
|
||||
res.body() = "404 Not Found\n";
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
}
|
||||
|
||||
// ---- 状态显示 ----
|
||||
void HttpServer::showStatus() const {
|
||||
std::cout << "=== " << (ssl_enabled_ ? "HTTPS" : "HTTP") << " Server Status ===" << std::endl;
|
||||
std::cout << "Protocol: " << (ssl_enabled_ ? "HTTPS (SSL/TLS)" : "HTTP") << std::endl;
|
||||
std::cout << "Address: " << address_ << std::endl;
|
||||
std::cout << "Port: " << port_ << std::endl;
|
||||
std::cout << "Threads: " << threads_ << std::endl;
|
||||
std::cout << "Running: " << (running_ ? "Yes" : "No") << std::endl;
|
||||
if (ssl_enabled_) {
|
||||
std::cout << "Certificate: " << cert_file_ << std::endl;
|
||||
std::cout << "Private Key: " << key_file_ << std::endl;
|
||||
}
|
||||
std::cout << "Static Directory: " << (static_directory_.empty() ? "Not set" : static_directory_) << std::endl;
|
||||
std::cout << "Registered Routes:" << std::endl;
|
||||
std::cout << " GET: " << get_handlers_.size() << " handler(s)" << std::endl;
|
||||
for (const auto& entry : get_handlers_)
|
||||
std::cout << " GET " << entry.path << (entry.is_wildcard ? " (wildcard)" : "") << std::endl;
|
||||
std::cout << " POST: " << post_handlers_.size() << " handler(s)" << std::endl;
|
||||
for (const auto& entry : post_handlers_)
|
||||
std::cout << " POST " << entry.path << (entry.is_wildcard ? " (wildcard)" : "") << std::endl;
|
||||
std::cout << " PUT: " << put_handlers_.size() << " handler(s)" << std::endl;
|
||||
for (const auto& entry : put_handlers_)
|
||||
std::cout << " PUT " << entry.path << (entry.is_wildcard ? " (wildcard)" : "") << std::endl;
|
||||
std::cout << " DELETE: " << delete_handlers_.size() << " handler(s)" << std::endl;
|
||||
for (const auto& entry : delete_handlers_)
|
||||
std::cout << " DELETE " << entry.path << (entry.is_wildcard ? " (wildcard)" : "") << std::endl;
|
||||
std::cout << "Middlewares: " << middlewares_.size() << std::endl;
|
||||
std::cout << "==========================" << std::endl;
|
||||
}
|
||||
|
||||
// ---- 会话管理 ----
|
||||
void HttpServer::addSession(const std::shared_ptr<Session>& session) {
|
||||
std::lock_guard<std::mutex> lock(sessions_mutex_);
|
||||
sessions_.erase(std::remove_if(sessions_.begin(), sessions_.end(),
|
||||
[](const std::weak_ptr<Session>& wp) { return wp.expired(); }), sessions_.end());
|
||||
sessions_.push_back(session);
|
||||
}
|
||||
|
||||
void HttpServer::removeSession(const Session* session) {
|
||||
std::lock_guard<std::mutex> lock(sessions_mutex_);
|
||||
sessions_.erase(std::remove_if(sessions_.begin(), sessions_.end(),
|
||||
[session](const std::weak_ptr<Session>& wp) {
|
||||
auto sp = wp.lock();
|
||||
return !sp || sp.get() == session;
|
||||
}), sessions_.end());
|
||||
}
|
||||
|
||||
void HttpServer::closeAllSessions() {
|
||||
std::lock_guard<std::mutex> lock(sessions_mutex_);
|
||||
for (auto& weak : sessions_) {
|
||||
auto sp = weak.lock();
|
||||
if (sp) { /* 会话析构时自动关闭 socket */ }
|
||||
}
|
||||
sessions_.clear();
|
||||
}
|
||||
|
||||
// ---- 调试工具 ----
|
||||
void HttpServer::debug::print_all_header_fields(const http::request<http::string_body>& req) {
|
||||
for (auto const& field : req) {
|
||||
std::cout << "Name: " << field.name_string()
|
||||
<< ", Value: " << field.value() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace pmc
|
||||
@@ -0,0 +1,386 @@
|
||||
#include "us/WebSocketClient.hpp"
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/asio/dispatch.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <boost/asio/connect.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <condition_variable>
|
||||
#include <atomic>
|
||||
|
||||
namespace pmc {
|
||||
namespace net {
|
||||
|
||||
namespace beast = boost::beast;
|
||||
namespace asio = boost::asio;
|
||||
using tcp = asio::ip::tcp;
|
||||
|
||||
// Internal implementation structure
|
||||
struct WebSocketClient::Impl {
|
||||
// Configuration parameters
|
||||
std::string host;
|
||||
std::string port;
|
||||
std::string path;
|
||||
|
||||
// Boost objects
|
||||
asio::io_context ioc;
|
||||
tcp::resolver resolver;
|
||||
beast::websocket::stream<beast::tcp_stream> ws;
|
||||
|
||||
// Thread management
|
||||
std::thread io_thread;
|
||||
std::atomic<bool> running{false};
|
||||
|
||||
// Callback functions
|
||||
WebSocketClientBase::MessageCallback message_callback;
|
||||
WebSocketClientBase::ErrorCallback error_callback;
|
||||
WebSocketClientBase::ConnectCallback connect_callback;
|
||||
WebSocketClientBase::CloseCallback close_callback;
|
||||
|
||||
// Message queue
|
||||
std::queue<std::string> send_queue;
|
||||
std::mutex queue_mutex;
|
||||
std::condition_variable queue_cv;
|
||||
std::atomic<bool> sending{false};
|
||||
|
||||
// Status flags
|
||||
std::atomic<bool> connected{false};
|
||||
std::atomic<bool> connecting{false};
|
||||
std::atomic<bool> stopping{false};
|
||||
|
||||
// Configuration options
|
||||
bool auto_reconnect{false};
|
||||
int reconnect_interval_ms{3000};
|
||||
int heartbeat_interval_ms{30000};
|
||||
|
||||
// Constructor
|
||||
Impl(const std::string& host, const std::string& port, const std::string& path)
|
||||
: host(host)
|
||||
, port(port)
|
||||
, path(path)
|
||||
, resolver(asio::make_strand(ioc))
|
||||
, ws(asio::make_strand(ioc))
|
||||
{
|
||||
}
|
||||
|
||||
// Destructor
|
||||
~Impl() {
|
||||
stop();
|
||||
}
|
||||
|
||||
// Start IO thread
|
||||
void start() {
|
||||
if (!running) {
|
||||
running = true;
|
||||
io_thread = std::thread([this]() {
|
||||
while (running) {
|
||||
try {
|
||||
ioc.run();
|
||||
break;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "IO context exception: " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Stop IO thread
|
||||
void stop() {
|
||||
running = false;
|
||||
stopping = true;
|
||||
|
||||
if (io_thread.joinable()) {
|
||||
ioc.stop();
|
||||
io_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
// Perform connection
|
||||
void doConnect() {
|
||||
if (connecting || connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
connecting = true;
|
||||
connected = false;
|
||||
|
||||
// Resolve hostname
|
||||
resolver.async_resolve(host, port,
|
||||
[this](beast::error_code ec, tcp::resolver::results_type results) {
|
||||
onResolve(ec, results);
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve completion callback
|
||||
void onResolve(beast::error_code ec, tcp::resolver::results_type results) {
|
||||
if (ec) {
|
||||
onError("Resolve failed: " + ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect to resolved endpoints
|
||||
beast::get_lowest_layer(ws).async_connect(results,
|
||||
[this](beast::error_code ec, tcp::resolver::results_type::endpoint_type) {
|
||||
onConnect(ec);
|
||||
});
|
||||
}
|
||||
|
||||
// Connection completion callback
|
||||
void onConnect(beast::error_code ec) {
|
||||
if (ec) {
|
||||
onError("Connection failed: " + ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
// Set WebSocket options
|
||||
ws.set_option(beast::websocket::stream_base::timeout::suggested(
|
||||
beast::role_type::client));
|
||||
|
||||
// Perform WebSocket handshake
|
||||
ws.async_handshake(host, path,
|
||||
[this](beast::error_code ec) {
|
||||
onHandshake(ec);
|
||||
});
|
||||
}
|
||||
|
||||
// Handshake completion callback
|
||||
void onHandshake(beast::error_code ec) {
|
||||
connecting = false;
|
||||
|
||||
if (ec) {
|
||||
onError("Handshake failed: " + ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
connected = true;
|
||||
|
||||
// Call connection success callback
|
||||
if (connect_callback) {
|
||||
connect_callback(true);
|
||||
}
|
||||
|
||||
// Start reading messages
|
||||
doRead();
|
||||
|
||||
// Start sending queued messages
|
||||
doSend();
|
||||
}
|
||||
|
||||
// Read messages
|
||||
void doRead() {
|
||||
if (!connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
ws.async_read(buffer,
|
||||
[this](beast::error_code ec, std::size_t bytes_transferred) {
|
||||
onRead(ec, bytes_transferred);
|
||||
});
|
||||
}
|
||||
|
||||
// Read completion callback
|
||||
void onRead(beast::error_code ec, std::size_t bytes_transferred) {
|
||||
if (ec) {
|
||||
if (ec == beast::websocket::error::closed) {
|
||||
// Normal close
|
||||
onClose();
|
||||
} else {
|
||||
onError("Read failed: " + ec.message());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Process received message
|
||||
std::string message = beast::buffers_to_string(buffer.data());
|
||||
buffer.consume(bytes_transferred);
|
||||
|
||||
if (message_callback) {
|
||||
message_callback(message);
|
||||
}
|
||||
|
||||
// Continue reading
|
||||
doRead();
|
||||
}
|
||||
|
||||
// Send messages
|
||||
void doSend() {
|
||||
if (!connected || sending) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> lock(queue_mutex);
|
||||
if (send_queue.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
sending = true;
|
||||
std::string message = std::move(send_queue.front());
|
||||
send_queue.pop();
|
||||
lock.unlock();
|
||||
|
||||
ws.async_write(asio::buffer(message),
|
||||
[this](beast::error_code ec, std::size_t bytes_transferred) {
|
||||
onWrite(ec, bytes_transferred);
|
||||
});
|
||||
}
|
||||
|
||||
// Write completion callback
|
||||
void onWrite(beast::error_code ec, std::size_t bytes_transferred) {
|
||||
sending = false;
|
||||
|
||||
if (ec) {
|
||||
onError("Write failed: " + ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
// Continue sending queued messages
|
||||
doSend();
|
||||
}
|
||||
|
||||
// Error handling
|
||||
void onError(const std::string& error) {
|
||||
connecting = false;
|
||||
connected = false;
|
||||
|
||||
std::cerr << "WebSocket error: " << error << std::endl;
|
||||
|
||||
if (error_callback) {
|
||||
error_callback(error);
|
||||
}
|
||||
|
||||
// Auto reconnect
|
||||
if (auto_reconnect && !stopping) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(reconnect_interval_ms));
|
||||
asio::post(ioc, [this]() { doConnect(); });
|
||||
}
|
||||
}
|
||||
|
||||
// Connection close
|
||||
void onClose() {
|
||||
connected = false;
|
||||
|
||||
if (close_callback) {
|
||||
close_callback();
|
||||
}
|
||||
|
||||
// Auto reconnect
|
||||
if (auto_reconnect && !stopping) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(reconnect_interval_ms));
|
||||
asio::post(ioc, [this]() { doConnect(); });
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
beast::flat_buffer buffer;
|
||||
};
|
||||
|
||||
// WebSocketClient implementation
|
||||
WebSocketClient::WebSocketClient(const std::string& host, const std::string& port, const std::string& path)
|
||||
: impl_(std::make_unique<Impl>(host, port, path))
|
||||
{
|
||||
impl_->start();
|
||||
}
|
||||
|
||||
WebSocketClient::~WebSocketClient() {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
void WebSocketClient::setMessageCallback(MessageCallback callback) {
|
||||
impl_->message_callback = callback;
|
||||
}
|
||||
|
||||
void WebSocketClient::setErrorCallback(ErrorCallback callback) {
|
||||
impl_->error_callback = callback;
|
||||
}
|
||||
|
||||
void WebSocketClient::setConnectCallback(ConnectCallback callback) {
|
||||
impl_->connect_callback = callback;
|
||||
}
|
||||
|
||||
void WebSocketClient::setCloseCallback(CloseCallback callback) {
|
||||
impl_->close_callback = callback;
|
||||
}
|
||||
|
||||
bool WebSocketClient::connect(int timeout_ms) {
|
||||
if (impl_->connecting || impl_->connected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
impl_->doConnect();
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebSocketClient::disconnect() {
|
||||
if (!impl_->connected && !impl_->connecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
impl_->stopping = true;
|
||||
|
||||
asio::post(impl_->ioc, [this]() {
|
||||
beast::error_code ec;
|
||||
impl_->ws.close(beast::websocket::close_code::normal, ec);
|
||||
impl_->connected = false;
|
||||
impl_->connecting = false;
|
||||
});
|
||||
}
|
||||
|
||||
bool WebSocketClient::isConnected() const {
|
||||
return impl_->connected;
|
||||
}
|
||||
|
||||
bool WebSocketClient::send(const std::string& message) {
|
||||
if (!impl_->connected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(impl_->queue_mutex);
|
||||
impl_->send_queue.push(message);
|
||||
}
|
||||
|
||||
impl_->queue_cv.notify_one();
|
||||
asio::post(impl_->ioc, [this]() { impl_->doSend(); });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WebSocketClient::sendBinary(const void* data, size_t size) {
|
||||
if (!impl_->connected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string message(static_cast<const char*>(data), size);
|
||||
return send(message);
|
||||
}
|
||||
|
||||
void WebSocketClient::setAutoReconnect(bool enable, int interval_ms) {
|
||||
impl_->auto_reconnect = enable;
|
||||
impl_->reconnect_interval_ms = interval_ms;
|
||||
}
|
||||
|
||||
void WebSocketClient::setHeartbeatInterval(int interval_ms) {
|
||||
impl_->heartbeat_interval_ms = interval_ms;
|
||||
}
|
||||
|
||||
std::string WebSocketClient::getHost() const {
|
||||
return impl_->host;
|
||||
}
|
||||
|
||||
std::string WebSocketClient::getPort() const {
|
||||
return impl_->port;
|
||||
}
|
||||
|
||||
std::string WebSocketClient::getPath() const {
|
||||
return impl_->path;
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace pmc
|
||||
@@ -0,0 +1,383 @@
|
||||
// WebSocketServer.cpp - WebSocket server implementation
|
||||
// Based on Boost.Beast and Boost.Asio
|
||||
|
||||
#include "us/WebSocketServer.hpp"
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
#include <deque>
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
|
||||
namespace beast = boost::beast;
|
||||
namespace websocket = beast::websocket;
|
||||
namespace asio = boost::asio;
|
||||
using tcp = asio::ip::tcp;
|
||||
|
||||
namespace pmc::net {
|
||||
|
||||
// Internal Session class implementation
|
||||
class WebSocketServer::Session : public std::enable_shared_from_this<Session> {
|
||||
public:
|
||||
Session(tcp::socket socket, WebSocketServer* server)
|
||||
: ws_(std::move(socket))
|
||||
, server_(server) {
|
||||
|
||||
// Get remote address
|
||||
auto endpoint = ws_.next_layer().socket().remote_endpoint();
|
||||
remoteAddress_ = endpoint.address().to_string() + ":" + std::to_string(endpoint.port());
|
||||
}
|
||||
|
||||
void start() {
|
||||
// Generate client ID
|
||||
if (server_) {
|
||||
clientId_ = server_->generateClientId();
|
||||
} else {
|
||||
// If server is null, use default ID
|
||||
static std::atomic<uint64_t> counter{0};
|
||||
clientId_ = "client_" + std::to_string(++counter);
|
||||
}
|
||||
|
||||
// Asynchronously accept WebSocket handshake
|
||||
ws_.async_accept(
|
||||
beast::bind_front_handler(
|
||||
&Session::onAccept,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
void send(const std::string& message) {
|
||||
// Add message to send queue
|
||||
bool write_in_progress = !sendQueue_.empty();
|
||||
sendQueue_.push_back(message);
|
||||
|
||||
if (!write_in_progress) {
|
||||
// Start asynchronous write
|
||||
doWrite();
|
||||
}
|
||||
}
|
||||
|
||||
const std::string& getClientId() const { return clientId_; }
|
||||
const std::string& getRemoteAddress() const { return remoteAddress_; }
|
||||
|
||||
private:
|
||||
websocket::stream<beast::tcp_stream> ws_;
|
||||
beast::flat_buffer buffer_;
|
||||
std::deque<std::string> sendQueue_;
|
||||
WebSocketServer* server_;
|
||||
std::string clientId_;
|
||||
std::string remoteAddress_;
|
||||
|
||||
void onAccept(beast::error_code ec) {
|
||||
if (ec) {
|
||||
std::cerr << "WebSocket accept error: " << ec.message() << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to server client list
|
||||
if (server_) {
|
||||
server_->addClient(clientId_, shared_from_this());
|
||||
|
||||
// Call connection handler
|
||||
if (server_->connectHandler_) {
|
||||
server_->connectHandler_(clientId_, remoteAddress_);
|
||||
}
|
||||
}
|
||||
|
||||
// Start reading messages
|
||||
doRead();
|
||||
}
|
||||
|
||||
void doRead() {
|
||||
// Asynchronously read message
|
||||
ws_.async_read(
|
||||
buffer_,
|
||||
beast::bind_front_handler(
|
||||
&Session::onRead,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
void onRead(beast::error_code ec, std::size_t bytes_transferred) {
|
||||
if (ec == websocket::error::closed) {
|
||||
// Normal close
|
||||
if (server_) {
|
||||
server_->removeClient(clientId_);
|
||||
if (server_->disconnectHandler_) {
|
||||
server_->disconnectHandler_(clientId_);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (ec) {
|
||||
std::cerr << "WebSocket read error: " << ec.message() << std::endl;
|
||||
if (server_) {
|
||||
server_->removeClient(clientId_);
|
||||
if (server_->disconnectHandler_) {
|
||||
server_->disconnectHandler_(clientId_);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Process received message
|
||||
std::string message = beast::buffers_to_string(buffer_.data());
|
||||
buffer_.consume(bytes_transferred);
|
||||
|
||||
if (server_) {
|
||||
// Find message handler
|
||||
for (const auto& handler : server_->messageHandlers_) {
|
||||
// Path matching logic can be added here
|
||||
handler.second(clientId_, message, [this](const std::string& response) {
|
||||
send(response);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Continue reading
|
||||
doRead();
|
||||
}
|
||||
|
||||
void doWrite() {
|
||||
if (sendQueue_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get first message from queue
|
||||
auto message = sendQueue_.front();
|
||||
|
||||
// Asynchronously write message
|
||||
ws_.async_write(
|
||||
asio::buffer(message),
|
||||
beast::bind_front_handler(
|
||||
&Session::onWrite,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
void onWrite(beast::error_code ec, std::size_t bytes_transferred) {
|
||||
(void)bytes_transferred;
|
||||
if (ec) {
|
||||
std::cerr << "WebSocket write error: " << ec.message() << std::endl;
|
||||
if (server_) {
|
||||
server_->removeClient(clientId_);
|
||||
if (server_->disconnectHandler_) {
|
||||
server_->disconnectHandler_(clientId_);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove sent message
|
||||
sendQueue_.pop_front();
|
||||
|
||||
// Continue sending next message in queue
|
||||
if (!sendQueue_.empty()) {
|
||||
doWrite();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Internal Listener class implementation
|
||||
class WebSocketServer::Listener : public std::enable_shared_from_this<Listener> {
|
||||
public:
|
||||
static std::shared_ptr<Listener> create(asio::io_context& ioc, tcp::endpoint endpoint, WebSocketServer* server) {
|
||||
return std::shared_ptr<Listener>(new Listener(ioc, endpoint, server));
|
||||
}
|
||||
|
||||
void run() {
|
||||
if (!acceptor_.is_open()) {
|
||||
throw std::runtime_error("Acceptor is not open");
|
||||
}
|
||||
|
||||
doAccept();
|
||||
}
|
||||
|
||||
private:
|
||||
Listener(asio::io_context& ioc, tcp::endpoint endpoint, WebSocketServer* server)
|
||||
: ioc_(ioc)
|
||||
, acceptor_(asio::make_strand(ioc))
|
||||
, server_(server) {
|
||||
|
||||
beast::error_code ec;
|
||||
|
||||
// Open acceptor
|
||||
acceptor_.open(endpoint.protocol(), ec);
|
||||
if (ec) {
|
||||
std::cerr << "Failed to open acceptor: " << ec.message() << std::endl;
|
||||
throw std::runtime_error("Failed to open acceptor: " + ec.message());
|
||||
}
|
||||
|
||||
// Set address reuse
|
||||
acceptor_.set_option(asio::socket_base::reuse_address(true), ec);
|
||||
if (ec) {
|
||||
std::cerr << "Failed to set reuse address: " << ec.message() << std::endl;
|
||||
throw std::runtime_error("Failed to set reuse address: " + ec.message());
|
||||
}
|
||||
|
||||
// Bind to endpoint
|
||||
acceptor_.bind(endpoint, ec);
|
||||
if (ec) {
|
||||
std::cerr << "Failed to bind to endpoint: " << ec.message() << std::endl;
|
||||
throw std::runtime_error("Failed to bind to endpoint: " + ec.message());
|
||||
}
|
||||
|
||||
// Start listening
|
||||
acceptor_.listen(asio::socket_base::max_listen_connections, ec);
|
||||
if (ec) {
|
||||
std::cerr << "Failed to listen: " << ec.message() << std::endl;
|
||||
throw std::runtime_error("Failed to listen: " + ec.message());
|
||||
}
|
||||
}
|
||||
|
||||
void doAccept() {
|
||||
acceptor_.async_accept(
|
||||
asio::make_strand(ioc_),
|
||||
beast::bind_front_handler(
|
||||
&Listener::onAccept,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
void onAccept(beast::error_code ec, tcp::socket socket) {
|
||||
if (ec) {
|
||||
std::cerr << "Accept error: " << ec.message() << std::endl;
|
||||
} else {
|
||||
// Create new Session to handle connection
|
||||
std::make_shared<Session>(std::move(socket), server_)->start();
|
||||
}
|
||||
|
||||
// Continue accepting new connections
|
||||
doAccept();
|
||||
}
|
||||
|
||||
asio::io_context& ioc_;
|
||||
tcp::acceptor acceptor_;
|
||||
WebSocketServer* server_;
|
||||
};
|
||||
|
||||
WebSocketServer::WebSocketServer(unsigned short port, unsigned int threads)
|
||||
: port_(port)
|
||||
, threads_(threads)
|
||||
, impl_(std::make_unique<Impl>()) {}
|
||||
|
||||
WebSocketServer::~WebSocketServer() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void WebSocketServer::start() {
|
||||
if (running_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create listener
|
||||
tcp::endpoint endpoint(tcp::v4(), port_);
|
||||
impl_->listener_ = Listener::create(impl_->ioc_, endpoint, this);
|
||||
|
||||
// Start listener
|
||||
impl_->listener_->run();
|
||||
|
||||
// Start worker threads
|
||||
for (unsigned int i = 0; i < threads_; ++i) {
|
||||
impl_->threads_.emplace_back([this]() {
|
||||
impl_->ioc_.run();
|
||||
});
|
||||
}
|
||||
|
||||
running_ = true;
|
||||
std::cout << "WebSocketServer started on port " << port_ << " with " << threads_ << " threads" << std::endl;
|
||||
}
|
||||
|
||||
void WebSocketServer::stop() {
|
||||
if (!running_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop io_context
|
||||
impl_->ioc_.stop();
|
||||
|
||||
// Wait for all threads to finish
|
||||
for (auto& thread : impl_->threads_) {
|
||||
if (thread.joinable()) {
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
impl_->threads_.clear();
|
||||
running_ = false;
|
||||
std::cout << "WebSocketServer stopped" << std::endl;
|
||||
}
|
||||
|
||||
void WebSocketServer::onMessage(const std::string& path, WebSocketMessageHandler handler) {
|
||||
messageHandlers_[path] = std::move(handler);
|
||||
}
|
||||
|
||||
void WebSocketServer::onConnect(WebSocketConnectHandler handler) {
|
||||
connectHandler_ = std::move(handler);
|
||||
}
|
||||
|
||||
void WebSocketServer::onDisconnect(WebSocketDisconnectHandler handler) {
|
||||
disconnectHandler_ = std::move(handler);
|
||||
}
|
||||
|
||||
bool WebSocketServer::sendToClient(const std::string& clientId, const std::string& message) {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
auto it = clients_.find(clientId);
|
||||
if (it != clients_.end()) {
|
||||
it->second->send(message);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t WebSocketServer::broadcast(const std::string& message) {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
size_t count = 0;
|
||||
for (auto& pair : clients_) {
|
||||
pair.second->send(message);
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void WebSocketServer::disconnectClient(const std::string& clientId) {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
auto it = clients_.find(clientId);
|
||||
if (it != clients_.end()) {
|
||||
// Should close WebSocket connection here
|
||||
// Currently only removing from list
|
||||
clients_.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
size_t WebSocketServer::getClientCount() const {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
return clients_.size();
|
||||
}
|
||||
|
||||
std::string WebSocketServer::generateClientId() const {
|
||||
static std::atomic<uint64_t> counter{0};
|
||||
return "client_" + std::to_string(++counter);
|
||||
}
|
||||
|
||||
void WebSocketServer::addClient(const std::string& clientId, std::shared_ptr<Session> session) {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
clients_[clientId] = session;
|
||||
}
|
||||
|
||||
void WebSocketServer::removeClient(const std::string& clientId) {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
clients_.erase(clientId);
|
||||
}
|
||||
|
||||
std::shared_ptr<WebSocketServer::Session> WebSocketServer::getClient(const std::string& clientId) {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
auto it = clients_.find(clientId);
|
||||
if (it != clients_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace pmc::net
|
||||
@@ -0,0 +1,69 @@
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
# Compiled Object files
|
||||
*.slo
|
||||
*.lo
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Linker files
|
||||
*.ilk
|
||||
|
||||
# Debugger Files
|
||||
*.pdb
|
||||
|
||||
# Compiled Dynamic libraries
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
*.so.*
|
||||
|
||||
|
||||
# Fortran module files
|
||||
*.mod
|
||||
*.smod
|
||||
|
||||
# Compiled Static libraries
|
||||
*.lai
|
||||
*.la
|
||||
*.a
|
||||
*.lib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
|
||||
# Build directories
|
||||
build/
|
||||
Build/
|
||||
build-*/
|
||||
|
||||
# CMake generated files
|
||||
CMakeFiles/
|
||||
CMakeCache.txt
|
||||
cmake_install.cmake
|
||||
#Makefile
|
||||
install_manifest.txt
|
||||
compile_commands.json
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.log
|
||||
*.bak
|
||||
*.swp
|
||||
|
||||
# vcpkg
|
||||
vcpkg_installed/
|
||||
|
||||
# debug information files
|
||||
*.dwo
|
||||
|
||||
# test output & cache
|
||||
Testing/
|
||||
.cache/
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 建昌风物大顺脚本
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,37 @@
|
||||
VERSION := 0.0.1
|
||||
FRAMEWORK_NAME := us
|
||||
LIB_NAME := libus_sqlite3
|
||||
INSTL_TDIR := /lib
|
||||
INCLU_TDIR := /usr/include
|
||||
INCLU_SDIR := inc
|
||||
CPP := g++
|
||||
INCS := -I inc
|
||||
CPPFLAGS := --std=c++20 -O3 -pipe -Wall -Werror
|
||||
LIBS :=
|
||||
BUILD_TDIR := build
|
||||
$(shell mkdir -p $(BUILD_TDIR))
|
||||
|
||||
#--------
|
||||
#all: $(BUILD_TDIR)/$(LIB_NAME).so
|
||||
|
||||
#$(BUILD_TDIR)/$(LIB_NAME).so: $(BUILD_TDIR)/Thread.o
|
||||
# $(CPP) -shared $^ $(LIBS) -o $@
|
||||
|
||||
#$(BUILD_TDIR)/Thread.o: src/Thread.cpp
|
||||
# $(CPP) $(CPPFLAGS) -fPIC -c $^ $(INCS) -o $@
|
||||
|
||||
#clear:
|
||||
# @rm -rvf $(BUILD_TDIR)
|
||||
|
||||
install:
|
||||
@echo "install UniServe_libth..."
|
||||
@mkdir -vp $(INCLU_TDIR)/$(FRAMEWORK_NAME)
|
||||
@cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/Sqlite3.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/
|
||||
# @cp -vf $(BUILD_TDIR)/$(LIB_NAME).so $(INSTL_TDIR)/
|
||||
@echo "done."
|
||||
|
||||
uninstall:
|
||||
@echo "uninstall UniServe_libth..."
|
||||
@rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/Sqlite3.hpp
|
||||
# @rm -vf $(INSTL_TDIR)/$(LIB_NAME).so
|
||||
@echo "done."
|
||||
@@ -0,0 +1,5 @@
|
||||
# UniServe_libsqlite3
|
||||
UniServe Framework sqlite3 Library
|
||||
|
||||
# Dependancy
|
||||
libsqlite3-dev
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
#make clear
|
||||
#make
|
||||
make install
|
||||
@@ -0,0 +1,98 @@
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <sqlite3.h>
|
||||
namespace qing {
|
||||
|
||||
|
||||
class Sqlite3_stmt {
|
||||
public:
|
||||
Sqlite3_stmt(sqlite3 *db, const char *sql) {
|
||||
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr);
|
||||
if (rc != SQLITE_OK) {
|
||||
std::cerr << "Failed to prepare sql stmt: " << sqlite3_errmsg(db) << std::endl;
|
||||
throw std::runtime_error("Failed to prepare sql stmt");
|
||||
}
|
||||
}
|
||||
|
||||
~Sqlite3_stmt() {
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
void bind_int(int idx, int data) {
|
||||
int rc = sqlite3_bind_int(stmt, idx, data);
|
||||
if (rc != SQLITE_OK) {
|
||||
std::cerr << "Failed to bind an integer to sql stmt." << std::endl;
|
||||
throw std::runtime_error("Failed to bind an integer to sql stmt");
|
||||
}
|
||||
}
|
||||
|
||||
void bind_text(int idx, const std::string &data) {
|
||||
int rc = sqlite3_bind_text(stmt, idx, data.c_str(), -1, SQLITE_TRANSIENT);
|
||||
if (rc != SQLITE_OK) {
|
||||
std::cerr << "Failed to bind a text to sql stmt." << std::endl;
|
||||
throw std::runtime_error("Failed to bind a text to sql stmt");
|
||||
}
|
||||
}
|
||||
|
||||
bool step() {
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc != SQLITE_ROW && rc != SQLITE_DONE) {
|
||||
throw std::runtime_error("Failed to query");
|
||||
}
|
||||
return rc == SQLITE_ROW && rc != SQLITE_DONE;
|
||||
}
|
||||
|
||||
/*
|
||||
* int len = sqlite3_column_bytes(stmt, 1); // 获取字节长度(不含结束符)
|
||||
* std::string name(reinterpret_cast<const char*>(rawName), len);
|
||||
*/
|
||||
int read_int(int idx) {
|
||||
return sqlite3_column_int(stmt, idx);
|
||||
}
|
||||
|
||||
|
||||
const unsigned char *read_text(int idx) {
|
||||
return sqlite3_column_text(stmt, idx);
|
||||
}
|
||||
|
||||
private:
|
||||
sqlite3_stmt* stmt;
|
||||
};
|
||||
|
||||
class Sqlite3 {
|
||||
|
||||
public:
|
||||
Sqlite3(const std::string& filename) {
|
||||
int rc = sqlite3_open(filename.c_str(), &db);
|
||||
if (rc != SQLITE_OK) {
|
||||
std::cerr << "Failed to open database: " << sqlite3_errmsg(db) << std::endl;
|
||||
sqlite3_close(db);
|
||||
throw std::runtime_error("Failed to open sqlite3 database.");
|
||||
}
|
||||
}
|
||||
|
||||
~Sqlite3() {
|
||||
sqlite3_close(db);
|
||||
}
|
||||
|
||||
bool exec(const std::string& sql) {
|
||||
char *errMsg;
|
||||
int rc = sqlite3_exec(db, sql.c_str(), 0, 0, &errMsg);
|
||||
if (rc != SQLITE_OK) {
|
||||
std::cerr << "Failed to execute at sqlite3: " << errMsg << std::endl;
|
||||
sqlite3_free(errMsg);
|
||||
//sqlite3_close(db);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Sqlite3_stmt stmt(std::string& sql) {
|
||||
return Sqlite3_stmt(db, sql.c_str());
|
||||
}
|
||||
|
||||
private:
|
||||
sqlite3* db;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
# Compiled Object files
|
||||
*.slo
|
||||
*.lo
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Linker files
|
||||
*.ilk
|
||||
|
||||
# Debugger Files
|
||||
*.pdb
|
||||
|
||||
# Compiled Dynamic libraries
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
*.so.*
|
||||
|
||||
|
||||
# Fortran module files
|
||||
*.mod
|
||||
*.smod
|
||||
|
||||
# Compiled Static libraries
|
||||
*.lai
|
||||
*.la
|
||||
*.a
|
||||
*.lib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
|
||||
# Build directories
|
||||
build/
|
||||
Build/
|
||||
build-*/
|
||||
|
||||
# CMake generated files
|
||||
CMakeFiles/
|
||||
CMakeCache.txt
|
||||
cmake_install.cmake
|
||||
#Makefile
|
||||
install_manifest.txt
|
||||
compile_commands.json
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.log
|
||||
*.bak
|
||||
*.swp
|
||||
|
||||
# vcpkg
|
||||
vcpkg_installed/
|
||||
|
||||
# debug information files
|
||||
*.dwo
|
||||
|
||||
# test output & cache
|
||||
Testing/
|
||||
.cache/
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 dashun
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,43 @@
|
||||
VERSION := 0.0.1
|
||||
FRAMEWORK_NAME := us
|
||||
LIB_NAME := libus_th
|
||||
INSTL_TDIR := /lib
|
||||
INCLU_TDIR := /usr/include
|
||||
INCLU_SDIR := inc
|
||||
CPP := g++
|
||||
INCS := -I inc
|
||||
CPPFLAGS := --std=c++20 -O3 -pipe -Wall -Werror
|
||||
LIBS :=
|
||||
BUILD_TDIR := build
|
||||
$(shell mkdir -p $(BUILD_TDIR))
|
||||
|
||||
#--------
|
||||
all: $(BUILD_TDIR)/$(LIB_NAME).so
|
||||
|
||||
$(BUILD_TDIR)/$(LIB_NAME).so: $(BUILD_TDIR)/Thread.o
|
||||
$(CPP) -shared $^ $(LIBS) -o $@
|
||||
|
||||
$(BUILD_TDIR)/Thread.o: src/Thread.cpp
|
||||
$(CPP) $(CPPFLAGS) -fPIC -c $^ $(INCS) -o $@
|
||||
|
||||
clear:
|
||||
@rm -rvf $(BUILD_TDIR)
|
||||
|
||||
install:
|
||||
@echo "install UniServe_libth..."
|
||||
@mkdir -vp $(INCLU_TDIR)/$(FRAMEWORK_NAME)
|
||||
@cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/Cv_wait.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/
|
||||
@cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/Fsm.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/
|
||||
@cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/ITask.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/
|
||||
@cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/Thread.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/
|
||||
@cp -vf $(BUILD_TDIR)/$(LIB_NAME).so $(INSTL_TDIR)/
|
||||
@echo "done."
|
||||
|
||||
uninstall:
|
||||
@echo "uninstall UniServe_libth..."
|
||||
@rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/Cv_wait.hpp
|
||||
@rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/Fsm.hpp
|
||||
@rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/ITask.hpp
|
||||
@rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/Thread.hpp
|
||||
@rm -vf $(INSTL_TDIR)/$(LIB_NAME).so
|
||||
@echo "done."
|
||||
@@ -0,0 +1,14 @@
|
||||
# UniServe_libth
|
||||
UniServe Framework Thread Library.
|
||||
|
||||
# Dependancy Chain
|
||||
It should be C++17.
|
||||
|
||||
# Install Method
|
||||
Use UniServe Generate Installation Script.
|
||||
|
||||
``` bash
|
||||
bash _set.sh
|
||||
```
|
||||
|
||||
# ENJOY!
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
make clear
|
||||
make
|
||||
make install
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 利用条件变量进行等待
|
||||
*
|
||||
* FIXME: deepseek 认为有一个设计缺陷:它只能用于单次等待-唤醒的场景。
|
||||
* FIXME: 可能导致永久堵塞,应使用带超时的等待
|
||||
* FIXME: 使用标准库的 future/promise
|
||||
*/
|
||||
|
||||
#include <thread>
|
||||
#include <atomic> /* 也许将字段flag创建为原子变量 */
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
namespace qing {
|
||||
class Cv_wait { /* 用于等待的条件变量类,对同一个对象进行操作能够阻塞当前线程,或者唤醒被阻塞的线程 */
|
||||
private:
|
||||
std::mutex mtx{}; /* 用于控制条件变量的锁 */
|
||||
std::condition_variable cv{};
|
||||
bool flag = false; /* 用于解锁条件变量的标志 */
|
||||
public:
|
||||
|
||||
inline void Wait() /* 线程进入等待 */
|
||||
{
|
||||
auto lock = std::unique_lock<std::mutex>(mtx);
|
||||
flag = false;
|
||||
cv.wait(lock, [this] { return flag; });
|
||||
}
|
||||
|
||||
inline void WakeCv() /* 唤醒线程 */
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lcok(mtx);
|
||||
flag = true;
|
||||
}
|
||||
cv.notify_all();
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 有限状态机(Finite State Machine,FSM)
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <mutex> /*上锁 使得有限状态机线程安全*/
|
||||
#include <condition_variable> /*条件变量,使得子类能够等待状态的设置*/
|
||||
namespace qing {
|
||||
/*
|
||||
* FiniteStateMachine有限状态机
|
||||
*
|
||||
* 每个数字代表一个状态 使用static_cast<>()之类的函数去赋值
|
||||
* FIXME: 之前用过一种使用回调函数处理的方式
|
||||
*/
|
||||
class Fsm {
|
||||
public:
|
||||
|
||||
/* 状态枚举类 */
|
||||
enum class Stat: int {
|
||||
SHUT = -1, /* 关闭 */
|
||||
STOP = 0, /* 静止 */
|
||||
START = 1, /* 启动 */
|
||||
RUNNING = 2 /* 运行 */
|
||||
/* ... */
|
||||
};/*Stat*/
|
||||
|
||||
|
||||
/* 以STOP为初始状态 */
|
||||
Fsm() { set(Stat::STOP); }
|
||||
|
||||
enum Stat check() { /* 检查当前的状态 */
|
||||
std::lock_guard<std::mutex> lk(mtx);
|
||||
return stat;
|
||||
} /*check*/
|
||||
|
||||
|
||||
/*
|
||||
* 在不上锁的情况下设置一个状态
|
||||
* 这里不能使用锁,因为我们还要在临界区里唤醒条件变量
|
||||
*/
|
||||
void set_without_lock(Stat stat) { this->stat = stat; }
|
||||
|
||||
/* 带锁的状态设置 但是该方法不唤醒条件变量 */
|
||||
void set_without_notify(Stat stat) {
|
||||
std::lock_guard<std::mutex> lk(mtx);
|
||||
this->stat = stat;
|
||||
}
|
||||
|
||||
/* 带锁和条件唤醒的状态设置 */
|
||||
void set(Stat stat) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mtx);
|
||||
set_without_lock(stat);
|
||||
ready = true;
|
||||
}
|
||||
cv.notify_all();
|
||||
}
|
||||
|
||||
//protected:
|
||||
|
||||
void suspend() { /* 阻塞线程,等待事件唤醒 */
|
||||
std::unique_lock<std::mutex> lk(mtx);
|
||||
ready = false;
|
||||
cv.wait(lk, [this] { return ready; });
|
||||
}/*suspend */
|
||||
|
||||
private:
|
||||
std::mutex mtx; /* 线程安全 */
|
||||
bool ready = false; /* 用于条件变量的阻塞唤醒 */
|
||||
std::condition_variable cv; /* 条件变量 阻塞/唤醒线程 */
|
||||
enum Stat stat; /* 状态机的状态变量 */
|
||||
|
||||
};//Fsm
|
||||
|
||||
}//qing
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
namespace qing{
|
||||
class ITask { /* 单个任务通用接口 */
|
||||
public:
|
||||
virtual void start() = 0; /* 任务开始 */
|
||||
virtual void stop() = 0; /* 任务结束 */
|
||||
virtual bool isRunning() = 0; /* 是否在跑 */
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/* 线程
|
||||
*
|
||||
*
|
||||
* 包含一个简易的多线程控制机制,需要共享的变量和函数声明为成员。
|
||||
*
|
||||
* 线程主函数包括了流程控制,要使用需要实现其中的各个事件方法
|
||||
*
|
||||
* 20250621: 从派生改为持有
|
||||
*
|
||||
* NOTE: 在对象构造完之后应该激活
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <thread>
|
||||
#include <memory> /* 内存管理 */
|
||||
#include <chrono> /* 用于线程休息 */
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <functional>
|
||||
#include "us/Fsm.hpp" /* 有限状态机 */
|
||||
namespace qing {
|
||||
class Thread;
|
||||
//using f_t = void(*)(Thread&);
|
||||
using f_t = std::function<void(Thread&)>;
|
||||
class Thread {
|
||||
public:
|
||||
|
||||
/* 作为可变的函数,持有回调可能比继承更好
|
||||
*
|
||||
* FIXME: 可以改为链式传递参数 */
|
||||
//Thread(f_t stop_callback, f_t wake_callback, f_t loop_callback, f_t clear_callback);
|
||||
|
||||
/* 使用默认初始化方法,然后用几个函数分别进行回调初始化 */
|
||||
Thread() = default;
|
||||
|
||||
/*
|
||||
* 删除复制构造函数。
|
||||
*
|
||||
* 因为持有线程类,不要轻举妄动。
|
||||
*/
|
||||
Thread(const Thread&) = delete;
|
||||
|
||||
|
||||
/*
|
||||
* 虚析构函数
|
||||
*
|
||||
* FIXME: 具体有什么用,忘了
|
||||
*/
|
||||
virtual ~Thread() {}
|
||||
|
||||
|
||||
/* 设置停止回调函数 */
|
||||
void set_stop(f_t callback);
|
||||
|
||||
/* 设置唤醒回调函数 */
|
||||
void set_wake(f_t callback);
|
||||
|
||||
/* 设置循环体回调函数 */
|
||||
void set_loop(f_t callback);
|
||||
|
||||
/* 设置清理回调函数 */
|
||||
void set_clear(f_t callback);
|
||||
|
||||
/*-----------------------------------*/
|
||||
/*-----------------------------------*/
|
||||
/*
|
||||
* 检查状态的方法
|
||||
*
|
||||
* NOTE: 做成虚函数,万一子类有特别要求呢
|
||||
*
|
||||
* FIXME: 将检查器和变异器做成一个简单的回调函数
|
||||
*/
|
||||
Fsm::Stat check();
|
||||
|
||||
/*-----------------------------------*/
|
||||
/*-----------------------------------*/
|
||||
/* 让线程能够阻塞直到新的状态被赋予的方法 */
|
||||
void suspend();
|
||||
|
||||
/*-----------------------------------*/
|
||||
/*-----------------------------------*/
|
||||
/* 状态改变的方法 - 变异器 */
|
||||
|
||||
/* 进入唤醒状态
|
||||
*
|
||||
* 唤醒函数不可以进行该类中的锁操作,否则会造成死锁
|
||||
*/
|
||||
void wake();
|
||||
|
||||
/* 进入静止状态
|
||||
*
|
||||
* 上锁、唤醒阻塞的线程
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/* 进入关闭状态 */
|
||||
void shut();
|
||||
|
||||
/* 进入运行状态 */
|
||||
void run();
|
||||
|
||||
/*-----------------------------------*/
|
||||
/*-----------------------------------*/
|
||||
|
||||
/*
|
||||
* 激活线程,申请线程对象
|
||||
*
|
||||
* 该函数不能放在构造函数内,因为需要确保对象已经被构造了
|
||||
*/
|
||||
void Activate();
|
||||
|
||||
/*
|
||||
* 尝试唤醒线程并等待它开始 据说该函数有问题可能导致异常
|
||||
*/
|
||||
void WaitStart();
|
||||
|
||||
/* 等待线程关闭 */
|
||||
void WaitClose();
|
||||
|
||||
/*-----------------------------------*/
|
||||
/* 线程主函数 */
|
||||
void main();
|
||||
|
||||
private:
|
||||
|
||||
qing::Fsm fsm{}; /* 持有状态机类型 */
|
||||
|
||||
std::unique_ptr<std::thread> th; /* 持有线程类型 */
|
||||
|
||||
std::mutex mtx_th; /* 持有锁类型 */
|
||||
|
||||
/*
|
||||
* 这里持有一个条件变量,用于阻塞以等待线程的关闭
|
||||
*
|
||||
* 注意状态机中的cv是另一个变量
|
||||
*/
|
||||
std::condition_variable cv;
|
||||
|
||||
/* 辅助条件变量的逻辑型 */
|
||||
bool ready;
|
||||
|
||||
/* 事件回调 */
|
||||
std::unique_ptr<f_t> StopEvent;
|
||||
std::unique_ptr<f_t> WakeEvent;
|
||||
std::unique_ptr<f_t> LoopEvent;
|
||||
std::unique_ptr<f_t> ClearEvent;
|
||||
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
#include "us/Thread.hpp"
|
||||
namespace qing{
|
||||
|
||||
//Thread::Thread(f_t stop_callback, f_t wake_callback, f_t loop_callback, f_t clear_callback)
|
||||
//{
|
||||
// StopEvent = std::make_unique<f_t>(stop_callback);
|
||||
// WakeEvent = std::make_unique<f_t>(wake_callback);
|
||||
// LoopEvent = std::make_unique<f_t>(loop_callback);
|
||||
// ClearEvent = std::make_unique<f_t>(clear_callback);
|
||||
//};
|
||||
|
||||
void Thread::set_stop(f_t callback) {
|
||||
StopEvent = std::make_unique<f_t> (callback);
|
||||
}
|
||||
|
||||
void Thread::set_wake(f_t callback) {
|
||||
WakeEvent = std::make_unique<f_t> (callback);
|
||||
}
|
||||
|
||||
void Thread::set_loop(f_t callback) {
|
||||
LoopEvent = std::make_unique<f_t> (callback);
|
||||
}
|
||||
|
||||
void Thread::set_clear(f_t callback) {
|
||||
ClearEvent = std::make_unique<f_t> (callback);
|
||||
}
|
||||
|
||||
|
||||
Fsm::Stat Thread::check() {
|
||||
return fsm.check();
|
||||
}
|
||||
|
||||
void Thread::suspend() {
|
||||
fsm.suspend();
|
||||
}
|
||||
|
||||
/* 唤醒函数不可以进行该类中的锁操作,否则会造成死锁 */
|
||||
void Thread::wake() {
|
||||
//{
|
||||
//std::lock_guard<std::mutex> lk(mtx_th);
|
||||
fsm.set(Fsm::Stat::START);
|
||||
//ready = true;
|
||||
//}
|
||||
//cv.notify_all();
|
||||
} /* wake */
|
||||
|
||||
/* 上锁、唤醒阻塞的线程 */
|
||||
void Thread::stop() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mtx_th);
|
||||
fsm.set(Fsm::Stat::STOP);
|
||||
ready = true;
|
||||
}
|
||||
/* 可能会出现多个线程在等待的情况 */
|
||||
cv.notify_all();
|
||||
}/* stop */
|
||||
|
||||
void Thread::shut() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mtx_th);
|
||||
fsm.set(Fsm::Stat::SHUT);
|
||||
ready = true;
|
||||
}
|
||||
cv.notify_all();
|
||||
} /*shut*/
|
||||
|
||||
void Thread::run() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mtx_th);
|
||||
fsm.set(Fsm::Stat::RUNNING);
|
||||
ready = true;
|
||||
}
|
||||
cv.notify_all();
|
||||
}/*run*/
|
||||
|
||||
void Thread::Activate() {
|
||||
|
||||
/* 防止重复初始化
|
||||
* FIXME: 抛出异常?感觉也不太合适 */
|
||||
if (th) return;
|
||||
|
||||
/* 申请线程资源 */
|
||||
th = std::make_unique<std::thread>(&Thread::main, this);
|
||||
|
||||
}
|
||||
|
||||
void Thread::WaitStart() {
|
||||
std::unique_lock<std::mutex> lk(mtx_th); /* 不进入临界区可能会造成死等,在唤醒过程非常短暂的情况下 */
|
||||
wake(); /* 这就是为什么wake()不可以获取临界区,否则会造成死锁 */
|
||||
ready = false; /* 这个临界区结束后,WakeEvent()中的run()会唤醒线程的堵塞 */
|
||||
cv.wait(lk, [this] { return ready; });
|
||||
}
|
||||
|
||||
void Thread::WaitClose() {
|
||||
shut();
|
||||
if (th->joinable())
|
||||
th->join();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 线程主函数 */
|
||||
void Thread::main() {
|
||||
|
||||
/* 回字形循环 */
|
||||
while (check() != Fsm::Stat::SHUT) {
|
||||
|
||||
try { /* 异常只会使线程静止 */
|
||||
|
||||
|
||||
//------------------------------------------------
|
||||
while (check() == Fsm::Stat::STOP) {
|
||||
(*StopEvent)(*this); /* 静止状态 */
|
||||
}
|
||||
|
||||
if (check() == Fsm::Stat::START) {
|
||||
//(*ClearEvent)(*this); /* 在开始之前做一次清理 */
|
||||
(*WakeEvent)(*this); /* 开始事件 */
|
||||
}
|
||||
|
||||
while (check() == Fsm::Stat::RUNNING) {
|
||||
(*LoopEvent)(*this); /* 从设置阶段进入运行态 */
|
||||
}
|
||||
|
||||
/* 清理事件 (记得检查成员变量有没有被设置) */
|
||||
(*ClearEvent)(*this);
|
||||
|
||||
//------------------------------------------------
|
||||
|
||||
}
|
||||
|
||||
catch (std::exception& exp) {
|
||||
std::cout << "Thread throw a exception: " << exp.what() << std::endl;
|
||||
stop(); /* 因为这里的停止,线程在循环内抛出异常进程不会死亡,所以需要特别注意内存泄漏 */
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
# IDE
|
||||
.vscode/
|
||||
fsys.dist
|
||||
fsys.build
|
||||
*.bin
|
||||
*.out
|
||||
|
||||
# PRJ
|
||||
#config.ini
|
||||
*.pem
|
||||
aes_key.txt
|
||||
public/users/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 卿松
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
source ./venv/bin/activate
|
||||
# 直接运行Python应用,HTTPS/HTTP配置在app.py中处理
|
||||
exec fsys.bin --config config.ini 1>/dev/null
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
python3 -m venv venv
|
||||
#sudo chmod +x .venv/bin/activate
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
|
||||
nuitka --onefile fsys.py
|
||||
@@ -0,0 +1,14 @@
|
||||
[server]
|
||||
host = 0.0.0.0
|
||||
port = 9200
|
||||
|
||||
[directory]
|
||||
root = ./public
|
||||
|
||||
[ssl]
|
||||
enabled = false
|
||||
cert_file = cert.pem
|
||||
key_file = key.pem
|
||||
|
||||
[cors]
|
||||
allowed_origins = *
|
||||
+591
@@ -0,0 +1,591 @@
|
||||
# file_server.py - 极简版文件托管服务器
|
||||
from flask import Flask, send_from_directory, jsonify, request, Response, send_file
|
||||
from flask_cors import CORS
|
||||
import os
|
||||
import argparse
|
||||
import datetime
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
import configparser
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
file_handler = RotatingFileHandler('app.log', maxBytes=10485760, backupCount=10)
|
||||
file_handler.setFormatter(logging.Formatter(
|
||||
'%(asctime)s %(levelname)s: %(message)s'
|
||||
))
|
||||
file_handler.setLevel(logging.INFO)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
parser = argparse.ArgumentParser(description='File Server')
|
||||
parser.add_argument('--config', type=str, default='config.ini', help='Configuration file path')
|
||||
args = parser.parse_args()
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
# ==================== 配置文件加载 ====================
|
||||
CONFIG_FILE = args.config
|
||||
|
||||
file_config = {
|
||||
'server': {'host': '0.0.0.0', 'port': '5000'},
|
||||
'directory': {'root': './public'},
|
||||
'ssl': {'enabled': 'false', 'cert_file': 'cert.pem', 'key_file': 'key.pem'},
|
||||
}
|
||||
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
config_parser = configparser.ConfigParser()
|
||||
config_parser.read(CONFIG_FILE, encoding='utf-8')
|
||||
for section in ['server', 'directory', 'ssl']:
|
||||
if section in config_parser:
|
||||
for key, value in config_parser[section].items():
|
||||
if key in file_config[section]:
|
||||
file_config[section][key] = value
|
||||
|
||||
HTML_ROOT_DIR = file_config['directory']['root']
|
||||
FILESYSTEM_ENABLED = bool(HTML_ROOT_DIR and HTML_ROOT_DIR.strip())
|
||||
|
||||
if FILESYSTEM_ENABLED:
|
||||
try:
|
||||
Path(HTML_ROOT_DIR).mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"Created directory: {HTML_ROOT_DIR}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create directory: {str(e)}")
|
||||
FILESYSTEM_ENABLED = False
|
||||
|
||||
SSL_ENABLED = file_config['ssl'].get('enabled', 'false').lower() == 'true'
|
||||
SSL_CERT_FILE = file_config['ssl'].get('cert_file', 'cert.pem')
|
||||
SSL_KEY_FILE = file_config['ssl'].get('key_file', 'key.pem')
|
||||
|
||||
# ==================== 视频文件支持 ====================
|
||||
VIDEO_MIME_TYPES = {
|
||||
'.mp4': 'video/mp4', '.webm': 'video/webm', '.ogg': 'video/ogg',
|
||||
'.avi': 'video/x-msvideo', '.mov': 'video/quicktime', '.mkv': 'video/x-matroska'
|
||||
}
|
||||
|
||||
AUDIO_MIME_TYPES = {
|
||||
'.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.ogg': 'audio/ogg',
|
||||
'.flac': 'audio/flac', '.aac': 'audio/aac', '.m4a': 'audio/mp4'
|
||||
}
|
||||
|
||||
def is_video_file(filename):
|
||||
return os.path.splitext(filename)[1].lower() in VIDEO_MIME_TYPES
|
||||
|
||||
def is_audio_file(filename):
|
||||
return os.path.splitext(filename)[1].lower() in AUDIO_MIME_TYPES
|
||||
|
||||
def get_media_mime_type(filename):
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
return VIDEO_MIME_TYPES.get(ext) or AUDIO_MIME_TYPES.get(ext) or 'application/octet-stream'
|
||||
|
||||
def safe_path_join(base_dir, user_path):
|
||||
if not user_path or user_path.strip() == '' or user_path.strip() == '/':
|
||||
return base_dir
|
||||
if user_path.startswith('/'):
|
||||
user_path = user_path[1:]
|
||||
full_path = os.path.normpath(os.path.join(base_dir, user_path))
|
||||
base_dir_real = os.path.realpath(base_dir)
|
||||
full_path_real = os.path.realpath(full_path)
|
||||
if not full_path_real.startswith(base_dir_real):
|
||||
return None
|
||||
return full_path_real
|
||||
|
||||
def format_file_size(size):
|
||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||
if size < 1024.0:
|
||||
return f"{size:.1f}{unit}"
|
||||
size /= 1024.0
|
||||
return f"{size:.1f}TB"
|
||||
|
||||
# ==================== API 接口 ====================
|
||||
@app.route('/api/filelist', methods=['GET'])
|
||||
def get_file_list():
|
||||
try:
|
||||
if not FILESYSTEM_ENABLED:
|
||||
return jsonify({'error': '文件系统未启用'}), 403
|
||||
|
||||
requested_path = request.args.get('path', '')
|
||||
full_path = safe_path_join(HTML_ROOT_DIR, requested_path)
|
||||
|
||||
if full_path is None:
|
||||
return jsonify({'error': '无效的路径'}), 403
|
||||
|
||||
if not os.path.exists(full_path) or not os.path.isdir(full_path):
|
||||
return jsonify({'error': '目录不存在'}), 404
|
||||
|
||||
items = []
|
||||
for item in os.listdir(full_path):
|
||||
if item == '__pycache__':
|
||||
continue
|
||||
|
||||
item_path = os.path.join(full_path, item)
|
||||
item_stat = os.stat(item_path)
|
||||
is_dir = os.path.isdir(item_path)
|
||||
|
||||
item_rel_path = os.path.join(requested_path, item) if requested_path else item
|
||||
|
||||
file_type = 'dir' if is_dir else ('video' if is_video_file(item) else ('audio' if is_audio_file(item) else 'file'))
|
||||
|
||||
items.append({
|
||||
'name': item,
|
||||
'type': file_type,
|
||||
'path': item_rel_path,
|
||||
'size': item_stat.st_size if not is_dir else 0,
|
||||
'size_formatted': format_file_size(item_stat.st_size) if not is_dir else '',
|
||||
'modified': datetime.datetime.fromtimestamp(item_stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S')
|
||||
})
|
||||
|
||||
items.sort(key=lambda x: (x['type'] != 'dir', x['name'].lower()))
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'path': requested_path or '/',
|
||||
'parent_path': os.path.dirname(requested_path) if requested_path and os.path.dirname(requested_path) != '/' else '',
|
||||
'items': items
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Get file list error: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/video/<path:file_path>', methods=['GET'])
|
||||
def stream_video(file_path):
|
||||
try:
|
||||
if not FILESYSTEM_ENABLED:
|
||||
return jsonify({'error': '文件系统未启用'}), 403
|
||||
|
||||
full_path = safe_path_join(HTML_ROOT_DIR, file_path)
|
||||
if full_path is None or not os.path.exists(full_path) or not os.path.isfile(full_path):
|
||||
return jsonify({'error': '文件不存在'}), 404
|
||||
|
||||
if not is_video_file(full_path) and not is_audio_file(full_path):
|
||||
return jsonify({'error': '不支持的文件类型'}), 400
|
||||
|
||||
file_size = os.path.getsize(full_path)
|
||||
mime_type = get_media_mime_type(full_path)
|
||||
|
||||
range_header = request.headers.get('Range', None)
|
||||
|
||||
if range_header:
|
||||
byte_range = range_header.replace('bytes=', '').split('-')
|
||||
start = int(byte_range[0]) if byte_range[0] else 0
|
||||
end = int(byte_range[1]) if len(byte_range) > 1 and byte_range[1] else file_size - 1
|
||||
|
||||
if start >= file_size:
|
||||
return jsonify({'error': '请求范围超出文件大小'}), 416
|
||||
|
||||
length = end - start + 1
|
||||
|
||||
with open(full_path, 'rb') as f:
|
||||
f.seek(start)
|
||||
data = f.read(length)
|
||||
|
||||
response = Response(data, 206, mimetype=mime_type, direct_passthrough=True)
|
||||
response.headers.add('Content-Range', f'bytes {start}-{end}/{file_size}')
|
||||
response.headers.add('Accept-Ranges', 'bytes')
|
||||
response.headers.add('Content-Length', str(length))
|
||||
response.headers.add('Cache-Control', 'no-cache')
|
||||
return response
|
||||
else:
|
||||
return send_file(full_path, mimetype=mime_type, as_attachment=False)
|
||||
except Exception as e:
|
||||
logger.error(f"Video streaming error: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
# ==================== 极简版HTML页面(使用字符串拼接,避免花括号问题) ====================
|
||||
def get_html_template(path, breadcrumb_html):
|
||||
"""生成HTML页面"""
|
||||
return f'''<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes">
|
||||
<title>文件浏览</title>
|
||||
<style>
|
||||
* {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
}}
|
||||
.header {{
|
||||
background: #4A90D9;
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}}
|
||||
.header h1 {{ font-size: 18px; font-weight: 600; }}
|
||||
.header a {{
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 4px 10px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}}
|
||||
.header a:active {{ background: rgba(255,255,255,0.4); }}
|
||||
.breadcrumb {{
|
||||
padding: 8px 16px;
|
||||
background: white;
|
||||
font-size: 13px;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}}
|
||||
.breadcrumb a {{ color: #4A90D9; text-decoration: none; padding: 2px 6px; }}
|
||||
.breadcrumb .sep {{ color: #999; margin: 0 4px; }}
|
||||
.breadcrumb .current {{ color: #333; font-weight: 500; }}
|
||||
.search-box {{
|
||||
padding: 8px 16px;
|
||||
background: white;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}}
|
||||
.search-box input {{
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
}}
|
||||
.search-box input:focus {{ border-color: #4A90D9; }}
|
||||
.stats {{
|
||||
padding: 6px 16px;
|
||||
background: #fafafa;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}}
|
||||
.container {{
|
||||
padding: 8px 12px 60px 12px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}}
|
||||
.item {{
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}}
|
||||
.item:active {{ background: #f0f0f0; }}
|
||||
.item .icon {{ font-size: 24px; flex-shrink: 0; width: 36px; text-align: center; }}
|
||||
.item .info {{ flex: 1; min-width: 0; }}
|
||||
.item .name {{ font-size: 15px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }}
|
||||
.item .meta {{ font-size: 12px; color: #999; display: flex; gap: 10px; flex-wrap: wrap; }}
|
||||
.item .actions {{ display: flex; gap: 4px; flex-shrink: 0; }}
|
||||
.item .actions button {{
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}}
|
||||
.item .actions button:active {{ transform: scale(0.95); }}
|
||||
.btn-play {{ background: #4CAF50; color: white; }}
|
||||
.btn-download {{ background: #4A90D9; color: white; }}
|
||||
.loading {{ text-align: center; padding: 40px; color: #999; }}
|
||||
.loading .spinner {{ display: inline-block; animation: spin 1s linear infinite; font-size: 28px; }}
|
||||
@keyframes spin {{ 100% {{ transform: rotate(360deg); }} }}
|
||||
.empty {{ text-align: center; padding: 60px 20px; color: #999; }}
|
||||
.empty .icon {{ font-size: 48px; display: block; margin-bottom: 12px; }}
|
||||
.error {{ text-align: center; padding: 40px 20px; color: #e74c3c; }}
|
||||
@media (max-width: 480px) {{
|
||||
.item {{ padding: 8px 10px; }}
|
||||
.item .icon {{ font-size: 20px; width: 30px; }}
|
||||
.item .name {{ font-size: 13px; }}
|
||||
.item .meta {{ font-size: 11px; gap: 6px; }}
|
||||
.item .actions button {{ padding: 3px 8px; font-size: 11px; }}
|
||||
.header h1 {{ font-size: 16px; }}
|
||||
}}
|
||||
@media (prefers-color-scheme: dark) {{
|
||||
body {{ background: #1a1a2e; color: #e0e0e0; }}
|
||||
.header {{ background: #2d2d5e; }}
|
||||
.breadcrumb {{ background: #16213e; border-color: #2a2a4a; }}
|
||||
.breadcrumb .current {{ color: #e0e0e0; }}
|
||||
.stats {{ background: #16213e; color: #aaa; border-color: #2a2a4a; }}
|
||||
.search-box {{ background: #16213e; border-color: #2a2a4a; }}
|
||||
.search-box input {{ background: #2a2a4a; border-color: #3a3a5a; color: #e0e0e0; }}
|
||||
.item {{ background: #16213e; border-color: #2a2a4a; }}
|
||||
.item:active {{ background: #2a2a4a; }}
|
||||
.item .meta {{ color: #888; }}
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>📁 文件浏览</h1>
|
||||
<div>
|
||||
<a href="/">🏠</a>
|
||||
<a href="#" id="btnRefresh" style="margin-left:6px;">🔄</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a href="/">根目录</a>
|
||||
{breadcrumb_html}
|
||||
</div>
|
||||
|
||||
<div class="search-box">
|
||||
<input type="text" id="searchInput" placeholder="搜索文件..." autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<span>📊 共 <span id="totalItems">0</span> 项</span>
|
||||
<span>📁 <span id="totalDirs">0</span></span>
|
||||
<span>📄 <span id="totalFiles">0</span></span>
|
||||
<span>🎬 <span id="totalVideos">0</span></span>
|
||||
<span>🎵 <span id="totalAudios">0</span></span>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="loading" id="loading">
|
||||
<span class="spinner">⏳</span>
|
||||
<div>加载中...</div>
|
||||
</div>
|
||||
<div id="fileList"></div>
|
||||
<div class="empty" id="empty" style="display:none;">
|
||||
<span class="icon">📭</span>
|
||||
<div>此目录为空</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const currentPath = "{path}";
|
||||
let items = [];
|
||||
let searchQuery = "";
|
||||
|
||||
const fileList = document.getElementById("fileList");
|
||||
const loading = document.getElementById("loading");
|
||||
const empty = document.getElementById("empty");
|
||||
const searchInput = document.getElementById("searchInput");
|
||||
const totalItems = document.getElementById("totalItems");
|
||||
const totalDirs = document.getElementById("totalDirs");
|
||||
const totalFiles = document.getElementById("totalFiles");
|
||||
const totalVideos = document.getElementById("totalVideos");
|
||||
const totalAudios = document.getElementById("totalAudios");
|
||||
const btnRefresh = document.getElementById("btnRefresh");
|
||||
|
||||
function formatSize(bytes) {{
|
||||
if (bytes === 0) return "0B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return (bytes / Math.pow(k, i)).toFixed(1) + sizes[i];
|
||||
}}
|
||||
|
||||
function getIcon(item) {{
|
||||
if (item.type === "dir") return "📁";
|
||||
if (item.type === "video") return "🎬";
|
||||
if (item.type === "audio") return "🎵";
|
||||
const ext = item.name.split(".").pop().toLowerCase();
|
||||
const icons = {{
|
||||
"pdf": "📄", "doc": "📝", "docx": "📝", "xls": "📊", "xlsx": "📊",
|
||||
"zip": "📦", "rar": "📦", "7z": "📦",
|
||||
"jpg": "🖼️", "jpeg": "🖼️", "png": "🖼️", "gif": "🖼️",
|
||||
"txt": "📃", "json": "📋", "html": "🌐", "css": "🎨", "js": "⚡"
|
||||
}};
|
||||
return icons[ext] || "📄";
|
||||
}}
|
||||
|
||||
function navigate(path, type) {{
|
||||
if (type === "dir") window.location.href = "/" + path;
|
||||
else if (type === "video" || type === "audio") window.location.href = "/video/" + path;
|
||||
else window.location.href = "/" + path;
|
||||
}}
|
||||
|
||||
function playMedia(path) {{ window.location.href = "/video/" + path; }}
|
||||
function downloadFile(path) {{ window.location.href = "/" + path + "?download=true"; }}
|
||||
|
||||
function render() {{
|
||||
const filtered = items.filter(item => {{
|
||||
if (!searchQuery) return true;
|
||||
return item.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
}});
|
||||
|
||||
totalItems.textContent = filtered.length;
|
||||
totalDirs.textContent = filtered.filter(i => i.type === "dir").length;
|
||||
totalFiles.textContent = filtered.filter(i => i.type === "file").length;
|
||||
totalVideos.textContent = filtered.filter(i => i.type === "video").length;
|
||||
totalAudios.textContent = filtered.filter(i => i.type === "audio").length;
|
||||
|
||||
if (filtered.length === 0) {{
|
||||
fileList.innerHTML = "";
|
||||
empty.style.display = "block";
|
||||
loading.style.display = "none";
|
||||
return;
|
||||
}}
|
||||
empty.style.display = "none";
|
||||
loading.style.display = "none";
|
||||
|
||||
fileList.innerHTML = filtered.map(item => `
|
||||
<div class="item">
|
||||
<span class="icon">${{getIcon(item)}}</span>
|
||||
<div class="info" onclick="navigate('${{item.path}}', '${{item.type}}')">
|
||||
<div class="name">${{item.name}}</div>
|
||||
<div class="meta">
|
||||
${{item.type !== "dir" ? `<span>${{formatSize(item.size)}}</span>` : '<span>📁 文件夹</span>'}}
|
||||
<span>${{item.modified}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
${{(item.type === "video" || item.type === "audio") ?
|
||||
`<button class="btn-play" onclick="event.stopPropagation(); playMedia('${{item.path}}')">▶</button>` : ''}}
|
||||
<button class="btn-download" onclick="event.stopPropagation(); downloadFile('${{item.path}}')">⬇</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join("");
|
||||
}}
|
||||
|
||||
async function loadData() {{
|
||||
loading.style.display = "block";
|
||||
fileList.innerHTML = "";
|
||||
empty.style.display = "none";
|
||||
|
||||
try {{
|
||||
const url = `/api/filelist?path=${{encodeURIComponent(currentPath)}}`;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
if (data.success) {{
|
||||
items = data.items || [];
|
||||
render();
|
||||
}} else {{
|
||||
throw new Error(data.error || "加载失败");
|
||||
}}
|
||||
}} catch (e) {{
|
||||
fileList.innerHTML = `<div class="error">❌ 加载失败: ${{e.message}}</div>`;
|
||||
loading.style.display = "none";
|
||||
}}
|
||||
}}
|
||||
|
||||
let timeout;
|
||||
searchInput.addEventListener("input", function() {{
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {{
|
||||
searchQuery = this.value.trim();
|
||||
render();
|
||||
}}, 200);
|
||||
}});
|
||||
|
||||
document.addEventListener("keydown", function(e) {{
|
||||
if ((e.ctrlKey && e.key === "f") || (e.key === "/" && !e.ctrlKey && !e.metaKey)) {{
|
||||
e.preventDefault();
|
||||
searchInput.focus();
|
||||
searchInput.select();
|
||||
}}
|
||||
if (e.key === "Escape") {{
|
||||
searchInput.value = "";
|
||||
searchQuery = "";
|
||||
render();
|
||||
searchInput.blur();
|
||||
}}
|
||||
}});
|
||||
|
||||
btnRefresh.addEventListener("click", loadData);
|
||||
loadData();
|
||||
</script>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
# ==================== 主路由 ====================
|
||||
@app.route('/', defaults={'path': ''})
|
||||
@app.route('/<path:path>')
|
||||
def serve_file(path=''):
|
||||
try:
|
||||
if not FILESYSTEM_ENABLED:
|
||||
return "文件系统未启用", 403
|
||||
|
||||
full_path = safe_path_join(HTML_ROOT_DIR, path)
|
||||
if full_path is None:
|
||||
return "无效的路径", 403
|
||||
|
||||
if not os.path.exists(full_path):
|
||||
return "路径不存在", 404
|
||||
|
||||
# 文件处理
|
||||
if os.path.isfile(full_path):
|
||||
if request.args.get('download') == 'true':
|
||||
return send_file(full_path, as_attachment=True, download_name=os.path.basename(full_path))
|
||||
|
||||
if is_video_file(full_path) or is_audio_file(full_path):
|
||||
return send_file(full_path, mimetype=get_media_mime_type(full_path))
|
||||
|
||||
# 普通文件
|
||||
with open(full_path, 'rb') as f:
|
||||
content = f.read()
|
||||
ext = os.path.splitext(full_path)[1].lower()
|
||||
mime_map = {
|
||||
'.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css',
|
||||
'.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg', '.gif': 'image/gif', '.txt': 'text/plain',
|
||||
'.pdf': 'application/pdf', '.svg': 'image/svg+xml', '.ico': 'image/x-icon'
|
||||
}
|
||||
return Response(content, mimetype=mime_map.get(ext, 'application/octet-stream'))
|
||||
|
||||
# 目录处理 - 生成面包屑
|
||||
breadcrumb = ''
|
||||
if path:
|
||||
parts = path.split('/')
|
||||
current = ''
|
||||
for i, part in enumerate(parts):
|
||||
if i == len(parts) - 1:
|
||||
breadcrumb += f'<span class="sep">›</span><span class="current">{part}</span>'
|
||||
else:
|
||||
current += part + '/'
|
||||
breadcrumb += f'<span class="sep">›</span><a href="/{current}">{part}</a>'
|
||||
|
||||
# 使用函数生成HTML,避免字符串格式化问题
|
||||
html = get_html_template(path, breadcrumb)
|
||||
return Response(html, mimetype='text/html')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return f"服务器内部错误: {str(e)}", 500
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health_check():
|
||||
return jsonify({
|
||||
'status': 'healthy',
|
||||
'filesystem': {'enabled': FILESYSTEM_ENABLED, 'root': HTML_ROOT_DIR if FILESYSTEM_ENABLED else None}
|
||||
})
|
||||
|
||||
# ==================== 主程序 ====================
|
||||
if __name__ == '__main__':
|
||||
host = file_config['server']['host']
|
||||
port = int(file_config['server']['port'])
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("📁 文件托管服务器 (极简版)")
|
||||
print("="*50)
|
||||
print(f"地址: http://{host}:{port}")
|
||||
print(f"目录: {os.path.abspath(HTML_ROOT_DIR) if FILESYSTEM_ENABLED else '未启用'}")
|
||||
print("="*50)
|
||||
print("\n快捷键: / 或 Ctrl+F 搜索, ESC 清空搜索")
|
||||
print("\n")
|
||||
|
||||
if SSL_ENABLED and os.path.exists(SSL_CERT_FILE) and os.path.exists(SSL_KEY_FILE):
|
||||
app.run(host=host, port=port, ssl_context=(SSL_CERT_FILE, SSL_KEY_FILE), debug=False)
|
||||
else:
|
||||
app.run(host=host, port=port, debug=False)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,530 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes, viewport-fit=cover">
|
||||
<title>中国传统色彩 - 移动端适配版</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", Roboto, Helvetica, Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 32px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.9rem;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 移动端选项卡优化 */
|
||||
.tabs {
|
||||
display: flex;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
padding: 14px 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
color: #6c757d;
|
||||
border: none;
|
||||
background: none;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
animation: fadeIn 0.25s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* 移动端:色彩网格改为2列布局,卡片更易点击 */
|
||||
.color-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
/* 针对小屏手机强制2列 */
|
||||
@media (max-width: 480px) {
|
||||
.color-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
.tab {
|
||||
font-size: 0.85rem;
|
||||
padding: 12px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 中等屏幕自动调整 */
|
||||
@media (min-width: 481px) and (max-width: 768px) {
|
||||
.color-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.color-grid {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.color-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.color-card:active {
|
||||
transform: scale(0.97);
|
||||
transition: 0.05s;
|
||||
}
|
||||
|
||||
.color-swatch {
|
||||
height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.color-info {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.color-name {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.color-code {
|
||||
font-size: 0.7rem;
|
||||
color: #6c757d;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Toast 提示优化 - 移动端居中底部 */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(100px);
|
||||
background: #1e293b;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 40px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
opacity: 0;
|
||||
transition: all 0.25s ease;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(4px);
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.toast {
|
||||
white-space: normal;
|
||||
max-width: 80%;
|
||||
text-align: center;
|
||||
padding: 10px 18px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
/* 配色方案区域 - 移动端卡片布局优化 */
|
||||
#palettes-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
#palettes-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.palette-section {
|
||||
margin-bottom: 0;
|
||||
background: #fefefe;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.palette-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
margin: 0;
|
||||
padding: 12px 16px 8px 16px;
|
||||
background: #fafcff;
|
||||
border-bottom: 1px solid #eef2f6;
|
||||
}
|
||||
|
||||
.color-palette {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 0 0 16px 16px;
|
||||
overflow: hidden;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.color-palette:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.palette-color {
|
||||
flex: 1;
|
||||
min-width: 0; /* 防止flex溢出 */
|
||||
height: 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
transition: 0.1s;
|
||||
}
|
||||
|
||||
/* 移动端让颜色代码折行更友好 */
|
||||
.palette-color span:first-child {
|
||||
font-size: 0.7rem;
|
||||
font-family: monospace;
|
||||
background: rgba(0,0,0,0.3);
|
||||
padding: 2px 6px;
|
||||
border-radius: 30px;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.palette-color span:last-child {
|
||||
font-size: 0.6rem;
|
||||
opacity: 0.9;
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
/* 说明文字区域移动端优化 */
|
||||
.palettes-desc {
|
||||
background: #f1f5f9;
|
||||
margin: 0 16px 16px 16px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
color: #475569;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.palettes-desc strong {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
/* 增加触摸反馈 */
|
||||
.color-card, .color-palette {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 滚动条隐藏但保持功能 (移动端美观) */
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🎨 中国传统色彩</h1>
|
||||
<p>轻触颜色块,复制色值</p>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-tab="traditional">中国传统色</button>
|
||||
<button class="tab" data-tab="chinese">中文色彩</button>
|
||||
<button class="tab" data-tab="palettes">色彩搭配</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content active" id="traditional">
|
||||
<div class="color-grid" id="traditional-grid"></div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="chinese">
|
||||
<div class="color-grid" id="chinese-grid"></div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="palettes">
|
||||
<div class="palettes-desc">
|
||||
💡 <strong>主色</strong>(第一色)品牌核心色 · <strong>辅助色</strong>(第二色)点缀强调 · <strong>中性色</strong>(第三色)文字/背景<br>
|
||||
✨ 点击配色块,一键复制整套色彩代码(三个色值)。
|
||||
</div>
|
||||
<div id="palettes-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast">🎨 颜色代码已复制</div>
|
||||
|
||||
<script>
|
||||
// 原始数据保持不变(完全未修改接口/功能)
|
||||
const traditionalColors = [
|
||||
{ name: '蔚蓝', hex: '#70f3ff' }, { name: '蓝', hex: '#44cef6' }, { name: '碧蓝', hex: '#3eede7' }, { name: '石青', hex: '#1685a9' }, { name: '靛青', hex: '#177cb0' },
|
||||
{ name: '靛蓝', hex: '#065279' }, { name: '花青', hex: '#003472' }, { name: '宝蓝', hex: '#4b5cc4' }, { name: '蓝灰色', hex: '#a1afc9' }, { name: '藏青', hex: '#2e4e7e' },
|
||||
{ name: '藏蓝', hex: '#3b2e7e' }, { name: '黛', hex: '#4a4266' }, { name: '黛绿', hex: '#426666' }, { name: '黛蓝', hex: '#425066' }, { name: '黛紫', hex: '#574266' },
|
||||
{ name: '紫色', hex: '#8d4bbb' }, { name: '紫酱', hex: '#815463' }, { name: '酱紫', hex: '#815476' }, { name: '紫檀', hex: '#4c221b' }, { name: '绀青', hex: '#003371' },
|
||||
{ name: '紫棠', hex: '#56004f' }, { name: '青莲', hex: '#801dae' }, { name: '群青', hex: '#4c8dae' }, { name: '雪青', hex: '#b0a4e3' }, { name: '丁香色', hex: '#cca4e3' },
|
||||
{ name: '藕色', hex: '#edd1d8' }, { name: '藕荷色', hex: '#e4c6d0' }, { name: '朱砂', hex: '#ff461f' }, { name: '火红', hex: '#ff2d51' }, { name: '朱膘', hex: '#f36838' },
|
||||
{ name: '妃色', hex: '#ed5736' }, { name: '洋红', hex: '#ff4777' }, { name: '品红', hex: '#f00056' }, { name: '粉红', hex: '#ffb3a7' }, { name: '桃红', hex: '#f47983' },
|
||||
{ name: '海棠红', hex: '#db5a6b' }, { name: '樱桃色', hex: '#c93756' }, { name: '酡颜', hex: '#f9906f' }, { name: '银红', hex: '#f05654' }, { name: '大红', hex: '#ff2121' },
|
||||
{ name: '石榴红', hex: '#f20c00' }, { name: '绛紫', hex: '#8c4356' }, { name: '绯红', hex: '#c83c23' }, { name: '胭脂', hex: '#9d2933' }, { name: '朱红', hex: '#ff4c00' },
|
||||
{ name: '丹', hex: '#ff4e20' }, { name: '彤', hex: '#f35336' }, { name: '酡红', hex: '#dc3023' }, { name: '炎', hex: '#ff3300' }, { name: '茜色', hex: '#cb3a56' },
|
||||
{ name: '绾', hex: '#a98175' }, { name: '檀', hex: '#b36d61' }, { name: '嫣红', hex: '#ef7a82' }, { name: '枣红', hex: '#c32136' }, { name: '殷红', hex: '#be002f' },
|
||||
{ name: '赫赤', hex: '#c91f37' }, { name: '银朱', hex: '#bf242a' }, { name: '赤', hex: '#c3272b' }, { name: '栗色', hex: '#60281e' }, { name: '玄色', hex: '#622a1d' },
|
||||
{ name: '松花色', hex: '#bce672' }, { name: '柳黄', hex: '#c9dd22' }, { name: '嫩绿', hex: '#bddd22' }, { name: '柳绿', hex: '#afdd22' }, { name: '葱黄', hex: '#a3d900' },
|
||||
{ name: '葱绿', hex: '#9ed900' }, { name: '豆绿', hex: '#9ed048' }, { name: '豆青', hex: '#96ce54' }, { name: '油绿', hex: '#00bc12' }, { name: '葱倩', hex: '#0eb83a' },
|
||||
{ name: '葱青', hex: '#0eb83a' }, { name: '青葱', hex: '#0aa344' }, { name: '石绿', hex: '#16a951' }, { name: '松柏绿', hex: '#21a675' }, { name: '松花绿', hex: '#057748' },
|
||||
{ name: '绿沈', hex: '#0c8918' }, { name: '绿色', hex: '#00e500' }, { name: '草绿', hex: '#40de5a' }, { name: '青翠', hex: '#00e079' }, { name: '青色', hex: '#00e09e' },
|
||||
{ name: '翡翠色', hex: '#3de1ad' }, { name: '碧绿', hex: '#2add9c' }, { name: '玉色', hex: '#2edfa3' }, { name: '缥', hex: '#7fecad' }, { name: '艾绿', hex: '#a4e2c6' },
|
||||
{ name: '碧色', hex: '#1bd1a5' }, { name: '青碧', hex: '#48c0a3' }, { name: '铜绿', hex: '#549688' }, { name: '竹青', hex: '#789262' }, { name: '墨灰', hex: '#758a99' },
|
||||
{ name: '墨色', hex: '#50616d' }, { name: '鸦青', hex: '#424c50' }, { name: '黯', hex: '#41555d' }, { name: '樱草色', hex: '#eaff56' }, { name: '鹅黄', hex: '#fff143' },
|
||||
{ name: '鸭黄', hex: '#faff72' }, { name: '杏黄', hex: '#ffa631' }, { name: '橙黄', hex: '#ffa400' }, { name: '橙色', hex: '#fa8c35' }, { name: '杏红', hex: '#ff8c31' },
|
||||
{ name: '橘黄', hex: '#ff8936' }, { name: '橘红', hex: '#ff7500' }, { name: '藤黄', hex: '#ffb61e' }, { name: '姜黄', hex: '#ffc773' }, { name: '雌黄', hex: '#ffc64b' },
|
||||
{ name: '赤金', hex: '#f2be45' }, { name: '缃色', hex: '#f0c239' }, { name: '雄黄', hex: '#e9bb1d' }, { name: '秋香色', hex: '#d9b611' }, { name: '金色', hex: '#eacd76' },
|
||||
{ name: '牙色', hex: '#eedeb0' }, { name: '枯黄', hex: '#d3b17d' }, { name: '黄栌', hex: '#e29c45' }, { name: '乌金', hex: '#a78e44' }, { name: '昏黄', hex: '#c89b40' },
|
||||
{ name: '棕黄', hex: '#ae7000' }, { name: '琥珀', hex: '#ca6924' }, { name: '棕色', hex: '#b25d25' }, { name: '茶色', hex: '#b35c44' }, { name: '棕红', hex: '#9b4400' },
|
||||
{ name: '赭', hex: '#9c5333' }, { name: '驼色', hex: '#a88462' }, { name: '秋色', hex: '#896c39' }, { name: '棕绿', hex: '#827100' }, { name: '褐色', hex: '#6e511e' },
|
||||
{ name: '棕黑', hex: '#7c4b00' }, { name: '赭色', hex: '#955539' }, { name: '赭石', hex: '#845a33' }, { name: '精白', hex: '#ffffff' }, { name: '银白', hex: '#e9e7ef' },
|
||||
{ name: '铅白', hex: '#f0f0f4' }, { name: '霜色', hex: '#e9f1f6' }, { name: '雪白', hex: '#f0fcff' }, { name: '莹白', hex: '#e3f9fd' }, { name: '月白', hex: '#d6ecf0' },
|
||||
{ name: '象牙白', hex: '#fffbf0' }, { name: '缟', hex: '#f2ecde' }, { name: '鱼肚白', hex: '#fcefe8' }, { name: '白粉', hex: '#fff2df' }, { name: '荼白', hex: '#f3f9f1' },
|
||||
{ name: '鸭卵青', hex: '#e0eee8' }, { name: '素', hex: '#e0f0e9' }, { name: '青白', hex: '#c0ebd7' }, { name: '蟹壳青', hex: '#bbcdc5' }, { name: '花白', hex: '#c2ccd0' },
|
||||
{ name: '老银', hex: '#bacac6' }, { name: '灰色', hex: '#808080' }, { name: '苍色', hex: '#75878a' }, { name: '水色', hex: '#88ada6' }, { name: '黝', hex: '#6b6882' },
|
||||
{ name: '乌色', hex: '#725e82' }, { name: '玄青', hex: '#3d3b4f' }, { name: '乌黑', hex: '#392f41' }, { name: '黎', hex: '#75664d' }, { name: '黧', hex: '#5d513c' },
|
||||
{ name: '黝黑', hex: '#665757' }, { name: '缁色', hex: '#493131' }, { name: '煤黑', hex: '#312520' }, { name: '漆黑', hex: '#161823' }, { name: '黑色', hex: '#000000' }
|
||||
];
|
||||
|
||||
const chineseColors = [
|
||||
{ name: '鸨色', hex: '#f7acbc' }, { name: '踯躅色', hex: '#ef5b9c' }, { name: '桜色', hex: '#feeeed' }, { name: '蔷薇色', hex: '#f05b72' }, { name: '韩红', hex: '#f15b6c' },
|
||||
{ name: '珊瑚色', hex: '#f8aba6' }, { name: '红梅色', hex: '#f69c9f' }, { name: '桃色', hex: '#f58f98' }, { name: '薄柿', hex: '#ca8687' }, { name: '薄红梅', hex: '#f391a9' },
|
||||
{ name: '曙色', hex: '#bd6758' }, { name: '红色', hex: '#d71345' }, { name: '赤丹', hex: '#d64f44' }, { name: '红赤', hex: '#d93a49' }, { name: '臙脂色', hex: '#b3424a' },
|
||||
{ name: '真赭', hex: '#c76968' }, { name: '今様色', hex: '#bb505d' }, { name: '梅染', hex: '#987165' }, { name: '退红色', hex: '#ac6767' }, { name: '苏芳', hex: '#973c3f' },
|
||||
{ name: '茜色', hex: '#b22c46' }, { name: '红', hex: '#a7324a' }, { name: '银朱', hex: '#aa363d' }, { name: '赤', hex: '#ed1941' }, { name: '朱色', hex: '#f26522' },
|
||||
{ name: '洗朱', hex: '#d2553d' }, { name: '红桦色', hex: '#b4534b' }, { name: '红绯', hex: '#ef4136' }, { name: '桦色', hex: '#c63c26' }, { name: '铅丹色', hex: '#f3715c' },
|
||||
{ name: '赭', hex: '#a7573b' }, { name: '绯色', hex: '#aa2116' }, { name: '丹', hex: '#b64533' }, { name: '土', hex: '#b54334' }, { name: '焦香', hex: '#853f04' },
|
||||
{ name: '真红', hex: '#840228' }, { name: '绯', hex: '#7a1723' }, { name: '红海老茶', hex: '#a03939' }, { name: '浅苏芳', hex: '#8a2e3b' }, { name: '鸢色', hex: '#8e453f' },
|
||||
{ name: '小豆色', hex: '#8f4b4a' }, { name: '弁柄色', hex: '#892f1b' }, { name: '栗梅', hex: '#6b2c25' }, { name: '海老茶', hex: '#733a31' }, { name: '深绯', hex: '#54211d' },
|
||||
{ name: '赤铜色', hex: '#78331e' }, { name: '赤褐色', hex: '#53261f' }, { name: '金赤', hex: '#f15a22' }, { name: '赤茶', hex: '#b4533c' }, { name: '赤锖色', hex: '#84331f' },
|
||||
{ name: '黄丹', hex: '#f47a55' }, { name: '赤橙', hex: '#f15a22' }, { name: '柿色', hex: '#f3704b' }, { name: '肉桂色', hex: '#da765b' }, { name: '炼瓦色', hex: '#ae5039' },
|
||||
{ name: '锖色', hex: '#6a3427' }, { name: '桧皮色', hex: '#8f4b38' }, { name: '栗色', hex: '#8e3e1f' }, { name: '黄赤', hex: '#f36c21' }, { name: '代赭', hex: '#b4532a' },
|
||||
{ name: '骆驼色', hex: '#b7704f' }, { name: '黄茶', hex: '#de773f' }, { name: '洗柿', hex: '#c99979' }, { name: '赤白橡', hex: '#deab8a' }, { name: '肌色', hex: '#fedcbd' },
|
||||
{ name: '橙色', hex: '#f47920' }, { name: '灰茶', hex: '#905a3d' }, { name: '茶色', hex: '#8f4b2e' }, { name: '桦茶色', hex: '#87481f' }, { name: '枯茶', hex: '#5f3c23' },
|
||||
{ name: '焦茶', hex: '#6b473c' }, { name: '柑子色', hex: '#faa755' }, { name: '杏色', hex: '#fab27b' }, { name: '蜜柑色', hex: '#f58220' }, { name: '褐色', hex: '#843900' },
|
||||
{ name: '路考茶', hex: '#905d1d' }, { name: '饴色', hex: '#8a5d19' }, { name: '丁子色', hex: '#8c531b' }, { name: '丁子茶', hex: '#826858' }, { name: '黄栌', hex: '#64492b' },
|
||||
{ name: '土器色', hex: '#ae6642' }, { name: '黄枯茶', hex: '#56452d' }, { name: '狐色', hex: '#96582a' }, { name: '黄橡', hex: '#705628' }, { name: '银煤竹', hex: '#4a3113' },
|
||||
{ name: '涅色', hex: '#412f1f' }, { name: '胡桃色', hex: '#845538' }, { name: '香色', hex: '#8e7437' }, { name: '国防色', hex: '#69541b' }, { name: '练色', hex: '#d5c59f' },
|
||||
{ name: '肉色', hex: '#cd9a5b' }, { name: '人色', hex: '#cd9a5b' }, { name: '土色', hex: '#b36d41' }, { name: '小麦色', hex: '#df9464' }, { name: '琥珀色', hex: '#b76f40' },
|
||||
{ name: '木兰色', hex: '#ad8b3d' }, { name: '栀子色', hex: '#dea32c' }, { name: '朽叶', hex: '#d1923f' }, { name: '萱草色', hex: '#c88400' }, { name: '黄金', hex: '#c37e00' },
|
||||
{ name: '金色', hex: '#c37e00' }, { name: '金茶', hex: '#e0861a' }, { name: '卵色', hex: '#ffce7b' }, { name: '山吹色', hex: '#fcaf17' }, { name: '黄土色', hex: '#ba8448' },
|
||||
{ name: '朽叶色', hex: '#896a45' }, { name: '空五倍子色', hex: '#76624c' }, { name: '莺茶', hex: '#6d5826' }, { name: '向日葵色', hex: '#ffc20e' }, { name: '郁金色', hex: '#fdb933' },
|
||||
{ name: '砂色', hex: '#d3c6a6' }, { name: '芥子色', hex: '#c7a252' }, { name: '淡黄', hex: '#dec674' }, { name: '亜麻色', hex: '#b69968' }, { name: '枯色', hex: '#c1a173' }
|
||||
];
|
||||
|
||||
const palettes = [
|
||||
{ title: '清新自然', colors: ['#FFFFCC', '#CCFFFF', '#FFCCCC'] }, { title: '雅致灰调', colors: ['#99CCCC', '#FFCC99', '#FFCCCC'] }, { title: '粉紫梦境', colors: ['#FF9999', '#996699', '#FFCCCC'] },
|
||||
{ title: '大地色系', colors: ['#CC9999', '#FFFFCC', '#CCCC99'] }, { title: '柔和暖阳', colors: ['#FFCCCC', '#FFFF99', '#CCCCFF'] }, { title: '海蓝印象', colors: ['#0099CC', '#CCCCCC', '#FF6666'] },
|
||||
{ title: '珊瑚橘粉', colors: ['#FF9966', '#FF6666', '#FFCCCC'] }, { title: '木质复古', colors: ['#CC9966', '#666666', '#CC9999'] }, { title: '翠绿春意', colors: ['#FF6666', '#FFFF66', '#99CC66'] },
|
||||
{ title: '深海静寂', colors: ['#CC3333', '#CCCCCC', '#003366'] }, { title: '紫罗兰夜', colors: ['#993333', '#CCCC00', '#663366'] }, { title: '燕麦拿铁', colors: ['#CCCC99', '#666666', '#CC9999'] },
|
||||
{ title: '柠檬水岸', colors: ['#FF6666', '#FFFF00', '#0066CC'] }, { title: '红黑经典', colors: ['#CC0033', '#333333', '#CCCC00'] }, { title: '森林与酒', colors: ['#336633', '#990033', '#FFCC99'] }
|
||||
];
|
||||
|
||||
function showToast(msg) {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = msg || '🎨 颜色代码已复制';
|
||||
toast.classList.add('show');
|
||||
setTimeout(() => toast.classList.remove('show'), 1800);
|
||||
}
|
||||
|
||||
function copyToClipboard(text, customMsg) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showToast(customMsg || `✅ 已复制: ${text}`);
|
||||
}).catch(() => {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
showToast(customMsg || `📋 已复制: ${text}`);
|
||||
});
|
||||
}
|
||||
|
||||
function getContrastColor(hex) {
|
||||
let r = parseInt(hex.slice(1, 3), 16);
|
||||
let g = parseInt(hex.slice(3, 5), 16);
|
||||
let b = parseInt(hex.slice(5, 7), 16);
|
||||
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
|
||||
return brightness > 128 ? '#1e1e2f' : '#ffffff';
|
||||
}
|
||||
|
||||
function createColorCard(color) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'color-card';
|
||||
card.onclick = () => copyToClipboard(color.hex, `🎨 ${color.name} → ${color.hex}`);
|
||||
const textColor = getContrastColor(color.hex);
|
||||
card.innerHTML = `
|
||||
<div class="color-swatch" style="background: ${color.hex}; color: ${textColor};">${color.hex}</div>
|
||||
<div class="color-info">
|
||||
<div class="color-name">${color.name}</div>
|
||||
<div class="color-code">${color.hex}</div>
|
||||
</div>
|
||||
`;
|
||||
return card;
|
||||
}
|
||||
|
||||
function createPalette(palette) {
|
||||
const section = document.createElement('div');
|
||||
section.className = 'palette-section';
|
||||
const title = document.createElement('div');
|
||||
title.className = 'palette-title';
|
||||
title.textContent = palette.title;
|
||||
const paletteDiv = document.createElement('div');
|
||||
paletteDiv.className = 'color-palette';
|
||||
paletteDiv.onclick = () => {
|
||||
const allCodes = palette.colors.join(' ');
|
||||
copyToClipboard(allCodes, `🎨 配色方案「${palette.title}」已复制: ${allCodes}`);
|
||||
};
|
||||
const roles = ['主色', '辅色', '中性'];
|
||||
palette.colors.forEach((color, idx) => {
|
||||
const colorDiv = document.createElement('div');
|
||||
colorDiv.className = 'palette-color';
|
||||
colorDiv.style.background = color;
|
||||
colorDiv.style.color = getContrastColor(color);
|
||||
const codeSpan = document.createElement('span');
|
||||
codeSpan.textContent = color;
|
||||
const roleSpan = document.createElement('span');
|
||||
roleSpan.textContent = roles[idx] || '';
|
||||
colorDiv.appendChild(codeSpan);
|
||||
colorDiv.appendChild(roleSpan);
|
||||
paletteDiv.appendChild(colorDiv);
|
||||
});
|
||||
section.appendChild(title);
|
||||
section.appendChild(paletteDiv);
|
||||
return section;
|
||||
}
|
||||
|
||||
function init() {
|
||||
const traditionalGrid = document.getElementById('traditional-grid');
|
||||
traditionalColors.forEach(c => traditionalGrid.appendChild(createColorCard(c)));
|
||||
const chineseGrid = document.getElementById('chinese-grid');
|
||||
chineseColors.forEach(c => chineseGrid.appendChild(createColorCard(c)));
|
||||
const palettesContainer = document.getElementById('palettes-container');
|
||||
palettes.forEach(p => palettesContainer.appendChild(createPalette(p)));
|
||||
}
|
||||
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
const tabId = tab.dataset.tab;
|
||||
document.getElementById(tabId).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,204 @@
|
||||
const msgTip = document.getElementById('msgTip');
|
||||
const pwdTip = document.getElementById('pwdTip');
|
||||
const emlTip = document.getElementById('emlTip');
|
||||
const agreeTip = document.getElementById('agreeTip');
|
||||
const form = document.getElementById('form');
|
||||
|
||||
const usernameInput = document.getElementById('loginUsername');
|
||||
const passwordInput = document.getElementById('loginPassword');
|
||||
const emailInput = document.getElementById('loginEmail');
|
||||
const agreeBox = document.getElementById('agreeBox');
|
||||
const protocol = document.getElementById('agreement');
|
||||
const submit = document.getElementById('loginBtn');
|
||||
|
||||
const resetUser = document.getElementById('resetUser');
|
||||
const registerBtn = document.getElementById('registerBtn');
|
||||
|
||||
resetUser.addEventListener('click', function() {
|
||||
clearTipStyle();
|
||||
})
|
||||
|
||||
function clearTipStyle() {
|
||||
msgTip.innerHTML = '';
|
||||
pwdTip.innerHTML = '';
|
||||
agreeTip.innerHTML = '';
|
||||
emlTip.innerHTML = '';
|
||||
|
||||
msgTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||||
pwdTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||||
agreeTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||||
emlTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||||
protocol.classList.remove('tip-red');
|
||||
protocol.classList.remove('tip-green');
|
||||
}
|
||||
clearTipStyle();
|
||||
|
||||
/**
|
||||
* @returns {string|false}
|
||||
*/
|
||||
function checkUsername() {
|
||||
const val = usernameInput.value.trim();
|
||||
msgTip.innerHTML = '';
|
||||
msgTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||||
|
||||
if (!val) {
|
||||
msgTip.innerHTML = '用户名不可为空';
|
||||
msgTip.classList.add('tip-red');
|
||||
return false;
|
||||
}
|
||||
if (val.length > 20) {
|
||||
msgTip.innerHTML = '用户名不得长于20字符!';
|
||||
msgTip.classList.add('tip-yellow');
|
||||
return false;
|
||||
}
|
||||
|
||||
msgTip.innerHTML = '√ 用户名格式正确';
|
||||
msgTip.classList.add('tip-green');
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string|false}
|
||||
*/
|
||||
function checkPwd() {
|
||||
const val = passwordInput.value.trim();
|
||||
pwdTip.innerHTML = '';
|
||||
pwdTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||||
|
||||
if (!val) {
|
||||
pwdTip.innerHTML = '密码不可为空!';
|
||||
pwdTip.classList.add('tip-red');
|
||||
return false;
|
||||
}
|
||||
|
||||
pwdTip.innerHTML = '河狸';
|
||||
pwdTip.classList.add('tip-green');
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string|false}
|
||||
*/
|
||||
function checkEml() {
|
||||
const val = emailInput.value.trim();
|
||||
const emlStand = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
emlTip.innerHTML = '';
|
||||
emlTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||||
|
||||
if (!val) {
|
||||
emlTip.innerHTML = '邮箱不可为空!';
|
||||
emlTip.classList.add('tip-red');
|
||||
return false;
|
||||
} else if (!emlStand.test(val)) {
|
||||
emlTip.innerHTML = '邮箱格式不正确!';
|
||||
emlTip.classList.add('tip-yellow');
|
||||
return false;
|
||||
}
|
||||
|
||||
emlTip.innerHTML = '河狸';
|
||||
emlTip.classList.add('tip-green');
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function checkAgree() {
|
||||
agreeTip.innerHTML = '';
|
||||
agreeTip.classList.remove('tip-red');
|
||||
protocol.classList.remove('tip-green', 'tip-red');
|
||||
if (!agreeBox.checked) {
|
||||
agreeTip.innerHTML = '请同意用户协议!';
|
||||
agreeTip.classList.add('tip-red');
|
||||
protocol.classList.add('tip-red');
|
||||
return false;
|
||||
}
|
||||
protocol.classList.add('tip-green');
|
||||
return true;
|
||||
}
|
||||
|
||||
usernameInput.addEventListener('focus', checkUsername);
|
||||
passwordInput.addEventListener('focus', checkPwd);
|
||||
|
||||
usernameInput.addEventListener('input', checkUsername);
|
||||
passwordInput.addEventListener('input', checkPwd);
|
||||
|
||||
usernameInput.addEventListener('blur', checkUsername);
|
||||
passwordInput.addEventListener('blur', checkPwd);
|
||||
|
||||
emailInput.addEventListener('focus', checkEml);
|
||||
emailInput.addEventListener('input', checkEml);
|
||||
emailInput.addEventListener('blur', checkEml);
|
||||
|
||||
agreeBox.addEventListener('change', checkAgree);
|
||||
|
||||
registerBtn.addEventListener('click', function() {
|
||||
location.href = 'register.html';
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {string}
|
||||
* @returns {string}
|
||||
*/
|
||||
function htmlEscape(str) {
|
||||
if (typeof str !== 'string') return str;
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
clearTipStyle();
|
||||
|
||||
const userOk = checkUsername();
|
||||
const pwdOk = checkPwd();
|
||||
const emlOk = checkEml();
|
||||
const agreeOk = checkAgree();
|
||||
|
||||
if (!(userOk && pwdOk && emlOk && agreeOk)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const jsonBody = JSON.stringify({
|
||||
username: userOk,
|
||||
password: pwdOk
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await fetch('/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: jsonBody
|
||||
});
|
||||
|
||||
const responseText = await res.text();
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = JSON.parse(responseText);
|
||||
} catch (err) {
|
||||
throw new Error(`服务器返回了非 JSON 内容(HTTP ${res.status}):${responseText.slice(0, 100)}`);
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(result.message || `登录失败(HTTP ${res.status})`);
|
||||
}
|
||||
|
||||
if (result.code === 0) {
|
||||
alert(`${result.message},账号:${result.data.username}`);
|
||||
sessionStorage.setItem('token', result.data.token);
|
||||
location.href = 'main.html';
|
||||
} else {
|
||||
alert(result.message || '登录失败');
|
||||
}
|
||||
} catch (err) {
|
||||
alert(err.message || '接口请求失败,请检查后端服务是否启动');
|
||||
console.error('登录请求异常:', err);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
let sendCode = "";
|
||||
let codeTarget = "";
|
||||
|
||||
getCode.onclick = function () {
|
||||
const phoneMail = email.value.trim();
|
||||
|
||||
if (phoneMail === "") {
|
||||
alert("请先填写邮箱号!");
|
||||
return;
|
||||
}
|
||||
|
||||
sendCode = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
codeTarget = phoneMail;
|
||||
|
||||
alert(`验证码已发送至 ${phoneMail},您的验证码:${sendCode}`);
|
||||
};
|
||||
|
||||
regForm.onsubmit = function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const uname = username.value.trim();
|
||||
const emailVal = email.value.trim();
|
||||
const codeVal = code.value.trim();
|
||||
const pwdVal = psw.value;
|
||||
const repwdVal = repwd.value;
|
||||
|
||||
if (uname === "") {
|
||||
alert("请填写用户名");
|
||||
return;
|
||||
}
|
||||
|
||||
if (emailVal === "") {
|
||||
alert("请填写邮箱号");
|
||||
return;
|
||||
}
|
||||
|
||||
if (sendCode === "") {
|
||||
alert("请先点击获取验证码!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (emailVal !== codeTarget) {
|
||||
alert("邮箱已修改,请重新获取验证码!");
|
||||
sendCode = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^\d{6}$/.test(codeVal)) {
|
||||
alert("验证码必须是6位数字");
|
||||
return;
|
||||
}
|
||||
|
||||
if (codeVal !== sendCode) {
|
||||
alert("验证码错误,请核对!");
|
||||
code.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (pwdVal.length < 6) {
|
||||
alert("密码至少6位字符");
|
||||
return;
|
||||
}
|
||||
|
||||
if (pwdVal !== repwdVal) {
|
||||
alert("两次输入的密码不一致");
|
||||
return;
|
||||
}
|
||||
|
||||
alert("注册成功!");
|
||||
regForm.reset();
|
||||
sendCode = "";
|
||||
codeTarget = "";
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!-- 移动端适配核心标签 -->
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
|
||||
<title>登录</title>
|
||||
<style>
|
||||
.tip-red {color:red;}
|
||||
.tip-yellow {color: orange;}
|
||||
.tip-green {color: greenyellow;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>用户登录</h2>
|
||||
<form action="" id="form" method="post">
|
||||
<div>
|
||||
<label>用户名/推荐邮箱:</label>
|
||||
<input type="text" id="loginUsername" name="username" placeholder="请输入用户名">
|
||||
<span id="msgTip" ></span>
|
||||
<br>
|
||||
<label>密码:</label>
|
||||
<input type="password" id="loginPassword" name="password" placeholder="请输入密码">
|
||||
<span id="pwdTip"></span>
|
||||
</div>
|
||||
<div>
|
||||
<label>验证邮箱:</label>
|
||||
<input type="email" id="loginEmail" name="email" placeholder="请输入验证邮箱">
|
||||
<span id="emlSub">
|
||||
<input type="submit" value="发送验证码">
|
||||
</span>
|
||||
<span id="emlTip"></span>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" id="registerBtn">注册</button>
|
||||
<input type="submit" id="loginBtn" value="登录">
|
||||
<input type="reset" id="resetUser" value="重置">
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="agreeBox" name="agree" value="access">
|
||||
<label id="agreement">同意该系统相关用户使用条例</label>
|
||||
<span id="agreeTip"></span>
|
||||
</div>
|
||||
</form>
|
||||
<script src="js/login.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,102 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!-- 移动端适配核心标签 -->
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
|
||||
<title>DNFS · 树状笔记</title>
|
||||
<style></style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 文章主标题 -->
|
||||
<div id="article-title">
|
||||
<h1>
|
||||
<p>Python</p>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- 文章详情 -->
|
||||
<div id="publish-time">
|
||||
<p>发布时间:2026-07-11</p>
|
||||
</div>
|
||||
<div>
|
||||
<span id="visit-count">浏览:126</span>
|
||||
<span id="like-count">点赞:28</span>
|
||||
<span id="share-count">分享:7</span>
|
||||
</div>
|
||||
|
||||
<!-- 文章正文-->
|
||||
<div id="article-content">
|
||||
<p>
|
||||
Python 的 <code>socket</code>
|
||||
模块是网络编程的核心,其函数可分为<strong>核心套接字操作</strong>、<strong>套接字选项设置</strong>、<strong>辅助工具函数</strong>三大类。本文将逐一拆解所有函数的语法、参数、用法和实战示例,覆盖从基础到高级的所有场景。
|
||||
</p>
|
||||
|
||||
<h2>一、模块初始化与核心对象</h2>
|
||||
<h3>1. socket.socket():创建套接字对象</h3>
|
||||
<p><strong>语法</strong>:</p>
|
||||
<pre><code>socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)</code></pre>
|
||||
<p><strong>作用</strong>:创建一个套接字对象,是所有网络操作的基础。</p>
|
||||
<p><strong>参数说明</strong>:</p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>参数</th>
|
||||
<th>取值</th>
|
||||
<th>说明</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>family</code></td>
|
||||
<td><code>AF_INET</code>(默认)</td>
|
||||
<td>IPv4 协议族(最常用)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><code>AF_INET6</code></td>
|
||||
<td>IPv6 协议族</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><code>AF_UNIX</code></td>
|
||||
<td>Unix 域套接字(本地进程通信)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>type</code></td>
|
||||
<td><code>SOCK_STREAM</code>(默认)</td>
|
||||
<td>TCP 套接字(面向连接、可靠)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><code>SOCK_DGRAM</code></td>
|
||||
<td>UDP 套接字(无连接、数据报)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><code>SOCK_RAW</code></td>
|
||||
<td>原始套接字(可操作 IP 层,用于抓包)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>proto</code></td>
|
||||
<td>0(默认)</td>
|
||||
<td>协议编号,默认自动匹配 type(TCP=6,UDP=17)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>fileno</code></td>
|
||||
<td>None(默认)</td>
|
||||
<td>复用已有文件描述符,无需手动指定</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="other-recommend">
|
||||
<h3>其他推荐</h3>
|
||||
</div>
|
||||
<div class="recommend-list" id="recommend-list">
|
||||
<ul>
|
||||
<li><a href="https://www.baidu.com">百度推荐</a></li>
|
||||
<li><a href="https://www.baidu.com">百度推荐</a></li>
|
||||
<li><a href="https://www.baidu.com">百度推荐</a></li>
|
||||
<li><a href="https://www.baidu.com">百度推荐</a></li>
|
||||
<li><a href="https://www.baidu.com">百度推荐</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
|
||||
<title>注册页面</title>
|
||||
</head>
|
||||
<body>
|
||||
<h3>注册页面</h3>
|
||||
<form>
|
||||
用户名/推荐邮箱:<input type="text" id="username" placeholder="请输入用户名"><br>
|
||||
邮箱号:<input type="text" id="email" placeholder="请输入邮箱号"><br>
|
||||
<div class="code-box">验证码:
|
||||
<input type="text" id="code" placeholder="请输入6位验证码">
|
||||
<input type="button" id="getCode" value="获取验证码">
|
||||
</div>
|
||||
密码:<input type="password" id="psw"><br>
|
||||
验证密码:<input type="password" id="repwd" placeholder="请再次输入密码"><br>
|
||||
<input type="submit" value="提交">
|
||||
<input type="button" value="返回登录" onclick="location.href='login.html'">
|
||||
<script src="register.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
nuitka
|
||||
Flask>=2.3.0,<3.0.0
|
||||
Flask-CORS>=4.0.0,<5.0.0
|
||||
Flask-SQLAlchemy>=3.1.0,<4.0.0
|
||||
Flask-Limiter>=3.3.0,<4.0.0
|
||||
Flask-SocketIO>=5.3.0,<6.0.0
|
||||
python-socketio>=5.10.0,<6.0.0
|
||||
Werkzeug>=2.3.0,<3.0.0
|
||||
requests>=2.31.0,<3.0.0
|
||||
websocket-client>=1.6.0,<2.0.0
|
||||
python-engineio>=4.7.0,<5.0.0
|
||||
Reference in new issue
Block a user