持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第27天,点击查看活动详情
Java网络编程(二)
前言
上一篇文章 Java网络编程(一) 简单介绍了Java中Socket的使用, 我们可以发现其弊端非常明显, 就是一次只能处理一个连接, 属于同步阻塞IO(BIO), 我们可以通过开线程的方式来处理多连接请求(早期的tomcat就是这么玩的)
多线程版
在之前的代码基础上稍微改造一下, 改为多线程处理, 这样就能支持并发请求了
private final static ExecutorService executorService = Executors.newFixedThreadPool(16);
public static void main(String[] args) throws Exception {
// 绑定端口
ServerSocket server = new ServerSocket(8080);
while (true) {
// 阻塞等待接收连接请求
Socket accept = server.accept();
executorService.execute(() -> execute(accept));
}
}
@SneakyThrows
private static void execute(Socket accept) {
try (InputStream in = accept.getInputStream(); OutputStream out = accept.getOutputStream()) {
byte[] bytes = new byte[1024];
int len;
while ((len = in.read(bytes)) != -1) {
System.out.println(new String(bytes, 0, len));
renderJson(out, "ha");
break;
}
}
}
@SneakyThrows
private static void renderJson(OutputStream out, Object res) {
out.write(write(res));
}
private static byte[] write(Object res){
String jsonStr = JSONObject.toJSONString(res);
return ("HTTP/1.1 200 \r\n" +
"Content-Type:application/json;charset=utf-8;" +
"Content-Length:" + jsonStr.getBytes().length + " \r\n\r\n" +
jsonStr).getBytes();
}
但是这样的弊端也显而易见, 线程资源是非常宝贵的, 对于应用程序来说开线程也是非常重的操作, 即使使用了线程池来维护线程资源, 也会带来其他的一些问题
NIO 版
Java1.4后提供了NIO API, 全称java non-blocking IO, 目前高性能的网络服务通常基于NIO构建, 我们来尝试一下
private static Selector selector = null;
public static void main(String[] args) throws Exception {
// 初始化socket
ServerSocketChannel ssc = ServerSocketChannel.open();
// 绑定端口
ssc.bind(new InetSocketAddress(8080));
// 设置非阻塞
ssc.configureBlocking(false);
// 初始化多路复用器
selector = Selector.open();
// 注册多路复用器, 监听accept操作
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 轮询多路复用器
while (selector.select() > 0) {
// 获取多路复用器返回的有效的SelectionKeys迭代器
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey sk = it.next();
// 不会自动删除, 手动删除SelectionKey
it.remove();
if (sk.isAcceptable()) {
// 连接操作
acceptHandler(sk);
} else if (sk.isReadable()) {
// 读操作
readHandler(sk);
} else if (sk.isWritable()) {
// 写操作
writeHandler(sk);
}
}
}
}
}
@SneakyThrows
private static void acceptHandler(SelectionKey key) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
// 获取客户端连接
SocketChannel client = ssc.accept();
// 设置非阻塞
client.configureBlocking(false);
// 分配ByteBuffer缓冲区
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
// 注册多路复用器, 监听read操作, 并分配buffer
client.register(selector, SelectionKey.OP_READ, buffer);
}
@SneakyThrows
private static void readHandler(SelectionKey key) {
// 获取SelectionKey上的channel
SocketChannel sc = (SocketChannel) key.channel();
// 获取分配的buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
// 清空buffer
buffer.clear();
while (true) {
int len;
String content = "";
// 读取请求数据
while ((len = sc.read(buffer)) > 0) {
// 翻转buffer, 指的是翻转数据读写操作的指针
buffer.flip();
while (buffer.hasRemaining()) {
byte[] bytes = new byte[buffer.limit()];
// 取出buffer中的数据
buffer.get(bytes);
content = new String(bytes);
}
}
// 等于-1手动关闭通道, 否则会出现空轮询
if (len < 0) {
sc.close();
}
buffer.clear();
sc.configureBlocking(false);
buffer.put(SocketDemo.write("ha"));
// 注册多路复用器, 监听write操作, 并分配buffer
sc.register(selector, SelectionKey.OP_WRITE, buffer);
break;
}
}
@SneakyThrows
private static void writeHandler(SelectionKey key) {
// 获取SelectionKey上的channel
SocketChannel sc = (SocketChannel) key.channel();
// 获取分配的buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
// 翻转
buffer.flip();
// 写服务器响应数据
sc.write(buffer);
buffer.clear();
// 关闭连接
sc.close();
}
基于NIO构建的网络服务, 即使是单线程也能提供不错的吞吐量, 主要得益于Selector(多路复用器)和DirectByteBuffer(字节缓冲区堆外内存), 对比代码可以看出, 基于NIO构建网络服务远比BIO复杂的多,
后续我们介绍下使用Netty来构建网络服务, 体验一下Netty对Java NIO的优雅的封装