项目八股-0904

服务端问题总结列表

  1. 整个项目的设计流程是什么
  2. 为什么需要设计一个网络包?包是怎么解析的?
  3. 具体的功能设计
  4. IOCP是什么?为什么需要使用IOCP?IOCP是怎么使用的?
  5. 项目里遇到的问题,怎么解决的
  6. 项目中的文件操作(打开文件,操作文件)是怎么处理的
  7. 手写线程池
  8. 你是如何做到跨局域网的
  9. 你是如何看到对方计算机的屏幕图像?能讲一下原理吗?
  10. 能描述一下 MVC 设计模式吗?(请说明你在该项目中使用了哪些设计模式,并简要介绍它们的作用)
  11. 远控项目里面你是如何锁定对方屏幕
  12. 为什么你的界面编写要采用 mfc,不采用 QT?
  13. 服务器不用 linux 下的 epoll,要用 windows 的 IOCP
  14. 你的跨局域网功能是如何实现的
  15. 你为什么要进行代码的二次重构
  16. 这个项目,你是如何实现获取文件驱动信息的,用什么样的数据结构存的,为什么?
  17. 描述一下这个项目,你是如何设计 UML 的?
  18. MFC 的消息机制
  19. windows 下面 ansi 版本和 unicode 版本的函数有何区别?
  20. 假设在该项目中有一个函数处理了一个事件,但由于某种原因处理事件的时间非常长,这会导致该函数在很长时间内一直占用处理器资源,你会如何优化这个问题?
  21. 远程桌面控制软件中,为了兼容网络不稳定的环境,你会如何处理数据丢失和重传问题?
  22. 远控的被控端的流程是什么?
  23. 远控的客户端的流程是什么?
  24. 对于远控的客户端,你的控件是怎么使用和设计的?

1.整个项目的设计流程是什么

需求分析,流程分析,功能分析,代码重构,代码优化

需求分析:通过网络通信的方式,远程操作对方的Windows电脑

流程分析:被控制端设置为服务端,所以服务端需要接收来自控制端的网络请求,根据网络传输内容来执行对应的操作

功能分析:驱动和文件信息的获取,大文件的下载,远程画面的监控,远程桌面的上锁和解锁

代码重构和代码优化:降低耦合和提高内聚,提高并发

2.为什么需要设计一个网络包?包是怎么解析的?

设计一个网络传输包的原因网络包提供了一个固定的格式,使得发送和接收的数据结构化,便于解析和处理,

具体到实现,包一共有五个部分,分别为包头(0xFFEF),长度,命令,消息体,校验和;包头的设计是为了方便定位到数据包的开头,避免了其他数据的干扰,长度是命令(2字节)+校验和(2字节)+消息体的大小,校验和是防止数据的完整性和安全性

包提供的功能有序列化和反序列化:序列化是将整个包转变成字节流的形式,具体表现为先分配6+消息体大小的内存空间,获取内存空间的指针后,填入包头,长度,命令,消息,校验和(整个过程只涉及到指针的偏移和赋值拷贝的操作)

反序列化也就是将字节流的数据填入到包的成员中,在接收缓冲区中找到数据包头,之后判断接收的包是否完整,在接收数据完整前不对包进行反序列化,接收数据完整后,获得长度,消息体和校验和的信息,之后进行和校验,判断数据是否出错

3.具体的功能设计

*1.驱动信息的发送*

驱动信息,通常指的是系统中可用的磁盘驱动器信息。在大多数现代操作系统中,A盘和B盘通常被保留用于软盘驱动器,因此在实际使用中,这两个盘符往往是不存在的。此外,由于Windows系统的限制,驱动器的盘符不会超出Z盘。在获取驱动信息时,我们可以通过遍历从1到26的盘符,并调用系统API _chdrive(int) 来判断每个盘符是否存在。如果该调用返回0,则表明对应的盘符存在。为了便于后续处理,我们在第一个有效盘符之后添加一个逗号,以便清晰地分隔各个盘符,最终形成的数据包的格式为 “C,D,…,Z”。将这些信息组装成数据包后,我们便可以将其发送到控制层,以便进行进一步的处理和管理。这种方法不仅简化了驱动信息的收集过程,还确保了信息的准确传递,使得系统能够有效地识别和使用可用的磁盘驱动器。

*2.文件树信息的获取*

获取当前文件夹下的所有文件和文件夹信息的过程首先需要确定当前的路径,这一信息由客户端提供,作为后续操作的基础。为了有效表示文件和文件夹的信息,我们定义了一个数据结构 FILEINFO,其中包含四个主要成员:是否为无效文件、名称、类型(文件或文件夹)、以及是否有后续信息。这些成员能够全面描述每个文件或文件夹的状态和属性。

在具体的信息获取流程中,我们首先使用系统API _chdrive 切换到指定的当前文件夹。接着,通过 _findfirst 函数检查该文件夹中是否存在文件或子文件夹。如果成功找到,我们将进入一个 do while 循环,通过 _findnext 函数不断遍历文件夹中的所有项,直到没有更多文件或文件夹可供处理。

为了提升用户与界面的交互性,我们选择将每个文件和文件夹的信息单独封装成数据包进行发送。这种方法特别适用于当文件夹中包含大量文件(例如十万以上的文件)时,避免了将所有信息打包成一个数据包所带来的延迟。每个数据包中将包含是否为文件夹的标识和文件名等信息,然后将这些封装好的数据包逐一发送给控制端,从而确保信息的及时传递和用户界面的流畅交互。这样的设计不仅提高了系统的响应速度,也提升了用户体验。

*3.大文件传输*

在文件传输的过程中,准确获取文件的大小是至关重要的一步。为此,我们首先以二进制模式打开目标文件,这样可以确保读取到文件的所有内容。接下来,利用 fseek 函数将文件指针移动到文件的末尾,随后通过 _ftelli64函数获取当前指针的位置,从而确定文件的字节大小。一旦获取到文件的大小,我们会立即将文件指针重置到开头,以便为后续的读取操作做好准备。

针对大文件的传输,我们采用长连接的方式,这样可以在一次建立的连接中持续传输数据,显著减少因频繁建立和关闭连接所带来的延迟。在文件读取过程中,我们将文件内容分块存储在一个大小为1024字节的缓冲区中,并通过循环读取的方式逐步提取文件内容,直到整个文件被完全读取。每次读取后,我们都会将缓冲区中的内容打包成数据包,并发送给控制端进行接收。这种方法不仅提升了传输效率,还能有效利用网络资源,确保数据的完整性和传输的稳定性。此外,分块传输的方式也便于在网络状况不佳时进行断点续传,进一步增强了文件传输的可靠性,使得用户在处理大文件时体验更加流畅。

*4.文件的执行*

在进行文件执行之前,首先需要准确获取文件的具体路径。这一过程通常涉及用户输入、系统搜索或通过程序逻辑自动生成路径。获取到路径后,我们可以利用系统提供的API ShellExecuteA 来打开或执行指定的文件。ShellExecuteA 是一个强大的函数,它不仅支持执行可执行文件(如 .exe 文件),还可以打开文档、启动网页,甚至调用特定的应用程序来处理文件类型。例如,当用户希望打开一个 PDF 文件时,ShellExecuteA 可以自动调用默认的 PDF 阅读器,确保用户体验的流畅性和便利性。此外,我们还可以通过设置不同的操作参数,例如“open”、“edit”或“print”,来实现更灵活的文件处理方式。在实际应用中,这种方法不仅提高了文件操作的效率,还能有效地集成到用户界面中,提供直观的交互体验。

*5.文件的删除*

文件删除操作同样需要首先获取文件的具体路径,这一过程可以通过用户的选择、系统目录扫描或程序内部逻辑来实现。一旦确定了目标文件的路径,我们可以调用系统API DeleteFile 进行文件的删除。DeleteFile 函数能够直接删除指定路径下的文件,确保文件的彻底清除。在执行删除操作之前,通常还需要进行一些额外的检查,例如确认文件是否存在、用户是否有权限删除该文件,以及是否需要进行用户确认,以避免误删重要文件。

*6.监控画面的发送*

捕获当前屏幕的图像并将其保存到内存流中,随后可以将该图像数据用于后续的处理或传输。首先,通过调用 GetDC(NULL) 获取整个屏幕的设备上下文句柄 hScreen,接着使用 GetDeviceCaps 函数获取屏幕的位宽、宽度和高度。这些信息用于创建一个 CImage 对象 screen,该对象用于存储捕获的屏幕图像。随后,使用 BitBlt 函数将屏幕内容复制到 CImage 对象中,完成图像的捕获。为了进行后续的图像处理,代码释放了设备上下文,并在堆上分配了一块可调整大小的内存,准备将屏幕图像保存到内存流中。通过 CreateStreamOnHGlobal 函数,创建一个内存流 pStream,并将图像以 PNG 格式保存到该流中。在保存完成后,代码使用 Seek 函数将流指针重置到开头,并通过 GlobalLock 获取内存中的图像数据指针 pData。接着,获取内存的大小并将图像数据封装成一个数据包 CPacket,添加到数据包列表 lstPackets 中,以便后续的传输操作。最后,释放内存流、解锁内存并释放之前分配的内存,确保资源得到妥善管理。

