Winsock编程原理

来源:百度文库 编辑:神马文学网 时间:2024/04/28 23:04:34
本课程主要讲Windows中TCP/IP编程接口Winsock,版本为1.1。高版本的Winsock实际与1.1版相差不多,主要是进行了一些扩充,如可超越TCP/IP协议直接用socket来实现IPX、NETBIOS等其它通信协议。

  这叙述方便在本文的其余部分中提到的Winsock指的就是Winsock1.1。

  通过Winsock可实现点对点或广播通信程序,实际这两者之间的区别不大,编程时其程序流程所用代码几乎相同,不同的地方在于目标地址选择的不同。本课程中所举实例为点对点的形式,并以客户/服务器形式来构建通过Winsock进行通信的点对点通信,并对通信过程的两点分别命名为Server和Client。

为更清楚的说明出Winsock的结构原理,下面以电信局的普通电话服务为比较对象进行说明:
1、电信局提供电话服务类似版主们这的Server,普通电话用户类似版主们这的Client。

2、首先电信局必须建立一个电话总机。这就如果版主们必须在Server端建立一个Socket(套接字),这一步通过调用socket()函数实现。

3、电信局必须给电话总机分配一个号码,以便使用户要拨找该号码得到电话服务,同时接入该电信局的用户必须知道该总机的号码。同样,版主也在Server端也要为这一套接字指定一port(端口),并且要连接该Server的Client必须知道该端口。这一步通过调用bind()函数实现。

4、接下来电信局必须使总机开通并使总机能够高效地监听用户拨号,如果电信局所提供服务的用户数太多,你会发现拨打电信局总机老是忙音,通常电信局内部会使该总机对应的电话号码连到好几个负责交换的处理中心,在一个处理中心忙于处理当前的某个用户时,新到用户可自动转到一下处理中心得到服务。同样版主们的Server端也要使自己的套接口设置成监听状态,这是通用listen()函数实现的,listen()的第二个参数是等待队列数,就如同你可以指定电信局的建立几个负责交换的处理中心。

5、用户知道了电信局的总机号后就可以进行拨打请求得到服务。在Winsock的世界里做为Client端是要先用socket()函数建立一个套接字,然后调connect()函数进行连接。当然和电话一样,如果等待队列数满了、与Server的线路不通或是Server没有提供此项服务时,连接就不会成功。

5、电信局的总机接受了这用户拨打的电话后负责接通用户的线路,而总机本身则再回到等待的状态。Server也是一样,调用accept()函数进入监听处理过程,Server端的代码即在中处暂停,一旦Server端接到申请后系统会建立一个新的套接字来对此连接做服务,而原先的套接字则再回到监听等待的状态。

6、当你电话挂完了,你就可以挂上电话,彼此间也就离线了。Client和Server间的套接字的关闭也是如此;这个关闭离线的动作,可由Client端或Server端任一方先关闭。有些电话查询系统不也是如此吗?关闭套接字的函数为
closesocket()。

从以上情况可以看出在服务器端建立一个套接字,并进入实际的监听步骤的过程如下:socket()->bind()->listen()->accept()

那么在accept()完了后,版主们说在Server端将生成一个新的套接字,然后Server将继续进入accept()状态,版主们该如何用这个新的套接字来进行与Client端的通信呢,这就用到了recv()函数,而Client端则是通过send()函数来向服务器发信息的。

在客户端也是采取类似的过程,其调用Winsock的过程如下:
socket()->connect()->send()
首先建立一个socket,然后用connect()函数将其与Server端的socket连接,连接成功后调用send()发送信息。

//A simplest web server
//Written by Shen zhiliang for learning Winsock & HTTP
file://zhiliang@sina.com
 
#include "winsock.h"
#include "stdio.h"
#include "conio.h"
#include "io.h"

#define BUFLEN 2048
#define DEFPATH ("C:\\frontpage webs\\content\\")
#define DEFFILE ("INDEX.HTM")
#define HTTPHEAD ("HTTP/1.0 200 OK\010Date: Monday, 04-Jan-99 17:06:17 GMT\x0aServer: HTTP-Server/1.0\x0a MIME-version: 1.0\x0a")
#define MIMEHTML ("Content-type: text/html\x0a Last-modified: Friday, 26-Sep-97 09:36:54 GMT\x0a")
#define MIMEGIF ("Content-type: /image/gif\x0a Last-modified: Friday, 26-Sep-97 09:36:54 GMT\x0a")
#define MIMEJPEG ("Content-type: /image/jpeg\x0a Last-modified: Friday, 26-Sep-97 09:36:54 GMT\x0a")
#define MIMEPAIN ("Content-type: text/pain\x0a Last-modified: Friday, 26-Sep-97 09:36:54 GMT\x0a")
#define HTMLHEAD ("\x0a")
#define HTMLTAIL ("\n")

void P(char * a)
{
    printf("Error in : %s\n",a);
}

/*
char * httphead(FILE * fp)
{
    struct tm *newtime;
    time_t aclock;
    time( &aclock );
    newtime = localtime( &aclock );
    printf( "The current date and time are: %s", asctime( newtime ) );
}
*/

void HtmlError(SOCKET s,unsigned int code,char * msg)
{
    char tmp[1024];
    sprintf(tmp,"Error code: %d %s",code,msg);
    send(s,HTTPHEAD,strlen(HTTPHEAD),0);
    send(s,HTMLHEAD,strlen(HTMLHEAD),0);
    send(s,tmp,strlen(tmp),0);
    send(s,HTMLTAIL,strlen(HTMLTAIL),0);
}

