Java 网络编程(二)—— TCP流套接字编程

Java 网络编程(二)—— TCP流套接字编程

TCP 和 UDP 的区别

在传输层,TCP 协议是有连接的,可靠传输,面向字节流,全双工
而UDP 协议是无连接的,不可靠传输,面向数据报,全双工

有连接和无连接的区别是在进行网络通信的时候,通信双方有没有保存对端的地址信息,即假设 A 和 B 进行通信,A 保存了 B 的地址信息,B 也保存了 A 的地址信息,此时双方都知道和谁建立了连接,这就是有连接的通信,在之前的 UDP 数据报套接字编程中就提到过 UDP 是无连接的,所以在发送数据报的时候要加上对端的信息,防止丢包。

可靠传输是通过各种手段来防止丢包的出现,而不可靠传输则没有做任何处理直接把数据报传输过去,但是可靠传输不意味着能 100% 把数据报完整无误地传输给对方,只是尽可能降低丢包发生的概率,并且可靠传输是要使用很多手段来保持的,所以付出的代价相比于不可靠传输要大。

面向字节流就是以字节为单位来进行数据的传输,面向数据报就是以数据报为单位进行数据的传输。

全双工就是通信的双发可以同时给对方发送数据,但是半双工是指双方只有一方可以发送数据。

TCP流套接字 API 介绍

ServerSocket

ServerSocket 是TCP服务端Socket 的API

构造方法:

方法名说明
ServerSocket(int port)创建一个TCP服务端流套接字Socket,并绑定端口号

ServerSocket 方法:

方法名返回值说明
accept()Socket开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket 对象,并基于该Socket 建立于客户端的连接,否则阻塞等待
close()void关闭此套接字

Socket

Socket 是客户端Socket 或者是 服务端那边收到客户端建立连接的请求(通过 accept() 方法)返回的Socket 对象。

不管是客户端还是服务端的Socket 对象,他们都保留了对端的地址信息,这也是TCP协议有连接的体现。

Socket 构造方法:

方法名说明
Socket(String host, int port)创建一个客户端流套接字Socket,并于对应IP 的主机对应的端口的进程建立连接

Socket 方法:

方法名返回值说明
getInetAddress()InetAddress返回套接字所连接的地址
getInputStream()InputStream返回此套接字的输入流
getOutputStream()OutputStream返回此套接字的输出流

回显服务器

首先在回显服务器的构造方法里初始化我们的ServerSocket

publicTcpEchoServer(int port)throwsIOException{ serverSocket =newServerSocket(port);}

然后就是服务器启动运行的代码了:在面对多个客户端的时候,我们可以使用线程池来进行处理。
这里使用Executors.newCachedThreadPool()是不固定线程的个数的线程池,这样可以灵活地处理多个客户端的请求。

