Socket通道

上文讲述了通道、文件通道,这篇文章来讲述一下Socket通道,Socket通道与文件通道有着不一样的特征,分三点说:

1、NIO的Socket通道类可以运行于非阻塞模式并且是可选择的,这两个性能可以激活大程序(如网络服务器和中间件组件)巨大的可伸缩性和灵活性,因此,再也没有为每个Socket连接使用一个线程的必要了。这一特性避免了管理大量线程所需的上下文交换总开销,借助NIO类,一个或几个线程就可以管理成百上千的活动Socket连接了并且只有很少甚至没有性能损失

2、全部Socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)在被实例化时都会创建一个对应的Socket对象,就是我们所熟悉的来自java.net的类(Socket、ServerSocket和DatagramSocket),这些Socket可以通过调用socket()方法从通道类获取,此外,这三个java.net类现在都有getChannel()方法

3、每个Socket通道(在java.nio.channels包中)都有一个关联的java.net.socket对象,反之却不是如此,如果使用传统方式(直接实例化)创建了一个Socket对象,它就不会有关联的SocketChannel并且它的getChannel()方法将总是返回null

概括地讲,这就是Socket通道所要掌握的知识点知识点,不难,记住并通过自己写代码/查看JDK源码来加深理解。

非阻塞模式

前面第一点说了,NIO的Socket通道可以运行于非阻塞模式,这个陈述虽然简单却有着深远的含义。传统Java Socket的阻塞性质曾经是Java程序可伸缩性的最重要制约之一,非阻塞I/O是许多复杂的、高性能的程序构建的基础。

要把一个Socket通道置于非阻塞模式,要依赖的是Socket通道类的弗雷SelectableChannel,下面看一下这个类的简单定义:

public abstract class SelectableChannel extends AbstractInterruptibleChannel implements Channel
{
...
public abstract void configureBlocking(boolean block) throws IOException;
public abstract boolean isBlocking();
public abstract Object blockngLock();
...
}

因为这篇文章是讲述Socket通道的,因此省略了和选择器相关的方法,这些省略的内容将在下一篇文章中说明。

从SelectableChannel的API中可以看出,设置或重新设置一个通道的阻塞模式是很简单的,只要调用configureBlocking()方法即可,传递参数值为true则设为阻塞模式,参数值为false则设为非阻塞模式,就这么简单。同时,我们可以通过调用isBlocking()方法来判断某个Socket通道当前处于哪种模式中。

偶尔,我们也会需要放置Socket通道的阻塞模式被更改,所以API中有一个blockingLock()方法,该方法会返回一个非透明对象引用,返回的对象是通道实现修改阻塞模式时内部使用的,只有拥有此对象的锁的线程才能更改通道的阻塞模式,对于确保在执行代码的关键部分时Socket通道的阻塞模式不会改变以及在不影响其他线程的前提下暂时改变阻塞模式来说,这个方法是非常方便的。

Socket通道服务端程序

OK,接下来先看下Socket通道服务端程序应该如何编写:

 public class NonBlockingSocketServer
{
public static void main(String[] args) throws Exception
{
int port = 1234;
if (args != null && args.length > 0)
{
port = Integer.parseInt(args[0]);
}
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ServerSocket ss = ssc.socket();
ss.bind(new InetSocketAddress(port));
System.out.println("开始等待客户端的数据!时间为" + System.currentTimeMillis());
while (true)
{
SocketChannel sc = ssc.accept();
if (sc == null)
{
// 如果当前没有数据,等待1秒钟再次轮询是否有数据,在学习了Selector之后此处可以使用Selector
Thread.sleep(1000);
}
else
{
System.out.println("客户端已有数据到来,客户端ip为:" + sc.socket().getRemoteSocketAddress()
+ ", 时间为" + System.currentTimeMillis()) ;
ByteBuffer bb = ByteBuffer.allocate(100);
sc.read(bb);
bb.flip();
while (bb.hasRemaining())
{
System.out.print((char)bb.get());
}
sc.close();
System.exit(0);
}
}
}
}

整个代码流程大致上就是这样,没什么特别值得讲的,注意一下第18行~第22行,由于这里还没有讲到Selector,因此当客户端Socket没有到来的时候选择的处理办法是每隔1秒钟轮询一次。

Socket通道客户端程序

服务器端经常会使用非阻塞Socket通达,因为它们使同时管理很多Socket通道变得更容易,客户端却并不强求,因为客户端发起的Socket操作往往比较少,且都是一个接着一个发起的。但是,在客户端使用一个或几个非阻塞模式的Socket通道也是有益处的,例如借助非阻塞Socket通道,GUI程序可以专注于用户请求并且同时维护与一个或多个服务器的会话。在很多程序上,非阻塞模式都是有用的,所以,我们看一下客户端应该如何使用Socket通道:

 public class NonBlockingSocketClient
{
private static final String STR = "Hello World!";
private static final String REMOTE_IP= "127.0.0.1"; public static void main(String[] args) throws Exception
{
int port = 1234;
if (args != null && args.length > 0)
{
port = Integer.parseInt(args[0]);
}
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
sc.connect(new InetSocketAddress(REMOTE_IP, port));
while (!sc.finishConnect())
{
System.out.println("同" + REMOTE_IP+ "的连接正在建立,请稍等!");
Thread.sleep(10);
}
System.out.println("连接已建立,待写入内容至指定ip+端口!时间为" + System.currentTimeMillis());
ByteBuffer bb = ByteBuffer.allocate(STR.length());
bb.put(STR.getBytes());
bb.flip(); // 写缓冲区的数据之前一定要先反转(flip)
sc.write(bb);
bb.clear();
sc.close();
}
}

总得来说和普通的Socket操作差不多,通过通道读写数据,非常方便。不过再次提醒,通道只能操作字节缓冲区也就是ByteBuffer的数据

运行结果展示

上面的代码,为了展示结果的需要,在关键点上都加上了时间打印,这样会更清楚地看到运行结果。

首先运行服务端程序(注意不可以先运行客户端程序,如果先运行客户端程序,客户端程序会因为服务端未开启监听而抛出ConnectionException),看一下:

看到红色方块,此时程序是运行的,接着运行客户端程序:

看到客户端已经将"Hello World!"写入了Socket并通过通道传到了服务器端,方框变灰,说明程序运行结束了。此时看一下服务器端有什么变化:

看到服务器端打印出了字符串"Hello World!",并且方框变灰,程序运行结束,这和代码是一致的。

注意一点,客户端看到的时间是XXX10307,服务器端看到的时间是XXX10544,这是很正常的,因为前面说过了,服务器端程序是每隔一秒钟轮询一次是否有Socket到来的。

当然,由于服务端程序的作用是监听1234端口,因此完全可以写客户端的代码,可以直接访问http://127.0.0.1:1234/a/b/c/d/?e=5&f=6&g=7就可以了,看一下效果:

有了这个基础,我们就可以自己解析HTTP请求,甚至可以自己写一个Web服务器。

客户端Socket通道复用性的研究

这个是我今天上班的时候想到的一个问题,补充到最后。

服务器端程序不变,客户端现在是单个线程发送了一次数据到服务端的,假如现在我的客户端有多条线程同时通过Socket通道发送数据到服务端又会是怎么样的现象?首先将服务端端的代码稍作改变,让服务端SocketChannel在拿到客户端的数据之后程序不会停止运行而是会持续监听来自客户端的Socket,由于服务器端的代码比较多,这里只列一下改动的地方,:

...
bb.flip();
while (bb.hasRemaining())
{
System.out.print((char)bb.get());
}
System.out.println();
//sc.close();
//System.exit(0);
...

