Java NIO之套接字通道

简介: 1.简介前面一篇文章讲了文件通道,本文继续来说说另一种类型的通道 -- 套接字通道。在展开说明之前,咱们先来聊聊套接字的由来。套接字即 socket,最早由伯克利大学的研究人员开发,所以经常被称为Berkeley sockets。

1.简介

前面一篇文章讲了文件通道,本文继续来说说另一种类型的通道 -- 套接字通道。在展开说明之前,咱们先来聊聊套接字的由来。套接字即 socket,最早由伯克利大学的研究人员开发,所以经常被称为Berkeley sockets。UNIX 4.2BSD 内核版本中加入了 socket 的实现,此后,很多操作系统都提供了自己的 socket 接口实现。通过 socket 接口,我们就可以与不同地址的计算机实现通信。

如果大家使用过 Unix/Linux 系统下的 socket 接口,那么对 socket 编程的过程应该有一些了解。对于 TCP 服务端,接口调用的顺序为socket() -> bind() -> listen() -> accept() -> 其他操作 -> close(),客户端的顺序为socket() -> connect() -> 其他操作 -> close()。如下图所示:

img_a841d753042ee4de872cf5d6a983f769.jpe
* 图片来源于《深入理解计算机系统》

如上所示,直接调用操作系统 socket 相关接口还是比较麻烦的。所以我们的 Java 语言对上面的步骤进行了封装,方便使用。比如我们今天要讲的套接字通道就比原生的接口好用的多。好了,关于 socket 的简介先说到这,接下进入正题吧。

2 通道类型

Java 套接字通道包含三种类型,分别是

类型 说明
DatagramChannel UDP 网络套接字通道
SocketChannel TCP 网络套接字通道
ServerSocketChannel TCP 服务端套接字通道

Java 套接字通道类型对应于两种通信协议 TCP 和 UDP,这个大家应该都知道。本文将介绍 TCP 网络套接字通道的使用,并在最后实现一个简单的聊天功能。至于 UDP 类型的通道,大家可以自己看看。

3.基本操作

3.1 打开通道

SocketChannel 和 ServerSocketChannel 都是抽象类,所以不能直接通过构造方法创建通道。这两个类均是使用 open 方法创建通道,如下:

SocketChannel socketChannel = SocketChannel.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

3.2 关闭通道

SocketChannel 和 ServerSocketChannel 均提供了 close 方法,用于关闭通道。示例如下:

SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("www.coolblog.xyz", 80));
// do something...
socketChannel.close();

/*******************************************************************/

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
SocketChannel socketChannel = serverSocketChannel.accept();
// do something...
socketChannel.close();
serverSocketChannel.close();

3.3 读写操作

读操作

通过使用 SocketChannel 的 read 方法,并配合 ByteBuffer 字节缓冲区,即可以从 SocketChannel 中读取数据。示例如下:

ByteBuffer buffer = ByteBuffer.allocate(32);
int num = socketChannel.read(buffer);

写操作

读取数据使用的是 read 方法,那么写入自然也就是 write 方法了。NIO 通道是面向缓冲的,所以向管道中写入数据也需要和缓冲区配合才行。示例如下

String data = "Test data..."

ByteBuffer buffer = ByteBuffer.allocate(32);
buffer.clear();
buffer.put(data.getBytes());

bbuffer.flip();
channel.write(buffer);

3.4 非阻塞模式

与文件通道不同,套接字通道可以运行在非阻塞模式下。在此模式下,调用 connect(),read() 和 write() 等方法时,进程/线程会立即返回。设置非阻塞模式的方法为configureBlocking,我们来看一下该方法的使用示例:

SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("www.coolblog.xyz", 80));

// 这里要循环检测是否已经连接上
while(!socketChannel.finishConnect()){
    // do something
}

// 连接建立起来后,才能进行读取或写入操作

由于在非阻塞模式下,调用 connect 方法会立即返回。如果在连接未建立起来的情况下,从管道中读取,或向管道写入数据,会触发 NotYetConnectedException 异常。所以要进行循环检测,以保证连接完成建立。如果代码按照上面那样去写,会引发另外一个问题。非阻塞模式虽然不会阻塞线程,但是在方法返回后,还要进行循环检测,线程实际上还是被阻塞。出现这个问题的原因是和 Java NIO 套接字通道的 IO 模型有关,套接字通道采用的是“同步非阻塞”式 IO 模型,用户发起一个 IO 操作后,即可去做其他事情,不用等待 IO 完成。但是 IO 是否已完成,则需要用户自己时不时的去检测,这样实际上还是会浪费 CPU 资源。

关于 IO 模型相关的知识,大家可以参考我之前的一篇文章I/O模型简述 ,这里不再赘述。另外,大家还需要去参考一下权威资料《UNIX网络编程卷 第1卷:套接口API》第6章关于 IO 模型的介绍,那一章除了对5种 IO 模型进行了介绍,还介绍了同步与异步的概念,值得一读。好了,本节就先说到这里。

