使用 WINSOCK 实现Windows下网络通信

摘 要 本文介绍了Sockets 通信原理,从程序员角度着重讨论了WindowsSockets为支持异步通信对 Sockets 的功能扩充,并给出了应用 WindowsSockets实现网络实时通信的一个程序实例。

  关键词 Windows Sockets 异步通信 阻塞

  《计算机世界月刊》1994年第10期发表了王亚民、魏肖先生的文章《Windows下网络实时通信技术》,该文介绍了在Windows 3.X下利用NetBIOS网络协议实现工作站之间点对点的网络实时通信功能的方法,阅后很受启发。但遗憾的是,NetBIOS首先是面向DOS开发的,虽然它的非等待工作方式可以实现Windows下的异步通信,但毕竟有许多缺憾(这在王文中已有论述),给人以不太方便的感觉,并且NetBIOS没有Windows下适当的开发环境,普通用户要用它来实现Windows下网络通信,是一件很麻烦的事情。

  所幸的是, IBM 公司于94年4月推出的TCP/IP for DOS V2.1.1提供的开发软件包Programmer's Tool Kit不仅带有DOS下网络编程接口,而且提供了Windows下网络异步通信接口WINSOCK。

一、 Socket网络编程原理

  Socket是BSD UNIX提供的网络应用编程接口,它采用客户-服务器的通讯机制,使网络客户方和服务器方通过Socket实现网络之间的连接和数据交换。Socket提供了一系列的系统调用,使用这些系统调用可以实现TCP, UDP,ICMP和IP等多种网络协议之间的通讯。

  Socket有三种主要类型:stream sockets, datagram sockets 和raw sockets 。 Stream socket接口定义了一种可靠的面向连接的服务,它实现了无差错无重复的顺序数据传输。它通过内置的流量控制解决了数据的拥塞,应用程序可以发送任意长度的数据,将数据当作字节流。Datagram socket接口定义了一种无连接的服务,数据通过相互独立的包进行传输,包的传输是无序的,并且不保证是否出错、丢失和重复。包长度是有限的(隐含长度为8,192字节,最大长度可设为32,768字节)。Raw socket接口允许对低层协议如IP和ICMP的直接存取,它主要用于新的网络协议实现的测试等。

下面我们通过一个面向连接的传输发生的典型情况来说明socket网络通信的实现。

服务器 客户

wndstx-image002

图1. 面向连接的协议实现的Socket调用

  从上图我们可以看出,客户和服务器的关系不是对称的,服务器首先启动,然后在某一时间启动客户与服务器建立连接。服务器和客户开始都必须用调用socket()建立一个套接字(socket),然后服务器调用bind()将套接字与一个本地网络地址捆扎在一起,再用调用listen()使套接字处于一种被动的准备接收状态,同时规定它的请求队列长度,之后服务器就可以调用accept()来接收连接了。客户在建立套接字之后,便可以通过调用connect()和服务器建立连接。连接建立后,客户和服务器之间就可以通过连接发送和接收数据(调用read()和write())。最后,待数据传送结束,双方调用close()关闭套接字。

二、 WINSOCK对Socket的扩充

  BSD Socket支持阻塞(blocking)和非阻塞(non_blocking)两种工作方式。在阻塞方式下工作,connect()、accept()、read()和recv()等调用在执行时都处于阻塞状态直到它成功或出错返回。在非阻塞方式下工作,这些调用是立即返回的,但是它们是否完成得靠查询才能知道。对于Windows这种非抢先多任务操作系统来说,这两种工作方式都是很难以接受的,为此,WINSOCK在尽量与BSD Socket保持一致外,又对它作了必要的扩充。

  WINSOCK对BSD Socket的扩充主要是在基于消息、对网络事件的异步存取接口上。下表列出了 WINSOCK扩充的函数功能。

函 数 名

功 能

WSAAsyncGetHostByAddr()

标准 Berkeley函数getXbyY的异步版本,例

WSAAsyncGetHostByName()

如:函数 WSAAsyncGetHostByName()就是提

WSAAsyncGetProtoByName()

供了标准 Berkeley函数gethostbyname的一

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()

恢复原来的“阻塞”处理函数

表1 . WINSOCK扩充函数功能表

从表1可以看出,WINSOCK的扩充功能可以分为如下几类:

(1)异步选择机制:

