手写高性能http服务器,手写httpserver

需求:

简单实现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());

}

//设值

ArrayList values = paramentMap.get(key);

values.add(value);

}

}

2.3 根据name得到请求参数的值

上一步解析完数据之后,会把请求参数放在Map中,key为请求参数name,value为请求参数值,根据key得到值

/**

* 根据key得到多个值

*/

public String[] getParamenters(String name) {

ArrayList 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("\r\n" +

"

\r\n" +

" \r\n" +

"\r\n" +

"

\r\n" +

"欢迎你\r\n" + request.getParamenter("username")+

"\r\n" +

"\r\n" +

"");

}

}

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 servletMap;

//存放url对应的别名

private Map mappingMap;

public ServletContext() {

servletMap = new HashMap<>();

mappingMap = new HashMap<>();

}

public class WebApp {

private static ServletContext context;

static {

context = new ServletContext();

//存放servlet 和其对应的别名

Map servletMap = context.getServletMap();

servletMap.put("login", new LoginServlet());

servletMap.put("register", new RegisterServlet());

Map 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文件

login别名

jk.zmn.server.demo4.LoginServlet类的全路径

配置映射

login别名

/login 访问路径

/ 访问路径

reg

jk.zmn.server.demo4.RegisterServlet

reg

/reg

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 entityList = web.getEntityList();

List mappingList = web.getMappingList();

context = new ServletContext();

//存放servlet 和其对应的别名

Map servletMap = context.getServletMap();

for(ServletEntity servletEntity: entityList) {

servletMap.put(servletEntity.getName(),servletEntity.getClz());

}

//存放urlpatten和servlet别名

Map mappingMap = context.getMappingMap();

for(MappingEntity mappingEntity:mappingList) {

List urlPattern = mappingEntity.getUrlPattern();

for(String url:urlPattern) {

mappingMap.put(url, mappingEntity.getName());

}

}

不用每次都直接修改这个文件,直接在配置文件中配置就行

##########################################################

最后:我已将文件抽好,

2c608b1baccf?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

image.png

servlet包,是用户自定义包,新建的servlet必须要继承servlet类,

web.xml必须在src目录下,配置servlet也需按照格式配置,

运行core java application, 浏览器访问你的项目,就可以正常运行了,

效果:

2c608b1baccf?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

image.png

2c608b1baccf?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

image.png

2c608b1baccf?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

image.png

本例存在诸多bug,请多多指教,在读取请求信息时候,我是直接读了20480个字节,实际上应该一个一个字节的读,但是写不出来,希望大佬们帮帮忙

源码:https://gitee.com/zhangqiye/httpserver

qq群:552113611

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值