糖儿飞教你学C++ Socket网络编程——7. Win32 API网络通信程序

对于WinSock编程的初学者来说,由于控制台程序不涉及Windows的界面及消息响应机制,因此能更容易理解WinSock编程的流程,但目前大多数应用程序都是Windows界面的,因此需要学习将控制台程序改造成Windows界面的程序,而改成Windows界面程序的关键是将WinSock编程的代码嵌入到Windows界面程序的合适位置中。

3.1 Windows对话框程序

对于Windows界面的程序来说,可分为2种,一种是对话框程序,一种是窗口程序,其中窗口程序又可分为SDI(单文档窗口)和MDI(多文档窗口)。

对话框与窗口的区别在于:窗口的上方有菜单栏和工具按钮栏,下方有状态栏。例如Word软件,就是一个窗口程序;而对话框没有菜单栏和工具栏等,例如QQ的登录界面就是一个对话框。因此,对话框其实是一种简单的窗体,对于简单的Windows界面程序,一般都采用对话框程序来制作(本书所有的Windows程序都是基于对话框的)。

3.1.1 新建对话框程序

在VC6中,执行菜单命令“文件→新建”,将打开如图3-1所示的新建工程对话框中,要新建Windows界面的应用程序,有以下两种方法:

1)选择Win32 Application,这种方法是使用Windows API函数开发图形界面的应用程序,API是应用程序接口(Application Programming Interface)的意思,Windows API编程就是指调用Windows的接口函数来进行程序的编写,例如MessageBox()就是一个API函数或者说接口函数。微软提供了上千个标准的API函数,这些函数定义在微软的SDK(Software Development Kit,软件开发包)提供的头文件、库文件、工具之中。因此Windows API编程也称为Windows SDK编程。

2)选择MFC APPWizard(exe),这是使用MFC类库开发图形界面的应用程序。为了降低API函数编写Windows应用程序的难度,微软开发了微软基础类库(Microsoft Foundation Class, MFC),MFC采用面向对象技术,将几乎所有的SDK API函数封装在不同的类之中,从而提供了一套专门用于开发Windows应用程序的开发框架。

图3-1 新建工程对话框

一般来说,初学者学习Windows界面编程,应先学习Windows API编程,有了一定基础后再学习MFC编程,这样可以降低学习难度,且能更深入地理解MFC的实现原理,因此本章专门介绍Windows API编程。

MFC在新建工程的第二步提供了一个“基本对话框”选项,专门用来创建对话框程序,而Win32 Application在新建工程时没有提供新建对话框程序选项,需要手工修改“一个典型的Hello World程序”的代码,具体步骤如下:

1. 使用Win32 Application新建对话框程序

1)新建工程,选择“Win32 Application”,输入工程名(如Dem),单击“下一步”,在图3-2所示的对话框中,选择“一个典型的Hello World程序”,单击“完成”。

图3-2 建立Win32 Application

如果要在VS2010中新建项目,则执行菜单命令“文件→新建→项目”,在Visual C++中,选择“Win32项目”,在下方“名称”后输入项目名,在下一步中单击“完成”即可。

2)直接运行该工程。在图3-3所示的工作空间窗口,单击Build(F7),再单击Execute(Ctrl+F5),就会弹出如图3-4所示的Windows窗体程序,执行该窗体中的菜单命令“Help→About”,图3-4中将出现About对话框。可见该程序包含一个窗体和一个对话框。

图3-3 工作空间窗口

图3-4 Hello World程序运行效果

在图3-3中,选择底部的ResourceView选项卡,可以查看该窗口程序对应的资源文件,如图3-5所示。其中,IDD_ABOUTBOX是图3-4中弹出对话框的ID,双击它可对该对话框资源进行编辑。另外,由于该程序是一个窗口程序,所以具有菜单(Menu)资源。

在图3-3中,选择ClassView选项卡,可以查看该程序中的所有类,如图3-6所示。由于该程序中没有创建任何类(class),因此程序中所有的函数和变量在Globals目录下,表示它们是全局函数或全局变量。可见,全局函数就是不属于任何类的函数。

         

图3-5 ResourceView选项卡      图3-6 ClassView选项卡

