Java Nio Server
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.Date;import java.util.Iterator;public class NioServer implements Runnable { private Selector selector; private ServerSocketChannel serverSocketChannel; public static void main(String[] args) { new Thread(new NioServer(8765), "NioServer-001").start(); } public NioServer(int port) { try { selector = Selector.open(); serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.socket().bind(new InetSocketAddress(port), 1024); serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT); System.out.println("Server start, port :" + port); } catch (IOException e) { e.printStackTrace(); System.exit(1); } } @Override public void run() { while(true){ try { selector.select(); Iterator iterator = selector.selectedKeys().iterator(); SelectionKey key = null; while(iterator.hasNext()) { key = iterator.next(); iterator.remove(); try { handleEvent(key); } catch (IOException e) { if (key != null) { key.cancel(); if (key.channel() != null) { key.channel().close(); } } } } } catch (IOException e) { e.printStackTrace(); } } } private void handleEvent(SelectionKey key) throws IOException { if (key.isValid()) { if (key.isAcceptable()) { handleAccept(key); } if (key.isReadable()) { handleRead(key); } } } private void handleAccept(SelectionKey key) throws IOException { System.out.println("connect accept,time" + System.currentTimeMillis()); ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); SocketChannel sc = ssc.accept(); sc.configureBlocking(false); sc.register(this.selector, SelectionKey.OP_READ); } private void handleRead(SelectionKey key) throws IOException { SocketChannel sc = (SocketChannel) key.channel(); ByteBuffer readBuffer = ByteBuffer.allocate(1024); int readBytes = sc.read(readBuffer); if (readBytes > 0) { readBuffer.flip(); byte[] bytes = new byte[readBuffer.remaining()]; readBuffer.get(bytes); String body = new String(bytes).trim(); System.out.println("server recive body:" + body); String response = "currentTime=" + new Date().toString(); doWrite(sc, response); } else if (readBytes < 0) { key.cancel(); sc.close(); } } private void doWrite(SocketChannel socketChannel, Strin response) throws IOException { byte[] bytes = response.getBytes(); ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length); writeBuffer.put(bytes); writeBuffer.flip(); socketChannel.write(writeBuffer); if (!writeBuffer.hasRemaining()) { System.out.println("response 2 client succeed:" + response); } }
让我们使用strace命令,来看看Nio Server端的代码工作中内核的系统调用过程:
服务器版本: CentOS Linux release 7.6.1810
strace: 跟踪到一个进程产生的系统调用,包括参数,返回值,执行消耗的时间;
strace命令参数:
-ff 如果提供-o filename,则所有进程的跟踪结果输出到相应的filename.pid中,pid是各进程的进程号.
-o filename 将strace的输出写入文件filename
推荐使用man命令自行查阅;
1. 执行strace命令,启动NioServer
# ~/kernelout/ss是系统调用过程输出的文件路径~$ strace -ff -o kernelout/ss java NioServerServer start, port :8765
可以看到输出了Server start, port :8765,说NioServer启动成功;
2. 打开新的终端,进入~/kernelout/,查看输出文件内容
~$ cd kernelout~/kernelout$ ll-rw-r--r-- 1 user00 user00 9472 Jun 29 11:15 ss.23922-rw-r--r-- 1 user00 user00 219262 Jun 29 11:15 ss.23923-rw-r--r-- 1 user00 user00 863 Jun 29 11:15 ss.23924-rw-r--r-- 1 user00 user00 961 Jun 29 11:15 ss.23925-rw-r--r-- 1 user00 user00 25170 Jun 29 11:17 ss.23926-rw-r--r-- 1 user00 user00 1095 Jun 29 11:15 ss.23927-rw-r--r-- 1 user00 user00 1081 Jun 29 11:15 ss.23928-rw-r--r-- 1 user00 user00 942 Jun 29 11:15 ss.23929-rw-r--r-- 1 user00 user00 12646 Jun 29 11:17 ss.23930-rw-r--r-- 1 user00 user00 11210 Jun 29 11:17 ss.23931-rw-r--r-- 1 user00 user00 942 Jun 29 11:15 ss.23932-rw-r--r-- 1 user00 user00 483374 Jun 29 11:17 ss.23933-rw-r--r-- 1 user00 user00 1562 Jun 29 11:15 ss.23934
可以看到有很多ss文件,那首先看哪个文件呢?
3. 找到java NioServer 主进程的PID
~/kernelout$ ps -ef | grep javauser00 23920 4120 0 11:15 pts/0 00:00:01 strace -ff -o kernel/ss java NioServeruser00 23922 23920 0 11:15 pts/0 00:00:00 java NioServer
可以看到
java NioServer
命令启动的java进程PID是23922,所以,我们首先看下ss.23922这个文件。
根据man strace: -o filename Write the trace output to the file filename rather than to stderr. Use filename.pid if -ff is used.
4. ss.23922文件内容
clone(child_stack=0x7f5f40316fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f5f403179d0, tls=0x7f5f40317700, child_tidptr=0x7f5f403179d0) = 23923
还记得我们的java代码里,启动了一个新线程NioServer-001么,这里:
new Thread(new NioServer(8765), "NioServer-001").start();
其实就对应着上面ss.23922文件中的clone()函数,并返回线程id23923;
5. 接下来看ss.23923文件
java代码里,有一个绑定ip,port的方法
serverSocketChannel.socket().bind(new InetSocketAddress(port), 1024);
我们用端口号8765去文件ss.23923中搜索,可以发现:
bind(9, {sa_family=AF_INET, sin_port=htons(8765), sin_addr=inet_addr("0.0.0.0")}, 16) = 0listen(9, 1024)
bind函数的第一个参数是socket file description,那这个socket是什么时候创建的呢,我们继续在ss.23923查找后发现:
# ipv6socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 6# ipv4socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 9
很明显,因为我们绑定的是ipv4的ip,所以bind()函数里的socketfd就是9;
并且listen()函数说明开启里对fd=9的socket的监听;
我们也能看到epoll系列函数的出现:
epoll_create(256) = 8
它其实就对应这行Java代码,表示创建一个多路复用器
selector = Selector.open();
接下来,ss.23923文件里,似乎除了创建线程,也没做其他什么值得我们关注的事情了,我们将目光转向到其他ss.pid文件;
6. ss.23934
我们在ss.23934文件中,又发现了epoll系列函数 ;为什么到目前为止,除了ss.23923外,只有这一个文件出现了epoll系列,这个任务委派关系是如何建立的,其他线程又是用来做什么的;这些问题,我还在学习中;
epoll_ctl(8, EPOLL_CTL_ADD, 9, {EPOLLIN, {u32=9, u64=4538492564453457929}}) = 0epoll_wait(8,
这里epoll_ctl函数表示将fd为9的socket的读事件注册到fd为8的多路复用器上,而epoll_wait函数用来阻塞等待事件发生;就对应下面两行Java代码
serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT);selector.select();
7. 打开新的终端,使用nc命令,与NioServer建立连接
~$ nc 127.0.0.1 8765
有打印内容connect accept,time1593401891111,表示client与Server已建立好了连接;
~$ strace -ff -o kernelout/ss java NioServerServer start, port :8765connect accept,time1593401891111
这时,我们再去看ss.23934文件
epoll_wait(8, [{EPOLLIN, {u32=9, u64=4538492564453457929}}], 8192, -1) = 1accept(9, {sa_family=AF_INET, sin_port=htons(35744), sin_addr=inet_addr("127.0.0.1")}, [16]) = 11epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN, {u32=11, u64=4521727116663848971}}) = 0epoll_wait(8,
发现之前的epoll_wait函数有返回了,就是因为有客户端连接进来了,然后accept函数接收这个连接,生成了另一个fd为11的socket;然后就将fd为11的socket的读事件注册到fd为8的多路复用器上,然后继续阻塞等待事件发生
对应Java代码
SocketChannel sc = ssc.accept();sc.register(this.selector, SelectionKey.OP_READ);
8. client发送消息
在client端随便输入些内容,传输给NIOServer;我们能在ss.23934文件看到,通过read函数读取fd为11的scoket的内容,即client发送的内容;并写回一些内容;
read(11, "dfsdfsf\n", 1024) = 8write(1, "server recive body:sdfsdf", 25) = 25write(1, "\n", 1) = 1epoll_wait(8,
这里的read、write函数,对应我们的Java代码
int readBytes = sc.read(readBuffer); socketChannel.write(writeBuffer);
从上面的操作步骤,我们完成了一次client与server端的网络通信,大致了解了整个通信的过程中涉及到一些内核系统调用;有兴趣到同学可以自行操作下。