Java网络编程

Java网络编程

1、连接到服务器

1.1、使用telnet

telnet是一种用于网络编程的非常强大的调试工具,可以在命令shell中输入telnet来启动它。

注:在Windows中需要激活telnet,需要到“控制面板”,选择“程序”,点击“打开/关闭Windows特性”,选择“Telnet客户端”复选框。

例子:

telnet time-a.nist.gov 13

可以连接到大多数UNIX计算机都支持的“当日时间服务”。而刚才所连接的那台服务器是由国家标准与技术研究所运维的,这家研究所负责铯原子钟的计量时间。按照惯例,“当日时间”服务总是连接到端口13。

注:在网络术语中,端口并不是指物理设备,而是为了便于实现服务器与客户端之间的通信所使用的抽象概念

运行在远程计算机上的服务器软件不停地等待那些希望与端口13连接的网络请求。当远程计算机上的操作系统接收到一个请求与端口13连接的网络数据包时,它便唤醒正在监听网络连接请求的服务器进程,并为两者建立连接。这种连接将一直保持下去,直至一方终止。

当你开始用time-a.nist.gov在端口13上建立telnet对话是,网络软件中有一段代码会清楚地知道应该将字符串“time-a.nist.gov”转换为正确的IP地址129.6.15.28。随后telnet软件发送一个请求给该地址,请求到一个端口13的连接。一旦建立连接,远程程序便发送回一行数据然后关闭该连接。

例:

telnet horstmann.com 80

键入以下内容:
GET / HTTP/1.1

Host: horstmann.com

然后连按两次Enter键,将得到一个HTML格式的文本页

1.2、用Java连接到服务器

下面这个程序的作用是连接到某个端口并打印出它所找到的信息

示例程序
import java.io.IOException;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class SocketTest {
    public static void main(String[] args) throws IOException {
        try(var s=new Socket("time-a.nist.gov",13);
            var in=new Scanner(s.getInputStream(), StandardCharsets.UTF_8)){
            while (in.hasNextLine())
            {
                String line=in.nextLine();
                System.out.println(line);
            }
        }
    }
}

运行结果:

59670 22-04-01 04:51:43 50 0 0 727.9 UTC(NIST) *

Process finished with exit code 0

代码解读

var s=new Socket(“time-a.nist.gov”,13);

此行代码用于打开一个套接字,它也是网络软件中的一个抽象概念,负责启动该程序内部和外部之间的通信。

该构造器接受一个远程地址和端口号,若连接失败则抛出UnknownHostException异常;若存在其他问题则抛出IOException异常,其中UnknownHostException异常为IOException异常的子类

一旦套接字被打开,java.net.Socket类中的getInputStream方法将会返回一个InputStream对象,一旦获取了这个流,该程序便把每一行打印在标准输出。这个过程一直持续到流发送完毕服务器断开连接为止。

该程序仅适用于非常简单的服务器,在比较复杂的网络程序中,客户端发送请求数据给服务器,而服务器可能在响应结束时并不立刻断开连接。java.net包提供的编程接口与操作文件时所使用的接口基本相同

相关接口
API java.net.Socket

Socket(String host,int port)
构建一个套接字,用来连接给定的主机和端口

InputStream getInputStream()
OutputStream getOutputStream()
获取可以从套接字中读取数据的流,以及可以想套接字写出数据的流

1.3、套接字超时

从套接字读取信息时,在有数据可供访问之前,该操作会被阻塞。若此时主机不可达,那么应用会等待很长的时间,并且因为受底层操作系统的限制而最终导致超时

对于不同的应用应该确定合理的超时值,然后调用setSoTimeout方法设置这个超时值(ms)

var s=new Socket(…);

s.setSoTimeout(10000);

如果已经为套接字设置了超时值,并且以后的读写操作在未完成前就超过了时间限制,呢么这些操作将会抛出SocketTimeoutException异常

Socket(String host,int port)构造器将会一直无限期地持续下去,直到建立了到达主机的初始连接为止。可以先构建一个无连接的套接字,然后再使用一个超时来进行连接的方法解决此问题

var s=new Socket();

s.connect(new InetSocketAddress(host,port),timeout);

相关接口
API java.net.Socket
    
Socket()
    创建一个还没被连接的套接字
void connect(SocketAddress address)
    将套接字连接到给定的地址
void connect(SocketAddress address,int timeoutInMilliseconds)
    将套接字连接到给定的地址.若在给定的时间内没有响应,则返回
void setSoTimeout(int timeoutInMilliseconds)
    设置该套接字上读请求的阻塞时间。若超时,则抛出SocketTimeoutException异常
boolean isConnected()
    若该套接字已被连接,则返回true
boolean isClosed()
    若该套接字已被关闭,则返回true

1.4、因特网地址

因特网地址是由一串数字表示的主机地址,一个因特网地址由四个字节组成(在IPv6中为6字节),比如129.6.15.28。但是如若需要在主机名和因特网地址间进行转换,可以用InetAddress类

静态的getByName方法可以返回代表某个主机的InetAddress对象,例如

InetAddress address=InetAddress.getByName(“time-a.nist.gov”);

将返回一个InetAddress对象,该对象封装了4个字节的序列: 129.6.15.28.然后个亿使用getAddress方法访问这些字节

byte[] addressBytes=address.getAddress();

一些访问量较大的主机通常会对应多个因特网地址,以实现负载均衡。例如主机名google.com对应多个不同的因特网地址。当访问主机时,会随机选取其中一个。可以通过调用getAllByName方法来获得所有主机

InetAddress[] addresses=InetAddress.getAllByName(host);

如果要获得本地主机的地址,若只是要求得到localhost的地址,那总会得到本地回环地址127.0.0.1,但是其他程序无法用这个地址来连接到这台机器上。此时可以通过调用静态的getLocalHost方法来得到本地主机的地址:
InetAddress address=InetAddress.getLocalHost();

示例程序

下面这段代码将打印出该主机的所有因特网地址

import java.net.InetAddress;
import java.net.UnknownHostException;

public class InetAddressTest {
    public static void main(String[] args) throws UnknownHostException {
        if(args.length>0)
        {
            String host=args[0];
            InetAddress[] addresses=InetAddress.getAllByName(host);
            for(InetAddress a:addresses)
            {
                System.out.println(a);
            }
        }else {
            InetAddress localHostAddress=InetAddress.getLocalHost();
            System.out.println(localHostAddress);
            System.out.println(localHostAddress.getHostName());
            System.out.println(localHostAddress.getHostAddress());
        }
    }
}
相关接口
API java.net.InetAddress

static InetAddress getByName(String host)
static InetAddress[] getAllByName(String host)
	为给定的主机名创建一个InetAddress对象,或者一个包含了该主机名所对应的所有因特网地址的数组
static InetAddress getLocalHost()
	为本地主机创建一个InetAddress对象
byte[] getAddress()
	返回一个包含数字型地址的字节数组
String getHostAddress()
	返回一个由十进制组成的字符串,各数字间用圆点符号隔开,例如"129.6.15.28"
String getHostName()
	返回主机名

2、实现服务器

在上一节我们实现了一个基本的网络客户端,并且用它从因特网上获取了数据。在这一节中我们将实现一个简单的服务器,它可以向客户端发送信息。

2.1、服务器套接字

一旦启动了服务器程序,它便会等待某个客户端连接到它的端口。在下面的实例程序中,我们选择端口8189,因为所有的标准服务都不使用这个端口。ServerSocket类用于建立套接字

var s=new ServerSocket(8189);

用于建立一个负责监控端口8189的服务器。以下命令

Socket incoming =s.accept();

用于告诉程序不断地等待直至有客户端连接到这个端口,一旦有人通过网络发送了正确的连接请求并以此连接到了端口上,该方法将会返回一个表示连接已建立的Socket对象。可以使用这个对象来得到输入和输出流:

InputStream inStream=incoming.getInputStream();

OutputStream outStream=incomng.getOutputStream();

服务器发送给服务器输出流的所有信息都会编程客户端程序的输入。同时来自客户端的所有输出都会被包含在服务器输入流中。

再下面的实例程序中我们都要通过套接字来发送文本,所以我们将流转换为扫描器和写入器

var in=new Scanner(inStream,StandardCharsets.UTF-8);

var out=new PrintWriter(new OutputStreamWriter(outStream,StandardCharsets.UTF-8),true/* autoFlush */);

以下代码将给客户端发送一条问候消息

out.println(“Hello! Enter BYE to exit.”);

当使用telnet通过端口8189连接到这个服务器程序时,将会在终端屏幕上看到上述问候信息