3)接下来我们要去掉图3-4的窗体界面让程序直接弹出About对话框。方法是:在图3-3的左侧工作区中选择FileView选项卡,找到该工程的源文件(工程名.cpp,如Dem.cpp),将WinMain函数中的第1行(文件的第25行)开始到DialogBox上一行(147行)的代码全部删除,再从DialogBox下一行(149行)到return 0的上一行之间的代码也全部删除。也就是只保留WinMain函数中DialogBox一行(148行)和return 0; 一行,再将DialogBox函数中第3个参数由hWnd改为NULL。此时WinMain函数的代码如下:

int APIENTRY WinMain(……){

   DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, NULL, (DLGPROC)About);

   return 0;

}

其中:

WinMain()函数是Windows应用程序的入口函数,相当于控制台程序中的main函数。

DialogBox函数的功能是创建一个对话框,hWnd是调用该对话框的窗口的句柄变量名,由于窗口对应的所有代码已经被删除,因此将调用该对话框的窗口句柄改成NULL,这样启动程序时,会直接加载对话框。

DialogBox()函数有4个参数,含义如下:

① hInst:设置本对话框属于当前进程,因为HINSTANCE是窗口进程的句柄;

② IDD_ABOUTBOX:设置本对话框使用哪个对话框的资源;

③ NULL:指定本对话框的父窗口是哪个,如果没有父窗口,则设为NULL;

④ About:指定本对话框的消息处理函数,用来处理对话框初始化及按钮等产生的消息。

提示:句柄(handle)是Windows编程中的重要概念,句柄是Windows中各个对象的一个唯一的、固定不变的ID;作用上,Windows使用句柄来标识应用程序中的不同对象或同类中的不同实例,诸如,一个窗口,按钮,图标,滚动条,输出设备,控件或者文件等。逻辑上,句柄相当于Windows对象指针的指针;Windows通过对象的地址来访问对象,但Windows采用虚拟内存机制,对象的地址是经常发生变化的,为此,需要使用一个内存区域来记录对象的地址,这块内存区域的地址在程序的运行中是不会变化的,而句柄的值就是这块内存区域的地址,因此,句柄相当于Windows对象指针的指针。

4)在对话框中添加控件,修改后对话框界面如图3-7所示,方法是:

①单击图3-8所示的控件工具栏中的“选择控件”工具,按住Shift键依次选中图3-4About对话框左侧的图片框和2个静态文本,按Del键将其删除。

②在图3-8所示的控件工具栏中选择相应的控件,向对话框中添加1个静态文本控件,2个编辑框和1个按钮。

③ 在图3-9所示的属性面板中设置控件的属性。方法是先选中控件,按右键单击属性(或按Alt+Enter键),将弹出如图3-9所示的控件属性面板,将各控件的ID设置为如图3-10所示。再设置静态文本控件、两个按钮和对话框的标题如图3-7所示,并在属性面板中将图3-7中下方编辑框的样式选项勾选为“只读”。

图3-7 对话框的界面

图3-8 控件工具栏

图3-9 控件的属性面板

图3-10 对话框各控件的ID值

提示:

1)在图3-9的属性面板中,ID是程序访问控件的依据,例如GetDlgItem(hDlg, IDC_ EDIT2),就能让程序获取到IDC_EDIT2这个编辑框,因此,除静态文本外,其他控件的ID名都不能重复。而标题属性用来设置控件上显示的内容,因此,静态文本和按钮通常必须设置标题属性,而编辑框等可供用户输入内容的控件是没有标题属性的。

2)如果图3-8所示的控件工具箱不见了,可在VC6主界面的工具栏的空白处上按右键,在右键菜单中勾选“控件”就能显示控件工具箱。

3)单击图3-9属性面板左上角的“图钉”按钮,可以让属性面板总是保持显示。

3.1.2 处理Windows消息

Windows程序是通过系统发送的消息来响应用户的输入的。例如,用户单击按钮、单击鼠标、移动鼠标等程序运行时所产生的动作,都称为事件,应用程序的初始化也是一个事件,每个事件都会产生一个对应的消息,应用程序通过接收消息、分发消息、处理消息来和用户进行交互。

Windows消息可分为标准Windows消息、控件通知和命令消息等类型。其中,命令消息是指用户单击按钮、菜单项、工具栏按钮产生的消息,其名称为WM_COMMAND。除WM_COMMAND外,所有以“WM_”为前缀的消息都是标准Windows消息,例如,WM_INITDIALOG表示对话框及其所有子控件都创建完毕(完成初始化),另外,还能自定义Windows消息,如WM_SOCKET。实际上,每个Windows消息都对应一个常量值。

