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. Python ToDo List

    这是我在学习python过程中,想做又没来得及做的事情一览.最初只有寥寥几个字,我会尽力去消化,让它不会只增不减. 由于博客园奇怪的算法,明明是一篇非常没有含量的东西(连字数都没有达到),居然能荣登p ...

  2. PHP7函数大全(4553个函数)

    转载来自: http://www.infocool.net/kb/PHP/201607/168683.html a 函数 说明 abs 绝对值 acos 反余弦 acosh 反双曲余弦 addcsla ...

  3. CSS系列:在HTML中引入CSS的方法

    HTML与CSS是两个作用不同的语言,它们同时对一个网页产生作用,因此必须将CSS与HTML链接在一起使用.在HTML中,引入CSS的方法主要有4种:行内式.内嵌式.导入式和链接式. 1. 行内式 行 ...

  4. 【DWR系列01】-DWR简介及入门例子

    .literal { background-color: #f2f2f2; border: 1px solid #cccccc; padding: 1px 3px 0; white-space: no ...

  5. shell(一)

    #服务器之间拷贝数据 scp  -r   本地文件目录    系统用户名@IP:目标文件夹路径

  6. 学习微信小程序之css9内边距

    padding内边距 <!DOCTYPE html> <html lang="en"> <head> <meta charset=&quo ...

  7. Java实现操作dos命令

    java实现操作dos命令的两种方式 1.读取文件中的命令 package com; import java.io.InputStream; public class cmd { public sta ...

  8. getComputedStyle 方法

    一:getComputedStyle getComputedStyle是一个可以获取当前元素所有最终使用的CSS属性值.返回的是一个CSS样式声明对象([object CSSStyleDeclarat ...

  9. json相关,浏览器打开json格式的api接口时,进行格式化,chrome插件

    在chrome浏览器中安装Google jsonview插件能够自动格式化json格式的数据.

  10. TMemo.Text 回车键会变成#$D#$A,而非#13#10

    mmoComplain: TMemo;//cxmComplain.Text 会造成回车键 转换成十六进制的字符串 #$D#$A,而非#13#10 //cxmComplain.Text范例:'风发的是' ...