Socket通信-web服务器基本原理(静态)

一个Web服务器也被称为HTTP服务器,它通过HTTP协议与客户端通信。这个客户端通常指的是Web浏览器。一个基于Java的Web服务器用到二个重要的类,java.net.Socket与java.net.ServerSocket,并通过HTTP消息通信。因此,本文从讨论HTTP与这二个类开始,然后我将解释一个与本文相关的简单的Web应用。

 

The Hypertext Transfer Protocol(HTTP)

    HTTP是一种让Web服务器与浏览器(客户端)通过Internet发送与接收数据的协议。它是一个请求、响应协议--客户端发出一个请求,服务器响应这个请求。HTTP运用可靠的TCP连接,通常用的TCP80端口。它的第一个版本是HTTP/0.9 ,然后被HTTP/1.0取代。当前的版本是HTTP/1.1,由RFC2616(.pdf)定义。

本节主要对应HTTP1.1,足够使你充分理解由Web服务器程序发出的消息。如果你对更加详细的知识有兴趣,可以参考 RFC2616。

在HTTP中,客户端总是通过建立一个连接与发送一个HTTP请求来发起一个事务。服务器不能主动去与客户端联系,也不能给客户端发出一个回叫连接。客户端与服务器端都可以提前中断一个连接。例如,当用一个浏览器下载一个文件时,你可以通过点击“停止”键来中断文件的下载,关闭与服务器的HTTP连接。

HTTP请求

一个HTTP请求包含三个部分:

Method-URI-Protocol/Version 方法-地址-版本

Request header 请求头

Entity body 请求实体

 

下面是一个 HTTP 请求实例:

POST /servlet/default.jsp HTTP/1.1

Accept: text/plain; text/html

Accept-Language: en-gb

Connection: Keep-Alive

Host: localhost

Referer: http://localhost/ch8/SendDetails.htm

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

The Method-URI-Protocol/Version 在这个请求的第一行:

POST /servlet/default.jsp HTTP/1.1

 

其中 POST 是请求的类型。每个客户端HTTP请求可以是HTTP规范中指定的许多请求类型中的一种。HTTP1.1支持七种类型的请求,它们是GET,POST,HEAD,OPTIONS,PUT,DELETE,TRACE。其中GET与POST是Internet 应用中经常用到的二种请求类型。

URI 完整地指定了 Internet 资源。一个URI通常被解析为相对服务器的根目录。这样,它应该总是以一个 '/' 前缀开始。一个URL实际上是 URI 的一种类型。

Version 指的是该 HTTP 请求所用到的HTTP协议版本。

请求头包含了客户端环境与请求实体的一些有用的信息。例如它包含浏览器设定的语言、实体的长度等等。每条请求头用回车换行符(CRLF)分开。

一个非常重要的空行分开了请求头与实体,它标志着实体内容的开始。一些 Internet 开发书籍认为这个 CRLF 空行是 HTTP 请求的第四个部分。

在上面的 HTTP 请求中,实体只是简单以下的一行:

LastName=Franks&FirstName=Michael

在一个典型的 HTTP 请求中,请求实体内容会长得多。

HTTP 响应

与请求相似,HTTP 响应也由三部分组成:

Protocol-Status code-Description 协议状态 描述代码

Response headers 响应头

Entity body 响应实体

以下是一个 HTTP 响应的实例:

HTTP/1.1 200 OK

Server: Microsoft-IIS/4.0

Date: Mon, 3 Jan 1998 13:13:33 GMT

Content-Type: text/html

Last-Modified: Mon, 11 Jan 1998 13:23:42 GMT

Content-Length: 112

Welcome to Brainy Software

响应头的第一行类似请求头的第一行,告诉你所用的协议是 HTTP 1.1 ,请求成功(200=success),以及没有任何问题。

响应头类似请求头也包含了一些有用的信息。响应的实体响应本身的 HTML 内容。头与实体之间由回车换行的空行(CRLF)分开。

 

Socket 

    一个 socket 是一个网络连接的端点,它使得一个应用可以从网络读与写。在不同电脑上的二个应用软件能够通过收发字节流而彼此通信。要发一个信息到另一个应用程序,你需要知道它的IP地址,以及它的 socket 端口号。在 Java 中,一个 socket 用 java.net.Socket 来实现。

要创建一个 socket ,你可以用 Socket 类中几个构建方法中的一个。其中一个接受主机名与端口号作为参数:

new Socket("yahoo.com", 80);

一旦你成功地创建了一个 Socket 类的实例,你就可以用它去发送与接收字节流了。要发送字节流,你需要呼叫 Socket 类的 getOutputStream 方法来得到一个 java.io.OutputSteam 对象。要发送文本到远程的程序,你通常需要从返回的 OutputStream 创建一个 java.io.PrintWriter 对象。要从连接的另一端接收字节流,你需要呼叫 Socket 类的 getInputStream 方法,它返回一个 java.io.InputStream 对象。

