多线程在网络编程中的应用
Java IO与多线程
多线程在Socket编程中的作用
IO可以分为文件IO,网络IO等等。本篇我们主要使用网络IO来介绍。
对于标准的网络IO来说,我们一般使用Socket进行网络的读写,为了让服务器可以支持更多的客户端连接,通常为每一个客户端连接开启一个线程。
服务器会为每一个客户端连接启用一个线程,这个线程将只为这个客户端服务。同时,为了接受客户端连接,服务器还会额外使用一个派发线程。
Socket多线程网络编程案例
服务端代码如下:
package test30;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
/**
* 服务端处理消息线程
* 读取客户端消息之后
* 计算每个客户端消息的耗时
*/
public class HandleMsg implements Runnable {
private Socket clientSocket;
public HandleMsg(Socket clientSocket) {
this.clientSocket = clientSocket;
}
@Override
public void run() {
BufferedReader is = null;
PrintWriter os = null;
try{
is = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
os = new PrintWriter(clientSocket.getOutputStream(),true);
String inputLine = null;
long b = System.currentTimeMillis();
while ((inputLine = is.readLine())!=null){
os.println(inputLine);
}
long e = System.currentTimeMillis();
System.out.println("spend:"+(e-b)+"ms");
}catch (IOException e){
e.printStackTrace();
}finally {
try{
if(is!=null) is.close();
if(os!=null) os.close();
clientSocket.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
package test30;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 服务端启动类
* 监听客户端的连接
* 连接成功之后,
* 使用newCachedThreadPool()类型的线程池提交客户端任务,相当于为每个客户端开启一个线程
*/
public class ServerTest {
public static void main(String[] args) {
ExecutorService es = Executors.newCachedThreadPool();
ServerSocket echoServer = null;
Socket clientSocket = null;
try{
echoServer = new ServerSocket(8000);
}catch (IOException e){
System.out.println(e);
}
while(true){
try{
clientSocket = echoServer.accept();
System.out.println(clientSocket.getRemoteSocketAddress()+" connect!");
es.execute(new HandleMsg(clientSocket));
}catch (IOException e){
System.out.println(e);
}
}
}
}
客户端代码如下:
package test30;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.concurrent.locks.LockSupport;
/**
* 客户端线程,负责向服务端发送消息
* 并接受服务端返回的消息
*/
public class EchoClient implements Runnable {
private final int sleep_time= 1000*1000*1000;
@Override
public void run() {
Socket client = null;
PrintWriter writer = null;
BufferedReader reader = null;
try{
client = new Socket();
client.connect(new InetSocketAddress("localhost",8000));
writer = new PrintWriter(client.getOutputStream(),true);
writer.print("H");
LockSupport.parkNanos(sleep_time);
writer.print("e");
LockSupport.parkNanos(sleep_time);
writer.print("l");
LockSupport.parkNanos(sleep_time);
writer.print("l");
LockSupport.parkNanos(sleep_time);
writer.print("o");
LockSupport.parkNanos(sleep_time);
writer.print("!");
LockSupport.parkNanos(sleep_time);
writer.println();
writer.flush();
reader = new BufferedReader(new InputStreamReader(client.getInputStream()));
System.out.println("from server: "+reader.readLine());
} catch (IOException e) {
e.printStackTrace();
} finally {
try{
if(writer!=null) writer.close();
if(reader!=null) reader.close();
if(client!= null) client.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
package test30;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 使用newCachedThreadPool类型的线程池提交10次任务
* 相当于开启10个线程
*/
public class ClientTest {
public static void main(String[] args) throws IOException {
ExecutorService es = Executors.newCachedThreadPool();
EchoClient ec = new EchoClient();
for (int i = 0; i < 10; i++) {
es.execute(ec);
}
}
}
程序的运行结果如下所示
服务端运行结果
客户端运行结果
在这个案例中,服务器之所以处理慢,并不是因为在服务端有多少繁重的任务,而是应为服务线程在等待IO,让高速运转的CPU去等待及其低效的网络IO是非常不合算的行为,那么,我们需要想一个办法,将网络IO的等待时间从线程中分离出来
Java NIO与多线程
使用Java nio就可以将网络io等待时间从业务处理线程中抽取出来。
NIO中关键组件
Channel(通道):类似于普通Java IO编程中的流。这个Channel可以和文件或者网络Socket对应。如果Channel对应着一个Socket,那么往Channel中写数据,就等于向Socket中写入数据。
Buffer(缓冲区):负责数据存储。数据需要包装成Buffer形式才能和Channel交互(写入或者读取)。
Selector(选择器):在Channel的众多实现中,有一个SelectableChannel实现,表示“可被选择的通道”,任何一个SelectableChannel都可以将自己注册到一个Selector中,因此这个Channel就可以被Selector管理。一个Selector可以管理多个SelectableChannel。当SelectableChannel的数据准备好时,Selector就会接到通知,得到那些已经准备好的数据,而SocketChannel就是SelectableChannel的一种。
它们的关系如下图所示。
NIO案例
服务端代码:
package test31;
/**
* 服务端启动类
*/
public class ServerTest {
public static void main(String[] args) {
MultiThreadNIOEchoServer echoServer = new MultiThreadNIOEchoServer();
try {
echoServer.startServer();
} catch (Exception e) {
System.out.println("Exception caught, program exiting...");
e.printStackTrace();
}
}
}
package test31;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 服务端资源操作类
*/
public class MultiThreadNIOEchoServer {
//定义一个选择器
private Selector selector;
private ExecutorService es = Executors.newCachedThreadPool();
//统计服务器线程在一个客户端花费的时间,定义一个与时间统计的变量
Map<Socket,Long> time_stat = new HashMap<>(10240);
/**
* 启动服务器方法
* @throws Exception
*/
public void startServer() throws Exception{
//得到selector对象实例
selector = SelectorProvider.provider().openSelector();
//得到服务器的ServerSocketChannel实例
ServerSocketChannel ssc = ServerSocketChannel.open();
//将其设置为非阻塞方式。
ssc.configureBlocking(false);
//进行端口绑定,将通道绑定至8000端口
//InetSocketAddress isa = new InetSocketAddress(InetAddress.getLocalHost(),8000);
InetSocketAddress isa = new InetSocketAddress(8000);
ssc.socket().bind(isa);
//将ServerSocketChannel绑定到Selector上,并注册它的事件为Accept
SelectionKey acceptKey = ssc.register(selector,SelectionKey.OP_ACCEPT);
while (true){
//阻塞方法,没有数据准备好,等待;有数据可读,返回
selector.select();
//selector可同时为多个channel服务,所以已经准备就绪的channel可能是多个。
Set readyKeys = selector.selectedKeys();
Iterator i = readyKeys.iterator();
long e = 0;
while (i.hasNext()){
SelectionKey sk = (SelectionKey)i.next();
//获取到selectionKey后,就将其从集合中移除,以免重复处理
//注:正确删除集合元素的方法,是使用迭代器
i.remove();
//判断当前selectionKey是否在Acceptable状态,如果是,就进行接收客户端内容
if(sk.isAcceptable()){
doAccept(sk);
}else if(sk.isValid()&& sk.isReadable()){
//记录一下此客户端连接的时间,在读取前的时间戳。
if(!time_stat.containsKey(((SocketChannel)sk.channel()).socket())){
time_stat.put(((SocketChannel)sk.channel()).socket(),System.currentTimeMillis());
}
doRead(sk);
}else if(sk.isValid()&&sk.isWritable()){
doWrite(sk);
e = System.currentTimeMillis();
//写出完成后,移除该socket,并返回读入前的时间戳。
long b = time_stat.remove(((SocketChannel)sk.channel()).socket());
System.out.println("spend:"+(e - b)+"ms");
}
}
}
}
/**
* 与客户端建立连接的方法
* @param sk
*/
private void doAccept(SelectionKey sk) {
ServerSocketChannel server = (ServerSocketChannel)sk.channel();
SocketChannel clientChannel;
try{
clientChannel = server.accept();
//设置为非阻塞模式,要求系统在准备好IO之后,就通知线程来读取或者写入
clientChannel.configureBlocking(false);
//注册此通道为读
//当selector发现这个channel已经准备好读时,就能给线程一个通知
SelectionKey clientKey = clientChannel.register(selector,SelectionKey.OP_READ);
//分配一个客户端Bean实例,并绑定在clientKey,这样在整个连接的处理过程中,我们可以共享此实例
Echo echo = new Echo();
clientKey.attach(echo);
InetAddress clientAddress = clientChannel.socket().getInetAddress();
System.out.println("Accepted connection from: "+clientAddress.getHostAddress()+".");
}catch (Exception e){
System.out.println("Failed to accept new client.");
e.printStackTrace();
}
}
/**
* 当channel可以读取时,doRead()方法就可以被调用
* @param sk
*/
private void doRead(SelectionKey sk) {
SocketChannel channel = (SocketChannel)sk.channel();
ByteBuffer byteBuffer=ByteBuffer.allocate(8192);
int len;
try{
len = channel.read(byteBuffer);
if(len<0){
disconnect(sk);
return;
}
}catch (Exception e){
System.out.println("Failed to read from client.");
e.printStackTrace();
disconnect(sk);
return;
}
//读取完成后,重置缓冲区
byteBuffer.flip();
es.execute(new HandleMsg(sk,byteBuffer,selector));
}
/**
* 回写至客户端
* @param sk
*/
private void doWrite(SelectionKey sk) {
SocketChannel channel = (SocketChannel)sk.channel();
Echo echo = (Echo)sk.attachment();
LinkedList<ByteBuffer> outputQueueContent = echo.getOutqueue();
//获得列表顶部元素
ByteBuffer byteBuffer = outputQueueContent.getLast();
try{
int len = channel.write(byteBuffer);
if(len == -1){
disconnect(sk);
return;
}
if(byteBuffer.remaining() == 0){
outputQueueContent.removeLast();
}
}catch (Exception e){
System.out.println("Failed to write to client.");
e.printStackTrace();
disconnect(sk);
}
//全部数据发送完成后,需要将写事件从感兴趣的操作中移除。
if(outputQueueContent.size()==0){
sk.interestOps(SelectionKey.OP_READ);
}
}
private void disconnect(SelectionKey sk) {
SocketChannel channel = (SocketChannel) sk.channel();
InetAddress clientAddress = channel.socket().getInetAddress();
System.out.println(clientAddress.getHostAddress() + " disconnected.");
try {
channel.close();
} catch (Exception e) {
System.out.println("Failed to close client socket channel.");
e.printStackTrace();
}
}
}
package test31;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
/**
* 服务端处理消息线程
* 读取客户端消息
*/
public class HandleMsg implements Runnable {
private SelectionKey sk;
private ByteBuffer bb;
private Selector selector;
public HandleMsg(SelectionKey sk, ByteBuffer bb, Selector selector) {
this.sk = sk;
this.bb = bb;
this.selector = selector;
}
@Override
public void run() {
Echo echo = (Echo)sk.attachment();
echo.enterQueue(bb);
//我们将数据处理完成后,还会回写客户端,所以将读和写都提交,这样在
//通道准备好写入时,就能通知线程。
sk.interestOps(SelectionKey.OP_READ|SelectionKey.OP_WRITE);
//强迫selector立即返回
selector.wakeup();
}
}
package test31;
import java.nio.ByteBuffer;
import java.util.LinkedList;
/**
* 客户端实例类,封装队列,保存在需要回复给此客户端
* 的所有信息
*/
public class Echo {
private LinkedList<ByteBuffer> outqueue;
public Echo() {
this.outqueue = new LinkedList<>();
}
/**
* 返回队列
* @return
*/
public LinkedList<ByteBuffer> getOutqueue() {
return outqueue;
}
/**
* 进入队列
* @param byteBuffer
*/
public void enterQueue(ByteBuffer byteBuffer) {
this.outqueue.addFirst(byteBuffer);
}
}
客户端代码如下:
package test31;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.Iterator;
public class NIOClient {
private Selector selector;
public void init(String ip, int port) throws IOException {
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
this.selector = SelectorProvider.provider().openSelector();
channel.connect(new InetSocketAddress(ip, port));
channel.register(selector, SelectionKey.OP_CONNECT);
}
public void working() throws IOException {
while (true) {
if (!selector.isOpen())
break;
selector.select();
Iterator<SelectionKey> ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = ite.next();
ite.remove();
// 连接事件发生
if (key.isConnectable()) {
connect(key);
} else if (key.isReadable()) {
read(key);
}
}
}
}
/**
* 处理读取服务端发来的信息 的事件
*
* @param key
* @throws IOException
*/
public void read(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(100);
channel.read(buffer);
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("客户端收到信息:" + msg);
channel.close();
key.selector().close();
}
public void connect(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
// 如果正在连接,则完成连接
if (channel.isConnectionPending()) {
channel.finishConnect();
}
channel.configureBlocking(false);
channel.write(ByteBuffer.wrap(new String("hello server!\r\n")
.getBytes()));
channel.register(this.selector, SelectionKey.OP_READ);
}
/**
* 启动客户端测试
*
* @throws IOException
*/
public static void main(String[] args) throws IOException {
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
NIOClient client = new NIOClient();
try {
client.init("localhost", 8000);
client.working();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
案例运行效果如下:
AIO与多线程
AIO概述
AIO是异步IO的缩写,虽然NIO在网络操作中提供了非阻塞的方法,但是NIO的IO行为还是同步的。对于NIO来说,我们的业务线程是在IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身还是同步的。
对于AIO来说,它不是在IO准备好时再通知线程,而是在IO操作已经完成后,再给线程发出通知。因此,AIO是完全不会阻塞的。此时,我们的业务逻辑将变成一个回调函数,等待IO操作完成后,由系统自动触发。
异步类和方法介绍
异步服务端Socket通道(AsynchronousServerSocketChannel):accept()方法会立即返回,它并不会真的等待客户端的到来
/**
* Accepts a connection.
*
* <p> This method initiates an asynchronous operation to accept a
* connection made to this channel's socket. The {@code handler} parameter is
* a completion handler that is invoked when a connection is accepted (or
* the operation fails). The result passed to the completion handler is
* the {@link AsynchronousSocketChannel} to the new connection.
*
* <p> When a new connection is accepted then the resulting {@code
* AsynchronousSocketChannel} will be bound to the same {@link
* AsynchronousChannelGroup} as this channel. If the group is {@link
* AsynchronousChannelGroup#isShutdown shutdown} and a connection is accepted,
* then the connection is closed, and the operation completes with an {@code
* IOException} and cause {@link ShutdownChannelGroupException}.
*
* <p> To allow for concurrent handling of new connections, the completion
* handler is not invoked directly by the initiating thread when a new
* connection is accepted immediately (see <a
* href="AsynchronousChannelGroup.html#threading">Threading</a>).
*
* <p> If a security manager has been installed then it verifies that the
* address and port number of the connection's remote endpoint are permitted
* by the security manager's {@link SecurityManager#checkAccept checkAccept}
* method. The permission check is performed with privileges that are restricted
* by the calling context of this method. If the permission check fails then
* the connection is closed and the operation completes with a {@link
* SecurityException}.
*
* @param <A>
* The type of the attachment
* @param attachment
* The object to attach to the I/O operation; can be {@code null}
* @param handler
* The handler for consuming the result
*
* @throws AcceptPendingException
* If an accept operation is already in progress on this channel
* @throws NotYetBoundException
* If this channel's socket has not yet been bound
* @throws ShutdownChannelGroupException
* If the channel group has terminated
*/
public abstract <A> void accept(A attachment,
CompletionHandler<AsynchronousSocketChannel,? super A> handler);
此方法第一个参数是一个附件,可以是任意类型,作用是让当前线程和后续的回调方法可以共享信息,它会在后续调用中传递给handler。
第二个参数是CompletionHandler接口,此接口有两个方法
/**
* A handler for consuming the result of an asynchronous I/O operation.
*
* <p> The asynchronous channels defined in this package allow a completion
* handler to be specified to consume the result of an asynchronous operation.
* The {@link #completed completed} method is invoked when the I/O operation
* completes successfully. The {@link #failed failed} method is invoked if the
* I/O operations fails. The implementations of these methods should complete
* in a timely manner so as to avoid keeping the invoking thread from dispatching
* to other completion handlers.
*
* @param <V> The result type of the I/O operation
* @param <A> The type of the object attached to the I/O operation
*
* @since 1.7
*/
public interface CompletionHandler<V,A> {
/**
* Invoked when an operation has completed.
*
* @param result
* The result of the I/O operation.
* @param attachment
* The object attached to the I/O operation when it was initiated.
*/
void completed(V result, A attachment);
/**
* Invoked when an operation fails.
*
* @param exc
* The exception to indicate why the I/O operation failed
* @param attachment
* The object attached to the I/O operation when it was initiated.
*/
void failed(Throwable exc, A attachment);
}
accept方法实际上做了两件事。第一,发起accept请求,告诉系统可以开始监听端口。第二,注册CompletionHandler实例,告诉系统一旦有客户端前来连接,如果连接成功,就去执行completed()方法,如果连接失败,就去执行failed()方法。
所以,server.accept()方法不会阻塞,它会立即返回。
AIO案例
服务端代码:
package test32;
public class ServerTest {
public static void main(String[] args) throws Exception {
new AIOEchoServer().start();
//主线程可以继续自己的行为
//由于start()方法使用的都是异步方法,因此它会马上返回,它并不像阻塞方法那样会进行的等待
//因此如果想让驻守执行,下列的语句是必须的。否则start()方法结束后,不等客户端到来
//程序已经完成,主线程退出
while (true){
Thread.sleep(1000);
}
}
}
package test32;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class AIOEchoServer {
public final static int PORT = 8000;
//异步IO需要使用异步通道
private AsynchronousServerSocketChannel server;
public AIOEchoServer() throws IOException {
//通道打开并绑定服务端端口
this.server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(PORT));
}
public void start(){
System.out.println("Server listen on "+PORT);
//注册事件和事件完成后的处理器
//accept()方法会立即返回,它不会真的等待客户端的到来
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
final ByteBuffer buffer = ByteBuffer.allocate(1024);
@Override
public void completed(AsynchronousSocketChannel result, Object attachment) {
System.out.println(Thread.currentThread().getName());
Future<Integer> writeResult = null;
try{
buffer.clear();
//读取客户端传来的数据,read方法是异步的
//它不会等待读取完成了再返回,而是立即返回
result.read(buffer).get(100, TimeUnit.SECONDS);
buffer.flip();
//将数据回写客户端
writeResult = result.write(buffer);
}catch (InterruptedException | ExecutionException | TimeoutException e){
e.printStackTrace();
}finally {
try {
//进行下一个客户端连接的准备
server.accept(null,this);
//同时关闭当前正在处理的客户端连接
//在关闭之前,确保write方法已经完成,因此,使用Future.get()方法进行等待。
writeResult.get();
result.close();
}catch (Exception e){
System.out.println(e.toString());
}
}
}
@Override
public void failed(Throwable exc, Object attachment) {
System.out.println("failed: "+exc);
}
});
}
}
客户端代码:
package test32;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
public class AIOClient {
public static void main(String[] args) throws Exception {
//打开通道
final AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
client.connect(new InetSocketAddress("localhost", 8000), null, new CompletionHandler<Void, Object>() {
@Override
public void completed(Void result, Object attachment) {
//连接成功,进行数据写入,向服务端发送数据
client.write(ByteBuffer.wrap("Hello!".getBytes()), null, new CompletionHandler<Integer, Object>() {
@Override
public void completed(Integer result, Object attachment) {
//进行数据读取,从服务端读取回写的数据
try {
ByteBuffer buffer = ByteBuffer.allocate(1024);
//立即返回
client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
//打印输出
buffer.flip();
System.out.println(new String(buffer.array()));
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Object attachment) {
}
});
}
@Override
public void failed(Throwable exc, Object attachment) {
}
});
//由于主线程马上结束,这里等待上述处理全部完成
Thread.sleep(1000);
}
}
运行结果: