-
需求
- 支持浏览器客户端接入
- 根据请求的资源路径响应正确的结果
- 支持访问静态资源
- 支持访问动态资源
- 当资源不存在时响应
404
提示 - 当发生异常时提示
500
错误 - 为保证服务器安全稳定,服务器端不可无限开启新线程
-
思路
- 启动ServerSocket,监听指定端口
- 等待客户端接入,将接入的客户端交给线程池去处理,主线程继续监听客户端接入
- 静态资源:从指定的静态资源路径去查找文件,将文件转换为字节,写入输出流
- 动态资源:从类路径下查找响应的Servlet,调用Servlet的service处理程序,将返回值写入输出流
- 当请求的资源不存在,将
404.html
文件写入输出流 - 当发生异常,将
500.html
文件写入输出流
在实现HTTP服务器之前,我们需要先来了解一下HTTP的报文结构。
# HTTP报文结构
-
可参考
- https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Messages
-
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 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 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 aClass = (Class) 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
# 系列文章
- 【BIO】在聊天室项目中的演化
- 【BIO】通过指定消息大小实现的多人聊天室-终极版本
欢迎在评论区留下你看文章时的思考,及时说出,有助于加深记忆和理解,还能和像你一样也喜欢这个话题的读者相遇~