一、概述

       随着互联网的兴起,网络编程不再专家性的研究领域,已经成为了很多开发人员都必须掌握的内容。在开发过程中,网络的程序已然很多。除了经典的应用程序(如电子邮件、浏览器等),不少应用程序都在某种程度上与网络功能有些联系。       

        Java一开始就为网络应用而进行了设计,目前Java已成为非常适合构建网络应用程序的语言了。因为个人学习和工作的学习,需要学习网络编程的开发,在学习了基础知识后,使用netty并用到工作中。下面是一些网络编程中涉及到的基本概念。

二、网络基础

2.1 网络

          网络(Network)是几乎可以实时相互发送和接收数据的计算机和其他设备的集合。网络中的每台机器称为一个节点(node),大多数是计算机,但也有如打印机等这样的设备。可以使用Java与打印机进行交互,但多数情况下都是其与计算机对话。具有完备功能的计算机节点也称为主机(host)。每个网络节点都有地址(address),这是用于唯一标识节点的一个字节序列。

        现在计算机网络都是包交换(分组交换)网络:流经网络的数据分割成小块,称为包(packet,也称分组),每个包都单独加以处理。每个包都包含了由谁发送和发往何处的信息。将数据分成单独的带有地址的包,其最重要的优点是多个即将交换的包可以在一第线缆上传输,这样建立网络的成本更低,多个计算机呆以互不干扰的共用一条线缆。其另一个好处是可以进行校验,用来检测包在传输中是否遭到破坏。

        但计算机来回传递数据时,还需要提供协议。协议(protocol)是定义计算机如何通信的明确的规则,它包含了地址格式、数据如何分包等。在网络通信的不同方面,也有很多不同的协议。如超文本传输协议(Hypertext Transfer Protocol, HTTP

)定义了Web浏览器如何与服务器通信等。

 

2.2 网络分层

        通过网络发送数据是一项复杂的操作,须协调网络的物理特性及所发数据的逻辑特征。发送数据的软件须了解如何避免包的冲突,将数字数据转换为模拟信息,检测和修正错误等。

        为了对应用程序开发人员和最终用户隐藏这种复杂性,网络通信的不同方面被分解为多层。每一层表示物理硬件(即线缆和电流)与所传输信息之间的不同抽象层次。在理论上,每一层只与紧挨其上和其下的层对话。这样可修改甚至替换某一层的软件,只要层与层之间的接口保持不变,就不会影响到其他层。下图是ISO的七层网络模型:

         下图是标准的TCP/IP四层模型:

        上述各层将数据逐级上移传输到远程系统的应用层。

        

2.3 TCP/UDP/URL

2.3.1 TCP

       Transmission Control Protocol,传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。它保障了两个应用程序之间的可靠通信,通常用于互联网协议。在简化的计算机网络OSI模型中,它完成第四层传输层所指定的功能。在因特网协议族(Internet protocol suite)中,TCP层位于IP层之上,应用层之下的中间层。应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,然后TCP把数据流分区成适当长度的报文段,之后TCP把结果包传给IP层,由它来通过网络将包传送给接收端实体的TCP。

2.3.2 UDP

       User Datagram Protocol,是用户数据协议的缩写,一个无连接的协议。提供面向事务的简单不可靠信息传送服务,换言之,提供了应用程序之间要发送的数据的数据包。在网络中与TCP协议一样用于处理数据包,在OSI模型中,位于第四层——传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,即当报文发送之后,是无法得知其是否案例完整到达的。UDP协议的主要作用是将网络数据流量压缩成数据包的形式。一个典型的数据包就是一个二进制数据的传输单位。

2.3.3 URL

        Uniform Resource Locator,统一资源定位符,有时也被俗称为网页地址。表示互联网上的资源,比如网页或FTP地址。URL解析如下:

  1. 协议(protocol):http;
  2. 主机是(host:port):www.baidu.com;
  3. 端口号(port):80,上面的URL没有指定端口,因为HTTP协议默认的端口为80;
  4. 文件路径(path):/index.html;
  5. 请求参数(query):language=cn;
  6. 定位位置(fragment):name,定位到网页中id属性为name的HTML元素位置。

 

三、(非)阻塞(Block)与同/异步

3.1 阻塞(Blocking)

        调用结果返回之前,其它线程被挂起。对于CPU是一直等待CPU处理完成,然后才会执行后面的操作(CPU跑去干别的事了,没空搭理我)。比如浏览器调用应用程序的某个接口,阻塞就是浏览器发起请求A后,会阻塞后面的请求,直接应用程序处理完并返回结果给请求A。

 

3.2 非阻塞(Non-Blocking)

        在不能立即返回结果之前,不会阻塞当前线程,而是立即返回,这样不会影响其它线程的使用(可以通过主动轮询得知调用情况)。CPU处理一个任务时,不管当时来不来得及会不会处理完,都会返回,然后处理后面的任务。和异步看起来非常像。不过个人想从字面上去分析一点,阻塞强调了运行的顺畅,侧重于负重(像不堵车),异步强调了运行的流转,之间相互不影响。

         比如浏览器调用应用程序的某个接口,阻塞就是浏览器发起请求A后,应用程序会立即返回,所以不会阻塞后面的请求。即应用程序还没开始开始处理或已经在处理的时候,就会先返回。然后再悄悄的处理完,再次返回真正结果给请求A。

 

3.3 同步(Synchronization)

       同步的方法调用时,调用者必须等待方法返回,然后才能继续执行后面的操作。而异步方法调用后就会立即返回,调用都不用等待就可以继续执行后面的操作。

 

3.4 异步(Asynchronous)

       异步方法调用之所以立即有返回而不用等待,是“隐藏了”真正的执行——通常会在另外的线程中去执行,并将结果通知给调用者,这样就不会影响调用者执行后面的操作。通知的方式一般有三种:状态、通知、回调。

  1. 状态:监听被调用者的状态(比如通过轮询、长连接等),调用者需要每隔一段时间进行检查,效率低;
  2. 通知:当被调用者执行完以后,发通知告诉调用者,不是很消耗性能;
  3. 回调:与通知比较类似,当被调用者执行完以后,会调用调用者提供的函数继续执行。

        两者区别:发出同步请求后,需要等待,发出异步请求后,无需等待。

 

3.5 总结

与同步不同,同步虽然也是要等到调用有结果返回,是因为它们有依赖关系,后面的执行需要根据返回结果再做决定,所以在没有返回结果之前,其实仍然是处于激活的,我还可以处理没有依赖关系的事情。而阻塞就像堵车了,同步是我就等你给我了一个结果。

同步和异步通常用来描述应用程序的调用方式,比如浏览器调用服务器的方法。而在服务器内部,会区分同步与异步。实际上同异步是针对应用程序与内核的交互来说的。另外,同步讲的是服务端的执行方式。

        阻塞与非阻塞,是进程在访问数据时,数据是否准备就绪的一处理方式。当数据没有准备时会阻塞,往往需要等待缓冲区中的数据准备好后才处理其它事情,否则就一直处于等待中。而对于非阻塞,若数据没有准备好也会直接返回,不会一直去等待。当数据准备好后,直接返回。阻塞强调的是具体的技术,接收数据的方式及状态(处于阻塞还是非阻塞)。

 

四、各种IO及区别

       在网络世界时,一切皆是文件。而文件是二进制的,不管对套接字(Socket)、管道(Channel)等来说,一切都是(信息)流。IO操作,通常是读(read)和写(write)。

       对于一个网络IO来说,以读来说明。它会涉及两个系统对象:一是调用这个IO的进程(process)或线程(thread);二是系统内核(kernel)。当一个读操作发生时,它会经历如下三个阶段,不同IO模型的区别就体现在这几个阶段上,每个不同的IO模型这三个阶段有不同的表现。再具体一点说,阻塞与否、同步或异步主要表现在这几个地方体现。

  1. 连接请求及数据传输:比如浏览器调用服务器中的某个接口,发起请求,同时传递输入数据;再比如外卖,用户下单时,数据就是订单的信息;
  2. 等待数据准备:比如根据输入数据进行处理,但对外部表现是数据准备,就好比点外卖,用户下单了,你对外是准备用户所点的某个食品,但对餐饮来说是有一系列的处理;
  3. 将数据从内核拷贝到进程中:还是以外卖来说,是餐馆将用户所点的食品返回给用户,当然目前是骑手送,但餐馆仍需将食品返回给骑手,由骑手代为送达。

 

4.1 BIO

先在服务端启动ServerSocket,然后在客户端启动Socket,客户端与服务端进行连接通讯。默认情况下,服务端会对每个客户端的请求建立线程等待请求,客户端发送连接请求。整体过程如下图:

dht网络编程 java java基础网络编程_客户端

       通常是Acceptor线程负责客户端的连接。它在接收到客户连接请求后,为每个客户端创建一个新的线程进行链路处理,处理完后,通过输出流应答给客户端,最后线程销毁。如下图所示:

dht网络编程 java java基础网络编程_nio_02

