IO模式(1):BIO模式下的即时通信—以Minecraft内置聊天室为例

前言

阅读这篇文章前,您需要以下基础

  • java IO流

  • 多线程

  • 一颗脑子🧠

BIO模式

概述

同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,该线程需要时刻监听连接。模式图如下:

实现

关键组件

  • ServerSocket 服务端通道,用来接收客户端的连接通道

  • Socket 数据传输的通道

  • InputStream/OutputStream 数据传输的载体

服务端接收一个客户端

服务端:1.接收客户端的连接 2.接收客户端传来的数据

1.创建ServerSocket对象,注册端口,用来接收客户端的连接

java 复制代码
ServerSocket serverSocket = new ServerSocket(9999);

2.调用ServerSocket的accept方法,获取客户端的连接

java 复制代码
Socket socket = serverSocket.accept();

3.连接建立后,可以调用socket.getInputStream()方法获取输入流,获取客户端传输来的数据并进行处理

java 复制代码
//从Socket中获取输入流
InputStream is = socket.getInputStream();
//把字节输入流包装成一个缓存字符输入流            
BufferedReader br = new BufferedReader(new InputStreamReader(is));           
String msg;
//如果没有数据可读,代码会停在这里等待数据,线程阻塞
while ((msg = br.readLine()) != null){                
    System.out.println("服务端接收到:" + msg);                                    
}

客户端:1.向服务端发送连接请求 2.向服务端发送数据

1.创建Socket对象,指定服务端的ip与端口

java 复制代码
Socket socket=new Socket("127.0.0.10",9999);

2.从Socket中获取输出流,写入数据

java 复制代码
OutputStream os = socket.getOutputStream(); 
while (true){
    System.out.println("请说:");
    String msg =sc.nextLine();
    ps.println(msg); 
    ps.flush();
}

服务端接收多个客户端

java 复制代码
InputStream is = socket.getInputStream();         
BufferedReader br = new BufferedReader(new InputStreamReader(is));           
String msg;
//如果没有数据可读,代码会停在这里等待数据,线程阻塞
while ((msg = br.readLine()) != null){                
    System.out.println("服务端接收到:" + msg);                                    
}

从这里可见,在BIO模式下,如果输入流中无数据,线程会停留在readLine() 中等待数据形成阻塞。因此服务端需要为每个客户端连接分配一个线程,时刻接收客户端发来的数据。 实现如下:

java 复制代码
ServerSocket serverSocket = new ServerSocket(9999);
while (true) {
    //时刻监听有无新客户端接入
    Socket socket = serverSocket.accept();
    System.out.println(">>建立新的连接>>");
    //为接入的客户端连接分配一个线程,负责处理该客户端发来的数据
    new ServerSocketThread(socket).start();
}
java 复制代码
public class ServerSocketThread extends Thread {
    private Socket socket;    
    public ServerSocketThread(Socket socket){
        this.socket = socket;   
     }    
     @Override    
     public void run() {
         try {
             //从socket对象中得到一个字节输入流 
             InputStream is = socket.getInputStream(); 
             //使用缓存字符输入流包装字节输入流
             BufferedReader br = new BufferedReader(new InputStreamReader(is)); 
             String msg;
             while ((msg = br.readLine()) != null){ 
                 System.out.println(msg);
             }
         } catch (IOException e) { 
             e.printStackTrace();
            }  
        } 
    }

群聊的实现

需求:

1.用户可以获知当前在线的其他用户,有用户登录(连接服务端)/下线(断开服务器连接)时,在线用户列表会被刷新

2.用户可以群发消息给其他用户

思路分析:

  1. 消息转发:要实现群聊功能,我们可以采用端口转发的模式,即客户端发送消息给服务端,服务端将消息转发给其他已经连接的客户端。

  2. 实时显示在线用户: 客户端在与服务端建立连接后发送一条登录消息(包含用户名),服务端获取后将socket和用户名存入map集合(key为socket,value为用户名), 同时向所有用户发送一条更新在线用户的消息(信息包含所有现在用户的用户名)。同理,当有人下线时,服务端负责与该客户端连接的读操作会抛出异常,我们可以在抛出异常时向所有客户端发送一条更新在线用户的消息,并移除map集合中的socket。

  3. 客户端接收服务端数据: BIO模式下接收消息是阻塞的,因此我们要额外开一个线程监听服务器发来的消息。

  4. 通信格式: 上述需求中,服务器需要转发两种格式的消息。 在实现实时显示在线用户 时,服务端发送消息的格式为:所有用户名的拼接并用分隔符间隔 (如: 张三✿net✿李四✿net✿王五)。 服务器普通群聊消息的消息格式为 用户名+分隔符+消息 。为了知道当前消息是上述哪种格式,定义了以下常量

