网络编程实用教程(第三版)段利国主编 复习材料

我用夸克网盘分享了「网络编程复习.zip」辛苦整理不易,可以的话给个打赏~
链接:https://pan.quark.cn/s/40c80cb88c47

目录

预备知识

第一章

1.1 网络编程与进程通信

1.1.1 进程与线程的基本概念

1.1.2 因特网中网间进程的标识

1.1.3 网络协议的特征

1.1.4 高效的用户数据报协议UDP

1.1.5 可靠的传输控制协议TCP

1.2 三类网络编程

1.2.1 基于TCP/IP协议栈的网络编程

1.2.2 基于WWW应用的网络编程

1.2.3 基于.NET框架的Web Services网络编程

1.3 客户/服务器交互模式

1.3.1 网络应用软件的地位和功能

1.3.2 客户/服务器模式

1.3.3 客户与服务器的特性

1.3.4 容易混淆的术语

1.3.5 客户与服务器的通信过程

1.3.6 网络协议与C/S模式的关系

1.3.7 错综复杂的客户/服务器交互

1.3.8 服务器如何同时为多个客户服务

1.3.9 标识一个特定服务

第二章

2.1UNX套接字网络编程接口的产生与发展

2.1.1 问题的提出

2.1.2 套接字编程接口的起源与应用

2.1.3 套接字编程接口的两种实现方式

2.1.4 套接字通信与UNIX操作系统的输入/输出

2.2 套接字编程的基本概念

2.2.1 什么是套接字(SOCKET)

2.2.2 套接字的特点

2.2.3 套接字的应用场合

2.2.4 套接字使用的数据类型和相关的问题

1、 3种表示套接字地址的结构

INADDR ANY

2、本机字节顺序和网络字节顺序

3、点分十进制的P地址的转换

4、域名服务

gethostbynamel函数用法

IP 地址转换函数

主机名IP地址函数

2.3 面向连接的套接字编程

2.3.1 套接字的工作过程

2.3.2 UNIX套接字编程接口的系统调用

TCP连接队列和 SOMAXCONN

阻塞式Socket的send函数执行流程

同步Socket的recv函数执行流程

2.3.3 面向连接的套接字编程实例

进程因调用recv()而被阻塞

服务器进程因调用ACCEPT()而被阻塞

代码差错处理

GetLastError()函数的使用

2.3.4 进程的阻塞问题和对策

解除阻塞的方法

select()--多路同步 I/O

使用 struct timeval 和操作 fd_set 集合

2.4 无连接的套接字编程

2.4.1 无连接的套接字编程的两种模式

2.4.2 两个专用的系统调用

2.4.3 数据报套接字的对等模式编程实例

2.5 原始套接字

第三章

3.1 Windows Sockets规范

3.1.1 概述

3.1.2 Windows Sockets规范

阻塞处理例程(阻塞钩子函数) BlockingHook()

3.1.3 WinSock规范与Berkeley套接口的区别

near指针和far指针的区别

3.2 Winsock库函数

3.2.1 Winsock的注册与注销

3.2.2 Winsock的错误处理函数

3.2.3 主要的Winsock函数

3.2.4 Winsock的辅助函数

3.2.5 Winsock的信息查询函数

3.2.6 WSAAsyncGetXByY类型的扩展函数

3.3 Windows环境下的多路异步选择I/O

第四章

4.1 MFC概述

4.1.1 MFC是一个编程框架

4.1.2 典型的MDI应用程序的构成

4.2 MFC和Win32

4.2.1 MFC对象和Windows对象的关系

4.2.2 几个主要的类

4.4 Windows系统的消息机制

4.5 MFC对象的创建

4.6 应用程序的退出

第五章

5.1 CasyncSocket类

5.1.1 使用CAsyncSocket类的一般步骤

5.1.2 创建CAsyncSocket类对象

WM_SOCKET_NOTIFY

5.1.3 关于CAsyncSocket类可以接受并处理的消息事件

5.1.4 客户端套接字对象请求连接到服务器端套接字对象

5.1.5 服务器接受客户端的连接请求

5.1.6 发送与接收流式数据

5.1.7 关闭套接字

5.1.8 错误处理

5.1.9 其它的成员函数

5.2 CSocket类

5.2.1 创建 CSocket 对象

5.2.2 建立连接

5.2.3 发送和接收数据

5.2.4 CSocket类与CArchive类和CSocketFile类

CSocketFile

CArchive 类

5.2.5 关闭套接字和清除相关的对象

5.3 CSocket 类的编程模型

第六章

6.1 WinInet API 的导入

6.1.1 WinINet API 函数使用的 HINTERNET 句柄

6.1.2 典型的操作流程和它们使用的句柄

6.1.3 获取 WinInet API 函数执行的错误信息

6.1.5 WinInet API 的异步操作模式

6.1.6 回调函数的定义实现与注册

6.3 MFC WinInet

6.3.1 概述

MFC WinInet 类的关系

6.3.2 MFC WinInet所包含的类

查询或设置Internet请求选项

CInternetConnection类成员

CFtpConnection

一般使用MFC WinInet的流程:

Internet应用的数据交换流程:


预备知识

现代软件系统的分布式基础设施通常涉及分布式文件系统、分布式协作系统、分布式数据库系统等支撑系统。分布式基础设施均依赖于计算机网络通信从应用的角度看,计算机网络通信的实现依赖于两个因素:网络通信协议:网络消息传输的基础操作系统网络服务接口(即网络接口):用户利用操作系统提供的API接口实现对网络服务的使用

image-20231212133124098

网络接口层的功能

功能实现层调用网络协议栈的服务实现网络通信的“接口”。

网络通信层需要解决的问题包括:通过什么样的“接口”?如何使用这些“接口”?

本课程的重点(学习如何高效的使用网络接口层)

“接口”是什么?有哪些常用类型?如何定义这些“接口”?

有哪些常用的“接口”使用方式?实际应用中如何选择?如何高效的使用这些接口

系统的各种延迟

系统的各种延迟

第一章

1.1 网络编程与进程通信

1.1.1 进程与线程的基本概念

1.进程是处于运行过程中的程序实例,是操作系统调度分配资源的基本单位。

一个进程实体由程序代码、数据和进程控制块三部分构成。

各种计算机应用程序在运行时,都以进程的形式存在。网络应用程序也不例外。

每个进程都有独立的代码和数据空间(进程上下文),进程切换的开销大。

线程:进程中程序代码的一个执行序列。同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。

操作系统通常不但支持多进程,还支持多线程。

当创建一个进程时,系统会自动创建它的第一个线程,称为主线程。然后,该线程可以创建其他的线程,而这些线程又能创建更多的线程。

2. 网络应用进程在网络体系结构中的位置

从计算机网络体系结构的角度来看,网络应用进程处于网络层次结构的最上层。

从功能上,可以将网络应用程序分为两部分:

一部分是专门负责网络通信的模块,它们与网络协议栈相连接,借助网络协议栈提供的服务完成网络上数据信息的交换。

另一部分是面向用户或者作其他处理的模块,它们接收用户的命令,或者对借助网络传输过来的数据进行加工,这两部分模块相互配合,来实现网络应用程序的功能。

网络应用程序最终要实现网络资源的共享,共享的基础就是必须能够通过网络轻松地传递各种信息。网络编程首先要解决网间进程通信的问题。然后才能在通信的基础上开发各种应用功能。

3. 实现网间进程通信必须解决的问题

网间进程通信是指网络中不同主机中的应用进程之间的相互通信问题,网间进程通信必须解决以下问题:

(1)网间进程的标识问题(2)如何调用网络协议栈服务(3)多重应用协议的识别问题(4)不同的通信服务质量的问题

1.1.2 因特网中网间进程的标识

1.传输层在网络通信中的地位

按照OSI七层协议的描述,传输层与网络层在功能上的最大区别是传输层提供进程通信的能力。TCP/IP协议提出了传输层协议端口(protocol port,简称端口)的概念,成功地解决了通信进程的标识问题。

传输层是计算机网络中,通信主机内部进行独立操作的第一层,是支持端到端的进程通信的关键的一层。

2.端口的概念

端口是TCP/IP协议族中,应用层进程与传输层协议实体间的通信接口

类似于进程ID号,每个端口都拥有一个叫作端口号(port number)的整数型标识符,端口号唯一标识了本机网络协议栈上的一个通信接口。

从实现的角度讲,端口是一种抽象的软件机制,包括一些数据结构和I/O缓冲区。

3.端口号的分配机制

TCP/IP协议采用了全局分配(静态分配)和本地分配(动态分配)相结合的分配方法。对于TCP,或者UDP,将它们的全部65535个端口号分为保留端口号和自由端口号两部分。

保留端口的范围是0—1023,又称为众所周知的端口或熟知端口(well-known port),只占少数,采用全局分配或集中控制的方式,由一个公认的中央机构根据需要进行统一分配,静态地分配给因特网上著名的众所周知的服务器进程,并将结果公布于众。

其余的端口号,1024-65535,称为自由端口号,采用本地分配,又称为动态分配的方法。

总之,TCP或UDP端口的分配规则是:

端口0:不使用,或者作为特殊的使用;

端口1-255:保留给特定的服务,TCP和UDP均规定,小于256的端口号才能分配给网上著名的服务;

端口256-1023:保留给其他的服务,如路由;

端口1024-4999:可以用作任意客户的端口;

端口5000-65535:可以用作用户的服务器端口。

4.进程的网络地址的概念

在因特网络中,用一个三元组可以在全局中唯一地标识一个应用层进程:

应用层进程标识=(传输层协议,主机的IP地址,传输层的端口号)

这样一个三元组,叫做一个半相关(half-association),它标识了因特网中,进程间通信的一个端点,也把它称为进程的网络地址。

5.网络中进程通信的标识

一个完整的网间通信需要一个五元组在全局中唯一地来标识:

(传输层协议,本地机IP地址,本地机传输层端口,远地机IP地址,远地机 传输层端口)

这个五元组称为一个全相关(association)。即两个协议相同的半相关才能组合成一个合适的全相关,或完全指定一对网间通信的进程。

1.1.3 网络协议的特征

1.面向消息的协议与基于流的协议

(1)面向消息的协议:面向消息的协议以消息为单位在网上传送数据,在发送端,消息一条一条地发送,在接收端,也只能一条一条地接收,每一条消息是独立的,消息之间存在着边界。

(2)基于流的协议:基于流的协议不保护消息边界,将数据当作字节流连续地传输,不管实际消息边界是否存在。

2.面向连接的服务和无连接的服务

一个协议可以提供面向连接的服务,或者提供无连接的服务。

面向连接服务是电话系统服务模式的抽象,即每一次完整的数据传输都要经过建立连接,使用连接,终止连接的过程。

无连接服务是邮政系统服务的抽象,每个分组都携带完整的目的地址,各分组在系统中独立传送。

3.可靠性和次序性

次序性是指对数据到达接收端的顺序进行处理。保护次序性的协议保证接收端收到数据的顺序就是数据的发送顺序,称为按序递交

可靠性保证了发送端发出的每个字节都能到达既定的接收端,不出错,不丢失,不重复,保证数据的完整性,称为保证投递

次序性的保障机制是实现可靠性的前提条件。

1.1.4 高效的用户数据报协议UDP

传输层的用户数据报协议(User Datagram Protocol,UDP)是一种尽力传送的无连接的不保障可靠的传输服务,是一种保护消息边界的数据的传输。

1.1.5 可靠的传输控制协议TCP

1.可靠性是很多应用的基础

2.TCP为应用层提供的服务

传输控制协议 (Transmission Control Protocol,TCP)应用层进程提供一个面向连接的、端到端的、完全可靠的(无差错、无丢失、无重复或失序)全双工的流传输服务。

3.TCP利用IP数据报实现了端对端的传输服务

TCP被称作一种端对端(end to end)协议,这是因为它提供一个直接从一台计算机上的应用进程到另一远程计算机上的应用进程的连接。

应用进程能请求TCP构造一个连接,通过这个连接发送和接收数据,以及关闭连接。

由TCP提供的连接叫做虚连接(virtual connection),虚连接是由软件实现的。

image-20231212134907511

4.三次握手

为确保连接的建立和终止都是可靠的,TCP使用三次握手(3-way handshake)的方式来建立连接

image-20231212134954351

image-20231212135056353

至此,整个连接已经全部释放。

从 A 到 B 的连接就释放了,连接处于半关闭状态。相当于 A 向 B 说:“我已经没有数据要发送了。但你如果还发送数据,我仍接收。”

1.2 三类网络编程

1.2.1 基于TCP/IP协议栈的网络编程

基于TCP/IP协议栈的网络编程是最基本的网络编程方式,主要是使用各种编程语言,利用操作系统提供的套接字网络编程接口,直接开发各种网络应用程序。本书主要讲解这种网络编程的相关技术。

1.2.2 基于WWW应用的网络编程

WWW应用(Web应用)是因特网上最广泛的应用。基于WWW应用的网络编程技术,包括所见即所得的网页制作工具,和动态服务器页面的制作技术。

1.2.3 基于.NET框架的Web Services网络编程

1.关于.NET平台

 Web服务从由简单网页构成的静态服务网站,发展到可以交互执行一些复杂步骤的动态服务网站,这些服务可能需要一个Web服务调用其他的Web服务,并且像一个传统软件程序那样执行命令。这就需要和其他服务整合,例如:**需要多个服务能够一起无缝地协同工作,需要能够创建出与设备无关的应用程序,需要能够容易地协调网络上的各个服务的操作步骤,容易地创建新的用户化的服务。**        

微软公司推出的.NET系统技术正是为了满足这种需求。微软公司在2000年7月公布了.NET平台开发框架,.NET将Internet本身作为构建新一代操作系统的基础,并对Internet和操作系统的设计思想进行了延伸,使开发人员能够创建出与设备无关的应用程序,容易地实现Internet连接。

.NET开发平台是一组用于建立Web服务器应用程序和Windows桌面应用程序的软件组件(综合类库),用该平台创建的应用程序在Common Language Runtime(CLR)(通用语言运行环境)(底层)的控制下运行。

综合类库提供了使应用程序可以读写XML数据、在Internet上通信、访问数据库等的代码。所有的类库都建立在一个基础的类库之上,它提供管理使用最为频繁的数据类型(例如数值或文本字符串)的功能,以及诸如文件输入/输出等底层功能。

CLR是一个软件引擎,用来加载应用程序,确认它们可以没有错误地执行,进行相应的安全许可验证,执行应用程序,然后在运行完成后将它们清除。

2.关于Web服务

什么是Web服务?Web服务是松散耦合的可复用的软件模块,在Internet上发布后,能通过标准的Internet 协议在程序中访问,具有以下的特点:

(1)可复用 (2)松散耦合 (3)封装了离散(4)Web服务可以在程序中访问(5)Web服务在Internet上发布

Web 服务是一种可以用来解决跨网络应用集成问题的开发模式,这种模式为实现“软件作为服务”提供了技术保障。

“软件作为服务”实质上是一种提供软件服务的机制,这种机制可以在网络上暴露可编程接口,并通过这些接口来共享站点开放出来的功能。

从技术角度来讲,Web 服务实现了最广泛的应用软件集成,弥补了传统分布式软件开发模型的限制。

传统的分布式软件开发模型各自为政,所以只能用来开发紧耦合类型的分布式应用系统。所谓紧耦合,就是指客户端必须按照特定的规范去访问服务端提供的服务,而这种规范只在一个有限的范围内通用。

为了可以在整个因特网中实现对服务的自由访问,有必要提供一种崭新的模式或信息交换手段来达到这个目的。于是,微软提出了Web 服务。

Web 服务的主要特点之一是,客户端访问Web 服务只需要通过因特网标准协议,如HTTP或XML,以及SOAP,不需要专门的协议。因为HTTP协议和XML都是与平台无关的标准协议,因此,可以被任何主流操作系统正确理解和解释。

另外,更为关键的特性是,Web 服务可以被XML语言进行详尽的描述。这就是说,提供Web服务的站点可以提供一个(或多个)该站点可以对外提供服务的描述文件,这个文件的内容可以被访问者理解。更进一步说,就是客户端可以从网络上直接得到代码!

假设开发人员需要搭建一个商务网站,这个网站需要一个验证客户合法身份的功能。为了实现这个功能,下面分别描述了可以采用的办法。

● 由开发人员自己编写安全验证所需的全部代码。这样做显然不现实,一个安全验证程序涉及到诸多专业知识,并需要相当长的时间才能够完成。

● 购买这段程序(通常是一个ActiveX组件)。在收到组件之后,首先将组件注册在自己的机器上,然后根据组件类型库产生接口文件。在实际编程中就可以使用这个接口文件来访问组件服务。很明显,这种方式在目前使用得最为广泛。

● 有了Web 服务,情况就不同了,只需要在自己的程序中通过访问某个服务的URL地址,得到一份XML描述,并使用这个描述文件产生一个接口文件。然后,在实际编程中,只需要通过这个接口文件来访问服务就可以了。一定要注意,这个服务可不是运行在我们机器上的,是运行在因特网上URL地址所指向的地方。

如果这个网站需要更多的功能,而这些功能在一些网站上已经被开发出来,并以各种方式(免费或收费)公开出来供所有需要它们的开发人员来使用,那么,尽量使用它们好了。当然,如果开发人员所在的公司,也想成为Web 服务提供者的话,同样可以轻松地将他们编写的Web 服务在网络上公布出来,供大家使用。

与紧耦合服务概念相对,由于Web 服务具备通信协议标准性和服务自描述性,所以,使用Web 服务可以开发出松耦合的分布式应用程序来。这也是Web 服务要实现的最根本的设计目标。Web 服务的体系如图所示。

image-20231212135629071

1.3 客户/服务器交互模式

1.3.1 网络应用软件的地位和功能

Internet仅仅提供一个通用的通信构架,它只负责传送信息,而对于信息传过去干什么用,利用因特网究竟提供什么服务,由哪些计算机来运行这些服务,如何确定服务的存在,如何使用这些服务等等问题,都要由应用软件和用户解决。

1.3.2 客户/服务器模式

网络应用进程通信时,普遍采用客户/服务器交互模式(client-server paradigm of interaction),简称C/S模式。这是因特网上应用程序最常用的通信模式,即客户向服务器发出服务请求,服务器接收到请求后,提供相应的服务。

C/S模式的建立基于以下两点:

网络中软硬件资源、运算能力和信息分布的不对等; 网间进程通信完全是异步的,需要一种机制为通信的进程之间建立联系,为二者的数据交换提供同步。

C/S模式过程中服务器处于被动服务的地位。首先服务器方要先启动,并根据客户请求提供相应服务,服务器的工作过程是:

(1)打开一通信通道,并告知服务器所在的主机,它愿意在某一公认的地址上(熟知知端口,如FTP为21)接收客户请求。(2)等待客户的请求到达该端口。

(3)服务器接收到服务请求,处理该请求并发送应答信号。为了能并发地接收多个客户的服务请求,要激活一个新进程或新线程来处理这个客户请求(如UNIX系统中用fork、exec)。服务完成后,关闭此新进程与客户的通信链路,并终止。

(4)返回第二步,等待并处理另一客户请求。(5)在特定的情况下,关闭服务器。

客户方采取的是主动请求方式,其工作过程是:

(1)打开一通信通道,并连接到服务器所在主机的特定监听端口。(2)向服务器发送请求报文,等待并接收应答;继续提出请求,与服务器的会话按照应用协议进行。(3)请求结束后,关闭通信通道并终止。

从上面的描述可知:

客户机和服务器都是运行于计算机中的网络协议栈之上的应用进程,借助网络协议栈进行通信。

服务器运行于高档的服务器类计算机之上,借助网络,可以为成千上万的客户机服务。

客户机软件运行在用户的PC上,有良好的人机界面,通过网络请求并得到服务器的服务,共享网络信息和资源。

1.3.3 客户与服务器的特性

客户软件和服务器软件通常还具有以下一些主要特点:

1.客户软件

(1)在进行网络通信时临时成为客户,但它也可在本地进行其他的计算。

(2)被用户调用,只为一个会话运行。在打算通信时主动向远地服务器发起通信。

(3)能访问所需的多种服务,但在某一时刻只能与一个远程服务器进行主动通信。

(4)主动地启动与服务器的通信。

(5)在用户的计算机上运行,不需要特殊的硬件和很复杂的操作系统。

2.服务器软件

(1)是一种专门用来提供某种服务的程序,可同时处理多个远地客户的请求。

(2)当系统启动时即自动调用,并且连续运行着,不断地为多个会话服务。

(3)接受来自任何客户的通信请求,但只提供一种服务。

(4)被动地等待并接受来自多个远端客户的通信请求。

(5)在共享计算机上运行,一般需要强大的硬件和高级的操作系统支持。

3.基于因特网的C/S模式的应用程序的特点

(1)客户和服务器都是软件进程,C/S模式是网络上通过进程通信建立分布式应用的常用模型。

(2)非对称性:服务器通过网络提供服务,客户通过网络使用服务,这种不对称性体现在软件结构和工作过程上。

(3)对等性:客户和服务器必有一套共识的约定,必与以某种应用层协议相联,并且协议必须在通信的两端实现。比如浏览器和3W服务器就都基于HTTP超文本传输协议。

(4)服务器的被动性:服务器必须先行启动,时刻监听,日夜值守,及时服务,只要有客户请求,就立即处理并响应,回传信息。但决不主动提供服务。

(5)客户机的主动性:客户机可以随时提出请求,通过网络得到服务,也可以关机走人,一次请求与服务的过程是由客户机首先激发的。

(6)一对多:一个服务器可以为多个客户机服务,客户机也可以打开多个窗口,连接多个服务器。

(7)分布性与共享性:资源在服务器端组织与存储,通过网络分散在多个客户端使用。

C/S模式优缺点

优点:

结构简单,系统中不同类型的任务分别由客户和服务器承担,有利于发挥不同机器平台的优势;

支持分布式、并发环境,可以有效地提高资源的利用率和共享程度;

服务器集中管理资源,有利于权限控制和系统安全。

由于客户端实现与服务器的直接相连,没有中间环节,因此响应速度快。

操作界面漂亮、形式多样,可以充分满足客户自身的个性化要求。

缺点:

需要专门的客户端安装程序,分布功能弱,针对点多面广且不具备网络条件的用户群体,不能够实现快速部署安装和配置。

兼容性差,对于不同的开发工具,具有较大的局限性。若采用不同工具,需要重新改写程序。

开发成本较高,需要具有一定专业水准的技术人员才能完成。

1.3.4 容易混淆的术语

1.服务器程序与服务器类计算机

服务器(server)这个术语来指那些运行着的服务程序。

服务器类计算机(server-class computer)这一术语来称呼那些运行服务器软件的强大的计算机。

2.客户与用户

“客户”(client)和服务器都指的是应用进程,即计算机软件。

“用户”(user)指的是使用计算机的人。

1.3.5 客户与服务器的通信过程

客户与服务器的通信过程一般是这样的:

(1)通信之前,服务器应先行启动,并通知它的下层协议栈做好接收客户请求的准备,然后被动地等待客户的通信请求,称服务器处于监听状态。

(2)一般是先由客户向服务器发送请求,服务器向客户返回应答。客户随时可以主动启动通信,向服务器发出连接请求,服务器接收这个请求,建立了二者的通信关系。

(3)客户与服务器的通信关系一旦建立,客户和服务器都可发送和接收信息。信息在客户与服务器之间可以沿任一方向或两个方向传递。在某些情况下,客户向服务器发送一系列请求,服务器相应地返回一系列应答。

1.3.6 网络协议与C/S模式的关系

客户与服务器作为两个软件实体,它们之间的通信是虚拟的,是概念上的,实际的通信要借助下层的网络协议栈来进行。

网络应用进程与应用层协议的关系:

为了具体的应用问题而彼此通信的进程称为应用进程

应用层协议并不解决用户的各种具体问题,而是规定了应用进程在通信时所必须遵循的约定

从网络体系结构的角度来说,应用层协议位于应用进程之下,应用层协议是为应用进程提供服务的,帮助应用进程组织数据。

应用层协议往往在应用进程中实现

1.3.7 错综复杂的客户/服务器交互

在C/S模式中,存在着三种一个与多个的关系:

(1)一个服务器同时为多个客户服务;

(2)一个用户的计算机上同时运行多个连接不同服务器的客户

(3)一个服务器类的计算机同时运行多个服务器

1.3.8 服务器如何同时为多个客户服务

并发性是客户/服务器交互模式的基础,并发允许多个客户获得同一种服务,而不必等待服务器完成对上一个请求的处理。这样才能很好地同时为多个客户提供服务。

1.3.9 标识一个特定服务

在一台服务器类的计算机中可以并发地运行多个服务器进程。它们都要借助协议栈来交换信息,协议栈就是多个服务器进程传输数据的公用通道

这有了一个问题,既然在一个服务器类计算机中运行着多个服务器,如何能让客户无二义性地指明所希望的服务?

这个问题是由传输协议栈提供的一套机制来解决的。这种机制必须赋给每个服务一个唯一的标识,并要求服务器和客户都使用这个标识。

当服务器开始执行时,它在本地的协议栈软件中登记,指明它所提供的服务的标识。当客户与远程服务器通信时,客户在提出请求时,通过这个标识来指定所希望的服务。

客户端机器的传输协议栈软件在发送请求之前也会给客户端进程分配一个唯一的标识。

客户端机器的传输协议栈软件将服务器标识和客户端标识同时传给服务器端机器。服务器端机器的传输协议栈则根据该标识对来确定当前的服务会话过程。

第二章

2.1UNX套接字网络编程接口的产生与发展

2.1.1 问题的提出

从应用程序实现的角度,应用程序如何方便地使用协议栈软件进行通信呢?

如果能在应用程序与协议栈软件之间提供一个软件接口,就可以方便客户与服务器软件的编程。

套接字应用程序编程接口是网络应用程序通过网络协议栈进行通信时所使用的接口,即应用程序与协议栈软件之间的接口,简称套接字编程接口(Socket API)。

它定义了应用程序与协议栈软件进行交互时可以使用的一组操作,决定了应用程序使用协议栈的方式、应用程序所能实现的功能、以及开发具有这些功能的程序的难度。

2.1.2 套接字编程接口的起源与应用

加州大学伯克利(Berkley)分校开发并推广了一个包括TCP/IP互联协议的UNIX,称为BSD UNIX(Berkeley Software Distribution UNIX)操作系统,套接字编程接口是这个操作系统的一个部分。

后来的许多操作系统并没有另外搞一套其它的编程接口,而是选择了对于套接字编程接口的支持。

由于这个套接字规范最早是由Berkeley大学开发的,一般将它称为Berkeley Sockets规范。

2.1.3 套接字编程接口的两种实现方式

要想实现套接字编程接口,可以采用两种实现方式:

一种是在操作系统的内核中增加相应的软件来实现,

一种是通过开发操作系统之外的函数库来实现。

2.1.4 套接字通信与UNIX操作系统的输入/输出

UNIX操作系统对文件和所有其它的输入/输出设备采用一种统一的的操作模式,就是“打开-读-写-关闭”(open-read-write-close)的I/O模式。

用户进程I/O操作的基本流程:

调用open命令,获得对指定文件或设备的使用权,并返回一个描述符(描述符是用来标识该文件或设备的整型数,作为用户在打开的文件或设备上进行操作的句柄)

然后这个用户进程可以多次调用“读”或“写”命令来传输数据。在读写命令中,要将描述符作为命令的参数,来指明所操作的对象。

当所有传输操作完成后,用户进程调用close命令,通知操作系统已经完成了对某对象的使用,释放所占用的资源。

当TCP/IP协议被集成到UNIX内核中的时候,相当于在UNIX系统中引入了一种新型的I/O操作,就是应用程序通过网络协议栈来交换数据。

在UNIX系统的实现中,套接字是完全与其他/O集成在一起的。操作系统和应用程序都将套接字编程接口看作一种输入/输出机制。

·操作过程类似。沿用打开-读-写-关闭模式

·操作方法类似。采用套接字描述符。

·使用的过程的名字可以是相同的。

用户进程与网络协议的交互作用实际要比用户进程与传统的I/O设备相互作用要复杂得多。

使用套接字的应用程序必须说明很多细节。仅仅提供open,read,write,close四个过程远远不够。为避免单个套接字函数参数过多,套接字编程接口的设计者定义了多个函数。

2.2 套接字编程的基本概念

2.2.1 什么是套接字(SOCKET)

套接口是对网络中不同主机上应用进程之间进行双向通信的端点的抽象,一个套接口就是网络上进程通信的一端,提供了应用层进程利用网络协议栈交换数据的机制。

应当从多个层面来理解套接字这个概念的内涵:

·从套接字所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议栈进行通信的接口,是应用程序与网络协议栈进行交互的接口

·从实现的角度来讲,非常复杂。套接字是一种复杂的软件机制,是一种包含了特定的数据结构,包含许多选项,由操作系统内核管理的内核对象

·从使用的角度来讲,对于套接字的操作形成了一种网络应用程序的编程接口(API),包括一组操作套接字的系统调用,或者是库函数,把这一组操作套接字的编程接口函数称作套接字编程接口,套接字是它的操作对象。总之,套接字是网络通信的基石

2.2.2 套接字的特点

1、通信域

套接字存在于通信域中,通信域是为了处理一般的进程通过套接字通信而引入的一种抽象概念,套接字通常只和同一域中的套接字交换数据。

如果数据交换要穿越域的边界,就一定要执行某种解释程序。

现在,仅仅针对Internet.域,并且使用Internet协议族(即TCP/IP协议族)来通信。

套接字实际是通过网络协议栈来进行通信,是对网络协议栈通信服务功能的抽象和封装,通信双方应当使用相同的通信协议。

通信域是一个计算机网络的范围,在这个范围中,所有的计算机使用同一种网络体系机构,使用同一种协议栈。

2、套接字具有三种类型

每一个正被使用的套接字都有它确定的类型,只有相同类型的套接字才能相互通信。

(1)数据报套接字(Datagram SOCKET)

数据报套接字提供无连接的、不保证可靠的、独立的数据报传输服务。在Internet通信域中,数据报套接字使用UDP数据报协议形成的进程间通路,具有UDP协议为上层所提供的服务的所有特点。

(2)流式套接字(Stream SOCKET)

