网络编程

发布时间 2023-06-30 16:00:59作者: lawranceCha

网络编程

引言

什么是网络编程?

  • 网络编程可以让程序与网络上的其他设备中的程序进行数据交互。

网络通信的基本形式

常见的通信模式有如下2种形式:

Client-Server(CS) 、 Browser/Server(BS)

1、网络通信三要素

1.1、三要素概述

1.1.1、实现网络编程关键的三要素

  • IP地址:设备在网络中的地址,是唯一的标识。

  • 端口:应用程序在设备中唯一的标识。

  • 协议: 数据在网络中传输的规则,常见的协议有UDP协议和TCP协议。

1.2、要素一:IP地址

  • IP(Internet Protocol):全称”互联网协议地址”,是分配给上网设备的唯一标志。
  • 常见的IP分类为:IPv4和IPv6

IPv4

Ipv6

  • IPv6:128位(16个字节),号称可以为地球每一粒沙子编号。
  • IPv6分成8个整数,每个整数用四个十六进制位表示, 数之间用冒号(:)分开。

1.2.1、IP地址形式

  • 公网地址、和私有地址(局域网使用)。
  • 192.168. 开头的就是常见的局域网地址,范围即为192.168.0.0--192.168.255.255,专门为组织机构内部使用。

1.2.3、IP常用命令

  • ipconfig:查看本机IP地址
  • ping IP地址:检查网络是否连通

1.2.4、特殊IP地址

本机IP: 127.0.0.1或者localhost:称为回送地址也可称本地回环地址,只会寻找当前所在本机。

1.2.5、小节

说说网络通信至少需要几个要素
  • IP、端口、协议。
IP地址是做什么的,具体有几种
  • 定位网络上的设备的,有IPv4 , IPv6.
如何查看本机IP地址,如何看是否与对方互通
  • ipcofig
  • ping 对方IP地址
本机IP是谁?
  • 127.0.0.1或者是localhost

1.3、IP地址操作类-InetAddress

此类表示Internet协议(IP)地址。

1.3.1、InetAddress的API

名称 说明
public static InetAddress getLocalHost() 返回本主机的地址对象
public static InetAddress getByName(String host) 得到指定主机的IP地址对象,参数是域名或者IP地址
public String getHostName() 获取此IP地址的主机名
public String getHostAddress() 返回IP地址字符串
public boolean isReachable(int timeout) 在指定毫秒内连通该IP地址对应的主机,连通返回true
// 1.获取本机地址对象。
InetAddress ip1 = InetAddress.getLocalHost();
System.out.println(ip1.getHostName());
System.out.println(ip1.getHostAddress());

// 2.获取域名ip对象
InetAddress ip2 = InetAddress.getByName("www.baidu.com");
System.out.println(ip2.getHostName());
System.out.println(ip2.getHostAddress());

// 3.获取公网IP对象。
InetAddress ip3 = InetAddress.getByName("112.80.248.76");
System.out.println(ip3.getHostName());
System.out.println(ip3.getHostAddress());

// 4.判断是否能通: ping  5s之前测试是否可通
System.out.println(ip3.isReachable(5000));

1.3.2、小节

IP地址的代表类是谁?
  • InetAddress类
如何获取本机IP对象
  • public static InetAddress getLocalHost()
如何判断与该IP地址对象是否互通?
  • public boolean isReachable(int timeout)

1.4、要素二:端口号

端口号

  • 端口号:标识正在计算机设备上运行的进程(程序),被规定为一个 16 位的二进制,范围是 0~65535。

端口类型

  • 周知端口:0~1023,被预先定义的知名应用占用(如:HTTP占用 80,FTP占用21)
  • 注册端口:1024~49151,分配给用户进程或某些应用程序。(如:Tomcat占 用8080,MySQL占用3306)
  • 动态端口:49152到65535,之所以称为动态端口,是因为它 一般不固定分配某种进程,而是动态分配。

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

小节

端口号的作用是什么?
  • 唯一标识正在计算机设备上运行的进程(程序)
一个设备中,能否出现2个应用程序的端口号一样,为什么?
  • 不可以,如果一样会出现端口冲突错误。

1.5、要素三:协议

通信协议

  • 连接和通信数据的规则被称为网络通信协议

