哈喽啊,各位亲爱的可爱的小伙伴🚪,来啦?
这系列的文章应该会陪伴大家一段时间,会从原理到实现,本秃在学习时候的思考和总结,希望你看了后能有所启发,指正我结论错误。
首先得先明白,用java实现一个请求响应式的服务器需要哪些知识:
- IO AIO/BIO/NIO
- 多线程
- 网络编程
- HTTP协议
我们从HTTP协议开始,一步一个脚印,画个高性能服务器的大饼~
HTTP协议处在之前说的应用层协议,它的实现基于传输层的TCP协议,相信很多人都知道TCP的三次握手和四次挥手,这次我们来点不一样的,更深入的探究一哈。
首先思考一下,为婶摸要三次才能建立连接?一次不可以?那现在思考一下,让两个互不相识的机器,建立连接还得确保稳定,你会怎么做?得先明白为什么需要三次才能建立完整可靠的连接。客户端发SYN=1和seq=100(初始序列号)的请求到服务器,服务器根据SYN=1能知道这是一个建立连接的请求,也能确定两件事:
- 客户端发送没问题
- 服务端接收没问题
但是客户端它这时候啥都不知道啊,所以服务器就返回SYN=1、ACK=1、ack=对方seq+1=101和seq=200,ack是为了告诉客户端:嘿,你小子的发送没问题啊~。ACK=1和SYN=1告诉客户端那小子同意连接,SEQ作用和客户端发过来的类似,通知客户端,服务端开始发送数据段的初始序列号。这时候客户端收到就能确定:
- 客户端发送没问题
- 客户端接收没问题
- 服务端接收没问题
- 服务端发送也没问题
但是这只是客户端知道的啊,服务端可不知道客户端这小子接收怎么样或服务端发送有没有问题,所以这时候客户端就返回ACK=1,ack=seq+1=201,seq=101。告诉服务端哥们发送功能没问题,客户端小弟的接收功能也没问题。这时候服务器收到了之后就明白了:
- 客户端发送没问题
- 服务端接收没问题
- 客户端接收没问题
- 服务端发送没问题
这样就建立起安全的连接啦。
DDOS攻击就是在第二次服务端发送报文后,客户端恶意挂起不回报文,服务端进入等待状态。
再来说说四次挥手,为什么要四次呢?三次不行吗?不行!为啥不行呢?首先挥手的目的是为了关闭连接,那么我们来盘一盘,关闭连接得怎么做。
客户端和服务端在手握手的时候,客户端这小子变了心,对服务端说:咱们分手吧,把我送你的两双袜子和其他东西都还给我。向服务端发送FIN=1,seq=100的消息。这时候客户端的状态是:
- 客户端不再发送请求数据
服务端接收到FIN=1,知道这小子变心了,就返回:ACK=1告诉客户端分手就分手谁稀罕谁啊,seq=200,ack=100+1这信息明确提出服务器会把客户端送过来的东西都还给客户端。这时候服务端的状态是:
- 明确客户端不会在发送数据
- 通知应用进程,一切该结束了
客户端收到服务端的请求后,看着服务端的灰色头像独自心伤,感觉世界都崩塌了,这时候客户端的状态是:
- 客户端不再发送数据
- 明确服务端也确定分手
没过一会,服务端就把东西整理好了:FIN=1,ACK=1,seq=200,ack=100+1。FIN=1,ACK=1告诉客户端:结束了,我再也不会发送东西给你,我同意你提的分手,我也明白了渣男,就这样吧。这时候服务端的状态是:
- 明确客户端不会再发送数据
- 明确服务端不会再发送请求
- 等待客户端确认东西收到
客户端醉醺醺的拿着服务端传回来的东西,这一切都历历在目,但是,该放手了。返回服务端:ACK=1,seq=100+1,ack=200+1,告诉服务端,东西收到了,再见。这时候客户端等待2MSL的时间,看服务端有没有回头。结果,没有,毛的感情。这时候客户端的状态:
- 明确客户端不会再发送请求
- 明确服务端不会再发送请求
服务端在接到客户端的数据知道客户端已经收到,转身就走了。
只有在客户端和服务端都没有数据要发送的时候才能断开连接。客户端发出的FIN只能保证客户端没有数据再发送,服务端发俩次,第一次是告诉客户端确认收到FIN报文,服务端处理还没完成。等二次FIN告诉客户端发送数据结束。
我们明白了连接的建立和断开,那怎么实现tcp连接呢?java的socket套接字就是实现tcp/IP连接的,而且功能强大。话不多说,搞起来!
ServerSocket server = new ServerSocket(8888);
Socket accept = server.accept();
InputStream inputStream = accept.getInputStream();
byte[] bytes = new byte[1024*8];
int len = 0;
try(InputStream inputStream = accept.getInputStream()){
len = inputStream.read(bytes);
}
System.out.println(new String(bytes,0,len));
server.close();
我们运行上面那段程序,然后用浏览器连接localhost:8888,可以看到:
GET / HTTP/1.1
Host: localhost:8888
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36
Sec-Fetch-User: ?1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: Idea-661643a5=2b2391a7-0f48-4359-b19e-97b29321203d
这些数据呢,就是我们上面说的http协议。
它其实就是这样的一段数据格式,假如让您定义一个应用层数据交互的协议,最先定义的是什么?格式。其实http协议里每个键值对都有它的意义,这里就不再细说,请看 https://developer.mozilla.org/zh-CN/docs/Web/HTTP
那我们基本的服务器实现了之后接下来怎么返回数据给浏览器?怎么实现页面展示到浏览器解析呢?
首先我们得想一下,我们处理简单IO流的时候,输入输出流是相对于内存来说的。那在服务器里面,我们是不是可以相对于服务器来决定这输入输出流呢?也即是请求为输入流,响应为输出流?哇,这位同学,悟性很高嘛。话不少说,我们还是先来试试是不是输出流能响应浏览器请求呢?
ServerSocket server = new ServerSocket(8888);
Socket accept = server.accept();
byte[] bytes = new byte[1024*8];
int len = 0;
try(InputStream inputStream = accept.getInputStream()){
while(-1!=(len = inputStream.read(bytes))){
System.out.println(new String(bytes,0,len));
}
}
server.close();
这样是行不通的大笨蛋,本秃一开始唰唰唰两下就写了这种代码,经历多次错误后,我冷静了下来。
首先思考一下,服务器是以流的方式接收请求,那这个输入流存在连接的时候是不会有明确的文本结束的,所以输入流读取时不能用-1判断。第二个是try里面的流用完后是自动关闭的,输入流一关闭,那连接也就关闭了,还传浏览器个锤子,行不通行不通。
public class ServerCore {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8888);
Socket accept = server.accept();
byte[] bytes = new byte[1024*8];
InputStream inputStream = accept.getInputStream();
int len = inputStream.read(bytes);
System.out.println(new String(bytes,0,len));
String html ="<!DOCTYPE html>\n" +
"<html lang=\"en\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <title>您好</title>\n" +
"</head>\n" +
"<body>\n" +
"hallo啊,小朋友\n" +
"</body>\n" +
"</html>";
StringBuilder sb = new StringBuilder();
//拼装http响应的数据格式
sb.append("HTTP/1.1 200 ok\r\n")
.append("Server: lintutu 1.0;charset=utf-8;\r\n")
.append("Content-Type: text/html\r\n")
.append("DATE: ").append(new Date()).append("\r\n")
.append("Content-Length: ").append(html.length()).append("\r\n\r\n").append(html);
OutputStream outputStream = accept.getOutputStream();
outputStream.write(sb.toString().getBytes());
outputStream.flush();
}
}
这里要注意的地方有:
http响应格式一定要注意,推荐看这个
响应内容和响应头一定得空行,不然浏览器读不出来。
点击运行,打开浏览器localhost:8080 一气呵成,然后:
浏览器连接后效果是这样的:
我们最初版本的服务器就完成啦,撒花撒花~能连就是好服务器。
但是还是得先思考一下,这个服务器每次连完之后,都得重新启动,这样有点Emmmm…
页面写带代码里Emmmm…
性能好像Emmmm…
我们的目标是打造高性能的服务器,任重道远。