网络编程

241 阅读20分钟

网络编程

1. 网络编程概述

1. 1 什么是网络编程

网络编程,是指 在不同进程之间、不同计算机设备之间进行数据交互与通信的编程技术。

网络通信的基本架构主要有两种形式:

  • 一种是CS架构(Client 客户端/Server服务端)

    游戏,微信,QQ

  • 一种是BS架构(Brower 浏览器/Server服务端)

    京东,淘宝,百度等等所有通过浏览器访问的web应用,都是BS架构

  • CS架构的特点: CS架构需要用户在自己的电脑或者手机上安装客户端软件,然后由客户端软件通过网络连接服务器程序,由服务器把数据发给客户端,客户端就可以在页面上看到各种数据了。

1668262460269.png

  • BS架构的特点: BS架构不需要开发客户端软件,用户只需要通过浏览器输入网址就可以直接从服务器获取数据,并由服务器将数据返回给浏览器,用户在页面上就可以看到各种数据了。

1668262589242.png

这两种结构不管是CS、还是BS都是需要用到网络编程的相关技术。我们学习Java的程序员,以后从事的工作方向主要还是BS架构的。

1.2 为什么要学习网络编程

各种高级编程语言几乎都提供了网络编程技术,Java也不例外,相关的类和API都在java.net包下。Java的很多框架,特别是微服务相关的构架,底层都大量使用到了网络编程技术。

所以,我们学习网络编程技术的目的是什么?

其实是开发工作中,几乎是完全不需要我们进行网络编程。但是网络编程作为一项基础技术,掌握它的概念和作用,对于我们将来的学习是有支撑和帮助的,利于快速掌握微服务相关的技术。

学习网络编程的要求:

  • 理解网络编程的作用
  • 理解网络网络编程相关的技术(开发中不需要我们写相关代码)

2. 网络编程三要素★

如果.实现网络通信,必须要满足以下三要素:

  • IP地址:用于在网络中定位到某一台计算机设备
  • 端口:用于在计算机设备中,与指定程序进行通信的出入口
  • 协议:计算机设备之间进行 连接和数据传输 的规则

2.1 IP地址

IP(Ineternet Protocol)全称互联网协议地址,是分配给网络设备的唯一标识。

IP地址分为:

  • IPV4地址:目前应用最为广泛
  • IPV6地址:正在推广,以期望将来替代掉IPv4

2.1.1 IPv4

IPv4介绍

IPV4地址由32个比特位组成,将每8位看成一组,把每一组用十进制表示(叫做点分十进制表示法)

所以就有了我们经常看到的IP地址形式,如:192.168.1.66

1668263179793.png

如何查看本机IPv4地址

如果想查看本机的IP地址,可以在命令行窗口,输入ipconfig命令查看

:bulb: Mac或Linux系统里,在终端里执行命令 ifconfig 可查看ip地址

1668263481919.png

IP地址枯竭问题

IPv4由32bit组成,所以只有2^32^个可用地址,大概是42亿个。

但是随着互联网的不断发展,现在越来越多的设备需要联网,IPV4地址已经不够用了,所以就有了IPv6地址。

2.1.2 IPv6

介绍

IPV6采用128位二进制数据来表示(16个字节),可用地址约有3.4E+38个,号称可以为地球上的每一粒沙子编一个IP地址。

IPV6比较长,为了方便阅读,每16位编成一组,每组采用十六进制数据表示,然后用冒号隔开(称为冒分十六进制表示法),如下图所示

1668263759999.png

如何查看本机IPv6地址

我们在命令行窗口输入ipconfig命令,同样可以看到ipv6地址,如下图所示

1668263881709.png

现在的网络设备,一般IPV4和IPV6地址都是支持的。

2.1.3 域名

什么是域名

当我们需要访问一个网址时,可以根据ip地址进行访问。由于IP地址不方便记忆,并且不能显示地址组织的名称和性质,人们设计出了域名。

比如百度,域名是www.baidu.com,ip地址是110.242.68.4。 很明显,域名比ip更好记:

  • 通过ip地址访问:http://110.242.68.4 (md文档里,按住ctrl点击链接,可以直接在浏览器里打开网址)
  • 通过域名访问:www.baidu.com

域名解析原理

域名和IP其实是一一对应的,由运营商来管理域名和IP的对应关系。我们在浏览器上敲一个域名时,首先由运营商的域名解析服务,把域名转换为ip地址,再通过IP地址去访问对应的服务器设备。

