黄莹莹你微博:Socket 简易入门手册

来源:百度文库 编辑:九乡新闻网 时间:2024/04/29 11:34:47
我的地盘 任我玩乐 杂七杂八 想嘛做嘛
主页 | 档案文件 网络日志 照片 | 列表 | 音乐
  网络日志   2006-2-17 年前年后的一些照片 放这儿留个纪念 还有一段大家劝酒的录象,很是精彩,不知道怎么放 添加评论 | 阅读评论 (4) 13:46:50  |  固定链接 | 引用通告 (0) | 记录它 固定链接 关闭 2006-2-15 再加几个有关unix socket的有用的连接

BSD Socket Programming,

?

BSD Socket

?

Socket编程中select()的妙用

?

永远的unix

添加评论 | 阅读评论 (4) 16:43:29  |  固定链接 | 引用通告 (0) | 记录它 | 计算机与 Internet 固定链接 关闭 BSD Socket 简易入门手册 翻译:Wilbur Lang

目录
介绍
类比 (什么是 socket ?)
装上你的新电话(怎样侦听?)
拨号 (如何调用 socket)
谈话(如何通过 sockets 交谈)
挂起(结束)
世界语(交流的语言很重要)
未来在你的掌握了(下一步?)


--------------------------------------------------------------------------------

介绍
当你进入 UNIX 的神秘世界后,立刻会发现越来越多的东西难以理解。对于大多数人来说,BSD socket 的概念就是其中一个。这是一个很短的教程来解释他们是什么、他们如何工作并给出一些简单的代码来解释如何使用他们。

类比 (什么是 socket ?)
socket 是进行程序间通讯(IPC)的 BSD 方法。这意味着 socket 用来让一个进程和其他的进程互通信息,就象我们用电话来和其他的人交流一样。

用电话来比喻是很恰当的,我们在后面将一直用电话这个概念来描叙 socket 。

装上你的新电话(怎样侦听?)
一个人要能够收到别人打给他的电话,首先他要装上一门电话。同样,你必须先建立 socket 以侦听线路。这个过程包含几个步骤。首先,你要建立一个新的 socket,就象先装上电话一样。socket() 命令就完成这个工作。

因 为 sockets 有几种类型,你要注明你要建立什么类型的。你要做一个选择是 socket 的地址格式。如同电话有音频和脉冲两种形式一样,socket 有两个最重要的选项是 AF_UNIX 和 IAF_INET。AF_UNIX 就象 UNIX 路径名一样识别 sockets。这种形式对于在同一台机器上的 IPC 很有用。而 AF_INET 使用象 192.9.200.10 这样被点号隔开的四个十进制数字的地址格式。除了机器地址以外,还可以利用端口号来允许每台机器上的多个 AF_INET socket。我们这里将着重于 AF_INET 方式,因为他很有用并广泛使用。

另外一个你必须提供的参数是 socket 的类型。两个重要的类型是 SOCK_STREAM 和 SOCK_DGRAM。 SOCK_STREAM 表明数据象字符流一样通过 socket 。而 SOCK_DGRAM 则表明数据将是数据报(datagrams)的形式。我们将讲解 SOCK_STREAM sockets,他很常见并易于使用。

在建立 socket 后,我们就要提供 socket 侦听的地址了。就象你还要个电话号码来接电话一样。bind() 函数来处理这件事情。

SOCK_STREAM sockets 让连接请求形成一个队列。如果你忙于处理一个连接,别的连接请求将一直等待到该连接处理完毕。listen() 函数用来设置最大不被拒绝的请求数(一般为5个)。一般最好不要使用 listen() 函数。

下面的代码说明如何利用 socket()、 bind() 和 listen() 函数建立连接并可以接受数据。


/* code to establish a socket; originally from bzs@bu-cs.bu.edu
*/

int establish(unsigned short portnum)
{ char myname[MAXHOSTNAME+1];
int s;
struct sockaddr_in sa;
struct hostent *hp;

memset(&sa, 0, sizeof(struct sockaddr_in)); /* clear our address */
gethostname(myname, MAXHOSTNAME); /* who are we? */
hp= gethostbyname(myname); /* get our address info */
if (hp == NULL) /* we don‘t exist !? */
return(-1);
sa.sin_family= hp->h_addrtype; /* this is our host address */
sa.sin_port= htons(portnum); /* this is our port number */
if ((s= socket(AF_INET, SOCK_STREAM, 0)) < 0) /* create socket */
return(-1);
if (bind(s,&sa,sizeof(struct sockaddr_in)) < 0) {
close(s);
return(-1); /* bind address to socket */
}
listen(s, 3); /* max # of queued connects */
return(s);
}

在建立完 socket 后,你要等待对该 socket 的调用了。accept() 函数为此目的而来。调用 accept() 如同在电话铃响后提起电话一样。Accept() 返回一个新的连接到调用方的 socket 。

下面的代码演示使用是个演示。


/* wait for a connection to occur on a socket created with establish()
*/
int get_connection(int s)
{ int t; /* socket of connection */

if ((t = accept(s,NULL,NULL)) < 0) /* accept connection if there is one */
return(-1);
return(t);
}

和电话不同的是,在你处理先前的连接的时候,你还可以接受调用。为此,一般用 fork 来处理每个连接。下面的代码演示如何使用 establish() 和 get_connection() 来处理多个连接。


#include /* obligatory includes */
#include
#include
#include
#include
#include
#include
#include
#include

#define PORTNUM 50000 /* random port number, we need something */

void fireman(void);
void do_something(int);

main()
{ int s, t;

if ((s= establish(PORTNUM)) < 0) { /* plug in the phone */
perror("establish");
exit(1);
}

signal(SIGCHLD, fireman); /* this eliminates zombies */

for (;;) { /* loop for phone calls */
if ((t= get_connection(s)) < 0) { /* get a connection */
if (errno == EINTR) /* EINTR might happen on accept(), */
continue; /* try again */
perror("accept"); /* bad */
exit(1);
}
switch(fork()) { /* try to handle connection */

 

[1]?[2]?下一页??

 

case -1 : /* bad news. scream and die */
perror("fork");
close(s);
close(t);
exit(1);
case 0 : /* we‘re the child, do something */
close(s);
do_something(t);
exit(0);
default : /* we‘re the parent so look for */
close(t); /* another connection */
continue;
}
}
}

/* as children die we should get catch their returns or else we get
* zombies, A Bad Thing. fireman() catches falling children.
*/
void fireman(void)
{
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}

/* this is the function that plays with the socket. it will be called
* after getting a connection.
*/
void do_something(int s)
{
/* do your thing with the socket here
:
:
*/
}

拨号 (如何调用 socket)
现在你应该知道如何建立 socket 来接受调用了。那么如何调用呢?和电话一样,你要先有个电话。用 socket() 函数来完成这件事情,就象建立侦听的 socket 一样。

在给 socket 地址后,你可以用 connect() 函数来连接侦听的 socket 了。下面是一段代码。


int call_socket(char *hostname, unsigned short portnum)
{ struct sockaddr_in sa;
struct hostent *hp;
int a, s;

if ((hp= gethostbyname(hostname)) == NULL) { /* do we know the host‘s */
errno= ECONNREFUSED; /* address? */
return(-1); /* no */
}

memset(&sa,0,sizeof(sa));
memcpy((char *)&sa.sin_addr,hp->h_addr,hp->h_length); /* set address */
sa.sin_family= hp->h_addrtype;
sa.sin_port= htons((u_short)portnum);

if ((s= socket(hp->h_addrtype,SOCK_STREAM,0)) < 0) /* get socket */
return(-1);
if (connect(s,&sa,sizeof sa) < 0) { /* connect */
close(s);
return(-1);
}
return(s);
}

这个函数返回一个可以流过数据的 socket 。

谈话(如何通过 sockets 交谈)
好 了,你在要传输数据的双方建立连接了,现在该传输数据了。read() 和 write() 函数来处理吧。除了在 socket 读写和文件读写中的一个区别外,和处理一般的文件一样。区别是你一般不能得到你所要的数目的数据。所以你要一直循环到你需要的数据的到来。一个简单的例 子:将一定的数据读到缓存。


int read_data(int s, /* connected socket */
char *buf, /* pointer to the buffer */
int n /* number of characters (bytes) we want */
)
{ int bcount; /* counts bytes read */
int br; /* bytes read this pass */

bcount= 0;
br= 0;
while (bcount < n) { /* loop until full buffer */
if ((br= read(s,buf,n-bcount)) > 0) {
bcount += br; /* increment byte counter */
buf += br; /* move buffer ptr for next read */
}
else if (br < 0) /* signal an error to the caller */
return(-1);
}
return(bcount);
}

相同的函数也可以写数据,留给我们的读者吧。

挂起(结束)
和你通过电话和某人交谈后一样,你要在 socket 间关闭连接。一般 close() 函数用来关闭每边的 socket 连接。如果一边的已经关闭,而另外一边却在向他写数据,则返回一个错误代码。

世界语(交流的语言很重要)
现在你可以在机器间联络了,可是要小心你所说的话。许多机器有自己的方言,如 ASCII 和 EBCDIC。更常见的问题是字节顺序问题。除非你一直传输的都是文本,否则你一定要注意这个问题。幸运的是,人们找出了解决的办法。

在很久以前,人们争论哪种顺序更“正确”。现在必要时有相应的函数来转换。其中有 htons()、ntohs()、htonl() 和 ntohl()。在传输一个整型数据前,先转换一下。


i= htonl(i);
write_data(s, &i, sizeof(i));

在读数据后,再变回来。


read_data(s, &i, sizeof(i));
i= ntohl(i);

如果你一直坚持这个习惯,你将比别人少出错的机会。

未来在你的掌握了(下一步?)
就用我们刚才讨论的东西,你就可以写自己的通讯程序了。和对待所有的新生事物一样, 最好还是看看别人已经做了些什么。这里有许多关于 BSD socket 的东西可以参考。

请注意,例子中没有错误检查,这在“真实”的程序中是很重要的。你应该对此充分重视 添加评论 16:24:29  |  固定链接 | 引用通告 (0) | 记录它 | 计算机与 Internet 固定链接 关闭 Unix和Windows跨系统通讯编程

摘 要 本文介绍了套接字(Socket)的基本概念及编程技术,并结合实例说明在Unix和Windows下如何用套接字实现客户/服务器方式的通讯编程。
关键词 Berkeley Sockets Windows Sockets 通讯编程

一、 前言
  随着Internet的不断发展,客户机/服务器模型得到了广泛的应用。客户应用程序向服务器程序请求服务。一个服务程序通 常在一个众所周知的端口监听对服务的请求,也就是说,服务进程一直处于休眠状态,直到一个客户对这个服务的地址提出了连接请求。在这个时刻,服务程序被 “惊醒”并且为客户提供服务或对客户的请求作出适当的反应。   Unix最早是由美国贝尔实验室发明的一种多用户、多任务的通用操作系统,由于 Unix具有技术成熟、可靠性高、网络和数据库功能强、伸缩性突出和开发性好的特色,可满足各行各业的实际需要,特别是企业重要业务的需要,已经成为主要 的工作平台和重要的企业操作平台,而微软公司的 Windows操作系统用户界面友好,安装、使用也比较方便,应用软件丰富,在个人PC机上成为主流操作系统。因此服务端使用 Unix,客户端使用 Windows能充分利用它们各自的优点,这也是今后发展的一个趋势。
  同时,金融行业的中间业务发展迅速,银行主机采用 Unix系统,而各代收代付单位多采用 Windows或 WindowsNT系统,在它们之间传输数据文件也涉及跨系统通讯的问题,通过套接字 Socket能方便地实现 Unix和 Windows的跨系统通讯,本文拟就这一问题作一探讨。

二、 SOCKET简介
   TCP/IP是计算机互连最常使用的网络通讯协议, TCP/IP的核心部分由网络操作系统的内核实现,应用程序通过编程接口来访问 TCP/IP,见下图:

 

图1 应用程序与Windows Socket关系图

  七十年代中,美国国防部高研署(DARPA)将TCP/IP的软件提供给加利福尼亚大学Berkeley分校后,TCP/IP很快被集成到 Unix中,同时出现了许多成熟的TCP/IP应用程序接口(API)。这个API称为Socket接口。今天,SOCKET接口是TCP/IP网络最为 通用的API,也是在INTERNET上进行应用开发最为通用的API。
  九十年代初,由Microsoft联合了其他几家公司共同制定了一套 WINDOWS下的网络编程接口,即Windows Sockets规范。它是Berkeley Sockets的重要扩充,主要是增加了一些异步函数,并增加了符合 Windows 消息驱动特性的网络事件异步选择机制。 Windows Sockets规范是一套开放的、支持多种协议的 Windows下的网络编程接口。目前,在实际应用中的Windows Sockets规范主要有1.1版和2.0版。两者的最重要区别是1.1版只支持TCP/IP协议,而2.0版可以支持多协议,2.0版有良好的向后兼容 性,目前,Windows下的Internet软件都是基于 WinSock开发的。
  Socket实际在计算机中提供了一个通信端口,可以通 过这个端口与任何一个具有Socket接口的计算机通信。应用程序在网络上传输,接收的信息都通过这个Socket接口来实现。在应用开发中就像使用文件 句柄一样,可以对 Socket句柄进行读、写操作。我们将 Socket翻译为套接字,套接字分为以下三种类型:
  字节流套接字(Stream Socket) 是最常用的套接字类型,TCP/IP协议族中的 TCP 协议使用此类接口。字节流套接口提供面向连接的(建立虚电路)、无差错的、发送先后顺序一致的、无记录边界和非重复的网络信包传输。
