紫月城游戏软件

低调做技术

2008年8月20日 #

代码自动生成-宏递归思想

Macro Recursion
author: Kevin Lynx

Preface

    本文可能是<代码自动生成-宏带来的奇技淫巧>的续写。我尽力阐述如何让宏递归(或者说重复)地有规律地产生一
些符号,而让我们少写很多重复代码,也许这些代码只有那么一点点的不同。将这项小技巧用于底层库的编写,会让代码
看起来干净不少,同时文件尺寸也会骤然下降。


Problem


    如果你曾经写过functor,那么你肯定对某些代码进行粘贴复制然后修改。更让人郁闷的是,这些代码基本是一样的。
例如,一个典型的functor可能为:

    template <typename Prototype>
    
class functor;
    template 
<typename R, typename P1>
    
class functor<R(P1)>;
    template 
<typename R, typename P1, typename P2>
    
class functor<R(P1,P2)>;


    //好,接下去你可能厌烦了,可能会复制一个带有两个参数的functor,然后修改为处理3个参数的。
    这只是一个很简单的问题。宏不是c++里的东西,本文自然也不是讨论各种花哨的模板技术的。如果我之前那篇关于
宏的文章只是让你去分析问题以及更深层次地认识宏,那么现在我将分享我的这部分思想给你。
    关于上面的问题,我们期待得到这样的解决方案:

    template <typename R, DEF_PARAM( 2 )>
    
class functor<R( DEF_ARG( 2 ) )>;


    那么,它将自动生成:

    template <typename R, typename P1, typename P2>
    
class functor<R(P1,P2)>


    也就是说,DEF_PARAM(n)这个宏将根据n值自动生成一串符号,例如DEF_PARAM(2)就生成typename P1, typename P2。
同样,DEF_ARG(n)也会根据参数生成类似于P1,P2,...,Pn的符号串。

思考

    仔细思考下,我们可以看出DEF_PARAM和DEF_ARG这样的宏具有一种递归的特性(其实说成重复可能更合适):每次展
开的内容基本一样,不断调用自身直到遇到终止条件。
    那么,我们的目标锁定于,用宏来实现递归。


Pre-Implement

    在开始之前,我需要告诉你一些基本的东西:
    在阅读一个宏时,你最好按照预处理的处理方式去逐个展开。当我说到展开时,我的意思是把宏替换为宏体。预处理器
展开宏的过程大致为:如果宏参数也是个宏,那么先将宏参数全部展开,再展开该宏;这个时候会扫描展开后的宏,如果
遇到其他宏,则继续展开。例如有一下宏:

 

#define PI 3.14
#define MUL_PI( n ) n * PI
#define TWO 2


    当我们写下MUL_PI( TWO )时,预处理发现MUL_PI的参数TWO 是个宏,那么先将TWO展开得到2,然后将2放进宏体展开
得到 2 * PI;预处理器对 2 * PI 进行扫描,发现还有宏PI,于是对PI做展开,得到 2 * 3.14。这个过程是递归的。
    但是也有例外,如果MUL_PI对宏参数进行了#或者##,那么该宏参数不会被展开。(参见以前那篇文章吧)
    任何时候,你可以通过以下宏去查看某个宏展开后的样子,可以方便你调试你的宏:

#define TO_STRING( x ) TO_STRING1( x )
#define TO_STRING1( x ) #x 


    (为什么要写个TO_STRING1,因为这是为了让x充分展开,避免上面提到的那个例外)   

    其他规则我会在文中需要的地方提出来。
实现

    就像大部分介绍递归函数时候给的例子,这里我也将阶乘作为例子。考虑如下典型的阶乘函数:

    int fac( int n )
    
{
        
if( n == 1 ) return 1;
        
return n * fac( n - 1 );
    }
 


    其核心部分在于 n * fac( n - 1 ),我们假设我们的宏也可以写成这样的的形式:

    #define FAC( n ) n * FAC( n - 1 )


    但是这样的宏是有问题的:
    当宏被展开时,如果遇到了自身,那么将被处理为一般符号,例如展开FAC( 3 )时,会遇到 FAC( 2 ),那么就把FAC
( 2 )中的FAC当成了一搬符号。
    这样的限制注定了我们无法让宏真正地调用自身来实现递归。于是,我们不得不写下以下丑陋的符号,从而去模拟递
归的每一次符号调用:

