• WinSock网络编程实用宝典(一)


    一、TCP/IP 体系结构与特点 
      1、TCP/IP体系结构
      TCP/IP协议实际上就是在物理网上的一组完整的网络协议。其中TCP是提供传输层服务,而IP则是提供网络层服务。TCP/IP包括以下协议:(结构如图1.1)
    WinSock网络编程实用宝典(一) - hackbin - 一个人的天空
    (图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协议核心与应用程序关系图。

    WinSock网络编程实用宝典(一) - hackbin - 一个人的天空
    (图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 面向连接的套接字的系统调用时序图

    WinSock网络编程实用宝典(一) - hackbin - 一个人的天空

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

    WinSock网络编程实用宝典(一) - hackbin - 一个人的天空

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

    WinSock网络编程实用宝典(一) - hackbin - 一个人的天空 

    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秒左右 

      暂略 
  • 相关阅读:
    RabbitMQ消息队列 基本订阅/发布Demo(PHP版)
    Docker安装部署RabbitMQ
    CentOS Docker 基本操作
    new worker
    JavaScript避坑
    如何开启MySQL慢查询日志
    kinshard
    Linux shell
    Linux shell
    Linux shell
  • 原文地址:https://www.cnblogs.com/qq78292959/p/2077058.html
Copyright © 2020-2023  润新知