什么是网络编程?
网络编程的本质是两个设备之间的数据交换,当然,在计算机网络中,设备主要指计算机。数据传递本身没有多大的难度,不就是把一个设备中的数据发送给两外一个设备,然后接受另外一个设备反馈的数据。 现在的网络编程基本上都是基于请求/响应方式的,也就是一个设备发送请求数据给另外一个,然后接收另一个设备的反馈。
如何进行网络编程?
-
Socket是什么?
操作系统为程序员提供了网络相关的 API ,通常把它叫做 Socket。为此,网络编程也叫 Socket 编程、套接字(套接字=主机+端口号)编程。
通过 socket这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read关闭close”模式来操作。
我的理解就是Socket就是该模式的一个实现,它只是提供了一个针对TCP或者UDP编程的接口:即socket是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。
-
Socket通信流程
Socket通信实现步骤解析:
(1)创建ServerSocket和Socket
(2)打开连接到的Socket的输入/输出流
(3)按照协议对Socket进行读/写操作
(4)关闭输入输出流,以及Socket
示例代码:
/** 服务端:*/
public class SocketServer {
public static void main(String[] args) throws IOException {
//1.创建一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口
ServerSocket serverSocket = new ServerSocket(12680);
//2.调用accept()等待客户端连接
Socket socket = serverSocket.accept();
//3.连接后获取输入流,读取客户端信息
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(socket.getInputStream()));
String info = null;
while((info=br.readLine())!=null){
//循环读取客户端的信息
System.out.println("客户端发送过来的信息" + info);
}
//关闭输入流
socket.shutdownInput();
socket.close();
}
}
/** 客户端:*/
private void acceptServer() throws IOException {
//1.创建客户端Socket,指定服务器地址和端口
Socket socket = new Socket("127.0.0.1", 12680);
//2.获取输出流,向服务器端发送信息
OutputStream os = socket.getOutputStream();
PrintWriter pw = new PrintWriter(os);
//3、获取客户端的IP地址
InetAddress address = InetAddress.getLocalHost();
String ip = address.getHostAddress();
pw.write("客户端:" + ip + " 已连接服务器!");
pw.flush();
//关闭输出流
socket.shutdownOutput();
socket.close();
}
-
经典的三种IO模式
BIO
同步阻塞IO模式:一直阻塞直到数据就绪
-
优点:模型简单、编码简单
-
缺点:性能瓶颈
服务器的实现模式是一个连接一个线程,由于客户端连接数与服务器线程数成正比关系,可能造成不必要的线程开销,严重的还将导致服务器内存溢出。
-
适用场景:BIO方式适用于连接数比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限性,BIO是在JDK1.4以前唯一选择,BIO程序简单易理解。一般在客户端编程里使用更多一点,客户端不需要接受外来请求,所以这个缺点对他没什么影响。
NIO
同步非阻塞IO模式:会有一个Selector管理多个线程,当有事件发生后,进行处理、不会发生阻塞。阻塞的主体是selector,等待事件,而非读写数据时的阻塞。
-
优点:性能瓶颈高
-
缺点:(1)模型复杂、编码复杂(2)需处理半包问题
-
适用场景:适用于连接数目多且流量小的架构(轻操作),比如聊天服务器
AIO
异步非阻塞IO:服务器实现模式为一个有效请求对应一个线程,客户端的IO请求都是由操作系统先完成IO操作后再通知服务器应用来直接使用准备好的数据。
-
优点:并发性高,CPU和线程利用率高
-
缺点:不适合轻量级数据传输。因为进程间对于通信管理和资源消耗大,接收数据需要预先分配缓存,会造成内存的浪费。AIO在Windows上实现很成熟,但Windows本身很少用作服务器,Linux下AIO的实现不够成熟且相较NIO性能提升不够明显。
-
适用场景:适用于连接数目多且流量大的架构(重操作),比如相册服务器,充分调用操作系统参与并发操作,编程比较复杂
JAVA NIO 、AIO的不足之处
-
虽然JAVA NIO 和 JAVA AIO框架提供了多路复用IO/异步IO的支持,但是并没有提供上层“信息格式”的良好封装。例如前两者并没有提供针对 ProtocolBuffer、JSON这些信息格式的封装,但是Netty框架提供了这些数据格式封装(基于责任链模式的编码和解码功能)
-
JDK的API它并不够友好,功能也比较弱,例如ByteBuffer这个类,它里面只有一个指针,维护它的状态,所以它在读写切换的时候都要执行一个额外的flip操作
-
还有一个问题是说它的内部是一个final的字节数组,所以它自然无法自动扩容。
-
-
要编写一个可靠的、易维护的、高性能的(注意它们的排序)NIO/AIO服务器应用。除了框架本身要兼容实现各类操作系统的实现外。更重要的是它应该还要处理很多上层特有服务,例如:客户端的权限、还有上面提到的信息格式封装、简单的数据读取。这些Netty框架都提供了响应的支持。
-
JAVA NIO框架存在一个poll/epoll bug:Selector doesn’t block on Selector.select(timeout),不能block意味着CPU的使用率会变成100%(这是底层JNI的问题,上层要处理这个异常实际上也好办)。当然这个bug只有在Linux内核上才能重现。这个问题在JDK 1.7版本中还没有被完全解决:http://bugs.java.com/bugdatabase/view_bug.do?bug_id=2147719。虽然Netty 4.0中也是基于JAVA NIO框架进行封装的(上文中已经给出了Netty中NioServerSocketChannel类的介绍),但是Netty已经将这个bug进行了处理。
Netty
概述
Netty是一个异步事件驱动的网络应用框架,可以用以快速开发高性能、高可靠性的网络服务器和客户端程序。
本质:网络应用程序框架
实现:异步、事件驱动
特性:高性能、可维护、快速开发
用途:开发服务器和客户端
结构
核心层
主要包括三个方面
-
零拷贝的Buffer,为什么叫零拷贝?
因为在数据传输时,最终处理的数据会需要对单个传输层的报文,进行组合或者拆分。NIO原生的ByteBuffer要做到这件事,需要对ByteBuffer内容进行拷贝,产生新的ByteBuffer,而Netty通过提供Composite(组合)和Slice(切分)两种Buffer来实现零拷贝。
-
方便通用的通信层API
Java新的I/O API(NIO)与原有的阻塞式的I/O API(OIO)并不兼容
PS:有人将NIO称作(New-I/O),所以也将BIO称之为(Old-I/O),简称OIO,
-
BIO就是OIO,BIO是阻塞IO模型(Block-I/O)
-
NIO是非阻塞IO模型(Non-Block I/O),
Netty相对于传统的JDK-API实现来说,更加的模块化,我们仅需要在不同的阶段添加我们的处理逻辑就可以了,而不需要实现整个通信流程,然后在通信流程的不同阶段实现我们的逻辑
Netty的OIO、NIO实现几乎一摸一样,说明Netty本身抽象程度足够高,我们仅需要知道Netty的流程就可以,而不需要知道OIO模型或者NIO模型的不同之处。
EventLoopGroup group = new OioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(group)
.channel(OioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host,port))
.handler(new EchoClientHandler());
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host,port))
.handler(new EchoClientHandler());
上面两点已经足够减少我们对于项目模型的构建复杂程度,而只需要专心于服务器逻辑的实现就好
-
第三个方面是可扩展的事件模型,Netty框架是基于事件驱动的,所以设计一个良好的具有很好的可扩展性的事件模型是非常必要的。
传输层服务
支持tcp的socket,udp的datagram、http的 tunnel(隧道)、In-VM的Pipe(管道)
协议支持
它支持各种各样的协议,比如我们常说的HTTP和WebSocket协议
还有Google的protoBuf的支持,这些协议当中明显不属于同一个层次的
Google的protoBuf 它确切应该是一个编解码方式,而http是一种流行的应用层协议。我们打开netty jar包里codec目录,可以看到netty对各种常用编解码方式和各种协议的支持。
所以从这个结果图当中,我们可以看的出来netty分层其实很清晰的,虽然协议这边归类到一起不见得特别科学。但是功能非常全面,具有很强的扩展性
为什么现在只支持NIO?
1、为什么不建议(deprecate)阻塞I/O(BIO/OIO)?
连接数高的情况下:阻塞 -> 耗资源、效率低
但并不是NIO就一定优于BIO,比如在连接数少,并发度低的特定的场景下,BIO的性能不输NIO,相当于1对1的VIP模式了。所以nio不一定优于BIO。但是为了考虑未来的发展,我们都是会推荐NIO,这应该也是netty不推荐BIO的原因。
2、为什么删掉已经做好的AIO支持?
-
Windows实现成熟,但是很少用来做服务器
-
Linux常用来做服务器,但是AIO实现不够成熟
-
Linux下AIO相比较NIO性能提升不明显。
-
Netty中的Reactor模式
3、为什么server端这里要用两个eventLoopGroup,一个boss,一个worker呢?
当我们讨论Netty线程模型的时候,一般首先会想到的是经典的Reactor线程模型。尽管不同的NIO框架对于Reactor模式的实现存在差异,但是本质上还是遵循了Reactor的基础线程模型。
Reactor模式(反应堆模式)是一种基于事件驱动的模式,适合做海量数据的事件,属于同步非阻塞的一种NIO实现模式。也有人称为"分发者模式"。工作原理是由一个线程来接收所有的请求,然后派发这些请求到相关的工作线程中。
Reactor模式的构成
Reactor 模式的核心组成部分包括 Reactor 和处理资源池(进程池或线程池),其中 Reactor 负责监听和分配事件,处理资源池负责处理事件。 基本上Reacotor模式按是否多模式和是否采用多线程可以分为单Reactor单线程,单Reactor多线程,多Reactor多线程三种类型。
Reacotor模式的线程模型
按照Reactor模式类型,有三种线程模型的具体实现:单线程模型,多线程模型,主从多线程模型。
1、单线程模型
单线程就是客户端过来的请求分发后都在一个线程里做处理(处理包括读请求,解码,计算,编码,发送)。
例如,通过Accpetor类接收客户端的TCP连接请求,当链路建立成功之后,通过Dispatch将对应的的ByteBuffer派发到指定的Handler上,进行消息解码。
用户线程消息编码后通过NIO线程将消息发送给客户端。这种模式在小容量应用场景下,可以使用单线程模型,但是对于高负载、大并发的应用场景却不合适
一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的CPU负荷达到100%,也无法满足海量消息的编码、解码,读取和发送,而且一旦这个NIO线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,造成节点故障,所以基于可靠性的考虑,这种模式也不要在高负载、大并发的应用场景下使用。
这种模型的优点是简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成。当然缺点也非常明显,主要体现在性能和可靠性上。首先是只有一个线程,无法完全发挥多核 CPU 的性能,Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈,其次线程意外终止或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。
第一种Reactor单线程模型,是指所有的IO操作都在同一个NIO线程上面完成,如图所示由于Reactor模式使用的是异步非阻塞IO,所有的IO操作都不会导致阻塞,
理论上一个线程可以独立所有IO相关的操作。从架构层面上看,一个NIO线程确实可以完成其承担的工作。
2、多线程模型
第二个Reactor多线程模型与单线程模型最大的区别就是有一组NIO线程来处理IO操作,acceptor线程用于监听服务端,接收客户端的TCP连接请求。
网络IO操作,读写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些线程负责消息的读取,解码,编码,一个NIO线程可以同时处理N条链路,但是一个链路只对应一个NIO线程,防止发生并发操作问题。
在绝大多数场景下,Reactor多线程模型可以满足性能需求。但是在个别特殊场景中,一个NIO线程既负责监听又要处理所有客户端连接可能存在性能问题。
例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是这个认证本身非常损耗性能,单独一个Acceptor线程可能会存在性能不足的问题。
3、主从线程模型
主从多线程模型属于多反应堆多线程模式,和单Reator多线程模型不同的是拆分了主反应堆(mainReactor)和从反应堆(subReactor),是把反应堆分为了主从两个反应堆。
服务端用于接收客户端连接的不再是一个单独的NIO线程,而是一个独立的NIO线程池。Acceptor接收到客户端TCP连接请求并处理完成后,将新创建的SocketChannel注册到subReactor线程池的某一个IO线程上,由它负责SocketChannel的读写和编解码工作。Acceptor线程池仅仅用于客户端的登录、握手和安全认证,一旦链路建立成功,就注册到subReactor线程池上由它的IO线程负责后续的IO操作。利用主从NIO线程模型,可以解决一个服务端监听线程无法有效处理所有客户端连接的性能不足问题。因此,在Netty的官方demo中,推荐使用该线程模型。
所以代码中的boss线程组就相当于mainReactor。worker线程组相当于subReactor,就像老板负责揽活儿,具体的处理让员工来做,分工明确。为了更好的理解。
版本选择
实战
1、通信协议:TCP
2、定义数据传输结构,选择和实现编解码
3、编写我们的客户端和服务器应用程序
4、优化和安全增强