创建阻塞的服务器

ServerSocketChannelSockelChannel 采用默认的阻塞模式时,为了同时处理多个客户的连接,必须使用多线程

public class EchoServer {

	private int port = 8000;
private ServerSocketChannel serverSocketChannel = null;
private ExecutorService executorService; //线程池
private static final int POOL_MULTIPLE = 4; //线程池中工作线程的数目 public EchoServer() throws IOException {
//创建一个线程池
executorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() * POOL_MULTIPLE);
//创建一个ServerSocketChannel对象
serverSocketChannel = ServerSocketChannel.open();
//使得在同一个主机上关闭了服务器程序,紧接着再启动该服务器程序时,可以顺利绑定相同的端口
serverSocketChannel.socket().setReuseAddress(true);
//把服务器进程与一个本地端口绑定
serverSocketChannel.socket().bind(new InetSocketAddress(port));
System.out.println("服务器启动");
} public void service() {
while (true) {
SocketChannel socketChannel = null;
try {
socketChannel = serverSocketChannel.accept();
//处理客户连接
executorService.execute(new Handler(socketChannel));
} catch(IOException e) {
e.printStackTrace();
}
}
} public static void main(String args[])throws IOException {
new EchoServer().service();
} //处理客户连按
class Handler implements Runnable { private SocketChannel socketChannel; public Handler(SocketChannel socketChannel) {
this.socketChannel = socketChannel;
} public void run() {
handle(socketChannel);
} public void handle(SocketChannel socketChannel) {
try {
//获得与socketChannel关联的Socket对象
Socket socket = socketChannel.socket();
System.out.println("接收到客户连接,来自:" + socket.getInetAddress() + ":" + socket.getPort()); BufferedReader br = getReader(socket);
PrintWriter pw = getWriter(socket); String msg = null;
while ((msg = br.readLine()) != null) {
System.out.println(msg);
pw.println(echo(msg));
if (msg.equals("bye")) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(socketChannel != null) {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
} private PrintWriter getWriter(Socket socket) throws IOException {
OutputStream socketOut = socket.getOutputStream();
return new PrintWriter(socketOut,true);
} private BufferedReader getReader(Socket socket) throws IOException {
InputStream socketIn = socket.getInputStream();
return new BufferedReader(new InputStreamReader(socketIn));
} public String echo(String msg) {
return "echo:" + msg;
}
}

创建非阻塞的服务器

在非阻塞模式下,EchoServer 只需要启动一个主线程,就能同时处理三件事:

  • 接收客户的连接
  • 接收客户发送的数据
  • 向客户发回响应数据

EchoServer 委托 Selector 来负责监控接收连接就绪事件、读就绪事件和写就绪事件如果有特定事件发生,就处理该事件

// 创建一个Selector对象
selector = Selector.open();
//创建一个ServerSocketChannel对象
serverSocketChannel = ServerSocketChannel.open();
//使得在同一个主机上关闭了服务器程序,紧接着再启动该服务器程序时
//可以顺利绑定到相同的端口
serverSocketChannel.socket().setReuseAddress(true);
//使ServerSocketChannel工作于非阻塞模式
serverSocketChannel.configureBlocking(false):
//把服务器进程与一个本地端口绑定
serverSocketChannelsocket().bind(new InetSocketAddress(port));

EchoServer 类的 service() 方法负责处理本节开头所说的三件事,体现其主要流程的代码如下:

public void service() throws IOException {
serverSocketChannel.reqister(selector, SelectionKey.OP_ACCEPT);
//第1层while循环
while(selector.select() > 0) {
//获得Selector的selected-keys集合
Set readyKeys = selector.selectedKeys();
Iterator it = readyKeys.iterator();
//第2层while循环
while (it.hasNext()) {
SelectionKey key = null;
//处理SelectionKey
try {
//取出一个SelectionKey
key = (SelectionKey) it.next();
//把 SelectionKey从Selector 的selected-key 集合中删除
it.remove();
1f (key.isAcceptable()) { 处理接收连接就绪事件; }
if (key.isReadable()) { 处理读就绪水件; }
if (key.isWritable()) { 处理写就绪事件; }
} catch(IOException e) {
e.printStackTrace();
try {
if(key != null) {
//使这个SelectionKey失效
key.cancel();
//关闭与这个SelectionKey关联的SocketChannel
key.channel().close();
}
} catch(Exception ex) {
e.printStackTrace();
}
}
}
}
}
  • 首先由 ServerSocketChannelSelector 注册接收连接就绪事件,如果 Selector 监控到该事件发生,就会把相应的 SelectionKey 对象加入 selected-keys 集合
  • 第一层 while 循环,不断询问 Selector 已经发生的事件,select() 方法返回当前相关事件已经发生的 SelectionKey 的个数,如果当前没有任何事件发生,该方法会阻塞下去,直到至少有一个事件发生。SelectorselectedKeys() 方法返回 selected-keys 集合,它存放了相关事件已经发生的 SelectionKey 对象
  • 第二层 while 循环,从 selected-keys 集合中依次取出每个 SelectionKey 对象并从集合中删除,,然后调用 isAcceptable()isReadable()isWritable() 方法判断到底是哪种事件发生了,从而做出相应的处理

1. 处理接收连接就绪事件

if (key.isAcceptable()) {
//获得与SelectionKey关联的ServerSocketChannel
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//获得与客户连接的SocketChannel
SocketChannel socketChannel = (SocketChannel) ssc.accept();
//把Socketchannel设置为非阻塞模式
socketChannel.configureBlocking(false);
//创建一个用于存放用户发送来的数据的级冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//Socketchannel向Selector注册读就绪事件和写就绪事件
socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, buffer);
}

2. 处理读就绪事件

public void receive(SelectionKey key) throws IOException {
//获得与SelectionKey关联的附件
ByteBuffer buffer = (ByteBuffer) key.attachment();
//获得与SelectionKey关联的Socketchannel
SocketChannel socketChannel = (SocketChannel)key.channel();
//创建一个ByteBuffer用于存放读到的数据
ByteBuffer readBuff = ByteBuffer.allocate(32);
socketChannel.read(readBuff);
readBuff.flip();
//把buffer的极限设为容量
buffer.limit(buffer.capacity());
//把readBuff中的内容拷贝到buffer
buffer.put(readBuff);
}

3. 处理写就绪事件

public void send(SelectionKey key) throws IOException {
//获得与SelectionKey关联的ByteBuffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
//获得与SelectionKey关联的SocketChannel
SocketChannel socketChannel = (SocketChannel) key.channel();
buffer.flip();
//按照GBK编码把buffer中的字节转换为字符串
String data = decode(buffer);
//如果还没有读到一行数据就返回
if(data.indexOf("\r\n") == -1)
return;
//截取一行数据
String outputData = data.substring(0, data.indexOf("\n") + 1);
//把输出的字符串按照GBK编码转换为字节,把它放在outputBuffer中
ByteBuffer outputBuffer = encode("echo:" + outputData);
//输出outputBuffer的所有字节
while(outputBuffer,hasRemaining())
socketChannel.write(outputBuffer);
//把outputData字符审按照GBK编码,转换为字节,把它放在ByteBuffer
ByteBuffer temp = encode(outputData);
//把buffer的位置设为temp的极限
buffer.position(temp.limit()):
//删除buffer已经处理的数据
buffer.compact();
//如果已经输出了字符串“bye\r\n”,就使SelectionKey失效,并关闭SocketChannel
if(outputData.equals("bye\r\n")) {
key.cancel();
socketChannel.close();
}
}

完整代码如下:

public class EchoServer {

	private int port = 8000;
private ServerSocketChannel serverSocketChannel = null;
private Selector selector;
private Charset charset = Charset.forName("GBK"); public EchoServer() throws IOException {
// 创建一个Selector对象
selector = Selector.open();
//创建一个ServerSocketChannel对象
serverSocketChannel = ServerSocketChannel.open();
//使得在同一个主机上关闭了服务器程序,紧接着再启动该服务器程序时
//可以顺利绑定到相同的端口
serverSocketChannel.socket().setReuseAddress(true);
//使ServerSocketChannel工作于非阻塞模式
serverSocketChannel.configureBlocking(false):
//把服务器进程与一个本地端口绑定
serverSocketChannelsocket().bind(new InetSocketAddress(port));
} public void service() throws IOException {
serverSocketChannel.reqister(selector, SelectionKey.OP_ACCEPT);
//第1层while循环
while(selector.select() > 0) {
//获得Selector的selected-keys集合
Set readyKeys = selector.selectedKeys();
Iterator it = readyKeys.iterator();
//第2层while循环
while (it.hasNext()) {
SelectionKey key = null;
//处理SelectionKey
try {
//取出一个SelectionKey
key = (SelectionKey) it.next();
//把 SelectionKey从Selector 的selected-key 集合中删除
it.remove();
1f (key.isAcceptable()) {
//获得与SelectionKey关联的ServerSocketChannel
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//获得与客户连接的SocketChannel
SocketChannel socketChannel = (SocketChannel) ssc.accept();
//把Socketchannel设置为非阻塞模式
socketChannel.configureBlocking(false);
//创建一个用于存放用户发送来的数据的级冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//Socketchannel向Selector注册读就绪事件和写就绪事件
socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, buffer);
}
if (key.isReadable()) { receive(key); }
if (key.isWritable()) { send(key); }
} catch(IOException e) {
e.printStackTrace();
try {
if(key != null) {
//使这个SelectionKey失效
key.cancel();
//关闭与这个SelectionKey关联的SocketChannel
key.channel().close();
}
} catch(Exception ex) {
e.printStackTrace();
}
}
}
}
} public void receive(SelectionKey key) throws IOException {
//获得与SelectionKey关联的附件
ByteBuffer buffer = (ByteBuffer) key.attachment();
//获得与SelectionKey关联的Socketchannel
SocketChannel socketChannel = (SocketChannel)key.channel();
//创建一个ByteBuffer用于存放读到的数据
ByteBuffer readBuff = ByteBuffer.allocate(32);
socketChannel.read(readBuff);
readBuff.flip();
//把buffer的极限设为容量
buffer.limit(buffer.capacity());
//把readBuff中的内容拷贝到buffer
buffer.put(readBuff);
} public void send(SelectionKey key) throws IOException {
//获得与SelectionKey关联的ByteBuffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
//获得与SelectionKey关联的SocketChannel
SocketChannel socketChannel = (SocketChannel) key.channel();
buffer.flip();
//按照GBK编码把buffer中的字节转换为字符串
String data = decode(buffer);
//如果还没有读到一行数据就返回
if(data.indexOf("\r\n") == -1)
return;
//截取一行数据
String outputData = data.substring(0, data.indexOf("\n") + 1);
//把输出的字符串按照GBK编码转换为字节,把它放在outputBuffer中
ByteBuffer outputBuffer = encode("echo:" + outputData);
//输出outputBuffer的所有字节
while(outputBuffer,hasRemaining())
socketChannel.write(outputBuffer);
//把outputData字符审按照GBK编码,转换为字节,把它放在ByteBuffer
ByteBuffer temp = encode(outputData);
//把buffer的位置设为temp的极限
buffer.position(temp.limit()):
//删除buffer已经处理的数据
buffer.compact();
//如果已经输出了字符串“bye\r\n”,就使SelectionKey失效,并关闭SocketChannel
if(outputData.equals("bye\r\n")) {
key.cancel();
socketChannel.close();
}
} //解码
public String decode(ByteBuffer buffer) {
CharBuffer charBuffer = charset.decode(buffer);
return charBuffer.toStrinq();
} //编码
public ByteBuffer encode(String str) {
return charset.encode(str);
} public static void main(String args[])throws Exception {
EchoServer server = new EchoServer();
server.service();
}
}

阻塞模式与非阻塞模式混合使用

使用非阻塞模式时,ServerSocketChannel 以及 SocketChannel 都被设置为非阻塞模式,这使得接收连接、接收数据和发送数据的操作都采用非阻塞模式,EchoServer 采用一个线程同时完成这些操作

假如有许多客户请求连接,可以把接收客户连接的操作单独由一个线程完成,把接收数据和发送数据的操作由另一个线程完成,这可以提高服务器的并发性能

负责接收客户连接的线程按照阻塞模式工作,如果收到客户连接,就向 Selector 注册读就绪和写就绪事件,否则进入阻塞状态,直到接收到了客户的连接。负责接收数据和发送数据的线程按照非阻塞模式工作,只有在读就绪或写就绪事件发生时,才执行相应的接收数据和发送数据操作

public class EchoServer {

