2007年4月9日星期一

简单的Winsock应用程式设计(1) [转]

简单的Winsock应用程式设计(1) [转]

相信各位读者现在对於 Winsock 的定义、系统环境,以及一些 Winsock Stack

及 Winsock 应用程式,都有基本的认识了。接下来笔者希望能分几期为各位读者
介绍一下简单的 Winsock 网路应用程式设计。

我们将以 Winsock 1.1 规格所定义的 46 个应用程式介面(API)为基础,逐
步来建立一对 TCP socket 主从架构(Client / Server)的程式。在这两个程式
中,Server 将使用 Winsock 提供的「非同步」(asynchronous)函式来建立 s
ocket 连结、关闭、及资料收送等等;而 Client 则采类似传统 UNIX 的「阻拦
式」(blocking)。由於我们的重点并不在於 MS Windows SDK 的程式设计,所
以我们将使用最简便的方式来显示讯息;有关 MS Windows 程式的技巧,请各位
读者自行研究相关的书籍及文章。

今天我们先要看一下主从架构 TCP socket 的建立连结(connect)及关闭
(close)。
以前笔者曾简单地介绍过主从架构的概念,现在我们再以生活上更浅显的例
子来说明一下,读者稍後也较容易能明白笔者的叙述。我们可以假设 Server 就
像是电信局所提供的一些服务,比如「104 查号台」或「112 障碍台」。

(1)电信局先建立好了一个电话总机,这就像是呼叫 socket() 函式开启了一

个 socket。
(2)接著电信局将这个总机的号码定为 104,就如同我们呼叫 bind() 函式,将 Server 的这个 socket 指定(bind)在某一个 port。当然电信局必须让用户知道这个号码;而我们的 Client 程式同样也要知道 Server 所用的 port,待会才有办法与之连接。
(3)电信局的 104 查号台底下会有一些自动服务的分机,但是它的数量是有限的,所以有时你会拨不通这个号码(忙线)。同样地,我们在建立一个 TCP 的Server socket 时,也会呼叫 listen() 函式来监听等待;listen() 的第二个参数即是 waiting queue 的数目,通常数值是由 1 到 5。(事实上这两者还是
有点不一样。)
(4)用户知道了电信局的这个 104 查号服务,他就可以利用某个电话来拨号连接这个服务了。这就是我们 Client 程式开启一个相同的 TCP socket,然後呼叫 connect() 函式去连接 Server 指定的那个 port。当然了,和电话一样,如果 waiting queue 满了、与 Server 间线路不通、或是 Server 没提供此项服务
时,你的连接就会失败。
(5)电信局查号台的总机接受了这通查询的电话後,它会转到另一个分机做服务,而总机本身则再回到等待的状态。Server 的 listening socket 亦是一样,当你呼叫了 accept() 函式之後,Server 端的系统会建立一个新的 socket 来对此连接做服务,而原先的 socket 则再回到监听等待的状态。
(6)当你查询完毕了,你就可以挂上电话,彼此间也就离线了。Client和Server间的 socket 关闭亦是如此;不过这个关闭离线的动作,可由 Client 端或Server 端任一方先关闭。有些电话查询系统不也是如此吗?
接下来,我们就来看主从架构的 TCP socket 是如何利用这些 Winsock 函式来达成的;并利用资策会资讯技术处的「WinKing」这个 Winsock Stack 中某项功能来显示 sockets 状态的变化。文章中仅列出程式的片段,完整的程式请看附录的程式。

【Server 端建立 socket 并进入监听等待状态】

首先我们先看 Server 端如何建立一个 TCP socket,并使其进入监听等待的状态。

在图 1. 上,我们可以看到最先被呼叫到的是 WSAStartup() 函式。说明下:

WSAStartup():连结应用程式与 Winsock.DLL 的第一个函式。
格 式: 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() 函式来开启 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)

接下来我们要指定一个位址及 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; /* address */
bind(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)指定完位址及 port 之後,我们呼叫 listen() 函式,让这个 socket 进入监听状态。一个 Server 端的 TCP socket 必须在做完了 listen 的呼叫後,才能接受 Client 端的连接。

listen():设定 Socket 为监听状态,准备被连接。
格 式: 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 要连接了。(图1. 中并未将此函式绘出)