*7.解锁和加锁*

在 LockMachine 函数中,首先检查一个名为 dlg 的对话框窗口句柄 m_hWnd 是否有效。如果窗口句柄为 NULL 或者 INVALID_HANDLE_VALUE,说明对话框尚未创建或已被销毁。在这种情况下,函数调用 _beginthreadex 创建一个新线程,执行 CCommand::threadLockDlg 方法,以实现对话框的锁定功能。同时,线程的 ID 被存储在 threadid 变量中,并通过 TRACE 输出以进行调试。随后,函数将一个新的数据包 CPacket(类型为 7)添加到 lstPackets 列表中,以表示锁定请求的发送。

在 UnLockMachine 函数中,主要功能是模拟按下 ESC 键,通过 PostThreadMessage 向之前创建的线程发送 WM_KEYDOWN 消息,参数为 ESC 键的虚拟键码(0x1B)。这意味着该函数的目的是向锁定的机器发送解锁信号。与 LockMachine 类似,函数也将一个新的数据包 CPacket(类型为 8)添加到 lstPackets 列表中,以表示解锁请求的发送。

*8.鼠标操作*

主要用于处理鼠标事件,包括鼠标的位置设置、点击、双击、长按和放开等操作。函数接受两个参数:一个数据包列表 lstPackets 和一个输入数据包 inPacket,用于传递鼠标事件的信息。

首先,函数通过 memcpy 将输入数据包中的数据复制到 MOUSEEVENT 结构体 mouse 中,以提取鼠标按钮和操作类型的信息。根据 mouse.nButton 的值,函数确定鼠标按钮的类型(左键、右键、滚轮或无按键),并为其设置相应的标志 nflags。如果按钮为“无按键”,则不进行位置设置;否则,通过 SetCursorPos 函数将鼠标光标移动到指定的坐标 mouse.ptXY。

接下来,函数根据 mouse.nAction 的值,进一步更新 nflags 以反映鼠标的操作类型(单击、双击、长按或放开)。在处理完按钮和操作类型后,函数使用 TRACE 输出当前的鼠标按钮和操作信息,以便于调试。

然后,函数根据 nflags 的不同值执行相应的鼠标事件操作。通过调用 mouse_event 函数,模拟不同的鼠标操作,如左键单击、双击、长按、右键操作和滚轮操作等。每种操作对应特定的 nflags 值,确保在正确的情况下触发相应的鼠标事件。

最后,函数将当前鼠标位置的信息通过 TRACE 输出,并将一个新的数据包(类型为 5)添加到 lstPackets 列表中,以表示鼠标事件的处理完成。

4.IOCP是什么?为什么需要使用IOCP?IOCP是怎么使用的?

  1. IOCP是一种通讯模型,是Windows端下高负载服务器下的一种技术。IOCP是IO端口映射,IOCP是一个异步IO的WIndows端的API,可以高效的将事件通知给应用程序,类似于Linux的Epoll。
  2. 在处理大量用户并发请求时,如果采用一个用户一个线程的方式,那将造成CPU在这成千上万的线程中进行切换,造成大量的性能浪费。而IOCP完成端口模型则完全不会如此处理,IOCP会维护一个线程池,允许程序根据当前的IO负载动态调整活跃线程的数量,因此IOCP是并行的线程数量是有一个上限,不会无限开辟新的线程,从而减少了线程上下文切换的开销和内存消耗。IOCP作为一个异步的IO编程模型,使得不阻塞线程的情况下发起IO操作,当IO操作完成时,系统会将结果通知给应用程序,提高程序运行效率。此外传统的send和recv会要求驱动缓冲,系统缓冲和用户内存相互进行内存复制和传递。当交互量很大的时候,既浪费内存也效率低下。IOCP避免了这个问题,内核对象直接给用户分配内存,然后交给驱动,少了很多内存转移的过程。IOCP
  3. 1.初始化IOCP:HADNLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0); 2.创建Socket并绑定IOCP:CreateIoCompletionPort((HANDLE)socket,iocp,(ULONG_PTR)0,0); 3.获取队列完全状态:GetQueueCompletionStatus(iocp,+32,&lpCompletion,&lpOverlapped,INFINITE);调用成功返回非零值,相关数据存于 lpNumberOfBytes、lpCompletionKey、lpoverlapped中 4.用于IOCP的特定函数:AcceptEx,ConnectEx,WSARecv,WSASend,只有这些函数可以附加重叠IO数据 5.投递一个队列完成状态来进行特殊控制PostQueuedCompletionStatus

5.项目里遇到的问题,怎么解决的

  1. 画面卡死的问题原因是缓冲区太小了。桌面的截图,如果内容比较少的时候,截图数据会比较小。一般在 200k左右。但是如果东西很多,或者桌面背景比较复杂,则图片可以超过 500k。一开始的时候,随便测试了几次,就按照余量设计了一个 400k 的缓冲区。结果后面在有壁纸的机器上,就因为图片过大,导致缓冲区塞满了,都无法解析出一个包。使得网络通信实际上卡死了。后面追加缓冲区到 1M,解决了这个问题。当然,另外一个思路是拆分图片包。一个图片如果太大,则分割为 200k 或者 300k 的若干个包。也可以防止这种情况的出现。1 描述现象 2 解释排查思路 3 解决方案 4 验证
  2. 文件列表不稳定(从日志调试的角度来回答解决过程)一开始不知道什么原因,只能加日志。因为下断点调试的时候,发现一切正常;一旦运行就会复现 bug。加上日志后,发现部分数据包丢失,但是发送端数据是完整发送的。所以确定问题出现在接收的时候。然后给 recv 直接加日志,发现这里的数据没有丢失。进而判断数据问题是出现在解析的时候。然后在解析的地方加日志,发现如果出现长数据包分批次到达客户端的时候,最容易出现丢包。最后发现是 index 出了问题,本来应该是成员变量的,结果被一个本地的同名变量给覆盖了,变成了本地变量,没有记录作用,每次缓冲区都从 0 开始接收和解析数据,使得数据发生了丢失。修改本地变量名称后,该问题消失。
  3. 驱动列表缺失最后一个驱动一开始怀疑驱动获取函数有错误,断点调试发现并不是。然后在发送端下断点,发现发送的数据正常。怀疑是接收端有错误。在接收端下断点,接收到的数据一切正常。然后顺着往后单步,结果发现发现是驱动字符串转树节点的时候出现了问题。因为解析的时候依赖逗号分隔符,最后一个驱动后面没有逗号,所以被忽略了。后面在最后追加了一个逻辑,针对最后一个驱动进行了处理。再次运行,bug 消失

6.项目中的文件操作(打开文件,操作文件)是怎么处理的

要处理文件,有几个问题需要解决:一是文件函数比较零散,但是又需要前后呼应;二是部

分文件尺寸可能较大,无法直接加载到内存中来;三是文件读写其实是比较耗时间的。

对于第一个问题,一般是将文件封装到类当中,打开和关闭可以在构造和析构里面做。也可

以防止打开忘记关闭的情况。第二种情况,则需要按照需求分批次读取。这种需要能够在指定位置

读取指定大小的数据。第三种情况则往往需要配合线程,进行异步操作。(一种是直接在线程中读

取;一种是使用 select 异步 io;还有一种是使用 IOCP 来读取文件。)

7.手写线程池

1 首先要先封装线程类。线程的创建、销毁、设置、启动、停止和主线程函数都要设置好接口。

2 然后设计线程池类。需要设计好接口:初始化、大小调整、添加任务、关闭、获取尺寸、启动等。

线程池有一个任务列表属性,一个事件属性和一个互斥锁。外部投递进来的任务先是加入任务列表

属性。然后设置事件属性。每个子线程都会等待事件。一旦事件被设置,则获取到该事件的线程就

会通过互斥锁来同步任务列表,取其中一个任务出来开始执行。

3 设计任务类。任务类需要有一个任务函数接口。这个接口线程类会自动调用。此外需要封装一些

做参数用的属性。一般任务类是基类,用户可以依据自己的需求去派生子类。子类重载任务函数即

可,也可以自己追加一些属性,用于任务函数。

4 最后整合三个模块,一个最原始的线程池就完成了

#pragma once
#include "pch.h"
#include <atomic>
#include <vector>
#include <mutex>
#include <Windows.h>

class ThreadFuncBase {};
typedef int(ThreadFuncBase::*FUNCTYPE)();
class ThreadWorker {
public:
	ThreadWorker() : thiz(NULL), func(NULL) {};

	ThreadWorker(void* obj, FUNCTYPE f) :thiz((ThreadFuncBase*)obj), func(f) {}

	ThreadWorker(const ThreadWorker& worker) {
		thiz = worker.thiz;
		func = worker.func;
	}
	ThreadWorker& operator=(const ThreadWorker&& worker) {
		if (this != &worker) {
			thiz = worker.thiz;
			func = worker.func;
		}
		return *this;
	}

	int operator()() {
		if (IsValid()) {
			return (thiz->*func)();
		}
		return -1;
	}
	bool IsValid() const {
		return (thiz != NULL) && (func != NULL);
	}
	ThreadFuncBase* thiz;
	FUNCTYPE func;
};

class EdoyunThread
{
public:
	EdoyunThread() {
		m_hThread = NULL;
		m_bStatus = false;
	}
	~EdoyunThread() {
		Stop();
	}

	//true 表示成功 false表示失败
	bool Start() {
		m_bStatus = true;
		m_hThread = (HANDLE)_beginthread(&EdoyunThread::ThreadEntry, 0, this);
		if (!IsValid()) {
			m_bStatus = false;
		}
		return m_bStatus;
	}

	bool IsValid() { //返回true 表示有效  返回false表示线程异常或者已经终止
		if (m_hThread == NULL || (m_hThread == INVALID_HANDLE_VALUE)) return false;
		return WaitForSingleObject(m_hThread, 0) == WAIT_TIMEOUT;
	}

	bool Stop() {
		if(m_bStatus == false) return true;
		m_bStatus = false;
		DWORD ret = WaitForSingleObject(m_hThread, 1000);
		if (ret == WAIT_TIMEOUT) {
			TerminateThread(m_hThread,-1);
		}
		UpdateWorker();
		return ret == WAIT_OBJECT_0;
	}
	
	void UpdateWorker(const ::ThreadWorker& worker = ::ThreadWorker()) {
		if (m_worker.load() != NULL && (m_worker.load() != &worker)) {
			::ThreadWorker* pWorker = m_worker.load();
			TRACE("delete pWorker = %08X m_worker = %08X\r\n", pWorker, m_worker.load());
			m_worker.store(NULL);
			delete pWorker;
		}
		if (m_worker.load() == &worker) return;
		if (!worker.IsValid()) {
			m_worker.store(NULL);
			return;
		}
		::ThreadWorker* pWorker = new ::ThreadWorker(worker);
		TRACE("new pWorker = %08X m_worker = %08X\r\n", pWorker, m_worker.load());
		m_worker.store(new ::ThreadWorker(worker));
	}
	
	//true表示空闲 false表示已经分配了工作
	bool IsIdle() {
		if (m_worker.load() == NULL) return true;
		return !m_worker.load()->IsValid();
	}
private:
	 void ThreadWorker() {
		while (m_bStatus) {
			if (m_worker.load() == NULL) {
				Sleep(1);
				continue;
			}
			::ThreadWorker worker = *m_worker.load();
			if (worker.IsValid()) {
				if (WaitForSingleObject(m_hThread, 0) == WAIT_TIMEOUT) {
					int ret = worker();
					if (ret != 0) {
						CString str;
						str.Format(_T("thread found warning code %d\r\n"), ret);
						OutputDebugString(str);
					}
					if (ret < 0) {
						::ThreadWorker* pWorker = m_worker.load();
						m_worker.store(NULL);
						delete pWorker;
					}
				}

				
			}
			else {
				Sleep(1);
			}
			

		}
	}
	static void ThreadEntry(void* arg) {
		EdoyunThread* thiz = (EdoyunThread*)arg;
		if (thiz) {
			thiz->ThreadWorker();
		}
		_endthread();
	}
private:
	HANDLE m_hThread;
	bool m_bStatus; //false 表示线程将要关闭  true 表示线程正在运行
	std::atomic<::ThreadWorker*> m_worker;
};


class EdoyunThreadPool  //线程池
{
public:
	EdoyunThreadPool(size_t size) {
		m_threads.resize(size);
		for (size_t i = 0; i < size; i++)
			m_threads[i] = new EdoyunThread();
	}
	EdoyunThreadPool() {}
	~EdoyunThreadPool() {
		Stop();
		for (size_t i = 0; i < m_threads.size(); i++) {
			delete m_threads[i];
			m_threads[i] = NULL;
		}
		m_threads.clear();
	}
	bool Invoke() {
		bool ret = true;
		for (size_t i = 0; i < m_threads.size(); i++) {
			if (m_threads[i]->Start() == false) {
				ret = false;
				break;
			}
		}
		if (ret == false) {
			for (size_t i = 0; i < m_threads.size(); i++) {
				m_threads[i]->Stop();
			}
		}
		return ret;
	}
	void Stop() {
		for (size_t i = 0; i < m_threads.size(); i++) {
			m_threads[i]->Stop();
		}
	}
	//返回-1  表示分配失败,所有线程都在忙 大于等于0,表示第n个线程分配来做这个事情
	int DispatchWorker(const ThreadWorker& worker) {
		int index = -1;
		m_lock.lock();
		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;
	}

	bool CheckThreadValid(size_t index) {
		if (index < m_threads.size()) {
			return m_threads[index]->IsValid();
		}
		return false;
	}
private:
	std::mutex m_lock;
	std::vector<EdoyunThread*> m_threads;
};

8.你是如何做到跨局域网的

概述

UDP穿透是一种用于在NAT(网络地址转换)环境中建立UDP连接的技术。

UDP穿透允许两个位于不同NAT设备(路由器,猫)后的设备相互通信,而无需依赖中央服务器。

PC通过路由器(能够产生临时IP,有一定防火墙功能阻止外面看到局域网IP)->拨号(运营商提供服务)连接到Internet上去

NAT的工作原理

NAT设备会将内部网络的私有IP转换为公共IP。NAT设备会记录内部连接的源IP和端口,并在外部与之关联,由于NAT的存在,外部设备无法直接访问内部设备

UDP的无连接特性

UDP是一种无连接的协议,意味着没有建立连接和维护连接的过程

UDP穿透的基本思想

UDP穿透的基本思想是利用NAT设备的行为,来创建一个孔以允许UDP数据包通过。通过同时在两个NAT后的设备上发送数据包,NAT设备可以记录下这些数据包的源IP和端口,从而允许后续的数据包通过

UDP穿透的过程

  1. 客户端注册。两个客户端(A和B)首先向一个公共的信令服务器发送注册请求,报告他们的公共IP地址和端口号
  2. 获取各自的地址。信令服务器接收到客户端A和B的请求后,记录下他们的公共IP和端口,并将A的地址发送给B,将B的地址发送给A
  3. 同时发送数据包。客户端 A 和客户端 B 在收到对方的地址后,几乎同时向对方的公共 IP 和端口发送 UDP 数据包。这个数据包可以是一个简单的“心跳”包或任何有效的 UDP 数据包。由于 NAT 的工作机制,两个 NAT 设备会记录下这两个数据包的源 IP 和端口,并在它们的 NAT 表中建立相应的映射。
  4. 建立连接。一旦 NAT 设备记录了这些映射,后续的数据包就可以通过 NAT 设备进行传输。客户端 A 现在可以向客户端 B 发送数据,反之亦然。
  5. 保持连接。为了保持 NAT 映射的活跃,客户端可能会定期发送数据包(例如“心跳”包),以防 NAT 设备超时清除映射。

信令服务器:信令服务器通常是一个简单的服务器,负责协调和交换信息。它不参与实际的数据传输。

NAT 类型:不同类型的 NAT(如全锥形 NAT、限制锥形 NAT、端口限制锥形 NAT、对称 NAT)对穿透的支持程度不同。全锥形和限制锥形 NAT 通常更容易进行 UDP 穿透,而对称 NAT 则更难处理。

UDP 穿透是一种利用 NAT 行为来建立 UDP 连接的技术。通过依赖公共信令服务器和同时发送数据包,两个 NAT 后的设备可以相互通信。尽管 UDP 穿透在许多情况下都能成功,但其效果依赖于 NAT 的类型和网络环境。

需求->网络环境->问题->协议tcp udp->设计方案

需求:如何就不同局域网的客户端连接起来

网络环境:不同的局域网下的两个设备的网络连接

问题:如何在保证安全,临时IP,拥有路由器防火墙时使得两个客户端能够连接

协议:由于tcp是维护连接的,所以无法直接暴露自身的IP情况下建立连接,而且就算建立连接的情况下,由于临时IP的存在也会可能破坏连接;所以udp就能满足,UDP是不维护连接的,只在乎发来消息的ip和端口,所以无需建立连接

