Windows编程笔记

前言

  记录下自己学习Windows编程的知识点。做个笔记,方便自己查阅。

进程

什么是进程

  进程是程序的实体,是资源分配的最小单位。进程是一种空间上的概念,它的责任就是提供资源。每一个进程都有一个4GB大小的虚拟空间,范围从0x0-0xFFFFFFFF。其中内核占高2GB,低2GB给程序的堆栈使用

  在Windows中,系统通过句柄管理进程中的资源,句柄存储在内核空间中的一个全局句柄表中,而每个进程也都有一个句柄表,这个句柄表是私有的。

  PID 是指的是全局句柄表的值。

进程执行的加载过程

  1.映射EXE文件(低2G)
  2.创建内核对象EPROCESS(高2G)
  3.映射系统DLL(ntdll.dll)
  4.创建线程内核对象ETHREAD
  5.系统启动线程
    a.映射DLL(ntdll.LdrInitalizeThunk)
    b.线程开始执行

创建进程

  任何进程都是别的进程创建的,当用户双击exe时,实际上是Explorer.exe这个进程所创建的,使用了CreateProcess函数

BOOL CreateProcess(
    LPCWSTR lpApplicationName, 					// 文件名 完整文件路径
    LPWSTR lpCommandLine, 						// 命令行参数
    LPSECURITY_ATTRIBUTES lpProcessAttributes, 	// SD 进程句柄
    LPSECURITY_ATTRIBUTES lpThreadAttributes, 	// SD 线程句柄
    BOOL bInheritHandles, 						// 句柄是否可继承
    DWORD dwCreationFlags, 						// 创建进程方式
    LPVOID lpEnvironment, 						// 父进程环境变量
    LPCWSTR lpCurrentDirectory, 				// 子进程工作路径
    LPSTARTUPINFOW lpStartupInfo, 				// 启动进程相关信息
    LPPROCESS_INFORMATION lpProcessInformation	// 子进程的相关信息 (进程ID,线程ID,进程句柄,线程句柄)
);												// 返回值BOOL

  这里讲解CreateProcess()的前两个参数和后两个参数。第一个lpApplicationName启动进程的绝对路径,比如记事本的路径为:C:\Windows\System32\notepad.exe。第二个lpCommandLine对命令行进行传参。

  例如下面的例子生成test.exe

#include <stdio.h>

int main(int argc,char *argv[])
{
    printf("%s - %s",argv[0],argv[1]);
    return 0;
}

  传入hello得到

D:\>test.exe hello
test.exe - hello

  第9个lpStartupInfo,函数原型

typedef struct _STARTUPINFOW {
    DWORD   cb;
    LPWSTR  lpReserved;
    LPWSTR  lpDesktop;
    LPWSTR  lpTitle;
    DWORD   dwX;
    DWORD   dwY;
    DWORD   dwXSize;
    DWORD   dwYSize;
    DWORD   dwXCountChars;
    DWORD   dwYCountChars;
    DWORD   dwFillAttribute;
    DWORD   dwFlags;
    WORD    wShowWindow;
    WORD    cbReserved2;
    LPBYTE  lpReserved2;
    HANDLE  hStdInput;
    HANDLE  hStdOutput;
    HANDLE  hStdError;
} STARTUPINFOW, *LPSTARTUPINFOW;

   有非常多的成员,我们只需要关注第一个成员cb,意思是包含STARTUPINFO结构体的大小(需要的原因,方便windows未来更新换代)。cb必须初始化sizeof(STARTUPINFO)。其他属性都置0即可。

   第10个lpProcessInformation,函数原型

typedef struct _PROCESS_INFORMATION {
    HANDLE hProcess;	// 进程句柄
    HANDLE hThread;		// 线程句柄
    DWORD dwProcessId;	// 进程ID
    DWORD dwThreadId;	// 线程ID
} PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;

   创建进程例子

#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>

int main()
{
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
	
    // 填充置0
    ZeroMemory(&pi, sizeof(pi));
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
	
    // 打开cmd
    char processName[] = "C:\\Windows\\System32\\cmd.exe";
    // 打开cmd后 ping百度
    char cmdLine[] = "/c ping www.baidu.com -n 1";

    if (CreateProcess(processName, cmdLine, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
    {
        printf("CreateProcess Success\n");
    }
    else
    {
        printf("CreateProcess Error:%d\n", GetLastError());
    }
}

  运行结果

D:\>test.exe
CreateProcess Success

D:\>
正在 Ping www.a.shifen.com [14.215.177.39] 具有 32 字节的数据:
来自 14.215.177.39 的回复: 字节=32 时间=34ms TTL=54

14.215.177.39 的 Ping 统计信息:
    数据包: 已发送 = 1,已接收 = 1,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 34ms,最长 = 34ms,平均 = 34ms

  上面的GetLastError函数来获取问题编号,具体编号对应内容可以参考百度百科

结束进程

  使用TerminateProcess函数,传入一个句柄和退出状态码

BOOL WINAPI TerminateProcess(HANDLE hProcess,UINT uExitCode);

  使用TerminateProcess结束打开的记事本

#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>

int main()
{
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ZeroMemory(&pi, sizeof(pi));
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
	
    // 打开记事本
    char processName[] = "C:\\Windows\\System32\\notepad.exe";
    if (CreateProcess(processName, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
    {
    	// 等待按键
        system("pause");
        // 结束子进程
        TerminateProcess(pi.hProcess,0);
        printf("success exit childProcess\n");
    }
    else
    {
        printf("CreateProcess Error:%d\n", GetLastError());
    }
}

  使用OpenProcess函数,通过pid去操作进程对象

HANDLE WINAPI OpenProcess(
    DWORD dwDesiredAccess,	// 此进程访问权限
    BOOL bInheritHandle,	// 进程句柄是否可以被继承
    DWORD dwProcessId		// 进程ID
);							// 返回句柄对象

  dwDesiredAccess 有以下几种

PROCESS_ALL_ACCESS							获取所有权限
PROCESS_CREATE_PROCESS						创建进程
PROCESS_CREATE_THREAD						创建线程
PROCESS_DUP_HANDLE							使用DuplicateHandle()函数复制一个新句柄
PROCESS_QUERY_INFORMATION					获取进程的令牌、退出码和优先级等信息
PROCESS_QUERY_LIMITED_INFORMATION			获取进程特定的某个信息
PROCESS_SET_INFORMATION						设置进程的某种信息
PROCESS_SET_QUOTA							使用SetProcessWorkingSetSize函数设置内存限制
PROCESS_SUSPEND_RESUME						暂停或者恢复一个进程
PROCESS_TERMINATE							使用Terminate函数终止进程
PROCESS_VM_OPERATION						在进程的地址空间执行操作
PROCESS_VM_READ								使用ReadProcessMemory函数在进程中读取内存
PROCESS_VM_WRITE							使用WriteProcessMemory函数在进程中写入内存
SYNCHRONIZE									使用wait函数等待进程终止

   使用以下代码查看记事本的pid,也可以使用任务管理器或者是tasklist等等工具

#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>

int main()
{
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ZeroMemory(&pi, sizeof(pi));
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);

    char processName[] = "C:\\Windows\\System32\\notepad.exe";

    if (CreateProcess(processName, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
    {
        printf("PID:%d",pi.dwProcessId);
    }
    else
    {
        printf("CreateProcess Error:%d\n", GetLastError());
    }
}

  运行上面代码,得到记事本PID:32,之后使用OpenProcess来操作进程

#include <stdio.h>
#include <Windows.h>

int main()
{
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 32);	// 32就是记事本的PID
    if (!TerminateProcess(hProcess, 0))
    {
        printf("error %d",GetLastError());
    }
    return 0;
}

挂起方式创建进程

  对应第六个参数dwCreationFlags,进程的创建方式,如果想让新的进程使用一个新的控制台,而不是继承父进程的控制台。使用CREATE_NEW_CONSOLE即可

  运行后子进程以新窗口的方式打开

#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>

int main()
{
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ZeroMemory(&pi, sizeof(pi));
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    
    char processName[] = "C:\\Windows\\System32\\cmd.exe";
    char cmdLine[] = " /c net user && pause";
    
    if (CreateProcess(processName, cmdLine, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi))
    {
        printf("PID:%d",pi.dwProcessId);
    }
    else
    {
        printf("CreateProcess Error:%d\n", GetLastError());
    }
}

  对我们最有用的是以挂起方式创建进程,值为CREATE_SUSPENDED。本质上还是挂起的主线程。下面代码按任意键后,弹出记事本

#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>

int main()
{
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ZeroMemory(&pi, sizeof(pi));
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);

    char processName[] = "C:\\Windows\\System32\\notepad.exe";

    if (CreateProcess(processName, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi))
    {
        system("pause");
        ResumeThread(pi.hThread);	// 继续运行
    }
    else
    {
        printf("CreateProcess Error:%d\n", GetLastError());
    }
}

遍历进程

  以快照的方式遍历进程CreateToolhelp32Snapshot,需包含头文件TlHelp32.h。快照的意思是先给当前的进程截个图,之后再告诉图里的内容。是非实时的

// 为特定的进程拍系统快照,也可以为进程使用的堆、模块或线程拍快照。
HANDLE WINAPI CreateToolhelp32Snapshot(
 	DWORD dwFlags,			// 设置快照对象 值为:TH32CS_SNAPPROCESS 表示获取进程
    DWORD th32ProcessID		// 快照进程号 要获取当前进程快照时设为0
);							// 成功 返回句柄  失败 返回INVALID_HANDLE_VALUE

  PROCESSENTRY32用来存放快照进程信息的一个结构体

typedef struct tagPROCESSENTRY32
{
    DWORD   dwSize;					// 结构体的大小 必须初始化为sizeof(PROCESSENTRY32)
    DWORD   cntUsage;				// 已废弃
    DWORD   th32ProcessID;          // 进程ID
    ULONG_PTR th32DefaultHeapID;	// 已废弃
    DWORD   th32ModuleID;           // 已废弃
    DWORD   cntThreads;				// 此进程开启的线程总数
    DWORD   th32ParentProcessID;    // 父进程ID
    LONG    pcPriClassBase;         // 线程优先权
    DWORD   dwFlags;				// 已废弃
    CHAR    szExeFile[MAX_PATH];    // 进程名
} PROCESSENTRY32;

  首次遍历进程

BOOL WINAPI Process32First(
    HANDLE hSnapshot,			// 进程快照句柄
    LPPROCESSENTRY32 lppe		// 进程信息结构体
);

  遍历下一个进程

BOOL WINAPI Process32NextW(
    HANDLE hSnapshot,			// 进程快照句柄
    LPPROCESSENTRY32W lppe		// 进程信息结构体
);

  示例代码

#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>
#include <TlHelp32.h>

int main()
{
    // 创建进程快照
    HANDLE hProcess = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    // 句柄无效
    if (hProcess == INVALID_HANDLE_VALUE)
    {
        return 0;
    }

    // 快照信息结构体
    PROCESSENTRY32 pe;
    ZeroMemory(&pe, sizeof(pe));
    pe.dwSize = sizeof(pe);

    // 首次遍历进程
    BOOL flag = Process32First(hProcess, &pe);
    // 循环遍历进程
    while (flag)
    {
        printf("%s %d\n",pe.szExeFile,pe.th32ProcessID);
        flag = Process32Next(hProcess, &pe);
    }

}

其他API

获取当前进程ID(PID):GetCurrentProcessId()
获取当前进程句柄:GetCurrentProcess()
获取命令行:GetCommandLine()
获取启动信息:GetStartupInfo()

线程

什么是线程

  线程是程序执行时的最小单位。是附属在进程上的执行实体,是代码的执行流程。一个进程可以有多个线程,但至少都有一个主线程。没有线程的进程是无法执行的。
  有几个线程就表示着有几个代码在执行,但是它们并不一定是同时执行,例如单核的CPU情况下是不存在多线程的,线程的执行是有时间顺序的,但是CPU切换的非常快,所以给我们的感觉和多核CPU没什么区别。

创建线程

  采用CreateThread()创建线程,语法格式如下

HANDLE WINAPI CreateThread(
    LPSECURITY_ATTRIBUTES lpThreadAttributes,	// 安全属性 通常为NULL表示使用默认设置
    SIZE_T dwStackSize,							// 线程栈空间大小 传入0表示使用默认大小(1MB)
    LPTHREAD_START_ROUTINE lpStartAddress,		// 表示新线程所执行的线程函数地址
    LPVOID lpParameter,							// 线程函数的参数
    DWORD dwCreationFlags,						// 控制线程的创建 0 创建完毕立即调度 CREATE_SUSPENDED 创建后挂起
    LPDWORD lpThreadId							// 返回线程的ID号 NULL表示不需要返回该线程ID号
);												// 返回值:线程句柄 创建失败返回NULL

  线程执行函数的语法规则

DWORD WINAPI MyThreadFunction( LPVOID lpParam ); //LPVOID = void *

  创建一个线程,执行for循环打印数字

#include <stdio.h>
#include <Windows.h>

DWORD WINAPI Demo(LPVOID parameter)
{
    for (int i = 0; i < 100; i++)
    {
        printf("%d\n", i);
        Sleep(1000);
    }
}

int main()
{
    CreateThread(NULL, 0, Demo, NULL, 0, NULL);
    system("pause");	// 防止主线程退出,导致程序立即结束,试试注释这行后运行
    return 0;
}

  输出结果为

0
请按任意键继续. . . 1
2
3
4
...... 更多的数字

线程传参

  给线程函数传递参数也是非常简单的,创建线程函数的第4个参数就是线程函数传参。这里演示传递一个参数n,让n在线程中递增

#include <stdio.h>
#include <Windows.h>

DWORD WINAPI Demo(LPVOID parameter)
{
    int *n = (int *)parameter;
    for (int i = 0; i < 100; i++)
    {
        printf("%d\n", *n);
        (*n)++;
        Sleep(1000);
    }
}

int main()
{
    int n = 0;
    CreateThread(NULL, 0, Demo, &n, 0, NULL);
    system("pause");
    return 0;
}

  输出结果为

0
请按任意键继续. . . 1
2
3
4
...... 更多的数字

挂起线程

  挂起线程也叫暂停线程,当暂停后该线程不会占用cpu,语法格式如下,只需要传入一个线程句柄即可

DWORD WINAPI SuspendThread(HANDLE hThread);

恢复线程

  恢复线程就是让暂停(挂起)的线程继续运行,语法格式如下,只需要传入一个线程句柄即可

DWORD WINAPI ResumeThread(HANDLE hThread);

终止线程

方式一:ExitThread(DWORD dwExitCode); 在线程内部使用,参数为退出状态码。
方式二:线程函数 执行return或者break
方式三:TerminateThread(HANDLE hThread,DWORD dwExitCode); 分别传入线程句柄和状态码

等待线程结束

  WaitForSingleObject用于等待一个内核对象状态发生已通知状态为止,。通常TerminateThread和此函数搭配使用

DWORD WINAPI WaitForSingleObject(
    HANDLE hHandle,
    DWORD dwMilliseconds	// 等待超时时间 传入INFINITE 表示一直等待
);

综合代码

#include <stdio.h>
#include <Windows.h>
#include <conio.h>

// 是否终止线程
// TRUE 终止 FALSE 不终止
BOOL flag = FALSE;

DWORD WINAPI Demo(LPVOID parameter)
{
    printf("已启动线程\n");

    int *n = (int *)parameter;
    for (int i = 0; i < 100; i++)
    {
        // 终止线程
        if (flag == TRUE)
        {
            // 也可以直接写break或return 一样的效果
            ExitThread(0); //线程内部终止
        }

        printf("%d\n", *n);
        (*n)++;
        Sleep(1000);
    }

    printf("子线程已结束\n");
}

// 线程操作
void ControlThread(HANDLE hThread)
{
    // 主线程死循环
    while (1)
    {
        int key = _getch();

        // 按1 挂起线程
        if (key == '1')
        {
            SuspendThread(hThread);
            printf("已挂起线程\n");
        }
        // 按2 恢复线程
        else if (key == '2')
        {
            ResumeThread(hThread);
            printf("已恢复线程\n");
        }
        // 按3|4 终止线程
        else if (key == '3' || key == '4')
        {
            if (key == '3')
            {
                flag = TRUE;
            }
            else
            {
                TerminateThread(hThread, 0);
                WaitForSingleObject(hThread, INFINITE);
            }

            printf("已终止主线程");
            break;
        }
    }
}

int main()
{
    int n = 0;
    HANDLE hThread = CreateThread(NULL, 0, Demo, &n, 0, NULL);
    // 传入线程句柄
    ControlThread(hThread);
    // 关闭句柄
    CloseHandle(hThread);

    return 0;
}

  运行时输入1 2 3,得到下面输出结果

已启动线程
0
1
2
已挂起线程
已恢复线程
3
4
5
6
已终止主线程

线程安全

  先看一段代码,多线程同时操作同一变量引发的问题

#include <Windows.h>
#include <stdio.h>
#define THREADCNT 2

int num = 0;

DWORD WINAPI threadTest(void* lp)
{
	for (int i = 0; i < 100000; i++)
	{
		num++;
	}
	return 0;
}

int main()
{
	HANDLE hThread[2];
	for (int i = 0; i < THREADCNT; i++)
	{
		hThread[i] = CreateThread(NULL, 0, threadTest, NULL, 0, NULL);		
	}
	WaitForMultipleObjects(THREADCNT, hThread, TRUE, INFINITE);
	printf("%d", num);
	return 0;
}

  线程对变量执行+100000的操作,两个线程执行完毕,num按理说应该=200000。然而结果我这里的结果为num=112968。注意每次运行的结果都是不一样的。

  从上述结果来看,说明了多线程访问同一个全局变量会使全局变量的值无法预测。这样会存在安全问题,如果仅仅是读的话是没有问题的。

临界区

  想要解决线程安全问题,可以使用临界区。临界区指的是一个访问共用资源的程序片段无法同时被多个线程访问的特性。

  临界资源是一次仅允许一个进程使用的共享资源。每次只准许一个进程进入临界区,进入后不允许其他进程进入。不论是硬件临界资源,还是软件临界资源,多个进程必须互斥地对它进行访问。

实现

  下面是临界区api的使用

InitializeCriticalSection 	// 初始化临界区
DeleteCriticalSection		// 销毁临界区
EnterCriticalSection		// 进入临界区
LeaveCriticalSection		// 退出临界区

  代码非常简单,如下,即可完美解决线程安全问题

#include <Windows.h>
#include <stdio.h>
#define THREADCNT 2

CRITICAL_SECTION cs;

int num = 0;

DWORD WINAPI threadTest(void* lp)
{
	for (int i = 0; i < 100000; i++)
	{
		// 进入临界区
		EnterCriticalSection(&cs);
		num++;
		// 退出临界区
		LeaveCriticalSection(&cs);
	}
	return 0;
}

int main()
{
	// 初始化临界区
	InitializeCriticalSection(&cs);

	HANDLE hThread[2];
	for (int i = 0; i < THREADCNT; i++)
	{
		hThread[i] = CreateThread(NULL, 0, threadTest, NULL, 0, NULL);
	}

	WaitForMultipleObjects(THREADCNT, hThread, TRUE, INFINITE);
	printf("%d", num);
	// 销毁临界区
	DeleteCriticalSection(&cs);

	return 0;
}

误用

  要想使用临界区写出一个安全的代码,其实是比较困难的,下面是临界区的典型误用例子

int num = 0;

DWORD WINAPI thread1(void* lp)
{
	EnterCriticalSection(&cs);
	for (int i = 0; i < 100000; i++)
	{
		num++;
	}
	LeaveCriticalSection(&cs);
}

DWORD WINAPI thread2(void* lp)
{
	EnterCriticalSection(&cs);
	for (int i = 0; i < 100000; i++)
	{
		num++;
	}
	LeaveCriticalSection(&cs);
}

  这样的代码咋一看没有啥毛病,对,至少在结果看来还是num=200000。可仔细想想,当线程1在for之前进入了临界区,这个时候线程2执行准备进入临界区,因为线程1还没退出临界区,于是线程2就一直等待中直到线程1退出临界区。

  这样的写法,根本就没有体现出多线程,相反效率还低了起来。换言之它完全等于下面写法

for (int i = 0; i < 2; i++)
{
	for (int j = 0; j < 100000; j++)
	{
		num++;
	}
}

  正确的理解应该是,那个全局变量要被多线程访问,就在此全局变量前使用临界区即可。参考临界区实现代码。

死锁

  死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象。

  举个例子,线程A需要线程B的某一个资源才能继续执行,同时线程B需要线程A的某个资源才能继续执行。它们都在等待对方的资源,从而导致一直阻塞不能正常执行

  使用临界区造成死锁例子

#include <Windows.h>
#include <stdio.h>

CRITICAL_SECTION A;
CRITICAL_SECTION B;

DWORD WINAPI threadA(void* lp)
{
	printf("线程A进入临界区A\n");
	EnterCriticalSection(&A);
	{
		printf("线程A进入临界区B\n");
		EnterCriticalSection(&B);
		LeaveCriticalSection(&B);
		printf("线程A退出临界区B\n");
	}
	LeaveCriticalSection(&A);
	printf("线程A退出临界区A\n");
	return 0;
}

DWORD WINAPI threadB(void* lp)
{
	printf("线程B进入临界区B\n");
	EnterCriticalSection(&B);
	{
		printf("线程B进入临界区A\n");
		EnterCriticalSection(&A);
		LeaveCriticalSection(&A);
		printf("线程B退出临界区A\n");
	}
	LeaveCriticalSection(&B);
	printf("线程B退出临界区B\n");
	return 0;
}

// 死锁测试
void Deadlock()
{
	HANDLE hThreadA = CreateThread(NULL, 0, threadA, NULL, 0, NULL);
	HANDLE hThreadB = CreateThread(NULL, 0, threadB, NULL, 0, NULL);
	
	WaitForSingleObject(hThreadA, INFINITE);
	WaitForSingleObject(hThreadB, INFINITE);
	CloseHandle(hThreadA);
	CloseHandle(hThreadB);
}
int main()
{
	// 初始化临界区
	InitializeCriticalSection(&A);
	InitializeCriticalSection(&B);

	while (1)
	{
		Deadlock();
		printf("-------------------------------\n");
	}
	return 0;
}

  运行结果

-------------------------------
线程A进入临界区A
线程A进入临界区B
线程A退出临界区B
线程A退出临界区A
线程B进入临界区B
线程B进入临界区A
线程B退出临界区A
线程B退出临界区B
-------------------------------
线程A进入临界区A
线程B进入临界区B
线程B进入临界区A
线程A进入临界区B
产生死锁,无限等待

  希望好好理解下上面的示例,死锁可能很久都不会发生,但发生锁一辈子。

  想要解决上面的死锁状态,必须按照完全相同的顺序对资源进行访问。修改后

DWORD WINAPI threadA(void* lp)
{
	EnterCriticalSection(&A);
	{
		EnterCriticalSection(&B);
		LeaveCriticalSection(&B);
	}
	LeaveCriticalSection(&A);
}

DWORD WINAPI threadB(void* lp)
{
	EnterCriticalSection(&A);
	{
		EnterCriticalSection(&B);
		LeaveCriticalSection(&B);
	}
	LeaveCriticalSection(&A);
}

事件

  在讲事件之前,补充一些内容在此,线程内核对象总是在未通知状态中创建的。线程内核对象也可以处于已通知状态和未通知状态。

  未通知时,线程处于不可调度,即无法运行线程。当线程处于已通知状态,即变为可调度状态,线程很快恢复运行

  事件能够通知一个操作已经完成。有两种不同类型的事件,一种是手动重置事件,另一种是自动重置事件。当手动重置事件得到通知时,等待该事件的所有线程变为可调度。当自动重置事件得到通知时,等待该事件的线程中只有一个线程可变为可调度线程。

  使用CreateEvent创建事件

HANDLE CreateEvent(
	LPSECURITY_ATTRIBUTES lpEventAttributes,	// 指向SECURITY_ATTRIBUTES结构体指针
	BOOL                  bManualReset,			// TRUE:手动重置 FALSE:自动重置
	BOOL                  bInitialState,		// TRUE:已通知状态 FALSE:未通知状态
	LPCSTR                lpName				// 事件名
);												// 成功返回 句柄 失败返回 NULL

  一旦事件已经创建,就可以控制它的状态。当调用SetEvent可以将事件改为已通知状态。

BOOL SetEvent(HANDLE hEvent);					

  当调用ResetEvent函数时,可以将该事件改为未通知状态

BOOL ResetEvent(HANDLE hEvent);

手动

  下面是手动重置事件的例子

#include <stdio.h>
#include <Windows.h>

HANDLE hEvent;

DWORD WINAPI thread1(LPVOID lp)
{
	// 无限等待事件变为通知
	WaitForSingleObject(hEvent, INFINITE);
	printf("thread1 running\n");
}

DWORD WINAPI thread2(LPVOID lp)
{
	// 无限等待事件变为通知
	WaitForSingleObject(hEvent, INFINITE);
	printf("thread2 running\n");
}

int main()
{
	// 创建手动重置,默认未通知状态的事件
	hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

	CreateThread(NULL, 0, thread1, NULL, 0, NULL);
	CreateThread(NULL, 0, thread2, NULL, 0, NULL);

	printf("按任意键让线程运行\n");
	getchar();
	// 设置事件为已通知
	SetEvent(hEvent);
	
	CloseHandle(hEvent);

	return 0;
}

  上面代码,创建了一个手动重置的未通知状态的事件,上面两个线程都调用WaitForSingleObject,使得线程暂停运行。一旦主线程调用SetEvent,给事件发出通知信号,这时,系统就使所有线程进入可调度状态,它们都获得了CPU时间。

  运行结果

按任意键让线程运行

thread1 running
thread2 running

  如果将hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); 改为hEvent = CreateEvent(NULL, TRUE, TRUE, NULL);。就创建了一个手动重置的通知状态的事件,即使不按任意键线程也会运行

  运行结果

按任意键让线程运行
thread1 running
thread2 running

  事件也可以让线程有序执行,示例代码

#include <stdio.h>
#include <Windows.h>

HANDLE hEvent1, hEvent2;

DWORD WINAPI thread1(LPVOID lp)
{
	for (int i = 0; i < 3; i++)
	{
		// 等待线程1变为通知状态
		WaitForSingleObject(hEvent1, INFINITE);
		printf("第%d次 thread1 running\n",i + 1);
		// 设置线程1为未通知
		ResetEvent(hEvent1);
		// 设置线程2为通知
		SetEvent(hEvent2);
	}
}

DWORD WINAPI thread2(LPVOID lp)
{
	for (int i = 0; i < 3; i++)
	{
		// 等待线程2变为通知状态
		WaitForSingleObject(hEvent2, INFINITE);
		printf("第%d次 thread2 running\n",i + 1);
		// 设置线程2为未通知
		ResetEvent(hEvent2);
		// 设置线程1为通知
		SetEvent(hEvent1);
	}
}

int main()
{

	hEvent1 = CreateEvent(NULL, TRUE, FALSE, NULL);
	hEvent2 = CreateEvent(NULL, TRUE, FALSE, NULL);

	CreateThread(NULL, 0, thread1, NULL, 0, NULL);
	CreateThread(NULL, 0, thread2, NULL, 0, NULL);

	// 线程1先运行
	SetEvent(hEvent1);
	// 线程2先运行 
	// SetEvent(hEvent2);
	system("pause");
	CloseHandle(hEvent1);
	CloseHandle(hEvent2);

	return 0;
}

  运行结果

第1次 thread1 running
第1次 thread2 running
第2次 thread1 running
第2次 thread2 running
第3次 thread1 running
第3次 thread2 running
请按任意键继续. . .

自动

  使用自动重置事件,系统只允许一个线程变成可调度状态,同时,也无法保证系统将那个线程变为可调度状态

  示例代码

#include <stdio.h>
#include <Windows.h>

HANDLE hEvent;

DWORD WINAPI Thread(LPVOID lp)
{
	WaitForSingleObject(hEvent, INFINITE);
	printf("thread1 running\n");
}

int main()
{
	// 创建一个自动重置、默认未通知状态的事件
	hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
	CreateThread(NULL, 0, Thread, NULL, 0, NULL);
	system("pause");

	return 0;
}

  运行后,无任何输出结果,将hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);,改为hEvent = CreateEvent(NULL, FALSE, TRUE, NULL);

  输出结果为thread1 running

线程池

  线程池是聚集了一大堆的线程资源,等需要它们的时候,就可以使用它们,而不必向操作系统去申请。当申请线程资源的话,要从应用层转到内核层,这期间花费的时间是非常慢的,如果要经常使用线程,而采用动态申请就需要非常多的时间,这会大大降低程序的效率。因此,针对这种情况,在程序开始时,可以申请一大堆线程,需要的时候就从线程池里边拿,不需要的时候就让回到线程池。

异步方式

  第一种,不创建工作项的方式。使用TrySubmitThreadpoolCallback函数是将一个任务添加进线程池队列

  请注意,这种方式有可能失败,即线程不会启动,为了确保线程能够启动,看第二种方式。

BOOL TrySubmitThreadpoolCallback(
	PTP_SIMPLE_CALLBACK  pfns,		// 请求调用的回调函数
	PVOID                pv,		// 向回调函数传递的参数
	PTP_CALLBACK_ENVIRON pcbe		// 定制线程池
);									// 成功为TRUE  失败为FALSE

  跟进PTP_SIMPLE_CALLBACK,定义如下,必须按下面格式来定义函数

typedef VOID (NTAPI *PTP_SIMPLE_CALLBACK)(
	PTP_CALLBACK_INSTANCE Instance,
	PVOID                 Context
);

  示例代码

#include <stdio.h>
#include <Windows.h>

void NTAPI SimpleCallBack(PTP_CALLBACK_INSTANCE Instance, PVOID Context)
{
	printf("---------------------\n");
	printf("hello %s\n", (char*)Context);
	printf("Thread ID %d\n", GetCurrentThreadId());
}

int main()
{
	char* str = "world";
	if (!TrySubmitThreadpoolCallback(SimpleCallBack, (PVOID)str, NULL)) return 0;
	if (!TrySubmitThreadpoolCallback(SimpleCallBack, (PVOID)str, NULL)) return 0;
	// 延迟1s 确保两个线程都能够执行
	Sleep(1000);
	return 0;
}

  运行结果,可看到线程id是不同的,这就更加证明了这是一个线程池

---------------------
hello world
Thread ID 17344
---------------------
hello world
Thread ID 16944

  第二种方式,使用CreateThreadpoolWork创建工作项

PTP_WORK CreateThreadpoolWork(
	PTP_WORK_CALLBACK    pfnwk,		// 请求调用的回调函数
	PVOID                pv,		// 向回调函数传递的参数
	PTP_CALLBACK_ENVIRON pcbe		// 定制线程池
);									// 成功返回PTP_WORK 失败返回NULL

  PTP_WORK_CALLBACK定义

typedef VOID (NTAPI *PTP_WORK_CALLBACK)(
	PTP_CALLBACK_INSTANCE Instance,
	PVOID                 Context,
	PTP_WORK              Work
);

  当创建工作项成功后,可以使用SubmitThreadpoolWork提交请求

void SubmitThreadpoolWork(
	PTP_WORK pwk	// 指向PTP_WORK 由CreateThreadpoolWork函数返回此指针
);

  最后用CloseThreadpoolWork来释放工作项。

  如果想等待线程池中的线程全部都执行完毕,使用WaitForThreadpoolWorkCallbacks函数

void WaitForThreadpoolWorkCallbacks(
	PTP_WORK pwk,						// 指向PTP_WORK
	BOOL     fCancelPendingCallbacks	// TRUE:取消尚未运行的任务 FALSE:等待所有的任务完成
);

  下面示例会输出三遍hello world

#include <stdio.h>
#include <Windows.h>


VOID NTAPI WorkCallBack(PTP_CALLBACK_INSTANCE Instance,PVOID Context,PTP_WORK Work)
{
	printf("hello %s\n", (char*)Context);
}

int main()
{
	char* str = "world";

	// 创建一个线程池工作项
	PTP_WORK pwk = CreateThreadpoolWork(WorkCallBack, str, NULL);

	// 提交 3 次线程池请求,也就是运行 4 次线程池函数
	SubmitThreadpoolWork(pwk);
	SubmitThreadpoolWork(pwk);
	SubmitThreadpoolWork(pwk);

	// TRUE 取消尚未运行的任务 FALSE 等待所有任务完成
	WaitForThreadpoolWorkCallbacks(pwk, FALSE);

	// 销毁工作项 并不是销毁线程池
	CloseThreadpoolWork(pwk);
	return 0;
}

定时器

  CreateThreadpoolTimer,可以每隔一段时间调用一个函数

PTP_TIMER CreateThreadpoolTimer(
	PTP_TIMER_CALLBACK   pfnti,
	PVOID                pv,
	PTP_CALLBACK_ENVIRON pcbe
);

  PTP_TIMER_CALLBACK定义

typedef VOID (NTAPI *PTP_TIMER_CALLBACK)(
	PTP_CALLBACK_INSTANCE Instance,
	PVOID                 Context,
	PTP_TIMER             Timer
);

  之后,调用SetThreadpoolTimer设置定时器

void SetThreadpoolTimer(
	PTP_TIMER pti,				// CreateThreadpoolTimer函数返回的指针
	PFILETIME pftDueTime,		// 指向FILETIME结构的指针
	DWORD     msPeriod,			// 调用时间间隔单位毫秒 0表示只调用一次
	DWORD     msWindowLength	// 0即可
);

  等待计时器完成WaitForThreadpoolTimerCallbacks,实测不起作用

void WaitForThreadpoolTimerCallbacks(
	PTP_TIMER pti,
	BOOL      fCancelPendingCallbacks
);

  关闭定时器CloseThreadpoolTimer

  示例

#include <stdio.h>
#include <Windows.h>

VOID NTAPI TimerCallBack(PTP_CALLBACK_INSTANCE Instance,PVOID Context,PTP_TIMER Timer)
{
	printf("hello\n");
}

int main()
{
	PTP_TIMER pti = CreateThreadpoolTimer(TimerCallBack, NULL, NULL);

	ULARGE_INTEGER ulRelativeTimer;
	ulRelativeTimer.QuadPart = (LONGLONG)-(10000000);
	FILETIME ftRelativeTimer;
	ftRelativeTimer.dwHighDateTime = ulRelativeTimer.HighPart;
	ftRelativeTimer.dwLowDateTime = ulRelativeTimer.LowPart;
	SetThreadpoolTimer(pti, &ftRelativeTimer, 0, 0);

	Sleep(2000);
	// WaitForThreadpoolTimerCallbacks(pti,TRUE); // 不起作用
	CloseThreadpoolTimer(pti);
	return 0;
}

内核对象

  CreateThreadpoolWait创建线程池等待对象

PTP_WAIT CreateThreadpoolWait(
	PTP_WAIT_CALLBACK    pfnwa,
	PVOID                pv,
	PTP_CALLBACK_ENVIRON pcbe
);

  PTP_WAIT_CALLBACK定义

typedef VOID (NTAPI *PTP_WAIT_CALLBACK)(
	PTP_CALLBACK_INSTANCE Instance,
	PVOID                 Context,
	PTP_WAIT              Wait,
	TP_WAIT_RESULT        WaitResult
);

  SetThreadpoolWait将内核对象绑定到线程池

void SetThreadpoolWait(
	PTP_WAIT  pwa,			// CreateThreadpoolWait的返回值
	HANDLE    h,			// 句柄
	PFILETIME pftTimeout	// 等待时间 0 不等待 NULL 无限等待
);

  等待线程池中的线程操作完成使用WaitForThreadpoolWaitCallbacks,关闭使用CloseThreadpoolWait
  示例,吐槽一下我真的不知道WaitForThreadpoolWaitCallbacks在这里有什么用

#include <stdio.h>
#include <Windows.h>

VOID NTAPI WaitCallBack(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WAIT Wait, TP_WAIT_RESULT WaitResult)
{
	printf("hello");
}
int main()
{
	HANDLE hEvent = CreateEvent(NULL, FALSE, TRUE, NULL);
	PTP_WAIT pwa = CreateThreadpoolWait(WaitCallBack, NULL, NULL);
	SetThreadpoolWait(pwa, hEvent, NULL);
	Sleep(2000);
	CloseThreadpoolWait(pwa);
	return 0;
}

异步IO

  CreateThreadpoolIo创建线程池IO对象

PTP_IO CreateThreadpoolIo(
	HANDLE                fl,
	PTP_WIN32_IO_CALLBACK pfnio,
	PVOID                 pv,
	PTP_CALLBACK_ENVIRON  pcbe
);

  PTP_WIN32_IO_CALLBACK定义

typedef VOID (WINAPI *PTP_WIN32_IO_CALLBACK)(
    _Inout_     PTP_CALLBACK_INSTANCE Instance,
    _Inout_opt_ PVOID                 Context,
    _Inout_opt_ PVOID                 Overlapped,
    _In_        ULONG                 IoResult,
    _In_        ULONG_PTR             NumberOfBytesTransferred,
    _Inout_     PTP_IO                Io
);

  StartThreadpoolIo将线程池IO对象与线程池内部的完成端口关联

void StartThreadpoolIo( PTP_IO pio);

  示例

#include <stdio.h>
#include <Windows.h>

VOID WINAPI IoCallBack(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PVOID Overlapped, ULONG IoResult, ULONG_PTR NumberOfBytesTransferred, PTP_IO Io)
{
	printf("hello");
}

int main()
{
	HANDLE hFile = CreateFile("test.txt", GENERIC_WRITE, 0, 0, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, 0);
	PTP_IO pio = CreateThreadpoolIo(hFile, IoCallBack, NULL, NULL);
	StartThreadpoolIo(pio);

	char str[] = "hello";
	OVERLAPPED ol = { 0 };
	WriteFile(hFile, str, strlen(str), NULL, &ol);
	CloseHandle(hFile);
	WaitForThreadpoolIoCallbacks(pio, FALSE);
	CloseThreadpoolIo(pio);
	return 0;
}

内存

  物理内存分为两类。私有内存和共享内存,私有内存的意思是这块物理内存只能本进程使用。共享内存是多个进程一起用。

申请内存的两种方式

  私有内存:VirtualAlloc
  共享内存:CreateFileMapping

内存页面三种状态

  Free:进程不能访问这种页面,因为这个页面还没有被分配。任何属于这个页面的虚拟内存地址进行访问都将引用异常。

  Reserved:页面被保留以备将来使用,这些页面已被分配,但是没使用,物理地址空间中的内存不存在与其对应的物理内存分页。处于被保留的内存分页也不能被访问。

  Committed:内存已经被分配,并且已经被使用,具有与之对应的物理地址空间中的内存分页。

  VirtualAlloc可用于指定分配的内存是什么状态,如果当前内存的状态是Committed,则可以直接访问。

  VirtualAlloc能够将内存页面的状态从Free、Reserved改为Committed,也可以将Free->Reserved,Reserved->Committed。

私有内存

申请和释放

  申请函数 VirtualAlloc  申请其他进程内存使用VirtualAllocEx

LPVOID WINAPI VirtualAlloc(
    LPVOID 	lpAddress,			// 分配内存区域起始地址 一般为NULL
   	SIZE_T 	dwSize,				// 分配内存大小 为了内存对齐值为 0X1000 * n
    DWORD 	flAllocationType,	// 分配内存类型
    DWORD 	flProtect			// 内存保护属性
);								// 成功返回分配的页面区域的基址 失败返回NULL

  flAllocationType 参数

// 还有其他参数没写出
MEM_COMMIT		// 为指定地址空间提交物理内存
MEM_RESERVE		// 保留指定地址空间,不分配物理内存

  flProtect 参数

// 还有其他参数没写出
PAGE_NOACCESS			// 不可访问
PAGE_READONLY 			// 可读 
PAGE_READWRITE			// 可读写
PAGE_EXECUTE			// 可执行
PAGE_EXECUTE_READ		// 可执行 可读
PAGE_EXECUTE_READWRITE	// 可读 可写 可执行
PAGE_GUARD				// 设置为保护页 如果试图对该区域进行读写操作,会产生一个STATUS_GUARD_PAGE异常

  释放内存 VirtualFree  释放其他进程内存使用VirtualFreeEx

BOOL WINAPI VirtualFree(
    LPVOID lpAddress,	// 内存区域地址
    SIZE_T dwSize,		// 内存大小
    DWORD dwFreeType	// 释放类型 MEM_DECOMMIT(释放物理内存,保留线性地址) MEM_RELEASE(释放物理地址和线性地址,使用此值,内存大小必须为0)
);						// 成功返回非零 失败返回0

  简单使用例子

#include <stdio.h>
#include <Windows.h>

int main()
{
    LPVOID lpAddress = VirtualAlloc(NULL, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    // 成功申请内存
    if(lpAddress != NULL)
    {
        // 成功释放内存
        if(VirtualFree(lpAddress, 0, MEM_RELEASE))
        {
            printf("success free");
        }
    }
    return 0;
}
查询内存块信息

  获取内存块信息 VirtualQuery  获取其他进程内存块信息用VirtualQueryEx

SIZE_T VirtualQuery(
  LPCVOID                   lpAddress,	// 查询区域的地址
  PMEMORY_BASIC_INFORMATION lpBuffer,	// 接收返回信息的指针
  SIZE_T                    dwLength	// 缓冲区大小,上述结构体的大小
);										// 返回实际查询的字节数 失败返回0

  保存内存块信息结构体 MEMORY_BASIC_INFORMATION

typedef struct _MEMORY_BASIC_INFORMATION {
  PVOID  BaseAddress;			// 该页面的起始地址
  PVOID  AllocationBase;		// 配给该页面的首地址
  DWORD  AllocationProtect;		// 页面的保护属性
  WORD   PartitionId;
  SIZE_T RegionSize;			// 从BaseAddress开始,具有相同属性的页面的大小,
  DWORD  State;					// 页面的状态,有三种值:MEM_COMMIT、MEM_FREE和MEM_RESERVE,
  DWORD  Protect;				// 保护属性
  DWORD  Type;					// 内存块属性 MEM_IMAGE(映射类型) MEM_MAPPED(共享内存) MEM_PRIVATE(私有内存)
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;

  使用例子

#include <stdio.h>
#include <Windows.h>

int main()
{
    // 内存块信息结构体
    MEMORY_BASIC_INFORMATION memoryInfo;

    LPVOID lpAddress = VirtualAlloc(NULL, 0x1000, MEM_COMMIT, PAGE_READWRITE);
    if (lpAddress != NULL)
    {
        // 获取内存块信息
        VirtualQuery(lpAddress, &memoryInfo, sizeof(memoryInfo));
		
        printf("lpAddress:%p\n",lpAddress);
        printf("BaseAddress:%X\n", memoryInfo.BaseAddress);
        printf("AllocationBase:%X\n", memoryInfo.AllocationBase);

        if (memoryInfo.State == MEM_COMMIT)
        {
            printf("State is MEM_COMMIT\n");
        }

        if (memoryInfo.Protect == PAGE_READWRITE)
        {
            printf("Protect is PAGE_READWRITE\n");
        }

        if (memoryInfo.Type == MEM_PRIVATE)
        {
            printf("Type is MEM_PRIVATE\n");
        }

        VirtualFree(lpAddress, 0, MEM_RELEASE);
    }

    return 0;
}

  运行结果

lpAddress:00030000
BaseAddress:30000
AllocationBase:30000
State is MEM_COMMIT
Protect is PAGE_READWRITE
Type is MEM_PRIVATE
更改保护属性

  改变保护属性VirtualProtect  改变其他进程保护属性使用VirtualProtectEx

BOOL WINAPI VirtualProtect(
  LPVOID lpAddress,			// 改变内存属性起始地址
  SIZE_T dwSize,			// 改变内存属性区域大小
  DWORD  flNewProtect,		// 新的内存属性类型
  PDWORD lpflOldProtect		// 旧的内存属性类型
);							// 成功返回非零 失败返回0

  一个规范的例子

#include <stdio.h>
#include <Windows.h>

int main()
{
    LPVOID lpAddress = VirtualAlloc(NULL, 0x1000, MEM_COMMIT, PAGE_READWRITE);

    if (lpAddress != NULL)
    {
        // 保存旧的内存属性类型
        DWORD oldProtect = 0;

        if (VirtualProtect(lpAddress, 0x1000, PAGE_READONLY, &oldProtect))
        {
            if (oldProtect == PAGE_READWRITE)
            {
                printf("oldProtect is PAGE_READWRITE\n");
            }

            // 获取当前内存属性
            MEMORY_BASIC_INFORMATION memoryInfo;

            // 查询成功
            if (VirtualQuery(lpAddress, &memoryInfo, sizeof(memoryInfo)))
            {
                if (memoryInfo.Protect == PAGE_READONLY)
                {
                    printf("newProtect is PAGE_READONLY\n");
                }
            }
        }

        if (VirtualFree(lpAddress, 0, MEM_RELEASE))
        {
            printf("success free\n");
        }
    }
    return 0;
}

  输出结果

oldProtect is PAGE_READWRITE
newProtect is PAGE_READONLY
success free

读内存

  读取内存的函数为ReadProcessMemory

BOOL ReadProcessMemory(
	HANDLE  hProcess,				// 进程句柄 此句柄必须有PROCESS_VM_READ权限
    LPCVOID lpBaseAddress,			// 读取内存首地址
	LPVOID  lpBuffer,				// 指向缓冲区的指针
	SIZE_T  nSize,					// 读取大小
	SIZE_T  *lpNumberOfBytesRead	// 设为NULL即可
);									// 成功返回非0 失败返回0

  被读取内存的简单程序

#include <stdio.h>
#include <Windows.h>

int main()
{
    char message[] = "I am secure";
    printf("PID: %d\nMessage: %s\nAddr: %p", GetCurrentProcessId(), message, message);
    getchar();
    return 0;
}

  运行结果

PID: 14172
Message: I am secure
Addr: 0060FF04

  读取内存

#include <stdio.h>
#include <windows.h>

int main()
{
    // 进程pid
    DWORD pid = 14172;
    // 读取地址
    DWORD lpBaseAddress = 0x0060FF04;
    // 获取进程句柄
    HANDLE hProcess = OpenProcess(PROCESS_VM_READ, FALSE, pid);
    if (!hProcess)
    {
        printf("%d", GetLastError());
        return 0;
    }

    char lpBuffer[30];
    if(!ReadProcessMemory(hProcess, (LPCVOID)lpBaseAddress, (LPVOID)lpBuffer, 30, NULL))
    {
        printf("%d", GetLastError());
        return 0;
    }

    printf("%s", lpBuffer);
    CloseHandle(hProcess);

    return 0;
}

  运行结果

I am secure

写内存

  写内存和读内存的方式都大同小异,用到的函数为WriteProcessMemory

BOOL WriteProcessMemory(
    HANDLE  hProcess,					// 进程句柄 此句柄必须有PROCESS_VM_WRITE,PROCESS_VM_OPERATION权限
    LPVOID  lpBaseAddress,				// 写入内存首地址
    LPCVOID lpBuffer,					// 指向缓冲区的指针
    SIZE_T  nSize,						// 写入读取大小
    SIZE_T  *lpNumberOfBytesWritten		// 设为NULL即可
);										// 成功返回非0 失败返回0

  被写入内存的程序

#include <stdio.h>
#include <Windows.h>

int main()
{
    char message[] = "I am secure";
    printf("PID: %d\nMessage: %s\nAddr: %p", GetCurrentProcessId(), message, message);
    getchar();
    printf("Message:%s", message);
    return 0;
}

  运行结果

PID: 6644
Message: I am secure
Addr: 0060FF04
Message:pwnedsecure

  写内存

#include <stdio.h>
#include <windows.h>

int main()
{
    // 进程pid
    DWORD pid = 6644;
    // 读取地址
    DWORD lpBaseAddress = 0x0060FF04;
    // 获取进程句柄
    HANDLE hProcess = OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, pid);
    if (!hProcess)
    {
        printf("error hProcess %d", GetLastError());
        return 0;
    }

    char lpBuffer[] = "pwned";
    if(!WriteProcessMemory(hProcess, (LPVOID)lpBaseAddress, (LPCVOID)lpBuffer, strlen(lpBuffer), NULL))
    {
        printf("error ReadProcessMemory %d", GetLastError());
        return 0;
    }

    printf("success!\n");
    CloseHandle(hProcess);

    return 0;
}

文件系统

  文件系统是操作系统用于管理磁盘上文件的方法和数据结构;也就是在磁盘上如何组织文件的方法。本章介绍卷、目录、文件相关操作的API

  卷就是我们本地磁盘(逻辑驱动器),看不懂没关系,继续看下面的内容就能理解了。

获取所有卷

  函数为GetLogicalDriveStrings

DWORD GetLogicalDriveStrings(
	DWORD nBufferLength,	// 缓冲区的大小
	LPTSTR lpBuffer			// 指向缓冲区的指针
);							// 返回实际需要缓冲区的大小 失败返回0 返回值大于nBufferLength 说明给定的缓冲区大小不够

  例子

#include <stdio.h>
#include <Windows.h>

int main()
{
    char drives[100] = {0};
    DWORD realSize = GetLogicalDriveStrings(100, drives);

    printf("realSize=%d\n",realSize);

    for (int i = 0; i < 100; i++)
    {
        if (drives[i] != '\0')
        {
            printf("%c",drives[i]);
        }
    }
    return 0;
}

  输出

realSize=16
C:\D:\E:\G:\
获取卷类型

  函数为GetDriveType

UINT GetDriveType(
	LPCSTR lpRootPathName	// 驱动器根目录 为NULL使用当前卷
);							// 返回驱动器类型

   驱动器类型如下

DRIVE_UNKNOWN     = 0; 		// 无法确定驱动器类型
DRIVE_NO_ROOT_DIR = 1; 		// 根路径无效
DRIVE_REMOVABLE   = 2; 		// 软盘
DRIVE_FIXED       = 3; 		// 本地硬盘
DRIVE_REMOTE      = 4; 		// 网络磁盘
DRIVE_CDROM       = 5; 		// CD-ROM
DRIVE_RAMDISK     = 6; 		// RAM 磁盘

  例子

#include <stdio.h>
#include <Windows.h>

int main()
{
    UINT type = GetDriveType("c:\\");
    printf("%d",type);
    return 0;
}

  输出

3
获取卷信息

  函数为GetVolumeInformation

BOOL GetVolumeInformationA(
	LPCSTR  	lpRootPathName,				// 卷根目录 为NULL使用当前卷
    LPSTR   	lpVolumeNameBuffer,			// 存放卷名
    DWORD   	nVolumeNameSize,			// 卷名长度
    LPDWORD 	lpVolumeSerialNumber,		// 卷序列号 不需要可以设置成NULL
    LPDWORD 	lpMaximumComponentLength,	// 最大文件文件名组件长度
    LPDWORD 	lpFileSystemFlags,			// 文件系统属性
    LPSTR   	lpFileSystemNameBuffer,		// 文件系统 NTFS/FAT
    DWORD   	nFileSystemNameSize			// 文件系统缓冲区长度
);											// 成功返回非0 失败返回0

  例子

#include <stdio.h>
#include <Windows.h>

int main()
{
    char szVolumeNameBuf[MAX_PATH] = {0};
    DWORD dwVolumeSerialNum;
    DWORD dwMaxComponentLength;
    DWORD dwSysFlags;
    char szFileSystemBuf[MAX_PATH] = {0};

    BOOL bGet = GetVolumeInformation(
                    "C:\\",
                    szVolumeNameBuf,
                    MAX_PATH,
                    &dwVolumeSerialNum,
                    &dwMaxComponentLength,
                    &dwSysFlags,
                    szFileSystemBuf,
                    MAX_PATH
                );
    if(bGet)
    {
        printf("%s",szFileSystemBuf);
    }

    return 0;
}

  输出

NTFS

目录

创建目录

  函数为CreateDirectory

BOOL CreateDirectory(
	LPCSTR                lpPathName,			// 要创建的目录的路径
	LPSECURITY_ATTRIBUTES lpSecurityAttributes	// 安全描述符
);												// 成功返回非0 失败返回0
删除目录

  函数为RemoveDirectory

BOOL RemoveDirectory(
	LPCSTR lpPathName		// 要删除的目录的路径
);							// 成功返回非0 失败返回0
移动目录

  函数为MoveFile

BOOL MoveFile(
	LPCTSTR lpExistingFileName,		// 目录名
	LPCTSTR lpNewFileName			// 新目录名
);									// 成功返回非0 失败返回0
获取程序当前目录

  函数为GetCurrentDirectory

DWORD GetCurrentDirectory(
	DWORD  nBufferLength,			// 字符串的缓冲区长度
	LPTSTR lpBuffer					// 接收缓冲区目录指针
);									// 成功返回写入缓冲区的字符数 失败返回0
// 若要确定所需的缓冲区大小 lpBuffer设置为 NULL 并将nBufferLength设置为0
设置程序当前目录

  函数为SetCurrentDirectory

BOOL SetCurrentDirectory(
	LPCTSTR lpPathName		// 新目录名
);							// 成功返回非0 失败返回0
综合代码
#include <stdio.h>
#include <Windows.h>

void RemoveDirectoryDemo()
{
    // 删除c:\text
    if (RemoveDirectory("c:\\test"))
    {
        printf("remove directory success\n");
    }
}

void CreateDirectoryDemo()
{
    // 在c盘下创建text文件夹
    if (CreateDirectory("c:\\test", NULL))
    {
        printf("create directory success\n");
    }
}

void MoveFileDemo()
{
    // 将c:\text 重命名成 c:\1
    if (MoveFile("c:\\test", "c:\\1"))
    {
        printf("move file success\n");
    }
}

void GetCurrentDirectoryDemo()
{
    // 获取实际大小
    DWORD size = GetCurrentDirectory(0, NULL);
    char *buffer = (char *)malloc(sizeof(char) * (size + 5));

    GetCurrentDirectory(size, buffer);
    printf("%s\n", buffer);
}

void SetCurrentDirectoryDemo()
{
    SetCurrentDirectory("c:\\");
}

int main()
{

    //CreateDirectoryDemo();
    //RemoveDirectoryDemo()
    //MoveFileDemo();

    GetCurrentDirectoryDemo();
    SetCurrentDirectoryDemo();
    GetCurrentDirectoryDemo();

    return 0;
}

文件

创建文件

  函数为CreateFile

HANDLE CreateFile(
    LPCSTR                lpFileName,				// 文件名
    DWORD                 dwDesiredAccess,			// 访问模式 GENERIC_READ GENERIC_WRITE
    DWORD                 dwShareMode,				// 共享模式
    LPSECURITY_ATTRIBUTES lpSecurityAttributes,		// SD 安全属性
    DWORD                 dwCreationDisposition,	// 创建模式 
    DWORD                 dwFlagsAndAttributes,		// 文件属性
    HANDLE                hTemplateFile				// 设为NULL即可
);													// 成功返回文件句柄 失败返回INVALID_HANDLE_VALUE

  dwCreationDisposition 有以下参数

dwCreationDisposition	文件存在			文件不存在
CREATE_ALWAYS			 覆盖					新建
OPEN_ALWAYS				 打开					新建
CREATE_NEW				 ERROR_FILE_EXISTS   新建
OPEN_EXISTING			 打开				   	ERROR_FILE_NOT_FOUND
TRUNCATE_EXISTING		 清空文件内容			  ERROR_FILE_NOT_FOUND

  以可读可写方式,有就覆盖没有就新建的方式 创建文件

CreateFile("c:\\1.txt", GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
获取文件大小

  函数为GetFileSize

DWORD GetFileSize(
	HANDLE  hFile,			// 文件句柄
	LPDWORD lpFileSizeHigh	// 设为NULL即可
);							// 成功返回文件大小(单位字节) 失败返回INVALID_FILE_SIZE

  获取d:\1.txt文件大小

HANDLE hFile = CreateFile("d:\\1.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

if (hFile != INVALID_HANDLE_VALUE)
{
    // 获取文件大小
    DWORD size = GetFileSize(hFile, NULL);
    printf("%d", size);
}
其他API
ReadFile	读文件
WriteFile	写文件
CopyFile	复制文件
DeleteFile	删除文件

进程通信

  进程通信就是指不同进程间进行数据共享和数据交换。

socket

  socket(套接字) 是操作系统提供给程序员操作「网络协议栈」的接口,通过socket 的接口,来控制协议工作,从而实现网络通信,达到跨主机通信。有tcp udp两种通信方法,这里使用tcp。

  凡是涉及到网络通信,都会有发送方和接收方,也就是我们平常说的服务器/客户端

  而编写winsock都需要头文件WinSock2.h,和链接ws2_32.lib

  大致流程
  发送方:socket -> connect -> send/recv -> close
  接收方:socket -> bind -> listen -> accept -> send/recv -> close

发送方

  1.初始化Winsock,用到WSAStartup函数

int WSAStartup(
	WORD      wVersionRequired,	 // 指定socket版本 现在已经是2.2版本了
	LPWSADATA lpWSAData			 // wsaData结构体指针
);								 // 成功返回0 失败返回非0

  最后在调用WSACleanup,进行收尾操作。

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

#pragma comment(lib, "ws2_32.lib")

int main()
{
    WSADATA wsa;
    int ret;
    // 初始化socket 使用2.2版本 成功返回0
    ret = WSAStartup(MAKEWORD(2, 2), &wsa);
    if(ret)
    {
        printf("WSAStartup fail\n");
    }

    // 清理
    WSACleanup();
}

  2.创建socket,使用socket函数

SOCKET WSAAPI socket(
	int af,			// 网络地址族 值为AF_INET 代表ipv4
	int type,		// 网络协议类型 SOCK_STREAM使用TCP SOCK_DGRAM使用UDP
	int protocol	// 网络地址族的特殊协议 IPPROTO_TCP使用TCP IPPROTO_UDP使用UDP 设为0自动使用对应的协议
);					// 成功返回SOCKET 失败返回INVALID_SOCKET

  SOCKET类型定义如下

typedef    UINT_PTR        SOCKET;
typedef    unsigned int    UINT_PTR;

  实际上就是一个unsigned int类型,它将被Socket环境管理和使用。套接字将被创建、设置、用来发送和接收数据,最后会被关闭。
  使用closesocket,来关闭一个套接字

int closesocket(
	SOCKET s	// SOCKET类型
);				// 成功返回0
// 创建Socket
SOCKET sClient = socket(AF_INET, SOCK_STREAM, 0);
if (sClient == INVALID_SOCKET)
{
    printf("socket fail\n");
    return 0;
}

// 关闭套接字
closesocket(sClient);

  3.指定socket发送和接收数据包的地址,使用sockaddr_in 结构体

typedef struct sockaddr_in {
  short          sin_family;	// 只能取值AF_INET
  USHORT         sin_port;		// IP地址端口
  IN_ADDR        sin_addr;		// 指向IN_ADDR结构体
  CHAR           sin_zero[8];	// 保留
} SOCKADDR_IN, *PSOCKADDR_IN;

  IN_ADDR结构体

typedef struct in_addr {
  union {
    	struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
      	struct { USHORT s_w1,s_w2; } S_un_w;
      	ULONG S_addr;
  } S_un;
} IN_ADDR, *PIN_ADDR, *LPIN_ADDR;

  使用connet 建立连接,是客户方连接服务方的函数

int WSAAPI connect(
	SOCKET         s,		// socket结构体
	const sockaddr *name,	// 指向sockaddr结构体的指针
	int            namelen	// sockaddr结构体大小
);							// 成功返回0 失败返回SOCKET_ERROR
// 指定发送端口
int port = 5899;
// 指定发送地址
char ip[] = "127.0.0.1";
// 设置发送地址
SOCKADDR_IN server;
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.S_un.S_addr = inet_addr(ip);
printf("connect to %s:%d\n", inet_ntoa(server.sin_addr), htons(server.sin_port));

// 建立连接
ret = connect(sClient, (SOCKADDR *)&server, sizeof(SOCKADDR));
if (ret == SOCKET_ERROR)
{
    printf("connect fail\n");
    return 0;
}

  4.发送/接收数据 ,发送数据用send函数

int WSAAPI send(
	SOCKET     s,		// socket结构体
	const char *buf,	// 发送数据指针
	int        len,		// 发送大小
	int        flags	// 设为0即可
);						// 成功返回实际发送大小 失败返回SOCKET_ERROR

  发送方完整代码

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

#pragma comment(lib, "ws2_32.lib")

int main()
{

    WSADATA wsa;
    int ret;
    // 初始化socket 使用2.2版本 成功返回0
    ret = WSAStartup(MAKEWORD(2, 2), &wsa);
    if (ret)
    {
        printf("WSAStartup fail\n");
    }

    // 创建Socket
    SOCKET sClient = socket(AF_INET, SOCK_STREAM, 0);
    if (sClient == INVALID_SOCKET)
    {
        printf("socket fail\n");
        return 0;
    }

    // 指定发送端口
    int port = 5899;
    // 指定发送地址
    char ip[] = "127.0.0.1";
    // 设置发送地址
    SOCKADDR_IN server;
    server.sin_family = AF_INET;
    server.sin_port = htons(port);
    server.sin_addr.S_un.S_addr = inet_addr(ip);
    printf("connect to %s:%d\n", inet_ntoa(server.sin_addr), htons(server.sin_port));

    // 建立连接
    ret = connect(sClient, (SOCKADDR *)&server, sizeof(SOCKADDR));
    if (ret == SOCKET_ERROR)
    {
        printf("connect fail\n");
        return 0;
    }

    // 发送数据
    char buffer[] = "hello";
    ret = send(sClient, buffer, sizeof(buffer), 0);
    if (ret == SOCKET_ERROR || ret == 0)
    {
        printf("send fail\n");
        return 0;
    }

    printf("sending:%s\n", buffer);
    printf("sending:%d bytes", ret);

    // 关闭套接字
    closesocket(sClient);
    // 清理
    WSACleanup();
    
}
接收方

  前面的步骤和发送方都一样,初始化socket,创建socket。之后就需要绑定和监听端口,使用bind绑定端口

int bind(
	SOCKET         s,		// socket
	const sockaddr *addr,	// 指向sockaddr结构体的指针
	int            namelen 	// sockaddr结构体大小
);							// 成功返回0 失败返回SOCKET_ERROR

  绑定完成后,使用listen监听端口

int WSAAPI listen(
	SOCKET s,			// socket
	int    backlog		// 挂起的连接队列的最大长度
);						// 成功返回0 失败返回SOCKET_ERROR

  使用accept 建立连接,是服务方同意客户方连接的函数

SOCKET WSAAPI accept(
	SOCKET   s,			// socket结构体
	sockaddr *addr,		// 指向sockaddr结构体的指针
	int      *addrlen	// sockaddr结构体大小
);						// 成功返回socket 失败返回INVALID_SOCKET

  成功之后,使用recv接收数据

int recv(
	SOCKET s,		// socket结构体
	char   *buf,	// 接收数据指针
	int    len,		// 接收大小
	int    flags	// 设为0即可
);					// 成功返回实际接收大小 失败返回SOCKET_ERROR

  接收方完整代码

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

#pragma comment(lib, "ws2_32.lib")
int main()
{

    WSADATA wsa;
    int ret;
    // 初始化socket
    ret = WSAStartup(MAKEWORD(2, 2), &wsa);
    if (ret)
    {
        printf("WSAStartup fail\n");
        return 0;
    }

    // 创建socket
    SOCKET sListen = socket(AF_INET, SOCK_STREAM, 0);
    if (sListen == INVALID_SOCKET)
    {
        printf("socket fail\n");
        return 0;
    }

    // 监听端口
    int port = 5899;
    // 接收数据
    SOCKADDR_IN local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    // 接收任意ip数据
    local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

    // 绑定端口
    ret = bind(sListen, (SOCKADDR*)&local, sizeof(SOCKADDR));
    if (ret == SOCKET_ERROR)
    {
        printf("bind fail\n");
        return 0;
    }

    // 监听端口
    ret = listen(sListen, 5);
    if (ret == SOCKET_ERROR)
    {
        printf("listen fail\n");
        return 0;
    }

    printf("Listening %d\n", port);

    // 建立连接
    SOCKADDR_IN client;
    int AddrSize = sizeof(SOCKADDR);
    SOCKET sClient = accept(sListen, (SOCKADDR*)&client, &AddrSize);
    if (sClient == INVALID_SOCKET)
    {
        printf("accept fail\n");
        return 0;
    }

    printf("connect from %s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));

    // 接收数据
    char buffer[1024];
    ZeroMemory(buffer, sizeof(buffer));

    ret = recv(sClient, buffer, 1024, 0);
    if (ret == 0 || ret == SOCKET_ERROR)
    {
        printf("recv fail\n");
        return 0;
    }

    printf("recv:%s\n", buffer);
    printf("recv:%d bytes", ret);

    // 收尾
    closesocket(sClient);
    closesocket(sListen);
    WSACleanup();
}

  运行顺序是:先启动接收方,再启动发送方
  发送方结果

connect to 127.0.0.1:5899
sending:hello
sending:6 bytes

  接收方结果

Listening 5899
connect from 127.0.0.1:8893
recv:hello
recv:6 bytes

命名管道

  命名管道(Named Pipes),按照字面意思理解就是有名字的管道,它可在同一台计算机的不同进程之间或在跨越一个网络的不同计算机的不同进程之间,支持可靠的、单向或双向的数据通信。

  命名管道是由服务器端的进程建立的,管道的命名必须遵循特定的命名方法。 本地访问:\\.\pipe\管道名,远程访问:\\ServerName\pipe\管道名。客户端要想连接服务端,必须知道服务端的管道名称。

服务端

  1.使用CreateNamedPipe来创建一个命名管道

HANDLE CreateNamedPipe(
	LPCSTR                lpName,				// 创建的管道名称
	DWORD                 dwOpenMode,			// 管道打开方式
	DWORD                 dwPipeMode,			// 管道模式
	DWORD                 nMaxInstances,		// 管道最大连接数 必须大于1小于255(PIPE_UNLIMITED_INSTANCES)
	DWORD                 nOutBufferSize,		// 管道输出缓冲区 0 使用默认大小
	DWORD                 nInBufferSize,		// 管道输入缓冲区 0 使用默认大小
	DWORD                 nDefaultTimeOut,		// 管道连接超时时间 0 默认超时50毫秒
	LPSECURITY_ATTRIBUTES lpSecurityAttributes  // 指向SECURITY_ATTRIBUTES指针
);												// 成功返回服务端管道句柄 失败返回INVALID_HANDLE_VALUE

  dwOpenMode 常用的有以下值,更多查阅msdn

PIPE_ACCESS_DUPLEX	 服务器和客户端都可以从管道读写数据
PIPE_ACCESS_INBOUND  客户端只能写 服务端只能读
PIPE_ACCESS_OUTBOUND 客户端只能读 服务端只能写

  dwPipeMode 管道模式,有以下常用值

写入类型二选一
PIPE_TYPE_BYTE			数据作为字节流写入管道
PIPE_TYPE_MESSAGE   	数据作为消息流写入管道
读取类型二选一
PIPE_READMODE_BYTE  	数据作为字节流读入管道
PIPE_READMODE_MESSAGE 	数据作为消息流读入管道
等待类型二选一
PIPE_WAIT				阻塞模式
PIPE_NOWAIT 			非阻塞模式
// 1.创建命名管道
HANDLE hPipe = CreateNamedPipe(
	"\\\\.\\pipe\\local",
	PIPE_ACCESS_DUPLEX,
	PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
	PIPE_UNLIMITED_INSTANCES,
	0, 0, 0, NULL
);

if (!hPipe)
{
	printf("CreateNamedPipe fail\n");
	return 0;
}

  2.使用ConnectNamedPipe,等待客户端连接命名管道

BOOL ConnectNamedPipe(
	HANDLE       hNamedPipe,	// 服务端管道句柄
	LPOVERLAPPED lpOverlapped	// 指向 OVERLAPPED 结构的指针 为NULL即可
);								// 成功非0 失败为0

  3.使用ReadFile来接收数据,或者使用WriteFile来发送数据。

BOOL ReadFile(
	HANDLE       hFile,						// 句柄
	LPVOID       lpBuffer,					// 缓冲区指针
	DWORD        nNumberOfBytesToRead,		// 读取大小
	LPDWORD      lpNumberOfBytesRead,		// 实际读取大小指针
	LPOVERLAPPED lpOverlapped				// 设为NULL
);											// 成功非0 失败为0

BOOL WriteFile(
	HANDLE       hFile,						// 句柄
	LPCVOID      lpBuffer,					// 缓冲区指针
	DWORD        nNumberOfBytesToWrite,		// 接收大小
	LPDWORD      lpNumberOfBytesWritten,	// 实际接收大小
	LPOVERLAPPED lpOverlapped				// 设为NULL
);											// 成功非0 失败为0

  4.最后使用DisconnectNamedPipe来断开与客户端的连接,必须由CreateNamedPipe创建。

  服务端 示例代码

#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>


int main()
{
	printf("waiting to connect\n");

	// 1.创建命名管道
	HANDLE hPipe = CreateNamedPipe(
		"\\\\.\\pipe\\local",
		PIPE_ACCESS_DUPLEX,
		PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
		PIPE_UNLIMITED_INSTANCES,
		0, 0, 0, NULL
	);

	if (!hPipe)
	{
		printf("CreateNamedPipe fail\n");
		return 0;
	}

	// 2.等待客户端连接
	if (!ConnectNamedPipe(hPipe, NULL))
	{
		printf("ConnectNamedPipe fail\n");
		return 0;
	}

	// 3.接收客户端数据
	char buffer[256] = { 0 };
	ZeroMemory(buffer, sizeof(buffer));

	if (!ReadFile(hPipe, buffer, sizeof(buffer), 0, NULL))
	{
		printf("ReadFile fail\n");
		return 0;
	}

	printf("recvData is %s", buffer);

	// 4.向客户端发送数据
	ZeroMemory(buffer, sizeof(buffer));
	strcpy(buffer, "hello Client");

	if (!WriteFile(hPipe,buffer,sizeof(buffer),0,NULL))
	{
		printf("WriteFile fail\n");
		return 0;
	}

	// 断开连接
	DisconnectNamedPipe(hPipe);
	CloseHandle(hPipe);
	return 0;
}
客户端

  1.客户端使用WaitNamedPipe来等待管道的出现

BOOL WaitNamedPipe(
	LPCSTR lpNamedPipeName, // 命名管道名称
    DWORD  nTimeOut			// 超时时间 单位毫秒
);							// 成功返回非0 失败返回0

  nTimeOut 有两种值

NMPWAIT_USE_DEFAULT_WAIT	服务端在CreateNamedPipe函数中指定的默认值
NMPWAIT_WAIT_FOREVER 		一直等待一个命名管道
// 1.等待管道的出现
if (!WaitNamedPipe("\\\\.\\pipe\\local",NMPWAIT_WAIT_FOREVER))
{
    printf("WaitNamedPipe fail\n");
    return 0;
}

  2.调用CreateFile来打开命名管道的一个句柄,然后在调用WriteFile发数据,或者ReadFile接收数据
  客户端 代码示例

#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    // 1.等待管道出现
    if (!WaitNamedPipe("\\\\.\\pipe\\local", NMPWAIT_WAIT_FOREVER))
    {
        printf("WaitNamedPipe fail\n");
        return 0;
    }

    printf("connect namePipe success\n");

    // 2.打开命名管道句柄
    HANDLE hPipe = CreateFile("\\\\.\\pipe\\local", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (!hPipe)
    {
        printf("CreateFile fail\n");
        return 0;
    }

    // 3.向服务端发送数据
    char buffer[256] = "hello Server";

    if (!WriteFile(hPipe, buffer, sizeof(buffer), 0, NULL))
    {
        printf("WriteFile fail\n");
        return 0;
    }


    // 4.从服务端接收数据
    ZeroMemory(buffer, sizeof(buffer));
    if (!ReadFile(hPipe, buffer, sizeof(buffer), 0, NULL))
    {
        printf("ReadFile fail\n");
        return 0;
    }

    printf("recvData is %s\n", buffer);
    CloseHandle(hPipe);
	return 0;
}

  运行结果

服务端
waiting to connect
recvData is hello Server

客户端
connect namePipe success
recvData is hello Client

http

  http即超文本传输协议,是基于tcp/ip的通信协议。winhttp提供了http协议的实现。微软给出了工作流程图如下
  
  需包含头文件winhttp.h,和链接库winhttp.lib

初始化

  在与服务器交互之前,必须调用WinHttpOpen进行初始化。WinHttpOpen创建一个会话,并返回该会话的句柄

HINTERNET WinHttpOpen(
	LPCWSTR pszAgentW,			// user agent 可传NULL
	DWORD   dwAccessType,		// 设置代理 WINHTTP_ACCESS_TYPE_NO_PROXY 表示不用代理
	LPCWSTR pszProxyW,			// 代理名称 WINHTTP_NO_PROXY_NAME 表示无代理
	LPCWSTR pszProxyBypassW,	// 设置成WINHTTP_NO_PROXY_BYPASS即可
	DWORD   dwFlags				// 设置成0即可
);								// 成功返回 HINTERNET 失败返回 NULL

  使用WinHttpCloseHandle关闭句柄

#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include <winhttp.h>
#pragma comment(lib, "winhttp.lib")
#define HTTP_USER_AGENT L"Mozilla/5.02 (Linux; Android 4.1.1; Nexus 7 Build/JRO03D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Safari"

int main()
{
	HINTERNET hSession = NULL, hConnect = NULL, hRequest = NULL;

	// 获得一个会话句柄
	hSession = WinHttpOpen(HTTP_USER_AGENT, WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
	if (!hSession) return 0;
	
	printf("win http open ok");
	WinHttpCloseHandle(hSession);
	return 0;
}
指定服务器

  当成功获得会话句柄后,就可以使用WinHttpConnect请求指定的目标服务器

HINTERNET WinHttpConnect(
	HINTERNET     hSession,			// 由WinHttpOpen返回的句柄
	LPCWSTR       pswzServerName,	// 域名或ip
	INTERNET_PORT nServerPort,		// 端口号 INTERNET_DEFAULT_PORT 自动根据协议使用默认端口号
	DWORD         dwReserved		// 保留参数 必须为0
);									// 成功返回 HINTERNET 失败返回 NULL

  这里我的目标网址是: http://www.weather.com.cn/data/sk/101010100.html

// 指定HTTP服务器
hConnect = WinHttpConnect(hSession, L"www.weather.com.cn", INTERNET_DEFAULT_PORT, 0);
创建请求

  继续使用WinHttpOpenReques来创建一个请求

HINTERNET WinHttpOpenRequest(
	HINTERNET hConnect,				// 由WinHttpConnect返回的句柄
	LPCWSTR   pwszVerb,				// 请求方式 值为GET POST NULL=GET 值必须大写
	LPCWSTR   pwszObjectName,		// 目标路径 可为NULL
	LPCWSTR   pwszVersion,			// HTTP版本 设为NULL即可
	LPCWSTR   pwszReferrer,			// 设为WINHTTP_NO_REFERER即可
	LPCWSTR   *ppwszAcceptTypes,	// 接受媒体类型 设为WINHTTP_DEFAULT_ACCEPT_TYPES即可
	DWORD     dwFlags				// internet标志 设为WINHTTP_FLAG_ESCAPE_DISABLE即可
);									// 成功返回 HINTERNET 失败返回 NULL
// 创建HTTP请求连接
hRequest = WinHttpOpenRequest(hConnect, L"GET", L"/data/sk/101010100.html", NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_ESCAPE_DISABLE);
发送请求

  使用WinHttpSendRequest发送请求

BOOL WinHttpSendRequest(
	HINTERNET hRequest,				// 由WinHttpOpenRequest返回的句柄
	LPCWSTR   lpszHeaders,			// 附加头 WINHTTP_NO_ADDITIONAL_HEADERS 表示没有附加头
	DWORD     dwHeadersLength,		// 附加头长度
	LPVOID    lpOptional,			// 发送的POST或者PUT数据
	DWORD     dwOptionalLength,		// 发送的数据长度
	DWORD     dwTotalLength,		// 发送的总数据长度
	DWORD_PTR dwContext				// 设置0即可
);									// 成功返回 TRUE 失败返回 FALSE
// 向服务器发送请求
BOOL bResults = WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, NULL, 0, 0, 0);
接收响应

  使用WinHttpReceiveResponse接收服务器返回的响应

BOOL WinHttpReceiveResponse(
	HINTERNET hRequest,			// 由WinHttpOpenRequest返回的句柄
	LPVOID    lpReserved		// 保留参数 必须为NULL
);								// 成功返回 TRUE 失败返回 FALSE
// 接收服务器的响应
bResults = WinHttpReceiveResponse(hRequest, NULL);
获取数据

  当成功接收到了响应后,就可以使用一些api来获取请求的信息了,首先使用WinHttpQueryDataAvailable来获取返回的数据大小。

BOOL WinHttpQueryDataAvailable(
	HINTERNET hRequest,						// 由WinHttpOpenRequest返回的句柄
	LPDWORD   lpdwNumberOfBytesAvailable	// 返回数据大小的变量指针
);											// 成功返回 TRUE 失败返回 FALSE

  之后再读取使用WinHttpReadData来读取数据

BOOL WinHttpReadData(
	HINTERNET hRequest,					// 由WinHttpOpenRequest返回的句柄
    LPVOID    lpBuffer,					// 返回读取数据的指针
	DWORD     dwNumberOfBytesToRead,	// 读取数据的大小
	LPDWORD   lpdwNumberOfBytesRead		// 返回成功读取数据的指针	
);

  因为返回的数据不是一次性返回完毕的,是边请求边返回给我们数据,所以要放进循环里面接收返回的数据

DWORD dwSize = 0;
DWORD dwDownloaded = 0;
do
{
    dwSize = 0;
    // 获取返回的数据的大小
    if (!WinHttpQueryDataAvailable(hRequest, &dwSize) || dwSize == 0)
        break;

    // 根据返回数据的长度为buffer申请内存空间
    char* buffer = (char*)malloc(sizeof(char) * (dwSize + 1));
    if (!buffer)	break;

    ZeroMemory(buffer, dwSize + 1);	// 这行非常关键,一定要写上这行不然会出现数据末尾乱码
    // 读取服务器返回的数据
    if (!WinHttpReadData(hRequest, buffer, dwSize, &dwDownloaded))
        break;

    printf("%s", buffer);
    free(buffer);

} while (dwSize > 0);

  运行结果

{"weatherinfo":{"city":"鍖椾含","cityid":"101010100","temp":"27.9","WD":"鍗楅","WS":"灏忎簬3绾?,"SD":"28%","AP":"1002hPa","njd":"鏆傛棤瀹炲喌","WSE":"<3","time":"17:55","sm":"2.1","isRadar":"1","Radar":"JC_RADAR_AZ9010_JB"}}

  可看到上面的数据是乱码的,关于编码这块不是我写的重点,下面的完整代码解决了乱码

完整代码
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include <winhttp.h>
#pragma comment(lib, "winhttp.lib")
#define HTTP_USER_AGENT L"Mozilla/5.02 (Linux; Android 4.1.1; Nexus 7 Build/JRO03D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Safari"

/*
 * 参考文章
 * https://blog.csdn.net/weixin_47232366/article/details/123555611
 * https://blog.csdn.net/qq_28743877/article/details/53439350
 * http://www.manongjc.com/detail/24-einfadjuvdgktsa.html
 */

char* UnicodeToANSI(wchar_t* str)
{
	int len = WideCharToMultiByte(CP_ACP, 0, str, -1, NULL, 0, NULL, NULL);
	char* result = (char*)malloc((len + 1) * sizeof(char));
	ZeroMemory(result, len + 1);
	WideCharToMultiByte(CP_ACP, 0, str, -1, result, len, NULL, NULL);
	return result;
}

WCHAR* UTF8ToUnicode(char* buffer)
{
	int len = MultiByteToWideChar(CP_UTF8, 0, buffer, -1, NULL, 0);
	WCHAR* unicode = (WCHAR*)malloc(sizeof(WCHAR) * (len + 1));
	ZeroMemory(unicode, sizeof(WCHAR) * (len + 1));
	MultiByteToWideChar(CP_UTF8, 0, buffer, -1, unicode, len);
	return unicode;
}
int main()
{
	BOOL bResults = FALSE;
	HINTERNET hSession = NULL, hConnect = NULL, hRequest = NULL;

	// 获得一个会话句柄
	hSession = WinHttpOpen(HTTP_USER_AGENT, WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);

	// 指定HTTP服务器
	if (hSession)
		hConnect = WinHttpConnect(hSession, L"www.weather.com.cn", INTERNET_DEFAULT_PORT, 0);

	// 创建HTTP请求连接
	if (hConnect)
		hRequest = WinHttpOpenRequest(hConnect, L"GET", L"/data/sk/101010100.html", NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_ESCAPE_DISABLE);

	// 向服务器发送请求
	bResults = WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, NULL, 0, 0, 0);

	// 接收服务器的响应
	if (bResults)
		bResults = WinHttpReceiveResponse(hRequest, NULL);

	if (!bResults)
		return 0;

	DWORD dwSize = 0;
	DWORD dwDownloaded = 0;
	do
	{
		dwSize = 0;
		// 获取返回的数据的大小
		if (!WinHttpQueryDataAvailable(hRequest, &dwSize) || dwSize == 0)
			break;

		// 根据返回数据的长度为buffer申请内存空间
		char* buffer = (char*)malloc(sizeof(char) * (dwSize + 1));
		if (!buffer)	break;
			
		ZeroMemory(buffer, dwSize + 1);	// 这行非常关键,一定要写上这行不然会出现数据末尾乱码
		// 读取服务器返回的数据
		if (!WinHttpReadData(hRequest, buffer, dwSize, &dwDownloaded))
			break;

		printf("编码前\n");
		printf("%s\n\n", buffer);

		printf("编码后\n");
		WCHAR* unicode = UTF8ToUnicode(buffer);
		char* str = UnicodeToANSI(unicode);
		printf("%s", str);

		free(unicode);
		free(str);
		free(buffer);

	} while (dwSize > 0);

	// 关闭所有句柄
	if (hRequest) WinHttpCloseHandle(hRequest);
	if (hConnect) WinHttpCloseHandle(hConnect);
	if (hSession) WinHttpCloseHandle(hSession);

	return 0;
}

  运行结果

编码前
{"weatherinfo":{"city":"鍖椾含","cityid":"101010100","temp":"27.9","WD":"鍗楅","WS":"灏忎簬3绾?,"SD":"28%","AP":"1002hPa","njd":"鏆傛棤瀹炲喌","WSE":"<3","time":"17:55","sm":"2.1","isRadar":"1","Radar":"JC_RADAR_AZ9010_JB"}}

编码后
{"weatherinfo":{"city":"北京","cityid":"101010100","temp":"27.9","WD":"南风","WS":"小于3级","SD":"28%","AP":"1002hPa","njd":"暂无实况","WSE":"<3","time":"17:55","sm":"2.1","isRadar":"1","Radar":"JC_RADAR_AZ9010_JB"}}

注册表

  注册表是Windows中的一个重要的数据库,用于存储系统和应用程序的设置信息。

  注册表由主键、子键、键值构造,主键就是根,有以下主键

  HKEY_CLASSES_ROOTHKEY_CURRENT_CONFIGHKEY_CURRENT_USERHKEY_LOCAL_MACHINEHKEY_USERS

  而子键就是主键下的键,类似于文件夹的结构,例如HKEY_LOCAL_MACHINE\SOFTWARESOFTWARE就是子键。

  键值是子键下的具体内容,由名称、类型、数据组成

子键

打开

  打开注册表有两个api:RegOpenKeyRegOpenKeyEx。微软推荐使用后缀带Ex的api,因为RegOpenKey是以前16位机用的,Ex版本更灵活和强大。

LSTATUS RegOpenKeyEx(
	HKEY   hKey,		// 注册表句柄 或 主键
	LPCSTR lpSubKey,	// 注册表子键
	DWORD  ulOptions,	// 保留 必须是0
	REGSAM samDesired,	// 访问权限
	PHKEY  phkResult	// 返回打开的注册表句柄
);						// 成功返回 ERROR_SUCCESS 失败返回 非0错误码

  samDesired访问权限,这里列出常用的值,

KEY_ALL_ACCESS				// 获取所有权限
KEY_CREATE_SUB_KEY			// 创建子键
KEY_ENUMERATE_SUB_KEYS		// 枚举子键
KEY_EXECUTE					// 允许读操作
KEY_QUERY_VALUE				// 查询子键
KEY_READ					// STANDARD_RIGHTS_READ、KEY_QUERY_VALUE、KEY_ENUMERATE_SUB_KEYS、KEY_NOTIFY值的组合
KEY_SET_VALUE				// 创建、删除或设置子键
KEY_WRITE					// STANDARD_RIGHTS_WRITE、KEY_SET_VALUE、KEY_CREATE_SUB_KEY值的组合
KEY_WOW64_32KEY 			// 访问32位注册表
KEY_WOW64_64KEY				// 访问64位注册表

  关于KEY_WOW64_32KEYKEY_WOW64_64KEY,我建议你单独去了解了解。有些时候创建成功或者打开失败,都有可能和这个有关。

  关闭注册表句柄使用RegCloseKey函数

  例子,注意键名不区分大小写

HKEY hKey;
// 键名不区分大小写
// 打开HKEY_LOCAL_MACHINE\SOFTWARE
long ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SoFTwARE", 0, KEY_ALL_ACCESS, &hKey);
if (ret == ERROR_SUCCESS)
{
    printf("open key success\n");
    RegCloseKey(hKey);
}
创建

  RegCreateKeyEx,创建指定的注册表项。如果键已经存在,函数将打开它

LSTATUS RegCreateKeyEx(
	HKEY                        hKey,				// 注册表句柄 或 主键
	LPCSTR                      lpSubKey,			// 创建子键的名称
	DWORD                       Reserved,			// 保留 必须为0
	LPSTR                       lpClass,			// 忽略 填NULL
	DWORD                       dwOptions,			// 填0 具体参考msdn
	REGSAM                      samDesired,			// 访问权限
	LPSECURITY_ATTRIBUTES lpSecurityAttributes,		// 指向SECURITY_ATTRIBUTES结构的指针
	PHKEY                       phkResult,			// 返回打开或创建注册表句柄
	LPDWORD                     lpdwDisposition		// 接收处理后的结果 可以置NULL
);													// 成功返回 ERROR_SUCCESS 失败返回 非0错误码

  lpdwDisposition 有以下值

REG_CREATED_NEW_KEY 		// 不存在子键则创建
REG_OPENED_EXISTING_KEY		// 子键已经存在则打开

  这里演示两种创建子键的方法,一种是基于已经打开的注册表句柄创建,一种是直接创建

#include <Windows.h>
#include <stdlib.h>
#include <stdio.h>

// 基于打开的注册表句柄创建
void method1()
{
    HKEY hKey;

    long ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, L"SOftWAre", 0, KEY_ALL_ACCESS, &hKey);
    if (ret != ERROR_SUCCESS)
        return 0;

    printf("open key success\n");

    DWORD result = 0;
    
    // 可一次性创建多级子键
    // 创建HKEY_LOCAL_MACHINE\SOFTWARE\aaa\b\c\d
    ret = RegCreateKeyEx(hKey, L"aaa\\b\\c\\d", 0, NULL, 0, KEY_CREATE_SUB_KEY, NULL, &hKey, &result);
    if (ret != ERROR_SUCCESS)
        return 0;

    // 成功创建子键
    if (result == REG_CREATED_NEW_KEY)
    {
        printf("create new key\n");
    }

    // 子键已经存在
    if (result == REG_OPENED_EXISTING_KEY)
    {
        printf("key is existing\n");
    }

    RegCloseKey(hKey);
}

// 直接创建子键
void method2()
{
    HKEY hKey;
    // 创建HKEY_LOCAL_MACHINE\SOFTWARE\aaa\b\c\d
    long ret = RegCreateKeyEx(HKEY_LOCAL_MACHINE, L"SOFTWARE\\aaa\\b\\c\\d", 0, NULL, 0, KEY_CREATE_SUB_KEY, NULL, &hKey, NULL);
    if (ret == ERROR_SUCCESS)
    {
        printf("RegCreateKeyEx success\n");;
        RegCloseKey(hKey);
    }
}

int main() 
{
    // 直接创建子键
    method2();
    // 基于打开的注册表句柄创建
    method1();
    return 0;
}

  运行结果

RegCreateKeyEx success
open key success
key is existing
删除

  RegDeleteKeyEx,只能删除一个非空子键

LSTATUS RegDeleteKeyEx(
	HKEY   hKey,			// 注册表句柄 或 主键
	LPCSTR lpSubKey,		// 要删除的子键
	REGSAM samDesired,		// 访问掩码
	DWORD  Reserved			// 保留置0
);							// 成功返回 ERROR_SUCCESS 失败返回 非0错误码

  samDesired,有以下两种值

KEY_WOW64_32KEY		从 32 位注册表中删除该项
KEY_WOW64_64KEY		从 64 位注册表中删除该项

  示例

// 删除HKEY_LOCAL_MACHINE\SOFTWARE\aaa\b\c\d
long ret = RegDeleteKeyEx(HKEY_LOCAL_MACHINE, L"SOFTWARE\\aaa\\b\\c\\d", KEY_WOW64_64KEY, 0);
if (ret == ERROR_SUCCESS)
    printf("delete key success\n");

  如果要递归删除,用RegDeleteTree
  示例

// 删除HKEY_LOCAL_MACHINE\SOFTWARE\aaa 以及aaa下的所有子键
RegDeleteTree(HKEY_LOCAL_MACHINE, L"SOFTWARE\\aaa");

键值

查询

  获取键值中的某个值的数据,可以使用RegQueryValueEx

LSTATUS RegQueryValueEx(
	HKEY    hKey,			// 注册表句柄 或 主键
	LPCSTR  lpValueName,	// 键值名称
	LPDWORD lpReserved,		// 保留置NULL
	LPDWORD lpType,			// 键值类型 可置NULL
	LPBYTE  lpData,			// 接收数据缓冲区指针 可置NULL
	LPDWORD lpcbData		// 缓冲区指针大小
);							// 成功返回 ERROR_SUCCESS 失败返回 非0错误码

  当lpData为NULL时,lpcbData返回实际获取到的缓冲区大小
  示例

#include <Windows.h>
#include <stdlib.h>
#include <stdio.h>

int main() 
{
    HKEY hKey;
    TCHAR subKey[] = TEXT("HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0");
    long ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, subKey, 0, KEY_QUERY_VALUE | KEY_WOW64_64KEY, &hKey);
    if (ret != ERROR_SUCCESS)
    {
        printf("RegOpenKeyEx fail\n");
        return 0;
    }
       
    DWORD size = 0;
    DWORD type = 0;
    // 第一次获取缓冲区大小
    ret = RegQueryValueEx(hKey, L"ProcessorNameString", NULL, &type, NULL, &size);
    if (ret != ERROR_SUCCESS)
        return 0;
    
    if (type == REG_SZ)
        printf("type is string\n");

    // 动态分配空间
    TCHAR* data = (TCHAR*)malloc(sizeof(TCHAR) * size + 5);

    ret = RegQueryValueEx(hKey, L"ProcessorNameString", NULL, NULL, data, &size);
    if (ret != ERROR_SUCCESS)
        return 0;

    wprintf(L"cpu is %s\n", data);
    free(data);
    RegCloseKey(hKey);
    return 0;
}

  运行结果

type is string
cpu is AMD Ryzen 5 2600 Six-Core Processor
增加&修改

  RegSetValueEx,可以修改键值的值,如果此键值不存在,则创建此键值

LSTATUS RegSetValueEx(
	HKEY       hKey,			// 注册表句柄
	LPCSTR     lpValueName,		// 键值名称
	DWORD      Reserved,		// 保留必须是0
	DWORD      dwType,			// 键值类型
	const BYTE *lpData,			// 写入的缓冲区指针
	DWORD      cbData			// 缓冲区大小
);								// 成功返回 ERROR_SUCCESS 失败返回 非0错误码

  经测试,使用RegSetValueExA,写入的数据才不会乱码
  示例

#include <Windows.h>
#include <stdlib.h>
#include <stdio.h>

int main() 
{
    // 创建HKEY_LOCAL_MACHINE\SOFTWARE\aaa 子键
    HKEY hKey;
    long ret = RegCreateKeyEx(HKEY_LOCAL_MACHINE, L"SOFTWARE\\aaa", 0, NULL, 0, KEY_ALL_ACCESS | KEY_WOW64_64KEY, NULL, &hKey, NULL);
    if (ret != ERROR_SUCCESS)
    {
        printf("RegCreateKeyEx fail\n");
        return 0;
    }

    // 创建test键值 内容为this is demo 这是个示例
    BYTE data[] = "this is demo 这是个示例";
    ret = RegSetValueExA(hKey, "test", 0, REG_SZ, (BYTE*)data, sizeof(data));
    if (ret == ERROR_SUCCESS)
        printf("sucess\n");

    RegCloseKey(hKey);
    return 0;
}
删除

  删除键值非常简单,只需要调用RegDeleteValue
  示例

// 创建HKEY_LOCAL_MACHINE\SOFTWARE\aaa 子键
HKEY hKey;
RegCreateKeyEx(HKEY_LOCAL_MACHINE, L"SOFTWARE\\aaa", 0, NULL, 0, KEY_ALL_ACCESS | KEY_WOW64_64KEY, NULL, &hKey, NULL);

// 创建test键值
BYTE data[] = "this is demo 这是个示例";
RegSetValueExA(hKey, "test", 0, REG_SZ, (BYTE*)data, sizeof(data));

// 删除test键值
RegDeleteValue(hKey, L"test");
RegCloseKey(hKey);
枚举

  获取子键下的所有数据需要两个api,第一个RegQueryInfoKey,获取注册表项的相关信息

LSTATUS RegQueryInfoKey(
	HKEY      hKey,						// 注册表句柄
	LPSTR     lpClass,					// 可为NULL
 	LPDWORD   lpcchClass,				// 可为NULL
	LPDWORD   lpReserved,				// 必须为NULL
	LPDWORD   lpcSubKeys,				// 子键数量 可为NULL
	LPDWORD   lpcbMaxSubKeyLen,			// 可为NULL
	LPDWORD   lpcbMaxClassLen,			// 可为NULL
	LPDWORD   lpcValues,				// 键值数量 可为NULL
	LPDWORD   lpcbMaxValueNameLen,		// 键名最大大小 可为NULL
	LPDWORD   lpcbMaxValueLen,			// 键值最大大小 可为NULL
	LPDWORD   lpcbSecurityDescriptor,	// 可为NULL
	PFILETIME lpftLastWriteTime			// 可为NULL
);										// 成功返回 ERROR_SUCCESS 失败返回 非0错误码

  第二个RegEnumValue,枚举键值信息

LSTATUS RegEnumValueA(
	HKEY    hKey,				// 注册表句柄
	DWORD   dwIndex,			// 索引
	LPSTR   lpValueName,		// 键名缓冲区指针
	LPDWORD lpcchValueName,		// 键名缓冲区大小
	LPDWORD lpReserved,			// 保留 必须为NULL
	LPDWORD lpType,				// 键值类型 可为NULL
	LPBYTE  lpData,				// 键值缓冲区指针 可为NULL
	LPDWORD lpcbData			// 键值缓冲区大小 可为NULL
);								// 成功返回 ERROR_SUCCESS 失败返回 非0错误码

  示例

#include <Windows.h>
#include <stdlib.h>
#include <stdio.h>

int main() 
{
    HKEY hKey;
    TCHAR subKey[] = TEXT("HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0");
    RegOpenKeyEx(HKEY_LOCAL_MACHINE, subKey, 0, KEY_QUERY_VALUE | KEY_WOW64_64KEY, &hKey);

    DWORD cnt, maxNameLen, maxValueLen;
    // 查询键值信息
    RegQueryInfoKey(hKey, NULL, NULL, NULL, NULL, NULL, NULL, &cnt, &maxNameLen, &maxValueLen, NULL, NULL);
    printf("一共有%d个键\n最大键名:%d个字符\n最大键值:%d个字符\n", cnt, maxNameLen, maxValueLen);

    TCHAR* name = (TCHAR*)malloc(sizeof(TCHAR) * maxNameLen + 5);
    printf("\nHKEY_LOCAL_MACHINE\\HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0 有以下键名\n\n");

    // 循环遍历
    for (DWORD i = 0; i < cnt; i++)
    {
        ZeroMemory(name, maxNameLen);   // 内存置0
        // 必须+1 多个'\0'的空间
        DWORD nameSize = maxNameLen + 1;
        RegEnumValue(hKey, i, name, &nameSize, NULL, NULL, NULL, NULL);
        wprintf(L"%s\n", name);
    }
    
    free(name);
    RegCloseKey(hKey);

    return 0;
}

  运行结果

一共有11个键
最大键名:24个字符
最大键值:96个字符

HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\CentralProcessor\0 有以下键名

Component Information
Identifier
Configuration Data
ProcessorNameString
VendorIdentifier
FeatureSet
~MHz
Update Revision
Update Status
Previous Update Revision
Platform Specific Field1

系统服务

  服务程序是windows上重要的一类程序,它们虽然不与用户进行界面交互,但是它们对于系统有着重要的意义。windows上为了管理服务程序提供了一个特别的程序:服务控制管理程序,系统上关于服务控制管理的API基本上都与这个程序打交道。下面通过对服务程序的操作来说明这些API函数

打开

  必须先建立一个到服务控制管理器的连接,即打开一个数据库。使用OpenSCManager

SC_HANDLE OpenSCManagerW(
	LPCWSTR lpMachineName,	// 主机名 NULL表示本机
	LPCWSTR lpDatabaseName,	// 数据库名 设置
	DWORD   dwDesiredAccess	// 访问权限 
);							// 成功返回 操作数据库句柄 失败返回 NULL

  dwDesiredAccess,常用值,更多值查询MSDN

SC_MANAGER_ALL_ACCESS			拥有所有权限
SC_MANAGER_CREATE_SERVICE		创建服务权限
SC_MANAGER_ENUMERATE_SERVICE	枚举服务权限
SERVICE_QUERY_CONFIG 			查询服务配置权限
SERVICE_START					启动服务权限
SERVICE_STOP					停止服务权限

  关闭服务句柄,使用CloseServiceHandle
  示例

#include <Windows.h>
#include <stdlib.h>
#include <stdio.h>

int main()
{
	SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
	if (!hSCM) return;

	printf("OpenSCManager sucess");
	CloseServiceHandle(hSCM);
    
	return 0;
}

枚举

  枚举系统服务可以使用EnumServicesStatus,或者EnumServicesStatusEx

BOOL EnumServicesStatus(
	SC_HANDLE              hSCManager,			// 数据库句柄
	DWORD                  dwServiceType,		// 服务类型 SERVICE_WIN32(win32类型服务)
	DWORD                  dwServiceState,		// 服务状态
	LPENUM_SERVICE_STATUSW lpServices,			// 指向ENUM_SERVICE_STATUS结构体指针
	DWORD                  cbBufSize,			// 结构体缓冲区大小
	LPDWORD                pcbBytesNeeded,		// 实际需要缓冲区大小
	LPDWORD                lpServicesReturned,	// 服务个数
	LPDWORD                lpResumeHandle		// 额外的句柄
);

  dwServiceType,服务类型,主要有两种

SERVICE_DRIVER	驱动类型服务
SERVICE_WIN32	win32类型服务

  dwServiceState,服务状态,有以下值

SERVICE_ACTIVE		已启动的服务
SERVICE_INACTIVE	未启动的服务
SERVICE_STATE_ALL	所有服务

  ENUM_SERVICE_STATUS结构体定义

typedef struct _ENUM_SERVICE_STATUSA {
  LPSTR          lpServiceName;		// 服务名称
  LPSTR          lpDisplayName;		// 显示名称
  SERVICE_STATUS ServiceStatus;		// 指向SERVICE_STATUS结构体 下文讲解
} ENUM_SERVICE_STATUSA, *LPENUM_SERVICE_STATUSA;

  下图说明了服务名称和显示名称的关系,服务名称可能不等于显示名称。
  
  因为我们并不知道有多少个服务,也就无法申请一个合适的结构体大小。所以应该先调用EnumServicesStatus,来获取缓冲区的大小,注意首次调用EnumServicesStatuslpResumeHandle 必须为0
  示例代码

// 打开数据库
SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!hSCM) return;

DWORD needSize = 0, serviceCnt = 0;
	
// 第一次调用获取缓冲区空间
BOOL ret = EnumServicesStatus(hSCM, SERVICE_WIN32, SERVICE_STATE_ALL, NULL, 0, &needSize, &serviceCnt, 0);
// 第一次调用返回0
if (ret) return;

LPENUM_SERVICE_STATUS servStatus;
// 动态分配空间
servStatus = (LPENUM_SERVICE_STATUS)malloc(sizeof(LPENUM_SERVICE_STATUS) * (needSize + 1));
if (!servStatus) return;

ret = EnumServicesStatus(hSCM, SERVICE_WIN32, SERVICE_STATE_ALL, servStatus, needSize, &needSize, &serviceCnt, NULL);
if (!ret) return;

// 循环显示服务信息
for (DWORD i = 0; i < serviceCnt; i++)
{
	printf("%s | %s\n", servStatus[i].lpDisplayName, servStatus[i].lpServiceName);
}

CloseServiceHandle(hSCM);
free(servStatus);

  运行结果

Acunetix | Acunetix
Acunetix Database | Acunetix Database
AllJoyn Router Service | AJRouter
Application Layer Gateway Service | ALG
Alibaba PC Safe Service | AlibabaProtect
... ...

查询

  查询服务信息之前,我们需要使用OpenService获取服务的句柄

SC_HANDLE OpenService(
	SC_HANDLE hSCManager,		// 数据库句柄
	LPCSTR    lpServiceName,	// 服务名称
	DWORD     dwDesiredAccess	// 访问权限
);								// 成功返回 服务句柄 失败返回 NULL

  常用访问权限,更多查阅msdn

SERVICE_ALL_ACCESS 		所有权限
SERVICE_QUERY_CONFIG 	查询配置权限
SERVICE_QUERY_STATUS 	查询状态权限
SERVICE_START 			启动服务权限
SERVICE_STOP 			停止服务权限
SERVICE_CHANGE_CONFIG 	改变配置权限
服务配置

  当成功拿到服务句柄后,可使用QueryServiceConfig查询服务配置信息,如果想获取更多的信息可使用QueryServiceConfig2

BOOL QueryServiceConfig(
	SC_HANDLE               hService,			// 服务句柄
	LPQUERY_SERVICE_CONFIGA lpServiceConfig,	// 指向QUERY_SERVICE_CONFIG结构体
	DWORD                   cbBufSize,			// 结构体缓冲区大小
	LPDWORD                 pcbBytesNeeded		// 实际需要缓冲区大小
);												// 成功返回 非0 失败返回 0

  QUERY_SERVICE_CONFIG结构体定义

typedef struct _QUERY_SERVICE_CONFIG {
  DWORD dwServiceType;		// 服务类型
  DWORD dwStartType;		// 启动类型
  DWORD dwErrorControl;		
  LPSTR lpBinaryPathName;	// 可执行文件路径
  LPSTR lpLoadOrderGroup;	
  DWORD dwTagId;
  LPSTR lpDependencies;		// 依赖项
  LPSTR lpServiceStartName;
  LPSTR lpDisplayName;		// 显示名称
} QUERY_SERVICE_CONFIG, *LPQUERY_SERVICE_CONFIG;

  示例

// 这里是服务名称 不是显示名称
LPCSTR servName = "NaturalAuthentication";
	
// 打开数据库
SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!hSCM) return;

// 获取服务句柄
SC_HANDLE hSvc = OpenService(hSCM, servName, SERVICE_ALL_ACCESS);
if (!hSvc) return;

DWORD needSize = 0;
// 获取缓冲区大小
BOOL ret = QueryServiceConfig(hSvc, NULL, 0, &needSize);
if (ret)	return;

LPQUERY_SERVICE_CONFIG pSrvConfig = NULL;
pSrvConfig = (LPQUERY_SERVICE_CONFIG)malloc(sizeof(LPQUERY_SERVICE_CONFIG) * needSize);
if (!pSrvConfig) return;

ret = QueryServiceConfig(hSvc, pSrvConfig, needSize, &needSize);
if (!ret) return;

printf("可执行文件路径: %s\n", pSrvConfig->lpBinaryPathName);
	
printf("启动类型: ");
DWORD startType = pSrvConfig->dwStartType;
if (startType == SERVICE_AUTO_START)
{
	printf("自动");
}
else if (startType == SERVICE_DEMAND_START)
{
	printf("手动");
}
else if (startType == SERVICE_DISABLED)
{
	printf("禁用");
}

printf("\n显示名称: %s", pSrvConfig->lpDisplayName);

free(pSrvConfig);
CloseServiceHandle(hSCM);
CloseServiceHandle(hSvc);

  运行结果

可执行文件路径: C:\windows\system32\svchost.exe -k netsvcs -p
启动类型: 手动
显示名称: 自然身份验证
服务状态

  使用QueryServiceStatus,或者QueryServiceStatusEx获取服务状态

BOOL QueryServiceStatus(
	SC_HANDLE        hService,			// 服务句柄
	LPSERVICE_STATUS lpServiceStatus	// 指向SERVICE_STATUS结构体
);										// 成功返回 非0 失败返回 0

  SERVICE_STATUS 结构体定义

typedef struct _SERVICE_STATUS {
  DWORD dwServiceType;				// 服务类型
  DWORD dwCurrentState;				// 当前服务状态
  DWORD dwControlsAccepted;			// 允许的操作
  DWORD dwWin32ExitCode;
  DWORD dwServiceSpecificExitCode;
  DWORD dwCheckPoint;
  DWORD dwWaitHint;
} SERVICE_STATUS, *LPSERVICE_STATUS;

  dwCurrentState,服务状态常用值

SERVICE_STOPPED			已停止
SERVICE_RUNNING 		正在运行
SERVICE_PAUSED			已暂停

  示例代码

LPCSTR servName = "tzautoupdate";
SERVICE_STATUS sTs = { 0 };

SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!hSCM) return;

SC_HANDLE hSvc = OpenService(hSCM, servName, SERVICE_QUERY_STATUS);
if (!hSvc) return;

BOOL ret = QueryServiceStatus(hSvc, &sTs);
if (!ret) return;

DWORD status = sTs.dwCurrentState;
if (status == SERVICE_RUNNING)
{
	printf("正在运行");
}
else if (status == SERVICE_STOPPED)
{
	printf("已停止");
}

修改

服务配置

  修改服务配置用到ChangeServiceConfig函数,要修改更多配置参数请用ChangeServiceConfig2

BOOL ChangeServiceConfig(
	SC_HANDLE hService,				// 服务句柄
	DWORD     dwServiceType,		// 服务类型
	DWORD     dwStartType,			// 启动类型
	DWORD     dwErrorControl,
	LPCWSTR   lpBinaryPathName,		// 可执行文件路径
	LPCWSTR   lpLoadOrderGroup,
	LPDWORD   lpdwTagId,
	LPCWSTR   lpDependencies,		// 依赖项
	LPCWSTR   lpServiceStartName,
	LPCWSTR   lpPassword,
	LPCWSTR   lpDisplayName			// 显示名称
);									// 成功返回 非0 失败返回 0

  函数中传递的都是服务的新信息,如果希望改变则填入相应的值,如果不想改变则对于DWORD类型的成员来说填入SERVICE_NO_CHANGE,对于指针类型的只需要填入NULL即可。
  示例代码

// 改变启动类型为手动
DWORD newType = SERVICE_DEMAND_START;
LPCSTR servName = "AntiCheatExpert Service";

SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!hSCM) return;

SC_HANDLE hSvc = OpenService(hSCM, servName, SERVICE_ALL_ACCESS);
if (!hSvc) return;

BOOL ret = ChangeServiceConfig(hSvc, SERVICE_NO_CHANGE, newType, SERVICE_NO_CHANGE, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
if (ret)
	printf("sucess");
else
	printf("fail");

CloseServiceHandle(hSvc);
CloseServiceHandle(hSCM);
服务状态

  启动服务StartService

BOOL StartService(
	SC_HANDLE hService,				// 服务句柄
	DWORD     dwNumServiceArgs,		// 启动参数的个数
	LPCSTR    *lpServiceArgVectors	// 参数列表指针
);

  这个函数类型与main函数外部传参,传递命令行参数给程序,以便实现程序与用户的交互。当第二个参数为0时,第三个参数为NULL

  要修改成其他状态,要用ControlService

BOOL ControlService(
	SC_HANDLE        hService,			// 服务句柄
	DWORD            dwControl,			// 新状态类型
	LPSERVICE_STATUS lpServiceStatus	// 原始状态
);

  示例

LPCSTR servName = "NaturalAuthentication";
// 启动服务
DWORD newStatus = SERVICE_RUNNING;
SERVICE_STATUS sTs = { 0 };

SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!hSCM) return;

SC_HANDLE hSvc = OpenService(hSCM, servName, SERVICE_ALL_ACCESS);
if (!hSvc) return;
	
BOOL ret = QueryServiceStatus(hSvc, &sTs);
if (!ret) return;

	
ret = FALSE;
// 相同状态
if (sTs.dwCurrentState == newStatus)
{
	ret = TRUE;
}
// 运行服务
else if (sTs.dwCurrentState == SERVICE_STOPPED && newStatus == SERVICE_RUNNING)
{	
	ret = StartService(hSvc, 0, NULL); 	 // 启动服务
}
// 停止服务
else if ((sTs.dwCurrentState == SERVICE_RUNNING || sTs.dwCurrentState == SERVICE_PAUSED) && newStatus == SERVICE_STOPPED)
{
	ret = ControlService(hSvc, SERVICE_CONTROL_STOP, &sTs);
}
// 暂停服务
else if (sTs.dwCurrentState == SERVICE_RUNNING && newStatus == SERVICE_PAUSED)
{
	ret = ControlService(hSvc, SERVICE_CONTROL_PAUSE, &sTs);
}

if (!ret)
	printf("fail");
else
	printf("sucess");

CloseServiceHandle(hSCM);
CloseServiceHandle(hSvc);

创建

  创建服务使用CreateService函数

SC_HANDLE CreateService(
	SC_HANDLE hSCManager,			// 数据库句柄
	LPCWSTR   lpServiceName,		// 服务名
	LPCWSTR   lpDisplayName,		// 显示名
	DWORD     dwDesiredAccess,		// 权限
	DWORD     dwServiceType,		// 服务类型 
	DWORD     dwStartType,			// 启动类型
	DWORD     dwErrorControl,		// 错误操作
	LPCWSTR   lpBinaryPathName,		// 可执行文件路径
	LPCWSTR   lpLoadOrderGroup,		
	LPDWORD   lpdwTagId,
	LPCWSTR   lpDependencies,
	LPCWSTR   lpServiceStartName,
	LPCWSTR   lpPassword
);									// 成功返回 服务句柄 失败返回 NULL

  后五项基本都用不到,填NULL即可。服务类型,一般填写SERVICE_WIN32_OWN_PROCESS,表示服务类型是win32类型拥有独立进程的服务。

  默认创建服务是没有服务描述信息的,如果要添加要使用ChangeServiceConfig2,这里就不具体讲解了。

  示例

SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!hSCM) return;
LPCSTR path = "D:\\1.exe";
SC_HANDLE hService = CreateService(hSCM, "test", "test", SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, path, NULL, NULL, NULL, NULL, NULL);
if (!hService) return;

// 修改服务描述信息
SERVICE_DESCRIPTION servDesc = { 0 };
servDesc.lpDescription = "this is desc";
ChangeServiceConfig2(hService, SERVICE_CONFIG_DESCRIPTION, &servDesc);

printf("sucess\n");
CloseServiceHandle(hService);
CloseServiceHandle(hSCM);

删除

  删除服务使用的函数是DeleteService,注意的是这个函数只对已停止的服务起作用,所以在删除之前需要将服务停止

BOOL DeleteService(
	SC_HANDLE hService	// 服务句柄
);						// 成功返回 非0 失败返回 0

  示例

SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!hSCM) return;

