URLConnection类概述与实例

绝大部分知识与实例来自O’REILLY的《Java网络编程》(Java Network Programming,Fourth Edition,by Elliotte Rusty Harold(O’REILLY))。

URLConnection类简介

URLConnection类是一个抽象类,每个URLConnection对象代表一个指向URL指定资源的活动连接。它与URL类最大的不同体现在以下两点:

  • URLConnection类提供了更多方法,用于精细地控制与服务器的交互过程,比如检察首部并作出相应;
  • URLConnection可以用POST、PUT等HTTP请求方法与服务器交互。

URLConnection类是Java的协议处理器机制的一部分。协议处理器将处理不同协议的细节和处理特定数据类型分离开,并提供相应的用户接口。URLConnection是抽象类,想要实现一个特定的协议,就必须派生出子类,并覆盖相应的方法。

获取URLConnection对象

使用URLConnection类的方法大致如下:
(1)构造一个URL对象;
(2)调用这个URL对象的openConnection()方法获取一个对应于该URL的协议的URLConnection对象;
(3)对该URLConnection进行配置;
(4)读取首部字段;
(5)获得输入流并读取数据,或是获得输出流并写入数据;
(6)关闭连接。
URLConnection只提供了一个protected的构造方法:protected URLConnection(URL url),因此除非要继承它创建新的子类,否则只能使用openConnection()方法获取对象。
URLConnection对象被构造时,它是未连接的,只有调用了connect()方法才能真正连接本地主机和远程主机。不过,所有需要打开连接才能工作的方法都会确认是否已经建立连接,若未建立则会调用connect()方法,因此一般不需要手动调用connect()方法建立连接。

读取服务器数据

与URL的openStream()方法一样,URLConnection类提供了getInputStream()方法用于获取输入流。事实上,URL的openStream()就是通过调用自己的URLConnection.getInputStream()实现的,因此这两个方法完全等价。

读取首部字段

URLConnection的一大特色就是能够读取首部信息。下面是获取不同首部字段的一系列方法:

  • getContentType():返回响应主体的MIME内容类型,可能还会包含字符集类型(charset=xxx);
  • getContentLength()与getContentLengthLong(Java 7):返回内容的字节数,如果没有该首部字段则返回-1。两个方法的区别在于,可能资源的字节数超过了int的表示范围,这种情况下getContentLength()会返回-1。此时就需要用getContentLengthLong()。
  • getContentEncoding():返回编码方式,若未经编码则返回null。
  • getDate():返回一个代表发送时间的long,表示按自格林尼治标准时间(GMT)1970年1月1日子夜12:00后过去了多少毫秒来给出。可以将其转换为一个java.util.Date对象(直接传入构造器即可)。
  • getExpiration():返回过期日期,格式与getDate()一样。如果没有Expires字段则返回0,表示永不过期。
  • getLastModified():返回最后修改日期,若没有该字段则返回0。

实际上,首部字段远远不止这么几种,这几个方法是URLConnection为几种常用的首部字段特别进行包装的结果。获取任意名称的首部字段的方法如下:

  • public String getHeaderField(String name)
  • public long getHeaderFieldDate(String name,long default)
  • public int getHeaderFieldInt(String name,int default)

这一组方法根据指定的字段名返回对应的值,若无该字段,第一个方法会返回null,而后面两个会用default作为默认值。

  • public String getHeaderFieldKey(int n)
  • public String getHeaderField(int n)

这两个方法根据首部字段的编号n返回对应的键(字段名)和值。需要注意的是,在HTTP中,包含请求方法和路径的起始行是第0个首部字段,实际的第1个首部字段编号为1。

实例1:显示所有首部字段

public static void showHeaders(URLConnection connection){
    if(connection == null){
        System.out.println("null");
        return;
    }
    for(int i = 1 ; ; i++){
        String header = connection.getHeaderField(i);
        if(header == null) break;
        System.out.println(
                connection.getHeaderFieldKey(i) + ": " + header);
    }
}

