【网络原理】网络编程基础:TCP Echo Server 的底层逻辑与实现

前言:本文将简单认识一下TCP回显服务器的基本原理以及如何搭建一个TCP Echo Server

一,什么是回显服务器

什么是回显?

  • 回显是指服务器会原封不动的返回客户端发送的请求,类似于一个复读机的功能

为什么要学习它?

这是网络编程的"Hello World",是理解 TCP 三次握手、四次挥手以及 Socket 流式传输的最佳实践。

二,TCP 网络通信核心原理

1.TCP Socket(套接字)

简单来讲,TCP Socket(套接字)就是网络通信之间不同主机的应用进程之间的通信端点,什么是通信端点?如果把网络通信比作双方打电话的流程,IP地址和端口号就是电话号码,TCP Socket就好似双方的电话机。

  • 本质 :Socket 是应用层与 TCP/IP 协议栈通信的中间软件抽象层
    一个确定的 TCP 连接由 五元组 唯一标识:
    `{源IP, 源端口, 目的IP, 目的端口, 协议(TCP)

2.TCP Socket通信基本流程

TCP有连接,代表其需要站在客户端和服务端两个视角去处理通信过程

服务端(Server)

  • Socket(): 创建一个初始套接字。正如打电话一定要有电话机,Socket对象的创建是一切基础的保证

  • Bind(): 绑定一个固定的端口和 IP。这是通信的基础信息

  • Listen(): 进入监听状态,等待客户"敲门"。

  • Accept() : 阻塞等待连接。当客户端连入时,它会返回一个全新的 Socket 专门负责与该客户通信,这也是后续TCP多线程处理请求的关键

  • Read/Write: 交换数据,交换的数据取决于process()的处理逻辑如何写

  • Close(): 关闭连接,防溢出。

客户端(Client)

  • Socket(): new Socket() 发起连接。

  • Connect(): 主动向服务器发起"三次握手"(由系统内核完成)。

  • Write/Read : 数据传输,使用InputStream / OutputStream

  • Close(): 任务完成,断开连接。

三,代码实现

分为两个版本实现TCP回显服务器的编写,理解其核心原理以及设计思路

版本 1:单线程基础版

!EXAMPLE\] 点击展开:单线程 TCP 服务器代码 ```java public class TcpEchoServer { private ServerSocket serverSocket;//ServerSocket对象,用于建立连接 //构造方法,服务器的端口号作为参数 public TcpEchoServer(int port) throws IOException { serverSocket = new ServerSocket(port); } //启动TCP服务器 public void start() throws IOException { System.out.println("服务器已启动..."); //开始处理请求,请求有多个,while循环处理 while (true){ //接受请求 Socket socket = serverSocket.accept(); //处理请求 processConnection(socket); } } public void processConnection(Socket socket) throws IOException { //1.打印记录 System.out.printf("[%s:%d] 客户端上线!\n", socket.getInetAddress(), socket.getPort()); //2.使用 try-with-resources 自动关闭流和 socket try(InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream(); Scanner scanner = new Scanner(inputStream); PrintWriter printWriter = new PrintWriter(outputStream); ){ while (true){ if(!scanner.hasNext()){ break;//读取不到字节流末尾就一直循环,收到 EOF(文件结束符)返回 false } //1.获取请求 String request = scanner.next(); //2.根据请求计算响应 String response = process(request); //3.把响应写回给客户端 printWriter.println(response); //4.冲刷缓冲区 printWriter.flush();//防止响应积攒在缓冲区,导致客户端在缓冲区未满时无法第一时间拿到响应 System.out.printf("[%s:%d] req: %s; resp: %s\n",socket.getInetAddress(), socket.getPort(), request, response); } }catch (IOException e){ e.printStackTrace(); }finally { try{ //打印下线记录 System.out.printf("[%s:%d] 客户端下线!\n", socket.getInetAddress(), socket.getPort()); //释放Socket资源,显示释放 socket.close(); }catch (IOException e){ e.printStackTrace(); } } } //服务器处理逻辑 public String process(String request){ return request;//回显服务器,只需原封不动返回 } public static void main(String[] args) throws IOException { TcpEchoServer server = new TcpEchoServer(9090); server.start();//启动服务器 } } ``` \[!EXAMPLE\] 点击展开:单线程 TCP 客户端代码 ```java public class TcpEchoClient { private Socket socket; //包含ip地址和端口号的构造方法 public TcpEchoClient(String serverIp,int serverPort) throws IOException { socket = new Socket(serverIp,serverPort); } public void start() throws IOException { System.out.println("客户端已启动..."); try(InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream(); Scanner scannerConsole = new Scanner(System.in); Scanner scannerNetwork = new Scanner(inputStream); PrintWriter printWriter = new PrintWriter(outputStream)){ while (true){ //1.从控制台读取输入 System.out.print("-> "); String request = scannerConsole.next();//读取输入内容 //2.构建请求,发送给服务器 printWriter.println(request); printWriter.flush();//冲刷缓冲区 // 3. 从服务器读取响应 if (!scannerNetwork.hasNext()) { break; } String response = scannerNetwork.next(); //4.打印响应 System.out.println(response); } } catch (IOException e) { e.printStackTrace(); }finally { try{ System.out.println("客户端已下线"); socket.close();//释放socket }catch (IOException e){ e.printStackTrace(); } } } public static void main(String[] args) throws IOException { TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090); client.start(); } } ``` #### 流程图 以下是一个简易版的流程图: ![image.png](https://i-blog.csdnimg.cn/img_convert/0f5c6ab883891e764988402aae91c9f9.png) **数据的"奇幻漂流":** 1. **客户端:** 输入 →\\rightarrow→ Java 缓冲区 →\\rightarrow→ **(Flush)** →\\rightarrow→ 客户端内核 →\\rightarrow→ 网络。 2. **服务器:** 网络 →\\rightarrow→ 服务器内核 →\\rightarrow→ **(Read)** →\\rightarrow→ Java 缓冲区 →\\rightarrow→ 业务处理(回显)。 3. **返回:** 业务处理 →\\rightarrow→ Java 缓冲区 →\\rightarrow→ **(Flush)** →\\rightarrow→ 服务器内核 →\\rightarrow→ 网络。 4. **客户端获取:** 网络 →\\rightarrow→ 客户端内核 →\\rightarrow→ **(Read/Scanner)** →\\rightarrow→ 得到响应。 #### 效果展示:发现缺陷 先后启动服务端和客户端,观察效果: ![image.png](https://i-blog.csdnimg.cn/img_convert/f9e0a4b48fec197444a97d7f9bed7d53.png) * **缺点** :以上是基于单线程模式下实现的TCP回显服务器,当多个客户端建立连接,观察效果 先打开多线程模式 ![image.png](https://i-blog.csdnimg.cn/img_convert/70fbc7270e956fe9c0403ea163352f28.png) ![image.png](https://i-blog.csdnimg.cn/img_convert/274cb981e8de8b7a0993f3d49d839af2.png) 这是客户端2发送的请求失败了吗?其实不然,客户端2成功发送了请求,但是服务器此时忙不过来。当关闭客户端1的连接时,会发现: ![image.png](https://i-blog.csdnimg.cn/img_convert/25a395f9a584385c4c3cc004bffa571b.png) **显然多线程模式下,我们写的TCP回显服务器无法并发处理多个客户端发送的请求。** #### 分析问题 底层服务器能够接收到后发来的请求,但是它此时人手不足,全部忙着去处理第一个连接的客户端了,没有线程去完成后续任务了。 ![image.png](https://i-blog.csdnimg.cn/img_convert/c2d1bf5dee913e486ed46b412d16f739.png) 为了解决这一问题,我们可以有以下几种方案: * **方案一**:使用多线程处理请求,每次执行到start()时,就创建一个线程去处理该次任务,但是连接过多时频繁创建带来的开销太重 * **方案二**:为服务器创建一个线程池,实现线程复用,每次start()就把socket封装成一个task提交给线程池的线程去执行,如此一来既做到了并发执行,同时开销还不会太大(线程池线程复用)。 ### 版本 2:多线程优化版(重点) 通过上面分析,我们明白了串行的原因是服务器的主线程既要当前台柜员去接待客户(serverSocket.accept),还需要去处理请求processConnection,不妨把职责分开,主线程负责accept(),让线程池的线程去process即可 ```java private ExecutorService service = Executors.newCachedThreadPool();//可缓存的线程池,线程空闲自动回收 ``` 这里只需要修改先前的单线程服务器的start()方法即可 \[!EXAMPLE\] 点击展开:单线程 TCP 服务端代码(**线程池优化版**) ```java //启动TCP服务器 public void start() throws IOException { System.out.println("服务器已启动..."); //开始处理请求,请求有多个,while循环处理 while (true){ //接受请求 Socket socket = serverSocket.accept(); //处理请求 service.submit(()->{ try { processConnection(socket); } catch (IOException e) { throw new RuntimeException(e); } }); } } ``` 不妨试试这个版本的TCP回显服务器能不能正常并发处理请求吧\~\~ #### 效果展示 ![image.png](https://i-blog.csdnimg.cn/img_convert/4c3bfd0d1747dfdc9a4e485494f83a08.png) ## 四,总结与注意事项 下面是不同模式下的TCP回显服务器代码间的区别 ### 区别 | **模式** | **缺点** | **适用场景** | |----------------------|-----------------------|-------------------------| | **单线程** | 同一时间只能服务一个客户,其他客户死等。 | 仅用于学习底层原理。 | | **多线程 (New Thread)** | 请求多时会榨干服务器内存,线程创建成本高。 | 极少量长连接。 | | **线程池 (Executor)** | 资源可控,复用性强。 | **生产环境中最常用的 BIO 优化方案。** | ### 注意事项 #### 1:显式资源释放的"双重保障" * **关闭Socket** :在处理多个客户端并发时,及时关闭 `socket` 是防止**文件描述符耗尽**的关键。 * **注意点** :其实在 `try(...)` 括号里声明了 `InputStream` 等流,当 try 结束时,它们会自动关闭。在 Java 中,关闭包装流(如 `PrintWriter`)通常会自动关闭底层的 `Socket`。手动在 `finally` 里显式关闭 `socket.close()` 是一种更稳健的习惯,能确保在异常极端情况下资源也能释放。 #### 2:缓冲区的"冲刷策略" * **核心动作** :`printWriter.flush()`。 * **总结建议** :这是 TCP 编程中最容易遗漏的地方。如果不手动冲刷,数据可能会因为体积太小而"憋"在 Java 层的 `BufferedWriter` 或内核的 Nagle 算法缓冲区里,导致客户端迟迟收不到响应,产生"**死锁**"假象。 #### 3:线程池的选择 * **如何选择** :`Executors.newCachedThreadPool()`。 * **技术分析**:这种线程池在短时间内有大量短连接时非常高效,因为它会复用空闲线程。 #### 4: 读写的"边界协议" * **现状** :使用了 `scanner.next()` 和 `printWriter.println()`。 * **原理** :这本质上是约定了**以空白符/换行符作为消息边界**。 * **注意事项** :如果客户端发送了一个带空格的句子(如 "Hello World"),`scanner.next()` 会将其拆分成两个请求处理。对于更复杂的业务,通常需要自定义协议(比如先读 4 字节的长度,再读内容),以解决 **TCP 拆包粘包** 问题。 #### 5: 交互流程的完整性 * **EOF 处理** :使用了 `scanner.hasNext()`。 * **原理** :当客户端主动断开连接(调用 `socket.close()`)时,TCP 会发送 FIN 包,此时 `hasNext()` 会返回 `false`。如果不判断这个,服务器的线程可能会陷入死循环或抛出大量 `NoSuchElementException` 以上就是有关如何编写一个TCP Echo Server的注意事项,如有纰漏还请指出\~\~

相关推荐
青山是哪个青山3 小时前
Linux 基础与环境搭建
linux·服务器·网络
huohaiyu7 小时前
从URL到页面的完整解析流程
前端·网络·chrome·url
winfreedoms10 小时前
Puppypi——hiwonder-toolbox中配置文件解析
网络·智能路由器
Elastic 中国社区官方博客12 小时前
使用 Elastic 进行网络监控:统一网络可观测性
大数据·开发语言·网络·人工智能·elasticsearch·搜索引擎·全文检索
德迅云安全-小潘13 小时前
德迅零域(微隔离):破解云时代横向渗透困局的“手术刀”
网络·数据库·安全
敲代码的哈吉蜂13 小时前
高可用集群Keepalived
运维·服务器·网络·数据库
盟接之桥14 小时前
盟接之桥说制造:从客供的外在共生到内在的身心合一
运维·服务器·网络·人工智能·制造
一名爱学习的ikun14 小时前
VMware 虚拟机设置成静态IP后无法联网
网络·vmware