VC中为普通程序添加ODBC应用

摘要:本文通过用VC++6.0实现一个程序实例来介绍如何在一个普通的无数据库关联的应用程序中添加ODBC应用,使应用程序能在原有的功能基础上增加对数据库的操作。

  关键字:VC++ 、ODBC、数据库

  一、 引言

  数据库属于最流行的应用程序之一,几乎每个商业部门都使用数据库来记录、管理各种各样的数据。在VC下我们可以在创建工程时选择数据库支持,并选定数据源和相关的表,并选择CRecordView作为我们这个程序的基类,这样做可以毫不费力的将应用程序和数据库建立了关联,而几乎不用编什么代码,但这样做的前提是在新建工程时已明确知道用到哪个数据库,并且有相关的数据库。事实上我们往往有许多已做好的应用程序和类,其功能除了未和数据源建立关联外以基本满足要求,我们只要在其基础上添加ODBC接口,使之与数据库建立关联即可,这样做避免了与数据库无关部分代码的重新编写所造成的重复性操作,大大提高了代码的重用性和利用率。所以在普通程序上通过添加ODBC应用而与数据库建立关联的方法是完全行之有效的。

  二、 ODBC技术

  ODBC(Open Database Conectivity 开放式数据库互联)技术,作为Microsoft公司对数据库进行访问的标准应用程序接口(API)和Windows开放式服务体系结构OSA的一个重要组成部分已广为众多的Windows程序员所熟悉、认可。ODBC的工作依赖于数据库制造商提供的驱动程序,使用ODBC API的时候,Windows的ODBC管理程序,把数据库访问的请求传递给正确的驱动程序,驱动程序再使用SQL语句指示DBMS完成数据库访问工作,因此,ODBC的存在为我们开发应用数据库程序提供了非常强大的能力和灵活性。

  三、 程序示例

  (一)打开Visual C++,在"File"菜单上点选"New…",然后在弹出的"New"对话框中选定"MFC AppWizard(exe)"类的项目,"Project name"为Normal,按下OK键,下一Step 1屏幕中选"Single document"单文档支持,用到后面的选项除在最后一步选择"CFormView"作为本工程视类的基类外均为确省值,此时即可按下Finish键,结果系统将生成一个新的项目Normal。

  我们就将此工程当做原有的工程,接下来我们便在此工程基础上对其添加ODBC应用,使该工程能同数据源建立关联,能对数据库中的数据进行操作和管理。

  (二)打开"控制面板"上的"ODBC (32bits)",对数据源进行注册。为了使ODBC能与数据库一起工作,就必须把数据库注册到ODBC驱动程序管理器,这项工作可以通过定义一个DSN或数据源名字来完成。在弹出的"ODBC数据源管理器"中选择"User DSN"属性页,点击"Add…"按钮。选择"Microsoft Access Driver(*.mdb)"作为数据源的驱动器,点击"完成"按钮。在弹出的"ODBC Microsoft Access 97 Setup"对话框中在"Data Source Name:"栏添入RP97,"Description:"栏只起注释说明的作用,可以不填,然后点击"Select…"按钮,选择所要注册的数据源,然后点击"OK"就完成了对数据源的注册,到这一步,本机上的任意程序只要通过ODBC接口和数据源名"RP97"就能完成对数据库的访问了。

  (三)在VC的"Workspace"活动窗口中选择"FileView"属性页,打开标准框架头文件"StdAfx.h",并在最后一个#include后面添加对"afxdb.h"的引用:#include

  (四) 在"Workspace"活动窗口中选择"ClassView"属性页,在"Normal Classes"上右键,选"New Class…",在弹出的"New Class"对话框的"Base Class"栏选择"CRecordSet"做为新添加的类的基类,在"Name"栏填写类名"CODBCSet",点击"OK",在随后弹出的对话框的"ODBC"栏选择刚注册的"RP97"数据源,点击"OK"后选择该数据库的一个表,点击"OK"在"ClassView"里就多了一个以CRecordSet为基类的新类"CODBCSet"。下面三个函数完成了数据库各级元素的绑定工作:

CString CODBCSet::GetDefaultConnect()
{
 return _T("ODBC;DSN=RP97");
}

CString CODBCSet::GetDefaultSQL()
{
 return _T("[单据表]");
}

void CODBCSet::DoFieldExchange(CFieldExchange* pFX)
{
 //{{AFX_FIELD_MAP(CODBCSet)
 pFX->SetFieldType(CFieldExchange::outputColumn);
 RFX_Text(pFX, _T("[单据ID]"), m_column1);
 RFX_Text(pFX, _T("[单据名称]"), m_column2);
 RFX_Text(pFX, _T("[报帐人]"), m_column3);
 //}}AFX_FIELD_MAP
}

  (五)按同样的方法再添加一个基于"generic CWnd"的新类"CConnectDB"。在该类的源文件里添加对"ODBCSet.h"的引用:#include "ODBCSet.h"。在该类的头文件的"class CconnectDB"前添加class CODBCSet;并在该类里添加公有型成员变量和函数:

CDatabase m_dbData;
CODBCSet* m_pSet;
void CConnectDB::Initial()
{
 //打开数据源RP97
 CString os=_T("odbc; dsn=RP97");
 m_dbData.Open(NULL,FALSE,FALSE,0);
 m_pSet=new CODBCSet(&m_dbData);
 //通过SQL结构化查询语言打开RP97里的单据表
 CString sql="SELECT * FROM 单据表";
 m_pSet->Open(AFX_DB_USE_DEFAULT_TYPE,sql);
}


  (六)在Form上添加一个"测试"按钮及其响应函数OnTest():

void CNormalView::OnTest()
{
 CConnectDB connectDB;
 //执行完Initial()后m_pSet指针才不为空,方可安全使用。
 connectDB.Initial();
 if(connectDB.m_pSet==NULL)
  return;
 connectDB.m_pSet->MoveFirst();
 CString str=connectDB.m_pSet->m_column3;
 AfxMessageBox(str);
}

  最后在该文件开始处添加两个引用:

#include "ConnectDB.h"
#include "ODBCSet.h"

  四、 运行与测试

  编译运行程序,点击"测试"按钮,就会将"RP97"数据库的"单据表"的第一条记录的"报帐人"字段所在的内容通过对话框弹出来。

  小结:

  本程序的关键在于对数据库指针m_pSet的获取,当类CConnectDB 的成员函数Initial()被执行完时,m_pSet就已被获取到了,而在此之前该指针是空的,是不能使用的,所以在实际应用中必须保证在使用m_pSet之前调用过函数Initial()。当m_pSet被获取到之后,就可以想其他ODBC应用程序一样使用CrecordSet类里的各种函数对数据库进行各种需要的操作和管理了。
--------------------------------------------------------------------------------------------------------------------

CDatabase\CRecordSet\CRecordView

 

10.4 CDatabase

  要建立与数据源的连接,首先应构造一个CDatabase对象,然后再调用CDatabase的Open成员函数.Open函数负责建立连接,其声明为

virtual BOOL Open( LPCTSTR lpszDSN, BOOL bExclusive = FALSE, BOOL bReadOnly = FALSE, LPCTSTR lpszConnect = “ODBC;”, BOOL bUseCursorLib = TRUE ); throw( CDBException, CMemoryException );

 

  参数lpszDSN指定了数据源名(构造数据源的方法将在后面介绍),在lpszConnect参数中也可包括数据源名,此时lpszDSN必需为NULL,若在函数中未提供数据源名且使lpszDSN为NULL,则会显示一个数据源对话框,用户可以在该对话框中选择一个数据源.参数bExclusive说明是否独占数据源,由于目前版本的类库还不支持独占方式,故该参数的值应该是FALSE,这说明数据源是被共享的.参数bReadOnly若为TRUE则对数据源的连接是只读的.参数lpszConnect指定了一个连接字符串,连接字符串中可以包括数据源名、用户帐号(ID)和口令等信息,字符串中的"ODBC"表示要连接到一个ODBC数据源上.参数bUseCursorLib若为TRUE,则会装载光标库,否则不装载,快照需要光标库,动态集不需要光标库. 若连接成功,函数返回TRUE,若返回FALSE,则说明用户在数据源对话框中按了Cancel按钮。若函数内部出现错误,则框架会产生一个异常。

  下面是一些调用Open函数的例子。

CDatabase m_db; //在文档类中嵌入一个CDatabase对象

//连接到一个名为"Student Registration"的数据源

m_db.Open("Student Registration");

//在连接数据源的同时指定了用户帐号和口令

m_db.Open(NULL,FALSE,FALSE,"ODBC;DSN=Student Registration;UID=ZYF;PWD=1234");

m_db.Open(NULL); //将弹出一个数据源对话框

 

  要从一个数据源中脱离,可调用函数Close。在脱离后,可以再次调用Open函数来建立一个新的连接.调用IsOpen可判断当前是否有一个连接,调用GetConnect可返回当前的连接字符串。函数的声明为

virtual void Close( );

BOOL IsOpen( ) const; //返回TRUE则表明当前有一个连接

const CString& GetConnect( ) const;

  CDatabase的析构函数会调用Close,所以只要删除了CDatabase对象就可以与数据源脱离。


10.5 CRecordset

  CRecordset类代表一个记录集.该类是MFC的ODBC类中最重要、功能最强大的类。

10.5.1 动态集、快照、光标和光标库

  在多任务操作系统或网络环境中,多个用户可以共享同一个数据源。共享数据的一个主要问题是如何协调各个用户对数据源的修改。例如,当某一个应用改变了数据源中的记录时,别的连接至该数据源的应用应该如何处理。对于这个问题,基于MFC的ODBC应用程序可以采取几种不同的处理办法,这将由程序采用哪种记录集决定。

  记录集主要分为快照(Snapshot) 和动态集(Dynaset)两种,CRecordset类对这两者都支持。这两种记录集的不同表现在它们对别的应用改变数据源记录采取了不同的处理方法。

  快照型记录集提供了对数据的静态视.快照是个很形象的术语,就好象对数据源的某些记录照了一张照片一样.当别的用户改变了记录时(包括修改、添加和删除),快照中的记录不受影响,也就是说,快照不反映别的用户对数据源记录的改变.直到调用了CRecordset::Requery重新查询后,快照才会反映变化.对于象产生报告或执行计算这样的不希望中途变动的工作,快照是很有用的。需要指出的是,快照的这种静态特性是相对于别的用户而言的,它会正确反映由本身用户对记录的修改和删除,但对于新添加的记录直到调用Requery后才能反映到快照中.

  动态集提供了数据的动态视.当别的用户修改或删除了记录集中的记录时,会在动态集中反映出来:当滚动到修改过的记录时对其所作的修改会立即反映到动态集中,当记录被删除时,MFC代码会跳过记录集中的删除部分.对于其它用户添加的记录,直到调用Requery时,才会在动态集中反映出来。本身应用程序对记录的修改、添加和删除会反映在动态集中。当数据必须是动态的时侯,使用动态集是最适合的。例如,在一个火车票联网售票系统中,显然应该用动态集随时反映出共享数据的变化。

  在记录集中滚动,需要有一个标志来指明滚动后的位置(当前位置)。ODBC驱动程序会维护一个光标,用来跟踪记录集的当前记录,可以把光标理解成跟踪记录集位置的一种机制。

  光标库(Cursor Library)是处于ODBC驱动程序管理器和驱动程序之间的动态链接库(ODBCCR32.DLL).光标库的主要功能是支持快照以及为底层驱动程序提供双向滚动能力,高层次的驱动程序不需要光标库,因为它们是可滚动的.光标库管理快照记录的缓冲区,该缓冲区反映本程序对记录的修改和删除,但不反映其它用户对记录的改变,由此可见,快照实际上相当于当前的光标库缓冲区.

  应注意的是,快照是一种静态光标(Static Cursor).静态光标直到滚动到某个记录才能取得该记录的数据.因此,要保证所有的记录都被快照,可以先滚动到记录集的末尾,然后再滚动到感兴趣的第一个记录上.这样做的缺点是滚动到末尾需要额外的开销,会降低性能.

  与快照不同,动态集不用光标库维持的缓冲区来存放记录.实际上,动态集是不使用光标库的,因为光标库会屏蔽掉一些支持动态集的底层驱动程序功能.动态集是一种键集驱动光标(Keyset-Driven Cursor),当打开一个动态集时,驱动程序保存记录集中每个记录的键.只要光标在动态集中滚动,驱动程序就会通过键来从数据源中检取当前记录,从而保证选取的记录与数据源同步.

  从上面的分析中可以看出,快照和动态集有一个共同的特点,那就是在建立记录集后,记录集中的成员就已经确定了.这就是为什么两种记录集都不能反映别的用户添加记录的原因.

10.5.2 域数据成员与数据交换

  CRecordset类代表一个记录集.用户一般需要用ClassWizard创建一个CRecordset的派生类.ClassWizard可以为派生的记录集类创建一批数据成员,这些数据成员与记录的各字段相对应,被称为字段数据成员或域数据成员.例如,对于表10.2所示的将在后面例子中使用的数据库表,ClassWizard会在派生类中加入6个域数据成员,如清单10.1所示.可以看出域数据成员与表中的字段名字类似,且类型匹配.

 

表10.2 stdreg32.mdb中的Section表

CourseID

(Text)

SectionNo

(Text)

InstructorID

(Text)

RoomNo

(Text)

Schedule

(Text)

Capacity

(int)

MATH101

1

KLAUSENJ

KEN-12

MWF10-11

40

MATH101

2

ROGERSN

WIL-1088

TTH3:30-5

15

MATH201

1

ROGERSN

WIL-1034

MWF2-3

20

MATH201

2

SMITHJ

WIL-1054

MWF3-4

25

MATH202

1

KLA

WIL-1054

MWF9-10

20

MATH202

2

ROGERSN

KEN-12

TTH9:30-11

15

MATH202

3

KLAUSENJ

WIL-2033

TTH3-4:30

15

清单10.1 派生类中的域数据成员

class CSectionSet : public CRecordset

{

public:

. . . . . .

//{{AFX_FIELD(CSectionSet, CRecordset)

CString m_CourseID;

CString m_SectionNo;

CString m_InstructorID;

CString m_RoomNo;

CString m_Schedule;

int m_Capacity;

//}}AFX_FIELD

. . . . . .

};

 

  域数据成员用来保存某条记录的各个字段,它们是程序与记录之间的缓冲区.域数据成员代表当前记录,当在记录集中滚动到某一记录时,框架自动地把记录的各个字段拷贝到记录集对象的域数据成员中.当用户要修改当前记录或增加新记录时,程序先将各字段的新值放入域数据成员中,然后调用相应的CRecordset成员函数把域数据成员设置到数据源中.

  不难看出,在记录集与数据源之间有一个数据交换问题.CRecordset类使用"记录域交换"(Record Field Exchange,缩写为RFX)机制自动地在域数据成员和数据源之间交换数据.RFX机制与对话数据交换(DDX)类似.CRecordset的成员函数DoFieldExchange负责数据交换任务,在该函数中调用了一系列RFX函数.当用户用ClassWizard加入域数据成员时,ClassWizard会自动在DoFieldExchange中建立RFX.典型DoFieldExchange如清单10.2所示:

