第3回 黑客眼中的Windows程序设计

你介绍Windows API、动态链接库、进程、线程,以及网络编程等有关黑客编程的基础知识。

话说张飞在Visual C++ 2008的编程环境下学习了编程数天,基本把语法都学会了,但继而就遇到了瓶颈,自己编写的那些傻瓜程序,根本体现不出什么作用,更不用说是用来入侵宿敌曹操的电脑了,实在是让人泄气。

还需要利用什么才能编写像样的黑客工具呢?他只好前去请教诸葛亮,终于得到了自己想要的答案:“调用Windows API”。张飞欣喜若狂,在诸葛亮的帮助下,对API这个东西有了一定的了解。他发现API被传得神乎其神,有说是盘古开天辟地时留下的战斧,更有说得到这把战斧,就可以劈山成河。仔细想想,API虽不至于如传说的这般,但斩了曹操的电脑肯定不在话下。张飞于是奋发图强,没过多久就把Windows API学了个熟练。Windows API的本质就是Windows系统留给程序员编程的接口,它确实功能强大,完全可以帮助自己编写和黑客有关的程序。张飞心里沾沾自喜,已经构思出了入侵曹操电脑的伟大宏图。

张飞,你最近的学习进展如何?是否熟练掌握了VC++ 2008平台?

军师,俺正是为这事来特意请教您呢。VC++ 2008早已上手,写些简单的代码也不成问题,但是到底怎么样才能编写出真正的黑客工具呢?

心急吃不了热豆腐,在熟练掌握C/C++语言之后,就可以进入黑客程序设计阶段了,我们可以使用Windows操作系统提供给很多应用程序的编程接口(API, Application Programming Interface,应用程序编程接口),这些接口为黑客编程提供了平台。

API真的有这么强大么?难道它就是我要寻找的法宝?

API自然功能强大,详细了解API对黑客编程人员来说是非常有必要的。

那好,军师今天就讲讲这个吧。

那我便简要概述下Windows程序设计的主要方法,其中包括Windows API、动态链接库、进程和线程等知识,当然,通过以后具体的编程还可以进一步理解Windows程序设计。

3.1 WindowsAPI简介

什么是Windows API?

简单说来,Windows API即为Microsoft 32位平台的应用程序编程接口,所有在Win32平台上运行的应用程序都可以调用这些函数。用户的每个动作都会引发一个或几个函数的运行,以告诉Windows发生了什么。

3.1.1 Windows API概述

具体来说,API(Application Programming Interface,应用程序编程接口)是一些预先定义的函数,目的是向应用程序与开发人员提供基于某软件或硬件的访问一组例程的能力,而又无须访问源码,或理解内部工作机制的细节。Windows这个多作业系统除了可以协调应用程序的执行、内存分配、资源管理之外,还是一个很大的服务中心,可以调用服务中心的各种服务。Windows可以帮助应用程序达到开启视窗、描绘图形、使用周边设备等目的。这些服务就是一些函数,Windows留给开发人员的一些函数接口。

3.1.2 Windows API分类

Windows具有上千个API函数,包含了各种窗口类和系统资源。利用这些函数,可以编写出常见的具有Windows风格的程序。利用高级语言,能直接或间接地应用API进行应用程序的开发。

上千个API啊,这么多,俺老张可记不住啊!

没有你想象的那么复杂,Windows已经把它们分为了几类,非常容易掌握。

都有哪几类呢?

标准的Win32 API函数可以分为几大类:窗口管理、窗口通用控制、Shell特性类、图形设备接口(GDI)、系统服务、国际特性和网络服务。我把每一类具体解释下吧,免得你搞不懂。

(1)窗口管理

窗口管理函数向应用程序提供了一些创建和管理用户界面的方法。开发人员可以使用窗口管理函数,创建和使用窗口来显示输出、提示用户进行输入,以及完成其他一些与用户进行交互所需的工作。大多数应用程序都至少要创建一个窗口。应用程序通过创建窗口类及相应的窗口过程来定义它们所用窗口的外观和行为。窗口类可标识窗口的默认属性,比如窗口是否接受双击鼠标的操作,或是否带有菜单等。窗口过程中包含的代码用于定义窗口的行为、完成所需的任务,以及处理用户的输入。窗口管理函数还提供了其他一些与窗口有关的特性,比如插入标记(Caret)、剪贴板、光标、挂钩(Hook)、图标及菜单等函数。