数据报套接字 (Datagram Socket) TCP/IP协议族中的UDP协议使用此类接口,它是无连接的服务,它以独立的信包进行网络传输,信包最大长度为32KB,传输不保证顺 序性、可靠性和无重复性,它通常用于单个报文传输或可靠性不重要的场合。数据报套接口的一个重要特点是它保留了记录边界。对于这一特点。数据报套接口采用 了与现在许多包交换网络(例如以太网)非常类似的模型。
  原始数据报套接字(Raw Socket) 提供对网络下层通讯协议(如IP协议)的直接访问,它一般不是提供给普通用户的,主要用于开发新的协议或用于提取协议较隐蔽的功能。

三、 基于SOCKET的应用开发

 

图2 面向连接协议的SOCKET编程模型

  面向连接协议的SOCKET编程模型应用最为广泛,因为面向连接协议提供了一系列的数据纠错功能,可以保证在网络上传输的数据及时、无误地到达对方。基于连接协议(字节流套接字)的服务是设计客户机/服务器应用程序时的标准,其编程模型如下:
尽管Windows Sockets和Berkeley Sockets都是TCP/IP应用程序的编程接口,但二者由于分属不同的系统,在某些环节仍有一些差别。Windows Sockets API没有严格地坚持Berkeley传统风格,通常这么做是因为在Windows环境中实现的难度。
  1.套接口数据类型和错误数值
   Windows Sockets规范中定义了一个新的数据类型 SOCKET,这一类型的定义对于将来Windows Sockets规范的升级是必要的。这一类型的定义保证了应用程序向Win32 环境的可移植性。因为这一类型会自动地从16位升级到32位。
在 UNIX中,所有句柄包括套接口句柄,都是非负的短整数。 Windows Sockets 句柄则没有这一限制,除了INVALID_SOCKET 不是一个有效的套接口外,套接口可以取从0到 INVALID_SOCKET-1 之间的任意值。因为 SOCKET 类型是unsigned ,所以编译已经存在于UNIX 环境中的应用程序的源代码可能会导致 signed/unsigned 数据类型不匹配的警告。
  因此,在socket() 例程和accept() 例程返回时,检查是否有错误发生就不应该再使用把返回值和-1比较的方法,或判断返回值是否为负(这两种方法在BSD 中都是很普通,很合法的途径)。取而代之的是,一个应用程序应该使用常量INVALID_SOCKET ,该常量已在WINSOCK.H 中定义。
  例如:
  BDS 风格
  m_hSocket=socket(…);
  if(m_hSocket=-1)   /*or m_hSocket<0*/
    {…}
  Windows风格:
  m_hSocket=socket(…);
  if(m_hSocket=INVALID_SOCKET)
    {…}
  2.select() 函数和FD_*宏
  由于一个套接口不再表示为非负的整数,select() 函数在Windows Sockets API中的实现有一些变化:每一组套接口仍然用 fd_set 类型来代表,但是它并不是一个位掩码。
  typedef struct fd_set{
   u_int fd_count;
   SOCKET fd_array;
  } fd_set;
  整个组的套接口是用了一个套接口的数组来实现的。为了避免潜在的危险,应用程序应该坚持用FD_XXX 宏(FD_SET,FD_ZERO,FD_CLR,FD_ISSET)来设置,初始化,清除和检查fd_set 结构。
  3.错误代码-errno,h_errno,WSAGetLastError()
Windows Sockets 实现所设置的错误代码是无法通过 errno 变量得到的。另外对于 getXbyY() 这一类的函数,错误代码无法从h _errno 变量得到,错误代码可以使用 WSAGetLastError()调用得到,这种改进是为了适应多线程程序设计的需要。WSAGetLastError()允许程序员能够得到对应于每 一线程的最近的错误代码。
  为了保持与 BSD 的兼容性,应用程序可以加入以下一行代码:
  #define errno WSAGetLastError()
这就保证了用全程的 errno 变量所写的网络程序代码在单线程环境中可以正确使用。当然,这样做有许多明显的缺点:如果一个源程序对套接口和非套接口函数都用 errno 变量来检查错误,那么这种机制将无法工作。此外,一个应用程序不可能为 errno 赋一个新的值 (在Windows Sockets 中,WSAGetLastError()函数可以做到这一点)。
  例如:
  BSD风格:
  retcode=recv(…);
  if(retcode=-1 && errno=EWOULDBLOCK)
  {…}
  Windows风格:
  retcode=recv(…);
  if(retcode=-1 && WSAGetLastError ()=EWOULDBLOCK)
  {…}
  虽然为了兼容性原因,错误常量与4.3BSD 所提供的一致:应用程序应该尽可能地使用“WSA”系列错误代码定义。且常量 SOCKET_ERROR是被用来检查API 调用失败的。虽然对这一常量的使用并不是强制性的,但这是推荐的。例如,上面程序更准确应该是:
  retcode=recv(…)
  if(retcode=SOCKET_ERROR&&WSAGetLastError()=
  WSAEWOULDBLOCK)
  {…}
  4.重命名的函数
  有两种原因Berkeley 套接口中的函数必须重命名,以避免与其他的 API 冲突:
  ① close() 和closesocket()
在Berkeley套接口中,套接口出现的形式与标准文件描述字相同,所以 close() 函数可以用来象关闭文件一样关闭套接口。在Windows Sockets API中,套接字和正常文件句柄不是等同的,例如read(),write() 和close() 在应用于套接口后不能保证正确工作。套接口必须使用 closesocket()例程来关闭,用close() 例程来关闭套接口是不正确的。
  ② ioctl()和iooctlsocket()
   Windows Sockets定义ioctlsocket() 例程,用于实现 BSD中ioctl() 和fcntl() 的功能。
  5.阻塞例程和 EINPROGRESS 宏
虽然Windows Sockets 支持关于套接口的阻塞操作,但是这种应用是被强烈反对的,如果程序员被迫使用阻塞模式(例如一个准备移植的已有的程序,BSD 包含了大量的阻塞函数,且其默认的工作方式都是阻塞的),那么他应该清楚地知道Windows Sockets中阻塞操作的语义。
  6.指针
  所有应用程序与Windows Sockets 使用的指针都必须是FAR 指针,为了方便应用程序开发者作用,Windows Sockets规范定义了数据类型LPHOSTENT 。
  7. Windows Sockets支持的最大套接口数目
一个Windows Sockets 应用程序可以使用的套接口的最大数目是在编译时由常量 FD_SETSIZE 决定的。这个常量在 select() 函数中被用来组建fd_set 结构。在WINSOCK.H 中缺省值是64。如果一个应用程序希望能够使用超过64个套接口,则编程人员必须在每一个源文件包含 WINSOCK.H 前定义确切的FD_SETSIZE值。有一种方法可以完成这项工作,就是在工程项目文件中的编译器选项上加入这一定义。 FD_SETSIZE定义的值对Windows Sockets实现所支持的套接口的数目并无任何影响。
  8.头文件
  为了方便基于 Berkeley 套接口的已有的源代码的移植, Windows Sockets支持许多 Berkeley头文件。这些 Berkeley头文件被包含在WINSOCK.H中。所以一个 Windows Sockets应用程序只需简单的包含 WINSOCK.H就足够了(这也是一种被推荐使用的方法)。

四、 跨系统通讯编程实例
  下面通过一个实例来具体说明 Socket 在Unix 和Windows 跨系统通讯编程中的应用。
Internet 上可以提供一种叫 IRC(Internet Relay Chatting,Internet 在线聊天系统)的服务。使用者通过客户端的程序登录到 IRC 服务器上,就可以与登录在同一 IRC 服务器上的客户进行交谈,这也就是平常所说的聊天室。在这里,给出了一个在运行 TCP/IP 协议的网络上实现 IRC 服务的程序。其中,服务器运行在 SCO Open Server 5.0.5上,客户端运行在Windows98 或 Windows NT 上。
  在一台计算机上运行服务端程序,同一网络的其他计算机上运行客户端程序,登录到服务器上,各个客户之间就可以聊天了。
  1.服务端
服务端用名为 client 的整型数组记录每个客户的已连接套接口描述字,此数组中的所有元素都初始化为-1。同时服务器既要处理监听套接口,又要处理所有已连接套接口,因此需要用 到 I/O 复用。通过 select 函数内核等待多个事件中的任一个发生,并仅在一个或多个事件发生或经过某指定时间后才唤醒进程。维护一个读描述字集 rset ,当有客户到达时,在数组 client中的第一个可用条目(即值为-1的第一个条目)记录其已连接套接口的描述字。同时把这个已连接描述字加到读描述字集rset 中,变量maxi 是当前使用的数组 client 的最大下标,而变量 maxfd+1 是函数 select第一个参数的当前值。如下图:(假设有两个客户与服务器建立连接)

 

 

图3 第二客户建立连接后的TCP服务器

 

