OMToolkit介绍(2) :Web Server 实现

[align=center][size=large][b]OMToolkit介绍(2) :Web Server 实现[/b][/size][/align]
  本文将介绍OMToolkit中Web Server部分的实现,涉及的内容包括基于NIO的Server,配置文件的读取,Http 请求报文的分析,Session和Cookie的获取等。

[align=center][size=medium][b]1. 基于NIO的Server轮询[/b][/size][/align]
  首先,是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相似。

  现在可以运行程序了,在浏览器中输入[url]http://localhost[/url],浏览器将显示网页无法显示,但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并继续。


[align=center][size=medium][b]2. 配置文件的读取[/b][/size][/align]
  我们将实现自己的配置文件读取类。为什么不使用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()));

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

[align=center][size=medium][b]3.Accepter:接受Socket连接[/b][/size][/align]
  我们现在是直接在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。

[align=center][size=medium][b]4.Worker:读写和处理Web请求[/b][/size][/align]
  接下来我们将实现处理请求的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);
}


  现在可以在浏览器中输入[url]http://localhost[/url]进行测试了。浏览器将显示“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。

[align=center][size=medium][b]5.Request:Web请求报文的分析[/b][/size][/align]

  接下来,我们将使用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,解压后复制到项目中。
  运行程序,输入[url]http://localhost/resources/banner.jpg[/url],浏览器中将显示一张图片,效果如图:
[img]http://dl.iteye.com/upload/picture/pic/84365/5f6cb7cb-cf88-36d8-843b-a0d2371da604.png[/img]

[align=center][size=medium][b]6.Session的获取和设置[/b][/size][/align]
  接下来就是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()方法)。
  现在启动程序并在浏览器中输入[url]http://localhost[/url],将看到如下输出:

HTTP/1.1 200 OK
Set-Cookie: session=c62b456b-5107-4703-929a-2133dc7292d1;Path=/

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

[align=center][size=medium][b]7.Cookie的获取和设置[/b][/size][/align]
  最后是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();
}

  运行程序,输入[url]http://localhost[/url],将看到如下输出:
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 的功能是如何得到扩展的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值