JAVA默认提供了对file,ftp,gopher,http,https,jar,mailto,netdoc协议的支持。当我们要利用这些协议来创建应用时,主要会涉及到如下几个类:
1.java.net.URL:URL资源
2.java.net.URLConnection:各种URL资源连接器
例如,当我们利用HTTP协议获取Web资源时,通常的过程如下:
- URL url = new URL("http://www.163.com");
- URLConnection conneciotn = url.openConnection();
URL和URLConnection是如何做到对协议支持的呢?在它们的内部,主要涉及到了如下几个类:
1.URLStreamHandler:协议的流处理器,负责对特定URL协议的解析,并创建符合当前协议的URLConnection;
2.URLStreamHandlerFactory:协议处理工厂,负责为特定协议找到正确的URLStreamHandler。
当利用URL对象创建资源时,其构造函数在去掉协议字段后将其传给URLStreamHandlerFactory,由该工厂来接受协议,为该协议找到并创建适当的URLStreamHandler实现类,最后存储在URL对象的一个字段中(即URL类中transient修饰的URLStreamHandler成员属性)。
URLStreamHandler和URLConnection总是成对出现的。因此,若要实现对新协议的支持时,需同时实现这两个抽象类,分别负责对协议的解析,以及与服务器的交互(数据转换等)。
另外,JAVA是如何识别当前URL协议该由哪个URLStreamHandler和URLConnection来处理的呢?在创建URL对象时,其内部调用了一个getURLStreamHandler(String protocol)静态方法,它将根据协议的名称来找到对应的URLStreamHandler实现类,其查找规则如下:
1)检测是否创建了URLStreamHandlerFactory对象:如果创建,则直接使用createURLStreamHandler(String protocol)方法创建的协议处理器,否则进入步骤2);
2)在java.protocol.handler.pkgs系统属性指定的包中查找与协议同名的子包和名为Handler的类,即负责处理当前协议的URLStreamHandler实现类必须在的<包名>.<协议名定义的包>中,并且类名称必须为Handler。例如:com.company.net.protocol.rdp包中的Handler类将用于处理RDP协议。若仍未找到则进入步骤3);
3)在JDK rt.jar中的sun.net.www.protocol.<name>包中查找Handler类。例如,若当前协议为ftp,则URL所使用协议处理器就应该为sun.net.www.protocol.ftp包中的Handler类。如下图:
下面结合一个实例来说明如何开发一个新的网络协议。
背景:senv(Server Environment Protocol)协议可让客户端连接远程服务器后发送出请求,请求的内容就是URL查询参数部分,该协议的默认端口为9527。例如:senv://192.168.1.101:9527?pro=os.name,java.version表示获取192.168.1.101这台主机的操作系统名和JRE的版本信息,如果没有查询参数,客户端将默认发送一个"?",表示获取所有的系统属性信息。
1.Senv协议处理器
- package com.daniele.appdemo.net.protocol.custom.senv;
- import java.io.IOException;
- import java.net.URL;
- import java.net.URLConnection;
- import java.net.URLStreamHandler;
- /**
- * <p>
- * 自定义的senv协议处理器。
- * 由于senv协议的格式符合标准的URL格式:
- * protocol://username@hostname:port/path/filename?query#fragment
- * 因此,这个实现类只需实现父类中的openConnection()方法即可。否则,需重写父类方法
- * protected void parseURL(URL u, String spec, int start, int limit),
- * 来重新正确的设置URL的各个属性值,例如:host,port,query等。
- * </p>
- * @author <a href="mailto:code727@gmail.com">Daniele</a>
- * @version 1.0.0, 2013-5-8
- * @see
- * @since AppDemo1.0.0
- */
- public class Handler extends URLStreamHandler {
- /**
- * <p>当URL根据协议找到该处理器并调用openConnection()方法后,返回负责处理该协议连接的连接器</p>
- * @author <a href="mailto:code727@gmail.com">Daniele</a>
- * @param u
- * @return
- * @throws IOException
- * @since AppDemo1.0.0
- */
- @Override
- protected URLConnection openConnection(URL u) throws IOException {
- return new SenvURLConnection(u);
- }
- }
2.Senv协议连接器
- package com.daniele.appdemo.net.protocol.custom.senv;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.net.Socket;
- import java.net.URL;
- import java.net.URLConnection;
- import com.daniele.appdemo.util.StringUtils;
- /**
- * <p>自定义senv协议连接器</p>
- * @author <a href="mailto:code727@gmail.com">Daniele</a>
- * @version 1.0.0, 2013-5-8
- * @see
- * @since AppDemo1.0.0
- */
- public class SenvURLConnection extends URLConnection {
- /** senv协议的默认端口号 */
- public static final int DEFAULT_PORT = 9527;
- private Socket connection = null;
- public SenvURLConnection(URL url) {
- super(url);
- }
- /**
- * <p>由于父类URLConnection中的getInputStream()方法不提供输入流的获取实现逻辑,因此这里需要重写此方法</p>
- * @author <a href="mailto:code727@gmail.com">Daniele</a>
- * @return
- * @throws IOException
- * @since AppDemo1.0.0
- */
- @Override
- public synchronized InputStream getInputStream() throws IOException {
- if (!connected)
- this.connect();
- return connection.getInputStream();
- }
- /**
- * <p>senv协议连接操作</p>
- * @author <a href="mailto:code727@gmail.com">Daniele</a>
- * @throws IOException
- * @since AppDemo1.0.0
- */
- @Override
- public synchronized void connect() throws IOException {
- if (!connected) {
- int port = url.getPort();
- if (port < 1 || port > 65535)
- port = DEFAULT_PORT;
- this.connection = new Socket(url.getHost(), port);
- connected = true;
- // 连接后立即发送请求
- sendRequest(url);
- }
- }
- /**
- * <p>发送senv协议请求</p>
- * @author <a href="mailto:code727@gmail.com">Daniele</a>
- * @param u
- * @throws IOException
- * @since AppDemo1.0.0
- */
- protected void sendRequest(URL u) throws IOException {
- OutputStream outputStream = this.connection.getOutputStream();
- String queryString = u.getQuery();
- /*
- * 将URL的查询参数部分发送给服务器,由服务器负责解析查询后返回结果。
- * 当参数参数部分为空时,则发送一个"?",表示查询服务器系统环境的所有信息。
- */
- outputStream.write(StringUtils.isNotNullOrBlank(queryString)? queryString.getBytes() : "?".getBytes());
- outputStream.flush();
- }
- }
3.协议处理器工厂
- package com.daniele.appdemo.net.protocol.factory;
- import java.net.URLStreamHandler;
- import java.net.URLStreamHandlerFactory;
- /**
- * <p>
- * 自定义协议的处理器工厂,负责针对每种自定义的协议而返回它们各自对应的协议处理器
- * 如果要用上述的查找规则1来安装协议处理器时,则需要用到这个类
- *</p>
- * @author <a href="mailto:code727@gmail.com">Daniele</a>
- * @version 1.0.0, 2013-5-9
- * @see
- * @since AppDemo1.0.0
- */
- public class CustomProtocolFactory implements URLStreamHandlerFactory {
- public URLStreamHandler createURLStreamHandler(String protocol) {
- if ("senv".equalsIgnoreCase(protocol))
- return new com.daniele.appdemo.net.protocol.custom.senv.Handler();
- return null;
- }
- }
4.处理Senv协议的服务器
- package com.daniele.appdemo.net.protocol.test;
- import java.io.IOException;
- import java.net.InetAddress;
- import java.net.InetSocketAddress;
- import java.nio.ByteBuffer;
- import java.nio.channels.SelectionKey;
- import java.nio.channels.Selector;
- import java.nio.channels.ServerSocketChannel;
- import java.nio.channels.SocketChannel;
- import java.nio.charset.Charset;
- import java.nio.charset.CharsetDecoder;
- import java.util.Iterator;
- import org.apache.log4j.Logger;
- import com.daniele.appdemo.util.StringUtils;
- import com.daniele.appdemo.util.SystemUtils;
- /**
- * <p>处理Senv协议的服务器
- * 1.接收客户端请求
- * 2.发送响应结果
- * </p>
- * @author <a href="mailto:code727@gmail.com">Daniele</a>
- * @version 1.0.0, 2013-5-10
- * @see
- * @since AppDemo1.0.0
- */
- public class SenvProtocolServer {
- private static final Logger logger = Logger.getLogger(SenvProtocolServer.class);
- /** Senv协议的请求参数标识 */
- public static final String REQUEST_PARAM_MARK = "pro=";
- /** Senv协议服务的默认端口号 */
- private static final int DEFAULT_PORT = 9527;
- /** 服务器的IP或主机名 */
- private String host;
- /** 绑定了Senv协议服务的端口号 */
- private int port = 9527;
- /** 当前就绪的服务端通道 */
- private ServerSocketChannel serverChannel;
- /** 当前就绪的客户端通道 */
- private SocketChannel clientChannel;
- /** 服务端的事件注册器 */
- private Selector selector;
- /**
- * <p>启动Senv协议服务器</p>
- * @author <a href="mailto:code727@gmail.com">Daniele</a>
- * @throws IOException
- * @since AppDemo1.0.0
- */
- public void start() throws IOException {
- serverChannel = ServerSocketChannel.open();
- if (port < 1 || port > 65535)
- port = DEFAULT_PORT;
- if (StringUtils.isNotNullOrBlank(host)) {
- serverChannel.socket().bind(new InetSocketAddress(InetAddress.getByName(host), port));
- logger.info("Start server " + host + ":" + port);
- } else {
- serverChannel.socket().bind(new InetSocketAddress(port));
- logger.info("Start server on port " + port);
- }
- serverChannel.configureBlocking(false);
- selector = Selector.open();
- serverChannel.register(selector, SelectionKey.OP_ACCEPT);
- handle();
- }
- /**
- * <p>处理Senv协议请求</p>
- * @author <a href="mailto:code727@gmail.com">Daniele</a>
- * @throws IOException
- * @since AppDemo1.0.0
- */
- protected void handle() throws IOException {
- while (true) {
- selector.select();
- Iterator<SelectionKey> keySetIterator = selector.selectedKeys().iterator();
- SelectionKey cuurentKey = null;
- while (keySetIterator.hasNext()) {
- // 获取当前就绪通道的键对象
- cuurentKey = keySetIterator.next();
- // 避免同一个就绪通道被重复处理
- keySetIterator.remove();
- try {
- if (cuurentKey.isAcceptable()) {
- serverChannel = (ServerSocketChannel) cuurentKey.channel();
- clientChannel = serverChannel.accept();
- if (clientChannel != null) {
- logger.info("Receive request from "
- + clientChannel.socket().getInetAddress().getHostAddress() + ":"
- + clientChannel.socket().getLocalPort());
- clientChannel.configureBlocking(false);
- clientChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
- }
- } else {
- clientChannel = (SocketChannel) cuurentKey.channel();
- if (cuurentKey.isReadable())
- writeResponse();
- }
- } catch (IOException e) {
- if (clientChannel != null && clientChannel.isOpen())
- try {
- /*
- * 为防止服务端在读写客户端信息时,客户端由于某种原因被意外关闭引起服务端也被强制关闭的情况发生。
- * 需在catch块中也需要对客户端的通道做关闭处理, 从而防止服务端也被强制关闭的严重问题。
- * 另外,对就绪通道的读写过程需单独的在一个try...catch块中。
- */
- clientChannel.close();
- } catch (IOException ioe) {
- ioe.printStackTrace();
- }
- }
- }
- }
- }
- /**
- * <p>读取客户端请求</p>
- * @author <a href="mailto:code727@gmail.com">Daniele</a>
- * @return
- * @throws IOException
- * @throws
- * @since AppDemo1.0.0
- */
- protected String readRequest() throws IOException {
- StringBuffer request = new StringBuffer();
- CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
- ByteBuffer buffer = ByteBuffer.allocate(1024);
- while (clientChannel.read(buffer) > 0) {
- buffer.flip();
- request.append(decoder.decode(buffer).toString());
- buffer.clear();
- }
- return request.toString();
- }
- /**
- * <p>向客户端返回响应结果</p>
- * @author <a href="mailto:code727@gmail.com">Daniele</a>
- * @throws IOException
- * @since AppDemo1.0.0
- */
- protected void writeResponse() throws IOException {
- String request = readRequest();
- int start = -1;
- // 如果发送的请求为"?"或请求中无指定的参数时,则查询所有的系统环境属性
- if ("?".equals(request) ||
- (start = request.toLowerCase().indexOf(REQUEST_PARAM_MARK)) < 0) {
- clientChannel.write(ByteBuffer.wrap(SystemUtils.formatSystemProperties().getBytes()));
- } else {
- // 获取请求参数值
- String queryValueString = request.substring(start + REQUEST_PARAM_MARK.length());
- if (StringUtils.isNullOrBlank(queryValueString))
- clientChannel.write(ByteBuffer.wrap(SystemUtils.formatSystemProperties().getBytes()));
- else {
- int index = queryValueString.indexOf("&");
- if (index > -1)
- /*
- * 如果请求参数值里出现了"&"字符,
- * 则说明这个字符后面的内容则认为是其它一些请求参数的内容,
- * 因此不对这部分内容作处理
- */
- queryValueString = queryValueString.substring(0, index);
- clientChannel.write(ByteBuffer.wrap(SystemUtils.formatSystemProperties(queryValueString.split(",")).getBytes()));
- }
- }
- /*
- * 响应内容被发送出去之后添加换行标识,
- * 目的是让客户端的BufferedReader对象调用readLine()方法后能将当前行的内容读取出来
- */
- clientChannel.write(ByteBuffer.wrap("\n".getBytes()));
- /*
- * 发送完响应信息后马上关闭与客户端之间的通道。
- * 目的在于让客户端读取完这些响应之后,就立即释放掉资源,从而让读操作不会一直处于阻塞状态
- */
- clientChannel.close();
- }
- public String getHost() {
- return host;
- }
- public void setHost(String host) {
- this.host = host;
- }
- public int getPort() {
- return port;
- }
- public void setPort(int port) {
- this.port = port;
- }
- public static void main(String[] args) {
- SenvProtocolServer server = new SenvProtocolServer();
- server.setHost("192.168.1.101");
- try {
- server.start();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
4.Senv协议请求客户端
- /**
- * <p>
- * Senv协议请求的客户端,主要功能分为:
- * 1.在创建第一次创建URL对象之前,添加对自定义协议的支持
- * 2.发送请求
- * 3.展示响应数据
- * </p>
- * @author <a href="mailto:code727@gmail.com">Daniele</a>
- * @version 1.0.0, 2013-5-9
- * @see
- * @since AppDemo1.0.0
- */
- public class SenvProtocolClient {
- public static void main(String[] args) {
- BufferedReader reader = null;
- try {
- // 配置协议处理器查找规则一
- if (StringUtils.isNullOrBlank(System.getProperty("java.protocol.handler.pkgs"))) {
- // 设置各个协议包所在的父包路径
- System.setProperty("java.protocol.handler.pkgs", "com.daniele.appdemo.net.protocol.custom");
- }
- /*
- * 配置协议处理器查找规则二
- * 这种方式在整个应用范围之内只能被执行一次。
- * 如果多于一次则会出现"java.lang.Error: factory already defined"这样的错误。但不会受规则一的限制.
- */
- // URL.setURLStreamHandlerFactory(new CustomProtocolFactory());
- URL url = new URL("senv://192.168.1.101:9527/");
- reader = new BufferedReader(new InputStreamReader(url.openConnection().getInputStream()));
- String result = "";
- while ((result = reader.readLine()) != null)
- System.out.println(result);
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- try {
- if (reader != null)
- reader.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
- package com.daniele.appdemo.util;
- import java.util.Enumeration;
- import java.util.Properties;
- /**
- * <p>运行环境工具类</p>
- * @author <a href="mailto:code727@gmail.com">Daniele</a>
- * @version 1.0.0, 2013-5-9
- * @see
- * @since AppDemo1.0.0
- */
- public class SystemUtils {
- private static Properties properties = null;
- static {
- properties = System.getProperties();
- }
- /**
- * <p>返回格式化后的所有系统属性信息</p>
- * @author <a href="mailto:code727@gmail.com">Daniele</a>
- * @return
- * @since AppDemo1.0.0
- */
- @SuppressWarnings("unchecked")
- public static String formatSystemProperties() {
- StringBuffer formatResult = new StringBuffer();
- Enumeration<String> names = (Enumeration<String>) properties.propertyNames();
- while (names.hasMoreElements()) {
- String name = names.nextElement();
- formatResult.append(name).append("=")
- .append(properties.getProperty(name)).append("\n");
- }
- int length = 0;
- return (length = formatResult.length()) > 0 ?
- formatResult.substring(0, length - 1) : "";
- }
- /**
- * <p>返回格式化后的所有指定的系统属性信息</p>
- * @author <a href="mailto:code727@gmail.com">Daniele</a>
- * @param propertyKeys
- * @return
- * @since AppDemo1.0.0
- */
- public static String formatSystemProperties(String[] propertyKeys) {
- StringBuffer formatResult = new StringBuffer();
- if (propertyKeys != null && propertyKeys.length > 0) {
- for (String key : propertyKeys)
- formatResult.append(key).append("=")
- .append(properties.getProperty(key)).append("\n");
- }
- int length = 0;
- return (length = formatResult.length()) > 0 ?
- formatResult.substring(0, length - 1) : "";
- }
- }
- package com.daniele.appdemo.util;
- public class StringUtils {
- public static boolean isNullOrBlank(String str) {
- return str == null || str.length() == 0;
- }
- public static boolean isNotNullOrBlank(String str) {
- return !isNullOrBlank(str);
- }
- }
运行时,依次启动SenvProtocolServer和SenvProtocolClient类即可。