一。单线程下的BIO实现
服务端:
public class Server {
public static void main(String[] args) {
byte[] buffer = new byte[1024];
try {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("服务器已启动并监听8080端口");
while (true) {
System.out.println();
System.out.println("服务器正在等待连接...");
// (1) 阻塞方法获取新的连接
Socket socket = serverSocket.accept();
System.out.println("服务器已接收到连接请求...");
System.out.println();
System.out.println("服务器正在等待数据...");
// (2) 阻塞读
// (3) 按字节流方式读取数据
socket.getInputStream().read(buffer);
System.out.println("服务器已经接收到数据");
System.out.println();
String content = new String(buffer);
System.out.println("接收到的数据:" + content);
}
} catch (IOException e) {
e.printStackTrace();
}
}
客户端:
public class Client {
public static void main(String[] args) {
Socket socket = null;
try {
socket = new Socket("127.0.0.1",8080);
String message = "";
Scanner sc = new Scanner(System.in);
message = sc.next();
socket.getOutputStream().write(message.getBytes());
socket.close();
sc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
场景1:启动server端,然后分别启动client1,client2,client3
运行结果:
启动服务端:
然后启动client1 client2 client3,查看服务端控制台日志
分析结果现象:
如图所示:服务端启动时,并监听端口8080,然后处于阻塞状态,等待客户端连接,当有客户端连接服务端时,服务端接受连接请求,然后等待客户端发送数据。最后客户端1 发送消息,服务端收到消息。从运行结果上来看,说明了服务器端有两个阻塞,一个是等待客户端连接阻塞,另一个就是读客户端数据阻塞。
单线程下的BIO实现缺点:
1.当服务器端收到客户端连接,但是没有收到客户端的发送数据请求,服务端读客户端数据read()会一直阻塞,如果其他客户端在来请求是无法响应的。
二、多线程下的BIO实现
场景,每次服务器读到客户端发来请求数据就开启一个线程读数据。
public class Server1 {
public static void main(String[] args) {
byte[] buffer = new byte[1024];
try {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("服务器已启动并监听8080端口");
while (true) {
System.out.println();
System.out.println("服务器正在等待连接...");
// (1) 阻塞方法获取新的连接
Socket socket = serverSocket.accept();
// (2) 每一个新的连接都创建一个线程,负责读取数据
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("服务器已接收到连接请求...");
System.out.println();
System.out.println("服务器正在等待数据...");
try {
// (3) 按字节流方式读取数据
socket.getInputStream().read(buffer);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("服务器已经接收到数据");
System.out.println();
String content = new String(buffer);
System.out.println("接收到的数据:" + content);
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
缺点:多线程解决了无法并行读取客户端的数据,但是如果大量出现不发送数据的客户端,会导致很多的连接,连接多会导致服务器压力增大。所以如果出现不活跃的线程比较多,应该采用单线程的方案。但是单线程无法处理并发,于是有了NIO。
NIO重要解决客户连接和读取数据阻塞问题的。
三、模拟NIO
1.解决客户端连接和读取数据阻塞问题。
public class NioServer {
public static void main(String[] args) throws InterruptedException{
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
try {
//Java为非阻塞设置的类
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
while(true) {
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannel==null) {
//表示没人连接
System.out.println("正在等待客户端请求连接...");
Thread.sleep(5000);
}else {
System.out.println("当前接收到客户端请求连接...");
}
if(socketChannel!=null) {
//设置为非阻塞
socketChannel.configureBlocking(false);
byteBuffer.flip();//切换模式 写-->读
int effective = socketChannel.read(byteBuffer);
if(effective!=0) {
String content = Charset.forName("utf-8").decode(byteBuffer).toString();
System.out.println(content);
}else {
System.out.println("当前未收到客户端消息");
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:
分析运行结果:
从结果上看,接受到客户端的请求,没有阻塞,但是又回到客户端等待请求,所以会导致丢失当前客户连接请求并丢失客户端发来的消息,怎么办呢?把当前客户请求存储到list上,然后轮询遍历list,验证客户端是否准备好消息,然后打印。如下代码:
public class NioServer1 {
public static void main(String[] args) {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
List<SocketChannel> socketList = new ArrayList();
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
//设置非阻塞
serverSocketChannel.configureBlocking(false);
while (true){
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannel == null){
System.out.println("正在等待客户连接..");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
System.out.println("当前收到客户端的连接请求");
socketList.add(socketChannel);
}
socketList.forEach(s ->{
//设置非阻塞
try {
s.configureBlocking(false);
int read = s.read(byteBuffer);
if(read != 0){
byteBuffer.flip();//切换模式 写-->读
String content = Charset.forName("utf-8").decode(byteBuffer).toString();
System.out.println(content);
byteBuffer.clear();
}else{
System.out.println("当前未收到客户端消息");
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
分析:如图代码所示,我们没有开启第二个线程,使用的是一个线程解决了并发处理客户端请求和发送消息,这样的模式可以很好的解决了BIO在单线程下的并发处理多个客户端的请求的问题,并且解决了非阻塞情况下客户端丢失的问题。
缺点:在接受消息处理上是有问题的,每次来个客户端都要轮询判断数据是否准备好,假设有1000w个连接,甚至更多,采用这种轮询方式效率是低的。
另外,1000w连接中,我们可能只会有100w会有消息,剩下的900w并不会发送任何消息,那么这些连接程序依旧要每次都去轮询,这显然是不合适的。怎么解决呢?将轮询那块代码使用操作系统级别的系统函数(select函数),主动的去感知有数据的socket。
四、真实的NIO
/**
* NIO 模型中 selector 的作用,
* 一条连接来了之后,现在不创建一个 while 死循环去监听是否有数据可读了,
* 而是直接把这条连接注册到 selector 上,
* 然后,通过检查这个 selector,就可以批量监测出有数据可读的连接,进而读取数据,
*/
public class NioServerSelector {
public static void main(String[] args) throws IOException{
//使用selector 解决while的轮询问题,把所有的连接的注册到Selector上
Selector serverSelector = Selector.open();
Selector clientSelector = Selector.open();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
new Thread(()->{
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
while (true){
// 监测是否有新的连接,这里的1指的是阻塞的时间为 1ms
if(serverSelector.select(1) > 0){
System.out.println("监测是否有新的连接, 当前收到客户端的连接请求..");
Set<SelectionKey> selectionKeys = serverSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()){
SelectionKey key = keyIterator.next();
if(key.isAcceptable()){
try {
// (1) 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
clientChannel.register(clientSelector, SelectionKey.OP_READ);
}finally {
keyIterator.remove();
}
}
}
}else {
System.out.println("监测是否有新的连接, 正在等待客户端连接..");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
///(2) 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms
while (true){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("批量轮询是否有哪些连接有数据可读");
if(clientSelector.select(1) > 0){
Set<SelectionKey> selectionKeys = clientSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator .hasNext()){
SelectionKey key = keyIterator.next();
if(key.isReadable()){
try {
SocketChannel channel = (SocketChannel)key.channel();
// (3) 面向 Buffer
channel.read(byteBuffer);
byteBuffer.flip();
System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
.toString());
} catch (IOException e) {
keyIterator.remove();
key.interestOps(SelectionKey.OP_READ);
e.printStackTrace();
}
}
}
}
}
}catch (Exception e){
e.printStackTrace();
}
}).start();
}
}