WSAAsyncSelect():要求某一 Socket 有事件 (event) 发生时通知使用者。
格 式: int PASCAL FAR WSAAsyncSelect( SOCKET s, HWND hWnd,
unsigned int wMsg, long lEvent ;
参 数: s Socket 的编号
hWnd 动作完成後,接受讯息的视窗 handle
wMsg 传回视窗的讯息
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。


(图2) WSAAsyncSelect 函式参数与应用程式关系

我们在程式中要求 Winsock 系统知道 Client 要来连接时,送一个ASYNC_EVENT 的讯息到程式中 hwnd 这个视窗;由於我们想知道的只有 accept 事件,所以我们只设定 FD_ACCEPT。

WSAAsyncSelect(listen_sd, hwnd, ASYNC_EVENT, FD_ACCEPT)


(图 3)demoserv 在 WinKing 系统上建立 socket 并进入监听状态

读者必须注意一点,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():控制 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 模式
FIONREAD -- 自 Socket 一次可读取的资料量(目前 in buffer 的资料量)

SIOCATMARK -- OOB 资料是否已被读取完

由於我们 Server 端的 socket 是用非同步模式,且设定了 FD_ACCEPT 事件,所以当 Client 端和我们连接时,Winsock Stack 会主动通知我们;我们再先来看看Client 端要如何和 Server 端建立连接?

【Client 端向 Server 端主动建立连接】

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 端接受 Client 端的连接】

由於我们 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 *add
r,
int FAR *addrlen ;
参 数: s Socket的识别码
addr 存放来连接的彼端的位址
addrlen addr的长度
传回值:成功 - 新的Socket识别码
失败 - INVALID_SOCKET (呼叫 WSAGetLastError() 可得知原因)
说明: Server 端之应用程式呼叫此一函式来接受 Client 端要求之 Socket 连
接动
作;如果Server 端之 Socket 是为 Blocking 模式,且没有人要求连接动作,那
麽此一
函式会被 Block 住;如果为 Non-Blocking 模式,此函式会马上回覆错误。acc
ept()
函式的答覆值为一新的 Socket,此新建之 Socket 不可再用来接受其它的连接要
求;但是原先监听之 Socket 仍可接受其他人的连接要求。

TCP socket 的 Server 端在呼叫 accept() 後,会传回一个新的 socket 号码;
而这个新的 socket 号码才是真正与 Client 端相通的 socket。比如说,我们用
socket() 建
立了一个 TCP socket,而此 socket 的号码(系统给的)为 1,然後我们呼叫的

bind()、listen()、accept() 都是针对此一 socket;当我们在呼叫 accept()

後,传回值是另一个 socket 号码(也是系统给的),比如说 3;那麽真正与 C
lient 端连接的是号码 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 来送资料到对方或收
对方送来
的资料了。(有关资料的收送,我们等下一期再谈)


(图 4) demoserv 与 democlnt 在 WinKing 上连接成功後状态

【Server 及 Client 端结束 socket 连接】

最後我们来看一下如何结束 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() 来通知 Winsock
Stack;如果您不呼叫此一函式,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应用程式设计(2)

简单的Winsock应用程式设计(2)

林 军 鼐

在前一期的文章中,笔者为大家介绍了如何在 Winsock 环境下,建立主从
架构(Client/Server)的 TCP socket 的连接建立与关闭;今天笔者将继续为大

介绍如何利用 TCP socket 来收送资料,并详细解说 WSAAsyncSelect 函式中的

FD_READ 及 FD_WRITE 事件(笔者曾发现有相当多人对这两个事件甚不了
解)。

相信读者们已经知道 TCP socket 的连接是在 Client 端呼叫 connect 函式成

功,且 Server 端呼叫 accept 函式後,才算完全建立成功;当连接建立成功後

Client 及 Server 也就可以利用这个连接成功的 socket 来传送资料到对方,或

收取对方送过来的资料了。


(图 1. TCP socket 的资料收送)

在介绍资料的收送前,笔者先介绍一下 TCP socket 与 UDP socket 在传送资
料时的特性:

Stream (TCP) Socket 提供「双向」、「可靠」、「有次序」、「不重覆」之
资料传送。

Datagram (UDP) Socket 则提供「双向」之沟通,但没有「可靠」、「有次
序」、「不重覆」等之保证; 所以使用者可能会收到无次序、重覆之资料,甚至

资料在传输过程中也可能会遗漏。

由於 UDP Socket 在传送资料时,并不保证资料能完整地送达对方,所以我
们常用的一些应用程式(如 telnet、mail、ftp、news...等)都是采用 TCP
Socket,以保证资料的正确性。(TCP 及 UDP 封包的传送协定不在我们讨论□

围,想要了解的读者们,请自行参考相关书籍)

TCP 及 UDP Socket 都是双向的,所以我们是利用同一个 Socket 来做传送及
收取资料的动作;一般言 TCP Socket 的资料送、收是呼叫 send() 及 recv()
这两
个函式来达成,而 UDP Socket 则是用 sendto() 及 recvfrom() 这两个函式。
不过
TCP Socket 也可用 sendto() 及 recvfrom() 函式,UDP Socket 同样可用 sen
d() 及
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 模式下,若是传送 (transpor
t) 系统
内之储存空间(output buffer)不够存放这些要传送的资料,send() 将会被 b
lock
住,直到资料送完为止;如果该 Socket 被设定为 Non-Blocking 模式,那麽将
视目
前的 output buffer 空间有多少,就送出多少资料,并不会被 block 住。使用
者亦须
注意 send()函式执行完成,并不表示资料已经成功地送抵对方了,而是已经放到

系统的 output buffer 中,等待被送出。 flags 的值可设为 0 或 MSG_DONTRO
UTE
及 MSG_OOB 的组合。(参见 WINSOCK第1.1版48页)

◎ recv():自 Socket 接收资料。
格 式: int PASCAL FAR recv( SOCKET s, char FAR *buf, int len, int f
lags ;
参 数: 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 大 於使用者提供的储存空间,那麽只有
该空
间大小的资料被取出,多馀的资料将遗失,且回覆错误的讯息。另外如果 Sock
et
为 Blocking 模式,且目前 input buffer 内没有任何资料,则 recv() 将 blo
ck 到有任
何资料到达为止;如果为 Non-Blocking 模式,且 input buffer 无任何资料,
则会马
上回覆错误。参数 flags 的值可为 0 或 MSG_PEEK、MSG_OOB 的组合;
MSG_PEEK 代表将资料拷贝到使用者提供的 buffer,但是资料并不从系统的 inp
ut
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 的要求,然後再做答覆,所以我们设定
FD_READ 事件;我们也希望 Winsock Stack 在知道 Client 关闭 Socket 时,能

动通知我们,所以同时也设定 FD_CLOSE 事件。(读者须注意,我们设定事件
的 Socket 号码是呼叫 accept 後传回的新 Socket 号码,而不是原先监听状态

Socket 号码)

WSAAsyncSelect(my_sd, hwnd, ASYNC_EVENT, FD_READ|FD_CLOSE)

在这里,我们同样是利用 hwnd 这个视窗及 ASYNC_EVENT 这个讯息;在
前文中,笔者曾告诉各位,在收到 ASYNC_EVENT 讯息时,我们可以利用
WSAGETSELECTEVENT(lParam) 来判断究竟是哪一事件(FD_READ 或
FD_CLOSE)发生了;所以并不会混淆。那我们到底在什麽时候会收到
FD_READ 或 FD_CLOSE 事件的讯息呢?

【FD_READ 事件】

我们会收到 FD_READ 事件通知我们去读取资料的情况有 :

(1)呼叫 WSAAsyncSelect 函式来对此 Socket 设定 FD_READ 事件时,
input buffer 中已有资料。
(2)原先系统的 input buffer 是空的,当系统再收到资料时,会通知我们。

(3)使用者呼叫 recv 或 recvfrom 函式,从 input buffer 读取资料,但是并

没有一次将资料读光,此时会再驱动一个 FD_READ 事件,表示仍有资料在
input buffer 中。

读者必须注意:如果我们收到 FD_READ 事件通知的讯息,但是我们故意
不呼叫 recv 或 recvfrom 来读取资料的话,尔後系统又收到资料时,并不会再

通知我们,一定要等我们呼叫了 recv 或 recvfrom 後,才有可能再收到
FD_READ 的事件通知。

【FD_CLOSE 事件】

当系统知道对方已经将 Socket 关闭了的情况下(收到 FIN 通知,并和对方
做关闭动作的 hand-shaking),我们会收到 FD_CLOSE 的事件通知,以便我
们也能将这个相对的 Socket 关闭。FD_CLOSE 事件只会发生於 TCP Socket,因

为它是 connection-oriented;对於 connectionless 的 UDP Socket,即使设了

FD_CLOSE,也不会有作用的。

程式中,当 Client 端送一个要求(request)来时,系统会以
ASYNC_EVENT 讯息通知我们的 hwnd 视窗;我们在利用
WSAGETSELECTEVENT(lParam) 及 WSAGETSELECTERROR(lParam) 知道是
FD_READ 事件及检查无误後,便呼叫 recv() 函式来收取 Client 端送来的资料


recv(wParam, &data, sizeof(data), 0)

笔者在前一期文章中也曾提到说,FD_XXXX 事件发生,收到讯息时,视
窗 handle 被呼叫时的参数 wParam 代表的就是事件发生的 Socket 号码,所以

处 wParam 的值也就是前面提到的 my_sd 这个 Socket 号码。recv() 的第四个

数设为 0,表示我们要将资料从系统的 input buffer 中读取并移走。

收到要求後,我们要答覆 Client 端,也就是要送资料给 Client;这时我们就

要利用 send() 这个函式了。

我们先将资料放到 data 这个资料暂存区,然後呼叫 send() 将它送出,我们
利用的也是 wParam (my_sd) 这个同样的 Socket 来做传送的动作,因为它是双

的。

send(wParam, &data, strlen(data), 0)
Server 与 Client 收送资料一段时间後(资料全部收送完毕),如果 Client 端



先呼叫 closesocket() 将它那端的 Socket 关闭,那麽系统在知道後,会通知我

一个 FD_CLOSE 事件的讯息,此时我们也可以呼叫 closesocket() 将我们这端的

Socket 关闭了;当然我们也可以呼叫 closesocket() 先主动关闭我们这端的

Socket。

【Client 端的资料收送及关闭 Socket】

我们例子的 Client 是采 Blocking 模式,所以在呼叫 connect() 函式与 Serv
er
连接时,可能会等一下子才成功;connect() 函式返回後,且无错误发生的话,

Client 与 Server 端的 TCP socket 连接就算成功了。这时,我们便可利用这个

接成功的 Socket 来送收资料了。由於我们并没有要设定为 Asynchronous 模式

所以也不用呼叫 WSAAsyncSelect() 来设定事件。

Client 端通常是会先主动发出要求到 Server 端,因此我们呼叫 send() 来传送

此一资料。我们的资料量很小,所以并不会被 send() 函式 Block 住;不过如果

您要送的资料量很大,那麽可能会等一段时间才会自 send() 函式返回;也就是

说必须等资料都放到系统的 output buffer 後才会返回;这是因为我们 Client

Socket 是阻拦模式。如果我们用的是非阻拦模式的 Socket,那麽 send() 函式

视系统的 output buffer 的空间有多少,只拷贝那麽多的资料到 output buffe
r,然
後就返回,并告知使用者送出了多少资料,并不须等所有资料都放到 output
buffer 才返回。

我们将要求放在 data 资料暂存区,然後呼叫 send() 将要求送出。资料送出
後,我们呼叫 recv() 来等待 Server 端的答覆。

send(mysd, data, strlen(data), 0)

recv(mysd, &data, sizeof(data), 0)

由於我们 Client 端是 Blocking 模式,所以 recv() 会一直 Block 住,直到下

列的情况之一发生,才会返回。

(1)Server 端送来资料。(此时 return 值是读取的资料长度)
(2)Server 端将相对的 Socket 关闭了。(此时的 return 值会是 0)
(3)Client 端自己呼叫 WSACancelBlockingCall() 来取消 recv() 的呼叫。

(此时 return 值是 SOCKET_ERROR 错误,错误码 10004 WSAEINTR)

同样地,资料全部送收完毕後,我们也呼叫 closesocket() 来将 Socket 关
闭。

◎ WSACancelBlockingCall():取消目前正在进行中的 blocking 动作。
格 式: int PASCAL FAR WSACancelBlockingCall( void ;
参 数: 无
传回值: 成功 - 0
失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)
说明: 此函式用来取消该应用程式正在进行中的 blocking 动作。通常的
使用时机有:(a) Blocking 动作正在进行中,该应用程式又收到某一讯息
(Mouse、Keyboard、Timer 等),则可在处理该讯息的段落中呼叫此函式。(b)

Blocking 动作正在进行中,而 Windows Sockets 又呼叫回应用程式的
「blocking hook」函式时,在该函式内可呼叫此函式来取消 blocking 动作。

使用者必须注意,在某一 Winsock blocking 函式动作进行时,除了
WSAIsBlocking() 及 WSACancelBlockingCall() 外,不可以再呼叫其它任何
Windows Sockets DLL 提供的函式,否则会产生错误。另外若取消的
blocking 动作不是 accept() 或 select() 的话,那麽该 Socket 可能会处於未

状态,使用者最好是呼叫 closesocket() 来关闭该 Socket,而不该再对它做任

何动作。


(图 2.)demoserv 与 democlnt 在资策会 WinKing 上收送资料的画面


(图 3.)demoserv 与 democlnt 在资策会 WinKing 上关闭 Socket 後的画面


介绍完了 TCP Socket 的资料收送,笔者接著为读者介绍 sendto() 及
recvfrom() 这两个函式,以及许多人可能很容易搞错的 FD_WRITE 事件。

【sendto 及 recvfrom 函式】

一般言,TCP Socket 使用的是 send() 及 recv() 这两个函式;而 UDP Socket

用的是 sendto() 及 recvfrom() 函式。这是因为 TCP 是 Connection-oriente
d,必须
做完 Socket 真正的连接程序後,才可以开始收送资料,此时系统已经知道了连

接的对方,所以我们不用再指定资料要送到哪里。而 UDP 是 Connectionless,

收送资料的双方并没有建立真正的连接,所以我们要利用 sendto() 及 recvfro
m()
来指定收资料的对方及获知是谁送资料给我们。

TCP Socket 也可以用 sendto() 及 recvfrom() 来送收资料,只是此时这两个

函式的最後两个参数没有作用,会被系统所忽略。而 UDP Socket 如果呼叫了

connect() 函式来指定对方的位址(这个 connect 并不会真的和对方做连接的动

作,而是告知我们本身的系统说我们只想收、送何方的资料),那麽也可以利
用 send() 及 recv() 来送收资料。

◎ sendto():将资料送到使用者指定的目的地。
格 式: int PASCAL FAR sendto( SOCKET s, const char FAR *buf,
int len, int flags, const struct sockaddr FAR *to, int
tolen ;
参 数: s Socket 的识别码
buf 存放要传送的资料的暂存区
len buf 的长度
flags 此函式被呼叫的方式
to 资料要送达的位址
tolen to 的大小
传回值: 成功 - 送出的资料长度
失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)
说明: 此函式适用於 Datagram 或 Stream Socket 来传送资料到指定的
位址。 对 Datagram Socket 言,若是 datagram 的大小超过限制,则将不会
送出任何资料,并会传回错误值。对 Stream Socket 言,其作用与 send() 相

同;参数 to 及 tolen 的值将被系统所忽略。 若是传送 (transport) 系统内之

存空间不够存放这些要传送的资料,sendto() 将会被 block 住,直到资料都被

送出;除非该 Socket 被设定为 non-blocking 模式。使用者亦须注意 sendto(
)
函式执行完成,并不表示资料已经成功地送抵对方了,而可能仍在系统的 outpu
t
buffer 中。 flags 的值可设为 0、MSG_DONTROUTE 及 MSG_OOB 的组合。
(参见 WINSOCK第1.1版51页)

◎ recvfrom():读取资料,并储存资料来源的位址。
格 式: int PASCAL FAR recvfrom( SOCKET s, char FAR *buf, int len, i
nt flags,
struct socketaddr FAR *from, int FAR *fromlen ;
参 数: s Socket 的识别码
buf 存放接收到的资料的暂存区
len buf 的长度
flags 此函式被呼叫的方式
from 资料来源的位址
fromlen from 的大小
传回值: 成功 - 接收到的资料长度 (若对方 Socket 已关闭,则为 0)
失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)
说明: 此函式用来读取资料并记录资料来源的位址。对 Datagram Socket
(UDP)言,一次读取一个 Datagram;对 Stream Socket (TCP)言,其作用与

recv() 相同,参数 from 及 fromlen 的值会被系统忽略。如果 Socket 为 Bl
ocking 模
式,且目前 input buffer 内没有任何资料,则 recvftom() 将 block 到有任何
资料到
达为止;如果为 Non-Blocking 模式,且 input buffer 无任何资料,则会马上
回覆错
误。

【FD_WRITE 事件】

笔者在前面介绍过 FD_READ 事件的发生时机,现在继续介绍 FD_WRITE
这个较易使人混淆的事件,因为真的有相当多的人对此一事件的发生不明了。

由字面上看,FD_WRITE 应该是要求系统通知我们某个 Socket 现在是否可
以呼叫 send() 或 sendto() 来传送资料?答案可以说「是」,但是它和 FD_RE
AD
却又有不同的地方。

在前面我们知道呼叫一次 recv() 後,如果 input buffer 中尚有资料未被取出

的话,系统会再通知我们一次 FD_READ。那麽如果我们呼叫一次 send() 後,
系统的 output buffer 仍有空间可写入的话,它是否会再通知我们一个
FD_WRITE,叫我们继续传送资料呢?这个答案就是「否定」的了!系统并不
会再通知我们了。

系统会通知我们 FD_WRITE 事件的讯息,只有下列几种情况:

(1)呼叫 WSAAsyncSelect() 来设定 FD_WRITE 事件时,Socket 已经可以
传送资料(TCP scoket 已经和对方连接成功了,或 UDP socket 已建立完成),

且目前 output buffer 仍有空间可写入资料。
(2)呼叫 WSAAsyncSelect() 来设定 FD_WRITE 事件时,Socket 尚不能传
送资料,不过一旦 Socket 与对方连接成功,马上就会收到 FD_WRITE 的通
知。
(3)呼叫 send() 或 sendto() 传送资料时,系统告知错误,且错误码为
10035 WSAEWOULDBLOCK (呼叫 WSAGetLastError() 得知这项错误),这
时表示 output buffer 已经满了,无法再写入任何资料(此时即令呼叫再多次的

send() 也都一定失败);一旦系统将部份资料成功送抵对方,空出 output buf
fer
後,便会送一个 FD_WRITE 给使用者,告知可继续传送资料了。换句话说,读
者在呼叫 send() 传送资料时,只要不是返回错误 10035 的话,便可一直继续呼

叫 send() 来传送资料;一旦 send() 回返错误 10035,那麽便不要再呼叫 sen
d()
传送资料,而须等收到 FD_WRITE 後,再继续传送资料。

【结语】

在这一期的文章中,笔者介绍了各位有关 TCP Socket 的资料收、送方式及
FD_READ、FD_WRITE 等事件的发生时机;读者们综合前一期的文章,应该
已经可以建立出一对主从架构的程式,并利用 TCP Socket 来传送资料了。

下一期,笔者将继续介绍有关如何获取网路资讯的函式,如
gethostname()、getsockname()、getpeername(),以及同步与非同步的网路资料

撷取函式 getXbyY()、WSAAsyncGetXByY()。

本文中所提到的 WinKing 试用版可自 SEEDNET 台北主机 tpts1.seed.net.tw

(139.175.1.10)的 UPLOAD/WINKING 目录中取得,档名为 wkdemo.exe;
WinKing 提供 Ethernet 及 PPP 连线功能,适用於一般 Ethernet 网路,亦可用

以电话、数据机连上 SEEDNET 的 PPP 伺服主机;□例 demoserv、democlnt,

以及一些笔者所写的 Winsock 程式(含原始程式码)则存放在
UPLOAD/WINKING/JNLIN 目录下;有兴趣的读者可自行用 anonymous ftp 方式
取得。

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

简单的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 位址与
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 (画面左边最小化者)。Se
rver
所用的 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 视窗亦可观察到相互连接的 Socke
ts 资
料,与我们呼叫函式所得结果相同)


(图 1)利用 hello 程式来模拟 Client 和 Server

读者必须注意一点,getsockname() 及 getpeername() 所取得的 IP 位址及 po
rt
number 都是 network byte order,而不是 host byte order;如果您想转成 h
ost byte
order,就必须借助 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 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() 或 getservbyp
ort()
来取得,而不必刻意去记颂它们。

◎ 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, (c
har 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 f
ar
*)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 网路程式了吗?下一期,笔者会

将剩下的函式都介绍完。再此笔者并期待各位除了使用别人设计的网路软体
外,大家也都能自己练习设计出一些不错的网路应用软体,让世界其他国家的
人知道台湾也有能人的;愿共勉之。

简单的Winsock应用程式设计(4) [转]

简单的Winsock应用程式设计(4) [转]

林 军 鼐

笔者在前几期的文章中已经介绍了大部份 Winsock 1.1 所提供的应用程式发
展介面;笔者也相信有读者已经开始利用这些 API 来开发自己的网路应用程式

了。但是可能仍有部份读者还是不清楚自己该先有哪些发展工具才能开发
Winsock 1.1 的应用程式?

基本上,读者当然一定要有 Microsoft C 或 Borland C 之类的编译程式
(Compiler)才能编译您的程式;至於和 Winsock 有关的档案只有两个,一个

是『winsock.h』,另一个是『winsock.lib』。这两个档案,读者们可以利用

anonymous ftp 的方式从 SEEDNET 台北主机「tpts1.seed.net.tw」的
『UPLOAD/WINKING/Winsock_Documents』目录下取得。

接著笔者要再为各位介绍剩下的几个函式,包括 select()、setsockopt()、
getsockopt(),以及变更系统的 Blocking Hook 函式时,所要用到的
WSASetBlockingHook() 和 WSAUnhookBlockingHook()。

【特殊的 select 函式】

如果写过 UNIX BSD socket 程式的读者,一定都知道这个 select() 函式是很

好用的。因为它可以帮您检查一整组(set)的 sockets 是否可以读、写资料,

可以用来检查 socket 是否已和对方连接成功,或者是对方是否已将相对的
socket 关闭了等等。

但是在 Winsock 1.1 及 MS Windows 3.X 「非强制性多工」的环境下,它是
否仍是那麽好用呢?我们在使用它时,是否要注意些什麽呢?现在就让笔者来
告诉您吧。

◎ select():检查一或多个 Sockets 是否处於可读、可写或错误的状态。
格 式: int PASCAL FAR select( int nfds, fd_set FAR *readfds,
fd_set FAR *writefds, fd_set FAR *exceptfds, const struct time
val FAR
*timeout
参 数: nfds 此参数在此并无作用
readfds 要被检查是否可读的 Sockets
writefds 要被检查是否可写的 Sockets
exceptfds 要被检查是否有错误的 Sockets
timeout 此函式该等待的时间
传回值: 成功 - 符合条件的 Sockets 总数 (若 Timeout 发生,则为
0)
失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)
说明: 使用者可利用此函式来检查 Sockets 是否有资料可被读取,
或是有空间可以写入,或是有错误发生。

Winsock 1.1 所提供的 select() 函式与 UNIX BSD 的 select() 函式,在参数

个数及资料型态上是一样,都有 nfds、readfds、writefds、exceptfds、及 ti
meout
五个参数;但是 Winsock 的 nfds 是没有作用的,有这个参数的目的,只是为了

与 UNIX BSD 的 select() 函式一致。

至於 readfds、writefds、exceptfds 同样是一组 sockets 的集合,所以您可以

同时设定许多 sockets 的号码在这三个参数里面;当然这些 sockets 必须是属

您的这个应用程式所建立的。如果您设定的 socket 号码中有任一个不是属於您

的这个程式的话,呼叫 select() 函式便会失败(错误码为 10038
WSAENOTSOCK)。

Winsock 同样也提供了一些 macros 来让您设定或检查 readfds、writefds、
exceptfds 的值,包括有:(其中 s 代表的是某一个 socket 的号码,set 代表
的就
是 readfds、writefds 或 exceptfds)

FD_ZERO(*set) -- 将 set 的值清乾净
FD_SET(s, *set) -- 将 s 加到 set 中
FD_CLR(s, *set) -- 将 s 从 set 中删除
FD_ISSET(s, *set) -- 检查 s 是否存在於 set 中

读者们要知道参数 readfds、writefds、及 exceptfds 都是 「called by valu
e-
result」;而「called by value-result」的意思就是说,我们在将参数传给系

时,要先设启始值,并将这些参数的位址(address)告诉系统;而系统则会利

用到这些值来做些运算或其他用途,最後并将结果再写回这些参数的位址中。
因此这些参数的值在传入前和函式回返後,可能会不同;所以读者们每次呼叫

select() 前,对这些参数一定要重新设定它们的值。

假设我们要检查 socket 1 和 2 目前是否可以用来传送资料,以及 socket 3 是

否有资料可读;我们不打算检查 sockets 是否有错误发生,所以 exceptfds 设

NULL。步骤大致如下:

FD_ZERO( &writefds ; /* 清除 writefds */
FD_ZERO( &readfds ; /* 清除 readfds */
FD_SET( 1, &writefds ; /* 将 socket 1 加到 writefds */
FD_SET( 2, &writefds ; /* 将 socket 2 加到 writefds */
FD_SET( 3, &readfds ; /* 将 socket 3 加到 readfds */
select( ..., &readfds, &writefds, NULL, ...) /* 呼叫 select() 来检查事
件 */
if (FD_ISSET( 1, &writefds ) /* 检查 socket 1 是否可写 */
send( 1, data ; /* 呼叫 send() 一定成功 */
if (FD_ISSET( 2, &writefds ) /* 检查 socket 2 是否可写 */
send( 2, data ; /* 呼叫 send() 一定成功 */
if (FD_ISSET( 3, &readfds ) /* 检查 socket 2 是否可读 */
recv( 3, data ; /* 呼叫 recv() 一定成功 */

select() 函式的第五个参数「timeout」,是让我们用来设定 select 函式要等

待(block)多久。兹述说如下:

(1)如果 timeout 设为「NULL」,那麽 select() 就会一直等到「至少」某
一个 socket 的事件成立了才会 return,这和其他的 blocking 函式一样。

select( ..., NULL /* blocking */

(2)如果 timeout 的值设为 {0, 0} (秒, 微秒),那麽 select() 在检查後

不管有没有 socket 的事件成立,都会马上 return,而不会停留。

timeout.tv_sec = timeout.tv_usec = 0;
select( ..., &timeout /* non-blocking */

(3)如果 timout 设为 {m, n},那麽就会等到至少某一个 socket 的事件发
生,或是时间到了(m 秒 n 微秒),才会 return。

timeout.tv_sec = m;
timeout.tv_usec = n;
select( ..., &timeout /* wait m secconds n microseconds */


在 UNIX 系统上,我们通常会利用 select() 来做「polling」的动作,检查事

件是否发生;但是在 MS Windows 3.X 的环境下一直做 polling 的动作一定要非

常小心,不然可能会造成整个 Windows 系统停住(因为 CPU 都被您的程式占
用了);所以使用时一定要注意「控制权释放」,不然就是「不要将 timeout 设

为 {0,0}」(因为 timeout 设为 {0,0} 的话, Winsock 系统内部可能不会呼叫

Blocking Hook 函式来释放控制权)。UNIX 系统由於是「Time Sharing」的方

式,所以并不会有类似的问题。(所谓 polling 的动作是指,您在程式中有一个

回圈,而在回圈内一直呼叫像 select 这样的函式做检查的动作)

select() 除了可以用来检查 socket 是否可读写外;对於 non-blocking 的
socket 在呼叫 connect() 後,也可利用 select() 的 writefds 来检查连接是
否已经成
功了(当这个 non-blocking 的 socket 被设定在 writefds,且被 select 成功
时);
此外,我们亦可利用 readfds 来检查 TCP socket 连接的对方是否已经关闭了(

此 socket 被设定在 readfds,且被 select 成功,但呼叫 recv 去收资料却 r
eturn 0
时)。


(图 1.) select 函式的几种不同用途

UNIX 系统上因为没有提供 WSAAsyncSelect() 函式,所以我们要用 select()

函式来做 polling 的动作;但是 Winsock 系统上已经有了可以设定非同步事件

WSAAsyncSelect() 函式,为了让 MS Windows 「讯息驱动」(message driven)

的环境更有效率,读者们应该尽量使用 WSAAsyncSelect(),而少用 select() 的

式;这也是当初为什麽要定义一个 WSAAsyncSelect() 函式的最大目的。

【变更 socket 的 options 的函式】

Winsock 1.1 也提供了一个变更 socket options 的 setsockopt() 函式;由於

options 的项目很多,笔者仅就数个较会用到的项目来解说,其馀的项目请读者

们自行研究。

◎ setsockopt():设定 Socket 的 options。
格 式: int PASCAL FAR setsockopt( SOCKET s, int level, int
optname,
const char FAR *optval, int optlen
参 数: s Socket 的识别码
level option 设定的 level (SOL_SOCKET 或
IPPROTO_TCP)
optname option 名称
optval option 的设定值
optlen option 设定值的长度
传回值: 成功 - 0
失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)
说明: 此函式用来设定 Socket 的一些 options,藉以更改其动作。可更改的

options 有:(详见 Winsock Spec. 54 页)

Option Type
-----------------------------------------------------
SO_BROADCAST BOOL
SO_DEBUG BOOL
SO_DONTLINGER BOOL
SO_DONTROUTE BOOL
SO_KEEPALIVE BOOL
SO_LINGER struct linger FAR*
SO_OOBINLINE BOOL
SO_RCVBUF int
SO_REUSEADDR BOOL
SO_SNDBUF int
TCP_NODELAY BOOL

(1)SO_BROADCAST -- 适用於 UDP socket。其意义是允许 UDP socket
「广播」(broadcast)讯息到网路上。
(2)SO_DONTLINGER -- 适用於 TCP socket。其意义是让 socket 在呼叫
closesocket() 关闭时,能马上 return,而不用等到资料都送完後才从函式呼叫

return;closesocket() 函式 return 後,系统仍会继续将资料全部送完後,才
真正地
将这个 socket 关闭。一个 TCP socket 在开启时的预设值即是 Don't Linger。

(3)SO_LINGER -- 适用於 TCP socket 来设定 linger 值之用。如果 linger

值设为 0,那麽在呼叫 closesocket() 关闭 socket 时,如果该 socket 的 ou
tput
buffer
中还有资料的话,将会被系统所忽略,而不会被送出,此时 closesocket() 也会

上 return;如果 linger 值设为 n 秒,那麽系统就会在这个时间内,尝试去送

output buffer 中的资料,时间到了或是资料送完了,才会从 closesocket() 呼

return。
(4)SO_REUSEADDR -- 允许 socket 呼叫 bind() 去设定一个已经用过的位址

(含 port number)。

我们就以设定某个 socket 的 linger 值为例,看看程式中该如何呼叫 setsock
opt()
这个函式:

struct linger Linger;
Linger.l_onoff = 1; /* 开启 linger 设定*/
Linger.l_linger = n; /* 设定 linger 时间为 n 秒 */
setsockopt( s, SOL_SOCKET, SO_LINGER, &Linger, sizeof(struct linger)


相对地,如果我们想要知道目前的某个 option 的设定值,那麽就可以利用
getsockopt() 函式来取得。

◎ getsockopt():取得某一 Socket 目前某个 option 的设定值。
格式: int PASCAL FAR getsockopt( SOCKET s, int level, int optname,
char FAR *optval, int FAR *optlen
参 数: s Socket 的识别码
level option 设定的 level
optname option 名称
optval option 的设定值
optlen option 设定值的长度
传回值: 成功 - 0
失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)
说明: 此函式用来获取目前 Socket的某些 options 设定值。

同样地,我们仍以取得某个 socket 的 linger 值为例,看一下程式中应该如何

呼叫 getsockopt():

struct linger Linger;
int opt_len = sizeof(struct linger);
getsockopt( s, SOL_SOCKET, SO_LINGER, &Linger, &opt_len)

【什麽是 Blocking Hook 函式及如何设定自己的 Blocking Hook 函式】

什麽是「Blocking Hook」函式呢?在解释之前,我们要先来剖析一下
Winsock 1.1 提供的 Blocking 函式(如 accept、connect 等)的内部究竟做了
哪些
事?

在 Winsock Stack 的 Blocking 函式内部,除了会检查一些条件外(比如该应

用程式是否已呼叫过 WSAStartup()?传入的参数是否正确?等等),便会进入一

个类似下面的回圈:

for (; {
/* 执行 Blocking Hook 函式 */
while (BlockingHook());
/* 检查使用者是否已经呼叫了 WSACancelBlockingCall()? */
if (operation_cancelled())
break;
/* 检查动作是否完成了? */
if (operation_complete())
break;
}

现在我们可以很清楚地知道 Blocking 函式的回圈中,有三件重要的事:(1)

执行 Blocking Hook 函式(2)检查使用者是否呼叫了 WSACancelBlockingCall
()
来取消此 Blocking 函式的呼叫?(3)检查此 Blocking 函式的动作是否已经完

了?

读者们必须注意,不同的 Winsock Stack 在执行这三件事时的顺序可能会不相

同;有的 Winsock Stack 可能会先检查 Blocking 函式的动作是否已经完成了,

後再执行 Blocking Hook 函式;所以 Blocking Hook 函式有可能不会被呼叫到
。待
会解释完 Blocking Hook 函式的重点後,读者们就可以知道笔者为什麽在前面告

诉各位在使用 polling 方式时一定要非常小心了。

由上面的回圈,我们现在可以知道 Blocking Hook 函式的使用时机是让系统在

等待 Blocking 函式完成前所呼叫的,它并不是给我们自己的应用程式所使用的

Winsock 系统本身内部就有一个预设的 Blocking Hook 函式;现在我们就来看一

这个预设的 Blocking Hook 函式会做些什麽事?

BOOL DefaultBlockingHook(void) {
MSG msg;
BOOL ret;
/* 取得下一个讯息;如果有,就处理它;如果没有,就释出控制权 */
ret = (BOOL) PeekMessage(&msg, NULL, 0, 0, PM_REMOVE);
if (ret) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return ret;
}

哦!原来 Blocking Hook 函式中很重要的地方就是:让 Blocking 函式在等待

动作完成前能够处理其他讯息,或是释出 CPU 控制权,以便让其他的应用程式

也有执行的机会。

现在回到前面一点的地方,大家仔细想一想:如果在一个 Winsock Stack 的
Blocking 函式的回圈内,先检查 Blocking 函式的动作是否已经完成了,然後再

行 Blocking Hook 函式的话;那麽是否就有可能不会释出 CPU 控制权来让其他

程式有执行的机会呢?如果我们的程式中再有类似下面的一个回圈,那麽整个

Windows 环境可能就会因我们的程式而 hang 住了。

for (; {
FD_ZERO(&writefds);
FD_SET( s, &writefds ;
timeout.tv_sec = timeout.tv_usec = 0;
n = select( 64, NULL, &writefds, NULL, &timeout ;
if ( n > 0
break;
if ( n == 0) /* timeout */
continue;
...
}
send( s, data ... ;

在这个回圈例子中,我们原是希望利用 select() 及 polling 的方式来检查 so
cket
的 output buffer 中是否尚有空间可写入资料?如果此时 output buffer 恰好
满了,
select() 函式中一检查到如此的情况,且 timeout 又是 {0,0},那麽就会马上
return
0,而不会呼叫到 Blocking Hook 函式来释放 CPU 控制权给 Windows 环境中的

他程式(包括 Winsock 收送的 Protocol Stack );由於没有分配到 CPU 时间
,所
以 Winsock Kernel 便无法将 output buffer 中任何资料送出;回圈中由 sele
ct() 回?.
後,又回到回圈的最前面,然後又呼叫 select(),马上又 timeout......;Win
dows 系
统因此就 hang 住了 !

Blocking Hook 函式中除了 CPU 控制权释放的问题外,还需注意什麽呢?大
家再看一看前面 Blocking 函式的回圈;回圈内呼叫 Blocking Hook 函式是包在

一个无穷的 while 回圈内。如果一个 Blocking Hook 函式的 return 值永远不
为 0 的
话,那麽也就永远被困在这个无穷回圈内了;所以我们在设计自己的 Blocking

Hook 函式时一定也要非常小心这个 return 值。

知道了 Blocking Hook 函式的用途及设计 Blocking Hook 函式该注意的地方
後,我们究竟要如何取代掉系统原有的 Blocking Hook 函式呢?那就要利用
WSASetBlockingHook() 函式了。

◎ WSASetBlockingHook():建立应用程式指定的 blocking hook 函式。
格 式: FARPROC PASCAL FAR WSASetBlockingHook( FARPROC
lpBlockFunc
参 数: lpBlockfunc 指向要装设的 blocking hook 函式的位址的
指标
传回值: 指向前一个 blocking hook 函式的位址的指标
说明: 此函式让使用者可以设定他自己的 Blocking Hook 函式,以取代原先
系统预设的函式。被设定的函式将会在应用程式呼叫到「blocking」动作时执
行。唯一可在使用者指定的 blocking hook 函式中呼叫的 Winsock 介面函式只

WSACancelBlockingCall()。

假设我们自己设计了一个 Blocking Hook 函式叫 myblockinghook(),那麽在程

式中向 Winsock 系统注册的方法如下:(其中 _hInst 代表此 task 的 Instan
ce)

FARPROC lpmybkhook = NULL;
lpmybkhook = MakeProcInstance( (FARPROC)myblockinghook, _hInst) ;
WSASetBlockingHook( (FARPROC)lpmybkhook ;


(图 2.)设定自己的 Blocking Hook 函式

我们在设定自己的 Blocking Hook 程式後,仍可以利用
WSAUnhookBlockingHook() 函式,来取消我们设定的 Blocking Hook 函式,而变

更回原先系统内定的 Blocking Hook 函式。

◎ WSAUnhookBlockingHook():复原系统预设的 blocking hook 函式。
格 式: int PASCAL FAR WSAUnhookBlockingHook( void
参 数: 无
传回值: 成功 - 0
失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)
说明: 此函式取消使用者设定的 blocking hook 函式,而回复系统原先预
设的 blocking hook 函式。

最後笔者要再说明一点,一个应用程式所设定的 Blocking Hook 函式,只会被

这个应用程式所使用;其他的应用程式并不会执行到您设定的 Blocking Hook 函

式的。另外,若非极有必要,最好是不要任意变更系统的 Blocking Hook 函式;

因为一旦您没有设计好的话,整个 Windows 环境可能就完蛋了。


(图 3.)使用自己的 Blocking Hook 函式时该注意事项

【结语】

四期的「Winsock 应用程式设计篇」在此结束了;笔者除了介绍 Winsock API

外,也将自己亲身设计 winsock.dll 的经验与各位读者分享了;希望这几期的文

章,对於国内想要在 Winsock 1.1 环境上开发网路应用程式的读者有些许的帮

助。谢谢大家。

From: zhangwe.bbs@bbs.net.tsinghua.edu.cn
Date: Wed, 22 Jan 1997 11:43:10 +0800 (CST)
Message-Id: <199701220343.LAA22158@bbs.net.tsinghua.edu.cn>
X-Authentication-Warning: bbs.net.tsinghua.edu.cn: bbsroot set sender
to zhangwe.bbs@bbs.net.tsinghua.edu.cn using -f
Reply-To: zhangwe.bbs@bbs.net.tsinghua.edu.cn
To: fz868@public.zz.hy.cn
Subject: Winsock API 大全 (2) -- Alex 整理(转寄)
X-Forwarded-By: zhangwe (乐乐)
X-Disclaimer: BBS 水木清华站 对本信内容恕不负责。
Precedence: junk
Content-Type: text
Content-Length: 12357

Posted By: Godman (天地通) on 'Internet'
Title: Winsock API 大全 2/2 (中文)
Date: Mon Sep 25 09:26:11 1995


[Microsoft Windows-specific Extensions]

(1) WSAAsyncGetHostByAddr():利用某一 host 的位址来获取该 host 的资料。

(非同步方式)

格 式: HANDLE PASCAL FAR
WSAAsyncGetHostByAddr( HWND hWnd,
unsigned int wMsg,
const char FAR *addr,
int len,
int type,
char FAR *buf,
int buflen ;

参 数: hWnd 动作完成後,接受讯息的视窗 handle
wMsg 传回视窗的讯息
addr network 排列方式的位址
len addr 的长度
type PF_INET(AF_INET)
buf 存放 hostent 资料的区域
buflen buf 的大小

传回值: 成功 - 代表此 Async 动作的 handle
失败 - 0 (呼叫 WSAGetLastError() 可得知原因)

说明: 此函式是利用位址来获取 host 的其他资料,如 host 的名称、
别名, 位址的型态、长度等。使用者呼叫此函式时必须传入要接收资料的视窗

handle、讯息代码、资料的存放位置指标等,以便得到资料时可以通知该视窗
来使用资料。呼叫此函式後会马上回到使用者的呼叫点并传回一个 handle,
此 handle 可用来辨别此非同步动作或用来取消此非同步动作。当资料取得後,

会送一个讯息到使用者指定的视窗。


(2) 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 的大小

传回值: 成功 - 代表此 Async 动作的 handle
失败 - 0 (呼叫 WSAGetLastError() 可得知原因)

说明: 此函式是利用 host 名称来获取其他的资料,如 host 的位址、
别名, 位址的型态、长度等。使用者呼叫此函式时必须传入要接收资料的视窗

handle、讯息代码、资料的存放位置指标等,以便得到资料时可以通知
该视窗来使用资料。呼叫此函式後会马上回到使用者的呼叫点并传回一
个 handle,此handle 可用来辨别此非同步动作或用来取消此非同步动
作。当资料取得後,会送一个讯息到使用者指定的视窗。

(3) WSAAsyncGetProtoByName():依照通讯协定的名称来获取该通讯协定的
其他资料。(非同步方式)

格 式: HANDLE PASCAL FAR
WSAAsyncGetProtoByName( HWND hWnd,
unsigned int wMsg,
const char FAR *name,
char FAR *buf,
int buflen ;

参 数: hWnd 动作完成後,接受讯息的视窗 handle
wMsg 传回视窗的讯息
name 通讯协定名称
buf 存放 protoent 资料的区域
buflen buf 的大小

传回值: 成功 - 代表此 Async 动作的 handle
失败 - 0 (呼叫 WSAGetLastError() 可得知原因)

说明: 利用通讯协定的名称来得知该通讯协定的别名、编号等资料。
使用者呼叫此函式时必须传入要接收资料的视窗 handle、讯息代码、
资料的存放位置指标等,以便得到资料时可以通知该视窗来使用资料。
呼叫此函式後会马上回到使用者的呼叫点并传回一个 handle,此 handle
可用来辨别此

2007年4月3日星期二

I/O完成端口模型

[网络]I/O完成端口模型
参考了很多资料,组织这些教程,也是个复习和再学习的过程,目的是共享还有以备以后可以作为复习的纲要。这几天看了很多资料,发现这些资料的行文有2种表达发式,一种是最常见的:从技术的源头说起,直到未来发展的趋势。还有一种比较开门见山,可能功利了些:首先这种技术是为了解决什么问题,用在什么地方,有什么好处,怎么用,呵呵:)整个一个倒叙,要是再结合项目的实践就更好了,个人比较喜欢这种表达发式,所以这篇文章的结构也是这么组织的。




问题
现在网络游戏很流行,上万个玩家同时在线的情况很常见,网游服务器如何处理这么巨量的数据?!要是读过了"Winsock I/O方法"这篇文章,可以了解到套接字I/O模型中的:select,WSAAsyncSelect,WSAEventSelect,Overlapped I/O模型一次最多都只能支持6 4个套接字!这些模型显然不能胜任。而Winsock I/O模型中的"Completion port",可以同时管理数百乃至上千个套接字,辅以集群技术和提升硬件配置等措施,应付网游肯定可以了。

对IOCP的评价
I/O完成端口可能是Win32提供的最复杂的内核对象。
[Advanced Windows 3rd] Jeffrey Richter
这是[IOCP]实现高容量网络服务器的最佳方法。
[Windows Sockets2.0:Write Scalable Winsock Apps Using Completion Ports]
Microsoft Corporation
完成端口模型提供了最好的伸缩性。这个模型非常适用来处理数百乃至上千个套接字。
[Windows网络编程2nd] Anthony Jones & Jim Ohlund
I/O completion ports特别显得重要,因为它们是唯一适用于高负载服务器[必须同时维护许多连接线路]的一个技术。Completion ports利用一些线程,帮助平衡由I/O请求所引起的负载。这样的架构特别适合用在**P系统中产生的"scalable"服务器。
[Win32多线程程序设计] Jim Beveridge & Robert Wiener
IOCP的限制
该模型只适用于Windows NT和Windows 2000操作系统

什么是IOCP?

IOCP全称I/O Completion Port,中文译为I/O完成端口。所谓"完成端口",实际是Win32、Windows NT以及Windows 2000采用的一种I/O构造机制,除套接字句柄之外,实际上还可接受其他东西。IOCP是一个异步I/O的API,它可以高效地将I/O事件通知给应用程序。从本质上说,完成端口模型要求我们创建一个Win32完成端口对象,通过指定数量的线程,对重叠I/O请求进行管理,以便为已经完成的重叠I/O请求提供服务。IOCP只不过是用来进行读写操作,和文件I/O有些类似。

使用IOCP

Microsoft为IOCP提供了相应的API函数,主要的就两个: CreateIoCompletionPort( ... ), GetQueuedCompletionStatus( ... )。

1.首先要创建一个I/O完成端口对象,用它面向任意数量的套接字句柄,管理多个I/O请求。
HANDLE CreateIoCompletionPort(
HANDLE FileHandle,//既然要处理网络事件,那也就是将客户的socket作为HANDLE传进去
HANDLE ExistingCompletionPort,//是一个现有的完成端口
DWORD CompletionKey,
DWORD NumberOfConcurrentThreads//并发线程的数量
);
*NumberOfConcurrentThreads告诉系统一个完成端口上同时允许运行的线程最大数。在默认情况下,所开线程数和CPU数量相同,经验公式: 线程数 = CPU数 * 2 + 2。若将该参数设为0,表明系统内安装了多少个处理器,便允许同时运行多少个线程!
此函数有2种用法,不同的作用:
1.创建一个完成端口对象
hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
2.将一个句柄同完成端口关联到一起
CreateIoCompletionPort((HANDLE)hAccept,hCompletionPort,(DWORD)PerHandleData,0);

2. 接收有关I / O操作完成情况的通知
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,//线程要监视哪一个完成端口
LPDWORD lpNumberOfBytes,//接收实际传输的字节数
PULONG_PTR lpCompletionKey,//为原先传递进入CreateCompletionPort函数的套接字返回"单句柄数据"
LPOVERLAPPED *lpOverlapped, //接收完成的I/O操作的重叠结果
DWORD dwMilliseconds //指定等待一个完成数据包在完成端口上出现的时间。
);
一个工作者线程从G e tQueuedCompletionStatus这个API调用接收到I/O完成通知后,在lpCompletionKey和lpOverlapped参数中,会包含一些必要的套接字信息。利用这些信息,可通过完成端口,继续在一个套接字上的I/O处理。
lpCompletionKey参数包含了"单句柄数据",因为在一个套接字首次与完成端口关联到一起的时候,那些数据便与一个特定的套接字句柄对应起来了。通常情况下,应用程序会将与I / O请求有关的套接字句柄保存在这里。
lpOverlapped参数则包含了一个OVERLAPPED结构,在它后面跟随"单I / O操作数据"。单I / O操作数据可以是追加到一个OVERLAPPED结构末尾的、任意数量的字节。要想做到这一点,一个简单的方法是定义一个结构,然后将OVERLAPPED结构作为新结构的第一个元素使用。

同时运行了一个或多个线程,在几个不同的套接字上执行I/O操作的时候。要避免的一个重要问题是在进行重叠I/O操作的同时,强行释放一个OVERLAPPED结构。要想避免出现这种情况,最好的办法是针对每个套接字句柄,调用closesocket函数,任何尚未进行的重叠I/O操作都会完成。一旦所有套接字句柄都已关闭,便需在完成端口上, 终止所有工作者线程的运行。要想做到这一点,需要使用PostQueuedCompletionStatus函数,向每个工作者线程都发送一个特殊的完成数据包。该函数会指示每个线程都"立即结束并退出"。

大致的流程:

1.初始化Winsock
2.创建一个完成端口
3.根据服务器线程数创建一定量的线程数
4.准备好一个socket进行bind然后listen
5.进入循环accept等待客户请求
6.创建一个数据结构容纳socket和其他相关信息
7.将连进来的socket同完成端口相关联
8.投递一个准备接受的请求
以后就不断的重复5至8的过程

示例
1.
HANDLE InitializeThreads()
{
HANDLE hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
if (hCompletionPort == NULL) return NULL;
for (i = 0; i < g_dwNumThreads; i++)
{
hThread = CreateThread(NULL, 0, WorkerThreadProc, hCompletionPort, CREATE_SUSPENDED, &dwThreadId);
if (hThread == NULL){
CloseHandle(hCompletionPort);
return NULL;
}
SetThreadPriority(hThread, THREAD_PRIORITY_TIME_CRITICAL);
ResumeThread(hThread);
CloseHandle(hThread);
}
return hCompletionPort;
};

2.
g_hCompletionPort = InitializeThreads();
AcceptClients();
3.
BOOL AcceptClients()
{
...
g_nNumSocket = EnumNetworkAdapter();
if (g_nNumSocket == 0) return FALSE;

for (int nIndex = 0; nIndex < g_nNumSocket ; nIndex++)
{
...
g_interface[nIndex].hSocket = socket(AF_INET, SOCK_STREAM, 0);
if (g_interface[nIndex].hSocket == INVALID_SOCKET) return FALSE;
nError = bind(g_interface[nIndex].hSocket, (LPSOCKADDR)&sin, sizeof(sin));
nError = listen(g_interface[nIndex].hSocket, 5);
...
}
while ( TRUE )
{
for (i = 0; i < g_nNumSocket; i++)
{
FD_SET(g_interface[i].hSocket, &readfds);
}
nRet = select(0, &readfds, NULL, NULL, NULL);
if ((nRet == 0) || (nRet == SOCKET_ERROR)) continue;

for (i = 0; i < g_nNumSocket; i++)
{
if (!FD_ISSET(g_interface[i].hSocket, &readfds))
continue;
hSocket = accept(g_interface[i].hSocket, NULL, NULL);
...
g_hCompletionPort = CreateIoCompletionPort(
(HANDLE)hSocket,
g_hCompletionPort,
lpClientContext->dwVerifyClientContext,
0);
if (g_hCompletionPort == NULL){
CloseClient(lpClientContext);
continue;
}
...
nError = setsockopt(hSocket, SOL_SOCKET, SO_SNDBUF, (char *)&nZero, sizeof(nZero));
if (nError == SOCKET_ERROR)
{
PostQueuedCompletionStatus(g_hCompletionPort, 0, (DWORD)lpClientContext->dwVerifyClientContext, NULL);
continue;
}
...
}
return TRUE;
}
4.
DWORD WINAPI WorkerThreadProc(LPVOID lpCmpltPort)
{
HANDLE hCompletionPort = (HANDLE)lpCmpltPort;
...
while (TRUE)
{
GetQueuedCompletionStatus(hCompletionPort,dwIoSize,&dwClientNo,&lpOverlapped,INFINITE);
...
}
}
- 作者: XZXBLOGCHINA 2004年12月27日, 星期一 17:24  回复(0) |  引用(0) 加入博采

[网络]Winsock I/O方法 主要介绍:套接字模式、套接字I/O模型


套接字模式:锁定、非锁定
套接字I/O模型: select(选择)
WSAAsyncSelect(异步选择)
WSAEventSelect(事件选择)
Overlapped I/O(重叠式I / O)
Completion port(完成端口)



一、 简介

套接字模型的出现,是为了解决套接字模式存在的某些限制。
所有Wi n d o w s平台都支持套接字以锁定或非锁定方式工作。然而,并非每种平台都支持每一种I / O模型。
操作系统对套接字I / O模型的支持情况

平台
select
WSAAsync
WSAEventSelect
Overlapped
Completion Port

Windows CE
支持
不支持
不支持
不支持
不支持

Windows 95(Winsock 1)
支持
支持
不支持
不支持
不支持

Windows 95(Winsock 2)
支持
支持
支持
支持
不支持

Windows 98
支持
支持
支持
支持
不支持

Windows NT
支持
支持
支持
支持
支持

Windows 2000
支持
支持
支持
支持
支持



二、 套接字模式

在锁定模式下,在I / O操作完成前,执行操作的Winsock函数(比如send和recv)会一直等候下去,不会立即返回程序(将控制权交还给程序)。而在非锁定模式下,Winsock函数无论如何都会立即返回。

锁定模式
处在锁定模式的套接字,因为在一个锁定套接字上调用任何一个Winsock API函数,都会产生相同的后果—耗费或长或短的时间"等待"。

非锁定模式
SOCKET s;
unsigned long ul=1;
int nRet;
s = socket(AF_INET,SOCK_STREAM,0);
nRet = ioctlsocket(s,FIOBIO,(unsigned long *) &ul);
if(nRet == SOCKET_ERROR)
{
//Failed to put the socket into nonblocking mode
}

三、 套接字I/O模型

1. select模型
select函数,可以判断套接字上是否存在数据,或者能否向一个套接字写入数据。
处于锁定模式时,防止I / O调用(如send或recv),进入"锁定"状态;同时防止在套处于非锁定模式时,防止产生WSAEWOULDBLOCK错误。
除非满足事先用参数规定的条件,否则select函数会在进行I / O操作时锁定。

select的函数原型:
int select(
int nfds,//会被忽略, 为了保持与早期的B e r k e l e y套接字应用程序的兼容
fd_set FAR * readfds,//检查可读性
fd_set FAR * writefds,//检查可写性
fd_set FAR * exceptfds,//于例外数据
const struct timeval FAR * timeout//最多等待I / O操作完成多久的时间。如是空指针,无限期等
);
//fd_set数据类型代表着一系列特定套接字的集合
Wi n s o c k提供下列宏对f d _ s e t进行处理与检查:
■ FD_CLR(s, *set):从s e t中删除套接字s。
■ FD_ISSET(s, *set):检查s是否s e t集合的一名成员;如答案是肯定的是,则返回T R U E。
■ FD_SET(s, *set):将套接字s加入集合s e t。
■ F D _ Z E R O ( * s e t ):将s e t初始化成空集合。

步骤:
1) 使用F D _ Z E R O宏,初始化自己感兴趣的每一个f d _ s e t。
2) 使用F D _ S E T宏,将套接字句柄分配给自己感兴趣的每个f d _ s e t。
3) 调用s e l e c t函数,然后等待在指定的f d _ s e t集合中,I / O活动设置好一个或多个套接字句柄。s e l e c t完成后,会返回在所有f d _ s e t集合中设置的套接字句柄总数,并对每个集合进行相应的更新。
4) 根据s e l e c t的返回值,我们的应用程序便可判断出哪些套接字存在着尚未完成的I / O操作—具体的方法是使用F D _ I S S E T宏,对每个f d _ s e t集合进行检查。
5) 知道了每个集合中"待决"的I / O操作之后,对I / O进行处理,然后返回步骤1 ),继续进行s e l e c t处理。
s e l e c t返回后,它会修改每个f d _ s e t结构,删除那些不存在待决I / O操作的套接字句柄。这正是我们在上述的步骤( 4 )中,为何要使用F D _ I S S E T宏来判断一个特定的套接字是否仍在集合中的原因。
SOCKET s;
fd_set fdread;
int ret;
//create a socket, and accept a connection
//Manage I/O on the socket
while(TRUE)
{
//Always clear the read set before calling select()
FD_ZERO(&fdread);
//Add socket s to the read set
FD_SET(s,&fdread);
If((ret=select(0,&fdread,NULL,NULL,NULL))== SOCKET_ERROR){
//Error condition
}
if(ret>0)
{
//For this simple case, select() should return the value 1. An application dealing with
//more than one socket could get a value greater than 1.At this point,your application
//should check to see whether the socket is part of a set.
If(FD_ISSET(s,&fdread))
{
//A read event has occurred on socket s
}
}
}

2. WSAAsyncSelect
有用的异步I / O模型,利用这个模型,应用程序可在一个套接字上,接收以Windows消息为基础的网络事件通知。
函数原型:
int WSAAsyncSelect(
SOCKET s,
HWND hWnd,//收到通知消息的那个窗口
unsigned int wMsg, //指定在发生网络事件时,打算接收的消息
long lEvent//位掩码,对应于一系列网络事件的组合
);

用于WSAAsyncSelect函数的网络事件:
FD_READ 应用程序想要接收有关是否可读的通知,以便读入数据
FD_WRITE 应用程序想要接收有关是否可写的通知,以便写入数据
FD_OOB 应用程序想接收是否有带外(OOB)数据抵达的通知
FD_ACCEPT 应用程序想接收与进入连接有关的通知
FD_CONNECT 应用程序想接收与一次连接或者多点join操作完成的通知
FD_CLOSE 应用程序想接收与套接字关闭有关的通知
FD_QOS 应用程序想接收套接字"服务质量"(QoS)发生更改的通知
FD_GROUP_QOS 应用程序想接收套接字组"服务质量"发生更改的通知(为未来套接字组的使用保留)
FD_ROUTING_INTERFACE_CHANGE 应用程序想接收在指定的方向上,与路由接口发生变化的通知
FD_ADDRESS_LIST_CHANGE 应用程序想接收针对套接字的协议家族,本地地址列表发生变化的通知

窗口例程,函数原型:
LRESULT CALLBACK WindowProc(
HWND hWnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
);
WSAAsyncSelect调用中定义的消息。wParam参数指定在其上面发生了一个网络事件的套接字。假若同时为这个窗口例程分配了多个套接字,这个参数的重要性便显示出来了。在lParam参数中,包含了两方面重要的信息。其中, lParam的低位字指定了已经发生的网络事件,而lParam的高位字包含了可能出现的任何错误代码。
网络事件消息抵达一个窗口例程后,应用程序首先应检查lParam的高字位,以判断是否在套接字上发生了一个网络错误。这里有一个特殊的宏: WSAGETSELECTERROR,可用它返回高字位包含的错误信息。若应用程序发现套接字上没有产生任何错误,接着便应调查到底是哪个网络事件类型,造成了这条Windows消息的触发—具体的做法便是读取lParam之低字位的内容。此时可使用另一个特殊的宏:WSAGETSELECTEVENT,用它返回lParam的低字部分。

3.WSAEventSelect
异步I / O模型, 网络事件投递至一个事件对象句柄,而非投递至一个窗口例程。

1. 针对打算使用的每一个套接字,首先创建一个事件对象。
WSAEVENT WSACreateEvent(void);
*相关的函数:BOOL WSAResetEvent(WSAEVENT hEvent);BOOL WSACloseEvent(WSAEVENT hEvent);
WSACreateEvent创建的事件拥有两种工作状态:signaled和nonsignaled,以及两种工作模式:manual reset和auto reset。

2. 将其与某个套接字关联在一起,同时注册自己感兴趣的网络事件类型。
int WSAEventSelect(Socket s,WSAEVENT hEventObject,long lNetworkEvents);

3. 套接字同一个事件对象句柄关联在一起后,应用程序便可开始I/O处理;方法是等待网络事件触发事件对象句柄的工作状态。WSAWaitForMultipleEvents函数的设计宗旨便是用来等待一个或多个事件对象句柄,并在事先指定的一个或所有句柄进入"已传信"状态后,或在超过了一个规定的时间周期后,立即返回。
DWORD WSAWaitForMultipleEvents(
DWORD cEvents,
const WSAEVENT FAR * lphEvents,
BOOL fWaitAll,
DWORD dwTimeOut,
BOOL fAlertable
);
WSAWaitForMultipleEvents只能支持由WSA_MAXIMUM_WAIT_EVENTS对象规定的一个最大值,在此定义成64个。因此,针对发出WSAWaitForMultipleEvents调用的每个线程,该I/O模型一次最多都只能支持6 4个套接字。假如想让这个模型同时管理不止64个套接字,必须创建额外的工作者线程,以便等待更多的事件对象。

4. 知道了造成网络事件的套接字后,接下来调用WSAEnumNetworkEvents函数,调查发生了什么类型的网络事件。
int WSAEnumNetworkEvents(SOCKET s,WSAEVENT hEventObject,LPWSANETWORKEVENTS lpNetworkEvents);

4. Overlapped I/O
重叠I / O(Overlapped I/O)模型使应用程序能达到更佳的系统性能。
重叠模型的基本设计原理便是让应用程序使用一个重叠的数据结构,一次投递一个或多个Winsock I/O请求。
首先使用WSA_FLAG_OVERLAPPED这个标志,创建一个套接字。s = WSASocket(AF_INET, SOCK_STREAM, 0 ,NULL,0,WSA_FLAG_OVERLAPPED);创建套接字的时候,假如使用的是socket函数,而非WSASocket函数,那么会默认设置WSA_FLAG_OVERLAPPED标志。成功建好一个套接字,同时将其与一个本地接口绑定到一起后,便可开始进行重叠I / O 操作,方法是调用下述的Wi n s o c k 函数,同时指定一个WSAOVERLAPPED结构(可选):
■ WSASend
■ WSASendTo
■ WSARecv
■ WSARecvFrom
■ WSAIoctl
■ AcceptEx
■ TrnasmitFile
步骤:
1) 创建一个套接字,开始在指定的端口上监听连接请求。
2) 接受一个进入的连接请求。
3) 为接受的套接字新建一个WSAOVERLAPPED结构,并为该结构分配一个事件对象句柄。也将事件对象句柄分配给一个事件数组,以便稍后由WSAWaitForMultipleEvents函数使用。
4) 在套接字上投递一个异步WSARecv请求,指定参数为WSAOVERLAPPED结构。注意函数通常会以失败告终,返回SOCKET_ERROR错误状态WSA_IO_PENDING(I/O操作尚未完成)。
5) 使用步骤3 )的事件数组,调用WSAWaitForMultipleEvents函数,并等待与重叠调用关联在一起的事件进入"已传信"状态(换言之,等待那个事件的"触发")。
6) WSAWaitForMultipleEvents函数完成后,针对事件数组,调用WSAResetEvent(重设事件)函数,从而重设事件对象,并对完成的重叠请求进行处理。
7) 使用WSAGetOverlappedResult函数,判断重叠调用的返回状态是什么。
8) 在套接字上投递另一个重叠WSARecv请求。
9) 重复步骤5 ) ~ 8 )。

void main(void)
{
WSABUF DataBuf;
DWORD EventTotal = 0;
WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS];
WSAOVERLAPPED AcceptOverlapped;
SOCKET ListenSocket,AcceptSocket;
//Step 1:Start Winsock and set up a listening socket
...
//Step 2:Accept an inbound connection
AcceptSocket = accept(ListenSocket, NULL , NULL);
//Step 3:Set up an overlapped structure
EventArray[EventTotal] = WSACreateEvent();
ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));
AcceptOverlapped.hEvent= EventArray[EventTotal];
DataBuf.len = DATA_BUFSIZE;
DataBuf.buf = buffer;
EventTotal ++;
//Step 4:Post a WSARecv request to begin receiving data on the socket
WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags, &AcceptOverlapped, NULL);
While(TRUE)
{
//Step 5:Wait for the overlapped I/O call to complete
Index = WSAWaitForMultipleEvents(EventTotal,EventArray,FALSE,WSA_INFINITE,FALSE);
//Index should be 0,because we have only one event handle in EventArray
//Step 6:Reset the signaled event
WSAResetEvent(EventArray[Index - WSA_WAIT_EVENT_0]);
//Step 7:Determine the status of the overlapped request
WSAGetOverlappedResult(AcceptSocket, &AcceptOverlapped, &BytesTransferred, FALSE, &Flags);
//First check to see whether the peer has closed the connection, and if so, close the socket
if(BytesTransferred == 0){
closesocket(AcceptSocket);
WSACloseEvent(EventArray[Index - WSA_WAIT_EVENT_0]);
Return;
}
//Do something with the received data.
//DataBuf contains the received data.
...
//Step 8:Post another WSARecv() request on the socket
Flags = 0;
ZeroMemory(&AcceptOverlapped,sizeof(WSAOVERLAPPED));
AcceptOverlapped.hEvent = EventArray[Index - WSA_WAIT_EVENT_0];
DataBuf.len = DATA_BUFSIZE;
DataBuf.buf = Buffer;
WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags, &AcceptOverlapped,NULL);
}
}
在Windows NT和Windows 2000中,重叠I/O模型也允许应用程序以一种重叠方式,实现对连接的接受。具体的做法是在监听套接字上调用AcceptEx函数。

完成例程
"完成例程"是我们的应用程序用来管理完成的重叠I / O请求的另一种方法。完成例程其实就是一些函数。设计宗旨是通过调用者的线程,为一个已完成的I / O请求提供服务。
void CALLBACK CompletionROUTINE(
DWORD dwError, //表明了一个重叠操作(由l p O v e r l a p p e d指定)的完成状态是什么。
DWORD cbTransferred,//实际传输的字节量
LPWSAOVERLAPPED lpOverlapped,
DWORD dwFlags // 0
);
在用一个完成例程提交的重叠请求,与用一个事件对象提交的重叠请求之间,存在着一项非常重要的区别。WSAOVERLAPPED结构的事件字段hEvent并未使用;
步骤:
1) 新建一个套接字,开始在指定端口上,监听一个进入的连接。
2) 接受一个进入的连接请求。
3) 为接受的套接字创建一个WSAOVERLAPPED结构。
4) 在套接字上投递一个异步WSARecv请求,方法是将WSAOVERLAPPED指定成为参数,同时提供一个完成例程。
5) 在将fAlertable参数设为TRUE的前提下,调用WSAWaitForMultipleEvents,并等待一个重叠I/O请求完成。重叠请求完成后,完成例程会自动执行,而且WSAWaitForMultipleEven ts会返回一个WSA_IO_COMPLETION。在完成例程内,可随一个完成例程一道,投递另一个重叠WSARecv请求。
6) 检查WSAWaitForMultipleEvents是否返回WSA_IO_COMPLETION。
7) 重复步骤5 )和6 )。

