简单的WEB服务器

2023-12-13 23:48:12

优质博文:IT-BLOG-CN

目的: 了解Java Web服务器是如何运行的。Web服务器使用HTTP与其客户端,也就是Web浏览器进行通信。基于JavaWeb服务器会使用两个重要类:java.net.Socket类和java.net.ServerSocket类,并通过发送HTTP消息进行通信。

一、HTTP

超文本传输协议Hypertext Transfer Protocol,HTTP是一个简单的请求-响应协议,它通常运行在TCP之上。运行Web服务器和浏览器通过Internet发送并接收数据。请求和响应消息的头以ASCII形式给出;这个简单模型是早期Web成功的有功之臣,因为它使开发和部署非常地直截了当。HTTP使用可靠的TCP连接,默认使用TCP80端口。

HTTP中,总是由客户端通过建立连接并发送HTTP请求来初始化一个事物的。Web服务器端并不负责联系客户端或建立一个到客户端的回调链接。客户端或服务器端可提前关闭连接, 例如, 当使用Web浏览器浏览网页时, 可以单击浏览器上的stop按钮来停止下载文件, 这样就有效的关闭了一个 Web服务器的http连接。

一个HTTP请求包含以下三部分:
【1】请求方法:统一资源标识符Uniform Resource Identifier, URI协议/版本;
【2】请求头;
【3】实体;

// 请求方式 - URL - 协议/版本
POST /examples/default.jsp HTTP/1.1 
Accept: text/plain; text/html 
Accept-Language: en-gb 
Connection: Keep-Alive 
Host: localhost 
User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)
Content-Length: 33 Content-Type: application/x-www-form-urlencoded Accept-Encoding: gzip, deflate 

HTTP 1.1支持7种类型的请 求:GET, POST, HEAD, OPTIONS, PUT, DELETETRACEGETPOST在互联网中最常用的两种请求。

一个HTTP响应包含以下三部分:
【1】协议、状态码、描述;
【2】响应头;
【3】响应实体段;

HTTP/1.1 200 OK 
Server: Microsoft-IIS/4.0 
Date: Mon, 5 Jan 2004 13:13:33 GMT 
Content-Type: text/html 
Last-Modified: Mon, 5 Jan 2004 13:13:12 GMT 
Content-Length: 112 
<html> 
    <head> 
        <title>HTTP Response Example</title> 
    </head> 
    <body> 
        Welcome to Brainy Software 
    </body> 
</html>

响应头部的第一行类似于请求头部的第一行。第一行告诉你该协议使用HTTP 1.1,请求成功200,表示一切都运行良好。 响应头部和请求头部类似,也包括很多有用的信息。响应的主体内容是响应本身的HTML内容。

二、Socket类

Socket为网络通信提供了一组丰富的方法和属性。 Socket允许使用枚举中列出的ProtocolType任何通信协议执行同步和异步数据传输。套接字是网络连接的一个端点。套接字使得一个应用可以从网络中读取和写入数据。放在两个不同计算机上的两个应用可以通过连接发送和接受字节流。为了从你的应用发送一条信息到另一个应用,你需要知道另一个应用的IP地址和套接字端口。

// host远程主机的地址,port远程端口
public Socket (java.lang.String host, int port) 

一旦你成功创建了一个Socket类的实例,你可以使用它来发送和接受字节流。要发送字节流,你首先必须调用Socket类的getOutputStream方法来获取一个java.io.OutputStream对象。要发送文本到一个远程应用,你经常要从返回的OutputStream对象中构造一个java.io.PrintWriter对象。要从连接的另一端接受字节流,你可以调用Socket类的getInputStream方法用来返回一个java.io.InputStream对象。 以下的代码片段创建了一个套接字,可以和本地HTTP服务器(127.0.0.1是指本地主机)进行通讯,发送一个HTTP请求,并从服务器接受响应。它创建了一个StringBuffer对象来保存响应并在控制台上打印出来。