(2)窗口通用控制类

系统SHELL提供了一些控制,使用这些控制可以使窗口具有与众不同的外观,通用控制是由通用控制库COMCTL32.DLL提供的。

(3)Shell特性类

应用程序可以使用这些类来增强系统各方面的功能。

(4)图形设备接口(GDI)

该接口提供了一系列的函数和相关的结构,应用程序可以使用它们在显示器、打印机或其他设备上生成图形化的输出结果。使用GDI函数可以绘制直线、曲线、闭合图形、路径、文本及位图图像。所绘制的图形的颜色和风格依赖于所创建的绘图对象,即画笔、笔刷和字体。可以使用画笔来绘制直线和曲线,使用笔刷来填充闭合图形的内部,使用字体来书写文本。

(5)系统服务

该类函数为应用程序提供了访问计算机资源及底层操作系统特性的手段,比如访问内存、文件系统、设备、进程和线程。应用程序使用系统服务函数来管理和监视它所需要的资源。例如,应用程序可使用内存管理函数来分配和释放内存,使用进程管理和同步函数来启动和调整多个应用程序或在一个应用程序中运行的多个线程的操作。

系统服务函数提供了访问文件、目录及输入输出(I/O)设备的手段。应用程序使用文件I/O函数可以访问保存在指定计算机,以及网络计算机磁盘和其他存储设备上的文件和目录。这些函数支持各种文件系统,从FAT文件系统、CD-ROM文件系统(CDFS)到NTFS。

系统访问函数为应用程序提供了一些可以与其他应用程序共享代码或信息的方法。使用系统服务函数,可以访问有关系统和其他应用程序的信息。应用程序使用系统服务函数,可以处理执行过程中的一些特殊情况,比如错误处理、事件日志、异常处理以及一些可用于调试和提高性能的属性。系统服务函数还提供了一些特性,可用于创建其他类型的应用程序,比如控制台应用程序和服务。

(6)国际特性

这些特性有助于用户编写国际化的应用程序。Unicode字符集使用16位的字符值来表示计算过程中所用到的字符,比如各种符号及很多编程语言。国家语言支持(NLS)函数可帮助用户将应用程序本地化;输入方法编辑器(IME)函数(在Windows亚洲版中可用)可帮助用户输入包含Unicode和DCBS字符的文本。

(7)网络服务

允许网络上的不同计算机之间的不同应用程序进行通信,用于在各计算机上创建和管理共享资源的连接,如共享目录和网络打印机。

网络服务接口主要包含Windows网络函数、Windows套接字(Socket)、NetBIOS、RAS、SNMP、Net函数和网络DDE。

张将军!张黑子!

干啥!

你睡觉就睡嘛,干嘛让口水把书打湿了。

你管天管地,难道还管俺老张睡觉流口水!

但是那本书是我的。

呃,我想起了,我现在只是了解了有API这个东西,但是具体怎么使用还请军师明示。

又转移话题……

3.1.3 使用Windows API

传统的Windows编程主要是利用API函数,而Windows API是在以C语言作为主要通用语言的时代开发的,因此经常用来在Windows和应用程序之间传送数据的是结构体。调用API函数的方法和平常调用函数的方法一致,要注意的是API函数的参数较为规范。在使用API的时候,经常看到头文件中包含“Windows. h”,该文件定义了Windows应用程序中包含的种类繁多的数据类型。这些数据类型,经常作为API函数的参数或者返回值。

空说无凭,军师就写个代码让俺瞅瞅呗。

那我就通过一个简单的例子来说明Windows API的使用方法。

1)启动Visual C++ 2008,通过执行“文件”→“新建”→“项目”命令,在弹出的对话框中选择“Win32项目”选项,项目名称为“UseAPI”。在随后的应用程序向导中选择“控制台应用程序”选项,且设置为“空项目”。

2)为工程添加资源文件“UseMain.cpp”,其代码如下。

        #include<windows.h>  //包含头文件
        ////////////////////////////////////////////////////////////////
        //主函数
        ////////////////////////////////////////////////////////////////
        int main( )
        {
          //使用API的提示对话框函数
          MessageBox(NULL, "欢迎来到黑客编程世界", "提示", MB_OK);
          return 0;
        }