在Win32 Application中,对消息处理的代码都要写在回调(CALLBACK)函数中,所谓回调函数,是指一个通过函数指针调用的函数。与普通函数不同,回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件触发时由其他的函数调用,用于对该事件或条件进行响应。

例如DialogBox函数的作用是从一个对话框资源中创建一个模态对话框。该函数一直到指定的回调函数通过调用EndDialog函数中止模态对话框才能返回控制,当对话框初始化时或单击对话框上的按钮,都会产生一个Windows消息,在消息发生时,DialogBox函数就会调用指定的回调函数对消息进行处理。DialogBox函数的第4个参数用来指定它的回调函数,例如:

DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, NULL, (DLGPROC)About); 

其中,第4个参数(DLGPROC)About指定了该函数的回调函数是About()。

因此,我们可在名为About的回调函数中对该对话框的消息进行处理,示例代码如下:

LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)            //4个参数为:窗口句柄,消息名,消息参数1,消息参数2

       switch (message) {                     //处理感兴趣的消息

              case WM_INITDIALOG:     //处理对话框初始化消息

                            return TRUE;

              case WM_COMMAND:       //处理按钮消息

                     if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) {

                            EndDialog(hDlg, LOWORD(wParam));      //关闭对话框

                            return TRUE;                }

                     break;     }

return FALSE;       }

可见,一个消息由一个消息名称(UINT)和两个参数(WPARAM,LPARAM)组成。

当用户单击某个按钮就会产生一个WM_COMMAND的通知消息,这条消息的wParam参数的低位字节中含有该控件的ID。因此通过LOWORD(wParam)的值就能判断用户单击的是哪个按钮,wParam参数的高位字节则为通知代码,lParam参数则是指向控件的句柄。

因此要处理用户单击确定按钮(IDC_QD)产生的通知消息,只要对case WM_COMMAND中的代码作如下扩充即可。

case WM_COMMAND:

       if (LOWORD(wParam) == IDOK) {           //处理单击退出按钮的消息

              EndDialog(hDlg, LOWORD(wParam));      //关闭对话框

       }

       if (LOWORD(wParam) == IDC_QD) {           //处理单击确定按钮的消息

                      ……

       }

提示:回调函数的返回值类型用LRESULT CALLBACK定义,LRESULT是一个数据类型,L表示long(长整型),result表示结果,说明这个函数的返回值是某个结果。CALLBACK是一个空宏,只是为了表示这是一个回调函数。

3.1.3 获取和设置控件的内容

图3-11所示程序的功能是,当用户在编辑框1中输入内容,单击“确定”按钮,编辑框2中就会显示“欢迎您,编辑框1的内容”。因此单击确定按钮的程序流程是:①获取用户在编辑框1中输入的文本内容,②将该文本内容进行修改后显示到编辑框2中。

因此程序的关键是获取和设置编辑框的内容。Windows API提供了如下2个函数:

1)GetDlgItemText():获取编辑框中的内容,例如:

GetDlgItemText(hDlg,IDC_EDIT1,user,50);

表示获取对话框hDlg中编辑框IDC_EDIT1中的内容,并将该内容保存到变量user中,最多获取50个字符。

2)SetDlgItemText():设置编辑框中的内容,例如:

SetDlgItemText(hDlg,IDC_EDIT2,wel);

表示将hDlg中编辑框IDC_EDIT2中的内容设置为字符数组变量wel的值。

因此,3.1.2节中处理单击确定按钮消息的代码如下:

char user[50];               //声明字符数组变量用于保存编辑框的内容

char wel[60]="欢迎您:";

if (LOWORD(wParam) == IDC_QD) {              //处理单击确定按钮的消息

       GetDlgItemText(hDlg,IDC_EDIT1,user,50);       //获取IDC_EDIT1中的内容

       strcat(wel,user);                          //连接两个字符串,连接后的值保存在wel中

       SetDlgItemText(hDlg,IDC_EDIT2,wel);            //设置IDC_EDIT2的内容

}

图3-11 本实例效果

3.2 Windows API程序示例

3.2.1 计算器程序