1668264280209.png

2.1.4 ping命令

本机ip:127.0.0.1

本机域名:localhost

测试网络命令:ping 域名或ip ping命令出现以下的提示,说明网络是通过的

1668264499362.png

2.1.5 InetAddress类

Java按照面向对象的思想,提供了一个类封装了IP地址相关的方法,这个类是java.net.InetAddress。常用的API如下:

1668265337659.png

示例代码

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;

public class DemoInetAddress {
    public static void main(String[] args) throws IOException {
        //要求1:获取本机的ip信息
        InetAddress local = InetAddress.getLocalHost();
        String localHostName = local.getHostName();
        System.out.println("本机的主机名 :" + localHostName);
        String localHostAddress = local.getHostAddress();
        System.out.println("本机的ip地址 :" + localHostAddress);

        //要求2:获取www.baidu.com的ip信息
        InetAddress baidu = InetAddress.getByName("www.baidu.com");
        String baiduHostName = baidu.getHostName();
        System.out.println("baidu的域名 : " + baiduHostName);
        String baiduHostAddress = baidu.getHostAddress();
        System.out.println("baidu的ip地址:" + baiduHostAddress);

        //要求3:ping一下www.bai.com,验证能否ping通
        boolean reachable = InetAddress.getByName("www.bai.com").isReachable(200);
        System.out.println("reachable = " + reachable);
    }
}

2.2 端口号

端口:port,是计算机里的程序与外界通信交流的出入口,总共有65536个,范围0~65535。任何应用程序如果想要通过网络与外界交互,就必须占用一个端口。

注意:端口是抢占性资源,哪个程序先占用端口,其它程序就不能用这个端口了,否则会出现端口冲突。

端口分类:了解

  • 周知端口:0~1023,被预先定义的知名应用程序占用(如:HTTP占用80,FTP占用21,HTTPS占用443等等)
  • 注册端口:1024~49151,分配给用户经常或者某些应用程序。应用程序通常使用这些端口
  • 动态端口:49152~65536,之所以称为动态端口,是因为它一般不固定分配给某进程,而是动态分配的。

2.3 协议

网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议。

1668267760295.png

为了让世界上各种上网设备能够互联互通,肯定需要有一个组织出来,指定一个规则,大家都遵守这个规则,才能进行数据通信。

2.3.1 TCP/IP网络模型

1668267891624.png

只要按照OSI网络参考模型制造的设备,就可以在国际互联网上互联互通。其中传输层有两个协议,是我们今天会接触到的(UDP协议、TCP协议)

2.3.2 UDP协议

UDP:User Datagram Protocol,用户数据报协议。为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。效率高但不可靠

特点:

  • 无连接:双向通信的时候不需要建立连接
  • 不可靠:通信中可能丢失数据包
  • 通信效率高

适用场景:

  • 视频直播、游戏等等

1668268046104.png

2.3.3 TCP协议

TCP:Transmission Control Protocol,传输控制协议。是一种面向连接的、可靠的、基于字节流的传输层通信协议。

特点:

  • 面向连接:通信双方必须建立连接,才可能通信
  • 可靠通信:通信时会校验数据完整性,底层会尽一切可能保证数据的完整可靠;如果数据实在是不完整,报错
  • 通信效率略低

适用场景:

  • BS架构的基本都是用TCP。淘宝、百度、京东等等 1668268144938.png

2.3.3.1 TCP建立连接:三次握手

v2-8ce8c897b4d5e7397b25eb4d4b31d7fc_r.jpg

2.3.3.2 TCP断开连接:四次挥手

v2-1b3935fa6bcccddd2d9d1b04d92ef563_r.jpg

  • 三次握手:建立连接

    第一次:客户端向服务端发送请求,尝试建立 (客户端->服务端) 连接

    第二次:服务端收到请求后确认可连接;并向客户端发数据,尝试建立(服务端->客户端)连接

    第三次:客户端收到请求后确认可连接

    最终:双向都确认已建立连接

  • 四次挥手:断开连接

    第一次:客户端向服务端发,尝试断开(客户端->服务端的)连接

    第二次:服务端给客户端返回确认; 客户端->服务端方向成功断开

    第三次:服务端向客户端发,尝试断开(服务端->客户端)连接

    第四次:客户端给服务端返回确认; 服务端->客户端成功断开

