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 selectionKeys = selector.selectedKeys(); Iterator 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基本开发与改进【心跳、粘包与拆包、闲置连接】》