清单10.2 典型的DoFieldExchange函数

void CSectionSet::DoFieldExchange(CFieldExchange* pFX)

{

//{{AFX_FIELD_MAP(CSectionSet)

pFX->SetFieldType(CFieldExchange::outputColumn);

RFX_Text(pFX, _T("[CourseID]"), m_CourseID);

RFX_Text(pFX, _T("[SectionNo]"), m_SectionNo);

RFX_Text(pFX, _T("[InstructorID]"), m_InstructorID);

RFX_Text(pFX, _T("[RoomNo]"), m_RoomNo);

RFX_Text(pFX, _T("[Schedule]"), m_Schedule);

RFX_Int(pFX, _T("[Capacity]"), m_Capacity);

//}}AFX_FIELD_MAP

}

 

 

10.5.3 SQL查询

  记录集的建立实际上主要是一个查询过程,SQL的SELECT语句用来查询数据源.在建立记录集时,CRecordset会根据一些参数构造一个SELECT语句来查询数据源,并用查询的结果创建记录集.明白这一点对理解CRecordset至关重要.SELECT语句的句法如下:

SELECT rfx-field-list FROM table-name [WHERE m_strFilter]

[ORDER BY m_strSort]

  其中table-name是表名,rfx-field-list是选择的列(字段).WHERE和ORDER BY是两个子句,分别用来过滤和排序。下面是SELECT语句的一些例子:

SELECT CourseID, InstructorID FROM Section

SELECT * FROM Section WHERE CourseID=‘MATH202’ AND Capacity=15

SELECT InstructorID FROM Section ORDER BY CourseID ASC

  其中第一个语句从Section表中选择CourseID和InstructorID字段.第二个语句从Section表中选择CourseID为MATH202且Capacity等于15的记录,在该语句中使用了象"AND"或"OR"这样的逻辑连接符.要注意在SQL语句中引用字符串、日期或时间等类型的数据时要用单引号括起来,而数值型数据则不用.第三个语句从Section表中选择InstructorID列并且按CourseID的升序排列,若要降序排列,可使用关键字DESC.

提示:如果列名或表名中包含有空格,则必需用方括号把该名称包起来。例如,如果有一列名为“Client Name”,则应该写成“[Client Name]”。

 

10.5.4 记录集的建立和关闭

  要建立记录集,首先要构造一个CRecordset派生类对象,然后调用Open成员函数查询数据源中的记录并建立记录集.在Open函数中,可能会调用GetDefaultConnect和GetDefaultSQL函数.函数的声明为

CRecordset( CDatabase* pDatabase = NULL);
参数pDatabase指向一个CDatabase对象,用来获取数据源.如果pDatabase为NULL,则会在Open函数中自动构建一个CDatabase对象.如果CDatabase对象还未与数据源连接,那么在Open函数中会建立连接,连接字符串(参见10.3.1)由成员函数GetDefaultConnect提供.

virtual CString GetDefaultConnect( );
该函数返回缺省的连接字符串.Open函数在必要的时侯会调用该函数获取连接字符串以建立与数据源的连接.一般需要在CRecordset派生类中覆盖该函数并在新版的函数中提供连接字符串.

virtual BOOL Open( UINT nOpenType = AFX_DB_USE_DEFAULT_TYPE, LPCTSTR lpszSQL = NULL, DWORD dwOptions = none );
throw( CDBException, CMemoryException );
该函数使用指定的SQL语句查询数据源中的记录并按指定的类型和选项建立记录集.参数nOpenType说明了记录集的类型,如表10.3所示,如果要求的类型驱动程序不支持,则函数将产生一个异常.参数lpszSQL是一个SQL的SELECT语句,或是一个表名.函数用lpszSQL来进行查询,如果该参数为NULL,则函数会调用GetDefaultSQL获取缺省的SQL语句.参数dwOptions可以是一些选项的组合,常用的选项在表10.4中列出.若创建成功则函数返回TRUE,若函数调用了CDatabase::Open且返回FALSE,则函数返回FALSE.

 

表10.3 记录集的类型

类型

含义

AFX_DB_USE_DEFAULT_TYPE

使用缺省值.

CRecordset::dynaset

可双向滚动的动态集.

CRecordset::snapshot

可双向滚动的快照.

CRecordset::dynamic

提供比动态集更好的动态特性,大部分ODBC驱动程序不支持这种记录集.

CRecordset::forwardOnly

只能前向滚动的只读记录集.

 

 

表10.4 创建记录集时的常用选项

选项

含义

CRecordset::none

无选项(缺省).

CRecordset::appendOnly

不允许修改和删除记录,但可以添加记录.

CRecordset::readOnly

记录集是只读的.

CRecordset::skipDeletedRecords

有些数据库(如FoxPro)在删除记录时并不真删除,而是做个删除标记,在滚动时将跳过这些被删除的记录.

 

 

virtual CString GetDefaultSQL( );
Open函数在必要时会调用该函数返回缺省的SQL语句或表名以查询数据源中的记录.一般需要在CRecordset派生类中覆盖该函数并在新版的函数中提供SQL语句或表名.下面是一些返回字符串的例子.
“Section” //选择Section表中的所有记录到记录集中
“Section, Course” //合并Section表和Course表的各列到记录集中

//对Section表中的所有记录按CourseID的升序进行排序,然后建立记录集

“SELECT * FROM Section ORDER BY CourseID ASC”

  上面的例子说明,通过合理地安排SQL语句和表名,Open函数可以十分灵活地查询数据源中的记录.用户可以合并多个表的字段,也可以只选择记录中的某些字段,还可以对记录进行过滤和排序.

  上一小节说过,在建立记录集时,CRecordset会构造一个SELECT语句来查询数据源.如果在调用Open时只提供了表名,那么SELECT语句还缺少选择列参数rfx-field-list(参见10.5.3).框架规定,如果只提供了表名,则选择列的信息从DoFieldExchange中的RFX语句里提取.例如,如果在调用Open时只提供了"Section"表名,那么将会构造如下一个SELECT语句:

SELECT CourseID,SectionNo,InstructorID,RoomNo, Schedule,Capacity FROM Section

 

  建立记录集后,用户可以随时调用Requery成员函数来重新查询和建立记录集.Requery有两个重要用途:

  • 使记录集能反映用户对数据源的改变(参见10.5.1).

  • 按照新的过滤或排序方法查询记录并重新建立记录集.

 

  在调用Requery之前,可调用CanRestart来判断记录集是否支持Requery操作.要记住Requery只能在成功调用Open后调用,所以程序应调用IsOpen来判断记录集是否已建立.函数的声明为

virtual BOOL Requery( );throw( CDBException, CMemoryException );
返回TRUE表明记录集建立成功,否则返回FALSE.若函数内部出错则产生异常.

BOOL CanRestart( ) const; //若支持Requery则返回TRUE

BOOL IsOpen( ) const; //若记录集已建立则返回TRUE

  CRecordset类有两个公共数据成员m_strFilter和m_strSort用来设置对记录的过滤和排序.在调用Open或Requery前,如果在这两个数据成员中指定了过滤或排序,那么Open和Requery将按这两个数据成员指定的过滤和排序来查询数据源.

成员m_strFilter用于指定过滤器.m_strFilter实际上包含了SQL的WHERE子句的内容,但它不含WHERE关键字.使用m_strFilter的一个例子为:

m_pSet->m_strFilter=“CourseID=‘MATH101’”; //只选择CourseID为MATH101的记录

if(m_pSet->Open(CRecordset::snapshot, “Section”))

. . . . . .

  成员m_strSort用于指定排序.m_strSort实际上包含了ORDER BY子句的内容,但它不含ORDER BY关键字.m_strSort的一个例子为

m_pSet->m_strSort=“CourseID DESC”; //按CourseID的降序排列记录

m_pSet->Open();

. . . . . .

  事实上,Open函数在构造SELECT语句时,会把m_strFilter和m_strSort的内容放入SELECT语句的WHERE和ORDER BY子句中.如果在Open的lpszSQL参数中已包括了WHERE和ORDER BY子句,那么m_strFilter和m_strSort必需为空.

  调用无参数成员函数Close可以关闭记录集.在调用了Close函数后,程序可以再次调用Open建立新的记录集.CRecordset的析构函数会调用Close函数,所以当删除CRecordset对象时记录集也随之关闭。

10.5.5 滚动记录

  CRecordset提供了几个成员函数用来在记录集中滚动,如下所示.当用这些函数滚动到一个新记录时,框架会自动地把新记录的内容拷贝到域数据成员中.

void MoveNext( ); //前进一个记录

void MovePrev( ); //后退一个记录

void MoveFirst( ); //滚动到记录集中的第一个记录

void MoveLast( ); //滚动到记录集中的最后一个记录

void SetAbsolutePosition( long nRows );
该函数用于滚动到由参数nRows指定的绝对位置处.若nRows为负数,则从后往前滚动.例如,当nRows为-1时,函数就滚动到记录集的末尾.注意,该函数不会跳过被删除的记录.

virtual void Move( long nRows, WORD wFetchType = SQL_FETCH_RELATIVE );
该函数功能强大.通过将wFetchType参数指定为SQL_FETCH_NEXT、SQL_FETCH_PRIOR、SQL_FETCH_FIRST、SQL_FETCH_LAST和SQL_FETCH_ABSOLUTE,可以完成上面五个函数的功能.若wFetchType为SQL_FETCH_RELATIVE,那么将相对当前记录移动,若nRows为正数,则向前移动,若nRows为负数,则向后移动.

 

  如果在建立记录集时选择了CRecordset::skipDeletedRecords选项,那么除了SetAbsolutePosition外,在滚动记录时将跳过被删除的记录,这一点对象FoxPro这样的数据库十分重要.

  如果记录集是空的,那么调用上述函数将产生异常.另外,必须保证滚动没有超出记录集的边界.调用IsEOF和IsBOF可以进行这方面的检测.

 

BOOL IsEOF( ) const;
如果记录集为空或滚动过了最后一个记录,那么函数返回TRUE,否则返回FALSE.

BOOL IsBOF( ) const;
如果记录集为空或滚动过了第一个记录,那么函数返回TRUE,否则返回FALSE.

 

下面是一个使用IsEOF的例子:

while(!m_pSet->IsEOF( ))

m_pSet->MoveNext( );

调用GetRecordCound可获得记录集中的记录总数,该函数的声明为

long GetRecordCount( ) const;
要注意这个函数返回的实际上是用户在记录集中滚动的最远距离.要想真正返回记录总数,只有调用MoveNext移动到记录集的末尾(MoveLast不行).

 

10.5.6 修改、添加和删除记录

要修改当前记录,应该按下列步骤进行:

调用Edit成员函数.调用该函数后就进入了编辑模式,程序可以修改域数据成员.注意不要在一个空的记录集中调用Edit,否则会产生异常.Edit函数会把当前域数据成员的内容保存在一个缓冲区中,这样做有两个目的,一是可以与域数据成员作比较以判断哪些字段被改变了,二是在必要的时侯可以恢复域数据成员原来的值.若再次调用Edit,则将从缓冲区中恢复域数据成员,调用后程序仍处于编辑模式.调用Move(AFX_MOVE_REFRESH)或Move(0)可退出编辑模式(AFX_MOVE_REFRESH的值为0),同时该函数会从缓冲区中恢复域数据成员.

设置域数据成员的新值.

调用Update完成编辑.Update把变化后的记录写入数据源并结束编辑模式.

 

要向记录集中添加新的记录,应该按下列步骤进行:

调用AddNew成员函数.调用该函数后就进入了添加模式,该函数把所有的域数据成员都设置成NULL(注意,在数据库术语中,NULL是指没有值,这与C++的NULL是不同的).与Edit一样,AddNew会把当前域数据成员的内容保存在一个缓冲区中,在必要的时侯,程序可以再次调用AddNew取消添加操作并恢复域数据成员原来的值,调用后程序仍处于添加模式.调用Move(AFX_MOVE_REFRESH)可退出添加模式,同时该函数会从缓冲区中恢复域数据成员.

设置域数据成员.

调用Update.Update把域数据成员中的内容作为新记录写入数据源,从而结束了添加.

 

如果记录集是快照,那么在添加一个新的记录后,需要调用Requery重新查询,因为快照无法反映添加操作.

要删除记录集的当前记录,应按下面两步进行:

调用Delete成员函数.该函数会同时给记录集和数据源中当前记录加上删除标记.注意不要在一个空记录集中调用Delete,否则会产生一个异常.

滚动到另一个记录上以跳过删除记录.

 

上面提到的函数声明为:

virtual void Edit( );throw( CDBException, CMemoryException );

virtual void AddNew( );throw( CDBException );

virtual void Delete( );throw( CDBException );

virtual BOOL Update( );throw( CDBException );
若更新失败则函数返回FALSE,且会产生一个异常.

 

  在对记录集进行更改以前,程序也许要调用下列函数来判断记录集是否是可以更改的,因为如果在不能更改的记录集中进行修改、添加或删除将导致异常的产生.

BOOL CanUpdate( ) const; //返回TRUE表明记录是可以修改、添加和删除的.

BOOL CanAppend( ) const; //返回TRUE则表明可以添加记录.


10.6 CRecordView

  CRecordView(记录视图)是CFormView的派生类,它提供了一个表单视图(参见6.4.1)来显示当前记录.一个典型的记录视图如图10.3所示,用户可以通过表单视图显示当前记录.通过记录视图,可以修改、添加和删除数据.用户一般需要创建一个CRecordView的派生类并在其对应的对话框模板中加入控件.

T10_3.tif (174388 bytes)

图10.3 典型的记录视图

 

  记录视图使用DDX数据交换机制在表单中的控件和记录集之间交换数据。在前面介绍的DDX都是在控件和控件父窗口的数据成员之间交换数据,而记录视图则是在控件和一个外部对象(CRecordset的派生类对象)之间交换数据.清单10.3显示了一个CRecordView的派生类的DoDataExchange函数,读者可以看出,该函数是与m_pSet指针指向的记录集对象的域数据成员交换数据的,而且,交换数据的代码是ClassWizard自动加入的.在后面的例子中,将向读者介绍用ClassWizard连接记录视图与记录集对象的方法.

 

清单10.3 用来与记录集对象的域数据成员交换数据的DoDataExchange函数

void CSectionForm::DoDataExchange(CDataExchange* pDX)