图4 第二客户建立连接后的数据结构

  服务器在指定的端口上监听,每当一个套接口接收到信息,都将会把接收到的信息发送给每一个Client 。其主要源程序如下:
  int main(int argc,char **argv)
  {
  int   i,maxi,maxfd,listenfd,connfd,sockfd;
  int   nready,client;
  ssize_t n;
  fd_set rset,allset;
  char line〔MAXLNE];
  socklen_t clilen;
  struct sockaddr_in cliaddr,servaddr;
  const int on=1;
  listenfd=Socket(AF_INET,SOCK_STREAM,0);
  if(setsockopt (listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))=-1);    err_ret(〃setsockopt error〃);
  bzero(&servaddr,sizeof(servaddr));
  servaddr.sin_family =AF_INET;
  servaddr.sin_addr.s_addr =htonl(INADDR_ANY);
  servaddr.sin_port =htons(SERV_PORT);
  Bind(listenfd,(SA*)&servaddr,sizeof(servaddr));
  Listen(listenfd,LISTENQ);
  maxfd =listenfd; //初始化
  maxi = -1; //最大下标
  for(i=0;i    client[i]=-1; //-1表示可用
  FD_ZERO(&allset);
  FD_SET(listenfd,&allset);
  for(;;){
   rset=allset; //结构赋值
    nready=Select(maxfd+1,&rset,NULL,NULL,NULL);
   if (FD_ISSET(listenfd,&rset)){ //有用户连接
    clilen=sizeof(cliaddr);
    connfd=Accept(listenfd,(SA*)&cliaddr,&clilen);
    for(i=0;i      if(client[i]< 0 {
       client[i]=connfd;//保存套接字
       break;
      }
    if(i=FD_SETSIZE);
     err_quit(〃too many clients〃);
    FD_SET (connfd,&allset); //增加套接字到读描述字集
    if(connfd>maxfd);
      maxfd=connfd;
    if(i>maxi);
      maxi=i;
    if(--nready<=0)
     continue;
  }
  for (i=0;i<=maxi;i++){ //检查所有用户连接
    if((sockfd=client[i])<0)
     continue;
    if(FD_ISSET(sockfd,&rset))
{
     if((n=Readline(sockfd,line,MAXLINE))=0)
{               //用户关闭连接
       Close(sockfd);
       FD_CLR(sockfd,&allset);
       client[i]=-1
      }else
        printf(〃%s〃,line);
       Broadcast(client,maxi,line,n);
       if (--nready<=0)
         break;
        }
      }
    }
  }
  //将聊天内容发送到所有已连接的用户
  Broadcast(int client,int maxi,char*str,size_t n)
  {
      int i;
      int sockfd;
      for(i=0;i       if((sockfd=client[i])<0)
       continue;
      writen(sockfd,str,n);
    }
  }
  2.客户端
为了实现非阻塞通信,利用异步选择函数 WSAAsynSelect() 将网络事件与 WinSock 消息联系起来,由该函数注册一些用户感兴趣的网络事件(如接收缓冲区满,允许发送数据,请求连接等)。当这些被注册的网络事件发生时,应用程序的相应函数 将接收到有关消息。
  应用程序在使用Windows Sockets DLL 之前必须先调用启动函数WSAStartup() ,该函数的功能有两点:一是由应用程序指定所要求Windows Sockets DLL 版本;二是获得系统 Windows Sockets DLL的一些技术细节。每一个WSAStartup()函数必须和一个WSACleanup()函数对应,当应用程序终止时,必须调用 WSACleanup()将自己从OLL 中注销。
  客户端程序用 VC++6.0在Windows98 操作系统下设计,程序主框架由 AppWizard 生成,客户端核心代码在 CTalkDialog类中。只有一个 socket 变量m_hSocket 与服务端进行连接。连接建立好后,通过此 SOCKET发送和接收信息。
  手工加入CTalkDialog::OnSockConnect() ,完成基本套接字编程,为客户程序申请一个套接字,并将该套接字与指定服务器绑定,然后向服务器发出连接请求,启动异步选择函数等待服务器的响应。
  void CTalkDialog:〃OnSockConnect()
{
   struct sockaddr_in servaddr;
   WSADATA wsaData;
   if(WSAStartup(WINSOCK_VERSION,&wsaData));
   {
    MessageBox(〃Could not load Windows Sockets DLL,〃,NULL,MB_Ok);
    return;
  }
m_hSocket=socket(AF_INET,SOCK_STREAM,0);
   memset(&servaddr,0,sizeof(servaddr));
   servaddr.sin_family=AF_INET;
   //m_iPort,m_csIP 为通过注册对话框返回的端口号和IP地址
  servaddr.sin_port=htons(m_iport);
  servaddr.sin_addr.S_un.S_addr=inet_addr(m_csIP);
  if (connect(m_hSocket,(SA*)& servaddr,sizeof (servaddr))!=0){
     AfxMessageBox(〃连接服务器失败!〃);
   GetDlgItem(IDC_BUTTON_OUT)->EnableWindow(FALSE);
     GetDlgItem(IDC_BUTTON_IN)->EnableWindowTRUE
     GetDlgItem(IDC_EDIT-SEND)->EnableWindow(FALSE);
     UpdateData(FALSE);
  }
  else{
     GetDlgItem(IDC_BUTTON_IN)->EnableWindow(FALSE);
   GetDlgItem(IDC_BUTTON_OUT)->EnableWindow(TRUE);
   GetDlgItem(IDC_EDIT-SEND)->EnableWindow(TRUE);
     int iErrorCode=WSAAsyncSelect(m_hSocket,m_hWnd,WM_SOCKET_READ,   FD_READ);
     if(iErrorCode=SOCKET_ERROR)
      MessageBox(〃WSAAsyncSelect failed on socket〃,NULL,MB_OK);
    }
  }  手工加入CTalkDialog::OnSockRead(), 响应WinSock 发来的消息
  LRESULT CTalkDialog::OnSockRead(WPARAM wParamLPARAM 1Param)
  {
    int iRead;
    int iBufferLength;
    int iEnd;
    int iSpaceRemain;
    char chIncomingData[100];
    iBufferLength=iSpaceRemain=sizeof (chIncomingData);
   iEnd=0;
   iSpaceRemain-=iEnd;
   iRead=recv(m_hSocket,(LPSTR)(chIncomingData+iEnd),iSpaceRemain,0);
   iEnd+=iRead;
   if (iRead=SOCKET_ERROR)
    AfxMessageBox(〃接收数据错误!〃);
   chIncomingData[iEnd]=‘\0’;
   if(lstrlen(chIncomingData)!=0)
{
  m_csRecv=m_csRecv+chIncomingData;
  GetDlgItem(IDC_EDIT_RECV)->SetWindowText((LPCSTR)m_csRecv);
  CEdit*pEdit;
  pEdit=(CEdit*)GetDlgItem(IDC_EDIT_RECV);
  int i=pEdit->GetLineCount();
     pEdit->LineScroll(i,0);
    }
    return(OL);
  }
  手工加入CTalkDialog::OnSend(),将指定缓冲区中的数据发送出去。
  void CTalkDialog::OnSend()
   {
    UpdateData();
    m_csSend.TrimLeft();
    m_csSend.TrimRight();
    if(!m_csSend.IsEmpty())
   {
m_csSend=m_csName+〃:〃+m_csSend+〃\r\n〃;
   int nCharSend=send(m_hSocket,m_csSend,m_csSend.GetLength(),0);
   if(nCharSend=SOCKET_ERROR)
    MessageBox(〃 发送数据错误!〃,NULL,MB_OK);
   }
   m_csSend=〃〃;
   UpdateData(FALSE);
   CWnd *pEdit=GetDlgItem(IDC_EDIT_SEND);
   pEdit->SetFocus();
   return;
  }
  通过ClassWizard 增加 virtual function PreTranslateMessage(), 控制当按回车键时调用OnSend(),而不是执行缺省按钮的动作。
  BOOL CTalkDialog::PreTranslateMessage(MSG*pMsg)
  {
if(pMsg-> message==WM_KEYDOWN && pMsg-> wParam=VK_RETURN){
     OnSend();
    }
    return CDialog::PreTranslateMessage(pMsg);
  }
为了简化设计,用户名在客户端控制,服务端只进行简单的接收信息和“广播”此信息,不进行名字校验,也就是说,可以有同名客户登录到服务端。这个程序设 计虽然简单,但是已经具备了聊天室的最基本的功能。服务端程序在SCO OpenServer 5.0.5 下编译通过,客户端程序在VC++6.0 下编译通过,在使用TCP/IP 协议的局域网上运行良好。
  上述实例仅用于说明通过 Socket 编程接口能方便地实现跨系统通讯,而这种跨系统通讯在金融行业中有着越来越广泛的应用,利用它可以实现各种多媒体查询,数据传输,网络通讯等功能。因此对跨系统通讯的探讨是非常必要和有意义的,希望上述探讨对大家能有所启发。

添加评论 13:17:52  |  固定链接 | 引用通告 (0) | 记录它 | 计算机与 Internet 固定链接 关闭 2006-2-14 WinSock网络编程实用宝典 http://blog.yesky.com/blog/xioxu/archive/2005/05/12/124233.html ? ? ? VC编程轻松获取局域网连接通知
摘要:
本文从解决实际需要出发,通过采用Windows Socket API等网络编程技术实现了在局域网共享一条电话线的情况下,当服务器拨号上网时能及时通知各客户端通过代理服务器进行上网。本文还特别给出了基于 Microsoft Visual C++ 6.0的部分关键实现代码。

  一、 问题提出的背景

  笔 者所使用的局域网拥有一个服务器及若干分布于各办公室的客户机,通过网卡相连。服务器不提供专线上网,但可以拨号上网,而各客户机可以通过装在服务器端的 代理服务器共用一条电话线上网,但前提必须是服务器已经拨号连接。考虑到经济原因,服务器不可能长时间连在网上,因此经常出现由于分布于各办公室的客户机 不能知道服务器是否处于连线状态而造成的想上网时服务器没有拨号,或是服务器已经拨号而客户机却并不知晓的情况,这无疑会在工作中带来极大的不便。而笔者 作为一名程序设计人员,有必要利用自己的专业优势来解决实际工作中所遇到的一些问题。通过对实际情况的分析,可以归纳为一点:当服务器在进行拨号连接时能 及时通知在网络上的各个客户机,而各客户机在收到服务器发来的消息后可以根据自己的情况来决定是否上网。这样就可以在同一时间内同时为较多的客户机提供上 网服务,此举不仅提高了利用效率也大大节省了上网话费。

  二、 程序主要设计思路及实现

  由于本网络 是通过网卡连接的局域网,因此可以首选Windows Socket API进行套接字编程。整个系统分为两部分:服务端和客户端。服务端运行于服务器上负责监视服务器是否在进行拨号连接,一旦发现马上通过网络发送消息通知 客户端;而客户端软件则只需完成同服务端软件的连接并能接收到从服务端发送来的通知消息即可。服务器端要完成比客户端更为繁重的任务。下面对这几部分的实 现分别加以描述:

  (一)监视拨号连接事件的发生

  在采用拨号上网时,首先需要通过拨号连接通过电话线连接到ISP 上,然后才能享受到ISP所提供的各种互联网服务。而要捕获拨号连接发生的事件不能依赖于消息通知,因为此时发出的消息同一个对话框出现在屏幕上时所产生 的消息是一样的。唯一同其他对话框区别的是其标题是固定的"拨号连接",因此在无其他特殊情况下(如其他程序的标题也是"拨号连接"时)可以认定当桌面上 的所有程序窗口出现以"拨号连接" 为标题的窗口时,即可认定此时正在进行拨号连接。因此可以通过搜寻并判断窗口标题的办法对拨号连接进行监视,具体可以用CWnd类的 FindWindows()函数来实现:

CWnd *pWnd=CWnd::FindWindow(NULL,"拨号连接");
  第一个参数为NULL,指定对当前所有窗口都进行搜索。第二个参数就是待搜寻的窗口标题,一旦找到将返回该窗口的窗口句柄。因此可 以在窗口句柄不为空的情况下去通知客户端服务器现在正在拨号。由于一般的拨号连接都需要一段时间的连接应答后才能登录到ISP上,因此从提高程序运行效率 角度出发可以通过定时器的使用来每间隔一段时间(如500毫秒)去搜寻一次,以确保能监视到每一次的拨号连接而又不致过分加重CPU的负担。

(二)服务器端网络通讯功能的实现

在此采用的是可靠的有连接的流式套接字,并且采用了多线程和异步通知机制能有效避免一些函数如accept()等的阻塞会引起整个程序的阻塞。由于套接 字编程方面的书籍资料非常丰富,对其进行网络编程做了很详细的描述,故本文在此只针对一些关键部分做简要说明,有关套接字网络编程的详细内容请参阅相关资 料。采用流式套接字的服务器端的主要设计流程可以归结为以下几步:

  1. 创建套接字

sock=socket(AF_INET,SOCK_STREAM,0);
  该函数的第一个参数用于指定地址族,在Windows下仅支持AF_INET(TCP/IP地址);第二个参数用于描述套接字的类 型,对于流式套接字提供有SOCK_STREAM;最后一个参数指定套接字使用的协议,一般为0。该函数的返回值保存了新套接字的句柄,在程序退出前可以 用closesocket()函数来将其释放。

  2. 绑定套接字

  服务器方一旦获取了一个新的套接字后应通过 bind()将该套接字与本机上的一个端口相关联。此时需要预先对一个指向包含有本机IP地址和端口信息的sockaddr_in结构填充一些必要的信 息,如本地端口号和本地主机地址等。然后就可经过bind()将服务器进程在网络上标识出来。需要注意的是由于1024以内的埠号都是保留的端口号因此如 无特别需要一般不能将sockin.sin_port的端口号设置为1024以内的值:

……
sockin.sin_family=AF_INET;
sockin.sin_addr.s_addr=0;
sockin.sin_port=htons(USERPORT);
bind(sock,(LPSOCKADDR)&sockin,sizeof(sockin));
……
  3. 侦听套接字

listen(sock,1);
  4. 等待客户机的连接

  这里需要通过accept()调用等待接收客户端的连接以完成连接的建立,由于该函数 在没有客户端进行申请连接之前会处于阻塞状态,因此如果采取通常的单线程模式会导致整个程序一直处于阻塞状态而不能响应其他的外界消息,因此为该部分代码 单独开辟一个线程,这样阻塞将被限制在该线程内而不会影响到程序整体。

AfxBeginThread(Server,NULL);//创建一个新的线程
……
UINT Server(LPVOID lpVoid)//线程的处理函数
{
//获取当前视类的指针,以确保访问的是当前的实例对象。
CNetServerView* pView=((CNetServerView*)(
(CFrameWnd*)AfxGetApp()->m_pMainWnd)->GetActiveView());
while(pView->nNumConns<1)//当前的连接者个数
{
int nLen=sizeof(SOCKADDR);
pView->newskt= accept(pView->sock,
(LPSOCKADDR)& pView->sockin,(LPINT)& nLen);
WSAAsyncSelect(pView->newskt,
pView->m_hWnd,WM_SOCKET_MSG,FD_CLOSE);
pView->nNumConns++;
}
return 1;
}
  这里在accept ()后使用了WSAAsyncSelect()异步选择函数。对于网络事件的响应最好采取异步选择机制,只有采取这种方式才可以在由网络对方所引起的不可 预知的网络事件发生时能马上在进程中做出及时的响应处理,而在没有网络事件到达时则可以处理其他事件,这种效率是很高的,而且完全符合Windows所标 榜的消息触发原则。WSAAsyncSelect()函数便是实现网络事件异步选择的核心函数。通过第四个参数FD_CLOSE注册了应用程序感兴取的网 络事件是网络断开,当客户方端开连接时该事件会被检测到,同时会发出由第三个参数指定的自定义消息WM_SOCKET_MSG。

  5. 发送/接收

  当客户机同服务器建立好连接后就可以通过send()/recv()函数进行发送和接收数据了,对于本程序只需在监测到有拨号连接事件发生时向客户机发送通知消息即可:

char buffer[1]={‘a‘};
send(newskt,buffer,1,0);//向客户机发送字符a,表示现在服务器正在拨号。
  6. 关闭套接字

  在全部通讯完成之后,在退出程序之前需要调用closesocket();函数把创建的套接字关闭。

  (三)客户机端的程序设计

  客户机的编程要相对简单许多,全部通讯过程只需以下四步:

  1. 创建套接字
  2. 建立连接
  3. 发送/接收
  4. 关闭套接字

具体实现过程同服务器编程基本类似,只是由于需要接收数据,因此待监测的网络事件为FD_CLOSE和FD_READ,在消息响应函数中可以通过对消息 参数的低位字节进行判断而区分出具体发生是何种网络事件,并对其做出响应的反应。下面结合部分主要实现代码对实现过程进行解释:

……
m_ServIP=SERVERIP; //指定服务器的IP地址
m_Port=htons(USERPORT); //指定服务器的端口号
if((IPaddr=inet_addr(m_ServIP))==INADDR_NONE) //转换成网络地址
return FALSE;
else
{
sock=socket(AF_INET,SOCK_STREAM,0); //创建套接字
sockin.sin_family=AF_INET; //填充结构
sockin.sin_addr.S_un.S_addr=IPaddr;
sockin.sin_port=m_Port;
connect(sock,(LPSOCKADDR)&sockin,sizeof(sockin)); //建立连接
//设定异步选择事件
WSAAsyncSelect(sock,m_hWnd,WM_SOCKET_MSG,FD_CLOSE|FD_READ);
//在这里可以通过震铃、弹出对话框等方式通知客户已经连上服务器
}
……

//网络事件的消息处理函数
int message=lParam & 0x0000FFFF;//取消息参数的低位
switch(message) //判断发生的是何种网络事件
{
case FD_READ: //读事件
AfxBeginThread(Read,NULL);
break;
case FD_CLOSE: //服务器关闭事件
……
break;
}

  在读事件的消息处理过程中,单独为读处理过程开辟了一个线程,在该线程中接收从服务器发送过来的信息,并通过震铃、弹出对话框等方式通知客户端现在服务器正在拨号:

……
int a=recv(pView->sock,cDataBuffer,1,0); //接收从服务器发送来的消息
if(a>0)
AfxMessageBox("拨号连接已启动!"); //通知用户
……

三、必要的完善

  前面只是介绍了程序设计的整体框架和设计思路,仅仅是一个雏形,有许多重要的细节没有完善,不能用于实际使用。下面就对一些完全必要的细节做适当的完善:

  (一) 界面的隐藏

由于本程序系自动检测、自动通知,完全不需要人工干预,因此可以将其视为后台运行的服务程序,因此程序主界面现在已无存在的必要,可以在应用程序类的初 始化实例函数InitInstance()中将ShowWindow();的参数SW_SHOW改成SW_HIDE即可。当需要有对话框弹出通知用户时仅 对话框出现,主界面仍隐藏,因此是完全可行的。

  (二) 自启动的实现

  由于服务端软件需要时刻监视有无进行拨号连接,所以必须具缸云舳奶匦浴6突Ф巳砑捎诮邮障⒑屯ㄖ突Ф伎梢宰远瓿桑虼巳绻芫弑缸云舳匦栽蚩梢酝耆牙胗没У母稍ざ〉媒细叩淖远潭取I柚米云舳奶匦裕梢源右韵录父鐾揪都右钥悸牵?BR>
  1. 在"启动"菜单上添加指向程序的快捷方式。
  
  2. 在Autoexec.bat中添加启动程序的命令行。

  3. 在Win.ini中的[windows]节的run项目后添加程序路径。

  4. 修改注册表,添加键值的具体路径为:

"HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run"

  并将添加的键值修改为程序的存放路径即可。以上几种方法既可以手工添加,也可以通过编程使之自动完成。

  (三) 自动续联

  对于服务/客户模式的网络通讯程序普遍要求服务端要先于客户端运行,而本系统的客户、服务端均为自启动,不能保证服务器先于客户机启动,而且本系统要求只要客户机和服务器连接在网络上就要不间断保持连接,因此需要使客户和服务端都要具备自动续联的功能。

对于服务器端,当客户端断开时,需要关闭当前的套接字,并重新启动一个新的套接字以等待客户机的再次连接。这可以放在FD_CLOSE事件对应的消息 WM_SOCKET_MSG的消息响应函数中来完成。而对于客户端,如果先于服务器而启动,则connect()函数将返回失败,因此可以在程序启动时用 SetTimer()设置一个定时器,每隔一段时间(10秒)就试图连接服务器一次,当connect()函数返回成功即服务器已启动并与之连接上之后可 以用KillTimer()函数将定时器关闭。另外当服务器关闭时需要再次开启定时器,以确保当服务器再次运行时能与之建立连接,可以通过响应 FD_CLOSE事件来捕获该事件的发生。

  小结:本文通过Windows Sockets API实现了基于TCP/IP协议的面向连接的流式套接字的网络通讯程序的设计,通过网络通讯程序的支持可以把服务器捕获到的拨号连接发生的事件及时通知 给客户端,最后通过对一些必要的细节的完善很好解决了在局域网上能及时得到服务器拨号连接的消息通知。本文所述程序在Windows 98 SE下,由Microsoft Visual C++ 6.0编译通过;使用的代理服务器软件为WinGate 4.3.0;上网方式为拨号上网。

VC++编程实现网络嗅探器

引言

  从事网络安全的技术人员和相当一部分准黑客(指那些使用现成的黑客软件进行攻击而不是根据需要去自己编写代码的人)都一定不会对网络嗅探器 (sniffer)感到陌生,网络嗅探器无论是在网络安全还是在黑客攻击方面均扮演了很重要的角色。通过使用网络嗅探器可以把网卡设置于混杂模式,并可实 现对网络上传输的数据包的捕获与分析。此分析结果可供网络安全分析之用,但如为黑客所利用也可以为其发动进一步的攻击提供有价值的信息。可见,嗅探器实际 是一把双刃剑。 虽然网络嗅探器技术被黑客利用后会对网络安全构成一定的威胁,但嗅探器本身的危害并不是很大,主要是用来为其他黑客软件提供网络情报,真正的攻击主要是由 其他黑软来完成的。而在网络安全方面,网络嗅探手段可以有效地探测在网络上传输的数据包信息,通过对这些信息的分析利用是有助于网络安全维护的。权衡利 弊,有必要对网络嗅探器的实现原理进行介绍。

  嗅探器设计原理

  嗅探器作为一种网络通讯程序,也是通过对网卡的编程来实现网络通讯的,对网卡的编程也是使用通常的套接字(socket)方式来进行。但是,通 常的套接字程序只能响应与自己硬件地址相匹配的或是以广播形式发出的数据帧,对于其他形式的数据帧比如已到达网络接口但却不是发给此地址的数据帧,网络接 口在验证投递地址并非自身地址之后将不引起响应,也就是说应用程序无法收取到达的数据包。而网络嗅探器的目的恰恰在于从网卡接收所有经过它的数据包,这些 数据包即可以是发给它的也可以是发往别处的。显然,要达到此目的就不能再让网卡按通常的正常模式工作,而必须将其设置为混杂模式。

  具体到编程实现上,这种对网卡混杂模式的设置是通过原始套接字(raw socket)来实现的,这也有别于通常经常使用的数据流套接字和数据报套接字。在创建了原始套接字后,需要通过setsockopt()函数来设置IP 头操作选项,然后再通过bind()函数将原始套接字绑定到本地网卡。为了让原始套接字能接受所有的数据,还需要通过ioctlsocket()来进行设 置,而且还可以指定是否亲自处理IP头。至此,实际就可以开始对网络数据包进行嗅探了,对数据包的获取仍象流式套接字或数据报套接字那样通过recv() 函数来完成。但是与其他两种套接字不同的是,原始套接字此时捕获到的数据包并不仅仅是单纯的数据信息,而是包含有 IP头、 TCP头等信息头的最原始的数据信息,这些信息保留了它在网络传输时的原貌。通过对这些在低层传输的原始信息的分析可以得到有关网络的一些信息。由于这些 数据经过了网络层和传输层的打包,因此需要根据其附加的帧头对数据包进行分析。下面先给出结构.数据包的总体结构:

数据包 IP头 TCP头(或其他信息头) 数据

  数据在从应用层到达传输层时,将添加TCP数据段头,或是UDP数据段头。其中UDP数据段头比较简单,由一个8字节的头和数据部分组成,具体格式如下:

16位 16位 源端口 目的端口 UDP长度 UDP校验和

  而TCP数据头则比较复杂,以20个固定字节开始,在固定头后面还可以有一些长度不固定的可选项,下面给出TCP数据段头的格式组成:

16位 16位 源端口 目的端口 顺序号 确认号 TCP头长 (保留)7位 URG ACK PSH RST SYN FIN 窗口大小 校验和 紧急指针 可选项(0或更多的32位字) 数据(可选项)

  对于此TCP数据段头的分析在编程实现中可通过数据结构_TCP来定义:

typedef struct _TCP{ WORD SrcPort; // 源端口
WORD DstPort; // 目的端口
DWORD SeqNum; // 顺序号
DWORD AckNum; // 确认号
BYTE DataOff; // TCP头长
BYTE Flags; // 标志(URG、ACK等)
WORD Window; // 窗口大小
WORD Chksum; // 校验和
WORD UrgPtr; // 紧急指针
} TCP;
typedef TCP *LPTCP;
typedef TCP UNALIGNED * ULPTCP;

  在网络层,还要给TCP数据包添加一个IP数据段头以组成IP数据报。IP数据头以大端点机次序传送,从左到右,版本字段的高位字节先传输 (SPARC是大端点机;Pentium是小端点机)。如果是小端点机,就要在发送和接收时先行转换然后才能进行传输。IP数据段头格式如下:

16位 16位 版本 IHL 服务类型 总长 标识 标志 分段偏移 生命期 协议 头校验和 源地址 目的地址 选项(0或更多)

  同样,在实际编程中也需要通过一个数据结构来表示此IP数据段头,下面给出此数据结构的定义:

typedef struct _IP{
union{ BYTE Version; // 版本
BYTE HdrLen; // IHL
};
BYTE ServiceType; // 服务类型
WORD TotalLen; // 总长
WORD ID; // 标识
union{ WORD Flags; // 标志
WORD FragOff; // 分段偏移
};
BYTE TimeToLive; // 生命期
BYTE Protocol; // 协议
WORD HdrChksum; // 头校验和
DWORD SrcAddr; // 源地址
DWORD DstAddr; // 目的地址
BYTE Options; // 选项
} IP;
typedef IP * LPIP;
typedef IP UNALIGNED * ULPIP;

  在明确了以上几个数据段头的组成结构后,就可以对捕获到的数据包进行分析了。


嗅探器的具体实现

 

  根据前面的设计思路,不难写出网络嗅探器的实现代码,下面就给出一个简单的示例,该示例可以捕获到所有经过本地网卡的数据包,并可从中分析出协 议、IP源地址、IP目标地址、TCP源端口号、TCP目标端口号以及数据包长度等信息。由于前面已经将程序的设计流程讲述的比较清楚了,因此这里就不在 赘述了,下面就结合注释对程序的具体是实现进行讲解,同时为程序流程的清晰起见,去掉了错误检查等保护性代码。主要代码实现清单为:

// 检查 Winsock 版本号,WSAData为WSADATA结构对象
WSAStartup(MAKEWORD(2, 2), &WSAData);
// 创建原始套接字
sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW));
// 设置IP头操作选项,其中flag 设置为ture,亲自对IP头进行处理
setsockopt(sock, IPPROTO_IP, IP_HDRINCL, (char*)&flag, sizeof(flag));
// 获取本机名
gethostname((char*)LocalName, sizeof(LocalName)-1);
// 获取本地 IP 地址
pHost = gethostbyname((char*)LocalName));
// 填充SOCKADDR_IN结构
addr_in.sin_addr = *(in_addr *)pHost->h_addr_list[0]; //IP
addr_in.sin_family = AF_INET;
addr_in.sin_port = htons(57274);
// 把原始套接字sock 绑定到本地网卡地址上
bind(sock, (PSOCKADDR)&addr_in, sizeof(addr_in));
// dwValue为输入输出参数,为1时执行,0时取消
DWORD dwValue = 1;
// 设置 SOCK_RAW 为SIO_RCVALL,以便接收所有的IP包。其中SIO_RCVALL
// 的定义为: #define SIO_RCVALL _WSAIOW(IOC_VENDOR,1)
ioctlsocket(sock, SIO_RCVALL, &dwValue);

  前面的工作基本上都是对原始套接字进行设置,在将原始套接字设置完毕,使其能按预期目的工作时,就可以通过recv()函数从网卡接收数据了, 接收到的原始数据包存放在缓存RecvBuf[]中,缓冲区长度BUFFER_SIZE定义为65535。然后就可以根据前面对IP数据段头、TCP数据 段头的结构描述而对捕获的数据包进行分析:

while (true)
{
// 接收原始数据包信息
int ret = recv(sock, RecvBuf, BUFFER_SIZE, 0);
if (ret > 0)
{
// 对数据包进行分析,并输出分析结果
ip = *(IP*)RecvBuf;
tcp = *(TCP*)(RecvBuf + ip.HdrLen);
TRACE("协议: %s\r\n",GetProtocolTxt(ip.Protocol));
TRACE("IP源地址: %s\r\n",inet_ntoa(*(in_addr*)&ip.SrcAddr));
TRACE("IP目标地址: %s\r\n",inet_ntoa(*(in_addr*)&ip.DstAddr));
TRACE("TCP源端口号: %d\r\n",tcp.SrcPort);
TRACE("TCP目标端口号:%d\r\n",tcp.DstPort);
TRACE("数据包长度: %d\r\n\r\n\r\n",ntohs(ip.TotalLen));
}
}

  其中,在进行协议分析时,使用了GetProtocolTxt()函数,该函数负责将IP包中的协议(数字标识的)转化为文字输出,该函数实现如下:

#define PROTOCOL_STRING_ICMP_TXT "ICMP"
#define PROTOCOL_STRING_TCP_TXT "TCP"
#define PROTOCOL_STRING_UDP_TXT "UDP"
#define PROTOCOL_STRING_SPX_TXT "SPX"
#define PROTOCOL_STRING_NCP_TXT "NCP"
#define PROTOCOL_STRING_UNKNOW_TXT "UNKNOW"
……
CString CSnifferDlg::GetProtocolTxt(int Protocol)
{
switch (Protocol){
case IPPROTO_ICMP : //1 /* control message protocol */
return PROTOCOL_STRING_ICMP_TXT;
case IPPROTO_TCP : //6 /* tcp */
return PROTOCOL_STRING_TCP_TXT;
case IPPROTO_UDP : //17 /* user datagram protocol */
return PROTOCOL_STRING_UDP_TXT;
default:
return PROTOCOL_STRING_UNKNOW_TXT;
}

  最后,为了使程序能成功编译,需要包含头文件winsock2.h和ws2tcpip.h。在本示例中将分析结果用TRACE()宏进行输出,在调试状态下运行,得到的一个分析结果如下:

协议: UDP
IP源地址: 172.168.1.5
IP目标地址: 172.168.1.255
TCP源端口号: 16707
TCP目标端口号:19522
数据包长度: 78
……
协议: TCP
IP源地址: 172.168.1.17
IP目标地址: 172.168.1.1
TCP源端口号: 19714
TCP目标端口号:10
数据包长度: 200
……

  从分析结果可以看出,此程序完全具备了嗅探器的数据捕获以及对数据包的分析等基本功能。

  小结

  本文介绍的以原始套接字方式对网络数据进行捕获的方法实现起来比较简单,尤其是不需要编写VxD虚拟设备驱动程序就可以实现抓包,使得其编写过 程变的非常简便,但由于捕获到的数据包头不包含有帧信息,因此不能接收到与 IP 同属网络层的其它数据包, 如 ARP数据包、RARP数据包等。在前面给出的示例程序中考虑到安全因素,没有对数据包做进一步的分析,而是仅仅给出了对一般信息的分析方法。通过本文的 介绍,可对原始套接字的使用方法以及TCP/IP协议结构原理等知识有一个基本的认识。本文所述代码在Windows 2000下由Microsoft Visual C++ 6.0编译调试通过。

添加评论 16:16:28  |  固定链接 | 引用通告 (0) | 记录它 | 计算机与 Internet 固定链接 关闭 WinSock网络编程实用宝典 一、TCP/IP 体系结构与特点

  1、TCP/IP体系结构

  TCP/IP协议实际上就是在物理网上的一组完整的网络协议。其中TCP是提供传输层服务,而IP则是提供网络层服务。TCP/IP包括以下协议:(结构如图1.1)


(图1.1)

  IP: 网间协议(Internet Protocol) 负责主机间数据的路由和网络上数据的存储。同时为ICMP,TCP,   UDP提供分组发送服务。用户进程通常不需要涉及这一层。

  ARP: 地址解析协议(Address Resolution Protocol)
   此协议将网络地址映射到硬件地址。

  RARP: 反向地址解析协议(Reverse Address Resolution Protocol)
   此协议将硬件地址映射到网络地址

  ICMP: 网间报文控制协议(Internet Control Message Protocol)
   此协议处理信关和主机的差错和传送控制。

  TCP: 传送控制协议(Transmission Control Protocol)
   这是一种提供给用户进程的可靠的全双工字节流面向连接的协议。它要为用户进程提供虚电路服务,并为数据可靠传输建立检查。(注:大多数网络用户程序使用TCP)

  UDP: 用户数据报协议(User Datagram Protocol)
   这是提供给用户进程的无连接协议,用于传送数据而不执行正确性检查。

  FTP: 文件传输协议(File Transfer Protocol)
   允许用户以文件操作的方式(文件的增、删、改、查、传送等)与另一主机相互通信。

  SMTP: 简单邮件传送协议(Simple Mail Transfer Protocol)
   SMTP协议为系统之间传送电子邮件。

  TELNET:终端协议(Telnet Terminal Procotol)
   允许用户以虚终端方式访问远程主机

  HTTP: 超文本传输协议(Hypertext Transfer Procotol)
  
  TFTP: 简单文件传输协议(Trivial File Transfer Protocol)

  2、TCP/IP特点

  TCP/IP协议的核心部分是传输层协议(TCP、UDP),网络层协议(IP)和物理接口层,这三 层通常是在操作系统内核中实现。因此用户一般不涉及。编程时,编程界面有两种形式:一、是由内核心直接提供的系统调用;二、使用以库函数方式提供的各种函 数。前者为核内实现,后者为核外实现。用户服务要通过核外的应用程序才能实现,所以要使用套接字(socket)来实现。

  图1.2是TCP/IP协议核心与应用程序关系图。


(图1.2)

  二、专用术语

  1、套接字

  套接字是网络的基本构件。它是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连听进程。套接字存在通信区域(通信区 域又称地址簇)中。套接字只与同一区域中的套接字交换数据(跨区域时,需要执行某和转换进程才能实现)。WINDOWS 中的套接字只支持一个域——网际域。套接字具有类型。

  WINDOWS SOCKET 1.1 版本支持两种套接字:流套接字(SOCK_STREAM)和数据报套接字(SOCK_DGRAM)

  2、WINDOWS SOCKETS 实现

  一个WINDOWS SOCKETS 实现是指实现了WINDOWS SOCKETS规范所描述的全部功能的一套软件。一般通过DLL文件来实现

  3、阻塞处理例程

  阻塞处理例程(blocking hook,阻塞钩子)是WINDOWS SOCKETS实现为了支持阻塞套接字函数调用而提供的一种机制。

  4、多址广播(multicast,多点传送或组播)

  是一种一对多的传输方式,传输发起者通过一次传输就将信息传送到一组接收者,与单点传送
(unicast)和广播(Broadcast)相对应。

一、客户机/服务器模式

  在TCP/IP网络中两个进程间的相互作用的主机模式是客户机/服务器模式(Client/Server model)。该模式的建立基于以下两点:1、非对等作用;2、通信完全是异步的。客户机/服务器模式在操作过程中采取的是主动请示方式:

  首先服务器方要先启动,并根据请示提供相应服务:(过程如下)

  1、打开一通信通道并告知本地主机,它愿意在某一个公认地址上接收客户请求。

  2、等待客户请求到达该端口。

  3、接收到重复服务请求,处理该请求并发送应答信号。

  4、返回第二步,等待另一客户请求

  5、关闭服务器。

  客户方:

  1、打开一通信通道,并连接到服务器所在主机的特定端口。

  2、向服务器发送服务请求报文,等待并接收应答;继续提出请求……

  3、请求结束后关闭通信通道并终止。

  二、基本套接字

  为了更好说明套接字编程原理,给出几个基本的套接字,在以后的篇幅中会给出更详细的使用说明。

  1、创建套接字——socket()

  功能:使用前创建一个新的套接字

  格式:SOCKET PASCAL FAR socket(int af,int type,int procotol);

  参数:af: 通信发生的区域

  type: 要建立的套接字类型

  procotol: 使用的特定协议

  2、指定本地地址——bind()

  功能:将套接字地址与所创建的套接字号联系起来。

  格式:int PASCAL FAR bind(SOCKET s,const struct sockaddr FAR * name,int namelen);

  参数:s: 是由socket()调用返回的并且未作连接的套接字描述符(套接字号)。

  其它:没有错误,bind()返回0,否则SOCKET_ERROR

  地址结构说明:

struct sockaddr_in
{
short sin_family;//AF_INET
u_short sin_port;//16位端口号,网络字节顺序
struct in_addr sin_addr;//32位IP地址,网络字节顺序
char sin_zero[8];//保留
}

  3、建立套接字连接——connect()和accept()

  功能:共同完成连接工作

  格式:int PASCAL FAR connect(SOCKET s,const struct sockaddr FAR * name,int namelen);

  SOCKET PASCAL FAR accept(SOCKET s,struct sockaddr FAR * name,int FAR * addrlen);

  参数:同上

  4、监听连接——listen()

  功能:用于面向连接服务器,表明它愿意接收连接。

  格式:int PASCAL FAR listen(SOCKET s, int backlog);

  5、数据传输——send()与recv()

  功能:数据的发送与接收

  格式:int PASCAL FAR send(SOCKET s,const char FAR * buf,int len,int flags);

  int PASCAL FAR recv(SOCKET s,const char FAR * buf,int len,int flags);

  参数:buf:指向存有传输数据的缓冲区的指针。

  6、多路复用——select()

  功能:用来检测一个或多个套接字状态。

  格式:int PASCAL FAR select(int nfds,fd_set FAR * readfds,fd_set FAR * writefds,
fd_set FAR * exceptfds,const struct timeval FAR * timeout);

  参数:readfds:指向要做读检测的指针

     writefds:指向要做写检测的指针

     exceptfds:指向要检测是否出错的指针

     timeout:最大等待时间

  7、关闭套接字——closesocket()

  功能:关闭套接字s

  格式:BOOL PASCAL FAR closesocket(SOCKET s);

三、典型过程图

  2.1 面向连接的套接字的系统调用时序图

 



  2.2 无连接协议的套接字调用时序图



?  2.3 面向连接的应用程序流程图


Windows Socket1.1 程序设计

一、简介

  Windows Sockets 是从 Berkeley Sockets 扩展而来的,其在继承 Berkeley Sockets 的基础上,又进行了新的扩充。这些扩充主要是提供了一些异步函数,并增加了符合WINDOWS消息驱动特性的网络事件异步选择机制。

  Windows Sockets由两部分组成:开发组件和运行组件。

  开发组件:Windows Sockets 实现文档、应用程序接口(API)引入库和一些头文件。

  运行组件:Windows Sockets 应用程序接口的动态链接库(WINSOCK.DLL)。

  二、主要扩充说明

  1、异步选择机制:

  Windows Sockets 的异步选择函数提供了消息机制的网络事件选择,当使用它登记网络事件发生时,应用程序相应窗口函数将收到一个消息,消息中指示了发生的网络事件,以及与事件相关的一些信息。

  Windows Sockets 提供了一个异步选择函数 WSAAsyncSelect(),用它来注册应用程序感兴趣的网络事件,当这些事件发生时,应用程序相应的窗口函数将收到一个消息。

  函数结构如下:

int PASCAL FAR WSAAsyncSelect(SOCKET s,HWND hWnd,unsigned int wMsg,long lEvent);

  参数说明:

   hWnd:窗口句柄

   wMsg:需要发送的消息

   lEvent:事件(以下为事件的内容)

值: 含义: FD_READ 期望在套接字上收到数据(即读准备好)时接到通知 FD_WRITE 期望在套接字上可发送数据(即写准备好)时接到通知 FD_OOB 期望在套接字上有带外数据到达时接到通知 FD_ACCEPT 期望在套接字上有外来连接时接到通知 FD_CONNECT 期望在套接字连接建立完成时接到通知 FD_CLOSE 期望在套接字关闭时接到通知

  例如:我们要在套接字读准备好或写准备好时接到通知,语句如下:

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

  如果我们需要注销对套接字网络事件的消息发送,只要将 lEvent 设置为0

  2、异步请求函数

  在 Berkeley Sockets 中请求服务是阻塞的,WINDOWS SICKETS 除了支持这一类函数外,还增加了相应的异步请求函数(WSAAsyncGetXByY();)。

  3、阻塞处理方法

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

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

  在Windows Sockets 实现中,对于不能立即完成的阻塞操作做如下处理:DLL初始化→循环操作。在循环中,它发送任何 WINDOWS 消息,并检查这个 Windows Sockets 调用是否完成,在必要时,它可以放弃CPU让其它应用程序执行(当然使用超线程的CPU就不会有这个麻烦了^_^)。我们可以调用 WSACancelBlockingCall() 函数取消此阻塞操作。

  在 Windows Sockets 中,有一个默认的阻塞处理例程 BlockingHook() 简单地获取并发送 WINDOWS 消息。如果要对复杂程序进行处理,Windows Sockets 中还有 WSASetBlockingHook() 提供用户安装自己的阻塞处理例程能力;与该函数相对应的则是 SWAUnhookBlockingHook(),它用于删除先前安装的任何阻塞处理例程,并重新安装默认的处理例程。请注意,设计自己的阻塞处理例程 时,除了函数 WSACancelBlockingHook() 之外,它不能使用其它的 Windows Sockets API 函数。在处理例程中调用 WSACancelBlockingHook()函数将取消处于阻塞的操作,它将结束阻塞循环。

  4、出错处理

  Windows Sockets 为了和以后多线程环境(WINDOWS/UNIX)兼容,它提供了两个出错处理函数来获取和设置当前线程的最近错误号。(WSAGetLastEror()和WSASetLastError())

  5、启动与终止

  使用函数 WSAStartup() 和 WSACleanup() 启动和终止套接字。


三、Windows Sockets网络程序设计核心

 

  我们终于可以开始真正的 Windows Sockets 网络程序设计了。不过我们还是先看一看每个 Windows Sockets 网络程序都要涉及的内容。让我们一步步慢慢走。

  1、启动与终止

  在所有 Windows Sockets 函数中,只有启动函数 WSAStartup() 和终止函数 WSACleanup() 是必须使用的。

  启动函数必须是第一个使用的函数,而且它允许指定 Windows Sockets API 的版本,并获得 SOCKETS的特定的一些技术细节。本结构如下:

int PASCAL FAR WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);

  其中 wVersionRequested 保证 SOCKETS 可正常运行的 DLL 版本,如果不支持,则返回错误信息。