3. UDP通信(入门案例)

Java提供了一个类叫java.net.DatagramSocket来完成基于UDP协议的收发数据。使用DatagramSocket收发数据时,数据要以数据包的形式体现,一个数据包限制在64KB以内。

下面我们看一个案例,需要有两个程序,一个表示客户端程序,一个表示服务端程序。

需求:发送者程序发一个字符串数据给 接收者,接收者程序接收数据并打印。

3.1 思路分析

3.1.1 API介绍

DatagramPacket

UDP的数据包对象,相当于一个盒子。UDP通信的双方必须借助这个对象进行数据的包装与拆解:

  • 发送者:把数据封装成DatagramPacket对象,才能把数据发出去
  • 接收者:接收到的数据会放到DatagramPacket对象里,我们才能成功获取数据

构造方法:

image-20240227233751824.png

常用方法: image-20240227233724598.png

DatagramSocket

构造方法 image-20240228111706258.png 常用方法: image-20240228111736174.png

3.1.2 实现过程

UDP发送数据的步骤:

1. 创建`DatagramSocket`对象:`new DatagramSocket()`
2. 准备数据包:`new DatagramPacket(byte[] 数据, int 数据长度, InetAddress.getLocalHost(), int 端口)`
3. 发送数据包:使用`DatagramSocket``send(DatagramPacket对象)`
4. 释放资源:关闭`DatagramSocket`对象,调用close()方法

UDP接收数据的步骤:

1. 创建`DatagramSocket`对象,监听端口:`new DatagramSocket(端口)`
2. 准备空的数据包:`new DatagramPacket(byte[] 数据, int 数据长度)`
3. 接收数据包:使用`DatagramSocket``receive(DatagramPacket对象)`,接收到的数据被封装到了数据包里
4. 从数据包里获取数据:`new String(数据包.getData(), 数据包.getOffset(), 数据包.getLength())`
5. 释放资源:关闭`DatagramSocket`对象,调用close()方法

3.2. 示例代码

3.2.1 发送数据

public class Sender {
    public static void main(String[] args) throws IOException {
        //创建udp套接字对象:这个对象可以帮我们发、收 udp数据包
        DatagramSocket datagramSocket = new DatagramSocket();

        //准备发送数据:new DatagramPacket(数据字节数组,数据长度,目标ip地址,目标端口)
        byte[] data = "hello".getBytes();
        DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getLocalHost(), 7777)
        datagramSocket.send(packet);

        //关闭
        datagramSocket.close();
    }
}

3.3.2 接收数据

public class Receiver {
    public static void main(String[] args) throws IOException {
        //创建udp套接字
        DatagramSocket datagramSocket = new DatagramSocket(7777);
        //准备接收数据
        byte[] buffer = new byte[1024*64];
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
        datagramSocket.receive(packet);
        //解析收到的数据
        String data = new String(packet.getData(), packet.getOffset(), packet.getLength());
        String senderAddr = packet.getAddress().getHostAddress();
        System.out.println("接收到" + senderAddr + "发送的数据:" + data);
        
    }
}

4. UDP通信(持续通信)

刚才的案例,我们只能客户端发一次,服务端接收一次就结束了。下面我们想把这个代码改进一下,

需求:实现客户端不断的发数据,而服务端能不断的接收数据,客户端发送exit时客户端程序退出。

4.1 思路分析

4.2 示例代码

4.2.1 发送数据

public class Sender {
    public static void main(String[] args) throws IOException {
        //创建udp套接字对象:这个对象可以帮我们发、收 udp数据包
        DatagramSocket datagramSocket = new DatagramSocket();

        //准备发送数据
        Scanner scanner = new Scanner(System.in);
        while (true) {
            String str = scanner.next();
            if ("bye".equals(str)) {
                break;
            }

            byte[] data = str.getBytes();
            datagramSocket.send(new DatagramPacket(data, data.length, InetAddress.getLocalHost(), 7777));
        }

        //关闭
        datagramSocket.close();
    }
}

4.2.2 接收数据

public class Receiver {
    public static void main(String[] args) throws IOException {
        //创建udp套接字
        DatagramSocket datagramSocket = new DatagramSocket(7777);
        //准备接收数据
        byte[] buffer = new byte[1024*64];
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

        while (true) {
            datagramSocket.receive(packet);
            //解析收到的数据
            String data = new String(packet.getData(), packet.getOffset(), packet.getLength());
            String senderAddr = packet.getAddress().getHostAddress();
            System.out.println(senderAddr + "发送了数据:" + data);
        }
    }
}