3)利用快捷键“F5”编译调试工程,使用API显示提示对话框,如图3.1所示。

图3.1

太神奇了,军师果然神通广大,一个MessageBox()就超过了俺以前写的所有的代码。

这个可是API的功劳啊,所以你一定要认真研习之。

3.2 动态链接库简介

3.2.1 动态链接库概述

话说这个Windows API太好用了,方便了俺老张。

呵呵,Windows API就是为了方便,让大家在不用了解底层的情况下就可以完成某些功能。

那军师你说有没有更方便的东西,让俺这种懒人不费吹灰之力就能写出强大的黑客工具?

想得倒美,不过Windows为了方便我们调用API,还发明了一种叫做动态链接库的东西,我跟你介绍下它。

动态链接库(DLL, Dynamic Link Library的缩写形式)是作为共享函数库的可执行文件。动态链接库提供了一种方法,使进程可以调用不属于其自身可执行代码的函数。函数的可执行代码位于一个DLL中,该DLL包含一个或多个已被编译、链接并与使用它们的进程分开存储的函数。DLL还有助于共享数据和资源。多个应用程序可同时访问内存中单个DLL副本的内容。DLL是一个包含可由多个程序同时使用的代码和数据的库。例如,在Windows操作系统中,Comdlg32.DLL执行与对话框有关的常见函数。因此,每个程序都可以使用该DLL中包含的功能来实现“打开”对话框。这有助于促进代码重用和内存的有效使用。

微软为什么要发明DLL啊,它都有哪些好处?

简要来说有两点:

通过使用DLL,程序可以实现模块化,使之由相对独立的组件构成。例如,一个程序可以按模块来销售。可以在运行时将各个模块加载到主程序中(如果安装了相应模块)。因为模块是彼此独立的,所以程序的加载速度更快,而且模块只在相应的功能被请求时才被加载。

此外,可以更为容易地将更新应用于各个模块,而不会影响该程序的其他部分。例如,用户可能具有一个存款计算程序,而利率每年都会更改。当这些更改被隔离到DLL中以后,无需重新生成或安装整个程序,就可以应用更新。

太好了,这确实方便了不少,适合俺老张这种懒人。

创建动态链接库工程的方法和创建其他工程差别不大,下面就为你演示下。

3.2.2 编写动态链接库

1)新建项目,选择Win32项目,在名称中输入“CustomDll”,单击“确定”按钮。在随后出现的Win32应用程序向导中单击“下一步”按钮,进行如图3.2所示的选择,单击“完成”按钮,完成该工程的创建。

图3.2

2)从菜单栏中单击“项目”→“添加新项”命令,在弹出的“添加新项”对话框中选择“头文件(.h)”,在“名称”文本框中输入“CustomDll”,单击“添加”按钮,并在其中加入如下代码。

        #ifdef CustomDLL_EXPORTS                       //预定义宏
        #define CustomDLL_API__declspec(dllexport)
        #else
        #define CustomDLL_API__declspec(dllimport)
        #endif
        //声明自定义导出函数
        CustomDLL_APIint DLLFuncAdd(int a, int b);

3)从菜单栏中单击“项目”→“添加新项”命令,在弹出的“添加新项”对话框中选择“C++文件(.cpp)”,在“名称”文本框中输入“CustomDll”,单击“添加”,并在其中加入如下代码。

        #include "CustomDll.h"
        #include <windows.h>
        BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call,
                            LPVOID lpReserved
                       )
        {
            switch(ul_reason_for_call)
            {
            //动态链接库被映射到某个进程的地址空间
            case DLL_PROCESS_ATTACH:
            //应用程序创建新的线程
            case DLL_THREAD_ATTACH:
            //应用程序某个线程正常终止
            case DLL_THREAD_DETACH:
            //动态链接库将被卸载
            case DLL_PROCESS_DETACH:
                break;
            }
            return TRUE;
        }
        //自定义导出函数,求两个整数的和
        int DLLFuncAdd(int a, int b)
        {
            return a+b;
        }

4)从菜单栏中单击“生成”→“生成解决方案”命令(快捷键“F7”),编译链接程序。

这段代码好奇怪啊,俺只知道main()函数,从没见过什么DllMain()。

这个DllMain()就是动态链接库的入口点,Windows在库装载、卸载,线程创建和结束时都要调用入口函数,以便动态链接库可以采取相应的动作。

DllMain函数中,hModule参数是该DLL模块的句柄,代表这个文件的映像加载到进程的地址空间时使用的基地址。ul_reason_for_call参数的值表示本次调用的原因,可能是下列4种情况中的某一种。

● DLL_PROCESS_ATTACH:表示动态链接库刚被某个进程加载。程序可以在这里做一些初始化工作,并返回TRUE表示初始化成功,返回FALSE表示初始化出错,这样库的装载就会失败。这给了动态链接库一个选择是否被载入的机会。

● DLL_PROCESS_DETACH:表示动态链接库将被卸载。程序可以在这里进行一些资源的释放工作,如关闭文件、释放内存等。

● DLL_THREAD_ATTACH:表示应用程序创建了一个新的线程。

● DLL_THREAD_DETACH:表示某个线程正常终止。

DLL都还没搞清楚,进程和线程又钻出来了。军师,你耍俺啊?

谁叫你平时不多看点书,至于这里提到的进程与线程的概念,我待会再告诉你吧,现在先说DLL函数。

DLL能够定义两种函数:内部函数和导出函数。内部函数只能被定义这个函数的模块调用,而导出函数不仅可以在本模块调用,还可以被其他模块调用。

DLL的主要功能是向外导出函数,供进程中的其他模块使用。要想将函数导出供其他模块调用,需要在头文件中进行声明。例如,在本例中的函数即为导出函数,在头文件“CustomDll.h”中进行了声明。

3.2.3 使用动态链接库

调用DLL有两种方法:隐式动态链接和显式动态链接。

这倒是好记,一隐一显。军师让我看看它们具体是怎么调用的。

1.隐式动态链接

模块可以像调用本地函数一样调用从其他模块导入的函数,但这种方法必须使用DLL的导入库(.lib文件),它为系统提供了加载这个DLL和定位DLL中的导入函数所需的信息。

新建一个名称为“TestDll”的Win32控制台项目,并添加C++文件“TestDll. cpp”。然后在前面编写的DLL项目中把“CustomDll.h”、“CustomDll.lib”、“CustomDll.dll”三个文件复制到TestDll目录下,最后在“TestDll.cpp”文件中加入如下代码。

        #include <windows.h>
        #include <iostream>
        #include "CustomDll.h"
        using namespace std;
        #pragma comment(lib, "CustomDll")
        int main( )
        {
            int c = 12, d = 20;
            int result;
            result = DLLFuncAdd(c, d);
            cout<<"如果输出32,则表示成功调用了DLL。输出结果是:"<<result<<endl;
            return 0;
        }

#pragma命令指明要链接到“CustomDll.lib”库。也可以不使用该语句,但要将“CustomDll.lib”文件添加到项目中。使用隐式动态链接动态库要指明库文件所在的路径,并且静态编译会导致生成的可执行文件体积变大,这也是用该方法加载DLL库的缺点。按“Ctrl+F5”组合链编译运行程序,显示结果如图3.3所示。

图3.3

2.显式动态链接

模块使用LoadLibrary( )或者LoadLibraryEx( )函数显式加载DLL。DLL被加载之后,加载模块调用GetProcAddress( )函数取得DLL导出函数的地址,然后通过函数地址调用DLL中的函数。显示动态链接是在程序运行过程中显式地去加载DLL库,从中导出需要的函数。一般需要在原DLL工程中建立一个模块定义文件(DEF)来指定要导出的函数。

在原CustomDll工程中,单击菜单栏的“项目”→“添加新项”命令,在弹出的“添加新项”对话框中选择“模块定义文件(.def)”,输入名称“Custo-mDll”,单击“添加”按钮,并在其中加入如下内容:

        EXPORTS
        DLLFuncAdd

这两行代码说明此DLL库要向外导出DLLFuncAdd函数,最后按“F7”键重新编译CustomDll工程。回到工程TestDll中,将代码做如下修改。

        #include <windows.h>
        #include <iostream>
        using namespace std;
        //声明DLL里的函数原型
        typedef int(*PFNEXPORTFUNC)(int, int);
        int main( )
        {
            int c = 12, d = 20;
            int result;
            HMODULE hModule = LoadLibrary("CustomDll.dll");
            if(hModule ! = NULL)
            {
              PFNEXPORTFUNC mDLLFuncAdd =(PFNEXPORTFUNC)
              GetProcAddress(hModule, "DLLFuncAdd");
              if(mDLLFuncAdd ! = NULL)
              {
                  result = mDLLFuncAdd(c, d);
                  cout<<"如果输出32,则表示成功调用了DLL。输出结果是:
                  "<<result<<endl;
              }
              FreeLibrary(hModule);
            }
            return 0;
        }

运行程序,结果和上面的一样,同样输出“32”。

军师,俺觉着第一种方法不错,简单方便,倒是第二种方法太过复杂,俺不学了中不?

这万万不可,我重点给你介绍的就是第二种方法,它在黑客编程中经常会用到。

啊,那你再详细给俺说说。

调用DLL导出函数分为3步进行。

1)声明要导出的DLL函数,注意类型和参数的一致性。

2)加载目标DLL,利用LoadLibrary( )函数加载指定目录下的DLL库到进程的虚拟地址空间,函数执行成功,返回此DLL模块的句柄,否则返回NULL。

LoadLibrary( )函数声明如下:

        HMODULE WINAPI
        LoadLibrary(
        LPCTSTR  lpFileName
        );

功能:加载指定名称的动态链接库。

参数:

lpFileName:动态链接库的名称。

返回值:函数执行成功返回动态链接库的句柄。

3)最后由GetProcAddress( )函数来获得目标DLL中导出函数的地址。

GetProcAddress( )函数声明如下:

        FARPROC GetProcAddress(
          HMODULE                hModule,
          LPCSTR                 lpProcName
        );

功能:获得目标动态链接库中导出函数的地址。

参数:

hModule:函数所在模块的句柄。

lpProcName:函数的名称。

返回值:函数执行成功返回函数的地址,否则返回NULL。

当不使用DLL模块时,应调用FreeLibrary( )函数释放它占用的资源。

3.3 进程与线程简介

军师,现在你可以说说那个什么“进城”、“县城”之类的东西了吧?

是“进程”与“线程”。

反正俺也不懂,你就讲吧。

嗯,我这就慢慢道来。

3.3.1 进程与线程的概述

1.进程

对应用程序来说,进程就像一个大容器。当应用程序运行时,就好比被装入到这个容器中了,同时,还可以往该容器中加入其他配件,例如,应用程序在运行时所需的变量数据和需要引用的DLL文件等。当应用程序被重复运行时,系统会找一个新的进程容器来容纳第二次运行时键入的新配件。

进程是操作系统结构的基础;是一个正在执行的程序;是计算机中正在运行的程序实例;是可以分配给处理器并由处理器执行的一个实体;是由单一顺序的执行显示,一个当前状态和一组相关的系统资源所描述的活动单元。

2.线程

是进程中的一个实体,是被系统独立调度和分派的基本单位。线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中体现出间断性。线程因此有就绪、阻塞和运行三种基本状态。线程是程序中一个单一的顺序控制流程。在单个程序中同时运行多个线程,完成不同的工作称为多线程。

怎么听起来这两个东西差不多呢,它们的区别在哪里?

我还没说它们之间的异同呢,你听好了。

3.3.2 进程与线程的异同

进程由进程控制块、程序段、数据段三部分组成。一个进程可以包含若干线程,线程可以帮助应用程序同时做几件事(比如一个线程向磁盘写入文件,另一个则接收用户的按键操作并及时做出反应,互不干扰),在程序运行中,系统首先要做的就是为该程序进程建立一个默认线程,默认线程也称为主线程。然后,程序可以根据需要自行添加或删除相关的线程,是可并发执行的程序。数据集合的运行过程,是系统进行资源分配和调度的一个独立单位,也称为活动、路径或任务,它有两方面性质:活动性和并发性。进程可以划分为运行、阻塞、就绪三种状态,并随一定条件而相互转化:就绪—运行,运行—阻塞,阻塞—就绪。

线程和进程的区别在于,任意两个进程有不同的代码和数据空间,而多个线程则共享数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文。线程的运行需要占用计算机的内存资源和CPU。

简而言之,一个程序至少有一个进程,一个进程至少有一个线程。线程的划分尺度小于进程,使得多线程程序的并发性高。另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