异步选择函数WSAAsyncSelect()允许应用程序提名一个或多个感兴趣的网络事 件,所有非阻塞的网络I/O例程(如send()和resv()),不管它是已经使用还是即将使用,都可作为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的版本支持,并分配必要的资源。在应用程序退出之前,应该先调用函数WSAClearnup()终止对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由下列事件的一个或多个组成:

       值
           含 义

              FD_READ 希望在套接字s收到数据(即读准备好)时接到通知

                FD_WRITE 希望在套接字s可发送数据(即写准备好)时接到通知

                FD_OOB 希望在套接字s上有带外数据到达时接到通知

                FD_ACCEPT 希望在套接字s上有外部连接到来时接到通知

                FD_CONNECT 希望在套接字s连接建立完成时接到通知

                FD_CLOSE 希望在套接字s关闭时接到通知

表2. 异步选择网络事件表

例如,我们要在套接字s读准备好或写准备好时接到通知,可以使用下面的语句:

rc = WSAAsyncSelect(s, hWnd, wMsg, FD_READ | FD_WRITE);

当套接字 s上被提名的一个网络事件发生时,窗口hWnd将收到消息wMsg,变量lParam的低字指示网络发生的事件,高字指示错误码。应用程序就可以通过这些信息来决定自己的下一步动作。

三、网络实时通信的实现

我们来设计一个简单的基于连接的点对点网络实时通信程序:服务器首先启动,它建立套接字之后等待客户的连接;客户在启动后,建立套接字,然后和服务器建立连接;连接建立后,客户通过连接给服务器发送一段数据,服务器接收后又将它发送回来,客户再发送,如此循环,直至用户命令客户退出或网络出错;客户关闭连接和套接字后退出,服务器在检测到连接关闭后,关闭套接字自动结束。

我们的实例是UNIX下基于BSD Socket的服务器程序和Windows下基于WINSOCK的客户程序之间的通信。服务器在主机UNIX下直接运行,前台和后台均可;客户在Windows下运行,带一个参数,即主机的名字。如 win client rs6000,rs6000是在HOSTS文件中已定义好的主机名。

我们先看客户程序,首先定义几个宏、菜单资源和部分全局变量:

程序1:部分 Windows程序源代码(宏、菜单和变量)

#define USERPORT 3333 /* 用户定义端口号 */

#define IDM_START 101 /* “启动”菜单项标志 */

#define IDM_EXIT 102 /* “退出”菜单项标志 */

#define UM_SOCK WM_USER+0x100 /* 用户定义网络消息 */

ClientMenu MENU /* 客户菜单 */

BEGIN

POPUP "&Server"

BEGIN

MENUITEM "&Start...", IDM_START

MENUITEM "S&top", IDM_STOP

END

END

#include /* 必须包含winsock.h头文件 */

HANDLE hInst;

char server_address[256] = {0}; /* 服务器地址缓冲区 */

char buffer[1024]; /* 接收发送缓冲区 */

char FAR * lpBuffer = &buffer[0];

SOCKET s = 0; /* 套接字 */

struct sockaddr_in dst_addr; /* 目标地址 */

struct hostent *hostaddr; /* 主机地址 */

struct hostent hostnm;

int count = 0; /* 发送接收循环计数器 */

客户程序的窗口主函数很简单,它在注册窗口类、建立窗口后,只是给主窗口函数发送一个用户消息,然后就进入Windows消息处理循环。

程序2:部分 Windows程序源代码(窗口主函数)

int PASCAL WinMain( HANDLE hInstance,HANDLE hPrevInstance,LPSTR lpCmdLine, int nCmdShow)

{

HWND hWnd;

MSG msg;

lstrcpy((LPSTR) server_address, lpCmdLine); /* 取主机名字 */

if (!hPrevInstance)

if (!InitApplication(hInstance))

return (FALSE);

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 (FALSE);

ShowWindow(hWnd, nCmdShow);

UpdateWindow(hWnd);

/* 给主窗口函数发送WM_USER消息 */

PostMessage(hWnd,WM_USER,(WPARAM) 0, (LPARAM) 0);

while (GetMessage(&msg, NULL, NULL, NULL)) {

TranslateMessage(&msg);

DispatchMessage(&msg);

}

return (msg.wParam);

}

主窗口函数ClientProc是程序的主要部分,它处理相关的消息:在接到消息WM_USER后,它调用函数WSAStartup()初始化Windows Sockets DLL,并检查其版本号,然后通过主机名获取主机地址;在接到消息WM_COMMAND时,如果是命令IDM_START,则调用子程序Client()建立套接字,并试图和服务器建立连接,如果是命令IDM_STOP,则调用函数WSACleanup()终止Windows Sockets DLL,并发出终止应用程序的消息;在接到消息UM_SOCK时,它根据参数lParam指示的网络事件,进行相应的操作,然后选择下一个期望的网络事件。

程序3:部分 Windows程序源代码(主窗口函数)

long FAR PASCAL

ClientProc( HWND hWnd, unsigned message, UINT wParam, LONG lParam)

{

int length,i;

WSADATA wsaData; /* 描述Windows Sockets实现细节的数据结构 */

int Status;

switch (message) {

case WM_USER:

Status = WSAStartup(0x101, &wsaData);

if (Status != 0) {

AlertUser(hWnd, "WSAStartup() failed/n");

PostQuitMessage(0);

}

if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) { /* 现在支持的版本时WINSOCK.DLL 1.1 */

AlertUser(hWnd, "WSAStartup() Version not match/n");

WSACleanup();

PostQuitMessage(0);

}

hostaddr = gethostbyname(server_address);

if (hostaddr == NULL) {

AlertUser(hWnd, "gethostbyname ERROR /n");

WSACleanup();

PostQuitMessage(0);

}

memcpy(&hostnm, hostaddr,sizeof(struct hostent));

break;

case WM_COMMAND:

switch (wParam) {

case IDM_START:

if (!Client(hWnd)) {

closesocket(s);

AlertUser(hWnd, "Start Failed");

}

break;

case IDM_STOP:

WSACleanup();

PostQuitMessage(0);

break;

}

break;

case UM_SOCK:

switch (lParam) {

case FD_CONNECT: /* 网络事件:连接建立 */

if (!set_select(hWnd, FD_WRITE)) /* 选择:期望发送 */

closesocket(s);

break;

case FD_READ: /* 网络事件:读准备好 */

if (!receive_pkt(hWnd)) { /* 接收数据 */

AlertUser(hWnd, "Receive Packet Failed");

closesocket(s);

break;

}

if (!set_select(hWnd, FD_WRITE)) /* 选择:期望发送 */

closesocket(s);

break;

case FD_WRITE: /* 网络事件:写准备好 */

for (i=0;i<1024;i++)

buffer[i] = (char) 'A' + i % 26;

length = 1024;

if (!(send_pkt(hWnd, length))) { /* 发送数据 */

AlertUser(hWnd, "Packet Send Failed");

closesocket(s);

break;

}

if (!set_select(hWnd, FD_READ)) /* 选择:期望接收 */

closesocket(s);

break;

case FD_CLOSE: /* 网络事件:连接关闭。操作:停止异步选择 */

if (WSAAsyncSelect(s,hWnd,0,0) == SOCKET_ERROR)

AlertUser(hWnd, "WSAAsyncSelect Failed");

AlertUser(hWnd, "Socket has been closed");

break;

default: /* 网络出错则警告,其他事件忽略 */

if (WSAGETSELECTERROR(lParam) != 0) {

AlertUser(hWnd, "Socket Report Failure");

closesocket(s);

break;

}

break;

}

break;

case WM_DESTROY:

closesocket(s); /* 关闭窗口前应该关闭套接字,并 */

WSACleanup(); /* 终止Windows Sockets DLL */

PostQuitMessage(0);

break;

default:

return (DefWindowProc(hWnd, message, wParam, lParam));

}

return (NULL);

}

程序4:部分 Windows程序源代码(子程序)

BOOL Client( HWND hWnd ) /* 客户子程序 */

{

if (!make_skt(hWnd)) /* 建立套接字 */

return(FALSE);

if (!set_select(hWnd, FD_CONNECT)) /* 设置异步连接 */

return(FALSE);

if (!connect_skt(hWnd)) /* 建立连接 */

return(FALSE);

return(TRUE);

}

BOOL receive_pkt(HWND hWnd) /* 接收数据子程序 */

{

HDC dc;

int length;

int l1, l2, l3;

char line1[255], line2[255], line3[255];

count ++; /* 循环计数器加1 */

if ((length = recv(s, lpBuffer, 1024, 0)) == SOCKET_ERROR) return(FALSE);/* 如果接收数据出错,则返回FALSE */

if (length == 0) /* 接收数据长度为零,表示连接中断 */

return(FALSE);

if (dc = GetDC(hWnd)) { /* 接收数据成功,显示信息 */

l1 = wsprintf((LPSTR) line1, "TCP Echo Client No.%d ",count);

l2 = wsprintf((LPSTR) line2, "Received %d bytes ", length);

l3 = wsprintf((LPSTR) line3, "Those are: %c,%c,%c,%c,%c,%c ", buffer[0],buffer[1],buffer[2],buffer[100],buffer[1000],buffer[1023]);

TextOut(dc, 10, 2, (LPSTR) line1, l1);

TextOut(dc, 10, 22, (LPSTR) line2, l2);

TextOut(dc, 10, 42, (LPSTR) line3, l3);

ReleaseDC(hWnd, dc);

}

return(TRUE);

}

