前言:通信中我们常常建立socket 通过其tcp完成通信;
1 Socket 介绍:
所谓socket 通常也称作”套接字“,用于描述IP地址和端口,是一个通信链的句柄。应用程序通常通过”套接字”向网络发出请求或者应答网络请求;socket是基于应用服务与TCP/IP通信之间的一个抽象,他将TCP/IP协议里面复杂的通信逻辑进行分装,对用户来说,只要通过一组简单的API就可以实现网络的连接;
服务端初始化ServerSocket,然后对指定的端口进行绑定,接着对端口及进行监听,通过调用accept方法阻塞,此时,如果客户端有一个socket连接到服务端,那么服务端通过监听和accept方法可以与客户端进行连接。服务端和客户端通过socket获得输入输出流进行数据传输;
2 Socket 客户端与服务端通信:
2.1 BIO 通信:
1)服务端:建立 ServerSocket 并监听端口,当有连接进入时,获取数据进行处理:
package com.gupaoedu.springcloud.example.demo.socketdemo.server;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerSocketDemo {
public static void main(String[] args) {
// 声明服务端 ServerSocket
ServerSocket serverSocket= null;
while (true){
// 不断轮训
try {
// 监听 8080 端口
serverSocket = new ServerSocket(8080);
// 连接阻塞,直到有新的连接进入
Socket socket = serverSocket.accept();
System.out.println("socket.getPort() = " + socket.getPort());
// 获取客户端信息-- io 阻塞阻塞
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String str = bufferedReader.readLine();
System.out.println("收到客户端信息 : " + str+System.currentTimeMillis());
// 写给客户端信息
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write("服务器端回复客户端信息\n");
bufferedWriter.flush();
bufferedReader.close();
bufferedWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2)客户端:建立Socket 连接:
package com.gupaoedu.springcloud.example.demo.socketdemo.client;
import java.io.*;
import java.net.Socket;
public class SocketClientDemo {
public static void main(String[] args) {
try {
Socket socket = new Socket("localhost",8080);
// 写给客户端信息--io阻塞
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write("客户端:客户端写入信息\n");
bufferedWriter.flush();
// 接收服务端信息--io阻塞
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String str = bufferedReader.readLine();
System.out.println("收到服务器端信息 : " + str+System.currentTimeMillis());
bufferedWriter.close();
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3)因为是阻塞式的io 当多个客户端都连接到服务端时,同一时刻服务端只能接收一个客户端的请求,其它客户端的请求是被阻塞的,显然服务端的这种处理效率是非常低下的;
既然是服务端io阻塞导致了,那么是否可以使用线程池,每次接入一个客户端,就可以为其分配一个线程进行处理,处理完毕线程归还到线程池中;
4)线程任务处理:
服务端:
package com.gupaoedu.springcloud.example.demo.socketdemo.server;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ServerSocketDemo {
static ExecutorService executorService = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept();// 连接阻塞
System.out.println("socket.getPort() = " + socket.getPort());
executorService.execute(new ServerTask(socket));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
线程任务处理:
package com.gupaoedu.springcloud.example.demo.socketdemo.server;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
public class ServerTask implements Runnable{
private Socket socket;
public ServerTask(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 获取客户端信息 io阻塞
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String str = bufferedReader.readLine();
System.out.println("收到客户端信息 : " + str + System.currentTimeMillis());
// 写给客户端信息
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write("服务器端回复客户端信息\n");
bufferedWriter.flush();
bufferedReader.close();
bufferedWriter.close();
} catch (Exception ex) {
}
}
}
此时服务端处理请求的性能,取决于线程池的大小,显然线程池不能一直增大;那么怎么才能进一步提高服务器处理性能,既然服务端因为io 阻塞导致了必须处理一个客户端后在去处理下一个客户端,那么是否能让这个io 不阻塞;
2.2 NIO 通信:非阻塞IO(NIO);使得连接不阻塞;输入输出流不阻塞:
1)服务端:
package com.gupaoedu.springcloud.example.demo.niosocket.server;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
public class NioServer {
public static void main(String[] args) {
// Channel / Buffer / Selector
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);// 设置非阻塞
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
while (true){
// 监听客户端请求
SocketChannel socketChannel = serverSocketChannel.accept();
if (null != socketChannel){// 因为非阻塞,所以当没有客户端连接是获取到的socket 为null
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);// 设置缓冲区大小为1kb
System.out.println("socket.getLocalAddress() = " + socketChannel.getLocalAddress());
socketChannel.read(byteBuffer);// 获取客户端信息并将数据读取到缓冲区,如果客户端io阻塞,此处服务端也会阻塞
System.out.println("byteBuffer.array() = " +new String( byteBuffer.array()));
// 获取写出流
byteBuffer.flip();// 写入流反转
socketChannel.write(byteBuffer);
}else{
Thread.sleep(1000);
// 1s 之后再次尝试去获得客户端请求
System.out.println("\"连接未就绪\" = " + "连接未就绪");
}
}
}catch (Exception ex){
ex.printStackTrace();
}
}
}
2)客户端:
package com.gupaoedu.springcloud.example.demo.niosocket.cliet;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
public class NioClient {
public static void main(String[] args) {
try{
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress(8080));
if (socketChannel.isConnectionPending()){
// 如果连接还没有建立好那么先建立连接
socketChannel.finishConnect();
}
// 建立缓冲区大小
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 向缓冲区写入数据
byteBuffer.put("I am socketChannel client".getBytes(StandardCharsets.UTF_8));
byteBuffer.flip();
socketChannel.write(byteBuffer);
// 获取服务端数据
byteBuffer.clear();
// 获取服务端数据
int i = socketChannel.read(byteBuffer);
if (i>0){
System.out.println("客户端收取到服务端的信息byteBuffer.array() = " +new String( byteBuffer.array()));
}else {
System.out.println("\"没有收到服务端数据\" = " + "没有收到服务端数据");
}
}catch (Exception ex){
ex.printStackTrace();
}
}
}
3)实现了非阻塞的连接和非阻塞的IO ,显然并不是所有只要连接了服务端口的客户端就会立刻与服务端通信,这样就好造成cpu 资源的无端占用,那么我们怎么知道那些客户端和服务端已经将通信准备好了,可以直接进行通信;
select 模型:
4)多路复用体现在可以只用一个线程,将与服务端建立连接的所有管道进行管理,并且通过轮询/事件 可以知道那个通道已经完成就绪,可以进行io通信;
4.1)服务端:
package com.gupaoedu.springcloud.example.demo.selectorsocket.server;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
public class SelectorNioServer {
static Selector selector;
public static void main(String[] args) {
// Channel / Buffer / Selector
try {
selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);// 设置非阻塞
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
// 连接事件注册到多路复用
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 当有事件到达时会被唤醒,否则会一直进行阻塞
selector.select();
// 获取多个事件
Set<SelectionKey> selectionKeySet = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeySet.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()) {
// 有客户端连接事件
handleAcceptEvent(selectionKey);
} else if (selectionKey.isReadable()) {
// 有客户端写事件--》服务端进行读取
handleReadEvent(selectionKey);
} else if (selectionKey.isWritable()) {
System.out.println("\"server write\" = " + "server write");
}
iterator.remove();
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
/**
* 客户端有写入事件
*
* @param selectionKey
*/
private static void handleReadEvent(SelectionKey selectionKey) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
try {
socketChannel.read(byteBuffer);
System.out.println("server receive byteBuffer.array() = " + new String(byteBuffer.array()));
} catch (Exception ex) {
ex.printStackTrace();
}
}
/**
* 有客户端连接事件
*
* @param selectionKey
*/
private static void handleAcceptEvent(SelectionKey selectionKey) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
try {
SocketChannel socketChannel = serverSocketChannel.accept();
// 设置非阻塞
socketChannel.configureBlocking(false);
// 服务端协会数据
socketChannel.write(ByteBuffer.wrap("I am server".getBytes(StandardCharsets.UTF_8)));
socketChannel.register(selector, SelectionKey.OP_READ);
// socketChannel.register(selector,SelectionKey.OP_WRITE);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
4.2)客户端:
package com.gupaoedu.springcloud.example.demo.selectorsocket.client;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
public class SelectorNioClient1 {
static Selector selector;
public static void main(String[] args) {
try{
selector = Selector.open();
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress(8080));
// 连接事件注册到多路复用
socketChannel.register(selector, SelectionKey.OP_CONNECT);
while (true) {
// 当有事件到达时会被唤醒,否则会一直进行阻塞
selector.select();
// 获取多个事件
Set<SelectionKey> selectionKeySet = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeySet.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isConnectable()) {
// 客户端与服务端的连接可用
handleConnectEvent(selectionKey);
} else if (selectionKey.isReadable()) {
// 客户端读取事件
handleReadEvent(selectionKey);
} else if (selectionKey.isWritable()){
System.out.println("\"write\" = " + "write");
}
iterator.remove();
}
}
}catch (Exception ex){
ex.printStackTrace();
}
}
private static void handleReadEvent(SelectionKey selectionKey) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
try {
socketChannel.read(byteBuffer);
System.out.println("client receive byteBuffer.array() = " +new String( byteBuffer.array()));
}catch (Exception ex){
ex.printStackTrace();
}
}
private static void handleConnectEvent(SelectionKey selectionKey) {
try{
SocketChannel socketChannel = (SocketChannel)selectionKey.channel();
if (socketChannel.isConnectionPending()){
// 如果连接还没有建立好那么先建立连接
socketChannel.finishConnect();
}
socketChannel.configureBlocking(false);
socketChannel.write(ByteBuffer.wrap(" I am Client2".getBytes(StandardCharsets.UTF_8)));
socketChannel.register(selector,SelectionKey.OP_READ);
}catch (Exception ex){
}
}
}
2.3 AIO 通信:通过信号量回调的方式真正实现异步
服务端:
package org.lgx.bluegrass.bluegrasscoree.util.selectorsocket.server;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.Charset;
/**
* @Description TODO
* @Date 2023/2/17 17:19
* @Author lgx
* @Version 1.0
*/
public class AioServer {
public static void main(String[] args) throws IOException {
AsynchronousServerSocketChannel ssc = AsynchronousServerSocketChannel.open();
ssc.bind(new InetSocketAddress(8080));
ssc.accept(null, new AcceptHandler(ssc));
System.in.read();
}
private static void closeChannel(AsynchronousSocketChannel sc) {
try {
System.out.printf("[%s] %s close\n", Thread.currentThread().getName(), sc.getRemoteAddress());
sc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private static class ReadHandler implements CompletionHandler<Integer, ByteBuffer> {
private final AsynchronousSocketChannel sc;
public ReadHandler(AsynchronousSocketChannel sc) {
this.sc = sc;
}
@Override
public void completed(Integer result, ByteBuffer attachment) {
try {
if (result == -1) {
closeChannel(sc);
return;
}
System.out.printf("[%s] %s read\n", Thread.currentThread().getName(), sc.getRemoteAddress());
attachment.flip();
System.out.println(Charset.defaultCharset().decode(attachment));
attachment.clear();
// 处理完第一个 read 时,需要再次调用 read 方法来处理下一个 read 事件
sc.read(attachment, attachment, this);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
closeChannel(sc);
exc.printStackTrace();
}
}
private static class WriteHandler implements CompletionHandler<Integer, ByteBuffer> {
private final AsynchronousSocketChannel sc;
private WriteHandler(AsynchronousSocketChannel sc) {
this.sc = sc;
}
@Override
public void completed(Integer result, ByteBuffer attachment) {
// 如果作为附件的 buffer 还有内容,需要再次 write 写出剩余内容
if (attachment.hasRemaining()) {
sc.write(attachment);
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
closeChannel(sc);
}
}
private static class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, Object> {
private final AsynchronousServerSocketChannel ssc;
public AcceptHandler(AsynchronousServerSocketChannel ssc) {
this.ssc = ssc;
}
@Override
public void completed(AsynchronousSocketChannel sc, Object attachment) {
try {
System.out.printf("[%s] %s connected\n", Thread.currentThread().getName(), sc.getRemoteAddress());
} catch (IOException e) {
e.printStackTrace();
}
ByteBuffer buffer = ByteBuffer.allocate(16);
// 读事件由 ReadHandler 处理
sc.read(buffer, buffer, new ReadHandler(sc));
// 写事件由 WriteHandler 处理
sc.write(Charset.defaultCharset().encode("server hello!"), ByteBuffer.allocate(16), new WriteHandler(sc));
// 处理完第一个 accpet 时,需要再次调用 accept 方法来处理下一个 accept 事件
ssc.accept(null, this);
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
}
}
3 总结:
通过bio 建立的socket 连接,并发的效率取决于线程池线程的大小;通过nio 使用select 轮询的方式可以实现非阻塞,这样大大提供了效率;通过aio 信号量回调的方式,真正实现非阻塞;
参考:
1 Java socket详解;