客戶端的流程和服務端的大致相同,在服務端分析過的這裏不再分析,看這篇文章前可以先看下服務端源碼分析的文章。先大致看下客戶端的代碼。
public void start() throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();/*線程組*/
try {
Bootstrap b = new Bootstrap();/*客戶端啓動必備*/
b.group(group)
.channel(NioSocketChannel.class)/*指明使用NIO進行網絡通訊*/
.remoteAddress(new InetSocketAddress(host,port))/*配置遠程服務器的地址*/
.handler(new EchoClientHandler());
ChannelFuture f = b.connect().sync();/*連接到遠程節點,阻塞等待直到連接完成*/
/*阻塞,直到channel關閉*/
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
調用Bootstrap的connect方法之前的代碼,在之前服務端源碼分析的文章中已經介紹了,這裏不再重複,這裏直接從調用connect方法開始分析。
1. connect
public ChannelFuture connect() {
//參數校驗
validate();
SocketAddress remoteAddress = this.remoteAddress;
if (remoteAddress == null) {
throw new IllegalStateException("remoteAddress not set");
}
return doResolveAndConnect(remoteAddress, config.localAddress());
}
這裏核心在doResolveAndConnect方法。
private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();
if (regFuture.isDone()) {
if (!regFuture.isSuccess()) {
return regFuture;
}
return doResolveAndConnect0(channel, remoteAddress, localAddress, channel.newPromise());
} else {
// Registration future is almost always fulfilled already, but just in case it's not.
final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
regFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
// Directly obtain the cause and do a null check so we only need one volatile read in case of a
// failure.
Throwable cause = future.cause();
if (cause != null) {
// Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
// IllegalStateException once we try to access the EventLoop of the Channel.
promise.setFailure(cause);
} else {
// Registration was successful, so set the correct executor to use.
// See https://github.com/netty/netty/issues/2586
promise.registered();
doResolveAndConnect0(channel, remoteAddress, localAddress, promise);
}
}
});
return promise;
}
}
1.1 initAndRegister
final ChannelFuture initAndRegister() {
Channel channel = null;
try {
//服務端不同的是,客戶端這裏創建的是NioSocketChannel
channel = channelFactory.newChannel();
init(channel);
} catch (Throwable t) {
if (channel != null) {
// channel can be null if newChannel crashed (eg SocketException("too many open files"))
channel.unsafe().closeForcibly();
// as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
}
// as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t);
}
ChannelFuture regFuture = config().group().register(channel);
if (regFuture.cause() != null) {
if (channel.isRegistered()) {
channel.close();
} else {
channel.unsafe().closeForcibly();
}
}
return regFuture;
}
這個方法和服務器啓動的時候調用的是同一個方法,不過在細節上有所不同。
1.1.1 NioSocketChannel
服務端在這裏創建的是NioServerSocketChannel,客戶端這裏創建的是NioSocketChannel。看下構造方法。
public NioSocketChannel() {
//DEFAULT_SELECTOR_PROVIDER是JDK原生的SelectorProvider
this(DEFAULT_SELECTOR_PROVIDER);
}
public NioSocketChannel(SelectorProvider provider) {
this(newSocket(provider));
}
newSocket利用SelectorProvider創建JDK的SocketChannel對象。
public NioSocketChannel(Channel parent, SocketChannel socket) {
//這裏的parent是null
super(parent, socket);
config = new NioSocketChannelConfig(this, socket.socket());
}
super調用父類AbstractNioByteChannel的構造方法
protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) {
super(parent, ch, SelectionKey.OP_READ);
}
繼續調用父類AbstractNioChannel的構造方法
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
//父類構造方法創建了NioByteUnsafe對象和該channel的Pipeline
super(parent);
this.ch = ch;
//這裏的感興趣事件是read事件
this.readInterestOp = readInterestOp;
try {
//設置通道非阻塞
ch.configureBlocking(false);
} catch (IOException e) {
try {
ch.close();
} catch (IOException e2) {
if (logger.isWarnEnabled()) {
logger.warn(
"Failed to close a partially initialized socket.", e2);
}
}
throw new ChannelException("Failed to enter non-blocking mode.", e);
}
}
1.1.2 init
這裏調用的是Bootstrap的init方法。
void init(Channel channel) throws Exception {
ChannelPipeline p = channel.pipeline();
//將配置的handler加入Pipeline
p.addLast(config.handler());
//設置屬性
final Map<ChannelOption<?>, Object> options = options0();
synchronized (options) {
setChannelOptions(channel, options, logger);
}
final Map<AttributeKey<?>, Object> attrs = attrs0();
synchronized (attrs) {
for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
}
}
}
1.1.3 register
register方法和服務端一樣調用的是MultithreadEventLoopGroup的register方法。
public ChannelFuture register(Channel channel) {
return next().register(channel);
}
next().register調用的是SingleThreadEventLoop的register方法。
public ChannelFuture register(Channel channel) {
return register(new DefaultChannelPromise(channel, this));
}
public ChannelFuture register(final ChannelPromise promise) {
ObjectUtil.checkNotNull(promise, "promise");
promise.channel().unsafe().register(this, promise);
return promise;
}
這裏將繼續調用AbstractChannel內部類AbstractUnsafe的register方法。
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
...//省略代碼
AbstractChannel.this.eventLoop = eventLoop;
if (eventLoop.inEventLoop()) {
register0(promise);
} else {
try {
eventLoop.execute(new Runnable() {
@Override
public void run() {
register0(promise);
}
});
} catch (Throwable t) {
...//省略代碼
}
}
}
這個方法之前講過,就是分配一個eventLoop給channel,創建一個線程和eventLoop綁定,並且執行register0方法。
1.1.4 register0
private void register0(ChannelPromise promise) {
try {
...//省略代碼
boolean firstRegistration = neverRegistered;
doRegister();
neverRegistered = false;
registered = true;
...//省略代碼
pipeline.invokeHandlerAddedIfNeeded();
safeSetSuccess(promise);
pipeline.fireChannelRegistered();
...//省略代碼
if (isActive()) {
if (firstRegistration) {
pipeline.fireChannelActive();
} else if (config().isAutoRead()) {
...//省略代碼
beginRead();
}
}
} catch (Throwable t) {
...//省略代碼
}
}
這個方法在之前也講過,不分析了。回到doResolveAndConnect方法繼續看接下來執行的doResolveAndConnect0方法。
1.2 doResolveAndConnect0
private ChannelFuture doResolveAndConnect0(final Channel channel, SocketAddress remoteAddress,
final SocketAddress localAddress, final ChannelPromise promise) {
try {
final EventLoop eventLoop = channel.eventLoop();
final AddressResolver<SocketAddress> resolver = this.resolver.getResolver(eventLoop);
if (!resolver.isSupported(remoteAddress) || resolver.isResolved(remoteAddress)) {
//執行這裏的方法
doConnect(remoteAddress, localAddress, promise);
return promise;
}
final Future<SocketAddress> resolveFuture = resolver.resolve(remoteAddress);
if (resolveFuture.isDone()) {
final Throwable resolveFailureCause = resolveFuture.cause();
if (resolveFailureCause != null) {
// Failed to resolve immediately
channel.close();
promise.setFailure(resolveFailureCause);
} else {
// Succeeded to resolve immediately; cached? (or did a blocking lookup)
doConnect(resolveFuture.getNow(), localAddress, promise);
}
return promise;
}
// Wait until the name resolution is finished.
resolveFuture.addListener(new FutureListener<SocketAddress>() {
@Override
public void operationComplete(Future<SocketAddress> future) throws Exception {
if (future.cause() != null) {
channel.close();
promise.setFailure(future.cause());
} else {
doConnect(future.getNow(), localAddress, promise);
}
}
});
} catch (Throwable cause) {
promise.tryFailure(cause);
}
return promise;
}
這裏將執行doConnect方法。
(1)doConnect
private static void doConnect(
final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise connectPromise) {
// This method is invoked before channelRegistered() is triggered. Give user handlers a chance to set up
// the pipeline in its channelRegistered() implementation.
final Channel channel = connectPromise.channel();
channel.eventLoop().execute(new Runnable() {
@Override
public void run() {
if (localAddress == null) {
channel.connect(remoteAddress, connectPromise);
} else {
channel.connect(remoteAddress, localAddress, connectPromise);
}
connectPromise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
}
});
}
這裏的channel.connect將繼續調用AbstractChannel的connect方法。
public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
return pipeline.connect(remoteAddress, promise);
}
(2)connect
public final ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
return tail.connect(remoteAddress, promise);
}
connect是個出站事件,從TailContext開始出傳播。我們現在這裏有三個handler。HeadContext、EchoClientHandler(我們自定義的)、TailContext。
TailContext的connect方法是繼承其父類AbstractChannelHandlerContext的方法。
public ChannelFuture connect(
final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
if (remoteAddress == null) {
throw new NullPointerException("remoteAddress");
}
if (isNotValidPromise(promise, false)) {
// cancelled
return promise;
}
//找到下一個出站事件去執行
final AbstractChannelHandlerContext next = findContextOutbound();
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeConnect(remoteAddress, localAddress, promise);
} else {
safeExecute(executor, new Runnable() {
@Override
public void run() {
next.invokeConnect(remoteAddress, localAddress, promise);
}
}, promise, null);
}
return promise;
}
這裏能處理出站事件的只有HeadContext。看下它的connect方法。
public void connect(
ChannelHandlerContext ctx,
SocketAddress remoteAddress, SocketAddress localAddress,
ChannelPromise promise) throws Exception {
unsafe.connect(remoteAddress, localAddress, promise);
}
這裏將繼續調用AbstractNioChannel內部類AbstractNioUnsafe的connect方法。
(3)AbstractNioUnsafe的connect
public final void connect(
final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
if (!promise.setUncancellable() || !ensureOpen(promise)) {
return;
}
try {
if (connectPromise != null) {
// Already a connect in process.
throw new ConnectionPendingException();
}
boolean wasActive = isActive();
if (doConnect(remoteAddress, localAddress)) {
fulfillConnectPromise(promise, wasActive);
} else {
...//省略代碼}
} catch (Throwable t) {
promise.tryFailure(annotateConnectException(t, remoteAddress));
closeIfClosed();
}
}
看下doConnect方法。
(4)doConnect
protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
//如果本地地址不爲空,綁定本地地址,我們這裏是空的
if (localAddress != null) {
doBind0(localAddress);
}
boolean success = false;
try {
//調用JDK原生API連接遠程主機
boolean connected = SocketUtils.connect(javaChannel(), remoteAddress);
if (!connected) {
//如果連接不成功,註冊CONNECT事件
selectionKey().interestOps(SelectionKey.OP_CONNECT);
}
success = true;
return connected;
} finally {
if (!success) {
doClose();
}
}
}
到這裏客戶端啓動完畢。
如果這裏註冊了CONNECT事件,後面連接成功,觸發了CONNECT事件客戶端怎麼處理的。
2. connect事件
我們在上一篇文章中知道所有的處理事件都是由NioEventLoop的processSelectedKey方法處理。
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
if (!k.isValid()) {
final EventLoop eventLoop;
try {
eventLoop = ch.eventLoop();
} catch (Throwable ignored) {
...//省略代碼
return;
}
...//省略代碼
unsafe.close(unsafe.voidPromise());
return;
}
try {
int readyOps = k.readyOps();
...//省略代碼
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
//取消註冊connect事件
k.interestOps(ops);
unsafe.finishConnect();
}
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
ch.unsafe().forceFlush();
}
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
這裏首先取消了註冊的connect事件,然後調用了AbstractNioUnsafe的finishConnect方法。
(1)finishConnect
public final void finishConnect() {
assert eventLoop().inEventLoop();
try {
boolean wasActive = isActive();
doFinishConnect();
fulfillConnectPromise(connectPromise, wasActive);
} catch (Throwable t) {
fulfillConnectPromise(connectPromise, annotateConnectException(t, requestedRemoteAddress));
} finally {
// Check for null as the connectTimeoutFuture is only created if a connectTimeoutMillis > 0 is used
// See https://github.com/netty/netty/issues/1770
if (connectTimeoutFuture != null) {
connectTimeoutFuture.cancel(false);
}
connectPromise = null;
}
}
(2)doFinishConnect
protected void doFinishConnect() throws Exception {
if (!javaChannel().finishConnect()) {
throw new Error();
}
}
這裏只是做了下檢查
(3)fulfillConnectPromise
private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) {
if (promise == null) {
// Closed via cancellation and the promise has been notified already.
return;
}
boolean active = isActive();
// trySuccess() will return false if a user cancelled the connection attempt.
boolean promiseSet = promise.trySuccess();
if (!wasActive && active) {
pipeline().fireChannelActive();
}
// If a user cancelled the connection attempt, close the channel, which is followed by channelInactive().
if (!promiseSet) {
close(voidPromise());
}
}
這裏最終調用了pipeline的fireChannelActive方法,這個方法在前一篇文章分析過,這裏不分析了,最終它會在channel上註冊read事件。
看到這裏服務端和客戶端的啓動源碼已經分析完了,accept、connect、read事件也分析過了,到這裏是不是有點疑惑,還有個write事件在哪。
3. write
3.1 ChannelHandlerContext的writeAndFlush
我們在netty中寫數據比較常用的是調用和handler綁定的ChannelHandlerContext的writeAndFlush方法。
這裏調用的是AbstractChannelHandlerContext的writeAndFlush方法。
public ChannelFuture writeAndFlush(Object msg) {
return writeAndFlush(msg, newPromise());
}
public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
…//省略代碼
write(msg, true, promise);
return promise;
}
(1)write
private void write(Object msg, boolean flush, ChannelPromise promise) {
//這裏找能處理出站事件的handler,這裏是HeadContext
AbstractChannelHandlerContext next = findContextOutbound();
final Object m = pipeline.touch(msg, next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
//判斷是否需要flush
if (flush) {
next.invokeWriteAndFlush(m, promise);
} else {
next.invokeWrite(m, promise);
}
} else {
AbstractWriteTask task;
if (flush) {
task = WriteAndFlushTask.newInstance(next, m, promise);
} else {
task = WriteTask.newInstance(next, m, promise);
}
safeExecute(executor, task, promise, m);
}
}
這裏先找下一個能處理出站事件的handler,這裏能處理出站事件的只有HeadContext。
(2) invokeWriteAndFlush
這裏會判斷是否需要flush,如果需要調用HeadContext的invokeWriteAndFlush,否則調用invokeWrite。
private void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
if (invokeHandler()) {
invokeWrite0(msg, promise);
invokeFlush0();
} else {
writeAndFlush(msg, promise);
}
}
private void invokeWrite(Object msg, ChannelPromise promise) {
if (invokeHandler()) {
invokeWrite0(msg, promise);
} else {
write(msg, promise);
}
}
這裏可以看出來flush的方法只比不需要多了invokeFlush0方法。
(3)invokeWrite0
private void invokeWrite0(Object msg, ChannelPromise promise) {
try {
((ChannelOutboundHandler) handler()).write(this, msg, promise);
} catch (Throwable t) {
notifyOutboundHandlerException(t, promise);
}
這裏調用的是HeadContext的write方法。
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
unsafe.write(msg, promise);
}
接着調用AbstractUnsafe的write方法。
(4)write
public final void write(Object msg, ChannelPromise promise) {
assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) {
safeSetFailure(promise, WRITE_CLOSED_CHANNEL_EXCEPTION);
// release message now to prevent resource-leak
ReferenceCountUtil.release(msg);
return;
}
int size;
try {
msg = filterOutboundMessage(msg);
size = pipeline.estimatorHandle().size(msg);
if (size < 0) {
size = 0;
}
} catch (Throwable t) {
safeSetFailure(promise, t);
ReferenceCountUtil.release(msg);
return;
}
outboundBuffer.addMessage(msg, size, promise);
}
這個方法將數據寫入了outboundBuffer。
(4)invokeFlush0
private void invokeFlush0() {
try {
((ChannelOutboundHandler) handler()).flush(this);
} catch (Throwable t) {
notifyHandlerException(t);
}
}
這裏最終調用的還是AbstractUnsafe的flush方法。
(5)flush
public final void flush() {
assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) {
return;
}
outboundBuffer.addFlush();
flush0();
}
這裏調用了AbstractUnsafe的flush0方法。
protected void flush0() {
...//省略代碼
inFlush0 = true;
...//省略代碼
try {
doWrite(outboundBuffer);
} catch (Throwable t) {
...//省略代碼
} else {
try {
shutdownOutput(voidPromise(), t);
} catch (Throwable t2) {
close(voidPromise(), t2, FLUSH0_CLOSED_CHANNEL_EXCEPTION, false);
}
}
} finally {
inFlush0 = false;
}
}
最終調用了NioSocketChannel的doWrite方法。
(6)doWrite
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
SocketChannel ch = javaChannel();
//最多循環寫入16次
int writeSpinCount = config().getWriteSpinCount();
do {
if (in.isEmpty()) {
// 如果沒有要寫的數據,清除write事件
clearOpWrite();
return;
}
// Ensure the pending writes are made of ByteBufs only.
int maxBytesPerGatheringWrite = ((NioSocketChannelConfig) config).getMaxBytesPerGatheringWrite();
ByteBuffer[] nioBuffers = in.nioBuffers(1024, maxBytesPerGatheringWrite);
int nioBufferCnt = in.nioBufferCount();
switch (nioBufferCnt) {
case 0:
// We have something else beside ByteBuffers to write so fallback to normal writes.
//這裏是處理其他類型的數據寫入
writeSpinCount -= doWrite0(in);
break;
case 1: {
//nioBuffers數量爲1處理邏輯
ByteBuffer buffer = nioBuffers[0];
int attemptedBytes = buffer.remaining();
//寫數據
final int localWrittenBytes = ch.write(buffer);
if (localWrittenBytes <= 0) {
incompleteWrite(true);
return;
}
adjustMaxBytesPerGatheringWrite(attemptedBytes, localWrittenBytes, maxBytesPerGatheringWrite);
in.removeBytes(localWrittenBytes);
--writeSpinCount;
break;
}
default: {
//nioBuffers數不爲1和0處理邏輯
long attemptedBytes = in.nioBufferSize();
final long localWrittenBytes = ch.write(nioBuffers, 0, nioBufferCnt);
if (localWrittenBytes <= 0) {
//如果沒有要寫的數據,註冊write事件
incompleteWrite(true);
return;
}
// Casting to int is safe because we limit the total amount of data in the nioBuffers to int above.
adjustMaxBytesPerGatheringWrite((int) attemptedBytes, (int) localWrittenBytes,
maxBytesPerGatheringWrite);
in.removeBytes(localWrittenBytes);
--writeSpinCount;
break;
}
}
} while (writeSpinCount > 0);
//writeSpinCount 大於0,代表在16次循環中數據已經寫完,否則代表沒寫完
incompleteWrite(writeSpinCount < 0);
}
nioBufferCnt爲0的處理邏輯,這個平時用的少不分析。nioBufferCnt爲1和不爲1的處理邏輯差不多,這裏以1爲例進行分析。
這個方法首先判斷是否有要寫入數據,沒有的話調用clearOpWrite方法清除write事件。
protected final void clearOpWrite() {
final SelectionKey key = selectionKey();
if (!key.isValid()) {
return;
}
final int interestOps = key.interestOps();
if ((interestOps & SelectionKey.OP_WRITE) != 0) {
key.interestOps(interestOps & ~SelectionKey.OP_WRITE);
}
}
如果數據寫完後,調用incompleteWrite方法重新註冊write事件。
protected final void incompleteWrite(boolean setOpWrite) {
// Did not write completely.
if (setOpWrite) {
setOpWrite();
} else {
clearOpWrite();
// Schedule flush again later so other tasks can be picked up in the meantime
eventLoop().execute(flushTask);
}
}
protected final void setOpWrite() {
final SelectionKey key = selectionKey();
if (!key.isValid()) {
return;
}
final int interestOps = key.interestOps();
if ((interestOps & SelectionKey.OP_WRITE) == 0) {
key.interestOps(interestOps | SelectionKey.OP_WRITE);
}
}
如果有要寫入的數據,則循環向buffer中寫入數據,這裏最多循環寫入16次,最後調用incompleteWrite方法。
(7)incompleteWrite
protected final void incompleteWrite(boolean setOpWrite) {
// Did not write completely.
if (setOpWrite) {
setOpWrite();
} else {
clearOpWrite();
eventLoop().execute(flushTask);
}
}
如果在16次內已經把所有數據寫完,則重新註冊write事件,如果沒有寫完,先把write事件取消,再把flushTask加入線程等待隊列。
我們前面分析過,線程的等待隊列會在線程執行完所有在selector上註冊的感興趣事件後,會把等待隊列中的任務執行完。
看下這個任務是什麼
private final Runnable flushTask = new Runnable() {
@Override
public void run() {
// Calling flush0 directly to ensure we not try to flush messages that were added via write(...) in the
// meantime.
((AbstractNioUnsafe) unsafe()).flush0();
}
};
這個任務其實就是重新調用flush0寫數據。寫完後重新註冊write事件。
3.1 write事件
最後我們看下如果註冊了write事件,怎麼處理的。
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
ch.unsafe().forceFlush();
}
這裏最後調用的是AbstractNioUnsafe的forceFlush方法。
public final void forceFlush() {
// directly call super.flush0() to force a flush now
super.flush0();
}
這裏又回到調用AbstractUnsafe的flush0方法。上面已經分析過了。
到這裏所有accept、connect、read、write事件已經分析完畢了。這裏我們省略了不少過程,具體的可以在前一篇服務端的分析代碼中去看看。