客户端/服务器的简易实现

news2024/9/19 7:21:21

目录

一,网络编程套接字

二,UDP/TCP的区别(​编辑)

三,UDP API使用

四,TCP API使用


一,网络编程套接字

socket

socket(操作系统给应用程序的API,起了一个名字,就成为socket API)

socket API提供了两套API分别为UDP和TCP:

二,UDP/TCP的区别()

TCP有链接,可靠传输,面向字节流,全双工

UDP无连接,不可靠传输,面向数据报,全双工

1), 此处谈到的链接,是抽象链接.通讯双方,如果保留了通信对端的信息(端口和IP),就相当于是"有链接",如果不保存对端的的信息,那就是"无连接"

2), 可靠传输/不可靠传输:"可靠"不是指100%能到达对方,而是"尽可能".网络环境非常复杂,但存在非常不确定的因素.相对来说,"不可靠"就是不考虑信息是否可以到达对方

TCP内置了一些机制,保障可靠传输

1)感知对方是否收到了

2)重传机制,在对方没收到的时候进行重传

UDP则无重传机制UDP完全不管发出去的数据是否传到对方

但是可靠传输,需要付出代价,设计就更加复杂,传输效率也会有些损失

3), TCP是面向字节流的传输过程就和文件流/水流类似

UDP是面向数据包的,这是传输数据的基本单位,一次发送/接收,必须发送/接收完整的UDP数据报

4), 全双工:一个通信链路可以发送数据也可以接受数据(双向通信)

     半双工:一个数据链路只能发送数据或者只能接受数据(单向通信)

三,UDP API使用

DatagramSocket:代表一个socket对象(这是操作系统的概念,socket就可以认为是是操作系统中广义的文件下里面一种文件类型,这样的文件就是网卡这种硬件设备抽象的的表达形式)所以他就和文件具有相同的性质,需要打开才可以读写,还需要及时关闭,会占用文件读取表里面的资源...

网卡有不同的型号,也会提供不同的API,就会使代码直接操控网卡不太好操作,把网卡封装成了"socket",使程序员不需要关注不同的硬件的差异和细节,统一的去操作socket对象,就能简介操控网卡

DatagramPacket:代表一个UDP数据报:

import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class UDPEchoClient {
    DatagramSocket socket=null;
    private  String serverIP;
    private int serverPort;
    public UDPEchoClient(String serverIP,int serverPort) throws SocketException {
        socket=new DatagramSocket();
        //不需要指定端口号,不指定,不代表没有,而是客户端这边的的端口号是系统自动分配的
        //原因,如果在客户端这里这边指定了端口号之后,由于客户端是在用户这边运行的,
        //并不清楚用户的电脑上有哪些程序都已经占用了那些端口号,如果你指定的端口号和电脑上运行
        // 其他程序的的端口号发生冲突,就会出现bug,让系统自动分配一个端口号,就可以确保分配到的是一个没有人使用的端口号
        this.serverIP=serverIP;
        this.serverPort=serverPort;
    }
    public void start() throws IOException {
        System.out.println("启动客户端");
        Scanner scanner=new Scanner(System.in);
        while (true){
            //1,从控制台,读取到用户的输入
            System.out.print("->");
            String request=scanner.next();
            //2,构造出一个请求
            DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(this.serverIP), this.serverPort);
            socket.send(requestPacket);
            //3,从服务器读到相应
            DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePacket);
            String response=new String(responsePacket.getData(),0,responsePacket.getLength());
            //4,把响应打印的控制台上
            System.out.println(response);
        }

    }

    public static void main(String[] args) throws IOException {
        UDPEchoClient client=new UDPEchoClient("127.0.0.1",9090);
        client.start();
    }
}

-------------------------------------------------------------------------------------------------------------------------

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;

public class UDPEchoServer {
    private DatagramSocket socket=null;
    //socket的生命周期应该是跟随整个进程,进程结束了,socket才需要关闭,进程结束就会自定进行关闭,所以不需要专门去写close
    public UDPEchoServer(int port) throws SocketException {
        //对于服务器来说需要在socket对象创建的时候,就指定一个端口号,作为构造方法的参数
        //后续服务器开始运行的时候操作系统就会把端口号和该进程关联起来
        socket=new DatagramSocket(port);
        //再调用这个构造方法的过程中,JVM就会调用系统的socket API完成"端口号-进程"之间的关联动作
        //也被称为"绑定端口号"
        //对于操作系统来说,一个端口号只能绑定一个进程,但是一个进程可以创建多个socket对象,绑定多个端口号
    }
    public void start() throws IOException {
        System.out.println("服务器启用");
        while (true){
            //通过死循环,不停地处理客户端的请求
            //1,读取客户端的请求并解析,receive从网卡上读取数据,如果网卡上有数据,就会立刻receive返回,
            // 获得收到的数据,但是如果网卡上没有数据,就会阻塞等待.receive也是一个输出型参数
            DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            //2,上述收到的数据,是二进制的byte[]的形式,后续如果要进行打印之类的处理操作,需要转化成字符串才好处理
            String request=new String(requestPacket.getData(),0,requestPacket.getLength());
            //3,根据请求计算响应,由于此处是回显服务器,响应就是请求
            String response=process(request);
            //4,把响应返回给客户端
            DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),
                    response.getBytes().length,requestPacket.getSocketAddress());
            socket.send(responsePacket);
            //5,打印日志
            System.out.printf("[%s:%d] req=%s,resp=%s\n",requestPacket.getAddress()
                    ,requestPacket.getPort(),request,response);
        }
    }

    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        UDPEchoServer server=new UDPEchoServer(9090);
        server.start();
    }

}

注意这种写法,这里是获取的字节数组的长度,而非字符串的长度,如果字符串里面都是英文字母/阿拉伯数字/英文标点符号的话,都是ASCII编码的,一个字符也是有一个字节那么长,但是如果字符串中有中文,是utf8编码的,一个中文是3个字节长度,utf8也是能兼容ASCII的,当使用utf8表示英文的时候和ASCII表示英文是完全相同的

此处通信是本机上的,如果客户端和服务器搞两个主机,如此能够跨主机通信吗?

答:如果把我们写的代码放到"云服务器上"此时就可以通信.如果把服务器代码运行到自己的电脑上,此时,其他人是无法访问到我这个服务器的,除非他和我连一样的WiFi(云服务器:也是一个电脑,但是硬件配件和性能相比于台式机要低很多,但是带有公网IP)

如何在跨主机通信??

1)先有一个云服务器

2)在本地电脑上有一个能连接服务器的"终端"软件

3)登录到服务器上

4)把写的服务器代码,打包为jar包文件,上传到服务器上(Java编译生成.class文件,把.class文件打成特定结构的压缩包就是.jar)

5)在云服务器上运行程序

6)运行客户端,连接服务器

通过端口后找到对应的PID,然后找到相对的应用程序:

同一个协议下,当接口号被两个进程同时使用的时候就会直接进行报错,这时候我们就需要决定,是给新的进程换一个端口号还是把旧的删掉

后面的数字是绑定端口9090进程的PID

四,TCP API使用

前面针对文件的操作,TCP socket来说,也同样适用

public class TCPEchoClient {
    private Socket socket=null;

    public TCPEchoClient(String serverIP,int serverPort) throws IOException {
        socket=new Socket(serverIP,serverPort);
    }
    public void start(){
        System.out.println("客户端启动");
        try (InputStream inputStream=socket.getInputStream();
             OutputStream outputStream=socket.getOutputStream()){
            Scanner scanner=new Scanner(inputStream);
            PrintWriter printWriter=new PrintWriter(outputStream);
            Scanner scannerIn=new Scanner(System.in);
            while (true){
                //1,从控制台读取数据
                System.out.print("->");
                String request=scannerIn.next();
                //2,把请求发给服务器
                printWriter.println(request);
                printWriter.flush();
                //3,从服务器读取响应
                if (!scanner.hasNext()){
                    break;
                }
                String response=scanner.next();
                //4,打印结果
                System.out.println(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();
    }
}

----------------------------------------------------------------------------
public class TCPEchoServer {
    private ServerSocket serverSocket=null;
    public TCPEchoServer(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("启动服务器");
        while (true){
            //TCP建立连接的过程是在操作系统内核完成的,accept操作,这时内核已经完成建立连接操作了,然后才可以"接通电话"
            //accept相当于是针对内核已经建立好的连接进行一个确认的作用
            Socket clientSocket=serverSocket.accept();
            //每一次服务器调用一次accept,就会创建一个新的socket对象,用来和客户进行"一对一服务
            Thread thread=new Thread(()->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            thread.start();


        }
    }

    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort());
        //获取到socket对象中的流对象
        try (InputStream inputStream=clientSocket.getInputStream();
             OutputStream outputStream=clientSocket.getOutputStream()){
            //使用Scanner把inputStream进行包装一下,这样就可以更加方便的读取到这里的请求
            Scanner scanner=new Scanner(inputStream);
            PrintWriter printWriter=new PrintWriter(outputStream);
            while (true) {
                //1,读取请求,并解析
                if (!scanner.hasNext()){
                    //如果scanner无法读取到数据,说明客户端关了,导致服务器这边读到了"末尾"
                    break;
                }
                String request=scanner.next();
                //2,根据请求计算响应
                String response=process(request);
                //3,把响应写回给客户端
                printWriter.println(response);
                printWriter.flush();
                //此处将\n作为请求和响应的结尾,因此后续客户端也会使用scanner.next来进行读取
                //outputStream.write(response.getBytes());
                //4,打印日志
                System.out.printf("[%s:%d] req=%s,resp=%s\n",clientSocket.getInetAddress(),clientSocket.getPort()
                        ,request,response);
            }

        }catch (IOException e){
            e.printStackTrace();
        }finally {
            System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());
            clientSocket.close();
        }

    }

    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TCPEchoServer tcpEchoServer=new TCPEchoServer(9090);
        tcpEchoServer.start();
    }
}

三个bug:

1)客户发送数据后没有任何响应,此处的情况是,客户端并没有真正的把请求发送出去

原因:printWrite这样的类,以及其他的很多IO流中的类,都是"自带缓冲区"的,引入缓冲区后,进行写入数据的操作,不会立刻触发IO,而是先放到缓冲区中,等到缓冲区有一定量的数据后,再统一进行发送

解决:

可以引入flush操作,主动"刷新缓冲区"

2)当前的服务器代码,针对clientSocket没有进行close操作

ServerSocket和DatagramSocket他们的生命周期是跟随整个进程的,但是clientSocket是连接级别的数据,随着客户端断开连接,这个socket就不再使用了(即使同一个客户端,断开后,重新连接,也是一个新的socket和旧的socket不是同一个了),因此这里需要主动关闭掉(文件泄露)

3)尝试使用多个客户端同时连接服务器

同一时刻,只能给一个客户端提供服务,只有停止上一个客户端,才能服务下一个客户端(这是不科学的),当一个客户端连上服务器后,服务器代码就会进入processConnect内部的while循环,此时第二个客户端尝试连接的时候,无法执行第二个客户端发送的请求数据,都积压在操作系统内核的接收缓冲区中,第一个客户端退出后,processConnect中的循环结束了,于是外部的循环就可以执行了,于是就可以处理第二个客户端积压的数据了

解决:多线程

如果短时间内要创建大量的线程,那么当前代码就不够用了,需要引入线程池

短时间有大量客户端:

1)客户端发送一个请求就快速断开连接->线程池(线程池本质上服务与一个客户端,使用线程池就是在复用线程)

2)客户端持续的发送请求处理响应,连接会持续很久->这种情况依旧可以使用较少的线程,提供高效的服务,"IO多路复用"

网络开发的时候,使用更少的线程,处理更多的客户端,由于刚刚的线程只服务于一个客户端,这样的线程很容易发生阻塞,相比于解决请求的时间,更多的时间是出于阻塞的状态的.所以我们可以给一个线程分配更多的客户端进行处理(IO多路复用具体的方案有很多种,最知名的就是Linux下的epoll).epoll就是在内核中,搞了一个数据结构,你可以把多个socket放到这个数据结构里面,,同时刻,大部分的socket都是处于阻塞等待状态,少数收到数据的socket,epoll就会通过回调函数的方式,通知应用程序,应用程序就会调用少量的线程,针对这里的"有数据"的socket进行处理

一个进程中,有三个特殊的流对象(特殊的文件描述符)不需要关闭,当他们启动的时候,操作系统自动打开,他们的生命周期都是要跟随整个进程的(System.in=>标准输入;System.out=>标准输出;System.err=>标准错误)

长链接:客户端连上服务器之后,一个连接中,会发起多次请求,接受多次响应

短连接:客户端和服务器连接上以后,一个链接,只发一个请求,接受一个响应,然后就断开连接,因此可能会频繁地建立/断开链接,而建立断开连接也是有开销的

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2145774.html

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

相关文章

图解view_transformation(可视化BEV视角转换的过程)

BEV视图生成的流程总结: (1)生成视锥 (2)将视锥由uv坐标系转到EGO坐标系 (3)视锥体素化 (4)bev_pool (1)生成视锥 长宽数量: 模型输入图片尺寸//…

Winform登录实现及工具栏切换

1、登录实现 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms;namespace LoginApp {public par…

峟思传感器在水库坝体渗漏防治措施中的应用

在水库工程管理中,坝体渗漏是一个不容忽视的问题,它不仅影响水库的正常运行,还可能对下游地区造成严重的安全隐患。因此,采取有效的渗漏防治措施至关重要。在这一过程中,传感器作为关键的技术手段,发挥着不…

react + antDesignPro 企业微信扫码登录

效果 实现步骤 1、项目中document.ejs文件引入企微js链接 注意&#xff1a;技术栈是使用的react antDesignPro&#xff0c;不同的技术栈有不同的入口文件&#xff08;如vue在html文件引入&#xff09; <script src"https://wwcdn.weixin.qq.com/node/wework/wwopen/j…

02 ETH

以太坊与比特币有什么不同&#xff1f; 以太坊立足比特币创新之上&#xff0c;于 2015 年启动&#xff0c;两者之间有一些显著不同。 比特币就仅仅是比特币&#xff1b;以太坊包括以太币&#xff0c;以太币才是和比特币对等的存在。以太坊是可编程的&#xff0c;所以你可以在…

Mybatis+Druid+MybatisPlus多数据源配置

MybatisDruidMybatisPlus多数据源配置 平常我们使用的是 properties 或者 yaml 来配置数据库的地址、用户名、密码等 但是这样只能配置一个数据源 现在我们想在一个项目里面配置多个数据源&#xff0c;那么我们就需要配置自己的配置类 配置类和配置文件 Mybatismysqldruid配置…

[数据集][目标检测]棉花叶子病害检测数据集VOC+YOLO格式977张22类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;977 标注数量(xml文件个数)&#xff1a;977 标注数量(txt文件个数)&#xff1a;977 标注类别…

Linux标准IO-系统调用详解

1.1 系统调用 系统调用&#xff08;system call&#xff09;其实是 Linux 内核提供给应用层的应用编程接口&#xff08;API&#xff09;&#xff0c;是 Linux 应用层进入内核的入口。不止 Linux 系统&#xff0c;所有的操作系统都会向应用层提供系统调用&#xff0c;应用程序通…

软件测试技术之 GPU 单元测试是什么!

1 背景 测试是开发的一个非常重要的方面&#xff0c;可以在很大程度上决定一个应用程序的命运。良好的测试可以在早期捕获导致应用程序崩溃的问题&#xff0c;但较差的测试往往总是导致故障和停机。 单元测试用于测试各个代码组件&#xff0c;并确保代码按照预期的方式工作。单…

《MmAP : Multi-Modal Alignment Prompt for Cross-Domain Multi-Task Learning》中文校对版

系列论文研读目录 文章目录 系列论文研读目录摘要1 引言2 相关工作3 方法3.1对比图像预训练3.2 多模式对齐提示3.3 多任务提示学习框架 4 实验4.1基准设置4.2实验结果4.3消融研究 5、结论 摘要 多任务学习&#xff08;Multi-Task Learning&#xff0c;MTL&#xff09;是为了同…

Vue3.0组合式API:使用defineEmits()实现子组件向父组件传递数据

1、使用 defineEmits() 函数 父组件通过使用 Prop 为子组件传递数据&#xff0c;但如果子组件要把数据传递回去&#xff0c;就需要使用自定义事件来实现。父组件可以通过 v-on 指令&#xff08;简写形式“”&#xff09;监听子组件实例的自定义事件&#xff0c;而子组件可以通…

大数据Flink(一百二十一):Flink CDC基本介绍

文章目录 Flink CDC基本介绍 一、什么是CDC 二、CDC的实现机制 三、​​​​​​​​​​​​​​传统 CDC ETL 分析 四、​​​​​​​​​​​​​​基于 Flink CDC 的 ETL 分析 五、​​​​​​​​​​​​​​什么是 Flink CDC 六、​​​​​​​​​​​​​​…

KeyError: u‘2‘

字典键的定义跟要求不一致 如&#xff1a;digit‘23’ (字符串&#xff09; 定义字典时 dict1{‘1’:X, ‘2’:X} 而不是dict1{1:X, 2:X}

8.2Roberts算子边缘检测

基本概念 Roberts算子是一种简单的一阶导数边缘检测算子&#xff0c;它通过计算图像在水平和垂直方向上的梯度来检测边缘。在OpenCV中&#xff0c;Roberts算子可以通过手动应用卷积核来实现。Roberts算子是一组2x2的小型滤波器&#xff0c;用于检测图像中的垂直和水平边缘。 …

飞睿智能UWB BLE Tag蓝牙防丢器模块,APP测距定位一键绑定,安全守护每一刻

我们总在不经意间与生活中的小物件擦肩而过——钥匙遗忘在咖啡厅的角落&#xff0c;钱包遗失在拥挤的地铁&#xff0c;甚至孩子的书包在人群中悄然消失……每一次的失而复得都是幸运的眷顾&#xff0c;但更多的是遗憾与不便。今天&#xff0c;就让我带你走进一个智能守护的新世…

【Python报错已解决】AttributeError: ‘WindowsPath‘ object has no attribute ‘rstrip‘

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 专栏介绍 在软件开发和日常使用中&#xff0c;BUG是不可避免的。本专栏致力于为广大开发者和技术爱好者提供一个关于BUG解决的经…

鸿蒙OpenHarmony【轻量系统芯片移植】轻量系统STM32F407芯片移植案例

轻量系统STM32F407芯片移植案例 介绍基于STM32F407IGT6芯片在拓维信息[Niobe407]开发板上移植OpenHarmony LiteOS-M轻量系统&#xff0c;提供交通、工业领域开发板解决方案。移植架构采用Board与SoC分离方案&#xff0c;使用arm gcc工具链Newlib C库&#xff0c;实现了lwip、l…

基于动态顺序表实现病历存储项目

基于动态顺序表实现通讯录项目https://blog.csdn.net/Eristic0618/article/details/135718230?spm1001.2014.3001.5506 原文在这里嗷&#xff0c;我进行了小小修改&#xff0c;快去关注这位佬。阿瑾0618https://blog.csdn.net/Eristic0618?typeblog &#xff08;1&#xff…

百度Android IM SDK组件能力建设及应用

作者 | 星途 导读 移动互联网时代&#xff0c;随着社交媒体、移动支付、线上购物等行业的快速发展&#xff0c;对即时通讯功能的需求不断增加。对于各APP而言&#xff0c;接入IM SDK&#xff08;即时通讯软件开发工具包&#xff09;能够大大降低开发成本、提高开发效率&#…

rocky9.2的lvs的NAT模式下的基本使用的详细示例

文章目录 前言什么是LVS?&#xff08;Linux Virtual Server&#xff09;LVS的组成1. 负载均衡器&#xff08;Load Balancer&#xff09;2. 后端服务器池&#xff08;Real Servers&#xff09;3. IPVS&#xff08;IP Virtual Server&#xff09;4. 调度算法&#xff08;Schedul…