在这个简单的服务器程序中,他只是读取客户端输入,每次读取一行,并回送这一行。这表明服务器程序接收到了客户端的输入。当然,实际应用中的服务器都会对输入进行计算并返回计算结果。

String line=in.nextLine();

out.println("Echo: "+line);

if(line.trim().equals(“BYE”)) done=true;

在代码的最后关闭了连接进来的套接字

incoming.close();

每一个服务器程序,比如一个HTTP Web服务器,都不间断地执行下面这个循环:

  1. 通过输入数据流从客户端接收一个命令(“get me this information”)
  2. 解码这个客户端命令
  3. 收集客户端所请求的信息
  4. 通过输入数据流发送消息给客户端
示例程序
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class EchoServer {
    public static void main(String[] args) throws IOException {
        try(var s=new ServerSocket(8189))
        {
            //wait for client connection
            try(Socket incoming=s.accept())
            {
                //服务器输入流,接收客户端的输出流
                InputStream inStream=incoming.getInputStream();
                //服务器输出流,变成客户端的输入流
                OutputStream outStream=incoming.getOutputStream();

                try (var in=new Scanner(inStream, StandardCharsets.UTF_8)){
                    var out=new PrintWriter(
                            new OutputStreamWriter(outStream,StandardCharsets.UTF_8),
                            true/* autoFlush */);

                    out.println("Hello! Enter BYE to exit.");

                    //echo client input
                    var done=false;
                    while (!done&&in.hasNextLine())
                    {
                        String line=in.nextLine();
                        out.println("Echo: "+line);
                        if(line.trim().equals("BYE"))
                        {
                            done=true;
                        }
                    }
                }
            }
        }
    }
}
相关接口
API java.net.ServerSocket

ServerSocket(int port)
	创建一个监听端口的服务器套接字
Socket accept()
	等待连接。该方法阻塞当前线程直到建立连接为止。该方法返回一个Socket对象,程序可以通过这个对象与连接中的客户端进行通信
void close()
	关闭服务器套接字

2.2、为多个客户端服务

前面例子的简单服务器存在一个问题。假设我们希望有多个客户端同时连接到我们的服务器上。通常,服务器总是不间断地运行在服务器计算机上,来自整个因特网的用户希望同时使用服务器。前面的简单服务器会提供对客户端连接的支持,使得任何一个客户端都可以因长时间地连接服务而独占服务,其实我们可以利用线程把这个问题解决得更好

每当程序建立一个新的套接字连接,也就是说当调用accept()时,将会启动一个新的线程来处理服务器和该客户端之间的连接,而主程序将立即返回并等待下一个连接。为了实现这种机制,服务器应该具有类似以下代码的循环操作

while(true)
{	
	Socket incoming=s.accept();
	var r=new ThreadedEchoHandler(incoming);
	var t=new Thread(r);
	t.start();

}

ThreadedEchoHandler类实现了Runnable接口,而且在它的run方法中包含了与客户端循环通信的代码

class TheadedEchoHandler implements Runnable()

{
	. . .
    public void run()
    {
        try(InputStream inStream=incoming.getInputStream();
           OutputStream outStream=incoming.getOutputStream())
        {
         //   Process input and send response 
        }catch(IOException e)
        {
         //   Handle exception
        }
    }
    
}

由于每一个连接都会启动一个新的线程,因而多个客户端就可以同时连接到服务器了,对此进行一个简单的测试:

  1. 编译和运行服务器程序(下面这个程序)
  2. 打开数个telnet窗口
  3. 在这些窗口之间切换,并键入命令。可以同时通过这些窗口进行通信
  4. 完成后切换到启动服务器程序的窗口,并通过CTRL+C强行关闭它
