一天天太能心血来潮,昨天在看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.SocketExceptionclass UdpManager(ipAddress: Int,listener:UdpMessageListener) {private var mLocalIp:String ?=nullprivate val mIntIpAddress = ipAddressprivate val mPort = 8211private val mListener = listenerfun sendUdpMsg(data: String,sendListener: UdpMessageSendListener) {if (data.isEmpty() || data.isBlank()){return}/*这一步就是将本机的IP地址转换成xxx.xxx.xxx.255*/val broadCastIP = mIntIpAddress or -0x1000000mLocalIp = "/${Formatter.formatIpAddress(mIntIpAddress)}"Log.d(TAG, "sendUdpMsg ip = $broadCastIP")var sendSocket: DatagramSocket? = nulltry {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? = nulltry {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.ConstraintLayoutxmlns: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.Toolbarandroid: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.RecyclerViewandroid: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.ConstraintLayoutandroid: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"><Buttonandroid: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"/><EditTextandroid: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><variablename="item"type="com.psychedelic.udpchat.ChatEntity" /></data><androidx.constraintlayout.widget.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"><TextViewandroid: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><variablename="item"type="com.psychedelic.udpchat.ChatEntity" /></data><androidx.constraintlayout.widget.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"android:layout_height="wrap_content"><TextViewandroid: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.udpchatimport 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.StatusBarUtilconst val TAG = "MainActivity"
class MainActivity : AppCompatActivity(),UdpMessageListener {private val mContext = thisprivate var mList = ArrayList<ChatEntity>()private lateinit var mAdapter: ChatRvAdapterprivate lateinit var mBinding: ActivityMainBindingprivate lateinit var mWifiManager: WifiManagerprivate lateinit var mWifiInfo: WifiInfoprivate lateinit var mViewModel: MainViewModeloverride 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 WifiManagermWifiInfo = mWifiManager.connectionInfoLog.d(TAG, "SSID = ${mWifiInfo.ssid}")mBinding.chatToolBar.title = mWifiInfo.ssidsetSupportActionBar(mBinding.chatToolBar)mAdapter = ChatRvAdapter(this, mList, BR.item)mBinding.chatRecycleView.layoutManager = LinearLayoutManager(this)mBinding.chatRecycleView.adapter = mAdaptermBinding.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.MainViewModelclass MainActivityObserver(context: Context,wifiManager: WifiManager,viewModel:MainViewModel,listener: UdpMessageListener):LifecycleObserver {private val mContext = contextprivate val mListener = listenerprivate val mViewModel = viewModelprivate var mWifiManager: WifiManager = wifiManagerprivate 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.ipAddressmViewModel.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.Executorsclass MainViewModel():ViewModel(){private lateinit var mReceiveExecutor: ExecutorServiceprivate lateinit var mSendExecutor: ExecutorServiceprivate lateinit var mUdpManager: UdpManagerfun 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.ChatItemToBindingconst val CHAT_TYPE_SEND_TXT = 1
const val CHAT_TYPE_GET_TXT = CHAT_TYPE_SEND_TXT + 1class ChatRvAdapter(context: Context, list: ArrayList<ChatEntity>, variableId: Int) :RecyclerView.Adapter<ChatRvAdapter.ViewHolder>() {private val mContext = contextprivate var mList: ArrayList<ChatEntity> = listprivate val mVariableId = variableIdinner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {private var binding: ViewDataBinding? = nullfun getBinding(): ViewDataBinding {return binding!!}fun setBinding(binding: ViewDataBinding) {this.binding = binding}}fun refreshData(list:ArrayList<ChatEntity>){mList = listnotifyDataSetChanged()}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!

基于UDP广播的局域网匿名聊天APP相关推荐

  1. 基于UDP协议的局域网网络聊天工具

    /* * 本程序实现了基于UDP协议的局域网网络聊天工具. * 参考网上的源码,发现一个calss就可以搞定. * ChatFrame类创建窗口,包含JTextField和TextArea. * 前者 ...

  2. 基于UDP广播的局域网聊天工具

     最近项目在做一个基于UDP模式的通信程序,考虑到项目的需求有一对多的需要,所以采用socket UDP广播模式进行数据通信.网上了解了一下知道这种模式也是目前QQ采用的方式,于是为了更好的理解s ...

  3. Android Studio 连接阿里云数据库【制作基于数据库的多人远程聊天APP】

    Android Studio 连接阿里云数据库的简单方法[制作基于数据库的多人远程聊天APP] 首先购买好一个阿里云数据库RDS[我买了一年用了49元] 1.进入官网–>打开右上角的控制台 2. ...

  4. 基于Android开发的即时通讯聊天app

    基于Android开发的即时通讯聊天app 前言 即时通讯(Instant Messaging,简称IM)在互联网中应用十分广泛,它可以和很多的领域结合,发挥十分重要的作用.比如金融行业的支付宝.各大 ...

  5. java udp简单聊天程序_Java基于UDP协议实现简单的聊天室程序

    最近比较闲,一直在抽空回顾一些java方面的技术应用. 今天没什么事做,基于udp协议,写了一个非常简单的聊天室程序. 现在的工作,很少用到socket,也算是对java网络编程方面的一个简单回忆. ...

  6. java udp 聊天室_Java基于UDP协议实现简单的聊天室程序

    最近比较闲,一直在抽空回顾一些Java方面的技术应用. 今天没什么事做,基于UDP协议,写了一个非常简单的聊天室程序. 现在的工作,很少用到socket,也算是对Java网络编程方面的一个简单回忆. ...

  7. C# 实现udp广播收集局域网类所有设备信息

    一个简单好理解的例子,复制过去就能用,能看到效果 首先对功能的思考,他怎么去实现 1.制定udp广播的端口(如果收发用同一个端口就会一直接收到自己给自己广播的消息) 2.启动后向局域网广播约定的字符串 ...

  8. C 基于UDP实现一个简易的聊天室

    引言 本文是围绕Linux udp api 构建一个简易的多人聊天室.重点看思路,帮助我们加深 对udp开发中一些api了解.相对而言udp socket开发相比tcp socket开发注意的细节要少 ...

  9. 基于Udp的Socket网络编程聊天程序

    1.新建一个工程区Net 在工作区中添加两个工程 NetSrv 和 NetClient 为两个工程添加库文件 (Link中) ws2_32.lib 2.在工程NetSrv中添加Server.cpp文件 ...

  10. 基于Vue+Java实现的在线聊天APP系统设计与实现

    全套资料下载地址:https://download.csdn.net/download/sheziqiong/85595798 一.需求分析 1.核心用户分析 在线聊天系统主要针对一些年轻用户群体以及 ...

最新文章

  1. 嵌入式培训学习历程第三天
  2. Java 中的字符串(String)与C# 中字符串(string)的异同
  3. Python 3.X 要使用urllib.request 来抓取网络资源。转
  4. socket和http协议
  5. Linux Perl 升级
  6. python pcm,python pcm音频添加头转成Wav格式文件的方法
  7. 深度学习算法和机器学习算法_啊哈! 4种流行的机器学习算法的片刻
  8. html 将两个标签绑在一起,基本标签2
  9. Taro+react开发(76):taro安装
  10. 全自动化虽然还早,但机器人劳力确实越来越便宜了
  11. G.8032协议 ERPS
  12. cpda数据分析师证书含金量高吗
  13. 软件测试电脑内存适配,利用Memtest86 测试你电脑的内存
  14. 三菱PLC缓冲表操作
  15. python 去重方法
  16. SDOI 2018 R2 游记
  17. 国标:计算机软件文档编制规范
  18. 平安人寿御享福,新噪点新优势
  19. hive分隔符_Hive中默认分隔符介绍
  20. LibreOffice 宏

热门文章

  1. wget 的安装与使用(Windows)
  2. Delphi6.0的那些
  3. 蚁群算法原理以及应用
  4. 不吹不黑,这5款浏览器安全无广告无弹窗,亲测好用
  5. 35岁没成高管被优化了.... 网友炸了!!!
  6. OFDM-训练序列与导频
  7. 《东周列国志》第五十三回 楚庄王纳谏复陈 晋景公出师救郑
  8. 彻底搞懂SSD网络结构
  9. 被反爬虫搞到心态崩溃
  10. adguard home上网慢_老毛子/Padavan设置SmartDNS提速+AdGuard Home去广告之东施效颦