一、网络编程模型介绍
作为一个WEB程序员,网络编程模型是我们需要掌握的基础知识,在介绍具体模型之前先聊下几个基本的概念:
1.1 基本概念
阻塞和非阻塞
这里说的阻塞指调用者调用一个函数的时候,如果这个函数不能马上返回,则调用者会一直等函数返回才能往下执行其它代码;
而非阻塞则是反过来,如果函数不能马上返回,调用者线程往下执行其它代码,不过需要自己询问前面的调用结果。
打个比方,在餐厅中,服务员A现在在为一位客人C点菜,如果C还没点好菜之前A不能为其他客人点菜就是阻塞的,如果可以则是非阻塞的。
同步和异步
同步:调用者调用某个函数时,必须等待结果才能返回。
异步:调用者调用某个函数,如果函数不是马上返回,调用者可以往下执行,并且函数返回的时候会主动通知调用者。
还是回到上面的例子,A为C点菜的时候,如果不做其它的事,则是同步的,如果可以做其他的事,并且C点菜完了再叫A,则称为异步的。
实际情况有的可能是一起的,像同步阻塞,网上有的也说要分调用者和被调用者,这里我们不准备做这方面的深入讨论,大家大概明白这回事就行,异步和非阻塞我们先简单的理解为大家都不等,但是异步是前面的任务完成了主动通知我,而非阻塞需要自己实现查询。
1.2 网络编程模型
接下来说下网络模型,这里涉及到内核和用户态的概念了,前提是我们已经了解这些概念。
具体的模型大概分为以下几种类型:
A、阻塞I/O模型
在用户线程发出请求后,内核会检查数据是否就绪,此时用户线程会一直阻塞等待数据就绪,数据就绪后,内核将数据复制到用户线程空间中,并将用户线程恢复成可执行的,此时用户线程将解除阻塞状态并开始处理数据。
用户线程在发起一个I/O操作,无需阻塞便可以马上得到内核返回的结果。如果内核的返回数据为空,则表示内核还没准备好数据,需要稍后再次发起I/O请求。一旦内核中数据准备好,并且再次收到用户线程的请求,内核就会立刻将数据复制到用户线程,用户线程下次发起查询的时候就可以得到数据。
是多线程并发编程用的较多的模型,通过在一个Selector线程上以轮训方式检测在多个Socket上是否有多个事件到达,并逐个进行事件处理和响应。
这种模型下Selector线程可能会成为性能瓶颈,导致后续的事件迟迟的得不到响应。在实际应用中多路复用建议只做数据的接收和转发,将具体的业务操作以及复杂的逻辑运算转发给后面的业务线程处理。
D、信号驱动I/O模型
用户线程发起一个I/O请求,系统会为该请求对应的Socket注册一个信号函数,然后用户线程可以继续执行其他业务逻辑,在内核数据就绪时,系统发送一个信号到用户线程,用户线程在收到信号后,会在信号函数中调用对应的I/O读写操作来完成实际的I/O请求操作。
E、异步I/O模型
在该模型中,用户线程会发起一个请求到内核后,内核会立即返回一个状态,来说明此时的请求是否成功发起,在这个过程中用户线程不会发生任何的阻塞;
接着内核会等待数据准备完成并将数据复制到用户线程中,复制完后内核发送一个信号到用户线程,通知用户线程操作完成。
这个模型和信号驱动的区别是:信号驱动I/O由内核通知我们何时可以开始I/O操作,而异步I/O模型则由内核通知我们I/O什么时候完成。
二、Java中对应模型实现
1、同步阻塞I/O(BIO)
最古老的模型,服务端创建ServerSocket,然后绑定监听端口,然后不断的accept,然后处理具体一个个的Socket连接;客户端创建Socket,然后不断的发送/接收数据就可以了。
2、非阻塞I/O(NIO)
有的人也称New I/O,NIO提供了SocketChannel和ServiceSocketChannel不同套接字实现,这个后面专门细述。
3、异步I/O(AIO)
NIO2.0引入新的异步通道的概念,并提供文件和套接字的异步实现,这里也不详述。
如果写过C语言的就应该对这些有些了解,最早的Select,然后是Epoll,最终的原理是一样的,不过在Java层面的一些概念可能不一样。
三、BIO示例
下面写个简单的demo来了解BIO编程的过程:
1、服务端
public class EchoServer{
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
int port = 9001;
ServerSocket server = null;
try {
server = new ServerSocket(port);
Socket socket = null;
while (true) {
socket = server.accept();
new Thread(new EchoServerHandler(socket)).start();
}
} finally {
if (server != null) {
server.close();
server = null;
}
}
}
}
具体步骤如下:
先创建ServerSocket;
不断的Accept,然后创建新线程去处理新进来的连接;
具体处理逻辑类:
public class EchoServerHandler implements Runnable {
private Socket socket;
public EchoServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try {
in = new BufferedReader(new InputStreamReader(
this.socket.getInputStream()));
out = new PrintWriter(this.socket.getOutputStream(), true);
String currentTime = null;
String body = null;
while (true) {
body = in.readLine();
if (body == null)
break;
out.println(body);
}
} catch (Exception e) {
if (in != null) {
try {
in.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if (out != null) {
out.close();
out = null;
}
if (this.socket != null) {
try {
this.socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
this.socket = null;
}
}
再看客户端代码
public class EchoClient {
/**
* @param args
*/
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
run(args);
}
}
private static void run(String[] args) {
int port = 9001;
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
socket = new Socket("127.0.0.1", port);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
out.println("hello world");
String resp = in.readLine();
System.out.println("response is : " + resp);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (out != null) {
out.close();
out = null;
}
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
in = null;
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
socket = null;
}
}
}
}
客户端代码编写步骤如下:
1、创建Socket,注意这里要指定服务器的IP和端口号;
2、获取Socket的输入输出流;
3、根据情况对输出流写数据,或从输入流读数据。