JavaEE之网络编程(TomCat介绍)

计算机网络预备知识

基本的通信架构

  • 基本的通信架构有2种形式:CS架构( Client客户端/Server服务端 ) 、 BS架构(Browser浏览器/Server服务端)。

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

    • BS架构(Browser浏览器/Server服务端)。

网络通信三要素

  • IP地址
    • 设备在网络中的地址,是设备在网络中的唯一标识
  • 端口
    • 应用程序在设备中的唯一标识
  • 协议
    • 连接和数据在网络中传输的规则。

IP地址

  • IP(Internet Protocol):全称"互联网协议地址",是分配给上网设备的唯一标识。
  • 目前,被广泛采用的IP地址形式有两种:IPv4、IPv6
  • IPv4
    • IPv4是Internet Protocol version 4的缩写,它使用32位地址,通常以点分十进制表示。
  • IPv6
    • IPv6是Internet Protocol version 6的缩写,它使用128位地址,号称可以为地球上的每一粒沙子编号。
    • IPv6分成8段,每段每四位编码成一个十六进制位表示, 每段之间用冒号(:)分开,将这种方式称为冒分十六进制。
IP域名(Domain Name)
  • 用于在互联网上识别和定位网站的人类可读的名称。
  • 例如:
  • DNS域名解析(Domain Name System)
    • 是互联网中用于将域名转换为对应IP地址的分布式命名系统。它充当了互联网的"电话簿",将易记的域名映射到数字化的IP地址,使得用户可以通过域名来访问网站和其他网络资源。
DNS域名解析(Domain Name System)
  • 是互联网中用于将域名转换为对应IP地址的分布式命名系统。它充当了互联网的"电话簿",将易记的域名映射到数字化的IP地址,使得用户可以通过域名来访问网站和其他网络资源。
公网IP、内网IP
  • 公网IP:是可以连接到互联网的IP地址;
  • 内网IP:也叫局域网IP,是只能组织机构内部使用的IP地址;例如,192.168. 开头的就是常见的局域网地址,范围为192.168.0.0--192.168.255.255,专门为组织机构内部使用。
  • 本机IP(回环地址)
    • 127.0.0.1、localhost:代表本机IP,只会寻找当前程序所在的主机。
  • IP常用命令
    • ipconfig:查看本机IP地址。
    • ping IP地址:检查网络是否连通
InetAddress类
  • 代表IP地址。
  • InetAddress的常用方法
InetAddress类的常用方法 说明
public static InetAddress getLocalHost() throws UnknownHostException 获取本机IP,返回一个InetAddress对象
public String getHostName() 获取该ip地址对象对应的主机名。
public String getHostAddress() 获取该ip地址对象中的ip地址信息。
public static InetAddress getByName(String host) throws UnknownHostException 根据ip地址或者域名,返回一个inetAddress对象
public boolean isReachable(int timeout) throws IOException 判断主机在指定毫秒内与该ip对应的主机是否能连通

端口

  • 用来标记标记正在计算机设备上运行的应用程序,被规定为一个 16 位的二进制,范围是 0~65535。
  • 端口分类
    • 周知端口:0~1023,被预先定义的知名应用占用(如:HTTP占用 80,FTP占用21)

    • 注册端口:1024~49151,分配给用户进程或某些应用程序。

    • 动态端口:49152到65535,之所以称为动态端口,是因为它一般不固定分配某种进程,而是动态分配。

    • 注意:我们自己开发的程序一般选择使用注册端口,且一个设备中不能出现两个程序的端口号一样,否则报错。

通信协议

  • 网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议。
开放式网络互联标准:OSI网络参考模型
  • OSI网络参考模型:全球网络互联标准。
  • TCP/IP网络模型:事实上的国际标准。
传输层的2个通信协议
  • UDP(User Datagram Protocol):用户数据报协议。

    • 通信效率高,适用于视频直播、语音通话
    • 特点:无连接、不可靠通信。
    • 不事先建立连接,数据按照包发,一包数据包含:自己的IP、端口、目的地IP、端口和数据(限制在64KB内)等。
    • 发送方不管对方是否在线,数据在中间丢失也不管,如果接收方收到数据也不返回确认,故是不可靠的 。
  • TCP(Transmission Control Protocol) :传输控制协议。

    • 特点:面向连接、可靠通信。
    • TCP的最终目的:要保证在不可靠的信道上实现可靠的数据传输。
    • TCP主要有三个步骤实现可靠传输:三次握手建立连接,传输数据进行确认,四次挥手断开连接。
  • 三次握手建立可靠连接

    • 可靠连接:确保通信的双方收发消息都是没问题的(全双工)
  • 四次挥手断开连接

    • 目的:确保通信的双方收发消息都已经完成

Java的网络编程

  • 可以让设备中的程序与网络上其他设备中的程序进行数据交互的技术(实现网络通信)。

UDP通信的实现

  • 特点:无连接、不可靠通信。
  • 不事先建立连接;发送端每次把要发送的数据(限制在64KB内)、接收端IP、等信息封装成一个数据包,发出去就不管了。
  • Java提供了一个java.net.DatagramSocket类来实现UDP通信。
  • UDP通讯的API:
    • DatagramSocket用于创建客户端、服务端

      构造器 说明
      public DatagramSocket() 创建客户端的Socket对象, 系统会随机分配一个端口号。
      public DatagramSocket(int port) 创建服务端的Socket对象, 并指定端口号
      方法 说明
      public void send(DatagramPacket dp) 发送数据包
      public void receive(DatagramPacket p) 使用数据包接收数据
    • DatagramPacket:创建数据包

      构造器 说明
      public DatagramPacket(byte\[\] buf, int length, InetAddress address, int port) 创建发出去的数据包对象
      public DatagramPacket(byte\[\] buf, int length) 创建用来接收数据的数据包
      方法 说明
      public int getLength() 获取数据包,实际接收到的字节个数

使用UDP通信实现:发送消息、接收消息

  • 客户端实现步骤

    • 创建DatagramSocket对象(客户端对象)--- --- 扔韭菜的人
    • 创建DatagramPacket对象封装需要发送的数据(数据包对象)--- --- 韭菜盘子
    • 使用DatagramSocket对象的send方法,传入DatagramPacket对象--- ---开始抛出韭菜
    • 释放资源
  • 客户端代码:

    java 复制代码
    package UDPNetWorkPragaming;
    
    import java.net.DatagramPacket;
    import java.net.DatagramSocket;
    import java.net.InetAddress;
    
    public class Client {
        public static void main(String[] args) throws Exception {
            // 目标:使用udp协议发送数据实现刻客户端发送
    
            // 1. 创建客户端发送对象DatagramSocket
            DatagramSocket socket = new DatagramSocket();
            System.out.println("客户端启动!");
    
            // 2. 创建数据包 DatagramPacket
            byte[] bytes = "hello world".getBytes();
            DatagramPacket datagramPacket = new DatagramPacket(
                    bytes, bytes.length, InetAddress.getLocalHost(), 9999);
    
            // 3. 发送数据
            socket.send(datagramPacket);
            System.out.println("客户端发送数据成功,数据:" + new String(bytes));
    
            // 4. 释放资源
            socket.close();
        }
    }
  • 可以看到只启动客户端,却可以发送信息成功,这恰好说明udp只负责发送,不事先建立连接,不关注通讯是否成功。

  • 服务端实现步骤

    • 创建DatagramSocket对象并指定端口(服务端对象)--- ---接韭菜的人
    • 创建DatagramPacket对象接收数据(数据包对象)--- ---韭菜盘子
    • 使用DatagramSocket对象的receive方法,传入DatagramPacket对象--- ---开始接收韭菜
    • 释放资源
  • 服务端代码

    java 复制代码
    package UDPNetWorkPragaming;
    
    import java.net.DatagramPacket;
    import java.net.DatagramSocket;
    import java.net.InetAddress;
    
    public class Server {
        public static void main(String[] args) throws Exception {
            // 目标:创建UDP通信服务器端接收数据
    
            // 1. 创建服务器端接收数据对象,指定端口号
            DatagramSocket socket = new DatagramSocket(9999);
            System.out.println("服务器已启动!");
    
            // 2. 创建数据包对象,用于封装接收到的数据 DatagramPacket
            byte[] bytes = new byte[1024 * 64];
            DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length);
    
            // 3. 阻塞等待接收数据
            System.out.println("等待接收客户端的数据!");
            socket.receive(datagramPacket); // 阻塞等待接收数据,如果客户端不发送,执行的代码就卡在这里
    
            // 4. 解析数据包,获取数据,并打印出来
            int length = datagramPacket.getLength();
            String data = new String(bytes, 0, length);
    
            // 5. 获取客户端ip地址和端口 (想知道是谁发过来的)
            InetAddress clientNetAddress = datagramPacket.getAddress();
            int clientPort = datagramPacket.getPort();
            String ip = clientNetAddress.getHostAddress();
            System.out.println("数据来自客户端ip=" + ip + ",端口号=" + clientPort + ",接收的数据:" + data);
    
            // 6. 释放资源
            socket.close();
            System.out.println("服务器已关闭!");
        }
    }

使用UDP通信实现:多次发送消息、多次接收消息

  • 客户端可以反复发送数据

    • 使用while死循环不断的接收用户的数据输入,如果用户输入的exit则退出程序
  • 代码

    java 复制代码
    package MoreSendUDPNetWorkPragaming;
    
    import java.net.DatagramPacket;
    import java.net.DatagramSocket;
    import java.net.InetAddress;
    import java.util.Scanner;
    
    public class Client {
        public static void main(String[] args) throws Exception {
            // 目标:使用udp协议发送数据实现客户端反复发送
    
            // 1. 创建客户端发送对象DatagramSocket
            DatagramSocket socket = new DatagramSocket();
            System.out.println("客户端启动!");
    
            // 2. 循环发送:创建键盘录入对象
            Scanner sc = new Scanner(System.in);
    
            while (true) {
                System.out.println("请输入要发送的数据:");
                String data = sc.nextLine(); // 获取输入的一行数据
    
                // 将字符串转为字节数组
                byte[] bytes = data.getBytes();
    
                // 创建数据包对象封装数据
                DatagramPacket datagramPacket = new DatagramPacket(
                        bytes, bytes.length, InetAddress.getLocalHost(), 9999);
    
                // 3. 发送数据
                socket.send(datagramPacket);
                System.out.println("客户端发送数据成功,数据:" + new String(bytes));
    
                // 如果输入的是 exit,则跳出循环,结束程序
                if (data.equals("exit")) {
                    break;
                }
            }
    
            // 4. 释放资源
            socket.close();
            System.out.println("客户端已关闭!");
        }
    }
  • 接收端可以反复接收数据

    • 使用while死循环不断的进行第3步
    java 复制代码
    package MoreSendUDPNetWorkPragaming;
    import java.net.DatagramPacket;
    import java.net.DatagramSocket;
    import java.net.InetAddress;
    
    public class Server {
        public static void main(String[] args) throws Exception {
            // 目标:创建UDP通信服务器端反复接收数据
    
            // 1. 创建服务器端接收数据对象,指定端口号
            DatagramSocket socket = new DatagramSocket(9999);
            System.out.println("服务器已启动!");
    
            // 2. 创建数据包对象,用于封装接收到的数据
            byte[] bytes = new byte[1024 * 64];
            DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length);
    
            System.out.println("等待接收客户端的数据!");
    
            // 3. 阻塞等待接收数据(通过死循环实现反复接收)
            while (true) {
                socket.receive(datagramPacket); // 阻塞等待接收数据
    
                // 4. 解析数据包,获取数据
                int length = datagramPacket.getLength();
                String data = new String(bytes, 0, length);
    
                // 如果接收到的是 exit,说明客户端通知关闭,服务端也跳出循环
                if (data.equals("exit")) {
                    break;
                }
    
                // 5. 获取客户端ip地址和端口
                InetAddress clientNetAddress = datagramPacket.getAddress();
                int clientPort = datagramPacket.getPort();
                String ip = clientNetAddress.getHostAddress();
    
                System.out.println("数据来自客户端ip=" + ip + ",端口号=" + clientPort + ",接收的数据:" + data);
            }
    
            // 6. 释放资源
            socket.close();
            System.out.println("服务器已关闭!");
        }
    }

TCP通信的实现

  • 特点:面向连接、可靠通信。
  • 通信双方事先会采用"三次握手"方式建立可靠连接,实现端到端的通信;底层能保证数据成功传给服务端。
  • Java提供了一个java.net.Socket类来实现TCP通信。

TCP通信的实现一发一收

  • 客户端实现API:通过java.net包下的Socket类来实现的。
构造器 说明
public Socket(String host , int port) 根据指定的服务器ip、端口号请求与服务端建立连接,连接通过,就获得了客户端socket
方法 说明
public OutputStream getOutputStream() 获得字节输出流对象
public InputStream getInputStream() 获得字节输入流对象
  • 客户端实现步骤
    • 创建客户端的Socket对象,请求与服务端的连接。
    • 使用socket对象调用getOutputStream()方法得到字节输出流。
    • 使用字节输出流完成数据的发送。
    • 释放资源:关闭socket管道。
  • 客户端实现代码:
java 复制代码
package TCPNetWorkProgaming;

import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;

public class TCPClient {
    public static void main(String[] args) {
        try {
            System.out.println("===客户端启动===");

            // 1. 创建Socket对象,请求连接服务端的IP和端口
            Socket socket = new Socket("127.0.0.1", 8888);

            // 2. 从Socket对象中获取字节输出流,并包装为高级流(打印流 PrintStream)
            OutputStream os = socket.getOutputStream();
            PrintStream ps = new PrintStream(os);

            // 3. 发送数据 (使用println发送,配合服务端的readLine读取)
            ps.println("你好,服务端!这是一条来自客户端的测试消息。");
            ps.flush(); // 刷新流,确保数据发出去
            System.out.println("消息已发送!");

            // 4. 释放资源
            ps.close();
            socket.close();

        } catch (Exception e) {
            e.printStackTrace(System.out);
        }
    }
}
  • 服务端实现API:通过java.net包下的ServerSocket类来实现的。
构造器 说明
public Socket(String host , int port) 根据指定的服务器ip、端口号请求与服务端建立连接,连接通过,就获得了客户端socket
方法 说明
public OutputStream getOutputStream() 获得字节输出流对象
public InputStream getInputStream() 获得字节输入流对象
  • 服务端实现步骤
    • 创建ServerSocket对象,注册服务端端口。
    • 调用ServerSocket对象的accept()方法,等待客户端的连接,并得到Socket管道对象。
    • 通过Socket对象调用getInputStream()方法得到字节输入流、完成数据的接收。
    • 释放资源:关闭socket管道
  • 实现服务端代码:
java 复制代码
package TCPNetWorkProgaming;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {
    public static void main(String[] args) {
        try {
            System.out.println("===服务端启动,等待连接===");

            // 1. 创建ServerSocket对象,注册服务端端口 (例如:8888)
            ServerSocket serverSocket = new ServerSocket(8888);

            // 2. 调用accept()方法,阻塞等待客户端的连接,并得到Socket管道对象
            Socket socket = serverSocket.accept();
            System.out.println("有客户端连接成功!IP地址: " + socket.getInetAddress().getHostAddress());

            // 3. 通过Socket得到字节输入流,并包装为高级流(缓冲字符输入流)完成数据接收
            InputStream is = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is));

            // 读取客户端发送的一行数据
            String msg;
            if ((msg = br.readLine()) != null) {
                System.out.println("服务端收到消息:" + msg);
            }

            // 4. 释放资源:关闭流和socket管道
            br.close();
            socket.close();
            serverSocket.close(); // 实际开发中服务端通常不关闭,这里仅为演示单次一发一收

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

TCP通信的实现多发多收

  • 客户端使用死循环,让用户不断输入消息。

    java 复制代码
    package MoreSendTCPNetWorkPragaming;
    import java.io.OutputStream;
    import java.io.PrintStream;
    import java.net.Socket;
    import java.util.Scanner;
    
    public class TCPClient {
        public static void main(String[] args) {
            try {
                System.out.println("===客户端启动===");
                // 1. 连接服务端
                Socket socket = new Socket("127.0.0.1", 8888);
    
                // 2. 获取输出流
                OutputStream os = socket.getOutputStream();
                PrintStream ps = new PrintStream(os);
    
                // 引入Scanner接收用户键盘输入
                Scanner sc = new Scanner(System.in);
    
                // 【改造核心】使用死循环,让用户不断输入消息
                while (true) {
                    System.out.print("请输入要发送的消息(输入exit退出):");
                    String msg = sc.nextLine();
    
                    // 判断用户是否输入了退出指令 (忽略大小写)
                    if ("exit".equalsIgnoreCase(msg) || "exits".equalsIgnoreCase(msg)) {
                        System.out.println("退出通信,断开连接...");
                        break; // 跳出死循环
                    }
    
                    // 3. 发送数据
                    ps.println(msg);
                    ps.flush();
                }
    
                // 4. 释放资源
                sc.close();
                ps.close();
                socket.close();
    
            } catch (Exception e) {
                System.out.println("客户端发生异常:" + e.getMessage());
            }
        }
    }
  • 服务端也使用死循环,控制服务端程序收完消息后,继续去接收下一个消息。

    java 复制代码
    package MoreSendTCPNetWorkPragaming;
    
    import java.io.BufferedReader;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    public class TCPServer {
        public static void main(String[] args) {
            try {
                System.out.println("===服务端启动,等待连接===");
                // 1. 创建ServerSocket
                ServerSocket serverSocket = new ServerSocket(8888);
    
                // 2. 等待客户端连接
                Socket socket = serverSocket.accept();
                System.out.println("有客户端连接成功!IP: " + socket.getInetAddress().getHostAddress());
    
                // 3. 获取输入流
                InputStream is = socket.getInputStream();
                BufferedReader br = new BufferedReader(new InputStreamReader(is));
    
                // 【改造核心】使用循环不断接收该客户端发来的消息
                String msg;
                while ((msg = br.readLine()) != null) {
                    System.out.println("服务端收到消息:" + msg);
                }
    
                System.out.println("客户端已下线,停止接收。");
    
                // 4. 释放资源
                br.close();
                socket.close();
                serverSocket.close();
    
            } catch (Exception e) {
                System.out.println("服务端发生异常:" + e.getMessage());
            }
        }
    }

TCP通信-支持与多个客户端同时通信

  • 目前我们开发的服务端程序,是否可以支持同时与多个客户端通信 ?

    • 不可以
    • 因为服务端现在只有一个主线程,只能处理一个客户端的消息。
  • 如何实现服务端同时接收多个客户端的消息的?

    • 主线程定义了循环负责接收客户端Socket管道连接
    • 每接收到一个Socket通信管道后分配一个独立的线程负责处理它。
  • 实现步骤

    • 实现Rannabele接口,在里面封装处理客户端Socket的代码
    • 创建线程池类负责循环接收客户端Socket管道连接,并从线程池中分配线程给Socket管道连接
    • 设计客户端发送信息的代码
  • 实现Runnable的代码

    java 复制代码
    package ThreadTCPNetWorkPrograming;
    
    import java.io.BufferedReader;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.net.Socket;
    
    public class ClientHandlerRunnable implements Runnable{
        private Socket socket;
    
        // 通过构造器接收主线程传递过来的 Socket 管道
        public ClientHandlerRunnable(Socket socket) {
            this.socket = socket;
        }
        @Override
        public void run() {
            String clientIP = socket.getInetAddress().getHostAddress();
            System.out.println("【线程池分配】线程 " + Thread.currentThread().getName() + " 正在处理客户端: " + clientIP);
    
            try {
                // 获取输入流,包装为高级流
                InputStream is = socket.getInputStream();
                BufferedReader br = new BufferedReader(new InputStreamReader(is));
    
                String msg;
                // 这里的 readLine() 会阻塞,但由于在独立线程/线程池中,不会影响主线程接收新客户端
                while ((msg = br.readLine()) != null) {
                    System.out.println("【" + clientIP + "】发来消息:" + msg);
                }
            } catch (Exception e) {
                System.out.println("客户端 " + clientIP + " 异常断开:" + e.getMessage());
            } finally {
                // 释放资源(必须放在 finally 中确保执行)
                try {
                    System.out.println("客户端 " + clientIP + " 已下线。");
                    if (socket != null && !socket.isClosed()) {
                        socket.close();
                    }
                } catch (Exception e) {
                    e.printStackTrace(System.out);
                }
            }
        }
    }  
  • 线程池

    java 复制代码
    package ThreadTCPNetWorkPrograming;
    
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    
    public class TCPServerThreadPool {
        public static void main(String[] args) {
            // 【针对 8核 CPU 的线程池配置】
            // 核心线程数:8 * 2 = 16 (I/O密集型任务推荐)
            // 最大线程数:这里设定为 32 (应对突发流量)
            // 任务队列:容量为 100 的有界队列
            ThreadPoolExecutor pool = new ThreadPoolExecutor(
                    16,
                    32,
                    60,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(100)
            );
    
            try {
                System.out.println("=== 线程池版服务端启动,等待海量连接 ===");
                ServerSocket serverSocket = new ServerSocket(8888);
    
                // 主线程使用死循环,专门负责接收客户端连接
                while (true) {
                    // 1. 阻塞等待客户端连接
                    Socket socket = serverSocket.accept();
                    System.out.println(">>> 发现新客户端连接!IP: " + socket.getInetAddress().getHostAddress());
    
                    // 2. 将得到的 Socket 管道封装成 Runnable 任务
                    ClientHandlerRunnable target = new ClientHandlerRunnable(socket);
    
                    // 3. 将任务交给线程池处理(向线程池请求线程)
                    pool.execute(target);
    
                    // 此时主线程立刻回到循环顶部,继续 accept() 下一个连接,毫无阻塞!
                }
    
            } catch (Exception e) {
                e.printStackTrace(System.out);
            } finally {
                pool.shutdown();
            }
        }
    }
  • 客户端代码

    java 复制代码
    package ThreadTCPNetWorkPrograming;
    import java.io.OutputStream;
    import java.io.PrintStream;
    import java.net.Socket;
    import java.util.Scanner;
    
    public class TCPClient {
        public static void main(String[] args) {
            try {
                System.out.println("===客户端启动===");
                // 1. 连接服务端
                Socket socket = new Socket("127.0.0.1", 8888);
    
                // 2. 获取输出流
                OutputStream os = socket.getOutputStream();
                PrintStream ps = new PrintStream(os);
    
                // 引入Scanner接收用户键盘输入
                Scanner sc = new Scanner(System.in);
    
                // 【改造核心】使用死循环,让用户不断输入消息
                while (true) {
                    System.out.print("请输入要发送的消息(输入exit退出):");
                    String msg = sc.nextLine();
    
                    // 判断用户是否输入了退出指令 (忽略大小写)
                    if ("exit".equalsIgnoreCase(msg) || "exits".equalsIgnoreCase(msg)) {
                        System.out.println("退出通信,断开连接...");
                        break; // 跳出死循环
                    }
    
                    // 3. 发送数据
                    ps.println(msg);
                    ps.flush();
                }
    
                // 4. 释放资源
                sc.close();
                ps.close();
                socket.close();
    
            } catch (Exception e) {
                System.out.println("客户端发生异常:" + e.getMessage());
            }
        }
    }
  • 将ide启动入口设置为允许多实例

  • 启动服务端之后,启动多个客户端实例,下方是运行测试结果

BS架构的原理

模拟BS架构网页

  • 要求从浏览器中访问服务器

  • 并立即让服务器响应一个很简单的网页给浏览器展示

  • 网页内容就是"Hello 程序员"

  • HTTP协议规定:响应给浏览器的数据格式必须满足如下格式

简单模拟BS架构网页

  • 实现步骤

    • 创建 ServerSocket 对象,注册服务端端口
    • 调用 accept() 方法,循环等待浏览器的连接,并得到 Socket 管道
    • 解析请求协议与路由,多线程分发处理
    • 严格按照 HTTPHTTP HTTP 协议格式,通过输出流响应数据并释放资源
  • 代码示例

    java 复制代码
    package SimpleBrowserServiceArchitecture;
    
    import java.io.IOException;
    import java.io.OutputStream;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.nio.charset.StandardCharsets;
    
    public class SimpleHttpServer {
        public static void main(String[] args) {
            // 定义服务器监听的端口号
            int port = 8080;
    
            try (ServerSocket serverSocket = new ServerSocket(port)) {
                System.out.println("====== 服务器已启动,正在监听端口: " + port + " ======");
    
                // 循环监听客户端(浏览器)的连接
                while (true) {
                    // 1. 阻塞等待浏览器的连接请求
                    Socket socket = serverSocket.accept();
                    System.out.println("收到来自浏览器的连接请求...");
    
                    // 2. 开启一个新线程处理请求(防止阻塞其他浏览器访问)
                    new Thread(() -> handleRequest(socket)).start();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        private static void handleRequest(Socket socket) {
            // 使用 try-with-resources 自动关闭资源
            try (socket; OutputStream os = socket.getOutputStream()) {
    
                // 3. 准备要返回的网页内容 (HTML 格式,解决中文乱码)
                String htmlContent = "<!DOCTYPE html>" +
                        "<html>" +
                        "<head>" +
                        "    <meta charset='UTF-8'>" +
                        "    <title>BS架构演示</title>" +
                        "</head>" +
                        "<body>" +
                        "    <h1>Hello 程序员</h1>" +
                        "</body>" +
                        "</html>";
    
                // 将字符串转换为字节数组
                byte[] contentBytes = htmlContent.getBytes(StandardCharsets.UTF_8);
    
                // 4. 核心:严格按照 HTTP/1.1 协议格式编写响应头
                // 必须包含 状态行、响应头、空行、响应体
                String httpResponse = "HTTP/1.1 200 OK\r\n" +                  // 状态行
                        "Content-Type: text/html; charset=UTF-8\r\n" +   // 响应头:告诉浏览器这是网页,编码是UTF-8
                        "Content-Length: " + contentBytes.length + "\r\n" + // 响应头:内容长度
                        "\r\n";                                          // 空行(必须有!)
    
                // 5. 将 HTTP 响应头和网页内容发送给浏览器
                os.write(httpResponse.getBytes(StandardCharsets.UTF_8));
                os.write(contentBytes);
                os.flush(); // 刷新缓冲区,确保数据发送完毕
    
                System.out.println("成功响应网页内容!");
    
            } catch (IOException e) {
                System.err.println("处理请求时发生异常: " + e.getMessage());
            }
        }
    }
  • 访问网页结果

使用线程池优化模拟的BS架构网页

  • 可以使用线程池进行优化,每个浏览器对网页的请求都作为一个线程。

  • 同时结合先前学的消息交互,设计一个可以发消息的页面

  • 实现步骤

    • 初始化线程池,注册并绑定服务端端口
    • 循环接收浏览器连接,将管道包装并交付线程池执行
    • 异步解析 HTTP协议,精准提取 URL传参数据
    • 动态组装交互表单,标准响应后释放本次短连接
  • 代码示例:

    • 建立线程池:
    java 复制代码
    package BrowserServiceArchitecture;
    
    import java.io.IOException;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    
    public class BSThreadPoolServer {
        public static void main(String[] args) {
            // 1. 初始化线程池 (根据图片和上文代码优化配置)
            // 核心线程16,最大32,队列100。匹配图片中的"核心线程"和"任务队列"结构
            ThreadPoolExecutor pool = new ThreadPoolExecutor(
                    16,
                    32,
                    60,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(100)
            );
    
            int port = 8080;
    
            try (ServerSocket serverSocket = new ServerSocket(port)) {
                System.out.println("====== B/S 架构服务端已启动 (线程池版) ======");
                System.out.println("请在浏览器中访问: http://localhost:" + port);
    
                // 主线程只负责不断接收客户端(浏览器)连接
                while (true) {
                    // 阻塞等待浏览器的连接请求
                    Socket socket = serverSocket.accept();
    
                    // 将接收到的浏览器 Socket 包装为 Runnable 任务,扔进任务队列,由线程池去处理
                    pool.execute(new HttpHandlerRunnable(socket));
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                pool.shutdown();
            }
        }
    }
    • 实现Runnable接口
    java 复制代码
    package BrowserServiceArchitecture;
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.net.Socket;
    import java.net.URLDecoder;
    import java.nio.charset.StandardCharsets;
    public class HttpHandlerRunnable implements Runnable{
        private Socket socket;
    
        public HttpHandlerRunnable(Socket socket) {
            this.socket = socket;
        }
    
        @Override
        public void run() {
            String clientIP = socket.getInetAddress().getHostAddress();
            System.out.println("【线程池分配】线程 " + Thread.currentThread().getName() + " 正在处理浏览器请求: " + clientIP);
    
            try {
                // 获取输入输出流
                BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                OutputStream os = socket.getOutputStream();
    
                // 1. 读取 HTTP 请求的第一行 (例如: GET /?msg=你好 HTTP/1.1)
                String requestLine = br.readLine();
                if (requestLine != null && requestLine.startsWith("GET")) {
                    // 2. 解析浏览器通过 URL 传过来的消息参数
                    String[] parts = requestLine.split(" ");
                    if (parts.length > 1) {
                        String url = parts[1];
                        // 判断 URL 中是否包含了用户提交的信息 "?msg=xxx"
                        if (url.contains("msg=")) {
                            // 提取出消息内容
                            String msg = url.substring(url.indexOf("msg=") + 4);
                            // 因为浏览器传中文会进行 URL 编码,这里需要解码
                            msg = URLDecoder.decode(msg, "UTF-8");
                            System.out.println(">>> 收到来自浏览器【" + clientIP + "】的消息:" + msg);
                        }
                    }
                }
    
                // 读取掉剩余的 HTTP 请求头(清理缓冲区,防止阻塞)
                while (br.ready()) {
                    String header = br.readLine();
                    if (header == null || header.isEmpty()) break;
                }
    
                // 3. 构建 HTTP 响应内容 (带交互表单的 HTML 页面)
                String htmlContent = "<!DOCTYPE html>" +
                        "<html>" +
                        "<head>" +
                        "    <meta charset='UTF-8'>" +
                        "    <title>BS架构通信</title>" +
                        "    <style>" +
                        "       body { font-family: Arial; margin: 40px; background-color: #f4f4f9; }" +
                        "       .chat-box { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); width: 400px; }" +
                        "       input[type=text] { width: 70%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; }" +
                        "       button { padding: 10px 20px; background-color: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; }" +
                        "       button:hover { background-color: #218838; }" +
                        "    </style>" +
                        "</head>" +
                        "<body>" +
                        "    <div class='chat-box'>" +
                        "        <h2>向服务器发送消息</h2>" +
                        "        <form action='/' method='GET'>" +
                        "            <input type='text' name='msg' placeholder='请输入要发送的消息...' autocomplete='off' autofocus required />" +
                        "            <button type='submit'>发送</button>" +
                        "        </form>" +
                        "    </div>" +
                        "</body>" +
                        "</html>";
    
                byte[] contentBytes = htmlContent.getBytes(StandardCharsets.UTF_8);
    
                // 4. 组装 HTTP 响应头
                String httpResponse = "HTTP/1.1 200 OK\r\n" +
                        "Content-Type: text/html; charset=UTF-8\r\n" +
                        "Content-Length: " + contentBytes.length + "\r\n" +
                        "\r\n";
    
                // 5. 写入流并返回给浏览器
                os.write(httpResponse.getBytes(StandardCharsets.UTF_8));
                os.write(contentBytes);
                os.flush();
    
            } catch (Exception e) {
                System.err.println("处理浏览器请求时发生异常: " + e.getMessage());
            } finally {
                // 释放资源,断开这次 HTTP 短连接
                try {
                    if (socket != null && !socket.isClosed()) {
                        socket.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace(System.out);
                }
            }
        }
    }
  • 运行后页面,进行接收消息测试

模拟BS架构简易聊天室

  • 继续优化,我们可以利用广播消息机制,和URL路由逻辑设计一个简易聊天室

  • 实现步骤

    • 初始化高并发线程池,设立全局并发安全的"广播信道容器"
    • 采用多线程路由分发,针对不同 URL 路径实施"动静分离"处理
    • 建立长连接挂起机制(SSE 订阅),将特定输出流持久化锁定
    • 异步接收短连接消息,触发全网管道遍历广播
  • 代码示例

    • 设计聊天室线程池与广播消息规则
    java 复制代码
    package BrowserServiceArchitecture.BrowserServiceArchitectureInstantMessaging;
    
    import java.io.IOException;
    import java.io.PrintWriter;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.CopyOnWriteArrayList;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    
    public class BSChatServer {
        // 【核心新增】用于存储所有连接上来的浏览器"广播通道"
        // 使用 CopyOnWriteArrayList 保证多线程并发安全
        static final CopyOnWriteArrayList<PrintWriter> onlineClients = new CopyOnWriteArrayList<>();
    
        public static void main(String[] args) {
            // 1. 初始化线程池
            ThreadPoolExecutor pool = new ThreadPoolExecutor(
                    16,
                    32,
                    60,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(100)
            );
    
            int port = 8080;
            try (ServerSocket serverSocket = new ServerSocket(port)) {
                System.out.println("====== B/S 即时聊天室服务端已启动 ======");
                System.out.println("请打开多个浏览器窗口访问: http://localhost:" + port);
    
                while (true) {
                    Socket socket = serverSocket.accept();
                    pool.execute(new ChatHandlerRunnable(socket));
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                pool.shutdown();
            }
        }
        /**
         * 将消息广播给所有在线的浏览器
         */
        public static void broadcastMessage(String message) {
            System.out.println("【广播消息】: " + message);
            for (PrintWriter clientWriter : onlineClients) {
                try {
                    // SSE 协议规定的数据格式:以 "data: " 开头,以 "\n\n" 结尾
                    clientWriter.print("data: " + message + "\n\n");
                    clientWriter.flush();
                } catch (Exception e) {
                    // 如果发送失败,说明客户端断开了,将其移出列表
                    onlineClients.remove(clientWriter);
                }
            }
        }
    }
    • 设计处理消息线程
    java 复制代码
    package BrowserServiceArchitecture.BrowserServiceArchitectureInstantMessaging;
    
    import java.io.*;
    import java.net.Socket;
    import java.net.URLDecoder;
    import java.nio.charset.StandardCharsets;
    
    import static BrowserServiceArchitecture.BrowserServiceArchitectureInstantMessaging.BSChatServer.onlineClients;
    
    public class ChatHandlerRunnable implements Runnable {
        private Socket socket;
    
        public ChatHandlerRunnable(Socket socket) {
            this.socket = socket;
        }
    
        @Override
        public void run() {
            try {
                InputStream is = socket.getInputStream();
                BufferedReader br = new BufferedReader(new InputStreamReader(is));
                OutputStream os = socket.getOutputStream();
                PrintWriter pw = new PrintWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8));
    
                String requestLine = br.readLine();
                if (requestLine == null) return;
    
                // 简单的路由分发
                if (requestLine.startsWith("GET /events")) {
                    // 1. 处理 SSE 订阅请求(保持长连接,不关闭 Socket)
                    handleSSEConnection(pw);
                } else if (requestLine.startsWith("GET /send")) {
                    // 2. 处理发送消息请求
                    handleSendMessage(requestLine, pw);
                    socket.close(); // 发送完毕就断开这次短连接
                } else {
                    // 3. 处理默认的网页请求
                    handleHtmlPage(pw);
                    socket.close(); // 网页给完就断开
                }
            } catch (Exception e) {
                // 异常处理省略
            }
        }
    
    // ================== 三个核心路由逻辑 ==================
    
        private void handleSSEConnection(PrintWriter pw) {
            // 返回 SSE 需要的特殊响应头
            pw.print("HTTP/1.1 200 OK\r\n");
            pw.print("Content-Type: text/event-stream; charset=UTF-8\r\n");
            pw.print("Cache-Control: no-cache\r\n");
            pw.print("Connection: keep-alive\r\n"); // 告诉浏览器:保持连接,别断!
            pw.print("\r\n");
            pw.flush();
    
            // 将这个长连接对应的输出流加入到广播列表中
            onlineClients.add(pw);
            // 注意:这里我们故意不关闭 Socket,让线程或连接挂起,随时准备推送数据
        }
    
        private void handleSendMessage(String requestLine, PrintWriter pw) throws Exception {
            String[] parts = requestLine.split(" ");
            if (parts.length > 1) {
                String url = parts[1];
                if (url.contains("msg=")) {
                    String msg = url.substring(url.indexOf("msg=") + 4);
                    msg = URLDecoder.decode(msg, "UTF-8");
    
                    // 提取到消息后,触发全网广播!
                    BSChatServer.broadcastMessage(msg);
                }
            }
            // 返回一个简单的成功响应即可
            pw.print("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK");
            pw.flush();
        }
    
        private void handleHtmlPage(PrintWriter pw) {
            String htmlContent = "<!DOCTYPE html>\n" +
                    "<html>\n" +
                    "<head>\n" +
                    "    <meta charset='UTF-8'>\n" +
                    "    <title>B/S 极简聊天室</title>\n" +
                    "    <style>\n" +
                    "       body { font-family: Arial; padding: 20px; background: #ececec; }\n" +
                    "       #chatBox { height: 300px; background: white; border: 1px solid #ccc; overflow-y: scroll; padding: 10px; margin-bottom: 10px; }\n" +
                    "       .msg { margin: 5px 0; padding: 5px; background: #e3f2fd; border-radius: 4px; }\n" +
                    "       input { width: 70%; padding: 8px; }\n" +
                    "       button { padding: 8px 15px; cursor: pointer; }\n" +
                    "    </style>\n" +
                    "</head>\n" +
                    "<body>\n" +
                    "    <h2>即时聊天室 (SSE 版)</h2>\n" +
                    "    <div id='chatBox'></div>\n" +
                    "    <input type='text' id='msgInput' placeholder='输入消息...' onkeydown='if(event.keyCode==13) sendMsg()'/>\n" +
                    "    <button onclick='sendMsg()'>发送</button>\n" +
                    "\n" +
                    "    <script>\n" +
                    "        // 1. 监听服务器推送的消息\n" +
                    "        const source = new EventSource('/events');\n" +
                    "        source.onmessage = function(event) {\n" +
                    "            const chatBox = document.getElementById('chatBox');\n" +
                    "            chatBox.innerHTML += '<div class="msg"><b>匿名者:</b> ' + event.data + '</div>';\n" +
                    "            chatBox.scrollTop = chatBox.scrollHeight;\n" +
                    "        };\n" +
                    "\n" +
                    "        // 2. 异步发送消息给服务器\n" +
                    "        function sendMsg() {\n" +
                    "            const input = document.getElementById('msgInput');\n" +
                    "            const text = input.value.trim();\n" +
                    "            if (text) {\n" +
                    "                fetch('/send?msg=' + encodeURIComponent(text));\n" +
                    "                input.value = '';\n" +
                    "            }\n" +
                    "        }\n" +
                    "    </script>\n" +
                    "</body>\n" +
                    "</html>";
    
            byte[] contentBytes = htmlContent.getBytes(StandardCharsets.UTF_8);
            pw.print("HTTP/1.1 200 OK\r\n");
            pw.print("Content-Type: text/html; charset=UTF-8\r\n");
            pw.print("Content-Length: " + contentBytes.length + "\r\n");
            pw.print("\r\n");
            pw.print(htmlContent);
            pw.flush();
        }
    }
  • 测试运行

    • 页面和功测试能如下,可以实现实时聊天
    • 后台运行记录

网络编程在Web服务器中的应用(TomCat服务器为例)

Tomcat是什么

  • Tomcat 是 Apache 软件基金会(ASF)开发的开源 Web 服务器和 Servlet 容器,主要用于运行 Java Web 应用程序。它实现了 Java Servlet、JavaServer Pages(JSP)、Java Expression Language(EL)和 Java WebSocket 等 Java EE 规范,是 Java Web 开发中非常流行的部署平台。
  • 核心作用
    • 接收和处理 HTTP 请求:Tomcat 通过 Connector 组件监听指定端口(默认 8080),接收浏览器或客户端的请求。
    • 调度 Servlet 处理业务:它不会自己写业务逻辑,而是将请求交给对应的 Servlet(或 JSP)处理。
    • 返回响应结果:Servlet 处理完后,Tomcat 将结果(HTML、JSON 等)返回给用户浏览器 Servlet 处理完后,Tomcat 将结果(HTML、JSON 等)返回给用户浏览器。
    • 持静态和动态资源:静态资源(HTML、CSS、JS、图片等)由默认 Servlet 直接返回;动态资源由 Servlet 或 JSP 处理。
  • 常见应用场景:
    • 部署 Java Web 项目(如 Spring Boot、Struts2、SpringMVC 等框架的应用)。
    • 运行 JSP 页面,Tomcat 会将其编译为 Servlet 后执行。
    • 作为企业级应用服务器的轻量级替代(相比 WebLogic、WebSphere 等,Tomcat 更轻量、免费)。

TomCat是怎么实现的(和网络编程的关系)

  • Tomcat 的核心底层完全就是通过这样的网络编程(Socket、线程池、I/O 模型)来实现的 ,刚才手写的这个极简服务端,其实就是一个极其微型的 Tomcat 雏形。现实中的 Tomcat 无论功能多复杂,它打开网络大门的第一步,依然是向操作系统申请端口、监听连接。

  • 我们可以利用实现简易聊天室的代码和 Tomcat 的核心架构做个对比,你会发现两者的设计思想是一模一样的

    功能角色 在代码中 在 Tomcat 中 核心作用
    接入层(连接器) new ServerSocket(8080) Connector (连接器) 负责在指定端口死循环等待浏览器的 TCP 连接(accept())。
    高并发处理 ThreadPoolExecutor Executor (线程池) 负责管理一大堆工作线程,有请求来了就丢给线程池里的线程去处理。
    路由与业务(容器) if (requestLine.startsWith(...)) Container (Engine/Host/Context/Wrapper) 负责解析 HTTP 协议,并根据 URL 路径,把请求分发给对应的 Servlet 处理器。
  • Tomcat 究竟比我们的代码多做了什么

    • 协议解析更完美、更通用
    • 真正的"路由分发":引入 Servlet 规范
    • 性能的终极优化:从 BIO 到 NIO:
      • 在先前的聊天室的代码中,用的BIO模式(阻塞模式) :为了保持长连接,你故意让线程不退出、Socket 不关闭 。这意味着一个在线用户就会死死霸占线程池里的一个核心线程
      • Tomcat 的优化NIO模式(非阻塞模式 ):Tomcat 引入了 Java 的 NIO(New I/O) 技术和 Selector(多路复用器机制)。即使有一万个用户保持长连接(比如像 SSE 或 WebSocket 挂着),在没有发消息的静默状态下,这一万个连接不需要占用任何工作线程。只有当某个连接真正传来数据时,Tomcat 才会临时派一个线程去处理,处理完立刻放回线程池。这就是 Tomcat 能支持几万高并发的秘密所在。
  • 总结

    • 以后学习 Tomcat、Servlet、甚至 Spring Boot 时,千万不要觉得它们是高深莫测的魔法。
    • 请始终记住你现在写的这两段 Java 代码:无论上层框架怎么封装,最底层的源头,永远是一个死循环的 ServerSocket.accept(),和顺着 Socket.getOutputStream() 吐给浏览器的 HTTP 字符串。 你现在已经把这条路彻底走通了!
相关推荐
预知同行1 小时前
订单超时自动取消,你真的做对了吗?从定时任务到千万级高并发方案的完整演进
后端
sdm0704271 小时前
网络原理-2.传输层协议TCP
网络·网络协议·tcp/ip
ServBay2 小时前
2026年重新定义 Python 开发工作流的8个现代化工具
后端·python
Java内核笔记2 小时前
Spring Security 过滤器链的构建过程:从 HttpSecurity 到 DefaultSecurityFilterChain
后端
自进化Agent智能体2 小时前
Hermes 自进化Skill:让AI能力自己长出来
后端
宇宙之一粟3 小时前
如何判断是时候离开了
后端·程序员
武子康4 小时前
Java-16 深入浅出MyBatis 架构设计与源码剖析:从初始化到 SQL 执行全流程
java·后端
代码中介商4 小时前
HTTP 完全指南(一):请求与响应报文结构深度详解
网络·网络协议·http