WebServer

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-qilixiang.jpg

test-2:

![test-xingkong.gif . . .]


Paste_Image.png

test-3

Paste_Image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,169评论 11 349
  • HTTP概述 超文本传输协议(HTTP,HyperText Transfer Protocol) 是互联网上应用最...
    曹渊说创业阅读 3,838评论 2 61
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,397评论 25 707
  • 生活中的跌跌撞撞,不断试错,在工作中修行----最终是为了找到愉悦的灵魂! 郭相麟
    郭相麟阅读 118评论 0 0