目录
java.net包下提供了一套客户端协议的处理框架,这个框架封装了Socket,它是在传输层之上对客户程序的通信过程进行了抽象,提供了通过的应用层协议处理框架,比如基于这个框架可以实现处理HTTP协议客户端。
一、框架的主要类
- URL:统一资源定位器,表示客户程序要访问的远程资源;
- URLConnection:表示客户程序与远程服务器的连接;
- URLStreamHandler:协议处理器,主要负责创建与协议相关的URLConntecion对象;
- ContentHandler:内容处理器,负责解析服务器发送的数据,把他转化为相应的Java对象
- URLStreamHandlerFactory:实例化URLStreamHandler的工厂类;
- ContentHandlerFactory:实例化ContentHandler的工厂类。
如下为上述几个类耦合的关系:
Java为协议处理框架提供了基于HTTP的实现。
1.URL类
URL类用将URL地址进行了封装,通过该URL地址可以解析出协议、主机名、端口号、定位的资源文件等等信息。我们可以通过如下方式创建一个URL对象:
URL url = new URL("URL地址")
在通过构造方法创建URL对象时,它内部会先解析出协议(protocol)、 主机名(host)、端口号(port)、需要访问的该主机上的资源以及创建一个与协议匹配的URLStreamHandler实例。创建URLStreamHandler的源码如下:
static URLStreamHandler getURLStreamHandler(String protocol) {
//1.handlers为Hashtable<String,URLStreamHandler>
URLStreamHandler handler = handlers.get(protocol);
if (handler == null) {
boolean checkedWithFactory = false;
//2.如果已经设置了URLStreamHandlerFactory。
if (factory != null) {
handler = factory.createURLStreamHandler(protocol);
checkedWithFactory = true;
}
if (handler == null) {
String packagePrefixList = null;
// 3.根据系统属性java.protocol.handler.pkgs决定URLStreamHandler子类的名字,并尝试对其实例化。
//protocolPathProp = "java.protocol.handler.pkgs";
packagePrefixList = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(protocolPathProp,""));
if (packagePrefixList != "") {
packagePrefixList += "|";
}
//4.拼接字符串,再下面循环中会最后做处理
packagePrefixList += "sun.net.www.protocol";
//上面利用"|"拼接字符串,此处将"|"作为分隔标记。
StringTokenizer packagePrefixIter = new StringTokenizer(packagePrefixList, "|");
//packagePrefixIter可以得到"|"分隔后的字符串(hasMoreTokens用于判断是否还有分隔出来的下一个元素)
while (handler == null && packagePrefixIter.hasMoreTokens()) {
//比如上述packagePrefixList若为com.abc.net.www|sun.net.www.protocol
//则,此处遍历的packagePrefix 即为com.abc.net.www和sun.net.www.protocol
String packagePrefix = packagePrefixIter.nextToken().trim();
try {
String clsName = packagePrefix + "." + protocol +".Handler";
Class<?> cls = null;
try {
cls = Class.forName(clsName);
} catch (ClassNotFoundException e) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
if (cl != null) {
cls = cl.loadClass(clsName);
}
}
if (cls != null) {
handler = (URLStreamHandler)cls.newInstance();
}
} catch (Exception e) {
// any number of exceptions can get thrown here
}
}
}
synchronized (streamHandlerLock) {
URLStreamHandler handler2 = null;
handler2 = handlers.get(protocol);
if (handler2 != null) {
return handler2;
}
if (!checkedWithFactory && factory != null) {
handler2 = factory.createURLStreamHandler(protocol);
}
if (handler2 != null) {
handler = handler2;
}
//将该协议对应的URLStreamHandler对象存入缓存中
if (handler != null) {
handlers.put(protocol, handler);
}
}
}
return handler;
}
大致流程如下:
- 如果在URL缓存中已经存在该协议对应的URLStreamHandler实例,则直接从缓存中获取。(该类中维护一个Hashtable<String,URLStreamHandler>用来做缓存);
- 如果程序已经通过URL类的静态方法setURLStreamHandlerFactory()设置了URLStreamHandlerFactory接口的具体实现类,则通过这个工厂类的createURLHandlerFactory()方法构造URLStreamHandler实例;
- 通过系统属性java.protocol.handler.pkgs来决定URLStreamHandler具体子类的名字,然后对其实例化。
- 如果失败则实例化位于sun.net.www.protocol包中的sun.net.www.protocol.协议名.Handler类,如果失败则会抛出MalformedURLException异常。(3和4是在循环内操作的)。
URL类的方法如下:
- openConnection():创建并返回一个URLConnection对象,这个openConnection()方法实际上是通过调用 URLStreamHandler类的openConnection()方法来创建的。
- openStream():返回用于读取服务器发送数据的输入流,该方法实际上通过调用URLConnection类的getInputStream()方法获得输入流。
- getContent()方法:返回包装了服务器发送数据的Java对象,实际调用URLConnection的getContent()方法,而它又调用了ContentHandler类的getContent()方法。
//URL类:
public final Object getContent() throws java.io.IOException {
return openConnection().getContent();
}
//URLConnection类:
public Object getContent() throws IOException {
getInputStream();
return getContentHandler().getContent(this);
}
可以看到URL中核心就是解析了URL地址,并且通过构造方法创造URLStreamHandler,上面这几个方法都是利用URLConnection来做的,我们可以只需利用URL的openConnection()方法获取URLConnection对象,然后操作该类中的方法即可,该类提供了可以向远程服务器发送数据和得到响应数据的方法。
2.URLConnection类
URLConnection类表示客户程序与远程服务器的连接。URLConnection有两个boolean类型的属性以及相应的get和set方法。
- doInput属性:若为true,表示允许获得输入流,读取远程服务器发送得数据,该属性得默认值为true;
- doOutput属性:若为true,表示允许获得输出流,向远程服务器发送数据。该属性得默认值为false。
URLConnection类提供了读取远程服务器得响应数据得一系列方法。
- getHeaderField(String name):返回响应头中参数name指定得属性值。
- getContentType():返回响应正文的类型。如果无法获取响应正文的类型,则返回null。对于HTTP响应结果,在响应头中可能会包含响应正文的类型信息。
- getContentLength():返回响应正文的长度。若无法获取,则返回“-1”.
- getContentEncoding():返回响应正文的编码类型,若无法获取,则返回null。
使用URLConnection建议按照如下步骤:
- 通过在URL上调用
openConnection
方法创建连接对象。 - 设置一般请求属性和请求参数。
- 使用
connect
方法实现与远程对象的实际连接。 - 读取响应数据。
下面是一个客户端的简单程序示例:
public static void main(String[] args) throws IOException {
String target = "http://localhost:9090/SpringMVC/test.jsp";
URL url = new URL(target);
// 1.创建连接对象
URLConnection conn = url.openConnection();
// 2.设置一般请求属性和请求参数。(如果没有可以不设置,但是如果需要设置则必须在connect()方法之前,否则抛出异常)
// 设置不能使用缓存
conn.setUseCaches(false);
conn.setRequestProperty("Connection", "keep-alive");
conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
//发送POST请求,get请求直接在URL后面拼接"name=value"字符串参数即可。
//只不过POST请求将"name=value"字符串参数通过流发送。此处发送json请求参数,那么传输json字符串即可。
conn.setDoOutput(true);
OutputStream requestOut = conn.getOutputStream();
String requestParam = JSON.toJSONString(new User("张三丰", "139929991XX"));
requestOut.write(requestParam.getBytes("UTF-8"));
requestOut.close();
// 超时设置,防止网络异常情况下,可能会导致程序僵死而不继续往下执行
// 设置连接服务器的超时时间
conn.setConnectTimeout(30000);
// 设置从服务器读取数据的超时时间
conn.setReadTimeout(30000);
// 3.实现与远程对象的实际连接。(这里也可以省略,当我们获取远程对象中的数据时内部会先建立连接发送请求)
conn.connect();
// 4.可以访问头字段和远程对象的内容
// 获取响应正文类型
String contentType = conn.getContentType();
// 获取响应正文长度
int len = conn.getContentLength();
// 读取响应正文
InputStream in = conn.getInputStream();
byte[] bytes = new byte[1024];
in.read(bytes);
in.close();
String content = new String(bytes, Charset.forName("UTF-8"));
System.out.println("响应正文类型:" + contentType);
System.out.println("响应正文长度:" + len);
System.out.println("响应正文:\n" + content);
}
服务端JSP页面如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>测试页面</title>
</head>
<body>
<%
byte[] bytes = new byte[request.getContentLength()];
request.getInputStream().read(bytes);
String charEncoding = request.getCharacterEncoding();
String jsonStr = new String(bytes,charEncoding);
JSONObject user = JSON.parseObject(jsonStr);
%>
<h1>姓名:<%=user.getString("name") %></h1>
<h1>电话:<%=user.getString("phone") %></h1>
</body>
</html>
客户端程序输出结果如下:
3.HttpURLConnection
上述URLConnection是为我们自定义通信协议处理通信连接的底层支持,如果需要自定义某个协议的客户端程序我们继承URLConnection即可,不过上述之所以能够访问Http服务器,是因为Java提供了处理HTTP协议的客户端程序。其中HttpURLConnection就是Http协议的通信连接,它可以建立与HTTP服务器的连接发送请求和接收响应。所以其实是由于底层通过sun.net.www.protocol.http.Handler类(URLStreamHandler)为我们创造了HttpURLConnection对象,所以上面的操作都是基于此对象的,每个HttpURLConnection实例只能发送一个请求,可发送GET请求和POST请求。
HttpURLConnection在URLConnection的基础上针对HTTP协议提供了更加便捷的API:
- String getResponseMethod():获取发送请求的方法。
- void setRequestMethod(String method):设置发送请求的方法。
- int getResponseCode():获取服务器的响应代码。
- String getResponseMessage():获取服务器的响应消息。
二、基于通信框架自定义客户端处理程序
首先需要实现EchoURLStreamHandlerFactory,这是URL能够解析自己定义的协议的基础,如”echo“协议。
public class EchoURLStreamHandlerFactory implements URLStreamHandlerFactory {
@Override
public URLStreamHandler createURLStreamHandler(String protocol) {
if ("echo".equals(protocol)) {
return new EchoURLStreamHandler();
}
return null;
}
}
public class EchoURLStreamHandler extends URLStreamHandler {
@Override
protected URLConnection openConnection(URL u) throws IOException {
return new EchoURLConnection(u);
}
}
提前(在构造URL对象前)设置好EchoURLStreamHandlerFactory对象,构造URL对象时,会通过构造参数URL地址获取到EchoURLStreamHandler,通过它的openConnection()方法得到自定义的URLConnection连接服务器,发送请求和接收响应。自定义EchoURLConnection如下:
public class EchoURLConnection extends URLConnection {
private Socket connection;
public final static int DEFAULT_PORT = 54199;
protected EchoURLConnection(URL url) {
super(url);
}
@Override
public void connect() throws IOException {
if (this.connected) {
return;
}
connection = new Socket();
connection.connect(new InetSocketAddress(url.getHost(), url.getPort()), this.getConnectTimeout());
this.connected = true;
}
@Override
public InputStream getInputStream() throws IOException {
synchronized (EchoURLConnection.class) {
if (!this.connected) {
connect();
}
return connection.getInputStream();
}
}
@Override
public OutputStream getOutputStream() throws IOException {
synchronized (EchoURLConnection.class) {
if (!this.connected) {
connect();
}
return connection.getOutputStream();
}
}
@Override
public String getContentType() {
return "text/plain";
}
// 断开连接
public void disconnect() throws IOException {
if (this.connected) {
this.connected = false;
this.connection.close();
}
}
}
如果需要使用URLConnection中的getContent()方法获取响应内容,那么上面的getContentType()方法重写是必要的。URLConnection中定义的getContent()方法定义如下:
public Object getContent() throws IOException {
getInputStream();
return getContentHandler().getContent(this);
}
首先获取ContentHandler(),然后调用其getContent()方法。getContentHandler()的逻辑如下:
//URLConnection类:
synchronized ContentHandler getContentHandler() throws UnknownServiceException{
//注意这里调用的是URLConnection自己的getContentType()方法.
//该方法默认实现是通过getHeaderField()方法获取"content-type"响应头
//而getHeaderField(String name)默认实现是返回null。所以如果不重写getContentType()方法那么下面将会抛出UnknownServiceException异常
String contentType = stripOffParameters(getContentType());
ContentHandler handler = null;
if (contentType == null)
throw new UnknownServiceException("no content-type");
try {
//首先从缓存中查找有没有对应的ContentHandler,若有则直接返回。
handler = handlers.get(contentType);
if (handler != null)
return handler;
} catch(Exception e) {}
//若没有则从工厂中查找该类型对应的ContentHandler(故必须提前设置ContentHandlerFactory)
if (factory != null)
handler = factory.createContentHandler(contentType);
if (handler == null) {
try {
handler = lookupContentHandlerClassFor(contentType);
} catch(Exception e) {
e.printStackTrace();
handler = UnknownContentHandler.INSTANCE;
}
handlers.put(contentType, handler);
}
return handler;
}
EchoContentHandler和EchohContentHandlerFactory
如果不需要使用URLConnection的getContent()方法,那么这两个类也可以不实现。
public class EchohContentHandlerFactory implements ContentHandlerFactory {
@Override
public ContentHandler createContentHandler(String mimetype) {
if ("text/plain".equals(mimetype)) {
return new EchoContentHandler();
}
return null;
}
}
public class EchoContentHandler extends ContentHandler {
@Override
public Object getContent(URLConnection connection) throws IOException {
//获取服务器的响应,将其转换为字符串对象
return new DataInputStream(connection.getInputStream()).readUTF();
}
}