java 复制代码
public class ServerConstant {
  //登录消息
  public static final int LOGIN=1;
  //退出消息
  public static final int EXIT=2;
  //人数更新消息
  public static final int UPDATE=0;
  //群聊消息
  public static final int GROUP_CHAT=3;
  //分隔符
  public static String PPROTOCOL="✿net✿";
}

因为我们的数据传输分为了两部分,int部分用来标识这次传的消息的格式,而string部分是实际消息,我们用DataInputStream这个流来传递消息,会用到如下方法

java 复制代码
//从流中读取int部分的内容
dataInputStream.readInt();
//从流中读取string部分的内容
dataInputStream.readUTF()

//向流中写入int部分内容
dataOutputStream.writeInt(int model);
//向流中写入string部分内容
dataOutputStream.writeUTF(String msg);

服务端代码如下

java 复制代码
public class Server {
    //key:所有连接的socket  value:该socket对应客户端的用户名
    public static Map<Socket, String> onLineSockets = new HashMap<>();

    public static void main(String[] args) throws IOException {
        //注册端口
        ServerSocket serverSocket = new ServerSocket(9999);
        while (true) {
            Socket socket = serverSocket.accept();
            //分配新的线程处理这个socket
            new ServerSocketThread(socket).start();
        }

    }
java 复制代码
public class ServerSocketThread extends Thread {

    private Socket socket;

    public ServerSocketThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        DataInputStream dataInputStream=null;
        try {
            dataInputStream=new DataInputStream(socket.getInputStream());
            while (true) {
                //读消息的格式 对应上文的常量类
                int flag = dataInputStream.readInt();
                //如果是用户登录的消息,则更新在线集合
                if (flag == ServerConstant.LOGIN) {
                    String username = dataInputStream.readUTF();
                    Server.onLineSockets.put(socket, username);
                }
                //转发消息
                Server.broadcastMsg(flag, dataInputStream);
            }
        } catch (IOException e) {
             //如果报错,则说明这个连接被关闭(用户离线),从在线socket集合中取出该socket
             //并发送 在线用户更新消息
            Server.onLineSockets.remove(socket);
            try {
                //如果此时没人就不发送在线用户更新信息了
                if(Server.onLineSockets.keySet().size()>0) {
                    Server.broadcastMsg(ServerConstant.EXIT, dataInputStream);
                }
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
        }

    }
}
java 复制代码
/**
 * !该方法是在Server类下定义的静态方法
 * 先处理消息字符串,再群发
 */
public static void broadcastMsg(int flag, DataInputStream dataInputStream) throws IOException {
    //广播消息,让所有客户端去更新在线用户集合
    if (flag == ServerConstant.LOGIN || flag == ServerConstant.EXIT) {
        //构造包含在线用户的消息,用协议分隔符分割
        StringBuilder onLineUser=new StringBuilder();
        for (String username : onLineSockets.values()) {
            onLineUser.append(username+ServerConstant.PPROTOCOL);
        }
        String msg = onLineUser.substring(0, onLineUser.lastIndexOf(ServerConstant.PPROTOCOL));
        //发送
        sendMsgToAll(ServerConstant.UPDATE,msg);
        //如果是群聊消息
    }else if(flag==ServerConstant.GROUP_CHAT){
        String msg= dataInputStream.readUTF();
        sendMsgToAll(ServerConstant.GROUP_CHAT,msg);
    }
}

/**
 * 群发消息
 */
private static void sendMsgToAll(int flag,String msg) throws IOException {
    for (Socket socket : Server.onLineSockets.keySet()) {
        DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
        dataOutputStream.writeInt(flag);
        dataOutputStream.writeUTF(msg);
        dataOutputStream.flush();
    }
}

客户端代码如下

java 复制代码
public class Client {
    