	private int port = 8000;
private ServerSocketChannel serverSocketChannel = null;
private Selector selector = null;
private Charset charset = Charset.forName("GBK"); public EchoServer() throws IOException {
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().setReuseAddress(true);
serverSocketChannelsocket().bind(new InetSocketAddress(port));
} public void accept() {
while(true) {
try {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false); ByteBuffer buffer = ByteBuffer.allocate(1024);
synchronized(gate) {
selector.wakeup();
socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, buffer);
}
} catch(IOException e) {
e.printStackTrace();
}
}
} private Object gate=new Object(); public void service() throws IOException {
while(true) {
synchronized(gate){}
int n = selector.select();
if(n == 0) continue;
Set readyKeys = selector.selectedKeys();
Iterator it = readyKeys.iterator();
while (it.hasNext()) {
SelectionKey key = null;
try {
it.remove();
if (key.isReadable()) {
receive(key);
}
if (key.isWritable()) {
send(key);
}
} catch(IOException e) {
e.printStackTrace();
try {
if(key != null) {
key.cancel();
key.channel().close();
}
} catch(Exception ex) { e.printStackTrace(); }
}
}
}
} public void receive(SelectionKey key) throws IOException {
...
} public void send(SelectionKey key) throws IOException {
...
} public String decode(ByteBuffer buffer) {
...
} public ByteBuffer encode(String str) {
...
} public static void main(String args[])throws Exception {
final EchoServer server = new EchoServer();
Thread accept = new Thread() {
public void run() {
server.accept();
}
};
accept.start();
server.service();
}
}