{

CRecordView::DoDataExchange(pDX);

//{{AFX_DATA_MAP(CSectionForm)

DDX_FieldText(pDX, IDC_COURSE, m_pSet->m_CourseID, m_pSet);

DDX_FieldText(pDX, IDC_SECTION, m_pSet->m_SectionNo, m_pSet);

DDX_FieldText(pDX, IDC_INSTRUCTOR, m_pSet->m_InstructorID, m_pSet);

DDX_FieldText(pDX, IDC_ROOM, m_pSet->m_RoomNo, m_pSet);

DDX_FieldText(pDX, IDC_SCHEDULE, m_pSet->m_Schedule, m_pSet);

DDX_FieldText(pDX, IDC_CAPACITY, m_pSet->m_Capacity, m_pSet);

//}}AFX_DATA_MAP

}

  作为总结,图10.4显示了MFC的ODBC应用程序中的DDX和RFX数据交换.

点击查看原始尺寸

图10.4 DDX和RFX数据交换机制

CRecordView本身提供了对下面四个命令的支持:

ID_RECORD_FIRST //滚动到记录集的第一个记录

ID_RECORD_LAST //滚动到记录集的最后一个记录

ID_RECORD_NEXT //前进一个记录

ID_RECORD_PREV //后退一个记录

 

CRecordView提供了OnMove成员函数处理这四个命令消息,OnMove函数对用户是透明的,清单10.4列出了OnMove的源代码.

 

清单10.4 OnMove函数

BOOL CRecordView::OnMove(UINT nIDMoveCommand)

{

CRecordset* pSet = OnGetRecordset();

if (pSet->CanUpdate())

{

pSet->Edit();

if (!UpdateData())

return TRUE;

 

pSet->Update();

}

 

switch (nIDMoveCommand)

{

case ID_RECORD_PREV:

pSet->MovePrev();

if (!pSet->IsBOF())

break;

 

case ID_RECORD_FIRST:

pSet->MoveFirst();

break;

 

case ID_RECORD_NEXT:

pSet->MoveNext();

if (!pSet->IsEOF())

break;

if (!pSet->CanScroll())

{

// clear out screen since we're sitting on EOF

pSet->SetFieldNull(NULL);

break;

}

 

case ID_RECORD_LAST:

pSet->MoveLast();

break;

 

default:

// Unexpected case value

ASSERT(FALSE);

}

 

// Show results of move operation

UpdateData(FALSE);

return TRUE;

}

  在函数的开头先调用CRecordset::Edit进入编辑模式,接着调用UpdateData将控件中的数据更新到记录集对象的域数据成员中,然后调用CRecordset::Update将域数据成员的值写入数据源.这说明OnMove在滚动记录的同时会完成对原来记录的修改.

  在函数的中间有一个分支语句用来处理四个不同的命令,在这个分支语句中调用了CRecordset的各种用于滚动记录的成员函数,这些函数在滚动到一个新的记录时会把该记录的内容设置到域数据成员中.在函数的末尾调用UpdateData(FALSE)把新的当前记录的内容设置到表单的控件中。

  由此可见,OnMove一来一回完成了两次表单控件和数据源的数据交换过程.通过分析该函数,读者可以学会在浏览记录时如何控制DDX和DFX数据交换.

 


引用:同一个世界,不同的你我

VC ODBC使用总结

1.打开数据库

CDatabase database;
database.OpenEx( _T( "DSN=zhuxue" ),CDatabase::noOdbcDialog);//zhuxue为数据源名称

2.关联记录集

CRecordset recset(&database);

3.查询记录

CString sSql1="";
 sSql1 = "SELECT * FROM tablename" ;   
  recset.Open(CRecordset::forwardOnly, sSql1, CRecordset::readOnly);

int ti=0;
CDBVariant var;//var可以转换为其他类型的值

 while (!recset.IsEOF())
        {
   //读取Excel内部数值
   recset.GetFieldValue("id",var);
   jiangxiang[ti].id=var.m_iVal;
   recset.GetFieldValue("name", jiangxiang[ti].name);
   ti++;
   recset.MoveNext();
  }

recset.Close();//关闭记录集

4.执行sql语句

CString sSql="";
 sSql+="delete * from 院系审核";//清空表
 database.ExecuteSQL(sSql);

sSql也可以为Insert ,Update等语句

5.读取字段名

 sSql = "SELECT * FROM Sheet1" ;    //读取的文件有Sheet1表的定义,或为本程序生成的表.       
    
   // 执行查询语句
   recset.Open(CRecordset::forwardOnly, sSql, CRecordset::readOnly);
   int excelColCount=recset.GetODBCFieldCount();//列数
   CString excelfield[30];
  //得到记录集的字段集合中的字段的总个数  
  for( i=0;i<excelColCount;i++)   
  {   
   CODBCFieldInfo fieldinfo;
   recset.GetODBCFieldInfo(i,fieldinfo);
   excelfield[i].name =fieldinfo.m_strName;//字段名
   
    } 

6.打开excel文件

CString sDriver = "MICROSOFT EXCEL DRIVER (*.XLS)"; // Excel安装驱动
 CString sSql,sExcelFile; //sExcelFile为excel的文件路径

TRY
 {
  // 创建进行存取的字符串
  sSql.Format("DRIVER={%s};DSN='';FIRSTROWHASNAMES=1;READONLY=FALSE;CREATE_DB=\"%s\";DBQ=%s",sDriver, sExcelFile, sExcelFile);
  
  // 创建数据库 (既Excel表格文件)
  if( database.OpenEx(sSql,CDatabase::noOdbcDialog) )
{

//可以把excel作为一个数据库操作

}

 }
 catch(e)
 {
  TRACE1("Excel驱动没有安装: %s",sDriver);
  AfxMessageBox("读取失败,请检查是否定义数据区Sheet1");
 }

本文原创出处 couple_xiewei@163.com,例程原代码请发邮件索取

摘要: 本文介绍了在 Visual C++ ODBC 访问 SQL Server 数据库的方法,包括如何在程序中动态配置 SQL Server 数据源,如何与数据源建立连接,如何得到数据库结构信息等,并给出了示例程序。

关键词: ODBC, SQL Server, 数据源

ODBC Open Database Connectivity ,开放数据库连接)是由 Microsoft 定义的一种数据库访问标准,它提供了一种标准的数据库访问方法以访问不同数据库提供商的数据库,其本质上是一组数据库访问 API 。虽然数据库访问有多种方法,但 ODBC 以其编程相对简单,在实际编程中被广泛使用。

       VC++ 中提供了一组封装了 ODBC API MFC ODBC 类,以减少程序代码编写量。在 VC++ 中使用 MFC ODBC 类访问数据库时,一般都是先配置或选择已有的数据源,再构造 CDatabase 类对象,打开数据源,用该数据库类对象和 SQL (结构化查询语言)可以对库进行访问,再构造 CRecordset 类或其继承类对象,用数据集类对象和 SQL 可以实现对库中的表的操作。在 VC++ 中用 MFC ODBC 类操作 SQL Server 数据库基本上也是这个思路。

1.         配置数据源

在程序中根据用户选择动态配置数据源而不调用 ODBC 数据源管理器,对于应用程序开发有时是十分必要的。毕竟 ODBC 数据源管理器对于对数据库不熟悉的用户显的复杂了,且 ODBC 数据源管理器的界面风格有可能与整个应用程序的界面风格不一致,对于严谨的应用程序开发者来说,这些都是不能容忍的。

配置 SQL Server 数据源时,必须有 SQL Server 服务器名和服务器中的目标数据库名 ( 否则打开该数据源时,就会弹出配置数据源对话框或失败 ) 。这与配置 Access 等数据库的数据源时指定数据库文件的路径不同。

可以通过 ODBC API 函数 SQLBrowseConnect 得到本地所有的 SQL Server 服务器、服务器中的库、语言信息等。其使用方法如下所示:

得到服务器名

    SQLBrowseConnect(hdbc, "DRIVER={SQL Server};", SQL_NTS, BrowseResult, sizeof(BrowseResult), &BrowseResultLen);

       其中 hdbc 是用 SQLAllocHandle 函数得到的连结句柄,第二个参数是指定连结属性的输入字符串,第三个参数是输入字符串的长度,第四个参数是输出字符串的指针,我们要得到的信息就存于这个字符串中,第五个参数指定输出字符串的长度,第六个参数是实际返回字符串的长度。 BrowseResult 所指向的函数返回的字符串中含有形如 “SERVER:Server={Server_name1,Server_name2, }” 的字符串,其中 Server_name1 Server_name2 就是 SQL Server 服务器名。

       得到库名

       当用户选定一个服务器之后,可以进一步利用 SQLBrowseConnect 函数得到服务器中存在的库或语言信息(如果需要的话)。

SQLBrowseConnect(hdbc,"SERVER=Server_name1;UID=sa;PWD=515578;",SQL_NTS, BrowseResult, sizeof(BrowseResult), &BrowseResultLen);

其中 UID PWD 是访问该服务器的用户名和密码。这次 BrowseResult 所指向的函数返回的字符串中含有形如 DATABASE:Database={master,model, } LANGUAGE:Language={Arabic, Brazilian, English, } 的字符串,其中 master model 为服务器中的数据库名。

利用这些得到信息就可以配置 SQL Server 数据源了:

SQLConfigDataSource(NULL,ODBC_ADD_DSN,"SQL Server","DSN=myDSN\0 SERVER=xhm\0DATABASE=pubs\0\0" ))

       作者有幸在网上找到 Santosh Rao 编写的 CSQLInfoEnumerator 类,该类有 EnumerateSQLServers EnumerateDatabase EnumerateDatabaseLanguage 三个成员函数,其封装了 SQLBrowseConnect 函数并进行相应的字符处理,大大简化了 SQLBrowseConnect 函数的使用。

2.         与数据源建立连接

如果使用 ODBC API 函数进行连结,函数 SQLConnect SQLDriverConnect SQLBrowseConnect 都可以实现。其中 SQLConnect 函数用于给定所有参数直接建立与数据源的连接; SQLDriverConnect 用于给定了部分连接参数,并弹出数据源浏览窗口与用户交互,获得足够的参数后建立与数据源的连接;而 SQLBrowseConnect 是通过迭代获取连结参数再进行连接,其使用如前所述。

MFC 为连接数据源提供了一个数据库类 CDatabase ,通过它我们可以非常方便地与数据源建立连结:

//m_db CDatabase 的对象

//m_szUserId m_szPassword CString 对象,为访问数据源的用户名和密码

       CString str;

       str = _T("DSN=xhmtest;UID=")+m_szUserId+_T(";PWD=")+m_szPassword;

       if (! m_db.OpenEx(str, CDatabase::noOdbcDialog ) ){

              AfxMessageBox(" 打开数据源失败 ");

}

另外,如果我们想对数据源进行操作,就可以利用打开的 CDatabase 对象执行 SQL 语句实现。如新建一张表:

    str = "CREATE TABLE TableDemo (Column1TEXT, Column2 NUMBER)";

    m_db.ExecuteSQL(sSql);

3.         得到数据库的结构信息

VC++ 中可以单独利用 ODBC API 获取数据库的结构信息,但方法非常复杂,各种参数也不易理解,且返回的结果不易获取。 MFC 中也没有为应用程序开发者得到数据库结构信息而提供单独的类。但是我们还是可以通过一些已有的方法方便地获得数据库的结构信息。

3.1 得到数据库中的表

利用 ODBC API 函数 SQLTables 可以得到数据库中的表信息,但使用不易。

MSDN sample 中有一个 catalog 例程,它示范了如果得到一个数据库的表信息和表中的字段信息,该程序中有两个非常实用的类: Ctables Ccolumns 。其中 CTables 继承 CRecordset 类,通过非常巧妙的方法封装了 SQLTables 函数,并将结果集存于 CRecordset 类,方便获取。

在程序中我们可以这样应用:

              //m_db 是已经打开的 CDatabse 对象 ,

              //m_strTableOwner CComboBox 对象

       CTables rs(&m_db);

       rs.Open(NULL, NULL, NULL, "TABLE");

       CString strTableRef;

       m_combTable.ResetContent();

       while (!rs.IsEOF())

       {

              strTableRef = _T("[");

              if (!rs.m_strTableOwner.IsEmpty())

                     strTableRef += rs.m_strTableOwner + _T("].[");

              strTableRef += rs.m_strTableName + _T("]");

              m_combTable.AddString(strTableRef);

              rs.MoveNext();

       }

       rs.Close();

CTables 对象 Open 方法第四个能数指定 “TABLE” 是指返回库中的表,当该参数为 “SYSTEM TABLE” 时返回系统表,当为 NULL 时,返回所有表。

3.2 得到表中字段结构

       得到表中字段结构有三种具体实现方式:

直接利用 ODBC API 函数 SQLColumn ;其实现也同样复杂。

利用前面提到的封装了 SQLColumns 函数的 CColumns 类;其使用方法与 CTables 类似,其成员变量 m_strColumnName 即为字段名

利用 CRecordset 类的成员函数。 MFC CRecordset 类中提供了获取表结构信息方法,使用非常方便。(但很奇怪为什么 CDatabase 类没有获取库结构信息的类方法?)

我们以利用 CRecodrset 为例简单说明怎样得到表结构。假设现在要得到用户在 CComboBox 对象中所选表的所有字段名,并将它埴入一个 CListCtrl 对象(其 View 属性被设为 Report )的列名中去:

       //m_strTableOwner 和上同,为 CComboBox 对象

       //m_listData ClistCtrl 对象

       CRecordset rs

       CODBCFieldInfo info;