因为数据还没到而导致的阻塞(可能性很小,时间一般也比较短)。对于多个连接请求,也不可能一次全部都接收了。在内核接收数据后需要进行处理,但在处理好并返回给某个进程A前,某个进程A会被阻塞,因为它需要处理后的数据,但此时内核可能还没有返回它要的数据。当内核将数据准备好以后,它会将数据从内核拷贝到A的内存,然后内核再返回结果。这时某个进程A才解除阻塞状态,就可以顺畅的跑起来了。所以从这个过程来看,个人理解BIO有两个阻塞的地方,一是连接请求后,数据由进程到内核的过程(这个可能性很小,时间一般也比较短);二是内核处理数据过程中,进程A等待数据的过程。

dht网络编程 java java基础网络编程_socket_03

       BIO因为性能不高,且并发性差,所以在并发访问量增大后,线程数会较多,系统性能急剧下降。

 

4.2 NIO

       BIO(Non-block I/O)非阻塞IO,也是新的IO(New I/O)。就一个调用来说,当进程A向系统B发送数据,要进行读操作时,若系统B内核的数据还没有准备好,它并不会阻塞进程A,而是先返回一个信息(错误提示)。当系统B内核的数据准备好后,再次接收到进程A的请求时,就会将数据拷贝,返回给进程A。另外,非阻塞IO中涉及Buffer、Selector等几个概念,会在下方说明。

dht网络编程 java java基础网络编程_客户端_04

        以外卖来说,用户小明点了份辣椒炒肉(发送请求,并传入数据),当餐馆接单后(请求成功),此时菜没有做好,但立马就会告诉小明过半个小时做好(没有准备好返回的数据,但在请求后马上收到消息)。然后餐馆马上做菜,快速做完后,就马上把菜送给骑手(真实的数据返回)。这样小明就不会一直阻塞着,也就不会影响后面的用户下单了。若是BIO,那小明下单就会一直等待做的菜,后面的用户就下不了单了。

dht网络编程 java java基础网络编程_dht网络编程 java_05

 

dht网络编程 java java基础网络编程_客户端_06

dht网络编程 java java基础网络编程_dht网络编程 java_07

      Java的NIO和IO的区别如下表所示。BIO在客户端连接服务器端时,借助于多线程,连接成功后传递数据进行处理。而NIO有缓冲区(Buffer)和选择器(Selector),缓冲区包含了写入或读取的数据,不是直接的Stream。

IO模型

IO

NIO

方式

从硬盘到内存

从内存到硬盘

通信

面向流

面向缓冲(多路复用技术)

处理

阻塞IO

非阻塞IO(反应堆Reactor)

触发


选择器Selector(轮询机制)

 

 

 

       

 

 

4.3 伪异步IO

      伪异步IO,不是真正的异步IO,是对同步阻塞IO进行了优化,看起来像是异步IO。

 

4.4 AIO

        Asynchronous IO,即异步IO。假如进程A要执行读取数据的操作,发起请求并请求成功后,进程A就可以做别的事了。系统内核接收到一个异步的读请求后,它会先立即返回,然后再去进行处理,所以不会阻塞。当系统内核处理完后,将返回的数据拷贝到进程A内存。最后内核再给进程A发现一个信号,说我处理完了,你可以直接拿读取的数据了。

        还是以外卖来说,小明在餐馆下单(异步请求),说半个小时后点的外卖会送到(餐馆接受请求后会先返回,只不过个间隔时间会比较长)。餐馆接单(请求连接成功),下单成功后小明就可以做自己的事了,比如开个玩笑,继续写小明的故事续集。餐馆开始准备做菜(调用的程序隐藏处理的过程),当菜做好了(处理结果准备好了),骑手会来拿菜送给小明(这里骑手充当了小明的代理,这里返回的数据原本是直接给小明的,但这里是先给了骑手,再由骑手送给小明)。

 

4.5 各种IO的区别 

        上面对四种IO模型做了简单描述,下面的两张图从不同方面展示了各种不同IO的区别:

dht网络编程 java java基础网络编程_socket_08

dht网络编程 java java基础网络编程_socket_09

 

五、NIO体系

5.1 Buffer

在NIO类库中加入Buffer对象,体现了新库与原IO的一个重要区别。在面向流的IO中,可以将数据直接写入或读取到Stream对象中。在NIO库中,所有的数据都是用缓存区处理的(读写)。缓存区实质上是一个数组,通常是一个字节数组(ByteBuffer),也可以使用其他类型的数组。这个数组为缓冲区提供了数据的访问读写等操作属性,如位置、容量、上限等概念。

        Buffer有多种类型,最常用的是ByteBuffer,实际上每一种Java基本类型都对应了一种缓存区类型(Boloean除外),如ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer等。

 

