I.はじめに
前のプロジェクトのグループチャットはデータベースで直接操作されていて、経験が非常に悪く、メッセージの即時フィードバックを提供するのが困難だったので、最終的にはTencentのIMを使用してグループチャットアクセスを完了することを検討しましたが、途中で少しでこぼこしていて、アクセスが完了してからわかりました。試用版ではグループチャットは20人しかいませんでした。当時、試用版は100人のユーザーをサポートしているのを見ました。グループチャットには20人のユーザーがいます。同時に50人のユーザーのオンラインチャットをサポートするだけでは不十分です。それだけでは十分ではありません。以下では、2つの実装スキームへのアクセスを紹介します。テキストは、まもなく開始されます~~
2.TencentIMアクセス
Tencent Cloud IMの公式ウェブサイト。ここにアクセスすると、グループチャットに関連するAPIが抽出されます。詳細については、ドキュメントを参照してください(時間があれば、QQと同様のシンプルなチャットプラットフォームを実現できます)。
https://cloud.tencent.com/document/product/269/42440
复制代码
1.準備
-
需要分析
QQでグループチャットと同様の機能を実装する必要があります。メッセージの受信、メッセージの送信、および履歴レコードの取得という3つの単純な機能を開発するだけで済みます。
-
アプリを作成する
この部分はデモンストレーションされません。非常に単純です。作成すると、次の図のようになります。
試用版は、グループチャットで100人のユーザーと20人のユーザーをサポートし、7日間無料のクラウドストレージを提供し、同時に複数のIMインスタンスを作成できます。使用法を学んでいる場合は、試用版で十分であり、商用化を検討しています。プロフェッショナルバージョンとアルティメットバージョン
-
依存関係の統合
Gradle統合を使用すると、sdk統合も使用できます。ここでは、新しいバージョンのsdkが統合に使用されます。
api 'com.tencent.imsdk:imsdk-plus:6.1.2155' 复制代码
2.初期化作業
IMを初期化する
-
インスタンスを作成する
パラメータにコールバックがあり、ここのオブジェクトはJavaの匿名クラスと同等です
val config = V2TIMSDKConfig() V2TIMManager.getInstance() .initSDK(this, sdkId, config, object : V2TIMSDKListener() { override fun onConnecting() { // 正在连接到腾讯云服务器 Log.e("im", "正在连接到腾讯云服务器") } override fun onConnectSuccess() { // 已经成功连接到腾讯云服务器 Log.e("im", "已经成功连接到腾讯云服务器") } override fun onConnectFailed(code: Int, error: String) { // 连接腾讯云服务器失败 Log.e("im", "连接腾讯云服务器失败") } }) 复制代码
-
ログインクレデンシャルを生成する
この部分は、クライアントによって生成されたコードとサーバー側のコードをすばやく公式に提供します。公式Webサイトで見つけることができます。テストするとき、クライアント側のコードの背後にある公式プロジェクトがサーバーに最適に展開されていると見なすことができます。処理中。この部分はここで説明します。目を覚ますと、サーバー側に2つのファイルがあります。当時ははっきりと見えませんでした。長い間関数を探していたところ、ようやくjavaファイルであることがわかりました。読むのを忘れたか、同じレベルのディレクトリにありました。他のAPIもBase64URLクラスを再利用しているはずです。
同時に、職員は資格情報を生成および検証するためのツールも提供します
ユーザーログイン
この部分は、パラメータを渡すだけで済みます
V2TIMManager.getInstance().login(currentUser,sig, object : V2TIMCallback {
override fun onSuccess() {
Log.e("im", "${currentUser}登录成功")
}
override fun onError(code: Int, desc: String?) {
Log.e("im", "${currentUser}登录失败,错误码为:${code},具体错误:${desc}")
}
})
复制代码
- currentUserはユーザーのIDです
- sigはユーザーのログイン資格情報です
- V2TIMCallbackコールバックのクラス
3.グループチャット関連
グループチャットを作成する
创建群聊的时候需要注意几个方面的问题
-
群聊类别(groupType)
需要审批还是不需要,最大的容纳用户数,未支不支持未入群查看群聊消息,详见下图
其中社群其实挺符合我的需求的,但有个问题,社群需要付费才能开通(还挺贵),所以最后选择了Meeting类型的群组
-
群聊资料设置
群聊id(groupID)是没有字母数字和特殊符号(当然不能中文)都是可以的,群聊名字(groupName),群聊介绍(introduction)等等,还有就是设置初始的成员,可以将主管理员加入(这里稍微有点疑惑的就是创建群聊,居然没有默认添加创建人)
-
创建群聊的监听回调
这里传入的参数就是上述的groupInfo和memberInfoList,主要用于初始化群聊,然后有一个回调的参数监听创建结果
val group = V2TIMGroupInfo()
group.groupName = "test"
group.groupType = "Meeting"
group.introduction = "more to show"
group.groupID = "test"
val memberInfoList: MutableList<V2TIMCreateGroupMemberInfo> = ArrayList()
val memberA = V2TIMCreateGroupMemberInfo()
memberA.setUserID("master")
memberInfoList.add(memberA)
V2TIMManager.getGroupManager().createGroup(
group, memberInfoList, object : V2TIMValueCallback<String?> {
override fun onError(code: Int, desc: String) {
// 创建失败
Log.e("im","创建失败${code},详情:${desc}")
}
override fun onSuccess(groupID: String?) {
// 创建成功
Log.e("im","创建成功,群号为${groupID}")
}
})
复制代码
加入群聊
这部分只需要一个回调监听即可,这里没有login的用户的原因是,默认使用当前登录的id加群,所以一个很重要的前提是登录
V2TIMManager.getInstance().joinGroup("群聊ID","验证消息",object :V2TIMCallback{
override fun onSuccess() {
Log.e("im","加群成功")
}
override fun onError(p0: Int, p1: String?) {
Log.e("im","加群失败")
}
})
复制代码
4.消息收发相关
发送消息
这里发送消息是采用高级接口,发送的消息类型比较丰富,并且支持自定义消息类型,所以这里采用了高级消息收发接口
首先创建消息,这里是创建自定义消息,其他消息同理
val myMessage = "一段自定义的json数据"
//由于这里自定义消息接收的参数为byteArray类型的,所以进行一个转换
val messageCus= V2TIMManager.getMessageManager().createCustomMessage(myMessage.toByteArray())
复制代码
发送消息,这里需要设置一些参数
messageCus即转换过后的byte类型的数据,toUserId即接收方,这里为群聊的话,用空字符串置空即可,groupId即群聊的ID,如果是单聊的话,这里同样置空字符串即可,weight即你的消息被接收到的权重(不保证全部都能收到,这里设置权重确定优先级),onlineUserOnly即是否只有在线的用户可以收到,这个的话设置false即可,offlinePushInfo这个只有旗舰版才有推送消息的功能,所以这里设置null即可,然后就是一个发送消息的回调
V2TIMManager.getMessageManager().sendMessage(messageCus,toUserId,groupId,weight,onlineUserOnly, offlinePushInfo,object:V2TIMSendCallback<V2TIMMessage>{
override fun onSuccess(message: V2TIMMessage?) {
Log.e("im","发送成功,内容为:${message?.customElem}")
//这里同时需要自己进行解析消息,需要转换成String类型的数据
val data = String(message?.customElem?.data)
...
}
override fun onError(p0: Int, p1: String?) {
Log.e("im","错误码为:${p0},具体错误:${p1}")
}
override fun onProgress(p0: Int) {
Log.e("im","处理进度:${p0}")
}
})
复制代码
获取历史消息
- groupId即群聊ID
- pullNumber即拉取消息数量
- lastMessage即上一次的消息,用于获取更多消息的定位
- V2TIMValueCallback即消息回调
这里关于lastMessage进行解释说明,这个参数可以设置成全局变量,然后一开始设置为null,然后获取到的消息列表的最后一条设置成lastMessage即可
V2TIMManager.getMessageManager().getGroupHistoryMessageList(
groupId,pullNumber,lastMessage,object:V2TIMValueCallback<List<V2TIMMessage>>{
override fun onSuccess(p0: List<V2TIMMessage>?) {
if (p0 != null) {
if (p0.isEmpty()){
Log.e("im","没有更多消息了")
"没有更多消息了".showToast()
}else {
//记录最后一条消息
lastMessage = p0[p0.size - 1]
for (msgIndex in p0.indices) {
//解析各种消息
when(p0[msgIndex].elemType){
V2TIMMessage.V2TIM_ELEM_TYPE_CUSTOM ->{
...
}
V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> {}
...
}
else -> {
...
}
}
}
}
}
}
override fun onError(p0: Int, p1: String?) {
....
}
})
复制代码
新消息的监听
这个主要用于新消息的接收和监听,同时需要自己对于各种消息的解析和相关处理
V2TIMManager.getMessageManager().addAdvancedMsgListener(object:V2TIMAdvancedMsgListener(){
override fun onRecvNewMessage(msg: V2TIMMessage?) {
Log.e("im","新消息${msg?.customElem}")
//这里针对多种消息类型有不同的处理方法
when(msg?.elemType){
V2TIMMessage.V2TIM_ELEM_TYPE_CUSTOM ->{
val message = msg.customElem?.data
...
}
V2TIMMessage.V2TIM_ELEM_TYPE_TEXT ->{
val message = msg.textElem.text
...
}
else -> {
"暂时不支持此消息的接收".showToast()
Log.e("im","${msg?.elemType}")
}
}
}
})
复制代码
至此接入部分就已经完成了,这里只是简单的介绍接入,还有更多的细节可以查看项目源码
三、WebSocket接入
这个需求和上面的是一样的,同时提供和上面腾讯IM类似功能的api,这部分涉及网络相关的api(不是非常专业),主要描述一些思路上的,具体代码不是很困难
1.WebSocket介绍
webSocket可以实现长连接,可以作为消息接收的即时处理的一个工具,采用ws协议或者wss协议(SSL)进行通信,腾讯IM的版本也推出了webSocket实现方案,webSocket主要解决的痛点就是服务端不能主动推送消息,代替之前轮询的实现方案
2.服务端相关
服务端采用springboot进行开发,同时也是使用kotlin进行编程
-
webSoket 依赖集成
下面是gradle的依赖集成
implementation "org.springframework.boot:spring-boot-starter-websocket" 复制代码
-
WebSocketConfig配置相关
@Configuration class WebSocketConfig { @Bean fun serverEndpointExporter(): ServerEndpointExporter { return ServerEndpointExporter() } } 复制代码
-
WebSocketServer相关
这部分代码是关键代码,里面重写了webSocket的四个方法,然后配置静态的变量和方法用于全局通信,下面给出一个框架
@ServerEndpoint("/imserver/{userId}") @Component class WebSocketServer { @OnOpen fun onOpen(session: Session?, @PathParam("userId") userId: String) { ... } @OnClose fun onClose() { ... } @OnMessage fun onMessage(message: String, session: Session?) { ... } @OnError fun onError(session: Session?, error: Throwable) { ... } //主要解决@Component和@Resource冲突导致未能自动初始化的问题 @Resource fun setMapper(chatMapper: chatMapper){ WebSocketServer.chatMapper = chatMapper } //这是发送消息用到的函数 @Throws(IOException::class) fun sendMessage(message: String?) { session!!.basicRemote.sendText(message) } //静态变量和方法 companion object { ... } } 复制代码
companion object
这里一个比较关键的变量就是webSocketMap存储用户的webSocket对象,后面将利用这个实现消息全员推送和部分推送
companion object { //统计在线人数 private var onlineCount: Int = 0 //用于存放每个用户对应的webSocket对象 val webSocketMap = ConcurrentHashMap<String, WebSocketServer>() //操作数据库的mapper对象的延迟初始化 lateinit var chatMapper:chatMapper //服务端主动推送消息的对外开放的方法 @Throws(IOException::class) fun sendInfo(message: String, @PathParam("userId") userId: String) { if (userId.isNotBlank() && webSocketMap.containsKey(userId)) { webSocketMap[userId]?.sendMessage(message) } else { println("用户$userId,不在线!") } } //在线统计 @Synchronized fun addOnlineCount() { onlineCount++ } //离线统计 @Synchronized fun subOnlineCount() { onlineCount-- } } 复制代码
@OnOpen
这个方法在websocket打开时执行,主要执行一些初始化和统计工作
@OnOpen fun onOpen(session: Session?, @PathParam("userId") userId: String) { this.session = session this.userId = userId if (webSocketMap.containsKey(userId)) { //包含此id说明此时其他地方开启了一个webSocket通道,直接kick下线重新连接 webSocketMap.remove(userId) webSocketMap[userId] = this } else { webSocketMap[userId] = this addOnlineCount() } println("用户连接:$userId,当前在线人数为:$onlineCount") } 复制代码
@OnClose
这个方法在webSocket通道结束时调用,执行下线逻辑和相关的统计工作
@OnClose fun onClose() { if (webSocketMap.containsKey(userId)) { webSocketMap.remove(userId) subOnlineCount() } println("用户退出:$userId,当前在线人数为:$onlineCount") } 复制代码
@OnMessage
这个方法用于处理消息分发,这里一般需要对消息进行一些处理,具体处理参考自定义消息的处理,这里是设计成群聊的方案,所以采用
@OnMessage fun onMessage(message: String, session: Session?) { if (message.isNotBlank()) { //解析发送的报文 val newMessage = ... //这里需要进行插入一条数据,做持久化处理,即未在线的用户也同样可以看到这条消息 chatMapper.insert(newMessage) //遍历所有的消息 webSocketMap.forEach { it.value.sendMessage(sendMessage.toMyJson()) } } } 复制代码
@OnError
发生错误调用的方法
@OnError fun onError(session: Session?, error: Throwable) { println("用户错误:$userId 原因: ${error.message}") error.printStackTrace() } 复制代码
sendMessage
此方法用于消息分发给各个客户端时调用的
fun sendMessage(message: String?) { session!!.basicRemote.sendText(message) } 复制代码
-
WebSocketController
这部分主要是实现服务端直接推送消息设计的,类似系统消息的设定
@PostMapping("/sendAll/{message}") fun sendAll(@PathVariable message: String):String{ //消息的处理 val newMessage = ... //需不要存储系统消息就看具体需求了 WebSocketServer.webSocketMap.forEach { WebSocketServer.sendInfo(sendMessage.toMyJson(), it.key) } return "ok" } @PostMapping("/sendC2C/{userId}/{message}") fun sendC2C(@PathVariable userId:String,@PathVariable message:String):String{ //消息的处理 val newMessage = ... WebSocketServer.sendInfo(newMessage, userId) return "ok" } 复制代码
至此服务端的讲解就结束了,下面就看看我们安卓客户端的实现了
3.客户端相关
-
依赖集成
集成java语言的webSocket(四舍五入就是Kotlin版本的)
implementation 'org.java-websocket:Java-WebSocket:1.5.2' 复制代码
-
实现部分
这部分的重写的方法和服务端差不多,但少了服务相关的处理,代码少了很多,这里需要提醒的一点就是,重写的这些方法都是子线程中运行的,不允许直接写入UI相关的操作,所以这里需要使用handle进行处理或者使用runOnUIThread
val userSocket = object :WebSocketClient(URI("wss://服务端地址:端口号/imserver/${userId}")){ override fun onOpen(handshakedata: ServerHandshake?) { //打开进行初始化的操作 } override fun onMessage(message: String?) { ... //这里做recyclerView的更新 } override fun onClose(code: Int, reason: String?, remote: Boolean) { //这里执行一个通知操作即可 ... } override fun onError(ex: Exception?) { ... } } userSocket.connect() //断开连接的话使用自带的reconnect重新连接即可 //需要注意的一点就是不能在重写方法里面执行这个操作 userSocket.reconnect() 复制代码
这里还有太多很多细节不能一一展示,但就总体而言是模仿上述腾讯IM实现的,具体的可以看项目地址
四、列表设计的一些细节
这里简单叙述一下列表设计的一些细节,这部分设计还是挺繁琐的
1.handle的使用
列表的更新时间和时机是取决于具体网络获取情况的,故需要一个全局的handle用于处理其中的消息,同时列表滑动行为不一样,这里需要注意的一个小问题,就是message最好是用一个发一个,不然可能出现内存泄漏的风险
- 下拉刷新,此时刷新完毕列表肯定就是在第一个item的位置不然就有点奇怪
- 首次获取历史消息,此时的场景应该是列表最后一个item
- 获取新消息,也是最后一个item
private val up = 1
private val down = 2
private val fail = 0
private val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: android.os.Message) {
when (msg.what) {
up -> {
viewBinding.chatRecyclerview.scrollToPosition(0)
viewBinding.swipeRefresh.isRefreshing = false
}
down ->{
viewBinding.chatRecyclerview.scrollToPosition(viewModel.chatList.size-1)
}
fail -> {
"刷新失败请检查网络".showToast()
viewBinding.swipeRefresh.isRefreshing = false
}
}
}
}
复制代码
2.消息的获取和RecycleView的刷新
消息部分设计成从新到老的设计,上述腾讯IM也是这个顺序,所以这部分添加列表时需要加在最前面
viewModel.chatList.add(0,msg)
adapter.notifyItemInserted(0)
复制代码
同时需要注意的就是刷新位置,这部分是插入故使用adapter中响应的notifyItemInserted方法进行提醒列表刷新,虽然直接使用最通用的notifyDataSetChanged也是可以达到相同的目的,但体验效果就不那么好了,如果是大量的数据,可能会产生比较大的延迟
3.关于消息item的设计细节
这个item具体是模仿QQ的布局进行设计的,这里底色部分没有做调整
最適化できるより良い部分は時間です。リストの時間を判断して、昨日、昨日の前日などの相対時間を実現できます。ここでは、 linearlayoutとconstraintlayout wrap_contentの塗りつぶしメソッドがインターフェイスを超える可能性があり、半分の単語があります。wrap_contentの最大幅は、ルートレイアウトの幅が原因であると推測されるため、最終的にレイアウトネストされている解決済み、以下はデザインのフレーム図です
5.プロジェクトで使用されるインターフェースとアドレス
Webプロジェクトは比較的複雑で、前のプロジェクトに基づいて開発されました。独立して分離するのは少し難しいので、Web側のコードはここに入れません。クライアント側のコードはここにあります。独自のsdkIdとサーバー側に関連するURLを置き換えるだけで済みます。同時に、サーバー側に関連するいくつかの相互作用があります。サーバー側が開発する必要のあるインターフェイスの簡単な紹介を次に示します。
-
履歴データを取得するためのインターフェース
ここには2つのパラメーターがあります。1つはプルメッセージの数を決定し、もう1つはプル開始時点を決定します。
//获取聊天记录 @GET("chat/refreshes/{time}/{number}") fun getChat(@Path("time")time:String, @Path("number")count:Int): Call<MessageResponse> 复制代码
-
TencentIMのユーザー署名を取得します
//生成应用凭据 @GET("imSig/{userId}/{expire}") fun getSig(@Path("userId")userId:String,@Path("expire")expire:Long):Call<String> 复制代码
プッシュで使用される2つのインターフェースもありますが、これらは前述のとおりです。
-
プロジェクトアドレス
https://github.com/xyh-fu/ImTest.git 复制代码
-
デモアプリケーションアドレス
ここのアプリケーションは現在2つのIDにしか開かれていません。友達がいる場合は、顔を合わせてテストできます。
https://res.dreamstudio.online/apk/imtest.apk 复制代码
6.まとめ
今回は、IMインスタントメッセージングの設計が収穫に満ちており、新しい知識ポイント(主に貧困によって制限されます)を取得することは悪くありません。後の段階で、TencentのIMに置き換えることを検討できます。小規模なテストとビジネスにすぎないことに気づきます。製品はまだ非常に異なります。サーバー側はもう少し複雑で、クライアント側は比較的単純で、メッセージ処理メカニズムがより厄介です。設計されたさまざまなインターフェイスやサーバー側のデータベースなどを考慮すると、統合するのは困難です。そのため、1つずつ説明することはしません。