Remote control project notes

Posted by countrygyrl on Sat, 11 Dec 2021 07:06:17 +0100

Project introduction:

Complete the development of client (control end) + server (controlled end). The client mainly includes: acquisition of disk and file information; File download; Monitor, lock and unlock each other's screen, and the server realizes the function of automatic operation after startup.

Sorting of some codes:

1. Encapsulation of data packet

Package design diagram

CPacket(const BYTE* pData, size_t& nSize)
{
    size_t i = 0; //i: Ruler
    /*Parse Baotou*/
	for (; i < nSize; i++)
	{
		if (*(WORD*)(pData + i) == 0xFEFF)
		{
			sHead = *(WORD*)(pData + i);//Set the value of the header
			i += 2;//i move back 2 bytes to process the next data content (length of read data)
			break;
		}
	}
	/*nSize If it is less than the length of packet header (2) + packet length (4) + command (2) + checksum (2), it indicates that the parsing fails because there is no basic packet length*/
	if (i + 4 + 2 + 2 > nSize)
	{
		nSize = 0;
		return;
	}
	
	/*Read the length of the packet data*/
	nLength = *(DWORD*)(pData + i);
	i += 4;//i move back 4 bytes to process the next data content (parsing command)
	if (nLength + i > nSize)
	{//If nSize is less than the length of the packet data + (2 bytes of the header + 4 bytes of the written packet data length), the parsing fails because there is no basic packet length
		nSize = 0;
		return;
	}
	
	/*Parse command*/
	sCmd = *(WORD*)(pData + i);  //The two bytes here are commands,
	i += 2;  //i move back 2 bytes to process the next data content (parse data)
	if (nLength > 4)
	{
		strData.resize(nLength - 2 - 2);//Data length = packet data length - command (2) - sum check (2)
		memcpy((void*)strData.c_str(), pData + i, nLength - 4); //Copy data content to strData
		i += nLength - 4;//i move nLength-4 bytes backward to process the next data content (parsing and verification)
	}

    /*Parsing and verification*/
	sSum = *(WORD*)(pData + i);
	i += 2;//i moves back 2 bytes, and i points to the end of the packet
	WORD sum = 0;
	for (size_t j = 0; j < strData.size(); j++)  
		sum += BYTE(strData[j]) & 0xFF;//Sum
	if (sum == sSum)//Data received successfully
	{
		nSize = i; //The size of a complete package
		return;
	}

    /*Parsing failed, return 0*/
	nSize = 0;
}

2. Analysis of several ways of thread synchronization

a. Mutually exclusive

Under this mechanism, threads need to lock and unlock public variables after use. At this time, if there is a thread that directly accesses the variable without locking, this mechanism is invalid. Therefore, mutual exclusion depends on the programming of developers; Secondly, after thread 1lock variable, if thread 2 also needs to access the variable at this time, thread 2 will block until thread 1lock, that is, the "simultaneous" becomes "queuing", which reduces the efficiency, and the more threads, the more significant the impact on the efficiency.

b. News
WM_XXX parameters WPARAM and LPARAM. In the process of transmission, the value of the variable is copied into the parameter, so there is no problem of lock and unlock. Send message and cross thread PostThreadMessage in the same dialog box. However, the disadvantage of message is that the amount of data that parameters can carry is limited, and it depends on message queue.

c. Network
The stand-alone program also has a network (loopback network, 127.0.0.1, local network). Advantages: high speed; Multiple requests can be served; There is no need to pay attention to the queue. The message queue is processed by software, while the network queue is processed by hardware (network card). The efficiency is completely different, and there is almost no need to pay attention to the latter; Completing port mapping (epoll / IOCP) can continue to improve efficiency.

3. Implement a simple thread safe queue using IOCP

The general process is as follows: ① create a handle to complete the port object (which is taken over by the operating system) and communicate with the kernel through the handle; ② Create a thread that is responsible for processing the queue.

//Operation enumeration value
enum
{
	IocpListEmpty=0,
	IcopListPush=1,
	IocpListPop=2,
}

struct IOCP_PARAM
{
	int Opr;//operation
	std::string strData;//data
	_beginthread_proc_type cbFun;//Callback

	IOCP_PARAM() { Opr=-1; }
	IOCP_PARAM(int inOpr,const char* inData,_beginthread_proc_type inCbFun=NULL)
	{
		Opr=inOpr;
		strData=inData;
		cbFun=inCbFun;
	}
}

void threadMain(HANDLE hIOCP)
{
	std::list<std::string> lstString;
	DWORD dwTransferred=0;
	ULONG_PTR CompletionKey=0;
	OVERLAPPED* pOverlapped=NULL;
	//Gets the status of the completion port
	while(GetQueuedComletionStatus(hIOCP,&dwTransferred,
	     &CompletionKey,&pOverlapped,INFINITE))
	{
		if(dwTransferred==0 || CompletionKey==NULL)
		{
			printf("Thread is prepare to exit!\r\n");
			break;
		}
		IOCP_PARAM* parm=(IOCP_PARAM*)CompletionKey;
		//push operation
		if(parm->Opr==IocpListPush)
			lstString.push_back(parm->strData);
		//pop operation	
		if(parm->Opr==IocpListPop)
		{
			std::string* pStr=NULL;
			if(lstString.size()>0)
			{
				pStr=new std::string(lstString.front());
				lstString.pop_front();
			}
			if(parm->cbFun)
				parm->cbFun(pStr);
		}
		//empty operation
		if(parm->Opr==IocpListEmpty)
			lstString.clear();	
		delete parm;
	}
}

void threadQueueEntry(HANDLE hIOCP)
{
	threadMain(hIOCP);

	//The code ends so far that the local object cannot call the destructor, resulting in memory leakage
	_endthread();
}

void Fun(void* arg)
{
	std::string* pStr=(std::string*)arg;
	if(pStr!=NULL)
	{
		printf("Pop from list,value is : %s",pStr->c_str());
		delete pStr;
	}
	else
		printf("List is empty!");
}

int main()
{
	HANDLE hIOCP=INVALID_HANDLE_VALUE;
	//Create a completion port object
	hIOCP=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,NULL,1);//Because it is a queue, the thread transmits data 1
	//Pass completion port object to thread
	HANDLE hThread=(HANDLE)_beginthread(threadQueueEntry,0,hIOCP);

	ULONGLONG tick=GetTickCount64();
	while(_kbhit()==0)//The completion port separates the request from the implementation
	{
		//Delivery status
		if(GetTickCount64()-tick>1300)
		{
		    PostQueuedComletionStatus(hIOCP,sizeof(IOCP_PARAM),
		        (ULONG_PTR)new IOCP_PARAM(IocpListPop,"Hello World",Fun),NULL);
		}
		if(GetTickCount64()-tick>2000)
		{
		    PostQueuedComletionStatus(hIOCP,sizeof(IOCP_PARAM),
		        (ULONG_PTR)new IOCP_PARAM(IocpListPush,"Hello World"),NULL);
		    tick=GetTickCount64();
		}
		Sleep(1);
	}
	
	if(hIOCP!=NULL)
	{
		PostQueuedComletionStatus(hIOCP,0,NULL,NULL);
		WaitForSingleObject(hThread,INFINITE);
	}

	CloseHandle(hIOCP);
}

This implementation is mainly to facilitate the understanding of newly contacted IOCP. The current writing method also has a logic vulnerability. When WaitForSingleObject is executed, hIOCP is still valid. At this time, if a thread delivers data, it will lead to memory leakage.

Reason for calling threadMain separately in threadQueueEntry: when calling_ When endthread, the execution of the code is here, and the subsequent contents will not be executed (or called). Therefore, when the code is written in threadQueueEntry, the exit will lead to memory leakage, because the local object cannot call the destructor. This part of the code is taken out separately as a function. When the function execution ends, the local object will call the destructor, and then the problem of memory leakage is solved.

4. Overlapped structure

General process:

To use the overlapping structure, you need to modify the initialization slightly,

bool Init()
{
	//normal
	WSADATA data;
	if( WSAStartup(MAKEWORD(1,1),&data)!=0 )
		return false;
	return true;

	//unnormal,use overlapped
	WSADATA data;
	if( WSAStartup(MAKEWORD(2,0),&data)!=0 )
		return false;
	return true;
}
class COverLapped
{
public:
	//You have to put it first
	OVERLAPPED m_overlapped;
	DWORD      m_opr;//Operation value or command
	char       m_buffer[4096];
	COverLapped()
	{
		m_opr=0;
		memset(&m_overlapped,0,sizeof(m_overlapped));
		memset(&m_buffer,0,sizeof(m_buffer));
	}
}

enum
{
	opr_Accept=1,
	opr_Send=2,
	opr_Recv=3,
	//other opr value...
}

void testOverLapped()
{
    //Normal usage
	SOCKET sock_normal=socket(AF_INET,SOCK_STREAM,0);
	//Use of overlapping structures
	SOCKET sock_unnoemal=WSASocket(AF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED);
	if(sock_unnormal==INVALID_SOCKET)
		return;
	HANDLE hIOCP=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,sock_unnormal,4);
	//Binding IOCP to socket
	CreateIoCompletionPort((HANDLE)sock_unnormal,hIOCP,0,0);
	SOCKET client=WSASocket(AF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED);
	
	sockaddr_in addr;
	addr.sin_family=PF_INET;
	addr.sin_addr.s_addr=inet_addr("0.0.0.0");
	addr.sin_port=htons(9527);
	
	bind(sock_unnormal,(sockaddr*)addr,sizeof(addr));
	listen(sock_unnormal,5);
	
	COverLapped overlapped;
	overlapped.m_opr=opr_Accept;
	memset(&overlapped,0,sizeof(OVERLAPPED));
	DWORD received=0;
	if(!AcceptEx(sock_unnormal,client,overlapped.m_buffer,0,sizeof(sockaddr_in)+16,sizeof(sockaddr_in)+16,&received,&overlapped.m_overlapped))
		return;
	
	//todo: start a thread
	
	//Represents a thread
	while(true)
	{
		DWORD dwTransferred=0;
	    DWORD CompletionKey=0;
	    LPOVERLAPPED pOverlapped=NULL;
	    if(GetQueuedCompletionStatus(hIOCP,&dwTransferred,&CompletionKey,&pOverlapped,INFINITY))
	    {
			COverLapped* ptrOL = CONTAINING_RECORD(pOverlapped,COverLapped,m_overlapped);
			switch(ptrOL->m_opr)
			{
			case opr_Accept:
				//todo: handle accept
			case opr_Send:
				//todo: process send
			case opr_Recv:
				//todo: processing recv
			}
	    }
	}
}

5. Design and implementation of thread class and thread pool class

Why design thread pools? The main task of the thread mentioned in the code is to continuously Get queuedcompletionstatus, and IOCP can reach tens of thousands of concurrency. If each request is processed in this thread, the significance of using IOCP has been lost. Therefore, it should be correct that when the thread gets to a state, it immediately assigns the work to a new thread to continue to execute the next Get. As for the thread assigned to the work, how to process the business and how long it takes to process the business, this is not what the current main thread needs to pay attention to.

class ThreadFuncBase{};
typedef int (ThreadFuncBase::* FuncType)();
class ThreadWorker
{
public:
	ThreadWorker():thiz(NULL),func(NULL) {}
	ThreadWorker(ThreadFuncBase* obj,FuncType func);
	ThreadWorker(const ThreadWorker& worker);
	ThreadWorker& operator=(const ThreadWorker& worker);

	int operator()()
	{
		return (isValid()?(thiz->*func)():-1);
	}
	bool isValid()
	{
		return (thiz!=NULL)&&(func!=NULL);
	}
private:
	ThreadFuncBase* thiz;//Pointer to the ThreadFuncBase member object
	FuncType func;//Pointer to ThreadFuncBase member function
}

