前言
2024年对于自己来说是一个新的开始,从今年开始我将会专注于技术能力的提升,以博客的方式记录自己成长的脚步。今年的主线就是这个专栏——手撕 JavaWeb 服务器系列。我将会以一个后端开发者的视角去探索 Web 服务器的底层运行机制,通过猜想->实践->验证的基本流程去学习,即以 JavaWeb 服务器所具备的各种能力,去猜想它的实现原理,然后通过实践去实现,最后去看看已有的开源项目(tomcat、jboss等)的源码并思考如此设计的原因。感兴趣的朋友可以关注一下,并在疑惑的地方提出您的宝贵意见,一起探讨进步!
从网络讲起
谈到 Web 服务器,就离不开网络。通常,我们需要先通过网络发送一个请求到 Web 服务器,Web 服务器根据我们的请求信息再通过网络返回对应的响应信息。信息是怎么通过网络传输地呢?接下来我们简单地聊一下(ps:讲的很浅,请耐心看完哈)
寄快递
我们可以把网络传输数据的过程想象成物流公司运送快递,其中的很多原理是相通的。对于普通人来说,快递是怎么包装、运输、拆卸的,他们不关心,他们只需要找到快递驿站去寄送快递和取快递。同样,对于web服务器来说,它只管从指定的地方去发送和取数据即可。
寄过快递的同学应该清楚,我们在寄快递的时候需要填写自己的地址和目的地地址,类比到服务器也是一样,需要指明数据的目的地。那么在网络中如何定位一个网络设备?
地址
相信这个问题难不倒大家,就是 ip 地址,我们可以通过 ip 地址去定位一个网络设备。这里我们也不做展开,有兴趣的同学可以自己去搜索相关的资料。我们可以把 ip 地址看作是网络中的 xxx 省 xxx 市 xxx 区,按照一定的规则可以通过这串字符定位到唯一的一个网络设备。
广义上来说,接入网络的设备都可以称作为网络设备,包括我们的个人电脑、手机、公司的服务器等等。找到了网络设备我们是不是就能直接发送数据了呢?
端口
答案是:否。在一个网络设备中往往存在多个进程,打个比方把同一台电脑中的 qq 和 微信 当作两个不同的进程。如果只通过 ip 地址去区分数据,那么我的 qq 的数据有可能发到微信,微信的也有可能发送到 qq。因此在 ip 地址的基础上,同一个网络设备还需要去区分该设备中的多个应用。那么如何区分呢?这个时候就要用到端口了。
端口,大家可以简单的理解为网络设备中派送数据的门口。每个进程守在一个门口前面,网络设备会对这些“门”进行编号,根据数据包上的端口号找到对应的门,把数据包丢过去。
Socket 编程
通过上面的叙述,我们知道了网络数据传输数据的逻辑过程,具体的实现有时间大家可以去研究一下,这里我们主要讲一下代码层面的东西。
既然我们需要守着一个端口,在 Java 里面我们怎么实现呢?
public static void main(String[] args) throws IOException { //监听8080端口 ServerSocket serverSocket = new ServerSocket(8080); }
在上面的程序中,我们可以通过创建 ServerSocket 类,并且在构造方法中传入端口号的方式去监听指定的端口。关于端口号,我们需要再解释一点,那就是它的取值范围,一般端口号的取值范围是 1到65535,用了 16 位二进制来表示端口号。
接收数据
现在我们已经监听了 8080 端口,我们如何获取传输过来的数据呢?
ServerSocket serverSocket = new ServerSocket(8080); Socket accept = serverSocket.accept(); InputStream inputStream = accept.getInputStream(); int i; while((i=inputStream.read())!=-1){ System.out.print((char)i); }
创建 serverSocket 对象之后,我们通过调用 accept() 方法去获取 Socket 对象,这个方法会阻塞等待直到有数据到达我们监听的端口。数据就被封装在 IO 流中,我们可以通过 Socket 对象去获取输入流获取,以操作输入流的方式去获取网络中的数据。
返回响应
同样,我们也可以通过操作输出流去向客户端返回响应的数据。
public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(8080); Socket accept = serverSocket.accept(); InputStream inputStream = accept.getInputStream(); byte[] bytes = new byte[1024]; int i; //输出请求信息 while((i=inputStream.read(bytes,0,bytes.length))!=-1){ System.out.println(new String(bytes,0,i,StandardCharsets.UTF_8)); if (i<1024){ break; } } //响应内容 String responseContext = "HTTP/1.1 200 OK " + "Content-Type: text/plain " + "Content-Length: 11 " + " " + "hello world "; OutputStream outputStream = accept.getOutputStream(); outputStream.write(responseContext.getBytes(StandardCharsets.UTF_8)); //清空缓存区,刷新到目的空间 outputStream.flush(); outputStream.close(); inputStream.close(); }
这段代码比较关键的地方就是 flush 方法,如果不调用这个方法,浏览器是不会立即得到响应信息的,该方法的作用就是将缓存区的数据立即返回。
上面就是我们本次博客要介绍的完整代码了。我们返回了一个 http 报文格式的响应信息,内容是 hello world。在启动项目之后,我们使用浏览器访问 http://localhost:8080/ 即可得到下面的结果
总结
- 首先我们浅谈了一下在应用服务器,网络以及数据之间的关系
- 然后我们通过 Java 代码的方式实现了对于指定端口数据的接收和响应
到这里,手撕 JavaWeb 服务器的第一篇就结束了,是不是很简单?接下来我将会一边学习,一边分享,然后将自己的学习感悟和心得以博客的方式分享出来,感兴趣的同学也可以关注我,以评论和私信的方式和我一起讨论。