BOOL set_select( HWND hWnd, long lEvent) /* 异步选择子程序 */

{

if (WSAAsyncSelect(s,hWnd,UM_SOCK, lEvent) == SOCKET_ERROR) {

AlertUser(hWnd, "WSAAsyncSelect Failed");

return (FALSE);

}

return (TRUE);

}

BOOL make_skt( HWND hWnd ) /* 建立套接字子程序 */

{

if ((s = socket(AF_INET, sock_type, 0)) == INVALID_SOCKET) {

AlertUser(hWnd, "Socket Failed");

return (FALSE);

}

return (TRUE);

}

BOOL connect_skt( HWND hWnd ) /* 建立连接子程序 */

{

memset((void *) &dst_addr, sizeof(dst_addr), 0);

dst_addr.sin_family = AF_INET;

dst_addr.sin_port = htons(USERPORT);

dst_addr.sin_addr.s_addr = *((unsigned long *)hostnm.h_addr_list[0]);

if (connect(s, (struct sockaddr *) & dst_addr,

sizeof(dst_addr)) == SOCKET_ERROR) {

AlertUser(hWnd, "Connect Failed");

return (FALSE);

}

return (TRUE);

}

BOOL send_pkt( HWND hWnd, int len) /* 发送数据子程序 */

{

int length;

if ((length = send(s, lpBuffer, len, 0)) == SOCKET_ERROR)

return (FALSE);

else if (length != len) {

AlertUser(hWnd, "Send Lenght NOT Match!");

return(FALSE);

}

return (TRUE);

}

我们用最简单的语句编制一个UNIX下基于BSD SOCKET的服务器程序,它在建立连接后,只负责将收到的数据发回去,在连接断开后,服务器关闭套接字返回。要编制在Windows下的服务器程序,可参照客户程序,使用WINSOCK的异步选择机制。

程序5: UNIX下服务器程序源代码

/* TCP/IP 必要的头文件 */

#include

#include

#include

#include

#define USERPORT 3333 /* 用户定义端口号,与客户相同 */

#define HOST_IP_ADDR "166.111.8.80" /* 我们的主机地址 */

main(int argc, char **argv)

{

char buf[1024]; /* buffer for sending and receiving data */

struct sockaddr_in client; /* client address information */

struct sockaddr_in server; /* server address information */

int s; /* socket for accepting connections */

int ns; /* socket connected to client */

int namelen; /* length of client name */

int pktlen; /* length of packet received or sended */

if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0) {

perror("Socket()");

return;

}

/* Bind the socket to the server address. */

bzero((char*)&server,sizeof(server));

server.sin_len = sizeof(struct sockaddr_in);

server.sin_family = AF_INET;

server.sin_port = htons(USERPORT);

server.sin_addr.s_addr = INADDR_ANY;

if (bind(s, (struct sockaddr *)&server, sizeof(server)) < 0) {

perror("Bind()");

return;

}

/* Listen for connections. Specify the backlog as 1. */

if (listen(s, 1) != 0) {

perror("Listen()");

return;

}

/* Accept a connection. */

namelen = sizeof(client);

if ((ns = accept(s, (struct sockaddr *)&client, &namelen)) == -1) {

perror("Accept()");

return;

}

/* Receive the message on the newly connected socket. */

for (;;) {

if ((pktlen = recv(ns, buf, 1024, 0)) < 0) {

perror("Recv()");

break;

}

else if (pktlen == 0) {

printf("Recv(): return FAILED, connection is shut down! /n");

break;

}

else printf("Recv(): return SUCCESS, packet length = %d /n",pktlen);

sleep(1); /* Sleep() 1秒钟是为了减慢数据交换速度 */

/* Send the message back to the client. */

if (send(ns, buf, pktlen, 0) < 0) {

perror("Send()");

break;

}

else printf("Send(): return SUCCESS, packet length = %d /n",pktlen);

}

close(ns);

close(s);

printf("Server ended successfully/n");

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值