SC_HANDLE hServ = OpenService(hSCM, "test", SERVICE_ALL_ACCESS);
if (!hServ) return;

SERVICE_STATUS sTs = { 0 };
// 尝试停止服务
BOOL ret = ControlService(hServ, SERVICE_CONTROL_STOP, &sTs);
if (!ret) return;

// 删除服务
if (!DeleteService(hServ)) return;

printf("ok");
CloseServiceHandle(hSCM);
CloseServiceHandle(hServ);

动态链接库

  动态链接库不能直接运行,不能接收消息.它们是一些独立的文件,其中包含能被可执行程序或其它DLL调用来完成某项工作的函数。只有在其它模块调用动态链接库中的函数时,它才发挥作用。

  微软任何一个版本的Windows操作系统,动态链接库(DLL)都是其核心和基础。

创建链接库

  链接库的创建有两种方法,这里先讲第一种。vs中创建一个项目。分别添加两个文件进项目
  DllTest.h

#pragma once

__declspec(dllexport) void SayHello();

  DllTest.c

#include <stdio.h>
#include "DllTest.h"

void SayHello()
{
	printf("hello dll");
}

  项目结构
  
  来到项目属性下的常规下的配置类型,选择动态库(.dll)
  
  之后再生成解决方案,将会成功生成dll
  再新建一个项目,将生成的DllTest.dllDllTest.libDllTest.h放到新项目目录下
  

无参调用

方法一

  将DllTest.h添加进头文件
  
  demo.c

#include <stdio.h>
#include "DllTest.h"

#pragma comment(lib,"DllTest.lib")

int main()
{
	SayHello();
	return 0;
}

  运行结果

hello dll
方法二

  不需要将DllTest.h添加进头文件
  demo.c

#include <stdio.h>
#include <Windows.h>

int main()
{
	HMODULE hModule = LoadLibrary("DllTest.dll");
	if (!hModule)
	{
		printf("load dll fail\n");
		return 0;
	}
	FARPROC fn = GetProcAddress(hModule,"SayHello");
	fn();
	FreeLibrary(hModule);
	return 0;
}

  运行结果

hello dll

  可有些时候,会调用GetModuleHandle来加载dll。GetModuleHandleLoadLibrary的区别是,GetModuleHandle是返回一个已经映射进调用进程地址空间的模块的句柄,并不增加引用计数。LoadLibrary是把一个模块映射进调用进程的地址空间,需要时增加引用计数。

  通俗点说如果dll已经加载进内存了,应该使用GetModuleHandle。反之使用LoadLibrary
  demo.c,示例

#include <stdio.h>
#include <Windows.h>

int main()
{
	HMODULE hModule = GetModuleHandle("DllTest.dll");
	// dll没加载进内存
	if (hModule == NULL)
	{
		printf("DllTest.dll not load in memory\n");
		// 将dll加载进内存
		hModule = LoadLibrary("DllTest.dll");
		if (!hModule)
		{
			printf("load dll fail\n");
			return 0;
		}
	}
	
	FARPROC fn = GetProcAddress(hModule,"SayHello");
	fn();
	FreeLibrary(hModule);
	return 0;
}

