心碰心,我们一起造个高性能服务器(一)

本文深入探讨HTTP协议的工作原理,包括三次握手和四次挥手的过程,并通过Java代码实例展示了如何构建一个基本的请求响应式服务器。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

哈喽啊,各位亲爱的可爱的小伙伴🚪,来啦?

这系列的文章应该会陪伴大家一段时间,会从原理到实现,本秃在学习时候的思考和总结,希望你看了后能有所启发,指正我结论错误。

首先得先明白,用java实现一个请求响应式的服务器需要哪些知识:

  • IO AIO/BIO/NIO
  • 多线程
  • 网络编程
  • HTTP协议

我们从HTTP协议开始,一步一个脚印,画个高性能服务器的大饼~
在这里插入图片描述
HTTP协议处在之前说的应用层协议,它的实现基于传输层的TCP协议,相信很多人都知道TCP的三次握手和四次挥手,这次我们来点不一样的,更深入的探究一哈。

首先思考一下,为婶摸要三次才能建立连接?一次不可以?那现在思考一下,让两个互不相识的机器,建立连接还得确保稳定,你会怎么做?得先明白为什么需要三次才能建立完整可靠的连接。客户端发SYN=1和seq=100(初始序列号)的请求到服务器,服务器根据SYN=1能知道这是一个建立连接的请求,也能确定两件事:

  1. 客户端发送没问题
  2. 服务端接收没问题

但是客户端它这时候啥都不知道啊,所以服务器就返回SYN=1、ACK=1、ack=对方seq+1=101和seq=200,ack是为了告诉客户端:嘿,你小子的发送没问题啊~。ACK=1和SYN=1告诉客户端那小子同意连接,SEQ作用和客户端发过来的类似,通知客户端,服务端开始发送数据段的初始序列号。这时候客户端收到就能确定:

  1. 客户端发送没问题
  2. 客户端接收没问题
  3. 服务端接收没问题
  4. 服务端发送也没问题

但是这只是客户端知道的啊,服务端可不知道客户端这小子接收怎么样或服务端发送有没有问题,所以这时候客户端就返回ACK=1,ack=seq+1=201,seq=101。告诉服务端哥们发送功能没问题,客户端小弟的接收功能也没问题。这时候服务器收到了之后就明白了:

  1. 客户端发送没问题
  2. 服务端接收没问题
  3. 客户端接收没问题
  4. 服务端发送没问题

这样就建立起安全的连接啦。
在这里插入图片描述

DDOS攻击就是在第二次服务端发送报文后,客户端恶意挂起不回报文,服务端进入等待状态。

再来说说四次挥手,为什么要四次呢?三次不行吗?不行!为啥不行呢?首先挥手的目的是为了关闭连接,那么我们来盘一盘,关闭连接得怎么做。

在这里插入图片描述
客户端和服务端在手握手的时候,客户端这小子变了心,对服务端说:咱们分手吧,把我送你的两双袜子和其他东西都还给我。向服务端发送FIN=1,seq=100的消息。这时候客户端的状态是:

  1. 客户端不再发送请求数据

服务端接收到FIN=1,知道这小子变心了,就返回:ACK=1告诉客户端分手就分手谁稀罕谁啊,seq=200,ack=100+1这信息明确提出服务器会把客户端送过来的东西都还给客户端。这时候服务端的状态是:

  1. 明确客户端不会在发送数据
  2. 通知应用进程,一切该结束了

客户端收到服务端的请求后,看着服务端的灰色头像独自心伤,感觉世界都崩塌了,这时候客户端的状态是:

  1. 客户端不再发送数据
  2. 明确服务端也确定分手

没过一会,服务端就把东西整理好了:FIN=1,ACK=1,seq=200,ack=100+1。FIN=1,ACK=1告诉客户端:结束了,我再也不会发送东西给你,我同意你提的分手,我也明白了渣男,就这样吧。这时候服务端的状态是:

  1. 明确客户端不会再发送数据
  2. 明确服务端不会再发送请求
  3. 等待客户端确认东西收到

客户端醉醺醺的拿着服务端传回来的东西,这一切都历历在目,但是,该放手了。返回服务端:ACK=1,seq=100+1,ack=200+1,告诉服务端,东西收到了,再见。这时候客户端等待2MSL的时间,看服务端有没有回头。结果,没有,毛的感情。这时候客户端的状态:

  1. 明确客户端不会再发送请求
  2. 明确服务端不会再发送请求

服务端在接到客户端的数据知道客户端已经收到,转身就走了。

只有在客户端和服务端都没有数据要发送的时候才能断开连接。客户端发出的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…

我们的目标是打造高性能的服务器,任重道远。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值