3.5 实例演示

本节用一个简单的例子来演示套接字通道的使用,这个例子演示了一个客户端与服务端互相聊天的场景。首先服务端会监听某个端口,等待客户端来连接。客户端连接后,由客户端先向服务端发送消息,然后服务端再回复一条消息。这样,客户端和服务端就能你一句我一句的聊起来了。背景先介绍到这,我们来看看代码实现吧,首先看看服务端的代码:

package wetalk;

import static wetalk.WeTalkUtils.recvMsg;
import static wetalk.WeTalkUtils.sendMsg;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Scanner;

/**
 * WeTalk 服务端
 * @author coolblog.xyz
 * @date 2018-03-22 12:43:26
 */
public class WeTalkServer {

    private static final String EXIT_MARK = "exit";

    private int port;

    WeTalkServer(int port) {
        this.port = port;
    }

    public void start() throws IOException {
        // 创建服务端套接字通道,监听端口,并等待客户端连接
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.socket().bind(new InetSocketAddress(port));
        System.out.println("服务端已启动,正在监听 " + port + " 端口......");
        SocketChannel channel = ssc.accept();
        System.out.println("接受来自" + channel.getRemoteAddress().toString().replace("/", "") + " 请求");

        Scanner sc = new Scanner(System.in);
        while (true) {
            // 等待并接收客户端发送的消息
            String msg = recvMsg(channel);
            System.out.println("\n客户端:");
            System.out.println(msg + "\n");

            // 输入信息
            System.out.println("请输入:");
            msg = sc.nextLine();
            if (EXIT_MARK.equals(msg)) {
                sendMsg(channel, "bye~");
                break;
            }

            // 回复客户端消息
            sendMsg(channel, msg);
        }
        
        // 关闭通道
        channel.close();
        ssc.close();
    }

    public static void main(String[] args) throws IOException {
        new WeTalkServer(8080).start();
    }
}

上面的代码基本上进行了逐步注释,应该不难理解,这里就不啰嗦了。上面有两个方法没有贴代码,就是sendMsgrecvMsg,由于通用操作,在下面的客户端代码里也可以使用,所以这里做了封装。封装代码如下:

package wetalk;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * 工具类
 *
 * @author coolblog.xyz
 * @date 2018-03-22 13:13:41
 */
public class WeTalkUtils {

    private static final int BUFFER_SIZE = 128;

    public static void sendMsg(SocketChannel channel, String msg) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
        buffer.put(msg.getBytes());
        buffer.flip();
        channel.write(buffer);
    }

    public static String recvMsg(SocketChannel channel) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
        channel.read(buffer);

        buffer.flip();
        byte[] bytes = new byte[buffer.limit()];
        buffer.get(bytes);
        return new String(bytes);
    }
}

工具类的代码比较简单,没什么好说的。接下来再来看看客户端的代码。

package wetalk;

import static wetalk.WeTalkUtils.recvMsg;
import static wetalk.WeTalkUtils.sendMsg;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.Scanner;

/**
 * WeTalk 客户端
 * @author coolblog.xyz
 * @date 2018-03-22 12:38:21
 */
public class WeTalkClient {

    private static final String EXIT_MARK = "exit";

    private String hostname;

    private int port;

    WeTalkClient(String hostname, int port) {
        this.hostname = hostname;
        this.port = port;
    }

    public void start() throws IOException {
        // 打开一个套接字通道,并向服务端发起连接
        SocketChannel channel = SocketChannel.open();
        channel.connect(new InetSocketAddress(hostname, port));

        Scanner sc = new Scanner(System.in);
        while (true) {
            // 输入信息
            System.out.println("请输入:");
            String msg = sc.nextLine();
            if (EXIT_MARK.equals(msg)) {
                sendMsg(channel, "bye~");
                break;
            }
            
            // 向服务端发送消息
            sendMsg(channel, msg);
            
            // 接受服务端返回的消息
            msg = recvMsg(channel);
            System.out.println("\n服务端:");
            System.out.println(msg + "\n");
        }
        
        // 关闭通道
        channel.close();
    }

    public static void main(String[] args) throws IOException {
        new WeTalkClient("localhost", 8080).start();
    }
}

客户端做的事情也比较简单,首先是打开通道,然后连接服务单。紧接着进入 while 循环,然后就可以和服务端愉快的聊天了。

上面的代码和叙述都没啥意思,最后我们还是来看看上面代码的运行效果,一图胜前言。

img_b3bb7e9e1a837aa7d6c6abae074e468e.gif

4.总结

