Java使用NIO实现Socket通信

一、介绍

在上次的博客中,已经了解到NIO当中最为重要的两个对象。分别是缓冲Buffer和通道Channel,也进行了基本的使用,不过使用的是FileChannel,主要用来与文件打交道。

那么,这一次使用NIO实现Socket网络通信,主要是使用到ServerSocketChannelSocketChannel

同样,在本次作为NIO的网络通信,建议先了解传统BIO的网络通信,传送门在此

二、实现

1)服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package com.banmoon.test;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;

@Slf4j
public class TestSocketServer {

public static void main(String[] args) throws IOException {
// 开启通道,得到一个ServerSocketChannel对象
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 得到一个选择器
Selector selector = Selector.open();
// 绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(2333));
// 设置为非阻塞
serverSocketChannel.configureBlocking(false);

// 将socketChannel注册到selector上,事件为OP_ACCEPT,接收事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

// 循环等待客户端连接
while (true) {
// 服务器等待2秒,如果没有接收事件发生,将进入下次循环
if (selector.select(2000) == 0) {
log.info("服务器已等待2秒,继续静默等待");
continue;
}

// 如果有事件发成,将会进入此逻辑,获取到相关的SelectionKey集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();

// 遍历这个集合
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 判断是否发生了OP_ACCEPT事件
if (selectionKey.isAcceptable()) {
// 获取到SocketChannel通道。
// 此方法虽然是阻塞的,但由于上面基于事件的判断,到这一步时,连接就已经产生了,所以不会阻塞
SocketChannel socketChannel = serverSocketChannel.accept();
// 将socketChannel设置为非阻塞
socketChannel.configureBlocking(false);
// 将上面的socketChannel注册到selector选择器上,事件注册为OP_READ,读取事件
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}

// 判断是否发生了OP_READ事件
if (selectionKey.isReadable()) {
// 通过选择器获取到发生了OP_READ事件的SocketChannel
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
// 获取到该socketChannel关联的缓冲
ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
// 读取通道中的数据
socketChannel.read(byteBuffer);
// 反转缓冲,进行读取
byteBuffer.flip();
log.info("服务端接受到数据:{}", new String(byteBuffer.array(), 0, byteBuffer.limit(), StandardCharsets.UTF_8));
}

// 删除当前的selectionKey,避免重复执行
iterator.remove();
}
}

}

}

2)客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.banmoon.test;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@Slf4j
public class TestSocketClient {

public static void main(String[] args) throws IOException, InterruptedException {
// 开启通道,得到一个SocketChannel对象
SocketChannel socketChannel = SocketChannel.open();
// 设置为非阻塞
socketChannel.configureBlocking(false);
// 创建一个服务器端地址信息的对象
InetSocketAddress netAddress = new InetSocketAddress("127.0.0.1", 2333);
// 连接服务器,如果没有连接成功将进入循环
if (!socketChannel.connect(netAddress)) {
// 循环判断是否完成连接,阻塞方法,2秒后释放
while (!socketChannel.finishConnect()) {
log.info("与服务器连接中,请稍后");
TimeUnit.SECONDS.sleep(1);
}
}
log.info("连接服务器成功");
// 当执行到此处时,说明与服务器的连接已经成功,发送数据
String str = "hello,半月无霜";
ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes(StandardCharsets.UTF_8));
// 写入通道
socketChannel.write(byteBuffer);
// 避免通道关闭阻塞在此
CountDownLatch countDownLatch = new CountDownLatch(1);
countDownLatch.await();
}

}

3)测试

先启用服务端,再启用客户端,客户端可以启用多个

或者也可以先启用客户端,再启用服务端

服务端 客户端
image-20220702234733427 image-20220705231125252

三、最后

实际上,这只是简单的一个应用,后续复杂的都是基于此简单的服务、客户端进行展开。继续加油!

我是半月,祝你幸福!!!