网络通信协议国际上有两套参考标准

  • OSI参考模型:世界互联协议标准,全球通信规范,由于此模型过于理想化,未能在因特网上进行广泛推广。

  • TCP/IP参考模型(或TCP/IP协议):事实上的国际标准。

  • OSI参考模型 TCP/IP参考模型 各层对应 面向操作
    应用层
    表示层 应用层 HTTP、FTP、DNS、SMTP… 应用程序需要关注的:浏览器,邮箱。程序员一般在这一层开发
    会话层
    传输层 传输层 TCP、UDP… 选择使用的TCP , UDP协议
    网络层 网络层 IP、ICMP… 封装源和目标IP,进行路径选择
    数据链路层 数据链路层+物理 物理寻址、比特流… 物理设备中传输
    物理层

传输层的2个常见协议

  • TCP(Transmission Control Protocol) :传输控制协议
  • UDP(User Datagram Protocol):用户数据报协议
TCP协议特点
  • 使用TCP协议,必须双方先建立连接,它是一种面向连接的可靠通信协议。
  • 传输前,采用“三次握手”方式建立连接,所以是可靠的 。
  • 在连接中可进行大数据量的传输 。
  • 连接、发送数据都需要确认,且传输完毕后,还需释放已建立的连接,通信效率较低。
TCP协议通信场景
  • 对信息安全要求较高的场景,例如:文件下载、金融等数据通信。
UDP协议
  • UDP是一种无连接、不可靠传输的协议。
  • 将数据源IP、目的地IP和端口封装成数据包,不需要建立连接
  • 每个数据包的大小限制在64KB内
  • 发送不管对方是否准备好,接收方收到也不确认,故是不可靠的
  • 可以广播发送 ,发送数据结束时无需释放资源,开销小,速度快。
UDP协议通信场景
  • 语音通话,视频会话等。
小节
通信协议是什么?
  • 计算机网络中,连接和通信数据的规则被称为网络通信协议。
TCP通信协议的特点是什么样的?
  • 它是一种面向连接的可靠通信协议。
  • 传输前,采用“三次握手”方式建立连接,点对点的通信,所以可靠。
  • 在连接中可进行大数据量的传输。
  • 通信效率较低。
UDP协议的特点是什么
  • 用户数据报协议(User Datagram Protocol)
  • UDP是面向无连接,不可靠传输的通信协议。
  • 速度快,有大小限制一次最多发送64K,数据不安全,易丢失数据。

2、UDP通信

2.1、快速入门

UDP协议的特点

  • UDP是一种无连接、不可靠传输的协议。
  • 将数据源IP、目的地IP和端口以及数据封装成数据包,大小限制在64KB内,直接发送出去即可。

DatagramPacket:数据包对象(韭菜盘子)

构造器 说明
public DatagramPacket(byte[] buf, int length, InetAddress address, int port) 创建发送端数据包对象 buf:要发送的内容,字节数组 length:要发送内容的字节长度 address:接收端的IP地址对象 port:接收端的端口号
public DatagramPacket(byte[] buf, int length) 创建接收端的数据包对象 buf:用来存储接收的内容 length:能够接收内容的长度

DatagramPacket常用方法

方法 说明
public int getLength() 获得实际接收到的字节个数

DatagramSocket:发送端和接收端对象(人)

构造器 说明
public DatagramSocket() 创建发送端的Socket对象,系统会随机分配一个端口号。
public DatagramSocket(int port) 创建接收端的Socket对象并指定端口号

DatagramSocket类成员方法

方法 说明
public void send(DatagramPacket dp) 发送数据包
public void receive(DatagramPacket p) 接收数据包

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

需求:客户端实现步骤

①创建DatagramSocket对象(发送端对象) 扔韭菜的人

②创建DatagramPacket对象封装需要发送的数据(数据包对象) 韭菜盘子

③使用DatagramSocket对象的send方法传入DatagramPacket对象 开始抛出韭菜

④释放资源

/**
  发送端  一发 一收
 */
public class Client {
    public static void main(String[] args) throws Exception {
        System.out.println("=====客户端启动======");

        // 1、创建发送端对象:发送端自带默认的端口号(人)
        DatagramSocket socket = new DatagramSocket();

        // 2、创建一个数据包对象封装数据(韭菜盘子)
        /**
         public DatagramPacket(byte buf[], int length,
         InetAddress address, int port)
         参数一:封装要发送的数据(韭菜)
         参数二:发送数据的大小
         参数三:服务端的主机IP地址
         参数四:服务端的端口
         */
        byte[] buffer = "我是一颗快乐的韭菜,你愿意吃吗?".getBytes();
        DatagramPacket packet = new DatagramPacket( buffer, buffer.length,
                InetAddress.getLocalHost() , 8888);

        // 3、发送数据出去
        socket.send(packet);

        socket.close();
    }
}

需求:接收端实现步骤

①创建DatagramSocket对象并指定端口(接收端对象) 接韭菜的人

②创建DatagramPacket对象接收数据(数据包对象) 韭菜盘子

③使用DatagramSocket对象的receive方法传入DatagramPacket对象 开始接收韭菜

④释放资源

/**
  接收端
 */
public class Server {
    public static void main(String[] args) throws Exception {
        System.out.println("=====服务端启动======");
        // 1、创建接收端对象:注册端口(人)
        DatagramSocket socket = new DatagramSocket(8888);

        // 2、创建一个数据包对象接收数据(韭菜盘子)
        byte[] buffer = new byte[1024 * 64];
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

        // 3、等待接收数据。
        socket.receive(packet);

        // 4、取出数据即可
        // 读取多少倒出多少
        int len = packet.getLength();
        String rs = new String(buffer,0, len);
        System.out.println("收到了:" + rs);

        // 获取发送端的ip和端口
        String ip  =packet.getSocketAddress().toString();
        System.out.println("对方地址:" + ip);

        int port  = packet.getPort();
        System.out.println("对方端口:" + port);

        socket.close();
    }
}

小节

UDP发送端和接收端的对象是哪个?
  • public DatagramSocket():创建发送端的Socket对象
  • public DatagramSocket(int port):创建接收端的Socket对象
数据包对象是哪个?
  • DatagramPacket
  • 如何发送、接收数据包
使用DatagramSocket的如下方法
  • public void send(DatagramPacket dp):发送数据包
  • public void receive(DatagramPacket dp) :接收数据包

2.2、多发多收

案例

需求

使用UDP通信方式开发接收端和发送端。

分析

①发送端可以一直发送消息。

②接收端可以不断的接收多个发送端的消息展示。

③发送端输入了exit则结束发送端程序。

需求:客户端实现步骤

①创建DatagramSocket对象(发送端对象) 扔韭菜的人

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

③如果用户输入的不是exit, 把数据封装成DatagramPacket 韭菜盘子

④使用DatagramSocket对象的send方法将数据包对象进行发送 开始抛出韭菜

⑤释放资源

需求:接收端实现步骤

①创建DatagramSocket对象并指定端口(接收端对象) 接韭菜的人

②创建DatagramPacket对象接收数据(数据包对象) 韭菜盘子

③使用while死循环不断的进行第4步

④使用DatagramSocket对象的receive方法传入DatagramPacket对象 开始接收韭菜

小节

UDP的接收端为什么可以接收很多发送端的消息?
  • 接收端只负责接收数据包,无所谓是哪个发送端的数据包.

3、UDP通信-广播、组播

3.1、UDP的三种通信方式

  • 单播:单台主机与单台主机之间的通信。
  • 广播:当前主机与所在网络中的所有主机通信。
  • 组播:当前主机与选定的一组主机的通信。

3.2、UDP如何实现广播

  • 使用广播地址:255.255.255.255
  • 具体操作:
    • 发送端发送的数据包的目的地写的是广播地址、且指定端口。 (255.255.255.255 , 9999)
    • 本机所在网段的其他主机的程序只要注册对应端口就可以收到消息了。(9999)

3.3、UDP如何实现组播

  • 使用组播地址:224.0.0.0 ~ 239.255.255.255
  • 具体操作:
    • 发送端的数据包的目的地是组播IP (例如:224.0.1.1, 端口:9999)
    • 接收端必须绑定该组播IP(224.0.1.1),端口还要注册发送端的目的端口9999 ,这样即可接收该组播消息。
    • DatagramSocket的子类MulticastSocket可以在接收端绑定组播IP。

3.4、小节

如何实现广播,具体怎么操作?

  • 发送端目的IP使用广播IP: 255.255.255.255 9999。
  • 所在网段的其他主机对应了端口(9999)即可接收消息。

如何实现组播,具体怎么操作?

  • 发送端目的IP使用组播IP,且指定端口。
  • 所在网段的其他主机注册了该组播IP和对应端口即可接收消息。

4、TCP通信

4.1、快速入门

编写客户端代码

TCP协议回顾
  • TCP是一种面向连接,安全、可靠的传输数据的协议
  • 传输前,采用“三次握手”方式,点对点通信,是可靠的
  • 在连接中可进行大数据量的传输
TCP通信模式演示

Socket
构造器 说明
public Socket(String host , int port) 创建发送端的Socket对象与服务端连接,参数为服务端程序的ip和端口。
Socket类成员方法
方法 说明
OutputStream getOutputStream() 获得字节输出流对象
InputStream getInputStream() 获得字节输入流对象
案例:编写客户端发送消息
  • 需求:客户端实现步骤
    创建客户端的Socket对象,请求与服务端的连接。

  • 使用socket对象调用getOutputStream()方法得到字节输出流。

  • 使用字节输出流完成数据的发送。

  • 释放资源:关闭socket管道。

  • public static void main(String[] args) {
        try {
            System.out.println("====客户端启动===");
            // 1、创建Socket通信管道请求有服务端的连接
            // public Socket(String host, int port)
            // 参数一:服务端的IP地址
            // 参数二:服务端的端口
            Socket socket=new Socket("127.0.0.1",8888);
            // 2、从socket通信管道中得到一个字节输出流 负责发送数据
            OutputStream os=socket.getOutputStream();
            // 3、把低级的字节流包装成打印流
            PrintStream ps=new PrintStream(os);
            // 4、发送消息
            ps.println("我是TCP的客户端,我已经与你建立连接!!!");
            ps.flush();
            // 不建议直接关闭
            // socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
小节
TCP通信的客户端的代表类是谁?
  • Socket类
  • public Socket(String host , int port)
TCP通信如何使用Socket管道发送、接收数据?
  • OutputStream getOutputStream():获得字节输出流对象(发)
  • InputStream getInputStream():获得字节输入流对象(收)

编写服务端代码、原理分析

ServerSocket(服务端)
构造器 说明
public ServerSocket(int port) 注册服务端端口
ServerSocket类成员方法
方法 说明
public Socket accept() 等待接收客户端的Socket通信连接 连接成功返回Socket对象与客户端建立端到端通信
public static void main(String[] args) {
    try {
        System.out.println("===服务端启动成功===");
        // 1、注册端口
        ServerSocket serverSocket=new ServerSocket(8888);
        // 2、必须调用accept方法:等待接收客户端的Socket连接请求,建立Socket通信管道
        Socket socket=serverSocket.accept();
        // 3、从socket通信管道中得到一个字节输入流
        InputStream is=socket.getInputStream();
        // 4、把字节输入流包装成缓冲字符输入流进行消息的接收  转换流
        BufferedReader br=new BufferedReader(new InputStreamReader(is));
        // 5、按照行读取消息
        String content;
        if((content=br.readLine())!=null)
        {
            System.out.println("收到了来自"+socket.getRemoteSocketAddress()+"的消息;"+content);
        }
        //不建议直接关闭
    } catch (Exception e) {
        e.printStackTrace();
    }
}
案例:编写服务端接收信息

需求:服务端实现步骤

①创建ServerSocket对象,注册服务端端口。

②调用ServerSocket对象的accept()方法,等待客户端的连接,并得到Socket管道对象。

③通过Socket对象调用getInputStream()方法得到字节输入流、完成数据的接收。

④释放资源:关闭socket管道

小节
TCP通信服务端用的代表类?
  • ServerSocket类,注册端口。
  • 调用accept()方法阻塞等待接收客户端连接。得到Socket对象。
TCP通信的基本原理?
  • 客户端怎么发,服务端就应该怎么收。
  • 客户端如果没有消息,服务端会进入阻塞等待。
  • Socket一方关闭或者出现异常、对方Socket也会失效或者出错。

4.2、多发多收消息

需求:使用TCP通信方式实现:多发多收消息。

具体要求:

①可以使用死循环控制服务端收完消息继续等待接收下一个消息。

②客户端也可以使用死循环等待用户不断输入消息。

③客户端一旦输入了exit,则关闭客户端程序,并释放资源。

public static void main(String[] args) {
    try {
        System.out.println("===服务端启动成功===");
        // 1、注册端口
        ServerSocket serverSocket = new ServerSocket(7777);
        while (true) {
            // 2、必须调用accept方法:等待接收客户端的Socket连接请求,建立Socket通信管道
            Socket socket = serverSocket.accept();
            // 3、从socket通信管道中得到一个字节输入流
            InputStream is = socket.getInputStream();
            // 4、把字节输入流包装成缓冲字符输入流进行消息的接收
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            // 5、按照行读取消息
            String msg;
            while ((msg = br.readLine()) != null){
                System.out.println(socket.getRemoteSocketAddress() + "说了:: " + msg);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

小节

本案例实现了多发多收,那么是否可以同时接收多个客户端的消息?
  • 不可以的。
  • 因为服务端现在只有一个线程,只能与一个客户端进行通信。
本次多发多收是如何实现的
  • 客户端使用循环反复地发送消息。
  • 服务端使用循环反复地接收消息。
现在服务端为什么不可以同时接收多个客户端的消息。
  • 目前服务端是单线程的,每次只能处理一个客户端的消息。

4,3、同时接受多个客户端消息

思考

之前我们的通信是否可以同时与多个客户端通信,为什么?
  • 不可以的
  • 单线程每次只能处理一个客户端的Socket通信
如何才可以让服务端可以处理多个客户端的通信需求?
  • 引入多线程。

public static void main(String[] args) {
    try {
        System.out.println("===服务端启动成功===");
        // 1、注册端口
        ServerSocket serverSocket = new ServerSocket(7777);
        // a.定义一个死循环由主线程负责不断的接收客户端的Socket管道连接。
        while (true) {
            // 2、每接收到一个客户端的Socket管道,交给一个独立的子线程负责读取消息
            Socket socket = serverSocket.accept();
            System.out.println(socket.getRemoteSocketAddress()+ "它来了,上线了!");
            // 3、开始创建独立线程处理socket
            new ServerReaderThread(socket).start();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
-----------------------------------------------------
public class ServerReaderThread extends Thread{
    private Socket socket;
    public ServerReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            // 3、从socket通信管道中得到一个字节输入流
            InputStream is = socket.getInputStream();
            // 4、把字节输入流包装成缓冲字符输入流进行消息的接收
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            // 5、按照行读取消息
            String msg;
            while ((msg = br.readLine()) != null){
                System.out.println(socket.getRemoteSocketAddress() + "说了:: " + msg);
            }
        } catch (Exception e) {
            System.out.println(socket.getRemoteSocketAddress() + "下线了!!!");
        }
    }
}    

小节

本次是如何实现服务端接收多个客户端的消息的。
  • 主线程定义了循环负责接收客户端Socket管道连接
  • 每接收到一个Socket通信管道后分配一个独立的线程负责处理它。

4.4、线程池优化

目前的通信架构模型

思考

目前的通信架构存在什么问题?
  • 客户端与服务端的线程模型是: N-N的关系。
  • 客户端并发越多,系统瘫痪的越快。

引入线程池处理多个客户端消息

小节

本次使用线程池的优势在哪里?
  • 服务端可以复用线程处理多个客户端,可以避免系统瘫痪。
  • 适合客户端通信时长较短的场景。

5、实战

即时通信

思考

即时通信是什么含义,要实现怎么样的设计?
  • 即时通信,是指一个客户端的消息发出去,其他客户端可以接收到。
  • 之前我们的消息都是发给服务端的。
  • 即时通信需要进行端口转发的设计思想。

客户端
public static void main(String[] args) {
try {
    System.out.println("====客户端启动===");
    // 1、创建Socket通信管道请求有服务端的连接
    // public Socket(String host, int port)
    // 参数一:服务端的IP地址
    // 参数二:服务端的端口
    Socket socket = new Socket("127.0.0.1", 6868);

    // 马上为客户端分配一个独立的线程负责读取它收到的消息
    new ClientReaderThread(socket).start();

    // 2、从socket通信管道中得到一个字节输出流 负责发送数据
    OutputStream os = socket.getOutputStream();

    // 3、把低级的字节流包装成打印流
    PrintStream ps = new PrintStream(os);

    Scanner sc =  new Scanner(System.in);
    while (true) {
        System.out.println("请说:");
        String msg = sc.nextLine();
        // 4、发送消息
        ps.println(msg);
        ps.flush();
    }
    // 关闭资源。
    // socket.close();

} catch (Exception e) {
    e.printStackTrace();
}
}
-----------------------------------------------------
public class ClientReaderThread extends Thread{
    private Socket socket;
    public ClientReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            // 3、从socket通信管道中得到一个字节输入流
            InputStream is = socket.getInputStream();
            // 4、把字节输入流包装成缓冲字符输入流进行消息的接收
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            // 5、按照行读取消息
            String msg;
            while ((msg = br.readLine()) != null){
                System.out.println(socket.getRemoteSocketAddress() + "收到了: " + msg);
            }
        } catch (Exception e) {
            System.out.println("服务端把你踢出去了~~");
        }
    }

}
服务端
public static List<Socket> onLineSockets = new ArrayList<>();

public static void main(String[] args) {
    try {
        System.out.println("===服务端启动成功===");
        // 1、注册端口
        ServerSocket serverSocket = new ServerSocket(6868);
        // a.定义一个死循环由主线程负责不断的接收客户端的Socket管道连接。
        while (true) {
            // 2、每接收到一个客户端的Socket管道,交给一个独立的子线程负责读取消息
            Socket socket = serverSocket.accept();
            System.out.println(socket.getRemoteSocketAddress()+ "它来了,上线了!");
            // 把当前客户端管道Socket加入到在线集合中去
            onLineSockets.add(socket);

            // 3、开始创建独立线程处理socket
            new ServerReaderThread(socket).start();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
-----------------------------------------------------
public class ServerReaderThread extends Thread{
    private Socket socket;
    public ServerReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            // 3、从socket通信管道中得到一个字节输入流
            InputStream is = socket.getInputStream();
            // 4、把字节输入流包装成缓冲字符输入流进行消息的接收
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            // 5、按照行读取消息
            String msg;
            while ((msg = br.readLine()) != null){
                System.out.println(socket.getRemoteSocketAddress() + "说了:: " + msg);
                // 把这个消息发给当前所有在线socket
                sendMsgToAll(msg);
            }
        } catch (Exception e) {
            System.out.println(socket.getRemoteSocketAddress() + "下线了!!!");
            // 从在线集合中抹掉本客户端socket
            ServerDemo2.onLineSockets.remove(socket);
        }
    }

    private void sendMsgToAll(String msg) {
        try {
            // 遍历全部的在线 socket给他们发消息
            for (Socket onLineSocket : ServerDemo2.onLineSockets) {
                // 除了自己的socket,其他socket我都发!!
                if(onLineSocket != socket){
                    PrintStream ps = new PrintStream(socket.getOutputStream());
                    ps.println(msg);
                    ps.flush();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}    

小节

即时通信是什么含义,要实现怎么样的设计?
  • 即时通信,是指一个客户端的消息发出去,其他客户端可以接收到
  • 即时通信需要进行端口转发的设计思想。
  • 服务端需要把在线的Socket管道存储起来
  • 一旦收到一个消息要推送给其他管道

模拟BS系统

思考

之前的客户端都是什么样的?
  • 其实就是CS架构,客户端实需要我们自己开发实现的。
BS结构是什么样的,需要开发客户端吗?
  • 浏览器访问服务端,不需要开发客户端。

  • // 使用静态变量记住一个线程池对象
    private static ExecutorService pool = new ThreadPoolExecutor(3,
            5, 6, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(2)
            , Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
    
    public static void main(String[] args) {
        try {
            // 1.注册端口
            ServerSocket ss = new ServerSocket(8080);
            // 2.创建一个循环接收多个客户端的请求。
            while(true){
                Socket socket = ss.accept();
                // 3.交给一个独立的线程来处理!
                pool.execute(new ServerReaderRunnable(socket));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    --------------------------------------------------
    public class ServerReaderRunnable implements Runnable{
        private Socket socket;
        public ServerReaderRunnable(Socket socket){
            this.socket = socket;
        }
        @Override
        public void run() {
            try {
                // 浏览器 已经与本线程建立了Socket管道
                // 响应消息给浏览器显示
                PrintStream ps = new PrintStream(socket.getOutputStream());
                // 必须响应HTTP协议格式数据,否则浏览器不认识消息
                ps.println("HTTP/1.1 200 OK"); // 协议类型和版本 响应成功的消息!
                ps.println("Content-Type:text/html;charset=UTF-8"); // 响应的数据类型:文本/网页
    
                ps.println(); // 必须发送一个空行
    
                // 才可以响应数据回去给浏览器
                ps.println("<span style='color:red;font-size:90px'>你好!世界!!! </span>");
                ps.close();
            } catch (Exception e) {
                System.out.println(socket.getRemoteSocketAddress() + "下线了!!!");
            }
        }
    }