神奇好望角 The Magical Cape of Good Hope

          庸人不必自擾,智者何需千慮?
          posts - 26, comments - 50, trackbacks - 0, articles - 11
            BlogJava :: 首頁 ::  :: 聯系 :: 聚合  :: 管理

          前面介紹了各種請求參數的注入,這些參數在 HTTP 請求中都是以純文本的方式存在的。在處理參數的時候,往往需要把這些文本參數轉換為 Java 對象。JAX-RS 提供了一些內置的規則里自動完成這種轉換。

          轉換規則一覽

          JAX-RS 提供了四條自動類型轉換規則,下面我們逐條考察。

          原始類型

          這個早就見識過了,無需多說。舉例回顧一下:

          @GET
          @Path("{id}")
          public Movie getXxx(@PathParam("id") int id) {/*...*/}
              

          提供接受單個 String 參數的構造器的類型

          這個也不難理解,JAX-RS 會自動調用該構造器創建一個對象:

          public class Style {
              public Style(String name) {/* ... */}
              // ...
          }
          
          @GET
          @Path("{name}")
          public Movie getXxx(@PathParam("name") Style style) {
              // JAX-RS 已自動調用 xxx = new Style(name)
              // ...
          }
              

          提供靜態工廠方法 valueOf(String) 的類型

          也好理解。特別需要注意的是,所有的枚舉類型都在此列,因為編譯器會自動給枚舉類型加上一個這樣的工廠方法。例如:

          public enum Style {/*...*/}
          
          @GET
          @Path("{name}")
          public Movie getXxx(@PathParam("name") Style style) {
              // JAX-RS 已自動調用 style = Style.valueOf(name)
              // ...
          }
              

          類型參數滿足前兩個條件的 List<T>Set<T>SortedSet<T>

          這條規則適用于多值參數,例如查詢參數:

          @GET
          @Path("xxx")
          public Movie getXxx(@QueryParam("style") Set<Style> styles) {
              // JAX-RS 已自動轉換每個 Style 對象并組裝到 Set 中
              // ...
          }
              

          轉換失敗的處理

          如果轉換失敗,JAX-RS 會根據情況自動拋出一個包裝了初始異常,但是帶不同 HTTP 錯誤碼的 WebApplicationException:對矩陣參數(@MatrixParam)、查詢參數 (@QueryParam)或路徑參數(@PathParam)來說為 HTTP 404 找不到,而對頭部參數(@HeaderParam)或 Cookie 參數(@CookieParam)為 HTTP 400 錯誤請求

          posted @ 2012-01-10 13:17 蜀山兆孨龘 閱讀(3194) | 評論 (2)編輯 收藏

          JDK 提供了對 TCP(Transmission Control Protocol,傳輸控制協議)和 UDP(User Datagram Protocol,用戶數據報協議)這兩個數據傳輸協議的支持。本文開始探討 TCP。

          TCP 基礎知識

          在“服務器-客戶端”這種架構中,服務器和客戶端各自維護一個端點,兩個端點需要通過網絡進行數據交換。TCP 為這種需求提供了一種可靠的流式連接,流式的意思是傳出和收到的數據都是連續的字節,沒有對數據量進行大小限制。一個端點由 IP 地址和端口構成(專業術語為“元組 {IP 地址, 端口}”)。這樣,一個連接就可以由元組 {本地地址, 本地端口, 遠程地址, 遠程端口} 來表示。

          連接過程

          在 TCP 編程接口中,端點體現為 TCP 套接字。共有兩種 TCP 套接字:主動和被動,“被動”狀態也常被稱為“偵聽”狀態。服務器和客戶端利用套接字進行連接的過程如下:

          1. 服務器創建一個被動套接字,開始循環偵聽客戶端的連接。
          2. 客戶端創建一個主動套接字,連接服務器。
          3. 服務器接受客戶端的連接,并創建一個代表該連接的主動套接字。
          4. 服務器和客戶端通過步驟 2 和 3 中創建的兩個主動套接字進行數據傳輸。

          下面是連接過程的圖解:

          TCP 連接
          TCP 連接

          一個簡單的 TCP 服務器

          JDK 提供了 ServerSocket 類來代表 TCP 服務器的被動套接字。下面的代碼演示了一個簡單的 TCP 服務器(多線程阻塞模式),它不斷偵聽并接受客戶端的連接,然后將客戶端發送過來的文本按行讀取,全文轉換為大寫后返回給客戶端,直到客戶端發送文本行 bye

          public class TcpServer implements Runnable {
              private ServerSocket serverSocket;
          
              public TcpServer(int port) throws IOException {
                  // 創建綁定到某個端口的 TCP 服務器被動套接字。
                  serverSocket = new ServerSocket(port);
              }
          
              @Override
              public void run() {
                  while (true) {
                      try {
                          // 以阻塞的方式接受一個客戶端連接,返回代表該連接的主動套接字。
                          Socket socket = serverSocket.accept();
                          // 在新線程中處理客戶端連接。
                          new Thread(new ClientHandler(socket)).start();
                      } catch (IOException ex) {
                          ex.printStackTrace();
                      }
                  }
              }
          }
          
          public class ClientHandler implements Runnable {
              private Socket socket;
          
              public ClientHandler(Socket socket) {
                  this.socket = Objects.requireNonNull(socket);
              }
          
              @Override
              public void run() {
                  try (Socket s = socket) {  // 減少代碼量的花招……
                      // 包裝套接字的輸入流以讀取客戶端發送的文本行。
                      BufferedReader in = new BufferedReader(new InputStreamReader(
                              s.getInputStream(), StandardCharsets.UTF_8));
                      // 包裝套接字的輸出流以向客戶端發送轉換結果。
                      PrintWriter out = new PrintWriter(new OutputStreamWriter(
                              s.getOutputStream(), StandardCharsets.UTF_8), true);
          
                      String line = null;
                      while ((line = in.readLine()) != null) {
                          if (line.equals("bye")) {
                              break;
                          }
          
                          // 將轉換結果輸出給客戶端。
                          out.println(line.toUpperCase(Locale.ENGLISH));
                      }
                  } catch (IOException ex) {
                      ex.printStackTrace();
                  }
              }
          }
              

          阻塞模式的編程方式簡單,但存在性能問題,因為服務器線程會卡死在接受客戶端的 accept() 方法上,不能有效利用資源。套接字支持非阻塞模式,現在暫時略過。

          一個簡單的 TCP 客戶端

          JDK 提供了 Socket 類來代表 TCP 客戶端的主動套接字。下面的代碼演示了上述服務器的客戶端:

          public class TcpClient implements Runnable {
              private Socket socket;
          
              public TcpClient(String host, int port) throws IOException {
                  // 創建連接到服務器的套接字。
                  socket = new Socket(host, port);
              }
          
              @Override
              public void run() {
                  try (Socket s = socket) {  // 再次減少代碼量……
                      // 包裝套接字的輸出流以向服務器發送文本行。
                      PrintWriter out = new PrintWriter(new OutputStreamWriter(
                              s.getOutputStream(), StandardCharsets.UTF_8), true);
                      // 包裝套接字的輸入流以讀取服務器返回的文本行。
                      BufferedReader in = new BufferedReader(new InputStreamReader(
                              s.getInputStream(), StandardCharsets.UTF_8));
          
                      Console console = System.console();
                      String line = null;
                      while ((line = console.readLine()) != null) {
                          if (line.equals("bye")) {
                              break;
                          }
          
                          // 將文本行發送給服務器。
                          out.println(line);
                          // 打印服務器返回的文本行。
                          console.writer().println(in.readLine());
                      }
          
                      // 通知服務器關閉連接。
                      out.println("bye");
                  } catch (IOException ex) {
                      ex.printStackTrace();
                  }
              }
          }
              

          從 JDK 文檔可以看到,ServerSocketSocket 在初始化的時候,可以設定一些參數,還支持延遲綁定。這些東西對性能和行為都有所影響。下一篇文章將詳解這兩個類的初始化。

          posted @ 2012-01-04 22:21 蜀山兆孨龘 閱讀(3071) | 評論 (5)編輯 收藏

          我竟然到現在才發現《Fundamental Networking in Java》這本神作,真有點無地自容的感覺。最近幾年做的都是所謂的企業級開發,免不了和網絡打交道,但在實際工作中,往往會采用框架將底層細節和上層應用隔離開,感覺就像是在一個 Word 模板表單里面填寫內容,做出來也沒什么成就感。雖然沒有不使用框架的理由,但我還真是有點懷念當初直接用套接字做網絡編程的日子,既能掌控更多東西,還可以學到更多知識,為研究框架的實現原理打基礎。閑話完畢,轉入今天的正題:IP(Internet Protocol,互聯網協議)。

          IP 基礎知識

          說到 IP,大多數人的第一反應估計都是 IP 地址。其實 IP 是一種協議,IP 地址只是協議的一部分。《RFC 791 - INTERNET PROTOCOL》說:“互聯網協議是為在包交換計算機通信網絡的互聯系統中使用而設計的。”IP 包含三方面的功能:

          1. 用于查找主機的尋址系統
          2. 包格式的定義
          3. 傳輸和接收包的規則

          IP 的相關 Java 類

          從 Java 的角度來看上面說到的三個功能,只有第一個是開發人員需要關心的。另外兩個都依賴底層系統的實現,JDK 也沒有提供相關的類去操作。下面一一介紹 JDK 提供的用于處理 IP 地址的類。

          InetAddress

          此類用來表示 IP 地址,它有兩個子類:Inet4AddressInet6Address,分別用于處理 IPv4 和 IPv6 兩個版本。在實際應用中,InetAddress 足以應付絕大多數情況。它提供了一些靜態方法來構造實例,能根據參數格式自動識別 IP 版本:

          public static InetAddress[] getAllByName(String host) throws UnknownHostException
          解析指定的主機地址,并返回其所有的 IP 地址;如果傳入 IP 地址字符串,則只會校驗格式,返回的數組也只包含一個代表該 IP 地址的實例。例如,想看看谷歌有多少馬甲的話,InetAddress.getAllByName("www.google.com") 就可以了。
          public static InetAddress getByAddress(byte[] addr) throws UnknownHostException
          用表示 IP 地址的字節數組(專業術語稱為“原始 IP 地址”)構造一個實例。IPv4 地址必須是 4 個字節,IPv6 必須 16 個。不常用。
          public static InetAddress getByAddress(String host, byte[] addr) throws UnknownHostException
          用主機地址和原始 IP 地址構造一個實例。此方法應該慎用,因為它不會對主機名進行解析。即使主機名為 IP 地址字符串,也不會檢查是否與字節數組一致。
          public static InetAddress getByName(String host) throws UnknownHostException
          用主機地址構造一個實例,也可以直接傳入 IP 地址字符串,等同于 getAllByName(host)[0]
          public static InetAddress getLocalHost() throws UnknownHostException
          返回本機在網絡中的地址。
          public static InetAddress getLoopbackAddress()
          返回環回地址 127.0.0.1,不拋出異常,等同于 getByName("localhost")(不要和 getLocalHost() 搞混)。環回地址使主機能夠自己連接自己,常被用來對在同一臺機器上測試網絡應用程序。在 IPv4 中,環回地址的網段為 127.0.0.0/8,通常用 127.0.0.1;IPv6 中只有一個 ::1

          接下來看看 InetAddress 中定義的部分實例方法:

          public byte[] getAddress()
          返回原始 IP 地址。
          public String getCanonicalHostName()
          返回全限定域名。這個方法可以用來探查實際的主機名,例如 InetAddress.getByName("www.google.com").getCanonicalHostName() 返回 we-in-f99.1e100.net
          public String getHostAddress()
          返回構造時傳入的主機地址。
          public String getHostName()
          返回主機名。如果構造時傳入的主機地址為 IP 地址字符串,則調用 getCanonicalHostName(),否則直接返回構造時傳入的主機地址。
          public boolean isAnyLocalAddress()
          檢查是否為通配符地址。通配符地址為 0.0.0.0(IPv4)或 ::0(IPv6),代表所有的本地 IP 地址。例如,假設電腦有兩塊網卡,各有一個地址,如果想讓一個程序同時監聽這兩個地址,就需要用通配符地址。
          public boolean isLinkLocalAddress()
          檢查是否為鏈路本地地址。IPv4 里定義為地址段 169.254.0.0/16,Ipv6 里是以 fe80::/64 為前綴的地址。在電腦沒聯網的時候查看本機 IP,就能看到這種地址。
          public boolean isLoopbackAddress()
          檢查是否為環回地址。
          public boolean isSiteLocalAddress()
          檢查是否為站點本地地址。站點本地地址這個名詞實際上已經過時了,現在叫唯一本地地址。IPv4 中未定義;IPv6 中定義為地址段 fc00::/7。這些地址用于私有網絡,例如企業內部的局域網。

          此外還有一些有關多播地址的方法,暫時略過。

          JDK 默認同時支持 IPv4 和 IPv6。如果只想使用一種,可以根據情況將 java.net.preferIPv4Stackjava.net.preferIPv6Addresses 這兩個系統屬性之一設為 true。兩個屬性的默認值都為 false。一般來說不需要去驚動它們。

          SocketAddress

          該類是一個空殼,事實上應用程序使用的是它的唯一子類 InetSocketAddress,目前還看不出這樣設計有什么意義。該類只不過在 InetAddress 的基礎上增加了一個端口屬性。

          NetworkInterface

          該類代表網絡接口,例如一塊網卡。一個網絡接口可以綁定一些 IP 地址。具有多個網絡接口的主機被稱為多宿主主機。下面的代碼可打印出所有本機網絡接口的信息:

          for (NetworkInterface ni : Collections.list(NetworkInterface.getNetworkInterfaces())) {
              System.out.println(ni);
              for (InterfaceAddress ia : ni.getInterfaceAddresses()) {
                  System.out.println("\t" + ia);
              }
              System.out.println();
          }
              

          在我的筆記本上運行結果為:

          name:lo (Software Loopback Interface 1)
          	/127.0.0.1/8 [/127.255.255.255]
          	/0:0:0:0:0:0:0:1/128 [null]
          name:net0 (WAN Miniport (SSTP))
          name:net1 (WAN Miniport (L2TP))
          name:net2 (WAN Miniport (PPTP))
          name:ppp0 (WAN Miniport (PPPOE))
          name:eth0 (WAN Miniport (IPv6))
          name:eth1 (WAN Miniport (Network Monitor))
          name:eth2 (WAN Miniport (IP))
          name:ppp1 (RAS Async Adapter)
          name:net3 (WAN Miniport (IKEv2))
          name:net4 (Intel(R) Wireless WiFi Link 4965AGN)
          	/fe80:0:0:0:288a:2daf:3549:1811%11/64 [null]
          name:eth3 (Broadcom NetXtreme 57xx Gigabit Controller)
          	/10.140.1.133/24 [/10.140.1.255]
          	/fe80:0:0:0:78c7:e420:1739:f947%12/64 [null]
          name:net5 (Teredo Tunneling Pseudo-Interface)
          	/fe80:0:0:0:e0:0:0:0%13/64 [null]
          name:net6 (Bluetooth Device (RFCOMM Protocol TDI))
          name:eth4 (Bluetooth Device (Personal Area Network))
          name:eth5 (Cisco AnyConnect VPN Virtual Miniport Adapter for Windows x64)
          name:net7 (Microsoft ISATAP Adapter)
          	/fe80:0:0:0:0:5efe:a8c:185%17/128 [null]
          name:net8 (Microsoft ISATAP Adapter #2)
          name:net9 (Intel(R) Wireless WiFi Link 4965AGN-QoS Packet Scheduler-0000)
          name:eth6 (Broadcom NetXtreme 57xx Gigabit Controller-TM NDIS Sample LightWeight Filter-0000)
          name:eth7 (Broadcom NetXtreme 57xx Gigabit Controller-QoS Packet Scheduler-0000)
          name:eth8 (Broadcom NetXtreme 57xx Gigabit Controller-WFP LightWeight Filter-0000)
          name:eth9 (WAN Miniport (Network Monitor)-QoS Packet Scheduler-0000)
          name:eth10 (WAN Miniport (IP)-QoS Packet Scheduler-0000)
          name:eth11 (WAN Miniport (IPv6)-QoS Packet Scheduler-0000)
          name:net10 (Intel(R) Wireless WiFi Link 4965AGN-Native WiFi Filter Driver-0000)
          name:net11 (Intel(R) Wireless WiFi Link 4965AGN-TM NDIS Sample LightWeight Filter-0000)
          name:net12 (Intel(R) Wireless WiFi Link 4965AGN-WFP LightWeight Filter-0000)

          posted @ 2011-12-30 17:39 蜀山兆孨龘 閱讀(2703) | 評論 (0)編輯 收藏

          《JAX-RS 從傻逼到牛叉 3:路徑匹配》中,我們已經見過如何使用 @PathParam@QueryParam@MatrixParam 分別注入 URI 中的路徑參數、矩陣參數和查詢參數,以及如何編程訪問這些參數。本文介紹表單參數、HTTP 頭部參數和 Cookie 參數的注入。

          表單參數

          HTTP 請求也可以使用提交表單的方式。這時請求方法一般是 POST,當然春哥也無法阻止你用 GET。在前面我們雖然介紹過處理 POST 請求的例子,但那只是利用了 JAX-RS 對 JAXB 的支持,并沒有涉及到對具體請求參數的注入。JAX-RS 提供了 @FormParam 注解來注入 POST 請求的參數,例如:

          @POST
          public Response createMovie(@FormParam("title") String title) {
              // 此處省略若干行
          }
              

          這兒省略了 @Consumes 注解,JAX-RS 會自動默認為 @Consumes(MediaType.APPLICATION_FORM_URLENCODED),也就是 application/x-www-form-urlencoded 格式的請求。如果請求格式為 multipart/form-data,就必須顯示指明:

          @POST
          @Consumes(MediaType.MULTIPART_FORM_DATA)
          public Response createMovie(@FormParam("title") String title) {
              // 此處省略若干行
          }
              

          JAX-RS 還支持文件的上傳和下載,以后再介紹。

          HTTP 頭部參數

          注入 HTTP 頭部參數簡單得不能再簡單了:

          @GET
          @Path("xxx")
          @Produces(MediaType.TEXT_PLAIN)
          public String xxx(@HeaderParam("User-Agent") String userAgent) {
              // 此處省略若干行
          }
              

          如果有很多頭部參數,為了避免臃腫的參數列表,可以注入一個頭部對象,然后編程訪問頭部參數:

          @GET
          @Path("xxx")
          @Produces(MediaType.TEXT_PLAIN)
          public String xxx(@Context HttpHeaders headers) {
              // 此處省略若干行
          }
              

          Cookie 參數

          注入 Cookie 參數同樣的簡單:

          @GET
          @Path("xxx")
          @Produces(MediaType.TEXT_PLAIN)
          public String xxx(@CookieParam("userName") String userName) {
              // 此處省略若干行
          }
              

          如果希望編程訪問,則可以像編程訪問那樣注入一個 HttpHeaders 對象,然后通過它的 getCookies() 方法來獲取所有的 Cookie。

          posted @ 2011-12-29 16:34 蜀山兆孨龘 閱讀(4403) | 評論 (4)編輯 收藏

          Exchanger 用來讓兩個線程互相等待并交換計算結果。這個類的用法很簡單,因為它就定義了兩個重載的 exchange 方法,參數多的那個無非增加了對超時的支持。當一個線程調用 exchange 的時候(以計算結果作為參數),它就開始等待另一個線程調用 exchange,然后兩個線程分別收到對方調用 exchange 時傳入的參數,從而完成了計算結果的交換。

          不用太多的解釋,運行下面這個例子就一清二楚:

          final Exchanger<String> e = new Exchanger<>();
          
          new Thread() {
              @Override
              public void run() {
                  long id = Thread.currentThread().getId();
                  String s = "abc";
                  System.out.println("線程 [" + id + "] 算出 " + s);
          
                  try {
                      TimeUnit.SECONDS.sleep(new Random().nextInt(5));
                      System.out.println("線程 [" + id + "] 收到 " + e.exchange(s));
                  } catch (InterruptedException ex) {
                      ex.printStackTrace();
                  }
              }
          }.start();
          
          new Thread() {
              @Override
              public void run() {
                  long id = Thread.currentThread().getId();
                  String s = "xyz";
                  System.out.println("線程 [" + id + "] 算出 " + s);
          
                  try {
                      TimeUnit.SECONDS.sleep(new Random().nextInt(5));
                      System.out.println("線程 [" + id + "] 收到 " + e.exchange(s));
                  } catch (InterruptedException ex) {
                      ex.printStackTrace();
                  }
              }
          }.start();
              

          運行結果(可能為):

          線程 [9] 算出 abc
          線程 [10] 算出 xyz
          線程 [10] 收到 abc
          線程 [9] 收到 xyz

          最后強調下,該類只適用于兩個線程,妄圖用它來處理多個生產者和消費者之間的數據交換是注定要失敗的……

          posted @ 2011-12-27 10:50 蜀山兆孨龘 閱讀(1519) | 評論 (0)編輯 收藏

          僅列出標題
          共8頁: 上一頁 1 2 3 4 5 6 7 8 下一頁 
          主站蜘蛛池模板: 星子县| 清流县| 大连市| 遂川县| 普兰店市| 临汾市| 万州区| 牙克石市| 噶尔县| 铜梁县| 龙里县| 白河县| 惠水县| 临海市| 大田县| 长治市| 仲巴县| 桓台县| 天台县| 宣威市| 东台市| 宽甸| 社旗县| 翁牛特旗| 福贡县| 吉林省| 天全县| 克东县| 资阳市| 时尚| 漳州市| 丹凤县| 同仁县| 中西区| 乐都县| 象山县| 出国| 若尔盖县| 洪湖市| 青阳县| 满城县|