服务端的两大作用:
1.和浏览器通过HTTP协议进行数据交互
2.管理网络资源
要理解服务端的工作流程首先要清楚HTTP协议的具体内容,可以参考以下链接查看HTTP协议的具体内容HTTP协议简介_剑剑剑剑啊的博客-CSDN博客。
![](https://img-blog.csdnimg.cn/08b9eabf6e4649e1983fd8ac3e588af9.png)
浏览器发送的一个请求中包含三个部分:请求行、消息头和消息正文。
请求行中包含三个部分,分别是:请求方式(SP)抽象路径(SP)协议版本(CRLF) 注:SP是空格,CRLF是回车换行
在GET请求中,浏览器表单提交的参数会直接拼接在抽象路径后面;而POST请求方式,参数是放在消息正文中的。
服务端的响应中包含三个部分:状态行、响应头、响应正文。
过程详解
服务端三大功能:解析请求,处理请求,发送响应。解析请求的主要工作就是将浏览器发送过来的请求拆分,并分别保存。处理请求的主要工作就是根据浏览器请求的参数处理对应的业务并设置好响应对象中的参数,最后将响应发送给浏览器。
下面我们对流程的详细过程进行描述:
以浏览器发送一个GET请求为例
服务端接收到请求后创建一个ClientHandler对象,并将获取到的连接socket传入该对象中
ClientHandler handler = new ClientHandler(socket);
在ClientHandler中创建请求对象HttpServletRequest
//1.解析请求
HttpServletRequest request = new HttpServletRequest(socket);
1.解析请求
接下来在HttpServletRequest对象中有三个解析请求的方法:解析请求行,解析消息头,解析消息正文,这里将这三个方法直接放在请求对象的构造方法中。
public HttpServletRequest(Socket socket) throws IOException, EmptyRequestException {
this.socket = socket;
//解析请求行
parseRequestLine();
//解析消息头
parseHeaders();
//解析消息正文
parseContent();
}
1.1解析请求行
解析请求行:按照空格拆分,将请求方式、抽象路径、协议版本分别保存到属性method、uri、protocol中。
private void parseRequestLine() throws IOException, EmptyRequestException {
String line = readLine();//这是读取一行请求的方法
if (line.isEmpty()) {//如果请求行没有内容,则说明本次为空请求
throw new EmptyRequestException();
}
System.out.println("请求行内容:" + line);
//将请求行按照空格拆分为三部分,并分别保存
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
parseUri();//进一步解析uri
}
由于GET请求方式会将表单中的参数拼接在抽象路径uri后面
因此我们需要进一步解析uri,将抽象路径部分和参数部分分别保存在requestURI和queryString中。
private void parseUri() {
String[] data = uri.split("\\?");//按照?拆分
requestURI = data[0];
if (data.length > 1) {//按照?拆分后,数组有第二个元素,说明这个uri是含有参数部分的
queryString = data[1];//保存参数
parseParameters(queryString);//进一步解析参数
}
}
现在的参数部分还是一个字符串:
下一步我们将参数部分进一步拆分:按 = 拆分,并保存在HashMap中。
POST请求方式的消息正文和这里的格式是一样的,因此解析消息正文的核心代码也是调用了此方法。
这里转码的原因是:因为浏览器支持的字符集是ISO8859-1,这是一个欧洲的字符集不支持中文字符。详细内容移步:浏览器和服务器解决传输中文数据的方法详解_剑剑剑剑啊的博客-CSDN博客
private void parseParameters(String line) {
try {
//将参数转码,可以支持中文,转码规则详情参考note.txt笔记
line = URLDecoder.decode(line, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String[] paraArr = line.split("&");
for (String para : paraArr) {
//将每一组参数按照"="拆分为参数名和参数值:{username, fanchuanqi}
String[] arr = para.split("=");
//用三元运算判断是否有参数值,有则将该参数值赋给value,没有则将空串赋值给value
parameters.put(arr[0], arr.length > 1 ? arr[1] : "");//保存在HashMap中
}
}
通过上述操作,我们完成了对请求行的解析,接下来进行解析消息头的操作。
1.2解析消息头
通过循环读取每一行,再将每一行消息头按照空格拆分,保存到HashMap中
private void parseHeaders() throws IOException {
while (true) {//消息行数量不确定,因此循环拆分
String line = readLine();
if (line.isEmpty()) {//判断为空,结束循环
break;
}
String[] data = line.split(":\\s");//按空格拆分
headers.put(data[0], data[1]);//以键值对的形式保存在HashMap中
}
}
1.3解析消息正文
private void parseContent() throws IOException {
//1.判断请求方式是否为POST请求。因为POST请求会带着正文内容
if ("POST".equalsIgnoreCase(method)) {
//2.获取消息头:Content-Length,根据该值得知正文的长度(总共多少个字节)
String value = headers.get("Content-Length");
if (value != null) {
int contentLength = Integer.parseInt(value);//将正文长度转换为一个int值
byte[] contentData = new byte[contentLength];
//3.按照正文长度将正文所有的字节读取出来
InputStream in = socket.getInputStream();
in.read(contentData);//将正文内容读取到字节数组上
/*4.获取消息头:Content-Type,并根据该值得知正文的类型。不同类型我们解析正文数据
操作不完全一致。
这里仅先实现form表单POST请求提交的正文。该种正文就是一个字符串。格式就是原GET
请求提交表单时在抽象路径中"?"右侧的内容。
name=value&name=value&name=value&...
*/
String contentType = headers.get("Content-Type");
//判断类型是否为form表单提交的数据
if ("application/x-www-form-urlencoded".equals(contentType)) {
//将字节数组以ISO8859-1的格式转换为字符串
String line = new String(contentData, StandardCharsets.ISO_8859_1);
//5.将该正文内容进行参数拆分,并存入parameters这个Map。
parseParameters(line);
}
// else if("".equals(contentType)){ 后续可扩展判断其他类型进行正文处理
//
// }
}
}
}
通过以上一系列的操作后,我们就得到了浏览器给我们发送的所有数据,并保存在对应的变量中
同时我们提供对应属性的get方法,这样我们就可以在通过request.getXxx的方式获取对应属性的值。在获取Map的值时有一个小细节,我们应该自定义一个获取Map中value的方法,方法中传入的参数为Map的key,通过getValue(key)方法,返回对应的value。
//请求行相关信息
private String method;//请求方式
private String uri;//抽象路径
private String protocol;//协议版本
private String requestURI;//保存uri中的请求部分(?左侧内容)
private String queryString;//保存uri中的参数部分(?右侧内容)
//GET请求或者POST请求中的参数均保存在此属性中
private Map<String, String> parameters = new HashMap<>();//保存每一组参数
//消息头相关信息
//这个Map存所有消息头,key为消息头的名字 value为消息头的值
private Map<String, String> headers = new HashMap<>();
接下来进行解析请求的操作。
在解析请求前,我们会先实例化响应对象HttpServletResponse
//新建响应对象
HttpServletResponse response = new HttpServletResponse(socket);
在实例化响应对象的同时,给内部的部分属性附上初始值:状态代码默认初始值=200,状态描述默认初始值=ok。
那么我们接下来的任务就是根据请求中的参数给状态行、响应头和响应正文赋上对应的值。
2.处理请求
在ClientHandler中新建一个DispatcherServlet的对象,然后调用service方法,传入request 和 response对象
//2.处理请求
DispatcherServlet servlet = new DispatcherServlet();
servlet.service(request,response);
DispatcherServlet是一个专门用于处理请求的类,通过调用:request.getRequestURI获取浏览器发送过来的抽象路径。首先,根据获取到的抽象路径判断是否为请求一个业务,若为请求业务的路径则根据相应的抽象路径处理对应的业务;否则,根据请求的抽象路径去static目录下定位用户实际请求的资源,并判断该文件是否存在且是一个文件?根据判断结果去设置相应的状态代码、正文及响应头。下面我们看看这些操作是如何实现的。
1.请求业务
判断获取到的抽象路径是否是请求业务
String path = request.getRequestURI();//获取抽象路径
//这里判断的路径是根据form表单的action参数来定义的
if ("/myweb/reg".equals(path)) {
//用户注册
UserController controller = new UserController();
controller.reg(request,response);
}else if(其他业务路径){
新建相应业务的controller
调用相关方法...
}...
用户注册页面form表单上对应的抽象路径:
下面以用户注册的业务举例
public void reg(HttpServletRequest request, HttpServletResponse response) {
//1.通过request获取reg.html页面上表单里用户输入的信息
//获取参数时,索引值name必须与reg.html中对应输入框的名字一致
String username = request.getParameter("user");
String psw = request.getParameter("psw");
String nick = request.getParameter("nick");
String ageStr = request.getParameter("age");
//判断注册信息是否为空,为空则返回错误页面并退出此方法
if (username.isEmpty() || psw.isEmpty() || nick.isEmpty() ||
ageStr.isEmpty() || !ageStr.matches("[0-9]+")) {
//返回错误页面
//File file = new File(DispatcherServlet.staticDir,"/myweb/reg_info_error.html");
//response.setContentFile(file);
//这里使用重定向的方式实现业务页面的返回
response.sendRedirect("/myweb/reg_info_error.html");
return;
}
int age = Integer.parseInt(ageStr);//将年龄转换为int
//2将该用户信息保存,暂时保存在硬盘中,后续保存到数据库中
//将该用户信息以User对象形式序列化到users目录中,取名叫:[用户名].obj文件
User user = new User(username, psw, nick, age);
//以用户名作为文件名保存
File userFile = new File(userDir, username + ".obj");
//判断该用户是否已经存在
if (userFile.exists()) {
// File file = new File(DispatcherServlet.staticDir, "/myweb/hava_user.html");
// response.setContentFile(file);
response.sendRedirect("/myweb/hava_user.html");
return;
}
try (
FileOutputStream fos = new FileOutputStream(userFile);
ObjectOutputStream oos = new ObjectOutputStream(fos);
) {
oos.writeObject(user);
//3.设置response的正文文件为注册结果页面
// File file = new File(DispatcherServlet.staticDir, "/myweb/reg_success.html");
// response.setContentFile(file);
response.sendRedirect("/myweb/reg_success.html");
} catch (IOException e) {
e.printStackTrace();
}
其他业务:用户登录、显示所有已注册用户信息的动态页面,根据输入信息生成二维码等业务在此不再赘述。
2.请求页面
下面以请求主页面为例,index.html静态页面放在/resources/static/myweb目录下
先在静态代码块中实现定位到该路径的代码,如下所示
//将路径声明为静态,每次创建这个对象时,都只会新建一次
private static File rootDir;
public static File staticDir;
static {
try {
rootDir = new File(
DispatcherServlet.class.getClassLoader()
.getResource(".").toURI()
);
//定位resources下的static目录
staticDir = new File(rootDir, "static");
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
设置页面的具体操作,在设置响应正文时,我们也将部分响应头也放在设置响应正文的方法中,原因在于:我们可以通过被设置的文件获取文件类型Content-Type和文件大小Content-Length,在设置响应正文时也刚好设置好这两个响应头。
if(业务路径){
新建相应业务的controller
调用相关方法...
}...
else {
//定位到index.html文件:resource/static/myweb/index.html
File file = new File(staticDir, path);
if (file.isFile()) {//file是否为一个文件并且存在
//状态代码 状态描述默认200 ok 因此不再赋值
//设置响应正文,响应头也放到setContentFile方法中设置
response.setContentFile(file);
} else {//不存在 状态代码=404,状态描述=NotFound
response.setStatusCode(404);
response.setStatusReason("NotFound");
file = new File(staticDir, "/root/404.html");
response.setContentFile(file);
}
}
此时我们已经设置好全部需要发送的参数,如下图所示:
接下来进行最后的工作,发送响应。
3.发送响应