神呐,请听听军师在胡说八道些什么吧!他让俺昏昏欲睡,欲睡昏昏,就是听不懂。

多说无益,这两个概念是需要你多加揣摩才行的。

我需要一听就懂,一摸就会,马上成为高得不能再高的高端职业黑客。

这倒也不难。

真的?

蒸的,还煮的呢。你给我一百元,我告诉你。

太好了,拿去。

恭喜你,你已经被我黑了,你学会了吧!

……原来是骗我钱啊!

除了骗子,你认为还有什么可以不劳而获。

3.4 Windows网络编程基础

张将军,你平时对于网络了解有多少?

军师,你可算是问对人了,俺平时酷爱上网,什么网络游戏啊、QQ啊玩得可转了。

咳咳,你只知其表面,却完全不知它们是如何实现的。

哦,那军师你倒是说说它们是如何实现的呢?

网络数据的传输依靠TCP/IP协议,而在Windows下编写QQ之类的软件还得依靠强大的API——Winsock API。

咦,还有这种事?

3.4.1 TCP/IP协议概述

确切地说,TCP/IP协议是一组包括TCP协议和IP协议,UDP(User Datagram Protocol)协议、ICMP(Internet Control Message Protocol)协议和其他一些协议的协议组。TCP/IP协议组之所以流行,部分原因是它可以用在各种各样的信道和底层协议(如T1和X.25、以太网,以及RS-232串行接口)之上。

传统的开放式系统互连参考模型是一种通信协议的七层抽象的参考模型,称为OSI七层模型。其中每一层都执行某一特定任务。该模型的目的是使各种硬件在相同的层次上相互通信。这七层是:物理层、数据链路层、网路层、传输层、话路层、表示层和应用层。

TCP/IP协议并不完全符合OSI的七层参考模型。而TCP/IP通信协议采用了四层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求。这四层分别为:

1)应用层——应用程序间沟通的层,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。

2)传输层——在此层中,提供了节点间的数据传送服务,如传输控制协议(TCP)、用户数据报协议(UDP)等,TCP和UDP给数据包加入传输数据并把它传输到下一层中,这一层负责传送数据。

3)互连网络层——负责提供基本的数据封包传送功能,让每一块数据包都能够到达目的主机(但不检查是否被正确接收),如网际协议(IP)。

4)网络接口层——对实际的网络媒体的管理,定义如何使用实际网络(如Ethernet、Serial Line等)来传送数据。

3.4.2 Winsock入门

Windows Sockets(Windows套接字,简称Winsock)是Windows下得到广泛应用的、开放的、支持多种协议的网络编程接口。Winsock规范是以U.C. Berkeley大学BSD UNIX中流行的Socket接口为范例定义的一套Microsoft Windows下网络编程接口。它不仅包含了人们所熟悉的Berkeley Socket风格的库函数,也包含了一组针对Windows的扩展库函数,以使程序员能充分地利用Windows消息驱动机制进行编程。Windows Sockets规范本意在于提供给应用程序开发者一套简单的API,并让各家网络软件供应商共同遵守。

任何能够与Windows Sockets兼容实现协同工作的应用程序就被认为是具有Windows Sockets接口。通常称这种应用程序为Windows Sockets应用程序。Windows Sockets规范定义并记录了如何使用API与Internet协议族连接,尤其要指出的是所有的Windows Sockets实现都支持流套接字和数据报套接字。应用程序调用Windows Sockets的API实现相互之间的通信。Windows Sockets又利用下层的网络通信协议功能和操作系统调用实现实际的通信工作。

通信的基础是套接字(Socket),一个套接字是通信的一端。在这一端上可以找到与其对应的一个名字。一个正在被使用的套接字都有它的类型和与其相关的进程。套接字存在于通信域中。通信域是为了处理一般的线程通过套接字通信而引进的一种抽象概念。套接字通常和同一个域中的套接字交换数据(数据交换也可能穿越域的界限,但这时一定要执行某种解释程序)。Windows Sockets规范支持单一的通信域,即Internet域。各种进程使用这个域及Internet协议族来进行相互通信。利用Socket进行通信,有两种主要的方式:流套接字和数据报套接字。

