关于一个手机控制电脑执行特定任务的解决方案探索【1】
【前言】
说来话长,关于这个手机控制电脑执行特定任务的想法早在几年前就有,但因为对安卓平台开发经验实在不足,就一直拖到了现在。不过好在没有忘记初衷,接下来我们一起来看我的思路和方法。
【思路】
想要通过手机作为控制端,来发送指令给同一网络下的电脑端,执行特定任务,例如打开桌面应用。
【思考】
市面上有很多电脑端和移动端相互通讯并控制的软件,可以“暴力”得控制并实现。但是现在不想依赖这类软件。这样的话我们需要设立模型,一个电脑服务端,一个安卓客户端。即多个安卓移动端可请求控制。
设计思路:
- 通信协议:通过TCP/IP协议在局域网内建立手机和电脑的Socket连接。
- 指令传输:手机端发送预定义的指令字符串(如open_app:notepad),电脑端解析后执行对应操作。
- 软件启动:电脑端通过命令行或脚本启动目标程序(例如借用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窗口程序,换成正常的窗口程序,并隐藏窗口,只出现在任务栏。
【再次编程】
思路:
- 修改项目输出类型为Windows应用程序。
- 创建托盘图标窗体,处理系统托盘图标和菜单。
- 重构后台服务,使用线程持续监听TCP连接。
- 修改主程序入口,启动托盘窗体和后台服务。
- 添加必要的异常处理和资源释放。
// 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}";
}
}
这样的话,就避免了出现黑窗口,顺利执行。
【总结】
初步实现了设想。
不过这才刚刚开始,在此基础上可以添加很多功能,优化应用,提升体验。我们列一下优化方向。
- 美化电脑端应用,替换默认图标,美化安卓端应用
- 添加电脑端应用右键菜单内容,比如
查看当前IP地址
、说明
、设置
等 - 设置电脑端应用自启动
- 安卓端应用添加功能,可以直接编写一段字符串,当成指令发送给电脑端执行,比如cmd指令
- 安卓端应用添加功能,可以检查版本,可以更新。
- 安卓端应用可以自动查找网络中的特定电脑端并设定目标IP地址。
这些是目前的想法。希望笔者自己可以不忘初心,再接再厉,整理出来供读者参考学习。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~