- 最基本的就是基于Socket的通信,统称为BIO,意味着同步阻塞通信。有消息也好,没消息也好,都会等待接收。通信的A、B两方,一直连接着,当发出请求后,持续等待另一方返回响应,不返回就一直等待,性能较差。当一方请求下线,另一方也是,体现了同步的特点,socket是两方紧紧耦合在一起的
- 进阶的通信技术:NIO通信模式。同步非阻塞通信。当发出请求后,不需要持续等待另一方返回响应,可以过一会来查看是否有返回信息,不会一直等待阻塞。
- 进阶的通信技术:NIO2.0(AIO通信)模式。异步非阻塞通信。有数据的时候通知,不需要请求方去访问,等待,性能较好
BIO通信模式下的案例介绍
案例1:一个客户端发消息,一个服务端收消息(这个案例只能发一次消息)
public class ServerDemo {
public static void main(String[] args) throws IOException {
System.out.println("服务器启动");
//服务器注册一个端口
ServerSocket serverSocket = new ServerSocket(8888);
//暂停接收客户端请求,得到一个端到端的socket管道
Socket accept = serverSocket.accept();
//从socket管道得到字节输入流
InputStream inputStream = accept.getInputStream();
//将字节输入流包装成自己想要的流读取
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
//读取数据
String len;
while ((len = bufferedReader.readLine()) != null) {
System.out.println("服务端收到消息:" + len);
}
}
}
public class ClientDemo {
public static void main(String[] args) throws IOException {
System.out.println("客户端启动");
Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
OutputStream outputStream = socket.getOutputStream();
PrintStream printStream = new PrintStream(outputStream);
printStream.println("客户端:我们开始交流吧");
printStream.flush();
}
}
上述的服务端代码是有问题的,当客户端发送一次消息就结束了,socket是耦合的,客户端断开,服务器也断开了,那么当客户端再次发送的时候,服务端没有启动,会抛出Connection refused (Connection refused)异常,只支持一次发送。
案例2:客户端可以反复的发送消息,服务端可以反复的接收消息
public class ServerDemo {
public static void main(String[] args) throws IOException {
System.out.println("服务器启动===");
//注册端口
ServerSocket serverSocket = new ServerSocket(8888);
//这里暂停等待客户端的连接,得到一个端到端的socket管道
Socket socket = serverSocket.accept();
//从Socket中得到一个字节输入流
InputStream inputStream = socket.getInputStream();
//将流封装成需要的流对象,进去读取操作
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
//读取操作
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println("服务器收到:" + line);
}
}
}
public class ClientDemo {
public static void main(String[] args) throws IOException {
System.out.println("客户端启动");
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream outputStream = socket.getOutputStream();
PrintStream printStream = new PrintStream(outputStream);
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("请说");
String msg = scanner.nextLine();
printStream.println(msg);
//这里不能写printStream.close();如果将流关闭了,socket是端到端的,服务器也就关闭,无法接收消息了
printStream.flush();
}
}
}
本案例可以实现多发多收,但是只能处理一个客户端的请求,当多个客户端连接时,只接收第一个客户端的请求,服务端是单线程的,一次只能和一个客户端进行通信。
案例3:一个服务端可以接收无数个客户端的连接,并且可以接收他们反复发送消息
public class ServerReadThread extends Thread {
private Socket socket;
public ServerReadThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String len;
while ((len = bufferedReader.readLine()) != null) {
System.out.println("服务端收到:" + socket.getRemoteSocketAddress() + ":" + len);
}
} catch (Exception e) {
e.printStackTrace();
System.out.println(socket.getRemoteSocketAddress() + "下线了");
}
}
}
public class ServerDemo {
/**
* 思路:接收多个客户端,那么使用多个服务器线程来接收
*
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
System.out.println("服务器启动===");
//注册端口
ServerSocket serverSocket = new ServerSocket(8888);
while (true) {
Socket socket = serverSocket.accept();
//创建多个客户端连接线程来接收消息
new ServerReadThread(socket).start();
System.out.println(socket.getRemoteSocketAddress() + "上线了");
}
}
}
public class ClientDemo {
public static void main(String[] args) throws IOException {
System.out.println("客户端启动");
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream outputStream = socket.getOutputStream();
PrintStream printStream = new PrintStream(outputStream);
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("请说");
String msg = scanner.nextLine();
printStream.println(msg);
//这里不能写printStream.close();如果将流关闭了,socket是端到端的,服务器也就关闭,无法接收消息了
printStream.flush();
}
}
}
客户端1输出:
客户端启动
请说
1
请说
2
=========
客户端2输出:
客户端启动
客户端启动
请说
3
请说
4
=========
服务端输出:
服务器启动===
/127.0.0.1:59226上线了
服务端收到:/127.0.0.1:59226:1
服务端收到:/127.0.0.1:59226:2
/127.0.0.1:59231上线了
服务端收到:/127.0.0.1:59231:3
服务端收到:/127.0.0.1:59231:4
总结
- 每个Socket接收到,都会创建一个线程,线程的竞争、切换上下文影响性能
- 每个线程都会占用栈空间和CPU资源
- 并不是每个Socket都进行IO操作,无意义的线程处理
- 客户端的并发访问增加时,服务器将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或僵死,从而无法对外提供服务
案例4:伪异步I/O通信方案
提供一个线程池里面放固定的线程数,处理若干个客户端。将客户端的Socket封装成一个Task,交给后端线程池处理,JDK的线程池维护消息队列和N个活跃的线程,对消息队列中Socket任务进行处理。
- 优点:无论有多少个客户端进行连接,线程数量是可控的,不会引起服务器的宕机和僵死
- 缺点:虽然线程池可以固定线程数量,但是同时也只能处理这么多客户端,在满负荷状态下必须某个客户端断开连接,其他的客户端才可以接入
//线程池处理类
public class HandlerSocketThreadPool {
//线程池
private ExecutorService executorService;
//线程池,最大运行3个,队列数最多为100个
public HandlerSocketThreadPool(int maxPoolSize, int queueSize) {
this.executorService = new ThreadPoolExecutor(3, maxPoolSize,
120L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize));
}
public void execute(Runnable task) {
this.executorService.execute(task);
}
}
//读取客户端消息类
public class ReaderClientRunnable implements Runnable {
private Socket socket;
public ReaderClientRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String len;
while ((len = bufferedReader.readLine()) != null) {
System.out.println("服务器端收到了:" + len);
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("有人下线了");
}
}
}
//模拟客户端发请求
public class ClientDemo {
public static void main(String[] args) throws IOException {
System.out.println("客户端启动");
//建立一个socket连接
Socket socket = new Socket("127.0.0.1", 8888);
//从socket管道中获取一个输出流,写数据到服务端
OutputStream outputStream = socket.getOutputStream();
//将输出流包装成打印流
PrintWriter printStream = new PrintWriter(outputStream);
//接收用户的输入
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
String len;
//写入消息
while ((len = bufferedReader.readLine()) != null) {
//注意,这里得使用println(),因为服务器端是readLine()读取的
printStream.println(len);
//为了将内存中的数据全部强制写出
printStream.flush();
}
}
}
//服务器接收多个客户端的请求
public class ServerDemo {
/**
* 思路:接收多个客户端,那么使用多个服务器线程来接收
*
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
System.out.println("服务器启动===");
try {
//注册端口
ServerSocket serverSocket = new ServerSocket(8888);
//只需要一个服务端对应线程池,不需要每次都创建一个线程处理客户端的请求
HandlerSocketThreadPool threadPool = new HandlerSocketThreadPool(3, 100);
//客户端可能有多个
while (true) {
//阻塞式请求
Socket socket = serverSocket.accept();
System.out.println("有人上线了");
//每次收到一个客户端的socket请求,都需求为该客户端分配一个线程,处理其请求
threadPool.execute(new ReaderClientRunnable(socket));
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("有人下线了");
}
}
}
案例5:实现客户端与客户端通信消息(端口转发)
场景:适合固定通信个数的应用,不会引起宕机!伪异步实现的端口转发
思路:服务端要记录当前全部在线的客户端通道,一旦有一个客户端发来消息,服务端就把这个消息全部推送给全部在线的客户端通道
总结BIO思想
- 同步阻塞式通信,一个线程处理一个客户端,而且是等着消息回复。
- 伪异步I/O可以固定服务端的线程个数,不会引起高并发下宕机,但是同时只能处理若干个客户端,性能极差,只适合低并发,低负载的业务场景
NIO通信模式
同步非阻塞式通信,性能较好,在没有消息的情况下,服务端可以去看其他通道是否有消息,服务端只提供一个线程去轮询所有的客户端,有就处理,没有就继续轮询。但是编程很复杂,调试也很麻烦,优点是性能较好,可以支持并发通信
说到Java通信,就不得不说开源框架Netty。Netty具备以下优点
- API使用简单,开发门槛较低
- 功能强大,预置了多种编码功能,支持多种主流协议
- 定义能力强,可以通过ChannelHandler对通信框架进行灵活地扩展
- 与其他NIO框架相比,Netty的综合性能最优
- 成熟,稳定,Netty修复了已经发现的所有JDK NIO的Bug