使用Socket实现HttpServer(二)
使用Socket实现HttpServer(二)
前面我们使用 Socket 实现了一个简易的 HttpServer,接下来我们将对我们的服务器进行优化:
- 面向对象的封装
- 优化线程模型(引入多线程)
- Request/Response 对象抽象
Step1(面向对象的封装)
对我们之前所写的 HttpServer 进行面向对象封装。
主要封装了 listen() 和 accept() 方法。
package com.fengsir.network;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.function.Function;
/**
* @Author FengZeng
* @Date 2022-01-24 13:35
* @Description TODO
*/
public class Step1Server {
ServerSocket socketServer;
Function<String, String> handler;
public Step1Server(Function<String, String> handler) {
this.handler = handler;
}
/**
* @param port listen port
* @throws IOException
*/
public void listen(int port) throws IOException {
socketServer = new ServerSocket(port);
while (true) {
accept();
}
}
private void accept() throws IOException {
Socket socket = socketServer.accept();
System.out.println("a socket created");
// 拿到 socket 的请求内容,也就是 inputStream,封装成 bufferedReader,方便读取
InputStream inputStream = socket.getInputStream();
BufferedReader bfReader = new BufferedReader(new InputStreamReader(inputStream));
// 按行读取,把内容放到 stringBuilder 中
StringBuilder requestBuilder = new StringBuilder();
String line = "";
// 由于有时候 readLine() 会读取到 null,就会报错 nullException,
// 所以在这里进行了一点修改
while (true) {
line = bfReader.readLine();
if (line == null || line.isBlank()) {
break;
}
requestBuilder.append(line);
}
// 打印请求内容
String request = requestBuilder.toString();
System.out.println(request);
// 封装 response
BufferedWriter bfWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String response = this.handler.apply(request);
bfWriter.write(response);
// 一定要 flush
bfWriter.flush();
socket.close();
}
public static void main(String[] args) throws IOException {
Step1Server step1Server = new Step1Server(res->{
return "HTTP/1.1 200 ok\n\nGood!\n";
});
step1Server.listen(8000);
}
}
到这里也没有什么太大的问题,不过我们的服务器还是单线程的,如果只是单纯的返回一个字符串,我们的架构也没有问题,如果此时每个请求会去做别的处理,比如去查一下数据库,那么我们的服务器性能就很低了,还会出现服务器拒绝的错误(因为操作系统的 pending Queue 已经满了),所以我们需要进一步优化。
Step2(引入多线程)
这里引入了多线程,不再是一个线程单独在跑了,试了一下 Jemeter 的压测,10000个线程的并发就会把内存拉得很高,主要还是因为创建了线程后 Java 的垃圾回收还没有启动,所以现在一般不会用这种方式去创建线程,后面会引入线程池来优化。
package com.fengsir.network;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.function.Function;
/**
* @Author FengZeng
* @Date 2022-01-24 14:01
* @Description TODO
*/
public class Step2Server {
ServerSocket socketServer;
Function<String, String> handler;
public Step2Server(Function<String, String> handler) {
this.handler = handler;
}
/**
* @param port listen port
* @throws IOException
*/
public void listen(int port) throws IOException {
socketServer = new ServerSocket(port);
while (true) {
accept();
}
}
private void accept() throws IOException {
// 如果不把 accept 提到这里,那么上面的 while循环会一直创建线程
// 这也体现了 accept() 是 blocking 的
Socket socket = socketServer.accept();
System.out.println("a socket created");
new Thread(()->{
try {
handler(socket);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
private void handler(Socket socket) throws IOException {
// 拿到 socket 的请求内容,也就是 inputStream,封装成 bufferedReader,方便读取
InputStream inputStream = socket.getInputStream();
BufferedReader bfReader = new BufferedReader(new InputStreamReader(inputStream));
// 按行读取,把内容放到 stringBuilder 中
StringBuilder requestBuilder = new StringBuilder();
String line = "";
while (true) {
line = bfReader.readLine();
if (line == null || line.isBlank()) {
break;
}
requestBuilder.append(line);
}
// 打印请求内容
String request = requestBuilder.toString();
System.out.println(request);
// 封装 response
BufferedWriter bfWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String response = this.handler.apply(request);
bfWriter.write(response);
// 一定要 flush
bfWriter.flush();
socket.close();
}
public static void main(String[] args) throws IOException {
Step2Server step2Server = new Step2Server(res->{
return "HTTP/1.1 200 ok\n\nGood!\n";
});
step2Server.listen(8000);
}
}
Step3(封装Request/Response)
由于 step2 中,我们使用的是 Function 对象来实现的请求和响应,我们进一步抽象封装,创建 IHandlerInterface 接口:
package com.fengsir.network.step3;
import java.io.IOException;
/**
* @Author FengZeng
* @Date 2022-01-24 14:26
* @Description TODO
*/
@FunctionalInterface
public interface IHandlerInterface {
/**
* 处理响应
* @param request request
* @param response response
* @throws IOException
*/
void handler(Request request, Response response) throws IOException;
}
然后我们创建 Request 类:
package com.fengsir.network.step3;
import org.apache.commons.httpclient.HttpParser;
import java.io.*;
import java.net.Socket;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @Author FengZeng
* @Date 2022-01-24 14:27
* @Description TODO
*/
public class Request {
// 使用正则表达式提取请求的方法
static Pattern methodRegex = Pattern.compile("(GET|PUT|POST|DELETE|OPTIONS|TARCE|HEAD)");
private final String body;
private final String method;
private final HashMap<String, String> headers;
public String getBody() {
return body;
}
public String getMethod() {
return method;
}
public HashMap<String, String> getHeaders() {
return headers;
}
public Request(Socket socket) throws IOException {
// 这是 DataInputStream 和 InputStream 的区别
// DataInputStream -> primitives(char,float)
// InputStream -> bytes
DataInputStream iptStream = new DataInputStream(socket.getInputStream());
BufferedReader bfReader = new BufferedReader(new InputStreamReader(iptStream));
// 从第一行读取请求的方法,HttpParser 是我引入的 commons-httpClient 包中的对象
String methodLine = HttpParser.readLine(iptStream,"UTF-8");
Matcher matcher = methodRegex.matcher(methodLine);
matcher.find();
String method = matcher.group();
// 解析请求头
// Content-Type: xxxx
var headers = HttpParser.parseHeaders(iptStream, "UTF-8");
HashMap<String, String> headMap = new HashMap<>();
for (var h : headers) {
headMap.put(h.getName(), h.getValue());
}
// 解析请求体
var bufferReader = new BufferedReader(new InputStreamReader(iptStream));
var body = new StringBuilder();
char[] buffer = new char[1024];
while (iptStream.available() > 0) {
bufferReader.read(buffer);
body.append(buffer);
}
this.body = body.toString();
this.method = method;
this.headers = headMap;
}
}
创建 Response 类:
package com.fengsir.network.step3;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.HashMap;
/**
* @Author FengZeng
* @Date 2022-01-24 14:27
* @Description TODO
*/
public class Response {
Socket socket;
private int status;
static HashMap<Integer, String> codeMap;
public Response(Socket socket) {
this.socket = socket;
if (codeMap == null) {
codeMap = new HashMap<>();
codeMap.put(200, "ok");
}
}
/**
* http 标准响应
* @param msg message
* @throws IOException
*/
public void send(String msg) throws IOException {
this.status = 200;
var resp = "HTTP/1.1 " + this.status + " " + codeMap.get(this.status) + "\n";
resp += "\n";
resp += msg;
this.sendRaw(resp);
}
/**
* 发送原始的响应
* @param msg message
* @throws IOException
*/
public void sendRaw(String msg) throws IOException {
var bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write(msg);
bufferedWriter.flush();
bufferedWriter.close();
}
}
到此,我们再重构一下主函数:
package com.fengsir.network.step3;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @Author FengZeng
* @Date 2022-01-24 14:28
* @Description TODO
*/
public class Step3Server {
ServerSocket serverSocket;
IHandlerInterface httpHandler;
public Step3Server(IHandlerInterface httpHandler) {
this.httpHandler = httpHandler;
}
public void listen(int port) throws IOException {
serverSocket = new ServerSocket(port);
while (true) {
this.accept();
}
}
private void accept() throws IOException {
Socket socket = serverSocket.accept();
new Thread(() -> {
try {
this.handler(socket);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
private void handler(Socket socket) throws IOException {
Request request = new Request(socket);
Response response = new Response(socket);
this.httpHandler.handler(request, response);
}
public static void main(String[] args) throws IOException {
var server = new Step3Server((req,resp)->{
System.out.println(req.getHeaders());
resp.send("Greetings\n");
});
server.listen(8000);
}
}
可以看到,我们现在的主函数已经很简短了,其实大多数的框架都是从这么一个过程过来的,所以抽象,封装,重构能力是很重要的,优秀的程序员在coding的时候,都会去往这几个方面去想,下一章我打算引入NIO继续优化我们的 Http Server
使用Socket实现HttpServer(二)的更多相关文章
- Linux学习之socket编程(二)
Linux学习之socket编程(二) 1.C/S模型——UDP UDP处理模型 由于UDP不需要维护连接,程序逻辑简单了很多,但是UDP协议是不可靠的,实际上有很多保证通讯可靠性的机制需要在应用层实 ...
- C#高性能大容量SOCKET并发(二):SocketAsyncEventArgs封装
原文:C#高性能大容量SOCKET并发(二):SocketAsyncEventArgs封装 1.SocketAsyncEventArgs介绍 SocketAsyncEventArgs是微软提供的高性能 ...
- 使用Socket实现HttpServer(三)
使用Socket实现HttpServer(三) 这一章继续对我们的服务器进行优化,引入 NIO package com.fengsir.network.step4; import java.io.IO ...
- 使用Socket实现HttpServer(一)
使用Socket实现HttpServer(一) Socket 编程 socket 翻译过来叫插槽,一张图你就明白 socket 就插在 TCP 也就是传输层上,对用户的请求和服务器的响应进行处理. 下 ...
- Python基础篇【第8篇】: Socket编程(二)SocketServer
SocketServer 在上一篇文章中我们学习了利用socket模块创建socket通信服务,但细心学习后就会发现利用socket模块创建的服务无法进行多进程的处理,当需要进行大量请求处理时,请求就 ...
- C# Socket学习笔记二
小记:昨天咱们已经了解了Socket的通信原理,可是点对点的一次通信并不是我们想要的,那么今天那我们就继续学习异步通信,简单来说就是服务器端和客户端可以进行多次 互发信息的通信而不用担心通道会关闭.在 ...
- Python 基础之socket编程(二)
Python 基础之socket编程(二) 昨天只是对socket编程做了简单的介绍,只是把socket通信的框架搭建起来,要对其中的功能进行进一步的扩充,就来看看今天的料哈! 一.基于tcp的套接字 ...
- 网络编程基础【day09】:简单socket实例(二)
本节内容 1.概述 2.socket实例 3.总结 一.概述 之前我们只是介绍了soket的概念和一些逻辑图表,下面我们来看看,socket的客户端和服务端到底是怎么用的? 二.socket实例 2. ...
- socket编程之二:两种链接类型tcp和udp
前面一篇文章说到了一些计算机网络的基础知识.引入了socket.从这节開始,就进入正题了. 一 概述 TCP:Transimission Control Protocol传输控制协议. UPD:Use ...
随机推荐
- tensorflow源码解析之common_runtime-direct_session
目录 核心概念 direct_session direct_session.h direct_session.cc 1. 核心概念 读过之前文章的读者应该还记得,session是一个执行代理.我们把计 ...
- Ubuntu 16.04.3 Server 版安装过程图文详解
Ubuntu 16.04.3 Server 版安装过程图文详解 首先,我们会进入系统安装的第一个界面,开始系统的安装操作.每一步的操作,左下角都会提示操作方式! 1.选择系统语言-English2.选 ...
- ssm配置推荐
1.JDK 1.8 2.Mysql 5.7 3.Maven 3.6.1
- springboot 踩坑之路之 Configuration Annotation Proessor not found in classpath
1.出现spring boot Configuration Annotation Proessor not found in classpath的提示是在用了@ConfigurationPropert ...
- C语言知识点的实例
编译环境:Visual Studio 2012 编程语言:C @ 目录 1.memcpy与'/0' 2.volatile的使用 3.数字转字符 4.memcpy len 与指针加减 len 的区别 5 ...
- [转载]从phpinfo中能获取哪些敏感信息
phpinfo()想必的最熟悉的了,在搭建环境之后都会随后写一个 phpinfo()来测试环境是否正常,很多人测试完毕忘记删除就开始部署环境了,这就造成了一些敏感信息的泄漏.那么我们能从 phpinf ...
- Prometheus由于时间不同步导致数据不显示
原文链接:Prometheus由于时间不同步导致数据不显示 问题 部署 prometheus 后,访问前端界面发现: 这是由于你windows机器与部署prometheus服务器的时间不同步导致的. ...
- 整理分布式锁:业务场景&分布式锁家族&实现原理
1.引入业务场景 业务场景一出现: 因为小T刚接手项目,正在吭哧吭哧对熟悉着代码.部署架构.在看代码过程中发现,下单这块代码可能会出现问题,这可是分布式部署的,如果多个用户同时购买同一个商品,就可能导 ...
- 使用IDEA开发SpringBoot不加载application.yml配置文件的解决方案
1.如果启动项目不加载application.yml配置文件,那么请确认下是否应用了Resources为项目资源文件夹 2.如果项目起初是可以正常使用的,突然不知道改了什么,然后进行启动项目的时候不加 ...
- @Qualifier 注解?
当有多个相同类型的bean却只有一个需要自动装配时,将@Qualifier 注解和@Autowire 注解结合使用以消除这种混淆,指定需要装配的确切的bean. Spring数据访问