认识UDP、TCP协议

news2024/10/5 14:00:49


一、Socket

       首先,我们需要了解一下socket。

       在上一篇文章当中,我们了解了TCP-IP五层协议模型初识网络:IP、端口、网络协议、TCP-IP五层模型_革凡成圣211的博客-CSDN博客TCP/IP五层协议详解https://blog.csdn.net/weixin_56738054/article/details/128666970?spm=1001.2014.3001.5502

        在这篇文章当中,可以得知,应用层是面向客户的层面。如果发起网络通信,那么应用层需要向传输层发送应用层报文。

       那么,应用层如果想给传输层发送报文,那么就一定需要调用操作系统提供的一些api,那么,建立起应用层和传输层之间的联系的这些api,就是socket。严格意义上面来说,这些socket的api属于传输层。

TCP、UPD协议就是socket的api提供的两种风格。 


 二、TCP、UDP协议的区别

 UPDP协议,概括一下,就是以下几个特点:无连接、不可靠传输、面向数据报、全双工。

 TCP协议,概括一下,就是以下几个特点:有连接、可靠传输、面向字节流、全双工。


有连接&无连接

       这里,想举一个比较形象的例子来说明:

       发短信、发微信这一类的通信、我们通常视为无连接的网络通信。

       原因:当我们发送一条消息的时候,仅仅只负责发出,无论接收方是否收得到、有无作出回应,都会照样发送。不确保接收方是否接收到消息

       而打电话这样的连接方式,就是属于典型的有连接。为什么呢?因为打电话的时候,一定要双方都可以接听电话,才可以互传信息。确保接收方会接收到消息

       总结一下:UDP协议当中,发送方和接收方的运输层进程之间没有建立"握手",只负责把应用层的协议打包成UDP报文段然后发送,不关注接收方是否能接收到需要发送的信息,因此,UDP协议被视为无连接的协议。

       而TCP协议,在开始传输数据之前,需要经过三次握手,确保接收方一定可以接收到消息,因此TCP协议是有连接的协议


可靠传输&不可靠传输

      所谓的可靠性与不可靠性,实际上就是发送方是否得知接收方已经接收到需要发送的信息。

      例如:在微信,QQ通话当中,发送方仅仅负责发送,但是它无法得知接收方是否已经接收到消息,这种情况下面,我们就认为协议是不可靠的。

      如果发送消息的时候,新增了"已读不回"等类似的功能,确定接收方已经接收到发送方传输的内容,说明协议是可靠的。


 面向数据报&面向字节流

       UDP协议,是面向数据报的协议。

       在上一篇文章当中,我们提到了,传输层协议是以"数据报"为基本单位进行传输的,操作系统不会对消息进行拆分。也就是,直接把应用层传输过来的报文打包成UPD数据段,然后传输到网络层。

       TCP协议,是面向字节流的协议。

       TCP把数据看成一个无结构,但是有序的字节流。


       当使用TCP协议进行传输的时候,一条应用层消息可能会被操作系统分组成多个TCP报文。也就是一个完整的应用层传输过来的一句话,有可能会被操作系统拆分成多个TCP数据段,也有可能仅仅是一个数据段。

       例如应用层打算发送Hi This is JIM这一条消息到传输层的时候,有可能这一条消息被拆分成两个TCP段:

       

也有可能仅仅是一个TCP段:


 而对于UDP协议,不会对消息进行拆分:


 全双工

       一个通信通道,可以双向传输(既可以发送,又可以接收)

       我们常见的高速公路,一般都是双向六车道,或者双向八车道这样的类型的。这个场景就可以理解为全双工,同时支持发送和接收。

       而半双工,可以理解为不支持发送和接收信号同时进行。例如青藏铁路,因为施工难度极大,并且当时修建铁路的条件有限,因此建立的场景为单向通行的。可以理解为"半双工"。


三、Java当中对于传输层的一些api

     ①DatagramSocket

       在操作系统当中,一切皆为文件。

       使用这个类,表示一个socket对象。在操作系统当中,也是把这个socket当成一个文件来处理的。相当于进程的文件描述符表上面的某一项。

       普通的文件对应的硬件设备为硬盘,而socket文件,对应的硬件设备为网卡

       一个socket对象,就可以和另外一个主机进行通信了;如果需要和多个主机通信,就需要使用多个socket对象。


      构造方法:

DatagramSocket()DatagramSocket(int port)

      如果没有指定端口(左图),那么系统会自动分配一个没有被占用的端口。

      如果指定了端口(右图),那么就会把socket和对应的端口关联起来。


      send/receive方法:

void send(DatagramPacket packet)void receive(DatagramPacket packet)

      这两个方法,分别代表socket发送、接收应用层的报文的方法。

      其中,需要发送/接收的DatagramPackett就是一个应用层报文。


      close()方法

      用于关闭文件描述符表项,释放进程当中的文件描述符表项所占用的空间。


     ②DatagramPacket

      表述udp当中传输的一个应用层报文。构造这个对象,可以指定一些具体的数据进去。

      构造方法:

对应方法方法说明
DatagramPacket(byte[] buf,int length)把buf数组作为地址
DatagramPacket(byte[] buf,int offset,int length,SocketAddress)把buf数组作为地址,并且指定了需要传输的目标主机IP和端口号

 四、实现一个UDP客户端-服务端的代码

        编写之前,我们首先需要做一个假设,此处的UDP的客户端,是运行在客户的手中的,也就是访问某个网站/app的普通用户。

         而服务端,是运行在程序员的电脑当中的


 UDP服务端

       首先,需要明确服务端是需要做什么的。

       步骤①:读取客户端的响应

       步骤②:  根据请求计算响应

       步骤③:  把响应返回给客户端


       需要指定的属性:DatagramSocket socket;用于为客户端提供socket来接收应用层传输来的应用层报文。

       构造方法当中初始化socket对象,并且指定本机当中需要建立通信的端口号

       需要注意的是,应用层与传输层建立连接的时候,一定要指定socket的端口号,如果不指定,选用无参数的构造方法,那样就无法明确UDP与哪个需要联系的应用层端口建立联系,无法正常通信。

/**
 * udp服务端
 * @author 25043
 */
public class UdpEchoServer {

    /**
     * 与应用层建立联系的datagramSocket对象
     */
    private DatagramSocket datagramSocket;
    /**
     * 服务器当中一定要关联一个端口号
     * 端口号@param port
     * 异常@throws SocketException
     */
    public UdpEchoServer(int port) throws SocketException {
        //构造一个对象
        //让这个socket和客户端主机当中的进程建立联系
        //port就是对应客户端主机的进程id
        datagramSocket=new DatagramSocket(port);
    }

UDP服务端代码编写

步骤1、

创建应用层报文对象(DatagramSocket receiveSocket)并且在构造方法当中指定一个字节类型的数组,来存储客户端发送来的应用层报文的信息。

DatagramPacket receivePacket=new DatagramPacket(new byte[4096],4096);

 步骤2、

       使用socket来接收这个报文(reveive方法),并且为这个字节类型的数组填充信息。其中,下划线的部分,是在reveive方法内部进行填充的。

datagramSocket.receive(receivePacket);

  下面,图解一下这个receive方法:

         此处,数据经过层层分用,到达了udp传输层协议。

         当应用程序调用receive方法的时候,相当于执行到了内核udp相关的代码。

         前面的文章当中提到了,udp报文到达传输层的时候,会把udp数据段当中的载荷(也就是去掉udp数据报头之后的内容)取出来,取出来的是应用层的数据,存放到byte数组当中。


         需要注意的是,如果此时服务端没有收到客户端发送过来的数据,那么程序就会在receive方法处阻塞等待。这种阻塞等待,类似于Scanner的等待,就是"等待IO"。


 步骤3、

截取receivePacket当中实际的应用层报文的实际字节数组的长度(调用receivePacket.getData()方法

 //截取到实际数据,例如应用层发来的"hello"被存放在byte数组当中的时候,可能仅仅占用了
 //一点点的空间,因此截取的实际长度为"hello"字节数组的长度
 // packet.getLength()
 //获取到数据报的实际长度部分
 String request=new String(receivePacket.getData(),0, receivePacket.getLength());

步骤4、

调用process方法,模拟客户端对报文作出响应。假设返回一个新的字符串(response)

 //这里模拟一下回显服务器
 String response=process(request);


  public String process(String request){
     return "udp服务端已经响应:"+request;
  }

步骤5、

再次构造应用层报文对象(DatagramSocket responseSocket),在构造方法当中,指定需要返回给客户端的信息,包括:

         ①response转化为的字节数组

         ②字节数组的实际长度

         ③应用层的IP地址、端口号信息(避免客户端发错响应)

//把需要回应的字符串转化为字节数组
byte[] receiveBytes=response.getBytes();
//获取到这个字节数组的长度
int receiveLength=receiveBytes.length;
//获取到对应客户端的IP和端口号(SocketAddress)
SocketAddress address =receivePacket.getSocketAddress();
//构造返回给客户端的socket对象
DatagramPacket responsePacket=new DatagramPacket(receiveBytes,receiveLength,address);

步骤6、

把responseSocket发送给客户端

//发送给应用层
datagramSocket.send(responsePacket);

步骤7、 

启动服务端

(由于此时还没有客户端给服务端发送数据,因此服务端会在receive方法这里阻塞等待)

  此时指定服务端的端口号为9090。

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

 整体服务端代码:

/**
 * udp服务端
 * @author 25043
 */
public class UdpEchoServer {

    /**
     * 与应用层建立联系的datagramSocket对象
     */
    private DatagramSocket datagramSocket;
    /**
     * 服务器当中一定要关联一个端口号
     * 端口号@param port
     * 异常@throws SocketException
     */
    public UdpEchoServer(int port) throws SocketException {
        //构造一个对象
        //让这个socket和客户端主机当中的进程建立联系
        //port就是对应服务端主机的进程端口号
        datagramSocket=new DatagramSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true){
            //用于接收应用层的内容的字节数组一定要长度足够长
            DatagramPacket receivePacket=new DatagramPacket(new byte[4096],4096);
            datagramSocket.receive(receivePacket);
            //截取到实际数据,例如应用层发来的"hello"被存放在byte数组当中的时候,可能仅仅占用了
            //一点点的空间,因此截取的实际长度为"hello"字节数组的长度
            // packet.getLength()
            //获取到数据报的实际长度部分
            String request=new String(receivePacket.getData(),0, receivePacket.getLength());
            //这里模拟一下回显服务器
            String response=process(request);

            //把需要回应的字符串转化为字节数组
            byte[] receiveBytes=response.getBytes();
            //获取到这个字节数组的长度
            int receiveLength=receiveBytes.length;
            //获取到对应客户端的IP和端口号(SocketAddress)
            SocketAddress address =receivePacket.getSocketAddress();
            //构造返回给客户端的socket对象
            DatagramPacket responsePacket=new DatagramPacket(receiveBytes,receiveLength,address);
            //发送客户端
            datagramSocket.send(responsePacket);
            //输出处理结果
            System.out.println("客户端IP:"+
                    receivePacket.getAddress()+
                    ";客户端端口:"
                    +receivePacket.getPort());
        }
    }

    public String process(String request){
        return "服务端已经响应:"+request;
    }
}

启动服务端

 可以看到,服务端一直在reveive方法处阻塞等待,等待客户端发送数据:


UDP客户端 

       客户端主要负责的工作就是和服务端建立通信,并且为服务端的receive方法内部输送数据(DatagramPacket),等待服务端的send方法发送数据(DatagramPacket)并且作出回应。


       客户端的参数

       在客户端服务器当中初始化socket对象。

       既然要发送消息,那么肯定需要知道服务端的端口号+Ip地址 

/**
 * UDP客户端
 * @author 25043
 */
public class UdpEchoClient {

    /**
     *与服务端建立联系的socket
     */
   private DatagramSocket socket;

    /**
     * 服务端的ip
     */
   private String serverIp;

    /**
     * 服务端的端口号
     */
   private int serverPort;

    /**
     * 指定服务器的ip以及端口
     * 服务器的ip@param serverIp
     * 服务器的端口i@param serverPort
     * 抛出的异常@throws SocketException
     */
   public UdpEchoClient(String serverIp,int serverPort) throws SocketException {
       socket=new DatagramSocket();
       this.serverIp=serverIp;
       this.serverPort=serverPort;
   }

}

 如果客户端给服务端发送一次请求,

 那么源ip就是客户端ip,源端口号就是客户端的端口;

 目的ip就是服务端的ip地址,目的端口号就是服务端的端口号。

        现在,我们已知的情况就是:

        由于此时客户端和服务端都运行在本机上面,因此客户端服务端ip地址都是127.0.0.1。

        启动服务端的的时候,我们已经指定了socket的端口号为9090.那么服务端的进程端口号就是9090.

     可以看到,在这里的客户端,没有指定端口号。

      虽然没有指定,但是我们还是可以了解到一些默认的信息:

     一次通信当中,涉及到的ip和端口号有两组。

      源ip、源端口号。

     目的ip、目的端口号。


       在前面,我们提到的知识当中,如果没有指定端口号,那么在初始化客户端的socket的时候,会被操作系统默认绑定一个没有被其他进程占用的端口号。

      为什么客户端不需要指定一个特定的端口号呢?

      原因就是,如果客户端在编写代码的时候指定了一个端口号,那么这个端口号如果此时被电脑上面的其他进程占用了,就无法正常和服务端通信。

      会抛出一个异常,无法占用端口的异常。


      既然客户端不用指定一个特定的端口号,但是为什么服务端需要指定一个特定的端口号呢?

       客户端,是不可控的。

       客户端,在实际的应用场景当中,是运行在客户的电脑/手机当中的,也许运行着许许多多的应用程序程序(进程),并且哪些进程运行多久,这些完全取决于用户,而不取决于程序员。

       因此无法确定哪个客户端端口什么时间被占用,也就不好指定客户端的端口号。

       既然这样,那就不如让操作系统随机为客户端分配一个空闲的进程(端口号)就好了。


       而服务端,是可控的。

       服务端是运行在程序员的电脑当中的,

       因此,服务端进程占用哪些端口,这些是可以被程序员自己控制的,程序员可以在编写服务端代码的时候,手动控制端口的占用情况。

       如果不设置服务端的端口,那么操作系统会随机为服务端分配一个端口,这样,反而提高了程序员管理代码的难度。


      启动客户端(start方法)

       步骤1、

       从控制台获取用户的输入:

 System.out.println("客户您好,请输入您想向服务器发送的内容:");
           String request=input.next();

       步骤2、构造UDP请求

       此处的DatagramPacket需要包含request转化的byte[]数组及其长度

       还需要包含服务端的IP、端口号。

      

//构造UDP请求
           //转化为字节数组
           byte[] requestBytes=request.getBytes();
           int length=requestBytes.length;
           //指定服务端的Ip以及端口号
           DatagramPacket requestPacket=new DatagramPacket(requestBytes,
                   length,
                   InetAddress.getByName(serverIp),serverPort);

       步骤3、发送数据包(DatagramPacket)

       到服务端的receive方法当中:

//发送到服务端的receive方法当中
           socket.send(requestPacket);

       步骤4、构造DatagramPacket并且读取服务器的响应

        此处,将要初始化一定长度的空的字节数组,然后传入到receive方法当中,等待服务器为这个传入的数组填充内容。

 //读取服务器的UDP响应
           DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);
           //接收服务端的响应
           socket.receive(responsePacket);

        步骤5、构造响应字符串

//构造响应的字符串
           String response=new String(responsePacket.getData(),0,responsePacket.getLength());
           //输出响应的字符串
           System.out.println(response);

下面,图解一下客户端&服务端通信的整个流程:

 服务端(先启动):

 客户端(后启动)

 启动服务器


客户端启动:

 可以看到,在启动了服务端、客户端之后,客户端已经收到了服务端的响应了。

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

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

相关文章

Crack:RadiAnt DICOM Viewer 2023.1 BETA #1300

RadiAnt DICOM Viewer 2023.1 BETA #1300 built on January 13, 2023 New features: Length ratio calculation. Ellipsoid / bullet volume calculation. Added option to color and/or pin specific items to top in the DICOM tags window. 多式DICOM的技术支持 该软件能够打…

是时候分享一波jenkins centos的安装了

1、下载注意:至少安装2.319的版本,否则插件安装失败,2.357 之后版本需要java11,请注意java版本a、开始下载,利用华为云地址 https://mirrors.huaweicloud.com/home,速度杠杠快搜索jenkins,点击j…

api接口对接如何实现,php如何对接api

这篇文章来分享下api接口对接如何实现,还有源码,希望对新手有所帮助。 什么是API? 我的回答:API( 应用程序编程接口):一般来说,这是一套明确定义的各种软件组件之间的通信方法。 什么是API&…

Android数据库之SharedPreferences、SQLite、Room

文章目录一、SharedPreferences二、SQLite三、Room使用Room进行增删改查Room数据库升级一、SharedPreferences 要想使用SharePreferences来存储数据,首先需要获取到SharedPreferences对象。Android中提供了三种方法用于得到SharedPreferences对象 1.Context类中的g…

20230119英语学习

Back to the Future 在故宫修钟表是种什么样的体验? After a year of complex restoration, specialists from the Palace Museum in Beijing have given a pair of antique pagoda clock automata a new lease on life. In the form of a nine-tiered pagoda, th…

单片机寄存器

单片机寄存器简述 1、单片机寄存器就是单片机片内存储器(片内RAM)一部分,每一个都有地址。只不过这几个寄存器有特殊的作用,比如指令:MUL AB,这条指令用到两个寄存器A,B进行乘法,结果存到BA里面,这条指令必…

Linux基本功系列之type命令实战

文章目录一. type命令介绍二. 语法格式及常用选项三. 参考案例3.1 查看别名3.2 查看是否是内建命令3.3 查看是否为关键字3.4 显示所有命令的位置3.5 判断当前命令是否为alias或者keyword等总结前言🚀🚀🚀 想要学好Linux,命令是基本…

4-2指令系统-指令的寻址方式

文章目录一.指令寻址1.顺序寻址2.跳跃寻址二.数据寻址1.隐含寻址2.立即(数)寻址3.直接寻址4.间接寻址5.寄存器寻址6.寄存器间接寻址7.相对寻址(程序浮动、转移指令)8.基址寻址(多道程序)9.变址寻址&#xf…

移动web字体图标

字体图标下载字体图标使用字体图标使用类名引入字体图标使用unicode编码(了解)在线字体图标使用伪元素字体图标小结下载字体图标 具体的步骤&#xff1a; 使用字体图标 引入相关文件 复制相关的文件&#xff0c;到 fonts文件夹里面。 引入 css <link rel"styleshe…

回溯法复习(总结篇)

根据课本上的学习要点梳理&#xff0c;“通用解题法”&#xff0c;可以系统的搜索一个问题的所有解、任一解&#xff0c;他是一个既带有系统性&#xff08;暴力遍历&#xff09;又带有跳跃性&#xff08;剪枝&#xff09;的搜索算法。 理解回溯法和深度优先搜索策略 回溯的本质…

Kafka入门与核心概念

前言在我们开发过程中&#xff0c;有一些业务功能比较耗时&#xff0c;但是又不是很重要的核心功能&#xff0c;最典型的场景就是注册用户以后发送激活邮件分为两步1&#xff1a;向数据库插入一条数据2&#xff1a;向注册用户发送邮件第2步其实并不是核心功能&#xff0c;但是发…

SpringMVC-拦截器

1&#xff0c;pringMVC-拦截器 对于拦截器这节的知识&#xff0c;我们需要学习如下内容: 拦截器概念入门案例拦截器参数拦截器工作流程分析 1.1 拦截器概念 讲解拦截器的概念之前&#xff0c;我们先看一张图: (1)浏览器发送一个请求会先到Tomcat的web服务器 (2)Tomcat服务…

字节青训前端笔记 | 响应式系统与 React

本节课为前端框架 React 的基础课程讲解 React的设计思路 UI编程的特点 状态更新的时候&#xff0c;UI不会自动更新&#xff0c;需要手动调用DOM接口进行更新欠缺基本的代码层面的封装和隔离&#xff0c;代码层面没有组件化UI之间的数据依赖关系&#xff0c;需要手动维护&am…

【程序人生 | 价值扳机】你的寒假自律第一步(建议收藏)

&#x1f482;作者简介&#xff1a; THUNDER王&#xff0c;一名热爱财税和SAP ABAP编程以及热爱分享的博主。目前于江西师范大学会计专业大二本科在读&#xff0c;阿里云社区专家博主&#xff0c;华为云社区云享专家&#xff0c;CSDN SAP应用技术领域新兴创作者。   在学习工…

小技巧:Excel顽固的名称、引用冲突的解决

共享编辑、跨文档引用的不便 Excel 的共享文档多人编辑、跨文档引用等功能似乎很美好&#xff0c;实际用下来很成问题。 首先共享文档虽然用约定按标注责任人的方式由不同的人编辑不同的行避免的多人同时编辑一行的冲突&#xff0c;但是这通常是个大文件&#xff0c;经常(大概…

函数 C语言】

函数的声明和定义 函数间调用关系是&#xff0c;由于函数调用其他函数&#xff0c;替他函数也可以互相调用&#xff0c;同一个函数可以被一个或多个函数调用任意次。 先声明&#xff0c;后调用。 #include <stdio.h> //去标准库下找文件 #include "stdio.h"…

分件操作和IO

1.文件的路径如图:当我们打开此电脑后(windows系统),上面会显示我们当前的位置&#xff0c;点击之后会出现如图片中的一段字符&#xff0c;这段字符代表着当前你所处位置的路径。最开头的D&#xff1a;/d&#xff1a;是盘符&#xff0c;后面会用斜杠‘/’或者反斜杠‘\’分开,该…

MFC的使用

1.初使用的简单代码该程序包含两个代码 头文件mfc.h和mfc.cpp文件头文件mfc.h#pragma once #include<afxwin.h>class MyApp:public CWinApp//CWinApp应用程序类 { public://程序入口virtual BOOL InitInstance(); }; class MyFrame : public CFrameWnd//继承窗口框架类a …

Java_Git:2. 使用git管理文件版本

目录 1 创建版本库 1.1 使用GitBash 1.2 使用TortoiseGit 2 添加文件 2.1 添加文件过程 2.2 工作区和暂存区 3 修改文件 3.1 提交修改 3.2 查看修改历史 3.3 差异比较 3.4 还原修改 4 删除文件 5 案例&#xff1a;将java工程提交到版本库 5.1 复制文件到工作目录 …

论文阅读:Boosting 3D Object Detection by Simulating Multimodality on Point Clouds

Boosting 3D Object Detection by Simulating Multimodality on Point CloudsResponse DistillationSparse-Voxel DistillationVoxel-to-Point DistillationInstance DistillationlossExperiments稠密&#xff08;多模态、多帧&#xff09;信息->稀疏&#xff08;单模态点云…