void SendHtmlFile(SOCKET s,char * filename)
{
    FILE * fp;
    char * tmp;
    char fullname[512];
    char buf[1024];
    int i;
 
    strcpy(fullname,DEFPATH);
 
    if(strlen(filename)==0||(strlen(filename)==1&&filename[0]=='/'))
        strcat(fullname,DEFFILE);
    else
    {
        do{
            tmp=strchr(filename,'/');
            if(tmp!=NULL)
                tmp[0]='\\';
        }while(tmp!=NULL);

        if(filename[0]=='\\')
            strcat(fullname,&filename[1]);
        else
            strcat(fullname,filename);
    }

    FILE * fpt=fopen("recv.dat","a+b");
    fwrite(fullname,sizeof(char),strlen(fullname),fpt);
    fclose(fpt);
    fp=fopen(fullname,"rb");
 
    if(fp==NULL)
    {
        char msg[512];
        if(filename[0]=='\\')
            filename[0]='/';
        sprintf(msg," URI no found: %s",filename);
        HtmlError(s,404,msg);
        return;
    }

    send(s,HTTPHEAD,strlen(HTTPHEAD),0);
    if(stricmp(&filename[strlen(filename)-4],".GIF")==0)
        send(s,MIMEGIF,strlen(MIMEGIF),0);
    else if(stricmp(&filename[strlen(filename)-4],".JPG")==0|| stricmp(&filename[strlen(filename)-5],".JPEG")==0)
        send(s,MIMEJPEG,strlen(MIMEJPEG),0);
    else if(stricmp(&filename[strlen(filename)-4],".HTM")==0|| stricmp(&filename[strlen(filename)-5],".HTML")==0)
    {
        send(s,MIMEHTML,strlen(MIMEHTML),0);
    }
 
    long fs=_filelength(_fileno(fp));
    buf[0]=0;
    sprintf(buf,"Content-length: %ld\x0a\x0a",fs);
    send(s,buf,strlen(buf),0);
    for(i=0;!feof(fp);i++)
    {
        buf[i]=fgetc(fp);
        if(i>=1023||feof(fp))
        {
            send(s,buf,i,0);
            i=0;
        }
    }

    fclose(fp);
}

void SocketError(char * call)
{
    fprintf(stderr," WinSock Error# function: %s, error code:%ld\n",call,WSAGetLastError());
}