#define FAC_1( n ) 1
#define FAC_2( n ) n * FAC_##(n-1)( n - 1 )
#define FAC_3( n ) n * FAC_##(n-1)( n - 1 ) 


    这系列宏有点别扭(如果你足够细心),因为我们明显知道FAC_2返回的将是2,而FAC_3返回的当时6。我们这里只是
模拟,同样,这使得我们可以把FAC_1作为递归的终止条件。
    我们的预想是,当调用FAC_3时,它把n-1的值2合并到FAC_中,从而调用FAC_2,以此类推。
    但是这依然有问题,编译器会提示‘找不到符号FAC_’。导致这个问题的原因在于:宏展开只是单纯的字符替换,是我们
想太多了,预处理器并不会去计算( n - 1 )的值是多少,也就是我们无法得到FAC_2这个宏。

    所以,FAC_3( 3 ) 会被初次替换为 3 * FAC_(3-1)( 3 - 1 )。这个时候编译器就把FAC_当成了一个普通符号。我们可以
自己定义个FAC_来证明这一点:

 

#define FAC_( n ) T 

 

    那么,FAC_3( 3 )就被替换为 3 * T(3-1)( 3 - 1 )。

    解决这个问题的办法关键在于,让预处理器自动计算出( n - 1 )。记住,我们解决问题的唯一办法是:字符替换。
所以,我们可以写下如下代码:

 

#define DEC_1 0
#define DEC_2 1
#define DEC_3 2 

#define DEC( n ) DEC_##n 

 

    通过,DEC( n )这个宏,我们可以获取到一个( n -1 )的数。例如,DEC( 3 )被替换为 DEC_3,继续替换为 2。

    于是,我们新的FAC系列宏变为:

 

#define FAC_1( n ) 1
#define FAC_2( n ) n * FAC_##DEC( n )( n - 1 )
#define FAC_3( n ) n * FAC_##DEC( n )( n - 1 ) 

 

    不好意思,这样依然是不正确的!预处理器直接把FAC_和DEC( n )连接成符号了,而不是单个地先处理他们,最后再
合并他们。

    OK,先解决这个问题:先处理FAC_和DEC( n ),再合并他们,而不是先合并他们。要解决这个问题,可以通过第三个宏
来实现:

 

#define CHR( x, y ) x##y 

 

    作为连接两个符号为一个符号的宏,这个宏显然是不正确的,因为宏展开还有个规则:如果宏体对宏参数使用了#或##,
那么宏参数不会被展开,也就是说:如果CHR( FAC_, DEC( 3 ) 那么得到的只会是 FAC_DEC( 3 )。通常情况下我们是
再写个宏:

 

#define CHR( x, y ) CHR1( x, y )
#define CHR1( x, y ) x##y 

 

    从而可以保证在正式连接x和y前,x和y都被完全展开。

    这个时候,我们的FAC系列宏变为:

 

#define FAC_1( n ) 1
#define FAC_2( n ) n * CHR( FAC_, DEC( n ) )( n - 1 )
#define FAC_3( n ) n * CHR( FAC_, DEC( n ) )( n - 1 ) 

 

    结果呢?结果还是有问题。= =
    我们假设CHR( FAC_, DEC( n ) )已经真的按我们预想展开为 FAC_2了,那么FAC_3( 3 )会被展开为什么呢?
被展开为 3 * FAC_2( 3 - 1 )。这是错误的,传给 FAC_2 的参数是 3 - 1就意味着错误。我们又臆想预处理器会
帮我们计算 3 - 1的结果了。我们必须保证传给 FAC_2的参数是个数字2。解决这个问题的办法就是通过DEC(n)宏。

   于是,FAC系列宏变为:

 

#define FAC_1( n ) 1
#define FAC_2( n ) n * CHR( FAC_, DEC( n ) )( DEC( n ) )
#define FAC_3( n ) n * CHR( FAC_, DEC( n ) )( DEC( n ) ) 

 

    这个时候,FAC_3( 3 )将会被替换为:3*2*1。这就是我们要的结果。

In practice

    以上只是向你展示一个过程,用宏去计算阶乘,就像用模板去计算阶乘(模板元编程)一样,只是一个用于展示的东西,
没有什么实际价值。接下来我们开始有实际的工作,完成之前的预想:

 

template <typename R, typename P1, typename P2, typename P3>
class functor<R (P1, P2, P3)> 

 

    直接:

 

template <typename R, PARAM( 3 )>
class functor<R (ARG( 3 ))> 

 

    先考虑PARAM宏,该宏的任务就是生成类似于:typename P1, typename P2, typename P3这样的符号。我们假象它每一次
递归都生成 typename Pn, 的字符串,那么当他递归完时,可能就生成typename P1, typename P2, typename P3, 结果
多了个逗号,也许最后一次结果不该有逗号。

    ARG宏和PARAM宏本质上相同,只是重复的符号不是typename Pn,而是Pn。

    最直接想到的是:

 

#define PARAM_1( n ) typename P##n
#define PARAM_2( n ) CHR( PARAM_, DEC( n ) )( DEC( n ) )##,typename P##n
#define PARAM_3( n ) CHR( PARAM_, DEC( n ) )( DEC( n ) )##,typename P##n 

 

    结果我们得到了个错误的展开结果:
typename PDEC( 2 ),typename PDEC( 3 ),typename P3

    这个问题出在:PARAM_3( 3 )当替换为 PARAM_2( DEC( n ) )时,因为PARAM_2(n)宏对于宏参数n使用了##,也就是那个
typename P##n,所以这里不会把 DEC( n )展开,而是直接接到P后面。所以就成了typename PDEC( 3 )。

    为了消除这个问题,我们改进PARAM为:

 

#define TYPE( n ) ,typename P##n
#define PARAM_1( n ) CHR( typename P, n )
#define PARAM_2( n ) CHR( CHR( PARAM_, DEC( n ) )( DEC( n ) ), TYPE( n ) )
#define PARAM_3( n ) CHR( CHR( PARAM_, DEC( n ) )( DEC( n ) ), TYPE( n ) ) 

 

    之所以加入TYPE(n),是因为 ,typename P##n 这个宏本身存在逗号,将其直接用于宏体会出现问题。

    于是,我们得到了正确的结果。

    其实,PARAM系列宏宏体基本是一样的,除了终止条件那个宏,为什么我们要写这么多呢?理由在于宏体不能自己调用
自己,所以才有了PARAM_3, PARAM_2。

    我们可以将上面的一系列宏抽象化,使其具有可复用性:

 

#define PARAM( n ) ,typename P##n
#define PARAM_END typename P 

#define ARG( n ) ,P##n
#define ARG_END P 

#define PARAM_1( n ) CHR( typename P, n )
#define PARAM_2( n ) CHR( CHR( PARAM_, DEC( n ) )( DEC( n ) ), TYPE( n ) )
#define PARAM_3( n ) CHR( CHR( PARAM_, DEC( n ) )( DEC( n ) ), TYPE( n ) ) 

#define REPEAT_1( n, f, e ) CHR( e, n )
#define REPEAT_2( n, f, e ) CHR( CHR( REPEAT_, DEC( n ) )( DEC( n ), f, e ), f( n ) )
#define REPEAT_3( n, f, e ) CHR( CHR( REPEAT_, DEC( n ) )( DEC( n ), f, e ), f( n ) ) 

#define DEF_PARAM( n ) REPEAT_##n( n, PARAM, PARAM_END )
#define DEF_ARG( n ) REPEAT_##n( n, ARG, ARG_END ) 

 

    我们创建了可重用的REPEAT系列宏,用于创建诸如typename P1, typename P2, typename P3或者P1,P2,P3之类的符号,
通过更上层的DEF_PARAM(n)和DEF_ARG(n),就可以直接创建出我们上面所需要的符号串,例如:

    DEF_PARAM( 3 ) 就得到 typename P1, typename P2, typename P3
    DEF_ARG( 3 ) 就得到 P1, P2, P3

More in practice

    下载中提供了我使用这个宏递归技术写的lua_binder(如果你看过<实现自己的LUA绑定器-一个模板编程挑战 >),你
可以与上一个版本做一下比较,代码少了很多。
    同样,我希望你也能获取这种宏递归的思想。   

相关下载

   使用宏递归的lua_binder

posted @ 2008-08-20 17:48 Kevin Lynx 阅读(1406) | 评论 (9)编辑 收藏

2008年8月13日 #

实现自己的LUA绑定器-一个模板编程挑战

     摘要: 实现LUA绑定器 author : Kevin Lynx Preface     当LUA脚本调用我们注册的C函数时,我们需要逐个地从LUA栈里取出调用参数,当函数返回时,又需要一个一个地往LUA栈压入返回值,并且我们注册的函数只能是int()(lua_State*)类型。这很不方便,对于上层程序员来说更不方便。    因此我们要做的...  阅读全文

posted @ 2008-08-13 09:33 Kevin Lynx 阅读(1034) | 评论 (5)编辑 收藏

2008年8月12日 #

实现LUA脚本同步处理事件:LUA的coroutine

author : Kevin Lynx

需求

    受WOW的影响,LUA越来越多地被应用于游戏中。脚本被用于游戏中主要用于策划编写游戏规则相关。实际运用中,
我们会将很多宿主语言函数绑定到LUA脚本中,使脚本可以更多地控制程序运行。例如我们可以绑定NPCDialog之类的函数
到LUA中,然后策划便可以在脚本里控制游戏中弹出的NPC对话框。
    我们现在面临这样的需求:对于宿主程序而言,某些功能是不能阻塞程序逻辑的(对于游戏程序尤其如此),但是为
了方便策划,我们又需要让脚本看起来被阻塞了。用NPCDialog举个例子,在脚本中有如下代码 :

    ret = NPCDialog( "Hello bitch" )
   
if ret == OK then print("OK") end


    对于策划而言,NPCDialog应该是阻塞的,除非玩家操作此对话框,点击OK或者关闭,不然该函数不会返回。而对于
宿主程序C++而言,我们如何实现这个函数呢:

 

    static int do_npc_dialog( lua_State *L )
   
{
       
const char *content = lua_tostring( L, -1 );
       
        lua_pushnumber( ret );
       
return 1;
    }


    显然,该函数不能阻塞,否则它会阻塞整个游戏线程,这对于服务器而言是不可行的。但是如果该函数立即返回,那
么它并没有收集到玩家对于那个对话框的操作。
    综上,我们要做的是,让脚本感觉某个操作阻塞,但事实上宿主程序并没有阻塞。

事件机制

    一个最简单的实现(对于C程序员而言也许也是优美的),就是使用事件机制。我们将对话框的操作结果作为一个事件。
脚本里事实上没有哪个函数是阻塞的。为了处理一些“阻塞”函数的处理结果,脚本向宿主程序注册事件处理器(同GUI事件
处理其实是一样的),例如脚本可以这样:

    function onEvent( ret )
       
if ret == OK then print("OK") end
    end
   
-- register event handler
    SetEventHandler(
"onEvent" )
    NPCDialog(
"Hello bitch")


    宿主程序保存事件处理器onEvent函数名,当玩家操作了对话框后,宿主程序回调脚本中的onEvent,完成操作。
    事实上我相信有很多人确实是这么做的。这样做其实就是把一个顺序执行的代码流,分成了很多块。但是对于sleep
这样的脚本调用呢?例如:

 

    --do job A
    sleep(
10)
   
--do job B
    sleep(
10)
   
--do job C
   


    那么采用事件机制将可能会把代码分解为:

    function onJobA
       
--do job A
        SetEventHandlerB(
"onJobB")
        sleep(
10)
    end
    function onJobB
       
--do job B
        SetEventHandlerC(
"onJobC")
    end
    function onJobC
       
--do job C
    end
   
-- script starts here
    SetEventHandlerA(
"onJobA" )
    sleep(
10)


    代码看起来似乎有点难看了,最重要的是它不易编写,策划估计会抓狂的。我想,对于非专业程序员而言,程序的
顺序执行可能理解起来更为容易。

SOLVE IT

    我们的解决方案,其实只有一句话:当脚本执行到阻塞操作时(如NPCDialog),挂起脚本,当宿主程序某个操作完
成时,让脚本从之前的挂起点继续执行。
    这不是一种假想的功能。我在刚开始实现这个功能之前,以为LUA不支持这个功能。我臆想着如下的操作:
    脚本:
    ret = NPCDialog("Hello bitch")
    if ret == 0 then print("OK") end
    宿主程序:

    static int do_npc_dialog( lua_State *L )
   
{
       
        lua_suspend_script( L );
       
    }


    某个地方某个操作完成了:
    lua_resume_script( L );
    当我实现了这个功能后,我猛然发现,实际情况和我这里想的差不多(有点汗颜)。


认识Coroutine

    coroutine是LUA中类似线程的东西,但是它其实和fiber更相似。也就是说,它是一种非抢占式的线程,它的切换取决
于任务本身,也就是取决你,你决定它们什么时候发生切换。建议你阅读lua manual了解更多。
    coroutine支持的典型操作有:lua_yield, lua_resume,也就是我们需要的挂起和继续执行。
    lua_State似乎就是一个coroutine,或者按照LUA文档中的另一种说法,就是一个thread。我这里之所以用’似乎‘是
因为我自己也无法确定,我只能说,lua_State看起来就是一个coroutine。
    LUA提供lua_newthread用于手工创建一个coroutine,然后将新创建的coroutine放置于堆栈顶,如同其他new出来的
对象一样。网上有帖子说lua_newthread创建的东西与脚本里调用coroutine.create创建出来的东西不一样,但是根据我
的观察来看,他们是一样的。lua_newthread返回一个lua_State对象,所以从这里可以看出,“lua_State看起来就是一个
coroutine”。另外,网上也有人说创建新的coroutine代价很大,但是,一个lua_State的代价能有多大?当然,我没做过
测试,不敢多言。
    lua_yield用于挂起一个coroutine,不过该函数只能用于coroutine内部,看看它的参数就知道了。
    lua_resume用于启动一个coroutine,它可以用于coroutine没有运行时启动之,也可以用于coroutine挂起时重新启动
之。lua_resume在两种情况下返回:coroutine挂起或者执行完毕,否则lua_resume不返回。
    lua_yield和lua_resume对应于脚本函数:coroutine.yield和coroutine.resume,建议你写写脚本程序感受下coroutine,
例如:

    function main()
        print(
"main start")
        coroutine.yield()
        print(
"main end")
    end
    co
=coroutine.create( main );
    coroutine.resume(co)


REALLY SOLVE IT

    你可能会想到,我们为脚本定义一个main,然后在宿主程序里lua_newthread创建一个coroutine,然后将main放进去,
当脚本调用宿主程序的某个’阻塞‘操作时,宿主程序获取到之前创建的coroutine,然后yield之。当操作完成时,再resume
之。
    事实上方法是对的,但是没有必要再创建一个coroutine。如之前所说,一个lua_State看上去就是一个coroutine,
而恰好,我们始终都会有一个lua_State。感觉上,这个lua_State就像是main coroutine。(就像你的主线程)
    思路就是这样,因为具体实现时,还是有些问题,所以我罗列每个步骤的代码。
    初始lua_State时如你平时所做:

    lua_State *L = lua_open();
    luaopen_base( L );


    注册脚本需要的宿主程序函数到L里:

    lua_pushcfunction( L, sleep );
    lua_setglobal( L,
"my_sleep" );


    载入脚本文件并执行时稍微有点不同:

    luaL_loadfile( L, "test.lua" );
lua_resume( L,
0 ); /* 调用resume */


    在你的’阻塞‘函数里需要挂起coroutine:

    return lua_yield( L, 0 );


    注意,lua_yield函数非常特别,它必须作为return语句被调用,否则会调用失败,具体原因我也不清楚。而在这里,
它作为lua_CFunction的返回值,会不会引发错误?因为lua_CFunction约定返回值为该函数对于脚本而言的返回值个数。
实际情况是,我看到的一些例子里都这样安排lua_yield,所以i do what they do。

    在这个操作完成后(如玩家操作了那个对话框),宿主程序需要唤醒coroutine:

    lua_resume( L, 0 );

 

    大致步骤就这些。如果你要单独创建新的lua_State,反而会搞得很麻烦,我开始就是那样的做的,总是实现不了自己
预想中的效果。

相关下载:
    例子程序中,我给了一个sleep实现。脚本程序调用sleep时将被挂起,宿主程序不断检查当前时间,当时间到时,resume
挂起的coroutine。下载例子

 

8.13补充

   可能有时候,我们提供给脚本的函数需要返回一些值给脚本,例如NPCDialog返回操作结果,我们只需要在宿主程序里lua_resume

之前push返回值即可,当然,需要设置lua_resume第二个参数为返回值个数。

posted @ 2008-08-12 16:02 Kevin Lynx 阅读(987) | 评论 (5)编辑 收藏

2008年8月5日 #

为http server加入CGI--网页外观的应用程序

Implement CGI in your httpd

author : Kevin Lynx

Purpose

    为我们的http server加入CGI的功能,并不是要让其成为真正响应CGI query功能的web server。正如klhttpd的开发初衷一样,
更多的时候我们需要的是一个嵌入式(embeded)的web server。

    而加入CGI功能(也许它算不上真正的CGI),则是为了让我们与client拥有更多的交互能力。我们也许需要为我们的应用程序
加入一个具有网页外观的界面,也许这才是我的目的。

Brief

    CGI,common gateway interface,在我看来它就是一个公共约定。客户端浏览器提交表单(form)操作结果给服务器,服务器
解析这些操作,完成这些操作,然后创建返回信息(例如一个静态网页),返回给浏览器。
    因为浏览器遵循了CGI发送请求的标准,那么我们所要做的就是解析收到的请求,处理我们自己的逻辑,返回信息给客户端即可。
Detail...
    如果你要获取CGI的更多信息,你可以在网上查阅相关信息。如果你要获取web server对CGI标准的处理方式,我无法提供给你
这些信息。不过我愿意提供我的臆测:
    处理CGI请求通常都是由脚本去完成(当然也可以用任何可执行程序完成)。就我所获取的资料来看,只要一门语言具备基本的
输入输出功能,它就可以被用作CGI脚本。浏览器以key-value的形式提交query string,一个典型的以GET方式提交query string的
URL类似于:
http://host/cgi-bin/some-script.cgi?name=kevin+lynx&age=22。不必拘泥于我上句话中出现的一些让你产生问号的
名词。我可以告诉你,这里举出的URL(也许更准确的说法是URI)中问号后面的字符串即为一个query string。
    我希望你看过我的上一篇草文<实现自己的http server>,你应该知道一个典型的GET请求时怎样的格式,应该知道什么是initial
line,应该知道HTTP请求有很多method,例如GET、POST、HEAD等。
    一个CGI请求通常使用GET或POST作为其request method。对于一个GET类型的CGI请求,一个典型的request类似于:
    GET /cgi-bin/some-script.cgi?name=kevin+lynx&age=22 HTTP/1.1
    other headers...
    ...
    我上面举例的那个URL则可能出现在你的浏览器的地址栏中,它对我们不重要。
    而对于POST类型的CGI请求呢?query string只是被放到了optional body里,如:
    POST /cgi-bin/some-script.cgi HTTP/1.1
    other heads...
    ...
    name=kevin+lynx&age=22
    不管我说到什么,我希望你不要陷入概念的泥潭,我希望你能明确我在brief里提到的what can we do and how to do。

How about the web browser ?

    我总是想引领你走在实践的路上(on the practice way)。作为一个程序员,你有理由自负,但是你没任何理由小觑任何一个
编程问题。打开你的编译器,现在就敲下main。
    so,我们需要知道如何利用我们的浏览器,让我们更为顺手地实践我们的想法。
    你也许会写:<html><head><title>HTML test</title></head><body>it's a html</body></html>这样的HTML标记语言。但是
这没有利用上CGI。check out the url in the reference section and learn how to write FORMs.
    这个时候你应该明白:

   <html>
      
<head>
        
<title>CGI test</title>
      
</head>
      
<body>
        
<FORM ACTION="./cgi-bin/test.cgi">
           
<INPUT TYPE="text" NAME="name" SIZE=20 VALUE="Your name">
         
</FORM>
      
</body>
    
</html>

    
    加入<FORM>来添加表单,加入<INPUT TYPE="text"加一个edit box控件。那么当你在里面输入名字按下回车键时,浏览器将整理
出一个CGI query string发给服务器。
    我希望你能在浏览器里看到以问号打头的key-value形式的字符串。

So here we go...

    在你自己已有的http server的代码基础上,让浏览器来请求一个包含FORM的网页,例如以上我举例的html。当浏览器将请求发
给服务器的时候,看看服务器这边收到的request。即使你猜到了什么,我也建议你看看,这样你将看到一个真正的CGI query是怎么
样的。
    so you know,CGI请求本质上也就是一连串的key-value字符串。真正的http server会将这些字符串传给CGI脚本。脚本只需要
输出处理结果信息到标准输出,服务器就会收集这些输出然后发给客户端。
    你也许在揣摩,我们的httpd如何地exec一个外部程序或脚本,如何等待脚本执行完(需不需要异步?),然后如何收集标准输
出上的信息,然后是否需要同步地将这些信息送往客户端。
    这很没必要。如果我们的http server真的只是被embeded到其他程序的。我们只需要将query的逻辑处理交给上层模块即可。

What's the next..

    现在你知道什么是CGI query string,你也许稍微思考下就知道接下来你该如何去做(if you smart enough)。记住我们的目标,
只是解析CGI query string,然后处理相关逻辑,返回处理结果信息给客户端。
    所以接下来,呃,接下来,everything is up to you:分析那些恼人的字符串,将其从&的间隔中整理出来,得到一个key-value
的表,将这个表给你的上层模块,上层模块根据query scritp决定执行什么逻辑,根据这个表获取需要的参数,处理,将结果格式
化为HTML之类的东西,then response。
    就这么简单。

Example...

    同样,我提供了源码,从而证明我不是纸上谈兵夸夸其谈。我希望我公开源码的习惯能受到你的赞同。

   下载带CGI的http server.

Reference:

http://hoohoo.ncsa.uiuc.edu/cgi/ (获取服务器需要做的)
http://www.ietf.org/rfc/rfc3875 (rfc cgi)
http://www.cmis.brighton.ac.uk/~mas/mas/courses/html/html2.html (学会写带FORM的HTML)

posted @ 2008-08-05 22:01 Kevin Lynx 阅读(1072) | 评论 (2)编辑 收藏

2008年7月30日 #

实现自己的http server

Write your own http server

author : Kevin Lynx

Why write your own?

    看这个问题的人证明你知道什么是http server,世界上有很多各种规模的http server,为什么要自己实现一个?其实没什么
理由。我自己问自己,感觉就是在自己娱乐自己,或者说只是练习下网络编程,或者是因为某日我看到某个库宣称自己附带一个小
型的http server时,我不知道是什么东西,于是就想自己去实现一个。

What's httpd ?

    httpd就是http daemon,这个是类unix系统上的名称,也就是http server。httpd遵循HTTP协议,响应HTTP客户端的request,
然后返回response。
    那么,什么是HTTP协议?最简单的例子,就是你的浏览器与网页服务器之间使用的应用层协议。虽然官方文档说HTTP协议可以
建立在任何可靠传输的协议之上,但是就我们所见到的,HTTP还是建立在TCP之上的。
    httpd最简单的response是返回静态的HTML页面。在这里我们的目标也只是一个响应静态网页的httpd而已(也许你愿意加入CGI
特性)。

More details about HTTP protocol

    在这里有必要讲解HTTP协议的更多细节,因为我们的httpd就是要去解析这个协议。
    关于HTTP协议的详细文档,可以参看rfc2616。但事实上对于实现一个简单的响应静态网页的httpd来说,完全没必要读这么一
分冗长的文档。在这里我推荐<HTTP Made Really Easy>,以下内容基本取自于本文档。

- HTTP协议结构
  HTTP协议无论是请求报文(request message)还是回应报文(response message)都分为四部分:
  * 报文头 (initial line )
  * 0个或多个header line
  * 空行(作为header lines的结束)
  * 可选body
  HTTP协议是基于行的协议,每一行以\r\n作为分隔符。报文头通常表明报文的类型(例如请求类型),报文头只占一行;header line
  附带一些特殊信息,每一个header line占一行,其格式为name:value,即以分号作为分隔;空行也就是一个\r\n;可选body通常
  包含数据,例如服务器返回的某个静态HTML文件的内容。举个例子,以下是一个很常见的请求报文,你可以截获浏览器发送的数据
  包而获得:

    1  GET /index.html HTTP/1.1
    2  Accept-Language: zh-cn
    3  Accept-Encoding: gzip, deflate
    4  User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; MAXTHON 2.0)
    5  Host: localhost
    6  Connection: Keep-Alive
    7
  我为每一行都添加了行号,第1行就是initial line,2-6行是header lines,7行是一个header line的结束符,没有显示出来。
  以下是一个回应报文:
    1  HTTP/1.1 200 OK
    2  Server: klhttpd/0.1.0
    3  Content-Type: text/html
    4  Content-Length: 67
    5
    6  <head><head><title>index.html</title></head><body>index.html</body>
  第6行就是可选的body,这里是index.html这个文件的内容。

