问题描述
一、服务器如何获取到浏览器的请求
服务器启动ServerSocket套接字端口,启动accpet()来接受端口的连接,在socket中存储着浏览器发来的消息头。
public WebServerApplication(){
System.out.println("正在启动服务端...");
serverSocket = new ServerSocket(8088);
threadPool = Executors.newFixedThreadPool(50);
System.out.println("服务端启动完毕!");
}
public void start(){
while(true) {
System.out.println("等待客户端连接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端连接了!");
//启动一个线程处理该客户端的交互
ClientHandler handler = new ClientHandler(socket);
threadPool.execute(handler);
}
//当对同一个socket调用多次getInputStream方法时,获取回来的输入流始终是同一条流
InputStream in = socket.getInputStream();
这里装了各项消息://请求行 //消息头 //消息正文
二、如何分析到浏览器发来的响应
首先对浏览器的发送请求截取
//当对同一个socket调用多次getInputStream方法时,获取回来的输入流始终是同一条流
InputStream in = socket.getInputStream();
int d;
StringBuilder builder = new StringBuilder();
char pre='a',cur='a';
while((d = in.read())!=-1){
cur = (char)d;
if(pre==13&&cur==10){
break;
}
builder.append(cur);
pre = cur;
}
return builder.toString().trim();
对截取内容再次截取,获取状态行
String line = readLine();//对消息头进行截取
if(line.isEmpty()){//若请求行是空字符串,则说明本次为空请求
throw new EmptyRequestException("request is empty");
}
System.out.println("请求行:"+line);
//请求行:GET /logo.png HTTP/1.1
//按照 空格再次截取
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
//进一步解析uri
parseURI();
获取消息头,在这里获取的消息头大部分没有再次使用,只有“Content-Length”和“Content-Type”在post请求中调用了一下,用来获取post正文,和判断是否为form表单。
while(true) {
String line = readLine();
if(line.isEmpty()){
break;
}
System.out.println("消息头:" + line);
String[] data = line.split(":\\s");
//key:Connection value:keep-alive
headers.put(data[0],data[1]);//key:消息头的名字 value:消息头的值
}
System.out.println("headers:"+headers);
获取消息正文,如果提交方式为Post ,他的提交数据不会在浏览器的地址栏中显示,而会放在正文中,此正文不是html正文。
//请求方式是否为POST请求
if("post".equalsIgnoreCase(method)){
if(headers.containsKey("Content-Length")) {
//根据消息头Content-Length确定正文长度
int contentLength = Integer.parseInt(
headers.get("Content-Length")
);
System.out.println("正文长度:"+contentLength);
//读取正文数据
InputStream in = socket.getInputStream();
byte[] data = new byte[contentLength];
in.read(data);
/*
根据Content-Type来分析正文是什么以便进行对应的处理
*/
String contentType = headers.get("Content-Type");
if("application/x-www-form-urlencoded".equals(contentType)){//是否为form表单提交数据
String line = new String(data, StandardCharsets.ISO_8859_1);
parseParameter(line);
}
完整的浏览器请求,正文之后没有任何东西了。
请求行:POST /regUser HTTP/1.1
requestURI:/regUser
queryString:null
parameters:{}
method:POST
uri:/regUser
protocol:HTTP/1.1
消息头:Host: localhost:8088
消息头:Connection: keep-alive
消息头:Content-Length: 46
消息头:Cache-Control: max-age=0
消息头:sec-ch-ua: "Chromium";v="104", " Not A;Brand";v="99", "Google Chrome";v="104"
消息头:sec-ch-ua-mobile: ?0
消息头:sec-ch-ua-platform: "Windows"
消息头:Upgrade-Insecure-Requests: 1
消息头:Origin: http://localhost:8088
消息头: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/104.0.0.0 Safari/537.36
消息头:Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
消息头:Sec-Fetch-Site: same-origin
消息头:Sec-Fetch-Mode: navigate
消息头:Sec-Fetch-User: ?1
消息头:Sec-Fetch-Dest: document
消息头:Referer: http://localhost:8088/reg.html
消息头:Accept-Encoding: gzip, deflate, br
消息头:Accept-Language: zh-CN,zh;q=0.9
消息头:Cookie: Idea-f76c3dca=e7c9f62c-057a-473f-986d-2b007a38a54c
headers:{Origin=http://localhost:8088, Cookie=Idea-f76c3dca=e7c9f62c-057a-473f-986d-2b007a38a54c, Accept=text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9, Connection=keep-alive, User-Agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36, Referer=http://localhost:8088/reg.html, Sec-Fetch-Site=same-origin, Sec-Fetch-Dest=document, Host=localhost:8088, Accept-Encoding=gzip, deflate, br, Sec-Fetch-Mode=navigate, sec-ch-ua="Chromium";v="104", " Not A;Brand";v="99", "Google Chrome";v="104", sec-ch-ua-mobile=?0, Cache-Control=max-age=0, Upgrade-Insecure-Requests=1, sec-ch-ua-platform="Windows", Sec-Fetch-User=?1, Accept-Language=zh-CN,zh;q=0.9, Content-Length=46, Content-Type=application/x-www-form-urlencoded}
正文长度:46
正文内容 :[117, 115, 101, 114, 110, 97, 109, 101, 61, 49, 50, 51, 38, 112, 97, 115, 115, 119, 111, 114, 100, 61, 49, 50, 51, 38, 110, 105, 99, 107, 110, 97, 109, 101, 61, 49, 50, 51, 38, 97, 103,
三、响应
完整的页面需要包含三部分:状态行,响应头,响应正文。
3.1向浏览器发送
private void println(String line) throws IOException {
OutputStream out = socket.getOutputStream();
byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
out.write(data);
out.write(13);
out.write(10);
}
在向浏览器发送时需要设置字符编码 ISO_8859_1,每个响应都是以回车换行为结尾。
3.2 发送状态行
private void sendStatusLine() throws IOException {
// HTTP/1.1 200 OK
println("HTTP/1.1" + " " + statusCode + " " + statusReason);
}
3.3 发送响应头
private void sendHeaders() throws IOException {
/*
headers:
key value
Content-Type text/html
Content-Length 245
... ...
*/
Set<Map.Entry<String,String>> entrySet = headers.entrySet();
for(Map.Entry<String,String> e: entrySet){
String name = e.getKey();
String value = e.getValue();
println(name + ": " + value);
}
//单独发送个回车+换行表示响应头发送完毕
println("");
}
public void setContentFile(File contentFile) {
this.contentFile = contentFile;
//添加用于说明正文的响应头Content-Type和Content-Length
try {
String contentType = Files.probeContentType(contentFile.toPath());
/*
如果根据正文文件分析出了Content-Type的值则设置该响应头.
HTTP协议规定如果服务端发送的响应中没有包含这个头,就表明让浏览器自行判断
响应正文的内容类型.
*/
if(contentType!=null){
addHeader("Content-Type",contentType);
}
} catch (IOException e) {
e.printStackTrace();
}
addHeader("Content-Length",contentFile.length()+"");
输出map集合里添加的addHeader的字符串,单独调用prinln输出空白字符,会只输出回车换行,浏览器会认为响应头输出结束。
3.4 获取地址contentFile
String path = request.getRequestURI();
File file = new File(staticDir, path);
if (file.isFile()) {//浏览器请求的资源是否存在且是一个文件
//正确响应其请求的文件
response.setContentFile(file);
}
3.5 输出正文
private void sendContent() throws IOException {
OutputStream out = socket.getOutputStream();
if(this.out!=null){
byte[] data = this.out.toByteArray();
out.write(data);//将动态数据作为正文发送给浏览器
}else if(contentFile!=null) {
FileInputStream fis = new FileInputStream(contentFile);
byte[] buf = new byte[1024 * 10];//10kb
int len = 0;//记录每次实际读取的字节数
while ((len = fis.read(buf)) != -1) {
out.write(buf, 0, len);
}
}
}
获取contentFile的文件路径,文件流读取然后输出到浏览器的网络输出流中。
四、注解
4.1 创建注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
String value();
}
4.2 使用注解,定义方法
@RequestMapping("/userList")
public void userList(HttpServletRequest request, HttpServletResponse response) {
System.out.println("开始处理动态页面!!!!!!!!!!!!!!!!!!!");
}
注解最大的作用就是配合反射来实现动态的调用方法,如果不加注解会用 if ( ) else ( ) 这样调用方法,每次截取到的地址进行判断。
if("/regUser".equals(path)){
System.out.println("开始处理注册业务!!!!!!!!!!!!!!!!!!!!!");
UserController controller = new UserController();
controller.reg(request,response);
}else if("/loginUser".equals(path)){
UserController controller = new UserController();
controller.login(request,response);
五、反射
通过注解加载标识的方法,进行判断调用方法
5.1 获取需要加载的类的地址(约定大于配置,规定只能在controller文件夹下中写方法,并只扫描controller文件)
try {
File dir = new File(
HandlerMapping.class.getClassLoader()
.getResource(".").toURI()
);
File controllerDir = new File(dir,"/com/webserver/controller");
File[] subs = controllerDir.listFiles(f->f.getName().endsWith(".class"));
} catch (Exception e) {
e.printStackTrace();
}
}
5.2 获取controller层下的所有方法并放入map集合
//遍历目录中所有的.class文件
for(File sub : subs){
//获取文件名
String fileName = sub.getName();
//根据文件名截取出类名
String className = fileName.substring(0,fileName.indexOf("."));
//加载类对象
Class cls = Class.forName("com.webserver.controller."+className);
//判断这个类是否被@Controller注解标注
if(cls.isAnnotationPresent(Controller.class)){
//获取这个类定义的所有方法
Method[] methods = cls.getDeclaredMethods();
//遍历每一个方法
for(Method method : methods){
//判断该方法是否被@RequestMapping注解标注
if(method.isAnnotationPresent(RequestMapping.class)){
//获取该方法上的注解@RequestMapping
RequestMapping rm = method.getAnnotation(RequestMapping.class);
//获取该注解上定义的参数
String value = rm.value();
//将获取到的方法名和方法放入集合
mapping.put(value,method);
}
}
}
5.3 通过map集合获取方法
public static Method getMethod(String path){
return mapping.get(path);
}
5.4 调用方法
首先获取request截取的请求路径,放入map集合,并判断是否有该方法,如果method不为空
实例化该类传入参数执行方法。
String path = request.getRequestURI();
Method method = HandlerMapping.getMethod(path);
if(method!=null){
method.invoke(method.getDeclaringClass().newInstance(),
request,response);
return;
}
解决方法
暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!
如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。
小编邮箱:dio#foxmail.com (将#修改为@)