main(int argc,char ** argv)
{
    int iRes,iPort,iTmp;
    SOCKET s,rs;
    SOCKADDR_IN sin,rsin;
    WSADATA wsad;
    WORD wVersionReq;
    char recvBuf[BUFLEN];
 
    if(argc<2)
    {
        fprintf(stderr,"Usage: WebServer 999\n\t999 - Port number for this server.");
        return -1;
    }

    iPort=0;
    iPort=atoi(argv[1]);
    if(iPort<=0)
    {
        fprintf(stderr,"must specify a port number");
        return -1;
    }
    wVersionReq=MAKEWORD(1,1);
 
    iRes=WSAStartup(wVersionReq,&wsad);
    if(iRes!=0)
    {
        SocketError("WSAStartup()");
        return -1;
    }
 
    s=socket(PF_INET,SOCK_STREAM,0);
    if(s==INVALID_SOCKET)
    {
        SocketError("socket()");
        return -1;
    }

    sin.sin_family=PF_INET;
    sin.sin_port=htons(iPort);
    sin.sin_addr.s_addr=INADDR_ANY;
    iTmp=sizeof(sin);

    if(bind(s,(LPSOCKADDR)&sin,iTmp)==SOCKET_ERROR)
    {
        SocketError("bind()");
        closesocket(s);
        WSACleanup();
        return -1;
    }
 
    if(listen(s,1)==SOCKET_ERROR)
    {
        SocketError("listen()");
        closesocket(s);
        WSACleanup();
        return -1;
    }

    fprintf(stderr,"WebServer Running......\n");
    iTmp=sizeof(rsin);
    rs=0;
    while(1)
    {
        if(_kbhit()!=0)
        {
            if(_getch()!=27)
                break;
            if(rc!=0)
            closesocket(rs);
            closesocket(s);
            WSACleanup();
            fprintf(stderr,"WebServer Stopped......\n");
            return 0;
        }

        rs=accept(s,(LPSOCKADDR)&rsin,&iTmp);
        if(rs==INVALID_SOCKET)
        {
            SocketError("accept()");
            closesocket(s);
            WSACleanup();
            return -1;
        }

        iRes=recv(rs,recvBuf,BUFLEN,0);
        printf("RECEIVED DATA: \n---------------------------------\n%s\n---------------------------------\n",recvBuf);
        if(iRes==SOCKET_ERROR)
        {
            SocketError("recv()");
            closesocket(rs);
            closesocket(s);
            WSACleanup();
            return -1;
        }
 
        char * sRes;
        sRes=strstr(recvBuf,"GET");
        if(sRes!=NULL&&(sRes-recvBuf)<3)
            sRes=strchr(recvBuf,'/');
        if(sRes!=NULL)
        {
            char * sRes1;
            sRes1=strchr(sRes,'\r');
            if(strchr(sRes,' ')                sRes1=strchr(sRes,' ');
            if(sRes1!=NULL&&(sRes1-sRes)<256)
            {
                char tmp[256];
                strncpy(tmp,sRes,(sRes1-sRes));
                tmp[sRes1-sRes]=0;
                int i;
                for(i=strlen(tmp)-1;(tmp[i]==' '||tmp[i]=='\t')&&i>=0;i--)
                    tmp[i]=0;
                for(i=0;tmp[i]==' '||tmp[i]=='\t';i++);
                SendHtmlFile(rs,&tmp[i]);
            }
        }
        else
        {
            HtmlError(rs,202,"Bad request");
        }
        closesocket(rs);
    }

    return 0;
}


//A simplest web client
//Written by Shen zhiliang for learning Winsock & HTTP
//zhiliang@sina.com
//1998.7.29

#include "winsock.h"
#include "stdio.h"

#define BUFLEN 4096

void SocketError(char * call)
{
    fprintf(stderr," WinSock Error# function: %s, error code:%ld\n",call,WSAGetLastError());
}

main(int argc,char ** argv)
{
    int iRes,iPort,iTmp;
    SOCKET s,rs;
    SOCKADDR_IN sin,rsin;
    WSADATA wsad;
    WORD wVersionReq;
    char recvBuf[BUFLEN];
 
    if(argc<4)
    {
        fprintf(stderr,"Usage: sockserver ip port message\n");
        return -1;
    }
    if(inet_addr(argv[1])==INADDR_NONE)
    {
        fprintf(stderr,"Error ip gaving\n");
        return -1;
    }
    iPort=0;
    iPort=atoi(argv[2]);
    sin.sin_addr.s_addr=inet_addr(argv[1]);
    sin.sin_family=PF_INET;
    sin.sin_port=htons(iPort);
    if(iPort<=0)
    {
        fprintf(stderr,"must specify a number for port\n");
        return -1;
    }
    wVersionReq=MAKEWORD(1,1);
 
    iRes=WSAStartup(wVersionReq,&wsad);
    if(iRes!=0)
    {
        SocketError("WSAStartup()");
        return -1;
    }
 
    s=socket(PF_INET,SOCK_STREAM,0);
    if(s==INVALID_SOCKET)
    {
        SocketError("socket()");
        return -1;
    }
    iTmp=sizeof(sin);
    fprintf(stderr,"WinSock Client Start......\n");
    if(connect(s,(LPSOCKADDR)&sin,iTmp)==SOCKET_ERROR)
    {
        SocketError("connect()");
        closesocket(s);
        WSACleanup();
        return -1;
    }

    strcpy(recvBuf,argv[3]);
    strcat(recvBuf,"\r\n\r\n");
    iRes=send(s,recvBuf,strlen(recvBuf),0);
    if(iRes==SOCKET_ERROR)
    {
        SocketError("send()");
        closesocket(s);
        WSACleanup();
        return -1;
    }
    printf("Sent Data:\n------------------\n%s\n------------------\n",recvBuf);


    FILE * fp=fopen("send.dat","a+b");
    if(fp==NULL)
        return -1;
    iRes=recv(s,recvBuf,BUFLEN,0);
    while(iRes!=SOCKET_ERROR&&iRes!=0)
    {
        printf("Received Data:\n------------------\n%s\n------------------\n",recvBuf);
        fwrite(recvBuf,sizeof(char),iRes,fp);
        iRes=recv(s,recvBuf,BUFLEN,0);
    }
 
    fclose(fp);
    closesocket(s);
    WSACleanup();
    return 0;
}


Winsock函数用法说明

WSAStartup()
连结应用程序与Winsock.DLL 的第一个函数。
格 式:
  int WSAStartup( WORD wVersionRequested,LPWSADATA lpWSAData )
参 数:
  wVersionRequested 欲使用的 Windows Sockets API 版本
  lpWSAData 指向 WSADATA 资料的指标
传回值:
  成功 - 0
  失败 - WSASYSNOTREADY / WSAVERNOTSUPPORTED / WSAEINVAL
说明:
  此函数「必须」是应用程序呼叫到 Windows Sockets DLL 函数中的第一个函数呼叫成功后,才可以再呼叫其他 Windows Sockets DLL 的函数。此函数亦让使用者可以指定要使用的 Windows Sockets API 版本,及获取设计者的一些信息。

socket()
建立Socket。
格 式:
  SOCKET socket( int af, int type, int protocol )
参 数:
  af 目前只提供 PF_INET(AF_INET)
  type Socket 的型态 (SOCK_STREAM、SOCK_DGRAM)
  protocol 通讯协定(如果使用者不指定则设为0)
传回值:
  成功 - Socket 的识别码
  失败 - INVALID_SOCKET(呼叫 WSAGetLastError() 可得知原因)
说明:
  此函数用来建立一 Socket,并为此 Socket 建立其所使用的资源。Socket 的型态可为 Stream Socket 或 Datagram Socket。

bind()
指定 Socket 的 Local 地址 (Address)。
格 式:
  int bind( SOCKET s, const struct sockaddr FAR *name,int namelen );
参 数:
  s Socket的识别码
  name Socket的地址值
  namelen name的长度
传回值:
  成功 - 0
  失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)
说明:
  此一函数是指定 Local 地址及 Port 给某一未定名之 Socket。使用者若不在意地址或 Port 的值,那麽他可以设定地址为 INADDR_ANY,及 Port 为 0;那么Windows Sockets 会自动将其设定适当之地址及 Port (1024 到 5000之间的值),使用者可以在此 Socket 真正连接完成后,呼叫 getsockname() 来获知其被设定的值。