有参调用

  DllTest.h

#pragma once

__declspec(dllexport) int Add(int a,int b);

  DllTest.c

#include <stdio.h>
#include "DllTest.h"

int Add(int a, int b)
{
	return a + b;
}

  将生成的文件老规矩复制到新项目目录下

方法一

  将DllTest.h添加进头文件后
  demo.c

#include <stdio.h>
#include "DllTest.h"

#pragma comment(lib,"DllTest.lib")

int main()
{
	printf("%d", Add(1, 2));
	return 0;
}

  运行结果

3
方法二

  demo.c,如果有参函数无返回值,请使用无参调用方法二

#include <stdio.h>
#include <Windows.h>

typedef int (*Add)(int a, int b);

int main()
{
	HMODULE hModule = LoadLibrary("DllTest.dll");
	Add add = (Add)GetProcAddress(hModule,"Add");
	printf("%d", add(1, 2));
	FreeLibrary(hModule);
	return 0;
}

  运行结果

3

DllMain

  每一个DLL都会有一个入口函数,它是DLLMain,和在c开发当中的main函数是一样的都作为程序dll的入口段,系统在特定环境下会调用DLLMain。需要注意的是并不是所有的dll都必须要有DllMain,参考前面创建dll的例子,

  Dll被调用的四种状态

