Wednesday, August 24, 2011

实现自己的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协议是基于行的协议,每一行以作为分隔符。报文头通常表明报文的类型(例如请求类型),报文头只占一行;header line
  附带一些特殊信息,每一个header line占一行,其格式为name:value,即以分号作为分隔;空行也就是一个;可选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是短连接,当服务器端返回第一个请求时,连接就断开了)。
    这里,我们无论客户端,例如浏览器,发出什么样的请求,请求什么资源,我们都回应相同的数据:

C++代码
  1. /**//* 阻塞地接受一个客户端连接 */  
  2.         SOCKET con = accept( s, 0, 0 );    
  3.         /**//* recv request */  
  4.         char request[1024] = { 0 };   
  5.         ret = recv( con, request, sizeof( request ), 0 );   
  6.         printf( request );   
  7.         /**//* whatever we recv, we send 200 response */  
  8.         {   
  9.             char content[] = "<head><head><title>index.html</title></head><body>index.html</body>";   
  10.             char response[512];   
  11.             sprintf( response, "HTTP/1.1 200 OKContent-Type: text/htmlContent-Length: %d%s", strlen( content ), content );   
  12.             ret = send( con, response, strlen( response ), 0 );   
  13.         }   
  14.         closesocket( con );   

程序以最简单的阻塞模式运行,我们可以将重点放在协议的分析上。运行程序,在浏览器里输入http://localhost:8080/index.html
,然后就可以看到浏览器正常显示content中描述的HTML文件。假设程序在8080端口监听。
   现在你基本上明白了整个工作过程,我们可以把代码写得更全面一点,例如根据GET的URI来载入对应的文件然后回应给客户端。
其实这个很简单,只需要从initial line里解析出(很一般的字符串解析)URI字段,然后载入对应的文件即可。例如以下函数:

C++代码
  1. void http_response( SOCKET con, const char *request )   
  2. {   
  3.     /**//* get the method */  
  4.     char *token = strtok( request, " " );   
  5.     char *uri = strtok( 0, " " );   
  6.     char file[64];   
  7.     sprintf( file, ".%s", uri );    
  8.   
  9.     {   
  10.         /**//* load the file content */  
  11.         FILE *fp = fopen( file, "rb" );   
  12.         if( fp == 0 )   
  13.         {   
  14.             /**//* response 404 status code */  
  15.             char response[] = "HTTP/1.1 404 NOT FOUND";   
  16.             send( con, response, strlen( response ), 0 );   
  17.         }   
  18.         else  
  19.         {   
  20.             /**//* response the resource */  
  21.             /**//* first, load the file */  
  22.             int file_size ;   
  23.             char *content;   
  24.             char response[1024];   
  25.             fseek( fp, 0, SEEK_END );   
  26.             file_size = ftell( fp );   
  27.             fseek( fp, 0, SEEK_SET );   
  28.             content = (char*)malloc( file_size + 1 );   
  29.             fread( content, file_size, 1, fp );   
  30.             content[file_size] = 0;    
  31.   
  32.             sprintf( response, "HTTP/1.1 200 OKContent-Type: text/htmlContent-Length: %d%s", file_size, content );   
  33.             send( con, response, strlen( response ), 0 );   
  34.             free( content );   
  35.         }   
  36.     }   
  37. }    
  38.   

其他
    要将这个简易的httpd做完善,我们还需要注意很多细节。包括:对不支持的method返回501错误;对于HTTP1.1要求有Host这个
header;为了支持客户端cache,需要添加Date header;支持HEAD请求等。
    相关下载中我提供了一个完整的httpd library,纯C的代码,在其上加上一层资源载入即可实现一个简单的httpd。在这里我将
对代码做简要的说明:
    evbuffer.h/buffer.c : 取自libevent的buffer,用于缓存数据;
    klhttp-internal.h/klhttp-internal.c :主要用于处理/解析HTTP请求,以及创建回应报文;
    klhttp-netbase.h/klhttp-netbase.c :对socket api的一个简要封装,使用select模型;
    klhttp.h/klhttp.c :库的最上层,应用层主要与该层交互,这一层主要集合internal和netbase。
    test_klhttp.c :一个测试例子。
相关下载:
    klhttpd
    文中相关代码
参考资料:

Monday, August 1, 2011

实现分享到各大SNS的代码

为了寻求简便,在和炫仔定制UChome的时候想把内容分享到各大的SNS和微博上,所以找了一圈,最后使用分享条来做,http://www.sharebar.cn/ ,具体我用了图标式分享。
当然,修改一下UChome的代码,在ROOT/template/default/space_blog_list.htm 中的64行左右和ROOT/template/default/space_blog_view.htm 的第20行左右,分别加入代码,代码内容为


<div>
<!-- JiaThis Button BEGIN -->
<div id="jiathis_style_32x32">
<a class="jiathis_button_tsina"></a>
<a class="jiathis_button_tqq"></a>
<a class="jiathis_button_renren"></a>
<a class="jiathis_button_tsohu"></a>
<a class="jiathis_button_douban"></a>
<a class="jiathis_button_qzone"></a>
<a class="jiathis_button_googlereader"></a>
<a class="jiathis_button_buzz"></a>
<a class="jiathis_button_feixin"></a>
<a class="jiathis_button_douban"></a>
<a class="jiathis_button_twitter"></a>
<a class="jiathis_button_fb"></a>
<a class="jiathis_button_follow5"></a>
<a class="jiathis_button_kaixin001"></a>
<a href="http://www.jiathis.com/share" class="jiathis jiathis_txt jtico jtico_jiathis" target="_blank"></a>
</div>
<script type="text/javascript" src="http://v2.jiathis.com/code/jia.js" charset="utf-8"></script>
<!-- JiaThis Button END -->
</div>

然后就可以实现分享的功能了。

以上是一种方法,当然还有一种更加简单的,就是在ROOT/template/default/footer.htm 中的第14行加入以下代码:

<script type="text/javascript" src="http://v2.jiathis.com/code/jiathis_r.js?btn=r3.gif" charset="utf-8"></script>
这是用来实现侧边栏的分享。以上两种均可以

PHP 下 CURL 通过 POST 提交表单失败的原因之一与解决办法

前几天在学习使用 CURL 时遇到一个问题:在 a.php 中以 POST 方式向 b.php 提交数据,但是 b.php 下就是无法接收到数据,而 CURL 操作又显示成功,非常诡异。后经多方查找,在某个网友的博客中得到了启发。原来,“传递一个数组到CURLOPT_POSTFIELDS,cURL会把数据编码成 multipart/form-data,而然传递一个URL-encoded字符串时,数据会被编码成 application/x-www-form-urlencoded。
”,而和我一样对 CURL 不太熟悉的人在编写程序时,代码往往是下面的样子:
$data = array( 'Title' => $title, 'Content' => $content, 'ComeFrom' => $comefrom ); 
curl_setopt($ch, CURLOPT_DNS_USE_GLOBAL_CACHE, false); 
curl_setopt($ch, CURLOPT_URL, 'http://example.com/b.php'); 
curl_setopt($ch, CURLOPT_POST, 1); 
curl_setopt($ch, CURLOPT_POSTFIELDS, $data); 
 curl_exec($ch);
也就是将所要提交的数据以数组的形式通过 POST 发送,而这样就会导致 CURL 使用“错误”的编码“multipart/form-data”,其效果相当于我们直接以“<form method="post" action="b.php" enctype="multipart/form-data">”这样的表单来完成操作,大家可以试试,这时的“b.php”是无论如何也无法通过 $_POST 来接收数据的。
所以,正确的做法应该是将上述范例代码中的 $data 由数组变为经 urlencode() 编码后的字符串连接,类似:
$data = 'Title=' .urlencode($title) . '&Content=' .urlencode($content) . '&ComeFrom=' .urlencode($comefrom);
这样就可以了。

注意:两种发送编码格式的区别,"multipart/form-data" 是把表单设置为MIME编码,目的是用来传输二进制文件的,若想上传文件,必须用这个编码。但是普通的url数据request使用的是“application/x-www-form-urlencoded” 格式, 所以如果传普通数据用2进制格式,对方就无法得不到值了。所以特别注意应该将发送请求的编码确定。

顺便再记录一下echo和print_r()的区别,一般总是用echo来输出一个变量,比如
echo $data;
echo  " i am a fool!!!";
echo主要用来输出字符串,或者变量,不能用于直接输出数组

print_r()是一个函数,专门用来直接输出数组的,比如
print_r($array);     他会按照一定的格式,把数组的标号和内容打印出来。