目录
1. 内核态与用户态切换
2. 基于 BIO 设计的 Socket 示例
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author floatcloud
*/
public class SocketServerDemo {
private static final Integer PORT = 9200;
/**
* 线程池
*/
private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10,
50, 5, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100));
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket( PORT);
System.out.printf("socket 启动,端口:%s", PORT);
while(true){
// 死循环中执行连接和IO逻辑
// 该accept过程为阻塞
Socket accept = socket.accept();
System.out.printf("socket accept 过程阻塞中,连接端口:%s", accept.getPort());
threadPool.execute(()->{
// 线程池执行IO逻辑
Socket threadSocket = accept;
try {
// IO 的过程也是阻塞的,因此需要另开一线程
// 防止都在主线程下,IO阻塞后,后续Socket连接无法获取到。
InputStream inputStream = threadSocket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
while (true){
System.out.println(bufferedReader.readLine());
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
}
# 连接到socket服务端后,输出内容
nc localhost 9200
# socket服务端打印内容如下
socket 启动,端口:9200socket accept 过程阻塞中,连接端口:55086jjajdhb
jajsb
dadslman
socket accept 过程阻塞中,连接端口:56064jajdhasbdba
jabbbb
dhajdb
从上不难看出:BIO的特点
- 阻塞 :ServerSocket调用accept方法阻塞(接受客户端阻塞)【相对于OS中指令,accept(数字,】数字为socket服务端的文件描述符;另一处阻塞,为IO程序的阻塞,对应方法readLine();
- 一连接一线程:缺点是大量线程资源的损耗;同时,线程切换,在OS层次上,即为CPU在内核态(内核程序)和用户态(应用)来回切换,这部分操作也是损耗CPU性能的。
3. OS 指令上 BIO 阻塞验证
这里需要使用到strace命令(linux操作系统),mac系统下使用dtruss命令:
简单介绍如下
strace是一个可用于诊断、调试和教学的Linux用户空间跟踪器。用它来监控用户空间进程和内核的交互,比如系统调用、信号传递、进程状态变更等。
系统调用
系统调用(英语:system call),又称为系统呼叫,指运行在用户空间的程序向操作系统内核请求需要更高权限运行的服务。
操作系统的进程空间分为用户空间和内核空间
- 操作系统内核直接运行在硬件上,提供设备管理、内存管理、任务调度等功能。
- 用户空间通过API请求内核空间的服务来完成其功能——内核提供给用户空间的这些API, 就是系统调用。
strace 参数设置
-tt 在每行输出的前面,显示毫秒级别的时间
-T 显示每次系统调用所花费的时间
-v 对于某些相关调用,把完整的环境变量,文件stat结构等打出来。
-f 跟踪目标进程,以及目标进程创建的所有子进程
-e 控制要跟踪的事件和跟踪行为,比如指定要跟踪的系统调用名称
-o 把strace的输出单独写到指定的文件
-s 当系统调用的某个参数是字符串时,最多输出指定长度的内容,默认是32个字节
-p 指定要跟踪的进程pid, 要同时跟踪多个pid, 重复多次-p选项即可。
命令如下:
strace -ff -o out java class文件名
# Mac 下使用 dtruss
dtruss -f -o out java class文件名
在 out 输出文件中,有以下关键指令
socket(...) = 3 # 3为文件描述符
...
bind(3,(...)) # 将 socket客户端与指定端口绑定
listen(3, 端口) # 监听指定端口,是否有连接
# 此后为socket连接的out文件输出
accept(3, # 该方法为阻塞方法,当有连接时,才会将连接绑定,并产生一个文件描述符,用作IO处理
4. NIO代码示例
import org.jboss.netty.util.CharsetUtil;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* @author floatcloud
*/
public class SocketNIODemo {
public static void main(String[] args) throws IOException, InterruptedException {
// 由于是非阻塞的,所以用于保存连接的SocketChannel
List<SocketChannel> clients = new ArrayList<>(20);
// 使用的java.nio包的类
ServerSocketChannel socketChannel = ServerSocketChannel.open();
// 绑定端口
socketChannel.bind(new InetSocketAddress(8090));
// 重点,设置Channel为非阻塞,默认为阻塞
socketChannel.configureBlocking(false);
while(true){
// 可以不加,这里为了避免五连接时刷新过快,便于演示
Thread.sleep(1000);
// accept方法在这里是非阻塞的(OS层次上为,socket 设置了 NONBLOCKING)
SocketChannel accept = socketChannel.accept();
if (accept == null) {
System.out.println("无连接......");
} else {
System.out.printf("连接的端口为:%s \t", accept.socket().getPort());
// IO 流程(非阻塞)
accept.configureBlocking(false);
clients.add(accept);
}
// 直接内存(零Copy)
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(2048);
// 遍历所有连接,写出数据
Iterator<SocketChannel> iterator = clients.iterator();
SocketChannel channel = null;
while(iterator.hasNext()){
channel = iterator.next();
try {
int read = channel.read(byteBuffer);
if (read > 0){
byteBuffer.flip();
byte[] msg = new byte[byteBuffer.remaining()];
byteBuffer.get(msg);
String msgStr = new String(msg, CharsetUtil.UTF_8);
System.out.printf("端口为:%s;输出内容为:%s \t", channel.socket().getPort(),
msgStr);
} else if (read < 0){
// 客户端关闭
channel.close();
System.out.printf("端口%s的客户端关闭", channel.socket().getPort());
}
} catch (IOException e) {
e.printStackTrace();
if(channel == null){
channel.close();
}
}
}
}
}
}
通过 nc localhost 8090 进行测试,输出结果如下:
无连接......
无连接......
无连接......
无连接......
连接的端口为:57438
无连接......
连接的端口为:57547 /n无连接......
无连接......
无连接......
端口为:57547;输出内容为:aaaaa
无连接......
无连接......
无连接......
端口为:57438;输出内容为:lllll
无连接......
无连接......
Process finished with exit code 130 (interrupted by signal 2: SIGINT)
可以看出,一个主线程完成了 Socket 接收以及 IO操作,但是两个过程均是非阻塞的。
OS 上 Socket 非阻塞 accept 方法,是其配置了SOCK_NONBLOCK
[root@... ~]# man 2 socket # 查看 socket 系统调用指令信息
...
Since Linux 2.6.27, the type argument serves a second purpose: in addition to specifying a socket type, it may include the bitwise OR of any of the following values, to modify the beh