我们看一下下面这段代码,看一下如何进行 WSAStartup() 的调用

WORD wVersionRequested;// 定义版本信息变量
WSADATA wsaData;//定义数据信息变量
int err;//定义错误号变量
wVersionRequested = MAKEWORD(1,1);//给版本信息赋值
err = WSAStartup(wVersionRequested, &wsaData);//给错误信息赋值
if(err!=0)
{
return;//告诉用户找不到合适的版本
}
//确认 Windows Sockets DLL 支持 1.1 版本
//DLL 版本可以高于 1.1
//系统返回的版本号始终是最低要求的 1.1,即应用程序与DLL 中可支持的最低版本号
if(LOBYTE(wsaData.wVersion)!= 1|| HIBYTE(wsaData.wVersion)!=1)
{
WSACleanup();//告诉用户找不到合适的版本
return;
}
//Windows Sockets DLL 被进程接受,可以进入下一步操作

  关闭函数使用时,任何打开并已连接的 SOCK_STREAM 套接字被复位,但那些已由 closesocket() 函数关闭的但仍有未发送数据的套接字不受影响,未发送的数据仍将被发送。程序运行时可能会多次调用 WSAStartuo() 函数,但必须保证每次调用时的 wVersionRequested 的值是相同的。

  2、异步请求服务

  Windows Sockets 除支持 Berkeley Sockets 中同步请求,还增加了了一类异步请求服务函数 WSAAsyncGerXByY()。该函数是阻塞请求函数的异步版本。应用程序调用它时,由 Windows Sockets DLL 初始化这一操作并返回调用者,此函数返回一个异步句柄,用来标识这个操作。当结果存储在调用者提供的缓冲区,并且发送一个消息到应用程序相应窗口。常用结 构如下:

