补充

之前讲SOCKET的时候,有一点忘了讲了。那就是端口号和IP的问题。
由于客户端连接服务端的时候需要知道服务端的IP地址,所以在这里我讲一下相关信息。

端口,IP

什么是端口?
我这里说的端口,是逻辑意义上的端口,即 TCP/IP 协议上的端口,一般范围在 0 —— 65535 。那么端口拿来干嘛呢,我们来看看百度的定义:“软件领域的端口一般指网络中面向连接服务和无连接服务的通信协议端口,是一种抽象的软件结构,包括一些数据结构和I/O(基本输入输出)缓冲区。” 那这里我就指出一点,缓冲区,所以按照我的理解,先把电脑整个缓冲区分成块,那么端口就是这一块又一块的缓冲区或者说是它的编号。
我为什么要讲端口,我们来看看端口分类。
端口按端口号可分为3大类:
(1)公认端口(Well Known Ports):从0到1023,它们紧密绑定(binding)于一些服务。通常这些端口的通讯明确表明了某种服务的协议。例如:80端口实际上总是HTTP通讯。
(2)注册端口(Registered Ports):从1024到49151。它们松散地绑定于一些服务。也就是说有许多服务绑定于这些端口,这些端口同样用于许多其它目的。例如:许多系统处理动态端口从1024左右开始。
(3)动态和/或私有端口(Dynamic and/or Private Ports):从49152到65535。理论上,不应为服务分配这些端口。实际上,机器通常从1024起分配动态端口。但也有例外:SUN的RPC端口从32768开始。

看到这里,你可以打开任务管理器看看进程,里面的 PID 其实也就是你的各个应用程序的端口号了。

监控端口获取数据 监控端口是什么意思_远程桌面


你可以看到,这些端口都是随机分配的,注册端口区域你肯定是不能用的,而部分系统端口也会分布在公认端口区,所以一般来说,我们这种自己写的网络程序,绑定的端口最好在2000+,这样才能尽量不与其他程序矛盾。(也就是缓冲区矛盾,当然这个缓冲区这个概念,在后面解决问题的时候会有用处。)

我的程序框架

然后就是IP地址,由于在建立SOCKET连接的时候需要服务器的 IP 地址,所以我的客户端界面就拉了个小插件(MFC的)IP Address Control。然后这就是我的客户端大致样貌。

监控端口获取数据 监控端口是什么意思_SOCKET_02


另外,由于服务端需要实现一些监控功能,所以,用户列表和进程列表我用了 List Control, 系统操作用了 下拉列表 , 另外又外加了几个按钮。

监控端口获取数据 监控端口是什么意思_SOCKET_03

所以然后就是一些基本的初始化,列表初始化,按键的函数初始化,这里我就不一 一诉说了。

系统实现

那现在就来讲,做这个系统, 我的具体步骤。

第一步:如何传输指令。

我没有想那么多,先做哪个按钮什么的。我首先应该考虑的是如何实现指令传输。我开始只会传一个,收一个,并且两边数据结构要一致。所以呢,在这里我用了我一个处理字符串的方法。
传输指令的时候就传输一个字符串,不同指令的区别就在于传输字符串的第一个数据不同。
这里贴出其中一个按钮代码:(这是系统操作按钮)

void CDMMserverDlg::OnBnClickedDoButton()
{
	int nSel;
	// 获取组合框控件的列表框中选中项的索引
	nSel = m_list.GetCurSel();
	char str = (char)(nSel);
	// 发送操作信息
	int res = 0;
	char recvBuf[100] = { 0 };
	// 第一个字符初始化为 1
	char s[100] = { 1 };
	recvBuf[0] = str;
	strcat(s, recvBuf);
	res = send(socketname, (char*)&s, sizeof(s), 0);
	if (res >= 0)
		MessageBox(L"执行成功");
	else
		MessageBox(L"执行失败");
	
	
	//这里需要一个等待框,之后才是成功框 等待操作
	
	// TODO: 在此添加控件通知处理程序代码
}

那么在接收的时候,就先识别第一个字符,然后分支处理语句。
然后这里我就贴出我的这个客户端处理代码:

DWORD WINAPI Fun1(LPVOID lpParamter)
{
	
	while (TRUE)
	{
		CDMMClientDlg*pdlg = (CDMMClientDlg *)lpParamter;
		char recvBuf[100] = { 0 };
		int res = recv(sock_connent2, recvBuf, 100, 0);
		

		if (res < 0)
		{	
			::AfxMessageBox(L"服务器断开");
			return -1;
		}
		if (recvBuf[0] == 1)
		...........省略

那么传输指令的问题解决了。也就可以开始具体实现了。

第二步:功能实现
1.用户列表的创建

最开始肯定就是创建用户列表啦。那首先说我的用户列表的内容,我开始的时候没有做那么多内容,也就显示,用户分配的SOCKET,IP地址,连接状态。那么列表初始化代码就不贴了,详情看我最后一章笔记。
如何在打开服务器之后一直接收后面连接的用户,这里的实现代码如下:

HANDLE hThread2 = CreateThread(NULL, 0, reciveClient, this, 0, NULL);
// 这里的 this 传的是主对话框指针
DWORD WINAPI reciveClient(LPVOID lpParamter)
{
	CDMMserverDlg*pdlg = (CDMMserverDlg *)lpParamter;


	while (true)
	{
		sockaddr_in addr_client = { 0 };
		SOCKET *ClientSocket = new SOCKET;
		sock_IMG = (SOCKET*)malloc(sizeof(SOCKET));
		// 接收客户端连接请求
		int SockAddrlen = sizeof(sockaddr);
		int SockClientlen = sizeof(sockaddr);
		*sock_IMG = accept(sock_IMG0, 0, 0);
		if (*sock_IMG > 0)
		{
			pdlg->m_IMG.InsertItem(col,(LPCTSTR)sock_IMG);
		}

		 addr_client = { 0 };
		 ClientSocket = new SOCKET;
		ClientSocket = (SOCKET*)malloc(sizeof(SOCKET));
		// 接收客户端连接请求
		 SockAddrlen = sizeof(sockaddr);
		 SockClientlen = sizeof(sockaddr);
		*ClientSocket = accept(sock_server, 0, 0);
		if (*ClientSocket > 0)
		{
			
			
			
			pdlg->MessageBox(L"有一位用户连接成功");
			int id = *ClientSocket;
			CString idd;
			 
			idd.Format(_T("%d"), id);
			getpeername(*ClientSocket, (sockaddr *)&addr_client, &SockClientlen);
			char * IP = ::inet_ntoa(addr_client.sin_addr);
			CString str = CString(IP);
			USES_CONVERSION;
			LPCWSTR wszClassName = A2CW(W2A(str));
			char buffff[100] = { 0 };
			char s[100] = { 0 };
			s[0] = { 9 };
			
			char a[100];
			itoa(id, a, 10);
			strcat(s, a);
			//第一个是待转换数字,第二个参数为存放字符串,第三个参数为进制
			int res = send(*ClientSocket, s, 100, 0);
			if (res < 0)
			{
				pdlg->MessageBox(L"发送数据失败");
				return -1;
			}
			str.ReleaseBuffer();
			//m_clientlist.InsertItem(0, _T("Java"));
			pdlg->m_clientlist.InsertItem(col,(LPCTSTR)ClientSocket);
			pdlg->m_clientlist.SetItemText(col, 1, wszClassName);
			pdlg->m_clientlist.SetItemText(col,2,idd);
			pdlg->m_clientlist.SetItemText(col, 3, _T("连接中"));
			//CreateThread(NULL, 0, &ServerThread, ClientSocket, 0, NULL);
			col++;
		}
	}
}

然后就肯定要有一个选定用户操作的步骤,如何选定? 那肯定是对列表进行操作了。
这里贴出来 NMCLICK 代码:

void CDMMserverDlg::OnNMClickList1(NMHDR *pNMHDR, LRESULT *pResult)
{
	LPNMITEMACTIVATE pNMItemActivate = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
	*pResult = 0;

	SOCKET strLangName;    // 选择语言的名称字符串   
	NMLISTVIEW *pNMListView = (NMLISTVIEW*)pNMHDR;

	if (-1 != pNMListView->iItem)        // 如果iItem不是-1,就说明有列表项被选择   
	{
		// 获取被选择列表项第一个子项的文本   
		sock_IMGname = (SOCKET)*m_IMG.GetItemText(pNMListView->iItem, 0);
		//接收图片
		strLangName = (SOCKET)*m_clientlist.GetItemText(pNMListView->iItem, 0);
		//获取选择用户的SOCKET
		socketname =strLangName;
		// 将选择的显示与编辑框中   
		//SetDlgItemText(IDC_LANG_SEL_EDIT, strLangName);
	}	
	// TODO: 在此添加控件通知处理程序代码
	*pResult = 0;
}

注:这个选定就会重新初始化这个全局变量(或者说是成员变量)sock_IMGname,strLangName ,socketname .

2.系统操作

系统操作,这个需要在客户端上进行,首先需要写一个提升系统权限的函数:

//使能关机的特权函数
BOOL EnableShutdownPrivilege()
{
	HANDLE hProcess = NULL;
	HANDLE hToken = NULL;
	LUID uID = { 0 };
	TOKEN_PRIVILEGES stToken_Privileges = { 0 };

	hProcess = ::GetCurrentProcess();  //获取当前应用程序进程句柄

	if (!::OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES, &hToken))  //打开当前进程的访问令牌句柄(OpenProcessToken函数调用失败返回值为零)
		return FALSE;

	if (!::LookupPrivilegeValue(NULL, SE_SHUTDOWN_NAME, &uID))  //获取权限名称为"SeShutdownPrivilege"的LUID(LookupPrivilegeValue函数调用失败返回值为零)
		return FALSE;

	stToken_Privileges.PrivilegeCount = 1;  //欲调整的权限个数
	stToken_Privileges.Privileges[0].Luid = uID;  //权限的LUID
	stToken_Privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;  //权限的属性,SE_PRIVILEGE_ENABLED为使能该权限

	if (!::AdjustTokenPrivileges(hToken, FALSE, &stToken_Privileges, sizeof stToken_Privileges, NULL, NULL))  //调整访问令牌里的指定权限(AdjustTokenPrivileges函数调用失败返回值为零)
		return FALSE;

	if (::GetLastError() != ERROR_SUCCESS)  //查看权限是否调整成功
		return FALSE;

	::CloseHandle(hToken);
	return TRUE;
}

然后,就是一个个的功能实现了!
贴!:

//关机函数
BOOL Shutdown(BOOL bForce)
{
	EnableShutdownPrivilege();  //使能关机特权函数
	if (bForce)
		return ::ExitWindowsEx(EWX_SHUTDOWN | EWX_FORCE, 0);  //强制关机
	else
		return ::ExitWindowsEx(EWX_SHUTDOWN, 0);
}

//注销函数
BOOL Logoff(BOOL bForce)
{
	if (bForce)
		return ::ExitWindowsEx(EWX_LOGOFF | EWX_FORCE, 0);  //强制注销
	else
		return ::ExitWindowsEx(EWX_LOGOFF, 0);
}

//重启函数
BOOL Reboot(BOOL bForce)
{
	EnableShutdownPrivilege();  //使能关机特权函数
	if (bForce)
		return ::ExitWindowsEx(EWX_REBOOT | EWX_FORCE, 0);  //强制重启
	else
		return ::ExitWindowsEx(EWX_REBOOT, 0);
}
//睡眠函数
BOOL SLEEPASK(BOOL bForce)
{


	HANDLE hToken;
	TOKEN_PRIVILEGES tp;
	LUID luid;
	if (::OpenProcessToken(GetCurrentProcess(),
		TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
		&hToken))
	{
		::LookupPrivilegeValue(NULL, SE_SHUTDOWN_NAME, &luid);
		tp.PrivilegeCount = 1;
		tp.Privileges[0].Luid = luid;
		tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
		::AdjustTokenPrivileges(hToken, false, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL);
	}
	::SetSystemPowerState(bForce, true);//第一个参数值为真则待机,参数值为假则休眠。

	return TRUE;

}

最后就是具体的如何处理消息然后再执行函数:

if (recvBuf[0] == 1)
			
			{
			int l = 0;


				//关机,重启,注销等操作
				
				switch (recvBuf[1])
				{
				case    (char)0:
					Logoff(FALSE);  //注销
					break;      //执行完成后跳出
				case    (char)1:
					Reboot(FALSE);  //重启
					break;
				case    (char)2:
					Shutdown(FALSE);  //关机
					break;
				case    (char)3:
					Logoff(TRUE);  //强制注销
					break;
				case    (char)4:
					Reboot(TRUE);  //强制重启
					break;
				case    (char)5:
					Shutdown(TRUE);  //强制关机
					break;
				case	(char)6:
				SLEEPASK(TRUE); //待机
				break; 
				case	(char)7:
					SLEEPASK(FALSE); //休眠
					break;
				default:
					break;     //如果i 不符合以上条件直接跳出不执行任何东西
				}
			}
3.进程查杀

进程查杀消息发送与接收:
客户端:
首先是获取系统所有PID,我这里其实有点多此一举,我在弄的时候把PID传给了服务端,然后服务端获取选中的进程PID又发给了客户端,然后客户端才开始进行进程终止。

那第一步!:客户端获取系统进程PID;

//获取当前系统的所有进程PID
vector<ProcessInfo> GetProcessInfo()
{
	STARTUPINFO st;
	PROCESS_INFORMATION pi;
	PROCESSENTRY32 ps;
	HANDLE hSnapshot;
	vector<ProcessInfo> PInfo;

	ZeroMemory(&st, sizeof(STARTUPINFO));
	ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
	st.cb = sizeof(STARTUPINFO);
	ZeroMemory(&ps, sizeof(PROCESSENTRY32));
	ps.dwSize = sizeof(PROCESSENTRY32);

	//拍摄进程快照
	hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

	if (hSnapshot == INVALID_HANDLE_VALUE)
	{
		//快照拍摄失败
		return PInfo;
	}

	if (!Process32First(hSnapshot, &ps))
	{
		return PInfo;
	}

	//将进程PID、进程名称存到容器Vector中
	do
	{
		PInfo.emplace_back(ps.th32ProcessID, WCHAR2String(ps.szExeFile));

	} while (Process32Next(hSnapshot, &ps));

	//关闭快照句柄
	CloseHandle(hSnapshot);

	//排序
	sort(PInfo.begin(), PInfo.end());

	return PInfo;
}

然后客户端发给服务端PID + name:

else if (recvBuf[0] == 4)
		{
				int i=getprocess(pdlg);
		}
		// 获取进程
BOOL getprocess(CDMMClientDlg*pdlg)
{
	vector<ProcessInfo> PInfo = GetProcessInfo();
	//如果size大于0
	if (PInfo.size())
	{
		
		for (vector<DWORD>::size_type iter = 0	; iter < PInfo.size(); iter++)
		{
			int PID;
			PID = PInfo[iter].PID;
			//char pid = (char)(PID);
			int res = send(sock_connent2, (char *)&PID, sizeof(PID), 0);
			if (res < 0)
			{
				pdlg->MessageBox(L"send pro error");
				return -1;
			}
			char recvBuf2[100] = { 0 };
			res = recv(sock_connent2, recvBuf2, 100, 0);
			if (res < 0)
				return -1;
			//PInfo[iter].PID  PInfo[iter].PName.c_str() 
			USES_CONVERSION;
			char str[100] = { 0 };
			//函数T2A和W2A均支持ATL和MFC中的字符
			char* s = T2A(PInfo[iter].PName);
			strcat(str, s);
			 res = send(sock_connent2, str, 100, 0);
			if (res < 0)
			{
				pdlg->MessageBox(L"send pro error");
				 return -1;
			}
			
			char recvBuf1[100] = { 0 };
			 res = recv(sock_connent2, recvBuf1, 100, 0);
			if (res < 0)
				return -1;
			
			if (iter >= (PInfo.size()-1))
			{
				int PID0;
				PID0 = 99999;
				//char pid = (char)(PID);
				int res = send(sock_connent2, (char *)&PID0, sizeof(PID0), 0);
				if (res < 0)
				{
					pdlg->MessageBox(L"send pro error");
					return -1;
				}
				return -1;
			}

		}
	}
	else
	{0
		pdlg->MessageBox(L"未找到进程");
	}

服务端:
那个发送指令的代码我就省略了,就简单贴个列表信息的初始化:

//获取进程
DWORD WINAPI recvpro(LPVOID lpParamter)
{
	int col = 0;
	
	CDMMserverDlg *pdlg = (CDMMserverDlg *)lpParamter;
	pdlg->m_processlist.DeleteAllItems();
	while (true)
	{
		int PID = 0;
		int res = recv(socketname, (char *)&PID, sizeof(PID), 0);
		if (res < 0)
		{
			pdlg->MessageBox(L"error");
			return -1;
		}
		if (PID == 99999)
			return -1;
		//CString idd;
		//idd.Format(_T("%lld"), PID);
		CString idd;
		idd.Format(_T("%d"), PID);
		
		pdlg->m_processlist.InsertItem(col,idd);
		char recvBuf2[100] = { 0 };
		res = send(socketname, recvBuf2, 100, 0);
		if (res < 0)
			return -1;

		char str[100] = { 0 };
		 res = recv(socketname, str, 100, 0);
		if (res < 0)
		{
			pdlg->MessageBox(L"error");
			
			return -1;
		}
		
		USES_CONVERSION;
		 CString s = A2T(str);
		pdlg->m_processlist.SetItemText(col, 1,s);
		char recvBuf1[100] = { 0 };
		res = send(socketname, recvBuf1, 100, 0);
		if (res < 0)
			return -1;
		
		col++;
		
	}
	
}

之后就是,首先服务端获取点击列表,
所以贴出获取列表信息 PID + name 的代码:

// 列表点击
void CDMMserverDlg::OnNMClickList2(NMHDR *pNMHDR, LRESULT *pResult)
{
	LPNMITEMACTIVATE pNMItemActivate = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
	*pResult = 0;
  // 选择语言的名称字符串   
	NMLISTVIEW *pNMListView = (NMLISTVIEW*)pNMHDR;
	CString PIDc;
	if (-1 != pNMListView->iItem)        // 如果iItem不是-1,就说明有列表项被选择   
	{
		// 获取被选择列表项第一个子项的文本   
		PIDc = m_processlist.GetItemText(pNMListView->iItem, 0);
		//PID 赋值
		PIDname = _ttoi(PIDc);
		// 将选择的语言显示与编辑框中   
		//SetDlgItemText(IDC_LANG_SEL_EDIT, strLangName);
		UpdateData(false);
	}
	
	*pResult = 0;

}

服务端发送指令:

// 进程查杀
void CDMMserverDlg::OnBnClickedButton1()
{
	char s0[100] = { 10 };
	int res = send(socketname, s0, sizeof(s0), 0);
	if (res > 0)
		MessageBox(L"执行成功");
	else MessageBox(L"执行失败");
	char s[100] = { 0 };
	char a[100];
	itoa(PIDname, a, 10);
	strcat(s, a);
	//第一个是待转换数字,第二个参数为存放字符串,第三个参数为进制
	res = send(socketname, s, 100, 0);
	if (res < 0)
		MessageBox(L"执行失败");
	// TODO: 在此添加控件通知处理程序代码
}

客户端接收指令:

else if (recvBuf[0] == 10)
		{
			//提权
			USES_CONVERSION;
			CString str = A2T(recvBuf);
			char buf[100] = { 0 };
			res = recv(sock_connent2, buf, sizeof(buf), 0);
			int  PIDname = atoi(buf);	
			AdjustPrivilege();
			//这里的进程的PID(每次都会变的)
			//打开进程返回一个打开的进程句柄
			HANDLE m_hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, PIDname);
			if (TerminateProcess(m_hProc, 0) < 0)
				//pdlg->MessageBox(L"执行成功");
			pdlg->MessageBox(L"执行失败");
		}
4.消息框

这个很简单。
服务端:

// 信息发送
void CDMMserverDlg::OnBnClickedSendmButton()
{
	// TODO: 在此添加控件通知处理程序代码
	CString str;
	this->m_editSend.GetWindowText(str);
	char buf[100] = {0};
	//SOCKET *ClientSocket = new SOCKET;
	//ClientSocket = (SOCKET*)malloc(sizeof(SOCKET));
	//*ClientSocket = socketname;
	// 接收客户端连接请求
	
	int num =WideCharToMultiByte(CP_ACP, 0, str, -1, NULL, 0, NULL, NULL);
	WideCharToMultiByte(CP_ACP, 0, str, -1, buf, num, NULL, NULL);
	char s[100] = { 2 };
	strcat(s, buf);
	
	int res = send(socketname,s,100, 0);
	str.ReleaseBuffer();
	if (res < 0)
	{
		this->MessageBox(L"发送数据失败");
		return;
	}
	this->MessageBox(L"发送数据成功");
	char ss[100] = { 0 };
	res = recv(socketname, ss, 100, 0);
	if (res > 0)
	{
		this->MessageBox(L"用户已看到您的消息");
	}
	// TODO: 在此添加控件通知处理程序代码
}

客户端:

else if (recvBuf[0] == 2)
			
			{


				USES_CONVERSION;
				CString str = A2T(recvBuf);
				if (pdlg->MessageBox(L"服务器发来消息"))
				{
					char recvBuf00[100] = { 1 };
					 int res0 = send(sock_connent2, recvBuf, 100, 0);
				}
				pdlg->m_staticShow.SetWindowTextW(str);

				//	str.ReleaseBuffer();
				//m_staticShow.SetWindowTextW(str);
			}
		else if (recvBuf[0] == 9)
		{
			USES_CONVERSION;
			CString str = A2T(recvBuf);
			pdlg->MessageBox(L"您的ID已存入框中");
			pdlg->m_listID.SetWindowTextW(str);
		}
5.重头戏:视频传输(见下一节 (滑稽.JPG))