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

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上的源码已经修改了,后续的文章也将使用这个名称。

相关推荐

    OMToolkit: Web Server,Web Framework 及 Object-Orinted Database 的简单实现

    OMToolkit 是一个集成 Web 服务器、Web 框架以及对象-关系数据库的工具包,旨在简化开发过程,提供一站式的解决方案。通过这个工具,开发者可以快速构建基于 Web 的应用程序,同时利用面向对象的数据库来存储和管理...

    数学建模学习资料 神经网络算法 参考资料-Matlab 共26页.pptx

    数学建模学习资料 神经网络算法 参考资料-Matlab 共26页.pptx

    happybirthday2 升级版生日祝福密码0000(7).zip

    happybirthday2 升级版生日祝福密码0000(7).zip

    ssm框架Java项目源码-基于web技术的税务门户网站的实现+vue毕设-大作业.zip

    本项目是一个基于SSM框架的税务门户网站实现,结合了Vue技术,旨在提供一个全面的税务信息管理平台。该项目主要功能包括税务信息查询、税务申报、税务政策浏览及用户管理等多个模块。通过这些功能,用户可以方便地查询和管理税务相关的各类信息,同时也能及时了解最新的税务政策和规定。 项目采用SSM框架,即Spring、Spring MVC和MyBatis,这三者的结合为项目提供了强大的后端支持,确保了数据的安全性和系统的稳定性。前端则采用Vue.js框架,以其高效的数据绑定和组件化开发模式,提升了用户界面的响应速度和用户体验。 开发此项目的目的不仅是为了满足计算机相关专业学生在毕业设计中的实际需求,更是为了帮助Java学习者通过实战练习,深入理解并掌握SSM框架的应用,从而在实际工作中能够更好地运用这些技术。

    php7.4.33镜像7z压缩包

    php7.4.33镜像7z压缩包

    ssm框架Java项目源码-基于java的珠宝购物网站系统的建设+jsp毕设-大作业.zip

    本项目是一个基于Java的珠宝购物网站系统,采用SSM框架进行开发,旨在为计算机相关专业学生提供一个实践平台,同时也适合Java学习者进行实战练习。项目的核心功能涵盖商品展示、用户注册登录、购物车管理、订单处理和支付系统等。通过这一系统,用户可以浏览各类珠宝商品,包括详细的商品描述、高清图片和价格信息,同时能够方便地添加商品至购物车,并进行结算和支付操作。 在技术实现方面,项目运用了Spring、Spring MVC和MyBatis三大框架,确保系统的稳定性和扩展性。Spring负责业务逻辑层,提供依赖注入和面向切面编程的支持;Spring MVC则处理Web层的请求和响应,实现MVC设计模式;MyBatis作为持久层框架,简化了数据库操作。 此外,项目采用JSP技术进行前端页面展示,结合HTML、CSS和JavaScript等技术,为用户提供友好的交互界面。

    基于java的高校大学生党建系统设计与实现.docx

    基于java的高校大学生党建系统设计与实现.docx

    毕设源码-python-django疫情数据可视化分析系统(论文+PPT)-期末大作业+说明文档.rar

    本项目是一个基于Python-Django框架开发的疫情数据可视化分析系统,旨在为计算机相关专业的学生提供一个实践平台,同时也适用于需要进行项目实战练习的同学。项目集成了疫情数据的收集、处理、分析和可视化功能,为用户提供了一个直观、高效的数据分析环境。 在功能方面,系统能够自动抓取最新的疫情数据,包括确诊、疑似、治愈和死亡人数等关键指标。数据处理模块则负责清洗和整理这些数据,以确保分析的准确性。分析模块采用了多种统计方法和机器学习算法,以揭示疫情的发展趋势和潜在模式。可视化模块则通过图表和地图等形式,直观地展示了分析结果,便于用户理解和分享。 项目的开发框架选择了Django,这是一个高级Python Web框架,它鼓励快速开发和清晰、务实的设计。Django的强大功能和灵活性,使得项目能够快速响应需求变化,同时保证了系统的稳定性和安全性。

    果树领养计划.docx

    果树领养计划.docx

    java毕设项目之java基于云平台的信息安全攻防实训平台(源码+说明文档+mysql).zip

    环境说明:开发语言:Java 框架:springboot JDK版本:JDK1.8 服务器:tomcat7 数据库:mysql 5.7 数据库工具:Navicat 开发软件:eclipse/myeclipse/idea Maven包:Maven 浏览器:谷歌浏览器。 项目均可完美运行 基于Java的云平台信息安全攻防实训平台提供了以下核心功能: 1. **实训课程与项目**:平台提供了丰富多样的实训课程和项目,覆盖网络安全基础知识、漏洞挖掘与利用、渗透测试技术、安全防护策略等多个领域。 2. **在线学习模块**:学员可以通过在线学习模块观看教学视频、阅读文档资料,系统地学习信息安全知识。 3. **虚拟实验室环境**:平台提供虚拟实验室环境,学员可以在模拟的真实网络场景中进行攻防演练,包括漏洞扫描、攻击测试和防御措施的学习。 4. **教学管理功能**:教师可以创建和管理课程内容,制定教学计划,布置实训作业和考试任务。 5. **监控和统计功能**:教师可以实时了解学员的学习进度、实践操作情况和考试成绩,进行有针对性的指导和辅导。 6. **平台管理功能**:管理员负责用户管理、资源分配、系统安全维护等,确保平台稳定运行和实训环境的安全性。 7. **实时监控和评估**:系统具备实时监控和评估功能,能够及时反馈学生的操作情况和学习效果。 8. **用户认证和授权机制**:平台采用了严格的用户认证和授权机制,确保数据的安全性和保密性。 这些功能共同构建了一个功能丰富、操作便捷的实训环境,旨在提升学员的信息安全技能,为信息安全领域的发展输送专业人才。

    基于GrampusFramework的轻量级单体RBAC权限管理系统.zip

    基于GrampusFramework的轻量级单体RBAC权限管理系统

    软考(中级-软件设计师)知识点汇总与解析

    内容概要:本文档全面整理了软考(中级-软件设计师)的关键知识点,涵盖了计算复杂度、网络协议、数据结构、编程语言、数据库理论、软件测试、编译原理、设计模式、安全协议等多个方面的内容。具体涉及环路复杂度计算、SSH协议、数据字典与数据流图、对象的状态与数字签名、编程语言分类、海明码、著作权法、物理层与数据链路层设备、归纳法与演绎法、模块间耦合、能力成熟度模型集成、配置管理与风险管理、数据库关系范式、内存技术、计算机网络端口、路由协议、排序算法、中间代码、软件测试类型、编译器各阶段任务、设计模式、耦合与内聚、计算机病毒种类等。 适用人群:备考软考(中级-软件设计师)的技术人员,尤其是有一定工作经验但希望进一步提升自身技能和知识的IT从业人员。 使用场景及目标:帮助考生系统梳理考试重点,理解和掌握软件设计师应具备的专业知识和技术。适合考前复习和巩固基础知识。文档还可以作为参考资料,用于日常工作中遇到相关问题时查阅。 其他说明:本文档不仅提供了丰富的知识点,还附带了一些关键术语的定义和详细的解释,确保读者能够全面理解相关内容。建议在复习过程中结合实际案例进行练习,加深理解。

    数学建模学习资料 神经网络算法 Hopfield网络 共58页.pptx

    数学建模学习资料 神经网络算法 Hopfield网络 共58页.pptx

    工作寻(JobHunter)是一款招聘信息整合的网站,目前固定的模板有拉勾网,中华英才网,前程无忧。工作寻可以在线通过关.zip

    工作寻(JobHunter)是一款招聘信息整合的网站,目前固定的模板有拉勾网,中华英才网,前程无忧。工作寻可以在线通过关

    毕设源码-基于python协同过滤的音乐推荐系统的设计与实现_joqt--论文-期末大作业+说明文档.rar

    本项目是基于Python实现的协同过滤音乐推荐系统,旨在为计算机相关专业学生提供一个完整的毕设实战案例。项目以协同过滤算法为核心,通过分析用户历史行为数据,为用户推荐符合其兴趣偏好的音乐。 主要功能包括用户兴趣建模、音乐推荐生成以及用户反馈机制。系统能够实时捕捉用户听歌行为,动态更新用户兴趣模型,从而更精准地推送个性化音乐推荐。同时,系统设计了友好的用户界面,使用户能够方便地获取推荐音乐,并通过反馈机制不断完善推荐算法。 在技术框架方面,项目采用了Python编程语言,借助scikit-learn等机器学习库实现协同过滤算法,并结合Flask框架搭建了Web服务,确保了系统的性能和稳定性。此项目的开发,不仅能够帮助学生深入理解协同过滤算法及音乐推荐系统的工作原理,还能提升其软件开发和项目管理能力。

    微型餐饮补正备案材料通知书.docx

    微型餐饮补正备案材料通知书.docx

    食品生产许可质量跟踪监督建议书.docx

    食品生产许可质量跟踪监督建议书.docx

    基于django的音乐推荐系统.zip

    基于django的音乐推荐系统.zip

    如果让某人推荐Python技术书,请让他看这个列表.zip

    如果让某人推荐Python技术书,请让他看这个列表很棒的 Python 书籍如果让某人推荐Python技术书,请让他看这个列表前言好的技术书籍可以帮助我们快速成长,大部分人新生儿或者少部分受益于经典的技术书籍。在「Python开发者」微信公号后台,我们经常能收到帮忙推荐书籍的消息。此类问题在@Python开发者微博和伯乐在线的Python小组讨论中也绝非耳熟能详。 7月3日,伯乐在线在「Python开发者」微信公号发起了一个讨论(注PC端无法看到大家的评论,需要关注微信公号后,从微信公号才可以看到),通过这个讨论话题,在评论中分享对自己有帮助的大量Python技术书籍。 (Python开发者)入门《Head First Python》+入门级+微信49票+豆瓣评分9.5推荐语**66**浅显易懂,编排的顺序特别,有大量插图、对话,感觉枯燥古心通熟易懂,大量の图片,不会觉得枯燥,是一本不错的入门书《集体智慧编程》+入门级+微信123票+豆瓣评分 9.0推荐语**Mèrçurý**以实例具体的方式来展示Python的编程技巧,受益良多《Py

Global site tag (gtag.js) - Google Analytics