Simple Http Server

Simple Http Server(SHS)是Linux下一个超轻量级的Http Server,可以接受用户的静态或动态请求,对于大致了解服务器的工作流程、熟悉HTTP协议及UNIX网络编程有一定帮助。


1. 项目简介

一个简单的开源项目,对于熟悉HTTP协议,服务器的工作流程,特别是UNIX网络编程,有较大的帮助。同时还继续用到了稍复杂的双向管道等进程间通信的知识。

项目结构

  • httpd.c: 服务器端主程序,用于建立一个http Server。
  • simpleclient.c: 服务器简易测试程序,一个客户端,用于测试服务器的连通。
  • makefile: 描述了整个工程的编译、连接等规则。
  • README.md: 描述了项目的安装及使用方法。
  • httpdocs: 服务器本地文件夹,包含一些网页和CGI脚本。

2. 核心文件: httpd.c

核心函数

  • main: 主要用于创建并监听服务器连接,当有连接到来时,创建线程对其套结字进行处理。
  • startup: 主要用于初始化httpd服务,包括socket、bind、listen等。
  • accept_request: 主要用于处理从监听套结字上获得的一个HTTP请求。
  • get_line: 主要用于读取套结字的一行,把/r/n或/n等结尾都统一为换行结束符/n。
  • server_file: 主要用于处理静态请求,调用cat函数把服务器文件返回给浏览器。
  • execute_cgi: 主要用于处理动态请求,运行CGI处理程序。
  • headers: 把HTTP响应的头部写到套结字中。
  • cat: 读取服务器上某个文件并写到套结字中。

3. 细节实现

3.1 get_line函数

在Windows和Linux中有不同的结束符,有时候会带来很麻烦的问题,通过使用自定义的getline函数,无论结尾是什么结束符,都将其转换成‘\n’。

常见问题

Windows中的结束符为“\r\n”,‘\r‘(回车符)代表每次光标移动到本行的行首位置处,‘\n’(换行符)代表每次光标移动到下一行的行首位置处;在Linux中的结束符为‘\n’;在Mac中的结束符为‘\r‘。在Linux中遇到‘\n’会进行回车+换行的操作,回车符反而只会作为控制字符(“^M”)显示,不会发生回车操作;而Windows中要“\r\n”才会回车+换行,缺少一个控制符或者顺序不对都不能正确的另起一行。

3.2 unimplement函数

httpd.c中还有不少类似的函数,如not_found、headers、cannot_execute、bad_request等,它们都是根据HTTP的请求,模拟的HTTP响应。HTTP请求和响应的具体报文格式可以参照我的博文HTTP协议

3.3 stat函数

  • 头文件: #include <stat.h> #include <unistd.h>
  • 函数声明: int stat(const char *file_name,struct stat *buf)
  • 函数功能: 通过文件名filename获取文件信息,并保存在buf所指的结构体stat中

参数结构stat定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct stat{
dev_t st_dev;
ino_t st_ino;
mode_t st_mode;
nlink_t st_nlink;
uid_t st_uid;
git_t st_gid;
dev_t st_rdev;
off_t st_size;
unsigned long st_blksize;
unsigned long st_blocks;
time_t st_atime;
time_t st_mtime;
time_t st_ctime;
}
错误代码 说明
ENOENT 参数file_name指定的文件不存在
ENOTDIR 路径中的目录存在但却非真正的目录
ELOOP 欲打开的文件有过多符号连接问题,上限为16符号连接
EFAULT 参数buf为无效指针,指向无法存在的内存空间
EACCESS 存取文件时被拒绝
ENOMEM 核心内存不足
ENAMETOOLONG 参数file_name的路径名称太长
st_mode 说明
S_IFMT 0170000 文件类型选择
S_IFSOCK 0140000 socket
S_IFLNK 0120000 符号连接
S_IFREG 0100000 一般文件
S_IFBLK 0060000 区块装置
S_IFDIR 0040000 目录
S_IFCHR 0020000 字符装置
S_IFIFO 0010000 先进先出
S_ISUID 04000 文件的SUID
S_ISGID 02000 文件的SGID
S_ISVTX 01000 文件的sticky位
S_IRUSR(S_IREAD) 00400 文件所有者具可读取权限
S_IWUSR(S_IWRITE) 00200 文件所有者具可写入权限
S_IXUSR(S_IEXEC) 00100 文件所有者具可执行权限
S_IRGRP 00040 用户组具可读取权限
S_IWGRP 00020 用户组具可写入权限
S_IXGRP 00010 用户组具可执行权限
S_IROTH 00004 其他用户具可读取权限
S_IWOTH 00002 其他用户具可写入权限
S_IXOTH 00001 其他用户具可执行权限

通过该函数对文件权限进行判断,判断是否具有可执行权限,如果有,就调用函数execute_cgi执行动态脚本,否则,调用函数server_file返回静态页面。

注意事项

相关的cgi文件要增加可执行权限,否则不会被当作可执行文件。

3.4 execute_cgi函数

当遇到服务器端的POST请求以及GET请求带有?参数时,用此函数来执行动态脚本。以显示颜色为例。打开网页,连接服务器后,输入颜色,点击“Submit”后,传递HTTP的POST请求到服务器,服务器解析后得到method、path。将这些参数传递给该函数,创建子进程,建立管道。从客户浏览器传入的POST数据,由父进程写入管道cgi_input[1];子进程从STDIN接受数据,即管道cgi_input[0],负责设置环境变量,并执行execl函数,结果将被传入STDOUT,即管道cgi_output[1],父进程从cgi_output[0]中读取相关信息返回给客户浏览器。这里需要使用一个双向数据流管道,如下图所示。

