用Java编写你自己的简单HTTP服务器
来源:http://blog.csdn.net/yanghua_kobe/article/details/7296156
HTTP是个大协议,完整功能的HTTP服务器必须响应资源请求,将URL转换为本地系统的资源名。响应各种形式的HTTP请求(GET、POST等)。处理不存在的文件请求,返回各种形式的状态码,解析MIME类型等。但许多特定功能的HTTP服务器并不需要所有这些功能。例如,很多网站只是想显示“建设中“的消息。很显然,Apache对于这样的网站是大材小用了。这样的网站完全可以使用只做一件事情的定制服务器。Java网络类库使得编写这样的单任务服务器轻而易举。 定制服务器不只是用于小网站。大流量的网站如Yahoo,也使用定制服务器,因为与一般用途的服务器相比,只做一件事情的服务器通常要快得多。针对某项任务来优化特殊用途的服务器很容易;其结果往往比需要响应很多种请求的一般用途服务器高效得多。例如,对于重复用于多页面或大流量页面中的图标和图片,用一个单独的服务器处理会更好(并且还可以避免在请求时携带不必要的Cookie,因而可以减少请求/响应数据,从而减少下载带宽,提升速度);这个服务器在启动时把所有图片文件读入内存,从RAM中直接提供这些文件,而不是每次请求都从磁盘上读取。此外,如果你不想在包含这些图片的页面请求之外单独记录这些图片,这个单独服务器则会避免在日志记录上浪费时间。
1 import java.io.File; 2 import java.io.IOException; 3 import java.net.ServerSocket; 4 import java.net.Socket; 5 6 /** 7 * @Title: JHTTP.java 8 * @Package 9 * @author 任伟 10 * @date 2014-12-4 下午1:30:07 11 * @version V1.0 12 */ 13 14 /** 15 * @ClassName: JHTTP 16 * @Description: JHTTP线程反复地接受入站连接,并将其放在RequestProcessor池中 17 * @author 任伟 18 * @date 2014-12-4 下午1:30:07 19 */ 20 public class JHTTP extends Thread { 21 private File documentRootDirectory; //文档根目录 22 private String indexFileName = "index.html"; //引导文件 23 private ServerSocket server; //Server 24 private int numThreads = 50; //线程数量 25 26 public JHTTP(File documentRootDirectory, int port, String indexFileName) 27 throws IOException { 28 if (!documentRootDirectory.isDirectory()) { 29 throw new IOException(documentRootDirectory 30 + " does not exist as a directory "); 31 } 32 this.documentRootDirectory = documentRootDirectory; 33 this.indexFileName = indexFileName; 34 this.server = new ServerSocket(port); 35 } 36 37 private JHTTP(File documentRootDirectory, int port) throws IOException { 38 this(documentRootDirectory, port, "index.html"); 39 } 40 41 public void run() { 42 //新建numThreads个请求处理线程,并开启线程 43 for(int i=0; i<numThreads; i++){ 44 Thread t = new Thread(new RequestProcessor(documentRootDirectory, indexFileName)); 45 t.start(); 46 } 47 System.out.println("Accepting connection on port "+server.getLocalPort()); 48 System.out.println("Document Root: "+documentRootDirectory); 49 50 //无限循环接受请求,收到一个请求将其放入RequestProcessor的请求池 51 while(true){ 52 try{ 53 Socket request=server.accept(); 54 RequestProcessor.processRequest(request); 55 }catch (Exception e) { 56 // TODO: handle exception 57 } 58 } 59 } 60 61 /** 62 * @param args 63 */ 64 public static void main(String[] args) { 65 //设置文档根目录 66 File docroot; 67 try { 68 docroot = new File(args[0]); 69 } catch (ArrayIndexOutOfBoundsException e) { 70 System.out.println("Usage: java JHTTP docroot port indexfile"); 71 return; 72 } 73 74 //读取端口号 75 int port; 76 try { 77 port = Integer.parseInt(args[1]); 78 if (port < 0 || port > 65535) { 79 port = 80; 80 } 81 } catch (Exception e) { 82 port = 80; 83 } 84 85 //构造一个新的JHTTP线程并启动 86 try { 87 JHTTP webserver = new JHTTP(docroot, port); 88 webserver.start(); 89 } catch (IOException e) { 90 System.out.println("Server could not start because of an " 91 + e.getClass()); 92 System.out.println(e); 93 } 94 95 } 96 97 }
1 import java.io.BufferedInputStream; 2 import java.io.BufferedOutputStream; 3 import java.io.DataInputStream; 4 import java.io.File; 5 import java.io.FileInputStream; 6 import java.io.IOException; 7 import java.io.InputStreamReader; 8 import java.io.OutputStream; 9 import java.io.OutputStreamWriter; 10 import java.io.Reader; 11 import java.io.Writer; 12 import java.net.Socket; 13 import java.util.Date; 14 import java.util.LinkedList; 15 import java.util.List; 16 import java.util.StringTokenizer; 17 18 /** 19 * @Title: RequestProcessor.java 20 * @Package 21 * @author 任伟 22 * @date 2014-12-4 下午1:42:22 23 * @version V1.0 24 */ 25 26 /** 27 * @ClassName: RequestProcessor 28 * @Description: 请求处理程序 29 * @author 任伟 30 * @date 2014-12-4 下午1:42:22 31 */ 32 public class RequestProcessor implements Runnable { 33 private static List pool = new LinkedList(); 34 private File documentRootDirectory; 35 private String indexFileName = "index.html"; 36 37 // 构造方法 38 public RequestProcessor(File documentRootDirectory, String indexFileName) { 39 if (documentRootDirectory.isFile()) { 40 throw new IllegalArgumentException(); 41 } 42 this.documentRootDirectory = documentRootDirectory; 43 try { 44 this.documentRootDirectory = documentRootDirectory 45 .getCanonicalFile(); 46 } catch (IOException e) { 47 } 48 49 if (indexFileName != null) { 50 this.indexFileName = indexFileName; 51 } 52 } 53 54 // 向请求池加入请求 55 public static void processRequest(Socket request) { 56 synchronized (pool) { 57 pool.add(pool.size(), request); 58 pool.notifyAll(); 59 } 60 } 61 62 /* 63 * (non-Javadoc) 64 * 65 * @see java.lang.Runnable#run() 66 */ 67 @Override 68 public void run() { 69 // 无限循环处理 70 while (true) { 71 72 // 先进性安全性检测,然后从连接池获取一个链接 73 Socket connection; 74 synchronized (pool) { 75 while (pool.isEmpty()) { 76 try { 77 pool.wait(); 78 } catch (Exception e) { 79 } 80 } 81 connection = (Socket) pool.remove(0); 82 } 83 84 // 开始处理 85 try { 86 // 获得输入输出流 87 OutputStream raw = new BufferedOutputStream( 88 connection.getOutputStream()); 89 Writer out = new OutputStreamWriter(raw); 90 Reader in = new InputStreamReader(new BufferedInputStream( 91 connection.getInputStream())); 92 93 // 拼接请求字符串 94 StringBuffer request = new StringBuffer(80); 95 while (true) { 96 int c = in.read(); 97 if (c == '\t' || c == '\n' || c == -1) { 98 break; 99 } 100 request.append((char) c); 101 } 102 103 // 记录日志 eg: 104 // localhost:port/a/b/c/index.html 105 // GET /a/b/c/index.html HTTP/1.1 106 String get = request.toString(); 107 System.out.println(get); 108 109 // 分析请求 110 StringTokenizer st = new StringTokenizer(get); 111 String method = st.nextToken();// 请求的方法 GET 112 String fileName;// 请求的文件名 113 String version = "";// 协议版本 114 String contentType;// 相应返回的内容类型 115 116 if (method.equals("GET")) {// 方法是“GET” 117 fileName = st.nextToken(); 118 if (fileName.endsWith("/")) { 119 fileName += indexFileName; 120 } 121 contentType = guessContentTypeFromName(fileName); 122 if (st.hasMoreTokens()) { 123 version = st.nextToken(); 124 } 125 126 // 根据文件目录读出文件,并返回响应 127 File theFile = new File(documentRootDirectory, 128 fileName.substring(1, fileName.length())); 129 String root = documentRootDirectory.getPath(); 130 if (theFile.canRead() 131 && theFile.getCanonicalPath().startsWith(root)) {// 读取文件成功 132 DataInputStream fis = new DataInputStream( 133 new BufferedInputStream(new FileInputStream( 134 theFile))); 135 byte[] theData = new byte[(int) theFile.length()]; 136 fis.readFully(theData); 137 fis.close(); 138 139 // HTTP请求,返回响应头 140 if (version.startsWith("HTTP")) { 141 out.write("HTTP/1.0 200 OK\r\n"); 142 Date now = new Date(); 143 out.write("Date: " + now + "\r\n"); 144 out.write("Server: JHTTP 1.0\r\n"); 145 out.write("Content-length: " + theData.length 146 + "\r\n"); 147 out.write("Content-Type: " + contentType 148 + "\r\n\r\n"); 149 out.flush(); 150 } 151 raw.write(theData); 152 raw.flush(); 153 154 } else {// 读取文件不成功 155 if (version.startsWith("HTTP")) { // 是HTTP请求返回 响应头 404 156 out.write("HTTP/1.0 404 File Not Found\r\n"); 157 Date now = new Date(); 158 out.write("Date: " + now + "\r\n"); 159 out.write("Server: JHTTP 1.0\r\n"); 160 out.write("Content-Type: text/html\r\n\r\n"); 161 } 162 out.write("<HTML>\r\n"); 163 out.write("<HEAD><TITLE>File Not Found</TITLE></HRAD>\r\n"); 164 out.write("<BODY>\r\n"); 165 out.write("<H1>HTTP Error 404: File Not Found</H1>"); 166 out.write("</BODY></HTML>\r\n"); 167 out.flush(); 168 } 169 } else {// 方法不是“GET” 170 if (version.startsWith("HTTP")) { 171 out.write("HTTP/1.0 501 Not Implemented\r\n"); 172 Date now = new Date(); 173 out.write("Date: " + now + "\r\n"); 174 out.write("Server: JHTTP 1.0\r\n"); 175 out.write("Content-Type: text/html\r\n\r\n"); 176 } 177 out.write("<HTML>\r\n"); 178 out.write("<HEAD><TITLE>Not Implemented</TITLE></HRAD>\r\n"); 179 out.write("<BODY>\r\n"); 180 out.write("<H1>HTTP Error 501: Not Implemented</H1>"); 181 out.write("</BODY></HTML>\r\n"); 182 out.flush(); 183 } 184 185 } catch (Exception e) { 186 // TODO: handle exception 187 } finally { 188 try { 189 connection.close(); 190 } catch (IOException e) { 191 e.printStackTrace(); 192 } 193 } 194 } 195 196 } 197 198 // 根据文件名字猜测返回的文件的内容类型 199 public static String guessContentTypeFromName(String name) { 200 if (name.endsWith(".html") || name.endsWith(".htm")) { 201 return "text/html"; 202 } else if (name.endsWith(".txt") || name.endsWith(".java")) { 203 return "text/plain"; 204 } else if (name.endsWith(".class")) { 205 return "application/octet-stream"; 206 } else if (name.endsWith(".gif")) { 207 return "image/gif"; 208 } else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) { 209 return "image/jpeg"; 210 } else if (name.endsWith(".png")) { 211 return "image/png"; 212 } else { 213 return "text/plain"; 214 } 215 } 216 217 }
测试结果:
图1
JHTTP类的main()方法根据args[0]设置文档的根目录。端口从args[1]读取,或者使用默认的80.然后构造一个新的JHTTP线程并启动。此JHTTP线程生成50个RequestProcessor线程处理请求,每个线程在可用时从RequestProcessor池获取入站连接请求。JHTTP线程反复地接受入站连接,并将其放在RequestProcessor池中。每个连接由下例所示的RequestProcessor类的run()方法处理。此方法将一直等待,直到从池中得到一个Socket。一旦得到Socket,就获取输入和输出流,并链接到阅读器和书写器。接着的处理,除了多出文档目录、路径的处理,其他的同单文件服务器。
最后,花点时间考虑一下可以采用什么方法来优化此服务器。如果真的希望使用JHTTP运行高流量的网站,还可以做一些事情来加速此服务器。第一点也是最重要的一点就是使用即时编译器(JIT),如HotSpot。JIT可以将程序的性能提升大约一个数量级。第二件事就是实现智能缓存。记住接受的请求,将最频繁的请求文件的数据存储在Hashtable中,使之保存在内存中。使用低优先级的线程更新此缓存。