BIO
bio也叫同步阻塞IO,位于java.io包下,也就是面向数据流的IO(Stream)
它的整体架构如下图所示
可以从上图看出除去文件系统,剩下的就是BIO,可以按照读写单元将它们同意分为两类
- 面向字节流
- InputStream
- FileInputSteam -> BufferedInputSteam (文件操作相关)
- ObjectInputStream等等 (数据对象操作相关)
- OutputStream
- FileOutputSteam -> BufferedOutputSteam(文件操作相关)
- ObjectOutputStream等等(数据对象操作相关)
- InputStream
- 面向字符流
-
Reader
- InputStreamReader (提供InputStream转为Reader,可设置字符集)
- FileReader是InputStreamReader的子类,此类方法全是调用的InputStreamReader的方法(传入一个FileName或File对象,底层使用InputSteamReader构造方法初始化一个FileInputStream)
- BufferedReader,CharArrayReader,PipedReader等等 (直接操作字符无转换)
- InputStreamReader (提供InputStream转为Reader,可设置字符集)
-
Writer
- OutputStreamWriter(提供OutputStream转为Writer,可设置字符集)
- FileWriter 可以根据传入的FileName或File对象,调用父类构造方法构造出FileOutputStream使用父类方法
- BufferedWriter,CharArrayWriter,PipedWriter等等 (直接操作字符无转换)
- OutputStreamWriter(提供OutputStream转为Writer,可设置字符集)
-
在BIO中使用了大量的装饰者模式,因此代码的可读性还是比较高的。
下面就来结合BIO进行网络编程,编写一个TCP服务器和一个UDP服务器
网络编程
Socket简介
在网络编程中Socket是十分重要的,无论是发送还是接受消息都难免要经过Socket,俗称套接字
,用于描述IP地址和端口号,它大致可以分为两种类型,一种是TCP Socket包含ServerSocket
另外一种是UDP Socket包含DatagramSocket
。
- ServerSocket是工作于TCP服务端的Socket,主要用于接受客户端Socket发来的请求并获取客户端Socket进行处理。其中的backlog可以指定连接请求队列的长度(一般为50,可以自己设定),binAdrr可以指定服务器要绑定的IP地址
- DatagramSocket是UDP服务端的Socket,主要接受客户端发来的数据报
- Socket是工作于客户端上的一个Socket,通过规定服务器IP和端口号保证数据的流向。
Socket的工作流程
发送数据
- 得到一个Socket对象
- 将socket与网络驱动层进行绑定
- 写数据到socket中
- 网络驱动层取出socket数据,从网卡发过去
接收数据
- 得到一个Socket实例
- 网卡接受到数据,网络驱动层根据绑定关系将数据放入特定的Socket
- 从Socket中读取出数据
Socket的几个额外配置参数
这几个选择都在SocketOption这个接口里面
public interface SocketOptions {
// 子类实现设置可选项的方法
public void setOption(int optID, Object value) throws SocketException;
// 默认情况采用Neagle算法,具体开启还是关闭看自身的需求(True or False)
@Native public final static int TCP_NODELAY = 0x0001;
// 在一个进程关闭socket后(没有进行释放端口)统一主机上的其他进程可对端口进行重用(可以在重用前设置为true才会生效)
@Native public final static int SO_REUSEADDR = 0x04;
// 用来控制广播(只有数据报中支持)
@Native public final static int SO_BROADCAST = 0x0020;
// 用来控制多播参数选项
@Native public final static int IP_MULTICAST_IF = 0x10;
// 和上面一样,支持IPv6
@Native public final static int IP_MULTICAST_IF2 = 0x1f;
// 设置本地回环(Ip 多播)
@Native public final static int IP_MULTICAST_LOOP = 0x12;
//TOS就是Type Of Service(服务质量)
@Native public final static int IP_TOS = 0x3;
// linger(缓慢消失),在默认情况下调用socket.close方法会立刻返回但是底层的Socket并不会立刻关闭
// 这个参数是设置调用close方法后不会立刻返回而是进入阻塞状态,在发送完成数据或是超过60s没发完才返回,然后关闭底层的socket(没发完的数据就丢弃)
@Native public final static int SO_LINGER = 0x0080;
// 用来设置读取数据超时,如果read阻塞的时间大于超时时间就会抛出异常SocketTimeoutException
@Native public final static int SO_TIMEOUT = 0x1006;
// Socket发送缓冲区的大小(可设置)
@Native public final static int SO_SNDBUF = 0x1001;
// Socket接收缓冲区的大小(可设置)
@Native public final static int SO_RCVBUF = 0x1002;
// 如果此选项为true,那就是在客户端和服务端的连接中加入心跳机制(True or False)
// 连接空闲两个小时,会对远程Socket发送Tcp数据报,若无响应就继续发一段时间,若一直无响应,就关闭连接
@Native public final static int SO_KEEPALIVE = 0x0008;
// Socket类的sendUrgentData方法向服务器发送一个单字节的数据,这个单字节数据不经过输出缓冲区,而是立即发出
// 客户端发送此字节虽然不使用Stream,但是在服务端还是和Stream发的数据混在一起
@Native public final static int SO_OOBINLINE = 0x1003;
}
注:在网络中如果有很多小的数据,每次发的话,它的包头部长度可能都超过它本身长度,为了优化这一问题就有了Neagle算法,发送数据时不会立即发送出去,而是先在缓冲区中停留,等待数据到达一定大小才一次性发送出去(相当于分批次发送),具体开启还是关闭看自身的需求。
TCP编程
下面以一个简单的Echo Server为例回顾Tcp网络编程
Server
package BIO.tcp;
/*
* @Author Wrial
* @Date Created in 22:45 2020/4/8
* @Description TCPServer
*/
import java.io.*;
import java.net.*;
public class TCPServer {
public static void main(String[] args) throws IOException {
TCPServer tcpServer = new TCPServer(8090);
tcpServer.service();
}
private ServerSocket serverSocket;
public TCPServer(int port) throws IOException {
init(port);
}
private void init(int port) throws IOException {
serverSocket = new ServerSocket(port);
System.out.println("server init ——> " + serverSocket.getInetAddress().getHostAddress() + "port :"+ port);
}
private String serverReceiveAndEcho(String msg) {
System.out.println("Receive :" + msg);
return "echo from server :" + msg;
}
private PrintWriter getWriter(Socket socket) throws IOException {
return new PrintWriter(socket.getOutputStream(),true);
}
private BufferedReader getReader(Socket socket) throws IOException {
return new BufferedReader(new InputStreamReader(socket.getInputStream()));
}
public void service() throws IOException {
Socket accept;
while (true) {
accept = serverSocket.accept();
System.out.println("client :" + accept.getInetAddress().getHostName() + "已经连接");
BufferedReader bufferedReader = getReader(accept);
PrintWriter printWriter = getWriter(accept);
String msg;
while ((msg = bufferedReader.readLine()) != null) {
printWriter.println(serverReceiveAndEcho(msg));
if (msg.equals("end")){
break;
}
}
try {
if (accept!=null) accept.close();
if (bufferedReader!=null) bufferedReader.close();
if (printWriter!=null) printWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Client
package BIO.tcp;
/*
* @Author Wrial
* @Date Created in 23:19 2020/4/8
* @Description
*/
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException {
new Client("localhost", 8090).sendMsg();
}
private String ip;
private int port;
Socket socket;
public Client(String ip, int port) throws IOException {
this.ip = ip;
this.port = port;
init();
}
private void init() throws IOException {
socket = new Socket(ip, port);
System.out.println("client init - ->" + port);
}
private PrintWriter getWriter(Socket socket) throws IOException {
// true是自动刷新
return new PrintWriter(socket.getOutputStream(),true);
}
private BufferedReader getReader(Socket socket) throws IOException {
return new BufferedReader(new InputStreamReader(socket.getInputStream()));
}
private void sendMsg() throws IOException {
BufferedReader bufferedReader = getReader(socket);
PrintWriter printWriter = getWriter(socket);
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String sendMsg;
while ((sendMsg = reader.readLine())!=null){
printWriter.println(sendMsg);
System.out.println(bufferedReader.readLine());
if (sendMsg.equals("end")){
System.out.println("end");
break;
}
}
if (socket!=null) socket.close();
}
}
当客户端输入end就会结束此次socket连接,双方都关闭socket
服务端
客户端
如果我们需要Server同时进行多个cllient就要适当的加入多线程,可以循环new Thread,也可以使用线程池来更好的管理线程。如下图所示 :
总的来说还是阻塞导致的执行多个任务需要多个线程来完成,无疑是加大了开销,降低了性能。
Java中引起线程阻塞的几种方式
- Thread.Sleep,可以让线程放弃CPU,睡眠一段时间,然后恢复
- I/O阻塞,如从控制台输入等等
- 执行的该对象的wait方法,只有notify/notifyAll才能唤醒
- 执行同步代码块的时候不能立即获取锁,就会处于阻塞状态
- Socket的带参构造方法或connect方法,直到连接成功才返回
- 当给Socket设置了Linger延时处理,在调用close会等待数据发完或者超时才会返回(期间是阻塞的)
- ServerSocket的accept方法等等
UDP编程
UDP编程和TCP编程不同,UDP是以Datagrampacket数据包发送的而不是根据IO流来完成的。其中比较核心的两个类为DatagramPacket
和DatagramSocket
,DatagramPacket对于发送方可以看做是缓冲区数据+目的IP+目的port,对于接受放可以看做是receive的一个容器,可以用来接收发送方发来的数据报
具体代码如下
接收端 UDPServer
package BIO.udp;
/*
* @Author Wrial
* @Date Created in 14:41 2020/4/9
* @Description
*/
import java.io.IOException;
import java.net.*;
public class UDPServer {
public static void main(String[] args) throws IOException {
UDPServer udpServer = new UDPServer(8090);
udpServer.service();
}
private DatagramSocket datagramSocket;
public UDPServer(int port) throws SocketException {
init(port);
}
private void init(int port) throws SocketException {
datagramSocket = new DatagramSocket(port);
System.out.println("server init ");
}
private void service() throws IOException {
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
while (true) {
datagramSocket.receive(packet);
System.out.println("receive from " + packet.getAddress());
String msg = new String(packet.getData());
if (msg == "") break;
System.out.println("msg == " + msg);
}
}
}
发送端:UDPClient
package BIO.udp;
/*
* @Author Wrial
* @Date Created in 22:18 2020/4/9
* @Description 发送端
*/
import java.io.IOException;
import java.net.*;
public class UDPClient {
public static void main(String[] args) throws IOException, InterruptedException {
UDPClient udpClient = new UDPClient();
for (int i = 0; i < 5; i++) {
DatagramPacket msg = udpClient.transferMsgToPacket("127.0.0.1", "hello server", 8090);
udpClient.sendMessage(msg);
Thread.sleep(100);
}
}
private DatagramSocket datagramSocket;
public UDPClient() throws SocketException {
datagramSocket = new DatagramSocket();
System.out.println("init datagramSocket");
}
public void sendMessage(DatagramPacket datagramPacket) throws IOException {
datagramSocket.send(datagramPacket);
System.out.println("Message send success ---" + new String(datagramPacket.getData()));
}
public DatagramPacket transferMsgToPacket(String addr, String msg, int port) throws UnknownHostException {
DatagramPacket packet = new DatagramPacket(msg.getBytes(), 0,
msg.length(), InetAddress.getByName(addr), port);
return packet;
}
}
演示如下:
这个就是大致对BIO和简单的网络编程的一个回顾,它用起来不难,但是它的缺陷也是显而易见的,由于线程的阻塞而导致资源利用不充分,因此就出现了NIO!