目录
传统 BIO 编程
1、网络编程的基本模型时 Client/Server 模型,也就是两个进程之间相互通信其中服务端提供位置信息(绑定的 IP 地址与监听的端口),客户端通过连接操作向服务器监听地址发起连接骑请求,通过三次握手连接,如果连接建立成功,双方就可以通过网络套接字(Socket) 进行通信。
2、传统的同步阻塞模型(BIO)开发中,ServerSocket 负责绑定 IP 地址,启动监听端口,Socket 负责发起连接操作。连接成功之后,双方通过输入和输出流进行同步阻塞式通信。
BIO 通信模型的服务端通常由一个独立的 Accpetor(接受者)线程负责监听客户端的连接,它接收到客户端连接请求后为每个客户端创建一个新的线程进行单独处理,处理完成后,通过输出流返回给客户端,线程销毁,这就是典型的一请求一应答通信模型。
BIO 模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1正比关系,由于线程是 Java 虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急骤下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。
BIO 编 码:这里以一个例子进行说明,客户端往服务器发送数据,服务器回复数据。
·服务端·
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Created by Administrator on 2018/10/14 0014.
* 时间服务器
*/
public class TimeServer {
public static void main(String[] args) {
tcpAccept();
}
public static void tcpAccept() {
ServerSocket serverSocket = null;
try {
/**Tcp 服务器监听端口,ip 默认为本机地址*/
serverSocket = new ServerSocket(8080);
/**循环监听客户端的连接请求
* accept 方法会一直阻塞,直到 客户端连接成功,主线程才继续往后执行*/
Socket socket = null;
while (true) {
System.out.println("等待客户端连接..........");
socket = serverSocket.accept();
System.out.println("客户端连接成功..........");
/**
* 为每一个客户端连接都新开线程进行处理
*/
new Thread(new TimeServerHandler(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
/**发生意外时,关闭服务端*/
if (serverSocket != null && !serverSocket.isClosed()) {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
对每个客户端连接新开线程单独处理:
import java.io.*;
import java.net.Socket;
import java.util.Date;
/**
* Created by Administrator on 2018/10/14 0014.
* 为每个 TCP 客户端新开线程进行处理
*/
public class TimeServerHandler implements Runnable {
private Socket socket = null;
/**
* 将每个 TCP 连接的 Socket 通过构造器传入
*
* @param socket
*/
public TimeServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
DataInputStream dataInputStream = null;
DataOutputStream dataOutputStream = null;
try {
/**读客户端数据*/
InputStream inputStream = socket.getInputStream();
dataInputStream = new DataInputStream(inputStream);
String message = dataInputStream.readUTF();
System.out.println(Thread.currentThread().getName() + " 收到客户端消息:" + message);
/**往客户端写数据*/
OutputStream outputStream = socket.getOutputStream();
dataOutputStream = new DataOutputStream(outputStream);
dataOutputStream.writeUTF(new Date().toString());
dataOutputStream.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
/**操作完成,关闭流*/
if (dataOutputStream != null) {
try {
dataOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (dataInputStream != null) {
try {
dataInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**操作完成,关闭连接,线程自动销毁*/
if (socket != null && !socket.isClosed()) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
·客户端·
import java.io.*;
import java.net.Socket;
/**
* Created by Administrator on 2018/10/14 0014.
* 时间 客户端
*/
public class TtimeClient {
public static void main(String[] args) {
/**
* 3个线程模拟3个客户端
*/
for (int i = 0; i < 3; i++) {
new Thread() {
@Override
public void run() {
tcpSendMessage();
}
}.start();
}
}
/**
* Tcp 客户端连接服务器并发送消息
*/
public static void tcpSendMessage() {
Socket socket = null;
DataOutputStream dataOutputStream = null;
DataInputStream dataInputStream = null;
try {
/**
* Socket(String host, int port):
* host)被连接的服务器 IP 地址
* port)被连接的服务器监听的端口
* Socket(InetAddress address, int port)
* address)用于设置 ip 地址的对象
* 此时如果 TCP 服务器未开放,或者其它原因导致连接失败,则抛出异常:
* java.net.ConnectException: Connection refused: connect
*/
socket = new Socket("127.0.0.1", 8080);
System.out.println("连接成功.........." + Thread.currentThread().getName());
/**往服务端写数据*/
OutputStream outputStream = socket.getOutputStream();
dataOutputStream = new DataOutputStream(outputStream);
dataOutputStream.writeUTF("我是长城" + Thread.currentThread().getName());
dataOutputStream.flush();
/**读服务端数据*/
InputStream inputStream = socket.getInputStream();
dataInputStream = new DataInputStream(inputStream);
String message = dataInputStream.readUTF();
System.out.println("收到服务器消息:" + message);
} catch (IOException e) {
e.printStackTrace();
} finally {
/**关闭流,释放资源*/
if (dataOutputStream != null) {
try {
dataOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (dataInputStream != null) {
try {
dataInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/** 操作完毕关闭 socket*/
if (socket != null && !socket.isClosed()) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
服务端输出:
等待客户端连接..........
客户端连接成功..........
等待客户端连接..........
客户端连接成功..........
等待客户端连接..........
客户端连接成功..........
等待客户端连接..........
Thread-0 收到客户端消息:我是长城Thread-2
Thread-1 收到客户端消息:我是长城Thread-0
Thread-2 收到客户端消息:我是长城Thread-1客户端输出:
连接成功..........Thread-1
连接成功..........Thread-2
连接成功..........Thread-0
收到服务器消息:Sun Oct 14 18:38:53 CST 2018
收到服务器消息:Sun Oct 14 18:38:53 CST 2018
收到服务器消息:Sun Oct 14 18:38:53 CST 2018
总 结
BIO 阻塞式编程如上所示说明完毕,BIO 主要的问题在于每当有一个新的客户端请求连接时,服务器端必须新建线程进行处理,一个线程只能处理一个客户端连接。
在高性能服务器应用领域,往往需要面向成千上万个客户端的并发连接,这种模型显然无法满足需求。为了改进这种一个线程一个连接的模型,后来又演进出了一种通过线程池或者消息队列实现一个或者多个线程处理 N 个客户端的模型,由于它的底层通信机制仍然使用同步阻塞 I/O ,所以被称为 “伪异步”。