关于一个手机控制电脑执行特定任务的解决方案探索【1】

【前言】
说来话长,关于这个手机控制电脑执行特定任务的想法早在几年前就有,但因为对安卓平台开发经验实在不足,就一直拖到了现在。不过好在没有忘记初衷,接下来我们一起来看我的思路和方法。

【思路】
想要通过手机作为控制端,来发送指令给同一网络下的电脑端,执行特定任务,例如打开桌面应用。

【思考】
市面上有很多电脑端和移动端相互通讯并控制的软件,可以“暴力”得控制并实现。但是现在不想依赖这类软件。这样的话我们需要设立模型,一个电脑服务端,一个安卓客户端。即多个安卓移动端可请求控制。
设计思路:

  1. 通信协议:通过TCP/IP协议在局域网内建立手机和电脑的Socket连接。
  2. 指令传输:手机端发送预定义的指令字符串(如open_app:notepad),电脑端解析后执行对应操作。
  3. 软件启动:电脑端通过命令行或脚本启动目标程序(例如借用Windows的cmd指令 /c start notepad.exe)。

【编程】
首先编写电脑端的程序,使用C#写一个cmd应用,等待被连接然后接受命令字符并解析执行

// Program.cs
using System;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Text;

class Server
{
    static void Main()
    {
        TcpListener server = null;
        try
        {
            // 绑定IP和端口
            IPAddress localAddr = IPAddress.Any;
            int port = 12345;
            server = new TcpListener(localAddr, port);
            server.Start();

            Console.WriteLine("等待手机连接...");

            while (true)
            {
                // 接受客户端连接
                TcpClient client = server.AcceptTcpClient();
                Console.WriteLine("已连接客户端.");

                // 处理指令
                NetworkStream stream = client.GetStream();
                byte[] buffer = new byte[1024];
                int bytesRead = stream.Read(buffer, 0, buffer.Length);
                string command = Encoding.ASCII.GetString(buffer, 0, bytesRead);
                Console.WriteLine($"收到指令: {command}");

                // 执行命令并返回结果
                string response = ExecuteCommand(command);
                byte[] responseData = Encoding.ASCII.GetBytes(response);
                stream.Write(responseData, 0, responseData.Length);

                // 关闭连接
                client.Close();
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"错误: {ex.Message}");
        }
        finally
        {
            server?.Stop();
        }
    }

    static string ExecuteCommand(string command)
    {
        try
        {
            if (command.StartsWith("open_app:"))
            {
                string appPath = command.Substring(9).Trim('"'); // 移除引号
                Process.Start("cmd.exe", $"/c start \"\" \"{appPath}\""); // 兼容路径空格
                return "Success: 程序已启动";
            }
            return "Error: 未知指令";
        }
        catch (Exception ex)
        {
            return $"Error: {ex.Message}";
        }
    }
}

接着编写安卓端程序。使用Kotlin借Android studio软件。
新建一个Empty Views Activity Project,编写MainActivity:

package com.example.myapplication6

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import com.example.myapplication6.databinding.ActivityMainBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.Socket
import java.nio.charset.StandardCharsets

// MainActivity.kt
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 按钮点击事件:打开桌面快捷方式
        binding.btnOpenApp.setOnClickListener {
            sendCommand("open_app:\"C:\\Users\\Administrator\\Desktop\\cctv13.url\"")
        }

    }

    private fun sendCommand(command: String) {
        CoroutineScope(Dispatchers.IO).launch {
            try {
                val socket = Socket("192.168.1.8", 12345) // 替换为电脑IP
                val outputStream = socket.getOutputStream()
                outputStream.write(command.toByteArray(Charsets.UTF_8))
              //  outputStream.write(command.getBytes(StandardCharsets.UTF_8))
                outputStream.flush()

                // 读取响应
                val inputStream = socket.getInputStream()
                val buffer = ByteArray(1024)
                val bytesRead = inputStream.read(buffer)
                val response = String(buffer, 0, bytesRead)
               // val response = buffer.decodeToString(0, bytesRead)

               Log.d("ServerResponse", response)

                socket.close()
            } catch (e: Exception) {
                e.printStackTrace()
                withContext(Dispatchers.Main) {
                    Toast.makeText(this@MainActivity, "连接失败: ${e.message}", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }
}

接着设置布局Activity_main.xml。创建一个按钮,方便发送指令:

<?xml version="1.0" encoding="utf-8"?>  <!-- activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <Button
        android:id="@+id/btnOpenApp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="打开桌面应用" />

</LinearLayout>

另外build.gradle设置也一并给出参考:
这个是build.gradle(module-app)

plugins {
    id 'com.android.application'
    id 'kotlin-android'
}

android {
    compileSdk 34

    defaultConfig {
        applicationId "com.example.myapplication6"
        minSdk 23
        targetSdk 34
        versionCode 1
        versionName "1.0"
        namespace 'com.example.myapplication6'
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildFeatures {
        viewBinding true  // 启用视图绑定
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:1.6.4"

    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    implementation 'androidx.core:core-ktx:1.9.0'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
    androidTestImplementation 'androidx.test:runner:1.5.2'
    androidTestImplementation 'androidx.test:core:1.5.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

}

这个是build.gradle(project)

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    repositories {
        google()
        mavenCentral()
    }
    ext.kotlin_version = '1.8.22' // 或者更新版本
    dependencies {
        classpath "com.android.tools.build:gradle:8.0.0"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com.android.tools.build:gradle:8.0.0' // 更新到最新版本


        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }

}

task clean(type: Delete) {
    delete rootProject.buildDir
}

【阶段性结果】

各自编译生成应用并运行,可以得到最基本的功能。注意的是,debug阶段,很多参数都设了常量,如IP地址、执行命令的字符串等。
值得优化的地方还有很多,比如替换cmd窗口程序,换成正常的窗口程序,并隐藏窗口,只出现在任务栏。

【再次编程】
思路:

  1. 修改项目输出类型为Windows应用程序。
  2. 创建托盘图标窗体,处理系统托盘图标和菜单。
  3. 重构后台服务,使用线程持续监听TCP连接。
  4. 修改主程序入口,启动托盘窗体和后台服务。
  5. 添加必要的异常处理和资源释放。
// Program.cs
using System;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Windows.Forms;
using System.Threading;
using System.Drawing;

namespace RemoteControlServer
{
    static class Program
    {
        /// <summary>
        /// 应用程序的主入口点
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            // 初始化托盘图标窗体
            var trayForm = new TrayIconForm();

            // 启动后台服务
            var server = new BackgroundServer();
            server.Start();

            // 启动消息循环(保持程序运行)
            Application.Run(trayForm);
        }
    }

    /// <summary>
    /// 后台服务类 - 处理TCP通信和指令执行
    /// </summary>
    public class BackgroundServer
    {
        private TcpListener listener;
        private bool isRunning = true;

        /// <summary>
        /// 启动服务监听
        /// </summary>
        public void Start()
        {
            new Thread(() =>
            {
                try
                {
                    listener = new TcpListener(IPAddress.Any, 12345);
                    listener.Start();
                    //Console.WriteLine("服务已启动,监听端口 12345");

                    while (isRunning)
                    {
                        // 异步接受客户端连接
                        var client = listener.AcceptTcpClient();
                        new Thread(HandleClient).Start(client);
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"服务异常: {ex.Message}");
                }
            })
            { IsBackground = true }.Start(); // 设置为后台线程
        }

        /// <summary>
        /// 处理客户端请求
        /// </summary>
        private void HandleClient(object obj)
        {
            using (var client = (TcpClient)obj)
            using (var stream = client.GetStream())
            {
                try
                {
                    byte[] buffer = new byte[1024];
                    int bytesRead = stream.Read(buffer, 0, buffer.Length);
                    string command = Encoding.UTF8.GetString(buffer, 0, bytesRead);
                    //Console.WriteLine($"收到指令: {command}");

                    // 执行命令并返回响应
                    string response = ExecuteCommand(command);
                    byte[] responseData = Encoding.UTF8.GetBytes(response);
                    stream.Write(responseData, 0, responseData.Length);
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"处理客户端错误: {ex.Message}");
                }
            }
        }

        /// <summary>
        /// 执行指令逻辑
        /// </summary>
        
        private string ExecuteCommand(string command)
        {
            try
            {
                if (command.StartsWith("open_app:"))
                {
                    string appPath = command.Substring(9).Trim('"');
                    Process.Start("cmd.exe", $"/c start \"\" \"{appPath}\"");
                    return "SUCCESS: 程序已启动";
                }
                return "ERROR: 未知指令";
            }
            catch (Exception ex)
            {
                return $"ERROR: {ex.Message}";
            }
        }

        

        /// <summary>
        /// 停止服务
        /// </summary>
        public void Stop()
        {
            isRunning = false;
            listener?.Stop();
            Console.WriteLine("服务已停止");
        }
    }

    /// <summary>
    /// 托盘图标窗体 - 处理系统托盘交互
    /// </summary>
    public class TrayIconForm : Form
    {
        private NotifyIcon trayIcon;
        private ContextMenuStrip trayMenu;

        public TrayIconForm()
        {
            // 初始化托盘图标
            trayIcon = new NotifyIcon
            {
                Text = "远程控制服务",
                Icon = SystemIcons.Application, // 可替换为自定义.ico文件
                Visible = true
            };

            // 初始化右键菜单
            trayMenu = new ContextMenuStrip();
            trayMenu.Items.Add("退出", null, OnExitClick);

            trayIcon.ContextMenuStrip = trayMenu;

            // 双击托盘图标事件(示例:显示日志窗口)
            trayIcon.DoubleClick += (s, e) =>
            {
                MessageBox.Show("服务运行中", "状态", MessageBoxButtons.OK, MessageBoxIcon.Information);
            };
        }

        /// <summary>
        /// 退出菜单点击事件
        /// </summary>
        private void OnExitClick(object sender, EventArgs e)
        {
            var result = MessageBox.Show("确定要退出服务吗?", "确认退出", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
            if (result == DialogResult.Yes)
            {
                trayIcon.Visible = false;
                Application.Exit();
            }
        }

        /// <summary>
        /// 窗体加载时隐藏窗口
        /// </summary>
        protected override void OnLoad(EventArgs e)
        {
            Visible = false;       // 隐藏窗体
            ShowInTaskbar = false; // 不在任务栏显示
            base.OnLoad(e);
        }

        /// <summary>
        /// 清理资源
        /// </summary>
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                trayIcon?.Dispose();
                trayMenu?.Dispose();
            }
            base.Dispose(disposing);
        }
    }
}

这样,我们就可以实现一个无窗口的windows监听程序。不过通过测试发现,一旦接收到请求并执行,屏幕会闪过一个cmd黑窗口。这不是想要的结果,进行优化。放弃使用cmd指令,转而直接使用Process.start函数来开始新任务。重写ExecuteCommand方法:

        // 修改后的 ExecuteCommand 方法
        private string ExecuteCommand(string command)
        {
            try
            {
                if (command.StartsWith("open_app:"))
                {
                    string appPath = command.Substring(9).Trim('"');

                    // 创建进程配置
                    var processInfo = new ProcessStartInfo
                    {
                        FileName = appPath,          // 直接指向目标程序
                        UseShellExecute = true,      // 使用系统Shell解析路径(支持快捷方式)
                        CreateNoWindow = true,       // 不创建任何窗口
                        WindowStyle = ProcessWindowStyle.Hidden // 隐藏窗口(双重保险)
                    };

                    Process.Start(processInfo);
                    return "SUCCESS: 程序已静默启动";
                }
                return "ERROR: 未知指令";
            }
            catch (Exception ex)
            {
                return $"ERROR: {ex.Message}";
            }
        }

这样的话,就避免了出现黑窗口,顺利执行。

【总结】
初步实现了设想。
不过这才刚刚开始,在此基础上可以添加很多功能,优化应用,提升体验。我们列一下优化方向。

  1. 美化电脑端应用,替换默认图标,美化安卓端应用
  2. 添加电脑端应用右键菜单内容,比如查看当前IP地址说明设置
  3. 设置电脑端应用自启动
  4. 安卓端应用添加功能,可以直接编写一段字符串,当成指令发送给电脑端执行,比如cmd指令
  5. 安卓端应用添加功能,可以检查版本,可以更新。
  6. 安卓端应用可以自动查找网络中的特定电脑端并设定目标IP地址。

这些是目前的想法。希望笔者自己可以不忘初心,再接再厉,整理出来供读者参考学习。

posted @   SHARP-EYE  阅读(24)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
点击右上角即可分享
微信分享提示