图3-12是一个计算器程序,制作方法是:首先按照3.1.1节新建对话框程序的方法新建该工程,然后绘制对话框界面,该对话框有3个编辑框,1个组合框和2个按钮,各个控件的ID如图3-12所示。

图3-12 计算器程序界面

在绘制组合框时,应注意先单击组合框右侧的下三角箭头,此时虚线框会增大(如图3-13所示),表示下拉列表的显示范围,将下方的控制点往下拖动可保证下拉菜单能完全显示出来。

图3-13 调整组合框的下拉列表范围

1. 实现仅有加法功能的计算器

计算器程序的编程步骤是:当单击计算按钮时,先获取IDC_EDIT1和IDC_EDIT2的内容,分别保存到两个变量中,然后对两个变量值进行相加运算,再把IDC_EDIT3的值设置为计算结果值。代码如下:

if (LOWORD(wParam) == IDC_CALC) {                 //如果单击了计算按钮

int nLeft=GetDlgItemInt(hDlg,IDC_LEFT,NULL,TRUE); //获取IDC_EDIT1的值

int nRight=GetDlgItemInt(hDlg,IDC_RIGHT,NULL,TRUE);     //获取IDC_EDIT2的值

SetDlgItemInt(hDlg,IDC_RESULT,nLeft+nRight,TRUE);          //设置IDC_EDIT3的值

}

本例中,获取和设置控件的内容使用了两个新的Win32 API函数:

GetDlgItemInt()和SetDlgItemInt(),它们能将获取的值自动转换为整型。

2. 扩充计算器的功能

为了实现具有加减乘除功能的计算器,可以在对话框中添加一个组合框IDC_COMBO1,在对话框初始化时对IDC_COMBO1进行初始化,也就是将四个运算符号添加到它的列表项中,为组合框添加列表项的方法如下:

SendMessage(GetDlgItem(hDlg, IDC_COMBO1), CB_ADDSTRING, 0, (LPARAM)fh[i]);

其中,第1个参数表示获取hDlg中的IDC_COMBO1控件,第2个参数表示为组合框添加列表项,第4个参数是要添加的内容。

当用户单击计算按钮时,必须获取用户在组合框中选择的符号,获取组合框的选中项的方法如下:

int cursel=SendDlgItemMessage(hDlg,IDC_COMBO1,CB_GETCURSEL,0,0);

其中,第3个参数CB_GETCURSEL表示获取组合框的选中项,CB_表示ComboBox。

下面是具有加减乘除功能和小数计算功能的计算器的完整代码。

#include "stdafx.h"

#include "resource.h"

#include                        //sprintf函数需要这个头文件

HINSTANCE hInst;                    

LRESULT CALLBACK  About(HWND, UINT, WPARAM, LPARAM);

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,

                     LPSTR lpCmdLine, int nCmdShow){

       DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, NULL, (DLGPROC)About);

       return 0;

}

bool isnum(char *str1) {                     //函数功能:判断输入是否为数字

       int i, flag=1; 

       for(i=0;i

              if ( (str1[i]<'0' || str1[i]>'9')&&str1[i]!='.'&&str1[i]!='.')  //如果str1中有非数字

              {     flag=0;           return FALSE;              } 

       } 

       return TRUE;  //   否则返回TRUE

LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam){

       char * fh[4]={"+","-","*","/"};             //声明字符串数组

       int i;

       switch (message)   {                   //选择处理感兴趣的消息

       case WM_INITDIALOG:            //对话框初始化消息

              for (i=0; i<4; i++)   {  //向组合框中添加运算符

       SendMessage(GetDlgItem(hDlg, IDC_COMBO1), CB_ADDSTRING, 0, (LPARAM)fh[i]);

              }

              return TRUE;        

       case WM_COMMAND:

              if (LOWORD(wParam) == IDOK) {          //单击了退出按钮        

                     EndDialog(hDlg, LOWORD(wParam));

              }

              if (LOWORD(wParam) == IDC_CALC)            //单击了计算按钮

              {

                     TCHAR str1[256],str2[256]; 

                     GetDlgItemText(hDlg,IDC_EDIT1,str1,sizeof(str1));  //得到输入的字符串 

                     GetDlgItemText(hDlg,IDC_EDIT2,str2,sizeof(str2)); 

                     if (isnum(str1) && isnum(str2)) {       //如果获得的字符串是数值                 

                            double i1=atof(str1);             //将字符串转换成浮点数

                            double i2=atof(str2); 

                            double i3;                    //用于保存运算结果

                                   //获取下拉框当前选中项

                     int cursel=SendDlgItemMessage(hDlg,IDC_COMBO1,CB_GETCURSEL,0,0);

                            switch(cursel)      { 

                            case 3:  i3=i1+i2;        break;            //选中项是+号      

                            case 2:  i3=i1/i2;         break; 

                            case 1:  i3=i1*i2;        break; 

                            case 0:  i3=i1-i2;         break; 

                            } 

                            TCHAR str3[256]; 

                            sprintf(str3,"%8.3lf",i3);      //将浮点数i3转换为字符串

                            SetDlgItemText(hDlg,IDC_EDIT3,str3);    //设置IDC_EDIT3的内容

                     } 

                     else  MessageBox(hDlg,"请输入数字","输入非法",MB_OK);          } 

              break;     }

    return FALSE;        }