HANDLE taskHnd;
char hostname="rs6000";
taskHnd = WSAAsyncBetHostByName(hWnd,wMsg,hostname,buf,buflen);

  需要注意的是,由于 Windows 的内存对像可以设置为可移动和可丢弃,因此在操作内存对象是,必须保证 WIindows Sockets DLL 对象是可用的。

  3、异步数据传输

使用 send() 或 sendto() 函数来发送数据,使用 recv() 或recvfrom() 来接收数据。Windows Sockets 不鼓励用户使用阻塞方式传输数据,因为那样可能会阻塞整个 Windows 环境。下面我们看一个异步数据传输实例:

  假设套接字 s 在连接建立后,已经使用了函数 WSAAsyncSelect() 在其上注册了网络事件 FD_READ 和 FD_WRITE,并且 wMsg 值为 UM_SOCK,那么我们可以在 Windows 消息循环中增加如下的分支语句:

case UM_SOCK:
switch(lParam)
{
case FD_READ:
len = recv(wParam,lpBuffer,length,0);
break;
case FD_WRITE:
while(send(wParam,lpBuffer,len,0)!=SOCKET_ERROR)
break;
}
break;

  4、出错处理

  Windows 提供了一个函数来获取最近的错误码 WSAGetLastError(),推荐的编写方式如下:

len = send (s,lpBuffer,len,0);
of((len==SOCKET_ERROR)&&(WSAGetLastError()==WSAWOULDBLOCK)){...}


基于Visual C++的Winsock API研究
为了方便网 络编程,90年代初,由Microsoft联合了其他几家公司共同制定了一套WINDOWS下的网络编程接口,即Windows Sockets规范,它不是一种网络协议,而是一套开放的、支持多种协议的Windows下的网络编程接口。现在的Winsock已经基本上实现了与协议 无关,你可以使用Winsock来调用多种协议的功能,但较常使用的是TCP/IP协议。Socket实际在计算机中提供了一个通信端口,可以通过这个端 口与任何一个具有Socket接口的计算机通信。应用程序在网络上传输,接收的信息都通过这个Socket接口来实现。

  微软为VC定 义了Winsock类如CAsyncSocket类和派生于CAsyncSocket 的CSocket类,它们简单易用,读者朋友当然可以使用这些类来实现自己的网络程序,但是为了更好的了解Winsock API编程技术,我们这里探讨怎样使用底层的API函数实现简单的 Winsock 网络应用程式设计,分别说明如何在Server端和Client端操作Socket,实现基于TCP/IP的数据传送,最后给出相关的源代码。

  在VC中进行WINSOCK的API编程开发的时候,需要在项目中使用下面三个文件,否则会出现编译错误。

  1.WINSOCK.H: 这是WINSOCK API的头文件,需要包含在项目中。

  2.WSOCK32.LIB: WINSOCK API连接库文件。在使用中,一定要把它作为项目的非缺省的连接库包含到项目文件中去。

  3.WINSOCK.DLL: WINSOCK的动态连接库,位于WINDOWS的安装目录下。

  一、服务器端操作 socket(套接字)

  1)在初始化阶段调用WSAStartup()

此函数在应用程序中初始化Windows Sockets DLL ,只有此函数调用成功后,应用程序才可以再调用其他Windows Sockets DLL中的API函数。在程式中调用该函数的形式如下:WSAStartup((WORD)((1<<8|1),(LPWSADATA) &WSAData),其中(1<<8|1)表示我们用的是WinSocket1.1版本,WSAata用来存储系统传回的关于 WinSocket的资料。

  2)建立Socket

  初始化WinSock的动态连接库后,需要在服务器端建立一个 监听的Socket,为此可以调用Socket()函数用来建立这个监听的Socket,并定义此Socket所使用的通信协议。此函数调用成功返回 Socket对象,失败则返回INVALID_SOCKET(调用WSAGetLastError()可得知原因,所有WinSocket 的函数都可以使用这个函数来获取失败的原因)。

SOCKET PASCAL FAR socket( int af, int type, int protocol )
参数: af:目前只提供 PF_INET(AF_INET);
type:Socket 的类型 (SOCK_STREAM、SOCK_DGRAM);
protocol:通讯协定(如果使用者不指定则设为0);

如果要建立的是遵从TCP/IP协议的socket,第二个参数type应为SOCK_STREAM,如为UDP(数据报)的socket,应为SOCK_DGRAM。

  3)绑定端口

  接下来要为服务器端定义的这个监听的Socket指定一个地址及端口(Port),这样客户端才知道待会要连接哪一个地址的哪个端口,为此我们要调用bind()函数,该函数调用成功返回0,否则返回SOCKET_ERROR。
int PASCAL FAR bind( SOCKET s, const struct sockaddr FAR *name,int namelen );

参 数: s:Socket对象名;
name:Socket的地址值,这个地址必须是执行这个程式所在机器的IP地址;
namelen:name的长度;

如果使用者不在意地址或端口的值,那么可以设定地址为INADDR_ANY,及Port为0,Windows Sockets 会自动将其设定适当之地址及Port (1024 到 5000之间的值)。此后可以调用getsockname()函数来获知其被设定的值。

  4)监听

当服务器端的Socket对象绑定完成之后,服务器端必须建立一个监听的队列来接收客户端的连接请求。listen()函数使服务器端的Socket 进入监听状态,并设定可以建立的最大连接数(目前最大值限制为 5, 最小值为1)。该函数调用成功返回0,否则返回SOCKET_ERROR。

int PASCAL FAR listen( SOCKET s, int backlog );
参 数: s:需要建立监听的Socket;
backlog:最大连接个数;
服务器端的Socket调用完listen()后,如果此时客户端调用connect()函数提出连接申请的话,Server 端必须再调用accept() 函数,这样服务器端和客户端才算正式完成通信程序的连接动作。为了知道什么时候客户端提出连接要求,从而服务器端的Socket在恰当的时候调用 accept()函数完成连接的建立,我们就要使用WSAAsyncSelect()函数,让系统主动来通知我们有客户端提出连接请求了。该函数调用成功 返回0,否则返回SOCKET_ERROR。

int PASCAL FAR WSAAsyncSelect( SOCKET s, HWND hWnd,unsigned int wMsg, long lEvent );
参数: s:Socket 对象;
hWnd :接收消息的窗口句柄;
wMsg:传给窗口的消息;
lEvent: 被注册的网络事件,也即是应用程序向窗口发送消息的网路事件,该值为下列值FD_READ、FD_WRITE、FD_OOB、FD_ACCEPT、 FD_CONNECT、FD_CLOSE的组合,各个值的具体含意为FD_READ:希望在套接字S收到数据时收到消息;FD_WRITE:希望在套接字 S上可以发送数据时收到消息;FD_ACCEPT:希望在套接字S上收到连接请求时收到消息;FD_CONNECT:希望在套接字S上连接成功时收到消 息;FD_CLOSE:希望在套接字S上连接关闭时收到消息;FD_OOB:希望在套接字S上收到带外数据时收到消息。
  具体应用时,wMsg应是在应用程序中定义的消息名称,而消息结构中的lParam则为以上各种网络事件名称。所以,可以在窗口处理自定义消息函数中使用以下结构来响应Socket的不同事件:  

switch(lParam) 
  {case FD_READ:
    …  
  break;
case FD_WRITE、
    …
  break;
    …
}
  5)服务器端接受客户端的连接请求

当Client提出连接请求时,Server 端hwnd视窗会收到Winsock Stack送来我们自定义的一个消息,这时,我们可以分析lParam,然后调用相关的函数来处理此事件。为了使服务器端接受客户端的连接请求,就要使用 accept() 函数,该函数新建一Socket与客户端的Socket相通,原先监听之Socket继续进入监听状态,等待他人的连接要求。该函数调用成功返回一个新产 生的Socket对象,否则返回INVALID_SOCKET。

SOCKET PASCAL FAR accept( SCOKET s, struct sockaddr FAR *addr,int FAR *addrlen );
参数:s:Socket的识别码;
addr:存放来连接的客户端的地址;
addrlen:addr的长度
  6)结束 socket 连接

结束服务器和客户端的通信连接是很简单的,这一过程可以由服务器或客户机的任一端启动,只要调用closesocket()就可以了,而要关闭 Server端监听状态的socket,同样也是利用此函数。另外,与程序启动时调用WSAStartup()憨数相对应,程式结束前,需要调用 WSACleanup() 来通知Winsock Stack释放Socket所占用的资源。这两个函数都是调用成功返回0,否则返回SOCKET_ERROR。

int PASCAL FAR closesocket( SOCKET s );
参 数:s:Socket 的识别码;
int PASCAL FAR WSACleanup( void );
参 数: 无

二、客户端Socket的操作

  1)建立客户端的Socket

客户端应用程序首先也是调用WSAStartup() 函数来与Winsock的动态连接库建立关系,然后同样调用socket() 来建立一个TCP或UDP socket(相同协定的 sockets 才能相通,TCP 对 TCP,UDP 对 UDP)。与服务器端的socket 不同的是,客户端的socket 可以调用 bind() 函数,由自己来指定IP地址及port号码;但是也可以不调用 bind(),而由 Winsock来自动设定IP地址及port号码。

  2)提出连接申请

  客户端的Socket使用connect()函数来提出与服务器端的Socket建立连接的申请,函数调用成功返回0,否则返回SOCKET_ERROR。

int PASCAL FAR connect( SOCKET s, const struct sockaddr FAR *name, int namelen );
参 数:s:Socket 的识别码;
name:Socket想要连接的对方地址;
namelen:name的长度
  三、数据的传送

虽然基于TCP/IP连接协议(流套接字)的服务是设计客户机/服务器应用程序时的主流标准,但有些服务也是可以通过无连接协议(数据报套接字)提供 的。先介绍一下TCP socket 与UDP socket 在传送数据时的特性:Stream (TCP) Socket 提供双向、可靠、有次序、不重复的资料传送。Datagram (UDP) Socket 虽然提供双向的通信,但没有可靠、有次序、不重复的保证,所以UDP传送数据可能会收到无次序、重复的资料,甚至资料在传输过程中出现遗漏。由于UDP Socket 在传送资料时,并不保证资料能完整地送达对方,所以绝大多数应用程序都是采用TCP处理Socket,以保证资料的正确性。一般情况下TCP Socket 的数据发送和接收是调用send() 及recv() 这两个函数来达成,而 UDP Socket则是用sendto() 及recvfrom() 这两个函数,这两个函数调用成功发挥发送或接收的资料的长度,否则返回SOCKET_ERROR。

int PASCAL FAR send( SOCKET s, const char FAR *buf,int len, int flags );
参数:s:Socket 的识别码
buf:存放要传送的资料的暂存区
len buf:的长度
flags:此函数被调用的方式
对于Datagram Socket而言,若是 datagram 的大小超过限制,则将不会送出任何资料,并会传回错误值。对Stream Socket 言,Blocking 模式下,若是传送系统内的储存空间不够存放这些要传送的资料,send()将会被block住,直到资料送完为止;如果该Socket被设定为 Non-Blocking 模式,那么将视目前的output buffer空间有多少,就送出多少资料,并不会被 block 住。flags 的值可设为 0 或 MSG_DONTROUTE及 MSG_OOB 的组合。

int PASCAL FAR recv( SOCKET s, char FAR *buf, int len, int flags );
参数:s:Socket 的识别码
buf:存放接收到的资料的暂存区
len buf:的长度
flags:此函数被调用的方式
  对Stream Socket 言,我们可以接收到目前input buffer内有效的资料,但其数量不超过len的大小。

  四、自定义的CMySocket类的实现代码:

  根据上面的知识,我自定义了一个简单的CMySocket类,下面是我定义的该类的部分实现代码:

//////////////////////////////////////
CMySocket::CMySocket() : file://类的构造函数
{
 WSADATA wsaD;
 memset( m_LastError, 0, ERR_MAXLENGTH );
 // m_LastError是类内字符串变量,初始化用来存放最后错误说明的字符串;
 // 初始化类内sockaddr_in结构变量,前者存放客户端地址,后者对应于服务器端地址;
 memset( &m_sockaddr, 0, sizeof( m_sockaddr ) );
 memset( &m_rsockaddr, 0, sizeof( m_rsockaddr ) );
 int result = WSAStartup((WORD)((1<<8|1), &wsaD);//初始化WinSocket动态连接库;
 if( result != 0 ) // 初始化失败;
 { set_LastError( "WSAStartup failed!", WSAGetLastError() );
  return;
 }
}

//////////////////////////////
CMySocket::~CMySocket() { WSACleanup(); }//类的析构函数;
////////////////////////////////////////////////////
int CMySocket::Create( void )
 {// m_hSocket是类内Socket对象,创建一个基于TCP/IP的Socket变量,并将值赋给该变量;
  if ( (m_hSocket = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP )) == INVALID_SOCKET )
  {
   set_LastError( "socket() failed", WSAGetLastError() );
   return ERR_WSAERROR;
  }
  return ERR_SUCCESS;
 }
