- 浏览: 265005 次
- 性别:
- 来自: 北京
文章分类
最新评论
-
DragonKiiiiiiiing:
支持楼主,中国互联网太缺这种无私奉献的人了。您的这本书我已拜读 ...
JAVA NIO 全书 译稿 -
sp42:
非常感谢!热部署帮助很大!
Pure JS (2): 热部署 (利用 JDK 7 NIO 监控文件变化) -
sp42:
其实在我的架构中,我更倾向于 JSP 作为前端模板系统~还是写 ...
Pure JS (5.3):pure.render 的实现(构造window对象,实现服务器端 JQuery Template) -
sp42:
非常不错,楼主做的正是鄙人想做的做的,而且比鄙人的来的成熟、健 ...
OMToolkit介绍(5): 总结 -
cfanllm:
楼主辛苦了,,,谢谢分享啊
JAVA NIO 全书 译稿
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.txtport参数用于指定监听的端口,而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 的功能是如何得到扩展的。
- OMServer_Step1.rar (3.8 KB)
- 下载次数: 2
- OMServer_Step2.rar (10.4 KB)
- 下载次数: 2
- OMServer_Step3.rar (12.6 KB)
- 下载次数: 2
- OMServer_Step4.rar (47.8 KB)
- 下载次数: 3
- resources.rar (25 KB)
- 下载次数: 2
- OMServer_Step5.rar (52 KB)
- 下载次数: 2
- OMServer_Step6.rar (57 KB)
- 下载次数: 2
- OMServer_Complete.rar (61.7 KB)
- 下载次数: 5
发表评论
-
OMToolkit介绍(5): 总结
2011-03-22 19:13 1733OMToolkit介绍(5): 总结 ( ... -
OMToolkit介绍(4) :Object-Oriented Database 实现
2011-03-22 07:26 1173OMToolkit介绍(4) :Object-Oriented ... -
OMToolkit介绍(3) :Web Framework 实现
2011-03-20 21:17 1567OMToolkit介绍(3) :Web Framework 实 ... -
OMTookit介绍(1) 简单示例:OMSimpleBlog
2011-03-17 07:50 1271OMTookit介绍(1) 简单示 ... -
OMToolkit: Web Server,Web Framework 及 Object-Orinted Database 的简单实现
2011-03-15 02:01 1599概述 最近在看一些NIO和concurrent的资 ...
相关推荐
OMToolkit 是一个集成 Web 服务器、Web 框架以及对象-关系数据库的工具包,旨在简化开发过程,提供一站式的解决方案。通过这个工具,开发者可以快速构建基于 Web 的应用程序,同时利用面向对象的数据库来存储和管理...
基于java的贝儿米幼儿教育管理系统答辩PPT.pptx
本压缩包资源说明,你现在往下拉可以看到压缩包内容目录 我是批量上传的基于SpringBoot+Vue的项目,所以描述都一样;有源码有数据库脚本,系统都是测试过可运行的,看文件名即可区分项目~ |Java|SpringBoot|Vue|前后端分离| 开发语言:Java 框架:SpringBoot,Vue JDK版本:JDK1.8 数据库:MySQL 5.7+(推荐5.7,8.0也可以) 数据库工具:Navicat 开发软件: idea/eclipse(推荐idea) Maven包:Maven3.3.9+ 系统环境:Windows/Mac
基于java的消防物资存储系统答辩PPT.pptx
项目经过测试均可完美运行! 环境说明: 开发语言:java jdk:jdk1.8 数据库:mysql 5.7+ 数据库工具:Navicat11+ 管理工具:maven 开发工具:idea/eclipse
项目经过测试均可完美运行! 环境说明: 开发语言:java jdk:jdk1.8 数据库:mysql 5.7+ 数据库工具:Navicat11+ 管理工具:maven 开发工具:idea/eclipse
TA_lib库(whl轮子),直接pip install安装即可,下载即用,非常方便,各个python版本对应的都有。 使用方法: 1、下载下来解压; 2、确保有python环境,命令行进入终端,cd到whl存放的目录,直接输入pip install TA_lib-xxxx.whl就可以安装,等待安装成功,即可使用! 优点:无需C++环境编译,下载即用,方便
使用软件自带的basic脚本编辑制作的脚本 低版本软件无法输出Excel报告,可以通过脚本方式实现这一功能
基于java的就业信息管理系统答辩PPT.pptx
25法理学背诵逻辑.apk.1g
基于java的大学生校园兼职系统答辩PPT.pptx
做到代码,和分析的源数据
本压缩包资源说明,你现在往下拉可以看到压缩包内容目录 我是批量上传的基于SpringBoot+Vue的项目,所以描述都一样;有源码有数据库脚本,系统都是测试过可运行的,看文件名即可区分项目~ |Java|SpringBoot|Vue|前后端分离| 开发语言:Java 框架:SpringBoot,Vue JDK版本:JDK1.8 数据库:MySQL 5.7+(推荐5.7,8.0也可以) 数据库工具:Navicat 开发软件: idea/eclipse(推荐idea) Maven包:Maven3.3.9+ 系统环境:Windows/Mac
项目经过测试均可完美运行! 环境说明: 开发语言:java jdk:jdk1.8 数据库:mysql 5.7+ 数据库工具:Navicat11+ 管理工具:maven 开发工具:idea/eclipse
适用于ensp已经入门人群的学习,有一定难度
基于java的数码论坛系统设计与实现答辩PPT.pptx
tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl
基于java的医院信管系统答辩PPT.pptx
项目经过测试均可完美运行! 环境说明: 开发语言:java jdk:jdk1.8 数据库:mysql 5.7+ 数据库工具:Navicat11+ 管理工具:maven 开发工具:idea/eclipse