Netty、MINA、Twisted一起學系列01:實現簡單的TCP伺服器

程式設計師私房菜發表於2019-01-03

Netty、MINA、Twisted一起學系列01:實現簡單的TCP伺服器

文章已獲得作者授權

MINA、Netty、Twisted為什麼放在一起學習?首先,不妨先分別看一下它們官方網站對其的介紹。

MINA:

Apache MINA is a network application framework which helps users develop high performance and high scalability network applications easily. It provides an abstract event-driven asynchronous API over various transports such as TCP/IP and UDP/IP via Java NIO.

Netty:

Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.

Twisted:

Twisted is an event-driven networking engine written in Python and licensed under the open source MIT license.

Twisted 官從上面簡短的介紹中,就可以發現它們的共同特點:event-driven 以及 asynchronous。它們都是事件驅動非同步的網路程式設計框架。由此可見,它們之間的共同點還是很明顯的。所以我這裡將這三個框架放在一起,實現相同的功能,不但可以用少量的精力學三樣東西,而且還可以對它們之間進行各方面的對比。網的文案不專業啊,居然不寫 asynchronous。

從上面簡短的介紹中,就可以發現它們的共同特點:event-driven 以及 asynchronous。它們都是事件驅動非同步的網路程式設計框架。由此可見,它們之間的共同點還是很明顯的。所以我這裡將這三個框架放在一起,實現相同的功能,不但可以用少量的精力學三樣東西,而且還可以對它們之間進行各方面的對比。

其中 MINA 和 Netty 是基於 Java 語言的,而 Twisted 是 Python 語言的。不過語言不是重點,重點的是理念。

使用傳統的 BIO(Blocking IO/阻塞IO)進行網路程式設計時,進行網路 IO 讀寫時都會阻塞當前執行緒,如果實現一個 TCP 伺服器,都需要對每個客戶端連線開啟一個執行緒,而很多執行緒可能都在傻傻的阻塞住等待讀寫資料,系統資源消耗大。

而 NIO(Non-Blocking IO/非阻塞IO)或 AIO(Asynchronous IO/非同步IO)則是透過 IO 多路複用技術實現,不需要為每個連線建立一個執行緒,其底層實現是透過作業系統的一些特性如 select、poll、epoll、iocp 等。這三個網路框架都是基於此實現。

下面分別用這三個框架實現一個最簡單的 TCP 伺服器。當接收到客戶端發過來的字串後,向客戶端回寫一個字串作為響應。

Netty:

public class TcpServer {

 public static void main(String[] args) throws InterruptedException {
   EventLoopGroup bossGroup = new NioEventLoopGroup();
   EventLoopGroup workerGroup = new NioEventLoopGroup();
   try {
     ServerBootstrap b = new ServerBootstrap();
     b.group(bossGroup, workerGroup)
         .channel(NioServerSocketChannel.class)
         .childHandler(new ChannelInitializer<SocketChannel>() {
           @Override
           public void initChannel(SocketChannel ch)
               throws Exception
{
             ch.pipeline().addLast(new TcpServerHandler());
           }
         });
     ChannelFuture f = b.bind(8080).sync();
     f.channel().closeFuture().sync();
   } finally {
     workerGroup.shutdownGracefully();
     bossGroup.shutdownGracefully();
   }
 }

}

class TcpServerHandler extends ChannelInboundHandlerAdapter {

 // 接收到新的資料
 @Override
 public void channelRead(ChannelHandlerContext ctx, Object msg) throws UnsupportedEncodingException {
   try {
     // 接收客戶端的資料
     ByteBuf in = (ByteBuf) msg;
     System.out.println("channelRead:" + in.toString(CharsetUtil.UTF_8));

     // 傳送到客戶端
     byte[] responseByteArray = "你好".getBytes("UTF-8");
     ByteBuf out = ctx.alloc().buffer(responseByteArray.length);
     out.writeBytes(responseByteArray);
     ctx.writeAndFlush(out);

   } finally {
     ReferenceCountUtil.release(msg);
   }
 }

 @Override
 public void channelActive(ChannelHandlerContext ctx) {
   System.out.println("channelActive");
 }

 @Override
 public void channelInactive(ChannelHandlerContext ctx){
   System.out.println("channelInactive");
 }

 @Override
 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
   cause.printStackTrace();
   ctx.close();
 }
}

MINA:

public class TcpServer {

