Java NIO之网络编程
最近在研究Java NIO和netty,曾经一度感觉很吃力,根本原因还是对操作系统、TCP/IP、socket编程的理解不到位。
不禁感叹,还是当初逃的课太多。
假如上天给我一次机会,能够再回到意气风发的校园时代,我想那些逃过的课,应该还是会逃。
毕竟在那个躁动的年纪,有很多的事情都比上课有意思。
不扯闲篇了,进入正题。
先重新理解一下socket编程,主要是基于TCP协议。上一张我从《Unix网络编程》里面截取的一张图
通过这张图,能够大概理解socket编程的几个函数功能和调用顺序,更为关键的是可以看出TCP协议的3次握手发生的时机。
但是这张图并没有很好的揭示socket是怎样体现插座、插口的含义,所以我自己斗胆画了一张图,请多多指教。
借着这张图,说几个要点:
1、刚创建出来的socket,其实并没有server和client之分,只是socket调用了listen方法之后,角色才改变,处理逻辑也随之改变
2、client端的socket发送连接请求,server端的socket接收请求后,再创建一个socket与client端的socket传递数据,就像两个插口在通信
3、每个socket都有发送缓存和接收缓存,操作系统可以根据这些缓存来判断socket可读、可写、异常等状态
4、server端的socket保存着2种连接队列,后面还会说到
5、每个socket还会关联一个文件描述符(文件句柄),操作系统通过这个文件描述符(文件句柄)操作socket。图中并未画出。
再来说说Linux的IO多路复用。
Linux的多种IO模型以及select、poll、epoll等的详细介绍,我这里不赘述,主要也是因为段位不够。
我比较关注的是IO多路复用的那些IO事件。先看看jdk里面SelectionKey类里面的几个方法
public final boolean isReadable() {
return (readyOps() & OP_READ) != 0;
} public final boolean isWritable() {
return (readyOps() & OP_WRITE) != 0;
} public final boolean isConnectable() {
return (readyOps() & OP_CONNECT) != 0;
} public final boolean isAcceptable() {
return (readyOps() & OP_ACCEPT) != 0;
}
方法名很简单:可读、可写、可连接、可接收。从socket的缓存判断可读、可写倒是很好理解;可是什么时候socket是可连接或者可接收呢???
于是硬着头皮慢慢啃《TCP-IP详解:卷2》,终于找到了一些端倪。不得不说,欠的债迟早是要还的。
下面再引入书中的一段文字:
图 1 6 - 5 2 显 示 了 插 口 的 读 、 写 和 例 外 情 况 。 我 们 将 看 到 s o o _ s e l e c t 使用了 s o r e a d a b l e 和 s o w r i t e a b l e 宏 , 这 些 宏 在 s y s / s o c k e t v a r . h 中定义。
1. 插口可读吗
1 1 3 - 1 2 0 s o r e a d a b l e 宏的定义如下:
#define soreadable(so) \
((so)->so_rcv.sb_cc >= (so)->so_rcv.sb_lowat || \
((so)->so_state & SS_CANTRCVMORE) || \
(so)->so_qlen || (so)->so_error)
因为 U D P 和 T C P 的 接 收 下 限 默 认 值 为 1 ( 图 1 6 - 4 ) , 下 列 情 况 表 示 插 口 是 可 读 的 : 接 收 缓 存 中有数据,连接的读通道被关闭,可以接受任何连接或有挂起的差错。
2. 插口可写吗
1 2 1 - 1 2 8 s o w r i t e a b l e 宏的定义如下:
#define sowriteable(so) \
(sbspace(&(so)->so_snd) >= (so)->so_snd.sb_lowat && \
(((so)->so_state&SS_ISCONNECTED) || \
((so)->so_proto->pr_flags&PR_CONNREQUIRED)==0) || \
((so)->so_state & SS_CANTSENDMORE) || \
(so)->so_error)
T C P 和 U D P 默 认 的 发 送 低 水 位 标 记 是 2 0 4 8 。对于 U D P 而言, s o w r i t e a b l e 总 是 为 真 , 因 为 s b s p a c e 总是等于 s b _ h i w a t , 当 然 也 总 是 大 于 或 等 于 s o _ l o w a t , 且 不 要 求 连 接 。对于 T C P 而 言 , 当 发 送 缓 存 中 的 可 用 空 间 小 于 2 0 4 8 个 字 节 时 , 插 口 不 可 写 。 其 他 的 情 况在图 1 6 - 5 2 中讨论。
3. 还有挂起的例外情况吗
1 2 9 - 1 4 0 对于例外情况,需检查标志 s o _ o o b m a r k 和 S S _ R E C V A T M A R K 。 直 到 进 程 读 完 数 据流中的同步标记后,例外情况才可能存在。
原来,select调用的底层实现里面,把很多个事件都只是归并进了可读和可写这两种状态。比如在我之前看来,server端的socket已经将连接排队,就代表可连接状态,可是在select看来,这就是可读状态。
有了前面的一些基础,现在上一段Java NIO的代码
// 创建一个selector
Selector selector = Selector.open();
// 创建一个ServerSocketChannel
ServerSocketChannel servChannel = ServerSocketChannel.open();
servChannel.configureBlocking(false);
// 绑定端口号
servChannel.socket().bind(new InetSocketAddress(8080), 1024);
// 注册感兴趣事件
servChannel.register(selector, SelectionKey.OP_ACCEPT); // select系统调用
selector.select(1000); Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
if (key.isValid()) {
// 处理新接入的请求消息
if (key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
// 接收客户端的连接,并创建一个SocketChannel
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
// 将SocketChannel和感兴趣事件注册到selector
sc.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
// 读数据的处理
}
}
}
分析这段代码之前,先搞清楚selector、SelectionKey、pollArray等几个数据结构以及相互持有关系。
pollArray干的是数组的活,但是并不是一个直接的数组。
selector诞生的时候,随之关联了一块内存(pollArray),然后用unsafe类来小心翼翼的按字节顺序写入数据,最终实现了数组结构的功能。这种看似怪异的实现方式,应该是处于效率的考虑吧。
selector并没有直接持有pollArray,而是持有一个pollArray的封装类PollArrayWrapper的引用。
// The poll fd array
PollArrayWrapper pollWrapper; // 在selector的父类里面 // The set of keys with data ready for an operation
protected Set<SelectionKey> selectedKeys;
selectedKeys是一个集合,代表poll系统调用后返回的所有就绪事件,里面存放的数据结构是SelectionKey。
final SelChImpl channel; // package-private
public final SelectorImpl selector; // Index for a pollfd array in Selector that this key is registered with
private int index; // pollArray里面的索引值,保存在这里是方便实现数组操作 private volatile int interestOps; // 注册的感兴趣事件掩码
private int readyOps; // 就绪事件掩码
SelectionKey不但持有channel,还持有selector;interestOps、readyOps与pollArray里面的eventOps、reventOps对应。
Java定义了一些针对文件描述符的事件,其实也是对底层操作系统poll定义的事件的一个映射。事件用掩码来表示,非常方便进行位操作。如下:
public static final short POLLIN = 0x0001; // 文件描述符可读
public static final short POLLOUT = 0x0004; // 文件描述符可写
public static final short POLLERR = 0x0008; // 文件描述符出现错误
public static final short POLLHUP = 0x0010; // 文件描述符挂断
public static final short POLLNVAL = 0x0020; // 文件描述符不对
public static final short POLLREMOVE = 0x0800; // 文件描述符移除 @Native static final short POLLCONN = 0x0002; // 可连接
我记得POLLCONN在之前的版本中直接被赋值成POLLOUT,这里改成了0x0002,这里我是真不知道为什么。希望高手来回复一下。
最终这些事件都会传递到内核的poll系统调用,去监控所有传递给poll的文件描述符。
回到之前的NIO代码
1、先看看 servChannel.register(selector, SelectionKey.OP_ACCEPT) 是如何实现注册的
一路调用后,会到一个关键方法
protected final SelectionKey register(AbstractSelectableChannel ch,
int ops,
Object attachment)
{
if (!(ch instanceof SelChImpl))
throw new IllegalSelectorException();
SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
k.attach(attachment);
synchronized (publicKeys) {
implRegister(k); // 这一步把channel的文件描述符fd添加到pollArray(见上图)
}
k.interestOps(ops); // 这一步把感兴趣事件eventOps添加到pollArray(见上图)
return k;
}
具体的逻辑肯定比注释要复杂。接下来看看pollArray的内存操作,以添加文件描述符fd为例
void putDescriptor(int i, int fd) {
int offset = SIZE_POLLFD * i + FD_OFFSET;
pollArray.putInt(offset, fd);
} final void putInt(int offset, int value) {
unsafe.putInt(offset + address, value);
}
最终还是用unsafe直接修改内存
2、再看看最核心的selector.select(1000)。次方法最终调用doSelect方法,而doSelect方法的实现有多种,我们就以poll版本进行探秘
// 做了很多删减
protected int doSelect(long timeout)
throws IOException
{
// 执行最核心的poll系统调用
pollWrapper.poll(totalChannels, 0, timeout);
// 将到来的就绪事件更新保存
int numKeysUpdated = updateSelectedKeys();
return numKeysUpdated;
}
poll系统调用会把用户空间的线程挂起,也就是阻塞调用,timeout指定多长时间后必须返回。
updateSelectedKeys方法根据poll返回的channel就绪事件,去更新pollArray对应fd的reventOps(见上图),以及selector的selectedKeys。
/**
* Copy the information in the pollfd structs into the opss
* of the corresponding Channels. Add the ready keys to the
* ready queue.
*/
protected int updateSelectedKeys() {
int numKeysUpdated = 0;
// Skip zeroth entry; it is for interrupts only
for (int i=channelOffset; i<totalChannels; i++) {
// 得到就绪事件的掩码
int rOps = pollWrapper.getReventOps(i);
if (rOps != 0) {
SelectionKeyImpl sk = channelArray[i];
pollWrapper.putReventOps(i, 0); // 重置为0,即为未就绪
if (selectedKeys.contains(sk)) {
// 把事件的掩码翻译成SelectionKey中定义的操作(OP_READ,OP_WRITE,OP_CONNECT,OP_ACCEPT)
if (sk.channel.translateAndSetReadyOps(rOps, sk)) {
numKeysUpdated++;
}
} else {
sk.channel.translateAndSetReadyOps(rOps, sk);
if ((sk.nioReadyOps() & sk.nioInterestOps()) != 0) {
// 更新selectedKeys
selectedKeys.add(sk);
numKeysUpdated++;
}
}
}
}
return numKeysUpdated;
}
把就绪事件的掩码进行翻译,感觉就像是Java做的一层适配,让我们用户不用去关注事件掩码等细节
看一下实现这一逻辑的一段代码,在ServerSocketChannel类里面:
/**
* Translates native poll revent set into a ready operation set
*/
public boolean translateReadyOps(int ops, int initialOps,
SelectionKeyImpl sk) {
int intOps = sk.nioInterestOps(); // Do this just once, it synchronizes
int oldOps = sk.nioReadyOps();
int newOps = initialOps; if ((ops & PollArrayWrapper.POLLNVAL) != 0) {
// This should only happen if this channel is pre-closed while a
// selection operation is in progress
// ## Throw an error if this channel has not been pre-closed
return false;
} if ((ops & (PollArrayWrapper.POLLERR
| PollArrayWrapper.POLLHUP)) != 0) {
newOps = intOps;
sk.nioReadyOps(newOps);
return (newOps & ~oldOps) != 0;
}
// 这里将可连接当作可读来看待的
if (((ops & PollArrayWrapper.POLLIN) != 0) &&
((intOps & SelectionKey.OP_ACCEPT) != 0))
newOps |= SelectionKey.OP_ACCEPT; sk.nioReadyOps(newOps);
return (newOps & ~oldOps) != 0;
}
通过上面的分析,大概有了一个清晰的思路:
Java NIO主要是基于底层操作系统提供的的IO多路复用功能,比如Linux下的select/poll、epoll等系统调用。Java层面为每个selector开辟了一块内存,用来保存用户注册的所有channel、所有感兴趣事件,并最终当作参数传递给底层的系统调用,最后将内核返回的结果封装成selectedKeys等数据结构。
Java NIO之网络编程的更多相关文章
- Java网络编程和NIO详解9:基于NIO的网络编程框架Netty
Java网络编程和NIO详解9:基于NIO的网络编程框架Netty 转自https://sylvanassun.github.io/2017/11/30/2017-11-30-netty_introd ...
- Java学习之网络编程实例
转自:http://www.cnblogs.com/springcsc/archive/2009/12/03/1616413.html 多谢分享 网络编程 网络编程对于很多的初学者来说,都是很向往的一 ...
- 黑马程序员:Java基础总结----网络编程
黑马程序员:Java基础总结 网络编程 ASP.Net+Android+IO开发 . .Net培训 .期待与您交流! 网络编程 网络通讯要素 . IP地址 . 网络中设备的标识 . 不易记忆,可用 ...
- Java进阶之网络编程
网络编程 网络编程对于很多的初学者来说,都是很向往的一种编程技能,但是很多的初学者却因为很长一段时间无法进入网络编程的大门而放弃了对于该部分技术的学习. 在 学习网络编程以前,很多初学者可能觉得网络编 ...
- 第84节:Java中的网络编程(中)
第84节:Java中的网络编程(中) 实现客户端和服务端的通信: 客户端需要的操作,创建socket,明确地址和端口,进行键盘录入,获取需要的数据,然后将录入的数据发送给服务端,为socket输出流, ...
- 第78节:Java中的网络编程(上)
第78节:Java中的网络编程(上) 前言 网络编程涉及ip,端口,协议,tcp和udp的了解,和对socket通信的网络细节. 网络编程 OSI开放系统互连 网络编程指IO加网络 TCP/IP模型: ...
- 第62节:探索Java中的网络编程技术
前言 感谢! 承蒙关照~ 探索Java中的网络编程技术 网络编程就是io技术和网络技术的结合,网络模型的定义,只要共用网络模型就可以两者连接.网络模型参考. 一座塔有七层,我们需要闯关. 第一层物理层 ...
- java第九节 网络编程的基础知识
/** * * 网络编程的基础知识 * 网络协议与TCP/IP * IP地址和Port(端口号) * 本地回路的IP地址:127.0.0.1 * 端口号的范围为0-65535之间,0-1023之间的端 ...
- 20165324 Java实验五 网络编程与安全
20165324 Java实验五 网络编程与安全 一.实验报告封面 课程:Java程序设计 班级:1653班 姓名:何春江 学号:20165324 指导教师:娄嘉鹏 实验日期:2018年5月28日 实 ...
随机推荐
- 七牛php-sdk使用-多媒体处理
在七牛对象存储可以创建公共的bucket和私有的bucket,私有的不可以直接使用域名加资源key的方式进行访问,需要附加下载凭证. 私有bucket 关于下载凭证的生成,php-sdk已经提供了方法 ...
- Erlang epmd官方文档中文翻译
本文含epmd简介及官方文档之翻译,文档地址 http://erlang.org/doc/man/epmd.html翻译时的版本 R19.1 中英文水平都不咋地,不通顺处海涵,就酱. 简介 Erlan ...
- 认识Java中的字符串
Java 中 String 类的常用方法 Ⅰ String 类提供了许多用来处理字符串的方法,例如,获取字符串长度.对字符串进行截取.将字符串转换为大写或小写.字符串分割等,下面我们就来领略它的强大之 ...
- 【ASP.NET MVC系列】浅谈表单和HTML辅助方法
[01]浅谈Google Chrome浏览器(理论篇) [02]浅谈Google Chrome浏览器(操作篇)(上) [03]浅谈Google Chrome浏览器(操作篇)(下) [04]浅谈ASP. ...
- 【批处理学习笔记】第十一课:常用DOS命令(1)
[ 文件夹管理 ]cd 显示当前目录名或改变当前目录.md 创建目录.rd 删除一个目录.dir 显示目录中的文件和子目录列表.tree 以图形显示驱动器或路径的文件夹结构.path 为可执行文件显示 ...
- 2017广东工业大学程序设计竞赛决赛 题解&源码(A,数学解方程,B,贪心博弈,C,递归,D,水,E,贪心,面试题,F,贪心,枚举,LCA,G,dp,记忆化搜索,H,思维题)
心得: 这比赛真的是不要不要的,pending了一下午,也不知道对错,直接做过去就是了,也没有管太多! Problem A: 两只老虎 Description 来,我们先来放松下,听听儿歌,一起“唱” ...
- bzoj:2423: [HAOI2010]最长公共子序列
Description 字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列.令给定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0, ...
- C#面试题整理(1)
最近在看CLR VIA C#,发现了一些案例很适合来做面试题.特此整理: 1,System.Object里的GetType方法是否为虚函数?说出理由. 答案:不是,因为C#是一种类型安全的语言,如果覆 ...
- 转:绝对干货--WordPress自定义查询wp_query所有参数详细注释
<?php /** * WordPress 查询综合参考 * 编译:luetkemj - luetkemj.com * * 官方文档: http://codex.wordpress.org/Cl ...
- dedecms幻灯片调用图片模糊的解决办法
dedecms幻灯片调用的是缩略图,如果图片尺寸比例和幻灯片的大小相差太大的话,图片就会自动拉伸模糊,比较影响美观和用户体验,下面就有常用的2个方法来解决这个图片模糊的问题. 第一种:手动制图 我们用 ...