管道示意图

扩展介绍

CGI程序的特点是通过标准输入(stdin)和环境变量(可以理解成有两个传递数据的途径,二者相辅相成,其实跟请求方法是get或post也相关)来得到服务器的信息,并通过标准输出(stdout)向服务器输出信息。


4. 调试

有了前一个项目的GDB调试经验,这个调试起来较为方便,详细的GDB操作可以查看上一个项目的博文。这里是先开启服务器再进行调试。

可以使用如下命令找到httpd的进程号并开启GDB调试:

1
2
ps aux | grep httpd
gdb attach xxx

调试问题

在创建线程时,有时会自动的创建两个线程,没有找到原因,继续运行时,最后都以SIGPIPE信号终止。信号的原因:连接建立,若某一端关闭连接,而另一端仍然向它写数据,第一次写数据后会收到RST响应,此后再写数据,内核将向进程发出SIGPIPE信号,通知进程此连接已经断开。而SIGPIPE信号的默认处理是终止程序,导致上述问题的发生。

测试结果

  • 显示颜色

blue1

blue2

  • 显示服务器时间

time1

time2


5. 完善与维护

完成后,将项目上传至Github维护。

项目地址:Simple Http Server


6. 总结

这是自己学习的第二个项目,以之前的HTTP协议的知识为基础,实践了一下它在Http Server中的应用,了解了服务器的工作流程,但多线程及IPC相关的知识还有待加强。

下面一阶段要不断的看书充电,把没有解决的问题继续解决,以这两个项目为实战参考。See you in next program!

HTTP协议

HTTP一直在身边,可是从来没有系统的去学习过这方面的知识,后面要做Http Server,先进行必要的知识补充。


1. 引言

HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展。目前在WWW中使用的是HTTP/1.1。

HTTP协议的主要特点可概括如下:

  • 支持客户/服务器模式。
  • 简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
  • 灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。
  • 无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
  • 无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

2. URL

URL(Uniform Resource Locator) 地址用于描述一个网络上的资源。

  • 格式:schema://host[:port#]/path/…/[;url-params][?query-string][#anchor]
参数 说明
scheme 指定低层使用的协议(例如:http, https, ftp)
host HTTP服务器的IP地址或者域名
port# HTTP服务器的默认端口是80
path 访问资源的路径
url-params url参数
query-string 发送给http服务器的数据
anchor-

示例:

1
2
3
4
5
6
Schema: http
host: www.mywebsite.com
path: /sj/test
URL params: id=8079
Query String: name=sviergn&x=true
Anchor: stuff

2.1 HTTP URL

http表示要通过HTTP协议来定位网络资源;host表示合法的Internet主机域名或者IP地址;port指定一个端口号,为空则使用缺省端口80;abs_path指定请求资源的URI;如果URL中没有给出abs_path,那么当它作为请求URI时,必须以“/”的形式给出,通常这个工作浏览器自动帮我们完成。

示例

扩展介绍

URI,是uniform resource identifier,统一资源标识符,用来唯一的标识一个资源。而URL是uniform resource locator,统一资源定位器,它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。而URN,uniform resource name,统一资源命名,是通过名字来标识资源,比如mailto:java-net@java.sun.com。也就是说,URI是以一种抽象的,高层次概念定义统一资源标识,而URL和URN则是具体的资源标识的方式。URL和URN都是一种URI。


3. 消息格式

HTTP消息由客户端到服务器的请求和服务器到客户端的响应组成。
请求消息和响应消息都是由开始行(对于请求消息,开始行就是请求行;对于响应消息,开始行就是状态行),消息报头(可选),空行(只有CRLF的行),消息正文(可选)组成。

请求报文格式

响应报文格式


4. 开始行

4.1 请求消息——请求行

  • 格式:Request-method Request-URI HTTP-Version CRLF

其中,Request-method表示请求方法;Request-URI是一个统一资源标识符;HTTP-Version表示请求的HTTP协议版本;CRLF表示回车和换行(除了作为结尾的CRLF外,不允许出现单独的CR或LF字符)。

请求方法 说明
GET 请求获取Request-URI所标识的资源
POST 在Request-URI所标识的资源后附加新的数据
HEAD 请求获取由Request-URI所标识的资源的响应消息报头
PUT 请求服务器存储一个资源,并用Request-URI作为其标识
DELETE 请求服务器删除Request-URI所标识的资源
TRACE 请求服务器回送收到的请求信息,主要用于测试或诊断
CONNECT 保留将来使用
OPTIONS 请求查询服务器的性能,或者查询与资源相关的选项和需求

示例:

  • GET方法:在浏览器的地址栏中输入网址的方式访问网页时,浏览器采用GET方法向服务器获取资源,例:GET /form.html HTTP/1.1。

  • POST方法:要求被请求服务器接受附在请求后面的数据,常用于提交表单。

1
2
3
4
5
6
7
8
9
POST /reg.jsp HTTP/ (CRLF)
Accept:image/gif,image/x-xbit,... (CRLF)
...
HOST:www.guet.edu.cn (CRLF)
Content-Length:22 (CRLF)
Connection:Keep-Alive (CRLF)
Cache-Control:no-cache (CRLF)
(CRLF) //该CRLF表示消息报头已经结束,在此之前为消息报头
user=jeffrey&pwd=1234 //此行以下为提交的数据,即消息正文

