准备找工作,把自己以前做的项目做一下整理,回顾一下知识
使用技术
基于Java BIO,多线程,Socket网络编程,xml解析
具备功能
Request封装Http请求报文
Response封装HTTP响应报文
Dispatcher实现分发器功能,进行Servlet的分发
xmlParse解析web.xml文件
支持Get和Post两种请求
文件结构
项目主要是红线框中的内容,其余的部分是自己写服务器之前的一些学习,不影响项目,只是不舍得删除所以留着
HTTPServer
下面只放了重要部分的代码
值得注意的是,这个服务器基于BIO,那么如果只开一个主线程必然会在socket的accept部分阻塞,所以bio的解决方案就是开新的进程,一个客户端连接开一个线程,但这种方式仍然是同步阻塞的(最后会讨论方案可改进的地方,以及另一种方案NIO)
while (isRunning){
try{
Socket client = serverSocket.accept();
System.out.println("一个客户端建立连接");
//多线程处理
new Thread(new Dispatcher(client)).start();
}catch (Exception e){
e.printStackTrace();
System.out.println("客户端错误");
}
}
HTTPRequest的封装
首先补充一下基础的知识,HTTP请求的格式如下
例如:(空格是存在的!虽然看不太出来的样子)
这里主要是解析请求,需要解析出什么呢?请求方式,请求url,请求参数(需要注意的是请求参数根据get和post请求方式不同,会有一点点的区别,比如post请求参数不但可能出现在输入的网址中,也可能在请求体中出现)
这里主要放了解析请求行部分的内容,像处理中文,以及将分解出来的请求参数存为map等等细节的处理没有放代码
private void parseRequestInfo(){
System.out.println("----------分解----------");
//1.获取请求方式 开头到第一个/
method = requestInfo.substring(0,requestInfo.indexOf("/")).toLowerCase();
method = method.trim();
System.out.println(method);
//1.获取请求uri, 第一个/到HTTP/
//uri可能包含请求参数,只取前面的部分
int startIdx = requestInfo.indexOf("/")+1;//取第一个/的位置
int endIdx = requestInfo.indexOf("HTTP/");//取HTTP/的位置
uri = requestInfo.substring(startIdx,endIdx).trim();
//处理请求参数
int queryIdx = uri.indexOf("?");
if (queryIdx >= 0){//存在请求参数
String[] uriArray = uri.split("\\?");//需要转义
uri = uriArray[0];
queryStr = uriArray[1];
}
System.out.println(uri);
//获取请求参数,是get则获取到了,post可能在请求体中还存在
if (method.equals("post")){
String qStr = requestInfo.substring(requestInfo.lastIndexOf(CRLF)).trim();
if(null == queryStr){
queryStr = qStr;
}else {
queryStr += "&"+qStr;
}
}
queryStr = null==queryStr?"":queryStr;//http://localhost:8888/aaa,出现了queryStr为null的情况,处理null的情况
System.out.println(method+"----"+uri+"-------"+queryStr);
//处理请求参数为Map pare=1¶=2
convertMap();
}
HTTPResponse的封装
封装响应报文主要的工作是:
①:严格按照HTTP响应报文的格式封装响应头,只有按照HTTP的格式封装,浏览器才能正确解析显示出你想要的内容
②:统计请求报文的长度(这里需要注意,要求出字节的长度,而不是求字符的长度!!不然可能会显示内容不完全!)
//动态添加内容
public Response print(String info){
content.append(info);
len += info.getBytes().length;
return this;
}
public Response println(String info){
content.append(info).append(CRLF);
len += (info + CRLF).getBytes().length;
return this;
}
private void createHeadInfo(int code){
//1 响应行:HTTP/1.1 200 OK
headInfo.append("HTTP/1.1").append(BLANK);
headInfo.append(code).append(BLANK);
switch (code){
case 200:
headInfo.append("OK").append(CRLF);
break;
case 404:
headInfo.append("NOT FOUND").append(CRLF);
break;
case 500:
headInfo.append("SERVER ERROR").append(CRLF);
break;
}
headInfo.append("Date:").append(new Date()).append(CRLF);
headInfo.append("Content-type:text/html;charset=UTF-8").append(CRLF);
headInfo.append("Content-length:").append(len).append(CRLF);
headInfo.append(CRLF);
}
public void pushToBrowser(int code) throws IOException {
if (null == headInfo){
code = 505;
}
createHeadInfo(code);
bw.append(headInfo);
bw.append(content);
bw.flush();
}
Dispatcher分发器
Dispatcher实现了Runnable接口,每次接受一个客户端连接都会创建一个线程进行处理
根据http请求报文的uri找到符合的Servlet(通过uri找到servlet主要利用了反射技术,在下面会写到这部分内容),然后将封装好的请求报文发送出去
public void run() {
try{
Servlet servlet = WebApp.getServletFromUrl(request.getUri());
if(null != servlet){
servlet.service(request,response);
//关注状态码
response.pushToBrowser(200);
}else {
//错误页面
response.println("");
response.pushToBrowser(404);
}
}catch (Exception e){
e.printStackTrace();
try {
// response.println("发生错误");
response.pushToBrowser(500);
} catch (IOException e1) {
e1.printStackTrace();
}
}
release();
}
Servlet部分
一个Servlet的接口,有一个service方法,其余的xxxServlet类都实现该接口,并重写service方法
例如:Servlet接口
public interface Servlet {
void service(Request request, Response response);
}
例如:LoginServlet类
public class LoginServlet implements Servlet {
@Override
public void service(Request request, Response response) {
StringBuilder content = new StringBuilder();
response.print("<html>");
response.print("<head>");
response.print("<title>");
response.print("页面");
response.print("</title>");
response.print("</head>");
response.print("<body>");
response.print("come back。"+request.getParameter("username"));
response.print("</body>");
response.print("</html>");
}
}
XML解析
xml解析的一篇文章
link.
首先来看web.xml文件
<?xml version="1.0" encoding="utf-8"?>
<web-app>
<servlet>
<servlet-name>login</servlet-name>
<servlet-class>ssm.server.Myservlect.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>login</servlet-name>
<url-pattern>/login</url-pattern>
<url-pattern>/log</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>reg</servlet-name>
<servlet-class>ssm.server.Myservlect.RegisterServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>reg</servlet-name>
<url-pattern>/reg</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>others</servlet-name>
<servlet-class>ssm.server.Myservlect.OtherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>others</servlet-name>
<url-pattern>/others</url-pattern>
</servlet-mapping>
</web-app>
xml问价需要两个对象来存储,一个存储servlet,一个存储servlet-mapping
servlet对象
public class IServlet {
private String name;
private String clz;
public IServlet(){
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getClz() {
return clz;
}
public void setClz(String clz) {
this.clz = clz;
}
}
servletmapping对象
public class IServletMapping {
private String name;
private Set<String> patterns;
public void addPattern(String pattern){
this.patterns.add(pattern);
}
public IServletMapping() {
patterns = new HashSet<String>();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<String> getPatterns() {
return patterns;
}
public void setPatterns(Set<String> patterns) {
this.patterns = patterns;
}
}
上面是准备工作,下面解析xml,必须明确解析xml是为了什么?
解析是为了从url-pattern拿到url后找到对应的类对象,通过反射技术拿到需要分发到哪个servlet。
详细步骤
①从web.xml中的servlet-mapping中 的url-pattern找到对应的servlet-name,例如:这里有一个url-pattern是/log(/login也是可以的),找到它对应的servlet-name是login
<servlet-mapping>
<servlet-name>login</servlet-name>
<url-pattern>/login</url-pattern>
<url-pattern>/log</url-pattern>
</servlet-mapping>
②将servlet-mapping中得到的servlet-name去servlet中找对应的servlet-name,看servlet中的servlet-name对应的servlet-class,这个时候拿到了calss就可以使用反射获取Class对象
<servlet>
<servlet-name>login</servlet-name>
<servlet-class>ssm.server.Myservlect.LoginServlet</servlet-class>
</servlet>
解析部分的代码为红色部分的内容
webapp是主要的解析流程,具体的解析分离出来放在了webhandler文件中,webcontext主要是解析后得到的结果
WebApp
public class WebApp {
private static WebContext webContext;
static {
try {
//1.获取解析工厂
SAXParserFactory factory = SAXParserFactory.newInstance();
//2.从解析工厂获取解析器
SAXParser parse = factory.newSAXParser();
//3.编写处理器
//4.加载文档Document注册处理器
WebHandler handler = new WebHandler();
//5.解析
parse.parse(Thread.currentThread().getContextClassLoader()
.getResourceAsStream("ssm/server/web.xml"),handler);
//获取数据
webContext = new WebContext(handler.getiServlets(),handler.getiServletMappings());
}catch (Exception e){
System.out.println("解析配置文件错误");
}
}
public static Servlet getServletFromUrl(String url){
String className = webContext.getClz("/"+url);
Class<?> clz;
try {
clz = Class.forName(className);
Servlet servlet = (Servlet)clz.getConstructor().newInstance();
return servlet;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
WebHandler
class WebHandler extends DefaultHandler {
private List<IServlet> iServlets;//容器
private List<IServletMapping> iServletMappings;//容器
private IServlet iServlet;//web.xml中的servlet标签作为一个对象
private IServletMapping iServletMapping;//web.xml中的servlet-mapping标签作为一个对象
private String tag;//存储操作标签
private boolean isMapping = false;//因为sevlet和servletmapping里面有相同的name属性赋值,需要一个区分的标识
@Override
public void startDocument() throws SAXException {
iServlets = new ArrayList<IServlet>();
iServletMappings = new ArrayList<IServletMapping>();
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if (null != qName){
tag = qName;//存储标签名
if (tag.equals("servlet")){
iServlet = new IServlet();
isMapping = false;
}
else if (tag.equals("servlet-mapping")){
iServletMapping = new IServletMapping();
isMapping = true;
}
}
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
String contents = new String(ch,start,length).trim();
if (null != tag){
//因为endElement那里丢弃第二个如</xxx>后面的内容,将tag=null丢弃
//是为了防止第二次赋值把前面的值覆盖为第二次内容空
if (isMapping){
//给servlet-mapping的属性赋值
if (tag.equals("servlet-name")){
iServletMapping.setName(contents);
}else if (tag.equals("url-pattern")){
iServletMapping.addPattern(contents);
}
}else {
//给servlet的属性赋值
if (tag.equals("servlet-name")){
iServlet.setName(contents);
}else if (tag.equals("servlet-class")){
iServlet.setClz(contents);
}
}
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if (null != qName){
if (qName.equals("servlet")){
iServlets.add(iServlet);
}else if (qName.equals("servlet-mapping")){
iServletMappings.add(iServletMapping);
}
}
tag = null;//将tag段丢弃
}
public List<IServlet> getiServlets() {
return iServlets;
}
public List<IServletMapping> getiServletMappings() {
return iServletMappings;
}
}
WebContext
/**
* @author ssm
* 主要功能:通过URL路径找到对应的class
*/
public class WebContext {
private List<IServlet> iServlets = null;
private List<IServletMapping> iServletMappings = null;
//key是servlet-name, value是servlet-class
private Map<String,String> iservletMap = new HashMap<String,String>();
//key是url-pattern, value是servlet-name
private Map<String,String> iservletmappingMap = new HashMap<String,String>();
public WebContext(List<IServlet> iServlets, List<IServletMapping> iServletMappings) {
this.iServlets = iServlets;
this.iServletMappings = iServletMappings;
//分别将List数据存到为Map
for(IServlet iServlet: iServlets){
iservletMap.put(iServlet.getName(),iServlet.getClz());
}
for(IServletMapping iServletMapping: iServletMappings){
for (String pattern: iServletMapping.getPatterns()){
iservletmappingMap.put(pattern,iServletMapping.getName());
}
}
}
/*
<servlet-mapping>中的url-pattern ---> 找servlet-name --->
<servlet>中的servlet-name ---> 找servlet-class,
究极目标通过URL路径找到对应的class
(求class用处:由Class反射对应的对象)
*/
public String getClz(String pattern){
String name = iservletmappingMap.get(pattern);
String clz = iservletMap.get(name);
return clz;
}
}
码云地址:https://gitee.com/vampire-boom/http-server.
思考:
实现的只是一个简易的http服务器,基于BIO,每当有客户端来连接服务器都开一个新线程去处理,同步阻塞,因为内存资源很容易被消耗完,有很多客户端连接的时候就会出现在浏览器输入一个网址,一直显示加载的情况。
有一种不同的工作方式的io流NIO,NIO主要有三个重要的概念:selector,bytebuffer,channel,NIO开启一个线程可以处理多个客户端请求,监听到事件再去处理
还有一个地方可以完善,接受请求的时候我的是默认一次性接受到所有的数据,但其实会出现TCP的粘包问题,这里可改进