SOCKET AcceptSocket;
WSABUF DataBuf;
void main(void)
{
WSAOVERLAPPED Overlapped;
//Step 1:Start Winsock, and set up a listening socket
...
//Step 2:Accept a new connection
AcceptSocket = accept(ListenSocket, NULL, NULL);
//Step 3:Now that we have an accepted socket,start processing I/O using overlapped I/O with a completion routine.
//To get the overlapped I/O processing started,first submit an overlapped WSARECV() request.
Flags = 0;
ZeroMemory(&Overlapped, sizeof(WSAOVERLAPPED));
DataBuf.len = DATA_BUFSIZE;
DataBuf.buf = Buffer;
//Step 4: Post an asynchronous WSARecv() request on the socket by specifying the WSAOVERLAPPED structure
//as a parameter, and supply the WorkerRoutine function below as the completion routine
if(WSARecv(AcceptSocket,&DataBuf,1,&RecvBytes,&Flags,&Overlapped,WorkerRoutine) == SOCKET_ERROR){
if(WSAGetLastError()!=WSA_IO_PENDING){
return;
}
//Since the WSAWaitForMultipleEvents() API requires waiting on one or more event objects
//we will have to create a dummy event object. As an alternative, we can use SleepEx() instead.
EventArray[0] = WSACreatedEvent();
While(TRUE)
{
//Step 5:
Index = WSAWaitForMultipleEvents(1, EventArray, FALSE, WSA_INFINITE, TRUE);
//Step 6:
if(Index = = WAIT_IO_COMPLETION) {
//An overlapped request completion routine just completed.Continue servicing more completion rouines.
break;
}
else{
//A bad error occurred - stop processing.
Return;
}
}
}
}