提示:

程序中sprintf()函数常用于将数值型数据转换为字符串,也可用于多个字符串的连接:

sprintf(sbuff,"%s:%d说:%s", strip,strport,buff);     

而strcat()函数只能用于2个字符串的连接,例如:strcat(strip,buff);。

3.2.2 获取主机名、IP和时间的程序

在WinSock中,用gethostname()函数可获取本机的主机名,而用gethostbyname()函数可以通过主机名获取本机的IP地址。利用这两个函数,将获取的主机名、IP和时间显示在静态文本框上,就能制作出如图3-14所示的程序。

具体步骤是:首先按照3.1.1节新建对话框程序的方法新建一个Win32 Application工程,然后绘制如图3-14对话框界面,添加6个静态文本,左侧3个的标题分别为“主机名”、“IP”和“时间”。右侧3个从上到下设置ID值依次是IDC_HOST、IDC_SHOWIP和IDC_TIME。最后编写如下代码。

图3-14 程序界面

#include "stdafx.h"

#include "resource.h"

#include "winsock2.h"

#include "iostream.h"

#include "time.h"                  //时间函数需要的头文件

#include "stdio.h"

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

 

HINSTANCE hInst;      

LRESULT CALLBACK  About(HWND, UINT, WPARAM, LPARAM);

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,

         LPSTR   lpCmdLine, int       nCmdShow){

      WSADATA wsaData;    

       WSAStartup(MAKEWORD(2,2), &wsaData);            //初始化协议栈

       DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, NULL, (DLGPROC)About);

   return 0;

}

LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam){

       char hostname[100];

       hostent *hst = NULL;

       in_addr inaddr;

    char *pp=NULL;

       time_t tt = time(NULL);               //这句返回的只是一个时间戳

       tm* t= localtime(&tt);                  //返回当地格式的时间,保存在变量t中

       char szTime[20];

       switch (message)   {

       case WM_INITDIALOG:

              gethostname(hostname,sizeof(hostname)); //获取主机名,保存在变量hostname中

              hst =gethostbyname(hostname);   //获取IP地址,保存在变量hst中

              memcpy((char*)(&inaddr),hst->h_addr_list[0],hst->h_length);

              pp=inet_ntoa(inaddr);                   //将IP地址格式转换为本机字节

              sprintf(szTime,"%d-%02d-%02d %02d:%02d:%02d", t->tm_year + 1900,t->tm_mon + 1,t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);               //设置时间格式

              SetDlgItemText(hDlg,IDC_HOST, hostname);            //显示主机名

              SetDlgItemText(hDlg,IDC_SHOWIP, pp);                  //显示IP

              SetDlgItemText(hDlg,IDC_TIME, szTime);               //显示时间

              return TRUE;

              case WM_COMMAND:

                     if (LOWORD(wParam) == IDOK)       {            //退出按钮

                            EndDialog(hDlg, LOWORD(wParam));      //关闭对话框

                            return TRUE;                }

                     break;

       }

return FALSE;              }

总结:要获取本机IP地址,第一步是使用gethostname()函数获取本机的名称,第二步是使用gethostbyname()函数,并将本机名称作为参数,该函数返回值是一个hostent结构体的指针,IP地址存放在该结构体中的数组h_addr_list的第0个数组元素中,因此第三步是使用memcpy()函数将第0个数组元素取出并复制到inaddr变量中,第四步是使用inet_ntoa()将IP地址由in_addr无符号长整型转换成点分十进制的表示形式。

3.3 Win32 API版本的TCP通信程序

在项目2.2中,已经实现了一个能互相发送消息的TCP通信程序,但由于是控制台程序,不仅界面不够友好,还存在客户端和服务器端每次只能发送一条消息给对方的不足。本节我们将控制台程序转换成Windows对话框界面的程序。读者主要掌握控制台程序转换成Windows对话框程序的方法。该程序服务器端和客户端的运行界面如图3-15所示。

 

图3-15 Win32 API版本的TCP通信程序(左图为服务器端,右图为客户端)

3.3.1 控制台程序改造成Windows程序的方法

在图3-15的服务器端程序中,主要涉及如下4个Windows消息:

① 窗口初始化消息(WM_INITDIALOG);

② “发送数据”按钮消息(WM_COMMAND中的IDC_SEND);

③ “接收数据”按钮消息(WM_COMMAND中的IDC_RECV);

④ “退出”按钮消息(WM_COMMAND中的IDC_QUIT)。

如果要转换成Windows程序,控制台版TCP通信程序中各个函数对应的语句应分别放到上述4个消息的处理函数中。如图3-16所示。

 

socket()

bind()

accept()

获取编辑框中的内容

send()

 

listen()

WM_INITDIALOG

IDC_SEND

清空编辑框中的内容

将已发送的消息添加到列表框中

recv()

 

IDC_RECV

将接收到的消息添加到列表框中

WSAStartup()

设置编辑框初始显示的内容

closesocket ()

 

IDC_QUIT

EndDialog()

WSACleanup() ()

 

 

图3-16 TCP通信程序的WinSock函数调用流程

另外,控制台程序中只有一个main函数,而改造成Windows界面程序后,存在多个函数,这些函数都需要使用Socket及SOCKADDR_IN等套接字和地址变量,因此应该将套接字和套接字地址的声明写在所有函数之外,使它们成为公共变量,让所有的函数都可使用。

一般来说,编写Windows对话框程序的方法如下:

① 将公共变量的声明(包括声明套接字和套接字地址)写在所有函数之外,使所有函数都可以使用这些公共变量。

② 将初始化WinSock协议栈的代码写到WM_INITDIALOG初始化事件中。

③ 将创建套接字、以及监听、接受连接的代码写到“创建服务器”的按钮消息中。

④ 将获取用户消息、发送消息的代码写在“发送”按钮的消息处理函数中。

⑤ 将接收消息、并显示消息的代码写在“接收”按钮的消息处理函数中。

⑥ 将关闭套接字的代码写到“退出”按钮或WM_QUIT的消息处理函数中。

3.3.2 服务器端程序制作步骤

1)新建工程,选择“Win32 Application”,输入工程名(如WinApi_Server),然后选择“一个典型的Hello World程序”。

2)在左侧选择FileView选项卡,找到对应的源文件(如WinApi_Server.cpp),将WinMain函数中DialogBox一行(148行)和return 0; 保留,其他代码全部删除,再将DialogBox中第3个参数HWnd改为NULL。

3)切换到ResourceView选项卡,找到Dialog下的“IDD_ABOUTBOX”,将对话框的界面改为如图3-17所示,并设置各个控件的ID值。

图3-17 服务器端程序界面设计和控件ID值

提示:对于ListBox列表框控件,在其属性面板“样式”选项卡中,“分类”应取消勾选,可保证添加的消息从上到下按顺序显示。

4)打开WinApi_Server.cpp文件,编写如下代码:

#include "stdafx.h"

#include "resource.h"

#include

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

HINSTANCE hInst;      

LRESULT CALLBACK  About(HWND, UINT, WPARAM, LPARAM);

 

SOCKET sockSer,sockConn;                     //服务器端要创建两个套接字

SOCKADDR_IN addrSer, addrCli;

int iIndex=0;   int len =sizeof(SOCKADDR);

char sendbuf[256],recvbuf[256];

char clibuf[999]="客户端: >",serbuf[999]="服务器: >";

 

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,

LPSTR lpCmdLine, int nCmdShow){  

       DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, NULL, (DLGPROC)About);

       return 0; 

}

LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam,

