之前我们写的服务器Socket示例demo,都是只能接收单次请求,一般服务器肯定是需要持续提供服务的,我们可以对demo进行一些改造。
服务器Socket持续同步接收请求
为了让服务器Socket能够持续接收请求,我们可以利用while循环。
例如:
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerSocketTest {
public static void main(String[] args) throws Exception {
// TODO 服务端处理客户端连接请求
ServerSocket serverSocket = new ServerSocket(3333);
//while循环持续监听请求
while (true) {
try {
// 阻塞方法获取新的连接
Socket socket = serverSocket.accept();
try {
int len;
byte[] data = new byte[1024];
InputStream inputStream = socket.getInputStream();
// 按字节流方式读取数据
while ((len = inputStream.read(data)) != -1) {
System.out.println(new String(data, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
这样虽然可以让ServerSocket持续监听客户端请求,但是如果有一个请求耗时太久,就会阻塞后面其他的连接,效率太低。
这就是BIO通信模型,采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接。我们一般通过在while(true) 循环中服务端会调用accept()方法等待接收客户端的连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成, 不过可以通过多线程来支持多个客户端的连接。
服务器Socket利用多线程异步接收请求
如果要让 BIO 通信模型 能够同时处理多个客户端请求,就必须使用多线程(主要原因是socket.accept()、socket.read()、socket.write() 涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的一请求一应答通信模型 。

例如:
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerSocketTest {
public static void main(String[] args) throws Exception {
// TODO 服务端处理客户端连接请求
ServerSocket serverSocket = new ServerSocket(3333);
// while循环持续监听请求
while (true) {
try {
// 阻塞方法获取新的连接
Socket socket = serverSocket.accept();
// 每一个新的连接都创建一个线程,负责读取数据
new Thread(() -> {
try {
int len;
byte[] data = new byte[1024];
InputStream inputStream = socket.getInputStream();
// 按字节流方式读取数据
while ((len = inputStream.read(data)) != -1) {
System.out.println(new String(data, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
利用多线程虽然提高了服务端socket的效率,但是我们知道在Java虚拟机中,线程是宝贵的资源,线程的创建和销毁成本很高,除此之外,线程的切换成本也是很高的。尤其在Linux 这样的操作系统中,线程本质上就是一个进程,创建和销毁线程都是重量级的系统函数。如果并发访问量增加会导致线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。
服务器Socket利用线程池异步接收请求
采用线程池和任务队列可以实现一种叫做伪异步的 I/O 通信框架,它的模型图如下图所示。当有新的客户端接入时,将客户端的 Socket 封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK 的线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。

伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。不过因为它的底层仍然是同步阻塞的BIO模型,因此无法从根本上解决问题。
例如:
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class ServerSocketTest {
public static void main(String[] args) throws Exception {
// TODO 服务端处理客户端连接请求
ServerSocket serverSocket = new ServerSocket(3333);
// 核心线程数量
int corePoolSize = 5;
// 最大线程数量
int maximumPoolSize = 8;
// 线程活跃时间
long keepAliveTime = 10000;
// keepAliveTime 参数的时间单位
TimeUnit unit = TimeUnit.MILLISECONDS;
// 工作队列
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);
// 线程创建工厂
ThreadFactory threadFactory = new TreadFactoryWithName();
// 拒绝服务策略:AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit,
workQueue, threadFactory, handler);
// 预启动所有核心线程
poolExecutor.prestartAllCoreThreads();
// while循环持续监听请求
int i = 0;
while (true) {
try {
// 阻塞方法获取新的连接
Socket socket = serverSocket.accept();
// 每一个新的连接都创建一个任务task,负责读取数据
MyTask task = new MyTask(String.valueOf(i++), socket);
poolExecutor.execute(task);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/*
* 线程创建工厂类
*/
public static class TreadFactoryWithName implements ThreadFactory {
//原子方式更新的int值(CAS无锁并发)
private final AtomicInteger mThreadNum = new AtomicInteger();
@Override
public Thread newThread(Runnable r) {
// 推荐创建带有名字的线程
Thread t = new Thread(r, "MyThread -- " + mThreadNum.getAndIncrement());
System.out.println(t.getName() + " has been created");
return t;
}
}
/*
* 自定义Runnable类
*/
static class MyTask implements Runnable {
private String name;
private Socket socket;
public MyTask(String name, Socket socket) {
this.name = name;
this.socket = socket;
}
@Override
public void run() {
try {
int len;
byte[] data = new byte[1024];
InputStream inputStream = socket.getInputStream();
// 按字节流方式读取数据
while ((len = inputStream.read(data)) != -1) {
System.out.println(this.toString() + "" + new String(data, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
}
}
public String getName() {
return name;
}
@Override
public String toString() {
return "MyTask [name=" + name + "]";
}
}
}
本文介绍了如何改进服务器Socket以持续接收请求。从同步接收请求导致的阻塞问题,到通过多线程异步处理提高效率,再到使用线程池和任务队列实现伪异步I/O,降低了资源消耗。线程池允许设置消息队列大小和最大线程数,避免资源耗尽,但并未解决BIO模型的根本问题。

559

被折叠的 条评论
为什么被折叠?