public static void main(String[] args){
    String urlString = "https://osu.ppy.sh";
    try {
        URL url = new URL(urlString);
        showHeaders(url.openConnection());
    } catch (IOException e) {
        System.out.println("Fail to connect to " + urlString);
    }
}

输出:
Date: Fri, 08 Sep 2017 08:27:28 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Pragma: public
Cache-Control: max-age=50
Expires: Fri, 08 Sep 2017 08:27:33 GMT
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=15552000; includeSubDomains; preload
X-Content-Type-Options: nosniff
Server: cloudflare-nginx
CF-RAY: 39b087602ab26c28-SJC

缓存

缓存是一项非常常用的技术,用于降低服务器负载以及提高客户端访问速度。HTTP首部中提供了Cache-Control、Expires等首部字段来控制客户端的缓存策略,以下是Cache-Control几个常用的字段值:

  • max-age:缓存可用的时间,单位为秒;
  • s-maxage:缓存项在共享缓存中可用的时间,单位为秒;
  • public:可以在共享缓存中使用;
  • private:仅单个用户可以使用;
  • no-cache:资源可以缓存,但是每次使用前必须和服务器进行确认,无论是否过期;
  • no-store:禁止缓存;
  • must-revalidate:资源过期后必须和服务器确认是否还有效。

Expires字段代表资源过期的时间,如果和Cache-Control同时出现会被覆盖。

实例2:检查缓存策略

public class CacheControl {
    private Date maxAge = null;
    private Date sMaxAge = null;
    private boolean mustRevalidate = false;
    private boolean noCache = false;
    private boolean noStore = false;
    private boolean proxyRevalidate = false;
    private boolean publicCache = false;
    private boolean privateCache = false;
    public CacheControl(String cacheControl) {
        if(cacheControl == null || !cacheControl.contains(":")){
            return;
        }
        Date now = new java.util.Date();
        String value = cacheControl.split(":")[1].trim();
        String[] components = value.split(",");
        for(String component : components){
            try{
                component = component.trim().toLowerCase(Locale.US);
                if(component.startsWith("max-age=")){
                    int index = component.indexOf("=");
                    String seconds = component.substring(index + 1).trim();
                    maxAge = new Date(now.getTime()
                            + 1000 * Integer.parseInt(seconds));
                }else if (component.equals("s-maxage=")) {
                    int index = component.indexOf("=");
                    String seconds = component.substring(index + 1).trim();
                    sMaxAge = new Date(now.getTime()
                            + 1000 * Integer.parseInt(seconds));
                }else if (component.equals("must-revalidate")) {
                    mustRevalidate = true;
                }else if (component.equals("proxy-revalidate")) {
                    proxyRevalidate = true;
                }else if (component.equals("no-cache")) {
                    noCache = true;
                }else if (component.equals("no-store")) {
                    noStore = true;
                }else if (component.equals("public")) {
                    publicCache = true;
                }else if (component.equals("private")) {
                    privateCache = true;
                }
            }catch (RuntimeException e) {
                continue;
            }
        }
    }

    public Date getMaxAge(){
        return maxAge;
    }

    public Date getSharedMaxAge(){
        return sMaxAge;
    }

    public boolean mustRevalidate(){
        return mustRevalidate;
    }

    public boolean proxyRevalidate(){
        return proxyRevalidate;
    }

    public boolean noCache(){
        return noCache;
    }

    public boolean noStore(){
        return noStore;
    }

    public boolean publicCache(){
        return publicCache;
    }

    public boolean privateCache(){
        return privateCache;
    }

    public static void main(String args[]){
        CacheControl control = new CacheControl("Cache-Control:public, max-age=50, must-revalidate, no-cache");
        System.out.println(control.publicCache());
        System.out.println(control.getMaxAge());
        System.out.println(control.mustRevalidate());
        System.out.println(control.noCache);
        System.out.println(control.privateCache());
    }
}

输出:
true
Sat Sep 09 09:15:02 CST 2017
true
true
false

上面部分完成了缓存策略的获取,还没有真正实现缓存。Java在默认情况下并不完成缓存,要安装URL类使用的系统级缓存,还需要实现ResponseCache、CacheRequest、CacheResponse三个抽象类。
ResponseCache是实际的缓存类,可以使用ResponseCache.setDefault()来为系统安装一个默认缓存对象。该类有两个方法需要实现:

public CacheResponse get(URI uri, String rqstMethod, Map<String, List<String>> rqstHeaders) throws IOException
public CacheRequest put(URI uri, URLConnection conn) throws IOException

一旦安装了缓存,只要系统尝试加载一个新的URL,它首先会在缓存中查找,如果缓存返回了需要的内容,URLConnection就不需要和远程服务器连接。如果缓存中查找不到,系统才会到远程服务器下载数据,并将响应放在缓存中。
下面对这三个类作用原理的个人理解:

  • ResponseCache需要实现put(存储缓存)和get(获取缓存)两个方法。put方法返回一个CacheRequest对象,这个对象需要实现一个getBody()方法,返回一个输出流,缓存文件是通过这个输出流进行保存的;get方法返回一个CacheResponse对象,这个对象需要实现一个getBody()方法,返回一个输入流,缓存文件是通过这个输入流进行读取的。
  • URI充当了保存和获取URI的钥匙,一般会将URI对象作为Map的key和对应的缓存对象联系起来。
  • 通过在CacheRequest和CacheResponse的getBody()实现中使用不同类型的输入/输出流,即可实现不同的缓存方式。

实例3 三个缓存相关类的简单实现

public class SimpleCacheRequest extends CacheRequest {
    private ByteArrayOutputStream out = new ByteArrayOutputStream();
    @Override
    public OutputStream getBody() throws IOException {
        return out;
    }

    @Override
    public void abort() {
        out.reset();
    }

    public byte[] getData(){
        if(out.size() == 0){
            return null;
        }else {
            return out.toByteArray();
        }
    }
}

public class SimpleCacheResponse extends CacheResponse {
    private final Map<String, List<String>> headers;
    private final SimpleCacheRequest request;
    private final Date expires;
    private final CacheControl control;
    public SimpleCacheResponse(SimpleCacheRequest request,
            URLConnection connection,CacheControl control) {
        this.headers = Collections.unmodifiableMap(connection.getHeaderFields());
        this.request = request;
        this.expires = new Date(connection.getExpiration());
        this.control = control;
    }
    @Override
    public Map<String, List<String>> getHeaders() throws IOException {
        return headers;
    }

    @Override
    public InputStream getBody() throws IOException {
        return new ByteArrayInputStream(request.getData());
    }

    public boolean isExpired(){
        Date now = new Date();
        if(control.getMaxAge() == null){
            if(expires.before(now)){
                return false;
            }
        }else{
            if(control.getMaxAge().before(now)){
                return false;
            }
        }
        return true;
    }
}

public class SimpleResponseCache extends ResponseCache {
    private final Map<URI, SimpleCacheResponse> responses =
            new ConcurrentHashMap<>();
    private final int maxEntries = 100;
    @Override
    public CacheResponse get(URI uri, String rqstMethod, Map<String, List<String>> rqstHeaders) throws IOException {
        if("GET".equals(rqstMethod)){
            SimpleCacheResponse response = responses.get(uri);
            if(response != null && response.isExpired()){
                responses.remove(response);
                response = null;
            }
            return response;
        }else {
            return null;
        }
    }