5.2 Channel

        管道,通道。数据通过Channel读取和写入。需要注意,通道和流不一样,通道是双向的,所以通道可以用于读、写或二者同时进行,最关键的是可以与多路复用器结合起来,有多种的状态位,方便多路复用器去识别。而流只是一个方向上移动(一个流必须是InputStream或OutPutStream的子类)。

       通道可分为两大类,一类是网络读写(SelectableChannel),一类是用于文件操作(FileChannel)。通常使用的SocketChannel和ServerSocketChannel都是SelectableChannel的子类。

 

5.3 Selector

选择已经就绪的任务的能力。它会不断的轮询注册于其上的通道(Channel),若某通道发生了读写操作,那这个通道就处于就绪状态,就会被Selector轮询出来,然后通过SelectionKey可以取得就绪的Channel集合,从而执行后续的IO操作。一个多路复用器可以负责成千上万的Channel通道,没有上限。这也是JDK使用epoll代替了传统的select实现,获得连接句柄没有限制,这就意味着只要一个线程负责Selector的轮询,就可以接入成千上万个客户端,这就JDK NIO的巨大进步。

       Select线程类似一个管理者(Master),它管理成千上万个管道。通过轮询管道的数据,对数据已经准备好的管道,Selecter通知CPU执行IO的读写操作。

        Selector模式:当IO事件(管道)注册到选择器以后,selector会分配给每个管道一个key值,相当于标签。selector是以轮询的方式进行查找注册的所有IO事件的,当IO事件准备就绪好,selector便会识别,然后通过key值找到相应的管道,进行数据处理(从管道读或写数据,写到我们数据缓冲区中)。

        每个管道都会对选择器注册不同的事件状态,以便选择器查找:

  • SelectionKey.OP_CONNECT:连接状态
  • SelectionKey.OP_ACCEPT:阻塞状态
  • SelectionKey.OP_READ:可读状态
  • SelectionKey.OP_WRITE:可写状态

5.4 Reactor

       Reactor模式是事件驱动的,具体一点说,有一个或多个并发输入源,还有一个Service Handler,还有多个Request Handlers,Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler。

        在Java的NIO中,对Reactor模式有无缝的支持,使用Selector类封装了操作系统提供的Synchronous Event Demultiplexer功能。Reactor的核心是Selector。

 

六、ServerSocket与Socket

        Java对非阻塞I/O的支持是在2002年引入的,在JDK的java.net包下。

 

6.1 Socket

        套接字,是网络连接的一个端点,它使一个应用可以从网络中读取和写入数据。在不同计算机上的两个应用可通过连接发送和接收字节流。其中需要知道连接的IP和端口,以可以进行准确的连接。总结来说,是应用程序可通过套接节向网络发出请求或应答网络请求。

dht网络编程 java java基础网络编程_客户端_10

 

6.2 ServerSocket

      服务器套字节, ServerSocket代表了服务器端,而Socket代表了一个客户端套接字。accept()方法是侦听并接收套接字的连接。

dht网络编程 java java基础网络编程_dht网络编程 java_11

        ServerSocket和Socket位于java.net包中。ServerSocket用于服务顺端,Socket是建立网络连接时使用的。在连接成功时,应用程序两端都会产生一个Socket实例。然后操作中此实例,完成所需要的会话。对网络连接而言,套接字是平等的,即在服务器端和客户端的级别是一样的。不管是Socket还是ServerSocket,它们工作都是通过SocketImpl类及其子类完成的。

        套接字之间的连接过程分为以下四步:

  1. 服务器监听:服务器处于等待连接状态,实时监控网络状态;
  2. 客户端请求服务器:客户端的套接字发出连接请求;
  3. 服务器确认;服务器端套接字监听到或接收到客户端套接字的连接请求,响应请求,将服务器端套接字的描述发送给客户端;
  4. 客户端确认:客户端确认了描述,连接就建立了,与服务器端进行通信。服务器端套接字处于监听状态,继续继续接收客户端套接字的连接请求。

        经过以上四步,然后进行通信。通信完成后,关闭套接字。整个过程是由客户端和服务器端组成的,对于服务器端,具体有以下几步:

  1. 创建套接字;
  2. 套接字绑定在一个地址和端口上;
  3. 套接字设为监听模式,准备接收客户端请求;
  4. 等待客户端请求,当有客户端的请求来时,接受客户端的连接请求,返回新的对应此连接的套接字,启动线程为当前的连接服务;
  5. 处理完请求后返回,等待另一个客户端的请求;
  6. 关闭套接字。

        对于客户端来说,具体有以下几步:

  1. 创建套接字;
  2. 向服务器发出连接请求;
  3. 和服务器进行通信;
  4. 关闭套接字。