简单的WINSOCK2应用程序

         Tcp应用介绍

        首先大家对WINSOCK应该都有一些了解吧! Winsock 的定义、系统环境,以及一些 Winsock Stack及 Winsock 应用程式都要有一定的认知。接下来就来简单讲解一下Tcp网络连接的程式设计。我是以WINSOCK2.2为基础学习的规定的应用程式界面(API),逐步来建立TCP socket主/从构架。首先讲的是同步模式,通俗点来说打电话就是同步行为,发短信就是异步行为。

       今天我们先简单了解一下Tcp的主从构架如何建立连接(connect)及关闭(close)。就从打电话来讲述一下他的原理。我们假设Server就像电信局所提供的一些服务,比如[10000]或[104查号台]。

(1)电信局首先要建立好一个电话总机,这就像呼叫socket()启动一个socket。

(2)接着电信局把端口号设置为104,就如同bind()函式,将Server的socket绑定到这个端口上,并且通知所有用户这个号是用来干什么的,Client如果有这方面的需求就访问这个,即于此端口连接。

(3)电信局的104查号台会有一些自动的服务分机,数量是有限的,所以有时你会拨不通这个号码(忙线)(这就是同步的阻塞)。同样地,我们建立一个TCPServersocket时,就需要用listen()来监听等待;listen()的第二个参数为Waiting queue的数目,通常数值为1~5。

(4)Client如果使用104查号服务,Client就通过Connect()函式去连接Server指定的Port,如果waiting queue占满,会通知你线路繁忙,请稍后再试。

(5)电信局查号台接受这通查询的电话后,它就会转到另一个分机做服务,而总机本身则回到等待的状态。Server的listen socket也一样。当你呼叫了accept()后,Server端的系统会建立一个新的socket来对此连接做服务,而原先的socket则回到监听等待的状态。

(6)当查询完毕后,就可以直接挂断。Client和Server间的socket都将关闭;只要有一方关闭,Client和Server都将关闭。

        首先要调用socket()必须先调用WSAStartup(),他就像电话的开关按钮。必须先拿起话筒才能拨号通话。

WSAStartup

格  式: int WSAStartup( WORD wVersionRequested,  LPWSADATA lpWSAData );

参  数:   wVersionRequested 欲使用的 Windows Sockets API 版本

lpWSAData  指向 WSADATA 资料的指标

传回值:   成功 – 0

           失败 - WSASYSNOTREADY / WSAVERNOTSUPPORTED /  WSAEINVAL

       

        WSASYSNOTREADY 指出网络通信依赖的网络子系统还没有准备好。
        WSAVERNOTSUPPORTED 所需的Windows Sockets API的版本未由特定的Windows Sockets实现提供。
        WSAEINVAL 应用程序 指出的Windows Sockets版本不被该DLL支持。

       为了在应用程序当中调用任何一个Winsock API函数,首先第一件事情就是必须通过WSAStartup函数完成对Winsock服务的初始化,因此需要调用WSAStartup函数。使用Socket的程序在使用Socket之前必须调用WSAStartup函数。该函数的第一个参数指明程序请求使用的Socket版本,其中高位字节指明副版本、低位字节指明主版本;操作系统利用第二个参数返回请求的Socket的版本信息。当一个应用程序调用WSAStartup函数时,操作系统根据请求的Socket版本来搜索相应的Socket库,然后绑定找到的Socket库到该应用程序中。以后应用程序就可以调用所请求的Socket库中的其它Socket函数了。

        wVersionRequested  = MAKEWORD( 2, 2);用这种方法可以获得双字节,也可以直接用16直接表示0x0202;

       下面就是Tcp套接字建立 SOCKET PASCAL FAR socket( int af, int type, int protocol );     

        af主要用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 这个变数。

       SOCKET Skt  =  socket(AF_INET,SOCK_STREAM,IPPROTO_TCP)

       BIND

        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。读者必须注意一点,TCP socket 一旦选定了一个位址及 port 後,就无法再呼叫另一次 bind 来任意更改它的位址或 port。在程式中我们将 Server 端的 port 指定为 7016,位址则由系统来设定。