示例程序
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class ThreadedEchoServer {
    public static void main(String[] args) {
        try (var s=new ServerSocket(8189)){
            int i=1;
            while (true)
            {
                Socket incoming=s.accept();
                System.out.println("Spawning "+i);
                Runnable r=new ThreadedEchoHandler(incoming);
                var t=new Thread(r);
                t.start();
                i++;
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
class ThreadedEchoHandler implements Runnable
{
    private Socket incoming;

    public ThreadedEchoHandler(Socket incoming) {
        this.incoming = incoming;
    }

    @Override
    public void run() {
        try (InputStream inStream=incoming.getInputStream();
             OutputStream outStream=incoming.getOutputStream();
             var in=new Scanner(inStream, StandardCharsets.UTF_8);
             var out=new PrintWriter(new OutputStreamWriter(outStream,StandardCharsets.UTF_8),true))
        {
            System.out.println(Thread.currentThread().getName()+"启动");
            out.println("Hello! Enter BYE to exit");

            //echo client input
            var done=false;
            while (!done&&in.hasNextLine())
            {
                String line=in.nextLine();
                out.println("Echo: "+line);
                if (line.trim().equals("BYE"))
                {
                    done=true;
                }
            }
            System.out.println(Thread.currentThread().getName()+"终止");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.3、半关闭

半关闭(half-close)提供了这样一种能力:套接字连接的一端可以终止其输出,同时仍旧可以接受来自另一端的数据

例如我们在向服务器传输数据,但一开始不知道要传输多少数据。在向文件写数据是,我们只需要在数据写入后关闭文件即可,但是如果要关闭一个套接字,那么与服务器的连接将立刻断开,因而也就无法读取服务器的响应了。

使用半关闭的方法可以解决上述问题。可以通过关闭一个套接字的输出流来表示发送给服务器的请求数据已经结束,但是必须保持输入流处于打开状态。

如下代码演示了如何在客户端使用半关闭方法:

try(var socket=new Socket(host,port))
{
    var in=new Scanner(socket.getInputStream(),StandardCharsets.UTF-8);
    var writer=new PrintWriter(socket.getOutputStream());;
    //send request data
    writer.priint(...);
    writer.flush();
    socket.shutdownOutput();
    //now socket is half-closed
    //read response data
    while(in.hasnextLine()!=null)
    {
        String line=in.nextLine();
        . . .
    }
}

服务器端将读取输入信息,直至到达输入流的结尾,然后它再发送响应。

该协议只适用于一站式(one-shot)的服务,例如HTTP服务,在这种服务中,客户端连接服务器,发送一个请求,捕获响应信息,然后断开连接

相关接口
API java.net.Socket

void shutdownOutput()
	将输出流设置为"流结束"
void shutdownInput()
	将输入流设置为"流结束"
boolean isOutputShutdown()
	如果输出流已被关闭,返回ture
boolean isInputShutdown()
	如果输入流已被关闭,返回true

2.4、可中断套接字

当连接到一个套接字时,当前线程将会被阻塞直至建立连接或产生超时。同样地,当通过套接字读数据时,当前线程也会被阻塞直至操作成功或产生超时。

当线程因套接字无法响应而发生阻塞时,则无法通过调用interrupt来解除阻塞。为了中断套接字操作,可以使用java.nio包提供的一个特性——SocketChannel类。可以使用如下方法打开SocketChannel:

SocketChannel channel=SocketChannel,open(new InetSocketAddress(host,port));

通道(channel)并没有与之相关联的流,实际上它所拥有的read和write方法都是通过使用Buffer对象来实现的。ReadableByteChannel接口和WritableByteChannel接口都声明了这两个方法。

如果不想处理缓冲区,可以使用Scanner类从SocketChannel中读取信息,因为Scanner有一个带ReadableByteChannel参数的构造器:

var in=new Scanner(channel,StandardCharsets.UTF-8);

通过调用静态方法Channels.newOutputStream,可以将通道转换成输出流。

OutputStream outStream=Channels.newOutputStream(channle);

当线程正在执行打开、读取或写入操作时,如果线程发生中断,那么这些操作将不会陷入阻塞,而是以抛出异常的方式结束。

示例程序

下面这个程序对比了可中断套接字和阻塞套接字:服务器将连续发送数字,并在每发送是个数字后停滞一下。点击两个按钮中的任何一个都会启动一个线程来连接服务器并打印输出。第一个线程使用可中断套接字,而第二个线程使用阻塞套接字。如果在第一批的十个数字的读取过程中点击“Cancel”按钮,这两个线程都会中断。

import javax.swing.*;
import java.awt.*;
import java.io.*;
import java.net.*;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class InterruptibleSocketTest {
    public static void main(String[] args) {
        EventQueue.invokeLater(()->{
            var frame=new InterruptibleSocketFrame();
            frame.setTitle("InterruptibleSocketFrame");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
        });
    }
}

class InterruptibleSocketFrame extends JFrame{
    private Scanner in;
    private JButton interruptibleButton;
    private JButton blockingButton;
    private JButton cancelButton;
    private JTextArea messages;
    private TestServer server;
    private Thread connectThread;

    /**
     * 一个监听8189端口并发送数据给客户端的多线程服务器
     * 每发送十个数字后停滞
     */
    class TestServer implements Runnable{
        @Override
        public void run() {
            try (var s=new ServerSocket(8189)){
                while (true)
                {
                    Socket incoming=s.accept();
                    Runnable t=new TestServerHandler(incoming);
                    new Thread(t).start();
                }
            } catch (IOException e) {
                messages.append("\nTestServer.run: "+e);
            }
        }
    }

    /**
     * 此类处理一个服务器套接字连接的客户端输入
     */
    class TestServerHandler implements Runnable{
        private Socket incoming;
        private int counter;

        public TestServerHandler(Socket incoming) {
            this.incoming = incoming;
        }

        @Override
        public void run() {
            try {
                try {
                    OutputStream outputStream=incoming.getOutputStream();
                    var out=new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8),true);
                    while (counter<100)
                    {
                        counter++;
                        if(counter<=10){
                            out.println(counter);
                        }
                        Thread.sleep(100);
                    }
                }finally {
                    incoming.close();
                    messages.append("Closing server\n");
                }
            }catch (Exception e)
            {
                messages.append("\nTestServerHandler.run: "+e);
            }
        }
    }
    public InterruptibleSocketFrame()
    {
        //在窗口上方添加面板,并将按钮添加进去
        var northPanel=new JPanel();
        add(northPanel, BorderLayout.NORTH);

        final int TEXT_ROWS=20;
        final int TEXT_COLUMNS=60;
        messages=new JTextArea(TEXT_ROWS,TEXT_COLUMNS);
        add(new JScrollPane(messages));

        interruptibleButton=new JButton("Interruptible");
        blockingButton=new JButton("Blocking");
        northPanel.add(interruptibleButton);
        northPanel.add(blockingButton);

        //为interruptibleButton添加时间监听器
        interruptibleButton.addActionListener(e ->
        {
            interruptibleButton.setEnabled(false);
            blockingButton.setEnabled(false);
            cancelButton.setEnabled(true);
            connectThread=new Thread(()->{
                try {
                    connectInterruptibly();
                }catch (IOException exception)
                {
                    messages.append("\nInterruptibleSocketTest.cconnectInterruptibly: "+exception);
                }
            });
            connectThread.start();
        });

        //为blockingButton添加事件监听器
        blockingButton.addActionListener(e ->
        {
            interruptibleButton.setEnabled(false);
            blockingButton.setEnabled(false);
            cancelButton.setEnabled(true);
            connectThread=new Thread(()->{
               try {
                   connectBlocking();
               }catch (IOException exception)
               {
                   messages.append("\nInterruptibleSocketTest.connectBlocking: "+exception);
               }
            });
            connectThread.start();
        });
        
        //为cancelButton添加事件监听器
        cancelButton=new JButton("Cancel");
        cancelButton.setEnabled(false);
        northPanel.add(cancelButton);
        cancelButton.addActionListener(e -> {
            connectThread.interrupt();
            cancelButton.setEnabled(false);
        });
        server=new TestServer();
        new Thread(server).start();
        pack();
    }
    /**
     * 连接测试服务器,使用可中断套接字
     */
    public void connectInterruptibly()throws IOException
    {
        messages.append("Interruptible:\n");
        try (SocketChannel channel=SocketChannel.open(new InetSocketAddress("localhost",8189))){
            System.out.println(Thread.currentThread().getName()+"执行");
            in=new Scanner(channel,StandardCharsets.UTF_8);
            while (!Thread.currentThread().isInterrupted())
            {
                messages.append("Reading ");
                if(in.hasNextLine())
                {
                    String line=in.nextLine();
                    messages.append(line);
                    messages.append("\n");
                }
            }
        }finally {
            System.out.println(Thread.currentThread().getName()+"中断");
            EventQueue.invokeLater(()->{
                messages.append("Channel closed\n");
                interruptibleButton.setEnabled(true);
                blockingButton.setEnabled(true);
            });
        }
    }

    /**
     * 连接测试服务器,使用阻塞套接字
     */
    public void connectBlocking()throws IOException
    {
        messages.append("Blocking:\n");
        try (var sock=new Socket("localhost",8189)){
            System.out.println(Thread.currentThread().getName()+"执行");
            in=new Scanner(sock.getInputStream());
            while (!Thread.currentThread().isInterrupted())
            {
                messages.append("Reading ");
                if(in.hasNextLine())
                {
                    String line=in.nextLine();
                    messages.append(line);
                    messages.append("\n");
                }
            }
        }finally {
            System.out.println(Thread.currentThread().getName()+"中断");
            EventQueue.invokeLater(()->{
                messages.append("Socket closed\n");
                interruptibleButton.setEnabled(true);
                blockingButton.setEnabled(true);
            });
        }
    }

}
相关接口
API java.net.InetSocketAddress


InetSocketAddress(String hostname,int port)
	用给定的主机和端口参数创建一个地址对象,并在创建过程中解析主机名。如果主机名不能被解析,那么该地址对象的unresolved属性将被设置为true
boolean isUnresolved()
	如果不能解析该地址对象,则返回true
API java.nio.channels.SocketChannel


static SocketChannel open(SocketAddress address)
	打开一个套接字通道,并将其连接到远程地址
API java.nio.channels.Channels


static InputStream newInputStream()
	创建一个输入流,用以从指定的通道读取数据
static OutputStream newOutputStream()
	创建一个输出流,用以向指定的通道写入数据

2.5、ServerSocket

3、获取Web数据

3.1、URL和URI

URL和URLConnection类封装了大量复杂的实现细节,这些细节涉及如何从远程站点获取信息。例如,可以自一个字符串构建一个URL对象:

var url=new URL(urlstring);

如果只是想获得该资源的内容,可以使用URL类的openStream方法产生一个InputStream对象:

InputStream inStream=url.openStream();

var in=new Scanner(inStream,StandardCharsets.UTF_8);

java.net包对**统一资源定位符(Uniform Resourse Locator,URL)统一资源标识符(Uniform Resourse Identifier,URI)**进行了非常有用的区分

URI是个纯粹的语法结构,包含用来指定Web资源的字符串的各种组成部分。URL是URI的一个特例,它包含了用于定位Web资源的足够信息。其他URI,比如:

mailto:cay@horstmann.com

则不属于定位符,因为根据该标识符我们无法定位任何数据。像这样的URI我们称之为URN(uniform resource name,统一资源名称)

在Java类库里,URI类并不包含任何用于访问资源的方法,它的唯一作用就是解析。但是URL类可以打开一个连接到资源的流。因此,URL类只能作用域那些Java类库知道该如何处理的模式 (scheme) ,比如http:https:ftp:本地文件系统(file:)JAR文件(jar:).

URI规范给出了标记这些标识符的规则。一个URI具有以下句法:.

[scheme:]schemeSpecificPart[#fragment]

例如:
http:/google.com?q=Beach+Chalet

ftp://username:password@ftp.yourserver.com/pub/file.txt

上式中,[. . .]表示可选部分,并且 :和#可以被包含在标识符内。

包含*scheme:*部分的URI称为绝对URI。否则称为相对URI

如果绝对URI的schemeSpecificPart部分不是以 / 开头的,我们就称它是不透明的,例如:

mailto:cay@horstmann.com

所有绝对的透明URI和所有相对URI都是分层的(hierarchical)。例如:

http://horstmann.com/index.html

…/…/java/net/Socket.html#Socket()

一个分层URI的schemeSpecificPart具有以下结构:

[//authority] [path] [?query]

在这里,[. . .]同样表示可选的部分。

对于那些基于服务器的URI,authority部分具有以下形式:

[user-info@]host[:port]

port必须是一个整数。

**RFC2396(标准化URI的文献)**还支持一种基于注册表的机制,此时authority采用了一种不同的格式,不过这种情况并不常见。

URI的作用之一是解析标识符并将它分解成各种不同的组成部分。你可以用以下方法读取它们:

getScheme

getSchemeSpecificPart

getAuthority

getUserInfo

getHost

getPort

getPath

getQuery

getFragment

URI类的的另一个作用是处理绝对标识符和相对标识符。如果存在如下的绝对URI:

http://docs/mycompany.com/api/java/net/Socket.html

和一个如下的相对URI:

…/…/java/net/Socket.html#Socket()

那么可以用它们组合出一个绝对URI:

http://docs/mycompany.com/api/java/net/Socket.html#Socket()

这个过程称为解析相对URI,于此相反的过程称为相对化(relativization),例如,假设有一个基本URI:

http://docs/mycompany.com/api

和另一个URI:

http://docs.mycompany.com/api/java/lang/String.html

那么相对化之后的URI就是:
java/lang/String.html

URI类同时支持以下两个操作:
relative=base.relativize(combined);

combined=base.resolve(relative);

构造方法
方法名说明
URL(String spec)根据 String 表示形式创建 URL 对象
URL(String protocol, String host, int port, String file)根据指定 protocol、host、port 号和 file 创建 URL 对象
URL(String protocol, String host, int port, String file, URLStreamHandler handler)根据指定的 protocol、host、port 号、file 和 handler 创建 URL 对象
URL(String protocol, String host, String file)根据指定的 protocol 名称、host 名称和 file 名称创建 URL
URL(URL context, String spec)通过在指定的上下文中对给定的 spec 进行解析创建 URL
URL(URL context, String spec, URLStreamHandler handler)通过在指定的上下文中用指定的处理程序对给定的 spec 进行解析来创建 URL
方法摘要
返回值方法名说明
intgetDefaultPort()获取与此 URL 关联协议的默认端口号
StringgetFile()获取此 URL 的文件名
StringgetHost()获取此 URL 的主机名(如果适用)
StringgetPath()获取此 URL 的路径部分
intgetPort()获取此 URL 的端口号
StringgetProtocol()获取此 URL 的协议名称
StringgetQuery()获取此 URL 的查询部分
StringgetRef()获取此 URL 的锚点(也称为“引用”)
StringgetUserInfo()获取此 URL 的 userInfo 部分
URLConnectionopenConnection()返回一个 URLConnection 对象,它表示到 URL 所引用的远程对象的连接
URLConnectionopenConnection(Proxy proxy)与 openConnection()类似,所不同是连接通过指定的代理建立;不支持代理方式的协议处理程序将忽略该代理参数并建立正常的连接
InputStreamopenStream()打开到此 URL 的连接并返回一个用于从该连接读入的 InputStream
static voidsetURLStreamHandlerFactory(URLStreamHandlerFactory fac)设置应用程序的 URLStreamHandlerFactory
StringtoExternalForm()构造此 URL 的字符串表示形式
StringtoString()构造此 URL 的字符串表示形式
URItoURI()返回与此 URL 等效的 URI

3.2、使用URLConnection获取信息

如果想从某个Web资源获取更多信息,那么应该使用URLConnection类,通过它能够得到比基本的URL类更多的控制功能。

建立连接的步骤

当操作一个URLConnection对象时,必须像下面这样非常小心地安排操作步骤:

1、调用URL类中的openConnection()方法获得URLConnection对象

​ URLConnection connection=url.openConnection();

2、使用以下方法来设置任意的请求属性

​ setDoInput

​ setDoOutput

​ setIfModifiedSince

​ setUseCaches

​ setAllowUserInteraction

​ setRequestProperty

​ setConnectTimeout

3、调用connect方法连接远程资源

​ connection.connect();

除了与服务器建立套接字连接外,该方法还可以向服务器查询头信息**(header information)**。

4、与服务器建立连接后,你可以查询头信息getHeaderFieldKeygetHeaderField这两个方法枚举了消息头的所有字段。getHeaderFields方法返回一个包含了消息头中所有字段的标准Map对象。为了方便使用,以下方法可以查询各标准字段:

​ getContentType

​ getContentLength

​ getContentEncoding

​ getDate

​ getExpiration

​ getLastModified

5、最后,访问资源数据。使用getInputStream方法获取一个输入流用以读取信息(这个输入流与URL类中的openStream方法所返回的流相同)。另一个放啊getContent在实际操作中并不是很有用。由标准内容类型(比如text/plain和image/gif)所返回的对象需要使用com.sun层次结构中的类来进行处理。

警告:一些程序员在使用URLConnection类的过程中形成了错误的观念:他们认为URLConnection类中的getInputStreamgetOutputStream方法与Socket类中的这些方法相似。实际上这种想法并不是十分正确。URLConnection类具有很多表象之下的神奇功能,尤其在处理请求和响应消息头时。正因为如此,严格遵循建立连接的每个步骤显得非常重要


下面介绍URLConnection类中的一些方法。有几个方法可以在与服务器建立连接之前设置连接属性,其中最重要的是setDoInputsetDoOutput。在默认情况下,建立的连接只产生从服务器读取信息的输入流,并不产生任何执行写操作的输出流。如果想获得输出流(例如,用于向一个Web服务器提交数据),那么需要调用:

connection.setDoOutput(true);

接下来,也许想设置某些请求头(request header)。请求头是与请求命令一起被发送到服务器的。

setIfModifiedSince方法用于告诉连接你只对自某个特定日期以来被修改过的数据感兴趣。

最后我们再介绍一个总览全局的方法:setRequestProperty,它可以用来设置对特定协议起作用的任何“名-值(name/value)对”。关于HTTP请求头的格式,参见RFC2616,其中的某些参数没有很好地建档,它们通常再程序员之间口头相传。例如,如果你想访问一个有密码保护的Web页,那么就必须按如下步骤操作:

1、将用户名、冒号和密码以字符串形式连接在一起。

​ String input=username+“:”+password;

2、计算上一步骤所得字符串的Base64编码(Base64编码用于将字节序列编码成可打印的ASCII字符序列)

​ Base64.Encoder encoder=Base64.getEncoder();

​ String encoding=encoder.encodeToString(input.getBytes(StandardCharsets.UTF_8));

3、用“Authorization”这个名字和"Basic"+encoding的值调用setRequestProperty方法。

​ connection.setRequestProperty(“Authorization”,“Basic”+encoding);


一旦调用了connect方法,就可以查询相应头信息了。首先我们将介绍如何枚举所有相应头的字段:

​ String key=connection.getHeaderFieldKey(n);

可以获得相应头的第n个键,其中n从1开始。若n为0或大于消息头的字段总数,该方法返回null。没有方法可以返回字段的数量,必须反复调用getHeaderFieldKey方法直到返回null为止。同样地,调用以下方法:

​ String value=connection.getHeaderField(n)

可以得到第n个值。

getHeaderFields方法可以返回一个封装了响应头字段的Map对象。

​ Map< String , List< string >> headerFields=connection.getHeaderFields();

可以用**connection.getHeaderField(0)headerFields.get(null)**获取响应状态(例如”HTTP/1.1 200 OK“)

为了简便起见,Java提供了6个方法用以访问最常用的消息头类型的值,并在需要的时候将它们转换成数字类型。返回类型为long的方法返回的是从格林尼治时间1970年1月1日起开始计算的秒数。

用于访问相应头值的简便方法
键名方法名返回类型
DategetDatelong
ExpiresgetExpirationlong
Last-ModifiedgetLastModifiedlong
Content-LengthgetContentLengthint
Content-TypegetContentTypeString
Content-EncodinggetContentEncodingString
示例程序

通过下面这个程序可以对URL连接做出一些试验。程序运行起来后请在命令行中输入一个URL以及用户名和密码(可选),例如:

java URLConnectionTest http://yourserver.com user password

该程序将输出以下内容:

  • 消息头中的所有键和值。
  • 上表中6个简便方法的返回值。
  • 被请求资源的前10行信息
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Scanner;

public class URLConnectionTest {
    public static void main(String[] args) {
        try {
            String urlName;
            if(args.length>0) urlName=args[0];
            else urlName="https://yumoyumo.top";

            var url=new URL(urlName);
            URLConnection connection=url.openConnection();

            //set username,password if specified on command line
            if (args.length>2)
            {
                String username=args[1];
                String password=args[2];
                String input=username+":"+password;
                Base64.Encoder encoder=Base64.getEncoder();
                String encoding=encoder.encodeToString(input.getBytes(StandardCharsets.UTF_8));
                connection.setRequestProperty("Authorization","Basic "+encoding);
            }

            connection.connect();;

            //print header fields
            Map<String, List<String>> headers=connection.getHeaderFields();
            for(Map.Entry<String,List<String>>entry:headers.entrySet())
            {
                String key=entry.getKey();
                for (String value:entry.getValue())
                    System.out.println(key+": "+value);
            }

            //print convenience functions

            System.out.println("----------");
            System.out.println("getContentType: "+connection.getContentType());
            System.out.println("getContentLength: "+connection.getContentLength());
            System.out.println("getContentEncoding: "+connection.getContentEncoding());
            System.out.println("getDate: "+connection.getDate());
            System.out.println("getExpiration: "+connection.getExpiration());
            System.out.println("getLastModified: "+connection.getLastModified());
            System.out.println("----------");

            String encoding=connection.getContentEncoding();
            if (encoding==null) encoding="UTF-8";
            try (var in=new Scanner(connection.getInputStream(),encoding)){
                //print first ten lines of contents

                for (int n=1;in.hasNextLine()&&n<=10;n++)
                {
                    System.out.println(in.nextLine());
                }
                if (in.hasNextLine()) System.out.println(". . .");
            }

        }catch (IOException e)
        {
            e.printStackTrace();
        }
    }
}
相关接口
API java.net.URL


InputStream openStream()
	打开一个用于读取资源数据的输入流
URLConnection openConnection()
	返回一个URLConnection对象,该对象负责管理与资源之间的连接。
API java.net.URLConnection

void setDoInput(boolean doInput)
boolean getDoInput()
	如果doInput为true,那么用户可以接收来自该URLConnection的输入。
void setDoOutput(boolean doOutput)
boolean getDoOutput()
	如果doOutput为ture,那么用户可以将输出发送到该URLConnection
    
void setIfModifiedSince(long time)
long getIfModifiedSince()
 	属性ifModifiedSince用于配置该URLConnection对象,使他只获取那些自从某个给定时间以来被修改过的数据。调用方法时需要传入的time参数指的是从格林尼治时间  	   197011日起开始计算的秒数。
void setConnectTimeout(int timeout)
int getConnectTimeout()
	设置或得到连接超时时限(ms)。如果在连接建立之前就已经达到了超时的时限,那么相关联的输入流的connect方法就会抛出一个SocketTimeoutException异常
void setReadTimeout(int timeout)
int getReadTimeout()
	设置读取数据的超时时限(ms)。如果在一个读操作成功之前就已经达到了超时时限,那么read方法就会抛出一个SocketTimeoutException异常
void setRequestProperty(String key,String value)
	设置请求头的一个字段
    
Map<String, List<String>> getRequestProperties()
	返回请求头属性的一个映射表。相同的键对应的所有值被放置在同一个列表中
void connect()
	连接远程资源并获取响应头信息
Map<String, List<String>> getHeaderFields()
	返回响应头的一个映射表。相同的键对应的所有值被放置在同一个列表中
String getHeaderFieldKey(int n)
	得到响应头第n个字段的键。如果n<=0或大于响应头字段的总数,则该方法返回nullString getHeaderField(int n)
	得到响应头第n个字段的值。如果n<=0或大于响应头字段的总数,则该方法返回nullint getContentLength()
	如果内容长度可获得,则返回该长度值,否则返回-1
String getContentType()
	获取内容的类型,比如text/plain或image/gif
String getContentEncoding()
	获取内容的编码机制,比如gzip。这个值不太常用,因为默认的identity编码机制并不是用Content-Encoding头来设定的
    
long getDate()
long getExpiration()
long getLastModifed()
	获取创建日期、过期日以及最后一次被修改的日期。这些日期指的是从格林尼治时间197011日起开始计算的秒数。
    
InputStream getInputStream()
OutputStream getOutputStream()
	返回从资源读取信息或向资源写入信息的流
Object getContent()
	选择适当的内容处理器,以便读取资源数据并将它转换成对象。该方法对于读取诸如text/plain或image/gif之类的标准内容类型并没有什么用处,除非你安装了自己的内	容处理器

3.3、使用HttpURLConnection获取信息

HttpURLConnectionURLConnection子类,所以URLConnection中的方法,HttpURLConnection中都有,HttpURLConnection对URLConnection进行了扩展。
每个 HttpURLConnection 实例都可用于生成单个请求,但是其他实例可以透明地共享连接到 HTTP 服务器的基础网络。请求后在 HttpURLConnection 的 InputStream 或 OutputStream 上调用 close() 方法可以释放与此实例关联的网络资源,但对共享的持久连接没有任何影响。如果在调用 disconnect() 时持久连接空闲,则可能关闭基础套接字。

方法摘要
返回值方法名说明
abstract voiddisconnect()指示近期服务器不太可能有其他请求。
InputStreamgetErrorStream()如果连接失败但服务器仍然发送了有用数据,则返回错误流。
static booleangetFollowRedirects()返回指示是否应该自动执行 HTTP 重定向 (3xx) 的 boolean 值。
longgetHeaderFieldDate(String name, long Default)返回解析为日期的指定字段的值。
longgetHeaderFieldDate(String name, long Default)返回解析为日期的指定字段的值。
booleangetInstanceFollowRedirects()返回此 HttpURLConnection 的 instanceFollowRedirects 字段的值。
StringgetRequestMethod()获取请求方法。
intgetResponseCode()从 HTTP 响应消息获取状态码。
StringgetResponseMessage()获取与来自服务器的响应代码一起返回的 HTTP 响应消息(如果有)。
voidsetChunkedStreamingMode(int chunklen)此方法用于在预先不知道内容长度时启用没有进行内部缓冲的 HTTP 请求正文的流。
voidsetFixedLengthStreamingMode(int contentLength)此方法用于在预先已知内容长度时启用没有进行内部缓冲的 HTTP 请求正文的流。
static voidsetFollowRedirects(boolean set)设置此类是否应该自动执行 HTTP 重定向(响应代码为 3xx 的请求)。
voidsetInstanceFollowRedirects(boolean followRedirects)设置此 HttpURLConnection 实例是否应该自动执行 HTTP 重定向(响应代码为 3xx 的请求)。
voidsetInstanceFollowRedirects(boolean followRedirects)设置此 HttpURLConnection 实例是否应该自动执行 HTTP 重定向(响应代码为 3xx 的请求)。
abstract booleanusingProxy()指示连接是否通过代理
示例程序
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

public class HttpURLConnectionDemo
{
   public static void main(String[] args)
   {
      try
      {
         // 以百度为例
         URL url = new URL("http://www.baidu.com");

         HttpURLConnection connection = (HttpURLConnection) url
               .openConnection();
         // 设置连接超时时间
         connection.setConnectTimeout(10000);
         // 设置读超时时间
         connection.setReadTimeout(10000);
         // 打开到此 URL 引用的资源的通信链接(如果尚未建立这样的连接)
         connection.connect();

         // 获得响应状态信息
         if (connection.getResponseCode() != HttpURLConnection.HTTP_OK)
         {
            System.out.println("请求失败...");
            return;
         }

         // 获得头信息
         Map<String, List<String>> headers = connection.getHeaderFields();
         Iterator<Entry<String, List<String>>> iterator = headers.entrySet()
               .iterator();
         while (iterator.hasNext())
         {
            Entry<String, List<String>> entry = iterator.next();
            System.out.println(entry.getKey() + ":" + entry.getValue());
         }

         System.out.println("响应内容如下:");
         // 内容是文本,直接以缓冲字符流读取
         BufferedReader reader = new BufferedReader(new InputStreamReader(
               connection.getInputStream(), "UTF-8"));
         String data = null;
         while ((data = reader.readLine()) != null)
         {
            System.out.print(data);
         }
         reader.close();
      }
      catch (IOException e)
      {
         e.printStackTrace();
      }
   }
}

3.4、提交表单数据

在上一节我们介绍了如何从Web服务器读取数据。在这一节中,我们介绍如何让程序再将数据反馈回Web服务器和那些被Web服务器调用的程序。

有许多技术可以让Web服务器实现对程序的调用。其中最广为人知的是Java ServletJavaServer Face、微软的ASP(Active Server Pages,动态服务器主页)以及CGI(Common Gateway Interface,通用网关接口)脚本。

服务器端程序用于处理表单数据并生成另一个HTML页,该页会被Web服务器发给浏览器,这个操作过程如下图所示。返回给浏览器的响应页可以包含新的信息(例如,信息检索程序中的响应页)或者只是一个确认。之后,Web浏览器将显示响应页。

​ 图:执行服务器端脚本过程中的数据流

这里不会介绍如何实现服务器端程序,而是将侧重点放在如何编写客户端程序使之与已有的服务器端程序进行交互。

当表单数据被发送到Web服务器时,数据到底由谁来解释并不重要,可能是Servlet或CGI脚本,也有可能时其他服务器端技术。客户端以标准格式将数据发送给Web服务器,而Web服务器则负责将数据传递给具体的程序以产生响应。


在向Web服务器发送信息时,通常有两个命令会被用到:GETPOST

在使用GET命令时,只需将参数附在URL的结尾处即可。这种URL的格式如下:

http://host/path?query

其中,每个参数都具有“名字=值”的形式,而这些参数之间用**&**字符分隔开。参数的值将遵循下面的规则,使用URL编码模式进行编码:

  • 保留字符A-Za-z0-9,以及 . - ~ _
  • 用**+字符替换所有的空格**。
  • 将其他字符编码为UTF-8,并将每个字节都编码为%后面紧跟一个两位的十六制数字。

例如,若要发送San Francisco,CA,可以使用 San+Francisco%2c+CA。

这种编码方式使得在任何中间程序中都不会混入空格和其他特殊字符。

例如在www.google.com/maps得到1 Market Street,San Franscisco,CA的地图,并让响应使用德语,只需访问下面的URL:

http://www.google.com/maps?q=1+Market+Street+San+Fransicco&hl=de


老式的浏览器和代理对在GET请求中能够包含的字符数量做出了限制。因此,POST请求经常用来处理具有大量数据的表单。

POST请求中,我们不会再URL上附着参数,而是从URLConnection中获得输出流,并将名/值对写入到该输出流中。且仍需要对这些值进行URL编码,并用&字符将它们隔开。

下面我们将详细介绍这个过程:

1、在提交数据给服务器端程序之前,首先需要创建一个URLConnection对象

​ var url=new URL(“http://host/path”);

​ URLConnection connection=url.openConnection();

2、调用setDoOutput方法建立一个用于输出的连接

​ connection.setDoOutput(true);

3、调用getOutputStream方法获得一个流,可以通过这个流向服务器发送数据。如果要向服务器发送文本信息,那么可以非常方便地将流包装在PrintWriter对象中

​ var out=new PrintWriter(connection.getOutputStream(),StandardCharsets.UTF_8);

4、向服务器发送数据

​ out.print(name1+“=”+URLEncoder.encode(value1,StandardCharsets.UTF_8)+“&”);

​ out.print(name2+“=”+URLEncoder.encode(value2,StandardCharsets.UTF_8));

5、关闭输出流

​ out.close();

6、调用getInputStream方法读取服务器的响应

示例程序

地址为https://www.yumoyumo.top/wp-login.php的页面用于验证用户的登录,若登录成功则跳转到新的页面。想要在Java程序中使用这个表单,需要知道POST请求的URL和参数

获取请求URL参数

Edge浏览器中按F12进入开发者工具页面,选择网络选项,然后点击登录(提交表单)

点击所有请求中的第一个POST请求,找到对应的请求URL

在负载中即可获取参数log和pwd

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

public class PostTest {
    public static void main(String[] args) throws IOException{

        String urlString="https://www.yumoyumo.top/wp-login.php";
        Map<String,String> props= new HashMap<>(Map.of(
                "log", "yumo1304960237@gmail.com",
                "pwd", "123456"));
        CookieHandler.setDefault(new CookieManager(null,CookiePolicy.ACCEPT_ALL));
        String userAgent=props.remove("User-Agent");
        String redirects=props.remove("redirects");
        String result=doPost(new URL(urlString),props,
                userAgent==null?null:userAgent,
                redirects==null?-1:Integer.parseInt(redirects.toString()));
        System.out.println(result);
    }

    /**
     *
     * @param url the url to post to
     * @param nameValuePairs the query parameters
     * @param userAgent the user agent to use,or null for the default user agent
     * @param redirects the number of redirects to follow manually,or -1 for automatic
     * @return the data return from the sever
     */
    public static String doPost(URL url, Map<String,String> nameValuePairs,String userAgent,int redirects)throws IOException
    {
        //打开连接并设置用户代理
        var connection=(HttpURLConnection)url.openConnection();
        if(userAgent!=null)
            connection.setRequestProperty("User-Agent",userAgent);
        if (redirects>=0)
            //在连接到服务器之前将关闭自动重定向
            connection.setInstanceFollowRedirects(false);
        connection.setDoOutput(true);
        
        
        try (var out=new PrintWriter(connection.getOutputStream())){
            var first=true;
            for(Map.Entry<String,String> pair: nameValuePairs.entrySet())
            {
                if (first)first=false;
                else out.print("&");
                String name=pair.getKey();
                String value=pair.getValue();
                out.print(name);
                out.print('=');
                out.print(URLEncoder.encode(value, StandardCharsets.UTF_8));
            }
        }
        
        //在从写出请求切换到读取响应的任何部分时,就会发生与服务器的实际交互
        String encoding=connection.getContentEncoding();
        
        
        if (encoding==null) encoding="UTF-8";

        if (redirects>0)
        {
            //在发送请求后,获取响应码
            int responseCode = connection.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_MOVED_PERM
                    || responseCode == HttpURLConnection.HTTP_MOVED_TEMP
                    || responseCode == HttpURLConnection.HTTP_SEE_OTHER) {
                String location = connection.getHeaderField("Location");
                if (location != null) {
                    URL base = connection.getURL();
                    connection.disconnect();
                    return doPost(new URL(base, location), nameValuePairs, userAgent, redirects - 1);
                }
            }
        }
        else if(redirects==0)
        {
            throw new IOException("Too many redirects");
        }

        var response=new StringBuilder();
        try (var in=new Scanner(connection.getInputStream(),encoding))
        {
            while (in.hasNextLine())
            {
                response.append(in.nextLine());
                response.append("\n");
            }
        }catch (IOException e)
        {
            InputStream err=connection.getErrorStream();
            if(err==null)throw e;
            try (var in=new Scanner(err)){
                response.append(in.nextLine());
                response.append("\n");
            }
        }
        
        	 //print header fields
            Map<String, List<String>> headers=connection.getHeaderFields();
            for(Map.Entry<String,List<String>>entry:headers.entrySet())
            {
                String key=entry.getKey();
                for (String value:entry.getValue())
                    System.out.println(key+": "+value);
            }

            //print convenience functions

            System.out.println("----------");
            System.out.println("getContentType: "+connection.getContentType());
            System.out.println("getContentLength: "+connection.getContentLength());
            System.out.println("getContentEncoding: "+connection.getContentEncoding());
            System.out.println("getDate: "+connection.getDate());
            System.out.println("getExpiration: "+connection.getExpiration());
            System.out.println("getLastModified: "+connection.getLastModified());
            System.out.println("----------");
        return response.toString();
    }
}

这个程序溢出了url和User-Agent项,并将其他内容都发送到了doPost方法。

在doPost方法中,我们首先打开连接并设置用户代理。(一些网站在默认的User-Agent请求参数中包含字符串Java时无法工作,这可能是因为这些网站不想为程序自动生成的请求服务)

然后调用setOutPut(true)并打开输入流。然后枚举Map对象中的所有键和值。对每一个键-值对,我们发送key,=字符,valie和&分隔符:

 try (var out=new PrintWriter(connection.getOutputStream())){
            var first=true;
            for(Map.Entry<String,String> pair: nameValuePairs.entrySet())
            {
                if (first)first=false;
                else out.print("&");
                String name=pair.getKey();
                String value=pair.getValue();
                out.print(name);
                out.print('=');
                out.print(URLEncoder.encode(value, StandardCharsets.UTF_8));
            }
        }
代码解读

在从写出请求切换到读取响应的任何部分时,就会发生与服务器的实际交互。下面为打印在屏幕上的服务器响应头中的几个属性:

getContentType: text/html; charset=UTF-8
getContentLength: -1
getContentEncoding: null
getDate: 1648889170000
getExpiration: 442645200000
getLastModified: 0

可以看到,当服务器响应主体部分(输出的尺寸)过大时,Content-Length头被设置为-1,而Content-Type头被设置为text/html。在该示例程序中,这种切换发生在对connection.getContentEncoding()的调用时。

在读取响应过程中会碰到一个问题。如果服务器端出现错误,那么调用connection.getInputStream()时就会抛出一个FileNotFoundException异常。但是此时服务器仍然会向浏览器返回一个错误页面(例如,404页面)。为了捕捉这个错误页面,可以调用getErrorStream()方法:

​ InputStream err=connection.getErrorStream();

注意:getErrorStream属于URL类的子类HttpURLConnection。如果要创建以http://或https://开头的URL,那么可以将所产生的连接对象强制转型为HttpURLConnection。


人工重定向

在将POST数据发送给服务器是,服务器端程序产生的响应可能是redirect(302 Found,原始描述短语为Moved Temporarily,是HTTP协议中的一个状态码(Status Code)。可以简单的理解为该资源原本确实存在,但已经被临时改变了位置;换而言之,就是请求的资源暂时驻留在不同的URI下,故而除非特别指定了缓存头部指示,该状态码不可缓存。对于服务器,通常会给浏览器发送HTTP Location头部来重定向到新的新位置),后面跟着一个完全不同的URL,该URL应该被调用以获取实际的信息。服务器可以这么做,因为这些信息位于他处,或者提供了一个可以作为书签标记的URL。HttpURLConnection类在大多数情况下可以处理这种重定向。

注释:如果cookie需要在重定向中从一个站点发送给另一个站点,呢么可以向下面这样配置一个全局的cookie处理器

​ CookieHandler.serDefault(new CookieManager(null,CookiePolicy.ACCEPT_ALL));

然后,cookie就可以被正确地包含在重定向请求中了

尽管重定向通常是自动处理的,但是有些情况下你需要自己完成重定向,例如,在HTTPHTTPS之间的自动重定向因为安全问题而不被支持。

重定向还会因更细微的原因而失败。例如,早期版本的邮政编码服务就使用的重定向。如果我们在最初设置了User-Agent请求参数以便让邮局网站认为我们不是在通过Java API发送请求,但是这项设置在自动重定向中并没有用到。自动重定向总是会发送包含单词Java的通用用户代理字符串。

在这种情况下,可以人工实现重定向:

1、在连接到服务器之前将关闭自动重定向

​ connection.setInstanceFollowRedirects(false);

2、在发送请求后,获取响应码

​ int responseCode = connection.getResponseCode();

检查它是否是下列值之一:

​ HttpURLConnection.HTTP_MOVED_PERM

​ HttpURLConnection.HTTP_MOVED_TEMP

​ HttpURLConnection.HTTP_SEE_OTHER

3、如果是这些值之一,那么获取Location响应头,以获得重定向的URL。然后断开连接,并创建到新的URL的连接:

tring location = connection.getHeaderField(“Location”);
if (location != null) {
URL base = connection.getURL();
connection.disconnect();
return doPost(new URL(base, location), nameValuePairs, userAgent, redirects - 1);
}

每当需要从某个现有的Web站点查询信息时,该程序所展示的处理技术就显得很有用。只需找出需要发送的参数,然后从回复信息中剔除HTML和其他不必要的信息。

相关接口
API java.net.HttpURLConnection

InputStream getErrorStream()
返回一个流,通过这个流可以读取Web服务器的错误信息
API java.net.URLEncoder

static String encode(String s,String encoding)
采用指定的字符编码模式对字符串s进行编码,并返回它的URL编码形式。在URL编码中,'A'-'Z','a'-'z','0'-'9','-','_','.','*'等字符保持不变,空格被编码成
'+',所有其他字符被编码成"%XY"形式的字节序列,其中0xXY为该字节十六进制数
API java.net.URLDecoder

static String decode(String s,String encoding)
采用指定编码模式对已编码字符串s进行解码,并返回结果

4、HTTP客户端

URLConnection类是在HTTP成为Web普适协议之前设计的,它提供对大量协议的支持,但是它对HTTP协议的支持有些笨重。HttpClient提供了更简洁的API和对HTTP/2的支持,到了Java11,HttpClient位于java.net.http包中。

与URLConnection类相比,HTTP客户端API从设计初始就提供了一种更简单的连接到Web服务器的机制。

HttpClient对象可以发出请求并接收响应。可以通过下面的调用获取客户端:

​ HttpClient client=HttpClient.newHttpClient();

或者,如果需要配置客户端,可以使用下面的构建器API:

​ HttpClient client=HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build();

即,获取一个构建器,调用其方法定制需要待构建的项,然后调用build方法来终结构建过程。这是一种构建不可修改对象的常见模式。

还可以遵循构建器模式来定制请求,下面是一个Get请求:

​ HttpRequest request=HttpRequest.newBuilder()

​ .uri(new URI(“https://yumoyumo.top”))

​ .GET()

​ .build();

URI是指"统一资源标识符",在使用HTTP时,它与URL相同。但是在Java中,URL类确实有一些用来打开到某个URL的连接的方法,而URI类只关心语法(模式(scheme)、主机(host)、端口(port)、路径(path)、查询(query)、片段(fragment)等)

对于POST请求,需要一个**”体发布器“(body publisher)**,它会将请求数据转换为要推送的数据。有针对字符串、字节数组和文件的体发布器。例如,如果请求是JSON格式的,呢么只需将JSON字符串提供给某个字符串发布器:
HttpRequest request=HttpRequest.newBuilder()

​ .uri(new URI(url))

​ .header(“Content-Type”,“application/json”)
​ .POST(HttpRequest.BodyPublishers.ofString(jsonString))

​ .build();

但是该API不支持对常见内容类型做上面锁要求的格式化处理,在下面的程序中提供了用于表单数据和文件上传的体发布器。

在发送请求时,必须告诉客户端如何处理响应。如果只是想将体当字符串处理,那么就可以像下面这样用**HttpResponse.BodyHandlers.ofString()**来发送请求:

​ HttpResponse< String > response=client.send(request,HttpResponse.BodyHandlers.ofString());

HttpResponse类是一个泛化类,它的参数类型表示体的类型。可以直接获取响应体字符串:

​ String bodyString=response.body();

还有其他的响应体处理器,可以将响应作为字节数组或输入流来获取。**BodyHandlers.ofFile(filePath)会产生一个处理器,将响应存储到给定的文件中,BodyHandlers.ofFileDownload(directoryPath)会用Content-Disposition头中的信息将响应存入给定的目录中。最后,从BodyHandlers.dicarding()**中获得的处理器会直接丢弃响应。

处理响应的内容并不在该API的考虑范围之内。例如,如果收到了JSON数据,那么就需要某个JSON库来解析其中的内容。

HttpResponse对象还会产生状态码响应头

​ int status=response.statusCode();

​ HttpHeaders responseHeaders=response.headers();

可以将HttpHeader对象转换成一个映射表

​ Map< String,List< String >> headerMap=responseHeaders.map();

这个映射表的值是列表,因为在HTTP中,每个键都可以有多个值。

如果只想要某个特定键的值,并且知道它没有多个值,那么可以调用firstValue方法:

​ Optional< String > lastModified = headerMap.firstValue(“Last-Modified”);

这样可以得到该响应的值,或者在没有提供该值时,返回空的Optional对象。

可以异步地处理响应。在构建客户端时,可以提供一个执行器

​ ExecutorService executor=Executors.newCachedThreadPool();

​ HttpClient client = HttpClient.newBuilder().**executor(executor).**build();

构建一个请求,然后再该客户端上调用sendAsync方法,就会收到一个CompletableFuture< HttpResponse< T >>对象,其中T是体处理器的类型。需要使用CompletableFuture API:

​ HttpRequest request = HttpRequest.newBuilder().uri(uri).GET().build();

​ cilent.**sendAsync(**request,HttpResponse.BodyHandlers.ofString())

​ thenAccept(response-> . . . );

示例程序
package javabase.socket;


import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.net.*;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.*;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;

/**
 * 提供表单数据和文件上传等 体发布器
 */
class MoreBodyPublisher{
    /**
     * 表单数据上传的体发布器
     * @param data
     */
    public static BodyPublisher ofFormData(Map<String, String> data)
    {
        var first=true;
        var builder=new StringBuilder();
        for(Map.Entry<String, String> entry:data.entrySet())
        {
            if (first) {first=false;}
            else {builder.append("&");}
            builder.append(URLEncoder.encode(entry.getKey().toString(), StandardCharsets.UTF_8));
            builder.append("=");
            builder.append(URLEncoder.encode(entry.getValue().toString(),StandardCharsets.UTF_8));
        }
        return BodyPublishers.ofString(builder.toString());
    }

    /**
     * 将字符串转换为字节数组
     * @param s 待转换的字符串
     * @return 转换过后的字节数组
     */
    private static byte[] bytes(String s){ return s.getBytes(StandardCharsets.UTF_8); }

    public static BodyPublisher ofMimeMultipartData(Map<String, String> data, String boundary)throws IOException
    {
        var byteArrays=new ArrayList<byte[]>();
        byte[] separator=bytes("--"+boundary+"\nContent-Disposition: form-data;name=");
        for (Map.Entry<String, String> entry:data.entrySet())
        {
            Path path=Path.of(entry.getValue());
            byteArrays.add(separator);

            String mimeType= Files.probeContentType(path);
            byteArrays.add(bytes("\""+entry.getKey()+"\"; filename=\""+path.getFileName()
                +"\"\nContent-Type: "+mimeType+"\n\n"));
        }
        byteArrays.add(bytes("--"+boundary+"--"));
        return BodyPublishers.ofByteArrays(byteArrays);
    }

    public static BodyPublisher ofSimpleJSON(Map<String, String> data)
    {
        var builder=new StringBuilder();
        builder.append("{");
        var first=true;
        for(Map.Entry<String, String> entry:data.entrySet())
        {
            if (first) {first=false;}
            else {builder.append(",");}

            builder.append(jsonEscape(entry.getKey().toString())).append(": ").append(jsonEscape(entry.getValue().toString()));
        }
        builder.append("}");
        return BodyPublishers.ofString(builder.toString());
    }

    private static final Map<Character,String> replacements=Map.of('\b',"\\b",'\f',"\\f",'\n',"\\n",
            '\r',"\\r",'\t',"\\t",'"',"\\\"",'\\',"\\\\");

    private static StringBuilder jsonEscape(String str)
    {
        var result=new StringBuilder("\"");
        for (int i=0;i<str.length();i++)
        {
            char ch=str.charAt(i);
            String replacement=replacements.get(ch);
            if (replacement==null) {result.append(ch);}
            else {result.append(replacement);}
        }
        result.append("\"");
        return result;
    }

}
public class HttpClientTest {
    public static void main(String[] args) throws IOException, URISyntaxException, InterruptedException {
        System.setProperty("jdk.httpclient.HttpClient.log","headers,errors");


        Map<String,String> props= new HashMap<String,String>(Map.of(
                "log", "yumo1304960237@gmail.com",
                "pwd", "123456",
                "Content-Type","application/x-www-form-urlencoded"
        ));
        
        String urlString="https://www.yumoyumo.top/wp-login.php";
        String contentType=""+props.remove("Content-Type");
        if("multipart/form-data".equals(contentType))
        {
            var generator=new Random();
            String boundary=new BigInteger(256,generator).toString();;
            contentType+=";boundary="+boundary;

        }
        String result=doPost(urlString,contentType,props);
        System.out.println(result);
    }

    public static String doPost(String url, String contentType, Map<String, String> data) throws IOException, URISyntaxException, InterruptedException {
        HttpClient client=HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build();

        BodyPublisher publisher=null;
        if(contentType.startsWith("multipart/form-data"))
        {
            String boundary=contentType.substring(contentType.lastIndexOf("=")+1);
            publisher=MoreBodyPublisher.ofMimeMultipartData(data,boundary);
        }
        else if("application/x-www-form-urlencoded".equals(contentType)) {
        {
            publisher=MoreBodyPublisher.ofFormData(data);
        }
        } else
        {
            contentType="application/json";
            publisher=MoreBodyPublisher.ofSimpleJSON(data);
        }

        HttpRequest request=HttpRequest.newBuilder()
                .uri(new URI(url))
                .header("Content-Type",contentType)
                .POST(publisher)
                .build();
        HttpResponse<String> response=client.send(request,HttpResponse.BodyHandlers.ofString());
        return response.body();
    }
}
相关接口
API java.net.http.HttpClient

static HttpClient newHttpClient()
用默认配置产生一个HttpClient对象
static HttpClient.Builder newBuilder()
产生一个用于构建HttpClient对象的构建器
    
<T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler)
<T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request,BodyHandler<T> responseBodyHandler);
产生一个同步或异步的请求,并使用给定的处理器来处理响应体
API java.net.http.HttpClient.Builder

HttpClient build()
用由当前构建器配置的属性产生一个HttpClient对象。

HttpClient.Builder followRedirects(HttpClient.Redirect policy)
将重定向策略设置为HttpClient.Redirect枚举中的ALWAYS,NEVER或NORMAL之一(仅拒绝从HTTPS重定向到HTTP)

HttpClient.Builder executor(Executor executor)
设置用于异步请求的执行器
API java.net.http.HttpRequest

HttpRequest.Builder newBuilder()
产生一个用于构建HttpRequest对象的构建器。
API java.net.http.HttpRequest.Builder

HttpRequest build()
用由当前构建器配置的属性产生一个HttpRequest对象

HttpRequest.Builder uri(URI uri)
为当前请求设置URI

HttpRequest.Builder header(String name,String value)
为当前请求设置请求头

HttpRequest.Builder GET()
HttpRequest.Builder DELETE()
HttpRequest.Builder POST(HttpRequest.BodyPublisher bodyPublisher)
HttpRequest.Builder PUT(HttpRequest.BodyPublisher bodyPublisher)
为当前请求设置请求方法和请求体
API java.net.http.HttpResponse<T>

T body()
产生当前响应的体

int statusCode()
产生当前响应的状态码

HttpHeaders headers()
产生响应头
API java.net.http.HttpHeaders

Map<String,List<string>> map()
产生这些头的映射

Optional<String> firstValue(String name)
在头中具有给定名的第一个值,如果存在的话

5、发送E-mail

过去,编写程序通过创建到邮件服务器上SMTP(Simple Mail Transform Policy,简单邮件传输协议)专用的端口25来发送邮件是一件很简单的是。简单邮件传输协议用于描述E-mail消息的格式。一旦连接到服务器,就可以发送一个邮件报头(采用SMTP格式,该格式很容易生成)。紧随其后的是邮件信息。

以下是操作的详细过程:

1、打开一个到达主机的套接字:

​ var s=new Socket(“mail.yourserver.com”,25); //25 is SMTP

​ var out=new PrintWriter(s.getOutputStream(),StandardCharsets.UTF_8);

2、发送以下信息到打印流:

​ HELO sending host

​ MAIL FROM: sender e-mail address

​ RCPT TO: recipient e-mail address

​ DATA

​ Subject: subject

(blank line)

mail message (any number of lines)

​ .

​ QUIT

SMTP规范(RFC 821)规定,每一行都要以**\r再紧跟一个\n**来结尾。

SMTP曾经总是例行公事般地接收任何人的E-mail,但是在蠕虫泛滥的今天,许多服务器都内置了检查功能,并且只接受来自授信用户授信IP地址范围的请求。其中,认证通常是通过安全套接字连接来实现的。

实现人工认证模式的代码非常冗杂乏味,因此,我们将展示如何利用JavaMail API在Java程序中发送E-mail

在maven仓库中找到对应的依赖,复制粘贴进pom.xml

<!-- https://mvnrepository.com/artifact/javax.mail/javax.mail-api -->
        <dependency>
            <groupId>javax.mail</groupId>
            <artifactId>javax.mail-api</artifactId>
            <version>1.6.2</version>
        </dependency>

如果需要使用JavaMail,则需要设置一些和邮件服务器相关的属性。例如,在使用GMail时,需要设置:

​ mail.transport.protocol=smtps;

​ mail.smtps.auth=true;

​ mail.smtps.host=smtp.gmail.com;

​ mail.smtps.user=accountname@gmail.com

示例程序时从一个属性文件中读取这些属性值的。

处于安全的问题没有把密码放在属性文件中,而是要求提示用户需要输入。

1、首先需要读入一个属性文件,然后像下面这样获取一个邮件会话:

​ Session mailSession=Session.getDefaultInstance(props);

2、接着,用恰当的发送者、接受者、主题和消息文本来创建消息:

​ var message=new MimeMessage(mailSession);

​ message.setFrom(new InternetAddress(from));

​ message.addRecipient(RecipientType.TO,new InternetAddress(to));

​ message.setSubject(subject);

​ message.setText(builder.toString());

3、然后将消息发送走:

​ Transport tr=mailSession.getTransport();

​ tr.connect(null,password);

​ tr.sendMessage(message,message.getAllRecipients());

​ tr.close();

下面这个示例程序是从具有下面这种格式的文本文件中读取消息的:

Sender

Recipient

Subject

Message text (any number of lines)

你可以输入任何你喜欢的发送者。(当你下一次收到来自 president@whitehouse.gov 的E-mail消息邀请你盛装出席白宫南草坪的活动时,请牢记这一点,谨防上当)

如果你搞不清楚为什么你的邮件连接无法正常工作,那么可以调用:‘

mailSession.setDebug(true);

并检查消息。而且,JavaMail API FAQ也有些挺有用的调试提示。


import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.internet.MimeMessage;
import java.io.Console;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.Properties;

public class MailTest {
    public static void main(String[] args) throws IOException, MessagingException {
        var props=new Properties();
        try (InputStream in= Files.newInputStream(Paths.get("mail","mail.properties"))){
            props.load(in);
        }
        String contentString=args.length>0?args[0] :"content.txt";
        List<String> lines=Files.readAllLines(Paths.get("mail",contentString));

        String from=lines.get(0);
        String to=lines.get(1);
        String subject=lines.get(2);

        var builder =new StringBuilder();
        for(int i=3;i<lines.size();i++)
        {
            builder.append(lines.get(i));
            builder.append("\n");
        }

        Console console=System.console();
        var password=new String(console.readPassword("Password: "));

        Session mailSession=Session.getDefaultInstance(props);
        //mailSession.setDebug(true);
        var message=new MimeMessage(mailSession);
        message.setFrom(from);
        message.addRecipients(Message.RecipientType.TO, to);
        message.setSubject(subject);
        message.setText(builder.toString());
        Transport tr=mailSession.getTransport();
        try {
            tr.connect(null,password);
            tr.sendMessage(message,message.getAllRecipients());
        }
        finally {
            tr.close();
        }

    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值