Windows API 教程(十) 注册表操作

注册表在系统中也是一个很重要的部分。通过 win + R -> regedit 可以调出系统自带的注册表编辑
regedit

可以看到系统常用的注册表有五个大类:

名称 描述
HKEY_CLASSES_ROOT 用于存储文档类型(type or classes)及其相关联的特性(如图标、打开方式等)。该类注册表中的信息常用于 Shell 和 COM 应用程序。
HKEY_CURRENT_USER 用于存储当前用户的偏好(preferences)设置。其中包括当前用户的环境变量、项目组数据、颜色、字体、网络连接、应用程序偏好。
HKEY_LOCAL_MACHINE

存储计算机的物理状态,包括总线类型数据、系统内存、已安装的硬件和软件。

HKEY_USERS 存储用于新注册用户的默认配置以及当前用户的配置。
HKEY_CURRENT_CONFIG

包含本地计算机系统的当前硬件简介信息。其中保存的信息仅描述当前硬件配置和标准配置之间的差异. 标准硬件配置的信息保存在 HKEY_LOCAL_MACHINESoftwareSystem 键之中.

HKEY_CURRENT_CONFIGHKEY_LOCAL_MACHINESystemCurrentControlSetHardware ProfilesCurrent 的别名.

更多信息,详见 注册表预定义键(Predefined Keys)

RegOpenKeyEx

打开已存在的注册表项,更多信息详见 RegOpenKeyEx On MSDN

函数原型

LONG WINAPI RegOpenKeyEx(
  _In_        HKEY hKey,        // 要打开的键的句柄
  _In_opt_    LPCTSTR lpSubKey, // 其子项
  _Reserved_  DWORD ulOptions,  // 保留位,无用,请无视
  _In_        REGSAM samDesired,// 打开权限
  _Out_       PHKEY phkResult   // 返回打开的句柄
);

参数

参数一 hKey (输入参数)
可以传入已存在的注册表句柄(HKEY类型),也可以传入默认存在的项如:HKEY_CLASSES_ROOTHKEY_CURRENT_USERHKEY_LOCAL_MACHINEHKEY_USERSHKEY_CURRENT_CONFIG

参数二 lpSubKey (输入参数、可选)

子项字符串,可不填。如果有填写的话,那么久相当于打开 “hKeylpSubKey” (参数一与参数二拼接起来) 的注册表项。

参数三 ulOptions (保留参数)

保留参数,必须填 0。

参数四 samDesired (输入参数)

打开的注册表项句柄的权限。常见如下:

权限名 描述
KEY_READ (0x20019) 读取权限
KEY_WRITE (0x20006) 写入权限
KEY_SET_VALUE (0x0002) 创建、删除或修改
KEY_QUERY_VALUE (0x0001) 查询值
KEY_CREATE_LINK (0x0020) 系统保留(无用)
KEY_CREATE_SUB_KEY (0x0004) 注册键和创建子键
KEY_ENUMERATE_SUB_KEYS (0x0008) 遍历子键
KEY_EXECUTE (0x20019) 相当于KEY_READ
KEY_NOTIFY (0x0010)
KEY_WOW64_32KEY (0x0200)
KEY_WOW64_64KEY (0x0100)
KEY_ALL_ACCESS (0xF003F) 所有权限

详细请参见 MSDN:注册表项的安全和访问权限

参数五 phkResult (输出参数)

返回输出的句柄。

返回值

成功返回 ERROR_SUCCESS 即 0,失败则返回错误码。可以使用 FormatMessage 函数读取相应错误码的信息。

RegCreateKey

创建注册表项,详见 RegCreateKey on MSDN

LONG WINAPI RegCreateKey(
  _In_      HKEY hKey,          // 创建在哪个注册表项之下
  _In_opt_  LPCTSTR lpSubKey,   // 项名
  _Out_     PHKEY phkResult     // 返回创建的句柄
);

参数

不赘述,不明的同学可以参见稍后的例子。

返回值

同 RegOpenKeyEx 的返回值描述。

创建注册表项实例

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

void showErrorText(DWORD error_num);

int main()
{
    HKEY hKey;
    HKEY subKey;
    DWORD result;
    char sname[] = "Write some thing as you like"; // 随便写点什么

    //打开注册表, HKEY_CURRENT_USERSoftware
    result = RegOpenKeyEx(
                 HKEY_CURRENT_USER, "Software", // 打开
                 0,              // 保留参数必须填 0
                 KEY_WRITE,      // 打开权限,写入
                 &hKey           // 打开之后的句柄
             );

    if (result == ERROR_SUCCESS)
    {
        printf("open success!n");
    }
    else
    {
        printf("open failed!n");
        showErrorText(result);
    }

    // 添加注册表项, 即 HKEY_CURRENT_USERSoftwareAnywayCompany
    RegCreateKey(hKey, "AnywayCompany", &subKey);

    // 设置 HKEY_CURRENT_USERSoftwareAnywayCompanyName 的值
    result = RegSetValueEx(
                 subKey,
                 "Name",         // Name字段
                 0,              // 保留参数必须填 0
                 REG_SZ,         // 键值类型为字符串
                 (const unsigned char *)sname, // 字符串首地址
                 sizeof(sname)   // 字符串长度
             );

    if (result == ERROR_SUCCESS)
    {
        printf("set success!n");
    }
    else
    {
        printf("set failed!n");
        showErrorText(result);
    }

    //关闭注册表:
    RegCloseKey(hKey);
    // 暂停
    system("pause");
    return 0;
}

/*
 * 根据错误码输出错误信息
 */
void showErrorText(DWORD error_num)
{
    char *msg = NULL;
    FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        error_num,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // 使用默认语言
        (LPSTR)&msg,
        0,
        NULL
    );

    printf("Error code %d: ", error_num);
    if (msg == NULL)
        printf("%sn", "Unknown error");
    else
        printf("%sn", msg);
}

截图:

PS:打开 HKEY_CURRENT_USER 中的项来操作是不需要管理员权限的。

RegDeleteValue

删除某个注册表项的值,更多信息详见 RegDeleteValue on MSDN

LONG WINAPI RegDeleteValue(
  _In_      HKEY hKey,          // 要删除目标注册表项的主体
  _In_opt_  LPCTSTR lpValueName // 具体要删除的值
);

删除注册表项值的实例

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

void showErrorText(DWORD error_num);

int main()
{
    HKEY hKey;
    HKEY subKey;
    DWORD result;
    char sname[] = "Write some thing as you like"; // 随便写点什么

    //打开注册表
    result = RegOpenKeyEx(
                 // 打开 HKEY_CURRENT_USERSoftwareAnywayCompany
                 HKEY_CURRENT_USER, "Software\AnywayCompany", 
                 0,              // 保留参数必须填 0
                 KEY_WRITE,      // 打开权限,写入
                 &hKey           // 打开之后的句柄
             );

    if (result == ERROR_SUCCESS)
    {
        printf("open success!n");
    }
    else
    {
        printf("open failed!n");
        showErrorText(result);
    }

    // 删除 HKEY_CURRENT_USERSoftwareAnywayCompany 下的 Name
    result = RegDeleteValue(hKey, "Name");

    if (result == ERROR_SUCCESS)
    {
        printf("delete success!n");
    }
    else
    {
        printf("delete failed!n");
        showErrorText(result);
    }

    //关闭注册表:
    RegCloseKey(hKey);
    // 暂停
    system("pause");
    return 0;
}

/*
 * 根据错误码输出错误信息
 */
void showErrorText(DWORD error_num)
{
    char *msg = NULL;
    FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        error_num,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // 使用默认语言
        (LPSTR)&msg,
        0,
        NULL
    );

    printf("Error code %d: ", error_num);
    if (msg == NULL)
        printf("%sn", "Unknown error");
    else
        printf("%sn", msg);
}

RegDeleteKey

删除注册表中的某个键, RegDeleteKey on MSDN

LONG WINAPI RegDeleteKey(
  _In_  HKEY hKey,          // 要删除目标注册表项的主体
  _In_  LPCTSTR lpSubKey    // 具体要删除的键
);

删除注册表项键的实例

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

void showErrorText(DWORD error_num);

int main()
{
    HKEY hKey;
    HKEY subKey;
    DWORD result;
    char sname[] = "Write some thing as you like"; // 随便写点什么

    //打开注册表
    result = RegOpenKeyEx(
                 // 打开 HKEY_CURRENT_USERSoftwareAnywayCompany
                 HKEY_CURRENT_USER, "Software", 
                 0,              // 保留参数必须填 0
                 KEY_WRITE,      // 打开权限,写入
                 &hKey           // 打开之后的句柄
             );

    if (result == ERROR_SUCCESS)
    {
        printf("open success!n");
    }
    else
    {
        printf("open failed!n");
        showErrorText(result);
    }

    // 删除 HKEY_CURRENT_USERSoftwareAnywayCompany
    result = RegDeleteKey(hKey, "AnywayCompany");

    if (result == ERROR_SUCCESS)
    {
        printf("delete success!n");
    }
    else
    {
        printf("delete failed!n");
        showErrorText(result);
    }

    //关闭注册表:
    RegCloseKey(hKey);
    // 暂停
    system("pause");
    return 0;
}

/*
 * 根据错误码输出错误信息
 */
void showErrorText(DWORD error_num)
{
    char *msg = NULL;
    FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        error_num,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // 使用默认语言
        (LPSTR)&msg,
        0,
        NULL
    );

    printf("Error code %d: ", error_num);
    if (msg == NULL)
        printf("%sn", "Unknown error");
    else
        printf("%sn", msg);
}

列举某个注册表项之下的所有子项实例

内容有些复杂,不过平常都是直接 copy 这个 RegCloseKey 函数过去用,大家知道去哪里 copy 就行了。

// 出处:http://msdn.microsoft.com/en-us/library/ms724256(v=vs.85).aspx
// QueryKey - 列举目标注册表项之下关联的子项
//     hKey - Key whose subkeys and values are to be enumerated.

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

#define MAX_KEY_LENGTH 255
#define MAX_VALUE_NAME 16383

void QueryKey(HKEY hKey)
{
    TCHAR    achKey[MAX_KEY_LENGTH];   // buffer for subkey name
    DWORD    cbName;                   // size of name string
    TCHAR    achClass[MAX_PATH] = TEXT("");  // buffer for class name
    DWORD    cchClassName = MAX_PATH;  // size of class string
    DWORD    cSubKeys = 0;             // number of subkeys
    DWORD    cbMaxSubKey;              // longest subkey size
    DWORD    cchMaxClass;              // longest class string
    DWORD    cValues;              // number of values for key
    DWORD    cchMaxValue;          // longest value name
    DWORD    cbMaxValueData;       // longest value data
    DWORD    cbSecurityDescriptor; // size of security descriptor
    FILETIME ftLastWriteTime;      // last write time

    DWORD i, retCode;

    TCHAR  achValue[MAX_VALUE_NAME];
    DWORD cchValue = MAX_VALUE_NAME;

    // Get the class name and the value count.
    retCode = RegQueryInfoKey(
                  hKey,                    // key handle
                  achClass,                // buffer for class name
                  &cchClassName,           // size of class string
                  NULL,                    // reserved
                  &cSubKeys,               // number of subkeys
                  &cbMaxSubKey,            // longest subkey size
                  &cchMaxClass,            // longest class string
                  &cValues,                // number of values for this key
                  &cchMaxValue,            // longest value name
                  &cbMaxValueData,         // longest value data
                  &cbSecurityDescriptor,   // security descriptor
                  &ftLastWriteTime);       // last write time

    // Enumerate the subkeys, until RegEnumKeyEx fails.

    if (cSubKeys)
    {
        printf( "nNumber of subkeys: %dn", cSubKeys);

        for (i = 0; i < cSubKeys; i++)
        {
            cbName = MAX_KEY_LENGTH;
            retCode = RegEnumKeyEx(hKey, i,
                                   achKey,
                                   &cbName,
                                   NULL,
                                   NULL,
                                   NULL,
                                   &ftLastWriteTime);
            if (retCode == ERROR_SUCCESS)
            {
                printf(TEXT("(%d) %sn"), i + 1, achKey);
            }
        }
    }

    // Enumerate the key values.

    if (cValues)
    {
        printf( "nNumber of values: %dn", cValues);

        for (i = 0, retCode = ERROR_SUCCESS; i < cValues; i++)
        {
            cchValue = MAX_VALUE_NAME;
            achValue[0] = '\0';
            retCode = RegEnumValue(hKey, i,
                                   achValue,
                                   &cchValue,
                                   NULL,
                                   NULL,
                                   NULL,
                                   NULL);

            if (retCode == ERROR_SUCCESS )
            {
                printf(TEXT("(%d) %sn"), i + 1, achValue);
            }
        }
    }
}

int main(void)
{
    HKEY hTestKey;

    if ( RegOpenKeyEx(
                // 打开 HKEY_CURRENT_USERSOFTWARE\Microsoft
                HKEY_CURRENT_USER, TEXT("SOFTWARE\Microsoft"),
                0,
                KEY_READ,
                &hTestKey) == ERROR_SUCCESS
       )
    {
        // 如果打开成功, 则遍历其下的子项
        QueryKey(hTestKey);
    }

    RegCloseKey(hTestKey);

    system("pause");
}

你可能感兴趣的:

文章索引

上一讲: Windows API 教程(九) 网络编程

Advertisements

Windows API 教程(九) 网络编程

茵蒂克丝

基础概念

IP 地址

IP 是英文 Internet Protocol (网络之间互连的协议)的缩写,也就是为计算机网络相互连接进行通信而设计的协议。任一系统,只要遵守 IP协议就可以与因特网互连互通。

所谓IP地址就是给每个遵循tcp/ip协议连接在Internet上的主机分配的一个32bit地址。按照TCP/IP协议规定,IP地址用二进制来表示,每个IP地址长32bit,比特换算成字节,就是4个字节。为了方便人们的使用,IP地址经常被写成十进制的形式,中间使用符号 “.” 分开不同的字节,如:192.168.1.1。在因特网中,主机的标识便是它ip地址。

常见的获取服务器ip的方法是使用系统自带的ping命令。例,打开cmd输入:
ping http://www.baidu.com

输出:
ping_baidu

其中的 115.239.210.27 就是baidu服务器的地址了

服务端与客户端

服务端与客户端在计算机的世界里,凡是提供服务的一方我们称为服务端(Server),而接受服务的另一方我们称作客户端(Client)。

网站提供网页数据的服务,使用者接受网站所提供的数据服务,所以使用者在这里就是客户端,响应使用者要求的网站即称为服务端。

不过客户端及服务端的关系不见得一定建立在两台分开的机器上,同一台机器中也有这种主从关系的存在,提供服务的服务端及接受服务的客户端也有可能都在同一台机器上。

例如我们在提供网页的服务器上执行浏览器浏览本机所提供的网页,这样在同一台机器上就同时扮演服务端及客户端。

Socket

socket的英文原义是“孔”或“插座”。作为4BDS UNIX的进程通信机制,取后一种意思。通常也称作”套接字”,用于描述IP地址和端口,是一个通信链的句柄。

Scoket程勋分为服务端与客户端,服务端程序监听端口,等待客户端程序的连接。客户端程序发起连接,等待服务端的相应。客户端想要连接上服务端的话,需要 知道服务端的ip地址。

例如,客户想要访问百度网站的首页,通过浏览器访问http://www.baidu.com。浏览器发出请求之后,先是DNS服务器将百度的域名解析成ip地址之后,访问到ip地址为115.239.210.27服务器的80端口(http协议默认在80端口),请求发送后,百度的服务器作为响应将页面的源代码发送至客户端(通过浏览器右键->源代码,或者ctrl+u可以看到服务器返回的页面源代码),随后浏览器将源代码渲染成页面。这样用户就看到了百度网站的首页。

头文件和库文件

在windows系统上,当前的Windows Socket接口是Windows Sockets 2,所有接口函数都是有Ws2_32.dll导出。在进行程序设计时,祥光的数据类型、结构定义、函数声明等卫浴头文件winsock2.h中,编译时需要包含此文件,并且连接到库Ws2_32.lib。

常用函数

常见的socket函数

  • WSAStartup 初始化Ws2_32.lib库
  • WSACleanup 关闭Ws2_32.lib库
  • Socket 创建socket
  • closesocket 关闭socket
  • sockaddr和sockaddr_in 结构体socket地址
  • bind 绑定
  • listen 监听
  • accept 接收
  • connect 连接
  • send 发送
  • recv 获取返回
  • WSAStartup () 函数

    WSAStartup函数的功能使加载Ws2_32.dll等Socket程序运行的环境。在程序初始化后,Socket程序运行所依赖的动态链接库不一定已经在家,WSAStartup保证了Socket 动态链接库的加载。

    int WSAStartup(
    __in WORD wVersionRequested,
    __out LPWSADATA lpWSAData
    ); 
    

    参数一 wVersionRequested
    Socket程序库的版本,一般使用MAKEWORD(2,2)宏生成。

    参数二 lpWSAData
    输出参数,指向 WSADATA 结构的指针,用于返回Socket库初始化的信息。

    返回值 用来判断程序是否成功加载

    WSACleanup () 函数

    与 WSAStartup 相反, WSACleanup 释放Ws2_32.dll,函数无参数。

    Socket () 函数

    Socket函数的功能是建立一个绑定到制定协议和传输类型的Socket。

    SOCKET WSAAPI socket(
    __in int af,
    __in int type,
    __in int protocol
    );
    

    参数一 af
    address family的缩写,制定该Socket使用哪一种类型的网络地址。可以是IPv4(AF_INET),也可以是IPv6(AF_INET6)、蓝牙(AF_BTM)、NetBios(AF_NETBIOS)等。

    参数二 type
    设置传输类型,常见类型有 SOCK_STREAMSOCK_DGRAMSOCK_RAMSOCK_SEQPACKET 等。SOCK_STREAM 类型是基于连接的(TCP),所收的数据时数据流形式的,传输层的数据包已经经过处理。SOCK_DGRAM是基于报文(UDP)。如果制定为SOCK_RAM,那么可以建立原始套接字,所收到的数据是以数据包(包括包头)的形式存在的。

    参数三 protocol
    设置传输协议,常用的协议为 IPPROTO_TCPIPPROTO_UDP

    colsesocket() 函数

    与 socket 函数对应,用于关闭 socket

    int WSAAPI closesocket(
     __in SOCKET s
     );
     

    参数一 s
    指定要关闭的socket

    sockaddrsockaddr_in 结构体

    sockaddrSOCKADDRsockaddr_inSOCKADDR_IN 这 几个结构用于表示地址很端口。在IPv4下,这几个结构体是可以通用的。

    typedef struct sockaddr {
    
    #if (_WIN32_WINNT < 0x0600)
        u_short sa_family;
    #else 
        ADDRESS_FAMILY sa_family; // Address family.
    #endif //(_WIN32_WINNT < 0x0600)
    
        CHAR sa_data[14]; // Up to 14 bytes of direct address.
    } SOCKADDR, *PSOCKADDR, FAR *LPSOCKADDR;
    
    typedef struct sockaddr_in {
    
    #if(_WIN32_WINNT < 0x0600)
        short sin_family; 
    #else //(_WIN32_WINNT < 0x0600)
        ADDRESS_FAMILY sin_family;
    #endif //(_WIN32_WINNT < 0x0600)
    
        USHORT sin_port;
        IN_ADDR sin_addr;
        CHAR sin_zero[8];
    } SOCKADDR_IN, *PSOCKADDR_IN;
    

    bind() 函数

    服务器端,将Socket与网络地址和端口绑定起来,函数原型如下:

    int WSAAPI bind(
    __in SOCKET s,
    __in_bcount(namelen) const struct sockaddr FAR * name,
    __in int namelen
    );
    

    参数一 s
    要绑定的socket

    参数二 name
    要绑定的网络地址结构体

    参数三 namelen
    name参数所指向的结构体的大小

    listen() 函数

    设置 socket 的状态为监听,使客户端程序可以进行连接,函数原型:

    int WSAAPI listen(
    __in SOCKET s,
    __in int backlog
    );
    

    参数一 s
    绑定的socket

    参数二 backlog
    指定最大的连接数

    accept() 函数

    接受客户端的连接

    SOCKET WSAAPI accept(
    __in SOCKET s,
    __out_bcount_opt(*addrlen) struct sockaddr FAR * addr,
    __inout_opt int FAR * addrlen
    );
    

    参数一 s
    正在监听的socket,该 socket 使用 listen() 设置。

    参数二 addr (可选)
    指针,指向一缓冲区,其中接收为通讯层所知的连接实体的地址。Addr参数的实际格式由套接口创建时所产生的地址族确定。

    参数三 addrlen (可选)
    指针,指向存有addr地址长度的整形数。

    返回:
    成功:非负描述字
    失败:-1

    accept 默认会阻塞进程,直到有客户端建立连接后返回,它返回的是连接用的 socket 。如果 accept 成功返回,则服务器与客户已经正确建立连接了,此时服务器通过 accept 返回的 socket 来完成与客户的通信。

    connect() 函数

    connect函数的功能使与服务器建立连接。这个函数只能由客户端程序调用。

    int WSAAPI connect(
    __in SOCKET s,
    __in_bcount(namelen) const struct sockaddr FAR * name,
    __in int namelen
    );
    

    参数一 s
    socket 函数建立的套接字。

    参数二 name
    指向sockaddr结构体指针,包括了所要连接的服务端的地址和端口等。

    参数三 namelen
    sockaddr结构的长度

    send() 函数

    向连接的另一端发送数据,不论是客户还是服务器应用程序都用 send 函数来向 TCP 连接的另一端发送数据。

    int WSAAPI send(
    __in SOCKET s,
    __in_bcount(len) const char FAR * buf,
    __in int len,
    __in int flags
    );
    

    参数一 s
    指定发送端socket描述符。

    参数二 *buf
    指明存放要发送的数据的缓冲区。

    参数三 len
    指明实际要发送的数据的字节数。

    参数四 flags
    指明发送的方法,常见宏MSG_DONTROUTEMSG_OOB等,一般置0。

    将*buf指向的字符串发送至客户端

    recv() 函数

    不论是客户还是服务器应用程序都用 recv 函数 从 TCP 连接的另一端接收数据。

    int WSAAPI recv(
    __in SOCKET s,
    __out_bcount_part(len, return) __out_data_source(NETWORK) char FAR * buf,
    __in int len,
    __in int flags
    );
    

    参数一 s
    指定接收端socket描述符;

    参数二 *buf
    指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;

    参数三 len
    指明buf的长度;

    参数四 flags
    指明接收的方法,常见宏 MSG_DONTROUTEMSG_OOB 等,一般置0。
    获取到客户端返回的字符串并将其写入到buf中

    简单的 socket 通信示例

    服务端(Server)

    #include <Winsock2.h>
    #include <stdio.h>
    #pragma comment(lib, "ws2_32.lib")
    
    void main()
    {
        int err; // 错误信息
        int len;
    
        char sendBuf[100]; // 发送至客户端的字符串
        char recvBuf[100]; // 接受客户端返回的字符串
    
        SOCKET sockServer;     // 服务端 Socket
        SOCKADDR_IN addrServer;// 服务端地址
        SOCKET sockClient;     // 客户端 Scoket
        SOCKADDR_IN addrClient;// 客户端地址
    
        WSADATA wsaData;       // winsock 结构体
        WORD wVersionRequested;// winsock 的版本
    
        // 配置 Windows Socket版本
        wVersionRequested = MAKEWORD( 2, 2 );
    
        // 初始化 Windows Socket
        err = WSAStartup( wVersionRequested, &wsaData );
    
        if ( err != 0 )
        {
            // 启动错误,程序结束
            return;
        }
    
        /*
         * 确认WinSock DLL支持2.2
         * 请注意如果DLL支持的版本大于2.2至2.2
         * 它仍然会返回wVersion2.2的版本
         */
    
        if ( LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 2 )
        {
            // 启动错误,程序结束
            WSACleanup(); // 终止Winsock 2 DLL (Ws2_32.dll) 的使用
            return;
        }
    
        // 定义服务器端socket
        sockServer = socket(AF_INET, SOCK_STREAM, 0);
    
        //  设置服务端 socket
        addrServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // 本机IP
        addrServer.sin_family = AF_INET;                   // 协议类型是INET
        addrServer.sin_port = htons(6000);                 // 绑定端口6000
    
        // 将服务器端socket绑定在本地端口
        bind(sockServer, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));
    
        // Listen 监听端口
        listen(sockServer, 5); // 5 为等待连接数目
    
        printf("服务器已启动:n监听中...n");
    
        len = sizeof(SOCKADDR);
    
        // accept 会阻塞进程,直到有客户端连接上来为止
        sockClient = accept(sockServer, (SOCKADDR *)&addrClient, &len);
        // 当客户端连接上来时, 拼接如下字符串        
        sprintf(sendBuf, "欢迎 ip: %s 的用户连接, 这里是 Lellansin 的服务器n", inet_ntoa(addrClient.sin_addr));
        // 向客户端发送字符串
        send(sockClient, sendBuf, strlen(sendBuf) + 1, 0);
        // 获取客户端返回的数据
        recv(sockClient, recvBuf, 100, 0);
        // 打印客户端返回的数据
        printf("%sn", recvBuf);
        // 关闭socket
        closesocket(sockClient);
    
        getchar();
    }
    

    启动后防火墙有阻挡提示,这里接受就可以了。另外下图是原来博主测试时截的图,跟上面代码的输出部分稍稍有些区别。

    tcp_server_test

    客户端(Client)

    #include <Winsock2.h>
    #include <stdio.h>
    #pragma comment(lib, "ws2_32.lib")
    
    void main()
    {
    	int err;
    	char *message;
    	char recvBuf[100];
    
    	SOCKET sockClient; // 客户端 Scoket
    	SOCKADDR_IN addrServer; // 服务端地址
    
    	WSADATA wsaData;
    	WORD wVersionRequested;
    
    	wVersionRequested = MAKEWORD( 2, 2 );
    
    	err = WSAStartup( wVersionRequested, &wsaData );
    
    	if ( err != 0 )
    	{
    		return;
    	}
    
    	if ( LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 2 )
    	{
    		// 启动错误,程序结束
    		WSACleanup( );
    		return;
    	}
    
    	// 新建客户端 scoket
    	sockClient = socket(AF_INET, SOCK_STREAM, 0);
    
    	// 定义要连接的服务端地址
    	addrServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");  // 目标IP (127.0.0.1是本机地址)
    	addrServer.sin_family = AF_INET;                           // 协议类型是INET
    	addrServer.sin_port = htons(6000);                         // 连接端口1234
    
    	// 让 sockClient 连接到 服务端
    	connect(sockClient, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));
    
    	// 从服务端获取数据
    	recv(sockClient, recvBuf, 100, 0);
    
    	// 打印数据
    	printf("%sn", recvBuf);
    
    	message = "服务端你好~";
    
    	// 发送数据到服务端
    	send(sockClient, message, strlen(message) + 1, 0);
    
    	// 关闭socket
    	closesocket(sockClient);
    	WSACleanup();
    
    	getchar(); // 暂停
    }
    

    tcp_client_test

    客户端与服务端更多通信,例子:《木马,你好!(二)最简单的木马》

    简单的端口扫描程序

    hostent结构体

    host entry的缩写,该结构记录主机的信息,包括主机名、别名、地址类型、地址长度和地址列表。之所以主机的地址是一个列表的形式,原因是当一个主机有多个网络接口时会有多个地址。

    struct hostent {
    	char FAR * h_name; 				/* 主机正式名称 */
    	char FAR * FAR * h_aliases; 	/* 主机别名 */
    	short h_addrtype; 				/* 地址类型 */
    	short h_length; 				/* 地址长度 */
    	char FAR * FAR * h_addr_list; 	/* 主机网络地址指针 */
    	#define h_addr h_addr_list[0] 	/* 指向第一个地址 */
    };
    

    代码

    #include <winsock2.h>
    #include <stdio.h>
    #include <stdlib.h>
    #pragma comment(lib,"ws2_32.lib")
    
    void Help( void );
    
    int main(int argc, char *argv[])
    {
    	WORD wVersion = MAKEWORD(2, 0);
    	WSADATA wsaData;
    
    	//sockaddr_in结构体
    	struct sockaddr_in sin;
    
    	int iFromPort;
    	int iToPort;
    	int iNowPort;
    	char *cHost;
    
    	HOSTENT *host_entry;
    	char *host_name;
    	char host_address[256];
    
    	SOCKET s;
    
    	int iOpenPort = 0;
    	int port[256], i;
    
    	if (argc != 4)
    	{
    		Help();
    		return -1;
    	}
    
    	iFromPort = atoi(argv[2]);
    	iToPort = atoi(argv[3]);
    	host_name = argv[1];
    
    	if (iFromPort > iToPort || iFromPort < 0 || iFromPort > 65535 || iToPort < 0 || iToPort > 65535)
    	{
    		printf("起始端口不能大于结束端口,且范围为:1--65535 ");
    		return 0;
    	}
    
    	if ( WSAStartup(wVersion , &wsaData) )
    	{
    		printf("初始化失败!");
    		return -1;
    	}
    
    	// 根据用户输入的域名来获取服务器主机信息
    	host_entry = gethostbyname(host_name);
    
    	if (host_entry != 0)
    	{
    		wsprintf( host_address, "%d.%d.%d.%d",
    			(host_entry->h_addr_list[0][0] & 0x00ff),
    			(host_entry->h_addr_list[0][1] & 0x00ff),
    			(host_entry->h_addr_list[0][2] & 0x00ff),
    			(host_entry->h_addr_list[0][3] & 0x00ff)
    			);
    		printf("n主机名称:%s  主机地址:%s n", host_name, host_address);
    	}
    
    	cHost = host_address;
    
    	printf("======= 开始扫描 ======= n");
    
    	for (iNowPort = iFromPort; iNowPort <= iToPort; iNowPort++)
    	{
    		s = socket(AF_INET, SOCK_STREAM, 0);
    		if (s == INVALID_SOCKET)
    		{
    			printf("create socket() failed!");
    			WSACleanup();
    		}
    
    		sin.sin_family = AF_INET;
    		sin.sin_port = htons(iNowPort);
    		sin.sin_addr.S_un.S_addr = inet_addr(cHost);
    
    		if (connect(s, (struct sockaddr *)&sin, sizeof(sin)) == SOCKET_ERROR)
    		{
    			printf("%s -> %d:未开放 n", cHost, iNowPort);
    			closesocket(s);
    		}
    		else
    		{
    			printf("%s -> %d:开放 n", cHost, iNowPort);
    			port[iOpenPort] = iNowPort;
    			iOpenPort ++;
    			closesocket(s);
    		}
    	}
    
    	printf("======= 扫描结果 ======= n");
    	printf("主机:%s 扫描到%d个端口,分别是: n", host_name, iOpenPort);
    
    	for ( i = 0; i < iOpenPort; i++)
    	{
    		printf("%d ", port[i]);
    	}
    
    	//关闭socket
    	closesocket(s);
    	WSACleanup();
    	return 0;
    }
    
    //帮助函数
    void Help()
    {
    	printf("Usage: n");
    	printf("Port.exe <TargetIP> <BeginPort> <EndPort> n");
    	printf("Example: n");
    	printf("scan.exe 127.0.0.1 100 200 ");
    }
    

    函数列表

    一、基本Socket函数

  • accept() 响应连接请求,返回一个连接socket,原来的socket返回监听状态
  • bind() 把一个socket绑定在某个端口上
  • CloseSocket() 关闭套接字
  • Connect() 连接
  • GetPeerName() 得到连接在指定套接口上的对等通信方的名字
  • GetSockName() 得到指定套接口上当前的名字
  • GetSockopt() 得到与制定套接口相关的属性选项
  • htonl() 把32位的数字从主机字节顺序转换到网络字节顺序
  • htons() 把16位的数字从主机字节顺序转换到网络字节顺序
  • inet_addr() 把一个Internet标准的点分十进制地址转换成Internet地址数值
  • inet_ntoa() 把Internet地址转换成点分十进制的字符串
  • ioctlsocket() 为套接字提供控制
  • listen() 监听套接字上连接请求的到来
  • ntohl() 把32位的数字从网咯字节顺序转换为主机字节顺序
  • ntons() 把16位的数字从网咯字节顺序转换为主机字节顺序
  • recv() 从已经连接的套接字接受数据
  • recvfrom() 从已连接的或没有连接的套接口接受数据
  • select() 执行同步IO多路复用
  • send() 从已连接的套几口发送数据
  • sendto() 从已连接的或没有连接的套接口发送数据
  • setsockopt() 设置与指定套接口相关的属性选项
  • shutdown() 关闭一部分全双工的链接
  • socket() 创建并返回一个socket
  • 二、获取信息

  • Gethostbyaddr() 根据网络地址得到对应的名字(会有多个)和地址
  • Gethostbyname() 根据主机名得到对应的名字(会有多个)和地址
  • gethostname() 得到本机主机名
  • getservbyname() 根据服务的名字得到对应的服务名和端口号
  • getservbyport() 根据端口号得到对应的服务名和端口号
  • getprotobyname() 根据协议名得到对应的协议名和数值
  • getprotobynumber() 根据端口号得到对应的协议名和数值
  • 三、DLL操作

  • WSAStartup 初始化底层的windows Sockets DLL
  • WSACleanup 从底层的Sockets DLL中撤销注册
  • WSAAsyncSelect Select函数的异步版本
  • WSAIsBlocking 确定底层的winsock DLL是否在该线程已经被一个调用阻塞
  • WSACancelBlockingCall 取消未完成的阻塞的API调用
  • WSASetBlockingHook 为底层的windows sockets实现设置阻塞钩子
  • WSASetLastError 设置下一次的WSAGetLastError返回的错误信息
  • WSAGetLastError 得到最近的一个windows sockets API调用错误的详细信息
  • WSAUnhookBlockingHook 恢复原始的阻塞钩子
  • WSACancelAsyncRequest 取消一个未完成的 WSAAsyncGetXByY 函数的实例
  • 文章索引
    上一讲: Windows API 教程(八) 注册快捷键
    下一讲: Windows API 教程(十) 注册表操作

    Windows API 教程(八) 注册快捷键

    茵蒂克丝

    注册快捷键与hook监听键盘的区别

    快捷键注册是针对某一个窗口的,注册之后当这个窗口收到快捷键,操作系统就会发送 WM_HOTKEY 消息来通知该窗口。所以快捷键注册是没办法监听全局的,比如你要定义一个快捷键 Ctrl + ` 在桌面上也能调出你的窗口,可是这个消息是发给桌面这个程序去了没办法触发你的程序。 快捷键收到消息之后消息会被转发给注册的hwnd 相当于截断了消息,如果要做全局监听的话还是推荐用 hook。

    控制台注册快捷键

    
    #include 
    #include 
    
    int main()
    {
    	MSG msg = {0};
    	HWND hConsole = GetActiveWindow(); // 获取当前显示窗口的句柄(即控制台这个窗口的句柄)
    
    	RegisterHotKey(
    		hConsole,	// 注册快捷键的窗口句柄
    		1,			// 热键标识符
    		MOD_CONTROL | MOD_NOREPEAT, // Ctrl 键  No Repeat 不重复发送
    		'A'			// A
    	);	// Ctrl + A
    
    	RegisterHotKey(hConsole, 2, MOD_CONTROL | MOD_NOREPEAT,'B');// Ctrl + B
    	RegisterHotKey(hConsole, 3, MOD_ALT | MOD_NOREPEAT,'A');	// Alt + A
    	RegisterHotKey(hConsole, 4, MOD_ALT | MOD_NOREPEAT,'B');	// Alt + B
    	RegisterHotKey(hConsole, 5, MOD_NOREPEAT,'S');				// 直接按 S
    
    	
    	// 循环获取操作系统发来的消息
    	while (GetMessage(&msg, NULL, 0, 0) != 0)
    	{
    		// 当收到快捷键消息时
    		if (msg.message == WM_HOTKEY)
    		{
    			switch(msg.wParam)
    			{
    				case 1:
    					printf("Ctrl + A 被按下! \n");
    				break;
    
    				case 2:
    					printf("Ctrl + B 被按下! \n");
    				break;
    
    				case 3:
    					printf("Alt + A 被按下! \n");
    				break;
    				
    				case 4:
    					printf("Alt + B 被按下! \n");
    				break;
    
    				case 5:
    					printf("S 被按下! \n");
    				break;
    			}
    		}
    	}
    }
    

    hotkey_console

    窗口注册快捷键

    在window窗体开发中,窗口过程函数中实际上还可以通过 WM_KEYUP WM_KEYDOWN 等消息获取到用户的按键,大家可以通过如下类似的代码来查看窗口收到的消息(其实控制台下也是可以这样拿的)

    case WM_KEYDOWN:
    	wsprintf(text, "wParam : 0x%x  lParam : 0x%x", wParam, lParam);
    	echo(text);
    break;
    

    不过这种的缺点也是很明显的,就是复合键等快捷键定义没有使用 WM_HOTKEY 方便

    #include 
    
    void echo(LPSTR str)
    {
    	MessageBox(NULL, str, TEXT("提示"), MB_OK);
    }
    
    // 5. 窗口过程处理
    LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
    {	
    	CHAR text[50];
    	switch(msg)
    	{
    	// 窗口创建消息
    	case WM_CREATE:
    		RegisterHotKey(
    			hwnd,	// 注册快捷键的窗口句柄
    			1,		// 热键标识符避免热键冲突
    			MOD_CONTROL | MOD_NOREPEAT, // Ctrl 键  No Repeat 不重复发送
    			'A'		// A
    			); // Ctrl + A
    		RegisterHotKey(hwnd, 2, MOD_CONTROL | MOD_NOREPEAT,'B');// Ctrl + B
    		RegisterHotKey(hwnd, 3, MOD_ALT | MOD_NOREPEAT,'A');	// Alt + A
    		RegisterHotKey(hwnd, 4, MOD_ALT | MOD_NOREPEAT,'B');	// Alt + B
    		RegisterHotKey(hwnd, 5, MOD_NOREPEAT,'S');				// 直接按 S
    	break;
    
    	// 快捷键消息
    	case WM_HOTKEY:
    
    		switch(wParam)
    		{
    			case 1:
    				printf("Ctrl + A 被按下! \n");
    			break;
    
    			case 2:
    				printf("Ctrl + B 被按下! \n");
    			break;
    
    			case 3:
    				printf("Alt + A 被按下! \n");
    			break;
    			
    			case 4:
    				printf("Alt + B 被按下! \n");
    			break;
    
    			case 5:
    				printf("S 被按下! \n");
    			break;
    		}		
    	break;
    
    	case WM_CLOSE:
    		DestroyWindow(hwnd);
    		break;
    	case WM_DESTROY:
    		PostQuitMessage(0);
    		break;
    	default:
    		return DefWindowProc(hwnd, msg, wParam, lParam);
    	}
    	return 0;
    }
    
    int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    	LPSTR lpCmdLine, int nCmdShow)
    {
    	WNDCLASSEX wc;
    	HWND hwnd;
    	MSG Msg;
    	char text[30];
    
    	const char szClassName[] = "myWindowClass";
    
    	// 1. 设置注册窗口结构体
    	wc.cbSize        = sizeof(WNDCLASSEX);				// 注册窗口结构体的大小
    	wc.style         = 0;								// 窗口的样式
    	wc.lpfnWndProc   = WndProc;							// 指向窗口处理过程的函数指针
    	wc.cbClsExtra    = 0;								// 指定紧跟在窗口类结构后的附加字节数
    	wc.cbWndExtra    = 0;								// 指定紧跟在窗口事例后的附加字节数
    	wc.hInstance     = hInstance;						// 本模块的实例句柄
    	wc.hIcon         = LoadIcon(hInstance, IDI_APPLICATION);	// 图标的句柄
    	wc.hCursor       = LoadCursor(NULL, IDC_ARROW);		// 光标的句柄
    	wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);		// 背景画刷的句柄
    	wc.lpszMenuName  = NULL;							// 指向菜单的指针
    	wc.lpszClassName = szClassName;						// 指向类名称的指针
    	wc.hIconSm       = LoadIcon(hInstance, IDI_APPLICATION);	// 和窗口类关联的小图标
    
    	// 2. 使用【窗口结构体】注册窗口
    	if(!RegisterClassEx(&wc))
    	{
    		MessageBox(NULL, TEXT("窗口注册失败!"), TEXT("错误"), MB_ICONEXCLAMATION | MB_OK);
    		return 0;
    	}
    
    	// 3. 创建窗口
    	hwnd = CreateWindowEx(
    		WS_EX_CLIENTEDGE,		// 窗口的扩展风格
    		szClassName,			// 指向注册类名的指针
    		TEXT("窗口标题"),		// 指向窗口名称的指针
    		WS_OVERLAPPEDWINDOW,	// 窗口风格
    		CW_USEDEFAULT, CW_USEDEFAULT, 350, 200, // 窗口的 x,y 坐标以及宽高
    		NULL,					// 父窗口的句柄
    		NULL,					// 菜单的句柄
    		hInstance,				// 应用程序实例的句柄
    		NULL					// 指向窗口的创建数据
    		);
    
    	if(hwnd == NULL)
    	{
    		MessageBox(NULL, TEXT("窗口创建失败"), TEXT("错误"),MB_ICONEXCLAMATION | MB_OK);
    		return 0;
    	}
    
    	// 4. 显示窗口
    	ShowWindow(hwnd, nCmdShow);
    	UpdateWindow(hwnd);
    
    	// 6. 消息循环
    	while(GetMessage(&Msg, NULL, 0, 0) > 0)
    	{
    		TranslateMessage(&Msg);
    		DispatchMessage(&Msg);
    	}
    	return Msg.wParam;
    }
    

    hotkey_window

    Ctrl + Alt 等复杂组合键

    还是在控制台下编写例子,其实没有差多少

    
    #include 
    #include 
    
    int main()
    {
    	MSG msg = {0};
    	HWND hConsole = GetActiveWindow();
    
    	// Ctrl + D
    	RegisterHotKey(hConsole,1,MOD_CONTROL | MOD_NOREPEAT,'D');
    	// Win键 + Z
    	RegisterHotKey(hConsole,2,MOD_WIN | MOD_NOREPEAT,'Z');
    	// Ctrl + Alt + S
    	RegisterHotKey(hConsole,3,MOD_CONTROL | MOD_ALT | MOD_NOREPEAT,'S');
    	// Ctrl + Alt + Shift + A
    	RegisterHotKey(hConsole,4,MOD_CONTROL | MOD_ALT | MOD_SHIFT | MOD_NOREPEAT,'A');
    	
    	// 循环获取操作系统发来的消息
    	while (GetMessage(&msg, NULL, 0, 0) != 0)
    	{
    		// 当收到快捷键消息时
    		if (msg.message == WM_HOTKEY)
    		{
    			printf("收到 WM_HOTKEY 快捷键消息n");
    			printf("wParam : 0x%x  lParam : 0x%x n", msg.wParam, msg.lParam);
    
    			switch(msg.wParam)
    			{
    				case 1:
    					printf("Ctrl + D 被按下! \n");
    				break
    
    				case 2:
    					printf("Win + Z 被按下! \n");
    				break
    
    				case 3:
    					printf("Ctrl + Alt + S 被按下! \n");
    				break
    
    				case 4:
    					printf("Ctrl + Alt + Shift + A 被按下! \n");
    				break;
    			}
    		}
    	}
    }
    

    hotkey_complex

    文章索引
    上一讲: Windows API 教程(七) hook 监听
    下一讲: Windows API 教程(九) 网络编程

    Windows API 教程(七) hook 钩子监听

    茵蒂克丝

    如何创建一个窗口

    另外一个再录的 Windows SDK教程 里面有讲到快捷创建窗口的方式,不过这样的话要分好几个文件,感觉有点混所以这里就用原始的方式创建一个窗口。

    那么,为什么讲到 hook(钩子)的时候要去创建窗口呢?其实这个问题说起来也不复杂,简单点说,按博主这样写不用写DLL也不用资源文件,实际上是把问题简化了一些。通常 hook 是用来监听自己窗口上的键盘和鼠标输入的,监听全局的通常是设置一些全局的热键(如QQ的 Ctrl+Alt+Z 调出QQ窗口),这些常见的功能也都是要依托窗口才能存在。所以我们先来简单说下手动建立一个窗口的流程。

    手动创建窗口的流程

    1. 设置注册窗口结构体
    2. 使用【窗口结构体】注册窗口
    3. 创建窗口
    4. 显示窗口
    5. 窗口过程处理
    6. 消息循环

    实际代码

    这里不会详细讲这个,有感兴趣的可以去追博主的 SDK教程 或者去搜 杨中科的《C语言也能干大事》

    
    #include <windows.h>
    
    // 5. 窗口过程处理
    LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
    {	
    	switch(msg)
    	{
            case WM_CLOSE:
                DestroyWindow(hwnd);
            break;
    		case WM_DESTROY:
    			PostQuitMessage(0);
    			break;
            default:
                return DefWindowProc(hwnd, msg, wParam, lParam);
        }
        return 0;
    }
    
    int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
        LPSTR lpCmdLine, int nCmdShow)
    {
        WNDCLASSEX wc;	// 更多详细都可以去百度的 http://baike.baidu.com/view/1750396.htm
        HWND hwnd;
        MSG Msg;
    	char text[30];
    
    	const char szClassName[] = "myWindowClass";
    
        // 1. 设置注册窗口结构体
        wc.cbSize        = sizeof(WNDCLASSEX);				// 注册窗口结构体的大小
        wc.style         = 0;								// 窗口的样式
        wc.lpfnWndProc   = WndProc;							// 指向窗口处理过程的函数指针
        wc.cbClsExtra    = 0;								// 指定紧跟在窗口类结构后的附加字节数
        wc.cbWndExtra    = 0;								// 指定紧跟在窗口事例后的附加字节数
        wc.hInstance     = hInstance;						// 本模块的实例句柄
        wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);	// 图标的句柄
        wc.hCursor       = LoadCursor(NULL, IDC_ARROW);		// 光标的句柄
        wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);		// 背景画刷的句柄
        wc.lpszMenuName  = NULL;							// 指向菜单的指针
        wc.lpszClassName = szClassName;						// 指向类名称的指针
        wc.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);	// 和窗口类关联的小图标
    
    	// 2. 使用【窗口结构体】注册窗口
        if(!RegisterClassEx(&wc))
        {
            MessageBox(NULL, TEXT("窗口注册失败!"), TEXT("错误"), MB_ICONEXCLAMATION | MB_OK);
            return 0;
        }
    
        // 3. 创建窗口
        hwnd = CreateWindowEx(
    		WS_EX_CLIENTEDGE,		// 窗口的扩展风格
    		szClassName,			// 指向注册类名的指针
    		TEXT("窗口标题"),		// 指向窗口名称的指针
    		WS_OVERLAPPEDWINDOW,	// 窗口风格
            CW_USEDEFAULT, CW_USEDEFAULT, 350, 200, // 窗口的 x,y 坐标以及宽高
            NULL,					// 父窗口的句柄
    		NULL,					// 菜单的句柄
    		hInstance,				// 应用程序实例的句柄
    		NULL					// 指向窗口的创建数据
    		);
    
        if(hwnd == NULL)
        {
            MessageBox(NULL, TEXT("窗口创建失败"), TEXT("错误"),MB_ICONEXCLAMATION | MB_OK);
            return 0;
        }
    
    	// 4. 显示窗口
        ShowWindow(hwnd, nCmdShow);
        UpdateWindow(hwnd);
    
        // 6. 消息循环
        while(GetMessage(&Msg, NULL, 0, 0) > 0)
        {
            TranslateMessage(&Msg);
            DispatchMessage(&Msg);
        }
        return Msg.wParam;
    }
    

    因为是比较死的形式,以上代码大家混个脸熟,大概知道各个部分的作用就行了,博主也从来没有专门记过。

    安装钩子 (Install hook)

    简介

    窗口建好了之后就要开始转到我们的正题了,首先我们需要明确的是,这个钩子(hook)到底是什么,那么博主这里也不做太书面的解释留下几个链接:

    百度百科:hook
    MSDN: Hooks
    博客园: Beginning HOOK

    各位可以多多参考,那么博主说下自己的理解:

    windows 系统中的【hook 机制】,就类似于一个【消息过滤网】,如果我们向操作系统申请并成功对某个窗口安装了一个【hook】指定了【回调函数】,那么这个【回调函数】也就相当于我们人为对这个窗口添加了一个【消息过滤网】。此时当 windows 操作系统要对这个窗口发送任何消息的时候(例如按键、鼠标点击等消息)操作系统会先调用我们在【消息过滤网】中设置的【回调函数】去接受、处理、过滤等等,当然如果你在【回调函数】中拿到了数据却没有继续传递给窗口的话,就相当于拦截了这些消息。

    打个简单的比方,如果你在系统全局安装了一个【键盘消息】的钩子,并且在其指定的【回调函数】中没有把这个键盘消息继续传递给系统上的窗口,那么你的所有【键盘消息】都被这个【hook】也就我们挂在这个【消息过滤网】上的【回调函数】给拦截了,这也意味着你的键盘会失灵。

    SetWindowsHookEx 函数

    那么 SetWindowsHookEx 函数就是我们用来在 windows 操作系统上安装钩子的函数,我们简单来看一下这个函数的原型:

    HHOOK WINAPI SetWindowsHookEx(
      _In_  int idHook,			// 安装的钩子类型
      _In_  HOOKPROC lpfn,		// 处理消息的回调函数
      _In_  HINSTANCE hMod,		// 当前实例句柄
      _In_  DWORD dwThreadId	// 线程ID
    );
    

    钩子类型有很多种,本页中留的大部分链接上都有讲到这里就不废话了,关于 hMod(当前实例句柄)和 dwThreadId(线程ID)之间的一些小九九博主这里也不多说,各位可以到下方的链接中去看看,博主这里就举一个容易实现的实例。

    百度百科: SetWindowsHookEx
    MSDN: SetWindowsHookEx

    设置监听【键盘】消息

    PKBDLLHOOKSTRUCT 是 WH_KEYBOARD_LL 方式中用来接收消息的结构体,大家可以到 WindUser.h 中多逛逛。

    我们监听键盘的时候主要用的是该结构体的 vkCode(value code)和 scanCode 这两个字段。即键盘的【值码】和【扫描码】那么为什么判断一个按键要分成两个部分呢,原因是因为世界上的键盘有很多种,不同国家、不同厂商生产的键盘甚,至同一个键盘上【同样的键】不同的地方按下都可能会有差异。vkCode 是常见的一般都是公用的键盘值,而 scanCode 扫描码则是用来辅助区分的一个一个参数,例如同样是按下 ctrl 键,他们的 vkCode 是相同的但是 scanCode 却不同。

    
    #include <windows.h>
    
    HHOOK myhook;	// 保存当前钩子句柄
    
    /**************************************************************** 
      WH_KEYBOARD hook procedure 
      鍵盤钩子处理过程
     ****************************************************************/ 
    LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) 
    { 	
    	char text[50], data[20];	// 输出字符串
    	const char *info = NULL;	// 类型字符指针
    	PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam; // 获取按键消息
    	HDC hdc;	// 画图设备句柄
    
    	// 判断是否收到键盘消息
        if (nCode >= 0)
    	{
    		// 判断消息类型
    		if 		(wParam == WM_KEYDOWN) 		info = "普通按鍵抬起";
    		else if (wParam == WM_KEYUP) 		info = "普通按鍵按下";
    		else if (wParam == WM_SYSKEYDOWN) 	info = "系統按鍵抬起";
    		else if (wParam == WM_SYSKEYUP) 	info = "系統按鍵按下";
    
    		// 初始化数组
    		ZeroMemory(text, sizeof(text));
    		ZeroMemory(data, sizeof(data));
    		// 拼装字符串
    		wsprintf(text, "%s - 键盘码 [%04d], 扫描码 [%04d]  ", info, p->vkCode, p->scanCode);
    		wsprintf(data, "按鍵目测为: %c  ", p->vkCode);
    
    		// 此处调用 GDI 画图函数来将截取到的内容画在窗口上
    		hdc = GetDC(画图的窗口句柄);		// 获取要画图的设备句柄
    		TextOut(hdc, 10, 10, text, strlen(text));	// 在窗口上画文字 
    		TextOut(hdc, 10, 30, data, strlen(data));	// 参数分别是 目标设备, x坐标, y坐标, 字符串内容, 字符串长度
    		ReleaseDC(画图的窗口句柄, hdc);		// 释放设备句柄
    	}
    
        // 将消息继续往下传递
        return CallNextHookEx(myhook, nCode, wParam, lParam);
    } 
    
    int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
        LPSTR lpCmdLine, int nCmdShow)
    {
    	/* 其他代码 */
    
    	// 设置键盘全局监听
    	myhook = SetWindowsHookEx( 
    		WH_KEYBOARD_LL, // 监听类型【键盘消息】
    		KeyboardProc,	// 处理函数
    		hInstance,		// 当前实例句柄
    		0				// 监听线程ID(NULL为全局监听)
    	); 
    
    	// 判断是否成功
    	if(myhook == NULL)
    	{		
    		wsprintf(text, "键盘监听失败!error : %d n", GetLastError());
    		MessageBox(hwnd, text, TEXT("错误"), MB_OK);
    	}
    
    	/* 其他代码 */
    }
    

    注:其中在输出按键的时候,直接用 %c 输出了 p->vkCode 部分,这个实际上不是很准确。

    顺便提一句,各位也不要用这个功能去做什么坏事,比如去监听QQ窗口的键盘消息然后偷到密码盗别人号之类的, 博主已经试过了 这个本身有带防护的,用户在输密码的时候,会有干扰的键盘消息也一起冒出来所以没那么简单能到到别人输的密码。至于写个程序去拦截别人的键盘还有鼠标消息让别人的电脑不能用的情况,这个确实很容易做到,而且貌似杀毒软件都没办法防,只能自己在自己的电脑上留个后门,怎么写后门?后面的网络编程会说这个。

    键盘监听完整代码:

    
    #include <windows.h>
    
    HWND hgWnd;
    HHOOK myhook;
    
    /**************************************************************** 
      WH_KEYBOARD hook procedure 
      鍵盤钩子处理过程
     ****************************************************************/ 
    LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) 
    { 	
    	PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam;
    	const char *info = NULL;
    	char text[50], data[20];
    
    	PAINTSTRUCT ps;
    	HDC hdc;
    
        if (nCode >= 0)
    	{
    		if      (wParam == WM_KEYDOWN)		info = "普通按鍵抬起";
    		else if (wParam == WM_KEYUP)		info = "普通按鍵按下";
    		else if (wParam == WM_SYSKEYDOWN)	info = "系統按鍵抬起";
    		else if (wParam == WM_SYSKEYUP)		info = "系統按鍵按下";
    
    		ZeroMemory(text, sizeof(text));
    		ZeroMemory(data, sizeof(data));
    		wsprintf(text, "%s - 键盘码 [%04d], 扫描码 [%04d]  ", info, p->vkCode, p->scanCode);
    		wsprintf(data, "按鍵目測為: %c  ", p->vkCode);
    
    		hdc = GetDC(hgWnd);			
    		TextOut(hdc, 10, 10, text, strlen(text));
    		TextOut(hdc, 10, 30, data, strlen(data));
    		ReleaseDC(hgWnd,hdc);
    	}
        
        return CallNextHookEx(myhook, nCode, wParam, lParam);
    } 
    
    // 5. 窗口过程处理
    LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
    {	
    	hgWnd = hwnd;
    
    	switch(msg)
    	{
            case WM_CLOSE:
                DestroyWindow(hwnd);
            break;
    		case WM_DESTROY:
    			PostQuitMessage(0);
    			break;
            default:
                return DefWindowProc(hwnd, msg, wParam, lParam);
        }
        return 0;
    }
    
    int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
        LPSTR lpCmdLine, int nCmdShow)
    {
        WNDCLASSEX wc;	// http://baike.baidu.com/view/1750396.htm
        HWND hwnd;
        MSG Msg;
    	char text[30];
    
    	const char szClassName[] = "myWindowClass";
    
        // 1. 设置注册窗口结构体
        wc.cbSize        = sizeof(WNDCLASSEX);				// 注册窗口结构体的大小
        wc.style         = 0;								// 窗口的样式
        wc.lpfnWndProc   = WndProc;							// 指向窗口处理过程的函数指针
        wc.cbClsExtra    = 0;								// 指定紧跟在窗口类结构后的附加字节数
        wc.cbWndExtra    = 0;								// 指定紧跟在窗口事例后的附加字节数
        wc.hInstance     = hInstance;						// 本模块的实例句柄
        wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);	// 图标的句柄
        wc.hCursor       = LoadCursor(NULL, IDC_ARROW);		// 光标的句柄
        wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);		// 背景画刷的句柄
        wc.lpszMenuName  = NULL;							// 指向菜单的指针
        wc.lpszClassName = szClassName;						// 指向类名称的指针
        wc.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);	// 和窗口类关联的小图标
    
    	// 2. 使用【窗口结构体】注册窗口
        if(!RegisterClassEx(&wc))
        {
            MessageBox(NULL, TEXT("窗口注册失败!"), TEXT("错误"), MB_ICONEXCLAMATION | MB_OK);
            return 0;
        }
    
        // 3. 创建窗口
        hwnd = CreateWindowEx(
    		WS_EX_CLIENTEDGE,		// 窗口的扩展风格
    		szClassName,			// 指向注册类名的指针
    		TEXT("窗口标题"),		// 指向窗口名称的指针
    		WS_OVERLAPPEDWINDOW,	// 窗口风格
            CW_USEDEFAULT, CW_USEDEFAULT, 350, 200, // 窗口的 x,y 坐标以及宽高
            NULL,					// 父窗口的句柄
    		NULL,					// 菜单的句柄
    		hInstance,				// 应用程序实例的句柄
    		NULL					// 指向窗口的创建数据
    		);
    
        if(hwnd == NULL)
        {
            MessageBox(NULL, TEXT("窗口创建失败"), TEXT("错误"),MB_ICONEXCLAMATION | MB_OK);
            return 0;
        }
    
    	// 4. 显示窗口
        ShowWindow(hwnd, nCmdShow);
        UpdateWindow(hwnd);
    
    	// 设置键盘全局监听
    	myhook = SetWindowsHookEx( 
    		WH_KEYBOARD_LL, // 监听类型【键盘】
    		KeyboardProc,	// 处理函数
    		hInstance,		// 当前实例句柄
    		0				// 监听窗口句柄(NULL为全局监听)
    	); 
    
    	if(myhook == NULL)
    	{		
    		wsprintf(text, "键盘监听失败!error : %d n", GetLastError());
    		MessageBox(hwnd, text, TEXT("错误"), MB_OK);
    	}
    
    
        // 5. 消息循环
        while(GetMessage(&Msg, NULL, 0, 0) > 0)
        {
            TranslateMessage(&Msg);
            DispatchMessage(&Msg);
        }
        return Msg.wParam;
    }
    
    

    运行截图

    键盘监听

    设置监听【鼠标】消息

    与键盘监听类似,各位直接看代码:

    #include <windows.h>
    
    HWND hgWnd;
    HHOOK myhook;
    
    /**************************************************************** 
      WH_KEYBOARD hook procedure 
      鍵盤钩子处理过程
     ****************************************************************/ 
    LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam) 
    { 	
    	LPMSLLHOOKSTRUCT p = (LPMSLLHOOKSTRUCT)lParam;
    	POINT   pt = p->pt;
    	DWORD   mouseData = p->mouseData;
    	const char *info = NULL;
    	char text[60], pData[50], mData[50];
    
    	PAINTSTRUCT ps;
    	HDC hdc;
    
        if (nCode >= 0)
    	{
    		if   (wParam == WM_MOUSEMOVE)		info = "鼠标移动    ";
    		else if(wParam == WM_LBUTTONDOWN)	info = "鼠标【左键】按下";
    		else if(wParam == WM_LBUTTONUP)		info = "鼠标【左键】抬起";
    		else if(wParam == WM_LBUTTONDBLCLK)	info = "鼠标【左键】双击";
    		else if(wParam == WM_RBUTTONDOWN)	info = "鼠标【右键】按下";
    		else if(wParam == WM_RBUTTONUP)		info = "鼠标【右键】抬起";
    		else if(wParam == WM_RBUTTONDBLCLK)	info = "鼠标【右键】双击";
    		else if(wParam == WM_MBUTTONDOWN)	info = "鼠标【滚轮】按下";
    		else if(wParam == WM_MBUTTONUP)		info = "鼠标【滚轮】抬起";
    		else if(wParam == WM_MBUTTONDBLCLK)	info = "鼠标【滚轮】双击";
    		else if(wParam == WM_MOUSEWHEEL)	info = "鼠标【滚轮】滚动";
    
    		ZeroMemory(text, sizeof(text));
    		ZeroMemory(pData, sizeof(pData));
    		ZeroMemory(mData, sizeof(mData));
    
    		wsprintf( text, "当前状态: %10s   ", info);
    		wsprintf(pData, "0x%x - X: [%04d], Y: [%04d]  ", wParam, pt.x, pt.y);
    		wsprintf(mData, "附带数据: %16u   ", mouseData);
    
    		hdc = GetDC(hgWnd);			
    		TextOut(hdc, 10, 10,  text, strlen(text));
    		TextOut(hdc, 10, 30, pData, strlen(pData));
    		TextOut(hdc, 10, 50, mData, strlen(mData));
    		ReleaseDC(hgWnd,hdc);
    	}
        
        return CallNextHookEx(myhook, nCode, wParam, lParam);
    } 
    
    // 5. 窗口过程处理
    LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
    {	
    	hgWnd = hwnd;
    
    	switch(msg)
    	{
            case WM_CLOSE:
                DestroyWindow(hwnd);
            break;
    		case WM_DESTROY:
    			PostQuitMessage(0);
    			break;
            default:
                return DefWindowProc(hwnd, msg, wParam, lParam);
        }
        return 0;
    }
    
    int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
        LPSTR lpCmdLine, int nCmdShow)
    {
        WNDCLASSEX wc;	// http://baike.baidu.com/view/1750396.htm
        HWND hwnd;
        MSG Msg;
    	char text[30];
    
    	const char szClassName[] = "myWindowClass";
    
        // 1. 设置注册窗口结构体
        wc.cbSize        = sizeof(WNDCLASSEX);				// 注册窗口结构体的大小
        wc.style         = 0;								// 窗口的样式
        wc.lpfnWndProc   = WndProc;							// 指向窗口处理过程的函数指针
        wc.cbClsExtra    = 0;								// 指定紧跟在窗口类结构后的附加字节数
        wc.cbWndExtra    = 0;								// 指定紧跟在窗口事例后的附加字节数
        wc.hInstance     = hInstance;						// 本模块的实例句柄
        wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);	// 图标的句柄
        wc.hCursor       = LoadCursor(NULL, IDC_ARROW);		// 光标的句柄
        wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);		// 背景画刷的句柄
        wc.lpszMenuName  = NULL;							// 指向菜单的指针
        wc.lpszClassName = szClassName;						// 指向类名称的指针
        wc.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);	// 和窗口类关联的小图标
    
    	// 2. 使用【窗口结构体】注册窗口
        if(!RegisterClassEx(&wc))
        {
            MessageBox(NULL, TEXT("窗口注册失败!"), TEXT("错误"), MB_ICONEXCLAMATION | MB_OK);
            return 0;
        }
    
        // 3. 创建窗口
        hwnd = CreateWindowEx(
    		WS_EX_CLIENTEDGE,		// 窗口的扩展风格
    		szClassName,			// 指向注册类名的指针
    		TEXT("窗口标题"),		// 指向窗口名称的指针
    		WS_OVERLAPPEDWINDOW,	// 窗口风格
            CW_USEDEFAULT, CW_USEDEFAULT, 350, 200, // 窗口的 x,y 坐标以及宽高
            NULL,					// 父窗口的句柄
    		NULL,					// 菜单的句柄
    		hInstance,				// 应用程序实例的句柄
    		NULL					// 指向窗口的创建数据
    		);
    
        if(hwnd == NULL)
        {
            MessageBox(NULL, TEXT("窗口创建失败"), TEXT("错误"),MB_ICONEXCLAMATION | MB_OK);
            return 0;
        }
    
    	// 4. 显示窗口
        ShowWindow(hwnd, nCmdShow);
        UpdateWindow(hwnd);
    
    	// 设置鼠标全局监听
    	myhook = SetWindowsHookEx( 
    		WH_MOUSE_LL,	// 监听类型【鼠标】
    		MouseProc,	// 处理函数
    		hInstance,		// 当前实例句柄
    		0				// 监听窗口句柄(NULL为全局监听)
    	); 
    
    	if(myhook == NULL)
    	{		
    		wsprintf(text, "键盘监听失败!error : %d n", GetLastError());
    		MessageBox(hwnd, text, TEXT("错误"), MB_OK);
    	}
    
    
        // 5. 消息循环
        while(GetMessage(&Msg, NULL, 0, 0) > 0)
        {
            TranslateMessage(&Msg);
            DispatchMessage(&Msg);
        }
        return Msg.wParam;
    }
    
    

    MouseHook

    上一讲: Windows API 教程(六) 动态链接库
    下一讲: Windows API 教程(八) 注册快捷键

    Windows API 教程(六) 动态链接库

    简介

      动态链接库(DLL,全称Dynamic Link Library)进程之间共享使用的函数库。动态链接库提供了一种方法,是进程可以调用不属于其自身可执行代码的函数。函数的可执行代码位于一个DLL中,当调用DLL中的方法时,进程由原本自身的代码执行到DLL中。DLL有助于程序模块化,减少重复开发。
      在这之前已经提到的多次的 kernel32.dll ,也即 windows 的核心DLL,我们进行 windows 编程开发中很多函数都是从其中导出。

    编写 DLL

    1) 新建项目

    【文件】->【新建】->【项目】->选择【win32 项目】->【下一步】->【DLL 空项目】

    新建项目的目录要记一下,等会会用到。

    2)开始编写 DLL

    dll.h

    
    #ifdef MYDLL_EXPORTS
    	#define MYDLL_API _declspec(dllexport)
    #else
    	#define MYDLL_API _declspec(dllimport)
    #endif
    
    // 声明自定义导出函数
    MYDLL_API void helloDLL(void);
    
    

    dll.c

    
    #include <windows.h>// for MessageBox
    #include "dll.h"	// 引用导出定义
    
    // DLL入口函数
    BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)
    {
    	switch(dwReason)
    	{
    		// 动态链接库映射到某个进程的地址空间
    		case DLL_PROCESS_ATTACH:
    			/**
    			 * 当DLL刚被加载时触发(LoadLibrary),此处专门用来做初始化工作,
    			 * 如果初始化失败可以返回 false 这样DLL就不会被继续加载了
    			 **/
    		break;
    
    		// 应用程序创建新的线程
    		case DLL_THREAD_ATTACH:
    			
    		break;
    
    		// 应用程序某个线程正常终止
    		case DLL_THREAD_DETACH:
    
    		break;
    		
    		// 动态链接库将被卸载
    		case DLL_PROCESS_DETACH:
    			/**
    			 * 当DLL将要被卸载时触发(FreeLibrary),此处专门用来做清理工作
    			 * 如关闭文件,释放内存空间等
    			 **/
    		break;
    	}
    	return 1;
    }
    
    void helloDLL(void)
    {
    	MessageBox(NULL, TEXT("Hello DLL~"), TEXT("Title"), MB_OK);
    }
    
    

    编写好之后 F7 生成(visual studio 2008以上),到项目的 Debug 目录下找到生成的 DLL 一般是 【项目名.DLL】

    调用 DLL

    再新建一个普通的【空项目】就可以了

    
    #include <stdio.h> 
    #include <windows.h> 
    
    // 指向函数的指针类型
    typedef void (*fun_pointer)(void);
    
    // main
    VOID main(VOID) 
    { 
    	HINSTANCE hDLL; 
    	BOOL flagGetFun = FALSE; 
    	fun_pointer p;
    
    	// 加载 Dll
    	hDLL = LoadLibrary(TEXT("DLLTEST.dll")); 
    
    	// 判断是否加载成功
    	if (hDLL != NULL) 
    	{ 
    		printf("模块加载成功n");
    
    		// 获得指定导出函数的地址
    		p = (fun_pointer) GetProcAddress(hDLL, "helloDLL"); 
    
    		// 判断是否成功
    		if (NULL != p) 
    		{
    			printf("函数获取成功,准备调用函数n");
    			p(); // 成功则调用DLL中的函数
    		}else
    		{
    			printf("函数获取失败!n");
    			printf("error: %un", GetLastError()); 
    		}
    		// 释放DLL 
    		if (!FreeLibrary(hDLL))
    		{
    			printf("DLL释放失败n");
    			printf("error: %un", GetLastError()); 
    		}		
    	}else
    	{
    		printf("模块加载失败!n");
    		printf("error: %un", GetLastError()); 
    	}		
    
    	system("pause");
    }
    

    编写好之后,点击生成,将开始的 DLL 文件拷贝到该项目的 Debug 目录(与exe同一个目录),接着双击执行即可。

    调用DLL中的函数

    源码下载

    上一讲: Windows API 教程(五) 线程编程

    下一讲: Windows API 教程(七) hook 监听

    Windows API教程(五) 线程编程

    经过了一段时间的思考,以及一阵子的忙碌时期之后觉得“进程通信”放在进程编程之后有点太早了。而且,有些背离初衷了。所以我们还是继续看下下一个知识点,也就是这一讲的主要内容 —— “线程”

    作为多任务操作系统,一台电脑同时跑多个程序是一件理所当然的事情,那么实际上对于一个程序而言自然也是可以同时做多件时间。那么简单的抽象一下就能初步的理解进程与线程的概念。

    操作系统管理多个程序的时候,多个程序对于系统而言是进程(Process),而一个程序同时做多件事,那么每一件事的执行线路就是该程序的线程(Thread)。即系统与进程是一对多的关系,单个进程与线程也是一对多的关系。

    CreateThread

    创建进程

    HANDLE WINAPI CreateThread(
      _In_opt_   LPSECURITY_ATTRIBUTES lpThreadAttributes,
      _In_       SIZE_T dwStackSize,
      _In_       LPTHREAD_START_ROUTINE lpStartAddress,
      _In_opt_   LPVOID lpParameter,
      _In_       DWORD dwCreationFlags,
      _Out_opt_  LPDWORD lpThreadId
    );
    

    lpThreadAttributes [in, optional]

    一个指向SECURITY_ATTRIBUTES结构体的指针该结构体决定了返回的句柄能否被子进程继承。如果lpThreadAttributes为NULL, 那么这个句柄不能被继承。

    dwStackSize [in]

    栈(Stack)最初的长度(reserved size),单位是字节。 The system rounds this value to the nearest page。 如果这个参数为零, 那么新线程的栈将使用默认长度。更多信息, 参见 Thread Stack Size

    lpStartAddress [in]

    一个指向线程执行过程的函数指针。该指针存储的地址(函数名的值是函数代码的起始地址)是线程的入口。关于更多线程函数(thread function)的信息, 请参见 ThreadProc

    lpParameter [in, optional]

    一个将要传递给线程的变量的地址。

    dwCreationFlags [in]

    该标志(flags)控制着线程的创建。

    Value Meaning
    0 线程创建后立即开始执行。
    CREATE_SUSPENDED
    0x00000004
    该线程创建之后处于悬挂状态, 知道调用 ResumeThread 函数才开始执行。
    STACK_SIZE_PARAM_IS_A_RESERVATION
    0x00010000
    前面的 dwStackSize 参数指明栈初始的保留大小。 如果这个 flag 没有立起来,则由 dwStackSize 参数来指明提交大小(commit size)。(以上来组MSDN)注意区别commit size 和 reserved size。没有竖该flag的情况下,dwStackSize这个参数用来调整一开始commit给栈的空间,即initially commit size。那么调整了commit size后,reserved size应该怎么有什么相应的调整呢?如果dwStackSize小于默认reserve大小,则reserve size使用默认reserve大小;如果dwStackSize大于默认reserve size,则reserve size将会向上取整变成1MB的整数倍。如果要求默认的StackSize为64K,则将设置/Stack:65536

    lpThreadId [out, optional]

    输出参数,指向返回的线程ID。如果执行完该函数之后其值为NULL, 那么表示函数执行失败。

    返回值

    如果函数执行成功, 那么返回值是新线程的句柄。
    如果函数执行失败,那么返回值是NULL。想要获得 更多的错误信息, 请使用 GetLastError。

    注意 CreateThread 这个函数在 lpStartAddress 指向一个数据,一段代码的地址,甚至一个不可读的地址时都可能成功(这是线程注入的基础吗,博主吐槽道)。如果线程执行时,过程指向的地址是无效的, 意味着异常产生, 与此同时线程会被终止,并返回一个错误码给主管它的进程。

    创建两个线程

    
    #include <Windows.h>
    #include <stdio.h>
    
    DWORD WINAPI ThreadProc (LPVOID lpThreadParameter)
    {
    	while(1)
    	{
    		printf("%s n", (char *)lpThreadParameter);
    		Sleep(533);
    	}
    	return 0;
    }
    
    int main()
    {
    	DWORD Tid;
    	char *PassData;
    
    	PassData = "Hello 线程1";
    
    	CreateThread(
    		NULL,		// 不能被子进程继承
    		0,			// 默认堆栈大小
    		ThreadProc,	// 线程调用函数过程
    		PassData,	// 传递参数
    		0,			// 创建后立即执行
    		&Tid		// 保存创建后的线程ID
    	);
    
    	printf("线程1 创建成功 线程ID:%u n", Tid);
    
    	PassData = "hi    线程2";
    
    	CreateThread(
    		NULL,		// 不能被子进程继承
    		0,			// 默认堆栈大小
    		ThreadProc,	// 线程调用函数过程
    		PassData,	// 传递参数
    		0,			// 创建后立即执行
    		&Tid		// 保存创建后的线程ID
    	);
    
    	printf("线程2 创建成功 线程ID:%u n", Tid);
    
    	system("pause");
    	// 主线程暂停在这里等待用户按任意键,其他线程则继续执行
    	// 不过一旦按下任意键,主线程退进程,则其他线程均终止
    	// 注意这不是因为主线程的原因,在子线程中退进程效果一样
    	// 因为线程是归属于进程的,进程退出那么旗下的所有线程也都会终止
    
    	return 0;
    }
    

    输出效果:

    线程1 创建成功 线程ID:1168
    Hello 线程1
    线程2 创建成功 线程ID:3696
    hi    线程2
    Press any key to continue . . . hi    线程2
    Hello 线程1
    hi    线程2
    Hello 线程1
    hi    线程2
    Hello 线程1
    Hello 线程1
    hi    线程2
    hi    线程2
    Hello 线程1
    hi    线程2
    Hello 线程1
    

    很多年以前 Linus 编写最早期的 Linux 操作系统时,曾花了不少的力气来实现一个效果,即在电脑屏幕上分别有两个进程一个打印A一个打印B,显示的效果便是在屏幕上交错的出现,这个功能曾让Linus兴奋不已,因为这是多任务操作系统的标识。

    有人会觉得可能会觉得疑惑,要实现这个功能的话到底是用多进程比较好还是用多线程?在我看来这二者都是可以的。就一个比较大的区别是,多个线程之间共用的是一个进程空间,所以在该进内的全局变量是所有线程都能访问的到。但是多个进程之间就不一样了,他们各自的空间都是被操作系统独立。甚至可以说系统还会提防多个进程之间的互相访问。

    原因很简单,例如,我可以写一个程序偷偷的搜集你电脑上的东西,从这一个程序的进程去访问你电脑上的其他进程(可以是QQ、浏览器、你在玩的游戏或者是你的任何操作),这听起来就不那么让人愉快吧。当然,系统的主要工作不是去防止这个,这样的工作通常是由杀毒软件之类的东西去保护。

    好吧,话题绕回来,继续说我们的 Linus ,当初的时候他实现多任务的功能跟我们现在已经完全不一样了。现在的我们只需要简单的调用一下系统的API函数,就可以轻松的使用这样的功能。有些人可能会有些偏执的去深究这个底层的问题(比如博主Orz),这里博主的建议是————要学会站在巨人的肩膀上。善于利用前人留下来的(咳咳,某些人还活着)知识,走一条通向未来的路,而不是把以前的老路翻出来走一遍,这也是我们学习 API 的意义所在。

    贪吃蛇

    那么接下来我们来写一个代码多一点的小游戏,要实现游戏的话,多线程是一个很常用的功能。比如蛇在屏幕上移动,于此同时还要有另外一个线程去监听用户输入的方向键,方向有改变的话,相应的蛇的移动也会改变。

    由于时间问题,博主这里先吧完整的代码贴上来,回头会会拆成几个步骤慢慢的说。

    #include<Windows.h>
    #include<stdio.h>
    #include<conio.h>
    #include<time.h>
    
    // 后方函数申明
    void restart();
    void gotoxy(int x,int y);
    
    // 方向对应值
    enum MoveDir { RIGHT = 0, UP = 1, LEFT = 2, DOWN = 3  };
    
    // 开始时间戳, 结束时间戳
    time_t start,end;
    
    int food[25][20] = {0};	 // 果实对应数组
    int snakel[25][20] = {0};// 蛇身对应数组
    
    int score = 0;		// 得分
    int x = 0, y = 0;	// 坐标
    int direct = RIGHT;	// 移动方向
    int mark = 0;		// 标记原方向
    
    int length = 0;		// 蛇身链表长度
    int slength = 1;	// 蛇身记录长度
    
    // 方向数组
    int  dir[4][2]={
    		{ 1,  0 }, // 0 向右  x坐标 +1  y坐标 +0
    		{ 0, -1 }, // 1 向上  x坐标 +0  y坐标 -1
    		{-1,  0 }, // 2	向左  x坐标 -1  y坐标 +0
    		{ 0,  1 }  // 3 向下  x坐标 +0  y坐标 +1
    	};
    
    /*
    	snake 结构体 用于构建贪吃蛇链表
    */
    struct snake
    {
    	int x,y;
    	struct snake *next,*pre;
    } *head, *tail, *p; // 声明结构体的同时定义头指针、尾指针、临时指针
    
    
    DWORD WINAPI ThreadProc(LPVOID lpPraram)
    {
    	SYSTEMTIME sys; // 系统时间结构体
    
    	// 线程启动时, 头尾指针均为空
    	head = tail = NULL;
    	int a , b;
    
    	// 随即获取果实坐标 (a,b)
    	a=(rand()+start)%25; // [0,24]
    	b=(rand()+start)%20; // [0,19]
    
    	/* Note	起始时间 start + 返回的随机值 rand() = 一个不规律的随机值
    	 *      随后 % 求模 25 即将这个书限制在0到25之内 */
    
    	// 子线程循环打印蛇身和果实
    	while(1)
    	{
    		//打印已耗游戏时间;
    		end = time(NULL);// 当前时间
    		gotoxy(35,4);
    		printf("%03d", (int)difftime(end,start)); // 当前时间与开始时间的差值
    		
    		GetLocalTime(&sys); // 获取当前时间
    
    		//打印系统时间;
    		gotoxy(28,18);		
    		printf("%4d/%02d/%02d", sys.wYear, sys.wMonth, sys.wDay );
    		gotoxy(29,19);
    		printf("%02d:%02d:%02d", sys.wHour, sys.wMinute, sys.wSecond);
    
    		//判断果实是否被吃了
    		if(food[a][b] != '*' || snakel[a][b] == 1)
    		{
    			a=(rand()+start)%25; // [0,24]
    			b=(rand()+start)%20; // [0,19]
    			food[a][b]='*';
    		}
    
    		// 到屏幕的(a,b)坐标打印果实
    		gotoxy(a,b); printf("*");
    
    		//获取下一个坐标
    		x += dir[direct][0];
    		y += dir[direct][1];
    		x += 25; x %= 25;
    		y += 20; y %= 20;
    
    		// 如果果实(food)数组中当前坐标对应为果实
    		if(food[x][y]=='*')
    		{			
    			slength++;	// 蛇身长度自增
    			score++;	// 分数自增
    			// 刷新分数
    			gotoxy(35,3); printf("%03d",score);
    			// 清理果实(food)数组
    			food[x][y]=0;
    		}
    		// 若不是果实则,判断当前坐标是否为蛇身
    		else if(snakel[x][y]==1)
    		{
    			// 如果是蛇身即玩家吃到自己,游戏重启
    			restart();
    		}
    
    		snakel[x][y]=1;	// 蛇身对应新坐标置1
    		
    		// 创建新坐标对应的链表节点
    		p = (snake*)malloc(sizeof(snake));
    		p->x = x;
    		p->y = y;
    		p->next = p->pre = NULL;
    				
    		if(head == NULL) {
    		// 若只有一个节点(即蛇的长度为1,链表长度为0)
    			head = tail = p;
    		}
    		else
    		{
    		// 若不只一个节点
    			head->pre=p;	// 头结点的前一个指向 p 节点
    			p->next=head;	// p 节点下一个指向当前头结点
    			head=p;			// 新的 p 变成头结点变成
    		}
    
    		length++; // 链表长度加1 
    
    		// 到新坐标,也就是头部打印 #
    		gotoxy(head->x,head->y);
    		printf("#");
    
    		// 输出调试信息
    		gotoxy(49, 10); printf("length: %d slength: %d", length, slength);
    
    		// 删除蛇尾
    		if(length > slength)
    		{
    			// 清空蛇尾数组对应值
    			snakel[tail->x][tail->y]=0;
    			// 控制台输出流指针移动到蛇尾
    			gotoxy(tail->x,tail->y);
    			// 输出空格
    			printf(" ");
    
    			// 临时指针指向蛇尾
    			p = tail;
    			// 尾指针前移
    			tail = tail->pre;
    			// 释放原本的尾指针
    			free(p);
    			// 蛇身链表长度减1
    			length--;
    		}
    
    		/* 
    		 * 调控速度暂时休眠
    		 * 如果需要控制游戏速度只需要修改休眠的值即可
    		 */
    		_sleep(50);
    
    		// 标记当前方向
    		mark = direct;
    	}
    }
    
    /*
    	初始化配置
    */
    void init()
    {
    	int i;
    
    	// 绘制外围边框
    	for( i=0; i<20; i++)
    	{
    		gotoxy(39,i); printf("|");
    		gotoxy(25,i); printf("|");
    	}
    	for( i=0; i<40; i++)
    	{
    		gotoxy(i,20); printf("~");
    	}
    
    	// 右侧游戏记录, 规则说明
    	gotoxy(26, 3); printf("游戏得分:%03d",score);
    	gotoxy(26, 4); printf("游戏时间:");
    	gotoxy(29,17); printf("系统时间");
    
    	gotoxy(49, 1); printf("@lellansin");
    	gotoxy(49, 2); printf("www.lellansin.com");
    	
    	gotoxy(49, 4); printf("一群  10191598");
    	gotoxy(49, 5); printf("二群 163859361");
    	gotoxy(49, 6); printf("三群  10366953");
    
    	gotoxy(49, 8); printf("按ESC键退出");
    
    	// 初始化游戏开始时间
    	start = time(NULL);
    }
    
    int main()
    {	
    	int i = 0;
    	HANDLE sonThreadHandle; // 子线程句柄
    	DWORD dwThreadId;		// 子线程ID
    
    	// 按键上半部分以及下半部分
    	char keyCodePart1,keyCodePart2;
    
    	// 执行初始化
    	init();	
    	
    	//创建线程
    	sonThreadHandle = CreateThread(
    		NULL,		// 不能被子进程继承
    		0,			// 默认堆栈大小
    		ThreadProc,	// 线程调用函数过程
    		NULL,		// 传递参数
    		0,			// 创建后立即执行
    		&dwThreadId	// 保存创建后的线程ID
    	);
    
    	// 如果线程句柄为空,即新建线程失败
    	if(sonThreadHandle == NULL)
    	{
    		ExitProcess(i); // 退进程, 程序结束
    	}
    
    	// 主线程循环获取按键
    	while(1)
    	{
    		// getch 一次只能从输入流中读取一个字节
    		// 不过一个按键通常是两个字节
    		keyCodePart1 = getch();	// 获取第一个字节
    
    		// 判断是否等于逃脱键(Esc)的第一个字节
    		if(keyCodePart1 == 0x1b)
    		{
    			ExitProcess(2); // 是的话就退出进程
    		}
    
    		// 方向键第一个字节的值等于 -32
    		if(keyCodePart1 == -32)
    		{
    			keyCodePart2 = getch();	// 获取第二个字节
    			switch(keyCodePart2)
    			{
    				case(72):
    					// direct=1;
    					direct = UP;
    				break;
    				case(75):
    					// direct=2;
    					direct = LEFT;
    				break;
    				case(77):
    					// direct=0;
    					direct = RIGHT;
    				break;
    				case(80):
    					// direct=3;
    					direct = DOWN;
    				break;
    			}
    			// 如果当前方向与原方向相反
    			if( direct != mark && (direct+mark==RIGHT+LEFT||direct+mark==UP+DOWN) )
    				direct=mark;
    		}
    		/*
    			为什么是 0x1b 为什么是 -32 之类的值?
    			这些都是原本用同样的办法,两次 getch 之后
    			打印并记录下各个按键的值得到的判断
    		*/
    	}
    	return 0;
    }
    
    void restart()
    {
    	//判断死亡,重新开始游戏;
    	MessageBox(NULL,(LPCSTR)"傻逼,死了吧!!!Once More???",(LPCSTR)"GAME VOER!",MB_OK);
    
    	// 分数清零
    	score=0; gotoxy(35,3); printf("%03d",score);
    	// 开始时间重记
    	start = time(NULL);
    	// 游戏已进行时间打印为0
    	gotoxy(35,4); printf("%03d",0);
    
    	// 从尾部开始循环消除蛇身
    	while(tail)
    	{
    		// 休眠以延缓消除时间
    		_sleep(200);
    		snakel[tail->x][tail->y]=0;	// 清除当前蛇身表的值
    		// 移动到屏幕对应坐标输出宫格
    		gotoxy(tail->x,tail->y); printf(" ");
    		// 临时指针指向尾指针的前一个节点
    		p = tail->pre;
    		// 释放最后一个节点
    		free(tail);
    		// 临时节点变成新的尾部
    		tail = p;
    	}
    	// 所有节点释放完毕,头尾指针清空
    	head = tail = NULL;
    	slength = 1; // 蛇身长度置1
    	length=0;	 // 链表长度置0
    }
    
    
    /*
    	跳至控制台的(x,y)坐标
    */
    void gotoxy(int x,int y)
    {
    	COORD coord; // 控制台坐标结构体
    	coord.X = x;
    	coord.Y = y;
    	SetConsoleCursorPosition( 
    		GetStdHandle( STD_OUTPUT_HANDLE ), // 获取控制台输出流的句柄
    		coord	// 设置输出位置的坐标
    	);
    }
    

    源代码地址:snake.zip

    上一讲:Windows API 教程(四) 进程编程
    下一讲:Windows API 教程(六) 动态链接库

    Windows API教程(四) 进程编程

    茵蒂克丝

    博主很想详细的介绍一下进程神马的、内存神马的,但是真的整理起来发现要做到让有C基础的学生能很好理解,一看就懂还是感觉很有难度。也许是博主水平还不够吧。

    作为90后程序员,博主还是不走寻常路吧,到了进程这里开始,我们反过来学一下,顺便补一补一些基础知识。

    上一节文件编程的时候,有说过文件流的状态,当我们开始操作一个文件,这个文件的打开关闭都由程序控制,而其中的内容是可以动态增减的,这样的情况类似水流一般,我们把这样的文件状态称作文件流。

    那么实际上,用户的输入、输出这一类的“缓存”也就是这样的状态,通常是叫做输入流和输出流,除了这两个还有一个错误流。C语言中的标准库(stdio.h)中有定义这三个常用的流,也即stdin(标准输入流)、stdout(标准输出流)和stderr(标准错误流)。如果大家仔细深究 stdio.h 的头文件会发现,这些流的定义跟文件的定义实际上是同样的。

    好了,回到终点,那么既然这些东西都已经是 “流” 的状态了,那么也就意味着我们不需要去打开什么文件,来生成什么文件流,直接可以用文件操作的函数来操作这些输入输出流。

    例如:

    scanf("%d", &i);
    实际上就是 fscanf(stdin, "%d", &i); 的简写而已。
    

    PS:这实际上应该算是C语言的基础知识,不能理解为什么大学都不教(博主观点)

    CMD 工具集

    那么,开始有讲过的,windows编程其实就是去查API,然后用API。这一节讲进程,我们就通过编写一个学习用的cmd工具集来一步步走向进程编程。

    首先我们要从输入流来读取用户的输入,这一操作在标准库中可以使用 fgets 函数来实现,调用 windows API 的话我们就用 ReadFile 就可以了。

    BOOL WINAPI ReadFile(
      _In_         HANDLE hFile,				// 读取的文件句柄
      _Out_        LPVOID lpBuffer,				// 保存读取缓冲字符数组
      _In_         DWORD nNumberOfBytesToRead,	// 缓冲数组的大小
      _Out_opt_    LPDWORD lpNumberOfBytesRead,	// 实际读出的大小
      _Inout_opt_  LPOVERLAPPED lpOverlapped 	// 异步IO文件结构体
    );
    
    
    #include <Windows.h>;
    #include <stdio.h>;
    
    void welcome();
    
    int main()
    {
    	char Command_str[MAX_PATH];
    	DWORD Command_len;
    	HANDLE hConsoleInput;  
    
        // 获取输出流的句柄
        hConsoleInput = GetStdHandle(STD_INPUT_HANDLE);  
    
    	// 输出欢迎信息
    	welcome();
    
    	while(1)
    	{
    		// 清空命令字符串
    		memset(&Command_str, 0, MAX_PATH);
    		// 输出提示符
    		printf("nLscmd>;");
    		// 读取输入流
    		ReadFile(
    			hConsoleInput,	// 文件句柄
    			Command_str,	// 获取内容的缓冲字符数组
    			MAX_PATH,		// 缓冲数组大小
    			&Command_len,	// 实际读出的大小
    			NULL);
    
    		printf("接收到命令:[%s]", Command_str);
    	}
    }
    
    void welcome()
    {
    	printf("Lellansin's CMD Tool [版本 0.0.1]n");
    	printf("学习自制 (c) www.lellansin.com 欢迎交流n");
    }
    

    可以简单的看到,我们的输入都有获取到,并且输出的时候还连带我们输的回车(换行符)

    定制两个简单的命令

    #include <Windows.h>;
    #include <stdio.h>;
    #include <string.h>;	// for strcmp
    #include <stdlib.h>;	// for exit
    
    void welcome();
    void command_switch(char *cmd_str);
    
    int main()
    {
    	char Command_str[MAX_PATH];
    	DWORD Command_len;
    	HANDLE hConsoleInput;  
    
        // 获取输出流的句柄
        hConsoleInput = GetStdHandle(STD_INPUT_HANDLE);  
    
    	// 输出欢迎信息
    	welcome();
    
    	while(1)
    	{
    		// 清空命令字符串
    		memset(&Command_str, 0, MAX_PATH);
    		// 输出提示符
    		printf("nLscmd>;");
    		// 读取输入流
    		ReadFile(
    			hConsoleInput,	// 文件句柄
    			Command_str,	// 获取内容的缓冲字符数组
    			MAX_PATH,		// 缓冲数组大小
    			&Command_len,	// 实际读出的大小
    			NULL);
    
    		command_switch(Command_str);
    	}
    }
    
    void command_switch(char *cmd_str)
    {
    	char cmd_tmp[MAX_PATH]={0};
    	char *pstr = cmd_str, *ptmp = cmd_tmp;
    
    	// 一直赋值到换行之前
    	while(*pstr != '\r' && *pstr != '\n')
    	{
    		*ptmp++ = *pstr++;
    	}
    	// printf("收到命令:[%s]n", cmd_tmp);
    
    	// 判断命令
    	if( strcmp(cmd_tmp, "hi") == 0 )
    	{
    		printf("你好~");
    	} else if ( strcmp( cmd_tmp, "exit" ) == 0 )
    	{
    		exit(0);
    	}else
    	{
    		printf("Error: 命令未找到n");
    	}
    }
    
    void welcome()
    {
    	printf("Lellansin's CMD Tool [版本 0.0.1]n");
    	printf("学习自制 (c) www.lellansin.com 欢迎交流n");
    }
    

    创建进程

    那么到现在为止,我们的cmd工具已经有了一个基本的雏形,接下来要做的就是调用我们原来写的命令。

    有的同学可能会想到直接用 stdlib.h 里的 system 函数来解析命令,这个是可以的。但是这个函数是C标准库的,并不是windows系统的API,它的效率是非常的低的。而且,这里是在在讲的是 windows 编程所以读者请不要偷懒哦。

    首先,我们来了解一下进程的概念。当一个程序运行起来的时候,操作系统一定要为这个程序(Program)创建一个进程(Process),以方便管理。有些同学也知道每个程序跑起来之后都被分配了一个进程ID,实际上在进程调度的时候,操作系统还会为我们的程序分配进程的一些列事务:资源、虚拟内存地址空间、系统调用接口、优先级、环境变量等等。

    所以进程实际上可以理解成一个运行起来的程序,就如进程的英文原本的意思(Process)一样这是个运行过程。每个运行起来的程序都要被操作系统安排进程(过程)的来管理。

    如果你能想象这一过程,那么就不难理解,想要运行一个程序必须要创建一个进程。所以,这里我们就需要为我们原本所写的程序创建一个进程,就可以调用了。

    BOOL WINAPI CreateProcess(
      _In_opt_     LPCTSTR lpApplicationName,	// 启动程序路径
      _Inout_opt_  LPTSTR lpCommandLine,		// 启动程序的命令行代码
      _In_opt_     LPSECURITY_ATTRIBUTES lpProcessAttributes,	// 进程属性
      _In_opt_     LPSECURITY_ATTRIBUTES lpThreadAttributes,	// 线程属性
      _In_         BOOL bInheritHandles,		// 是否继承句柄
      _In_         DWORD dwCreationFlags,		// 标识和优先级
      _In_opt_     LPVOID lpEnvironment,		// 环境变量设置
      _In_opt_     LPCTSTR lpCurrentDirectory,	// 当前目录设置
      _In_         LPSTARTUPINFO lpStartupInfo,	// 启动信息设置
      _Out_        LPPROCESS_INFORMATION lpProcessInformation 	// 新进程的信息
    );
    

    MSDN 文档: CreateProcess function

    返回值

    如果函数执行成功,返回非零值。如果函数执行失败,返回零,可以使用GetLastError函数获得错误的附加信息。

    
    #include <Windows.h>;
    #include <stdio.h>;
    #include <string.h>;	// for strcmp
    #include <stdlib.h>;	// for exit
    
    void welcome();
    void command_switch(char *cmd_str);
    BOOL CreateChildProcess(char *cmd_str);
    
    int main()
    {
    	char Command_str[MAX_PATH];
    	DWORD Command_len;
    	HANDLE hConsoleInput;  
    
        // 获取输出流的句柄
        hConsoleInput = GetStdHandle(STD_INPUT_HANDLE);  
    
    	// 输出欢迎信息
    	welcome();
    
    	while(1)
    	{
    		// 清空命令字符串
    		ZeroMemory(&Command_str, MAX_PATH);
    		// 输出提示符
    		printf("nLscmd>;");
    		// 读取输入流
    		ReadFile(
    			hConsoleInput,	// 文件句柄
    			Command_str,	// 获取内容的缓冲字符数组
    			MAX_PATH,		// 缓冲数组大小
    			&Command_len,	// 实际读出的大小
    			NULL);
    
    		command_switch(Command_str);
    	}
    }
    
    void command_switch(char *cmd_str)
    {
    	char *pstr = cmd_str;
    
    	// 遍历到换行之前
    	while(*pstr != '\r' && *pstr != '\n')
    	{
    		*pstr++;
    	}
    	// 覆盖换行
    	*pstr = '\0';
    	// printf("收到命令:[%s]n", cmd_str);
    
    	// 判断命令
    	if( strcmp(cmd_str, "hi") == 0 )
    	{
    		printf("你好~ 欢迎使用 Lellansin 的cmd工具n");
    	} else if ( strcmp( cmd_str, "exit" ) == 0 )
    	{
    		exit(0);
    	}else
    	{
    		// 创建子进程
    		CreateChildProcess(cmd_str);
    	}
    }
    
    BOOL CreateChildProcess(char *cmd_str)
    {
    	STARTUPINFO start_info;
    	PROCESS_INFORMATION process_info;
    	BOOL flag;
    
    	// 将启动信息结构清零 ( 相当于 memset 0, 不过效率更高 )
    	ZeroMemory( &start_info, sizeof(start_info) );
    	// 设置结构大小,cb属性应为结构的大小
    	start_info.cb = sizeof(start_info);
    	// 将进程信息结构清零
    	ZeroMemory( &process_info, sizeof(process_info) );
    
    	flag = CreateProcess(
    		NULL,			// 不传程序路径, 使用命令行
    		cmd_str,		// 命令行命令
    		NULL,			// 不继承进程句柄(默认)
    		NULL,			// 不继承线程句柄(默认)
    		FALSE,			// 不继承句柄(默认)
    		0,				// 没有创建标志(默认)
    		NULL,			// 使用默认环境变量
    		NULL,			// 使用父进程的目录
    		&start_info,    // STARTUPINFO 结构
    		&process_info );// PROCESS_INFORMATION 保存相关信息
    
    	if ( !flag )
    	{
    		// 创建失败
    		printf( "Error: 命令未找到 (%d).n", GetLastError() );
    		return 0;
    	}
    
    	// 等待子进程结束
    	// 使用到了通过 PROCESS_INFORMATION 结构体获取子进程的句柄 hProcess
    	WaitForSingleObject( process_info.hProcess, INFINITE );
    	// 关闭进程句柄和线程句柄
    	CloseHandle( process_info.hProcess );
    	CloseHandle( process_info.hThread );
    
    	return 1;
    }
    
    void welcome()
    {
    	printf("Lellansin's CMD Tool [版本 0.0.1]n");
    	printf("学习自制 (c) www.lellansin.com 欢迎交流n");
    }
    

    查看进程

    CreateToolhelp32Snapshot

    获取当前的系统快照

    HANDLE WINAPI CreateToolhelp32Snapshot(
      _In_  DWORD dwFlags,
      _In_  DWORD th32ProcessID
    );
    

    dwFlags [输入参数]
    指明所需的系统快照。该参数可以是如下列表中的一个或多个值。

    Value Meaning
    TH32CS_INHERIT (0x80000000) 表明快照 (snapshot) 句柄是可以被继承 (inheritable) 。
    TH32CS_SNAPALL 包含所有系统中的所有进程和线程,加上指定进程中堆和模块的信息( 通过th32ProcessID特别指明的进程id,如果为0则无)。相当于指定 TH32CS_SNAPHEAPLIST, TH32CS_SNAPMODULE, TH32CS_SNAPPROCESS, 和 TH32CS_SNAPTHREAD 通过或运算(‘|’) 联合使用
    TH32CS_SNAPHEAPLIST (0x00000001) 快照中包含通过 th32ProcessID 指定进程的所有堆 (heaps) 信息。 想要列举堆的信息,请查询 Heap32ListFirst
    TH32CS_SNAPMODULE (0x00000008) 快照中包含通过 th32ProcessID 指定进程的所有modules信息。 想要列举modules信息,请查询 Module32First 。 如果函数失败报错ERROR_BAD_LENGTH,请重试此函数直到成功。
    TH32CS_SNAPMODULE32 (0x00000010) 快照中包含所有通过 th32ProcessID 指定进程的32位模块(modules)信息(通过64位调用也是返回32位)。 该flag可以与 TH32CS_SNAPMODULE 或者 TH32CS_SNAPALL 联合使用。如果函数失败报错 ERROR_BAD_LENGTH ,请重试此函数直到成功。
    TH32CS_SNAPPROCESS (0x00000002) 快照中包含系统中的所有进程 (processes) 信息。 想要列举 processes 信息,请查询 Process32First
    TH32CS_SNAPTHREAD (0x00000004) 快照中包含系统中的所有线程 (threads) 信息。想要列举 threads 信息, 请查询 Thread32First

    确认是否属于某个指定的进程,可以在列举线程信息的时候,拿进程标识与 THREADENTRY32 结构体的 th32OwnerProcessID 成员相比较来判断 。

    th32ProcessID [输入参数]
    如果为0则获取所有进程的快照,如果不为零则获取该进程id的信息

    返回值

    成功则返回该快照的句柄

    遍历进程信息

    BOOL WINAPI Process32First(
      _In_     HANDLE hSnapshot,
      _Inout_  LPPROCESSENTRY32 lppe
    );
    
    BOOL WINAPI Process32Next(
      _In_   HANDLE hSnapshot,
      _Out_  LPPROCESSENTRY32 lppe
    );
    

    与文件目录类似, Process32First 用于获取第一个进程的信息, Process32Next 用于获取下一个进程的信息。参数一 hSnapshot 是指获取的进程快照,参数二 lppe 则是一个指向 PROCESSENTRY32 结构体的指针(LP + PROCESSENTRY32)。

    PROCESSENTRY32 结构体

    系统进程快照中某一个进程信息的具体结构

    typedef struct tagPROCESSENTRY32
    {
        DWORD   dwSize;
        DWORD   cntUsage;
        DWORD   th32ProcessID;          // this process
        ULONG_PTR th32DefaultHeapID;
        DWORD   th32ModuleID;           // associated exe
        DWORD   cntThreads;
        DWORD   th32ParentProcessID;    // this process's parent process
        LONG    pcPriClassBase;         // Base priority of process's threads
        DWORD   dwFlags;
        CHAR    szExeFile[MAX_PATH];    // Path
    } PROCESSENTRY32;
    

    MSDN 文档 :
    CreateToolhelp32Snapshot
    Process32First
    Process32Next
    PROCESSENTRY32 结构体

    ps 查看进程列表

    再来新建一个项目名字叫做 ps ,记得要是空项目随后添加如下代码

    
    #include <Windows.h>
    #include <stdio.h>
    
    #include <TlHelp32.h>
    /*
    TlHelp32.h for
    	PROCESSENTRY32
    	CreateToolhelp32Snapshot()
    	Process32First()
    	Process32Next()
    */
    
    int main(int argc, char const *argv[])
    {
    	HANDLE hSnapshot;
    	HANDLE hProcess;
    	PROCESSENTRY32 pe32;
    	// 获取进程快照
    	hSnapshot = CreateToolhelp32Snapshot( TH32CS_SNAPPROCESS, 0 );
    	if( hSnapshot == INVALID_HANDLE_VALUE )
    	{
    		printf( "CreateToolhelp32Snapshot (of processes) 失败" );
    		return ;
    	}
    	// 设置输入参数,结构的大小
    	pe32.dwSize = sizeof( PROCESSENTRY32 );
    
    	// 开始列举进程信息
    	if( !Process32First( hSnapshot, &pe32 ) )
    	{
    		printf( "Process32First() 失败" );
    		CloseHandle( hSnapshot ); // 关闭句柄
    		return ;
    	}
    
    	printf("进程IDt父进程t线程数t优先级t进程名"); // 基本优先级
    	do {
    		// 打印进程相关信息
    		printf( "n%u", pe32.th32ProcessID );	// 进程id
    		printf( "t%u", pe32.th32ParentProcessID );	// 父进程id
    		printf( "t%d", pe32.cntThreads );		// 线程数
    		printf( "t%d", pe32.pcPriClassBase );	// 基本优先级
    		printf( "t%s", pe32.szExeFile );		// 进程名
    
    	} while( Process32Next( hSnapshot, &pe32 ) );
    
    	CloseHandle( hSnapshot );	//关闭句柄
    
    	return ;
    }
    

    因为是在命令行的环境下运行,所以直接F7生成exe即可,不需要直接执行。如果是拿lscmd来做实验的话,程序写好了还要找到exe复制到path目录下,感觉有点麻烦,为了方便大家也可以直接将程序的生成目录,改成博主开始使用的 F:mytools目录 (Visual Studio 如何设置生成目录)

    测试数据:

    Lellansin's CMD Tool [版本 0.0.1]
    学习自制 (c) www.lellansin.com 欢迎交流
    
    Lscmd>ps
    进程ID  父进程  线程数  优先级  进程名
    0       0       2       0       [System Process]
    4       0       106     8       System
    292     4       2       11      smss.exe
    392     380     49      8       avgrsa.exe
    432     392     10      8       avgcsrva.exe
    640     632     10      13      csrss.exe
    704     632     3       13      wininit.exe
    720     696     13      13      csrss.exe
    764     696     3       13      winlogon.exe
    812     704     10      9       services.exe
    824     704     7       9       lsass.exe
    832     704     11      8       lsm.exe
    940     812     10      8       svchost.exe
    1020    812     9       8       svchost.exe
    724     812     19      8       svchost.exe
    888     812     22      8       svchost.exe
    1732    812     33      8       avgwdsvc.exe
    1816    812     4       8       sqlwriter.exe
    1852    812     6       8       svchost.exe
    1924    812     4       8       vmware-usbarbitrator64.exe
    1976    812     6       8       vmnat.exe
    ...... 省略N条 ......
    7008    1676    3       8       notepad++.exe
    6204    1500    11      6       chrome.exe
    6600    1500    11      6       chrome.exe
    6628    1500    11      6       chrome.exe
    6920    1500    11      6       chrome.exe
    7716    3800    10      8       MSBuild.exe
    5980    724     5       8       audiodg.exe
    4132    1500    11      8       chrome.exe
    5048    1500    11      8       chrome.exe
    4976    2052    6       8       vcpkgsrv.exe
    8072    7196    5       8       mspdbsrv.exe
    4724    3800    8       8       vcpkgsrv.exe
    3844    720     5       8       conhost.exe
    7124    3400    1       8       ps.exe
    Lscmd>
    
    

    终止进程

    OpenProcess 获取进程句柄

    HANDLE WINAPI OpenProcess(
      _In_  DWORD dwDesiredAccess,
      _In_  BOOL bInheritHandle,
      _In_  DWORD dwProcessId
    );
    

    参数

    dwDesiredAccess [输入参数]

    进程句柄对该进程的访问权限。详见进程访问权限

    bInheritHandle [输入参数]

    句柄是否继承(填写 TRUE 或者 FALSE),如果继承(TRUE)那么如果该进程创建子进程的时候这个句柄也会被继承到子进程。

    dwProcessId [输入参数]

    将要打开(open)的进程ID。

    如果指定进程是系统进程 (0x00000000),该函数会失败并且最后的错误会是ERROR_INVALID_PARAMETER。如果指定进程是系统空闲进程(原文:Idle process 博主备注:一种内存管理进程,准确的来讲名字叫做 System Idle Process)或者某个子系统进程(原文:CSRSS processes 博主备注:准确的说是 Client Server Runtime Process 任务管理器里可以看到它 csrss.exe),该函数会失败并且最后的错误会是ERROR_ACCESS_DENIED 因为其访问限制会阻止用户级别(user-level)的代码获取其句柄。

    返回值

    如果函数成功,返回值为指定进程的句柄。
    如果函数失败,返回值为NULL。可以使用 GetLastError 函数获得错误的附加信息。

    MSDN: OpenProcess

    TerminateProcess

    终止(Terminate) + 进程(Process) = 终止进程(TerminateProcess)

    BOOL WINAPI TerminateProcess(
      _In_  HANDLE hProcess,
      _In_  UINT uExitCode
    );
    

    参数一 hProcess [输入参数]

    待终止的进程句柄。该句柄必须拥有 PROCESS_TERMINATE (进程终止) 的权限。更多信息,请查看进程的安全与访问权限

    参数二 uExitCode [输入参数]

    设置通过使用该方法退出的进程与线程的退出码?(exit code 嘛,博主觉得返回值更好理解一些)。 使用GetExitCodeProcess函数可以可以获取到进程退出时返回的值。使用GetExitCodeThread函数可以获取到线程退出时返回的值。

    返回值
    函数执行成功,返回值为非零。
    函数执行失败,返回值为零。更多信息可以通过GetLastError函数获取。

    kill 终止进程

    新建一个空项目名为 kill ,随后代码如下

    
    #include <windows.h>
    #include <stdio.h>
    
    void help();
    
    int main(int argc, char const *argv[])
    {
    	int ProcessID;
    	HANDLE hProcess;
    
    	// 如果只有一个参数
    	if(argc == 1)
    	{
    		help();
    		return 0;
    	}
    
    	// 如果有两个参数
    	if(argc == 2)
    	{
    		// 获取进程id
    		ProcessID = atoi(argv[1]);
    		// 获取进程句柄
    		hProcess = OpenProcess( PROCESS_ALL_ACCESS, FALSE, (DWORD)ProcessID );
    		// 终止进程
    		TerminateProcess(hProcess, 0);
    	}
    }
    
    void help()
    {
    	printf("终止进程n");
    	printf("kill <进程id>n");
    }
    

    使用演示:

    打开两个lscmd,第一个执行

    Lellansin's CMD Tool [版本 0.0.1]
    学习自制 (c) www.lellansin.com 欢迎交流
    
    Lscmd>cmd.exe
    Microsoft Windows [版本 6.1.7601]
    版权所有 (c) 2009 Microsoft Corporation。保留所有权利。
    
    C:UsersLellansin>
    
    

    执行cmd.exe之后发现程序切换到了cmd之下,这个时候我们在打开第二个 lscmd ,(注:如果你的 lscmd 放在path路径之下的话,博主的也就是F:mytools,可以直接 win+r 调出运行,输入 lscmd 回车即可调用出来)

    在我们的第二个CMD工具集中使用ps查看进程

    Lellansin's CMD Tool [版本 0.0.1]
    学习自制 (c) www.lellansin.com 欢迎交流
    
    Lscmd>ps
    进程ID  父进程  线程数  优先级  进程名
    ... 其余省略 ...
    1676    1956    36      8       explorer.exe
    7904    1676    1       8       lscmd.exe
    5844    7904    1       8       cmd.exe
    4180    1676    1       8       lscmd.exe
    5568    4180    1       8       ps.exe
    
    #通过观察进程列表可以发现我们使用lscmd打开的cmd.exe
    #其进程id是5844,那么我们用新鲜的kill.exe来试试它
    
    Lscmd>kill 5844
    
    #运行之后发现第一个lscmd中的cmd.exe退出来了,牛刀小试、程序ok
    
    

    更多改进

    1.首先工具集本身还缺乏一些过多的指令,比如我们常用的cd(切换目录)命令,我们可以继续自定义一些命令

    2.实际上关于一个进程我们还可以再获取更多的信息,也即我们的ps命令可以修改一下多加一个参数。形如: ps 1676 这样调用时则列举出进程id为1676的搜有信息,这个信息可以有很多,包括其进程具体的:

    • 线程信息(Thread32FirstThread32Next
    • 模块信息(Module32FirstMoudle32Next
    • 堆信息(Heap32ListFirstHeap32Next
    • 内存使用情况(GetProcessMemoryInfo

    3.kernel32.dll是windows的核心DLL,很多内核级别的API都需要从其中导出,其实上述的例子中关于查看进程信息的大部分介绍的都是该DLL导出的函数(如果上一个问题又解决的,仔细查看模块信息会可以找到到程序调用掉用的每一个DLL),不过不是在Windows API中而是在Tool help API中,需要引用的是 Tlhelp32.h (不是Windows.h)

    关于进程一些信息操作,除了kernel32.dll中的Tool help API还有一个 PS API (从Psapi.dll中导出头文件为 psapi.h ),大家有兴趣的可以搜搜看,然后尝试用其中的函数来改写。

    4.kill 命令需要我们通过一个进程的id来杀死它,有的时候找一个进程的id有些麻烦,不过找名字却很容易所以你可以考虑改写这个程序,让它可以通过名称来(etProcessIdByName函数)终止一个进程。

    5.设置与获取环境变量( GetEnvironmentStrings,GetEnvironmentVariableSetEnvironmentVariable等)属于支线部分,大家可以研究使用这个API来跳过设置PATH系统环境变量,直接弄一个程序内部的环境变量就可以了,这样程序的可移植性更高。

    小结

    任何一个程序,想要运行那么必须为这个进程分配空间并且分配一个唯一的进程标识(进程ID),在上面的例子中我们有看到这样的额一串数据:

    进程ID  父进程  线程数  优先级  进程名
    1676    1956    36      8       explorer.exe
    7904    1676    1       8       lscmd.exe
    5844    7904    1       8       cmd.exe
    4180    1676    1       8       lscmd.exe
    5568    4180    1       8       ps.exe
    

    在其中一个lscmd中打开cmd,我们很直观的就能看到cmd.exe的父进程就是该lscmd.exe。
    仔细看我们可以发现,我打开了两个lscmd程序,而这个两个进程的父进程则是explorer.exe(1676)即我们的桌面,不论是通过win+r调用还是双击打开,实际上都需要explorer为其新建一个进程来运行我们打开的程序。

    概念上理解之后,剩下的就是熟悉API了,各位可以参照上面的 “更多改进” 来编写一些程序提高API的熟练度

    相关函数

    大部分与进程或线程有关的函数可以在MSND的 Process and Thread Functions 中被找到。小部分,例如 PS API 中的一些函数就找不到了。

    文章索引

    上一讲:windows 编程之路(三) 文件系统
    下一讲:windows 编程之路(五) 线程编程