sockaddr_in sa;

sa.sin_family = AF_INET;

sa.sin_port = htons(7016);      //port number

sa.sin_addr.s_addr = INADDR_ANY;//address

bind(Skt, (sockaddr)&sa, sizeof(sa))

我们在指定 port 号码时会用到 htons() 这个函式,主要是因为各机器的数值读取方式不同(PC与UNIX系统即不相同),所以我们利用这个函式来将 host order 的排列方式转换成 network order 的排列方式;相同地,我们也可以呼叫ntohs() 这个相对的函式将其还原。网络地址如果输入的话,需要用到inet_addr,即将一个点分十进制的IP转换成一个长整数型数(u_long类型)。

listen

指定完位址及 port 之後,我们呼叫 listen() 函式,让这个 socket 进入监听状态。一个 Server 端的 TCP socket 必须在做完了 listen 的呼叫後,才能接受 Client 端的连接。

格式:

int  listen( SOCKET s, int backlog );

参数:

s:         Socket 的识别码

backlog:   未真正完成连接前(尚未呼叫 accept 前)彼端的连接要求的最大个数

传回值:

成功 – 0

失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)

说明: 使用者可利用此函式来设定 Socket 进入监听状态,并设定最多可有多少个在未真正完成连接前的彼端的连接要求。(目前最大值限制为 5, 最小值为1)程式中我们将 backlog 设为 1 。

listen(SKt,1)

呼叫完 listen 後,此时 Client 端如果来连接的话,Client 端的连接动作(connect)会成功,不过此时 Server 端必须再呼叫 accept() 函式,才算正式完成Server 端的连接动作。但是我们什麽时候可以知道 Client 端来连接,而适时地呼叫 accept 呢?在这里我们就要利用 WSAAsyncSelect 函式,将Server 端的这个 socket 转变成 Asynchronous 模式,让系统主动来通知我们有Client 要连接了。

 

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 来做传送或接收资料之用了。

我们的例子中, Client 是要连接的是自己机器上 Server 所监听的 10001 这个port,所以我们有以下的程式片段。(假设我们机器的 IP 存在my_host_ip)

struct sockaddr_in sa;
sa.sin_family = AF_INET;
sa.sin_port = htons(10001);

sa.sin_addr.s_addr = htonl(127.0.0.1);

connect(Skt, (struct sockaddr far *)&sa, sizeof(sa))

Server端,用Accept一直在等待着连接。

格 式: SOCKET 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);

ClientSkt = accept(Skt, (struct sockaddr far *)&sa, &sa_len)

当 Server 端呼叫完 accept() 後,主从架构的 TCP socket 连接才算真正建立完毕; Server 及 Client 端也就可以分别利用此一 socket 来送资料到对方或收对方送来的资料了。

          获得SOCKET ClientSkt,就可以应用此套接字,来和Client通信recv和send。

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

 s:一个用于标识已连接套接口的描述字。

 buf:包含待发送数据的缓冲区

 len:缓冲区中数据的长度。

 flags:调用执行方式。一般设为0.

 说明:如果无错误,返回值为所发送数据的总数,否则返回SOCKET_ERROR。 

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

 s:一个标识已连接套接口的描述字。

 buf:用于接收数据的缓冲区

 len:缓冲区长度。

 flags:指定调用方式。一般设为0.

 说明:如果成功则nRecv == sizeof(buf),否则返回SOCKET_ERROR。 

        这里只描述同步Socket的recv函数的执行流程。当 应用程序 调用recv函数时:
(1)recv先等待s的发送缓冲中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR;
(2)如果s的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv先检查套接字s的接收 缓冲区 ,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的);
       recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
       具体实现可以用一个while(1)死循环试一下,可以不断的发送和接收,当然记得考虑阻塞问题。

 

 

 

 

 

 

你可能感兴趣的:(简单的WINSOCK2应用程序)