网络的分层:
OSI:
网络划分为7层:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层
TCP/IP:
网络接口层、网络层、传输层、应用层
各层协议:
应用层:HTTP、FTP、SMTP、Telnet
传输层:TCP、UDP
网络层:IP
网络接口层:ARP、RARP
TCP:
TCP报文头、三次握手、四次挥手、滑动窗口、重试机制、拥塞控制…TCP编程
UDP:UDP报文头、UDP的编程
UDP和TCP的区别
网路层:IP协议、IP的协议头,IP协议的划分、 A、B、C、D、E类的网络划分、子网掩码
IP路由规则
socket编程步骤
同步、异步、阻塞、非阻塞
BIO(同步阻塞IO)
NIO(同步非阻塞IO)
AIO(异步非阻塞IO)
网络通信框架:Netty\Mina -》dubbo
参考书籍
《TCP/IP协议 卷一》 第3、9、11、16-21章
网络概括
计算机网络:
通过传输介质、通信设备和网络通信的协议、把分散在不同地点的计算机设备互联起来、实现资源共享和数据传输的系统
传输介质:光纤、网线、光波
通信设备:卫星、光缆、交换机。。。
网络协议:TCP、UDP、IP…
网络编程:编写程序使网络中的两个或多个设备(计算机)直接进行通信
OSI网络模型
国际标准化组织提出的网络的开发互联参考模型OSI(Open System InterConnection),将网络划分成7层
应用层:
OSI的最高层,是直接为应用程序提供服务,作用是实现一系列的业务处理需要服务
表示层:
关注的是传输数据信息的格式定义,为应用成提供的信息服务、提供的数据格式、控制信息的格式、数据加密的统一表示
会话层:
主要功能是负责应用程序的建立和释放、维护通信的稳定,提供单工、半双工、全双工通信方式、使系统服务通信更加有序
传输层:
保证源端点和目的端点之间(应用间可靠传输)的可靠传输,建立连接时三次握手、断开连接时四次挥手。
网络层:
保证源主机节点和目的主机的节点的可靠传输、包括路由选择、网络寻址、流量控制
数据链路层:
计算机网络相邻节点的可靠传输
物理层:
通过通信信道传输原始比特流 ,为数据端设备提供传输数据的通道
TCP/IP协议族
TCP/IP即Transmission Control Protocol/Internet Protocol 的缩写,即是传输控制协议/因特网互联协议
应用层:提供网络应用的服务
传输层:保证源程序到目的程序的端到端的可靠通信(端口)
网络层:源主机到目的主机间的可靠通信(IP)
网络接口层:对应的是OSI中的数据链路层和物理层
各层协议简介
应用层:
协议:FTP、SMTP、HTTP
FTP(File Transfer Protocol):文件传输协议<端口号 21> 进行文件传输,减少或消除不同操作系统下处理文件的不兼容性
HTTP(Hypertext Transfer Protocol)超文本传输协议<端口号 80> 面向浏览器的事务处理协议
SMTP(Simple Mail Transfer Protocol)简单邮件传输协议<端口号25> 用来发送邮件
传输层:
TCP(Transmission Control Protocol)传输控制协议提供的可靠的面向连接的服务
传输数据前需要建立通信,结束后需要释放,全双工,可靠,有序,无丢失,不重复
UDP(User Datagram Protocol)用户数据包协议发送数据是不可靠的,不需要连接,不能保证数据可靠交互交付
网络层:
IP(IPv4、IPv6)(Internet Protocol)网络互连互通协议
ICMP(Internet Control Message Protocol)Internet控制报文协议
数据链路层:
ARP(Address Resolution Protocol)地址解析协议,实现通过IP地址得到物理地址(Mac 地址)
RARP( Reverse Address Resolution Protocol)反向地址解析协议 ,将MAC地址解析成IP地址
数据包
每个分层中,都会对发送的数据封装一个首部,首部包含必要的信息,地址信息和协议信息
数据包首部
网络中传输的数据包由两部分组成:一部分是协议所要用到的首部,另一部分是上一层传过来的数据。首部的结构由协议的具体规范详细定义。在数据包的首部,明确标明了协议应该如何读取数据。反过来说,看到首部,也就能够了解该协议必要的信息以及所要处理的数据。包首部就像协议的脸。
数据处理过程
假如a向b发送消息为例:
数据处理流程
① 应用程序处理
首先应用程序会进行编码处理,这些编码相当于 OSI 的表示层功能;
编码转化后,邮件不一定马上被发送出去,这种何时建立通信连接何时发送数据的管理功能,相当于 OSI 的会话层功能。
② TCP 模块的处理
TCP 根据应用的指示,负责建立连接、发送数据以及断开连接。TCP 提供将应用层发来的数据顺利发送至对端的可靠传输。为了实现这一功能,需要在应用层数据的前端附加一个 TCP 首部。
③ IP 模块的处理
IP 将 TCP 传过来的 TCP 首部和 TCP 数据合起来当做自己的数据,并在 TCP 首部的前端加上自己的 IP 首部。IP 包生成后,参考路由控制表决定接受此 IP 包的路由或主机。
④ 网络接口(以太网驱动)的处理
从 IP 传过来的 IP 包对于以太网来说就是数据。给这些数据附加上以太网首部并进行发送处理,生成的以太网数据包将通过物理层传输给接收端。
⑤ 网络接口(以太网驱动)的处理
主机收到以太网包后,首先从以太网包首部找到 MAC 地址判断是否为发送给自己的包,若不是则丢弃数据。
如果是发送给自己的包,则从以太网包首部中的类型确定数据类型,再传给相应的模块,如 IP、ARP 等。这里的例子则是 IP 。
⑥ IP 模块的处理
IP 模块接收到 数据后也做类似的处理。从包首部中判断此 IP 地址是否与自己的 IP 地址匹配,如果匹配则根据首部的协议类型将数据发送给对应的模块,如 TCP、UDP。这里的例子则是 TCP。
另外吗,对于有路由器的情况,接收端地址往往不是自己的地址,此时,需要借助路由控制表,在调查应该送往的主机或路由器之后再进行转发数据。
⑦ TCP 模块的处理
在 TCP 模块中,首先会计算一下校验和,判断数据是否被破坏。然后检查是否在按照序号接收数据。最后检查端口号,确定具体的应用程序。数据被完整地接收以后,会传给由端口号识别的应用程序。
⑧ 应用程序的处理
接收端应用程序会直接接收发送端发送的数据。通过解析数据,展示相应的内容。
传输层:TCP和UDP协议
TCP协议
TCP(Transmission Control Protocol)传输控制协议,面向连接的传输协议,在传输层。
TCP协议特点
• 面向连接:通信之前必须建立连接,通信后断开连接
• 每一个TCP连接只能是点对点的(一对一)
• 提供的可靠的交付服务:通过TCP连接传输的数据,无差错,不丢失,不重复
• 提供全双工通信
• 面向字节流:TCP中的传输数据是以流的形式传输
• TCP的首部占20字节
TCP报文头部
源端口
源端口占用16bit位,表示发送方主机进程占用的端口号,通过端口可以表时主机上的某一个应用
目的端口
目的端口占用16bit位,表示的是目的主机的端口号,网络通信连接接收方需要用IP+端口,IP在网络层,端口就在传输层记录, 端口号的个数 2^16 =65536
序号
序号占用32位,对发送的数据进行编号,接收方返回的确认序号就是下一个发送包的编号
确认号
确认号32位,确认号接收端发送给发送端的确认编号,表示该编号之前的数据都成功接收,接收端接收到该消息之后就可以继续发送后续的报文
报头长度(数据偏移)
占用了4个bit位,以32位(4字节)即字长为单位,报头的长度是可变的,最大是60个字节(占用4个bit位,2^4=15, 15 *4(字节) =60字节)
保留位
保留位占6位,必须全为0
标志位
标志位占6bit,存在6个标志,每个占用一个bit,如果有效则置为1,依次URG、ACK、PSH、RST、SYN、FIN
URG:该位为1表示TCP包的紧急指针有效,督促上层应用尽快的处理紧急处理
ACK:确认号有效
PSH:push操作,该标志位为1接收方应尽快将报文交给应用层
RST:(rest)连接复位,在通信过程找那个存在主机奔溃或其他原因早成连接错误,用该标志位来拒绝连接
SYN:是一个同步序号,通常是与ACK合用来建立连接,三次握手
FIN:需要断开连接时用到该标志位
窗口大小
占用16bit位,TCP中用来进行流量控制,通过窗口可以告知发送方窗口的大小,通过动态的控制发送窗口大小来控制发送到网络中包的大小,网络通畅时发送大的数据包,网络发送拥塞时发送小的数据包
检验和
占用16bit,通过对首部和数据进行校验来判断当前包是否正确
紧急指针
只有当URG标志置为1时才有效,紧急指针是一个正的偏移,和序号字段中的是相加表示紧急指针的最后的序号,TCP的紧急方式发送一条数据给接收端
三次握手
三次握手过程:
第一次握手:
客户端发送请求报文大服务端,并进入SYN_SENT状态,等待服务的确认(SYN1,seq=x)
第二次握手:
服务端接收到连接请求,如果同意建立连接,向客户端发回确认报文段,服务端进入到SYN_RECV状态,
(SYN=1,ACK=1,ack=x+1,seq=y)
第三次握手:
客户端接收到服务端的确认报文后,向服务端给出确认报文段,连接完成,客户端和服务端都进入了ESTAB-LISHED状态,即可以双向通信(ACK=1,ack=y+1,seq=x+1)
四次挥手
四次挥手过程:
第一次挥手:
客户端向发送端发送断开连接报文段,客户端进入FIN-WAIT1状态(FIN1,seq=u)
第二次挥手:
服务端接收到客户端的请求报文后,确认客户端的消息,由服务端回复客户端一个ACK报文,服务端进入到
CLOSE-WAIt状态(ACK=1,seq=v, ack=u+1),客户端接收到第二次挥手消息后计入到FIN-WAIT2状态,这事客户端不能给服务端发送消息,但服务端可以继续给客户端发送消息(单通道通信)
第三次挥手:
服务端向客户端发起断开连接报文,服务端进入到LASK-ACK状态(FIN=1,ACK=1,seq=w, ack=u+1)
第四次挥手:
客户端接收到服务端发送的请求报文后,向服务端发送一个确认消息,客户端进入到TIME-WAIT状态。在等待2MSL时间后才进入到CLose状态,服务端接收到客户端的消息后进入到CLOSE状态(ACK=1,seq=u+1,ack =w+1)
Q:
1、三次握手可以改成2次握手嘛?为什么
不可以,TCP的三次握手是为了防止已经过期的连接再次连接上
2、TCP可以设置成3次挥手?为什么?
不可以,假如是3次挥手
存在三次挥手的情况:客户端和服务端同时发起挥手请求
3、为什么第四次挥手之后需等待2MSL时间呢?
1、为了保证发送的最后一个ACK到达服务端
2、防止已经失效的连接请求报文段出现在本次连接中
TCP如何发送数据的
TCP的层的包发送和接收存在两个缓冲区,一个是发送缓冲区,一个是接收缓冲区
发送缓冲区:
TCP传输层数据包有大小限制,如果包过大的话,拆分成多个TCP包,发送的数据包过小,等待多个应用层消息打包成一个TCP的数据包进行发送 TCP报文头至少是20个字节
接收缓冲区:
数据是没有明确的边界,接收数据是没办法指定一个或者多个消息一起读,只能选择一次读取多大的数据流,而这个数据流中包含着某个消息包的一部分数据
名词:半包,粘包,拆包
滑动窗口协议
用于网络数据传输时的流量控制,以免发生拥塞
移动过程:
拥塞控制
防止过多的数据包注入到网络中。通过拥塞控制使网络中的路由器或者链路不可过载
慢启动,拥塞控制
快重传,快回复
快重传机制
TCP如何保证数据可靠性
1、校验和:报文头的检验和用来保证当前传输包的完整性
2、序列号:报文头中的序列号是用来对TCP包进行编号,接收端通过编号可以对数据进行去重和排序
3、确认应答机制(ACK)
4、超时重传/快重传
5、拥塞控制
UDP协议
UDP
(User Datagram Protocol)用户数据包协议,是TCP/IP协议中无连接的传输层协议
UDP协议特点
无连接
尽最大努力交付
面向报文
无拥塞控制
支持一对一,一对多,多对一的交互通信
首部开销小
UDP的报文头
UDP报文头信息:
源端口,目的端口:都占用是16bit,分别表示源端口和目的端口
长度:UDP数据包的长度,包含头部和数据,最小值为8字节(仅包含头部)
校验和:用校验UDP包的完整性,有错包就丢失
应用场景
适用实时性要求比较高的场景,比如视频,语言、直播…
Socket通信
Socket即套接字,两个主机之间的逻辑连接的断点
Socket编程是一种C/S模式的编程,即客户端和服务端
TCP编程
服务端:
public class TcpServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket=null;
Socket socket=null;
BufferedReader reader=null;
try {
//创建ServerSocket实例
serverSocket=new ServerSocket();
//绑定端口
serverSocket.bind(new InetSocketAddress(8888));
System.out.println("服务端启动");
//等待客户端连接,返回的是socket实例,即客户端和服务端连接的实例
socket=serverSocket.accept();
System.out.println("有客户端连接上:"+socket.getRemoteSocketAddress());
reader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
String s = reader.readLine();
System.out.println("客户端发送的数据:"+s);
} catch (IOException e) {
e.printStackTrace();
}
finally {
//关闭资源
if(reader!=null) reader.close();
if(socket!=null) socket.close();
if(serverSocket!=null)serverSocket.close();
System.out.println("服务器关闭");
}
}
}
客户端:
public class TcpClient {
public static void main(String[] args) throws IOException {
//创建Socket实例
Socket socket = new Socket();
OutputStream outputStream=null;
try {
//连接Connection
socket.connect(new InetSocketAddress("127.0.0.1",8888));
//发送数据给服务端
outputStream=socket.getOutputStream();
outputStream.write("hello,xujiahuan".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
finally {
socket.close();
if(outputStream!=null) outputStream.close();
}
}
}
注意:
1、客户端连接的端口和服务端绑定的端口必须一致才能连接成功
否则会抛出以下异常
2、C/S模型中一定先启动服务端(服务端需要绑定端口处于Listen状态),客户端才能启动连接
否则抛出以下异常
UDP编程
服务端:
public class UDPServer {
public static void main(String[] args) {
//服务端给客户端发送消息
DatagramSocket datagramsocket=null;
DatagramPacket packet=null;
try {
//创建发送对象
datagramsocket=new DatagramSocket();
//发送内容封装
String str="hello ,xiaoxu";
packet=new DatagramPacket(str.getBytes(),str.length(),
new InetSocketAddress("127.0.0.1",8888));
//发送包
datagramsocket.send(packet);
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
finally {
datagramsocket.close();
}
}
}
客户端:
public class UPDClient {
public static void main(String[] args) {
DatagramSocket datagramSocket=null;
try {
//创建DatagramSocket实例
datagramSocket=new DatagramSocket(9999);
//接收数据
byte[] bytes = new byte[1024];
DatagramPacket datagramPacket=new DatagramPacket(bytes,1024);
//接收操作
datagramSocket.receive(datagramPacket);
//取出数据
String s=new String(datagramPacket.getData(),0,datagramPacket.getLength());
System.out.println("数据内容:"+s);
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
finally {
if(datagramSocket!=null) datagramSocket.close();
}
}
}
通过UDP编程:发送的数据不关注接收端是否在线,接收端在线就可以接受到数据,接收端不在线数据也可以正常发送
C/S模式 B/S模式
C/S模式(Client/Server )客户端和服务端
C/S模式中服务端中主要进行业务交互,数据存储,考虑并发,高可用等,客户端主要是页面展示,将服务端返回去的内容展示在客户端,将客户端提交的信息做一处理
B/S(Browser/Server) 浏览器和服务端
B/S模式中服务端用来进行业务交互,数据存储等,浏览器主要用来展示和基础的交互
C/S和B/S的区别:
B/S模型浏览器具有通用性,对于不同的系统中安装浏览器都可以进行交互,不用考虑系统间的差异问题
B/S模型中浏览器侧重内容展示,页面渲染,不处理业务逻辑,业务逻辑都放在服务端做处理,交互上比较多,需要网络带宽大
B/S模型对网络要求比较高,网络条件差的情况下用户体验较差
B/S模型中更新内容,只需要对服务端进行升级,客户端请求就能直接看到最新的结果
C/S模型中客户端也能实现一些业务逻辑,能够减轻服务端的业务交互提高服务端的响应速度和提高并发量
C/S模型中客户端是需要适配系统,不同的系统要考虑适配,开发客户端成本是比较高
C/S模型中客户端考虑页面展示,服务端提供数据交给客户端,客户端和服务端仅仅交互数据,网络带宽消耗小
即使网络不佳,C/S的模型响应是比较快
C/S模式中需要更新内容,就需要对客户端和服务端同时进行升级
课后作业
UDP编程中数据可能丢失,如何保证数据不丢失?
TCP和UDP的区别?
TCP:面向连接、可靠的、流式服务
UDP:无连接,不可靠的、数据包服务
如何保证UDP的数据不丢失?
借鉴TCP的ACK机制:UDP发送出的数据包,接收方接收到之后回复一个确认包,发送端若未接收到确认包可以再次返送
客户端需要IP+端口的形式,而服务端直接绑定端口没有IP?
三次握手(客户端connection)客户端主动连接服务端(连接的服务端的IP和服务端的端口),服务端等待连接(端口)
网络层:IP协议
IP协议属于ICP/IP协议族中的网络层,网络层主要的作用“实现终端点对点的通信”
IP报头格式
计算机中,为了识别通信的对端,必须要一个类似地址的识别码进行识别。在数据链路中是MAC地址标识不同的计算机,在网络层,使用的IP地址
IP首部至少20个字节
• 版本
占用4bit,有4(IPV4)和6(IPV6)两个值
• 首部长度
占用4位,最大值为15,值为1表示的是一个32位的长度,也就是4字节,首部占用最大字节是:4*15=60字节,固定占用字节是20字节,首部的长度必须是4字节的整数倍,如果不足,在尾部填充部分进行填充
• 区分服务
占8位,一般情况下不使用
• 总长度
占用16位,包括首部长度和数据部分长度
• 标识
占用16位,在数据报长度过长发生分片的情况下,相同的数据报的不同分片具有相同的标识符
• 标志
占用3位,第一位保留位,第二位表示该IP报文是否分片(1表禁止分片 0表示分片)第三位表示该IP报文是非最后一位
• 片偏移
和标识一起,用于发生分片的情况下,片偏移的单位是8字节
• 生存时间
TTL:存在是为了方式无法交付的数据报一致存在互联网中,以路由器跳数为单位,当TTL为0时就丢弃数据包
• 协议
指出携带的数据应该上交给哪一个协议处理,例如ICMP、TCP、UDP等
• 首部校验和
首部校验和是用来检测数据包在网络传输中是否完整
• 源地址、目的地址
各占用32位,分别用来存放发送方IP和接收端IP
IP地址表示形式
IP地址(IPv4地址)由32位正整数表示,IP地址在计算内部是以二进制的形式处理,将32位的IP地址以每8位为一组,分成4组,每组以“.”隔开,再将每一组转化为十进制表示
点分十进制:
数据分4组,每一组的范围在[0,255],如:数据表示为255.255.255.255
二进制表示:每一组8位,如:11111111 11111111 11111111 11111111
IP地址划分
IP地址是由网络标识和主机标识组成
网络标识在数据链路中每个端配置的不同的值,网络标识必须保证相互连接的每个段的地址不能重复,而相同端内相连的主机必须是相同的网络标识
主机标识则不允许在同一个网段内(同一个网络标识段内)不允许出现重复
IP地址的分级
IP地址分为4个级别:分别是A类、B类、C类、D类,根据IP地址从高第一个到第4位的比特位对其网络标识和主机标识进行划分
• A类地址:
首位以“0”开头的地址, 高8位为网络标识,用0.0.0.0~127.0.0.0 ,低24位为主机标识,一个网段中能够容纳的主机地址上线2^24-2
• B类地址:
高两位是“10”开头的地址,高16位为网络标识,128.0.0.0~191.255.0.0,低16位置为主机标识,一个网段能够容纳的主机标识是2^16=65536-2
• C类地址:
高3位是“110”开头的地址,高24位作为网络标识,192.0.0.0~223.255.255.0,低8位是主机标识,一个网段中主机标识上线为254个
• D类地址:
高4位为“1110”开头的地址,32位全部作为网络标识,224.0.0.0~239.255.255.255
子网掩码、IP路由、DNS域名系统
子网掩码
不在受限地址的类型,“子网掩码"通过子网网络地址细分出比A类、B类、C类、D类等更小粒度的网络
子网掩码用二进制表示的话是一个32位的数字,对应的IP地址网络标识部分的位全部为"1”,对应的主机标识部分全部为“0”,IP地址就受限自己的类别,子网掩码可以自由的定位网络表示的长度,子网掩码必须是IP地址的首位开始连续的“1”
子网掩码的表示形式
有两种表示形式:
第一种:将IP地址与子网掩码的地址分别用两行表示
以127.10.5.9的前26位是网络地址的话,如下:
IP地址:127.10.5.9 <-01111111 00001010 00000101 00001001
子网掩码:255.255.255. 192 <-11111111 11111111 11111111 11000000
网络地址:127.10.5.0 <-01111111 00001010 00000101 00000000
第二种:在每个IP地址后面追加网络地址的位数用“/”隔开,如下
IP地址:127.10.5.9/26
网络地址:127.10.5.0/26
在第二种方式下记录网络地址时可以省略后面的0
例如:IP地址127.10.0.0/26 跟 127.10/26表示意思是一样的
子网掩码、IP路由、DNS域名系统
路由控制
IP地址中的网络地址部分用户进行路由控制,路由控制表记录着网络地址与下一步听该发送的路由器的地址
在发送IP报时,首先确定IP头部中的目标地址,在从路由控制表中找到与该地址具有相同网络地址的记录,根据该记录将IP地址转发到响应的下一个路由器,如果路由器表中存在相同或网络地址的记录,就选择一个最吻合的网络地址。
如果在某一个路由表中没有匹配到网络地址,就会从默认的路由地址查找。
路由表的控制有两种方式:
静态路由控制:是管理员手动设置
动态路由控制:路由器与其他的路由器相互交互信息时自动刷新
IPv6
IPv6(IP version 6)解决IPv4地址耗尽的问题
IPv4地址长度是23位,4个8位字节
IPv6地址长度是128位,一般携程8个16位字节
IPv6包首部长度固定的值(40个字节)
IPv6标记: 一般将128比特位地址以每16比特位一组,每组用冒号(“:”)隔开表示
如果出现连续的0可以省略,用两个冒号(“::”)隔开,但是一个IP地址中只允许出现一次连续的冒号
DNS:域名系统
DNS:将域名解析为IP地址
一个域名有多个层次组成,包含顶级域名,二级域名,三级域名以及四级域名
域名服务器分类:
域名服务器主要分为四类
根域名服务器:解析顶级域名
顶级域名服务器:解析二级域名
权限域名服务器:解析区内的域名
本地域名服务器:默认域名服务器,可以在其中配置缓存
域名解析过程
域名通过域名服务器解析使用递归和迭代两种方式:
迭代方式:本地域名服务器向一个域名服务器发送域名解析请求之后,结果返回到本地域名服务器,然后本地域名服务器继续向其他的域名服务器请求解析
递归方式:请求的结果不是直接返回去,而是继续向前请求解析,最后的结果才会返回
经典面试题
从输入网址到获取页面的网络请求的过程?
• 域名解析
1、先查找浏览器中缓存是否存在对应IP,浏览器会缓存DNS一段时间
2、如果没找到,在从Hosts文件查找是否有域名和对应IP的信息
3、如果没找,再从路由器中查找
4、如果没找到,再从DNS缓存查找
5、如果还是没找到,浏览器域名服务器会想根域名服务器查找域名对应的IP
• 网络传输通信
1、拿到IP之后进行通信,先和对方建立连接(三次握手…),连接完成后建立了可靠的通道,就可以通过http请求数据打包发送给服务端,
2、服务端接收请求报文,处理报文信息,处理完成后将数据打包发送给客户端
3、通信完成后,通过4次挥手,断开TCP的连接
4、客户端拿到服务端返回的数据
• 页面渲染
页面的解析渲染(js、CSS、Html)
Java的网络编程
IO相关的操作:
操作字节的:InputStream和OutputStream
操作字符:Reader和Writer
操作文件:File
操作网络:Socket
网络IO编程是:BIO、NIO、AIO
IO模型术语
IO模型之前需要明白同步/异步、阻塞/非阻塞
同步(Synchronization)/异步(Asynchronization)
同步和异步是基于应用程序与操作系统处理IO所采用的方式
同步:是应用程序直接参与IO读写的操作
异步:所有的IO读写操作都交给操作系统处理,应用程序只需要等待通知
阻塞(Block)/非阻塞(NonBlock)
阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式
阻塞:需要等待缓冲区的数据准备好后才能处理其他的事情,否则一直等待在这里
非阻塞:当进程访问数据缓冲区时,如果数据没有准备好则直接返回,不会等待。如果数据已经准备好了,也直接返回。
IO模型可以分为:
同步非阻塞/同步阻塞/异步阻塞/异步非阻塞
同步阻塞IO:
在这种IO模型下,用户进程发起一个IO操作以后,必须等待IO操作完成,只有当真正完成IO操作之后,用户程序才能继续运行。Java中的传统的IO模型就是同步阻塞模型,BIO
同步非阻塞IO:
在这种IO模型下,用户进行发起一个IO操作后可立即返回做其他事情,但是用户进行需要时不时询问IO操作是否完成。这个要求用户进行不断的进行询问,从而引起不必要的CPU资源的浪费,NIO
异步阻塞IO:
在这种IO模型下,应用发起一个IO操作,不等待内核IO操作完成,等内核完成IO操作之后会通知应用程序。
异步非阻塞IO:
在这种IO模型下,用户进程发起一个IO操作然后立即返回,等IO操作真正完成之后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理,不需要进行实际的IO读写操作。AIO
常见的IO模型
对于IO操作,主要包含两个阶段:
- 等待数据准备就绪
- 将数据从内核拷贝到进程中
例如:读函数:分为等待系统可读和真正读
写函数:分为等待网卡可以写和真正写
• 阻塞IO
• 非阻塞IO
• I/O复用
• 信号驱动IO
• 异步IO
示例说明:
活动:演唱会
角色1:举办方,售票业务员
角色2:黄牛
角色3:小明
角色4:送票快递员
阻塞IO
举例:小明从家里先到演唱会现场购票问售票员买票,但是票还没有出来,还需要两天才出来,小明就直接在售票厅打地铺,一直等到票出来,然后买票
非阻塞IO
通过进程反复调用IO函数,采用轮训方式,占用CPU
举例:小明从家里先到演唱会现场购票问售票员买票,但是票还没有出来,然后小明走了,办理其他事情,然后过两个小时,又去售票现场买票,如果票还没有出来,小明又去办其他的事情,重复上面的操作,直到买到票结束
IO复用
主要使用IO复用器:select\poll\epoll,将关注的时间都注册到复用器上,由OS来关注事件是否就绪
一个IO请求,需要两次调用,两次返回。
能够实现对多个IO端口进行监听,多个连接共用一个等待机制
举例:小明想买票看演唱会,直接给黄牛(select等IO复用器)打电话,帮我留意买个票,票买了通知我,我自己去取,那么票还没出来之前,小明完全可以做自己的事情
信号驱动IO
举例:小明想买票看演唱会,给举办方业务员说,给你一个电话,有票了给我打个电话通知一下(需要系统支持,Linux系统支持,window没有这种机制),我自己再来买票(小明完全可以干自己的时事情,但是票还是需要小明自己去取)
异步IO
举例:小明想买票看演唱会,给举办方的售票员打电话说,给你留个地址,有票了请你通知快递员,由快递员将票送到给定的地址,当小明听到敲门声,看见快递员了,就知道票已经好了并且已经送到他手上
Java中IO模型
BIO(Blocking IO)同步阻塞模型 JDK1.4之前使用该模型
NIO 同步非阻塞模型,JDK1.5开始提供
AIO 异步非阻塞模型
BIO:同步阻塞模型
在JDK1.4之前,建立网络通信采用的BIO模型
需要现在服务器启动一个ServerSocket,然后客户端启动一个Socket来对服务端进行连接,默认的情况下服务端需要对每一个请求建立一个线程等待处理,客户端发送请求后,需要等待服务端是否有相应的线程来处理客户端的连接,如果没有线程则会一致等待或者拒绝请求,如果有的话,客户端会等待线程来处理客户端请求
回顾Socket通信流程
基于BIO的编程
通过网络通信模拟echo命令的实现
服务端编程:
ServerSocket ssocket = null;
try {
//创建ServerSocket实例
ssocket = new ServerSocket();
//绑定端口
ssocket.bind(new InetSocketAddress(9999));
System.out.println("服务端已经启动...");
//进行监听,等待客户端的连接
Socket socket = ssocket.accept();
System.out.println("客户端:"+socket.getRemoteSocketAddress()+"连接上");
//进行读写操作
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg = reader.readLine();
System.out.println("客户端:"+socket.getRemoteSocketAddress()+"发送消息:"+msg);
//回复客户端消息
OutputStream write = socket.getOutputStream();
write.write(("echo:"+msg+"\n").getBytes());
//关闭资源
write.close();
reader.close();
socket.close();
System.out.println("服务端结束");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (ssocket != null) {
try {
ssocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端编程:
//启动socket实例
Socket socket = new Socket();
try {
//连接服务端
socket.connect(new InetSocketAddress("127.0.0.1",9999));
System.out.println("客户端连接服务端成功");
//进行读写操作
OutputStream outputStream = socket.getOutputStream();
outputStream.write("hello\n".getBytes());
outputStream.flush();
//读数据
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg = reader.readLine();
System.out.println(msg);
//关闭资源
reader.close();
outputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
BIO的编程流程
服务端:
1、创建ServerSocket实例
2、绑定端口
3、通过accept来监听客户端的连接,有客户端连接会返回socket实例
4、进行读写操作
5、关闭资源
客户端:
1、创建socket实例
2、通过connect并指定服务端的IP+端口连接服务端
3、进行读写操作
4、关闭资源
小练习:实现一个用户多次消息发送接收
服务端:
客户端:
BIO是同步阻塞模型,BIO编程中阻塞体现在哪里?
accept方法:阻塞接收客户端的连接
read方法/write方法
connect方法:和服务端建立连接,连接的过程中connect会阻塞
服务端可以处理很多的客户端的连接?
怎么设计考虑呢??
在accept能进行循环接收客户端连接,借助于多线程处理
主线程负责接收客户端的连接:accept放在主线程执行
子线程负责和客户端交互,每一个客户端连接都分配一个子线程
多线程+BIO完成多用户请求处理
服务端编程
public class MutilThreadBIOServer {
public static void main(String[] args) {
try {
//创建serverSocket实例
ServerSocket serverSocket = new ServerSocket();
//绑定端口
serverSocket.bind(new InetSocketAddress(9999));
System.out.println("服务端启动了");
while (true) {
//等待多个客户端的连接
Socket socket = serverSocket.accept();
System.out.println("客户端:"+socket.getRemoteSocketAddress()+" 上线了");
//将Socket实例交给子线程处理
new ServerHandler(socket).start();
}
//关闭资源
// serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class ServerHandler extends Thread{
private Socket socket;
/**
* 构造函数,将socket实例传递给子线程
* @param socket
*/
public ServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//读取客户端的消息
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//回复消息
OutputStream outputStream = socket.getOutputStream();
String msg = null;
while ((msg = reader.readLine())!= null) {
System.out.println("客户端:"+socket.getRemoteSocketAddress()+" 消息:"+msg);
//给客户端回复消息
outputStream.write((msg+"\n").getBytes());
//循环结束条件
if ("".equals(msg) || "exit".equals(msg)) break;
}
//关闭资源
reader.close();
outputStream.close();
System.out.println("客户端:"+socket.getRemoteSocketAddress()+" 关闭");
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
NIO:同步非阻塞IO
NIO中提供了选择器(Selector 类似底层操作系统提供的IO复用器:select、poll、epoll),也叫做多路复用器,作用是检查一个或者多个NIO Channel(通道)的状态是否是可读、可写。。。可以实现单线程管理多个channel,也可以管理多个网络请求
**Channel:**通道,用于IO操作的连接,在Java.nio.channels包下定义的,对原有IO的一种补充,不能直接访问数据需要和缓冲区Buffer进行交互
通道主要实现类:
**SocketChannel:**通过TCP读写网络中的数据,一般客户端的实现
**ServerSocketChannel:**监听新进来的TCP连接,对每一个连接都需要创建一个SocketChannel。一般是服务端的实现
**Buffer:**缓冲区
IO流中的数据需要经过缓冲区交给Channel
NIO的编程
服务端:
public class NIOServer {
public static void main(String[] args) {
ServerSocketChannel serverSocketChannel = null;
try {
//创建ServerSocketChannel通道实例
serverSocketChannel = ServerSocketChannel.open();
//绑定端口
serverSocketChannel.bind(new InetSocketAddress(9998));
System.out.println("服务端启动了");
//将serverSocketChannel设置为非阻塞 configureBlocking设置阻塞非阻塞 false:非阻塞 true:阻塞
serverSocketChannel.configureBlocking(false);
//创建selector选择器
Selector selector = Selector.open();
//将通道serverSocketChannel注册到选择器selector,关注可接受事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//等待监听结果,调用选择器的select阻塞等待,直到有事件发生才返回
while (selector.select() > 0) {
Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
//是否是可接受事件
if (selectionKey.isAcceptable()) {
System.out.println("可接受事件");
//有新用户连接
ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();
//接受客户端的连接,通过accept(不在阻塞)接受一个SocketChannel通道
SocketChannel socketChannel = serverSocketChannel1.accept();
//设置socketChannel为非阻塞
socketChannel.configureBlocking(false);
//将socketChannel注册到选择器selector选择器,关注可读事件
socketChannel.register(selector, SelectionKey.OP_READ);
}
//是否是可读事件
if (selectionKey.isReadable()) {
System.out.println("可读事件");
//获取SocketChannel通道
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//创建Buffer
ByteBuffer buffer = ByteBuffer.allocate(100);
//进行读取操作
socketChannel.read(buffer);
//进行读写模式的切换
buffer.flip();
//将数据从Buffer中读取
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
//打印结果
System.out.println("客户端:"+socketChannel.getRemoteAddress()+new String(bytes,0,bytes.length));
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭资源
try {
if (serverSocketChannel != null) {
serverSocketChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
服务端的编程步骤:
1、实例化通道:ServerSocketChannel
2、绑定端口:通过ServerSocketChannel实例调用bindI()方法绑定端口
3、将ServerSocketChannel设置为非阻塞
4、实例化选择器(IO复用器)Selector
5、将ServerSocketChannel注册给选择器,并且关注accept事件
6、监听事件是否完成,selector.select,如果事件未完成则一直阻塞直到事件完成
7、获取已完成事件的集合并遍历,判断是否是accept事件,是,则调用accept方法,获取SocketChannel通道
8、设置SocketChannel为非阻塞,并将SocketChannel注册到选择器Selector,并关注read事件
9、监听事件是否完成,若有事件完成,则判断是否是read读事件
10、通过SocketChannel通道读取数据(Buffer中),读完数据循环事件监听,即步骤6
11、关闭资源:ServerSocketChannel,SocketChannel,Selector
客户端:
public class NIOClient {
public static void main(String[] args) {
SocketChannel socketChannel = null;
try {
//创建SocketChannel通道
socketChannel = SocketChannel.open();
//设置socketChannel为非阻塞
socketChannel.configureBlocking(false);
//创建Selector选择器
Selector selector = Selector.open();
//主动的进行连接,connect操作不在会阻塞,会直接返回,如果连接成功返回true ,连接还未完成返回false
if (!socketChannel.connect(new InetSocketAddress("127.0.0.1",9998))) {
//当前连接操作未完成
//将SocketChannel注册到选择器,并关注可连接事件
socketChannel.register(selector, SelectionKey.OP_CONNECT);
//等待连接完成
selector.select();
Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
//是否是可连接事件
if (selectionKey.isConnectable()) {
//可连接事件完成
SocketChannel channel = (SocketChannel) selectionKey.channel();
//连接操作完成
channel.finishConnect();
}
}
}
//连接成功,给服务端发送消息
ByteBuffer buffer = ByteBuffer.allocate(100);
//将发送的数据写到Buffer中
buffer.put("hello tulun\n".getBytes());
//读写模式的切换
buffer.flip();
socketChannel.write(buffer);
//关闭资源
selector.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (socketChannel != null) {
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
NIO的客户端编程流程:
1、实例化通道:SocketChannel
2、设置SocketChannel为非阻塞
3、实例化复用器:Selector
4、连接服务器connect()(该方法不会阻塞直接返回结果,返回为Boolean,是否连接成功)
5、若返回为false,则将SocketChannel注册到复用器中,并监听connect可读事件
6、监听复用器事件是否完成(Selector.select),判断完成集合中是否有可连接事件,将可连接事件完成(channel.finishConnet())
7、给服务端发送消息,channel.write()操作
8、关闭资源:selector、SocketChannel
课后作业:完成客户端多次给服务端发送消息,完成Echo命令
public static void main(String[] args) {
SocketChannel socketChannel = null;
try {
//创建SocketChannel通道
socketChannel = SocketChannel.open();
//设置socketChannel为非阻塞
socketChannel.configureBlocking(false);
//创建Selector选择器
Selector selector = Selector.open();
//主动的进行连接,connect操作不在会阻塞,会直接返回,如果连接成功返回true ,连接还未完成返回false
if (!socketChannel.connect(new InetSocketAddress("127.0.0.1",9998))) {
//当前连接操作未完成
//将SocketChannel注册到选择器,并关注可连接事件
socketChannel.register(selector, SelectionKey.OP_CONNECT);
//等待连接完成
selector.select();
Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
//是否是可连接事件
if (selectionKey.isConnectable()) {
//可连接事件完成
SocketChannel channel = (SocketChannel) selectionKey.channel();
//连接操作完成
channel.finishConnect();
}
}
}
//注册读事件
socketChannel.register(selector, SelectionKey.OP_READ);
Scanner scanner = new Scanner(System.in);
//连接成功,给服务端发送消息
ByteBuffer buffer = ByteBuffer.allocate(100);
while (scanner.hasNext()) {
String msg = scanner.nextLine();
buffer.put((msg+"\n").getBytes());
//读写模式的切换
buffer.flip();
socketChannel.write(buffer);
//关注服务端的返回 read
selector.select();
Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isReadable()) {
SocketChannel channel = (SocketChannel) selectionKey.channel();
//将数据读取到Buffer中
buffer.clear();
channel.read(buffer);
//读写模式切换
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String s = new String(bytes);
System.out.println(s);
}
}
if ("".equals(msg) || "exit".equals(msg)) break;
//Buffer是重复使用,需要进行清空
buffer.clear();
}
//关闭资源
selector.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (socketChannel != null) {
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
服务端改动代码:
为什么写没有注册到复用器中?
写操作在NIO中也是一个事件,注意:写事件是需要主动发起写操作,一般写完之后立即write操作不会进行阻塞,即通常写操作并不需要注册。
NIO+ 多线程形式
NIO中一个selector可以关注多个用户的连接(即一个线程可以同时处理多个用户的通信),为了并发用户量能够处理更多,可以使用NIO+多线程的形式来处理。
NIO+多线程的处理思路:主线程主要来接收客户端的连接(accept),子线程处理用户的IO操作
主线程接收到客户端连接socketchannel通道,将SocketChannel交给子线程。selector选择器需要给子线程嘛?
如果子线程和主线程共用一个选择器:主线程注册的是可接受事件。子线程注册可读事件,即主线程处理可接受事件,子线程处理可读事件,选择器有返回结果,假如当前子线程获取到就绪事件(可读事件、可连接事件),
主线程和子线程分别使用各自的选择器。
服务端是固定数量为2的线程池,当进行IO操作是,子线程还在一致执行,意味着没有空闲的线程,引起结果就是两个用户请求可以处理,新用户请求没法继续处理了。
public static void main(String[] args) {
ServerSocketChannel serverSocketChannel = null;
try {
//创建ServerSocketChannel通道实例
serverSocketChannel = ServerSocketChannel.open();
//绑定端口
serverSocketChannel.bind(new InetSocketAddress(9998));
System.out.println("服务端启动了");
//将serverSocketChannel设置为非阻塞 configureBlocking设置阻塞非阻塞 false:非阻塞 true:阻塞
serverSocketChannel.configureBlocking(false);
//创建selector选择器
Selector selector = Selector.open();
//将通道serverSocketChannel注册到选择器selector,关注可接受事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//子线程以线程池的形式提供
ExecutorService executorService = Executors.newFixedThreadPool(10);
//等待监听结果,调用选择器的select阻塞等待,直到有事件发生才返回
while (selector.select() > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
//是否是可接受事件
if (selectionKey.isAcceptable()) {
System.out.println("可接受事件");
//有新用户连接
ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();
//获取新用户channel
SocketChannel socketChannel = serverSocketChannel1.accept();
System.out.println(Thread.currentThread().getName()+":客户端:"+socketChannel.getRemoteAddress()+" 连接上。。。");
//将通道交给子线程
executorService.submit(new NIOServerHandler(socketChannel));
}
}
}
} catch (Exception e) {
}
}
public class NIOServerHandler implements Runnable {
//通过主线程将socketChannel获取到
private SocketChannel socketChannel;
//创建selector实例
private Selector selector = null ;
public NIOServerHandler(SocketChannel socketChannel) {
this.socketChannel = socketChannel;
try {
//在一个子线程中只需要创建一个selector实例
if (selector == null)
selector = Selector.open();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
//将socketChannel设置为非阻塞
socketChannel.configureBlocking(false);
//将socketChannel注册到选择器中,并且关注可读事件
socketChannel.register(selector, SelectionKey.OP_READ);
int num;
while (selector.select() > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
//是否是可读事件
if (selectionKey.isReadable()) {
//获取SocketChannel通道
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//创建Buffer
ByteBuffer buffer = ByteBuffer.allocate(100);
//进行读取操作
socketChannel.read(buffer);
//进行读写模式的切换
buffer.flip();
//将数据从Buffer中读取
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String msg = new String(bytes, 0, bytes.length);
//给客户端回复消息
buffer.clear();
buffer.put(("echo:"+msg).getBytes());
//读写模式切换
buffer.flip();
//回复消息
socketChannel.write(buffer);
//打印结果
System.out.println(Thread.currentThread().getName()+"客户端:"+socketChannel.getRemoteAddress()+" 消息:"+msg);
if ("".equals(msg)|| "exit".equals(msg)){
System.out.println(Thread.currentThread().getName()+"客户端:"+socketChannel.getRemoteAddress()+" 下线");
//当前注册的感兴趣事件取消
selectionKey.cancel();
//关闭通道
socketChannel.close();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
channel 通道
channel和用户操作IO相连,但通道的使用是不能直接访问数据的需要和缓冲区Buffer相连
读数据:将数据从channel中读取到Buffer,从Buffer在获取到数据
写数据:将数据线写入Buffer,Buffer中的数据写入到通道
读数据的代码实例:
Channnel 的主要实现类:
FileChannel:用于读取、写入和操作文件的通道 IO ->File
DatagramChannel:通过UDP读取网络中的数据
SocketChannel:通过TCP读写网络中的数据,一般是客户端的实例实现
ServerSocketChannel:监听接收新进来的TCP的连接,对于每一个新的连接在服务端都会创建SocketChannel,一般在服务端的实现
Channel和流Stream的区别:
channel不仅可以读也可以写,stream通常要么读、要么写
channel可以同步也可以异步操作
channel总是读取或者写入一个Buffer
基于SocketChannel和ServerSocketChannel实现C/S的大致流程
客户端:
1、通过SocketChannel连接服务端
2、创建读数据/写数据的缓冲区对象来读取服务端的数据或者是想服务端发送数据
3、关闭SocketChannel
服务端:
1、通过ServersocketChannel的实例绑定端口
2、通过ServerSocketChannel的accept方法接口一个客户端的SocketChannel的连接
3、创建读数据/写数据的缓冲区对象来实现从客户端读数据后者向客户端发送数据
4、关闭SocketChannel和ServerSocketChannel
Buffer 缓冲区
缓冲区的主要作用就是进行channel中数据操作是必须使用到缓冲区
Buffer是一个抽象类,其实现的子类有ByteBuffer(字节缓冲区)、charBuffer(字符缓冲区)…
Buffer提供的方法:
缓冲区有两种:堆上开辟的空间,堆外开辟的空间
Buffer的创建:
ByteBuffer为例:
ByteBuffer allocate(int capacity):在堆上创建指定大小的缓冲
ByteBuffer allocateDirect(int capacity):在堆外空间创建指定大小的缓冲
ByteBuffer wrap(byte[] array):通过byte数组实例创建一个缓冲区
ByteBuffer wrap(byte[] array, int offset, int length) 指定byte数据中的内容写入到一个新的缓冲区
Buffer实现的分析:
Buffer的实现底层是通过特定类型(byte\char…)数组来存储数据
数组中数据的操作需要借助4个指针来操作:
// Invariants: mark <= position <= limit <= capacity private int mark
= -1; //标记 private int position = 0; //位置 private int limit; //限制 private int capacity; 容量
selector选择器
selector选择器也叫做IO复用器,作用是用来检查一个或者多个NIO Channel的状态是否是可读、可写。。。,可以实现单线程管理多个channels
优势:使用更少的线程管理更多的通道了,相比较多线程,减少上下文切换过程
selector的使用
1、创建selector的实例
通过调用Selector.open()创建一个Selector的对象
Selector selector = Selector.open();
2、注册channel到选择器selector
注册的channel是非阻塞的
//设置socketChannel为非阻塞
socketChannel.configureBlocking(false);
//将socketChannel注册到选择器selector选择器,关注可读事件
socketChannel.register(selector, SelectionKey.OP_READ);
SelectableChannel抽象类有提供configureBlocking()方法用于设置当前通道是处于则塞模式还是非阻塞模式
3、使用selector选择器来监听事件是否完成
selector.select()方法会阻塞,直至内核监听到注册的感兴趣事件发生才会返回
4、遍历感兴趣事件集合
通过调用selector.selectedKeys()方法返回的是SelectionKey的Set集合,通过集合迭代器对事件进行遍历
当前的SelectionKey例存放的是注册的已准备就绪的时间
Iterator iterator = selector.selectedKeys().iterator();
5、如事件还在关注,则跳转到第3不继续监听
6、最终关闭,选择器
SelectionKey介绍
一个SelectionKey表示的一个特定的通道和一个特定的选择器的对象的直接注册关系
在SelectionKey有4个关注的之间在SelectionKey中通过常量来维护的
public static final int OP_READ = 1 << 0; 可读事件 0001
public static final int OP_WRITE = 1 << 2; 可写事件
public static final int OP_CONNECT = 1 << 3; 可连接事件
public static final int OP_ACCEPT = 1 << 4; 可接受事件
register()方法的第二个参数就是一个“Inters集合”,通过regiter来告诉选择器在对应的通道上帮监听的时间类型
,事件存在4中,Accept\Connect\Read\Write
如果对多个事件感兴趣也可以一次将多个事件注册:SelectionKey.OP_READ| SelectionKey.OP_WRITE
提供方法来判断Seletor是否对Channel的某个事件感兴趣
boolean isReadable():是否是可读事件 true:可读事件 false:没有可读事件
boolean isWritable():是否可写事件
boolean isConnectable():是否可连接事件
boolean isAcceptable() :是否可接受事件
int interestOps():当前通道的感兴趣事件集合
int readyOps():返回一个int值,表示下channel上进行读操作
SelectableChannel channel():返回SelectionKey对应的通道
Selector selector():返回SelectionKey对应的选择器
void cancel():取消当前的通道channel
attach(Object ob):传递带外数据
Selector选择器选择过程
selector提供的课选择过程相关的方法
select方法
int select() 会阻塞直至至少一个通道的上注册的事件就绪才会返回
int select(long timeout) 和select()一样,但最长阻塞时间是timeout毫秒时间
int selectNow():非阻塞,只要就通道就绪就立即返回
select()方法返回的int值表示有多少个通道就绪,是自上一次调用select()方法后有多少个通道变成就绪,之前在select()调用是进入到就绪的通道不会在本次调用中被计入
selector中维护的三种SelectionKey集合
• 已注册的键的集合
所有与选择器关联的通道所生成键的集合称之为已注册键的集合,而集合中键时包括有效的和无效的键,已注册的键的集合是可以通过keys()方法返回
• 已选择的键的集合
已注册键的子集,这个集合中的每个成员都是关联的通道被选择器判断为已经准备好的。这个集合通过selectedKeys()返回
• 已取消键的集合
已注册键的集合,这个集合中包含了cancel()方法调用过的键,这下键并没有立即注销,而是无法直接访问了
注意:
当键被取消时,可以通过isValid()方法来判断
使用时安全用法:
int 返回的结果不是是通道的个数???
选择过程:
Java中的seletor的复用器其完成对事件的监听是靠底层操作系统提供的IO复用的系统调用方法,其只是对底层的一个封装
停止选择的方法
wakeup()方法:通过Selector对象的wakeup方法让处于阻塞状态的select立即返回,该方法是得选择器上的第一个还没有返回的选择操作立即返回去,如果当前没有进行中的选择操作,那么对下一次的select()方法的调用会立即返回。
close()方法:通过close操作是关闭seletor,该方法使得任何一个选择操作都会被唤醒,同时使得注册到选择器的所有的channel被注销,所有的键被取消,但是channel本身不会被关闭
对Seletor的源码进行剖析
BIO和NIO的区别
使用BIO的时候往往是需要引入多线程,每个连接一个单独的线程
NIO则使用单线程或者是少量的多线程,多个连接共用一个线程,NIO当一个连接创建后不需要对应一个线程的,这个连接会被注册到IO复用器上面,所有的连接只需要一个线程就可以,一个请求一个线程模型
NIO比BIO好,一个线程可以处理多个socket,NIO+多线程提高网络服务性能,降低线程的数量,服务器下线程数量过多对系统有那些影响?
1、Java中创建线程,最终会映射到本地操作系统上的进程红藕这是线程,Linux为例,fork是重量级的操作,系统开销大
2、多线程需要CPU调度,会有上下文的切换,线程过多时,上下文切换的时间会趋近于或大于线程本身指定执行的时间,CPU就会存在浪费,降低了系统的性能
3、线程创建需要开辟线程私有的内空间,线程过多的话,为线程运行准备的内存就回去占去很大一部分,真正用来分配还给业务的内存就大大减少,系统运行不可靠
4、线程过多的业务中,阻塞等待网络时间发生,如果一瞬间客户端的请求量比较大,系统会瞬间唤醒很多数量的线程,造成系统内存和COUD额使用率居高不小,造成系统负载过高
AIO:异步非阻塞模型
AIO需要操作系统的支持,在linux内核2.6版本中加入了对真正异步IO的支持,java从jdk1.7开始支持AIO
核心类有AsynchronousSocketChannel 、AsynchronousServerSocketChannel、AsynchronousChannelGroup
AsynchronousChannelGroup是异步Channel的分组管理器,它可以实现资源共享。
创建AsynchronousChannelGroup时,需要传入一个ExecutorService,也就是绑定一个线程池,
该线程池负责两个任务:处理IO事件和触发CompletionHandler回调接口。代码如下:
AsynchronousServerSocketChannel:AIO中网络通信服务端Socket
accept方法
AsynchronousServerSocketChannel创建成功后,类似于ServerSocket,也是调用accept()方法来接受来自客户端的连接,
由于异步IO实际的IO操作是交给操作系统来做的,用户进程只负责通知操作系统进行IO和接受操作系统IO完成的通知。
所以异步的ServerChannel调用accept()方法后,当前线程不会阻塞,
程序也不知道accept()方法什么时候能够接收到客户端请求并且操作系统完成网络IO,
为解决这个问题,AIO为accept方法提供两个版本:
Future accept() :
开始接收客户端请求,如果当前线程需要进行网络IO(即获得AsynchronousSocketChannel),
则应该调用该方法返回的Future对象的get()方法,但是get方法会阻塞该线程,所以这种方式是阻塞式的异步IO。
void accept(A attachment ,CompletionHandler<AsynchronousSocketChannel,? super A> handler):
开始接受来自客户端请求,连接成功或失败都会触发CompletionHandler对象的相应方法。
其中AsynchronousSocketChannel就代表该CompletionHandler处理器在处理连接成功时的result是AsynchronousSocketChannel的实例。
而CompletionHandler接口中定义了两个方法,
completed(V result , A attachment):当IO完成时触发该方法,该方法的第一个参数代表IO操作返回的对象,
第二个参数代表发起IO操作时传入的附加参数。
faild(Throwable exc, A attachment):当IO失败时触发该方法,第一个参数代表IO操作失败引发的异常或错误。
** AIO编程**
服务端:
public class AioServer {
private static int DEFAULT_PORT = 12345;
private static ServerHandler serverHandle;
public volatile static long clientCount = 0;
public static void start(){
start(DEFAULT_PORT);
}
public static synchronized void start(int port){
if(serverHandle!=null)
return;
serverHandle = new ServerHandler(port);
new Thread(serverHandle,"Server").start();
}
public static void main(String[] args) {
AioServer.start();
}
}
public class ServerHandler implements Runnable{
private AsynchronousServerSocketChannel channel;
public ServerHandler(int port) {
try {
//创建服务端通道
channel = AsynchronousServerSocketChannel.open();
//绑定端口
channel.bind(new InetSocketAddress(port));
System.out.println("服务端已启动,端口号:"+port);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
channel.accept(this, new AcceptHandler());
// Future <AsynchronousSocketChannel> accept = channel.accept();
//该步操作是异步操作 防止当前线程直接执行结束
//方案1: while(true)+sleep
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// //方案2 CountDownLatch 作用:在完成一组正在执行的操作之前,允许当前的现场一直阻塞 此处,让现场在此阻塞,防止服务端执行完成后退出
//
// CountDownLatch count = new CountDownLatch(1);
// channel.accept(this, new AcceptHandler());
// try {
// count.await();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
// CompletionHandler<V,A>
// V-IO操作的结果,这里是成功建立的连接,AsynchronousSocketChannel
// A-IO操作附件,这里传入AsynchronousServerSocketChannel便于继续接收请求建立新连接
class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, ServerHandler> {
@Override
public void completed(AsynchronousSocketChannel channel, ServerHandler serverHandler) {
//创建新的Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
//异步读 第三个参数为接收消息回调的业务Handler
// channel.read(buffer, buffer, new ReadHandler(channel));
//继续接受其他客户端请求
serverHandler.channel.accept(null, this);
}
@Override
public void failed(Throwable exc, ServerHandler serverHandler) {
exc.printStackTrace();
}
}
class ReadHandler implements CompletionHandler<ByteBuffer, ByteBuffer> {
//用户读取或者发送消息的channel
private AsynchronousSocketChannel channel;
public ReadHandler(AsynchronousSocketChannel channel) {
this.channel = channel;
}
@Override
public void completed(ByteBuffer result, ByteBuffer attachment) {
result.flip();
byte[] msg = new byte[result.remaining()];
result.get(msg);
try {
String expression = new String(msg, "UTF-8");
System.out.println("服务器收到消息: " + expression);
// String result1 = "服务端收到消息\n";
result.clear();
//向客户端发送消息
doWrite(expression);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
//发送消息
private void doWrite(String msg) {
byte[] bytes = msg.getBytes();
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(bytes);
buffer.flip();
//异步写数据
channel.write(buffer, buffer, new CompletionHandler <Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
//如果没有发送完,继续发送
if (attachment.hasRemaining()) {
channel.write(attachment, attachment, this);
} else {
//创建新的Buffer
ByteBuffer allocate = ByteBuffer.allocate(1024);
//异步读 第三个参数为接收消息回调的业务Handler
// channel.read(allocate, attachment, new ReadHandler(channel));
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客户端
public class AioClient {
private static String DEFAULT_HOST = "127.0.0.1";
private static int DEFAULT_PORT = 12345;
private static ClientHandler clientHandle;
public static void start(){
start(DEFAULT_HOST,DEFAULT_PORT);
}
public static synchronized void start(String ip,int port){
if(clientHandle!=null)
return;
clientHandle = new ClientHandler(ip,port);
new Thread(clientHandle,"Client").start();
}
//向服务器发送消息
public static boolean sendMsg(String msg) throws Exception{
if(msg.equals("exit")) return false;
clientHandle.sendMsg(msg);
return true;
}
public static void main(String[] args) throws Exception{
AioClient.start();
System.out.println("请输入请求消息:");
Scanner scanner = new Scanner(System.in);
while(AioClient.sendMsg(scanner.nextLine()));
}
}
public class ClientHandler implements Runnable{
private AsynchronousSocketChannel clientChannel;
private String host;
private int port;
private CountDownLatch latch;
public ClientHandler(String host, int port) {
this.host = host;
this.port = port;
try {
//创建异步的客户端通道
clientChannel = AsynchronousSocketChannel.open();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
//创建CountDownLatch等待
// latch = new CountDownLatch(1);
//发起异步连接操作,回调参数就是这个类本身,如果连接成功会回调completed方法
clientChannel.connect(new InetSocketAddress(host, port), this, new AcceptHandler());
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// try {
// latch.await();
// } catch (InterruptedException e1) {
// e1.printStackTrace();
// }
// try {
// clientChannel.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
}
//向服务器发送消息
public void sendMsg(String msg){
byte[] req = msg.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
System.out.println(">>>>>>msg:"+msg);
writeBuffer.put(req);
writeBuffer.flip();
//异步写
clientChannel.write(writeBuffer, writeBuffer,new WriteHandler(clientChannel));
}
/**
* 接收类
*/
class AcceptHandler implements CompletionHandler<Void, ClientHandler> {
public AcceptHandler() {}
@Override
public void completed(Void result, ClientHandler attachment) {
System.out.println("连接服务器成功");
}
@Override
public void failed(Throwable exc, ClientHandler attachment) {
exc.printStackTrace();
try {
attachment.clientChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class WriteHandler implements CompletionHandler<Integer, ByteBuffer> {
private AsynchronousSocketChannel channel;
public WriteHandler(AsynchronousSocketChannel channel) {
this.channel = channel;
}
@Override
public void completed(Integer result, ByteBuffer attachment) {
//完成全部数据的写入
if (attachment.hasRemaining()) {
//数据没有写完,继续写
System.out.println("WriteHandler.hasRemaining>>>>>");
clientChannel.write(attachment, attachment, this);
} else {
//读取数据
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
clientChannel.read(readBuffer, readBuffer, new ReadHandler(clientChannel));
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ReadHandler implements CompletionHandler<Integer, ByteBuffer> {
private AsynchronousSocketChannel clientChannel;
public ReadHandler(AsynchronousSocketChannel clientChannel) {
this.clientChannel = clientChannel;
}
@Override
public void completed(Integer result,ByteBuffer buffer) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String body;
try {
body = new String(bytes,"UTF-8");
System.out.println("客户端收到结果:"+ body);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc,ByteBuffer attachment) {
System.err.println("数据读取失败...");
try {
clientChannel.close();
} catch (IOException e) {
}
}
}
}
netty网络通信框架
基于NIO做开发的,编程更加复杂,业务和系统逻辑解耦合的
通信框架:Netty
Netty比较成熟的一个开源的网络通信框架
Netty官网:http://netty.io
Netty接口文档:http://netty.io/4.1/api
• 并发高
• 传输快
Netty的优势
并发量高
Netty是基于NIO的网络IO模型,同步非阻塞的IO模型
NIO的底层网络模型:
netty底层使用的就是IO复用的模型(select、poll、epoll)
传统的IO模型,BIO模型,一个用户连接需要一个线程来做处理
非阻塞的IO方式:NIO模型,将关注的事件注册到复用器,由复用器来关注事件的就绪状态,复用器可同时关注多个事件的就绪情况,就可以实现一个线程来处理多个用户的连接
传输快
Netty的传输快是依赖NIO中的一个特性–零拷贝技术
传统的数据拷贝形式:
以发送数据为例:对当前应用程序而言,需要先读取数据:先从本地磁盘将数据拷贝到内核缓冲区,再行内核缓冲区拷贝到用户空间,往网络中发送,将数据从用户空间拷贝到内核socket缓冲区,在将数据从内核交给网卡,在整个数据发送过程中进行4次的数据拷贝过程
以发送数据为例,读取数据是直接将数据拷贝到用户空间,发送是将数据从用户空间写入网卡,在零拷贝技术下,数据发送过程就进行了2次拷贝。
相比较传统的数据拷贝,减少了两次拷贝的过程。可以大大提高传输速率
Netty中数据的发送和接收使用ByteBuf采用堆外内存
Netty的特点
基于NIO的事件驱动框架,提供了对TCP和UDP和文件传输的支持
使用了更加高效的socket的底层,对epoll空轮训引起的CPU占用率飙升的内部进行处理,避免了NIO的问题
采用多种decoder、encoder的支持,对TCP粘包、分包做自动话的处理
可以接受/处理线程池,提高连接效率,对重连,心跳检测的支持
可配置化IO线程、TCP的参数、TCP接受发送缓冲区的参数
…
Netty 组件
Bootstrap:启动辅助类
Netty的启动辅助类,netty客户端和服务端的入口,Bootstrap是创建客户端连接的启动类,ServerBootstrap是监听服务端端口的启动器
EventLoop:事件循环
netty中最核心的组件之一,是reactor模型,划分为boss reactor 和woker reactor,boss用来监听客户端连接, worker是用来处理客户端连接
通过EventLoopGroup生成,常用NIOEventLoopGroup,
EventLoop内部是一个无限循环,维护一个seletor,处理所有注册到seletor的IO操作,就是实现一个线程维护多个连接的工作
Channel:通道
直接和IO相连接,常用的NioSocketChannel和NioServerSocketChannel, NioSocketChannel主要负责连接服务端,NioServerSocketChannel主要作用是绑定端口,接口客户端的连接并返回一个NioSocketChannel
ChannelPipeline:ChannelHandler的容器
Netty的IO处理的通道,与ChannelHandler组成责任链,
ChannelHandler:IO处理的单元
ChannelHandlerContext:用于传输业务数据的
责任链模式:
ByteBuf:缓冲区
Netty编程
实现一个echo命令
首先引入Netty框架依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.10.Final</version>
</dependency>
服务端编程
public class NettyServer {
public static void main(String[] args) {
/**
* Netty中提供了需求的不同的EventLoopGroup的实现用来处理不同的传输
* 在这里使用2个NioEventLoopGroup
* 一个叫做boss ,用来接收客户端的连接
* 一个叫做worker,用来处理客户端的连接
* boss一旦接收到连接,就会将连接信息交给worker来处理
*/
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup(5);
try {
/**
* 使用启动辅助类
* 服务端的启动辅助类ServerBootStrap
*/
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap
//指定事件循环组
.group(boss,worker)
//指定boss事件循环组的channel类型
.channel(NioServerSocketChannel.class)
//指定worker事件循环组中的Channel信息,
.childHandler(new ChannelInitializer<NioSocketChannel>() {
//添加ChannelHandler到ChannelPipeLine容器中
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
//获取ChannelPipeline的容器
ChannelPipeline pipeline = ch.pipeline();
//处理字符串数据,将字符串的编码解码作为ChannelHandler放入容器中
pipeline.addLast(new StringEncoder());
pipeline.addLast(new StringDecoder());
//添加自定义的实现
pipeline.addLast(new ServerHandler());
}
});
//同步阻塞启动服务端
ChannelFuture sync = bootstrap.bind(8888).sync();
System.out.println("服务端启动了");
//同步阻塞等待服务端关闭
sync.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 接收数据自定义ChannelHander时,必须实现ChannelInboundHandler接口
* ChannelInboundHandler有一个实现类SimpleChannelInboundHandler
*
*/
public class ServerHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
}
//接收客户端的数据接口
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(ctx.channel().remoteAddress()+":"+msg);
String recMsg="ECHO:"+msg;
ctx.channel().writeAndFlush(recMsg);
}
//客户端连接上时
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().remoteAddress()+"上线了");
}
//客户地下线时
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().remoteAddress()+"下线了");
}
}
客户端
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
//创建事件循环组
NioEventLoopGroup loopGroup = new NioEventLoopGroup();
//实例化启动辅助类
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(loopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringEncoder());
pipeline.addLast(new StringDecoder());
pipeline.addLast(new ClientHandler());
}
});
//同步阻塞启动后,获取当前的channel实例
Channel channel = bootstrap.connect("127.0.0.1", 8888).sync().channel();
System.out.println("客户端启动了");
//给服务端发送消息
channel.writeAndFlush("hello");
//关闭客户端通道
channel.closeFuture().sync();
}
}
public class ClientHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(msg);
}
}
NioEventLoopGroup组的参数是传递线程数量,默认无参构造函数时,线程数量默认是多少呢?
在使用线程池时?指定线程的数量????
基于当前的业务和硬件配置作为依据来设置合理的线程数量
业务:当前使用线程的业务代码:逻辑密集型和IO密集型 逻辑密集型(业务大部分涉及到是计算(CPU):例如) IO密集型(涉及到大量的读文件,写文件,操作数据库) 2N(CPU的核数) N+1(CPU的核数)
当无参构造NIOEventLoopGroup时,底层创建了2N(CPU核数)个线程作为线程池来处理
Netty的启动会过程
1、使用启动辅助类:Bootstrap需要配置一些信息在如何处理?
2、主事件(boss)循环组是如何将用户连接的NIOSocketChannel交给子事件(worker)循环组
bind操作调用了AbstractBootstrap的bind实现
继续在AbstractBootstap中调用,先做基本参数校验,然后调用dobind
在initAndRegister中先实例化Channel实例,
Channel的实例过程:
启动辅助类将通过channel将类实例类型传递,在启动辅助类中根据当前的类类型反射获取到实例,在boss线程中是实例了一个NioServerBootstrap
在启动中
第一步:实例化一个对象
第二步:通过channel实例进行初始化
第三步:进行注册:将channel实例注册到事件循环组中的对应线程的selector上
拓展:
NIOEventLoopGroup:线程池+NIO