websocket
原文地址 juejin.cn
通过观察我的更新频率,你可能会得出我是一个鸽子的结论。不过请听我狡辩一下。最近我沉迷于编写服务器端的应用,因为虽然我们说Android使用Java,但实际上还有一些小技巧。今天,我决定痛改前非,并为大家带来一篇关于Android WebSocket的教程。当然,我们将从零开始,一步一步地构建它。(咕咕咕)一起学习、一起进步。如果写的不好,或者有错误之处,恳请在评论、私信、邮箱指出,万分感谢🙏
2、WebScoket
它的主要功能是允许服务器主动向客户端推送信息,同时也允许客户端主动向服务器发送信息。
WebSocket是一种基于TCP的全双工通信协议,通过它可以在客户端和服务器之间建立一个持久的连接,实现实时的双向数据传输。与传统的HTTP请求-响应模式不同,WebSocket提供了更高效、实时性更强的通信方式。
由于WebSocket是一种协议,因此不能直接使用。但是幸运的是,我们有许多成熟的开源库和框架可以帮助我们轻松地使用WebSocket功能。
- OKHttp:OKHttp是一个强大的网络库,提供了对WebSocket的完整支持。它具有良好的文档和活跃的维护,是许多Android开发者首选的WebSocket方案。
- Java-WebSocket:Java-WebSocket是一个纯Java实现的WebSocket库,可以在Android项目中使用。它具有简单易用的API,并支持WebSocket协议的各种功能。
- AndroidAsync:AndroidAsync是一个基于Java NIO的异步网络库,提供了对WebSocket的支持。它具有简洁的API和高性能,适用于处理高并发的WebSocket连接。
- Autobahn Android:Autobahn Android是基于Autobahn库的Android版本,它实现了WebSocket协议的客户端和服务器端功能。它支持高级特性如RPC(远程过程调用)和发布-订阅模式。
这里我选择OKHttp,如果你问我为什么。我只能说,正经人都在用Retrofit,干嘛不用OKHttp呢。
3、OKHttp WebScoket
- 添加依赖项到你的build.gradle(随便选个版本吧)。
arduino复制代码`implementation 'com.squareup.okhttp3:okhttp:4.9.3' //or implementation("com.squareup.okhttp3:okhttp:4.9.3")`
-
创建WebSocket客户端
相信如果你了解过Retrofit或者OKHttp,这是非常简单的
scss复制代码`private val wsHttpClient by lazy { OkHttpClient.Builder() .pingInterval(10, TimeUnit.SECONDS) // 设置 PING 帧发送间隔 .build() } val request = Request.Builder() .url("ws://xixixixixix") .build()` -
建立一个WS连接~
kotlin复制代码`wsHttpClient.newWebSocket(request, object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { super.onOpen(webSocket, response) Log.i(TAG,"WS connection successful") // WebSocket 连接建立 } override fun onMessage(webSocket: WebSocket, text: String) { super.onMessage(webSocket, text) Log.i(TAG,"openWs onMessage $text") // 收到服务端发送来的 String 类型消息 } override fun onMessage(webSocket: WebSocket, bytes: ByteString) { super.onMessage(webSocket, bytes) // 收到服务端发送来的 ByteString 类型消息 Log.i(TAG,"openWs onMessage $text") } override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { super.onClosing(webSocket, code, reason) Log.i(TAG,"openWs onClosing") // 收到服务端发来的 CLOSE 帧消息,准备关闭连接 } override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { super.onClosed(webSocket, code, reason) Log.i(TAG,"openWs onClosed") // WebSocket 连接关闭 } override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { super.onFailure(webSocket, t, response) Log.i(TAG,"openWs onFailure : Ws连接失败 ${response}") // WebSocket连接失败 } })`
对于接收消息是onMessage,不过我们暂且按下不表
-
发送消息至服务端
可以看到,我们建立连接成功后,在onOpen方法中会返回webSocket: WebSocket,使用这个对象,你可以向服务器发送消息
less复制代码`// 发送文本消息 webSocket.send("Hello, WebSocket!") // 发送二进制消息 webSocket.send("Hello, WebSocket!".toByteString())`
记得在使用WebSocket发送消息时,要确保连接已经成功建立。否则,如果在连接未建立或已关闭的状态下尝试发送消息,将会导致错误。
4、onMessage
在使用OKHttp WebSocket时,一旦成功建立WebSocket连接并接收到onOpen
方法的回调后,基本上一个WebSocket的设置就完成了。
然而,在处理服务器发送的消息时,你需要将onMessage
中接收到的消息进行适当的分发,以便根据你的需求进行后续处理。这样可以确保接收到的消息能够被正确地传递和处理,从而实现更灵活的消息处理机制。
一般而言你需要如此:(接下来的代码不要抄!!!!)
1)监听器
-
创建WebSocket监听器接口:在WebSocket通信类外部创建一个接口,用于定义
onMessage
回调方法。例如:kotlin复制代码`interface OnListener { fun onMessageReceived(message: String) }` -
在WebSocket通信类中持有监听器引用:在WebSocket通信类中添加一个成员变量来持有
WebSocketListener
接口的引用。例如:kotlin复制代码`class WebSocketClient(private val webSocketListener: WebSocketListener) { // ... }` -
在WebSocket通信类的
onMessage
回调中调用监听器方法:在WebSocket通信类的onMessage
回调中,将接收到的消息传递给监听器的方法。例如:kotlin复制代码`wsHttpClient.newWebSocket(request, object : WebSocketListener() { // ... override fun onMessage(webSocket: WebSocket, text: String) { // 收到文本消息 webSocketListener.onMessageReceived(text) } // ... })` -
在另一个类中实现WebSocketListener接口:在另一个类中实现WebSocketListener接口,并实现
onMessageReceived
方法来接收消息。例如:kotlin复制代码`class MyWebSocketListener : WebSocketListener { override fun onMessageReceived(message: String) { // 处理接收到的消息 // ... } }` -
使用自定义的WebSocketListener:在您的应用程序中,创建一个实例化了自定义的WebSocketListener的WebSocketClient对象。例如:
ini复制代码`val webSocketListener = MyWebSocketListener() val webSocketClient = WebSocketClient(webSocketListener)`
以上1-5,都是骗你的,因为不可能只有一个方法或类可以处理消息,所以可以使用观察者模式或事件总线来实现。
2)观察者模式
忘掉上面的代码
- 创建一个消息事件类:创建一个表示WebSocket消息的事件类。
kotlin复制代码`data class MessageEvent(val message: String)`
- 创建一个WebSocket事件观察者接口:创建一个WebSocket事件观察者接口,定义用于接收WebSocket消息的方法。
kotlin复制代码`interface OnMessageListener { fun onWebSocketMessage(event: MessageEvent) }`
- 在WebSocket通信类中添加观察者列表:在WebSocket通信类中添加一个观察者列表和相应的方法,用于添加、移除和通知观察者。
kotlin复制代码`object WsManager { private val observers = mutableListOf<OnMessageListener>() fun addWebSocketListener(observer: OnMessageListener) { observers.add(observer) } fun removeWebSocketListener(observer: OnMessageListener) { observers.remove(observer) } private fun notifyWebSocketMessage(message: String) { val event = MessageEvent(message) for (observer in observers) { observer.onWebSocketMessage(event) } } // WebSocket onMessage回调 private val webSocketListener = object : WebSocketListener() { override fun onMessage(webSocket: WebSocket, text: String) { notifyWebSocketMessage(text) } } // 其他WebSocket相关方法 }`
- 在接收WebSocket消息的方法或类中实现OnMessageListener接口:在需要处理WebSocket消息的方法或类中,实现OnMessageListener接口,并在
onWebSocketMessage
方法中处理消息。
kotlin复制代码`class MyWebSocketActivity : OnMessageListener { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WsManager.addWebSocketListener(this) } override fun onDestroy() { super.onDestroy() WsManager.removeWebSocketListener(this) } override fun onWebSocketMessage(event: MessageEvent) { val message = event.message // 处理WebSocket消息 // ... } }`
现在,当WebSocket接收到消息时,WebSocket通信类将通知所有注册的观察者,它们将独立地接收和处理WebSocket消息。
这种方法使用观察者模式将onMessage
回调的消息分发给多个方法或类,让它们可以独立地处理消息。您可以根据需要添加更多的观察者,并在notifyWebSocketMessage
方法中通知它们。
当然,这只是一个未完善的版本,你还需要考虑多线程的,粘性消息等。我们接着说,总会说完的
3)事件总线
1、EventButs
使用事件总线来实现WebSocket消息的分发,可以考虑使用第三方库,如EventBus或RxJava,来简化事件的发布和订阅过程。
以下是使用EventBus库的示例:
-
首先,确保已将EventBus库添加到您的项目中。您可以在Gradle文件的dependencies块中添加以下行:
scss复制代码`implementation("org.greenrobot:eventbus:3.2.0")` -
然后,定义一个WebSocket消息事件类:
kotlin复制代码`data class WebSocketMessageEvent(val message: String)` -
将WebSocket接收到的消息发布到事件总线:
kotlin复制代码`wsHttpClient.newWebSocket(request, object : WebSocketListener() { // ... override fun onMessage(webSocket: WebSocket, text: String) { // 收到文本消息 EventBus.getDefault().post(WebSocketMessageEvent(message)) } // ... })` -
在需要处理WebSocket消息的方法或类中,订阅WebSocket消息事件并定义相应的处理方法:
kotlin复制代码`class MyWebSocketActivity : OnMessageListener { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) EventBus.getDefault().register(this) } override fun onDestroy() { super.onDestroy() EventBus.getDefault().unregister(this) } // 定义处理WebSocket消息事件的方法 @Subscribe(threadMode = ThreadMode.MAIN) fun onWebSocketMessage(event: WebSocketMessageEvent) { val message = event.message println("Received message: $message") // 处理接收到的消息 // ... } }`
2、自定义EventBus
为了更加直观的,看到分发的原理,我们自己写一个,嘻嘻嘻。
-
WsModel
EventBus是通过对象类型来判断是否需要接收,定义消息结构。但是我想通过自己控制传入一个标识,所以我们需要一个
kotlin复制代码`enum class WsModel { MAIN, CHAT }`
-
@WsSubscribe
首先我们需要一个订阅的注解
less复制代码`@Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.FUNCTION) annotation class WsSubscribe(val model: WsModel, val priority: Int = 0)`
使用的话就很简单了
kotlin复制代码`@WsSubscribe(WsModel.CHAT) fun onChatMessage(any: Any) {}`
-
改造WsManger
我们需要改造下WsManger,用来register、unRegister方法
kotlin复制代码`fun register(subscriber: Any) {} @Synchronized fun unregister(subscriber: Any) {}`
-
WsSubscriberMethod
在开始之前register,我们创建一个
WsSubscriberMethod
- 存储方法信息:
WsSubscriberMethod
对象用于存储订阅方法的相关信息,包括方法名、参数类型、线程模式等。通过将这些信息封装到WsSubscriberMethod
对象中,可以方便地管理和传递方法相关的信息。 - 方法调用:在事件发布时,需要找到对应的订阅方法,并将事件传递给这些方法进行处理。
WsSubscriberMethod
对象中存储了方法的相关信息,包括方法名和参数类型,可以在事件发布时快速找到对应的方法并进行调用。 - 线程调度:
WsSubscriberMethod
对象还可以存储线程模式信息,指示订阅方法应该在哪个线程上执行。这可以用于实现事件的异步处理或跨线程通信。通过存储线程模式信息,可以确保订阅方法在适当的线程上执行,避免了线程安全性问题或阻塞主线程的情况。 - 注册和取消注册:
WsSubscriberMethod
对象可以用于注册和取消注册订阅方法。在注册过程中,可以通过WsSubscriberMethod
对象将订阅方法与事件类型建立关联,从而实现事件的订阅。在取消注册时,可以通过WsSubscriberMethod
对象找到对应的订阅方法,并将其从事件系统中移除。
- 存储方法信息:
-
Register
注册订阅者对象,将对象中带有 @WsSubscribe 注解的订阅方法添加到订阅列表中
- 系统获取注册对象中的所有方法。
- 对于每个方法,系统检查是否存在
@WsSubscribe
注解。 - 如果存在
@WsSubscribe
注解,则提取相关的事件类型信息。 - 将事件类型和对应的方法建立映射关系,将方法注册为事件处理程序或订阅者。
看代码
kotlin复制代码`/** * @param subscriber 要注册的订阅者对象 */ fun register(subscriber: Any) { // 查找订阅者对象中的订阅方法 val subscriberMethods: List<WsSubscriberMethod> = findSubscriberMethods(subscriber) // 对订阅操作进行同步,避免多线程竞争问题 synchronized(this) { // 遍历订阅方法列表,将每个方法添加到订阅列表中 for (subscriberMethod in subscriberMethods) { subscribe(subscriber, subscriberMethod) } } }` 先看看神奇的
findSubscriberMethods
kotlin复制代码`/** * 查找对象中带有 @WsSubscribe 注解的订阅方法并返回它们的信息列表 * * @param obj 要查找的对象 * @return 订阅方法的信息列表 */ private fun findSubscriberMethods(obj: Any): List<WsSubscriberMethod> { // 存储订阅方法的列表 val subscribers = mutableListOf<WsSubscriberMethod>() // 获取对象的类信息 val objClass = obj.javaClass // 获取对象类中声明的所有方法 val declaredMethods = objClass.declaredMethods // 遍历每个方法 for (method in declaredMethods) { // 获取方法的修饰符 val modifiers = method.modifiers // 构造完整的方法名,格式为 "类名.方法名" val methodName = "${method.declaringClass.name}.${method.name}" // 检查方法的修饰符,满足条件才进行处理 if ((modifiers and Modifier.PUBLIC) != 0 && (modifiers and MODIFIERS_IGNORE) == 0) { // 获取方法的参数类型列表 val parameterTypes = method.parameterTypes // 检查方法的参数个数,必须为 1 if (parameterTypes.size == 1) { // 获取方法上的 @WsSubscribe 注解 val subscribeAnnotation: WsSubscribe? = method.getAnnotation(WsSubscribe::class.java) // 检查注解是否存在 if (subscribeAnnotation != null) { // 将订阅方法的信息封装为 WsSubscriberMethod 对象,并添加到列表中 subscribers.add( WsSubscriberMethod( subscribeAnnotation.model, method, subscribeAnnotation.priority, ) ) } } else if (method.isAnnotationPresent(WsSubscribe::class.java)) { // 参数个数不为 1,但方法上存在 @WsSubscribe 注解,抛出异常(为什么一定要一个呢,不是两个呢,不是0个呢?你可以思考下,嘻嘻) throw IllegalArgumentException("@WsSubscribe method $methodName must have exactly 1 parameter but has ${parameterTypes.size}") } } else { // 方法的修饰符不符合要求,但方法上存在 @WsSubscribe 注解,抛出异常 if (method.isAnnotationPresent(WsSubscribe::class.java)) { throw IllegalArgumentException("$methodName is a illegal @WsSubscribe method: must be public, non-static, and non-abstract") } } } // 返回订阅方法的信息列表 return subscribers }` 结合注释,你肯定可以的啦。
你可能注意到了
subscriptionsByModelType
,他的声明如下:用于存储按照WsModel
类型分类的订阅列表。方便到时候根据WsModel分发~swift复制代码`private val subscriptionsByModelType: MutableMap<WsModel, CopyOnWriteArrayList<WsSubscription>> = EnumMap(WsModel::class.java)` 当然了,register中还有一个方法subscribe,是在
findSubscriberMethods
后,对返回值进行遍历那么它是kotlin复制代码`private fun subscribe(subscriber: Any, subscriberMethod: WsSubscriberMethod) { // 获取订阅的模型类型 val modelType: WsModel = subscriberMethod.modelType // 创建新的订阅对象 val newSubscription = WsSubscription(subscriber, subscriberMethod) // 获取模型类型对应的订阅列表 var subscriptions: CopyOnWriteArrayList<WsSubscription>? = subscriptionsByModelType[modelType] // 如果订阅列表为空,则创建一个新的订阅列表并将其关联到模型类型 if (subscriptions == null) { subscriptions = CopyOnWriteArrayList<WsSubscription>() subscriptionsByModelType[modelType] = subscriptions } else { // 如果订阅列表不为空,检查是否已存在相同的订阅对象,若存在则抛出异常 if (subscriptions.contains(newSubscription)) { throw IllegalArgumentException("Subscriber ${subscriber.javaClass} already registered to event $modelType") } } // 在合适的位置插入新的订阅对象,根据优先级从高到低排序 val size = subscriptions.size for (i in 0..size) { if (i == size || subscriberMethod.priority > subscriptions[i].subscriberMethod.priority) { subscriptions.add(i, newSubscription) break } } // 更新订阅者订阅的事件列表 var subscribedEvents: MutableList<WsModel>? = typesBySubscriber[subscriber] if (subscribedEvents == null) { subscribedEvents = ArrayList() typesBySubscriber[subscriber] = subscribedEvents } subscribedEvents.add(modelType) }` 总的来说就是将一个订阅者对象和其对应的订阅方法添加到事件总线中进行订阅。具体来说:
-
获取订阅方法对应的模型类型。
-
创建一个新的订阅对象,将订阅者对象和订阅方法封装起来。
-
根据模型类型从 subscriptionsByModelType 中获取对应的订阅列表。
- 如果订阅列表不存在,就创建一个新的
CopyOnWriteArrayList<WsSubscription>
并将其关联到模型类型。 - 如果订阅列表已经存在,检查是否已经存在相同的订阅对象,如果存在则抛出异常。
- 如果订阅列表不存在,就创建一个新的
-
根据订阅方法的优先级,将新的订阅对象插入到订阅列表中的合适位置,以保持订阅方法的优先级有序。
-
更新订阅者对象的订阅事件列表,将模型类型添加到该列表中。
通过执行这些步骤,订阅者就能成功订阅指定的事件,并且事件总线会将订阅者的订阅方法按优先级有序地存储起来,以便在事件发布时按照订阅者的要求进行调用。
-
UnRegister
Register都写好了,Unregister就更简单了
-
从
typesBySubscriber
中获取订阅者对象已经订阅的事件类型列表。 -
如果获取到的
subscribedTypes
不为 null,则表示该订阅者对象有进行过订阅操作。- 遍历订阅者对象已经订阅的事件类型列表。
- 对于每个事件类型,调用
unsubscribeByEventType(subscriber, eventType)
方法进行取消订阅操作。 - 从
typesBySubscriber
中移除该订阅者对象的订阅记录。
-
如果获取到的
subscribedTypes
为 null,则表示该订阅者对象在事件总线中未进行过注册。
kotlin复制代码`@Synchronized fun unregister(subscriber: Any) { val subscribedTypes: List<WsModel>? = typesBySubscriber[subscriber] if (subscribedTypes != null) { for (eventType in subscribedTypes) { unsubscribeByEventType(subscriber, eventType) } typesBySubscriber.remove(subscriber) } else { Log.i( TAG, "WsManager Subscriber to unregister was not registered before: ${subscriber.javaClass}" ) } }` -
-
Post
嘻嘻嘻,注册、反注册都好了,就只有Post啦;那么如何Post呢
首先我们如何将事件发布给订阅者的方法呢?
kotlin复制代码`private fun postToSubscription(subscription: WsSubscription,event: Any) { try { // 使用反射调用订阅方法来处理事件 subscription.subscriberMethod.method.invoke(subscription.subscriber, event) } catch (e: InvocationTargetException) { Log.i(TAG, "WsManager $e") // 处理订阅者异常(暂未实现) } catch (e: IllegalAccessException) { throw IllegalStateException("Unexpected exception", e) } }` 直接使用subscriberMethod.method.invoke就完事了~
接着直接在for
循环中调用postToSubscription()
方法,那么就完事了。NO,NO,NO
在多线程环境下,直接在for
循环中调用postToSubscription()
方法可能会导致并发访问的问题,从而导致线程安全性问题。因此,使用PostingThreadState
类来维护当前线程的发布状态,以确保线程安全性。
csharp复制代码`/** * 用于保存当前线程的发布状态的内部类。 */ internal class PostingThreadState { val eventQueue: MutableList<Any> = arrayListOf() var isPosting = false var isMainThread = false var subscription: WsSubscription? = null var event: Any? = null var canceled = false }`
kotlin复制代码`/** * 将单个事件发布给其订阅者,针对给定的事件类型。 * 如果存在订阅者,返回true;否则返回false。 */ private fun postSingleEventForEventType( event: Any, postingState: PostingThreadState, eventModel: WsModel ): Boolean { var subscriptions: CopyOnWriteArrayList<WsSubscription>? synchronized(this) { subscriptions = subscriptionsByModelType[eventModel] } Log.i(TAG, "subscriptions $subscriptions") if (!subscriptions.isNullOrEmpty()) { // 遍历订阅者列表,依次发布事件 for (subscription in subscriptions!!) { postingState.event = event postingState.subscription = subscription // 发布事件并检查是否被取消 val aborted: Boolean = try { postToSubscription(subscription, event) postingState.canceled } finally { postingState.event = null postingState.subscription = null postingState.canceled = false } // 如果事件发布被取消,则终止发布过程 if (aborted) { break } } return true } return false }`
kotlin复制代码`/** * 将单个事件发布给其订阅者,针对特定的事件类型。 */ @Throws(Error::class) private fun postSingleEvent(model: WsModel, event: Any, postingState: PostingThreadState) { // 尝试发布事件并返回是否找到订阅者 val subscriptionFound: Boolean = postSingleEventForEventType(event, postingState, model) // 如果没有订阅者,则记录日志 if (!subscriptionFound) { Log.i(TAG, "WsManager No subscribers registered for event $model") } }`
post代码在这
kotlin复制代码`/** * 将给定的事件发布到事件总线。 */ private fun post(model: WsModel, event: Any) { // 获取当前线程的发布状态 val postingState: PostingThreadState = currentPostingThreadState.get() as PostingThreadState // 获取事件队列 val eventQueue: MutableList<Any> = postingState.eventQueue // 将事件添加到队列中 eventQueue.add(event) // 如果当前没有正在发布事件,则开始进行事件发布 if (!postingState.isPosting) { // 判断是否在主线程中发布事件 postingState.isMainThread = Looper.getMainLooper() == Looper.myLooper() postingState.isPosting = true // 检查发布状态是否被取消 if (postingState.canceled) { throw IllegalArgumentException("Internal error. Abort state was not reset") } try { // 循环处理事件队列中的事件 while (eventQueue.isNotEmpty()) { postSingleEvent(model, eventQueue.removeAt(0), postingState) } } finally { // 发布完成后重置发布状态 postingState.isPosting = false postingState.isMainThread = false } } }`
结合注释看,一下子就明白了~~
好了现在我们只需要在WsManager中调用一下就完事了(这里我和服务器约定的是在返回数据的data中带上model:WsModel),总之这里其实和请求数据接收一个意思。
大概长这样
css复制代码`{ "code": 200, "msg": "string", "data": { "model": "CHAT" } }`
kotlin复制代码`wsHttpClient.newWebSocket(request, object : WebSocketListener() { // ... override fun onMessage(webSocket: WebSocket, text: String) { try { val msg: BaseResponse<WsMessage>? = text.toObject() msg?.getData()?.model?.let { post(it, msg) } ?: run { LogW("openWs onMessage error msg?.getData()?.model?") } } catch (e: ClassCastException) { LogW("openWs onMessage error $e") } } // ... })`
这样你的@WsSubscribe就可以收到对应model类型的消息啦。
5、下个篇章
因为篇幅原因,我们先到这,哈,还有很关键的重连策略没有说,我还没写完,马上~咕咕咕。
如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏
代码都在这里啦,可能有所出入,大差不差啦
本文作者:cps666
本文链接:https://www.cnblogs.com/cps666/p/17983066
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步