[转帖]简单的 Winsock 应用程序设计(3)

**************************************************************Copyright by: 林军鼐本文稿非经本人同意,不得任意刊载于任何书报杂志或做为商业用途**************************************************************
    
    简单的 Winsock 应用程序设计(3)
    
    林 军 鼐
    
    在前两期的文章中,笔者介绍了如何在 Winsock 环境下建立主从架构的TCP Socket,以及如何利用 Socket 来收送资料;今天,我们接着来看一看如何利用 Winsock 所提供的函式来取得一些基本的网络资料,包括我们本身主机的名称是什么、系统主动指定给我们的 Socket 的 IP 地址及 port number、我们的Socket 所连接的对方是谁、如何查得某些主机的 IP 地址或名称、以及某些well-known 服务(如 ftp、telnet 等)所用的 port number 是哪一个等等。
    
    今天我们使用的展示程序是笔者以前所撰写的一个针对 Winsock 1.1 的 46个函式做测试或教学用的程序,有兴趣了解 46 个函式该如何呼叫的读者,可用anonymous ftp 方式到 「tpts1.seed.net.tw」 的 「UPLOAD/WINKING/JNLIN」目录下取得此程序的执行文件及原始程序代码,档名为 hello.*。读者们也可利用hello 程序来仿真 Server 或 Client 程序,以验证我们所做的动作。
    
    【如何知道我们所使用的 local 主机名称】
    
    通常我们都会帮我们自己所使用的这台主机设定一个名称;在程序中,我们也可以透过 Winsock 所提供的一个称为 gethostname() 的函式来取得这一个主机名称。
    
    ◎ gethostname():获取目前使用者使用的 local host 的名称。格 式: int PASCAL FAR gethostname( char FAR *name, int namelen );参 数: name 用来存放 local host 名称的暂存区namelen name 的大小传回值: 成功 - 0失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明: 此函式用来获取 local host 的名称。
    
    在程序中我们呼叫的方法如下:
    
    gethostname( (char FAR *) hname, sizeof(hname) )
    
    读者们如果使用过 Trumpet Winsock 的话,可能知道 Trumpet 的环境设定中并没有让我们设定 local host 名称的字段,所以在执行一些 Public Domain 的Winsock 应用程序(如 ws_ping、wintalk)时,在呼叫 gethostname() 时会产生错误;解决的方法是在 Trumpet 的 「hosts」 文件中加上您的主机 IP 地址及名称,那么呼叫这个函式时就不会再产生错误了。
    
    【如何得知系统主动指定给我们的 IP 地址及 port number】
    
    以前的文章中,笔者曾提到 Client 端的 TCP Socket 在呼叫 connect() 函式去连接 Server 端之前,可以呼叫 bind() 函式来指定 Client 端 Socket 所用的 IP 地址及 port number;但是一般而言,我们 Client 端并不需要呼叫 bind() 来指定特定的 IP 地址及 port number 的,而是交由系统主动帮我们的 Socket 设定 IP 地址及port number (呼叫 connect() 函式时)。但是我们如何得知系统指定了什么 IP地址及 port number 给我们呢?这就要借助 getsockname() 这个函式了。
    
    ◎ getsockname():获取 Socket 的 Local 地址及 port number 资料。格式: int PASCAL FAR getsockname( SOCKET s,struct sockaddr FAR *name, int FAR *namelen );参 数: s Socket 的识别码name 存放此 Socket 的 Local 地址的暂存区namelen name 的长度传回值: 成功 - 0失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明: 此函式是用来取得已设定地址或已连接之 Socket 的本端地址资料。若是此 Socket 被设定为 INADDR_ANY,则需等真正建立连接成功后才会传回正确的地址。
    
    在程序中呼叫的方法为:
    
    struct sockaddr_in sa;int salen = sizeof(sa);getsockname( sd, (struct sockaddr FAR *)&sa, &salen )
    
    【如何知道和我们的 Socket 连接的对方是谁】
    
    连接的 Socket 是有两端的,所以相对于 getsockname() 函式,Winsock 也提供了一个 getpeername() 函式,来让我们获得与我们连接的对方的 IP 地址与 portnumber。
    
    ◎ getpeername():获取连接成功之 Socket 的对方 IP 地址及 port number。格 式: int PASCAL FAR getpeername( SOCKET s,struct sockaddr FAR *name, int FAR *namelen );参 数: s Socket 的识别码name 储存与此 Socket 连接的对方 IP 地址的暂存区namelen name 的长度传回值: 成功 - 0失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明: 此函式可用来取得已连接成功的 Socket 的彼端之地址资料。
    
    呼叫的方式如下:
    
    struct sockaddr_in sa;int salen = sizeof(sa);getpeername( sd, (struct sockaddr FAR *)&sa, &salen )
    
    现在我们仍然利用 WinKing 来当我们的 Winsock Stack,并利用它所提供的工具来观察 Sockets 的连结及资料是否正确。
    
    由图 1,我们可以由 WinKing 的窗口看到我们设定这台主机的名称是「vincent」,IP 地址是 「140.92.61.24」。我们并利用两个 hello 程序,一个当成 Client (画面右边打开者),一个当成 Server (画面左边最小化者)。Server所用的 port number 是 「7016」; Client 并没有呼叫 bind() 来指定 portnumber,而是呼叫 connect() 时由系统指定。
    
    我们呼叫 gethostname(),得到的答案是 「vincent」;而 Client 呼叫getsockname() 得到自己的 IP 地址是 「140.92.61.24」,port number 是 「2110」(笔者以前曾提过,由系统主动指定的 port number 会介于 1024 到 5000 间);再呼叫 getpeername() 得到与 Client 连接的 Server 端 IP 地址是 「140.92.61.24」(因为我们的 Client 和 Server 都在同一台主机),port number 是 「7016」。果然没错!(由 WinKing 的 Sockets' Status 窗口亦可观察到相互连接的 Sockets 资料,与我们呼叫函式所得结果相同)
    
    (图 1)利用 hello 程序来仿真 Client 和 Server
    
    读者必须注意一点,getsockname() 及 getpeername() 所取得的 IP 地址及 portnumber 都是 network byte order,而不是 host byte order;如果您想转成 host byteorder,就必须借助 ntohl() 及 ntohs() 两个函式。而我们能看到 IP 地址以「字串」方式表达出来,则又是利用了 inet_ntoa() 函式;相对地,我们也可利用inet_addr() 函式将字符串方式的 IP 地址转换成 in_addr 格式(network byte order 的unsigned long)。
    
    ◎ inet_ntoa():将一网络地址转换成「点格式」字符串。格 式: char FAR * PASCAL FAR inet_ntoa( struct in_addr in );参 数: in 一个代表 Internet host 地址的结构传回值: 成功 - 一个代表地址的「点格式」(dotted) 字符串失败 - NULL说明: 此函式将一 Internet 地址转换成「a.b.c.d」字符串格式。
    
    ◎ inet_addr():将字符串格式的地址转换成 32 位 in_addr 的格式。格 式: unsigned long PASCAL FAR inet_addr( const char FAR *cp );参 数: cp 一个代表 IP 地址的「点格式」(dotted) 字符串传回值: 成功 - 一个代表 Internet 地址的 unsigned long失败 - INADDR_NONE说明: 此函式将一「点格式」的地址字符串转换成适用之 Intenet 地址。「点格式」字符串可为以下四种方式之任一:(i) a.b.c.d (ii) a.b.c (iii) a.b (iv)
    
    a图 1 的 hello 程序中,我们将 Local 资料写到 dispmsg 中,再显示出来;其用法如下:
    
    wsprintf((LPSTR)dispmsg, "OK! local ip=%s, local port=%d",inet_ntoa(sa.sin_addr), ntohs(sa.sin_port));
    
    【Winsock 提供的数据库函式】
    
    Winsock 也提供了同步与异步的网络数据库函式;不过读者们要知道,此处的数据库指的并非如 Informix, Oracle 等商业用途的数据库系统,而是指主机IP 地址及名称、well-known 服务的名称及 Socket 型态及所用的 port number、以及协议(protocol)名称及代码等。
    
    【同步数据库函式】
    
    首先我们来看一下第一组:gethostbyname() 及 gethostbyaddr() 函式
    
    这两个函式的用途是让我们可以由某个主机名称求得它的 IP 地址,或是由它的 IP 地址求得它的名称。一般我们经常会用到的是由名称求得 IP 地址;因为很少人会去记某台机器的 IP 地址的,另外 TCP/IP 封包的 IP header 上也必须记载送、收主机的 IP 地址,而不是主机名称。
    
    ◎ gethostbyname():利用某一 host 的名称来获取该 host 的资料。格 式: struct hostent FAR * PASCAL FARgethostbyname( const char FAR *name );参 数: name host 的名称传回值: 成功 - 指向一个 hostent 结构的指针失败 - NULL (呼叫 WSAGetLastError() 可得知原因)说明: 此函式是利用 host 名称来获取该主机的其它资料,如 host 的地址、别名,地址的型态、长度等。
    
    ◎ gethostbyaddr():利用某一 host 的 IP 地址来获取该 host 的资料。格 式: struct hostent FAR * PASCAL FARgethostbyaddr( const char FAR *addr, int len, int type );参 数: addr network 排列方式的地址len addr 的长度type PF_INET(AF_INET)传回值: 成功 - 指向一个 hostent 结构的指针失败 - NULL (呼叫 WSAGetLastError() 可得知原因)说明: 此函式是利用 IP 地址来获取该主机的其它资料,如 host 的名称、别名,地址的型态、长度等。
    
    程序中呼叫的方式分别如下:
    
    char host_name[30];struct hostent far *htptr;/* 假设 host_name 的值已先设定为我们要求得资料的主机名称 */htptr = (struct hostent FAR *) gethostbyname( (char far *) host_name )
    
    struct in_addr host_addr;struct hostent far *htptr;/* 假设 host_addr 的值已先设定为我们要求得资料的主机的network byteorder 方式的 IP 地址*/htptr = (struct hostent FAR *) gethostbyaddr((char far *)&host_addr, 4,PF_INET)
    
    一般言,程序中呼叫到 gethostbyname() 及 gethostbyaddr() 时,WinsockStack 会先在 local 的 「hosts」档中找看看是否有这个主机的资料;如果没有,则可能再透过「领域名称服务」(Domain Name Service)的功能,向「名称伺服器」(Name Server)查询;所以呼叫这两个函式时,有时会等一下子才获得答复。如果您想让程序执行快一些的话,可将常用主机的资料放在 hosts 文件中,这样就不必透过 DNS 去查询了。
    
    接下来我们来看 getservbyname() 及 getservbyport() 这两个函式。
    
    大部份的读者应该都用过 telnet、mail、ftp、news 等服务应用程序;这些应用程序的协议,比如服务名称、服务器端所用的 port number、以及 Socket 的型态,都是固定的;这些资料,我们就可以利用 getservbyname() 或 getservbyport()来取得,而不必刻意去记颂它们。
    
    ◎ getservbyname():依照服务 (service) 名称及通讯协议(tcp/udp)来获取该服务的其它资料。格 式: struct servent * PASCAL FARgetservbyname( const char FAR *name, const char FAR *proto );参 数: name 服务名称proto 通讯协议名称传回值: 成功 - 一指向 servent 结构的指针失败 - NULL (呼叫 WSAGetLastError() 可得知原因)说明: 利用服务名称及通讯协议来获得该服务的别名、使用的 port 号码等。
    
    ◎ getservbyport():依照服务 (service) 的 port 号码及通讯协议(tcp/udp)来获取该服务的其它资料。格 式: struct servent * PASCAL FARgetservbyport( int port, const char FAR *proto );参 数: port 服务的 port 编号proto 通讯协议名称传回值: 成功 - 一指向 servent 结构的指针失败 - NULL (呼叫 WSAGetLastError() 可得知原因)说明: 利用 port 编号及通讯协议来获得该服务的名称、别名等。
    
    程序中的使用方法分别为:
    
    char serv_name[20];char proto[10];struct servent far *svptr;/* 假设 serv_name 及 proto 已先设好服务名称及通讯协议 */svptr = (struct servent FAR *)getservbyname( (char far *)serv_name, (char far*)proto )
    
    int serv_port;char proto[10];struct servent far *svptr;/* 假设 serv_port 及 proto 已先设好服务所用的 port number 及通讯协议 */svptr = (struct servent FAR *)getservbyport( htons(serv_port), (char far *)proto) )
    
    Winsock 环境下,我们能够查询到的服务资料都是存放在 local 的「services」檔中;这个档所存放的都是 well-known 的服务,基本上我们是不需去更改它的。读者也可以将自己提供的服务加到这个档中,不过您所用的服务资料要公诸于世,不然别人的 services 档中可是没有您的服务的资料哟。
    
    最后的这组 getprotobyname() 及 getprotobynumber() 函式是用来取得一些「协议」的资料,比如 tcp、udp、igmp 等。一般而言,我们是不太会用到的。
    
    ◎ getprotobyname():依照通讯协议 (protocol) 的名称来获取该通讯协议的其他资料。格 式: struct protoent FAR * PASCAL FARgetprotobyname( const char FAR *name );参 数: name 通讯协议名称传回值: 成功 - 一指向 protoent 结构的指针失败 - NULL (呼叫 WSAGetLastError() 可得知原因)说明: 利用通讯协议的名称来得知该通讯协议的别名、编号等资料。
    
    ◎ getprotobynumber():依照通讯协议的编号来获取该通讯协议的其它资料。格 式: struct protoent FAR * PASCAL FARgetprotobynumber( int number );参 数: number 以 host order 排列方式的通讯协议编号传回值: 成功 - 一指向 protoent 结构的指针失败 - NULL (呼叫 WSAGetLastError() 可得知原因)说明: 利用通讯协议的编号来得知该通讯协议的名称、别名等资料。
    
    程序中呼叫方式分别如下:
    
    struct protoent far *ptptr;char proto_name[20];/* 假设 proto_name 已先设好协议名称 */ptptr = (struct protoent FAR *)getprotobyname( (char far *)proto_name )
    
    struct protoent far *ptptr;int proto_num;/* 假设 proto_num 已先设好协议编号 */ptptr = (struct protoent FAR *)getprotobynumber( proto_num )
    
    Winsock Stack 对于应用程序呼叫 getprotobyname() 及 getprotobynumber() 的资料,是取自于 local 的「protocol」檔;如无需要,我们也不用去变更这个档案的内容。
    
    (图 2)hello 程序呼叫同步数据库函式
    
    【异步数据库函式】
    
    Winsock 1.1 针对前面笔者所描述的 6 个同步数据库函式,也提供了相对的6 个异步数据库函式,它们分别是 WSAAsyncGetHostByName()、WSAAsyncGetHostByAddr()、WSAAsyncGetServByName()、WSAAsyncGetServByPort()、WSAAsyncGetProtoByName()、WSAAsyncGetProtoByNumber()。
    
    由于它们取得的资料与同步数据库函式相同,所以笔者仅以WSAAsyncGetHostByName() 为例,说明这些异步函式,并告诉各位读者,同步和异步数据库函式不同的地方。
    
    由字面来看,「异步」的意思就是我们发出问题时,并不会马上得到答覆,而等到系统取到资料时再告知我们。没错,这些异步数据库函式的作用就是这样。和 WSAAsyncSelect() 函式一样,我们要告诉 Winsock 系统一个接受通知讯息的窗口及讯息代码,以便系统通知我们。
    
    我们呼叫同步数据库函式时,return 值是一个指到相对资料的暂存区,而这个资料暂存区是由系统所提供的;但是呼叫异步数据库函式时,我们必须自己准备资料暂存区,并将此暂存区的地址当成参数,传给系统,以便系统用来储存取到的资料。读者们必须特别注意一点:在系统通知资料取得成功或失败前,千万不可将传给系统的资料暂存区删除释放,不然当系统取得资料要写入时,资料区已不见了,会导至当机的。除此之外,资料暂存区的大小一定要够大,才足够让系统用来存放取得的资料。(Winsock 规格中的建议值是MAXGETHOSTSTRUCT 1024 bytes 大小的暂存区,笔者认为太大了,100 byets差不多就太够了)
    
    呼叫异步数据库函式时,得到的 return 值是一个代码,此代码代表的就是此项呼叫在系统内的编号;由于是异步,所以我们在得到答案前,仍可呼叫 WSACancelAsyncRequest() 函式来取消原先的呼叫,这个取消的动作就要利用到该代码了。另外,当我们收到结果通知时,wParam 的值也是这个代码;我们此时可以利用 WSAGETASYNCERROR(lParam) 来得知资料取得是成功或失败;如果失败的原因是原先传入的暂存区太小的话,我们亦可利用WSAASYNCGETBUFLEN(lParam) 来得知至少要多大的暂存区才够。
    
    ◎ WSAAsyncGetHostByName():利用某一 host 的名称来获取该 host 的资料。(异步方式)格 式: HANDLE PASCAL FAR WSAAsyncGetHostByName( HWND hWnd,unsigned int wMsg, const char FAR *name, char FAR *buf, intbuflen );参 数: hWnd 动作完成后,接受讯息的窗口 handlewMsg 传回窗口的讯息name host 名称buf 存放 hostent 资料的暂存区buflen buf 的大小传回值: 成功 - 代表此异步动作的 handle 代码失败 - 0 (呼叫 WSAGetLastError() 可得知原因)说明: 此函式是利用 host 名称来获取其它的资料,如 host 的地址、别名,地址的型态、长度等。使用者呼叫此函式时必须传入要接收资料的窗口 handle、讯息代码、资料的存放地址指针等,以便得到资料时可以通知该窗口来使用资料。呼叫此函式后会马上回到使用者的呼叫点并传回一个 handle 代码,此代码可用来辨别此异步动作或用来取消此异步动作。当资料取得后,系统会送一个讯息到使用者指定的窗口。
    
    ◎ WSACancelAsyncRequest():取消某一未完成的异步要求。格 式: int PASCAL FAR WSACancelAsyncRequest( HANDLEhAsyncTaskHandle );参 数:hAsyncTaskHandle 要取消的 task handle 代码传回值: 成功 - 0失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明: 此函式是用来取消原先呼叫但尚未完成的WSAAsyncGetXByY(),例如 WSAAsyncGetHostByName(),的动作。参数 hAsyncTaskHandle 即为呼叫WSAAsyncGetXByY() 时传回之代码值。若是原先呼叫之异步要求已经完成,则无法加以取消。
    
    (图 3)hello 程序呼叫异步数据库函式
    
    【结语】
    
    笔者已经为各位介绍了大部份 Winsock 应用程序设计时会用到的函式,不知读者中是否已有人开始练习自己写 Winsock 网络程序了吗?下一期,笔者会将剩下的函式都介绍完。再此笔者并期待各位除了使用别人设计的网络软件外,大家也都能自己练习设计出一些不错的网络应用软件,让世界其它国家的人知道台湾也有能人的;愿共勉之

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页