框架图
一般情况下的基本web服务框架:
- httpd(即apache)直接给用户提供web访问服务,它自身可以处理静态请求(如html页面),也可以处理php及cgi这两类动态请求
- tomcat不直接面向用户,一般在httpd的配置文件中增加配置将某类请求转发至tomcat,特别是httpd无法处理的jsp动态请求
httpd
httpd的部署
本机通过yum安装httpd服务,自动安装在目录/etc/httpd/下,利用systemctl进行服务控制,服务名字叫做httpd。
配置文件是conf/httpd.conf,修改处:
Listen 801;
是的,我就改了一下端口,因为本机还有其他无关的服务(和本文无关)监听80端口。然后启动服务。
记住:httpd所有响应都来自于/var/www/下,无论是静态响应还是动态响应。
httpd处理静态请求
静态请求即用户请求一个静态文件,该文件来自于磁盘。httpd的静态文件放在目录/var/www/html/下,如该目录下有一个test_static.html文件,内容是hello ,this is httpd
以下是浏览器测试:
httpd处理动态请求
动态请求即请求到来的时候服务需要执行代码产生内容,然后响应给用户,该响应来自于CPU计算。
cgi模块
所谓的cgi,全称是Common Gateway Interface(通用网关接口),它是一种标准,符合该标准的程序叫做cgi程序。看名字看不懂,它实际上就是httpd进程拉起另外一个进程,由另外一进程来接手处理该请求,步骤如下:
- httpd进程的cgi模块fork一个子进程
- 子进程调用setenv将请求头及其它请求相关的信息写入到环境变量中,环境变量名为:QUERY_STRING、SERVER_PORT、REQUEST_METHOD、REMOTE_ADDR、REMOTE_HOST、CONTENT_TYPE、CONTENT_LENGTH等
- 子进程调用dup2将标准输入重定向到和客户端相关联的已连接的描述符,然后就可以从标准输入中读取请求体
- 子进程调用dup2将标准输出重定向到和客户端相关联的已连接的描述符,然后它写到标准输出的东西都会直接到达客户端(状态行以及大部分响应头已由父进程httpd产生,响应体以及响应体属性相关的响应头由子进程产生)
- 子进程调用execve在子进程的上文中执行请求所要求的程序(程序名在uri中指定)
- 父进程(httpd进程)阻塞在对wait的调用中,等待子进程终止的时候回收操作系统分配给子进程的资源
cgi程序都在/var/www/cgi-bin/下。cgi程序包括可执行的二进制文件或者脚本文件。
二进制
二进制一般用编译型语言(比如C和C++)编写,然后编译链接成一个可执行文件。
现在用C语言编写一个文件,然后编译链接成可执行文件并增加可执行权限,文件名是test_c_cgi,其对应的C源代码是:
#include<stdio.h>
int main()
{
printf("Content-Type:text/html\n\n");//响应头
printf("Hello, this is cgi written in C");//响应体
return 0;
}
以下是浏览器测试:
bash脚本
现在用bash脚本语言编写一个文件,然后增加可执行权限,文件名是test_bash_cgi,其内容是:
#!/bin/bash
echo "Content-Type:text/html"
echo ""
echo "hello, this is cgi written in bash"
以下是浏览器测试:
perl脚本
现在用perl脚本语言编写一个文件,然后增加可执行权限,文件名是test_perl_cgi,其内容是:
#!/usr/local/bin/perl
print "Content-Type:text/html","\n","\n";
print "hello, this is cgi written in perl";
以下是浏览器测试:
python脚本
现在用python脚本语言编写一个文件,然后增加可执行权限,文件名是test_python_cgi,其内容是:
#!/usr/bin/python
print("Content-Type:text/html")
print("")
print("hello, this is cgi written in python")
以下是浏览器测试:
php引擎
php文件和静态文件一样,都在目录/var/www/html/下。
什么是php文件?通俗说就是在一个静态html文件中嵌入php代码,当用户请求这个文件(uri中指定)的时候,httpd的php引擎先执行文件中的php代码,然后用执行后的结果替代掉代码,最后将整个文件内容响应给用户。
现在用php脚本语言在一个有一些静态html内容的文件中插入一段代码,代码的功能是根据请求参数中的age的值来计算明年用户是多少岁(这是一个很吊的逻辑),文件名是test_php.php,文件内容是:
<!DOCTYPE html>
<html>
<body>
<?php//php代码开始
$age=($_GET["age"]);
echo "你的年龄是: ", $age, ", ";
echo "明年你的岁数是: ", ($age+1);
?>//php代码结束
<p>hello, this is a php file, this content is same for every request</p>//这是html文件中写死的一个段落
</body>
</html>
以下是浏览器测试:
tomcat
tomcat的部署
本机直接到tomcat官网下载二进制版本apache-tomcat-8.5.85.tar.gz,选择解压在目录/root/apache-tomcat-8.5.85/下,以下所有提到的相对路径都是现对于这个目录。
启动服务命令是:/root/apache-tomcat-8.5.85/bin/startup.sh
停止服务命令是:/root/apache-tomcat-8.5.85/bin/shutdown.sh
配置文件是conf/server.xml,修改处:
<Connector port="8001" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
此处我也只是改了一下端口,把它默认监听的8080端口改成8001,然后启动服务。
tomcat处理动态请求
tomcat是可以单独对外提供web服务的,但是一般是隐藏在httpd后面,在httpd的配置文件中配置一个转发指令来将某些请求转发到tomcat来处理,我的做法是:在/etc/httpd/conf/httpd.conf中加入这样一条指令:
ProxyPass /tomcat/ http://127.0.0.1:8001/
该指令的意思是:所有以/tomcat/开始的uri的请求都转发到本机的8001端口,该端口正是tomcat在监听。
servlet引擎
什么叫做servlet?简单来说,一个servlet就是一个java类。先按照特殊规范写一个能够处理请求的java类,然后编译成java字节码(从.java文件到.class文件),当请求经路由后被指定由这个java类来处理的时候,tomcat中的servlet引擎将解释执行相应的java字节码来处理请求。
所有编译后的servlet都放在目录webapps/ROOT/WEB-INF/classes/下。
上述介绍过的处理动态请求的方法都没有涉及数据库,servlet举例将涉及到数据库查询以免让你觉得我很菜。
- 创建servlet。现在有一个servlet叫作TestMySQL,它是一个java类,类名就叫TestMySQL,定义在文件TestMySQL.java中。该类按照规范有一个doGet方法(该方法是固定的名字不是随便取的)来处理GET请求,该方法的逻辑是根据uri参数中的name参数值到数据库pzhtest的表haha中查询该name对应的age是多少,然后响应给用户。文件内容如下:
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.*;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class TestMySQL extends HttpServlet {
//JDBC驱动名及数据库URL
static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
static final String DB_URL = "jdbc:mysql://localhost:3306/pzhtest";
//数据库的用户名与密码,需要根据自己的设置
static final String USER = "root";
static final String PASS = "****";//这密码是我写文章的时候故意设置成星号,能让你看到?
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Connection conn = null;
Statement stmt = null;
//设置响应内容类型
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
String docType = "<!DOCTYPE html>\n";
out.println("<!DOCTYPE html>\n" +
"<html>\n" +
"<body>\n");
try{
//注册JDBC驱动器
Class.forName("com.mysql.jdbc.Driver");
//组装SQL查询语句
String query_name = new String(request.getParameter("name").getBytes("ISO8859-1"),"UTF-8");
String sql = "SELECT age FROM haha where name='" + query_name + "'";
//连接并执行查询
conn = DriverManager.getConnection(DB_URL,USER,PASS);
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
//展开结果集
while(rs.next()){
//通过字段检索
int age = rs.getInt("age");
//输出数据
out.println("age of " + query_name);
out.println(" is : " + age);
out.println("<br />");
}
out.println("</body></html>");
//完成后关闭
rs.close();
stmt.close();
conn.close();
}
}
}
- 编译servlet。在servlet的编写规范中,以为要处理http请求所以每个servlet都要导入javax.servlet,如果需要和数据库交互,还要导入java.sql,这两个jar包要提前下载到目录lib/下,编译的时候指定好(用-cp选项指定)。该servlet已经写好,现在要编译它,编译语句为:
javac -cp /root/apache-tomcat-8.5.85/lib/mysql-connector-java-5.1.39-bin.jar -cp /root/apache-tomcat-8.5.85/lib/servlet-api.jar TestMySQL.java
编译后得到了文件TestMySQL.class,它的内容就是java字节码,可以由servlet引擎解释执行。
- 路由。那么如果将请求路由到该servlet呢?打开文件webapps/ROOT/WEB-INF/web.xml,在标签之前加入以下条目:
<servlet>
<servlet-name>aaaaaa</servlet-name><!--此处的servle-name可以随便取名-->
<servlet-class>TestMySQL</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>aaaaaa</servlet-name>
<url-pattern>/query_age</url-pattern>
</servlet-mapping>
配置的意思是:uri为/query_age的时候,将请求路由给TestMySQL这个servlet处理。
以上就阐述完了servlet的创建、编译和路由。
假设数据库中name为pzh的人的age是30(不用猜,我就是pzh,我今年30岁),下面是浏览器测试:
jsp引擎
jsp本质上和php本质类似,就是在一个静态html文件中嵌入动态代码,不同的是语法和引擎执行过程有区别。
jsp文件是用java写的,那它和同是java写的servlet有啥区别?我们细看servlet发现:它需要在代码里大量调用out.println打印出所有的html页面内容,而jsp文件中大部分html内容都写死,只在部分需要动态产生的内容处加入代码逻辑。
jsp引擎做的事情超级容易理解,它接手请求后做的事情:
- 加载相应jsp文件
- 把jsp文件内容转化为一个java类:其中写死的html内容改用out.println来代替(就这?),再加上其他jsp代码一起转化为一个servlet
- 编译该servlet
- 将请求转给servlet引擎执行该sevlet
这么看,jsp引擎就是一个皮包公司。php引擎和它同为兄弟,php就要事事亲力亲为了。但是毫无疑问,jsp文件的编写比自己写一个不出错的servlet更容易(所以到处取代码农的事情到处都在发生)
jsp文件都在目录webapps/ROOT/下。
现在有一个jsp文件,文件名是test_jsp.jsp,内容是:
<html>
<body>
<%//jsp代码开始
out.println("hey, brother, you address is " + request.getRemoteAddr());
%>//jsp代码结束
</body>
</html>
浏览器测试:
总结
以上对该服务框架进行全面但浅显的介绍。本文一定有不严谨处可供专业看官喷一喷,但是我的目的就是通识了解,我也是因为工作中恰好需要具备对它的通识了解才专门实践、证实并编写的本文。
因为本人的工作内容是用nginx+lua做web网关开发。web网关一个重要功能模块是waf检查,不少代码逻辑和大量的waf规则都是和httpd/tomcat有关,所以我就写了本文来记录一下,防止自己忘了又去乱查。