目录
Java BIO基本介绍
Java BIO工作机制
传统的BIO编程实例回顾
1、BIO模式下发送和接收消息
2、BIO模式下多发和多收消息
3、BIO模式下接收多个客户端
伪异步I/O编程
基于BIO形式下的文件上传
Java BIO模式下的端口转发思想
Java BIO基本介绍
- Java BIO就是传统的java io 编程,其相关的类和接口在java.io
- BlO(blocking I/O):同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器).
Java BIO工作机制
传统的BIO编程实例回顾
网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信(绑定IP地址和端口),客户端通过连接操作向服务端监听的端口地址发起连接请求,基于TCP协议下进行三次握手连接,连接成功后,双方通过网络套接字(Socket)进行通信。
传统的同步阻塞模型开发中,服务端ServerSocket负责绑定IP地址,启动监听端口;客户端Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。
基于BIO模式下的通信,客户端-服务端是完全同步,完全耦合的。
1、BIO模式下发送和接收消息
服务端
/**
* 服务端接受消息
*/
public class Server {
public static void main(String[] args) {
try {
System.out.println("——服务端启动——");
//定义一个ServerSocket对象进行服务端的端口注册
ServerSocket ss = new ServerSocket(9999);
//监听客户端的socket链接请求
Socket socket = ss.accept();
//从socket管道中得到一个字节输入流对象
InputStream is = socket.getInputStream();
//把字节输入流包装成一个缓冲字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
if ((msg = br.readLine()) != null){
System.out.println("服务端接收到:"+msg);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
客户端
/**
* 客户端发送消息
*/
public class Client {
public static void main(String[] args) {
try {
//创建socket对象请求服务端的链接
Socket socket = new Socket("127.0.0.1",9999);
//从socket对象中获取一个字节输出流
OutputStream os = socket.getOutputStream();
//把字节输出流包装成一个打印流
PrintStream ps = new PrintStream(os);
ps.println("hello world!");
ps.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
2、BIO模式下多发和多收消息
服务端
/**
* 服务端可以反复的接收消息,
*/
public class Server {
public static void main(String[] args) {
try {
System.out.println("——服务端启动——");
//定义一个ServerSocket对象进行服务端的端口注册
ServerSocket ss = new ServerSocket(9999);
//监听客户端的socket链接请求
Socket socket = ss.accept();
//从socket管道中得到一个字节输入流对象
InputStream is = socket.getInputStream();
//把字节输入流包装成一个缓冲字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
while ((msg = br.readLine()) != null){
System.out.println("服务端接收到:"+msg);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
客户端
/**
* 客户端可以反复的发送消息。
*/
public class Client {
public static void main(String[] args) {
try {
//创建socket对象请求服务端的链接
Socket socket = new Socket("127.0.0.1",9999);
//从socket对象中获取一个字节输出流
OutputStream os = socket.getOutputStream();
//把字节输出流包装成一个打印流
PrintStream ps = new PrintStream(os);
//键盘输入
Scanner sc = new Scanner(System.in);
while (true){
System.out.print("请输入:");
String msg = sc.nextLine();
ps.println(msg);
ps.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
3、BIO模式下接收多个客户端
打开idea点击运行
点击拷贝,然后点击apply和ok,然后点击运行
客户端分别输入信息发现只能接受第一个客户端的信息,因为服务端只有一个线程只能处理一个客户端的消息
服务端
/**
* 目标:实现服务端可以同时接收多个客户端的Socket通信需求。
* 思路:是服务端每接收到一个客户端socket请求对象之后都交给一个独立的线程
*/
public class Server {
public static void main(String[] args) {
ServerSocket ss = null;
try {
System.out.println("——服务端启动——");
//定义一个ServerSocket对象进行服务端的端口注册
ss = new ServerSocket(9999);
//监听客户端的socket链接请求
while (true){
Socket socket = ss.accept();
//创建一个独立的线程来处理与这个客户端的socket
new ServerThreadReader(socket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端使用多线程处理多个客户端
public class ServerThreadReader extends Thread{
private Socket socket;
public ServerThreadReader(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
InputStream is = null;
try {
//从socket管道中得到一个字节输入流对象
is = socket.getInputStream();
//把字节输入流包装成一个缓冲字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
if ((msg = br.readLine()) != null){
System.out.println("服务端接收到:"+msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端
/**
* 客户端
*/
public class Client {
public static void main(String[] args) {
try {
//创建socket对象请求服务端的链接
Socket socket = new Socket("127.0.0.1",9999);
//从socket对象中获取一个字节输出流
OutputStream os = socket.getOutputStream();
//把字节输出流包装成一个打印流
PrintStream ps = new PrintStream(os);
//键盘输入
Scanner sc = new Scanner(System.in);
while (true){
System.out.print("请输入:");
String msg = sc.nextLine();
ps.println(msg);
ps.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
结果显示服务端可以接受多个客户端发出的消息
小结
1.每个Socket接收到,都会创建一个线程,线程的竞争、切换上下文影响性能;
2.每个线程都会占用栈空间和CPU资源;
3.并不是每个socket都进行lO操作,无意义的线程处理;
4.客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
伪异步I/O编程
伪异步I/O的通信框架,采用线程池和任务队列实现,当客户端接入时将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable线程任务接口)交给后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
服务端
/**
* 目标:开发实现伪异步通信架构
*/
public class Server {
public static void main(String[] args) {
try {
//定义一个ServerSocket对象进行服务端的端口注册
ServerSocket ss = new ServerSocket(9999);
//初始化一个线程池
HandlerSocketServerPool pool = new HandlerSocketServerPool(6,10);
//监听客户端的socket链接请求
while (true){
//2、定义一个循环接收客户端的Socket链接请求
Socket socket = ss.accept();
//3、把socket对象交给一个线程池进行处理
//把socket封装成一个任务对象交给线程池处理
Runnable target = new ServerRunnableTarget(socket);
pool.execute(target);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
创建一个线程池
/**
* 创建一个线程池来处理消息
*/
public class HandlerSocketServerPool {
//1、创建一个线程池的成员变量用于存储一个线程池对象
private ExecutorService executorService;
//2、创建这个类的对象的时候就需要初始化线程池对象
public HandlerSocketServerPool(int maxThread , int queueSize){
executorService = new ThreadPoolExecutor(3,maxThread,120,
TimeUnit.SECONDS ,new ArrayBlockingQueue<Runnable>(queueSize));
}
//3、提供一个方法来提交任务给线程池的任务队列来暂存,等着线程池来处理
public void execute(Runnable target){
executorService.execute(target);
}
}
/**
* 创建多线程处理消息
*/
public class ServerRunnableTarget implements Runnable{
private Socket socket;
public ServerRunnableTarget(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 (Exception e){
e.printStackTrace();
}
}
}
客户端
/**
* 客户端可以反复的发送消息。
*/
public class Client {
public static void main(String[] args) {
try {
//创建socket对象请求服务端的链接
Socket socket = new Socket("127.0.0.1",9999);
//从socket对象中获取一个字节输出流
OutputStream os = socket.getOutputStream();
//把字节输出流包装成一个打印流
PrintStream ps = new PrintStream(os);
//键盘输入
Scanner sc = new Scanner(System.in);
while (true){
System.out.print("请输入:");
String msg = sc.nextLine();
ps.println(msg);
ps.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
小结
伪异步io采用了线程池实现,因此避免了为每个请求创建一个独立线程造成线程资源耗尽的问题,但由于底层依然是采用的同步阻塞模型,因此无法从根本上解决问题。
如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续socket的i/o消息都将在队列中排队。新的Socket请求将被拒绝,客户端会发生大量连接超时。
基于BIO形式下的文件上传
服务端
/**
* 目标:服务端开发,可以实现接收客户端的任意类型文件,并保存到服务端磁盘
*/
public class Server {
public static void main(String[] args) {
try {
ServerSocket ss = new ServerSocket(8888);
while (true){
Socket socket = ss.accept();
//交给一个独立的线程来处理与这个客户端的文件通信需求。
new ServerReaderThread(socket).start();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
OutputStream os = null;
try {
//1、得到一个数据输入流读取客户端发送过来的数据
DataInputStream dis = new DataInputStream(socket.getInputStream());
//2、读取客户端发送过来的文件类型
String suffix = dis.readUTF();
System.out.println("文件类型位:"+suffix);
//3、定义一个字节输出管道负责把客户端发来的文件数据写出去
os = new FileOutputStream("E:\\photo\\server\\"+ UUID.randomUUID().toString()+suffix);
//4、从数据输入流中读取文件数据,写出到字节输出流中去
byte[] bytes = new byte[1024];
int len;
while ((len = dis.read(bytes)) != -1){
os.write(bytes,0,len);
}
System.out.println("服务器端接受文件成功!");
}catch (Exception e){
e.printStackTrace();
}finally {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客户端
/**
* 目标:实现客户端上传任意类型的文件数据给服务端保存起来。
*/
public class Client {
public static void main(String[] args) {
try {
//1、请求与服务端的Socket链接
Socket socket = new Socket("127.0.0.1",8888);
//2、把字节输出流包装成一个数据输出流
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
//3、法发送上传文件的后缀给服务端
dos.writeUTF(".png");
//4、把文f件数据发送给服务嵩进行接收
InputStream is = new FileInputStream("E:\\photo\\figure\\1.png");
byte [] bytes = new byte[1024];
int len;
while ((len = is.read(bytes)) != -1){
dos.write(bytes,0,len);
}
dos.flush();
//通知服务端这边的数据发送完毕
socket.shutdownOutput();
}catch (Exception e){
e.printStackTrace();
}
}
}
Java BIO模式下的端口转发思想
客户端
/**
*目标:BIO模式下的端口转发思想-服务端实现。
* 服务端实现的需求:
* 1、注册端口
* 2、接收客户端的socket连接,交给一个独立的线程来处理。
* 3、把当前连接的客户端socket存入到一个所谓的在线socket集合中保存
* 4、接收客户端的消息,然后推送给当前所有在线的socket接收。
*/
public class Server {
//定义一个静态集合
public static List<Socket> allSocketOnLine = new ArrayList<>();
public static void main(String[] args) {
try {
//
ServerSocket ss = new ServerSocket(9999);
while (true){
Socket socket = ss.accept();
//把登录的客户端socket存入到一个在线集合中去
allSocketOnLine.add(socket);
//为当前登录成功的socket分配一个独立的线程来处理与之通信
new ServerReaderThread(socket).start();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//1、从socket中去获取当前客户端的输入流
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg;
while ((msg = br.readLine()) != null){
//2、服务端接收到了客户端的消息之后,是需要推送给当前所有的在线socket
sendMsgToAllClient(msg);
}
}catch (Exception e){
System.out.println("当前有人下线了");
//从在线socket集合中移除本socket
Server.allSocketOnLine.remove(socket);
}
}
/**
* 把当前客户端发来的消息推送给全部在线的socket
* @param msg
*/
private void sendMsgToAllClient(String msg){
for (Socket sk : Server.allSocketOnLine) {
PrintStream ps = null;
try {
ps = new PrintStream(sk.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
ps.println(msg);
ps.flush();
}
}
}