接着看一下对客户端代码的启动,把写数据的操作放到线程的run方法中去:

 public class NonBlockingSocketClient
{
private static final String STR = "Hello World!";
private static final String REMOTE_IP = "127.0.0.1";
private static final int THREAD_COUNT = 5; private static class NonBlockingSocketThread extends Thread
{
private SocketChannel sc; public NonBlockingSocketThread(SocketChannel sc)
{
this.sc = sc;
} public void run()
{
try
{
System.out.println("连接已建立,待写入内容至指定ip+端口!时间为" + System.currentTimeMillis());
String writeStr = STR + this.getName();
ByteBuffer bb = ByteBuffer.allocate(writeStr.length());
bb.put(writeStr.getBytes());
bb.flip(); // 写缓冲区的数据之前一定要先反转(flip)
sc.write(bb);
bb.clear();
}
catch (IOException e)
{
e.printStackTrace();
}
}
} public static void main(String[] args) throws Exception
{
int port = 1234;
if (args != null && args.length > 0)
{
port = Integer.parseInt(args[0]);
}
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
sc.connect(new InetSocketAddress(REMOTE_IP, port));
while (!sc.finishConnect())
{
System.out.println("同" + REMOTE_IP + "的连接正在建立,请稍等!");
Thread.sleep(10);
} NonBlockingSocketThread[] nbsts = new NonBlockingSocketThread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++)
nbsts[i] = new NonBlockingSocketThread(sc);
for (int i = 0; i < THREAD_COUNT; i++)
nbsts[i].start();
// 一定要join保证线程代码先于sc.close()运行,否则会有AsynchronousCloseException
for (int i = 0; i < THREAD_COUNT; i++)
nbsts[i].join(); sc.close();
}
}

启动了5个线程,我们可能期待服务端能有5次的数据到来,实际上是:

原因就是客户端的五个线程共用了同一个SocketChannel,这样相当于五个线程把数据轮番写到缓冲区,写完之后再把数据通过通道传输到服务器端。ByteBuffer的write方法放心,是加锁的,反编译一下sun.nio.ch.SocketChannelImpl就知道了,因此不会出现"Hello World!Thread-X"这些字符交叉的情况。

所以有了这个经验,我们让每个线程都new一个自己的SocketChannel,于是客户端程序变成了:

 public class NonBlockingSocketClient
{
private static final String STR = "Hello World!";
private static final String REMOTE_IP = "127.0.0.1";
private static final int THREAD_COUNT = 5; private static class NonBlockingSocketThread extends Thread
{
public void run()
{
try
{
int port = 1234;
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
sc.connect(new InetSocketAddress(REMOTE_IP, port));
while (!sc.finishConnect())
{
System.out.println("同" + REMOTE_IP + "的连接正在建立,请稍等!");
Thread.sleep(10);
}
System.out.println("连接已建立,待写入内容至指定ip+端口!时间为" + System.currentTimeMillis());
String writeStr = STR + this.getName();
ByteBuffer bb = ByteBuffer.allocate(writeStr.length());
bb.put(writeStr.getBytes());
bb.flip(); // 写缓冲区的数据之前一定要先反转(flip)
sc.write(bb);
bb.clear();
sc.close();
}
catch (IOException e)
{
e.printStackTrace();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
} public static void main(String[] args) throws Exception
{
NonBlockingSocketThread[] nbsts = new NonBlockingSocketThread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++)
nbsts[i] = new NonBlockingSocketThread();
for (int i = 0; i < THREAD_COUNT; i++)
nbsts[i].start();
// 一定要join保证线程代码先于sc.close()运行,否则会有AsynchronousCloseException
for (int i = 0; i < THREAD_COUNT; i++)
nbsts[i].join();
}
}

此时再运行,观察结果:

看到没有问题,服务器端分五次接收来自客户端的请求了。

当然,这也是有一定问题的:

1、如果服务器端开放多线程使用ServerSocket通道去处理来自客户端的数据的话,面对成千上万的高并发很容易地就会耗尽服务器端宝贵的线程资源

2、如果服务器端只有一条ServerSocket通道线程处理来自客户端的数据的话,一个客户端的数据处理得慢将直接影响后面线程的数据处理

这么一说似乎又回到了非阻塞I/O的老问题了。不过,Socket通道讲解到此,大体的概念我们已经清楚了,接着就轮到NIO的最后也是最难、最核心的部分----选择器,将在下一篇文章进行详细的讲解。

Java NIO4:Socket通道的更多相关文章

  1. JAVA NIO Socket通道

      DatagramChannel和SocketChannel都实现定义读写功能,ServerSocketChannel不实现,只负责监听传入的连接,并建立新的SocketChannel,本身不传输数 ...

  2. Java nio 笔记:系统IO、缓冲区、流IO、socket通道

    一.Java IO 和 系统 IO 不匹配 在大多数情况下,Java 应用程序并非真的受着 I/O 的束缚.操作系统并非不能快速传送数据,让 Java 有事可做:相反,是 JVM 自身在 I/O 方面 ...

  3. Java NIO系列教程(三) Channel之Socket通道

