Netty权威指南(笔记一)

转载:http://blog.csdn.net/clarkkentyang/article/details/52529785

第一章(略)

第二章 NIO入门

2.1传统的BIO编程(同步阻塞I/O服务端通信模型【一客户一线程】)

网络编程的基本模型:Client/Server模型,也就是2个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。

传统同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功之后,双方通过输入和输出流进行同步阻塞式通信。

缺点:缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,由于线程是Java虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。

服务端代码:

  1. <span style="white-space:pre">    </span>public static void main(String[] args) throws IOException {
  2. int port = 8080;
  3. if (args != null && args.length > 0) {
  4. try {
  5. port = Integer.valueOf(args[0]);
  6. } catch (NumberFormatException e) {
  7. // 采用默认值
  8. }
  9. }
  10. ServerSocket server = null;
  11. try {
  12. server = new ServerSocket(port);
  13. System.out.println("The time server is start in port : " + port);
  14. Socket socket = null;
  15. while (true) {
  16. socket = server.accept();
  17. new Thread(new TimeServerHandler(socket)).start();
  18. }
  19. } finally {
  20. if (server != null) {
  21. System.out.println("The time server close");
  22. server.close();
  23. server = null;
  24. }
  25. }
  26. }

TimeServerHandler代码:

  1. public class TimeServerHandler implements Runnable {
  2. private Socket socket;
  3. public TimeServerHandler(Socket socket) {
  4. this.socket = socket;
  5. }
  6. /*
  7. * (non-Javadoc)
  8. *
  9. * @see java.lang.Runnable#run()
  10. */
  11. @Override
  12. public void run() {
  13. BufferedReader in = null;
  14. PrintWriter out = null;
  15. try {
  16. in = new BufferedReader(new InputStreamReader(
  17. this.socket.getInputStream()));
  18. out = new PrintWriter(this.socket.getOutputStream(), true);
  19. String currentTime = null;
  20. String body = null;
  21. while (true) {
  22. body = in.readLine();
  23. if (body == null)
  24. break;
  25. System.out.println("The time server receive order : " + body);
  26. currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(
  27. System.currentTimeMillis()).toString() : "BAD ORDER";
  28. out.println(currentTime);
  29. }
  30. } catch (Exception e) {
  31. if (in != null) {
  32. try {
  33. in.close();
  34. } catch (IOException e1) {
  35. e1.printStackTrace();
  36. }
  37. }
  38. if (out != null) {
  39. out.close();
  40. out = null;
  41. }
  42. if (this.socket != null) {
  43. try {
  44. this.socket.close();
  45. } catch (IOException e1) {
  46. e1.printStackTrace();
  47. }
  48. this.socket = null;
  49. }
  50. }
  51. }
  52. }

客户端代码:

  1. <span style="white-space:pre">    </span>public static void main(String[] args) {
  2. int port = 8080;
  3. if (args != null && args.length > 0) {
  4. try {
  5. port = Integer.valueOf(args[0]);
  6. } catch (NumberFormatException e) {
  7. // 采用默认值
  8. }
  9. }
  10. Socket socket = null;
  11. BufferedReader in = null;
  12. PrintWriter out = null;
  13. try {
  14. socket = new Socket("127.0.0.1", port);
  15. in = new BufferedReader(new InputStreamReader(
  16. socket.getInputStream()));
  17. out = new PrintWriter(socket.getOutputStream(), true);
  18. out.println("QUERY TIME ORDER");
  19. System.out.println("Send order 2 server succeed.");
  20. String resp = in.readLine();
  21. System.out.println("Now is : " + resp);
  22. } catch (Exception e) {
  23. e.printStackTrace();
  24. } finally {
  25. if (out != null) {
  26. out.close();
  27. out = null;
  28. }
  29. if (in != null) {
  30. try {
  31. in.close();
  32. } catch (IOException e) {
  33. e.printStackTrace();
  34. }
  35. in = null;
  36. }
  37. if (socket != null) {
  38. try {
  39. socket.close();
  40. } catch (IOException e) {
  41. e.printStackTrace();
  42. }
  43. socket = null;
  44. }
  45. }
  46. }

2.2伪异步I/O编程

在BIO的基础上进行优化,后端通过一个线程池来处理多个客户端的请求接入,通过线程池可以灵活的调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。

服务端代码:

  1. public static void main(String[] args) throws IOException {
  2. int port = 8080;
  3. if (args != null && args.length > 0) {
  4. try {
  5. port = Integer.valueOf(args[0]);
  6. } catch (NumberFormatException e) {
  7. // 采用默认值
  8. }
  9. }
  10. ServerSocket server = null;
  11. try {
  12. server = new ServerSocket(port);
  13. System.out.println("The time server is start in port : " + port);
  14. Socket socket = null;
  15. TimeServerHandlerExecutePool singleExecutor = new TimeServerHandlerExecutePool(
  16. 50, 10000);// 创建IO任务线程池
  17. while (true) {
  18. socket = server.accept();
  19. singleExecutor.execute(new TimeServerHandler(socket));
  20. }
  21. } finally {
  22. if (server != null) {
  23. System.out.println("The time server close");
  24. server.close();
  25. server = null;
  26. }
  27. }
  28. }

连接池代码:

  1. public class TimeServerHandlerExecutePool {
  2. private ExecutorService executor;
  3. public TimeServerHandlerExecutePool(int maxPoolSize, int queueSize) {
  4. executor = new ThreadPoolExecutor(Runtime.getRuntime()
  5. .availableProcessors(), maxPoolSize, 120L, TimeUnit.SECONDS,
  6. new ArrayBlockingQueue<java.lang.Runnable>(queueSize));
  7. }
  8. public void execute(java.lang.Runnable task) {
  9. executor.execute(task);
  10. }
  11. }

优点:避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。

缺点:但是由于它底层的通信依然采用同步阻塞模型,因此无法从跟本上解决问题。

2.3 NIO编程

NIO为New I/O的简称,也是非阻塞I/O。

INO提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。开发人员可以根据自己需求选择合适的模式。一般来说,低负载,低并发的应用程序可以选择同步阻塞I/O以降低编程复杂度,但是对于高负载,高并发的网络应用,需要使用NIO的非阻塞模式进行开发。

2.3.1 NIO类库解析

1.缓冲区Buffer

最常用的缓冲区为ByteBuffer,提供了一组功能用于操作数组。除此之外,还有CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer。

2.通道Channel

是一个通道,主要通过它读取和写入数据。

Channel主要分为网络读写SelectableChannel和文件操作FileChannel

Netty主要涉及ServerSocketChannel和SocketChannel都是SelectableChannel的子类

3.多路复用器Selector

不断的扫描新的TCP连接接入、读和写事件的Channel,如果有,Channel就会处于就绪状态,被Selector轮询出来,然后通过selectionKey可以获取就绪Channel的集合,进行后续的I/O操作。

2.3.2 NIO源码

1.服务端代码

  1. public static void main(String[] args) throws IOException {
  2. int port = 8080;
  3. if (args != null && args.length > 0) {
  4. try {
  5. port = Integer.valueOf(args[0]);
  6. } catch (NumberFormatException e) {
  7. // 采用默认值
  8. }
  9. }
  10. MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port);
  11. new Thread(timeServer, "NIO-MultiplexerTimeServer-001").start();

2.MultiplexerTimeServer类

  1. public class MultiplexerTimeServer implements Runnable {
  2. private Selector selector;
  3. private ServerSocketChannel servChannel;
  4. private volatile boolean stop;
  5. /**
  6. * 初始化多路复用器、绑定监听端口
  7. *
  8. * @param port
  9. */
  10. public MultiplexerTimeServer(int port) {
  11. try {
  12. selector = Selector.open();
  13. servChannel = ServerSocketChannel.open();
  14. servChannel.configureBlocking(false);
  15. servChannel.socket().bind(new InetSocketAddress(port), 1024);
  16. servChannel.register(selector, SelectionKey.OP_ACCEPT);
  17. System.out.println("The time server is start in port : " + port);
  18. } catch (IOException e) {
  19. e.printStackTrace();
  20. System.exit(1);
  21. }
  22. }
  23. public void stop() {
  24. this.stop = true;
  25. }
  26. /*
  27. * (non-Javadoc)
  28. *
  29. * @see java.lang.Runnable#run()
  30. */
  31. @Override
  32. public void run() {
  33. while (!stop) {
  34. try {
  35. selector.select(1000);
  36. Set<SelectionKey> selectedKeys = selector.selectedKeys();
  37. Iterator<SelectionKey> it = selectedKeys.iterator();
  38. SelectionKey key = null;
  39. while (it.hasNext()) {
  40. key = it.next();
  41. it.remove();
  42. try {
  43. handleInput(key);
  44. } catch (Exception e) {
  45. if (key != null) {
  46. key.cancel();
  47. if (key.channel() != null)
  48. key.channel().close();
  49. }
  50. }
  51. }
  52. } catch (Throwable t) {
  53. t.printStackTrace();
  54. }
  55. }
  56. // 多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
  57. if (selector != null)
  58. try {
  59. selector.close();
  60. } catch (IOException e) {
  61. e.printStackTrace();
  62. }
  63. }
  64. private void handleInput(SelectionKey key) throws IOException {
  65. if (key.isValid()) {
  66. // 处理新接入的请求消息
  67. if (key.isAcceptable()) {
  68. // Accept the new connection
  69. ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
  70. SocketChannel sc = ssc.accept();
  71. sc.configureBlocking(false);
  72. // Add the new connection to the selector
  73. sc.register(selector, SelectionKey.OP_READ);
  74. }
  75. if (key.isReadable()) {
  76. // Read the data
  77. SocketChannel sc = (SocketChannel) key.channel();
  78. ByteBuffer readBuffer = ByteBuffer.allocate(1024);
  79. int readBytes = sc.read(readBuffer);
  80. if (readBytes > 0) {
  81. readBuffer.flip();
  82. byte[] bytes = new byte[readBuffer.remaining()];
  83. readBuffer.get(bytes);
  84. String body = new String(bytes, "UTF-8");
  85. System.out.println("The time server receive order : "
  86. + body);
  87. String currentTime = "QUERY TIME ORDER"
  88. .equalsIgnoreCase(body) ? new java.util.Date(
  89. System.currentTimeMillis()).toString()
  90. : "BAD ORDER";
  91. doWrite(sc, currentTime);
  92. } else if (readBytes < 0) {
  93. // 对端链路关闭
  94. key.cancel();
  95. sc.close();
  96. } else
  97. ; // 读到0字节,忽略
  98. }
  99. }
  100. }
  101. private void doWrite(SocketChannel channel, String response)
  102. throws IOException {
  103. if (response != null && response.trim().length() > 0) {
  104. byte[] bytes = response.getBytes();
  105. ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
  106. writeBuffer.put(bytes);
  107. writeBuffer.flip();
  108. channel.write(writeBuffer);
  109. }
  110. }
  111. }

3.客户端代码

  1. public static void main(String[] args) {
  2. int port = 8080;
  3. if (args != null && args.length > 0) {
  4. try {
  5. port = Integer.valueOf(args[0]);
  6. } catch (NumberFormatException e) {
  7. // 采用默认值
  8. }
  9. }
  10. new Thread(new TimeClientHandle("127.0.0.1", port), "TimeClient-001")
  11. .start();
  12. }

4.TimeClientHandle类

  1. public class TimeClientHandle implements Runnable {
  2. private String host;
  3. private int port;
  4. private Selector selector;
  5. private SocketChannel socketChannel;
  6. private volatile boolean stop;
  7. public TimeClientHandle(String host, int port) {
  8. this.host = host == null ? "127.0.0.1" : host;
  9. this.port = port;
  10. try {
  11. selector = Selector.open();
  12. socketChannel = SocketChannel.open();
  13. socketChannel.configureBlocking(false);
  14. } catch (IOException e) {
  15. e.printStackTrace();
  16. System.exit(1);
  17. }
  18. }
  19. /*
  20. * (non-Javadoc)
  21. *
  22. * @see java.lang.Runnable#run()
  23. */
  24. @Override
  25. public void run() {
  26. try {
  27. doConnect();
  28. } catch (IOException e) {
  29. e.printStackTrace();
  30. System.exit(1);
  31. }
  32. while (!stop) {
  33. try {
  34. selector.select(1000);
  35. Set<SelectionKey> selectedKeys = selector.selectedKeys();
  36. Iterator<SelectionKey> it = selectedKeys.iterator();
  37. SelectionKey key = null;
  38. while (it.hasNext()) {
  39. key = it.next();
  40. it.remove();
  41. try {
  42. handleInput(key);
  43. } catch (Exception e) {
  44. if (key != null) {
  45. key.cancel();
  46. if (key.channel() != null)
  47. key.channel().close();
  48. }
  49. }
  50. }
  51. } catch (Exception e) {
  52. e.printStackTrace();
  53. System.exit(1);
  54. }
  55. }
  56. // 多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
  57. if (selector != null)
  58. try {
  59. selector.close();
  60. } catch (IOException e) {
  61. e.printStackTrace();
  62. }
  63. }
  64. private void handleInput(SelectionKey key) throws IOException {
  65. if (key.isValid()) {
  66. // 判断是否连接成功
  67. SocketChannel sc = (SocketChannel) key.channel();
  68. if (key.isConnectable()) {
  69. if (sc.finishConnect()) {
  70. sc.register(selector, SelectionKey.OP_READ);
  71. doWrite(sc);
  72. } else
  73. System.exit(1);// 连接失败,进程退出
  74. }
  75. if (key.isReadable()) {
  76. ByteBuffer readBuffer = ByteBuffer.allocate(1024);
  77. int readBytes = sc.read(readBuffer);
  78. if (readBytes > 0) {
  79. readBuffer.flip();
  80. byte[] bytes = new byte[readBuffer.remaining()];
  81. readBuffer.get(bytes);
  82. String body = new String(bytes, "UTF-8");
  83. System.out.println("Now is : " + body);
  84. this.stop = true;
  85. } else if (readBytes < 0) {
  86. // 对端链路关闭
  87. key.cancel();
  88. sc.close();
  89. } else
  90. ; // 读到0字节,忽略
  91. }
  92. }
  93. }
  94. private void doConnect() throws IOException {
  95. // 如果直接连接成功,则注册到多路复用器上,发送请求消息,读应答
  96. if (socketChannel.connect(new InetSocketAddress(host, port))) {
  97. socketChannel.register(selector, SelectionKey.OP_READ);
  98. doWrite(socketChannel);
  99. } else
  100. socketChannel.register(selector, SelectionKey.OP_CONNECT);
  101. }
  102. private void doWrite(SocketChannel sc) throws IOException {
  103. byte[] req = "QUERY TIME ORDER".getBytes();
  104. ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
  105. writeBuffer.put(req);
  106. writeBuffer.flip();
  107. sc.write(writeBuffer);
  108. if (!writeBuffer.hasRemaining())
  109. System.out.println("Send order 2 server succeed.");
  110. }
  111. }

2.4 AIO编程

JDK7的产物,即NIO2.0。

1.服务端代码

  1. public class TimeServer {
  2. /**
  3. * @param args
  4. * @throws IOException
  5. */
  6. public static void main(String[] args) throws IOException {
  7. int port = 8080;
  8. if (args != null && args.length > 0) {
  9. try {
  10. port = Integer.valueOf(args[0]);
  11. } catch (NumberFormatException e) {
  12. // 采用默认值
  13. }
  14. }
  15. AsyncTimeServerHandler timeServer = new AsyncTimeServerHandler(port);
  16. new Thread(timeServer, "AIO-AsyncTimeServerHandler-001").start();
  17. }
  18. }

