在上一篇《JAVA简易WEB服务器(二)》中我们完成了对浏览器请求的解析,这一篇我们继续来实现响应浏览器的请求,同样的,我们还是先来看一下服务端响应给浏览器的数据格式
HTTP/1.1 200 OK Server: Apache-Coyote/1.1 Accept-Ranges: bytes ETag: W/"129-1456125361109" Last-Modified: Mon, 22 Feb 2016 07:16:01 GMT Content-Type: text/html Content-Length: 129 Date: Mon, 22 Feb 2016 08:08:32 GMT <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>test</title> </head> <body>this is test page. </body> </html> 123456789101112131415161718 123456789101112131415161718只要我们响应的数据满足这个格式,浏览器就可以正常解析了,在解析之前还是先来做一些准备工作。 相应请求时可能会出现异常,所以我们先编写一个异常类,和请求的异常类类似。
package com.gujin.server; /** * 响应异常 * * @author jianggujin * */ public class HQResponseException extends RuntimeException { private static final long serialVersionUID = 1L; public HQResponseException() { super(); } public HQResponseException(String message) { super(message); } public HQResponseException(String message, Throwable cause) { super(message, cause); } public HQResponseException(Throwable cause) { super(cause); } } 1234567891011121314151617181920212223242526272829303132 1234567891011121314151617181920212223242526272829303132不管是请求还是响应都需要对字符串进行操作,所以将字符串操作的方法抽取出来形成一个字符串工具类。
package com.gujin.server.utils; /** * 字符串工具 * * @author jianggujin * */ public class HQString { /** * 判断字符串为空 * * @param s * @return */ public static boolean isEmpty(String s) { return s == null || s.length() == 0; } /** * 判断字符串是否非空 * * @param s * @return */ public static boolean isNotEmpty(String s) { return !isEmpty(s); } /** * 判断字符串是空白字符 * * @param s * @return */ public static boolean isBlack(String s) { return isEmpty(s) || s.matches("\\s*"); } /** * 判断字符串是非空白字符F * * @param s * @return */ public static boolean isNotBlack(String s) { return !isBlack(s); } /** * 截取字符串 * * @param s * @param flag * @return */ public static String subStringBefore(String s, String flag) { if (isEmpty(s) || isEmpty(flag)) { return s; } int index = s.indexOf(flag); if (index != -1) { return s.substring(0, index); } return flag; } /** * 截取字符串 * * @param s * @param flag * @return */ public static String subStringAfter(String s, String flag) { if (isEmpty(s) || isEmpty(flag)) { return s; } int index = s.indexOf(flag); if (index != -1) { return s.substring(index + flag.length()); } return flag; } } 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596好了,准备工作已经基本完成了,下面我们来编写响应类
package com.gujin.server; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.Socket; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import com.gujin.server.utils.HQString; /** * HTTP响应 * * @author jianggujin * */ public class HQResponse { /** 缓冲区大小 **/ private final int BUFSIZE = 512; /** 响应时间格式化 **/ private final String RESPONSE_DATE_TIME = "EEE, dd MMM yyyy HH:mm:ss 'GMT'"; /** HTTP版本 **/ private final String HTTP_VERSION = "HTTP/1.1"; /** 响应时间格式化 **/ private final SimpleDateFormat RESPONSE_DATE_FORMAT = new SimpleDateFormat( RESPONSE_DATE_TIME, Locale.US); /** 缓冲输出流 **/ private ByteArrayOutputStream bufferStream = null; /** Socket输出流 **/ private OutputStream stream = null; /** 响应码 **/ private int statusCode = 200; /** 内容类型 **/ private String contentType; /** * 构造方法 * * @param socket * @throws IOException */ public HQResponse(Socket socket) throws IOException { bufferStream = new ByteArrayOutputStream(BUFSIZE); stream = socket.getOutputStream(); } /** * 向客户端写数据 * * @param data * @throws IOException */ public void write(byte[] data) throws IOException { bufferStream.write(data); } /** * 向客户端写数据 * * @param data * @param start * @param len */ public void write(byte[] data, int start, int len) { bufferStream.write(data, start, len); } /** * 向客户端发送头信息 * * @throws IOException */ private void writeHeader() throws IOException { stream.write(MessageFormat.format("{0} {1} {2}\r\n", HTTP_VERSION, statusCode, "OK").getBytes()); stream.write(MessageFormat.format("Date: {0}\r\n", RESPONSE_DATE_FORMAT.format(new Date())).getBytes()); stream.write("Server: HQHttpServer 1.0".getBytes()); if (HQString.isNotEmpty(contentType)) { stream.write(MessageFormat .format("Content-Type: {0}\r\n", contentType).getBytes()); } stream.write(MessageFormat.format("Content-Length: {0}\r\n", bufferStream.size()).getBytes()); } /** * 实际响应 * * @throws IOException */ public void response() throws IOException { writeHeader(); // 换一行 stream.write("\r\n".getBytes()); bufferStream.writeTo(stream); bufferStream.flush(); stream.flush(); } /** * 获得内容类型 * * @return */ public String getContentType() { return contentType; } /** * 设置内容类型 * * @param contentType */ public void setContentType(String contentType) { this.contentType = contentType; } } 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131最后我们对上一篇博客中的HQHttpServer类中的handleRequest方法进行改造,将浏览器请求的头信息响应给浏览器,完成一次交互,完整代码如下:
package com.gujin.server; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.text.MessageFormat; import java.util.Iterator; import java.util.logging.Level; import com.gujin.server.utils.HQClose; /** * 服务端 * * @author jianggujin * */ public class HQHttpServer implements HQHttpServerLog { /** 端口号 **/ private int port = 80; /** 服务套接字 **/ private ServerSocket serverSocket = null; /** * 默认构造方法 */ public HQHttpServer() { } /** * 构造方法 * * @param port */ public HQHttpServer(int port) { this.port = port; } /** * 启动服务器 */ public synchronized void start() { try { serverSocket = new ServerSocket(port); LOG.info("server init success."); } catch (IOException e) { LOG.log(Level.SEVERE, e.getMessage(), e); } new Thread() { public void run() { while (!isStop()) { Socket socket; try { socket = serverSocket.accept(); handleRequest(socket); } catch (IOException e) { LOG.log(Level.SEVERE, e.getMessage(), e); } } }; }.start(); } /** * 处理请求 * * @param socket * @throws IOException */ public void handleRequest(Socket socket) throws IOException { HQRequest request = new HQRequest(socket); request.execute(); HQResponse response = new HQResponse(socket); response.setContentType("text/plain"); Iterator<String> iterator = request.getHeaderNames(); while (iterator.hasNext()) { String name = iterator.next(); response.write(MessageFormat.format("{0}: {1}", name, request.getHeader(name)).getBytes()); } response.response(); socket.close(); } /** * 是否停止 * * @return */ public boolean isStop() { return serverSocket == null || serverSocket.isClosed(); } /** * 停止服务器 */ public synchronized void stop() { if (!isStop()) { HQClose.safeClose(serverSocket); serverSocket = null; } } public static void main(String[] args) { new HQHttpServer().start(); } } 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127运行程序,在浏览器输入:http://127.0.0.1,我们可以看到浏览器已经可以正常的接收服务端相应的数据了,页面显示内容如下: ACCEPT-ENCODING: gzip,deflate,sdchHOST: 127.0.0.1USER-AGENT: Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36CONNECTION: keep-aliveACCEPT: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8ACCEPT-LANGUAGE: zh-CN,zh;q=0.8
打开浏览器的开发者工具观察服务端响应的数据信息
我们可以看到浏览器接收到的头信心与我们相应的数据是一致的。 到这里,一个最简单的WEB服务器已经完成了与浏览器的一次完成整的交互,接下来我们要继续对其优化。