    public static void main(String[] args) {
        try {
            String username="hokkine";
            Socket socket=new Socket("127.0.0.10",9999);
            DataOutputStream dataOutputStream=new DataOutputStream(socket.getOutputStream());
            //发送登录消息
            dataOutputStream.writeInt(ServerConstant.LOGIN);
            dataOutputStream.writeUTF(username);
            dataOutputStream.flush();
            //开启监听线程,接收客户端发送来的数据
            new ClientSocketThread(socket).start();
            
            while (true){
                //获取键盘输入写入,发送给服务器
                Scanner scanner=new Scanner(System.in);
                dataOutputStream.writeInt(ServerConstant.GROUP_CHAT);
                dataOutputStream.writeUTF(username+ServerConstant.PPROTOCOL+scanner.nextLine());
                dataOutputStream.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

}
java 复制代码
public class ClientSocketThread extends Thread{

    private Socket socket;

    public ClientSocketThread(Socket socket) {
        this.socket=socket;
    }

    @Override
    public void run() {
        try {
            DataInputStream dataInputStream=new DataInputStream(socket.getInputStream());
            while (true) {
                int flag = dataInputStream.readInt();
                if (flag == ServerConstant.UPDATE) {
                    System.out.println("在线用户:" + dataInputStream.readUTF());
                }else if(flag==ServerConstant.GROUP_CHAT){
                    String[] split = dataInputStream.readUTF().split(ServerConstant.PPROTOCOL);
                    String sender = split[0];
                    String msg = split[1];
                    System.out.println(sender+" 说:" + msg);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }
}

在Minecraft中内置聊天室的实现

效果图

思路分析

在Minecraft中实现上述功能,首先要配置一个指令,输入该指令后就将指令前缀后的内容发送给远程服务器。同时,在接收到服务器发来的消息时,将该消息渲染到消息栏上。

实现

1.配置自定义指令

在EntityPlayerSP类下找到如下方法,在这里配置我们的自定义命令管理器

java 复制代码
/**
 * Sends a chat message from the player.
 * 用户每次在消息栏输入消息都会调用该方法,参数为输入内容
 */
public void sendChatMessage(String message)
{
    //判断输入内容是否符合我们自定义指令的前缀 如.irc .bind 
    //如果符合,就调用对应的方法;如果不符合,则执行原版方法
    if(!Client.commandManger.run(message)) {
        this.connection.sendPacket(new CPacketChatMessage(message));
    }
}

2.输入指令后,调用方法对应,发送消息到远程服务器

java 复制代码
/**
 * 输入.irc 或者.i 时,run方法被调用
 */
public class RemoteChatCommand extends Command {
    public RemoteChatCommand() {
        super(new String[]{"irc","i"});
    }

    @Override
    public void run(String msg) {
        String username=Minecraft.getMinecraft().player.getName();
        //拼接格式 用户名+分隔符+消息
        RemoteChat.chat(username+ServerConstant.PPROTOCOL+msg);
    }
}
java 复制代码
public class RemoteChat extends Mod {
    private static Socket socket;
    private static final String username="hokkine";
    //初始化,连接远程服务器,向服务器发送登陆消息
    static {
        try {
            RemoteChat.socket =new Socket("127.0.0.1",9999);;
            DataOutputStream dataOutputStream=new DataOutputStream(socket.getOutputStream());
            //发送登录消息
            dataOutputStream.writeInt(ServerConstant.LOGIN);
            dataOutputStream.writeUTF(username);
            dataOutputStream.flush();
            //启动监听线程
            new ClientSocketThread(socket).start();
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }
    public RemoteChat() {
        super("RemoteChat", false);
    }
    //发送消息
    public static void chat(String msg)  {
        try {
            DataOutputStream dataOutputStream=new DataOutputStream(socket.getOutputStream());
            dataOutputStream.writeInt(ServerConstant.GROUP_CHAT);
            dataOutputStream.writeUTF(msg);
            dataOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

3.接收到服务器发送的消息时,调用渲染方法将消息渲染到消息栏

java 复制代码
//通过minecraft实例获取到GuiIngame对象
GuiIngame gameGUI=Minecraft.getMinecraft().ingameGUI;
//调用GuiIngame的getChatGUI().printChatMessage(newTextComponentString(  ))方法即可
//?处传入要渲染的内容,字符串格式
gameGUI.getChatGUI().printChatMessage(newTextComponentString( ? ));
java 复制代码
//监听线程
public class ClientSocketThread extends Thread{
    private Socket socket;
    public ClientSocketThread(Socket socket) {
        this.socket=socket;
    }
    @Override
    public void run() {
        try {
            DataInputStream dataInputStream=new DataInputStream(socket.getInputStream());
            while (true) {
                int flag = dataInputStream.readInt();
                //用户更新消息(有人登录/离线了)
                if (flag == ServerConstant.UPDATE) {
                    //这里对上个代码块的内容封装成了工具类,也可以直接调用上个代码块的代码
                    MessageUtil.message("在线用户:" + dataInputStream.readUTF()); 
                //普通群聊消息
                }else if(flag==ServerConstant.GROUP_CHAT){
                    String[] split = dataInputStream.readUTF().split(ServerConstant.PPROTOCOL);
                    String sender = split[0];
                    String msg = split[1];
                    MessageUtil.message(sender+" 说:" + msg);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }
}
相关推荐
Chrikk17 分钟前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*20 分钟前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue21 分钟前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man23 分钟前
【go从零单排】go语言中的指针
开发语言·后端·golang
customer082 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml43 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
小码编匠4 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#
AskHarries4 小时前
Java字节码增强库ByteBuddy
java·后端