2.AsyncTimeServerHandler类

  1. public class AsyncTimeServerHandler implements Runnable {
  2. private int port;
  3. CountDownLatch latch;
  4. AsynchronousServerSocketChannel asynchronousServerSocketChannel;
  5. public AsyncTimeServerHandler(int port) {
  6. this.port = port;
  7. try {
  8. asynchronousServerSocketChannel = AsynchronousServerSocketChannel
  9. .open();
  10. asynchronousServerSocketChannel.bind(new InetSocketAddress(port));
  11. System.out.println("The time server is start in port : " + port);
  12. } catch (IOException e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. /*
  17. * (non-Javadoc)
  18. *
  19. * @see java.lang.Runnable#run()
  20. */
  21. @Override
  22. public void run() {
  23. latch = new CountDownLatch(1);
  24. doAccept();
  25. try {
  26. latch.await();
  27. } catch (InterruptedException e) {
  28. e.printStackTrace();
  29. }
  30. }
  31. public void doAccept() {
  32. asynchronousServerSocketChannel.accept(this,
  33. new AcceptCompletionHandler());
  34. }
  35. }

3.AcceptCompletionHandler类

  1. public class AcceptCompletionHandler implements
  2. CompletionHandler<AsynchronousSocketChannel, AsyncTimeServerHandler> {
  3. @Override
  4. public void completed(AsynchronousSocketChannel result,
  5. AsyncTimeServerHandler attachment) {
  6. attachment.asynchronousServerSocketChannel.accept(attachment, this);
  7. ByteBuffer buffer = ByteBuffer.allocate(1024);
  8. result.read(buffer, buffer, new ReadCompletionHandler(result));
  9. }
  10. @Override
  11. public void failed(Throwable exc, AsyncTimeServerHandler attachment) {
  12. exc.printStackTrace();
  13. attachment.latch.countDown();
  14. }
  15. }

4.ReadCompletionHandler类

  1. public class ReadCompletionHandler implements
  2. CompletionHandler<Integer, ByteBuffer> {
  3. private AsynchronousSocketChannel channel;
  4. public ReadCompletionHandler(AsynchronousSocketChannel channel) {
  5. if (this.channel == null)
  6. this.channel = channel;
  7. }
  8. @Override
  9. public void completed(Integer result, ByteBuffer attachment) {
  10. attachment.flip();
  11. byte[] body = new byte[attachment.remaining()];
  12. attachment.get(body);
  13. try {
  14. String req = new String(body, "UTF-8");
  15. System.out.println("The time server receive order : " + req);
  16. String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(req) ? new java.util.Date(
  17. System.currentTimeMillis()).toString() : "BAD ORDER";
  18. doWrite(currentTime);
  19. } catch (UnsupportedEncodingException e) {
  20. e.printStackTrace();
  21. }
  22. }
  23. private void doWrite(String currentTime) {
  24. if (currentTime != null && currentTime.trim().length() > 0) {
  25. byte[] bytes = (currentTime).getBytes();
  26. ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
  27. writeBuffer.put(bytes);
  28. writeBuffer.flip();
  29. channel.write(writeBuffer, writeBuffer,
  30. new CompletionHandler<Integer, ByteBuffer>() {
  31. @Override
  32. public void completed(Integer result, ByteBuffer buffer) {
  33. // 如果没有发送完成,继续发送
  34. if (buffer.hasRemaining())
  35. channel.write(buffer, buffer, this);
  36. }
  37. @Override
  38. public void failed(Throwable exc, ByteBuffer attachment) {
  39. try {
  40. channel.close();
  41. } catch (IOException e) {
  42. // ingnore on close
  43. }
  44. }
  45. });
  46. }
  47. }
  48. @Override
  49. public void failed(Throwable exc, ByteBuffer attachment) {
  50. try {
  51. this.channel.close();
  52. } catch (IOException e) {
  53. e.printStackTrace();
  54. }
  55. }
  56. }

5.客户端代码

  1. public class TimeClient {
  2. /**
  3. * @param args
  4. */
  5. public static void main(String[] args) {
  6. int port = 8080;
  7. if (args != null && args.length > 0) {
  8. try {
  9. port = Integer.valueOf(args[0]);
  10. } catch (NumberFormatException e) {
  11. // 采用默认值
  12. }
  13. }
  14. new Thread(new AsyncTimeClientHandler("127.0.0.1", port),
  15. "AIO-AsyncTimeClientHandler-001").start();
  16. }
  17. }

6.AsyncTimeClientHandler

  1. public class AsyncTimeClientHandler implements
  2. CompletionHandler<Void, AsyncTimeClientHandler>, Runnable {
  3. private AsynchronousSocketChannel client;
  4. private String host;
  5. private int port;
  6. private CountDownLatch latch;
  7. public AsyncTimeClientHandler(String host, int port) {
  8. this.host = host;
  9. this.port = port;
  10. try {
  11. client = AsynchronousSocketChannel.open();
  12. } catch (IOException e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. @Override
  17. public void run() {
  18. latch = new CountDownLatch(1);
  19. client.connect(new InetSocketAddress(host, port), this, this);
  20. try {
  21. latch.await();
  22. } catch (InterruptedException e1) {
  23. e1.printStackTrace();
  24. }
  25. try {
  26. client.close();
  27. } catch (IOException e) {
  28. e.printStackTrace();
  29. }
  30. }
  31. @Override
  32. public void completed(Void result, AsyncTimeClientHandler attachment) {
  33. byte[] req = "QUERY TIME ORDER".getBytes();
  34. ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
  35. writeBuffer.put(req);
  36. writeBuffer.flip();
  37. client.write(writeBuffer, writeBuffer,
  38. new CompletionHandler<Integer, ByteBuffer>() {
  39. @Override
  40. public void completed(Integer result, ByteBuffer buffer) {
  41. if (buffer.hasRemaining()) {
  42. client.write(buffer, buffer, this);
  43. } else {
  44. ByteBuffer readBuffer = ByteBuffer.allocate(1024);
  45. client.read(
  46. readBuffer,
  47. readBuffer,
  48. new CompletionHandler<Integer, ByteBuffer>() {
  49. @Override
  50. public void completed(Integer result,
  51. ByteBuffer buffer) {
  52. buffer.flip();
  53. byte[] bytes = new byte[buffer
  54. .remaining()];
  55. buffer.get(bytes);
  56. String body;
  57. try {
  58. body = new String(bytes,
  59. "UTF-8");
  60. System.out.println("Now is : "
  61. + body);
  62. latch.countDown();
  63. } catch (UnsupportedEncodingException e) {
  64. e.printStackTrace();
  65. }
  66. }
  67. @Override
  68. public void failed(Throwable exc,
  69. ByteBuffer attachment) {
  70. try {
  71. client.close();
  72. latch.countDown();
  73. } catch (IOException e) {
  74. // ingnore on close
  75. }
  76. }
  77. });
  78. }
  79. }
  80. @Override
  81. public void failed(Throwable exc, ByteBuffer attachment) {
  82. try {
  83. client.close();
  84. latch.countDown();
  85. } catch (IOException e) {
  86. // ingnore on close
  87. }
  88. }
  89. });
  90. }
  91. @Override
  92. public void failed(Throwable exc, AsyncTimeClientHandler attachment) {
  93. exc.printStackTrace();
  94. try {
  95. client.close();
  96. latch.countDown();
  97. } catch (IOException e) {
  98. e.printStackTrace();
  99. }
  100. }
  101. }

2.5  4中I/O对比

Netty权威指南(笔记一)

上一篇:Live555 实战之框架简单介绍


下一篇:S2_OOP第二章