java 网络编程 UDP协议

UDP协议

英文:User Datagram Protocol即:用户数据报协议。
不可靠,传输少量的数据(限制在64KB下),效率高,在两端建立socket(负责发送和接收,无服务端和客户端的概念),位于传输层,而IP为网络层

使用场景:网络游戏,视频会议,实时性高的情况。
主要作用:完成网络数据流和数据报之间的转换。在信息的发送端,UDP协议将网络数据流封装成数据报,然后将数据报发送出去,接收端为逆过程。

传输层和网络层有什么区别呢?

网络层(IP层)提供点到点的连接即提供主机之间的逻辑通信,传输层提供端到端的连接——提供进程之间的逻辑通信。

那上面的端对端,即:什么是端口呢

为了使运行不同操作系统的计算机的应用进程能够互相通信,就必须用统一的方法对TCP/IP体系的应用进程进行标志。
解决这个问题的方法就是在运输层使用协议端口号(protocol port number),或通常简称为端口(port)

简单理解,IP的地址负责的是点到点,而仅仅通过IP来连接到对方的电脑是不够的,因为电脑中要很多的应用程序,到底是将数据传输给那个应用程序呢?我们还要需要端口(TCP/IP)来区分是哪一个应用程序。两者结合达到网络通信。

与TCP协议的比较

TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序。

UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。

java是怎么使用UDP协议达到网络通信的呢?

java使用DaatagramSocket代表UDP协议的socket,它本身只是码头,只负责接收和发送数据报,使用DatagramPacket来代表数据报。发送的数据通过这个对象进行完成。
DatagramPacket来决定数据报的目的地
实际上,还是可以区分出服务端和客户端的,下面展示服务端与客户端和客户端与客户端的传输。

服务端与客户端

TCPsocket中,我们需要建立连接后再进行传输,所以有明显的服务端和客户端之分。而UDP则不同,不需要建立连接,直接往目标进行发送数据。但还是可以人为的指定好客户端和服务端的。服务端的特性是有固定的IP和端口
下面展示用法:
服务端

public class UDPServer {
    public static final int PORT=30000;
    //定义每个数据报的最大的大小为4KB
    public static final int DATA_LEN=4096;
    //定义接受网络数据的字节数组
    byte[] inBuff=new byte[DATA_LEN];
    //已指定字节数组创建准备接受数据的DatagramPacket对象
    private DatagramPacket inPacket=new DatagramPacket(inBuff, inBuff.length);
    //定义发送的DatagramPacket对象
    private DatagramPacket outPacket;
    //定义一个字符串数组,服务器发送该数组的元素
    String[] book=new String[]{"I","am","Stu"};
    public void init(){
        try {
            //创建datagramsocket对象
            DatagramSocket socket=new DatagramSocket(PORT);
            {
                //采用循环接受数据
                for(int i=0;i<1000;i++){
                    //读取inPacket的数据
                    socket.receive(inPacket);
                    //判断getData()和inbuf是否为同一个数组
                    System.out.println(inPacket.getData()==inBuff);
                    System.out.println(socket.getSoTimeout());
                    //将接受后的内容转化为字符串进行输出
                    System.out.println(new String(inBuff,0,inPacket.getLength()));
                    //从字符串中取出一个元素作为发送数据
                    byte[] sendData=book[i%4].getBytes();
                    //已指定的字符数组作为发送数据,以刚接手到的datagramPacket的
                    //源socketAddress作为目标socketAddress创建DatagramPacket
                    outPacket=new DatagramPacket(sendData,sendData.length, inPacket.getSocketAddress());//通过这个getSocketAddress就可以得到相应的IP地址和端口了
                    //发送数据
                    socket.send(outPacket);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        new UDPServer().init();
    }
}

相应的客户端:

public class UdpClient {
    // 定义数据报的目的地
    public static final int DEST_PORT = 30000;
    public static final String DEST_IP = "127.0.0.1";
    // 定义每个数据报的大小,最大为4kb
    ...//更UdpServer一样相应成员的声明
    public void init(){
        try {
            //创建一个客户端DatagramSocket使用随机端口
            DatagramSocket socket=new DatagramSocket();
            outPacket=new DatagramPacket(new byte[0], 0,InetAddress.getByName(DEST_IP),DEST_PORT);
            //创建键盘输入流
            Scanner scan=new Scanner(System.in);
            //不断读取键盘输入
            while(scan.hasNextLine()){
                //将键盘输入的一行字符串转换字节数组
                byte[] buff=scan.nextLine().getBytes();
                //设置发送用到的DatagramPacket中的字节数据
                outPacket.setData(buff);
                //发送数据报
                socket.send(outPacket);
                //读取Socket中的数据,读到的数据放在inPacket所封装的字节数组中
                socket.receive(inPacket);
                System.out.println(new String(inBuff,0,inPacket.getLength()));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        new UdpClient().init();
    }
}

通过以上的代码,我们可以总结以下规律,即:使用步骤

  1. 定义好IP地址和端口,以及字节数组的大小(最终以此形式传输)并初始化输入的DatagramPacket
  2. 创建DatagramSocket实例(监听端口,S/C都各自不同)
  3. S和C都有所不同
    3.1 S则先进行socket.receive(inPacket);
    3.2 而C则创建outPacket(IP地址和端口,字节数组),并使用outPacket.setData(buff);将需要发送的数据进行设置,再使用socket.send(outPacket);进行发送
  4. 当发送后的就进行等待接受,接受完的进行处理并发送的一个循环里面,这优点像进程间通信的情况

使用当中的注意点

  1. 一定要注意DatagramPacket的设置,因为它指定发送给哪一个应用程序,通过接受的数据报可等到相应的ip地址和端口
  2. socket.receive(inPacket);是阻塞的。必要时需要对应启动一个线程。

针对注意的第一点,我们看客户端对客户端的传输

其实很简单,我们只要对每个客户端的DatagramSocket监听不同的端口,而使用DatagramPacket指定要传输的端口和IP地址即可。我们只需要对上面的类UdpClient进行修改如下:
第一个客户端

DatagramSocket socket=new  DatagramSocket(30001);//添加端口,而不是随机端口,
outPacket=new  DatagramPacket(new  byte[0],0,InetAddress.getByName(DEST_IP),30000);//将要传输放的端口和ip设置

第二个客户端则相反即可

DatagramSocket socket=new  DatagramSocket(30000);//添加端口,而不是随机端口,
outPacket=new  DatagramPacket(new  byte[0],0,InetAddress.getByName(DEST_IP),30001);//将要传输放的端口和ip设置

针对注意第二点,当socket.receive(inPacket);如何不阻塞主线程

下面我对相应的代码进行了封装和优化,并让类实现Runnable接口

public class UdpClient implements Runnable {
    // 定义数据报的目的地
    public static final int DEST_PORT = 30001;
    public static final String DEST_IP = "127.0.0.1";
    ...//相应成员的声明

    public UdpClient() {
        try {
            // 创建一个客户端DatagramSocket使用随机端口
            socket = new DatagramSocket(DEST_PORT);
            // 初始化发送数据报,包含一个长度为0的字节数组
            outPacket = new DatagramPacket(new byte[0], 0,
                    InetAddress.getByName(DEST_IP), 30000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 发送字符串坐标strxy,用于使用ai先手的情况
     *
     * @param strXY
     */
    public void sendPointXy(String strXY) {
        byte[] buff = strXY.getBytes();
        outPacket.setData(buff);// 设置数据报的字节数据
        try {
            // 发送数据报
            socket.send(outPacket);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 进行接受坐标strXY,并进行处理进行下棋,最后进行发送
     * 
     * @param strXY
     */
    public void receiverPointXy(String content) {
        String[] strXY = content.split(",");
        ...//对数据进行处理
        sendPointXy(chComputer.getX() + "," + chComputer.getY());
    }

    /**
     * 成为被动接受方
     */
    public void acceptPointXy() {
        try {
            socket.receive(inPacket);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        receiverPointXy(new String(inBuff, 0, inPacket.getLength()));
    }

    @Override
    public void run() {
        //死循环不断地进行监听
        while(true){
            acceptPointXy();
        }
    }

}

从上面的代码可以看到,当我们发送后,再也不进行receive,而是交给线程的run中进行死循环地receive监听。并把send方法和receive方法封装出来,是为了区分谁先发送,谁监听的情况。否则发送方就无法将数据到达目的地。

如何让receive跳出?

也就是告诉等待的对方,我已经退出了,不需要继续等待呢?
这里分两种情况的方法,一种是正常退出,另一种是非正常退出。

正常退出:

传入特定的字符进行判断对方已经退出了,并主动停止等待。我根据上一个代码块进行修改如下:

public static boolean isRunning=true;//默认true;
public void receiverPointXy(String content) {
        //首先进行判断是否关闭的情况
        if(content.isEmpty()){//当正常关闭的时候,对方发送空字符来标示关闭
            isRunning=false;//不再循环等待
            if(socket!=null)
                socket.close();
            return ;
        }
        ....
}
public void run() {
        //isRunning进行标识是否等待
        while(isRunning){
            acceptPointXy();
        }
    }

非正常退出:

程序遇到异常,网络不正常的时候,我们可以设置timeout来停止等待:

socket.setSoTimeout(60000);// 设置等待时间为1分钟

那么你可能会好奇,默认的Timeout是多久呢?我们通过查看源码的注释

/**
* ...
* A timeout of zero is interpreted as an infinite timeout.
*/
public synchronized void setSoTimeout(int timeout) throws SocketException {
        if (isClosed())
            throw new SocketException("Socket is closed");
        getImpl().setOption(SocketOptions.SO_TIMEOUT, new Integer(timeout));
    }
 DatagramSocketImpl getImpl() throws SocketException {
        if (!created)
            createImpl();
        return impl;
    }
 /**
     * Retrieve setting for SO_TIMEOUT.  0 returns implies that the
     * option is disabled (i.e., timeout of infinity).
*/
public synchronized int getSoTimeout() throws SocketException {
        if (isClosed())
            throw new SocketException("Socket is closed");
        if (getImpl() == null)
            return 0;
            ...
}

通过查看源码可以发现,当我们执行setSoTimeout的时候,将调用getImpl()方法创建DatagramSocketImpl,使得getImpl()!=null,返回不为零的数字。所以,默认的timeout0。但是为零的情况,我们通过看方法的注释可以发现,为零意味着无限等待

使用MulticastSocket实现多点广播

使用MulticastSocket可以将数据报以广播方式发送到数量不等的多个客户端

原理

若要使用多点广播时,则需要让一个数据报有一组目标主机地址,当数据报发出后,整个组的所有主机都能收到该数据报。
IP多点广播(或多点发送)实现了将单一信息发送到多个接收者的广播
其思想是设置一组特殊网络地址作为多点广播地址,每一个多点广播地址都被看做一个组,当客户端需要发送、接收广播信息时,加入到该组即可。

IP协议为多点广播提供了这批特殊的IP地址,这些IP地址的范围是224.0.0.0至239.255.255.255

多点广播的示意图
多点广播的示意图

注意MulticastSocket重要的属性:

使用jionGroup()方法来加入指定组;使用leaveGroup()方法脱离一个组。
使用setTimeToLive(int ttl)方法,通过参数ttl来指定最多可以跨过多少个网络。(默认情况ttl=1

  • ttl=0时,指定数据报应停留在本地主机
  • ttl=1时,指定数据报发送到本地局域网
  • ttl=32时,指定数据报只能发送到本站点的网络上
  • ttl=64时,指定数据报应保留在本地区
  • ttl=128时,指定数据报应保留在本大洲
  • ttl=64时,指定数据报可发送到所有地方

当在某些系统中,可能有多个网络接口:(将会对多点广播带来问题)

通过调用setInterface可选择MulticastSocket使用的网络接口;也可以使用getInterface方法查询MulticastSocket监听的网络接口。

示例代码:

public class MulticastSocketTest implements Runnable {
    // 使用常量作为本程序的多点广播IP地址
    private static final String BROADCAST_IP = "230.0.0.1";
    // 使用常量作为本程序的多点广播目的的端口
    public static final int BROADCAST_PORT = 30000;
    // 定义每个数据报的最大大小为4K
    private static final int DATA_LEN = 4096;
    // 定义本程序的MulticastSocket实例
    private MulticastSocket socket = null;
    private InetAddress broadcastAddress = null;
    private Scanner scan = null;
    // 定义接收网络数据的字节数组
    byte[] inBuff = new byte[DATA_LEN];
    // 以指定字节数组创建准备接受数据的DatagramPacket对象
    private DatagramPacket inPacket = new DatagramPacket(inBuff, inBuff.length);
    // 定义一个用于发送的DatagramPacket对象
    private DatagramPacket outPacket = null;

    public void init() throws IOException {
        try {
            // 创建用于发送、接收数据的MulticastSocket对象
            // 因为该MulticastSocket对象需要接收,所以有指定端口
            socket = new MulticastSocket(BROADCAST_PORT);
            broadcastAddress = InetAddress.getByName(BROADCAST_IP);
            // 将该socket加入指定的多点广播地址
            socket.joinGroup(broadcastAddress);
            // 设置本MulticastSocket发送的数据报被回送到自身
            socket.setLoopbackMode(false);
            // 初始化发送用的DatagramSocket,它包含一个长度为0的字节数组
            outPacket = new DatagramPacket(new byte[0], 0, broadcastAddress,
                    BROADCAST_PORT);
            // 启动以本实例的run()方法作为线程体的线程
            new Thread(this).start();
            // 创建键盘输入流
            scan = new Scanner(System.in);
            // 不断读取键盘输入
            while (scan.hasNextLine()) {
                // 将键盘输入的一行字符串转换字节数组
                byte[] buff = scan.nextLine().getBytes();
                // 设置发送用的DatagramPacket里的字节数据
                outPacket.setData(buff);
                // 发送数据报
                socket.send(outPacket);
            }
        } finally {
            socket.close();
        }
    }

    public void run() {
        try {
            while (true) {
                // 读取Socket中的数据,读到的数据放在inPacket所封装的字节数组里。
                socket.receive(inPacket);
                // 打印输出从socket中读取的内容
                System.out.println("聊天信息:"
                        + new String(inBuff, 0, inPacket.getLength()));
            }
        }
        // 捕捉异常
        catch (IOException ex) {
            ex.printStackTrace();
            try {
                if (socket != null) {
                    // 让该Socket离开该多点IP广播地址
                    socket.leaveGroup(broadcastAddress);
                    // 关闭该Socket对象
                    socket.close();
                }
                System.exit(1);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws IOException {
        new MulticastSocketTest().init();
    }
}

多次按ctrl+f11生成多个示例进行测试。可以发现,使用MulticastSocket监听统一端口,多个示例并不会产生端口被占用的错误。再者监听的IP地址要为IP协议上的广播地址

扩展:(聊天室的实现)
使用MulticastSocket实现多点广播(2)

参考资料

TCP、UDP详解
17.4.3 使用MulticastSocket实现多点广播(1),这个居然是《java疯狂讲义第二部》的全教程。
java.net.MulticastSocket Example

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容

  • 个人认为,Goodboy1881先生的TCP /IP 协议详解学习博客系列博客是一部非常精彩的学习笔记,这虽然只是...
    贰零壹柒_fc10阅读 5,053评论 0 8
  • 1.这篇文章不是本人原创的,只是个人为了对这部分知识做一个整理和系统的输出而编辑成的,在此郑重地向本文所引用文章的...
    SOMCENT阅读 13,060评论 6 174
  • 11.1 引言 UDP是一个简单的面向数据报的运输层协议:进程的每个输出操作都正好产生一个UDP数据报,并组装成一...
    张芳涛阅读 2,803评论 1 6
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,647评论 18 139
  • 定义 网络协议为计算机网络中进行数据交换而建立的规则、标准或约定的集合。网络协议主要由三个要素组成:语义、语法及时...
    FlyAndroid阅读 988评论 0 10