一、引言
- 系统架构演变
随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用(MVC)架构已无法应对,分布式服务架构以及流动计算架构(伸缩性)势在必行,亟需一个治理系统确保架构有条不紊的演进。
- 单一架构:例如早期servlet/jsp - ORM(对象关系映射) Hibernate|MyBatis
- 垂直架构:将一个应用分层,实现协同开发,便于后期项目升级维护 - MVC Struts1/2|SpringMVC
- 分布式服务:提升服务整体性能,将一些模块独立出去(SOA service orented architecture),独立运行,随着业务发展,不可避免需要调度第三方服务(物流信息、第三支付)。RPC - Remote Process Call (即时消息) webservice、Dubbo、SpringCloud
- 流动式计算架构:要求服务提供方能够根据负载情况,动态增加或者减少。同时实现服务可视化,简化服务运维,提升系统资源的利用率。 - SOA治理框架
微服务
- 对服务进行集中管理调控。
-
高并发互联网设计原则 ——
高负载
、高存储
、高容错
也称AKF拆分原则
X轴
:服务水平扩展
服务具有可复制性 ,水平分割
,解决一些并发问题
(水平拓展,比性能的垂直提升成本低-摩尔定律)
Y轴
:服务间各司其职
弱耦合,垂直分割
,物理节点物尽其用
(泳道设计)
Z轴
:对x,y
打包,进行物理隔离。
X中有Y,Y中有X
x和y互相转换
y中有x:对y轴做垂直拆分,层与层间独立,为了保证每一层的可用性,每一层做水平分割 -
高并发的场景问题 —— 优化思路
如何让服务支持x轴的水平扩展,利用分布式的思维解决高并发;如何做y中的垂直拆分,是应用之间尽可能隔离,弱化服务件耦合度;结合两个维度来解决高并发的场景问题。(Socket + IO)
二、网络基础(TCP/IP)
-
网络编程:通过编码的方式,让不同计算机之间相互通信(数据的传递)。
-
两个核心问题:
寻址问题:ip地址+port端口号
协议问题:数据传输格式 -
OSI 七层模型(标准,了解就好)
-
OSI 五层模型(七层的转变)
-
协议
http协议:应用层协议,超文本传输协议,不同计算机间传输文本的协议(字符串),底层tcp协议
tcp/ip协议:传输层协议
tcp协议:传输安全,效率低
udp协议:传输不安全,效率高
- 了解
应用层(Http协议:WebService,tomcat协议)——普通程序员
传输层(TCP/IP UDP:基于tcp/ip封装的ftp、smtp/pop3、websocket…定制协议rpc协议)——架构程序员 也是代码最终触及到的地方
物理层(嵌入式开发)
三、传统 BIO 网络编程模型(TCP/IP)
- IO模型分类
-
BIO 同步阻塞IO
传统IO 或者 Blocking IO
特点:面向流Input | Output
【每个线程只能处理一个channel(同步的,该线程和该channel绑定)。
线程发起IO请求,不管内核是否准备好IO操作,从发起请求起,线程一直阻塞,直到操作完成。】 -
NIO 同步非阻塞IO
New IO 或者Non Blocking IO
特点:面向缓冲区Buffer(基于通道)
【每个线程可以处理多个channel(异步)。
线程发起IO请求,立即返回;内核在做好IO操作的准备之后,通过调用注册的回调函数通知线程做IO操作,线程开始阻塞,直到操作完成 。】 -
AIO(Async Non Blocking IO) 异步非阻塞
【线程发起IO请求,立即返回;内存做好IO操作的准备之后,做IO操作,直到操作完成或者失败,通过调用注册的回调函数通知线程做IO操作完成或者失败 。】
-
BIO
方向—— 输入、输出流(InputStream | OutputStream)
类型—— 字节、字符流(Reader | Writer)
功能—— 节点、过滤流(BufferedInputStream | BufferedOutputStream ) -
BIO网络编程
-
服务端:ServerSocket 【接收和响应,
请求转发
-ServerSocket
、请求响应
-Socket
】
①初始化服务器ServerSocket,绑定监听端口
②等待客户端连接serverSocket.accept();
③处理请求/响应sockect.getInputStream(); / socket.getOutputStream();
④关闭资源 -
客户端:Socket 【发送和接收,发送请求|接收响应:
Socket
】
①初始化客户端Socket,绑定服务器IP/端口
②发起请求/获取响应socket.getOutputStream(); / socket.getInputStream();
③关闭资源
- BIO代码
- Server
public class BIOBootstrapServer {
public static void main(String[] args) throws IOException {
server();
}
public static void server() throws IOException {
//创建服务转发ServerSocket
ServerSocket ss = new ServerSocket();
ss.bind(new InetSocketAddress(9999)); //server可以省略好hostname
ExecutorService threadPool= Executors.newFixedThreadPool(10);
while(true) {
//等待请求到来,转发产生Socket(系统内部资源)
final Socket socket = ss.accept(); //没有请求就等待,阻塞
//final 匿名内部类使用局部变量要final修饰
threadPool.submit(new Runnable() {
public void run() {
try {
//利用IO,读取用户请求
InputStream req = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(req);
BufferedReader br = new BufferedReader(isr);
String line = null;
StringBuilder sb = new StringBuilder();
while ((line = br.readLine()) != null) {
sb.append(line);
}
System.out.println("服务器收到:" + sb.toString());
//给出用户响应
OutputStream res = socket.getOutputStream();
PrintWriter pw = new PrintWriter(res);
pw.println(new Date().toLocaleString());
pw.flush();
socket.shutdownOutput(); //告知客户端写结束
//关闭系统资源
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
}
- Client
public class BIOBootstrapClient {
public static void main(String[] args) throws IOException {
send("这是一条来自client 的 message!");
}
public static void send(String msg) throws IOException {
//创建Socket对象
Socket s = new Socket();
s.connect(new InetSocketAddress("127.0.0.1",9999)); //所以几乎给ip和端口的,底层走的就是socket
//发送请求
OutputStream req = s.getOutputStream();
PrintWriter pw=new PrintWriter(req);
pw.println(msg);
pw.flush();
s.shutdownOutput();//告知服务端写结束
//接收响应
InputStream res = s.getInputStream();
InputStreamReader isr = new InputStreamReader(res);
BufferedReader br = new BufferedReader(isr);
String line=null;
StringBuilder sb=new StringBuilder();
while((line=br.readLine())!=null){
sb.append(line);
}
System.out.println("客户端收到:"+sb.toString());
//关闭资源
s.close();
}
}
思考下,BIO模型的缺点?
accept转发产生socket资源,一次请求转发等待请求响应的过程有阻塞。main线程作accept请求转发、子线程作socket读写IO处理。
- 机器间通信是靠传输
套接字
完成的。
套接字,是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
简单的举例说明下:Socket = Ip address + TCP/UDP + port。
每个socket代表一个系统的随机端口,用来在服务和客户端之间建立连接。
- BIO网络编程问题
通过将相应处理和转发进行分离,但是在转发的时候,服务器是通过先开启线程资源(计算),然后在计算中动用IO操作,IO存在总线占用问题,导致当前线程操作的IO资源没有就绪,导致系统计算资源的浪费。如果要解决该问题,必须先考虑让IO就绪,然后在开启线程处理或者理解为让线程去处理一些非阻塞的IO操作。
BIO编程诟病 :多线程模型解决服务端并发,但是无法判断当前处理的IO状态是否就绪,此时就会导致线程在做无畏等待,导致系统资源利用率不高。 先开线程 -> 在线程等待IO就绪->处理IO->线程资源释放
四、NIO编程模型
- Channel(FileChannel-读写文件、ServerSocketChannel-请求转发、SocketChannel-响应处理)
- ByteBuffer (字节缓冲区)
- 创建FileChannel
//可读的FileChannel
FileChannel fr=new FileInputStream("xxx路径").getChannel();
//可写的FileChannel
FileChannel fw=new FileOutputStream("xxx路径").getChannel();
文件操作
//读文件件
fr.read(缓冲区)
//写文件
fw.write(缓冲区)
- 字节缓冲区
flip:pos赋值给limit,post归0
clear:恢复变量为初始状态
- 一个基于NIO的文件拷贝【面试&笔试】
【NIO没有流的概念,用通道来处理。】
public class FileCopyDemo {
public static void main(String[] args) throws IOException {
//创建读通道 磁盘读入数据 到 ByteBuffer
FileInputStream in = new FileInputStream("C:\\Users\\Administrator\\Desktop\\123.txt");
FileChannel fcr = in.getChannel(); //fileChannelRead
//创建写通道 将 ByteBuffer 数据写入磁盘
FileOutputStream out = new FileOutputStream("C:\\Users\\Administrator\\Desktop\\321.txt");
FileChannel fcw= out.getChannel(); //fileChannelWrite
//创建ByteBuffer
ByteBuffer buffer = ByteBuffer.wrap(new byte[1024]);//.allocate()也可以
while (true){
buffer.clear();
int n = fcr.read(buffer);
if(n==-1) break;
buffer.flip();
fcw.write(buffer);
}
//关闭资源
fcr.close();
fcw.close();
}
}
- “挂羊头,卖狗肉” —— NIO的API,BIO的编程模型
public class NIOBootstrapServer_Fake {
public static void main(String[] args) throws IOException {
server();
}
public static void server() throws IOException {
//创建服务转发ServerSocket
ServerSocketChannel ss = ServerSocketChannel.open();
ss.bind(new InetSocketAddress(9999));
ExecutorService threadPool= Executors.newFixedThreadPool(10);
while(true) {
//等待请求到来,转发产生Socket(系统内部资源)
final SocketChannel socket = ss.accept();// 阻塞
threadPool.submit(new Runnable() {
public void run() {
try{
//读取客户端响应
ByteBuffer buffer=ByteBuffer.allocate(1024);
ByteArrayOutputStream baos=new ByteArrayOutputStream();//类似stringbuilder,用来暂存用
while (true){
buffer.clear();
int n = socket.read(buffer);
if(n==-1) break;
buffer.flip();
baos.write(buffer.array(),0,n);
}
System.out.println("服务器收到:"+new String(baos.toByteArray()));
//给出客户端响应
ByteBuffer respBuffer = ByteBuffer.wrap((new Date().toLocaleString()).getBytes());
socket.write(respBuffer);
socket.shutdownOutput();//告知客户端写结束
//关闭系统资源
socket.close();
}catch (Exception e){
}
}
});
}
}
}
- 通道选择器
NIO核心思想,将网络编程中所有操作(转发,读请求,写响应)封装成事件Event,之后用事件形式通知程序进行计算,事件发生则通知程序,这样处理的IO都是就绪的IO,保证计算资源的充分利用。这时候,引入通道选择器
的概念。
我们需要把通道注册到通道选择器的注册列表中,由通道选择器维护注册列表;一旦我们所关注的事件发生了,他会把关注的事件封装到事件处理列表当中;为了防止处理空或无效IO,每个事件处理结束后,要从列表中移除;移除不代表取消注册。只有两种可能才取消注册,一种是通道关闭,一种是主动注销。