107-Java网络编程:TCP通信实战案例:即时通信、BS架构模拟

TCP通信实战案例

一、即时通信

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

2、端口转发设计思想
(1)客户端发送消息
  • 客户端1发送了一条消息:“约吗?”
  • 注意:客户端1发送的 “约吗?”,是先发给服务端的,而不是直接发送给其他客户端的。

107-Java网络编程:TCP通信实战案例:即时通信、BS架构模拟_第1张图片
107-Java网络编程:TCP通信实战案例:即时通信、BS架构模拟_第2张图片



(2)群发
  • 此时,服务端接收到客户端1发送的 “约吗?”
  • 进行端口转发给其他的客户端,这样其他的客户端就可以看到客户端1发出去的 “约吗?”。

107-Java网络编程:TCP通信实战案例:即时通信、BS架构模拟_第3张图片



(3)私发
  • 此时,服务端接收到客户端1发送的 “约吗?”
  • 进行端口转发给要私发的那个客户端,这样被私发的那个客户端就可以看到客户端1发出去的 “约吗?”

107-Java网络编程:TCP通信实战案例:即时通信、BS架构模拟_第4张图片



3、群聊实现
(1)需求
  • 客户端可以发送消息,同时也可以接收到其他客户端和自己发出的消息。
  • 服务端接收到多个客户端发送的消息,转发给全部在线的客户端。
