1. IP地址
2. 客户端/服务器模式
3. 套接字
4. Windows套接字
5. Windows套接字编程机理
6. WinSock API
7. MFC中的套接字
8. MFC中的套接字2
9. CAsyncSocket与CSocket的比较
一、IP地址
IP地址用于表示网络上的各个不同主机的节点,就像家庭住址一样,邮递员通过家庭住址以决定将该信件投往何处。IP地址是一个32位的二进制数。
IP地址包含两部分:网络号和主机号,又称为前缀地址和后缀地址。
IP协议将IP地址划分为5中基本类型:A、B、C、D、E
网络类别 | 最大网络数 | 第一个网络号 | 最后一个网络号 | 最高位二进制数 |
A类 | 126 | 1 | 126 | 0000 0000 |
B类 | 16383 | 128.1 | 191.255 | 1000 0000 |
C类 | 2097151 | 192.0.1 | 233.255.255 | 1100 0000 |
网络中要传输的数据包括TCP、UDP、ICMP以及IGMP数据都是以IP数据报格式传输的,他们都是先封装为IP数据报,再封装为以太网帧,最后通过数据线路传输。
二、客户端/服务器模式
1、 二者通信的模式如下:
服务器程序特点:
l 一般启动后就一直处于运行状态,以等待客户机进程的请求;
l 使用的端口往往是熟知端口,便于客户机进程连接请求;
l 一般拥有较多的系统资源,以便及时响应各个客户机进程的请求;
l 可以并行处理多个客户机进程的请求,但数目是有一定的限制;
l 在通信时一般处于被动的一方,不需要知道客户机的IP地址和端口信息。
客户机程序特点
l 在需要服务器进程的服务时将向服务器进程请求服务,并建立通信连接,得到满足并完成处理后就终止通信连接;
l 使用向系统申请的临时端口与服务器进程进行通信,通信完成后将释放该端口;
l 拥有相对较少的系统资源;
l 在通信时属于主动的一方,需要事先知道服务器的IP地址和端口信息。
2、 客户端/服务器编程
l 服务器的并发
服务器的并发可以通过三种方式实现:多线程、消息驱动、循环处理。
(1)、多线程
处理方式是当有一个客户机连接请求到来时,服务器创建一个新线程与这个客户机进行交互,通过为每一个连接请求创建一个线程,服务器可以同时给多个客户机提供服务,同时处理多个请求。
(2)、消息驱动
消息驱动之需要一个线程,在消息驱动模式下,当请求到来时,系统发出指定的消息,消息出发服务器进行处理。需要重写消息回调函数。
(3)、循环处理
服务器通过主动轮询查看是否有客户机到达的请求,在循环处理过程中,程序一般不能在I/O请求上阻塞,应采用异步I/O调用。当没有数据传输时功能调用及时返回,以便同时进行其他处理,实际执行的就是对客户端的排队处理。
说明:循环处理是指一个时刻只能处理一个请求的一种服务器的实现,并发服务器是指一个时刻可以处理多个请求的一种服务器。
l 服务器的设计算法
面向连接的多线程:当有一个新的客户请求市创建一个新线程。
无连接的多线程:每收到一个来自客户端的数据报后就创建一个从线程,在这个线程里。处理完成请求后就退出。
三、套接字socke
套接字的概念最初是由BSD Unix操作系统所实现的,是网络通信的基本构架以及一种网络编程接口,还可以称作:插座、插口。可以形象地将套接字理解为应用程序与网络协议之间的插口,也就是编程接口。是应用层与传输层之间的接口。
从实现的角度来讲,非常复杂。套接字是一个复杂的软件机构,包含了一定的数据结构,包含许多选项,由操作系统内核管理。
从使用的角度来讲,非常简单。对于套接字的操作形成了一种网络应用程序的编程接口(API)。套接字是网络通信的基石。
套接字是对网络中不同主机上应用进程之间进行双向通信的端点的抽象,一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议栈交换数据的机制。
套接字的概念与文件句柄类似,一个套接字就是一个通信标识,由一个短整数表示,实际上就是一个句柄,代表网络协议中的一组数据,该数据包含了通信双方的IP地址和当前连接的端口信息。在网络中要全局地标识一个参与通信的进程,需要采用三元组:协议、主机IP地址、端口号。要描述两个应用进程之间的端到端的通信关联则需要一个五元组:协议、信源机IP地址、信源应用进程端口、信宿机IP地址、信宿应用进程端口。
套接字连接的理论步骤:
1) 在服务器端声明一个用于监听客户端的套接字对象,同时对某个端口设定为监听状态;
2) 在客户端声明套接字对象,并通过IP和端口向服务器请求连接;
3) 服务器端监听到请求指令后,新建一个套接字对象,用于与客户端绑定,传送和接收数据,服务器用于监听的套接字继续监听下一个客户端的请求。
从套接字所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议栈进行通信的接口,是应用程序与网络协议栈进行交互的接口。
根据传输协议的不同,套接字的类型有三类:
l 流式套接字(SOCK_STREAM)
用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复发送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议——TCP。这类套接字中,传输数据之前必须在两个应用进程之间建立一条通信连接,这就确保了参与通信的两个应用进程都是活动并且响应的。当连接建立之后,应用进程只要通过套接字向TCP层发送数据流,而另一个应用进程便可以接收到相应的数据流,它们不需要知道传输层是如何对数据流进行处理。特别需要注意的是通信连接必须显式建立。该套接字类型适合传输大量的数据,但不支持广播和多播方式。
l 数据报式套接字(SOCK_DGRAM)
提供了一种无连接的服务,通信双方不需要建立任何显式连接,数据可以发送到指定的套接字,并且可以从指定的套接字接收数据。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP进行数据的传输。由于数据包套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理。与数据报套接字相比,使用流式套接字是一个更为可靠的方法,但对于某些应用,建立一个显式连接所导致的系统开销是令人难以接收的,并且数据报套接字支持广播和多播方式。
l 原始套接字
与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP的数据,数据报套接字只能读取UDP的数据。使用原始套接字的主要目的是为了避开TCP/IP处理机制,被传送的数据包可以被直接传送给需要它的应用程序。因此,其主要是在编写自定义底层协议的应用程序时使用,例如各种不同的TCP/IP实用程序(如ping和arp)都使用原始套接字实现,也可以用来实现数据包捕捉分析等。
从应用编程角度来看,套接字就是TCP/IP网络编程接口的集合,它是应用程序与TCP/IP协议族通信的中间软件抽象层,其中包含了许多函数或例程,程序员可以用它们来开发网络应用程序。
网络字节顺序:
不同体系的CPU在内存中的数据存储往往存在差异。如Intel的x86系列处理器将低序字节存储在起始地址,而一些RISC架构的处理器,如IBM的370主机使用的PowerPC或Motorola公司生产的CPU,都将高序字节存储在起始位置。这两种不同的存储方式被称为低位优先(little-endian)和高位优先(big-endian)。
对于网络上的字节表示法有一个标准——网络字节顺序,它与高位优先相同。以便于不同体系结构的计算机间的通信。Intel 采用的字节顺序称为"小头方式",即低字节在前,高字节在后 的方式,而标准的网络顺序是"大头方式",即高字节在前,低字节在后的方式。
一般情况下,用户不需要处理在网络上发送和接收数据的字节顺序的转换,但是在下列情况下,需要用户手动转换字节顺序。
用户传输的信息需要网络解释,这与发送到其他机器的数据不一样。如用户传输端口和地址时,必须由网络理解。当与之通信的服务器应用程序不是MFC应用程序时,如果通信的两台机器使用的字节顺序不同,则需要调用字节转换。
而下列情况下,不需要用户手动调用字节转换。两台机器使用相同的字节顺序,并且两端约定不进行字节交换。与之通信的服务器是MFC应用程序。用户有与之通信的服务器的源代码,因此,可以显式地说明是否转换字节顺序。可以将服务器转换成MFC程序。
在后面会介绍到MFC的 CAsyncSocket类,如果使用此类,用户必须自己管理需要的字节顺序转换。
Windows Socket 标准化"大头方式"字节顺序模型,并提供与"小头方式"字节顺序的转换函数。而 CSocket 使用的 CArchive 类使用"小头方式"字节 顺序,但是 CArchive 类处理了字节顺序转换的细节。通过在应用程序中使用标准的字节顺序,或使用 WindowsSockets 字节顺序转换函数,用户可以编写灵活的代码。
如果使用MFC Sockets 编程,即客户端和服务器端都使用MFC,则不需要关心字节顺序的细节。如果编写与非MFC应用程序进行通信的应用程序,如FTP服务器,则用户在将数据传入存档对象前,需要自己管理字节顺序转换。Windows Sockets 提供了 4 个转换函数,ntohs()、ntohl()、htons()和 htonl()后面将会介绍到。
四、Windows套接字
Microsoft将Unix套接字中的大部分函数移植到Windows操作系统,形成了Windows套接字。Windows套接字针对Windows操作系统的消息驱动机制,对原有的Unix套接字进行了扩展,定义了一部分新的函数。是 Windows 平台下定义的可以兼容二进制数据传输的网络编程接口,是基于伯克利加利福尼亚大学的 BSD UNIX Sockets 的实现,当前的版本是 2.0。此规范包括 BSD 格式的 Sockets 函数和 Windows 扩展函数。使用 Windows Sockets 的应用程序可以与任何兼容 Windows Sockets API 的网络程序进行数据通信。
Windows的网络通信建立在TCP/IP协议的基础上,TCP/IP协议族包含一系列构成互联网基础结构的网络协议。
Windows 套接字是开放的网络编程接口,完成网络环境中的数据传输功能。
Windows Sockets规范
目前,市面上很多网络软件支持 Windows Sockets,包括传输控制协议/Internet 协议(TCP/IP)、 Xerox 网络系统(XNS)、DECNet 协议、Novell 公司的 Internet 包交换和顺序包交换协议(IPX/SPX) 等。虽然现在的 Windows Sockets 规范定义了提取 TCP/IP 的 Sockets,但是,任何网络协议可以通过提供自己实现的 Windows Sockets 的 DLL 版本支持 Windows Sockets。终端仿真器和电子邮件系统都是使用 Windows Sockets 典型实例。因为 Windows Sockets 是抽象于底层网络的,因此,开发人员不 需要了解有关网络的知识,就可以编写运行在任何支持 Sockets 的网络上的应用程序。
五、Windows套接字编程机理
使用 Windows Socket 编程时,需要了解几种编程方式,理解这几种编程方式的机理,从而能够根 据实际情况编写适合系统需求的程序。主要包括以下几个方面:阻塞操作、非阻塞操作、异步方式、数据收发。
Windows Socket 中最简单的方式就是阻塞操作,这也是 Windows 套接字的默认方式。在此种方式下,所有的 I/O 操作都会阻塞,直到操作完全执行完毕。因此,任何线程在同一时间只能执行一个读写操作。如果线程正在执行接收操作,而又没有数据到达,则线程会阻塞直到有数据到达。虽然此种方式操作最简单,但是并不是最有效的方式。
与阻塞操作相反,非阻塞读写操作在执行操作后立即返回,并返回错误代码为WSAEWOULDBLOCK 表示操作还没有完全执行完。在此种机制下,需要处理当操作执行完成后的代码,在Windows Socket 中使用网络事件通知的方式实现。用户可以使用 WSPSelect()函数注册感兴趣的事件,则当接收到相应的网络事件,系统会为程序发送事件通知,程序可以再根据自己的需要进行数据处理。
重叠读写操作,就是同时执行多个读写操作。在 Windows Socket中使用带有WSA_FLAG_OVERLAPPED选项的WSPSocket()函数创建支持重叠读写操作的套接字。客户端使用WSPRecv()函数或 WSPRecvFrom()函数提供接收数据的缓冲区。如果同时提供一个或多个缓冲区,则数据被放置到其中任何一个用户缓冲区 中。数据发送端则使用 WSPSend()函数或 WSPSendTo()函数 提供发送数据的缓冲区。重叠读写操作都会立即返回,返回 0 表示读写操作立即完 成,并且使用事件对象 或回调函数通知程序是否已经成功发送或接收,返回值WSA_IO_PENDING 表示读写操作成功,但是还没有执行完毕。
Windows Socket 中使用 WSPSend()函数和 WSPSendTo()函数完成套接字数据发送功能。使用 WSPRecv()函数和 WSPRecvFrom()函数完成数据接收功能。并且这些函数可以实现自动增加数据包包头和自动减去数据包包头的功能,简化数据解析的过程。
六、WinSockAPI
WinSock套接字是一个基于套接字模型的API,提供了许多套接字函数,他们并不代表协议的某一层次,其实质就是一组编程接口。用户可以利用这些函数进行编写网络程序。
套接字编程相关数据结构(套接字寻址):
结构体 | 说明 |
sockaddr | 用于保存套接字的地址信息 |
ockaddr_in | 与sockaddr类似 |
1) sockaddr结构体
struct sockaddr{unsigned short sa_family; char sa_data[14]};
参数说明:sa_family :用于指定地址族,如果是TCP/IP通信,该值取PF_INET和AF_NET;
sa_data :用于保存套接字的IP地址和端口号信息。
2) sockaddr_in数据结构
struct sockaddr_in {short int sin_family; unsigned short int sin_port;
struct in_addr sin_addr;unsigned char sin_zero[8];};
参数说明:sin_family:用于指定地址族,必须是AF_INET。
sin_port:套接字通信的端口号;
sin_addr:通信的4字节IP地址,也是一个结构体。
sin_zero[8]:用以填充0,保持与structsockaddr同样大小。
由于sockaddr数据结构与sockaddr_in数据结构的大小是相同的,指向sockaddr_in的指针可以通过强制转换,转换成指向sockaddr结构的指针。以下举例说明定义一个数据结构以及怎样定制一个数据结构的方法:
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(整型的端口号);
addr.sin_addr.S_un.S_addr = INADDR_ANY;//代指本机,一般推荐
最后一行可以使用另一种方法:
addr.sin_addr.S_un.S_addr=inet_addr("点分十进制字符串");
3)IPv4套接字地址结构以“sockaddr_in”命名。
以下是关于WinSock API常用的函数:
函数名 | 解释 | 适用场合 |
socket | 用于创建一个套接字 | TCP、UDP |
bind | 实现套接字与主机本地IP地址和端口号的绑定 | TCP、UDP |
listen | 用于将创建的套接字设置为监听模式 | TCP |
accept | 用于接受客户端使用connect函数发出的连接请求,返回一个新的套接字 | TCP |
connect | 用于发出一个连接请求 | TCP |
recv | 用于从连接的套接字中取数据 | TCP |
send | 用于向已建立连接的套接字发送数据 | TCP |
closesocket | 关闭套接字连接 | TCP、UDP |
shutdown | 关闭套接字读写通道,停止套接字接收/传送的功能。 | |
recvfrom | 在无连接的套接字上接收数据。 | UDP |
sendto | 在无连接套接字上发送数据。 | UDP |
1) socket函数用于创建套接字并指定套接字的服务类型。
SOCKET socket(int af, int type, intprotocol);
参数说明:af :表示一个地址家族,IPv4是AF_INET,IPv6是AF_INET6。
type :标识套接字类型,流式套接字还是数据报套接字或者原始套接字。
Protocol :表示一个特殊的协议,通常为0,表示采用TCP/IP协议。
返回值:建立成功返回新建套接字描述,否则返回SOCKET_ERROR错误,可以使用函数WSAGetLastError获取相应的错误代码。
af的其他常量:
type的可选值:
2) bind函数用于绑定本地IP地址和端口号到新建立的套接字。
int bind ( SOCKET s, const struct sockaddr FAR *name, int namelen );
参数说明:s :是一个套接字。
name :是一个sockaddr_in结构的指针,包含了要绑定的地址和端口号。
namelen :确定name缓冲区的长度。
返回值:执行成功返回0,否则返回SOCKET_ERROR。
示例代码:int port = 9090;
SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.S_un.S_addr = inet_addr(IPString);
bind(s, (sockaddr*)&addr, sizeof(addr));
3) listen函数(不会阻塞进程)
int listen ( SOCKET s, int backlog );
参数说明:s :套接字
Backlog :等待连接的最大队列长度,比如:设置为3,有4个客户请求时,最后一个会得到错误信息。SOMAXCONN表示最大的连接数。
返回值:成功返回0,失败返回SOCKET_ERROR。
4) accept函数(会阻塞进程)
SOCKET accept ( SCOKET s, struct sockaddr FAR *addr, int FAR *addrlen );
参数说明:s :是一个处于监听状态的套接字。
addr :是一个sockaddr_in的指针,包含一组客户的端口号、IP地址等信息。
addrlen :用于接收参数addr的长度。可用sizeof()获取。
返回值:成功返回新建套接字的描述句柄,否则返回INVALID_SOCKET。
5) connect函数(会阻塞进程)
int connect ( SOCKET s, const struct sockaddr FAR *name, int namelen );
参数说明:s :标识一个套接字。
name :连接主机的IP地址和端口号。
namelen :name缓冲区的长度。
返回值:成功返回0,否则返回SOCKET_ERROR。
6) recv函数
int recv ( SOCKET s, char FAR *buf,int len, int flags );
参数说明:s :连接好的套接字。
buf :接收数据的缓冲区。
len :buf的长度。
flags :函数的调用方式。一般情况为0,MSG_PEEK 查看数据,输入队列不删除,MSG_OOB 处理带外数据。
返回值:成功返回接收到的字节数,否则返回SOCKET_ERROR。
7) send函数
int send ( SOCKET s, const char FAR *buf, int len,int flags );
参数说明:s :已建立连接的套接字。
buf :发送数据的缓冲区。
len :buf缓冲区的长度。为0是不发送任何数据。
flag :表示函数的调用方式。通常为0,MSG_DONTROUTE不为数据选择路由MSG_OOB发送带外数据警队TCP有效。
返回值:成功返回发送的字节数,失败返回SOCKET_ERROR。
有关这个函数的两种形式加以说明,两种形式指的是使用字符数组和字符串的形式:
字符数组:charbuf[1024];
send(m_link, buf, strlen(buf), 0);
字符串 :CString str;
send(m_link, str.GetBuffer(0),str.GetLength(), 0);
8) closesocket函数
int closesocket (SOCKET s );
参数说明:s :要关闭的套接字,如果s设置为SO_DONTLINGER,等所有数据发完后才关闭套接字连接。
返回值:成功返回0,失败返回SOCKET_ERROR。
9) shutdown函数禁止在套接字上发送和接收数据,但是不会关闭套接字。
一般在关闭套接字之前调用。
int shutdown ( SOCKET s, int how );
参数说明:s :要关闭的套接字连接。
how :标志位。SD_RECEIVE 禁止调用接收函数
SD_SEND 禁止调用发送数据函数
SD_BOTH 接收和发送函数均禁止。
返回值:成功返回0,失败返回SOCKET_ERROR。
10) recvfrom函数
int recvfrom ( SOCKET s,char FAR *buf,int len, int flags,struct socketaddr FAR *from, int FAR *fromlen );
参数说明:s :已建立连接的套接字。
buf :接收数据缓冲区。
len :缓冲区长度。
flags :接收方式指针。
from:可选指针,指向装有源地址的缓冲区。
fromlen :可选指针,指向from缓冲区的长度。
返回值:成功返回接收到的字节数,否则返回SOCKET_ERROR。
说明:其中的最后两个参数是用来存放对方的SOCKET信息的数据结构。这个数据结构可以用在sendto函数的后两个参数中。
11) sendto函数
intsendto ( SOCKET s, const char FAR *buf, int len,int flags, const structsockaddr FAR *to, int tolen );
参数说明:s :建立连接成功的套接字
buf :发送数据的缓冲区。
len :缓冲区buf的长达。
flags :函数调用方式。
to :可选指针,指向目的套接字的地址。
tolen :to所指地址的长度。
返回值:成功返回发送的字节数,失败返回SOCKET_ERROR。
关于WinSock API的扩展函数:
htonl函数 :将4字节主机字节顺序的数转换为网络字节顺序。
格式 :u_long htonl (u_long hostlong);
htons函数 :将2字节主机字节顺序的数据转换为网络字节顺序。
格式 :u_short htons (u_short hostshort);
ntohl函数 :4字节网络字节顺序的数转换为主机字节顺序。
格式 :u_long ntohl (u_long netlong);
ntohs函数 :将2字节网络字节顺序的数据转换为主机字节顺序。
格式 :u_short ntohs(u_short netshort);
inet_addr函数 :将点分十进制字符串表示的IP地址转换为网络字节顺序的IP地址。
格式 :unsigned long inet_addr (const char* cp);
inet_ntoa函数 :将网络字节顺序表示的IP地址转换为点分十进制数表示的IP地址。
格式:char* FAR inet_ntoa(struct in_addrin);
WinSock2的扩展函数
Windows 扩展函数 | 功 能 |
WSAAccept() | accept()函数的扩展版本,允许条件接收和 Socket 分组 |
WSAAsyncGetHostByAddr() | 根据地址异步获取主机,基于消息实现 |
WSAAsyncGetHostByName() | 根据名称异步获取主机,基于消息实现 |
WSAAsyncGetProtoByName() | 根据名称异步获取协议信息,基于消息实现 |
WSAAsyncGetProtoByNumber() | 根据协议号异步获取协议信息,基于消息实现 |
WSAAsyncGetServByName() | 根据服务器名称和端口号,异步获取服务器信息,其是基于消息实现的 |
WSAAsyncGetServByPort() | 根据端口号和协议,异步获取服务器信息,其是基于消息实现的 |
WSAAsyncSelect() | 实现异步版本的 select()函数 |
WSACancelAsyncRequest() | 取消异步获取系列的函数,即取消WSAAsyncGetXByY()函数 |
WSACleanup() | 退出底层的 Windows Socket DLL的引用 |
WSACloseEvent() | 销毁事件对象 |
WSAConnect() | Connect()函数的扩展版本,允许交换连接数据和QOS标准 |
WSACreateEvent() | 创建事件对象 |
WSADuplicateSocket() | 复制 Socket |
WSAEnumNetworkEvents() | 枚举网络事件 |
WSAEnumProtocols() | 枚举当前系统中每个有效的协议信 |
WSAEventSelect() | 连接网络事件和事件对象 |
WSAGetLastError() | 获取最近的 Windows Socket 错误信息 |
WSAGetOverlappedResult() | 返回重叠操作的完成状态 |
WSAGetQOSByName() | 根据服务名获取 QOS 参数 |
WSAHtonl() | Htonl()函数的扩展版本,将32位整数从主机字节顺序转换成网络字节顺序 |
WSAHtons() | Htons()函数的扩展版本,将16位整数从主机字节顺序转换成网络字节顺序 |
WSAIoctl() | ioctl 函数的重叠执行版本 |
WSAJoinLeaf() | 增加一个结点到会话中 |
WSANtohl() | ntohl()函数的扩展版本,将32位整数从网络字节顺序转换成主机字节顺序 |
WSANtohs() | ntohs()函数的扩展版本,将16位整数从网络字节顺序转换成主机字节顺序 |
WSAProviderConfigChange() | 接收安装服务或卸载服务的通知消息 |
WSARecv() | Recv()函数的扩展版本 |
WSARecvFrom() | recvfrom()函数的扩展版本 |
WSAResetEvent() | 重置事件对象 |
WSASend() | send()函数的扩展版本 |
WSASendTo() | sendto()函数的扩展版本 |
WSASetEvent() | 设置事件对象 |
WSASetLastError() | 设置最近的错误信息 |
WSASocket() | socket()函数的扩展版本。使用WSAPROTOCOL_INFO结构作为输入参数并创建重叠socket |
WSAStartup() | 初始化 Windows Sockets DLL |
WSAWaitForMultipleEvents() | 在多个事件对象上阻塞 |
上面这些扩展函数是对Windows Socket 规范提供的Socket 函数的封装,支持消息和函数处理。如在WSAAsyncGetServByName()函数中,可以指定接收消息的对话框句柄和消息,当异步函数执行完毕后,会发送消息给对话框,用户可以在对话框中捕获相应的消息进行处理。这与Windows的消息编程模式是一致的。因此,Windows Socket 扩展函数的封装方便了Socket 程序的开发。用户可以尽量使用扩展函数开发 Socket 程序。
部分函数说明:
WSAStartup函数:初始化ws2_32.lib动态库,确定windows Socket使用的版本。
格式 :int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
返回值:成功返回0
下面的代码用于确定其使用版本:
WSADATA wsd;
If(WSAStartup(MAKEWORD(2,0), &wsd) != 0)//定义的版本号并初始化
{
Return;
}
WSACleanup函数 :终止使用WinSock,释放为应用程序分配的相关资源。当时用完Windows Socket时,应该调用本函数释放分配给应用程序或动态库的资源。
格式 :int WSACleanup (void);
WSAAsyncSelect函数 :用于将网络中发生的事件关联到窗口的某个消息中。
格式 :int WSAAsyncSelect(SOCKET s, HWND hWnd, unsigned int wMsg, longlEvent);
示例代码:绑定到窗口之后只需将消息函数PreTranslateMessage即可。
绑定函数:
WSAAsyncSelect(m_client,m_hWnd, RECEIVEMEAASGE, FD_READ);
client :SOCKET连接
m_hWnd :窗口句柄,不可用GetSafeHwnd来获取。
RECEIVEMEAASGE :消息编号,是一个符号常量。
FD_READ :可处理的事件
窗口消息:
BOOLCTCPApiProConCliDlg::PreTranslateMessage(MSG* pMsg)
{
//TODO: Add your specialized code here and/or call the base class
if(pMsg->message== RECEIVEMEAASGE)
{
HandleReceive();//自定义消息处理函数
returnTRUE;
}
else
{
returnCDialog::PreTranslateMessage(pMsg);
}
}
获取主机信息函数
Windows Socket 规范中定义了一组专门用于处理域名、通信服务和通信协议等网络信息的数据库函数。使用这些函数可以获取网络能力的检测,使用getXbyY的函数形式,含义是通过Y值获取X值。主 要包括如下9个函数。
函数名 | 描述 |
gethostbyaddr | 返回对应于地址的主机信息 |
gethostbyname | 返回对应于给定主机名的包含主机名字和地址信息的hostent结构指针。 |
gethostname | 返回本地计算机的标准主机名 |
getpeername | 返回和套接字连接的对方地址 |
getsockname | 返回和套接字连接的本机地址名 |
getprotobyname | 返回与协议名相对的协议信息 |
getprotobynumbe | 返回与协议号相对应的协议信息 |
getservbyname | 返回对应于给定服务名和协议名的相关服务信息。 |
getservbyport | 返回对应于给定端口号和协议名的相关服务信息。 |
getsockopt | 取得套接字选项 |
setsockopt | 设置套接字选项 |
(1)gethostbyaddr()函数:根据网络地址获取主机信息。其函数原型为:
struct HOSTENT FAR *gethostbyaddr (
const char FAR * addr, //网络字节顺序的地址指针
int len, //addr参数的长度
int type, //指定地址的类型
);
返回值 :函数返回一个HOSTEN 结构的指针,其中包含传入的网络地址对应的名称和地址,所有数据都是以NULL结束。如果返回值为 NULL,表示函数调用失败。
(2)gethostbyname()函数:从主机数据库中根据主机名称获取主机信息。其函数原型为:
struct hostent FAR * gethostbyname (
const char FAR * name );// 以 NULL 结束的主机名称
返回值:函数返回一个指向hostent结构的指针,结构中的name参数中包含查询到的结果值。如果返回值为 NULL表示函数调用失败。
补充:struct hostent
struct hostent
{
char *h_name;
char **h_aliases;
int h_addrtype;
int h_length;
char **h_addr_list;
#define h_addrh_addr_list[0]
};
参数说明:
h_name:表示的是主机的规范名。例如www.google.com的规范名其实是www.l.google.com。
h_aliases:表示的是主机的别名www.google.com就是google他自己的别名。有的时候,有的主机可能有好几个别名,这些,其实都是为了易于用户记忆而为自己的网站多取的名字。
h_addrtype:表示的是主机ip地址的类型,到底是ipv4,还是pv6
h_length:表示的是主机ip地址的长度
h_addr_lisst:表示的是主机的ip地址,注意,这个是以网络字节序存储的。千万不要直接用printf带%s参数来打这个东西,会有问题的。所以到真正需要打印出这个IP的话,需要调用inet_ntop()。
(3)gethostname()函数:返回本地主机的机器名称。其函数原型为:
int gethostname(
char FAR * name,
int namelen
);
参数说明:name :存放返回的主机名称的缓冲区的指针
namelen :存放缓冲区的长度 此函数返回本地主机的机器名称到 name 参数指定的缓冲区中,返回的主机名是以 NULL 结束的字符串。返回的主机名的形式,可能是简单的主机名也可能是完整的域称。
返回值:如果函数调用成功,则返回0,否则返回SOCKET_ERROR和相应的错误代码,使用 WSAGet LastError()函数可以获取错误代码的值。
示例:(2)、(3)的应用
charhostname[256]; //存放的是主机名
struct hostent *host;
if(gethostname(hostname, 256) == 0)
{
host = gethostbyname(hostname);
for(int i = 0;host != NULL&& host->h_addr_list[i] != NULL; i ++)
{
LPCTSTR hostip =inet_ntoa(*(struct in_addr*)host->h_addr_list[i]);//主机IP
m_edit_ip = hostip;//直接是一个字符串类型
}
UpdateData(FALSE);
}
(6)getprotobyname()函数:根据协议名称获取协议信息。其函数原型为:
struct PROTOENT FAR * getprotobyname (
const char FAR * name );// 以 NULL 结束的协议名的指针
返回值:函数返回name参数指定的包含协议名和协议号的 PROTOENT 结构指针。如果返回值为NULL,表示函数调用失败。
(7)getprotobynumber()函数:根据协议号获取协议信息。其函数原型为:
struct PROTOENT FAR * getprotobynumber (
int number );//要查询的协议的主机字节顺序的协议号
返回值:函数返回包含协议名和协议号的PROTOENT结构指针。如果返回值为 NULL,表示函数调用失败。
(8)getservbyname()函数:根据服务器名和协议获取服务器信息。其函数原型为:
struct servent FAR * getservbyname (
const char FAR * name, // 指向服务器 名称的以 NULL 结束的字符串的指针
const char FAR * proto); //指向协议名 的以 NULL 结束的字符串的指针
返回值:函数返回包含服务名称和服务号的SERVENT结构的指针。如果返回值为 NULL,表示函数调用失败。
(9)getservbyport()函数:根据端口号和协议获取服务信息。其函数原型为:
struct servent FAR * getservbyport (
int port, // 指定网络字节顺序的服务的端口
const char FAR* proto); //协议名指针
返回值:以函数返回包含服务名称和服务号的SERVENT结构的指针。如果返回值为 NULL,表示函数调用失败。
上面这些函数是用于获取有关网络方面的通信、协议、域名等方面的信息的数据库函数。用户可以使用这些函数查询到socket程序所使用的网络资源信息。在socket程序中需要使用这些函数配合socket()函数完成socket通信功能。
函数总结:Socket接口包括三类函数:
第一类是WinSock API包含的Berkeley socket函数。这类函数分两部分。第一部分是用于网络I/O的函数,如
accept、closesocket、connect、recv、recvfrom、select、send、sendto
另一部分是不涉及网络I/O、在本地端完成的函数,如
bind、getpeername、getsockname、getsocketopt、htonl、htons、inet_addr、inet_nton、ioctlsocket、listen、ntohl、ntohs、setsocketopt、shutdow、socket等
第二类是检索有关域名、通信服务和协议等Internet信息的数据库函数,如
gethostbyaddr、gethostbyname、gethostname、getprotolbyname、getprotolbynumber、getserverbyname、getservbyport。
第三类是Berkekley socket例程的Windows专用的扩展函数,如gethostbyname对应的WSAAsynGetHostByName(其他数据库函数除了gethostname都有异步版本),select对应的WSAAsynSelect,判断是否阻塞的函数WSAIsBlocking,得到上一次Windsock API错误信息的WSAGetLastError,等等。
使用winSock API编程的准备工作如下(TCP/UDP都需要):
1) 在头文件StdAfx.h中引入头文件并导入网络库文件。
方式一:#include<stdio.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
方式二:#include<stdio.h>
#include <winsock.h>
#pragma comment(lib, "wsock32.lib")
说明:可以使用链接库函数:工程->设置->Link->输入ws2_32.lib,OK!替换最后一句。
2) 在CXXXApp的初始化函数中进行初始化套接字操作:
WSADATA wsd;
if (WSAStartup(MAKEWORD(2, 2),&wsd) != 0)
{
WSACleanup();
exit(1);
}
基于TCP的编程流程:
基于UDP的编程流程如下:
七、MFC中的套接字
随着计算机网络化的深入,计算机网络编程在程序设计的过程中变得日益重要。由于C++语言对底层操作的优越性,许多文章都曾经介绍过用VC++进行Socket编程的方法。但由于都是直接利用动态连接库wsock32.dll进行操作,实现比较繁琐。其实,VC++的MFC类库中提供了CAsyncSocket这样一个套接字类,用他来实现Socket编程,是非常方便的。
在MFC中累的继承关系如下所示:CObject–> CAsyncSocket –> CSocket
MFC 提供两个类支持使用 Windows Sockets API 进行网络编程。类 CAsyncSocket 一对一地封装 了 Windows Sockets API。类 CSocket 提供了从 CArchive 对象中序列化数据的 Sockets 功能。
下面对两个累的介绍分别叙述如下:
1、 CAsyncSocket类的相关知识
为了简化套接字编程,MFC使用CAsyncSocket类较为底层封装了Windows Sockets API。使用此类可以直接使用 Sockets API 编写灵活的程序,同时又可以方便地处理网络事件。除了使用C++将Sockets包装成面向对象的形式外,类还将与Sockets相关的Windows消息转换成事件通知。CAsyncSocket 类的部分函数与Windows Socket API的函数是一对一的,但是简化了事件通知的开发过程。
原本WinSock API是同步模式,如果网络出了问题或者其它原因他会一直阻塞,原因就是太过简单的同步模式,它是让像send()这样的调用一直处于线程挂起状态,英特网上的通信可不像在PC里CPU跟外设的通信一样简单,比如我们打开一个网页,不可能一下就打开,阻塞是常有的事,很多时候我们都在等,等待你的PC连到外面集线器的那根线上什么时候有信号过来,事实是这样,需要等待,但是我们不能让线程傻傻地等待,采用多线程的方法,让另外一个线程等待,设定超时计时器等等方法,让原来的线程可以干别的事,比如写一行字‘正在连接...’,这就是异步方式,CAsyncSocket就是个比较好的异步方式封装。
CAsyncSock类是一个异步非阻塞Socket封装类,该类封装了一些基本的WinSockAPI函数,提供了与较底层的Windows套接字的对话接口,其对象既可以工作在“阻塞模式”也可以工作在“非阻塞模式”,一般主要用其功能的是在非阻塞模式下工作。以下是CAsyncSocket的模式切换方式:
DWORD dw;
dw=0; //切换为同步模式
IOCtl(FIONBIO,&dw);
dw=1; //切换回异步模式
IOCtl(FIONBIO,&dw);
CAsyncSocket::Create()有一个参数指明了你想要处理哪些Socket事件,你关心的事件被指定以后,这个Socket默认就被用作了异步方式。那么CAsyncSocket内部到底是如何将事件交给你的呢?
CAsyncSocket的Create()函数,除了创建了一个SOCKET以外,还创建了个CSocketWnd窗口对象,并使用 WSAAsyncSelect()将这个SOCKET与该窗口对象关联,以让该窗口对象处理来自Socket的事件(消息),然而CSocketWnd收到Socket事件之后,只是简单地回调CAsyncSocket::OnReceive(),CAsyncSocket::OnSend(), CAsyncSocket::OnAccept(),CAsyncSocket::OnConnect()等虚函数。所以CAsyncSocket的派生类,只需要在这些虚函数里添加发送和接收的代码。以下是这个消息的实现机制:
switch (WSAGETSELECTEVENT(lParam))
{
case FD_READ:
pSocket->OnReceive(nErrorCode);
break;
case FD_WRITE:
pSocket->OnSend(nErrorCode);
break;
case FD_OOB:
pSocket->OnOutOfBandData(nErrorCode);
break;
case FD_ACCEPT:
pSocket->OnAccept(nErrorCode);
break;
case FD_CONNECT:
pSocket->OnConnect(nErrorCode);
break;
case FD_CLOSE:
pSocket->OnClose(nErrorCode);
break;
}
}
CAsyncSocket异步机制如下:
当你获得了一个异步连接后,实际上你扫除了发送动作与接收动作之间的依赖性。所以你随时可以发包,也随时可能收到包。发送、接收函数都是异步非阻塞的,顷刻就能返回,所以收发交错进行着,你可以一直工作,保持很高的效率。但是,正因为发送、接收函数都是异步非阻塞的,所以仅调用它们并不能保障发送或接收的完成。例如发送函数Send,调用它可能有4种结果:
1、错误,Send()==SOCKET_ERROR,GetLastError()!=WSAEWOULDBLOCK,这种情况可能由各种网络问题导,你需要马上决定是放弃本次操作,还是启用某种对策
2、忙,Send()==SOCKET_ERROR,GetLastError()==WSAEWOULDBLOCK,导致这种情况的原因是,你的发送缓冲区已被填满或对方的接受缓冲区已被填满。这种情况你实际上不用马上理睬。因为CAsyncSocket会记得你的Send WSAEWOULDBLOCK了,待发送的数据会写入CAsyncSocket内部的发送缓冲区,并会在不忙的时候自动调用OnSend,发送内部缓冲区里的数据。
3、部分完成,0<Send(pBuf,nLen)<nLen,导致这种情况的原因是,你的发送缓冲区或对方的接收缓冲区中剩余的空位不足以容纳你这次需要发送的全部数据。处理这种情况的通常做法是继续发送尚未发送的数据直到全部完成或WSAEWOULDBLOCK。这种情况很容易让人产生疑惑,既然缓冲区空位不足,那么本次发送就已经填满了缓冲区,干嘛还要继续发送呢,就像WSAEWOULDBLOCK了一样直接交给OnSend去处理剩
余数据的发送不是更合理吗?然而很遗憾,CAsyncSocket不会记得你只完成了部分发送任务从而在合适的时候触发OnSend,因为你并没有WSAEWOULDBLOCK。你可能认为既然已经填满缓冲区,继续发送必然会WSAEWOULDBLOCK,其实不然,假如WSAEWOULDBLOCK是由于对方读取接收缓冲区不及时引起的,继续发送确很可能会WSAEWOULDBLOCK,但假如WSAEWOULDBLOCK是由于发送缓冲区被填满,就不一定了,因为你的网卡处理发送缓冲区中数据的速度不见得比你往发送缓冲区拷贝数据的速度更慢,这要取决与你竞争CPU、内存、带宽资源的其他应用程序的具体情况。假如这时候CPU负载较大而网卡负载较低,则虽然刚刚发送缓冲区是满的,你继续发送也不会WSAEWOULDBLOCK。
4、完成,Send(pBuf,nLen)==nLen
与OnSend协助Send完成工作一样,OnRecieve、OnConnect、OnAccept也会分别协助Recieve、Connect、Accept完成工作。这一切都通过消息机制完成。 使用CAsyncSocket时,Send流程和Recieve流程是不同的,不理解这一点就不可能顺利使用CAsyncSocket。MSDN对CAsyncSocket的解释很容易让你理解为:只有OnSend被触发时你Send才有意义,你才应该Send,同样只有OnRecieve被触发时你才应该Recieve。很不幸,你错了:你会发现,连接建立的同时,OnSend就第一次被触发了,嗯,这很好,但你现在还不想Send,你让OnSend
返回,干点其他的事情,等待下一次OnSend试试看?实际上,你再也等不到OnSend被触发了。因为,除了第一次以外,OnSend的任何一次触发,都源于你调用了Send,但碰到了WSAEWOULDBLOCK!所以,使用CAsyncSocket时,针对发送的流程逻辑应该是:你需两个成员变量,一个发送任务表,一个记录发送进度。你可以在任何你需要的时候,主动调用Send来发送数据,同时更新任务表和发送进度。而OnSend,则是你的负责擦屁股工作的助手,它被触发时要干的事情就是根据任务表和发送进度调用Send继续发。若又没能将任务表全部发送完成,更新发送进度,退出,等待下一次OnSend;若任务表已全部发送完毕,则清空任务表及发送进度。
使用CAsyncSocket的接收流程逻辑是不同的:你永远不需要主动调用Recieve,你只应该在OnRecieve中等待。由于你不可能知道将要抵达的数据类型及次序,所以你需要定义一个已收数据表作为成员变量来存储已收到但尚未处理的数据。每次OnRecieve被触发,你只需要被动调用一次Recieve来接受固定长度的数据,并添加到你的已收数据表后。然后你需要扫描已收数据表,若其中已包含一条或数条完整的可解析的业务数据包,截取出来,调用业务处理窗口的处理函数来处理或作为消息参数发送给业务处理窗口。而已收数据表中剩下的数据,将等待下次OnRecieve中被再次组合、扫描并处理。
l m_hSocket
该数据变量包含由CAsyncSocket类封装的SOCKET类型的套接字句柄。有一个有用的用处就是判断套接字对象是佛偶Create过,如下代码:
if(m_handle.m_hSocket != INVALID_SOCKET)
{
m_handle.Close();
}
意思是当套接字创建之后就不可再创建了,套接字创建了就不是INVALID_SOCKET,
在长连接应用中,连接可能因为各种原因中断,所以你需要自动重连。你需要根据CAsyncSocket的成员变量m_hSocket来判断当前连接状态:
if(m_hSocket==INVALID_SOCKET)。当然,很奇怪的是,即使连接已经中断,OnClose也已经被触发,你还是需要在OnClose中主动调用Close,否则m_hSocket并不会被自动赋值为INVALID_SOCKET。
l 相关函数简介
CAsyncSocket类定义了一组非常有用的成员函数,这些函数的功能和格式与WinSockAPI函数类似。
(1) CAsyncSocket
这是CAsyncSocket的构造函数,构造出一个空的套接字对象,但没有指定端口和IP,构造完后还必须调用CAsyncSocket类的Create函数创建SOCKET句柄。函数原型:
CAsyncSocket()
(2)Create
调用它的Create成员函数,来创建底层的套接字数据结构,并绑定它的地址,决定套接字对象的具体特性。函数原型:
BOOL Create(
UINT nSocketPor,//套接字绑定端口
Int nSocketType, //用于选择套接字类性
Long Ievent, //注册程序感兴趣的网络事件
LPCTSTR lpszSocketAddress = NULL, //指定与套接字绑定的主机地址。
);
返回值:成功返回非0,失败返回0。
说明:实现代码
BOOLCAsyncSocket::Create(UINT nSocketPort, int nSocketType,long lEvent, LPCTSTRlpszSocketAddress)
{
if (Socket(nSocketType, lEvent))
{
if (Bind(nSocketPort,lpszSocketAddress))
return TRUE;
int nResult = GetLastError();
Close();
WSASetLastError(nResult);
}
return FALSE;
}
可见在一般的TCP/IP协议中是不需要再重载bind函数的。
(3)Accept
用于连接请求,只能用于基于面向连接的类型的套接字。函数原型如下:
virtual BOOL Accept(
CAsyncSocket& rConnectedSocket,//返回可用的新套接字
SOCKADDR* lpSockAddr = NULL,//返回发送连接请求的套接字地址
int* lpSockAddrLen = NULL// lpSockAddr的实际长度。
);
返回值:成功返回非0,失败返回0。
说明:服务方如何Accept(),以建立连接的问题。简单的做法就是在监听的Socket收到OnAccept()时,用一个新的CAsyncSocket对象去建立连接,例如:
void CMySocket::OnAccept( intErrCode )
{
CMySocket* pSocket =new CMySocket;
Accept( *pSocket );
}
于是,上面的pSocket和客户方建立了连接,以后的通信就是这个pSocket对象去和客户方进行,而监听的Socket仍然继续在监听,一旦又有一个客户方要连接服务方,则上面的OnAccept()又会被调用一次。当然pSocket是和客户方通信的服务方,它不会触发OnAccept()事件,因为它不是监听Socket。
(4)AsyncSelect
为套接字请求事件通知,调用后将套接字置于非阻塞状态。函数原型:
BOOL AsyncSelect(long lEvent);//注册程序感兴趣网络事件
返回值:成功返回非0,失败返回0。
(5)bind
该成员函数为套接字绑定一个本地地址,该函数有两种形式:
BOOL Bind(
UINT nSocketPort,//绑定的端口号
LPCTSTRT lpszSocketAddress = NULL//制定与套接字绑定的主机地址)
BOOL Bind(
const SOCKADDR* lpSockAddr, //SOCKADDR*类型的指针
int nSockAddrLen//lpSockAddr的字节长度)
返回值:成功返回非0,失败返回0。
注意:MFC程序里创建支持IPX/SPX协议的套接字的时候才需要重载此函数。
(6)Connect
在服务器端套接字对象已经进入监听状态之后,客户应用程序可以调用CAsyncSocket类的Connect()成员函数,向服务器发出一个连接请求,请求连接到服务器端套接字对象。函数原型:
BOOL Connect(
LPCTSTR lpszHostAddress, //要求连接的网络地址,可以使域名也可以是字符串
UINT nHostPort );//要求连接的端口号
BOOL Connect(
const SOCKADDR* lpSockAddr,//包含要求链接的套接字地址
int nSockAddrLen );// lpSockAddr的长度
返回值:成功返回非0,失败返回0。
说明:如果调用成功或者发生了WSAEWOULDBLOCK错误,当调用结束返回时,都会发生FD_CONNECT事件,MFC框架会自动调用客户端套接字的OnConnect()事件处理函数,并将错误代码作为参数传送给它。客户方在使用CAsyncSocket::Connect()时,往往返回一个WSAEWOULDBLOCK的错误,实际上这不应该算作一个错误,它是Socket提醒我们,由于你使用了非阻塞Socket方式,所以(连接)操作需要时间,不能瞬间建立。我们可以在Connect()调用之后等待 CAsyncSocket::OnConnect()事件被触发,一般不要对connect函数的返回做判断,过一会儿它连接完成后,自然就能跳到FD_CONNECT事件那里了。
对于返回值错误的说法:异步方式connect是不会立刻成功的,WSAEWOULDBLOCK(10035)告诉你当前这个操作还没有成功,你还需等待。
所以一般真要捕捉返回值,那么就使用如下代码:
if(!m_Socket.Connect(strServerIP,TCP_PORT))
{ nErr = m_Socket.GetLastError();
if(nErr!=WSAEWOULDBLOCK)
{
DisConnect();
MessageBox(“连接服务器错误”);
}
}
(7)GetLastError
该函数用于返回这个线程中最后调用套接字函数的错误代码。函数原型:
Static int GetLastError()
说明:可以使用返回的值进行判断发生何种错误。
(8)Listen
监听套接字对象开始监听来自客户端的连接请求,函数原型如下:
BOOL Listen( int nConnectionBacklog =5);//连接请求数
返回值:成功返回非0,失败返回0。
说明: 当Listen函数确认并接纳了一个来自客户端的连接请求后,会触发FD_ACCEPT事件,监听套接字会收到通知,表示监听套接子已经接纳了一个客户的连接请求,MFC框架会自动调用监听套接字的OnAccept事件处理函数,编程者一般应重载此函数,在其中调用监听套接字对象的Accept函数,来接收客户端的连接请求。
执行流程是:首先监听(listen) 再触发FD_ACCEPT事件,直接去由OnAccept函数来处理,在其中又由Accept函数处理。
(9)Send
在已建立连接的套接字上发送数据,函数原型:
virtual int Send(
const void* lpBuf, //发送数据缓冲区
int nBufLen, //缓冲区长度
int nFlags = 0);//发送方式标志,一般为0。
返回值:成功返回实际发送字节数,失败返回SOCKET_ERROR。
说明:对于一个CAsyncSocket套接字对象,当它的发送缓冲区腾空时,会激发FD_WRITE事件,套接字会得到通知,MFC框架会自动调用这个套接字对象的OnSend事件处理函数。一般编程者会重载这个函数,在其中调用Send成员函数来发送数据。
(10)Receive
从已连接的套接字上接收数据,函数原型:
Virtual int Receive(
Void* lpBuf,
Int nBufLen,
Int nFlags = 0);
返回值:成功返回接收到的字节数,失败返回SOCKET_ERROR,如果套接字关闭则返回0。
说明:对于一个CAsyncSocket套接字对象,当有数据到达它的接收队列时,会激发FD_READ事件,套接字会得到已经有数据到达的通知,MFC框架会自动调用这个套接字对象的OnReceive事件处理函数。一般编程者会重载这个函数,在其中调用Receive成员函数来接收数据。在应用程序将数据取走之前,套接字接收的数据将一直保留在套接字的缓冲区中。
说明:send和receive发送的可以使结构体类型,也可以是CSstring或者Char类型。
一下就以各种类型的数据进行说明:
CString类型:
CString str = _T("你好!");
m_link.Send(str.GetBuffer(0), str.GetLength(), 0);
Charl类型:
char str[] = "你好!";
m_link.Send(str, strlen(str), 0);
结构体类型:
typedef struct mess
{
char str[100];
int flag;
BOOL all;
}MESS;
MESS info;
info.all = FALSE;
info.flag = 1;
info.str = "你好!";
m_link.Send(&info, sizeof(MESS), 0);
说明:在结构体类型中不可有CString类型的数据,因为CString相当于一个指针,也不可有指针类型的数据。只发了地址,但是地址所指的内容并没有发送。
(11)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 );
返回值:成功返回实际发送字节数,失败返回SOCKET_ERROR。
(12)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 //指明数据的接收方式
);
返回值:成功返回接收到的字节数,失败返回SOCKET_ERROR,如果套接字关闭则返回0。
(13)Shutdown
禁止接收或者发送数据,可以选择关闭套接字的方式。将套接字置为不能发送数据,或不能接收数据,或二者均不能的状态。
函数原型:
BOOL ShutDown( int nHow);
nHow的可选值:receives = 0、sends = 1、both = 2
返回值:成功返回非0,失败返回0。
(14)close
关闭套接字,释放资源,函数原型:
virtual void Close( );
(15)SetSocketOpt
设置底层套接字对象的属性,要获取套接字的设置信息,可调用GetSocketOpt()。
(16)IOCtl
控制套接字的工作模式,选择合适的参数,可以将套接字设置在阻塞模式(Blocking mode)下工作。
(17)GetPeerName
用于获得套接字连接的IP地址。函数原型:
BOOL GetPeerName(CString &rPeerAddress, UINT & rPeerPort)
l CAsyncSocket类可以接受并处理的消息事件
六种套接字相关的事件与通知消息,参数Ievent可以选用的六个符号常量是在winsock.h文件中定义的。
#define FD_READ 0x01 通知有数据可读。
#define FD_WRITE 0x02 通知可以写数据。
#define FD_OOB 0x04 通知将有带外数据到达。
#define FD_ACCEPT 0x08 通知监听套接字有连接请求可以接受。
#define FD_CONNECT 0x10 通知请求连接的套接字,连接的要求已被处理。
#define FD_CLOSE 0x20 通知套接字已关闭。
他们代表MFC套接字对象可以接受并处理的六种网络事件,当事件发生时,套接字对象会收到相应的通知消息,并自动执行套接字对象响应的事件处理函数。
l CAsyncSocket 类的通知事件
当上述的网络事件发生时,MFC框架作何处理呢?按照Windows的消息驱动机制,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事件
参数说明:
nErrorCode 套接字上最近的错误代码。此成员函数可用的错误代码有:
· 0 函数成功地执行并返回。
· WSAENETDOWN Windows Sockets检测到了网络故障。
当某个网络事件发生时,MFC框架会自动调用套接字对象的对应的事件处理函数。这就相当给了套接字对象一个通知,告诉它某个重要的事件已经发生。所以也称之为套接字类的通知函数(notification functions)或回调函数(callback functions)。如果你从CAsyncSocket类派生了自己的套接字类,你必须重载你的应用程序所感兴趣的那些网络事件所对应的通知函数。MFC框架自动调用通知函数,使得你可以在套接字被通知的时候来优化套接字的行为。
在编程的时候,这些事件将来都在自己派生CAsyncSocket的类中去重写其虚函数。关于其重写的方式有很多种方法,但是有一种最简便的方法就是把所有的操作都写在窗口类的实现文件中,这样在派生类中来调用相应的响应函数。其实这些事件对应的响应函数中时调用CAsyncSocket的相应的函数,比如:OnReceive(int nErrorCode) 调用Receive等。记得这些函数需要操作的必须在派生类中去重写,如果你想使用非阻塞方式的话。
调用方式一 :
使用主窗口指针法,假如现在有一个对话框 CXXXDlg、有一个派生CAsyncSocket的类CMySocket。要实现对OnReceive(int nErrorCode)虚函数的重写,方式如下:
首先:在CMySocket的头文件中加入以下两行代码:
Class CXXXDlg;
CXXXDlg *m_pDlg;
在CMySocket的实现文件中引入CXXXDlg的头文件。
第二步:在CXXXDlg中的头文件中引入CMySocket的头文件,并且定义一个CMySocket的普通变量m_mySock,并在其初始化中将本窗口指针传给CMySocket的m_pDlg:
m_mySock.m_pDlg = this;
第三步:在CXXXDlg中添加接收函数,其形式为:
void CXXXDlg::receiveMessage(CMySocket *mySock)
{
//用mySock来接收消息
}
第四步:在CMySocket中添加虚函数OnReceive(int nErrorCode),并执行以下操作
If(nErrorCCode== 0)
{
m_pDlg-> receiveMessage(this);
}
当有消息到达的时候会自动调用CMySocket::OnReceive函数,CMySocket::OnReceive又去调用CXXXDlg::receiveMessage,在其中又去执行CAsyncSocket::Receive来接收消息。
说明:获得主窗口指针的另一种方法:
CDlgTcpServerApp *pApp =(CDlgTcpServerApp *)AfxGetApp();
CDlgTcpServerDlg *pMainWnd =(CDlgTcpServerDlg *)pApp->m_pMainWnd;
pMainWnd为主窗口指针。
调用方式二:
采用自定义消息的方式,将套接字线程收到的事件当做消息发给主窗口,在主窗口中处理事件,主要方法如下:
第一步:在派生套接字类中添加构造函数以及HWND 句柄
头文件中:
HWND m_mainWnd;
实现文件中:
Void CListenSocket::CAsyncSocket(HWND hWnd)
{
m_mainHwnd = hWnd;
}
第二步:在主窗口中添加CListenSocket的头文件以及定义一个CListenSocket变量
引入头文件:
#include “ListenSocket.h”
定义一个变量:
CListenSocket m_listen(m_hWnd);
第三步:在CListenSocket中的OnXXX(nErrorCode)函数中进行发送消息,PostMessage。
第四步:在CXXXDlg中定义消息即可,并写消息处理函数。
l CAsyncSocket的编程流程:
序号 | 服务器(Server) | 客户机(Client) |
1 | //构造一个套接字 CAsyncSocket sockSrvr; | //构造一个套接字 CAsyncSocket sockClient; |
2 | //创建SOCKET句柄,绑定到指定的端口 sockSrvr.Create(nPort); | //创建SOCKET句柄,使用默认参数 sockClient.Create(); |
3 | //启动监听,时刻准备接受连接请求 sockSrvr.Listen(); | |
4 | //请求连接到服务器 sockClient.Connect(strAddr.nport); | |
5 | //构造一个新的空的套接字来接受连接 CAsyncSocket sockRecv; sockSrvr.Accept(sockRecv); | |
6 | //接收数据 sockRecv.Receive(pBuf, nLen); | //发送数据 sockClient.Send(pBuf, nLen); |
7 | //发送数据 sockRecv.Send(pBuf, nLen); | //接收数据 sockClient.Receive(pBuf, nLen); |
8 | //关闭套接字对象 sockRecv.Close(); | //关闭套接字对象 sockClient.Close(); |
说明:
1、创建CasyncSocket类对象
CAsyncSocket类对象称为异步套接字对象。创建异步套接字对象一般分为两个步骤,首先通过调用CAsyncSocket类的构造函数,创建一个新的空CAsyncSocket类套接字对象,构造函数不带参数。然后必须调用它的Create成员函数,来创建底层的套接字数据结构,并绑定它的地址,决定套接字对象的具体特性。
可以使指针对象也可以是普通对象。
2、 执行步骤:
因为CAsyncSocket类较好的封装了WinSocket API,因此使用CAsyncSocket类的步骤与直接使用WinSocket API的步骤类似,步骤如下:
1) 构造 CAsyncSocket 对象并使用对象创建底层的 SOCKET 句柄。代码如下:
CAsyncSocket* pSocket = new CAsyncSocket; // 创建CAsyncSocket对象
pSocket->Create(6650, SOCK_DGRAM ); // 创建无连接套接字
2) 如是客户方程序,用CAsyncSocket∷Connect()成员函数连接到服务方;如是服务方程序,用CAsyncSocket∷Listen()成员函数开始监听,一旦收到连接请求,则调用CAsyncSocket∷Accept()成员函数开始接收。注意:CAsyncSocket∷Accept()成员函数要用一个新的并且是空的CAsyncSocket对象作为它的参数,这里所说的"空的"指的是这个新对象还没有调用Create()成员函数。
3) 调用其他的CAsyncSocket类的Receive()、ReceiveFrom()、Send()和SendTo()等成员函数进行数据通信。
4) 销毁 CAsyncSocket 对象。如果套接字对象在栈上,则析构函数会在套接字变量超出作用范围后,自动执行;如果使用new操作符创建套接字,则需要使用delete操作符销毁对象。析构函数调用对象的Close()函数关闭套接字连接。
在创建 CAsyncSocket 对象时,对象封装了 Windows 的 SOCKET 句柄并提供了在此句柄上的操作。 当用户使用 CAsyncSocket 对象时,必须要处理阻塞操作、接收和发送机制使用的字节顺序以及 Unicode 字符集和多字节字符集的转换等问题。
图示:
流程图:基于TCP
基于UDP
l MFC中编程准备工作
在进行MFC 编程的时候,应该和WinSockAPI一样,有一些初始化工作,在建立工程的过程中,如果选择了Windows Sockets选项则会自动加入头文件和初始化代码,否则必须手动添加如下代码:
1) 在StdAfx.h头文件中加入以下代码
#include<afxsock.h>
2) 在CTCPMFCApp::InitInstance()中加入套接字初始化代码
if (!AfxSocketInit())
{
AfxMessageBox(IDP_SOCKETS_INIT_FAILED);
return FALSE;
}
在你使用CAsyncSocket之前,必须调用AfxSocketInit初始化WinSock环境,而AfxSocketInit会创建一个隐藏的CSocketWnd对象,由于这个对象由Cwnd派生,因此它能够接收Windows消息。所以它能够成为高层CAsyncSocket对象与WinSock底层之间的桥梁。例如某CAsyncSocket在Send时WSAEWOULDBLOCK了,它就会发送一条消息给CSocketWnd作为报告,CSocketWnd会维护一个报告登记表,当它收到底层WinSock发出的空闲消息时,就会检索报告登记表,然后直接调用报告者的OnSend函数。所以前文所说的CAsyncSocket会自动调用OnXxx,实际上是不对的,真正的调用者是CSocketWnd——它是一个CWnd对象,运行在独立的线程中。
八、MFC中的套接字2
l CSocket类
在MFC中的另一个套接字是CSocket,CSocket是CAsyncSocket类的派生类,除了继承了父类中一些常用、易懂的Windows Sockets API函数外,还对CAsyncSocket底层中较难控制的一些函数进行了处理,提供了对比CAsyncSocket类抽象化级别更高套接字的支持。CSocket是用MFC序列化协议的一种版本,通过MFC中的CArchive对象将数据传递给套接字对象,或者从套接字对象接收数据,使得套接字上的数据输入输出就如同使用MFC的文档一样简捷易用,即CArchive管理着原来必须由用户自己使用原始API或CAsyncSocket类来管理通信的许多工作,进而方便了用户的使用。
通过MFC的CArchive对象提供Sockets的存档功能,使用过程比CAsyncSocket模型要简单得多。CSocket类从CAsyncSocket类继承了很多封装了Windows Sockets API的成员函数。因此,使用CSocket类一般不需要深入了解Socket编程。更方便的是,CSocket提供了CArchive的同步操作,实现了Socket通信的文档序列化。可以使用MFC序列化协议发送数据和接收数据。网络传输层会将数据分割成大小合适的数据包,CSocket类可以处理包 装和解包工作。
CSocket实现了阻塞操作,其对象在调用Connect、Send、Accept、Close、Receive等成员函数时,这些函数在完成任务之后才会返回。因此,Connect和Send不会导致OnConnect和OnSend事件响应函数的调用。如果重载了OnReceive、OnAccept、OnClose事件响应函数,则在网络事件到达之后将导致对应的事件响应函数被调用。在这些事件响应函数中应该调用Receive、Accept、Close来完成相应操作。
CSocket在CAsyncSocket的基础上,修改了Send、Recieve等成员函数,帮你内置了一个用以轮询收发缓冲区的循环,变成了同步短连接模式。短连接应用简单明了,CSocket经常不用派生就可以直接使用,但也有些问题:
1、用作监听的时候
曾经看到有人自己创建线程,在线程中创建CSocket对象进行Listen、Accept,若Accept成功则再起一个线程继续Listen、Accept可以说他完全不理解CSocket,实际上CSocket的监听机制已经内置了多线程机制,你只需要从CSocket派生,然后重载OnAccept:
//CListenSocket头文件
class CListenSocket : public CSocket
{
public:
CListenSocket(HWNDhWnd=NULL);
HWND m_hWnd; //事件处理窗口
virtual void OnAccept(intnErrorCode);
};
//CListenSocket实现文件
#include "ListenSocket.h"
CListenSocket::CListenSocket(HWND hWnd){m_hWnd=hWnd;}
void CListenSocket::OnAccept(int nErrorCode)
{
SendMessage(m_hWnd,WM_SOCKET_MSG,SOCKET_CLNT_ACCEPT,0);
CSocket::OnAccept(nErrorCode);
}
//主线程
m_pListenSocket=new ListenSocket(m_hWnd);
m_pListenSocket->Create(...);
m_pListenSocket->Listen();
LRESULT CXxxDlg::OnSocketMsg(WPARAM wParam, LPARAM lParam)
{
UINT type=(UINT)wParam;
switch(type)
{
case SOCKET_CLNT_ACCEPT:
{
CSocket*pSocket=new CSocket;
if(!m_pListenSocket->Accept(*pSocket))
{
deletepSocket;
break;
}
}
}
}
2、用于多线程的时候
常看到人说CSocket在子线程中不能用,其实不然。实际情况是:直接使用CSocket动态创建的对象,将其指针作为参数传递给子线程,则子线程中进行收发等各种操作都
没问题。但如果是使用CSocket派生类创建的对象,就要看你重载了哪些方法,假如你仅重载了OnClose,则子线程中你也可以正常收发,但不能Close!因为CSocket是用内部循环做到同步的,并不依赖各OnXxx,它不需要与CSocketWnd交互。但当你派生并重载OnXxx后,它为了提供消息机制就必须与CSocketWnd交互。当你调用AfxSocketInit时,你的主线程会获得一个访问CSocketWnd的句柄,对CSocketWnd的访问是MFC自动帮你完成的,是被隐藏的。而你自己创建的子线程并不自动具备访问CSocketWnd的机制,所以子线程中需要访问CSocketWnd的操作都会失败。常看到的解决办法是给子线程传递SOCKET句柄而不是CSocket对象指针,然后在子线程中创建CSocket临时对象并Attach传入的句柄,用完后再Dettach并delete临时对象。但是。一般使用发送消息的方法:使用自定义消息,不能在子线程中close,那么,可以给主线程发送一条消息,让主线程的消息处理函数来完成Close,也很方便。
CSocket一般配合多线程使用,只要你想收发数据,你就可以创建一个CSocket对象,并创建一个子线程来进行收发。所以被阻塞的只是子线程,而主线程总是可以随时创建子线程去帮它干活。由于可能同时有很多个CSocket对象在工作,所以你一般还要创建一个列表来储存这些CSocket对象的标识,这样你可能通过在列表中检索标识来区分各个CSocket对象,当然,由于内存地址的唯一性,对象指针本身就可以作为标识。相对CAsyncSocket而言,CSocket的运作流程更直观也更简单。
l CArchive
CArchive允许以一个永久二进制(通常为磁盘存储)的形式保存一个对象的复杂网络,它可以在对象被删除时,还能永久保存。可以从永久存储中装载对象,在内存中重新构造它们。使得数据永久保留的过程就叫作“串行化”。
CArchive不支持在数据报的Socket连接上序列化数据
l CSocketFile
在#include <afxsock.h>中,一个CSocketFile对象是一个用来通过Windows Sockets 在网络中发送和接收数据的CFile 对象。为了这个目的,你可以CSocketFile对象与一个CSocket 对象连接。你也可以将CSocketFile 对象与一个CArchive 对象连接,以使用MFC 系列来简化发送和接收数据。
l CSocket有关的函数
首先,CSocket比CAsyncSocket类的成员函数少得多,以下分别简介:
1) CSocket
是CSocket类的构造函数,它构造一个空的套接字对像,但是并没有指定端口和IP,构造完成够还必须调用Create函数创建SOCKET句柄。
2) Create
该函数在构造函数之后使用将SOCKET句柄附到套接字上,并绑定套接字指定的地址。如果套接字是SOCK_DGRAM,不能和CArchive对象关联使用。函数原型:
BOOL Create (
UINT nSocketPort , //端口号
int nSocketType = SOCK_STREAM, //套接字类型
LPCTSTR lpszSocketAddress, //制定与套接字绑定的IP地址
);
返回值:成功返回非0,失败返回0。
3) Attach
功能是将对象和资源句柄联系起来,可以将一个SOCKET句柄附加到CSocket对象上。这个套接字句柄存放在对象数据成员m_hSocket中。函数原型:
BOOL CSocket(SOCKET hSocket);
返回值:成功返回非0,失败返回0。
4) CancleBlockingCall
终止正在进行的阻塞调用,但是终止出Accept以外的函数均会使套接字处于不确定状态。函数原型:
void CancelBlockingCall()
5) OnMessagePending
可以再Windows派发消息时,处理自己感兴趣的消息。函数原型:
virtual BOOL OnMessagePending()
返回值:如果消息被处理返回非0,否则返回0。
6) IsBlocking()函数:此函数可以确定当前是否在执行一个阻塞调用。
7) FromHandle()函数:返回一个指向 CSocket 对象的指针,其中存放了SOCKET句柄。使用SOCKET句柄,可以使用WinSocket API执行其他套接字函数。
8) 关于连接的三个函数
CAsyncSocket::Accept
CAsyncSocket::Connect
CAsyncSocket::Listen
9) 接收发送
使用CAsyncSocket的Receive和Send
10) 关联CSocketFile和CSocket
explicitCSocketFile( CSocket* pSocket, BOOL bArchiveCompatible = TRUE );
关联CArchive和CSocketFile
CArchive(
CFile* pFile,
UINT nMode,
int nBufSize =4096,
void* lpBuf =NULL );
使用CArchive对象的Read、Write等函数在客户机与服务器进程之间进行数据传递。
l CSocket事件
同CAsyncSocket一样使用的是响应六个FD_XXX事件。
l 使用CSocket编程思想
TCP通信图示:
CArchive不能用于数据报套接字
通信的流程图:
编程模型:
使用CSocket对象,需要创建CSocket对象,并将几个MFC类对象关联起来。下面的程序,服务器Sockets和客户端Sockets除了第(3)步其余每步都必须执行,其中每种Sockets 类型需要不同的操作。当运行时,通常服务器应用程序先启动准备好并"监听",然后客户端应用程序发起连接。如果客户端试图连接时,服务器没有准备好,则需要客户端程序稍后再试。使用CSocket在服务器端Socket和客户端Socket之间进行通信的流程图如上图所示。
(1)构造CSocket对象。
(2)使用CSocket::Create函数创建底层的SOCKET句柄。对于客户端CSocket对象,使用默认参数调用Create()方法就可以。对于服务器Csocket()对象,必须在Create()方法中指定端口。但是CArchive不能与数据报套接字一起使用。如果要以数据报套接字的方式使用CSocket,则不能使用档案文件。因为数据报是不可靠的,不能保证数据到达、数据不重复和顺序,因此,不能通过档案文件与序列化兼容。如果使用带有CArchive对象的CSocket 类操作数据报套接字时,MFC会抛出错误。
(3)如果socket是服务器,调用CAsyncSocket::Listen开始"监听"客户端的连接。如果socket
是客户端,调用CAsyncSocket::Connect连接socket对象到服务器socket。Connect()函数会首先检查地址是否是IP地址的形式, 如果不是,则会将地址作为机器地址处理。当服务器端收到连接请求,如果接收,则调用CAsyncSocket::Accept()函数。Accept 成员函数需要一个新socket的引用,即空CSocket对象。用户在调用Accept()函数前,需要构造对象。如果 socket对象超出范围,则连接会被关闭。MFC会连接新对象到SOCKET句柄。
(4)创建CSocketFile对象,使其与CSocket对象相连。
(5)创建用于接收或发送数据的CArchive对象,使其与CSocketFile对象相连。
(6)使用CArchive对象在客户端和服务器sockets之间传输数据。要注意,CArchive 对象只能单方向存取数据。因此,有些情况下,需要使用两个CArchive对象,一个用于发送数据,另外一个用于接收。在接收连接和建立档案后,用户可以通过验证密码等操作,保证数据传输的安全性。CArchive 类为 CSocket 类提供 IsBufferEmpty 成员函数确保接收所有数据。如果缓冲区中包含多条数据消息,则需要循环处理,读取全部数据消息,并且清空缓冲区。否则,有数据可以接收的下条通知会不确定的延期。
(7)销毁档案对象和socket对象。关闭套接字和清除相关的对象:在使用完CSocket对象以后,应用程序应调用它的Close()成员函数来释放套接字占用的系统资源,也可以调用它的ShutDown()成员函数来禁止套接字读写。而对于相应的CArchive对象、CSocketFile对象和CSocket对象,可以将它们销毁;也可以不作处理,因为当应用程序终止时,会自动调用这些对象的析构函数,从而释放这些对象占用的资源。
说明:
CSocket类使用基类CAsyncSocket的同名成员函数Connect()、Listen()、Accept()来建立服务器和客户机套接字之间的连接,使用方法相同。不同的是:CSocket类的Connect()和Accept()支持阻塞调用。比如:在调用Connect()函数时会发生阻塞,直到成功地建立了连接或有错误发生才返回,CSocket对象从不调用OnConnect()事件处理函数。
发送和接收数据,在创建CSocket类对象后,对于数据报套接字,直接使用CSocket类的SendTo()、ReceiveFrom()成员函数来发送和接收数据。对于流式套接字,一般将CSocket类与CArchive类和CSocketFile类结合,来发送和接收数据,这将使编程更为简单。CSocket对象从不调用OnSend()事件处理函数。
九、CAsyncSocket与CSocket的比较
前者是异步通信,后者是同步通信;前者是非阻塞模式,后者是阻塞模式。另外,异步非阻塞模式有时也被称为长连接,同步阻塞模式则被称为短连接。
为了更明白地讲清楚两者的区别,举个例子:设想你是一位体育老师,需要测验100位同学的400米成绩。你当然不会让100位同学一起起跑,因为当同学们返回终点时,你根本来不及掐表记录各位同学的成绩。如果你每次让一位同学起跑并等待他回到终点你记下成绩后再让下一位起跑,直到所有同学都跑完,这就是同步阻塞模式。你设计了一个函数,传入参数是学生号和起跑时间,返回值是到达终点的时间。你调用该函数100次,就能完成这次测验任务。这个函数是同步的,因为只要你调用它,就能得到结果;这个函数也是阻塞的,
因为你一旦调用它,就必须等待,直到它给你结果,不能去干其他事情。如果你一边每隔10秒让一位同学起跑,直到所有同学出发完毕;另一边每有一个同学回到终点就记录成
绩,直到所有同学都跑完,这就是异步非阻塞模式。你设计了两个函数,其中一个函数记录起跑时间和学生号,该函数你会主动调用100次;另一个函数记录到达时间和学生号,该函数是一个事件驱动的callback函数,当有同学到达终点时,你会被动调用。你主动调用的函数是异步的,因为你调用它,它并不会告诉你结果;这个函数也是非阻塞的,因为你一
旦调用它,它就马上返回,你不用等待就可以再次调用它。但仅仅将这个函数调用100次,你并没有完成你的测验任务,你还需要被动等待调用另一个函数100次。
当然,你马上就会意识到,同步阻塞模式的效率明显低于异步非阻塞模式。那么,谁还会使用同步阻塞模式呢?不错,异步模式效率高,但更麻烦,你一边要记录起跑同学的数据,一边要记录到达同学的数据,而且同学们回到终点的次序与起跑的次序并不相同,所以你还要不停地在你的成绩册上查找学生号。忙乱之中你往往会张冠李戴。你可能会想出更聪明的办法:你带了很多块秒表,让同学们分组互相测验,这是多线程同步模式。每个拿秒表的同学都可以独立调用你的同步函数,这样既不容易出错,效率也大大提高,只要秒表足够多,同步的效率也能达到甚至超过异步。
既然多线程同步既快又好,异步模式还有存在的必要吗?很遗憾,异步模式依然非常重要,因为在很多情况下,你拿不出很多秒表。你需要通信的对端系统可能只允许你建立一个SOCKET连接,很多金融、电信行业的大型业务系统都如此要求。
一般CAsyncSocket用于在少量连接时,处理大批量无步骤依赖性的业务。CSocket
用于处理步骤依赖性业务,或在可多连接时配合多线程使用。
十、总结
l 编程步骤
无论使用MFC中的那种类型的套接字进行编程,在编程的时候都是从MFC 套接字类中继承自己的类,然后在自己继承的类中去重写自己感兴趣的事件即可。一般是按以下方式进行编程的:
服务器端:
1、 继承MFC 套接字的监听类CListenSocket,获得主窗口指针,编写OnAccept()函数,进行监听。
2、 继承MFC 套接字的客户端通信类CListenSocke,获得主窗口指针,编写OnReceive函数,进行接收消息。
客户端:
1、 继承MFC 套接字的客户端通信类CListenSocke,获得主窗口指针,编写OnReceive函数,进行接收消息。
l 获取主机IP地址
charhostname[256];
structhostent *host;
LPCTSTRhostip;
if(gethostname(hostname,sizeof(hostname)) == 0)
{
host= gethostbyname(hostname);
for(inti = 0; host != NULL && host->h_addr_list[i] != NULL; i ++)
{
hostip= inet_ntoa(*(struct in_addr*)host->h_addr_list[i]);
}
}
说明:hostname中放的是主机名称。
Hostip中放的是主机IP地址,而且可以直接付给CString类型的变量。
l 使用CPtrList存放CSocket指针,这也是存放指针的一个很好的链表。
1、 定义
CPtrList m_userList;
2、 增加节点
m_userList.AddTail(pSocket);//pSoxket是一个指针变量。
3、 搜索符合条件的节点
POSITIONpos;
CClientSocket*pSock;
for(pos =m_userList.GetHeadPosition(); pos != NULL; )
{
pSock= (CClientSocket *)m_userList.GetNext(pos);
if(pSock->GetUserName()== name)
{
returnFALSE;
}
}
4、 删除节点
m_userList.RemoveAt(pos);//pos代表指定位置的节点