热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

netty源码分析(四)Netty提供的Future与ChannelFuture优势分析与源码讲解

上一节我们讲到netty启动服务类AbstractBootstrap的doBind的方法: private ChannelFuture doBind(final Socket

上一节我们讲到netty启动服务类AbstractBootstrap的doBind的方法:

private ChannelFuture doBind(final SocketAddress localAddress) {final ChannelFuture regFuture = initAndRegister();...略}

  • 1
  • 2
  • 3
  • 4

这里边有一个重要的类ChannelFuture :
这里写图片描述

最红他们的接口会来到jdk的Future接口,Future代表了一个异步处理的结果。
/*** A {@code Future} represents the result of an asynchronous* computation. Methods are provided to check if the computation is* complete, to wait for its completion, and to retrieve the result of* the computation. The result can only be retrieved using method* {@code get} when the computation has completed, blocking if* necessary until it is ready. Cancellation is performed by the* {@code cancel} method. Additional methods are provided to* determine if the task completed normally or was cancelled. Once a* computation has completed, the computation cannot be cancelled.* If you would like to use a {@code Future} for the sake* of cancellability but not provide a usable result, you can* declare types of the form {@code Future} and* return {@code null} as a result of the underlying task.译:
一个Future代表一个一步计算的结果,它提供了一些方法检查是否计算完毕,比如等待计算完毕,获取计算结果的方法。
当计算完毕之后只能通过get方法获取结果,或者一直阻塞等待计算的完成。取消操作通过cancle方法来取消,
另外还提供了检测是正常的完成还是被取消的方法,当一个计算完成后,不能进行取消操作。如果你想用Future实现取消,
但是却没有一个可用的结果,你可以声明很多Future的类型,然后返回一个null的结果给当前任务。*

* Sample Usage (Note that the following classes are all* made-up.)*

 {@code* interface ArchiveSearcher { String search(String target); }* class App {*   ExecutorService executor = ...//线程池*   ArchiveSearcher searcher = ...//搜索接口*   void showSearch(final String target)*       throws InterruptedException {*     Future future*       = executor.submit(new Callable() {//创建一个Callable给线程池,Callable是有返回结果的。*         public String call() {*             return searcher.search(target);*         }});*     displayOtherThings(); // do other things while searching  中间可以做其他的事情,submit不会阻塞。*     try {*       displayText(future.get()); // use future  使用future的get拿到线程的处理结果,此处是阻塞的方式。*     } catch (ExecutionException ex) { cleanup(); return; }*   }* }}
** The {@link FutureTask} class is an implementation of {@code Future} that* implements {@code Runnable}, and so may be executed by an {@code Executor}.* FutureTask是Future的实现类,并且实现了Runnable接口,因此可以被Executor执行。* For example, the above construction with {@code submit} could be replaced by:* 之前的方式可以被下边的FutureTask的方式替换。*
 {@code* FutureTask future =*   new FutureTask(new Callable() {*     public String call() {*       return searcher.search(target);*   }});* executor.execute(future);}
**

Memory consistency effects: Actions taken by the asynchronous computation* "package-summary.html#MemoryVisibility"> happen-before* actions following the corresponding {@code Future.get()} in another thread.* 内存一致性影响:异步计算的动作的完成,发生在Future.get()之前。* @see FutureTask* @see Executor* @since 1.5* @author Doug Lea* @param The result type returned by this Future's {@code get} method*/public interface Future {boolean cancel(boolean mayInterruptIfRunning);boolean isCancelled();boolean isDone();V get() throws InterruptedException, ExecutionException;V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

io.netty.util.concurrent.Future对java.util.concurrent.Future进行了扩展:

public interface Future<V> extends java.util.concurrent.Future<V> {boolean isSuccess();//是否计算成功boolean isCancellable();//可以被取消Throwable cause();//原因Future addListener(GenericFutureListenersuper V>> listener);//添加一个监听器Future addListeners(GenericFutureListenersuper V>>... listeners);//添加多个监听器Future removeListener(GenericFutureListenersuper V>> listener);//移除一个监听器Future removeListeners(GenericFutureListenersuper V>>... listeners);//移除多个监听器Future sync() throws InterruptedException;//等待结果返回Future syncUninterruptibly();//等待结果返回&#xff0c;不能被中断Future await() throws InterruptedException;//等待结果返回Future awaitUninterruptibly();//等待结果返回&#xff0c;不能被中断boolean await(long timeout, TimeUnit unit) throws InterruptedException;boolean await(long timeoutMillis) throws InterruptedException;boolean awaitUninterruptibly(long timeout, TimeUnit unit);boolean awaitUninterruptibly(long timeoutMillis);V getNow();//立刻返回&#xff0c;没有计算完毕&#xff0c;返回null&#xff0c;需要配合isDone()方法判定是不是已经完成&#xff0c;因为runnable没有返回结果&#xff0c;//而callable有返回结果boolean cancel(boolean mayInterruptIfRunning); //取消
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

提到一个监听器GenericFutureListener的封装&#xff0c;一碰到XXXlistener&#xff0c;都会用到监听器模式&#xff1a;

/*** Listens to the result of a {&#64;link Future}. The result of the asynchronous operation is notified once this listener* is added by calling {&#64;link Future#addListener(GenericFutureListener)}.* 监听Future的结果&#xff0c;当一个监听器被注册后&#xff0c;结果的异步操作会被注册的监听器监听。*/
public interface GenericFutureListener<F extends Future> extends EventListener {/*** Invoked when the operation associated with the {&#64;link Future} has been completed.** 和Future的完成计算相关的事件&#xff0c;次方法会被调用。*/void operationComplete(F future) throws Exception;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

监听器对Future的扩展起到了很灵活的作用&#xff0c;当某个计算完毕&#xff0c;会触发相应的时间&#xff0c;得到Future的结果&#xff0c;因为jdk的get方法我们知道什么时候去掉&#xff0c;调早了需要等待&#xff0c;调晚了浪费了一段时间&#xff0c;还有isDone里边有2种情况&#xff0c;无法区分到底是正常的io完毕返回的true还是被取消之后返回的true&#xff0c;所有到了netty的Future里边加了一个isSuccess()方法&#xff0c;只有正常的io处理结束isSuccess()才返回true。

接下来我们会走一下ChannelFuture的源码的doc&#xff1a;


/*** The result of an asynchronous {&#64;link Channel} I/O operation.* Channel的异步io操作的结果。*

* All I/O operations in Netty are asynchronous. It means any I/O calls will* return immediately with no guarantee that the requested I/O operation has* been completed at the end of the call. Instead, you will be returned with* a {&#64;link ChannelFuture} instance which gives you the information about the* result or status of the I/O operation.* netty中所有的i/o都是异步的&#xff0c;意味着很多i/o操作被调用过后会立刻返回&#xff0c;并且不能保证i/o请求操作被调用后计算已经完毕&#xff0c;* 替代它的是返回一个当前i/o操作状态和结果信息的ChannelFuture实例。*

* A {&#64;link ChannelFuture} is either uncompleted or completed.* When an I/O operation begins, a new future object is created. The new future* is uncompleted initially - it is neither succeeded, failed, nor cancelled* because the I/O operation is not finished yet. If the I/O operation is* finished either successfully, with failure, or by cancellation, the future is* marked as completed with more specific information, such as the cause of the* failure. Please note that even failure and cancellation belong to the* completed state.* 一个ChannelFuture要么是完成的&#xff0c;要么是未完成的。当一个i/o操作开始的时候&#xff0c;会创建一个future 对象&#xff0c;future 初始化的时候是为完成的状态&#xff0c;* 既不是是成功的&#xff0c;或者失败的&#xff0c;也不是取消的&#xff0c;因为i/o操作还没有完成&#xff0c;如果一个i/o不管是成功&#xff0c;还是失败&#xff0c;或者被取消&#xff0c;future 会被标记一些特殊* 的信息&#xff0c;比如失败的原因&#xff0c;请注意即使是失败和取消也属于完成状态。*

*                                      &#43;---------------------------&#43;*                                      | Completed successfully    |*                                      &#43;---------------------------&#43;*                                 &#43;---->      isDone() &#61; true      |* &#43;--------------------------&#43;    |    |   isSuccess() &#61; true      |* |        Uncompleted       |    |    &#43;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#43;* &#43;--------------------------&#43;    |    | Completed with failure    |* |      isDone() &#61; false    |    |    &#43;---------------------------&#43;* |   isSuccess() &#61; false    |----&#43;---->      isDone() &#61; true      |* | isCancelled() &#61; false    |    |    |       cause() &#61; non-null  |* |       cause() &#61; null     |    |    &#43;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#43;* &#43;--------------------------&#43;    |    | Completed by cancellation |*                                 |    &#43;---------------------------&#43;*                                 &#43;---->      isDone() &#61; true      |*                                      | isCancelled() &#61; true      |*                                      &#43;---------------------------&#43;* 
** Various methods are provided to let you check if the I/O operation has been* completed, wait for the completion, and retrieve the result of the I/O* operation. It also allows you to add {&#64;link ChannelFutureListener}s so you* can get notified when the I/O operation is completed.* ChannelFuture提供了很多方法让你检查i/o操作是否完成、等待完成、获取i/o操作的结果&#xff0c;他也允许你添加ChannelFutureListener* 因此可以在i/o操作完成的时候被通知。*

Prefer {&#64;link #addListener(GenericFutureListener)} to {&#64;link #await()}

* 建议使用addListener(GenericFutureListener)&#xff0c;而不使用await()* It is recommended to prefer {&#64;link #addListener(GenericFutureListener)} to* {&#64;link #await()} wherever possible to get notified when an I/O operation is* done and to do any follow-up tasks.* 推荐优先使用addListener(GenericFutureListener)&#xff0c;不是await()在可能的情况下&#xff0c;这样就能在i/o操作完成的时候收到通知&#xff0c;并且可以去做 * 后续的任务处理。*

* {&#64;link #addListener(GenericFutureListener)} is non-blocking. It simply adds* the specified {&#64;link ChannelFutureListener} to the {&#64;link ChannelFuture}, and* I/O thread will notify the listeners when the I/O operation associated with* the future is done. {&#64;link ChannelFutureListener} yields the best* performance and resource utilization because it does not block at all, but* it could be tricky to implement a sequential logic if you are not used to* event-driven programming.* addListener(GenericFutureListener)本身是非阻塞的&#xff0c;他会添加一个指定的ChannelFutureListener到ChannelFuture* 并且i/o线程在完成对应的操作将会通知监听器&#xff0c;ChannelFutureListener也会提供最好的性能和资源利用率&#xff0c;因为他永远不会阻塞&#xff0c;但是如果* 不是基于事件编程&#xff0c;他可能在顺序逻辑存在棘手的问题。*

* By contrast, {&#64;link #await()} is a blocking operation. Once called, the* caller thread blocks until the operation is done. It is easier to implement* a sequential logic with {&#64;link #await()}, but the caller thread blocks* unnecessarily until the I/O operation is done and there&#39;s relatively* expensive cost of inter-thread notification. Moreover, there&#39;s a chance of* dead lock in a particular circumstance, which is described below.*相反的&#xff0c;await()是一个阻塞的操作&#xff0c;一旦被调用&#xff0c;调用者线程在操作完成之前是阻塞的&#xff0c;实现顺序的逻辑比较容易&#xff0c;但是他让调用者线程等待是没有必要* 的&#xff0c;会造成资源的消耗&#xff0c;更多可能性会造成死锁&#xff0c;接下来会介绍。*

Do not call {&#64;link #await()} inside {&#64;link ChannelHandler}

* 不要再ChannelHandler里边调用await()方法*

* The event handler methods in {&#64;link ChannelHandler} are usually called by* an I/O thread. If {&#64;link #await()} is called by an event handler* method, which is called by the I/O thread, the I/O operation it is waiting* for might never complete because {&#64;link #await()} can block the I/O* operation it is waiting for, which is a dead lock.* ChannelHandler里边的时间处理器通常会被i/o线程调用&#xff0c;如果await()被一个时间处理方法调用&#xff0c;并且是一个i/o线程&#xff0c;那么这个i/o操作将永远不会 * 完成&#xff0c;因为await()是会阻塞i/o操作&#xff0c;这是一个死锁。*

* // BAD - NEVER DO THIS 不推荐的使用方式* {&#64;code &#64;Override}* public void channelRead({&#64;link ChannelHandlerContext} ctx, Object msg) {*     {&#64;link ChannelFuture} future &#61; ctx.channel().close();*     future.awaitUninterruptibly();//不要使用await的 方式*     // Perform post-closure operation*     // ...* }** // GOOD* {&#64;code &#64;Override} //推荐使用的方式* public void channelRead({&#64;link ChannelHandlerContext} ctx, Object msg) {*     {&#64;link ChannelFuture} future &#61; ctx.channel().close();*     future.addListener(new {&#64;link ChannelFutureListener}() {//使用时间的方式*         public void operationComplete({&#64;link ChannelFuture} future) {*             // Perform post-closure operation*             // ...*         }*     });* }* 
*

* In spite of the disadvantages mentioned above, there are certainly the cases* where it is more convenient to call {&#64;link #await()}. In such a case, please* make sure you do not call {&#64;link #await()} in an I/O thread. Otherwise,* {&#64;link BlockingOperationException} will be raised to prevent a dead lock.* 尽管出现了上面提到的这些缺陷&#xff0c;但是在某些情况下更方便&#xff0c;在这种情况下&#xff0c;请确保不要再i/o线程里边调用await()方法&#xff0c;* 否则会出现BlockingOperationException异常&#xff0c;导致死锁。*

Do not confuse I/O timeout and await timeout

*不要将i/o超时和等待超时混淆。* The timeout value you specify with {&#64;link #await(long)},* {&#64;link #await(long, TimeUnit)}, {&#64;link #awaitUninterruptibly(long)}, or* {&#64;link #awaitUninterruptibly(long, TimeUnit)} are not related with I/O* timeout at all. If an I/O operation times out, the future will be marked as* &#39;completed with failure,&#39; as depicted in the diagram above. For example,* connect timeout should be configured via a transport-specific option:* 使用await(long)、await(long, TimeUnit)、awaitUninterruptibly(long)、awaitUninterruptibly(long, TimeUnit)设置的超时时间* 和i/o超时没有任何关系&#xff0c;如果一个i/o操作超时&#xff0c;future 将被标记为失败的完成状态&#xff0c;比如连接超时通过一些选项来配置&#xff1a;*
* // BAD - NEVER DO THIS //不推荐的方式* {&#64;link Bootstrap} b &#61; ...;* {&#64;link ChannelFuture} f &#61; b.connect(...);* f.awaitUninterruptibly(10, TimeUnit.SECONDS);//不真正确的等待超时* if (f.isCancelled()) {*     // Connection attempt cancelled by user* } else if (!f.isSuccess()) {*     // You might get a NullPointerException here because the future//不能确保future 的完成。*     // might not be completed yet.*     f.cause().printStackTrace();* } else {*     // Connection established successfully* }** // GOOD//推荐的方式* {&#64;link Bootstrap} b &#61; ...;* // Configure the connect timeout option.* b.option({&#64;link ChannelOption}.CONNECT_TIMEOUT_MILLIS, 10000);</b>//配置连接超时* {&#64;link ChannelFuture} f &#61; b.connect(...);* f.awaitUninterruptibly();** // Now we are sure the future is completed.//确保future 一定是完成了。* assert f.isDone();** if (f.isCancelled()) {*     // Connection attempt cancelled by user* } else if (!f.isSuccess()) {*     f.cause().printStackTrace();* } else {*     // Connection established successfully* }* 
*/
public interface ChannelFuture extends Future {/*** Returns a channel where the I/O operation associated with this* future takes place.* 返回和当前future 相关联的i/o操作的channel */Channel channel();ChannelFuture addListener(GenericFutureListenersuper Void>> listener);ChannelFuture addListeners(GenericFutureListenersuper Void>>... listeners);ChannelFuture removeListener(GenericFutureListenersuper Void>> listener);ChannelFuture removeListeners(GenericFutureListenersuper Void>>... listeners);ChannelFuture sync() throws InterruptedException;ChannelFuture syncUninterruptibly();ChannelFuture await() throws InterruptedException;ChannelFuture awaitUninterruptibly();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176

下一接介绍initAndRegister( )方法。



推荐阅读
  • Netty源代码分析服务器端启动ServerBootstrap初始化
    本文主要分析了Netty源代码中服务器端启动的过程,包括ServerBootstrap的初始化和相关参数的设置。通过分析NioEventLoopGroup、NioServerSocketChannel、ChannelOption.SO_BACKLOG等关键组件和选项的作用,深入理解Netty服务器端的启动过程。同时,还介绍了LoggingHandler的作用和使用方法,帮助读者更好地理解Netty源代码。 ... [详细]
  • 纠正网上的错误:自定义一个类叫java.lang.System/String的方法
    本文纠正了网上关于自定义一个类叫java.lang.System/String的错误答案,并详细解释了为什么这种方法是错误的。作者指出,虽然双亲委托机制确实可以阻止自定义的System类被加载,但通过自定义一个特殊的类加载器,可以绕过双亲委托机制,达到自定义System类的目的。作者呼吁读者对网上的内容持怀疑态度,并带着问题来阅读文章。 ... [详细]
  • Python正则表达式学习记录及常用方法
    本文记录了学习Python正则表达式的过程,介绍了re模块的常用方法re.search,并解释了rawstring的作用。正则表达式是一种方便检查字符串匹配模式的工具,通过本文的学习可以掌握Python中使用正则表达式的基本方法。 ... [详细]
  • 本文介绍了使用C++Builder实现获取USB优盘序列号的方法,包括相关的代码和说明。通过该方法,可以获取指定盘符的USB优盘序列号,并将其存放在缓冲中。该方法可以在Windows系统中有效地获取USB优盘序列号,并且适用于C++Builder开发环境。 ... [详细]
  • 系列目录Guava1:概览Guava2:Basicutilities基本工具Guava3:集合CollectionsGuava4:GuavacacheGuava6:Concurre ... [详细]
  • 如何自行分析定位SAP BSP错误
    The“BSPtag”Imentionedintheblogtitlemeansforexamplethetagchtmlb:configCelleratorbelowwhichi ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • ALTERTABLE通过更改、添加、除去列和约束,或者通过启用或禁用约束和触发器来更改表的定义。语法ALTERTABLEtable{[ALTERCOLUMNcolu ... [详细]
  • Fixes/redwoodjs/redwood/#55Thegooglesaidtodothis:https://webpack.js.org/co ... [详细]
author-avatar
过客
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有