目录
一、线程池
1. 构建线程池的类
2. 创建任务
3. 执行任务
测试
二、URL编码
解决方案
测试
三、如何接收客户端发送的全部信息
解决方案
测试
四、作业
1. 了解工厂模式
2. 了解反射技术
一、线程池
昨天使用了数据库连接池,我们了解了连接池的优点,那么也可以使用线程池来管理线程,
java自带的线程池的参数有 核心线程数,最大线程数,线程活跃时间,时间单位,任务队列,线程工厂,拒绝策略
为了学习了解线程池,我们先手写一个简单的线程池,只需要做到核心线程可重复利用就行
1. 构建线程池的类
属性:核心线程数,任务队列
方法:获取线程(静态代码块),执行任务(需要的参数:线程任务 Runnable)
为了避免创建多个对象,还需要设置单例模式
package com.shao.net;
import java.util.concurrent.LinkedBlockingQueue;
public class ThreadPool {
// 定义一个成员静态变量,存储单例对象
private static ThreadPool instance;
// 线程池核心线程数
private final static int MAX_THREAD_NUM = 10;
// 存放任务的队列
private static final LinkedBlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>();
static {
for (int i = 0; i < MAX_THREAD_NUM; i++) {
final int finalI = i;
new Thread(new Runnable() {
@Override
public void run() {
/**
* 线程池的线程,从队列中取出任务,这时线程不在临界区了,自动释放锁,然后执行任务,当执行完任务后,
* 因为是while循环,所以会在 synchronized (taskQueue) 等待,
* 当锁释放后,并且当前线程被唤醒时,会尝试获取锁,
* 如果获取到锁,会进入临界区,如果队列中有任务,则取出,然后执行任务,如果没有,则等待
* 等待下次获取到锁,会继续从上次进入等待态的位置继续往下执行,也就是 taskQueue.wait() 开始往下执行
* */
while (true) {
Runnable task = null;
synchronized (taskQueue) {
System.out.println("线程" + finalI + "准备完成");
// 队列为空,等待
while (taskQueue.isEmpty()) {
try {
taskQueue.wait(); // 使当前线程等待,释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程" + finalI + "开始执行");
// 从队列中取出任务
task = taskQueue.poll();
}
if (task != null) {
// 执行任务
task.run();
}
}
}
}).start();
}
}
// 私有化构造函数
private ThreadPool() {
}
// 获取对象
public static ThreadPool getInstance() {
synchronized (ThreadPool.class) {
if (instance == null) {
instance = new ThreadPool();
}
return instance;
}
}
public void execute(Runnable task) {
// 当方法被调用时,会尝试获取锁,如果获取到锁,则将任务加入队列,并唤醒等待的线程
synchronized (taskQueue) {
taskQueue.add(task);
taskQueue.notify();
}
}
}
2. 创建任务
这里的任务是之前线程执行的代码,我们把需要线程执行的任务放到一个类里,然后实现Runnable
package com.shao.net;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class MyTask implements Runnable {
private InputStream is;
private OutputStream os;
public MyTask(InputStream is, OutputStream os) {
this.is = is;
this.os = os;
}
@Override
public void run() {
// 定义一个字节数组,存放客户端发送的请求信息
byte[] bytes = new byte[1024];
// 读取客户端发送的数据,返回读取的字节数
int len = 0;
try {
len = is.read(bytes);
if (len == -1) {
return;
}
// 将读取的字节数组转换为字符串
String msg = new String(bytes, 0, len);
// 调用HttpRequest类解析请求信息
HttpRequest httpRequest = new HttpRequest(msg);
// 拼接请求的静态资源的路径
// 路径是相对路径,从模块的根路径开始
String filePath = "webs/" + httpRequest.getRequestModule();
HttpResponse httpResponse = new HttpResponse(os, httpRequest);
// 响应数据
httpResponse.response(filePath);
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. 执行任务
初始化线程池,来一个用户连接时,就创建一个任务,然后交给线程池,线程池取出一条线程执行任务的 run 方法
package com.shao.net;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class Tomcat {
// 初始化线程池
ThreadPool threadPool = ThreadPool.getInstance();
public Tomcat() {
ServerSocket ss = null;
try {
ss = new ServerSocket(8080);
while (true) {
// 调用accept()方法阻塞等待,直到有客户端连接到服务器,返回一个Socket对象用于与该客户端通信
Socket socket = ss.accept();
System.out.println("客户端连接成功");
// 获取Socket对象的输入流,用于读取客户端发送的数据
InputStream is = socket.getInputStream();
// 获取Socket对象的输出流,用于向客户端发送数据
OutputStream os = socket.getOutputStream();
// 创建一个任务对象,将输入输出流作为参数传过去
MyTask myTask = new MyTask(is, os);
// 把任务作为参数传递给ThreadPool的execute()方法,启动一个线程执行MyTask对象中的run()方法
threadPool.execute(myTask);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭连接通道
try {
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
测试
二、URL编码
在HTTP请求中,如果参数包含中文字符,会进行URL编码,以避免乱码或传输错误。URL编码是一种将URL中的非ASCII字符(如中文字符)转换为可以在Web浏览器和服务器之间传输的格式的过程。
URL编码会将非ASCII字符转换为十六进制编码,以便于在HTTP请求中安全传输。
URL编码的基本原理: URL编码将非ASCII字符(如中文字符)转换为"%"后跟两位十六进制数字的形式。例如,空格在URL编码中通常被转换为"%20"。对于中文字符,它们会被转码为以"%E"开头,后面跟着若干位十六进制数字的字符串。
解决方案
在接收到请求信息后,先进行解码,然后再解析信息
URLDecoder.decode(需要解码的字符串, 字符集或编码方式)
在 MyTask 类中添加
测试
三、如何接收客户端发送的全部信息
目前,我们的 Tomcat 最多只能一次接收 1KB,因为定义的字节数组只有1024个字节
但是,如果客户端发送的请求参数非常非常多呢?超过了 1024 个字节了怎么办?
把字节数组定义的大一点?不行的,因为网络传输一次最多传输 8KB,超过 8KB 就会分批传输,接收参数时也需要分批接收
那怎么判断参数已经接收完?
参数有很多一般是使用POST方法,而POST方法的请求头有 Content-Length 的字段,表示请求体的总长度
我们来试一下,打印一下请求的参数信息
这里可以看到 Content-Length 的值是 26,表示请求体的参数长度为26字节,图中显示参数的长度为 25,因为解析后没有显示参数连接符 &
我们来使用 Apipost 来压力测试一下,参数很多是什么样子
可以看到,只读取到了一部分 出师表 的内容,而且还有乱码,这是因为没有完整读取一个 汉字的字节,UTF-8 编码中一个汉字需要 3 个字节
解决方案
package com.shao.net;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MyTask implements Runnable {
private InputStream is;
private OutputStream os;
private int totalLength;
private StringBuilder sb;
public MyTask(InputStream is, OutputStream os) {
this.is = is;
this.os = os;
}
@Override
public void run() {
// 定义一个字节数组,存放客户端发送的请求信息
byte[] bytes = new byte[1024];
// 读取客户端发送的数据,返回读取的字节数
int len = 0;
try {
// 第一次读取请求信息
len = is.read(bytes);
if (len == -1) {
return;
}
// 将读取的字节数组转换为字符串
String msg = new String(bytes, 0, len);
// 调用HttpRequest类解析请求信息
HttpRequest httpRequest = new HttpRequest(msg);
/*
* 如果已读取的数据长度等于请求体的总长度,并且请求方法是 POST,说明请求体可能还没有读取完,需要读取剩余的数据
* */
if (bytes.length == len && "POST".equals(httpRequest.getRequestMethod())) {
// 创建一个StringBuilder对象,用于拼接请求信息
sb = new StringBuilder();
sb.append(msg);
// 获取 POST 请求方法中的请求体的总长度
String length = httpRequest.getRequestHeaderParams().get("Content-Length");
if (length != null) {
totalLength = Integer.parseInt(length);
}
// 调用方法,读取剩余的请求体数据
msg = getNotReadMsg(httpRequest, bytes, msg);
}
// 把请求信息进行URL解码,然后根据 UTF-8 进行编码
String decodedMsg = URLDecoder.decode(msg, "utf-8");
// 调用HttpRequest类解析请求信息
httpRequest = new HttpRequest(decodedMsg);
// 拼接请求的静态资源的路径
// 路径是相对路径,从模块的根路径开始
String filePath = "webs/" + httpRequest.getRequestModule();
HttpResponse httpResponse = new HttpResponse(os, httpRequest);
// 响应数据
httpResponse.response(filePath);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 读取剩余的请求体数据
*/
private String getNotReadMsg(HttpRequest httpRequest, byte[] bytes, String msg) throws IOException {
int len;
// 获取请求的参数
HashMap<String, String> requestBodyParams = httpRequest.getRequestBodyParams();
// 请求参数的数量
int size = requestBodyParams.size();
// 计算第一次读取到的请求体中参数的长度
Set<Map.Entry<String, String>> entries = requestBodyParams.entrySet();
int partLength = 0;
for (Map.Entry<String, String> entry : entries) {
partLength += (entry.getKey() + "=" + entry.getValue()).length();
}
// 减去第一次读取到的请求体中参数的长度,如果存在多个参数,需要考虑到 '&' 的个数
if (size > 1) {
totalLength -= (partLength + (size - 1));
} else {
totalLength -= partLength;
}
// 判断是否还有数据没有读完
while (totalLength > 0) {
// 第二次读取请求信息
len = is.read(bytes);
// 如果读取的字节数大于0,表示读取到数据了
if (len > 0) {
// 将读取的字节数组转换为字符串
msg = new String(bytes, 0, len);
// 拼接字符串
sb.append(msg);
// 减去读取的字节数
totalLength -= len;
} else {
break;
}
}
// 转成字符串格式返回
return sb.toString();
}
}
测试
可以看出已经全部读取到了,第二个参数也读取到了
四、作业
1. 了解工厂模式
优化 Dao,现在在 Service 层,调用Dao层都要 new 一下,这样就比较占内存,比如调用的都是 UserDao,那么只需要创建一次 UserDao 的对象就行了
2. 了解反射技术
优化 Servlet ,通过配置文件可以动态的创建 Servlet 对象