三、使用 Java NIO 搭建简单的客户端与服务端实现网络通讯
本节我们使用JDK中原生 NIO API来创建一个简单的TCP客户端与服务器交互的网络程序。
3.1 客户端程序
这个客户端功能是当客户端连接到服务端后,给服务器发送一个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);
}
}
}
}
}
-
代码(1)分别创建了一个发送和接受buffer,用来发送数据时候byte化内容和接受数据。
-
代码(2)获取一个客户端套接字通道。
-
代码(3)设置socket通道为非阻塞模式,默认是阻塞模式。
-
代码(4)(5)获取一个选择器,然后注册客户端套接字通道到该选择器,并且设置感兴趣的事情为0,就是不对任何事件感兴趣。
-
代码(6)(7)调用套接字通道的connect方法,连接服务器(服务器套接字地址为127.0.0.1:7001),由于步骤(3)设置了为非阻塞,所以步骤(6)马上会返回。代码(7)判断连接是否已经完成,如果没有,则设置选择器去监听OP_CONNECT事件,也就是指明对该事件感兴趣。
-
然后进入while循环进行事件处理,其中代码(8)选择已经就绪的网络IO事件,如果当前没有就绪的则阻塞当前线程。当有就绪事件后,会返回获取的事件个数,会执行代码(9)具体取出来具体事件列表。
-
代码(10)循环处理所有就绪事件,代码(10.1)迭代出一个事件key,然后从集合中删除,代码(10.2)获取事件key感兴趣的标志,代码(10.3)则看兴趣集合里面是否有OP_CONNECT,如果有则说明有OP_CONNECT事件已经就绪了,那么执行步骤(10.3.1)等待客户端与服务端完成三次握手,然后步骤(10.3.2)(10.3.3)写入hello server,im a client到服务器端。然后代码(10.3.4)设置对OP_READ事件感兴趣。
-
代码(10.4)则看如果当前事件key是OP_READ事件,说明服务器发来的数据已经在接受buffer就绪了,客户端可以去具体拿出来了,然后代码10.4.1从客户端套接字里面读取数据并打印。
注:设置套接字为非阻塞后,connect方法会马上返回的,所以需要根据结果判断是否为链接建立OK了,如果没有成功,则需要设置对该套接字的op_connect事件感兴趣,在这个事件到来的时候还需要调用finishConnect方法来具体完成与服务器的链接,在finishConnect返回true后说明链接已经建立完成了,则这时候可以使用套接字通道发送数据到服务器,并且设置堆该套接字的op_read事件感兴趣,从而可以监听到服务端发来的数据,并进行处理。
3.2 服务端程序
服务端程序代码如下:
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);
}
}
- 代码(1)分别创建了一个发送和接受buffer,用来发送数据时候byte化内容,和接受数据。
- 代码(2)获取一个服务端监听套接字通道。
- 代码(3)设置socket通道为非阻塞模式,默认是阻塞模式。
- 代码(4)获取与该通道关联的服务端套接字
- 代码(5)绑定服务端套接字监听端口为7001
- 代码(6)(7) 获取一个选择器,并注册通道到选择器,选择对OP_ACCEPT事件感兴趣,到这里服务端已经开始监听客户端链接了。
- 代码(8) 具体处理事件,8.1选择当前就绪的事件,8.2遍历所有就绪事件,顺序调用processSelectedKey进行处理。
- 代码(8.2.1) 当前事件key对应的OP_ACCEPT事件,则执行代码8.2.1.1获取已经完成三次握手的链接套接字,并通过代码8.2.1.2设置该链接套接字为非阻塞模式,通过代码8.2.1.3注册该链接套接字到选择器,并设置对对OP_READ事件感兴趣。
- 代码(8.2.2) 判断如果当前事件key为OP_READ则通过代码8.2.2.1链接套接字里面获取客户端发来的数据,通过代码8.2.2.2发送数据到客户端。
注:在这个例子里面监听套接字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
简单分析下结果:
-
服务器端启动后,会先输出----Server Started----
-
客户端启动后去链接服务器端,三次握手完毕后,服务器会获取op_accept事件,会通过accept获取链接套接字,所以输出了:
0selectCount:1
--- accepted client--- -
然后客户端接受到三次握手信息后,获取到了op_connect事件,所以输出:
0selectCount:1
--- client already connected----
然后发送数据到服务器端 -
服务端收到数据后,选择器会选择出op_read事件,读取客户端发来的内容,并发送回执到客户端:
1selectCount:1
receive client info:hello server,im a client
send info to client:hello client ,im server -
客户端收到服务器端回执后,选择器会选择出op_read事件,所以客户端会读取服务器端发来的内容,所以输出:
1selectCount:1
2receive from server:hello client ,im server
最后
想了解JDK NIO和更多Netty基础的可以单击我
更多关于分布式系统中服务降级策略的知识可以单击 单击我
想系统学dubbo的单击我
想学并发的童鞋可以 单击我
网友评论