Netty源码—一、server启动(1)
说明:netty源码系列是基于4.1.25版本的netty源码的
Netty作为一个Java生态中的网络组件有着举足轻重的位置,各种开源中间件都使用Netty进行网络通信,比如Dubbo、RocketMQ。可以说Netty是对Java NIO的封装,比如ByteBuf、channel等的封装让网络编程更简单。
在介绍Netty服务器启动之前需要简单了解两件事:
- reactor线程模型
- linux中的IO多路复用
reactor线程模型
关于reactor线程模型请参考这篇文章,通过不同的配置Netty可以实现对应的三种reactor线程模型
- reactor单线程模型
- reactor多线程模型
- reactor主从多线程模型
// reactor单线程模型,accept、connect、read、write都在一个线程中执行
EventLoopGroup group = new NioEventLoopGroup(1);
bootStrap.group(group);
// reactor多线程,accept在bossGroup中的一个线程执行,IO操作在workerGroup中的线程执行
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
bootStrap.group(bossGroup , workerGroup);
// reactor主从多线程,用来accept连接的是在一个线程池中执行,这个时候需要bind多个port,因为Netty一个bind的port会启动一个线程来accept
EventLoopGroup bossGroup = new NioEventLoopGroup(2);
EventLoopGroup workerGroup = new NioEventLoopGroup();
bootStrap.group(bossGroup , workerGroup);
注意:本文后面的介绍如无特别说明都是基于reactor多线程模型
linux中的IO多路复用
linux中的网络编程模型也是在不断演变的,下面是依次演变的顺序(具体可参考《UNIX网络编程卷1:套接字联网API》第三版的第4、6章)
accept
阻塞等待连接,接收到新的连接后新起线程来处理接收到的连接,然后在新的线程中阻塞等待新的数据到来
select
根据入参的不同有三种情况
- 永远等下去,直到监听的描述符有任意的IO事件才返回
- 等待一段固定时间,如果时间到之前有IO事件则提前返回,否则等待超时后返回
- 不等待,检查描述符后立即返回,称为轮询
select会返回就绪的文件描述符的个数,需要轮询所有socket,判断每个socket的状态来确定是否有事件、是什么事件
poll
相比较于selectpoll是阻塞等待的,只有有读写事件的时候才会返回,返回的是有读写事件的socket个数,并且将对应的socket的事件置位,自己从所有socket中找到具体的socket
epoll
相比较于poll,epoll可以将只有确实有IO事件的描述符返回,大并发下只有少量活跃连接的情况下使用
较poll的优势
- 不用开发者重新准备文件描述符集合(较poll入参简单)
- 无需遍历所有监听的描述符,只要遍历哪些被内核IO事件异步唤醒而加入ready队列的描述符集合
Java NIO在linux的实现就是基于epoll的。epoll的编程模型:
- 创建socket,socket方法
- 绑定服务器ip,port,bind方法
- 监听绑定了ip:port的文件描述符,listen方法
- 创建epoll句柄(文件描述符),配置最大监听的文件描述符个数,epoll_create方法
- 配置epoll监听的文件描述符的事件:注册、修改、删除某个文件描述符对应的事件
- 监听所有已配置的描述符,epoll_wait
- 有新的事件的时候遍历返回的描述符,处理对应的事件
- 如果是来自客户端的连接,则将accept到的文件描述符注册到epoll中
- 如果是读写事件则分别处理
注意:Netty封装的Java NIO是跨平台的,后面还是以linux平台为例来介绍
接下来言归正传,来看看Netty的服务器启动过程做了什么事情。Netty作为一个网络框架,和普通网络编程做的事情基本上一样,对应于上面epoll的编程模型,Netty的启动过程为
- 初始化线程池,初始化selector
- 初始化NioServerSocketChannel
- 绑定服务器ip:port
- 将NioServerSocketChannel注册到selector中
- 配置NioServerSocketChannel监听的事件
- 使用selector.select等待新的IO事件
- 如果是来自客户端的连接则将NioSocketChannel注册到selector上(如果是新的线程则是新的selector)
- 如果是普通IO事件则在worker线程中处理
线程池初始化
在介绍NioEventLoopGroup之前先看下NioEventLoop
可以看到NioEventLoop继承自SingleThreadEventExecutor,是一个单线程的executor,在线程中死循环监听IO事件。主要方法有
// 初始化selector
io.netty.channel.nio.NioEventLoop#openSelector
// 将channel注册到selector
io.netty.channel.nio.NioEventLoop#register
// 监听selector上的事件
io.netty.channel.nio.NioEventLoop#select
一个NioEventLoop会初始化一个selector,处理selector上注册的channel。
NioEventLoopGroup从名字上就可以看出来是由多个NioEventLoop组成,类关系图如下
NioEventLoopGroup的重要属性为:
// 包含的EventExecutor数组
private final EventExecutor[] children;
// 选择哪一个EventExecutor执行task的选择器,不同的选择器有不同的策略
private final EventExecutorChooserFactory.EventExecutorChooser chooser;
重要方法有:
// 选择下一个执行任务的线程
io.netty.util.concurrent.MultithreadEventExecutorGroup#next
// 创建EventLoop
io.netty.channel.nio.NioEventLoopGroup#newChild
// 在线程池中执行注册channel的任务
io.netty.channel.MultithreadEventLoopGroup#register(io.netty.channel.Channel)
// 创建默认的threadFactory
io.netty.channel.MultithreadEventLoopGroup#newDefaultThreadFactory
线程池初始化的代码为
EventLoopGroup workerGroup = new NioEventLoopGroup();
如果使用无参的构造方法的话,最后会执行下面这个构造方法,这里面做要做了以下几件事
- 如果executor没有初始化,使用默认的executor初始化
- 初始化线程池中每个EventLoop
- 如果其中一个初始化过程中抛出异常,关闭所有的NioEventLoop
protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
if (nThreads <= 0) {
throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
}
if (executor == null) {
executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
}
children = new EventExecutor[nThreads];
for (int i = 0; i < nThreads; i ++) {
boolean success = false;
try {
// 创建EventLoop
children[i] = newChild(executor, args);
success = true;
} catch (Exception e) {
// TODO: Think about if this is a good exception type
throw new IllegalStateException("failed to create a child event loop", e);
} finally {
if (!success) {
for (int j = 0; j < i; j ++) {
children[j].shutdownGracefully();
}
for (int j = 0; j < i; j ++) {
EventExecutor e = children[j];
try {
while (!e.isTerminated()) {
e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
}
} catch (InterruptedException interrupted) {
// Let the caller handle the interruption.
Thread.currentThread().interrupt();
break;
}
}
}
}
}
// 初始化chooser,决定选择下一个线程的策略
chooser = chooserFactory.newChooser(children);
final FutureListener<Object> terminationListener = new FutureListener<Object>() {
@Override
public void operationComplete(Future<Object> future) throws Exception {
if (terminatedChildren.incrementAndGet() == children.length) {
terminationFuture.setSuccess(null);
}
}
};
for (EventExecutor e: children) {
e.terminationFuture().addListener(terminationListener);
}
Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
Collections.addAll(childrenSet, children);
readonlyChildren = Collections.unmodifiableSet(childrenSet);
}
使用默认参数构造参数的话,上面这个构造方法的入参的值分别是
nThreads
// 默认的线程池大小
private static final int DEFAULT_EVENT_LOOP_THREADS;
static {
// 如果配置了io.netty.eventLoopThreads参数的话,先取该参数的值
// 如果没有配置上面的参数,则取机器处理器个数的2倍
// 如果上面算出的结果小于1则取1
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
"io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2));
if (logger.isDebugEnabled()) {
logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
}
}
// 默认没有指定线程池大小,取DEFAULT_EVENT_LOOP_THREADS
protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}
executor
默认没有指定executor,为null
chooserFactory
protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) {
this(nThreads, executor, DefaultEventExecutorChooserFactory.INSTANCE, args);
}
// io.netty.util.concurrent.DefaultEventExecutorChooserFactory
使用默认的chooser,该类的主要功能是提供选择下一个线程的策略
public final class DefaultEventExecutorChooserFactory implements EventExecutorChooserFactory {
// 单例
public static final DefaultEventExecutorChooserFactory INSTANCE = new DefaultEventExecutorChooserFactory();
private DefaultEventExecutorChooserFactory() { }
@SuppressWarnings("unchecked")
@Override
public EventExecutorChooser newChooser(EventExecutor[] executors) {
if (isPowerOfTwo(executors.length)) {
// 如果是2的幂次则使用这个chooser
return new PowerOfTowEventExecutorChooser(executors);
} else {
return new GenericEventExecutorChooser(executors);
}
}
private static boolean isPowerOfTwo(int val) {
// 判断一个数是否2的幂,方法很巧妙
return (val & -val) == val;
}
private static final class PowerOfTowEventExecutorChooser implements EventExecutorChooser {
private final AtomicInteger idx = new AtomicInteger();
private final EventExecutor[] executors;
PowerOfTowEventExecutorChooser(EventExecutor[] executors) {
this.executors = executors;
}
@Override
public EventExecutor next() {
// 如果是2的幂次个线程,可以使用位运算计算出下一个选出的线程的index
return executors[idx.getAndIncrement() & executors.length - 1];
}
}
private static final class GenericEventExecutorChooser implements EventExecutorChooser {
private final AtomicInteger idx = new AtomicInteger();
private final EventExecutor[] executors;
GenericEventExecutorChooser(EventExecutor[] executors) {
this.executors = executors;
}
@Override
public EventExecutor next() {
// 使用求余的方法计算出下一个线程的index
return executors[Math.abs(idx.getAndIncrement() % executors.length)];
}
}
}
可以看出上面两个chooser计算出的最终结果是一致的,但是使用位运算更快一点,所以如果是线程池的大小刚好是2的幂次的话使用位运算的chooser。
args
// args[0],下面方法返回的provider,在linux平台上默认是EPollSelectorProvider
java.nio.channels.spi.SelectorProvider#provider
// args[1],决定eventLoop每次执行select还是执行队列中的任务
io.netty.channel.DefaultSelectStrategyFactory
// args[2],等待队列满以后的拒绝策略
io.netty.util.concurrent.RejectedExecutionHandlers#REJECT
初始化NioEventLoopGroup过程主要是为了初始化线程池中每一个NioEventLoop,而每一个NioEventLoop包含一个selector。
初始化selector
接着上面说到的初始化NioEventLoop,调用newChild方法来初始化
// io.netty.channel.nio.NioEventLoopGroup#newChild
protected EventLoop newChild(Executor executor, Object... args) throws Exception {
// 下面这几个参数上面已经介绍过
return new NioEventLoop(this, executor, (SelectorProvider) args[0],
((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
}
NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
// 调用父类构造方法初始化taskQueue,taskQueue的大小取Math.max(16, maxPendingTasks)
super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
// 校验selectorProvider
if (selectorProvider == null) {
throw new NullPointerException("selectorProvider");
}
// 校验EventLoop每次执行的select策略是否为空
if (strategy == null) {
throw new NullPointerException("selectStrategy");
}
provider = selectorProvider;
// 初始化selector
selector = openSelector();
selectStrategy = strategy;
}
private Selector openSelector() {
final Selector selector;
try {
// 调用的是sun.nio.ch.EPollSelectorProvider#openSelector
// 返回的是sun.nio.ch.EPollSelectorImpl
selector = provider.openSelector();
} catch (IOException e) {
throw new ChannelException("failed to open a new selector", e);
}
// 是否使用SelectedSelectionKeySet优化,默认不禁用false
if (DISABLE_KEYSET_OPTIMIZATION) {
return selector;
}
// Netty优化过后的
final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
// 尝试获取SelectorImpl对象,后续会使用反射操作这个类的属性
Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
try {
return Class.forName(
"sun.nio.ch.SelectorImpl",
false,
PlatformDependent.getSystemClassLoader());
} catch (ClassNotFoundException e) {
return e;
} catch (SecurityException e) {
return e;
}
}
});
// 确保有权限访问该类
if (!(maybeSelectorImplClass instanceof Class) ||
// ensure the current selector implementation is what we can instrument.
!((Class<?>) maybeSelectorImplClass).isAssignableFrom(selector.getClass())) {
if (maybeSelectorImplClass instanceof Exception) {
Exception e = (Exception) maybeSelectorImplClass;
logger.trace("failed to instrument a special java.util.Set into: {}", selector, e);
}
return selector;
}
final Class<?> selectorImplClass = (Class<?>) maybeSelectorImplClass;
Object maybeException = AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
try {
// 得到字段selectedKeys
Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
// 得到字段publicSelectedKeys
Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
selectedKeysField.setAccessible(true);
publicSelectedKeysField.setAccessible(true);
// 将selectedKeys、publicSelectedKeys均设置为Netty自定义的SelectedSelectionKeySet
selectedKeysField.set(selector, selectedKeySet);
publicSelectedKeysField.set(selector, selectedKeySet);
return null;
} catch (NoSuchFieldException e) {
return e;
} catch (IllegalAccessException e) {
return e;
} catch (RuntimeException e) {
// JDK 9 can throw an inaccessible object exception here; since Netty compiles
// against JDK 7 and this exception was only added in JDK 9, we have to weakly
// check the type
if ("java.lang.reflect.InaccessibleObjectException".equals(e.getClass().getName())) {
return e;
} else {
throw e;
}
}
}
});
if (maybeException instanceof Exception) {
selectedKeys = null;
Exception e = (Exception) maybeException;
logger.trace("failed to instrument a special java.util.Set into: {}", selector, e);
} else {
selectedKeys = selectedKeySet;
logger.trace("instrumented a special java.util.Set into: {}", selector);
}
return selector;
}
初始化selector的过程中主要做了几件事:
- 使用平台相关的provider初始化对应的SelectorImpl,这里使用了Java的SPI来加载平台相关的provider,每一种provider又对应一种SelectorImpl
- 如果没有禁用selectedKey优化,Netty会使用自定的SelectedSelectionKeySet替换SelectorImpl的publicSelectedKeys、selectedKeys
对SelectorImpl.selectedKey优化的说明
- 利用反射将SelectorImpl.selectedKey替换成了SelectedSelectionKeySet,SelectedSelectionKeySet利用数组实现元素存放
- 在调用select方法的时候如果有事件进来的时候会调用SelectedSelectionKeySet#add,将有IO事件的selectKey添加到keyset中
- 使用数组遍历(processSelectedKeysOptimized)要比使用set遍历快一些,参考文后第一篇参考文章
- 在Java9以后这个优化就失效了,因为Java9引入了Jigsaw
接下来看看Selector创建过程,上面调用了EPollSelectorProvider#openSelector来开始初始化selector
public AbstractSelector openSelector() throws IOException {
// 直接new 一个EPollSelectorImpl
return new EPollSelectorImpl(this);
}
// 该构造方法只能是包内使用,供provider来调用
EPollSelectorImpl(SelectorProvider sp) throws IOException {
// 调用父类SelectorImpl的构造方法初始化selectedKeys、publicKeys、publicSelectedKeys
// 上面已经说过了,如果使用Netty的优化,publicKeys、publicSelectedKey会被替换
super(sp);
// 调用linux的pipe方法,创建一个管道,配置为非阻塞的
long pipeFds = IOUtil.makePipe(false);
// 高32为读文件描述符
fd0 = (int) (pipeFds >>> 32);
// 低32位为写文件描述符
fd1 = (int) pipeFds;
// EPollArrayWrapper包含一系列native方法来调用EPollArrayWrapper.c本地方法
pollWrapper = new EPollArrayWrapper();
pollWrapper.initInterrupt(fd0, fd1);
// fdToKey用来保存文件描述符和SelectionKeyImpl的映射
fdToKey = new HashMap<>();
}
EPollArrayWrapper() throws IOException {
// creates the epoll file descriptor
// 创建epoll的文件描述符
epfd = epollCreate();
// the epoll_event array passed to epoll_wait
int allocationSize = NUM_EPOLLEVENTS * SIZE_EPOLLEVENT;
pollArray = new AllocatedNativeObject(allocationSize, true);
pollArrayAddress = pollArray.address();
// eventHigh needed when using file descriptors > 64k
if (OPEN_MAX > MAX_UPDATE_ARRAY_SIZE)
eventsHigh = new HashMap<>();
}
终于看到创建epoll文件描述符相关代码了,上面这个还是看不到究竟调用了哪些本地方法,我们看看相关的c代码
// jdk/src/solaris/native/sun/nio/ch/IOUtil.c
JNIEXPORT jlong JNICALL
Java_sun_nio_ch_IOUtil_makePipe(JNIEnv *env, jobject this, jboolean blocking)
{
int fd[2];
// 打开pipe
if (pipe(fd) < 0) {
JNU_ThrowIOExceptionWithLastError(env, "Pipe failed");
return 0;
}
if (blocking == JNI_FALSE) {
// 配置管道为非阻塞
if ((configureBlocking(fd[0], JNI_FALSE) < 0)
|| (configureBlocking(fd[1], JNI_FALSE) < 0)) {
JNU_ThrowIOExceptionWithLastError(env, "Configure blocking failed");
close(fd[0]);
close(fd[1]);
return 0;
}
}
// 将读写文件描述符放入一个long型中返回
return ((jlong) fd[0] << 32) | (jlong) fd[1];
}
// jdk/src/solaris/native/sun/nio/ch/EPollArrayWrapper.c
JNIEXPORT jint JNICALL
Java_sun_nio_ch_EPollArrayWrapper_epollCreate(JNIEnv *env, jobject this)
{
/*
* epoll_create expects a size as a hint to the kernel about how to
* dimension internal structures. We can't predict the size in advance.
*/
// 这里调用linux函数epoll_create创建epoll的文件描述符
int epfd = epoll_create(256);
if (epfd < 0) {
JNU_ThrowIOExceptionWithLastError(env, "epoll_create failed");
}
return epfd;
}
总结
经过上面说明,现在对于Netty启动过程中线程池的初始化过程和selector初始化过程已经比较清晰了,对于native方法的分析让我们对比linux中epoll编程,对于原理更加清楚。
接下来就是将需要监听的描述符注册到epoll上,对应到Netty就是讲channel注册到selector上,下一篇文章继续写Netty源码—二、server启动(2)
参考
Netty源码分析——Reactor的processSelectedKeys
关于SelectedSelectionKeySet优化的讨论
https://github.com/netty/netty/issues/2363
https://github.com/netty/netty/commit/cd579f75d2b5f236f35bc47f454cc07e50ae8037
Netty源码—一、server启动(1)的更多相关文章
- Netty源码解析—客户端启动
Netty源码解析-客户端启动 Bootstrap示例 public final class EchoClient { static final boolean SSL = System.getPro ...
- Netty源码阅读(一) ServerBootstrap启动
Netty源码阅读(一) ServerBootstrap启动 转自我的Github Netty是由JBOSS提供的一个java开源框架.Netty提供异步的.事件驱动的网络应用程序框架和工具,用以快速 ...
- Netty 源码(一)服务端启动
Netty 源码(一)服务端启动 Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html) ServerBootstap 创建时序图如 ...
- Netty源码分析第1章(Netty启动流程)---->第1节: 服务端初始化
Netty源码分析第一章: Server启动流程 概述: 本章主要讲解server启动的关键步骤, 读者只需要了解server启动的大概逻辑, 知道关键的步骤在哪个类执行即可, 并不需要了解每一步的 ...
- Netty源码分析第1章(Netty启动流程)---->第2节: NioServerSocketChannel的创建
Netty源码分析第一章: Server启动流程 第二节:NioServerSocketChannel的创建 我们如果熟悉Nio, 则对channel的概念则不会陌生, channel在相当于一个通 ...
- Netty源码分析第1章(Netty启动流程)---->第5节: 绑定端口
Netty源码分析第一章:Netty启动步骤 第五节:绑定端口 上一小节我们学习了channel注册在selector的步骤, 仅仅做了注册但并没有监听事件, 事件是如何监听的呢? 我们继续跟第一小节 ...
- Netty源码分析第2章(NioEventLoop)---->第4节: NioEventLoop线程的启动
Netty源码分析第二章: NioEventLoop 第四节: NioEventLoop线程的启动 之前的小节我们学习了NioEventLoop的创建以及线程分配器的初始化, 那么NioEvent ...
- Netty源码分析 (三)----- 服务端启动源码分析
本文接着前两篇文章来讲,主要讲服务端类剩下的部分,我们还是来先看看服务端的代码 /** * Created by chenhao on 2019/9/4. */ public final class ...
- Netty源码剖析-启动服务
参考文献:极客时间傅健老师的<Netty源码剖析与实战>Talk is cheap.show me the code! --1主线分两步: 一:首先在our thread里,如果写在mai ...
随机推荐
- 解锁 vmware esxi 6.7 并安装 mac os 10.13
1.安装 esxi 6.7 2.下载 unlocker 2.1.1.zip 3.上传 unlocker 2.1.1.zip esxi的磁盘中 4.开启esxi的ssh登录 5.使用 ssh 登录 es ...
- bzoj 2428 均分数据 模拟退火
模拟退火 按照自己的思路打了,结果WA,发现退火最关键的就是初温,降温,和修改次数, 这个题还在外层带了一个循环,骚气 #include<cstdio> #include<iostr ...
- 【dfs+连通分量】Bzoj1123 POI2008 BLO
Description Byteotia城市有n个 towns m条双向roads. 每条 road 连接 两个不同的 towns ,没有重复的road. 所有towns连通. Input 输入n&l ...
- 在线数据库表(sql语句)生成java实体类工具
相信每个做java开发的读者,都接触过SQL建表语句,尤其是在项目开发初期,因为数据库是项目的基石. 在现代项目开发中,出现了许多ORM框架,通过简单的实体映射,即可实现与数据库的交互,然而我们最初设 ...
- ionic3 懒加载在微信上缓存的问题
1.懒加载是什么? 在ionic2中所有的组件.模块.服务.管道等都堆积在app.module.ts模块中,在页面初始化的时候会一次性加载所有的资源,导致资源过大,页面渲染缓慢,也导致app.modu ...
- 【爆料】-《西悉尼大学毕业证书》UWS一模一样原件
☞西悉尼大学毕业证书[微/Q:865121257◆WeChat:CC6669834]UC毕业证书/联系人Alice[查看点击百度快照查看][留信网学历认证&博士&硕士&海归&a ...
- Hadoop配置第1节-集群网络配置
Hadoop-集群网络配置 总体目标:完成zookeeper+Hadoop+Hbase 整合平台搭建 进度:1:集群网络属性配置2:集群免密码登陆配置3:JDK的安装4:Zookeeper的安装5 ...
- java泛型中使用的排序算法——归并排序及分析
一.引言 我们知道,java中泛型排序使用归并排序或TimSort.归并排序以O(NlogN)最坏时间运行,下面我们分析归并排序过程及分析证明时间复杂度:也会简述为什么java选择归并排序作为泛型的排 ...
- Promise原理—一步一步实现一个Promise
promise特点 一个promise的当前状态只能是pending.fulfilled和rejected三种之一.状态改变只能是pending到fulfilled或者pending到rejected ...
- socketserver实现并发
socketserver实现并发原理:给每一个前来链接的客户端开启一个线程执行通信.也就是给每一个连接“配备”了一个管家. 下面用一个简单的示例来演示socketserver实现并发(一个服务端,两个 ...