dependencies { // 实现服务端(http、socket) implementation("org.nanohttpd:nanohttpd:2.3.1") implementation("org.nanohttpd:nanohttpd-websocket:2.3.1") // 与服务端通信 implementation("com.squareup.okhttp3:okhttp:4.12.0") // 堆代码 duidaima.com // 扫描解析、生成二维码 implementation("com.github.jenly1314:zxing-lite:3.1.0") }服务端
使用NanoHttpD实现Socket服务端(与被扫端通信)和Http服务端(与扫码端通信),示例代码如下:
class ServerSocketClient : NanoWSD(9090) { private var serverWebSocket: ServerWebSocket? = null override fun openWebSocket(handshake: IHTTPSession?): WebSocket { return ServerWebSocket(handshake).also { serverWebSocket = it } } private class ServerWebSocket(handshake: IHTTPSession?) : WebSocket(handshake) { override fun onOpen() {} override fun onClose(code: WebSocketFrame.CloseCode?, reason: String?, initiatedByRemote: Boolean) {} override fun onMessage(message: WebSocketFrame?) {} override fun onPong(pong: WebSocketFrame?) {} override fun onException(exception: IOException?) {} } override fun stop() { super.stop() serverWebSocket = null } fun sendMessage(message: String) { serverWebSocket?.send(message) } }Http服务
const val APP_SCAN_INTERFACE = "loginViaScan" const val USER_ID = "userId" const val EXAMPLE_USER_ID = "123456789" const val DEVICE_ID = "deviceId" const val EXAMPLE_DEVICE_ID = "example_device_id0001" class ServerHttpClient(private var scanLoginSucceedListener: ((userId: String) -> Unit)? = null) : NanoHTTPD(8080) { override fun serve(session: IHTTPSession?): Response { val uri = session?.uri return if (uri == "/$APP_SCAN_INTERFACE" && session.parameters[USER_ID]?.first() == EXAMPLE_USER_ID && session.parameters[DEVICE_ID]?.first() == EXAMPLE_DEVICE_ID ) { scanLoginSucceedListener?.invoke(session.parameters[USER_ID]?.first() ?: "") newFixedLengthResponse("Login Succeed") } else { super.serve(session) } } }服务控制类
object ServerController { private var serverSocketClient: ServerSocketClient? = null private var serverHttpClient: ServerHttpClient? = null fun startServer() { (serverSocketClient ?: ServerSocketClient().also { serverSocketClient = it }).run { if (!isAlive) { start(0) } } (serverHttpClient ?: ServerHttpClient { serverSocketClient?.sendMessage("Login Succeed, user id is $it") }.also { serverHttpClient = it }).run { if (!isAlive) { start(NanoHTTPD.SOCKET_READ_TIMEOUT, true) } } } fun stopServer() { serverSocketClient?.stop() serverSocketClient = null serverHttpClient?.stop() serverHttpClient = null } }被扫端
class DevicesSocketHelper(private val messageListener: ((message: String) -> Unit)? = null) { private var webSocket: WebSocket? = null private val webSocketListener = object : WebSocketListener() { override fun onMessage(webSocket: WebSocket, bytes: ByteString) { super.onMessage(webSocket, bytes) messageListener?.invoke(bytes.utf8()) } override fun onMessage(webSocket: WebSocket, text: String) { super.onMessage(webSocket, text) messageListener?.invoke(text) } } fun openSocketConnection(serverPath: String) { val okHttpClient = OkHttpClient.Builder() .connectTimeout(120, TimeUnit.SECONDS) .readTimeout(120, TimeUnit.SECONDS) .build() val request = Request.Builder().url(serverPath).build() webSocket = okHttpClient.newWebSocket(request, webSocketListener) } fun release() { webSocket?.close(1000, "") webSocket = null } }被扫端示例页面
class DeviceExampleActivity : AppCompatActivity() { private lateinit var binding: LayoutDeviceExampleActivityBinding private var socketHelper: DevicesSocketHelper? = DevicesSocketHelper() { message -> // 接收到服务端发来的消息,改变显示内容 runOnUiThread { binding.tvUserInfo.text = message binding.ivQrCode.visibility = View.GONE binding.tvUserInfo.visibility = View.VISIBLE } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = LayoutDeviceExampleActivityBinding.inflate(layoutInflater).also { setContentView(it.root) it.includeTitle.tvTitle.text = "Device Example" } lifecycleScope.launch(Dispatchers.IO) { // 使用设备id生成二维码 CodeUtils.createQRCode(EXAMPLE_DEVICE_ID, DensityUtil.dp2Px(200)).let { qrCode -> withContext(Dispatchers.Main) { binding.ivQrCode.setImageBitmap(qrCode) } } } socketHelper?.openSocketConnection("ws://localhost:9090/") } override fun onDestroy() { super.onDestroy() socketHelper?.release() socketHelper = null } }扫描端
class ScanQRCodeActivity : BarcodeCameraScanActivity() { override fun initCameraScan(cameraScan: CameraScan<Result>) { super.initCameraScan(cameraScan) // 播放扫码音效 cameraScan.setPlayBeep(true) } override fun createAnalyzer(): Analyzer<Result> { return QRCodeAnalyzer(DecodeConfig().apply { // 设置仅识别二维码 setHints(DecodeFormatManager.QR_CODE_HINTS) }) } override fun onScanResultCallback(result: AnalyzeResult<Result>) { // 已获取结果,停止识别二维码 cameraScan.setAnalyzeImage(false) // 返回扫码结果 setResult(Activity.RESULT_OK, Intent().apply { putExtra(CameraScan.SCAN_RESULT, result.result.text) }) finish() } }扫描端示例页面
class AppScanExampleActivity : AppCompatActivity() { private lateinit var binding: LayoutAppScanExampleActivityBinding private var serverIp: String = "" private val scanQRCodeLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == Activity.RESULT_OK) { it.data?.getStringExtra(CameraScan.SCAN_RESULT)?.let { deviceId -> sendRequestToServer(deviceId) } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = LayoutAppScanExampleActivityBinding.inflate(layoutInflater).also { setContentView(it.root) } OkHttpHelper.init() binding.btnScan.setOnClickListener { // 获取输入的服务端ip(两台设备在同一WIFI下,直接通过IP访问服务端) serverIp = binding.etInputIp.text.toString() if (serverIp.isEmpty()) { showSnakeBar("Server ip can not be empty") return@setOnClickListener } hideKeyboard(binding.etInputIp) scanQRCodeLauncher.launch(Intent(this, ScanQRCodeActivity::class.java)) } } private fun sendRequestToServer(deviceId: String) { OkHttpHelper.sendGetRequest("http://${serverIp}:8080/${APP_SCAN_INTERFACE}", mapOf(Pair(USER_ID, EXAMPLE_USER_ID), Pair(DEVICE_ID, deviceId)), object : RequestCallback { override fun onResponse(success: Boolean, responseBody: ResponseBody?) { showSnakeBar("Scan login ${if (success) "succeed" else "failure"}") } override fun onFailure(errorMessage: String?) { showSnakeBar("Scan login failure") } }) } private fun hideKeyboard(view: View) { view.clearFocus() WindowInsetsControllerCompat(window, view).hide(WindowInsetsCompat.Type.ime()) } private fun showSnakeBar(message: String) { runOnUiThread { Snackbar.make(binding.root, message, Snackbar.LENGTH_SHORT).show() } } }示例入口页
class ScanLoginExampleActivity : AppCompatActivity() { private lateinit var binding: LayoutScanLoginExampleActivityBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = LayoutScanLoginExampleActivityBinding.inflate(layoutInflater).also { setContentView(it.root) it.includeTitle.tvTitle.text = "Scan Login Example" it.btnOpenDeviceExample.setOnClickListener { // 打开被扫端同时启动服务 ServerController.startServer() startActivity(Intent(this, DeviceExampleActivity::class.java)) } it.btnOpenAppExample.setOnClickListener { startActivity(Intent(this, AppScanExampleActivity::class.java)) } } } override fun onDestroy() { super.onDestroy() ServerController.stopServer() } }效果演示与示例代码