Java 网络编程


Java 网络编程



1 网络编程基础知识




2 Java 的基本网络支持


Java 为网络支持提供了 java.net 包,该包下的 URL 和 URLConnection 等类提供了以编程方式访问 web 服务的功能,

URLEncoder , URLDecoder 提供了普通字符串和 application/x-www-form-urlencoded MIME 字符串相互转换的静态方法:


使用 InetAddress


InetAddress 类代表 IP 地址,有两个子类:
    Inet4Address : 代表 IPv4 地址
    Inet6Address : 代表 IPv6 地址
    
InetAddress 没有提供构造器,而是提供如下静态方法获取 InetAddress 对象
    static InetAddress     getByName(String host) :根据主机名获取对应的 InetAddress
    static InetAddress     getByAddress(byte[] addr) :根据原始 IP 地址获取对应的 InetAddress
    
InetAddress 提供了如下方法获取 InetAddress 实例对应的 IP 地址和主机名
    String     getCanonicalHostName() :获取此 IP 地址的全限定域名
    String     getHostAddress() :返回 IP 地址字符串
    String     getHostName() : 返回 IP 地址的主机名
    
InetAddress 类提供了一个方法:获取本机 IP 地址对应的 InetAddress 实例
    static InetAddress     getLocalHost()

InetAddress 提供如下方法用于测试是否可达到该地址
    boolean     isReachable(int timeout) :
    该方法将尽最大努力试图到达主机,但防火墙或服务器配置可能阻塞请求,使得它在访问某些特定端口时处于不可达状态。
    如果可以获得权限,典型的实现将使用 ICMP ECHO REQUEST, 否则它将试图在目标主机的端口 7(Echo) 上建立 TCP 连接。

例子:

public class InetAddressTest
{
    public static void main(String[] args)
        throws Exception
    {
        // 根据主机名来获取对应的InetAddress实例
        InetAddress ip = InetAddress.getByName("www.crazyit.org");
        // 判断是否可达
        System.out.println("crazyit是否可达:" + ip.isReachable(2000));
        // 获取该InetAddress实例的IP字符串
        System.out.println(ip.getHostAddress());
        // 根据原始IP地址来获取对应的InetAddress实例
        InetAddress local = InetAddress.getByAddress(
            new byte[]{127,0,0,1});
        System.out.println("本机是否可达:" + local.isReachable(5000));
        // 获取该InetAddress实例对应的全限定域名
        System.out.println(local.getCanonicalHostName());
    }
}

    
使用 URLDecoder 和 URLEncoder
-----------------------------------------------------------------------------------------
用于完成普通字符串和 application/x-www-form-urlencoded MIME 字符串相互转换
URLDecoder 类包含:
    static String     decode(String s, String enc) : 将 application/x-www-form-urlencoded 字符串转换成普通字符串

URLEncoder 类包含:
    static String     encode(String s, String enc) : 将普通字符串转换成 application/x-www-form-urlencoded 字符串

enc : 字符集名称

例子:

public class URLDecoderTest
{
    public static void main(String[] args)
        throws Exception
    {
        // 将application/x-www-form-urlencoded字符串
        // 转换成普通字符串
        // 其中的字符串直接从图3所示窗口复制过来
        String keyWord = URLDecoder.decode(
            "%E7%96%AF%E7%8B%82java", "utf-8");
        System.out.println(keyWord);
        // 将普通字符串转换成
        // application/x-www-form-urlencoded字符串
        String urlStr = URLEncoder.encode(
            "疯狂Android讲义" , "GBK");
        System.out.println(urlStr);
    }
}

使用 URL , URLConnection 和 URLPermission
-----------------------------------------------------------------------------------------
URL (Uniform Resource Locator) 对象代表统一资源定位器,它是指向互联网资源的指针。资源可以是简单的文件或目录,也可以是对更复杂对象的引用,
例如对数据库或搜索引擎的查询,
在通常情况下,URL 可以由协议名、主机名、端口和资源组成,满足如下格式:
    protocol://host:port/resourceName
例如:
    http://www.apaache.org/index.html
    
JDK 中还提供了一个 URI (Uniform Resource Identifiers) 类,其实例代表一个统一资源标识符, Java 的 URI 不能用于定位任何资源,它的唯一作用就是解析。
与此对应的是, URL 则包含一个可打开到达的资源输入流。可将  URL理解成 URI 的特例。

URL 提供了多个构造器用于创建 URL对象,一旦获取了  URL对象后,就可以调用如下方法访问 URL 对应的资源:
    String     getFile() :获取该 URL 的资源名
    String     getHost() :获取该 URL 的主机名
    String     getPath() :获取该 URL 的路径部分
    int     getPort() :获取该 URL 的端口号
    String     getProtocol() :获取该 URL 协议名
    String     getQuery() : 获取该 URL 的查询字符串部分
    URLConnection     openConnection() :返回一个 URLConnection 对象,它代表了与 URL 所引用的远程对象的连接
    InputStream     openStream() : 打开与此 URL 的连接,并返回一个用于读取该 URL资源的 InputStream

URLConnection 和 HttpURLConnection : 前者表示应用程序和 URL 之间的通信连接, 后者表示与URL 之间的 HTTP 连接,程序可以通过 URLConnection 实例向该 URL 发送请求、读取 URL 引用的资源

Java 8 新增了 URLPermission 工具类,用于管理 HttpURLConnection 的权限问题,如果在 HttpURLConnection 安装了安全管理器,通过该对象打开连接时就需要先获得权限。

通常创建一个和 URL 的连接,并发送请求,读取此 URL 引用的资源需要如下几个步骤:
    1. 通过调用 URL 对象的 openConnection() 方法来创建 URLConnection 对象
    2. 设置 URLConnection 的参数和普通请求属性
    3. 如果只是发送 GET 方式请求,则使用 connect() 方法建立和远程资源之间的实际连接即可;如果需要发送 POST 方式的请求,则需要获得 URLConnection 实例对应的输出流来发送请求参数
    4. 远程资源变为可用,程序可以访问远程资源的头字段或通过输入流读取远程资源数据。
    
