- 需求
- 支持浏览器客户端接入
- 根据请求的资源路径响应正确的结果
- 支持访问静态资源
- 支持访问动态资源
- 当资源不存在时响应
404
提示 - 当发生异常时提示
500
错误 - 为保证服务器安全稳定,服务器端不可无限开启新线程
- 思路
- 启动ServerSocket,监听指定端口
- 等待客户端接入,将接入的客户端交给线程池去处理,主线程继续监听客户端接入
- 静态资源:从指定的静态资源路径去查找文件,将文件转换为字节,写入输出流
- 动态资源:从类路径下查找响应的Servlet,调用Servlet的service处理程序,将返回值写入输出流
- 当请求的资源不存在,将
404.html
文件写入输出流 - 当发生异常,将
500.html
文件写入输出流
在实现HTTP服务器之前,我们需要先来了解一下HTTP的报文结构。
# HTTP报文结构
- 可参考
- Request与Response的报文结构
Request的报文结构
- 所以按照约定的报文格式进行消息的解析与发送即可~
# 资源准备
- 静态资源
- 动态资源,Servlet
- Servlet规范
/** * Servlet规范接口 * * @author futao * @date 2020/7/6 */ public interface Servlet { /** * 业务处理程序 * * @return 响应 */ Object service(); }
- 动态资源
/** * 返回字符串 * * @author futao * @date 2020/7/6 */ public class HelloServlet implements Servlet { @Override public Object service() { return "greet from dynamic server..."; } } -------------------------------------------------------- -------------------------------------------------------- /** * 返回list集合 * * @author futao * @date 2020/7/6 */ public class UserListServlet implements Servlet { @Override public Object service() { ArrayList<User> users = new ArrayList<>(); for (int i = 0; i < 10; i++) { users.add( User.builder() .name("喜欢天文的pony站长") .age(i) .address("浙江杭州") .build() ); } return users; } @Getter @Setter @Builder static class User { private String name; private int age; private String address; } } -------------------------------------------------------- -------------------------------------------------------- /** * 返回当前时间 * * @author futao * @date 2020/7/6 */ public class CurTimeServlet implements Servlet { private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @Override public Object service() { return "当前时间为: " + DATE_TIME_FORMATTER.format(LocalDateTime.now(ZoneOffset.ofHours(8))); } } -------------------------------------------------------- -------------------------------------------------------- /** * 模拟异常的Servlet * * @author futao * @date 2020/7/6 */ public class ExceptionServlet implements Servlet { @Override public Object service() { throw new RuntimeException("发生了异常"); } }
# 服务器代码编写
- 核心代码:
/** * 基于BIO实现的静态 and 动态服务器 * * @author futao * @date 2020/7/6 */ public class BIODynamicServer { private static final Logger logger = LoggerFactory.getLogger(BIODynamicServer.class); /** * 用于处理客户端接入的线程池 */ private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(10); /** * 静态资源路径 */ private static final String STATIC_RESOURCE_PATH = System.getProperty("user.dir") + "/practice/src/main/resources/pages/"; /** * Servlet的类路径 */ private static final String DYNAMIC_RESOURCE_CLASS_PATH = "com.futao.practice.chatroom.bio.v6server.servlet."; /** * Servlet后缀 */ private static final String SERVLET_SUFFIX = "Servlet"; /** * Servlet缓存 */ private static final Map<String, Servlet> SERVLET_MAP = new HashMap<>(); /** * 默认页面 */ private static final String DEFAULT_PAGE = STATIC_RESOURCE_PATH + "index.html"; /** * 响应的基础信息 */ public static final String BASIC_RESPONSE = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html;charset=utf-8\r\n" + "Vary: Accept-Encoding\r\n"; /** * 回车换行符 */ private static final String carriageReturn = "\r\n"; public void start() { ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(Constants.SERVER_PORT); logger.debug("========== 基于BIO实现的服务器,开始提供服务 =========="); while (true) { Socket socket = serverSocket.accept(); THREAD_POOL.execute(() -> { try { InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream(); byte[] bytes = new byte[1024]; int curByteLength = inputStream.read(bytes); byte[] dest = new byte[curByteLength]; System.arraycopy(bytes, 0, dest, 0, curByteLength); //请求报文 String request = new String(dest); logger.info("接收到客户端的数据:\n{}\n{}", request, StringUtils.repeat("=", 50)); // 解析请求地址 String requestUri = BIODynamicServer.getRequestUri(request); // 静态资源处理器 boolean staticHandler = staticHandler(requestUri, outputStream); if (!staticHandler) { //动态资源处理器 if (!dynamicHandler(requestUri, outputStream)) { //动态资源不存在,响应404 logger.debug("资源[{}]不存在,响应404", requestUri); staticHandler("404.html", outputStream); } } } catch (IOException e) { e.printStackTrace(); } }); } } catch (IOException e) { e.printStackTrace(); } } }
- 解析请求地址
/** * 获取请求的资源地址 * * @param request * @return */ private static String getRequestUri(String request) { //GET /index.html HTTP/1.1 int firstBlank = request.indexOf(" "); String excludeMethod = request.substring(firstBlank + 2); return excludeMethod.substring(0, excludeMethod.indexOf(" ")); }
- 处理静态资源
/** * 静态资源处理器 * * @return */ public boolean staticHandler(String page, OutputStream outputStream) throws IOException { //资源的绝对路径 String filePath = BIODynamicServer.STATIC_RESOURCE_PATH + page; boolean fileExist = false; File file = new File(filePath); if (file.exists() && file.isFile()) { logger.debug("静态资源[{}]存在", page); fileExist = true; //读取文件内容 byte[] bytes = Files.readAllBytes(Paths.get(filePath)); //写入响应 BIODynamicServer.writeResponse(outputStream, bytes); } return fileExist; }
- 处理动态资源
/** * 动态资源处理器 * * @param requestUri 请求资源名 * @param outputStream 输出流 * @return * @throws IOException */ private boolean dynamicHandler(String requestUri, OutputStream outputStream) throws IOException { //Servlet是否存在 boolean servletExist = false; //Servlet Servlet servletInstance = null; //从缓存中取 Servlet servlet = SERVLET_MAP.get(requestUri); if (servlet == null) { //缓存中不存在 try { //反射获取Class Class<Servlet> aClass = (Class<Servlet>) Class.forName(BIODynamicServer.DYNAMIC_RESOURCE_CLASS_PATH + requestUri + BIODynamicServer.SERVLET_SUFFIX); //创建Servlet对象 servletInstance = aClass.newInstance(); //缓存 SERVLET_MAP.put(requestUri, servletInstance); servletExist = true; logger.debug("动态资源[{}]存在", requestUri); } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) { return false; } } else { //缓存中存在 servletInstance = servlet; servletExist = true; logger.debug("动态资源[{}]存在", requestUri); } //执行业务逻辑 try { Object result = servletInstance.service(); String resp = JSON.toJSONString(result, SerializerFeature.PrettyFormat); //结果写入输出流 BIODynamicServer.writeResponse(outputStream, resp.getBytes(Constants.CHARSET)); } catch (Exception e) { //响应500 staticHandler("500.html", outputStream); } return servletExist; }
- 将结果写入输出流
/** * 写入响应 * * @param outputStream 输出流 * @param content 内容 * @throws IOException */ private static void writeResponse(OutputStream outputStream, byte[] content) throws IOException { //写入基础响应头 outputStream.write(BASIC_RESPONSE.getBytes(Constants.CHARSET)); //写入服务器信息 outputStream.write(("Server: futaoServer/1.1" + BIODynamicServer.carriageReturn).getBytes(Constants.CHARSET)); //写入传输的正文内容大小 outputStream.write(("content-length: " + content.length + BIODynamicServer.carriageReturn).getBytes(Constants.CHARSET)); //响应头与响应体之间需要空一行 outputStream.write(BIODynamicServer.carriageReturn.getBytes(Constants.CHARSET)); //写入响应正文 outputStream.write(content); outputStream.flush(); }
- 测试
- 静态资源
index.html
动态资源
异常情况
# 源代码
- 源代码都给你了你还不看看?
- https://github.com/FutaoSmile/learn-IO/tree/master/practice/src/main/java/com/futao/practice/chatroom/bio/v6server
# 系列文章
欢迎在评论区留下你看文章时的思考,及时说出,有助于加深记忆和理解,还能和像你一样也喜欢这个话题的读者相遇~