网络编程3——TCP Socket实现的客户端服务器通信完整代码(详细注释帮你快速理解)

news/2025/1/7 21:34:57/

文章目录

  • 前言
  • 一、理论准备
    • Socket套接字是什么
    • TCP协议的特点
  • 二、TCP 流套接字提供的API
    • ServerSocket API
    • Socket API
  • 三、代码实现请求响应式 客户端服务器
    • 服务器
    • 客户端
    • 疑惑解答
      • 为什么服务器进程需要手动指定端口号而客户端进程不需要
      • 为什么客户端中的服务器IP与端口号是"127.0.0.1" 与 9090
      • 为什么服务器Socket对象要关闭,ServerSocket对象却不用,客户端的Socket对象也不用关闭
      • 缓冲区是什么?为什么要手动刷新缓冲区???
  • 总结


前言

本人是一个刚刚上路的IT新兵,菜鸟!分享一点自己的见解,如果有错误的地方欢迎各位大佬莅临指导,如果这篇文章可以帮助到你,劳请大家点赞转发支持一下!

今天分享的内容是TCP流套接字实现的客户端与服务器的通信,一定要理解 DatagramSocket,DatagramPacket 这两个类的作用以及方法,十分有助于你理解服务器,客户端代码。


一、理论准备

Socket套接字是什么

Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。

程序猿👨‍💻编写网络程序,主要编写的是 应用层的程序代码 ,但是真正想要发送或接收数据,都是要 通过应用层调用传输层

因此传输层就为应用层(为我们编写代码)提供了一组api统称为
Socket api

简单来说,这一组api是提供给咱们 编写网络程序使用的接口 用来发送 / 接收网络数据使用的接口

Socket套接字主要针对传输层协议划分为如下三类:
1️⃣ 数据报套接字:使用传输层UDP协议 (本文重点讲解)
2️⃣ 流套接字:使用传输层TCP协议 (下篇文章重点讲解)
3️⃣原始套接字(不做介绍)


TCP协议的特点

特点说明
有连接刻意保存对端的相关信息
可靠传输尽全力将数据传输过去不是百分百成功,自己会知道数据传输是否成功
面向字节流以一个字节为基本单位(一个数据可以分成几份 多次发多次收)
有接收缓冲区,也有发送缓冲区后续文章介绍
大小不受限对于要传输的数据大小没有要求
全双工一条通信路径,双向通信。(可以同时发送和接收数据)

二、TCP 流套接字提供的API

ServerSocket API

ServerSocket 创建TCP服务端Socket的API

Server Socket对象可以理解为一个管家,每当有客户端想要连接服务器时,他就会为每个连接进来的服务器提供一个专门伺候他的Socket对象(保姆)

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

Socket API

Socket是客户端的Socket,或服务端中接收到客户端建立连接的请求后,accept方法 返回的服务端Socket。 是 创建TCP服务端Socket的API

Socket对象就是ServerSocket API这个管家分配给每个服务器的保姆

Socket 构造方法方法说明
Socket(String host, int port)创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接
Socket 方法方法说明
InetAddress getInetAddress()返回套接字所连接的地址
InputStream getInputStream()返回此套接字的输入流,可以直接使用这个输入流读取对端发送的数据
OutputStream getOutputStream()返回此套接字的输出流,可以直接使用这个输出流向对端发送数据

三、代码实现请求响应式 客户端服务器

服务器

TCP 流套接字是字节流读取,因此要给每个数据规定一个结束标志,即我们要自定义一个协议,下面就以换行为结束标志当作协议

服务器大致就分为三个功能。
1️⃣ 读取解析客户端发来的请求
2️⃣ 根据请求计算影响
3️⃣ 把响应结果写回客户端

下面代码中一步一步实现了这三个功能,并配有详细的注释帮你快速理解

核心思路:
1️⃣服务器的核心成员属性 ServerSocket serverSocket (管家且只有一个)
2️⃣构造方法要给serverSocket指定端口号便于客户端连接,
每有一个客户端连接服务器,serverSocket就会通过accept方法专门指定一个Socket clientSocket(保姆),并生成一个独立线程来服务这个客户端。
3️⃣processConnection 方法(服务器处理客户端的主逻辑的方法),里面负责读取客户端的请求,然后调用process方法(根据请求计算响应的主逻辑),计算出响应后再返回给客户端。

processConnection 方法通过clientSocket的getInputStream(),getOutputStream()这两个方法得到能与对端直接通信的输入输出流,实现发送与读取功能。

// 服务器
public class TcpEchoServer {// serverSocket 就是管家// clientSocket 就是伺候每个客户端的保姆// serverSocket 只有一个. clientSocket 会给每个客户端都分配一个~private ServerSocket serverSocket = null;// 指定一个端口号绑定,便于客户端连接public TcpEchoServer(int port) throws IOException {this.serverSocket = new ServerSocket(port);}public void start() throws IOException {System.out.println("服务器启动");while (true) {// 如果没有客户端连接,accept方法会阻塞等待Socket clientSocket = serverSocket.accept();// 如果直接调用 processConnection(clientSocket)方法// 那么此时就会进入该方法,无法及时处理其他连接进来的客户端的请求// 解决方案:创建新的线程, 用新线程来调用 processConnection// 每次来一个新的客户端都搞一个新的线程即可!!// 方法1. 每次都手动创建新线程/*Thread t = new Thread(() -> {try {processConnection(clientSocket);} catch (IOException e) {e.printStackTrace();}});t.start();*/// 方法2. 创建线程池来解决ExecutorService executorService = Executors.newCachedThreadPool();executorService.submit(new Runnable() {@Overridepublic void run() {try {processConnection(clientSocket);} catch (IOException e) {e.printStackTrace();}}});}}// 服务器处理客户端请求的主逻辑private void processConnection(Socket clientSocket) throws IOException {// 因为对端是通过字节流来发送的数据,因此如果对方发送多条数据,就无法区分数据// 所以要双方约定好,数据的结束标记,遇到结束标记就代表收到了一个完整的数据// 此次客户端服务器使用的结束标记为换行 \nSystem.out.printf("[%s,%d 客户端上线!\n]",clientSocket.getInetAddress().toString(),clientSocket.getPort());// try () 这种写法, ( ) 中允许写多个流对象,// 并且会在try结束后,自动调用对应流的close方法// 使用 ; 来分割try (InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()){Scanner scanner = new Scanner(inputStream);// 读PrintWriter printWriter = new PrintWriter(outputStream);// 写// 没有这个 scanner 和 printWriter, 完全可以!! 但是代价就是得一个字节一个字节扣, 找到哪个是请求的结束标记 \n// 不是不能做, 而是代码比较麻烦.// 为了简单, 把字节流包装成了更方便的字符流~~while (true) {// 如果对端关闭连接,hasNext就会返回false// 如果对端有数据,hasNext就会返回trueif (!scanner.hasNext()) {// 读取的流到了结尾了 (对端关闭了)System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(),clientSocket.getPort());break;}// 1. 读取请求// 直接使用 scanner 读取一段字符串.// next遇到换行自动停止读取String request = scanner.next();// 2. 根据请求计算响应String response = process(request);// 3. 把响应写回给客户端. 不要忘了, 响应里也是要带上换行 \nprintWriter.println(response);// 该方法会在放送的同时添加换行添加了换行 \n//手动刷新缓冲区printWriter.flush();System.out.printf("[%s:%d] req: %s; resp: %s\n", clientSocket.getInetAddress().toString(),clientSocket.getPort(), request, response);}} catch (IOException e) {e.printStackTrace();} finally {clientSocket.close();}}// 根据请求计算响应的逻辑private String process(String request) {return request;}public static void main(String[] args) throws IOException {// 实例化服务器对象TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);// 启动主逻辑tcpEchoServer.start();}
}

客户端

客户端大致就分为三个功能。
1️⃣ 读取用户输入的请求
2️⃣ 将请求发送至服务器
3️⃣ 读取服务器的响应
4️⃣ 将响应转换为字符串并打印

下面代码中一步一步实现了这四个功能,并配有详细的注释帮你快速理解

核心思路:
1️⃣服务器的核心成员属性 Socket socket (保姆)
2️⃣构造方法要给socket指定IP地址与端口号与服务器进行连接。
3️⃣start方法(客户端主逻辑),用来向服务器发送请求与读取服务器的响应

start方法通过socket的getInputStream(),getOutputStream()这两个方法得到能与对端直接通信的输入输出流,实现发送与读取功能。

// 客户端
public class TcpEchoClient {private Socket socket = null;public TcpEchoClient(String serverIp,int port) throws IOException {// 这个操作相当于让客户端和服务器建立 tcp 连接.// 这里的连接连上了, 服务器的 accept 就会返回.this.socket = new Socket(serverIp,port);}public void start() {Scanner scanner = new Scanner(System.in);try (InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream();Scanner scannerFromSocket = new Scanner(inputStream);PrintWriter printWriter = new PrintWriter(outputStream)){while (true) {// 1. 从键盘上读取用户输入的内容.System.out.print("-> ");String request = scanner.next();// 2. 把读取的内容构造成请求, 发送给服务器.//    注意, 这里的发送, 是带有换行的!!printWriter.println(request);printWriter.flush();// 手动刷新缓冲区// 3. 从服务器读取响应内容String response = scannerFromSocket.next();// 4. 把响应结果显示到控制台上.System.out.printf("req: %s; resp: %s\n", request, response);}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) throws IOException {// 实例化客户端对象TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1", 9090);// 启动客户端主逻辑tcpEchoClient.start();}
}

通信结果:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


如何同时多次运行同一个代码在这里插入图片描述
在这里插入图片描述
选中第一个即可。


疑惑解答

为什么服务器进程需要手动指定端口号而客户端进程不需要

服务器的功能是用来处理其他客户端发来的请求,因此需要为客户端提供自己的端口号,方便客户端进行访问。

虽然服务器要给客户端一个响应,但是客户端的IP地址与端口号都可以在客户端发来请求的数据报中获得,因此客户端不需要手动指定端口号


为什么客户端中的服务器IP与端口号是"127.0.0.1" 与 9090

127.0.0.1 是主机环回地址。主机环回是指地址为 127.0.0.1 的任何数据包都不应该离开计算机(主机),发送它——而不是被发送到本地网络或互联网,它只是被自己“环回”,并且发送数据包的计算机成为接收者。

端口号是9090是因为是随意指定的,当然也有一些特殊端口号被指定分配给了一些牛逼的程序。


为什么服务器Socket对象要关闭,ServerSocket对象却不用,客户端的Socket对象也不用关闭

Socket对象与ServerSocket对象都会产生文件描述符,如果如果文件描述符表满了会产生文件资源泄露的严重bug,那么为什么有的调用,有的没有调用close方法???

服务器的Socket对象为什么要关闭?
因为每有一个客户端连接服务器,服务器当中的就会产生一个Socket对象(保姆),如果有n个客户端连接服务器,那么服务器就很有可能会产生文件资源泄露,因此服务器的Socket对象在完成业务后,要调用close方法

服务器的ServerSocket对象为什么不用关闭?
服务器就只有唯一一个ServerSocket对象(管家),他会伴随服务器整个生命周期,调用close的时候,也就是服务器这个进程结束的时候,因此没必要调用,进程结束时会自动将文件关闭。

每个客户端都只有唯一一个Socket对象,他也会伴随整个客户端的生命周期,调用close的时候,也就是客户端这个进程结束的时候,因此没必要调用。


缓冲区是什么?为什么要手动刷新缓冲区???

读写硬盘,读写网卡都视为IO操作

网卡的IO操作很慢,为了提高效率就引入了缓冲区

假如要往网卡中写入10次,那么就先把这些数据都写进缓冲区,等缓冲区满了,就集中写入网卡1次,这样就尽量减少了IO的操作次数,就提高了效率。

因此只有缓冲区满了,才会真正写入网卡。
因此代码中要手动刷新缓冲区,才能保证无论数据大小都可以及时发送。


总结

以上就是今天要分享的内容,本文介绍了Socket套接字,以及使用TCP协议的特点以及TCP流套接字实现的客户端与服务器的通信。网络编程让我愈发感觉到了编程的魅力,也让我领略到了科技的神奇。各位加油!

路漫漫不止修身,也养性。


http://www.ppmy.cn/news/790370.html

相关文章

Android:安卓开发采用Volley网络框架+MySQL数据库,实现从服务器获取数据并展示完成记单词APP

一、功能与要求 实现功能:设计一个记单词APP。服务器采用Tomcat,数据库采用Mysql。实现用户的注册登录功能以及单词的增删改查。 指标要求:实现UI布局;将系统数据保存到Mysql数据库中,并采用Volley网络框架实现从服务…

技嘉 H310M S2 i3-8100电脑 Hackintosh 黑苹果efi引导文件

原文来源于黑果魏叔官网,转载需注明出处。(下载请直接百度黑果魏叔) 硬件型号驱动情况 主板技嘉 H310M S2 台式电脑 处理器英特尔 Core i3-8100 3.60GHz 四核已驱动 内存16 GB ( KLEVV DDR4 2400MHz / 金士顿 DDR4 2400MHz )已驱动 硬盘…

Windows10 蓝屏 DRIVER_IRQL_NOT_LESS_OR_EQUAL (vfilter.sys)的可能解决方法

早上我的笔记本从休眠中开机的时候突然出现了蓝屏,这个蓝屏在前几天出现过了。两次提示的终止代码都一样。我的笔记本型号是DELL XPS15 9560 我的笔记本配置: 类别型号内存16GB DDR4 2666 内存(KLEVV)显卡NVIDIA GeForce GTX 1050硬盘PC300 NVMe SK hynix 256GB系统版本Window…

黑苹果efi制作_黑苹果微星B450AMD完美方案分享包括EFI制作工具及教程

黑苹果-微星B450-AMD完美方案分享包括EFI制作工具及教程 我的主机配置信息如下:处理器 AMD Ryzen 7 3700X 8-Core 八核主板 微星 B450 GAMING PRO CARBON AC (MS-7B85) ( AMD PCI 标准主机 CPU 桥 )内存 16 GB ( KLEVV DDR4 3200MHz )主硬盘 西…

Dell Inspirion 5547电脑 Hackintosh 黑苹果efi引导文件

原文来源于黑果魏叔官网,转载需注明出处。(下载请直接百度黑果魏叔) 硬件型号驱动情况 主板Dell Inspirion 5547 处理器Intel i5-4210U已驱动 内存16 GB ( KLEVV DDR4 2400MHz / 金士顿 DDR4 2400MHz )已驱动 硬盘CT1000MX500SSD1 ( 1 TB…

探索stable-diffusion技术乐园:活学活用界面参数

开篇 嗨!欢迎踏入我们充满有趣和创新的stable-diffusion技术乐园,让我们一起走进stable-diffusion界面参数的世界,看看怎样如行家袋里取物般自在地活用这些参数! 看了这么多大V、大卡和群粉们使用的英文,提起来有点沉,别急,我会尽量使用轻松的语气带你一起探索这些小秘…

设计模式(简单工厂模式)

设计模式(简单工厂模式) 1.什么是设计模式 从建筑设计领域引入到计算机科学中 设计模式一共有23种 代码设计经验的总结,稳定,拓展性更强。一系列编程思想 作用:代码更容易被他人理解、保证代码可靠性、程序的重用…

详解C盘Windows文件夹

详解C盘Windows文件夹里重要文件的作用 在整个Windows操作系统中,最重要的莫过于“Windows”文件夹,对电脑进行任何操作几乎都有关。了解这里对于掌握整个系统的运作有很大的作用,如果有兴趣不妨往下看看。 一、印象中的Windows文件夹 “W…