注:本文为对Java socket详解,看这一篇就够了续的重排版,方便自己浏览。
接下来我们将进一步讨论Java socket 异步通信,Java socket异步通信包主要位于是在Java.nio框架下,在讲解Java socket异步通信前,我们先来回顾一下传统socket通信的演进。
Blocking I/O 模式
BlockIng I/O模式下,主要缺点如下:
-
只能用于小规模下多个socket通信,因为客户端socket每次连接请求后,服务端ServerSocket都会创建一个线程来处理当前客户端的连接请求,如果连接数非常大,以千万级为单位,那么服务端的CPU资源开销会是一个非常庞大的数据。
-
Read、Write读写资源问题,由于是阻塞的读写模式,如果大量线程处于空闲状态没有数据可读写,则会造成空闲socket的Read 、Write操作大量阻塞,对系统资源线程的开销也会造成非常大的浪费。
接下来我们看看NIO(not Blocking I/O ,也有人叫他new IO)的工作原理,NIO主要实现机制于IO最大的区别在他通过选择器与采用观察者模式将之前大量连接采用一个线程即可搞定,同时通过通道的方式,可对流进行重复选择的读取,下面我们通过图形来描述一下NIO的工作原理。
NIO模式下下的优点:
-
NIO采用channel 与selector结合方式,可以多次从通过读写或者写入通道数据,并且可以读取指定位置的数据,而传统io方式,采用流的方式对数据进行读取,一但打开流,那么只能读取到流的结尾,无法从流指定的位置进行读取。我们把数据流比作打开的自来水管一样,你没法只获取水流中的一部分数据。
-
socketor选择器,在通过socketchannl将socket注册到选择器中,那么就可以通过一个线程处理注册进来的所有socket。socketor说的通俗一点就像饭店的点菜系统,比如说在传统上,我们点菜的流程是这样的,拿着菜单,把服务员叫过来,你在点菜,服务员在旁边候着,形成的方式是客户和服务员一对一的方式,如果饭店只有10个服务员,那么我只能服务10个用户,这样是效率及其底下的。而使用socketor后,在你点菜的时候,服务员给你一个电子菜单(或者像海底捞的纸质可以选择的菜单),你自己将需要点的菜在菜单上勾选,点好了直接给服务员就可以了,这样加入饭店来了100个客人,那个10个服务员就只需要将菜单发给客户,客户自己选择菜名后,交给服务员即可。
-
我们知道流的数据是单向的,而socketChannel则是双向的,我们继可以向通道中写数据,也可以从通道中读取数据,并且通道中的数据读写都是通过buffer实现的。
上面我们简单的介绍了一下NIO中socket的应用原理,接下来我们详细介绍一下NIO中socket相关的知识点,由于NIO框架下涉及的类和接口非常多,在这里我们主要讲解的是nio下的socket通信,所有我把nio下的关于socket相关的主要的几个类和接口进行整理和分类一下,方便大家有个脉络,其实,我们分析一下,nio下和socket通信相关的我们可以把大分为三大类(其实应该是俩类,channel 与buffer,selector相关的也是在channel下,在这里是主要是为了给大家讲解的清楚,我把selector拿出来了,进行单独的分类),channel、buffer与selector三大类,每一种类型下面涉及到常用的类和接口我在大家整理一下,请看下面的这个思维导图:
首先我们来看一下buffer、channel与selector这三者之间的区别和联系,channel通道,这里我们可以把它理解为传统io的流,而buffer就是针对channel 的一个缓冲区,他就是一个连续的内存块,是NIO数据的一个中转站。我们可以将channel中的数据读取到buffer中,也可以将buffer中的数据写入到channel中,所以channel是双向的,可以进行读写操作,而传统IO基于字节流的操作,读和写都是分开的,我们必须打开对应Input才可以操作IO。
接下来,我们首先看channel包中的这几个核心的类。在这里我主要介绍一下服务端的socketChannel 与客户端的socketchannel。其他的类大家可以自行阅读API,结合源码我详细有更深入的了解。
ServerSocketChannel 类是有常用的几个方法分别是:
-
abstract SocketChannel accept() 。接受来之Channel通道socket的连接。
-
ServerSocketChannel bind(SocketAddress local)。将通道的socket绑定到本地地址。
-
abstract ServerSocketChannel bind(SocketAddresslocal, int backlog)。是上一个方法的重载,也是刚通道的socket绑定到本地地址,第一个参数是本地地址,第二个表示挂起连接数的最大值。
-
abstract SocketAddress getLocalAddress()。返回当前通道socket绑定的本地地址
-
static ServerSocketChannel open()。 打开一个ServersocketChannel。
-
abstract ServerSocket socket()。检索通道相关联的socket
由于ServerSocketChannel 继承了ServerSocketChannel 并且实现了NetworkChannel 的接口,所有他换有一些其他的方法可用,比如:
-
void close()。 关闭通道的方法。
-
abstract SelectableChannel configureBlocking(boolean block)。调整通道的阻塞模式。
-
SelectionKey register(Selectorsel, int ops)。将通道注册到制定的选择器上,
-
SelectorProvider provider()。返回创建通道的提供程序
SocketChannel 类是有常用的几个方法分别是:
-
abstract SocketChannel bind(SocketAddresslocal)。 将通道的socket绑定到本地地址。
-
abstract boolean connect(SocketAddress remote)。
-
abstract SocketAddress getLocalAddress()。
-
abstract SocketAddress getRemoteAddress()。
-
abstract boolean isConnected()。
-
static SocketChannel open()。 打开一个socketChannel
-
static SocketChannel open(SocketAddress remote)。
-
abstract Socket socket()。 检索与通道相关联的socket
-
abstract SocketChannels hutdownInput()。 在不关闭通道的情况下,关闭连接已方便获取数据。
Selector 类是有常用的几个方法分别是:
-
abstract void close() 。 关闭当前选择器
-
abstract boolean isOpen()。 当前选择器是否打开
-
abstract Set keys()。返回当前选择器中的key,是一个set集合
-
static Selector open()。 打开一个选择器。
-
abstract int select()。当对的通道io准备好时选择一组键,
-
abstract Set selectedKeys()。返回当前选择器的selected-key set.集合
SelectionKey类有四个属性,分别是:
static int OP_ACCEPT 接受socket
static int OP_CONNECT 开始连接
static int OP_READ 开始读数据
static int OP_WRITE 开始写数据
同时也有对应的几个方法。分别是isAcceptable()、isConnectable()、isReadable() 和isWritable()。
上面我们对常用的几个接口和方法进行进行了详细的介绍,接下来我们就通过详细的例子抽丝剥茧了解他们的原理,
先来一个简单的例子:
ServerSocketChannel 服务端.
package SocketChannel;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
public class ServerSocketChnnel1 {
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9000));
serverSocketChannel.configureBlocking(false);
while (true){
SocketChannel socketChannel = serverSocketChannel.accept();
while (socketChannel!=null){
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int i = socketChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
.toString());
}
}
}catch (IOException e) {
e.printStackTrace();
}
}
}
客户端:
package socket;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Date;
public class ClientSocket {
public static void main(String[] args) {
Socket socket;
{
try {
socket = new Socket("127.0.0.1",9000);
OutputStream outputStream = socket.getOutputStream();
outputStream.write("你好".getBytes());
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
运行后我们可以看到控制台收到了客户端的信息。
错误,我们先不需要关注,后面我们一步一步带大家讲解。
首先我们分析服务端程序:
第一步:通过ServerSocketCHannel.open()打开这个Channel通道,我们看一下他这个源码:
public static ServerSocketChannel open() throws IOException {
return SelectorProvider.provider().openServerSocketChannel();
}
他是调用SelectorProvider类的provider()方法,获取SelectorProvider,然后在调用SelectorProvider的openServerSocketChannel()方法。其中provider()方法是一个线程安全的。
public static SelectorProvider provider() {
synchronized (lock) {
if (provider != null)
return provider;
return AccessController.doPrivileged(
new PrivilegedAction<SelectorProvider>() {
public SelectorProvider run() {
if (loadProviderFromProperty())
return provider;
if (loadProviderAsService())
return provider;
provider = sun.nio.ch.DefaultSelectorProvider.create();
return provider;
}
});
}
}
通过上面我们可以看到,open()方法打开一个线程安全的ServerSocketChannel。
第二步:我们通过bind()方法绑定对应的端口。这个和我们普通的ServerSocket类似。
第三步:通过configureBlocking()方法设置阻塞方式。
第四步:就可以通过accept方法接受对应的请求了。
通过上面的小例子,我们简单的描述了一下ServerSocketChannel最基本的概念和应用,让大家有一个初步的认识,那么在接下来的示例中,我会引入Selector 选择器、ByteBuffer缓存、已经IO多路复用的几种模式。
上面只是一个简单的SocketChannel示例,一次接受一个一个socket请求,接下来我们对上面的是示例进一步细化。
服务端ServerSocketChannel 示例:
package SocketChannel;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
/**
* @创建人:zhangzhiqiang
* @创建时间:2019/09/03
* @描述:
* @联系方式:QQ:125717901
**/
public class ServerSocketChannel2 {
public static void main(String[] args) {
try {
// 第一步
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9000));
serverSocketChannel.configureBlocking(false);
while (true){
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannel!=null){
System.out.println("有新的客户端进来连接");
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (socketChannel.read(byteBuffer)!=-1){
byteBuffer.flip();
System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer).toString());
byteBuffer.clear();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端SocketChannel示例:
package SocketChannel;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* @创建人:zhangzhiqiang
* @创建时间:2019/09/03
* @描述:
* @联系方式:QQ:125717901
**/
public class SocketChannel2 {
public static void main(String[] args) {
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1",9000));
socketChannel.configureBlocking(false);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
String str = null;
while ((str=bufferedReader.readLine())!=null){
ByteBuffer byteBuffer = ByteBuffer.allocate(str.length());
byteBuffer.put(str.getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteBuffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
在这里,我们服务端和客户端都是采用Channel的方式创建的,和传统的Socket不一样,服务端不通过的轮询是否有信息进来,如果有信息进来我们就创建一个ByteBuffer用来接收信息,然后在打印出来。服务端我们设置的非阻塞的方式,所以在下面接收到客户端socket的时候,需要进行一个判断,当前socket是否为null。
知识点:
- serverSocketChannel.configureBlocking(false); 设置通道的阻塞方式。
- if(socketChannel!=null) ;判断当前socket是否为空,如果不为空我们才能进行后续的业务操作。
- ByteBuffer.allocate(1024); 分配指定大小的ByteBuffer,ByteBuffer.allocateDirect(1024)分配一个直接的ByteBuffer效率更高,对于将一些文件读取到内存中处理来说可以使用该方法。
- byteBuffer.flip();用于将缓存区进行翻转,通过我们将数据写入Buffer中,如果想读取的话,一般我们采用byteBuffer.flip()方法将缓存区进行翻转,然后在读取缓存区的数据,其实这个byteBuffer.flip()方法并不是真的把缓存区翻了一个,他只是将buffer中的mark、position、limit进行重新标记了一下,方便数据读取。我们来看一下flip()方法的源码:
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
在这里我们可以看到,重置了position 和limit 。
- ByteBuffer数据打印问题,由于采用的Unicode编码,如果我们直接通过array方法打印可能出现乱码的问题。
- byteBuffer.clear();清除Buffer,在数据读取完毕后,我们一般会调用byteBuffer.clear();方便下次数据的写入,如果不调用的话,每次读数的数据都是在上次数据后的追加。其实byteBuffer.clear()方法也不是真的把buffer中的数据清除掉,他也是将buffer中的mark、position、limit进行重新标记了一下,方便下次数写入,我们来看一下源码:
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
他和flip()的区别就在与limit 的值,下面我们举例说明:
比如说我们在Buffer中有一个“helloword"字符串。并且我们Buffer的 capacity设置的为20,则:
当前buffer的各项值:
position = 9
limit = 20
capacity = 20
执行flip后:
position = 0
limit = 9
capacity = 20
执行clear 后:
position = 0
limit = 20
capacity = 20
package Buffer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
/**
* @创建人:zhangzhiqiang
* @创建时间:2019/09/03
* @描述:
* @联系方式:QQ:125717901
**/
public class Buffer1 {
public static void main(String[] args) {
//1 将数据写到buff
CharBuffer charBuffer = CharBuffer.allocate(20);
charBuffer.put("helloword");
System.out.println("capacity = "+charBuffer.capacity());
System.out.println("limit = " +charBuffer.limit());
System.out.println("position = "+charBuffer.position());
charBuffer.flip();
System.out.println("执行flip后:");
System.out.println("capacity = "+charBuffer.capacity());
System.out.println("limit = " +charBuffer.limit());
System.out.println("position = "+charBuffer.position());
System.out.println("执行clear后:");
charBuffer.clear();
System.out.println("capacity = "+charBuffer.capacity());
System.out.println("limit = " +charBuffer.limit());
System.out.println("position = "+charBuffer.position());
System.out.println("打印buffer中的数据 = "+ charBuffer.clear());
}
}
执行后的结果我们可以冲控制台看到和我们上面分析的结果一致,执行clear后,我们换是可以将buffer的数据打印出来的。
下面是position 和limit的一个图例:
通过上面的例子我们了解的CHannel、和Buffer的基本一些用法和原理,写了一个简单的例子,展示了一下客户端socket通过System的in方式获取数,然后发送到服务端,服务端是如何接受的,在这里的基础上,我们下一步引入selector选择器,如果是没有selector,我感觉Channel和传统的socket没有什么大的区别,并且意义不大。接下来我们引入selector,还是通过详细的示例一步一步给大家讲解。
。。。。。。。。。。。。。