4.2 响应消息——状态行

  • 格式:HTTP-Version Status-Code Reason-Phrase CRLF

其中,HTTP-Version表示服务器HTTP协议的版本;Status-Code表示服务器发回的响应状态代码;Reason-Phrase表示状态代码的文本描述。

响应状态码 说明
1xx 指示信息:表示请求已接收,继续处理
2xx 成功:表示请求已被成功接收、理解、接受
3xx 重定向:要完成请求必须进行更进一步的操作
4xx 客户端错误:请求有语法错误或请求无法实现
5xx 服务器端错误:服务器未能实现合法的请求
常见响应状态码 状态描述 说明
200 OK 客户端请求成功
400 Bad Request 客户端请求有语法错误,不能被服务器所理解
401 Unauthorized 请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
403 Forbidden 服务器收到请求,但是拒绝提供服务
404 Not Found 请求资源不存在
500 Internal Server Error 服务器发生不可预期的错误
503 Server Unavailable 服务器当前不能处理客户端的请求,一段时间后可能恢复正常

示例:

  • HTTP/1.1 200 OK (CRLF)

5. 消息报头

HTTP消息报头包括普通报头、请求报头、响应报头、实体报头。
每一个报头域都是由名字+“:”+空格+值组成,消息报头域的名字是大小写无关的。

5.1 普通报头

在普通报头中,有少数报头域用于所有的请求和响应消息,但并不用于被传输的实体,只用于传输的消息。

示例:

  • Cache-Control:用于指定缓存指令,缓存指令是单向的(响应中出现的缓存指令在请求中未必会出现),且是独立的(一个消息的缓存指令不会影响另一个消息处理的缓存机制),HTTP1.0使用的类似的报头域为Pragma。
    请求时的缓存指令包括:no-cache(用于指示请求或响应消息不能缓存)、no-store、max-age、max-stale、min-fresh、only-if-cached;响应时的缓存指令包括:public、private、no-cache、no-store、no-transform、must-revalidate、proxy-revalidate、max-age、s-maxage。为了指示IE浏览器(客户端)不要缓存页面,服务器端的JSP程序可以编写如下:response.setHeader(“Cache-Control”,”no-cache”)。这句代码将在发送的响应消息中设置普通报头域:Cache-Control:no-cache。

  • Date:表示消息产生的日期和时间。

  • Connection:允许发送指定连接的选项。例如指定连接是连续,或者指定“close”选项,通知服务器,在响应完成后,关闭连接。

5.2 请求报头

请求报头允许客户端向服务器端传递请求的附加信息以及客户端自身的信息。

示例:

  • Accept:用于指定客户端接受哪些类型的信息。Accept:image/gif,表明客户端希望接受GIF图象格式的资源;Accept:text/html,表明客户端希望接受html文本。
  • Accept-Charset:用于指定客户端接受的字符集。Accept-Charset:iso-8859-1,gb2312.如果在请求消息中没有设置这个域,缺省是任何字符集都可以接受。
  • Accept-Encoding:类似于Accept,但是它是用于指定可接受的内容编码。Accept-Encoding:gzip.deflate.如果请求消息中没有设置这个域服务器假定客户端对各种内容编码都可以接受。
  • Accept-Language:类似于Accept,但是它是用于指定一种自然语言。eg:Accept-Language:zh-cn.如果请求消息中没有设置这个报头域,服务器假定客户端对各种语言都可以接受。
  • Authorization:主要用于证明客户端有权查看某个资源。当浏览器访问一个页面时,如果收到服务器的响应代码为401(未授权),可以发送一个包含Authorization请求报头域的请求,要求服务器对其进行验证。
  • Host(发送请求时,该报头域是必需的):主要用于指定被请求资源的Internet主机和端口号,它通常从HTTP URL中提取出来的,在浏览器中输入:http://www.seu.edu.cn/index.html, 浏览器发送的请求消息中,就会包含Host请求报头域,例:Host:www.seu.edu.cn,此处使用缺省端口号80,若指定了端口号,则变成:Host:www.seu.edu.cn:指定端口号。
  • User-Agent:User-Agent请求报头域允许客户端将它的操作系统、浏览器和其它属性告诉服务器。
1
2
3
4
5
6
7
8
9
10
GET /form.html HTTP/1.1 (CRLF)
Accept:image/gif,image/x-xbitmap,image/jpeg,application/x-shockwave-flash,application/vnd.ms-excel,application/vnd.ms-powerpoint,application/msword,*/* (CRLF)
Accept-Language:zh-cn (CRLF)
Accept-Encoding:gzip,deflate (CRLF)
If-Modified-Since:Wed,05 Jan 2016 11:21:25 GMT (CRLF)
If-None-Match:W/"80b1a4c018f3c41:8317" (CRLF)
User-Agent:Mozilla/8.0(compatible;MSIE10.0;Windows 7) (CRLF)
Host:www.seu.edu.cn (CRLF)
Connection:Keep-Alive (CRLF)
(CRLF)

5.3 响应报头

响应报头允许服务器传递不能放在状态行中的附加响应信息,以及关于服务器的信息和对Request-URI所标识的资源进行下一步访问的信息。

示例:

  • Location:用于重定向接受者到一个新的位置。Location响应报头域常用在更换域名的时候。
  • Server:包含了服务器用来处理请求的软件信息。与User-Agent请求报头域是相对应的,Server:Apache-Coyote/1.1。
  • WWW-Authenticate:必须被包含在401(未授权的)响应消息中,客户端收到401响应消息时候,并发送Authorization报头域请求服务器对其进行验证时,服务端响应报头就包含该报头域,WWW-Authenticate:Basic realm=”Basic Auth Test!”,可以看出服务器对请求资源采用的是基本验证机制。

5.4 实体报头

请求和响应消息都可以传送一个实体。一个实体由实体报头域和实体正文组成,但并不是说实体报头域和实体正文要在一起发送,可以只发送实体报头域。实体报头定义了关于实体正文(有无实体正文)和请求所标识的资源的元信息。

示例:

  • Content-Encoding:被用作媒体类型的修饰符,它的值指示了已经被应用到实体正文的附加内容的编码,因而要获得Content-Type报头域中所引用的媒体类型,必须采用相应的解码机制。Content-Encoding这样用于记录文档的压缩方法,Content-Encoding:gzip。
  • Content-Language:描述了资源所用的自然语言。没有设置该域则认为实体内容将提供给所有的语言阅读者,Content-Language:da。
  • Content-Length:用于指明实体正文的长度,以字节方式存储的十进制数字来表示。
  • Content-Type:用语指明发送给接收者的实体正文的媒体类型,Content-Type:text/html; charset=ISO-8859-1。
  • Last-Modified:用于指示资源的最后修改日期和时间。
  • Expires:给出响应过期的日期和时间。为了让代理服务器或浏览器在一段时间以后更新缓存中(再次访问曾访问过的页面时,直接从缓存中加载,缩短响应时间和降低服务器负载)的页面,可以使用Expires实体报头域指定页面过期的时间,Expires:Thu,15 Sep 2016 16:23:12 GMT。HTTP1.1的客户端和缓存必须将其他非法的日期格式(包括0)看作已经过期。为了让浏览器不要缓存页面,也可以利用Expires实体报头域,设置为0,jsp中程序如下:response.setDateHeader(“Expires”,”0”)。

6. 相关补充

HTTP1.0和HTTP1.1的区别?

6.1 可扩展性

可扩展性的一个重要原则:如果HTTP的某个实现接收到了自身未定义的头域,将自动忽略它。

  • 在消息中增加版本号,用于兼容性判断。注意,版本号只能用来判断逐段(hop-by-hop)的兼容性,而无法判断端到端(end-to-end)的兼容性。例如,一台HTTP/1.1的源服务器从使用HTTP/1.1的Proxy那儿接收到一条转发的消息,实际上源服务器并不知道终端客户使用的是HTTP/1.0还是HTTP/1.1。因此,HTTP/1.1定义Via头域,用来记录消息转发的路径,它记录了整个路径上所有发送方使用的版本号。
  • HTTP/1.1增加了OPTIONS方法,它允许客户端获取一个服务器支持的方法列表。
  • 为了与未来的协议规范兼容,HTTP/1.1在请求消息中包含了Upgrade头域,通过该头域,客户端可以让服务器知道它能够支持的其它备用通信协议,服务器可以据此进行协议切换,使用备用协议与客户端进行通信。

6.2 缓存

HTTP/1.0中,使用Expire头域来判断资源的fresh或stale,并使用条件请求(conditional request)来判断资源是否仍有效。例如,cache服务器通过If-Modified-Since头域向服务器验证资源的Last-Modefied头域是否有更新,源服务器可能返回304(Not Modified),则表明该对象仍有效;也可能返回200(OK)替换请求的Cache对象。此外,HTTP/1.0中还定义了Pragma:no-cache头域,客户端使用该头域说明请求资源不能从cache中获取,而必须回源获取。

  • HTTP/1.1在1.0的基础上加入了一些cache的新特性,当缓存对象的Age超过Expire时变为stale对象,cache不需要直接抛弃stale对象,而是与源服务器进行重新激活(revalidation)。
  • HTTP/1.0中,If-Modified-Since头域使用的是绝对时间戳,精确到秒,但使用绝对时间会带来不同机器上的时钟同步问题。而HTTP/1.1中引入了一个ETag头域用于重激活机制,它的值entity tag可以用来唯一的描述一个资源。请求消息中可以使用If-None-Match头域来匹配资源的entitytag是否有变化。
  • 为了使caching机制更加灵活,HTTP/1.1增加了Cache-Control头域(请求消息和响应消息都可使用),它支持一个可扩展的指令子集:例如max-age指令支持相对时间戳;private和no-store指令禁止对象被缓存;no-transform阻止Proxy进行任何改变响应的行为。
  • Cache使用关键字索引在磁盘中缓存的对象,在HTTP/1.0中使用资源的URL作为关键字。但可能存在不同的资源基于同一个URL的情况,要区别它们还需要客户端提供更多的信息,如Accept-Language和Accept-Charset头域。为了支持这种内容协商机制(content negotiation mechanism),HTTP/1.1在响应消息中引入了Vary头域,该头域列出了请求消息中需要包含哪些头域用于内容协商。

6.3 带宽优化

HTTP/1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了。例如,客户端只需要显示一个文档的部分内容,又比如下载大文件时需要支持断点续传功能,而不是在发生断连后不得不重新下载完整的包。

  • HTTP/1.1中在请求消息中引入了range头域,它允许只请求资源的某个部分。在响应消息中Content-Range头域声明了返回的这部分对象的偏移值和长度。如果服务器相应地返回了对象所请求范围的内容,则响应码为206(Partial Content),它可以防止Cache将响应误以为是完整的一个对象。
    另外一种情况是请求消息中如果包含比较大的实体内容,但不确定服务器是否能够接收该请求(如是否有权限),此时若贸然发出带实体的请求,如果被拒绝也会浪费带宽。
  • HTTP/1.1加入了一个新的状态码100(Continue)。客户端事先发送一个只带头域的请求,如果服务器因为权限拒绝了请求,就回送响应码401(Unauthorized);如果服务器接收此请求就回送响应码100,客户端就可以继续发送带实体的完整请求了。注意,HTTP/1.0的客户端不支持100响应码。但可以让客户端在请求消息中加入Expect头域,并将它的值设置为100-continue。

节省带宽资源的一个非常有效的做法就是压缩要传送的数据。Content-Encoding是对消息进行端到端(end-to-end)的编码,它可能是资源在服务器上保存的固有格式(如jpeg图片格式);在请求消息中加入Accept-Encoding头域,它可以告诉服务器客户端能够解码的编码方式。而Transfer-Encoding是逐段式(hop-by-hop)的编码,如Chunked编码。在请求消息中加入TE头域用来告诉服务器能够接收的transfer-coding方式。

6.4 长连接

HTTP 1.0规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求。此外,由于大多数网页的流量都比较小,一次TCP连接很少能通过slow-start区,不利于提高带宽利用率。

  • HTTP 1.1支持长连接(PersistentConnection)和请求的流水线(Pipelining)处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。例如:一个包含有许多图像的网页文件的多个请求和应答可以在一个连接中传输,但每个单独的网页文件的请求和应答仍然需要使用各自的连接。
  • HTTP 1.1还允许客户端不用等待上一次请求结果返回,就可以发出下一次请求,但服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果,以保证客户端能够区分出每次请求的响应内容,这样也显著地减少了整个下载过程所需要的时间。

在HTTP/1.0中,要建立长连接,可以在请求消息中包含Connection: Keep-Alive头域,如果服务器愿意维持这条连接,在响应消息中也会包含一个Connection: Keep-Alive的头域。同时,可以加入一些指令描述该长连接的属性,如max,timeout等。
事实上,Connection头域可以携带三种不同类型的符号:

  • 一个包含若干个头域名的列表,声明仅限于一次hop连接的头域信息;
  • 任意值,本次连接的非标准选项,如Keep-Alive等;
  • close值,表示消息传送完成之后关闭长连接;

客户端和源服务器之间的消息传递可能要经过很多中间节点的转发,这是一种逐跳传递(hop-by-hop)。

  • HTTP/1.1相应地引入了hop-by-hop头域,这种头域仅作用于一次hop,而非整个传递路径。每一个中间节点(如Proxy,Gateway)接收到的消息中如果包含Connection头域,会查找Connection头域中的一个头域名列表,并在将消息转发给下一个节点之前先删除消息中这些头域。通常,HTTP/1.0的Proxy不支持Connection头域,为了不让它们转发可能误导接收者的头域,协议规定所有出现在Connection头域中的头域名都将被忽略。

6.5 消息传递

HTTP消息中可以包含任意长度的实体,通常它们使用Content-Length来给出消息结束标志。但是,对于很多动态产生的响应,只能通过缓冲完整的消息来判断消息的大小,但这样做会加大延迟。如果不使用长连接,还可以通过连接关闭的信号来判定一个消息的结束。

  • HTTP/1.1中引入了Chunkedtransfer-coding来解决上面这个问题,发送方将消息分割成若干个任意大小的数据块,每个数据块在发送时都会附上块的长度,最后用一个零长度的块作为消息结束的标志。这种方法允许发送方只缓冲消息的一个片段,避免缓冲整个消息带来的过载。
  • 在HTTP/1.0中,有一个Content-MD5的头域,要计算这个头域需要发送方缓冲完整个消息后才能进行。而HTTP/1.1中,采用chunked分块传递的消息在最后一个块(零长度)结束之后会再传递一个拖尾(trailer),它包含一个或多个头域,这些头域是发送方在传递完所有块之后再计算出值的。发送方会在消息中包含一个Trailer头域告诉接收方这个拖尾的存在。
    状态

6.6 Host头域

在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。

  • HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。此外,服务器应该接受以绝对路径标记的资源请求。

6.7 错误提示

  • HTTP/1.0中只定义了16个状态响应码,对错误或警告的提示不够具体。HTTP/1.1引入了一个Warning头域,增加对错误或警告信息的描述。
  • 此外,在HTTP/1.1中新增了24个状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。

6.8 内容协商

为了满足互联网使用不同母语和字符集的用户,一些网络资源有不同的语言版本(如中文版、英文版)。HTTP/1.0定义了内容协商(contentnegotiation)的概念,也就是说客户端可以告诉服务器自己可以接收以何种语言(或字符集)表示的资源。例如如果服务器不能明确客户端需要何种类型的资源,会返回300(Multiple Choices),并包含一个列表,用来声明该资源的不同可用版本,然后客户端在请求消息中包含Accept-Language和Accept-Charset头域指定需要的版本。
就像有些人会说几门外语,但每种外语的流利程度并不相同。类似地,网络资源也可以有不同的表达形式,比如有母语版和各种翻译版本。HTTP引入了一个品质因子(quality values)的概念来表示不同版本的可用性,它的取值从0.0到1.0。例如一个母语是英语的人也能讲法语、甚至还学了点丹麦语,那么他的浏览器可用作如下配置:Accept-Language: en, fr;q=0.5, da;q=0.1。这时,服务器会优先选取品质因子高的值对应的资源版本作为响应。

Web Pressure Test

Web Pressure Test(WPT)是一个在Linux下使用的非常简单的网站压测工具。它使用fork()模拟多个客户端访问设定的URL,测试网站在压力下工作的性能,最多可以模拟3万个并发连接去测试网站的负载能力。


1. 项目简介

一个简单的开源项目,在Linux下用于网站压力测试,内容虽然不多,但涉及方面很全,适合深入学习。

项目结构

  • socket.c: 定义了Socket函数,用于对socket函数进行包裹。
  • wpt.c: 用于命令行解析和创建并发进程,进行压力测试,并显示相应结果。
  • makefile: 描述了整个工程的编译、连接等规则。
  • README.md: 描述了项目的安装及使用方法。
  • centOS: 包含了项目的更改日志、控制信息、版权及相关规则和目录。

扩展介绍

包裹函数: 完成实际的函数调用,检查返回值,并在发生错误时终止进程,可以通过定义包裹函数来缩短程序。包裹函数名一般是实际函数名的首字母大写形式。


2. 核心文件: wpt.c

核心函数

  • main: 主要调用getopt_long函数处理命令行参数,紧接着调用以下函数。
  • build_request: 主要用来创建http连接请求。
  • wpt: 主要用于复制进程,通过子进程来测试http连接,并把测试结果写入管道,再由父进程读取管道信息,计算并输出相关测试信息。
  • wptcore: 主要用于子进程对http请求进行测试。

3. 细节实现

3.1 getopt_long函数

  • 头文件: #include <getopt.h>
  • 相关函数: getopt
  • 函数声明: int getopt_long(int argc, char* const argv[], const char* opstring, const struct option* longopts, int* longindex)
  • 函数功能: 用于解析命令行选项参数,支持长选项。

getopt与getopt_long相似,而前者只有前三个参数,下面具体讲解getopt_long函数。

函数中有两个默认的参数:
optarg: 如果一个选项有参数,则该参数存放在optarg中,这是一个char*类型。
optind: 下一个将被处理到的参数在argv中的下标值。

前两个参数分别由main函数传入,分别代表参数的个数和参数详细值列表。opstring是选项参数组成的字符串,字符串opstring可以是下列元素: 单个字符,表示选项;单个字符后接一个冒号,表示该选项后必须跟一个参数,参数紧跟在选项后或以空格隔开,该参数的指针赋给optarg;单个字符后跟两个冒号,表示该选项后可以有参数也可以没有参数,如果有参数,参数必须紧跟在选项后不能以空格隔开,该参数的指针赋给optarg。

参数longopts,其实是一个结构的实例:

1
2
3
4
5
6
struct option {
const char *name;
int has_arg;
int *flag;
int val;
}

name表示的是长参数名;has_arg有3个值,no_argument(或者是0),表示该参数后面不跟参数值,required_argument(或者是1),表示该参数后面一定要跟个参数值,optional_argument(或者是2),表示该参数后面可以跟,也可以不跟参数值;flag用来决定,getopt_long()的返回值到底是什么,如果flag是NULL(通常情况),则函数会返回与该项option匹配的val值;如果flag不是NULL,则将val值赋予flag所指向的内存,并且返回值设置为0;val用于和flag联合决定返回值。

参数longindex,表示当前长参数在longopts中的索引值。

设计方法

通过while循环+switch…case…语句配合处理命令行选项,并结合optarg和optind检查参数的合法性。

3.2 build_request函数

主要用一些基本的C字符函数处理有代理或无代理情况下的主机名和段口号,并生成相应的请求消息。如果有代理,则直接根据请求方法和url生成request消息;如果没有代理,则采用url中的域名作为主机名,如果url中含有端口,则采用相应的端口,否则采用默认端口80。

C字符函数

  • strchr(string, char): 查找字符串中首次出现指定字符的位置,如果成功,返回该字符以及其后面的字符,如果失败,则返回NULL。
  • strrchr(string, char): 查找字符在指定字符串中从正面开始的最后一次出现的位置,如果成功,返回该字符以及其后面的字符,如果失败,则返回NULL。
  • strstr(str1, str2): 判断字符串str2是否是str1的子串。如果是,则该函数返回str2在str1中首次出现的地址;否则,返回NULL。
  • strncasecmp(s1, s2): 比较参数s1和s2字符串前n个字符,比较时会自动忽略大小写的差异。如果相同,返回0;s1>s2,返回大于0的值;s1<s2,返回小于0的值。
  • strcspn(s, reject): 参数s字符串的开头计算连续的字符, 而这些字符都完全不在参数reject 所指的字符串中.简单地说,若strcspn()返回的数值为n,则代表字符串s开头连续有n个字符都不含字符串reject 内的字符。

本项目中HTTP的请求格式如下:

1
2
3
4
5
6
GET http://www.github.com:80/ HTTP/1.1 (CRLF)
User-Agent: WPT 1.5 (CRLF)
Host: www.github.com (CRLF)
Pragma: no-cache (CRLF)
Connection: close (CRLF)
(CRLF)

HTTP请求和响应的具体报文格式及更详细的内容可以参照我的博文HTTP协议

3.3 wpt函数

通过主机名和端口号建立套结字,并测试管道的可用性,使用fork函数创建子进程,在子进程中fork返回0,在父进程中返回大于0的值,出错则返回小于0的值。利用pid,让子进程执行wptcore函数对网站进行测试,并将得到的数据写入管道,包括速度,失败数和大小;父进程则继续运行,循环从管道中读取这些值,计算后输出。这里只需使用一个简单的单向数据流管道即可,如下图所示,仅描述管道一般原理,方向其实相反。

管道示意图

3.4 wptcore函数

进行压力测试的核心函数,通过建立套结字发送请求消息,并读取响应消息来计算成功、失败、字节数。使用alarm函数计时并发送SIGALRM信号。遇到套结字建立不成功、读写套结字不成功、协议不支持、关闭套结字不成功等都算失败。

  • 头文件: #include <signal.h>
  • 函数声明: int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
  • 函数功能: sigaction()会依参数signum指定的信号编号来设置该信号的处理函数。参数signum可以指定SIGKILL和SIGSTOP以外的所有信号。

参数结构sigaction定义如下:

1
2
3
4
5
6
7
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}

信号处理函数可以采用void (sa_handler)(int)或void (sa_sigaction)(int, siginfo_t, void)。到底采用哪个要看sa_flags中是否设置了SA_SIGINFO位,如果设置了就采用void (sa_sigaction)(int, siginfo_t, void),此时可以向处理函数发送附加信息;默认情况下采用void (sa_handler)(int),此时只能向处理函数发送信号的数值。

设计方法

使用变量timerexpired来判断连接是否超时,声明为volatile,初始化为0,默认连接成功。当alarm超时后,会设置该变量为1。


4. 调试

调试也是项目不可缺少的一部分,采用GDB进行调试。完成代码编写后,进行调试,发现了几个问题,学习了GDB的常用操作,常用操作如下。

首先,在生成程序时,需要加入-g选项,该选项可以利用操作系统“原生格式”生成调试信息。GDB可以直接利用这个信息,其他调试器也可以使用这个调试信息。该选项和-ggdb的区别是,该选项使gcc为GDB生成专用的更为丰富的调试信息,但是,此时就不能用其他的调试器来进行调试了。

命令 功能
gdb 开启GDB程序
file 指定调试的二进制文件
list 查看源代码,约十行左右
break 设置断点
info breakpoint 查看断点信息
clear 清除断点
run 启动程序,可以加参数
print 查看变量信息
watch 观察变量
next 单步执行,不进入函数
step 单步执行,进入函数
shell 调用shell命令行
quit 退出调试

调试问题1

第一个问题,真没有想到自己会犯。程序可以正常启动,但无论输入什么参数,都只显示help信息。刚开始以为是自己输入的参数格式有误,因为有URL和代理等等比较复杂。最后发现getopt_long函数根本没有执行,原来在刚开始判断参数个数时候就已经写错了,argc == 1 时,缺少参数,显示帮助信息,写成了 argc = 1,这样总是成立,因为argc参数至少为1,所以总是显示帮助信息。还是应该老老实实写成1 == argc,要养成良好习惯。

调试问题2

第二个问题,发现连接总是失败,设置断点后,发现socket函数中的host值总是http:://www.而不并是所希望的www.baidu.com,推测应该在获取主机名时错误,而获取主机名的主要部分在build_request中,进一步检查,发现在URL截取的时候,漏加了i,漏加了1,共有两处。

测试结果

  • 对最大连接数测试

理论上来说,最多可以支持3万多个并发连接,因为最多可以有32768个进程,这个值是根据/proc/kernel/pid_max得到的。但考虑到系统对内存、CPU等方面的限制,一般会有一个上限值,这个值可以通过ulimit -u进行修改,CentOS下该值默认是4096。我们测试了3万了连接,根据设置的出错提示,发现进程号到4000左右时,就提示出错了。这一结果是正常的,创建的进程加上系统自身的服务进程,差不多是这个值。


wpttest1

  • 对网站进行测试

对几个网站进行了一些简单的测试,没做极限测试,因为……


wpttest2


5. makefile

makefile也是项目中重要的一环,也需要认真学习,找了相关的文档,内容很是多,看了一遍。这个项目较小,但也按照相应的形式进行编写,以后如果有机会做大的项目,维护起来,这是必不可少的!

首先,设置相关的标志信息,如CFLAGS等。紧接着是all,最终生成的文件;还有一些常见的伪目标,如install,安装文件到相应的目录,为文件添加man帮助到相应目录,为文件添加版权信息和修改日志到相应目录;tar,打包命令;最后是clean命令清除相关的中间文件等。


6. 完善与维护

大功告成后,还需部分完善:例如,详细的man信息,版权信息,修改日志,控制信息等。同时将项目上传至Github,分享和维护。Github的相关使用,不在这里详述,因为我们之前已经hello world过啦!

项目地址:Web Pressure Test


7. 总结

这是自己第一个学习的开源项目,虽然很小,但是内容很丰富,学习到了很多东西,学到的东西我都列在本文的关键词中了,比如,fork、UNIX网络编程、IPC、GDB调试、makefile等等。其实动手做了就知道,远比这些学到的要多,不过还需要深入看书。这比在学校老师安排的项目有价值的多。学习了一整套开发流程。从项目需求编写(学校项目做过),到实施,到调试,到上传维护,到测试(学校项目做过)以及最后使用手册编写(学校项目做过)都做了一遍。

不过这次不是为的老师,为的是自己的兴趣!希望以后自己能越做越好,好了,就写到这里了。See you in next program!

利用二级指针删除单向链表

Linus曾谈到,他喜欢core low-level coding。今天来用一个简单的例子看一下什么叫core low-level coding。
这个问题是在做一道简单的算法题的时候发现的,自己从来没有看过这样做法,指针真是蕴含着无穷的魅力。


1. 标准化的删除

定义链表结点结构和一个指向判断结点是否满足删除条件的函数的指针。设计和实现一个删除函数删除满足特定条件的结点。

实现函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef struct node{
struct node * next;
...
}node;

typedef bool (* remove_fn)(node const *v);

node * remove_if(node *head, remove_fn rm){
for (node *prev = NULL, *curr = head; curr != NULL;){
node * const next = curr->next;
if (rm(curr)){
if (prev){
prev->next = next;
}
else {
head = next;
}
free(curr);
}
else
curr = next;
}
return head;
}

常见问题

  • 需要一个额外的指针,也是大多数教科书中提到的,previous;
  • 需要做边界条件检查:看curr是否为链表头。

2. 二级指针

实现函数

1
2
3
4
5
6
7
8
9
10
11
void remove_if(node **head, remove_fn rm){
for (node ** curr = head; *curr;){
node * entry = *curr;
if (rm(entry)){
*curr = entry->next;
free(entry);
}
else
curr = &(entry->next);
}
}

常见问题

  • 不再需要previous指针,这里使用的链表指针的指针,curr代表当前结点的next指针的地址,*curr代表了当前结点的next指针,即指向下一结点的指针。
  • 比较抽象,画图可以更好的理解,一般,curr指向当前结点,代表其地址,而现在,curr指向当前结点中的next指针域,代表其地址。
  • 这里的关键是:在链表中的链接本身就是指针,所以可以使用指针的指针来作为改变链表的优先选择。

3. 简单的应用

删除链表中倒数第n个结点,并返回该链表。

实现函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
LinkNode* rm(LinkNode * head, int n){
LinkNode** t1 = &head, *t2 = head;

for (int i = 1; i < n; i++){
t2 = t2->next;
}

while (t2->next != NULL){
t1 = &((*t1)->next);
t2 = t2->next;
}

*t1 = (*t1)->next;

return head;
}

Hello World 2

Hello World 1中的博客搭建与配置是在Windows底下完成的,这次将其移植到Linux下。没有太多的变化,在CentOS下操作异常的方便,安装完必要软件后,没有很多的错误,兼容性极佳,直接将当初Hexo Init的文件夹拷贝到Linux下就可以了。不过因为换了系统,需要重新上传SSH Key,可以直接跳到第三步。


1. 系统环境

常见问题1

在安装Node.js时,要注意下载的版本,是二进制文件,还是源代码。默认下载的是二进制文件,非常方便,只需将Node的bin目录放入PATH中,即可在任意处使用node和npm命令。如果下载的是源代码,则需要进行编译和安装,这里不详述。

注意事项

端口问题在Linux下消失了,默认4000端口就可以用,严格的Linxu没有不知名的端口占用,好评!


2. 主题安装

没有变化。


3. 同步到github

常见问题

需要重新进行SSH Key的生成与上传。

Hello World

终于建立了自己的博客,现在正在完成最后一步————写第一篇文章,关于博客的诞生。这也是第一步。


1. 系统环境

采用Hexo + Github pages搭建自己的博客。

需要的安装程序

  • Node.js
  • Git
  • Hexo 3.2.0

操作命令

1
2
3
4
npm install hexo -g
hexo init
npm install
hexo s -p 3456

这样,打开浏览器http://localhost:3456,就可以看到Hexo的默认主题页面。

常见问题

这里经常会出现的一个问题,就是虽然hexo s成功了,提示从http://localhost:4000访问,但是仍然打不开页面。本机器的原因是端口占用,可以使用
上述命令,修改发布端口,从而解决这个问题。这个问题并没有错误提示,但是安装2.8.2的Hexo会出现该错误提示。


2. 主题安装

可以从Hexo官网寻找Theme或者使用他人推荐的主题。

操作命令

1
2
3
git clone https://github.com/xxx/xxx.git theme/xxx
cd theme/xxx
git pull

下载和获得最新的主题后,需要启动该主题,在Hexo的配置文件中启用:修改目录下的_config.yml中的theme关键字后的内容为xxx。

注意事项

theme后的冒号后有空格。


3. 同步到github

首先要同步SSH Key,这样才能正常和github连接。

操作命令

1
ssh-keygen -t rsa -C "your_email@example.com"

按照提示找到并打开id_rsa私钥文件,并上传到github中。通过下面的命令进行连接测试:

1
ssh -T git@github.com

成功连接后,将Hexo部署到github上:这里要注意,Hexo 3.0.0以上的版本,需要先安装部署插件。

1
npm install hexo-deployer-git --save

之后,需要修改目录下的_config.yml文件中的deploy项:

1
2
3
4
deploy: 
type: git
repo: git@github.com:username/username.github.io.git
branch: master

完成后,进行部署:

1
2
3
hexo clean
hexo g
hexo d

注意事项

type、repo、branch后的冒号均有空格。