4、网络IO:C10K问题及NIO和IO模型性能压测
网络IO变化模型
说的io,都知道同步、异步、阻塞、非阻塞,还有同步阻塞、同步非阻塞、异步非阻塞。
不过没有异步阻塞的,这个是矛盾的,异步怎么还会阻塞?
strace -ff -o out 路径
: 追踪系统调用,关注io的实现
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;
public class SocketBIO {
public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket(9090,20);
System.out.println("step1: new ServerSocket(9090) ");
while (true) {
Socket client = server.accept(); //阻塞1
System.out.println("step2:client\t" + client.getPort());
new Thread(new Runnable(){
public void run() {
InputStream in = null;
try {
in = client.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
while(true){
String dataline = reader.readLine(); //阻塞2
if(null != dataline){
System.out.println(dataline);
}else{
client.close();
break;
}
}
System.out.println("客户端断开");
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
应用和内核直接有一套基本的socket系统调用是不被破坏的,会返回一个文件描述符(比如fd3),然后还会绑定到8090端口,绑定后还会监听fd3这个文件描述符,然后才会得到一个结果,才可以使用netstat -natp
去看一个本地的一个0.0.0.0:8090,然后对到0.0.0.0:*,也就是任何端口都可以连接过来,然后监听着。
然后程序才可以调用accept(fd3),进行一个阻塞状态,阻塞状态过去后,得到一个新的连接(比如fd5),这个新的连接可能阻塞,阻塞过后肯定会有一个客户端连接过来和这个连接进行连接,连接后如果想要读取fd5这个连接的话,会又有一个阻塞状态。
也就是说有两个阻塞,一个客户端连接阻塞,一个读取时候的阻塞。
解决这个两个阻塞,可以创建一个新的线程,把这两个阻塞拆开了,主线程一直是死循环,接受客户端连接后就克隆,每一个克隆的线程去读取阻塞在某一个连接上。
所以可以知道,因为BIO阻塞,所以要抛出线程去读取数据。
NIO
测试代码:
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
public class SocketNIO {
// what why how
public static void main(String[] args) throws Exception {
LinkedList<SocketChannel> clients = new LinkedList<>();
ServerSocketChannel ss = ServerSocketChannel.open(); //服务端开启监听:接受客户端
ss.bind(new InetSocketAddress(9090));
ss.configureBlocking(false); //重点 OS NONBLOCKING!!! //只让接受客户端 不阻塞,如果为true,就在这里阻塞住了,不会往下执行,那就是BIO,如果是false,那就是NIO
// ss.setOption(StandardSocketOptions.TCP_NODELAY, false);
// StandardSocketOptions.TCP_NODELAY
// StandardSocketOptions.SO_KEEPALIVE
// StandardSocketOptions.SO_LINGER
// StandardSocketOptions.SO_RCVBUF
// StandardSocketOptions.SO_SNDBUF
// StandardSocketOptions.SO_REUSEADDR
while (true) {
//接受客户端的连接
Thread.sleep(1000); // 每隔一秒执行下面代码
SocketChannel client = ss.accept(); //不会阻塞? -1 NULL
//accept 调用内核了:1,没有客户端连接进来,返回值?在BIO 的时候一直卡着,但是在NIO ,不卡着,返回-1,NULL
//如果来客户端的连接,accept 返回的是这个客户端的fd 5,client object
//NONBLOCKING 就是代码能往下走了,只不过有不同的情况
if (client == null) {
// System.out.println("null.....");
} else {
client.configureBlocking(false); //重点 socket(服务端的listen socket<连接请求三次握手后,往我这里扔,我去通过accept 得到 连接的socket>,连接socket<连接后的数据读写使用的> )
int port = client.socket().getPort();
System.out.println("client..port: " + port);
clients.add(client);
}
ByteBuffer buffer = ByteBuffer.allocateDirect(4096); //可以在堆里 堆外
//遍历已经链接进来的客户端能不能读写数据
for (SocketChannel c : clients) { //串行化!!!! 多线程!!
int num = c.read(buffer); // >0 -1 0 //不会阻塞
if (num > 0) {
buffer.flip();
byte[] aaa = new byte[buffer.limit()];
buffer.get(aaa);
String b = new String(aaa);
System.out.println(c.socket().getPort() + " : " + b);
buffer.clear();
}
}
}
}
}
编译运行,然后监听socket,每隔1秒打印null。。。。说明没有阻塞住,是非阻塞的
如果这个时候在开启一个窗口连接端口9090,当然也可以再连接一个
可以看到是不会阻塞住的,
但有一个问题,之前BIO是跑出去一个线程解决读取数据,但现在完全是一个线程,在下面有个for循环里读数据,这样就可能一个线程把另一个连接数据给读取到
如果这个时候两个客户端连接发送数据,都是被一个线程给读取到了
nio是一直不停的去循环,接收连接请求
和BIO相比,NIO不需要抛出太多的线程,直接一个线程不停的循环,
监听到连接的文件描述符,把这个监听的文件描述符设置成非阻塞,然后使用死循环,循环里面使用.accept()方法,当这个文件描述符接收客户端时候,就不阻塞了,所以它就有返回值(返回值有可能是-1,表示没收到客户端,有可能是新的文件描述符,表示成功连接客户端),然后把这个返回值的文件描述符设置成非阻塞,然后再使用循环,循环里对它进行读取
C10K问题:http://www.kegel.com/c10k.html
翻译版:https://blog.csdn.net/wangtaomtk/article/details/51811011
修改不打印null。。。内容,
测试一下10k个连接连上来,启动代码:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
/**
* @author: 马士兵教育
* @create: 2020-06-06 15:12
*/
public class C10Kclient {
public static void main(String[] args) {
LinkedList<SocketChannel> clients = new LinkedList<>();
InetSocketAddress serverAddr = new InetSocketAddress("192.168.150.11", 9090);
//端口号的问题:65535
// windows
for (int i = 10000; i < 65000; i++) {
try {
SocketChannel client1 = SocketChannel.open();
SocketChannel client2 = SocketChannel.open();
/*
linux中你看到的连接就是:
client...port: 10508
client...port: 10508
*/
client1.bind(new InetSocketAddress("192.168.150.1", i));
// 192.168.150.1:10000 192.168.150.11:9090
client1.connect(serverAddr);
clients.add(client1);
client2.bind(new InetSocketAddress("192.168.110.100", i));
// 192.168.110.100:10000 192.168.150.11:9090
client2.connect(serverAddr);
clients.add(client2);
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("clients "+ clients.size());
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}
发现服务端有很多连接,不过速度不是很快
原因是这个NIO代码下面的这个循环,当连接越来越多,这里遍历的也越多,每个线程不光要接受客户端连接,还要遍历接收数据
多路复用器
单线程的多路复用器代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
public class SocketMultiplexingSingleThreadv1 {
//马老师的坦克 一 二期
private ServerSocketChannel server = null;
private Selector selector = null; //linux 多路复用器(select poll epoll kqueue) nginx event{}
int port = 9090;
public void initServer() {
try {
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
//如果在epoll模型下,open--》 epoll_create -> fd3
selector = Selector.open(); // select poll *epoll 优先选择:epoll 但是可以 -D修正
//server 约等于 listen状态的 fd4
/*
register
如果:
select,poll:jvm里开辟一个数组 fd4 放进去
epoll: epoll_ctl(fd3,ADD,fd4,EPOLLIN
*/
server.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
initServer();
System.out.println("服务器启动了。。。。。");
try {
while (true) { //死循环
Set<SelectionKey> keys = selector.keys();
System.out.println(keys.size()+" size");
//1,调用多路复用器(select,poll or epoll (epoll_wait))
/*
select()是啥意思:
1,select,poll 其实 内核的select(fd4) poll(fd4)
2,epoll: 其实 内核的 epoll_wait()
*, 参数可以带时间:没有时间,0 : 阻塞,有时间设置一个超时
selector.wakeup() 结果返回0
懒加载:
其实再触碰到selector.select()调用的时候触发了epoll_ctl的调用
*/
while (selector.select() > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys(); //返回的有状态的fd集合
Iterator<SelectionKey> iter = selectionKeys.iterator();
//so,管你啥多路复用器,你呀只能给我状态,我还得一个一个的去处理他们的R/W。同步好辛苦!!!!!!!!
// NIO 自己对着每一个fd调用系统调用,浪费资源,那么你看,这里是不是调用了一次select方法,知道具体的那些可以R/W了?
//幕兰,是不是很省力?
//我前边可以强调过,socket: listen 通信 R/W
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove(); //set 不移除会重复循环处理
if (key.isAcceptable()) {
//看代码的时候,这里是重点,如果要去接受一个新的连接
//语义上,accept接受连接且返回新连接的FD对吧?
//那新的FD怎么办?
//select,poll,因为他们内核没有空间,那么在jvm中保存和前边的fd4那个listen的一起
//epoll: 我们希望通过epoll_ctl把新的客户端fd注册到内核空间
acceptHandler(key);
} else if (key.isReadable()) {
readHandler(key); //连read 还有 write都处理了
//在当前线程,这个方法可能会阻塞 ,如果阻塞了十年,其他的IO早就没电了。。。
//所以,为什么提出了 IO THREADS
//redis 是不是用了epoll,redis是不是有个io threads的概念 ,redis是不是单线程的
//tomcat 8,9 异步的处理方式 IO 和 处理上 解耦
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void acceptHandler(SelectionKey key) {
try {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel client = ssc.accept(); //来啦,目的是调用accept接受客户端 fd7
client.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(8192); //前边讲过了
// 0.0 我类个去
//你看,调用了register
/*
select,poll:jvm里开辟一个数组 fd7 放进去
epoll: epoll_ctl(fd3,ADD,fd7,EPOLLIN
*/
client.register(selector, SelectionKey.OP_READ, buffer);
System.out.println("-------------------------------------------");
System.out.println("新客户端:" + client.getRemoteAddress());
System.out.println("-------------------------------------------");
} catch (IOException e) {
e.printStackTrace();
}
}
public void readHandler(SelectionKey key) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear();
int read = 0;
try {
while (true) {
read = client.read(buffer);
if (read > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
client.write(buffer);
}
buffer.clear();
} else if (read == 0) {
break;
} else {
client.close();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SocketMultiplexingSingleThreadv1 service = new SocketMultiplexingSingleThreadv1();
service.start();
}
}
启动代码:
然后还是用NIO里面测试C10k的那个客户端连接,启动那个客户端代码
查看多路复用器的服务端,可以看到连接速度非常快,没一会儿就好几万连接上了