之前我以为java的socket很简单,不就是创建一个socketServet,然后不断的accept么?这种代码网上很多,基本流程是这样的:
ServerSocket server = new ServerSocket(8080); while (true) { Socket socket = server.accept(); InputStream input = socket.getInputStream(); int length = input.available(); if(length>0){ byte[] data = new byte[length]; input.read(data); String str = new String(data); String result = execute(str); OutputStream out = socket.getOutputStream(); out.write(("ok"+result+"\r\n").getBytes()); out.flush(); } socket.close(); }
虽然在做轮询,但accept()会阻塞,直到有新的客户端请求过来才会产生一个新的socket,然后读取数据,处理数据,最后关闭。基本流程是 accept()--->read()--->hand()--->close()。
代码看上去很简单,也能成功运行,但这段代码存在2个问题:
1. 一个socket只能处理一次就被关闭了,对于局域网的RPC来说这是很不划算的,很多时候我们希望能是长链接。处理流程变成accept()-->read()-->hand()-->read()-->hand()-->close()
2. 如果一个client在一个socket中连续发送两次数据,client端代码会报错,因为socket在处理玩第一次read数据之后就被server关闭了。
所以为了解决上面两个问题,我把这段代码修改一下,每个socket一个线程来处理:
class Channel implements Runnable { private Socket socket; private ServiceRegisterTester t ; public Channel(Socket socket,ServiceRegisterTester t ){ super(); this.socket = socket; this.t = t; } public void run() { try{ Socket socket = this.socket; while(true){ InputStream input = socket.getInputStream(); int length = input.available(); if(length>0){ byte[] data = new byte[length]; input.read(data); String str = new String(data); String result = t.test(str); OutputStream out = socket.getOutputStream(); out.write(("ok"+result+"\r\n").getBytes()); out.flush(); } } }catch(Exception e){ e.printStackTrace(); } } }
ServerSocket server = new ServerSocket(8080); while (true) { Socket socket = server.accept(); Channel chanenl = new Channel(socket,t); new Thread(chanenl).start(); }
这样每个socket都有一个单独的线程来处理,前面2个问题解决了!
但通过top观测发现java进程cpu占用非常高,都在做空转,每个socket都有线程在做轮询,这太伤害性能了吧,怎么才能让
socket.getInputStream();
也阻塞,让他能智能的读取到可用的数据才唤醒呢?
我相信netty也会遇到这个问题看看netty是如何解决的吧:
ChannelFactory factory = new OioServerSocketChannelFactory ( Executors.newCachedThreadPool(), Executors.newCachedThreadPool()); ServerBootstrap bootstrap = new ServerBootstrap (factory); DiscardServerHandler handler = new DiscardServerHandler(); ChannelPipeline pipeline = bootstrap.getPipeline(); pipeline.addLast("handler", handler); bootstrap.setOption("child.tcpNoDelay", true); bootstrap.setOption("child.keepAlive", true); bootstrap.bind(new InetSocketAddress(8080));
ChannelFactory需要两个线程池,一个是boss,一个是worker,boss只需要一个线程即可,worker可以根据合适的情况配置。当在执行 bootstrap.bind()的时候会启动boss线程,代码如下:
class OioServerSocketPipelineSink{ private void bind( OioServerSocketChannel channel, ChannelFuture future, SocketAddress localAddress) { Executor bossExecutor = ((OioServerSocketChannelFactory) channel.getFactory()).bossExecutor; bossExecutor.execute( new IoWorkerRunnable( new ThreadRenamingRunnable( new Boss(channel), "Old I/O server boss (channelId: " + channel.getId() + ", " + localAddress + ')'))); bossStarted = true; } }
OioServerSocketPipelineSink&Boss 是一个Runnable,其run方法如下:
while (channel.isBound()) { try { Socket acceptedSocket = channel.socket.accept(); try { ChannelPipeline pipeline = channel.getConfig().getPipelineFactory().getPipeline(); final OioAcceptedSocketChannel acceptedChannel = new OioAcceptedSocketChannel( channel, channel.getFactory(), pipeline, OioServerSocketPipelineSink.this, acceptedSocket); workerExecutor.execute( new IoWorkerRunnable( new ThreadRenamingRunnable( new OioWorker(acceptedChannel), "Old I/O server worker (parentId: " + channel.getId() + ", channelId: " + acceptedChannel.getId() + ", " + channel.getRemoteAddress() + " => " + channel.getLocalAddress() + ')'))); } catch (Exception e) { logger.warn( "Failed to initialize an accepted socket.", e); try { acceptedSocket.close(); } catch (IOException e2) { logger.warn( "Failed to close a partially accepted socket.", e2); } } }
可以看到Boss的职责就是轮询获取每个新请求Socket,立即交给workerExecutor处理,workerExecutor的逻辑单元封装在OioWorker,OioWorker的run方法如下:
public void run(){ final PushbackInputStream in = channel.getInputStream(); while (channel.isOpen()) { synchronized (channel.interestOpsLock) { while (!channel.isReadable()) { try { // notify() is not called at all. // close() and setInterestOps() calls Thread.interrupt() channel.interestOpsLock.wait(); } catch (InterruptedException e) { if (!channel.isOpen()) { break; } } } } byte[] buf; int readBytes; try { int bytesToRead = in.available(); if (bytesToRead > 0) { buf = new byte[bytesToRead]; readBytes = in.read(buf); } else { int b = in.read(); if (b < 0) { break; } in.unread(b); continue; } } catch (Throwable t) { if (!channel.socket.isClosed()) { fireExceptionCaught(channel, t); } break; } ChannelBuffer buffer; if (readBytes == buf.length) { buffer = ChannelBuffers.wrappedBuffer(buf); } else { // A rare case, but it sometimes happen. buffer = ChannelBuffers.wrappedBuffer(buf, 0, readBytes); } fireMessageReceived(channel, buffer); } // Setting the workerThread to null will prevent any channel // operations from interrupting this thread from now on. channel.workerThread = null; // Clean up. close(channel, succeededFuture(channel)); }
OioWorker里有一个非常重要的InputStream-PushbackInputStream,这个输入流能阻塞io,请看其read()方法的注释:“从此输入流中读取下一个数据字节。返回 0 到 255 范围内的 int 字节值。如果因流的末尾已到达而没有可用的字节,则返回值 -1。在输入数据可用、检测到流的末尾或者抛出异常前,此方法一直阻塞。”
netty就是利用这种方式来做轮询,而CPU又不至于空转。