使用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(二)的更多相关文章

  1. Linux学习之socket编程(二)

    Linux学习之socket编程(二) 1.C/S模型——UDP UDP处理模型 由于UDP不需要维护连接,程序逻辑简单了很多,但是UDP协议是不可靠的,实际上有很多保证通讯可靠性的机制需要在应用层实 ...

  2. C#高性能大容量SOCKET并发(二):SocketAsyncEventArgs封装

    原文:C#高性能大容量SOCKET并发(二):SocketAsyncEventArgs封装 1.SocketAsyncEventArgs介绍 SocketAsyncEventArgs是微软提供的高性能 ...

  3. 使用Socket实现HttpServer(三)

    使用Socket实现HttpServer(三) 这一章继续对我们的服务器进行优化,引入 NIO package com.fengsir.network.step4; import java.io.IO ...

  4. 使用Socket实现HttpServer(一)

    使用Socket实现HttpServer(一) Socket 编程 socket 翻译过来叫插槽,一张图你就明白 socket 就插在 TCP 也就是传输层上,对用户的请求和服务器的响应进行处理. 下 ...

  5. Python基础篇【第8篇】: Socket编程(二)SocketServer

    SocketServer 在上一篇文章中我们学习了利用socket模块创建socket通信服务,但细心学习后就会发现利用socket模块创建的服务无法进行多进程的处理,当需要进行大量请求处理时,请求就 ...

  6. C# Socket学习笔记二

    小记:昨天咱们已经了解了Socket的通信原理,可是点对点的一次通信并不是我们想要的,那么今天那我们就继续学习异步通信,简单来说就是服务器端和客户端可以进行多次 互发信息的通信而不用担心通道会关闭.在 ...

  7. Python 基础之socket编程(二)

    Python 基础之socket编程(二) 昨天只是对socket编程做了简单的介绍,只是把socket通信的框架搭建起来,要对其中的功能进行进一步的扩充,就来看看今天的料哈! 一.基于tcp的套接字 ...

  8. 网络编程基础【day09】:简单socket实例(二)

    本节内容 1.概述 2.socket实例 3.总结 一.概述 之前我们只是介绍了soket的概念和一些逻辑图表,下面我们来看看,socket的客户端和服务端到底是怎么用的? 二.socket实例 2. ...

  9. socket编程之二:两种链接类型tcp和udp

    前面一篇文章说到了一些计算机网络的基础知识.引入了socket.从这节開始,就进入正题了. 一 概述 TCP:Transimission Control Protocol传输控制协议. UPD:Use ...

随机推荐

  1. 同事都说有SQL注入风险,我非说没有

    前言 现在的项目,在操作数据库的时候,我都喜欢用ORM框架,其中EF是一直以来用的比较多的:EF 的封装的确让小伙伴一心注重业务逻辑就行了,不用过多的关注操作数据库的具体细节.但是在某些场景会选择执行 ...

  2. mybatis crud 学习总结02

    1.db.properties 原因:数据库的连接直接写到配置(mybati-config.xml)里存在安全隐患 解决:将配置的信息放到一个文件中管理 右键resources --> File ...

  3. 完全激活win server 2012的方法(亲测可行!)

    1.从微软官网下载评估版. 2.运行->CMD(最好以管理员身份运行)->输入"DISM /online /Get-CurrentEdition"(最好直接复制粘贴,然 ...

  4. pandas常用操作详解——.loc与.iloc函数的使用及区别

    loc与iloc功能介绍:数据切片.通过索引来提取数据集中相应的行数据or列数据(可以是多行or多列) 总结: 不同:1. loc函数通过调用index名称的具体值来取数据2. iloc函数通过行序号 ...

  5. 解决vue单页面应用做微信JSSDK注入权限时出现“invalid signature”(ios端)

    --都说微信开发多坑,没想到遇到一个天坑. 在做一个vue项目时,要用到微信JS-SDK,官方文档详见:https://developers.weixin.qq.com/doc/offiaccount ...

  6. Django基础五之Ajax

    Django基础五之Ajax 目录 Django基础五之Ajax 1. Ajax介绍 2. Ajax前后端传值 2.1 方法一HttpResponse直接返回 2.2 方法二使用Json格式返回 2. ...

  7. Mybatis工具类(自动提交事务)

    public class MybatisUtils { private static SqlSessionFactory sqlSessionFactory; static { try { //获取工 ...

  8. windows安装oos遇到的坑 (汇总)

    一.环境安装 --  搭建域控服务器 1. 打开服务器管理器,添加角色和功能: 2.下一步: 3.下一步: 4.下一步 5.选择添加AD域服务,同时添加所需功能,下一步: 6.安装功能,下一步: 7. ...

  9. 问鼎杯预赛web writeup

    1. php的一个精度问题,具体什么精度自己查. 2017.000000000001=2017 2016.999999999999=2017 直接拿谷歌浏览器访问那个链接就可以拿到flag 2. 访问 ...

  10. Linux系统常用的命令

    1.查看本机IP地址:ifconfig 2.查看当前所在路径:pwd 3.查看指定名称线程:ps -ef | grep tomcat 4.查看当前目录结构:ll 或者 ls 5.杀死指定线程:kill ...