前言:
最近公司安排我做一个设备日志加密的需求,story大致是:一台具有syslog上报日志功能的网络设备,在每执行一条指令之后会上报日志到我们的日志服务器,而我们的工作,是负责在服务器端收集日志,并且设备与服务器要实现加密日志传输,其实就是通过SSL加密。期间调试花费了一个多月的时间,耗时这么长时间的主要原因:之间对于IO这块内容基本空白;服务器是netty写的,部门里的人都没有用过;完善加密过程也就是ssl握手过程异常艰难。
这里将写几篇博文深入学习有关知识,具体几篇不知道,内容比较多,主要包含的知识点有:IO模式,netty,SSL加密原理及过程。
一、IO通信模式
IO,Input/Output。这块知识往往是很多JAVA开发人员不够重视或者说没有花时间去了解的郊区吧,至少我和我们部门大部分人是这样的,平时倾向于业务开发,web开发,数据库开发,觉得IO这种东西只要知道怎么去读写文件就行了。误区在于,这只是IO中的磁盘IO,而我们这里主要讲的是网络IO。实际上,我们所做的绝大部分项目开发,都离不开IO通信。把服务器想象成一位老师王老师,客户端是一名学生小明。那么一个最简单的通信过程如下:
小明:王老师,JVM的垃圾回收算法主要有哪些呀?
王老师:嗯嗯,这个问题的答案是:标记清除、标记整理、标记复制...
这个过程看起来很简单吧,而实际上,到软件开发这个角度来看的话应该是这样的:
小明:发起TCP连接...此处省略TCP和http握手过程,通过get或post方法将请求路径置于http消息体内发送至服务器
王老师:收到请求,首先根据请求路径准备好数据,处理好之后,首先发送一个http状态码(200),接着将数据以约定好的格式发送至客户端。
这就是大致的一个服务器处理一个request请求的过程。看起来跟IO好像没啥关系呀,而实际上服务器将数据写入http消息体内发送至客户这个过程就是IO操作,只是我们使用的服务器如tomcat以及将这层处理好了。事实上,目前几乎所有的服务器技术瓶颈都是IO,只要涉及到数据流动,就不可避免的会涉及到IO。而不幸的是,IO操作往往是耗时操作。
接下来介绍目前的常见的IO模式。
(1):阻塞IO(blocking IO)
阻塞IO很容易理解,当客户端起了一个线程,发起一个请求,此时开始等待读取数据,服务器收到请求之后,也新起一个线程,等待数据准备处理好。当数据准备处理好之后,服务器线程结束等待,开始写入数据至http消息体内,写操作完成之后,线程关闭;数据发送至客户端之后,客户端线程结束等待,开始读取数据,数据读取完成之后,线程停止。
由上面这个过程可以看到,客户端和服务器端各有一个线程在很长一段时间内都是处于等待状态的(因为数据的IO操作很耗时),尤其在高并发场景下,服务器同时受到了成千上万个请求,那么等待的时间将是很可怕的。顺便一提,在上面这个过程中数据在由内存缓冲区写入用户缓冲区这部分阻塞没提,这点将在之后AIO介绍。就用上面的例子介绍:
小明:王老师,JVM的垃圾回收算法主要有哪些呀?
小红:王老师,动态代理的原理是啥呀?
小周:王老师,内部类的作用是什么呀?
......
......
小鹏:今年英雄联盟冠军是谁呀?
王老师当思考这么多问题的时候,小明等待答案的时间自然就会变长,而王老师同时要处理这么多请求,其大脑就算能一个分成两个用,也没办法流利的回答这么多问题,只能无奈地回复一句:RNG牛逼。
接下来写一个最简单的BIO模型demo:
服务器:
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(4444);
while (true){
//accept客户端发来的Socket请求,产生对应的Socket
Socket socket = serverSocket.accept();
//普通磁盘IO操作
PrintStream ps = new PrintStream(socket.getOutputStream());
ps.println("message from server!!");
//关闭输出流和Socket
ps.close();
socket.close();
}
}
}
客户端:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1",4444);
//接收Socket中的输入流
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = br.readLine();
System.out.println("来自服务器的消息: "+line);
br.close();
socket.close();
}
}
上面的例子就是最简单的socket编程,当使用serversocket和socket建立连接之后,接下来的通信过程与普通磁盘IO过程没有什么区别。而现在我们知道BIO是阻塞IO,具体是哪里阻塞,我们首先要理清思路:
在哪里阻塞?当服务器收到一个请求之后,新起一个线程处理这个请求的读写操作,上面代码中accpet之后创建的socket就是处理这个操作的,它会实时去监听客户端是否有数据发送过来,直到连接断开,socket关闭,而在此期间,该线程不会处理其他任何事情,也不会相应其他连接。下面还是类似的BIO模式服务器:
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
static int connectionCount = 1;
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocket serverSocket = new ServerSocket(4444);
while (true){
//accept客户端发来的Socket请求,产生对应的Socket
Socket socket = serverSocket.accept();
System.out.println("有一个新连接"+connectionCount++);
PrintStream printStream = new PrintStream(socket.getOutputStream());
printStream.println("connected success!");
InputStream is = socket.getInputStream();
byte[] buf = new byte[64];
int hasRead = 0;
while ((hasRead = is.read(buf))>0){
System.out.println((new String(buf,0,hasRead)).trim());
}
}
}
}
这里我们下载了一个telnet工具,用于模拟客户端的连接请求,由于不清楚这个三方工具的输出流编码格式,因此上面的服务器用的是字节流输入。
服务器收到的数据:
此时我们再新建一个连接:
在第一个连接没有关闭的情况下,连接未能成功,没有收到服务器发回的连接成功的相应数据,服务器也收不到第二个客户端发送的数据。此时将第一个连接关闭:
嗯嗯,差不多了吧。这就是服务器端的阻塞,当然现在的服务器不会只有一个线程的,但是线程池的最大线程数也是有限的,线程数量并不是越多越好的,而关键的问题是,这个线程在很大一部分时间是在等待客户端的消息,客户端不发消息,它还是傻傻地等着,这种方式必然不是一种好的处理IO请求的方式。可以想象一下,我们点开一个web页面,内容比较丰富的页面上面都是各种图片呀,文字什么的,而服务器往浏览器(客户端)传输这些资源的时候必然不是只用一个线程来处理的,这就是为什么有时候网络不好,我们打开一个页面,会先看到文字,而有些图片还没加载出来。一个请求也许就要用到多个线程,那么若是碰到明天(也许是今天哈,这篇还不知道什么时候写完,现在是11月10号晚上23点)双11的时候,那淘宝网页如果用这种阻塞IO模式的话,可能就卖不出几件东西了。
接下来,不着急,BIO这一部分还没完,我们继续,上面的服务器用的是一个线程处理请求,那么下面在服务器端使用多线程处理请求,至今仍然有很多服务器使用的是这种IO模式,又被称为伪异步IO。
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class FakeBIOServer {
//Socket容器,并且加锁包装为线程安全的
public static List<Socket> socketList = Collections.synchronizedList(new ArrayList<>());
static int connectionCount = 1;
//Socket编程
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocket serverSocket = new ServerSocket(4444);
while (true){
Socket socket = serverSocket.accept();
socketList.add(socket);
new Thread(new ServerThead(socket)).start();
}
}
static class ServerThead implements Runnable{
Socket socket = null;
InputStream is = null;
PrintStream ps = null;
public ServerThead(Socket socket) throws IOException {
this.socket = socket;
is = socket.getInputStream();
ps = new PrintStream(socket.getOutputStream());
}
@Override
public void run() {
String content = null;
//打印收到的客户端数据
while ((content = readFromClient())!=null){
System.out.println(content);
}
}
public String readFromClient() {
//处理字节流读取客户端发来的数据
int hasRead = 0;
byte[] buf = new byte[1024];
try {
while ((hasRead = is.read(buf))>0){
return new String(buf,0,hasRead);
}
}catch (IOException e){
System.out.println("ioException");
FakeBIOServer.socketList.remove(socket);
}
return null;
}
}
}
上面的代码简而言之,就是每收到一个客户端发来的请求,就新建一个线程处理这个请求,并且通过一个加锁的List容器保障线程安全。现在再试着使用telnet工具发起两个连接:
两个连接都建立成功了并且服务器也收到了客户端发送的消息:
这种方式看起来像是实现了异步IO通信,而实际上,每个线程还是同步阻塞的,若是请求数量增加,线程数量也要增加,那么服务器的荷载将不能承受。
好啦,BIO就到这里了,下一章节将介绍异步IO,纯手打原创,最后说一句:RNG牛逼