流式套接字提供双向的、有序的、无重复的、无记录边界的可靠的数据流传输服务。在Internet通信域中,流式套接字使用TCP协议形成的进程间通路,具有TCP协议为上层所提供的服务的所有特点,在使用流式套接字传输数据之前,必须在数据的发送端和接收端之间建立连接。

(3)原始式套接字(RAW SOCKET)

·原始式套接字允许对较低层次的协议,如P、ICMP直接访问,用于检验新的协议的实现。

·原始式套接字可以自如地控制Windows下的多种协议,能够对网络底层的传输机制进行控制,所以可以应用原始套接字来操纵网络层和传输层应用。比如,可以通过原始式套接字来接收发向本机的ICMP、IGMP协议包,或者接收TCP/IP栈不能够处理的IP包,也可以用来发送一些自定包头或自定协议的IP包。网络监听技术很大程度上依赖于原始式套接字。

3、套接字由应用层的通信进程创建,并为其服务

套接字是一种内核对象,由操作系统内核进行直接管理;

套接字需由用户进程进行创建,创建成功即表示获得了对系统网络协议栈的1次使用权;

用户进程在需要进行网络通信时,通过套接字间接的使用系统网络协议栈资源;

实际的网络通信过程,由操作系统内核根据用户的要求进行直接管理和调度;

4、使用确定的IP地址和传输层端口号

用户进程创建套接字成功后,会得到一个套接字描述符(一种资源句柄)

在得到套接字描述符后,要将套接字与计算机上的特定的P地址和传输层端口号相关联,这个过程称为绑定(使当前套接字与其他用户的套接字相区分)

一个套接字要使用一个确定的三元组网络地址信息,才能使它在系统中唯一地被标识。

2.2.3 套接字的应用场合

套接字编程适合于开发一些新的网络应用,这类应用具有如下特点:

(1)不管是采用对等模式或者客户机/服务器模式,通信双方的应用程序都需要开发。

(2)双方所交换数据的结构和交换数据的顺序有特定的要求,不符合现在成熟的应用层协议,甚至需要自己去开发应用层协议,自己设计最适合的数据结构和信息交换规程。

2.2.4 套接字使用的数据类型和相关的问题

1、 3种表示套接字地址的结构

在套接字编程接口中,专门定义了三种结构体数据类型,用来存储协议相关的网络地址,在套接字编程接口的函数调用中要用到它们。

(1)通用的socket地址:sockaddr结构,针对各种通信域的套接字,存储它们的地址信息。

 struct sockaddr{
     unsigned short sa family;       //地址家族
     char        sa data[14];        //14字节协议地址
 }

(2)sockaddr_in结构,专门针对Internet通信域,存储套接字相关的网络地址信息,例如IP地址,传输层端口号等信息。

 struct sockaddr_in{
     short int           sin_family;     //地址家族
     unsigned short int  sin_port;       //端口号
     struct in_addr      sin_addr;       //IP地址
     unsigned char       sin_zero[8];    //全为0
 }

(3)in_addr结构,专门用来存储IP地址。

 struct in_addr{
     union{
             struct{u_char s_b1,s_b2,s_b3,s_b4;}S_un b;
             struct {u_short s_w1,s_w2;}S_un_w;
             u long S_addr;
         }S_un;
             
         #define s_addr S_un.S_addr;
     };

注意:IP地址以网络字节序进行保存

The IN ADDR derived structures are only defined on the Windows SDK released with Windows Vista and later. On earlier versions of the Windows SDK,variables of this type should be declared as struct in_addr.

例:IP地址10.14.25.90,依据in addr结构体的定义,可以有4种不同的表示方式:假设定义结构体变量in_addr sin_addr

1、 sin_addr.S_un.S_un.b.s_b1=10;

sin_addr.S_un.S_un.b.s_b2=14;

sin_addr.S_un.S_un.b.s_b3=25;

sin_addr.S_un.S_un.b.s_b4=90;

2、 sin_addr.S_un.S_un_w.s_w1=(14<<8)10;

sin_addr.S_un.S_un_w.s_w1=(90<<8)|25;

3、 sin_addr.S_un.S_addr=(90<<24)l(25<<16)l(14<<8)|10;

4、 sin_addr.s_addr=(90<<8)(25<<16)|(14<<8)|10;

IPv6的地址结构体

 struct in6_addr {
     u_int8_t s6_addr[16];/*128bit字节地址*/
 };
 #define SIN6 LEN /*required for compile-time tests */
 struct sockaddr_in6 {
     u_int8_t        sin6_len;/*SIN6 LEN */
     sa_family_t     sin6_family;/*AF INET6 */
     in_port_t       sin6_port;
     u_int32_t       sin6_flowinfo;/*priority and flow label */
     struct in6_addr sin6_addr;/*IlPV6的任意地址是in6addr_any*/
     u_int32_t       sin6_scope_id;
 };

(4)这些数据结构的一般用法:

首先,定义一个Sockaddr_in的结构实例,并将它清零。比如:

 struct sockaddr_in myad;
 memset(&myad,O,sizeof(struct sockaddr in));

函数原型:void * memset(void *s,int ch,unsigned n);

然后,为这个结构赋值,如:

 myad.sin_family=AF_INET;
 myad.sin_port=htons(8080);
 myad.sin_addr.s_addr=INADDR-ANY;

最后,在函数调用中使用时,将这个结构强制转换为sockaddr类型。

 accept(listenfd,(sockaddr*)(&myad),&addrlen);
INADDR ANY

表示不确定地址,或“所有地址”、“任意地址”。一般来说,在各个系统中均定义成为0值。 将IP地址指定为INADDR_ANY,允许套接字向任何分配给本地机器的P地址发送或接收数据。 多数情况下,每个机器只有一个P地址,但有的机器可能会有多个网卡,每个网卡都可以有自己的IP地址,用INADDR ANY可以简化应用程序的编写。将地址指定为NADDR ANY,允许一个独立应用接受发自多个接口的回应。 如果我们只想让套接字使用多个P中的一个地址,就必须指定实际地址。

2、本机字节顺序和网络字节顺序

·在具体计算机中的多字节数据的存储顺序,称为本机字节顺序。不同的计算机存放多字节值的顺序不同,有的机器在起始地址存放低位字节(低位先存),有的机器在起始地址存放高位字节(高位先存)。基于Intel的CPU,即我们常用的PC机采用的是低位先存。

·多字节数据在网络协议报头中的存储顺序,称为网络字节顺序。为保证数据的正确性,在网络协议中需要指定网络字节顺序。TCP/IP协议使用16位整数和32位整数的高位先存格式。

网络应用程序要在不同的计算机中运行,本机字节顺序是不同的,但是,网络字节顺序是一致的。

所以,应用程序在编程的时候,在把P地址和端口号装入套接字的时候,应当把它们从本机字节顺序转换为网络字节顺序;相反,在本机输出时,应将它们从网络字节顺序转换为本机字节顺序。

套接字编程接口特为解决这个问题设置了四个函数:

htons():短整数本机顺序转换为网络顺序,用于端口号。

htonl():长整数本机顺序转换为网络顺序,用于IP地址。

ntohs():短整数网络顺序转换为本机顺序,用于端口号。

ntohl():长整数网络顺序转化为本机顺序,用于IP地址。

这四个函数将被转换的数值作为函数的参数,函数返回值是转换后的结果。

3、点分十进制的P地址的转换

在因特网中,IP地址常常用点分十进制的表示方法,但在套接字中,IP地址是无符号的长整型数,套接字编程接口设置了两个函数,专门用于两种形式的IP地址的转换。

(1)inet_addr函数:

unsigned long inet-addr(const char*cp)

入口参数cp:点分十进制形式的P地址。

返回值:网络字节顺序的P地址,是无符号的长整数。

const int *ptr; 表示ptr所指的地址可更改,但ptr指向的数据内容不可更改。

int *const ptr; 表示ptr所指的地址不可更改,但ptr指向的数据内容可更改。

const int *const ptr; 表示pr所指的地址和ptr指向的数据内容均不可更改。

(2)inet ntoal函数:

char*inet ntoa(struct in_addr in)

入口参数in:包含长整型IP地址的in_addr结构变量

返回值:指向点分十进制P地址的字符串的指针。

注意的是:函数inet ntoa0的参数是struct in addr,而不是Iong。同时要注意的是该函数返回值为指向字符串地址的指针,该字符串的空间为静态分配的,这意味着在第二次调用该函数时,上一次调用将会被重写(复盖)。例如:

char *al,*a2;
a1 = inet_ntoa(inal.sin_addr);/*this is 198.92.129.1 */
a2 = inet_ntoa(ina2.sin_addr);/*this is 132.241.5.10*/
printf("address 1:%s n",al);
printf("address 2:%s n",a2);

运行结果是:

address 1:132.241.5.10
address 2:132.241.5.10

如果你想保存地址,那么用strcpy() 保存到自己的字符数组中。

4、域名服务

通常,我们使用域名来标识站点,可以将文字型的主机域名直接转换成IP地址,

struct hostent*gethostbyname(const char* name);

入口参数:是站点的主机域名字符串

返回值:是指向hostent结构的指针,

hostent结构包含主机名,主机别名数组,返回地址的类型(一般是AF_INET),地址长度的字节数,已符合网络字节顺序的主机网络地址等。

#include <netdb.h>
struct hostent{
    char *h_name;
	char **h_aliases;
    int h_addrtype;
    int h_length;
    char **h_addr_list;};
#define h_addr  h_addr_list[O]

这个结构的解释:

char *h_name 表示的是主机的规范名。例如www.google.com的规范名其实是www.l.google.com.

char **h aliases表示的是主机的别名。

int h_addrtype 表示的是主机ip地址的类型,ipv4(AF_INET) , ipv6(AF_INET6)

int h_length 表示的是主机ip地址的长度

char **h_addr_list表示的是主机的ip地址,注意,这个是以网络字节序存储的。


例子:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>

int main(int argc, char *argv[]) {
    struct hostent *h;
    
    if (argc != 2) {
        // 错误检查命令行参数
        fprintf(stderr, "usage: get ip address\n");
        exit(1);
    }
    
    if ((h = gethostbyname(argv[1])) == NULL) {
        // 获取主机信息
        herror("gethostbyname");
        exit(1);
    }
    
    printf("Host name: %s\n", h->h_name);
    printf("IP Address: %s\n", inet_ntoa(*((struct in_addr *)h->h_addr)));
    
    return 0;
}
gethostbynamel函数用法
#include <stdio.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>

int main(int argc, char **argv) {
    char *ptr, **pptr;
    struct hostent *hptr;
    char str[32];
	/*取得命令后第一个参数,即要解析的域名或主机名*/
    ptr = argv[1];
    /* 调用gethostbyname()获取主机信息 */
    if ((hptr = gethostbyname(ptr)) == NULL) {
        printf("gethostbyname error for host: %s\n", ptr);
        return 1; /* 如果调用gethostbyname发生错误,返回1 */
    }

    /* 打印规范主机名 */
    printf("Official hostname: %s\n", hptr->h_name);

    /* 主机可能有多个别名,将所有别名分别打印出来 */
    for (pptr = hptr->h_aliases; *pptr != NULL; pptr++) {
        printf("Alias: %s\n", *pptr);
    }

    /* 根据地址类型,将地址打印出来 */
    switch (hptr->h_addrtype) {
        case AF_INET:
        case AF_INET6:
            pptr = hptr->h_addr_list;
            /* 将刚才得到的所有地址都打印出来。其中调用了inet_ntop()函数 */
            for (; *pptr != NULL; pptr++) {
                printf("Address: %s\n", inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));
            }
            break;
        default:
            printf("Unknown address type\n");
            break;
    }
    return 0;
}

这段代码是一个简单的C程序,用于获取主机名对应的信息,包括官方主机名、别名以及对应的IP地址列表,并根据地址类型打印IP地址。以下是对代码的详细解释:包含所需的头文件。

<stdio.h>:标准输入输出库,用于打印信息到控制台。

<netdb.h>:网络数据库操作库,包含了主机信息查询函数。

<arpa/inet.h>:包含了用于IP地址转换的函数,如 inet_ntop

<netinet/in.h>:包含了与网络相关的数据结构和宏定义。

<sys/socket.h>:包含了套接字编程相关的函数和数据结构。

main 函数是程序的入口函数,接受命令行参数 argcargv

ptr 是一个字符指针,用于存储命令行参数中传递的主机名或域名。

使用 gethostbyname() 函数获取主机信息,并将结果保存在 hptr 中。如果获取失败,程序会打印错误信息并返回1。

打印官方主机名 hptr->h_name

使用 for 循环遍历可能的别名列表,并打印出每个别名。

使用 switch 语句根据地址类型来处理IP地址。如果是IPv4或IPv6,将遍历地址列表,并使用 inet_ntop() 函数将二进制IP地址转换为可读的字符串形式,并打印出来。

如果地址类型不是IPv4或IPv6,则打印"Unknown address type"。

返回0表示成功执行,程序正常结束。

这个程序的主要目的是获取一个主机的信息,包括其官方主机名、别名和IP地址。它演示了如何使用一些网络编程的基本函数来获取这些信息,并将其打印到控制台。需要注意的是,gethostbyname() 在现代网络编程中已经不再推荐使用,建议使用 getaddrinfo() 函数来代替,因为后者支持IPv4和IPv6,并提供更多灵活的选项。

输出:

  1. 对于 bit.edu.cn

Official hostname: bit.edu.cn
Address: 202.204.80.7
  1. 对于 xmu.edu.cn

Official hostname: xmu.edu.cn
Address: 210.34.0.2
  1. 对于 sina.com

Official hostname: sina.com
Address: 71.5.7.191
  1. 对于 sohu.com

Official hostname: sohu.com
Address: 61.135.181.176
Address: 61.135.181.175
IP 地址转换函数

在Linux下的 inet_ptoninet_ntop 这两个 IP 地址转换函数,可以在将 IP 地址在 "点分十进制" 和 "整数" 之间进行转换。而且,这两个函数能够处理 IPv4 和 IPv6。其中,

const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt);

参数:

af:地址族,可以是 AF_INET(IPv4)或 AF_INET6(IPv6)。

src:要转换的点分十进制字符串表示的 IP 地址。

dst:存储转换后的二进制 IP 地址的目标缓冲区。

socklen_t cnt:套接字长度

用于将二进制 IP 地址转换为点分十进制字符串表示。

在Windows中也提供了对应的两个函数,InetPtonInetNtop(VS2008以上可用)。其中,

PCTSTR WSAAPI InetNtop(
	_In_ INT Family, 
	_In_ PVOID pAddr, 
	_Out_ PTSTR pStringBuf, 
	_In_ size_t StringBufSize);

用于将二进制 IP 地址转换为点分十进制字符串表示。

主机名IP地址函数

gethostname 函数返回运行程序的计算机的主机名。然后,你可以使用 gethostbyname() 函数来获取该计算机的IP地址。以下是函数的定义:

#include <unistd.h>
int gethostname(char *hostname, size_t size);

参数非常简单:hostname 是一个字符数组指针,它将在函数返回时存储主机名,sizehostname 数组的字节长度。

函数成功调用时返回 0,失败时返回 -1。

gethostbyname 函数:

功能:根据主机名获取主机的IP地址。

原型:struct hostent *gethostbyname(const char *name);

参数:

name:要查询的主机名的字符串。

返回值:

如果查询成功,返回一个指向 struct hostent 结构的指针,该结构包含了主机名、别名和IP地址列表等信息。

如果查询失败,返回NULL

使用示例:

#include <netdb.h>
struct hostent *host;
host = gethostbyname("example.com");
if (host != NULL) {
    printf("Official hostname: %s\n", host->h_name);
    // 打印别名和IP地址列表等信息
} else {
    herror("gethostbyname");
}

这个函数用于根据主机名查询主机的IP地址和相关信息,以便在网络编程中建立连接或进行通信。


2.3 面向连接的套接字编程

2.3.1 套接字的工作过程

p33 图2.5

2.3.2 UNIX套接字编程接口的系统调用

1、创建套接字SOCKET()---打开一个通道(第1步)

socket函数创建一个套接字并返回一个整型描述符:

int socket(int Protofamily,int Type,int Protocol);

1.Protofamily应该设置成“AF_INET”。

2.参数type告诉内核是SOCK STREAM类型还是SOCK DGRAM类型。

3.把protocol设置为"0"。

4.socket()返回socket描述符,或者在错误的时候返回-l。

#include <sys/types.h>
#include <sys/socket.h>
// 设置套接字选项
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen)

sockfd:标识一个套接口的描述字。 level:选项定义的层次;支持SOL_SOCKETIPPROTO_TCPIPPROTO_IPIPPROTO_IPV6等不同的层次。 optname:需要设置的选项。 optval:指针,指向存放选项值的缓冲区。 optlen:optval缓冲区的长度。

有两种类型的套接口选项:

  1. 布尔型选项,允许或禁止一种特性。

    • 允许一个布尔型选项,则将optval指向非零整数。

    • 禁止一个选项,则optval指向一个等于零的整数。

    • 对于布尔型选项,optlen应该等于sizeof(int)。

  2. 整数或结构选项。

    • optval指向包含所需选项的整数或结构的指针。

    • optlen则为整数或结构的长度。

setsockopt()支持下列选项。其中"类型”表明optval所指数据类型。

选项类型含义
SO_BROADCASTBOOL允许套接口传送广播信息。
SO_DEBUGBOOL记录调试信息。
SO_DONTLINGERBOOL不要因为数据未发送就阻塞关闭操作。
SO_DONTROUTEBOOL禁止选径;直接传送。
SO_KEEPALIVEBOOL发送 "保持活动" 包。
SO_LINGERstruct linger FAR*如关闭时有未发送数据,则逗留。
SO_OOBINLINEBOOL在常规数据流中接收带外数据。
SO_RCVBUFint为接收确定缓冲区大小。
SO_REUSEADDRBOOL允许套接口和一个已在使用中的地址捆绑。
SO_SNDBUFint指定发送缓冲区大小。
TCP_NODELAYBOOL禁止发送合并的 Nagle 算法。

若无错误发生,setsockopt()返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。

例如:

// 设置接收缓冲区大小为32K
int nRecvBufLen = 32 * 1024; // 设置为32K
setsockopt(s, SOL_SOCKET, SO_RCVBUF, (const char*)&nRecvBufLen, sizeof(int));

// 设置发送缓冲区大小为32K
int nSendBufLen = 32 * 1024; // 设置为32K
setsockopt(s, SOL_SOCKET, SO_SNDBUF, (const char*)&nSendBufLen, sizeof(int));

议栈在调用 setsockopt 时会与 rmem_max 进行比较。rmem_max 是一个内核调优参数,可以通过 /proc/sys/net/core/rmem_max 进行调整。rmem_max 表示内核接受数据缓冲区的最大大小。

mem_max 是内核接受数据缓冲的大小,它的默认值与内核版本有关。例如,在Linux 2.6.2版本中,mem_max 的默认值是 65535。需要注意的是,mem_max 表示的是缓冲区的大小,而不是一次要接收的数据的大小。

应用程序可以通过调用 recv 函数的参数来指定一次接收的数据大小,但指定的参数最大不会超过 mem_max。内核会根据 mem_max 来缓存接收到的数据,并在应用程序调用 recv 函数时进行处理。

2、绑定套接字到指定的地址BIND()---打开一个通道(第2步)

int bind(int sockfd,struct sockaddr*My addr,int Addrlen);

  • sockfd 是调用 socket 返回的文件描述符。

  • myaddr 是指向数据结构 struct sockaddr 的指针,用于保存地址信息,包括端口和IP地址。

  • addrlen 设置为 sizeof(struct sockaddr)

#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#define MYPORT 3490

int main() {
    int sockfd;
    struct sockaddr_in my_addr;

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

    // 进行错误检查,确保套接字创建成功
    if (sockfd == -1) {
        perror("Socket creation failed");
        return 1; // 或者采取适当的错误处理措施
    }

    // 设置地址结构体
    my_addr.sin_family = AF_INET;      // 使用IPv4地址
    my_addr.sin_port = htons(MYPORT);  // 设置端口号,注意字节序转换
    my_addr.sin_addr.s_addr = inet_addr("132.241.5.10");  // 设置IP地址
    bzero(&(my_addr.sin_zero), 8);     // 清零结构体的其余部分

    // 进行错误检查,确保绑定成功
    if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) {
        perror("Bind failed");
        return 1; // 或者采取适当的错误处理措施
    }

    // 其他操作...

    return 0;
}

my_addr.sin_port 是网络字节顺序,my_addr.sin_addr.s_addr 也是的。

在处理自己的IP地址和/或端口时,有些工作是可以自动处理的。

my_addr.sin_port = 0;//选择一个未使用的端口(随机)
my_addr.sin_addr.s_addr = INADDR_ANY;//使用本地机器的任何可用IP地址

通过将0赋给my_addr.sin_port,你告诉bind函数自己选择合适的端口。同样,将my_addr.sin_addr.s_addr设置为INADDR_ANY,你告诉它自动填上你所运行的机器的IP地址。

这里没有将INADDR_ANY转换为网络字节顺序!这是因为:INADDR_ANY实际上就是0!即使改变字节的顺序,0依然是0。

3、启动监听Listen()---等待通信请求

int listen(int sockfd,int backlog);
  • sockfd 是调用 socket 返回的套接口文件描述符。

  • backlog 是在进入队列中允许的连接数目(queue of pending connections)。

描述:

listen 函数用于将套接字设置为监听状态,以等待传入的连接请求。在调用 listen 后,套接字将能够接受客户端的连接请求,并将它们放入两个不同的队列中:

  1. 未完成连接队列(SYN_RCVD):维护等待完成三次握手的连接,这些连接已经收到了客户端的SYN报文,但尚未建立完全的连接。

  2. 已连接队列(ESTABLISHED):包含已经完成三次握手的连接,这些连接已经准备好进行通信。

backlog 参数指定了允许在未完成连接队列中排队的连接数目。如果队列已满,新的连接请求将被拒绝。

返回值:

  • 如果成功,返回0。

  • 如果失败,返回-1,并设置全局变量 errno 以指示错误的类型。

示例:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>

int main() {
    int sockfd;

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

    if (sockfd == -1) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 设置套接字为监听状态,backlog为5
    if (listen(sockfd, 5) == -1) {
        perror("Listen failed");
        exit(EXIT_FAILURE);
    }

    printf("Socket listening...\n");

    // 其他操作...

    return 0;
}

上述示例演示了如何创建套接字并将其设置为监听状态,以等待传入的连接请求。在此示例中,backlog 参数被设置为5,允许最多排队5个未完成连接。

TCP连接队列和 SOMAXCONN

在TCP通信中,连接队列的实际长度由系统常量 SOMAXCONN 决定,默认值为128,并且可以通过参数 backlog 共同决定。

  • backlog 的值小于 SOMAXCONN 时,已完成连接队列的数量最多为 backlog 的值,未完成连接队列的数量大约在10左右。

  • backlog 的值大于等于 SOMAXCONN 时,已完成连接队列的数量最多为 SOMAXCONN,未完成连接队列的数量仍然大约在10左右。

在客户端的第一个SYN到达时,TCP会在未完成连接队列中增加一个新的记录,然后回复给客户端三次握手的第二个报文(服务端的SYN和针对客户端的ACK)。这个记录将一直存在于未完成连接队列中,直到三次握手中的最后一个报文到达,或者直到发生超时。在Berkeley套接字实现中,超时被定义为75秒。

如果在客户端的SYN到达时,未完成连接队列已经满了,TCP将忽略后续到达的SYN报文,但不会发送RST信息给客户端,这允许客户端重传SYN报文以尝试重新建立连接。

举例:LISTEN(Sockfe,3); 监听套接字使用缓冲区接纳多个客户端的连接请求

image-20231212162552537

4、接收连接请求ACCEPT()---接收服务请求

int accept(int sockfd, struct sockaddr *addr, int *addrlen);

参数:

  • sockfd:用于监听客户端连接请求的套接字描述符。

  • addrsockaddr 结构变量的指针,是一个输出型参数。当函数执行完成后,这个变量中包含所接收的客户端的地址信息。

  • addrlen:输出型参数,调用函数时需要初始化为 addr 结构的长度,不能为0或NULL。执行完毕后,返回所接收客户端网络地址的长度。

描述:

accept 函数用于接受客户端的连接请求,它会等待客户端连接并返回一个新的套接字描述符(称为响应套接字)。这个新的套接字已经与客户端建立了连接,并可以用于以后与客户端进行数据交换。

返回值:

  • 如果执行正确,accept 函数返回新的套接字描述符(响应套接字)。

  • 如果失败,返回-1,并设置全局变量 errno 以指示错误的类型。

注意:

  • accept 函数通常在服务器端用于接受连接请求。一旦连接被接受,服务器可以使用返回的新套接字来与客户端进行通信。

  • addr 参数用于存储客户端的地址信息,包括IP地址和端口号。

  • addrlen 参数在调用函数之前应该初始化为 addr 结构的长度。函数执行完成后,它会包含客户端地址的实际长度。

示例:

// 假设已经创建了监听套接字 listenfd
int clientfd; // 定义响应套接字描述符变量
int addrlen = sizeof(struct sockaddr); // 获取套接字地址结构长度
struct sockaddr_in cltsockaddr; // 定义用于返回客户端地址的结构

clientfd = accept(listenfd, (struct sockaddr*)(&cltsockaddr), &addrlen); // 接收客户连接请求
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main() {
    int sockfd, new_sock;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);

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

    if (sockfd == -1) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 绑定套接字到端口
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("Bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听连接请求
    if (listen(sockfd, 5) == -1) {
        perror("Listen failed");
        exit(EXIT_FAILURE);
    }

    printf("Server listening...\n");

    // 接受连接请求
    new_sock = accept(sockfd, (struct sockaddr *)&client_addr, &addr_len);

    if (new_sock == -1) {
        perror("Accept failed");
        exit(EXIT_FAILURE);
    }

    printf("Accepted connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

    // 其他操作...

    return 0;
}

5、请求建立连接CONNECT()---建立连接请求

int connect(int sockfd, struct sockaddr *server_addr, int addrlen);
  • sockfd:客户端生成并安装的套接字描述符(请求套接字)。

  • server_addr:存放服务器端的网络地址。

  • addrlensockaddr 结构的长度。

描述:

connect 函数用于客户端建立与服务器端的连接。它将客户端的套接字(请求套接字)与服务器的套接字进行连接。在调用 connect 之前,客户端无需进行 bind 调用,因为内核将选择一个合适的端口号并将其与当前套接字绑定。

返回值:

  • 如果连接成功,返回0。

  • 如果连接失败,返回-1,并设置全局变量 errno 以指示错误的类型。

注意:

  • connect 函数通常在客户端用于建立与服务器的连接。一旦连接成功,客户端可以使用返回的套接字来与服务器进行数据通信。

  • server_addr 参数用于指定服务器端的网络地址信息,包括IP地址和端口号。

  • addrlen 参数应该设置为 server_addr 结构的长度。

示例:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main() {
    int sockfd;
    struct sockaddr_in server_addr;

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

    if (sockfd == -1) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 设置服务器地址信息
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    // 连接到服务器
    if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("Connect failed");
        exit(EXIT_FAILURE);
    }

    printf("Connected to server\n");

    // 其他操作...

    return 0;
}

6、读/写套接字read函数和write函数

read函数

int read(int sockfd, void *buffer, int len);
  • sockfd:要读取的套接字描述符。在客户端是请求套接字,在服务器端是响应套接字。

  • buffer:指向内存中用于存放数据的读取缓冲区。

  • len:读取缓冲区的长度,或者希望读取的字符长度。

描述:

read 函数用于从套接字中读取数据。它从指定的套接字 sockfd 中读取数据,并将其存储到指定的缓冲区 buffer 中。len 参数表示要读取的数据的长度。

返回值:

  • 如果成功,返回已读取的字节数。

  • 如果达到文件末尾,返回0。

  • 如果失败,返回-1,并设置全局变量 errno 以指示错误的类型。

注意:

  • read 函数必须用于已连接的套接字,即在成功建立连接之后才能使用。

  • 通常在客户端用于从服务器端读取响应数据。

write函数

int write(int sockfd, const void *buffer, int len);
  • sockfd:要写入的套接字描述符。在客户端是请求套接字,在服务器端是响应套接字。

  • buffer:指向内存中存储要写入的数据的缓冲区。

  • len:要写入的数据的长度。

描述:

write 函数用于向套接字中写入数据。它将指定的数据从缓冲区 buffer 写入到套接字 sockfd 中,写入的数据长度由参数 len 指定。

返回值:

  • 如果成功,返回已写入的字节数。

  • 如果失败,返回-1,并设置全局变量 errno 以指示错误的类型。

注意:

  • write 函数必须用于已连接的套接字,即在成功建立连接之后才能使用。

  • 通常在客户端用于向服务器端发送请求数据。

示例:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>

int main() {
    int sockfd;
    char buffer[1024];

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

    if (sockfd == -1) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 其他操作...

    // 读取数据
    int bytesRead = read(sockfd, buffer, sizeof(buffer));

    if (bytesRead == -1) {
        perror("Read failed");
        exit(EXIT_FAILURE);
    }

    // 写入数据
    int bytesWritten = write(sockfd, buffer, bytesRead);

    if (bytesWritten == -1) {
        perror("Write failed");
        exit(EXIT_FAILURE);
    }

    // 其他操作...

    return 0;
}

7. 向套接字发送 SEND() 和从套接字接收 RECV()

send函数

int send(int sockfd, const void *buf, int len, int flags);
  • sockfd:要写入的套接字描述符。在客户端是请求套接字,在服务器端是响应套接字。

  • buf:指向内存中存储要发送的数据的缓冲区。

  • len:要发送的数据的长度。

  • flags:执行本调用的方式,通常置为0。

描述:

send 函数用于向套接字中发送数据。它将指定的数据从缓冲区 buf 发送到套接字 sockfd 中,发送的数据长度由参数 len 指定。

返回值:

  • 如果成功,返回已发送的字节数。

  • 如果失败,返回-1,并设置全局变量 errno 以指示错误的类型。

