`
裴小星
  • 浏览: 265833 次
  • 性别: Icon_minigender_1
  • 来自: 北京
博客专栏
8ccf5db2-0d60-335f-a337-3c30d2feabdb
Java NIO翻译
浏览量:27853
F3e939f0-dc16-3d6e-8c0b-3315c810fb91
PureJS开发过程详解
浏览量:74380
07a6d496-dc19-3c71-92cf-92edb5203cef
MongoDB Java ...
浏览量:63081
社区版块
存档分类
最新评论

OMToolkit介绍(2) :Web Server 实现

阅读更多
OMToolkit介绍(2) :Web Server 实现

  本文将介绍OMToolkit中Web Server部分的实现,涉及的内容包括基于NIO的Server,配置文件的读取,Http 请求报文的分析,Session和Cookie的获取等。

1. 基于NIO的Server轮询

  首先,是Server类的框架:
package com.omc.server;

import java.io.*;
import java.net.*;
import java.nio.channels.*;
import java.util.*;

/**
 * The start point of the framework, a daemon used to accept requests and update
 * the reading and writing.
 */
public class Server {
	private static final int PORT = 80;

	public void run() throws Exception {
		Selector selector = openSelector();
		while (true) {
			doSelect(selector);
		}
	}

	private Selector openSelector() throws Exception {
		Selector selector = Selector.open();
		// codes ...
		return selector;
	}

	private void doSelect(Selector selector) throws Exception {
		// codes ...
	}

	public static void main(String[] args) throws Exception {
		new Server().run();
	}
}
  先打开selector,然后利用selector进行轮询,单线程管理连接。

  openSelector()方法的实现如下:
	private Selector openSelector() throws Exception {
		Selector selector = Selector.open();
		ServerSocketChannel server = ServerSocketChannel.open();

		server.configureBlocking(false);
		server.register(selector, SelectionKey.OP_ACCEPT);

		server.socket().bind(new InetSocketAddress(PORT));

		return selector;
	}
  打开ServerSocketChannel,设置为非阻塞状态(否则无法注册到selector上),并注册到selector上,关心的事件为SelectionKey.OP_ACCEPT(接受连接),然后监听指定的端口,最后返回selector。

  doSelect(Selector selector)方法的实现如下:
	private void doSelect(Selector selector) throws Exception {
		selector.select();
		Set<SelectionKey> selected = selector.selectedKeys();

		Iterator<SelectionKey> it = selected.iterator();
		while (it.hasNext()) {
			processKey(it.next());
		}

		selected.clear();
	}

	private void processKey(SelectionKey key) throws IOException {
		// codes ...
	}
  执行select(),获取被选择到的key(目前只有ServerSocketChannel对应的key),然后处理key。

  processKey(SelectionKey key)方法的实现如下:
	private void processKey(SelectionKey key) throws IOException {
		ServerSocketChannel server = (ServerSocketChannel) key.channel();
		SocketChannel socket = server.accept();
		
		System.out.println(key);
		System.out.println(socket);

		socket.close();
	}
  实际上没有做什么处理,只是接受连接并打印key和socket,然后关闭socket。后面我们会加上一些处理逻辑。目前的代码应该与附件中的OMServer_Step1.rar相似。

  现在可以运行程序了,在浏览器中输入http://localhost,浏览器将显示网页无法显示,但eclipse控制台上将有如下输出:
sun.nio.ch.SelectionKeyImpl@13e205f
java.nio.channels.SocketChannel[connected local=/127.0.0.1:80 remote=/127.0.0.1:1111]
  这就是一个最基本的Server框架了,之后我们会将接收到的Socket也注册到selector上,这样我们就可以在轮询中同时管理socket了。不过在此之前,我们需要注意一下这行代码:
	private static final int PORT = 80;
  端口号被写死了,更好的方法是从配置文件中读取这个端口号;此外,将日志信息输出到日志文件,而不是控制台,通常会更实用一些。OMToolkit中的许多地方也会需要可配置的参数。因此,我们在这里停留一下,顺便实现配置文件的读取。
  当然,这部分实现与Server关系不大,不感兴趣的读者也可以直接跳过,下载附件中的OMServer_Step2.rar并继续。


2. 配置文件的读取

  我们将实现自己的配置文件读取类。为什么不使用JDK中的Properties类?主要原因是这个类对中文的支持不好。
  在此之前,我们先编写一个读取文件的辅助类:
package com.omc.util;

import java.io.*;

/**
 * Proving method to create readers and writers, and several several versions of
 * <code>read</code> methods to reading file content directly.
 */
public class FileUtil {
	public static BufferedReader reader(String path) throws IOException {
		return new BufferedReader(new FileReader(path));
	}
}
  这个辅助类实现创建读取文件的reader的功能。因为读写文件是经常用到的操作,所以我们将文件相关的操作独立为一个辅助类。

  然后是用来读取配置文件的CfgUtil类:
package com.omc.util;

import java.io.BufferedReader;
import java.util.*;

/**
 * A tool to load configuration file.
 */
public class CfgUtil {
	public static Map<String, String> load(String path) throws Exception {
		Map<String, String> result = new HashMap<String, String>();		
		BufferedReader reader = FileUtil.reader(path + ".cfg");			

		String line;
		while ((line = reader.readLine()) != null) {
			String[] pair = line.trim().split("=");
			result.put(pair[0], pair[1]);
		}

		return result;
	}
}
  这个辅助类实现的功能是根据制定的路径读取配置文件,得到一个Map。对应的配置文件如下:
port=80
log=log.txt
  port参数用于指定监听的端口,而log参数用于指定日志文件的位置。

  下面的代码实现的是通过反射设置类的static属性:
package com.omc.util;

import java.lang.reflect.*;

/**
 * A warp for java reflect package, providing methods to deal with classes, get
 * and set fields of objects.
 */
public class ReflectUtil {
	private static interface FieldSetter {
		public void set(Field field) throws Exception;
	}

	public static void setField(Class<?> clz, String name, final String value)
			throws Exception {
		FieldSetter setter = new FieldSetter() {
			public void set(Field field) throws Exception {
				field.set(null, parseField(field.getType(), value));
			}
		};

		doSetField(clz, name, setter);
	}

	public static Object parseField(Class<?> clz, String value) {
		String type = clz.getName();
		if (type.equals("int")) {
			return Integer.parseInt(value);
		} else if (type.equals("long")) {
			return Long.parseLong(value);
		} else if (type.equals("boolean")) {
			return true;
		} else {
			return value;
		}
	}

	private static void doSetField(Class<?> clz, String name, FieldSetter setter)
			throws Exception {
		for (Field field : clz.getDeclaredFields()) {
			if (field.getName().equals(name)) {
				field.setAccessible(true);
				setter.set(field);
				break;
			}
		}

		Class<?> superClass = clz.getSuperclass();
		if (superClass.getSimpleName().equals("Object")) {
			return;
		}

		doSetField(clz.getSuperclass(), name, setter);
	}
}
  之所以把parseField(...)独立出来,是因为这个方法可能还会在OMtoolkit的其他地方被使用。同样,doSetField(...)方法也是可复用的,但只在类的内部复用,因此访问权限设为private。
  parseField(...)方法将根据类型对字符串进行解析,并返回解析的结果。doSetField(...)方法则找出指定名称的属性,并进行相应设置。setField(...)方法将这两者结合,以实现对类的指定名称的静态属性进行赋值。

  我们可以在Cfg类中看到辅助类 CfgUtil 和 ReflectUtil 的应用:
package com.omc.util;

import java.io.*;
import java.util.Map.*;

/**
 * A wrap for the content of file "Cfg.cfg", providing methods to extract some
 * important info, such as the port number to monitor, the size of the thread
 * pool, the buffer size for reading and writing, etc.
 */
public class Cfg {
	private static int port;
	private static String log;

	public static void init() throws Exception {
		for (Entry<String, String> pair : CfgUtil.load("Cfg").entrySet()) {
			ReflectUtil.setField(Cfg.class, pair.getKey(), pair.getValue());
		}
	}

	public static int port() {
		return port;
	}

	public static String log() {
		return log;
	}
}
  虽然目前只对端口号和日志文件位置进行读取,但后面还将增加更多的配置项。

  由于我们还希望将日志信息输出到日志文件,而不是控制台,因此我们还需要在Cfg.init()方法中加入以下代码:
		OutputStream fos = new FileOutputStream(Cfg.log(), true);
		PrintStream out = new PrintStream(fos);
		System.setErr(out);
		System.setOut(out);

  接着,还需要对Server类进行一些修改,以应用我们从配置文件读取的参数。

  首先,从Server类中移除以下代码:
private static final int PORT = 80;

  然后,修改run()方法并增加init()方法:
	public void run() throws Exception {
		init();
		Selector selector = openSelector();
		while (true) {
			doSelect(selector);
		}
	}

	private void init() throws Exception {
		Cfg.init();
	}

  最后,将openSelector()方法中的以下代码:
server.socket().bind(new InetSocketAddress(PORT));
  替换为:
server.socket().bind(new InetSocketAddress(Cfg.port()));

  现在可以运行程序,并输入http://localhost进行测试,与之前不同的是,现在信息会输出到log.txt中,而不是控制台。试着改变port参数,如9999,那么就可以在浏览器中输入http://localhost:9999进行测试了。

3.Accepter:接受Socket连接

  我们现在是直接在Server类中对select()到的key进行处理的,这样一来,Server类的职责就有点不清晰了。我们希望Server类只负责轮询检查注册到selector上的key的状态,因此我们将把处理key的代码转移到其他类中:处理ServerSocketChannel的Accepter类和处理SocketChannel的Worker类。
  这两个类都实现了OMRunable接口:
package com.omc.core;

/**
 * A interface for the method {@link #run()}.
 */
public interface OMRunnable {
	public void run() throws Exception;
	
	public static class Empty implements OMRunnable {
		public void run() throws Exception{			
		}
	}
}

  这使得我们可以在Server类中统一处理Accepter和Worker,现在可以移除processKey(...)方法,并将doSelect(...)改为如下形式:
	private void doSelect(Selector selector) throws Exception {
		selector.select();
		Set<SelectionKey> selected = selector.selectedKeys();

		Iterator<SelectionKey> it = selected.iterator();
		while (it.hasNext()) {
			((OMRunnable) it.next().attachment()).run();
		}

		selected.clear();
	}

  新建Accepter类:
package com.omc.server;

import java.nio.channels.*;

import com.omc.core.*;

/**
 * Attached on the server socket to accept sockets. When a request accepted, the
 * acceptor will employ a worker to handle it, and then waiting for the next
 * request.
 */
public class Accepter implements OMRunnable {
	SelectionKey key;

	public Accepter(SelectionKey key) {
		this.key = key;
	}

	public void run() throws Exception {
		SocketChannel socket = accept();
		socket.configureBlocking(false);

		System.out.println(socket);

		socket.close();
	}

	private SocketChannel accept() throws Exception {
		return ((ServerSocketChannel) key.channel()).accept();
	}
}

  将Server类的openSelector(...)方法中的如下代码:
server.register(selector, SelectionKey.OP_ACCEPT);
  替换为:
SelectionKey key = server.register(selector, SelectionKey.OP_ACCEPT);
key.attach(new Accepter(key));

  这就实现了将key的处理委托给Accepter了。现在再次运行程序,效果与之前相似,不过只输出socket的信息。现在的代码类似于附件中的 OMServer_Step3.rar。

4.Worker:读写和处理Web请求

  接下来我们将实现处理请求的Worker类。因为Worker类中将用到线程,而JDK中的Thread的run()方法不允许抛出Exception,这多少有些不便,因此我们编写了自己的OMThread类:
package com.omc.core;

/**
 * A wrapper for {@link Thread}, wrapping the {@link #run()} with a exception
 * handler, in order to throw exception in {@link #doRun()}.
 */
public abstract class OMThread extends Thread {
	public void run() {
		wrapRun();
	}

	private void wrapRun() {
		try {
			doRun();
		} catch (Exception e) {
			handleExeption(e);
		}
	}

	protected abstract void doRun() throws Exception;

	protected void handleExeption(Exception e) {
		e.printStackTrace();
	}
}

  好的,除此之外,我们需要读写SoketChannel中的数据,OMtoolkit中的其他地方可能也会有这种需求,因此我们编写了较为通用的ChannelReader和ChannelWriter:
package com.omc.util;

import java.nio.*;
import java.nio.channels.*;
import java.util.*;

/**
 * A tool for reading bytes from channel.
 */
public class ChannelReader {
	private ByteChannel channel;
	private ByteBuffer buffer;
	private Listener listener;
	private List<Byte> bytes = new ArrayList<Byte>();

	public static interface Listener {
		public void onFinish(byte[] bytes) throws Exception;
	}

	public ChannelReader(ByteChannel channel) {
		this.channel = channel;
		this.buffer = ByteBuffer.allocate(Cfg.buffer());
	}

	public ChannelReader(ByteChannel channel, ByteBuffer buffer, Listener listener) {
		this.channel = channel;
		this.buffer = buffer;
		this.listener = listener;
	}

	public boolean update() throws Exception {
		int length = channel.read(buffer);

		if (length > 0) {
			buffer.flip();
			drain();
			buffer.clear();
		}

		if (length < Cfg.buffer()) {
			onFinish();
			return false;
		}

		return true;
	}

	public byte[] read() throws Exception {
		while (update()) {}
		return ArrayUtil.toArray(bytes);
	}

	private void drain() {
		while (buffer.hasRemaining()) {
			bytes.add(buffer.get());
		}
	}

	private void onFinish() throws Exception {
		if (listener != null) {
			listener.onFinish(ArrayUtil.toArray(bytes));
		}
	}
}
  这个类的重点是update()函数,逻辑为每次读取一部分数据,读取的数据量可以在Cfg.cfg中进行配置,并发要求越高的情况,buffer的设置就应该越小。当读取的数据量小于buffer时,说明已经到了末尾了,这个时候就可以通过listener告诉观察者操作已经结束了。
  当然,我们需要在Cfg中添加以下代码,以读取buffer:
	private static int buffer;

	public static int buffer() {
		return buffer;
	}
  同时,在Cfg.cfg中添加:
buffer=2048
  另外,ChannelReader还用到了辅助类 ArrayUtil 的toArray(...)方法:
package com.omc.util;

import java.util.*;

/**
 * Operations related to array or collections.
 */
public class ArrayUtil {
	public static byte[] toArray(List<Byte> list) {
		byte[] bytes = new byte[list.size()];
		for (int i = 0; i < list.size(); ++i) {
			bytes[i] = list.get(i);
		}

		return bytes;
	}
}

  接下来是ChannelWriter类,逻辑与ChannelReader相似,只不过这次是向channel中写入数据:
package com.omc.util;

import java.nio.*;
import java.nio.channels.*;

/**
 * A tool for writing bytes to channel.
 */
public class ChannelWriter {
	protected ByteChannel channel;
	protected ByteBuffer buffer;
	private byte[] toWrite;
	private Listener listener;
	private int index = 0;
	
	public static interface Listener {
		public void onFinish() throws Exception;
	}
	
	public ChannelWriter(ByteChannel channel, ByteBuffer buffer,
			byte[] toWrite, Listener listener) {
		this.channel = channel;
		this.buffer = buffer;
		this.toWrite = toWrite;
		this.listener = listener;
	}
	
	public void update() throws Exception {
		int length = Math.min(Cfg.buffer(), toWrite.length - index);

		if (length > 0) {
			buffer.put(toWrite, index, length);

			buffer.flip();
			channel.write(buffer);
			buffer.clear();
		}

		if (length < Cfg.buffer()) {
			listener.onFinish();
			return;
		}

		index += Cfg.buffer();
	}
}

  最后,终于来到我们的Worker类了:
package com.omc.server;

import java.nio.*;
import java.nio.channels.*;
import java.util.concurrent.*;

import com.omc.core.*;
import com.omc.util.*;

/**
 * Reading message for web request, processing task and writing message back.
 */
public class Worker implements OMRunnable {
	private static ByteBuffer buffer;
	private static ExecutorService pool;

	static {
		buffer = ByteBuffer.allocate(Cfg.buffer());
		pool = Executors.newFixedThreadPool(Cfg.pool());
	}

	private SelectionKey key;
	private SocketChannel socket;
	private OMRunnable runnable;

	public Worker(SelectionKey key) {
		this.key = key;
		socket = (SocketChannel) key.channel();
		runnable = new Reading();
	}

	public void run() throws Exception {
		runnable.run();
	}

	private class Reading implements OMRunnable, ChannelReader.Listener {
		// codes ...
	}

	private class Processing extends OMThread {
		// codes ...
	}

	private class Writing implements OMRunnable, ChannelWriter.Listener {
		// codes ...
	}
}
  Worker类包含了Reading,Processing 和 Wrting 三个内部类,分别处理数据的读取、请求的处理和数据的写回。
  这里用到的pool参数(表示线程池中的线程数量)需要在Cfg中添加:
	private static int pool;

	public static int pool() {
		return pool;
	}

  另外,需要在Cfg.cfg中添加:
pool=10

  Reading类的实现如下:
private class Reading implements OMRunnable, ChannelReader.Listener {
		ChannelReader reader;

		public Reading() {
			reader = new ChannelReader(socket, buffer, this);
		}

		public void run() throws Exception {
			reader.update();
		}

		public void onFinish(byte[] bytes) throws Exception {
			runnable = new OMRunnable.Empty();
			key.interestOps(0);

			if (bytes.length == 0) {
				socket.close();
				return;
			}

			String in = new String(bytes);			
			pool.execute(new Processing(in));
		}
	}
  使用ChannelReader读取SocketChannel的数据,读取完毕时,将请求提交到线程池中。请求的处理时通过Processing类进行封装的。
  另外,之所以将runnable设置为OMRunnable.Empty,是因为即使我们经关心的操作设置为0(key.interestOps(0)),这种改变也并不能立即反映在selector中,而是仍然会被select到几次(通常是两次)。因此我们用空的OMRunnable实例来忽略这些操作。

  Processing类的实现如下:
	private class Processing extends OMThread {
		private String in;

		public Processing(String in) {
			this.in = in;
		}

		protected void doRun() throws Exception {
			System.out.println(in);
			toWrite("Hello World!".getBytes());
		}

		private void toWrite(byte[] out) throws Exception {
			if (!key.isValid()) {
				socket.close();
				return;
			}

			runnable = new Writing(out);
		}
	}
  处理请求。这里只是简单地打印请求报文,并准备好将“Hello World!”写回。然后就转到Writing状态。在此之前还检查了 key 的有效性。

  Writing类的实现如下:
	private class Writing implements OMRunnable, ChannelWriter.Listener {
		ChannelWriter writer;

		public Writing(byte[] out) {
			writer = new ChannelWriter(socket, buffer, out, this);

			key.interestOps(SelectionKey.OP_WRITE);
			key.selector().wakeup();
		}

		public void run() throws Exception {
			writer.update();
		}

		public void onFinish() throws Exception {
			socket.close();
		}
	}
  Writing利用ChannelWriter将数据写回到Socket中,写回结束时则关闭Socket。

  可以看到,读写数据的过程都是单线程管理的,每次只读写部分数据;而请求的处理是使用线程池进行管理的。这么做的原因是通常单线程的开销更小,但请求的处理并非总是能够按照时间片进行分割的(很难像读写操作那样每次只处理一部分),因此还是需要使用多线程进行处理,以免阻塞。

  修改Accepter的run()方法,以注册SocketChannel并引入Worker:
	public void run() throws Exception {
		SocketChannel socket = accept();
		socket.configureBlocking(false);

		SelectionKey k = register(socket);
		k.attach(new Worker(k));
	}

	private SelectionKey register(SocketChannel socket) throws Exception {
		return socket.register(key.selector(), SelectionKey.OP_READ);
	}


  现在可以在浏览器中输入http://localhost进行测试了。浏览器将显示“Hello World!”字样,同时log.txt会输出Web请求报文,内容大致如下:
GET / HTTP/1.1
Accept: */*
Accept-Language: zh-cn
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; Tablet PC 2.0)
Accept-Encoding: gzip, deflate
Host: localhost
Connection: Keep-Alive
  现在的代码类似于附件中的OMServer_Step4.rar。

5.Request:Web请求报文的分析


  接下来,我们将使用Request类对请求报文进行分析。Request类的框架如下:
package com.omc.server;

import static java.net.URLDecoder.*;

import java.util.*;

import com.omc.util.*;

/**
 * The request warp the request message sent by the browser, providing methods
 * to extract some important info (Such as cookies, session, parameters, etc).
 */
public class Request {
	private static final String FAVICON = "favicon.ico";
	private static final String RESOURCES = "resources";

	private String head;
	private String body;
	private String path;
	private String[] parts;
	private String entity;

	private Map<String, String> params;

	public Request(String s) throws Exception {
		String[] in = s.split("(?:\n\n)|(?:\r\n\r\n)");
		head = in[0];
		path = decode(head.split(" ", 3)[1].substring(1), Cfg.charset());
		body = in.length >= 2 ? decode(in[1], Cfg.charset()) : null;
		init();
	}

	private void init() {
		// codes ...
	}

	public String head() {
		return head;
	}

	public String path() {
		return path;
	}

	public boolean isResources() {
		// codes ...
	}

	public String action() {
		return parts.length >= 2 ? parts[1] : "";
	}

	public Map<String, String> params() {
		// codes ...
	}
}
  Request以Http报文作为输入,先将报文按两个换行符分为head和body,在从head中读取path,之后进行一些初始化处理。URL解码时用到了Cfg中的参数charset:
	private static String charset;

	public static String charset() {
		return charset;
	}

  Cfg.cfg中也需要增加:
charset=GBK

  init()方法的实现如下:
	private void init() {
		parts = path.split("/");

		entity = parts[0].isEmpty() ? "" : parts[0];

		if (entity.equals(FAVICON)) {
			entity = RESOURCES;
			path = RESOURCES + "/" + path;
		}
	}
  entity指的是path后的第一个参数,之所以命名为entity,是与OMToolkit的约定有关的,即URL http://localhost/EntityClass/action/param1/value1/param2/value2 表示调用EntityClass的action方法,并将属性param1设置为value1,属性param2设置为value2。,这将在后面介绍 Web Framework实现时用到。
  但仍然有两个例外,一个是形如 http://localhost/resources/* 的形式,表示读取资源文件;另一个是http://localhost//favicon.ico,这是浏览器经常需要访问的一个文件。isResources()方法的实现如下:
	public boolean isResources() {
		return entity.equals(RESOURCES);
	}

  接下来是获取参数的方法params:
	public Map<String, String> params() {
		if (params == null) {
			params = new HashMap<String, String>();
			return body == null ? fromPath() : fromBody();
		}

		return params;
	}

	private Map<String, String> fromPath() {
		// codes ...
	}

	private Map<String, String> fromBody() {
		// codes ...
	}
  如果body为空,则从路径中读取参数;否则,从body中读取参数。这两个方法的实现如下:
	private Map<String, String> fromPath() {
		for (int i = 2; i < parts.length; i += 2) {
			params.put(parts[i], parts[i + 1]);
		}

		return params;
	}

	private Map<String, String> fromBody() {
		List<String> pairs = StringUtil.split(body, '&');
		for (String pair : pairs) {
			List<String> p = StringUtil.split(pair, '=');
			params.put(p.get(0), p.get(1));
		}

		return params;
	}
  需要注意的是,fromBody()方法中,划分字符串时使用了StringUtil.split(...),而非String.split(...),这两者有细微的差别。例如"name=".split("=")得到的是["name"],而我们希望得到的是["name",""]。具体实现如下:
package com.omc.util;

import java.util.*;

/**
 * Operations related to the {@link String} class.
 */
public class StringUtil {
	/**
	 * <code>"\0".split("\0")</code> returns an empty array, while
	 * <code>StringUtil.split("\0", '\0')</code> returns {"", ""}.
	 */
	public static List<String> split(String s, char sperator) {
		List<String> result = new ArrayList<String>();
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < s.length(); ++i) {
			char c = s.charAt(i);

			if (c == sperator) {
				result.add(sb.toString());
				sb = new StringBuilder();
			} else {
				sb.append((char) c);
			}
		}
		result.add(sb.toString());

		return result;
	}
}

  回到Worker类,我们需要做一些修改,以引入Request。修改Processing类的doRun()函数:
		protected void doRun() throws Exception {
			Request req = new Request(in);
			
			if (req.isResources()) {
				toWrite(FileUtil.bytes(req.path()));
			} else {
				toWrite("Hello World!".getBytes());
			}
		}
  这里实际只使用了Request的isResources()方法和path()方法,你也可以将Request的其他方法的结果打印出来,看看效果。这里的处理逻辑是,如果请求的是resources文件夹下的资源,则显示该文件的内容;否则依然写回“Hello World!”。
  这里用到了FileUtil类新增的方法FileUtil.bytes(...),实现如下:
	public static byte[] bytes(String path) throws Exception {
		FileChannel file = new FileInputStream(path).getChannel();
		return new ChannelReader(file).read();
	}

  目前的代码,应该类似于附件中的OMServer_Step5.rar;另外,resources文件夹下的文件,可以下载附件中的resources.rar,解压后复制到项目中。
  运行程序,输入http://localhost/resources/banner.jpg,浏览器中将显示一张图片,效果如图:


6.Session的获取和设置

  接下来就是Session的处理了。OMToolkit是利用Cookie来设置session编号的,如果Http报文中包含了session编号,则在已有的HashMap中查找session;否则创建一个session,并通过 Set-cookie 的方式将session编号告知浏览器。
  下面是Session类的实现:
package com.omc.server;

import java.util.*;

/**
 * The session is a special cookie that also stores some data on the server.
 */
public class Session {
	private String id;
	private long touched = System.currentTimeMillis();
	private Map<String, Object> map = new HashMap<String, Object>();

	public Session(String id) {
		this.id = id;
	}

	public String toString() {
		return "Set-Cookie: session=" + id + ";Path=/";
	};

	public void touch() {
		touched = System.currentTimeMillis();
	}
	
	public long touched() {
		return touched;
	}

	public void set(String key, Object value) {
		map.put(key, value);
	}

	public Object get(String key) {
		return map.get(key);
	}
}

  另外,我们还需要一个操作Sesssion的辅助类SessionUtil:
package com.omc.server;

import java.util.*;
import java.util.concurrent.*;
import java.util.regex.*;

import com.omc.core.*;
import com.omc.util.*;

/**
 * Providing methods to extract sessions from request headers, and a killer
 * thread to kill dead session. The timeout argument can be configured in the
 * file "cfg.properties" (Measure in minutes).
 */
public class SessionUtil {
	private static Map<String, Session> sessions = new ConcurrentHashMap<String, Session>();
	private static Pattern pattern = Pattern.compile("session=(.+?);");

	public static Session getSession(String head) {
		Session session = find(head);
		if (session == null) {
			String uuid = UUID.randomUUID().toString();
			session = new Session(uuid);
			sessions.put(uuid, session);
		} else {
			session.touch();
		}

		return session;
	}

	private static Session find(String head) {
		Matcher matcher = pattern.matcher(head + ";");
		if (matcher.find()) {
			return sessions.get(matcher.group(1));
		}

		return null;
	}
}
  同时,需要在Request类中加入获取session的方法:
	private Session session;

	public Session session() {
		return session;
	}
  并在Request.init()中加入:
session = SessionUtil.getSession(head);

  这里还需要考虑Kill Session的问题,在SessionUtil中加入以下代码:
	public static void init() {
		new Killer().start();
	}

	private static class Killer extends OMThread {
		protected void doRun() throws Exception {
			Thread.sleep(Cfg.timeout());
			while (true) {
				Thread.sleep(Cfg.timeout());
				kill();
			}
		}

		private void kill() {
			long now = new Date().getTime();
			Iterator<Session> it = sessions.values().iterator();
			while (it.hasNext()) {
				checkAndKill(now, it);
			}
		}

		private void checkAndKill(long now, Iterator<Session> it) {
			Session session = it.next();
			if (now - session.touched() > Cfg.timeout()) {
				it.remove();
			}
		}
	}
  其中,session超时参数timeout可以从配置文件中读取。
  在Cfg类中加入:
	private static long timeout;

	public static long timeout() {
		return timeout;
	}
  在Cfg.cfg中加入:
timeout=20
  还要再Server的init()方法中加入:
SessionUtil.init();

  修改Worker类,以显示session。将Processing.doRun()方法中的
toWrite("Hello World!".getBytes());

  改为
toWrite(response(req).getBytes());

response(...)方法的实现如下:
		private String response(Request req) {
			StringBuilder result = new StringBuilder();

			result.append("HTTP/1.1 200 OK\n");
			result.append(req.session() + "\n\n");
			result.append(result.toString().replace("\n", "<br />"));

			return result.toString();
		}
  以两个换行符"\n\n"为界,写回浏览器的内容也分为head和body两个部分。这里的body只是简单地将head复制一份。head中包含了 200 OK 响应编码,以及设置session的字符串(参见Session.toString()方法)。
  现在启动程序并在浏览器中输入http://localhost,将看到如下输出:
HTTP/1.1 200 OK
Set-Cookie: session=c62b456b-5107-4703-929a-2133dc7292d1;Path=/

  目前的代码,应该与附件中的OMSever_Step6.rar相似。

7.Cookie的获取和设置

  最后是Cookie的获取与设置,与Session相似。Cookie类的实现如下:
package com.omc.server;

import java.text.*;
import java.util.*;

/**
 * The cookies save some data in browser.
 */
public class Cookie {
	private static final long LONG_TIME = 365*24*60*60*1000L;
	private static final String GMT = "EEE,d MMM yyyy hh:mm:ss z";

	private String name;
	private String value;
	private Date expires;

	public Cookie(String name, String value) {
		this.name = name;
		this.value = value;

		long now = System.currentTimeMillis();
		expires = new Date(now + LONG_TIME);
	}

	public Cookie(String name, String value, Date expires) {
		this.name = name;
		this.value = value;
		this.expires = expires;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getValue() {
		return value;
	}

	public void setValue(String value) {
		this.value = value;
	}

	public Date getExpires() {
		return expires;
	}

	public void setExpires(Date expires) {
		this.expires = expires;
	}

	public String toString() {
		return "Set-Cookie: " + name + "=" + value
			+ "; Path=/; Expires=" + toGMT(expires);
	}

	public static String get(List<Cookie> cookies, String name) {
		for (Cookie cookie : cookies) {
			if (cookie.getName().equals(name)) {
				return cookie.getValue();
			}
		}

		return "";
	}
	
	private static String toGMT(Date date) {
		Locale locale = Locale.ENGLISH;
		DateFormatSymbols symbols = new DateFormatSymbols(locale);
		DateFormat fmt = new SimpleDateFormat(GMT, symbols);
		fmt.setTimeZone(TimeZone.getTimeZone("GMT"));
		return fmt.format(date);
	}
}

  CookieUtil的实现如下:
package com.omc.server;

import java.util.*;
import java.util.regex.*;

import com.omc.util.StringUtil;

public class CookieUtil {
	private static Pattern pattern = Pattern.compile("Cookie: (.+)");

	public static List<Cookie> cookies(String head) {
		Matcher matcher = pattern.matcher(head);

		List<Cookie> cookies = new ArrayList<Cookie>();

		if (matcher.find()) {
			for (String pair : matcher.group(1).split("; ")) {
				List<String> parts = StringUtil.split(pair, '=');
				if (!parts.get(0).equals("session")) {
					cookies.add(new Cookie(parts.get(0), parts.get(1)));
				}
			}
		}

		return cookies;
	}
}

  在Requset类中增加:
	public List<Cookie> cookies() {
		return CookieUtil.cookies(head);
	}

  在Woker类中增加设置和显示Cookie的代码:
		private String response(Request req) {
			StringBuilder result = new StringBuilder();

			// head...
			result.append("HTTP/1.1 200 OK\n");
			result.append(req.session() + "\n");

			List<Cookie> cookies = req.cookies();
			String oldCookie = oldCookie(cookies);
			String newCookie = newCookie(cookies);

			result.append(newCookie + "\n");

			// body ...
			result.append("oldCookie:<br/>");
			result.append(oldCookie.replace("\n", "<br />"));
			result.append("<br/>");
			
			result.append("newCookie:<br/>");
			result.append(newCookie.replace("\n", "<br />"));

			return result.toString();
		}

		private String oldCookie(List<Cookie> cookies) {
			StringBuilder result = new StringBuilder();
			for (Cookie cookie : cookies) {
				result.append(cookie + "\n");
			}

			return result.toString();
		}
		
		private String newCookie(List<Cookie> cookies) {
			StringBuilder result = new StringBuilder();
			if (cookies.isEmpty()) {
				cookies.add(new Cookie("name", "张三"));
				cookies.add(new Cookie("password", "zhangsan"));
			}

			for (Cookie cookie : cookies) {
				result.append(cookie + "\n");
			}
			return result.toString();
		}

  运行程序,输入http://localhost,将看到如下输出:
oldCookie:

newCookie:
Set-Cookie: name=张三; Path=/; Expires=Sat,17 Mar 2012 07:47:03 GMT
Set-Cookie: password=zhangsan; Path=/; Expires=Sat,17 Mar 2012 07:47:03 GMT

  刷新页面,将看到如下输出:
oldCookie:
Set-Cookie: name=张三; Path=/; Expires=Sat,17 Mar 2012 07:47:55 GMT
Set-Cookie: password=zhangsan; Path=/; Expires=Sat,17 Mar 2012 07:47:55 GMT

newCookie:
Set-Cookie: name=张三; Path=/; Expires=Sat,17 Mar 2012 07:47:55 GMT
Set-Cookie: password=zhangsan; Path=/; Expires=Sat,17 Mar 2012 07:47:55 GMT


  到此为止,OMToolkit中的Server部分就介绍完毕了。现在的代码应该与OMServer_Complete.rar相似。当然,现在Server的功能还很局限。下一篇文章将介绍OMToolkit中的 Web Framework 的实现,到时我们将看到目前的 Server 的功能是如何得到扩展的。
9
9
分享到:
评论
1 楼 裴小星 2011-03-20  
Accepter应该写作Acceptor,SVN上的源码已经修改了,后续的文章也将使用这个名称。

相关推荐

Global site tag (gtag.js) - Google Analytics