///////////////////////////////////////////////
int CMySocket::Close( void )//关闭Socket对象;
{
 if ( closesocket( m_hSocket ) == SOCKET_ERROR )
 {
  set_LastError( "closesocket() failed", WSAGetLastError() );
  return ERR_WSAERROR;
 }
 file://重置sockaddr_in 结构变量;
 memset( &m_sockaddr, 0, sizeof( sockaddr_in ) );
 memset( &m_rsockaddr, 0, sizeof( sockaddr_in ) );
 return ERR_SUCCESS;
}
/////////////////////////////////////////
int CMySocket::Connect( char* strRemote, unsigned int iPort )//定义连接函数;
{
 if( strlen( strRemote ) == 0 || iPort == 0 )
  return ERR_BADPARAM;
 hostent *hostEnt = NULL;
 long lIPAddress = 0;
 hostEnt = gethostbyname( strRemote );//根据计算机名得到该计算机的相关内容;
 if( hostEnt != NULL )
 {
  lIPAddress = ((in_addr*)hostEnt->h_addr)->s_addr;
  m_sockaddr.sin_addr.s_addr = lIPAddress;
 }
 else
 {
  m_sockaddr.sin_addr.s_addr = inet_addr( strRemote );
 }
 m_sockaddr.sin_family = AF_INET;
 m_sockaddr.sin_port = htons( iPort );
 if( connect( m_hSocket, (SOCKADDR*)&m_sockaddr, sizeof( m_sockaddr ) ) == SOCKET_ERROR )
 {
  set_LastError( "connect() failed", WSAGetLastError() );
  return ERR_WSAERROR;
 }
 return ERR_SUCCESS;
}
///////////////////////////////////////////////////////
int CMySocket::Bind( char* strIP, unsigned int iPort )//绑定函数;
{
 if( strlen( strIP ) == 0 || iPort == 0 )
  return ERR_BADPARAM;
 memset( &m_sockaddr,0, sizeof( m_sockaddr ) );
 m_sockaddr.sin_family = AF_INET;
 m_sockaddr.sin_addr.s_addr = inet_addr( strIP );
 m_sockaddr.sin_port = htons( iPort );
 if ( bind( m_hSocket, (SOCKADDR*)&m_sockaddr, sizeof( m_sockaddr ) ) == SOCKET_ERROR )
 {
  set_LastError( "bind() failed", WSAGetLastError() );
  return ERR_WSAERROR;
 }
 return ERR_SUCCESS;
}
//////////////////////////////////////////
int CMySocket::Accept( SOCKET s )//建立连接函数,S为监听Socket对象名;
{
 int Len = sizeof( m_rsockaddr );
 memset( &m_rsockaddr, 0, sizeof( m_rsockaddr ) );
 if( ( m_hSocket = accept( s, (SOCKADDR*)&m_rsockaddr, &Len ) ) == INVALID_SOCKET )
 {
  set_LastError( "accept() failed", WSAGetLastError() );
  return ERR_WSAERROR;
 }
 return ERR_SUCCESS;
}
/////////////////////////////////////////////////////
int CMySocket::asyncSelect( HWND hWnd, unsigned int wMsg, long lEvent )
file://事件选择函数;
{
 if( !IsWindow( hWnd ) || wMsg == 0 || lEvent == 0 )
  return ERR_BADPARAM;
 if( WSAAsyncSelect( m_hSocket, hWnd, wMsg, lEvent ) == SOCKET_ERROR )
 {
  set_LastError( "WSAAsyncSelect() failed", WSAGetLastError() );
  return ERR_WSAERROR;
 }
 return ERR_SUCCESS;
}
////////////////////////////////////////////////////
int CMySocket::Listen( int iQueuedConnections )//监听函数;
{
 if( iQueuedConnections == 0 )
  return ERR_BADPARAM;
 if( listen( m_hSocket, iQueuedConnections ) == SOCKET_ERROR )
 {
  set_LastError( "listen() failed", WSAGetLastError() );
  return ERR_WSAERROR;
 }
 return ERR_SUCCESS;
}
////////////////////////////////////////////////////
int CMySocket::Send( char* strData, int iLen )//数据发送函数;
{
 if( strData == NULL || iLen == 0 )
  return ERR_BADPARAM;
 if( send( m_hSocket, strData, iLen, 0 ) == SOCKET_ERROR )
 {
  set_LastError( "send() failed", WSAGetLastError() );
  return ERR_WSAERROR;
 }
 return ERR_SUCCESS;
}
/////////////////////////////////////////////////////
int CMySocket::Receive( char* strData, int iLen )//数据接收函数;
{
 if( strData == NULL )
  return ERR_BADPARAM;
 int len = 0;
 int ret = 0;
 ret = recv( m_hSocket, strData, iLen, 0 );
 if ( ret == SOCKET_ERROR )
 {
  set_LastError( "recv() failed", WSAGetLastError() );
  return ERR_WSAERROR;
 }
 return ret;
}
void CMySocket::set_LastError( char* newError, int errNum )
file://WinSock API操作错误字符串设置函数;
{
 memset( m_LastError, 0, ERR_MAXLENGTH );
 memcpy( m_LastError, newError, strlen( newError ) );
 m_LastError[strlen(newError)+1] = ‘\0‘;
}
有了上述类的定义,就可以在网络程序的服务器和客户端分别定义CMySocket对象,建立连接,传送数据了。例如,为了在服务器和客户端发送数据,需 要在服务器端定义两个CMySocket对象ServerSocket1和ServerSocket2,分别用于监听和连接,客户端定义一个 CMySocket对象ClientSocket,用于发送或接收数据,如果建立的连接数大于一,可以在服务器端再定义CMySocket对象,但要注意 连接数不要大于五。

  由于Socket API函数还有许多,如获取远端服务器、本地客户机的IP地址、主机名等等,读者可以再此基础上对CMySocket补充完善,实现更多的功能。

TCP/IP Winsock编程要点

利用Winsock编程由同步和异步方式,同步方式逻辑清晰,编程专注于应用,在抢先式的多任务操作系统中(WinNt、Win2K)采用多线程方式效率基本达到异步方式的水平,应此以下为同步方式编程要点。

  1、快速通信

  Winsock的Nagle算法将降低小数据报的发送速度,而系统默认是使用Nagle算法,使用

int setsockopt(

SOCKET s,

int level,

int optname,

const char FAR *optval,

int optlen

);函数关闭它
  例子:

SOCKET sConnect;

sConnect=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

int bNodelay = 1;

int err;

err = setsockopt(

sConnect,

IPPROTO_TCP,

TCP_NODELAY,

(char *)&bNodelay,

sizoeof(bNodelay));//不采用延时算法

if (err != NO_ERROR)

TRACE ("setsockopt failed for some reason\n");;
  2、SOCKET的SegMentSize和收发缓冲

  TCPSegMentSize是发送接受时单个数据报的最大长度,系统默认为1460,收发缓冲大小为8192。

在SOCK_STREAM方式下,如果单次发送数据超过1460,系统将分成多个数据报传送,在对方接受到的将是一个数据流,应用程序需要增加断帧的判 断。当然可以采用修改注册表的方式改变1460的大小,但MicrcoSoft认为1460是最佳效率的参数,不建议修改。

  在工控系统中,建议关闭Nagle算法,每次发送数据小于1460个字节(推荐1400),这样每次发送的是一个完整的数据报,减少对方对数据流的断帧处理。

  3、同步方式中减少断网时connect函数的阻塞时间

  同步方式中的断网时connect的阻塞时间为20秒左右,可采用gethostbyaddr事先判断到服务主机的路径是否是通的,或者先ping一下对方主机的IP地址。

  A、采用gethostbyaddr阻塞时间不管成功与否为4秒左右。

  例子:

LONG lPort=3024;

struct sockaddr_in ServerHostAddr;//服务主机地址

ServerHostAddr.sin_family=AF_INET;

ServerHostAddr.sin_port=::htons(u_short(lPort));

ServerHostAddr.sin_addr.s_addr=::inet_addr("192.168.1.3");

HOSTENT* pResult=gethostbyaddr((const char *) &

(ServerHostAddr.sin_addr.s_addr),4,AF_INET);

if(NULL==pResult)

{

int nErrorCode=WSAGetLastError();

TRACE("gethostbyaddr errorcode=%d",nErrorCode);

}

else

{

TRACE("gethostbyaddr %s\n",pResult->h_name);;

}
  B、采用PING方式时间约2秒左右

  暂略

4、同步方式中解决recv,send阻塞问题

  采用select函数解决,在收发前先检查读写可用状态。

  A、读

  例子:

TIMEVAL tv01 = {0, 1};//1ms钟延迟,实际为0-10毫秒

int nSelectRet;

int nErrorCode;

FD_SET fdr = {1, sConnect};

nSelectRet=::select(0, &fdr, NULL, NULL, &tv01);//检查可读状态

if(SOCKET_ERROR==nSelectRet)

{

nErrorCode=WSAGetLastError();

TRACE("select read status errorcode=%d",nErrorCode);

::closesocket(sConnect);

goto 重新连接(客户方),或服务线程退出(服务方);

}

if(nSelectRet==0)//超时发生,无可读数据

{

继续查读状态或向对方主动发送

}

else

{

读数据

}
  B、写

TIMEVAL tv01 = {0, 1};//1ms钟延迟,实际为9-10毫秒

int nSelectRet;

int nErrorCode;

FD_SET fdw = {1, sConnect};

nSelectRet=::select(0, NULL, NULL,&fdw, &tv01);//检查可写状态

if(SOCKET_ERROR==nSelectRet)

{

nErrorCode=WSAGetLastError();

TRACE("select write status errorcode=%d",nErrorCode);

::closesocket(sConnect);

//goto 重新连接(客户方),或服务线程退出(服务方);

}

if(nSelectRet==0)//超时发生,缓冲满或网络忙

{

//继续查写状态或查读状态

}

else

{

//发送

}
  5、改变TCP收发缓冲区大小

  系统默认为8192,利用如下方式可改变。

SOCKET sConnect;

sConnect=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

int nrcvbuf=1024*20;

int err=setsockopt(

sConnect,

SOL_SOCKET,

SO_SNDBUF,//写缓冲,读缓冲为SO_RCVBUF

(char *)&nrcvbuf,

sizeof(nrcvbuf));

if (err != NO_ERROR)

{

TRACE("setsockopt Error!\n");

}

在设置缓冲时,检查是否真正设置成功用

int getsockopt(

SOCKET s,

int level,

int optname,

char FAR *optval,

int FAR *optlen

);
  6、服务方同一端口多IP地址的bind和listen

  在可靠性要求高的应用中,要求使用双网和多网络通道,再服务方很容易实现,用如下方式可建立客户对本机所有IP地址在端口3024下的请求服务。

SOCKET hServerSocket_DS=INVALID_SOCKET;

struct sockaddr_in HostAddr_DS;//服务器主机地址

LONG lPort=3024;

HostAddr_DS.sin_family=AF_INET;

HostAddr_DS.sin_port=::htons(u_short(lPort));

HostAddr_DS.sin_addr.s_addr=htonl(INADDR_ANY);

hServerSocket_DS=::socket( AF_INET, SOCK_STREAM,IPPROTO_TCP);

if(hServerSocket_DS==INVALID_SOCKET)

{

AfxMessageBox("建立数据服务器SOCKET 失败!");

return FALSE;

}

if(SOCKET_ERROR==::bind(hServerSocket_DS,(struct

sockaddr *)(&(HostAddr_DS)),sizeof(SOCKADDR)))

{

int nErrorCode=WSAGetLastError ();

TRACE("bind error=%d\n",nErrorCode);

AfxMessageBox("Socket Bind 错误!");

return FALSE;

}

if(SOCKET_ERROR==::listen(hServerSocket_DS,10))//10个客户

{

AfxMessageBox("Socket listen 错误!");

return FALSE;

}

AfxBeginThread(ServerThreadProc,NULL,THREAD_PRIORITY_NORMAL);
  在客户方要复杂一些,连接断后,重联不成功则应换下一个IP地址连接。也可采用同时连接好后备用的方式。

  7、用TCP/IP Winsock实现变种Client/Server

传统的Client/Server为客户问、服务答,收发是成对出现的。而变种的Client/Server是指在连接时有客户和服务之分,建立好通信 连接后,不再有严格的客户和服务之分,任何方都可主动发送,需要或不需要回答看应用而言,这种方式在工控行业很有用,比如RTDB作为I/O Server的客户,但I/O Server也可主动向RTDB发送开关状态变位、随即事件等信息。在很大程度上减少了网络通信负荷、提高了效率。

  采用1-6的TCP/IP编程要点,在Client和Server方均已接收优先,适当控制时序就能实现。

Windows Sockets API实现网络异步通讯

摘要:本文对如何使用面向连接的流式套接字实现对网卡的编程以及如何实现异步网络通讯等问题进行了讨论与阐述。

  一、 引言

在80年代初,美国加利福尼亚大学伯克利分校的研究人员为TCP/IP网络通信开发了一个专门用于网络通讯开发的API。这个API就是Socket接 口(套接字)--当今在TCP/IP网络最为通用的一种API,也是在互联网上进行应用开发最为通用的一种API。在微软联合其它几家公司共同制定了一套 Windows下的网络编程接口Windows Sockets规范后,由于在其规范中引入了一些异步函数,增加了对网络事件异步选择机制,因此更加符合Windows的消息驱动特性,使网络开发人员可 以更加方便的进行高性能网络通讯程序的设计。本文接下来就针对Windows Sockets API进行面向连接的流式套接字编程以及对异步网络通讯的编程实现等问题展开讨论。

  二、 面向连接的流式套接字编程模型的设计

