VB 版 (精华区)
发信人: zxfsnow (最近睡眠太少), 信区: VB
标 题: Winsock 应用程式设计3
发信站: 哈工大紫丁香 (2000年06月06日09:14:13 星期二), 转信
发信人: esc (书剑飘零), 信区: VB
标 题: Winsock 应用程式设计 (3)
发信站: 虎踞龙盘东南站 (Fri Aug 20 16:39:07 1999), 转信
发信人: any (*_*), 信区: Winsock
标 题: Winsock 应用程式设计 (3)
发信站: 武汉白云黄鹤站 (Fri May 21 08:26:56 1999), 站内信件
在前两期的文章中,笔者介绍了如何在 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 位址与 port
number。
◎ 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() 来指定 port
number,而是呼叫 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 位址及 port
number 都是 network byte order,而不是 host byte order;如果您想转成 host
byte
order,就必须借助 ntohl() 及 ntohs() 两个函式。而我们能看到 IP 位址以「字
串」方式表达出来,则又是利用了 inet_ntoa() 函式;相对地,我们也可利用
inet_addr() 函式将字串方式的 IP 位址转换成 in_addr 格式(network byte orde
r 的
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 FAR
gethostbyname( const char FAR *name );
参 数: name host 的名称
传回值: 成功 - 指向一个 hostent 结构的指标
失败 - NULL (呼叫 WSAGetLastError() 可得知原因)
说明: 此函式是利用 host 名称来获取该主机的其他资料,如 host 的位址、
别名,位址的型态、长度等。
◎ gethostbyaddr():利用某一 host 的 IP 位址来获取该 host 的资料。
格 式: struct hostent FAR * PASCAL FAR
gethostbyaddr( 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 byte
order 方式的 IP 位址*/
htptr = (struct hostent FAR *) gethostbyaddr((char far *)&host_addr, 4,
PF_INET)
一般言,程式中呼叫到 gethostbyname() 及 gethostbyaddr() 时,Winsock
Stack 会先在 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 FAR
getservbyname( const char FAR *name, const char FAR *proto );
参 数: name 服务名称
proto 通讯协定名称
传回值: 成功 - 一指向 servent 结构的指标
失败 - NULL (呼叫 WSAGetLastError() 可得知原因)
说明: 利用服务名称及通讯协定来获得该服务的别名、使用的 port 号码
等。
◎ getservbyport():依照服务 (service) 的 port 号码及通讯协定(tcp/udp)来
获取该服务的其他资料。
格 式: struct servent * PASCAL FAR
getservbyport( 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 FAR
getprotobyname( const char FAR *name );
参 数: name 通讯协定名称
传回值: 成功 - 一指向 protoent 结构的指标
失败 - NULL (呼叫 WSAGetLastError() 可得知原因)
说明: 利用通讯协定的名称来得知该通讯协定的别名、编号等资料。
◎ getprotobynumber():依照通讯协定的编号来获取该通讯协定的其他资料。
格 式: struct protoent FAR * PASCAL FAR
getprotobynumber( 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, int
buflen );
参 数: hWnd 动作完成後,接受讯息的视窗 handle
wMsg 传回视窗的讯息
name host 名称
buf 存放 hostent 资料的暂存区
buflen buf 的大小
传回值: 成功 - 代表此非同步动作的 handle 代码
失败 - 0 (呼叫 WSAGetLastError() 可得知原因)
说明: 此函式是利用 host 名称来获取其他的资料,如 host 的位址、别名,
位址的型态、长度等。使用者呼叫此函式时必须传入要接收资料的视窗 handle、
讯息代码、资料的存放位址指标等,以便得到资料时可以通知该视窗来使用资
料。呼叫此函式後会马上回到使用者的呼叫点并传回一个 handle 代码,此代码
可用来辨别此非同步动作或用来取消此非同步动作。当资料取得後,系统会送一
个讯息到使用者指定的视窗。
◎ WSACancelAsyncRequest():取消某一未完成的非同步要求。
格 式: int PASCAL FAR WSACancelAsyncRequest( HANDLE
hAsyncTaskHandle );
参 数:hAsyncTaskHandle 要取消的 task handle 代码
传回值: 成功 - 0
失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)
说明: 此函式是用来取消原先呼叫但尚未完成的WSAAsyncGetXByY(),例
如 WSAAsyncGetHostByName(),的动作。参数 hAsyncTaskHandle 即为呼叫
WSAAsyncGetXByY() 时传回之代码值。若是原先呼叫之非同步要求已经完成,
则无法加以取消。
(图 3)hello 程式呼叫非同步资料库函式
【结语】
【结语】
笔者已经为各位介绍了大部份 Winsock 应用程式设计时会用到的函式,不
知读者中是否已有人开始练习自己写 Winsock 网路程式了吗?下一期,笔者会
将剩下的函式都介绍完。再此笔者并期待各位除了使用别人设计的网路软体
外,大家也都能自己练习设计出一些不错的网路应用软体,让世界其他国家的
人知道台湾也有能人的;愿共勉之。
--
既然要注定流浪,风又何必苦苦推难。
既然帆想要靠岸,海又何必处处阻拦。
--
以科计为本,以产业报国!
超越自我,飞跃无限!
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 202.118.235.249]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:201.726毫秒