Programming Assignment 1: Building a Multi-Threaded Web Server
一、什么是Web服务器
网页服务器(Web server)一词有两个意思:一台负责提供网页的电脑,主要是各种编程语言构建而成,通过HTTP协议传给客户端(一般是指网页浏览器)。一个提供网页的服务器程序。
虽然每个网页服务器程序有很多不同,但有一些共同的特点:每一个网页服务器程序都需要从网络接受HTTP request,然后提供HTTP response给请求者。HTTP回复一般包含一个HTML文件,有时也可以包含纯文本文件、图像或其他类型的文件。
二、HTTP协议
简介
本试验中我们将通过两个阶段来开发一个web服务器,最后完成一个能够并行服务与多个请求的多线程Web服务器。
我们将实现在RFC 1945定义的HTTP1.0。根据定义,每个Web page中的对象将通过单独的HTTP消息来获取。所实现Web服务器将能够并发地服务于多个请求,这意味着Web服务器是多线程的。 Web服务器的主线程负责侦听某个端口,当收到TCP连接请求时,将创建一个新的socket负责与该TCP连接,并创建新的线程具体负责通过该连接的消息传递。为了简化程序设计任务,我们分两阶段来设计Web服务器。
第一阶段:编写仅仅显示所收到HTTP Request消息所有头部行的一个多线程Web服务器。当该程序运行正确后,将添加适当的代码以实现对Request消息的适当响应。
开发Web服务器时,可以通过Web浏览器来测试它。不过,所编写的Web服务器通常并不工作于80端口,因此,测试时在浏览器的地址栏中需要指定Web服务器的工作端口。例如:假设Web服务器运行在域名为host.someschool.edu的主机上,监听端口6789,我们想获取文件index.html。需要在浏览器的地址栏中输入如下的URL:
http://host.someschool.edu:6789/index.html
如果忽略了 ":6789", 浏览器则默认地认为Web服务器监听80端口。
当Web服务器遇到问题,将向浏览器发送包含适当响应消息的HTML页面,以便在浏览器中显示错误信息。
Web Server in Java: Part A
下面,我们将实现第一阶段的编程任务。当看到"?"时,你需要在该处添加相应的代码。
我们的第一个Web服务器将是多线程的,所收到的每个Request消息将交由单独的线程进行处理。这使得服务器可以并发地为多个客户服务, 或者是并发地服务于一个客户的多个请求.当创建一个新线程时,需要向线程的构造函数传递实现了Runnable 接口的类的一个实例(即通过实现接口Runnable来实现多线程)。这正是我们定义单独的类HttpRequest的原因。Web服务器的结构如下:
import java.io.* ;
import java.net.* ;
import java.util.* ;
public final class WebServer
{
public static void main(String argv[]) throws Exception
{
. . .
}
}
final class HttpRequest implements Runnable
{
. . .
}
通常,Web服务器为通过周知(well known)端口80收到的请求提供服务。可以选择大于1024的任意端口作为Web服务器的监听端口,但需要记着在浏览器地址栏中输入URL时指定Web服务器的动作端口。
public static void main(String argv[]) throws Exception
{
// Set the port number.
int port = 6789;
. . .
}
下面,创建监听端口以等待TCP连接请求。由于Web服务器将不间断地提供服务,我们将侦听操作放在一个无穷循环的循环体中。这意味着需要通过在键盘上输入^C来结束Web服务器的运行。
// Establish the listen socket.
?
// Process HTTP service requests in an infinite loop.
while (true) {
// Listen for a TCP connection request.
?
. . .
}
当收到请求后,我们创建一个HttpRequest 对象,将标征着所建立TCP连接的Socket作为参数传递到它的构造函数中。
// Construct an object to process the HTTP request message.
HttpRequest request = new HttpRequest( ? );
// Create a new thread to process the request.
Thread thread = new Thread(request);
// Start the thread.
thread.start();
为了让HttpRequest对象在一个单独的线程中处理随后的HTTP请求,我们首先创建一个Thread对象,将HttpRequest对象作为参数传递给Thread的构造函数,然后调用Thread的start()方法启动线程。
当一个Thread创建并启动后,主线程回到了循环体的首部。主线程将被阻塞(block)在accept处等待另一个TCP 连接请求的到达。此时,刚刚创建的线程正在运行。当另一个TCP连接请求到达时,主线程将不管前面创建的线程是否结束,重复上面的操作,创建新线程负责新连接的请求处理。
到这为止,主线程的工作就完成了,后面我们将集中精力设计类 HttpRequest。
我们声明HttpRequest类中的两个变量: CRLF and socket。根据HTTP规范, 我们需要用”回车换行”作为Response消息头部行的结束。因此,为了使用方便,我们定义了一个CRLR字符串变量。变量socket用作connection socket, 它将被类HttpRequest的构造函数初始化。
final class HttpRequest implements Runnable
{
final static String CRLF = "\r\n";
Socket socket;
// Constructor
public HttpRequest(Socket socket) throws Exception
{
this.socket = socket;
}
// Implement the run() method of the Runnable interface.
public void run()
{
. . .
}
private void processRequest() throws Exception
{
. . .
}
}
为了将类HttpRequest的实例作为参数传输传递到Thread的构造函数中,HttpRequest必须实现Runnable接口。因此,必须定义HttpRequest的public方法run(),其返回值类型为void。我们在run()中调用实现Request消息处理绝大部分操作的方法 processRequest()。
直到现在,我们其实一直在抛出异常, 而不是catching他们。不过,我们不能从方法run()中抛出异常,因为我们必须严格遵守Runnable接口对run()的声明。Runnable接口的run()方法不抛出任何异常。我们将在processRequest中放置处理代码,并从此在run方法中利用try/catch块处理异常。
// Implement the run() method of the Runnable interface.
public void run()
{
try {
processRequest();
} catch (Exception e) {
System.out.println(e);
}
}
现在,设计processRequest()中的代码。首先获得socket的输入/出流的reference引用。然后,我们给input stream包装过滤器(filters)。但是,输出流无须包装任何过滤器,主要原因是我们将向输出流直接写入bytes。
private void processRequest() throws Exception
{
// Get a reference to the socket's input and output streams.
InputStream is = ?;
DataOutputStream os = ?;
// Set up input stream filters.
?
BufferedReader br = ?;
. . .
}
现在我们已经准备好来获得客户发来的HTTP Request消息了(通过从socket的输入流读取消息)。类BufferedReader的方法readLine()方法将从输入流中读取字符,直到遇到CRLF为止(也就是从input stream中读取一行,行的结束符为CRLF)。
从input stream中读出的第一行为HTTP Request消息的请求行 (参看教材2.2,了解请求行的定义)。
// Get the request line of the HTTP request message.
String requestLine = ?;
// Display the request line.
System.out.println();
System.out.println(requestLine);
读取消息的请求行后,读取消息的其它头部行。由于我们并不知道客户发送消息中有多少头部行,必须利用一个循环操作来获取Request消息的所有头部行。
// Get and display the header lines.
String headerLine = null;
while ((headerLine = br.readLine()).length() != 0) {
System.out.println(headerLine);
}
由于除了需要将头部行中的内容显示在屏幕上外,现阶段无须针对头部行做其它的处理,我们仅仅利用临时变量headerLine来保存头部行的信息。循环操作直到下面的表达式值等于0时停止。也就是读取的 头部行的长度如果为零,表示读出了一个空行,意味着所有的头部行已经全部读出(参看教材的2.2 部分,头部行和entity body之间利用一个空行作为分割)。
(headerLine = br.readLine()).length()
后面我们将添加分析客户Request消息的代码,并发送Response消息 。在进行后面的程序设计前,我们先完成第一阶段的任务,并通过浏览器来测试它。添加如下代码以关闭输入/出流和connection socket。
// Close streams and socket.
os.close();
br.close();
socket.close();
当程序编译成功后,以适当的端口作为参数运行Web服务器,并利用浏览器访问它。在浏览器地址栏中输入下面的示例:
http://host.someschool.edu:6789/
Web服务器将显示HTTP Request消息的内容。检查请求消息的格式是否与教材2.2中描述的HTTP Request消息格式相符。
Web Server in Java: Part B
Web服务器不能仅仅显示收到的Request消息的内容,而是应该分析收到的Request消息并产生适当的Response消息。我们将忽略Request消息头部行中包含的信息,仅仅关注Request消息的请求行中包含的文件名字。我们将假设客户发送的Request消息中的Request行总是使用GET方法,实际上,一个浏览器可能使用GET、POST和HEAD方法(HTTP1.0)
利用类StringTokenizer从Request行中解析出文件名字。
首先,创建一个 StringTokenizer对象来容纳Request行;
第二步:跳过Method字段(因为总是GET方法);
第三步,解析出文件名字。
// Extract the filename from the request line.
StringTokenizer tokens = new StringTokenizer(requestLine);
tokens.nextToken(); // skip over the method, which should be "GET"
String fileName = tokens.nextToken();
// Prepend a "." so that file request is within the current directory.
fileName = "." + fileName;
由于浏览器在文件名字前加了一个“/“,我们在它前面加上一个字符 ”.”,从而限定从当前目录开始获取文件。
现在有了客户请求的文件名字,我们可以打开该文件作为向客户发送该文件的第一步。如果文件不存在,构造函数 FileInputStream() 将抛出异常FileNotFoundException,为了在抛出此可能的异常后不终止线程的执行,利用一个try/catch块将布尔型变量fileExists设置为false。后面我们将使用该变量来构建一个错误响应消息,而不是发送一个根本不存在的文件。
// Open the requested file.
FileInputStream fis = null;
boolean fileExists = true;
try {
fis = new FileInputStream(fileName);
} catch (FileNotFoundException e) {
fileExists = false;
}
Response消息有三部分: the status line, the response headers, 和entity body。状态行、头部行以CRLF作为结束。利用变量statusLine 来保存响应消息的statusline、contentTypeLine保存Content-Type头部行信息。当文件不存在时,Web服务器将返回状态行为“404 Not Found“,entity body中保存利用HTML创建的错误消息。
// Construct the response message.
String statusLine = null;
String contentTypeLine = null;
String entityBody = null;
if (fileExists) {
statusLine = ?;
contentTypeLine = "Content-type: " +
contentType( fileName ) + CRLF;
} else {
statusLine = ?;
contentTypeLine = ?;
entityBody = "<HTML>" +
"<HEAD><TITLE>Not Found</TITLE></HEAD>" +
"<BODY>Not Found</BODY></HTML>";
}
当文件存在,需要确定文件的MIME类型和发送适当的MIME-Type指示符,利用private方法contentType()实现该上述任务。该方法将返回包含在Conten-Type头部行的信息(字符串)。
现在,我们可以通过向socket的输出流写入status line 和唯一的一个header line来向客户浏览器发送信息。
// Send the status line.
os.writeBytes(statusLine);
// Send the content type line.
os.writeBytes(?);
// Send a blank line to indicate the end of the header lines.
os.writeBytes(CRLF);
下面需要发送消息的entity body了。如果请求的文件存在,我们调用另一个方法来发送文件;如果请求的文件不存在,我们向客户发送一个HTML编码的错误消息(前面已经准备好,即在变量entityBody中。
// Send the entity body.
if (fileExists) {
sendBytes(fis, os);
fis.close();
} else {
os.writeBytes(?);
}
发送完entitybody后,线程的任务已经全部完成,在结束线程前需要关闭流和socket.
我们还需要实现前面提到的两个方法:contentType()和
sendBytes()。
private static void sendBytes(FileInputStream fis, OutputStream os)
throws Exception
{
// Construct a 1K buffer to hold bytes on their way to the socket.
byte[] buffer = new byte[1024];
int bytes = 0;
// Copy requested file into the socket's output stream.
while((bytes = fis.read(buffer)) != -1 ) {
os.write(buffer, 0, bytes);
}
}
read()和write()均抛出异常,我们在sendBytes中并不处理这些异常,而是将异常处理的任务交给调用sendBytes的方法。
变量buffer,用于作为文件和输出流之间的中间存储空间。当从FileInputStream中读取字节时,,检查读取的字节是否为-1(即文件结束标识 EOF)。如果读到了EOF,read()返回已经放入buffer的字节数。利用方法类OutputStream 的方法write() 将保存在buffer中的字节数据发送到输出流, write的参数buffer、0、bytes分别为byte数组的名字、第一个字节的位置、需要写出的字节数。
Web Server中需要完成最后一部分代码为contentType,实现根据文件的扩展名来确定所代表的MIME 类型。如果文件扩展名未知,则方法返回application/octet-stream.
private static String contentType(String fileName)
{
if(fileName.endsWith(".htm") || fileName.endsWith(".html")) {
return "text/html";
}
if(?) {
?;
}
if(?) {
?;
}
return "application/octet-stream";
}
到现在为止,我们完成了Web Server的第二阶段任务。尝试从保存有homepage的目录运行Web服务器,记住在URL中包含Web服务器的工作端口。
整段代码
import java.net.ServerSocket;
import java.net.Socket;
import java.awt.im.InputContext;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.StringTokenizer;
public final class Webserver {
public static void main(String[] args) throws Exception {
int port = 6666;
//server创立接听端口
ServerSocket welcomeSocket = new ServerSocket(port);
//处理一个死循环中的 HTTP 服务请求
while(true)
{
//client监听一个 TCP 连接请求
Socket connectionSocket = welcomeSocket.accept();
// 构造一个对象来处理 HTTP 请求消息
HttpRequest request = new HttpRequest(connectionSocket);
//创建一个新的线程来处理请求
Thread thread = new Thread(request);
//开始新线程
thread.start();
}
}
}
final class HttpRequest implements Runnable {
//http空白行结束标志
final static String CRLF = "\r\n";
Socket socket;
//构造函数
public HttpRequest(Socket socket) {
this.socket = socket;
}
private void processRequest() throws Exception {
// 获取套接字的输入和输出流的引用
InputStream is = socket.getInputStream();
DataOutputStream os = new DataOutputStream(socket.getOutputStream());
//设置输入流的缓冲
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//获取请求的 HTTP 请求消息的行
String requestline = br.readLine();
//显示请求行
System.out.println();
System.out.println(requestline);
//得到且显示获取的头部
String headerline = null;
while ((headerline = br.readLine()).length() != 0) {
System.out.println(headerline);
}
//从请求行中提取文件名。
StringTokenizer tokens = new StringTokenizer(requestline);
tokens.nextToken();
String fileName = tokens.nextToken();
//前面加上“.”所以,在当前目录下的文件的请求
fileName = '.' + fileName;
//打开文件流和文件信息
FileInputStream fis = null;
boolean fileExists = true;
try {
fis = new FileInputStream(fileName);
} catch (FileNotFoundException e) {
fileExists = false;
}
//构建相应信息
String statusLine = null;
String contentTypeLine = null;
String entityBody = null;
if (fileExists) {
statusLine = "HTTP/1.0 200 OK";
contentTypeLine = "Content-type:" + contentType(fileName) + CRLF;
} else {
statusLine = "HTTP/1.0 404 Not Found";
contentTypeLine = "Content-type: text/html" + CRLF;
entityBody = "<HTML>" + "<HEAD><TITLE>Not Found</TITLE></HEAD>" + "<BODY>Not Found</BODY></BODY>";
}
//发送状态线
os.writeBytes(statusLine);
//发送链接类型
os.writeBytes(contentTypeLine);
///发送一个空白行,以指示头行的结束
os.writeBytes(CRLF);
if (fileExists) {
sendBytes(fis, os);
fis.close();
} else {
os.writeBytes(entityBody);
}
os.close();
br.close();
socket.close();
}
private void sendBytes(FileInputStream fis, DataOutputStream os) throws IOException {
//构建1K缓冲的方式字节
byte[] buffer = new byte[1024];
int bytes = 0;//桶装为0
//将请求的文件复制到套接字的输出流中
while ((bytes = fis.read(buffer)) != -1) {
os.write(buffer, 0, bytes);
}
}
private static String contentType(String fileName) {
if (fileName.endsWith(".htm") || fileName.endsWith(".html")) {
return "text/html";
}
if (fileName.endsWith(".jpg")) {
return "text/jpg";
}
if (fileName.endsWith(".gif")) {
return "text/gif";
}
if (fileName.endsWith(".mp3")) {
return "audio/mp3";
}
if (fileName.endsWith(".mp4")) {
return "video/mpeg4";
}
return "application/octet-stram";
}
//实现runnable接口的run函数
@Override
public void run() {
try {
processRequest();
} catch (Exception e) {
System.out.println(e);
}
}
}
实验结果
test-1:
test-2:
![test-xingkong.gif . . .]