上一期的时候,我们完成了http报文的解析,但是解析出来的参数还散落在各处,我们返回给浏览器的内容也没进行规整。相信搞过web后端开发的同志们对HttpServletRequest和HttpServletResponse都不陌生,同样地,我们也搞一个Request和Response出来,方便起见,我们就直接写两个类,先不进行封装了。走起,老铁们!
首先,我们需要把解析出来的参数进行存放,细想一下,每个请求的参数肯定都不一样撒,而且有的参数还有多个,有多个也好办,问题是有时候参数是一对多的,比如兴趣爱好这种字段,欧美、日韩、亚洲无码、华人精品。。毕竟各位老铁们口味不同嘛。而且后面我们还需要根据字段拿到参数值,所以很自然就是用map来搞。首先弄个Request出来。
private Socket client;
/**请求方法*/
private String methodName;
/**请求路径*/
private String requestUrl;
/**请求协议版本*/
private String protocolVersion;
/**请求参数的封装*/
private Map<String,List<String>> parameterMap;
/**构造函数*/
public Request(Socket client) {
this.client = client;
/**初始化用于保存参数的容器*/
this.parameterMap = new HashMap<>();
/**用于接收流数据*/
byte[] bytes = new byte[1024 * 1024 * 50];
try {
int dataLength = client.getInputStream().read(bytes);
/**将流数据转化为字符串*/
String content = new String(bytes,0,dataLength);
parseHttpContent(content);
} catch (IOException e) {
e.printStackTrace();
}
}
构造函数里边,我们就从socket里把数据挨盘拿出来,然后做解析就好了。
最后,将参数放到map就可以了。
private void putKV2Map(String key, String value) {
/**如果存在key,则进行参数追加*/
if (parameterMap.containsKey(key)) {
parameterMap.get(key).add(value);
return;
}
/**不存在则创建*/
List<String> valueList = new ArrayList<>();
valueList.add(value);
parameterMap.put(key,valueList);
}
这样请求部分就暂时先这样,我们再开看响应部分。
当前版本,我们的响应逻辑是直接放在了服务器的启动类的doService方法里,写写demo还行,逻辑稍微复杂,就要一顿if else for循环的操作。。。不利于扩展,不利于重构,不方便使用。所以需要抽象一番。
public class Response {
/**通信管道*/
private Socket client;
/**数据回写*/
private BufferedWriter bufferedWriter;
/**写回给浏览器的内容*/
private StringBuilder contentToClient;
/**写回内容的长度*/
private int len;
public Response(Socket client) {
this.client = client;
try {
bufferedWriter = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
} catch (IOException e) {
e.printStackTrace();
System.out.println("响应创建失败,socket忙碌或已关闭");
}
}
根据前面的学习,我们知道往浏览器写数据,需要遵从格式 header,body的要求;我们还知道http有个statusCode,不同code对应不同的响应状态。于是
/**
* 根据响应值创建返回内容
* @param code
* @return
*/
private String createHeadInfo(int code) {
/**回送一个信息给浏览器,因为要交给浏览器显示,所以要按照http协议的报文格式组织数据*/
StringBuilder headerInfo = new StringBuilder();
headerInfo.append("HTTP/1.1").append(Constant.BLANK).append(code).append(Constant.CRLF);
/**根据不同status添加不同响应内容*/
switch (code) {
case 200:
headerInfo.append("OK").append(Constant.CRLF);
break;
case 404:
headerInfo.append("NOT FOUND").append(Constant.CRLF);
break;
case 500:
headerInfo.append("SERVER INTERNAL ERROR").append(Constant.CRLF);
break;
}
/**添加header剩余内容*/
headerInfo.append("Content-Type: text/html;charset=utf-8").append(Constant.CRLF)
.append("Date:"+new Date()).append(Constant.CRLF)
.append("Content-Length: "+len).append(Constant.CRLF)
.append("Server: LynServer/1.1").append(Constant.CRLF).append(Constant.CRLF);
return headerInfo.toString();
}
为了方便使用,我们给Response加一个print方法:每次调用都返回自身
/**
* 包装返回体内容
* @param content
* @return
*/
private Response print(String content) {
contentToClient.append(content);
len += content.getBytes().length;
return this;
}
这样,我们服务器的doService方法就变成如下这样了:
/**创建请求对象*/
Request request = new Request(client);
/**处理请求对象中的参数*/
String currentUserName = request.getParameterByName("username");
if (request.getRequestUrl().contains("/index")) {
/**创建响应对象*/
Response response = new Response(client);
response.println("<html>");
response.println("<body>");
response.println("<h1>您好,"+currentUserName+",欢迎使用 Lyn Server ");
response.println("</h1>");
response.println("</body>");
response.println("</html>");
response.send2Client(200);
到这里,大家是不是找到一些感觉了?我们在doService方法里,只关注请求和响应了。做到了细节的屏蔽,我们来做一下测试:
我们看下服务器给我们的返回:
这样我们就顺利获取到了服务器的返回信息。错误页面呢?
浏览器输入下 http://localhost:8008/sdfwer ,
最后试一下我们的表单页面:
http://localhost:8008/login.do
全部OK,到这里我们就完成了请求和响应的封装。我们回过头看看,服务器代码精简了不少,但现在还存在什么问题呢?
现在首页、登录、错误页面的逻辑还是在doService方法里,按照职责单一原则,服务器只应该负责转发,根据不同的url将请求派发给不同的XXX去处理,那这些XXX是啥呢?http那帮人给取了个名字——Servlet,就是运行在服务器端的Applet。下面一期,我们就把Servlet引入进来,进一步解耦我们的服务器实现。今天就到这里,我们下期再见!
本期代码地址:https://github.com/justein/lyn-server/tree/master/server-wrapper