要实现手写Web-Server,需要9大知识储备
由于前五个为Java基础知识,因此,我们直接从第六项开始学习,如果你已经掌握了这些知识,请直接跳过,去看后面的实现部份。
把Java类中的各种结构方法(类名、属性、构造器、方法)映射成一个个Java对象。反射技术可以对一个类进行解剖,反射是框架设计的灵魂。
假如我们有一个Person类
class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
然后我们想创建Person类的对象,传统方法是这样:
Person person = new Person("不识不知" , 18);
然而反射则需要先拿到这个类的信息,然后再创建对象
拿到这个类的信息,也就是拿到这个Class,共有三种方法:
//1、通过Object类的getClass()方法:(需要先实例化一个对象)
Class clz = person.getClass();
//2、通过对象实例方法获取对象:(需要先实例化一个对象)
Class clz = person.class;
//3、类的全路径:(不需要实例对象)
Class clz = Class.forName("包名.类名");
我们推荐的反射方法为第三种:Class.forName("完整路径");
有空构造函数的类:直接用字节码文件获取实例
Person person = (Person)clz.newInstance();//会调用空参构造器(如果没有则会报错)
Person person = (Person)clz.getConstructor().newInstance();//更推荐
含参构造函数的类:先获取构造器,再通过该构造器获取实例
//1、获取构造器
//这里的参数要注意与Person类构造函数的参数相对应
Constroctor const = clz.getConstructor(String.class,int.class);
//2、通过构造器对象的newInsttance方法进行对象的初始化
//对应位置填入参数
Object obj = const.newInstance("不识不知",18);
Person person = (Person)obj;
总结一下:
//含参构造器
Person person = (Person)clz.getConstructor(String.class,int.class).newInstance("不识不知",18);
//无参构造器
Person person = (Person)clz.getConstructor().newInstance();//更推荐
package Reflaction;
import java.lang.reflect.InvocationTargetException;
public class TestReflection {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, InvocationTargetException {
Class clz = Class.forName("Reflaction.Boy");
Person person = (Person) clz.getConstructor(String.class).newInstance("不识");
person.Go();
Class clz2 = Class.forName("Reflaction.Girl");
Person person2 = (Person) clz2.getConstructor(String.class).newInstance("不知");
person2.Go();
}
}
interface Person {
void Go();
}
class Boy implements Person {
String name;
public Boy(String name) {
this.name = name;
}
@Override
public void Go() {
System.out.println("I am Boy!" + "My name is " + name + " .");
}
}
class Girl implements Person {
String name;
public Girl(String name) {
this.name = name;
}
@Override
public void Go() {
System.out.println("I am Girl!" + "My name is " + name + " .");
}
}
运行结果
I am Boy!My name is 不识 .
I am Girl!My name is 不知 .
Process finished with exit code 0
Class.forName()的参数可以是字符串!这样就为我们设计框架提供了方便。
XML是一种通用的数据交换格式,它的平台无关性、语言无关性、系统无关性、给数据集成与交互带来了极大的方便。XML在不同的语言环境中解析方式都是一样的,只不过实现的语法不同而已。
例如这样一个xml文件:test.xml
<persons>
<person>
<name>至尊宝name>
<age>9000age>
person>
<person>
<name>白晶晶name>
<age>7000age>
person>
persons>
解释一下,XML文件就像一棵树,我画了一张图:
这张图的内容就是上面XML文件所包含的内容。
SAX解析是一种自上而下的流解析,整个文件从顶行向下走一遍,即完成解析任务。
解析步骤很简单,可分为以下四个步骤
为了便于理解以上步骤,我编写了下面的程序,整个步骤可以在main函数中体现,并且我配上了中文注释。解析的文件是XML包下的test.xml,内容为上面的至尊宝白晶晶,请配合使用。
package XML;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class TestXML {
public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {
//SAX解析 以后以下代码可以直接复制,不需要重复敲打。
//1、获取解析工厂
SAXParserFactory factory = SAXParserFactory.newInstance();
//2、从解析工厂获取解析器
SAXParser parser = factory.newSAXParser();
//3、编写处理器PersonHandler和解析类Person
//4、加载处理器
PersonHandler handler = new PersonHandler();
//5、开始解析
parser.parse(Objects.requireNonNull(Thread.currentThread().getContextClassLoader()
.getResourceAsStream("XML/test.xml"))
, handler);
//6、拿到数据
List<Person> persons = handler.getPersons();
//7、查看数据
for (Person p : persons) {
System.out.println(p.getName() + "--->" + p.getAge());
}
}
}
class PersonHandler extends DefaultHandler {
private List<Person> persons;//用于保存结果 可在startDocument()处初始化
private Person person;//用于每次解析
private String tag;//存储操作的标签
public List<Person> getPersons() {
return persons;
}
/**
* 仅在解析文档开始时被执行一次
*
* @throws SAXException
*/
@Override
public void startDocument() throws SAXException {
System.out.println("---解析文档开始---");
//初始化容器
persons = new ArrayList<>();
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
System.out.println(qName + "--->解析开始");
if (null != qName) {
tag = qName;//存储当前的标签
if (tag.equals("person")) {//当开始解析一个person时创建一个Person对象
person = new Person();
}
}
}
/**
* 每次解析的内容模块
*
* @param ch
* @param start
* @param length
* @throws SAXException
*/
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
String contents = new String(ch, start, length).trim();
if (null != tag) {//处理tag为空的问题
if (tag.equals("name")) {
person.setName(contents);
} else if (tag.equals("age")) {
if (contents.length() > 0) {
person.setAge(Integer.valueOf(contents));
}
}
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
System.out.println(qName + "--->解析结束");
tag = null;//解析完毕丢弃标签
if (qName.equals("person")) {//当结束解析一个person时将person加入到容器中
persons.add(person);
}
}
@Override
public void endDocument() throws SAXException {
super.endDocument();
System.out.println("---解析文档结束---");
}
}
/**
* 与XML文件内容格式相对应的Person解析类
*/
class Person {
String name;
int age;
public Person() {
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
运行结果:
---解析文档开始---
persons--->解析开始
person--->解析开始
name--->解析开始
name--->解析结束
age--->解析开始
age--->解析结束
person--->解析结束
person--->解析开始
name--->解析开始
name--->解析结束
age--->解析开始
age--->解析结束
person--->解析结束
persons--->解析结束
---解析文档结束---
至尊宝9000
白晶晶7000
Process finished with exit code 0
现在考虑这样一个xml文件:test2.xml
<web-app>
<servlet>
<servlet-name>loginservlet-name>
<servlet-class>XML.WEBXML.MyClasses.LoginServletservlet-class>
servlet>
<servlet>
<servlet-name>regservlet-name>
<servlet-class>XML.WEBXML.MyClasses.RegisterServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>loginservlet-name>
<url-pattern>/loginurl-pattern>
<url-pattern>/gurl-pattern>
servlet-mapping>
<servlet-mapping>
<servlet-name>regservlet-name>
<url-pattern>/regurl-pattern>
servlet-mapping>
web-app>
与之前不同的是:
servlet块所对应的解析类类
package XML.WEBXML;
/**
* 对应的XML代码块为
*
* login
* com.shsxt.LoginServlet
*
*/
public class Entity {
//类名
private String name;
//类路径
private String clz;
public Entity() {
}
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;
}
}
servlet-mapping块的解析类
package XML.WEBXML;
import java.util.HashSet;
import java.util.Set;
/**
* 对应的XML代码块为
*
* login
* /login
* /g
*
*/
public class Mapping {
//URL
private Set<String> patterns;
//类名
private String name;
public Mapping() {
patterns = new HashSet<>();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<String> getPatterns() {
return patterns;
}
public void addPattern(String pattern) {
this.patterns.add(pattern);
}
}
LoginServlet和RegisterServlet都要实现本接口
package XML.WEBXML.MyClasses;
/**
* 配合反射使用的接口
*/
public interface Servlet {
void service();
}
package XML.WEBXML.MyClasses;
public class RegisterServlet implements Servlet {
@Override
public void service() {
System.out.println("RegisterServlet");
}
}
package XML.WEBXML.MyClasses;
public class LoginServlet implements Servlet {
@Override
public void service() {
System.out.println("LoginServlet");
}
}
WebContext是一个映射器,完成一个URL到类路径的映射。
package XML.WEBXML;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 映射器
*/
public class WebContext {
private List<Mapping> mappings = null;
private List<Entity> entities = null;
//key-->servlet-name value--->servlet-class
private Map<String, String> mappingsMap = new HashMap<>();
//key-->url-pattern value--->servlet-name
private Map<String, String> entitiesMap = new HashMap<>();
public WebContext(List<Mapping> mappings, List<Entity> entities) {
this.mappings = mappings;
this.entities = entities;
build();
}
/**
* 构建映射
*/
private void build() {
for (Entity e :
entities) {
entitiesMap.put(e.getName(), e.getClz());
}
for (Mapping m :
mappings) {
for (String s :
m.getPatterns()) {
mappingsMap.put(s, m.getName());
}
}
}
/**
* 通过URL找类路径
*
* @param pattern URL
* @return 类路径
*/
public String getClz(String pattern) {
String name = mappingsMap.get(pattern);
String Clz = entitiesMap.get(name);
return Clz;
}
}
这里的解析器和上一节的类似,来看看我们做了何种修改。
package XML.WEBXML;
import XML.WEBXML.MyClasses.Servlet;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class TestServlet {
public static void main(String[] args) throws Exception {
//SAX解析 以后以下代码可以直接复制,不需要重复敲打。
//1、获取解析工厂
SAXParserFactory factory = SAXParserFactory.newInstance();
//2、从解析工厂获取解析器
SAXParser parser = factory.newSAXParser();
//3、编写处理器PersonHandler和解析类Person
//4、加载处理器
WebHandler handler = new WebHandler();
//5、开始解析
parser.parse(Objects.requireNonNull(Thread.currentThread().getContextClassLoader()
.getResourceAsStream("XML/WEBXML/test2.xml"))
, handler);
//URL--->类路径的映射器
WebContext webContext = new WebContext(handler.getMappings(), handler.getEntities());
//假设你输入了/reg
String name = webContext.getClz("/reg");
//开始反射
Class clz = Class.forName(name);
Servlet servlet = (Servlet) clz.getConstructor().newInstance();
servlet.service();
}
}
class WebHandler extends DefaultHandler {
private List<Mapping> mappings;//用于保存结果 可在startDocument()处初始化
private List<Entity> entities;//用于保存结果 可在startDocument()处初始化
private Mapping mapping;//用于每次解析
private Entity entity;//用于每次解析
private String father_tag;//存储标签名
private String tag;//存储属性标签
public List<Mapping> getMappings() {
return mappings;
}
public List<Entity> getEntities() {
return entities;
}
/**
* 仅在解析文档开始时被执行一次
*
* @throws SAXException
*/
@Override
public void startDocument() throws SAXException {
System.out.println("---解析文档开始---");
//初始化容器
mappings = new ArrayList<>();
entities = new ArrayList<>();
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
System.out.println(qName + "--->解析开始");
if (null != qName) {
tag = qName;//存储当前属性
switch (tag) {
case "servlet-mapping":
mapping = new Mapping();
father_tag = qName;//存储当前标签
break;
case "servlet":
entity = new Entity();
father_tag = qName;//存储当前标签
break;
default:
break;
}
}
}
/**
* 每次解析的内容模块
*
* @param ch
* @param start
* @param length
* @throws SAXException
*/
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
String contents = new String(ch, start, length).trim();
if (null != tag && null != father_tag) {//处理tag fathertag为空的问题
switch (father_tag) {
case "servlet":
switch (tag) {
case "servlet-name":
entity.setName(contents);
break;
case "servlet-class":
entity.setClz(contents);
break;
}
break;
case "servlet-mapping":
switch (tag) {
case "servlet-name":
mapping.setName(contents);
break;
case "url-pattern":
mapping.addPattern(contents);
break;
}
break;
}
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
System.out.println(qName + "--->解析结束");
tag = null;//解析完毕丢弃tag
switch (qName) {
case "servlet-mapping":
mappings.add(mapping);
father_tag = null;//解析完毕丢弃标签
break;
case "servlet":
entities.add(entity);
father_tag = null;//解析完毕丢弃标签
break;
default:
}
}
@Override
public void endDocument() throws SAXException {
super.endDocument();
System.out.println("---解析文档结束---");
}
}
---解析文档开始---
web-app--->解析开始
servlet--->解析开始
servlet-name--->解析开始
servlet-name--->解析结束
servlet-class--->解析开始
servlet-class--->解析结束
servlet--->解析结束
servlet--->解析开始
servlet-name--->解析开始
servlet-name--->解析结束
servlet-class--->解析开始
servlet-class--->解析结束
servlet--->解析结束
servlet-mapping--->解析开始
servlet-name--->解析开始
servlet-name--->解析结束
url-pattern--->解析开始
url-pattern--->解析结束
url-pattern--->解析开始
url-pattern--->解析结束
servlet-mapping--->解析结束
servlet-mapping--->解析开始
servlet-name--->解析开始
servlet-name--->解析结束
url-pattern--->解析开始
url-pattern--->解析结束
servlet-mapping--->解析结束
web-app--->解析结束
---解析文档结束---
RegisterServlet
Process finished with exit code 0
超文本标记语言(英语:HyperText Markup Language,简称:HTML)是一种用于创建网页的标准标记语言,HTML 运行在浏览器上,由浏览器来解析。
目前一个完整的网页一般包括三个部分。
JavaScript,简称JS,是动态语言。
<html>---开始标签
<head>---网页上的控制信息
<title>---页面标题
title>
head>
<body>
页面显示的内容
body>
html>---带/的都是结束标签
例子:form表单
<html>
<head>
<title>
登录
title>
head>
<body>
<h1>表单的使用h1>
<pre>
post:提交,其内容被放在http请求内容体中,因此量大,请求参数不写在URL中,相对安全。<br>
get:获取,本意是获取服务器上某一个路径的文件,因此请求参数写在URL内,所以相对不安全。<br>
action:所请求的web服务器资源,即URL。<br>
name:后端(服务器JavaEE)使用的,因此必须存在,否则数据不能提交。<br>
id:前端(JavaScript)使用,因此可以不存在,不影响数据提交。<br>
在method为post的情况下,submit是提交到action所指向的服务器地址上,内容体的数据结构为<br>
(Key,Value)。其中key是name的值,value是控件输入的值。
pre>
<form method="post" action="http://localhost:8888/index.html">
用户名:<input type="text" name="uname" id="uname"/>
密码:<input type="password" name="pwd" id="pwd"/>
<input type="submit" value="登录"/>
form>
body>
html>
当我们输入用户名和密码,点击登录按钮后,就会向action的地址发送http请求了,并且这个请求的方法是post,我们不妨点击试一下。
可以看到,浏览器顶部的网址正是我们action的内容。这代表着,我们的浏览器已经向这个地址http://localhost:8888/index.html发送了一个http包,包的内容是刚刚我们所填写的用户名和密码。
我们还没有编写服务器,因此网页是无法访问,不要急,马上就到服务器部分了。
超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的WWW文件都必须遵守这个标准。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。
对于要手写服务器的我们来说,只需要知道,GET方法和POST方法不一样就够了,我们后面只需要用到这两个方法。
第一行为请求行,接下来为请求头部,隔一个空行接下来为请求内容。
http请求报文的结构如下:
说明 | 位置1 | 位置2 | 位置3 | 位置4 | 位置5 | 位置6 | 位置7 |
---|---|---|---|---|---|---|---|
请求行 | 请求方法(GET/POST) | 空格 | URL | 空格 | 协议版本 | 回车符 | 换行符 |
请求头部 | 头部字段名 | : | 值 | 回车符 | 换行符 | ||
… | |||||||
请求头部 | 头部字段名 | : | 值 | 回车符 | 换行符 | ||
空行 | 回车符 | 换行符 | |||||
请求内容 | xxxx | xxxx | xxxx | xxxx | xxxx | xxxx | xxxx |
… |
这里是一个POST方法的报文
POST /index.html HTTP/1.1
Host: localhost:8888
Connection: keep-alive
Content-Length: 24
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; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: _ga=GA1.1.1892484369.1559541150
uname=greathd&pwd=123456
这里有一个GET方法的报文
GET /index.html HTTP/1.1
Host: localhost:8888
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: _ga=GA1.1.1892484369.1559541150
可以看到,GET方式的请求一般不包含”请求内容”部分,请求数据以地址的形式表现在请求行。地址链接如下:
HTTP响应也由三个部分组成,分别是:状态行、响应头部、响应内容。
如下所示,HTTP响应的格式与请求的格式十分类似:
状态行(status-line)
响应头部(headers)
空行(blank line)
响应内容(response-body)
以下是一个响应报文
HTTP/1.1 200 OK
Date: Sat, 31 Dec 2005 23:59:59 GMT
Content-Type: text/html;charset=ISO-8859-1
Content-Length: 122
<html>
<head>
<title>Wrox Homepage</title>
</head>
<body>
<!-- body goes here -->
</body>
</html>
如你所见,响应报文与请求报文唯一的区别在第一行。
说明 | 位置1 | 位置2 | 位置3 | 位置4 | 位置5 | 位置6 | 位置7 |
---|---|---|---|---|---|---|---|
状态行 | 协议/版本 | 空格 | 状态码 | 空格 | 状态说明 | 回车符 | 换行符 |
响应头部 | 头部字段名 | : | 值 | 回车符 | 换行符 | ||
… | |||||||
响应头部 | 头部字段名 | : | 值 | 回车符 | 换行符 | ||
空行 | 回车符 | 换行符 | |||||
响应内容 | xxxx | xxxx | xxxx | xxxx | xxxx | xxxx | xxxx |
… |
状态代码由三位数字组成,第一个数字定义了响应的类别,且有五种可能取值。
状态码 | 含义 |
---|---|
1xx | 指示信息–表示请求已接收,继续处理。 |
2xx | 成功–表示请求已被成功接收、理解、接受。 |
3xx | 重定向–要完成请求必须进行更进一步的操作。 |
4xx | 客户端错误–请求有语法错误或请求无法实现。 |
5xx | 服务器端错误–服务器未能实现合法的请求。 |
现在有一些常见的状态代码、状态描述的说明如下。
状态码 | 状态描述 | 含义 |
---|---|---|
200 | OK | 客户端请求成功。 |
400 | Bad Request | 客户端请求有语法错误,不能被服务器所理解。 |
401 | Unauthorized | 请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。 |
403 | Forbidden | 服务器收到请求,但是拒绝提供服务。 |
404 | Not Found | 请求资源不存在,举个例子:输入了错误的URL。 |
500 | Internal Server Error | 服务器发生不可预期的错误。 |
503 | Server Unavailable | 服务器当前不能处理客户端的请求,一段时间后可能恢复正常,举个例子:HTTP/1.1 200 OK(CRLF)。 |
好了,知道了这些以后,就可以开始手写服务器了!
由于HTTP协议的底层是TCP/IP,因此,我们使用ServerSocket接收连接,然后读取请求内容。
示例代码
package SERVER;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 目标:使用ServerSocket建立与浏览器的连接,获取请求协议。
*/
public class Server {
private ServerSocket serverSocket;
public static void main(String[] args) {
Server server = new Server();
server.start();
}
/**
* 启动服务
*/
public void start(){
try {
serverSocket= new ServerSocket(8888);
receive();
} catch (IOException e) {
e.printStackTrace();
System.out.println("服务器启动失败...");
}
}
/**
* 连接处理
*/
public void receive(){
try {
Socket client = serverSocket.accept();
System.out.println("一个客户端建立了连接...");
//获取请求协议
InputStream is = client.getInputStream();
byte[] datas = new byte[1024*1024];//1M的空间
int len = is.read(datas);//全部读取
String requestinfo = new String(datas,0,len);//构造成字符串
System.out.println(requestinfo);
} catch (IOException e) {
e.printStackTrace();
System.out.println("客户端错误...");
}
}
/**
* 停止服务
*/
public void stop(){
}
}
运行程序后,使用RESTer模拟http请求,点send后可以看到输出结果
输出结果:
一个客户端建立了连接...
GET /index.html HTTP/1.1
Host: localhost:8888
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: _ga=GA1.1.1892484369.1559541150; _gid=GA1.1.2100199394.1559657416
Process finished with exit code 0
修改后的reveive方法代码如下:
/**
* 连接处理
*/
public void receive() {
try {
Socket client = serverSocket.accept();
System.out.println("一个客户端建立了连接...");
//获取请求协议
InputStream is = client.getInputStream();
byte[] datas = new byte[1024 * 1024];//1M的空间
int len = is.read(datas);//全部读取
String requestinfo = new String(datas, 0, len);//构造成字符串
System.out.println(requestinfo);
//StringBuilder更方便构造String 这里构造的是响应内容
StringBuilder content = new StringBuilder();
content.append("");
content.append("");
content.append("" );
content.append("服务器响应成功");
content.append("");
content.append("");
content.append("");
content.append("bsbz server终于回来了...");
content.append("");
content.append("");
//注意这里size要计算的一定是字节数而非字符数!
int size = content.toString().getBytes().length;
StringBuilder responsinfo = new StringBuilder();
String blank = " ";
String CRLF = "\r\n";
//返回
//1、响应行:HTTP/1.1 200 OK
responsinfo.append("HTTP/1.1").append(blank);
responsinfo.append(200).append(blank);
responsinfo.append("OK").append(CRLF);
//2、响应头:(最后一行存在空行)
/**
* Date: Sat, 31 Dec 2005 23:59:59 GMT
* Server:bsbz Server/0.0.1;charset=GBK
* Content-Type:text/html;charset=ISO-8859-1
* Content-Length: 122
*/
responsinfo.append("Date:").append(new Date()).append(CRLF);
responsinfo.append("Server:").append("bsbz Server/0.0.1;charset=GBK").append(CRLF);
responsinfo.append("Content-Type:").append("text/html;charset=ISO-8859-1").append(CRLF);
responsinfo.append("Content-Length:").append(size).append(CRLF);
responsinfo.append(CRLF);
//3、正文:
responsinfo.append(content.toString());
//4、写出到客户端
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
bw.write(responsinfo.toString());
bw.flush();
} catch (IOException e) {
e.printStackTrace();
System.out.println("客户端错误...");
}
}
这是一个封装类,负责构造和发送相应信息,经过封装后,我们可以只关注内容和状态码。
package SERVER;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Date;
public class Response {
private BufferedWriter bw;
//协议头信息(状态行与请求头 回车)
private StringBuilder headinfo;
//正文
private StringBuilder content;
private int len = 0;
private final String BLANK = " ";
private final String CRLF = "\r\n";
private Response() {
content = new StringBuilder();
headinfo = new StringBuilder();
len = 0;
}
public Response(Socket client) {
this();
try {
bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
} catch (IOException e) {
e.printStackTrace();
}
}
public Response(OutputStream os) {
this();
bw = new BufferedWriter(new OutputStreamWriter(os));
}
//动态添加内容
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;
}
//推送响应信息
public void pushToBrowser(int code) throws IOException {
//构建头信息
creatHeadInfo(code);
//加头信息
bw.append(headinfo);
//加内容信息
bw.append(content);
//推送
bw.flush();
}
//构建头信息
private void creatHeadInfo(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 505:
headinfo.append("SERVER ERROR").append(CRLF);
break;
}
//2、响应头:(最后一行存在空行)
/**
* Date: Sat, 31 Dec 2005 23:59:59 GMT
* Server:bsbz Server/0.0.1;charset=GBK
* Content-Type:text/html;charset=ISO-8859-1
* Content-Length: 122
*/
headinfo.append("Date:").append(new Date()).append(CRLF);
headinfo.append("Server:").append("bsbz Server/0.0.1;charset=GBK").append(CRLF);
headinfo.append("Content-Type:").append("text/html;charset=ISO-8859-1").append(CRLF);
headinfo.append("Content-Length:").append(len).append(CRLF);
headinfo.append(CRLF);
}
}
修改后的receive方法,先构建内容,再给定状态码,最后由封装的响应类准备头部信息并发送。
代码如下:
/**
* 连接处理
*/
public void receive() {
try {
Socket client = serverSocket.accept();
System.out.println("一个客户端建立了连接...");
//获取请求协议
InputStream is = client.getInputStream();
byte[] datas = new byte[1024 * 1024];//1M的空间
int len = is.read(datas);//全部读取
String requestinfo = new String(datas, 0, len);//构造成字符串
System.out.println(requestinfo);
//关注了内容
Response response = new Response(client);
response.print("");
response.print("");
response.print("" );
response.print("服务器响应成功");
response.print("");
response.print("");
response.print("");
response.print("bsbz server终于回来了...");
response.print("");
response.print("");
//关注了状态码
response.pushToBrowser(200);
} catch (IOException e) {
e.printStackTrace();
System.out.println("客户端错误...");
}
}
这部分我们要做的就是通过分解字符串来获取method、URL、请求参数,这里注意GET方法的请求参数在URL后,POST方法的请求参数还可以在请求体中。
代码如下:
package SERVER;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
public class Request {
//协议信息
private String requestInfo;
//请求方式
private String method;
//请求URL
private String url;
//请求参数
private String queryStr;
private final String BLANK = " ";
private final String CRLF = "\r\n";
private Request() {
}
public Request(Socket client) throws IOException {
this(client.getInputStream());
}
public Request(InputStream is) {
byte[] datas = new byte[1024 * 1024];
int len;
try {
len = is.read(datas);
requestInfo = new String(datas, 0, len);//构造成字符串
System.out.println(requestInfo);
} catch (IOException e) {
e.printStackTrace();
}
//分解字符串
parseRequestInfo();
}
private void parseRequestInfo() {
System.out.println("-----开始分解-----");
System.out.println("--1、获取请求方式--");
this.method = this.requestInfo.substring(0, this.requestInfo.indexOf("/")).trim().toLowerCase();
System.out.println(method);
System.out.println("--2、获取请求的URL--");//若包含请求参数,则url为?之前
//1)获取/的位置
int startIdx = this.requestInfo.indexOf("/") + 1;
//2)获取HTTP/的位置
int endIdx = this.requestInfo.indexOf("HTTP/");
//3)分割字符串
this.url = this.requestInfo.substring(startIdx, endIdx).trim();
//4)获取?的位置
int queryIdx = this.url.indexOf("?");
if (queryIdx >= 0) {//表示存在请求参数
String[] urlArray = this.url.split("\\?");
this.url = urlArray[0].trim();
queryStr = urlArray[1].trim();
}
System.out.println(this.url);
System.out.println("---3获取请求体参数,如果方法为GET则已经获取,如果是POST可能在请求体中---");
if (method.equals("post")) {
String qStr = this.requestInfo.substring(this.requestInfo.lastIndexOf(CRLF)).trim();
if (null == queryStr) {
queryStr = qStr;
} else {
queryStr += "&" + qStr;
}
}
queryStr = null == queryStr ? "" : queryStr;
System.out.println(method + "--->" + url + "--->" + queryStr);
}
}
运行结果如下:
一个客户端建立了连接...
GET /index.html?hello=3&why=5 HTTP/1.1
Host: localhost:8888
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: _ga=GA1.1.1892484369.1559541150
-----开始分解-----
--1、获取请求方式--
get
--2、获取请求的URL--
index.html
---3获取请求体参数,如果方法为GET则已经获取,如果是POST可能在请求体中---
get--->index.html--->hello=3&why=5
Process finished with exit code 0
以下代码可以将String转化编码格式,调用时传入源String和编码格式即可。
编码格式如"“utf-8”。
/**
* 处理中文
* @param value
* @param enc
* @return
*/
private String decode(String value, String enc) {
try {
return java.net.URLDecoder.decode(value, enc);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return value;
}
这里要注意,一个Key可能会对应多个Value值,比如传入参数爱好,爱好可以有多个。
修改后的Request类如下
package SERVER;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.util.*;
public class Request {
//协议信息
private String requestInfo;
//请求方式
private String method;
//请求URL
private String url;
//请求参数
private String queryStr;
private Map<String, List<String>> parameterMap;
private final String BLANK = " ";
private final String CRLF = "\r\n";
public Request(Socket client) throws IOException {
this(client.getInputStream());
}
public Request(InputStream is) {
parameterMap = new HashMap<>();
byte[] datas = new byte[1024 * 1024];
int len;
try {
len = is.read(datas);
requestInfo = new String(datas, 0, len);//构造成字符串
System.out.println(requestInfo);
} catch (IOException e) {
e.printStackTrace();
}
//分解字符串
parseRequestInfo();
}
private void parseRequestInfo() {
System.out.println("-----开始分解-----");
System.out.println("--1、获取请求方式--");
this.method = this.requestInfo.substring(0, this.requestInfo.indexOf("/")).trim().toLowerCase();
System.out.println(method);
System.out.println("--2、获取请求的URL--");//若包含请求参数,则url为?之前
//1)获取/的位置
int startIdx = this.requestInfo.indexOf("/") + 1;
//2)获取HTTP/的位置
int endIdx = this.requestInfo.indexOf("HTTP/");
//3)分割字符串
this.url = this.requestInfo.substring(startIdx, endIdx).trim();
//4)获取?的位置
int queryIdx = this.url.indexOf("?");
if (queryIdx >= 0) {//表示存在请求参数
String[] urlArray = this.url.split("\\?");
this.url = urlArray[0].trim();
queryStr = urlArray[1].trim();
}
System.out.println(this.url);
System.out.println("---3获取请求体参数,如果方法为GET则已经获取,如果是POST可能在请求体中---");
if (method.equals("post")) {
String qStr = this.requestInfo.substring(this.requestInfo.lastIndexOf(CRLF)).trim();
if (null == queryStr) {
queryStr = qStr;
} else {
if (qStr.length() > 0)
queryStr += "&" + qStr;
}
}
queryStr = null == queryStr ? "" : queryStr;
System.out.println(method + "--->" + url + "--->" + queryStr);
//转成Map fav=1&fav=2&uname=lhd&age=18&others=
convertMap();
}
//处理请求参数为Map
private void convertMap() {
//1、分割字符串
String[] keyValues = this.queryStr.split("&");
for (String queryStr : keyValues) {
//2、再次分割字符串 =
String[] kv = queryStr.split("=");
kv = Arrays.copyOf(kv, 2);//永远保证有两个长度
//获取key和value
String key = kv[0];
String value = kv[1] == null ? "" : decode(kv[1], "utf-8");
//存储到map中
if (!parameterMap.containsKey(key)) {//第一次
parameterMap.put(key, new ArrayList<>());
}
parameterMap.get(key).add(value);
}
}
/**
* 处理中文
* @param value
* @param enc
* @return
*/
private String decode(String value, String enc) {
try {
return java.net.URLDecoder.decode(value, enc);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return value;
}
/**
* 通过name获取对应的多个值
*
* @param name
* @return
*/
public String[] getParameterValues(String name) {
List<String> values = this.parameterMap.get(name);
if (null == values || values.size() < 1) {
return null;
}
return values.toArray(new String[0]);
}
/**
* 通过name获取对应的1个值
*
* @param name
* @return
*/
public String getParameterValue(String name) {
String[] values = getParameterValues(name);
return values == null ? null : values[0];
}
public String getMethod() {
return method;
}
public String getUrl() {
return url;
}
public String getQueryStr() {
return queryStr;
}
}
目标:解耦业务代码
首先我们需要一个接口:Servlet
package Servlet;
/**
* 服务器小脚本接口
*/
public interface Servlet {
void service(Request request, Response response);
}
让我们的业务处理类来实现这个接口,如注册类:
package Servlet;
public class RegisterServlet implements Servlet {
@Override
public void service(Request request, Response response) {
response.print("注册成功了!");
}
}
同样的,登录类也实现这个接口:
package Servlet;
public class LoginServlet implements Servlet {
@Override
public void service(Request request, Response response) {
//业务代码
response.print("");
response.print("");
response.print("" );
response.print("服务器响应成功");
response.print("");
response.print("");
response.print("");
response.print("欢迎回来" + request.getParameterValue("uname"));
response.print("");
response.print("");
}
}
可以看到,登录类里我把原先的业务逻辑放进来了,因此,server中的receive方法就相应的变成了这样:
/**
* 连接处理
*/
public void receive() {
try {
Socket client = serverSocket.accept();
System.out.println("一个客户端建立了连接...");
//获取请求协议
Request request = new Request(client);
//获取响应协议
Response response = new Response(client);
//通过Servlet解耦了业务代码
Servlet servlet = null;
if (request.getUrl().equals("login")) {
servlet = new LoginServlet();
} else if (request.getUrl().equals("reg")) {
servlet = new RegisterServlet();
} else {
//首页...
}
servlet.service(request, response);
//关注了状态码
response.pushToBrowser(200);
} catch (IOException e) {
e.printStackTrace();
System.out.println("客户端错误...");
}
}
这样,我们就实现了业务处理和请求响应的解耦。
还记得我们最开始所讲的webxml解析和反射吗?现在要派上用场了。
我们的webxml.xml内容如下:
<web-app>
<servlet>
<servlet-name>LoginServletservlet-name>
<servlet-class>Servlet.LoginServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>LoginServletservlet-name>
<url-pattern>/loginurl-pattern>
<url-pattern>/gurl-pattern>
servlet-mapping>
<servlet>
<servlet-name>RegisterServletservlet-name>
<servlet-class>Servlet.RegisterServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>RegisterServletservlet-name>
<url-pattern>/regurl-pattern>
servlet-mapping>
<servlet>
<servlet-name>OtherServletservlet-name>
<servlet-class>Servlet.OtherServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>OtherServletservlet-name>
<url-pattern>/ourl-pattern>
servlet-mapping>
web-app>
首先是用于解析xml文件的类,我们不用过多文件了,在这里,放到一个.java文件里。
WebHandler.java
package Servlet;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 目标:
* 1、使用ServerSocket建立与浏览器的连接,获取请求协议。
* 2、返回响应协议
* 3、内容可以动态添加
* 4、关注状态码,拼接好响应的协议信息
*/
public class Server {
private ServerSocket serverSocket;
public static void main(String[] args) {
Server server = new Server();
server.start();
}
/**
* 启动服务
*/
public void start() {
try {
serverSocket = new ServerSocket(8888);
receive();
} catch (IOException e) {
e.printStackTrace();
System.out.println("服务器启动失败...");
}
}
/**
* 连接处理
*/
public void receive() {
try {
Socket client = serverSocket.accept();
System.out.println("一个客户端建立了连接...");
//获取请求协议
Request request = new Request(client);
//获取响应协议
Response response = new Response(client);
//通过Servlet解耦了业务代码
Servlet servlet = WebApp.getServletFromURL(request.getUrl());
if (null != servlet) {
servlet.service(request, response);
//关注了状态码
response.pushToBrowser(200);
} else {
//错误
response.pushToBrowser(404);
}
} catch (IOException e) {
e.printStackTrace();
System.out.println("客户端错误...");
}
}
/**
* 停止服务
*/
public void stop() {
}
}
是不是非常简单?
到目前为止,我们都是单线程,为了提高性能,我们来设计分发器。
package Servlet;
import java.io.IOException;
import java.net.Socket;
public class Dispatcher implements Runnable {
private Socket client;
Request request;
Response response;
public Dispatcher(Socket client) {
this.client = client;
//获取请求协议
//获取响应协议
try {
request = new Request(client);
} catch (IOException e) {
e.printStackTrace();
this.release();
}
response = new Response(client);
}
@Override
public void run() {
try {
//通过Servlet解耦了业务代码
Servlet servlet = WebApp.getServletFromURL(request.getUrl());
if (null != servlet) {
servlet.service(request, response);
//关注了状态码
response.pushToBrowser(200);
} else {
//没找到
response.pushToBrowser(404);
}
} catch (Exception e) {
//错误
try {
response.pushToBrowser(500);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
private void release() {
try {
client.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
那么相应的,Server端的receive就相应变成
/**
* 连接处理
*/
public void receive() {
while (isRuning) {
try {
Socket client = serverSocket.accept();
System.out.println("一个客户端建立了连接...");
//多线程处理
new Thread(new Dispatcher(client)).start();
} catch (IOException e) {
e.printStackTrace();
System.out.println("客户端错误...");
}
}
}
package Servlet;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
public class Dispatcher implements Runnable {
private Socket client;
Request request;
Response response;
public Dispatcher(Socket client) {
this.client = client;
//获取请求协议
//获取响应协议
try {
request = new Request(client);
} catch (IOException e) {
e.printStackTrace();
this.release();
}
response = new Response(client);
}
@Override
public void run() {
try {
if (request.getUrl() == null || request.getUrl().equals("")) {
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("Servlet/index.html");
BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
String line = null;
while ((line = br.readLine()) != null) {
response.println(line);
}
br.close();
response.pushToBrowser(200);
return;
}
//通过Servlet解耦了业务代码
Servlet servlet = WebApp.getServletFromURL(request.getUrl());
if (null != servlet) {
servlet.service(request, response);
//关注了状态码
response.pushToBrowser(200);
} else {
//没找到
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("Servlet/error.html");
BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
String line = null;
while ((line = br.readLine()) != null) {
response.println(line);
}
br.close();
response.pushToBrowser(404);
}
} catch (Exception e) {
e.printStackTrace();
//错误
try {
response.pushToBrowser(500);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
private void release() {
try {
client.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
完结撒花!