以下代码创建一个可以与本地 HTTP 服务器通信的 socket (127.0.0.1 表示一个本地的主机),发送一个 HTTP 请求,并接收从服务器的响应。它还创建一个 StringBuffer 对象来接受响应,并打印到控制台。

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()));
//send an HTTP request to the web server
out.println("GET /index.jsp HTTP/1.1");
out.println("Host: localhost:8080");
out.println("Connection: Close");
out.println();
// read the response
boolean loop = true;
StringBuffer sb = new StringBuffer(8096);
while (loop) {

 if ( in.ready() ) {

int i=0;

i = in.read();

sb.append((char) i);

}
loop = false;
}
Thread.currentThread().sleep(50);
}
// display the response to the out console
System.out.println(sb.toString());
socket.close();
注意要从web服务器得到正确的响应,你必须要发送用HTTP协议编译了的HTTP请求。如果你看了上面的HTTP部分,你应该能够理解上面代码中的HTTP请求。

 

ServerSocket

    Socket类描述的是“客户端”socket,当你需要创建与远程服务程序连接时需要用到它。如果你想实现一个服务程序,如HTTP服务器或者FTP服务器,则需要另外不同的方法。这是因为你的服务器必须随时服务,它不知道什么时候会有一个客户端程序需要连接它。

因为这个目的,你需要用到java.net.ServerSocket这个类,它是服务器端socket的一个实现。服务器端socket等待来自客户端的连接请求。一旦它收到一个连接请求,它创建一个socket实例来与客户端进行通信。

要创建服务器端socket,需要用到ServerSocket类提供的四个构建方法中的一个。你需要指定服务器端socket侦听的IP地址与端口号。比较典型地,这个IP地址可以是127.0.0.1,意思是该服务器端socket侦听的是本地机器。服务器端socket侦听的IP地址指的是绑定地址。服务器端socket另一个重要的属性是队列长度,即它拒绝请求前所接受的最大请求排队长度。

ServerSocket类的构建方法之一如下:

public ServerSocket(int port,int backLog,InetAddress bindingAddress);

对于这个构建方法,绑定地址必须是 java.net.InetAddress 类的实例。创建一个 InetAddress类的对象的简单方法是呼叫其静态方法 getByName,传递一个包含主机名的字符串。

InetAddress.getByName("127.0.0.1");

以下行的代码创建了一个服务器端socket ,它侦听本地机器的 8080 端口,限制队列长度为 1 。

new ServerSocket(8080,1,InetAddress.getByName("127.0.0.1"));

一旦有了一个 ServerSocket 实例,就可以通过呼叫其 accept 方法来让它等待进来的链接请求。这个方法只有当接收到请求时才返回,它返回的是 Socket 类的实例。这个 Socket 对象就可以用来从客户端应用程序发送与接收字节流,正如上节据说的那样。实际上,accept 方法是本文例子中用到的唯一方法。

应用实例

我们的web服务器程序是 ex01.pyrmont 包的一部分,它包含三个类:HttpServer;Request;Response。

整个程序的入口(静态main方法)是HttpServer类。它创建一个HttpServer的实例,并呼叫其await方法。正如名字表达的,await在一个特定的端口等待HTTP请求,处理它们,并返回响应给客户端。它保持等待状态,直到收到停止命令。(用方法名await代替wait,是因为System中有一个重要的与线程相关的方法)

这个程序只从一个特定的目录发送静态资源,如 HTML 与图像文件。它只支持没有文件头(如日期与 cookie)的情况。现在我们将在如下的几节中看一下这三个类。

 

HttpServer

    HttpServer 实现了一个 web 服务器,它可以提供(serve)特定目录及其子目录下的静态资源。这个特定的目录由 public static final WEB_ROOT 指定。

WEB_ROOT 初始化如下:

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

代码列表中包含了一具叫做 webroot 的目录,里面有一些静态的资源,你可以用来测试本应用。

为了请求一个静态的资源,在浏览器的地址栏输入如是地址:http://machinename:port/staticResources

如果你从不同的机器上发送请求到运行本应用的机器,则machinename是运行应用机器的机器名或IP地址,port是8080,staticResources是被请求的文件名称,它必须包含在 WEB_ROOT目录内。

例如,如果你用同一台电脑来测试这个应用,你想要HttpServer发送index.html这个文件,用以下的地址:http://localhost:8080/index.html

要停止服务,只需要从浏览器发送一个停止(shutdown)命令,即在浏览器的地址栏输入 host:port字段后,加上预先定义好的字符串。在我们的HttpServer类中,停止命令被定义为SHUTDOWN,一个 static final变量。

private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";

因此,要停止服务,你可以这样:http://localhost:8080/SHUTDOWN

现在,让我们看一下列表 1.1 中给出的 await 方法。代码列表后面将对这段代码做一些解释。

Listing 1.1. The HttpServer class' await method

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 
方法以创建一个 ServerSocket 实例开始,然后进入一个 while 的循环。 
erverSocket = new ServerSocket(
port, 1, InetAddress.getByName("127.0.0.1"));...
// Loop waiting for a request
while (!shutdown) {

...
}

在 while 循环中的代码,运行到 ServerSocket 的 accept 方法即停止。这个方法只有在 8080 端口接收到 HTTP 请求才返回:

socket = serverSocket.accept();

收到请求后,await 方法从 accept 方法返回的 Socket 实例中等到 java.io.InputStream 与 java.io.OutputStream: 
input = socket.getInputStream();
output = socket.getOutputStream();然后 await 方法创建一个 Request 对象,呼叫它的 parse 方法来解析这个原始的 HTTP 请求:
 
// create Request object and parse
Request request = new Request(input);

request.parse();下一步,await 方法创建一个 Response 对象并把 Request 对象设置给它,呼叫它的 sendStaticResource 方法: 
// create Response object
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();最后,await 方法关闭 Socket ,呼叫 Request 的 getUri 方法来检查 HTTP 请求的地址是否是一个停止命令。如果是,则 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 请求。创建这个类的实例,并传给它从 Socket 获得的 InputStream 对象,从而捕获与客户端的通信。呼叫 InputStream 对象的 read 方法中的一个就可以得到 HTTP 请求的原始数据。

Request 类有二个 public 方法 parse 与 getUri。parse 方法解析 HTTP 请求的原始数据。它做的事情不多--唯一它使之有效的信息是 HTTP 请求的 URI ,这个通过呼叫私有方法 parseUri 来获得。parseUri 方法把 URI 作为一个变量。调用 getUri 方法可以得到 HTTP 请求的 URI 。

要明白 parse 与 parseUri 的工作原理,你需要知道 HTTP 请求的结构,由 RFC2616 定义。

一个 HTTP 请求包括三个部分:Request line;Headers;Message body 。

现在,我们只需要关注 HTTP 请求的第一部分--请求行。请求行以方法记号开始,接着是请求的 URI 与协议版本,以回车换行符结束。请求行的元素之间以空格分开。例如,一个用 GET 方法的 index.html 文件的请求行如下:

GET /index.html HTTP/1.1

parse 方法从 socket 的 InputStream 传递给 Request 对象中读取字节流,把这个字节数组存在缓冲里。然后,它把 buffer 字节数组里的字节放入叫做 request 的 StringBuffer 对象中,再把 StringBuffer 替换成 String 传递给 parseUri 方法。parse 方法的代码如列表 1.2 Listing

1.2. The Request class' parse method

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< buffer.length;j++)

request.append((char) buffer[j]);

}System.out.print(request.toString());

uri = parseUri(request.toString());

}

parseUri 方法查找请求行的第一个与第二个空格,从而从请求行获得了 URI 。列表 1.3 展示了 parseUri 方法的代码。

Listing 1.3. The Request class' parseUri method

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 

Response 类描述 HTTP 响应。它的构建方法接受 OutputStream 对象,如下:

public Response(OutputStream output) {

this.output = output;

}

Response 对象通过传递从 socket 获得的 OutputStream 对象到 HttpServer 类的 await 方法而创建。

Response 类有二个公共方法 setRequest 与 setStaticResource 。setRequest 用来传递 Request 对象到 Response 对象。它比较简单,代码如列表 1.4 所示:

Listing 1.4. The Response class' setRequest method

public void setRequest(Request request) {

this.request = request;

}

sendStaticResource 方法用来发送静态的资源,例如 HTML 文件。它的实现如列表 1.5 所示:

Listing 1.5. The Response class' sendStaticResource method

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 {

// file not found

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) {
// thrown if cannot instantiate a File object
System.out.println(e.toString() );
}
finally {
if (fis != null)
fis.close();
}
}
SendStaticResource 
方法非常简单。它首先通过传递父与子目录到 File 类的构建方法从而实例化 java.io.File 类。 
File file new File(HttpServer.WEB_ROOT, request.getUri());

然后检查这个文件是否存在。如果存在,则 sendStaticResource 方法传递 File 对象创建 java.io.FileInputStream 对象。然后调用 FileInputStream 的 read 方法,并把字节数组写到 OutputStream 对象 output 。就这样,静态资源的内容作为原始数据被发送到浏览器。 
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);
}
}如果文件不存在,sendStaticResource 发送一个错误信息到浏览器。
 
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());

编译与运行应用程序 
要编辑与运行本文的应用,首先你需要解压源码 zip 文件。直接解压出来的目录被称为工作目录,它有三个子目录:src/,classes/,lib/。要编译应用,从工作目录输入如下命令:
 
javac -d . src/ex01/pyrmont/*.java
-d 选项把结果写到当前目录,而不是 src/ 目录。
 
要运行应用,在当前工作目录输入如下命令:
 
java ex01.pyrmont.HttpServer测试这个应用,打开你的浏览器,在地址栏输入如下地址:http://localhost:8080/index.html

你将在你的浏览器看到 MSIE 4.01; MSIE 4.01; Windows 98) 
Host: localhost:8080
Connection: Keep-Alive

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值