class MyThread
{
public:
	MyThread() { m_hThread=NULL; }
	~MyThread() { Stop(); }
	//Start thread true: start succeeded false: start failed
	bool Start();
	//Valid true: valid false: thread exception or terminated
	bool isValid()
	{
		//If the handle is empty, an error will be returned; Wait is returned if the handle has ended_ ABANDONED
		return WaitForSingleObject(m_hThread,0)==WAIT_TIMEOUT;
	}
	//Close thread
	bool Stop();
	//Update work
	void updateWorker(const ::ThreadWorker& worker=::ThreadWorker())
	{
		m_worker.store(worker);
	}
	//Idle true: idle can allocate work false: non idle has been allocated work
	bool isIdle() 
	{ 
		return !m_worker.load().isValid(); 
	}
private:
	void threadWorker()
	{
		while(m_bStatus)
		{
			::ThreadWorker worker=m_worker.load();
			if(worker.isValid())
			{
				int iRet=worker();
				if(iRet!=0)
					//Print warning log
				if(iRet<0)//Set an invalid when a problem occurs
					m_worker.store(::ThreadWorker());
			}
			else
				Sleep(1);
		}
	}
	static void threadEntry(void* arg)
	{
		MyThread* thiz=(MyThread*)arg;
		if(thiz)
			thiz->threadWorker();
		_endthread();
	}
private:
	HANDLE m_hThread;
	bool   m_bStatus;//false: the thread is about to shut down, true: the thread is running
	std::atomic<::ThreadWorker> m_worker;
}

void MyThreadPool
{
public:
	MyThreadPool();
	MyThreadPool(size_t size) { m_threads.resize(size); }
	~MyThreadPool();

	bool Invoke();
	void Stop();
	//Assign work return value to assign work to the number of threads, and -1 if all threads are busy
	int dispatchWorker(const ThreadWorker& worker)
	{
		m_lock.lock();
		int index=-1;
		for(size_t i=0;i<m_threads.size(),++i)
		{
			if(m_threads[i].isIdle())
			{
				m_threads[i].updateWorker(worker);
				index=i;
				break;
			}
		}
		m_lock.unlock();
		return index;
	}
private:
	std::vector<MyThread> m_threads;
	std::mutex m_lock;
}

The meaning of encapsulating a ThreadWoker class separately is that in MyThread, the threadWoreker thread function is separated from the content to be executed. The user inherits the ThreadFuncBase class, and then creates a ThreadWoker object to get the content to be executed through the updateWorker of MyThread class. The advantage of this is that when a thread finishes executing a task, there is no need to close the thread, because it needs to be re created when re allocating work, and it takes time to create a thread. After its execution, it will be empty and still keep it in while. When updateWorker gets the work, it can be executed quickly.

6. UDP penetration

a. Principle
In fact, the principle is not very complex. UDP penetration makes use of the fixed IP of the public network in the public server and the characteristics of UDP protocol that can actively send requests without response.
Under such conditions, you can use UDP to establish a connection with the public server, and establish a connection with another client through the return IP and port of the public server.
In this process, the public server must exist and the connection with the public server cannot be interrupted.
b. Network environment
When ordinary PC machines connect to the Internet, they need to first connect to the domain name server of the Internet through the operator (dial-up), and the domain name server forwards the request to the real machine server, so as to realize the internal and external network connection.

In the intranet (local area network), no matter how many networking devices there are in the LAN at the same time, they are the same temporary IP for the external network; When the devices in the same LAN are connected to the external network, the IP is the same and the ports are different; All networking devices in the same LAN share the same temporary IP in the LAN.
c. Question
Based on the network environment, the following problems will be faced:
① Because the LAN IP is temporary, it will change, so it becomes very difficult for other machines to actively connect to the PC through IP;
② Cat and router do not allow external IP to actively connect to internal network by default;
③ Even if the cat or router can send IP to the public network so that the target machine can connect to you, other machines can also connect to you, which will lead to security problems such as data leakage.
d. Protocol analysis

