前言:
本文主要使用java编写各种网络IO服务端,并且深入到内核方法的调用
1. BIO
有如下服务端代码
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class SocketIO {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(9090);
System.out.println("step1:new ServerSocket(9090)");
while (true) {
Socket client = server.accept(); // Jvm转换成对内核系统调用 阻塞1
System.out.println("step2:client\t" + client.getPort());
new Thread(new Runnable() { // Java new Thread 其实是调用内核clone()方法分配操作系统内核线程
@Override
public void run() {
InputStream in = null;
try {
in = client.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
while (true) {
String dataline = reader.readLine(); // 真正调用时 阻塞2
if (null != dataline) {
System.out.println(dataline);
}else {
client.close();
break;
}
}
System.out.println("客户端断开");
}catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
它是如果建立tcp连接,并等待客户端调用呢。
我们将该代码放在linux系统上进行编译,执行如下命令
strace -ff -o out java SocketIO
输出:step1:new ServerSocket(9090)
查看调用内核日志
执行如下命令,查看主线程调用情况
vi out.6225
翻到最后几行
可以查看到如下几行日志
// 调用内核socket得到文件描述符7
socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 7
// 绑定文件描述符7 到端口9090
bind(7, {sa_family=AF_INET6, sin6_port=htons(9090), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::", &sin6_addr), sin6_scope_id=0}, 28) = 0
// 监听文件描述符7
listen(7, 50)
// 阻塞1 等待客户端连接 相当于java调用accept()
accept(7
另启一页,连接服务端,执行命令如下:
nc localhost 9090
服务端输出如下:
再次执行如下命令,查看主线程调用情况
vi out.6225
// 与客户端建立socket连接 8是唯一文件描述符
accept(7, {sa_family=AF_INET6, sin6_port=htons(55302), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_scope_id=0}, [28]) = 8
// new Thread() JVM调用内核的clone方法,分配操作系统内核线程 线程id 6260
clone(child_stack=0x7f2f75f8cfb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f2f75f8d9d0, tls=0x7f2f75f8d700, child_tidptr=0x7f2f75f8d9d0) = 6260
// 阻塞1 等待新客户端连接 相当于java调用accept()
accept(7
我们执行vi out.6260 查看new thread生成的线程做了什么。
翻到最后一行
// 阻塞2 接受客户端发送的数据
recvfrom(8
- 从上面可以看出,整个过程有2个阻塞,一个是服务端阻塞等待客户端连接,连接成功后,新线程阻塞等待已经连接的客户端发送数据,而主线程继续阻塞等待新客户端连接。
- 问题点:如果有1w个客户端连接服务端,那么服务端会抛出1w个线程,这么多线程资源就是一笔不小的开销;再者如果这1w个线程都是活跃线程,那么线程上下文切换也是很大的性能消耗;还有java线程依赖内核线程进入阻塞状态,也是一笔开销。
这里我们可能会思考几个问题:
-
内核调用 accept 方法生成的 socket 是什么?
accept 函数返回的新 socket 其实指代的是本次创建的连接(在linux,用文件描述符表示),而一个连接是包括两部分信息的,一个是源IP和源端口,另一个是宿IP和宿端口。所以,accept 函数可以产生多个不同的 socket,而这些 socket 里包含的宿IP和宿端口是不变的,变化的只是源IP和源端口。 -
操作系统如何知道发送网络数据对应哪个 socket 连接?
socket 连接包含有宿IP、端口和源IP、端口,Clietn 发送的数据包携带这两部分数据,数据包含有的宿IP、端口,可以让数据准确的到达 Server;而 Server 的网卡中断程序,解析数据包中的源IP、端口,就知道这个数据包应该发给哪个 socket 连接了。
2. NIO
有如下服务端代码
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
public class SocketNIO {
public static void main(String[] args) throws Exception {
LinkedList<SocketChannel> clients = new LinkedList<>();
ServerSocketChannel ss = ServerSocketChannel.open();
ss.bind(new InetSocketAddress(8090));
ss.configureBlocking(false); // 重点 os NONBLOCKING!!!
while (true) {
Thread.sleep(1000);
SocketChannel client = ss.accept(); // 不会阻塞
if (client == null) {
// System.out.println("null......");
}else {
client.configureBlocking(false);
int port = client.socket().getPort();
System.out.println("client...port:" + port);
clients.add(client);
}
ByteBuffer buffer = ByteBuffer.allocateDirect(4096); // 可以在堆里 堆外
for (SocketChannel c : clients) { // 串行化!!! 多线程!!
int num = c.read(buffer); // 不会阻塞
if (num > 0) {
buffer.flip();
byte[] aaa = new byte[buffer.limit()];
buffer.get(aaa);
String b = new String(aaa);
System.out.println(c.socket().getPort() + " : " + b);
buffer.clear();
}
}
}
}
}
我们将该代码放在linux系统上进行编译,执行如下命令
strace -ff -o out java SocketNIO
有这些线程启动
执行如下命令,查看主线程调用情况
tail -f out.6247
- 内核accept方法是非阻塞的,返回-1表示不阻塞,没有拿到客户端连接,被主线程循环调用
另启一页,执行如下命令调用服务端
nc localhost 8090
查看服务端主线程日志
端口为43884的客户端连接进来,文件描述符9是这次socket连接的唯一标识
主线程循环等待新客户端连接进来(accept方法非阻塞),循环访问连接进来的客户端是否接受到数据(read方法非阻塞)
- 从上面可以看出,等待客户端连接的内核方法accept是非阻塞,读取客户端数据内核方法read也是非阻塞。
- 问题点:如果有1w个客户端连接服务端,如果只有一个客户端发送数据过来,还是得去循环访问每个客户端是否发送数据过来,这样是很浪费性能的。
3. 多路复用 select、poll
在linux系统下,执行如下命令,查看多路复用器介绍
man select
#include <sys/select.h>
// nfds: 多少个文件描述符,readfds:文件描述符集合......
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *timeout,
const sigset_t *sigmask);
// 描述
DESCRIPTION
select() and pselect() allow a program to monitor multiple file descriptors, waiting until one or
more of the file descriptors become "ready" for some class of I/O operation......
- 通过一次系统调用,把fd set, 传递给内核,内核进行遍历(相对于NIO,这种轮询遍历减少了用户态到内核态的切换,也就是系统调用的次数)
- 问题点:1.重复传递fd 解决方案:内核开辟空间保留fd。 2.每次select、poll,都要重新遍历全量的fd 解决方案:中断,callback,回调函数
4. epoll
有如下服务端代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class SocketMultiplexingSingleThreadv1 {
private ServerSocketChannel server = null;
private Selector selector = null;
int port = 9090;
public void initServer() {
try {
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
selector = Selector.open(); // 默认选择顺序 内核epoll 内核poll 内核selector
server.register(selector, SelectionKey.OP_ACCEPT);
}catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
initServer();
System.out.println("服务器启动了。。。。。。");
try {
while (true) {
Set<SelectionKey> keys = selector.keys();
// System.out.println(keys.size() + " size");
while (selector.select(500) > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectionKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
acceptHandler(key);
}else if (key.isReadable()) {
readHandler(key);
}
}
}
}
}catch (IOException e) {
e.printStackTrace();
}
}
public void acceptHandler(SelectionKey key) {
try {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel client = ssc.accept();
client.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(8192);
client.register(selector,SelectionKey.OP_READ,buffer);
System.out.println("-----------------------");
System.out.println("新客户端:" + client.getRemoteAddress());
System.out.println("-----------------------");
} catch (IOException e) {
e.printStackTrace();
}
}
public void readHandler(SelectionKey key) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear();
int read = 0;
try {
while (true) {
read = client.read(buffer);
if (read > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
client.write(buffer);
}
}else if(read == 0) {
break;
}else {
client.close();
break;
}
}
}catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SocketMultiplexingSingleThreadv1 singleThreadv1 = new SocketMultiplexingSingleThreadv1();
singleThreadv1.start();
}
}
我们将该代码放在linux系统上进行编译,执行如下命令
strace -ff -o out java SocketMultiplexingSingleThreadv1
有这些线程启动
执行如下命令,查看主线程调用情况
vi out.12451
// 调用内核socket得到文件描述符8
socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 8
// 设置socket8为 非阻塞
fcntl(8, F_SETFL, O_RDWR|O_NONBLOCK) = 0
// 绑定文件描述符8 到端口9090
bind(8, {sa_family=AF_INET6, sin6_port=htons(9090), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::" , &sin6_addr), sin6_scope_id=0}, 28) = 0
// 监听8
listen(8, 50)
// 创建多路复用器 epoll 文件描述符为11
epoll_create(256) = 11
epoll_ctl(11, EPOLL_CTL_ADD, 9, {EPOLLIN, {u32=9, u64=16729297221677219849}}) = 0
// 把文件描述符8 注册到epoll 11
epoll_ctl(11, EPOLL_CTL_ADD, 8, {EPOLLIN, {u32=8, u64=18295631872807403528}}) = 0
// 相当于java的selector.select(500)/selector.selectedKeys();方法
epoll_wait(11, [], 8192, 500) = 0
另开一页,执行如下命令,连接服务端
nc localhost 8090
查看主线程日志
// 一个io通道准备就绪
epoll_wait(11, [{EPOLLIN, {u32=8, u64=1115190942760960008}}], 8192, 500) = 1
// 与客户端建立socket连接 12是唯一文件描述符
accept(8, {sa_family=AF_INET6, sin6_port=htons(52612), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_scope_id=0}, [28]) = 12
// 把文件描述符12,注册到epoll 11
epoll_ctl(11, EPOLL_CTL_ADD, 12, {EPOLLIN, {u32=12, u64=12}}) = 0
应用层将 Socket 连接通路通过 register 方法注入到内核 epoll 上,这些 IO 通道如果有客户端连接进来 or 数据发送进来,epoll 将它们 copy 到结果集空间,我们应用层调用 selector.select(50) 方法去检验结果集空间是否有准备的 Socket 连接通路。这里 cpu01 专门负责内核相关事情,而 cpu02 负责应用层方面,可以做到并行异步的效果。
- epoll怎 么知道你的 Socket 连接通路上有数据了,同时让你的程序开始处理你的数据?(cpu01复制通道到结果集空间) 答:网卡会发一个中断号给你的 cpu01,而不用 cpu01 轮询检查每个连接通路是否有数据过来。
- 问题点:应用层的服务端用户是单线程编码,如果客户端有10w个同时发送数据,单线程处理起来难免吃力。
epoll多线程java代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicInteger;
public class SocketMultiplexingThreads {
private ServerSocketChannel server = null;
private Selector selector0 = null;
private Selector selector1 = null;
private Selector selector2 = null;
int prot = 9090;
public void initServer() {
try {
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(prot));
selector0 = Selector.open();
selector1 = Selector.open();
selector2 = Selector.open();
server.register(selector0, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SocketMultiplexingThreads service = new SocketMultiplexingThreads();
service.initServer();
NioThread t1 = new NioThread(service.selector0, 2);
NioThread t2 = new NioThread(service.selector1 );
NioThread t3 = new NioThread(service.selector2);
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
t3.start();
}
public static class NioThread extends Thread {
Selector selector = null;
static int selectors = 0;
int id = 0;
volatile static BlockingQueue<SocketChannel>[] queue;
static AtomicInteger idx = new AtomicInteger();
NioThread(Selector sel, int n) {
NioThread.this.selector = sel;
NioThread.selectors = n;
queue = new LinkedBlockingDeque[selectors];
for (int i = 0; i < n; i++) {
queue[i] = new LinkedBlockingDeque<>();
}
System.out.println("Boss 启动");
}
NioThread(Selector sel) {
this.selector = sel;
id = idx.getAndIncrement() % selectors;
System.out.println("worker:" + id + "启动");
}
@Override
public void run() {
try {
while (true) {
while (selector.select(10) > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectionKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
acceptHandler(key);
}else if (key.isReadable()) {
readHandler(key);
}
}
}
if (!queue[id].isEmpty()) {
ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
SocketChannel client = queue[id].take();
client.register(selector,SelectionKey.OP_READ, buffer); // 把client连接,也就是内核fd 注册到epoll上
System.out.println("---------------------");
System.out.println("新客户端:" + client.socket().getPort() + "分配到:" + id);
System.out.println("---------------------");
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void acceptHandler(SelectionKey key) {
try {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel client = ssc.accept(); // 与客户端建立连接,生成内核fd
client.configureBlocking(false);
int num = idx.getAndIncrement() % selectors;
queue[num].add(client);
} catch (IOException e) {
e.printStackTrace();
}
}
public void readHandler(SelectionKey key) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear();
int read = 0;
try {
while (true) {
read = client.read(buffer);
if (read > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
client.write(buffer);
}
}else if(read == 0) {
break;
}else {
client.close();
break;
}
}
}catch (IOException e) {
e.printStackTrace();
}
}
}
}
5. netty
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import java.nio.charset.Charset;
public class NettyIO {
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup(1); // accpet 接受线程
NioEventLoopGroup worker = new NioEventLoopGroup(5); // recfron 读取线程
// 服务端启动引导
ServerBootstrap boot = new ServerBootstrap();
try {
boot.group(boss, worker) // 绑定两个程序组
.channel(NioServerSocketChannel.class) // 指定通道类型 nio
.option(ChannelOption.TCP_NODELAY, false) // 设置tcp
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(Channel channel) throws Exception {
ChannelPipeline p = channel.pipeline(); // 获取处理器链
p.addLast(new MyInbuund()); // 添加消息处理事件
}
})
.bind(9999) // 绑定断开
.sync()
.channel() // 阻塞主线程,知道网络服务被关闭
.closeFuture()
.sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class MyInbuund extends ChannelInboundHandlerAdapter {
// 每当从客户端收到新的数据时,这个方法会在收到消息时被调用
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("收到数据:" + ((ByteBuf) msg).toString(Charset.defaultCharset()));
ctx.write(Unpooled.wrappedBuffer("Server message".getBytes()));
ctx.fireChannelRead(msg);
}
// 数据读取完后被调用
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
// 当Netty由于IO错误或者处理器在处理事件时抛出的异常时被调用
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}