普通的socket通信是:ServerSocket 和Socket 配合工作
NIO的Socket是:ServerSocketChannel和SocketChannel 配合工作
我们知道ServerSocket 和 Socket是阻塞的
而ServerSocketChannel 和 SocketChannel 是否阻塞可设置的。
------ 通过configureBlocking(boolean) 方法指定 ----
如果不借助于Selector
ServerSocketChannel 和 SocketChannel 为阻塞时的通信体系
- 当ServerSocketChannel 和 SocketChannel 为阻塞的时候,其实就相当于普通的socket通信
public class ServerSocketChannelDemo {
public static Charset charset = Charset.forName("utf-8");
/**
* serverSocketChannel 和 socketChannel 都是阻塞式的。
* 所以 serverSocketChannel 的accept和socketChannel 的read 都会造成阻塞
* @param args
*/
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(30000));
// serverSocketChannel.configureBlocking(false);
SocketChannel socketChannel = serverSocketChannel.accept();
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer);
System.out.println(charset.decode(buffer));
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public class ClientSocketChannelDemo {
public static void main(String[] args) throws IOException, Exception {
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",30000));
Thread.currentThread().sleep(100000);//不要让客户端关闭了socket连接
}
}
调试代码,会发现 serverSocketChannel.accept() 和 socketChannel.read(buffer) 都会造成阻塞,与ServerSocket 和Socket的组合构建的通信系统差不多
ServerSocketChannel 和 SocketChannel 为非阻塞通信体系
- 当ServerSocketChannel 和 SocketChannel 设置为非阻塞的时候,
这种模式的设计,因为采用的是非阻塞式的,所以不管有没有接收到连接或者读取到数据,都会返回,因此要保证读取数据的完整性和准确性,就需要借助循环和线程休眠的方式进行设计。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentLinkedQueue;
// 由于是非阻塞的,所以可以引入线程池,阻塞式的不能采用线程池
public class NonBlockingSocketChannelServer {
public static volatile Map<Integer,SocketChannel> keys = Collections.synchronizedMap(new HashMap<Integer, SocketChannel>());//考虑并发 移除也可以用ConcurrentHashMap
static ConcurrentLinkedQueue<MsgWrapper> msgQueue = new ConcurrentLinkedQueue<MsgWrapper>();
static Charset charset = Charset.forName("utf-8");
public static void main(String[] args) {
AcceptSocketThread acceptSocketThread = new AcceptSocketThread();
acceptSocketThread.start();
ReadMsgThread readMsgThread = new ReadMsgThread();
readMsgThread.start();
ConsumerMsgThread consumerMsgThread = new ConsumerMsgThread();
consumerMsgThread.start();
}
static class AcceptSocketThread extends Thread {
volatile boolean runningFlag = true;
public void run(){
try {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(30000));
serverChannel.configureBlocking(false);
while(runningFlag){
SocketChannel channel = serverChannel.accept();
if(null == channel){
System.out.println("服务端监听中.....");
Thread.currentThread().sleep(1000);
}else{
channel.configureBlocking(false);
System.out.println("一个客户端上线,占用端口 :" + channel.socket().getPort());
keys.put(channel.socket().getPort(), channel);
}
}
} catch (IOException | InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
/**
* 由于读是非阻塞式的,所以没必要一个socketChannel一个线程
* 也可以通过线程池来执行,此处只做实例,学习,不扩展
* @author liangpro
*/
static class ReadMsgThread extends Thread{
ByteBuffer buffer = ByteBuffer.allocate(1024);
public void run(){
try {
int num = 0;
for (;;) {
Iterator<Integer> ite = keys.keySet().iterator();
while (ite.hasNext()) {
int key = ite.next();
StringBuffer stb = new StringBuffer();
try{
while((num = keys.get(key).read(buffer)) > 0 ){
buffer.flip();
stb.append(charset.decode(buffer).toString());
buffer.clear();
}
if(stb.length() > 0){
MsgWrapper msg = new MsgWrapper();
msg.key = key;
msg.msg = stb.toString();
System.out.println("端口:" + msg.key + "的通道,读取到的数据" + msg.msg);
msgQueue.add(msg);
}
}catch(Exception e){
System.out.println("error: 端口占用为:" + keys.get(key).socket().getPort() + ",的连接的客户端下线了");
ite.remove();
e.printStackTrace();//这只是个输出语句
}
}
sleep(1000);
System.out.println("读取线程监听中......");
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
static class ConsumerMsgThread extends Thread{
public volatile boolean isRunningFlag = false;
public void run (){
isRunningFlag = true;
try {
// sleep(10000);
while (isRunningFlag) {
MsgWrapper msg = msgQueue.poll();
for (; null != msg; msg = msgQueue.poll()) {
SocketChannel channel = keys.get(msg.key);
channel.write(charset.encode("response:" + msg.msg));
}
sleep(1000);
System.out.println("响应线程响应中......");
}
} catch (IOException | InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
static class MsgWrapper {
public int key;
public String msg;
}
}
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NonBlockingSocketChannelClient {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(){
ByteBuffer buffer = ByteBuffer.allocate(1024);
public void run(){
try {
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",30000));
socketChannel.configureBlocking(false);
socketChannel.write(NonBlockingSocketChannelServer.charset.encode(socketChannel.socket().getPort() + ": send message"));
int num = 0;
StringBuffer stb = new StringBuffer();
for (;;) {
while(socketChannel.read(buffer) > 0){
buffer.flip();
stb.append(NonBlockingSocketChannelServer.charset.decode(buffer));
buffer.clear();
}
if(stb.length()>0){
break;
}
sleep(1000);
}
System.out.println(stb);
} catch (IOException | InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("over!");
}
}.start();
}
}
}
**** 程序中还有一个问题
就是服务端怎么知道客户端是否还存在,如果客户端直接把进程杀了,服务端怎样保证正常运行。
- 通过socket类的方法isClosed()、isConnected()、isInputStreamShutdown()、isOutputStreamShutdown()等,这些方法都是本地端的状态,无法判断远端是否已经断开连接。
- 通过OutputStream发送心跳消息,如果发送失败就表示远端已经断开连接,类似ping,远端需把正常数据和心跳信息分开。
- 通过socket的InputStream.read返回-1/0表示 对方关闭连接,抛出异常表示对方异常终止连接。
- 方法sendUrgentData,往输出流发送一个字节的数据,只要对方Socket的SO_OOBINLINE属性没有打开,就会自动舍弃这个字节,SO_OOBINLINE属性默认情况下就是关闭的!
下面一段代码就可以判断远端是否断开了连接:
try{
socket.sendUrgentData(0xFF);
}catch(Exception ex){
reconnect();//客户端要是断了,服务端抛异常,
}
- 其实在通过socket.getoutstream和socket.getinputstream流对客户端发送、接受信息时如果socket没连接上是会抛出异常的,这也就是为什么Java会要求网络编程都要写在try里面,所以只要在catch里面写入客户端退出的处理就行了
- 对于非阻塞式的ServerSocketChannel 和 SocketChannel 使用起来有一个难题不好处理,那就是什么时候接收连接,什么时候接收数据,什么时候读取数据,不知道就只能一直循环遍历。
因此引入了Selector 负责监听通道的接收、读、写事件。
基于Selector的nio通信体系
设计思路
- 打开一个Selector,专门用于监听ServerSocketChannel,(之所以启用专门的Selector负责监听ServerSocketChannel,是因为ServerSocketChannel只有一个,在Selector中只注册了一个勾子,所以一次select()方法,只能接收一个客户端的连接,如果跟SocketChannel的读写监听注册同一个Selector,当读写比较耗时的时候,由于每次select()只能新增一个客户端,所以并发比较大的时候,会导致连接不及时。)
- 监听到的连接存储到队列中(而不是直接注册到Selector2中,因为注册连接,与Seletor的监听不能并发进行,注意Selector.select()方法是会对 selector对象加锁。)
- ServerReadWrite线程负责监听Selecor2中注册的 SocketChannel,将需要读的channel交由ReadMsgThread线程池处理,将可以写的SocketChannel交由WriteMsgThread处理(注意,通道非可读状态的时候,就是可写状态,所以注意控制循环,不要让循环太快)
- 交由Selector2监听负责控制通道的写操作,而不是交由ConsumerMsgThread随机并发的写,第一:对于单个通道来说,写操作是非并发的,第二:可以有效防止拥堵,阻塞,避免多个线程写同一个通道,而其它的通道空闲,避免同一通道既在读取数据,又在写数据,导致写数据阻塞