到这里,关于套接字通道的相关内容就讲完了,不知道大家有没有看懂。本文仅从使用的角度分析了套接字通道的用法,至于套接字通道的实现,这并不是本文关注的重点。实际上,我在上一篇文章中就说过,Java 所提供的很多类实际上是对操作系统层面上一些系统调用做了一层包装。所以大家在学习 Java 的同时,还应该去了解底层的一些东西,这样才算是知其然,又知其所以然。

好了,本文到这里就结束了,有错误的地方欢迎大家指出来。最后谢谢大家的阅读,祝周末愉快。

参考

本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处
作者:coolblog
本文同步发布在我的个人博客:http://www.coolblog.xyz/?r=cb

cc
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。

目录
相关文章
|
4月前
|
Java 大数据
解析Java中的NIO与传统IO的区别与应用
解析Java中的NIO与传统IO的区别与应用
|
6天前
|
Java
[Java]Socket套接字(网络编程入门)
本文介绍了基于Java Socket实现的一对一和多对多聊天模式。一对一模式通过Server和Client类实现简单的消息收发;多对多模式则通过Server类维护客户端集合,并使用多线程实现实时消息广播。文章旨在帮助读者理解Socket的基本原理和应用。
12 1
|
17天前
|
Java
让星星⭐月亮告诉你,Java NIO之Buffer详解 属性capacity/position/limit/mark 方法put(X)/get()/flip()/compact()/clear()
这段代码演示了Java NIO中`ByteBuffer`的基本操作,包括分配、写入、翻转、读取、压缩和清空缓冲区。通过示例展示了`position`、`limit`和`mark`属性的变化过程,帮助理解缓冲区的工作原理。
21 2
|
2月前
|
存储 网络协议 Java
Java NIO 开发
本文介绍了Java NIO(New IO)及其主要组件,包括Channel、Buffer和Selector,并对比了NIO与传统IO的优势。文章详细讲解了FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel及Pipe.SinkChannel和Pipe.SourceChannel等Channel实现类,并提供了示例代码。通过这些示例,读者可以了解如何使用不同类型的通道进行数据读写操作。
Java NIO 开发
|
3月前
|
Java
"揭秘Java IO三大模式:BIO、NIO、AIO背后的秘密!为何AIO成为高并发时代的宠儿,你的选择对了吗?"
【8月更文挑战第19天】在Java的IO编程中,BIO、NIO与AIO代表了三种不同的IO处理机制。BIO采用同步阻塞模型,每个连接需单独线程处理,适用于连接少且稳定的场景。NIO引入了非阻塞性质,利用Channel、Buffer与Selector实现多路复用,提升了效率与吞吐量。AIO则是真正的异步IO,在JDK 7中引入,通过回调或Future机制在IO操作完成后通知应用,适合高并发场景。选择合适的模型对构建高效网络应用至关重要。
67 2
|
3月前
|
网络协议 C# 开发者
WPF与Socket编程的完美邂逅:打造流畅网络通信体验——从客户端到服务器端,手把手教你实现基于Socket的实时数据交换
【8月更文挑战第31天】网络通信在现代应用中至关重要,Socket编程作为其实现基础,即便在主要用于桌面应用的Windows Presentation Foundation(WPF)中也发挥着重要作用。本文通过最佳实践,详细介绍如何在WPF应用中利用Socket实现网络通信,包括创建WPF项目、设计用户界面、实现Socket通信逻辑及搭建简单服务器端的全过程。具体步骤涵盖从UI设计到前后端交互的各个环节,并附有详尽示例代码,助力WPF开发者掌握这一关键技术,拓展应用程序的功能与实用性。
98 0
|
4月前
|
安全 Java Linux
(七)Java网络编程-IO模型篇之从BIO、NIO、AIO到内核select、epoll剖析!
IO(Input/Output)方面的基本知识,相信大家都不陌生,毕竟这也是在学习编程基础时就已经接触过的内容,但最初的IO教学大多数是停留在最基本的BIO,而并未对于NIO、AIO、多路复用等的高级内容进行详细讲述,但这些却是大部分高性能技术的底层核心,因此本文则准备围绕着IO知识进行展开。
147 1
|
3月前
|
存储 网络协议 Java
【Netty 神奇之旅】Java NIO 基础全解析:从零开始玩转高效网络编程!
【8月更文挑战第24天】本文介绍了Java NIO,一种非阻塞I/O模型,极大提升了Java应用程序在网络通信中的性能。核心组件包括Buffer、Channel、Selector和SocketChannel。通过示例代码展示了如何使用Java NIO进行服务器与客户端通信。此外,还介绍了基于Java NIO的高性能网络框架Netty,以及如何用Netty构建TCP服务器和客户端。熟悉这些技术和概念对于开发高并发网络应用至关重要。
63 0
|
3月前
|
Java 调度
|
4月前
|
安全 Java
【Java】已解决java.nio.channels.OverlappingFileLockException异常
【Java】已解决java.nio.channels.OverlappingFileLockException异常
104 1