文章目录
- 1. 单文件服务器
- 2. 重定向器Redirector
- 3. 功能完备的HTTP服务器
1. 单文件服务器
要研究HTTP服务器,先从一个简单的服务器开始,无论接受什么请求,这个服务器都始终发送同一个文件。这个单文件服务器名为SingleFileHTTPServer,如下:
public class QuizCardBuilder {
//获取Logger对象,用于记录日志
private static final Logger logger=Logger.getLogger("SingelFileHTTPServer");
//服务端发送给客户端的数据
private final byte[] content;
//请求头
private final byte[] header;
//端口
private final int port;
//编码方式
private final String encoding;
//构造函数
public QuizCardBuilder(String data, String encoding, String mimeType, int port) throws UnsupportedEncodingException {
//调用另一个构造函数
this(data.getBytes(encoding),encoding,mimeType,port);
}
public QuizCardBuilder(byte[] data, String encoding,String mimeType ,int port){
this.content=data;
this.port=port;
this.encoding=encoding;
String header="HTTP/1.0 200 Ok\r\n"
+"Server:OneFile 2.0\r\n"
+"Content-length:"+ this.content.length+ "\r\n"
+"Content-type:"+mimeType+"; charset="+encoding+"\r\n\r\n";
this.header=header.getBytes(Charset.forName("US-ASCII")) ;
}
public void start() {
ExecutorService pool=Executors.newFixedThreadPool(100);
try(ServerSocket serverSocket=new ServerSocket(this.port)){
logger.info("Accepting connections on port "+ serverSocket.getLocalPort());
logger.info("Data to be sent:");
logger.info(new String(this.content,encoding));
while (true){
try{
Socket connection=serverSocket.accept();
pool.submit(new HTTPHandler(connection));
}catch (IOException e){
logger.log(Level.WARNING,"Exception accepting connection",e);
}catch (RuntimeException e){
logger.log(Level.SEVERE,"Unexpected errors",e);
}
}
}catch (IOException ex){
logger.log(Level.SEVERE,"Could not start server"+ex);
}
}
private class HTTPHandler implements Callable<Void>{
private final Socket connection;
HTTPHandler(Socket connection){
this.connection=connection;
}
@Override
public Void call() throws Exception {
try{
//获得输出流
OutputStream out=new BufferedOutputStream(connection.getOutputStream());
//获得输入流
InputStream in=new BufferedInputStream(connection.getInputStream());
StringBuilder request=new StringBuilder(80);
while(true){
int c=in.read();
if(c=='\r' || c=='\n' || c==-1)break;
request.append((char) c);
}
//如果是HTTP/1.0以后的版本则发送一个MIME首部
if(request.toString().indexOf("HTTP/")!=-1){
out.write(header);
}
out.write(content);
out.flush();
}catch (IOException e){
logger.log(Level.WARNING,"Error writing to client ",e);
}finally {
connection.close();
}
return null;
}
}
public static void main(String[] args) {
String encoding="UTF-8";
try{
//以绝对路径获取文件
Path path= Paths.get("/Users/jackchai/Desktop/自学笔记/java项目/leetcode/leetcodetest/MyText.txt");
//将文件内容读成字节流
byte[] data= Files.readAllBytes(path);
String contentType= URLConnection.getFileNameMap().getContentTypeFor("/Users/jackchai/Desktop/自学笔记/java项目/leetcode/leetcodetest/MyText.txt");
QuizCardBuilder server=new QuizCardBuilder(data,encoding,contentType,8080);
server.start();
} catch (ArrayIndexOutOfBoundsException ex) {
System.out.println("Usage:java QuizCardBuilder filename port encoding");
}catch (IOException e){
logger.severe(e.getMessage());
}
}
}
下面是服务端日志
下面使用Telnet测试结果
2. 重定向器Redirector
重定向是特殊用途的HTTP服务器的另一个简单而有用的应用程序。他能从一个Web网站重定向到另一个网站。下面代码从指定的URL和端口,在这个端口打开一个服务器Socket,使用302 FOUND编码将接收的所有请求重定向到新URL表示的网站。
public class QuizCardBuilder {
//获取Logger对象,用于日志
private static final Logger logger=Logger.getLogger("Redirecotr");
//端口
private final int port;
//重定向的地址
private final String newSite;
//构造函数
public QuizCardBuilder(String newSite, int port){
this.port=port;
this.newSite=newSite;
}
public void start(){
try(ServerSocket serverSocket=new ServerSocket(port)){
logger.info("Redirecting connections on port "+ serverSocket.getLocalPort()+" to "+newSite);
while(true){
try {
//打开Socket开始等待
Socket s=serverSocket.accept();
//当接受到一个请求时,开启一个新的线程
Thread t=new RedirectThread(s);
//开启新线程
t.start();
}catch (IOException e){
logger.warning("Exception accepting connection");
}catch (RuntimeException ex){
logger.log(Level.SEVERE,"Unexpected error",ex);
}
}
}catch (BindException ex){
logger.log(Level.SEVERE,"Could not start server." +ex);
}catch (IOException ex){
logger.log(Level.SEVERE,"Error opening server socket ",ex);
}
}
private class RedirectThread extends Thread{
private final Socket connection;
RedirectThread(Socket s){
this.connection=s;
}
public void run(){
try{
//打开输出流
Writer out=new BufferedWriter(new OutputStreamWriter(connection.getOutputStream(),"US-ASCII"));
//打开输入流
Reader in=new InputStreamReader(new BufferedInputStream(connection.getInputStream()));
StringBuilder request=new StringBuilder(90);
//读取用户请求的第一行
while(true){
int c=in.read();
if(c=='\r' || c=='\n' || c==-1){
break;
}
request.append((char)c);
}
String get=request.toString();
String[] pieces =get.split("\\w*");
String theFile=pieces[1];
if(get.indexOf("HTTP")!=-1){
out.write("HTTP/1.0 302 FOUND\r\n");
Date now=new Date();
out.write("Date:"+now+"\r\n");
out.write("Server:Redirector 1.1\r\n");
out.write("Location:"+newSite+theFile+"\r\n");
out.write("Content-type: text/html\r\n\r\n");
out.flush();
}
//并不是所有的浏览器都支持重定向,所以我们需要生成HTML指出文档转移到哪里
out.write("<HTML><HEAD><TITLE>Document moved</TITLE></HEAD>\r\n");
out.write("<BODY><H1>Document moved</H1>\r\n");
out.write("The document"+theFile
+" has moved to \r\n <A HREF\""+newSite+theFile+"\">"
+newSite+theFile
+"</A>,\r\n Please update your bookmarks<p>");
out.write("</BODY><HTML>\r\n");
logger.log(Level.INFO,"Redirected "+connection.getRemoteSocketAddress());
}catch (IOException e){
logger.log(Level.WARNING,"Error talking to "+connection.getRemoteSocketAddress(),e);
}finally {
try{
connection.close();
} catch (IOException e) {
}
}
}
}
public static void main(String[] args) {
String theSite;
try{
theSite="www.baidu.com";
//删除末尾斜线
if(theSite.endsWith("/")){
theSite=theSite.substring(0,theSite.length()-1);
}
}catch (RuntimeException e){
System.out.println("Usage: java Redirector http://www.baidu.com/port");
return;
}
QuizCardBuilder redirector=new QuizCardBuilder(theSite,8080);
redirector.start();
}
}
允许代码,在浏览器中访问
http://localhost:8080
,就会发生重定向
3. 功能完备的HTTP服务器
上面介绍了单文件的HTTP服务器,它会接受用户请求,将本地的一个文件内容以流的形式返回给客户端。这里将开发一个功能完备的HTTP服务器,名为JHTTP,它可以提供一个完整的文档树,包括图像、applet、HTML文件、文本文件等。它与前面的单文件服务器非常相似,只不过它所关注的是GET请求。
下面是JHTTP web服务器
public class QuizCardBuilder {
//返回由 Java 语言规范定义的基础类的规范名称。如果基础类没有规范名称(即,如果它是本地或匿名类或其组件类型没有规范名称的数组),则返回 null。
private static final Logger logger=Logger.getLogger(QuizCardBuilder.class.getCanonicalName());
//线程数量
private static final int NUM_THREADS=50;
private static final String INDEX_FILE="index.html";
private final File rootDeirectory;
private final int port;
public QuizCardBuilder(File rootDeirectory, int port)throws IOException{
//如果rootDeirectory不是个文件目录则抛出异常
if(!rootDeirectory.isDirectory()){
throw new IOException(rootDeirectory+" does not exist as a directory");
}
this.rootDeirectory=rootDeirectory;
this.port=port;
}
public void start()throws IOException{
//定义线程池
ExecutorService pool= Executors.newFixedThreadPool(NUM_THREADS);
try(ServerSocket serverSocket=new ServerSocket(port)){
logger.info("Accepting connections on port "+serverSocket.getLocalPort());
logger.info("Document Root "+rootDeirectory);
while (true){
try{
//等待客户端请求
Socket socket=serverSocket.accept();
//创建一个线程,参数为文件的服务端文件系统的根目录,文件索引和服务器Socket
Runnable r=new RequestProcessors(rootDeirectory,INDEX_FILE,socket);
//提交线程任务
pool.submit(r);
}catch (IOException ex){
logger.log(Level.WARNING,"Error accepting connection",ex);
}
}
}
}
public static void main(String[] args) {
File docroot;
try{
docroot=new File("/Users/jackchai/Desktop/自学笔记/java项目/leetcode/leetcodetest");
}catch (ArrayIndexOutOfBoundsException ex){
System.out.println("Usage: java JHTTP docroot port");
return;
}
//设置要监听的端口
int port=8080;
try {
QuizCardBuilder Jhttp=new QuizCardBuilder(docroot,port);
Jhttp.start();
}catch (IOException e){
logger.log(Level.SEVERE,"Server could not start ",e );
}
}
}
class RequestProcessors implements Runnable{
//获取Logger对象,用于日志管理
private final static Logger logger=Logger.getLogger(RequestProcessor.class.getCanonicalName());
//文件系统的根目录
private File rootDirectory;
//文件索引
private String indexFilename="index.html";
//服务端Socket
private Socket connection;
public RequestProcessors(File rootDirectory,String indexFilename,Socket connection){ //如果文件系统的根目录是一个文件
if(rootDirectory.isFile()){
throw new IllegalArgumentException("rootDirectory must be a directory, not a file");
}
try {
//返回此抽象路径名的规范形式。等效于 new File(this.getCanonicalPath)。
rootDirectory=rootDirectory.getCanonicalFile();
}catch (IOException e){
}
this.rootDirectory=rootDirectory;
if(indexFilename!=null) this.indexFilename=indexFilename;
this.connection=connection;
}
@Override
public void run() {
//安全检查
String root=rootDirectory.getPath();
try {//获得输出流
OutputStream raw=new BufferedOutputStream(connection.getOutputStream());
Writer out=new OutputStreamWriter(raw);
//获得输入流
Reader in =new InputStreamReader(new BufferedInputStream(connection.getInputStream()),"US-ASCII");
//用于存储用户都请求头
StringBuilder requestLine=new StringBuilder();
while(true){
int c=in.read();
if(c=='\r'||c=='\n'||c==-1)break;
requestLine.append((char) c);
}
String get=requestLine.toString();
//获得客户端地址
logger.info(connection.getRemoteSocketAddress()+" "+get);
String[] tokens=get.split("\\s+");
//请求方法
String method=tokens[0];
//获得HTTP版本
String version="";
if(method.equals("GET")){
//用户请求的文件名
String filename=tokens[1];
if(filename.endsWith("/"))filename+=indexFilename;
//获得文件类型(这里肯定是text/html
String contentType= URLConnection.getFileNameMap().getContentTypeFor(filename);
if(tokens.length>2){
version=tokens[2];
}
File thefile=new File(rootDirectory,filename.substring(1,filename.length()));
if(thefile.canRead()
//不要让客户端超出文档根之外
&& thefile.getCanonicalPath().startsWith(root)){
byte[] thedata= Files.readAllBytes(thefile.toPath());
if(version.startsWith("HTTP/")){
sendHeader(out,"HTTP/1.0 200 OK",contentType,thedata.length);
}
//发送文件,这可能是一个图像或其他二进制数据
//所以要用底层流输出
//而不是writer
raw.write(thedata);
raw.flush();
}else {
String body=new StringBuilder("<HTML>\r\n")
.append("<HEAD><TITLE>FIle Not Found</TITLe>\r\n")
.append("</HEAD>\r\n")
.append("<BODY>\r\n")
.append("<H1>HTTP Error 404: File Not Found <H1>\r\n")
.append("<BODY></HTML>\r\n").toString();
if(version.startsWith("HTTP/")){
sendHeader(out,"HTTP/1.0 404 File Not Found","text/html; charset=utf-8",body.length());
}
out.write(body);
out.flush();
}
}else {//方法不等于GET
String body = new StringBuilder("<HTML>\r\n")
.append("<HEAD><TITLE>Not Implemented</TITLe>\r\n")
.append("</HEAD>\r\n")
.append("<BODY>\r\n")
.append("<H1>HTTP Error 501: Not Implemented <H1>\r\n")
.append("<BODY></HTML>\r\n").toString();
if(version.startsWith("HTTP/")){
sendHeader(out,"HTTP/1.0 501 Not Implemented ","text/html; charset=utf-8",body.length());
}
out.write(body);
out.flush();
}
}catch (IOException ex){
logger.log(Level.WARNING,"Error talking to "+connection.getRemoteSocketAddress(),ex);
}finally {
try {
connection.close();
}catch (IOException e){}
}
}
private void sendHeader(Writer out,String responseCode,String contentType,int length) throws IOException {
out.write(responseCode+"\r\n");
Date now=new Date();
out.write("Date: "+now+"\r\n");
out.write("Server:"+length+"\r\n");
out.write("Content-length:"+length+"\r\n");
out.write("Content-type:"+contentType+"\r\n\r\n");
out.flush();
}
}
运行成功获取文件
也可以获取图片
上面这个服务器提供了我们获取根目录下的文件,但还是十分简单,如果真的希望使用JHTTP来运行一个高流量网站,还可以做一些事情来加速这个服务器,最主要的是实现智能缓冲,跟踪记录接收到的请求,将最频繁请求的文件中的数据存储在一个Map中,使它们保存在内存中,使用一个低优先级的线程更新这个缓存。还可以使用非阻塞I/O和通道来代替线程和流。