首先抛出一个步骤,同步IO(BIO,NIO,多路复用器)都是同步的,它们在服务端有相同的启用端口开启监听的步骤。
- socket 得到一个文件描述符sfd
- bind 在服务端绑定端口
- listen 开启监听
- accept 获取客户端连接
- recv 获取客户端发来的数据(读写)
前三步在BIO,NIO,多路复用器里大同小异,差异在后面两步accept与recv的处理方法。通过几个例子来说明。
1.BIO(Blocking IO)
同步阻塞,⼀个请求过来,应⽤程序开了⼀个线程,等 IO 准备好,IO 操作也是自己干; 采用 BIO 模型的服务端,由⼀个独立的 Acceptor 线程负责进行监听;在 while(true) 循环中调用accept() 方法,等待 客户端的请求; 一旦接收到请求,就可以建立套接字开始进行读写操作,这时候不再接收其他的请求,直到读写完成; 为了让 BIO 能够同时处理多个请求,那么就需要使用多线程处理;当服务端接收到请求,就为客户端创建⼀个线程进行处理,处理完成后再做线程销毁;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
1. BIO 多个连接,多个线程
2. */
public class BIOSocket {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8081);
System.out.println("第一步:new ServerSocket(8081)");
while (true){
Socket client = serverSocket.accept();
System.out.println("第二步:client端口 "+client.getPort());
new Thread(new Runnable() {
Socket ss;
public Runnable setSS(Socket s){
ss=s;
return this;
}
@Override
public void run() {
try {
InputStream in = ss.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
while (true){
System.out.println(reader.readLine());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}.setSS(client)
).start();
}
}
}
启动程序
- 当没有客户端连接,程序会在
Socket client = serverSocket.accept();
处阻塞,等待客服端的连接。(accept) - 当获取到客户端连接后,新开一条线程获取客户端发来的数据,如果没有数据过来,此时线程会阻塞。(recv)
下图为在accept处阻塞,服务端8081端口处于监听状态,等待客户端连接。
新建一个客户端,连接服务端端口8081。
import java.io.IOException;
import java.net.Socket;
public class Client1 {
public static void main(String[] args) throws IOException {
Socket client=new Socket("127.0.0.1",8081);
System.out.println("连接服务器成功!"+client.getLocalSocketAddress());
}
}
下图为连接客户端成功,开了一个52565端口作为客户端,此时客户端没有数据发送,程序发生阻塞(recv)
总结BIO:
优势:可以连接很多的线程
问题:线程内存浪费,cpu调度消耗cpu时间片 (根源就是accept,recv会阻塞)
2.NIO
NIO在Java中是New IO,在操作系统中是NonBlocking IO(非阻塞IO)
同步非阻塞,不用等待 IO 准备,准备好了会通知,不过 IO 操作还是要自己干。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
/**
* NIO 一个线程可以解决很多个客户端连接
* */
public class NIOSocket {
public static void main(String[] args) throws IOException, InterruptedException {
//用一个linkedList集合存放连接完成的客户端
LinkedList<SocketChannel> clients = new LinkedList<>();
ServerSocketChannel ss = ServerSocketChannel.open();
ss.bind(new InetSocketAddress(8082));//绑定服务端端口8082
ss.configureBlocking(false);//设置内核为NonBlocking
while (true){
Thread.sleep(1000);
//尝试获取客户端连接,没有客户端连接就返回null,不会阻塞,在BIO的时候就会一直阻塞
//如果客户端来连接了,accept返回的是这个客户端的fd
SocketChannel client = ss.accept();
if (client==null){
System.out.println("null..");
}else{
/*
* 对客户端设置非阻塞,分两种情况:
1.客户端有数据过来->正常情况;
2.客户端没有数据过来->也不会阻塞,操作系统上返回状态-1,Java里返回null
* */
client.configureBlocking(false);
int port = client.socket().getPort();
System.out.println("client port:"+port);
clients.add(client);
}
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(4096);
//遍历连接的客户端能不能读写数据
for (SocketChannel c : clients) {
int num=c.read(byteBuffer);
if (num>0){
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
String s = new String(bytes);
System.out.println(c.socket().getPort()+" : "+s);
byteBuffer.clear();
}
}
}
}
}
启动程序,由于设置了非阻塞–> ss.configureBlocking(false)
在内核中将阻塞设置为非阻塞,client.configureBlocking(false)
客户端也设置为非阻塞。即使没有客户端连接,或者是没有数据发送过来,程序也不会阻塞,而是在Java中返回null,在操作系统中返回-1状态,程序继续循环,单线程轮询。
总结:
优势:规避多线程,减少线程内存浪费
问题:不断轮询,假设有10000个连接而只有一个连接发来数据,每循环一次,其实必须向内核发送10000次的recv系统调用(代码中的for循环),那么这里有9999次是无意义的,CPU空转造成消耗资源。
如何解决? 多路复用器…
3.多路复用器
多路复用器就是通过系统内核返回状态,告诉你哪些连接可读可写,具体的读写还是要用户程序自己干。三种方式如下图:
简单来讲select和poll通过一次系统调用,把fds传给内核,内核遍历,减少了系统调用的次数,这是优势,问题在每次select、poll都要重新遍历fd,所以这里就引出了epoll,在内核中开辟空间保存fd。