WebServer

作者: 未来城市说 | 来源:发表于2016-10-18 20:34 被阅读0次

    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

    相关文章

      网友评论

        本文标题:WebServer

        本文链接:https://www.haomeiwen.com/subject/ymututtx.html