5.TCP通信(入门案例)【掌握】

Java提供了java.net.Socket类和java.net.ServerSocket类来完成TCP通信。

  • ServerSocket:服务端套接字对象。用于监听端口、获取Socket连接对象
  • Socket:相当于连接对象。我们可以从这个连接对象里接收数据,或者通过这个连接对象输出数据

5.1 TCP思路分析

5.1.1 API介绍

Socket

国内通常把Socket称为“套接字”,这个翻译很拉胯。我们可以形象的把它理解为一个管子,管子的一方是当前程序自己,另一方是通信的目标;通过这个管子可以传输数据;而且这个管子是双筒的,可以双向同时传输数据。

方法返回值说明
new Socket(String host, int port)Socket连接目标主机的目标端口
getInputStream()InputStream获取输入流
通过这个输入流可以从Socket里读取数据
getOutputStream()OutputStream获取输出流
通过这个输出流可以向Socket里输出数据
close()void关闭连接

ServerSocket

用于监听指定端口,等待客户端连接的服务端对象

方法返回值说明
new ServerSocket(int port)ServerSocket开始监听指定端口号
accept()Socket获取连接进来的Socket对象
close()void关闭服务器

5.1.2 实现过程

TCP通信的步骤:

 客户端:
  1. 先创建Socket对象,连接目标服务器的目标端口:`new Socket("目标ip", 目标端口号)`
  2. 通过Socket向服务端发数据:`socket.getOutputStream()`,使用这个流输出数据,数据最终流向服务端
  3. 通过Socket从服务端接收数据:`socket.getInputStream()`,使用这个流读取数据,读取的是服务端返回的数据
  4. 关闭流、关闭socket
 服务端
  1. 开始监听某一端口:`new ServerSocket(端口号)`
  2. 接受客户端的连接:`Socket socket = serverSocket.accept()`
  3. 通过Socket接收客户端发过来的数据:`socket.getInputStream()`
  4. 通过Socket向客户端返回数据:`socket.getOutputStream()`
  5. 关闭流、释放资源

5.2 TCP入门示例

5.2.1 TCP客户端

下面我们写一个客户端,用来往服务端发数据。由于原始的字节流不是很好用,这里根据我的经验,将原始的OutputStream包装为DataOutputStream是比较好用的。

/**
 *  目标:完成TCP通信快速入门-客户端开发:实现1发1收。
 */
public class Client {
    public static void main(String[] args) throws Exception {
        // 1、创建Socket对象,并同时请求与服务端程序的连接。
        Socket socket = new Socket("127.0.0.1", 8888);

        // 2、从socket通信管道中得到一个字节输出流,用来发数据给服务端程序。
        OutputStream os = socket.getOutputStream();

        // 3、把低级的字节输出流包装成数据输出流
        DataOutputStream dos = new DataOutputStream(os);

        // 4、开始写数据出去了
        dos.writeUTF("在一起,好吗?");
        dos.close();

        socket.close(); // 释放连接资源
    }
}

5.2.2 TCP服务端

上面我们只是写了TCP客户端,还没有服务端,接下来我们把服务端写一下。这里的服务端用来接收客户端发过来的数据。

/**
 *  目标:完成TCP通信快速入门-服务端开发:实现1发1收。
 */
public class Server {
    public static void main(String[] args) throws Exception {
        System.out.println("-----服务端启动成功-------");
        // 1、创建ServerSocket的对象,同时为服务端注册端口。
        ServerSocket serverSocket = new ServerSocket(8888);

        // 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
        Socket socket = serverSocket.accept();

        // 3、从socket通信管道中得到一个字节输入流。
        InputStream is = socket.getInputStream();

        // 4、把原始的字节输入流包装成数据输入流
        DataInputStream dis = new DataInputStream(is);

        // 5、使用数据输入流读取客户端发送过来的消息
        String rs = dis.readUTF();
        System.out.println(rs);
        // 其实我们也可以获取客户端的IP地址
        System.out.println(socket.getRemoteSocketAddress());

        dis.close();
        socket.close();
    }
}

6. TCP通信(持续通信)

到目前为止,我们已经完成了客户端发送消息、服务端接收消息,但是客户端只能发一次,服务端只能接收一次。现在我想要客户端能过一直发消息,服务端能够一直接收消息。

下面我们把客户端代码改写一下,采用键盘录入的方式发消息,为了让客户端能够一直发,我们只需要将发送消息的代码套一层循环就可以了,当用户输入exit时,客户端退出循环并结束客户端。

6.1 思路分析

6.2 示例代码

6.2.1 TCP客户端

public class Client {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("localhost", 7777);

        Scanner scanner = new Scanner(System.in);
        DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
        DataInputStream dis = new DataInputStream(socket.getInputStream());

        while (true) {
            //读取键盘录入的数据
            System.out.println("请输入:");
            String data = scanner.nextLine();

            //通过socket,把数据发送给服务端
            dos.writeUTF(data);

            if ("bye".equals(data)) {
                break;
            }

            //通过socket,接收服务端返回的结果
            String answer = dis.readUTF();
            System.out.println("服务端响应: " + answer);
        }

        dis.close();
        dos.close();
        scanner.close();
        socket.close();
    }
}

6.2.2 TCP服务端

为了让服务端能够一直接收客户端发过来的消息,服务端代码也得改写一下。我们只需要将读取数据的代码加一个循环就可以了。

但是需要我们注意的时,如果客户端Socket退出之后,就表示连接客户端与服务端的数据通道被关闭了,这时服务端就会出现异常。服务端可以通过出异常来判断客户端下线了,所以可以用try...catch把读取客户端数据的代码套一起来,catch捕获到异常后,打印客户端下线。

public class Server {
    private static Set<Socket> sockets = new HashSet<>();

    public static void main(String[] args) throws IOException {
        //1. 创建服务端套接字对象,监听端口
        ServerSocket serverSocket = new ServerSocket(7777);

        //2. 获取连接对象
        Socket socket = serverSocket.accept();

        DataInputStream dis = new DataInputStream(socket.getInputStream());
        DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
        //3. 不断尝试从socket连接里读取数据、并给客户端返回“收到”
        while (true) {
            //从管道里读取数据 并打印
            String data = dis.readUTF();
            System.out.println("data = " + data);

            //向客户端返回数据
            dos.writeUTF("收到");
        }

    }
}

7. TCP通信(多线程改进)

上一个案例中我们写的服务端程序只能和一个客户端通信,如果有多个客户端连接服务端,此时服务端是不支持的。

7.1 思路分析

为了让服务端能够支持多个客户端通信,就需要用到多线程技术。具体的实现思路如下图所示:每当有一个客户端连接服务端,在服务端这边就为Socket开启一条线程取执行读取数据的操作,来多少个客户端,就有多少条线程。按照这样的设计,服务端就可以支持多个客户端连接了。

1668315156739.png

按照上面的思路,只要改写服务端代码即可。

7.2 示例代码

7.2.1 服务端的工作线程

首先,我们需要写一个服务端的读取数据的线程类,代码如下

public class ServerWorkerThread extends Thread {

    private Socket socket;

    public ServerWorkThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try (
                DataInputStream dis = new DataInputStream(socket.getInputStream());
                DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
        ) {
            //不断尝试从socket连接里读取数据、返回数据
            while (true) {
                //从管道里读取数据 并打印
                String data = dis.readUTF();
                System.out.println("data = " + data);

                //向客户端返回数据
                dos.writeUTF("收到");
            }
        } catch (Exception e) {
            System.out.println(socket.getRemoteSocketAddress() + "连接已断开");
        }
    }
}

7.2.2 服务端的主线程

接下来,再改写服务端的主程序代码,如下:

public class Server {
    public static void main(String[] args) throws IOException {
        //1. 创建服务端套接字对象,监听端口
        ServerSocket serverSocket = new ServerSocket(7777);
        //2. 开始获取连接:可能有多个客户端连接进来,每个连接都会有一个Socket对象。
        while (true) {
            //获取连接对象
            Socket socket = serverSocket.accept();
            //把连接对象交给一个工作线程进行处理。当前线程继续等待连接
            new ServerWorkThread(socket).start();
        }
    }
}

8. BS架构程序(简易版Tomcat)

前面我们所写的代码都是基于CS架构的。我们说网络编程还可以编写BS架构的程序,为了让同学们体验一下BS架构通信,这里我们写一个简易版的程序。仅仅只是体验一下,方便后期理解web应用的运行原理。

8.1 思路分析

BS架构程序的实现原理,如下图所示:不需要开发客户端程序,此时浏览器就是客户端,此时我们只需要写服务端程序就可以了。

1668316417640.png

在BS结构的程序中,浏览器和服务器通信是基于HTTP协议来完成的,浏览器给客户端发送数据需要按照HTTP协议规定好的数据格式发给服务端,服务端返回数据时也需要按照HTTP协议规定好的数据给是发给浏览器,只有这两双方才能完成一次数据交互。

客户端程序不需要我们编写(浏览器就是),所以我们只需要写服务端就可以了。

服务端给客户端响应数据的数据格式(HTTP协议规定数据格式)如下图所示:左图是数据格式,右图是示例。

1668316630797.png

接下来,我们写一个服务端程序按照右图示例的样子,给浏览器返回数据。注意:数据是由多行组成的,必须按照规定的格式来写。

8.2 示例代码

8.2.1 服务端程序

8.2.1.1 工作线程类

先写一个线程类,用于按照HTTP协议的格式返回数据

public class TomcatWorkerThread extends Thread{

    private Socket socket;

    public TomcatWorkerThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            OutputStream os = socket.getOutputStream();

            //向浏览器返回数据头:这些都是浏览器要求返回的内容
            os.write("HTTP/1.1 200\r\n".getBytes());
            os.write("Content-Type: text/html; charset=utf-8\r\n".getBytes());
            os.write("\r\n".getBytes());

            //向浏览器返回数据内容:这些是要让浏览器显示出来的内容。我们把html文件内容返回给浏览器
            FileInputStream is = new FileInputStream("demo03-net/src/main/java/d08_tomcat/hello.html");
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }

            //向客户端浏览器返回一个结束标志,否则浏览器收不到结束标志,就会一直转圈
            socket.shutdownOutput();
        } catch (IOException e) {
            System.out.println("连接已断开");
        }
    }
}

8.2.1.2 主线程

public class TomcatServer {
    public static void main(String[] args) throws IOException {
        //1. 创建ServerSocket,监听8080端口
        ServerSocket ss = new ServerSocket(8080);
        //2. 不断获取客户端连接,每个连接都交给一个工作线程进行处理
        while (true) {
            //获取连接
            Socket socket = ss.accept();
            //把连接交给一个线程进行处理。主线程继续等待下一个连接
            new TomcatWorkerThread(socket).start();
        }
    }
}

8.2.2 使用线程池改进

为了避免服务端创建太多的线程,可以把服务端用线程池改进,提高服务端的性能。

先写一个给浏览器响应数据的线程任务

public class TomcatWorkerRunnable implements Runnable{

    private Socket socket;

    public TomcatWorkerRunnable(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            OutputStream os = socket.getOutputStream();

            //向浏览器返回数据头:这些都是浏览器要求返回的内容
            os.write("HTTP/1.1 200\r\n".getBytes());
            os.write("Content-Type: text/html; charset=utf-8\r\n".getBytes());
            os.write("\r\n".getBytes());

            //向浏览器返回数据内容:这些是要让浏览器显示出来的内容。我们把html文件内容返回给浏览器
            FileInputStream is = new FileInputStream("demo03-net/src/main/java/d08_tomcat/hello.html");
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }

            //向客户端浏览器返回一个结束标志,否则浏览器收不到结束标志,就会一直转圈
            socket.shutdownOutput();
        } catch (IOException e) {
            System.out.println("连接已断开");
        }
    }
}

再改写服务端的主程序,使用ThreadPoolExecutor创建一个线程池,每次接收到一个Socket就往线程池中提交任务就行。

public class TomcatServer {
    public static void main(String[] args) throws IOException {
        //1. 创建ServerSocket,监听8080端口
        ServerSocket ss = new ServerSocket(8080);
        //2. 不断获取客户端连接,每个连接都交给一个工作线程进行处理
        //2.1 准备一个线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 6, 10, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100), new ThreadPoolExecutor.CallerRunsPolicy());

        //2.2 获取连接,交给线程池进行处理
        while (true) {
            //获取连接
            Socket socket = ss.accept();
            //把连接交给一个线程进行处理。主线程继续等待下一个连接
            executor.execute(new TomcatWorkerRunnable(socket));
        }
    }
}