diff --git a/mirai-core/src/main/java/net/mamoe/mirai/network/BotNetworkHandler.kt b/mirai-core/src/main/java/net/mamoe/mirai/network/BotNetworkHandler.kt
new file mode 100644
index 000000000..52adfbd8f
--- /dev/null
+++ b/mirai-core/src/main/java/net/mamoe/mirai/network/BotNetworkHandler.kt
@@ -0,0 +1,428 @@
+@file:JvmMultifileClass
+@file:JvmName("BotNetworkHandler")
+
+package net.mamoe.mirai.network
+
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.MiraiServer
+import net.mamoe.mirai.event.events.bot.BotLoginSucceedEvent
+import net.mamoe.mirai.event.events.network.BeforePacketSendEvent
+import net.mamoe.mirai.event.events.network.PacketSentEvent
+import net.mamoe.mirai.event.events.network.ServerPacketReceivedEvent
+import net.mamoe.mirai.event.hookWhile
+import net.mamoe.mirai.network.BotNetworkHandler.Login
+import net.mamoe.mirai.network.BotNetworkHandler.SocketHandler
+import net.mamoe.mirai.network.handler.*
+import net.mamoe.mirai.network.packet.*
+import net.mamoe.mirai.network.packet.login.*
+import net.mamoe.mirai.task.MiraiThreadPool
+import net.mamoe.mirai.utils.*
+import java.io.Closeable
+import java.net.DatagramPacket
+import java.net.DatagramSocket
+import java.net.InetSocketAddress
+import java.util.*
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.ScheduledFuture
+import java.util.concurrent.TimeUnit
+import javax.imageio.ImageIO
+import kotlin.reflect.KClass
+
+
+/**
+ * Mirai 的网络处理器, 它处理所有数据包([Packet])的发送和接收.
+ * [BotNetworkHandler] 是全程异步和线程安全的.
+ *
+ * [BotNetworkHandler] 由 2 个模块构成:
+ * - [SocketHandler]: 处理数据包底层的发送([ByteArray])
+ * - [PacketHandler]: 制作 [Packet] 并传递给 [SocketHandler] 继续处理; 分析来自服务器的数据包并处理
+ *
+ * 其中, [PacketHandler] 由 4 个子模块构成:
+ * - [DebugHandler] 输出 [Packet.toString]
+ * - [Login] 处理 touch/login/verification code 相关
+ * - [MessageHandler] 处理消息相关(群消息/好友消息)([ServerEventPacket])
+ * - [ActionHandler] 处理动作相关(踢人/加入群/好友列表等)
+ *
+ * A BotNetworkHandler is used to connect with Tencent servers.
+ *
+ * @author Him188moe
+ */
+@Suppress("EXPERIMENTAL_API_USAGE")//to simplify code
+class BotNetworkHandler(private val bot: Bot) : Closeable {
+    private val socket: SocketHandler = SocketHandler()
+
+    fun getSocket(): DataPacketSocket {
+        return socket
+    }
+
+
+    lateinit var debugHandler: DebugHandler
+    lateinit var login: Login
+    lateinit var messageHandler: MessageHandler
+    lateinit var actionHandler: ActionHandler
+
+    val packetHandlers: PacketHandlerList = PacketHandlerList()
+
+
+    //private | internal
+
+    /**
+     * 尝试登录
+     *
+     * @param touchingTimeoutMillis 连接每个服务器的 timeout
+     */
+    @JvmOverloads
+    internal fun tryLogin(touchingTimeoutMillis: Long = 200): CompletableFuture<LoginState> {
+        val ipQueue: LinkedList<String> = LinkedList(Protocol.SERVER_IP)
+        val future = CompletableFuture<LoginState>()
+
+        fun login() {
+            this.socket.close()
+            val ip = ipQueue.poll()
+            if (ip == null) {
+                future.complete(LoginState.UNKNOWN)//所有服务器均返回 UNKNOWN
+                return
+            }
+
+            this@BotNetworkHandler.socket.touch(ip, touchingTimeoutMillis).get().let { state ->
+                if (state == LoginState.UNKNOWN || state == LoginState.TIMEOUT) {
+                    login()//超时或未知, 重试连接下一个服务器
+                } else {
+                    future.complete(state)
+                }
+            }
+        }
+        login()
+        return future
+    }
+
+
+    private lateinit var sessionKey: ByteArray
+
+    override fun close() {
+        this.packetHandlers.forEach {
+            it.instance.close()
+        }
+        this.socket.close()
+    }
+
+
+    private inner class SocketHandler : Closeable, DataPacketSocket {
+        override fun distributePacket(packet: ServerPacket) {
+            try {
+                packet.decode()
+            } catch (e: java.lang.Exception) {
+                e.printStackTrace()
+                bot.debugPacket(packet)
+                return
+            }
+
+            if (ServerPacketReceivedEvent(bot, packet).broadcast().isCancelled) {
+                debugHandler.onPacketReceived(packet)
+                return
+            }
+
+            login.onPacketReceived(packet)
+            packetHandlers.forEach {
+                it.instance.onPacketReceived(packet)
+            }
+        }
+
+        private var socket: DatagramSocket? = null
+
+        internal var serverIP: String = ""
+            set(value) {
+                field = value
+
+                restartSocket()
+            }
+
+        internal var loginFuture: CompletableFuture<LoginState>? = null
+
+        @Synchronized
+        private fun restartSocket() {
+            socket?.close()
+            socket = DatagramSocket(0)
+            socket!!.connect(InetSocketAddress(serverIP, 8000))
+            Thread {
+                while (socket!!.isConnected) {
+                    val packet = DatagramPacket(ByteArray(2048), 2048)
+                    kotlin.runCatching { socket?.receive(packet) }
+                            .onSuccess {
+                                MiraiThreadPool.getInstance().submit {
+                                    try {
+                                        distributePacket(ServerPacket.ofByteArray(packet.data.removeZeroTail()))
+                                    } catch (e: Exception) {
+                                        e.printStackTrace()
+                                    }
+                                }
+                            }.onFailure {
+                                if (it.message == "Socket closed" || it.message == "socket closed") {
+                                    return@Thread
+                                }
+                                it.printStackTrace()
+                            }
+
+                }
+            }.start()
+        }
+
+        /**
+         * Start network and touch the server
+         */
+        fun touch(serverAddress: String, timeoutMillis: Long): CompletableFuture<LoginState> {
+            bot.info("Connecting server: $serverAddress")
+            if (this@BotNetworkHandler::login.isInitialized) {
+                login.close()
+            }
+            login = Login()
+            this.loginFuture = CompletableFuture()
+
+            serverIP = serverAddress
+            waitForPacket(ServerPacket::class, timeoutMillis) {
+                loginFuture!!.complete(LoginState.TIMEOUT)
+            }
+            sendPacket(ClientTouchPacket(bot.account.qqNumber, serverIP))
+
+            return this.loginFuture!!
+        }
+
+        /**
+         * Not async
+         */
+        @Synchronized
+        @ExperimentalUnsignedTypes
+        override fun sendPacket(packet: ClientPacket) {
+            checkNotNull(socket) { "network closed" }
+            if (socket!!.isClosed) {
+                return
+            }
+
+            try {
+                packet.encodePacket()
+
+                if (BeforePacketSendEvent(bot, packet).broadcast().isCancelled) {
+                    return
+                }
+
+                val data = packet.toByteArray()
+                socket!!.send(DatagramPacket(data, data.size))
+                bot.cyanL("Packet sent:     $packet")
+
+                PacketSentEvent(bot, packet).broadcast()
+            } catch (e: Throwable) {
+                e.printStackTrace()
+            }
+        }
+
+        @Suppress("UNCHECKED_CAST")
+        internal fun <P : ServerPacket> waitForPacket(packetClass: KClass<P>, timeoutMillis: Long, timeout: () -> Unit) {
+            var got = false
+            ServerPacketReceivedEvent::class.hookWhile {
+                if (packetClass.isInstance(it.packet) && it.bot === bot) {
+                    got = true
+                    true
+                } else {
+                    false
+                }
+            }
+
+
+            MiraiThreadPool.getInstance().submit {
+                val startingTime = System.currentTimeMillis()
+                while (!got) {
+                    if (System.currentTimeMillis() - startingTime > timeoutMillis) {
+                        timeout.invoke()
+                        return@submit
+                    }
+                    Thread.sleep(10)
+                }
+            }
+        }
+
+        override fun close() {
+            this.socket?.close()
+            if (this.loginFuture != null) {
+                if (!this.loginFuture!!.isDone) {
+                    this.loginFuture!!.cancel(true)
+                }
+                this.loginFuture = null
+            }
+        }
+
+        override fun isClosed(): Boolean {
+            return this.socket?.isClosed ?: true
+        }
+    }
+
+    /**
+     * 处理登录过程
+     */
+    inner class Login : Closeable {
+        private lateinit var token00BA: ByteArray
+        private lateinit var token0825: ByteArray
+        private var loginTime: Int = 0
+        private lateinit var loginIP: String
+        private var tgtgtKey: ByteArray = getRandomByteArray(16)
+
+        private var tlv0105: ByteArray = lazyEncode {
+            it.writeHex("01 05 00 30")
+            it.writeHex("00 01 01 02 00 14 01 01 00 10")
+            it.writeRandom(16)
+            it.writeHex("00 14 01 02 00 10")
+            it.writeRandom(16)
+        }
+
+        /**
+         * 0828_decr_key
+         */
+        private lateinit var sessionResponseDecryptionKey: ByteArray
+
+        private var captchaSectionId: Int = 1
+        private var captchaCache: ByteArray? = byteArrayOf()//每次包只发一部分验证码来
+
+        private var heartbeatFuture: ScheduledFuture<*>? = null
+
+
+        fun onPacketReceived(packet: ServerPacket) {
+            when (packet) {
+                is ServerTouchResponsePacket -> {
+                    if (packet.serverIP != null) {//redirection
+                        socket.serverIP = packet.serverIP!!
+                        //connect(packet.serverIP!!)
+                        socket.sendPacket(ClientServerRedirectionPacket(packet.serverIP!!, bot.account.qqNumber))
+                    } else {//password submission
+                        this.loginIP = packet.loginIP
+                        this.loginTime = packet.loginTime
+                        this.token0825 = packet.token0825
+                        socket.sendPacket(ClientPasswordSubmissionPacket(bot.account.qqNumber, bot.account.password, packet.loginTime, packet.loginIP, this.tgtgtKey, packet.token0825))
+                    }
+                }
+
+                is ServerLoginResponseFailedPacket -> {
+                    socket.loginFuture?.complete(packet.loginState)
+                    return
+                }
+
+                is ServerVerificationCodeCorrectPacket -> {
+                    this.tgtgtKey = getRandomByteArray(16)
+                    this.token00BA = packet.token00BA
+                    socket.sendPacket(ClientLoginResendPacket3105(bot.account.qqNumber, bot.account.password, this.loginTime, this.loginIP, this.tgtgtKey, this.token0825, this.token00BA))
+                }
+
+                is ServerLoginResponseVerificationCodeInitPacket -> {
+                    //[token00BA]来源之一: 验证码
+                    this.token00BA = packet.token00BA
+                    this.captchaCache = packet.verifyCodePart1
+
+                    if (packet.unknownBoolean != null && packet.unknownBoolean!!) {
+                        this.captchaSectionId = 1
+                        socket.sendPacket(ClientVerificationCodeTransmissionRequestPacket(1, bot.account.qqNumber, this.token0825, this.captchaSectionId++, this.token00BA))
+                    }
+                }
+
+                is ServerVerificationCodeTransmissionPacket -> {
+                    if (packet is ServerVerificationCodeWrongPacket) {
+                        bot.error("验证码错误, 请重新输入")
+                        captchaSectionId = 1
+                        this.captchaCache = byteArrayOf()
+                    }
+
+                    this.captchaCache = this.captchaCache!! + packet.captchaSectionN
+                    this.token00BA = packet.token00BA
+
+                    if (packet.transmissionCompleted) {
+                        bot.notice(CharImageUtil.createCharImg(ImageIO.read(this.captchaCache!!.inputStream())))
+                        bot.notice("需要验证码登录, 验证码为 4 字母")
+                        try {
+                            (MiraiServer.getInstance().parentFolder + "VerificationCode.png").writeBytes(this.captchaCache!!)
+                            bot.notice("若看不清字符图片, 请查看 Mirai 根目录下 VerificationCode.png")
+                        } catch (e: Exception) {
+                            bot.notice("无法写出验证码文件, 请尝试查看以上字符图片")
+                        }
+                        bot.notice("若要更换验证码, 请直接回车")
+                        val code = Scanner(System.`in`).nextLine()
+                        if (code.isEmpty() || code.length != 4) {
+                            this.captchaCache = byteArrayOf()
+                            this.captchaSectionId = 1
+                            socket.sendPacket(ClientVerificationCodeRefreshPacket(packet.packetIdLast + 1, bot.account.qqNumber, token0825))
+                        } else {
+                            socket.sendPacket(ClientVerificationCodeSubmitPacket(packet.packetIdLast + 1, bot.account.qqNumber, token0825, code, packet.verificationToken))
+                        }
+                    } else {
+                        socket.sendPacket(ClientVerificationCodeTransmissionRequestPacket(packet.packetIdLast + 1, bot.account.qqNumber, token0825, captchaSectionId++, token00BA))
+                    }
+                }
+
+                is ServerLoginResponseSuccessPacket -> {
+                    this.sessionResponseDecryptionKey = packet.sessionResponseDecryptionKey
+                    socket.sendPacket(ClientSessionRequestPacket(bot.account.qqNumber, socket.serverIP, packet.token38, packet.token88, packet.encryptionKey, this.tlv0105))
+                }
+
+                //是ClientPasswordSubmissionPacket之后服务器回复的
+                is ServerLoginResponseKeyExchangePacket -> {
+                    //if (packet.tokenUnknown != null) {
+                    //this.token00BA = packet.token00BA!!
+                    //println("token00BA changed!!! to " + token00BA.toUByteArray())
+                    //}
+                    if (packet.flag == ServerLoginResponseKeyExchangePacket.Flag.`08 36 31 03`) {
+                        this.tgtgtKey = packet.tgtgtKey
+                        socket.sendPacket(ClientLoginResendPacket3104(bot.account.qqNumber, bot.account.password, loginTime, loginIP, tgtgtKey, token0825, packet.tokenUnknown
+                                ?: this.token00BA, packet.tlv0006))
+                    } else {
+                        socket.sendPacket(ClientLoginResendPacket3106(bot.account.qqNumber, bot.account.password, loginTime, loginIP, tgtgtKey, token0825, packet.tokenUnknown
+                                ?: token00BA, packet.tlv0006))
+                    }
+                }
+
+                is ServerSessionKeyResponsePacket -> {
+                    sessionKey = packet.sessionKey
+                    heartbeatFuture = MiraiThreadPool.getInstance().scheduleWithFixedDelay({
+                        socket.sendPacket(ClientHeartbeatPacket(bot.account.qqNumber, sessionKey))
+                    }, 90000, 90000, TimeUnit.MILLISECONDS)
+
+                    BotLoginSucceedEvent(bot).broadcast()
+
+                    //登录成功后会收到大量上次的消息, 忽略掉
+                    MiraiThreadPool.getInstance().schedule({
+                        messageHandler.ignoreMessage = false
+                    }, 3, TimeUnit.SECONDS)
+
+                    this.tlv0105 = packet.tlv0105
+
+                    socket.loginFuture!!.complete(LoginState.SUCCESS)
+                }
+
+
+                is ServerEventPacket.Raw -> socket.distributePacket(packet.distribute())
+
+                is ServerVerificationCodePacket.Encrypted -> socket.distributePacket(packet.decrypt())
+                is ServerLoginResponseVerificationCodeInitPacket.Encrypted -> socket.distributePacket(packet.decrypt())
+                is ServerLoginResponseKeyExchangePacket.Encrypted -> socket.distributePacket(packet.decrypt(this.tgtgtKey))
+                is ServerLoginResponseSuccessPacket.Encrypted -> socket.distributePacket(packet.decrypt(this.tgtgtKey))
+                is ServerSessionKeyResponsePacket.Encrypted -> socket.distributePacket(packet.decrypt(this.sessionResponseDecryptionKey))
+                is ServerTouchResponsePacket.Encrypted -> socket.distributePacket(packet.decrypt())
+                is ServerAccountInfoResponsePacket.Encrypted -> socket.distributePacket(packet.decrypt(sessionKey))
+                is ServerEventPacket.Raw.Encrypted -> socket.distributePacket(packet.decrypt(sessionKey))
+
+
+                is ServerLoginSuccessPacket,
+                is ServerHeartbeatResponsePacket,
+                is UnknownServerPacket -> {
+                    //ignored
+                }
+                else -> {
+
+                }
+            }
+        }
+
+        override fun close() {
+            this.captchaCache = null
+
+            this.heartbeatFuture?.cancel(true)
+
+            this.heartbeatFuture = null
+        }
+    }
+}
diff --git a/mirai-core/src/main/java/net/mamoe/mirai/network/LoginSession.kt b/mirai-core/src/main/java/net/mamoe/mirai/network/LoginSession.kt
new file mode 100644
index 000000000..a5bab9af8
--- /dev/null
+++ b/mirai-core/src/main/java/net/mamoe/mirai/network/LoginSession.kt
@@ -0,0 +1,24 @@
+package net.mamoe.mirai.network
+
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.network.handler.DataPacketSocket
+import net.mamoe.mirai.utils.getGTK
+
+/**
+ * 一次会话. 当登录完成后, 客户端会拿到 sessionKey. 此时建立 session, 开始处理消息等事务
+ *
+ * @author Him188moe
+ */
+class LoginSession(
+        val bot: Bot,
+        val sessionKey: ByteArray,
+        val socket: DataPacketSocket
+) {
+    lateinit var cookies: String
+    var sKey: String = ""
+        set(value) {
+            field = value
+            gtk = getGTK(value)
+        }
+    var gtk: Int = 0
+}
\ No newline at end of file
diff --git a/mirai-core/src/main/java/net/mamoe/mirai/network/RobotNetworkHandler.kt b/mirai-core/src/main/java/net/mamoe/mirai/network/RobotNetworkHandler.kt
deleted file mode 100644
index f6c0fe8de..000000000
--- a/mirai-core/src/main/java/net/mamoe/mirai/network/RobotNetworkHandler.kt
+++ /dev/null
@@ -1,711 +0,0 @@
-@file:JvmMultifileClass
-@file:JvmName("BotNetworkHandler")
-
-package net.mamoe.mirai.network
-
-import net.mamoe.mirai.Bot
-import net.mamoe.mirai.MiraiServer
-import net.mamoe.mirai.contact.Group
-import net.mamoe.mirai.contact.QQ
-import net.mamoe.mirai.event.events.bot.BotLoginSucceedEvent
-import net.mamoe.mirai.event.events.network.BeforePacketSendEvent
-import net.mamoe.mirai.event.events.network.PacketSentEvent
-import net.mamoe.mirai.event.events.network.ServerPacketReceivedEvent
-import net.mamoe.mirai.event.events.qq.FriendMessageEvent
-import net.mamoe.mirai.event.hookWhile
-import net.mamoe.mirai.message.Message
-import net.mamoe.mirai.message.defaults.MessageChain
-import net.mamoe.mirai.network.BotNetworkHandler.*
-import net.mamoe.mirai.network.packet.*
-import net.mamoe.mirai.network.packet.action.*
-import net.mamoe.mirai.network.packet.image.ServerTryUploadGroupImageFailedPacket
-import net.mamoe.mirai.network.packet.image.ServerTryUploadGroupImageResponsePacket
-import net.mamoe.mirai.network.packet.image.ServerTryUploadGroupImageSuccessPacket
-import net.mamoe.mirai.network.packet.login.*
-import net.mamoe.mirai.task.MiraiThreadPool
-import net.mamoe.mirai.utils.*
-import java.awt.image.BufferedImage
-import java.io.Closeable
-import java.net.DatagramPacket
-import java.net.DatagramSocket
-import java.net.InetSocketAddress
-import java.util.*
-import java.util.concurrent.CompletableFuture
-import java.util.concurrent.ScheduledFuture
-import java.util.concurrent.TimeUnit
-import java.util.function.Supplier
-import javax.imageio.ImageIO
-import kotlin.reflect.KClass
-
-
-/**
- * Mirai 的网络处理器, 它处理所有数据包([Packet])的发送和接收.
- * [BotNetworkHandler] 是全程异步和线程安全的.
- *
- * [BotNetworkHandler] 由 2 个模块构成:
- * - [SocketHandler]: 处理数据包底层的发送([ByteArray])
- * - [PacketHandler]: 制作 [Packet] 并传递给 [SocketHandler] 继续处理; 分析来自服务器的数据包并处理
- *
- * 其中, [PacketHandler] 由 4 个子模块构成:
- * - [DebugHandler] 输出 [Packet.toString]
- * - [LoginHandler] 处理 touch/login/verification code 相关
- * - [MessageHandler] 处理消息相关(群消息/好友消息)([ServerEventPacket])
- * - [ActionHandler] 处理动作相关(踢人/加入群/好友列表等)
- *
- * A BotNetworkHandler is used to connect with Tencent servers.
- *
- * @author Him188moe
- */
-@Suppress("EXPERIMENTAL_API_USAGE")//to simplify code
-class BotNetworkHandler(private val bot: Bot) : Closeable {
-    private val socketHandler: SocketHandler = SocketHandler()
-
-    val debugHandler = DebugHandler()
-    val loginHandler = LoginHandler()
-    val messageHandler = MessageHandler()
-    val actionHandler = ActionHandler()
-
-    private val packetHandlers: Map<KClass<out PacketHandler>, PacketHandler> = linkedMapOf(
-            DebugHandler::class to debugHandler,
-            LoginHandler::class to loginHandler,
-            MessageHandler::class to messageHandler,
-            ActionHandler::class to actionHandler
-    )
-
-    /**
-     * Not async
-     */
-    @ExperimentalUnsignedTypes
-    fun sendPacket(packet: ClientPacket) {
-        socketHandler.sendPacket(packet)
-    }
-
-    override fun close() {
-        this.packetHandlers.values.forEach {
-            it.close()
-        }
-        this.socketHandler.close()
-    }
-
-
-    //private | internal
-
-    /**
-     * 仅当 [LoginState] 非 [LoginState.UNKNOWN] 且非 [LoginState.TIMEOUT] 才会调用 [loginHook].
-     * 如果要输入验证码, 那么会以参数 [LoginState.VERIFICATION_CODE] 调用 [loginHandler], 登录完成后再以 [LoginState.SUCCESS] 调用 [loginHandler]
-     *
-     * @param touchingTimeoutMillis 连接每个服务器的 timeout
-     */
-    @JvmOverloads
-    internal fun tryLogin(touchingTimeoutMillis: Long = 200): CompletableFuture<LoginState> {
-        val ipQueue: LinkedList<String> = LinkedList(Protocol.SERVER_IP)
-        val future = CompletableFuture<LoginState>()
-
-        fun login() {
-            this.socketHandler.close()
-            val ip = ipQueue.poll()
-            if (ip == null) {
-                future.complete(LoginState.UNKNOWN)//所有服务器均返回 UNKNOWN
-                return
-            }
-
-            this@BotNetworkHandler.socketHandler.touch(ip, touchingTimeoutMillis).get().let { state ->
-                if (state == LoginState.UNKNOWN || state == LoginState.TIMEOUT) {
-                    login()
-                } else {
-                    future.complete(state)
-                }
-            }
-        }
-        login()
-        return future
-    }
-
-    /**
-     * 分配收到的数据包
-     */
-    @ExperimentalUnsignedTypes
-    internal fun distributePacket(packet: ServerPacket) {
-        try {
-            packet.decode()
-        } catch (e: java.lang.Exception) {
-            e.printStackTrace()
-            bot.debug("Packet=$packet")
-            bot.debug("Packet size=" + packet.input.goto(0).readAllBytes().size)
-            bot.debug("Packet data=" + packet.input.goto(0).readAllBytes().toUHexString())
-            return
-        }
-
-        if (ServerPacketReceivedEvent(bot, packet).broadcast().isCancelled) {
-            debugHandler.onPacketReceived(packet)
-            return
-        }
-        this.packetHandlers.values.forEach {
-            it.onPacketReceived(packet)
-        }
-    }
-
-
-    private inner class SocketHandler : Closeable {
-        private var socket: DatagramSocket? = null
-
-        internal var serverIP: String = ""
-            set(value) {
-                field = value
-
-                restartSocket()
-            }
-
-        internal var loginFuture: CompletableFuture<LoginState>? = null
-
-        @Synchronized
-        private fun restartSocket() {
-            socket?.close()
-            socket = DatagramSocket(0)
-            socket!!.connect(InetSocketAddress(serverIP, 8000))
-            Thread {
-                while (socket!!.isConnected) {
-                    val packet = DatagramPacket(ByteArray(2048), 2048)
-                    kotlin.runCatching { socket?.receive(packet) }
-                            .onSuccess {
-                                MiraiThreadPool.getInstance().submit {
-                                    try {
-                                        distributePacket(ServerPacket.ofByteArray(packet.data.removeZeroTail()))
-                                    } catch (e: Exception) {
-                                        e.printStackTrace()
-                                    }
-                                }
-                            }.onFailure {
-                                if (it.message == "Socket closed" || it.message == "socket closed") {
-                                    return@Thread
-                                }
-                                it.printStackTrace()
-                            }
-
-                }
-            }.start()
-        }
-
-        /**
-         * Start network and touch the server
-         */
-        internal fun touch(serverAddress: String, timeoutMillis: Long): CompletableFuture<LoginState> {
-            bot.info("Connecting server: $serverAddress")
-            this.loginFuture = CompletableFuture()
-
-            socketHandler.serverIP = serverAddress
-            waitForPacket(ServerPacket::class, timeoutMillis) {
-                loginFuture!!.complete(LoginState.TIMEOUT)
-            }
-            sendPacket(ClientTouchPacket(bot.account.qqNumber, socketHandler.serverIP))
-
-            return this.loginFuture!!
-        }
-
-        /**
-         * Not async
-         */
-        @Synchronized
-        @ExperimentalUnsignedTypes
-        internal fun sendPacket(packet: ClientPacket) {
-            checkNotNull(socket) { "network closed" }
-            if (socket!!.isClosed) {
-                return
-            }
-
-            try {
-                packet.encodePacket()
-
-                if (BeforePacketSendEvent(bot, packet).broadcast().isCancelled) {
-                    return
-                }
-
-                val data = packet.toByteArray()
-                socket!!.send(DatagramPacket(data, data.size))
-                bot cyanL "Packet sent:     $packet"
-
-                PacketSentEvent(bot, packet).broadcast()
-            } catch (e: Throwable) {
-                e.printStackTrace()
-            }
-        }
-
-        @Suppress("UNCHECKED_CAST")
-        internal fun <P : ServerPacket> waitForPacket(packetClass: KClass<P>, timeoutMillis: Long, timeout: () -> Unit) {
-            var got = false
-            ServerPacketReceivedEvent::class.hookWhile {
-                if (packetClass.isInstance(it.packet) && it.bot == bot) {
-                    got = true
-                    true
-                } else {
-                    false
-                }
-            }
-
-
-            MiraiThreadPool.getInstance().submit {
-                val startingTime = System.currentTimeMillis()
-                while (!got) {
-                    if (System.currentTimeMillis() - startingTime > timeoutMillis) {
-                        timeout.invoke()
-                        return@submit
-                    }
-                    Thread.sleep(10)
-                }
-            }
-        }
-
-        override fun close() {
-            this.socket?.close()
-            if (this.loginFuture != null) {
-                if (!this.loginFuture!!.isDone) {
-                    this.loginFuture!!.cancel(true)
-                }
-                this.loginFuture = null
-            }
-        }
-
-        fun isClosed(): Boolean {
-            return this.socket?.isClosed ?: true
-        }
-    }
-
-
-    private lateinit var sessionKey: ByteArray
-
-    abstract inner class PacketHandler : Closeable {
-        abstract fun onPacketReceived(packet: ServerPacket)
-
-        override fun close() {
-
-        }
-    }
-
-    /**
-     * Kind of [PacketHandler] that prints all packets received in the format of hex byte array.
-     */
-    inner class DebugHandler : PacketHandler() {
-        override fun onPacketReceived(packet: ServerPacket) {
-            if (!packet.javaClass.name.endsWith("Encrypted") && !packet.javaClass.name.endsWith("Raw")) {
-                bot notice "Packet received: $packet"
-            }
-            if (packet is ServerEventPacket) {
-                sendPacket(ClientMessageResponsePacket(bot.account.qqNumber, packet.packetId, sessionKey, packet.eventIdentity))
-            }
-        }
-    }
-
-    /**
-     * 处理登录过程
-     */
-    inner class LoginHandler : PacketHandler() {
-        private lateinit var token00BA: ByteArray
-        private lateinit var token0825: ByteArray
-        private var loginTime: Int = 0
-        private lateinit var loginIP: String
-        private var tgtgtKey: ByteArray = getRandomByteArray(16)
-
-        private var tlv0105: ByteArray = lazyEncode {
-            it.writeHex("01 05 00 30")
-            it.writeHex("00 01 01 02 00 14 01 01 00 10")
-            it.writeRandom(16)
-            it.writeHex("00 14 01 02 00 10")
-            it.writeRandom(16)
-        }
-
-        /**
-         * 0828_decr_key
-         */
-        private lateinit var sessionResponseDecryptionKey: ByteArray
-
-        private var captchaSectionId: Int = 1
-        private var captchaCache: ByteArray? = byteArrayOf()//每次包只发一部分验证码来
-
-
-        private var heartbeatFuture: ScheduledFuture<*>? = null
-        private var sKeyRefresherFuture: ScheduledFuture<*>? = null
-
-        override fun onPacketReceived(packet: ServerPacket) {
-            when (packet) {
-                is ServerTouchResponsePacket -> {
-                    if (packet.serverIP != null) {//redirection
-                        socketHandler.serverIP = packet.serverIP!!
-                        //connect(packet.serverIP!!)
-                        sendPacket(ClientServerRedirectionPacket(packet.serverIP!!, bot.account.qqNumber))
-                    } else {//password submission
-                        this.loginIP = packet.loginIP
-                        this.loginTime = packet.loginTime
-                        this.token0825 = packet.token0825
-                        sendPacket(ClientPasswordSubmissionPacket(bot.account.qqNumber, bot.account.password, packet.loginTime, packet.loginIP, this.tgtgtKey, packet.token0825))
-                    }
-                }
-
-                is ServerLoginResponseFailedPacket -> {
-                    socketHandler.loginFuture?.complete(packet.loginState)
-                    return
-                }
-
-                is ServerVerificationCodeCorrectPacket -> {
-                    this.tgtgtKey = getRandomByteArray(16)
-                    this.token00BA = packet.token00BA
-                    sendPacket(ClientLoginResendPacket3105(bot.account.qqNumber, bot.account.password, this.loginTime, this.loginIP, this.tgtgtKey, this.token0825, this.token00BA))
-                }
-
-                is ServerLoginResponseVerificationCodeInitPacket -> {
-                    //[token00BA]来源之一: 验证码
-                    this.token00BA = packet.token00BA
-                    this.captchaCache = packet.verifyCodePart1
-
-                    if (packet.unknownBoolean != null && packet.unknownBoolean!!) {
-                        this.captchaSectionId = 1
-                        sendPacket(ClientVerificationCodeTransmissionRequestPacket(1, bot.account.qqNumber, this.token0825, this.captchaSectionId++, this.token00BA))
-                    }
-                }
-
-                is ServerVerificationCodeTransmissionPacket -> {
-                    if (packet is ServerVerificationCodeWrongPacket) {
-                        bot error "验证码错误, 请重新输入"
-                        captchaSectionId = 1
-                        this.captchaCache = byteArrayOf()
-                    }
-
-                    this.captchaCache = this.captchaCache!! + packet.captchaSectionN
-                    this.token00BA = packet.token00BA
-
-                    if (packet.transmissionCompleted) {
-                        bot notice (CharImageUtil.createCharImg(ImageIO.read(this.captchaCache!!.inputStream())))
-                        bot notice ("需要验证码登录, 验证码为 4 字母")
-                        try {
-                            (MiraiServer.getInstance().parentFolder + "VerificationCode.png").writeBytes(this.captchaCache!!)
-                            bot notice ("若看不清字符图片, 请查看 Mirai 根目录下 VerificationCode.png")
-                        } catch (e: Exception) {
-                            bot notice "无法写出验证码文件, 请尝试查看以上字符图片"
-                        }
-                        bot notice ("若要更换验证码, 请直接回车")
-                        val code = Scanner(System.`in`).nextLine()
-                        if (code.isEmpty() || code.length != 4) {
-                            this.captchaCache = byteArrayOf()
-                            this.captchaSectionId = 1
-                            sendPacket(ClientVerificationCodeRefreshPacket(packet.packetIdLast + 1, bot.account.qqNumber, token0825))
-                        } else {
-                            sendPacket(ClientVerificationCodeSubmitPacket(packet.packetIdLast + 1, bot.account.qqNumber, token0825, code, packet.verificationToken))
-                        }
-                    } else {
-                        sendPacket(ClientVerificationCodeTransmissionRequestPacket(packet.packetIdLast + 1, bot.account.qqNumber, token0825, captchaSectionId++, token00BA))
-                    }
-                }
-
-                is ServerLoginResponseSuccessPacket -> {
-                    this.sessionResponseDecryptionKey = packet.sessionResponseDecryptionKey
-                    sendPacket(ClientSessionRequestPacket(bot.account.qqNumber, socketHandler.serverIP, packet.token38, packet.token88, packet.encryptionKey, this.tlv0105))
-                }
-
-                //是ClientPasswordSubmissionPacket之后服务器回复的
-                is ServerLoginResponseKeyExchangePacket -> {
-                    //if (packet.tokenUnknown != null) {
-                    //this.token00BA = packet.token00BA!!
-                    //println("token00BA changed!!! to " + token00BA.toUByteArray())
-                    //}
-                    if (packet.flag == ServerLoginResponseKeyExchangePacket.Flag.`08 36 31 03`) {
-                        this.tgtgtKey = packet.tgtgtKey
-                        sendPacket(ClientLoginResendPacket3104(bot.account.qqNumber, bot.account.password, loginTime, loginIP, tgtgtKey, token0825, packet.tokenUnknown
-                                ?: this.token00BA, packet.tlv0006))
-                    } else {
-                        sendPacket(ClientLoginResendPacket3106(bot.account.qqNumber, bot.account.password, loginTime, loginIP, tgtgtKey, token0825, packet.tokenUnknown
-                                ?: token00BA, packet.tlv0006))
-                    }
-                }
-
-                is ServerSessionKeyResponsePacket -> {
-                    sessionKey = packet.sessionKey
-                    heartbeatFuture = MiraiThreadPool.getInstance().scheduleWithFixedDelay({
-                        sendPacket(ClientHeartbeatPacket(bot.account.qqNumber, sessionKey))
-                    }, 90000, 90000, TimeUnit.MILLISECONDS)
-
-                    BotLoginSucceedEvent(bot).broadcast()
-
-                    //登录成功后会收到大量上次的消息, 忽略掉
-                    MiraiThreadPool.getInstance().schedule({
-                        messageHandler.ignoreMessage = false
-                    }, 3, TimeUnit.SECONDS)
-
-                    this.tlv0105 = packet.tlv0105
-                    sendPacket(ClientChangeOnlineStatusPacket(bot.account.qqNumber, sessionKey, ClientLoginStatus.ONLINE))
-                }
-
-                is ServerLoginSuccessPacket -> {
-                    socketHandler.loginFuture!!.complete(LoginState.SUCCESS)
-                    sendPacket(ClientSKeyRequestPacket(bot.account.qqNumber, sessionKey))
-                }
-
-                is ServerSKeyResponsePacket -> {
-                    actionHandler.sKey = packet.sKey
-                    actionHandler.cookies = "uin=o" + bot.account.qqNumber + ";skey=" + actionHandler.sKey + ";"
-
-                    sKeyRefresherFuture = MiraiThreadPool.getInstance().scheduleWithFixedDelay({
-                        sendPacket(ClientSKeyRefreshmentRequestPacket(bot.account.qqNumber, sessionKey))
-                    }, 1800000, 1800000, TimeUnit.MILLISECONDS)
-
-                    actionHandler.gtk = getGTK(actionHandler.sKey)
-                    sendPacket(ClientAccountInfoRequestPacket(bot.account.qqNumber, sessionKey))
-                }
-
-                is ServerEventPacket.Raw -> distributePacket(packet.distribute())
-
-                is ServerVerificationCodePacket.Encrypted -> distributePacket(packet.decrypt())
-                is ServerLoginResponseVerificationCodeInitPacket.Encrypted -> distributePacket(packet.decrypt())
-                is ServerLoginResponseKeyExchangePacket.Encrypted -> distributePacket(packet.decrypt(this.tgtgtKey))
-                is ServerLoginResponseSuccessPacket.Encrypted -> distributePacket(packet.decrypt(this.tgtgtKey))
-                is ServerSessionKeyResponsePacket.Encrypted -> distributePacket(packet.decrypt(this.sessionResponseDecryptionKey))
-                is ServerTouchResponsePacket.Encrypted -> distributePacket(packet.decrypt())
-                is ServerSKeyResponsePacket.Encrypted -> distributePacket(packet.decrypt(sessionKey))
-                is ServerAccountInfoResponsePacket.Encrypted -> distributePacket(packet.decrypt(sessionKey))
-                is ServerEventPacket.Raw.Encrypted -> distributePacket(packet.decrypt(sessionKey))
-
-
-                is ServerAccountInfoResponsePacket,
-                is ServerHeartbeatResponsePacket,
-                is UnknownServerPacket -> {
-                    //ignored
-                }
-                else -> {
-
-                }
-            }
-        }
-
-        override fun close() {
-            this.captchaCache = null
-
-            this.heartbeatFuture?.cancel(true)
-            this.sKeyRefresherFuture?.cancel(true)
-
-            this.heartbeatFuture = null
-            this.sKeyRefresherFuture = null
-        }
-    }
-
-    /**
-     * 处理消息事件, 承担消息发送任务.
-     */
-    inner class MessageHandler : PacketHandler() {
-        internal var ignoreMessage: Boolean = true
-
-        init {
-            //todo for test
-            FriendMessageEvent::class.hookWhile {
-                if (socketHandler.isClosed()) {
-                    return@hookWhile false
-                }
-                if (it.message() valueEquals "你好") {
-                    it.qq.sendMessage("你好!")
-                } else if (it.message().toString().startsWith("复读")) {
-                    it.qq.sendMessage(it.message())
-                }
-
-                return@hookWhile true
-            }
-        }
-
-        override fun onPacketReceived(packet: ServerPacket) {
-            when (packet) {
-                is ServerGroupUploadFileEventPacket -> {
-                    //todo
-                }
-
-                is ServerFriendMessageEventPacket -> {
-                    if (ignoreMessage) {
-                        return
-                    }
-
-                    FriendMessageEvent(bot, bot.contacts.getQQ(packet.qq), packet.message).broadcast()
-                }
-
-                is ServerGroupMessageEventPacket -> {
-                    //todo message chain
-                    //GroupMessageEvent(this.bot, bot.contacts.getGroupByNumber(packet.groupNumber), bot.contacts.getQQ(packet.qq), packet.message)
-                }
-
-                is UnknownServerEventPacket -> {
-                    //todo
-                }
-
-                is ServerSendFriendMessageResponsePacket,
-                is ServerSendGroupMessageResponsePacket -> {
-                    //ignored
-                }
-                else -> {
-                    //ignored
-                }
-            }
-        }
-
-        fun sendFriendMessage(qq: QQ, message: MessageChain) {
-            sendPacket(ClientSendFriendMessagePacket(bot.account.qqNumber, qq.number, sessionKey, message))
-        }
-
-        fun sendGroupMessage(group: Group, message: Message): Unit {
-            TODO()
-            //sendPacket(ClientSendGroupMessagePacket(group.groupId, bot.account.qqNumber, sessionKey, message))
-        }
-    }
-
-    /**
-     * 动作: 获取好友列表, 点赞, 踢人等.
-     * 处理动作事件, 承担动作任务.
-     */
-    inner class ActionHandler : PacketHandler() {
-        internal lateinit var cookies: String
-        internal var sKey: String = ""
-            set(value) {
-                field = value
-                gtk = getGTK(value)
-            }
-        internal var gtk: Int = 0
-
-        private val addFriendSessions = Collections.synchronizedCollection(mutableListOf<AddFriendSession>())
-        private val uploadImageSessions = Collections.synchronizedCollection(mutableListOf<UploadImageSession>())
-
-        override fun onPacketReceived(packet: ServerPacket) {
-            when (packet) {
-                is ServerCanAddFriendResponsePacket -> {
-                    this.uploadImageSessions.forEach {
-                        it.onPacketReceived(packet)
-                    }
-                }
-                is ServerTryUploadGroupImageSuccessPacket -> {
-                    ImageNetworkUtils.postImage(packet.uKey.toUHexString(), )
-                }
-
-                is ServerTryUploadGroupImageFailedPacket -> {
-
-                }
-
-                is ServerTryUploadGroupImageResponsePacket.Encrypted -> distributePacket(packet.decrypt(sessionKey))
-                else -> {
-                }
-            }
-        }
-
-        fun addFriend(qqNumber: Long, message: Supplier<String>) {
-            addFriend(qqNumber, lazy { message.get() })
-        }
-
-        @JvmSynthetic
-        fun addFriend(qqNumber: Long, message: Lazy<String> = lazyOf("")): CompletableFuture<AddFriendResult> {
-            val future = CompletableFuture<AddFriendResult>()
-            val session = AddFriendSession(qqNumber, future, message)
-            uploadImageSessions.add(session)
-            session.sendAddRequest();
-            return future
-        }
-
-        override fun close() {
-
-        }
-
-        private inner class UploadImageSession(
-                private val group: Long,
-                private val future: CompletableFuture<AddFriendResult>,
-                private val image: BufferedImage
-        ) : Closeable {
-            lateinit var id: ByteArray
-
-            fun onPacketReceived(packet: ServerPacket) {
-                if (!::id.isInitialized) {
-                    return
-                }
-
-                when (packet) {
-                    is ServerCanAddFriendResponsePacket -> {
-                        if (!(packet.idByteArray[2] == id[0] && packet.idByteArray[3] == id[1])) {
-                            return
-                        }
-
-                        when (packet.state) {
-                            ServerCanAddFriendResponsePacket.State.FAILED -> {
-                                future.complete(AddFriendResult.FAILED)
-                                close()
-                            }
-
-                            ServerCanAddFriendResponsePacket.State.ALREADY_ADDED -> {
-                                future.complete(AddFriendResult.ALREADY_ADDED)
-                                close()
-                            }
-
-                            ServerCanAddFriendResponsePacket.State.REQUIRE_VERIFICATION -> {
-                                sendPacket(ClientAddFriendPacket(bot.account.qqNumber, qq, sessionKey))
-                            }
-
-                            ServerCanAddFriendResponsePacket.State.NOT_REQUIRE_VERIFICATION -> {
-
-                            }
-                        }
-                    }
-
-
-                }
-            }
-
-            override fun sendRequest() {
-
-            }
-
-            override fun close() {
-                uploadImageSessions.remove(this)
-            }
-        }
-
-        private inner class AddFriendSession(
-                private val qq: Long,
-                private val future: CompletableFuture<AddFriendResult>,
-                private val message: Lazy<String>
-        ) : Closeable {
-            lateinit var id: ByteArray
-
-            fun onPacketReceived(packet: ServerPacket) {
-                if (!::id.isInitialized) {
-                    return
-                }
-
-                when (packet) {
-                    is ServerCanAddFriendResponsePacket -> {
-                        if (!(packet.idByteArray[2] == id[0] && packet.idByteArray[3] == id[1])) {
-                            return
-                        }
-
-                        when (packet.state) {
-                            ServerCanAddFriendResponsePacket.State.FAILED -> {
-                                future.complete(AddFriendResult.FAILED)
-                                close()
-                            }
-
-                            ServerCanAddFriendResponsePacket.State.ALREADY_ADDED -> {
-                                future.complete(AddFriendResult.ALREADY_ADDED)
-                                close()
-                            }
-
-                            ServerCanAddFriendResponsePacket.State.REQUIRE_VERIFICATION -> {
-                                sendPacket(ClientAddFriendPacket(bot.account.qqNumber, qq, sessionKey))
-                            }
-
-                            ServerCanAddFriendResponsePacket.State.NOT_REQUIRE_VERIFICATION -> {
-
-                            }
-                        }
-                    }
-
-
-                }
-            }
-
-            fun sendAddRequest() {
-                sendPacket(ClientCanAddFriendPacket(bot.account.qqNumber, qq, sessionKey).also { this.id = it.packetIdLast })
-            }
-
-            override fun close() {
-                uploadImageSessions.remove(this)
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/mirai-core/src/main/java/net/mamoe/mirai/network/handler/ActionHandler.kt b/mirai-core/src/main/java/net/mamoe/mirai/network/handler/ActionHandler.kt
new file mode 100644
index 000000000..2a45138ff
--- /dev/null
+++ b/mirai-core/src/main/java/net/mamoe/mirai/network/handler/ActionHandler.kt
@@ -0,0 +1,216 @@
+package net.mamoe.mirai.network.handler
+
+import net.mamoe.mirai.network.LoginSession
+import net.mamoe.mirai.network.packet.*
+import net.mamoe.mirai.network.packet.action.AddFriendResult
+import net.mamoe.mirai.network.packet.action.ClientAddFriendPacket
+import net.mamoe.mirai.network.packet.action.ClientCanAddFriendPacket
+import net.mamoe.mirai.network.packet.action.ServerCanAddFriendResponsePacket
+import net.mamoe.mirai.network.packet.image.ServerTryUploadGroupImageFailedPacket
+import net.mamoe.mirai.network.packet.image.ServerTryUploadGroupImageResponsePacket
+import net.mamoe.mirai.network.packet.image.ServerTryUploadGroupImageSuccessPacket
+import net.mamoe.mirai.network.packet.login.ClientChangeOnlineStatusPacket
+import net.mamoe.mirai.task.MiraiThreadPool
+import net.mamoe.mirai.utils.ClientLoginStatus
+import net.mamoe.mirai.utils.ImageNetworkUtils
+import net.mamoe.mirai.utils.getGTK
+import net.mamoe.mirai.utils.toUHexString
+import java.awt.image.BufferedImage
+import java.io.Closeable
+import java.util.*
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.ScheduledFuture
+import java.util.concurrent.TimeUnit
+import java.util.function.Supplier
+
+/**
+ * 动作: 获取好友列表, 点赞, 踢人等.
+ * 处理动作事件, 承担动作任务.
+ */
+class ActionHandler(session: LoginSession) : PacketHandler(session) {
+    private val addFriendSessions = Collections.synchronizedCollection(mutableListOf<AddFriendSession>())
+    private val uploadImageSessions = Collections.synchronizedCollection(mutableListOf<UploadImageSession>())
+
+    private var sKeyRefresherFuture: ScheduledFuture<*>? = null
+
+    @ExperimentalUnsignedTypes
+    override fun onPacketReceived(packet: ServerPacket) {
+        when (packet) {
+            is ServerCanAddFriendResponsePacket -> {
+                this.uploadImageSessions.forEach {
+                    it.onPacketReceived(packet)
+                }
+            }
+            is ServerTryUploadGroupImageSuccessPacket -> {
+                ImageNetworkUtils.postImage(packet.uKey.toUHexString(), )
+            }
+
+            is ServerTryUploadGroupImageFailedPacket -> {
+
+            }
+
+            is ServerTryUploadGroupImageResponsePacket.Encrypted -> session.socket.distributePacket(packet.decrypt(session.sessionKey))
+
+            is ServerAccountInfoResponsePacket -> {
+
+            }
+
+            is ServerSKeyResponsePacket.Encrypted -> session.socket.distributePacket(packet.decrypt(session.sessionKey))
+            is ServerSKeyResponsePacket -> {
+                session.sKey = packet.sKey
+                session.cookies = "uin=o" + session.bot.account.qqNumber + ";skey=" + session.sKey + ";"
+
+                sKeyRefresherFuture = MiraiThreadPool.getInstance().scheduleWithFixedDelay({
+                    session.socket.sendPacket(ClientSKeyRefreshmentRequestPacket(session.bot.account.qqNumber, session.sessionKey))
+                }, 1800000, 1800000, TimeUnit.MILLISECONDS)
+
+                session.gtk = getGTK(session.sKey)
+            }
+
+            else -> {
+            }
+        }
+    }
+
+    @ExperimentalUnsignedTypes
+    fun addFriend(qqNumber: Long, message: Supplier<String>) {
+        addFriend(qqNumber, lazy { message.get() })
+    }
+
+    @ExperimentalUnsignedTypes
+    @JvmSynthetic
+    fun addFriend(qqNumber: Long, message: Lazy<String> = lazyOf("")): CompletableFuture<AddFriendResult> {
+        val future = CompletableFuture<AddFriendResult>()
+        val session = AddFriendSession(qqNumber, future, message)
+        uploadImageSessions.add(session)
+        session.sendAddRequest();
+        return future
+    }
+
+    @ExperimentalUnsignedTypes
+    fun requestSKey() {
+        session.socket.sendPacket(ClientSKeyRequestPacket(session.bot.account.qqNumber, session.sessionKey))
+    }
+
+    @ExperimentalUnsignedTypes
+    fun changeOnlineStatus(status: ClientLoginStatus) {
+        session.socket.sendPacket(ClientChangeOnlineStatusPacket(session.bot.account.qqNumber, session.sessionKey, status))
+    }
+
+    @ExperimentalUnsignedTypes
+    fun requestAccountInfo() {
+        session.socket.sendPacket(ClientAccountInfoRequestPacket(session.bot.account.qqNumber, session.sessionKey))
+    }
+
+    override fun close() {
+        this.sKeyRefresherFuture?.cancel(true)
+        this.sKeyRefresherFuture = null
+    }
+
+    private inner class UploadImageSession(
+            private val group: Long,
+            private val future: CompletableFuture<AddFriendResult>,
+            private val image: BufferedImage
+    ) : Closeable {
+        lateinit var id: ByteArray
+
+        @ExperimentalUnsignedTypes
+        fun onPacketReceived(packet: ServerPacket) {
+            if (!::id.isInitialized) {
+                return
+            }
+
+            when (packet) {
+                is ServerCanAddFriendResponsePacket -> {
+                    if (!(packet.idByteArray[2] == id[0] && packet.idByteArray[3] == id[1])) {
+                        return
+                    }
+
+                    when (packet.state) {
+                        ServerCanAddFriendResponsePacket.State.FAILED -> {
+                            future.complete(AddFriendResult.FAILED)
+                            close()
+                        }
+
+                        ServerCanAddFriendResponsePacket.State.ALREADY_ADDED -> {
+                            future.complete(AddFriendResult.ALREADY_ADDED)
+                            close()
+                        }
+
+                        ServerCanAddFriendResponsePacket.State.REQUIRE_VERIFICATION -> {
+                            session.socket.sendPacket(ClientAddFriendPacket(session.bot.account.qqNumber, qq, session.sessionKey))
+                        }
+
+                        ServerCanAddFriendResponsePacket.State.NOT_REQUIRE_VERIFICATION -> {
+
+                        }
+                    }
+                }
+
+
+            }
+        }
+
+        fun sendRequest() {
+
+        }
+
+        override fun close() {
+            uploadImageSessions.remove(this)
+        }
+    }
+
+    private inner class AddFriendSession(
+            private val qq: Long,
+            private val future: CompletableFuture<AddFriendResult>,
+            private val message: Lazy<String>
+    ) : Closeable {
+        lateinit var id: ByteArray
+
+        @ExperimentalUnsignedTypes
+        fun onPacketReceived(packet: ServerPacket) {
+            if (!::id.isInitialized) {
+                return
+            }
+
+            when (packet) {
+                is ServerCanAddFriendResponsePacket -> {
+                    if (!(packet.idByteArray[2] == id[0] && packet.idByteArray[3] == id[1])) {
+                        return
+                    }
+
+                    when (packet.state) {
+                        ServerCanAddFriendResponsePacket.State.FAILED -> {
+                            future.complete(AddFriendResult.FAILED)
+                            close()
+                        }
+
+                        ServerCanAddFriendResponsePacket.State.ALREADY_ADDED -> {
+                            future.complete(AddFriendResult.ALREADY_ADDED)
+                            close()
+                        }
+
+                        ServerCanAddFriendResponsePacket.State.REQUIRE_VERIFICATION -> {
+                            session.socket.sendPacket(ClientAddFriendPacket(session.bot.account.qqNumber, qq, session.sessionKey))
+                        }
+
+                        ServerCanAddFriendResponsePacket.State.NOT_REQUIRE_VERIFICATION -> {
+
+                        }
+                    }
+                }
+
+
+            }
+        }
+
+        @ExperimentalUnsignedTypes
+        fun sendAddRequest() {
+            session.socket.sendPacket(ClientCanAddFriendPacket(session.bot.account.qqNumber, qq, session.sessionKey).also { this.id = it.packetIdLast })
+        }
+
+        override fun close() {
+            uploadImageSessions.remove(this)
+        }
+    }
+}
\ No newline at end of file
diff --git a/mirai-core/src/main/java/net/mamoe/mirai/network/handler/BotSession.kt b/mirai-core/src/main/java/net/mamoe/mirai/network/handler/BotSession.kt
deleted file mode 100644
index ae872e2bb..000000000
--- a/mirai-core/src/main/java/net/mamoe/mirai/network/handler/BotSession.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package net.mamoe.mirai.network.handler
-
-import net.mamoe.mirai.Bot
-import net.mamoe.mirai.network.BotNetworkHandler
-
-/**
- * @author Him188moe
- */
-data class BotSession(
-        val bot: Bot,
-        val sessionKey: ByteArray,
-        val networkHandler: BotNetworkHandler
-) {
-}
\ No newline at end of file
diff --git a/mirai-core/src/main/java/net/mamoe/mirai/network/handler/DataPacketSocket.kt b/mirai-core/src/main/java/net/mamoe/mirai/network/handler/DataPacketSocket.kt
new file mode 100644
index 000000000..9c5aac23d
--- /dev/null
+++ b/mirai-core/src/main/java/net/mamoe/mirai/network/handler/DataPacketSocket.kt
@@ -0,0 +1,20 @@
+package net.mamoe.mirai.network.handler
+
+import net.mamoe.mirai.network.packet.ClientPacket
+import net.mamoe.mirai.network.packet.ServerPacket
+import java.io.Closeable
+
+/**
+ * @author Him188moe
+ */
+interface DataPacketSocket : Closeable {
+
+    fun distributePacket(packet: ServerPacket)
+
+    @ExperimentalUnsignedTypes
+    fun sendPacket(packet: ClientPacket)
+
+    fun isClosed(): Boolean
+
+    override fun close()
+}
\ No newline at end of file
diff --git a/mirai-core/src/main/java/net/mamoe/mirai/network/handler/DebugHandler.kt b/mirai-core/src/main/java/net/mamoe/mirai/network/handler/DebugHandler.kt
new file mode 100644
index 000000000..5be246db3
--- /dev/null
+++ b/mirai-core/src/main/java/net/mamoe/mirai/network/handler/DebugHandler.kt
@@ -0,0 +1,24 @@
+package net.mamoe.mirai.network.handler
+
+import net.mamoe.mirai.network.LoginSession
+import net.mamoe.mirai.network.packet.ClientMessageResponsePacket
+import net.mamoe.mirai.network.packet.ServerEventPacket
+import net.mamoe.mirai.network.packet.ServerPacket
+import net.mamoe.mirai.utils.notice
+
+/**
+ * Kind of [PacketHandler] that prints all packets received in the format of hex byte array.
+ */
+sealed class DebugHandler(session: LoginSession) : PacketHandler(session) {
+
+    @ExperimentalUnsignedTypes
+    override fun onPacketReceived(packet: ServerPacket) {
+        if (!packet.javaClass.name.endsWith("Encrypted") && !packet.javaClass.name.endsWith("Raw")) {
+            session.bot notice "Packet received: $packet"
+        }
+
+        if (packet is ServerEventPacket) {
+            session.socket.sendPacket(ClientMessageResponsePacket(session.bot.account.qqNumber, packet.packetId, session.sessionKey, packet.eventIdentity))
+        }
+    }
+}
\ No newline at end of file
diff --git a/mirai-core/src/main/java/net/mamoe/mirai/network/handler/MessageHandler.kt b/mirai-core/src/main/java/net/mamoe/mirai/network/handler/MessageHandler.kt
new file mode 100644
index 000000000..e542cb7a3
--- /dev/null
+++ b/mirai-core/src/main/java/net/mamoe/mirai/network/handler/MessageHandler.kt
@@ -0,0 +1,79 @@
+package net.mamoe.mirai.network.handler
+
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.contact.QQ
+import net.mamoe.mirai.event.events.qq.FriendMessageEvent
+import net.mamoe.mirai.event.hookWhile
+import net.mamoe.mirai.message.Message
+import net.mamoe.mirai.message.defaults.MessageChain
+import net.mamoe.mirai.network.LoginSession
+import net.mamoe.mirai.network.packet.*
+import net.mamoe.mirai.network.packet.action.ClientSendFriendMessagePacket
+import net.mamoe.mirai.network.packet.action.ServerSendFriendMessageResponsePacket
+import net.mamoe.mirai.network.packet.action.ServerSendGroupMessageResponsePacket
+
+/**
+ * 处理消息事件, 承担消息发送任务.
+ */
+class MessageHandler(session: LoginSession) : PacketHandler(session) {
+    internal var ignoreMessage: Boolean = true
+
+    init {
+        //todo for test
+        FriendMessageEvent::class.hookWhile {
+            if (session.socket.isClosed()) {
+                return@hookWhile false
+            }
+            if (it.message() valueEquals "你好") {
+                it.qq.sendMessage("你好!")
+            } else if (it.message().toString().startsWith("复读")) {
+                it.qq.sendMessage(it.message())
+            }
+
+            return@hookWhile true
+        }
+    }
+
+    override fun onPacketReceived(packet: ServerPacket) {
+        when (packet) {
+            is ServerGroupUploadFileEventPacket -> {
+                //todo
+            }
+
+            is ServerFriendMessageEventPacket -> {
+                if (ignoreMessage) {
+                    return
+                }
+
+                FriendMessageEvent(session.bot, session.bot.contacts.getQQ(packet.qq), packet.message).broadcast()
+            }
+
+            is ServerGroupMessageEventPacket -> {
+                //todo message chain
+                //GroupMessageEvent(this.bot, bot.contacts.getGroupByNumber(packet.groupNumber), bot.contacts.getQQ(packet.qq), packet.message)
+            }
+
+            is UnknownServerEventPacket -> {
+                //todo
+            }
+
+            is ServerSendFriendMessageResponsePacket,
+            is ServerSendGroupMessageResponsePacket -> {
+                //ignored
+            }
+            else -> {
+                //ignored
+            }
+        }
+    }
+
+    @ExperimentalUnsignedTypes
+    fun sendFriendMessage(qq: QQ, message: MessageChain) {
+        session.socket.sendPacket(ClientSendFriendMessagePacket(session.bot.account.qqNumber, qq.number, session.sessionKey, message))
+    }
+
+    fun sendGroupMessage(group: Group, message: Message): Unit {
+        TODO()
+        //sendPacket(ClientSendGroupMessagePacket(group.groupId, bot.account.qqNumber, sessionKey, message))
+    }
+}
\ No newline at end of file
diff --git a/mirai-core/src/main/java/net/mamoe/mirai/network/handler/PacketHandler.kt b/mirai-core/src/main/java/net/mamoe/mirai/network/handler/PacketHandler.kt
new file mode 100644
index 000000000..f2dd3e4a2
--- /dev/null
+++ b/mirai-core/src/main/java/net/mamoe/mirai/network/handler/PacketHandler.kt
@@ -0,0 +1,43 @@
+package net.mamoe.mirai.network.handler
+
+import net.mamoe.mirai.network.LoginSession
+import net.mamoe.mirai.network.packet.ServerPacket
+import java.io.Closeable
+import java.util.*
+import kotlin.NoSuchElementException
+
+/**
+ * 数据包(接受/发送)处理器
+ */
+abstract class PacketHandler(
+        val session: LoginSession
+) : Closeable {
+    abstract fun onPacketReceived(packet: ServerPacket)
+
+    override fun close() {
+
+    }
+}
+
+class PacketHandlerNode<T : PacketHandler>(
+        val clazz: Class<T>,
+        val instance: T
+)
+
+infix fun PacketHandler.to(handler: PacketHandler): PacketHandlerNode<PacketHandler> {
+    return PacketHandlerNode(handler.javaClass, handler)
+}
+
+class PacketHandlerList : LinkedList<PacketHandlerNode<*>>() {
+
+    fun <T : PacketHandler> get(clazz: Class<T>): T {
+        this.forEach {
+            if (it.clazz == clazz) {
+                @Suppress("UNCHECKED_CAST")
+                return@get it.instance as T
+            }
+        }
+
+        throw NoSuchElementException()
+    }
+}
diff --git a/mirai-core/src/main/java/net/mamoe/mirai/network/packet/SKey.kt b/mirai-core/src/main/java/net/mamoe/mirai/network/packet/SKey.kt
index 30f8c16b3..1e4d515ab 100644
--- a/mirai-core/src/main/java/net/mamoe/mirai/network/packet/SKey.kt
+++ b/mirai-core/src/main/java/net/mamoe/mirai/network/packet/SKey.kt
@@ -6,6 +6,8 @@ import java.io.DataInputStream
 
 
 /**
+ * SKey 用于 http api
+ *
  * @author Him188moe
  */
 @ExperimentalUnsignedTypes
diff --git a/mirai-core/src/main/java/net/mamoe/mirai/network/packet/login/LoginState.kt b/mirai-core/src/main/java/net/mamoe/mirai/network/packet/login/LoginState.kt
index 5f3f8b66f..f5ccba7d6 100644
--- a/mirai-core/src/main/java/net/mamoe/mirai/network/packet/login/LoginState.kt
+++ b/mirai-core/src/main/java/net/mamoe/mirai/network/packet/login/LoginState.kt
@@ -34,11 +34,6 @@ enum class LoginState {
      */
     TAKEN_BACK,
 
-    /**
-     * 需要验证码登录
-     */
-    VERIFICATION_CODE,
-
     /**
      * 未知. 更换服务器或等几分钟再登录可能解决.
      */
diff --git a/mirai-core/src/main/java/net/mamoe/mirai/utils/MiraiLogger.kt b/mirai-core/src/main/java/net/mamoe/mirai/utils/MiraiLogger.kt
index 349817f16..04b245e70 100644
--- a/mirai-core/src/main/java/net/mamoe/mirai/utils/MiraiLogger.kt
+++ b/mirai-core/src/main/java/net/mamoe/mirai/utils/MiraiLogger.kt
@@ -1,6 +1,8 @@
 package net.mamoe.mirai.utils
 
 import net.mamoe.mirai.Bot
+import net.mamoe.mirai.network.packet.ServerPacket
+import net.mamoe.mirai.network.packet.goto
 import java.text.SimpleDateFormat
 import java.util.*
 
@@ -11,20 +13,20 @@ import java.util.*
  * @author NaturalHG
  */
 object MiraiLogger {
-    infix fun log(o: Any?) = info(o)
-    infix fun println(o: Any?) = info(o)
-    infix fun info(o: Any?) = this.print(o.toString(), LoggerTextFormat.RESET)
+    fun log(o: Any?) = info(o)
+    fun println(o: Any?) = info(o)
+    fun info(o: Any?) = this.print(o.toString(), LoggerTextFormat.RESET)
 
 
-    infix fun error(o: Any?) = this.print(o.toString(), LoggerTextFormat.RED)
+    fun error(o: Any?) = this.print(o.toString(), LoggerTextFormat.RED)
 
-    infix fun notice(o: Any?) = this.print(o.toString(), LoggerTextFormat.LIGHT_BLUE)
+    fun notice(o: Any?) = this.print(o.toString(), LoggerTextFormat.LIGHT_BLUE)
 
-    infix fun success(o: Any?) = this.print(o.toString(), LoggerTextFormat.GREEN)
+    fun success(o: Any?) = this.print(o.toString(), LoggerTextFormat.GREEN)
 
-    infix fun debug(o: Any?) = this.print(o.toString(), LoggerTextFormat.YELLOW)
+    fun debug(o: Any?) = this.print(o.toString(), LoggerTextFormat.YELLOW)
 
-    infix fun catching(e: Throwable) {
+    fun catching(e: Throwable) {
         e.printStackTrace()
         /*
         this.print(e.message)
@@ -39,21 +41,26 @@ object MiraiLogger {
     }
 }
 
-infix fun Bot.log(o: Any?) = info(o)
-infix fun Bot.println(o: Any?) = info(o)
-infix fun Bot.info(o: Any?) = print(this, o.toString(), LoggerTextFormat.RESET)
+fun Bot.log(o: Any?) = info(o)
+fun Bot.println(o: Any?) = info(o)
+fun Bot.info(o: Any?) = print(this, o.toString(), LoggerTextFormat.RESET)
 
-infix fun Bot.error(o: Any?) = print(this, o.toString(), LoggerTextFormat.RED)
+fun Bot.error(o: Any?) = print(this, o.toString(), LoggerTextFormat.RED)
 
-infix fun Bot.notice(o: Any?) = print(this, o.toString(), LoggerTextFormat.LIGHT_BLUE)
+fun Bot.notice(o: Any?) = print(this, o.toString(), LoggerTextFormat.LIGHT_BLUE)
 
-infix fun Bot.purple(o: Any?) = print(this, o.toString(), LoggerTextFormat.PURPLE)
+fun Bot.purple(o: Any?) = print(this, o.toString(), LoggerTextFormat.PURPLE)
 
-infix fun Bot.cyanL(o: Any?) = print(this, o.toString(), LoggerTextFormat.LIGHT_CYAN)
+fun Bot.cyanL(o: Any?) = print(this, o.toString(), LoggerTextFormat.LIGHT_CYAN)
+fun Bot.success(o: Any?) = print(this, o.toString(), LoggerTextFormat.GREEN)
 
-infix fun Bot.success(o: Any?) = print(this, o.toString(), LoggerTextFormat.GREEN)
+fun Bot.debug(o: Any?) = print(this, o.toString(), LoggerTextFormat.YELLOW)
 
-infix fun Bot.debug(o: Any?) = print(this, o.toString(), LoggerTextFormat.YELLOW)
+fun Bot.debugPacket(packet: ServerPacket) {
+    debug("Packet=$packet")
+    debug("Packet size=" + packet.input.goto(0).readAllBytes().size)
+    debug("Packet data=" + packet.input.goto(0).readAllBytes().toUHexString())
+}
 
 
 @Synchronized