    目录: <Java NIO系列教程(二) Channel> <Java NIO系列教程(三) Channel之Socket通道> 在<Java NIO系列教程(二) Ch ...

  4. Java NIO3:通道和文件通道

    通道是什么 通道式(Channel)是java.nio的第二个主要创新.通道既不是一个扩展也不是一项增强,而是全新的.极好的Java I/O示例,提供与I/O服务的直接连接.Channel用于在字节缓 ...

  5. Java NIO之通道Channel

    channel与流的区别: 流基于字节,且读写为单向的. 通道基于快Buffer,可以异步读写.除了FileChannel之外都是双向的. channel的主要实现: FileChannel Data ...

  6. 简单通过java的socket&serversocket以及多线程技术实现多客户端的数据的传输,并将数据写入hbase中

    业务需求说明,由于公司数据中心处于刚开始部署的阶段,这需要涉及其它部分将数据全部汇总到数据中心,这实现的方式是同上传json文件,通过采用socket&serversocket实现传输. 其中 ...

  7. Java NIO Socket 非阻塞通信

    相对于非阻塞通信的复杂性,通常客户端并不需要使用非阻塞通信以提高性能,故这里只有服务端使用非阻塞通信方式实现 客户端: package com.test.client; import java.io. ...

  8. Java NIO之通道

    一.前言 前面学习了缓冲区的相关知识点,接下来学习通道. 二.通道 2.1 层次结构图 对于通道的类层次结构如下图所示. 其中,Channel是所有类的父类,其定义了通道的基本操作.从 Channel ...

  9. java的socket通信

    本文讲解如何用java实现网络通信,是一个非常简单的例子,我比较喜欢能够立马看到结果,所以先上代码再讲解具体细节. 服务端: import java.io.BufferedReader; import ...

随机推荐

  1. HDU5988 Coding Contest(费用流)

    2016青岛现场赛的一题,由于第一次走过不会产生影响,需要拆点,不过比赛时没想到,此外还有许多细节要注意,如要加eps,时间卡得较紧要注意细节优化等 #include <iostream> ...

  2. RxJava 学习笔记(一)

    最近Android6.0的权限问题,要把之前的APP进行改造,用到了RxPermission框架!之前了解过RXJAVA,但是由于之前项目一直没有使用这个框架,所以也就一直搁置了.正好Rxpermis ...

  3. 继承映射关系 TPH、TPT、TPC<EntityFramework6.0>

    每个类型一张表[TPT] 声明方式 public class Business { [Key] public int BusinessId { get; protected set; } public ...

  4. Jquery判断变量是否为空

    var aaa=''; if(aaa) { //aaa不为空也不是不可识别对象时执行 } else { //aaa为空或不可识别时执行 } aaa必须是变量,对象的属性好像是不行,

  5. B/S系统常见缺陷整理和解决方案

    最近部门整理了今年所有项目测试团队提出的BUG,筛选了几十个作为常规通用的缺陷,我根据这些缺陷内容,去掉和业务相关的知识,整理出了一份缺陷描述和解决方案. 其实WEB系统中常规的缺陷分类后也就那么多, ...

  6. I Could Have Danced All Night

    百老汇著名音乐剧<窈窕淑女(My Fair Lady)>中脍炙人口的经典歌曲. Bed, bed ,I couldn't go to bed床,床,我不能上床去 My head's too ...

  7. mysql 字符串 日期互转

    一.字符串转日期 下面将讲述如何在MySQL中把一个字符串转换成日期: 背景:rq字段信息为:20100901 1.无需转换的: SELECT * FROM tairlist_day WHERE rq ...

  8. ring3硬件断点

    4个断点寄存器DR0~DR3用来设置断点的线性地址. DR6为状态寄存器,DR7为控制寄存器. DR4和DR5保留.当CR4.DE==1时,访问DR4和DR5产生#UD异常:IF CR4.DE==0, ...

  9. c语言文件读写操作总结

    C语言文件读写操作总结 C语言文件操作 一.标准文件的读写 1.文件的打开 fopen() 文件的打开操作表示将给用户指定的文件在内存分配一个FILE结构区,并将该结构的指针返回给用户程序,以后用户程 ...

  10. .NET 读取本地文件绑定到GridViewRow

    wjgl.aspx.cs: using System; using System.Collections; using System.Configuration; using System.Data; ...