前言
不知道有没有人和我一样,开始学java就是springboot的天下了,springboot内嵌了tomcat,写一个controller加上RequestMapping
注解,启动项目就可以对前端提供服务了,而对底层网络编程题就像一个黑匣子触摸不到
今天就尝试再不使用spring,tomcat的情况下,纯java搭建一个web服务
socket
既然是处理前端请求,就绕不开网络编程socket,那就先使用ServerSocket
创建一个基础的网络服务:绑定端口-接受连接-打印接受数据-返回success
public class SampleServer {
public static void main(String[] args) throws IOException {
// 开启一个socket服务,绑定端口号8888
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("===server start===");
while (true) {
Socket clientSocket = serverSocket.accept();
byte[] bytes = new byte[1024];
System.out.println("准备read。。");
//接收客户端的数据,阻塞方法,没有数据可读时就阻塞
int read = clientSocket.getInputStream().read(bytes);
System.out.println("read完毕。。");
if (read != -1) {
System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
}
System.out.println("返回success");
clientSocket.getOutputStream().write("success".getBytes());
clientSocket.getOutputStream().flush();
clientSocket.close();
}
}
}
再写一个客户端发送“hello”到服务端
public class SocketClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost", 8888);
byte[] bytes = new byte[1024];
socket.getOutputStream().write("get".getBytes());
socket.getOutputStream().flush();
System.out.println("发送请求:get");
int read = socket.getInputStream().read(bytes);
System.out.println(new String(bytes, 0, read));
socket.close();
}
}
服务端打印结果
===server start===
准备read。。
read完毕。。
接收到客户端的数据:get
返回success
一个简单的网络服务就搭建好了
http
接下来我们使用浏览器发送一个请求过来测试一下:
chrome浏览器直接报错,在看服务端输出
准备read。。
read完毕。。
接收到客户端的数据:
GET / HTTP/1.1
Host: localhost:8888
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 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: none
Sec-Fetch-Mode: navigate
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
返回success
没有任何异常,看一下收到的信息,好家伙,这么多!仔细一看,这不就是http协议的格式吗:也就是说我们只是输入一个网址,浏览器把网址按http协议包装成规定的格式发给了后端接口,回顾一下http协议的格式如下:
http协议格式那服务端如果要获取用户请求的路径和请求方式(GET PUT等)等,首先就是按照http协议规定格式摘取出我们想要的信息,再回头看浏览器的错误:ERR_INVALID_HTTP_RESPONSE
,即无效的http响应,也就是我们的返回数据没有遵循http协议
在使用springboot时我们完全不用考虑http协议,只是简单的接受数据返回数据即可,因为这些事已经被tomcat处理好了,所以说客户端浏览器负责封装和解析http协议格式数据,服务端tomcat负责封装和解析http协议格式数据,如下图
tomcattomcat
所以当前我们要自己实现一个tomcat来作为请求数据和业务代码的对接中间件,负责接收数据是解析http协议格式的请求数据,返回是封装符合http协议响应格式的返回数据
新建两个类HttpRequest和HttpResponse,前者负责按http协议规定读取请求信息,解析成对象,后者负责把返回的数据封装成http协议规定响应格式
HttpRequest:
public class HttpRequest {
/**
* 请求路径
*/
private String pathInfo;
/**
* 请求方式
*/
private String method;
/**
* 请求方式
*/
private Map<String, String> header;
public HttpRequest(Reader inReader) {
try {
BufferedReader reader = new BufferedReader(inReader);
// 第一行:请求行
String firstLine = reader.readLine();
String[] firstLineItems = firstLine.split(" ");
// 请求方式
method = firstLineItems[0];
// 请求路径
pathInfo = firstLineItems[1];
// 读取接下来的行:请求头
String headerLine;
header = new HashMap<>();
while ((headerLine=reader.readLine())!=null) {
if(headerLine.length()==0){
break;
}
String[] headerLineItems = headerLine.split(": ");
header.put(headerLineItems[0], headerLineItems[1]);
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("错误");
}
}
public String getPathInfo() {
return pathInfo;
}
public String getMethod() {
return method;
}
public String getHeader(String name) {
return header.get(name);
}
}
HttpResponse:
public class HttpResponse {
public HttpResponse(String data) {
this.data = data;
}
private String data;
/**
* 返回数据
* @param
*/
public byte[] getBytes() {
// 最终返回的数据: 响应行+响应头+空行+响应正文
return ("HTTP/1.1 200\r\n" +
"Content-Type: application/json\r\n" +
"\r\n" + data).getBytes();
}
}
有了这两个类,我们就可以很轻松实现一个web服务
public class WebServer {
public static void main(String[] args) throws IOException {
// 开启一个socket服务,绑定端口号8888
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("===server start===");
while (true) {
Socket clientSocket = serverSocket.accept();
try {
// 解析请求信息为HttpRequest对象
HttpRequest request = new HttpRequest(new InputStreamReader(clientSocket.getInputStream(), "utf-8"));
// 返回数据
String data = "{\n" +
" \"code\": 200,\n" +
" \"message\": \"success\"\n" +
" \"path\": \""+request.getPathInfo()+"\"\n" +
"}";
// 响应
HttpResponse response = new HttpResponse(data);
// 返回数据
try {
clientSocket.getOutputStream().write(response.getBytes());
clientSocket.getOutputStream().flush();
} finally {
clientSocket.getOutputStream().close();
}
} finally {
clientSocket.close();
}
}
}
}
此时再用浏览器请求
chrome这样我们就做好一个简单的web服务
servlet
web服务虽然搭建好了,但却是只能提供一个服务:接受请求把请求的path返回
而这显然不是我们要的结果,我们希望比如/user就进入UserController(用户服务),/order就进入OrderController(订单服务)这样的结果
为了实现这个样的功能, 我们给所有服务做一个抽象,简单点就一个方法:service
代表服务开始执行入口,把之前封装的HttpRequest作为参数传入进去(HttpRequest也该传,比如做文件下载导出流的功能,这里简化就不传了)
public interface Servlet {
String service(HttpRequest request);
}
然后做一个path到服务的映射map,放到WebServer中,构造时传入,然后接受请求时按map执行不同Servlet的service
方法,修改后的WebServer如下
public class WebServer {
/**
* 存储path到服务的映射
*/
private Map<String, Servlet> servletMap;
/**
* 初始化
*
* @param servletMap
*/
public WebServer(Map<String, Servlet> servletMap) {
this.servletMap = servletMap;
}
/**
* 运行tomcat
*
* @throws IOException
*/
public void run() throws IOException {
// 开启一个socket服务,绑定端口号8888
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("===server start listen 8888===");
while (true) {
Socket clientSocket = serverSocket.accept();
try {
// 解析请求信息为HttpRequest对象
HttpRequest request = new HttpRequest(new InputStreamReader(clientSocket.getInputStream(), "utf-8"));
// 根据path获取servlet
Servlet servlet = servletMap.get(request.getPathInfo());
if (servlet == null) {
continue;
}
// 执行业务
String data = servlet.service(request);
// 响应
HttpResponse response = new HttpResponse(data);
// 返回数据
clientSocket.getOutputStream().write(response.getBytes());
clientSocket.getOutputStream().flush();
clientSocket.getOutputStream().close();
clientSocket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
到此,一个简易内嵌版的tomcat做完了,项目结构如下
项目结构接下来我们在apps包中分别写两个servlet实现UserController和OrderController测试一下
public class OrderController implements Servlet {
public String service(HttpRequest request) {
return "{\"message\": \"订单服务完成\"}";
}
}
public class UserController implements Servlet {
public String service(HttpRequest request) {
return "{\"message\": \"用户服务完成\"}";
}
}
写个启动类
public class MainApplication {
public static void main(String[] args) throws IOException {
new WebServer(new HashMap<String, Servlet>(){{
put("/user", new UserController());
put("/order", new OrderController());
}}).run();
}
}
效果如下
订单服务和用户服务实际上tomcat使用servlet并不是tomcat定义的而是JavaEE规范定义的
到此,我们的自己的web服务搭建完了,而且还顺便封装出一个简版tomcat-embed,接下来再有新的服务,只需在apps包下创建新的servlet并注册映射即可,完全不用考虑什么http协议了
注册映射有些麻烦,新增一个servlet就得改一下代码,在真实tomcat中是改xml配置,我们可以继续优化,比如自定义一个注解比如@RequestMapping("/user")
,然后扫描所有类然后把带注解的类注册到映射器中,这就是springboot帮我们干的事了
BIO&NIO
上面的代码跑通了,但性能上存在问题,上面的代码同一时间只能处理一个请求,如果某个请求一直不结束,那其他请求只能干等了
可以优化一下每个请求新建一个线程去处理(或者使用线程池),可以解决以上问题,这就是BIO模型
改造代码很简单,accept之后新开一个线程去处理请求,而主线程回到accpet上等待
public class BioWebServer {
/**
* 存储path到服务的映射
*/
private Map<String, Servlet> servletMap;
/**
* 初始化
*
* @param servletMap
*/
public BioWebServer(Map<String, Servlet> servletMap) {
this.servletMap = servletMap;
}
/**
* 运行tomcat
*
* @throws IOException
*/
public void run() throws IOException {
// 开启一个socket服务,绑定端口号8888
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("===server start listen 8888===");
// 创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(100);
while (true) {
Socket clientSocket = serverSocket.accept();
// 线程池处理请求
pool.execute(()->{
try {
// 解析请求信息为HttpRequest对象
HttpRequest request = new HttpRequest(new InputStreamReader(clientSocket.getInputStream(), "utf-8"));
// 根据path获取servlet
Servlet servlet = servletMap.get(request.getPathInfo());
if (servlet == null) {
return;
}
// 执行业务
String data = servlet.service(request);
// 响应
HttpResponse response = new HttpResponse(data);
// 返回数据
try {
clientSocket.getOutputStream().write(response.getBytes());
clientSocket.getOutputStream().flush();
} finally {
clientSocket.getOutputStream().close();
}
clientSocket.close();
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
}
但BIO模型有个缺点,主要在inputStream的read上,这个read是阻塞操作(这也是为什么这个模式叫BIO,即Blocking IO,除此之外accpet操作也是阻塞的),阻塞等待什么呐,等待客户端数据传过来,并等待内核把数据准备好,而这个等待过程线程阻塞,造成了资源的浪费
- 如果使用线程池,本来就那么几个人干活,还有几个人傻等着,效率能高吗
- 如果不使用线程池,线程过多又会给服务器造成压力,比如c10k问题
而如果我们把代码改成NIO模型,就可以解决这个问题了,这个下篇再说~
网友评论