Java基础-网络通信

Java基础-网络通信
1.1 基本认识

实现网络编程的三要素

  • IP地址:设备在网络中的地址 唯一的标识
  • 端口: 应用程序在设备中的唯一标识
  • 协议: 数据在网络中传输的规则,常见的协议有 UDP协议和TCP协议

1.IP地址

  • IPv6:128位(16字节),分为8个整数,每个整数用四个十六进制位表示,数之间用冒号(:)分开

    • image-20240207134435457
  • IP地址形式:

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

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

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

    • 计算机 依据域名(https://baidu.com)通过 DNS域名服务器,查询IP地址;然后访问对应的服务器IP地址,服务器返回数据展示在浏览器上。

InetAddress 的使用

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

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
public class InetAddressDemo1 {
    public static void main(String[] args) throws Exception{
        //1、获取本主机IP对象
        InetAddress ip = InetAddress.getLocalHost();
        //获取IP地址的主机名
        System.out.println(ip.getHostName());
        //获取Ip地址
        System.out.println(ip.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.74");
        System.out.println(ip3.getHostName());
        System.out.println(ip3.getHostAddress());
        //指定毫秒内连通IP对应主机,连通返回true
        System.out.println(ip3.isReachable(3000));
    }
}

运行结果:

DESKTOP-6O6MLG4
192.168.1.7
www.baidu.com
39.156.66.14
112.80.248.74
112.80.248.74
false

2.端口号

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

端口类型

  • 周知端口:0~1023 ,被预先定义的知名应用占用(如 HTTP 占用80 , FTP 21)

  • 注册端口:1024~49151,分配给用户进程或某些应用程序(如 Tomcat 8080, MySQL 3306),我们自己开发的程序选择注册端口,且一个设备中不能出现两个一样端口号的程序,否则出错

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

3.通信协议

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

网络通信协议有两套参考模型

  • OSI参考模型:世界互联协议标准,全球通信规范,由于此模型过于理想化,未能在因特网上进行广泛推广
  • TCP/IP参考模型(TCP/IP协议):事实上的国际标准
image-20240210132812964

传输层的2个常见协议

  • TCP (Transmission Control Protocol):传输控制协议
  • UDP(User Datagram Protocol):用户数据报协议

TCP协议特点

  • 使用TCP协议,必须双方先建立连接,它是一种面向连接的可靠通信协议
  • 传输前,采用”三次握手“方法建立连接,所以是可靠的
  • 在连接中可进行大数据量的传输
  • 连接、发送数据都需要确认,且传输完毕后,还需释放已建立的连接,通信效率较低

TCP协议通信场景

  • 对信息安全要求较高,例如:文件下载、金融等数据通信

UDP协议特点

  • UDP是一种无连接、不可靠连接的协议
  • 将数据源IP、目的地IP和端口封装成数据包,不需要建立连接
  • 每个数据包的大小限制在64KB内
  • 发送不管对方是否准备好,接收方收到也不确认,故是不可靠的
  • 可以广播放送,发送数据结束时无需释放资源,开销小,速度快

UDP协议通信场景

  • 语音通信,视频会话等

网络通信基本模式

  • 常见的通信模式有两种形式:Client-Server(CS),Browser-Server(BS)
  • CS 客户端-服务器
    • 客户端(需要程序员开发实现,用户需要安装客户端),服务器(需要程序员开发实现)
  • BS 浏览器-服务器
    • 浏览器(不需要程序员开发实现,用户需要安装浏览器),服务器(需要程序员开发实现)
1.2 UDP通信

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) 创建发送端 数据包对象
      • buf :要发送的内容,字节数组
      • length :要发送内容的字节长度
      • address:接收端的IP地址长度
      • port:接收端的端口号
    • public DatagramPacket(byte[] buf ,int length) 创建接收端 数据包对象
      • buf :用来存储接收的内容
      • length:能够接收内容的长度
  • 常用方法
    • public int getLength() 获得实际接收到的字节个数

UDP实例 单开单收

/**
 *  实现发送端,发送数据  
 *  客户端对象随机端口号,发送数据包四种参数
 */
public class ClientDataGramSocketDemo {
    public static void main(String[] args) throws Exception{
        //1.创建发送方对象:随机分配一个端口号,空
        DatagramSocket client = new DatagramSocket();   //可以自定义端口888888
        //2.创建发送的数据包对象:(韭菜盘子)
        /**
         * public DatagramPacket(byte buf[], int offset, int length,
         *                           InetAddress address, int port)
         */
        byte[] buffer = "我喜欢你~~~~".getBytes();
        //本主机
        DatagramPacket packet = new DatagramPacket(buffer,buffer.length, InetAddress.getLocalHost(),8888);
        //3.发送数据包
        client.send(packet);
        //4.关闭
        client.close();
    }
}
/**
 *  实现服务端对象 接收数据
 *  服务端对象:指定一个端口号
 *  服务端接收数据包对象:指定接收数据包与大小
 */
public class ServerDatagramSocketDemo {
    public static void main(String[] args) throws Exception{
        //1.创建服务端对象:服务端一定要 指定端口号 !!!
        DatagramSocket server = new DatagramSocket(8888);
        //2.创建服务端的数据包对象:(韭菜盘子)
        /**
         public DatagramPacket(byte buf[], int length)
         */
        byte[] buffer = new byte[1024*64]; // udp最大64B
        DatagramPacket packet = new DatagramPacket(buffer,buffer.length);
        //3.等待接收数据包
        server.receive(packet);
        //4.输出
        int len = packet.getLength();   //获取接收的数据包大小
        System.out.println(new String(buffer,0,len));
        //5.获取发送方的ip和端口
        SocketAddress ClientIp = packet.getSocketAddress();
        System.out.println("发送端ip:"+ClientIp);
        int port = packet.getPort();
        System.out.println("发送端口:"+port);
        server.close();
    }
}

运行结果:

我喜欢你~~~~
发送端ip:/192.168.1.7:63536
发送端口:63536

UDP通信实例

  • 需要服务器先运营,再客户端
  • 允许多开,多收

UDP的接收端为什么可以接收很多发送端的消息

  • 接收端只负责接收数据包,无所谓是哪个发送端的数据包

UDP案例 持续发送与接收消息 多开(一个电脑 只能间断性的运行多次)多发

//发送端
public class ClientDataGramSocketDemo {
    public static void main(String[] args) throws Exception{
        //1.创建发送方对象:随机分配一个端口号,空
        DatagramSocket client = new DatagramSocket();   //可以自定义端口888888
        Scanner sc = new Scanner(System.in);
        //2.创建发送的数据包对象:(韭菜盘子)
        /**
         * public DatagramPacket(byte buf[], int offset, int length,
         *                           InetAddress address, int port)
         */
        while (true) {
            System.out.println("请说:");
            String msg = sc.nextLine();
            if (Objects.equals("exit",msg)){
                //4.关闭
                System.out.println("离线成功~~~");
                client.close();
                break;
            }
            byte[] buffer = msg.getBytes();
            //本主机
            DatagramPacket packet = new DatagramPacket(buffer,buffer.length, InetAddress.getLocalHost(),8888);
            //3.发送数据包
            client.send(packet);
        }
    }
}
//接收端
public class ServerDatagramSocketDemo {
    public static void main(String[] args) throws Exception{
        //1.创建服务端对象:服务端一定要 指定端口号 !!!
        DatagramSocket server = new DatagramSocket(8888);
        //2.创建服务端的数据包对象:(韭菜盘子)
        /**
         public DatagramPacket(byte buf[], int length)
         */
        byte[] buffer = new byte[1024*64]; // udp最大64B
        DatagramPacket packet = new DatagramPacket(buffer,buffer.length);
        while (true) {
            //3.等待接收数据包
            server.receive(packet);
            //4.输出
            int len = packet.getLength();   //获取接收的数据包大小
            System.out.println("收到发送方ip:"+packet.getSocketAddress()+",端口:"+packet.getPort()+
                    ",发来消息:"+new String(buffer,0,len));
        }
        //server.close();
    }
}

发送端

请说:
hello
请说:
exit1
请说:
exit
离线成功~~~

接收端

收到发送方ip:/192.168.1.7:63581,端口:63581,发来消息:hello
收到发送方ip:/192.168.1.7:63581,端口:63581,发来消息:exit1

UDP的三种通信方式

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

UDP如何实现广播?

  • 使用广播地址:255.255.255.255
  • 具体操作:
    • 发送端 发送的数据包的目的地,写的是广播地址 并且 指定端口 (255.255.255.255 ,9999)
    • 本机所在网段的 其他主机 的程序只要匹配端口成功 , 即可收到消息 (9999)

实现 (上一个实例,仅修改数据包对象

    //本主机
    //广播
    //255.255.255.255,发送同一个局域网内的其他主机
    DatagramPacket packet = new DatagramPacket(buffer,buffer.length,
                                               InetAddress.getByName("255.255.255.255"),9999);

UDP如何实现组播?

  • 使用组播地址:224.0.0.0~239.255.255.255
  • 具体操作:
    • 发送端的数据包 的 目的地址 是组播IP(例如:224.0.1.1 , 端口号 :9999)
    • 接收端 必须 绑定该组播IP (224.0.1.1),端口还要对应发送端的目的端口 9999,这样即可接收该组播消息
    • DatagramSocket的子类 MulticastSocket可以在接收端绑定组播 IP
//224.0.0.0~239.255.255.255 之间
DatagramPacket packet = new DatagramPacket(buffer,buffer.length,
        InetAddress.getByName("224.0.1.1"),9999);
public class ServerDatagramSocketDemo {
    public static void main(String[] args) throws Exception{
        //1.创建服务端对象:服务端一定要 指定端口号 !!!
        //DatagramSocket 子类 Multicast
        MulticastSocket server = new MulticastSocket(9999);
        //将当前接收端加入到一个组播组中,绑定对应的组播消息的组播ip !!!!!!
        server.joinGroup(InetAddress.getByName("224.0.1.1"));
//        public void joinGroup(SocketAddress mcastaddr, NetworkInterface netIf
//        server.joinGroup(new InetSocketAddress(InetAddress.getByName("224.0.1.1"),9999),
//                NetworkInterface.getByInetAddress(InetAddress.getLocalHost()));
        //2.创建服务端的数据包对象:(韭菜盘子)
        /**
         public DatagramPacket(byte buf[], int length)
         */
        byte[] buffer = new byte[1024*64]; // udp最大64B
        DatagramPacket packet = new DatagramPacket(buffer,buffer.length);
        while (true) {
            //3.等待接收数据包
            server.receive(packet);
            //4.输出
            int len = packet.getLength();   //获取接收的数据包大小
            System.out.println("收到发送方ip:"+packet.getSocketAddress()+",端口:"+packet.getPort()+
                    ",发来消息:"+new String(buffer,0,len));
        }
        //server.close();
    }
}
1.3 TCP通信

客户端 - 服务端

image-20240210161615541

注意:

  • Java中 只要使用java.net.Socket类实现通信,底层即是使用了TCP协议
  • 服务端 通过 SocketServer注册服务端(端口)server,server 通过 accept() 获取 Socket对象
1.3.1 单发 单收

发送端(客户端)

客户端发送消息

  • 创建客户端的Socket对象,请求与服务器的连接
  • 使用 socket 对象调用 getOutputStream() 方法得到字节输出流
  • 使用字节输出流完成数据的发送 (使用打印流 printStream)
  • 释放资源:关闭 socket 管道(输出流)

Socket 类

  • 构造器
    • public Socket(String host ,int port) 创建发送端的 Socket 对象与服务端连接,参数为服务端程序的 ip 和端口
  • 成员方法
    • OutputStream getOutputStream() 获得字节输出流对象
    • InputStream getInputStream() 获得字节输入流对象

服务器实现接收消息

  • 创建 ServerSocket 对象,注册服务器端口
  • 调用 ServerSocket 对象的 accept()方法,等待客户端的连接,并得到Socket管道对象
  • 通过 获取的 Socket 对象调用 getInputStream() 方法得到字节流输入、完成数据的接收 (使用 转换流 —> 字符流 —> 缓冲流 )
  • 释放资源:关闭 socket 管道(输入流)

ServerSocket 类

  • 构造器

    • public ServerSocket (int port) 注册服务端端口
  • 成员方法

    • public Socket accept() 等待接收客户端的Socket通信连接,连接成功返回Socket对象与客户端建立端到端通信
//客户端
public class ClientSocketTcpDemo {
    public static void main(String[] args) {
        try {
            //1.创建 socket 管道!!!!
            /**    public Socket(String host, int port)
             */
            Socket socket = new Socket(InetAddress.getLocalHost().getHostAddress(),9999);
            //2.socket管道中,创建字节输出流
            OutputStream os = socket.getOutputStream();
            //3.低级字节输出流 转为 打印流
            //public PrintStream(OutputStream out)
            PrintStream  ps = new PrintStream(os);
            //4.发送消息
//            ps.print("Tcp连接成功,你好,今晚约吗?");
//            Connection reset  , 这里没有换行符而服务端是按行接收
//            Tcp 当一方挂了,另一方也会挂
            ps.println("Tcp连接成功,你好,今晚约吗?");
            ps.flush();
            //关闭资源
            //socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
//服务端
public class ServerSocketDemo {
    public static void main(String[] args) {
        try {
            //1.注册端口!!! 使用ServerSocket !!!!
            //指定一致的服务端端口号
            ServerSocket serverSocket = new ServerSocket(9999);
            //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;
            if ((msg = br.readLine()) != null){
                //获取远端ip地址
                System.out.println(socket.getRemoteSocketAddress()+"发送:"+msg);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

服务端

/192.168.1.7:55243发送:Tcp连接成功,你好,今晚约吗?

小结

  • TCP通信 服务端 用的代表类

    • ServerSocket类,注册端口
    • 调用 accept() 方法 阻塞等待接收客户端连接,得到Socket对象
  • TCP通信的基本原理?

    • 客户端怎么发,服务端就应该怎么收
    • 客户端如果没有消息,服务端会进入阻塞等待
    • Socket一方关闭或者出现异常、对方Socket也会失效或者出错 java.net.SocketException: Connection reset
1.3.2 多发 多收

与之前UDP 持续发送与接收消息 类似

  • 多发多收
    • 客户端使用循环反复地发送消息
    • 服务端使用循环反复地接收消息
//客户端
public class ClientSocketTcpDemo {
    public static void main(String[] args) {
        try {
            //1.创建 socket 管道!!!!
            /**    public Socket(String host, int port)
             */
            Socket socket = new Socket(InetAddress.getLocalHost().getHostAddress(),9999);
            //2.socket管道中,创建字节输出流
            OutputStream os = socket.getOutputStream();
            //3.低级字节输出流 转为 打印流
            //public PrintStream(OutputStream out)
            PrintStream  ps = new PrintStream(os);
            Scanner sc = new Scanner(System.in);
            while (true) {
                //4.发送消息
//            ps.print("Tcp连接成功,你好,今晚约吗?");
//            Connection reset  , 这里没有换行符而服务端是按行接收 ,
//            Tcp 当一方挂了,另一方也会挂
                System.out.println("请说:");
                String msg = sc.nextLine();
                ps.println(msg);
                ps.flush();
            }
            //socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
//服务端
public class ServerSocketDemo {
    public static void main(String[] args) {
        try {
            //1.注册端口!!! 使用ServerSocket
            //指定一致的服务端端口号
            ServerSocket serverSocket = new ServerSocket(9999);
            //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){
                //获取远端ip地址
                System.out.println(socket.getRemoteSocketAddress()+"发送:"+msg);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 现在服务端 为什么 不可以同时接收多个客户端地消息
    • 目前服务端是单线程的 , 每次只能处理一个客户端的消息

解决接收多个发送端消息问题? (真正的多发)

  • 方式一:
    • 1 主线程定义 循环负责接收客户端Socket管道 连接
    • 2 每接收到一个Socket通信管道后 分配一个独立的线程负责 处理它
//客户端
public class ClientSocketTcpDemo {
    public static void main(String[] args) {
        try {
            //1.创建 socket 管道!!!!
            Socket socket = new Socket(InetAddress.getLocalHost().getHostAddress(),9999);
            //2.socket管道中,创建字节输出流
            OutputStream os = socket.getOutputStream();
            //3.低级字节输出流 转为 打印流
            //public PrintStream(OutputStream out)
            PrintStream  ps = new PrintStream(os);
            Scanner sc = new Scanner(System.in);
            while (true) {
                //4.发送消息
                System.out.println("请说:");
                String msg = sc.nextLine();
                //输入exit,关闭socket管道
                if ("exit".equals(msg)){
                    break;
                }else {
                    ps.println(msg);
                    ps.flush();
                }
            }
            //socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
//服务端主线程 
public class ServerSocketDemo {
    public static void main(String[] args) {
        try {
            //1.注册端口!!! 使用ServerSocket
            //指定一致的服务端端口号
            ServerSocket serverSocket = new ServerSocket(9999);
            while (true) {
                //2.调用accept() , 等待接收客户端Socket连接请求 ,建立Socket通信管道
                Socket socket = serverSocket.accept();
                new ServerReaderThread(socket).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
//socket对象接收消息 的 独立线程
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));
            System.out.println(socket.getRemoteSocketAddress()+"已上线!!!欢迎!!!");
            //5.按行读取消息
            String msg;
            while ((msg = br.readLine()) != null){
                //获取远端ip地址
                System.out.println(socket.getRemoteSocketAddress()+"发送:"+msg);
            }
        } catch (Exception e) {
            //发送端异常!!!!
            //当发送方socket管道断开,结束接收
            System.out.println(socket.getRemoteSocketAddress() + "已下线~~~~");
        }
    }
}

同时运行 三个不同的客户端 发送消息

服务端的 运行结果

image-20240210172425166
  • 方式二
    • 引入线程池 处理多个客户端消息
image-20240210172843461

线程池使用:

客户端不用修改

//服务端 主线程
public class ServerSocketDemo {
    //创建线程池
    public static Executor executor = new ThreadPoolExecutor(3,5,3, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(2), Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy());
    public static void main(String[] args) {
        try {
            //1.注册端口!!! 使用ServerSocket
            //指定一致的服务端端口号
            ServerSocket serverSocket = new ServerSocket(9999);
            while (true) {
                //2.调用accept() , 等待接收客户端Socket连接请求 ,建立Socket通信管道
                Socket socket = serverSocket.accept();
                //3.创建 任务类对象
                Runnable target = new ServerSocketRunnable(socket);
                executor.execute(target);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
//服务端接收消息 的 独立线程
public class ServerSocketRunnable implements Runnable{
    private Socket socket;
    public ServerSocketRunnable(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            //3.通过Socket通信管道 得到 一个字节输入流
            InputStream is = socket.getInputStream();
            //4.低级输入流 转为 缓冲字节输入流
            //通过转换流 , 将字节流变为字符流
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            System.out.println(socket.getRemoteSocketAddress()+"已上线!!!欢迎!!!");
            //5.按行读取消息
            String msg;
            while ((msg = br.readLine()) != null){
                //获取远端ip地址
                System.out.println(socket.getRemoteSocketAddress()+"发送:"+msg);
            }
        } catch (IOException e) {
            //发送端异常!!!!
            //当发送方socket管道断开,结束接收
            System.out.println(socket.getRemoteSocketAddress() + "已下线~~~~");
        }
    }
}

使用线程池的优势在哪里

  • 服务端可以服用线程处理多个客户端,可以避免系统瘫痪
  • 适合客户端通信时长较短的场景
1.4 即时通信(重点)

即时通信是什么含义,要实现怎样的设计

  • 即时通信:一个客户端的消息发出去,其他客户端可以接收到

  • 之前 消息都是发给服务端

  • 即时通信 需要进行 端口转发 的设计思想

image-20240210174934339
/**
 *  重点 !!!!!
 *  目标 : 学会使用 socket类 ,实现即时通信 多发多收
 *         客户端发送消息(输出) ,服务器转交(输入和输出) , 所有客户端接收消息(输入)
 */
public class ClientSocketTcpDemo {
    public static void main(String[] args) {
        try {
            //1.创建 socket 管道!!!!
            Socket socket = new Socket(InetAddress.getLocalHost().getHostAddress(),9999);
            //创建 一个独立的线程 专门负责客户端的 接收 消息(服务器随时可能转发过来)
            new ClientReaderThread(socket).start();
            //2.socket管道中,创建字节输出流,发送消息
            OutputStream os = socket.getOutputStream();
            //3.低级字节输出流 转为 打印流
            PrintStream  ps = new PrintStream(os);
            Scanner sc = new Scanner(System.in);
            while (true) {
                //4.发送消息
                System.out.println("请说:");
                String msg = sc.nextLine();
                //输入exit,关闭socket管道
                if ("exit".equals(msg)){
                    break;
                }else {
                    ps.println(msg);
                    ps.flush();
                }
            }
            //socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
/**
 *  客户端 接收消息的线程 , 打印消息
 */
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){
                //获取远端ip地址
                System.out.println("收到"+socket.getRemoteSocketAddress()+"发送:"+msg);
            }
        } catch (Exception e) {
            //服务端异常!!!!
            //当服务端socket管道断开,结束接收
            System.out.println("服务端把你退出群聊~~~~");
        }
    }
}
/**
 *  重点 !!!
 *  目标:实现服务端 网络通信 多个发送端的多发多收  !!!
 *  注意!!! 主线程 根据socket管道创建子线程
 *      子线程启动后,接收消息
 *   服务端,接收发送端消息,转发给所有的发送端
 */
public class ServerSocketDemo {
    //创建 一个静态的Socket集合
    public static List<Socket> allSocketList = new ArrayList<>();
    public static void main(String[] args) {
        try {
            //1.注册端口!!! 使用ServerSocket
            //指定一致的服务端端口号
            ServerSocket serverSocket = new ServerSocket(9999);
            while (true) {
                //2.调用accept() , 等待接收客户端Socket连接请求 ,建立Socket通信管道
                Socket socket = serverSocket.accept();
                System.out.println(socket.getRemoteSocketAddress()+"已上线!!!欢迎!!!"); //上线
                //添加到 Socket集合
                ServerSocketDemo.allSocketList.add(socket);
                //子线程 启动(服务端 接收和发送消息)
                new ServerReaderThread(socket).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

/**
 *   服务端,接收发送端的Socket管道
 *   再向所有的Socket对象 发送消息
 */
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){
                //获取远端ip地址
                //服务器 接收(输入)
                System.out.println(socket.getRemoteSocketAddress()+"发送:"+msg);
                //每一行的消息,服务器再(输出)到所有管道
                sendMsgToAll(msg);
            }
        } catch (Exception e) {
            //发送端异常!!!!
            //当发送方socket管道断开,结束接收
            System.out.println(socket.getRemoteSocketAddress() + "已下线~~~~");
            ServerSocketDemo.allSocketList.remove(socket);      //移除
        }
    }
    /**
     * 向所有Socket管道发送(输出)消息
     * @param msg        消息
     * @throws Exception
     */
    private void sendMsgToAll(String msg) throws Exception{
        for (Socket socket : ServerSocketDemo.allSocketList) {
            PrintStream ps = new PrintStream(socket.getOutputStream());
            ps.println(msg);
            ps.flush();
        }
    }
}

之前 都是 CS通信

1.5 BS开发

image-20240210181517785

/**
 *  目标 : 模拟 BS模式
 *         不需要考虑开发浏览器(发送端), 只考虑服务器输出
 */
public class ServerDemo{
    public static Executor pool = new ThreadPoolExecutor(3,5,3, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(2), Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {
        try {
            //1.注册端口
            ServerSocket serverSocket = new ServerSocket(9999);

            //2.线程池处理 接收多个客户端请求
            while(true){
                Socket socket = serverSocket.accept();
                //3.交给一个独立线程来处理
                pool.execute(new ServerSocketRunnable(socket));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
/**
 *   注意: 响应HTTP协议格式!!!!
 */
public class ServerSocketRunnable implements Runnable{
    private Socket socket;
    public ServerSocketRunnable(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'>《最牛的149期》</span>");
            ps.close();
        } catch (Exception e) {
            //发送端异常!!!!
            //当发送方socket管道断开,结束接收
            System.out.println(socket.getRemoteSocketAddress() + "已下线~~~~");
        }
    }
}
image-20240210183116916

Http协议格式

image-20240210183357034

  • 24
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值