前言
阅读这篇文章前,您需要以下基础
-
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.用户可以群发消息给其他用户
思路分析:
-
消息转发:要实现群聊功能,我们可以采用端口转发的模式,即客户端发送消息给服务端,服务端将消息转发给其他已经连接的客户端。
-
实时显示在线用户: 客户端在与服务端建立连接后发送一条登录消息(包含用户名),服务端获取后将socket和用户名存入map集合(key为socket,value为用户名), 同时向所有用户发送一条更新在线用户的消息(信息包含所有现在用户的用户名)。同理,当有人下线时,服务端负责与该客户端连接的读操作会抛出异常,我们可以在抛出异常时向所有客户端发送一条更新在线用户的消息,并移除map集合中的socket。
-
客户端接收服务端数据: BIO模式下接收消息是阻塞的,因此我们要额外开一个线程监听服务器发来的消息。
-
通信格式: 上述需求中,服务器需要转发两种格式的消息。 在实现实时显示在线用户 时,服务端发送消息的格式为:所有用户名的拼接并用分隔符间隔 (如: 张三✿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);
}
}
}