Java NIO之套接字通道
1.简介
前面一篇文章讲了文件通道,本文继续来说说另一种类型的通道 -- 套接字通道。在展开说明之前,咱们先来聊聊套接字的由来。套接字即 socket,最早由伯克利大学的研究人员开发,所以经常被称为Berkeley sockets。UNIX 4.2BSD 内核版本中加入了 socket 的实现,此后,很多操作系统都提供了自己的 socket 接口实现。通过 socket 接口,我们就可以与不同地址的计算机实现通信。
如果大家使用过 Unix/Linux 系统下的 socket 接口,那么对 socket 编程的过程应该有一些了解。对于 TCP 服务端,接口调用的顺序为socket() -> bind() -> listen() -> accept() -> 其他操作 -> close(),客户端的顺序为socket() -> connect() -> 其他操作 -> close()。如下图所示:

* 图片来源于《深入理解计算机系统》
如上所示,直接调用操作系统 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();
}
}
上面的代码基本上进行了逐步注释,应该不难理解,这里就不啰嗦了。上面有两个方法没有贴代码,就是sendMsg和recvMsg,由于通用操作,在下面的客户端代码里也可以使用,所以这里做了封装。封装代码如下:
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 循环,然后就可以和服务端愉快的聊天了。
上面的代码和叙述都没啥意思,最后我们还是来看看上面代码的运行效果,一图胜前言。

4.总结
到这里,关于套接字通道的相关内容就讲完了,不知道大家有没有看懂。本文仅从使用的角度分析了套接字通道的用法,至于套接字通道的实现,这并不是本文关注的重点。实际上,我在上一篇文章中就说过,Java 所提供的很多类实际上是对操作系统层面上一些系统调用做了一层包装。所以大家在学习 Java 的同时,还应该去了解底层的一些东西,这样才算是知其然,又知其所以然。
好了,本文到这里就结束了,有错误的地方欢迎大家指出来。最后谢谢大家的阅读,祝周末愉快。
参考
- 《深入理解计算机系统》
- 《UNIX网络编程卷 第1卷:套接口API》
- https://www.zhihu.com/question/27991975/answer/69041973
- https://zhuanlan.zhihu.com/p/27365009
本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处
作者:coolblog
本文同步发布在我的个人博客:http://www.coolblog.xyz/?r=cb

本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
Java NIO之套接字通道的更多相关文章
- java输入输出 -- Java NIO之套接字通道
一.简介 前面一篇文章讲了文件通道,本文继续来说说另一种类型的通道 – 套接字通道.在展开说明之前,咱们先来聊聊套接字的由来.套接字即 socket,最早由伯克利大学的研究人员开发,所以经常被称为Be ...
- Java NIO SocketChannel套接字通道
原文链接:http://tutorials.jenkov.com/java-nio/socketchannel.html 在Java NIO体系中,SocketChannel是用于TCP网络连接的套接 ...
- Java NIO(四)文件通道
文件通道 通道是访问I/O服务的导管,I/O可以分为广义的两大类:File I/O和Stream I/O.那么相应的,通道也有两种类型,它们是文件(File)通道和套接字(Socket)通道.文件通道 ...
- Java NIO(五)套接字通道
Socket通道 Socket通道和文件通道有着不一样的特征: Socket通道类可以运行于非阻塞模式,并且是可选的.这两个特征可以激活大程序(如网络服务和中间件组件)巨大的可伸缩性和灵活性,因此再也 ...
- Java网络编程--套接字Socket
一.套接字Socket IP地址标志Internet上的计算机,端口号标志正在计算机上运行的进程(程序). 端口号被规定为一个16位的0--65535之间的整数,其中,0--1023被预先定义的服务通 ...
- Java如何使套接字向单个客户端显示消息?
在Java编程中,如何使用套接字向单个客户端显示消息? 以下示例演示了如何使用Socket类的ssock.accept()方法向单个套接字客户端上显示消息. package com.yiibai; i ...
- Java链接db2套接字出错
### Error querying database. Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Could ...
- Java NIO Channel to Channel Transfers通道传输接口
原文链接:http://tutorials.jenkov.com/java-nio/channel-to-channel-transfers.html 在Java NIO中如果一个channel是Fi ...
- Java NIO之通道
一.前言 前面学习了缓冲区的相关知识点,接下来学习通道. 二.通道 2.1 层次结构图 对于通道的类层次结构如下图所示. 其中,Channel是所有类的父类,其定义了通道的基本操作.从 Channel ...
随机推荐
- 自动化测试(—)Web自动化测试理解
最近几天,由于工作需要接触Web自动化测试,所以我从网上找的资料,学习了解,然后自己总结汇总的随笔,如文章中有不足的地方,请大家多多指教:或者文章内容与他人相似,望见谅. 手工测试跟自动化 ...
- Node.js模块导出module.exports 和 exports,Es6模块导出export 和export default的区别
1.module.exports module变量代表当前模块.这个变量是一个对象,module对象会创建一个叫exports的属性,这个属性的默认值是一个空的对象: module.exports ...
- 2.3 PCI桥与PCI设备的配置空间
PCI设备都有独立的配置空间,HOST主桥通过配置读写总线事务访问这段空间.PCI总线规定了三种类型的PCI配置空间,分别是PCI Agent设备使用的配置空间,PCI桥使用的配置空间和Cardbus ...
- [php] php操作xml
xml文件 <?xml version="1.0" encoding="ISO-8859-1"?> <root> <item id ...
- hi3531的i2c部分
一.关于编译Hi3531 SDK: 之前编译SDK时编译到make uImage总出错,一是找不到.config文件,这个问题是必须先make menuconfig 然后保存.config文件. 二是 ...
- List转换成JSON对象报错(一)
List转换成JSON对象 1.具体报错如下 Exception in thread "main" java.lang.NoClassDefFoundError: org/apac ...
- g++基本用法
用法:g++[选项]文件... g++编译流程: main.cxx #include <iostream> using namespace std; int main(void) { co ...
- IOS开发之XCode学习012:Slider和ProgressView
此文学习来源为:http://study.163.com/course/introduction/1002858003.htm 此工程文件实现功能: 1.定义UISlider和UIProgressV ...
- xml的SAX解析规则
一,为什么要用它 1.1,讲解 DOM解析原理:一次性把xml文档加载进内存,然后在内存中构建Document树. 对内存要求比较要. 缺点: 不适合读取大容量的xml文件,容易导致内存溢出. SAX ...
- JVM内存分析
1.java内存模型分析 java虚拟机运行时数据存储区域包括线程隔离和线程共享两类,整个PC的内存图如下所示: 下面对以上内存区域说明: 1.1 register和cache 当代计算机一般有多个c ...