20天深度复习JavaSE的详细笔记(十八)——网络编程

Demo18-网络编程

1.概述

什么是网络编程:

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

网络通信模式:

  • 常见的网络通信模式有如下两种:Client-Server(CS),Browser/Server(BS)

在这里插入图片描述

在这里插入图片描述

2.网络通信三要素

2.1三要素概述

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

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

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

2.2IP地址

2.2.1IP概述

IP(Internet Protocol):全称"互联网协议地址",是分配给上网设备的唯一标志,用来定位网络上的设备的

1.常见的IP分类为:IPv4和IPv6

  • IPv4:32bit(4字节)
    • 11000000 10101000 00000001 01000010
      • 计算机中存储的如上二进制人看不懂,所以用"点分十进制表示法"进行表示
      • 192.168.1.66
  • Ipv6:128位(16个字节),号称可以为地球上每一粒沙子编号(我们的电脑一般都是兼容IPv6的)
    • 用"冒分十六进制表示法"进行表示

2.IP地址基本寻路:

在这里插入图片描述

如果没有在本地dns服务器找到对应的IP地址(比如我这是一台新电脑,或者我第一次访问这个网站)会再将信息发送到运营商的dns服务器上,运营商的肯定有

3.IP地址形式:

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

4.IP常用命令:

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

5.特殊IP地址:

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

java中IP地址操作类:InetAddress:

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

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
public class InetAddressDemo01 {
    public static void main(String[] args) throws Exception {
        // 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("110.242.68.4");
        System.out.println(ip3.getHostName());
        System.out.println(ip3.getHostAddress());

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

2.3端口号

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

端口类型:

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

1.我们自己开发的程序选择注册端口,且一个设备中不能出现两个程序的端口号一样,否则出错2.不同设备的相同程序端口号要一样

2.4协议

1.通信协议:

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

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

  • OSI参考模型:世界互联协议标准,全球通信规范,由于此模型过于理想化,未能在因特网上进行广泛推广
  • TCP/IP参考模型(或TCP/IP协议):事实上的国际标准
OSI参考模型TCP/IP参考模型各层对应面向操作
应用层
表示层
会话层
应用层HTTP、FTP、DNS、SMTP…应用程序需要关注的:浏览器,邮箱.程序员一般在这一层开发
传输层传输层TCP、UDP…选择使用的TCP,UDP协议
网络层网络层IP、ICMP…封装源和目标IP,进行路径选择
数据链路层
物理层
数据链路层+物理物理寻址、比特流…物理设备中传输

3.传输层的2个常见协议

第一个是:TCP(Transmission Control Protocol) :传输控制协议:

  • 特点:
    • 使用TCP协议,必须双方先建立连接,它是一种面向连接的可靠通信协议
    • 传输前,采用“三次握手”方式建立连接,所以是可靠的 。
      • 客户端->服务器端:客户端向服务器发出连接请求,等待服务器确认
      • 服务器端->客户端:服务器端向客户端返回一个响应,告诉客户端收到了请求
      • 客户端->服务器端:客户端向服务器再次发出确认消息,连接建立
    • 传输后,采用"四次挥手"方式断开连接
      • 客户端->服务器端:客户端向服务器发出取消连接请求
      • 服务器端->客户端:服务器向客户端返回一个响应,表示收到客户端取消请求
      • 服务器端->客户端:服务器将最后的数据处理完毕后服务器向客户端发出确认取消信息
      • 客户端->服务器端:客户端再次发送确认消息,连接取消
    • 在连接中可进行大数据量的传输 。
    • 连接、发送数据都需要确认,且传输完毕后,还需释放已建立的连接,通信效率较低。
  • 通信场景:
    • 对信息安全要求较高的场景,例如:文件下载、金融等数据通信。

第二个是:UDP(User Datagram Protocol):用户数据报协议:

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

3.UDP通信

3.1UDP通信:快速入门(一发一收案例)

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:能够接收内容的长度

DatagramSocket:发送端和接收端对象

构造器说明
public DatagramSocket()创建发送端的Socket对象,系统会随机分配一个端口号(因为发送端只是把数据扔出去)
public DatagramSocket(int port)创建接收端的Socket对象并指定端口号(因为只有根据端口号才能找到接收端)

DatagramSocket类成员方法:

方法说明
public void send(DatagramPacket dp)发送数据包
public void receive(DatagramPacket p)接收数据包
public class ClientDemo1 {
    public static void main(String[] args) throws Exception {
        System.out.println("=====客户端启动======");

        // 1、创建发送端对象:发送端自带默认的端口号(也可以手动设置端口号)
//        DatagramSocket socket = new DatagramSocket(6666);
        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();
    }
}

public class ServerDemo2 {
    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、取出数据即可
        // getLength()获取接收数据的长度
        int len = packet.getLength();
        String rs = new String(buffer,0, len);
        System.out.println("收到了:" + rs);

        // 获取发送端的ip和端口
        String ip  =packet.getSocketAddress().toString();//方法返回值是SocketAddress类型,调用其toString()获得对象中的内容
        System.out.println("对方地址:" + ip);
        int port  = packet.getPort();//方法返回值是int类型
        System.out.println("对方端口:" + port);

        socket.close();
    }
}

3.2UDP通信:多发多收

需求:使用UDP通信方式开发接收端和发送端

分析:

  • 发送端可以一直发送消息
  • 接收端可以不断的接收多个发送端的消息
  • 发送端输入了exit则结束发送端程序
public class ClientDemo1 {
    public static void main(String[] args) throws Exception {
        System.out.println("=====客户端启动======");

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

        Scanner sc = new Scanner(System.in);
        while (true) {
            System.out.println("请说:");
            String msg = sc.nextLine();

            if("exit".equals(msg)){
                System.out.println("离线成功!");
                socket.close();
                break;
            }

            // 2、创建一个数据包对象封装数据
            byte[] buffer = msg.getBytes();
            DatagramPacket packet = new DatagramPacket( buffer, buffer.length,
                    InetAddress.getLocalHost() , 8888);

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

public class ServerDemo2 {
    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);

        while (true) {
            // 3、等待接收数据。
            socket.receive(packet);
            // 4、取出数据即可
            int len = packet.getLength();
            String rs = new String(buffer,0, len);
            System.out.println("收到了来自:" + packet.getAddress() +", 对方端口是" + packet.getPort() +"的消息:" + rs);
        }
    }
}

如果我想开多个客户端怎么办呢(前提是new发送端对象时没有设置客户端端口号而是让每个客户端有一个随机的端口号)?

  • idea默认不允许多开客户端,如果已经开了一个客户端再开一个时会出现如下提示:

在这里插入图片描述

  • 想要多开客户端需要进行如下操作:

在这里插入图片描述

在这里插入图片描述

然后点击apply即可(此时会跳出提示,不用管,直接关闭就可以了)

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

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

3.3UDP通信-广播,组播

UDP的三种通信方式:

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

等学完计网再回来研究

4.TCP通信

4.1快速入门(一发一收)

特点:

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

在这里插入图片描述

那我怎么知道有没有用TCP协议呢:在java中只要是使用java.net.Socket类实现通信,底层即是使用了TCP协议

1.客户端

Socket(客户端)

构造器说明
public Socket(String host , int port)创建发送端的Socket对象与服务端连接,参数为服务端程序的ip和端口

Socket类成员方法:

方法说明
OutputStream getOutputStream()获得字节输出流对象
InputStream getInputStream()获得字节输入流对象

2.服务端

ServerSocket(服务端)

构造器说明
public ServerSocket(int port)注册服务端端口

ServerSocket类成员方法

方法说明
public Socket accept()等待接受客户端的Socket通信连接,连接成功返回Socket对象与客户端建立端到端通信
public class ClientDemo1 {
    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", 7777);

            // 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();
        }
    }
}

public class ServerDemo2 {
    public static void main(String[] args) {
        try {
            System.out.println("===服务端启动成功===");
            // 1、注册端口
            ServerSocket serverSocket = new ServerSocket(7777);
            // 2、调用accept方法
            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){
                //socket.getRemoteSocketAddress()获取客户端ip
                System.out.println(socket.getRemoteSocketAddress() + "说了:" + msg);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

1.第23行socket.close()关闭资源:

  • 不建议在这里关闭管道,因为TCP是面向连接的协议,客户端关闭管道时信号会告诉服务端,然后服务端的管道也会随之关闭,但是可能此时服务端接收数据只接收了一半,那就会造成数据没有接收完全,这是老师这样说的,但是我不这样认为
    • 我认为断开连接时的四次挥手,其中有一点是:服务器将最后的数据处理完毕后服务器向客户端发出确认取消信息,所以我认为服务端会将消息处理完(接收和发送)才会断开连接(等我以后空闲了从客户端传2G的视频给服务端,看看是接收完断开连接还是直接断开连接,应该也没必要,等我学完计网应该就知道哪种说法对了)

2.第18行的println方法:

  • 如果改成print,结果是:
    • 客户端发完数据后任务就完成了,客户端程序结束
    • 与此同时因为客户端发送的数据没有换行符,而服务端是每次读取一行,所以服务端认为客户端还没有发送完数据,就一直等,结果等着等着客户端程序结束了,因为TCP是面向连接协议,所以此时服务端的Socket出错,服务端程序就会报错终止
    • 如果改为print的同时加了socket.close();不会报错,因为此时代表两端正常断开连接
      • 题外话:改为print的同时加了socket.close();不仅不再会报错,并且客户端发送的消息也会被服务端获取后输出输出,这也印证了我上面的观点:只有服务端将消息处理完(接收和发送)才会断开连接,并不会因为关闭管道而造成服务端接收数据不完全

3.第38行Socket socket = serverSocket.accept():

  • 服务端启动后会停在这行代码直到客户端的Socket socket = new Socket(“127.0.0.1”, 7777);执行后两者建立连接才会继续执行后面的代码,简言之这行代码的作用是:
    • 等待接收客户端的Socket连接请求,建立Socket通信管道

4.第45行br.readLine():

  • 客户端不发消息过来,服务端就会在这个方法堵塞等待,直到客户端发消息了才会执行readLine方法

5.1.TCP通信的基本原理?

  • 调用accept()方法阻塞等待接收客户端连接。得到Socket对象
  • 客户端如果没有消息,服务端会进入阻塞等待
  • 客户端怎么发,服务端就应该怎么收
  • Socket一方关闭(调用close方法)或者出现异常(如一方程序关闭就会使该方Socket出错)、对方Socket也会失效或者出错

4.2多发多收

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

具体要求:

  • 可以使用死循环控制服务端收完消息继续等待接收下一个消息
  • 客户端也可以使用死循环等待用户不断输入消息
  • 客户端一旦输入了exit,则关闭客户端程序,并释放资源

和一发一收区别是两端都多了个死循环,别的没区别,这里就不再展示代码

本案例实现了多发多收,那么是否可以同时接收多个客户端的消息?

  • 不可以
    • 因为服务端现在只有一个线程,当Socket socket = serverSocket.accept();执行后与客户端建立连接,从此以后服务端就只能负责与这一个客户端进行通信

4.3同时接收多个客户端消息

如何才能让服务端可以处理多个客户端的通信需求:引入多线程

  • 主线程定义循环负责接收客户端Socket管道连接
  • 每接收到一个Socket通信管道后分配一个独立的线程负责处理它
  • 最后结果是:
    • 服务端仍只有一个,但因为多线程的存在所以该服务端可以与多个客户端建立连接
public class ServerDemo2 {
    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() + "下线了!!!");
        }
    }
}

public class ClientDemo1 {
    public static void main(String[] args) {
        try {
            System.out.println("====客户端启动===");
            // 1、创建Socket通信管道请求与服务端的连接
            Socket socket = new Socket("127.0.0.1", 7777);
            // 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();
        }
    }
}

4.4使用线程池优化

目前的通信架构存在什么问题:

  • 客户端与服务端的线程模型是:N-N的关系
    • 导致客户端并发越多,系统瘫痪的越快
public class ServerDemo2 {
    // 使用静态变量记住一个线程池对象
    private static ExecutorService pool = new ThreadPoolExecutor(1,
            1, 6, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(1)
    , Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {
        try {
            System.out.println("===服务端启动成功===");
            // 1、注册端口
            ServerSocket serverSocket = new ServerSocket(6666);
            // a.定义一个死循环由主线程负责不断的接收客户端的Socket管道连接。
            while (true) {
                // 2、每接收到一个客户端的Socket管道,
                Socket socket = serverSocket.accept();
                System.out.println(socket.getRemoteSocketAddress()+ "它来了,上线了!");

                // 任务对象负责读取消息。
                Runnable target = new ServerReaderRunnable(socket);
                pool.execute(target);
            }
        } catch (Exception e) {
            System.out.println("别来了,已经满了");
        }
    }
}

public class ServerReaderRunnable implements Runnable{
    private Socket socket;
    public ServerReaderRunnable(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() + "下线了!!!");
        }
    }
}

public class ClientDemo1 {
    public static void main(String[] args) {
        try {
            System.out.println("====客户端启动===");
            // 1、创建Socket通信管道请求与服务端的连接
            Socket socket = new Socket("127.0.0.1", 6666);

            // 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();
        }
    }
}
  • 针对该案例(一个核心线程,最大线程数是1,任务队列最大任务数是1),启动客户端1,主线程会处理该任务,再启动客户端2,该任务会被放到任务队列,当正常(close方法)或非正常(直接终止客户端2的程序)关闭客户端1时,核心线程会处理任务2
    • 注意,此时并不是主线程死亡然后创建了一个新的线程处理任务2,因为该案例中处理任务时会捕获处理异常(第36-48行),所以当关闭客户端2时run()方法一定是正常执行完的,所以这里是属于核心线程处理完任务1后处理任务2且该线程并未死亡
  • 若启动客户端1,2后都未关闭,然后启动客户端3时会执行第24行的"System.out.println(“别来了,已经满了”);",此后,启动客户端将不再加入线程池,因为不会再执行while循环了
  • 按如下顺序操作:
    • 启动客户端1,2后都未关闭
    • 启动客户端3
    • 关闭客户端1
    • 结果是:此时核心线程会处理任务2
      • 我的疑惑是为什么在main方法中无论是抛出异常还是捕获处理异常(第24行就是捕获处理),按照这个顺序操作后最终结果都是核心线程会处理任务2?(抛出异常我用代码实验过了)
      • 如果是抛出异常,那主线程死亡了啊,线程池为什么还能正常运行呢?如果是main中捕获处理异常,那么在上述操作启动客户端3时主线程就执行完毕了啊,也相当于死亡了吧(因为启动客户端3时会处理异常,然后就不会再执行while循环了,会接着执行try…catch外部后面的代码,后面没代码了,所以主线程执行完毕)
      • 这就是我想问的:如果主线程创建了线程池,当主线程死亡后线程池会销毁吗

本次使用线程池的优势在哪里:

  • 服务端可以复用线程处理多个客户端,可以避免系统瘫痪
  • 适合客户端通信时长较短的场景(如果通信时长较长,那你这一个任务就一直占有着一个线程,别的任务可能就没机会执行了)

4.5实战案例

4.5.1即时通信

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

  • 即时通信,是指一个客户端的消息发出去,其他客户端可以接收到。
  • 之前我们的消息都是发给服务端的。
  • 即时通信需要进行端口转发的设计思想。
public class ServerDemo2 {
    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(onLineSocket.getOutputStream());
                    ps.println(msg);
                    ps.flush();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

public class ClientDemo1 {
    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("服务端把你踢出去了~~");
        }
    }
}

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

  • 即时通信,是指一个客户端的消息发出去,其他客户端可以接收到
  • 即时通信需要进行端口转发的设计思想(端口转发就是把一个端口的数据转发到另外一个端口)
  • 服务端需要把在线的Socket管道存储起来
  • 一旦收到一个消息要推送给其他管道
4.5.2模拟BS系统

之前的客户端都是什么样的:

  • 其实就是CS架构,客户端需要我们自己开发实现

BS结构是什么样的,需要开发客户端吗:

  • 浏览器访问服务端,不需要开发客户端

浏览器与服务端建立Socket管道,服务端将这些管道封装成任务交给线程池处理,最后响应网页给浏览器显示,注意:服务器必须给浏览器响应HTTP协议格式的数据,否则浏览器不识别

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>");

完整案例代码:

public class BSserverDemo {
    // 使用静态变量记住一个线程池对象
    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'>《最牛的Chinese》 </span>");
            ps.close();
        } catch (Exception e) {
            System.out.println(socket.getRemoteSocketAddress() + "下线了!!!");
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

认真生活的灰太狼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值