public static void main(String[] args) throws UnknownHostException, IOException, InterruptedException {
    Socket socket = new Socket("127.0.0.1", 80); 
    //想要发送自己流你需要的得到socket类返回的一个OutputStream对象
    OutputStream os = socket.getOutputStream(); 
    boolean autoflush = true; 
    //通过现有的OutputStream构建一个PrintWriter对象来向输出流中写数据
    PrintWriter out = new PrintWriter( socket.getOutputStream(), autoflush); 
    //从连接的另一端接受数据
    BufferedReader in = new BufferedReader( new InputStreamReader( socket.getInputStream())); 
    // 发送HTTP请求到web服务器
    out.println("GET /index.jsp HTTP/1.1"); 
    out.println("Host: localhost:8080"); 
    out.println("Connection: Close"); 
    out.println(); 
    // 读取返回值 
    boolean loop = true; 
    StringBuffer sb = new StringBuffer(8096); 
    while (loop) {
        // 告知是否准备读取此流
        if ( in.ready() ) {
            int i=0; 
            while (i!=-1) { 
                // 读取单个字符
                i = in.read();
                sb.append((char) i);
            } 
        loop = false;
    }
    Thread.currentThread().sleep(50);
} 

//关闭 socket
socket.close(); 

ServerSocket类

Socket类代表一个客户端套接字,即任何时候你想连接到一个远程服务器应用的时候你构造的套接字,现在,假如你想实施一个服务器应用,例如一个HTTP服务器或者FTP服务器,你需要一种不同的做法。这是因为你的服务器必须随时待命,因为它不知道一个客户端应用什么时候会尝试去连接它。为了让你的应用能随时待命,你需要使用java.net.ServerSocket类。这是服务器套接字的实现。

ServerSocketSocket不同,服务器套接字的角色是等待来自客户端的连接请求。一旦服务器套接字获得一个连接请求,它创建一个Socket实例来与客户端进行通信。 要创建一个服务器套接字,你需要使用ServerSocket类提供的四个构造方法中的一个。你需要指定IP地址和服务器套接字将要进行监听的端口号。通常,IP地址将会是127.0.0.1,也就是说,服务器套接字将会监听本地机器。服务器套接字正在监听的IP地址被称为是绑定地址。服务器套接字的另一个重要的属性是backlog,这是服务器套接字开始拒绝传入的请求之前,传入的连接请求的最大队列长度。 其中一个ServerSocket类的构造方法如下所示:

// 创建绑定到特定端口的服务器套接字。
public ServerSocket(int port) throws IOException
// 利用指定的 backlog 创建服务器套接字并将其绑定到指定的本地端口号。
public ServerSocket(int port, int backlog) throws IOException
// 使用指定的端口、侦听 backlog 和要绑定到的本地 IP 地址创建服务器。
public ServerSocket(int port, int backlog, InetAddress address) throws IOException
// 创建非绑定服务器套接字。使用此构造方法时, 如果没有抛出异常,就意味着应用程序已经成功绑定到指定的端口,并且侦听客户端请求。
public ServerSocket() throws IOException

通过ServerSocket创建实例后,可以让它在绑定地址和服务器套接字正在监听的端口上等待传入的连接请求。你可以通过调用ServerSocket类的accept方法做到这点。这个方法只会在有连接请求时才会返回,并且返回值是一个Socket类的实例。Socket对象接下去可以发送字节流并从客户端应用中接受字节流,就像上述的Socket类解释的那样。

//ServerSocketDemo
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;

public class ServerSocketDemo extends Thread {

    private ServerSocket serverSocket;
    private int i = 1;

    public ServerSocketDemo(int port) throws IOException {
        serverSocket = new ServerSocket(port);
        //设置20s内无客户端连接,则抛出SocketTimeoutException异常
        serverSocket.setSoTimeout(20000);
    }

    public void run(){
        while(true) {
            System.out.println("服务端第"+i+"次启动中...对应的端口号为:"+ serverSocket.getLocalPort());
            i++;
            try {
                Socket server = serverSocket.accept();
                
                //彩蛋
                //server.setSoTimeout(5);
				//彩蛋

                //当服务端监听到客户端的连接后才会执行以下代码
                System.out.println("服务端打印的远程主机地址为:"+server.getRemoteSocketAddress());

                //监听来自客户端的消息
                DataInputStream dis = new DataInputStream(server.getInputStream());
                System.out.println("服务端接收到的来自于客户端的信息为:"+dis.readUTF());

                //通过socket向客户端发送信息
                DataOutputStream dos = new DataOutputStream(server.getOutputStream());
                dos.writeUTF("我是服务端,您已连接到:"+server.getLocalSocketAddress());
                server.close();

            }catch (SocketTimeoutException e){
                System.out.println("20s内无客户端连接,正在关闭服务端监听服务");
                continue;
            }catch (IOException e) {
                e.printStackTrace();
                break;
            }


        }
    }

    public static void main(String[] args) {
        try {
            Thread t1 = new ServerSocketDemo(8089);
            t1.run();
        }catch(IOException e){
            e.printStackTrace();
            return;
        }
    }

}

三、HttpServer类

HttpServer类表示一个Web服务器,具体实现如代码如下:

public class HttpServer {
    public static void main(String[] args) {
        HttpServer server = new HttpServer();
        server.await();
    }

    public void await() {

    }
}

这个Web服务器可以处理对指定目录中的静态资源的请求,该目录包括由公有静态变量final WEB_ROOT指明的目录及其所有子目录。WEB_ROOT的初始值为:

public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot";

该代码清单包含一个名为webroot的目录,用于测试该应用程序的一些静态资源都位于该目录下。在该目录下还可以找到用于测试后续章节中应用程序的几个servlet程序。若要请求静态资源,可以在浏览器的地址栏或URL框中输入如下的URL

http://machineName:port/staticResource

若从另一台机器(不是运行应用程序的那台机器)上向该应用程序发出请求,则machineName是应用程序所在计算机的名称或IP地址;若在同一台机器上发出的请求,则可以将machineName替换为localhost,此外,连接请求使用的端口为8080staticResource是请求的文件的名字,该文件必须位于WEB_ROOT指向的目录下。

例如,如果你正使用同一台机器来测试该应用程序,你想让HttpServer对象发送index.html文件,就可以使用如下的URL

http://localhost:8080/index.html

若要关闭服务器,可以通过Web浏览器的地址栏或URL框,在URLhost:port部分后面输入预先定义好的字符串,从Web浏览器发送一条关闭命令,这样服务器就会收到关闭命令了。关闭命令定义在HttpServer类的SHUTDOWN静态final变量中:

private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";

因此,若要关闭服务器,需要使用如下的URL

private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";

因此,若要关闭服务器,需要使用如下的URL

http://localhost:8080/SHUTDOWN

在应用程序的入口点,也就是静态main函数中,创建一个HttpServer实例,然后调用其await()方法。顾名思义,await方法会在制定的端口上等待http请求,并对其进行处理,然后发送相应的消息回客户端。在接收到命令之前,它会一直保持等待的状态。

 public void await() {
    ServerSocket serverSocket = null;
    int port = 8080;
    try {
      serverSocket =  new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
    }
    catch (IOException e) {
      e.printStackTrace();
      System.exit(1);
    }

    // Loop waiting for a request
    while (!shutdown) {
      Socket socket = null;
      InputStream input = null;
      OutputStream output = null;
      try {
        socket = serverSocket.accept();
        input = socket.getInputStream();
        output = socket.getOutputStream();

        // create Request object and parse
        Request request = new Request(input);
        request.parse();

        // create Response object
        Response response = new Response(output);
        response.setRequest(request);
        response.sendStaticResource();

        // Close the socket
        socket.close();

        //check if the previous URI is a shutdown command
        shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
      }
      catch (Exception e) {
        e.printStackTrace();
        continue;
      }
    }
}

该方法名之所以称为await(),而不是wait(),是因为wait()方法是java.lang.Object类中与使用线程相关的重要方法。await()方法会先创建一个ServerSocket实例,然后进入一个while循环:

serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
......
// Loop waiting for a request
while(!shtdown) {
    ...
}

当从8080端口接收到HTTP请求后,ServerSocket类的accept()方法返回,等待结束:

socket = serverSocket.accept();

接收到请求后,await()方法会从accept()方法返回的Socket实例中获取java.io.InputStream对象和java.io.OutputStream对象:

input = socket.getInputStream(); 
output = socket.getOutputStream();

然后,await()方法会创建一个ex01.pyrmont.Request对象,并调用其parse()方法来解析HTTP请求的原始数据:

// create Request object and parse 
Request request = new Request(input); 
request.parse();

然后,await()方法会创建一个Response对象,并分别调用其setRequest()方法和sendStaticResource()方法:

// create Response object
Response response = new Response(output); 
response.setRequest(request); 
response.sendStaticResource();

最后,await()方法关闭套接字,调用Request类的getUri()方法来测试HTTP请求的URI是否是关闭命令。若是,则将变量shutdown设置为true,程序退出while循环。

// Close the socket 
socket.close (); 
 
//check if the previous URI is a shutdown command 
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);