设计方案:基于一个拥有固定IP的公网服务器的情况下,两个客户端向该服务器发送各自的IP和端口,再由公网服务器向两个客户端发送各自的信息给NAT设备记录端口和ip,这样就相当于建立一个公网连接

UDP穿透的示例

void initsock()
{
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData))
    {
        TRACE("WSAStartup failed");
    }
}
​
void clearsocket()
{
    WSACleanup();
}
​
void udp_server()
{
    printf("%s(%d):%s\r\n",__FILE__,__LINE__,__FUNCTION__);
    SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock == INVALID_SOCKET)
    {
        return;
    }
    std::list<sockaddr_in> lstclients;
    sockaddr_in server, client;
    memset(&server, 0, sizeof(server));
    memset(&client, 0, sizeof(client));
    server.sin_family = PF_INET;
    server.sin_port = htons(20000);
    server.sin_addr.s_addr = inet_addr("127.0.0.1");
    if (bind(sock, (sockaddr*)&server, sizeof(server)) == -1)
    {
        closesocket(sock);
        return;
    }
    std::string buffer;
    buffer.resize(1024 * 256);
    memset((char*)buffer.c_str(), 0, buffer.size());
    int len = sizeof(client);
    int ret = 0;
    while (!_kbhit())
    {
        ret = recvfrom(sock, (char*)buffer.c_str(), buffer.size(), 0, (sockaddr*)&client, &len);
        if (ret > 0) {
            //CEdoyunTool::Dump((BYTE*)buffer.c_str(), ret);
            if(lstclients.size() <= 0)
            { 
                lstclients.push_back(client);
                printf("%s(%d):%s ip : %08x port : %d \r\n", __FILE__, __LINE__, __FUNCTION__,
                    client.sin_addr.s_addr, ntohs(client.sin_port));
                ret = sendto(sock, buffer.c_str(), ret, 0, (sockaddr*)&client, len);
                printf("%s(%d):%s\r\n", __FILE__, __LINE__, __FUNCTION__);
            }
            else {
                memcpy((void*)buffer.c_str(), &lstclients.front(), sizeof(lstclients.front()));
                ret = sendto(sock, buffer.c_str(), sizeof(lstclients.front()), 0, (sockaddr*)&client, len);
            }
        }
        else {
            
        }
    }
    getchar();
    closesocket(sock);
    printf("%s(%d):%s\r\n", __FILE__, __LINE__, __FUNCTION__);
}
void udp_client(bool is_host = true)
{
    Sleep(1000);
    SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock == INVALID_SOCKET)
    {
        return;
    }
    sockaddr_in addr,client;
    int len = sizeof(client);
    addr.sin_family = PF_INET;
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    addr.sin_port = htons(20000);
    if (!is_host)
    {
        printf("%s(%d):%s\r\n", __FILE__, __LINE__, __FUNCTION__);
        std::string msg("hello world!\r\n");
        int ret = sendto(sock, msg.c_str(), msg.size(), 0, (sockaddr*)&addr, sizeof(addr));
        printf("%s(%d):%s ret : %d\r\n", __FILE__, __LINE__, __FUNCTION__,ret);
        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);
            printf("host %s(%d):%s ret : %d\r\n", __FILE__, __LINE__, __FUNCTION__, ret);
            if (ret > 0)
            {
                printf("%s(%d):%s ip : %08x port : %d \r\n", __FILE__, __LINE__, __FUNCTION__,
                    client.sin_addr.s_addr, ntohs (client.sin_port));
                printf("%s(%d):%s msg : %s ret: %d\r\n", __FILE__, __LINE__, __FUNCTION__,msg.c_str(), ret);
            }
            ret = recvfrom(sock, (char*)msg.c_str(), msg.size(), 0, (sockaddr*)&client, &len);
            printf("host %s(%d):%s ret : %d\r\n", __FILE__, __LINE__, __FUNCTION__, ret);
            if (ret > 0)
            {
                printf("%s(%d):%s ip : %08x port : %d \r\n", __FILE__, __LINE__, __FUNCTION__,
                    client.sin_addr.s_addr, ntohs(client.sin_port));
                printf("%s(%d):%s msg : %s ret: %d\r\n", __FILE__, __LINE__, __FUNCTION__, msg.c_str(), ret);
            }
​
        }
    }
    else {
        printf("%s(%d):%s\r\n", __FILE__, __LINE__, __FUNCTION__);
        std::string msg("hello world!\r\n");
        int ret = sendto(sock, msg.c_str(), msg.size(), 0, (sockaddr*)&addr, sizeof(addr));
        printf("%s(%d):%s ret : %d\r\n", __FILE__, __LINE__, __FUNCTION__, ret);
        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);
            printf("client %s(%d):%s ret : %d\r\n", __FILE__, __LINE__, __FUNCTION__, msg.size());
            if (ret > 0)
            {
                sockaddr_in adddr;
                memcpy(&adddr, msg.c_str(), sizeof(adddr));
                printf("%s(%d):%s ip : %08x port : %d \r\n", __FILE__, __LINE__, __FUNCTION__,
                    client.sin_addr.s_addr, ntohs(client.sin_port));
                printf("%s(%d):%s msg : %s ret : %d\r\n", __FILE__, __LINE__, __FUNCTION__, msg.c_str(), msg.size());
                printf("%s(%d):%s ip : %08x port : %d \r\n", __FILE__, __LINE__, __FUNCTION__,
                    adddr.sin_addr.s_addr, ntohs(adddr.sin_port));
                msg = "hello , i am client";
                ret = sendto(sock, (char*)msg.c_str(), msg.size(), 0, (sockaddr*)&adddr, sizeof(sockaddr_in));
            }
        }
    }
    closesocket(sock);
}
​
int main(int argc , char * argv[])
{
    if (!CEdoyunTool::Init()) return 1;
    // Iocp();
    initsock();
    if (argc == 1)
    {
        // 服务器
        char wstrDir[MAX_PATH];
        GetCurrentDirectoryA(MAX_PATH, wstrDir);
        STARTUPINFOA si;
        PROCESS_INFORMATION pi;
        memset(&si, 0, sizeof(si));
        memset(&pi, 0, sizeof(pi));
        string strCmd = argv[0];
        strCmd += " 1";
        BOOL bRet =  CreateProcessA(NULL,(LPSTR)strCmd.c_str(), NULL, NULL, FALSE,0 ,NULL,wstrDir,&si,&pi );
        if (bRet)
        {
           CloseHandle(pi.hThread);
           CloseHandle(pi.hProcess);
           TRACE("进程id : %d\r\n",pi.hProcess);
           TRACE("线程id : %d\r\n",pi.hThread);
           strCmd += " 2";
           bRet = CreateProcessA(NULL, (LPSTR)strCmd.c_str(), NULL, NULL, FALSE, 0, NULL, wstrDir, &si, &pi);
           if (bRet)
           {
               CloseHandle(pi.hThread);
               CloseHandle(pi.hProcess);
               TRACE("进程id : %d\r\n", pi.hProcess);
               TRACE("线程id : %d\r\n", pi.hThread);
               udp_server();
           }
        }
​
    }
    else if (argc == 2)
    {// 主客户端
        udp_client();
    }
    else {
     // 从客户端
        udp_client(false);
    }
    clearsocket();
    return 0;
}

9.你是如何看到对方计算机的屏幕图像?能讲一下原理吗?

基本原理就是将对方屏幕显示做成图片,然后回传到监控机器,只要回传的速度够快,我们

就能观察到对方屏幕的活动。

但是考虑到屏幕上一个点是四个字节(RGBA),一般 1920×1080 的分辨率会有至少 829440

个字节的图片需求。我们会在截屏之后,将其压缩为 png 格式的图片,来缩小需要的内存空间。不

用 jpg,是因为 png 能在相同的清晰度情况下,有更小的体积

10.能描述一下 MVC 设计模式吗?(请说明你在该项目中使用了哪些设计模式,并简要介绍它们的作用)

我在该项目中使用了 MVC 设计模式和 IOCP 模型。

MVC 设计模式是一种用于开发用户界面的模式,它将应用程序分为三个部分:模型、视图和控制器。其中,模型代表应用程序的数据和业务逻辑,视图是用户界面,控制器负责逻辑控制。MVC设计模式有助于提高应用程序的可重用性和可维护性,对于远程桌面控制软件中的用户界面设计和业务逻辑实现非常适用。

IOCP 模型是一种基于事件驱动的异步 I/O 模型,它能够高效地处理大量连接和数据传输请求,提高系统的性能和稳定性。在远程桌面控制软件中,使用 IOCP 模型可以实现高效的网络通信处理大量的连接和数据传输请求。