void CALLBACK WorkerRoutine(DWORD Error,DWORD BytesTransferred,LPWSAOVERLAPPED overlapped,DWORD InFlags)
{
DWORD SendBytes, RecvBytes;
DWORD Flags;
If(Error!=0||BytesTransferred == 0){
//a bad error occurred on the socket
closesocket(AcceptSocket);
return;
}
//At this point, an overlapped WSARecv() request completed successfully.Now we can retrieve the received data that is
//contained in the variable DataBuf. After Processing the received data,We need to post another overlapped
//WSARecv() or WSASend() request.For Simplicity,we need to post another overlapped WSARecv() or WSASend()
//request.For Simplicity,we will post another WSARecv() request.
Flags = 0;
ZeroMemory(&Overlapped, sizeof(WSAOVERLAPPED));
DataBuf.len = DATA_BUFSIZE;
DataBuf.buf = Buffer;
if (WSARecv(AcceptSocket,*DataBuf,1,&RecvBytes,&Flags,&Overlapped,WorkerRoutine)==SOCKET_ERROR){
if(WSAGetLastError()!=WSA_IO_PENDING){
return;
}
}
}

5、完成端口模型
"完成端口"模型是迄今为止最为复杂的一种I / O模型。然而,假若一个应用程序同时需要管理为数众多的套接字,那么采用这种模型,往往可以达到最佳的系统性能!但不幸的是,该模型只适用于Windows NT和Windows 2000操作系统。因其设计的复杂性,只有在你的应用程序需要同时管理数百乃至上千个套接字的时候,而且希望随着系统内安装的C P U数量的增多,应用程序的性能也可以线性提升,才应考虑采用"完成端口"模型。要记住的一个基本准则是,假如要为Windows NT或Windows 2000开发高性能的服务器应用,同时希望为大量套接字I / O请求提供服务(We b服务器便是这方面的典型例子),那么I / O完成端口模型便是最佳选择!
[将单独一章讲解完成端口模型。xzx 2004-12-24]
- 作者: XZXBLOGCHINA 2004年12月24日, 星期五 12:03  回复(1) |  引用(0) 加入博采

[网络]Winsock支持的协议 主要介绍:IP、红外线套接字、IPX/SPX、NetBIOS、AppleTalk、ATM



一、IP
网际协议( Internet Protocol)是一个无连接的协议,不能保证数据投递万无一失。两个比它高级的协议( T C P和U D P)用于依赖I P协议的数据通信。

TCP
传输控制协议(Transmission Control Protocol), 面向连接的通信,提供可靠无错的数据传输。连接一旦建立,就可双向字节流交换数据。

UDP用户数据报协议(User Datagram Protocol),无连接通信,不保障可靠数据的传输,但能够向若干个目标发送数据,接收发自若干个源的数据。

定址计算机都分配有一个I P v 4地址,用一个3 2位数来表示。当需通过T C P或U D P通信时,必须指定服务器的I P地址和服务端口号。
Wi n s o c k中,通过S O C K A D D R _ I N结构来指定地址和端口信息,该结构的格式如下:
Struct sockaddr_in
{
short sin_family;//必须设为A F _ I N E T,以使用I P地址家族
u_short sin_port;//端口
struct in_addr sin_addr;//把一个I P地址保存为一个4字节的数,无符号长整数类型
char sin_zero[8];//充当填充项,使结构和S O C K A D D R结构的长度一样
};

端口分为三类:"已知"端口、已注册端口、动态端口。
■ 0 ~ 1 0 2 3由I A N A控制,是为固定服务保留的。
■ 1 0 2 4 ~ 4 9 1 5 1是I A N A列出来的、已注册的端口,供普通用户的普通用户进程或程序使用。
■ 4 9 1 5 2 ~ 6 5 5 3 5是动态和(或)私用端口。


二、红外线套接字
红外线套接字,I r S o c k,通过红外串口通信。可在Windows 98和Windows 2000上使用。

Typedef struct sockaddr_irda{
u_short irdaAddressFamily;//A F _ I R D A
u_char irdaDeviceID[4];//唯一性地标识特定服务所运行的设备
char irdaServiceName[25];//服务名
}SOCKADDR_IRDA;

irdaDeviceID:
在建立I r S o c k服务器时,这个字段是忽略不计的。但是对客户机而言,却非常重要,因为它指定的是准备连接的那个I r D A设备(也可能有若干个)。
irdaServiceName:
服务名,应用要么利用这项服务对其本身进行注册,要么试着与这项服务建立连接。

红外线设备列举、创建一个客户机或服务器:
由于红外线设备的使用地点不固定,因此,必须有一种方法,可以动态地把特定范围内的所有可用红外线设备列举出来。
SOCKET sock;
DEVICELIST devList;
DWORD dwListLen=sizeof(DEVICELIST);
Sock=WSASocket(AF_IRDA,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED);
...
devList.numDevice=0;
dwRet=getsockopt(sock,SOL_IRLMP,IRLMP_ENUMDEVICES,(char *)&devList,&dwListLen);

建立I r S o c k服务器:
1) 建立一个地址家族A F _ I R D A套接字和套接字类型S O C K _ S T R E A M。
2) 用服务器的服务名填写一个S O C K A D D R _ I R D A结构。
3) 利用套接字句柄和S O C K A D D R _ I R D A结构调用b i n d。
4) 利用套接字句柄和b a c k l o g边限调用l i s t e n。
5) 为接入客户机锁定一个a c c e p t调用。

