如何用java.nio包中的类来创建客户程序EchoClient,本节提供了两种实现方式:
- 采用阻塞模式,单线程。
- 采用非阻塞模式,单线程。
1 采用阻塞模式,单线程
这个已经在服务端代码Demo(NIO)中提到了,代码如下:
package study.wyy.net.nio.client;
import lombok.Builder;
import lombok.extern.slf4j.Slf4j;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.channels.SocketChannel;
/**
* @author wyaoyao
* @date 2021/3/17 17:59
*/
@Slf4j
public class BlockEchoClient {
private final SocketChannel socketChannel;
private final String serverHost;
private final int serverPort;
public BlockEchoClient(String serverHost, int serverPort) throws IOException {
this.serverHost = serverHost;
this.serverPort = serverPort;
this.socketChannel = SocketChannel.open();
// 连接服务器
SocketAddress remote = new InetSocketAddress(serverHost, serverPort);
socketChannel.connect(remote);
log.info("connect echo server success");
}
public void send(String message) {
try {
BufferedReader reader = getReader(socketChannel.socket());
PrintWriter writer = getWriter(socketChannel.socket());
// 发送数据
writer.println(message);
log.info("send request success; content is {}", message);
// 读取服务端的响应
String s1 = reader.readLine();
log.info("get response success; response is {}", s1);
} catch (Exception e) {
e.printStackTrace();
}
}
public void close() throws IOException {
if(socketChannel != null){
socketChannel.close();
}
}
public BufferedReader getReader(Socket socket) throws IOException {
InputStream inputStream = socket.getInputStream();
return new BufferedReader(new InputStreamReader(inputStream));
}
public PrintWriter getWriter(Socket socket) throws IOException {
return new PrintWriter(socket.getOutputStream(), true);
}
}
调用了 socketChannel.connect(remote);
方法连接远程服务器,该方法在阻塞模式下,将等到与远程服务器的连接成功建立之后才返回。
在send方法中,通过socketChannel.socket()
获取与socketChannel关联的Socket对象,然后从Socket对象中获取输入和输出流,进行数据的发送和接收。
2 采用非阻塞模式,单线程
对于客户与服务器之间的通信,按照它们收发数据的协调程度来区分,可分为同步通信和异步通信:
- 同步通信是指甲方向乙方发送了一批数据后,必须等接收到了乙方的响应数据后,再发送下一批数据。
- 异步通信是指发送数据和接收数据的操作互不干扰,各自独立进行。
同步通信要求一个I/O操作完成之后,才能完成下一个I/O操作,用阻塞模式更容易实现它。异步通信允许发送数据和接收数据的操作各自独立进行,用非阻塞模式更容易实现它。
上面的就是一个同步通信,每当客户端发送一条数据之后,都必须等待响应数据才会发送下一条数据。
现在就使用非阻塞模式,来实现异步通信。
package study.wyy.net.nio.client;
import lombok.extern.slf4j.Slf4j;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
/**
* @author wyaoyao
* @date 2021/3/17 17:59
*/
@Slf4j
public class NoBlockEchoClient {
private final SocketChannel socketChannel;
private final String serverHost;
private final int serverPort;
private final Charset charset = Charset.forName("UTF-8");
private ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
private ByteBuffer responseBuffer = ByteBuffer.allocate(1024);
/**
* 委托给Selector来负责接收连接就绪事件,读就绪事件,写就绪事件
*/
private final Selector selector;
private final Object LOCK = new Object();
private Thread sendThread = new Thread(() -> {
try {
send();
} catch (IOException e) {
e.printStackTrace();
}
});
public NoBlockEchoClient(String serverHost, int serverPort) throws IOException {
this.serverHost = serverHost;
this.serverPort = serverPort;
this.socketChannel = SocketChannel.open();
// 连接服务器
SocketAddress remote = new InetSocketAddress(serverHost, serverPort);
socketChannel.connect(remote);
// 设置socketChannel为非阻塞模式
socketChannel.configureBlocking(false);
log.info("connect echo server success");
// 创建一个Selector对象
this.selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
sendThread.start();
}
public void submit(String message) throws IOException {
// 编码,在用户提交的数据上追加一个结束符号,这里就以换行符吧
log.info("submit :{}",message);
ByteBuffer encode = encode(message + "\r\n");
// 将要发送的内容添加到sendBuffer中
synchronized (LOCK) {
sendBuffer.put(encode);
}
}
public void send() throws IOException {
while (true) {
// 需要加锁,因为selector.select();这个方法时操作的selector的all-keys集合
int select = selector.select();
if (select == 0) {
continue;
}
// 获取已经捕获到的事件的SelectionKey的数量
// 数量大于0,捕获到事件发生 取出捕获的事件
Set<SelectionKey> readyKeys = selector.selectedKeys();
// 遍历事件,进行处理
Iterator<SelectionKey> iterator = readyKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = null;
try {
key = iterator.next();
iterator.remove();
if (key.isReadable()) {
// 读取服务响应结果
receive(key);
}
if (key.isWritable()) {
// 发送数据给服务端
sendMessage(key);
}
} catch (Exception e) {
e.printStackTrace();
if (key != null) {
key.cancel();
key.channel().close();
}
}
}
}
}
/**
* 读取服务端数据
*
* @param key
*/
private void receive(SelectionKey key) throws IOException {
// 获取SelectionKey关联的SocketChannel
SocketChannel channel = (SocketChannel) key.channel();
// 服务端的数据读取到responseBuffer中,如果responseBuffer中的数据有一行数据
// 就输出这一行数据,并删除这部分数据
channel.read(responseBuffer);
// 把极限设置为位置,位置设置为0
responseBuffer.flip();
// 解码
String response = decode(responseBuffer);
if (response.indexOf("\n") == -1) {
// 不足一行,就返回,等待下次
return;
}
// 截取出一行数据
String line = response.substring(0, response.indexOf("\n") + 1);
log.info("get response success; response is {}", line);
ByteBuffer temp = encode(line);
responseBuffer.position(temp.limit());
responseBuffer.compact();
}
/**
* 从sendBuffer缓冲中获取数据,向服务端发送数据
*
* @param key
*/
private void sendMessage(SelectionKey key) throws IOException {
// 获取SelectionKey关联的SocketChannel
SocketChannel channel = (SocketChannel) key.channel();
synchronized (LOCK) {
// 把极限设置为位置,位置设置为0
sendBuffer.flip();
// 发送sendBuffer中的数据
channel.write(sendBuffer);
// 删除已经发送的数据
sendBuffer.compact();
}
}
private ByteBuffer encode(String content) {
ByteBuffer encode = charset.encode(content);
return encode;
}
/**
* 解码
*
* @param byteBuffer
* @return
*/
private String decode(ByteBuffer byteBuffer) {
CharBuffer decode = charset.decode(byteBuffer);
return decode.toString();
}
}
测试:
public static void main(String[] args) throws IOException {
NoBlockEchoClient client = null;
try {
client = new NoBlockEchoClient("localhost", 10010);
client.submit("hello! from " + Thread.currentThread().getName());
client.submit("你好! from " + Thread.currentThread().getName());
client.submit("bye! from " + Thread.currentThread().getName());
} catch (IOException e) {
e.printStackTrace();
} finally {
}
}