LPARAM lParam){

       char ip[16],port[5];      

       switch (message)   {

       case WM_INITDIALOG:                    //对话框初始化

              SetDlgItemText(hDlg,IDC_IP,"127.0.0.1");  //设置ip文本框的内容

              SetDlgItemText(hDlg,IDC_PORT,"5566");   //设置端口文本框的内容

              WSADATA wsaData;

              if(WSAStartup(MAKEWORD(2,2), &wsaData)) {

                     MessageBox(hDlg,"Winsock加载失败","警告",0);

                     WSACleanup();

                     return -1;        }

              sockSer=socket(AF_INET,SOCK_STREAM,0);        

              return TRUE;        

       case WM_COMMAND:{

                     switch(LOWORD(wParam) )

                     { case IDC_QUIT:        //单击了退出按钮

                     EndDialog(hDlg, LOWORD(wParam));

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

                     WSACleanup();             //卸载WinSock协议栈

                     return TRUE;

                    

                     case IDC_CREATE:              //单击了创建服务器按钮

                            GetDlgItemText(hDlg,IDC_IP,ip,16);    //获取编辑框中的IP值

                            GetDlgItemText(hDlg,IDC_PORT,port,5);

                            EnableWindow(GetDlgItem(hDlg,IDC_CREATE),FALSE); //禁用按钮

                            addrSer.sin_family=AF_INET;

                            addrSer.sin_port=htons(atoi(port));

                            addrSer.sin_addr.S_un.S_addr=inet_addr(ip);

                            bind(sockSer,(SOCKADDR*) &addrSer,len);     //绑定套接字

                            listen(sockSer,5);                 //监听

                            sockConn=accept(sockSer,(SOCKADDR*) &addrCli,&len);//接受连接

                            if(sockConn!=INVALID_SOCKET)

                                   SendDlgItemMessage(hDlg,IDC_RECVBUF,LB_ADDSTRING,0,

                            (LPARAM)"客户端已经连接上");

                            break;

                     case IDC_SEND:                        //单击了发送按钮

                            GetDlgItemText(hDlg,IDC_SENDBUF,sendbuf,256);

                            send(sockConn,sendbuf,strlen(sendbuf)+1,0);     //发送消息

                            SetDlgItemText(hDlg,IDC_SENDBUF,"");          //清空编辑框

                            strcat(serbuf,sendbuf);

                                   //将已发送的消息添加到列表框中

              SendDlgItemMessage(hDlg,IDC_RECVBUF,LB_ADDSTRING,0,(LPARAM)serbuf);

                     strcpy(serbuf, "服务器: >");

                            break;

                           

                     case IDC_RECV:                  //单击了接收按钮

                            recv(sockConn,recvbuf,256,0);

                            strcat(clibuf,recvbuf);

                     //将接收到的消息添加到列表框中

       SendDlgItemMessage(hDlg,IDC_RECVBUF,LB_ADDSTRING,0,(LPARAM)clibuf);

              strcpy(clibuf, "客户端: >");                       

                            break;

                     }            }            }    

       return FALSE;        }

提示:向列表框中添加内容,需要使用的函数如下:

SendDlgItemMessage(hDlg,IDC_RECVBUF,LB_ADDSTRING,0,(LPARAM)clibuf);

其中,第1个参数是对话框的句柄,第2个参数是列表框的ID,第3个参数是控件消息名,其中LB_表示Listbox,ADDSTRING表示添加字符串,第5个参数是要添加的内容。

LB_SETTOPINDEX的作用是,当列表框中的内容很多显示不下时,该消息能让列表框自动滚动使当前项能显示出来。

3.3.3 客户端程序制作步骤

1)新建工程,选择“Win32 Application”,输入工程名(如Win_client),然后选择“一个典型的Hello World程序”。

2)在左侧选择FileView选项卡,找到对应的源文件(如Win_client.cpp),将WinMain函数中DialogBox一行和return 0; 保留,其他代码全部删除,再将DialogBox中第3个参数HWnd改为NULL。

3)切换到ResourceView选项卡,找到Dialog下的“IDD_ABOUTBOX”,将对话框的界面改为如图3-18所示,并设置各个控件的ID值。

图3-18 客户端程序界面设计和控件ID值

4)打开Win_client.cpp文件,编写代码如下:

#include "stdafx.h"

#include "resource.h"

#include

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

HINSTANCE hInst;