注意:

  • send() 函数在调用后会返回实际发送数据的长度。

  • send 函数必须用于已连接的套接字,即在成功建立连接之后才能使用。

  • 通常在客户端用于向服务器端发送请求数据。

  • send() 函数默认工作在阻塞模式下。

    在非阻塞情况下,send() 所发送的数据可能少于你给它的参数所指定的长度!这是因为如果你给 send() 的参数中包含的数据的长度远远大于 send() 所能一次发送的数据,那么 send() 函数只发送它所能发送的最大数据长度,然后对剩下的数据再次调用它来进行第二次发送。

    因此,记住如果 send() 函数的返回值小于 len 的话,你需要再次发送剩下的数据。幸运的是,大多数情况下,send() 都会一次发送成功。

recv函数

int recv(int sockfd, void *buf, int len, int flags);
  • sockfd:要读取的套接字描述符。在客户端是响应套接字,在服务器端是请求套接字。

  • buf:指向内存中用于存放接收数据的缓冲区。

  • len:接收缓冲区的长度,或者希望接收的字符长度。

  • flags:执行本调用的方式,通常置为0。

描述:

recv 函数用于从套接字中接收数据。它从指定的套接字 sockfd 中接收数据,并将其存储到指定的缓冲区 buf 中,接收的数据长度由参数 len 指定。

返回值:

  • 如果成功,返回已接收的字节数。

  • 如果连接已关闭,返回0。

  • 如果失败,返回-1,并设置全局变量 errno 以指示错误的类型。

注意:

  • recv 函数必须用于已连接的套接字,即在成功建立连接之后才能使用。

  • 通常在客户端用于从服务器端接收响应数据。

  • recv() 返回它所真正收到的数据的长度(也就是存储到 buf 中的数据的长度)。

    如果 recv() 返回 -1,则代表发生了错误。

示例:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>

int main() {
    int sockfd;
    char buffer[1024];

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

    if (sockfd == -1) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 其他操作...

    // 发送数据
    int bytesSent = send(sockfd, buffer, sizeof(buffer), 0);

    if (bytesSent == -1) {
        perror("Send failed");
        exit(EXIT_FAILURE);
    }

    // 接收数据
    int bytesRead = recv(sockfd, buffer, sizeof(buffer), 0);

    if (bytesRead == -1) {
        perror("Receive failed");
        exit(EXIT_FAILURE);
    }

    // 其他操作...

    return 0;
}
阻塞式Socket的send函数执行流程

send函数首先检查协议是否正在发送套接字s的发送缓冲区中的数据。

  • 如果协议正在发送数据,send函数等待协议将数据发送完毕。

  • 如果协议尚未开始发送套接字s的发送缓冲区中的数据,或者发送缓冲区中没有数据,那么 send 函数比较套接字s的发送缓冲区的剩余空间和参数 len

  • 如果 len 大于剩余空间大小,send 函数一直等待协议将套接字s的发送缓冲中的数据发送完毕。

  • 如果 len 小于剩余空间大小,send 函数仅将 buf 中的数据复制到剩余空间中(注意,send 不会传输套接字s的发送缓冲区中的数据到连接的另一端,而是由协议传输,send 仅将 buf 中的数据复制到套接字s的发送缓冲区的剩余空间中)。

  • 如果 send 函数成功复制数据,就返回实际复制的字节数。

  • 如果 send 在复制数据时出现错误,那么 send 函数返回 SOCKET_ERROR

  • 如果 send 在等待协议传输数据时发生网络断开,那么 send 函数也返回 SOCKET_ERROR

  • 需要注意的是,send 函数在成功复制数据到套接字s的发送缓冲区的剩余空间后立即返回,但这些数据不一定立即传输到连接的另一端。如果协议在后续的传输过程中出现网络错误,下一个Socket函数将返回 SOCKET_ERROR。(每一个除 send 外的Socket函数在执行的最开始总要先等待套接字的发送缓冲中的数据被协议传输完毕才能继续,如果在等待时出现网络错误,那么该Socket函数就返回 SOCKET_ERROR。)

同步Socket的recv函数执行流程

当应用程序调用 recv 函数时,recv 先等待套接字s的发送缓冲中的数据被协议传输完毕。如果协议在传输套接字s的发送缓冲中的数据时出现网络错误,那么 recv 函数返回 SOCKET_ERROR

  • 如果套接字s的发送缓冲中没有数据,或者数据被协议成功传输完毕后,recv 先检查套接字s的接收缓冲区。

  • 如果套接字s的接收缓冲区中没有数据,或者协议正在接收数据,recv 将一直等待,直到协议把数据接收完毕。

  • 当协议接收到数据后,recv 函数将套接字s的接收缓冲中的数据复制到 buf 中(注意,协议接收到的数据可能大于 buf 的长度,因此在这种情况下需要多次调用 recv 函数才能完全复制套接字s的接收缓冲中的数据)。recv 函数返回实际复制的字节数。

  • 如果 recv 在复制数据时出现错误,那么它返回 SOCKET_ERROR

  • 如果 recv 函数在等待协议接收数据时网络断开,它将返回0。

8. 关闭套接字CLOSE()

CLOSE()函数

int CLOSE(int sockfd);
  • CLOSE()函数的调用将阻止在套接口上进一步的数据读写。任何在另一端尝试读写套接口的操作都将返回错误信息。

shutdown()函数

int shutdown(int sockfd, int how);
  • shutdown()函数允许你有更多控制关闭套接口的方式。它可以关闭特定方向的通信,或关闭双向通信(与close()函数类似)。

参数说明:

  • sockfd:要关闭的套接口文件描述符。

  • how 的值可以是以下之一:

    • 0 - 禁止进一步的接收操作。

    • 1 - 禁止进一步的发送操作。

    • 2 - 禁止进一步的发送和接收操作。

使用shutdown()函数,你可以更具体地控制套接口的关闭方式,允许你选择禁止数据接收、数据发送或同时禁止两者。

2.3.3 面向连接的套接字编程实例

1.实例的功能

服务器对来访的客户计数,并向客户报告这个计数值。

客户建立与服务器的一个连接并等待它的输出。

每当连接请求到达时,服务器生成一个可打印的ASCII串信息,将它在连接上发回,然后关闭连接。

客户显示收到的信息,然后退出。例如,对于服务器接收的第10次客户连接请求,该客户将收到并打印如下信息:

This server has been contacted 10 times.

2.实例程序的命令行参数

实例是UNIX环境下的C程序,客户和服务器程序在编译后,均以命令行的方式执行。

服务器程序执行时可以带一个命令行参数,是用来接受请求的监听套接字的协议端口号。这个参数是可选的。如果不指定端口号,代码将使用程序内定的缺省端口号5188。

客户程序执行时可以带两个命令行参数: 一个是服务器所在计算 机的主机名 , 另一个是服务器监听的协议端口号。

这两个参数都是可选的。

如果没有指定协议端口号 ,客户使用程序内定的缺省值5188。

如果一个参数也没有 ,客户使用缺省端口和主机名localhost , localhost是映射到客户所运行的计算机的一个别名。

允许客户与本地机上的服务器通信 , 对调试是很有用的。

3.客户程序代码

/*----------------------------------------------------
* 程序: client.c
* 目的: 创建一个套接字,通过网络连接一个服务器,并打印来自服务器的信息
* 语法: client [ host [ port ] ]
*             host - 运行服务器的计算机的名字
*             port - 服务器监听套接字所用协议端口号
* 注意:两个参数都是可选的。如果未指定主机名,客户使用localhost;如果未指定端口号,
* 客户将使用PROTOPORT中给定的缺省协议端口号
*----------------------------------------------------
*/

#include <sys/types.h>
#include <sys/socket.h>  /* UNIX下,套接字的相关包含文件。*/
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>

#define PROTOPORT  5188  /*缺省协议端口号*/
extern int errno;

char localhost = "localhost";   /*缺省主机名*/

main(argc,argv)
int argc;
char *argv[];
{
    struct  hostent  *ptrh;          /* 指向主机列表中一个条目的指针 */
    struct  sockaddr_in  servaddr;   /* 存放服务器端网络地址的结构 */

    int      sockfd;/* 客户端的套接字描述符 */
    int       port; /* 服务器端套接字协议端口号*/
    char*  host;/* 服务器主机名指针 */
    int  n;/* 读取的字符数 */
    char  buf[1000];/* 缓冲区,接收服务器发来的数据 */

    memset((char*)& servaddr,0,sizeof(servaddr));    /* 清空sockaddr结构 */ 
    servaddr.sin_family = AF_INET;               /* 设置为因特网协议族 */

    /* 检查命令行参数,如果有,就抽取端口号。否则使用内定的缺省值*/
    if (argc>2){
        port = atoi(argv[2]);        /* 如果指定了协议端口,就转换成整数 */
    }else {
        port = PROTOPORT;       /* 否则,使用缺省端口号 */
    }

    if (port>0)           /* 如果端口号是合法的数值,就将它装入网络地址结构 */
        servaddr.sin_port = htons((u_short)port);
    else{                       /* 否则,打印错误信息并退出*/
        fprintf(stderr,”bad port number %s\n” ,argv[2]);
        exit(1);
    }

    /* 检查主机参数并指定主机名 */
    if(argc>1){
        host = argv[1];           /* 如果指定了主机名参数,就使用它 */
    }else{
        host = localhost;          /* 否则,使用缺省值 */
    }

    /* 将主机名转换成相应的IP地址并复制到servaddr 结构中 */
    ptrh = gethostbyname( host );   /* 从服务器主机名得到相应的IP地址 */
    if  ( (char *)ptrh == null ) {    /* 检查主机名的有效性,无效则退出 */
        fprintf( std err, ” invalid host: %s\n” , host );
        exit(1);
    }

    memcpy(&servaddr.sin_addr, ptrh->h_addr, ptrh->h_length );

    /* 创建一个套接字*/
    sockfd = SOCKET(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        fprintf(stderr, ”socket creation failed\n” );
        exit(1);
    }

    /* 请求连接到服务器 */
    if (connect( sockfd, (struct sockaddr *)& servaddr, sizeof(servaddr)) < 0) {
        fprintf( stderr,”connect failed\n” );
        /* 连接请求被拒绝,报错并退出 */
        exit(1);
    }

    /* 从套接字反复读数据,并输出到用户屏幕上 */
    n = recv(sockfd , buf, sizeof( buf ), 0 );
    while ( n > 0) {
        write(1 ,buf, n);
        /* unix I/O文件句柄: 0    std in;1     std out;2    std err */
        n = recv( sockfd , buf, sizeof( buf ), 0 );
    }

    /* 关闭套接字*/
    closesocket( sockfd );
    /* 终止客户程序*/
    exit(0);
}

4. 服务器实例代码

/*----------------------------------------------------
* 程序:server.c
* 目的: 分配一个套接字,然后反复执行如下几步:
* (1) 等待客户的下一个连接
* (2) 发送一个短消息给客户
* (3) 关闭与客户的连接
* (4) 转向(1)步
* 命令行语法: server [ port ]
*                 port - 服务器端监听套接字使用的协议端口号
* 注意: 端口号可选。如果未指定端口号,服务器使用PROTOPORT中指定的缺省 * 端口号
*---------------------------------------------------------------
*/

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#define PROTOPORT  5188        /* 监听套接字的缺省协议端口号 */
#define QLEN  6               /* 监听套接字的请求队列大小 */

int visits = 0;                    /* 对于客户连接的计数*/

main(argc,argc)
int  argc;
char* argv[];
{
    struct hostent *ptrh;
    /*指向主机列表中一个条目的指针*/
    struct sockaddr_in servaddr;
    /*存放服务器网络地址的结构*/
    struct sockaddr_in clientaddr;
    /*存放客户网络地址的结构*/
    int listenfd;
    /*监听套接字描述符*/
    int clientfd;
    /*响应套接字描述符*/
    int port;
    /*协议端口号*/
    int alen;
    /*地址长度*/
    char buf[1000];
    /*供服务器发送字符串所用的缓冲区*/
    
    memset( (char*)& servaddr, 0, sizeof(servaddr) );  /* 清空sockaddr结构 */
    servaddr.sin_family = AF_INET;               /* 设置为因特网协议族 */
    servaddr.sin_addr.s_addr = INADDR_ANY;      /* 设置本地IP地址 */

    /* 检查命令行参数,如果指定了,就是用该端口号,否则使用缺省端口号 */
    if (argc > 1){
        port = atoi(argv[1]);        /* 如果指定了端口号,就将它转换成整数 */
    } else {
        port = PROTOPORT;       /* 否则,使用缺省端口号 */
    }

    if (port > 0)                 /* 测试端口号是否合法 */
        servaddr.sin_port=htons( (u_short)port );
    else{                        /* 打印错误信息并退出 */
        fprintf( stderr, "bad port number %s\n", argv[1] );
        exit(1);
    }

    /* 创建一个用于监听的流式套接字 */
    listenfd = SOCKET(AF_INET,SOCK_STREAM,0);
    if (listenfd <0) {
        fprintf( stderr, "socket creation failed\n" );
        exit(1);
    }

    /* 将本地地址绑定到监听套接字*/
    if ( bind( listenfd, (struct sockaddr *)& servaddr, sizeof(servaddr)) < 0) {
        fprintf(stderr, "bind failed\n" );
        exit(1);
    }

    /* 开始监听,并指定监听套接字请求队列的长度 */
    if (listen(listenfd, QLEN) < 0) {
        fprintf(stderr, "listen filed\n" );
        exit(1);
    }

    /* 服务器主循环—接受和处理来自客户端的连接请求 */
    while(1) {
        alen = sizeof(clientaddr);  /* 接受客户端连接请求,生成响应套接字 */
        if((clientfd = accept( listenfd, (struct sockaddr *)& clientaddr, &alen)) < 0 ) {
            fprintf( stderr, "accept failed\n");
            exit(1);
        }
        visits++;                       /* 累加访问的客户数 */
        sprintf( buf, "this server has been contacted  %d  time \n", visits );                 send(clientfd, buf, strlen(buf), 0 );  /* 向客户端发送信息 */
        closesocket( clientfd );           /* 关闭响应套接字 */
    }
}

说明:

1 、客户机代码为何反复的调用recv来获取数据?

SOCK_STREAM套接字不保证每个recv调用返回的数据恰好是服务器在send调用中所发送的数据量。

2 、阻塞式套接字函数与进程阻塞

当应用进程调用阻塞式套接字函数时,如不能直接返回,则进程暂停执行直至函数结束而返回,进程才能继续运行下去。

当套接字函数不能及时完成而返回,进程就因此而处于等待状态,等待时间取决于套接字过程。当进程处于这种等待状态时,就说明该进程被阻塞了。

进程因调用recv()而被阻塞

image-20231212165513766

计算机中运行着 A 、B 与 C 三个进程,其 中进程 A 执行网络程序,一开始,这 3 个 进程都被操作系统的工作队列所引用,处 于运行状态,会分时执行。

image-20231212165524811

当进程 A 执行到创建 Socket 的语句时, 操作系统会创建一个由文件系统管理的 Socket 对象

image-20231212165533991

当程序执行到 Recv 时,操作系统会将进程 A 从工作队列移动到该 Socket 的等待队列中。 由于工作队列只 剩下了进程 B 和 C ,依据进程调度,CPU 会轮流执行 这两个进程的程序,不会执行进程 A 的程序。所以进 程 A 被阻塞,不会往下执行代码,也不会占用 CPU 资源。

计算机收到了对端传送的数据,数据经由网卡传送到内存, 然后网卡通过中断信号通知 CPU 有数据到达,CPU 执行 中断程序,此处的中断程序主要有两项功能,先将网络数据写入到对应 Socket 的接收缓冲区里面,再唤醒进程 A,重新将进程 A 放入工作队列中。

image-20231212165611114

当 Socket 接收到数据后,操作系统将该 Socket 等待队列 上的进程重新放回到工作队列,该进程变成运行状态,继 续执行代码。同时由于 Socket 的接收缓冲区已经有了数据, Recv 可以返回接收到的数据。

服务器进程因调用ACCEPT()而被阻塞

image-20231212165632960

代码差错处理

在C语言中,在调用API函数发生异常时,一般会将errno变量(需要包含errno.h头文件)赋一个整数值,不同的值表示不同的含义,可以通过查看该值推测出错的原因。但是errno是一个数字,代表的具体含义还要到errno.h中去阅读宏定义,而每次查阅是一件很繁琐的事情。有下面几种方法可以方便地得到错误信息:

  1. perror函数:

    perror()用来将上一个函数发生错误的原因输出到标准错误(stderr),参数s所指的字符串会先打印出,后面再加上错误原因字符串。此错误原因依照全局变量errno的值来决定要输出的字符串。

  2. strerror函数:

    strerror函数用于将错误代码转换为字符串错误信息,可以将该字符串和其他的信息组合输出到用户界面。例如

    fprintf(stderr, "error in CreateProcess %s", strerror(errno))

  3. printf("%m", errno):

    这种方式也可以用于获取错误信息。

  4. 使用h_errno:

    使用gethostbyname()gethostbyaddr()函数时,不能使用perror()来输出错误信息,因为错误代码存储在h_errno中而不是errno中。h_errno也是一个外部整型变量。所以,需要调用herror()函数。需要包含头文件netdb.h

    void herror(const char *s);

    herror()用来将上一个网络函数发生错误的原因输出到标准错误(stderr)。参数s所指的字符串会先打印出,后面再加上错误的原因字符串。此错误原因系依照全局变量h_errno的值来决定要输出的字符串。

GetLastError()函数的使用

在Win32平台上,可以使用GetLastError()函数来获取上一个函数操作时所产生的错误代码。通过错误代码,可以在winerror.h头文件中查找到每一中错误代码所代表的含义。你也可以使用VC++自带的Error Lookup工具来查找其所表示的含义,结果是一样的。

函数签名:

DWORD GetLastError(void);

这是一个没有参数的函数,通过调用,就返回一个32位的数值。

描述:

GetLastError()函数是一个没有参数的函数,调用它将返回一个32位的数值,即上一个函数的错误代码。这个错误代码可以用来诊断和处理函数操作中的错误情况。

2.3.4 进程的阻塞问题和对策

1.什么是阻塞阻塞

阻塞是指一个进程执行了一个函数或者系统调用,该函数由于某种原因不能立即完成, 因而不能返回调用它的进程,导致进程受控于这 个函数而处于等待的状态,进程的这种状态称为阻塞。

2. 能引起阻塞的套接字调用

在Berkeley套接字网络编程接口的模型中,套接字的默认行为是阻塞的,具 体地说,在一定情况下,有多个操作套接字的系统调用会引起进程阻塞。

(1)ACCEPT()(2)READ()、RECV()和READFORM()(3)WRITE()、SEND()和SENDTO()(4)CONNECT()(5)SELECT()(6)CLOSESOCKET()

3. 阻塞工作模式带来的问题

采用阻塞工作模式的单进程服务器是不能很好地同时为多个客户服务的

4. 一种解决方案

利用UNIX操作系统的fork()系统调用编写多进程并发执行的服务器程序

你可以使用UNIX操作系统的fork()系统调用来编写多进程并发执行的服务器程序。这允许你创建多个子进程,为每个客户端提供专门的服务。通过并发执行的进程,你可以实现对多个客户端的并发服务。下面是一些基本的编程框架:

#include <sys/types.h> // 提供类型pid_t的定义
#include <unistd.h>    // 提供函数的定义
pid_t fork(void);

fork系统调用的作用是复制一个进程。当一个进程调用它后,会创建一个几乎与原始进程相同的新进程。这意味着你会有两个几乎一模一样的进程,一个是原始进程,另一个是新进程。这也是为什么fork的名字与叉子的形状有点相似,因为它分叉了进程的工作流程。

/* fork_test.c */
#include <sys/types.h>
#include <unistd.h>

int main() {
    pid_t pid; /* 此时仅有一个进程 */
    pid = fork(); /* 此时已经有两个进程在同时运行 */
    
    if (pid < 0)
        printf("Error in fork!\n");
    else if (pid == 0)
        printf("I am the child process, my process ID is %d\n", getpid());
    else
        printf("I am the parent process, my process ID is %d\n", getpid());
        
    return 0;
}

这个程序会创建两个进程,一个父进程和一个子进程,它们将同时运行。父进程会输出它自己的进程ID,而子进程会输出它自己的进程ID。你可以编译并运行这个程序来看到输出结果。

编译并运行程序的示例命令:

$ gcc fork_test.c -o fork_test
$ ./fork_test

输出结果可能会类似于:

I am the parent process, my process ID is 1991
I am the child process, my process ID is 1992

这表明父进程和子进程都在同时运行,并且它们有不同的进程ID。

看这个程序的时候,头脑中必须首先了解一个概念:在语句pid=fork()之前,只有一个进程在执行这段代 码,但在这条语句之后,就变成两个进程在执行了,这两个进程的代码部分完全相同,将要执行的下一 条语句都是if(pid==0) … …。

两个进程中,原先就存在的那个被称作“父进程 ”,新出现的那个被称作“子进程 ”。父子进程的区别 除了进程标志符(process ID)不同外,变量pid的值也不相同,pid存放的是fork的返回值。fork调用的一 个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:

在父进程中,fork返回新创建子进程的进程ID; 在子进程中,fork返回0; 如果出现错误,fork返回一个负值;

fork出错可能有两种原因:

(1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为 EAGAIN 。

(2)系统内存不足,这时errno的值被设置为ENOMEM。

解除阻塞的方法

当第一次调用 socket() 建立套接口描述符的时候,内核就将相关的函数设置为阻塞。如果不想套接口阻塞,就要调用函数 fcntl()

#include <unistd.h>
#include <fcntl.h>

sockfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(sockfd, F_SETFL, O_NONBLOCK);

通过设置套接口为非阻塞,能够有效地“询问”套接口以获得信息。如果尝试着从一个非阻塞的套接口读信息并且没有任何数据,这时不会变成阻塞--将返回 -1 并将 errno 设置为 EWOULDBLOCK

但是一般说来,轮询不是个好主意。如果让程序在忙等状态查询套接口的数据,将浪费大量的 CPU 时间。更好的解决之道是用 select() 去查询是否有数据要读进来。

select()--多路同步 I/O

select()函数可以同时监视多个套接字。它会告诉你哪个套接字已准备好读取,哪个准备好写入,哪个套接字发生了异常。

函数签名

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

返回值

  • 负值:select() 函数发生错误。

  • 正值:某些文件描述符(套接字)已准备好读取、写入或发生错误。

  • 0:等待超时,没有可读、可写或出错的文件描述符。

参数

  • numfds:文件描述符集中所有文件描述符的范围,即所有文件描述符的最大值加1。

  • readfds:用于监视可读文件描述符的集合。

  • writefds:用于监视可写文件描述符的集合。

  • exceptfds:用于监视异常文件描述符的集合。

注意

当函数 select() 返回时,readfds 的值会被修改,以反映哪个文件描述符可以读取。可以使用宏 FD_ISSET() 来测试。

  • FD_ZERO(fd_set *set):清除一个文件描述符集合,将集合中的所有文件描述符清零。

  • FD_SET(int fd, fd_set *set):将文件描述符 fd 添加到集合中,表示你要监视这个文件描述符的状态。

  • FD_CLR(int fd, fd_set *set):从集合中移除文件描述符 fd,表示你不再监视这个文件描述符的状态。

  • FD_ISSET(int fd, fd_set *set):测试文件描述符 fd 是否在集合中,如果在集合中返回非零值(true),否则返回零值(false)。

使用 struct timeval 和操作 fd_set 集合

结构体 struct timeval

在多路复用操作中,有时你不想永远等待,而是希望在一定时间内执行某个操作。结构体 struct timeval 允许你设置一个等待的时间,如果在这个时间内没有发生某种事件,就可以继续处理其他任务。

struct timeval {
    int tv_sec;     /* seconds */
    int tv_usec;    /* microseconds */
};

你可以将 tv_sec 设置为你要等待的秒数,将 tv_usec 设置为你要等待的微秒数。当函数返回时,timeout 可能是剩余的时间。

• 标准的 Unix 系统的时间片是100毫秒,所以无论如何设置struct timeval ,都要等待 那么长的时间。

• 如果设置 struct timeval 中的数据为 0 ,select() 将立即超时,这样就可以有效地轮询集合中的所有的文件描述符。如果将参数 timeout 赋值为 NULL ,那么将永远不会 发生超时,即一直等到第一个文件描述符就绪。

• 如果你有一个正在侦听 (listen()) 的套 接字,你可以通过将该套接字的文件描述符 加入到 readfds 集合中来看是 否有新的连接。

使用 fd_set 集合

在多路复用中,你可以使用 fd_set 集合来监视多个文件描述符的状态,然后选择性地等待它们的状态变化。以下是一些操作 fd_set 集合的宏:

  • FD_ZERO(fd_set *set):清除一个文件描述符集合,将集合中的所有文件描述符清零。

  • FD_SET(int fd, fd_set *set):将文件描述符 fd 添加到集合中,表示你要监视这个文件描述符的状态。

  • FD_CLR(int fd, fd_set *set):从集合中移除文件描述符 fd,表示你不再监视这个文件描述符的状态。

  • FD_ISSET(int fd, fd_set *set):测试文件描述符 fd 是否在集合中,如果在集合中返回非零值(true),否则返回零值(false)。

这些宏通常用于设置要监视的文件描述符,然后通过 select() 函数等来等待这些文件描述符的状态变化。例如,在以下示例中,我们使用 select() 来等待多个套接字的状态变化:

fd_set fdread;
timeval tv = {1, 0};
while (1) {
    // 初始化 fd_set
    FD_ZERO(&fdread);
    for (int i = 0; i < nSock; i++)
        FD_SET(socks[i], &fdread);
    
    // 等待事件触发,或超时返回
    int ret = select(numfds, &fdread, NULL, NULL, &tv);
    for (int i = 0; ret > 0 && i < nSock; i++)
        // 检测哪个 sock 有事件触发
        if (FD_ISSET(socks[i], &fdread)) {
            read_buf(socks[i]);
            ret--;
        }
}

在Linux中,fd_set 结构体通常通过位域来表示文件描述符的集合。每个位域表示一个文件描述符,而一个 unsigned long 整数可以包含多个位域。这使得 fd_set 可以表示大量的文件描述符。

2.4 无连接的套接字编程

2.4.1 无连接的套接字编程的两种模式

使用数据报套接字开发网络应用程序,既可以采用客户/ 服务器模式,也可以采用对等模式。

2.4.2 两个专用的系统调用

1. 发送数据报SENDTO()

int SENDTO( int sockfd, const void* msg, int len, unsigned int flags, struct sock a ddr* to, int tolen);

2. 接收数据报 RECVFROM()

int RECVFROM( int sockfd, void* bu f, int len, unsigned int flags, struct sock a ddr* from, int* fromlen )

2.4.3 数据报套接字的对等模式编程实例

课本45

2.5 原始套接字

利用原始套接字可以绕过传输层直接访问IP协议、ICMP协议和IGMP协议.

目前,只有Winsock2提供了对原始套接字的支持,并将原始套接字称为 SOCK_RAW类型,操作系统应使用Windows2000以上版本。

1、原始套接字的创建

格式一:

int sock Raw =socket(AF_INET,SOCK_RAW,protocol)

格式二:

SOCKET sock Raw =WSASocket(AF_INET,SOCK_RAW,protocol,NULL,0,0)

其中 ,参数protocol用来指定协议类型 , 可取如下值:

IPPROTO_ICMPICMP协议IPPROTO_UDPUDP协议
IPPROTO_IGMPIGMP协议IPPROTO_IPIP协议
IPPROTO_TCPTCP协议IPPROTO_RAW原始IP

2.原始套接字的使用

设置套接字选项

在使用原始套接字时,你可能需要根据需要设置套接字选项,以满足你的特定需求。你可以使用 setsockopt 函数来设置套接字选项。

int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
  • sock:要设置或获取选项的套接字。

  • level:选项所在的协议层。.可以取三种值: SOL_SOCKET:通用套接字选项 IPPROTO_IP:IP选项 IPPROTO_TCP:TCP选项.

  • optname:指定控制的方式(选项的名称)

  • optval:设置套接字选项 , 根据选项名称的数据类型进行转换

  • optlen:选项值的长度。

使用 setsockopt 函数,你可以根据需要配置套接字的选项,以满足你的通信需求。这可以包括设置套接字的各种属性,例如缓冲区大小、超时设置、广播选项等。

选项名称说明数据类型
IPPROTO_IP
IP_HDRINCL在数据包中包含 IP 首部int
IP_OPTIONSIP 首部选项int
IP_TOS服务类型int
IP_TTL生存时间int
IPPRO_TCP
TCP_MAXSEGTCP最大数据段的大小int
TCP_NODELAY不使用Nagle算法int

(2) 调用connect和bind函数来绑定对方和本地地址

(3) 发送数据包

如果没有使用connect函数绑定对方地址 ,则应使用sendto或send msg函数发送数据包。

如果调用了connect函数 ,则可以直接使用send、write来发送数据包。

如果设置了IP_HDRINCL选项 ,则包内要填充的内容为数据部分和IP首部 , 内 核只负责填充数据包的标志域和首部校验和。

注意 , IP数据包首部各个域的内容是网络字节序

以下是关于接收数据包的信息的Markdown格式输出:

(4)接收数据包

接收数据包的内核遵循以下规则:

  1. UDP 和 TCP 数据包通常不会传递给原始套接字。要查看这两种数据包,需要通过直接访问数据链路层来实现。

  2. 大多数 ICMP 数据包的一个拷贝会传递给匹配的原始套接字,因此可以使用原始套接字来查看 ICMP 数据包。

  3. 所有内核不能识别的协议类型的 IP 数据包都传送给匹配的原始套接字。内核只会进行必要的检验。

在将一个 IP 数据包传送给原始套接字之前,内核需要选择匹配的原始套接字,这需要满足以下条件:

  1. 数据包的协议域必须与接收原始套接字的协议类型匹配。

  2. 如果原始套接字调用了 bind 函数绑定了本地 IP 地址,则到达的 IP 数据包的源 IP 地址必须与绑定的本地 IP 地址相匹配。

  3. 如果原始套接字调用了 connect 函数指定了对方的 IP 地址,则到达的 IP 数据包的源 IP 地址必须与指定的对方 IP 地址相同。

第三章

3.1 Windows Sockets规范

3.1.1 概述

Microsoft公司以Berkeley Sockets规范为范例,定义了Windows Socktes 规范,简称Winsock规范。这是Windows操作系统环境下的套接字网络应用程序编程接口(API)。

Winsock以库函数的方式实现。不仅包括了Berkeley Sockets风格的库函数,也包括了一组针对Windows操作系统的扩展库函数,使编程者能充分利用Windows操作系统的消息驱动机制进行编程。


阻塞与非阻塞

菜没好,要不要死等->数据就绪前要不要等待?

阻塞:没有数据传过来时,读会阻塞直到有数据;缓冲区满时,写操作也会阻塞。非阻塞遇到这些情况,都是直接返回。

同步与异步

菜好了,谁端->数据就绪后,数据操作谁完成?

数据就绪后需要自己去读是同步,数据就绪直接读好再回调给程序是异步。


同步阻塞I/O(BIO)伪异步I/O非阻塞I/O(NIO)异步I/O(AIO)
客户端个数:I/O线程1:1M : N ( 其 中 M 可以大于N)"M:1(1个I/O线程处理多个客户端连接)M:0(不需要启动额外的1/O线程,被动回调)
I/O类型(阻塞)阻塞I/O阻塞I/O非阻塞I/O非阻塞I/O
I/O类型(同步)同步I/O同步I/O同步I/O(I/O多路复用)异步I/O
API使用难度简单简单非常复杂复杂
调试难度简单简单复杂复杂
可靠性非常差
吞吐量

3.1.2 Windows Sockets规范

Windows Sockets 规范是一套开放的、支持多种协议的Windows下的网络编程接口。目前, Windows Sockets从1.0版发展到了2.2版,已成为Windows网络编程的事实上的标准。

1.Windows Sockets 1.1版本

在Winsock.h包含文件中,定义了所有WinSock 1.1版本库函数的语法、相关的符号常量和数据结构。库函数的实现在WINSOCK.DLL动态链接库文件中。

(1)WinSock 1.1 全面继承了Berkeley Sockets规范,见表 3.1

(2)数据库函数 表3.2列出了Winsock规范定义的数据库查询例程。其中六个采用getXbyY()的形式,大多要借助网络上的数据库来获得信息。 getXbyY()函数都返回一个指针,指向某种数据结构区域,这些结构区域是由Winsock实现分配的,用来放置函数返回的数据信息。

要注意:这些指针指向的数据结构数据是易失的,它们只在该线程的下一个WinSock API调用之前有效。在一个线程中,只有一份这个结构的副本。因此,应该在发出下一个WinSock API调用之前把所需的信息复制出来。

(3)WinSock 1.1 扩充了Berkeley Sockets规范 针对微软 Windows的特点,WinSock 1.1定义了一批新的库函数,提供了对于消息驱动机制的支持,有效地利用Windows多任务多线程的机制。见表3.3

这些扩充主要是提供了一些异步函数,并增加了符合Windows消息驱动特性的网络事件异步选择机制。

(4)WinSock 1.1只支持TCP/IP协议栈

2.WinSock 2.2

WinSock 2.2在源码和二进制代码方面与WinSock 1.1兼容,WinSock 2.2增强了许多功能。

(1)支持多种协议:通过SPI( Service Provider Interface )接口支持其他协议,例如AppleTalk、Novell与Xerox等。

(2)使用事件对象异步通知

(3)引入了重叠I/O的概念:增强I/O吞吐量与提高性能。

(4)服务的质量(QOS):例如使用WSAConnect函数提出连接请求时可以指定所要求的QoS。

注: 重叠 I/O是WIN32的一项技术,可以要求操作系统为你传送数据,并且在传送完毕时通知你。这项技术使你的程序在I/O进行过程中仍然能够继续处理事务。

(5)套接口组:允许应用程序通知底层的服务提供者一组特定的套接口是相关的。套接口组的特性包括了组内单个套接口之间的相关特性和整个组的服务规范的特性。

(6)扩展的字节顺序转换例程:可以针对不同的协议需求进行多种字节序的转换,具体的字节顺序由PROTOCL_INFO结构指明。

(7)分散/聚集方式I/O:可以在单次系统调用中对多个缓冲区输入输出的方法,可以把多个缓冲区的数据写到单个数据流,也可以把单个数据流读到多个缓冲区中。这种输入输出方法也称为向量 I/O。与之不同,标准读写系统调用(read,write)可以称为线性I/O(linear I/O)。

(8)新增了许多函数。

3.WinSocket 1.1中的阻塞问题

阻塞是在把应用程序从Berkeley套接口环境中移植到Windows环境中的一个主要焦点。

在Berkeley套接口模型中,一个套接口的操作的缺省行为是阻塞方式的,除非程序员显式地请求该操作为非阻塞方式。

在Windows环境下,推荐程序员在尽可能的情况下使用非阻塞方式(异步方式)的操作。因为非阻塞方式的操作能够更好地在Windows环境下工作。

Windows早期的版本(3.1版本)是非抢先的多任务环境,即若一个程序不主动放弃其控制权,别的程序就不能执行。因此在设计Windows Sockets 程序时,尽管系统支持阻塞操作,但还是反对程序员使用该操作。但由于 SUN 公司下的 Berkeley Sockets 的套接字默认操作是阻塞的,WINDOWS 作为移植的 SOCKETS 也不可避免对这个操作支持。

Windows Sockets为了实现当一个应用程序的套接字调用处于阻塞时,能够放弃CPU让其它应用程序运行,它在调用处于阻塞时便进入一个叫“HOOK”的例程,此例程负责接收和分配WINDOWS消息,使得其它应用程序仍然能够接收到自己的消息。

在Windows Sockets实现中,对于不能立即完成的阻塞操作做如下处理:DLL初始化→循环操作。在循环中,它可以处理任何 WINDOWS 消息,并检查这个Windows Sockets调用是否完成(WSAIsBlocking函数),在必要时,它可以放弃CPU让其它应用程序执行。可以调用WSACancelBlockingCall函数取消此阻塞操作。

一个阻塞操作尚未完成之前,除了 WSACancelBlockingCall() 和WSAIsBlocking() 函数,不允许应用程序再调用其他任何WinSock函数,如果某个WinSock函数在此时被调用,则会失败并返回错误代码WSAEINPROGRESS。  

阻塞操作的循环处理步骤:

(1)调用 BlockingHook 函数 。

(2)检查使用者是否已调用 WSACancelBlockingCall() 取消了当前WinSock函数的调用

(3)检查当前WinSock函数的调用是否已完成了?

  for(;;) {            
     /* flush messages for good user response */           
      while(BlockingHook())              
      ;            
      /* check for WSACancelBlockingCall()*/          
      if(operation_cancelled())               
      break;            
      /* check to see if operation completed */          
      if(operation_complete())               
      break; /* normal completion */            
      }

缺省的BlockingHook()函数如下:

 BOOL DefaultBlockingHook(void) {
   MSG msg;
   BOOL ret;
   /* get the next message if any */
  ret = (BOOL)PeekMessage(&msg,NULL,0,0,PM_REMOVE);
   /* if we got one,process it */
  if (ret) {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
   /* TRUE if we got a message */
   return ret;
 }
阻塞处理例程(阻塞钩子函数) BlockingHook()

在 Windows Socket 中,阻塞处理例程(阻塞钩子函数) BlockingHook() 简单地获取并发送WINDOWS 消息。如果要对复杂程序进行处理,Windows Sockets 中还有WSASetBlockingHook() 提供用户安装自己的阻塞处理例程能力;与该函数相对应的则是WSAUnhookBlockingHook(),它用于删除先前安装的任何阻塞处理例程,并重新安装默认的处理例程。

WSASetBlockingHook():建立应用程式指定的 blocking hook 函数。

格式:

 FARPROC PASCAL FAR WSASetBlockingHook( FARPROC  lpBlockFunc)

参数:lpBlockfunc指向要装设的 blocking hook 函式的位址

传回值: 指向前一个blocking hook函式的位址

说明: 此函式可以设定自己的 Blocking Hook 函数。被设定的函数将会在应用程序呼叫到“blocking”动作时执行。唯一可在指定的 blocking hook 函数中调用的 Winsock 函数只有WSACancelBlockingCall()

假设设计了一个 Blocking Hook 函数叫myblockinghook(),那么在程序中向 Winsock 系统注册的方法如下:(其中_hInst代表此 task 的 Instance)

 FARPROC lpmybkhook = NULL;
 lpmybkhook = MakeProcInstance( (FARPROC)myblockinghook, _hInst) );
 WSASetBlockingHook( (FARPROC)lpmybkhook );

在设定自己的 Blocking Hook 函数后,仍可以利用WSAUnhookBlockingHook()函数来取消设定的 Blocking Hook函数,而变更回原先系统默认的Blocking Hook 函数。

WSAUnhookBlockingHook():复原系统预设的 blocking hook 函数。

格式:

  int PASCAL FAR WSAUnhookBlockingHook( void )

参数:

传回值: 成功 - 0

失败 - SOCKET_ERROR (通过WSAGetLastError() 可得知原因)

说明: 此函数取消使用者设定的 blocking hook函数,而恢复系统原先预设的 blocking hook函数。

注意:设计自己的阻塞处理例程时,除了函数WSACancelBlockingHook()之外,它不能使用其它的 Windows Sockets API函数。在处理例程中调用WSACancelBlockingHook()函数将取消处于阻塞的操作,它将结束阻塞循环。

只有在以下条件为真的时候,Windows Sockets DLL才调用阻塞钩子函数:历程是被定义为可以阻塞的,指定的套接口也是阻塞套接口,而且请求不能立刻完成。

说明:一个应用程式所设定的 Blocking Hook 函式,只会被这个应用程式所使用;其他的应用程式并不会执行到您设定的 Blocking Hook 函式的。

3.1.3 WinSock规范与Berkeley套接口的区别

1.套接口数据类型和该类型的错误返回值

在UNIX中,包括套接口句柄在内的所有句柄,都是非负的短整数,在WinSock规范中定义了一个新的数据类型,称作SOCKET,用来代表套接字描述符。

 typedef   u_int   SOCKET;

SOCKET可以取从0到INVALID_SOCKET-1之间的任意值。

在socket()例程和accept()例程返回时,检查是否有错误发生就不应该再使用把返回值和-1比较的方法,或判断返回值是否为负(这两种方法在BSD中都是很普通,很合法的途径)。取而代之的是,一个应用程序应该使用常量INVALID_SOCKET,该常量已在WINSOCK.H中定义。

例如:

 典型的BSD风格:
          s = socket(...);
          if (s == -1)    /* of s<0 */
              {...}
 更优良的风格:
          s = socket(...);
          if (s == INVALID_SOCKET)
              {...}

2.select()函数和FD_*

select函数的实现有一些变化,每一组套接口仍然用fd_set类型代表,但它并不是一个位掩码。整个组的套接口是用了一个套接口数组来实现的。

 typedef struct fd_set{    
 ​
               u_int fd_count;                 // how many are SET     
 ​
               SOCKET fd_array[FD_SETSIZE]; // an array of SOCKETs,默认容量为64
 ​
      } fd_set;

3.错误代码的获得

在UNIX 套接字规范中,如果函数执行时发生了错误,会把错误代码放到errnoh_errno变量中。

在Winsock中,错误代码统一使用WSAGetLastError()调用得到。

 例如:
 典型的BSD风格:
         r = recv(...);
         if (r == -1
             && errno == EWOULDBLOCK)
            {...}
 更优良的风格:
         r = recv(...);
         if (r == -1                              
             && WSAGetLastError() == EWOULDBLOCK)
            {...} 
 虽然为了兼容性原因,错误常量与4.3BSD所提供的一致;应用程序应该尽可能地使用“WSA”系列错误代码定义。例如,一个更准确的上面程序片断的版本应该是:
         r = recv(...);
         if (r == -1
             && WSAGetLastError() == WSAEWOULDBLOCK)
            {...}

4.指针

所有应用程序与Windows Sockets使用的指针都必须是FAR指针。为了方便应用程序开发者使用,Windows Sockets规范定义了数据类型LPHOSTENT

near指针和far指针的区别

在DOS下(实模式)地址是分段的,每一段的长度为64K字节,刚好是16位(二进制的十六位)。

near指针的长度是16位的,所以可指向的地址范围是64K字节,通常说near指针的寻址范围是64K。

far指针的长度是32位,含有一个16位的基地址和16位的偏移量,将基地址乘以16后再与偏移量相加(所以实际上far指针是20位的长度)即可得到far指针的1M字节的偏移量。所以far指针的寻址范围是1M字节,超过了一个段64K的容量。

例如:一个far指针的段地址为0x7000,偏移量为0x1244,则该指针指向地址0x71224。如果一个far指针的段地址是0x7122,偏移量为0x0004,则该指针也指向地址0x71224。

在DOS下,如果没有指定一个指针是near或far,那么默认是near。所以far指针要显式指定。在Win32的保护模式默认的是far指针。

什么时候使用far指针?

当使用小代码或小数据存储模式(small)时,不能编译一个有很多代码或数据的程序。因为在64K的一个段中,不能放下所有的代码与数据。为了解决这个问题,需要指定以far函数或far指针来使用这部分的空间(64K以外的空间)。

5.重命名的函数

(1)close()改变为closesocket()

(2)ioctl()改变为ioctlsocket()

6.Winsock select函数支持的最大套接口数目

在WINSOCK.H中缺省值是64,在编译时由常量FD_SETSIZE决定。

7.头文件

一个Windows Sockets应用程序只需简单地包含winsock2.h就足够了。

8.支持原始套接口

9.Winsock规范对于消息驱动机制的支持

体现在异步选择机制、异步请求函数、阻塞处理方法、错误处理、启动和终止等方面。

3.2 Winsock库函数

 

WSAStartup初始化Windows_Sockets_API

应用程序

WSACleanup释放所使用的Windows_Sockets_DLL

3.2.1 Winsock的注册与注销

1.初始化函数WSAStartup()

Winsock 应用程序要做的第一件事,就是必须首先调用WSAStartup()函数对Winsock进行初始化。初始化也称为注册。注册成功后,才能调用其他的Winsock API函数。

(1)WSAStartup()函数的调用格式

  int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData );

参数wVersionRequested:指定应用程序所要使用的WinSock规范的版本。主版本号在低字节,副版本号在高字节。

参数lpWSAData:是一个指向WSADATA结构变量的指针,用来返回WinSock API实现的细节信息

(2)WSAStartup()函数的初始化过程

第一步:检查系统中是否有一个或多个Windows Sockets实现的实例。

第二步:检查找到的WinSock实现是否可用,主要是确认WinSock实现的版本号。

第三步:建立WinSock实现与应用程序的联系。

第四步:函数成功返回时,会在lpWSAData所指向的WSADATA结构中返回相关信息。

(3)WSADATA结构的定义

 // 定义常量 WSADESCRIPTION_LEN 为 256,用于描述缓冲区的最大长度
 #define WSADESCRIPTION_LEN    256
 ​
 // 定义常量 WSASYS_STATUS_LEN 为 128,用于系统状态缓冲区的最大长度
 #define WSASYS_STATUS_LEN    128
 ​
 // 定义结构体 WSADATA,用于存储关于Windows套接字库的信息
 typedef struct WSAData {
     WORD      wVersion;                     // 存储WinSock版本号的低字
     WORD      wHighVersion;                 // 存储WinSock版本号的高字
     char     szDescription[WSADESCRIPTION_LEN + 1];   // 存储描述信息的缓冲区,加1是为了存储字符串结束符'\0'
     char     szSystemStatus[WSASYS_STATUS_LEN + 1];   // 存储系统状态信息的缓冲区,加1是为了存储字符串结束符'\0'
     unsigned short iMaxSockets;             // 存储最大套接字数
     unsigned short iMaxUdpDg;               // 存储最大UDP数据报数
     char *   lpVendorInfo;                  // 指向供应商信息的指针
 } WSADATA;

wVersion为你将使用的Winsock版本号

wHighVersion为载入的Winsock动态库支持的最高版本,注意,它们的高字节代表次版本,低字节代表主版本。

szDescription返回描述WinSock实现和开发商标识信息的字符串。

szSystemStatus返回系统状态和配置信息的字符串。

iMaxSockets返回一个进程最多可以使用的套接字个数,其值依赖于可使用的硬件资源。

iMaxUdpDg表示数据报的最大长度。

lpVendorInfo是为Winsock实现而保留的制造商信息,这个在Windows平台上并没有什么用处。

(4)初始化函数可能返回的错误代码

 WSASYSNOTREADY:   
     网络通信依赖的网络子系统没有准备好。
 WSAVERNOTSUPPORTED:
     找不到所需的Winsock API相应的动态连接库。
 WSAEINVAL:       
     DLL不支持应用程序所需的Winsock版本。
 WSAEINPROGRESS:    
     正在执行一个阻塞的Winsock 1.1操作。
 WSAEPROCLIM:     
     已经达到Winsock支持的任务数上限。
  WSAEFAULT:       
     参数lpWSAData不是合法指针。 

(5)初始化Winsock的示例

 #include <winsock2.h>
 ​
 // 对于Winsock 1.0,应包括 Winsock.h 文件
 ​
 void aa() {
     WORD wVersionRequested; // 应用程序所需的Winsock版本号
     WSADATA wsaData;       // 用来返回Winsock实现的细节信息。
     int err;               // 出错代码。
 ​
     wVersionRequested = MAKEWORD(2, 2); // 生成版本号2.2。
     err = WSAStartup(wVersionRequested, &wsaData); // 调用初始化函数。
 ​
     if (err != 0) {
         return; // 通知用户找不到合适的DLL文件。
     }
 ​
     // 确认返回的版本号是客户要求的2.2
     if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
         WSACleanup();
         return;
     }
 ​
     // 至此,可以确认初始化成功,Winsock.DLL可用。
 }

2.注销函数WSACleanup()

当程序使用完Winsock.DLL提供的服务后,应用程序必须调用WSACleanup()函数,来解除与Winsock.DLL库的绑定,释放Winsock实现分配给应用程序的系统资源,中止对Windows Sockets DLL的使用。

  int WSACleanup ( void ); 

3.2.2 Winsock的错误处理函数

1.WSAGetLastError()函数

 int WSAGetLastError ( void );

本函数返回本线程进行的上一次Winsock函数调用时的错误代码(详见教材P77)。如果要显示错误代码对应的描述信息可调用函数FormatMessage 。

2.WSASetLastError()函数

 void WSASetLastError ( int iError );

本函数允许应用程序为当前线程设置错误代码,并可由后来的WSAGetLastError()调用返回。

3.2.3 主要的Winsock函数

1.创建套接口SOCKET()

 SOCKET  socket (int af, int type, int protocol);
 举例:
 SOCKET sockfd=SOCKET( AF_INET, SOCK_STREAM, 0); //创建一个流式套接字。
 SOCKET sockfd=SOCKET( AF_INET, SOCK_DGRAM, 0); // 创建一个数据报套接字。 

2.将套接口绑定到指定的网络地址BIND()

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

相关的三种Winsock地址结构

有许多函数都需要套接字的地址信息,像UNIX 套接字一样,Winsock也定义了三种关于地址的结构,经常使用。

①通用的Winsock地址结构,针对各种通信域的套接字,存储它们的地址信息。

 struct sockaddr {
 u_short sa_family;  /* 地址家族
 char sa_data[14];   /* 协议地址
 } 

②专门针对Internet 通信域的Winsock地址结构

 struct sockaddr_in {
     short sin_family;      // 指定地址家族,一定是 AF_INET.
     u_short sin_port;      // 指定将要分配给套接字的传输层端口号,
     struct in_addr sin_addr; // 指定套接字的主机的IP地址
     char sin_zero[8];      // 全置为0,是一个填充数。
 };

③专用于存储IP地址的结构

 struct in_addr {
     union {
         struct {
             u_char s_b1, s_b2, s_b3, s_b4;
         } S_un_b;
         struct {
             u_short s_w1, s_w2;
         } S_un_w;
         U_long S_addr;
     };
 };

在使用Internet域的套接字时,这三个数据结构的一般用法是:

首先:定义一个Sockaddr_in的结构实例变量,并将它清零。

然后:为这个结构的各成员变量赋值。

第三步:在调用bind()绑定函数时,将指向这个结构的指针强制转换为 sockaddr*类型。

 SOCKET serSock;                           // 定义了一个SOCKET类型的变量。
 struct sockaddr_in my_addr;              // 定义一个sockaddr_in类型的结构实例变量。
 int err;                                  // 出错码。
 int slen = sizeof(struct sockaddr);       // sockaddr结构的长度。
 ​
 serSock = socket(AF_INET, SOCK_DGRAM, 0);  // 创建数据报套接字。
 ​
 memset(&my_addr, 0, sizeof(struct sockaddr_in));
 // 将sockaddr_in的结构实例变量清零。
 ​
 my_addr.sin_family = AF_INET;             // 指定通信域是Internet。
 my_addr.sin_port = htons(21);             // 指定端口,将端口号转换为网络字节顺序。
 ​
 /* 指定IP地址,将IP地址转换为网络字节顺序。*/
 my_addr.sin_addr.s_addr = INADDR_ANY;
 ​
 /* 将套接字绑定到指定的网络地址,对&my_addr进行了强制类型转换。*/
 if (bind(serSock, (struct sockaddr *)&my_addr, slen) == SOCKET_ERROR) {
     /* 调用WSAGetLastError()函数,获取最近一个操作的错误代码。*/
     err = WSAGetLastError();
     /* 这里可以进行错误处理。*/
 }

3.启动服务器监听客户端的连接请求LISTEN()

 int  listen( SOCKET s, int backlog);

4.接收连接请求ACCEPT()

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

5.请求连接CONNECT()

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

举例:

 struct sockaddr_in daddr;                          // 定义一个sockaddr_in结构体变量daddr。
 memset((void *)&daddr, 0, sizeof(daddr));         // 初始化daddr结构体,将其内存清零。
 daddr.sin_family = AF_INET;                        // 指定通信域是IPv4。
 daddr.sin_port = htons(8888);                     // 指定连接的目标端口号,将端口号转换为网络字节顺序。
 daddr.sin_addr.s_addr = inet_addr("133.197.22.4"); // 指定连接的目标IP地址。
 ​
 // 使用connect函数来建立连接,将ClientSocket连接到目标地址(daddr)。
 connect(ClientSocket, (struct sockaddr *)&daddr, sizeof(daddr));

6.向一个已连接的套接口发送数据SEND()

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

image-20231212202356444

7.从一个已连接套接口接收数据RECV()

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

8.按照指定目的地向数据报套接字发送数据SENDTO()

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

9.接收一个数据报并保存源地址,从数据报套接字接收数据RECVFORM()

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

10.关闭套接字CLOSESOCKET()

 int closesocket( SOCKET s); 

11.禁止在一个套接口上进行数据的接收与发送SHUTDOWN()

 int shutdown( SOCKET s, int how); 

3.2.4 Winsock的辅助函数

1.Winsock中的字节顺序转换函数

Winsock API特为此设置了四个函数

(1)htonl()

将主机的无符号长整型数本机顺序转换为网络字节顺序 (Host to Network Long),用于IP地址。

 u_long PASCAL FAR htonl( u_long hostlong);  

hostlong是主机字节顺序表达的32位数。htonl()返回一个网络字节顺序的值。

(2)htons()

将主机的无符号短整型数转换成网络字节顺序(Host to Network Short),用于端口号。

 u_short PASCAL FAR htons( u_short hostshort);

hostshort:主机字节顺序表达的16位数。htons()返回一个网络字节顺序的值。

(3)ntohl()

将一个无符号长整型数从网络字节顺序转换为主机字节顺序。(Network to Host Long),用于IP地址。

 u_long PASCAL FAR ntohl( u_long netlong);  

netlong是一个以网络字节顺序表达的32位数,ntohl()返回一个以主机字节顺序表达的数。

(4)ntohs()

将一个无符号短整型数从网络字节顺序转换为主机字节顺序。(Network to Host Sort),用于端口号

 u_short PASCAL FAR ntohs( u_short netshort);

netshort是一个以网络字节顺序表达的16位数。ntohs()返回一个以主机字节顺序表达的数。

2.获取与套接口相连的外端地址GETPEERNAME()

 int getpeername( SOCKET s, struct sockaddr * name,  int * namelen); 

3.获取一个套接口的本地地址GETSOCKNAME()

 int getsockname( SOCKET s, struct sockaddr * name, int * namelen); 

4.将一个点分十进制形式的IP地址转换成一个长整型数INET_ADDR()

 unsigned long inet_addr (const char * cp); 

5.将网络地址转换成点分十进制的字符串格式INET_NTOA()

 char * inet_ntoa( struct  in_addr in); 

3.2.5 Winsock的信息查询函数

Winsock API提供了一组信息查询函数,让我们能方便地获取套接口所需要的网络地址信息以及其它信息,

(1)Gethostname()

用来返回本地计算机的标准主机名。

 int gethostname(char* name,  int namelen);

(2)Gethostbyname()

返回对应于给定主机名的主机信息。

 struct hostent*  gethostbyname(const  char* name); 

(3)Gethostbyaddr()

根据一个IP地址取回相应的主机信息。

 struct hostent* gethostbyaddr(const char* addr, int len, int type);

(4)Getservbyname()

返回对应于给定服务名和协议名的相关服务信息。

 struct servent* getservbyname(const char* name, const char* proto);

(5)Getservbyport()

返回对应于给定端口号和协议名的相关服务信息。

 struct servent * getservbyport(int port,    const char *proto); 

(6)Getprotobyname()

返回对应于给定协议名的相关协议信息。

 struct protoent *  getprotobyname(const char * name);

(7)Getprotobynumber ()

返回对应于给定协议号的相关协议信息。

 struct protoent * getprotobynumber(int number);

除了Gethostname()函数以外,其它六个函数有以下共同的特点:

①函数名都采用GetXbyY的形式。

②如果函数成功地执行,就返回一个指向某种结构的指针,该结构包含所需要的信息。

③如果函数执行发生错误,就返回一个空指针。应用程序可以立即调用WSAGetLastError()来得到一个特定的错误代码。

④函数执行时,可能在本地计算机上查询,也可能通过网络向域名服务器发送请求,来获得所需要的信息,这取决于用户网络的配置方式。

⑤为了能让程序在等待响应时能作其他的事情,Winsock API扩充了一组作用相同的异步查询函数,不会引起进程的阻塞。并且可以使用Windows的消息驱动机制。也是六个函数,与GetXbyY各函数对应,在每个函数名前面加上了WSAAsync前缀,名字采用WSAAsyncGetXByY()的形式。它们的工作机制在后面详述

3.2.6 WSAAsyncGetXByY类型的扩展函数

WSAAsyncGetXByY类型的扩展函数是GetXByY函数的异步版本,这些函数可以很好地利用Windows的消息驱动机制。 1.WSAAsyncGetHostByName()函数

 HANDLE WSAAsyncGetHostByName ( HWND hWnd,  unsigned int wMsg,
 const char * name,  char * buf,  int buflen );

说明:WSAAsyncGetHostByName函数在调用时要自定义一个消息作为参数,而等到获取了IP地址后程序会将此消息向主窗体发送,这就代表已经解析成功并返回了结果,这时候我们就可以在此消息底下读到相应的返回结果。

 #include <windows.h>
 #include <winsock.h>
 #pragma comment(lib, "WSock32.lib")
 ​
 #define WM_GETIPMSG WM_USER + 100
 ​
 // 声明全局变量
 WSADATA WSAData;
 HOSTENT HostEnt;
 HANDLE hAsync;
 BOOL bShow;
 ​
 // 窗口消息处理函数
 HRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
 {
     char* hostname;
     int len;
 ​
     switch (Msg)
     {
     case WM_CREATE:
         // 初始化WinSock库
         if (WSAStartup(MAKEWORD(2, 0), &WSAData) != 0)
         {
             // 如果初始化失败,则销毁窗口
             SendMessage(hWnd, WM_DESTROY, 0, 0);
             return 0;
         }
         len = 256;
         hostname = new char[len];
         // 获取本地主机名
         gethostname(hostname, len);
         // 异步获取主机IP信息
         hAsync = WSAAsyncGetHostByName(hWnd, WM_GETIPMSG, hostname, &HostEnt, MAXGETHOSTSTRUCT);
         delete[] hostname;
         bShow = FALSE;
         return 0;
 ​
     case WM_GETIPMSG:
         // 异步获取主机IP信息完成
         if (HostEnt.h_addr == NULL)
             return 0;
         bShow = TRUE;
 ​
     case WM_PAINT:
         // 显示IP地址
         HDC hDc;
         PAINTSTRUCT ps;
         hDc = BeginPaint(hWnd, &ps);
         if (bShow)
             TextOut(hDc, 0, 0, inet_ntoa(*(in_addr*)HostEnt->h_addr), strlen(inet_ntoa(*(in_addr*)HostEnt->h_addr)));
         EndPaint(hWnd, &ps);
         ReleaseDC(hDc);
         return 0;
 ​
     case WM_DESTROY:
         // 清理WinSock库并退出程序
         WSACleanup();
         PostQuitMessage(0);
         return 0;
     }
 ​
     return DefWindowProc(hWnd, Msg, wParam, lParam);
 }

2.WSAAsyncGetHostByAddr()函数

 HANDLE WSAAsyncGetHostByAddr ( HWND hWnd,  unsigned int wMsg,  
 const char * addr,  int len,  int type,  char * buf,  int buflen ); 

3.WSAAsyncGetServByName()函数

 HANDLE WSAAsyncGetServByName ( HWND hWnd, unsigned int wMsg,
 const char * name,  const char * proto,  char * buf,  int buflen );

4.WSAAsyncGetServByPort()函数

 HANDLE WSAAsyncGetServByPort ( HWND hWnd, unsigned int wMsg, 
 int port,  const char * proto,  char * buf,  int buflen );

5.WSAAsyncGetProtoByName()函数

 HANDLE WSAAsyncGetProtoByName ( HWND hWnd,  unsigned int wMsg, 
 const char * name,  char * buf,  int buflen );

6.WSAAsyncGetProtoByNumber()函数

 HANDLE WSAAsyncGetProtoByNumber ( HWND hWnd,  unsigned int wMsg,
 int number,  char * buf,  int buflen); 

3.3 Windows环境下的多路异步选择I/O

 int WSAAsyncSelect(SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent)  

该函数会自动将套接字设置为非阻塞模式,并且把发生在该套接字上且是你所感兴趣的事件,以Windows消息的形式发送到指定的窗口。应用程序需要做的就是在传统的消息处理函数中处理这些事件。

参数hWnd表示指定接受消息的窗口句柄;

参数wMsg表示消息码值(这意味着需要自定义一个Windows消息码);

参数IEvent表示你希望接受的网络事件的集合,它可以是如下值的任意组合:FD_READ, FD_WRITE,FD_OOB, FD_ACCEPT, FD_CONNECT, FD_CLOSE;

如果在某一套接字s上发生了一个已命名的网络事件,应用程序窗口hWnd会接收到消息wMsg。参数wParam即为该事件相关的套接字s;参数lParam的低字段指明了发生的网络事件,lParam的高字段则含有一个错误码,事件和错误码可以通过下面的宏从lParam中取出:

 #define WSAGETSELECTEVENT(lParam) LOWORD(lParam)
 #define WSAGETSELECTERROR(lParam) HIWORD(lParam) 

如何将上阻塞模式WinSock应用升级到非阻塞模式?

首先自定义一个Windows消息码,用于标识我们的网络消息。

 #define WM_CUSTOM_NETWORK_MSG (WM_USER + 100) 
 //服务器端,将监听套接字置为非阻塞模式,并且标明其感兴趣的事件为FD_ACCEPT。
 WSAAsyncSelect(server, wnd, WM_CUSTOM_NETWORK_MSG, FD_ACCEPT); 
 listen(server); 
 ​
 //客户端,将套接字置为非阻塞模式,并标明其感兴趣的事件为FD_CONNECT。
 WSAAsyncSelect(client, wnd, WM_CUSTOM_NETWORK_MSG, FD_CONNECT);
 connect(client, server); 
 //接着,在Windows消息处理函数中,将处理监听事件、连接事件、及读写事件,方便起见,这里将服务器和客户端的处理代码放在了一起。
 LRESULT CALLBACK  WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)    
 {    
     switch (message)    
     {
     case WM_CUSTOM_NETWORK_MSG: // 自定义的网络消息码    
         {    
             SOCKET socket = (SOCKET)wParam; // 发生网络事件的套接字    
             long event = WSAGETSELECTEVENT(lParam); // 事件    
             int error = WSAGETSELECTERROR(lParam); // 错误码    
     
             switch (event)    
             {    
             case FD_ACCEPT: // 服务器收到新客户端的连接请求    
                 {    
                     // 接收到客户端连接,分配一个客户端套接字    
                     SOCKET client = accept(socket);     
                     // 将新分配的客户端套接字置为非阻塞模式,并标明其感兴趣的事件为读、写及关闭    
                     WSAAsyncSelect(client, hWnd, message, FD_READ | FD_WRITE | FD_CLOSE);    
                 }    
                 break;    
             case FD_CONNECT: // 客户端连接到服务器的操作返回结果    
                 {    
                     // 成功连接到服务器,将客户端套接字置为非阻塞模式,并标明其感兴趣的事件为读、写及关闭    
                     WSAAsyncSelect(socket, hWnd, message, FD_READ | FD_WRITE | FD_CLOSE);    
                 }    
                 break;    
            
           case FD_READ: // 收到网络包,需要读取    
                 {    
                     /*使用套接字读取网络包
                     (一般来说,如果需要发送消息,直接调用send()发送即可。如果该次调用返回值为
                     SOCKET_ERROR且WSAGetLastError()得到错误码WSAEWOULDBLOCK,这意味着缓冲区已满
                     暂时无法发送,此刻我们需要将待发数据保存起来,等到系统发出FD_WRITE消息后尝试重新发送。) */   
                     recv(socket);    
                 }    
                 break;    
             case FD_WRITE:    
                 {    
                     //使用套接字写网络包    
                 }    
                 break;    
             case FD_CLOSE: // 套接字的连接方(而非本地socket)关闭消息    
                 {    
                    //关闭套接字    
                 }    
                 break;    
             default:    
                 break;    
             }    
         }    
         break;    
     …    
     }    
     …    
 }   

image-20231212203958307

构成MDI应用程序的各对象之间的派生关系

第四章

MFC类库是一种C++类库,构成了MFC编程框架。这些类分别封装了Windows应用程序编程接口、应用程序的概念。MFC类库中的类具有预定义的继承关系,也包含了虚拟函数和动态约束,并提供了MFC的开发模板。

软件框架通常包括一组已实现的、相互关联的软件模块和一套编程规范。

4.1 MFC概述

4.1.1 MFC是一个编程框架

MFC应用程序框架是由MFC(Microsoft Foundation Class Library)中的一组类组合起来构成的。

MFC框架从总体上定义了应用程序的原型,并为用户提供了标准的编程接口,程序员只须通过预定义的接口把具体应用特有的代码填入原型,就能快速建立Windows桌面应用程序。

MFC框架对MFC类、类的继承关系、类的组合关系及相互作用、动态约束等进行了高效封装。

Microsoft Visual C++提供了相应的工具来简化应用程序的开发过程:

  • 用应用程序向导(AppWizard)可以生成应用程序的骨架文件(代码和资源等);

  • 资源编辑器可以直观地设计用户接口;

  • 用类向导(ClassWizard)可以为类添加成员方法和成员变量;

  • 文件视图可以方查看类的定义文件和头文件。

1.MFC类库封装的内容

(1)对Windows应用程序编程接口的封装

MFC将每一个Windows对象封装成一个相应的C++ 对象。

例如:CWnd类是一个C++对象,它把Windows窗口对象的句柄(HWND)封装成CWnd类对象的成员变量m_hWnd,把操作该句柄的相关API函数封装成CWnd类对象的成员函数。

句柄:是整个Windows编程的基础。

句柄是指表示Windows对象的一个唯一的整数值,用来标识应用程序中的不同对象和同类对象中的不同的实例,如,一个窗口,按钮,图标,滚动条,输出设备,控件或者文件等。

句柄与指针又有所不同:例如窗口句柄HWND是可以跨进程可见的,而指针从来都是属于某个特定进程的。指针对应着一个数据在内存中的地址,得到了指针就可以自由地修改该数据。但不能使用句柄修改其对应对象内部数据结构。

描述Windows句柄FC Qbject
窗 口HWNDCWnd and CWnd-derived classes
设备上下 文HDCCDC and CDC-derived classes
菜单HKENJCJlenu
HPENCGdiObject类,CPen和CPen-derived classes
刷子HBRUSHCGdiObject类,CBrush和CBrush- derived classes
字体HFONTCGdiObject类,CFont和CFont- derived classes
位图HBITMAPCGdiObject类,CBitmap和CBitmap- derived classes
调色板HPALETTECGdiObject类,CPalette和CPalette- derived classes
区域HRGNCGdiObject类,CRgn和CRgn-derived classes
图像列表HimageLISTCimageList和CimageList-derived classes
套接字SOCKETCSocket,CAsynSocket及其派生类

(2)对应用程序概念的封装

使用SDK编写Windows应用程序时,总要定义窗口过程,注册Windows Class,创建窗口等等,要做许多处理工作。MFC封装了这些处理,替程序员完成这些工作。

MFC提出了以文档-视图为中心的编程模式,MFC类库封装了对它的支持。

(3)对COM/OLE特性的封装

OLE(对象的链接与嵌入)建立在COM(组件对象模型)之上,由于支持OLE的应用程序必须实现一系列的接口(Interface),因而相当繁琐。MFC的OLE类封装了OLE API大量的复杂工作,提供了实现OLE的更高级接口。

(4)对ODBC功能的封装

MFC封装了ODBC API的大量的复杂的工作,形成了与ODBC之间接口的高级C++类,提供了一种方便的访问数据库的编程模式。

注:开放数据库互连(ODBC)是MICROSOFT提出的数据库访问接口标准。ODBC定义了访问数据库的API一个规范,这些API独立于不同厂商的DBMS,也独立于具体的编程语言。

(5)对Winsock的封装

MFC中提供了多个对Winsock的封装类,包括CAsyncScoket,CSocket,WinInet类等,可以很方便实现异步或同步的套接字通信,以及开发典型应用层协议的客户端程序。

2.MFC中类的继承关系

MFC将众多类的共同特性抽象出来,设计出一些基类,作为实现其他类的基础。有两个类十分重要。

CObject是MFC的根类,绝大多数MFC类是从它派生的。CObject 实现了一些重要的特性,

包括运行时类型信息RTTI、运行时创建、对象序列化、对程序调试的支持等等。所有从CObject派生的类都将具备或者可以具备CObject所拥有的特性。

另一个是CCmdTarget类,它是从CObject派生的。CCmdTarget类通过进一步封装一些属性和方法,提供了消息处理的架构。在MFC中,任何可以处理消息的类都是从CCmdTarget类派生的。

针对每种不同的Windows对象,MFC都设计了一组类对这些对象进行封装,每一组类都有一个基类,从基类派生出众多更具体的类。这些对象包括以下种类:

  • 窗口对象,基类是CWnd;

  • 应用程序对象,基类是CWinThread;

  • 文档对象,基类是CDocument,等等。

程序员可以结合自己的实际问题,从适当的MFC类中派生出自己的类,实现特定的功能,达到自己的编程目的。

3.虚拟函数和动态约束

虚拟函数和消息映射,是MFC提供的两类主要的编程接口。程序员在继承基类的同时,可以把自己实现的虚拟函数和消息处理函数嵌入MFC的编程框架。MFC编程框架将在适当的时候、适当的地方来调用程序的代码。

其中,MFC建立的消息映射机制,以一种高效、便于使用的手段解决消息处理函数的动态约束问题。

在Windows程序中,对于消息的处理通常采用switch-case结构,但在程序要处理的消息较多的情况下,这个switch-case结构将会异常庞大。那么,对于不同的需求,在每个Windows程序中,开发者都需要去编写这个庞大的switch-case结构。如何改进呢?

根据面向对象的编程思想,可以提出这样一种思路:在基类中写出完整的switch语句,并通过虚函数机制让子类只覆盖感兴趣的代码

 class CWnd
 {
    …
    void onMessage(…)
   {
    switch(message)
     {
       case WM_COMMAND
          onCommand(&msg);
          break;
       case WM_PAINT
          onPaint(&msg);
          break;
       ……   
       }
    }
 virtual void onCommand(MSG &){};
 virtual void onPaint(MSG &){};
 …….
 }

在MFC中,实际并没有采用switch-Case结构的思路,原因在于太多的虚函数会带来内存的额外开销,而且Windows支持用户自定义消息,以上穷举的方案显然无法满足该需求。

MFC采用的是类似于map数据结构的方案来解决该问题-消息映射表

消息标识消息处理函数指针
WM_LBUTTONDOWNOn_LButtonDown
WM_LBUTTONDOWNOn_Paint
WM_LBUTTONDOWNOn_Restroy
 class CWnd
 {
   //消息映射表
   CMap<int, MessageHandler> _messageMap;
   
   //添加一条新的映射
    void AddEntry( int message, MessageHandler handler)
    {
       _messageMap[message]=handler;
     }
     void onMessage(…)
   {
      MessageHandler handler=_messageMap[message]
      handler();
    }
 }

在实际中,MFC将每一条消息映射用结构体AFX_MSGMAP_ENTRY结构来表示,其中nMessagepfn变量类似于前面的消息ID和操作函数。

 struct AFX_MSGMAP_ENTRY
 {
 UINT nMessage;    // windows message
 UINT nCode;         // control code or WM_NOTIFY code
 UINT nID;             // control ID (or 0 for windows messages)
 UINT nLastID;       // used for entries specifying a range of control id's
 UINT nSig;            // signature type (action) or pointer to message #
 AFX_PMSG pfn;    // routine to call (or special value)
 };

MFC为每个CWnd类都准备了一个消息映射变量,类型为AFX_MSGMAP

 struct AFX_MSGMAP
 {
 const AFX_MSGMAP*   (PASCAL* pfnGetBaseMap)();
 const AFX_MSGMAP_ENTRY*   lpEntries;
 };

AFX_MSGMAP主要作用是两个,

一:用来得到基类的消息映射表入口地址。

二:得到本身的消息映射表地址。

实际上,MFC把所有的消息一条条填入到AFX_MSGMAP_ENTRY结构中去,形成一个数组,该数组存放了所有的消息和与它们相关的参数。同时通过AFX_MSGMAP能得到该数组的首地址,同时得到基类的消息映射入口地址,这是为了当本身对该消息不响应的时候,就调用其基类的消息响应。

例如:B类是A类的子类,则经过消息映射后它们的消息映射表将形成一个链表结构

image-20231212205335645

4.MFC的开发模板 MFC实现了对应用程序概念的封装,实现了类、类的继承、动态约束、类的关系和相互作用的封装。这样封装的结果是为程序员提供了一套开发模板,罗列在应用程序向导AppWizard中。针对不同的应用和目的,程序员可以采用不同的模板。例如,对话框应用程序模板,SDI单文档应用程序模板,MDI多文档应用程序模板等。这些模板都采用以文档-视为中心的思想,每个模板都包含一组特定的类。

总之,MFC封装了Windows API,OLE API,ODBC API等底层函数的功能,并提供更高一层的接口,简化了Windows编程。同时,MFC支持对底层API的直接调用。

这种简化体现在MFC提供了一个Windows应用程序开发模式:

  • MFC框架完成对程序的控制,通过预定义或实现了许多事件和消息处理,来完成大部分编程任务。

  • MFC框架处理大部分事件,不依赖程序员的代码;

  • 程序员的代码集中用来处理应用程序特定的事件。

4.1.2 典型的MDI应用程序的构成

用AppWizard产生一个没有OLE等支持的MDI工程,工程名叫T。AppWizard会自动创建一系列文件,构成一个应用程序骨架。这些文件分为四类:

头文件(.h),实现文件(.cpp),资源文件(.rc),模块定义文件(.def)等。

(1)应用程序类CTApp

应用程序类CTApp派生于CWinApp类。基于框架的应用程序必须有且只有一个应用程序对象,它负责应用程序的初始化、运行和结束。

(2)主边框窗口类CMainFrame

对于MDI应用程序,从CMDIFrameWnd类派生主边框窗口类CMainFrame,CMDIChildWnd类派生文档边框窗口类CChildFrame,主边框窗口的客户区直接包含文档边框窗口。如果是SDI应用程序,从CFrameWnd类派生边框窗口类,边框窗口的客户区直接包含视窗口。

如果要支持工具条、状态栏,则派生的主边框窗口类还要添加CToolBar和CStatusBar类型的成员变量,并且要在一个OnCreate消息处理函数中初始化这两个控制窗口。

主边框窗口用来管理文档边框窗口、视窗口、工具条、菜单、加速键等,协调半模式状态(如上下文的帮助(SHIFT+F1模式)和打印预览)。

(3)文档边框窗口CChildFrame

文档边框窗口类从CMDIChildWnd类派生,MDI应用程序使用文档边框窗口来包含视窗口。

(4)文档CTDoc

文档类从CDocument类派生,用来管理数据,数据的变化、存取都是通过文档实现的。视窗口通过文档对象来访问和更新数据。

(5)视CTView

视类从CView或它的派生类派生。视和文档联系在一起,在文档和用户之间起中介作用,即视在屏幕上显示文档的内容,并把用户输入转换成对文档的操作。

(6)文档模板

文档模板类一般不需要派生。MDI应用程序使用多文档模板类CMultiDocTemplate;SDI应用程序使用单文档模板类CSingleDocTemplate。

应用程序通过文档模板类对象来管理上述对象(文档对象、主边框窗口对象、文档边框窗口对象、视对象)的创建。

2.构成应用程序的对象之间的关系

图88

3.构成应用程序的文件

从文件的角度来考察AppWizard生成了哪些源码文件,这些文件的作用是什么。

  • AppWizard所生成的头文件

  • AppWizard所生成的实现文件及其对头文件的包含关系。

4.2 MFC和Win32

4.2.1 MFC对象和Windows对象的关系

MFC中最重要的封装是对Win32 API的封装,因此,理解Windows对象和MFC对象之间的关系是理解MFC的一个关键。两者有很大的区别,但联系紧密。

所谓Windows对象是Win32下用句柄表示的Windows操作系统对象;

所谓MFC对象是C++对象,是一个C++类的实例。

(1)对应的数据结构不同

  • MFC对象是相应C++类的实例,这些类是由MFC或者程序员定义的;

  • Windows对象是Windows系统的内部结构的实例,通过一个句柄来引用;

  • MFC给这些类定义了一个成员变量来保存MFC对象对应的Windows对象的句柄。

(2)所处的层次不同

  • MFC对象是高层的,Windows对象是低层的。

  • MFC对象不仅把指向相应Windows对象的句柄封装成自己的成员变量(句柄实例变量),还把借助该句柄(HANDLE)来操作Windows对象的Win32 API函数封装为MFC对象的成员函数。

  • 通过MFC对象去操作低层的Windows对象,只须直接引用成员函数。

(3)创建的机制不同

MFC对象是由程序通过调用类的构造函数直接创建的;Windows对象是由相应的SDK函数创建的。创建的过程也不同 。

(4)二者转换的方式不同

使用MFC对象的成员函数GetSafeHandle,可以从一个MFC对象得到对应的Windows对象的句柄。使用MFC对象的成员函数Attach或者FromHandle,可以从一个已存在的Windows对象创建一个对应的MFC对象,前者得到一个永久性对象,后者得到的可能是一个临时对象。

(5)使用的范围不同

MFC对象只服务于创建它的进程,对系统的其他进程来说是不可见、不可用的;而一旦创建了Windows对象,其句柄在整个Windows系统中,是全局可见的。一些句柄可以被其他进程使用。典型的例子是,一个进程可以获得另一进程的窗口句柄,并给该窗口发送消息。

(6)销毁的方法不同

MFC对象随着析构函数的调用而消失;但Windows对象必须由相应的Windows系统函数销毁。

4.2.2 几个主要的类

1.Win32 API的窗口对象(Windows Object)

  • MFC的CWnd类是对Win32 API的窗口对象的封装。

  • 用SDK的Win32 API编写各种Windows应用程序,有其共同的规律:首先是编写WinMain函数,编写处理消息和事件的窗口过程WndProc,在WinMain里头注册窗口(Register Window),创建窗口,然后开始应用程序的消息循环。

  • 一个应用程序在创建某个类型的窗口前,必须首先注册该“窗口类型”(Register Window Class),把窗口过程、窗口风格以及其他信息和要登记的窗口类型关联起来。

“窗口类型”是Windows系统的数据结构,可以把它理解为Windows系统的类型定义,而窗口对象则是相应“窗口类型”的实例。Windows使用一个结构来描述“窗口类型”,其定义如下:

 typedef struct _WNDCLASS { 
 UINT cbSize;          //该结构的字节数 
 UINT style;           //窗口类型的风格 
 WNDPROC lpfnWndProc;  //窗口过程 WNDPROC   A 32-bit pointer to a window procedure 
 int cbClsExtra; 
 int cbWndExtra; 
 HANDLE hInstance;     //该窗口类型的窗口过程所属的应用实例 
 HICON hIcon;          //该窗口类型所用的像标 
 HCURSOR hCursor;      //该窗口类型所用的光标 
 HBRUSH hbrBackground; //该窗口类型所用的背景刷 
 LPCTSTR lpszMenuName; //该窗口类型所用的菜单资源 
 LPCTSTR lpszClassName;//该窗口类型的名称 A 32-bit pointer to a constant character string that is portable for Unicode and DBCS
 HICON hIconSm;        //该窗口类型所用的小像标 
 } WNDCLASS;

2.MFC的窗口类CWnd

在Windows系统里,一个窗口的属性分两个地方存放:一部分放在“窗口类型”里头,如上所述的在注册窗口时指定;另一部分放在窗口对象本身,如:窗口的尺寸,窗口的位置(X,Y轴),窗口的Z轴顺序,窗口的状态(ACTIVE,MINIMIZED,MAXMIZED,RESTORED…),和其他窗口的关系(父窗口,子窗口…),窗口是否可以接收键盘或鼠标消息,等等。

为了表达所有这些窗口的共性,MFC设计了一个窗口基类CWnd。有一点非常重要,那就是CWnd提供了一个标准而通用的MFC窗口过程,MFC下所有的窗口都使用这个窗口过程。利用MFC消息映射机制,使这个通用的窗口过程能为各种窗口实现不同的操作。

CWnd类提供了一系列成员函数。它们或者是对Win32相关函数的封装,或者是CWnd新设计的一些函数。主要有:

(1)窗口创建函数Create

(2)窗口销毁函数

(3)用于设定、获取、改变窗口属性的函数,

(4)用于完成窗口动作的函数

3.在MFC下创建一个窗口对象

MFC下创建一个窗口对象分两步,首先创建MFC窗口对象,然后创建对应的Windows窗口对象。在内存使用上,MFC窗口对象可以在栈或者堆(使用new创建)中创建。具体表述如下:

创建MFC窗口对象。通过定义一个CWnd或其派生类的实例变量或者动态创建一个MFC窗口的实例,前者在栈空间创建一个MFC窗口对象,后者在堆空间创建一个MFC窗口对象。

调用相应的窗口创建函数,创建Windows窗口对象。

4.MFC窗口的使用

直接使用MFC提供的窗口类,或者先从MFC窗口类派生一个新的C++类,然后使用它,这些在通常情况下都不需要程序员提供窗口注册的代码。

是否需要派生新的C++类,视MFC已有的窗口类是否能满足使用要求而定。派生的C++类继承了基类的特性并改变或扩展了它的功能,例如增加或者改变对消息、事件的特殊处理等。

5.在MFC下窗口的销毁

窗口对象使用完毕,应该销毁。在MFC下,一个窗口对象的销毁包括HWND窗口对象的销毁和MFC窗口对象的销毁。一般情况下,MFC编程框架自动地处理了这些。

如果在CWnd类型对象销毁时,其所拥有的Windows对象还被其它已知对象使用,则在该CWnd类型对象析构之前,首先必需调用Detach函数切断CWnd对象和Windows对象之间的联系,否则会出现运行时异常。

为什么要切断呢?因为CWnd是C++的对象,C++的对象有一个生存期的概念,脱离了该对象的作用域,这个对象就要被销毁,但是Windows对象没有这个特点,当销毁CWnd对象的时候,我们不一定希望Windows对象一起被销毁,那么在此之前,我们就先要把它们之间的联系剪断,以免其他对象去访问一个已经不存在的Windows对象。

4.4 Windows系统的消息机制

Windows系统将Windows应用程序的输入事件转换为消息,并将消息发送给应用程序的窗口。这些窗口通过窗口过程来接收和处理消息,然后把控制返还给Windows。 1.消息的分类

可以从消息的发送途径和消息的来源两方面对消息分类:

(1)队列消息和非队列消息

从消息的发送途径上,可将消息分为队列消息和非队列消息。队列消息首先送到系统消息队列,然后被分发送到线程消息队列;非队列消息直接送给目的窗口过程。

Windows系统维护着一个系统消息队列(System message queue),每个GUI线程有一个线程消息队列(Thread message queue)。

对于队列消息,最常见的是鼠标和键盘触发的消息,例如WM_MOUSERMOVE,WM_CHAR,WM_PAINT、WM_TIMER和WM_QUIT。当鼠标、键盘事件被触发后,相应的鼠标或键盘驱动程序就会把这些事件转换成相应的消息,然后输送到系统消息队列,由Windows系统去进行处理。Windows系统则在适当的时机,从系统消息队列中取出一个消息,根据MSG消息结构确定消息是要被送往那个窗口,然后把取出的消息送往创建窗口的线程的消息队列。线程从自己的消息队列中取消息,再通过操作系统发送到合适的窗口过程去处理。 一般来讲,系统总是保证窗口以先进先出的顺序接受消息。

非队列消息将会绕过系统队列和消息队列,直接将消息发送到窗口过程。例如,当用户激活一个窗口系统发送WM_ACTIVATE, WM_SETFOCUS, and WM_SETCURSOR。非队列消息也可以由当应用程序调用系统函数产生。例如,当程序调用SetWindowPos系统发送WM_WINDOWPOSCHANGED消息。

(2)系统消息和应用程序消息

从消息的来源,可将消息分为系统定义的消息和应用程序定义的消息。

系统消息ID的范围是从0到WM_USER-1,或从0X8000到0XBFFF;

应用程序消息ID的范围是从WM_USER(0X0400)到0X7FFF,或从0XC000到0XFFFF;

WM_USER到0X7FFF范围的消息由应用程序自己使用;

0XC000到0XFFFF范围的消息用来和其他应用程序通信,为了保证ID的唯一性,可使用::RegisterWindowMessage来得到该范围的消息ID。

2.MSG消息结构和消息处理

(1)MSG消息结构

为了从消息队列获取消息信息,可以调用一些函数,例如,

使用::GetMessage函数可从消息队列得到并从队列中移走消息,

使用::PeekMessage函数可从消息队列得到消息但不移走。

这些函数都需要使用MSG消息结构来保存获得的消息信息。

MSG结构包括了六个成员,用来描述消息的有关属性。它的定义是:

  typedef struct tagMSG {  // msg 结构
     HWND hwnd;       // 接收消息的窗口句柄
     UINT message;     // 消息标识( ID)  
     WPARAM wParam;  // 第一个消息参数
     LPARAM lParam;    // 第二个消息参数
     DWORD time;      // 消息产生的时间
     POINT pt;          // 消息产生时鼠标的位置
     } MSG; 

(2)应用程序通过窗口过程来处理消息

直接使用Win32 API编程时,每个“窗口类”都要登记一个如下形式的窗口过程:

 LRESULT CALLBACK MainWndProc (
     HWND hwnd,      // 窗口句柄 (a window handle)
     UINT msg,        // 消息标识 (a message identifier)
     WPARAM wParam, // 32位的消息参数1 (message parameters)
     LPARAM lParam   // 32位的消息参数2 
     )

应用程序通过窗口过程来处理消息:非队列消息由Windows直接送给目的窗口的窗口过程。窗口过程被调用时,接受上述的四个参数。

如果需要,窗口过程可以调用::GetMessageTime获取消息产生的时间,调用::GetMessagePos获取消息产生时鼠标光标所在的位置。

(3)应用程序通过消息循环来获得对消息的处理

直接使用Win32 API编程时,每个GDI应用程序在主窗口创建之后,都应进入消息循环,接受用户输入,解释和处理消息。消息循环的结构如下:

 while (GetMessage(&msg, (HWND) NULL, 0, 0)) 
 {   // 从消息队列得到消息 
     if (hwndDlgModeless == (HWND) NULL || 
     !IsDialogMessage(hwndDlgModeless, &msg) && 
     !TranslateAccelerator(hwndMain, haccel, &msg)) 
    { 
     TranslateMessage(&msg); 
     DispatchMessage(&msg); // 发送消息 
     } 
 }

DispatchMessage会先使当前进程进入管态,然后再由Windows操作系统内核调用窗口的回调函数。

为什么这样实现?

由windows来调用窗口回调函数,可以实现灵活的分配CPU资源。如果窗口没有新的消息进入消息队列,windows就不再会给该窗口线程分配时间片。

那么还要消息循环干什么,windows直接把消息发给窗口不就可以了吗?

因为你要在消息循环里把KEY_DOWN和KEY_UP组合成WM_CHAR;还可以直接屏蔽掉许多对你来说无用的消息,加快处理速度。

3.MFC消息处理

所谓消息映射,就是让程序员指定用来处理某个消息的某个MFC类。使用MFC提供的ClassWizard类向导,可以在处理消息的类中添加处理消息的成员函数,方便地实现消息映射。在此基础上,程序员可将自己的代码添加到这些消息处理函数中,实现所希望的消息处理。

如果派生类要覆盖基类的消息处理函数,就用ClassWizard在派生类中添加一个消息映射条目,用同样的原型定义一个函数,然后实现该函数。这个函数覆盖派生类的任何基类的同名处理函数。

4.MFC消息映射的定义

现在来介绍MFC的消息机制的实现原理和消息处理的过程。

(1)MFC处理的三类消息

MFC主要处理三类消息,它们对应的处理函数和处理过程有所不同:

①Windows消息,是消息名以前缀 “WM_”打头的消息,但WM_COMMAND消息除外。Windows消息直接被送给MFC的窗口过程处理,窗口过程再调用对应的消息处理函数。这类消息一般由窗口对象来处理,也就是说,这类消息处理函数一般是MFC窗口类的成员函数。

②控件通知消息,是控件子窗口送给父窗口的WM_COMMAND通知消息。窗口过程调用对应的消息处理函数。这类消息一般也由窗口对象来处理,对应的消息处理函数一般也是MFC窗口类的成员函数。

③命令消息,这是来自菜单、工具条按钮、加速键等用户接口对象的WM_COMMAND通知消息,属于应用程序自己定义的消息。命令消息往往与具体的窗口无关,只是为本程序完成一个功能操作。

通过消息映射机制,MFC框架把命令按一定的路径分发给多种类型的具备消息处理能力的对象来处理,如文档、窗口、应用程序、文档模板等对象。

能处理消息映射的类必须从CCmdTarget类派生。

WM_COMMAND消息的各种情况:

菜单、工具栏:

LOWORD(wParam): 菜单id、

HIWORD(wParam): 0

lParam: 0

如果这个消息是由子窗口控件产生,如button产生则:

LOWORD(wParam): 控件ID

HIWORD(wParam): 通知码

lParam: 子窗口句柄。

如果这个消息是由子窗口或者快捷键产生,则通知码为1,由菜单产生,通知码为0。

通过参数,可以区分这个消息的来源是来自于控件,快捷键还是菜单。

控件通知消息的通知码:

按扭控件

BN_CLICKED用户单击了按钮

BN_DISABLE按钮被禁止

BN_DOUBLECLICKED用户双击了按钮

BN_CHILITE用户加亮了按钮

BN_PAINT按钮应当重画

BN_UNHILITE加亮应当去掉

组合框控件

CBN_CLOSEUP组合框的列表框被关闭

CBN_DBLCLK用户双击了一个字符串

CBN_DROPDOWN组合框的列表框被拉出

CBN_EDITCHANGE用户修改了编辑框中的文本

CBN_EDITUPDATE编辑框内的文本即将更新

CBN_ERRSPACE组合框内存不足

CBN_KILLFOCUS组合框失去输入焦点

CBN_SELCHANGE在组合框中选择了一项

CBN_SELENDCANCEL用户的选择应当被取消

CBN_SELENDOK用户的选择是合法的

CBN_SETFOCUS组合框获得输入焦点编辑框控件