在建立和远程资源的实际连接之前,程序可以通过如下方法设置请求头字段
    void     setAllowUserInteraction(boolean allowuserinteraction) :设置 AllowUserInteraction 请求头字段的值
    void     setDoInput(boolean doinput):设置 DoInput 请求头字段的值
    void     setDoOutput(boolean dooutput):设置 DoOutput 请求头字段的值
    void     setIfModifiedSince(long ifmodifiedsince):设置 IfModifiedSince 请求头字段的值
    void     setUseCaches(boolean usecaches):设置 UseCaches 请求头字段的值
    void     setRequestProperty(String key, String value):设置 key 请求头字段的值为 value
    void     addRequestProperty(String key, String value):我 key 请求头字段增加 value 值
    
当远程资源可用之后,程序可以使用以下方法访问头字段和内容:
    Object     getContent():获取该 URLConnection 内容
    String     getHeaderField(String name) :获取指定响应头字段的值
    InputStream     getInputStream() :返回该 URLConnection 对应的输入流,用于获取响应内容
    OutputStream     getOutputStream() :返回该 URLConnection 对应的输出流,用于向 URLConnection 发送请求参数

getHeaderField() 方法用于根据响应头字段返回对应的值,某些头字段由于经常需要访问,所以 Java 提供了以下方法来访问特定相应头字段的值:
    String     getContentEncoding() :获取 content-encoding 响应头字段的值
    int     getContentLength():获取 content-length 响应头字段的值
    String     getContentType():获取 content-type 响应头字段的值
    long     getDate():获取 date 响应头字段的值
    long     getExpiration(): 获取 expires 响应头字段的值
    long     getLastModified():获取 last-modified 响应头字段的值
    
例子:

    

*
*
*

3 基于 TCP 协议的网络编程
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TCP/IP 通信协议是一种可靠的网络协议,它在通信的两端各建立一个 Socket , 从而在通信的两端之间形成网络虚拟链路。一旦建立了虚拟的网络链路,两端的程序就可以通过虚拟链路进行通信。
Java 对基于 TCP 协议的网络通信提供了良好的封装, Java 使用 Socket 对象代表两端的通信端口,并通过 Socket 产生 IO 流来进行网络通信。



使用 ServerSocket 创建 TCP 服务器端
-------------------------------------------------------------------------------------------
Java 中能接收其他通信实体连接请求的类是 ServerSocket,ServerSocket 对象用于监听来自客户端的 Socket 连接,如果没有连接,它就一直处于等待状态。
ServerSocket 包含一个监听来自客户端连接请求的方法:
    Socket     accept() :接收到客户端的 Socket 连接请求,该方法返回一个与客户端 Socket 对应的 Socket , 否则该方法将一直处于等待状态,线程也被阻塞。
ServerSocket 类提供了如下构造器:
    ServerSocket() :创建未绑定的 ServerSocket
    ServerSocket(int port):用指定端口创建 ServerSocket
    ServerSocket(int port, int backlog) 增加一个用来改变连接队列长度的参数 backlog
    ServerSocket(int port, int backlog, InetAddress bindAddr) : 在机器存在多个 IP 地址的情况下,允许 bindAddr 参数将 ServerSocket 绑定到指定的 IP
当 ServerSocket 使用完毕之后,应该调用 ServerSocket 的 close() 方法关闭 该 ServerSocket.

例子:
    //创建一个 ServerSocket, 用于监听 Socket 连接请求
    ServerSocket ss = new ServerSocket(30000);
    //采用循环不断地接收来自客户端的连接请求
    while(true)
    {
        //每当接收客户端的 Socket 连接请求时,服务端也对应产生一个 Socket
        Socket s = ss.accept();
        
        //通信代码
        ...
    }
    
    
使用 Socket 进行通信
-------------------------------------------------------------------------------------------
客户端通常使用 Socket 构造器连接到指定的服务器
    Socket() : 无连接 Socket, 需要调用 connect(SocketAddress endpoint) 建立连接
    Socket(InetAddress address, int port):
    Socket(String host, int port):创建连接到指定远程主机、远程端口的 Socket, 该构造器没有指定本地地址、本地端口,
                                    默认使用本地主机的默认 IP 地址,默认系统动态分配的端口
    
    Socket(InetAddress address, int port, InetAddress localAddr, int localPort):
    Socket(String host, int port, InetAddress localAddr, int localPort):创建连接到远程主机、端口的 Socket, 并指定本地 IP 地址和本地端口,
                                    适用于本地主机有多个IP地址的情形。
    
    构造器中指定远程主机时既可使用 InetAddress 指定,也可用 String 指定。

例子:
    //创建连接到本机、30000端口的 Socket
    Socket s = new Socket("127.0.0.1", 30000);
    //下面可以使用 Socket 通信了
    ...
    
当客户端、服务器端产生了对应的 Socket 之后,就建立了通信连接,程序无须再区分服务器端、客户端,而是通过各自的 Socket 进行通信。
Socket 提供如下方法获取输入流和输出流:
    InputStream     getInputStream()    :返回该Socket对象对应的输入流,程序通过该输入流从 Socket 中读取数据
    OutputStream     getOutputStream()    :返回该Socket对象对应的输出流,让程序通过该输出流向Socket中输出数据

例子:

public class Server
{
    public static void main(String[] args)
        throws IOException
    {
        // 创建一个ServerSocket,用于监听客户端Socket的连接请求
        ServerSocket ss = new ServerSocket(30000);
        // 采用循环不断接受来自客户端的请求
        while (true)
        {
            // 每当接受到客户端Socket的请求,服务器端也对应产生一个Socket
            Socket s = ss.accept();
            // 将Socket对应的输出流包装成PrintStream
            PrintStream ps = new PrintStream(s.getOutputStream());
            // 进行普通IO操作
            ps.println("您好,您收到了服务器的新年祝福!");
            // 关闭输出流,关闭Socket
            ps.close();
            s.close();
        }
    }
}

public class Client
{
    public static void main(String[] args)
        throws IOException
    {
        Socket socket = new Socket("127.0.0.1" , 30000);   // ①
        // 将Socket对应的输入流包装成BufferedReader
        BufferedReader br = new BufferedReader(
        new InputStreamReader(socket.getInputStream()));
        // 进行普通IO操作
        String line = br.readLine();
        System.out.println("来自服务器的数据:" + line);
        // 关闭输入流、socket
        br.close();
        socket.close();
    }
}



半关闭的 Socket
-------------------------------------------------------------------------------------------
Socket 提供了两个半关闭的方法,只关闭 Socket 的输入流或者输出流
    void     shutdownInput()    :关闭 Socket 输入流,程序还可以通过该 Socket 的输出流输出数据
    void     shutdownOutput():关闭 Socket 输出流,程序还可以通过该 Socket 的输入流读取数据
    
当调用 shutdownInput() 或 shutdownOutput() 关闭 Socket 的输入流或输出流之后,该 Socket 处于半关闭状态, Socket 可以通过
    boolean     isInputShutdown() : 判断该 Socket 是否处于半读状态
    boolean     isOutputShutdown():判断该 Socket 是否处于半写状态
即使同一个 Socket 实例先后调用 shutdownInput(), shutdownOutput() 方法,该 Socket 实例依然没有被关闭,只是该 Socket 既不能输出数据,也不能读取数据。
当调用 Socket 的 shutdownInput()或 shutdownOutput() 方法,关闭了输入流或输出流之后,该 Socket 无法再次打开输入流或输出流,因此这种做法通常不适合保持持久通信状态的交互应用。



使用 NIO 实现非阻塞 Socket 通信
-------------------------------------------------------------------------------------------
JDK 1.4 开始, Java 提供了 NIO API 来开发高性能的网络服务器,使用 NIO API 可以让服务器端使用一个或有限几个线程同时处理连接到服务器端的所有客户端。

非阻塞是 NIO 实现的重要功能之一,为了实现非阻塞, NIO 引入了选择器( Selector)和通道( Channel)的概念。
通道表示到实体,如硬件设备、文件、网络套接字或可执行一个或多个不同 I/O 操作(如读取或写入)的程序组件的开放连接。
一些通道。如文件、网络套接字,允许选择器对通道进行轮询,也就是说,通道能够注册到一个选择器实例,通过该实例的 select() 方法,用户可以询问 “在一个或一组通道中,哪些通道需要服务(即 read , write , accept)” 。
在一个准备好的通道上进行相应的 I/O 操作,就不需要等待,也就不会阻塞了。


NIO 为非阻塞式 Socket 通信提供了如下几个特殊的类。

Selector :
-------------------------------
    它是 SelectableChannel 对象的多路复用器,所有希望采用非阻塞方式进行通信的 Channel 都应该注册到 Selector 对象,
    可以通过此类的 open() 静态方法创建 Selector 实例,
        
        static Selector     open();
    
    该方法使用系统默认的 SelectorProvider 来创建新的 Selector 。
    
    Selector 可以同时监控多个 SelectableChannel 的 IO 状况,是非阻塞 IO 的核心。
    
    一个 Selector 实例有三个 SelectionKey 集合: Set<SelectionKey>
    ---------------------------------------------------------------
        1. 所有的 SelectionKey 集合:代表了注册在该 Selector 上的 Channel, 这个集合可以通过keys() 方法返回。
            
                abstract Set<SelectionKey>     keys()
                
        2. 被选择的 SelectionKey 集合:代表了所有之前通过 select() 方法获取的、需要进行 IO 处理的 Channel, 是所有SelectionKey集合的子集,这个集合可以通过 selectedKeys() 方法获取,
        
                abstract Set<SelectionKey>     selectedKeys();
                
        3. 被取消的 SelectionKey 集合:代表了所有被取消但还没有被 deregistered 的 Channel ( 通过 SelectionKey.cancel() 方法发出取消请求 ), 是所有SelectionKey集合的子集,
            在下一次执行 select() 方法时,这些 Channel 对应的 SelectionKey 会被彻底删除,程序无法直接访问这个集合。
            
        All three sets are empty in a newly-created selector.
        
    Selector 提供了一系列和 Select 相关的方法,允许程序同时监控多个 IO Channel:
    ---------------------------------------------------------------
        abstract int     select()             :执行一个阻塞式选择操作,当集合中至少有一个需要处理的 Channel IO 操作时返回,并将对应的 SelectionKey 加入被选择的 Set<SelectionKey> 中。该方法返回 SelectionKey 数量。
        abstract int     select(long timeout):可以设置超时时长的 select 操作。
        abstract int     selectNow()            :执行一次非阻塞式的选择操作,如果没有 Channel 可选择立即返回,Invoking this method clears the effect of any previous invocations of the wakeup method.
        abstract Selector     wakeup()        :使一个还未返回的 select() 方法立即返回。如果当前没有 select() 操作在处理,下一次调用选择方法时立即返回,除非调用一次 selectNow() 方法

        Selector 对象的 select 方法


SelectableChannel :
--------------------------------------------------------------------
代表可以支持非阻塞IO操作的 Channel 对象,可以被注册到 Selector 上,这种注册关系由 SelectionKey 实例表示。

应用程序可以调用 SelectableChannel 的 register() 方法将其注册到指定的 Selector 上。

    SelectionKey     register(Selector sel, int ops):
    abstract SelectionKey     register(Selector sel, int ops, Object att):
    
当 Selector 上的某些 SelectableChannel 上有需要处理的 IO 操作时,程序调用 Selector 实例的 select() 方法返回,
并可以通过 selectedKeys() 方法返回对应的 SelectionKey 集合,通过该集合获取所有需要 IO 处理的 SelectableChannel集

SelectableChannel 对象支持阻塞和非阻塞两种模式(所有 Channel 默认都支持阻塞模式),必须使用非阻塞模式才可以利用非阻塞IO操作。
SelectableChannel 提供了如下方法设置和返回 Channel 的阻塞模式状态:

    abstract SelectableChannel     configureBlocking(boolean block) :设置是否采用阻塞模式
    abstract boolean     isBlocking():返回该 Channel 是否是阻塞模式

不同的 SelectableChannel 所支持的操作不一样,例如 ServerSocketChannel 代表 ServerSocket , 它只支持 OP_ACCEPT 操作
SelectableChannel 提供了如下方法返回它支持的所有操作:
    
    abstract int     validOps():返回一个整数,表示这个 Channel 所支持的 IO 操作
    
SelectionKey 中,用静态常量定义了4种IO操作:
    static int     OP_ACCEPT    :16
    static int     OP_CONNECT    :8
    static int     OP_READ        :1
    static int     OP_WRITE    :4

SelectableChannel 提供了如下方法获取它的注册状态:
    abstract boolean         isRegistered()    :返回该 Channel 是否已注册在一个或多个 Selector 上
    abstract SelectionKey     keyFor(Selector sel) :返回该 cancel 和 Selector 之间的注册关系,如果不存在注册关系,则返回 null
    
    
ServerSocketChannel :
--------------------------------------------------------------------
支持非阻塞操作,对应于 java.net.ServerSocket 类,只支持 OP_ACCEPT 操作,
该类提供了 accept() 方法,功能相当于 ServerSocket 类的 accept() 方法


SocketChannel :
--------------------------------------------------------------------
支持非阻塞操作,对应于 java.net.Socket 类,支持 OP_CONNECT , OP_READ , OP_WRITE 操作,
这个类还实现了  ByteChannel, ScatteringByteChannel 和 GatheringByteChannel 接口,可以直接通过 SocketChannel 读写 ByteBuffer 对象



创建一个可用的 ServerSocketChannel 采用如下代码片段
    //通过 open() 方法打开一个未绑定的 ServerSocketChannel 实例
    ServerSocketChannel server = ServerSocketChannel.open();
    InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 30000);
    
    //将 ServerSocketChannel 绑定到指定的 IP 地址
    server.bind(isa);
    
如果需要使用非阻塞方式处理 ServerSocketChannel , 应该设置它的非阻塞模式,并将其注册到指定 Selector:
    //设置 ServerSocketChannel 以非阻塞方式工作
    server.configureBlocking(false);
    //将server注册到指定的 Selector 对象
    server.register(selector, SelectionKey.OP_ACCEPT);




使用 Java 7 的 AIO 实现非阻塞通信
-------------------------------------------------------------------------------------------------------------------------
NIO.2 提供了异步 Channel 支持, 这种异步 Channel 可以提供更高效的 IO , 这种基于异步的 Channel 的 IO 机制也被称为异步IO ( Asynchronous IO ------ AIO )

如果按 POSIX 的标准划分IO , 可以把IO分为两类:同步IO和异步IO 。对IO操作可分为两步:
    1. 程序发出IO请求;
    2. 完成实际的IO操作
    
前两节所介绍的阻塞IO, 非阻塞IO都是针对第一步划分的,如果发出IO请求会阻塞线程,就是阻塞IO , 如果发出IO请求没有阻塞线程,就是非阻塞IO ;
同步IO与异步IO的区别在第二步————如果实际的IO操作由操作系统完成,再将结果返回给应用程序,这就是异步IO;如果实际的IO需要应用程序本身去执行,会阻塞线程,那就是同步IO.
前面介绍的传统IO, 基于 Channel 的非阻塞IO其实都是同步IO

NIO.2 提供了一系列以 Asynchronous 开头的 Channel 接口和类:
    public interface AsynchronousChannel :
    public interface AsynchronousByteChannel
    public interface NetworkChannel        :
    public abstract class AsynchronousFileChannel :
    public abstract class AsynchronousServerSocketChannel :
    public abstract class AsynchronousSocketChannel :


AsynchronousServerSocketChannel : An asynchronous channel for stream-oriented listening sockets.
-----------------------------------------------------------------------------------------------------------
一个负责监听的 Channel , 与 ServerSocketChannel 相似,创建可以的 AsynchronousServerSocketChannel 需要两步:
    1. 调用它的open() 静态方法创建一个未监听端口的 AsynchronousServerSocketChannel
    2. 调用它的 bind() 方法指定该 Channel 在指定的地址、端口上监听
    
    open方法
    --------------------------------------------------------------------------------------------------------
    static AsynchronousServerSocketChannel     open() :创建一个默认的 AsynchronousServerSocketChannel
    static AsynchronousServerSocketChannel     open(AsynchronousChannelGroup group) :用指定 AsynchronousChannelGroup 创建 AsynchronousServerSocketChannel 实例

    AsynchronousChannelGroup :是异步 Channel 的分组管理器,它可以实现资源共享。创建 AsynchronousChannelGroup 时需要传入一个 ExecutorService ,
                                也就是说,它会绑定一个线程池,该线程池负责两个任务:处理IO 事件和触发 CompletionHandler.
    
    AsynchronousServerSocketChannel, AsynchronousSocketChannel 都允许使用线程池进行管理,创建 AsynchronousSocketChannel 时也可以传入 AsynchronousChannelGroup 进行分组管理

    创建AsynchronousServerSocketChannel 实例的方法 :
    -----------------------------------------------------
    1. 直接创建 AsynchronousServerSocketChannel 代码片段:
        AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(PORT));

    
    2. 使用 AsynchronousChannelGroup 创建 AsynchronousServerSocketChannel 代码片段:
    -----------------------------------------------------------------------------
        //创建一个线程池
        ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(80);
        //以指定的线程池创建一个 AsynchronousChannelGroup
        AsynchronousChannelGroup channelGroup = AsynchronousChannelGroup.withThreadPool(executor);
        
        //以指定的线程池创建一个 AsynchronousServerSocketChannel
        AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open(channelGroup).bind(new InetSocketAddress(PORT));

AsynchronousServerSocketChannel 创建成功之后,接下来就可以调用它的 accept() 方法来接受来自客户端的连接,由于异步IO的实际IO操作是交给操作系统来完成的,
因此程序并不知道异步IO操作什么时候完成————也就是说,程序调用 AsynchronousServerSocketChannel 的 accept() 方法之后,当前线程不会阻塞,而程序也不知道 accept() 方法声明时候会接收到客户端的请求。

为了解决这个异步问题, AIO 为 accept() 提供了两个重载版本:
----------------------------------------------------------------------------------
    abstract Future<AsynchronousSocketChannel>     accept() :接受客户端的请求。如果程序需要获得连接成功后返回的 AsynchronousSocketChannel, 则应该调用该方法返回的 Future 对象的 get() 方法。
                                                            但 get() 方法会阻塞线程,因此这种方法依然会阻塞当前线程。
    abstract <A> void     accept(A attachment, CompletionHandler<AsynchronousSocketChannel,? super A> handler) :接受来自客户端的请求,连接成功或连接失败都会触发CompletionHandler 对象内相应方法。
                                                            其中 AsynchronousSocketChannel 就代表连接成功后返回的 AsynchronousSocketChannel。

    CompletionHandler<V,A> 是一个接口,定义了如下两个方法:
        void     completed(V result, A attachment)    :当IO操作完成时触发该方法。 V result 代表IO操作返回的对象, A attachment 代表发起IO操作时传入的附加参数
        void     failed(Throwable exc, A attachment)    :当IO操作失败的时候触发该方法,Throwable exc 代表IO操作失败引发的异常或错误;A attachment 代表发起IO操作时传入的附加参数

提示:
-----------------------------------------------------------------------------------
类似地,异步 Channel 发起IO操作后,IO操作由操作系统执行,IO操作何时完成,程序无从知晓,因此程序使用 CompletionHandler<V,A> 对象来监听IO操作的完成。
实际上,不仅 AsynchronousServerSocketChannel 的 accept() 方法可以接受 CompletionHandler 监听器,AsynchronousSocketChannel 的 read() , write() 方法都有可以接受 CompletionHandler 参数的版本。
-----------------------------------------------------------------------------------


使用 AsynchronousServerSocketChannel 需要三步:
    1. 调用 open() 静态方法创建 AsynchronousServerSocketChannel
    2. 调用 AsynchronousServerSocketChannel 的 bind() 方法让它在指定的IP地址、端口上监听
    3. 调用 AsynchronousServerSocketChannel 的 accept() 方法接受连接请求


public class SimpleAIOServer
{
    static final int PORT = 30000;
    public static void main(String[] args)
        throws Exception
    {
        try(
            // ①创建AsynchronousServerSocketChannel对象。
            AsynchronousServerSocketChannel serverChannel =
                AsynchronousServerSocketChannel.open())
        {
            // ②指定在指定地址、端口监听。
            serverChannel.bind(new InetSocketAddress(PORT));
            while (true)
            {
                // ③采用循环接受来自客户端的连接
                Future<AsynchronousSocketChannel> future
                    = serverChannel.accept();
                // 获取连接完成后返回的AsynchronousSocketChannel
                AsynchronousSocketChannel socketChannel = future.get();
                // 执行输出。
                socketChannel.write(ByteBuffer.wrap("欢迎你来自AIO的世界!"
                    .getBytes("UTF-8"))).get();
            }
        }
    }
}

使用 AsynchronousSocketChannel 也可分为三步:
    1. 调用 open() 静态方法创建 AsynchronousSocketChannel ,调用 open() 方法时同样可以指定一个 AsynchronousChannelGroup 作为分组管理器
    2. 调用 AsynchronousSocketChannel 的 connect() 方法连接到指定的IP 地址、指定端口的服务器
    3. 调用 AsynchronousSocketChannel read(), write() 方法进行读写
    
    AsynchronousSocketChannel 的 connect(),read(), write() 方法都有返回 Future<V> 对象的方法和需要传入 CompletionHandler<V,A> 参数的重载版本。
    对于返回 Future<V> 对象的版本,必须等到 Future<V> 对象的 get() 方法返回时IO操作才真正完成。
    对于需要传入 CompletionHandler<V,A> 参数的版本,则可通过 CompletionHandler<V,A>在IO操作完成时触发相应的方法
        
        connect()
        ---------------------------------------------------------------------------------------------------------
        abstract Future<Void>     connect(SocketAddress remote);
        abstract <A> void     connect(SocketAddress remote, A attachment, CompletionHandler<Void,? super A> handler);
        
        read()
        ----------------------------------------------------------------------------------------------------------
        abstract Future<Integer>     read(ByteBuffer dst);
        abstract <A> void     read(ByteBuffer[] dsts, int offset, int length, long timeout, TimeUnit unit, A attachment, CompletionHandler<Long,? super A> handler);
        <A> void     read(ByteBuffer dst, A attachment, CompletionHandler<Integer,? super A> handler);
        abstract <A> void     read(ByteBuffer dst, long timeout, TimeUnit unit, A attachment, CompletionHandler<Integer,? super A> handler)
        
        write()
        ----------------------------------------------------------------------------------------------------------
        abstract Future<Integer>     write(ByteBuffer src);
        abstract <A> void     write(ByteBuffer[] srcs, int offset, int length, long timeout, TimeUnit unit, A attachment, CompletionHandler<Long,? super A> handler);
        <A> void     write(ByteBuffer src, A attachment, CompletionHandler<Integer,? super A> handler);
        abstract <A> void     write(ByteBuffer src, long timeout, TimeUnit unit, A attachment, CompletionHandler<Integer,? super A> handler);


例子:

public class SimpleAIOClient
{
    static final int PORT = 30000;
    public static void main(String[] args)
        throws Exception
    {
        // 用于读取数据的ByteBuffer。
        ByteBuffer buff = ByteBuffer.allocate(1024);
        Charset utf = Charset.forName("utf-8");
        try(
            // ①创建AsynchronousSocketChannel对象
            AsynchronousSocketChannel clientChannel
                = AsynchronousSocketChannel.open())
        {
            // ②连接远程服务器
            clientChannel.connect(new InetSocketAddress("127.0.0.1"
                , PORT)).get();     // ④
            buff.clear();
            // ③从clientChannel中读取数据
            clientChannel.read(buff).get();     // ⑤
            buff.flip();
            // 将buff中内容转换为字符串
            String content = utf.decode(buff).toString();
            System.out.println("服务器信息:" + content);
        }
    }
}


*
*
*

4 基于 UDP 协议的网络编程
-----------------------------------------------------------------------------------------------------------------------------------
UDP 协议是一种不可靠的网络协议,它在通信实例的两端各建立一个 Socket , 但这俩个 Socket 之间并没有虚拟链路,这两个 Socket 只是发送、接收数据报的对象。
Java 提供了 DatagramSocket 对象作为 UDP 协议的 Socket, 使用 DatagramPacket 代表 DatagramSocket 发送、接收的数据报。

UDP 协议是英文 User Datagram Protocol 的缩写,即用户数据报协议,主要用来支持那些需要在计算机之间传输数据的网络连接, UDP 的快速具有独特的魅力。

UDP 是一种面向非连接的协议,面向非连接的协议指的是在正式通信前不必与对方先建立连接,不管对方状态就直接发送。至于对方是否可以接收到这些数据, UDP 协议无法控制,
因此说, UDP 协议是一种不可靠的协议。 UDP 协议使用于一次只传送少量数据,对可靠性要求不高的应用环境。

因为 UDP 协议是面向非连接的协议,没有建立连接的过程,因此它的通信效率很高;但也正因为如此,它的可靠性不如 TCP 协议。

UDP 协议的主要作用是完成网络数据流和数据报之间的转换————在信息的发送端, UDP 协议将网络数据流封装成数据报,然后将数据报发送出去;
在接收端, UDP 协议将数据报转换成实际数据内容。

UDP 协议对比 TCP 协议:不可靠,差错控制开销小,传输大小限制在 64KB 以下,不需要建立连接。


使用 DatagramSocket 发送、接收数据
-------------------------------------------------------------------------------------------
Java 使用 DatagramSocket 代表 UDP 协议的 Socket, DatagramSocket 本身只是码头,不维护状态,不能产生IO流,它的唯一作用就是接收和发送数据报。
Java 使用 DatagramPacket 代表数据报, DatagramSocket 接收和发送的数据就是通过 DatagramSocket 对象完成的。


DatagramSocket 构造器:
-----------------------------------------------------------
    DatagramSocket() : 创建一个 DatagramSocket 实例, 并将该对象绑定到本机默认IP地址、本机所有可用端口中随机选择的某个端口
    DatagramSocket(int port) :创建实例,并将该对象绑定到本机默认IP, 指定端口
    DatagramSocket(int port, InetAddress laddr) :指定IP 和端口
    DatagramSocket(SocketAddress bindaddr): 指定IP和端口。
    
通常在创建服务器时,创建指定端口的 DatagramSocket ,这样保证其他客户端可以将数据发送到该服务器。

一旦得到了 DatagramSocket 实例之后,就可以通过如下方法接收和发送数据:
    void     receive(DatagramPacket p) :从该 DatagramSocket 接收数据报
    void     send(DatagramPacket p) :以该 DatagramSocket 对象向外发送数据
    
    使用 DatagramSocket 发送数据报时, DatagramSocket 并不知道将该数据报发送到哪里,而是由 DatagramPacket 自身决定数据报的目的地。
    
    
DatagramPacket 的构造器 :
------------------------------------------------------------
    DatagramPacket(byte[] buf, int length) :以一个空数组来创建 DatagramPacket 对象,该对象的作用是接收 DatagramSocket 的数据
    DatagramPacket(byte[] buf, int length, InetAddress address, int port) :以一个包含数据的数组来创建 DatagramPacket 对象,用于发送
                                                            创建该 DatagramPacket 对象时指定了 IP 和端口, 这就决定了该数据报的目的地
    DatagramPacket(byte[] buf, int offset, int length) :创建接收数据报
    DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port):创建发送数据报
    DatagramPacket(byte[] buf, int offset, int length, SocketAddress address):创建发送数据报
    DatagramPacket(byte[] buf, int length, SocketAddress address):创建发送数据报
    
    
提示:
-------------------------------------------------------------
当 Client/Server 程序使用 UDP 协议时,实际上并没有明显的服务器端和客户端,因为两方都要先建立一个 DatagramSocket 对象,用来接收或发送数据报,
然后使用 DatagramPacket 对象作为传输数据的载体。 通常固定IP地址、固定端口的 DatagramSocket 对象所在的程序被称为服务器,因为该DatagramSocket 可以主动接收客户端数据。


在接收数据时,应该采用接收构造器创建 DatagramPacket 对象,然后调用 DatagramSocket 的 receive(DatagramPacket p) 方法等待数据报的到来。
receive(DatagramPacket p) 方法会一直等待(该方法会阻塞调用该方法的线程),直到收到数据报为止:
    
    DatagramPacket packet = new DatagramPacket(buf, 256);
    socket.receive(packet);

在发送数据之前,调用发送构造器创建 DatagramPacket 对象,此时的字节数组里存放了想发送的数据,给出完整的目的地址IP 和端口号
发送数据是同 DatagramSocket 的 send(DatagramPacket p) 方法实现的,send(DatagramPacket p) 根据数据报的目的地址信息寻径以传送数据报。

    DatagramPacket packet = new DatagramPacket(buf, length, address, port);
    socket.send(packet);
    
    
可以调用 DatagramPacket 的如下方法获取发送者和目标主机的地址信息:
--------------------------------------------------------------------------------------
    InetAddress     getAddress() :当程序准备发送数据报时,该方法返回此数据报的目标机器的地址信息,当程序刚接收到数据报时,该方法返回该数据报发送主机的地址信息
    int             getPort(): 获取端口号
    SocketAddress     getSocketAddress() :获取 SocketAddress, 同时包含IP地址和端口号





使用 MulticastSocket 实现多点广播
--------------------------------------------------------------------------------------------------------------------------------
DatagramSocket 只允许数据报发送给指定的目标地址, 而 MulticastSocket 可以将数据报以广播的方式发送给多个客户端。

若要使用多点广播,则需要让一个数据报标有一组目标主机地址,当数据报发出后,整个组的所有主机都能收到该数据报。

IP 多点广播(或多点发送)实现了将单一信息发送给多个接受者的广播,其思想是设置一组特殊的网络地址作为多点广播地址,每个多点广播地址被看作一组,当客户端需要发送、接收广播信息时,加入该组即可。

IP 协议为多点广播提供了这批特殊的 IP 地址,这批 IP 地址的范围是 224.0.0.0 至 239.255.255.255

MulticastSocket 类是实现多点广播的关键,当 MulticastSocket 把一个 DatagramPacket 发送到多点广播 IP 地址时,该数据报将被自动广播到加入该地址的所有 MulticastSocket
MulticastSocket 即可以将数据报发送到多点广播地址,也可以接收其他主机的广播信息。

MulticastSocket 有点像 DatagramSocket ,事实上 MulticastSocket 是 DatagramSocket 的一个子类,也就是说, MulticastSocket 是特殊的 DatagramSocket
当要发送数据时, 可以使用随机端口创建 MulticastSocket , 也可以在指定端口上创建 MulticastSocket:
    
    MulticastSocket() :使用本机默认地址、随机端口创建 MulticastSocket 对象
    MulticastSocket(int port) :使用本机默认地址、指定端口创建 MulticastSocket 对象
    MulticastSocket(SocketAddress bindaddr) :使用本机指定的 IP 地址、指定的端口创建 MulticastSocket
    
创建 MulticastSocket 对象后,还需要将该 MulticastSocket 加入到指定的多点广播地址
    void     joinGroup(InetAddress mcastaddr) :将该 MulticastSocket 加入到指定的多点广播地址
    void     joinGroup(SocketAddress mcastaddr, NetworkInterface netIf) :
    
    void     leaveGroup(InetAddress mcastaddr) :离开指定的多点广播地址
    void     leaveGroup(SocketAddress mcastaddr, NetworkInterface netIf) :
    
    
    在某些系统上,可能有多个网络接口,这可能会给多点广播带来问题,这时候需要在一个指定的网络接口上监听
    ----------------------------------------------------------------------------------------------------
    void     setInterface(InetAddress inf) :
    InetAddress     getInterface() :
    
    如果创建仅用于发送数据报的 MulticastSocket 对象, 则使用默认的地址、随机端口即可。但如果创建接收用的 MulticastSocket 对象,
    则该 MulticastSocket 对象必须具有指定端口,否则发送方无法确定发送数据报的目标端口
    
MulticastSocket 用于发送、 接收数据报的方法与 DatagramSocket 完全一样。但 MulticastSocket 有一个设置多点广播作用域 scope 的方法

    void     setTimeToLive(int ttl) :
    
    Set the default time-to-live for multicast packets sent out on this MulticastSocket in order to control the scope of the multicasts.
    
    该 ttl 参数用于设置数据报最多可以跨过多少个网络:
    ----------------------------------------------------------------------
     ttl == 0    :指定数据报应停留在本机
            1    :指定数据报发送到本地局域网
            32    :只能发送到本站点网络
            64    :数据报应该保留在本地区
            128    :数据报应该保留在本大洲
            255    :意味着数据报可以发送到所有地方
            
            默认 ttl 的值为 1 。
        
使用 MulticastSocket 进行多点广播时所有的通信实体都是平等的,它们都是将自己的数据报发送到多点广播 IP 地址, 并使用 MulticastSocket 接收其他人发送的广播数据报。

    
    
*
*
*

5 使用代理服务器
---------------------------------------------------------------------------------------------------------------------------------------
Java 5 开始, Java 在 java.net 包下提供了 Proxy 和 ProxySelector 类,
其中 Proxy 代表一个代理服务器,可以在 URLConnection 连接时指定 Proxy , 创建 Socket 连接时也可以指定 Proxy;
ProxySelector 代表一个代理选择器,它提供了对代理服务器更加灵活的控制,它可以对 HTTP , HTTPS , FTP , SOCKS 等进行分别设置,而且还可以设置不需要通过代理服务器的和地址


直接使用 Proxy 创建连接
-------------------------------------------------------------------------------------------
    
构造器:
    
    Proxy(Proxy.Type type, SocketAddress sa) :创建表示代理服务器的 Proxy 对象, 参数 sa 指定代理服务器的地址信息,
                                                type 标识代理服务器类型, 支持如下三种:
                                                    Proxy.Type.DIRECT    : 直接连接,不使用代理
                                                    Proxy.Type.HTTP        : 表示高级协议代理 HTTP/HTTPS
                                                    Proxy.Type.SOCKS    : SOCKS (V4/V5) 代理
一旦创建 Proxy 对象之后,程序就可以使用:
    URL :
        URLConnection     openConnection(Proxy proxy) :使用指定的代理服务器打开连接
        
    Socket :
        Socket(Proxy proxy) :使用指定的代理服务器创建没有连接的 Socket 对象

例子:
    