注意一点:主线程的 selector select() 方法和 Accept 线程的 register(...) 方法都会造成阻塞,因为他们都会操作 Selector 对象的共享资源 all-keys 集合,这有可能会导致死锁

导致死锁的具体情形是:Selector 中尚没有任何注册的事件,即 all-keys 集合为空,主线程执行 selector.select() 方法时将进入阻塞状态,只有当 Accept 线程向 Selector 注册了事件,并且该事件发生后,主线程才会从 selector.select() 方法返回。然而,由于主线程正在 selector.select() 方法中阻塞,这使得 Acccept 线程也在 register() 方法中阻塞。Accept 线程无法向 Selector 注册事件,而主线程没有任何事件可以监控,所以这两个线程将永远阻塞下去

为了避免对共享资源的竞争,同步机制使得一个线程执行 register() 时,不允许另一个线程同时执行 select() 方法,反之亦然

Java 网络编程 —— 实现非阻塞式的服务器的更多相关文章

  1. Java网络编程 -- NIO非阻塞网络编程

    从Java1.4开始,为了替代Java IO和网络相关的API,提高程序的运行速度,Java提供了新的IO操作非阻塞的API即Java NIO.NIO中有三大核心组件:Buffer(缓冲区),Chan ...

  2. 网络编程之非阻塞connect编写

    一.connect非阻塞编写 TCP连接的建立涉及到一个三次握手的过程,且socket中connect函数需要一直等到客户接收到对于自己的SYN的ACK为止才返回, 这意味着每 个connect函数总 ...

  3. 20145215实验五 Java网络编程及安全

    20145215实验五 Java网络编程及安全 实验内容 掌握Socket程序的编写: 掌握密码技术的使用: 设计安全传输系统. 实验步骤 本次实验我的结对编程对象是20145208蔡野,我负责编写客 ...

  4. 20145239杜文超 实验五 Java网络编程

    20145239 实验五 Java网络编程 实验内容 组队,一人服务器,一人客户端. 下载加解密代码,先编译运行代码,一人加密一人解密,适当修改代码. 然后集成代码,一人加密后通过TCP发送,加密使用 ...

  5. Java网络编程和NIO详解5:Java 非阻塞 IO 和异步 IO

    Java网络编程和NIO详解5:Java 非阻塞 IO 和异步 IO Java 非阻塞 IO 和异步 IO 转自https://www.javadoop.com/post/nio-and-aio 本系 ...

  6. Java网络编程 -- BIO 阻塞式网络编程

    阻塞IO的含义 阻塞(blocking)IO :阻塞是指结果返回之前,线程会被挂起,函数只有在得到结果之后(或超时)才会返回 非阻塞(non-blocking)IO :非阻塞和阻塞的概念相对应,指在不 ...

  7. Java IO(3)非阻塞式输入输出(NIO)

    在上篇<Java IO(2)阻塞式输入输出(BIO)>的末尾谈到了什么是阻塞式输入输出,通过Socket编程对其有了大致了解.现在再重新回顾梳理一下,对于只有一个“客户端”和一个“服务器端 ...

  8. Socket-IO 系列(三)基于 NIO 的同步非阻塞式编程

    Socket-IO 系列(三)基于 NIO 的同步非阻塞式编程 缓冲区(Buffer) 用于存储数据 通道(Channel) 用于传输数据 多路复用器(Selector) 用于轮询 Channel 状 ...

  9. NIO非阻塞式编程

    /** * NIO非阻塞式编程<p> * 服务端和客户端各自维护一个管理通道的对象,我们称之为selector,该对象能检测一个或多个通道 (channel) 上的事件. * 我们以服务端 ...

  10. Java基础——NIO(二)非阻塞式网络通信与NIO2新增类库

    一.NIO非阻塞式网络通信 1.阻塞与非阻塞的概念  传统的 IO 流都是阻塞式的.也就是说,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在 ...

随机推荐

  1. Double-Checked Locking 双重检查锁问题

    Code Correctness: Double-Checked Locking Abstract Double-checked locking 是一种不正确的用法,并不能达到预期目标. Explan ...

  2. 53.cin、cin.get()、cin.getline()、getline()、gets()等函数的用法

    1.cin 用法1:最基本,也是最常用的用法,输入一个数字: #pragma warning(disable:4996) #define _CRT_SECURE_NO_WARNINGS 1 #incl ...

  3. Java面试——JVM知识

    一.什么情况下会发生栈内存溢出 [1]线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常.递归的调用一个简单的方法,不断累积就会抛出 StackOverflow ...

  4. Linux 多服务器时间同步设置

    找一个机器,作为时间服务器,所有的机器与这台集群时间进行定时的同步,比如,每隔十分钟,同步一次时间. 一.配置时间服务器具体步骤

  5. 德国坦克傲龙7.1PCIe | 魔音师 声源PCIe MaX 声卡驱动皮肤

    适用于德国坦克傲龙7.1PCIe和魔音师 声源PCIe MaX 声卡驱动皮肤的皮肤. 皮肤使用方法:退出声卡驱动程序(托盘图标右键退出),之后删除声卡驱动目录里面的TERRATECAudioCente ...

  6. 【LeetCode动态规划#05】背包问题的理论分析(基于代码随想录的个人理解,多图)

    背包问题 问题描述 背包问题是一系列问题的统称,具体包括:01背包.完全背包.多重背包.分组背包等(仅需掌握前两种,后面的为竞赛级题目) 下面来研究01背包 实际上即使是最经典的01背包,也不会直接出 ...

  7. 设计模式(二十九)----综合应用-自定义Spring框架-Spring IOC相关接口分析

    1 BeanFactory解析 Spring中Bean的创建是典型的工厂模式,这一系列的Bean工厂,即IoC容器,为开发者管理对象之间的依赖关系提供了很多便利和基础服务,在Spring中有许多IoC ...

  8. TS 基础及在 Vue 中的实践:TypeScript 都发布 5.0 版本啦,现在不学更待何时!

    大家好,我是 Kagol,OpenTiny 开源社区运营,TinyVue 跨端.跨框架组件库核心贡献者,专注于前端组件库建设和开源社区运营. 微软于3月16日发布了 TypeScript 5.0 版本 ...

  9. 图与网络分析—R实现(一)

    图与网络 一个网络G,也可以称为图(graph)或网络图,是一种包含了节点V(即网络参与者,也称顶点)与边E(即节点之间的连接关系)的数学结构,记作G={V,E}.可以使用一个矩阵来存放节点之间的连接 ...

  10. [Windows]CMD命令入门教程 与 Windows常见维护问题

    本博文最早是记录在本地电脑的,由于清理电脑的缘故,顺便将这篇笔记转移到公共博客,以便日后查阅和快速上手使用. 开门见山,步入正题,以下是Windows系统的常用CMD命令. ----2018-03-2 ...