    @Override
    public CacheRequest put(URI uri, URLConnection conn) throws IOException {
        if(responses.size() >= maxEntries){
            return null;
        }
        CacheControl control = 
                new CacheControl(conn.getHeaderField("Cache-Control"));
        if(control.noStore()){
            return null;
        }else if (!conn.getHeaderField(0).startsWith("GET")) {
            return null;
        }
        SimpleCacheRequest request = new SimpleCacheRequest();
        SimpleCacheResponse response = 
                new SimpleCacheResponse(request, conn, control);
        responses.put(uri, response);
        return request;
    }

}

之后只需要调用ResponseCache.setDefaultCache(new SimpleResponseCache)即可完成安装。

Cookie技术用于存储持久的客户端状态。Java中实现简单的Cookie功能非常容易,只需要下面两行代码即可完成安装:

CookieManager manager = new CookieManager();
CookieHandler.setDefault(manager);

如果想要控制cookie的接收,可以使用CookiePolicy:

  • CookiePolicy.ACCEPT_ALL:接受所有cookie;
  • CookiePolicy.ACCEPT_NONT:不接受任何cookie;
  • CookiePolicy.ACCEPT_ORIGINAL_SERVER:只接受第一方cookie。

只需要调用manager.setCookiePolicy并传入上面的某个参数即可,当然也可以通过实现CookiePolicy接口来自定义cookie接收策略。
如果想要在本地存放cookie(比如保存在磁盘上),可以通过manager.getCookieStore()方法获取CookieStore对象,里面存放着所有cookie,每个cookie封装在一个HttpCookie对象中。

实例4:启用Cookie功能并打印cookies

