本节我们使用JDK中原生 NIO API来创建一个简单的TCP客户端与服务器交互的网络程序。
这个客户端功能是当客户端连接到服务端后,给服务器发送一个Hello,然后从套接字里面读取服务器端返回的内容并打印,具体代码如下:
public class NioClient {
// (1)创建发送和接受缓冲区
private static ByteBuffer sendbuffer = ByteBuffer.allocate(1024);
private static ByteBuffer receivebuffer = ByteBuffer.allocate(1024);
public static void main(String[] args) throws IOException {
// (2) 获取一个客户端socket通道
SocketChannel socketChannel = SocketChannel.open();
// (3)设置socket为非阻塞方式
socketChannel.configureBlocking(false);
// (4)获取一个选择器
Selector selector = Selector.open();
// (5)注册客户端socket到选择器
SelectionKey selectionKey = socketChannel.register(selector, 0);
// (6)发起连接
boolean isConnected = socketChannel.connect(new InetSocketAddress("127.0.0.1", 7001));
// (7)如果连接没有马上建立成功,则设置对链接完成事件感兴趣
if (!isConnected) {
selectionKey.interestOps(SelectionKey.OP_CONNECT);
}
int num = 0;
while (true) {
// (8) 选择已经就绪的网络IO操作,阻塞方法
int selectCount = selector.select();
System.out.println(num + "selectCount:" + selectCount);
// (9)返回已经就绪的通道的事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//(10)处理所有就绪事件
Iterator<SelectionKey> iterator = selectionKeys.iterator();
SocketChannel client;
while (iterator.hasNext()) {
//(10.1)获取一个事件,并从集合移除
selectionKey = iterator.next();
iterator.remove();
//(10.2)获取事件类型
int readyOps = selectionKey.readyOps();
//(10.3)判断是否是OP_CONNECT事件
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
//(10.3.1)等待客户端socket完成与服务器端的链接
client = (SocketChannel) selectionKey.channel();
if (!client.finishConnect()) {
throw new Error();
}
System.out.println("--- client already connected----");
//(10.3.2)设置要发送给服务端的数据
sendbuffer.clear();
sendbuffer.put("hello server,im a client".getBytes());
sendbuffer.flip();
//(10.3.3)写入输入。
client.write(sendbuffer);
//(10.3.4)设置感兴趣事件,读事件
selectionKey.interestOps(SelectionKey.OP_READ);
//(10.4)判断是否是OP_READ事件
} else if ((readyOps & SelectionKey.OP_READ) != 0) {
client = (SocketChannel) selectionKey.channel();
//(10.4.1)读取数据并打印
receivebuffer.clear();
int count = client.read(receivebuffer);
if (count > 0) {
String temp = new String(receivebuffer.array(), 0, count);
System.out.println(num++ + "receive from server:" + temp);
}
}
}
}
}
注:设置套接字为非阻塞后,connect方法会马上返回的,所以需要根据结果判断是否为链接建立OK了,如果没有成功,则需要设置对该套接字的op_connect事件感兴趣,在这个事件到来的时候还需要调用finishConnect方法来具体完成与服务器的链接,在finishConnect返回true后说明链接已经建立完成了,则这时候可以使用套接字通道发送数据到服务器,并且设置堆该套接字的op_read事件感兴趣,从而可以监听到服务端发来的数据,并进行处理。
服务端程序代码如下:
public class NioServer {
// (1) 缓冲区
private ByteBuffer sendbuffer = ByteBuffer.allocate(1024);
private ByteBuffer receivebuffer = ByteBuffer.allocate(1024);
private Selector selector;
public NioServer(int port) throws IOException {
// (2)获取一个服务器套接字通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// (3)socket为非阻塞
serverSocketChannel.configureBlocking(false);
// (4)获取与该通道关联的服务端套接字
ServerSocket serverSocket = serverSocketChannel.socket();
// (5)绑定服务端地址
serverSocket.bind(new InetSocketAddress(port));
// (6)获取一个选择器
selector = Selector.open();
// (7)注册通道到选择器,选择对OP_ACCEPT事件感兴趣
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("----Server Started----");
// (8)处理事件
int num = 0;
while (true) {
// (8.1)获取就绪的事件集合
int selectKeyCount = selector.select();
System.out.println(num++ + "selectCount:" + selectKeyCount);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// (8.2)处理就绪事件
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
processSelectedKey(selectionKey);
}
}
}
private void processSelectedKey(SelectionKey selectionKey) throws IOException {
SocketChannel client = null;
// (8.2.1)客户端完成与服务器三次握手
if (selectionKey.isAcceptable()) {
// (8.2.1.1)获取完成三次握手的链接套接字
ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
client = server.accept();
if (null == client) {
return;
}
System.out.println("--- accepted client---");
// (8.2.1.2)该套接字为非阻塞模式
client.configureBlocking(false);
// (8.2.1.3)注册该套接字到选择器,对OP_READ事件感兴趣
client.register(selector, SelectionKey.OP_READ);
// (8.2.2)为读取事件
} else if (selectionKey.isReadable()) {
// (8.2.2.1) 读取数据
client = (SocketChannel) selectionKey.channel();
receivebuffer.clear();
int count = client.read(receivebuffer);
if (count > 0) {
String receiveContext = new String(receivebuffer.array(), 0, count);
System.out.println("receive client info:" + receiveContext);
}
// (8.2.2.2)发送数据到client
sendbuffer.clear();
client = (SocketChannel) selectionKey.channel();
String sendContent = "hello client ,im server";
sendbuffer.put(sendContent.getBytes());
sendbuffer.flip();
client.write(sendbuffer);
System.out.println("send info to client:" + sendContent);
}
}
public static void main(String[] args) throws IOException {
int port = 7001;
NioServer server = new NioServer(port);
}
}
注:在这个例子里面监听套接字serverSocket和serverSocket接受到的所有链接套接字都注册到了同一个选择器上,其中processSelectedKey里面8.2.1是用来处理serverSocket接受的新链接的,8.2.2是用来处理链接套接字的读写的。
到这里服务端和客户端就搭建好了,首先启动服务器,然后运行客户端,会输入如下:
0selectCount:1
--- client already connected----
1selectCount:1
2receive from server:hello client ,im server
这时候服务器的输出结果为:
----Server Started----
0selectCount:1
--- accepted client---
1selectCount:1
receive client info:hello server,im a client
send info to client:hello client ,im server
简单分析下结果: