首页 > 编程语言 >初始网络编程(下)

初始网络编程(下)

时间:2024-09-22 15:51:32浏览次数:8  
标签:重传 编程 网络 TCP 校验 字节 连接 初始 客户端

所属专栏:Java学习     

在这里插入图片描述

1. TCP 的简单示例

同时,由于 TCP 是面向字节流的传输,所以说传输的基本单位是字节,接受发送都是使用的字节流

方法签名

方法说明

Socket accept()

开始监听指定端口(创建时绑定的端口),有客户端连接时,返回一个服务端 Socket 对象,并基于 Socket 建立与客户端的连接,否则阻塞等待

void close()

关闭此套接字

accept 操作是内核已经完成了建立连接的操作,进行“确认”的动作

public void start() throws IOException {
        System.out.println("服务器启动...");
        while (true) {
            Socket clientSocket = serverSocket.accept();
            processConnection(clientSocket);
        }
    }

启动之后需要再次创建一个专门操作的 socket

private void processConnection(Socket clientSocket) throws IOException {
    System.out.printf("[%s:%d] 客户端上线\n", clientSocket.getInetAddress(), clientSocket.getPort());
    try (InputStream inputStream = clientSocket.getInputStream();
         OutputStream outputStream = clientSocket.getOutputStream()) {
        while (true) {
            //读取请求并进行遍历
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            if (!scanner.hasNext()) {
                break;
            }
            String request = scanner.next();
            //根据请求计算响应
            String response = process(request);
            //把响应写回客户端
            //outputStream.write(response.getBytes());
            printWriter.println(response);
            //打印日志
            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();
    }
}

与 UDP 不同的是,这里所有的传输都是用过字节流来完成的,首先读取客户端的请求,然后根据请求计算出对应的响应,再把响应写回客户端

接下来看客户端的主要功能:

public void start(){
    System.out.println("客户端启动...");
    try(InputStream inputStream = socket.getInputStream();
        OutputStream outputStream = socket.getOutputStream()){
        Scanner scanner = new Scanner(inputStream);
        Scanner scannerIn = new Scanner(System.in);
        PrintWriter printWriter = new PrintWriter(outputStream);
        while (true){
            //控制台读取数据
            System.out.print("->");
            String request = scannerIn.next();
            //把请求发送给服务器
            printWriter.println(request);
            //从服务器读取响应
            if(!scanner.hasNext()){
                break;
            }
            String response = scanner.next();
            //打印响应结果
            System.out.println(response);
        }
    }catch (IOException e){
        e.printStackTrace();
    }
}

客户端启动之后,从控制台上读取要发送的请求,接着把数据发送给服务器,然后从服务器读取响应,再打印相应的结果

上面服务端和客户端的代码其实还有 3 个 bug :

  1. 当我们运行之后发现,客户端发送了数据之后,服务端并没有任何响应,也就是客户端并没有把数据发送出去,原因就是客户端的请求是存放到了内存的缓冲区中,引入缓冲区之后进行写入数据的操作并不会立即触发 IO,由于此时要发送的数据比较少,所以需要存一会。

解决办法就是调用 flush() 方法,刷新缓冲区

  1. 当前的服务器代码,针对 clientSocket 没有进行 close 操作:ServerSocket,DatagramSocket 的生命周期都是伴随整个进程的,但是代码中的 clientSocket 是“连接级别的”数据,随着客户端断开连接,这个 socket 也就不再使用了(即使是同一个客户端,断开之后重新连接,也是一个新的 socket)因此这样的 socket 就需要手动关闭,防止文件资源泄露

  1. 服务器不能同时给多个客户端提供服务:当一个客户端连接上服务器之后,服务器代码就会进入 processConnect 内部的 while 循环,此时第二个客户端尝试连接服务器时就不能执行到第二次accept 所有的客户端发送的请求就会积攒到操作系统内核的缓冲区里,第一个客户端退出时,其他客户端才能连接

这里的问题本质上是代码结构的问题:采用了双重 while 循环的写法,就会导致进入里层 while 的时 候,外层无法执行,解决办法就是把双重 while 换成单重的:

就可以使用之前学到的多线程来解决这个问题

对于上述的代码,其实还是可以优化的,如果一段时间内有大量的客户端发送请求,就会给服务器带来比较大的压力,对于这种情况,可以通过使用线程池来优化:

通过使用线程池,解决了短时间内有大量客户端发送请求之后就断开了的问题,但是如果说客户端持续的发送请求处理响应,那么连接就会保持很久,这样的场景下使用多线程 / 线程池就不太合适了

针对上述问题,可以通过 IO 多路复用来解决,相比于处理请求的时间,大部分时间可能都是在阻塞等待,如果可以让一个线程同时给多个客户提供服务就可以了,IO 多路复用就是在操作系统内部提供的功能(IO 多路复用具体实现的方案有多种),例如 Linux 下的 epoll ,就是在内核中创建了一个数据结构,可以把多个 socket (每一个 socket 对应一个客户端)放到这个数据结构中,同一时刻,大部分的 socket 都是处于阻塞等待,少部分收到数据的 socket ,epoll 就会通过回调函数的方式通知应用程序,应用程序就可以使用少量的线程针对这些 socket 进行处理

2. 长连接和短连接

长连接:长连接是指客户端和服务器端建立连接后,在较长时间内保持连接状态,以便进行多次数据传输。这类似于建立了一条专用的通信线路,可以随时进行数据交互,直到一方主动关闭连接或者由于某些异常情况导致连接中断

短连接:短连接是指在每次数据传输时,客户端和服务器端建立连接,数据传输完成后立即关闭连接。这种连接方式就像打一次电话,通话(数据传输)结束后就挂断(关闭连接)

3. UDP 协议结构

报文格式:

UDP 的报文分为报头,正文/载荷(完整的应用层数据包),其中报头部分又分为四个部分,每一个部分都是固定的四个字节,分别存储源端口,目的端口,UDP 报文长度(报文长度 = 报头长度 + 载荷长度),校验和(检验和),每一个部分都是固定的两个字节存储,由于是两个字节存储 UDP 报文长度,所以最大值就是 65535 ,也就是 64KB ,这个时候就会出现一个问题,如果要表示的内容不止是 64KB ,就需要换用 TCP 来表示了

关于校验和:由于网络传输过程中是比较容易出现错误的,传输的电信号/光信号/电磁波等信息容易受到环境的干扰,使这里的传输信号发生转变,校验和的目的就是能够“发现”或者“纠正”这些错误,同时,如果只是发现错误,那么校验和携带的信息就可以很少,如果想要纠正错误,就需要再携带额外的信息(消耗更多的带宽)

在 UDP 协议中使用的简单有效的校验和是 CRC 校验和(循环冗余校验):对 UDP 数据报整个进行遍历,分别取出每一个字节,往一个字节或是两个字节的变量上进行累加,即使溢出之后也继续加,主要关注的是校验和的结果是否会在传输中改变

如果说传输的数据,在网络通信中没有发生任何改变,此时计算出来的就是 checksum1 == checksum2 反之,如果不相等,就代表数据传输中数据发生了改变,就会丢弃这次传输

此外还可能会发生传输过程中校验和的信息也发生改变了,也就是传输过程中校验和变成了 checksum3,此时接收方重新计算校验和得到了 checksum4 ,这种情况下两个校验和大概率是不相等的,所以影响也不大,还有可能出现两组不同的数据计算出相同的校验和,这种概率也是非常低的,所以上面这两种极端情况一般不考虑


MD5 算法:

本质上是一个“字符串 hash 算法”,特点:

  1. 定长:无论输入多长的字符串,得到的结果都是固定长度(适合做校验算法)
  2. 分散:输入的内容只要发生一点改变,得到的结果也是相差很大的(适合做哈希算法)
  3. 不可逆:根据输入的内容计算 md5 对计算机来说是不复杂的,但是如果根据 md5 的值来计算原始值,理论上是不可以的(适合做加密算法)

4. TCP 协议结构

4.1. 确认应答

在之前提到过 TCP 的核心机制是确认应答,可以确认对方是否收到数据,在数据传输的过程中,如果有多条请求,并且返回对应的响应,但是此时可能会出现这样的问题:最先发送的请求可能并不会最先收到响应,也就是收到响应的顺序会不一样。

针对这样的问题的解决方案就是给每一个字节都进行编号(TCP 的传输是面向字节流的),并且编号是连续且递增的,按照字节编号这样的机制就称为“TCP 的序号”,在应答报文中,针对之前收到的数据进行对应的编号,称为“TCP 的确认序号”

上面的 32 位序列号和确认序列号就是这种,由于序号是递增的,知道了第一个字节的序号,后续每一个字节的序号都能知道

假如 TCP 发送了的数据标记为了 1~1000,那么确认应答的序号应该是收到的数据最后一个字节序号的下一个序号,也就是1001,表示小于 1001 序号的数据都收到了

并且之后的六位标志位中的第二位(ack)就会设为 1(默认是0)

4.2. 丢包

丢包的原因:

  1. 数据传输过程中发生了 bit 翻转,收到这个数据的接收方/中间的路由器等,计算校验和发现不匹配,就会把当前数据包丢掉,不再交给应用层
  2. 数据传输到某个节点(路由器/交换机)时,当前节点负载过高,例如某个路由器单位时间内只能发送n 个包,但是遇到了高峰期,单位时间内需要发送的包超过了 n ,后续传输过来的数据就可能被路由器丢掉了

4.3. 超时重传

TCP 对抗丢包的方法:其实丢包是不可能避免的,TCP 感应到丢包之后就会再重新发一次数据,第二次再发生丢包的概率就会减小很多,TCP 感应丢包是通过应答报文来区分的,收到应答报文之后就说明没有丢包,没有收到应答报文就说明数据丢包了,但是也不能排除当时没收到后续收到了的情况,所以就需要设置一个时间限制,在时间限制内来判断是否丢包,不过还有一个特殊情况:

第一种就是正常的数据没有发送到丢包了,第二种是数据没有丢,但是 ack 丢了,不过无论是哪种情况都会认为是丢包并且进行数据重传,这时就会出现一个问题,第一种情况是没问题的,数据丢了重新传,但是第二种情况数据没有丢,再次发送就意味着主机2收到了两份同样的数据,如果是转账的请求,让你转两次账肯定也不合理

针对上述问题 TCP 也进行了处理,接收方会有一个接收缓冲区,收到的数据会先进入缓冲区中,后续再收到数据就会根据序号在缓冲区中找对应的位置,如果发现当前序号 1~1000 已经存在了,就会把新收到的数据丢弃了,以此来确保读取到的数据是唯一的

重传的时间设定:

这里的时间不是固定的,而是动态变化的,例如发送方第一次重传,超时时间为 t1,如果重传之后仍然没有 ack ,还是继续重传,第二次重传超时时间为 t2,,t2 是大于 t1 的,每多重传一次,超时时间的间隔就会变大

经过一次重传之后,就能让数据到达对方的概率显著提示,反之,如果重传几次都没有顺利到达,说明网络的丢包率已经达到了一个很大的程度

重传也不会无休止的进行,当重传到达一定次数的时候,TCP 就不会尝试重传了,就认为这个链接已经G了,此时先进行“重置/复位 连接”,发送一个特殊的数据包“复位报文”,如果网络恢复了,复位报文就会重置连接,使通信继续进行,如果网络还是有问题,复位报文没有得到回应,此时 TCP 就会单方面放弃连接

确认应答和超时重传这两个核心机制共同构建了 TCP 的“可靠传输机制”

 

在这里插入图片描述

标签:重传,编程,网络,TCP,校验,字节,连接,初始,客户端
From: https://blog.csdn.net/2202_76097976/article/details/142424183

相关文章

  • PoE三种标准:标准 PoE、PoE+、PoE++,网络工程师必知!
    你好,这里是网络技术联盟站,我是瑞哥。PoE(PoweroverEthernet)是一种通过网线同时传输数据和电力的技术,使得远程设备无需额外电缆便能获得电力供应。该技术在网络设备如VoIP电话、无线接入点和监控摄像头等应用中得到了广泛使用。随着网络设备的普及,PoE技术逐渐成为现......
  • 思科交换机命令大全,网络工程师必收藏!
    基本的命令行界面(CLI)导航思科交换机的CLI界面分为以下几种模式,每种模式提供不同的命令集:用户模式(UserEXECMode):此模式提供有限的查看命令,不能进行配置操作。用户模式的提示符通常以>结尾。例如:Switch>特权模式(PrivilegedEXECMode):此模式提供更多的监控和配置命......
  • 美食探索家:Spring Boot校园美食分享网络
    第四章系统实现4.1前台首页功能模块校园周边美食探索及分享平台,在系统首页可以查看首页、美食鉴赏、我的好友、个人中心、后台管理等内容,如图4-1所示。图4-1前台首页功能界面图用户登录、用户注册,在用户注册页面可以填写用户名、姓名、手机、邮箱、身份证等详细内容进......
  • k8s集群,master节点的初始化所用到的,init文件的分析,master节点的核心组件的作用,node节
    标准的k8s集群有三个组成部分管理控制节点、计算节点、私有镜像仓库。管理控制节点的功能:提供集群的控制对集群进行全局决策检测和响应集群事件管理控制节点中有四大核心服务服务端口含义用途APIServer6443api接口负责接收请求,实现功能Scheduler......
  • 【编程底层原理】彻底搞懂Spring是如何利用三级缓存来解决循环依赖问题的(一级缓存为
    一、整体推导思路为了彻底搞懂Spring是如何利用三级缓存来解决循环依赖问题的,要么去找三级缓存的设计者了解其设计的初衷,要么利用反推法来进行倒推(即一级缓存为啥不行,二级缓存为啥也不合适)。为了让大家能有一个更清晰的理解脉路,下面将先从反推法来介绍下一级缓存为啥不......
  • 哪个编程工具让你的工作效率翻倍?
     我觉得是对于我Androidstudio工具介绍作为一个Android开发者,AndroidStudio是我的首选编程工具,它极大地提高了我的工作效率。**功能特点:**1.**集成开发环境(IDE)**:AndroidStudio是一个全面的开发环境,它集成了代码编辑、调试、性能分析工具等。2.**智能代码补全**:利用......
  • Python 中的 HTTP 编程入门,如何使用 Requests 请求网络
    Python中的HTTP编程入门HTTP(超文本传输协议)是现代网络通信的基础,几乎所有的网络应用都依赖于HTTP协议进行数据交换。在Python中,处理HTTP请求和响应非常简单,可以通过内置的http模块或第三方库如requests来实现。本文将详细介绍Python中的HTTP编程,包括基本......
  • Python 中的 Socket 编程入门
    Python中的Socket编程入门Socket编程是网络编程的重要组成部分,允许计算机通过网络进行通信。在Python中,使用内置的socket模块,开发者可以轻松地实现客户端和服务器之间的交互。本文将详细介绍Python中的Socket编程,包括基本概念、常用操作、TCP和UDP通信的实......
  • 【编程基础知识】哪些行为算跨域,跨域会引发什么问题,怎么解决
    哪些行为算跨域(CORS,Cross-OriginResourceSharing)跨域是指浏览器在处理网页时,由于同源策略(Same-OriginPolicy)的限制,限制了来自与当前请求网页不同源的资源请求。这里的“源”(Origin)指的是协议、域名和端口的组合。以下是一些常见的跨域行为:不同域名的请求:从http://exa......
  • C语言内容函数大揭秘:轻松掌握,编程无忧(下)
    大家们好,废话不多说,我们接着继续来讲我们函数的章节。六.数组做函数参数在使用函数解决问题的时候,难免会将数组作为参数传递给函数,在函数内部对数组进行操作。比如:写一个函数将一个整型数组的内容,全部置为﹣1,再写一个函数打印数组的内容。1#include<stdio.h>23int......