LRESULT CALLBACK  About(HWND, UINT, WPARAM, LPARAM);

 

SOCKET sockCli;                //声明客户端套接字

SOCKADDR_IN addrSer, addrCli;

 

int APIENTRY WinMain(HINSTANCE hInstance,   HINSTANCE hPrevInstance,  

LPSTR  lpCmdLine, int  nCmdShow){    

       DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, NULL, (DLGPROC)About);

       return 0;

}

LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam,

LPARAM lParam){

       char ip[16], port[5];

       char sendbuf[256],recvbuf[256]; 

       char serbuf[256]="服务器: >", clibuf[256]="客户端: >";

       int len =sizeof(SOCKADDR);

 

       switch (message){

       case WM_INITDIALOG:                          //对话框初始化

              SetDlgItemText(hDlg,IDC_IP,"127.0.0.1");

              SetDlgItemText(hDlg,IDC_PORT,"5566");

              WSADATA wsaData;

              WSAStartup(MAKEWORD(2,2), &wsaData);            //加载协议栈

              sockCli=socket(AF_INET,SOCK_STREAM,0);          //创建套接字

              break;

             

       case WM_COMMAND:

              {     switch(LOWORD(wParam)) {

       case IDC_QUIT:                                //单击了退出按钮

              EndDialog(hDlg, LOWORD(wParam));

              closesocket(sockCli);

              WSACleanup();

              break;

             

       case IDC_CONN:                              //单击了“连接服务器”按钮

              GetDlgItemText(hDlg,IDC_IP,ip,16);           //获取ip

              GetDlgItemText(hDlg,IDC_PORT,port,5);

              EnableWindow(GetDlgItem(hDlg,IDC_CONN),FALSE);    //禁用连接按钮

              addrSer.sin_family=AF_INET;

              addrSer.sin_port=htons(atoi(port));

              addrSer.sin_addr.S_un.S_addr=inet_addr(ip);            

              if(connect(sockCli,(SOCKADDR*)&addrSer,sizeof(SOCKADDR)))

                     MessageBox(NULL,"客户端连接服务器失败","警告",0);                      

              else 

                     MessageBox(NULL,"客户端连接服务器成功","通知",0);        

              break;

             

       case IDC_SEND:          //单击了“发送”按钮

              GetDlgItemText(hDlg,IDC_SENDBUF,sendbuf,256);         //获得发送框的内容

              send(sockCli,sendbuf,strlen(sendbuf)+1,0);        //发送消息

              SetDlgItemText(hDlg,IDC_SENDBUF,"");          //清空发送框

              strcat(clibuf,sendbuf);                       

              SendDlgItemMessage(hDlg,IDC_RECVBUF,LB_ADDSTRING,0,(LPARAM)clibuf);

              strcpy(clibuf, "客户端: >");  //重新给字符串赋值   

              break;

 

       case IDC_RECV:                 //单击了“接收”按钮

              recv(sockCli,recvbuf,256,0);               //接收消息

              strcat(serbuf,recvbuf);

              SendDlgItemMessage(hDlg,IDC_RECVBUF,LB_ADDSTRING,0,(LPARAM)serbuf);

              strcpy(serbuf, "服务器: >");  //重新给字符串赋值

              break;            }

              }            }    

return FALSE;              }

习题

1. 每个按钮的ID属性存放在WM_COMMAND消息的        中。       (           )

A. wParam的高位字节中                         B. wParam的低位字节中

C. lParam的高位字节中                           D. lParam的低位字节中

2.下列哪个函数可以返回本机的IP地址:                                (             )

A. gethostbyname()                            B. gethostname()

C. gethostbyaddr()                             D. gethostaddr()

3. WM_COMMAND表示                         ,WM_INITDIALOG表示                       

4. 要设置编辑框显示的内容,应使用                                  函数。

5. 要获取编辑框显示的内容,应使用                                  函数。

6. 要向列表框中添加一行文本,应使用                       函数。

7. 在Windows API程序中,对话框是通过                           函数创建的。

8. 在VC6中,怎样保证向列表框Listbox中添加消息时,消息总是从上到下按时间顺序显示?

9. (实验)为3.3节的程序添加显示消息发送时间的功能。

你可能感兴趣的:(糖儿飞教你学C++ Socket网络编程——7. Win32 API网络通信程序)