基于UDP广播的局域网匿名聊天APP

5 篇文章 0 订阅

一天天太能心血来潮,昨天在看UDP的时候突然手痒想写一个基于UDP的聊天app,想着挺简单结果搞了很久才搞出来。话不多说,上代码。

这个项目使用Jetpack框架搭建,Kotlin编写。

1. UDP通信工具类

import android.text.format.Formatter
import android.util.Log
import com.psychedelic.udpchat.ChatEntity
import com.psychedelic.udpchat.FROM_OTHERS
import com.psychedelic.udpchat.FROM_SELF
import com.psychedelic.udpchat.TAG
import com.psychedelic.udpchat.listener.UdpMessageListener
import com.psychedelic.udpchat.listener.UdpMessageSendListener
import java.io.IOException
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetAddress
import java.net.SocketException

class UdpManager(ipAddress: Int,listener:UdpMessageListener) {
    private var mLocalIp:String ?=null
    private val mIntIpAddress = ipAddress
    private val mPort = 8211
    private val mListener = listener
    fun sendUdpMsg(data: String,sendListener: UdpMessageSendListener) {
        if (data.isEmpty() || data.isBlank()){
            return
        }
        /*这一步就是将本机的IP地址转换成xxx.xxx.xxx.255*/
        val broadCastIP = mIntIpAddress or -0x1000000
        mLocalIp = "/${Formatter.formatIpAddress(mIntIpAddress)}"
        Log.d(TAG, "sendUdpMsg ip = $broadCastIP")

        var sendSocket: DatagramSocket? = null
        try {
            val server: InetAddress = InetAddress.getByName(Formatter.formatIpAddress(broadCastIP))
            Log.d(TAG, "sendUdpMsg server = $server")

            sendSocket = DatagramSocket()
            val msg = String(data.toByteArray(),Charsets.UTF_8)
            Log.d(TAG,"msg = $msg")
            val theOutput = DatagramPacket((msg).toByteArray(), msg.toByteArray().size, server, mPort)
            Log.d(TAG, "mLocalIp = $mLocalIp")

            sendSocket.send(theOutput)
            sendListener.sendSuccess()
            Log.d(TAG, "sendUdpMsg send !!!")
        } catch (e: IOException) {
            e.printStackTrace()
        } finally {
            sendSocket?.close()
        }
    }

    fun receiverUdpMsg() {
        Log.d(TAG, "receiverUdpMsg")
        val buffer = ByteArray(1024)
        /*在这里同样使用约定好的端口*/
        var server: DatagramSocket? = null
        try {
            server = DatagramSocket(mPort)
            val packet = DatagramPacket(buffer, buffer.size)

            while (true) {
                try {
                    server.receive(packet)
                    val content = String(packet.data, 0, packet.length, Charsets.UTF_8)
                    Log.d(TAG,"content = $content ")
                    Log.d(TAG, "get ip = ${packet.address} mLocalIP = $mLocalIp")
                    val msg = ChatEntity().apply { text = content }
                    if (packet.address.toString() == mLocalIp){
                        msg.fromWho = FROM_SELF
                    }else{
                        msg.fromWho = FROM_OTHERS
                    }
                    mListener.onMessageReceive(msg)
                    Log.d(TAG,"address : " + packet.address + ", port : " + packet.port + ", content : " + content)
                } catch (e: IOException) {
                    e.printStackTrace()
                }
            }
        } catch (e: SocketException) {
            Log.d(TAG, "err")
            e.printStackTrace()
        } finally {
            server?.close()
        }
    }

}

通过WifiManager获取本地IP,然后给路由器发送UDP包,路由器会全频段广播,那么只要其他设备监听了这个端口就能收到消息,端口我写死了,以后会改成可设置的,这样也能监听其他设备的UDP广播。
除了利用路由器发送广播的方式,也可以遍历0到255所有的IP地址查找局域网中的设备,获取到对方的IP地址后可以定向发,也可以去建立稳定的TCP连接,这里不展开了。广播的UDP消息自己也能收到,为了区分对比IP地址,收到的包IP地址如果和本机相同就是自己发的。

2. XML页面

写一个简单的聊天页面:
在这里插入图片描述

<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>

    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        tools:context=".MainActivity">
        <androidx.appcompat.widget.Toolbar
            android:id="@+id/chat_tool_bar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@+id/chat_recycle_view"
            android:elevation="15dp"
            android:background="#F2F2F2"
            app:titleTextColor="#bfbfbf"
            app:navigationIcon="@mipmap/back"
            >
        </androidx.appcompat.widget.Toolbar>
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/chat_recycle_view"
            app:layout_constraintTop_toBottomOf="@+id/chat_tool_bar"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintBottom_toTopOf="@+id/chat_bottom_bar"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:elevation="5dp"
            />
        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/chat_bottom_bar"
            android:layout_width="match_parent"
            android:layout_height="56dp"
            app:layout_constraintTop_toBottomOf="@+id/chat_recycle_view"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            android:elevation="15dp"
            android:background="#F2F2F2"
            >
            <Button
                android:id="@+id/chat_button_send"
                android:layout_width="60dp"
                android:layout_height="40dp"
                android:layout_marginEnd="10dp"
                android:onClick="sendMessageButtonClick"
                android:background="@drawable/chat_send_btn_selector"
                android:text="@string/button_send"
                android:textColor="#ffffff"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintLeft_toRightOf="@+id/chat_edit_text"
                app:layout_constraintRight_toRightOf="parent"
                />

            <EditText
                android:id="@+id/chat_edit_text"
                android:layout_width="0dp"
                android:layout_height="40dp"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"
                android:layout_marginStart="20dp"
                android:layout_marginEnd="10dp"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toLeftOf="@+id/chat_button_send"
                android:background="#ffffff"
                />

        </androidx.constraintlayout.widget.ConstraintLayout>


    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

RecyclerView的Item布局,一个是收到消息的布局,一个是自己发送的消息布局

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="item"
            type="com.psychedelic.udpchat.ChatEntity" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:id="@+id/chat_activity_ll_receive_chat_content"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:maxWidth="300dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            android:minHeight="43dp"
            android:layout_margin="20dp"
            android:gravity="center_vertical"
            android:background="@mipmap/chatfrom_bg_normal"
            android:text="@{item.text}"
            android:textSize="20sp"
            />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="item"
            type="com.psychedelic.udpchat.ChatEntity" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:id="@+id/chat_activity_ll_receive_chat_content"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:maxWidth="300dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            android:minHeight="43dp"
            android:layout_margin="20dp"
            android:gravity="center_vertical"
            android:background="@mipmap/chatto_bg_normal"
            android:text="@{item.text}"
            android:textSize="20sp"
            />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

3. Activity

package com.psychedelic.udpchat

import android.content.Context
import android.content.pm.PackageManager
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.psychedelic.udpchat.databinding.ActivityMainBinding
import com.psychedelic.udpchat.listener.MainActivityObserver
import com.psychedelic.udpchat.listener.UdpMessageListener
import com.psychedelic.udpchat.listener.UdpMessageSendListener
import com.psychedelic.udpchat.mvvm.MainViewModel
import com.psychedelic.udpchat.util.StatusBarUtil


const val TAG = "MainActivity"
class MainActivity : AppCompatActivity(),UdpMessageListener {
    private val mContext = this

    private var mList = ArrayList<ChatEntity>()
    private lateinit var mAdapter: ChatRvAdapter
    private lateinit var mBinding: ActivityMainBinding
    private lateinit var mWifiManager: WifiManager
    private lateinit var mWifiInfo: WifiInfo
    private lateinit var mViewModel: MainViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        StatusBarUtil.setStatusTextColor(true, this)
        window.statusBarColor = resources.getColor(R.color.bar_color)
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        mViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        mWifiManager = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
        mWifiInfo = mWifiManager.connectionInfo
        Log.d(TAG, "SSID = ${mWifiInfo.ssid}")
        mBinding.chatToolBar.title = mWifiInfo.ssid
        setSupportActionBar(mBinding.chatToolBar)
        mAdapter = ChatRvAdapter(this, mList, BR.item)
        mBinding.chatRecycleView.layoutManager = LinearLayoutManager(this)
        mBinding.chatRecycleView.adapter = mAdapter
        mBinding.chatToolBar.setNavigationOnClickListener {
            finish()
        }
        lifecycle.addObserver(MainActivityObserver(mContext,mWifiManager,mViewModel,this))
    }


    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        if (requestCode == REQUEST_PERMISSIONS) {
            for ((index, permission) in permissions.withIndex()) {
                if (grantResults[index] != PackageManager.PERMISSION_GRANTED) {
                    Log.d(
                        TAG,
                        "permission = $permission grantResults[index] = ${grantResults[index]}"
                    )
                }
            }
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }


    private fun refreshNewMessage(msg: ChatEntity) {
        Log.d(TAG, "refreshNewMessage msg = ${msg.text}")
        runOnUiThread {
            mList.add(msg)
            mAdapter.refreshData(mList)
            scrollToEnd()
        }
    }

    fun sendMessageButtonClick(view: View) {
        if (mBinding.chatEditText.text.isNotEmpty()) {
            mViewModel.sendUdpMsg(mBinding.chatEditText.text.toString(),
                object : UdpMessageSendListener {
                    override fun sendSuccess() {
                        runOnUiThread {
                            mBinding.chatEditText.text.clear()
                        }
                    }
                })
        }
    }

    private fun scrollToEnd() {
    	//刷新消息的时候需要将RecycleView滚动到最后一行以显示最新消息
        if (mBinding.chatRecycleView.adapter!!.itemCount > 0) {
            mBinding.chatRecycleView.smoothScrollToPosition(mBinding.chatRecycleView.adapter!!.itemCount)
        }
    }

    override fun onMessageReceive(msg: ChatEntity) {
        refreshNewMessage(msg)
    }
}

使用了lifeCycle,这里碰到了点坑,我以前不用ActionBar或者ToolBar,都是自己写的布局。这次用了ToolBar,发现其使用的时候逻辑顺序有严格要求,比如title设置必须在setSupportActionBar之前,setNavigationOnClickListener则必须要在setSupportActionBar之后,否则设置无效。

还有我把当前Wifi的SSID也就是名称作为Title,一开始发现无论怎么获取得到的SSID都是空的,后来上网查发现Android 8.0之后需要添加上网络定位权限才能通过WifiManager获取到SSID,加上权限之后就好了。

    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

注意需要获取动态运行时权限,我放在LifeCycleObserver中了

4. MainActivityObserver

import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.net.wifi.WifiManager
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import com.psychedelic.udpchat.REQUEST_PERMISSIONS
import com.psychedelic.udpchat.mvvm.MainViewModel


class MainActivityObserver(context: Context,wifiManager: WifiManager,viewModel:MainViewModel,listener: UdpMessageListener):LifecycleObserver {
    private val mContext = context
    private val mListener = listener
    private val mViewModel = viewModel
    private var mWifiManager: WifiManager = wifiManager
    private val permissions = arrayOf<String>(
        Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.ACCESS_COARSE_LOCATION
    )

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun create(){
        requestPermission()
        if (!lackPermission()){
            val ipAddress = mWifiManager.connectionInfo.ipAddress
            mViewModel.startReceiveUdpMsg(ipAddress,mListener)
        }else{
            Toast.makeText(mContext,"缺少网络权限,请授权后重试",Toast.LENGTH_LONG).show()
            (mContext as Activity).finish()
        }
    }
    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun destroy(){
        mViewModel.shutDownExecutor()
    }

    private fun requestPermission(){

        if (lackPermission()) {
            ActivityCompat.requestPermissions(
                mContext as Activity,
                permissions,
                REQUEST_PERMISSIONS
            )
        }
    }

    private fun lackPermission():Boolean{
        for (permission in permissions){
            if (ContextCompat.checkSelfPermission(mContext,permission)!= PackageManager.PERMISSION_GRANTED){
                return true
            }
        }
        return false
    }
}

5. MainViewModel

UDPManager的调用放在了ViewModel中

import androidx.lifecycle.ViewModel
import com.psychedelic.udpchat.listener.UdpMessageListener
import com.psychedelic.udpchat.listener.UdpMessageSendListener
import com.psychedelic.udpchat.net.UdpManager
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

class MainViewModel():ViewModel(){
    private lateinit var mReceiveExecutor: ExecutorService
    private lateinit var mSendExecutor: ExecutorService
    private lateinit var mUdpManager: UdpManager

    fun startReceiveUdpMsg(intIpAddress:Int,listener:UdpMessageListener){
        mUdpManager = UdpManager(intIpAddress,listener)
        mReceiveExecutor = Executors.newSingleThreadExecutor()
        mSendExecutor = Executors.newFixedThreadPool(5)
        mReceiveExecutor.submit { mUdpManager.receiverUdpMsg()}
    }

    fun sendUdpMsg(msg:String,listener: UdpMessageSendListener){
        mSendExecutor.submit {
            mUdpManager.sendUdpMsg(msg,listener)
        }
    }

    fun shutDownExecutor(){
        mSendExecutor.shutdown()
        mReceiveExecutor.shutdown()
    }

}

最后附上RecyclerView Adapter的代码

6. ChatRvAdapter

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import com.psychedelic.udpchat.databinding.ChatItemFromBinding
import com.psychedelic.udpchat.databinding.ChatItemToBinding

const val CHAT_TYPE_SEND_TXT = 1
const val CHAT_TYPE_GET_TXT = CHAT_TYPE_SEND_TXT + 1

class ChatRvAdapter(context: Context, list: ArrayList<ChatEntity>, variableId: Int) :
    RecyclerView.Adapter<ChatRvAdapter.ViewHolder>() {
    private val mContext = context
    private var mList: ArrayList<ChatEntity> = list
    private val mVariableId = variableId

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private var binding: ViewDataBinding? = null
        fun getBinding(): ViewDataBinding {
            return binding!!
        }

        fun setBinding(binding: ViewDataBinding) {
            this.binding = binding
        }
    }

    fun refreshData(list:ArrayList<ChatEntity>){
        mList = list
        notifyDataSetChanged()
    }

    override fun getItemViewType(position: Int): Int {
        return mList[position].fromWho
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        if (viewType == CHAT_TYPE_SEND_TXT) {
            val chatSendBinding = DataBindingUtil.inflate<ChatItemToBinding>(
                LayoutInflater.from(mContext),
                R.layout.chat_item_to,
                parent,
                false
            )
            val viewHolder = ViewHolder(chatSendBinding.root)
            viewHolder.setBinding(chatSendBinding)
            return viewHolder
        } else {
            val chatFromBinding = DataBindingUtil.inflate<ChatItemFromBinding>(
                LayoutInflater.from(mContext),
                R.layout.chat_item_from,
                parent,
                false
            )
            val viewHolder = ViewHolder(chatFromBinding.root)
            viewHolder.setBinding(chatFromBinding)
            return viewHolder
        }

    }

    override fun getItemCount(): Int {
        return mList.size
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.getBinding().setVariable(mVariableId, mList[position])
        holder.getBinding().executePendingBindings()
    }
}

完整项目Github地址:UdpChat

项目效果:
在这里插入图片描述

只要安装此APP那么在局域网下的所有人都可以加入这个聊天室,即使没有连接到因特网也可以,没有去收集发送方的MAC地址,所以这个软件是匿名聊天的,而且聊天记录放在内存中,没有做持久化,推出APP就会销毁。

以后有空会拿这个DEMO用Room去做一下聊天记录存储,用mac标识联系人,并且提供加密传输选项。还是挺好玩的。

Over!
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值