EN_CHANGE编辑框中的文本己更新

EN_ERRSPACE编辑框内存不足

EN_HSCROLL用户点击了水平滚动条

(2)MFC消息映射的实现方法

程序员可以使用MFC的类向导ClassWizard来实现消息映射。类向导在源码中添加一些消息映射的内容,并声明和实现消息处理函数。

(3)消息映射宏的种类

①用于映射 Windows消息的宏,前缀为“ON_WM_”。

②用于映射命令消息的宏ON_COMMAND

③用于控制通知消息的宏

④用于用户界面接口状态更新的ON_UPDATE_COMMAND_UI宏

⑤用于其他消息的宏

⑥扩展消息映射宏

4.5 MFC对象的创建

MFC对象的创建过程

MFC应用程序的执行分为三个阶段:应用程序启动和初始化阶段,与用户交互阶段,程序退出和清理阶段。

MFC应用程序通过MFC的接口填入MFC框架的自己的特殊处理,可能在这三个阶段被MFC框架调用。

每个MFC程序都有一个全局的应用程序类的对象,在面向对象程度非常好的MFC程序中,应该只有这一个全局的对象。

MFC应用程序启动时,首先创建这个应用程序对象, 比如对象名为theApp,这时将调用这个对象的构造函数来初始化theApp。

然后由应用程序框架调用MFC提供的AfxWinMain主函数, 在这个主函数中,首先获得应用程序对象theApp的指针, 然后通过这个指针调用程序程序对象的有关函数,来完成程序的初始化和启动工作,最后调用Run函数,进入消息循环. 主要代码如下:

 CTheApp theApp;
 BOOL CTheApp::InitInstance()
 {         ....
          m_pMainWnd = new CTheWindow();//调用窗口类的构造函数来创建一个窗口
          m_pMainWnd->ShowWindow(SW_SHOW);//显示窗口
          m_pMainWnd->UpdateWindow();//更新窗口上的元素
          return TRUE;
 }
 //
 int AFXAPI AfxWinMain()
 {
          CWinThread *pThread = AfxGetThread();//获取主线程指针
          CWinApp *pApp = AfxGetApp();
          AfxWinInit();
          ....
          pApp->InitApplication();
            ...
          pThread->InitInstance();//初始化应用程序实例
          ...
          nReturnCode = pThread->Run();//开始消息循环
 }

与普通的WIN32 SDK应用程序相比, 入口函数换成AfxWinMain, 初始化和启动工作主要交给应用程序类处理. 另外, 创建管理窗口由窗口类负责,而且由于消息映像机制的引进, 不用再去写麻烦的回调函数和SWITCH-CASE语句。

另外, 在AfxWinMain中可以看到, 运行代码和消息循环是由主线程来完成的. 此外, 可以看到主线程完成了界面管理(如窗口创建)和消息循环(Run)。

4.6 应用程序的退出

一般Windows应用程序启动后就进入消息循环,等待或处理用户的输入,如果用户按了主窗口的关闭按钮,或者选择执行系统菜单的“关闭”项,或者从“文件”菜单选择执行“退出”,都会导致应用程序主窗口被关闭。主窗口关闭了,应用程序也随之退出。

MFC程序的退出主要是以下几步:   

1.使用者通过点击File/Close或程序窗口由上角的叉号发出WM_CLOSE消息。   

2.程序没有设置WM_CLOSE处理程序,交给默认处理程序。   

3.默认处理函数对于WM_CLOSE的处理方式为调用::DestoryWindow,并因而发出WM_DESTORY消息。   

4.默认的WM_DESTORY处理方式为调用::PostQuitMessage,发出WM_QUIT。   

5.CWinApp::Run收到WM_QUIT后结束内部消息循环,并调用ExinInstance函数,它是CWinApp的一个虚拟函数,可以由用户重载。   

6.最后回到AfxWinMain,执行AfxWinTerm,结束程序。

第五章

为简化套接字网络编程,更方便地利用Windows的消息驱动机制,微软的基础类库(Microsoft Foundation Class Libary,简称MFC),提供了两个套接字类,在不同的层次上对Windows Socket API函数进行了封装,为编写Windows Socket网络通信程序,提供了两种编程模式。

CAsyncSocket类

CAsyncSocket类,在较低的层次上对Windows Sockets API进行了封装。

CAsyncSocket采用的WSAAsynSelect模型,通过该模型,应用程序可以接收以Windows消息为基础的网络事件通知。

CAsyncSocket的成员函数和Windows Sockets API的函数调用直接对应。一个CAsyncSocket对象代表了一个Windows套接字。它是网络通信的端点

除了把套接字封装成C++的面向对象的形式供程序员使用以外,这个类将那些与套接字相关的Windows消息变为CAsyncSocket类的回调函数。

CSocket类

CSocket类:从CAsyncSocket类派生,是对Windows Sockets API的高级封装。CSocket类继承了CAsyncSocket类的许多成员函数,用法一致。CSocket类的主要特点表现在:

(1)CSocket结合CArchive类来使用套接字。

(2)CSocket类为Windows消息的后台处理提供了阻塞的工作模式。

这两个类提供了事件处理函数,编程者通过对事件处理函数进行重载,可方便地对套接字发送数据、接收数据等事件进行处理。同时,可以结合MFC的其它类来使用这两个套接字类,并利用MFC的各种可视化向导,从而大大简化了编程。

在MFC中,有一个名为afxSock.h的包含文件,在这个文件中定义了CSocketWnd,CAsyncSocket,CSocket和CSocketFile。

5.1 CasyncSocket类

CAsyncSocket类从Cobject类派生而来

5.1.1 使用CAsyncSocket类的一般步骤

网络应用程序一般采用客户/服务器模式,服务器端和客户端使用CAsyncSocket类编程的步骤有所不同,参看表5.1。

在服务器端,调用成员函数Accept()时,需要构造一个新的CAsyncSocket对象作为它的参数,这个新的套接字对象表示了与客户端的一个链接,注意不需要调用create函数创建它的底层套接字。

在CAsyncSocket对象析构之前,需要调用成员函数close关闭它。

CAsyncSocket类专用于异步模式,不支持阻塞工作方式。

MFC Winsock的初始化

在CWinApp::InitInstance 函数中调用AfxSocketInit函数初始化 Windows 套接字。

 BOOL AfxSocketInit( WSADATA* lpwsaData = NULL ) 

返回值:非零,如果函数运行成功;否则为 0。

不需要再调用WSAStartup和WSACleanup函数。

5.1.2 创建CAsyncSocket类对象

将CAsyncSocket类对象称为异步套接字对象。创建异步套接字对象一般分为两个步骤,首先构造一个CAsyncSocket对象,再创建该对象的底层的SOCKET句柄。

1.创建空的异步套接字对象

通过调用CAsyncSocket类的构造函数,创建一个新的空CAsyncSocket类套接字对象,构造函数不带参数。然后必须调用它的Create成员函数,来创建底层的套接字数据结构,并绑定它的地址。

有两种使用方法,会在不同的位置创建。

(1)如:CAsyncSocket aa;aa.Create(……);

(2)如: CAsyncSocket* Pa; Pa = new CAsyncSocket; Pa->Create(……);

2.创建异步套接字对象的底层套接字句柄

通过调用CAsyncSocket类的Create()成员函数,创建该对象的底层套接字句柄,决定套接字对象的具体特性。调用格式为:

 BOOL  Create( UINT nSocketPort=0,Int nSocketType = SOCK_STREAM,
 Long  Ievent = FD_READ | FD_WRITE | FD_OOB | FD_ACCEPT |FD_CONNECT | FD_CLOSE,  LPCTSTR lpszSocketAddress = NULL ); 

说明:

nSocketPort 标识套接字端口

nSocketType 标识套接字类型,默认为流式套接字

lEvent 表示套接字能够接受的事件

lpszSocketAddress 表示套接字的网络地址

 BOOL CAsyncSocket::Create(UINT nSocketPort, int nSocketType,
     long lEvent, LPCTSTR lpszSocketAddress)
 {
     // 使用 Socket 函数创建异步套接字,并指定套接字类型和事件类型
     if (Socket(nSocketType, lEvent))
     {
         // 尝试将套接字绑定到指定的端口和地址
         if (Bind(nSocketPort, lpszSocketAddress))
             return TRUE;
 ​
         // 如果绑定失败,获取错误码,关闭套接字,并设置全局错误码
         int nResult = GetLastError();
         Close();
         WSASetLastError(nResult);
     }
 ​
     // 返回 FALSE,表示创建和初始化套接字失败
     return FALSE;
 }
 // 函数名称:Socket
 // 描述:创建套接字并进行相关设置
 // 参数:
 //   nSocketType:套接字类型,如 SOCK_STREAM 或 SOCK_DGRAM
 //   lEvent:异步套接字事件类型,如 FD_READ、FD_WRITE 等
 //   nProtocolType:协议类型,通常为 0,表示使用默认协议
 //   nAddressFormat:地址格式,通常为 AF_INET,表示使用 IPv4 地址
 // 返回值:
 //   成功时返回 TRUE,否则返回 FALSE
 ​
 BOOL CAsyncSocket::Socket(int nSocketType, long lEvent,
     int nProtocolType, int nAddressFormat)
 {
     // 确保套接字句柄为无效状态
     ASSERT(m_hSocket == INVALID_SOCKET);
 ​
     // 使用 socket 函数创建套接字,指定套接字类型、协议类型和地址格式
     m_hSocket = socket(nAddressFormat, nSocketType, nProtocolType);
 ​
     // 检查套接字是否创建成功
     if (m_hSocket != INVALID_SOCKET)
     {
         // 将套接字句柄与当前 CAsyncSocket 对象关联
         CAsyncSocket::AttachHandle(m_hSocket, this, FALSE);
 ​
         // 使用 AsyncSelect 函数注册关注的事件类型,启用异步通知机制
         return AsyncSelect(lEvent);
     }
 ​
     // 返回 FALSE,表示套接字创建失败
     return FALSE;
 }
 // 函数名称:AttachHandle
 // 描述:将套接字句柄与 CAsyncSocket 对象关联,并设置异步通知机制
 // 参数:
 //   hSocket:要关联的套接字句柄
 //   pSocket:指向 CAsyncSocket 对象的指针
 //   bDead:标志是否为无效的套接字
 // 注意:该函数为静态成员函数
 ​
 void PASCAL CAsyncSocket::AttachHandle(SOCKET hSocket, CAsyncSocket* pSocket, BOOL bDead)
 {
     // 获取当前线程的套接字状态
     _AFX_SOCK_THREAD_STATE* pState = _afxSockThreadState;
 ​
     // …… (省略部分代码)
 ​
     // 如果套接字句柄映射表为空,创建一个 CSocketWnd 对象来接收异步通知
     if (pState->m_pmapSocketHandle->IsEmpty())
     {
         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))
         {
             // 如果窗口创建失败,抛出资源异常
             AfxThrowResourceException();
         }
 ​
         // 将窗口句柄保存到套接字状态中
         pState->m_hSocketWindow = pWnd->m_hWnd;
     }
 ​
     // 将套接字句柄与 CAsyncSocket 对象关联,放入套接字映射表中
     pState->m_pmapSocketHandle->SetAt((void*)hSocket, pSocket);
 ​
     // …… (省略部分代码)
 }
 #define _afxSockThreadState AfxGetModuleThreadState()

在同一线程中创建的所有 Socket 共享一个 CSocketWnd 对象,与之对应的是一个全局结构体变量 _AFX_SOCK_THREAD_STATE。这个结构体保存了共享的窗口句柄 m_hSocketWindow,以及其他几个重要的变量:

  1. mCEmbeddedButActsLikePtr<CMapPtrToPtr> m_pmapSocketHandle:这是一个 CMapPtrToPtr 类型的对象,用于维护 m_hSocket 和当前 socket 对象之间的映射关系。它记录了 CAysncSocket* 与有效 SOCKET 的对应关系。

  2. mCEmbeddedButActsLikePtr<CPtrList> m_plistSocketNotifications:这是一个 CPtrList 类型的对象,用于记录当前 socket 对象所有的 SOCKET 事件的通知消息。它可能用于追踪和处理与 socket 相关的异步事件。

AttachHandle 函数中,会将一个 CAysncSocket* 指针和其对应的 SOCKET 添加到 m_pmapSocketHandle 中,以建立新的映射关系。

 // 函数名称:AsyncSelect
 // 描述:通过 WSAAsyncSelect 函数注册异步事件
 // 参数:
 //   lEvent:异步套接字事件类型,如 FD_READ、FD_WRITE 等
 // 返回值:
 //   注册成功时返回 TRUE,否则返回 FALSE
 ​
 BOOL CAsyncSocket::AsyncSelect(long lEvent)
 {
     // 确保套接字句柄为有效状态
     ASSERT(m_hSocket != INVALID_SOCKET);
 ​
     // 获取当前线程的套接字状态
     _AFX_SOCK_THREAD_STATE* pState = _afxSockThreadState;
 ​
     // 使用 WSAAsyncSelect 函数注册异步事件,关联套接字句柄和窗口句柄
     return WSAAsyncSelect(m_hSocket,
                          pState->m_hSocketWindow,
                          WM_SOCKET_NOTIFY,
                          lEvent) != SOCKET_ERROR;
 }
 ​
 // 宏定义,获取当前模块的线程状态
 #define _afxSockThreadState AfxGetModuleThreadState()
WM_SOCKET_NOTIFY

以下是对 Winsock 事件消息的重述,并使用 Markdown 格式:

Winsock 事件消息

消息的参数 wParamlParam

  • wParam 表示与该事件相关的套接字,如 hSocket = (SOCKET)wParam; 可以将 wParam 转换为套接字句柄。

  • lParam 包含了一个 LPARAM 类型的数值,其中高位字段包含了错误码,低位字段包含了 Winsock 事件类型。使用以下宏可以从 lParam 中提取出 Winsock 事件和相应的错误码:

     // 从 lParam 中提取出 Winsock 事件类型
     #define WSAGETSELECTEVENT(lParam) LOWORD(lParam)
     ​
     // 从 lParam 中提取出错误码
     #define WSAGETSELECTERROR(lParam) HIWORD(lParam)

    使用这两个宏,可以得到事件类型和相关的错误码。例如:

     // 从 lParam 中提取出 Winsock 事件类型和错误码
     int nEvent = WSAGETSELECTEVENT(lParam);
     int nError = WSAGETSELECTERROR(lParam);

    其中,nEvent 表示 Winsock 事件类型(如 FD_READ、FD_WRITE 等),而 nError 表示相关事件的错误码(0 表示成功,WSAENETDOWN 表示失败)。

以下是对不同的 Winsock 事件的描述,使用 Markdown 格式:

  • FD_READ: 调用指定套接字的 OnReceive 函数。当套接字上有可读数据时触发此事件。

  • FD_WRITE: 调用指定套接字的 OnSend 函数。当套接字准备好发送数据时触发此事件。

  • FD_OOB: 调用指定套接字的 OnOutOfBandData 函数。当接收到带外数据时触发此事件。

  • FD_ACCEPT: 调用指定套接字的 OnAccept 函数。当套接字接受连接时触发此事件。

  • FD_CONNECT: 调用指定套接字的 OnConnect 函数。当套接字连接建立时触发此事件。

  • FD_CLOSE: 调用指定套接字的 OnClose 函数。当套接字关闭时触发此事件。

    WM_SOCKET_NOTIFY消息的响应函数:

 // WM_SOCKET_NOTIFY 消息的响应函数
 LRESULT CSocketWnd::OnSocketNotify(WPARAM wParam, LPARAM lParam)
 {
     // 将 WM_SOCKET_NOTIFY 消息添加到套接字的辅助队列中
     CSocket::AuxQueueAdd(WM_SOCKET_NOTIFY, wParam, lParam);
     
     // 处理套接字的辅助队列
     CSocket::ProcessAuxQueue();
 ​
     // 返回 0L 表示消息已被处理
     return 0L;
 }
 ​
 // 将消息添加到套接字的辅助队列中
 void PASCAL CSocket::AuxQueueAdd(UINT message, WPARAM wParam, LPARAM lParam)
 {
     // 获取当前线程的套接字状态
     _AFX_SOCK_THREAD_STATE* pState = _afxSockThreadState;
 ​
     // 创建一个新的消息结构体并设置其字段
     MSG* pMsg = new MSG;
     pMsg->message = message;
     pMsg->wParam = wParam;
     pMsg->lParam = lParam;
 ​
     // 将消息添加到套接字状态的辅助队列的尾部
     pState->m_plistSocketNotifications->AddTail(pMsg);
 }
 ​
 // 处理套接字的辅助队列
 int PASCAL CSocket::ProcessAuxQueue()
 {
     // 获取当前线程的套接字状态
     _AFX_SOCK_THREAD_STATE* pState = _afxSockThreadState;
 ​
     // …… (省略部分代码)
 ​
     int nCount = 0;
 ​
     // 循环处理辅助队列中的消息
     while (!pState->m_plistSocketNotifications->IsEmpty())
     {
         nCount++;
 ​
         // 移除队列头部的消息
         MSG* pMsg = (MSG*)pState->m_plistSocketNotifications->RemoveHead();
 ​
         // 根据消息类型进行相应的处理
         if (pMsg->message == WM_SOCKET_NOTIFY)
         {
             // 调用异步套接字的回调函数
             CAsyncSocket::DoCallBack(pMsg->wParam, pMsg->lParam);
         }
         else
         {
             // 若收到 WM_SOCKET_DEAD 消息,调用异步套接字的 DetachHandle 函数
             CAsyncSocket::DetachHandle((SOCKET)pMsg->wParam, TRUE);
         }
 ​
         // 释放消息结构体的内存
         delete pMsg;
     }
 ​
     return nCount;
 }
 ​
 // 异步套接字的回调函数
 void PASCAL CAsyncSocket::DoCallBack(WPARAM wParam, LPARAM lParam)
 {
     // 通过套接字句柄查找对应的异步套接字对象
     CAsyncSocket* pSocket = CAsyncSocket::LookupHandle((SOCKET)wParam, TRUE);
 ​
     // …… (省略部分代码)
 ​
     // 获取错误码和事件类型
     int nErrorCode = WSAGETSELECTERROR(lParam);
     switch (WSAGETSELECTEVENT(lParam))
     {
         // 根据事件类型调用相应的处理函数
         case FD_READ:
             {
                 DWORD nBytes;
                 // 查询可读数据的字节数
                 if (!pSocket->IOCtl(FIONREAD, &nBytes))  
                     nErrorCode = WSAGetLastError();
                 // 若有数据或者出错,调用 OnReceive 函数
                 if (nBytes != 0 || nErrorCode != 0)  
                     pSocket->OnReceive(nErrorCode);
             }
             break;
         case FD_WRITE:
             // 调用 OnSend 函数
             pSocket->OnSend(nErrorCode);
             break;
         case FD_OOB:
             // 调用 OnOutOfBandData 函数
             pSocket->OnOutOfBandData(nErrorCode);
             break;
         case FD_ACCEPT:
             // 调用 OnAccept 函数
             pSocket->OnAccept(nErrorCode);
             break;
         case FD_CONNECT:
             // 调用 OnConnect 函数
             pSocket->OnConnect(nErrorCode);
             break;
         case FD_CLOSE:
             // 调用 OnClose 函数
             pSocket->OnClose(nErrorCode);
             break;
     }
 }

举例:创建一个使用27端口的流式异步套接字对象

 // 创建一个流式异步套接字对象
 CAsyncSocket* pSocket = new CAsyncSocket;
 // 指定端口号
 int nPort = 27;
 // 通过Create方法建立Socket连接,同时简化了WSAStartup和bind过程
 pSocket->Create(nPort, SOCK_STREAM);

这段代码的目的是创建一个 CAsyncSocket 类的实例,然后通过 Create 方法指定了端口号为 27,套接字类型为流式套接字(SOCK_STREAM)。在 CAsyncSocket 类内部,Create 方法实现了套接字的创建、WSAStartup、绑定等过程,并且对 IP 地址类型转换、主机名和 IP 地址的转换进行了简化处理,使用了字符串和整数操作来简化复杂的变量类型。这使得创建和配置异步套接字对象变得更加方便。

5.1.3 关于CAsyncSocket类可以接受并处理的消息事件

1.六种套接字相关的事件与通知消息

参数Ievent可以选用的六个符号常量是在winsock.h文件中定义的。

 #define FD_READ     0x01   // 可读事件,触发 OnReceive 函数
 #define FD_WRITE    0x02   // 可写事件,触发 OnSend 函数
 #define FD_OOB      0x04   // 带外数据事件,触发 OnOutOfBandData 函数
 #define FD_ACCEPT   0x08   // 连接请求事件,触发 OnAccept 函数
 #define FD_CONNECT  0x10   // 连接建立事件,触发 OnConnect 函数
 #define FD_CLOSE    0x20   // 连接关闭事件,触发 OnClose 函数

它们代表MFC套接字对象可以接受并处理的六种网络事件,当事件发生时,套接字对象会收到相应的通知消息,并自动执行套接字对象响应的事件处理函数。

(1)FD_READ事件通知:通知有数据可读。

(2)FD_WRITE事件通知:通知可以写数据。

(3)FD_ACCEPT事件通知:通知监听套接字有连接请求可以接受。

(4)FD_CONNECT事件通知:通知请求连接的套接字,连接的要求已被处理。

(5)FD_CLOSE事件通知:通知套接字已关闭。

(6)FD_OOB事件通知:通知将有带外数据到达。

2.MFC框架对于六个网络事件的处理

当上述的网络事件发生时,MFC框架作何处理呢?按照Windows的消息驱动机制,MFC框架应当把消息发送给相应的套接字对象,并调用作为该对象成员函数的事件处理函数。事件与处理函数是一一映射的。

实际上,MFC定义了一个内部的类CSocketWnd,当调用Create函数创建一个套接字时,就会将该套接字连接到一个窗口(CSoketWnd的对象),CAsyncSocket的DoCallBack函数为该窗口的回调函数。这样,当一个网络事件发生时,经过MFC的消息循环,DoCallBack函数会根据不同的事件调用相应的消息处理函数。MFC将这些消息处理函数定义为虚函数,在编程时必须重载需要的消息处理函数。

afxSock.h 文件中,CAsyncSocket 类的声明中定义了与这六个网络事件对应的事件处理函数:

virtual void OnReceive(int nErrorCode); 对应 FD_READ 事件

virtual void OnSend(int nErrorCode); 对应 FD_WRITE 事件

virtual void OnAccept(int nErrorCode); 对应 FD_ACCEPT 事件

virtual void OnConnect(int nErrorCode); 对应 FD_CONNECT 事件

virtual void OnClose(int nErrorCode); 对应 FD_CLOSE 事件

virtual void OnOutOfBandData(int nErrorCode); 对应 FD_OOB 事件

当某个网络事件发生时,MFC框架会自动调用套接字对象的对应的事件处理函数。这就相当给了套接字对象一个通知,告诉它某个重要的事件已经发生。所以也称之为套接字类的通知函数(notification functions)或回调函数(callback functions)。

3.重载套接字对象的回调函数

如果你从CAsyncSocket类派生了自己的套接字类,你必须重载你的应用程序所感兴趣的那些网络事件所对应的通知函数。

MFC框架自动调用通知函数,使得你可以在套接字被通知的时候来优化套接字的行为。

5.1.4 客户端套接字对象请求连接到服务器端套接字对象

在服务器端套接字对象已经进入监听状态之后,客户应用程序可以调用 CAsyncSocket 类的 Connect() 成员函数,向服务器发出一个连接请求。

格式一:

 BOOL Connect(LPCTSTR lpszHostAddress, UINT nHostPort);

格式二:

 BOOL Connect(const SOCKADDR* lpSockAddr, int nSockAddrLen);
  • lpszHostAddress: 主机的 IP 地址或网址。

  • nHostPort: 标识当前应用程序的端口。

  • lpSockAddr: 是一个 SOCKADDR 结构指针,该结构标识套接字地址信息。

  • nSockAddrLen: 确定 lpSockAddr 的大小。

如果调用成功或者发生了 WSAEWOULDBLOCK 错误,该函数返回。当调用结束返回时,都会发生 FD_CONNECT 事件,MFC 框架会自动调用客户端套接字的 OnConnect() 事件处理函数,并将错误代码作为参数传送给它。它的原型调用格式如下:

 virtual void OnConnect(int nErrorCode);

这个函数用于处理客户端套接字连接到服务器端套接字时触发的事件。

5.1.5 服务器接受客户端的连接请求

在服务器端,使用 CAsyncSocket 流式套接字对象,一般按照以下步骤来接收客户端套接字对象的连接请求。

  1. 服务器应用程序必须首先创建一个 CAsyncSocket 流式套接字对象,并调用它的 Create 成员函数创建底层套接字句柄。这个套接字对象专门用来监听来自客户机的连接请求,所以称它为监听套接字对象。

  2. 调用监听套接字对象的 Listen 成员函数,使监听套接字对象开始监听来自客户端的连接请求。此函数的调用格式是:

     BOOL Listen(int nConnectionBacklog = 5);

    Listen 函数确认并接纳了一个来自客户端的连接请求后,会触发 FD_ACCEPT 事件,监听套接字会收到通知,表示监听套接字已经接纳了一个客户的连接请求,MFC 框架会自动调用监听套接字的 OnAccept 事件处理函数,它的原型调用格式如下:

     virtual void OnAccept(int nErrorCode);

    编程者一般应重载此函数,在其中调用监听套接字对象的 Accept 函数,来接收客户端的连接请求。

  3. 创建一个新的空的套接字对象,不需要使用它的 Create 函数来创建底层套接字句柄。这个套接字专门用来与客户端连接,并进行数据的传输。一般称它为连接套接字,并作为参数传递给下一步的 Accept 成员函数。

  4. 调用监听套接字对象的 Accept 成员函数,调用格式为:

     virtual BOOL Accept(CAsyncSocket& rConnectedSocket,
                         SOCKADDR* lpSockAddr = NULL,
                         int* lpSockAddrLen = NULL);

这一系列步骤实现了服务器端接受客户端连接请求的流程。

5.1.6 发送与接收流式数据

当服务器和客户机建立了连接以后,就可以在服务器端的连接套接字对象和客户端的套接字对象之间传输数据了。对于流式套接字对象,使用 CAsyncSocket 类的 Send 成员函数向流式套接字发送数据,使用 Receive 成员函数从流式套接字接收数据。

1. 用 Send 成员函数发送数据

格式:

 virtual int Send(const void* lpBuf, int nBufLen, int nFlags = 0);

对于一个 CAsyncSocket 套接字对象,当它的发送缓冲区可用时,会激发 FD_WRITE 事件,套接字会得到通知,MFC 框架会自动调用这个套接字对象的 OnSend 事件处理函数。一般编程者会重载这个函数,在其中调用 Send 成员函数来发送数据。

2. 用 Receive 成员函数接收数据

格式:

 virtual int Receive(void* lpBuf, int nBufLen, int nFlags = 0);

对于一个 CAsyncSocket 套接字对象,当有数据到达它的接收队列时,会激发 FD_READ 事件,套接字会得到已经有数据到达的通知,MFC 框架会自动调用这个套接字对象的 OnReceive 事件处理函数。一般编程者会重载这个函数,在其中调用 Receive 成员函数来接收数据。在应用程序将数据取走之前,套接字接收的数据将一直保留在套接字的缓冲区中。

发送函数 Send,调用它可能有3种结果:错误、部分完成、全部完成。其中错误又分两种情况:一种是由各种网络问题导致的失败,你需要马上决定是放弃本次操作,还是启用某种对策;另一种是“忙”,你实际上不用马上理睬。

因此,需要调用 WSAGetLastError 来判断返回值是哪种情况,WSAGetLastError 返回 WSAEWOULDBLOCK,代表“忙”,为什么当你 Send 得到 WSAEWOULDBLOCK 却不用理睬呢?因为 CAsyncSocket 会记得你的 Send WSAEWOULDBLOCK 了,待发送的数据会写入 CAsyncSocket 内部的发送缓冲区,并会在不忙的时候自动调用 OnSend,发送内部缓冲区里的数据。同样,如果 Send 只完成了一部分,你也不需要理睬,尚未发送的数据同样会写入 CAsyncSocket 内部的发送缓冲区,并在不“忙”的时候自动调用 OnSend 完成发送。

使用 CAsyncSocket 发送的流程:

需要两个成员变量,一个发送任务表,一个记录发送进度。

在任何需要发送数据的时候,主动调用 Send 函数,同时更新任务表和发送进度。

OnSend 被触发时要干的事情就是根据任务表和发送进度调用 Send 继续发。

若仍没能将任务表全部发送完成,更新发送进度,退出,等待下一次 OnSend

若任务表已全部发送完毕,则清空任务表及发送进度。

使用 CAsyncSocket 的接收流程:

不需要主动调用 Recieve,只应该在 OnRecieve 中等待。由于不可能知道将要抵达的数据类型及次序,所以需要定义一个已收数据表作为成员变量来存储已收到但尚未处理的数据。

每次 OnRecieve 被触发,只需要被动调用一次 Recieve 来接受固定长度的数据,并添加到已收数据表后。

然后需要扫描已收数据表,若其中已包含一条或数条完整的可解析的业务数据包,截取出来,调用业务处理窗口的处理函数来处理或作为消息参数发送给业务处理窗口。

而已收数据表中剩下的数据,将等待下次 OnRecieve 中被再次组合、扫描并处理。

5.1.7 关闭套接字

  1. 使用 CAsyncSocket 类的 Close 成员函数

    格式:

     virtual void Close();
  2. 使用 CAsyncSocket 类的 ShutDown() 成员函数

    使用 CAsyncSocket 类的 ShutDown() 成员函数,可以选择关闭套接字的方式。将套接字置为不能发送数据,或不能接收数据,或二者均不能的状态。

    格式:

     BOOL ShutDown(int nHow = sends);

在长连接应用中,连接可能因为各种原因中断,所以需要自动重连。此时需要根据 CAsyncSocket 的成员变量 m_hSocket 来判断当前连接状态:if (m_hSocket == INVALID_SOCKET)。连接中断时 OnClose 将被触发,此时需要在 OnClose 中主动调用 Close,否则 m_hSocket 并不会被自动赋值为 INVALID_SOCKET

5.1.8 错误处理

一般说来,调用 CAsyncSocket 对象的成员函数后,返回一个逻辑型的值,如果成员函数执行成功,返回 TRUE;如果失败,返回 FALSE。究竟是什么原因造成错误呢?这时,可以进一步调用 CAsyncSocket 对象的 GetLastError() 成员函数,来获取更详细的错误代码,并进行相应的处理。

格式:

 static int GetLastError();

返回值是一个错误码,针对刚刚执行的 CAsyncSocket 成员函数。

以下是对这段文字的 Markdown 格式的转换:

5.1.9 其它的成员函数

1. 关于套接字属性的函数

要设置底层套接字对象的属性,可以调用 SetSockOpt() 成员函数;

要获取套接字的设置信息,可调用 GetSockOpt() 成员函数;

要控制套接字的工作模式,可调用 IOCtl() 成员函数,选择合适的参数,可以将套接字设置在阻塞模式(Blocking mode)下工作。

  • CAsyncSocket::SetSockOpt

     BOOL SetSockOpt(int nOptionName, const void* lpOptionValue, int nOptionLen, int nLevel = SOL_SOCKET);

    返回值:调用成功时,返回非零值,否则为0,并可以调用 GetLastError() 取得特定的错误代码。

    参数:

    • nOptionName:准备设置值的套接字选项。

    • lpOptionValue:指向待设置的值所在缓冲的指针。

    • nOptionLenlpOptionValue 缓冲的字节数。

    • nLevel:选项定义所在的级别,系统支持的级别只有 SOL_SOCKETIPPROTO_TCP

    SetSockOpt 支持的选项如下表:

    选项名类型描述
    SO_BROADCASTBOOL允许在套接字上传输广播消息
    SO_DEBUGBOOL记录调试信息
    ...(其他选项)
  • CAsyncSocket::IOCtl

     BOOL IOCtl(long lCommand, DWORD* lpArgument);

    返回值:调用成功时返回非零值,否则为0。

    参数:

    • lCommand:要在套接字上执行的命令。

    • lpArgument:指向 lCommand 所需参数的指针。

    说明:本函数用于控制套接字的模式。它支持以下命令:

    • FIONBIO:允许或禁止套接字的非阻塞模式。lpArgument 参数指向一个 DWORD 类型的值,如果允许非成块方式,则非零,否则为零。

    • FIONREAD:检测一次 Receive 调用能从套接字中可读出的最大字节数,结果就是 lpArgument 参数指向的一个 DWORD 类型的值。

2. 发送和接收数据

如果创建的是数据报类型的套接字,用 SendTo() 成员函数来向指定的地址发送数据,事先不需要建立发送端和接收端之间的连接,用 ReceiveFrom() 成员函数可以从某个指定的网络地址接收数据。

  • 发送数据 SendTo() 的调用格式,有两种重载的形式,区别在于参数不同:

     int SendTo(const void* lpBuf, int nBufLen, UINT nHostPort, LPCTSTR lpszHostAddress = NULL, int nFlags = 0);
     int SendTo(const void* lpBuf, int nBufLen, const SOCKADDR* lpSockAddr, int nSockAddrLen, int nFlags = 0);
  • 接收数据 ReceiveFrom() 的调用格式,也有两种重载的形式,区别在于参数不同:

     int ReceiveFrom(void* lpBuf, int nBufLen, CString& rSocketAddress, UINT& rSocketPort, int nFlags = 0);
     int ReceiveFrom(void* lpBuf, int nBufLen, SOCKADDR* lpSockAddr, int* lpSockAddrLen, int nFlags = 0);

5.2 CSocket类

CSocket 类是从 CAsyncSocket 类派生而来的,它们的派生关系如图:

 

派生

派生

Cobject

CAsyncSocket

CSocket

CAsyncSocket 用于在少量连接时,处理大批量无步骤依赖性的业务。而 CSocket 用于处理步骤依赖性业务,对于流式套接字可以借助于 MFC 提供的序列化机制简化数据的收发过程(注:该机制不能用于数据报套接字)。

如果希望在用户界面线程中使用阻塞式套接字操作,则可以使用 CSocket它在 CAsyncSocket 基础之上实现了阻塞操作,在阻塞期间实现了消息循环。

对于 CSocket,处理网络事件通知的函数 OnAcceptOnCloseOnReceive 仍然可以使用,**OnConnectOnSend** 在 CSocket 中一般不会被调用。

CSocket对象在调用AcceptConnectSendReceiveClose等成员函数后,这些函数在完成任务之后才会返回。因此,ConnectSend不会导致OnConnectOnSend被调用。如果覆盖虚拟函数OnReceiveOnAcceptOnClose,则在网络事件到达之后导致对应的虚拟函数被调用,在对应的函数中应该调用CAsyncSocketReceiveAcceptClose来完成操作。

以下是对 CSocket 类的 Receive 函数的解释:

 int CSocket::Receive(void* lpBuf, int nBufLen, int nFlags)
 {
     // m_pbBlocking 是 CSocket 的成员变量,用来标识当前是否正在进行阻塞操作。
     if (m_pbBlocking != NULL)
     {
         WSASetLastError(WSAEINPROGRESS);
         return FALSE;
     }
 ​
     // 完成数据读取
     int nResult;
     while ((nResult = CAsyncSocket::Receive(lpBuf, nBufLen, nFlags)) == SOCKET_ERROR)
     {
         if (GetLastError() == WSAEWOULDBLOCK)
         {
             // 进入消息循环,等待网络事件 FD_READ
             if (!PumpMessages(FD_READ))
                 return SOCKET_ERROR;
         }
         else
         {
             return SOCKET_ERROR;
         }
     }
     return nResult;
 }

Receive 函数首先判断当前 CSocket 对象是否正在处理一个阻塞操作,如果是,则返回错误 WSAEINPROGRESS;否则,开始数据读取的处理。

在读取数据时,如果基类 CAsyncSocketReceive 读取到了数据,则返回;否则,如果返回一个错误,而且错误号是 WSAEWOULDBLOCK,则表示操作阻塞,于是调用 PumpMessage 进入消息循环检测数据是否到达(网络事件 FD_READ 发生,其中通过调用 PeekMessage 函数主动从消息队列中获取消息)。数据到达之后退出消息循环,再次调用 CAsyncSocketReceive 读取数据,直到没有数据可读为止。

PumpMessagesCSocket 成员函数,它完成以下工作:

  1. 设置 m_pbBlocking,表示进入阻塞操作。

  2. 进行消息循环,如果有以下事件发生则退出消息循环:

    • 收到指定定时器的定时事件消息 WM_TIMER,退出循环,返回 TRUE

    • 收到发送给本 socket 的消息 WM_SOCKET_NOTIFY,且如果网络事件 FD_CLOSE 或者等待的网络事件发生,退出循环,返回 TRUE

    • 发送错误或者收到 WM_QUIT 消息,退出循环,返回 FALSE

  3. 在消息循环中,把 WM_SOCKET_DEAD 消息和发送给其他 socket 的通知消息 WM_SOCKET_NOFITY 放进模块线程状态的通知消息列表 m_listSocketNotifications,在阻塞操作完成之后处理;

  4. 对其他消息,则把它们送给目的窗口的窗口过程处理。

5.2.1 创建 CSocket 对象

分为两个步骤:

  1. 调用 CSocket 类的构造函数,创建一个空的 CSocket 对象。

  2. 调用此 CSocket 对象的 Create() 成员函数,创建对象的底层套接字。调用格式是:

     BOOL Create(
         UINT nSocketPort = 端口号,
         Int nSocketPort = SOCK_STREAM | SOCK_DGRAM,
         LPCTSTR lpszSocketAddress = 套接字所用的网络地址
     );

    如果打算使用 CArchive 对象和套接字一起进行数据传输工作,必须使用流式套接字。

5.2.2 建立连接

CSocket 类重载了 CAsyncSocketAcceptSendReceiveClose 函数。服务器端使用重载的 Accept 函数接收客户端请求。

 BOOL CSocket::Accept(CAsyncSocket& rConnectedSocket, SOCKADDR* lpSockAddr, int* lpSockAddrLen)
 {
     if (m_pbBlocking != NULL)
     {
         WSASetLastError(WSAEINPROGRESS);
         return FALSE;
     }
     while (!CAsyncSocket::Accept(rConnectedSocket, lpSockAddr, lpSockAddrLen))
     {
         if (GetLastError() == WSAEWOULDBLOCK)
         {
             if (!PumpMessages(FD_ACCEPT))
                 return FALSE;
         }
         else
             return FALSE;
     }
     return TRUE;
 }

客户端建立连接时使用 Connect() 函数。

CSocket 对象从不调用 OnConnect() 事件处理函数。

5.2.3 发送和接收数据

在创建 CSocket 类对象后,对于数据报套接字,直接使用 CSocket 类的 SendTo()ReceiveFrom() 成员函数来发送和接收数据。

对于流式套接字,首先在服务器和客户机之间建立连接,然后使用 CSocket 类的 Send()Receive() 成员函数来发送和接收数据,它们的调用方式与 CAsyncSocket 类相同。不同的是:CSocket 类的这些函数工作在阻塞的模式。例如,一旦调用了 Send() 函数,在所有的数据发送之前,程序或线程将处于阻塞的状态。一般将 CSocket 类与 CArchive 类和 CSocketFile 类结合,来发送和接收数据,这将使编程更为简单。

CSocket 对象从不调用 OnSend() 事件处理函数。

5.2.4 CSocket类与CArchive类和CSocketFile类

使用 CSocket 类的最大优点在于,应用程序可以在连接的两端通过 CArchive 对象来进行数据传输。具体做法是:

  1. 创建 CSocket 类对象

  2. 创建一个基于 CSocketFile 类的文件对象,并将上面已创建的 CSocket 对象的指针传给它。

  3. 分别创建用于输入和输出的 CArchive 对象,并将它们与这个 CSocketFile 文件对象连接。

  4. 利用 CArchive 对象来发送和接收数据。

 class CSocketFile : public CFile
 {
     DECLARE_DYNAMIC(CSocketFile)
 public:
     CSocketFile(CSocket* pSocket, BOOL bArchiveCompatible = TRUE);
 public:
     CSocket* m_pSocket;
     BOOL m_bArchiveCompatible;
     virtual ~CSocketFile();
 #ifdef _DEBUG
     virtual void AssertValid() const;
     virtual void Dump(CDumpContext& dc) const;
 #endif
     virtual UINT Read(void* lpBuf, UINT nCount);
     virtual void Write(const void* lpBuf, UINT nCount);
     virtual void Close();
 };
CSocketFile

CSocketFile 类实际上并不用于在Socket双方直接发送文件,而是用于将需要序列化的数据(例如结构体数据)传递给对方。通过与 CArchive 类结合,可以轻松地在连接的两端进行数据传输。下面是一个示例,演示如何将文档数据通过 CSocketFile 传递给另一方:

 CSocketFile file(pSocket);
 CArchive ar(&file, CArchive::store);
 pDocument->Serialize(ar);
 ar.Close();

同样,接收方可以通过稍作修改即可接收数据:

 CArchive ar(&file, CArchive::load);
 // 从 ar 中读取数据并进行相应的处理

需要注意的是,CSocketFile 类虽然从 CFile 派生,但它屏蔽了 CFile::Open() 等函数。因此,不能调用 CSocketFileOpen 函数来打开一个实际的文件,否则会导致异常。如果要使用 CSocketFile 传送文件,必须提供自定义的实现来替代这些函数。

另外,CArchive不支持在datagramSocket连接上序列化操作。

CArchive 类

CArchive 对象在初始化时,内部先指定一个缓冲区作为临时存储,然后将需要保存的数据写入该缓冲区。当缓冲区被填满时,才将缓冲区中的内容写入它所指向的 CFile 文件对象中。

对于读取数据,CArchive 将数据从文件读取到指定的缓冲区,然后从缓冲区读取到与对象相关联的文件中。这样,使用缓冲区不仅减少了对物理硬盘的操作次数,而且提高了程序的运行速度。

构造函数原型

 CArchive::CArchive(CFile *pfile, UINT nMode, int nBufsize = 4096, void *lpBuf = NULL);
  • 参数 pfile 指向需要进行串行化的对象指针。

  • 参数 nMode 是设置创建对象的标志。如果设置了此标志,则必须在对象销毁前调用 Close() 函数关闭文件,否则文件中的数据将会被损坏。

  • 参数 nBufsize 是一个可选参数,指定缓冲区的大小,默认为4096字节。

  • 参数 lpBuf 是一个可选指针,指向用户提供的缓冲区。如果不指定此参数,存档会从本地堆中分配一个缓冲区,并在对象被销毁时释放该缓冲区。存档不会释放用户提供的缓冲区。

常用标志

常用标志标志所示意义
CArchive::load(store)从文件中读取(保存)数据
CArchive::bNoFlushOnDelete防止 CArchive 对象在被销毁时自动调用 Flush 进行更新

示例代码

 CSocket *m_clientsocket = new CSocket; // 创建套接字
 CSocketFile *m_sockfile = new CSocketFile(m_clientsocket); // 创建与 m_clientsocket 关联的对象
 CArchive *m_archive = new CArchive(m_sockfile, CArchive::load | CArchive::store, 1024, NULL);

在上述代码中,为创建的串行化对象 m_archive 设置了一个大小为1024字节的缓冲区。最后一个参数设为 NULL,表示缓冲区由系统决定。

串行化操作

CArchive 类中,可以使用 ReadString()WriteString() 函数进行 CSocketFile 文件的读写操作。这两个函数均包含一个字符串类型的参数,具体含义如下:

  • ReadString(CString str1): 读取后保存的字符串数据。

  • WriteString(CString str2): 写入的字符串数据。

除了上述方法,还可以使用基本的串行化操作方法:

 m_archive << str2;    // 向串行化对象 m_archive 写入字符串 str2
 m_archive >> str1;    // 从串行化对象 m_archive 读出数据到 str1
 m_archive->Close();   // 关闭串行化对象 m_archive

需要注意,在关闭串行化对象后,与其相关联的文件对象也会随之被关闭。CArchive::Close() 函数用于清除 CArchive 类创建时指定的缓冲区,然后关闭 CArchive 对象,将其与相关联的 CSocketFile 对象分离。

如果用户需要立即将数据写入到串行化对象中,需要使用 Flush 函数。它主要用于将缓冲区中剩余的数据强制地写入 CArchive 对象所关联的文件中:

 m_archive->WriteString(str + "\r\n");  // 在这里也可以使用 m_archive << str << "\r\n";
 m_archive->Flush();                    // 强制将数据 str 写入到串行化对象中
 m_archive->Close();                    // 关闭串行化对象

在程序中,如果没有调用 Flush() 函数,真正将数据写入到物理磁盘是在调用 Close() 函数关闭串行化对象之后。因此,一些重要的数据需要使用 Flush() 函数立即写入文件,以防丢失。

5.2.5 关闭套接字和清除相关的对象

在使用完 CSocket 对象后,应用程序应调用它的 Close() 成员函数来释放套接字占用的系统资源。也可以调用它的 ShutDown() 成员函数来禁止套接字读写。对于相应的 CArchive 对象、CSocketFile 对象和 CSocket 对象,可以将它们销毁;也可以不作处理,因为当应用程序终止时,会自动调用这些对象的析构函数,从而释放这些对象占用的资源。

5.3 CSocket 类的编程模型

下面给出针对流式套接字的 CSocket 类的编程模型。分为服务器端和客户端。

1. 服务器端

 // 创建空的服务器端监听套接字对象。
 CSocket sockServ;
 ​
 // 用众所周知的端口,创建监听套接字对象的底层套接字句柄。
 sockServ.Create(nPort);
 ​
 // 启动对于客户端连接请求的监听。
 sockServ.Listen();
 ​
 // 创建空的服务器端连接套接字对象。
 CSocket sockRecv;
 ​
 // 接收客户端的连接请求,并将其他的任务转交给连接套接字对象。
 sockServ.Accept(&sockRecv);
 ​
 // 创建文件对象并关联到连接套接字对象。
 CSockFile *file;
 file = new CSockFile(&sockRecv);
 ​
 // 创建用于输入的归档对象。
 CArchive *arIn, arOut;
 arIn = CArchive(&file, CArchive::load);
 ​
 // 创建用于输出的归档对象。归档对象必须关联到文件对象。
 arOut = CArchive(&file, CArchive::store);
 ​
 // 进行数据输入和输出。输入或输出可以反复进行。
 arIn >> dwValue;
 arOut << dwValue;
 ​
 // 传输完毕,关闭套接字对象。
 sockRecv.Close();
 sockServ.Close();

2. 客户端

 // 创建空的客户端套接字对象。
 CSocket sockClient;
 ​
 // 创建套接字对象的底层套接字。
 sockClient.Create();
 ​
 // 请求连接到服务器。
 sockClient.Connect(strAddr, nPort);
 ​
 // 创建文件对象并关联到套接字对象。
 CSockFile *file;
 file = new CSockFile(&sockClent);
 ​
 // 创建用于输入的归档对象。
 CArchive *arIn, arOut;
 arIn = CArchive(&file, CArchive::load);
 ​
 // 创建用于输出的归档对象。归档对象必须关联到文件对象。
 arOut = CArchive(&file, CArchive::store);
 ​
 // 进行数据输入和输出。输入或输出可以反复进行。
 arIn >> dwValue;
 arOut << dwValue;
 ​
 // 传输完毕,关闭套接字对象。
 sockClient.Close();

这些示例代码提供了使用 CSocket 类进行服务器端和客户端编程的基本模型。在服务器端,首先创建监听套接字对象并开始监听,然后通过接受连接请求创建连接套接字对象,最后进行数据的输入和输出。在客户端,创建套接字对象后,请求连接到服务器,然后进行数据的输入和输出,最后关闭套接字对象。

第六章

WinInet 是 Windows Internet 扩展应用程序高级编程接口,是专为开发具有 Internet 功能的客户端应用程序而提供的。

它有两种形式:

  • WinInet API包含一个C语言的函数集(Win32 Internet functions)

  • MFC WinInet类层次则是对前者的面向对象的封装

WinInet API 的特点

1、WinInet 是一个应用层网络编程接口,包含了 Internet 常用协议 HTTP ,FTP 。

2、WinInet API 接口封装了 TCP/IP 和特定 Internet 应用层协议及 Winsock 的编程细节,可用于快速编写 Internet 客户端程序。

3、WinInet 为 HTTP 、 FTP 客户端应用提供了统一 Windows API 接口函数,使用方便。

4、WinInet 简化了 HTTP 、 FTP 客户端程序的编程,可轻松地将上述 Internet 应用层客户端功能集成到应用程序中。例如,为了获取 Web Server 上的一个文件时,只需要调用 WinInet 函数即可,而不需要手动编码 HTTP 请求串,或解码 HTTP 响应串。

6.1 WinInet API 的导入

WinInet API 的函数原型定义在 Wininet.h 头文件中,对应的函数实现在 Wininet.lib 库文件中。

要想成功地编译使用 WinInet API 的应用程序,正在使用的 C/C++ 的 include 目录中必须有 Wininet.h 头文件,同时所建工程需要导入 Wininet.lib 库文件。

使用 WinInet API 的一般流程:

1、首先,调用 InternetOpen 函数获取一个 HINTERNET 句柄(会话句柄)

2、然后,调用 InternetConnect 函数创建一个指定的连接,它将通过传递给它的参数为指定的站点初始化 HTTP 、 FTP 连接,并创建一个新的的 HINTERNET 句柄,一般将这个句柄命名为 hConnection ( 连接句柄 )。

3、针对不同的服务类型,调用不同的函数,对 hConnection 进行操作。

4、操作完毕,调用 InternetCloseHandle 函数关闭打开的句柄。

6.1.1 WinINet API 函数使用的 HINTERNET 句柄

HINTERNET 句柄是一种特殊的数据类型,是由 WinINet API 函数创建的,大多数 WinINet API 函数通过使用 HINTERNET 类型的句柄来实现它的操作。

HINTERNET 句柄与常规 Windows 句柄不同,常规 Windows 句柄也不能在 WinINet API 函数中使用。

可以用一种树状结构来描述各类 HINTERNET 句柄及其产生/使用这些句柄的相关函数的层次结构:

  • 产生会话句柄的 InternetOpen 函数位于树的根;

  • 第二层是返回的连接句柄的 InternetConnectInternetOpenUrl 函数;

  • 第三层则是使用连接句柄的 FtpFindFirstFileFtpOpenFileHttpOpenRequest 等函数。

产生各种 HINTERNET 句柄的 WinINet 函数形成的树形体系结构

image-20231212224445094

 HINTERNET WINAPI InternetOpenA(
     IN LPCSTR lpszAgent,
     IN DWORD dwAccessType,
     IN LPCSTR lpszProxy OPTIONAL,
     IN LPCSTR lpszProxyBypass OPTIONAL,
     IN DWORD dwFlags
 );
 ​
 HINTERNET WINAPI InternetOpenW(
     IN LPCWSTR lpszAgent,
     IN DWORD dwAccessType,
     IN LPCWSTR lpszProxy OPTIONAL,
     IN LPCWSTR lpszProxyBypass OPTIONAL,
     IN DWORD dwFlags
 );
 ​
 #ifdef UNICODE
 #define InternetOpen  InternetOpenW
 #else
 #define InternetOpen  InternetOpenA

lpszAgent 指向以 null 结尾的字符串的指针,该字符串指定调用 WinINet 函数的应用程序或实体的名称。 此名称用作 HTTP 协议中的用户代理。

dwAccessType Type of access required. This parameter can be one of the following values.

INTERNET_OPEN_TYPE_DIRECT 直接连接到服务器。

INTERNET_OPEN_TYPE_PRECONFIG 使用 IE 中的连接设置。

INTERNET_OPEN_TYPE_PRECONFIG_WITH_NO_AUTOPROXY 使用 IE 中的连接设置,禁止 Microsoft JScript 或 Internet 设置(INS 文件)的使用。

INTERNET_OPEN_TYPE_PROXY 通过代理服务器进行连接

lpszProxyName 指向以 null 结尾的字符串的指针,该字符串指定代理服务器的名称,仅当将参数 dwAccessType 设置为 INTERNET_OPEN_TYPE_PROXY 时有效。

Wininet 函数仅能识别 CERN 类型代理(仅支持 http),TIS FTP 网关(仅支持 FTP),以及 SOCKS 代理(也称为全能代理),可支持多种协议,包括 http、ftp 请求及其他类型的请求。其中,SOCKS 代理包括 socks 4 和 socks 5,socks 4 只支持 TCP 协议,socks 5 支持 TCP/UDP 协议,还支持各种身份验证机制等协议。其标准端口为 1080。

lpszProxyBypass 指向以 null 结尾的字符串的指针,当 dwAccessType 设置为 INTERNET_OPEN_TYPE_PROXY 时,该参数表示不应通过的代理的主机名或 IP 地址。

dwFlags 选项参数。可取下列值之一

含义
INTERNET_FLAG_ASYNC仅对从此函数返回的句构降序的句构发出异步请求。
INTERNET_FLAG_FROM_CACHE不发出网络请求。所有实体都从缓存返回。如果请求的项不在缓存中,则返回合适的错误,例如 ERROR_FILE_NOT_FOUND。
INTERNET_FLAG_OFFLINE与INTERNET_FLAG_FROM_CACHE相同。不发出网络请求。所有实体都从缓存返回。如果请求的项不在缓存中 则返回合适的错误,例如ERROR_FILE_NOT_FOUND。

InternetOpen 返回值

成功: 返回一个有效的 HINTERNET 句柄,该句柄将由应用程序传递给接下来的 WinINet 函数。

失败: 返回 NULL

备注

函数必须是第一个由应用程序调用的 WinINet 函数 。它初始化内部数据结构并准备接收应用程序之后的其他调用。当应用程序结束使用 WinINet 函数时,应调用 InternetCloseHandle 函数来释放与之相关的资源。

关于代理服务器地址的设置

如果要自己配置代理服务器,InternetOpen 的第 3 个参数要设置成代理服务器的 IP 地址。代理的格式必须为:

[<protocol>=][<scheme>://]<proxy>[:<port>]

其中 protocol, scheme://, :port 是可选项,如果忽略这三者,则它们默认分别为 HTTP, HTTP://, :80。即默认为 HTTP 代理。

各种常用代理地址格式如下:

http= http://proxyserver:port

ftp= ftp://proxyserver:port

socks= socks://proxyserver:port

如果代理有用户名/密码,可以用下面的函数设置:

 InternetSetOption(hHandle, INTERNET_OPTION_USERNAME, "", );
 InternetSetOption(hHandle, INTERNET_OPTION_PASSWORD, "", );

6.1.2 典型的操作流程和它们使用的句柄

  1. 使用 InternetOpenUrl 直接打开因特网上指定的文件。

    依赖由 InternetOpenUrl 所创建句柄的三个函数。

    img

     HINTERNET InternetOpenUrl(
         HINTERNET hInternet,
         LPCTSTR lpszUrl,
         LPCTSTR lpszHeaders,
         DWORD dwHeadersLength,
         DWORD dwFlags,
         DWORD_PTR dwContext
     );

    参数:

    hInternet:当前的 Internet 会话句柄。句柄必须由前期的 InternetOpen 调用返回。

    lpszUrl:一个空字符结束的字符串变量的指针,指定读取的网址。只有以 ftp, http 或者 https 开头的网址被支持。

    lpszHeaders:一个空字符结束的字符串变量的指针,指定发送到 HTTP 服务器的头信息。

    dwHeadersLength:参数3的长度。

    dwFlags:此参数可为下列值之一。

    INTERNET_FLAG_EXISTING_CONNECT

    INTERNET_FLAG_HYPERLINK

    INTERNET_FLAG_IGNORE_CERT_CN_INVALID

    INTERNET_FLAG_IGNORE_CERT_DATE_INVALID

    INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP

    INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS

    INTERNET_FLAG_KEEP_CONNECTION

    INTERNET_FLAG_NEED_FILE

    INTERNET_FLAG_NO_AUTH

    INTERNET_FLAG_NO_AUTO_REDIRECT

    INTERNET_FLAG_NO_CACHE_WRITE

    INTERNET_FLAG_NO_COOKIES

    INTERNET_FLAG_NO_UI

    INTERNET_FLAG_PASSIVE

    INTERNET_FLAG_PRAGMA_NOCACHE

    INTERNET_FLAG_RAW_DATA

    INTERNET_FLAG_RELOAD

    INTERNET_FLAG_RESYNCHRONIZE

    INTERNET_FLAG_SECURE

    dwContext:指向变量的指针,该变量用于异步模式时回调函数返回的句柄的应用程序上下文。

    返回值: 如果已成功建立到 FTP 或 HTTP URL 的连接,返回一个有效的句柄,如果连接失败返回 NULL。要检索特定的错误信息,请调用 GetLastError。要确定为什么对服务器的访问被拒绝,请调用 InternetGetLastResponseInfo

    备注:

    • 这是一个通用的函数,可用于使用任何 WinINet 支持的协议检索数据。这个函数在应用程序并不需要指定特定的协议,只需要相应的 URL。InternetOpenUrl 函数解析 URL 字符串,建立连接到服务器,并准备下载的指定 URL 的数据。该应用程序可以用 InternetReadFile(对文件)或 InternetFindNextFile(对目录)来检索 URL 的数据。不需要在 InternetOpenUrl 前调用 InternetConnect

    • 在使用完 InternetOpenUrl 返回的 HINTERNET 句柄后,必须使用 InternetCloseHandle 函数关闭它。

2. FTP 操作

(1) 对 FTP 服务器的目录和文件进行操作

流程图: 对FTP服务器的目录和文件进行操作的流程

image-20231212231547829

 HINTERNET InternetConnect(
     HINTERNET hInternet,
     LPCTSTR lpszServerName,
     INTERNET_PORT nServerPort,
     LPCTSTR lpszUsername,
     LPCTSTR lpszPassword,
     DWORD dwService,
     DWORD dwFlags,
     DWORD_PTR dwContext
 );

参数:

hInternet [in]: 由前期的 InternetOpen 调用返回的句柄。

lpszServerName [in]: 指向以 null 结尾的字符串的指针,指定 Internet 服务器的主机名或站点的 IP 地址。

nServerPort [in]: 服务器上的端口。可使用以下默认值之一:

  • INTERNET_DEFAULT_FTP_PORT (port 21)

  • INTERNET_DEFAULT_HTTP_PORT (port 80)

  • INTERNET_DEFAULT_HTTPS_PORT (port 443)

  • INTERNET_DEFAULT_SOCKS_PORT (port 1080) 使用由 dwService 指定的服务的默认端口。

pszUsername [in]: 指向以 null 结尾的字符串的指针,指定要登录的用户名。如果为 NULL,则使用适当的默认值。对于 FTP 协议,默认值为 "anonymous"。

lpszPassword [in]: 指向以 null 结尾的字符串的指针,指定登录的密码。如果为 NULL,使用默认的 "anonymous" 密码。如果 lpszPasswordNULLlpszUsername 不为 NULL,则使用空白密码。

dwService [in]: 要访问的服务类型。

  • INTERNET_SERVICE_FTP (FTP service)

  • INTERNET_SERVICE_HTTP (HTTP service)

dwFlags [in]: 特定于所用服务的选项。

dwContext [in]: 指向变量的指针,该变量包含应用程序定义的值,用于标识回调中返回的句柄的应用程序上下文。

返回值: 如果连接成功,则返回会话的有效句柄,否则返回 NULL。要检索扩展的错误信息,请调用 GetLastError。应用程序还可以使用 InternetGetLastResponseInfo来确定拒绝访问服务的原因。

(2) 操作 FTP 服务器上的文件

使用内存缓冲区来操作FTP服务器上的文件。

image-20231212231709448

(3) 查询 FTP 服务器上的文件

流程图: 查询 FTP 服务器上的文件

image-20231212231716579

(4)HTTP 处理函数

HttpOpenRequest 创建一个 HTTP 请求的句柄

  • HttpSendRequest(Ex) 向 HTTP 服务器发送指定的请求

  • HttpQueryInfo 查询有关一次 HTTP 请求的信息

  • HttpEndRequest 结束一个 HTTP 请求

  • HttpAddRequestHeaders 添加一个或多个 HTTP 请求报头到 HTTP 请求句柄

6.1.3 获取 WinInet API 函数执行的错误信息

在使用 WinInet API 函数时,根据函数返回值的类型,可以分为两种情况:返回 HINTERNET 句柄型和返回布尔型。

  1. 返回 HINTERNET 句柄型的函数

    • 成功时: 返回一个有效的句柄。

    • 失败时: 返回 NULL

  2. 返回布尔型的函数

    • 成功时: 返回 TRUE

    • 失败时: 返回 FALSE

在函数调用失败后,用户通常需要了解导致失败的具体原因。为了获取更具体的错误信息,可以随即调用 GetLastError 函数。

 DWORD dwError = GetLastError();
 ​
 // Use dwError to determine the specific error
 // ...
 ​
 // Example: Convert error code to a human-readable string
 LPVOID lpMsgBuf;
 FormatMessage(
     FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
     NULL,
     dwError,
     0, // Default language
     (LPWSTR)&lpMsgBuf,
     0,
     NULL
 );
 ​
 // Display the error message
 wprintf(L"Error: %s\n", lpMsgBuf);
 ​
 // Free the buffer
 LocalFree(lpMsgBuf);

上述代码中,GetLastError 返回的错误码 dwError 可以通过 FormatMessage 函数转换为人类可读的错误消息。这样,应用程序可以更容易地诊断问题并采取适当的措施。

请注意,这里的代码以 Unicode 字符串(LPWSTR)为例。如果你的应用程序使用 ANSI 字符串,你可以将 LPWSTR 替换为 LPSTR

6.1.5 WinInet API 的异步操作模式

为了使 WinInet 以异步方式操作,应用程序需要执行以下四个步骤:

  1. 设置异步方式标志:

    在调用 InternetOpen 函数时,使用 INTERNET_FLAG_ASYNC 选项,将使后续所有使用该 InternetOpen 调用返回的句柄及其衍生句柄的函数以异步方式执行。

     HINTERNET hInternet = InternetOpen(L"YourApp", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, INTERNET_FLAG_ASYNC);
  2. 设置非零的环境值:

    确保在 InternetOpen 调用中设置了非零的上下文值,通常是通过传递非零的 dwContext 参数。

     HINTERNET hInternet = InternetOpen(L"YourApp", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, INTERNET_FLAG_ASYNC, 1);
  3. 定义并实现一个状态回调函数:

    应用程序必须定义并实现一个状态回调函数,该函数将在异步操作的不同阶段被调用。

     void CALLBACK StatusCallback(
         HINTERNET hInternet,
         DWORD_PTR dwContext,
         DWORD dwInternetStatus,
         LPVOID lpvStatusInformation,
         DWORD dwStatusInformationLength
     ) {
         // Handle different status notifications
         // ...
     }
  4. **为句柄注册有效的回调函数:

    调用 InternetSetStatusCallback 函数为使用异步方式操作的句柄注册状态回调函数。

     InternetSetStatusCallback(hInternet, StatusCallback);

    默认情况下,WinInet API 函数是同步操作的。如果需要异步操作,应用程序可以在调用 InternetOpen 函数时使用 INTERNET_FLAG_ASYNC 选项。这时,以后所有使用由该InternetOpen 调用返回的句柄及其衍生句柄的函数都将异步执行。

    对于需要进行异步操作的应用程序来说,还必须调用InternetSetStatusCallback函数为函数所使用的句柄注册一个回调函数。

    此后,该句柄上的所有操作都能够产生状态标识(该句柄在被创建时使用的必须是非0 的上下文ID,否则即使在InternetOpen函数中指定了INTERNET_FLAG_ASYNC 标志,操作也会被强制同步完成)。

    状态标识主要用于向应用程序提供网络操作进程信息。

    在接下来的操作中,使用异步方式的句柄将生成状态标识,这些标识提供有关网络操作进程的信息。对于一个句柄,可以设置三个特殊的状态标识:

    • INTERNET_STATUS_HANDLE_CLOSING 表示句柄上的最后一个状态标识。

    • INTERNET_STATUS_HANDLE_CREATED 表示句柄刚被创建。

    • INTERNET_STATUS_REQUEST_COMPLETE 表示异步操作完成。

    在接收到 INTERNET_STATUS_REQUEST_COMPLETE 标识后,应用程序必须检查 INTERNET_ASYNC_RESULT 结构以确定操作是否成功。

6.1.6 回调函数的定义实现与注册

1.回调函数的原型

 VOID (CALLBACK * INTERNET_STATUS_CALLBACK)(
     HINTERNET hInternet,
     DWORD dwContext,
     DWORD dwInternetStatus,
     LPVOID lpvStatusInformation,
     DWORD dwStatusInformationLength
 );

异步操作已完成。lpvStatusInformation 参数包含 INTERNET_ASYNC_RESULT 结构的地址。

2 INTERNET_ASYNC_RESULT 结构的定义

此结构包含异步回调函数的结果。

 typedef struct {
     DWORD dwResult;
     DWORD dwError;
 } INTERNET_ASYNC_RESULT, * LPINTERNET_ASYNC_RESULT;

dwResult:此参数可以是 HINTERNET 句柄、无符号长整型或来自异步函数的布尔返回码。

dwError:错误代码,如果 dwResult 指示函数失败。如果操作成功,此成员通常包含 ERROR_SUCCESS

备注:dwResult 的值由状态回调函数中的 dwInternetStatus 的值确定。

INTERNET_STATUS_HANDLE_CREATED:指向 HINTERNET 句柄的指针。

INTERNET_STATUS_REQUEST_COMPLETE:异步函数的布尔返回码。

3 注册句柄的回调函数

调用 InternetSetStatusCallback 函数可以建立回调函数与句柄的关联。

 INTERNET_STATUS_CALLBACK InternetSetStatusCallback(
     HINTERNET hInternet,
     INTERNET_STATUS_CALLBACK lpfnInternetCallback
 );

4 举例

下面的例子给出了一个回调函数的例子,和一个调用 InternetSetStatusCallback 来注册回调函数的例子。

 // 定义了一个回调函数,函数名是用户自己定义的。
 void CALLBACK InternetCallback(
     HINTERNET hInternet,
     DWORD dwcontext,
     DWORD dwInternetStatus,
     LPVOID lpvStatusInformation,
     DWORD dwStatusInformationLength
 )
 {
     // 在这里插入回调函数的实现代码。
 }
 ​
 // 定义了一个 INTERNET_STATUS_CALLBACK 型的变量
 INTERNET_STATUS_CALLBACK dwISC;
 ​
 // 建立句柄与回调函数的关联
 dwISC = InternetSetStatusCallback(hInternet, (INTERNET_STATUS_CALLBACK) InternetCallback);

6.3 MFC WinInet

6.3.1 概述

微软在MFC基础类库中提供了WinInet类,它是对于 WinInet API 函数的封装,是对所有的WinInet API函数按其应用类型进行分类和打包后,以面向对象的形式,向用户提供的一个更高层次上的更容易使用的编程接口。

利用MFC WinInet类来编写Internet应用程序还具有以下优点:

(1)提供缓冲机制。

(2)支持安全机制。

(3)支持Web代理服务器访问。

(4)常见Internet错误的异常处理

(5)轻松简洁。具有默认参数,自动清除打开的句柄和连接。

MFC WinInet 类的关系

image-20231212232650177

6.3.2 MFC WinInet所包含的类

MFC WinInet类在Afxinet.h包含文件中定义,不同的类是对不同层次的HINTERNET句柄的封装,分为以下几种:

  1. CInternetSession类

    直接继承自CObject类,用来创建并初始化一个或多个同步的Internet会话,还可以处理与代理服务器的连接。如果应用程序所使用的Internet连接必须保持一段时间,则可以在CWinApp类中创建相应的CInternetSession成员。

    成员函数:

    QueryOption:提供可能的错误检测判断

    SetOption:设置Internet会话的选项

    OpenURL:设置URL,并对其进行分析

    GetFtpConnection:打开一个FTP会话并进行连接

    GetHttpConnection:打开HTTP服务器并进行连接

    EnableStatusCallback:建立异步操作的状态回调

    ServiceTypeFromHandle:通过Internet句柄返回服务器类型

    GetContext:获取Internet和应用程序会话句柄

    Close:关闭Internet连接

    创建CInternetSession类对象,将创建并初始化Internet会话。像其它类一样,创建CInternetSession类对象需要执行该类的构造函数,它的原型是

     CInternetSession(
         LPCTSTR pstrAgent = NULL,
         DWORD dwContext = 1,
         DWORD dwAccessType = INTERNET_OPEN_TYPE_PRECONFIG,
         LPCTSTR pstrProxyName = NULL,
         LPCTSTR pstrProxyBypass = NULL,
         DWORD dwFlags = 0
     );

    成员函数是对使用Internet会话根句柄的WinInet API的相关函数的封装。

    查询或设置Internet请求选项

    创建CInternetSession类对象后,可以使用其QueryOption成员函数查询Internet请求选项,以及使用SetOption成员函数来设置这些选项。

    创建连接类对象

    通过调用CInternetSession对象的GetFtpConnectionGetHttpConnection成员函数,可以分别建立CInternetSession对象与网络上FTP、HTTP服务器的连接,并分别创建CFtpConnectionCHttpConnection类的对象,用于代表这两种连接。

    重载OnStatusCallback函数

    WinInet类封装了WinInet API的异步操作模式,并将此模式与Windows的消息驱动机制结合起来。对于CInternetSession类,用户可以通过以下步骤实现异步回调:

    1. 派生自己的Internet会话类。

    2. 重载派生类的状态回调函数,其函数原型:

       virtual void OnStatusCallback(
           DWORD dwContext,                 // 与调用此函数的操作相关的环境值
           DWORD dwInternetStatus,          // 回调函数调用的原因
           LPVOID lpvStatusInformation,    // 相关的信息
           DWORD dwStatusInformationLength  // 相关的信息长度
       );
    3. 启用状态回调函数,调用CInternetSessionEnableStatusCallback成员函数,允许MFC框架在相应事件发生时自动调用状态回调函数,向用户传递会话的状态信息,从而启动异步模式。

    2. 连接类:包括CInternetConnection类及其派生类

    CInternetConnection管理与Internet服务器的连接

    CHttpConnection管理与HTTP服务器的连接

    CFtpConnection管理与FTP服务器的连接,可以对服务器上的文件和目录进行直接操作

CInternetConnection类成员
函数名简介
CHttpConnection()构造函数,用于创建CHttpConnection对象。
OpenRequest()创建并打开一个HTTP请求。
Close()关闭HTTP连接。
SendRequest()向HTTP服务器发送HTTP请求。
QueryInfoStatusCode()查询HTTP响应的状态码。
QueryInfo()查询HTTP响应的头信息。
AddRequestHeaders()添加自定义的HTTP请求头。
SendRequestEx()扩展的发送HTTP请求的方法,支持更多的选项。
EndRequest()结束HTTP请求。
GetRequestHeaders()获取HTTP请求的头信息。
SetTimeouts()设置HTTP请求的超时时间。
GetConnection()获取与HTTP连接相关联的CInternetConnection对象。
GetErrorText()获取HTTP请求过程中的错误信息。
QueryAuthInfo()查询身份验证信息,用于HTTP身份验证。
QueryProxyInfo()查询代理信息,用于连接到HTTP代理。
GetStatus()获取HTTP请求的状态信息。
ReadFile()从HTTP连接读取数据。
WriteFile()向HTTP连接写入数据。
 CHttpFile* OpenRequest(
     LPCTSTR pstrVerb,
     LPCTSTR pstrObjectName,
     LPCTSTR pstrReferer = NULL,
     DWORD_PTR dwContext = 1,
     LPCTSTR* ppstrAcceptTypes = NULL,
     LPCTSTR pstrVersion = NULL,
     DWORD dwFlags = INTERNET_FLAG_EXISTING_CONNECT
 );
 ​
 CHttpFile* OpenRequest(
     int nVerb,
     LPCTSTR pstrObjectName,
     LPCTSTR pstrReferer = NULL,
     DWORD_PTR dwContext = 1,
     LPCTSTR* ppstrAcceptTypes = NULL,
     LPCTSTR pstrVersion = NULL,
     DWORD dwFlags = INTERNET_FLAG_EXISTING_CONNECT
 );
CFtpConnection

CFtpConnection 类的成员函数翻译如下:

Command: 直接向FTP服务器发送命令。

CreateDirectory: 在服务器上创建一个目录。

GetCurrentDirectory: 获取此连接的当前目录。

GetCurrentDirectoryAsURL: 获取此连接的当前目录作为URL。

GetFile: 从连接的服务器获取文件。

OpenFile: 在连接的服务器上打开文件。

PutFile: 将文件放置在服务器上。

Remove: 从服务器上删除文件。

RemoveDirectory: 从服务器上删除指定的目录。

Rename: 在服务器上重命名文件。

SetCurrentDirectory: 设置当前FTP目录。

3. 文件类:

CInternetFile类及其派生类CHttpFile:这些类实现对使用Internet协议的远程系统上的文件进行存取操作。

  • CInternetFile: 允许对使用Internet协议的远程系统中的文件进行操作。

  • CHttpFile: 提供对HTTP服务器上的文件进行操作的支持。

CFileFind类及其派生类CFtpFileFind:CFileFind类直接继承于CObject类,这些类实现对本地和远程系统上的文件的搜索和定位工作:

  • CFileFind: 对在本地系统进行文件检索提供支持。

  • CFtpFileFind: 为在FTP服务器上进行的文件检索操作提供支持。

3. CInternetException类:

代表MFC WinInet类的成员函数在执行时所发生的错误或异常。

以下是提供的文本内容的Markdown格式:

WinInet全局函数:

  1. AfxParseURL全局函数:用于解析一个URL字符串,并返回服务类型

     BOOL AFXAPI AfxParseURL(
        LPCTSTR pstrURL,              // 指向要解析的URL字符串的指针
        DWORD& dwServiceType,         // 指定Internet的服务类型,如 
                                      // AFX_INET_SERVICE_FTP
                                      // AFX_INET_SERVICE_HTTP                
                                      // AFX_INET_SERVICE_HTTPS…
        CString& strServer,           // 解析出来的主机字符串
        CString& strObject,           // 解析出来的URL内容字符串
        INTERNET_PORT& nPort          // 端口号(由Server或Object段指定的)
     );

    如果一个URL解析成功则返回非0值;如果为空URL或者不包含已知的服务类型则返回0值。例如用AfxParseURL解析URL:service://server/dir/dir/object.ext:port,执行成功后得到的值:

    strServer: "server"

    strObject: "/dir/dir/object.ext"

    nPort: port

    dwServiceType: service

  2. AfxGetInternetHandleType函数用于获取指定的Internet句柄的网络服务类型,这些返回网络服务类型在AFXINET.H中定义:

     DWORD AFXAPI AfxGetInternetHandleType(
        HINTERNET hQuery  // Internet查询的句柄
     );

    如果Handle为空或服务类型无法识别,则返回AFX_INET_SERVICE_UNK。成功则返回相应的服务类型,如INTERNET_HANDLE_TYPE_INTERNETINTERNET_HANDLE_TYPE_CONNECT_HTTP

  3. AfxThrowInternetException函数:

     void AFXAPI AfxThrowInternetException(
        DWORD dwContext,  // 引起异常的操作的上下文标识
        DWORD dwError = 0  // 引起异常的错误
     );

    这些类和全局函数除CFileFindAFX.H里声明之外,其余都在AFXINET.H文件里声明。

    MFC WinInet 类的关系

    image-20231212234451998

MFC(Microsoft Foundation Classes)中的WinInet类用于支持与Internet相关的操作,包括HTTP、FTP等协议的通信。以下是MFC中与WinInet相关的一些主要类和它们的关系:

  1. CInternetSession:

    • 代表与Internet的会话,是其他Internet相关类的起始点。

    • 通过 CInternetSession 可以创建 CHttpConnectionCFtpConnection 对象。

  2. CHttpConnection:

    • 表示与HTTP服务器的连接。

    • 通过 CInternetSession 的成员函数 GetHttpConnection 创建。

  3. CFtpConnection:

    • 表示与FTP服务器的连接。

    • 通过 CInternetSession 的成员函数 GetFtpConnection 创建。

  4. CInternetFile:

    • 允许对使用Internet协议的远程系统中的文件进行操作。

    • 可以通过 CHttpConnectionCFtpConnection 的成员函数 OpenFile 创建。

  5. CHttpFile:

    • 提供对HTTP服务器上的文件进行操作的支持。

    • 派生自 CInternetFile,通过 CHttpConnection 的成员函数 OpenRequest 创建。

  6. CFtpFileFind:

    • 为在FTP服务器上进行的文件检索操作提供支持。

    • 派生自 CFileFind,通过 CFtpConnection 的成员函数 FindFile 创建。

这些类构成了MFC中WinInet类的主要层次结构,使开发者能够方便地进行Internet通信和文件操作。CInternetException 类用于处理在执行WinInet类成员函数时可能发生的错误或异常。整体而言,这些类提供了一套封装良好的接口,简化了与Internet相关的编程任务。

一般使用MFC WinInet的流程:
  1. 创建CInternetSession对象:

    • 用于建立一个Internet会话。

  2. 建立与服务器的连接:

    • 根据协议的不同,使用 CInternetSession::GetFtpConnectionCInternetSession::GetHttpConnection 等函数来建立连接。

  3. 查询或设置Internet选项:

    • 使用 QueryOptionSetOption 函数进行Internet选项的查询或设置。

  4. 向用户反馈当前数据处理的进程信息:

    • 有时客户的应用程序在进行某些操作时,要较长时间,因此需要向用户反馈当前的状态,使用 EnableStatusCallback 函数来启用状态回调,并重载 OnStatusCallBack 函数以实现回调功能。

  5. 创建CInternetFile实例或CHttpFile实例:

    • 用于文件读写操作。

  6. 文件读写操作:

    • 调用 CInternetFile::ReadCInternetFile::Write 函数对服务器文件进行读写操作。

  7. 异常处理:

    • 为提高应用程序的可靠性和容错性,必须对可能出现的问题进行处理,这种处理通常是使用 CInternetException 类对象处理可能的异常,提高应用程序的可靠性和容错性。

  8. 结束:

    • 调用相应对象的 Close 函数销毁对象释放资源。

Internet应用的数据交换流程:

每一个Internet应用其数据交换都是建立在Internet会话(Session)的基础之上的,MFC是通过CInternetSession类对象来创建并初始化Internet会话的。用这个类不仅可以创建会话,而且可以创建几个并发的Internet会话。

为了与服务器进行通讯,除了要创建CInternetSession对象之外,还必须创建CInternetConnection对象,针对不同的协议,CInternetConnection对象有三种类型:

  • CInternetSession::GetFtpConnection

  • CInternetSession::GetHttpConnection

  • CInternetSession::GetGopherConnection

对这些函数的调用并不会读写服务器上的文件。如果要读写数据,必须要打开文件才能操作。其处理流程应该是这样的:

  1. 首先创建 CInternetSession 对象实例。

  2. 如果创建的Session要读写文件,则必须创建 CInternetFile 对象实例(或者是它的子类 CHttpFileCGopherFile 对象实例)。

    其实,读取数据最容易的方式是调用 CInternetSession::OpenURL 函数。这个函数解析你提供的统一资源定位符(URL),然后打开与URL指定的服务器连接,同时返回一个只读的 CInternetFile 对象。CInternetSession::OpenURL 不针对特定的协议类型――不管是FTP还是HTTP都可以调用,它甚至可以处理本地文件,此时返回的是 CStdioFile,而不是 CInternetFile

  3. 如果创建的Session不读写文件,而是要实现其他的任务,如删除某个FTP目录下的文件等,则你不需要创建 CInternetFile 实例。

创建 CInternetFile 对象的方法有两种:

  • 如果用 CInternetSession::OpenURL 建立与服务器的连接,调用返回 CStdioFile

  • 如果用 CInternetSession::GetFtpConnection 或者 CHttpConnection::GetHttpConnection 建立与服务器的连接,你必须调用相应的 FtpConnection::OpenFile 或者 CHttpConnection::OpenRequest,返回的内容也与 CInternetFile 或者 CHttpFile 对应。

综上所述,实现Internet客户端应用的步骤因协议而异。要看你是创建基于 OpenURL 的一般Internet客户端应用,还是使用 GetXXXConnection 函数之一针对特定协议的Internet客户端应用。

用WinInet实现Internet客户端应用程序的具体步骤和细节:

每个Internet客户端程序都伴随有一定的目的行为,如读文件、写文件、删除文件等等。客户端的程序要实现这些行为的先决条件是建立Internet连接。然后再根据不同的目的进行具体的操作。下面这些表格针对不同的应用行为列出了所需要的具体操作。其中列出了一般的Internet (FTP、或者 HTTP)客户端行为要实现某个目标所必须使用的方法。

img

image-20231212234956612

img

img

  • 24
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
第 1章 概述 1 1.1 网络编程相关的基本概念 1 1.1.1 网络编程与进程通信 1 1.1.2 Internet中网间进程的标识 3 1.1.3 网络协议的特征 7 1.2 三类网络编程 10 1.2.1 基于TCP/IP协议栈的网络编程 10 1.2.2 基于WWW应用的网络编程 10 1.2.3 基于.NET框架的Web Services网络编程 10 1.3 客户机/服务器交互模式 13 1.3.1 网络应用软件的地位和功能 13 1.3.2 客户机/服务器模式 14 1.3.3 客户机与服务器的特性 15 1.3.4 容易混淆的术语 16 1.3.5 客户机与服务器的通信过程 16 1.3.6 网络协议与C/S模式的关系 17 1.3.7 错综复杂的C/S交互 17 1.3.8 服务器如何同时为多个客户机服务 18 1.3.9 标识一个特定服务 20 1.4 P2P模式 21 1.4.1 P2P技术的兴起 21 1.4.2 P2P的定义和特征 21 1.4.3 P2P的发展 22 1.4.4 P2P的关键技术 22 1.4.5 P2P系统的应用与前景 22 习题 23 第 2章 套接字网络编程基础 24 2.1 套接字网络编程接口的产生与发展 24 2.1.1 问题的提出 24 2.1.2 套接字编程接口起源于UNIX操作系统 25 2.1.3 套接字编程接口在Windows和Linux操作系统中得到继承和发展 25 2.1.4 套接字编程接口的两种实现方式 25 2.1.5 套接字通信与UNIX操作系统的输入/输出的关系 26 2.2 套接字编程的基本概念 27 2.2.1 什么是套接字 27 2.2.2 套接字的特点 28 2.2.3 套接字的应用场合 30 2.2.4 套接字使用的数据类型和相关的问题 30 2.3 面向连接的套接字编程 32 2.3.1 可靠的传输控制协议 32 2.3.2 套接字的工作过程 33 2.3.3 面向连接的套接字编程实例 34 2.3.4 进程的阻塞问题和对策 40 2.4 无连接的套接字编程 43 2.4.1 高效的用户数据报协议 43 2.4.2 无连接的套接字编程的两种模式 43 2.4.3 数据报套接字的对等模式编程实例 45 2.5 原始套接字 47 2.5.1 原始套接字的创建 47 2.5.2 原始套接字的使用 48 2.5.3 原始套接字应用实例 49 习题 51 第3章 WinSock编程 53 3.1 WinSock概述 53 3.2 WinSock库函数 55 3.2.1 WinSock的注册与注销 55 3.2.2 WinSock的错误处理函数 58 3.2.3 主要的WinSock函数 61 3.2.4 WinSock的辅助函数 74 3.2.5 WinSock的信息查询函数 77 3.2.6 WSAAsyncGetXByY类型的扩展函数 79 3.3 网络应用程序的运行环境 82 习题 84 第4章 MFC编程 85 4.1 MFC概述 85 4.1.1 MFC是一个编程框架 85 4.1.2 典型的MDI应用程序的构成 87 4.2 MFC和Win32 89 4.2.1 MFC对象和Windows对象的关系 89 4.2.2 几个主要的类 91 4.3 CObject类 95 4.3.1 CObject类的定义 95 4.3.2 CObject类的特性 96 4.4 消息映射的实现 98 4.5 MFC对象的创建 102 4.5.1 MFC对象的关系 102 4.5.2 MFC提供的接口 104 4.5.3 MFC对象的创建过程 104 4.6 应用程序的退出 107 习题 107 第5章 MFC WinSock类的 编程 109 5.1 CAsyncSocket类 110 5.1.1 使用CAsyncSocket类的一般步骤 110 5.1.2 创建CAsyncSocket类对象 111 5.1.3 关于CAsyncSocket类可以接受并处理的消息事件 112 5.1.4 客户端套接字对象请求连接到服务器端套接字对象 114 5.1.5 服务器接收客户机的连接请求 115 5.1.6 发送与接收流式数据 116 5.1.7 关闭套接字 118 5.1.8 错误处理 118 5.1.9 其他成员函数 119 5.2 CSocket类 120 5.2.1 创建CSocket对象 120 5.2.2 建立连接 120 5.2.3 发送和接收数据 120 5.2.4 CSocket类、CArchive类和CSocketFile类 121 5.2.5 关闭套接字和清除相关的对象 122 5.3 CSocket类的编程模型 122 5.4 用CAsyncSocket类实现聊天室程序 123 5.4.1 实现目标 123 5.4.2 创建客户端应用程序 124 5.4.3 客户端程序的类与消息驱动 134 5.4.4 客户端程序主要功能的代码和分析 135 5.4.5 创建服务器端程序 142 5.4.6 服务器端程序的流程和消息驱动 144 5.4.7 点对点交谈的服务器端程序主要功能的代码和分析 145 5.5 用CSocket类实现聊天室程序 151 5.5.1 聊天室程序的功能 151 5.5.2 创建聊天室的服务器端程序 151 5.5.3 聊天室服务器端程序的主要实现代码和分析 154 5.5.4 创建聊天室的客户端程序 162 5.5.5 聊天室客户端程序的主要实现代码和分析 163 习题 170 实验 170 第6章 WinInet编程 172 6.1 MFC WinInet类 172 6.1.1 概述 172 6.1.2 MFC WinInet所包含的类 173 6.1.3 使用WinInet类编程的一般步骤 174 6.1.4 创建CInternetSession类对象 175 6.1.5 查询或设置Internet请求选项 176 6.1.6 创建连接类对象 177 6.1.7 使用文件检索类 178 6.1.8 重载OnStatusCallback函数 179 6.1.9 创建并使用网络文件类对象 180 6.1.10 CInternteException类 183 6.2 用MFC WinInet类实现FTP客户端 183 6.2.1 程序要实现的功能 183 6.2.2 创建应用程序的过程 184 习题 186 实验 187 第7章 WinSock的多线程 编程 188 7.1 WinSock为什么需要多线程编程 188 7.1.1 WinSock的两种I/O模式 188 7.1.2 两种模式的优缺点及解决方法 189 7.2 Win32操作系统下的多进程多线程机制 189 7.2.1 Win32 OS是单用户多任务的操作系统 189 7.2.2 Win32 OS是支持多线程的操作系统 190 7.2.3 多线程机制在网络编程中的应用 191 7.3 VC++对多线程网络编程的支持 192 7.3.1 MFC支持的两种线程 192 7.3.2 创建MFC的工作线程 193 7.3.3 创建并启动用户界面线程 195 7.3.4 终止线程 198 7.4 多线程FTP客户端实例 200 7.4.1 编写线程函数 200 7.4.2 添加事件处理函数 206 习题 208 第8章 WinSock的I/O模型 209 8.1 select模型 210 8.2 WSAAsyncSelect异步I/O模型 212 8.3 WSAEventSelect事件选择模型 216 8.4 重叠I/O模型 221 8.4.1 重叠I/O模型的优点 221 8.4.2 重叠I/O模型的基本原理 221 8.4.3 重叠I/O模型的关键函数和数据结构 222 8.4.4 使用事件通知实现重叠模型的步骤 225 8.4.5 使用完成例程实现重叠模型的步骤 227 8.5 完成端口模型 229 8.5.1 什么是完成端口模型 229 8.5.2 使用完成端口模型的方法 230 习题 238 第9章 HTTP及编程 239 9.1 HTTP 239 9.1.1 HTTP的背景 239 9.1.2 HTTP的内容 240 9.1.3 HTTP消息的一般格式 242 9.1.4 HTTP请求的格式 243 9.1.5 HTTP响应的格式 245 9.1.6 访问认证 248 9.1.7 URL编码 249 9.1.8 HTTP的应用 250 9.2 利用CHtmlView类创建Web浏览器型的应用程序 250 9.2.1 CHtmlView类与WebBrowser控件 250 9.2.2 CHtmlView类的成员函数 251 9.2.3 创建一个Web浏览器型的应用程序的一般步骤 256 9.3 Web浏览器应用程序实例 261 9.3.1 程序实现的目标 261 9.3.2 创建实例程序 262 习题 265 实验 265 第 10章 电子邮件协议与编程 267 10.1 电子邮件系统的工作原理 267 10.1.1 电子邮件的特点 267 10.1.2 电子邮件系统的构成 267 10.1.3 电子邮件系统的实现 268 10.2 简单邮件传送协议 270 10.2.1 概述 270 10.2.2 SMTP客户机与SMTP服务器之间的会话 270 10.2.3 常用的SMTP命令 271 10.2.4 常用的SMTP响应码 273 10.2.5 SMTP的会话过程 274 10.2.6 使用WinSock来实现电子邮件客户机与服务器的会话 274 10.3 电子邮件信件结构详述 275 10.3.1 Internet文本信件的格式标准——RFC 822 275 10.3.2 信件的头部 276 10.3.3 构造和分析符合RFC 822标准的电子信件 281 10.4 MIME编码解码与发送附件 281 10.4.1 MIME概述 281 10.4.2 MIME定义的新的信头字 282 10.4.3 MIME邮件的内容类型 283 10.4.4 MIME邮件的编码方式 292 10.5 POP3与接收电子邮件 294 10.5.1 POP3 294 10.5.2 POP3的会话过程 294 10.5.3 POP3会话的3个状态 295 10.5.4 POP3标准命令 296 10.5.5 接收电子邮件的一般步骤 298 10.6 接收电子邮件的程序实例 299 10.6.1 实例程序的目的和实现的技术要点 299 10.6.2 创建应用程序的过程 301 10.7 发送电子邮件的程序实例 302 10.7.1 实例程序的目的和实现的技术要点 302 10.7.2 创建应用程序的过程 303 习题 305 参考文献 307

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值