bind() 函数要指定地址及 port,这个地址必须是执行这个程序所在机器的 IP地址,所以如果读者在设计程序时可以将地址设定为 INADDR_ANY,这样Winsock 系统会自动将机器正确的地址填入。如果您要让程序只能在某台机器上执行的话,那么就将地址设定为该台机器的 IP 地址。由於此端是 Server 端,所以版主们一定要指定一个 port 号码给这个 socket。

listen()
设定 Socket 为监听状态,准备被连接。
格 式:
  int listen( SOCKET s, int backlog );
参 数:
  s Socket 的识别码
  backlog 未真正完成连接前(尚未呼叫 accept 前)彼端的连接要求的最大个数
传回值:
  成功 - 0
  失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)
说明:
  使用者可利用此函数来设定 Socket 进入监听状态,并设定最多可有多少个在未真正完成连接前的彼端的连接要求。(目前最大值限制为 5, 最小值为1)

connect()
要求连接某一 TCP Socket 到指定的对方。
格 式:
  int connect( SOCKET s, const struct sockaddr FAR *name, int namelen );
参 数:
  s Socket 的识别码
  name 此 Socket 想要连接的对方地址
  namelen name的长度
传回值:
  成功 - 0
  失败 - SOCKET_ERROR (呼叫WSAGetLastError()可得知原因)
说明:
  此函数用来向对方要求建立连接。若是指定的对方地址为 0 的话,会传回错误值。当连接建立完成后,使用者即可利用此一 Socket 来做传送或接收资料之用了。

accept()
接受某一 Socket 的连接要求,以完成 Stream Socket 的连接。
格 式:
  SOCKET accept(SCOKET s, SOCKADDR *addr,int FAR *addrlen )
参 数:
  s Socket的识别码
  addr 存放来连接的彼端的地址
  addrlen addr的长度
传回值:
  成功 - 新的Socket识别码
  失败 - INVALID_SOCKET (呼叫 WSAGetLastError() 可得知原因)
说明:
  Server 端的应用程序呼叫此一函数来接受 Client 端要求的 Socket 连接动作请求。

closesocket()
关闭某一Socket。
格 式:
  int closesocket( SOCKET s );
参 数:
  s Socket 的识别码
传回值:
  成功 - 0
  失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)
说明:
  此一函数是用来关闭某一 Socket 。

WSACleanup()
结束 Windows Sockets DLL 的使用。
格 式:
  int WSACleanup( void );
参 数: 无
传回值:
  成功 - 0
  失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)
说明:
  当应用程序不再需要使用Windows Sockets DLL 时,须呼叫此一函数来注销使用,以便释放其占用的资源。

send()
使用连接式(connected)的 Socket 传送资料。
格 式:
  int send( SOCKET s, const char FAR *buf, int len, int flags );
参 数:
  s Socket 的识别码
  buf 存放要传送的资料的暂存区
  len buf 的长度
  flags 此函数被呼叫的方式
传回值:
  成功 - 送出的资料长度
  失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)
说明:
  此函数用于将信息从本端通过socket发送到远程端。

recv()
自 Socket 接收资料。
格 式:
  int recv( SOCKET s, char FAR *buf, int len, int flags );
参 数:
  s Socket 的识别码
  buf 存放接收到的资料的暂存区
  len buf 的长度
  flags 此函数被呼叫的方式
传回值:
  成功 - 接收到的资料长度 (若对方 Socket 已关闭,则为 0)
  失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)
说明:
  此函数用来自连接式的 Datagram Socket 或 Stream Socket 接收资料。对 Stream Socket 言,版主们可以接收到目前 input buffer 内有效的资料,但其数量不超过 len 的大小。

WSAStartup()
连结应用程序与 Windows Sockets DLL 的第一个函数。
格 式:
  int WSAStartup( WORD wVersionRequested,LPWSADATA lpWSAData );
参 数:
  wVersionRequested 可使用的 Windows Sockets API 最高版本
  lpWSAData 指向 WSADATA 资料的指标
传回值:
  成功 - 0
  失败 - WSASYSNOTREADY / WSAVERNOTSUPPORTED / WSAEINVAL
说明:
  此函数「必须」是应用程序呼叫到 Windows Sockets DLL 函数中的第一个,也唯有此函数呼叫成功后,才可以再呼叫其他 Windows Sockets DLL 的函数。此函数亦让使用者可以指定要使用的 Windows Sockets API 版本,及获取设计者的一些信息。   接下来,我们就来看主从架构的 TCP socket 是如何利用这些 Winsock 函式来达成的;并利用资策会资讯技术处的「WinKing」这个 Winsock Stack 中某项功能来显示 sockets 状态的变化。文章中仅列出程式的片段,完整的程式请看附录的程式。Server进入监听状态
首先我们先看 Server 端如何建立一个 TCP socket,并使其进入监听等待的状态。在图 1. 上,我们可以看到最先被呼叫到的是 WSAStartup() 函式。WSAStartup
格  式: int PASCAL FAR WSAStartup( WORD wVersionRequested,  LPWSADATA lpWSAData );参  数:   wVersionRequested 欲使用的 Windows Sockets API 版本lpWSAData  指向 WSADATA 资料的指标传回值:   成功 – 0           失败 - WSASYSNOTREADY / WSAVERNOTSUPPORTED /  WSAEINVAL说明: 此函式「必须」是应用程式呼叫到 Windows Sockets DLL 函式中的第一个,也唯有此函式呼叫成功後,才可以再呼叫其他 Windows  Sockets DLL 的函式。此函式亦让使用者可以指定要使用的 Windows Sockets API 版本,及获取设计者的一些资讯。程式中我们要用 Winsock 1.1,所以我们在程式中有一段为:WSAStartup((WORD)((1<<8)|1),(LPWSADATA) &WSAData) 其中 ((WORD)((1<<8)|1) 表示我们要用的是 Winsock 「1.1」版本,而WSAData 则是用来储存由系统传回的一些有关此一 Winsock Stack 的资料。socket
再来我们呼叫 socket() 函式来开启 Server 端的 TCP socket。 socket():建立Socket。格 式: SOCKET PASCAL FAR socket( int af, int type, int protocol ); 参 数: af 目前只提供 PF_INET(AF_INET) type Socket 的型态 (SOCK_STREAM、SOCK_DGRAM)protocol 通讯协定(如果使用者不指定则设为0)传回值: 成功 - Socket 的识别码失败 - INVALID_SOCKET(呼叫 WSAGetLastError() 可得知原因)说明: 此函式用来建立一 Socket,并为此 Socket 建立其所使用的资源。Socket 的型态可为 Stream Socket 或 Datagram Socket。我们要建立的是 TCP socket,所以程式中我们的第二个参数为SOCK_STREAM,我们并将开启的这个 socket 号码记在 listen_sd 这个变数。listen_sd = socket(PF_INET, SOCK_STREAM, 0)bind
接下来我们要指定一个位址及 port 给 Server 的这个 socket,这样 Client 才知道待会要连接哪一个位址的哪个 port;所以我们呼叫 bind() 函式。bind():指定 Socket 的 Local 位址 (Address)。格式:int PASCAL FAR bind( SOCKET s, const struct sockaddr FAR *name,int namelen );参数:s:         Socket的识别码name:      Socket的位址值namelen:   name的长度传回值: 成功 – 0失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明: 此一函式是指定 Local 位址及 Port 给某一未定名之 Socket。使用者若不在意位址或 Port 的值,那麽他可以设定位址为 INADDR_ANY,及 Port 为 0;那麽Windows Sockets 会自动将其设定适当之位址及 Port (1024 到 5000之间的值),使用者可以在此 Socket 真正连接完成後,呼叫 getsockname() 来获知其被设定的值。bind() 函式要指定位址及 port,这个位址必须是执行这个程式所在机器的 IP位址,所以如果读者在设计程式时可以将位址设定为 INADDR_ANY,这样Winsock 系统会自动将机器正确的位址填入。如果您要让程式只能在某台机器上执行的话,那麽就将位址设定为该台机器的 IP 位址。由於此端是 Server 端,所以我们一定要指定一个 port 号码给这个 socket。读者必须注意一点,TCP socket 一旦选定了一个位址及 port 後,就无法再呼叫另一次 bind 来任意更改它的位址或 port。在程式中我们将 Server 端的 port 指定为 7016,位址则由系统来设定。struct sockaddr_in sa;sa.sin_family = PF_INET;sa.sin_port = htons(7016);      //port number sa.sin_addr.s_addr = INADDR_ANY;//addressbind(listen_sd, (struct sockaddr far *)&sa, sizeof(sa)) 我们在指定 port 号码时会用到 htons() 这个函式,主要是因为各机器的数值读取方式不同(PC与UNIX系统即不相同),所以我们利用这个函式来将 host order 的排列方式转换成 network order 的排列方式;相同地,我们也可以呼叫ntohs() 这个相对的函式将其还原。host order各机器不同,但network order都相同;htons是针对short数值,对於long数值则用hotnl及ntohl。listen
指定完位址及 port 之後,我们呼叫 listen() 函式,让这个 socket 进入监听状态。一个 Server 端的 TCP socket 必须在做完了 listen 的呼叫後,才能接受 Client 端的连接。格式:int PASCAL FAR listen( SOCKET s, int backlog );参数:s:         Socket 的识别码backlog:   未真正完成连接前(尚未呼叫 accept 前)彼端的连接要求的最大个数传回值:成功 – 0失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明: 使用者可利用此函式来设定 Socket 进入监听状态,并设定最多可有多少个在未真正完成连接前的彼端的连接要求。(目前最大值限制为 5, 最小值为1)程式中我们将 backlog 设为 1 。listen(listen_sd, 1)呼叫完 listen 後,此时 Client 端如果来连接的话,Client 端的连接动作(connect)会成功,不过此时 Server 端必须再呼叫 accept() 函式,才算正式完成Server 端的连接动作。但是我们什麽时候可以知道 Client 端来连接,而适时地呼叫 accept 呢?在这里我们就要利用 WSAAsyncSelect 函式,将Server 端的这个 socket 转变成 Asynchronous 模式,让系统主动来通知我们有Client 要连接了。WSAAsyncSelect
格式:int PASCAL FAR WSAAsyncSelect( SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent );参数:s:         Socket 的编号hWnd:      动作完成後,接受讯息的视窗 handlewMsg:      传回视窗的讯息lEvent:    应用程式有兴趣的网路事件传回值:成功 – 0失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明:此函式是让使用者用来要求 Windows Sockets DLL 在侦测到某一 Socket有网路事件时送讯息到使用者指定的视窗;网路事件是由参数 lEvent 设定。呼叫此函式会主动将该 Socket 设定为 Non-blocking 模式。lEvent 的值可为以下之「OR」组合:(参见 WINSOCK第1.1版88、89页) FD_READ、FD_WRITE、FD_OOB、FD_ACCEPT、FD_CONNECT、FD_CLOSE 使用者若是针对某一Socket再次呼叫此函式时,会取消对该 Socket 原先之设定。若要取消对该Socket 的所有设定,则 lEvent 的值必须设为 0。我们在程式中要求 Winsock 系统知道 Client 要来连接时,送一个ASYNC_EVENT 的讯息到程式中 hwnd 这个视窗;由於我们想知道的只有 accept事件,所以我们只设定 FD_ACCEPT。WSAAsyncSelect(listen_sd, hwnd, ASYNC_EVENT, FD_ACCEPT) 读者必须注意一点,WSAAsyncSelect 的设定是针对「某一个 socket」;也就是说,只有当您设定的这个 socket (listen_sd)的那些事件(FD_ACCEPT)发生时,您才会收到这个讯息(ASYNC_EVENT)。如果您开启了很多 sockets,而要让每个 socket 都变成 asynchronous 模式的话,那麽就必须对「每一个 socket」都呼叫 WSAAsyncSelect 来一一设定。而如果您想将某一个 socket 的 async 事件通知设定取消的话,那麽同样也是用 WSAAsyncSelect 这个函式;且第四个参数lEvent 一定要设为 0。WSAAsyncSelect( s, hWnd, 0, 0 ) -- 取消所有 async 事件设定呼叫 WSAAsyncSelect 的同时也将此socket改变成「非阻拦」(non-blocking)模式。但是此时这个 socket 不能很简单地用 ioctlsocket() 这个函式就将它再变回「阻拦」(blocking)模式。也就是说WSAAsyncSelect 和 ioctlsocket 所改变的「非阻拦」模式仍是有些不同的。如果您想将一个「非同步」(asynchronous)模式的 socket 再变回「阻拦」模式的话,必须先呼叫 WSAAsyncSelect() 将所有的 async 事件取消,再用 ioctlsocket() 将它变回阻拦模式。ioctlsocket
ioctlsocket():控制 Socket 的模式。格 式: int PASCAL FAR ioctlsocket( SOCKET s, long cmd, u_long FAR * argP );参 数:    s Socket 的识别码cmd 指令名称argP 指向 cmd 参数的指标传回值:   成功 – 0失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明: 此函式用来获取或设定 Socket 的运作参数。其所提供的指令有:(参见WINSOCK 第 1.1 版 35、36 页) cmd 的值可为:FIONBIO -- 开关 non-blocking 模式//允许或禁止套接字的非阻塞模式,允许为非0,禁止为0FIONREAD -- 自Socket一次可读取的资料量(目前 in buffer 的资料量//确定套接字自动读入的数据量SIOCATMARK -- OOB 资料是否已被读取完//确定是否所有带外数据都已被读入由於我们 Server 端的 socket 是用非同步模式,且设定了 FD_ACCEPT 事件,所以当 Client 端和我们连接时,Winsock Stack 会主动通知我们;我们再先来看看Client 端要如何和 Server 端建立连接?Client主动建立连接
Client 首先也是呼叫 WSAStartup() 函式来与 Winsock Stack 建立关系;然後同样呼叫 socket() 来建立一个 TCP socket。(读者此时一定要用 TCP socket 来连接Server 端的 TCP socket,而不能用 UDP socket 来连接;因为相同协定的 sockets 才能相通,TCP 对 TCP,UDP 对 UDP)和 Server 端的 socket 不同的地方是:Client 端的 socket 可以呼叫 bind()函式,由自己来指定 IP 位址及 port 号码;但是也可以不呼叫 bind(),而由 Winsock Stack来自动设定 IP 位址及 port 号码(此一动作在呼叫 connect() 函式时会由 Winsock 系统来完成)。通常我们是不呼叫 bind(),而由系统设定的,稍後可呼叫getsockname() 函式来检查系统帮我们设定了什麽 IP 及 port。一般言,系统会自动帮我们设定的 port 号码是在 1024 到 5000 之间;而如果读者要自己用 bind设定 port的话,最好是 5000 以上的号码。connect():要求连接某一 TCP Socket 到指定的对方。格 式: int PASCAL FAR connect( SOCKET s, const struct sockaddr FAR *name, int namelen );参 数: s Socket 的识别码name 此 Socket 想要连接的对方位址namelen name的长度传回值: 成功 – 0失败 - SOCKET_ERROR (呼叫WSAGetLastError()可得知原因) 说明: 此函式用来向对方要求建立连接。若是指定的对方位址为 0 的话,会传回错误值。当连接建立完成後,使用者即可利用此一 Socket 来做传送或接收资料之用了。我们的例子中, Client 是要连接的是自己机器上 Server 所监听的 7016 这个port,所以我们有以下的程式片段。(假设我们机器的 IP 存在my_host_ip)struct sockaddr_in sa; /* 变数宣告 */
sa.sin_family = PF_INET; /* 设定所要连接的 Server 端资料 */sa.sin_port = htons(7016); sa.sin_addr.s_addr = htonl(my_host_ip); connect(mysd, (struct sockaddr far *)&sa, sizeof(sa)) /* 建立连接 */ Server接受连接
由於我们 Server 端的 socket 是设定为「非同步模式」,且是针对 FD_ACCEPT这个事件,所以当 Client 来连接时,我们 Server 端的 hwnd 这个视窗会收到Winsock Stack 送来的一个 ASYNC_EVENT 的讯息。(参见前面 WSAAsyncSelect 的设定)这时,我们应该先利用 WSAGETSELECTERROR(lParam) 来检查是否有错误;并由 WSAGETSELECTEVENT(lParam) 得知是什麽事件发生(因为WSAAsyncSelect 函式可针对同一个 socket 同时设定很多事件,但是只用一个讯息来代表)(此处当然是 FD_ACCEPT 事件);然後再呼叫相关的函式来处理此一事件。所以我们呼叫 accept() 函式来建立 Server 端的连接。accept():接受某一 Socket 的连接要求,以完成 Stream Socket 的连接。格 式: SOCKET PASCAL FAR accept( SCOKET s, struct sockaddr FAR *addr, int FAR *addrlen );参 数: s Socket的识别码addr 存放来连接的彼端的位址addrlen addr的长度传回值:成功 - 新的Socket识别码失败 - INVALID_SOCKET (呼叫 WSAGetLastError() 可得知原因)说明: Server 端之应用程式呼叫此一函式来接受 Client 端要求之 Socket 连接动作;如果Server 端之 Socket 是为 Blocking 模式,且没有人要求连接动作,那麽此一函式会被 Block 住;如果为 Non-Blocking 模式,此函式会马上回覆错误。accept()函式的答覆值为一新的 Socket,此新建之 Socket 不可再用来接受其它的连接要求;但是原先监听之 Socket 仍可接受其他人的连接要求。TCP socket 的 Server 端在呼叫 accept() 後,会传回一个新的 socket 号码;而这个新的 socket 号码才是真正与 Client 端相通的 socket。比如说,我们用socket() 建立了一个 TCP socket,而此 socket 的号码(系统给的)为 1,然後我们呼叫的bind()、listen()、accept() 都是针对此一 socket;当我们在呼叫 accept()後,传回值是另一个 socket 号码(也是系统给的),比如说 3;那麽真正与 Client 端连接的是号码 3 这个 socket,我们收送资料也都是要利用 socket 3,而不是 socket 1;读者不可搞错。我们在程式中对 accept() 的呼叫如下;我们并可由第二个参数的传回值,得知究竟是哪一个 IP 位址及 port 号码的 Client 与我们 Server 连接。struct sockaddr_in sa; int sa_len = sizeof(sa); my_sd = accept(listen_sd, (struct sockaddr far *)&sa, &sa_len) 当 Server 端呼叫完 accept() 後,主从架构的 TCP socket 连接才算真正建立完毕; Server 及 Client 端也就可以分别利用此一 socket 来送资料到对方或收对方送来的资料了。Server/Client结束连接
最後我们来看一下如何结束 socket 的连接。socket 的关闭很简单,而且可由Server 或 Client 的任一端先启动,只要呼叫 closesocket() 就可以了。而要关闭监听状态的 socket,同样也是利用此一函式。closesocket():关闭某一Socket。格 式: int PASCAL FAR closesocket( SOCKET s ); 参 数: s Socket 的识别码传回值: 成功 – 0失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明: 此一函式是用来关闭某一 Socket。若是使用者原先对要关闭之 Socket 设定 SO_DONTLINGER,则在呼叫此一函式後,会马上回覆,但是此一 Sokcet 尚未传送完毕的资料会继续送完後才关闭。若是使用者原先设定此 Socket 为 SO_LINGER,则有两种情况:(a) Timeout 设为 0 的话,此一 Socket 马上重新设定 (reset),未传完或未收到的资料全部遗失。(b) Timeout 不为 0 的话,则会将资料送完,或是等到 Timeout 发生後才真正关闭。程式结束前,读者们可千万别忘了要呼叫 WSACleanup() 来通知 WinsockStack;如果您不呼叫此一函式,Winsock Stack 中有些资源可能仍会被您占用而无法清除释放哟。WSACleanup():结束 Windows Sockets DLL 的使用。格 式: int PASCAL FAR WSACleanup( void );参 数: 无传回值: 成功 – 0失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明: 应用程式在使用 Windows Sockets DLL 时必须先呼叫WSAStartup() 来向 Windows Sockets DLL 注册;当应用程式不再需要使用Windows Sockets DLL 时,须呼叫此一函式来注销使用,以便释放其占用的资源。结语
这期笔者先介绍主从架构 TCP sockets 的连接及关闭,以後会再陆续介绍如何收送资料,以及其他 API 的使用。想要进一步了解如何撰写 Winsock 程式的读者,可以好好研究一下笔者 demoserv 及 democlnt 这两个程式;也许不是写的很好,但是希望可以带给不懂 Winsock 程式设计的人一个起步。读者们亦可自行用 anonymous ftp 方式到 SEEDNET 台北主机 tpts1.seed.net. tw(139.175.1.10)的 UPLOAD / WINKING 目录下,取得笔者与陈建伶小姐所设计的WinKing 这个 Winsock Stack 的试用版,来跑 demoserv 与 democlnt 这两个程式及其他许许多多的 Winsock 应用程式。(正式版本请洽 SEEDNET 服务中心,新版的WinKing 已含 Windows 拨接及 PPP 程式,适合电话拨接用户在 Windows 环境下用 SEEDNET;WinKing 同样也提供 Ethernet 环境的使用。) 收送资料
在前一期的文章中,笔者为大家介绍了如何在 Winsock 环境下建立主从架构(Client/Server)的 TCP socket 的连接建立与关闭;今天笔者将继续为大家介绍如何利用 TCP socket 来收送资料,并详细解说 WSAAsyncSelect 函式中的FD_READ 及 FD_WRITE 事件。相信读者们已经知道 TCP socket 的连接是在 Client 端呼叫 connect 函式成功,且 Server 端呼叫 accept 函式後,才算完全建立成功;当连接建立成功後, Client 及 Server 也就可以利用这个连接成功的 socket 来传送资料到对方,或是收取对方送过来的资料了。在介绍资料的收送前,笔者先介绍一下 TCP socket 与 UDP socket 在传送资料时的特性:²           Stream (TCP) Socket 提供「双向」、「可靠」、「有次序」、「不重覆」之资料传送。²           Datagram (UDP) Socket 则提供「双向」之沟通,但没有「可靠」、「有次序」、「不重覆」等之保证;所以使用者可能会收到无次序、重覆之资料,甚至资料在传输过程中也可能会遗漏。由於 UDP Socket 在传送资料时,并不保证资料能完整地送达对方,所以我们常用的一些应用程式(如 telnet、mail、ftp、news...等)都是采用 TCP Socket,以保证资料的正确性。TCP 及 UDP Socket 都是双向的,所以我们是利用同一个 Socket 来做传送及收取资料的动作;一般言 TCP Socket 的资料送、收是呼叫 send 及 recv这两个函式来达成,而 UDP Socket 则是用 sendto 及 recvfrom 这两个函式。不过TCP Socket 也可用 sendto 及 recvfrom 函式,UDP Socket 同样可用 send 及recv 函式;这一点我们稍後再加以解释。现在我们先看一下 send 及 recv 的函式说明,并回到我们的前一期程式。◎ send():使用连接式(connected)的 Socket 传送资料。格 式: int PASCAL FAR send( SOCKET s, const char FAR *buf, int len, int flags );参 数: s Socket 的识别码buf 存放要传送的资料的暂存区len buf 的长度flags 此函式被呼叫的方式传回值:成功 - 送出的资料长度失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明: 此函式适用於连接式的 Datagram 或 Stream Socket 来传送资料。 对Datagram Socket 言,若是 datagram 的大小超过限制,则将不会送出任何资料,并会传回错误值。对 Stream Socket 言,Blocking 模式下,若是传送 (transport) 系统内之储存空间(output buffer)不够存放这些要传送的资料,send 将会被 block住,直到资料送完为止;如果该 Socket 被设定为 Non-Blocking 模式,那麽将视目前的 output buffer 空间有多少,就送出多少资料,并不会被 block 住。使用者亦须注意 send 函式执行完成,并不表示资料已经成功地送抵对方了,而是已经放到系统的 output buffer 中等待被送出。flags 的值可设为 0 或 MSG_DONTROUTE及 MSG_OOB 的组合。(参见 WINSOCK第1.1版48页)◎ recv():自 Socket 接收资料。格 式: int PASCAL FAR recv( SOCKET s, char FAR *buf, int len, int flags );参 数: s Socket 的识别码buf 存放接收到的资料的暂存区len buf 的长度flags 此函式被呼叫的方式传回值:成功 - 接收到的资料长度 (若对方 Socket 已关闭,则为 0)失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明: 此函式用来自连接式的 Datagram Socket 或 Stream Socket 接收资料。对 Stream Socket 言,我们可以接收到目前 input buffer 内有效的资料,但其数量不超过 len 的大小。若是此 Socket 设定 SO_OOBINLINE,且有 out-of-band 的资料未被读取,那麽只有 out-of-band 的资料被取出。对 Datagram Socket 言,只取出第一个 datagram;若是该 datagram 大於使用者提供的储存空间,那麽只有该空间大小的资料被取出,多馀的资料将遗失,且回覆错误的讯息。另外如果 Socket为 Blocking 模式,且目前 input buffer 内没有任何资料,则 recv() 将 block 到有任何资料到达为止;如果为 Non-Blocking 模式,且 input buffer 无任何资料,则会马上回覆错误。参数 flags 的值可为 0 或 MSG_PEEK、MSG_OOB 的组合; MSG_PEEK 代表将资料拷贝到使用者提供的 buffer,但是资料并不从系统的 input buffer 中移走;0 则表示拷贝并移走。(参考 WINSOCK 第1.1版41 页)Server收送及关闭Socket
在前一期中,建立的是一个 Asynchronous 模式的 Server,曾对listen_sd Socket呼叫 WSAAsyncSelect 函式,并设定FD_ACCEPT 事件,所以当 Client 与我们连接时,系统会传给我们一个ASYNC_EVENT 讯息;我们在收到讯息并判断是FD_ACCEPT 事件,於是呼叫 accept() 来建立连接。my_sd = accept(listen_sd, (struct sockaddr far *)&sa, &sa_len) 在呼叫完 accept 函式,成功地建立了 Server 端与 Client 端的连接後,便可利用新建的 Socket(my_sd)来收送资料了。由於我们同样希望用Asynchronous 的方式,因此要再利用 WSAAsyncSelect() 函式来帮新建的Socket 设定一些事件,以便事件发生时 Winsock Stack 能主动通知我们。由於我们的 Server 是被动的接受 Client 的要求,
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/msgsnd/archive/2008/03/06/2153768.aspx