本文在方案选择上采用了在网络编程中最常用的一种模型--客户机/服务器模型。这种客户/服务器模型是一种非对称式编程模式。该模式的基本思想是把集中 在一起的应用划分成为功能不同的两个部分,分别在不同的计算机上运行,通过它们之间的分工合作来实现一个完整的功能。对于这种模式而言其中一部分需要作为 服务器,用来响应并为客户提供固定的服务;另一部分则作为客户机程序用来向服务器提出请求或要求某种服务。

  本文选取了基于 TCP/IP的客户机/服务器模型和面向连接的流式套接字。其通信原理为:服务器端和客户端都必须建立通信套接字,而且服务器端应先进入监听状态,然后客 户端套接字发出连接请求,服务器端收到请求后,建立另一个套接字进行通信,原来负责监听的套接字仍进行监听,如果有其它客户发来连接请求,则再建立一个套 接字。默认状态下最多可同时接收5个客户的连接请求,并与之建立通信关系。因此本程序的设计流程应当由服务器首先启动,然后在某一时刻启动客户机并使其与 服务器建立连接。服务器与客户机开始都必须调用Windows Sockets API函数socket()建立一个套接字sockets,然后服务器方调用bind()将套接字与一个本地网络地址捆扎在一起,再调用listen() 使套接字处于一种被动的准备接收状态,同时规定它的请求队列长度。在此之后服务器就可以通过调用accept()来接收客户机的连接。

相对于服务器,客户端的工作就显得比较简单了,当客户端打开套接字之后,便可通过调用connect()和服务器建立连接。连接建立之后,客户和服务器 之间就可以通过连接发送和接收资料。最后资料传送结束,双方调用closesocket()关闭套接字来结束这次通讯。整个通讯过程的具体流程框图可大致 用下面的流程图来表示:


        面向连接的流式套接字编程流程示意图

三、 软件设计要点以及异步通讯的实现

  根据前面设计的程序流程,可将程序划分为两部分:服务器端和客户端。而且整个实现过程可以大致用以下几个非常关键的Windows Sockets API函数将其惯穿下来:

  服务器方:

socket()->bind()->listen->accept()->recv()/send()->closesocket()
  客户机方:

socket()->connect()->send()/recv()->closesocket()
有鉴于以上几个函数在整个网络编程中的重要性,有必要结合程序实例对其做较深入的剖析。服务器端应用程序在使用套接字之前,首先必须拥有一个 Socket,系统调用socket()函数向应用程序提供创建套接字的手段。该套接字实际上是在计算机中提供了一个通信埠,可以通过这个埠与任何一个具 有套接字接口的计算机通信。应用程序在网络上传输、接收的信息都通过这个套接字接口来实现的。在应用开发中如同使用文件句柄一样,可以对套接字句柄进行读 写操作:

sock=socket(AF_INET,SOCK_STREAM,0);
函数的第一个参数用于指定地址族,在Windows下仅支持AF_INET(TCP/IP地址);第二个参数用于描述套接字的类型,对于流式套接字提供 有SOCK_STREAM;最后一个参数指定套接字使用的协议,一般为0。该函数的返回值保存了新套接字的句柄,在程序退出前可以用 closesocket(sock);函数来将其释放。服务器方一旦获取了一个新的套接字后应通过bind()将该套接字与本机上的一个端口相关联:

sockin.sin_family=AF_INET;
sockin.sin_addr.s_addr=0;
sockin.sin_port=htons(USERPORT);
bind(sock,(LPSOCKADDR)&sockin,sizeof(sockin)));
该函数的第二个参数是一个指向包含有本机IP地址和端口信息的sockaddr_in结构类型的指针,其成员描述了本地端口号和本地主机地址,经过 bind()将服务器进程在网络上标识出来。需要注意的是由于1024以内的埠号都是保留的埠号因此如无特别需要一般不能将 sockin.sin_port的埠号设置为1024以内的值。然后调用listen()函数开始侦听,再通过accept()调用等待接收连接以完成连 接的建立:

//连接请求队列长度为1,即只允许有一个请求,若有多个请求,
//则出现错误,给出错误代码WSAECONNREFUSED。
listen(sock,1);
//开启线程避免主程序的阻塞
AfxBeginThread(Server,NULL);
……
UINT Server(LPVOID lpVoid)
{
……
int nLen=sizeof(SOCKADDR);
pView->newskt=accept(pView->sock,(LPSOCKADDR)& pView->sockin,(LPINT)& nLen);
……
WSAAsyncSelect(pView->newskt,pView->m_hWnd,WM_SOCKET_MSG,FD_READ|FD_CLOSE);
return 1;
}

这里之所以把accept()放到一个线程中去是因为在执行到该函数时如没有客户连接服务器的请求到来,服务器就会停在accept语句上等待连接请求 的到来,这势必会引起程序的阻塞,虽然也可以通过设置套接字为非阻塞方式使在没有客户等待时可以使accept()函数调用立即返回,但这种轮询套接字的 方式会使CPU处于忙等待方式,从而降低程序的运行效率大大浪费系统资源。考虑到这种情况,将套接字设置为阻塞工作方式,并为其单独开辟一个子线程,将其 阻塞控制在子线程范围内而不会造成整个应用程序的阻塞。对于网络事件的响应显然要采取异步选择机制,只有采取这种方式才可以在由网络对方所引起的不可预知 的网络事件发生时能马上在进程中做出及时的响应处理,而在没有网络事件到达时则可以处理其他事件,这种效率是很高的,而且完全符合Windows所标榜的 消息触发原则。前面那段代码中的WSAAsyncSelect()函数便是实现网络事件异步选择的核心函数。

通过第四个参数注册应用程序 感兴取的网络事件,在这里通过FD_READ|FD_CLOSE指定了网络读和网络断开两种事件,当这种事件发生时变会发出由第三个参数指定的自定义消息 WM_SOCKET_MSG,接收该消息的窗口通过第二个参数指定其句柄。在消息处理函数中可以通过对消息参数低字节进行判断而区别出发生的是何种网络事 件:

void CNetServerView::OnSocket(WPARAM wParam,LPARAM lParam)
{
int iReadLen=0;
int message=lParam & 0x0000FFFF;
switch(message)
{
case FD_READ://读事件发生。此时有字符到达,需要进行接收处理
char cDataBuffer[MTU*10];
//通过套接字接收信息
iReadLen = recv(newskt,cDataBuffer,MTU*10,0);
//将信息保存到文件
if(!file.Open("ServerFile.txt",CFile::modeReadWrite))
file.Open("E:ServerFile.txt",CFile::modeCreate|CFile::modeReadWrite);
file.SeekToEnd();
file.Write(cDataBuffer,iReadLen);
file.Close();
break;
case FD_CLOSE://网络断开事件发生。此时客户机关闭或退出。
……//进行相应的处理
break;
default:
break;
}
}
  在这里需要实现对自定义消息WM_SOCKET_MSG的响应,需要在头文件和实现文件中分别添加其消息映射关系:

  头文件:

//{{AFX_MSG(CNetServerView)
//}}AFX_MSG
void OnSocket(WPARAM wParam,LPARAM lParam);
DECLARE_MESSAGE_MAP()
  实现文件:

BEGIN_MESSAGE_MAP(CNetServerView, CView)
//{{AFX_MSG_MAP(CNetServerView)
//}}AFX_MSG_MAP
ON_MESSAGE(WM_SOCKET_MSG,OnSocket)
END_MESSAGE_MAP()

  在进行异步选择使用WSAAsyncSelect()函数时,有以下几点需要引起特别的注意:

  1. 连续使用两次WSAAsyncSelect()函数时,只有第二次设置的事件有效,如:

WSAAsyncSelect(s,hwnd,wMsg1,FD_READ);
WSAAsyncSelect(s,hwnd,wMsg2,FD_CLOSE);
  这样只有当FD_CLOSE事件发生时才会发送wMsg2消息。

  2.可以在设置过异步选择后通过再次调用WSAAsyncSelect(s,hwnd,0,0);的形式取消在套接字上所设置的异步事件。

  3.Windows Sockets DLL在一个网络事件发生后,通常只会给相应的应用程序发送一个消息,而不能发送多个消息。但通过使用一些函数隐式地允许重发此事件的消息,这样就可能再次接收到相应的消息。

  4.在调用过closesocket()函数关闭套接字之后不会再发生FD_CLOSE事件。

以上基本完成了服务器方的程序设计,下面对于客户端的实现则要简单多了,在用socket()创建完套接字之后只需通过调用connect()完成同服 务器的连接即可,剩下的工作同服务器完全一样:用send()/recv()发送/接收收据,用closesocket()关闭套接字:

sockin.sin_family=AF_INET; //地址族
sockin.sin_addr.S_un.S_addr=IPaddr; //指定服务器的IP地址
sockin.sin_port=m_Port; //指定连接的端口号
int nConnect=connect(sock,(LPSOCKADDR)&sockin,sizeof(sockin));

本文采取的是可靠的面向连接的流式套接字。在数据发送上有write()、writev()和send()等三个函数可供选择,其中前两种分别用于缓冲 发送和集中发送,而send()则为可控缓冲发送,并且还可以指定传输控制标志为MSG_OOB进行带外数据的发送或是为MSG_DONTROUTE寻径 控制选项。在信宿地址的网络号部分指定数据发送需要经过的网络接口,使其可以不经过本地寻径机制直接发送出去。这也是其同write()函数的真正区别所 在。由于接收数据系统调用和发送数据系统调用是一一对应的,因此对于数据的接收,在此不再赘述,相应的三个接收函数分别为:read()、readv() 和recv()。由于后者功能上的全面,本文在实现上选择了send()-recv()函数对,在具体编程中应当视具体情况的不同灵活选择适当的发送-接 收函数对。

  小结:TCP/IP协议是目前各网络操作系统主要的通讯协议,也是 Internet的通讯协议,本文通过Windows Sockets API实现了对基于TCP/IP协议的面向连接的流式套接字网络通讯程序的设计,并通过异步通讯和多线程等手段提高了程序的运行效率,避免了阻塞的发生。

用VC++6.0的Sockets API实现一个聊天室程序

1.VC++网络编程及Windows Sockets API简介

VC++对网络编程的支持有socket支持,WinInet支持,MAPI和ISAPI支持等。其中,Windows Sockets API是TCP/IP网络环境里,也是Internet上进行开发最为通用的API。最早美国加州大学Berkeley分校在UNIX下为TCP/IP协 议开发了一个API,这个API就是著名的Berkeley Socket接口(套接字)。在桌面操作系统进入Windows时代后,仍然继承了Socket方法。在TCP/IP网络通信环境下,Socket数据传 输是一种特殊的I/O,它也相当于一种文件描述符,具有一个类似于打开文件的函数调用-socket()。可以这样理解篠ocket实际上是一个通信端 点,通过它,用户的Socket程序可以通过网络和其他的Socket应用程序通信。Socket存在于一个"通信域"(为描述一般的线程如何通过 Socket进行通信而引入的一种抽象概念)里,并且与另一个域的Socket交换数据。Socket有三类。第一种是SOCK_STREAM(流式), 提供面向连接的可靠的通信服务,比如telnet,http。第二种是SOCK_DGRAM(数据报),提供无连接不可靠的通信,比如UDP。第三种是 SOCK_RAW(原始),主要用于协议的开发和测试,支持通信底层操作,比如对IP和ICMP的直接访问。

  2.Windows Socket机制分析

  2.1一些基本的Socket系统调用

主要的系统调用包括:socket()-创建Socket;bind()-将创建的Socket与本地端口绑定;connect()与accept() -建立Socket连接;listen()-服务器监听是否有连接请求;send()-数据的可控缓冲发送;recv()-可控缓冲接收; closesocket()-关闭Socket。

  2.2Windows Socket的启动与终止

  启动函数WSAStartup()建立与Windows Sockets DLL的连接,终止函数WSAClearup()终止使用该DLL,这两个函数必须成对使用。

  2.3异步选择机制

Windows是一个非抢占式的操作系统,而不采取UNIX的阻塞机制。当一个通信事件产生时,操作系统要根据设置选择是否对该事件加以处理, WSAAsyncSelect()函数就是用来选择系统所要处理的相应事件。当Socket收到设定的网络事件中的一个时,会给程序窗口一个消息,这个消 息里会指定产生网络事件的Socket,发生的事件类型和错误码。

  2.4异步数据传输机制

  WSAAsyncSelect()设定了Socket上的须响应通信事件后,每发生一个这样的事件就会产生一个WM_SOCKET消息传给窗口。而在窗口的回调函数中就应该添加相应的数据传输处理代码。

  3.聊天室程序的设计说明

  3.1实现思想

  在Internet上的聊天室程序一般都是以服务器提供服务端连接响应,使用者通过客户端程序登录到服务器,就可以与登录在同一服务器上的用户交谈,这是一个面向连接的通信过程。因此,程序要在TCP/IP环境下,实现服务器端和客户端两部分程序。

  3.2服务器端工作流程

服务器端通过socket()系统调用创建一个Socket数组后(即设定了接受连接客户的最大数目),与指定的本地端口绑定bind(),就可以在端 口进行侦听listen()。如果有客户端连接请求,则在数组中选择一个空Socket,将客户端地址赋给这个Socket。然后登录成功的客户就可以在 服务器上聊天了。

  3.3客户端工作流程

  客户端程序相对简单,只需要建立一个Socket与服务器端连接,成功后通过这个Socket来发送和接收数据就可以了。

4.核心代码分析

  限于篇幅,这里仅给出与网络编程相关的核心代码,其他的诸如聊天文字的服务器和客户端显示读者可以自行添加。

  4.1服务器端代码

  开启服务器功能:

void OnServerOpen() //开启服务器功能
{
 WSADATA wsaData;
 int iErrorCode;
 char chInfo[64];
 if (WSAStartup(WINSOCK_VERSION, &wsaData)) //调用Windows Sockets DLL
  { MessageBeep(MB_ICONSTOP);
   MessageBox("Winsock无法初始化!", AfxGetAppName(), MB_OK|MB_ICONSTOP);
   WSACleanup();
   return; }
 else
  WSACleanup();