publicvoidstart()throwsIOException{System.out.println("服务器启动...");ExecutorService executorService =Executors.newCachedThreadPool();while(true){//与客户端建立连接Socket clientSocket = serverSocket.accept();//处理客户端发出的多个请求 executorService.submit(()->{try{processClient(clientSocket);}catch(IOException e){thrownewRuntimeException(e);}});}}

处理请求

我们通过了一个方法processClient来封装了处理请求的逻辑

如何进行数据的获取和写入操作?
可以通过输入流和输出流来处理getInputStreamgetOutputStream

try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream())

为了更加方便地使用这两个流对象,我们进行了进一步的封装:

//对输入流和输出流进行进一步的封装,方便我们的使用Scanner scanner =newScanner(inputStream);PrintWriter writer =newPrintWriter(outputStream);

由于客户端可能发来的不止一个请求,我们可以使用循环来处理一下,在循环体中,我们处理请求有三个步骤,首先获取请求解析请求,然后计算响应,最后发送响应

while(true){if(!scanner.hasNext()){System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());break;}//解析请求String request = scanner.next();//计算响应String response =process(request);//发送响应 writer.println(response);//因为此时的响应数据还在缓存区里,所以需要使用 flush 来将内存的数据发送出去 writer.flush();System.out.printf("[%s:%d] request:%s response:%s\n",clientSocket.getInetAddress(),clientSocket.getPort(), request,response);}

由于这里是回显服务器,所以计算响应的代码是直接返回字符串就可以了

privateStringprocess(String request){return request;}

最后当客户端没有请求的时候,我们需要断开此次连接,释放资源,避免资源的泄漏

finally{//当请求处理完的时候记得关闭服务器与客户端的连接,防止资源泄漏 clientSocket.close();}

最终代码

importjava.io.IOException;importjava.io.InputStream;importjava.io.OutputStream;importjava.io.PrintWriter;importjava.net.ServerSocket;importjava.net.Socket;importjava.util.Scanner;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;publicclassTcpEchoServer{privateServerSocket serverSocket;publicTcpEchoServer(int port)throwsIOException{ serverSocket =newServerSocket(port);}publicvoidstart()throwsIOException{System.out.println("服务器启动...");ExecutorService executorService =Executors.newCachedThreadPool();while(true){//与客户端建立连接Socket clientSocket = serverSocket.accept();//处理客户端发出的多个请求 executorService.submit(()->{try{processClient(clientSocket);}catch(IOException e){thrownewRuntimeException(e);}});}}privatevoidprocessClient(Socket clientSocket)throwsIOException{//获取输入流和输出流try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()){System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort());//对输入流和输出流进行进一步的封装,方便我们的使用Scanner scanner =newScanner(inputStream);PrintWriter writer =newPrintWriter(outputStream);while(true){if(!scanner.hasNext()){System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());break;}//解析请求String request = scanner.next();//计算响应String response =process(request);//发送响应 writer.println(response);//因为此时的响应数据还在缓存区里,所以需要使用 flush 来将内存的数据发送出去 writer.flush();System.out.printf("[%s:%d] request:%s response:%s\n",clientSocket.getInetAddress(),clientSocket.getPort(), request,response);}}catch(IOException e){thrownewRuntimeException(e);}finally{//当请求处理完的时候记得关闭服务器与客户端的连接,防止资源泄漏 clientSocket.close();}}privateStringprocess(String request){return request;}publicstaticvoidmain(String[] args)throwsIOException{TcpEchoServer server =newTcpEchoServer(9090); server.start();}}

客户端

首先在客户端构造方法建立于服务器的连接:

publicTcpEchoClient(String serverIP,int port)throwsIOException{//与服务器建立连接 socket =newSocket(serverIP,port);}

运行逻辑

首先用户从控制台输入数据,然后发送请求,接着等待服务器的响应并接收响应然后打印响应的内容即可。

publicvoidstart(){try(InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()){//对输入流和输出流进行进一步的封装Scanner scanner =newScanner(System.in);Scanner scanner2 =newScanner(inputStream);PrintWriter writer =newPrintWriter(outputStream);while(true){//发送多个请求和接收多个响应if(!scanner.hasNext()){break;}//发送请求String request = scanner.next(); writer.println(request); writer.flush();//接收响应String response = scanner2.next();System.out.println(response);}}catch(IOException e){thrownewRuntimeException(e);}}

这里要注意用户通过控制台输入数据,我们要使用的是Scanner(System.in)
当我们要发送数据的时候是使用 Socket 的 getOutputStream 方法来获取对应的输出流对象,为了便于使用所以我们又使用 PrintWriter 来进一步封装输出流,来打印响应
在发送请求的时候我们需要使用 Socket 的 getInputStream 方法来获得输入流对象,为了方便使用,所以使用Scanner(inputStream)进一步封装。

最终代码

importjava.io.IOException;importjava.io.InputStream;importjava.io.OutputStream;importjava.io.PrintWriter;importjava.net.Socket;importjava.util.Scanner;publicclassTcpEchoClient{privateSocket socket;publicTcpEchoClient(String serverIP,int port)throwsIOException{//与服务器建立连接 socket =newSocket(serverIP,port);}publicvoidstart(){try(InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()){//对输入流和输出流进行进一步的封装Scanner scanner =newScanner(System.in);Scanner scanner2 =newScanner(inputStream);PrintWriter writer =newPrintWriter(outputStream);while(true){//发送多个请求和接收多个响应if(!scanner.hasNext()){break;}//发送请求String request = scanner.next(); writer.println(request); writer.flush();//接收响应String response = scanner2.next();System.out.println(response);}}catch(IOException e){thrownewRuntimeException(e);}}publicstaticvoidmain(String[] args)throwsIOException{TcpEchoClient client =newTcpEchoClient("127.0.0.1",9090); client.start();}}

细节说明

在我们使用PrintWriter 的 writer.println(xxx)之后,我们的数据其实还保留在缓存区中,也就是还没发出去,我们需要通过flush() 方法来刷新缓存区的数据,才能将数据真正发送到对端去。


我们不可以使用writer.print这种没有自动添加换行符的方法,因为我们在接收数据的时候,使用的是Scanner 的 next()方法,next() 是要接收到空白符(包括换行符,制表符,翻页符…)才停止接收的,如果你使用 print 来发送数据,这时候的数据是没有带任何空白符的,那么就不会停止接收数据而是继续等待空白符的到来,这时候服务器就无法处理客户端的请求:如下图:

服务器就阻塞在 下图标红的代码里:

在这里插入图片描述

客户端被阻塞在接收响应的代码里:

在这里插入图片描述

你在客户端的控制台输入的回车不算进数据的换行符里,控制台输入的回车时,只是将数据交给了客户端程序,并不会自动将这些数据转换为网络流中的换行符。

换一句话说,控制台的回车只是结束你在控制台的输入,并不会自动在数据末尾加上换行符

效果展示

在这里插入图片描述


在这里插入图片描述

Read more

Flutter 三方库 github_actions_toolkit 的鸿蒙化适配指南 - 实现 GitHub Actions 高效自动化任务构建、支持日志颜色修饰与核心工具集成

Flutter 三方库 github_actions_toolkit 的鸿蒙化适配指南 - 实现 GitHub Actions 高效自动化任务构建、支持日志颜色修饰与核心工具集成

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 github_actions_toolkit 的鸿蒙化适配指南 - 实现 GitHub Actions 高效自动化任务构建、支持日志颜色修饰与核心工具集成 前言 在进行 Flutter for OpenHarmony 的工程化 CI/CD(持续集成与交付)构建时,利用 GitHub Actions 进行自动化测试和流水线发布是主流选择。github_actions_toolkit 是一个专为编写非 Web 类 Action 脚本设计的工具集,它能让你在 Dart 脚本中轻松调用 Actions 的核心功能(如日志分级输出、设置导出变量等)。本文将探讨如何利用该库提升鸿蒙项目的自动化构建效率。 一、原理解析 / 概念介绍

By Ne0inhk
TCP 服务器如何支持高并发?单进程、多进程、多线程模型详解

TCP 服务器如何支持高并发?单进程、多进程、多线程模型详解

在上一篇博客中,我们基于 UDP 实现了一个简单的群聊模型。 今天,我们正式进入 TCP 网络编程,实现一个最经典的功能 —— 🧾 服务器回显(Echo Server) 就是我们发送的消息,服务器不做处理,直接给我们返回即可。 一、TCP 服务器整体流程 一个最基础的 TCP 服务器,需要经历以下步骤: socket() bind() listen() accept() read()/write() close() 流程图可以理解为: 创建套接字 → 绑定端口 → 开始监听 → 等待客户端连接 → 收发数据 → 关闭连接 我们都知道TCP是连接的,可靠的传输层协议,所以每一个客户端在访问服务器的时候都会建立连接(也就是我们课本上说的三次握手),在客户端没有申请建立连接的时候,服务器要始终保持这监听状态(调用系统调用接口listen)(因为用户可是一天24小时内任意时间都有可能对服务器进行访问,所以服务器必须始终保持这监听状态,这就好比我们半夜不睡觉,就是刷抖音短视频,我们可从来没有打不开抖音的时候,这就是因为服务器保持着监听状态,即使你半夜进行访问,

By Ne0inhk
Flutter 三方库 gviz 的鸿蒙化适配指南 - 实现复杂的 Graphviz 拓扑图布局计算、支持 DOT 语言解析与自动化图谱生成

Flutter 三方库 gviz 的鸿蒙化适配指南 - 实现复杂的 Graphviz 拓扑图布局计算、支持 DOT 语言解析与自动化图谱生成

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 gviz 的鸿蒙化适配指南 - 实现复杂的 Graphviz 拓扑图布局计算、支持 DOT 语言解析与自动化图谱生成 前言 在进行 Flutter for OpenHarmony 的企业级应用开发中,特别是在处理网络拓扑、数据库 ER 图或编译器架构分析时,自动绘制复杂的图形结构是一项巨大挑战。gviz 是一个基于 Graphviz 设计思路的 Dart 库,它能将 DOT 描述语言转化为结构化的图谱对象模型。本文将指导大家如何在鸿蒙端利用该库高效构建动态拓扑。 一、原理解析 / 概念介绍 1.1 基础原理 gviz 充当了 DOT 源码与渲染引擎之间的桥梁。它解析外部输入的 DOT 文本,

By Ne0inhk
Flutter for OpenHarmony:puppeteer 远程控制 Chrome 浏览器,实现截图与自动化操作(Headless Chrome 适配) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:puppeteer 远程控制 Chrome 浏览器,实现截图与自动化操作(Headless Chrome 适配) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 puppeteer 是一个 Node.js 库的 Dart 移植版,它提供了一套高级 API 来通过 DevTools 协议控制 Chrome 或 Chromium。通常用于爬虫、生成 PDF、截图或自动化测试。 在 OpenHarmony 移动设备上,直接启动一个 Headless Chrome 进程是不现实的(受限于系统权限和架构)。但是,我们可以利用 puppeteer 的远程连接能力,让 OpenHarmony 应用控制部署在服务器或局域网 PC 上的浏览器实例,实现强大的远程自动化功能。本文将介绍如何在 OpenHarmony 环境下使用 puppeteer 连接并控制远程浏览器。 一、puppeteer

By Ne0inhk