系列文章目录
请求分析HttpServletRequest
请求业务处理DispatcherServlet
---注解机制的引入
---实体创建
---实体操控类Controller
---利用反射机制建立请求方法与请求路径的对应关系HandlerMapping
---使用properties文件创建后缀与MIME类型的对应mimeMapping
响应发送HttpServletResponse
html页面的编写
前言
Socket的学习最终往往就是编写一个HTTP服务器了,下面我就分几个系列将手写WebServer的各项工作写出来
一、WebServer的启动——WebServerApplication
public class WebServerApplication {
private ServerSocket serverSocket;
private ExecutorService threadPool;
public WebServerApplication(){
try{
System.out.println("正在启动服务器..");
serverSocket=new ServerSocket(8088);
threadPool=Executors.newFixedThreadPool(2);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){
try {
while(true){
System.out.println("等待客户端连接..");
Socket socket = serverSocket.accept();
System.out.println("一个客户端已连接!");
//启动一个线程负责与该客户端进行交互
ClientHandler handler = new ClientHandler(socket);
threadPool.execute(handler);//execute:处决,执行
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
WebServerApplication server=new WebServerApplication();
server.start();
}
}
二、处理每一个客户端的线程——ClientHandler
public class ClientHandler implements Runnable{
private Socket socket;
public ClientHandler(Socket socket){//构造器
this.socket=socket;
}
@Override
public void run() {
try {
//1、解析请求
HttpServletRequest request=new HttpServletRequest(socket);
HttpServletResponse response=new HttpServletResponse(socket);
//2、处理请求
DispatcherServlet servlet=new DispatcherServlet();
servlet.service(request,response);//往response里边装东西
//3发送响应
response.response();
} catch (IOException e) {
e.printStackTrace();
} catch (EmptyRequestException e) {
//什么也不做,等着finally里边关闭socket
} finally {
//http一次交互后断开连接
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
三、请求的解析HTTPServletRequest
明确请求发送过来是包含三部分的:请求行,消息头,请求正文,我们的HttpServletRequest就是要对其进行分析,将对应的信息提取出来存起来,便于后续处理
其中,请求头有三部分组成,请求方法 (SP)uri(SP) 协议,我们在方法中定义三个变量,分别用于存放这三个值
再然后,当使用get方法请求时,uri中会有参数部分,我们需要对uri进一步处理,将参数部分的每一组值存入Map里,供后续使用
再有就是正文,当 请求使用POST方法发起时,参数部分是位于请求正文的,因此我们每次解析请求的时候也需要解析正文,判断有无正文存在就是看消息头中有无“Content-Length”这个Key,有就可以进行后续处理,注意对参数部分的解析抽成方法,可以对位于uri和正文的消息都可以进行解析,代码复用
本次的技术细节主要有,HTTP协议在传输过程中使用的字符集是OSI 8859-1,是不支持中文的,当接收传过来的参数信息时有两种:
一种是位于uri的,另一种是位于正文的,
位于uri的参数传过来本身就是解析好的OSI 8859 -1编码,可以直接解析参数,注意在转换参数时先按UTF-8解码(这是抽出的解析参数方法应具有的一段代码)
对于正文中的参数,我们是以字节的形式读取的(此时数据是010101...),
首先要按OIS 8859-1转码为欧洲字符集(汉字的表达是%E6%9D%8E%E9%91%AB%E6%B3%A2)
之后在进行参数解码(方法中第一步就是再按UTF-8)解码
解码完毕后同一装入Map等待后续处理
public class HttpServletRequest {
//请求行的相关信息
private String method;//请求方式
private String uri;//抽象路径
private String protocol;//协议名称
private String requestURI;//存URI中"?"左侧的请求部分
private String queryString;//存uri中"?"右边的参数部分
//存放请求和参数部分的值
Map<String,String> parameters=new HashMap<>();
//消息头相关信息
private Map<String,String> headers=new HashMap<>();
private Socket socket;
/**
* 实例化请求对象的过程也是解析的过程
*/
public HttpServletRequest(Socket socket) throws IOException, EmptyRequestException {
this.socket=socket;
//1、1解析请求行
parseRequestLine();
//1.2解析消息头
parseHeaders();
//1.3解析消息正文
parseContent();
}
/*解析请求行*/
private void parseRequestLine() throws IOException, EmptyRequestException {
System.out.println("开始证明!!!");
String line =readLine();
//如果请求的是一个空串说明本次是空请求
if(line.isEmpty()){
throw new EmptyRequestException();
}
System.out.println(line);
//开始拆分
String[] temp=line.split("\\s");
method=temp[0];
uri=temp[1];
protocol=temp[2];
parseUri();//进一步解析uri
System.out.println("\tmethod:"+method);
System.out.println("\turi:"+uri);
System.out.println("\tprotocol:"+protocol);
}
/** 进一步解析uri*/
private void parseUri(){
/*
uri是有两种情况的:含参数的和不含有参数的
*/
String[] t1=uri.split("\\?");
requestURI=t1[0];
if(t1.length>1){//处理右边为空,当右边为空时split分割赋值的数组长度就是1
queryString=t1[1];
parseParameters(queryString);
}
System.out.println("\trequestURI:"+requestURI);
System.out.println("\tqueryString:"+queryString);
System.out.println("\tparameters:"+parameters);
}
/**
* 解析参数
* @param line 格式应当为:name1=value1&name2=value2&...
*/
private void parseParameters(String line){
//先进行转码工作
try {
line= URLDecoder.decode(line,"UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String[] t1=line.split("&");//重用t1数组
for(String data:t1){
String[] t3=data.split("=");
parameters.put(t3[0],t3.length>1?t3[1]:null);//判断属性值是否未赋值,
// 未赋值进行“=”分割得到的结果就是分割数组长度为1
}
}
/*解析消息头*/
private void parseHeaders() throws IOException {
while (true){
String line=readLine();
if(line.isEmpty()){
break;
}
System.out.println("\t\t消息头:"+line);
String[] tmp=line.split(":\\s");
headers.put(tmp[0],tmp[1]);
}
System.out.println("消息头集合header:"+headers);
}
/*解析消息正文*/
private void parseContent() throws IOException {
System.out.println("开始解析消息正文...");
//通过判断请求中的消息头是否包含Content-Length来判定是否包含有正文
if(headers.containsKey("Content-Length")){
int contentLength=Integer.parseInt(headers.get("Content-Length"));
System.out.println("正文长度:"+contentLength);
//基于消息头告知的长度创建一个字节数组,用于保存正文内容
byte[] contentData=new byte[contentLength];
//读取正文所有字节存入contentData数组中
InputStream in=socket.getInputStream();
in.read(contentData);
//获取消息头
String contentType=headers.get("Content-Type");
if("application/x-www-form-urlencoded".equals(contentType)){
String line =new String(contentData, StandardCharsets.ISO_8859_1);
System.out.println("正文内容:"+line);
parseParameters(line);
}
}
}
/** 重写readLine方法*/
private String readLine() throws IOException {//外调的方法采用抛出异常,要提醒外面有异常
InputStream in =socket.getInputStream();
StringBuilder builder=new StringBuilder();
int d;
char pre='a',cur='a';
while ((d=in.read())!=-1){
cur=(char)d;
if(pre==CR&&cur==LF){
break;
}
builder.append(cur);
pre=cur;
}
return builder.toString().trim();
}
public String getMethod() {
return method;
}
public String getUri() {
return uri;
}
public String getProtocol() {
return protocol;
}
/**
* 不能直接把Map返回回去,危险
* 这里根据key返回value
* @param name
* @return
*/
public String getHeader(String name){
return headers.get(name);
}
public String getRequestURI() {
return requestURI;
}
public String getQueryString() {
return queryString;
}
/**
* 根据给定的参数名获取对应的参数值
* @param name
* @return
*/
public String getParameter(String name){
return parameters.get(name);
}
}
总结
本次就只分析解析请求的部分,业务处理以及响应发送下期更新