Based on the problems caused by the network environment, it can be determined that UDP protocol needs to be used after analyzing the communication protocol, because it almost perfectly solves the above problems.
e. Solution
① Problem with temporary IP: a public server in the public network is required to initiate a UDP request to the server through the PC, and establish a connection after obtaining the response packet from the server. The IP and port of the PC end will not change after the connection to the end.
② Firewall of cat and router: when the PC actively initiates a connection request, the firewall will actually leave a channel to receive the server's response packet to establish a connection. However, it should be noted that the PC must actively initiate the request and receive the response packet before confirming that the channel has been opened.
③ Security problem: because of the firewall mechanism, the external network cannot actively initiate requests to the PC, and the firewall will intercept these requests.
④ Connection between master and slave: since both master and slave are connected to the public server, the public server has the IP and port of both master and slave. As long as the server sends the IP and port to each other respectively, the two sides can establish a connection directly. However, even after the connection is established, both parties cannot disconnect from the server, because ① has explained that once the connection with the server is disconnected, the IP and port in the LAN will change.

void udp_server()
{
    SOCKET sock=socket(PF_INET,SOCK_DGRAM,0);
    if(sock==INVALID_SOCKET)
    	return;
    sockaddr_in server,client;
    memset(server,0,sizeof(server));
    memset(client,0,sizeof(client));
    server.sin_family=AF_INET;
    server.sin_port=htons(20000);
    server.sin_addr.s_addr=inet_addr("127.0.0.1");
}

void udp_client(bool ishost)
{
    Sleep(2000);
    sockaddr_in server, client;
    int len = sizeof(client);
    server.sin_family = AF_INET;
    server.sin_port = htons(20000);  //The udp port should preferably be between 20000 and 40000
    server.sin_addr.s_addr = inet_addr("127.0.0.1");
    SOCKET sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (sock == INVALID_SOCKET)
    	return;
    if(ishost)  //Primary client code
    {
        EBuffer msg = "Hello World!\n";
        int ret = sendto(sock, msg.c_str(), msg.size(), 0, (sockaddr*)&server, sizeof(server));
        if (ret > 0)
        {
            msg.resize(1024);
            memset((char*)msg.c_str(), 0, msg.size());
            //Receive data from the server
            ret = recvfrom(sock, (char*)msg.c_str(), msg.size(), 0, (sockaddr*)&client, &len);
            if (ret > 0)
            	//Print the acquired data
            //Receive data from another client
            ret = recvfrom(sock, (char*)msg.c_str(), msg.size(), 0, (sockaddr*)&client, &len);
            if (ret > 0)
            	//Print the acquired data
        }
    }
    else  //From client code
    {
        std::string msg = "Hello World!\n";
        int ret = sendto(sock, msg.c_str(), msg.size(), 0, (sockaddr*)&server, sizeof(server));
        if (ret > 0)
        {
            msg.resize(1024);
            memset((char*)msg.c_str(), 0, msg.size());
            ret = recvfrom(sock, (char*)msg.c_str(), msg.size(), 0, (sockaddr*)&client, &len);
            if (ret > 0)
            {
                sockaddr_in addr;
                memcpy(&addr, msg.c_str(), sizeof(addr));
                sockaddr_in* paddr = (sockaddr_in*)&addr;
                msg = "Hello,I am client!\n";
                ret = sendto(sock, (char*)msg.c_str(), msg.size(), 0, (sockaddr*)paddr, sizeof(sockaddr_in));
            }
        }
    }
    closesocket(sock);
}

