Chapter1 一个简单的Web服务器
Chapter1 一个简单的Web服务器
1.1 HTTP
HTTP
RFC 2616 - Hypertext Transfer Protocol — HTTP/1.1
版本: HTTP/1.1
TCP 连接
基于:“请求—响应”的协议
1.1.1 HTTP 请求
一个 HTTP 请求包含以下三部分
* 请求方法 —— URI(Uniform Resource Identifier, 统一资源标识符)
* 请求头
* 实体
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
http .png
HTTP 支持的七种请求方法
1. GET
2. POST
3. HEAD
4. OPTIONS
5. PUT
6. DELET
7. TRACE
URI(Uniform Resource Identifier, 统一资源标识符) 制定 Internet 资源的完整路径。 URI 通常会被解释为相对于服务器根目录的相对路径,因此,它总是以 “/” 开头的。
URL(Uniform Resource Locator, 统一资源定位符) 实际上是 URI 的一种类型。
版本协议指明了当前请求使用的 HTTP 协议的版本。
1.1.2 HTTP 响应
一个 HTTP 响应包括三部分
- 协议——状态码——描述
- 响应头
- 相应实体段
HTTP/1.1 200 OK
Server: Microsoft-IIS/4.0
Data: Mon, 5 Jan 2004 13:13:33 GMT
Content-Length: 112
<html>
<head>
<title>HTTP Response Example</title>
</head>
<body>
Welcome to Brainy Software
</body>
</html>
http-respond.jpg
状态码 200 表示请求成功。
响应实体正文是一段 HTML 代码
1.2 Socket 类
Socket (套接字) 是网络连接的端点。Socket 使应用程序可以从网络中读取数据,可以向网络中写入数据。不同计算机上的两个应用程序可以通过连接发送或接收字节流,以此达到相互通信的目的。为了从一个应用程序向另一个应用程序发送消息,需要知道另一个应用程序中 Socket 的 IP 地址和端口号。
public class Socket
extendsObject
implementsCloseable
This class implements client sockets (also called just "sockets"). A socket is an endpoint for communication between two machines.
The actual work of the socket is performed by an instance of theSocketImpl
class. An application, by changing the socket factory that creates the socket implementation, can configure itself to create sockets appropriate to the local firewall.
创建一个 Socket :
Socket()
Creates an unconnected socket, with the system-default type of SocketImpl. Socket(String host, int port)
Creates a stream socket and connects it to the specified port number on the named host. Socket(String host, int port, boolean stream)
Deprecated. Use DatagramSocket instead for UDP transport.
host
- the host name, ornull
for the loopback address.
port
- the port number.
host
:远程主机的名称或 IP 地址 (127.0.0.1 表示一个本地主机)
post
:连接远程应用程序的端口号
使用 socket 实例发送/接收字节流:
socket.getOutputStream()
发送文本
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
接收字节流:
socket.getInputStream()
Example: 创建一个Socket,用于与本地 HTTP 服务器进行通信
Socket socket = new Socket(host, portNumber);
OutputStream os = socket.getOutputStream();
boolean autoflush = true;
PrintWriter out = new PrintWriter(socket.getOutputStream(), autoflush);
String message = command.getText();
out.println(message);
out.println("Host: localhost:8080");
out.println("Connection: Close");
out.println();
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
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);
}
response.setText(sb.toString());
socket.close();
ServerSocket 类
Socket 类:表示一个客户端套接字。
ServerSocket 类:服务器套接字。
public class
ServerSocket
extendsObject
implementsCloseable
This class implements server sockets. A server socket waits for requests to come in over the network. It performs some operation based on that request, and then possibly returns a result to the requester.
The actual work of the server socket is performed by an instance of theSocketImpl
class. An application can change the socket factory that creates the socket implementation to configure itself to create sockets appropriate to the local firewall.
服务器套接字要等待来自客户端的连接请求。当服务器套接字接收到了连接请求之后,它会创建一个 Socket 实例来处理与客户端的通信。
ServerSocket
的构造函数
Constructor Description ServerSocket()
Creates an unbound server socket. ServerSocket(int port)
Creates a server socket, bound to the specified port. ServerSocket(int port, int backlog)
Creates a server socket and binds it to the specified local port number, with the specified backlog. ServerSocket(int port, int backlog,InetAddress bindAddr)
Create a server with the specified port, listen backlog, and local IP address to bind to.
port
- the port number, or0
to use a port number that is automatically allocated.
backlog
- requested maximum length of the queue of incoming connections.
bindAddr
- the local InetAddress the server will bind to
backlog
:服务端socket处理客户端socket连接是需要一定时间的。ServerSocket有一个队列,存放还没有来得及处理的客户端Socket,这个队列的容量就是backlog的含义。如果队列已经被客户端socket占满了,如果还有新的连接过来,那么ServerSocket会拒绝新的连接。也就是说backlog提供了容量限制功能,避免太多的客户端socket占用太多服务器资源。
bindAddr
:必须是java.net.InetAddress.
类的实例
创建 InetAddress
实例对象的一种简单方法是调用其静态方法 getByName()
,传入包含主机名的字符串:
InetAddress.getByName("127.0.0.1");
创建了 ServerSocket
实例后,可以使其等待传入的连接请求,该请求会通过服务器套接字侦听的端口上的绑定地址传入,这些工作可以通过调用 ServerSocket
类的 accept
方法完成。只有收到连接请求后,该方法才会返回。
public Socket accept() throws IOException
A new Socket
s
is created and, if there is a security manager, the security manager'scheckAccept
method is called withs.getInetAddress().getHostAddress()
ands.getPort()
as its arguments to ensure the operation is allowed. This could result in a SecurityException.Returns: the new Socket.
1.3 应用程序
本章的 Web 服务器包括三个类:
- HttpServer
- Request
- Response
- 程序的入口点(静态
main()
方法)在HttpServer
类中。main()
方法创建一个 HttpServer 实例,然后调用其await()
方法。 -
await()
方法会在指定端口上等待 HTTP 请求,对其处理,然后发送响应信息回客户端。在接收到关闭命令前,它会保持等待状态。
该应用程序仅发送位于指定目录的静态资源请求,如 HTML 文件和图像文件,它将传入到的 HTTP 请求字节流现实到控制台上。但是它不发送任何 header (头信息) 到浏览器,如日期或 cookies 等。
1.3.1 HttpServer
类
package ch1;
import java.io.IOException;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
// 处理对指定目录中静态资源的请求
public class HttpServer {
// 指明目录
public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot";
//目录名为 'webroot'
// shutdown command
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
// 关闭命令——/SHUTDOWN
// the shutdown command received
private boolean shutdown = false;
public static void main(String[] args) {
HttpServer server = new HttpServer();
server.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);
}
//Loop waiting for a request
while (!shutdown) {
Socket socket = null;
InputStream input = null;
OutputStream output = null;
try {
// accept()
// Listens for a connection to be made to this socket
// and accepts it. The method blocks until a connection is made.
socket = serverSocket.accept();
// getInputStream()
// an input stream for reading bytes from this socket.
input = socket.getInputStream();
// getOutputStream()
// an output stream for writing bytes to this socket.
output = socket.getOutputStream();
// creat Request object and parse
Request request = new Request(input);
request.parse();
// creat Response object
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();
// close the socket
socket.close();
// check if the prevous URI is a shutdown command
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
// 判断一个输入元素与是否与已知元素相同
} catch (Exception e) {
e.printStackTrace();
continue;
}
}
}
}
这个 Web 服务器可以处理对指定目录中的静态资源的请求。该目录包括公有静态变量 final WEB_ROOT
指明的目录及其所有子目录。WEB_ROOT
的初始值为:
public static final String WEB_ROOT =
System.getProperty("user.dir") + File.separator + "webroot";
若要请求静态资源,可以在浏览器的地址栏或 URL 框中输入如下的 URL:
http://machineName:port/staticResource
即
http://localhost:8080/source
若从另一台计算机上向该应用程序发出请求,则 machineName 是应用程序所在计算机的名称或 IP 地址;若在同一台机器上发出的请求,则可以将 machineName 替换为 localhost。
此连接请求使用的端口为 8080。
staticResourse
是请求的文件的名字,该文件必须位于 WEB_ROOT 指向的目录下。
若要关闭服务器,可以通过 Web 浏览器的地址栏或 URI 框,在 URI 的 host:port
部分后面输入预先定义好的字符串,从 WEB 浏览器发送一条关闭命令,这样服务器就会收到关闭命令了。
例中的关闭命令定义在 HttpServer 类的 SHUTDOWN 静态 final 变量中:
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
若要关闭服务器,输入
http://localhost:8080/SHUTDOWN
await()
方法而不是 wait()
方法,是因为 wait()
方法是 java.lang.Object
类中与使用线程相关的重要方法。
-
await()
方法先创建一个ServerSocket
实例,然后进入一个 while 循环,从 8080 端口接收到 Http 请求后,await()
方法会从accept()
方法返回的 Socket 实例中,获取java.io.InputStream
和java.io.OutputStream
对象:input = socket.getInputStream(); output = socket.getOutputStream();
-
之后,
await()
方法创建一个Request
对象,并调用其paser()
方法来解析 HTTP 请求的原始数据:
//create Request object and parse
Request request = new Request(input);
request.parse();
-
然后,
await()
方法会创建一个Response
对象,并分别调用其setRequest()
方法和sendStaticResource()
方法:// creat 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 prevous URI is a shutdown command shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
1.3.2 Request
类
Request
类表示一个 HTTP 请求,可以传递 InputStream
对象(从通过处理与客户端通信的 Socket
对象中获取的),来创建 Request
对象。可以调用 InputStream
对象中的 read()
方法来读取 HTTP
请求的原始数据。
package ch1;
import java.io.IOException;
import java.io.InputStream;
public class Request {
private InputStream input;
private String uri;
public Request(InputStream input) {
this.input = input;
}
// 解析 HTTP 请求的原始数据
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());
}
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;
}
}
Request
parse()
:解析 HTTP 请求中的原始数据。通过调用private
方法parseUri()
来解析 HTTP 请求的 URI 。
parseUri(String requestString)
:将 URI 存储在变量uri
中。
getUri()
:返回 HTTP 请求的 URI 。
-
parse()
方法从传入到Request
对象中的套接字的InputStream
对象中读取整个字节流,并将字节数据存储在缓冲区中。然后使用缓冲区字节数组中的数组填充StringBuffer
对象request
,并将StringBuffer
的String
表示 传递给parseUri()
方法。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()); }
-
parseURI()
方法从请求行中获取 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; }
1.3.3 Response
类
Response
类表示 HTTP 响应。
package ch1;
import java.io.*;
public class Response {
private static final int BUFFER_SIZE = 2048;
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);
String msg = "HTTP/1.1 404 File Not Found\r\n" +
"Content-Type: text/html\r\n" +
"\r\n";
output.write(msg.getBytes());
while (ch != -1){
output.write(bytes, 0, ch);
ch = fis.read(bytes, 0, BUFFER_SIZE);
}
}
else {
// find 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" +
"<h1>File Not Found</h1>";
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();
}
}
}
}
-
Response
类的构造函数接受一个java.io.OutputStream
对象,:public Response(OutputStream output) { this.output = output; }
-
Response
对象在HttpServer
类的await()
方法中,通过传入从套接字中获取的OutputStream
来创建。 -
Response
内有两个public
方法:setRequest()
和sendStaticResourse()
。setRequest()
方法会接受一个Request
对象作为参数。 -
sendStaticResource()
方法用于发丝路过一个静态资源到浏览器,如 HTML 文件。-
它首先会通过传入父路径和子路径到
File
类的构造函数中来实例化java.io.File
类:File file = new File(HttpServer.WEB_ROOT, request.getUri());
-
然后,它检查该文件是否存在。
-
若存在,
sedStaticResource()
方法会使用File
对象创建java.io.InputStream
。然后它调用FileInputStream
类的read()
方法,并将字节数据写入到OutputStream
输出中。(这种情况下,静态资源的内容作为原始数据发送到浏览器的):if (file.exists()){ fis = new FileInputStream(file); int ch = fis.read(bytes, 0, BUFFER_SIZE); // mac 防止出错 String msg = "HTTP/1.1 404 File Not Found\r\n" + "Content-Type: text/html\r\n" + "\r\n"; output.write(msg.getBytes()); while (ch != -1){ output.write(bytes, 0, ch); ch = fis.read(bytes, 0, BUFFER_SIZE); } }
-
若不存在,
staticResource()
会发送错误信息到浏览器:else { // find 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" + "<h1>File Not Found</h1>"; output.write(errorMessage.getBytes()); }
-
1.3.5 运行应用程序
-
执行 ch1 包
-
在地址栏或 URL 框输入:
http://localhost:8080/source.html
Result
GET /source.html HTTP/1.1
Host: localhost:8080
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Upgrade-Insecure-Requests: 1
Cookie: __utma=111872281.722673633.1521287843.1521287843.1521287843.1; __utmz=111872281.1521287843.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); Idea-7e6930cd=8b83b26b-bd8b-480c-832b-3c0b6e5d4c39
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0.3 Safari/604.5.6
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
Connection: keep-alive
GET /favicon.ico HTTP/1.1
Host: localhost:8080
Accept: */*
Connection: keep-alive
Cookie: __utma=111872281.722673633.1521287843.1521287843.1521287843.1; __utmz=111872281.1521287843.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); Idea-7e6930cd=8b83b26b-bd8b-480c-832b-3c0b6e5d4c39
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0.3 Safari/604.5.6
Accept-Language: zh-cn
Referer: http://localhost:8080/source.html
Accept-Encoding: gzip, deflate
ch1_result.png