一个web服务器也叫做HTTP服务器,因为它使用HTTP协议同客户端(即浏览器)通信。一个基于Java的web服务器用到的两个重要类:java.net.Socket和java.net.ServerSocket,通信协议采用HTTP。因此,很自然的接下来我们就以HTTP和java的这两个类来谈谈web服务器。随后我们再介绍一个简单的web服务器应用。
一、HTTP(The Hypertext Transfer Protocol):
Http是允许web服务端和浏览器之间通过Internet发送/接收的协议,它是一个请求/响应的协议。浏览器请求一个文件,服务器会响应这个请求。Http用Tcp连接方式----默认端口是80.Http的第一个发布版本是Http/0.9,目前一般用的是Http1.1.
通过Http协议,通常是浏览器通过建立连接并且发送请求来发起一个会话事务,服务器端会响应或者给浏览器一个响应的连接,浏览器端或者服务器端可以在会话中提前终止一个连接。例如,当用一个浏览器作为客户端,可以点击停止按钮就可以终止正在下载的文件,从而有效的关闭与web服务器端的Http连接。
1.HTTP请求:
一个Http请求包含以下3个部分:
·Method-URI-Protocal/Version
·Request headers
·Entity body
一个HTTP请求的例子如下:
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
lastName=Franks&firstName=Michael
Method-URI-Protocal/Version出现在请求的第一行,即:
POST /examples/default.jsp HTTP/1.1
其中POST表示是请求的方法,/examples/default.jsp代表URI,HTTP/1.1表示Protocol/Version.
HTTP1.1支持七种请求方法:GET、POST、HEAD、OPTIONS、PUT、DELETE和TRACE。其中GET和POST是最常用的。
URI完全地说明了源文件类型,一个URI通常是相对于服务器端的根目录。这样一来,URI的路径通常是这样的/*。URL统一资源定位符通常就是URI。协议版本代表所用的HTTP版本。
请求的文件头request header可以体现出请求的浏览器信息和实体等重要信息。例如,它包含浏览器所用的编码方式,实体的长度等等。每一个header被CRLF(carriage return/linefeed)分开,CRLF即回车换行。
在headers和entity实体之间,会有一个CRLF来分隔,这个对于HTTP请求格式非常重要。
CRLF告诉HTTP服务器请求的内容从哪里开始。
在上面的HTTP请求中,请求实体如下:
lastName=Franks&firstName=Michael
2.HTTP响应:
与Http请求相似,一个Http响应也由以下三部分组成:
·Protocol-Status code-Description
·Response headers
·Entity body
下面是一个HTTP响应的例子:
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
Welcome to Brainy Software
响应的第一行与请求的第一行格式有些相似。它告诉协议是HTTP1.1,请求成功标志200.并且一切正常OK。响应的报文头与请求的报文头相似,也包含了一些环境参数。同样响应报文也以CRLF来分隔开。
二、Socket类:
Socket是网络连接的一个端口。Socket可以使应用程序在网络中读/写到数据。分别位于不同计算机的两款应用软件可以依靠Socket相互进行接收/读取数据,为使一台计算机上的应用软件发送信息给另一台电脑,需要知道其IP地址和端口号。在Java中,socket类即是java.net.Socket类。
创建socket对象,可以用该类众多构造方法中的一种来构造对象,其中一个是这样的,需要host名字和端口号:public Socket (java.lang.String host,int port) .例如要连接端口号为80的yahoo.com,可以这样来写:new Socket(“yahoo.com”,80)。
下面的代码片断创建了一个socket类,不过是同本机127.0.0.1通信。
Socket socket = new Socket(“127.0.0.1”,”8080”);
OutputStream os = socket.getOutputStream();
boolean autoflush = true;
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”);
outprintln(“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);
//响应结果到显示平台
System.out.println(sb.toString());
Socket.close();
三、ServerSocket类
Socket类代表“客户端”的socket,也就是说无论什么时候要连接远端服务器时,创建一个socket对象即可。现在,如果要想创建一个服务器应用,比如HTTP server或者FTP server,则需要用不同的方式。这是因为server端要实时接收客户端的请求。
ServerSocket与Socket不同。ServerSocket的角色是一直在等待客户端的请求。一旦ServerSocket接收到客户端的请求,则会创建一个Socket对象来进行通信。
要创建一个服务器套接字,你需要使用ServerSocket类提供的四个构造方法中的一个。你需要指定IP地址和服务器套接字将要进行监听的端口号。通常,IP地址将会是127.0.0.1,也就是说,服务器套接字将会监听本地机器。服务器套接字正在监听的IP地址被称为是绑定地址。服务器套接字的另一个重要的属性是backlog,这是服务器套接字开始拒绝传入的请求之前,传入的连接请求的最大队列长度。
其中一个ServerSocket类的构造方法如下所示:
public ServerSocket(int port, int backLog, InetAddress bindingAddress);
对于这个构造方法,绑定地址必须是java.net.InetAddress的一个实例。一种构造InetAddress对象的简单的方法是调用它的静态方法getByName,传入一个包含主机名称的字符串,就像下面的代码一样。
InetAddress.getByName("127.0.0.1");
下面一行代码构造了一个监听的本地机器8080端口的ServerSocket,它的backlog为1。
new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
一旦你有一个ServerSocket实例,你可以让它在绑定地址和服务器套接字正在监听的端口上等待传入的连接请求。你可以通过调用ServerSocket类的accept方法做到这点。这个方法只会在有连接请求时才会返回,并且返回值是一个Socket类的实例。Socket对象接下去可以发送字节流并从客户端应用中接受字节流,就像前一节"Socket类"解释的那样。实际上,这章附带的程序中,accept方法是唯一用到的方法
四、实例运用
我们这个应用包含如下三个类:
·HttpServer
·Request
·Response
该应用程序的入口(即main方法)在HttpServer类中,main方法创建一个HttpServer对象,然后调用其await方法,该方法正如其名,一直在监听给定的端口,等待HTTP请求,一旦有请求,则进行接收,然后返回response对象。这个方法一直在监听客户端的请求,直到有shutdown命令关闭之。
这个应用不仅仅能发送静态资源,例如HTML文件、image图片以及某一文件夹下的文件,而且能处理动态传送而来的字节。但是它不传送任何的header,比如dates、cookies等。
下面我们来详细的看看这三个类。
HttpServer类
HttpServer类是服务器端代码,如清单1.1。清单1.2详细展现await方法,在清单1.1中不再显示。
list1.1:HttpServer类
public class HttpServer{
/**
*WEB_ROOT这个路径下放置HTML文件和其他一些文件。
*在这个包中,WEB_ROOT就是工作路径”webroot”。
*工作路径是文件系统中java命令所调用的位置。
*/
public static final String WEB_ROOT =
System.getProperty(“user.dir”)+File.separator + “webroot”;
//关闭命令
private static final String SHUTDOWN_COMMAND = “/SHUTDOWN”;
//接收shutdown命令
private boolean shutdown = false;
public static void main(String[] args){
HttpServer server = new HttpServer();
server.await();
}
public void await(){
…
}
}
await方法详细如下:
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);
}
//循环等待请求
while(!shutdown){
Socket socket = null;
InputStream input = null;
OutputStream output = null;
try{
socket = serverSocket.accept();
input = socket.getInputStream();
output = socket.getOutputStream();
//创建Request对象,并且parse
Request request = new Request(input);
request.parse();
//创建Response对象
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();
//关闭socket
socket.close();
//查看URI是否为关闭uri
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
}catch(Exception e){
e.printStackTrace();
continue;
}
}
}
这个web服务器可以访问到所有WEB_ROOT目录以及子目录下的静态资源,WEB_ROOT的初始化如下:public static final String WEB_ROOT =
System.getProperty(“user.dir”)+File.separator +”webroot”;
代码包含了一个叫做webroot的目录,该目录下有一些静态资源。要访问服务器下的静态资源,URL可以这样写:http://machineName:port/staticResource.如果是跨机器访问,那么machineName就是计算机的名字或者IP地址,如果是同一台机器,则为localhost或着计算机名,端口就是8080,staticResource就是你将要访问的文件名,但是该文件必须在WEB_ROOT目录下。
例如,如果你在同一机器通过服务器访问,要访问该服务的index.html文件,URL为:http://localhost:8080/index.html.
如果要关闭服务,则可以在浏览器上输入预先在程序中设置好的url路径。比如现在要停止当前正在运行的服务,我们这个例子中的关闭命令是通过HttpServer类中静态常量SHUTDOWN来控制,private static final String SHUTDOWN_COMMAND = “/SHUTDOWN”,因此我们要关闭该例子的服务,url可以这样来写:http://localhost:8080/SHUTDOWN.
现在,让我们来看看await方法。这个方法名是await而不是wait,主要是因为java.lang.Object这个超类中有个关于多线程的方法名叫做wait。await方法一开始就创建了一个ServerSocket对象,然后进行一个while循环。
serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
//循环等待request请求
while (!shutdown) { ... }
循环代码中当在端口8080上有HTTP的请求时,serverSocket便会通过accept方法返回一个socket对象,即:socket = serverSocket.accpet().
Request类:
该类代表一个HTTP request对象。该request对象实例化需要传递一个socket对象的inputstream对象。可以调用InputStream对象的read方法来获取HTTP request对象所传输的数据。
该类的具体内容如下listing 1.3.该类含有两个方法,parse方法(listing 1.4)和getUri(listing 1.5)方法。
listing 1.3:
public class Request {
private InputStream input;
private String uri;
public Request(InputStream input) {
this.input = input;
}
public String getUri() {
return uri;
}
public void parse() {
……
}
private String parseUri(String requestString) {
……
}
}
Listing 1.4:
public void parse() {
// Read a set of characters from the socket
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());
}
Parse方法解析了request对象传输而来的数据。这个方法别无它途,仅仅要获取该http请求中的url路径。下面的getUri方法则返回了该url路径。
Listing 1.5:
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;
}
Response类:
该类如下:
public class Response {
private static final int BUFFER_SIZE = 1024;
Request request;
OutputStream output;
public Response(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 {
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 {
// 找不到文件
String errorMessage = "HTTP/1.1 404 File Not Found\r\n" +
"Content-Type: text/html\r\n" +
"Content-Length: 23\r\n" +
"\r\n" +
"
File Not Found
";output.write(errorMessage.getBytes());
}
}catch (Exception e) {
// 如果不能实例化File对象,则会抛出一个异常
System.out.println(e.toString() );
}finally {
if (fis!=null)
fis.close();
}
}
}
首先注意到的是,通过构造函数获取到java.io.OutputStream对象,如下:
public Response(OutputStream output) {
this.output = output;
}
运行这个应用:
在浏览器地址栏上输入地址: http://localhost:8080/index.html,可以看到测试页面。
如果输入一个不存在的文件地址,http://localhost:8080/indexs.html 如下,则返回404错误。