背景:
我已经通过小米插座和修改bios参数实现了小爱同学开机,现在想要通过小爱同学关机,各种百度发现小爱同学不支持直接操作电脑软关机,后来在bilibili看到一个博主用开机卡实现电脑软关机、我也到淘宝去买了一个开机卡,买来之后不会安装,而且担心安装之后又后门我也没安装。
后来我在想可不可以通过siri来控制电脑,然后发现苹果手机有个快捷指令的东西,好像可以编写一些脚本。我最开始想要通过python来实现socket通讯来控制电脑,然后我就下了一个pythonista ,这个软件是要花钱的,65RMB,但是这不是重点,重点是这软件一点不好用,没次我用快捷指令执行python脚本的时候,都会打开pythonista这个软件,给我看执行控制台,不但反应慢,关键是我也不关心这个执行结果呀。。。。
后来发现快捷指令里面有个叫shell脚本的东西,我想shell不就是xshell吗,shell不就可以往远程服务器执行命令吗,嗯,这正是我想要的操作。我的想法是通过快捷指令的shell脚本命令给远程的腾讯云服务器写数据,然后腾讯云服务上部署用c语言写的Csocket服务器端,电脑上部署Cscocket的客户端。
用C语言的原因是依赖比较小,在linux服务器和windows上不需要安装什么特别的依赖,执行速度快,打包的文件体积小等特点。
准备工作:
- 腾讯云服务器【带有公网IP】,并且开放腾讯云安全组中策略端口8848(这是我的,你可以自定义)
- ipone手机带有快捷指令APP
- 一台带有windwos10系统电脑
- visual studio 2019
- C语言的基础,最好是Csocket相关知识
工作原理:
图是通过processon画的;
大概的原理是:
ipone1手机通过快捷指令的shell脚本给腾讯云服务器写入文本,文件名称是pc1客户端的查询名称,内容是要执行的指令,然后腾讯云上的ScoketServer会时刻监测是否在对应的地方有文本生成,如果有读取文件内容传递刚给对应的客户端pc1。
这里为什么要把文件名命名成客户端的SocketClient程序的名称呢?
这里主要是实现多个多个用户可以同时操作自己的电脑,pc1通过Socket连接服务器的时候会把自己的文件名传上去,然后服务器有一个map会缓存不同文件名对应的不同连接(key:文件名,value:当前连接),这样服务器就会到对应的文件夹下去取这个连接对应的文件名的内容。然后传回对应的连接。这样多个用户之间就独立执行自己的指令。注意服务器是每隔1s都会去检测当前连接对应的文件夹是否存在的,这样我们发送了指令,服务器就可以实时读取出来,最后传回对应的连接客户端。
实现代码:
CSokcetServer:服务器端是用linux下的c编程,用gcc编译
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/shm.h>
#include <pthread.h>
#include <signal.h>
#include<signal.h>
#include<sys/wait.h>
#define MYPORT 8848
#define QUEUE 20
#define BUFFER_SIZE 1024
//为了保证通讯不被中断,必须要有心跳包!heartbeat...
//cmd文件
FILE *fp;
//const char* cmdTextFile="/root/swap/cmd.txt";
char* cmdDir="/root/swap/";
//一个map,key是 连接ID,value 心跳次数[AlivedX],主机名,检测alive
int aliveMapKey[0x100];
char *aliveMapValue[0x100];
//一个map,key是 连接ID,value 读出来的命令[cmd]
int mapKey[0x100];
char* mapValue[0x100];
char* cndy=NULL;
int server_sockfd;
int globalA=1023;
int aliveSize=sizeof(aliveMapKey)/sizeof(int);
int sessionSize=sizeof(mapKey)/sizeof(int);
//map添加数据,存在就覆盖
void putValue(int key,char* value,int mapKeyA[],char *mapValueB[],int size)
{
int i;
for (i = 0; i < size; i++)
{
if (mapKeyA[i] == key)
{
mapValueB[i] = value;
}
else if (mapKeyA[i] == 0)
{
mapKeyA[i] = key;
mapValueB[i] = value;
}
break;
}
}
//map取出数据
char* getValue(int key,int mapKeyA[],char *mapValueB[],int size)
{
int i;
for (i = 0; i < size; i++)
{
if (key == mapKeyA[i])
{
printf("\n");
return mapValueB[i];
}
}
return NULL;
}
//map删除某个值
void deleteValue(int key,int mapKeyA[],char *mapValueB[],int size)
{
int i;
for (i = 0; i < size; i++)
{
if (key == mapKeyA[i])
{
mapKeyA[i]=0;
mapValueB[i] = NULL;
}
}
}
//合并两个字符串
char* concatStr(char* a, char* b)
{
int i,j;
int size = strlen(a) + strlen(b)+1;
char* ret = (char*)malloc(sizeof(char) * size);
for (i = 0; i < strlen(a); i++)
{
ret[i] = a[i];
}
for (j = 0; j < size; j++)
{
int index = j + strlen(a);
ret[index] = b[j];
}
ret[size-1] = '\0';
return ret;
}
// 线程的运行函数
void* clientInterrupt(void *arg)
{
int *conn=(int*)arg;
int len;
//等待中断
printf("线程创建成功!conn=%d\n",*conn);
int preLen=0;
int counter=0;
while(1)
{
//1s钟执行一次,10s钟之后没有响应就关闭连接、
sleep(1);
char buffer[BUFFER_SIZE];
memset(buffer,0,sizeof(buffer));
//非阻塞的接收消息
len=recv(*conn, buffer, sizeof(buffer),MSG_DONTWAIT);
//客户端发送exit或者异常结束时,退出
printf("len=%d buffer=%s\n",len,buffer);
if(len<0)
{
//客户端没有接受到任何消息 len=-1
(preLen<=0)?(counter+=1):(counter=0);
printf("counter=%d\n",counter);
if(counter==10)
{
printf("客户端出现掉网状态了!\n");
break;
}
preLen=len;
}
else if(len==0)
{
//客户端被正常关闭
printf("客户端正常关闭了!\n");
break;
}
else
{
preLen=len;
//截取前面的关键字
char alived[7];
strncpy(alived,buffer, 6);
alived[6] = '\0';
//接受客户端的心跳
if(strcmp(alived, "Alived") == 0)
{
continue;
}
//发送的 文件名字 例如cmd1 ,cmd2
printf("设置值到map:%d->%s\n",*conn,buffer);
putValue(*conn,buffer,mapKey,mapValue,sessionSize);
sleep(1);
}
}
//TODO 关闭连接
close(*conn);
close(server_sockfd);
printf("退出当前线程!\n");
pthread_exit(0);
}
int main()
{
//避免僵尸进程
signal(SIGCHLD, SIG_IGN);
int reuse = 1;
///定义sockfd
server_sockfd = socket(AF_INET,SOCK_STREAM, 0);
///定义sockaddr_in
struct sockaddr_in server_sockaddr;
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(MYPORT);
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
/*设置地址可以重复使用*/
int ret = setsockopt(server_sockfd,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse));
if(ret == -1)
{
perror("setsockopt fail");
exit(-1);
}
///bind,成功返回0,出错返回-1
if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1)
{
perror("bind");
exit(1);
}
printf("监听端口:%d\n",MYPORT);
///listen,成功返回0,出错返回-1
if(listen(server_sockfd,QUEUE) == -1)
{
perror("listen");
exit(1);
}
printf("等待客户端连接...\n");
//connTag:
while(1)
{
///客户端套接字
struct sockaddr_in client_addr;
socklen_t length = sizeof(client_addr);
int* conn = (int *)malloc(sizeof(int));
///成功返回非负描述字,出错返回-1
*conn = accept(server_sockfd, (struct sockaddr*)&client_addr, &length);
if(conn<0)
{
perror("connect");
continue;
}
printf("OK,客户端成功连接\n");
pid_t childid;
if(childid=fork()==0)//子进程
{
printf("child process: %d created.conn=%d\n", getpid(),*conn);
//多线程监听客户端中断
pthread_t thread1;
int ret = pthread_create(&thread1, NULL, clientInterrupt, (void *) conn);
pthread_detach(thread1);
//获取当前连接需要的文件名
char* cmdFileName;
while(!(cmdFileName=getValue(*conn,mapKey,mapValue,sessionSize)));
//合并路径
char* cmdTextFile=concatStr(cmdDir,cmdFileName);
printf("当前路径:%s\n",cmdTextFile);
printf("文件名:%s\n",cmdFileName);
//查看并删除cmd.txt文件
if((access(cmdTextFile,F_OK))!=-1)
{
if (remove(cmdTextFile) == 0)
{
printf ("删除%s文件成功!\n",cmdTextFile);
}
else
{
printf ("删除%s文件失败...\n",cmdTextFile);
}
}
while(1)
{
// 发送信号0,探测[中断,断网]线程是否存活
int kill_rc = pthread_kill(thread1, 0);
if (kill_rc == 3)
exit(0); //the specified thread did not exists or already quit
else if (kill_rc == 22)
exit(0); //the specified thread did not exists or already quit
// else
//cout<<"the specified thread is alive\n";
printf("当前连接:%d\n",*conn);
//文件存在才读取数据
if((access(cmdTextFile,F_OK))!=-1)
{
char buffer[BUFFER_SIZE];
/* 打开文件用于读写 */
fp = fopen(cmdTextFile, "r");
//读取 cmd命令
fgets(buffer, sizeof(buffer), fp);
printf("cmd:%s",buffer);
//发送消息到win10客户端
int sendRet=send(*conn, buffer, sizeof(buffer), 0);
printf("发送消息状态[%d]:%d\n",*conn,sendRet);
fclose(fp);
//删除文件
if (remove(cmdTextFile) == 0)
{
printf ("删除%s文件成功!\n",cmdFileName);
}
}
//延时1000ms
sleep(1);
}
}
}
close(server_sockfd);
return 0;
}
注意gcc编译的时候由于有线程和进程编译和普通编译不一样:
gcc cmdServer.c -o ./a.out -lpthread
让服务器端在后台运行:
nohup ./a.out >>./log.text &
CSocketClient:服务器端是windows下的c编程,用visual studio2019
#include <winsock2.h>
#include <windows.h>
#include <time.h>
#include <stdio.h>
#include <string.h>
#include <direct.h>
#pragma comment(lib,"ws2_32.lib")
#pragma warning(disable:4996)
//#pragma comment(linker, "/subsystem:windows /ENTRY:mainCRTStartup") // 屏蔽黑框
#define USER_ERROR -1
#define BUFFER_SIZE 1024
#define HEART_BEAT_FREQ 3000
const char* SERVER_ADDR = "106.52.95.28";
const int SERVER_PORT = 8848;
int exitChildFlag = 0;
//发送心跳
void heartBeat(int* socket_client)
{
int counter = 0;
while (1)
{
Sleep(HEART_BEAT_FREQ);
counter++;
char* buffer[0x10];
//合并字符串
sprintf_s(buffer, sizeof(buffer), "Alived%d", counter);
//发送到服务器
send(socket_client, buffer, sizeof(buffer), 0);
printf("发送心跳包[%d]:%s\n", counter, buffer);
//3s一次包
if (exitChildFlag == 1)
{
break;
}
}
}
//去除文件名后缀
char* trimSuffix(char* filename)
{
int size = strlen(filename) - 3;
char* ret = (char*)malloc(sizeof(char) * size);
for (int i = 0; i < strlen(filename); i++)
{
if (filename[i] == '.')
{
break;
}
ret[i] = filename[i];
}
ret[size - 1] = '\0';
return ret;
}
//主函数
int main(int argc, char* argv[])
{
WSADATA wsaData;
connTag:
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("Failed to load Winsock.\n");
return USER_ERROR;
}
SOCKET socket_client = socket(AF_INET, SOCK_STREAM, 0);
if (socket_client == INVALID_SOCKET)
{
printf(" Failed socket() \n");
return 0;
}
struct sockaddr_in server_in;
server_in.sin_family = AF_INET;
server_in.sin_port = htons(SERVER_PORT);
server_in.sin_addr.S_un.S_addr = inet_addr(SERVER_ADDR);
if (connect(socket_client, (struct sockaddr*)&server_in, sizeof(server_in)) == -1)
{
//连接失败
printf("connect remote server failed , 3 seconds reconnect! \n");
//window下是s为单位,linux下是ms为单位
exitChildFlag = 1;
Sleep(3000);
goto connTag;
}
else
{
exitChildFlag = 0;
//连接成功
printf("connect %s:%d\n", inet_ntoa(server_in.sin_addr), server_in.sin_port);
int sendFlag = 0;
while (1)
{
sendFlag++;
if (sendFlag == 1)
{
char fn[0x100], * p;
//获取文件名
strcpy(fn, (p = strrchr(argv[0], '\\')) ? p + 1 : argv[0]);
//去除文件名后缀
char* buffer = trimSuffix(fn);
printf("buffer=%s\n", buffer);
//发送到服务器
send(socket_client, buffer, strlen(buffer), 0);
//创建一个心跳的多线程的
DWORD dw1;
HANDLE hHandle1 = CreateThread((LPSECURITY_ATTRIBUTES)NULL,
0,
(LPTHREAD_START_ROUTINE)heartBeat,
(LPVOID)socket_client,
0,
&dw1);
}
//接收消息
char* recvData[BUFFER_SIZE];
int ret = recv(socket_client, recvData, BUFFER_SIZE, 0);
if (ret <= 0)
{
printf("connection iterrupt , 3 seconds reconnect!\n");
//window下是s为单位,linux下是ms为单位
exitChildFlag = 1;
Sleep(3000);
goto connTag;
}
//recvData[ret] = '\0';
printf("cmd:%s", recvData);
//执行系统操作
char* p = "notepad";
system(recvData);
}
}
closesocket(socket_client);
WSACleanup();
return 0;
}
屏蔽黑框:是客户端被打包成exe文件的时候双击运行的时候是否显示黑框,加上就不显示,不加折行就显示。
最后客户端打包后就是这样的,有条件的小伙伴可以自己下载visual studo2019调试编译:
我自己改了一个图标。
效果:
【服务器端】
运行CscoketServer 代码通过上边的步骤:
编译:gcc cmdServer.c -o ./a.out -lpthread
运行: nohup ./a.out >>./log.text &
查看进程:ps aux |grep a.out
查看网络:netstat -tupln |grep 8848
【客户端】
点击自己打包好的客户端exe,鼠标转两圈,然后什么也没有了,但是后台可以看到进程
此次时服务器端的变化是:
查看进程:ps aux |grep a.out
可以看到那个 状态是s的主进程,sl的是子进程。这个子进程就是我们点击的客户端产生的,我们没点击一次子进程就会多增加一次,所以在同一台电脑不要重复点击客户端,我这没有做重复进程判断。
同时服务器端设置有超时机制,当检测到客户端10s没有给客户端发送消息服务器端就会自动断开连接,服务端会多开一个线程用于3s发送一次心跳。
【手机端】
echo 那一段话就是 linux的shell指令,echo 中间那句就是我们要在windows电脑上执行的cmd命令。
总结:
大概就是这么多吧,原理其实都很简单,但是对于我一个四年没有摸过c代码的人来说,实现起来还是有点困难,特别是关于进程,线程,还有就是c语言在windows平台和linux语法各有差异,还有就是细节的处理,比如处理僵尸进程,处理多线程之间的数据共享,信号量监测线程的中断,处理因为断网导致客户端失去连接,但是服务器端依然存在连接进程的问题等等(心跳机制可解决),总之嫌麻烦就直接把我的代码拷过去改改,大概就能用了,喜欢折腾的可以自己开发一些界面出来。其实我也想在客户端加个页面,来显示当前连接状态,但是实力不允许就算了吧.....
扩展:
想想,既然能执行cmd命令了,基本上在客户端就无所不能了,我们可以预先在windows下写好比较复杂的脚本,然后通过这个cmd来调取,就可以在手机端通过简单的指令来完成比较复杂的操作,比如每天我都要看虎牙直播,但是手动去点太麻烦了,这个时候我想直接告诉siri给我打开,然后给我打开对应的主播,主播不在换另一个主播,然后画面调到高清,全屏都直接自动完成参考下一篇文章。
sikuli打开虎牙直播:xxxxxx