优质博文:IT-BLOG-CN
目的: 了解Java Web
服务器是如何运行的。Web
服务器使用HTTP
与其客户端,也就是Web
浏览器进行通信。基于Java
的Web
服务器会使用两个重要类:java.net.Socket
类和java.net.ServerSocket
类,并通过发送HTTP
消息进行通信。
一、HTTP
超文本传输协议Hypertext Transfer Protocol,HTTP
是一个简单的请求-响应协议,它通常运行在TCP
之上。运行Web
服务器和浏览器通过Internet
发送并接收数据。请求和响应消息的头以ASCII
形式给出;这个简单模型是早期Web
成功的有功之臣,因为它使开发和部署非常地直截了当。HTTP
使用可靠的TCP
连接,默认使用TCP
的80
端口。
在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
, DELETE
和TRACE
。GET
和POST
在互联网中最常用的两种请求。
一个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
类。这是服务器套接字的实现。
ServerSocket
和Socket
不同,服务器套接字的角色是等待来自客户端的连接请求。一旦服务器套接字获得一个连接请求,它创建一个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
,此外,连接请求使用的端口为8080
。staticResource
是请求的文件的名字,该文件必须位于WEB_ROOT
指向的目录下。
例如,如果你正使用同一台机器来测试该应用程序,你想让HttpServer
对象发送index.html
文件,就可以使用如下的URL
:
http://localhost:8080/index.html
若要关闭服务器,可以通过Web
浏览器的地址栏或URL
框,在URL
的host: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 requestwhile (!shutdown) {Socket socket = null;InputStream input = null;OutputStream output = null;try {socket = serverSocket.accept();input = socket.getInputStream();output = socket.getOutputStream();// create Request object and parseRequest request = new Request(input);request.parse();// create Response objectResponse response = new Response(output);response.setRequest(request);response.sendStaticResource();// Close the socketsocket.close();//check if the previous URI is a shutdown commandshutdown = 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
请求的URI
。parse()
方法从传入到Request
对象中的套接字的InputStream
对象中读取整个字节流,并将字节数组存储在缓冲区中。然后,它使用缓冲区字节数组中的数组填充StringBuffer
对象request
,并将StringBuffer
的String
表示传递给parseUri()
方法。
parseUri()
方法从请求行中获取URI
。parseUri()
方法在请求中搜索第一个和第二个空格,从中找出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
类有两个公共方法:setRequest
和sendStaticResource
。setRequest
方法用来传递一个Request
对象给Response
对象,sendStaticResource
方法是用来发送一个静态资源,例如一个HTML
文件。它首先通过传递上一级目录的路径和子路径给File
累的构造方法来实例化java.io.File
类。File file = new File(HttpServer.WEB_ROOT, request.getUri())
。然后它检查该文件是否存在。假如存在的话,通过传递File
对象让sendStaticResource
构造一个java.io.FileInputStream
对象。然后,它调用FileInputStream
的read
方法并把字节数组写入OutputStream
对象。请注意,这种情况下,静态资源是作为原始数据发送给浏览器的。假如文件并不存在,sendStaticResource
方法发送一个错误信息到浏览器。