需求:
简单实现http服务器功能,服务器运行之后,可以自定义servlet,完成指定功能,浏览器访问,后台可以处理请求,并返回相应内容
写在前面
http协议基于TCP/IP协议,本例是用socket做底层实现的
socket相关看这篇(https://www.jianshu.com/p/651fd7718450)
1.简易Server端构建
socket构建server端,浏览器不同方式访问,查看不同的请求
public class Server2 {
private static final String CRLF="\r\n";
private ServerSocket serverSocket;
public static void main(String[] args) {
Server2 server = new Server2();
server.start();
}
/**
* 服务器启动方法
* @throws IOException
*/
public void start(){
try {
serverSocket = new ServerSocket(8888);
this.recive();
} catch (Exception e) {
e.printStackTrace();
//关闭server
}
}
/**
* 服务器接收客户端方法
*/
public void recive() {
try {
Socket client = serverSocket.accept();
//得到客户端
StringBuilder sb =new StringBuilder();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
String mString = null;
while ((mString = bufferedReader.readLine()).length()>0) {
sb.append(mString);
sb.append(CRLF);
if (mString==null) {
break;
}
}
System.out.println(sb.toString());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* 关闭服务器方法
*/
public void stop(){
//CloseUtils.closeSocket(server);
}
}
GET请求:
GET /index?name=123&psw=fdskf HTTP/1.1
Host: localhost:8888
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
POST请求:
POST /index HTTP/1.1
Host: localhost:8888
Connection: keep-alive
Content-Length: 34
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: null
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
username=fdsfgsdfg&pwd=fdsfadsfasd
对两种不同的方式的请求信息解析
2.Request封装
先解析下请求:
第一行: 请求方式 请求资源 HTTP协议版本
后面几行是一些协议,客户端支持的数据格式
如果是POST请求,请求参数会放在最后一行,和上面一行有空行间隔,如果是GET方式,请求参数会放在第一行
2.1 得到浏览器的请求信息
通过构造方法将socket的输入流传入,读取解析
2.2 得到请求方式与请求资源
解析请求信息,字符串截取第一行,然后截取方法,根据方法判断,如果是GET,那么请求资源与请求参数在第一行,如果是POST,第一行是请求资源,最后一行是请求参数,然后解析请求资源
public Request(InputStream inputStream) {
this();
this.inputStream = inputStream;
//从输入流中取出请求信息
try {
byte[] data = new byte[20480];
int len = inputStream.read(data);
requestInfo=new String(data, 0, len);
//解析请求信息
parseRequestInfo();
} catch (Exception e) {
return;
}
}
/**
* 解析请求信息
* @param requestInfo2
*/
private void parseRequestInfo() {
if(requestInfo==null || requestInfo.trim().equals("")) {
return;
}
String paramentString="";//保存请求参数
//得到请求第一行数据
String firstLine = requestInfo.substring(0, requestInfo.indexOf(CRLF));
//第一个/的位置
int index = firstLine.indexOf("/");
this.method = firstLine.substring(0,index).trim();
String urlString = firstLine.substring(index,firstLine.indexOf("HTTP/")).trim();
//判断请求方式
if (method.equalsIgnoreCase("post")) {
url = urlString;
//最后一行就是参数
paramentString = requestInfo.substring(requestInfo.lastIndexOf(CRLF)).trim();
}else if (method.equalsIgnoreCase("get")) {
if (!urlString.contains("?")) {
this.url = urlString;
}else {
//分割url
String[] urlArray = urlString.split("\\?");
this.url = urlArray[0];
paramentString = urlArray[1];
}
}
if (paramentString!=null&&!paramentString.trim().equals("")) {
//解析请求参数
parseParament(paramentString);
}
}
/**
* 解决中文乱码
* @param value
* @param code
* @return
*/
private String decode(String value,String code) {
try {
return URLDecoder.decode(value, code);
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
/**
* 解析请求参数,放在数组里面
* name=12&age=13&fav=1&fav=2
*/
private void parseParament(String paramentString) {
String[] paramentsArray = paramentString.split("&");
for(String string:paramentsArray) {
//某个键值对的数组
String[] paramentArray = string.split("=");
//如果该键没有值,设值为null
if (paramentArray.length==1) {
paramentArray = Arrays.copyOf(paramentArray, 2);
paramentArray[1]=null;
}
String key = paramentArray[0];
String value = paramentArray[1]==null?null:decode(paramentArray[1].trim(), "utf8");
//分拣法
if (!paramentMap.containsKey(key)) {
paramentMap.put(key,new ArrayList<String>());
}
//设值
ArrayList<String> values = paramentMap.get(key);
values.add(value);
}
}
2.3 根据name得到请求参数的值
上一步解析完数据之后,会把请求参数放在Map中,key为请求参数name,value为请求参数值,根据key得到值
/**
* 根据key得到多个值
*/
public String[] getParamenters(String name) {
ArrayList<String> values =null;
if ((values=paramentMap.get(name))==null) {
return null;
}else {
return values.toArray(new String[0]);
}
}
/**
* 根据key得到值
*/
public String getParamenter(String name) {
if ((paramentMap.get(name))==null) {
return null;
}else {
return getParamenters(name)[0];
}
}
2.4 解决中文乱码
前台提交数据时候,中文有时候会乱码,
/**
* 解决中文乱码
* @param value
* @param code
* @return
*/
private String decode(String value,String code) {
try {
return URLDecoder.decode(value, code);
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
3. 封装response
当得到浏览器请求之后,需要给浏览器响应
响应格式:
响应:
- HTTP协议版本,状态码
- 响应头
- 响应正文
3.1 得到服务器的输出流
构造方法传入socket的输出流
3.2 构造响应头
常见响应码:200 404 500
根据状态码,构建不同的响应头,
3.3 构建方法,外界传入响应正文
暴露一个方法,用于外界传入响应值,与状态码
3.4 构建响应正文
根据不同响应码构建响应正文,如404,返回一个NOT FOUNF页面,
500返回一个SERVER ERROR 页面,如果是200,就返回正常界面,
3.5 构建推送数据到客户端的方法
将响应推送到客户端
4. 封装servlet
将响应与请求封装在一个servlet类中,主要是实现业务逻辑,不做其他事情 , 在server端直接new 一个servlet类,调用业务方法
public class Servlet {
public void service(Request request,Response response){
response.print("<html>\r\n" +
"<head>\r\n" +
" <META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\r\n" +
"</head>\r\n" +
"<body>\r\n" +
"欢迎你\r\n" + request.getParamenter("username")+
"</form>\r\n" +
"</body>\r\n" +
"</html>");
}
}
5. 处理不同请求的server
想让server可以处理不同请求,/login 是做登录请求 /reg 是做注册请求
需要多线程,当有请求过来之后,创建一个线程处理相关的请求与响应,每个请求的线程互不影响,
5.1 创建一个转发器
每有一个客户端连接,就会创建一个线程,处理改客户端的请求与响应
public class Dispatcher implements Runnable{
private Request request;
private Response response;
private Socket client;
private int code=200;
public Dispatcher(Socket client) {
try {
client = client;
request = new Request(client.getInputStream());
response = new Response(client.getOutputStream());
} catch (IOException e) {
code=500;
return;
}
}
@Override
public void run() {
Servlet servlet = new Servlet();
servlet.service(request, response);
response.pushToclient(code);
CloseUtils.closeSocket(client);
}
}
5.2 创建上下文对象,存放servlet与对应的mapping
使用工厂模式,得到不同url得到不同的servlet,
public class ServletContext {
/*
* 有LoginServlet 设值别名 login
* 访问login /login /log
*/
//存放servlet的别名
private Map<String, Servlet> servletMap;
//存放url对应的别名
private Map<String, String> mappingMap;
public ServletContext() {
servletMap = new HashMap<>();
mappingMap = new HashMap<>();
}
public class WebApp {
private static ServletContext context;
static {
context = new ServletContext();
//存放servlet 和其对应的别名
Map<String, Servlet> servletMap = context.getServletMap();
servletMap.put("login", new LoginServlet());
servletMap.put("register", new RegisterServlet());
Map<String, String> mappingMap = context.getMappingMap();
mappingMap.put("/login", "login");
mappingMap.put("/", "login");
mappingMap.put("/register", "register");
mappingMap.put("/reg", "register");
}
public static Servlet getServlet(String url) {
if (url==null || url.trim().equals("")) {
return null;
}else {
return context.getServletMap().get(context.getMappingMap().get(url));
}
}
}
6.反射获取servlet对象
根据请求的url, 在mappingmap中找到servlet的别名,根据servlet的别名在servletmap中得到servlet对象,map存对象过于耗费内存,并且,每次添加一个servlet,都要更改这个文件,所以讲servlet的配置,卸载xml文件中,读取xml文件
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<servlet>
<servlet-name>login</servlet-name>别名
<servlet-class>jk.zmn.server.demo4.LoginServlet</servlet-class>类的全路径
</servlet>
<servlet-mapping>配置映射
<servlet-name>login</servlet-name>别名
<url-pattern>/login</url-pattern> 访问路径
<url-pattern>/</url-pattern> 访问路径
</servlet-mapping>
<servlet>
<servlet-name>reg</servlet-name>
<servlet-class>jk.zmn.server.demo4.RegisterServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>reg</servlet-name>
<url-pattern>/reg</url-pattern>
</servlet-mapping>
</web-app>
6.1 解析xml文件
首先要先解析xml配置文件,得到servlet及其映射,
6.2 根据解析到的数据,动态添加到map中
//获取解析工厂
try {
SAXParserFactory factory =SAXParserFactory.newInstance();
//获取解析器
SAXParser sax =factory.newSAXParser();
//指定xml+处理器
WebHandler web = new WebHandler();
sax.parse(Thread.currentThread().getContextClassLoader()
.getResourceAsStream("jk/zmn/server/demo4/web.xml")
,web);
//得到所有的servlet 和别名
List<ServletEntity> entityList = web.getEntityList();
List<MappingEntity> mappingList = web.getMappingList();
context = new ServletContext();
//存放servlet 和其对应的别名
Map<String, String> servletMap = context.getServletMap();
for(ServletEntity servletEntity: entityList) {
servletMap.put(servletEntity.getName(),servletEntity.getClz());
}
//存放urlpatten和servlet别名
Map<String, String> mappingMap = context.getMappingMap();
for(MappingEntity mappingEntity:mappingList) {
List<String> urlPattern = mappingEntity.getUrlPattern();
for(String url:urlPattern) {
mappingMap.put(url, mappingEntity.getName());
}
}
不用每次都直接修改这个文件,直接在配置文件中配置就行
##########################################################
最后:我已将文件抽好,
servlet包,是用户自定义包,新建的servlet必须要继承servlet类,
web.xml必须在src目录下,配置servlet也需按照格式配置,
运行core java application, 浏览器访问你的项目,就可以正常运行了,
效果:
本例存在诸多bug,请多多指教,在读取请求信息时候,我是直接读了20480个字节,实际上应该一个一个字节的读,但是写不出来,希望大佬们帮帮忙
源码:https://gitee.com/zhangqiye/httpserver
qq群:552113611