单线程模型
服务端绑定一个端口,然后接收请求,每次请求就处理,后续请求进来时,等待之前的任务处理完成,如果任务处理非常快,也是不会有明显阻塞的。
单线程模型服务端代码
展示文件上传后处理逻辑,在一个while(true)中阻塞等待accept,由于是演示网络通信,这里的文件I/O缓存直接使用的是一个byte[1<<14] = 16k的容量,在项目中可以写成循环使用的方式。
private static void server(int port) throws IOException {
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(port));
System.out.println("服务端启动成功,绑定端口[" + port +"]");
//这里使用了一个单线程处理,后面讲多线程的时候,会改为多线程版本
while (true) {
System.out.println("服务端等待连接中。");
Socket socket = serverSocket.accept();
System.out.println("接收到一个客户端连接。");
InputStream inputStream = socket.getInputStream();
//16k
File file = new File("C:\\Users\\baopz\\Desktop\\test\\socket\\server\\1.gif");
if (!file.exists()) {
if (!file.createNewFile()) {
System.out.println("创建文件" + file.getAbsolutePath() + "失败。");
continue;
}
}
OutputStream outputStream = null;
//这里可以使用java 1.7的资源自动释放写法
try {
outputStream = new FileOutputStream(file);
byte[] bytes = new byte[1 << 14];
int length;
while ((length = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, length);
}
System.out.println("文件接受成功。");
} finally {
if (outputStream != null) {
outputStream.close();
}
}
}
}
客户端测试代码
可以在一个for循环中调用,看看会不会等待之前的处理
private static void client() throws IOException {
Socket socket = new Socket();
OutputStream outputStream = null;
try {
System.out.println("客户端请求连接。");
socket.connect(new InetSocketAddress("127.0.0.1", 8888));
outputStream = socket.getOutputStream();
File file = new File("C:\\Users\\baopz\\Desktop\\test\\socket\\client\\1.gif");
if (!file.exists()) {
System.out.println("需要传输的文件不存在。");
return;
}
InputStream inputStream = new FileInputStream(file);
//16k
byte[] bytes = new byte[1 << 14];
int length;
while((length=inputStream.read(bytes))!=-1){
outputStream.write(bytes,0,length);
}
outputStream.flush();
inputStream.close();
System.out.println("文件发送成功。");
}finally {
if(outputStream!=null){
outputStream.close();
}
socket.close();
System.out.println("客户端关闭连接。");
}
}
preThreading模式
在服务启动的时候,预先new出一个线程池,让每个连接都用一个线程处理。
基于线程池的服务端
需要注意一下,这里的ThreadFactoryBuilder是来自com.google.guava.guava包
/**
* 基于线程池的服务端
* @param port
* @throws IOException
*/
private static void server(int port) throws IOException {
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(port));
System.out.println("服务器绑定端口[" + port + "]成功");
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("任务处理线程.%d").build();
//这里的参数一次含义是,核心线程数量=4,最大线程数量8,空闲等待时间10,空闲等待时间单位s,
//阻塞队列,LinkedBlockingQueue无界队列,任务量大于了最大线程容量后,会放到这个等待队列当中
//线程工厂,定义线程名称,方便定位
//拒绝接受新任务策略,如果新来的任务大于了等待队列上限,那么就会调用这个拒绝策略,拒绝策略可以自己实现,也可以调用
//系统默认的4中策略,这里用的是AbortPolicy,直接抛出异常,终止线程池
//这里的线程池可以用自带的线程池工具new出来,如:ExecutorService executorService = Executors.newCachedThreadPool();
ExecutorService executorService = new ThreadPoolExecutor(4,
8,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(16),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
while (true) {
Socket socket = serverSocket.accept();
System.out.println("接收到一个客户端连接,分发任务。");
//这里文件名递增,每次一个线连接进来,就存放一个新文件
executorService.execute(new Task(socket,"C:\\Users\\baopz\\Desktop\\test\\socket\\server\\"+atomicInteger.getAndAdd(1)+".gif"));
}
}
基于线程池运行的任务代码
接受一个socket和保存路径,进行处理
/**
* 服务端的执行任务
*/
public static class Task implements Runnable {
private Socket socket;
private String filename;
public Task(Socket socket, String filename) {
this.socket = socket;
this.filename = filename;
}
@Override
public void run() {
System.out.println("线程"+Thread.currentThread().getName()+"正在处理任务。");
//16k
File file = new File(filename);
if (!file.exists()) {
try {
if (!file.createNewFile()) {
System.out.println("创建文件" + file.getAbsolutePath() + "失败。");
return;
}
} catch (IOException e) {
System.out.println("创建文件" + file.getAbsolutePath() + "失败。");
e.printStackTrace();
}
}
InputStream inputStream = null;
OutputStream outputStream = null;
try {
inputStream = socket.getInputStream();
outputStream = new FileOutputStream(file);
byte[] bytes = new byte[1 << 14];
int length;
while ((length = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, length);
}
System.out.println(Thread.currentThread().getName()+"文件接受成功。");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
try {
outputStream.close();
System.out.println(Thread.currentThread().getName()+"关闭连接。");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
测试客户端可以直接使用上面的客户端
Reactor模型服务端(一个Reactor,多个业务处理线程)
服务端分发程序,Reactor,注意,
1.这里在selectionKey.isReadable()里面,做了一个selectionKey.cancel();这个操作,主要是防止在起一个subChannel没有完成时,这个selectionKey依然有效,会重复提交任务的。
2.示例程序展示的是小文件的发送,很不稳定,如果文件大了,你会发现,服务端会接受不到完整的数据。后续在Netty里面会讲怎么处理这个问题,手工处理起来还是比较繁琐的。
private static void server(int port) throws IOException {
ExecutorService executorService = Executors.newCachedThreadPool();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
Selector selector = Selector.open();
//设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(port));
System.out.println("绑定端口成功.");
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
//这里就是在底层做的epoll操作,等待数据就绪
int completed = selector.select();
if (completed == 0) {
System.out.println("监听");
continue;
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//如果是连接事件,就在当前线程做连接操作,然后把连接得到的socketChannel注册到Selector中,等待下一个select
if (selectionKey.isAcceptable()) {
ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = serverSocketChannel1.accept();
System.out.println("得到一个新连接。");
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
//如果是写事件,放到线程池里面处理
else if (selectionKey.isReadable()) {
//
SocketChannel subSocketChannel = (SocketChannel) selectionKey.channel();
System.out.println("线程提交任务。");
ReadTask readTask = new ReadTask(subSocketChannel, "C:\\Users\\baopz\\Desktop\\test\\socket\\server\\" + atomicInteger.getAndAdd(1) + ".gif");
executorService.execute(readTask);
selectionKey.cancel();
}
//如果是读事件
else if (selectionKey.isWritable()) {
SocketChannel subSocketChannel = (SocketChannel) selectionKey.channel();
executorService.execute(new WriteTask(subSocketChannel));
}
iterator.remove();
}
}
}
Handler处理逻辑
实现了一个读事件,没有实现写事件。
public static class WriteTask implements Runnable {
private SocketChannel socketChannel;
public WriteTask(SocketChannel socketChannel) {
this.socketChannel = socketChannel;
}
@Override
public void run() {
System.out.println("执行一个读事件。");
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static class ReadTask implements Runnable {
private SocketChannel socketChannel;
private String filename;
public ReadTask(SocketChannel socketChannel, String filename) {
this.socketChannel = socketChannel;
this.filename = filename;
}
@Override
public void run() {
//16k
ByteBuffer buffer = ByteBuffer.allocateDirect(1 << 14);
Path path = Paths.get(filename);
Path parent = path.getParent();
if (Files.notExists(parent)) {
try {
Files.createDirectories(parent);
} catch (IOException e) {
e.printStackTrace();
System.out.println("创建文件夹失败:" + parent);
return;
}
}
if (Files.notExists(path)) {
try {
Files.createFile(path);
} catch (IOException e) {
e.printStackTrace();
System.out.println("文件失败:" + path);
return;
}
}
FileChannel outFileChannel = null;
RandomAccessFile randomAccessFile = null;
try {
randomAccessFile = new RandomAccessFile(path.toFile(), "rw");
outFileChannel = randomAccessFile.getChannel();
//这里buffer读取的时候,可能数据读不全,所以需要精细的设计,在Netty章节,我会详细讲一下它是怎么处理
while (socketChannel.read(buffer) > 0) {
buffer.flip();
outFileChannel.write(buffer);
buffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("执行清理工作");
if (outFileChannel != null) {
try {
outFileChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (randomAccessFile != null) {
try {
randomAccessFile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (socketChannel != null) {
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
异步网络通信
服务端程序
打开一个AsynchronusServerSocketChannel,绑定到端口上,accept请求。
private static void server(int port) throws IOException, InterruptedException {
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("服务处理程序%d").build();
//线程池
AsynchronousChannelGroup group = AsynchronousChannelGroup.withFixedThreadPool(4, threadFactory);
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open(group);
//这里的backlog可以参考网上资料,这里不详细讲,就是一个tcp/ip的一个参数,不想了解,可以忽略
serverSocketChannel.bind(new InetSocketAddress(port), 512);
System.out.println("服务端绑定端口[" + port + "]完成。");
serverSocketChannel.accept(new Attachment(serverSocketChannel, atomicInteger), new CopyHandler());
}
完成连接后的回调处理类
注意,这里在CopyHandler中的第一句,把AsynchronousServerSocketChannel.accept(),这里是当前这个serverchannel继续监听下一个请求。
可以用之前的client测试程序,for循环测试一下。
public static class CopyHandler implements CompletionHandler<AsynchronousSocketChannel, Attachment> {
@Override
public void completed(AsynchronousSocketChannel result, Attachment attachment) {
//接受下一次请求
attachment.getAsynchronousServerSocketChannel().accept(new Attachment(attachment.getAsynchronousServerSocketChannel(), attachment.atomicInteger), this);
//处理业务逻辑
try {
handle(result, attachment.getAtomicInteger());
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}
private void handle(AsynchronousSocketChannel result, AtomicInteger attachment) throws ExecutionException, InterruptedException {
System.out.println("连接完成。");
//16k
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1 << 14);
//这里是阻塞是获取数据,因为需要把数据放到byteBuffer中,所有,需要获取到数据
result.read(byteBuffer).get();
Path target = Paths.get("C:\\Users\\baopz\\Desktop\\test\\socket\\server\\" + attachment.getAndAdd(1) + ".gif");
if (Files.notExists(target)) {
try {
Files.createFile(target);
} catch (IOException e) {
e.printStackTrace();
}
}
//
try {
AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(target, StandardOpenOption.WRITE);
byteBuffer.flip();
while (!asynchronousFileChannel.write(byteBuffer, 0).isDone()) {
TimeUnit.MILLISECONDS.sleep(500);
}
asynchronousFileChannel.close();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
try {
result.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Attachment attachment) {
exc.printStackTrace();
}
}
保存参数的辅助类
在回调函数中需要传递进去一些数据。
public static class Attachment {
private AsynchronousServerSocketChannel asynchronousServerSocketChannel;
private AtomicInteger atomicInteger;
public Attachment(AsynchronousServerSocketChannel serverSocketChannel, AtomicInteger atomicInteger) {
this.asynchronousServerSocketChannel = serverSocketChannel;
this.atomicInteger = atomicInteger;
}
public AsynchronousServerSocketChannel getAsynchronousServerSocketChannel() {
return asynchronousServerSocketChannel;
}
public void setAsynchronousServerSocketChannel(AsynchronousServerSocketChannel asynchronousServerSocketChannel) {
this.asynchronousServerSocketChannel = asynchronousServerSocketChannel;
}
public AtomicInteger getAtomicInteger() {
return atomicInteger;
}
public void setAtomicInteger(AtomicInteger atomicInteger) {
this.atomicInteger = atomicInteger;
}
}
总结,到这里就把单线程模型,线程池处理,Reactor模型,异步通信模型都讲完了,下一篇讲解Java中的相关容器。