0

0

基于Servlet实现RMI突破防火墙

巴扎黑

巴扎黑

发布时间:2016-12-20 16:30:00

|

1967人浏览过

|

来源于php中文网

原创

package com.mypack.web.rmi;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.net.URL;
import java.net.UnknownHostException;
import java.rmi.Naming;
import java.rmi.RMISecurityManager;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.RMIClassLoader;
import java.rmi.server.UnicastRemoteObject;
import java.util.Hashtable;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * The default RMI socket factory contains several "fallback" mechanisms which
 * enable an RMI client to communicate with a remote server. When an RMI client
 * initiates contact with a remote server, it attempts to establish a connection
 * using each of the following protocols in turn, until one succeeds:
 * 
 * 1. Direct TCP connection. 2. Direct HTTP connection. 3. Proxy connection
 * (SOCKS or HTTP). 4. Connection on port 80 over HTTP to a CGI script. 5. Proxy
 * HTTP connection to CGI script on port 80.
 * 
 * The RMI ServletHandler can be used as replacement for the java-rmi.cgi script
 * that comes with the Java Development Kit (and is invoked in protocols 4 and 5
 * above). The java-rmi.cgi script and the ServletHandler both function as proxy
 * applications that forward remote calls embedded in HTTP to local RMI servers
 * which service these calls. The RMI ServletHandler enables RMI to tunnel
 * remote method calls over HTTP more efficiently than the existing java-rmi.cgi
 * script. The ServletHandler is only loaded once from the servlet
 * administration utility. The script, java-rmi.cgi, is executed once every
 * remote call.
 * 
 * The ServletHandler class contains methods for executing as a Java servlet
 * extension. Because the RMI protocol only makes use of the HTTP post command,
 * the ServletHandler only supports the doPost
 * HttpServlet method. The doPost method of this class
 * interprets a servlet request's query string as a command of the form
 * "=". These commands are represented by the abstract
 * interface, RMICommandHandler. Once the doPost
 * method has parsed the requested command, it calls the execute method on one
 * of several command handlers in the commands array.
 * 
 * The command that actually proxies remote calls is the
 * ServletForwardCommand. When the execute method is invoked on the
 * ServletForwardCommand, the command will open a connection on a local port
 * specified by its param parameter and will proceed to write the
 * body of the relevant post request into this connection. It is assumed that an
 * RMI server (e.g. SampleRMIServer) is listening on the local port, "param."
 * The "forward" command will then read the RMI server's response and send this
 * information back to the RMI client as the body of the response to the HTTP
 * post method.
 * 
 * Because the ServletHandler uses a local socket to proxy remote calls, the
 * servlet has the ability to forward remote calls to local RMI objects that
 * reside in the ServletVM or outside of it.
 * 
 * Servlet documentation may be found at the following location:
 * 
 * http://jserv.javasoft.com/products/java-server/documentation/
 * webserver1.0.2/apidoc/Package-javax.servlet.http.html
 */
public class ServletHandler extends HttpServlet implements Runnable {
/* Variables to hold optional configuration properties. */
/**
* serialVersionUID
*/
private static final long serialVersionUID = 1L;
/** codebase from which this servlet will load remote objects. */
protected static String initialServerCodebase = null;
/** name of RMI server class to be created in init method */
protected static String initialServerClass = null;
/** name of RMI server class to be created in init method */
protected static String initialServerBindName = null;
/**
* RMICommandHandler is the abstraction for an object that handles a
* particular supported command (for example the "forward" command
* "forwards" call information to a remote server on the local machine).
* 
* The command handler is only used by the ServletHandler so the interface
* is protected.
*/
protected interface RMICommandHandler {
/**
* Return the string form of the command to be recognized in the query
* string.
*/
public String getName();
/**
* Execute the command with the given string as parameter.
*/
public void execute(HttpServletRequest req, HttpServletResponse res,
String param) throws ServletClientException,
ServletServerException, IOException;
}
/**
* List of handlers for supported commands. A new command will be created
* for every service request
*/
private static RMICommandHandler commands[] = new RMICommandHandler[] {
new ServletForwardCommand(), new ServletGethostnameCommand(),
new ServletPingCommand(), new ServletTryHostnameCommand() };
/* construct table mapping command strings to handlers */
private static Hashtable commandLookup;
static {
commandLookup = new Hashtable();
for (int i = 0; i < commands.length; ++i)
commandLookup.put(commands[i].getName(), commands[i]);
}
/**
* Once loaded, Java Servlets continue to run until they are unloaded or the
* webserver is stopped. This example takes advantage of the extended
* Servlet life-cycle and runs a remote object in the Servlet VM.
* 
* To initialize this remote object the Servlet Administrator should specify
* a set of parameters which will be used to download and install an initial
* remote server (see readme.txt).
* 
* If configuration parameters are valid (not blank), the servlet will
* attempt to load and start a remote object and a registry in which the
* object may be bound.
* 
* @param config
*            Standard configuration object for an http servlet.
* 
* @exception ServletException
*                Calling super.init(config) may cause a
*                servlet exception to be thrown.
*/
public void init(ServletConfig config) throws ServletException {
super.init(config);
try {
setConfigParameters(config);
if (!verifyConfigParameters()) {
// dont export any objects.
System.err.println("Some optional parameters not set, "
+ "remote object not exported; "
+ "ServletHandler is runnning.");
return;
}
/*
* RMI requires that a local security manager be responsible for the
* method invocations from remote clients - we need to make sure a
* security manager is installed.
*/
if (System.getSecurityManager() == null) {
System.setSecurityManager(new RMISecurityManager());
}
// create a registry if one is not running already.
try {
LocateRegistry.createRegistry(1099);
} catch (java.rmi.server.ExportException ee) {
// registry already exists, we'll just use it.
} catch (RemoteException re) {
System.err.println(re.getMessage());
re.printStackTrace();
}
/**
* Download and create a server object in a thread so we do not
* interfere with other servlets. Allow init method to return more
* quickly.
*/
(new Thread(this)).start();
System.out.println("RMI Servlet Handler loaded sucessfully.");
} catch (Exception e) {
System.err.println("Exception thrown in RMI ServletHandler: "
+ e.getMessage());
e.printStackTrace();
}
}
/**
* Create the sample RMI server.
*/
public void run() {
try {
UnicastRemoteObject server = createRemoteObjectUsingDownloadedClass();
if (server != null) {
Naming.rebind(initialServerBindName, server);
System.err.println("Remote object created successfully.");
}
} catch (Exception e) {
System.err.println("Exception received while intalling object:");
System.err.println(e.getMessage());
e.printStackTrace();
}
}
/**
* Load and export an initial remote object. The implementation class for
* this remote object should not be loaded from the servlet's class path;
* instead it should be loaded from a URL location that will be accessible
* from a remote client. In the case of this example, that location will be
* initialServerCodebase
*/
UnicastRemoteObject createRemoteObjectUsingDownloadedClass()
throws Exception {
UnicastRemoteObject server = null;
Class serverClass = null;
int MAX_RETRY = 5;
int retry = 0;
int sleep = 2000;
while ((retry < MAX_RETRY) && (serverClass == null)) {
try {
System.err.println("Attempting to load remote class...");
serverClass = RMIClassLoader.loadClass(new URL(
initialServerCodebase), initialServerClass);
// Before we instantiate the obj. make sure it
// is a UnicastRemoteObject.
if (!Class.forName("java.rmi.server.UnicastRemoteObject")
.isAssignableFrom(serverClass)) {
System.err.println("This example requires an "
+ " instance of UnicastRemoteObject,"
+ " remote object not exported.");
} else {
System.out.println("Server class loaded successfully...");
server = ((UnicastRemoteObject) serverClass.newInstance());
}
} catch (java.lang.ClassNotFoundException cnfe) {
retry++;
/**
* The class for the remote object could not be loaded, perhaps
* the webserver has not finished initializing itself yet. Try
* to load the class a few more times...
*/
if (retry >= MAX_RETRY) {
System.err.println("Failed to load remote server "
+ " class. Remote object not " + " exported... ");
} else {
System.err.println("Could not load remote class, "
+ "trying again...");
try {
Thread.sleep(sleep);
} catch (InterruptedException ie) {
}
continue;
}
}
}
return server;
}
/*
* NOTE: If you are using JDK1.2Beta4 or later, it is recommended that you
* provide your servlet with a destroy method that will unexport any remote
* objects that your servlet ever exports. As mentioned in the readme file
* for this example, it is not possible to unexport remote objects in
* JDK1.1.x; there is no method in the RMI 1.1 public API that will perform
* this task. To restart remote objects in the servlet VM, you will have to
* restart your webserver. In JDK1.2x, the methods to unexport a remote
* object are as follows:
* 
* java.rmi.activation.Activatable. unexportObject(Remote obj, boolean
* force) java.rmi.server.UnicastRemoteObject. unexportObject(Remote obj,
* boolean force)
*/
/**
* Execute the command given in the servlet request query string. The string
* before the first '=' in the queryString is interpreted as the command
* name, and the string after the first '=' is the parameters to the
* command.
* 
* @param req
*            HTTP servlet request, contains incoming command and arguments
* @param res
*            HTTP servlet response
* @exception ServletException
*                and IOException when invoking methods of
*                req or res.
*/
public void doPost(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
try {
// Command and parameter for this POST request.
String queryString = req.getQueryString();
String command, param;
int delim = queryString.indexOf("=");
if (delim == -1) {
command = queryString;
param = "";
} else {
command = queryString.substring(0, delim);
param = queryString.substring(delim + 1);
}
System.out.println("command: " + command);
System.out.println("param: " + param);
// lookup command to execute on the client's behalf
RMICommandHandler handler = (RMICommandHandler) commandLookup
.get(command);
// execute the command
if (handler != null)
try {
handler.execute(req, res, param);
} catch (ServletClientException e) {
returnClientError(res, "client error: " + e.getMessage());
e.printStackTrace();
} catch (ServletServerException e) {
returnServerError(res, "internal server error: "
+ e.getMessage());
e.printStackTrace();
}
else
returnClientError(res, "invalid command: " + command);
} catch (Exception e) {
returnServerError(res, "internal error: " + e.getMessage());
e.printStackTrace();
}
}
/**
* Provide more intelligible errors for methods that are likely to be
* called. Let unsupported HTTP "do*" methods result in an error generated
* by the super class.
* 
* @param req
*            http Servlet request, contains incoming command and arguments
* 
* @param res
*            http Servlet response
* 
* @exception ServletException
*                and IOException when invoking methods of
*                req or res.
*/
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
returnClientError(res, "GET Operation not supported: "
+ "Can only forward POST requests.");
}
public void doPut(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
returnClientError(res, "PUT Operation not supported: "
+ "Can only forward POST requests.");
}
public String getServletInfo() {
return "RMI Call Forwarding Servlet Servlet.
\n"; } /** * Return an HTML error message indicating there was error in the client's * request. * * @param res * Servlet response object through which message * will be written to the client which invoked one of this * servlet's methods. * @param message * Error message to be written to client. */ private static void returnClientError(HttpServletResponse res, String message) throws IOException { res .sendError(HttpServletResponse.SC_BAD_REQUEST, "" + "Java RMI Client Error" + "" + "" + "

Java RMI Client Error

" + message + ""); System.err.println(HttpServletResponse.SC_BAD_REQUEST + "Java RMI Client Error" + message); } /** * Return an HTML error message indicating an internal error occurred here * on the server. * * @param res * Servlet response object through which message * will be written to the servlet client. * @param message * Error message to be written to servlet client. */ private static void returnServerError(HttpServletResponse res, String message) throws IOException { res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "" + "Java RMI Server Error" + "" + "" + "

Java RMI Server Error

" + message + ""); System.err.println(HttpServletResponse.SC_INTERNAL_SERVER_ERROR + "Java RMI Server Error: " + message); } /** * Retrieve parameters from servlet configuration object. * * @param config * Standard configuration object for an HTTP servlet. */ protected synchronized void setConfigParameters(ServletConfig config) { try { initialServerCodebase = config .getInitParameter("rmiservlethandler.initialServerCodebase"); initialServerClass = config .getInitParameter("rmiservlethandler.initialServerClass"); initialServerBindName = config .getInitParameter("rmiservlethandler.initialServerBindName"); } catch (Exception e) { System.err.println(""); System.err.println("Could not access init parameter:"); System.err.println(e.getMessage()); e.printStackTrace(); } } /** * Ensure that servlet configuration parameters are valid. * * @return true if all relevant configuration parameters are * valid (i.e. not "") false otherwise. */ protected synchronized boolean verifyConfigParameters() { return ((verifyParameter("rmiservlethandler.initialServerClass ", initialServerClass)) && (verifyParameter("rmiservlethandler.initialServerBindName ", initialServerBindName)) && (verifyParameter( "rmiservlethandler.initialServerCodebase", initialServerCodebase))); } /** * Verify that a single parameter is valid. * * @return true if the parameter is valid. */ protected boolean verifyParameter(String parameterName, String parameter) { if ((parameter == null) || (parameter.equals(""))) { System.err.println("optional parameter is invalid and " + "will not be used: \n " + parameterName + " = " + parameter); return false; } else { System.err.println(parameterName + " " + "valid: " + parameter); } return true; } /* * The ServletHandler class is the only object that needs to access the * CommandHandler subclasses, so we write the commands internal to the * servlet handler. */ /** * Class that has an execute command to forward request body to local port * on the server and send server reponse back to client. */ protected static class ServletForwardCommand implements RMICommandHandler { public String getName() { return "forward"; } /** * Execute the forward command. Forwards data from incoming servlet * request to a port on the local machine. Presumably, an RMI server * will be reading the data that this method sends. * * @param req * The servlet request. * @param res * The servlet response. * @param param * Port to which data will be sent. */ public void execute(HttpServletRequest req, HttpServletResponse res, String param) throws ServletClientException, ServletServerException, IOException { int port; try { port = Integer.parseInt(param); } catch (NumberFormatException e) { throw new ServletClientException("invalid port number: " + param); } if (port <= 0 || port > 0xFFFF) throw new ServletClientException("invalid port: " + port); if (port < 1024) throw new ServletClientException("permission denied for port: " + port); byte buffer[]; Socket socket; try { socket = new Socket(InetAddress.getLocalHost(), port); } catch (IOException e) { throw new ServletServerException("could not connect to " + "local port"); } // read client's request body DataInputStream clientIn = new DataInputStream(req.getInputStream()); buffer = new byte[req.getContentLength()]; try { clientIn.readFully(buffer); } catch (EOFException e) { throw new ServletClientException("unexpected EOF " + "reading request body"); } catch (IOException e) { throw new ServletClientException("error reading request" + " body"); } DataOutputStream socketOut = null; // send to local server in HTTP try { socketOut = new DataOutputStream(socket.getOutputStream()); socketOut.writeBytes("POST / HTTP/1.0\r\n"); socketOut.writeBytes("Content-length: " + req.getContentLength() + "\r\n\r\n"); socketOut.write(buffer); socketOut.flush(); } catch (IOException e) { throw new ServletServerException("error writing to server"); } // read response from local server DataInputStream socketIn; try { socketIn = new DataInputStream(socket.getInputStream()); } catch (IOException e) { throw new ServletServerException("error reading from " + "server"); } String key = "Content-length:".toLowerCase(); boolean contentLengthFound = false; String line; int responseContentLength = -1; do { try { line = socketIn.readLine(); } catch (IOException e) { throw new ServletServerException( "error reading from server"); } if (line == null) throw new ServletServerException( "unexpected EOF reading server response"); if (line.toLowerCase().startsWith(key)) { if (contentLengthFound) ; // what would we want to do in this case?? responseContentLength = Integer.parseInt(line.substring( key.length()).trim()); contentLengthFound = true; } } while ((line.length() != 0) && (line.charAt(0) != '\r') && (line.charAt(0) != '\n')); if (!contentLengthFound || responseContentLength < 0) throw new ServletServerException( "missing or invalid content length in server response"); buffer = new byte[responseContentLength]; try { socketIn.readFully(buffer); } catch (EOFException e) { throw new ServletServerException( "unexpected EOF reading server response"); } catch (IOException e) { throw new ServletServerException("error reading from server"); } // send local server response back to servlet client res.setStatus(HttpServletResponse.SC_OK); res.setContentType("application/octet-stream"); res.setContentLength(buffer.length); try { OutputStream out = res.getOutputStream(); out.write(buffer); out.flush(); } catch (IOException e) { throw new ServletServerException("error writing response"); } finally { socketOut.close(); socketIn.close(); } } } /** * Class that has an execute method to return the host name of the server as * the response body. */ protected static class ServletGethostnameCommand implements RMICommandHandler { public String getName() { return "gethostname"; } public void execute(HttpServletRequest req, HttpServletResponse res, String param) throws ServletClientException, ServletServerException, IOException { byte[] getHostStringBytes = req.getServerName().getBytes(); res.setStatus(HttpServletResponse.SC_OK); res.setContentType("application/octet-stream"); res.setContentLength(getHostStringBytes.length); OutputStream out = res.getOutputStream(); out.write(getHostStringBytes); out.flush(); } } /** * Class that has an execute method to return an OK status to indicate that * connection was successful. */ protected static class ServletPingCommand implements RMICommandHandler { public String getName() { return "ping"; } public void execute(HttpServletRequest req, HttpServletResponse res, String param) throws ServletClientException, ServletServerException, IOException { res.setStatus(HttpServletResponse.SC_OK); res.setContentType("application/octet-stream"); res.setContentLength(0); } } /** * Class that has an execute method to return a human readable message * describing which host name is available to local Java VMs. */ protected static class ServletTryHostnameCommand implements RMICommandHandler { public String getName() { return "hostname"; } public void execute(HttpServletRequest req, HttpServletResponse res, String param) throws ServletClientException, ServletServerException, IOException { PrintWriter pw = res.getWriter(); pw.println(""); pw.println("" + "Java RMI Server Hostname Info" + "" + ""); pw.println("

Java RMI Server Hostname Info

"); pw.println("

Local host name available to Java VM:

"); pw.print("

InetAddress.getLocalHost().getHostName()"); try { String localHostName = InetAddress.getLocalHost().getHostName(); pw.println(" = " + localHostName); } catch (UnknownHostException e) { pw.println(" threw java.net.UnknownHostException"); } pw.println("

Server host information obtained through Servlet " + "interface from HTTP server:

"); pw.println("

SERVER_NAME = " + req.getServerName()); pw.println("

SERVER_PORT = " + req.getServerPort()); pw.println(""); } } /** * ServletClientException is thrown when an error is detected in a client's * request. */ protected static class ServletClientException extends Exception { public ServletClientException(String s) { super(s); } } /** * ServletServerException is thrown when an error occurs here on the server. */ protected static class ServletServerException extends Exception { public ServletServerException(String s) { super(s); } } }

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
拼多多赚钱的5种方法 拼多多赚钱的5种方法
拼多多赚钱的5种方法 拼多多赚钱的5种方法

在拼多多上赚钱主要可以通过无货源模式一件代发、精细化运营特色店铺、参与官方高流量活动、利用拼团机制社交裂变,以及成为多多进宝推广员这5种方法实现。核心策略在于通过低成本、高效率的供应链管理与营销,利用平台社交电商红利实现盈利。

28

2026.01.26

edge浏览器怎样设置主页 edge浏览器自定义设置教程
edge浏览器怎样设置主页 edge浏览器自定义设置教程

在Edge浏览器中设置主页,请依次点击右上角“...”图标 > 设置 > 开始、主页和新建标签页。在“Microsoft Edge 启动时”选择“打开以下页面”,点击“添加新页面”并输入网址。若要使用主页按钮,需在“外观”设置中开启“显示主页按钮”并设定网址。

8

2026.01.26

苹果官方查询网站 苹果手机正品激活查询入口
苹果官方查询网站 苹果手机正品激活查询入口

苹果官方查询网站主要通过 checkcoverage.apple.com/cn/zh/ 进行,可用于查询序列号(SN)对应的保修状态、激活日期及技术支持服务。此外,查找丢失设备请使用 iCloud.com/find,购买信息与物流可访问 Apple (中国大陆) 订单状态页面。

31

2026.01.26

npd人格什么意思 npd人格有什么特征
npd人格什么意思 npd人格有什么特征

NPD(Narcissistic Personality Disorder)即自恋型人格障碍,是一种心理健康问题,特点是极度夸大自我重要性、需要过度赞美与关注,同时极度缺乏共情能力,背后常掩藏着低自尊和不安全感,影响人际关系、工作和生活,通常在青少年时期开始显现,需由专业人士诊断。

3

2026.01.26

windows安全中心怎么关闭 windows安全中心怎么执行操作
windows安全中心怎么关闭 windows安全中心怎么执行操作

关闭Windows安全中心(Windows Defender)可通过系统设置暂时关闭,或使用组策略/注册表永久关闭。最简单的方法是:进入设置 > 隐私和安全性 > Windows安全中心 > 病毒和威胁防护 > 管理设置,将实时保护等选项关闭。

5

2026.01.26

2026年春运抢票攻略大全 春运抢票攻略教你三招手【技巧】
2026年春运抢票攻略大全 春运抢票攻略教你三招手【技巧】

铁路12306提供起售时间查询、起售提醒、购票预填、候补购票及误购限时免费退票五项服务,并强调官方渠道唯一性与信息安全。

35

2026.01.26

个人所得税税率表2026 个人所得税率最新税率表
个人所得税税率表2026 个人所得税率最新税率表

以工资薪金所得为例,应纳税额 = 应纳税所得额 × 税率 - 速算扣除数。应纳税所得额 = 月度收入 - 5000 元 - 专项扣除 - 专项附加扣除 - 依法确定的其他扣除。假设某员工月工资 10000 元,专项扣除 1000 元,专项附加扣除 2000 元,当月应纳税所得额为 10000 - 5000 - 1000 - 2000 = 2000 元,对应税率为 3%,速算扣除数为 0,则当月应纳税额为 2000×3% = 60 元。

12

2026.01.26

oppo云服务官网登录入口 oppo云服务登录手机版
oppo云服务官网登录入口 oppo云服务登录手机版

oppo云服务https://cloud.oppo.com/可以在云端安全存储您的照片、视频、联系人、便签等重要数据。当您的手机数据意外丢失或者需要更换手机时,可以随时将这些存储在云端的数据快速恢复到手机中。

40

2026.01.26

抖币充值官方网站 抖币性价比充值链接地址
抖币充值官方网站 抖币性价比充值链接地址

网页端充值步骤:打开浏览器,输入https://www.douyin.com,登录账号;点击右上角头像,选择“钱包”;进入“充值中心”,操作和APP端一致。注意:切勿通过第三方链接、二维码充值,谨防受骗

7

2026.01.26

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Linux网络安全之防火墙技术汇总
Linux网络安全之防火墙技术汇总

共31课时 | 3.1万人学习

Servlet参考手册
Servlet参考手册

共0课时 | 0人学习

Servlet基础教程
Servlet基础教程

共24课时 | 15.5万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号