传统的网络编程,比如TCP的 socket.accept() 方法和UDP的 receive(packet) 方法都是具有阻塞功能的,所以属于同步阻塞网络编程。
ServerSocketChannel、SocketChannel可以实现非阻塞式网络编程。
ServerSocketChannel是一个基于通道的socket监听器,等同于ServerSocket类。
SocketChannel是一个基于通道的客户端套接字,等同于Socket类。
基本步骤
服务器端
1.创建ServerSocketChannel,服务器套接字
2.绑定地址
3.监听客户的套接字,创建连接
4.创建缓冲区ByteBuffer处理数据
5.关闭
客户端
1.创建SocketChannel
2.创建缓冲区ByteBuffer处理数据
3.发送数据
4.关闭
代码实现:
//服务器
public class TCPServer {
public static void main(String[] args) throws IOException {
//1.创建服务器端ServerSocketChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
//2.绑定地址和端口号,注意地址不是IP地址而是Socket的InetAddress地址
//SocektInetAddress = ip + port
ssc.bind(new InetSocketAddress(InetAddress.getByName("10.9.21.249"), 8848));
//3.监听客户端
System.out.println("服务器启动。。。。");
SocketChannel socketChannel = ssc.accept();
//4.处理数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (socketChannel.read(buffer) > 0) {
buffer.flip();
System.out.println("客户端说:" + new String(buffer.array(), 0, buffer.limit()));
buffer.clear();
}
//5.关闭
socketChannel.close();
ssc.close();
}
}
//客户端
public class TCPClient {
public static void main(String[] args) throws IOException {
//1.创建SocketChannel
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(InetAddress.getByName("10.9.21.249"), 8848));
//2.处理数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
Scanner input = new Scanner(System.in);
while (true) {
String data = input.nextLine();
buffer.put(data.getBytes());
//必须转模式,保证position指针归零,方便后面通道的读取
buffer.flip();
//3.发送数据
socketChannel.write(buffer);
buffer.clear();
if (data.equals("88")) {
break;
}
}
//4.关闭
input.close();
socketChannel.close();
}
}
要使用Selector,得向Selector注册Channel,然后调用它的select()方法。
这个方法会一直阻塞到某个注册的通道有事件就绪。
一旦这个方法返回,线程就可以处理这些事件,比如新连接进来的客户端,数据接收等。
选择器提供选择执行已经就绪的任务的能力.从底层来看,Selector提供了询问通道是否已经准备好执行每个I/O操作的能力。
Selector 允许单线程处理多个Channel。仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道,这样会大量的减少线程之间上下文切换的开销。但是不适合一个线程长时间操作
选择器(Selector): Selector选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。
可选择通道(SelectableChannel): SelectableChannel这个抽象类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。
因为FileChannel类没有继承SelectableChannel因此是不是可选通道,而所有socket通道都是可选择的,SocketChannel和ServerSocketChannel是SelectableChannel的子类,可以注册轮询器。
选择键(SelectionKey): 选择键封装了特定的通道与特定的选择器的注册关系。选择键对象被SelectableChannel.register()返回并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数的形式进行编码),选择键支持四种操作类型:
- Connect 连接
- Accept 接受请求(常用)
- Read 读(常用)
- Write 写
Java中定义了四个常量来表示这四种操作类型:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
//服务器
public class Server {
public static void main(String[] args) throws IOException {
//1.创建ServerSocektChannel
ServerSocketChannel listener = ServerSocketChannel.open();
//2.绑定
listener.bind(new InetSocketAddress(InetAddress.getLocalHost(), 7788));
//3.设置为非阻塞模式
listener.configureBlocking(false);
//4.轮询器启动
Selector selector = Selector.open();
//5.将服务器注册到轮询器,并未接收模式(就是图中的线程部分)
listener.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器已启动");
//6.开始轮询,这里处理的是事件,而不是客户端的SocketChannel
while (selector.select() > 0) {
//将所有的请求都加载到集合中,依次处理
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
while (it.hasNext()) {
//处理某一事件
SelectionKey selectionKey = it.next();
//事件分为对接请求事件和数据传输事件(对接之后的事件)
//处理请求,与BIO编程中的while(true)循环创建监听一样,但是轮询器会帮助服务器筛选,一个个对接处理
if (selectionKey.isAcceptable()) {
//服务器的监听就可以创建客户端的SocketChannel了,会一直存在
SocketChannel socketChannel = listener.accept();
//客户端的请求必须设置为非阻塞模式
socketChannel.configureBlocking(false);
//对接成功,设置成功,注册到轮询器,处理后续的数据传输,模式为读取
socketChannel.register(selector, SelectionKey.OP_READ);
//处理数据
} else if (selectionKey.isReadable()) {
//由于已经注册都轮询器上了,这里获取SocketChannel就必须通过SelectionKey获取,也就是从轮询器上搜索客户端的SocketChannel
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024 * 8);
int len = -1;
while ((len = socketChannel.read(buffer)) > 0) {
buffer.flip();
String data = new String(buffer.array(), 0, buffer.limit());
//这个地址是将SocketAddress转为InetSocketeAddress,然后getAddress获取ip地址
InetSocketAddress inetSocketAddress = (InetSocketAddress) socketChannel.getRemoteAddress();
System.out.println(inetSocketAddress.getAddress() + "说" + data);
//这个地址返回的是SocketAddress类型,包含的是Ip+端口号
//SocketAddress remoteAddress = socketChannel.getRemoteAddress();
//System.out.println(remoteAddress + "说" + data);
buffer.clear();
}
//如果读到了数据末尾,则是-1,必须关闭通道,因为客户端很多,不可能最后统一关闭通道
//但已经注册的SocketChannel一直在,只是关闭了管道,后面如果有数据事件,则直接重启
if (-1 == len) {
socketChannel.close();
}
}
//处理完毕之后,将该事件删除,继续处理后面的事件
it.remove();
}
}
}
}
//客户端,基本没有变化,就是必须转为非阻塞模式
public class Client {
public static void main(String[] args) throws IOException {
//1.创建SocketChannel
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(InetAddress.getLocalHost(), 7788));
//2.设置为非阻塞
socketChannel.configureBlocking(false);
//3.发送数据
Scanner input = new Scanner(System.in);
ByteBuffer buffer = ByteBuffer.allocate(1024 * 8);
while (true) {
String data = input.nextLine();
buffer.put(data.getBytes());
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
if (data.equals("88")) {
break;
}
}
//4.关闭
input.close();
socketChannel.close();
}
}