5.9.2 TCP的连接释放

  • 若通信完不释放连接,连接就会一直存在,占用资源。

  • TCP的 连接释放 是一个 四报文握手

  • 传输结束后,通信双方都可以释放连接。
    现在A和B都处于 ESTABLISHED状态 ,如下图。
    在这里插入图片描述
    A的应用进程 先向 其TCP 发出 连接释放报文段 ,并停止发送数据,主动关闭TCP连接。① A把 连接释放报文段 首部的 终止控制位FIN 置1,其序号seq=u,等于前面已传送过的数据的最后一个字节的序号加1。此时A进入了 FIN-WAIT-1(终止等待1)状态 ,等待B的确认。(注意FIN报文段即使不携带数据,也会消耗掉一个序号)。
    ② B收到 连接释放报文段 后就发回 确认 ,确认号是ack=u+1,而这个报文段自己的序号是v,等于B前面已传送过的数据的最后一个字节的序号加1。然后B就进入 CLOSE-WAIT(关闭等待)状态TCP服务器进程 此时通知 高层应用进程 ,此时 从A到B的这个方向的连接 就释放了,TCP连接处于 半关闭状态 :A无数据要发送,若B有数据要发送,A仍要接收即B到A这个方向上的连接未关闭,此状态可能要持续一段时间。
    A收到来自B的确认之后,进入 FIN-WAIT-2(终止等待2)状态 ,等待B发出的 连接释放报文段
    ③ 若B已经没有要向A发送的数据,其应用进程就通知TCP释放连接。此时B发出的 连接释放报文段 必须使FIN=1。先假定B的序号为w(在半关闭状态 B可能又发送了一些数据)。B还必须重复上次已经发送过的确认号ack=u+1。这时B就进入了 LAST-ACK(最后确认)状态 ,等待A的确认。
    ④ A在收到B的 连接释放报文段 后,必须对此发出确认。在确认报文段中把ACK置1,确认号ack=w+1,而自己的序号是seq=u+1(TCP标准规定前面发送过的SYN、FIN报文段需要消耗掉一个序号,确认报文段不消耗序号。)。然后进入到 TIME-WAIT(时间等待)状态
    此时TCP连接还没有释放掉。必须经过 时间等待计数器 设置的时间到了,A才进入 CLOSED状态 ,才能开始建立下一个新的连接。

  • TIME-WAIT状态存在的必要性
    1、为了保证 A发送的最后一个ACK报文段 能够到达B。这个ACK报文段可能丢失,B收不到对 自己向A发送的FIN+ACK报文段 的确认(B无法进入 CLOSED状态 ),B就会超时重传这个FIN+ACK报文段,而A在此延时状态下便能收到这个报文段,A也重传一次确认,重新启动时间等待计时器。
    2、防止 已失效的连接请求报文段 出现在本连接中。A发送完最后一个ACK报文段后,等待给定的时间。可以使本连接持续的时间内所产生的的所有报文段都从网络中消失。

  • B只要收到了A发出的确认,就进入了 CLOSED状态
    B此时撤销相应的传输控制块,结束本次TCP连接。
    B结束TCP连接的时间 要比 A 早一些。

  • 保活计时器
    客户已主动与服务器建立了TCP连接,后来客户端的主机突然出故障。
    服务器以后就不能再收到客户发来的数据,需要有措施使服务器不要再白白等待下去。即采用保活计时器。
    保活计时器的时间 通常设置为两小时,若两小时没收到客户的数据,服务器就发送一个 探测报文段 ,以后每隔75秒钟发送一次。
    若一连发送10个探测报文段后仍无客户的响应,服务器就认为客户端出了故障,接着就关闭这个连接。

public final class TcpClient { private static final Logger LOG = LoggerFactory.getLogger(TcpClient.class); private static final int DEFAULT_TIMEOUT_SECONDS = 5; // 默认响应超时时间(秒) private static final String DEFAULT_HOST = "127.0.0.1"; // 默认服务器地址 private static final int DEFAULT_PORT = 8080; // 默认服务器端口 private final String host; // 目标服务器地址 private final int port; // 目标服务器端口 private Channel channel; // 客户端TCP通道 private EventLoopGroup group; // 客户端事件循环组 /** * 使用默认地址(127.0.0.1:8080)初始化客户端 */ public TcpClient() { this(DEFAULT_HOST, DEFAULT_PORT); } /** * 指定服务器地址和端口初始化客户端 * * @param host 服务器IP地址(非空) * @param port 服务器端口(1-65535) */ public TcpClient(String host, int port) { this.host = host; this.port = port; } /** * 连接服务器(阻塞直到连接成功或失败) * * @throws InterruptedException 线程中断异常(连接过程被中断) */ public void connect() throws InterruptedException { group = new NioEventLoopGroup(); Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { // 客户端编解码器需与服务器严格对称(处理接收和发送的消息) ch.pipeline().addLast( // 接收方向:解析长度头 → 解码JSON → GreetResponse new LengthFieldFrameDecoder(), // 解析4字节长度头(服务器响应) new JsonResponseDecoder(), // JSON字节 → GreetResponse对象 new LengthFieldFrameEncoder(), // 为JSON字节添加4字节长度头 new JsonRequestEncoder() // GreetRequest对象 → JSON字节 ); } }); ChannelFuture connectFuture = b.connect(host, port).sync(); if (connectFuture.isSuccess()) { this.channel = connectFuture.channel(); LOG.info("客户端连接成功,目标地址:{}:{}", host, port); } else { LOG.error("客户端连接失败,目标地址:{}:{}", host, port); throw new IllegalStateException("连接服务器失败"); } } /** * 发送greet请求并同步等待响应(带超时机制) * * @param request 要发送的GreetRequest对象(非空) * @return 服务器返回的GreetResponse对象(超时或失败返回null) * @throws InterruptedException 线程中断异常(等待过程被中断) */ public GreetResponse sendRequest(GreetRequest request) throws InterruptedException { if (channel == null || !channel.isActive()) { LOG.error("无法发送请求:通道未激活或未连接"); return null; } CountDownLatch responseLatch = new CountDownLatch(1); TcpClientHandler responseHandler = new TcpClientHandler(responseLatch); // 动态添加临时处理器(避免多个请求响应交叉) channel.pipeline().addLast(responseHandler); try { // 发送请求(自动触发编码流程:GreetRequest → JSON → 长度头) channel.writeAndFlush(request).sync(); LOG.debug("已发送greet请求,name={}", request.getName()); // 等待响应(默认超时5秒) if (responseLatch.await(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { return responseHandler.getResponse(); } else { LOG.warn("等待响应超时,name={}", request.getName()); return null; } } finally { // 移除临时处理器 channel.pipeline().remove(responseHandler); } } /** * 关闭客户端资源(释放线程组和通道) */ public void shutdown() { if (channel != null) { channel.close().syncUninterruptibly(); } if (group != null) { group.shutdownGracefully(); } LOG.info("客户端已关闭"); } /** * 主方法:客户端使用流程 * * @param args 命令行参数(未使用) * @throws InterruptedException 线程中断异常 */ public static void main(String[] args) throws InterruptedException { TcpClient client = new TcpClient(); try { client.connect(); GreetRequest request = new GreetRequest(); request.setName("Netty User"); GreetResponse response = client.sendRequest(request); if (response != null) { LOG.info("收到服务器响应:{}", response.getMessage()); } else { LOG.error("未收到有效响应"); } } finally { client.shutdown(); } } }public final class TcpClientHandler extends SimpleChannelInboundHandler<GreetResponse> { private static final Logger LOG = LoggerFactory.getLogger(TcpClientHandler.class); private final CountDownLatch responseLatch; // 同步锁(等待响应) private GreetResponse response; // 存储服务器响应 public TcpClientHandler(CountDownLatch responseLatch) { this.responseLatch = responseLatch; } @Override protected void channelRead0(ChannelHandlerContext ctx, GreetResponse response) { this.response = response; LOG.debug("收到服务器响应: message={}", response.getMessage()); responseLatch.countDown(); // 响应到达后释放锁 } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { LOG.error("客户端通道异常: {}", cause.getMessage()); ctx.close(); } /** * 获取服务器返回的响应对象 * @return GreetResponse对象(可能为null) */ public GreetResponse getResponse() { return response; } }public class JsonRequestDecoder extends ByteToMessageDecoder { private static final Logger LOG = LoggerFactory.getLogger(JsonRequestDecoder.class); private final ObjectMapper objectMapper = new ObjectMapper(); @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { try { // 将ByteBuf转换为字节数组 byte[] bytes = new byte[in.readableBytes()]; in.readBytes(bytes); // 反序列化为GreetRequest对象 GreetRequest request = objectMapper.readValue(bytes, GreetRequest.class); out.add(request); LOG.debug("解码请求:{}", request.getName()); } catch (Exception e) { LOG.error("JSON解码失败", e); ctx.close(); // 解码失败关闭连接 } } }public class JsonRequestEncoder extends MessageToByteEncoder<GreetRequest> { private final ObjectMapper objectMapper = new ObjectMapper(); @Override protected void encode(ChannelHandlerContext ctx, GreetRequest request, ByteBuf out) throws Exception { // 将GreetRequest对象序列化为JSON字节数组 byte[] jsonBytes = objectMapper.writeValueAsBytes(request); // 将字节数组写入Netty的ByteBuf(必须使用out.writeBytes()) out.writeBytes(jsonBytes); } }public class JsonResponseDecoder extends ByteToMessageDecoder { private final ObjectMapper objectMapper = new ObjectMapper(); @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { // 将ByteBuf中的字节转换为JSON字符串 byte[] jsonBytes = new byte[in.readableBytes()]; in.readBytes(jsonBytes); // 将JSON字符串反序列化为GreetResponse对象 GreetResponse response = objectMapper.readValue(jsonBytes, GreetResponse.class); out.add(response); } }public class JsonResponseEncoder extends MessageToByteEncoder<GreetResponse> { private static final Logger LOG = LoggerFactory.getLogger(JsonResponseEncoder.class); private final ObjectMapper objectMapper = new ObjectMapper(); @Override protected void encode(ChannelHandlerContext ctx, GreetResponse response, ByteBuf out) { try { // 序列化为JSON字节数组 byte[] bytes = objectMapper.writeValueAsBytes(response); out.writeBytes(bytes); LOG.debug("编码响应:{}", response.getMessage()); } catch (Exception e) { LOG.error("JSON编码失败", e); ctx.close(); // 编码失败关闭连接 } } }public class LengthFieldFrameDecoder extends LengthFieldBasedFrameDecoder { private static final int MAX_FRAME_LENGTH = 1024 * 1024; // 1MB private static final int LENGTH_FIELD_OFFSET = 0; // 长度字段偏移量 private static final int LENGTH_FIELD_LENGTH = 4; // 长度字段占4字节 private static final int LENGTH_ADJUSTMENT = 0; // 长度字段不包含自身 private static final int INITIAL_BYTES_TO_STRIP = 4; // 剥离前4字节长度头 public LengthFieldFrameDecoder() { super(MAX_FRAME_LENGTH, LENGTH_FIELD_OFFSET, LENGTH_FIELD_LENGTH, LENGTH_ADJUSTMENT, INITIAL_BYTES_TO_STRIP); } @Override protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { // 调用父类方法完成帧解码 return super.decode(ctx, in); } }public class LengthFieldFrameEncoder extends LengthFieldPrepender { public LengthFieldFrameEncoder() { super(4); // 长度字段占4字节 } }public class GreetRequest { private String name; /** * 获取请求中的名称 * @return 名称字符串 */ public String getName() { return name; } /** * 设置请求中的名称 * @param name 名称字符串 */ public void setName(String name) { this.name = name; } }public class GreetResponse { private String message; /** * 获取响应消息内容 * @return 消息字符串 */ public String getMessage() { return message; } /** * 设置响应消息内容 * @param message 消息字符串 */ public void setMessage(String message) { this.message = message; } }public class GreetServerHandler extends SimpleChannelInboundHandler<GreetRequest> { private static final Logger LOG = LoggerFactory.getLogger(GreetServerHandler.class); @Override protected void channelRead0(ChannelHandlerContext ctx, GreetRequest request) { LOG.info("收到greet请求,name={}", request.getName()); // 生成响应 GreetResponse response = new GreetResponse(); response.setMessage("Hello, " + request.getName() + "!"); ctx.writeAndFlush(response); // 自动触发编码器添加长度头 } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { LOG.error("服务器通道异常: {}", cause.getMessage()); ctx.close(); } }public class TcpServer { private static final Logger LOG = LoggerFactory.getLogger(TcpServer.class); private static final int SERVER_PORT = 8080; // 服务器端口(常量大写) private final EventLoopGroup bossGroup; // Boss线程组(接收连接) private final EventLoopGroup workerGroup; // Worker线程组(处理IO) public TcpServer() { this.bossGroup = new NioEventLoopGroup(1); // 单线程Boss组 this.workerGroup = new NioEventLoopGroup(); // 默认线程数(CPU核心数×2) } /** * 启动TCP服务器 * @throws InterruptedException 线程中断异常 */ public void start() throws InterruptedException { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 128) // 连接队列大小 .childOption(ChannelOption.SO_KEEPALIVE, true) // 保持长连接 .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { // 按顺序添加编解码器和业务处理器 ch.pipeline().addLast( new LengthFieldFrameDecoder(), // 解析长度头+拆包 new JsonRequestDecoder(), // JSON → GreetRequest new LengthFieldFrameEncoder(), // 添加长度头 new JsonResponseEncoder(), // GreetResponse → JSON new GreetServerHandler() // 业务处理 ); } }); ChannelFuture f = b.bind(SERVER_PORT).sync(); LOG.info("TCP服务器启动,监听端口:{}", SERVER_PORT); f.channel().closeFuture().sync(); // 阻塞等待服务器关闭 } /** * 关闭服务器资源 */ public void shutdown() { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); LOG.info("服务器已关闭"); } public static void main(String[] args) throws InterruptedException { TcpServer server = new TcpServer(); try { server.start(); } finally { server.shutdown(); } } }请针对这个简易的tcp服务程序写出单元测试,并在pom引入合适版本的依赖
最新发布
08-23
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值