学了一个星期的java网络编程了。现在来总结一下。
基础:java.net包
InetAddress 类是表示 IP(Internet 协议)地址的抽象。它拥有两个子类: 用于 IPv4 地址的 Inet4Address。 用于 IPv6 地址的 Inet6Address。 但是,在大多数情况下,不必直接处理子类,因为 InetAddress 抽象应该覆盖大多数必需的功能。常用的IP和域名之间的相互转换。
Socket 是 TCP 客户端 API,通常用于连接远程主机。
ServerSocket 是 TCP 服务器 API,通常接受源于客户端套接字的连接。
DatagramSocket 是 UDP 端点 API,用于发送和接收数据包
MulticastSocket 是 DatagramSocket 的子类,在处理多播组时使用。
使用 TCP 套接字的发送和接收操作需要借助 InputStream 和 OutputStream 来完成,这两者是通过 Socket.getInputStream() 和 Socket.getOutputStream() 方法获取的。
URI 是表示在 RFC 2396 中指定的统一资料标识符的类。顾名思义,它只是一个标识符,不直接提供访问资源的方法。
URL 是表示统一资源定位符的类,它既是 URI 的旧式概念又是访问资源的方法。
URLConnection 是根据 URL 创建的,是用于访问 URL 所指向资源的通信链接。此抽象类将大多数工作委托给底层协议处理程序,如 http 或 ftp。
HttpURLConnection 是 URLConnection 的子类,提供一些特定于 HTTP 协议的附加功能。 建议的用法是使用 URI 指定资源,然后在访问资源时将其转换为 URL。从该 URL 可以获取 URLConnection 以进行良好控制,也可以直接获取 InputStream。
进阶:java.nio包
学习此包,首先要理解非阻塞通信的含义。简单介绍一下就是:只创建一个线程,利用类似统筹原理的思想,去处理多个并发的任务。
ServerSocketChannel: ServerSocket 的替代类, 支持阻塞通信与非阻塞通信。
SocketChannel: Socket 的替代类, 支持阻塞通信与非阻塞通信。
Selector: 为ServerSocketChannel 监控接收连接就绪事件, 为 SocketChannel 监控连接就绪, 读就绪和写就绪事件。
SelectionKey: 代表 ServerSocketChannel 及 SocketChannel 向 Selector 注册事件的句柄。 当一个 SelectionKey 对象位于Selector 对象的 selected-keys 集合中时, 就表示与这个 SelectionKey 对象相关的事件发生了。
Buffer类,重要的缓冲数据结构。其子类包括:ByteBuffer、CharBuffer、IntBuffer等,所有的缓冲区都有以下属性:
- 容量(capacity): 表示该缓冲区可以保存多少数据。
- 极限(limit): 表示缓冲区的当前终点, 不能对缓冲区中超过极限的区域进行读写操作。 极限是可以修改的, 这有利于缓冲区的重用。 例如, 假定容量100 的缓冲区已经填满了数据, 接着程序在重用缓冲区时, 仅仅将 10 个新的数据写入缓冲区中从位置0 到10 的区域, 这时可以将极限设为 10, 这样就不能读取先前的数据了。 极限是一个非负整数, 不应该大于容量。
- 位置(position): 表示缓冲区中下一个读写单元的位置, 每次读写缓冲区的数据时, 都会改变该值, 为下一次读写数据作准备。 位置是一个非负整数, 不应该大于极限。
Charset 类提供了编码与解码的方法:
- ByteBuffer encode(String str): 对参数 Str 指定的字符串进行编码, 把得到的字节序列存放在一个 ByteBuffer 对象中, 并将其返回;
- ByteBuffer encode(CharBuffer cb): 对参数 cb 指定的字符缓冲区中的字符进行编码,把得到的字节序列存放在一个 ByteBuffer 对象中, 并将其返回;
- CharBuffer decode(ByteBuffer bb): 把参数 bb 指定的 ByteBuffer 中的字节序列进行解码, 把得到的字符序列存放在一个 CharBuffer 对象中, 并将其返回。
Charset 类的静态 forName(String encode) 方法返回一个 Charset 对象, 它代表参数 encode 指定的编码类型。
SelectableChannel 类是一种支持阻塞 I/O 和非阻塞 I/O 的通道。 在非阻塞模式下, 读写数据不会阻塞, 并且SelectableChannel 可以向 Selector 注册读就绪和写就绪等事件。 Selector 负责监控这些事件, 等到事件发生时, 比如发生了读就绪事件, SelectableChannel 就可以执行读操作了。SelectableChannel 的主要方法如下:
- public SelecotableChannel configureBlocking(boolean block) throws IOException
数block 为true 时, 表示把 SelectableChannel 设为阻塞模式; 如果参数block 为false, 表示把 SelectableChannel 设为非阻塞模式。 默认情况下, SelectableChannel 采用阻塞模式。 该方法返回 SelectableChannel 对象本身的引用, 相当于" return this"。
- public SelectionKey register(Selector sel, int ops) throws ClosedChannelException
- public SelectionKey register(Selector sel, int ops, Object attachment) throws ClosedChannelException
SeverSocketChannel 从 SeletableChannel 中继承了 configureBlocking() 和 register()方法。 ServerSocketChannel 是 ServerSocket 的替换类, 也具有负责接收客户连接的 accept() 方法。 ServerSocket 并没有 public 类型的构造方法, 必须通过它的静态方法open() 来创建 ServerSocketChannel 对象。 每个ServerSocketChannel 对象都与一个ServerSocket 对象关联。 ServerSocketChannel 的 socket() 方法返回与它关联的 ServerSocket 对象。 可通过以下方法把服务器进程绑定到一个本地端口:
serverSocketChannel.socket().bind(port);
ServerSocketChannel 的主要方法如下:
- public static ServerSocketChannel open() throws IOException
这是 ServerSocketChannel 类的静态工厂方法, 它返回一个 ServerSocketChannel 对象, 这个对象没有与任何本地端口绑定, 并且处于阻塞模式。
- public SocketChannel accept() throws IOException
类似于 ServerSocket 的accept() 方法, 用于接收客户的连接。 如果 ServerSocketChannel 处于非阻塞状态, 当没有客户连接时, 该方法立即返回 null; 如果ServerSocketChannel 处于阻塞状态, 当没有客户连接时, 它会一直阻塞下去, 直到有客户连接就绪, 或者出现了IOException。
值得注意的是, 该方法返回的 SocketChannel 对象处于阻塞模式, 如果希望把它改为非阻塞模式, 必须执行以下代码:
socketChannel.configureBlocking(false);
- public final int validOps()
返回 ServerSocketChannel 所能产生的事件, 这个方法总是返回 SelectionKey.OP_ACCEPT。
- public ServerSocket socket()
返回与 ServerSocketChannel 关联的 ServerSocket 对象。 每个 ServerSocketChannel 对象都与一个 ServerSocket 对象关联。
SocketChannel 可看作是 Socket 的替代类, 但它比 Socket 具有更多的功能。 SocketChannel 不仅从 SelectableChannel 父类中继承了 configureBlocking() 和 register() 方法, 并且实现了 ByteChannel 接口, 因此具有用于读写数据的 read(ByteBuffer dst) 和 write(ByteBuffer src) 方法。 SocketChannel 没有public 类型的构造方法, 必须通过它的静态方法open() 来创建 SocketChannel 对象。
Selector 类,只要 ServerSocketChannel 及 SocketChannel 向 Selector 注册了特定的事件, Selector 就会监控这些事件是否发生。 SelectableChannel 的 register() 方法负责注册事件, 该方法返回一个SelectionKey 对象, 该对象是用于跟踪这些被注册事件的句柄。 一个Selector 对象中会包含 3 种类型的 SelectionKey 集合。
- all-keys 集合: 当前所有向Selector 注册的 SelectionKey 的集合, Selector 的keys() 方法返回该集合。
- selected-keys 集合: 相关时间已经被Selector 捕获的SelectionKey 的集合。 Selector 的selectedKeys() 方法返回该集合。
- cancelled-keys 集合: 已经被取消的 SelectionKey 的集合。 Selector 没有提供访问这种集合的方法。
扩展:java.util.concurrent包和Future 模式
前一段时间学习了一下线程池技术,其实java本身的这个包可以说已经实现的非常好了。据说是一位牛人:Doug Lee写出来的。写的很好,所以就被sun加入java了。能写出这么优秀的东西,真是不服不行呀。
java.util.concurrent包分成了三个部分,分别是java.util.concurrent、java.util.concurrent.atomic和java.util.concurrent.lock。内容涵盖了并发集合类、线程池机制、同步互斥机制、线程安全的变量更新工具类、锁等等常用工具。
Executors通过这个类能够获得多种线程池的实例,例如可以调用newSingleThreadExecutor()获得单线程的ExecutorService,调用newFixedThreadPool()获得固定大小线程池的ExecutorService。拿到ExecutorService可以做的事情就比较多了,最简单的是用它来执行Runnable对象,也可以执行一些实现了Callable<T>的对象。用Thread的start()方法没有返回值,如果该线程执行的方法有返回值那用ExecutorService就再好不过了,可以选择submit()、invokeAll()或者invokeAny(),根据具体情况选择合适的方法即可。
Lock多线程编程中常常要锁定某个对象,之前会用synchronized来实现,现在又多了另一种选择,那就是java.util.concurrent.locks。通过Lock能够实现更灵活的锁定机制,它还提供了很多synchronized所没有的功能,例如尝试获得锁(tryLock())。
使用Lock时需要自己获得锁并在使用后手动释放,这一点与synchronized有所不同,所以通常Lock的使用方式是这样的:
<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->
Lock l
=
;
l.lock();
try
{
//
执行操作
}
finally
{
l.unlock();
}
java.util.concurrent.locks中提供了几个Lock接口的实现类,比较常用的应该是ReentrantLock。
Conditon代替了Object对象上的wait()、notify()和notifyAll()方法(Condition中提供了await()、signal()和signalAll()方法),当满足运行条件前挂起线程。Condition是与Lock结合使用的,通过Lock.newCondition()方法能够创建与Lock绑定的Condition实例。
AtomicInteger对变量的读写操作都是原子操作(除了long或者double的变量),但像数值类型的++ --操作不是原子操作,像i++中包含了获得i的原始值、加1、写回i、返回原始值,在进行类似i++这样的操作时如果不进行同步问题就大了。好在java.util.concurrent.atomic为我们提供了很多工具类,可以以原子方式更新变量。
以AtomicInteger为例,提供了代替++ --的getAndIncrement()、incrementAndGet()、getAndDecrement()和decrementAndGet()方法,还有加减给定值的方法、当前值等于预期值时更新的compareAndSet()方法。
CountDownLatch是一个一次性的同步辅助工具,允许一个或多个线程一直等待,直到计数器值变为0。它有一个构造方法,设定计数器初始值,即在await()结束等待前需要调用多少次countDown()方法。CountDownLatch的计数器不能重置,所以说它是“一次性”的,如果需要重置计数器,可以使用CyclicBarrier。
设计模式除了GOF的23中经典模式以外,还有很多很实用的模式。比如这个Future模式,在多线程编程中就会常常用到。这个模式对于我们搞金融软件的人就比较容易理解了。炒期货的人也很容易的理解。
举个服务器和客户端交互的例子:
(1)Server先给Client一个“期权”,同时开一个线程去干活建房子(未来的“现房”);
(2)当“现房”RealData准备好了的时候,如何告诉FutureData说已经准备好了。(采用“回调过程”(借用观察者模式,来实现回调))
(3)如果客户比较着急,现房还没准备好的时候,就要取房,怎么办?那就只能“阻塞”了。
目标:Apache Mina
前面说了这么多,都是为了学习mina做准备。因为mina就是基于java.nio、java.util.concurrent、Future模式等的。
Mina的官方介绍:
MINA(Multipurpose Infrastructure for Network Applications)是用于开发高性能和高可用性的网络应用程序的基础框架。通过使用MINA框架可以可以省下处理底层I/O和线程并发等复杂工作,开发人员能够把更多的精力投入到业务设计和开发当中。MINA框架的应用比较广泛,应用的开源项目有Apache Directory、AsyncWeb、Apache Qpid、QuickFIX/J、Openfire、SubEthaSTMP、red5等。MINA框架当前稳定版本是1.1.6,最新的2.0版本目前已经发布了M1版本。
MINA框架的特点有:基于java NIO类库开发;采用非阻塞方式的异步传输;事件驱动;支持批量数据传输;支持TCP、UDP协议;控制反转的设计模式(支持Spring);采用优雅的松耦合架构;可灵活的加载过滤器机制;单元测试更容易实现;可自定义线程的数量,以提高运行于多处理器上的性能;采用回调的方式完成调用,线程的使用更容易。
要理解MINA有一张图片是不能错过的:
要深入学习MINA,有篇教程也是不能错过的:
Mina2.0框架源码剖析.doc
关于MINA,前人已经写了太多的东西了。那么上我的作品吧,在看了上面的教程之后,结合MINA源码,写了一个简单的Http代理程序。
Code
<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->package anotherproxy;
import java.net.InetSocketAddress;
import org.apache.mina.core.service.IoConnector;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
public class Main {
public static void main(String[] args) throws Exception {
NioSocketAcceptor acceptor = new NioSocketAcceptor();
acceptor.setReuseAddress(true);
IoConnector connector = new NioSocketConnector();
connector.setConnectTimeoutMillis(ProxyConstants.CONNECTTIMEOUT);
ClientToProxyIoHandler handler = new ClientToProxyIoHandler(connector);
acceptor.setHandler(handler);
acceptor.setBacklog(100);
IEProxy.on();
acceptor.bind(new InetSocketAddress(ProxyConstants.PROXYPORT));
System.out.println("Listening on port " + ProxyConstants.PROXYPORT);
}
}
Code
<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->package anotherproxy;
import java.io.IOException;
import java.nio.charset.Charset;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
public abstract class AbstractProxyIoHandler extends IoHandlerAdapter {
protected static final Charset CHARSET = Charset.forName("utf-8");
public static final String OTHER_IO_SESSION = AbstractProxyIoHandler.class.getName()
+ ".OtherIoSession";
@Override
public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
if (cause instanceof IOException) {
return;
}
cause.printStackTrace();
}
@Override
public void sessionOpened(IoSession session) {
session.getConfig().setIdleTime(IdleStatus.READER_IDLE, 5);
}
@Override
public void sessionIdle(IoSession session, IdleStatus status) {
if (status == IdleStatus.READER_IDLE) {
session.close(true);
}
}
@Override
public void sessionClosed(IoSession session) throws Exception {
if (session.getAttribute( OTHER_IO_SESSION ) != null) {
IoSession sess = (IoSession) session.getAttribute(OTHER_IO_SESSION);
sess.close(false);
sess.setAttribute(OTHER_IO_SESSION, null);
session.setAttribute(OTHER_IO_SESSION, null);
}
}
}
Code
<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->package anotherproxy;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.future.IoFutureListener;
import org.apache.mina.core.service.IoConnector;
import org.apache.mina.core.session.IoSession;
public class ClientToProxyIoHandler extends AbstractProxyIoHandler {
private final ServerToProxyIoHandler connectorHandler = new ServerToProxyIoHandler();
private final IoConnector connector;
private SocketAddress remoteAddress;
public ClientToProxyIoHandler(IoConnector connector) {
this.connector = connector;
this.connector.setHandler(connectorHandler);
}
@Override
public void messageReceived(final IoSession session, Object message) throws Exception {
final IoBuffer rb = (IoBuffer) message;
String request = rb.getString(CHARSET.newDecoder());
System.out.println("Send*********************************");
System.out.println("session " + session.getId());
System.out.println(request);
Pattern pattern = Pattern.compile("Host: (.*)");
Matcher matcher = pattern.matcher(request);
String host;
if (matcher.find()) {
host = matcher.group(1);
} else {
session.close(true);
return;
}
remoteAddress = new InetSocketAddress(InetAddress.getByName(host), 80);
connector.connect(remoteAddress).addListener(new IoFutureListener<ConnectFuture>() {
@Override
public void operationComplete(ConnectFuture future) {
if (future.isConnected()) {
future.getSession().setAttribute(OTHER_IO_SESSION, session);
session.setAttribute(OTHER_IO_SESSION, future.getSession());
rb.flip();
future.getSession().write(rb);
System.out.println("server session:" + future.getSession().getId()
+ " client session:" + session.getId());
}
}
});
}
}
Code
<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->package anotherproxy;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.future.WriteFuture;
import org.apache.mina.core.session.IoSession;
public class ServerToProxyIoHandler extends AbstractProxyIoHandler {
@Override
public void messageReceived(IoSession session, Object message) throws Exception {
IoBuffer rb = (IoBuffer) message;
IoBuffer wb = IoBuffer.allocate(rb.remaining());
rb.mark();
wb.put(rb);
wb.flip();
WriteFuture writeFuture = ((IoSession) session.getAttribute(OTHER_IO_SESSION)).write(wb);
writeFuture.awaitUninterruptibly();
System.out.println("****************Received*****************");
System.out.println("session " + session.getId());
System.out.println(wb.getString(CHARSET.newDecoder()));
rb.reset();
}
}