• 11145阅读
  • 0回复

【转载】Windows Socket 网络编程 [复制链接]

上一主题 下一主题
离线XChinux
 

只看楼主 倒序阅读 楼主  发表于: 2005-08-14
原文见:http://www.farproc.com/Article/ShowArticle.asp?ArticleID=147&Page=1
(一) -- TCP/IP体系结构、特点及相关术语

、TCP/IP 体系结构与特点

1、TCP/IP体系结构

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



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、套接字
它是网络的基本构件。它是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连听进程。套接字存在通信区域(通信区域又称地址簇)中。套接字只与同一区域中的套接字交换数据(跨区域时,需要执行某和转换进程才能实现)。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 SOCKETS 1.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)){...}



下面是两段示例代码:
原文见:http://bbs.chinaunix.net/forum/viewtopic.php?t=237176&show_type=&postdays=0&postorder=asc&start=0
服务器端代码:(Windows下)
/******* 服务器程序 (server.c) ************/
#include <WINSOCK.H>
#include <stdio.h>
#include <conio.h>
//tuxedo Header
#include <atmi.h>
void TOUPPER(void *p);
int nthread;
int ncur;
void main(int argc, char *argv[])
{
  int new_fd;
  struct sockaddr_in server_addr;
  struct sockaddr_in client_addr;
  int sin_size,portnumber;
  SOCKET sockfd;
  if(argc!=2)
  {
    printf("Usage:%s portnumber\a\n",argv[0]);
    return;
  }
  if((portnumber=atoi(argv[1])) <= 0)
  {
    portnumber = 5866;
    printf("Use default port number:%d \n",portnumber);
  }
  WORD wVersionRequested;
  WSADATA wsaData;
  int err;
  wVersionRequested = MAKEWORD(2,2);
  err = WSAStartup( wVersionRequested,&wsaData);
  if ( err != 0 )
  {
    printf("Cant find WinSock DLL\n");
    return;
  }
  if (LOBYTE(wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion )!=2)
  {
    printf("Could not find a usable WinSock DLL\n");
    WSACleanup( );
    return;
  }
  /* 服务器端开始建立socket描述符 */
  if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)  
  {
    if (WSAGetLastError() == WSANOTINITIALISED)
    {
      printf("Error:WSANOTINITIALISED,please Call WSAStartup first\n");
      return;
    }
    else
    {
      int err =WSAGetLastError();
      printf("Bind error:%s,errorcode :%d\n",strerror(errno),err);
      return;
    }
  }
  /* 服务器端填充 sockaddr结构 */
  memset(&server_addr,0,sizeof(struct sockaddr_in));
  server_addr.sin_family=AF_INET;
  server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
  server_addr.sin_port=htons(portnumber);
  /* 捆绑sockfd描述符 */
  if(bind(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)
  {
    int err = WSAGetLastError();
    fprintf(stderr,"Bind error:%s,errorcode :%d\n",strerror(errno),err);
    return;
  }
  /* 监听sockfd描述符 */
  if(listen(sockfd,5)==-1)
  {
    fprintf(stderr,"Listen error:%s\n\a",strerror(errno));
    return;
  }
  while(1)
  {
    /* 服务器阻塞,直到客户程序建立连接 */
    sin_size=sizeof(struct sockaddr_in);
    if((new_fd=accept(sockfd,(struct sockaddr *)(&client_addr),&sin_size))==INVALID_SOCKET )
    {
      printf("Accept error:%s\n\a",strerror(errno));
      break;
    }  
    printf("Server get connection from %s\n",inet_ntoa(client_addr.sin_addr));
  }
  tpterm();
}

客户端代码(类Unix下)

#include <iostream>
using namespace std;
                                                   
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>

int main(int argc, char* argv[])
{
  if (argc != 2) {
    cout << "Error! please input argv[1]" << endl;
    exit(1);
  }

  int fd;
  struct sockaddr_in servaddr;

  if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
    cout << "Can not create socket!" << endl;
    exit(2);
  }

  memset(&servaddr, 0, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_port = htons(atoi(argv[1]));
  inet_pton(AF_INET, "192.168.0.24", &servaddr.sin_addr);

  if (connect(fd, (sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
    cout << "Can not create connect!" << endl;
    exit(3);
  }

    char buf[1024];
    memset(buf, 0, 1024);
    cout << "Please input a line to server: " << endl;
    cin.getline(buf, 1024);

    if(write(fd, buf, strlen(buf) + 1) < 0) {
            cout << "Write error!" << endl;
      }

    memset(buf, 0, 1024);
    if(read(fd, buf, 1024) < 0) {
      cout << "Read error!" << endl;
    }

    cout << "I(client) got your message: " << buf << endl;

  close(fd);
  exit(5);
}
二笔 openSUSE Vim N9 BB10 XChinux@163.com 网易博客 腾讯微博
承接C++/Qt、Qt UI界面、PHP及预算报销系统开发业务
快速回复
限100 字节
 
上一个 下一个