1. BIO
1.1 MySocketBIOv1
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 MySocketBIOv1 {
public static void main(String[] args) throws IOException {
//开启9000端口的监听
ServerSocket serverSocket = new ServerSocket(9000, 20);
System.out.println("已开启9000端口监听");
//程序会卡死阻塞,直到有新的TCP进来。
Socket client = serverSocket.accept();
System.out.println("接收到一个新的连接:"+client.getRemoteSocketAddress());
InputStream inputStream = client.getInputStream();
//用buffer缓冲区读,读的快一点
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
for (;;){
String dataLine = reader.readLine(); //读取一行数据
if (dataLine != null) {
System.out.println("读取到客户端一行数据:"+dataLine);//打印
}else {
System.out.println("客户端已关闭");
client.close();
break;
}
}
}
}
好了。启动!然后使用linux的nc连接
wangfan@ubuntu:~/桌面$ nc 192.168.235.1 9000
wangfan@ubuntu:~/桌面$ nc 192.168.235.1 9000
111
222
333
444
555
666
777
^C
wangfan@ubuntu:~/桌面$
项目打印:
已开启9000端口监听接收到一个新的连接:/192.168.235.137:49122 读取到客户端一行数据:111 读取到客户端一行数据:222 读取到客户端一行数据:333 读取到客户端一行数据:444 读取到客户端一行数据:555 读取到客户端一行数据:666 读取到客户端一行数据:777 客户端已关闭
上面的代码只能有一个客户端连接,使用完就关闭了。 下面这个会支持多个客户端连接进来。
1.2 MySocketBIOv2
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 MySocketBIOv2 {
public static void main(String[] args) throws IOException {
//开启9000端口的监听
ServerSocket serverSocket = new ServerSocket(9000, 20);
System.out.println("已开启9000端口监听");
for (;;){
//程序会卡死阻塞,直到有新的TCP进来。
Socket client = serverSocket.accept();
System.out.println("接收到一个新的连接:"+client.getRemoteSocketAddress());
InputStream inputStream = client.getInputStream();
//用buffer缓冲区读,读的快一点
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
for (;;){
String dataLine = reader.readLine(); //读取一行数据
if (dataLine != null) {
System.out.println("读取到客户端一行数据:"+dataLine);
}else {
System.out.println("客户端已关闭");
client.close();
break;
}
}
}
}
}
好了。启动!然后使用linux的nc连接
wangfan@ubuntu:~/桌面$ nc 192.168.235.1 9000
111
222
^C
wangfan@ubuntu:~/桌面$ nc 192.168.235.1 9000
333
444
^C
wangfan@ubuntu:~/桌面$ nc 192.168.235.1 9000
555
666
^C
wangfan@ubuntu:~/桌面$
项目打印:
已开启9000端口监听接收到一个新的连接:/192.168.235.137:49140 读取到客户端一行数据:111 读取到客户端一行数据:222 客户端已关闭 接收到一个新的连接:/192.168.235.137:49142 读取到客户端一行数据:333 读取到客户端一行数据:444 客户端已关闭 接收到一个新的连接:/192.168.235.137:49144 读取到客户端一行数据:555 读取到客户端一行数据:666 客户端已关闭
好了,可以支持多个客户端了,但是还有一个弊端,就是,连接进来的只能是顺序的,加上前面的客户端一直不断开,那么后面的客户端就要永远排队。所以。我们是用多线程再次改进。
1.3 MySocketBIOv3
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 MySocketBIOv3 {
public static void main(String[] args) throws IOException {
//开启9000端口的监听
ServerSocket serverSocket = new ServerSocket(9000, 20);
System.out.println("已开启9000端口监听");
for (;;){
//程序会卡死阻塞,直到有新的TCP进来。
Socket client = serverSocket.accept();
new Thread(()->{
System.out.println("接收到一个新的连接:"+client.getRemoteSocketAddress());
InputStream inputStream = null;
try {
inputStream = client.getInputStream();
//用buffer缓冲区读,读的快一点
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
for (;;){
String dataLine = reader.readLine(); //读取一行数据
if (dataLine != null) {
System.out.println("读取到客户端"+client.getRemoteSocketAddress()+"一行数据:"+dataLine);
}else {
//读取不到则为客户端关闭
System.out.println("客户端"+client.getRemoteSocketAddress()+"已关闭");
client.close();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
好了。启动!然后使用linux的nc连接
wangfan@ubuntu:~/桌面$ nc 192.168.235.1 9000
111
222
^C
wangfan@ubuntu:~/桌面$
再打开一个标签,再次连接
wangfan@ubuntu:~/桌面$ nc 192.168.235.1 9000
333
444
^C
^C
wangfan@ubuntu:~/桌面$
项目打印:
已开启9000端口监听接收到一个新的连接:/192.168.235.137:49152 读取到客户端/192.168.235.137:49152一行数据:111 读取到客户端/192.168.235.137:49152一行数据:222 接收到一个新的连接:/192.168.235.137:49154 读取到客户端/192.168.235.137:49154一行数据:333 读取到客户端/192.168.235.137:49154一行数据:444 读取到客户端/192.168.235.137:49154一行数据: 客户端/192.168.235.137:49154已关闭 客户端/192.168.235.137:49152已关闭
好了。BIO
最多只能通过多线程支持到这种程度了,不能做更进一步的优化了。但是BIO
的缺陷已经显露出来了。
BIO的缺陷:
因为接收TCP握手以及握手成功后接收数据是阻塞操作,也就是上面的serverSocket.accept();
和 reader.readLine();
。所以系统不得不启用多线程处理,但是这样随着客户端连接数量越来越大。系统启动的多线程越来越多,cpu大部分时间消耗在线程之间切换上。严重浪费资源。
归根到底:BIO的根本缺陷在于它有2个是阻塞操作。即:接收到连接的阻塞,和接收数据的阻塞。
2. NIO
为解决BIO的阻塞操作的原因造成系统资源严重浪费,Linux的kernel推出了NIO的API。
即调用serverSocket.accept();
和reader.readLine();
这两个API时不再是“接收”的意思,而是“询问”的意思。 即有连接/数据就会有返回,没有则返回无。 这样线程不用卡住。
2.1 MySocketNIOv1
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class MySocketNIOv1 {
public static void main(String[] args) throws IOException {
//开启9000端口的监听并设置为非阻塞
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9000));
//设置为非阻塞。重点!!!!!
serverSocketChannel.configureBlocking(false);
System.out.println("已开启9000端口监听...");
//不断轮询接收客户端连接
for (;;){
//接受客户端的连接,而且这一步不阻塞,如果没有客户端连接进来,则client会返回null
SocketChannel client = serverSocketChannel.accept();//不会阻塞? -1 NULL
if (client != null) {
new Thread(()->{
try {
System.out.println("接收到一个新的连接:"+client.getRemoteAddress().toString());
//重点!!!!客户端已连接成功,设置接收数据不阻塞
client.configureBlocking(false);
//不断轮询接收客户端发过来的数据
for (;;){
ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
int readBytesNum = client.read(buffer);
//读取数据
if ( readBytesNum > 0 ){
buffer.flip();
byte[] readDataBytes = new byte[buffer.limit()];
buffer.get(readDataBytes);
String readData = new String(readDataBytes);
System.out.println("读取到客户端"+client.getRemoteAddress()+"一行数据:"+readData);
buffer.clear();
}else if (readBytesNum == -1){//客户端关闭
System.out.println("客户端"+client.getRemoteAddress()+"已关闭");
client.close();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
}
好了。启动!然后使用linux的nc连接
wangfan@ubuntu:~/桌面$ nc 192.168.235.1 9000
aaa
bbb
^C
wangfan@ubuntu:~/桌面$
再打开一个标签,再次连接
wangfan@ubuntu:~/桌面$ nc 192.168.235.1 9000
ccc
ddd
^C
wangfan@ubuntu:~/桌面$
再打开一个标签,再次连接
wangfan@ubuntu:~/桌面$ nc 192.168.235.1 9000
eee
fff
^C
wangfan@ubuntu:~/桌面$
项目打印:
已开启9000端口监听…接收到一个新的连接:/192.168.235.137:49178 读取到客户端/192.168.235.137:49178一行数据:aaa
读取到客户端/192.168.235.137:49178一行数据:bbb
接收到一个新的连接:/192.168.235.137:49180 读取到客户端/192.168.235.137:49180一行数据:ccc
读取到客户端/192.168.235.137:49180一行数据:ddd
接收到一个新的连接:/192.168.235.137:49182 读取到客户端/192.168.235.137:49182一行数据:eee
读取到客户端/192.168.235.137:49182一行数据:fff
客户端/192.168.235.137:49182已关闭 客户端/192.168.235.137:49178已关闭 客户端/192.168.235.137:49180已关闭
2.2 MySocketNIOv2
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.Iterator;
import java.util.LinkedList;
public class MySocketNIOv2 {
public static void main(String[] args) throws IOException {
//使用LinkedList收集所有已建立连接的客户端
LinkedList<SocketChannel> clients = new LinkedList<>();
//开启9000端口的监听并设置为非阻塞
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9000));
//设置为非阻塞。重点!!!!!
serverSocketChannel.configureBlocking(false);
System.out.println("已开启9000端口监听...");
//起新线程不断轮询接收客户端连接
new Thread(()->{
for (;;){
try {
synchronized (MySocketNIOv2.class){
//接受客户端的连接,而且这一步不阻塞,如果没有客户端连接进来,则client会返回null
SocketChannel client = serverSocketChannel.accept();
if (client != null) {
System.out.println("接收到一个新的连接:"+client.getRemoteAddress().toString());
//重点!!!!客户端已连接成功,设置接收数据不阻塞
client.configureBlocking(false);
//不断轮询接收客户端发过来的数据
clients.add(client);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
//起新线程不断轮询clients,从里面拿数据
new Thread(()->{
//不断轮询clients
for (;;){
synchronized (MySocketNIOv2.class){
Iterator<SocketChannel> iterator = clients.iterator();
while (iterator.hasNext()){
SocketChannel client = iterator.next();
//不断轮询接收客户端发过来的数据
for (;;){
try {
//开辟一个4096字节的buffer
ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
int readBytesNum = client.read(buffer);
if ( readBytesNum > 0 ){
buffer.flip();
byte[] readDataBytes = new byte[buffer.limit()];
buffer.get(readDataBytes);
String readData = new String(readDataBytes);
System.out.println("读取到客户端"+client.getRemoteAddress()+"一行数据:"+readData);
buffer.clear();
}else if (readBytesNum == -1){
System.out.println("客户端"+client.getRemoteAddress()+"已关闭");
client.close();
iterator.remove();
break;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}).start();
System.in.read();
}
}
好了。启动!然后使用linux的nc连接
[root@192 ~]# nc 192.168.235.1 9000
123
234
345
456
567
^C
[root@192 ~]#
项目打印:
已开启9000端口监听…接收到一个新的连接:/192.168.235.133:36198 读取到客户端/192.168.235.133:36198一行数据:123
读取到客户端/192.168.235.133:36198一行数据:234
读取到客户端/192.168.235.133:36198一行数据:345
读取到客户端/192.168.235.133:36198一行数据:456
读取到客户端/192.168.235.133:36198一行数据:567
客户端/192.168.235.133:36198已关闭
上面是优化后的NIO模型,这个模型放弃了一个连接建立一个线程的方式,而是使用两个线程,一个线程不断接收客户端连接,接收到的客户端放入到一个链表里面,另一个线程不断轮询链表内的客户端,从客户端里面读数据。MySocketNIOv2
看似解决了多线程的问题,但是遇到另外一个新的问题。那就是, LinkedList里面的元素都是排队取数据的,假设其中一个的数据很多,取出数据很慢,这样会阻塞其他客户端取出数据。所以这个时候我们可以把有数据到达的客户端启抛出一个线程单独处理数据的读取。
2.3 MySocketNIOv3
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.Iterator;
import java.util.LinkedList;
public class MySocketNIOv3 {
public static void main(String[] args) throws IOException {
//使用LinkedList收集所有已建立连接的客户端
LinkedList<SocketChannel> clients = new LinkedList<>();
//开启9000端口的监听并设置为非阻塞
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9000));
//设置为非阻塞。重点!!!!!
serverSocketChannel.configureBlocking(false);
System.out.println("已开启9000端口监听...");
//起新线程不断轮询接收客户端连接
new Thread(()->{
for (;;){
try {
synchronized (MySocketNIOv3.class){
//接受客户端的连接,而且这一步不阻塞,如果没有客户端连接进来,则client会返回null
SocketChannel client = serverSocketChannel.accept();
if (client != null) {
System.out.println("接收到一个新的连接:"+client.getRemoteAddress().toString());
//重点!!!!客户端已连接成功,设置接收数据不阻塞
client.configureBlocking(false);
//不断轮询接收客户端发过来的数据
clients.add(client);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
//起新线程不断轮询clients,从里面拿数据
new Thread(()->{
//不断轮询clients
for (;;){
synchronized (MySocketNIOv3.class){
Iterator<SocketChannel> iterator = clients.iterator();
while (iterator.hasNext()){
SocketChannel client = iterator.next();
if (!client.isConnected()){
iterator.remove();
}
//不断轮询接收客户端发过来的数据
for (;;){
try {
//开辟一个4096字节的buffer
ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
String data = MySocketNIOv3.readDataFromClient(buffer, client);
if (data == null){
System.out.println("客户端"+client.getRemoteAddress()+"已关闭");
client.close();
iterator.remove();
break;
}
if (!data.equals("")){
String finalData = data;
new Thread(()->{
try {
System.out.println("读取到客户端"+client.getRemoteAddress()+"一行数据:"+ finalData);
while (true){
buffer.flip();
String data1 = MySocketNIOv3.readDataFromClient(buffer, client);
if (data1 == null){
System.out.println("客户端"+client.getRemoteAddress()+"已关闭");
client.close();
break;
}else if (!data1.equals("")){
System.out.println("读取到客户端"+client.getRemoteAddress()+"一行数据:"+ data1);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}).start();
System.in.read();
}
public static String readDataFromClient(ByteBuffer buffer,SocketChannel client){
try {
int readBytesNum = client.read(buffer);
if (readBytesNum > 0){
buffer.flip();
byte[] readDataBytes = new byte[buffer.limit()];
buffer.get(readDataBytes);
String readData = new String(readDataBytes);
buffer.clear();
return readData;
}
if (readBytesNum == -1){
return null;
}else {
return "";
}
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
public class SocketNIO {
public static void main(String[] args) throws Exception {
//所有连接进来的客户端的集合
LinkedList<SocketChannel> clients = new LinkedList<>();
ServerSocketChannel ss = ServerSocketChannel.open(); //服务端开启监听:接受客户端
ss.bind(new InetSocketAddress(9090));
//设置为NONBLOCKING,不阻塞。
ss.configureBlocking(false); //重点 OS NONBLOCKING!!! //只让接受客户端 不阻塞
//不断轮询接收客户端连接
while (true) {
//接受客户端的连接,而且这一步不阻塞,如果没有客户端连接进来,则client会返回null
SocketChannel client = ss.accept(); //不会阻塞? -1 NULL
if (client == null) {
//System.out.println("没有客户端连接进来..");
} 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();
}
}
}
}
}
这样的不阻塞操作可以让线程不在浪费,只使用一个线程即可遍历所有连接进来的client。并读写数据。
但是这样的模型,如果遇到客户端数量再一次增加一个量级的客户端连接,依然不够用, 如果遇到C10K的问题。每次一循环就要,循环10000次,而且由于客户端的read操作都是一次用户态向内核态的切换,每循环一次就是10000次切换,这对于系统来说损失很大造成的资源浪费。
NIO模型的最大根本弊端在于:用户态和内核态的切换过于频繁,导致的性能下降。多路复用技术就是解决这个问题。
3. 多路复用
多路复用说的是:select、poll以及epoll的集合。也就是说:select、poll以及epoll都属于多路复用。