- HTTP request method
  因为我们做的事服务器端,所以我们重点对请求报文做说明。首先看initial line,该行包含几个字段,每个字段用空格分开,例
  如以上的GET /index.html HTTP/1.1就可以分为三部分:GET、/index.html、HTTP/1.1。其中第一个字段GET就是所谓的request
  method。它表明请求类型,HTTP有很多method,例如:GET、POST、HEAD等。

  就我们的目标而言,我们只需要实现对GET和HEAD做响应即可。

  GET是最普遍的method,表示请求一个资源。什么是资源?诸如HTML网页、图片、声音文件等都是资源。顺便提一句,HTTP协议
  中为每一个资源设置一个唯一的标识符,就是所谓的URI(更宽泛的URL)。
  HEAD与GET一样,不过它不请求资源内容,而是请求资源信息,例如文件长度等信息。

- More detail
  继续说说initial line后面的内容:
  对应于GET和HEAD两个method,紧接着的字段就是资源名,其实从这里可以看出,也就是文件名(相对于你服务器的资源目录),例
  如这里的/index.html;最后一个字段表明HTTP协议版本号。目前我们只需要支持HTTP1.1和1.0,没有多大的技术差别。

  然后是header line。我们并不需要关注每一个header line。我只罗列有用的header line :
  - Host : 对于HTTP1.1而言,请求报文中必须包含此header,如果没有包含,服务器需要返回bad request错误信息。
  - Date : 用于回应报文,用于客户端缓存数据用。
  - Content-Type : 用于回应报文,表示回应资源的文件类型,以MIME形式给出。什么是MIME?它们都有自己的格式,例如:
    text/html, image/jpg, image/gif等。
  - Content-Length : 用于回应报文,表示回应资源的文件长度。