M 表示 model,即模型层

V 表示 view,即显示层、表示层、界面层

C 表示 controller,即控制层界面层不关心具体逻辑,也不关心具体数据。如果界面层需要数据,则开放接口或者属性,让其他层来调用。注意,界面层需要处理用户的交互。将用户的行为反馈给控制层,但是如何处理要依据具体的情况来定。如果是界面层的处理,则自己完成;如果还涉及业务处理,则交由控制层来处理

模型层只关心数据模型,对于界面层如何展示、控制层如何使用,毫不关心。如果外部需要使用某些数据,或者进行某些数据处理,模型层会开放接口出来,供外部使用。

控制层是 MVC 的核心,这层的代码可复用性最差,一般都是和具体业务深度绑定的。控制层负责整个产品的逻辑和调度。从界面层来的用户请求,到模型层的数据摘选,以及业务进度的控制,都是在这一层完成的

11.远控项目里面你是如何锁定对方屏幕

锁屏本质上是要控制用户的所见和所行

所以这里要做几件事情

1 控制用户所能看到的东西

将我们的程序铺满屏幕

将我们的程序置顶显示

2 控制用户的行为

屏蔽用户的按键输入。

屏蔽系统按键的输入。

所谓屏蔽,就是接收这些消息,但是不做任何回应

12.为什么你的界面编写要采用 mfc,不采用 QT?

具体采用什么框架是要按照实际需求来定的。

一般 QT 用于多平台的情况,比如播放器。这种可以不需要怎么修改的情况下,适配各种

平台,而在 window 上多用 wtl、mfc。考虑到开发便捷性,我这里选取了 mfc。Vs 对 mfc 的开发

支持比较充足,开发起来也容易一些

【远控分为控制端和被控端。

对于被控端要求体积小、能后台以服务方式运行、随时能响应和处理系统消息或者用户消

息。这种显然使用带 MFC 的命令行程序要强于 Qt。

首先,因为 Qt 本身是基于信号和槽的机制,而非消息机制。所以它在处理消息这块有很大

的不便。其次,Qt 适合做 ui,而非做服务程序。最后被控端还是有一些 UI 需求的,用带 MFC 的命令行程序,就是更好的选择。而且命令

行程序转化成后台服务程序也更加容易。

所以被控端应该使用更容易使用的带 MFC 的命令行程序,这是最佳下选择。

对于控制端,当然也可以使用 Qt 做,但是这样会导致一套项目采用两种架构。

无论是联调、代码维护、版本控制,都有很大的不便

而使用 MFC 做,则可以最大限度的降低这些因素的影响。

VS 不仅仅支持多个项目的启动,还支持多个项目的同时调试。

而且可以将两套项目加入同一个解决方案,这样代码维护和版本控制也比较容易进行。所

以我最后选择了 MFC 而没有采用 Qt。

13.服务器不用 linux 下的 epoll,要用 windows 的 IOCP

首先远控项目是 window 下的项目,所以只能在 window 下运行的 epoll 肯定是不能使用的。

其次,原本是打算用 select 的,但是考虑到未来的扩展,所以使用了能够支持更大并发,效

率更高的 ICOP。

我们的远控是基于 Windows 系统的远程控制。Linux 下面大多数是命令行环境,图形界面比

较少。如果单纯是命令行界面,开源的 OpenSSH 已经足够了,没有必要再重复造轮子。

而 Windows 下的开源远程桌面项目,很少有成熟易用的 C++项目。所以我们只能自己实现

一个 Windows 下的远程桌面系统。

此外,作为网络服务器的是被控端。既然被控端设定是 Windows 系统,我们肯定是无需考

虑 Linux 的问题。

虽然控制端和被控端都无需使用 Linux,但是我最后还做了一些 UDP 穿透的尝试。

UDP 穿透需要一台公网的服务器作为桥梁。这台服务器,我们可以使用 Linux 做服务器,自

然也就可以使用 epoll 作为提高通信并发能力的强力技术。

最后我想说的是,其实无论是 epoll 还是 IOCP,只是不同系统下,用于提高 IO 并发能力的

技术。我会依据项目实际需求来使用,而不是无脑随意炫技式使用。毕竟这些技术还需要搭配 Reactor

模式来使用,会使得整个项目的开发成本,维护成本变高。

这些问题,都是我在项目第一次重构的时候,切身的体会。按需开发,才能减少开发周期,

降低开发成本。】

14.你的跨局域网功能是如何实现的

我的跨局域网功能,是利用 UDP 穿透技术来实现的。

和 TCP 通信不同,UDP 通信允许一个套接字既可以做客户端发送数据,也可以做服务端接收数据。

但是两台局域网内的机器之间,是无法知道对方真实 IP 和端口的。

所以要跨局域网,还必须要借助一个第三方服务器。

这个服务器的地址,是局域网内的机器所知道,并且能访问的。

有了这台服务,两台不同局域网内的计算机,就可以按照下面的步骤建立通信:

第一步,在第三方服务器上创建一个 UDP 服务器。然后等待客户端接入。

第二步,发起者创建 UDP 服务器,然后连接第三方服务器。连接完成之后,会向服务器发送一个数

据包,并且服务器会应答一个数据包。此时,公网上的网关会保留这个通信入口给本机。

第三步,接收者创建 UDP 服务器,然后连接第三方服务器。连接完成之后,会向服务器发送一个数

据包,并且服务器会应答一个数据包。此时,公网上的网关会保留这个通信入口给本机。

第四步,第三方服务器将发起者的地址发给接收者,将接收者的地址发给发起者。

第五步,收发双方拿到对方地址后,就可以通过地址开始发送消息。此时,因为网关保留了一个入

口给两者,所以两者可以自由的进行通信,不再需要第三方服务器了。

注意,这种方式会导致本地通信暴露在公网上,使用的时候请谨慎进行。

15.你为什么要进行代码的二次重构

代码重构是开发当中经常出现的情况,尤其是开发需求经常变化,项目经验匮乏,或者业务

场景相对复杂的项目。本次项目重构主要的原因是我没有同类型的项目开发经验。导致一些设计和

构想后面发现有缺陷,需要跟进开发过程中掌握的新的经验,来重新组合和调整。如果下次有类似的项目,这些弯路就完全可以避免。

比如一开始是打算用同步网络通信的,结果后面发现一些交互是比较耗时的。比如读取文件夹下的文件信息,比如文件传输功能。同步通信的过程会导致消息处理函数无法及时返回,从而导致界面卡顿。后面改成了异步网络通信,这种问题就消失了。但是在一开始,我是完全没有预想到这种情况的。

最后,重构也不是全部推倒重来。而是尽可能保留原有的大部分代码的情况,进行重构。最好像精准的手术一样,只切除有问题的部分,替换上新的部分。这也要求设计的时候,时刻遵循高内聚,低耦合的原则。出现问题的时候,不会导致问题扩散到项目的每个模块之中去。

1 算法优化 a 封包,解决 tcp 的粘包;b if-else 转化为 key-value 的回调函数;key-value

是线性的

2 流程优化 if-else 转化为 key-value 的回调函数;key-value 是线性的

3 业务匹配: a 文件列表 特殊的一个包结束 测试的文件比较少;一次通信就可以发过来;

上千个文件,树状存储;next 标记;

B 显示图片

4 代码的可读性

16.这个项目,你是如何实现获取文件驱动信息的,用什么样的数据结构存的,为什么?

文件驱动器有不少函数,这里选择的是_chdrive 函数

选择这个函数,考虑的是兼容性和可靠性。

这个函数是修改当前的驱动,成功返回 0,失败返回-1

遍历 1~26,可以确保返回的驱动都能够访问

而其他函数不具备这个特性,比如 GetLogicalDrivers 函数

有时候会返回空的光驱盘符,但是该盘无法访问,进而导致其他问题

而且这个函数无论是 server 还是桌面系统,都支持

但是 GetLogicalDrivers 则支持到 2003,2000 等更古老的系统就不支持了

17.描述一下这个项目,你是如何设计 UML 的?

这个项目我用到了 UML 中的类图、时序图和状态机。

一开始是从类图入手的。一般是依据架构、功能来设计类

比如 UI、控制层、模型层、通信层、命令处理层等等

给出名称、大致的接口、预期的参数和返回值

然后就是依据业务逻辑开始画时序图从测试业务、驱动读取业务、文件读取业务、远程监控业务等等开始一条条的整理

在具体的业务处理过程中,必然会有程序状态转换的情况

这个时候就画一下状态转换图来梳理一下思路

这样整个项目的 UML 就差不多出来了

如果是其他项目,用类似的方法也可以一样实现。当然如果涉及数据库,可能还需要补充数据流图

和数据词典。把数据库的逻辑也整理出来。

类图是静态图,用来辅助设计的,是详细设计阶段我们需要做的事情。

在这个阶段,技术选型已经完成,系统架构也大体拟定,我们需要更为详细的模块设计。

类图可以将模块细化为各个类,并且确定一些重要的接口和属性。

初步完成类图之后,重要的接口和属性已经定下来了,

接下来我们就需要参考业务逻辑来画时序图。

比如测试接口、驱动读取、文件上传和下载、远程监控等等业务。

此时一些细节的内部和外部接口,会随着时序图的深入而被发现和确定。

各个接口的参数,也会初步拟定。

当时序图遇到一些复杂的情况,我们可能还需要状态图来帮助我们梳理一下状态

比如远程状态有哪些,各个状态之间是如何转换的,转换的条件和接口是什么?

通过状态图,我们可以把模糊的状态明确化,已知状态的转换确定下来。

然后在使用状态图,完善时序图,明确类图当中的接口参数和属性。

这样整个项目的 UML 就逐步完成了。

如果项目当中涉及到数据库,则可能还需要补充数据字典、数据流图。

明确数据项的含义、范围和类型,

梳理出数据如何产生、使用和存储的。

然后在结合时序图,把类图也相应的进行完善。

最后把数据库的逻辑也纳入到 UML 的描述之中。

18.MFC 的消息机制

1 消息机制:(是什么?名词解释

消息简单的说就是指通过输入设备向程序发出指令要执行某个操作。具体的某个操作是你的一系列代码。称为消息处理函数。

MFC 的消息机制基于 Windows 消息处理模型,主要用于响应用户操作(如鼠标点击、键盘输入等)和系统事件(如窗口重绘、定时器事件等)。消息是应用程序与操作系统、用户之间的通信方式。

  • 消息:消息是一个结构体,包含了事件的类型、相关数据和发送者的信息。每个消息都有一个唯一的标识符(消息 ID)。
  • 消息队列:操作系统维护一个消息队列,用于存储待处理的消息。应用程序通过消息循环从队列中获取消息并处理。
  • 消息映射:MFC 使用消息映射机制将消息与处理函数关联起来,允许开发者定义如何响应特定的消息。

2 为什么 MFC 要引入消息映射机制?(为什么?)

一个程序是否拥有多个窗体,主窗口就算只有一个,那菜单、工具条、控件这些都是子窗口,那我们需要写多少个 switch case,并且还要为每个消息分配一个消息处理函数,这样做是多么的复杂呀。因此 MFC 采用了一种新的机制,利用一个数组,将窗口消息和相对应的消息处理函数进行映射,可以理解成这是一个表,这张表在窗口基类 CWnd 定义,派生类的消息映射表如果你没有动作它是空的,也就是说如果你不手工的增加消息处理函数,则当派生窗口接受一个消息时会执行父类的消息处理函数。简单地说就是大大提升了效率。

MFC 的消息循环和分发机制高效且可靠。通过 GetMessageTranslateMessageDispatchMessage 等函数,MFC 能够快速处理消息并将其分发到相应的窗口过程,保证了应用程序的响应速度。

MFC 消息机制支持多种类型的消息,包括系统消息、用户输入消息、定时器消息、自定义消息等。这使得开发者能够灵活应对不同的事件和需求;开发者可以定义自定义消息,扩展程序的功能。通过使用 WM_USER 常量及其后的值,开发者能够创建适合特定应用需求的消息,增强了程序的灵活性和可扩展性。消息映射和处理函数的分离使得开发者可以更容易地理解和修改代码。MFC 消息机制与多线程支持良好集成。通过消息队列,开发者可以在多线程环境中安全地处理消息,避免了直接的线程间通信复杂性。

3 步骤(怎么做

步骤如下:

第一步:定义消息。Microsoft 推荐用户自定义消息至少是 WM_USER+100,因为很多新控件也要使用 WM_USER 消息。

#define WM_MYMESSAGE (WM_USER + 100)

第二步:实现消息处理函数。该函数使用 WPRAM 和 LPARAM 参数并返回 LPESULT。

LPESULT CMainFrame::OnMyMessage(WPARAM wParam, LPARAM lParam)

{

// TODO: 处理用户自定义消息,填空就是要填到这里。

return 0;

}

第三步:在对应响应消息的类头文件的 AFX_MSG 块中说明消息处理函数:

// {{AFX_MSG(CMainFrame)

afx_msg LRESULT OnMyMessage(WPARAM wParam, LPARAM lParam);

//}}AFX_MSG

DECLARE_MESSAGE_MAP()

第四步:在用户类的消息块中,使用 ON_MESSAGE 宏指令将消息映射到消息处理函数中。

ON_MESSAGE( WM_MYMESSAGE, OnMyMessage )

19.windows 下面 ansi 版本和 unicode 版本的函数有何区别?

一.定义部分: ANSI:char str[1024]; 可用字符串处理函数:strcpy(

), strcat( ), strlen( )等等。 UNICODE:wchar_t str[1024];可用字符

串处理函数

二.可用函数: ANSI:即 char,可用字符串处理函数:

strcat( ),strcpy( ), strlen( )等以 str 打头的函数。

UNICODE:即 wchar_t 可用字符串处理函数:wcscat(),wcscpy(),wcslen()等以

wcs 打头的函数。

三.系统支持 Windows 98 :只支持 ANSI。

Windows 2k :既支持 ANSI 又支持 UNICODE。

Windows CE :只支持 UNICODE。

四.如何区分

在我们软件开发中往往需要即支持 ANSI 又支持 UNICODE,不可能在要求类型转换的时候,重新

改变字符串的类型,和使用于字符串上的操作函数。为此, 标准 C 运行期库和 Windows 提供了

宏定义的方式。

在 C 语言里面提供了 _UNICODE 宏(有下划线),在 Windows 里面提供了 UNICODE 宏(无下划

线),只要定了_UNICODE 宏和 UNICODE 宏,系统就会自动切换到 UNICODE 版本,否则,系统按照 ANSI 的方式进行编译和运行。

20.假设在该项目中有一个函数处理了一个事件,但由于某种原因处理事件的时间非常长,这会导致该函数在很长时间内一直占用处理器资源,你会如何优化这个问题?

如果该函数处理事件的时间非常长,会导致该函数在很长时间内一直占用处理器资源,这会导

致系统性能的下降。为了优化这个问题,我会尝试以下的解决方案:

1. 将该函数中的耗时操作放在独立的线程中进行,在主线程中只是启动和控制该线程的运行。这样可以避免主线程被阻塞,提高系统的性能。

2. 将该函数中的操作分解成多个步骤,每个步骤在处理完后调用一次系统调度函数,释放处理器资源,让其他线程有机会运行。这样可以让系统的资源得到更好的分配和利用,提高系统的处理能力。

3. 将该函数中的操作分解成多个小的操作,尽量减少每个操作的执行时间,将每个操作放到自己的处理流程中。这样可以提高系统的处理能力,并且可以及时响应用户的操作,增强系统的交互性

21.远程桌面控制软件中,为了兼容网络不稳定的环境,你会如何处理数据丢失和重传问题?

为了兼容网络不稳定的环境,需要对数据丢失和重传问题进行处理。在远程桌面控制软件中,

我会尝试以下的解决方案:

1. 对数据包进行编号和确认机制,实现数据传输的可靠性和正确性。在发送方发送数据包时,将每

个数据包赋予一个独立的编号,接收方在接收数据包后向发送方发送一个确认包,表示该数据包已

被正确接收。如果发送方接收不到确认包,则说明该数据包丢失,需要重传。通过这种方式可以实

现数据传输的可靠性和正确性。

2. 设置超时机制,当发送方在一定时间内没有收到接收方的确认包时,就会重新发送该数据包。在

接收方接收到重复的数据包时,只处理最新的数据包,避免重复数据的处理。

3. 对于大容量的数据传输,可以采用分块传输的方法,每次只传输一个小块,当接收方确认了该小块数据时,再传输下一块数据。这样可以避免因为一个数据块传输失败而导致整个数据传输失败。

22.远控的被控端的流程是什么?

首先需要创建被控服务器对象,初始化线程池,套接字;之后调用StartService函数,主要作用是创建套接字,并且设置为非阻塞(在默认情况下,套接字是阻塞的。这意味着当你执行如recv()send()等操作时,如果没有数据可读或无法发送数据,程序会被挂起,直到操作完成。而在非阻塞模式下,这些操作会立即返回),之后套接字开始监听,创建IOCP对象,并且将IOCP对象与套接字绑定,之后再线程池中分配任务:GetQueuedCompletionStatus来等待事件的投递,之后NewAccept来等待客户端的连接,使用异步的AcceptEx来向IOCP申请该事件,在接收到事件之后,GetQueuedCompletionStatus就会有所响应,根据CONTAINING_RECORD这个宏函数来获取重叠结构的信息:

EdoyunOverlapped* pOverlapped = CONTAINING_RECORD(lpOverlapped, EdoyunOverlapped, m_overlapped);

根据pOverlapped的操作符来进行对应的操作,比如连接事件的处理,就会:

ACCEPTOVERLAPPED* pOver = (ACCEPTOVERLAPPED*)pOverlapped;
m_pool.DispatchWorker(pOver->m_worker);

给线程池中分配这个任务,这个任务的具体实现方式是:

template<EdoyunOperator op>
int AcceptOverlapped<op>::AcceptWorker() {
    INT lLength = 0, rLength = 0;
    if (m_client->GetBufferSize() > 0) {
        sockaddr* plocal = NULL, *promote = NULL;
        GetAcceptExSockaddrs(*m_client, 0,
            sizeof(sockaddr_in) + 16, sizeof(sockaddr_in) + 16,
            (sockaddr**)&plocal, &lLength, //本地地址
            (sockaddr**)&promote, &rLength//远程地址
        );
        memcpy(m_client->GetLocalAddr(), plocal, sizeof(sockaddr_in));
        memcpy(m_client->GetRemoteAddr(),promote, sizeof(sockaddr_in));
        m_server->BindNewSocket(*m_client);
        int ret = WSARecv((SOCKET)*m_client,m_client->RecvWSABuffer(), 1, *m_client, &m_client->flags(), m_client->RecvOverlapped(), NULL);
        if (ret == SOCKET_ERROR && (WSAGetLastError() != WSA_IO_PENDING)) {
            //TODO:报错
            TRACE("ret = %d error = %d\r\n", ret, WSAGetLastError());
        }
        if (!m_server->NewAccept())
        {
            return -2;
        }
    }
    return -1;
}

具体的处理方式是:获取本地IP和远程IP地址,之后给客户端赋值本地IP和远程IP,那么将将该客户端绑定到服务器的连接上(绑定到IOCP端口映射上),之后再调用WSARecv这个异步接收客户端信息的函数,向IOCP帮忙监听这个事件,之后继续监听建立新的客户端连接。

23.远控的客户端的流程是什么?

客户端启动的执行流程:

1.CRemoteClientApp theApp;执行构造函数(在Main函数之前)

2.extern “C” void __cdecl _initterm(_PVFV* const first, _PVFV* const last)
{
   for (_PVFV* it = first; it != last; ++it)
  {
       if (*it == nullptr)
           continue;

      (**it)();
  }
}
该函数是把所有的全局变量和静态成员变量通通初始化

3.进入到main函数中去

extern "C" int WINAPI
_tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
_In_ LPTSTR lpCmdLine, int nCmdShow)
#pragma warning(suppress: 4985)
{
// call shared/exported WinMain
return AfxWinMain(hInstance, hPrevInstance, lpCmdLine, nCmdShow);
}

_tWinMain是系统的API,表示是程序的入口

4.main函数

int AFXAPI AfxWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
_In_ LPTSTR lpCmdLine, int nCmdShow)
{
ASSERT(hPrevInstance == NULL);

int nReturnCode = -1;
CWinThread* pThread = AfxGetThread();// pThread指向了theApp
CWinApp* pApp = AfxGetApp(); // 获取theApp

// AFX internal initialization
if (!AfxWinInit(hInstance, hPrevInstance, lpCmdLine, nCmdShow))
goto InitFailure;

// App global initializations (rare)
if (pApp != NULL && !pApp->InitApplication())
goto InitFailure;

// Perform specific initializations   // 这里才是项目代码的功能真正开始执行的地方
if (!pThread->InitInstance())
{
if (pThread->m_pMainWnd != NULL)
{
TRACE(traceAppMsg, 0, "Warning: Destroying non-NULL m_pMainWnd\n");
pThread->m_pMainWnd->DestroyWindow();
}
nReturnCode = pThread->ExitInstance();
goto InitFailure;
}
nReturnCode = pThread->Run();

InitFailure:
#ifdef _DEBUG
// Check for missing AfxLockTempMap calls
if (AfxGetModuleThreadState()->m_nTempMapLock != 0)
{
TRACE(traceAppMsg, 0, "Warning: Temp map lock count non-zero (%ld).\n",
AfxGetModuleThreadState()->m_nTempMapLock);
}
AfxLockTempMaps();
AfxUnlockTempMaps(-1);
#endif

AfxWinTerm();
return nReturnCode;
}