public static void main(String[] args){
    CookieManager manager = new CookieManager();
    CookieHandler.setDefault(manager);

    String urlString = "https://www.baidu.com";
    try {
        URL url = new URL(urlString);
        URLConnection connection = url.openConnection();
        try {
            List<HttpCookie> cookies = manager.getCookieStore().get(url.toURI());
            for(HttpCookie cookie : cookies){
                System.out.println(cookie);
            }
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    } catch (IOException e) {
        System.out.println("Fail to connect to " + urlString);
    }
}

配置连接

URLConnection类提供了以下字段,用于定义如何向服务器发出请求:

  • protected URL url;
  • protected boolean doInput = true;
  • protected boolean doOutout = false;
  • protected boolean allowUserInteracton = defaultAllowUserInteraction;
  • protected boolean useCaches = defaultUseCaches;
  • protected long ifModifiedSince = 0;
  • protected boolean connected = false;

这些字段都是protected的,因此需要通过它们的getter和setter来访问。这里有两个例外,一是url字段只有getter而没有setter,因此连接在打开之后就无法改变指向的url,二是connected既没有getter也没有setter。
除此之外,还有一些方法,用于定义URLConnection的默认行为:

public static boolean getDefaultAllowUserInteraction()
public static void setDefaultAllowUserInteraction(boolean defaultallowuserinteraction)
public boolean getDefaultUseCaches()
public void setDefaultUseCaches(boolean defaultusecaches)

新的默认值只在调用这些方法之后的URLConnection对象中生效。
下面是各个字段的功能介绍:
(1)url:指定了这个URLConnection对象连接的URL。这个字段在构造函数中赋值,此后不能再改变。
(2)connected:如果连接已经打开,字段值为true;如果连接关闭,字段值为false。这个字段无法被设置,也无法读取,只能由URLConnection的子类访问。任何导致URLConnection连接的方法都会将这个字段设置为true(connect()、getInputStream()、getOutputStream()),任何导致连接断开的方法都会将这个字段设置为false(URLConnection中没有这样的方法,但是其他子类中可能有)。
(3)allowUserInteraction:该字段指示了是否允许用户交互(比如输入用户名和密码),默认false。
(4)doInput:如果URLConnection可以用来读取,则该字段为true,否则为false。默认true。
(5)doOutput:如果URLConnection可以用来写入,则该字段为true,否则为false。默认false。
(6)ifModifiedSince:long类型,表示上一次修改的时间。这个字段会被放在If-Modified_Since字段中,默认是1970年1月1日子夜12:00。
(7)useCaches:确定是否可以使用缓存,true代表可以,false代表不可以,默认true。

超时

有4个方法可以查询和修改连接的超时值:

public void setConnectTimeout(int timeout)
public int getConnectTimeout()
public void setReadTimeout(int timeout)
public int getReadTimeout()

其中,ConnectTimeout指的是等待建立连接的时间,ReadTimeout指的是等待数据到达的时间。这些方法都用毫秒作为单位,并且将0解释为永不超时。

自定义请求首部字段

在发送请求报文时,系统会自动加上必要的首部字段。如果需要添加自定义的首部字段,可以使用addRequestProperty(String name,String value)方法。获取自定义的首部字段集(不包括系统加上的默认部分),可以使用getRequestProperties()方法。

向服务器写入数据

有时候会需要向URLConnection写入数据,最常见的是使用POST方法向Web服务器提交表单,一般流程如下:

  1. (需要在编程之前完成)根据网页源代码确定提交表单的格式以及表单提交的地址,并构建出包含表单内容的字符串;
  2. 根据表单提交地址创建一个URL对象,并获取URLConnection对象;
  3. 调用setDoOutput(true)设置URLConnection为可以写入,此时方法会自动变成POST;
  4. 调用getOutputStream()获取输出流,并写入表单内容,注意内容最后要加上“\r\n”;
  5. 调用getInputStream()获取输入流,读取服务器响应。

实例5:

//用于构造表单内容
public class QueryString {

    StringBuilder query = new StringBuilder();

    public synchronized void add(String name, String value){
        if(query.length() != 0){
            query.append("&");
        }
        encode(name, value);
    }

    private synchronized void encode(String name,String value){
        try {
            query.append(URLEncoder.encode(name,"UTF-8"));
            query.append('=');
            query.append(URLEncoder.encode(value,"UTF-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    public void clear(){
        query = new StringBuilder();
    }

    @Override
    public String toString() {
        return query.toString();
    }
}

//用于发送表单
public class FormPoster{
    private URL url;
    private URLConnection connection = null;
    private QueryString query;

    public FormPoster(URL url) throws IOException {
        String protocol = url.getProtocol().toLowerCase(Locale.US);
        if(!protocol.equals("http") && !protocol.equals("https")){
            throw new IllegalArgumentException("Only works for HTTP and HTTPS!");
        }
        this.url = url;
        connection = url.openConnection();
        query = new QueryString();
    }

    public void add(String name, String value){
        query.add(name, value);
    }

    public void addRequestProperty(String key, String value){
        connection.addRequestProperty(key, value);
    }

    public URL getURL(){
        return url;
    }

    public String getQuery(){
        return query.toString();
    }

    public URLConnection getConnection(){
        return connection;
    }

    public InputStream post() throws IOException{
        connection.setDoOutput(true);
        try(OutputStreamWriter out = 
                new OutputStreamWriter(connection.getOutputStream(), "UTF-8")){
            out.write(query.toString());
            out.write("\r\n");
            out.flush();
        }

        return connection.getInputStream();
    }
}

HttpURLConnection

HttpURLConnection是URLConnection的抽象子类,提供了一些额外的方法。事实上,当使用http URL时,调用URL的openConnection()方法返回的就是HttpURLConnection的一个实例,因此可以将其强制转换成HttpURLConnection类型。
下面列举HttpURLConnection类相比于URLConnection类新增的方法:
(1)setRequestMethod(),用于设置请求方法;
(2)getResponseCode()与getResponseMessage(),用于获取响应码和相应消息(404 Not Found);
(3)getErrorStream(),用于获取在服务器遇到错误时返回的信息(如404时出现的页面);
(4)setFollowRedirects(),用于设置是否跟随重定向(全局);setInstanceFollowRedirects(),用于设置单个实例是否跟随重定向;
(5)setChunkedStreamingMode(int chunkedLength),启用分块传输模式,并设置分块大小。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值