一个最简单的Web服务器
我们今天的任务主要是希望从浏览器中的请求中取得我们需要的文件路径,然后根据路径把客户请求的文件发回给客户端。我们就按照这个思路,先来取得我们需要的文件路径。
上一次最后的部分,我们知道客户请求的字符串中的第一行内容中第二个部分就是我们需要的。
GET /index.html HTTP/1.1
那我们首先就需要写一个字符串解析的方法,把这个路径取得。这个请求内容中,如果是正确的话,那文件路径是保存在第一个空格和第二个空格之间。那我们就按照这个规律,通过调用Java字符串的indexOf方法和substring方法来实现这个功能。既然是一个字符串解析的方法并且我们希望得到的是一个URL因此我们就给这个方法起名为parseUrl。这里我们为了简化问题,先将这个方法定义为一个静态的方法,最终方法会返回给我们需要的文件路径,具体代码如下:
public static String parseUri(String requestHeader){
String requestFilePath = "";
int index1 = requestHeader.indexOf(' ');
if(index1>0){
int index2 = requestHeader.indexOf(' ', index1);
if(index2>0){
requestFilePath = requestHeader.substring(index1, index2);
}
}
return requestFilePath;
}
这里建议你使用我们之前得到的用户请求字符串测试一下,看看是否可以取得 /index.html这个路径。
那我们该如何将客户端需要的这个文件发给浏览器呢?这里我们还需要多做一些准备工作,下面我们先暂时将文件这块放一下,回头看下我们之前的程序,该如何给浏览器返回数据。
之前我们曾经调用如下一段代码:
InputStream inputStream = socket.getInputStream();
获得了之前用户的请求。对应的,我们还可以通过socket对象获得OutputStream对象,这样我们就可以通过OutputStream对象的write方法就可以向浏览器发送数据,具体可以看下如下的示例代码:
package com.tomcat;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
public class Main {
public static void main(String[] args) throws UnknownHostException, IOException{
System.out.println("Server started!");
// 创建一个ServerSocket对象,监听本机8080端口
ServerSocket serverSocket = new ServerSocket(8080, 0, InetAddress.getByName("127.0.0.1"));
while(true){
// 当有在8080端口访问的时候,调用accept方法得到Socket对象
Socket socket = serverSocket.accept();
// 通过Socket获得OutputStream流,用于向浏览器发送消息
OutputStream outputStream = socket.getOutputStream();
// 向浏览器写内容
outputStream.write("<H1>Hello World!</H1>".getBytes());
// 关闭数据输出流
outputStream.close();
}
}
}
这次,我们从Socket对象中取得OutputStream对象,然后调用它的write方法,向浏览器发送一个经典的Hello World。这里要注意,OutputStream对象的write方法的参数是byte数组,因此需要调用字符串的getBytes方法取得当前字符串的byte数组,这点在后面我们作关于文件的传输时候还会提到。
好了,现在先运行一下我们这个程序吧,正常情况下,可以看到Server started后,在浏览器的地址栏输入http://localhost:8080/index.html后,看下浏览器上是否有一个大大的HelloWorld,恭喜你,其实我们离一个简单的Web服务器已经很近了!
不过每次都只是显示一个Hello World是不是有点太无聊了?而且我们前面都已经知道客户端问我们要的是什么文件了,下面我就让客户端能够得到到我们的文件吧。
浏览器需要取得的文件也一定是存放在服务器的某个目录内的,这里我们就需要先建立一个文件夹,名称为webroot(其实随便你命名为何,这里只是笔者的爱好),然后打开一个文本编辑器,录入如下代码,最后以index.html文件名保存:
<html>
<head>
<title>Hello World</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
然后将这个文件夹放在程序的同级目录(即与src、bin目录同级),具体目录层级如下图。
好,至此,我们已经将浏览器需要访问的文件准备好。
这里,我们再梳理一下服务器的执行逻辑:
1 当服务器收到一个请求的时候;
2 解析请求,取得文件路径;
3 需要从指定的文件夹下读取文件;
4 将文件以byte数组形式返回给浏览器;
这里第一步,我们先要创建一个变量,用于保存指定文件夹路径:
public static String WEB_ROOT = "webroot";
变量的值就是我们刚才创建的文件夹名称,因为我们将文件夹放在程序的同级目录,所以根据相对路径,我们只需要知道文件夹名称即可,可能你会觉得这样不够人性化,没关系,后面我们会慢慢完善。
与前面解析文件名称的处理相同,我们首先编写一个方法,传入参数分别为OutputStream和已经解析好的文件名称,主要的功能是读取文件内容,然后发回给客户端,这个算是个核心代码了,具体的程序代码如下:
public static void processStaticResource(OutputStream outputStream,String filePath) throws IOException{
FileInputStream fileInputStream = null;
try{
File file = new File(filePath);
fileInputStream = new FileInputStream(file);
byte[] bytesForFile = new byte[BufferSize];
int ch = fileInputStream.read(bytesForFile, 0, BufferSize);
while(ch!=-1){
outputStream.write(bytesForFile, 0, ch);
ch = fileInputStream.read(bytesForFile, 0, BufferSize);
}
}catch (FileNotFoundException e) {
// TODO: handle exception
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>";
outputStream.write(errorMessage.getBytes());
}finally{
if(fileInputStream!=null){
fileInputStream.close();
}
}
}
这里我再简单解释一下这个方法:
首先我们定义了一个FileInputStream的对象,用于从File对象中读取文件内容。
然后根据文件路径,构造一个File对象。
这里要特别注意这个bytesForFile这个byte数组。之前我们在读取用户的HTTP协议内容的时候也使用过这个byte数组,在网络之间传递数据都需要转换为这种形式,后面我们还会多次使用到,另外也建议深入去阅读JAVA的IO处理,以及NIO相关的知识。好,我们在回到我们定义的这个数组时候使用了BufferSize这个变量,这里是我们创建的一个全局变量,值为2048,这点是为了便于维护。
接着我们再看下这段代码,作用就是将文件的内容以Byte数组的形式发回给客户端:
int ch = fileInputStream.read(bytesForFile, 0, BufferSize);
while(ch!=-1){
outputStream.write(bytesForFile, 0, ch);
ch = fileInputStream.read(bytesForFile, 0, BufferSize);
}
我们调用fileInputStream对象的read方法,根据设定的缓冲区大小,将文件内容写入到byte数组内,返回值为读取的字节数。如果未读取完成,则需要依次调用fileInputStream对象的read方法和outputStream对象的write方法将bytes数组的内容发回到浏览器。其实道理上这个与我们前面写的那个Hello World是相同的。
另外还要注意我们需要处理的异常, FileNotFoundException,即当客户端希望访问的文件在服务器端没有,比如客户端发来的请求为:http://localhost:8080/helloworld.html。这时就需要捕捉这个文件无法找到的异常,返回给客户端一个报错信息:国际标准404的错误。
最后我还需要将我们的fileInputStream对象给关闭。在这个方法中还可能回引发IOException,这里为了简化我们就直接抛出。
好,我们现在把这我们写的这个方法也组合进我们的程序,最终的代码如下:
package com.tomcat;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
public class Main {
public static int BufferSize = 2048;
public static String WEB_ROOT = "webroot";
public static void main(String[] args) throws UnknownHostException, IOException{
System.out.println("Server started!");
// 创建一个ServerSocket对象,监听本机8080端口
ServerSocket serverSocket = new ServerSocket(8080, 0, InetAddress.getByName("127.0.0.1"));
while(true){
// 当有在8080端口访问的时候,调用accept方法得到Socket对象
Socket socket = serverSocket.accept();
// 通过Socket获得OutputStream流,用于向浏览器发送消息
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
String requestHeader;
StringBuilder stringBuilder = new StringBuilder();
byte[] bytes = new byte[2048];
int bytesLength = inputStream.read(bytes);
for(int i =0 ; i<bytesLength; i++){
stringBuilder.append((char)bytes[i]);
}
requestHeader = stringBuilder.toString();
// 解析客户端需要访问的文件路径
String filePath = WEB_ROOT +"/" +parseUrl(requestHeader);
// 将客户端需要的文件发出
processStaticResource(outputStream, filePath);
// 关闭Socket连接
socket.close();
}
}
// 解析客户端发来的请求报文
public static String parseUrl(String requestHeader){
String requestFilePath = "";
int index1 = requestHeader.indexOf(' ');
if(index1>1){
int index2 = requestHeader.indexOf(' ', index1+1);
if(index2>1){
requestFilePath = requestHeader.substring(index1+1, index2);
}
}
return requestFilePath;
}
// 将静态文件发送给客户端
public static void processStaticResource(OutputStream outputStream,String filePath) throws IOException{
FileInputStream fileInputStream = null;
try{
File file = new File(filePath);
fileInputStream = new FileInputStream(file);
byte[] bytesForFile = new byte[BufferSize];
int ch = fileInputStream.read(bytesForFile, 0, BufferSize);
while(ch!=-1){
outputStream.write(bytesForFile, 0, ch);
ch = fileInputStream.read(bytesForFile, 0, BufferSize);
}
}catch (FileNotFoundException e) {
System.out.println(e.getMessage());
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>";
outputStream.write(errorMessage.getBytes());
}finally{
if(fileInputStream!=null){
fileInputStream.close();
}
}
}
}
好了,这时将我们的服务器运行起来,然后打开你的浏览器,输入http://localhost:8080/index.html
如果你看到了那个大大的Hello World!恭喜你,你已经完成了一个简单的Web服务器了,如果有兴趣,你可以多找一些静态网页,带图片的测试一下,享受一下自己的胜利果实吧,其实挑战才刚刚开始!
另外,现在的代码量还不大,我会尽量把代码全部都贴上,后面也考虑阅读的连贯性和方便依据情况只将将最重要的代码写到正文里,而目前现在OSC的博客还不支持上传附件,我也只好把代码再归置归置放到代码分享里吧。如果需要源代码的话也可以直接联系我:changyu496#gmail.com。
非常感谢,欢迎批评指正!