5.BOOL CRemoteClientApp::InitInstance()
{
// 如果一个运行在 Windows XP 上的应用程序清单指定要
// 使用 ComCtl32.dll 版本 6 或更高版本来启用可视化方式,
//则需要 InitCommonControlsEx()。 否则,将无法创建窗口。
INITCOMMONCONTROLSEX InitCtrls;
InitCtrls.dwSize = sizeof(InitCtrls);
// 将它设置为包括所有要在应用程序中使用的
// 公共控件类。
InitCtrls.dwICC = ICC_WIN95_CLASSES;
InitCommonControlsEx(&InitCtrls);

CWinApp::InitInstance();

if (!AfxSocketInit())
{
AfxMessageBox(IDP_SOCKETS_INIT_FAILED);
return FALSE;
}


AfxEnableControlContainer();

// 创建 shell 管理器,以防对话框包含
// 任何 shell 树视图控件或 shell 列表视图控件。
CShellManager *pShellManager = new CShellManager;

// 激活“Windows Native”视觉管理器,以便在 MFC 控件中启用主题
CMFCVisualManager::SetDefaultManager(RUNTIME_CLASS(CMFCVisualManagerWindows));

// 标准初始化
// 如果未使用这些功能并希望减小
// 最终可执行文件的大小,则应移除下列
// 不需要的特定初始化例程
// 更改用于存储设置的注册表项
// TODO: 应适当修改该字符串,
// 例如修改为公司或组织名
SetRegistryKey(_T(“应用程序向导生成的本地应用程序”));

CRemoteClientDlg dlg;
m_pMainWnd = &dlg;
INT_PTR nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
// TODO: 在此放置处理何时用
// “确定”来关闭对话框的代码
}
else if (nResponse == IDCANCEL)
{
// TODO: 在此放置处理何时用
// “取消”来关闭对话框的代码
}
else if (nResponse == -1)
{
TRACE(traceAppMsg, 0, “警告: 对话框创建失败,应用程序将意外终止。\n”);
TRACE(traceAppMsg, 0, “警告: 如果您在对话框上使用 MFC 控件,则无法 #define _AFX_NO_MFC_CONTROLS_IN_DIALOGS。\n”);
}

// 删除上面创建的 shell 管理器。
if (pShellManager != nullptr)
{
delete pShellManager;
}

#if !defined(_AFXDLL) && !defined(_AFX_NO_MFC_CONTROLS_IN_DIALOGS)
ControlBarCleanUp();
#endif

// 由于对话框已关闭,所以将返回 FALSE 以便退出应用程序,
// 而不是启动应用程序的消息泵。
return FALSE;
}

实现是1.零散的 2.复杂的 3.繁琐的->无法一眼看透

有页面的项目采用MVC设计

MVC-> V : View,视图层。按钮,图片

C :Controller,控制层。

M:Model,模型层(数据层,所有的数据都来自数据层)。

当view与model直接联系时产生的不良影响:

当两个对话框进行通信时,一个对话框需要对另一个不同IP和端口号的窗口进行创建和唤醒,但是由于IP和端口号和窗口的高度耦合,我们必须通过手动创建窗口才能知道对话框的IP和端口,但是我们的初衷是通过IP和端口来告诉应用程序来创建,而不是创建后才能知道IP和端口,所以当视图层和模型层高度耦合会导致无法创建对话框的问题

所以我们更应该让页面只用于展示和输入需要传输的数据,而不是让窗口具备底层数据。所以我们可以通过控制层来对模型层进行一个抽象,只对视图层提供一些接口,当视图层需要连接的时候,只需要告诉控制层,让控制层来操控模型层来连接。同理,让控制层来传输从视图层来的数据。另外当模型层接收到数据的时候,也可以通知控制层,让控制层来通知视图层显示接收的数据

View层的生命周期是小于Model层的,但是View层是可显示的,可交互的,可复用的

界面应该专注于显示和输入

MVC架构并不是要增强控制层的可移植性,恰恰相反的是控制层往往是不可以移植的,控制层有大量的业务逻辑在里面

页面的数据并不会因为关闭而消失,而是会保存在Controller层,这样界面的数据就会持久化(Controller层是整个程序的生命周期都存在的)

View层只需要显示数据和告诉控制层需要什么就行了(就是向Controller层发送命令就好,让Controller层处理好数据后再通知View层来显示或者说反馈)

客户端执行流程:

初始化客户端程序主窗口:CRemoteClientDlg,等待用户的点击。

获取驱动信息:将从服务端得到的字符串:C,D,首先删除所有项,之后插入:HTREEITEM hTmp = tree.InsertItem(dr.c_str(), TVI_ROOT, TVI_LAST);
tree.InsertItem(“”, hTmp, TVI_LAST);
dr.clear();

将线程机制更改为消息机制的原因:由于网络模块是单例模式,所以无论是监视对话框,主程序对话框,文件的下载都需要与网络模块建立一个事件的触发机制,但是事件机制最好是一对一的,也就是说网络模块和一个东西有事件机制就好了。因为事件机制的初衷是异步的,但是当多个东西都与网络模块产生联系之后,异步也就是会变成同步了

关于主程序窗口更改核心

功能例子

void CRemoteClientDlg::LoadFileInfo()
{
CPoint ptMouse;
GetCursorPos(&ptMouse);
m_Tree.ScreenToClient(&ptMouse);
// 获取当前目录的展开路径,就需要遍历这个文件树
HTREEITEM hTree = m_Tree.HitTest(ptMouse, 0);
if (hTree == NULL)
{
return;
}
// 避免不停地增长
DeleteTreeChildrenItem(hTree);
m_list.DeleteAllItems();
CString Path = GetPath(hTree);
TRACE("hTree %08X\r\n",hTree);
   
int Cmd = CClientController::getInstance()->SendCommandPacket(GetSafeHwnd(),
2, false, (BYTE*)(LPCTSTR)Path,
Path.GetLength(),(WPARAM)hTree);
}

由主程序窗口向控制层发送请求 :请求发送数据包

bool CClientController::SendCommandPacket(HWND hWnd,int nCmd, bool bAutoClose, BYTE* pData, size_t nLength, WPARAM wParam)
{
TRACE("cmd : %d   %s : start %lld \r\n",nCmd,__FUNCTION__,GetTickCount64());
CClientSocket* pClient = CClientSocket::getInstance();
bool ret =  pClient->SendPacket(hWnd, CPacket(nCmd, pData, nLength), bAutoClose, wParam);
return ret;
}

由控制层控制网络层来发送数据包

bool CClientSocket::SendPacket(HWND hWnd,const CPacket& pack, bool isAutoClosed, WPARAM wParam)
{

UINT nMode = isAutoClosed ? CSM_AUTOCLOSE : 0;
std::string strOut;
pack.Data(strOut);
PACKET_DATA* pData = (new PACKET_DATA(strOut.c_str(), strOut.size(), nMode, wParam));;
bool ret =  PostThreadMessage(m_nThreadID, WM_SEND_PACKET,
(WPARAM)pData, (LPARAM)hWnd);
if (ret == false)
{
delete pData;
}
return ret;
}

调用PostThreadMessage来发送给消息处理线程

void CClientSocket::threadFunc2()
{
SetEvent(m_eventInvoke);// 发送事件
MSG msg;
while (::GetMessage(&msg, NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
TRACE("Get Messgae : %08X \r\n",msg.message);
if (m_mapFunc.find(msg.message) != m_mapFunc.end())
{
(this->*m_mapFunc[msg.message])(msg.message, msg.wParam, msg.lParam);

}
}
}

消息处理线程启动后,由该线程来来处理信息

    struct {
UINT message;
MSGFUNC func;
}funcs[] = {
{WM_SEND_PACKET,&CClientSocket::SendPack},
{0,NULL}
};
for (int i = 0; funcs[i].message != 0; ++i)
{
if (m_mapFunc.insert(std::pair<UINT, MSGFUNC>(funcs[i].message, funcs[i].func)).second == false)
{
TRACE("插入失败\r\");
}

}

对于WM_SEND_PACKET消息的处理核心


void CClientSocket::SendPack(UINT nMsg, WPARAM wParam, LPARAM lParam)
{
PACKET_DATA data = *(PACKET_DATA*)wParam;// 防止内存泄漏
delete (PACKET_DATA*)wParam;
HWND hWnd = (HWND)lParam;
size_t nTemp = data.strData.size();
CPacket current((BYTE*)data.strData.c_str(),nTemp);
if (InitSocket() == true)
{
int ret = send(m_client_sock, (char*)data.strData.c_str(), (int)data.strData.size(), 0);
if (ret > 0)
{
size_t index = 0;
std::string strBuffer;
strBuffer.resize(BUFFER_SIZE);
char* pBuffer = (char*)strBuffer.c_str();
while (m_client_sock != INVALID_SOCKET)
{
int len = recv(m_client_sock,pBuffer+index ,BUFFER_SIZE-index , 0);
if (len > 0 || (index > 0))
{
index += (size_t)len;
size_t nLen = index;
CPacket pack((BYTE*)pBuffer,nLen);
if (nLen > 0)// 解包成功
{
TRACE("ack pack %d to hWnd %08X\r\n",pack.sCmd,hWnd);
TRACE("%04X\r\n",*(WORD*)(pBuffer+nLen));
::SendMessage(hWnd, WM_SEND_PACKET_ACK, (WPARAM)new CPacket(pack),data.wParam);
if (data.nMode & CSM_AUTOCLOSE)
{
CloseSocket();
return;
}
index -= nLen;
memmove(pBuffer, pBuffer + nLen, index);
}
}
else {// 对方关闭套接字,或者网络设备异常
TRACE("recv failed , len : %d index : %d\r\n",len,index);
CloseSocket();
::SendMessage(hWnd, WM_SEND_PACKET_ACK, (WPARAM)new CPacket(current.sCmd,NULL,0), 1);
}
}

}
else {
// 网络终止处理
CloseSocket();
::SendMessage(hWnd, WM_SEND_PACKET_ACK, NULL, -1);
}
}
else {
// 错误处理
::SendMessage(hWnd, WM_SEND_PACKET_ACK, NULL, -2);
}

}

对于接收消息:WM_SEND_PACKET_ACK的处理

主窗口的处理

LRESULT CRemoteClientDlg::OnSendPackAck(WPARAM wParam, LPARAM lParam)
{
if (lParam == -1 || lParam == -2)
{
// 错误处理
TRACE("Socket is wrong\r\n");
}
else if (lParam == 1) {
//对方关闭了套接字
TRACE("Socket is closed\r\n");
}
else
{
if (wParam != NULL)
{
CPacket head = *(CPacket*)wParam;
delete (CPacket*)wParam;
DealCommand(head.sCmd,head.strData, lParam);
}
}

return 0;
}

void CRemoteClientDlg::DealCommand(WORD nCmd , const std::string& strData, LPARAM lParam)
{
switch (nCmd)
{
case 1:// 获取驱动信息
Str2Tree(strData, m_Tree);
break;
case 2:
UpdateFileInfo(*(PFILEINFO)strData.c_str(), (HTREEITEM)lParam);
break;
case 3:
MessageBox("运行文件完成!", "操作完成!", MB_ICONINFORMATION);
break;
case 4:
UpdateDownloadFile(strData, (FILE*)lParam);
break;
case 1981:
MessageBox("连接测试成功!", "连接成功!",MB_ICONINFORMATION);
break;
case 9:
MessageBox("删除文件完成!", "操作完成!", MB_ICONINFORMATION);
break;
default:
TRACE("unknown data received ! %d\r\n", nCmd);
break;
}
}

24.对于远控的客户端,你的控件是怎么使用和设计的?

对于客户端的主程序窗口,我使用了IP控件,一个Edit Control编辑窗口用于输入端口,三个按钮:测试连接,显示文件驱动信息,远程监控;一个Tree-View用于显示文件夹,一个List-Control用于显示文件列表

还有一个用来显示状态的状态窗口,一个用于文件下载的菜单,一个等比缩放的远程监控窗口,远程监控窗口上有一个Picture Controller来显示远程发送过来的图片

对于IP控件和端口控件,对于IP控件和端口的更新,需要先将空间的值赋值给变量,之后修改后再赋值给控件

对于三个按钮,主要是点击事件后触发事件处理函数

对于文件驱动信息的显示:根据文本的值插入对应的项

对于文件列表的显示:根据文件列表的值插入对应的项

对于远程监控窗口,需要显示图片,图片的处理是需要将一个字节流缓冲区的数据转变成一个图片对象,由于图片对象需要从内存流中导入,所以需要分配一个全局内存块,并且获取该全局内存块的内存流,将字节流缓冲区的内容导入全局内存块中,然后根据内存流导入到图片对象中。

由于对这个Picture Control这个控件绑定了变量,所以只要根据大小等比例显示就行,最后将缓冲区对应的变量设置为空