 public static void main(String[] args) throws IOException {
   IoAcceptor acceptor = new NioSocketAcceptor();
   acceptor.setHandler(new TcpServerHandle());
   acceptor.bind(new InetSocketAddress(8080));
 }

}

class TcpServerHandle extends IoHandlerAdapter {

 @Override
 public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
   cause.printStackTrace();
 }

 // 接收到新的資料
 @Override
 public void messageReceived(IoSession session, Object message) throws Exception {

   // 接收客戶端的資料
   IoBuffer ioBuffer = (IoBuffer) message;
   byte[] byteArray = new byte[ioBuffer.limit()];
   ioBuffer.get(byteArray, 0, ioBuffer.limit());
   System.out.println("messageReceived:" + new String(byteArray, "UTF-8"));

   // 傳送到客戶端
   byte[] responseByteArray = "你好".getBytes("UTF-8");
   IoBuffer responseIoBuffer = IoBuffer.allocate(responseByteArray.length);
   responseIoBuffer.put(responseByteArray);
   responseIoBuffer.flip();
   session.write(responseIoBuffer);
 }

 @Override
 public void sessionCreated(IoSession session) throws Exception {
   System.out.println("sessionCreated");
 }

 @Override
 public void sessionClosed(IoSession session) throws Exception {
   System.out.println("sessionClosed");
 }
}

Twisted:

# -*- coding:utf-8 –*-

from twisted.internet.protocol import Protocol
from twisted.internet.protocol import Factory
from twisted.internet import reactor

class TcpServerHandle(Protocol):

   # 新的連線建立
   def connectionMade(self):
       print 'connectionMade'

   # 連線斷開
   def connectionLost(self, reason):
       print 'connectionLost'

   # 接收到新資料
   def dataReceived(self, data):
       print 'dataReceived', data
       self.transport.write('你好')

factory = Factory()
factory.protocol = TcpServerHandle
reactor.listenTCP(8080, factory)
reactor.run()

從上面的程式碼可以看出,這三個框架實現的 TCP 伺服器,在連線建立、接收到客戶端傳來的資料、連線關閉時,都會觸發某個事件。例如接收到客戶端傳來的資料時,MINA 會觸發事件呼叫 messageReceived,Netty 會呼叫 channelRead,Twisted 會呼叫 dataReceived。編寫程式碼時,只需要繼承一個類並重寫響應的方法即可。這就是 event-driven 事件驅動。

下面是 Java 寫的一個 TCP 客戶端用作測試,客戶端沒有使用這三個框架,也沒有使用 NIO,只是一個普通的 BIO 的 TCP 客戶端。

TCP 在建立連線到關閉連線的過程中,可以多次進行傳送和接收資料。下面的客戶端傳送了兩個字串到伺服器並兩次獲取伺服器回應的資料,之間透過 Thread.sleep(5000) 間隔 5 秒。

public class TcpClient {

 public static void main(String[] args) throws IOException, InterruptedException {


   Socket socket = null;
   OutputStream out = null;
   InputStream in = null;

   try{

     socket = new Socket("localhost", 8080);      
     out = socket.getOutputStream();
     in = socket.getInputStream();

       // 請求伺服器
       out.write("第一次請求".getBytes("UTF-8"));
       out.flush();

       // 獲取伺服器響應,輸出
       byte[] byteArray = new byte[1024];
       int length = in.read(byteArray);
       System.out.println(new String(byteArray, 0, length, "UTF-8"));

       Thread.sleep(5000);

       // 再次請求伺服器
       out.write("第二次請求".getBytes("UTF-8"));
       out.flush();

       // 再次獲取伺服器響應,輸出
       byteArray = new byte[1024];
       length = in.read(byteArray);
       System.out.println(new String(byteArray, 0, length, "UTF-8"));


   } finally {
     // 關閉連線
     in.close();
     out.close();
     socket.close();
   }

 }

}

用客戶端分別測試上面三個 TCP 伺服器,看一下結果如何。

MINA伺服器輸出結果:

sessionCreated

messageReceived:第一次請求

messageReceived:第二次請求

sessionClosed

Netty伺服器輸出結果:

channelActive

channelRead:第一次請求

channelRead:第二次請求

channelInactive

Twisted伺服器輸出結果:

connectionMade

dataReceived: 第一次請求

dataReceived: 第二次請求

connectionLost

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31558358/viewspace-2295477/,如需轉載,請註明出處,否則將追究法律責任。

相關文章