四、Request类

Request类表示一个HTTP请求。可以传递InputStream对象(从通过处理与客户端通信的Socket对象中获取的),来创建Request对象。可以调用InputStream对象中的read()方法来读取HTTP请求的原始数据。

package ex01.pyrmont;

import java.io.InputStream;
import java.io.IOException;

public class Request {

    private InputStream input;
    private String uri;

    public Request(InputStream input) {
        this.input = input;
    }

    // 解析input输入流,这里只是获取请求行的URI
    // 实际的解析过程远不止这些
    public void parse() {

        //下面是用最常见的read()方法获取输入流的内容,也是为什么要传入输入流的原因
        StringBuffer request = new StringBuffer(2048);
        int i;
        byte[] buffer = new byte[2048];
        try {
            i = input.read(buffer);
        } catch (IOException e) {
            e.printStackTrace();
            i = -1;
        }
        for (int j = 0; j < i; j++) {
            request.append((char) buffer[j]);
        }
        System.out.print(request.toString());
        uri = parseUri(request.toString());
    }
    //获取URI,通过对字符串进行简单的查询和切割获得
    private String parseUri(String requestString) {
        int index1, index2;
        index1 = requestString.indexOf(' ');
        if (index1 != -1) {
            index2 = requestString.indexOf(' ', index1 + 1);
            if (index2 > index1)
                return requestString.substring(index1 + 1, index2);
        }
        return null;
    }

    public String getUri() {
        return uri;
    }
}

parse()方法用于解析HTTP请求中的原始数据。parse()方法会调用私有方法parseUri()来解析HTTP请求的URI,除此之外,并没有做太多的工作。parseUri()方法将URI存储在变量uri中。调用公共方法getUri()会返回HTTP请求的URIparse()方法从传入到Request对象中的套接字的InputStream对象中读取整个字节流,并将字节数组存储在缓冲区中。然后,它使用缓冲区字节数组中的数组填充StringBuffer对象request,并将StringBufferString表示传递给parseUri()方法。

parseUri()方法从请求行中获取URIparseUri()方法在请求中搜索第一个和第二个空格,从中找出URI

五、Respose类

对目标文件存在与否进行两种不同的处理 如果存在就将文件的内容写入浏览器,否则返回404页面到浏览器 从这个类可以看出,这个类只是简单的文件作为静态资源,将文件的内容写到浏览器中,没有加载servlet的代码

package ex01.pyrmont;

import java.io.OutputStream;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.File;

/*
 HTTP Response = Status-Line
 *(( general-header | response-header | entity-header ) CRLF)
 CRLF
 [ message-body ]
 Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
 */

public class CopyOfResponse {

    private static final int BUFFER_SIZE = 1024;
    Request request;
    OutputStream output;

    public CopyOfResponse(OutputStream output) {
        this.output = output;
    }

    public void setRequest(Request request) {
        this.request = request;
    }
    //设置静态资源
    public void sendStaticResource() throws IOException {
        byte[] bytes = new byte[BUFFER_SIZE];
        FileInputStream fis = null;
        try {
            //获取URI对应磁盘下的文件对象,因为需要用到URI,所以传入request参数
            File file = new File(HttpServer.WEB_ROOT, request.getUri());
            if (file.exists()) {
                //文件存在的话就将页面写到浏览器上
                fis = new FileInputStream(file);
                int ch = fis.read(bytes, 0, BUFFER_SIZE);
                while (ch != -1) {
                    output.write(bytes, 0, ch); //传入输出流是用于将内容写到浏览器上
                    ch = fis.read(bytes, 0, BUFFER_SIZE);
                }
            } else {
                //文件不存在,返回404页面
                String errorMessage = "HTTP/1.1 404 File Not Found\r\n"
                        + "Content-Type: text/html\r\n"
                        + "Content-Length: 23\r\n" + "\r\n"
                        + "<h1>File Not Found</h1>";
                output.write(errorMessage.getBytes());
            }
        } catch (Exception e) {

            System.out.println(e.toString());
        } finally {
            if (fis != null)
                fis.close();
        }
    }
}

response对象是通过传递由套接字获得的OutputStream对象给HttpServer类的await方法来构造的。Response类有两个公共方法:setRequestsendStaticResourcesetRequest方法用来传递一个Request对象给Response对象,sendStaticResource方法是用来发送一个静态资源,例如一个HTML文件。它首先通过传递上一级目录的路径和子路径给File累的构造方法来实例化java.io.File类。File file = new File(HttpServer.WEB_ROOT, request.getUri())。然后它检查该文件是否存在。假如存在的话,通过传递File对象让sendStaticResource构造一个java.io.FileInputStream对象。然后,它调用FileInputStreamread方法并把字节数组写入OutputStream对象。请注意,这种情况下,静态资源是作为原始数据发送给浏览器的。假如文件并不存在,sendStaticResource方法发送一个错误信息到浏览器。

文章来源:https://blog.csdn.net/zhengzhaoyang122/article/details/134911782
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。