神奇好望角 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 蜀山兆孨龘 阅读(3187) | 评论 (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 蜀山兆孨龘 阅读(3061) | 评论 (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 蜀山兆孨龘 阅读(2698) | 评论 (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 蜀山兆孨龘 阅读(4397) | 评论 (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 蜀山兆孨龘 阅读(1514) | 评论 (0)编辑 收藏

仅列出标题
共8页: 上一页 1 2 3 4 5 6 7 8 下一页