body域很简单,你只需要将一个文件全部读入内存,然后附加到回应报文段后发送即可,即使是二进制数据。

- 回应报文
  之前提到的一个回应报文例子很典型,我们以其为例讲解。首先是initial line,第一个字段表明HTTP协议版本,可以直接以请求
  报文为准(即请求报文版本是多少这里就是多少);第二个字段是一个status code,也就是回应状态,相当于请求结果,请求结果
  被HTTP官方事先定义,例如200表示成功、404表示资源不存在等;最后一个字段为status code的可读字符串,你随便给吧。

  回应报文中最好跟上Content-Type、Content-Length等header。

具体实现
    正式写代码之前我希望你能明白HTTP协议的这种请求/回应模式,即客户端发出一个请求,然后服务器端回应该请求。然后继续
这个过程(HTTP1.1是长连接模式,而HTTP1.0是短连接,当服务器端返回第一个请求时,连接就断开了)。
    这里,我们无论客户端,例如浏览器,发出什么样的请求,请求什么资源,我们都回应相同的数据:

               

/* 阻塞地接受一个客户端连接 */
        SOCKET con 
= accept( s, 00 ); 
        
/* recv request */
        
char request[1024= 0 };
        ret 
= recv( con, request, sizeof( request ), 0 );
        printf( request );
        