public class ProxyTest
{
    // 下面是代理服务器的地址和端口,
    // 换成实际有效的代理服务器的地址和端口
    final String PROXY_ADDR = "129.82.12.188";
    final int PROXY_PORT = 3124;
    // 定义需要访问的网站地址
    String urlStr = "http://www.crazyit.org";
    public void init()
        throws IOException , MalformedURLException
    {
        URL url = new URL(urlStr);
        // 创建一个代理服务器对象
        Proxy proxy = new Proxy(Proxy.Type.HTTP
            , new InetSocketAddress(PROXY_ADDR , PROXY_PORT));
        // 使用指定的代理服务器打开连接
        URLConnection conn = url.openConnection(proxy);
        // 设置超时时长。
        conn.setConnectTimeout(5000);
        try(
            // 通过代理服务器读取数据的Scanner
            Scanner scan = new Scanner(conn.getInputStream(), "utf-8");
            PrintStream ps = new PrintStream("index.htm"))
        {
            while (scan.hasNextLine())
            {
                String line = scan.nextLine();
                // 在控制台输出网页资源内容
                System.out.println(line);
                // 将网页资源内容输出到指定输出流
                ps.println(line);
            }
        }
    }
    public static void main(String[] args)
        throws IOException , MalformedURLException
    {
        new ProxyTest().init();
    }
}


使用 ProxySelector 自动选择代理服务器
-------------------------------------------------------------------------------------------------
ProxySelector 代表一个代理选择器,本身是个抽象类,程序无法创建它的实例,开发者可以考虑继承 ProxySelector 实现自己的代理选择器,实现如下两个抽象方法:
    abstract List<Proxy>     select(URI uri) :根据业务需要返回代理服务器列表,如果该方法返回的集合中包含一个 Proxy , 该 Proxy 将作为默认代理服务器
    abstract void     connectFailed(URI uri, SocketAddress sa, IOException ioe) :连接代理服务器失败时回调该方法。
    
实现了 ProxySelector 类之后,调用 ProxySelector 的
    
    static void     setDefault(ProxySelector ps)
    
    静态方法注册该代理选择器即可。
    


Java 为 ProxySelector 提供了一个实现类 sun.net.spi.DefaultProxySelector (这是一个未公开的 API ,应该尽量避免使用),系统已经将 DefaultProxySelector注册成默认的代理选择器,
因此程序可调用 ProxySelector.getDefault() 静态方法获取 DefaultProxySelector 实例。

DefaultProxySelector 继承 ProxySelector ,实现了两个抽象方法,实现策略如下:
----------------------------------------------------------------------------------------
    connectFailed(URI uri, SocketAddress sa, IOException ioe) :如果连接失败,DefaultProxySelector 将尝试不是有代理服务器,直接连接远程资源
    List<Proxy>     select(URI uri) :根据系统属性决定使用哪个代理服务器。 ProxySelector 会检测系统属性与 URL 之间的匹配,然后决定使用相应的属性值作为代理服务器
                                        关于代理服务器常用的属性名有如下三个:
                                            1. http.proxyHost : 设置 HTTP 访问所使用的代理服务器的主机地址,该属性名的前缀可改为 https, ftp 等,
                                                                分别用于设置 HTTP/HTTPS 访问和 FTP 访问所用的代理服务器的主机地址
                                            2. http.proxyPort : 设置 HTTP 访问所使用的代理服务器的端口,前缀名可改为 https, ftp 等
                                            3. http.nonProxyHosts : 设置 HTTP 访问中不需要使用代理服务器的主机,支持使用 * 通配符,支持指定多个地址,多个地址间用竖线(|)分隔。

例子:

public class DefaultProxySelectorTest
{
    // 定义需要访问的网站地址
    static String urlStr = "http://www.crazyit.org";
    public static void main(String[] args) throws Exception
    {
        // 获取系统的默认属性
        Properties props = System.getProperties();
        // 通过系统属性设置HTTP访问所用的代理服务器的主机地址、端口
        props.setProperty("http.proxyHost", "192.168.10.96");
        props.setProperty("http.proxyPort", "8080");
        // 通过系统属性设置HTTP访问无需使用代理服务器的主机
        // 可以使用*通配符,多个地址用|分隔
        props.setProperty("http.nonProxyHosts", "localhost|192.168.10.*");
        // 通过系统属性设置HTTPS访问所用的代理服务器的主机地址、端口
        props.setProperty("https.proxyHost", "192.168.10.96");
        props.setProperty("https.proxyPort", "443");
        /* DefaultProxySelector不支持https.nonProxyHosts属性,
         DefaultProxySelector直接按http.nonProxyHosts的设置规则处理 */
        // 通过系统属性设置FTP访问所用的代理服务器的主机地址、端口
        props.setProperty("ftp.proxyHost", "192.168.10.96");
        props.setProperty("ftp.proxyPort", "2121");
        // 通过系统属性设置FTP访问无需使用代理服务器的主机
        props.setProperty("ftp.nonProxyHosts", "localhost|192.168.10.*");
        // 通过系统属性设置设置SOCKS代理服务器的主机地址、端口
        props.setProperty("socks.ProxyHost", "192.168.10.96");
        props.setProperty("socks.ProxyPort", "1080");
        // 获取系统默认的代理选择器
        ProxySelector selector = ProxySelector.getDefault();   // ①
        System.out.println("系统默认的代理选择器:" + selector);
        // 根据URI动态决定所使用的代理服务器
        System.out.println("系统为ftp://www.crazyit.org选择的代理服务器为:"
            + ProxySelector.getDefault().select(new URI("ftp://www.crazyit.org"))); // ②
        URL url = new URL(urlStr);
        // 直接打开连接,默认的代理选择器会使用http.proxyHost、
        // http.proxyPort系统属性设置的代理服务器,
        // 如果无法连接代理服务器,默认的代理选择器会尝试直接连接
        URLConnection conn = url.openConnection();   // ③
        // 设置超时时长。
        conn.setConnectTimeout(3000);
        try(
            Scanner scan = new Scanner(conn.getInputStream() , "utf-8"))
        {
            // 读取远程主机的内容
            while(scan.hasNextLine())
            {
                System.out.println(scan.nextLine());
            }
        }
    }
}


 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值