socket编程
客户端与服务端创建tcp连接后,相互发送数据,如果数据量很大,会被分为多个报文段进行发送,报文段的大小可以通过ifconfig命令查看
图中的MTU的值表示1500个字节,说明这个网卡的一个报文段大小是1500个字节。
但是一个报文一个报文的发送速度太慢了,所以tcp有一个流量控制机制,可以一次性发送多个报文,只要服务端有能力接收。
linux内核在创建tcp连接后,会为客户端和服务端申请资源,其中包括存放数据的缓存,这个可以被称为窗口。客户端和服务端刚建立tcp连接时,会在报文中发送各自的空闲窗口大小。服务端发送空闲窗口大小是为了告诉客户端,此时可以接收多少数据,客户端会据此发送不会超过这个大小的数据。这个数据往往比一个报文段要大。这就是流量控制机制。
如果服务端的缓存已经满了,会向客户端发送空闲窗口大小为0,此时客户端会堵塞不发数据。直到服务端的进程处理了数据,缓存中有了多余的空间,服务端会向客户端发送一条报文,告诉客户端此时的空闲窗口大小,客户端再发送数据。
图中的win字段值就是空闲窗口大小。
socket编程的常见参数
ReceiveBufferSize:
服务端的接收缓存大小
SendBufferSize:
客户端的发送缓存大小
SoTimeout:
接收数据的超时时间
TcpNoDelay:
参数设置为true,表示不进行延迟发送,内核拿到数据就发送;设置为false,表示延迟发送,内核积攒数据到一定量后才会发送给服务端
KeepAlive:
参数设置为true,表示长连接,客户端和服务端长时间不传输数据,为了确保双方还是正常的,会定时发送保活报文;设置为false,表示短连接,超过一定时间不发送数据,连接就会断开
socket的系统调用
BIO的系统调用
public static void main(String[] args) throws Exception {
// 这一步会调用内核的三个方法,socket、bind、listen,并且生成一个文件描述符fd1
// 此时服务端已经绑定了IP和端口号,等待客户端来建立连接
// 如果客户端来建立了连接,那么会另外生成一个文件描述符fd2。fd2表示已经建立好的连接
ServerSocket server = new ServerSocket(9090,20);
System.out.println("step1: new ServerSocket(9090) ");
while (true) {
// 这一步会调用内核的accept方法,拿到已经建立好的连接的文件描述符fd2
// 如果调用accept方法时,还未建立好连接,就会在此处堵塞
Socket client = server.accept();
System.out.println("step2:client\t" + client.getPort());
new Thread(new Runnable(){
public void run() {
InputStream in = null;
try {
in = client.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
while(true){
// 这一步会调用内核的recv方法,读取数据,如果没有数据就会堵塞
String dataline = reader.readLine();
if(null != dataline){
System.out.println(dataline);
}else{
client.close();
break;
}
}
System.out.println("客户端断开");
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
socket在建立连接前后会生成两个文件描述符。第一个文件描述符是等待客户端的连接过来;第二个文件描述符是建立连接后生成的,通过这个文件描述符可以读写数据。
BIO效率低的原因是accept方法和read方法在调用内核提供的方法都是堵塞的,所以每一个连接过来都需要启动一个线程去处理,线程的启动是需要花费时间的,所以效率低。
linux系统内核后来也提供了不会堵塞的方法,java中的nio包中提供的网络编程语法就调用了这些方法,比BIO的效率要高
NIO的系统调用
public static void main(String[] args) throws Exception {
LinkedList<SocketChannel> clients = new LinkedList<>();
//创建服务端的socket
ServerSocketChannel ss = ServerSocketChannel.open();
ss.bind(new InetSocketAddress(9090));
//将ServerSocketChannel设置调用内核中非堵塞的方法
ss.configureBlocking(false);
while (true) {
//系统调用的还是accept方法,但是不会堵塞
SocketChannel client = ss.accept();
if (client == null) {
//当还没有建立连接,内核的accept方法返回-1,java中的accept方法返回null
System.out.println("null.....");
} else {
//将SocketChannel设置调用内核中非堵塞的方法
//read方法就不会堵塞了
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();
}
}
}
}
由于nio调用的内核方法都是不会堵塞的,所以一个线程就可以处理多个io连接。相比bio,节省了创建线程的过程,效率更高。
多路复用器
nio解决了bio的堵塞问题,一个线程就可以处理多个io,节约资源。但是,nio依然存在问题,假如进程有1万个io,一个线程每次都需要遍历这1万个io,而且,实际情况是,每次遍历只有3~4个io是有数据的,其他都是无效遍历。每次遍历都会进行一次用户态和内核态的切换,很浪费资源。这就是nio的缺点。
多路复用器模型提升了nio的效率。用户程序只需要调用一次内核函数,将需要遍历的文件描述符给内核,内核中的多路复用器会遍历这些文件描述符,将准备就绪的io返回,如此一来,应用程序再遍历这些有效的io,提高效率。
在linux系统中,SELECT、POLL、EPOLL都是多路复用器模型
SELECT和POLL
SELECT和POLL非常的相似,它们都是让应用程序先进行一次系统调用,将所有的文件描述符发送给内核,由内核遍历得到准备好的文件描述符返回给应用程序,应用程序再遍历这些有效的文件描述符进行读写。
SELECT和POLL的区别是,SELECT对一次遍历的文件描述符个数是有限制的(好像是1024个),而POLL是没有限制的。
SELECT和POLL依然存在问题,因为每次都需要应用程序将所有的文件描述符发送给内核,内核自己不会保存,而且每次应用程序进行一次系统调用,内核都需要遍历所有的文件描述符,这就比较浪费效率和空间。
EPOLL
EPOLL是在网卡进行中断时,cpu执行中断函数执行一些操作,解决了SELECT和POLL存在的问题。
我们知道当网卡接收到数据后,会给cpu发送中断,cpu会响应中断,执行中断函数,将网卡缓存中的数据迁移到对应的文件描述符的缓存区中,方便应用程序来读取。
EPOLL是内核首先开辟了一块空间,这个空间中有红黑树和链表。红黑树存储了应用程序的所有文件描述符。当网卡接收到数据,向cpu发送中断,cpu响应中断开始迁移网卡缓存中的数据时,内核会将对应的文件描述符从红黑树中复制一份,放到空间的链表中,所以链表里存的是准备就绪的文件描述符。如此一来,应用程序进行一次系统调用,将文件描述符发送给内核后就不用重复发送了,而且可以得到准备就绪的文件描述符进行读写操作了,效率比SELECT和POLL有所提升。
epoll模型中有三个系统调用,分别是epoll_create,epoll_ctl,epoll_wait。
首先应用程序调用epoll_create,在内核中创建一块空间以及红黑树和链表。然后调用epoll_ctl,将文件描述符传递给内核,由内核存入红黑树中。如果红黑树中的文件描述符的状态发生改变了,内核会复制一份放到链表中。如此一来,应用程序调用epoll_wait,就可以拿到链表中准备就绪的文件描述符进行操作了。
内核创建了一块空间用于存储应用程序的文件描述符,以至于应用程序不用每次传递所有的文件描述符了。还有,当网卡接收到数据后,内核会将对应的文件描述符复制一份到链表中,如此一来,应用程序调用epoll_wait,内核不用遍历红黑树中的文件描述符,判断哪些是准备就绪的,直接将链表中的返回即可。epoll的这种机制解决了select和poll存在的问题。
java编程实战
使用多路复用器模型的代码如下:
public class SocketMultiplexingSingleThreadv1 {
private ServerSocketChannel server = null;
//多路复用器对象
//linux内核提供了三种多路复用器,select、poll和epoll,默认情况下使用的是epoll
//可以通过启动参数:-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider进行修改
private Selector selector = null;
int port = 9090;
public void initServer() {
try {
//生成监听的fd
server = ServerSocketChannel.open();
//设置成非阻塞
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
//调用open方法
//在select和poll模型中,相当于啥也没做
//在epoll模型中,会调用epoll_create方法,在内核中开辟空间
selector = Selector.open();
//调用register方法
//在select和poll模型中,相当于将监听fd放到JVM的一个集合中
//在epoll模型中,会调用epoll_ctl方法,将监听fd放到内核空间的红黑树中
server.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
initServer();
System.out.println("服务器启动了。。。。。");
try {
while (true) { //死循环
//可以得到多路复用器中有多少个fd
Set<SelectionKey> keys = selector.keys();
System.out.println(keys.size()+" size");
//调用select方法
//在select和poll模型中,相当于将JVM中的fd集合传给内核,返回准备就绪的fd集合
//在epoll模型中,会调用epoll_wait方法,返回准备就绪的fd集合
//此方法返回了准备就绪fd集合的元素个数
while (selector.select() > 0) {
//返回准备就绪的fd集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectionKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
//删除原集合中的元素
iter.remove();
if (key.isAcceptable()) {
//进入这里说明有客户端过来建立了连接
//acceptHandler方法会将建立了连接的fd放到select、poll的JVM集合中,或者是epoll的内核红黑树中
acceptHandler(key);
} else if (key.isReadable()) {
//进入这里说明有客户端发送数据过来
readHandler(key);
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void acceptHandler(SelectionKey key) {
try {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//进入这个方法,说明accept方法一定有返回值
SocketChannel client = ssc.accept();
client.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(8192);
//将建立了连接的fd放到select、poll的JVM集合中,或者是epoll的内核红黑树中
//而且这个fd规定了是读事件
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);
}
buffer.clear();
} else if (read == 0) {
break;
} else {
client.close();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
tcp四次挥手状态分析
tcp建立连接需要三次握手,断开连接需要四次挥手,握手和挥手的过程都是内核之间的交互。在四次挥手过程中,客户端与服务端有状态的变化,例如:CLOSE_WAIT,FIN_WAIT1,FIN_WAIT2,TIME_WAIT,LAST_ACK,CLOST等
客户端与服务端刚建立连接时,双方都处于ESTABLISHED状态,此时可以发送报文通讯。
当客户端想断开连接,会发送FIN报文给服务端,此时客户端会处于FIN_WAIT1状态,服务端接收到FIN报文后,会响应一个FIN+ACK报文,服务端会变成CLOST_WAIT状态。客户端接收到报文后会变成FIN_WAIT2状态。
此时,服务端的内核不会立即向客户端发送FIN报文,表示我也要断开连接,因为内核需要等应用程序将客户端发送过来的数据处理完成,等待应用程序调用close方法。如果程序员在代码中忘记写调用close方法关闭连接,那么客户端和服务端就一直处于这种状态,内核中的socket四元组就一直被占用,浪费资源。
所以等应用程序调用了close方法,告诉内核可以断开连接了,服务端内核就会向客户端发送FIN报文,并且自身状态变成LAST_ACK。客户端接收到报文后,会向服务端发送ACK报文,并且自身变成TIME_WAIT状态。
客户端的TIME_WAIT状态会持续报文最大存活时间的2倍的时间,因为客户端发送了ACK报文,无法确保服务端一定可以接收到。如果服务端没有接收到,服务端会重新发送FIN报文,此时客户端的资源没有被清除,就可以正常处理这个报文,给服务端再发送ACK,以确保四次挥手过程的完整性。
最后,客户端与服务端都完成四次挥手的过程,双方都会经历非常短暂的CLOSE状态。
使用多路复用器完成监听连接、读数据和写数据
public class SocketMultiplexingSingleThreadv1_1 {
private ServerSocketChannel server = null;
private Selector selector = null;
int port = 9090;
public void initServer() {
try {
// 完成socket四元组的一半,等待客户端来连接,构建出完整的四元组
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
// 内核中开辟空间
selector = Selector.open();
// 监听连接事件发送给内核
server.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
initServer();
System.out.println("服务器启动了。。。。。");
try {
while (true) {
// 获得准备就绪的连接,如果没有准备就绪的,线程会堵塞在这里
// select方法还可以传入时间参数,表示只会堵塞一段时间,时间过后就会放行
while (selector.select() > 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()) {
// 有读事件
// key.cancel(); // 将此fd从内核的红黑树中删除
readHandler(key);
} else if(key.isWritable()){
// 有写事件
// 只要将写事件添加到内核空间中,就会一直触发
// 除非内核中的send_queue没有剩余空间了,就不会触发写事件
// key.cancel(); // 将此fd从内核的红黑树中删除
writeHandler(key);
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void writeHandler(SelectionKey key) {
System.out.println("write handler...");
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.flip();
while (buffer.hasRemaining()) {
try {
client.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
buffer.clear();
key.cancel();
try {
client.close();
} 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);
// 添加一个读fd
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) {
System.out.println("read handler.....");
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) {
client.register(key.selector(),SelectionKey.OP_WRITE,buffer);
//关心 OP_WRITE 其实就是关心send-queue是不是有空间
} else if (read == 0) {
break;
} else {
client.close();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SocketMultiplexingSingleThreadv1_1 service = new SocketMultiplexingSingleThreadv1_1();
service.start();
}
}
多线程的多路复用器
单线程的多路复用器需要遍历内核返回的fd集合,对每个fd进行读写操作。其实可以使用多线程,当进行读或者写操作时,可以另起一个线程去处理。如此一来,集合中的fd事件就是并行执行的,效率会高一些。
代码如下:
public class SocketMultiplexingSingleThreadv2 {
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();
server.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
initServer();
System.out.println("服务器启动了。。。。。");
try {
while (true) {
while (selector.select(50) > 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()) {
//key.cancel();
System.out.println("in.....");
//key.interestOps(key.interestOps() | ~SelectionKey.OP_READ);
readHandler(key);
} else if(key.isWritable()){
//key.cancel();
//key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
writeHandler(key);
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void writeHandler(SelectionKey key) {
new Thread(()->{
System.out.println("write handler...");
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.flip();
while (buffer.hasRemaining()) {
try {
client.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
buffer.clear();
// key.cancel();
// try {
//// client.shutdownOutput();
// client.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
}).start();
}
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) {
new Thread(()->{
System.out.println("read handler.....");
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear();
int read = 0;
try {
while (true) {
read = client.read(buffer);
System.out.println(Thread.currentThread().getName()+ " " + read);
if (read > 0) {
key.interestOps(SelectionKey.OP_READ);
client.register(key.selector(),SelectionKey.OP_WRITE,buffer);
} else if (read == 0) {
break;
} else {
client.close();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
public static void main(String[] args) {
SocketMultiplexingSingleThreadv2 service = new SocketMultiplexingSingleThreadv2();
service.start();
}
}
代码中,另起了一个线程去执行accept事件、read事件和write事件的处理逻辑,以提高io效率。但是,代码执行后会发现一个问题,同一个read和write事件频繁被触发,导致会生成多个线程去执行同一个读写事件。
这个问题出现的原因是,另起一个线程去处理读写事件,而主线程依然循环遍历内核返回的fd集合,主线程发现同一读写事件的fd也在集合中,就会将其再次触发。
为什么已经在处理的读写事件的fd还在集合中呢?因为他们没有处理结束。对于读事件,只要内核的recv_queue中有数据,那么这个读事件的fd就还在内核空间的链表中,会被内核返回。对于写事件,只要内核的send_queue有剩余空闲,那么这个写事件的fd就还在内核空间的链表中,会被内核返回。
所以,为了使用多线程提高io效率,也要避免这种情况发生,需要对多线程的多路复用器代码进行优化。
多线程的多路复用器优化(register和select方法相互阻塞)
为了避免同一个读写事件重复处理的情况,我们让每个线程都独立拥有自己的多路复用器。如此一来,每个多路复用器就只有一个线程在循环处理集合中的事件,不会出现问题了。
SelectorThreadv1:
这个类是一个线程,拥有自己的多路复用器,会遍历多路复用器中的fd进行操作
public class SelectorThreadv1 implements Runnable{
Selector selector = null;
SelectorThreadGroupv1 stg;
SelectorThreadv1(SelectorThreadGroupv1 stg){
try {
this.stg = stg;
selector = Selector.open();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
//Loop
while (true){
try {
// 堵塞在这里,select方法会导致register方法也无法执行下去
int nums = selector.select();
if(nums>0){
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while(iter.hasNext()){
SelectionKey key = iter.next();
iter.remove();
if(key.isAcceptable()){
}else if(key.isReadable()){
}else if(key.isWritable()){
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
SelectorThreadv1Group:
这个类是一个存放SelectorThreadv1对象的集合类
public class SelectorThreadGroupv1 {
// 存放SelectorThreadv1的数组
SelectorThreadv1[] sts;
ServerSocketChannel server=null;
AtomicInteger xid = new AtomicInteger(0);
// 构造函数,入参表示绑定多路复用器线程的个数
SelectorThreadGroupv1(int num){
sts = new SelectorThreadv1[num];
for (int i = 0; i < num; i++) {
// 生成绑定多路复用器线程
sts[i] = new SelectorThreadv1(this);
// 启动线程
new Thread(sts[i]).start();
}
}
public void bind(int port) {
try {
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
// 将监听连接fd放到某个多路复用器中
nextSelector(server);
} catch (IOException e) {
e.printStackTrace();
}
}
public void nextSelector(Channel c) {
// 随机选择一个多路复用器
SelectorThreadv1 st = next();
ServerSocketChannel s = (ServerSocketChannel) c;
try {
// 将accept放到选择的多路复用器中,这里存在一个问题
// 由于在SelectorThreadGroupv1的构造函数中,已经启动了线程
// 启动的线程必然会调用select方法,并且在那里堵塞
// 此处的线程又调用了register方法
// register方法是会和select方法相互阻塞的
// 因为register方法要访问内核中的空间,select也需要访问,内核可能对多线程访问这个空间加了锁类似的限制
s.register(st.selector, SelectionKey.OP_ACCEPT);
st.selector.wakeup();
} catch (ClosedChannelException e) {
e.printStackTrace();
}
}
private SelectorThreadv1 next() {
int index = xid.incrementAndGet() % sts.length;
return sts[index];
}
public static void main(String[] args) {
// 声明一个SelectorThreadGroupv1对象
// 声明一个SelectorThreadGroupv1中会存放SelectorThreadv1对象
// SelectorThreadv1对象是一个线程,线程中包含了自己的Selector
SelectorThreadGroupv1 stg = new SelectorThreadGroupv1(2);
// 绑定9999端口号,创建一个监听连接fd
stg.bind(9999);
}
}
上面的代码想要多线程处理自己的多路复用器中的io事件,但是,有个坑就是register方法居然会与select方法相互阻塞,导致想要往多路复用器中放入io事件是不可能的了。所以,需要优化代码,解决这个问题。
多线程的多路复用器优化(解决register和select方法相互阻塞)
SelectorThreadv1中增加一个阻塞队列,专门存放将要放到多路复用器中的io事件
public class SelectorThreadv1 implements Runnable{
Selector selector = null;
SelectorThreadGroupv1 stg;
LinkedBlockingQueue<Channel> lbq = new LinkedBlockingQueue<>();
SelectorThreadv1(SelectorThreadGroupv1 stg){
try {
this.stg = stg;
selector = Selector.open();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
//Loop
while (true){
try {
// 堵塞在这里,select方法会导致register方法也无法执行下去
int nums = selector.select();
if(nums>0){
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while(iter.hasNext()){
SelectionKey key = iter.next();
iter.remove();
if(key.isAcceptable()){
}else if(key.isReadable()){
}else if(key.isWritable()){
}
}
}
if(!lbq.isEmpty()){
// 从队列中取出Channel,准备将io事件放到多路复用器中
Channel c = lbq.take();
if(c instanceof ServerSocketChannel){
ServerSocketChannel server = (ServerSocketChannel) c;
server.register(selector,SelectionKey.OP_ACCEPT);
}else if(c instanceof SocketChannel){
SocketChannel client = (SocketChannel) c;
ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
client.register(selector, SelectionKey.OP_READ, buffer);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
SelectorThreadGroupv1不会直接在nextSelector方法中用当前线程将io事件放到多路复用器中,因为当前线程与多路复用器绑定的线程之间没有通信,所以当前线程不知道其是否正在执行select方法。所以,当前线程将这件事交给与多路复用器绑定的线程去做,就不会有问题了。
public class SelectorThreadGroupv1 {
// 存放SelectorThreadv1的数组
SelectorThreadv1[] sts;
ServerSocketChannel server=null;
AtomicInteger xid = new AtomicInteger(0);
// 构造函数,入参表示绑定多路复用器线程的个数
SelectorThreadGroupv1(int num){
sts = new SelectorThreadv1[num];
for (int i = 0; i < num; i++) {
// 生成绑定多路复用器线程
sts[i] = new SelectorThreadv1(this);
// 启动线程
new Thread(sts[i]).start();
}
}
public void bind(int port) {
try {
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
// 将监听连接fd放到某个多路复用器中
nextSelector(server);
} catch (IOException e) {
e.printStackTrace();
}
}
public void nextSelector(Channel c) {
// 随机选择一个多路复用器
SelectorThreadv1 st = next();
ServerSocketChannel s = (ServerSocketChannel) c;
// 将Channel放入阻塞队列中,等待线程去将io事件放到多路复用器中
st.lbq.add(c);
// 立刻唤醒阻塞的线程,让其处理队列中的Channel
st.selector.wakeup();
}
private SelectorThreadv1 next() {
int index = xid.incrementAndGet() % sts.length;
return sts[index];
}
public static void main(String[] args) {
// 声明一个SelectorThreadGroupv1对象
// 声明一个SelectorThreadGroupv1中会存放SelectorThreadv1对象
// SelectorThreadv1对象是一个线程,线程中包含了自己的Selector
SelectorThreadGroupv1 stg = new SelectorThreadGroupv1(2);
// 绑定9999端口号,创建一个监听连接fd
stg.bind(9999);
}
}
多线程的多路复用器进一步优化
对于io事件,accept与read和write不同,只要监听端口,accept就一直存在内核中,而且可以被多个客户端建立连接。如果监听的端口多,accept也是不止一个。客户端建立连接后,accept也没有什么业务代码要执行,只需要将read事件注册到多路复用器就可以了。
所以,进一步的代码优化是将accept事件专门用一个多路复用器处理。由于一个进程可能开启多个端口监听,每个端口可能会有多个客户端来建立连接。我们就每个端口给一个多路复用器。这样就和read已经write事件解耦了。
代码:
MainThread:
public class MainThread {
public static void main(String[] args) {
// 专门存放accept的多路复用器组
// 有多少个监听端口,组的容量就是多少
SelectorThreadGroup boss = new SelectorThreadGroup(3);
// 专门存放read和write的多路复用器组
SelectorThreadGroup worker = new SelectorThreadGroup(3);
// 因为boss组中多路复用器的accept触发后,需要将read事件注册到worker多路复用器组中
// 所以boss组中需要有worker组
boss.setWorker(worker);
boss.bind(9999);
boss.bind(8888);
boss.bind(7777);
}
}
SelectorThreadGroup:
/**
* SelectorThreadGroup对象分boss和worker
* boss专门负责处理accept
* worker专门负责处理read和write
*/
public class SelectorThreadGroup {
// 绑定多路复用器的线程集合
SelectorThread[] sts;
ServerSocketChannel server = null;
AtomicInteger xid = new AtomicInteger(0);
// 存放read和write事件的多路复用器组(worker)
// boss组中需要有worker组,因为绑定端口时,需要将accept事件注册到worker组的某个多路复用器上
SelectorThreadGroup stg = this;
// 给worker多路复用器组赋值
public void setWorker(SelectorThreadGroup stg) {
this.stg = stg;
// 给只处理accept事件的线程中的多路复用器组赋值为worker
// 因为这些线程在处理完accept事件后,需要将read/write事件注册到多路复用器中
// 而read/write事件是由worker多路复用器组处理的
// 所以需要在每个boss线程中存一份worker组
for (SelectorThread st : sts) {
st.setWorker(stg);
}
}
SelectorThreadGroup(int num) {
sts = new SelectorThread[num];
// 初始化组中的每个线程
for (int i = 0; i < num; i++) {
// 对于boss组中的线程,以下代码是将boss组赋值给了线程中的SelectorThreadGroup成员
// 对于worker组中的线程,以下代码是将worker组赋值给了线程中的SelectorThreadGroup成员
// 不过对于boss组中的线程,在boss组对象调用setWorker方法后,会将SelectorThreadGroup成员从boss组换成worker组
sts[i] = new SelectorThread(this);
new Thread(sts[i]).start();
}
}
public void bind(int port) {
try {
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
nextSelector(server);
} catch (IOException e) {
e.printStackTrace();
}
}
public void nextSelector(Channel c) {
try {
if (c instanceof ServerSocketChannel) {
// 进入这里说明是accept
SelectorThread st = nextBossThread();
st.lbq.put(c);
st.selector.wakeup();
} else {
// 进入这里说明是read/write
SelectorThread st = nextWorkerThread();
st.lbq.add(c);
st.selector.wakeup();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private SelectorThread nextBossThread() {
// 从boss组中选择一个线程
int index = xid.incrementAndGet() % sts.length;
return sts[index];
}
private SelectorThread nextWorkerThread() {
// 从worker组中选择一个线程
int index = xid.incrementAndGet() % stg.sts.length;
return stg.sts[index];
}
}
boss和worker的SelectorThreadGroup模型:
SelectorThread:
/**
* SelectorThread对象分boss和worker
* boss的SelectorThread对象只处理accept
* worker的SelectorThread对象只处理read和write
*/
public class SelectorThread extends ThreadLocal<LinkedBlockingQueue<Channel>> implements Runnable {
Selector selector = null;
// 从线程自己的ThreadLocalMap中获取到LinkedBlockingQueue<Channel>对象
LinkedBlockingQueue<Channel> lbq = get();
// 无论是boss组还是worker组的线程,这里都是worker组对象
SelectorThreadGroup stg;
@Override
protected LinkedBlockingQueue<Channel> initialValue() {
// 继承ThreadLocal类
// 将LinkedBlockingQueue<Channel>对象存到线程自己的ThreadLocalMap中,避免并发的情况
return new LinkedBlockingQueue<>();
}
SelectorThread(SelectorThreadGroup stg) {
try {
// 线程在初始化的时候,对SelectorThreadGroup对象赋值
// boss组线程赋boss组,worker组线程赋worker组
// 不过boss组会在调用setWorker方法后,将boss组换成worker组
this.stg = stg;
// 开启多路复用器
selector = Selector.open();
} catch (IOException e) {
e.printStackTrace();
}
}
// 设置线程的SelectorThreadGroup对象
public void setWorker(SelectorThreadGroup stgWorker) {
this.stg = stgWorker;
}
@Override
public void run() {
//死循环
while (true) {
try {
// 没有io事件发生时,这里会堵塞
int nums = selector.select();
// 到这里有两种情况
// 第一种:有io事件发生
// 第二种:有线程调用了wakeup方法
if (nums > 0) {
// 进入这里说明有io事件发生
// 获得io事件集合
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
// 遍历集合
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
// accept连接事件发生
acceptHandler(key);
} else if (key.isReadable()) {
// read事件发生
readHander(key);
} else if (key.isWritable()) {
// write事件发生
}
}
}
// 如果nums<=0,那说明是有线程调用了wakeup方法
if (!lbq.isEmpty()) {
// 进入这里说明有新的io事件要注册到多路复用器了
Channel c = lbq.take();
if (c instanceof ServerSocketChannel) {
// 进入这里说明是accept事件要注册
ServerSocketChannel server = (ServerSocketChannel) c;
server.register(selector, SelectionKey.OP_ACCEPT);
System.out.println(Thread.currentThread().getName() + " register listen");
} else if (c instanceof SocketChannel) {
// 进入这里说明是read/write事件要注册
SocketChannel client = (SocketChannel) c;
ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
client.register(selector, SelectionKey.OP_READ, buffer);
System.out.println(Thread.currentThread().getName() + " register client: " + client.getRemoteAddress());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 处理read事件
private void readHander(SelectionKey key) {
System.out.println(Thread.currentThread().getName() + " read......");
ByteBuffer buffer = (ByteBuffer) key.attachment();
SocketChannel client = (SocketChannel) key.channel();
buffer.clear();
while (true) {
try {
int num = client.read(buffer);
if (num > 0) {
buffer.flip(); //将读到的内容翻转,然后直接写出
while (buffer.hasRemaining()) {
client.write(buffer);
}
buffer.clear();
} else if (num == 0) {
break;
} else if (num < 0) {
//客户端断开了
System.out.println("client: " + client.getRemoteAddress() + "closed......");
key.cancel();
break;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 处理write事件
private void acceptHandler(SelectionKey key) {
System.out.println(Thread.currentThread().getName() + " acceptHandler......");
ServerSocketChannel server = (ServerSocketChannel) key.channel();
try {
SocketChannel client = server.accept();
client.configureBlocking(false);
stg.nextSelector(client);
} catch (IOException e) {
e.printStackTrace();
}
}
}
boss和worker的SelectoreThread模型: