java的UDP(二)

本文介绍了Java中用于UDP通信的DatagramSocket类和DatagramChannel类,包括构造函数、发送和接收数据报的方法,以及相关的Socket选项。还展示了简单的UDP客户端示例和非阻塞的DatagramChannel使用方法,强调了UDP的异步特性和在多客户端通信中的应用。
摘要由CSDN通过智能技术生成

1. DatagramSocket类

要收发DatagramPacket,必须打开一个数据报Socket。在java中,数据报Socekt通过DatagramSocekt类创建和访问。服务器Socket需要指定绑定端口,而用户端Socket不用关心(其他方面两个没有什么区别)。

  • 构造函数

DatagramSocekt构造函数用于不同的情况,这与DatagramSocket构造函数类似。

public DatagramSocket() throws SocketException

在匿名本地端口打开一个数据报Socket,一般用于客户端。用户不用关心端口,服务器会将响应发送到发出数据报的端口。让系统分配一个端口。如果出于某些原因需要知道本地端口,可以使用getLocalPort()方法得到。

public DatagramSocket(int port) throws SocektException

这个构造函数创建一个在指定端口监听入站数据报的Socket。TCP端口和UDP端口没有任何关联。对于两个不同的程序,如果一个使用UDP而另一个使用TCP,那么它们可以使用相同的端口号。

public DatagramSocket(int port,InetAddress interface) throws SocketException

创建一个绑定到指定端口和指定网络接口的DatagramSocket对象

public DatagramSocket(SocketAddress interface)throws SocketException

这个构造函数和前一个相似就,只是网络接口地址和端口,由SocektAddress读取。

protected DatagramSocket(DatagramSocketImpl impl) throws SocketException

这个构造函数允许子类提供自己的UDP实现,而不是默认实现。与其他4个构造函数创建的Socket不同,这个Socket一开始没有与端口绑定。使用前必须啊通过bind()方法绑定到一个SocketAddress。可以向这个方法传递null,将Socket绑定到任何可用的地址和端口

  • 发送和接受数据报

DatagramSocket类的首要任务是发送和接受UDP数据,一个Socket既可以发送又接收数据报,事实上,它可以和多台主机收发数据。

public void send(DatagramPacket dp) throws IOException

下面是一个基于UDP的discard客户端。它从System.in读取用户输入的行就,将其发送给discard服务器,这个服务器只是丢弃所有数据。每一行都填充在一个DatagramPacket中。

 public static void main(String[] args)  {
         try(DatagramSocket thesocket=new DatagramSocket()){
            InetAddress server=InetAddress.getByName("localhost");
            BufferedReader userInput=new BufferedReader(new InputStreamReader(System.in));
            while(true){
                String theline=userInput.readLine();
                if(theline.equals("."))break;
                byte[] data=theline.getBytes();
                DatagramPacket thoutput=new DatagramPacket(data,data.length,server,8080);
                thesocket.send(thoutput);
            }
        } catch (SocketException | UnknownHostException e) {
             throw new RuntimeException(e);
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
    }
public void receive(DatagramPacket dp)throws IOException

这个方法从网络中接收一个UDP数据报,存储在现有的DatagramPacket对象dp中。与ServerSocket类的accept方法类似,这个方法会阻塞调用线程,直到数据报到达。如果程序除了等待数据报外还有其他操作,就应当在单独线程中调用receive方法

public void close()

调用DatagramSocket对象的close方法将释放该Socket占用的端口。与流和TCP Socket一样。

public int getLocalPort()

DatagramSocket的getLocalPort()方法返回一个int,表示Socket正在监听的本地端口。如果我们创建DatagramSocket时是系统帮我们选定的端口,此时可以使用这个方法查看系统给我们分配的是什么端口。

public InetAddress getLocalAddress()

DatagramSocket的getLocalAddress() 方法返回一个InetAddress对象,表示Socekt绑定到的本地地址。实际中很少需要这样做。

public SocketAddress getLocalAddress()

该方法返回一个SocketAddress对象,这个对象包装了Socket绑定的本地接口和端口。实际用处也不大

  • 管理连接

与TCP socket不同,数据报Socket不太在意与谁对话。事实上,默认情况下它们可以与任何人对话,但这通常不是你希望的。

public void connect(InetAddress host, int port)

connect()方法并不真正建立TCP意义上的连接。不过,它确实指定了DatagramSocket只对指定远程主机和指定远程端口收发数据包。试图向其他主机发送数据包将抛出一个IllegalArgumentException异常。从其它的主机或其他的主机或其他的端口接收的数据报将被丢弃,没有异常,也没有通知。

public void disconnect()

disconnect()方法中断已连接DatagramSocket的“连接”,从而可以再次收发任何主机和端口的包。

public int getPort()

当且仅当DatagramSocket已连接时,getPort()方法返回它所连接的远程端口。否则返回-1.

public InetAddress getInetAddress()

当且仅当DatagramSocket已连接时,getInetAddress()方法返回它所连接的远程主机的地址。否则返回null

public InetAddress getRemoteSocketAddress()

如果DatagramSocket已连接,该方法返回它所连接的远程主机的地址

  • Socket选项

Java支持6个UDP Socket选项:

  1. SO_TIMEOUT
  2. SO_RCVBUF
  3. SO_SNDBUF
  4. SO_REUSEADDR
  5. SO_BROADCAST
  6. IP_TOS

SO_TIMEOUT
是receive()在抛出InterruptedIOException(IOException 的一个子类)异常前等待入站数据报的时间,以毫秒计算。可以用下面两个方法设置和查看

public void setSoTimeout(int timeout) throws SocketException
public int getSoTimeout() throws IOException

SO_RCVBUF
DatagramSocket的SO_RCVBUF选项与Socket的SO_RCVBUF选项紧密相关。它确定了用于网络I/O的缓冲区大小。对于相当快的连接(如以太网速的连接),较大的缓冲区有助于提升性能,因为在溢出前可以存储更多的入站数据报。与TCP相比,对于UDP,足够大的接收缓冲区甚至更加重要,因为在缓冲区满时到达的的UDP数据报就会丢失,而缓冲区满时到达的TCP数据报最后还会重传。此外,SO_RCVBUF设置了应用程序可以接受的数据报包的大小,接收的缓冲区中放不下的包会不声不响地被丢弃掉。

//它只是建议,具体的底层实现可以忽略这个建议
public void setReceiveBufferSize(int size) throws SocketException
public void getReceiveBufferSize() throws SocketException

如果底层Socket实现不能识别SO_RCVBUF选项,这两个方法都会抛出SocketException异常

SO_REUSEADDR

SO_REUSEADDR选项对于UDP Socket的意义与对于TCP Socket的意义有所不同,对于UDP,该选项可以控制是否允许多个数据报Socket同时绑定到相同端口和地址。如果多个Socket绑定到相同端口,接收的包将复制给绑定的所有Socket

public void setReuseAddress(boolean on) throws SocketException
public boolean getReuseAddress() SocketException

SO_BROADCAST
该选项控制是否允许一个Socket向广播地址收发包,如广播地址192.168.254.255。UDP广播常用于DHCP等协议。

public void setBroadcast(boolean on) throws SocketException
public boolean getBroadcast() throws SocketException

IP_TOS
由于业余流类型由多个IP数据报首部中的IP_TOS字段值来确定,所以它对于UDP与对于TCP同样重要,毕竟包要根据IP地址进行路由和区分优先级,而TCP和UPD都建立在IP基础之上。DatagramSocket的setTrafficClass()和getTrafficClass()方法与Socket中相应方法实际上没有分别。之所以必须在这里重复出现,只是因为DatagramSocket和Socket没有共同的超类

public int getTrafficClass() throws SocketException
public void setTrafficClass() throws SocketException

trafficClass参数是一个整数,表示要设置的流量类别。具体的数值取决于操作系统和网络设备的支持,常见的取值如下:

  • 最高两位(DSCP字段):
    • 0x00:Best Effort
    • 0x08:Express Forwarding
    • 0x10:Assured Forwarding
    • 0x18:Voice-Admit Forwarding
  • 后六位(ECN字段):
    • 0x00:Non-ECN
    • 0x01:ECT (0)
    • 0x02:ECT (1)
    • 0x03:CE (Congestion Encountered)

2. 简单的UDP客户端

一些Internet服务只需要知道客户端的地址和端口,它们会忽略客户端在数据报中发送的数据。所以下面实现一个UDPock简单额客户端

public class QuizCardBuilder {
    //希望从服务器接收到的数据报的大小
    private int bufferSize;
    //等待入站数据报超时时间
    private int timeout;
    private InetAddress host;
    private int port;
    public QuizCardBuilder(InetAddress address,int port , int bufferSize , int timeout){
        this.bufferSize=bufferSize;
        this.host=address;
        this.port=port;
        this.timeout=timeout;
    }

    public  QuizCardBuilder(InetAddress address, int port, int bufferSize){
        //没有指定超时时间的话就默认设置为30s
        this(address, port , bufferSize ,30000);
    }
    public QuizCardBuilder(InetAddress address , int port){
        //如果没有指定缓冲区的大小,就将缓冲区大小指定为8192
        this(address, port, 8192, 30000);
    }
    public byte[] poke(){
        try(DatagramSocket socket=new DatagramSocket(0)){
            DatagramPacket outgoing= new DatagramPacket(new byte[1] , 1 ,host ,port);
            socket.connect(host,port);
            socket.send(outgoing);
            DatagramPacket incoming=new DatagramPacket(new byte[bufferSize],bufferSize);
            socket.receive(incoming);
            int numBytes=incoming.getLength();
            byte[] response=new byte[numBytes];
            System.arraycopy(incoming.getData(),0,response,0,numBytes);
            return  response;
        } catch (SocketException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    public static void main(String[] args)  {
        InetAddress host;
        int port=0;
        try{
            host=InetAddress.getByName("time.nist.gov");
            port=Integer.parseInt("13");
        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        }
       try{
           QuizCardBuilder quizCardBuilder=new QuizCardBuilder(host,port);
           byte[] response=quizCardBuilder.poke();
           if(response==null){
               System.out.println("No response within allotted time");
               return;
           }
           String result=new String(response, "US-ASCII");
           System.out.println(result);
       } catch (UnsupportedEncodingException e) {
           throw new RuntimeException(e);
       }
    }
}

3. DatagramChannel

该类用于非阻塞的UDP应用程序,就是SocketChannel和ServerSocketChannel用于非阻塞的TCP应用程序一样。类似于SocketChannel和ServerSocketChannel,,DatagramChannel是SelectChannel的子类,可以注册到一个Selector。如果服务器中一个线程可以管理与多个不同客户端的通信,该类就很有用。不过,UDP天生就比TCP更具异步性,因而实际效果没有那么明显。在UDP汇总,一个数据报Socket可以处理多个客户端的输入和输出请求。该类所增加的就是以非阻塞方式来做到这一点,这样一来,如果网络没有立即准备好发送数据,这些方法就可以迅速返回

  • 打开一个Socket

该类没有公共构造函数,要使用静态方法open创建一个新的DatagramChannel对象

DatagramChannel channel= DatagramChannel.open()

这个通道开始时没有绑定到任何端口,在绑定端口,需要使用Socket()方法访问该通道的对等DatagramSocket对象。例如,下面的代码把通道绑定到3141:

SocketAddress address=new InetSocketAddress(3134);
DatagramSocket socket=channle.socket();
socket.bind(address);

Java 7提供了一个更加方便的方法

SocketAddress address=new InetSocketAddress(3134);
channel.bind(address);
  • 接收

receive方法从通道读取一个数据包,放在一个ByteBuffer中。它返回发送这个包的主机地址

publoic SocketAddress receive(ByteBuffer dst) throws IOException

如果通道是阻塞的,该方法在读取到包之前不会返回,如果通道是非阻塞的,没有包可以读取的情况下这个方法会立即返回null。如果数据报的数据超过了缓冲区的大小,多余的部分会被丢弃。

public static void main(String[] args){
	DatagramChannel channel =DatagramChannel.open();
	DatagramSocket socket=channel.socket();
	SocketAddress address= new InetSocketAddress(PORT);
	socket.bind(address);
	ByteBuffer buffer=ByteBuffer.allocateDirect(65507);
	while(true){
		SocketAddress client= channel.receive(buffer);
		buffer.flip();
		System.out.println(client+"says");
		while(buffer.hasRemainding()) System.out.write(buffer.get);
		System.out.println();
		buffer.close();
	}
}catch(IOException ex){
	System.err.println(ex);
}
	
  • 发送

send()方法将一个数据报从ByteBuffer写入通道,要写到由第二个参数指定的地址:

public int send(ByteBuffer src, SocketAddress target) throws IOException

send方法返回写入的字节数,这可能是要写的缓冲区中的可用的字节数,也可能是0,而不会是其他值,如果通道出于非阻塞模式,而且数据不能立即发送,就会返回0,否则,如果通道不在非阻塞模式,send会等待返回,知道他能发送缓冲区中的全部数据。

public static void main(String[] args){
	DatagramChannel channel=DatagramChannel.open();
	DatagramSocket address=channel.socket();
	SocketAddress address=new InetSocketAddress(7);
	socket.bind(address);
	ByteBuffer buffer=ByteBuffer.allocateDirect(65507);
	while(true){
		SocketAddress client=channel.receive(buffer);
		buffer.flip();
		channel.send(buffer,client);
		buffer.clear()
	}
}catch(IOException ex){
	System.err.println(ex);
}
  • 连接

一旦打开一个数据报通道,可以使用connect()方法,将它连接到一个特定的远程地址:

SocketAddress remote=new InetSocketAddress("time.nist.gov",37)

通道只向这个主机发送数据,或者只从这个主机接收数据,与SocketChannel的Connect()方法不同,这个方法本身不在网络上收发任何包,因为UDP是一种无连接协议。它只是建立一个主机,有数据准备好可以发送时,就会向这个主机发送数据包。因此,这个方法会相当快的返回,不会有任何方面阻塞。它有一个isConnected()方法,当且仅当DatagramSocket连接时,它会返回true。还有一个disconnect断开连接

  • 读取

除了用于特殊用途的receive方法,DatagramChannel还有3个一般的read()方法

public int read(ByteBuffer dst) throws IOException
public long read(ByteBuffer[] dsts) throws IOException
public read(ByteBuffer[] dsts, int offset ,int length) throws IOException

不过这些方法只能用于已连接的通道。也就是说,在调用这些方法之前。必须调用connect将通道连接到某个远程主机快,这使得这些方法适用客户端使用,因为它知道自己和谁在通信,而服务器可能需要和多个客户端通信。者3个方法都只从网络读取一个数据报包。数据报中的数据尽可能多地存储在参数ByteBuffer中。每个方法都会返回读取的字节数,或者如果通道关闭,则返回-1,如果出现羡慕的情况,这个方法会返回0

  1. 通道是非阻塞的,而且没有就绪的包
  2. 数据报中不包含任何数据
  3. 缓冲区已满
  • 写入
    很自然的DatagramChannel有3个write方法,可有可写、散布的通道有这3个方法写入,它们可以用来替代send方法
public int write(ByteBuffer src)throws IOException
public long write(ByteBuffer[] dsts) throws IOException
public long write(ByteBuffer[] dsts, int offset, int length)

同样这些方法也只能用于已经连接的通道。

public class UDPEchoClientWithChannels{
	public final static int PORT=7;
	private final static int LIMIT=100;
	public static void main(String[] args){
		SocketAddress remote;
		try{
			remote=new InetSocketAddress(args[0], PORT);
		}catch(RuntimeException ex){
		return;
		}
		try(DatagramChannel channel=DatagramChannel.open()){
		//开启非阻塞模式
			channel.configureBlocking(false);
			cahnnel.connect(remote);

			Selector selector=Selector.open();
			channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);

			ByteBuffer buffer= ByteBuffer.allocote(4);
			int n=0;
			int numbersRead=0;
			while(true){
				if(numbersRead==LIMIT)break;
				//为一个连接等待1分钟
				selector.select(60000);
				Set(SelectionKey> readyKeys= selector.selectKeys();
				if(readyKeys.isEmpty() && n==LIMIT){
					//所有的包已经写入
					//好像不会再有更多数据从网络到达
					break;
				}else{
				  Iterator<SelectionKey> iterator=readyKeys.iterator();
				  while(iterator.hasNext()){
				  	SelectionKey key=(SelectionKey) iterator.next();
				  	iterator.remove();
				  	if(key.isReachable){
				  		buffer.clear();
				  		channel.read(buffer);
				  		buffer.flip();
				  		int echo=buffer.getInt();
				  		System.out.println("Read:"+echo);
				  		numbersRead++;
				  	}
				  	if(key.isWritable()){
				  		buffer.clear();
				  		buffer.putInt(n);
				  		buffer.flip();
				  		channel.write(buffer);
				  		System.out.println("Wrote:"+n);
				  		n++;
				  		if(n==LIMIT){
				  			key.interestOps(SelectionKey.OP_READ);
				  		}
				  	}
				  }
				}
		}
	}catch(IOException ex){
	}
}
}
  • 关闭

就像常规的Socket一样,应当在结束操作时关闭通道,释放它使用的端口和任何其他资源

public void close() throws IOException

关闭已关闭的通道没有任何效果。试图向已关闭的通道写入或读取数据会抛出异常。如果不确定一个通道是否关闭可以使用isOpen方法查看。与所有通道一样,在Java 7中实现了AutoCloseable。

try(DatagramChannel channel= DatagramChannel.open()){
	
} catch(IOException e){

}

在java 7 以后版本中,DatagramChannel支持8个Socket选项

在这里插入图片描述

前5个选项与之前介绍的Socket选项一致。后3个会在IP组播中使用。这些选项可以用3个方法来检查和设置

//改变选项值
public<T> DatagramChannel setOption(SocketOption<T> name ,T value)throws IOException
//指出任意一个选项的当前值
public<T> T getOption(SocketOption<T> name) throws IOException
//会列出所有的可用的Socket选项
public Set<SocketOption<?>> supportedOptions()
public class QuizCardBuilder {
    public static void main(String[] args)  {
       try(DatagramChannel channel= DatagramChannel.open()){
           for(SocketOption<?> option: channel.supportedOptions()){
               System.out.println(option.name()+":"+channel.getOption(option));
           }
       } catch (IOException e) {
           throw new RuntimeException(e);
       }
    }
}

在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值