/* whatever we recv, we send 200 response */
        
{
            
char content[] = "<head><head><title>index.html</title></head><body>index.html</body>";
            
char response[512];
            sprintf( response, 
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: %d\r\n\r\n%s", strlen( content ), content );
            ret 
= send( con, response, strlen( response ), 0 );
        }

        closesocket( con ); 

 

    程序以最简单的阻塞模式运行,我们可以将重点放在协议的分析上。运行程序,在浏览器里输入http://localhost:8080/index.html
,然后就可以看到浏览器正常显示content中描述的HTML文件。假设程序在8080端口监听。

   现在你基本上明白了整个工作过程,我们可以把代码写得更全面一点,例如根据GET的URI来载入对应的文件然后回应给客户端。
其实这个很简单,只需要从initial line里解析出(很一般的字符串解析)URI字段,然后载入对应的文件即可。例如以下函数:

void http_response( SOCKET con, const char *request )
{
    
/* get the method */
    
char *token = strtok( request, " " );
    
char *uri = strtok( 0" " );
    
char file[64];
    sprintf( file, 
".%s", uri ); 

    
{
        
/* load the file content */
        FILE 
*fp = fopen( file, "rb" );
        
if( fp == 0 )
        
{
            
/* response 404 status code */
            
char response[] = "HTTP/1.1 404 NOT FOUND\r\n\r\n";
     &nbs