【Java TCP/IP Soket】— UDP套接字的基本使用

一.UDP套接字

     UDP协议提供了一种不同与TCP协议的端到端服务。实际上UDP协议只实现了两个功能:

       (1)在IP协议的基础上添加了另一层地址(端口)

       (2)对数据传输过程中可能产生的数据进行了检测,并抛弃已经损坏的数据。

     UDP套接字具有与我们之前所看到的TCP套接字不同的特征。例如


二.UDP套接字与TCP套接字的区别

     1.连接方式不同

       TCP套接字与打电话相似,在进行通信前必须先建立连接,在连接关闭前,该套接字就只能与相连接的那个套接字通信;

        而UDP套接字与邮件相似,在进行通信前无需建立连接,在UDP套接字中每条消息(即数据报文)负载了自己的地址信息。

        在接收信息时,UDP套接字扮演的角色就像一个信箱,从不同地址发送来的信件和包裹都可以放到里面。 一旦被创建,

        UDP套接字就可以用来连续的向不同的地址发送信息,或从任何地址接收信息。

     2.消息边界处理方式不同

        UDP套接字与TCP套接字的另一个不同点在于它们对信息边界的处理方式不同,UDP套接字将保留边界信息,而TCP则不保留边界信息,

        这个特性使应用程序在接收信息时,从某些方面来说比使用TCP套接字更简单。

     3.安全性不同

        UDP套接字将尽可能的传送信息,但并不保证信息一定能成功到达目的地,而且信息到达的顺序与其发送顺序不一定一致(就像通过邮政

        部门寄信一样),因此,使用UDP套接字的程序必须准备好处理信息的丢失和重排;

   

      使用UDP套接字的原因:

      既然UDP套接字为程序带来这个多额外的负担,怎么还要使用它呢?原因有2个:

     (1)效率:如果应用程序只交换非常少量的数据,TCP连接的建立阶段就至少要传输其2倍的信息量(还有2倍的往返延迟时间);

     (2)灵活性:UDP套接字提供了一个最小开销平台来满足任何需求的实现;


三.UDP套接字的基本使用

     Java程序员通过DatagramPacket类和DatagramSocket类来使用UDP套接字。客户端和服务端都使用DatagramSocket来发送

     数据,使用DatagramPacket来接收数据。

     1.DatagramPacket类

        UDP终端交换的是一种称为数据报文的自包含信息。这种信息在Java中表示为DatagramPacket类的实例。发送信息时,Java

        程序创建一个包含了待发送信息的DatagramPacket实例,并将其作为参数传递给DatagramSocket类的send()方法。接收信息时,

        Java程序首先创建一个DatagramPacket实例,该实例中预先分配了一些空间(一个字节数组byte[]),并将接收到的信息存放在该空间

        中。然后把该实例作为参数传递给DatagramSocket类的receive()方法。

     

      2.UDP客户端

          UDP客户端首先向被动等待联接的服务器端发送一个数据报文。

          第一步:创建一个DatagramSocket实例;

          第二步:使用DatagramSocket类的send()和receive()方法来发送和接收DatagramPacket实例,进行通信;

          第三步:使用DatagramSocket类的close()方法来销毁该套接字;


UDP客户端类:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.util.Arrays;

public class UDPEchoClientTimeout
{

    // 最多重发次数
    private static final int MAXTRIES = 5;

    // 最大超时时间
    private static final int TIMEOUT = 3000;

    public static void main(String[] args) throws IOException
    {

        byte[] msg = new String("Hello job").getBytes();
        InetAddress serverAddress = InetAddress.getLocalHost();

        DatagramSocket socket = new DatagramSocket(); 
        //设置超时时间
        socket.setSoTimeout(TIMEOUT);

        DatagramPacket sendPacket = new DatagramPacket(msg, msg.length,
                serverAddress, 8850);

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

        boolean receivedResponse = false;
        int tries = 0;
        do
        {
            socket.send(sendPacket);
            try
            {

                socket.receive(receivePacket);

                // 判断发送与接收的数据报包是否不属于同一个IP地址
                if (!receivePacket.getAddress().equals(serverAddress))
                {
                    throw new IOException(
                            "Received pakcet from a unknown source");
                }

                // 用于标识数据包接收是否成功
                receivedResponse = true;
            }
            catch (SocketTimeoutException e)
            {
                tries += 1;
                System.out.println("Timed out," + (MAXTRIES - tries)
                        + "more tries......");
            }
        }
        while ((!receivedResponse) && (tries < MAXTRIES));

        if (receivedResponse)
        {
            /*
             * 返回的缓存数组的长度可能比接收的数据报文内部长度更长 (也就是说:在接收的byte[]数组缓冲区中,有可能
             * 没有被发送的数据报文消息填满)
             */
            byte[] receiveMsg = Arrays.copyOfRange(receivePacket.getData(),
                    0, receivePacket.getOffset()
                            + receivePacket.getLength());

            System.out.println("Client receive data:" + new String(receiveMsg));
        }

        socket.close();
    }
}

     3.UDP服务端

        UDP服务端的工作是建立一个通信终端,并被动等待客户端发起连接。

        第一步:创建一个DatagramSocket类实例,指定本地端口号,此时,服务器已经准备好从任何客户端接收数据报文;

        第二步:使用DatagramSocket类的receive()方法来接收一个DatagramPacket实例,当receive()方法返回时,数据报文就包含了

                        客户端的地址,这样我们就知道了回复信息应该发送到什么地方。

        第三步:使用DatagramSocket类的send()、receive()方法来发送和接收DatagramPacket实例,进行通信。


UDP服务端类:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.Arrays;

public class UDPEchoServerTimeout
{

    public static void main(String[] args) throws IOException
    {

        DatagramSocket socket = new DatagramSocket(8850);
        DatagramPacket packet = new DatagramPacket(new byte[255], 255);
        while (true)
        {
            /*
             * 因为在DatagramPacket.setData()的内部,是将msg(字节数组)赋值给buff(数据缓冲区),
             * 这样的话两个字节数组就会指向同一个内存地址的引用
             * ,如果我们把buff(数据缓冲区)中的内容改变了,相应的msg(字节数组)内容也会改变,
             * 所以,每次接收一个请求都必须要实例化一次要发送的字节消息。
             */
            byte[] msg = new String("Hello sone").getBytes();
            socket.receive(packet);

            System.out.println("Handing client at"
                    + packet.getAddress().getHostAddress() + " on port:"
                    + packet.getPort());

            byte[] receiveMsg = Arrays.copyOfRange(packet.getData(), 0,
                    packet.getLength());

            System.out.println("Server receive data:" + new String(receiveMsg));

            // 设置数据报包的数据缓冲区
            packet.setData(msg, 0, msg.length);

            socket.send(packet);
        }
    }
}

     4.关于DatagramSocket构造函数

           DatagramSocket():构造数据报套接字并将其绑定到本地主机上任何可用的端口,通常用于客户端编程;

           DatagramSocket(int port):创建数据报套接字并将其绑定到本地主机上的指定端口,通常用于服务端编程;

           DatagramSocket(int port, InetAddress localAddr):创建数据报套接字,将其绑定到指定的本地地址。通常用于

           一台主机上拥有多个IP地址的时,可以设置要绑定的本地IP地址;


四.使用UDP套接字发送和接收信息时注意点

   1.消息为什么会丢失

      UDP套接字保留了消息边界,所以DatagramSocket的每一次receive()的调用最多只接收一次send()方法所发送的数据。但是,

      需要注意的是,一个UDP套接字所接收的数据存放在一个消息队列中,每个消息都关联了其源地址信息。每次receive()调用只

      返回一条消息。然而,如果receive( )方法传入的数据报包的数据缓冲区长度为n,而接收队列中第一条消息长度大于n,则receive()

      方法直返回这条信息的前n个字节。超出部分的其他字节都将自动被丢弃,并且也没有任何消息丢失的提示!


      (通俗的说就是:当receive( )传入的数据报包的数据缓冲区长度为n,如果接收的消息长度大于n时,那么receive()方法只返回这条消息的前n个字节,

       超出部分将自动的被丢弃,并没有任何提示)


      出于这个原因,接收者应该提供一个有足够大的数据缓冲区的DatagramPacket实例,当调用receive( )方法时以完整的接收所允许的最大长度消息。

      一个DatagramPacket实例中所运行传输的最大数据量为65507个字节(即UDP数据报文包所能负载的最多数据)。


  2.如何才能正确的接收消息

      UDP其中有最重要的一点,就是每一个DatagramPacket实例都包含一个内部消息长度值,而该实例只要一接收到新消息,这个内部消息长度值就会改变

      (用来反映实际接收的消息字节数)。如果一个应用程序使用同一个DatagramPacket实例并多次调用receive( )方法,每次调用前必须将内部消息长度重置为

      数据缓冲区的实际长度(即调用DatagramPacket.setLength( )方法,这里好像如果不重置也没有什么影响)。

     

      对于新手来说一个潜在的问题就是:DatagramPacket类的getData( )方法,该方法总是返回缓冲区的原始大小,忽略了实际数据的内部偏移量和内部长度信息。

      例如,假设buf是一个长度为20的字节数据,其在初始化时已使每个字节中存放了该字节在数组中的索引:

     

      同时假设dg是一个DatagramPacket实例,我们将dg的缓冲区设置为buf数组中间10个字节:

      dg.setData(buf,5,10);

      假设dgSocket是一个DatagramSocket的实例,某人向dgSocket发送了一个包含以下内容的8个字节信息:

     

      此时调用dg.getData( )方法将返回buf字节数组的原始引用,其内容变为:

    

     大家可以看到buf数组中只有索引5~12的字节被修改,一般而言,我们需要同时使用getOffset( )和getData( )方法来访问刚接收到的数据,我们可以将

     接收到的数据复制到一个单独的字节数组中。

     在Java 1.6中,我们可以使用:

     byte[] receive = Array.copyOfRange(dg.getData( ) , dg.getOffset( ), dg.getOffset( )+dg.getLength( ) )

   

     以下是我写的一个关于正确接收UDP套接字消息的例子:


客户端类:    

public class RealMsgClient
{
    public static void main(String[] args) throws IOException
    {
        //发送的消息字节
        byte[] msg = new String("1234").getBytes();
        InetAddress inetAddr = InetAddress.getLocalHost();

        DatagramSocket client = new DatagramSocket();
        DatagramPacket sendPacket = new DatagramPacket(msg, msg.length,
                inetAddr, 8888);

        client.send(sendPacket);
    }
}

服务端类:

public class RealMsgServer
{
    public static void main(String[] args) throws IOException
    {
        byte[] msg = new String("ABCDEFGHIG").getBytes();

        DatagramSocket server = new DatagramSocket(8888);
        DatagramPacket receivePacket = new DatagramPacket(msg, msg.length);

        // 设置接收数据报包的缓冲区
        receivePacket.setData(msg, 3, 4);
        server.receive(receivePacket);

        /*
         * 场景一: 使用错误的方式接收数据
         * 原因:getData()方法忽略了消息内部偏移量(offset)和内部长度(length)
         */
        byte[] receiveByte1 = receivePacket.getData();

        /*
         * 场景二: 使用正确的方式接收数据
         */
        byte[] receiveByte2 = Arrays.copyOfRange(receivePacket.getData(),
                receivePacket.getOffset(), receivePacket.getOffset()
                        + receivePacket.getLength());

        System.out.println("使用错误的方式接收数据的结果:"+new String(receiveByte1));
        System.out.println("使用正确的方式接收数据的结果:"+new String(receiveByte2));

    }
}

以上就是我对TCP和UDP的一些总结。总体来说,唯一遗憾的就是:关于UPD重置内部消息长度问题,我自己实验了以下,感觉不重置也没有什么问题,如果有知道的同学,可以给我留言,谢谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值