Windows
Sockets是Microsoft
Windows的网络程序设计接口,它是从Berkeley
Sockets扩展而来的。Windows
Sockets在继承了Berkeley
Sockets主要特征的基础上,又对它进行了重要扩充。这些扩充主要是提供了一些异步函数,并增加了符合Windows消息驱动特性的网络事件异步选择机制。这些扩充有利于应用程序开发者编制符合Windows编程模式的软件,它使在Windows下开发高性能的网络程序成为可能。
Socket是BSD UNIX提供的网络应用编程接口,它采用客户/服务器的通讯机制,使网络客户方和服务器方通过Socket实现网络之间的连接和数据交换。Socket提供了一系列的系统调用,使用这些系统调用可以实现TCP, UDP,ICMP和IP等多种网络协议之间的通讯。
Socket有三种主要类型:stream sockets, datagram
sockets 和raw sockets。 Stream socket接口定义了一种可靠的面向连接的服务,它实现了无差错无重复的顺序数据传输。它通过内置的流量控制解决了数据的拥塞,应用程序可以发送任意长度的数据,将数据当作字节流。Datagram socket接口定义了一种无连接的服务,数据通过相互独立的包进行传输,包的传输是无序的,并且不保证是否出错、丢失和重复。包长度是有限的(隐含长度为8,192Bytes,最大长度可设为32,768Bytes)。Raw socket接口允许对低层协议如IP和ICMP的直接存取,它主要用于新的网络协议实现的测试等。
下面我们通过一个面向连接的传输发生的典型情况来说明socket网络通信的实现。
服 务
器 客 户
图6.3 面向连接的协议实现的Socket调用
从图6.3可以看出,客户和服务器的关系不是对称的,服务器首先启动,然后在某一时间启动客户与服务器建立连接。服务器和客户开始都必须用调用socket()建立一个套接字(socket),然后服务器调用bind()将套接字与一个本地网络地址捆扎在一起,再用调用listen()使套接字处于一种被动的准备接收状态,同时规定它的请求队列长度,之后服务器就可以调用accept()来接收连接了。客户在建立套接字之后,便可以通过调用connect()和服务器建立连接。连接建立后,客户和服务器之间就可以通过连接发送和接收数据(调用read()和write())。最后,待数据传送结束,双方调用close()关闭套接字。
BSD
Socket支持阻塞(blocking)和非阻塞(non_blocking)两种工作方式。在阻塞方式下工作,connect()、accept()、read()和recv()等调用在执行时都处于阻塞状态直到它成功或出错返回。在非阻塞方式下工作,这些调用是立即返回的,但是它们是否完成得靠查询才能知道。对于Windows这种非抢先多任务操作系统来说,这两种工作方式都是很难以接受的,为此,WinSock在尽量与BSD Socket保持一致外,又对它作了必要的扩充。
WinSock对BSD Socket的扩充主要是在基于消息、对网络事件的异步存取接口上。表6.2列出了WinSock扩充的函数功能。
表6.2 WinSock扩充函数功能表
|
函 数
名
|
功 能
|
|
WSAAsyncGetHostByAddr()
|
标准Berkeley函数getXbyY的异步版本,例如:函数WSAAsyncGetHostByName()就是提供了标准Berkeley函数gethostbyname的一种基于消息的异步实现。
|
|
WSAAsyncGetHostByName()
|
|
WSAAsyncGetProtoByName()
|
|
WSAAsyncGetProtoByNumber()
|
|
WSAAsyncGetServByName()
|
|
WSAAsyncGetServByPort()
|
|
WSAAsyncSelect()
|
函数select()的异步版本
|
|
WSACancelAsyncRequest()
|
取消函数WSAAsyncGetXByY执行中的实例
|
|
WSACancelBlockingCall()
|
取消一个执行中的“阻塞”API调用
|
|
WSACleanup()
|
终止使用隐含的Windows Sockets DLL
|
|
WSAGetLastError()
|
获取Windows Sockets API的最近错误号
|
|
WSAIsBlocking()
|
检测隐含的Windows Sockets DLL是否阻塞了一个当前线索的调用
|
|
WSASetBlockingHook()
|
设置应用程序自己的“阻塞”处理函数
|
|
WSASetLastError()
|
设置Windows Sockets API的最近错误号
|
|
WSAStartup()
|
初始化隐含的Windows Sockets DLL
|
|
WSAUnhookBlockingHook()
|
恢复原来的“阻塞”处理函数
|
从表6.2可以看出,WinSock的扩充功能可以分为如下几类:
(1) 异步选择机制
异步选择函数WSAAsyncSelect()允许应用程序提名一个或多个感兴趣的网络事件,所有非阻塞的网络I/O例程(如send()和recv()),不管它是已经使用还是即将使用,都可作为WSAAsyncSelect()函数选择的候选。当被提名的网络事件发生时,Windows应用程序的窗口函数将收到一个消息,消息附带的参数指示被提名过的某一网络事件。
(2) 异步请求例程
异步请求例程允许应用程序用异步方式获取请求的信息,如WSAAsyncGetXByY()类函数允许用户请求异步服务,这些功能在使用标准Berkeley函数时是阻塞的。函数WSACancelAsyncRequest()允许用户终止一个正在执行的异步请求。
(3) 阻塞处理方法
WinSock在调用处于阻塞时进入一个叫“Hook”的例程,它负责处理Windows消息,使得Windows的消息循环能够继续。WinSock还提供了两个函数(WSASetBlockingHook()和WSAUnhookBlockingHook())让用户能够设置和取消自己的阻塞处理例程。另外,函数WSAIsBlocking()可以检测调用是否阻塞,函数WSACancelBlockingCall()可以取消一个阻塞的调用。
(4) 出错处理
为了和以后的多线索环境(如Windows/NT)兼容,WinSock提供了两个出错处理函数WSAGetLastError()和WSASetLastError()来获取和设置本线索的最近错误号。
(5) 启动与终止
WinSock的应用程序在使用上述WinSock函数前,必须先调用WSAStartup()函数对Windows Sockets DLL进行初始化,以协商WinSock的版本支持,并分配必要的资源。在应用程序退出之前,应该先调用函数WSACleanup()终止对Windows Sockets DLL的使用,并释放资源,以利下一次使用。
在这些函数中,实现Windows网络实时通信的关键是异步选择函数WSAAsyncSelect()的使用,其原型如下:
int PASCAL FAR WSAAsyncSelect(SOCTET s, HWND hWnd,
unsigned int wMsg, long lEvent);
它请求Windows
Sockets DLL在检测到在套接字s上发生的lEvent事件时,向窗口hWnd发送一个消息wMsg。它自动地设置套接字s处于非阻塞工作方式。参数lEvent由表6.3所列事件的一个或多个组成。例如,我们要在套接字s读准备好或写准备好时接到通知,可以使用下面的语句:
rc = WSAAsyncSelect(s, hWnd, wMsg, FD_READ |
FD_WRITE);
当套接字s上被提名的一个网络事件发生时,窗口hWnd将收到消息wMsg,变量lParam的低字指示网络发生的事件,高字指示错误码。应用程序就可以通过这些信息来决定自己的下一步动作。
表6.3 异步选择网络事件表
|
值
|
含
义
|
|
FD_READ
|
希望在套接字s收到数据(即读准备好)时接到通知
|
|
FD_WRITE
|
希望在套接字s可发送数据(即写准备好)时接到通知
|
|
FD_OOB
|
希望在套接字s上有带外数据到达时接到通知
|
|
FD_ACCEPT
|
希望在套接字s上有外部连接到来时接到通知
|
|
FD_CONNECT
|
希望在套接字s连接建立完成时接到通知
|
|
FD_CLOSE
|
希望在套接字s关闭时接到通知
|
6.3.3 网络程序示例Echo
本章的例子是一个简单的点对点网络实时通信程序Echo。实例分两部分:客户程序wecho.c与服务器程序wechos.c。
其工作过程是:服务器首先启动,它创建套接字之后等待客户的连接;客户在启动后,创建套接字,然后和服务器建立连接;连接建立后,客户接收键盘输入,然后
将数据发送到服务器,服务器收到数据后,只是简单地发送回来,客户将收到的数据在窗口中显示;如此循环,当客户接收到的输入数据是字母“Q”时,它关闭连接和套接字后退出,服务器在用户关闭窗口时关闭套接字。
6.3.3.1 网络客户程序
首先介绍客户程序,该源文件取名为wecho.c,其内容在下面列出。为了方便读者,在程序中关键部分采用中文注释的形式给出说明,读者可照这些注释加强对本节内容的理解。
/* This is a Sample for WinSock. It uses WinSock
routine to communicate with Server through a port -- USERPORT.
Usage: wecho servername */
#include <winsock.h>
#include <windows.h>
#include <stdio.h>
#include <ctype.h>
#include <memory.h>
#include <string.h>
#include "wecho.h"
HANDLE hInst,
AsyncHnd;
char server_address[256]
= {0};
char buffer[MAXGETHOSTSTRUCT];
char FAR * lpBuffer
= &buffer[0];
SOCKET
s =
0;
int connected
= 0;
struct sockaddr_in dst_addr;
struct hostent *hostaddr;
struct hostent hostnm;
unsigned short port
= USERPORT; // 用户端口号,应大于1024。客户和服务器的端口号必须相同。
int
sock_type = SOCK_STREAM;
BOOL
InitWindow(HANDLE);
long FAR PASCAL ClientProc(HWND, unsigned, UINT,
LONG);
VOID AlertUser(HWND, LPSTR);
BOOL Client(HWND);
BOOL set_select(HWND,
long);
BOOL make_skt(HWND);
BOOL connect_skt(HWND);
BOOL send_pkt(HWND, int);
int receive_pkt(HWND);
VOID close_skt(VOID);
void DisplayInfo(HWND,
int);
/****************************************************************************/
int PASCAL
WinMain( HANDLE hInstance, HANDLE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
HWND hWnd;
MSG msg;
lstrcpy((LPSTR)
server_address, lpCmdLine);
if
(!hPrevInstance)
if
(!InitWindow(hInstance))
return
(NULL);
hInst
= hInstance;
hWnd
= CreateWindow("ClientClass", "Windows ECHO Client",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL,
NULL, hInstance, NULL);
if
(!hWnd)
return
(NULL);
ShowWindow(hWnd,
nCmdShow);
UpdateWindow(hWnd);
EnableMenuItem(GetMenu(hWnd),
IDM_STOP, MF_DISABLED | MF_GRAYED);
PostMessage(hWnd,
WM_USER,(WPARAM) 0, (LPARAM) 0);
while
(GetMessage(&msg, NULL, NULL, NULL)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return
(msg.wParam);
}
BOOL
InitWindow( HANDLE hInstance )
{
WNDCLASSWndClass;
WndClass.style
= CS_HREDRAW | CS_VREDRAW;
WndClass.lpfnWndProc
= ClientProc;
WndClass.cbClsExtra
= 0;
WndClass.cbWndExtra
= 0;
WndClass.hInstance
= hInstance;
WndClass.hIcon
= LoadIcon(NULL, IDI_APPLICATION);
WndClass.hCursor
= LoadCursor(NULL, IDC_ARROW);
WndClass.hbrBackground
= COLOR_WINDOW + 1;
WndClass.lpszMenuName
= (LPSTR) "ClientMenu";
WndClass.lpszClassName
= (LPSTR) "ClientClass";
return
(RegisterClass((PWNDCLASS) & WndClass));
}
待续