Socket通道
Socket通道和文件通道有着不一样的特征:
- Socket通道类可以运行于非阻塞模式,并且是可选的。这两个特征可以激活大程序(如网络服务和中间件组件)巨大的可伸缩性和灵活性,因此再也没有为每个Socket连接添加一个线程的必要。这一特性避免了管理大量线程所需的上下文交换总开销,借助NIO,一个或几个线程就可以管理成百上千个Socket连接,并且没有或只有很少的性能损失
- 全部的Socket通道类(SocketChannel,ServerSocketChannel,DatagramChannel)在被实例化时都会创建一个对应的Socket对象,如Socket,ServerSocket,DatagramSocket,这些Socket可以通过调用对应通道类的socket()方法来获取。此外,这三个Socket都有getChannel()方法
- 每个Socket通道都有一个关联的java.net.socket对象,反之却不是这样。如果使用传统方式(直接实例化)创建了一个Socket对象,它不会关联有Socket通道,且它的getChannel()方法总是返回null
打开SocketChannel
SocketChannel channel = SocketChannel.open();
channel.connect(new InetSocketAddress("http://xxx,com", 80));
关闭SocketChannel
channel.close();
从SocketChannel读数据
ByteBuffer buf = ByteBuffer.allocate(100);
int bytr = channel.read(buf);
该方法将数据从SocketChannel读到Buffer中,read()方法的返回值表示读了多少字节到Buffer中,当返回值为-1时,表示已经读到了流的末尾
写入SocketChannel
String str = "some thing";
ByteBuffer buf = ByteBuffer.allocate(100);
buf.put(str.getBytes());
buf.flip();
while(buf.hasRemaining){
socketChannel.write(buf);
}
socketChannel.close();
注意: SocketChannel.write()方法的调用是在一个while循环中的。Write()方法无法保证能写多少字节到SocketChannel。所以,我们重复调用write()直到Buffer没有要写的字节为止。
非阻塞模式
要把一个Socket通道置于非阻塞模式,要依赖父类的父类SelectableChannel:
public abstract class SelectableChannel extends AbstractInterruptibleChannel implements Channel {
...
public abstract void configureBlocking(boolean block) throws IOException;
public abstract boolean isBlocking();
public abstract Object blockngLock();
...
}
从SelectableChannel的API可以看出,设置或重新设置一个通道的阻塞模式是很简单的,只要调用configureBlocking()方法即可,传递参数值为true则设为阻塞模式。参数值为false,则为非阻塞模式。同时,通过调用isBlocking()方法来判断是否为被阻塞。blockingLock()方法会返回一个不透明的对象引用,返回的对象是通道实现修改阻塞模式时内部使用的,只有拥有此对象的锁的线程才能更改通道的阻塞模式,对于确保在执行代码的关键部分时Socket通道的阻塞模式不会改变以及在不影响其他线程的前提下暂时改变阻塞模式来说,这个方法是非常方便的。
connect()方法
非阻塞模式下调用connect()方法,该方法可能在连接建立完之前就返回,为了确定连接是否建立,调用finishConnect()方法
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://xxx.com", ));
while(!socketChannel.finishConnect()){
//some codes 如果没有连接成功则...
}
write()方法
非阻塞模式下,write()方法在尚未写出任何内容时可能就返回了。所以需要在循环中调用write()
read()方法
非阻塞模式下,read()方法在尚未读取到任何数据时可能就返回了。所以需要关注它的int返回值,它会告诉你读取了多少字节
补充: 非阻塞模式与选择器搭配会工作的更好,通过将一或多个SocketChannel注册到Selector,可以询问选择器哪个通道已经准备好了读取,写入等
ServerSocketChannel
Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道, 就像标准IO中的ServerSocket一样。ServerSocketChannel类在 java.nio.channels包中
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.socket().bind(new InetSocketAddress(9999));
while(true){
SocketChannel socketChannel = serverChannel.accept();
}
打开ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
关闭ServerSocketChannel
serverChannel.close();
监听新进来的连接
通过ServerSocketchannel.accept()方法监听新进来的连接。accept()方法返回一个包含新进来的连接的SocketChannel,因此,accept()方法会一直阻塞到有新连接到达
通常不会仅仅只监听一个连接,在while循环中调用 accept()方法
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
}
非阻塞模式
ServerSocketChannel可以设置成非阻塞模式。在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是null。 因此,需要检查返回的SocketChannel是否是null
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.socket().bind(new InetSocketAddress(9999));
while(true){
SocketChannel socketChannel = serverChannel.accept();
if(socketChannel != null){
//some codes
}
}
Socket通道的服务端程序
public class SocketServer
{
public static void main(String[] args) throws Exception
{
int port = 1234;
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); //非阻塞模式
ServerSocket ss = ssc.socket();
ss.bind(new InetSocketAddress(port));
while (true)
{
SocketChannel sc = ssc.accept();
if (sc == null)
{
// 如果当前没有数据,等待1秒钟再次轮询是否有数据,在学习了Selector之后此处可以使用Selector
Thread.sleep(1000);
}
else {
ByteBuffer bb = ByteBuffer.allocate(100);
sc.read(bb);
bb.flip();
while (bb.hasRemaining()){
System.out.print((char)bb.get());
}
sc.close();
System.exit(0);
}
}
}
}
Socket通道的客戶端程序
public class NonBlockingSocketClient
{
private static final String STR = "Hello World!";
private static final String REMOTE_IP= "127.0.0.1"; public static void main(String[] args) throws Exception {
int port = 1234;
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false); //非阻塞模式
sc.connect(new InetSocketAddress(REMOTE_IP, port));
while (!sc.finishConnect()){
System.out.println("同" + REMOTE_IP+ "的连接正在建立,请稍等!");
Thread.sleep(10);
}
System.out.println("连接已建立,待写入内容至指定ip+端口!时间为" + System.currentTimeMillis());
ByteBuffer bb = ByteBuffer.allocate(STR.length());
bb.put(STR.getBytes());
bb.flip(); // 写缓冲区的数据之前一定要先反转(flip)
sc.write(bb);
bb.clear();
sc.close();
}
}
DatagramChannel
Java NIO中的DatagramChannel是一个能收发UDP包的通道。因为UDP是无连接的网络协议,所以不能像其它通道那样读取和写入。它发送和接收的是数据包
打开DatagramChannel
DatagramChannel datagramChannel = DatagramChannel.open();
datagramChannel.socket().bind(new InetSocketAddress(8089));
receive()方法
ByteBuffer buf = ByteBuffer.allocate(100);
datagramChannel.receive(buf);
receive()方法会将接收到的数据包内容复制到指定的Buffer. 如果Buffer容不下收到的数据,多出的数据将被丢弃
send()方法
String str = "some codes";
ByteBuffer buf = ByteBuffer.allocate(100);
buf.put(str.getBytes());
buf.flip();
int byt = datagramChannel.send(buf, new InetSocketAddress("xxx.com", 80));
这个例子发送一串字符到”xxx.com”服务器的UDP端口80。 因为服务端并没有监控这个端口,所以什么也不会发生。也不会通知你发出的数据包是否已收到,因为UDP在数据传送方面没有任何保证
连接到特点地址
datagramChannel.connect("xxx.com", 80);
可以将DatagramChannel“连接”到网络中的特定地址的。由于UDP是无连接的,连接到特定地址并不会像TCP通道那样创建一个真正的连接。而是锁住DatagramChannel ,让其只能从特定地址收发数据
当连接后,也可以使用read()和write()方法,就像在用传统的通道一样。只是在数据传送方面没有任何保证
int bytesRead = datagramChannel.read(buf);
int bytesWritten = datagramChannel.write(but);