建立I r S o c k客户机:
1) 建立地址家族A F _ I R D A套接字和套接字类型S O C K - S T R E A M。
2) 调用g e t s o c k o p t函数,列举所有可用的红外线设备。
3) 针对返回的每个设备,利用设备I D和准备连接的服务名填写一个SOCKADDR_ IRDA结构。
4) 利用套接字句柄和S O C K A D D R _ I R D A结构,调用c o n n e c t函数。针对步骤3)中所填的
结构,重复步骤4),直到连接成功。


三、IPX/SPX
互联网包交换协议,无连接通信,一般为承担Novell NetWa r e客户机/服务器联网服务的计算机所用。
如果应用程序需要数据投递保证,但仍坚持使用I P X,它就会选用一个比I P X高级的协议,比如说"顺序分组交换"S P X和SPX II协议,这两个协议中, S P X包通过I P X发送。


四、NetBIOS
五、AppleTalk
如果不与苹果公司的M A C机通信,你可能不会选择A p p l e Ta l k协议。从某种程度上来说,A p p l e Ta l k与N e t B I O S类似,因为两者都是针对每一个进程来进行名字注册的。

六、ATM
异步传输模式协议, AT M通常用于L A N和WA N上的高速联网,也用于各种类型的通信,比如说要求高速通信的语音、视频和数据等。一般说来, AT M利用网络上的虚拟连接(V C)来提供服务质量( Q O S)保证。Wi n s o c k能够通过AT M地址家族来使用AT M网络上的虚拟连接。AT M网络一般由通过交换机(它们将AT M网络桥接在一起)连接的端点(或计算机)构成。

ATM是一个媒体类型,而不是一个真正的协议。也就是说, AT M类似于直接在以太网上写入以太帧。和以太网一样, AT M协议没有提供流控制。它是一个面向连接的协议,要么提供消息模式,要么提供流模式。如果数据不能快速发送出去,发送应用则可能溢出本地缓冲。同样地,接收应用必须频繁投递收到的数据:否则,接收缓冲填满之时,任何一个另外接入的数据都可能被丢弃。如果需要流控制,方法之一是在AT M上使用I P协议,这样一来,应用便紧跟在上面描述的I P地址家族之后。

2007年4月2日星期一

建立基于Linux的RADIUS服务器

建立基于Linux的RADIUS服务器


一、引言

RADIUS是目前广泛使用的集中认证和记帐协议,本文介绍了在Linux服务器上安装和配置RADIUS服务器软件的过程,并使用MySQL数据库来存放用户信息。



二、RADIUS简介

RADIUS的全称为Remote Access Dail-In User Service,是对远程用户进行认证和记帐的一种协议,主要用在ISP处,可以集中对用户进行认证和记费。RFC2865、RFC2866定义了RADIUS的认证和记帐标准。通常用户访问NAS(Network Access Server)时,NAS会向RADIUS服务器发送认证请求,并传送用户名和口令给RADIUS服务器,RADIUS服务器要再询问用户数据库以核对用户名及口令,如果正确,就返回给NAS认证通过的信息。这时NAS再向RADIUS服务器发送记帐请求,并发送用户记帐信息,RADIUS服务器收到信息后存入用户数据库中。



三、获取软件

gnu-radius-0.96.4.tar.gz

http://ftp.gnu.org/gnu/radius/



注:需要系统上已安装有MySQL,如果没有请自行安装,这里就不介绍MySQL的安装和配置了。



四、编译安装

1、解压缩软件包

tar xzvf gnu-radius-0.96.4.tar.gz



2、配置编译选项

cd gnu-radius-0.96.4/

./configure --enable-yydebug --enable-pam --with-mysql --enable-nls

在这里我们选择编译时支持PAM和MySQL,缺省还会支持shadow password。请确认已安装了PAM和MySQL的开发支持包,否则配置程序无法正确完成。



3、编译安装

make

make check

make install

安装后执行文件在/usr/local/sbin和/usr/local/bin目录下,配置文件在/usr/local/etc/raddb目录下。



五、配置使用

1、生成数据库

这里我们使用已经装好的MySQL数据库,要为RADIUS生成MySQL的数据库和表。进入gnu-radius-0.96.4/db目录,编辑config.m4文件以符合你的环境的实际情况,内容如下:

define({server},{localhost})

define({port},{3306})

define({CREATOR},{root})

define({CREATOR_PASSWORD},{default})

define({DB_USER},radius)

define({DB_PWD},guessme)

这里定义了MySQL服务器在本地,端口是3306,MySQL管理帐号是root,口令是default,要创建的RADIUS数据库用户名为radius,口令为guessme。

编辑完后执行make mysql命令,就会自动生成名为RADIUS的MySQL的数据库了。数据库表结构见此目录下的mysql.struct文件。



2、配置RADIUS

RADIUS的配置文件都在/usr/local/etc/raddb目录下。我们主要修改几个文件就可以了。

/usr/local/etc/raddb/client.conf文件是本地客户端程序使用的,定义了与RADIUS服务器共享的key(foobar)和RADIUS服务器的端口(1812和1813),内容如下:

server local 127.0.0.1 foobar 1812 1813

source_ip 127.0.0.1

timeout 3

retry 1



/usr/loca/etc/raddb/clients文件定义了允许到RADIUS服务器认证的客户端的主机名或IP地址,还有共享的key,这里添加了一台Cisco 2600路由器的地址,我们要用它来测试。内容如下:

# This is a list of clients which are allowed to make authentication

# requests.

# Each record consists of two fields:

# i. Valid hostname.

# ii. The shared encryption key for this hostname.

#

#Client Name Key

#---------------- -------------------

localhost foobar

192.168.0.39 cisco



/usr/local/etc/raddb/sqlserver文件定义了RADIUS服务器访问MySQL数据库的信息,其中需要确认的内容:

interface mysql

server localhost

port 3306

login radius

password guessme

doauth yes

auth_db RADIUS

doacct yes

acct_db RADIUS



/usr/local/etc/raddb/users文件定义了认证用户的信息,这里指定了使用SQL数据库存放认证信息,并且使用DES或MD5的机制来加密口令,内容如下:

# This is the users database.



DEFAULT Auth-Type = Crypt-Local,

Password-Location = SQL

Service-Type = Framed-User,

Framed-Protocol = PPP



如果数据库中不希望存放加密的口令,可以使用“Auth-Type = Local”来设定。



以上文件都修改好后,就可以执行“/usr/local/sbin/radcrl start”命令来启动RADIUS服务器了,log信息可以看/var/log/radius.log文件。要停止RADIUS服务器,就执行“/usr/local/sbin/radcrl stop”命令。



3、本地测试

在测试前,我们先把用户信息加入数据库中:

mysql -u radius -pguessme RADIUS

>INSERT INTO passwd VALUES ('test', 'Framed-PPP', ENCRYPT('12345678', now()), 'Y');

>quit

因为我们要使用加密的口令,所以使用了MySQL的encrypt()函数。

接下来就使用命令“/usr/local/sbin/radauth -v test”来测试了,输入口令后,显示:

server 127.0.0.1:1812

send code 1 (RT_AUTHENTICATION_REQUEST)

send: User-Name = test

send: Password = 12345678

send: NAS-Port-Id = 0

recv code 2 (RT_AUTHENTICATION_ACK)

recv: Service-Type = Framed-User

recv: Framed-Protocol = PPP

expect 2

got 2

PASS

说明认证已经通过了。这时,我们可以看到数据库的calls表中没有任何记帐信息,因为radauth程序只是请求认证,没有要求RADIUS记帐。我们会在下面使用Cisco 2600路由器来进一步测试。



4、网络测试

我们使用一台Cisco 2600路由器作为Network Access Server(NAS)来实际测试RADIUS服务器。要求Cisco的串口使用PPP协议,对链路对端设备进行PAP认证,并向RADIUS服务器记帐。

Cisco上的配置如下:

!

aaa new-model

aaa authentication ppp test1 radius

aaa accounting network test2 start-stop radius

!

interface FastEthernet0/0

ip address 192.168.0.39 255.255.255.0

!

interface Serial1/1

ip address 10.0.0.2 255.255.255.0

no ip directed-broadcast

encapsulation ppp

ppp authentication pap test1

ppp accounting test2

!

radius-server host 192.168.0.250 auth-port 1812 acct-port 1813

radius-server key cisco

!



RADIUS服务器的IP地址是192.168.0.250,认证端口是1812,记帐端口是1813。Cisco以太口的IP地址是192.168.0.39,共享的key是cisco,和上面的clients文件中对应。

经测试RADIUS服务器工作正常,数据库的calls表中也记录了相应的记帐信息。



六、参考资料

http://www.gnu.org/software/radius/manual/

架设freeradius+mysql_的radius服务器

