【网络编程:BIO模型(实现群聊)】

希望这篇博客帮助你理解IO模型之BIO模型!

一、什么式BIO模型
  • 网络编程的基本模型是C/S模型,即是两个进程之间的通信。服务端提供 IP 和监听端口,客户端通过连接想操作向服务器地址发送请求,通过三次握手连接,如果建立成功,双方就通过套接字进行通信。
  • 传统的同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式的通信。
  • 简单的描述一下BIO服务端通信:采用BIO通信模型的服务端,通常由一个Acceptor线程负责监听客户端的连接,它接收到客户端连接请求后为每一个客户端创建一个线程处理,通过输出流返回给客户端,线程销毁。典型的一请求一答的通信模型

附:TCP三次握手、四次挥手

主线程负责监听有新的客户端连接就创建一个子线程处理任务
在这里插入图片描述

二、BIO模型的缺点
  • 该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发量访问增加后,服务器线程个数和客户端并发访问数呈 1:1 的正比关系,java 中的线程也是宝贵的资源,线程数量快速膨胀,系统的性能急速下降,随着访问量的继续增大,系统最终死掉!
三、BIO 模型的实现

在这里插入图片描述

  • 对于客户端,我们需要使用Socket类来创建对象。对于服务器端,我们需要使用ServerSocket来创建对象,通过对象调用accept()方法来进行监听是否有客户端访问

客户端:

  • 1.构建Socket实例,通过指定的服务器地址和端口建立连接。
  • 2.利用Socket实例包含的InputStream和OutputStream进行数据读写。
  • 3.操作结束后调用socket实例的close方法关闭连接
/**
 * 聊天室客户端
 */
public class Client {
    /*
        java.net.Socket 套接字
        Socket封装了TCP的通讯细节,我们使用Socket与服务端建立连接后,只需要通过两条流的
        读写来完成与服务端的交互操作.
     */
    private Socket socket;

    public Client(){
        try {
            /*
                实例化Socket时需要传入两个参数,分别表示服务端的IP地址与端口号
                IP地址:用于找到网络上服务端的计算机
                端口:用于找到服务端计算机上的服务端应用程序
             */
            System.out.println("正在连接服务端...");
            socket = new Socket("localhost",8088);
            System.out.println("与服务端建立连接!");
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public void start(){
        try {
            //启动线程用于读取服务端发送过来的消息
            ServerHandler handler = new ServerHandler();
            Thread t = new Thread(handler);
            t.setDaemon(true);
            t.start();

            /*
                Socket提供的方法:
                OutputStream getOutputStream()
                该方法会返回一个字节输出流,通过这个流写出的数据会通过网络发送给远端计算机
             */
            OutputStream out = socket.getOutputStream();
            //按行发送字符串给服务端
            OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");
            BufferedWriter bw = new BufferedWriter(osw);
            PrintWriter pw = new PrintWriter(bw,true);

            Scanner scanner = new Scanner(System.in);
            while(true) {
                String line = scanner.nextLine();
                if("exit".equals(line)){
                    break;
                }
                pw.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally{
            try {
                socket.close();//与服务端断开连接
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    public static void main(String[] args) {
        Client client = new Client();
        client.start();
    }

    /**
     * 该线程负责读取服务端发送过来的消息
     */
    private class ServerHandler implements Runnable{
        public void run(){
            try{
                //通过socket获取输入流读取服务端发送过来的消息
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in,"UTF-8");
                BufferedReader br = new BufferedReader(isr);
                String line;
                while((line = br.readLine())!=null){
                    System.out.println(line);
                }
            }catch(Exception e){
//                e.printStackTrace();
            }
        }
    }

}

服务器端:

  • 1.构建一个ServerSocket实例,指定本地的端口,用于监听其连接请求。
  • 2.调用socket的accept()方法获得客户端的连接请求,通过accept()方法返回的socket实例,建立与客户端的连接。
  • 3.通过返回的socket实例来获得InputStream和OutputStream,进行数据的写入和读出。
  • 4.调用socket的close()方法关闭socket连接 。

问题来了:我们要实现群聊,怎么做?

  • 思路:我们客户端和客户端之间是不知道 IP 端口号的,所以肯定客户端之间没有办法直接通信,那就必须通过服务端来通信,打个比方:我们几个人都用网易云音乐服务器,我们之间聊天,肯定是通过服务器来完成转发的,那么我们如何实现转发??我画了一个草图

在这里插入图片描述

  • 每连接一个用户,我们服务器都会生成一个socket,通过socket获取输出流,给客户端发消息,那我们可不可以 创建一个数组,里面就是存储的我们每位用户的输出流,那我们遍历数组,然后就可以把每位用户的输入给所有在线的用户转发,从而实现群聊
  • 注意:那用户下线后,我们如何把此用户在数组里面的输出流删除呢,我们是不是可以这样理解:A用户下线后,那么给A用户分配的线程也就死亡,那我们就可以把 删除A用户输出流操作 和关闭 socket 操作一起放在 finally 块中;其次并发环境下,线程安全问题是我们必须要考虑的,所以在有必要的方法上进行加锁同步,以保证我们程序的正确执行!
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;

public class Server1 {
    private ServerSocket serverSocket;
    private Collection<PrintWriter> allOut = new ArrayList<>();


    public Server1() {
        try {
            System.out.println("正在启动服务器...");
            serverSocket = new ServerSocket(8088);
            System.out.println("服务器启动成功");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {

        try {
            while (true) {
                //等待用户连接
                System.out.println("等待用户连接...");
                Socket socket = serverSocket.accept();//阻塞方法
                System.out.println("一个用户已连接");
                //任务
                Client1Handler client1Handler = new Client1Handler(socket);
                Thread thread = new Thread(client1Handler);
                thread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public class Client1Handler implements Runnable {
        private Socket socket;
        private String host;//当前客户端地址信息

        public Client1Handler(Socket socket) {
            this.socket = socket;
            host = socket.getInetAddress().getHostAddress();
        }

        @Override
        public void run() {

            PrintWriter p = null;
            //接受用户发来的消息
            try {
                InputStream in = socket.getInputStream();
                //转换流
                InputStreamReader ir = new InputStreamReader(in, "utf-8");
                BufferedReader buff = new BufferedReader(ir);

                //通过socket获取输出流,给客户端发消息
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out, "utf-8");
                BufferedWriter buf = new BufferedWriter(osw);
                p = new PrintWriter(buf, true);//自动刷新
                /*
                将该输出流存入数组allOut中,这样其他的ClientHandlder实例就可以得到
                当前的ClientHandlder实例中的输出流,以便把消息发送给客户端
                 */

                //数组扩容
                synchronized (Server1.class) {
                    allOut.add(p);
                }
               // System.out.println(host + "上线了,当前人数:" + allOut.length);
                System.out.println(host + "上线了,当前人数:" + allOut.size());

                String str;
                while ((str = buff.readLine()) != null) {
                    System.out.println(host + "说:" + str);
                    //回复给客户端
                    synchronized (Server1.this) {
                        for (PrintWriter pw:allOut){
                            pw.println(host + "说:" + str);
                        }
                    }
                    //p.println(host + "说:" + str);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                //处理当客户端连接断开的操作

                //将当前的客户端从输入流数据剔除
                synchronized (Server1.this) {
                    allOut.remove(p);
                }
     
                System.out.println(host + "下线了,当前人数:" + allOut.size());
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        Server1 server1 = new Server1();
        server1.start();
    }
}

运行结果:

  • 客户端:
    正在连接服务器…
    服务器连接成功
    请输入要给服务器发送的信息:
    你好哎 服务器
    127.0.0.1说:你好哎 服务器
    exit

服务端:

  • 正在启动服务器…
    服务器启动成功
    等待用户连接…
    一个用户已连接
    等待用户连接…
    127.0.0.1上线了,当前人数:1
    127.0.0.1说:你好哎 服务器
    127.0.0.1下线了,当前人数:0
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值