写一个简单的web服务器
本人出入门槛,做一个小项目分享一下。有什么纰漏还请各位大佬指出。一直在努力,从未停止,大家共同进步。
项目简介
使用B/S架构,简单实现web服务器。本项目未使用任何第三方jar,复制粘贴即可用
浏览器端向服务端发出请求,服务器根据请求的内容,做出相应的响应,返回给浏览器。
eg:浏览器做出了请求:请求一张图片
服务器会在自己的资源文件中查找有没有图片,有的话会返回一个图片。没有的话会做出资源未找到的响应
项目需要用到的主要知识:
IO流、网络编程、枚举、HTTP协议、线程池、map集合
项目简单思路:
1.准备一个Request、Response的实体类,
2.准备一个资源文件(source),用来存放浏览器请求的文件
3.写一个工具类(SocketToRequest),把请求到的Socket对象转化为request对象,方便获取请求行、请求头、请求体
4.写一个enum类,因为响应状态的代码是固定的,所以就用枚举把它写死
5.写一个类(FileTypeUtil),来存放响应文件的类型,和字符编码格式
6.因为不知道同时有多少个线程发来,所以这里用一个newCachedThreadPool()的线程池。来响应客户端。
Request实体类
public class Request implements Serializable{
private static final long serialVersionUID = 1L;
/*
* 请求方式
*/
private String requestMethod;
/*
* 请求的协议
*/
private String agreeMent = "HTTP/1.1";
/*
* 请求的资源
*/
private String requestSource;
/*
* 存放请求头
*/
private Map<String, String> map = new HashMap<>();
/*
* 请求体
*/
private String requestBody;
public String getAgreeMent() {
return agreeMent;
}
public String getRequestMethod() {
return requestMethod;
}
public void setRequestMethod(String requestMethod) {
this.requestMethod = requestMethod;
}
public String getRequestSource() {
return requestSource;
}
public void setRequestSource(String requestSource) {
this.requestSource = requestSource;
}
public Map<String, String> getMap() {
return map;
}
public void setMap(Map<String, String> map) {
this.map = map;
}
public String getRequestBody() {
return requestBody;
}
public void setRequestBody(String requestBody) {
this.requestBody = requestBody;
}
@Override
public String toString() {
return "Request [requestMethod=" + requestMethod + ", agreeMent=" + agreeMent + ", requestSource="
+ requestSource + ", map=" + map + ", requestBody=" + requestBody + "]";
}
}
Response实体类
public class Response implements Serializable{
private static final long serialVersionUID = 1L;
/*
*响应行
*/
private String agreeMent = "HTTP/1.1";
/*
* 响应行状态码
*/
private StateCodeEnum stateCode;
/*
* 响应头
*/
private Map<String, String> map = new HashMap<>();
public String getAgreeMent() {
return agreeMent;
}
public StateCodeEnum getStateCode() {
return stateCode;
}
public void setStateCode(StateCodeEnum stateCode) {
this.stateCode = stateCode;
}
public Map<String, String> getMap() {
return map;
}
public void setMap(Map<String, String> map) {
this.map = map;
}
@Override
public String toString() {
return "Response [agreeMent=" + agreeMent + ", stateCode=" + stateCode + ", map=" + map + "]";
}
}
上边的Request、Response类相比很容易就可以看出来是干什么用的。对于请求和响应有不清楚的可以详细查看本人博客
source文件的东西大家可以随便存放一些图片、网页、文本等等。
工具类SocketToRequest.java。主要就是把socket中的请求信息,通过IO流、字符串的拼接封装到Ruquest的属性中
public class SocketToRequest {
public static Request socketToRequest(Socket socket) throws IOException {
Request request = new Request();
BufferedReader br =
new BufferedReader(new InputStreamReader(socket.getInputStream()));
//读取socket的第一行,获取请求行
String line = null;
line = br.readLine();
String[] split = line.split(" ");
if(line != null) {
//获取请求方式
request.setRequestMethod(split[0].trim());
/*获取请求的资源
*因为要区分GET和POST请求所以这里用 ? 把地址分开。前一部分是请求文件、后边是请求体
*/
String[] split2 = split[1].split("\\?");
request.setRequestSource(split2[0]);
//数组长度>1时,把地址中的参数赋值给请求体
if(split2.length > 1) {
request.setRequestBody(split2[1]);
}
}
//获取请求头
while ((line = br.readLine()) != null && !"".equals(line)) {
split = line.split(":");
request.getMap().put(split[0].trim(), split[1].trim());
}
/*
* 判断请求头里面有没有Content-Length
* 有则代表有请求体,否则没有
*/
if(request.getMap().containsKey("Content-Length")) {
request.setRequestBody(br.readLine());
}
socket.shutdownInput();
return request;
}
}
枚举类StateCodeEnum,用来存放响应状态码
在这里我只写了两种状态码200 和 404
public enum StateCodeEnum {
OK(
200,"OK"),
NOT_FOUND(
404,"NOT_FOUND");
private int num;
private String str;
private StateCodeEnum(int num,String str) {
this.num = num;
this.str = str;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public String getStr() {
return str;
}
public void setStr(String str) {
this.str = str;
}
}
工具类FileTypeUtil.java
用来存放响应时的文件类型和字符编码,这里用map集合来存储。key值代表请求文件的后缀名,value值代表响应内容的类型和字符编码
浏览器发出请求,根据请求的地址文件的后缀名,服务器端做出相应的响应
public class FileTypeUtil {
public static Map<String, String> map = null;
static {
map = new HashMap<>();
map.put("jpg", "image/jpg;charset=utf-8");
map.put("jpep", "images/jpeg;charset=utf-8");
map.put("text", "text/html;charset=utf-8");
map.put("html", "text/html;charset=utf-8");
map.put("jsp", "text/html;charset=utf-8");
map.put("pdf", "application/pdf;charset=utf-8");
}
}
之前写的都是做的一些准备,主要的逻辑代码就在下边这个类中
类ThreadPoolImpl.java。用来给浏览器端进行响应
public class ThreadPoolImpl {
public static final ExecutorService POOL;
static {
POOL = Executors.newCachedThreadPool();
}
//向浏览器端写入东西的方法
@SuppressWarnings("unused")
private static void runn(File file,String type,Response response,Socket socket) {
try {
response.setMap(FileTypeUtil.map);
BufferedInputStream bis =
new BufferedInputStream(new FileInputStream(file));
BufferedOutputStream bos =
new BufferedOutputStream(socket.getOutputStream());
//这里用StringBuffer来进行字符串拼接,减少内存的消耗
StringBuffer sb = new StringBuffer();
//响应行的拼接 ---> HTTP协议 + 响应状态码
StringBuffer str1 = sb.append(response.getAgreeMent())
.append(" ")
.append(response.getStateCode().getNum())
.append(" ")
.append(response.getStateCode().getStr())
.append("\r\n");
bos.write(str1.toString().getBytes());
//响应头 ----> Content-Type: + 响应文件类型 + 响应字符编码
//清空sb
sb.delete(0, sb.length());
StringBuffer str2 = sb.append("Content-Type:")
.append(response.getMap().get(type))
.append("\r\n");
bos.write(str2.toString().getBytes());
bos.write("\r\n".getBytes());
//响应体 -----> 向浏览器写入文件
int len = 0;
byte[] by = new byte[1024];
while ((len = bis.read(by)) != -1) {
bos.write(by, 0, len);
bos.flush();
}
socket.shutdownOutput();
bos.close();
socket.close();
} catch (Exception e) {
System.out.println("响应异常: " + e.getMessage());
}
}
//响应的方法
@SuppressWarnings("unused")
private static void server(Socket socket,Response response,Request request) {
//请求的文件名
String fileName = null;
//响应的文件
File file = null;
//响应的文件类型
String type = null;
//如果状态是OK,则正常响应页面
if(response.getStateCode().equals(StateCodeEnum.OK)) {
//通过请求获取文件名
fileName = request.getRequestSource();
//通过请求的文件名,获取source中的文件
file = new File("source", fileName);
//获取文件的后缀名,也就是文件类型,用来获取响应的类型
type = fileName.substring(fileName.lastIndexOf(".") + 1);
//向浏览器端做出响应
runn(file, type, response, socket);
}
//如果是NOT_FOUND则响应“资源不存在”
else if (response.getStateCode().equals(StateCodeEnum.NOT_FOUND)) {
//文件不存在,则在source中找到404.html文件
file = new File("source", "404.html");
//做出响应,响应资源未找到
runn(file, "html", response, socket);
}
}
//get请求时的响应代码
private static void doGet(Socket socket,Response response,Request request) {
//获取响应体
String body = request.getRequestBody();
//以下代码针对登录页面请求时做出的响应
if(body != null && !"".equals(body)) {
//通过&对响应体分割,前一部分为username=? 后一部分为 password=?
String[] split = body.split("&");
//如果username=1并且password=1则正常跳转页面
if("username=1".equals(split[0]) && "password=1".equals(split[1])) {
runn(new File("source", "login.html"), "html", response, socket);
}else {
//反正跳转登录失败界面
runn(new File("source", "def.html"), "html", response, socket);
}
}else {
//如果不是登录页面的请求,则直接执行以下代码
server(socket, response, request);
}
}
//post请求时的响应代码
private static void doPost(Socket socket,Response response,Request request) {
doGet(socket, response, request);
}
//根据请求方式做出响应
public static void serverCode(Socket socket,Response response,Request request) {
POOL.execute(()->{
if ("GET".equals(request.getRequestMethod())) {
ThreadPoolImpl.doGet(socket, response, request);
}else if ("POST".equals(request.getRequestMethod())) {
ThreadPoolImpl.doPost(socket, response, request);
}
});
}
}
main方法
public static void main(String[] args) {
try {
ServerSocket ss = new ServerSocket(10086);
while (true) {
Socket socket = ss.accept();
System.out.println("服务器连接成功!");
//把socket转换成request对象
Request request = SocketToRequest.socketToRequest(socket);
//获取请求的文件名
String fileName = request.getRequestSource();
//通过请求获取source中的文件
File file = new File("source", fileName);
Response response = new Response();
//判断文件是否存在
if(file.exists()) {
//文件存在,响应状态码为OK
response.setStateCode(StateCodeEnum.OK);
//给客户端发送数据
ThreadPoolImpl.serverCode(socket, response, request);
}else {
//文件不存在,响应状态码为 NOT_FOUND
response.setStateCode(StateCodeEnum.NOT_FOUND);
//响应一个资源不存在的页面
ThreadPoolImpl.serverCode(socket, response, request);
}
}
} catch (Exception e) {
System.out.println("服务器响应异常:" + e.getMessage());
}
}