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