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

news2024/11/15 15:32:29

文章目录

  • 前言
  • 一、理论准备
    • 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()返回此套接字的输出流,可以直接使用这个输出流向对端发送数据

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

服务器

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

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

// 服务器
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() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    // 服务器处理客户端请求的主逻辑
    private void processConnection(Socket clientSocket) throws IOException {
        // 因为对端是通过字节流来发送的数据,因此如果对方发送多条数据,就无法区分数据
        // 所以要双方约定好,数据的结束标记,遇到结束标记就代表收到了一个完整的数据
        // 此次客户端服务器使用的结束标记为换行 \n
        System.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就会返回true
                if (!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. 把响应写回给客户端. 不要忘了, 响应里也是要带上换行 \n
                printWriter.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️⃣ 将响应转换为字符串并打印

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

// 客户端
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.coloradmin.cn/o/728151.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mysql架构篇--Mysql 主从同步方案

文章目录 前言一、传统的主从复制:1 原理:2 缺点: 二、半同步复制(Semi-Synchronous Replication):三、组复制:1 原理:2 实现:2.1 myql 实例安装:2.1 myql 实…

量子近似优化算法(QAOA)入门(1):从量子绝热算法(QAA)角度的直观理解

文章目录 前言:量子计算的本质是测量一、基于量子逻辑电路的常用算法1.NISQ:Noisy Intermediate-Scale Quantum(含噪声中等规模量子) 二、量子绝热算法(QAA:Quantum Adiabatic Algorithm)1.QAA的…

【KingFusion】用KingFusion3.6创建一个客户端工程的步骤

哈喽,大家好,我是雷工! 今天学习用KingFusion3.6创建一个客户端工程,以下记录创建过程。 客户端组件作为KingFusion3.6的数据展示功能模块,其主要功能是通过组态组态式配置以及丰富的图表元素、动画连接等多样的展示形…

ROS:TF坐标变换

目录 一、TF坐标变换背景二、概念三、静态坐标变换3.1概念3.2实际用例3.2.1分析3.2.2流程3.2.3C实现 一、TF坐标变换背景 机器人系统上,有多个传感器,如激光雷达、摄像头等,有的传感器是可以感知机器人周边的物体方位(或者称之为:坐标&#…

《LORA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS》论文笔记

引言 全量参数微调在LLM背景下由“不方便”演变为“不可行|高昂成本”,基于“收敛的模型参数可以压缩到低维空间”的假设: the learned over-parametrized models in fact reside on a low intrinsic dimension. 作者提出LORA(Low Rank Adap…

远程关闭或重新启动计算机

远程关机只是从远程位置关闭计算机的过程。主要领域是组织在没有知识的情况下失去收入将是电力费用。员工倾向于在周末打开他们的系统。不必要的电力消耗也会影响我们的环境。在这种情况下,系统管理员可以在周末和非工作时间安排自动系统关闭,或者在必要…

Valve 签约开源 Linux 图形驱动开发者

导读据外媒 phoronix 报道,Valve 最近聘用了著名开源 Linux 图形驱动开发者 Alyssa Rosenzweig,以改进开源 Linux 图形驱动程序堆栈,增强 Linux 游戏生态系统。 Alyssa Rosenzweig 多年来在 Panfrost 开源、逆向工程 Arm Mali 图形驱动程序方…

【自动化测试基础知识】什么是自动化测试?

什么是自动化测试? 自动化测试是一种软件工具的应用,用于自动化由人驱动的检查和验证软件产品的手工过程。大多数现代敏捷和DevOps软件项目现在都包括从一开始就进行自动化测试。然而,为了充分理解自动化测试的价值,先学习下在它被广泛采用…

优化|一阶方法:求解不具有凸性和lipschitz连续性的复合问题

论文解读者:陈康明,赵田田,李朋 编者按:​ 对于大多数一阶算法,我们会在收敛性分析时假设函数是凸的,且梯度满足全局 Lipschitz 条件。而本文中,对于某一类特殊函数。我们不仅不要求函数是凸的…

基于信号博弈模型的区块链赋能下中小企业融资问题

​ 我国的金融体系是银行主导性,银行信贷是企业融资的首要来源。然而银企之间存在着严重的信息不对称,根据经典的微观银行理论,银行与企业之间的信息不对称会引发道德风险和逆向选择问题。因此在银行信贷市场中,当中小企业需要融资…

MySQL实现数据炸裂拆分(类似Hive的explode函数的拆分数组功能)

MySQL实现数据炸裂拆分(类似Hive的"explode"函数的拆分数组功能) 需求背景 背景描述 ​ 在Hive中,"explode"函数用于将数组类型的列拆分为多行,以便对数组中的每个元素进行处理。然而,在MySQL中,并没有直接…

前置微小信号放大器怎么用

前置微小信号放大器是一种用于将微弱信号从传感器转换成足够强度的信号以便更好地进行检测和处理的设备。它主要应用于各种传感器领域,例如温度传感器、压力传感器、光学传感器和生物传感器等。前置微小信号放大器的作用是提高信号的信噪比,减小噪声干扰…

天津热门大数据培训班 大数据选课技巧

大数据开发技术的应用时时刻刻都会影响我们的生活,所以很多想转行做大数据开发,大数据开发技术不断更新和发展,很多企业在开发过程中需要的大数据开发技术不断提高要求,因此市面上缺少的是要全面技能的大数据开发人员。 什么是大…

使用 Docker Desktop 安装 Centos 系统

一、前言 由于 Docker 是一个容器,它支持在一个服务器进行多服务部署,并且还能保持服务的独立性,那么,在Docker 上的运用时 我们也是可以 独立部署多个系统来做不同是其他,这样环境独立的情况下,也就不会造…

投票评选活动小程序v2-用户报名图片上传

投票评选活动小程序v2-用户自行报名收集材料页面 主要收集项目或者作品图片及其描述,可以在后台进行统一录入,也可以是在用户界面,让用户自行报名上传。 这里开发了一个“我要报名”页面,在首页点击“我要报名”按钮跳转过来。 …

精耕细作的运维资源成本管控方法-互联网企业的Finops思考与实践

当前,降本增效成为各大互联网公司的重要方向,IT成本则占据了互联网成本的大头。随着IT资源成本花费越来越高,很多公司意识到掌握管控成本和优化成本的重要性。 如何有效的降本?如何做好成本的洞察管控?如何掌握资源成…

5000字干货!让你一次搞懂什么是高保真原型

在产品设计领域,尤其是在用户体验设(UX)中,高保真原型至关重要。它是一种几乎按照产品最终的呈现模样制作出来的原型,包含产品的细节、真实的交互和完善的UI。正因为高保真原型最接近真实产品,因此成为企业…

使用Streamlit和OpenAI API构建视频摘要

本文提供了使用Streamlit和OpenAI创建的视频摘要应用程序的概述。该程序为视频的每个片段创建简洁的摘要,并总结视频的完整内容。 要运行应用程序,需要安装以下依赖项: Python(3.7或更高版本)StreamlitOpenAI API密钥llama_indexyoutube_transcript_api…

Vue3+Vite+Pinia+Naive项目搭建之二:scss 的安装和使用

前言 如果对 vue3 的语法不熟悉的,可以移步 Vue3.0 基础入门,快速入门。 1. 安装依赖 yarn add sass -D // or npm install sass -D 2. 页面样式初始化 reset.scss /* 新建 src/assets/style/reset.scss */ /* 页面样式初始化 */ html, body, div, s…

Linux VS Windows 孰优孰劣?

目录 1. 开源 vs. 闭源:2. 用户界面:3. 软件兼容性:4. 系统安全性:5. 社区支持和文档资源: Linux和Windows是两个主要的操作系统,它们在很多方面都有不同的特点和使用体验。以下是对Linux和Windows进行比较…