UDP是用戶數據報協議(User Datagram Protocol,UDP)的簡稱,其主要作用是將網絡數據流量壓縮成數據報形式,提供面向事務的簡單信息傳送服務。與TCP協議不同,UDP協議直接利用IP協議進行UDP數據報的傳輸,UDP提供的是面向無連接的、不可靠的數據報投遞服務。當使用UDP協議傳輸信息時,用戶應用程序必須負責解決數據報丟失、重復、排序,差錯確認等問題。由於UDP具有資源消耗小、處理速度快的優點,所以通常視頻、音頻等可靠性要求不高的數據傳輸一般會使用UDP,即便有一定的丟包率,也不會對功能造成嚴重的影響。
UDP協議簡介
UDP是無連接的,通信雙方不需要建立物理鏈路連接。在網絡中它用於處理數據包,在OSI模型中,它處於第四層傳輸層,即位於IP協議的上一層。它不對數據報分組、組裝、校驗和排序,因此是不可靠的。報文的發送者不知道報文是否被對方正確接收。
UDP數據報格式有首部和數據兩個部分,首部很簡單,為8個字節,包括以下部分:
(1)源端口:源端口號,2個字節,最大值為65535;
(2)目的端口:目的端口號,2個字節,最大值為65535;
(3)長度:2字節,UDP用戶數據報的總長度;
(4)校驗和:2字節,用於校驗UDP數據報的數字段和包含UDP數據報首部的“偽首部”。其校驗方法類似於IP分組首部中的首部校驗和。
偽首部,又稱為偽包頭(Pseudo Header):是指在TCP的分段或UDP的數據報格式中,在數據報首部前面增加源IP地址、目的IP地址、IP分組的協議字段、TCP或UDP數據報的總長度等,共12字節,所構成的擴展首部結構。此偽首部是一個臨時的結構,它既不向上也不向下傳遞,僅僅是為了保證可以校驗套接字的正確性。
UDP協議數據報格式示意圖如圖:
UDP協議的特點如下。
(1)UDP傳送數據前並不與對方建立連接,即UDP是無連接的。在傳輸數據前,發送方和接收方相互交換信息使雙方同步;
(2)UDP對接收到的數據報不發送確認信號,發送端不知道數據是否被正確接收,也不會重發數據;
(3)UDP傳送數據比TCP快速,系統開銷也少:UDP比較簡單,UDP頭包含了源端口、目的端口、消息長度和校驗和等很少的字節。由於UDP比TCP簡單、靈活,常用於可靠性要求不高的數據傳輸,如視頻、圖片以及簡單文件傳輸系統(TFTP)等。TCP則適用於可靠性要求很高但實時性要求不高的應用,如文件傳輸協議FTP、超文本傳輸協議HTTP、簡單郵件傳輸協議SMTP等。
服務端開發
import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioDatagramChannel; public class ChineseProverbServer { public void run(int port) throws Exception { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); //由於使用UDP通信,在創建Channel的時候需要通過NioDatagramChannel來創建 b.group(group).channel(NioDatagramChannel.class) //隨后設置Socket參數支持廣播, .option(ChannelOption.SO_BROADCAST, true) //最后設置業務處理handler。 //相比於TCP通信,UDP不存在客戶端和服務端的實際連接, //因此不需要為連接(ChannelPipeline)設置handler, //對於服務端,只需要設置啟動輔助類的handler即可。 .handler(new ChineseProverbServerHandler()); b.bind(port).sync().channel().closeFuture().await(); } finally { group.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port = 8080; if (args.length > 0) { try { port = Integer.parseInt(args[0]); } catch (NumberFormatException e) { e.printStackTrace(); } } new ChineseProverbServer().run(port); } } import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.DatagramPacket; import io.netty.util.CharsetUtil; import io.netty.util.internal.ThreadLocalRandom; public class ChineseProverbServerHandler extends SimpleChannelInboundHandler { // 諺語列表 private static final String[] DICTIONARY = {"只要功夫深,鐵棒磨成針。", "舊時王謝堂前燕,飛入尋常百姓家。", "洛陽親友如相問,一片冰心在玉壺。", "一寸光陰一寸金,寸金難買寸光陰。", "老驥伏櫪,志在千里。烈士暮年,壯心不已!"}; private String nextQuote() { //由於ChineseProverbServerHandler存在多線程並發操作的可能, //所以使用了Netty的線程安全隨機類ThreadLocalRandom。 // 如果使用的是JDK7,可以直接使用JDK7的java.util.concurrent.ThreadLocalRandom。 int quoteId = ThreadLocalRandom.current().nextInt(DICTIONARY.length); return DICTIONARY[quoteId]; } @Override public void messageReceived(ChannelHandlerContext ctx, Object msg)throws Exception { //Netty對UDP進行了封裝,因此,接收到的是Netty封裝后的io.netty. channel.socket.DatagramPacket對象。 DatagramPacket packet = (DatagramPacket) msg; //將packet內容轉換為字符串(利用ByteBuf的toString(Charset)方法), String req = packet.content().toString(CharsetUtil.UTF_8); System.out.println(req); // 然后對請求消息進行合法性判斷:如果是“諺語字典查詢?”,則構造應答消息返回。 // DatagramPacket有兩個參數:第一個是需要發送的內容,為ByteBuf; // 另一個是目的地址,包括IP和端口,可以直接從發送的報文DatagramPacket中獲取。 if ("諺語字典查詢?".equals(req)) { ctx.writeAndFlush( new DatagramPacket(Unpooled.copiedBuffer("諺語查詢結果: " + nextQuote(), CharsetUtil.UTF_8), packet.sender())); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); cause.printStackTrace(); } }
客戶端開發
UDP程序的客戶端和服務端代碼非常相似,唯一不同之處是UDP客戶端會主動構造請求消息,向本網段內的所有主機廣播請求消息,對於服務端而言,接收到廣播請求消息之后會向廣播消息的發起方進行定點發送。
import io.netty.bootstrap.Bootstrap; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.DatagramPacket; import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.util.CharsetUtil; import java.net.InetSocketAddress; public class ChineseProverbClient { public void run(int port) throws Exception { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); //創建UDP Channel和設置支持廣播屬性等與服務端完全一致。 // 由於不需要和服務端建立鏈路,UDP Channel創建完成之后,客戶端就要主動發送廣播消息; // TCP客戶端是在客戶端和服務端鏈路建立成功之后由客戶端的業務handler發送消息,這就是兩者最大的區別。 b.group(group).channel(NioDatagramChannel.class) .option(ChannelOption.SO_BROADCAST, true) .handler(new ChineseProverbClientHandler()); Channel ch = b.bind(0).sync().channel(); // 向網段內的所有機器廣播UDP消息 // 用於構造DatagramPacket發送廣播消息, // 注意,廣播消息的IP設置為“255.255.255.255”。 // 消息廣播之后,客戶端等待15s用於接收服務端的應答消息,然后退出並釋放資源。 ch.writeAndFlush( new DatagramPacket(Unpooled.copiedBuffer("諺語字典查詢?",CharsetUtil.UTF_8), new InetSocketAddress("255.255.255.255", port)) ).sync(); if (!ch.closeFuture().await(15000)) { System.out.println("查詢超時!"); } } finally { group.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port = 8080; if (args.length > 0) { try { port = Integer.parseInt(args[0]); } catch (NumberFormatException e) { e.printStackTrace(); } } new ChineseProverbClient().run(port); } } import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.DatagramPacket; import io.netty.util.CharsetUtil; public class ChineseProverbClientHandler extends SimpleChannelInboundHandler { @Override public void messageReceived(ChannelHandlerContext ctx, Object o) throws Exception { //接收到服務端的消息之后將其轉成字符串,然后判斷是否以“諺語查詢結果:”開頭, //如果沒有發生丟包等問題,數據是完整的,就打印查詢結果,然后釋放資源。 DatagramPacket msg = (DatagramPacket)o; String response = msg.content().toString(CharsetUtil.UTF_8); if (response.startsWith("諺語查詢結果:")) { System.out.println(response); ctx.close(); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }