|
如我们在前面提到的,Java具有用来创建服务器应用程序的不同的套接字类。
ServerSocket类用来创建服务器,服务器监听本地或远程客户程序通过公共端口的连接。既然Web驱动着Internet中的大部分活动,本节就开发一个可运行的Web服务器(http)。
ServerSocket与通常的Sockets类完全不同。当创建一个ServerSocket类,它在系统注册自己对客户连接感兴趣。ServerSocket的构造函数反映了希望接受连接的端口号及你希望排队等待上述端口的时间(该项可选)。队列长度告诉系统多少与之连接的客户在系统拒绝连接之前可以挂起。队列的默认长度是50。构造函数在不利情况下可以引发IOException异常。下面是构造函数:
ServerSocket(int port) 在指定端口创建队列长度为50的服务器套接字。
ServerSocket(int port, int maxQueue) 在指定端口创建一个最大队列长度为maxQueue的服务器套接字。
ServerSocket(int port, intmaxQueue,
InetAddress localAddress)
在指定端口创建一个最大队列长度为maxQueue的服务器套接字。在一个多地址主机上,localAddress指定该套接字约束的IP地址。
ServerSocket有一个额外的accept()方法,该方法是一个等待客户开始通信的模块化调用,然后以一个用来与客户通信的常规Socket返回。
|
本节的剩余部分,我们讲述一个简单的缓存代理HTTP服务器,名为http,来演示客户与服务器套接字。http只支持GET操作及硬编码的MIME类型的一小部分(MIME类型是多媒体内容的类型描述符) 。 代理HTTP服务器是单线程的,该线程中每一个请求依次被处理,其他请求等待。这是缓存的相当天真的策略——它在RAM永久保存所有信息。http作为一个代理服务器时,它还拷贝每一个它获取的文件到本地缓存中。对于本地缓存,它没有用于刷新和无用单元回收的策略。除此之外,http代表了客户和服务器套接字的一个多产的例子,它是值得探究和容易扩展的。
|
HTTP服务器是通过5个类和一个接口实现的。更完善的实现方案可能在主类httpd外分裂很多的方法,以使组成结构更抽象。考虑本书的容量,多数功能是在单个类中实现的,小的支持类仅作为数据结构。我们仔细学习每一个类和方法来了解该服务器怎样工作,由支持类开始,终止于主程序。
MimeHeader.java MIME是通过电子邮件系统传达多媒体内容的一个Internet标准。该标准是由Nat Borenstein在1992年创建的。HTTP协议运用并扩展了MIME标头的概念,在HTTP客户和服务器之间传输常规的属性/值对。
构造函数 该类是Hashtable 的一个子类,所以它能方便的存储和检索与MIME标头有关的关键字/值对。它有两个构造函数。一个创建一个不含关键字的空的MimeHeader。另一个以一个格式化的字符串作为MIME标头,然后把它解析为对象的初始内容。参看下面的parse( )。
parse( )方法用来获取一个原始MIME格式的字符串, 并使它的关键字/值对进入一个给定的MimeHeader实例。 它用StringTokenizer 把输入数据分解成独立的由CRLF(\r\n)序列标记的行。然后用规范的while...hasMoreTokens( ) ... nextToken( ) 序列遍历每一行。
对于MIME标头的每一行,parse()通过冒号(:)把该行分解成两个字符串。两个变量key和val由substring( )方法设置,用来提取冒号前后的字符及后面的空隔。当两个字符串被提取后,使用put( )方法存储Hashtable中的关键字和值之间的关联。 toString( ) toString( )方法(用于String串联操作,+)只是parse()的反方法。它获取当前存储在MimeHeader 中的关键字/值对,返回一个MIME格式的字符串描述,然后打印关键字,跟着是冒号和空隔,然后是值,最后是CRLF。
put( ), get( ), AND fix( )如果不是特殊的任务,Hashtable中的put( )和get( )方法将运行良好。MIME规范定义了几个重要的关键字例如Content-Type和Content-Length。一些早期的MIME系统设备,特别是网络浏览器,对这些成员的大小写是自由的。一些用Content-type,另一些用content-type。为避免灾祸,我们的HTTP服务器努力将所有的输入和输出的MimeHeader关键字转换成规范形式Content-Type。因此,我们在它们进入Hashtable和寻找给定关键字之前用fix()方法重载put( )和get(),转变值的大小写。
代码 下面是MimeHeader的源代码:
import java.util.*; class MimeHeader extends Hashtable { voidparse(String data) { StringTokenizer st = new StringTokenizer(data, "\r\n"); while(st.hasMoreTokens()) { Strings = st.nextToken(); intcolon = s.indexOf(':'); Stringkey = s.substring(0, colon); Stringval = s.substring(colon + 2); // skip ": " put(key, val); } } MimeHeader() {} MimeHeader(String d) { parse(d); } publicString toString() { Stringret = ""; Enumeration e = keys(); while(e.hasMoreElements()) { Stringkey = (String) e.nextElement(); Stringval = (String) get(key); ret += key + ":" + val + "\r\n"; } returnret; } // Thissimple function converts a mime string from // anyvariant of capitalization to a canonical form. // Forexample: CONTENT-TYPE or content-type to Content-Type, // orContent-length or CoNTeNT-LENgth to Content-Length. privateString fix(String ms) { charchars[] = ms.toLowerCase().toCharArray(); booleanupcaseNext = true; for (inti = 0; i < chars.length - 1; i++) { char ch= chars[i]; if(upcaseNext && 'a' <= ch && ch <= 'z') { chars[i] = (char) (ch - ('a' - 'A')); } upcaseNext = ch == '-'; } returnnew String(chars); } publicString get(String key) { return(String) super.get(fix(key)); } public voidput(String key, String val) { super.put(fix(key), val); } }
HttpResponse.java
HttpResponse类是所有与HTTP服务器应答有关的事物的包装程序。它被httpd类的代理部分使用。当你向一个HTTP服务器发送一个请求时,它以一个存储在statusCode中的整数形式的代码以及一个存储在reasonPhrase中的文本应答(这些变量名在正式的HTTP规范中规定)。这个单行的响应后面跟随着一个包含进一步应答信息的MIME头。我们用以前解释过的MimeHeader对象来解析这个字符串。MimeHeader对象存储在HttpResponse类的mh变量中。这些变量不是私有的,所以httpd可以直接使用它们。
构造函数
如果用一个字符串参数创建一个HttpResponse类对象,它被用来作为一个HTTP服务器的原始响应,并传向下面描述的parse( )来初始化对象。你还可以传入一个预计算的状态码,原因语句以及MIME标头。
parse( )
parse( )方法获得从HTTP服务器上读取的原始数据,从第一行解析出statusCode 和reasonPhrase ,然后在剩下的行外部创建一个MimeHeader 。
toString( )
toString( )方法是parse( )的逆方法。它获取HttpResponse对象的当前值并返回一个字符串,HTTP客户希望从服务器读回该字符串。
代码 下面是HttpResponse 的源代码:
import java.io.*; /* *HttpResponse * Parse areturn message and MIME header from a server. * HTTP/1.0302 Found = redirection, check Location for where. * HTTP/1.0200 OK = file data comes after mime header. */ class HttpResponse { intstatusCode; // Status-Code in spec StringreasonPhrase; // Reason-Phrase in spec MimeHeadermh; staticString CRLF = "\r\n"; voidparse(String request) { int fsp =request.indexOf(' '); int nsp =request.indexOf(' ', fsp+1); int eol =request.indexOf('\n'); Stringprotocol = request.substring(0, fsp); statusCode = Integer.parseInt(request.substring(fsp+1, nsp)); reasonPhrase = request.substring(nsp+1, eol); Stringraw_mime_header = request.substring(eol + 1); mh = newMimeHeader(raw_mime_header); } HttpResponse(String request) { parse(request); } HttpResponse(int code, String reason, MimeHeader m) { statusCode = code; reasonPhrase = reason; mh = m; } publicString toString() { return"HTTP/1.0 " + statusCode + " " + reasonPhrase + CRLF + mh +CRLF; } }
UrlCacheEntry.java
为在服务器上保存文档的内容,必须在用于找回文档的URL和文档自身描述之间建立联系。一个文档由它的MimeHeader和原始数据描述。 例如一副图像可以被一个Content-Type:image/gif样式的MimeHeader描述,而原始图像数据就是一个字节数组。同样,一个网页在它的MimeHeader中有Content-Type:text/html关键字/值对,而原始数据就是HTML页的内容。
再次申明,实例变量不是私有的,所以httpd可以自由的访问它们。
构造函数 UrlCacheEntry
对象的构造函数需要用URL作为关键字以及一个与之相关的MimeHeader。 如果MimeHeader内部有一个名为Content-Length成员 (大多数情况下如此),数据区域被预先分配足够大的空间来保存它的内容。
append( )
append( ) 方法用来给UrlCacheEntry对象增添数据的。它不是一个简单的setData( )方法,原因是数据可能流经网络且需要在一定时间被存储成块。append()方法处理三种情形。第一种,数据缓冲区根本没有分配。第二种情形,数据缓冲区对于引入的数据来说太小,所以它被重新分配。最后一种情况,引入的数据正好可以插入缓冲区。在任何时候,length成员变量保存数据缓冲区当前的有效大小值。
代码 下面是UrlCacheEntry的源代码:
class UrlCacheEntry { String url; MimeHeadermh; bytedata[]; int length= 0; publicUrlCacheEntry(String u, MimeHeader m) { url = u; mh = m; String cl= mh.get("Content-Length"); if (cl !=null) { data =new byte[Integer.parseInt(cl)]; } } voidappend(byte d[], int n) { if (data== null) { data =new byte[n]; System.arraycopy(d, 0, data, 0, n); length= n; } else if(length + n > data.length) { byteold[] = data; data =new byte[old.length + n]; System.arraycopy(old, 0, data, 0, old.length); System.arraycopy(d, 0, data, old.length, n); } else { System.arraycopy(d, 0, data, length, n); length+= n; } } }
LogMessage.java
LogMessage是一个简单的接口,它只定义了一个方法log( ),该方法只有一个String型参数。它用来抽象从httpd获得消息的输出。在应用程序条件下,该方法用来打印标准应用程序起始处控制台的输出。在小应用程序情况下,数据被送到一个视窗文本缓冲区。
代码 下面是LogMessage的源程序:
interface LogMessage { public voidlog(String msg); }
httpd.java
这真是一个具有很多功能的大类。我们将一个方法一个方法的讲解它。
构造函数 存在5个主要的实例变量:port,docRoot,log,cache和stopFlag,它们都是私有的。其中的三个可以由httpd的独立构造函数设置,显示如下:
httpd(int p, String dr, LogMessage lm)
它初始化监听端口,初始化检索文件的目录以及初始化发送消息的接口。
第四个实例变量cache,是在RAM中保存所有文件的Hashtable,它是在对象创建时被初始化的。stopFlag 控制程序的执行。
静态部分
该类中有几个重要的静态变量。MIME标头中的“Server”域报告的版本在变量version中被发现。接着定义了一些常量:HTML文件的MIME类型,mime_text_html;MIME的结束顺序,CRLF;代替原始目录请求返回的HTML文件名,indexfile以及在输入/输出中用到的数据缓冲区的大小, buffer_size。 然后mt 定义了一系列文件扩展名和这些文件相应的MIME 类型。types Hashtable在下一个块中被静态初始化,以用来包含作为可选关键字和值的数组mt。接着可以用fnameToMimeType( )方法来返回传入的每个filename的合适的MIME类型。如果filename 不含mt 表中的任何一个扩展名,该方法返回defaultExt或“text/plain.”。
统计计算器
下面,我们声明另外5个实例变量。它们是没有private 修饰符,所以一个外部监控器可以检查这些值并以图形形式显示它们(我们将在后面演示)。这些变量表示了我们的Web服务器所用的统计资料。点击数和提供的字节的原始数目被存储在hits_served 和bytes_served中。通常存储在高速缓存中的文件和字节数被存放在files_in_cache 和bytes_in_cache中。最后,我们把成功在高速缓存外部提供服务的点击数目存放在hits_to_cache中。
toBytes( )
接着,我们有一个方便的程序,toBytes( )。该程序把它的字符串转变成一个字节数组。这是十分必要的,因为Java的String 对象是以统一编码的字符形式存储的,而Internet 协议中的混合语例如HTTP是老式的8位ASCII码。
makeMimeHeader( )
MakeMimeHeader()方法是另一个方便的方法,它用来创建由一些关键字值填充的MimeHeader对象。 该方法返回的MimeHeader在Date成员中含有当前时间和时期,在Server成员中有服务器的名称和版本,Content-Type成员中有type参数,Content-Length成员中有length参数。
error( )
error( )方法用来格式化HTML页并返回提出不能完成请求的Web客户。第一个参数code,是返回的出错代码。一般它在400到499之间。我们的服务器返回404和405错误。它用HttpResponse 类和适当的MimeHeader来封装返回的代码。该方法返回字符串表示是与HTML页有关的响应。该页包括易于人读的错误代码信息msg 和导致错误的url请求。
getRawRequest( )
GetRawRequest( )方法是很简单的。它从流读取数据直到它获得两个连续的换行符。它忽略回车符号并且只寻找换行符。一旦它已经发现了连续的两个换行符,它使字节数组转向一个String对象并返回该对象。如果输入流在结束之前没有生成两个连续的换行符,它将返回null。这说明了HTTP服务器和客户的消息是怎样被格式化的。它们以状态的一行开始然后立即跟着一个MIME头。 MIME头的结尾被两个换行符从剩余的内容中分离。
logEntry( )
logEntry( )方法用来报告标准格式下的HTTP服务器的每个点击数。 该方法生成的格式也许看起来有一点奇怪,但是它和HTTP日志文件的当前标准相匹配。该方法有若干个用来格式化每个日志项的日期戳的辅助变量和方法。months数组用来把月份转换成字符串。当host变量接受一个给定主机的连接时它由主HTTP循环设置。fmt02d( )方法把0到9的整数格式化成两位的, 第一位为零的数, 然后结果字符串通过LogMessage接口变量log传输。
writeString( )
另一个方便的方法writeString( ),用来隐藏字符到字节数组的转变,以使它可以被写入流。
writeUCE( )
writeUCE( )方法占取一个OutputStream和一个UrlCacheEntry。它从高速缓存项提取信息,以便给网络客户传送消息。消息中包含适当的响应代码,MIME标头和内容。
serveFromCache( )
这个布尔方法试图在高速缓存中发现一个特殊的URL。如果成功,缓存项的内容被写给客户,hits_to_cache变量递增,调用者被返回true。否则,它只返回false。
loadFile( )
该方法占用了一个InputStream、与之相应的url以及该URL的MimeHeader。用存储在MimeHeader的信息创建一个新的UrlCacheEntry。输入流在buffer_size字节块中被读取并传入UrlCacheEntry。结果的UrlCacheEntry 存储在高速缓存中。files_in_cache和bytes_in_cache变量更新,UrlCacheEntry返回调用者。
readFile( )
readFile( )方法就loadFile( )方法来说看起来有些多余,实际不然。该方法严格的从本地文件系统读取文件。在本地文件系统中,loadFile( )方法用来与各种类型的流交流。 如果File对象f存在, 将会为它创建一个InputStream类。 文件的大小是决定了的, MIME类型来自文件名。当loadFile( )被调用来做实际的读取和缓存高速工作时,有两个变量用来创建合适的MimeHeader。
writeDiskCache( )
writeDiskCache( )方法占用一个UrlCacheEntry 对象并且把它持久的写入本地磁盘。它从URL中建立一个目录名,确保用与系统有关的separatorChar代替斜线(/)字符。然后它调用mkdirs( )方法来保证这个URL的本地磁盘路径存在。最后,它打开一个FileOutputStream,向它写入所有的数据然后关闭它。
handleProxy( )
HandleProxy()程序是该服务器的两个主要模式之一。基础思想是:如果你把你的浏览器设置成把该服务器当成代理服务器,则要传给它的请求将包括完整的URL,URL中常规GET方法可删除“http://”和主机名部分。我们仅把完整的URL拆碎,寻找“://”序列,其次是斜线(/),然后是使用非标准端口号的服务器的另一个冒号(:)。
一旦我们发现了这些字符,我们就可以知道已经所需要的主机和端口号以及我们需要获取的URL。然后我们可以试图从RAM高速缓存中转载一个先前保存过的文档版本。 如果失败,我们可以试图从文件系统装载它到RAM高速缓存并且再尝试从RAM高速缓存装载它。 如果此举失败,那么事情变得很有趣,因为我们必须从远程站点读取该文件。
为此,我们打开一个远程站点和端口的套接字。我们发送一个GET请求,要求传给我们的URL。无论我们从远程站点获得什么响应标头信息,我们把它传给客户。如果代码是200,对于成功的文件传输,我们还把确认数据流读到一个新的UrlCacheEntry类并且把它写入客户套接字。然后,我们调用writeDiskCache()来保存传输结果到本地磁盘。我们记录传输日志,关闭套接字,然后返回。
handleGet( )
当http后台进程像一个普通的Web服务器一样工作时,handleGet( )被调用。它有一个服务于文件的本地磁盘文件根目录。handleGet()的参数告诉它向何处写结果,何处访问URL以及何处请求网络浏览器的MimeHeader。这个MIME标头将包括用户代理字符串和其他有用的属性。开始,我们试图在RAM高速缓存外为URL提供服务。如果此举失败,为寻找URL,我们顺序访问文件系统。如果文件不存在或不可读,我们向Web客户报告一个错误。否则,我们就用readFile( )方法获得文件的内容并把它们输入到高速缓存。然后调用writeUCE()方法以用来传输文件内容到客户套接字。
doRequest( )
每次连接服务器时都会调用一次 doRequest( )方法。 它解析请求字符串和引入的MIME标头。 它在请求字符串中是否存在 “: //” 的基础上判定是调用handleProxy( ) 还是 handleGet( )方法。如果不用GET,而使用任何其他的方法例如HEAD或POST,该程序向客户返回一个405错误。注意如果stopFlag 是true时HTTP请求被忽略。