(2)分析实现
  • 客户端

    1. 创建Socket对象,指定服务端的IP和端口,与服务端建立连接。

    2. 创建客户端读取消息的线程任务类:

      a. 定义socket变量,用于存储接收到的客户端的socket管道连接请求

      b. 定义有参数构造器,用于接收客户端的socket请求,初始化到socket变量

      c. 重写run方法:

      1. 从socket通信管道中得到字节输入流管道
      2. 将字节输入流管道转换成字符输入流管道,并包装成缓冲字符输入流管道:负责接收服务端转发其他客户端发送的消息过来
      3. 使用死循环开始按照行读取服务端转发过来的数据
      4. 展示读取到的数据
      5. 如果捕获到异常,说明服务端已经挂了!说明群聊已解散!
    3. 创建一个独立的线程对象专门负责这个客户端的读取消息任务(服务端随机可能转发其他客户端的消息过来!)

    4. 从socket通信管道中得到字节输出流管道,并包装成打印流管道

    5. 客户端开始发送数据给服务端

    package com.app.d10_tcp_socket5_sms;
    
    import java.io.PrintStream;
    import java.net.Socket;
    import java.util.Scanner;
    
    /**
        需求:
            1、客户端可以发送消息
            2、客户端可以接收到其他客户端和自己发出的消息
     */
    public class Client {
        public static void main(String[] args) throws Exception {
            System.out.println("-=-=-=-客户端启动成功-=-=-=-");
            // 1、创建Socket对象,指定服务端的IP和端口,与服务端建立连接
            Socket socket = new Socket("127.0.0.1", 7878);
    
            // 3、创建一个独立的线程对象专门负责这个客户端的读取消息任务(服务端随时可能转发其他客户端的消息过来!)
            new ClientReaderThread(socket).start();
    
            // 4、从socket通信管道中得到字节输出流管道,并包装成打印流管道
            PrintStream ps = new PrintStream(socket.getOutputStream());
    
            // 5、客户端开始发送数据给服务端
            Scanner sc = new Scanner(System.in);
            while (true) {
                String msg = sc.nextLine();
                ps.println(msg);
                ps.flush(); // 刷新数据
            }
        }
    }
    
    package com.app.d10_tcp_socket5_sms;
    
    import java.io.BufferedReader;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.net.Socket;
    
    /**
        2、创建客户端读取消息的线程任务类
     */
    public class ClientReaderThread extends Thread{
        // a. 定义socket变量,用于存储接收到的客户端的socket请求
        private Socket socket;
    
        // b. 定义有参数构造器,用于接收客户端的socket请求,初始化到socket变量
        public ClientReaderThread(Socket socket) {
            this.socket = socket;
        }
    
        // c. 重写run方法:
        @Override
        public void run() {
            try {
                // 1. 从socket通信管道中得到字节输入流管道
                InputStream is = socket.getInputStream();
    
                // 2. 将字节输入流管道转换成字符输入流管道,并包装成缓冲字符输入流管道,负责接收服务端转发其他客户端发送的消息过来
                BufferedReader br = new BufferedReader(new InputStreamReader(is));
    
                // 3. 使用死循环开始按照行读取服务端转发过来的数据
                String msg;
                while ( (msg = br.readLine()) != null ) {
                    // 4. 展示读取到的数据
                    System.out.println("收到了:" + msg);
                }
    
            } catch (Exception e) {
                // 5. 如果出现异常,说明群主已将你踢出群聊(服务端把你踢出去了~)
                System.out.println("服务端已将你踢出~~");
            }
        }
    }
    


  • 服务端

    1. 创建静态的List集合,用于存储当前全部在线的客户端的socket管道

    2. 创建ServerSocket对象,注册端口

    3. 使用死循环接收客户端的socket管道连接请求

    4. 开始接收每个客户端的socket管道连接请求(注意:在这里等待到客户端的socket管道连接)

    5. 跟踪客户端上线,将上线的客户端的socket管道添加到List集合中,完成上线

    6. 创建服务端接收客户端socket管道连接请求的线程任务类:

      a. 定义socket变量,用于存储接收到的客户端的socket请求

      b. 定义有参数构造器,用于接收客户端的socket请求,初始化到socket变量

      c. 重写run方法:

      1. 从socket通信管道中得到字节输入流管道

      2. 将字节输入流管道转换成字符输入流管道,并包装成缓冲字符输入流管道:负责接收服务端转发其他客户端发送的消息过来

      3. 使用死循环开始按照行读取客户端发送过来的数据

      4. 展示读取到的数据

      5. 创建独立的方法sendMsgToAllSockets:将该客户端发送的消息进行端口转发给全部在线的客户端的socket管道

        a. 遍历全部在线的客户端的socket管道的集合allOnlineSockets:

        ​ 1. 从socket管道中得到字节输出流管道,并包装成打印流管道:负责转发消息给全部在线的客户端的socket管道

    package com.app.d10_tcp_socket5_sms;
    
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.ArrayList;
    import java.util.List;
    
    /**
        需求:
            1、服务端接收多个客户端发送的消息
            2、服务端可以将多个客户端发送的消息转发给全部在线的客户端
     */
    public class Server {
        // 1、创建静态的List集合,用于存储当前全部在线的客户端的socket管道
        public static List<Socket> allOnlineSockets = new ArrayList<>();
    
        public static void main(String[] args) throws Exception {
            System.out.println("-=-=-=-服务端启动成功-=-=-=-");
            // 2、创建ServerSocket对象,注册端口
            ServerSocket serverSocket = new ServerSocket(7878);
    
            // 3、使用死循环接收客户端的socket管道连接请求
            while (true) {
                // 4、开始接收每个客户端的socket管道连接请求
                // 注意:在这里等到客户端的socket管道连接
                Socket socket = serverSocket.accept();
    
                // 5、跟踪客户端上线,将上线的客户端的socket管道添加到List集合中,完成上线
                System.out.println(socket.getRemoteSocketAddress() + "上线了~~");
                allOnlineSockets.add(socket);
    
                // 7、每接收到一个客户端的socket管道连接请求,创建一个独立的线程对象专门负责这个客户端的socket管道连接请求的任务
                new ServerReaderThread(socket).start();
            }
        }
    }
    
    package com.app.d10_tcp_socket5_sms;
    
    import java.io.*;
    import java.net.Socket;
    
    /**
        6、创建服务端接收客户端socket管道连接请求的线程任务类
     */
    public class ServerReaderThread extends Thread {
        // a. 定义socket变量,用于存储接收到的客户端的socket请求
        private Socket socket;
    
        // b. 定义有参数构造器,用于接收客户端的socket请求,初始化到socket变量
        public ServerReaderThread(Socket socket) {
            this.socket = socket;
        }
    
        // c. 重写run方法:
        @Override
        public void run() {
            try {
                // 1. 从socket通信管道中得到字节输入流管道
                InputStream is = socket.getInputStream();
    
                // 2. 将字节输入流管道转换成字符输入流管道,并包装成缓冲字符输入流管道,负责接收服务端转发其他客户端发送的消息过来
                BufferedReader br = new BufferedReader(new InputStreamReader(is));
    
                // 3. 使用死循环开始按照行读取客户端发送过来的数据
                String msg;
                while ( (msg = br.readLine()) != null ) {
                    // 4. 展示读取到的数据
                    System.out.println(socket.getRemoteSocketAddress() + "说了:" + msg);
    
                    // 5、创建独立的方法sendMsgToAllSockets:将该客户端发送的消息进行端口转发给全部在线的客户端的socket管道
                    sendMsgToAllSockets(msg);
                }
    
            } catch (Exception e) {
                // 6. 跟踪客户端下线,将下线的客户端的socket管道从List集合中删除
                System.out.println(socket.getRemoteSocketAddress() + "离线了~~");
                Server.allOnlineSockets.remove(socket);
            }
        }
    
        /**
            将消息进行端口转发给所有在线的客户端的方法
         */
        private void sendMsgToAllSockets(String msg) throws Exception {
            // a.遍历全部在线的客户端的socket管道的集合allOlineSockets
            for (Socket onlineSocket : Server.allOnlineSockets) {
                // 1. 从socket管道中得到字节输出流管道,并包装成打印流管道:负责转发消息给全部在线的客户端socket管道
                PrintStream ps = new PrintStream(onlineSocket.getOutputStream());
                ps.println(msg);
                ps.flush(); // 刷新数据
            }
        }
    }
    

(3)测试

107-Java网络编程:TCP通信实战案例:即时通信、BS架构模拟_第5张图片



总结

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

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



二、模拟BS请求[了解]

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


2、实现BS开发

107-Java网络编程:TCP通信实战案例:即时通信、BS架构模拟_第6张图片

注意:服务器必须给浏览器响应HTTP协议格式的数据,否则浏览器不识别


HTTP响应数据的协议格式:就是给浏览器显示的网页信息

107-Java网络编程:TCP通信实战案例:即时通信、BS架构模拟_第7张图片


package com.app.d11_tcp_socket6_thread_bs;

import java.net.ServerSocket;
import java.net.Socket;

/**
    目标:理解BS结构的基本原理
 */
public class BSServerDemo {
    public static void main(String[] args) {
        System.out.println("---------服务端启动成功----------");
        try {
            // 1、创建ServerSocket对象,注册端口
            ServerSocket serverSocket = new ServerSocket(8080);

            // 3、使用死循环接收多个客户端的socket管道连接请求
            while (true) {
                // a. 开始接收多个客户端的socket管道连接请求
                Socket socket = serverSocket.accept();

                // b. 将socket管道连接请求交给一个独立的线程来处理
                new ServerReaderThread(socket).start();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package com.app.d11_tcp_socket6_thread_bs;

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

/**
    2、创建线程任务类处理多个客户端的Socket管道连接请求
 */
public class ServerReaderThread extends Thread{
    // a. 定义socket变量,用于存储接收到的客户端的socket管道连接请求
    private Socket socket;

    // b. 定义有参数构造器,用于接收客户端的socket管道连接请求,并初始化给socket变量
    public ServerReaderThread(Socket socket) {
        this.socket = socket;
    }

    /**
        c. 重写run方法
     */
    @Override
    public void run() {
        try {
            // 浏览器已经与本线程建立了Socket管道
            // 1、响应消息给浏览器显示
            PrintStream ps = new PrintStream(socket.getOutputStream());

            // 2、必须响应HTTP协议格式数据,否则浏览器不认识消息
            ps.println("HTTP/1.1 200 OK");  // 协议类型/版本 状态码 响应成功的消息
            ps.println("Content-Type:text/html;charset=UTF-8"); // 响应的数据类型:文本/网页;字符编码

            ps.println();   // 必须发送一个空行

            // 3、响应数据回去给浏览器
            ps.println(" 《我是歌手》 ");
            ps.close(); // 释放资源!

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

测试

107-Java网络编程:TCP通信实战案例:即时通信、BS架构模拟_第8张图片


107-Java网络编程:TCP通信实战案例:即时通信、BS架构模拟_第9张图片



4、线程池优化BS
package com.app.d12_tcp_socket7_threadpool_bs;

import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;

/**
    目标:理解BS结构的基本原理,使用线程池优化BS
 */
public class BSServerDemo {
    // 定义静态的线程池,用于处理多个客户端的socket管道连接请求
    public static final ExecutorService pools = new ThreadPoolExecutor(3,
            5, 6, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3),
            Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {
        System.out.println("---------服务端启动成功----------");
        try {
            // 1、创建ServerSocket对象,注册端口
            ServerSocket serverSocket = new ServerSocket(8080);

            // 3、使用死循环接收多个客户端的socket管道连接请求
            while (true) {
                // a. 开始接收多个客户端的socket管道连接请求
                Socket socket = serverSocket.accept();

                // b. 将socket管道连接请求交给线程池来处理
                pools.execute(new ServerReaderRunnable(socket));
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package com.app.d12_tcp_socket7_threadpool_bs;

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

/**
    2、创建线程任务类处理多个客户端的Socket管道连接请求
 */
public class ServerReaderRunnable implements Runnable{
    // a. 定义socket变量,用于存储接收到的客户端的socket管道连接请求
    private Socket socket;

    // b. 定义有参数构造器,用于接收客户端的socket管道连接请求,并初始化给socket变量
    public ServerReaderRunnable(Socket socket) {
        this.socket = socket;
    }

    /**
        c. 重写run方法
     */
    @Override
    public void run() {
        try {
            // 浏览器已经与本线程建立了Socket管道
            // 1、响应消息给浏览器显示
            PrintStream ps = new PrintStream(socket.getOutputStream());

            // 2、必须响应HTTP协议格式数据,否则浏览器不认识消息
            ps.println("HTTP/1.1 200 OK");  // 协议类型/版本 状态码 响应成功的消息
            ps.println("Content-Type:text/html;charset=UTF-8"); // 响应的数据类型:文本/网页;字符编码

            ps.println();   // 必须发送一个空行

            // 3、响应数据回去给浏览器
            ps.println(" 《我是歌手》 ");
            ps.close(); // 释放资源!

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

测试

107-Java网络编程:TCP通信实战案例:即时通信、BS架构模拟_第10张图片


107-Java网络编程:TCP通信实战案例:即时通信、BS架构模拟_第11张图片



总结

1、TCP通信如何实现BS请求网页信息回来?

  • 客户端使用浏览器发起请求(不需要开发客户端)
  • 服务端必须按照浏览器的协议规则响应数据
  • 浏览器使用什么协议规则呢
    • HTTP协议(简单了解)

你可能感兴趣的:(JavaSE基础进阶篇,网络,java,tcp/ip)