流套接字提供了双向的、有序的、无重复并且无记录边界的数据流服务。数据报套接字支持双向的数据流,但并不保证是可靠、有序、无重复的。也就是说,一个从数据报套接字接收信息的进程有可能发现信息重复了,或者和发出时的顺序不同。典型应用为向网络发送广播数据包。

又是长篇大论,军师,俺都快要睡着了。

其实,简要来说,我们用一张图就可以概括。

……你干嘛不早说,这不是忽悠俺老张嘛。

图3.4

1. Winsock工作方式

两个TCP套接字在开始传输数据之前,必须先建立一个连接,也就是说由一方首先向另一方发送请求信息,双方互相确认后才能开始通信。TCP通信采用超时及重传机制来保证不丢失数据,确保可靠性。从TCP协议的特征可以看出通信双方的工作方式是不同的,这种工作方式可以用客户机/服务器模型(Client/Server或C/S)来描述,通信的发起端称为客户机,通信等待方称为服务器,如图3.5所示,显示了两者工作方式的不同。

图3.5

客户端在发送UDP数据包前,不需要先与服务器端进行相互确认,不管服务器是否在线,是否已经准备接收UDP数据包,客户端都可以随时发送数据。由于UDP协议是无连接的,所以通过一个UDP套接字可以向任何服务器地址发送数据,而不需要创建多个套接字。但UDP协议不对数据的可靠性与有序性等进行控制,它不是面向连接的,而是直接将数据包按照指定IP地址和端口发出,如果对方不在线,则数据就会丢失。

在客户端,UDP套接字不必经过连接的过程就可以直接发送数据。在服务器端,UDP套接字无所谓是否进入“监听”状态,只要有数据发送到套接字对应的端口,就可以被接收,所以TCP类型程序架构中的连接(connect)、监听(listen)和接受连接(accept)等动作都是不存在的。服务器和客户端唯一的区别就是服务器端程序必须首先将套接字绑定到一个固定的端口,以便客户端能够向约定的端口发送数据,如图3.6所示。

图3.6

这个WinSock相关的API好多啊。

共有11个,我已经帮你总结出来了,这些API都是以后进行网络编程时要熟练使用的哦。

2. Winsock相关函数

(1)WSAStarup( )函数

        int WSAStartup(
        WORD             wVersionRequired,
        LPWSADATA       lpWSAData
        );

功能:该函数用于初始化WS2_32动态库,一般是应用程序第一个调用的Windows Socket函数,用于确定Windows Socket使用的版本。

参数:

wVersionRequired表示调用者使用的Windows Socket的版本,高字节记录修订版本,低字节记录主版本。

lpWSAData,一个WSADATA结构指针,该结构记录了Windows Socket的详细信息。

返回值:函数执行成功返回0,否则返回错误代号。

(2)socket( )函数

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

功能:该函数用于创建一个套接字,为发起连接做准备。

参数:

af表示一个地址家族,通常为AF_INET。

type表示套接字类型,如果为SOCK_STREAM,表示创建面向连接的流式套接字;如果为SOCK_DGRAM,表示创建面向无连接的数据包套接字;如果为SOCK_RAW,表示创建原始套接字。

protocol表示一个特殊的协议被用于这个套接字。

返回值:函数执行成功,返回创建的套接字句柄,否则返回INVALID_SOCKET。

(3)connect( )函数

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

功能:使用TCP套接字进行通信时,客户端必须使用该函数用于发送一个连接请求,连接到指定端口。发起连接时,系统会自动为套接字选择一个空闲的端口,如果一定要用特定的端口连接到服务器端,可以在调用connect函数前用bind函数将套接字绑定到指定的端口上。

参数:

s表示一个套接字标识。

name表示套接字s想要连接的主机地址和端口号。

namelen是name缓冲区的长度。

返回值:函数执行成功返回0,否则返回SOCKET_ERROR。

(4)bind( )函数

        int bind(
        SOCKET                             s,
        const struct sockaddr FAR*       addr,
        int                                namelen
        );

功能:不管是TCP套接字还是UDP套接字,如果要套接字指定IP地址和端口监听,必须使用该函数将它绑定到该IP和端口上。绑定成功后,UDP套接字马上就可以接收数据包了。但TCP套接字还需要用listen函数进行监听,并使用accept函数接受进入的连接后才能进行通信。

参数:

s表示套接字标识。

addr是一个sockaddr结构指针,该结构中包含了要结合的地址和端口号。

namelen是addr缓冲区的长度。

返回值:函数执行成功返回0,否则返回SOCKET_ERROR。

(5)listen( )函数

        int listen(
        SOCKET           s,
        int               backlog
        );

功能:该函数用于将TCP套接字设置为监听状态。

参数:

s表示套接字标识。

backlog表示等待连接的最大队列长度。

返回值:函数执行成功返回0,否则返回SOCKET_ERROR。

(6)accept( )函数

        SOCKET accept(
        SOCKET                         s,
        struct sockaddr FAR*               addr,
        int FAR*                         addrlen
        );

功能:当客户端向监听的TCP套接字发起连接后,必须对监听的套接字调用该函数,然后连接才最后被确定。

参数:

s是一个套接字,应处于监听状态。

addr是一个sockaddr_in结构指针,包含一组客户端的端口号、IP地址等信息。

addrlen用于接收参数addr的长度。

返回值:函数执行成功,返回一个新的对应于已经接受的客户端连接的套接字,否则返回INVALID_SOCKET。

(7)send( )函数

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

功能:在使用TCP连接时,用该函数向对方发送数据包。

参数:

s表示套接字标识。

buf存放要发送数据的缓冲区。

len表示缓冲区长度。

flags表示函数的调用方式。

返回值:函数执行成功,返回发送的总字节数,否则返回SOCKET_ERROR。

(8)recv( )函数

        int recv(
        SOCKET             s,
        char FAR*         buf,
        int                len,
        int                flags
     );

功能:在使用TCP连接时,用该函数从指定套接字中接收数据包。

参数:

s表示套接字标识。

buf用于接收数据的缓冲区。

len, buf缓冲区的长度。

flags表示函数的调用方式,可选值为MSG_PEEK或MSG_OOB。

返回值:若函数执行成功,返回收到的字节数,否则返回SOCKET_ERROR。

(9)sendto( )函数

        int sendto(
        SOCKET                                s,
        const char FAR*                          buf,
        int                                     len,
        int                                     flags,
        const struct sockaddr FAR*          to,
        int                                     tolen
        );

功能:UDP协议不是面向连接的,但UDP套接字被创建后,可以直接通过该函数向服务器端发送数据。

参数:

s表示套接字标识。

buf,存放要发送数据的缓冲区。

len, buf缓冲区的长度。

flags表示函数的调用方式。

to(可选),指向目的套接字地址的指针。

tolen:to所指地址的长度。

返回值:函数执行成功,返回发送的总字节数,否则返回SOCKET_ERROR。

(10)recvfrom( )函数

        int recvfrom(
        SOCKET                         s,
        char FAR*                        buf,
        int                             len,
        int                             flags,
        struct sockaddr FAR*               from,
        int FAR*                         fromlen
        );

功能:在使用UDP连接时,用该函数从指定套接字中接收数据包。

参数:

s表示套接字标识。

buf,存放接收数据的缓冲区。

len, buf缓冲区的长度。

flags,表示函数的调用方式。

from(可选),指向装有源地址的缓冲区。

fromlen(可选),指向from缓冲区长度值的指针。

返回值:函数执行成功,返回收到的字节数,否则返回SOCKET_ERROR。

(11)WSACleanup( )函数

        int WSACleanup( );

功能:该函数与WSAStartup函数相对,用于终止使用WS2_32动态库。

参数:

返回值:函数执行成功返回0,否则返回SOCKET_ERROR。

3.5 茅庐对话

一天的时间很快就过去了,张飞很是得意。

军师,今天经你这么一指点,俺觉着自己有了质的提高。

哦,那敢情好,你说来听听。

今天俺学习了Windows API、动态链接库、进程与线程、网络编程基础,这些东西让俺意识到原来编程不仅是计算A+B的问题,还可以完成这么多复杂的功能。

嗯,还有几点你是要牢记的:Windows API作为Windows编程的接口,尤为重要,感兴趣的话可以查找API的函数列表,更进一步了解它的工作方式;使用动态链接库一个重要的功能是作为程序的外设,在想更改一些配置而不影响整个程序的时候,可以单独更新动态链接库而做到程序的优化。网络编程在黑客工具编写中尤其重要,不过这里只涉及一小部分,在后面的学习中会更加详细系统地讲解。