第 17 / 20 篇

附录 A 完整工程代码

本附录内容由随书工程源文件直接生成,避免电子书与工程发生手工抄写差异。请按标出的路径保存文件,并保留 package book;。项目根目录是 examples,所有 Maven 命令都在该目录执行。

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion><groupId>book</groupId><artifactId>netty-beginner</artifactId><version>1.0.0</version>
<properties><maven.compiler.release>17</maven.compiler.release><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><netty.version>4.1.139.Final</netty.version></properties>
<dependencies><dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>${netty.version}</version></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.11.4</version><scope>test</scope></dependency></dependencies>
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.13.0</version></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.2</version></plugin><plugin><groupId>org.codehaus.mojo</groupId><artifactId>exec-maven-plugin</artifactId><version>3.5.0</version></plugin></plugins></build></project>

src/main/java/book/LineServer.java

package book;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;

public final class LineServer {
    public static void main(String[] args) throws InterruptedException {
        int port = args.length == 0 ? 8080 : Integer.parseInt(args[0]);
        EventLoopGroup boss = new NioEventLoopGroup(1);
        EventLoopGroup worker = new NioEventLoopGroup();
        try {
            Channel server = new ServerBootstrap()
                .group(boss, worker)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override protected void initChannel(SocketChannel ch) {
                        ch.pipeline().addLast(
                            new LineBasedFrameDecoder(1024),
                            new StringDecoder(CharsetUtil.UTF_8),
                            new StringEncoder(CharsetUtil.UTF_8),
                            new ReplyHandler());
                    }
                }).bind("127.0.0.1", port).sync().channel();
            System.out.println("LISTENING " + port);
            server.closeFuture().sync();
        } finally {
            boss.shutdownGracefully().sync();
            worker.shutdownGracefully().sync();
        }
    }

    static final class ReplyHandler extends SimpleChannelInboundHandler<String> {
        @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) {
            if ("quit".equals(msg)) {
                ctx.writeAndFlush("再见\n").addListener(ChannelFutureListener.CLOSE);
            } else {
                ctx.writeAndFlush("收到:" + msg + "\n");
            }
        }
        @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            cause.printStackTrace();
            ctx.close();
        }
    }
}

src/main/java/book/LineClient.java

package book;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;

public final class LineClient {
    public static void main(String[] args) throws InterruptedException {
        int port = args.length == 0 ? 8080 : Integer.parseInt(args[0]);
        EventLoopGroup group = new NioEventLoopGroup(1);
        try {
            Channel ch = new Bootstrap().group(group).channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override protected void initChannel(SocketChannel c) {
                        c.pipeline().addLast(new LineBasedFrameDecoder(2048),
                            new StringDecoder(CharsetUtil.UTF_8),
                            new StringEncoder(CharsetUtil.UTF_8),
                            new SimpleChannelInboundHandler<String>() {
                                @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) {
                                    System.out.println(msg);
                                }
                                @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
                                    cause.printStackTrace(); ctx.close();
                                }
                            });
                    }
                }).connect("127.0.0.1", port).sync().channel();
            ch.writeAndFlush("你好,Netty\nquit\n").sync();
            ch.closeFuture().sync();
        } finally {
            group.shutdownGracefully().sync();
        }
    }
}

src/test/java/book/ProtocolTest.java

package book;
import io.netty.buffer.*;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.*;
import io.netty.handler.codec.string.*;
import io.netty.util.CharsetUtil;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class ProtocolTest {
    private EmbeddedChannel channel() {
        return new EmbeddedChannel(new LineBasedFrameDecoder(1024),
            new StringDecoder(CharsetUtil.UTF_8), new StringEncoder(CharsetUtil.UTF_8),
            new LineServer.ReplyHandler());
    }
    private String take(EmbeddedChannel ch) {
        ByteBuf b = ch.readOutbound();
        assertNotNull(b);
        try { return b.toString(CharsetUtil.UTF_8); } finally { b.release(); }
    }
    @Test void splitAndCombinedMessages() {
        EmbeddedChannel ch = channel();
        try {
            ch.writeInbound(Unpooled.copiedBuffer("he", CharsetUtil.UTF_8));
            assertNull(ch.readOutbound());
            ch.writeInbound(Unpooled.copiedBuffer("llo\nworld\n", CharsetUtil.UTF_8));
            assertEquals("收到:hello\n", take(ch));
            assertEquals("收到:world\n", take(ch));
        } finally { ch.finishAndReleaseAll(); }
    }
    @Test void utf8MaySplitInsideCharacter() {
        EmbeddedChannel ch = channel();
        byte[] bytes = "你\n".getBytes(CharsetUtil.UTF_8);
        try {
            ch.writeInbound(Unpooled.wrappedBuffer(new byte[]{bytes[0]}));
            assertNull(ch.readOutbound());
            ch.writeInbound(Unpooled.wrappedBuffer(java.util.Arrays.copyOfRange(bytes,1,bytes.length)));
            assertEquals("收到:你\n",take(ch));
        } finally { ch.finishAndReleaseAll(); }
    }
    @Test void quitFlushesThenCloses() {
        EmbeddedChannel ch = channel();
        try {
            ch.writeInbound(Unpooled.copiedBuffer("quit\n", CharsetUtil.UTF_8));
            assertEquals("再见\n",take(ch));
            assertFalse(ch.isOpen());
        } finally { ch.finishAndReleaseAll(); }
    }
    @Test void oversizedLineRejected() {
        EmbeddedChannel ch = new EmbeddedChannel(new LineBasedFrameDecoder(8, true, true));
        try {
            assertThrows(TooLongFrameException.class, () -> ch.writeInbound(
                Unpooled.copiedBuffer("123456789",CharsetUtil.UTF_8)));
        } finally { ch.finishAndReleaseAll(); }
    }
    @Test void lengthFieldSurvivesFragmentation() {
        EmbeddedChannel ch = new EmbeddedChannel(new LengthFieldBasedFrameDecoder(1024,0,4,0,4));
        try {
            ByteBuf first = Unpooled.buffer().writeInt(3).writeByte(1);
            assertFalse(ch.writeInbound(first));
            assertTrue(ch.writeInbound(Unpooled.wrappedBuffer(new byte[]{2,3})));
            ByteBuf result = ch.readInbound();
            try { assertEquals(3,result.readableBytes());assertEquals(1,result.readByte());assertEquals(2,result.readByte());assertEquals(3,result.readByte()); }
            finally { result.release(); }
        } finally { ch.finishAndReleaseAll(); }
    }
}