DLL_PROCESS_ATTACH 被进程装载时
DLL_THREAD_ATTACH  被线程装载时
DLL_THREAD_DETACH  被线程卸载时
DLL_PROCESS_DETACH 被进程卸载时

  这是第二种创建dll的方法,只需要新建项目的时候选择Dll即可
  
  改动后的dllmain.cpp

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include <stdio.h>

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        printf("DLL_PROCESS_ATTACH\n");
        break;
    case DLL_THREAD_ATTACH:
        printf("DLL_THREAD_ATTACH\n");
        break;
    case DLL_THREAD_DETACH:
        printf("DLL_THREAD_DETACH\n");
        break;
    case DLL_PROCESS_DETACH:
        printf("DLL_PROCESS_DETACH\n");
        break;
    }
    return TRUE;
}

  生成解决方案后,只会生成dll,不会生成lib文件。因为没有导出函数,将dll拷贝过去后,分别说明这4个case什么情况下会进入
  demo.c

#include <stdio.h>
#include <Windows.h>


int main()
{
	HMODULE hModule = LoadLibrary("DllTest.dll");
	FreeLibrary(hModule);
	return 0;
}

  运行结果

DLL_PROCESS_ATTACH
DLL_PROCESS_DETACH

  再看一种情况,子线程加载dll,主线程卸载dll

#include <stdio.h>
#include <Windows.h>

HMODULE hModule;

DWORD WINAPI Thread(LPVOID lp)
{
	hModule = LoadLibrary("DllTest.dll");
}

int main()
{
	HANDLE hThread = CreateThread(NULL, 0, Thread, NULL, 0, NULL);
	WaitForSingleObject(hThread, INFINITE);
	FreeLibrary(hModule);
	CloseHandle(hThread);
	return 0;
}

  运行结果

DLL_PROCESS_ATTACH
DLL_THREAD_DETACH
DLL_PROCESS_DETACH

  子线程卸载dll

#include <stdio.h>
#include <Windows.h>

HMODULE hModule;

DWORD WINAPI Thread(LPVOID lp)
{
	FreeLibrary(hModule);
}

int main()
{
	hModule = LoadLibrary("DllTest.dll");
	HANDLE hThread = CreateThread(NULL, 0, Thread, NULL, 0, NULL);
	WaitForSingleObject(hThread, INFINITE);
	CloseHandle(hThread);
	return 0;
}

  运行结果

DLL_PROCESS_ATTACH
DLL_THREAD_ATTACH
DLL_PROCESS_DETACH

网络模型

  在描述后面的网络模型之前,需要知道为什么要用网络模型。通常使用C/S架构的时候,服务器会同时服务多个客户端,这就涉及到如何管理这些客户端的Socket。来一个简单的多线程接收客户端Socket的例子。

服务端代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

DWORD WINAPI ListenFunc(LPVOID lp)
{
    SOCKET sClient = *(SOCKET*)lp;
    char buffer[1024];
    while (TRUE)
    {
        ZeroMemory(buffer, sizeof(buffer));
        int ret = recv(sClient, buffer, sizeof(buffer), 0);
        if (ret <= 0)
            break;
        
        printf("从客户端: %d 接收到的数据为: %s\n", (int)sClient, buffer);
    }

    closesocket(sClient);
    return TRUE;
}

int main()
{
	WSADATA wsa;
	WSAStartup(MAKEWORD(2, 2), &wsa);

	// 创建socket
	SOCKET sListen = socket(AF_INET, SOCK_STREAM, 0);

    // 监听端口
    int port = 5899;
    // 接收数据
    SOCKADDR_IN local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    // 接收任意ip数据
    local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

    bind(sListen, (SOCKADDR*)&local, sizeof(SOCKADDR));
    listen(sListen, 5);

    printf("等待客户端连接中...\n");

    // 死循环建立与客户端的连接
    while (TRUE)
    {
        SOCKET sClient = accept(sListen, NULL, NULL);
        printf("已成功与客户端%d已经连接\n", (int)sClient);
        HANDLE hThread = CreateThread(NULL, NULL, ListenFunc, (LPVOID)&sClient, 0, NULL);
        CloseHandle(hThread);
    }
    
}

客户端代码

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

int main(int argc, char* argv[])
{
    WSADATA wsa;
    WSAStartup(MAKEWORD(2, 2), &wsa);


    // 创建Socket
    SOCKET sClient = socket(AF_INET, SOCK_STREAM, 0);

    // 指定发送端口
    int port = 5899;
    // 指定发送地址
    char ip[] = "127.0.0.1";
    // 设置发送地址
    SOCKADDR_IN server;
    server.sin_family = AF_INET;
    server.sin_port = htons(port);
    server.sin_addr.S_un.S_addr = inet_addr(ip);
    
    // 建立连接
    connect(sClient, (SOCKADDR*)&server, sizeof(SOCKADDR));
    
    // 发送数据
    char buffer[1024];
    while (TRUE)
    {
        ZeroMemory(buffer, sizeof(buffer));
        printf("向服务端发送数据: ");
        scanf("%s", buffer);
        int ret = send(sClient, buffer, strlen(buffer), 0);
        if (ret <= 0)
            break;
    }
    
    closesocket(sClient);
    WSACleanup();

}

先启动服务端后,我又启动了两个客户端

客户端1发送的数据

向服务端发送数据: hello 1
向服务端发送数据:

客户端2发送的数据

向服务端发送数据: hello 2
向服务端发送数据:

服务端收到的数据

等待客户端连接中...
已成功与客户端304已经连接
从客户端: 304 接收到的数据为: hello 1
已成功与客户端300已经连接
从客户端: 300 接收到的数据为: hello 2

  通过多线程的方式,实现了一对多的连接,但是这不是最优选的方案。因为在做一个项目的时候,只用多线程来管理,难免会出现意想不到的bug。而且每创建一个线程,就要耗费一定的资源,要是面对成千上万的客户端,服务端的效率可想而知。

  老陈有一个在外地工作的女儿,不能经常回来,老陈和她通过信件联系。他们的信会被邮递员投递到他们的信箱里。这和Socket模型非常类似。下面我就以老陈接收信件为例讲解Socket I/O模型,详见后面的小故事篇。

Select

  使用Select模型,可以不用开辟额外的线程,即可完成上面的工作。Select模型只解决accept()傻等的问题,不解决recv(),send()执行阻塞问题。

  认识关键结构体fd_set

#ifndef FD_SETSIZE
#define FD_SETSIZE      64              // 默认64个
#endif /* FD_SETSIZE */

typedef struct fd_set {
        u_int fd_count;               	// 有效socket数量
        SOCKET  fd_array[FD_SETSIZE];   // socket数组
} fd_set;

  可以看到就是定义了一个大小为64位的数组,然后将socket加入此数组中。而select模型的本质其实是遍历这个数组,检查里面socket的状态而已。

  默认是64个,在WinSock2.h头文件前声明宏,给一个更大的值,可改变默认大小。

#define FD_SETSIZE 128
#include <WinSock2.h>

  因为原理就是不停遍历检测,越多效率越低,延迟越大,所以合适大小最好。select模型适合小用户量。

初始化

  在使用fd_set结构体之前,需要使用FD_ZERO来初始化

#define FD_ZERO(set) (((fd_set FAR *)(set))->fd_count=0)

  它做的事情就只是将fd_set.fd_count = 0

fd_set allSocket;
FD_ZERO(&allSocket);
添加

  使用FD_SET,将一个socket添加进fd_set数组中

#define FD_SET(fd, set) do { \
    u_int __i; \
    for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count; __i++) { \
        if (((fd_set FAR *)(set))->fd_array[__i] == (fd)) { \
            break; \
        } \
    } \
    if (__i == ((fd_set FAR *)(set))->fd_count) { \
        if (((fd_set FAR *)(set))->fd_count < FD_SETSIZE) { \
            ((fd_set FAR *)(set))->fd_array[__i] = (fd); \
            ((fd_set FAR *)(set))->fd_count++; \
        } \
    } \
} while(0, 0)

  上面的代码不难理解,先判断此socket是否在此数组中,不在就将socket添加到数组末尾。

FD_SET(sClient, &allSocket);
删除

  FD_CLR,会从数组中删除指定的socket。

#define FD_CLR(fd, set) do { \
    u_int __i; \
    for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count ; __i++) { \
        if (((fd_set FAR *)(set))->fd_array[__i] == fd) { \
            while (__i < ((fd_set FAR *)(set))->fd_count-1) { \
                ((fd_set FAR *)(set))->fd_array[__i] = \
                    ((fd_set FAR *)(set))->fd_array[__i+1]; \
                __i++; \
            } \
            ((fd_set FAR *)(set))->fd_count--; \
            break; \
        } \
    } \
} while(0, 0)

  慢慢遍历,找到了,就让后面一个元素等于前面一个元素,也就是往前摞数据。

FD_CLR(sClient, &allSocket);
查询

  FD_ISSET会查询socket是否在数组中,不存在返回0,存在返回非0

#define FD_ISSET(fd, set) __WSAFDIsSet((SOCKET)(fd), (fd_set FAR *)(set))
select()函数

  主角登场,select函数来获取可用的socket

int WSAAPI select(
  [in]      int           nfds,			// 忽略 设为0即可
  [in, out] fd_set        *readfds,		// 检查在向服务器发送信息的socket 并返回可响应的socket
  [in, out] fd_set        *writefds,	// 检查可向客户端发送信息的socket 并返回可响应的socket
  [in, out] fd_set        *exceptfds,	// 检查有异常错误的socket       并返回可响应的socket
  [in]      const timeval *timeout		// 选择等待的最大时间
);										// 错误返回 SOCKET_ERROR

  timeval结构体如下

typedef struct timeval {
  long tv_sec;		// 单位秒
  long tv_usec;		// 单位微妙
} TIMEVAL, *PTIMEVAL, *LPTIMEVAL;

  通过设置timeval结构体,来控制select的等待时间。select函数可以选择它的运行时间,前面说了它会一直遍历socket数组,来查看里面是否有可用的socket,如果将timeval设置为3秒,即只运行3s,select函数就会返回。如果设置为NULL,就会死等,直到有可用的socket。

  在前面的参数中,不要将三个fd_set都设置成NULL,不然啥也干不了,没啥敲别人家的门,不闲的慌吗。可能你不是太能理解*readfds*writefds*exceptfds,这三个参数具体还是啥意思,看完下面的例子就懂了。

单个示例

客户端代码不变,服务端代码如下

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

int main()
{
	WSADATA wsa;
	WSAStartup(MAKEWORD(2, 2), &wsa);

	// 创建socket
	SOCKET sListen = socket(AF_INET, SOCK_STREAM, 0);

    // 监听端口
    int port = 5899;
    // 接收数据
    SOCKADDR_IN local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    // 接收任意ip数据
    local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

    bind(sListen, (SOCKADDR*)&local, sizeof(SOCKADDR));
    listen(sListen, 5);

    printf("等待客户端连接中...\n");

    fd_set allSockets;
    // 初始化allSockets
    FD_ZERO(&allSockets);
    // 将服务端的socket装入
    FD_SET(sListen, &allSockets);
    
    // 让select等待3s
    TIMEVAL time = { 3,0 };
    while (TRUE)
    {
        // 临时保存allSockets
        fd_set readSockets = allSockets;

        // 也可以让最后一个参数设置为 NULL, 即必须获取到有响应的socket
        int ret = select(0, &readSockets, NULL, NULL, &time);   
        // 没有可用的socket
        if (ret == 0)
            continue;
        
        // 出错了
        if (ret == SOCKET_ERROR || ret < 0)
            break;
        
        // 循环遍历
        for (u_int i = 0; i < readSockets.fd_count; i++)
        {
            // 有客户端请求连接
            if (readSockets.fd_array[i] == sListen)
            {
                SOCKET sClient = accept(sListen, NULL, NULL);
                printf("已成功与客户端%d已经连接\n", (int)sClient);
                // 将客户端加入 socket 数组中
                FD_SET(sClient, &allSockets);
            }
            // 有客户端向服务端发送数据
            else
            {
                char buffer[1024];
                ZeroMemory(buffer, sizeof(buffer));

                int nRecv = recv(readSockets.fd_array[i], buffer, sizeof(buffer), 0);

                if (nRecv > 0)
                {
                    printf("从客户端: %d 接收到的数据为: %s\n", (int)readSockets.fd_array[i], buffer);
                }
                // 客户端下线
                else
                {
                    // 释放socket资源
                    closesocket(readSockets.fd_array[i]);
                    // 从socket数组中删除
                    FD_CLR(readSockets.fd_array[i], &allSockets);
                }

            }
        }
    }
    
    closesocket(sListen);
    WSACleanup();
}

  在第40行写了一个fd_set readSockets = allSocket;,是因为select会改变fd_set值,只返回有效的fd_set。如果直接传递allSocket,那么已经就不是原来的socket数组了,所以要先存起来。

完整示例

  在上面的例子中,只传递了readfds参数,是为了方便读者进行理解整个select网络模型。而writefdsexceptfds两个参数用的场景也比较少,这里也写一个完整的例子,让读者来参考。

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

int main()
{
	WSADATA wsa;
	WSAStartup(MAKEWORD(2, 2), &wsa);

	// 创建socket
	SOCKET sListen = socket(AF_INET, SOCK_STREAM, 0);

    // 监听端口
    int port = 5899;
    // 接收数据
    SOCKADDR_IN local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    // 接收任意ip数据
    local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

    bind(sListen, (SOCKADDR*)&local, sizeof(SOCKADDR));
    listen(sListen, 5);

    printf("等待客户端连接中...\n");

    fd_set allSockets;
    // 初始化allSockets
    FD_ZERO(&allSockets);
    // 将服务端的socket装入
    FD_SET(sListen, &allSockets);
    
    // 让select等待3s
    TIMEVAL time = { 3,0 };
    while (TRUE)
    {
        // 临时保存allSockets
        fd_set readSockets = allSockets;
        fd_set writeSockets = allSockets;
        fd_set errorSockets = allSockets;

        // 也可以让最后一个参数设置为 NULL, 即必须获取到有响应的socket
        int ret = select(0, &readSockets, &writeSockets, &errorSockets, &time);
        // 没有可用的socket
        if (ret == 0)
            continue;
        
        // 出错了
        if (ret == SOCKET_ERROR || ret < 0)
            break;
        
        // 有错误的socket
        for (u_int i = 0; i < errorSockets.fd_count; i++)
        {
            char str[100] = { 0 };
            int len = 99;
            if (SOCKET_ERROR == getsockopt(errorSockets.fd_array[i], SOL_SOCKET, SO_ERROR, str, &len))
            {
                printf("无法得到错误信息\n");
            }
            printf("%s\n", str);
        }


        // 可向客户端发送数据的socket
        for (u_int i = 0; i < writeSockets.fd_count; i++)
        {
            printf("客户端 %d 可写\n", (int)writeSockets.fd_array[i]);
            send(writeSockets.fd_array[i], "ok", 2, 0);
        }

        // 可以接收客户端信息的socket
        for (u_int i = 0; i < readSockets.fd_count; i++)
        {
            // 有客户端请求连接
            if (readSockets.fd_array[i] == sListen)
            {
                SOCKET sClient = accept(sListen, NULL, NULL);
                printf("已成功与客户端%d已经连接\n", (int)sClient);
                // 将客户端加入 socket 数组中
                FD_SET(sClient, &allSockets);
            }
            // 有客户端向服务端发送数据
            else
            {
                char buffer[1024];
                ZeroMemory(buffer, sizeof(buffer));

                int nRecv = recv(readSockets.fd_array[i], buffer, sizeof(buffer), 0);

                if (nRecv > 0)
                {
                    printf("从客户端: %d 接收到的数据为: %s\n", (int)readSockets.fd_array[i], buffer);
                }
                // 客户端下线
                else
                {
                    // 释放socket资源
                    closesocket(readSockets.fd_array[i]);
                    // 从socket数组中删除
                    FD_CLR(readSockets.fd_array[i], &allSockets);
                }

            }
        }
    }
    
    closesocket(sListen);
    WSACleanup();
}
小故事篇

  老陈非常想看到女儿的信。以至于他每隔10分钟就下楼检查信箱,看是否有女儿的信。在这种情况下,”下楼检查信箱”然后回到楼上耽误了老陈太多的时间,以至于老陈无法做其他工作。
select模型和老陈的这种情况非常相似:周而复始地去检查……如果有数据……接收/ 发送…….

事件选择

  事件选择(WSAEventSelect)模型是另一个有用的异步 I/O 模型,它对Select模型进行了改进。事件通知模型会对事件进行监听处理。它的监听处理逻辑如下,当客户端1,客户端2,客户端3,向服务器发送请求连接等等操作。它会依次询问客户端1是否有请求数据,客户端2是否有请求数据,客户端3是否有请求数据。如果客户端2有请求数据的时候,它会去处理客户端2,后面的客户端3就不会处理了,随后又重头询问,客户端1,客户端2,客户端3是否有请求数据。它并不是先来后到进行处理,而是谁跑在前面就处理谁,也就是一种无序的处理方式,这点请牢记,后面内容会专门解决这个问题。

创建事件对象

  使用WSACreateEvent创建一个事件对象

WSAEVENT WSAAPI WSACreateEvent(); // 失败返回 WSA_INVALID_EVENT 成功返回 事件对象句柄

  使用WSACloseEvent关闭事件对象

BOOL WSAAPI WSACloseEvent(
     WSAEVENT hEvent		// 已打开的事件对象句柄
);							// 成功返回 TRUE 失败返回 FALSE
绑定事件对象

  使用WSAEventSelect为每一个事件绑定一个socket以及操作FD_READ、FD_WRITE、FD_ACCEPT、FD_CONNECT、FD_CLOSE等并投递给操作系统, 相当于操作系统单开了一个线程, 这里就是异步了。

int WSAAPI WSAEventSelect(
    SOCKET   s,				// 事件对象套接字
  	WSAEVENT hEventObject,	// 事件对象句柄
  	long     lNetworkEvents	// 希望监视的事件对象类型
);							// 成功返回 0 失败返回 SOCKET_ERROR

  监听对象的套接字,只要发生了监视的事件,就会将事件对象句柄所指的内核对象改为有信号(signaled)状态,默认是无信号(nonsignaled)状态。并且无论事件是否发生,WSAEventSelect函数被调用后都会立即返回。所以可以去执行其他任务,也就是说,该函数以异步通知的方式工作。

  其中参数lNetworkEvents常用值如下。

  FD_READ:是否有接收数据通知
  FD_WRITE:是否有发送数据通知
  FD_CLOSE:是否有断开连接通知
  FD_ACCEPT:是否有连接请求通知

是否发生事件

  当成功绑定事件后,需要验证是否有事件发送,需要调用WSAWaitForMultipleEvents函数,来获取事件信息。

DWORD WSAAPI WSAWaitForMultipleEvents(
	DWORD          cEvents,			// 监视的事件对象个数 最大值为 WSA_MAXIMUM_WAIT_EVENTS = 64
	const WSAEVENT *lphEvents,		// 指向事件对象句柄数组的指针
	BOOL           fWaitAll,		// TRUE 所有事件对象都有信号状态就返回 FALSE 任何事件对象有信号就返回
	DWORD          dwTimeout,		// 超时时间 单位毫秒 值为WSA_INFINITE时,一直等到有信号才返回
	BOOL           fAlertable		// 主要用于在重叠I/O模型中 事件选择模型应设为FALSE
);									// 成功 返回一个值 失败返回 WSA_WAIT_FAILED

  若 WSAWaitForMultipleEvents 收到一个事件对象的网络事件通知,便会返回一个值,通过返回值减去常量 WSA_WAIT_EVENT_0,得到具体的事件数组索引位置。如下例所示:

DWORD nRes = WSAWaitForMultipleEvents(...);
// 得到下标
DWORD nIndex = nRes - WSA_WAIT_EVENT_0;

  而此函数最多只能传递64个事件对象,即宏定义的 WSA_MAXIMUM_WAIT_EVENTS,这样一看不就和Select模型一样了嘛,后面会专门解决这个问题。

获取事件类型

  当我们知道有事件被发送了,应最后调用WSAEnumNetworkEvents函数,获取具体发生了什么类型的网络事件。

int WSAAPI WSAEnumNetworkEvents(
	SOCKET             s,				// 事件对象套接字
	WSAEVENT           hEventObject,	// 事件对象句柄
	LPWSANETWORKEVENTS lpNetworkEvents	// 指向 WSANETWORKEVENTS 结构的指针,保存事件类型信息
);

  WSANETWORKEVENTS 结构体定义如下

typedef struct _WSANETWORKEVENTS {
  long lNetworkEvents;					// 指示发生了哪些FD_XXX网络事件
  int  iErrorCode[FD_MAX_EVENTS];		// 错误代码数组,同lNetworkEvents中的事件关联在一起。
} WSANETWORKEVENTS, *LPWSANETWORKEVENTS;

  错误信息将保存到iErrorCode数组中,如果发生FD_XXX相关错误,则在iErrorCode[FD_XXX_BIT]中保存除0以外的其他值。下面为伪代码

// 有请求连接 并且 iErrorCode无错误码
if ((lNetworkEvents & FD_ACCEPT) && netWorkEvents.iErrorCode[FD_ACCEPT_BIT] == 0)
完整示例
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

typedef struct 
{
    int cnt;
    SOCKET sockall[WSA_MAXIMUM_WAIT_EVENTS];
    WSAEVENT eventall[WSA_MAXIMUM_WAIT_EVENTS];
}fd_event;

int main()
{
	WSADATA wsa;
	WSAStartup(MAKEWORD(2, 2), &wsa);

	// 创建socket
	SOCKET sListen = socket(AF_INET, SOCK_STREAM, 0);

    // 监听端口
    int port = 5899;
    // 接收数据
    SOCKADDR_IN local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    // 接收任意ip数据
    local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

    bind(sListen, (SOCKADDR*)&local, sizeof(SOCKADDR));
    listen(sListen, 5);

    printf("等待客户端连接中...\n");

    
    // 创建事件
    WSAEVENT eventServer = WSACreateEvent();
    if (WSA_INVALID_EVENT == eventServer)
        return 0;
    
    if (WSAEventSelect(sListen, eventServer, FD_ACCEPT) == SOCKET_ERROR)
        return 0;

    fd_event fdEvent = { 0, { 0 }, { NULL } };

    // 装进去
    fdEvent.eventall[fdEvent.cnt] = eventServer;
    fdEvent.sockall[fdEvent.cnt] = sListen;
    fdEvent.cnt++;
    
    while (TRUE)
    {
        DWORD nRes = WSAWaitForMultipleEvents(fdEvent.cnt, fdEvent.eventall, FALSE, WSA_INFINITE, FALSE);
        // 出错了
        if (WSA_WAIT_FAILED == nRes)
            break;
        
        // 得到下标
        DWORD nIndex = nRes - WSA_WAIT_EVENT_0;
        // 得到下标对应的具体操作
        WSANETWORKEVENTS netWorkEvents;
        if (WSAEnumNetworkEvents(fdEvent.sockall[nIndex],fdEvent.eventall[nIndex],&netWorkEvents) == SOCKET_ERROR)
            break;
        
        long lNetworkEvents = netWorkEvents.lNetworkEvents;
        // 有客户端连接
        if ((lNetworkEvents & FD_ACCEPT) && netWorkEvents.iErrorCode[FD_ACCEPT_BIT] == 0)
        {
            SOCKET sClient = accept(sListen, NULL, NULL);
            if (sClient == INVALID_SOCKET)
                continue;

            // 创建事件对象
            WSAEVENT wsaClientEvent = WSACreateEvent();
            if (wsaClientEvent == WSA_INVALID_EVENT)
            {
                closesocket(sClient);
                continue;
            }

            // 投递给系统
            if (WSAEventSelect(sClient, wsaClientEvent, FD_READ | FD_CLOSE | FD_WRITE) == SOCKET_ERROR)
            {
                closesocket(sClient);
                WSACloseEvent(wsaClientEvent);
                continue;
            }

            // 装进结构体
            fdEvent.eventall[fdEvent.cnt] = wsaClientEvent;
            fdEvent.sockall[fdEvent.cnt] = sClient;
            fdEvent.cnt++;

            printf("来自%d的连接\n", (int)sClient);
        }
        
        // 向客户端发送数据 只会产生一次send事件
        if ((lNetworkEvents & FD_WRITE) && netWorkEvents.iErrorCode[FD_WRITE_BIT] == 0)
        {
            // 可做初始化数据操作
            send(fdEvent.sockall[nIndex], "success", strlen("success"), 0);
            printf("write event\n");
        }

        // 接收客户端的数据
        if ((lNetworkEvents & FD_READ) && netWorkEvents.iErrorCode[FD_READ_BIT] == 0)
        {
            char buffer[1024];
            ZeroMemory(buffer, sizeof(buffer));

            if (recv(fdEvent.sockall[nIndex], buffer, sizeof(buffer), 0) > 0)
                printf("已收到%d客户端的消息:%s \n", (int)fdEvent.sockall[nIndex], buffer);
            
        }

        // 客户端下线
        if (lNetworkEvents & FD_CLOSE)
        {
            printf("客户端%d断开连接\n", (int)fdEvent.sockall[nIndex]);

            // 清理下线客户端套接字
            closesocket(fdEvent.sockall[nIndex]);
            fdEvent.sockall[nIndex] = fdEvent.sockall[fdEvent.cnt - 1];
            
            // 关闭事件
            WSACloseEvent(fdEvent.eventall[nIndex]);
            fdEvent.eventall[nIndex] = fdEvent.eventall[fdEvent.cnt - 1];

            fdEvent.cnt--;
        }
        
    }
    // 释放事件局部
    WSACloseEvent(eventServer);
    closesocket(sListen);
    WSACleanup();
}

  在上面的例子中,定义了一个结构体,保存64个客户端socket和事件对象。在第99行,事件选择模型对于FD_WRITE来说,只会调用一次send,可以发送一些初始化数据给客户端,而Select模型会一直调用send,这也是不同的地方。

最终优化示例

  在前面提到过,事件选择模型是无序的,试想一种情况,假如客户端1,一直不停的发送数据。而因为此模型的无序特点,就会优先一直处理客户端1,后面的客户端无法处理,这是非常致命的。第二,因为WSAWaitForMultipleEvents监听事件函数,一次只能处理64个,对于想处理超过64个,要用其他办法。

  这里我们把代码,改成有序处理,即对每个Socket都调用一次WSAWaitForMultipleEvents,就可以轻松解决。完整代码如下

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

typedef struct
{
    int cnt;
    SOCKET sockall[1024];
    WSAEVENT eventall[1024];
}fd_event;

int main()
{
    WSADATA wsa;
    WSAStartup(MAKEWORD(2, 2), &wsa);

    // 创建socket
    SOCKET sListen = socket(AF_INET, SOCK_STREAM, 0);

    // 监听端口
    int port = 5899;
    // 接收数据
    SOCKADDR_IN local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    // 接收任意ip数据
    local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

    bind(sListen, (SOCKADDR*)&local, sizeof(SOCKADDR));
    listen(sListen, 5);

    printf("等待客户端连接中...\n");


    // 创建事件
    WSAEVENT eventServer = WSACreateEvent();
    if (WSA_INVALID_EVENT == eventServer)
        return 0;

    if (WSAEventSelect(sListen, eventServer, FD_ACCEPT) == SOCKET_ERROR)
        return 0;

    fd_event fdEvent = { 0, { 0 }, { NULL } };

    // 装进去
    fdEvent.eventall[fdEvent.cnt] = eventServer;
    fdEvent.sockall[fdEvent.cnt] = sListen;
    fdEvent.cnt++;

    while (TRUE)
    {
        int startIndex = WSAWaitForMultipleEvents(fdEvent.cnt, fdEvent.eventall, FALSE, WSA_INFINITE, FALSE);
        // 出错了
        if (WSA_WAIT_FAILED == startIndex)
            break;

        startIndex = startIndex - WSA_WAIT_EVENT_0;

        for (int nIndex = startIndex; nIndex < fdEvent.cnt; nIndex++)
        {
            DWORD nRes = WSAWaitForMultipleEvents(1, &fdEvent.eventall[nIndex], FALSE, 0, FALSE);
            // 出错了
            if (WSA_WAIT_FAILED == nRes)
                continue;

            // 超时了
            if (WSA_WAIT_TIMEOUT == nRes)
                continue;

            // 得到下标对应的具体操作
            WSANETWORKEVENTS netWorkEvents;
            if (WSAEnumNetworkEvents(fdEvent.sockall[nIndex], fdEvent.eventall[nIndex], &netWorkEvents) == SOCKET_ERROR)
                break;

            long lNetworkEvents = netWorkEvents.lNetworkEvents;
            // 有客户端连接
            if ((lNetworkEvents & FD_ACCEPT) && netWorkEvents.iErrorCode[FD_ACCEPT_BIT] == 0)
            {
                SOCKET sClient = accept(sListen, NULL, NULL);
                if (sClient == INVALID_SOCKET)
                    continue;

                // 创建事件对象
                WSAEVENT wsaClientEvent = WSACreateEvent();
                if (wsaClientEvent == WSA_INVALID_EVENT)
                {
                    closesocket(sClient);
                    continue;
                }

                // 投递给系统
                if (WSAEventSelect(sClient, wsaClientEvent, FD_READ | FD_CLOSE | FD_WRITE) == SOCKET_ERROR)
                {
                    closesocket(sClient);
                    WSACloseEvent(wsaClientEvent);
                    continue;
                }

                // 装进结构体
                fdEvent.eventall[fdEvent.cnt] = wsaClientEvent;
                fdEvent.sockall[fdEvent.cnt] = sClient;
                fdEvent.cnt++;

                printf("来自%d的连接\n", (int)sClient);
            }

            // 向客户端发送数据 只会产生一次send事件
            if ((lNetworkEvents & FD_WRITE) && netWorkEvents.iErrorCode[FD_WRITE_BIT] == 0)
            {
                // 可做初始化数据操作
                send(fdEvent.sockall[nIndex], "success", strlen("success"), 0);
                printf("write event\n");
            }

            // 接收客户端的数据
            if ((lNetworkEvents & FD_READ) && netWorkEvents.iErrorCode[FD_READ_BIT] == 0)
            {
                char buffer[1024];
                ZeroMemory(buffer, sizeof(buffer));

                if (recv(fdEvent.sockall[nIndex], buffer, sizeof(buffer), 0) > 0)
                    printf("已收到%d客户端的消息:%s \n", (int)fdEvent.sockall[nIndex], buffer);

            }

            // 客户端下线
            if (lNetworkEvents & FD_CLOSE)
            {
                printf("客户端%d断开连接\n", (int)fdEvent.sockall[nIndex]);

                // 清理下线客户端套接字
                closesocket(fdEvent.sockall[nIndex]);
                // 关闭事件
                WSACloseEvent(fdEvent.eventall[nIndex]);
                
                fdEvent.sockall[nIndex] = fdEvent.sockall[fdEvent.cnt - 1];
                fdEvent.eventall[nIndex] = fdEvent.eventall[fdEvent.cnt - 1];

                fdEvent.cnt--;
            }
        }
    }

    for (int i = 0; i < fdEvent.cnt; i++)
    {
        closesocket(fdEvent.sockall[i]);
        WSACloseEvent(fdEvent.eventall[i]);
    }

    // 释放事件局部
    WSACloseEvent(eventServer);
    closesocket(sListen);
    WSACleanup();
}
小故事篇

  后来,老陈使用了微软公司的新式信箱。这种信箱非常先进,在客户的家中添加一个附加装置,这个装置会监视客户的信箱,每当新的信件来临,此装置会发出”新信件到达”声,提醒老陈去收信。微软提供的WSAEventSelect模型就是这个意思。

重叠IO

  从本质上来说,重叠IO模型才是真正的异步模型,之前的事件模型都不算是真正意义上的异步模型。具体来说在之前的事件模型中,当有消息到来的时候,系统会通知我们调用recv()函数,而recv()函数是阻塞的,还是会等待数据接收完毕后才开始返回。而重叠IO模型则是调用WSARecv()函数,然后立即返回,当数据被拷贝进数据缓冲区的时候,系统才通知我们去处理,这时候我们处理的时候,数据已经在我们的缓冲区了,省去了继续等待数据拷贝进缓冲区的时间。因此才说重叠IO模型才是真正的异步模型。

  重叠IO模型中具体的实现方法有两种,分别是事件通知和完成例程。这次的是事件通知模型。之所以叫事件通知是因为异步还是基于事件的,这点和之前的事件模型相类似。

事件通知

  重叠I/O事件通知模型和WSAEventSelect模型在实现上非常相似,主要区别在Overlapped,Overlapped模型是让应用程序使用重叠数据结构(WSAOVERLAPPED),一次投递一个或多个Winsock I/O请求。这些提交的请求完成后,应用程序会收到通知。什么意思呢?就是说,如果你想从socket上接收数据,只需要告诉系统,由系统为你接收数据,而你需要做的只是为系统提供一个缓冲区。

创建套接字

  在使用这些事件对象时,函数调用方法,会加入WSA字符,比如创建套接字从socket(),变成WSASocket(),接收数据WSARecv()等等。使用

SOCKET WSAAPI WSASocketW(
	int                 af,				// 协议族信息
	int                 type,			// 传输类型
	int                 protocol,		// 传输协议
	LPWSAPROTOCOL_INFOW lpProtocolInfo,	// 该结构定义所创建套接口的特性 这里设为NULL
	GROUP               g,				// 预留参数 填0
	DWORD               dwFlags			// 套接字属性设置 重叠IO必须填WSA_FLAG_OVERLAPPED
);										// 成功返回 套接字 失败返回 INVALID_SOCKET

  简单的创建示例

SOCKET sListen = WSASocketW(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
OVERLAPPED结构体

  它包含了包含异步或重叠IO中使用的信息。

typedef struct _OVERLAPPED {
  ULONG_PTR Internal;			// 系统使用 无需关注
  ULONG_PTR InternalHigh;		// 系统使用 无需关注
  DWORD Offset;					// 系统使用 无需关注
  DWORD OffsetHigh;				// 系统使用 无需关注
  HANDLE    hEvent;				// 事件对象
} OVERLAPPED, *LPOVERLAPPED;

   可以看到我们只需要关注hEvent成员,并且在使用结构体时,给hEvent = WSACreateEvent()即可。这样整个事件对象和OVERLAPPED结构体关联起了。

接收消息

  接收消息使用WSARecv(),我更喜欢叫它投递接收消息,因为它是非阻塞的,告诉操作系统你帮我监视接收信息,一旦有接收信息就通知我。这样就解决了傻等问题了。

int WSAAPI WSARecv(
	SOCKET                             s,					// 套接字
	LPWSABUF                           lpBuffers,			// 指向WSABUF结构指针 用来保存接收的信息
	DWORD                              dwBufferCount,		// lpBuffers数组的个数
	LPDWORD                            lpNumberOfBytesRecvd,// 接收的数据 如果lpOverlapped不为NULL 这里应填NULL
	LPDWORD                            lpFlags,				// 指向传输标志的指针 赋予0即可
	LPWSAOVERLAPPED                    lpOverlapped,		// 指向 OVERLAPPED 结构体的指针
	LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine	// 完成例程才使用 这里填NULL
);

  来看WSABUF结构体,其实就是将要接收的数据信息,合在了一起

typedef struct _WSABUF {
  ULONG len;	// 缓冲区长度
  CHAR  *buf;	// 缓冲区指针
} WSABUF, *LPWSABUF;

  如果lpCompletionRoutine参数被设为NULL,当数据接收完成后,lpOverlapped指针指向的hEvent就会被设置成有信号状态,从而通知应用程序进行相应的处理。

  关于该函数的返回值,如果未发生错误并且接收操作立即完成,那么函数会返回0。否则返回SOCKET_ERROR,并且调用WSAGetLastError()来获取错误码。如果错误码为WSA_IO_PENDING,表示已成功启动重叠的操作,并且稍后将完成。 任何其他错误代码都表示重叠操作失败。

获取通知

  使用WSAGetOverlappedResult来获取是否有消息,此函数能获取传输结果与状态

BOOL WSAAPI WSAGetOverlappedResult(
	SOCKET          s,				// 套接字
	LPWSAOVERLAPPED lpOverlapped,	// 指向 OVERLAPPED 结构体的指针
	LPDWORD         lpcbTransfer,	// 指向接收或发送的实际传输字节数的指针 禁止为NULL
	BOOL            fWait,			// 事件通知才能设置成 TRUE
	LPDWORD         lpdwFlags		// 得到WSARecv函数中的lpFlags的标志 禁止为NULL
);									// 成功返回 TRUE 失败返回 FALSE
示例代码
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#define MAX_COUNT 1024

#pragma comment(lib, "ws2_32.lib")

// 定义数据结构
typedef struct
{
    SOCKET socket[MAX_COUNT];
    WSABUF Buffer[MAX_COUNT];
    DWORD dwFlag[MAX_COUNT];
    OVERLAPPED *overlapped[MAX_COUNT];
    int count;
}IO_OVERDATA;

IO_OVERDATA client;

// 清理函数
void Clean(int index);
// 定义服务线程
DWORD WINAPI ServerThread(LPVOID lpParam);

int main()
{
    WSADATA wsa;
    WSAStartup(MAKEWORD(2, 2), &wsa);

    // 创建socket
    SOCKET sListen = WSASocketW(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
    if (sListen == INVALID_SOCKET)
        return 0;

    // 监听端口
    int port = 5899;
    // 接收数据
    SOCKADDR_IN local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    // 接收任意ip数据
    local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

    bind(sListen, (SOCKADDR*)&local, sizeof(SOCKADDR));
    listen(sListen, 5);

    printf("等待客户端连接中...\n");

    // 初始化数量
    client.count = 0;

    //创建服务线程
    CreateThread(NULL, 0, ServerThread, NULL, 0, NULL);

    while (TRUE)
    {
        // 有新连接到来
        SOCKET sClient = accept(sListen, NULL, NULL);
        if (sClient == INVALID_SOCKET)
            continue;

        printf("已成功与客户端%d已经连接\n", (int)sClient);

        // 将客户端套接字添加给全局socket数组
        client.socket[client.count] = sClient;


        // 给缓冲区分配空间
        client.Buffer[client.count].buf = (char*)malloc(sizeof(char) * 1024);
        client.Buffer[client.count].len = 1024;
        ZeroMemory(client.Buffer[client.count].buf, client.Buffer[client.count].len);

        client.overlapped[client.count] = (OVERLAPPED*)malloc(sizeof(OVERLAPPED));
        ZeroMemory(client.overlapped[client.count], sizeof(OVERLAPPED));

        // 为客户端套接字创建事件对象
        client.overlapped[client.count]->hEvent = WSACreateEvent();

        client.dwFlag[client.count] = 0;

        // 对套接字进行接收数据
        WSARecv(sClient, &client.Buffer[client.count], 1, NULL,
            &client.dwFlag[client.count], client.overlapped[client.count], NULL);

        client.count++;
    }


    closesocket(sListen);
    WSACleanup();

    return 0;
}

// 处理相关的事件
DWORD WINAPI ServerThread(LPVOID lpParam)
{
    while (TRUE)
    {
        for (int i = 0; i < client.count; i++)
        {
            int nRet = WSAWaitForMultipleEvents(1, &client.overlapped[i]->hEvent, FALSE, 0, FALSE);
            if (nRet == WSA_WAIT_FAILED || nRet == WSA_WAIT_TIMEOUT)
                continue;

            // 重置事件对象 将事件对象设置成无信号状态
            WSAResetEvent(client.overlapped[i]->hEvent);

            DWORD dwBytesTransferred = 0;
            BOOL bFlag = WSAGetOverlappedResult(client.socket[i], client.overlapped[i], &dwBytesTransferred, TRUE, &client.dwFlag[i]);

            // 客户端退出
            if (dwBytesTransferred == 0)
            {
                // 这里要进行处理 比如资源的释放等等
                printf("客户端%d已下线\n", (int)client.socket[i]);
                Clean(i);
                continue;
            }

            if (bFlag == FALSE)
                continue;

            // 走到这里就说明已经收到消息了
            printf("从客户端: %d 接收到的数据为: %s\n", (int)client.socket[i], client.Buffer[i].buf);
            ZeroMemory(client.Buffer[i].buf, client.Buffer[i].len);

            // 继续对套接字进行接收数据
            WSARecv(client.socket[i], &client.Buffer[i], 1, NULL,
                &client.dwFlag[i], client.overlapped[i], NULL);
        }
    }
}

void Clean(int index)
{
    // 释放资源
    closesocket(client.socket[index]);
    WSACloseEvent(client.overlapped[index]->hEvent);

    // 释放空间
    free(client.Buffer[index].buf);

    OVERLAPPED* tmp = client.overlapped[index];

    if (index < client.count - 1)
    {
        // 交换被关闭的客户端数据与数组最后一个客户端数据
        client.socket[index] = client.socket[client.count - 1];
        client.Buffer[index] = client.Buffer[client.count - 1];
        client.dwFlag[index] = client.dwFlag[client.count - 1];
        client.overlapped[index] = client.overlapped[client.count - 1];
    }

    free(tmp);

    // 客户端数量 - 1
    client.count--;
}

  我们应该使用WSAResetEvent()函数将此事件重新置为无信号。而在事件选择模型中,系统已经帮我们做了重置操作。

小故事篇

  后来,微软通过调查发现,老陈不喜欢上下楼收发信件,因为上下楼其实很浪费时间。于是微软再次改进他们的信箱。新式的信箱采用了更为先进的技术,只要用户告诉微软自己的家在几楼几号,新式信箱会把信件直接传送到用户的家中,然后告诉用户,你的信件已经放到你的家中了!老陈很高兴,因为他不必再亲自收发信件了!

完成例程

  完成例程效率要比事件通知高不少,系统在网络操作完成以后会自动调用你提供的回调函数。可能不是太理解,等会看代码,就清晰很多了。并且整体逻辑和代码量也要比事件通知看起简单。

例程函数

  在WSARecv()函数的最后一个参数lpCompletionRoutine,就是我们要用的回调函数指针,函数定义如下

void CALLBACK CompletionRoutineFunc(
  DWORD dwError,				// 错误码
  DWORD cbTransferred,			// 实际传输的字节数
  LPWSAOVERLAPPED lpOverlapped,	// 指向 OVERLAPPED 结构体的指针
  DWORD dwFlags					// 操作结束的标志 一般没用
)

  dwError的错误码,是系统自己调用GetLastError()函数获得的。而已经不用使用事件了,所以OVERLAPPED.hEvent可以不用设置

示例代码
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")


// 定义数据结构
typedef struct _MY_WSAOVERLAPPED
{
    WSAOVERLAPPED overlapped;  // overlapped 成员 放在第一位 方便
    SOCKET socket;
    WSABUF Buffer;
    DWORD dwFlag;
}MY_WSAOVERLAPPED, * PMY_WSAOVERLAPPED;

// 清理函数
void Clean(PMY_WSAOVERLAPPED* client);
// 接收消息
void CALLBACK RecvCall(DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags);

int main()
{
    WSADATA wsa;
    WSAStartup(MAKEWORD(2, 2), &wsa);

    // 创建socket
    SOCKET sListen = WSASocketW(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
    if (sListen == INVALID_SOCKET)
        return 0;

    // 监听端口
    int port = 5899;
    // 接收数据
    SOCKADDR_IN local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    // 接收任意ip数据
    local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

    bind(sListen, (SOCKADDR*)&local, sizeof(SOCKADDR));
    listen(sListen, 5);

    printf("等待客户端连接中...\n");

    while (TRUE)
    {
        // 有新连接到来
        SOCKET sClient = accept(sListen, NULL, NULL);
        if (sClient == INVALID_SOCKET)
            continue;

        printf("已成功与客户端%d已经连接\n", (int)sClient);

        PMY_WSAOVERLAPPED client = (PMY_WSAOVERLAPPED)malloc(sizeof(MY_WSAOVERLAPPED));

        // 给缓冲区分配空间
        client->Buffer.buf = (char*)malloc(sizeof(char) * 1024);
        client->Buffer.len = 1024;
        ZeroMemory(client->Buffer.buf, client->Buffer.len);

        client->socket = sClient;
        client->dwFlag = 0;

        // 投递WSARecv请求
        int ret = WSARecv(sClient, &client->Buffer, 1, NULL, &client->dwFlag, &client->overlapped, RecvCall);
        if (ret == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING)
            Clean(&client);        
    }


    closesocket(sListen);
    WSACleanup();

    return 0;
}

// 接收消息回调函数
void CALLBACK RecvCall(DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags)
{
    /*
    * PMY_WSAOVERLAPPED->overlapped = lpOverlapped
    * 操作技巧 overlapped是第一个成员 所以首地址与PMY_WSAOVERLAPPED的首地址一样
    * 这样的好处是可以访问自定义结构的成员
    */

    PMY_WSAOVERLAPPED client = (PMY_WSAOVERLAPPED)lpOverlapped;

    // 客户端下线
    if (dwError != 0 || cbTransferred <= 0)
    {
        printf("客户端%d已下线\n", (int)client->socket);
        Clean(&client);
        return;
    }
    
    // 走到这里就说明已经收到消息了
    printf("从客户端: %d 接收到的数据为: %s\n", (int)client->socket, client->Buffer.buf);
    ZeroMemory(client->Buffer.buf, client->Buffer.len);

    // 投递WSARecv请求
    int ret = WSARecv(client->socket, &client->Buffer, 1, NULL, &client->dwFlag, &client->overlapped, RecvCall);
    if (ret == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING)
        Clean(&client);

}

// 清理函数
void Clean(PMY_WSAOVERLAPPED* client)
{
    // 检查指针是否为空,避免潜在的空指针解引用问题
    if (client == NULL)
        return;

    free((*client)->Buffer.buf);
    (*client)->Buffer.buf = NULL;
    (*client)->Buffer.len = 0;
    closesocket((*client)->socket);

    // 释放完资源后,将指针指向的内容置为NULL,以防止出现野指针
    free(*client);
    *client = NULL;
}
小故事篇

  老陈接收到新的信件后,一般的程序是:打开信封—-掏出信纸—-阅读信件—-回复信件。为了进一步减轻用户负担,微软又开发了一种新的技术:用户只要告诉微软对信件的操作步骤,微软信箱将按照这些步骤去处理信件,不再需要用户亲自拆信/阅读 /回复了!老陈终于过上了小资生活!

完成端口

  完成端口(IO Completion Port),简称IOCP,是目前window上最好的网络模型。

  下面对完成端口做一个较为详细的介绍(均来自《精通windows sockets网络开发》)

  完成端口目标是实现高效的服务器程序,它克服了并发模型的不足。其方法一是为完成端口指定并发线程的数量;二是在初始化套接字时创建一定数量的服务线程,即所谓的线程池。当客户请求到来时,这些线程立即为之服务。

  完成端口的理论基础是并行运行的线程数量必须有一个上限。这个数值是CPU的个数。如果一台计算机有两个CPU,那么多于两个可运行的线程就没有意义了。因为一旦运行线程数目超过CPU数目,系统就不得不花费时间来进行线程上下文切换,这将浪费宝贵的CPU周期。完成端口并行运行的线程数量和应用程序创建的线程数量是两个不同的概念。

  服务器应用程序需要创建多少个服务线程,这是一个很难解答的问题。一般规律是CPU数目乘以2.例如,单核CPU的电脑,套接字应用程序应该创建两个线程的线程池。

  接下来的问题是,完成端口如何实现对线程池的有效管理,使这些服务线程高效的运行起来。

  当系统完成IO操作后,向服务器完成端口发送IO Completion packet。这个过程发生在系统内部,对应用程序是不可见的。在应用程序方面,此时线程池中的线程在完成端口上排队等待IO操作的完成。如果在完成端口上没有收到IO Completion packet时,这些线程处于睡眠状态。当IO Completion packet被送到完成端口时,这些线程按照先进后出(LIFO)的方式被唤醒。

  完成端口之所以使用这种方式,其目的是为了提高性能。例如,有3个线程在完成端口上等待,当一个IO Completion packet到达后,队中最后一个线程被唤醒。该线程为客户端完成服务后,继续在完成端口上等待。如果此时又有一个IO Completion packet到达完成端口,则该线程又被唤醒,为该客户端提供服务。如果完成端口不采用FILO方式,完成端口唤醒另外一个线程,则必然进行上下文切换。通过LIFO方式,还可以使得不被唤醒的线程内存资源从缓存中清除。

  在前面讲到,应用程序需要创建一个线程池,在完成端口上进行等待。线程池中的线程数目一定要大于完成端口并发运行的线程数目,似乎应用程序创建了多余的线程,其实不然,之所以这样做是因为尽可能的保证CPU尽量忙碌。

  例如,在一台单CPU的计算机上,创建一个完成端口应用程序,为其指定并发线程数目为1.在应用程序中,创建2个线程在完成端口上等待。假如再一次为客户服务的时,被唤醒的线程因调用Sleep()之类的函数而处于阻塞状态,此时另外一个IO Completion packet被发送到完成端口上。完成端口会唤醒另一个线程为该客户端提供服务。这就是线程池中线程数量要大于完成端口指定的并发线程数量的原因。

  根据上面分析,在某些情况下,完成端口并行运行的线程数量会超过指定数量。但是,当服务线程为客户端完成服务后,在完成端口等待时,并发的线程数量还会降下来。

  总之,完成端口为套接字应用程序管理线程池,避免反复创建线程的开销,同时,根据CPU的数量决定并发线程数量,减少线程调度,从而提高服务器程序性能。

创建完成端口

  创建一个完成端口对象。创建的函数是CreateIoCompletionPort函数,此函数有两个作用:

  1.创建一个完成端口
  2.建立完成端口对象和套接字之间的联系

HANDLE WINAPI CreateIoCompletionPort(
	HANDLE    FileHandle,				// 文件句柄
	HANDLE    ExistingCompletionPort,	// 存在的完成端口句柄
	ULONG_PTR CompletionKey,			// 完成键
	DWORD     NumberOfConcurrentThreads	// 完成端口并发线程数量 0 表示 系统完成端口并发线程的数量等于CPU的数量
);										// 成功 端口句柄 失败 NULL

  而第一次调用此函数的时候,应该如下所示

// 创建IO端口
HANDLE hPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
绑定套接字

  当成功创建了IO端口,就需要再次调用此函数,将socket和它绑定起来,示例代码如下

// 创建IO端口
HANDLE hPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
SOCKET sClient = accept(sListen, (SOCKADDR*)&sockaddr, &len);
CreateIoCompletionPort((HANDLE)pClientInfo->socket, hPort, (ULONG_PTR)(pClientInfo), 0);

  现在肯定很疑惑,为什么要有个完成键,即CompletionKey,这里是pClientInfo,后面会使用GetQueuedCompletionStatus函数来获取到pClientInfo信息,也就是CompletionKey 传进来的。

获取操作结果

  获取操作结果需要使用 GetQueuedCompletionStatus,定义如下

BOOL GetQueuedCompletionStatus(
  HANDLE CompletionPort,                   // 完成端口句柄 
  LPDWORD lpNumberOfBytesTransferred,      // 返回实际传输数据的字节数
  LPDWORD lpCompletionKey,                 // 完成键指针,该指针由CreateIoCompletionPort()函数的第三个参数指定
  LPOVERLAPPED *lpOverlapped,              // 指向重叠结构的指针。通过该参数得到重叠IO操作的结果
  DWORD dwMilliseconds                     // 函数在完成端口上的等待时间
);										   // 成功返回 TRUE 失败返回 FALSE

  dwMilliseconds参数,如果在等待时间内,没有IO操作完成通知包送到完成端口,则该函数返回假,lpOverlapped参数为NULL。如果为INFINITE,则函数无限等待。如果为0,立即返回。

示例代码
#include <winsock2.h>
#include <stdio.h>
#include <MSWSock.h>
#pragma comment(lib,"ws2_32.lib")
#pragma comment(lib,"Mswsock.lib")

#define MAX_SOCKBUF 1024	

typedef enum
{
    RECV,
    SEND,
    ACCEPT
} OpType;

typedef struct {
    WSAOVERLAPPED   overlapped;
    WSABUF		    wsabuf;
    OpType          opType;
}OverlappedEx;

typedef struct {
    SOCKET			socket;
    OverlappedEx	RecvOverlappedEx;
    OverlappedEx	SendOverlappedEx;
    char		    SendBuf[MAX_SOCKBUF];
    char            RecvBuf[MAX_SOCKBUF];
}ClientInfo;

//服务线程
DWORD WINAPI ServerThread(LPVOID lpParam);

// 绑定接收函数
void BindRecv(ClientInfo* pClientInfo)
{
    DWORD dwFlag = 0;

    ZeroMemory(pClientInfo->RecvBuf, MAX_SOCKBUF);

    pClientInfo->RecvOverlappedEx.opType = RECV;
    pClientInfo->RecvOverlappedEx.wsabuf.len = MAX_SOCKBUF;
    pClientInfo->RecvOverlappedEx.wsabuf.buf = pClientInfo->RecvBuf;

    // 投递接收数据请求
    int nRet = WSARecv(pClientInfo->socket,
        &(pClientInfo->RecvOverlappedEx.wsabuf),
        1,
        NULL,
        &dwFlag,
        (LPWSAOVERLAPPED) & (pClientInfo->RecvOverlappedEx),
        NULL);

    if (nRet == SOCKET_ERROR && (WSAGetLastError() != WSA_IO_PENDING))
        printf("WSARecv fail %d \n", WSAGetLastError());
}

// 绑定发送消息函数
void SendMsg(ClientInfo* pClientInfo, char* pMsg, int nLen)
{
    ZeroMemory(pClientInfo->SendBuf, MAX_SOCKBUF);

    // 拷贝进发送缓冲区
    CopyMemory(pClientInfo->SendBuf, pMsg, nLen);

    pClientInfo->SendOverlappedEx.wsabuf.buf = pClientInfo->SendBuf;
    pClientInfo->SendOverlappedEx.wsabuf.len = nLen;
    pClientInfo->SendOverlappedEx.opType = SEND;

    // 投递发送数据请求
    int nRet = WSASend(pClientInfo->socket,
        &(pClientInfo->SendOverlappedEx.wsabuf),
        1,
        NULL,
        0,
        (LPWSAOVERLAPPED) & (pClientInfo->SendOverlappedEx),
        NULL);

    if (nRet == SOCKET_ERROR && (WSAGetLastError() != WSA_IO_PENDING))
        printf("WSASend fail %d \n", WSAGetLastError());
}

void CloseSocket(ClientInfo* pClientInfo)
{
    closesocket(pClientInfo->socket);
    free(pClientInfo);
}

int main()
{
    WSADATA wsa;
    WSAStartup(MAKEWORD(2, 2), &wsa);

    // 创建socket
    SOCKET sListen = WSASocketW(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
    if (sListen == INVALID_SOCKET)
        return 0;

    // 监听端口
    int port = 5899;
    // 接收数据
    SOCKADDR_IN local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    // 接收任意ip数据
    local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

    bind(sListen, (SOCKADDR*)&local, sizeof(SOCKADDR_IN));
    listen(sListen, 5);

    printf("等待客户端连接中...\n");

    // 创建IO端口
    HANDLE hPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
    if (hPort == NULL)
    {
        printf("创建IO端口错误 %d\n", GetLastError());
        return 0;
    }


    SYSTEM_INFO sysInfo;
    // 获取cpu的核心数
    GetSystemInfo(&sysInfo);

    // 创建线程池,线程的个数为cpu线程数 * 2
    // 我的cpu是6核12线程,这里的 sysInfo.dwNumberOfProcessors = 12
    for (DWORD i = 0; i < sysInfo.dwNumberOfProcessors * 2; i++)
    {
        HANDLE hThread = CreateThread(NULL, 0, ServerThread, (LPVOID)hPort, 0, NULL);
        CloseHandle(hThread);
    }

    // 创建结构体
    ClientInfo* pClientInfo = (ClientInfo*)malloc(sizeof(ClientInfo));
    if (pClientInfo == NULL)
        return 0;

    ZeroMemory(&pClientInfo->RecvOverlappedEx, sizeof(OverlappedEx));
    ZeroMemory(&pClientInfo->SendOverlappedEx, sizeof(OverlappedEx));

    while (TRUE)
    {
        // 有新连接到来
        SOCKADDR_IN sockaddr;
        int len = sizeof(sockaddr);
        SOCKET sClient = accept(sListen, (SOCKADDR*)&sockaddr, &len);
        if (sClient == INVALID_SOCKET)
            continue;

        printf("已成功与客户端%d进行连接\n", (int)sClient);

        ClientInfo* pClientInfo = (ClientInfo*)malloc(sizeof(ClientInfo));
        if (pClientInfo == NULL)
            continue;

        ZeroMemory(&pClientInfo->RecvOverlappedEx, sizeof(OverlappedEx));
        ZeroMemory(&pClientInfo->SendOverlappedEx, sizeof(OverlappedEx));

        pClientInfo->socket = sClient;

        CreateIoCompletionPort((HANDLE)pClientInfo->socket, hPort, (ULONG_PTR)(pClientInfo), 0);
        // 投递接收消息请求
        BindRecv(pClientInfo);
    }

    return 0;
}


DWORD WINAPI ServerThread(LPVOID lpParam)
{
    HANDLE hPort = (HANDLE)lpParam;
    ClientInfo* pClientInfo = NULL;

    DWORD dwIoSize = 0;
    LPOVERLAPPED lpOverlapped = NULL;

    while (TRUE)
    {
        BOOL bSuccess = GetQueuedCompletionStatus(hPort, &dwIoSize, (PULONG_PTR)&pClientInfo, &lpOverlapped, INFINITE);
        // 出错
        if (TRUE == bSuccess && 0 == dwIoSize && NULL == lpOverlapped)
            break;

        if (NULL == lpOverlapped)
            continue;

        // 客户端下线
        if (FALSE == bSuccess || (0 == dwIoSize && TRUE == bSuccess))
        {
            printf("客户端 %d 下线\n", (int)pClientInfo->socket);
            CloseSocket(pClientInfo);
            continue;
        }

        OverlappedEx* pOverlappedEx = (OverlappedEx*)lpOverlapped;
        // 收到消息了
        if (pOverlappedEx->opType == RECV)
        {
            printf("从客户端: %d 接收到的数据为: %s\n", (int)pClientInfo->socket, pClientInfo->RecvBuf);
            // 将收到的消息,原封不动,发给客户端
            SendMsg(pClientInfo, pClientInfo->RecvBuf, dwIoSize);

            // 继续投递接收消息
            BindRecv(pClientInfo);
        }
        else if (pOverlappedEx->opType == SEND)
        {
            printf("给客户端:%d 发送的信息为:%s\n", (int)pClientInfo->socket, pClientInfo->SendBuf);
        }
        else
        {
            printf("socket(%d) \n", (int)pClientInfo->socket);
        }
    }

    return 0;
}

  如果要让ServerThread线程正常退出,应该使用PostQueuedCompletionStatus函数,而且这里的accept函数,可以换成全异步工作的AcceptEx函数,连接效率可以更高!总的来说,开发一个健壮性高的网络服务器,不是一个简单的事情,而IOCP还有非常多的细节需要去探讨,大名鼎鼎的ghost远控就使用了IOCP,各位有兴趣的可以去学习。

小故事篇

  微软信箱似乎很完美,老陈也很满意。但是在一些大公司情况却完全不同!这些大公司有数以万计的信箱,每秒钟都有数以百计的信件需要处理,以至于微软信箱经常因超负荷运转而崩溃!需要重新启动!微软不得不使出杀手锏。微软给每个大公司派了一名名叫”Completion Port”的超级机器人,让这个机器人去处理那些信件!

  Windows NT小组注意到这些应用程序的性能没有预料的那么高。特别的,处理很多同时的连接请求,意味着很多线程并发地运行在系统中。因为所有这些线程都是可运行的 [没有被挂起和等待发生什么事],Microsoft意识到NT内核花费了太多的时间来转换运行线程的上下文[Context],线程就没有得到很多 CPU时间来做它们的工作。大家可能也都感觉到并行模型的瓶颈在于它为每一个客户请求都创建了一个新线程。创建线程比起创建进程开销要小,但也远不是没有开销的。我们不妨设想一下:如果事先开好N个线程,让它们在那hold[堵塞],然后可以将所有用户的请求都投递到一个消息队列中去。然后那N个线程逐一从消息队列中去取出消息并加以处理。就可以避免针对每一个用户请求都开线程。不仅减少了线程的资源,也提高了线程的利用率。理论上很不错,你想我等泛泛之辈都能想出来的问题,Microsoft又怎会没有考虑到呢?

查看评论 -
评论
文章目录
  1. 前言
  2. 进程
    1. 什么是进程
    2. 进程执行的加载过程
    3. 创建进程
    4. 结束进程
    5. 挂起方式创建进程
    6. 遍历进程
    7. 其他API
  3. 线程
    1. 什么是线程
    2. 创建线程
    3. 线程传参
    4. 挂起线程
    5. 恢复线程
    6. 终止线程
    7. 等待线程结束
    8. 综合代码
    9. 线程安全
  4. 临界区
    1. 实现
    2. 误用
    3. 死锁
  5. 事件
    1. 手动
    2. 自动
  6. 线程池
    1. 异步方式
    2. 定时器
    3. 内核对象
    4. 异步IO
  7. 内存
    1. 申请内存的两种方式
    2. 内存页面三种状态
    3. 私有内存
      1. 申请和释放
      2. 查询内存块信息
      3. 更改保护属性
    4. 读内存
    5. 写内存
  8. 文件系统
      1. 获取所有卷
      2. 获取卷类型
      3. 获取卷信息
    1. 目录
      1. 创建目录
      2. 删除目录
      3. 移动目录
      4. 获取程序当前目录
      5. 设置程序当前目录
      6. 综合代码
    2. 文件
      1. 创建文件
      2. 获取文件大小
      3. 其他API
  9. 进程通信
    1. socket
      1. 发送方
      2. 接收方
    2. 命名管道
      1. 服务端
      2. 客户端
    3. http
      1. 初始化
      2. 指定服务器
      3. 创建请求
      4. 发送请求
      5. 接收响应
      6. 获取数据
      7. 完整代码
  10. 注册表
    1. 子键
      1. 打开
      2. 创建
      3. 删除
    2. 键值
      1. 查询
      2. 增加&修改
      3. 删除
      4. 枚举
  11. 系统服务
    1. 打开
    2. 枚举
    3. 查询
      1. 服务配置
      2. 服务状态
    4. 修改
      1. 服务配置
      2. 服务状态
    5. 创建
    6. 删除
  12. 动态链接库
    1. 创建链接库
    2. 无参调用
      1. 方法一
      2. 方法二
    3. 有参调用
      1. 方法一
      2. 方法二
    4. DllMain
  13. 网络模型
    1. Select
      1. 初始化
      2. 添加
      3. 删除
      4. 查询
      5. select()函数
      6. 单个示例
      7. 完整示例
      8. 小故事篇
    2. 事件选择
      1. 创建事件对象
      2. 绑定事件对象
      3. 是否发生事件
      4. 获取事件类型
      5. 完整示例
      6. 最终优化示例
      7. 小故事篇
    3. 重叠IO
      1. 事件通知
        1. 创建套接字
        2. OVERLAPPED结构体
        3. 接收消息
        4. 获取通知
        5. 示例代码
        6. 小故事篇
      2. 完成例程
        1. 例程函数
        2. 示例代码
        3. 小故事篇
    4. 完成端口
      1. 创建完成端口
      2. 绑定套接字
      3. 获取操作结果
      4. 示例代码
      5. 小故事篇