int main(int argc,char* argv[])
{   
	InitSock();//socket initialization
    
    if (argc == 1)
    {
        char wstrDir[MAX_PATH];
        GetCurrentDirectoryA(MAX_PATH, wstrDir); //Gets the path of the current process
        STARTUPINFOA si;   
        PROCESS_INFORMATION pi;
        memset(&si, 0, sizeof(si));
        memset(&pi, 0, sizeof(pi));
        std::string strCmd = argv[0];//strCmd is the path of the program
        strCmd += " 1";   //Add a number 1 after the path to distinguish it, which is used to start a new process
        BOOL bRet = CreateProcessA(NULL, (LPSTR)strCmd.c_str(), NULL, NULL, FALSE, 0, NULL, wstrDir, &si, &pi);//Create a new process
        if (bRet)
        {
            CloseHandle(pi.hThread);
            CloseHandle(pi.hProcess);
            TRACE("process ID : %d\n", pi.dwProcessId); 
            TRACE("thread  ID : %d\n", pi.dwThreadId);
            strCmd += " 2";  //Then, on the basis of the above, add a number 2 after the path name to start a new process again
            bRet = CreateProcessA(NULL, (LPSTR)strCmd.c_str(), NULL, NULL, FALSE, 0, NULL, wstrDir, &si, &pi);//Create a new process
            if (bRet)   //If created successfully
            {
                CloseHandle(pi.hThread);
                CloseHandle(pi.hProcess);
                TRACE("process ID : %d\n", pi.dwProcessId);
                TRACE("thread  ID : %d\n", pi.dwThreadId);
                udp_server();// Server code, start the server
            }
        }
    }
    else if (argc == 2)//Primary client code
    	udp_client();
    else 
    	udp_client(false);//From client code
    
    ClearSock();//Clean socket
}

7. Administrator authority and startup

a. Administrator privileges:
vs can be selected during compilation: right click the item → attribute → linker → manifest file → UAC execution level → select Administrator, apply and confirm after selection, and then regenerate the project. The compiled exe file will have a shield icon in the lower right corner, that is, Administrator authority.

b. Power on
b-1: registry modification
HKEY_LOCAL_MACHINE → SOFTWARE → Microsoft → Windows → CurrentVersion → Run. The path records some boot programs.

Note that the path of the program must point to the system directory, generally system32, that is, copy the program to be started to system32. But it is better to use mklink to create a soft link under Windows.

Differences between soft links and shortcuts:
First, the file format is different, and the suffix of the shortcut is lnk;

Secondly, the value of type is also different. The shortcut starts with L and the soft link file starts with MZ. In fact, the soft link file is the original exe file, which is equivalent;


Third, the file size is different;

bool WriteRegisterTable(const CString& strPath)  //Functions required to complete startup in registry mode
{
    CString strSubKey = _T("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run");
    TCHAR sPath[MAX_PATH] = _T("");
    GetModuleFileName(NULL, sPath, MAX_PATH);
    BOOL ret = CopyFile(sPath, strPath, FALSE);
    if (ret == FALSE)
    {
        MessageBox(NULL, _T("File copy failed. Do you have insufficient permissions?\n"), _T("error"), MB_ICONERROR | MB_TOPMOST);
        return false;
    }
    HKEY hKey = NULL;
    ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, strSubKey, 0, KEY_ALL_ACCESS | KEY_WOW64_64KEY, &hKey);
    if (ret != ERROR_SUCCESS)
    {
        RegCloseKey(hKey);
        MessageBox(NULL, _T("Failed to set automatic startup after startup. Do you have insufficient permissions?\n Program startup failed!"), _T("error"), MB_ICONERROR | MB_TOPMOST);
        return false;
    }
    ret = RegSetValueEx(hKey, _T("RemoteCtrl"), 0, REG_SZ, (BYTE*)(LPCTSTR)strPath, strPath.GetLength() * sizeof(TCHAR));
    if (ret != ERROR_SUCCESS)
    {
        RegCloseKey(hKey);
        MessageBox(NULL, _T("Failed to set automatic startup after startup. Do you have insufficient permissions?\n Program startup failed!"), _T("error"), MB_ICONERROR | MB_TOPMOST);
        return false;
    }
    RegCloseKey(hKey);
    return true;
}

b-2: the second method of startup
Copying the program to the startup folder is simpler than the first method of modifying the registry, but this method needs to wait until the machine is fully started, so the time is slower than the way of modifying the registry.

BOOL WriteStartupDir(const CString& strPath)    //Directly copy the file to the startup folder to complete the functions required for startup
{
    TCHAR sPath[MAX_PATH] = _T("");
    GetModuleFileName(NULL, sPath, MAX_PATH);
    return CopyFile(sPath, strPath, FALSE);
}

Topics: C++