IO简介
一、前言
在java软件设计开发中,通信框架是不可避免的,我们在不同的系统或者这不同的进程之间进行数据交互,或者在高并发的场景下需要用到网络通信相关的技术,从上节课的例子当中我们看出同步阻塞式的IO通信(BIO)效率过于低下。于是Java在2002年开始支持同步非阻塞式的IO通信技术(NIO),那就让我们系统的学习一下java的IO吧。
二、javaIO发展之路
1.IO基本模型
IO模型:就是用什么样的通道或者说是通讯模式和架构进行数据的传输和接收,很大程度上决定了程序通讯的性能,java共支持3种网络的IO模型:BIO NIO AIO
实际通讯需求下,根据不同的业务场景和性能决定选择不同的I/O模型
2.I/O模型
Java BIO (同步阻塞型IO)
BIO属于同步阻塞型IO,在服务器端的实现模式为,一个连接对应一个线程。当客户端有连接请求的时候服务端需要启动一个新的线程与之进行对应处理。
这个模式的缺点很明显,当我们的连接请求发送到服务器端的时候服务器就会新启动一个线程来进行处理,当新建的线程不做任何处理只是挂着(阻塞)的时候,就会给服务器造成不必要的线程开销。
举个栗子:我们用BIO开发了一个即时聊天系统,每一个客户端给我们的服务器端发送消息之前都要和我们的服务器端进行连接。但是发送完消息之后我们的客户端却不下线(不主动关闭),服务器端的线程就要一直阻塞的等待客户端给他发消息,(将线程挂起来)。这就会给服务器造成不必要的线程开销。这还不是最可怕的,当我们的客户端越来越多的情况下,每一个客户端的接入服务器都会建立一个新的线程与之对应。当客户端的连接数增多,产生高并发的时候,整个BIO网络就会占用大量的jvm线程,造成服务器性能降低,最后可能导致服务器宕机。
Java NIO (同步非阻塞型IO)
服务器的实现模式为一个线程,处理多个请求(连接),既客户端发送的请求连接,客户端发送的请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。
这样做显然有一个缺点,就比如有一个通道上有大量的数据,那么这个唯一的线程就需要用大量的时间去处理这个通道上的数据,从而降低通道通道的利用率,造成系统资源的浪费
BIO、NIO适用场景分析
1.BIO 方式适用于连接数目较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,程序简单易理解。
2.NIO 方式适用于链接数目多且连接比较短(轻操作)的框架,比如聊天系统,单面系统,服务器间通讯等。编程比较复杂,jdk1.4开始支持。
什么是socket
一、计算机网络七层协议,以及每层的作用
二、什么是Socket
从上图当中我们可以看出应用层、表示层、会话层都属于我们的应程序,而后边四层都属于操作系统。
那么由我们程序员开发的的应用程序应该如何调用操作系统当中的后四层协议呢?
socket 其实就是操作系统提供给程序员操作「网络协议栈」的接口,说人话就是,你能通过socket 的接口,来控制协议找工作,从而实现网络通信,达到跨主机通信。
从上图可以看出Socket是网络上运行的程序之间双向通信链路的终结点,是TCP和UDP的基础,。使用java的net包当中的Socket API可以实现基于TCP或UDP的socket服务器端和客户端。
三、Socket TCP实现服务端和客户端的通信
服务器端代码
public class Server {
public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket(4700);
System.out.println("Server started...");
// while(true) 持续接收请求
while(true) {
Socket socket = server.accept();
System.out.println("有客户端连接了..." + socket.getRemoteSocketAddress());
// 给每个连接都启动一个线程,实现并发处理
new Thread(() -> {
try {
handler(socket);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
private static void handler(Socket socket) throws Exception {
System.out.println("-->" + Thread.currentThread().getName());
// 读
InputStream inputStream = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String str = reader.readLine();
System.out.println(socket.getRemoteSocketAddress() + ":" + str);
TimeUnit.SECONDS.sleep(10); // 通过sleep来验证可以通过处理多个连接
// 写
OutputStream outputStream = socket.getOutputStream();
PrintWriter writer = new PrintWriter(outputStream);
writer.println("ACK");
writer.flush();
System.out.println("已向" + socket.getRemoteSocketAddress() + "回发确认消息ACK");
}
}
客户端代码
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("101.43.152.120", 4700);
System.out.println("Connect success...");
Scanner scanner = new Scanner(System.in);
String input="";
input=scanner.next();
// 写
OutputStream outputStream = socket.getOutputStream();
PrintWriter writer = new PrintWriter(outputStream);
writer.println(input);
writer.flush();
System.out.println("向服务端发送数据结束");
// 读
InputStream inputStream = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String str = reader.readLine();
System.out.println("Server:" + str);
socket.close();
}
}
在本地启动,首先启动服务器端代码,其次启动客户端代码
以上在服务器端中可以直接使用Socket实例化一个socket对象实现tcp协议绑定端口并启动,然后通过阻塞方法accept接收并读取客户端发来的信息,而客户端则通过带套接字信息的Socket类连接服务器端,双方建立连接以后即可通过流进行双向通信。
四、Socket UDP实现服务端和客户端的通信
服务器端代码
public class UDPSocketServer {
public static void main(String[] args) {
recive();
}
public static void recive(){
System.out.println("服务器端启动");
try{
//创建接收方的套接字对象
DatagramSocket socket = new DatagramSocket(4701);
//接收数据的buf数组并指定大小
byte[] buf = new byte[1024];
//创建接收数据包,并存储在buf中
DatagramPacket packet = new DatagramPacket(buf,buf.length);
//接收操作,代码会停顿在这里,直到接收到数据包
socket.receive(packet);
//接收的数据
byte[] data = packet.getData();
//接收的地址
InetAddress address = packet.getAddress();
System.out.println("接收的文本=====》"+new String(data));
System.out.println("接收的ip地址====》"+ address.toString());
System.out.println("接收的端口=====》" + packet.getPort());
//告诉发送者数据接收完毕
String temp = "数据接收完毕";
byte buffer[] = temp.getBytes();
//创建数据报,指定发送者的地址socketAddress地址
DatagramPacket packet2 = new DatagramPacket(buffer,buffer.length,packet.getSocketAddress());
//发送
socket.send(packet2);
//关闭
socket.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
服务器端
public class UDPSocketClient {
public static void main(String[] args) {
send();
}
public static void send(){
System.out.println("发送端接收数据");
//发送端
try {
//创建发送段的socket对象
DatagramSocket socket = new DatagramSocket(4700);
//发送的内容
String text = "hello from sender";
byte[] buf = text.getBytes(StandardCharsets.UTF_8);
//创建数据包,将长度为length的数据包发送到指定的端口号
DatagramPacket packet = new DatagramPacket(buf,buf.length, InetAddress.getByName("localhost"),4701);
//发送数据
socket.send(packet);
//接收者返回的数据
displayReciveInfo(socket);
//关闭此数据报的套接字
socket.close();
}catch (Exception e){
e.printStackTrace();
}
}
public static void displayReciveInfo(DatagramSocket socket) throws Exception {
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer,buffer.length);
socket.receive(packet);
byte[] data = packet.getData(); //接收的数据
InetAddress address = packet.getAddress();//接收的地址
System.out.println("接收的文本=====》" + new String(data));
System.out.println("接收的ip地址=====》" + address.toString());
System.out.println("接收的端口====》" + packet.getPort());
}
}
服务端使用了DatagramSocket类创建接收端套接字,客户端使用DatagramSocket创建发送端套接字。因为UDP不是面向连接的,所以发送端创建套接字后就可以发送数据包,数据包通过DatagramPacket进行封装,在调用DatagramSocket的send方法来发送数据包
五、将客户端代码配置到服务器上进行通信
1.将servlet.java文件放入到服务器当中
2.将服务器的4700端口打开
3.更换客户端访问服务器端的ip和端口
4.启动客户端,进行通信
六、发现的问题
当客户端连接到服务器端以后,服务器端就会创建一个新的线程来处理客户端的请求,但是客户端迟迟不发生请求,这也就导致服务器端处理请求的线程被挂起,如果存在大量的这样的客户端,那么服务器端迟早会崩溃。
java BIO
一、BIO的工作原理
传统Io(BIO)的本质就是面向字节流来进行数据传输的
①:当两个进程之间进行相互通信,我们需要建立一个用于传输数据的管道(输入流、输出流),原来我们传输数据面对的直接就是管道里面一个个字节数据的流动(我们弄了一个 byte 数组,来回进行数据传递),所以说原来的 IO 它面对的就是管道里面的一个数据流动,所以我们说原来的 IO 是面向流的
②:我们说传统的 IO 还有一个特点就是,它是单向的。解释一下就是:如果说我们想把目标地点的数据读取到程序中来,我们需要建立一个管道,这个管道我们称为输入流。相应的,如果如果我们程序中有数据想要写到目标地点去,我们也得再建立一个管道,这个管道我们称为输出流。所以我们说传统的 IO 流是单向的
二、传统的BIO编程实例回顾
网络编程的基本模型是C/S(客户端/服务器端)模型,也就是两个进程之间的通讯,其中服务端提供位置信(绑定ip地址和端口),客户端通过连接操作向服务器端监听的端口地址发起连接请求,基于TCP协议下进行三次握手连接,连接成功后,双方进行socket通讯。
传统的同步阻塞模型开发中,服务端ServerSocket负责绑定ip地址,启动监听端口;客户端Socket负责发起连接操作。连接成功之后,双方通过输入和输出流进行同步阻塞式通信。
基于BIO模式下的通讯,客户端-服务器端是完全同步,完全耦合的。
服务器端代码
public class Server {
public static void main(String[] args) {
try {
System.out.append("服务器端启动。。。");
//1.定义ServerSocket对象进行服务端的端口注册
ServerSocket serverSocket = new ServerSocket(8080);
//2.监听客户端的Socket连接程序
Socket socket = serverSocket.accept();
//3.从socket对象当中获取到一个字节输入流对象
InputStream iStream = socket.getInputStream();
//打印输出
int len = 0;
int ReviceLen = 0;
//计算机网络数据是以8bit为一个单元进行发送,我们接收到发送方发送的byte数据
//将其转化为utf-8的格式进行输出
byte[] recvBuf = new byte[1024];
while ((ReviceLen = iStream.read(recvBuf)) != -1) {
System.out.println(" 客户端说:"
+ new String(recvBuf, 0, ReviceLen, "UTF-8"));
}
} catch (Exception e) {
}
}
}
客户端代码
public class Client {
public static void main(String[] args) throws Exception {
//1.创建socket对象请求服务器的连接
Socket socket = new Socket("127.0.0.1",8080);
//2.从socket对象中获取一个字节输出流、
OutputStream oStream = socket.getOutputStream();
oStream.write(("你好服务器").getBytes());//以字节流的形式发送数据
//4.关闭
oStream.flush();
}
}
三、BIO模式下的多发和多收消息
服务器端不变,客户端:
public class Client {
public static void main(String[] args) throws Exception {
//1.创建socket对象请求服务器的连接
Socket socket = new Socket("127.0.0.1",8080);
//2.从socket对象中获取一个字节输出流、
OutputStream oStream = socket.getOutputStream();
Scanner scanner = new Scanner(System.in);
while (true){
System.out.println("请说....");
String message = scanner.nextLine();
oStream.write(message.getBytes());
//4.关闭
oStream.flush();
}
}
}
四、BIO模式下接收多个客户端
在上述的案例当中,一个服务端只能接收一个客户端的通信请求,那么如果服务端需要处理很多个客户端的消息通讯请求应该如何处理呢?
当我们启动两个客户端,分别去访问服务器端的时候,我们发现服务器端只连接了一个客户端,并且只能和一个客户端进行通信。
什么原因导致了我们服务器只能链接一个客户端
那如何解决呢?
此时就需要在服务端引入线程了,也就是说客户端发起一次请求,服务端就会创建一个新的线程来处理一个新的线程来处理这个客户端的请求,这样就实现了一个客户端一个线程的模型,图解如下:
服务器端改进代码
public class Server {
public static void main(String[] args) {
try {
System.out.append("服务器端启动。。。");
//1.定义ServerSocket对象进行服务端的端口注册
ServerSocket serverSocket = new ServerSocket(8080);
while (true){
//2.监听客户端的Socket连接程序
Socket socket = serverSocket.accept();
//创建一个独立的线程来处理也客户端的Socket请求
new ServerThreadReader(socket).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class ServerThreadReader extends Thread {
private Socket socket;
public ServerThreadReader(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//3.从socket对象当中获取到一个字节输入流对象
try {
InputStream iStream = socket.getInputStream();
//打印输出
int len = 0;
int ReviceLen = 0;
//计算机网络数据是以8bit为一个单元进行发送
byte[] recvBuf = new byte[1024];
while ((ReviceLen = iStream.read(recvBuf)) != -1) {
System.out.println(" 客户端说:"
+ new String(recvBuf, 0, ReviceLen, "UTF-8"));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
小结
1.每个socket接受到,都会床架一个新的线程,线程的竞争、切换上下文影响性能。
2.每个线程都会占用栈空间和cpu资源
3.并不是每一个socket都进行IO操作,无意义的线程处理
4.客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程
栈溢出,线程创建失败,最终导致进程宕机或僵死,从而不能对外提供服务