架设freeradius+mysql_的radius服务器
本文献给广大linux的爱好者
  有人居然说设置radius要卖钱,本人花了几天的功夫,看了些资料,终于成功设置radius服务器,现在贴出来和大家分享。
  笔者用的平台是RedHat Linux 7.2
  一、安装mysql
  本人用的是光盘上的RPM包

  rpm –ivh mysql-3.23.41.1.i386.rpm
  rpm –ivh mysql-devel-3.23.41.1.i386.rpm
  rpm –ivh mysql-server-3.23.41.1.i386.rpm
  rpm –ivh mysqlclient9-3.23.22-6.i386.rpm
  启动mysql服务
  service mysqld start
  更改mysql的root密码(注意:mysql的root 和系统的root是2个不用的概念)
  mysql –uroot –p
  创建radius数据库
  creat database radius;
  use mysql;
  update user set password=password(‘你的密码’) where user=’root’;
  允许远程机器连接
  update user set host=’%’ where user=’root’;
  退出及重新启动mysql
  quit
  service mysqld restart
  二、安装openSSL
  本人也是用的光盘上的RPM包
  rpm –ivh openssl095a-0.9.5a-11.i386.rpm
  rpm –ivh openssl096-0.9.6-6.i386.rpm
  三、安装freeradius
  从www.freeradius.org上下载freeraidus,本文版本是0.8.1
  编译和安装
  tar xvfz freeradius.tar.gz
  cd xvfz freeradius-0.8.1
  ./configure
  make
  make install
  建立mysql的数据库raius的表
  cd src/modules/rlm_sql/drivers/rlm_sql_mysql
  mysql –uroot –p密码 radius < db_mysql.sql
  更改freeradius的设置
  cd /usr/local/etc/raddb
  更改radiusd.conf,让其支持sql (如下面所说)
  authorize {
  preprocess
  chap
  mschap
  suffix
  sql
  }
  accouting {
  ….
  sql
  …
  }
  更改sql.conf
  server=”localhost”
  login=”root”
  password=”mysql的root的密码”
  radius_db=”radius”
  更改 client.conf支持所用的NAS具体可以看该文档,要注意的是secret是NAS和radius服务器的共享密码
  数据库加入测试账号
  加入组
  mysql –uroot –p密码 radius
  insert into radgroupreply (groupname,attribute,op,values) values (‘user’,‘Auth-Type’,’:=’,’Local’);
  insert into radgroupreply (groupname,attribute,op,values) values (‘user’,‘Service-Type’,’:=’,’Framed-User’);
  insert into radgroupreply (groupname,attribute,op,values) values (‘user’,‘Framed-IP-Address’,’:=’,’255.255.255.254’);
  insert into radgroupreply (groupname,attribute,op,values) values (‘user’,‘Framed-IP-Netmask’,’:=’,’255.255.255.0’);
  加入测试账号
  insert into radcheck (username,attribute,op,value) values (‘test’,’User-Password’,’:=’,’test’)
  测试账号加入组
  insert into usergroup (username,groupname) values (‘test’,’user’);
  四、启动radius服务、测试账号
  启动到debug模式
  radiusd –X
  有时候会报找不到文件rlm_sql_mysql这个时候只要把库文件加入系统搜索的目录里
  比如:
  cp /usr/local/lib/* /usr/lib
  测试账号
  radtest test teset localhost 0 testing123
  如果能看到radius的应答,恭喜服务器设置成功了。
  本人才疏,如果本文有错误,请大家指正,并欢迎一起讨论。
  本文作者:余旭东

mpd pptp server 简易安装实例

mpd pptp server 简易安装实例



注:这是我把以前自己写的同名笔记稍做修改重新发布,以前的笔记似乎google不到了。

#mpd pptp server

mpd 是FreeBSD下的pptp 综合工具,可以用来做pptp客户端和服务器,通过适当配置作vpn也不错,这里只是一个作pptp server的实例

mpd自己带的文档已经很详细了,如果你需要其他的功能,看看文档吧,E文的。

#URL:http://www.sourceforge.net/projects/mpd

由于mpd pptp server配置非常简单快捷,而pptp通道的安全性还是相当高的,因此,很多时候,我用来作为连接到自己的服务器进行管理的
安全通道,一些基于web的管理是不加密的,pptp端到端的加密正好弥补了这个漏洞。

而现在mpd也支持RADIUS验证,如果你想要做稍微大型一点的pptp server也未尝不可。


#内核应该有如下支持
# for mpd pptp server
options NETGRAPH #netgraph(4) system
options NETGRAPH_ASYNC
options NETGRAPH_BPF
options NETGRAPH_ECHO
options NETGRAPH_ETHER
options NETGRAPH_HOLE
options NETGRAPH_IFACE
options NETGRAPH_KSOCKET
options NETGRAPH_LMI
# MPPC compression requires proprietary files (not included)
#options NETGRAPH_MPPC_COMPRESSION
options NETGRAPH_MPPC_ENCRYPTION
options NETGRAPH_PPP
options NETGRAPH_PPTPGRE
options NETGRAPH_RFC1490
options NETGRAPH_SOCKET
options NETGRAPH_UI
#end
#虽然系统会自动加载相应的内核模块,如果你的内核中没有以上选项,但是还是建议编译到内核里;


#安装、配置mpd pptp server.

cd /usr/ports/net/mpd
make clean
make install
make clean

#开放5个拨入;

#ports带来的配置文件样例里有很多种情形下的配置,我们只取其中的pptp server部分。

#vi /usr/local/etc/mpd/mpd.conf
#begin of mpd.conf
#
#default 部分表示启动mpd时激活的项目;
default:
load client1
load client2
load client3
load client4
load client5

client1:
new -i ng0 pptp1 pptp1
set ipcp ranges 172.16.120.80/32 172.16.120.100/32
load client_standard
#
client2:
new -i ng1 pptp2 pptp2
set ipcp ranges 172.16.120.81/32 172.16.120.101/32
load client_standard
#
client3:
new -i ng2 pptp3 pptp3
set ipcp ranges 172.16.120.82/32 172.16.120.102/32
load client_standard
#
client4:
new -i ng3 pptp4 pptp4
set ipcp ranges 172.16.120.83/32 172.16.120.103/32
load client_standard
#
client5:
new -i ng4 pptp5 pptp5
set ipcp ranges 172.16.120.84/32 172.16.120.104/32
load client_standard
#

client_standard:
set iface disable on-demand
#set iface enable proxy-arp
#set iface idle 1800
set bundle enable multilink
set link yes acfcomp protocomp
set link no pap chap
set link enable chap
#set link mtu 1460
set link mtu 1260
set link keep-alive 10 60
set ipcp yes vjcomp
set ipcp dns 61.145.117.164
# set ipcp nbns 172.16.120.4
set bundle enable compression
set ccp yes mppc
set ccp yes mpp-e40
set ccp yes mpp-e128
set ccp yes mpp-stateless
#end of mpd.conf

###注意:mpd现在支持RADIUS验证
radius:
# You can use radius.conf(5), its useful, because you can share the
# same config with userland-ppp and other apps.
set radius config /etc/radius.conf
# and/or specify the server directly here
set radius retries 3
set radius timeout 3
set radius server localhost testing123 1812 1813
# send the given IP in the RAD_NAS_IP_ADDRESS attribute to the server.
set radius me 1.1.1.1
# send accounting updates every 5 minutes
set radius acct-update 300
# let the RADIUS server assign the IP
set ipcp enable radius-ip
# enable RADIUS, and fallback to mpd.secret, if RADIUS auth failed
set bundle enable radius-auth radius-fallback
# enable RADIUS accounting
set bundle enable radius-acct
# use idle-timeout, session-timeout, routes list and mtu from the RADIUS server
set iface enable radius-idle radius-session radius-mtu radius-route
# activate MPPE and let the RADIUS server assign MPPE-types and MPPE-policies
set bundle enable compression
set ccp yes mppc
set ccp enable radius
##################

#vi /usr/local/etc/mpd/mpd.links
#bengin of mpd.links
pptp1:
set link type pptp
set pptp self 0.0.0.0
set pptp enable incoming
set pptp disable originate
#
pptp2:
set link type pptp
set pptp self 0.0.0.0
set pptp enable incoming
set pptp disable originate
#
pptp3:
set link type pptp
set pptp self 0.0.0.0
set pptp enable incoming
set pptp disable originate
#
pptp4:
set link type pptp
set pptp self 0.0.0.0
set pptp enable incoming
set pptp disable originate
#
pptp5:
set link type pptp
set pptp self 0.0.0.0
set pptp enable incoming
set pptp disable originate
#
#end of mpd.links

#注意:mpd.conf里面,每一个配置项的link名称(new -i ng0 pptp1 pptp1,这一行定义),
#比如client1(links = pptp1),client2(links = pptp2),在mpd.links中必须有相应的link type定义,
#pptp1:
# set link type pptp
#pptp2:
# set link type pptp
#其他类推;

#用户名和密码;
#vi /usr/local/etc/mpd/mpd.secret

#bengin of mpd.secret
#username <---> password <-----> ip address range to user
#这里可以指定某个用户拨号时使用的地址
#user1 fortest 172.16.120.111/24
#也可以不指定
user1 fortest
#end of mpd.secret

#注意:杀死进程时不要使用-9强行结束,否则可能导致进程不能正常关闭设备,必须重新启动系统才能恢复;

#使用懒得delay start up
#cp /usr/local/etc/rc.d/mpd.sh.sample /usr/local/modules/public/etc/rc.d/mpd-server
#ln -s /usr/local/modules/public/etc/rc.d/mpd-server /usr/local/sbin

#一般做法
cp /usr/local/etc/rc.d/mpd.sh.sample /usr/local/etc/rc.d/mpd-server.sh

#配置日志记录
touch /var/log/mpd.log

#FreeBSD 4.x的做法

#vi /etc/syslog.conf
#添加如下,!mpd表示mpd这个进程;
!mpd
*.* /var/log/mpd.log

#FreeBSD 5.x的做法
##奇怪,FBSD 5.2.1无daemon项目
daemon.* /var/log/mpd.log
####

#vi /etc/newsyslog.conf
/var/log/mpd.log 644 5 100 * Z
#重新启动syslogd
killall -HUP syslogd

####################################

介绍 Sysinstall

主页

http://www.freebsd.org/doc/zh_CN.GB2312/books/handbook/index.html





FreeBSD 使用手册

上一页 第2章  安装 FreeBSD 下一页

--------------------------------------------------------------------------------

2.5 介绍 Sysinstall
  sysinstall 是 FreeBSD 项目所提供的安装程序。 它以 console(控制台)为主,分为多个菜单及画面让您配置及控制安装过程。

  sysinstall 菜单画面由方向键、Enter 、 Space、以及其它按键所控制。在主画面的 Usage 菜单 有这些按键的说明。

  要查看这些说明,请将光标移到 Usage 项目,然后 [Select] 按键被选择,图 2-3, 然后按下 Enter 键。

  安装画面的使用说明会显示出来,阅读完毕请按 Enter 键回到主画面。

图 2-3. 选取 Sysinstall 主菜单的 Usage 项目



2.5.1 选择 Documentation(说明文件) 菜单
  用方向键从主菜单选择 Doc 条目然后按 Enter键。

图 2-4. 选择说明文件菜单



  这将会进入说明文件菜单。

图 2-5. Sysinstall 说明文件菜单



  阅读这些说明文件很重要。

  要阅读一篇文章,请用方向键选取要阅读的文章然后按 Enter 键。阅读中再按一下 Enter 就会回到说明文件画面。

  若要回到主菜单,用方向键选择 Exit 然后按下 Enter 键。

2.5.2 选择键盘对应(Keymap)菜单
  如果要改变键盘按键的对应方式,请在主菜单选取 Keymap 然后按 Enter 键。一般情况下不改变此项,除非您使用了非标准键盘或非 美国键盘。

图 2-6. Sysinstall 主菜单



  您可以使用上下键移动到您想使用的键盘对应方式,然后按下 Space 键以选取它;再按 Space 键可以取消选取。当您完成后,请选择 [ OK ] 然后按 Enter 键。

  这一屏幕只显示出部分列表。选择 [ Cancel ] 按 Tab 键将使用 默认的键盘对应,并返回到主菜单

图 2-7. Sysinstall 键盘对应菜单



2.5.3 安装选项设置画面
  选择 Options 然后按 Enter 键。

图 2-8. Sysinstall 主菜单



图 2-9. Sysinstall 选项设置



  预设值通常可以适用于大部分的使用者,您并不需要改变它们。版本名称要根据安装的版本进行变化。

  目前选择项目的描述会在屏幕下方以蓝底白字显示。注意其中有一个项目是 Use Defaults(使用默认值)您可以由此项将所有的设定还原为预设值。

  可以按下 F1 来阅读各选项的说明。

  按 Q 键可以回到主画面。

2.5.4 开始进行标准安装
  Standard(标准) 安装适用于那些 UNIX® 或 FreeBSD 的初级使用者。用方向键选择 Standard 然后按 Enter 键可开始进入标准安装。

图 2-10. 开始进行标准安装




--------------------------------------------------------------------------------
上一页 起始页 下一页
开始安装 上一级 分配磁盘空间

本文档和其它文档可从这里下载:ftp://ftp.FreeBSD.org/pub/FreeBSD/doc/.

如果对于FreeBSD有问题,请先阅读文档,如不能解决再联系.
关于本文档的问题请发信联系 .

review

review
pOut[optr++] = (char)bcal;
(char) 将int数据转为char字符.通过指针下标,传递给参数中的字符指针(间接访问),[optr++] 首次调用值为0,后递增,

6:27 PM | Add a comment | Permalink | Trackbacks (0) | Blog it | Delphi
September 25
位运算
int CodeMWCode( BYTE * pIn, int size, CHAR * pOut)
{
BYTE b1 = 0,bcal = 0;
BYTE bflag1 = 0,bflag2 = 0;
int i = 0;
int iptr = 0;
int optr = 0;
while( iptr < size )
{
b1 = pIn[iptr++] ^ 0xeb; //[11101011] 按位翻转
if( i < 2 ) //i为0、1时执行
{
bcal = b1;
bcal>>=2; //即[12345678]--->[00123456]=bcal
bflag1 =bcal;
bcal &= 0x3c; //0x3c(00111100) ,取bcal的3-6位,赋给bcal,即[00123456]--->[00123400]=bcal
b1 &=3; //3(00000011),取b1的1-2位,赋给b1,即[12345678]--->[00000078]=b1
bcal |= b1; //bcal=bcal合并b1,[00123400] | [00000078] = [00123478]=bcal
bcal += 0x3b;
pOut[optr++] = (char)bcal;

//(char) 将int数据转为char字符.通过指针下标,传递给参数中的字符指针(间接访问),[optr++] 首次调用值为0,后递增,


bflag2 = (bflag1 & 3 ) | (bflag2 << 2 ); //重复移位,取00123456中的56与bflag2(0)合并后,赋bflag2,等i=1,循环到达第2遍时,移位完成,00005656,
}
else //i为2时执行
{
bcal = b1;
bcal &= 0x3f;
bcal += 0x3b;
pOut[optr++] = (char)bcal;
b1>>= 2;
b1 &=0x30;
b1 |= bflag2;
b1 += 0x3b;
pOut[optr++] = (char)b1;
bflag2 = 0;
}
i ++; //i递增一次
i %=3; //i与3取模(求余),并重赋值。(0-2之间值)
}
pOut[optr] = 0;
if( i == 0 )
return optr;
pOut[optr++] = bflag2 + 0x3b;
pOut[optr] = 0;
// 打标记??
return optr;
}



写了一半的注释~事实还没完全明白第一行的反转是何用意。(也许仅仅是个翻转吧~)

累了~sleep~

部落龙火护符任务攻略(这些全都要做)

部落龙火护符任务攻略

一。龙火护符系列任务
奥尼克西亚巢穴钥匙任务
顺序:高图斯的命令 > 伊崔格的智慧 > 为部落而战!>风吹来的消息 > 部落的勇士 > 雷克萨的证明 > 黑龙幻象 > 埃博斯塔夫 > 龙骨试炼,索姆努斯 > 龙骨试炼,斯克利尔 > 龙骨试炼,克鲁纳里斯 > 龙骨试炼,埃克托兹 > 晋升…… > 黑龙勇士之血

第一个任务,“高图斯的命令”,接任务NPC名字叫做军官高图斯,大家可以在卡加斯旅店对面的哨塔顶上见到他,有时候他也会下来在卡加斯带领着卡加斯远征军绕圈巡逻,他的头上不一定有接任务的黄色叹号。
接到第一个任务之后就可以顺着情节一直做下去了,这里我不说详细攻略。
第二个任务,“伊崔格的智慧”,交任务的NPC---“伊崔格”就在智慧谷大厅,萨尔的身边。
雷克萨,带着他的熊--米沙,徘徊在凄凉之地,从石爪山脉到菲拉斯的那条大路上。
任务“ 黑龙幻象” 的NPC--巫女麦兰达 ,在西瘟疫之地的联盟飞行点悔恨岭门口的乌瑟尔之墓,一棵树桩子上面站着。地点自己去看网页:
http://games2.sina.com.cn/g_item/showpic.php?id=7138
任务 “黑龙勇士之血” ,每一次黑石塔上层副本,只有3个。

最后奖励:
龙火护符
拾取后绑定
唯一
颈部
+10 耐力
+15 火焰抗性
需要等级 50
"达基萨斯的血液在护符中涌动…"

没有这个物品,你将无法参加我们组织的屠龙行动,简称OMM。

二。水之精粹系列任务
奖励+15火焰抗性(即FR)的戒指
需要任务奖励的物品--水之精粹才能召唤MC第九和第十号boss
顺序:被囚禁的水元素 > 雷暴和磐石 > 艾博希尔之眼 > 熔火之心 > 海达克西斯的使者 > 敌人之手 > 英雄的奖赏

第一个和第二个任务可以同时接到,给这个任务的NPC----海达克西斯公爵,在艾萨拉海上最右下角的一个小岛上。地点自己去看网页:
http://games2.sina.com.cn/g_item/showpic.php?id=7861

任务完成到---海达克西斯的使者,之后大概打2次MC1~8,你的声望就会达到要求。

杀死几个MC的BOSS的时候,有人问我,地上的“XXXX符文”可以点吗,这里我再次说明:这个符文是用来召唤MC 的第九和第十号BOSS用的,需要这个任务的奖励物品---水之精粹,当我们杀死一个BOSS之后,团长会指定一个人去“灭火”,其他人不得使用这个符文!

水之精粹,每次使用完毕,需要去海达克西斯公爵那里再取一个,这样就要求大家都有水之精粹,而且指定去“灭火”的队员,在团队活动后应该及时去海达克西斯公爵那里再取一个。

三。超越系列任务
许多公会把这个任务作为参加MC团队的考察任务
我们虽然没有这个要求,但是强烈推荐大家都要完成,作为个人技术的评价
顺序:档案管理员 > 可怕的真相 > 超越

在东瘟疫飞行点找到NPC--尼古拉斯·瑟伦霍夫公爵,可以接"档案管理员"任务,任务是要求5人队伍去斯坦索姆杀死血色区的档案管理员加尔福特。
交了这个任务之后你不会接到新的任务,但是当你再次组织5人队伍去斯坦索姆血色区的时候,就可以在最后的boss恐惧魔王巴纳扎尔的尸体上拾取他的头颅,触发一个任务----可怕的真相,交了任务,可以接到任务----超越,需要5人队伍杀死斯坦索姆亡灵区的最后boss,瑞尔代文男爵(就是DK),取得他的头颅。

任务奖励:(注意奖励是在交任务NPC身边的箱子里面)
银色十字军
法杖
拾取后绑定
唯一
耐力+6,智力+30,精神+10
还有盾牌和单手剑可以选择,但是都没有使用价值。

四。爱与家庭系列任务
如果你对WOW历史有所了解,如果你从头到尾看过这一系列任务的人物说明,你会发现这是一个很感人的故事,推荐Tank战士完成。

顺序:腐肉虫,恶魔之犬,血染的天空 > 救赎 > 遗忘的记忆 > 失落的荣耀 > 爱与家庭 -1 > 爱与家庭-2 > 寻找麦兰达 > 血色幻象 > 在梦中

你可以在东瘟疫之地的左上角,河边的山洞口处找到接任务的NPC--提里奥·弗丁(老弗丁),接到腐肉虫,恶魔之犬,血染的天空 3个任务。地点自己去看网页:

http://games2.sina.com.cn/g_item/showpic.php?id=7143

完成了这3个冗长的任务,老弗丁会继续给你任务--救赎,听完老弗丁的故事,获得下一个任务--遗忘的记忆。
去南面墓室点坟墓,出现4怪小队,轻松灭之,拿到锤子,交给老弗丁,接到后续任务--失落的荣耀。
按照任务提示,在北谷水下找到旗帜,交给老弗丁,接到后续任务“爱与家庭-1”。
去通零学院的那个岛,凯尔达隆岛,找到名叫瑞弗蕾的艺术家,他就在渡口旁边的小房子里面,接到后续任务“爱与家庭-2”。
接下来组织5人队伍去攻克斯坦索姆血色区,在"档案管理员"屋子里面找到画(可以和"档案管理员"任务一起完成),交还给老弗丁,接到后续任务“寻找麦兰达”。
麦兰达就是制作“黑龙幻象”的那个巫婆,可以和“黑龙幻象”任务一起完成。
交过任务别忙着走,需要请麦兰达把你变成血色十字军的样子,状态持续30分钟,期间不能骑马,不能使用部分技能(战士切换形态竟然可以转换男女,有意思,别的职业不知道会怎样),向北深入壁炉谷。
下面的任务自己一个人就可以完成,前提是没有联盟骚扰。
你已经变成血色十字军的样子,壁炉谷里面和附近的血色十字军不会攻击你,你可以顺利到达最北面的最大的城堡2层找到大领主泰兰·弗丁(小弗丁),接到任务--在梦中,护送小弗丁离开壁炉谷。
下面需要注意的是,不要取消自己的变形状态,跟着小弗丁一路出来,相信他,他的实力可以单挑4个60的精英,完全不用帮忙。跟随他到了门口,会出现4个60的精英十字军可能还跟随有几个非精英,不要怕,小弗丁就算快没有血也不要你的帮助,他是圣骑士,会给自己加血的。
最后在通向壁炉谷的狭长小路上,左边的塔里面会出现一个大检查官及其仆从很多人,来阻拦你和小弗丁,这时候小弗丁会死亡。但是不要紧张,就算他死亡,你也不能取消变形来帮助他(因为加上你也失白给啊)。这是个悲剧的故事结局,小弗丁的死亡是必须发生的。在他死后不久,你将会看到老弗丁骑着马来营救,不过为时已晚(不明白得是老弗丁也是圣骑士,为什么不能复活自己的儿子),扶尸痛哭之后他会大展神威,把敌人全部杀光。最后老弗丁头上出现交任务的问号,你可以完成任务了。
如果在壁炉谷一行中,你失去了血色十字军的伪装,也不要紧,记得不要帮小弗丁太多,顶多帮他杀杀快要死了准备逃跑的血色十字军就行了,不然自己会死掉的,他只会自己加血,也不会吸引仇恨的技能,无法照顾到你。
如果你死在大检察官那里,你千万不要释放灵魂,可以躺在地上看完这场悲剧,当老弗丁头上出现交任务的问号的时候,你释放了赶快跑来复活还来得及。
在任务过程中,你不能距离小弗丁太远,否则会任务失败,失败了需要放弃任务,回到麦兰达那里重新接任务,重新伪装。

任务奖励:三选一

精制合金胸甲
拾取后绑定
胸部 板甲
807点护甲
+22 耐力
装备: 防御提高15点。

弗丁印记
拾取后绑定
颈部
装备: 使你造成致命一击的几率提高1%。
装备: +26 攻击强度。

流放者斗篷
拾取后绑定
背部
45点护甲
+7 耐力
+15 智力
+7 精神
可能还有别的奖励可以选择,我忘记了。推荐战士做这个任务的原因是这个奖励--精制合金胸甲,是比较好的TANK胸甲了。

顺便提一下,可以选择的TANK胸甲还有2个,分别是:
克罗卡斯的胸甲
拾取后绑定
胸部 板甲
777点护甲
+16 力量
+16 耐力
需要等级 57
装备: 防御提高15点。
-------厄运北,大王之前的卫兵克罗卡斯掉落,这个BOSS杀起来比较困难,而且杀了他就没有完美贡品了,所以一般去厄运北的队伍都选择做一件戈多克食人魔装,把他骗走。

亡骨胸甲
拾取后绑定
胸部 板甲
637点护甲
+12 耐力
+12 精神
生命值 +30
需要等级 56
装备: 防御技能提高17点。
套装:亡者之骨(1/5)
-------通灵学院,院长之前的几个BOSS随机掉落。


补充一个:
希利苏斯有一系列新的任务,做到最后,需要杀一个30~40人的团队BOSS洛曼卡恩大使,做完这个任务以后给的奖励听说也很不错,推荐大家都去把任务做到最后一步----召唤。等到会里有人的塞纳里奥议会声望到达尊敬或者更高的时候,公会可能考虑组织活动,团队杀掉那个BOSS洛曼卡恩大使,到时候你的任务没到这一步就赶不上了,组野队的话喊40人可不容易啊!
具体任务过程我就不写了,有个网页说的已经很详细了:

黑龙MM之[龙火护符]获得方法
作者:不吃巧克力来源:風云发布:2006-09-15 11:57:06.0阅读:1341次评论:0条
原文链接:http://hyfy2006.duniu.com/articleview/4.html
  本站原创文章均为版权所有,如需转载请注明出处
起始任务:
  · 高图斯的命令 · lvl61 Elite0
  杀死欧莫克大王、指挥官沃恩和维姆萨拉克。找到重要的黑石文件,然后向卡加斯的军官高图斯汇报。
  任务目标:
  维姆萨拉克
  欧莫克大王
  指挥官沃恩
  重要的黑石文件
  根据军官高图斯的命令,黑石塔的下列主要首领必须被消灭:
  对我们造成巨大威胁的欧莫克大王。
  残忍无情的巨魔指挥官沃恩。

  监督者维姆萨拉克。
  如果你找到何重要的文件,也必须一并上缴。
  成功则受人敬仰。
  失败则被人遗忘。
  任务给予人:
  高图斯的命令
  高图斯的命令 <卡加斯远征军>
  荒芜之地
  任务回覆人:
  军官高图斯 <卡加斯远征军>
  荒芜之地
  任务回报:
  赏金 185
  经验 8550
  可选: 维姆萨拉克的镣铐/欧莫克的瘦身腰带/哈雷肯的笼口/沃什加斯的绳索/沃恩的邪恶之握
  和卡加斯营地里,一个哨塔上的高图斯对话,有可以对话的选项,和他聊完天,可以得到一封信,打开就可以接到任务.按照任务指示去LBRS杀掉3个boss,文件会在3个boss身边以及乌洛克(需要召唤)那里随即刷,可以多人拿,看运气了.

  · 伊崔格的智慧 · lvl610
  和奥格瑞玛的伊崔格谈一谈。讨论完毕后,咨询萨尔的意见。
  你回忆起曾在萨尔的大厅中见过伊崔格。
  雷德还活着?
  不可能!
  据说雷德在数十年前就被杀掉了。
  去寻求伊崔格的建议吧,阿牧。没有谁能比他更了解黑石氏族的情况了,如果这上面所写的都是真的,那么你应该通知伊崔格。谁都有复仇的权利。
  你可以在奥格瑞玛找到他。
  在你跟伊崔格谈过话之后,便去与酋长交流一下,搞清楚他想要如何处理这个问题。
  任务给予人:
  军官高图斯 <卡加斯远征军>
  荒芜之地
  任务回覆人:
  萨尔 <酋长>
  奥格瑞玛 (32,38)
  任务回报:
  经验 6850
  去奥格瑞玛智慧谷,在萨尔的大厅里能找到2个NPC,对话后继续任务.

  · 为部落而战! · lvl63 Elite0
  去黑石塔杀死大酋长雷德·黑手,带着他的头颅返回奥格瑞玛。
  任务目标:
  雷德·黑手的头颅
  雷德受到黑龙的保护,因此他才如此狂妄嚣张。
  你,<玩家>须寻找一条穿过晋升大厅的道路,找到大酋长雷德·黑手并杀死他——为部落而战!
  <萨尔一拳打在王座上。>
  高举着他的头颅凯旋而归吧,将头颅呈献给你的酋长。去吧,你将成为部落的英雄!
  任务给予人:
  萨尔 <酋长>
  奥格瑞玛 (32,38)
  任务回覆人:
  萨尔 <酋长>
  奥格瑞玛 (32,38)
  任务回报:
  赏金 285
  经验 10900
  可选:暴君印记\比斯巨兽之眼\黑手饰物
  组团去UBRS,杀了雷德黑手,就能拿到任务物品,回去交了任务继续.
  风吹来的消息 · lvl630
  听萨尔讲话。
  很高兴再次看到你,<玩家>。风从东部王国带来了新的消息。
  坐下,听我说。
  任务给予人:
  萨尔 <酋长>
  奥格瑞玛 (32,38)
  任务回覆人:
  萨尔 <酋长>
  奥格瑞玛 (32,38)
  任务回报:经验 725

  · 部落的勇士 · lvl630
  按照酋长的指示找到雷克萨。他在石爪山和菲拉斯之间的凄凉之地游荡。
  我从我的一位勇士那里收到消息说,也许的确有一个进入龙穴的通道。你可以和他谈谈。
  雷克萨在凄凉之地的沙漠荒地上四处游荡,就在石爪山和菲拉斯之间。他在等着你。
  任务给予人:
  萨尔 <酋长>
  奥格瑞玛 (32,38)
  任务回覆人:
  雷克萨 <部落的勇士>
  菲拉斯
  凄凉之地
  石爪山
  任务回报:经验 3650
  雷克萨大人会沿着从石爪山脉到菲拉斯的主干道游走,慢慢找吧.

  · 雷克萨的证明 · lvl630
  把雷克萨的证明交给西瘟疫之地的巫女麦兰达。
  你对幻象有什么了解吗,<玩家>?你必须要制造一个幻象,这样你才能骗过那些黑龙的眼睛。
  我知道有一个人也许愿意帮你完成任务。她过去曾经帮助过我们,因此她值得信任。
  你可以在西瘟疫之地找到巫女麦兰达,她是是被洛丹伦联盟放逐的幻术师。把这个交给她。
  任务给予人:
  雷克萨 <部落的勇士>
  任务回覆人:
  巫女麦兰达
  西瘟疫之地
  任务回报:经验 5450
  麦兰达在西瘟疫的悔恨岭

  · 黑龙幻象 · lvl63 Elite0
  到黑石塔去收集20颗黑色龙人的眼球,完成任务之后回到巫女麦兰达那里。
  任务目标: 黑色龙人的眼球 ×20
  那么,一个巨魔想要变成一条黑龙,是吗?
  你所通过一个试炼才能制造幻象,但是我们所需的原料却是非常难以获得的。雷克萨和酋长都非常信任你,而且把这个任务交给了你,所以麦兰达也同样认为你可以胜任。
  到黑石塔的上层堡垒去,杀掉足够多的黑色龙人,把它们的眼睛给我拿回来。
  任务给予人:
  巫女麦兰达
  西瘟疫之地
  任务回覆人:
  巫女麦兰达
  西瘟疫之地
  任务回报:经验 7250
  接了任务后再次向UBRS出发,杀里面的龙型生物就能拿到眼球.
  · 埃博斯塔夫 · lvl63 Elite0
  到尘泥沼泽中的巨龙沼泽去,找到埃博斯塔夫的洞穴。进入洞穴之后戴上龙形护符,然后跟埃博斯塔夫交谈。
  我已经对这块奖章施加了魔法,它可以在埃博斯塔夫的巢穴里制造并维持幻影。
  埃博斯塔夫是一条古老的龙,它是奈法利安的卫士,专门负责考察那些即将成为奥妮克希亚精英守卫的龙人。
  到尘泥沼泽中的巨龙沼泽吧,埃博斯塔夫的巢穴就在那里。进入巢穴之后戴上护符,让命运指引你前进吧!
  任务给予人:
  巫女麦兰达
  西瘟疫之地
  任务回覆人:
  埃博斯塔夫
  尘泥沼泽
  任务回报:经验 5450
  埃博斯塔夫在尘泥沼泽东南的一个洞穴里
  进去后记得使用护符,变成龙后才能继续任务.以后几次进洞都要使用护符.

  · 龙骨试炼,斯克利尔 · lvl63 Elite0
  找到蓝龙斯克利尔并杀掉他。从他的身上取下他的颅骨,然后将其交给埃博斯塔夫。
  你可以在冬泉谷找到斯克利尔。
  任务目标: 斯克利尔的颅骨 ×1
  我们是死亡之翼的孩子,小黑龙。我们对于其它低等龙类的控制必须得到保障。
  如果你真的有价值的话,那些低等龙类就会因为你的到来而毁灭。
  找到他们,以我们父亲的名义消灭他们!
  到冬泉谷去猎杀强大的蓝龙斯克利尔,杀了它以及任何胆敢阻挡你的蓝龙。把它的颅骨给我拿回来。
  任务给予人:
  埃博斯塔夫
  尘泥沼泽
  任务回覆人:
  埃博斯塔夫
  尘泥沼泽
  任务回报:经验 7250
  现在开始著名的屠龙行动了.蓝龙在冬泉谷的麦索瑞尔,洞内外都是56+的精英龙,不过组队去很轻松,还能从他们身上打到很不错的东西.
  斯克利尔60级精英,会冰霜法术

  · 龙骨试炼,克鲁纳里斯 · lvl63 Elite0
  诺兹多姆的孩子克鲁纳里斯在塔纳利斯沙漠守卫着时光之穴。杀了他,把他的颅骨交给埃博斯塔夫。
  任务目标:克鲁纳里斯的颅骨 ×1
  诺兹多姆,时间之王……他的孩子们……
  有三条龙看守着时光之穴,但我们黑龙只对其中一条感兴趣:时光的转换者,克鲁纳里斯,他是诺兹多姆最喜欢的孩子……
  把他杀掉,让整个沙漠都因你的力量而震动,把他的颅骨给我拿回来……
  任务给予人:
  埃博斯塔夫
  尘泥沼泽
  任务回覆人:
  埃博斯塔夫
  尘泥沼泽
  任务回报:经验 7250
  克鲁纳里斯61级精英
  在杀他之前还会看见2条精英龙,一条50一条52,很嫩.

  · 龙骨试炼,索姆努斯 · lvl63 Elite0
  杀掉绿龙索姆努斯,把他的颅骨交给埃博斯塔夫。
  任务目标: 索姆努斯的颅骨 ×1
  我们的父亲击溃了其它守护巨龙,我们也必须给他们的孩子带来混乱和毁灭。
  你必须在那些绿龙的孩子身上检验你的力量。
  到悲伤沼泽去找到索姆努斯,它是绿龙的勇士。让它感受你的愤怒,杀掉那只可怜的小虫……把他的颅骨给我拿回来……
  任务给予人:
  埃博斯塔夫
  尘泥沼泽
  任务回覆人:
  埃博斯塔夫
  尘泥沼泽
  任务回报:经验 7250
  索姆努斯62级精英,自然属性法术攻击.

  · 龙骨试炼,埃克托兹 · lvl63 Elite0
  到格瑞姆巴托去杀掉红龙埃克托兹,把他的颅骨交给埃博斯塔夫。
  任务目标:埃克托兹的颅骨 ×1
  我们的父亲在格瑞姆巴托的群山中大发神威,对抗守护之龙的力量。阿莱克斯塔萨,那个懦弱的背叛者,她畏怯地逃跑了。
  多么可耻的行为……
  现在红龙守卫着格瑞姆巴托,他们的领导者埃克托兹必须被干掉,把他的颅骨给我带回来。为了我们的父亲而战,小黑龙!
  任务给予人:
  埃博斯塔夫
  尘泥沼泽
  任务回覆人:
  埃博斯塔夫
  尘泥沼泽
  任务回报:经验 7250
  埃克托兹62级精英,火焰属性法术攻击,在湿地的东面
  要杀红龙必须先杀了前面3条龙中的至少一条,而且红龙比较弓虽,最好带2个治疗者.
  · 晋升…… · lvl630
  看来这场假面舞会就要结束了。你知道麦兰达为你制作的龙形护符在黑石塔里面不会发挥作用,也许你应该去找雷克萨,将你的困境告诉他。把黯淡的龙火护符给他看看,也许他知道下一步该怎么做。
  我用我们的那些敌人的颅骨制作了一枚勋章,你知道这枚勋章的用处吧?你以前一定看到过你的前辈戴着这样的一枚勋章了。
  拿着它,<玩家>。回到黑石塔去,把它给达基萨斯将军看看,将军会对它进行最后的点化,将它与你的灵魂绑在一起。
  完成之后的勋章将使你感到你所拥有的荣誉,它象征着你已经达到了一个令人敬畏的高度:我们母亲的守护者。
  现在就出发吧!
  任务给予人:
  埃博斯塔夫
  尘泥沼泽
  任务回覆人:
  雷克萨 <部落的勇士>
  任务回报:经验 5450
  杀了4条龙后拿到黯淡的龙火护符
  ,然后去找雷克萨大人,他会给你进一步指示.

  · 黑龙勇士之血 · lvl63 Elite0
  到黑石塔去杀掉达基萨斯将军,把它的血交给雷克萨。
  任务目标:黑龙勇士之血 ×1
  你要去拜访一下将军,没错,但并不是以黑龙的样子去拜访。
  你看,这个仪式所需的不过是更多的流血而已。
  这个未完成的饰物只需要将军的血就可激活,这是它们那野蛮而原始的备用机制。
  回到黑石塔去干掉达基萨斯,把他的血拿来给我,然后我就可以帮你激活开启奥妮克希亚巢穴的钥匙。
  任务给予人:
  雷克萨 <部落的勇士>
  任务回覆人:
  雷克萨 <部落的勇士>
  任务回报:经验 10900
  龙火护符

  拾取后绑定
  唯一
  颈部
  +10 耐力
  +15 火焰抗性
  需要等级 50
  "达基萨斯的血液在护符中涌动…"
  这是最后一步了,UBRS的将军想必你已经杀了N次了吧,再多杀它一次吧.从将军身上拿到血,交给雷克萨大人就算收工了