Spring Boot 从Socket 到Netty网络编程(上):SOCKET 基本开发(BIO)与改进(NIO)

前言

无论是软件还是硬件的本质都是要解决IO问题(输入、输出),再说回网络编程本质上都是基于TCP/UP的开发,socket是在此基础上做的扩展与封装,而Netty又是对socket做的封装。本文旨在通过相关案例对socket进行探讨。

一、基本知识

交互方式

  • 连接三次握手:1.客户-服务 (请求)2.服务-客户(同意)3.客户-服务(连接)
  • 断开四次握手:1.客户-服务(请求断开)2.服务-客户(接受请求)3.服务-客户(断开) 4.客户-服务(断开完成)

Java类

        ServerSocket  服务类

        accept(()  开启连接

        close() 停止服务

        Socket 客户端类

        getInputStream()  输入内存流(接收)

        getOutputStream()  输出内存流(发布)

二、基于线程阻塞式socket(BIO)开发示例

实现

Server代码


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class SockerServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket= new ServerSocket(1200);
        while (true){
            Socket socket = serverSocket.accept();
            System.out.println("有新的客户端连接了:"+socket.getInetAddress());
            new Thread(new ClientHandler(socket)).start();
        }
    }
}

class ClientHandler implements Runnable {
    private Socket socket;

    public ClientHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            BufferedReader inStreamReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter outStreamWriter = new PrintWriter(socket.getOutputStream(), true);
            outStreamWriter.println("请输入内容:");
            String message;
            while ((message = inStreamReader.readLine()) != null) {
                System.out.println("收到客户端消息: " + message);
                if ("bye".equalsIgnoreCase(message)) {
                    outStreamWriter.println("服务器:连接已关闭,再见!");
                    break; // 结束连接
                }

                // 回显客户端消息
                outStreamWriter.println("服务器回显:" + message);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

Client 代码



import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class SocketClient {
    public static void main(String[] args) throws Exception {
        Socket socket = new Socket("127.0.0.1",1200);
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
        BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
        System.out.println("已连接到服务器!");
        System.out.println(in.readLine()); // 读取服务器欢迎消息

        String userInput;
        System.out.println("请输入消息(输入 exit 断开连接):");

        while ((userInput = console.readLine()) != null) {
            out.println(userInput); // 发送消息给服务器
            String serverResponse = in.readLine(); // 接收服务器响应
            System.out.println(serverResponse);

            if ("exit".equalsIgnoreCase(userInput)) {
                System.out.println("连接已关闭。");
                break;
            }
        }
    }
}

效果

缺点

虽然这个已经实现通信,由于它是基于线程控制通信的,换言之每个客户端连接后都会创建一个线程,客户端数量与线程增长成正比就意味着会吃更多的内存,这显然是不合理的。(如果你足够细心会有一些程序要隔一段时间需要重新启动一下否则会卡死,这或许是设计之初犯类似的错误),这就是BIO的致命缺点;

三、基于单线程socket(NIO)

前言

基于上面的问题,我们通过改造实现非隔断性Socket实现,而非多线程方式;这可以极大的提高交互效率与并发问题。

实现

服务端


import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NioServer {

    public static void main(String[] args) throws IOException {
        // 打开服务端Socket通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 绑定端口1200
        serverSocketChannel.socket().bind(new java.net.InetSocketAddress(1200));
        // 设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        // 创建Selector选择器
        Selector selector = Selector.open();
        // 将ServerSocketChannel注册到Selector,监听连接事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("系统运行中.......");
        while (true){
            // 阻塞直到有事件发生
            selector.select();
            // 获取所有发生的事件键
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()){
                SelectionKey key = iterator.next();
                if (key.isAcceptable()){ // 如果是客户端连接事件
                    // 处理客户端连接
                    ServerSocketChannel server = (ServerSocketChannel) key.channel(); // 获取触发事件的通道
                    SocketChannel sc = server.accept(); // 接受客户端连接
                    sc.configureBlocking(false); // 设置为非阻塞模式
                    sc.register(selector, SelectionKey.OP_READ); // 注册读取事件
                    System.out.println("有新的客户端连接了:"+sc.getRemoteAddress());
                }else if (key.isReadable()){ // 如果是客户端读取事件
                    // 处理客户端读取请求
                    SocketChannel sc = (SocketChannel) key.channel(); // 获取触发事件的通道
                    ByteBuffer buffer = ByteBuffer.allocate(1024); // 创建缓冲区
                    int len = sc.read(buffer); // 读取数据
                    if (len > 0){ // 如果有数据
                        String msg = new String(buffer.array(), 0, len); // 解析消息
                        System.out.println("收到客户端"+sc.getRemoteAddress()+"的消息:"+msg);
                        // 将收到的消息回传给客户端
                        ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes()); // 包装响应数据
                        sc.write(outBuffer); // 发送响应
                    }else if (len == -1){ // 如果客户端断开连接
                        System.out.println("客户端"+sc.getRemoteAddress()+"断开了连接");
                        sc.close(); // 关闭通道
                    }
                }
                iterator.remove(); // 移除当前事件键
            }
        }
    }
}

客户端

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;

public class NioClient {
    public static void main(String[] args) throws Exception
    {
        // 打开SocketChannel并连接到服务器
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost", 1200));
        
        // 创建控制台输入流,用于读取用户输入
        BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
        System.out.println("已连接到服务器!");

        // 初始化PrintWriter对象,用于向服务器发送数据
        PrintWriter writer = new PrintWriter(socketChannel.socket().getOutputStream(), true);
        String message;
        
        // 循环读取用户输入并发送给服务器
        while ((message = console.readLine()) != null) {
            if ("exit".equalsIgnoreCase(message)) {
                System.out.println("即将退出连接...");
                break;
            }
            
            // 向服务器发送消息
            writer.println(message);

            // 读取服务器返回的响应数据
            BufferedReader serverResponse = new BufferedReader(new InputStreamReader(socketChannel.socket().getInputStream()));
            String response;
            
            // 打印服务器返回的每一行数据
            while ((response = serverResponse.readLine()) != null) {
                System.out.println("服务器响应: " + response);
            }
        }
        
        // 关闭SocketChannel连接
        socketChannel.close();
        System.out.println("客户端已退出");
    }
}

效果

缺点

综上所述,NIO虽好,上面的代码仅做到了实现,但是进行性能调优、异常处理等等需要更写更多的代码;作为程序员目的是用好轮子而非造轮子,那么Netty这个网络通信工具以它的高性能、高并发、易配置的特点脱颖而出! 下篇我将进一步进行介绍。

下一篇《Spring Boot 从Socket 到Netty网络编程(下):Netty基本开发与改进【心跳、粘包与拆包、闲置连接】

### Spring Boot 中基于线程池的 Server Socket NIOBIO 实现 在 Spring Boot 应用程序中,可以通过配置线程池来管理服务器端套接字(Server Socket)的操作模式。以下是关于如何实现阻塞 I/O(BIO)和非阻塞 I/O(NIO)的方式及其对比。 #### 阻塞 I/O (BIO) 的实现 对于传统的 BIO 方式,每个客户端请求都需要分配一个新的线程来进行处理。这通常通过 `java.net.ServerSocket` 来完成,并结合 Spring 提供的任务执行器(Task Executor)。以下是一个简单的例子: ```java import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.io.*; import java.net.ServerSocket; import java.net.Socket; @Configuration @EnableAsync public class BioConfig { @Bean public Thread taskExecutor() { return new Thread(() -> { try (ServerSocket serverSocket = new ServerSocket(8080)) { while (true) { Socket clientSocket = serverSocket.accept(); handleClient(clientSocket); } } catch (IOException e) { throw new RuntimeException(e); } }); } private void handleClient(Socket socket) { new Thread(() -> { try ( BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter out = new PrintWriter(socket.getOutputStream(), true) ) { String inputLine; while ((inputLine = in.readLine()) != null) { System.out.println("Received: " + inputLine); out.println("Echo: " + inputLine); } } catch (IOException e) { throw new RuntimeException(e); } }).start(); } } ``` 上述代码展示了如何使用多线程模型来支持多个客户端连接[^1]。需要注意的是,在高并发场景下,这种方案可能会导致大量的线程开销以及资源浪费。 --- #### 非阻塞 I/O (NIO) 的实现 相比之下,NIO 使用单个或少量线程即可高效地处理大量并发连接。它依赖于 Java 的 `Selector` 类机制,允许在一个线程上监听多个通道的状态变化。下面提供了一个基本示例: ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.Set; @Configuration public class NioConfig { @Bean public Thread nioServerThread() throws IOException { return new Thread(() -> { try (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); Selector selector = Selector.open() ) { serverSocketChannel.bind(null /* Bind address */ ); serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { int readyChannels = selector.select(); if (readyChannels == 0) continue; Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isAcceptable()) { acceptConnection(serverSocketChannel, selector); } if (key.isReadable()) { readData(key); } keyIterator.remove(); } } } catch (Exception ex) { throw new RuntimeException(ex); } }); } private void acceptConnection(ServerSocketChannel serverSocketChannel, Selector selector) throws IOException { SocketChannel client = serverSocketChannel.accept(); client.configureBlocking(false); client.register(selector, SelectionKey.OP_READ); } private void readData(SelectionKey key) throws IOException { SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); int bytesRead = channel.read(buffer); if (bytesRead > 0) { buffer.flip(); byte[] data = new byte[buffer.remaining()]; buffer.get(data); System.out.println("Message received: " + new String(data)); } } } ``` 此方法利用了 NIO 的特性,能够显著减少线程数量并提高系统的吞吐量[^2]。 --- #### 性能比较适用场景分析 - **BIO** 更适合低并发的应用环境,因为它易于理解和维护。然而,在面对大规模并发访问时,其性能瓶颈明显。 - **NIO** 则更适合高负载的服务端应用开发,尽管其实现复杂度较高,但它提供了更优的时间效率和空间利用率。 为了进一步优化这两种方式下的表现,还可以引入诸如 Netty 这样的框架或者调整底层参数如缓冲区大小 (`SO_RCVBUF`) 等设置[^3][^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值