文章目录
- 1. DatagramSocket类
- 2. 简单的UDP客户端
- 3. DatagramChannel
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选项:
- SO_TIMEOUT
- SO_RCVBUF
- SO_SNDBUF
- SO_REUSEADDR
- SO_BROADCAST
- 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
- 通道是非阻塞的,而且没有就绪的包
- 数据报中不包含任何数据
- 缓冲区已满
- 写入
很自然的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);
}
}
}