网络上的两个程序经过一个双向的通讯链接实现数据的交换,这个链接的一端称为一个socket。
创建网络通讯链接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员作网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通讯的能力。
Socket的英文原义是“孔”或“插座”。做为BSD UNIX的进程通讯机制,取后一种意思。一般也称做套接字,用于描述IP地址和端口,是一个通讯链的句柄,能够用来实现不一样虚拟机或不一样计算机之间的通讯。在Internet上的主机通常运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不一样的端口对应于不一样的服务
示例代码html
//Server 端首先建立了一个serverSocket来监听 8000 端口,而后建立一个线程,线程里面不断调用阻塞方法 serversocket.accept();获取新的链接,见(1),当获取到新的链接以后,给每条链接建立一个新的线程,这个线程负责从该链接中读取数据,见(2),而后读取数据是以字节流的方式,见(3)。 public class IOServer { public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(8000); // (1) 接收新链接线程 new Thread(() -> { while (true) { try { // (1) 阻塞方法获取新的链接 Socket socket = serverSocket.accept(); // (2) 每个新的链接都建立一个线程,负责读取数据 new Thread(() -> { try { int len; byte[] data = new byte[1024]; InputStream inputStream = socket.getInputStream(); // (3) 按字节流方式读取数据 while ((len = inputStream.read(data)) != -1) { System.out.println(new String(data, 0, len)); } } catch (IOException e) { } }).start(); } catch (IOException e) { } } }).start(); } }
//链接上服务端 8000 端口以后,每隔 2 秒,咱们向服务端写一个带有时间戳的 "hello world" public class IOClient { public static void main(String[] args) { new Thread(() -> { try { Socket socket = new Socket("127.0.0.1", 8000); while (true) { try { socket.getOutputStream().write((new Date() + ": hello world").getBytes()); Thread.sleep(2000); } catch (Exception e) { } } } catch (IOException e) { } }).start(); } }
java.io包的好处是代码比较简单、直观,缺点则是 IO 效率和扩展性存在局限性,容易成为应用性能的瓶颈。不少时候,人们也把 java.net下面提供的部分网络 API,好比 Socket、ServerSocket、HttpURLConnection 也归类到同步阻塞 IO 类库,由于网络通讯一样是 IO 行为。java
在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,能够构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操做系统底层的高性能数据操做方式。react
在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有不少人叫它 AIO(Asynchronous IO)。异步 IO 操做基于事件和回调机制,能够简单理解为,应用操做直接返回,而不会阻塞在那里,当后台处理完成,操做系统会通知相应线程进行后续工做git
IO流即input和output流,是同步 阻塞程序员
NIO之因此是同步,是由于它的accept/read/write方法的内核I/O操做都会阻塞当前线程
NIO三个组成部分,Channel(通道)、Buffer(缓冲区)、Selector(选择器)算法
Channel:Channel是一个对象,能够经过它读取和写入数据。能够把它看作是IO中的流,不一样的是:编程
Channel是双向的,既能够读又能够写,而流是单向的 Channel能够进行异步的读写 对Channel的读写必须经过buffer对象
全部数据都经过Buffer对象处理,因此永远不会将字节直接写入到Channel中
在Java NIO中的Channel主要有以下几种类型:segmentfault
FileChannel:从文件读取数据的 DatagramChannel:读写UDP网络协议数据 SocketChannel:读写TCP网络协议数据 ServerSocketChannel:能够监听TCP链接
Buffer:Buffer是一个对象,它包含一些要写入或者读到Stream对象的。应用程序不能直接对 Channel 进行读写操做,而必须经过 Buffer 来进行,即 Channel 是经过 Buffer 来读写数据的
使用 Buffer 读写数据通常遵循如下四个步骤:数组
1.写入数据到 Buffer; 2.调用 flip() 方法; 3.从 Buffer 中读取数据; 4.调用 clear() 方法或者 compact() 方法。
//CopyFile执行三个基本的操做:建立一个Buffer,而后从源文件读取数据到缓冲区,而后再将缓冲区写入目标文件 public static void copyFileUseNIO(String src,String dst) throws IOException{ //声明源文件和目标文件 FileInputStream fi=new FileInputStream(new File(src)); FileOutputStream fo=new FileOutputStream(new File(dst)); //得到传输通道channel FileChannel inChannel=fi.getChannel(); FileChannel outChannel=fo.getChannel(); //得到容器buffer ByteBuffer buffer=ByteBuffer.allocate(1024); while(true){ //判断是否读完文件 int eof =inChannel.read(buffer); if(eof==-1){ break; } //重设一下buffer的position=0,limit=position buffer.flip(); //开始写 outChannel.write(buffer); //写完要重置buffer,重设position=0,limit=capacity buffer.clear(); } inChannel.close(); outChannel.close(); fi.close(); fo.close(); }
主要步骤和元素:缓存
- 首先,经过 Selector.open() 建立一个 Selector,做为相似调度员的角色。
- 而后,建立一个 ServerSocketChannel,而且向 Selector 注册,经过指定 SelectionKey.OP_ACCEPT,告诉调度员,它关注的是新的链接请求。
- 注意,为何咱们要明确配置非阻塞模式呢?这是由于阻塞模式下,注册操做是不容许的,会抛出 IllegalBlockingModeException 异常。
- Selector 阻塞在 select 操做,当有 Channel 发生接入请求,就会被唤醒。
- 在 具体的 方法中,经过 SocketChannel 和 Buffer 进行数据操做
IO 都是同步阻塞模式,因此须要多线程以实现多任务处理。而 NIO 则是利用了单线程轮询事件的机制,经过高效地定位就绪的 Channel,来决定作什么,仅仅 select 阶段是阻塞的,能够有效避免大量客户端链接时,频繁线程切换带来的问题,应用的扩展能力有了很是大的提升
AIO是异步IO的缩写
对于NIO来讲,咱们的业务线程是在IO操做准备好时,获得通知,接着就由这个线程自行进行IO操做,IO操做自己是同步的
可是对AIO来讲,则更加进了一步,它不是在IO准备好时再通知线程,而是在IO操做已经完成后,再给线程发出通知。所以AIO是不会阻塞的,此时咱们的业务逻辑将变成一个回调函数,等待IO操做完成后,由系统自动触发
与NIO不一样,当进行读写操做时,只须直接调用API的read或write方法便可。这两种方法均为异步的,对于读操做而言,当有流可读取时,操做系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操做而言,当操做系统将write方法传递的流写入完毕时,操做系统主动通知应用程序。 便可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。 在JDK1.7中,这部份内容被称做NIO.2,主要在Java.nio.channels包下增长了下面四个异步通道:
- AsynchronousSocketChannel
- AsynchronousServerSocketChannel
- AsynchronousFileChannel
- AsynchronousDatagramChannel
在AIO socket编程中,服务端通道是AsynchronousServerSocketChannel,这个类提供了一个open()静态工厂,一个bind()方法用于绑定服务端IP地址(还有端口号),另外还提供了accept()用于接收用户链接请求。在客户端使用的通道是AsynchronousSocketChannel,这个通道处理提供open静态工厂方法外,还提供了read和write方法。
在AIO编程中,发出一个事件(accept read write等)以后要指定事件处理类(回调函数),AIO中的事件处理类是CompletionHandler<V,A>,这个接口定义了以下两个方法,分别在异步操做成功和失败时被回调。
void completed(V result, A attachment);
void failed(Throwable exc, A attachment);
只支持信号在一个方向上传输
适用:数据收集系统,如气象数据收集 话费收集的集中计算等
容许信号在两个方向上传输,但某一时刻只容许在一个信道上单向传输
适用:问讯 检索 科学计算等数据通讯系统 如对讲机
容许数据同时在两个方向上传输,既有两个信道
适用:如计算机 手机 电话通讯
通讯中,协议是指双方实体完成通讯或服务所必须遵循的规则和约定,是双方对传输/接收数据流的编解码的实现算法。
数据在网络上是以字节流(二进制流)的形式传输的,而字节的定义在全部计算机上都是8bit。因此面向协议编程是与语言无关的。
三要素:
消息:协议实例化后即是消息。
消息分类:
- 一类消息:服务端与客户端之间通讯的全部消息长度都是必定范围的。
- 二类消息:绝大部分消息长度都未超过某阈值,但偶尔有几个消息长度超过,但不能够超过太多。
- 三类消息:消息太长而没法完整的进行内存存储
通常消息有两部分组成
消息长度=消息头长度+消息体长度
一个简单的协议算法:
- 标志当前buffer的position位置
- 获取本次消息的消息体长度,position递增1位
- 判断当前读取的消息长度是否知足消息体长度
- 出现半包,数据不完整,重置标志位,并返回null终止本次解码
- buffer中包含完整的消息体内容,则进行读取,position=position+增长消息体长度
- 更新标志位
- 将已读数据转换为字符串并返回,解码成功
public class StringProtocol implements Protocol<String> { public String decode(ByteBuffer buffer, AioSession<String> session) { buffer.mark(); // 1 byte length = buffer.get(); // 2 if (buffer.remaining() < length) { // 3 buffer.reset(); // 4 return null; } byte[] body = new byte[length]; buffer.get(body); // 5 buffer.mark(); // 6 return new String(body); // 7 } }
绝大多数协议或协议中的某字段约定的解析规则有两种:定长解析,特殊结束符解析
Reactor模式也叫反应器模式,大多数IO相关组件如Netty、Redis在使用的IO模式,是高性能网络编程的必知必会模式
while(true){ socket = accept(); handle(socket) }
这种同步阻塞方式没法并发,效率过低
实际上的Reactor模式,是基于Java NIO的,在他的基础上,抽象出来两个组件——Reactor和Handler两个组件:(1)Reactor:负责响应IO事件,当检测到一个新的事件,将其发送给相应的Handler去处理;新的事件包含链接创建就绪、读就绪、写就绪等。
(2)Handler:将自身(handler)与事件绑定,负责事件的处理,完成channel的读入,完成处理业务逻辑后,负责将结果写出channel
buffer:channel读写操做的内存,利用byte[]做为缓存区,一些属性:
属性 | 描述 |
---|---|
capacity | 容量,便可以容纳的最大数据量;在缓冲区建立时被设定而且不能改变 |
limit | 上界,缓冲区中当前数据量 |
position | 位置,下一个要被读或写的元素的索引 |
mark(位置标记) | 调用mark(pos)来设置mark=pos,再调用reset()可让position恢复到标记的位置即position=mark |
缺点:当其中某个 handler 阻塞时, 会致使其余全部的 client 的 handler 都得不到执行, 而且更严重的是, handler 的阻塞也会致使整个服务不能接收新的 client 请求(由于 acceptor 也被阻塞了)。 由于有这么多的缺陷, 所以单线程Reactor 模型用的比较少。这种单线程模型不能充分利用多核资源,因此实际使用的很少,
仅仅适用于handler 中业务处理组件能快速完成的场景。
将Handler处理器的执行放入线程池,多线程进行业务处理
对于多个CPU的机器,为充分利用系统资源,将Reactor拆分为两部分
Netty能够基于如上三种模型进行灵活的配置
Reactor编程的优势
1)响应快,没必要为单个同步时间所阻塞,虽然Reactor自己依然是同步的; 2)编程相对简单,能够最大程度的避免复杂的多线程及同步问题,而且避免了多线程/进程的切换开销; 3)可扩展性,能够方便的经过增长Reactor实例个数来充分利用CPU资源; 4)可复用性,reactor框架自己与具体事件处理逻辑无关,具备很高的复用性;和缺点
1)相比传统的简单模型,Reactor增长了必定的复杂性,于是有必定的门槛,而且不易于调试。 2)Reactor模式须要底层的Synchronous Event Demultiplexer支持,好比Java中的Selector支持,操做系统的select系统调用支持,若是要本身实现Synchronous Event Demultiplexer可能不会有那么高效。 3) Reactor模式在IO读写数据时仍是在同一个线程中实现的,即便使用多个Reactor机制的状况下,那些共享一个Reactor的Channel若是出现一个长时间的数据读写,会影响这个Reactor中其余Channel的相应时间,好比在大文件传输时,IO操做就会影响其余Client的相应时间,于是对这种操做,使用传统的Thread-Per-Connection或许是一个更好的选择,或则此时使用改进版的Reactor模式如Proactor模式。
Netty位于应用层,而网络数据的处理是操做系统底层按照字节流来读写的,而后传到应用层从新拼装bytebuf,这就有可能形成读写不对等。
在Netty中,已经造好了许多类型的拆包器,能够直接使用:
传统意义上发送数据:
经过java的FileChannel.transferTo方法,能够避免上面两次多余的拷贝(固然这须要底层操做系统支持)
- bytebuffer
Netty发送和接收消息主要使用bytebuffer,bytebuffer使用对外内存(DirectMemory)直接进行Socket读写。
缘由:若是使用传统的堆内存进行Socket读写,JVM会将堆内存buffer拷贝一份到直接内存中而后再写入socket,多了一次缓冲区的内存拷贝。DirectMemory中能够直接经过DMA发送到网卡接口
- Composite Buffers
传统的ByteBuffer,若是须要将两个ByteBuffer中的数据组合到一块儿,咱们须要首先建立一个size=size1+size2大小的新的数组,而后将两个数组中的数据拷贝到新的数组中。可是使用Netty提供的组合ByteBuf,就能够避免这样的操做,由于CompositeByteBuf并无真正将多个Buffer组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝。
- 对于FileChannel.transferTo的使用
Netty中使用了FileChannel的transferTo方法,该方法依赖于操做系统实现零拷贝