CString strSQL;

       m_combTable.GetLBText(m_combTable.GetCurSel(), strSQL);

       strSQL = _T("SELECT * FROM ") + strSQL;

       rs.Open(Open(CRecordset::snapshot, strSQL, CRecordset::readOnly);

       int nColumns = rs.GetODBCFieldCount();

       for (int nNum = 0; nNum < nColumns; nNum++)

       {

              prs->GetODBCFieldInfo(nNum, info);

              m_listData.InsertColumn(nNum, info.m_strName, LVCFMT_LEFT, 80)

       }

另外,我们可以利用 CRecordSet::GetFieldValue 函数通过指定列名或列序数得到记录集中游标所指记录的值,通过 CRecordset::AddNew CRecordset::CancelUpdate CRecordset::Delete CRecordset::Edit CRecordset::Update 等函数操作 CRecordset 打开的表。

4.         本文的例程

本文所附的例程演示了如何编程读取局域网中 SQL Server 服务器中的数据。

点击 SQL Server 下拉列表,选择局域网中可以连接的 SQL Server 服务器名,再输入选定服务器的用户名和密码,点击 Database 下拉列表得到服务器中存在的数据库。然后点配置数据源按键,配置数据源并和数据源并建立连接,在 Table 下拉列表栏中选择要打开的表就可以在下面的列表中显示表中的数据了。因为表中数据量可能比较大,程序每次只读取 25 条记录到内存中。

参考文献:

[1] MSDN Library, the July 2000 release, Microsoft Corporation

[2] 刘刀桂 孟繁晶, Visual C++ 实践与提高 - 数据库篇,中国铁道出版社, 2001

 

今天怀着虞城的心来探索打印CListCtrl的方法,可惜忙到现在被老掉牙的数据加载给绊倒。但是从中却学到了不少新东西,以前没有遇到过的。现在就写出来和大家分享。
ODBC数据源与CListCtrl的连接已经算是老生常谈的事情了。
1、先建立数据库(这里以一个PrintTest为数据源名来处理,该数据库包含一张表info,里面有四个字段,ID,NAME,GROUP,AGE,只是测试用因此随便列出几个字段,其中ID为数字类型,其余为文本,采用Access数据库来建立。方法就是添加一张表,然后分别对表中填充一些数据,这里就不再讲述!)
2、ODBC数据源与程序的连接
   a.在stdafx.h文件的尾部添加#include "afxdb.h"
   b.针对于整个工程的全局函数CDatabase db;
   c.在APP文件的初始化进程中添加打开数据库的语句
         

    CWinApp::InitInstance();    //此句为系统自动生成的;

    
if (!db.IsOpen())
        db.Open(
"PrintTest;");
    
else
        AfxMessageBox(
"数据库连接失败!");

    AfxEnableControlContainer();    
//此句为系统自动生成的;

至此,与数据库的连接基本上完成了,也正是因为这个db.Open("PrintTest;");才有了这篇文章。
3、向记录集(CRecordset)填充数据,在这里我们必须要谈到(数据库和记录集对象之间的)记录字段交换 (RFX)。(大家可以在MSDN中查阅相关信息。)现在只将步骤做一个简述:
   a.在“类视图”中添加类,然后选择“MFC ODBC 使用者”;
   b.在向导中,数据源按钮后选择“机器数据源”选择我们设置的ODBC数据源,这里为PrintTest;确定;
   c.在弹出的对话框中选择info表,确定;
   d.修改类名,文件名(如果有必要的话),这里改为CInfoRS,InfoRS.h,InfoRS.cpp
   e.注意下面的选择是“动态集”,确定。(警告关闭)
这时候你可以在类向导中看到添加的新类,CInfoRS
4、在初始化对话框的时候进行数据的读入。
值得注意的是:我们刚才定义的是全局的变量,但是定义的语句是在APP文件中写入的,因此在Dlg文件中调用的时候我们仍然需要去声明一下它是全局变量,也就是对Dlg来说,它是在外部已经定义过的变量。因此在Dlg.cpp的开头我们补充extern CDatabase db;
在BOOL CPrintListCtrlDlg::OnInitDialog()中添加

    // TODO: 在此添加额外的初始化代码//注意找到该函数中的这句话,在其后添加以下代码
    m_cList.SetExtendedStyle(LVS_EX_GRIDLINES);      //设置ListCtrl的风格
    int nWidth=110;
    m_cList.InsertColumn(
0,"会员编号",LVCFMT_LEFT,nWidth*2/3);
    m_cList.InsertColumn(
1,"会员姓名",LVCFMT_LEFT,nWidth);
    m_cList.InsertColumn(
2,"会员组织",LVCFMT_LEFT,nWidth*3/2);
    m_cList.InsertColumn(
3,"年龄",LVCFMT_LEFT,nWidth/2);
    CInfoRS rs(
&db);

    UpdateList(rs);


在这个应用程序中同样要注意到,因为我们需要的是一张类似Access表的表格,而不是类似我的电脑的图标形式的风格,因此我们需要在添加的ListControl的时候,将其属性中的View设置为Report。

 UpdateList(rs);是我们自己添加的一个函数,再此我再介绍一下使用类向导添加函数的方法。
在类视图中,右键,添加->添加函数,然后添加相关参数,比如返回值类型,参数类型等,记得添加参数的时候按“添加”将其添加到参数列表中。
下面列出UpdateList的代码:

void CPrintListCtrlDlg::UpdateList(CInfoRS& rs)
{
    
int i=0;
    CString strID;
    rs.Open();
    m_cList.DeleteAllItems();
    
while(!rs.IsEOF())
    
{
        m_cList.InsertItem(i,
"");
        strID.Format(
"%d",rs.m_ID);
        m_cList.SetItemText(i,
0,strID);
        m_cList.SetItemText(i,
1,rs.m_NAME );
        m_cList.SetItemText(i,
2,rs.m_GROUP);
        m_cList.SetItemText(i,
3,rs.m_AGE);
        rs.MoveNext();
        i
++;
    }

    rs.Close();
}
至此这个程序基本上就编写完成了,但是使用Ctrl+F5调试的时候出现了错误。错误具体就不再描述,只告诉解决的方法。
1、一个是CInfoRS类中一句#error Security Issue: The connection string may contain a password
解决方法:注释掉
2、类型不匹配:
  m_cList.SetItemText(i,1,rs.m_NAME );
  m_cList.SetItemText(i,2,rs.m_GROUP);
  m_cList.SetItemText(i,3,rs.m_AGE);
将提示rs.m_NAME,rs.m_GROUP,rs.m_AGE不能从“CStringW”转换为“LPCTSTR”
解决方法:在类视图中定位到CInfoRS,在其变量(蓝色方块后),随便点一个进入。按以下方法修改:
//将以下代码:
    long    m_ID;
    CStringW    m_NAME;
    CStringW    m_GROUP;
    CStringW    m_AGE;
//修改为:
    long    m_ID;
    CString    m_NAME;
    CString    m_GROUP;
    CString    m_AGE;
//即将这里的CStringW替换为CString


//其实可以注意到之前的一大段话:以下字符串类型(如果存在)反映数据库字段(ANSI 数据类型的 CStringA 和 Unicode数据类型的 CStringW)的实际数据类型。这是为防止 ODBC 驱动程序执行可能不必要的转换。如果希望,可以将这些成员更改为CString 类型,ODBC 驱动程序将执行所有必要的转换。(注意: 必须使用 3.5 版或更高版本的 ODBC 驱动程序以同时支持 Unicode 和这些转换)。
至此,编译通过。
但是,随后弹出错误:ODBC数据源不支持动态集
修正这个问题的方法有二:
一、在刚才添加的时候,将动态集(默认)改为快照,在这里,将不会出现错误。
二、令人庆幸的是我曾经做过一个例子,里面确实是使用动态集,但并没有出现这个错误,于是我找到了另一个程序,再次调试通过。于是就很是郁闷。于是用断点调试,最终确定问题是发生在rs.Open();的位置。
于是查找MSDN发现(For CRecordset, the default value is CRecordset::snapshot.翻译:对于CRecordset默认值类对象,默认值是 CRecordset::snapshot,也就是快照模式),而我们所用的是动态集的模式。这里可以将rs.Open();改为rs.Open(CRecordset::forwardOnly);再次编译就可以通过了。
另外可以将最初的db.Open("PrintTest;");改为db.OpenEx("DSN=PrintTest;");就可以解决问题了。这其中的奥妙就不是很清楚,只能当作经验来和大家分享一下。也希望有知道的人能够留言告诉我。谢谢先~!


以下将本示例的代码以及数据库打包供大家学习下载!
http://www.cppblog.com/Files/mymsdn/PrintListCtrl.rar

----------------------------------------------------------------------------------------------------------------------

有关MFC ODBC类对打开的CRecordset数据集无法进行更新操作释疑— VC6自带的MFC4.2中CString.Format与CRecordSet的兼容性问题
在用MFC数据库类CDatabase和CRecordset类声明的对像无法对打开的数据集进行编辑、添加、删除。偶然间让我碰到个这样的问题让我费了不少精神,不知道为什么数据库能正常打开,数据集也正常Open。可是就是没办法进入编辑状态,老是会弹出“记录集只读”的提示。
------------------------------------------------------------------------------------------------------------------------------
 CDatabase db;
 db.OpenEx (_T"DSN=strtab");
 CRecordset m_Set (&db);
 m_Set.Open (AFX_DB_USE_DEFAULT_TYPE, _T"select *from strtab");
 if (!m_Set.IsOpen ()) AfxMessageBox ("数据集没能打开!");
 m_Set.Edit ();
--------------------------------------------------------------------------------------------------------------------------------
         检查一下自己的程序,发现不管是声明的CDatabase对像还是通过CRecordset对像打开的记录集都使用的是缺省参数,而在我的记忆中CDatabase::OpenEx()和CRecordset::Open()这两个成员函数的缺省参数打开的记录集和数据库对像都是非只读的。倒底是那里的问题呢?
BOOL CDatabase::OpenEx(LPCTSTR lpszConnectString, DWORD dwOptions)
virtual BOOL Open(UINT nOpenType = AFX_DB_USE_DEFAULT_TYPE,
  LPCTSTR lpszSQL = NULL, DWORD dwOptions = none);
         我在我的程序中打开数据集的Open()处下了断点并跟踪进去,发现CRecordset对像的m_bUpdatable标志位最初始一直是为TRUE,即可更新状态。随后进入了如下一串令人眼花缭乱的函数调用里,终于找到m_bUpdatable的值在那里被更改,这一切的问题出自那里!
-----------------------------------------------------------------------------------------------------------------------------------------------
BuildSQL(lpszSQL);
if ((m_bUpdatable || m_bAppendable) && !IsRecordsetUpdatable())
if (!IsSQLUpdatable(m_strSQL))
return IsSelectQueryUpdatable(lpszSQL);
lpchTokenFrom = FindSQLToken(strSQL, _afxFrom); //
AFX_STATIC_DATA const TCHAR _afxFrom[] = _T(" FROM ");
lpszFoundToken = _tcsstr(lpszFoundToken + nTokenOffset, lpszSQLToken);
--------------------------------------------------------------------------------------------------------------------------------------------------
         上面这每一行是跟踪进去的函数,蓝色字是函数名。这些函数是分析传递进来的SQL语句的并进行相应的设置来打开数据集。注意第五行_afxFrom的值是什么!这是MFC定义的一个常量,它的订义在注释那里。问题就在这里了,MFC定义这个常量用来分析SQL语句中是否包含用FROM关键字以及其后的表名,要有这个关键字那么数据集才是以可更新的状态打开的。但是,很明显我的程序中打开数据集用的SQL语句是有这个FROM关键字和表名的。那么我应该得到一个可更新的数据集才对啊,为什么我不能更新呢?仔细看看MFC定义的这个_afxFrom常量,它的值是_T(" FROM ")FROM的前后各有一个空格,因此真相大白了。问题就是我的SQL语句因为要返回所有的字段,就没有给出字段名而是直接用*号表示要返回所有字段,而这个*号紧挨着FROM关键字,所以在最后一行中MFC用_tcsstr()函数在SQL语句中查找_afxFrom,结果是永远也找不到MFC定义的这个两头各有一个空格的FROM关键。_tcsstr()返回一个空指针,接着这些函数一路返回FALSE,这样m_bUpdatable的值就为FALSE,我所打开的数据集也是不可更新数据集。
         这样的问题为什么以前没有出现呢?那是因为以前我要不是只返回特定的字段,构造好的SQL语句中的FROM也是同样前后都有空格。就是直接CRecordset::Open()一个参数不带的执行这个Open()函数。这样由MFC自行构造的缺省SQL语句肯定是符合他自己的要求。
         实际上,我这样的*号紧挨FROM的SQL语句写法也是很多人的习惯。所以,我相信也有不少的朋友也碰到这样的问题。
-------------------------------------------------------------------------------------------------------------------

今天我在BBS的VC版上转悠,看到由个哥们出了这样的问题:
说他在编写MFC的数据库程序(ODBC)的时候出现了错误,再插入新记录后调用Update的时候出现了Assert,由于再BBS上,我和他通过信息交流了一下,发现他在AddNew和Update之间调用了Format。直觉告诉我问题出在这里。
于是分析了一下。这个是我在BBS上发的帖子。

这个问题我仔细看了一下,问题出在MFC内部:下面所述仅适用于VC6带的MFC4.2

我们在使用ODBC进行数据库的插入操作时,都是这么一个流程:
AddNew()
//给成员赋值
Update()
而在MFC的源文件dbcore.cpp 1040行,有这样一行注释:
// Buffer address must not change - ODBC's SQLBindCol depends upon this
由于MFC在进行默认的数据源绑定时,使用CString绑定字符串型的成员,而CString使用的是动态的内存管理方式,因此这个缓冲区地址其实是可以改变的,因此,在dbcore.cpp的1041行开始便是这样几句:

void* pvBind;
pvBind = value.GetBuffer(0);
value.ReleaseBuffer();
if (pvBind != pInfo->m_pvBindAddress)
{
   TRACE1("Error: CString buffer (column %u) address has changed!\n",
   nField);
   ASSERT(FALSE);
}

因此,如果你在调用AddNew和Update之间把CString的缓冲区移动了,对不起,你必须收到一个ASSERT。(Nickshen好像就是这里的问题吧)

这样问题就很清楚了,就是在你调用AddNew和Update之间不能移动缓冲区。但是据我和nickshen私下讨论的结果,他只是在其中调用了CString的成员Format,想要把一个浮点数转换成字符串,如果这么做就会有问题,但是直接赋值就不会,难道Format会移动CString的缓冲区?

于是我跟踪了一下CString的Format函数,发现在被Format函数调用的FormatV函数的流程是这样的:先根据格式串算出大约格式化之后的字符串要占多大的空间,然后就是看是否分配新的缓冲区,然后sprintf。这个学过数据结构的都可以理解。远离很简单,但是有这么一个问题:在FormatV函数中,有这么一段

(strex.cpp, 659行起)

case 'f':
  va_arg(argList, DOUBLE_ARG);
  nItemLen = 128; // width isn't truncated
  // 312 == strlen("-1+(309 zeroes).")
  // 309 zeroes == max precision of a double
  nItemLen = max(nItemLen, 312+nPrecision);
  break;

这个是Format对于格式串中的%f的处理,在一个switch块中。
switch之后,

// adjust nMaxLen for output nItemLen
  nMaxLen += nItemLen;
...
GetBuffer(nMaxLen);

看到了没?如果你使用了%f,MFC会很保守地认为你的一个%f会占用312的字符的位置(的确够保守的,至于为何时312,注释说得很清楚),于是用这个巨大的数调用GetBuffer。

然后是CString的operator=(LPCTSTR),这个就简单多了,不用保守的计算,源字符串有多少个字符就分配多少个字节,同样通过GetBuffer。

在GetBuffer的实现中,简单的说就是看看原来的长度够不够,不够重新分配一块够大的,然后memcpy,于是,缓冲区移动了。

慢着!
如果说长度不够就要移动缓冲区,而且两种操作都会移动缓冲区,那么为何只有Format会出错,赋值不会?

谁说不会?你尝试赋给你的变量一个长度超过256的字符串试试,肯定出错,我试过了。
那么,这个256又是何处来的?你在用一个RecordSet第一步一定是Open吧。跟踪一下发现,Open中有一步是BindFieldToColumns (dbcore.cpp 3854),经过一系列的分发,程序到了dbrfx.cpp 777:

case CFieldExchange::BindFieldToColumn:
...
// Constrain to user specified max length, subject to 256 byte min
if (cbColumn > (UINT)nMaxLength || cbColumn < 256)
    cbColumn = nMaxLength;// Set up binding addres
void* pvData;
value.GetBufferSetLength(cbColumn+1);
pvData = value.LockBuffer();    // will be overwritten if UNICODE

那么这个nMaxLength是多少呢?这个看看AfxDB.h中对于RFX_Text的声明,255!
明白了?

这么一说事情就很清楚了,所有的一切都是由于MFC内部造成的,由于我们大多数时候都不会像数据库中插入一个长度超过255的字符串(事实上Access和SQL Server都只支持最多255个字符),因此不会有问题,但是偏偏MFC的工程师们在做Format函数的时候保守了,于是,只要你用了%f格式符,就有问题无疑了。

知道了原因,解决方案就很简单了:
1、如果你可以改数据库,不妨把那个string(varchar)类型的字段改成double。
2、如果你没有这个权限,或者数据太多已经不能改了,那么只有退而求其次,先定义一个buffer,sprintf一下,然后赋值给CString。

的确很麻烦。

同样的程序,再VC7下调试没有问题,有空再跟踪一下看看吧。MFC7.1的CString已
经完全重写了...

引用自:菜鸟心情—————C.ZHANG`s Mind—————

在vc下设定如IP地址及Netsh使用方法

经过几天的摸索,在程序中实现了系统tcp/ip属性设定网络参数的功能,并且还提供了其它的信息,如网卡的各种信息等。实现方法自己知道的有三种:1,调用IP Help的API;2、从注册表中读取;3,设定IP等参数时使用netsh命令。

自己最初的工作量都在IP Help上了,并且实现了大部分的功能,但是有一点其中的AddIPAddress和DeleteIPAddress的使用上,DeleteIPAddress所删的IP只能是由AddIPAddress加上去的,不能删除静态IP(自己猜的)。AddIPAddress加到接口上的IP在机子重启,或者网络连接禁用再启用后就没了。所以不长久,不过这对于adhoc中的地址自动配置够用了。不过在window下,它只提供了为每个连接设定一个IP的界面,实际上一个interface可以由多个IP。

第二种方法是从注册表中读取SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\下的键值,但这种方法设定IP,子网掩码、默认网关等参数后,一般需要重启机器。但是在vckbase上下到一个例子程序,可以免重启,它添加了NotifyIPChange函数做到的。操作注册表,太麻烦,而且当os不同时候键值可能会不同。

第三种方法只用来配置静态IP地址,因为IP Help虽然好用但是不能改静态ip,自己试验总是error。调用netsh命令就可以很好的解决这个问题。

 netsh interface ip set address local static 192.168.0.79 255.255.255.0 192.168.0.1 1 这个命令详细用法见它的帮助,它设定了本机静态ip,mask和默认的gateway,最后的1是到gateway的metric。下为关键代码:

 UpdateData(TRUE);
 struct in_addr ia;
 ia.S_un.S_addr=htonl(m_ipctrl);
 CString strIP=inet_ntoa(ia) ;
 ia.S_un.S_addr=htonl(m_ipMask);
 CString strMask=inet_ntoa(ia);
 ia.S_un.S_addr=htonl(m_DGateway);
 CString strGate=inet_ntoa(ia);

 CString strParam = " interface ip set address \"本地连接\" static ";
 strParam+= strIP+" "+strMask+" "+strGate+" "+"1";
 ShellExecute(NULL,"open", "netsh",strParam,NULL,SW_HIDE);

存在的一个问题是“本地连接”这个名字可以在机子上改,这样就不好了,关于如何获得还不知道,以为是调用IP help中的GetIfTable函数得到一个结构体中有这项,(msdn这么说的)但是输出总是乱码,不知道原因还。谁要知道告诉我一声。

引用自:http://gniq0418.tianya.cn/blogger/post_show.asp?BlogID=359755&PostID=4593024

Netsh使用方法

Windows 2K/XP 修改IP,不用开启网络连接属性的对话框,用Netsh更改IP,是不需要这么麻繁
使用windows 內建的网路工具netsh便可轻松解决!
Netsh - Creates a shell for network information
微软官方有一堆文件,有兴趣练习英文阅读的人可以去看看。
How to Use the NETSH Command to Change from Static IP Address to DHCP in Windows 2000(http://support.microsoft.com/?kbid=257748)
Using Netsh (http://www.microsoft.com/technet/treeview/default.asp?url=/technet/prodtechnol/
winxppro/proddocs/netsh.asp)
Managing Windows2000 Networking Components with Netsh
(http://www.microsoft.com/technet/community/columns/cableguy/cg1101.mspx)

在这里跟大家介绍几个简单的指令
1.Show IP
1.1Cmd Mode
直接在cmd下面输入
netsh interface ip show address
亦可简写为
netsh int ip sh ad
看看,指令是不是和Cisco的nos指令很像!非常怀疑是抄袭Cisco的。

1.2Netsh Mode
您也可以进入netsh的命令模式下
netsh                          //进入到 netsh mode
netsh>int                    //进入到 interface 子选项。
interface>ip                //進入到 ip 子选项。
interface ip>show
show address - 显示 IP 位址。
show config - 显示 IP 位址及其他资料。
show dns - 显示 DNS 服务器位址。
show icmp - 显示 ICMP 统计
show interface - 显示 IP 介面统计
show ipaddress - 显示 IP 位址
show ipnet - 显示 IP net-to-media 对应
show ipstats - 显示 IP 统计
show joins - 显示加入的多点传送群组
show offload - 显示 offload 内容
show tcpconn - 显示 TCP 连线
show tcpstats - 显示 TCP 统计
show udpconn - 显示 UDP 连线
show udpstats - 显示 UDP 统计
show wins - 显示 WINS 服务器位址。
2.Set IP
下列是所有可用的指令。
这个內容中的指令:
set address - 在指定的介面设定 IP 位址或预设网关。
set dns - 设定 DNS 服务器模式及位址。
set wins - 设定 WINS 服务器模式及位址。

2.1.设定IP位址
2.1.1.DHCP
设定
若您希望由DHCP取得IP位址可输入
interface ip>set ad "
区域连接" DHCP
或简写成
interface ip>set ad "
区域连接" D

2.1.2.
静态IP设定
2.1.2.1.
设定IP位址与子网关
netsh -
进入到 netsh mode
netsh>int -
进入到 interface 子选项。
interface>ip -
进入到 ip 子选项。
interface ip>set address name = "
区域连接" source = static addr = 10.2.2.100 mask = 255.255.255.0
可简写成
interface ip>set ad "
区域连接" s 10.2.2.100 255.255.255.0

2.1.2.2.
设定IP路由
interface ip>set address name = "
区域连接" gateway = 10.2.2.254 gwmetric = 1
可简写成
interface ip>set ad "
区域连接" ga=10.2.2.254 gw = 1

2.1.2.3
同时设定IP位址和路由
interface ip>set address name = "
区域连接" source = static addr = 10.2.2.100 mask = 255.255.255.0 gateway = 10.2.2.254 gwmetric = 1
可简写成
interface ip>set ad "
区域连接" s 10.2.2.100 255.255.255.0 10.2.2.254 1

设定完後,记得用sh ad去看一下设定的對不對。

2.3
设定DNS来源
若是由DHCP取得,请输入
interface ip>set dns "
区域连接" source=dhcp
若是使用静态设定,请输入
interface ip>set dns name = "
区域连接" source = static addr = 10.2.5.2
新增第二組DNS,请输入
interface ip>add dns name = "
区域连接" addr = 10.2.5.3

2.4
设定WINS来源
若是由DHCP取得,请输入
interface ip>set wins "
区域连接" source=dhcp
若是使用静态设定,请输入
interface ip>set wins name = "
区域连接" source = static addr = 10.2.5.10
新增第二組WINS,请输入
interface ip>add wins name = "
区域连接" addr = 10.2.5.17


3.
将网路状态设定导出/导入
3.1
导出
netsh -c interface dump >c:/netset.txt
当然,interface可以简写成intdump更可简化成d,所以就变成了
netsh -c int d >c:/netset.txt
3.2
导入
netsh -f c:/netset.txt
既可
這个指令我觉得蛮好用的,適用在帮客户裝机时,我們先将网路状态设定储存起来。将来若
發生客户不小心变更了设定等,任何网路设定上的問題,可以执行一个批次檔,呼叫netsh
把设定取回。不然有时候,网路设定跑掉了,想用VNC连线修改都不行。
------------------------------------------------------------------------------------------------------------------
( 这个blog系统自动把windows的路径分隔符号右斜线自动屏蔽了,我只好用/代替。阅读时候请注意) netsh 是windows系统本身提供的功能强大的网络配置命令行工具。

导出配置脚本:   netsh -c interface ip dump > c:/interface.txt
导入配置脚本:   netsh -f c:/interface.txt
进入netsh环境后,在根级目录用exec命令也可以加载一个配置脚

本。还有对wins、route、ras等网络服务的配置也可以通过Netsh的内置命令操作。

下面是配置示例:



C:/Documents and Settings/Administrator>netsh /?

用法: netsh [-a AliasFile] [-c Context] [-r RemoteMachine] [-u [DomainName]UserName] [-p Password | *]
             [Command | -f ScriptFile]

下列指令有效:

此上下文中的命令:
?              - 显示命令列表。
aaaa           - 更改到 `netsh aaaa' 上下文。
add            - 在项目列表上添加一个配置项目。
delete         - 在项目列表上删除一个配置项目。
dhcp           - 更改到 `netsh dhcp' 上下文。
diag           - 更改到 `netsh diag' 上下文。
dump           - 显示一个配置脚本。
exec           - 运行一个脚本文件。
help           - 显示命令列表。
interface      - 更改到 `netsh interface' 上下文。
ipsec          - 更改到 `netsh ipsec' 上下文。
ras            - 更改到 `netsh ras' 上下文。
routing        - 更改到 `netsh routing' 上下文。
rpc            - 更改到 `netsh rpc' 上下文。
set            - 更新配置设置。
show           - 显示信息。
wins           - 更改到 `netsh wins' 上下文。

下列的子上下文可用:
aaaa dhcp diag interface ipsec ras routing rpc wins



家庭网络自动配置:home.cmd   ip-home.txt

home.cmd:

@echo off
echo.
echo ************ Ip切换器 By 蒋进平 ************
echo.
echo 正在设置成家庭网络IP,请稍等 . . .& netsh -f c:/iphome.txt
echo 设置成功,现在可以使用家庭网络了
echo.
echo ************ 2004 年 8 月 30 号 ************
echo.
pause
exit

ip-home.txt :

# ----------------------------------
# 接口 IP 配置
# ----------------------------------
pushd interface ip


# "本地连接" 的接口 IP  配置

set address name="本地连接" source=static addr=192.168.0.10 mask=255.255.255.0
set address name="本地连接" gateway=192.168.0.1 gwmetric=0
set dns name="本地连接" source=static addr=211.99.129.210 register=NONE
add dns name="本地连接" addr=211.99.129.211 index=2
set wins name="本地连接" source=static addr=none

popd
# 接口 IP 配置结束

XP的各种网络命令
1、net系列命令
2、TCP/IP命令
3、netsh工具
一、net系列命令
net help
net send
net start


1.1 网络信使
net send  注意不能跨网段
net stop messenger 停止信使服务,也可以在“服务”修改 net start messenger 开始信使服务
1.2 在网络邻居上隐藏你的计算机
net config server /hidden:yes
net config server /hidden:no 则为开启
二、TCP/IP命令
ipconfig
arp
netstat
ping
nslookup
tracert
route

三、netsh工具
netsh.exe可以用来配置TCP/IP设置:IP地址、子网掩码、默认网关、DNS和WINS地址和其他选项。
3.1 显示TCP/IP设置
netsh interface ip show config
3.2 配置IP地址
netsh interface ip set address name=“本地连接" static 192.168.0.100 255.255.255.0 192.168.0.1 1
3.3 export your current IP settings
netsh -c interface dump > c:\location1.txt

3.4 import your IP settings
netsh -f c:\location1.txt
或者是  netsh exec c:\location2.txt
3.5 自动获得IP地址和DNS地址
netsh interface ip set address “本地连接” dhcp
netsh interface ip set dns “本地连接” dhcp
3.6 configure DNS and WINS addresses
netsh interface ip set dns “本地连接" static 192.168.0.200
netsh interface ip set wins “本地连接" static 192.168.0.200

----------------------------------------------------------------------------------------------------------------
快速更改自己的IP地址
2007-01-31 11:00

第一招:批处理

我们知道在命令行下用netsh命令更改IP的步骤是:
1。在运行栏裡输入cmd打开命令提示符
2。输入netsh 回车
3。输入int ip 回车
4。输入set address name="本地连接" source="static" addr=ip mask=255.255.255.0 Gateway 1

解释一下:
set address 是更改IP的命令
name = 你要更改IP的连接名称
source = 设置成静态的IP
addr = 要更改成的IP
mask=子网掩码
gateway是你的网关IP,后面的1是到达网关的跃点数


等待几秒鐘会出现一个”确定“的信息,表示你的IP已经更改成功了,不信用ipconfig /all检验一下。
知道了命令的用户我们就可以把它写成批处理如下:

@ echo off
echo This Programe will change your Ipaddress and Gateway.
echo Press any key to continue
pause >nul

rem 设置变量
set Nic=本地连接  
rem //可以根据你的需要更改,
set Add=202.96.134.9 
rem //可以根据你的需要更改
set Gat=202.96.134.60

netsh interface ip set address name=%Nic% source=static addr=%add% mask=255.255.255.0 %Gat% 1
rem //顺便把DNS也改掉
netsh interface ip set dns name=%Nic% source=static addr=%add% primary

echo OK!

注:把上面代码复制到空白的记事本裡,把“Nic=、Add= Gat=”更改成你自己的值然后另存為*.bat即可

第二招 利用Dump导出导入配置文件

在命令提示符下输入netsh -c int ip dump >c:\net.txt
然后打开C盘,你将会看到一个net.txt的文本文档打开它会看到下列信息
注:各人电脑上的信息会有所不同

# ---------------------------------- 
# 介面 IP 设定         
# ---------------------------------- 
pushd interface ip


# "Local Area Connection" 的介面 IP 设定

set address name="Local Area Connection" source=static addr=202.96.134.9 mask=255.255.255.0
set address name="Local Area Connection" gateway=202.96.134.60 gwmetric=0
set dns name="Local Area Connection" source=static addr=202.96.134.60 register=PRIMARY
add dns name="Local Area Connection" addr=202.96.134.1 index=2
set wins name="Local Area Connection" source=static addr=none

popd
# 介面 IP 设定结束,把“addr=、gateway= 改成你自巳的值即可 注意第一个addr =后面跟著的是你的IP地址、第二个addr = 后面跟著的是你的主DNS地址,更改后把它别存為net1.txt。再次打开命令提示符,输入netsh -f c:\net1.txt,稍等一会,使用Ipconfig /all查查看ip是不是已经更改成功了。以后你就可以使用netsh -f c:\net1.txt
或进netsh -f c:\net.txt 在两者之间快速切换了。当然你也可以把它们写成两个批处理或者创建一个快捷方式更方便的执行。

第三招 利用Netsh 的exec命令

打开记事本输入
int ip
set address name="Local Area Connection" source=static addr=202.96.134.9 mask=255.255.255.0 202.96.134.60 1
set dns name="Local Area Connection" source=static addr=202.96.134.60 register=PRIMARY
注:addr=更改成你自己的值。
然后把它另存為c:\*.sh
打开命令提示符输入netsh exec c:\*.sh

稍等一会你的IP就更改成功了。

利用上面的三种方法再加以优化我相信你一定会把更改IP做得更好更简单.例如我们可以把第三种方法改成一键更改IP地址。
新建了一个*.sh文件之后我们在桌面上新建一个快捷方式,命令指向為c:\windows\system32\netsh.exe exec c:\*.sh 把它取一个名字。然后右击你刚创建的快捷方式切换到“快捷方式”选项卡在“快捷键”裡指定一个快捷键例如F6,在“运行方式”裡选择“最小化”。单击确定以后你只要按一下F6键就可以悄无声息的更改IP了。够快够简单吧!




 
1月29日

DOS常用命令

DOS 常用命令:

  dir 列文件名 deltree 删除目录树 cls 清屏 cd 改变当前目录

  copy 拷贝文件 diskcopy 复制磁盘 del 删除文件 format 格式化磁盘

  edit 文本编辑 mem 查看内存状况 md 建立子目录 move 移动文件、改目录名

  more 分屏显示 type 显示文件内容 rd 删除目录 sys 制作DOS系统盘

  ren 改变文件名 xcopy 拷贝目录与文件 chkdsk 检查磁盘 attrib 设置文件属性

  fdisk 硬盘分区 date 显示及修改日期 label 设置卷标号 defrag 磁盘碎片整理

  msd 系统检测 path 设置搜寻目录 share 文件共享 memmaker内存优化管理

  help 帮助 restore 恢复备份文件 set 设置环境变量 time 显示及修改时间

  tree 列目录树 debug 随机调试程序 doskey 重新调用DOS命令 prempt 设置提示符 undelete恢复被删的文件 scandisk检测、修理磁盘

  不常用DOS命令:

  diskcomp磁盘比较  append 设置非执行文件路径

  expand 还原DOS文件 fasthelp快速显示帮助信息

  fc 文件比较 interink启动服务器

  setver 设置版本 intersvr启动客户机

  subst 路径替换 qbasic Basic集成环境

  vsafe 防病毒 unformat恢复已格式化的磁盘

  ver 显示DOS版本号 smartdrv设置磁盘加速器

  vol 显示磁盘卷标号 lh 将程序装入高端内存

  ctty 改变控制设备 emm386 扩展内存管理

  常用命令具体介绍:

  一、Dir

  显示目录文件和子目录列表,呵呵,这个当然是人人要知道的。

  可以使用通配符(? 和 *),?表通配一个字符,*表通配任意字符

  *.后缀  指定要查看后缀的文件。 上面其实也可以为“ . 后缀”,例如dir *.exe 等于dir .exe

  /p  每次显示一个列表屏幕。要查看下一屏,请按键盘上的任意键。

  /w  以宽格式显示列表,在每一行上最多显示 5 个文件名或目录名。

  /s  列出指定目录及所有子目录中出现的每个指定的文件名。比win环境下的查找快多了

  dir *.* -> a.txt 把当前目录文件列表写入a.txt

  dir *.* /s -> a.txt 把当前目录文件列表写入a.txt,包括子目录下文件。

  二、Attrib

  显示、设置或删除指派给文件或目录的只读、存档、系统以及隐藏属性。如果在不含参数的情况下使用,则 attrib 会显示当前目录中所有文件的属性。

  +r  设置只读属性。

  -r  清除只读属性。

  +a  设置存档文件属性。

  -a  清除存档文件属性。

  +s  设置系统属性。

  -s  清除系统属性。

  +h  设置隐藏属性。

  -h  清除隐藏属性。

  三、Cls

  清除显示在命令提示符窗口中的所有信息,并返回空窗口,即“清屏”

  四、Exit

  退出当前命令解释程序并返回到系统。

  五、format

  格式化

  /q  执行快速格式化。删除以前已格式化卷的文件表和根目录,但不在扇区之间扫描损坏区域。使用 /q 命令行选项应该仅格式化以前已格式化的完好的卷。

  六、Ipconfig

  显示所有当前的 TCP/IP 网络配置值、刷新动态主机配置协议 (DHCP) 和域名系统 (DNS) 设置。使用不带参数的 ipconfig 可以显示所有适配器的 IP 地址、子网掩码、默认网关。

  /all  显示所有适配器的完整 TCP/IP 配置信息。

  ipconfig 等价于 winipcfg,后者在ME、98 和 95 上可用。尽管 Windows XP 没有提供象 winipcfg 命令一样的图形化界面,但可以使用“网络连接”查看和更新 IP 地址。要做到这一点,请打开 网络连接,右键单击某一网络连接,单击“状态”,然后单击“支持”选项卡。

  该命令最适用于配置为自动获取 IP 地址的计算机。它使用户可以确定哪些 TCP/IP 配置值是由 DHCP、自动专用 IP 地址 (APIPA) 和其他配置配置的。

  七、md

  创建目录或子目录

  八、Move

  将一个或多个文件从一个目录移动到指定的目录。

  九、Nbtstat

  显示本地计算机和远程计算机的基于 TCP/IP (NetBT) 协议的 NetBIOS 统计资料、NetBIOS 名称表和 NetBIOS 名称缓存。Nbtstat 可以刷新 NetBIOS 名称缓存和注册的 Windows Internet 名称服务 (WINS) 名称。使用不带参数的 nbtstat 显示帮助。Nbtstat 命令行参数区分大小写。

  -a remotename  显示远程计算机的 NetBIOS 名称表,其中,RemoteName 是远程计算机的 NetBIOS 计算机名称。

  -A IPAddress  显示远程计算机的 NetBIOS 名称表,其名称由远程计算机的 IP 地址指定(以小数点分隔)。

  十、Netstat

  显示活动的 TCP 连接、计算机侦听的端口、以太网统计信息、IP 路由表、IPv4 统计信息(对于 IP、ICMP、TCP 和 UDP 协议)以及 IPv6 统计信息(对于 IPv6、ICMPv6、通过 IPv6 的 TCP 以及通过 IPv6 的 UDP 协议)。使用时如果不带参数,netstat 显示活动的 TCP 连接。

  -a  显示所有活动的 TCP 连接以及计算机侦听的 TCP 和 UDP 端口。

  十一、Ping

  通过发送“网际消息控制协议 (ICMP)”回响请求消息来验证与另一台 TCP/IP 计算机的 IP 级连接。回响应答消息的接收情况将和往返过程的次数一起显示出来。Ping 是用于检测网络连接性、可到达性和名称解析的疑难问题的主要 TCP/IP 命令。如果不带参数,ping 将显示帮助。名称和Ip地址解析是它的最简单应用也是用的最多的。

  -t  指定在中断前 ping 可以持续发送回响请求信息到目的地。要中断并显示统计信息,请按 CTRL-BREAK。要中断并退出 ping,请按 CTRL-C。

  -lSize  指定发送的回响请求消息中“数据”字段的长度(以字节表示)。默认值为 32。size 的最大值是 65,527。

  十二、Rename (Ren)

  更改文件的名称。

  例如 ren *.abc *.cba

  十三、Set

  显示、设置或删除环境变量。如果没有任何参数,set 命令将显示当前环境设置。

  十四、Shutdown

  允许您关闭或重新启动本地或远程计算机。如果没有使用参数,shutdown 将注销当前用户。

  -m ComputerName  指定要关闭的计算机。

  -t xx  将用于系统关闭的定时器设置为 xx 秒。默认值是 20 秒。

  -l  注销当前用户,这是默认设置。-m ComputerName 优先。

  -s  关闭本地计算机。

  -r  关闭之后重新启动。

  -a  中止关闭。除了 -l 和 ComputerName 外,系统将忽略其它参数。在超时期间,您只可以使用 -a。

  十五、System File Checker (sfc)

  win下才有,在重新启动计算机后扫描和验证所有受保护的系统文件。

  /scannow  立即扫描所有受保护的系统文件。

  /scanonce  一次扫描所有受保护的系统文件。

  /purgecache  立即清除“Windows 文件保护”文件高速缓存,并扫描所有受保护的系统文件。

  /cachesize=x  设置“Windows 文件保护”文件高速缓存的大小,以 MB 为单位。

  十六、type

  显示文本文件的内容。使用 type 命令查看文本文件或者是bat文件而不修改文件

  十七、Tree

  图像化显示路径或驱动器中磁盘的目录结构。

  十八、Xcopy

  复制文件和目录,包括子目录。

  /s  复制非空的目录和子目录。如果省略 /s,xcopy 将在一个目录中工作。

  /e  复制所有子目录,包括空目录。

  十九、copy

  将一个或多个文件从一个位置复制到其他位置

  二十、del

  删除指定文件。

  ftp和bat批命令和net和telnet由于子命令太多,这里不说了,不过这几个都是常用到的。

在DOS下输入MD X:\文件名..\ 回车

所创建的文件夹,直接双击是打不开的,也删除不了.

例:在F盘创建一个名为123的绝密文件夹,再删除他.

在运行里输入CMD回车

输入MD F:\123..\ 回车

打开F盘时会看到有一个名为:"123."的文件夹,双击打开时,出现了一个对话框说.........不能打开.

在运行里输入F:\123..\ 试一试看 会发现该文件被打开了

但是要怎样才能删除此文件呢?请往下看.

在DOS下输入RD F:\123..\ 回车

如果该文件下还有其他文件.就要在后面加 /S 也就是RD F:\123..\ /S 回车.

觉的好的话.就顶吧!

方法一:用copy con DOS内部命令
a:\>copy con lx.txt
I'm a teacher
按ctrl+Z或者F6键
即可
方法二:用edit DOS外部命令
 在Windows 98下的MS-DOS下,用键盘新建文件,文件内容为:“I'm a teacher.”,文件名为LX.TXT,文件保存在A盘。

在模拟环境中运行的,其实在实际环境中我在命令行下键入:echo "I'm a teacher." > A:\LX.TXT,可以达到目的,但是在模拟环境中却不行,哪位知道是怎么回事啊?谢谢!
1月26日

CAsyncSocket对象不能跨线程之分析

现象

用多线程方法设计socket程序时,你会发现在跨线程使用CAsyncSocket及其派生类时,会出现程序崩溃。所谓跨线程,是指该对象在一个线程中调用Create/AttachHandle/Attach函数,然后在另外一个线程中调用其他成员函数。下面的例子就是一个典型的导致崩溃的过程:
CAsyncSocket Socket;
UINT Thread(LPVOID)
{
       Socket.Close ();
       return 0;
}
void CTestSDlg::OnOK() 
{
       // TODO: Add extra validation here
       Socket.Create(0);
       AfxBeginThread(Thread,0,0,0,0,0);
}

其中Socket对象在主线程中被调用,在子线程中被关闭。

跟踪分析

这个问题的原因可以通过单步跟踪(F11)的方法来了解。我们在Socket.Create(0)处设断点,跟踪进去会发现下面的函数被调用:

void PASCAL CAsyncSocket::AttachHandle(
          SOCKET hSocket, CAsyncSocket* pSocket, BOOL bDead)
{
    _AFX_SOCK_THREAD_STATE* pState = _afxSockThreadState;
    BOOL bEnable = AfxEnableMemoryTracking(FALSE);
    if (!bDead)
    {
             ASSERT(CAsyncSocket::LookupHandle(hSocket, bDead) == NULL);
             if (pState->m_pmapSocketHandle->IsEmpty())
             {
                  ASSERT(pState->m_pmapDeadSockets->IsEmpty());
                  ASSERT(pState->m_hSocketWindow == NULL);
                  CSocketWnd* pWnd = new CSocketWnd;
                  pWnd->m_hWnd = NULL;
                  if (!pWnd->CreateEx(0, AfxRegisterWndClass(0),
                                   _T("Socket Notification Sink"),
                                 WS_OVERLAPPED, 0, 0, 0, 0, NULL, NULL))
                 {
                       TRACE0("Warning: unable to create socket notify window!\n");
                       AfxThrowResourceException();
                 }
                 ASSERT(pWnd->m_hWnd != NULL);
                 ASSERT(CWnd::FromHandlePermanent(pWnd->m_hWnd) == pWnd);
                 pState->m_hSocketWindow = pWnd->m_hWnd;
            }
            pState->m_pmapSocketHandle->SetAt((void*)hSocket, pSocket);
    }
    else
    {
           int nCount;
           if (pState->m_pmapDeadSockets->Lookup((void*)hSocket, (void*&)nCount))
                     nCount++;
           else
                     nCount = 1;
           pState->m_pmapDeadSockets->SetAt((void*)hSocket, (void*)nCount);
   }
   AfxEnableMemoryTracking(bEnable);
}

在这个函数的开头,首先获得了一个pState的指针指向_afxSockThreadState对象。从名字可以看出,这似乎是一个和线程相关的变量,实际上它是一个宏,定义如下:

#define _afxSockThreadState AfxGetModuleThreadState()

我们没有必要去细究这个指针的定义是如何的,只要知道它是和当前线程密切关联的,其他线程应该也有类似的指针,只是指向不同的结构。

在这个函数中,CAsyncSocket创建了一个窗口,并把如下两个信息加入到pState所管理的结构中:

    pState->m_pmapSocketHandle->SetAt((void*)hSocket, pSocket);
    pState->m_pmapDeadSockets->SetAt((void*)hSocket, (void*)nCount);
    pState->m_hSocketWindow = pWnd->m_hWnd;
    pState->m_pmapSocketHandle->SetAt((void*)hSocket, pSocket);

当调用Close时,我们再次跟踪,就会发现在KillSocket中,下面的函数出现错误:

    void PASCAL CAsyncSocket::KillSocket(SOCKET hSocket, CAsyncSocket* pSocket)
    {
            ASSERT(CAsyncSocket::LookupHandle(hSocket, FALSE) != NULL);

我们在这个ASSERT处设置断点,跟踪进LookupHandle,会发现这个函数定义如下:

CAsyncSocket* PASCAL CAsyncSocket::LookupHandle(SOCKET hSocket, BOOL bDead)
{
     CAsyncSocket* pSocket;
     _AFX_SOCK_THREAD_STATE* pState = _afxSockThreadState;
     if (!bDead)
     {
             pSocket = (CAsyncSocket*)
             pState->m_pmapSocketHandle->GetValueAt((void*)hSocket);
             if (pSocket != NULL)
                  return pSocket;
    }
    else
    {
             pSocket = (CAsyncSocket*)
                  pState->m_pmapDeadSockets->GetValueAt((void*)hSocket);
             if (pSocket != NULL)
                   return pSocket;
    }
    return NULL;
}

显然,这个函数试图从当前线程查询关于这个 socket的信息,可是这个信息放在创建这个socket的线程中,因此这种查询显然会失败,最终返回NULL。

有人会问,既然它是ASSERT出错,是不是Release就没问题了。这只是自欺欺人。ASSERT/VERIFY都是检验一些程序正常运行必须正确的条件。如果ASSERT都失败,在Release中也许不会显现,但是你的程序肯定运行不正确,啥时候出错就不知道了。

如何在多线程之间传递socket

有些特殊情况下,可能需要在不同线程之间传递socket。当然我不建议在使用CAsyncSOcket的时候这么做,因为这增加了出错的风险(尤其当出现拆解包问题时,有人称为粘包,我基本不认同这种称呼)。如果一定要这么做,方法应该是:

  1. 当前拥有这个socket的线程调用Detach方法,这样socket句柄和C++对象及当前线程脱离关系
  2. 当前线程把这个对象传递给另外一个线程
  3. 另外一个线程创建新的CAsyncSocket对象,并调用Attach

上面的例子,我稍微做修改,就不会出错了:

CAsyncSocket Socket;
UINT Thread(LPVOID sock)
{
         Socket.Attach((SOCKET)sock);//如果在线程外close(),记得在离开时Detach().
         Socket.Close ();
         return 0;
}
void CTestSDlg::OnOK() 
{
         // TODO: Add extra validation here
         Socket.Create(0);
         SOCKET hSocket = Socket.Detach ();
         AfxBeginThread(Thread,(LPVOID)hSocket,0,0,0,0);
}
引用自:馨荣家园
----------------------------------------------------------------------------------------------------

本文以及另外两篇相关文章解释 Windows Sockets 编程方面的一些问题。本文介绍阻塞。其他问题包含在 Windows Sockets:字节排序Windows Sockets:转换字符串文章中。

如果使用 CAsyncSocket 类或从其派生,则您需要自己管理这些问题。如果您使用 CSocket 类或从其派生,则由 MFC 管理它们。阻塞

套接字可以处于“阻塞模式”或“无阻塞模式”。处于阻塞(或同步)模式时,套接字的函数直到可以完成自己的操作时才返回。这称为“阻塞”,因为函数被调用的套接字在调用返回前无法执行任何操作──它被阻塞了。例如,对 Receive 成员函数的调用可能需要任意长的时间才能完成,因为它要等待发送应用程序来发送(使用 CSocket 或使用带阻塞的 CAsyncSocke 即是如此)。如果 CAsyncSocket 对象处于无阻塞模式(异步操作),调用会立即返回,而当前错误代码(可使用 GetLastError 成员函数检索)为 WSAEWOULDBLOCK ,它指出由于模式的原因,调用若不立即返回则将阻塞。( CSocket 永远不返回 WSAEWOULDBLOCK 。该类为您管理阻塞。)

在 32 位操作系统(如 Windows 95 或 Windows 98)和 16 位操作系统(如 Windows 3.1)下,套接字的行为是不同的。与 16 位操作系统不同,32 位操作系统使用抢占式多任务处理技术并提供多线程运行方式。在 32 位操作系统下,可以将套接字放在单独的辅助线程中。线程中的套接字可以在不妨碍应用程序中其他活动的情况下阻塞,并且不必在阻塞上花费计算时间。有关多线程编程的信息,请参见文章多线程编程。

注意: 在多线程应用程序中,可以使用 CSocket 的阻塞特性来简化程序设计,而不影响用户界面的响应。通过处理主线程中的用户交互和备用线程中的 CSocket 处理,可以将这些逻辑操作分开。在非多线程的应用程序中,这两个活动必须合并为单个线程来处理。这通常意味着使用 CAsyncSocket 以根据需要处理通信请求,或重写 CSocket::OnMessagePending 以在漫长的同步活动中处理用户操作。

 

其余的讨论针对以 16 位操作系统为目标的程序员:

通常,如果使用的是 CAsyncSocket ,则应避免使用阻塞操作,而应使用异步操作。例如,在异步操作中,从调用 Receive 后接收到 WSAEWOULDBLOCK 错误代码那一刻开始,您将一直等到 OnReceive 成员函数被调用以通知您可以再次读取。通过回调套接字的适当回调通知函数(如 OnReceive)来完成异步调用。

在 Windows 下,阻塞调用被认为是错误的做法。默认情况下,CAsyncSocket 支持异步调用,而且您必须使用回调通知自己管理阻塞。另一方面,CSocket 类是同步的。它抽取 Windows 消息并为您管理阻塞。

有关阻塞的更多信息,请参见 Windows Sockets 规范。有关“On”函数的更多信息,请参见 Windows Sockets:套接字通知和 Windows Sockets:从套接字类派生。

本文以及另外两篇相关文章解释 Windows Sockets 编程方面的一些问题。本文介绍转换字符串。其他问题在 Windows Sockets:阻塞Windows Sockets:字节排序中介绍。

如果使用 CAsyncSocket 类或从其派生,则您需要自己管理这些问题。如果您使用 CSocket 类或从其派生,则由 MFC 管理它们。转换字符串

如果在使用以不同的宽字符格式(如 Unicode 或多字节字符集 MBCS)存储的字符串的应用程序间通信,或在这些应用程序与使用 ANSI 字符串的应用程序间通信,您必须使用 CAsyncSocket 自己管理转换。和 CSocket 对象一起使用的 CArchive 对象通过 CString 类的功能管理此转换。有关更多信息,请参见位于 Platform SDK 中的 Windows Sockets 规范。

Windows Sockets:使用 CAsyncSocket 类

http://tech.163.com/school · 2005-10-09 16:08:40 · 来源: MSDN

 

本文介绍 CAsyncSocket 类的用法。请注意,该类在非常低的级别上封装 Windows Sockets API。 CAsyncSocket 适合那些对网络通信细节很了解,但希望利用回调的便利通知网络事件的程序员使用。基于该假定,本文仅提供基本说明。如果想利用 Windows Sockets 方便地处理 MFC 应用程序中的多个网络协议,而又不想放弃灵活性,可以考虑使用 CAsyncSocket 。您可能也会感觉到,自己直接编写通信程序要比使用 CSocket 类的通用替换模型效果更好。

“MFC 参考”中对 CAsyncSocket 进行了描述。Visual C++ 也提供了位于 Platform SDK 中的 Windows Sockets 规范。具体细节由您决定。Visual C++ 不提供 CAsyncSocket 的示例应用程序。

如果您对网络通信不是很了解,希望获得一个简单的解决方案,请使用带 CArchive 对象的 CSocket 类。有关更多信息,请参见 Windows Sockets:使用带存档的套接字

本文包括:

  • 创建和使用 CAsyncSocket 对象。
  • 您具有的 CAsyncSocket 责任。
创建和使用 CAsyncSocket 对象

 

使用 CAsyncSocket

  1. 构造一个 CAsyncSocket 对象并使用该对象创建基础 SOCKET 句柄。

     

    套接字的创建遵循两阶段构造的 MFC 模式。

    例如: CAsyncSocket sock;sock.Create( ); // Use the default parameters
    - 或 -
    CAsyncSocket* pSocket = new CAsyncSocket;int nPort = 27;pSocket-> Create( nPort, SOCK_DGRAM );

    上面的第一个构造函数在堆栈上创建一个 CAsyncSocket 对象,第二个构造函数在堆上创建 CAsyncSocket 。上面的第一个 Create 调用使用默认参数创建流式套接字,第二个 Create 调用创建具有指定端口和地址的数据文报套接字。(任一个 Create 版本都可以和任一种构造方法一起使用。)

    Create 的参数有:

    • “端口”:短整型。

       

      对于服务器套接字,必须指定端口。对于客户端套接字,通常接受此参数的默认值,该值允许 Windows Sockets 选择端口。

    • 套接字类型: SOCK_STREAM (默认值)或 SOCK_DGRAM
    • 套接字“地址”,如“ftp.microsoft.com”或“128.56.22.8”。

       

      该地址为网络上的网际协议 (IP) 地址。很可能要始终依赖此参数的默认值。

     

    关于术语“端口”和“套接字地址”的解释见 Windows Sockets:端口和套接字地址

  2. 如果套接字是客户端,则使用 CAsyncSocket::Connect 将此套接字对象连接到服务器套接字。

     

    - 或 -

    如果套接字是服务器,则将套接字设置为开始侦听(使用 CAsyncSocket::Listen)来自客户端的连接尝试。接收到连接请求时,用 CAsyncSocket::Accept 接受该请求。

    接受连接后,可以执行验证密码等任务。

    注意 Accept 成员函数采用对新的空 CSocket 对象的引用作为它的参数。在调用 Accept 之前,必须构造该对象。如果此套接字对象超出范围,则连接关闭。不要对这个新套接字对象调用 Create 。有关示例,请参见文章 Windows Sockets:操作顺序
  3. 通过调用 CAsyncSocket 对象的封装 Windows Sockets API 函数的成员函数,与其他套接字进行通信。

     

    请参见“MFC 参考”中的 Windows Sockets 规范和 CAsyncSocket 类。

  4. 销毁 CAsyncSocket 对象。

     

    如果在堆栈上创建了套接字对象,当包含函数超出范围时将调用此对象的析构函数。如果使用 new 运算符在堆上创建了套接字对象,则您必须负责使用 delete 运算符销毁此对象。

    析构函数在销毁对象之前调用对象的 Close 成员函数。

 

有关代码中该顺序的示例(实际上是对于 CSocket 对象),请参见 Windows Sockets:操作顺序。您对 CAsyncSocket 的责任

创建 CAsyncSocket 类的对象后,该对象封装 Windows SOCKET 句柄并提供对此句柄的操作。使用 CAsyncSocket 时,如果您直接使用 API,则必须处理可能面对的所有问题。例如:

  • “阻塞”方案。
  • 发送和接收计算机之间的字节顺序差异。
  • 在 Unicode 和多字节字符集 (MBCS) 字符串之间转换。

 

有关这些术语的定义和其他信息,请参见 Windows Sockets:阻塞Windows Sockets:字节排序Windows Sockets:转换字符串

尽管存在这些问题,但如果应用程序需要您能获得所有的灵活性和控制能力, CAsycnSocket 类可能是正确的选择。如果应用程序没有这种需求,可考虑使用 CSocket 类。 CSocket 向您隐藏大量详细信息:它在阻塞调用期间抽取 Windows 消息并赋予您访问 CArchive 的权限,而 CArchive 为您管理字节顺序差异和字符串转换。

有关更多信息,请参见:

    Windows Sockets:背景知识

    Windows Sockets:流式套接字

    Windows Sockets:数据文报套接字

    Windows Sockets:使用带存档的套接字

    http://tech.163.com/school · 2005-10-09 15:22:30 · 来源: MSDN

     

    本文描述 CSocket 编程模型。CSocket 类提供了比 CAsyncSocket 类抽象化级别更高的套接字支持。CSocket 使用 MFC 序列化协议的一种版本,通过 MFC CArchive 对象将数据传递给套接字对象,或者从套接字对象传出数据。CSocket 提供阻塞(同时管理 Windows 消息的后台处理),并赋予您访问 CArchive 的权限,而 CArchive 则管理着必须由您自己使用原始 API 或 CAsyncSocket 类来管理的通信的许多方面。

    提示:可以单独使用 CSocket 类作为 CAsyncSocket 的更方便版本,但最简单的编程模型是使用带 CArchive 对象的 CSocket

     

    有关带存档的套接字实现的工作机制的更多信息,请参见 Windows Sockets:带存档的套接字的工作方式。有关示例代码,请参见 Windows Sockets:操作顺序Windows Sockets:带存档的套接字示例。有关通过从套接字类派生自己的类获得的某些功能的信息,请参见 Windows Sockets:从套接字类派生

    注意:如果正在编写与已建立的(非 MFC)服务器通信的 MFC 客户程序,则不要通过存档发送 C++ 对象。除非该服务器是一个 MFC 应用程序,它知道您要发送的对象的类型,否则服务器将无法接收和反序列化这些对象。有关与非 MFC 应用程序通信的主题的相关材料,另请参见文章 Windows Sockets:字节排序
    CSocket 编程模型

     

    使用 CSocket 对象涉及创建数个 MFC 类对象并将它们关联起来。在下面的一般过程中,服务器套接字和客户端套接字都将采取每一步骤(步骤 3 除外,此步骤中每个套接字类型要求不同的操作)。

    提示:在运行时,服务器应用程序通常首先做好准备然后“侦听”客户端应用程序何时寻求连接。如果客户端尝试连接时服务器未准备好,一般需要用户应用程序稍后再尝试连接。

     

    设置服务器套接字和客户端套接字之间的通信

    1. 构造一个 CSocket 对象。
    2. 使用此对象创建基础 SOCKET 句柄。

       

      对于 CSocket 客户端对象,除非需要数据文报套接字,否则通常应使用默认参数来 Create 该对象。对于 CSocket 服务器对象,则必须在 Create 调用中指定端口。

      注意:CArchive 不适用于数据文报套接字。如果想将 CSocket 用于数据文报套接字,必须像使用 CAsyncSocket 那样使用该类,即不带存档。因为数据文报是不可靠的(不保证送达,并且可能重复或顺序不对),它们不能通过存档与序列化兼容。而您期望序列化操作可以可靠地、按顺序完成。如果试图将带 CArchive 对象的 CSocket 用于数据文报,则 MFC 断言失败。
    3. 如果套接字是客户端对象,则调用 CAsyncSocket::Connect 将此套接字对象连接到服务器套接字。

       

      - 或 -

      如果套接字是服务器端对象,则调用 CAsyncSocket::Listen 开始侦听来自客户端的连接尝试。接收到连接请求时,调用 CAsyncSocket::Accept 接受该请求。

      注意:Accept 成员函数采用对新的空 CSocket 对象的引用作为它的参数。在调用 Accept 之前,必须构造该对象。如果此套接字对象超出范围,则连接关闭。不要对这个新套接字对象调用 Create
    4. 创建一个 CSocketFile 对象,将 CSocket 对象与它关联起来。
    5. 创建一个 CArchive 对象用于加载(接收)或存储(发送)数据。此存档与 CSocketFile 对象关联。

       

      注意:CArchive 不适用于数据文报套接字。

    6. 使用 CArchive 对象在客户端套接字与服务器套接字之间传递数据。

       

      注意,不管是加载(接收)还是存储(发送),给定的 CArchive 对象只在一个方向上移动数据。某些情况下,需要使用两个 CArchive 对象:一个用于发送数据,一个用于接收确认。

      接受连接并设置存档后,可以执行验证密码之类的任务。

    7. 销毁存档、套接字文件和套接字对象。
      注意CArchive 类提供了专门与 CSocket 类一起使用的 IsBufferEmpty 成员函数。例如,如果缓冲区包含多条数据消息,则需要一直循环到读完所有消息和清空缓冲区。否则,下一个指示有数据要接收的通知可能会无限期延迟。使用 IsBufferEmpty 可确保检索所有数据。有关使用 IsBufferEmpty 的示例,请参见 CHATSRVR 示例应用程序。有关 MFC 示例的源代码和信息,请参见 MFC 示例。

     

    Windows Sockets:操作顺序一文用示例代码阐释了此进程的两端。

    Windows Sockets:背景知识

    http://tech.163.com/school · 2005-10-09 14:54:00 · 来源: MSDN

     

    本文介绍 Windows Sockets 的性质和用途。其他内容还包括:

    • 定义术语“套接字”。
    • 描述 SOCKET 句柄数据类型。
    • 描述套接字的用途。

     

    Windows Sockets 规范为 Microsoft Windows 定义了一个二进制兼容网络编程接口。Windows Sockets 基于 Berkeley Software Distribution(BSD,4.3 版)中的 UNIX 套接字实现,后者是美国加州大学伯克利分校开发的。该规范包括针对 Windows 的 BSD 样式套接字例程和扩展。通过使用 Windows Sockets,应用程序能够在任何符合 Windows Sockets API 的网络上通信。在 Win32 上,Windows Sockets 提供线程安全。

    许多网络软件供应商支持网络协议下的 Windows Sockets,这些协议包括:传输控制协议/网际协议 (TCP/IP)、Xerox 网络系统 (XNS)、Digital Equipment Corporation 的 DECNet 协议和 Novell Corporation 的互联网包交换协议/顺序分组报文交换协议 (IPX/SPX) 等。虽然目前的 Windows Sockets 规范定义了 TCP/IP 的套接字抽象化,但任何网络协议都可以通过提供自己版本的、实现 Windows Sockets 的动态链接库 (DLL) 来满足 Windows Sockets。用 Windows Sockets 编写的商用应用程序示例包括 X Windows 服务器、终端模拟器和电子邮件系统。

    注意: Windows Sockets 的用途是将基础网络抽象出来,这样,您不必对网络非常了解,并且您的应用程序可在任何支持套接字的网络上运行。因此,本文档不讨论网络协议的细节内容。

     

    Microsoft 基础类库 (MFC) 通过提供两个类来支持使用 Windows Sockets API 进行编程。其中一个类为 CSocket ,它提供高级抽象化来简化网络通信编程。

    Windows Sockets 规范“Windows Sockets:用于 Microsoft Windows 环境下的网络计算的开放接口”现在为 1.1 版本,它是 TCP/IP 群体中一个由个人和公司组成的大团体开发的,是一个开放的网络标准,可免费使用。套接字编程模型当前支持一个“通信域”,该“通信域”使用网际协议组 (Internet Protocol Suite)。该规范可在 Platform SDK 中获得。

    提示: 因为套接字使用网际协议组,所以它们对于支持“信息高速公路”上 Internet 通信的应用程序是首选方式。

    套接字的定义

     

    套接字是一个通信终结点,它是 Windows Sockets 应用程序用来在网络上发送或接收数据包的对象。套接字具有类型,与正在运行的进程相关联,并且可以有名称。目前,套接字一般只与使用网际协议组的同一“通信域”中的其他套接字交换数据。

    这两种套接字都是双向的,是可以同时在两个方向上(全双工)进行通信的数据流。

    可用的套接字类型有以下两种:

    • 流式套接字

       

      流式套接字提供没有记录边界的数据流,即字节流。字节流能确保以正确的顺序无重复地被送达。

    • 数据文报套接字

       

      数据文报套接字支持面向记录的数据流,但不能确保能被送达,也无法确保按照发送顺序或不重复。

     

    “有序”指数据包按发送的顺序送达。“不重复”指一个特定的数据包只能获取一次。

    注意: 在某些网络协议下(如 XNS),流可以面向记录,即作为记录流而非字节流。但在更常用的 TCP/IP 协议下,流为字节流。Windows Sockets 提供与基础协议无关的抽象化级别。

     

    有关上述类型以及各种套接字适用情形的信息,请参见 Windows Sockets:流式套接字Windows Sockets:数据文报套接字

    SOCKET 数据类型

    每一个 MFC 套接字对象封装一个 Windows Sockets 对象的句柄。该句柄的数据类型为 SOCKET。SOCKET 句柄类似于窗口的 HWND。MFC 套接字类提供对封装句柄的操作。

    Platform SDK 中详细描述了 SOCKET 数据类型。

    套接字的用途

    套接字的作用非常大,至少在下面三种通信上下文中如此:

    • 客户端/服务器模型。
    • 对等网络方案,如聊天应用程序。
    • 通过让接收应用程序将消息解释为函数调用来进行远程过程调用 (RPC)。
    提示: 最适合使用 MFC 套接字的情况是当同时编写通信的两端时:在两端都使用 MFC。有关该主题(包括如何管理与非 MFC 应用程序通信的情况)的更多信息,请参见 Windows Sockets:字节排序

    Windows Sockets:流式套接字

    http://tech.163.com/school · 2005-10-09 15:03:48 · 来源: MSDN

     

    本文描述流式套接字,它是两种可用的 Windows Sockets 类型中的一种。(另一种类型是数据文报套接字。)

    流式套接字提供没有记录边界的数据流:可以是双向的字节流(应用程序是全双工:可以通过套接字同时传输和接收)。可依赖流传递有序的、不重复的数据。(“有序”指数据包按发送顺序送达。“不重复”指一个特定的数据包只能获取一次。)这能确保收到流消息,而流非常适合处理大量数据。

    网络传输层可将数据拆分为或分组为若干个大小适当的数据包。 CSocket 类将为您处理打包和解包。

    流基于显式连接:套接字 A 请求与套接字 B 建立连接;套接字 B 接受或拒绝此连接请求。

    打电话的情况与流非常相似:正常情况下,接听方听到您的话和您讲话时的顺序一样,没有重复和遗漏。流套接字适合文件传输协议 (FTP) 这类实现,此协议有利于传输任意大小的 ASCII 或二进制文件。

    如果必须保证数据送达而且数据大小很大时,流式套接字优于数据文报套接字。有关流式套接字的更多信息,请参见 Windows Sockets 规范。该规范可在 Platform SDK 中获得。

    MFC 示例 CHATTERCHATSRVR 都使用流式套接字。这些示例可能已经设计为使用数据文报套接字向网络上的所有接收套接字广播。而目前的设计更好,这是因为:

    • 广播模型受制于网络“洪水”(或“风暴”)问题。
    • 后来采用的客户端-服务器模型更有效。
    • 流式模型提供可靠的数据传输,数据文报模型则未提供。
    • 最终模型利用在 CArchive 类借给 CSocket 类的 Unicode 和 ANSI 套接字应用程序之间通信的能力。

    注意: 如果使用 CSocket 类,则必须使用流。如果将套接字类型指定为 SOCK_DGRAM ,则 MFC 断言失败。

    Windows Sockets:数据文报套接字

    http://tech.163.com/school · 2005-10-09 15:15:43 · 来源: MSDN

     

    本文描述数据文报套接字,它是两种可用的 Windows Sockets 类型中的一种。(另一种类型是 流式套接字。)

    数据文报套接字支持双向数据流,此数据留不能保证按顺序和不重复送达。数据文报也不保证是可靠的;它们可能无法到达目的地。数据文报可能不按顺序到达并且可能会重复,但只要记录的大小没有超过接收端的内部大小限制,就会保持数据中的记录边界。您负责管理顺序和可靠性。(可靠性在局域网 [LAN] 上往往很好,但在广域网 [WAN] 如 Internet 上却不太好。)

    数据文报为“无连接”的,也就是不建立显式连接。可将数据文报消息发送到指定的套接字,然后从指定的套接字接收消息。

    数据文报套接字的一个示例是使网络上的系统时钟保持同步的应用程序。这阐释了数据文报套接字的一个附加功能,即至少在某些设置中,向大量的网络地址广播消息。

    在面向记录的数据方面,数据文报套接字优于流式套接字。有关数据文报套接字的更多信息,请参见 Platform SDK 中的 Windows Sockets 规范。

    Windows Sockets:字节排序

    http://tech.163.com/school · 2005-10-09 16:30:27 · 来源: MSDN

     

    本文以及另外两篇相关文章解释 Windows Sockets 编程方面的一些问题。本文介绍字节排序。其他问题在文章 Windows Sockets:阻塞Windows Sockets:转换字符串中介绍。

    如果使用 CAsyncSocket 类或从其派生,则您需要自己管理这些问题。如果您使用 CSocket 类或从其派生,则由 MFC

    管理它们。

    字节排序

    不同的计算机结构有时使用不同的字节顺序存储数据。例如,基于 Intel 的计算机存储数据的顺序与 Macintosh

    (Motorola) 计算机相反。Intel 字节顺序称为“Little-Endian”,与网络标准“Big-Endian”顺序也相反。下表解

    释这些术语。

    Big-Endian 和 Little-Endian 字节排序

    字节排序 含义
    Big-Endian 最重要的字节在词的左端。
    Little-Endian 最重要的字节在词的右端。

    通常,您不必为在网络上发送和接收的数据的字节顺序转换担心,但在有些情况下,您必须转换字节顺序。

    何时必须转换字节顺序

    在下列情况中需要转换字节顺序:

    • 传递的信息需要由网络解释(与发送到另一台计算机的数据相反)。例如,可能传递端口和地址,这些信息

    必须为网络理解。

    • 与之通信的服务器应用程序不是 MFC 应用程序(并且没有它的源代码)。如果两台计算机不共享相同的字节

    排序,则需要字节顺序转换。

    何时不必转换字节顺序

    在下列情况下可以免去转换字节顺序的工作:

    • 两端的计算机可以同意不交换字节,并且这两台计算机使用相同的字节顺序。
    • 与之通信的服务器是 MFC 应用程序。
    • 有与之通信的服务器的源代码,因此可以明确地判断是否必须转换字节顺序。
    • 可以将服务器移植到 MFC。这样做相当容易,所得到的通常是更小、更快的代码。

    使用 CAsyncSocket 时,您必须自己管理任何必需的字节顺序转换。Windows Sockets 将“Big-Endian”字节顺

    序模型标准化,并提供在该顺序和其他顺序之间转换的函数。然而,与 CSocket 一起使用的 CArchive 使用相

    反的顺序(“Little-Endian”),但 CArchive 为您管理字节顺序转换的细节。通过在应用程序中使用这种标准

    排序,或通过使用 Windows Sockets 字节顺序转换函数,可增强代码的可移植性。

    最适合使用 MFC 套接字的情况是当编写通信的两端时:在两端都使用 MFC。如果正在编写将与非 MFC 应用程序

    通信的应用程序(如 FTP 服务器),则在向存档对象传递数据前,您可能需要使用 Windows Sockets 转换例程

    ntohs ntohl htons htonl 自己管理字节交换。本文稍后将给出这些用于与非 MFC 应用程序通信的函数

    示例。

    注意 当通信的另一端不是 MFC 应用程序时,也必须避免将从 CObject 派生的 C++ 对象以流的形式输入存

    档,因为接收端无法处理它们。请参见 Windows Sockets:使用带存档的套接字中的说明。

    有关字节顺序的更多信息,请参见 Platform SDK 中的 Windows Sockets 规范。

    字节顺序转换示例

    下面的示例显示使用存档的 CSocket 对象的一个序列化函数。它还阐释了在 Windows Sockets API 中如何使用

    字节顺序转换函数。

    该示例显示这样的情形:您正在编写与非 MFC 服务器应用程序通信的客户程序,而您没有访问该服务器应用程序

    源代码的权限。在这种情况下,必须假设非 MFC 服务器使用标准的网络字节顺序。相反,MFC 客户端应用程序对

    CSocket 对象使用 CArchive 对象,而 CArchive 使用与网络标准相反的“Little-Endian”字节顺序。

    假设要与之通信的非 MFC 服务器具有如下已建立的消息包协议:

    struct Message { long MagicNumber; unsigned short Command; short Param1; long Param2; }; 
    

    上述内容用 MFC 术语表示则为:

    struct Message { long m_lMagicNumber; short m_nCommand; short m_nParam1; long m_lParam2; void Serialize 
    
    ( CArchive& ar ); };

    在 C++ 中, struct 和类在本质上是一回事。 Message 结构可以有成员函数,如以上声明的 Serialize 成员函数。

    Serialize 成员函数可能为如下形式: void Message::Serialize(CArchive& ar)
    {
    if (ar.IsStoring())
    {
    ar < < (DWORD)htonl(m_lMagicNumber);
    ar < < (WORD)htons(m_nCommand);
    ar < < (WORD)htons(m_nParam1);
    ar < < (DWORD)htonl(m_lParam2);
    }
    else
    {
    WORD w;
    DWORD dw;
    ar > > dw;
    m_lMagicNumber = ntohl((long)dw);
    ar > > w ;
    m_nCommand = ntohs((short)w);
    ar > > w;
    m_nParam1 = ntohs((short)w);
    ar > > dw;
    m_lParam2 = ntohl((long)dw);
    }
    }

    该示例要求进行数据字节顺序转换,因为一端的非 MFC 服务器应用程序的字节排序与另一端在 MFC 客户端应用程序中使用的 CArchive 明显不匹配。该示例阐释了 Windows Sockets 提供的几个字节顺序转换函数。下表描述了这些函数。

    Windows Sockets 字节顺序转换函数

    函数 作用
    ntohs 将 16 位数量从网络字节顺序转换为主机字节顺序(从 Big-Endian 转换为 Little-Endian)。
    ntohl 将 32 位数量从网络字节顺序转换为主机字节顺序(从 Big-Endian 转换为 Little-Endian)。
    htons 将 16 位数量从主机字节顺序转换为网络字节顺序(从 Little-Endian 转换为 Big-Endian)。
    htonl 将 32 位数量从主机字节顺序转换为网络字节顺序(从 Little-Endian 转换为 Big-Endian)。

    此示例的另一个要点是,当通信另一端的套接字应用程序为非 MFC 应用程序时,必须避免出现如下列语句的操作:

    ar pMsg; 
    

    这里的 pMsg 是指向从 CObject 类派生的 C++ 对象的指针。这将发送多余的与对象关联的 MFC 信息,而服务器并不理解这些信息,因为只有服务器是 MFC 应用程序时才理解。

    有关更多信息,请参见: